#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 (); // The draw context is used to track the region drawn by each view. DrawContext context = new DrawContext (); foreach (View view in viewsArray) { if (force) { view.SetNeedsDraw (); } view.Draw (context); } // Draw the margins (those whith Shadows) last to ensure they are drawn on top of the content. 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 (DrawContext? context = null) { if (!CanBeVisible (this)) { return; } Region? originalClip = GetClip (); // TODO: This can be further optimized by checking NeedsDraw below and only // TODO: clearing, drawing text, drawing content, etc. if it is true. if (NeedsDraw || SubViewNeedsDraw) { // ------------------------------------ // Draw the Border and Padding. // Note Margin with a Shadow is special-cased and drawn in a separate pass to support // transparent shadows. DoDrawAdornments (originalClip); SetClip (originalClip); // ------------------------------------ // Clear 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 originalClip = AddViewportToClip (); // If no context ... context ??= new DrawContext (); // TODO: Simplify/optimize SetAttribute system. DoSetAttribute (); DoClearViewport (); // ------------------------------------ // Draw the subviews first (order matters: SubViews, Text, Content) if (SubViewNeedsDraw) { DoSetAttribute (); DoDrawSubViews (context); } // ------------------------------------ // Draw the text DoSetAttribute (); DoDrawText (context); // ------------------------------------ // Draw the content DoSetAttribute (); DoDrawContent (context); // ------------------------------------ // Draw the line canvas // Restore the clip before rendering the line canvas and adornment subviews // because they may draw outside the viewport. SetClip (originalClip); originalClip = AddFrameToClip (); 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. DoDrawAdornmentsSubViews (); // ------------------------------------ // Advance the diagnostics draw indicator Border?.AdvanceDrawIndicator (); ClearNeedsDraw (); } // ------------------------------------ // This causes the Margin to be drawn in a second pass if it has a ShadowStyle // PERFORMANCE: If there is a Margin w/ Shadow, it will be redrawn each iteration of the main loop. Margin?.CacheClip (); // ------------------------------------ // Reset the clip to what it was when we started SetClip (originalClip); // ------------------------------------ // We're done drawing - The Clip is reset to what it was before we started. DoDrawComplete (context); } #region DrawAdornments private void DoDrawAdornmentsSubViews () { // NOTE: We do not support subviews of Margin? 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?.AddFrameToClip (); Border?.DoDrawSubViews (); SetClip (saved); } if (Padding?.SubViews is { } && Padding.Thickness != Thickness.Empty) { foreach (View subview in Padding.SubViews) { subview.SetNeedsDraw (); } Region? saved = Padding?.AddFrameToClip (); Padding?.DoDrawSubViews (); SetClip (saved); } } private void DoDrawAdornments (Region? originalClip) { if (this is Adornment) { AddFrameToClip (); } else { // Set the clip to be just the thicknesses of the adornments // TODO: Put this union logic in a method on View? Region? clipAdornments = Margin!.Thickness.AsRegion (Margin!.FrameToScreen ()); clipAdornments?.Combine (Border!.Thickness.AsRegion (Border!.FrameToScreen ()), RegionOp.Union); clipAdornments?.Combine (Padding!.Thickness.AsRegion (Padding!.FrameToScreen ()), RegionOp.Union); clipAdornments?.Combine (originalClip, RegionOp.Intersect); SetClip (clipAdornments); } if (Margin?.NeedsLayout == true) { Margin.NeedsLayout = false; // BUGBUG: This should not use ClearFrame as that clears the insides too 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 (); Margin?.SetNeedsDraw (); } if (OnDrawingAdornments ()) { return; } // TODO: add event. DrawAdornments (); } /// /// Causes , , and to be drawn. /// /// /// /// is drawn in a separate pass if is set. /// /// public void DrawAdornments () { // 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 (); } if (Margin is { } && Margin.Thickness != Thickness.Empty && Margin.ShadowStyle == ShadowStyle.None) { Margin?.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 OnDrawingAdornments () { 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 internal void DoClearViewport () { if (ViewportSettings.HasFlag (ViewportSettings.Transparent)) { return; } if (OnClearingViewport ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, null); ClearingViewport?.Invoke (this, dev); if (dev.Cancel) { SetNeedsDraw (); return; } ClearViewport (); OnClearedViewport (); ClearedViewport?.Invoke (this, new (Viewport, Viewport, null)); } /// /// Called when the is to be cleared. /// /// to stop further clearing. protected virtual bool OnClearingViewport () { return false; } /// Event invoked when the is to be cleared. /// /// 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; /// /// Called when the has been cleared /// protected virtual void OnClearedViewport () { } /// Event invoked when the has been cleared. public event EventHandler? ClearedViewport; /// 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 (DrawContext? context = null) { if (OnDrawingText (context)) { return; } if (OnDrawingText ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); DrawingText?.Invoke (this, dev); if (dev.Cancel) { return; } DrawText (context); } /// /// Called when the of the View is to be drawn. /// /// The draw context to report drawn areas to. /// to stop further drawing of . protected virtual bool OnDrawingText (DrawContext? context) { return false; } /// /// 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 . /// /// The draw context to report drawn areas to. public void DrawText (DrawContext? context = null) { if (!string.IsNullOrEmpty (TextFormatter.Text)) { TextFormatter.NeedsFormat = true; } var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ()); // Use GetDrawRegion to get precise drawn areas Region textRegion = TextFormatter.GetDrawRegion (drawRect); // Report the drawn area to the context context?.AddDrawnRegion (textRegion); if (!NeedsDraw) { return; } 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 (DrawContext? context = null) { if (OnDrawingContent (context)) { return; } // TODO: Upgrade all overrides of OnDrawingContent to use DrawContext and remove this override if (OnDrawingContent ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); DrawingContent?.Invoke (this, dev); if (dev.Cancel) { return; } // No default drawing; let event handlers or overrides handle it } /// /// Called when the View's content is to be drawn. The default implementation does nothing. /// /// The draw context to report drawn areas to. /// to stop further drawing content. protected virtual bool OnDrawingContent (DrawContext? context) { return false; } /// /// 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 (DrawContext? context = null) { if (OnDrawingSubViews (context)) { return; } if (OnDrawingSubViews ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); DrawingSubViews?.Invoke (this, dev); if (dev.Cancel) { return; } if (!SubViewNeedsDraw) { return; } DrawSubViews (context); } /// /// Called when the are to be drawn. /// /// The draw context to report drawn areas to, or null if not tracking. /// to stop further drawing of . protected virtual bool OnDrawingSubViews (DrawContext? context) { return false; } /// /// 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 . /// /// The draw context to report drawn areas to, or null if not tracking. public void DrawSubViews (DrawContext? context = null) { if (InternalSubViews.Count == 0) { return; } // Draw the subviews in reverse order to leverage clipping. foreach (View view in InternalSubViews.Where (view => view.Visible).Reverse ()) { // TODO: HACK - This forcing of SetNeedsDraw with SuperViewRendersLineCanvas enables auto line join to work, but is brute force. if (view.SuperViewRendersLineCanvas || view.ViewportSettings.HasFlag (ViewportSettings.Transparent)) { view.SetNeedsDraw (); } view.Draw (context); 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 (DrawContext? context) { OnDrawComplete (context); DrawComplete?.Invoke (this, new (Viewport, Viewport, context)); // Now, update the clip to exclude this view (not including Margin) if (this is not Adornment) { if (ViewportSettings.HasFlag (ViewportSettings.Transparent)) { // context!.DrawnRegion is the region that was drawn by this view. It may include regions outside // the Viewport. We need to clip it to the Viewport. context!.ClipDrawnRegion (ViewportToScreen (Viewport)); // Exclude the drawn region from the clip ExcludeFromClip (context!.GetDrawnRegion ()); // Exclude the Border and Padding from the clip ExcludeFromClip (Border?.Thickness.AsRegion (FrameToScreen ())); ExcludeFromClip (Padding?.Thickness.AsRegion (FrameToScreen ())); } else { // Exclude this view (not including Margin) from the Clip Rectangle borderFrame = FrameToScreen (); if (Border is { }) { borderFrame = Border.FrameToScreen (); } // In the non-transparent (typical case), we want to exclude the entire view area (borderFrame) from the clip ExcludeFromClip (borderFrame); // Update context.DrawnRegion to include the entire view (borderFrame), but clipped to our SuperView's viewport // This enables the SuperView to know what was drawn by this view. context?.AddDrawnRectangle (borderFrame); } } // TODO: Determine if we need another event that conveys the FINAL DrawContext } /// /// Called when the View is completed drawing. /// /// /// The parameter provides the drawn region of the View. /// protected virtual void OnDrawComplete (DrawContext? context) { } /// 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 }