using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui.ViewBase; public partial class View // Drawing APIs { /// /// Draws a set of peer views (views that share the same SuperView). /// /// 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 peer subviews 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 Adornments. // 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 (context); // ------------------------------------ // Re-draw the Border and Padding Adornment SubViews // HACK: This is a hack to ensure that the Border and Padding Adornment 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 // But the context contains the region that was drawn by this view DoDrawComplete (context); // When DoDrawComplete returns, Driver.Clip has been updated to exclude this view's area. // The next view drawn (earlier in Z-order, typically a peer view or the SuperView) will see // a clip with "holes" where this view (and any SubViews drawn before it) are located. } #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 after have been 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 Z-order to leverage clipping. // SubViews earlier in the collection are drawn last (on top). 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 (DrawContext? context) { // TODO: Add context to OnRenderingLineCanvas if (!NeedsDraw || OnRenderingLineCanvas ()) { return; } // TODO: Add event RenderLineCanvas (context); } /// /// 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 lines to this LineCanvas. public LineCanvas LineCanvas { get; } = new (); /// /// Gets or sets whether this View will use its SuperView's for rendering any /// lines. If the rendering of any borders drawn by this view will be done by its /// 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 (DrawContext? context) { if (Driver is null) { return; } if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rectangle.Empty) { // Get both cell map and Region in a single pass through the canvas (Dictionary cellMap, Region lineRegion) = LineCanvas.GetCellMapWithRegion (); foreach (KeyValuePair p in cellMap) { // 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); } } // Report the drawn region for transparency support // Region was built during the GetCellMapWithRegion() call above if (context is { } && cellMap.Count > 0) { context.AddDrawnRegion (lineRegion); } LineCanvas.Clear (); } } #endregion DrawLineCanvas #region DrawComplete /// /// Called at the end of to finalize drawing and update the clip region. /// /// /// The tracking what regions were drawn by this view and its subviews. /// May be if not tracking drawn regions. /// private void DoDrawComplete (DrawContext? context) { // Phase 1: Notify that drawing is complete // Raise virtual method first, then event. This allows subclasses to override behavior // before subscribers see the event. OnDrawComplete (context); DrawComplete?.Invoke (this, new (Viewport, Viewport, context)); // Phase 2: Update Driver.Clip to exclude this view's drawn area // This prevents views "behind" this one (earlier in draw order/Z-order) from drawing over it. // Adornments (Margin, Border, Padding) are handled by their Adornment.Parent view and don't exclude themselves. if (this is not Adornment) { if (ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)) { // Transparent View Path: // Only exclude the regions that were actually drawn, allowing views beneath // to show through in areas where nothing was drawn. // The context.DrawnRegion may include areas outside the Viewport (e.g., if content // was drawn with ViewportSettingsFlags.AllowContentOutsideViewport). We need to clip // it to the Viewport bounds to prevent excluding areas that aren't visible. context!.ClipDrawnRegion (ViewportToScreen (Viewport)); // Exclude the actually-drawn region from Driver.Clip ExcludeFromClip (context.GetDrawnRegion ()); // Border and Padding are always opaque (they draw lines/fills), so exclude them too ExcludeFromClip (Border?.Thickness.AsRegion (Border.FrameToScreen ())); ExcludeFromClip (Padding?.Thickness.AsRegion (Padding.FrameToScreen ())); } else { // Opaque View Path (default): // Exclude the entire view area from Driver.Clip. This is the typical case where // the view is considered fully opaque. // Start with the Frame in screen coordinates Rectangle borderFrame = FrameToScreen (); // If there's a Border, use its frame instead (includes the border thickness) if (Border is { }) { borderFrame = Border.FrameToScreen (); } // Exclude this view's entire area (Border inward, but not Margin) from the clip. // This prevents any view drawn after this one from drawing in this area. ExcludeFromClip (borderFrame); // Update the DrawContext to track that we drew this entire rectangle. // This allows our SuperView (if any) to know what area we occupied, // which is important for transparency calculations at higher levels. context?.AddDrawnRectangle (borderFrame); } } // When this method returns, Driver.Clip has been updated to exclude this view's area. // The next view drawn (earlier in Z-order, typically a peer view or the SuperView) will see // a clip with "holes" where this view (and any SubViews drawn before it) are located. } /// /// Called when the View has completed drawing and is about to update the clip region. /// /// /// The containing the regions that were drawn by this view and its subviews. /// May be if not tracking drawn regions. /// /// /// /// This method is called at the very end of , after all drawing /// (adornments, content, text, subviews, line canvas) has completed but before the view's area /// is excluded from . /// /// /// Use this method to: /// /// /// /// Perform any final drawing operations that need to happen after SubViews are drawn /// /// /// Inspect what was drawn via the parameter /// /// /// Add additional regions to the if needed /// /// /// /// Important: At this point, has been restored to the state /// it was in when began. After this method returns, the view's /// area will be excluded from the clip (see for details). /// /// /// Transparency Support: If includes /// , the parameter /// contains the actual regions that were drawn. You can inspect this to see what areas /// will be excluded from the clip, and optionally add more regions if needed. /// /// /// /// /// protected virtual void OnDrawComplete (DrawContext? context) { } /// Raised when the View has completed drawing and is about to update the clip region. /// /// /// This event is raised at the very end of , after all drawing /// operations have completed but before the view's area is excluded from . /// /// /// The property provides information about what regions /// were drawn by this view and its subviews. This is particularly useful for views with /// enabled, as it shows exactly which areas /// will be excluded from the clip. /// /// /// Use this event to: /// /// /// /// Perform any final drawing operations /// /// /// Inspect what was drawn /// /// /// Track drawing statistics or metrics /// /// /// /// Note: This event fires after . If you need /// to override the behavior, prefer overriding the virtual method in a subclass rather than /// subscribing to this event. /// /// /// /// public event EventHandler? DrawComplete; #endregion DrawComplete }