Files
Terminal.Gui/Terminal.Gui/ViewBase/View.Drawing.cs
Tig 6049857813 Refactor LayoutAndDraw and fix ClearNeedsDraw bugs
Refactored the `LayoutAndDraw` method in `ApplicationImpl.Screen.cs` to improve clarity, naming consistency, and redraw logic. Enhanced handling of the `Driver` object to optimize redraws.

Simplified `IterationImpl` in `ApplicationMainLoop.cs` by commenting out redundant checks. Fixed a bug in `SetCursor` to ensure null safety and improve cursor positioning logic.

Modified `ClearNeedsDraw` in `View.Drawing.cs` to prevent premature clearing of the `SuperView`'s `SubViewNeedsDraw` flag. Added explanatory comments to clarify the behavior.

Introduced new unit tests in `NeedsDrawTests.cs` to verify the correctness of `ClearNeedsDraw`:
- Ensured sibling views do not prematurely clear `SuperView`'s flags.
- Verified proper clearing of flags for views, adornments, and descendants.

Improved test coverage and added detailed comments to document expected behavior.
2025-12-03 15:57:27 -07:00

917 lines
32 KiB
C#

using System.ComponentModel;
using System.Diagnostics;
namespace Terminal.Gui.ViewBase;
public partial class View // Drawing APIs
{
/// <summary>
/// Draws a set of views.
/// </summary>
/// <param name="views">The peer views to draw.</param>
/// <param name="force">If <see langword="true"/>, <see cref="View.SetNeedsDraw()"/> will be called on each view to force it to be drawn.</param>
internal static void Draw (IEnumerable<View> 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 (those with Shadows) last to ensure they are drawn on top of the content.
Margin.DrawMargins (viewsArray);
}
/// <summary>
/// Draws the view if it needs to be drawn.
/// </summary>
/// <remarks>
/// <para>
/// The view will only be drawn if it is visible, and has any of <see cref="NeedsDraw"/>,
/// <see cref="SubViewNeedsDraw"/>,
/// or <see cref="NeedsLayout"/> set.
/// </para>
/// <para>
/// See the View Drawing Deep Dive for more information: <see href="https://gui-cs.github.io/Terminal.Gui/docs/drawing.html"/>.
/// </para>
/// </remarks>
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 ();
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
// 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);
}
}
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?.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 ();
}
/// <summary>
/// Causes <see cref="Margin"/>, <see cref="Border"/>, and <see cref="Padding"/> to be drawn.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Margin"/> is drawn in a separate pass if <see cref="ShadowStyle"/> is set.
/// </para>
/// </remarks>
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 ();
}
/// <summary>
/// Called when the View's Adornments are to be drawn. Prepares <see cref="View.LineCanvas"/>. If
/// <see cref="SuperViewRendersLineCanvas"/> is true, only the
/// <see cref="LineCanvas"/> of this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is
/// false (the default), this method will cause the <see cref="LineCanvas"/> be prepared to be rendered.
/// </summary>
/// <returns><see langword="true"/> to stop further drawing of the Adornments.</returns>
protected virtual bool OnDrawingAdornments () { return false; }
#endregion DrawAdornments
#region ClearViewport
internal void DoClearViewport (DrawContext? context = null)
{
if (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));
}
}
/// <summary>
/// Called when the <see cref="Viewport"/> is to be cleared.
/// </summary>
/// <returns><see langword="true"/> to stop further clearing.</returns>
protected virtual bool OnClearingViewport () { return false; }
/// <summary>Event invoked when the <see cref="Viewport"/> is to be cleared.</summary>
/// <remarks>
/// <para>Will be invoked before any subviews added with <see cref="Add(View)"/> have been drawn.</para>
/// <para>
/// Rect provides the view-relative rectangle describing the currently visible viewport into the
/// <see cref="View"/> .
/// </para>
/// </remarks>
public event EventHandler<DrawEventArgs>? ClearingViewport;
/// <summary>
/// Called when the <see cref="Viewport"/> has been cleared
/// </summary>
protected virtual void OnClearedViewport () { }
/// <summary>Event invoked when the <see cref="Viewport"/> has been cleared.</summary>
public event EventHandler<DrawEventArgs>? ClearedViewport;
/// <summary>Clears <see cref="Viewport"/> with the normal background.</summary>
/// <remarks>
/// <para>
/// If <see cref="ViewportSettings"/> has <see cref="ViewBase.ViewportSettingsFlags.ClearContentOnly"/> only
/// the portion of the content
/// area that is visible within the <see cref="View.Viewport"/> will be cleared. This is useful for views that have
/// a
/// content area larger than the Viewport (e.g. when <see cref="ViewportSettingsFlags.AllowNegativeLocation"/> is
/// enabled) and want
/// the area outside the content to be visually distinct.
/// </para>
/// </remarks>
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);
}
/// <summary>
/// Called when the <see cref="Text"/> of the View is to be drawn.
/// </summary>
/// <param name="context">The draw context to report drawn areas to.</param>
/// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns>
protected virtual bool OnDrawingText (DrawContext? context) { return false; }
/// <summary>
/// Called when the <see cref="Text"/> of the View is to be drawn.
/// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="Text"/>.</returns>
protected virtual bool OnDrawingText () { return false; }
/// <summary>Raised when the <see cref="Text"/> of the View is to be drawn.</summary>
/// <returns>
/// Set <see cref="CancelEventArgs.Cancel"/> to <see langword="true"/> to stop further drawing of
/// <see cref="Text"/>.
/// </returns>
public event EventHandler<DrawEventArgs>? DrawingText;
/// <summary>
/// Draws the <see cref="Text"/> of the View using the <see cref="TextFormatter"/>.
/// </summary>
/// <param name="context">The draw context to report drawn areas to.</param>
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.
SetSubViewNeedsDraw ();
}
/// <summary>
/// Called when the <see cref="Text"/> of the View has been drawn.
/// </summary>
protected virtual void OnDrewText () { }
/// <summary>Raised when the <see cref="Text"/> of the View has been drawn.</summary>
public event EventHandler? DrewText;
#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
}
/// <summary>
/// Called when the View's content is to be drawn. The default implementation does nothing.
/// </summary>
/// <param name="context">The draw context to report drawn areas to.</param>
/// <returns><see langword="true"/> to stop further drawing content.</returns>
protected virtual bool OnDrawingContent (DrawContext? context) { return false; }
/// <summary>
/// Called when the View's content is to be drawn. The default implementation does nothing.
/// </summary>
/// <returns><see langword="true"/> to stop further drawing content.</returns>
protected virtual bool OnDrawingContent () { return false; }
/// <summary>Raised when the View's content is to be drawn.</summary>
/// <remarks>
/// <para>Will be invoked before any subviews added with <see cref="Add(View)"/> have been drawn.</para>
/// <para>
/// Rect provides the view-relative rectangle describing the currently visible viewport into the
/// <see cref="View"/> .
/// </para>
/// </remarks>
public event EventHandler<DrawEventArgs>? DrawingContent;
#endregion DrawContent
#region DrawSubViews
private void DoDrawSubViews (DrawContext? context = null)
{
if (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);
}
/// <summary>
/// Called when the <see cref="SubViews"/> are to be drawn.
/// </summary>
/// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param>
/// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
protected virtual bool OnDrawingSubViews (DrawContext? context) { return false; }
/// <summary>
/// Called when the <see cref="SubViews"/> are to be drawn.
/// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="SubViews"/>.</returns>
protected virtual bool OnDrawingSubViews () { return false; }
/// <summary>Raised when the <see cref="SubViews"/> are to be drawn.</summary>
/// <remarks>
/// </remarks>
/// <returns>
/// Set <see cref="CancelEventArgs.Cancel"/> to <see langword="true"/> to stop further drawing of
/// <see cref="SubViews"/>.
/// </returns>
public event EventHandler<DrawEventArgs>? DrawingSubViews;
/// <summary>
/// Draws the <see cref="SubViews"/>.
/// </summary>
/// <param name="context">The draw context to report drawn areas to, or null if not tracking.</param>
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 (OnRenderingLineCanvas ())
{
return;
}
// TODO: Add event
RenderLineCanvas ();
}
/// <summary>
/// Called when the <see cref="View.LineCanvas"/> is to be rendered. See <see cref="RenderLineCanvas"/>.
/// </summary>
/// <returns><see langword="true"/> to stop further drawing of <see cref="LineCanvas"/>.</returns>
protected virtual bool OnRenderingLineCanvas () { return false; }
/// <summary>The canvas that any line drawing that is to be shared by subviews of this view should add lines to.</summary>
/// <remarks><see cref="Border"/> adds border lines to this LineCanvas.</remarks>
public LineCanvas LineCanvas { get; } = new ();
/// <summary>
/// Gets or sets whether this View will use it's SuperView's <see cref="LineCanvas"/> for rendering any
/// lines. If <see langword="true"/> the rendering of any borders drawn by this Frame will be done by its parent's
/// SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawingAdornments"/> method will
/// be
/// called to render the borders.
/// </summary>
public virtual bool SuperViewRendersLineCanvas { get; set; } = false;
/// <summary>
/// Causes the contents of <see cref="LineCanvas"/> to be drawn.
/// If <see cref="SuperViewRendersLineCanvas"/> is true, only the
/// <see cref="LineCanvas"/> of this view's subviews will be rendered. If <see cref="SuperViewRendersLineCanvas"/> is
/// false (the default), this method will cause the <see cref="LineCanvas"/> to be rendered.
/// </summary>
public void RenderLineCanvas ()
{
if (Driver is null)
{
return;
}
if (!SuperViewRendersLineCanvas && LineCanvas.Bounds != Rectangle.Empty)
{
foreach (KeyValuePair<Point, Cell?> 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
}
/// <summary>
/// Called when the View is completed drawing.
/// </summary>
/// <remarks>
/// The <paramref name="context"/> parameter provides the drawn region of the View.
/// </remarks>
protected virtual void OnDrawComplete (DrawContext? context) { }
/// <summary>Raised when the View is completed drawing.</summary>
/// <remarks>
/// </remarks>
public event EventHandler<DrawEventArgs>? 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 { get; set; } = Rectangle.Empty;
/// <summary>Gets or sets whether the view needs to be redrawn.</summary>
/// <remarks>
/// <para>
/// Will be <see langword="true"/> if the <see cref="NeedsLayout"/> property is <see langword="true"/> or if
/// any part of the view's <see cref="Viewport"/> needs to be redrawn.
/// </para>
/// <para>
/// Setting has no effect on <see cref="NeedsLayout"/>.
/// </para>
/// </remarks>
public bool NeedsDraw
{
get => Visible && (NeedsDrawRect != Rectangle.Empty || Margin?.NeedsDraw == true || Border?.NeedsDraw == true || Padding?.NeedsDraw == true);
set
{
if (value)
{
SetNeedsDraw ();
}
else
{
ClearNeedsDraw ();
}
}
}
/// <summary>Gets whether any SubViews need to be redrawn.</summary>
public bool SubViewNeedsDraw { get; private set; }
/// <summary>Sets that the <see cref="Viewport"/> of this View needs to be redrawn.</summary>
/// <remarks>
/// If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), this method
/// does nothing.
/// </remarks>
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);
}
/// <summary>Expands the area of this view needing to be redrawn to include <paramref name="viewPortRelativeRegion"/>.</summary>
/// <remarks>
/// <para>
/// The location of <paramref name="viewPortRelativeRegion"/> is relative to the View's <see cref="Viewport"/>.
/// </para>
/// <para>
/// If the view has not been initialized (<see cref="IsInitialized"/> is <see langword="false"/>), the area to be
/// redrawn will be the <paramref name="viewPortRelativeRegion"/>.
/// </para>
/// </remarks>
/// <param name="viewPortRelativeRegion">The <see cref="Viewport"/>relative region that needs to be redrawn.</param>
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 new snapshot collection - probably a stop gap
foreach (View subview in InternalSubViews.Snapshot ())
{
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);
}
}
}
/// <summary>Sets <see cref="SubViewNeedsDraw"/> to <see langword="true"/> for this View and all Superviews.</summary>
public void SetSubViewNeedsDraw ()
{
if (!Visible)
{
return;
}
SubViewNeedsDraw = true;
if (this is Adornment adornment)
{
adornment.Parent?.SetSubViewNeedsDraw ();
}
if (SuperView is { SubViewNeedsDraw: false })
{
SuperView.SetSubViewNeedsDraw ();
}
}
/// <summary>Clears <see cref="NeedsDraw"/> and <see cref="SubViewNeedsDraw"/>.</summary>
protected void ClearNeedsDraw ()
{
NeedsDrawRect = Rectangle.Empty;
SubViewNeedsDraw = false;
if (Margin is { } && (Margin.Thickness != Thickness.Empty || Margin.SubViewNeedsDraw || Margin.NeedsDraw))
{
Margin?.ClearNeedsDraw ();
}
if (Border is { } && (Border.Thickness != Thickness.Empty || Border.SubViewNeedsDraw || Border.NeedsDraw))
{
Border?.ClearNeedsDraw ();
}
if (Padding is { } && (Padding.Thickness != Thickness.Empty || Padding.SubViewNeedsDraw || Padding.NeedsDraw))
{
Padding?.ClearNeedsDraw ();
}
// There was multiple enumeration error here, so calling new snapshot collection - probably a stop gap
foreach (View subview in InternalSubViews.Snapshot ())
{
subview.ClearNeedsDraw ();
}
// DO NOT clear SuperView.SubViewNeedsDraw here!
// The SuperView will clear its own SubViewNeedsDraw after it has drawn all its subviews.
// If we clear it here, and this view has siblings that still need drawing, we'll break the draw system.
// This ensures LineCanvas' get redrawn
if (!SuperViewRendersLineCanvas)
{
LineCanvas.Clear ();
}
}
#endregion NeedsDraw
}