#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 ();
foreach (View view in viewsArray)
{
if (force)
{
view.SetNeedsDraw ();
}
view.Draw ();
}
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 ()
{
if (!CanBeVisible (this))
{
return;
}
Region? saved = GetClip ();
// TODO: This can be further optimized by checking NeedsDraw below and only clearing, drawing text, drawing content, etc. if it is true.
if (NeedsDraw || SubViewNeedsDraw)
{
// Draw the Border and Padding.
// We clip to the frame to prevent drawing outside the frame.
saved = ClipFrame ();
DoDrawBorderAndPadding ();
SetClip (saved);
// Draw the content within 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
saved = ClipViewport ();
// Clear the viewport
// TODO: Simplify/optimize SetAttribute system.
DoSetAttribute ();
DoClearViewport ();
// Draw the subviews only if needed
if (SubViewNeedsDraw)
{
DoSetAttribute ();
DoDrawSubviews ();
}
// Draw the text
DoSetAttribute ();
DoDrawText ();
// Draw the content
DoSetAttribute ();
DoDrawContent ();
// Restore the clip before rendering the line canvas and adornment subviews
// because they may draw outside the viewport.
SetClip (saved);
saved = ClipFrame ();
// Draw the line canvas
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.
DoDrawBorderAndPaddingSubViews ();
// Advance the diagnostics draw indicator
Border?.AdvanceDrawIndicator ();
ClearNeedsDraw ();
}
// This causes the Margin to be drawn in a second pass
// PERFORMANCE: If there is a Margin, it will be redrawn each iteration of the main loop.
Margin?.CacheClip ();
// We're done drawing
DoDrawComplete ();
// QUESTION: Should this go before DoDrawComplete? What is more correct?
SetClip (saved);
// Exclude this view (not including Margin) from the Clip
if (this is not Adornment)
{
Rectangle borderFrame = FrameToScreen ();
if (Border is { })
{
borderFrame = Border.FrameToScreen ();
}
ExcludeFromClip (borderFrame);
}
}
#region DrawAdornments
private void DoDrawBorderAndPaddingSubViews ()
{
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?.ClipFrame ();
Border?.DoDrawSubviews ();
SetClip (saved);
}
if (Padding?.Subviews is { } && Padding.Thickness != Thickness.Empty)
{
foreach (View subview in Padding.Subviews)
{
subview.SetNeedsDraw ();
}
Region? saved = Padding?.ClipFrame ();
Padding?.DoDrawSubviews ();
SetClip (saved);
}
}
private void DoDrawBorderAndPadding ()
{
if (Margin?.NeedsLayout == true)
{
Margin.NeedsLayout = false;
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 ();
}
if (OnDrawingBorderAndPadding ())
{
return;
}
// TODO: add event.
DrawBorderAndPadding ();
}
///
/// Causes and to be drawn.
///
///
///
/// is drawn in a separate pass.
///
///
public void DrawBorderAndPadding ()
{
// 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 ();
}
}
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 OnDrawingBorderAndPadding () { 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
private void DoClearViewport ()
{
if (OnClearingViewport ())
{
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 () { 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 ()
{
if (OnDrawingText ())
{
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 () { 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;
}
// 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 ()
{
if (OnDrawingContent ())
{
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 () { 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 ()
{
if (OnDrawingSubviews ())
{
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 () { 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)
{
return;
}
// Draw the subviews in reverse order to leverage clipping.
foreach (View view in _subviews.Where (view => view.Visible).Reverse ())
{
// TODO: HACK - This enables auto line join to work, but is brute force.
if (view.SuperViewRendersLineCanvas)
{
view.SetNeedsDraw ();
}
view.Draw ();
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 ()
{
OnDrawComplete ();
DrawComplete?.Invoke (this, new (Viewport, Viewport));
// 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: 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
}