#nullable enable using System.ComponentModel; namespace Terminal.Gui; public partial class View // Drawing APIs { /// /// Draws a set of views. /// /// The peer views to draw. /// If , will be called on each view to force it to be drawn. internal static void Draw (IEnumerable views, bool force) { IEnumerable viewsArray = views as View [] ?? views.ToArray (); foreach (View view in viewsArray) { if (force) { view.SetNeedsDraw (); } view.Draw (); } Margin.DrawMargins (viewsArray); } /// /// Draws the view if it needs to be drawn. /// /// /// /// The view will only be drawn if it is visible, and has any of , /// , /// or set. /// /// /// See the View Drawing Deep Dive for more information: . /// /// public void Draw () { if (!CanBeVisible (this)) { return; } Region? saved = GetClip (); // TODO: This can be further optimized by checking NeedsDraw below and only clearing, drawing text, drawing content, etc. if it is true. if (NeedsDraw || SubViewNeedsDraw) { // Draw the Border and Padding. // We clip to the frame to prevent drawing outside the frame. saved = ClipFrame (); DoDrawBorderAndPadding (); SetClip (saved); // Draw the content within the Viewport // 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. // Get our Viewport in screen coordinates saved = ClipViewport (); // Clear the viewport // TODO: Simplify/optimize SetAttribute system. DoSetAttribute (); DoClearViewport (); // Draw the subviews only if needed if (SubViewNeedsDraw) { DoSetAttribute (); DoDrawSubviews (); } // Draw the text DoSetAttribute (); DoDrawText (); // Draw the content DoSetAttribute (); DoDrawContent (); // Restore the clip before rendering the line canvas and adornment subviews // because they may draw outside the viewport. SetClip (saved); saved = ClipFrame (); // Draw the line canvas DoRenderLineCanvas (); // Re-draw the border and padding subviews // HACK: This is a hack to ensure that the border and padding subviews are drawn after the line canvas. DoDrawBorderAndPaddingSubViews (); // Advance the diagnostics draw indicator Border?.AdvanceDrawIndicator (); ClearNeedsDraw (); } // This causes the Margin to be drawn in a second pass // PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop. Margin?.CacheClip (); // We're done drawing DoDrawComplete (); // QUESTION: Should this go before DoDrawComplete? What is more correct? SetClip (saved); // Exclude this view (not including Margin) from the Clip if (this is not Adornment) { Rectangle borderFrame = FrameToScreen (); if (Border is { }) { borderFrame = Border.FrameToScreen (); } ExcludeFromClip (borderFrame); } } #region DrawAdornments private void DoDrawBorderAndPaddingSubViews () { if (Border?.Subviews is { } && Border.Thickness != Thickness.Empty) { // PERFORMANCE: Get the check for DrawIndicator out of this somehow. foreach (View subview in Border.Subviews.Where (v => v.Visible || v.Id == "DrawIndicator")) { if (subview.Id != "DrawIndicator") { subview.SetNeedsDraw (); } LineCanvas.Exclude (new (subview.FrameToScreen())); } Region? saved = Border?.ClipFrame (); Border?.DoDrawSubviews (); SetClip (saved); } if (Padding?.Subviews is { } && Padding.Thickness != Thickness.Empty) { foreach (View subview in Padding.Subviews) { subview.SetNeedsDraw (); } Region? saved = Padding?.ClipFrame (); Padding?.DoDrawSubviews (); SetClip (saved); } } private void DoDrawBorderAndPadding () { if (Margin?.NeedsLayout == true) { Margin.NeedsLayout = false; Margin?.ClearFrame (); Margin?.Parent?.SetSubViewNeedsDraw (); } if (SubViewNeedsDraw) { // A Subview may add to the LineCanvas. This ensures any Adornment LineCanvas updates happen. Border?.SetNeedsDraw (); Padding?.SetNeedsDraw (); } if (OnDrawingBorderAndPadding ()) { return; } // TODO: add event. DrawBorderAndPadding (); } /// /// Causes and to be drawn. /// /// /// /// is drawn in a separate pass. /// /// public void DrawBorderAndPadding () { // We do not attempt to draw Margin. It is drawn in a separate pass. // Each of these renders lines to this View's LineCanvas // Those lines will be finally rendered in OnRenderLineCanvas if (Border is { } && Border.Thickness != Thickness.Empty) { Border?.Draw (); } if (Padding is { } && Padding.Thickness != Thickness.Empty) { Padding?.Draw (); } } private void ClearFrame () { if (Driver is null) { return; } // Get screen-relative coords Rectangle toClear = FrameToScreen (); Attribute prev = SetAttribute (GetNormalColor ()); Driver.FillRect (toClear); SetAttribute (prev); SetNeedsDraw (); } /// /// Called when the View's Adornments are to be drawn. 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. /// /// to stop further drawing of the Adornments. protected virtual bool OnDrawingBorderAndPadding () { return false; } #endregion DrawAdornments #region SetAttribute private void DoSetAttribute () { if (OnSettingAttribute ()) { return; } var args = new CancelEventArgs (); SettingAttribute?.Invoke (this, args); if (args.Cancel) { return; } SetNormalAttribute (); } /// /// Called when the normal attribute for the View is to be set. This is called before the View is drawn. /// /// to stop default behavior. protected virtual bool OnSettingAttribute () { return false; } /// Raised when the normal attribute for the View is to be set. This is raised before the View is drawn. /// /// Set to to stop default behavior. /// public event EventHandler? SettingAttribute; /// /// Sets the attribute for the View. This is called before the View is drawn. /// public void SetNormalAttribute () { if (ColorScheme is { }) { SetAttribute (GetNormalColor ()); } } #endregion #region ClearViewport private void DoClearViewport () { if (OnClearingViewport ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty); ClearingViewport?.Invoke (this, dev); if (dev.Cancel) { return; } if (!NeedsDraw) { return; } ClearViewport (); } /// /// Called when the is to be cleared. /// /// to stop further clearing. protected virtual bool OnClearingViewport () { return false; } /// 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? ClearingViewport; /// 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 ClearViewport () { if (Driver is null) { return; } // Get screen-relative coords Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) { Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); toClear = Rectangle.Intersect (toClear, visibleContent); } Attribute prev = SetAttribute (GetNormalColor ()); Driver.FillRect (toClear); SetAttribute (prev); SetNeedsDraw (); } #endregion ClearViewport #region DrawText private void DoDrawText () { if (OnDrawingText ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty); DrawingText?.Invoke (this, dev); if (dev.Cancel) { return; } if (!NeedsDraw) { return; } DrawText (); } /// /// Called when the of the View is to be drawn. /// /// to stop further drawing of . protected virtual bool OnDrawingText () { return false; } /// Raised when the of the View is to be drawn. /// /// Set to to stop further drawing of /// . /// public event EventHandler? DrawingText; /// /// Draws the of the View using the . /// public void DrawText () { if (!string.IsNullOrEmpty (TextFormatter.Text)) { TextFormatter.NeedsFormat = true; } // 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 ); // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn. SetSubViewNeedsDraw (); } #endregion DrawText #region DrawContent private void DoDrawContent () { if (OnDrawingContent ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty); DrawingContent?.Invoke (this, dev); if (dev.Cancel) { } // Do nothing. } /// /// Called when the View's content is to be drawn. The default implementation does nothing. /// /// /// /// to stop further drawing content. protected virtual bool OnDrawingContent () { return false; } /// Raised when the View's content 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? DrawingContent; #endregion DrawContent #region DrawSubviews private void DoDrawSubviews () { if (OnDrawingSubviews ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty); DrawingSubviews?.Invoke (this, dev); if (dev.Cancel) { return; } if (!SubViewNeedsDraw) { return; } DrawSubviews (); } /// /// Called when the are to be drawn. /// /// to stop further drawing of . protected virtual bool OnDrawingSubviews () { return false; } /// Raised when the are to be drawn. /// /// /// /// Set to to stop further drawing of /// . /// public event EventHandler? DrawingSubviews; /// /// Draws the . /// public void DrawSubviews () { if (_subviews is null) { return; } // Draw the subviews in reverse order to leverage clipping. foreach (View view in _subviews.Where (view => view.Visible).Reverse ()) { // TODO: HACK - This enables auto line join to work, but is brute force. if (view.SuperViewRendersLineCanvas) { view.SetNeedsDraw (); } view.Draw (); if (view.SuperViewRendersLineCanvas) { LineCanvas.Merge (view.LineCanvas); view.LineCanvas.Clear (); } } } #endregion DrawSubviews #region DrawLineCanvas private void DoRenderLineCanvas () { if (OnRenderingLineCanvas ()) { return; } // TODO: Add event RenderLineCanvas (); } /// /// Called when the is to be rendered. See . /// /// to stop further drawing of . protected virtual bool OnRenderingLineCanvas () { return false; } /// 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; /// /// Causes the contents of to be drawn. /// 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 void RenderLineCanvas () { if (Driver is null) { return; } if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rectangle.Empty) { foreach (KeyValuePair p in LineCanvas.GetCellMap ()) { // Get the entire map if (p.Value is { }) { 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 (); } } #endregion DrawLineCanvas #region DrawComplete private void DoDrawComplete () { OnDrawComplete (); DrawComplete?.Invoke (this, new (Viewport, Viewport)); // Default implementation does nothing. } /// /// Called when the View is completed drawing. /// protected virtual void OnDrawComplete () { } /// Raised when the View is completed drawing. /// /// public event EventHandler? DrawComplete; #endregion DrawComplete #region NeedsDraw // TODO: Change NeedsDraw to use a Region instead of Rectangle // TODO: Make _needsDrawRect nullable instead of relying on Empty // TODO: If null, it means ? // TODO: If Empty, it means no need to redraw // TODO: If not Empty, it means the region that needs to be redrawn // The viewport-relative region that needs to be redrawn. Marked internal for unit tests. internal Rectangle _needsDrawRect = Rectangle.Empty; /// Gets or sets whether the view needs to be redrawn. /// /// /// Will be if the property is or if /// any part of the view's needs to be redrawn. /// /// /// Setting has no effect on . /// /// public bool NeedsDraw { // TODO: Figure out if we can decouple NeedsDraw from NeedsLayout. get => Visible && (_needsDrawRect != Rectangle.Empty || NeedsLayout); set { if (value) { SetNeedsDraw (); } else { ClearNeedsDraw (); } } } /// Gets whether any Subviews need to be redrawn. public bool SubViewNeedsDraw { 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 SetNeedsDraw () { Rectangle viewport = Viewport; if (!Visible || (_needsDrawRect != Rectangle.Empty && viewport.IsEmpty)) { // This handles the case where the view has not been initialized yet return; } SetNeedsDraw (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 SetNeedsDraw (Rectangle viewPortRelativeRegion) { if (!Visible) { return; } if (_needsDrawRect.IsEmpty) { _needsDrawRect = 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); _needsDrawRect = new (x, y, w, h); } // Do not set on Margin - it will be drawn in a separate pass. if (Border is { } && Border.Thickness != Thickness.Empty) { Border?.SetNeedsDraw (); } if (Padding is { } && Padding.Thickness != Thickness.Empty) { Padding?.SetNeedsDraw (); } SuperView?.SetSubViewNeedsDraw (); if (this is Adornment adornment) { adornment.Parent?.SetSubViewNeedsDraw (); } // There was multiple enumeration error here, so calling ToArray - probably a stop gap foreach (View subview in Subviews.ToArray ()) { if (subview.Frame.IntersectsWith (viewPortRelativeRegion)) { Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, viewPortRelativeRegion); subviewRegion.X -= subview.Frame.X; subviewRegion.Y -= subview.Frame.Y; subview.SetNeedsDraw (subviewRegion); } } } /// Sets to for this View and all Superviews. public void SetSubViewNeedsDraw () { if (!Visible) { return; } SubViewNeedsDraw = true; if (this is Adornment adornment) { adornment.Parent?.SetSubViewNeedsDraw (); } if (SuperView is { SubViewNeedsDraw: false }) { SuperView.SetSubViewNeedsDraw (); } } /// Clears and . protected void ClearNeedsDraw () { _needsDrawRect = Rectangle.Empty; SubViewNeedsDraw = false; if (Margin is { } && Margin.Thickness != Thickness.Empty) { Margin?.ClearNeedsDraw (); } if (Border is { } && Border.Thickness != Thickness.Empty) { Border?.ClearNeedsDraw (); } if (Padding is { } && Padding.Thickness != Thickness.Empty) { Padding?.ClearNeedsDraw (); } foreach (View subview in Subviews) { subview.ClearNeedsDraw (); } if (SuperView is { }) { SuperView.SubViewNeedsDraw = false; } // This ensures LineCanvas' get redrawn if (!SuperViewRendersLineCanvas) { LineCanvas.Clear (); } } #endregion NeedsDraw }