#nullable enable #define HACK_DRAW_OVERLAPPED using System.ComponentModel; using System.Diagnostics; namespace Terminal.Gui; public partial class View // Drawing APIs { /// /// 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. /// /// /// // TODO: Add docs for the drawing process. /// /// public void Draw () { if (!CanBeVisible (this) || (!NeedsDraw && !SubViewNeedsDraw)) { return; } DoDrawAdornments (); DoSetAttribute (); // 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. Region prevClip = SetClip (); DoClearViewport (Viewport); DoDrawText (Viewport); DoDrawContent (Viewport); DoDrawSubviews (Viewport); // Restore the clip before rendering the line canvas and adornment subviews // because they may draw outside the viewport. if (Driver is { }) { Driver.Clip = prevClip; } DoRenderLineCanvas (); DoDrawAdornmentSubViews (); ClearNeedsDraw (); // We're done DoDrawComplete (); } #region DrawAdornments private void DoDrawAdornmentSubViews () { // This causes the Adornment's subviews to be REDRAWN // TODO: Figure out how to make this more efficient if (Margin?.Subviews is { }) { foreach (View subview in Margin.Subviews) { subview.SetNeedsDraw (); } Margin?.DoDrawSubviews (Margin.Viewport); } if (Border?.Subviews is { }) { foreach (View subview in Border.Subviews) { subview.SetNeedsDraw (); } Border?.DoDrawSubviews (Border.Viewport); } if (Padding?.Subviews is { }) { foreach (View subview in Padding.Subviews) { subview.SetNeedsDraw (); } Padding?.DoDrawSubviews (Padding.Viewport); } } private void DoDrawAdornments () { if (OnDrawingAdornments ()) { return; } // TODO: add event. // Subviews of Adornments count as subviews if (!NeedsDraw && !SubViewNeedsDraw) { return; } DrawAdornments (); } /// /// Causes each of the View's Adornments to be drawn. This includes the , , and . /// public void DrawAdornments () { // Each of these renders lines to either this View's LineCanvas // Those lines will be finally rendered in OnRenderLineCanvas Margin?.Draw (); Border?.Draw (); Padding?.Draw (); } /// /// 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; } if (!NeedsDraw && !SubViewNeedsDraw) { 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 (Rectangle viewport) { Debug.Assert (viewport == Viewport); if (OnClearingViewport (Viewport)) { 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 (Rectangle viewport) { 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 (Rectangle viewport) { Debug.Assert (viewport == Viewport); if (OnDrawingText (Viewport)) { 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 (Rectangle viewport) { 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; } // 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 ); // 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 (Rectangle viewport) { Debug.Assert (viewport == Viewport); if (OnDrawingContent (Viewport)) { 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 (Rectangle viewport) { 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 (Rectangle viewport) { Debug.Assert (viewport == Viewport); if (OnDrawingSubviews (Viewport)) { 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 (Rectangle viewport) { 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 || !SubViewNeedsDraw) { return; } #if HACK_DRAW_OVERLAPPED IEnumerable subviewsNeedingDraw = _subviews.Where (view => (view.Visible && (view.NeedsDraw || view.SubViewNeedsDraw)) || view.Arrangement.HasFlag (ViewArrangement.Overlapped)); #else IEnumerable subviewsNeedingDraw = _subviews.Where (view => (view.Visible && (view.NeedsDraw || view.SubViewNeedsDraw))); #endif foreach (View view in subviewsNeedingDraw) { #if HACK_DRAW_OVERLAPPED if (view.Arrangement.HasFlag (ViewArrangement.Overlapped)) { view.SetNeedsDraw (); } #endif view.Draw (); } } #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 () { // TODO: This is super confusing and needs to be refactored. if (Driver is null) { return; } // 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 { }) { 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 { }) { 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, EventArgs.Empty); // 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: 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. This is a temporary fix. get => _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 (_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 (_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); } Margin?.SetNeedsDraw (); Border?.SetNeedsDraw (); Padding?.SetNeedsDraw (); SuperView?.SetSubViewNeedsDraw (); if (this is Adornment adornment) { adornment.Parent?.SetSubViewNeedsDraw (); } 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.SetNeedsDraw (subviewRegion); } } } /// Sets to for this View and all Superviews. public void SetSubViewNeedsDraw () { 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; Margin?.ClearNeedsDraw (); Border?.ClearNeedsDraw (); Padding?.ClearNeedsDraw (); foreach (View subview in Subviews) { subview.ClearNeedsDraw (); } } #endregion NeedsDraw }