using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui.ViewBase; 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) { // **Snapshot once** — every recursion level gets its own frozen array View [] viewsArray = views.Snapshot (); // 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 last to ensure they are drawn on top of the content. Margin.DrawMargins (viewsArray); // DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all. foreach (View view in viewsArray) { view.ClearNeedsDraw (); } // After all peer views have been drawn and cleared, we can now clear the SuperView's SubViewNeedsDraw flag. // ClearNeedsDraw() does not clear SuperView.SubViewNeedsDraw (by design, to avoid premature clearing // when siblings still need drawing), so we must do it here after ALL peers are processed. // We only clear the flag if ALL the SuperView's subviews no longer need drawing. View? lastSuperView = null; foreach (View view in viewsArray) { if (view is not Adornment && view.SuperView is { } && view.SuperView != lastSuperView) { // Check if ANY subview of this SuperView still needs drawing bool anySubViewNeedsDrawing = view.SuperView.InternalSubViews.Any (v => v.NeedsDraw || v.SubViewNeedsDraw); if (!anySubViewNeedsDrawing) { view.SuperView.SubViewNeedsDraw = false; } lastSuperView = view.SuperView; } } } /// /// 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 (); SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); DoClearViewport (context); // ------------------------------------ // Draw the subviews first (order matters: SubViews, Text, Content) if (SubViewNeedsDraw) { DoDrawSubViews (context); } // ------------------------------------ // Draw the text SetAttributeForRole (Enabled ? VisualRole.Normal : VisualRole.Disabled); DoDrawText (context); // ------------------------------------ // Draw the content 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 (); if (this is not Adornment && SuperView is not Adornment) { // Parent Debug.Assert (Margin!.Parent == this); Debug.Assert (Border!.Parent == this); Debug.Assert (Padding!.Parent == this); // SubViewNeedsDraw is set to false by ClearNeedsDraw. Debug.Assert (SubViewNeedsDraw == false); Debug.Assert (Margin!.SubViewNeedsDraw == false); Debug.Assert (Border!.SubViewNeedsDraw == false); Debug.Assert (Padding!.SubViewNeedsDraw == false); // NeedsDraw is set to false by ClearNeedsDraw. Debug.Assert (NeedsDraw == false); Debug.Assert (Margin!.NeedsDraw == false); Debug.Assert (Border!.NeedsDraw == false); Debug.Assert (Padding!.NeedsDraw == false); } } // ------------------------------------ // This causes the Margin to be drawn in a second pass if it has a ShadowStyle 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 && Border.NeedsDraw) { // 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 && Padding.NeedsDraw) { foreach (View subview in Padding.SubViews) { subview.SetNeedsDraw (); } Region? saved = Padding?.AddFrameToClip (); Padding?.DoDrawSubViews (); SetClip (saved); } } internal 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; Margin?.Thickness.Draw (Driver, FrameToScreen ()); Margin?.Parent?.SetSubViewNeedsDrawDownHierarchy (); } 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 (GetAttributeForRole (VisualRole.Normal)); 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 ClearViewport internal void DoClearViewport (DrawContext? context = null) { if (!NeedsDraw || ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) || OnClearingViewport ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); ClearingViewport?.Invoke (this, dev); if (dev.Cancel) { // BUGBUG: We should add the Viewport to context.DrawRegion here? SetNeedsDraw (); return; } if (!ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)) { ClearViewport (context); 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 (DrawContext? context = null) { if (Driver is null) { return; } // Get screen-relative coords Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) }); if (ViewportSettings.HasFlag (ViewportSettingsFlags.ClearContentOnly)) { Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), GetContentSize ())); toClear = Rectangle.Intersect (toClear, visibleContent); } Driver.FillRect (toClear); // context.AddDrawnRectangle (toClear); SetNeedsDraw (); } #endregion ClearViewport #region DrawText private void DoDrawText (DrawContext? context = null) { if (!NeedsDraw) { return; } if (!string.IsNullOrEmpty (TextFormatter.Text)) { TextFormatter.NeedsFormat = true; } if (OnDrawingText (context)) { return; } // TODO: Get rid of this vf in lieu of the one above if (OnDrawingText ()) { return; } var dev = new DrawEventArgs (Viewport, Rectangle.Empty, context); DrawingText?.Invoke (this, dev); if (dev.Cancel) { return; } DrawText (context); OnDrewText (); DrewText?.Invoke (this, EventArgs.Empty); } /// /// 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) { 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 (Driver is { }) { TextFormatter.Draw ( Driver, drawRect, HasFocus ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal), HasFocus ? GetAttributeForRole (VisualRole.HotFocus) : GetAttributeForRole (VisualRole.HotNormal), Rectangle.Empty); } // We assume that the text has been drawn over the entire area; ensure that the subviews are redrawn. SetSubViewNeedsDrawDownHierarchy (); } /// /// Called when the of the View has been drawn. /// protected virtual void OnDrewText () { } /// Raised when the of the View has been drawn. public event EventHandler? DrewText; #endregion DrawText #region DrawContent private void DoDrawContent (DrawContext? context = null) { if (!NeedsDraw || OnDrawingContent (context)) { 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. /// /// /// Override this method to draw custom content for your View. /// /// /// Transparency Support: If your View has with /// set, you should report the exact regions you draw to via the parameter. This allows /// the transparency system to exclude only the drawn areas from the clip region, letting views beneath show through /// in the areas you didn't draw. /// /// /// Use for simple rectangular areas, or /// for complex, non-rectangular shapes. All coordinates passed to these methods must be in screen-relative coordinates. /// Use or to convert from /// viewport-relative or content-relative coordinates. /// /// /// Example of drawing custom content with transparency support: /// /// /// protected override bool OnDrawingContent (DrawContext? context) /// { /// base.OnDrawingContent (context); /// /// // Draw content in viewport-relative coordinates /// Rectangle rect1 = new Rectangle (5, 5, 10, 3); /// Rectangle rect2 = new Rectangle (8, 8, 4, 7); /// FillRect (rect1, Glyphs.BlackCircle); /// FillRect (rect2, Glyphs.BlackCircle); /// /// // Report drawn region in screen-relative coordinates for transparency /// if (ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)) /// { /// Region drawnRegion = new Region (ViewportToScreen (rect1)); /// drawnRegion.Union (ViewportToScreen (rect2)); /// context?.AddDrawnRegion (drawnRegion); /// } /// /// return true; /// } /// /// protected virtual bool OnDrawingContent (DrawContext? context) { return false; } /// Raised when the View's content is to be drawn. /// /// /// Subscribe to this event to draw custom content for the View. Use the drawing methods available on /// such as , , and . /// /// /// The event is invoked after and have been drawn, but before any are drawn. /// /// /// Transparency Support: If the View has with /// set, use the to report which areas were actually drawn. This enables proper transparency /// by excluding only the drawn areas from the clip region. See for details on reporting drawn regions. /// /// /// The property provides the view-relative rectangle describing the currently visible viewport into the View. /// /// public event EventHandler? DrawingContent; #endregion DrawContent #region DrawSubViews private void DoDrawSubViews (DrawContext? context = null) { if (!NeedsDraw || OnDrawingSubViews (context)) { return; } // TODO: Get rid of this vf in lieu of the one above 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.Snapshot ().Where (v => v.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 (ViewportSettingsFlags.Transparent)) { //view.SetNeedsDraw (); } view.Draw (context); if (view.SuperViewRendersLineCanvas) { LineCanvas.Merge (view.LineCanvas); view.LineCanvas.Clear (); } } } #endregion DrawSubViews #region DrawLineCanvas private void DoRenderLineCanvas () { if (!NeedsDraw || 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 ?? GetAttributeForRole (VisualRole.Normal)); Driver.Move (p.Key.X, p.Key.Y); // TODO: #2616 - Support combining sequences that don't normalize AddStr (p.Value.Value.Grapheme); } } 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 (ViewportSettingsFlags.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 (Border.FrameToScreen ())); ExcludeFromClip (Padding?.Thickness.AsRegion (Padding.FrameToScreen ())); // QUESTION: This makes it so that no nesting of transparent views is possible, but is more correct? context = new DrawContext (); } 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 }