#nullable enable using System.Diagnostics; namespace Terminal.Gui; public partial class View // Drawing APIs { private ColorScheme? _colorScheme; /// The color scheme for this view, if it is not defined, it returns the 's color scheme. public virtual ColorScheme? ColorScheme { get { if (_colorScheme is null) { return SuperView?.ColorScheme; } return _colorScheme; } set { if (_colorScheme != value) { _colorScheme = value; if (Border is { } && Border.LineStyle != LineStyle.None && Border.ColorScheme is { }) { Border.ColorScheme = _colorScheme; } SetNeedsDisplay (); } } } /// The canvas that any line drawing that is to be shared by subviews of this view should add lines to. /// adds border lines to this LineCanvas. public LineCanvas LineCanvas { get; } = new (); /// /// Gets or sets whether this View will use it's SuperView's for rendering any /// lines. If the rendering of any borders drawn by this Frame will be done by its parent's /// SuperView. If (the default) this View's method will be /// called to render the borders. /// public virtual bool SuperViewRendersLineCanvas { get; set; } = false; /// Draws the specified character in the specified viewport-relative column and row of the View. /// /// If the provided coordinates are outside the visible content area, this method does nothing. /// /// /// The top-left corner of the visible content area is ViewPort.Location. /// /// Column (viewport-relative). /// Row (viewport-relative). /// The Rune. public void AddRune (int col, int row, Rune rune) { if (Move (col, row)) { Driver?.AddRune (rune); } } /// Clears with the normal background. /// /// /// If has only /// the portion of the content /// area that is visible within the will be cleared. This is useful for views that have /// a /// content area larger than the Viewport (e.g. when is /// enabled) and want /// the area outside the content to be visually distinct. /// /// public void Clear () { if (Driver is null) { return; } // Get screen-relative coords Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); Rectangle prevClip = Driver.Clip; if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) { Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); toClear = Rectangle.Intersect (toClear, visibleContent); } Attribute prev = Driver.SetAttribute (GetNormalColor ()); Driver.FillRect (toClear); Driver.SetAttribute (prev); Driver.Clip = prevClip; } /// Fills the specified -relative rectangle with the specified color. /// The Viewport-relative rectangle to clear. /// The color to use to fill the rectangle. If not provided, the Normal background color will be used. public void FillRect (Rectangle rect, Color? color = null) { if (Driver is null) { return; } // Get screen-relative coords Rectangle toClear = ViewportToScreen (rect); Rectangle prevClip = Driver.Clip; Driver.Clip = Rectangle.Intersect (prevClip, ViewportToScreen (Viewport with { Location = new (0, 0) })); Attribute prev = Driver.SetAttribute (new (color ?? GetNormalColor ().Background)); Driver.FillRect (toClear); Driver.SetAttribute (prev); Driver.Clip = prevClip; } /// Sets the 's clip region to . /// /// /// By default, the clip rectangle is set to the intersection of the current clip region and the /// . This ensures that drawing is constrained to the viewport, but allows /// content to be drawn beyond the viewport. /// /// /// If has set, clipping will be /// applied to just the visible content area. /// /// /// /// The current screen-relative clip region, which can be then re-applied by setting /// . /// public Rectangle SetClip () { if (Driver is null) { return Rectangle.Empty; } Rectangle previous = Driver.Clip; // Clamp the Clip to the entire visible area Rectangle clip = Rectangle.Intersect (ViewportToScreen (Viewport with { Location = Point.Empty }), previous); if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly)) { // Clamp the Clip to the just content area that is within the viewport Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); clip = Rectangle.Intersect (clip, visibleContent); } Driver.Clip = clip; return previous; } /// /// Draws the view if it needs to be drawn. Causes the following virtual methods to be called (along with their related events): /// , . /// /// /// /// The view will only be drawn if it is visible, and has any of , , /// or set. /// /// /// Always use (view-relative) when calling , NOT /// (superview-relative). /// /// /// Views should set the color that they want to use on entry, as otherwise this will inherit the last color that /// was set globally on the driver. /// /// /// Overrides of must ensure they do not set Driver.Clip to a clip /// region larger than the property, as this will cause the driver to clip the entire /// region. /// /// public void Draw () { if (!CanBeVisible (this)) { return; } if (IsLayoutNeeded ()) { //Debug.WriteLine ($"Layout should be de-coupled from drawing: {this}"); } //// TODO: This ensures overlapped views are drawn correctly. However, this is inefficient. //// TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413 //if ((this != Application.Top || this is Toplevel { Modal: true }) && Arrangement.HasFlag (ViewArrangement.Overlapped)) //{ // SetNeedsDisplay (); //} if (!NeedsDisplay && !SubViewNeedsDisplay) { return; } OnDrawAdornments (); if (ColorScheme is { }) { //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ()); Driver?.SetAttribute (GetNormalColor ()); } // By default, we clip to the viewport preventing drawing outside the viewport // We also clip to the content, but if a developer wants to draw outside the viewport, they can do // so via settings. SetClip honors the ViewportSettings.DisableVisibleContentClipping flag. Rectangle prevClip = SetClip (); // Invoke DrawContentEvent var dev = new DrawEventArgs (Viewport, Rectangle.Empty); DrawContent?.Invoke (this, dev); if (!dev.Cancel) { OnDrawContent (Viewport); } if (Driver is { }) { Driver.Clip = prevClip; } OnRenderLineCanvas (); // TODO: This is a hack to force the border subviews to draw. if (Border?.Subviews is { }) { foreach (View view in Border.Subviews) { view.SetNeedsDisplay (); view.Draw (); } } // Invoke DrawContentCompleteEvent OnDrawContentComplete (Viewport); ClearNeedsDisplay (); } /// Event invoked when the content area of the View is to be drawn. /// /// Will be invoked before any subviews added with have been drawn. /// /// Rect provides the view-relative rectangle describing the currently visible viewport into the /// . /// /// public event EventHandler? DrawContent; /// Event invoked when the content area of the View is completed drawing. /// /// Will be invoked after any subviews removed with have been completed drawing. /// /// Rect provides the view-relative rectangle describing the currently visible viewport into the /// . /// /// public event EventHandler? DrawContentComplete; /// Utility function to draw strings that contain a hotkey. /// String to display, the hotkey specifier before a letter flags the next letter as the hotkey. /// Hot color. /// Normal color. /// /// /// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by /// default. /// /// The hotkey specifier can be changed via /// public void DrawHotString (string text, Attribute hotColor, Attribute normalColor) { Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier; Application.Driver?.SetAttribute (normalColor); foreach (Rune rune in text.EnumerateRunes ()) { if (rune == new Rune (hotkeySpec.Value)) { Application.Driver?.SetAttribute (hotColor); continue; } Application.Driver?.AddRune (rune); Application.Driver?.SetAttribute (normalColor); } } /// /// Utility function to draw strings that contains a hotkey using a and the "focused" /// state. /// /// String to display, the underscore before a letter flags the next letter as the hotkey. /// /// If set to this uses the focused colors from the color scheme, otherwise /// the regular ones. /// public void DrawHotString (string text, bool focused) { if (focused) { DrawHotString (text, GetHotFocusColor (), GetFocusColor ()); } else { DrawHotString ( text, Enabled ? GetHotNormalColor () : ColorScheme!.Disabled, Enabled ? GetNormalColor () : ColorScheme!.Disabled ); } } /// Determines the current based on the value. /// /// if is or /// if is . If it's /// overridden can return other values. /// public virtual Attribute GetFocusColor () { ColorScheme? cs = ColorScheme; if (cs is null) { cs = new (); } return Enabled ? GetColor (cs.Focus) : cs.Disabled; } /// Determines the current based on the value. /// /// if is or /// if is . If it's /// overridden can return other values. /// public virtual Attribute GetHotFocusColor () { ColorScheme? cs = ColorScheme ?? new (); return Enabled ? GetColor (cs.HotFocus) : cs.Disabled; } /// Determines the current based on the value. /// /// if is or /// if is . If it's /// overridden can return other values. /// public virtual Attribute GetHotNormalColor () { ColorScheme? cs = ColorScheme; if (cs is null) { cs = new (); } return Enabled ? GetColor (cs.HotNormal) : cs.Disabled; } /// Determines the current based on the value. /// /// if is or /// if is . If it's /// overridden can return other values. /// public virtual Attribute GetNormalColor () { ColorScheme? cs = ColorScheme; if (cs is null) { cs = new (); } Attribute disabled = new (cs.Disabled.Foreground, cs.Disabled.Background); if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) { disabled = new (disabled.Foreground.GetDarkerColor (), disabled.Background.GetDarkerColor ()); } return Enabled ? GetColor (cs.Normal) : disabled; } private Attribute GetColor (Attribute inputAttribute) { Attribute attr = inputAttribute; if (Diagnostics.HasFlag (ViewDiagnosticFlags.Hover) && _hovering) { attr = new (attr.Foreground.GetDarkerColor (), attr.Background.GetDarkerColor ()); } return attr; } /// Moves the drawing cursor to the specified -relative location in the view. /// /// /// If the provided coordinates are outside the visible content area, this method does nothing. /// /// /// The top-left corner of the visible content area is ViewPort.Location. /// /// /// Column (viewport-relative). /// Row (viewport-relative). public bool Move (int col, int row) { if (Driver is null || Driver?.Rows == 0) { return false; } if (col < 0 || row < 0 || col >= Viewport.Width || row >= Viewport.Height) { return false; } Point screen = ViewportToScreen (new Point (col, row)); Driver?.Move (screen.X, screen.Y); return true; } // TODO: Make this cancelable /// /// Prepares . If is true, only the /// of this view's subviews will be rendered. If is /// false (the default), this method will cause the be prepared to be rendered. /// /// public virtual bool OnDrawAdornments () { // Each of these renders lines to either this View's LineCanvas // Those lines will be finally rendered in OnRenderLineCanvas // QUESTION: Why are we not calling Draw here? Margin?.OnDrawContent (Margin.Viewport); Border?.OnDrawContent (Border.Viewport); Padding?.OnDrawContent (Padding.Viewport); return true; } /// /// Draws the view's content, including Subviews. /// /// /// /// The parameter is provided as a convenience; it has the same values as the /// property. /// /// /// The Location and Size indicate what part of the View's content, defined /// by , is visible and should be drawn. The coordinates taken by /// and /// are relative to , thus if ViewPort.Location.Y is 5 /// the 6th row of the content should be drawn using MoveTo (x, 5). /// /// /// If is larger than ViewPort.Size drawing code should use /// /// to constrain drawing for better performance. /// /// /// The may define smaller area than ; complex drawing code /// can be more /// efficient by using to constrain drawing for better performance. /// /// /// Overrides should loop through the subviews and call . /// /// /// /// The rectangle describing the currently visible viewport into the ; has the same value as /// . /// public virtual void OnDrawContent (Rectangle viewport) { if (!CanBeVisible (this)) { return; } // BUGBUG: this clears way too frequently. Need to optimize this. if (NeedsDisplay/* || Arrangement.HasFlag (ViewArrangement.Overlapped)*/) { Clear (); } if (!string.IsNullOrEmpty (TextFormatter.Text)) { TextFormatter.NeedsFormat = true; } // This should NOT clear // TODO: If the output is not in the Viewport, do nothing var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); TextFormatter?.Draw ( drawRect, HasFocus ? GetFocusColor () : GetNormalColor (), HasFocus ? GetHotFocusColor () : GetHotNormalColor (), Rectangle.Empty ); SetSubViewNeedsDisplay (); // TODO: Move drawing of subviews to a separate OnDrawSubviews virtual method // Draw subviews // TODO: Implement OnDrawSubviews (cancelable); if (_subviews is { } && SubViewNeedsDisplay) { IEnumerable subviewsNeedingDraw = _subviews.Where ( view => view.Visible && (view.NeedsDisplay || view.SubViewNeedsDisplay // || view.Arrangement.HasFlag (ViewArrangement.Overlapped) )); foreach (View view in subviewsNeedingDraw) { if (view.IsLayoutNeeded ()) { //Debug.WriteLine ($"Layout should be de-coupled from drawing: {view}"); //view.LayoutSubviews (); } // TODO: This ensures overlapped views are drawn correctly. However, this is inefficient. // TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413 if (view.Arrangement.HasFlag (ViewArrangement.Overlapped)) { // view.SetNeedsDisplay (); } view.Draw (); } } } /// /// Called after to enable overrides. /// /// /// The viewport-relative rectangle describing the currently visible viewport into the /// /// public virtual void OnDrawContentComplete (Rectangle viewport) { DrawContentComplete?.Invoke (this, new (viewport, Rectangle.Empty)); } // TODO: Make this cancelable /// /// Renders . If is true, only the /// of this view's subviews will be rendered. If is /// false (the default), this method will cause the to be rendered. /// /// public virtual bool OnRenderLineCanvas () { if (Driver is null) { return false; } // If we have a SuperView, it'll render our frames. if (!SuperViewRendersLineCanvas && LineCanvas.Viewport != Rectangle.Empty) { foreach (KeyValuePair p in LineCanvas.GetCellMap ()) { // Get the entire map if (p.Value is { }) { Driver.SetAttribute (p.Value.Value.Attribute ?? ColorScheme!.Normal); Driver.Move (p.Key.X, p.Key.Y); // TODO: #2616 - Support combining sequences that don't normalize Driver.AddRune (p.Value.Value.Rune); } } LineCanvas.Clear (); } if (Subviews.Any (s => s.SuperViewRendersLineCanvas)) { foreach (View subview in Subviews.Where (s => s.SuperViewRendersLineCanvas)) { // Combine the LineCanvas' LineCanvas.Merge (subview.LineCanvas); subview.LineCanvas.Clear (); } foreach (KeyValuePair p in LineCanvas.GetCellMap ()) { // Get the entire map if (p.Value is { }) { Driver.SetAttribute (p.Value.Value.Attribute ?? ColorScheme!.Normal); Driver.Move (p.Key.X, p.Key.Y); // TODO: #2616 - Support combining sequences that don't normalize Driver.AddRune (p.Value.Value.Rune); } } LineCanvas.Clear (); } return true; } #region NeedsDisplay // The viewport-relative region that needs to be redrawn. Marked internal for unit tests. internal Rectangle _needsDisplayRect = Rectangle.Empty; /// Gets or sets whether the view needs to be redrawn. public bool NeedsDisplay { get => _needsDisplayRect != Rectangle.Empty || IsLayoutNeeded (); set { if (value) { SetNeedsDisplay (); } else { ClearNeedsDisplay (); } } } /// Gets whether any Subviews need to be redrawn. public bool SubViewNeedsDisplay { get; private set; } /// Sets that the of this View needs to be redrawn. /// /// If the view has not been initialized ( is ), this method /// does nothing. /// public void SetNeedsDisplay () { Rectangle viewport = Viewport; if (_needsDisplayRect != Rectangle.Empty && viewport.IsEmpty) { // This handles the case where the view has not been initialized yet return; } SetNeedsDisplay (viewport); } /// Expands the area of this view needing to be redrawn to include . /// /// /// The location of is relative to the View's . /// /// /// If the view has not been initialized ( is ), the area to be /// redrawn will be the . /// /// /// The relative region that needs to be redrawn. public void SetNeedsDisplay (Rectangle viewPortRelativeRegion) { if (_needsDisplayRect.IsEmpty) { _needsDisplayRect = viewPortRelativeRegion; } else { int x = Math.Min (Viewport.X, viewPortRelativeRegion.X); int y = Math.Min (Viewport.Y, viewPortRelativeRegion.Y); int w = Math.Max (Viewport.Width, viewPortRelativeRegion.Width); int h = Math.Max (Viewport.Height, viewPortRelativeRegion.Height); _needsDisplayRect = new (x, y, w, h); } Margin?.SetNeedsDisplay (); Border?.SetNeedsDisplay (); Padding?.SetNeedsDisplay (); SuperView?.SetSubViewNeedsDisplay (); if (this is Adornment adornment) { adornment.Parent?.SetSubViewNeedsDisplay (); } foreach (View subview in Subviews) { if (subview.Frame.IntersectsWith (viewPortRelativeRegion)) { Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, viewPortRelativeRegion); subviewRegion.X -= subview.Frame.X; subviewRegion.Y -= subview.Frame.Y; subview.SetNeedsDisplay (subviewRegion); } } } /// Sets to for this View and all Superviews. public void SetSubViewNeedsDisplay () { SubViewNeedsDisplay = true; if (this is Adornment adornment) { adornment.Parent?.SetSubViewNeedsDisplay (); } if (SuperView is { SubViewNeedsDisplay: false }) { SuperView.SetSubViewNeedsDisplay (); } } /// Clears and . protected void ClearNeedsDisplay () { _needsDisplayRect = Rectangle.Empty; SubViewNeedsDisplay = false; Margin?.ClearNeedsDisplay (); Border?.ClearNeedsDisplay (); Padding?.ClearNeedsDisplay (); foreach (View subview in Subviews) { subview.ClearNeedsDisplay (); } } #endregion NeedsDisplay }