namespace Terminal.Gui;
public partial class View
{
// The view-relative region that needs to be redrawn. Marked internal for unit tests.
internal Rectangle _needsDisplayRect = Rectangle.Empty;
private ColorScheme _colorScheme;
/// The color scheme for this view, if it is not defined, it returns the 's color scheme.
public virtual ColorScheme ColorScheme
{
get
{
if (_colorScheme is null)
{
return SuperView?.ColorScheme;
}
return _colorScheme;
}
set
{
if (_colorScheme != value)
{
_colorScheme = value;
SetNeedsDisplay ();
}
}
}
/// 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 the view needs to be redrawn.
public bool NeedsDisplay
{
get => _needsDisplayRect != Rectangle.Empty;
set
{
if (value)
{
SetNeedsDisplay ();
}
else
{
ClearNeedsDisplay ();
}
}
}
/// Gets whether any Subviews need to be redrawn.
public bool SubViewNeedsDisplay { get; private set; }
///
/// Gets or sets whether this View will use it's SuperView's for rendering any border
/// 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;
/// Draws the specified character in the specified viewport-relative column and row of the View.
///
/// The top-left corner of the visible content area is ViewPort.Location.
///
/// Column (viewport-relative).
/// Row (viewport-relative).
/// Ch.
public void AddRune (int col, int row, Rune ch)
{
if (row < 0 || col < 0)
{
return;
}
// BUGBUG: This should be Viewport.Size
if (row > _frame.Height - 1 || col > _frame.Width - 1)
{
return;
}
Move (col, row);
Driver.AddRune (ch);
}
/// Clears with the normal background.
///
public void Clear () { Clear (Viewport); }
/// Clears the specified -relative rectangle with the normal background.
///
/// The Viewport-relative rectangle to clear.
public void Clear (Rectangle viewport)
{
if (Driver is null)
{
return;
}
Attribute prev = Driver.SetAttribute (GetNormalColor ());
// Clamp the region to the bounds of the view
viewport = Rectangle.Intersect (viewport, Viewport);
Driver.FillRect (ViewportToScreen (viewport));
Driver.SetAttribute (prev);
}
/// Expands the 's clip region to include .
///
/// The current screen-relative clip region, which can be then re-applied by setting
/// .
///
///
///
/// If and do not intersect, the clip region will be set to
/// .
///
///
public Rectangle ClipToViewport ()
{
if (Driver is null)
{
return Rectangle.Empty;
}
Rectangle previous = Driver.Clip;
Driver.Clip = Rectangle.Intersect (previous, ViewportToScreen (Viewport));
return previous;
}
///
/// Draws the view. Causes the following virtual methods to be called (along with their related events):
/// , .
///
///
///
/// Always use (view-relative) when calling , NOT
/// (superview-relative).
///
///
/// Views should set the color that they want to use on entry, as otherwise this will inherit the last color that
/// was set globally on the driver.
///
///
/// Overrides of must ensure they do not set Driver.Clip to a clip
/// region larger than the property, as this will cause the driver to clip the entire
/// region.
///
///
public void Draw ()
{
if (!CanBeVisible (this))
{
return;
}
OnDrawAdornments ();
Rectangle prevClip = ClipToViewport ();
if (ColorScheme is { })
{
//Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
Driver?.SetAttribute (GetNormalColor ());
}
// Invoke DrawContentEvent
var dev = new DrawEventArgs (Viewport);
DrawContent?.Invoke (this, dev);
if (!dev.Cancel)
{
OnDrawContent (Viewport);
}
if (Driver is { })
{
Driver.Clip = prevClip;
}
OnRenderLineCanvas ();
// Invoke DrawContentCompleteEvent
OnDrawContentComplete (Viewport);
// BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details.
ClearLayoutNeeded ();
ClearNeedsDisplay ();
}
/// 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 DrawContent;
/// Event invoked when the content area of the View is completed drawing.
///
/// Will be invoked after any subviews removed with have been completed drawing.
///
/// Rect provides the view-relative rectangle describing the currently visible viewport into the
/// .
///
///
public event EventHandler DrawContentComplete;
/// Utility function to draw strings that contain a hotkey.
/// String to display, the hotkey specifier before a letter flags the next letter as the hotkey.
/// Hot color.
/// Normal color.
///
///
/// The hotkey is any character following the hotkey specifier, which is the underscore ('_') character by
/// default.
///
/// The hotkey specifier can be changed via
///
public void DrawHotString (string text, Attribute hotColor, Attribute normalColor)
{
Rune hotkeySpec = HotKeySpecifier == (Rune)0xffff ? (Rune)'_' : HotKeySpecifier;
Application.Driver.SetAttribute (normalColor);
foreach (Rune rune in text.EnumerateRunes ())
{
if (rune == new Rune (hotkeySpec.Value))
{
Application.Driver.SetAttribute (hotColor);
continue;
}
Application.Driver.AddRune (rune);
Application.Driver.SetAttribute (normalColor);
}
}
///
/// Utility function to draw strings that contains a hotkey using a and the "focused"
/// state.
///
/// String to display, the underscore before a letter flags the next letter as the hotkey.
///
/// If set to this uses the focused colors from the color scheme, otherwise
/// the regular ones.
///
/// The color scheme to use.
public void DrawHotString (string text, bool focused, ColorScheme scheme)
{
if (focused)
{
DrawHotString (text, scheme.HotFocus, scheme.Focus);
}
else
{
DrawHotString (
text,
Enabled ? scheme.HotNormal : scheme.Disabled,
Enabled ? scheme.Normal : scheme.Disabled
);
}
}
/// Determines the current based on the value.
///
/// if is or
/// if is . If it's
/// overridden can return other values.
///
public virtual Attribute GetFocusColor ()
{
ColorScheme cs = ColorScheme;
if (ColorScheme is null)
{
cs = new ();
}
return Enabled ? cs.Focus : cs.Disabled;
}
/// Determines the current based on the value.
///
/// if is or
/// if is . If it's
/// overridden can return other values.
///
public virtual Attribute GetHotNormalColor ()
{
ColorScheme cs = ColorScheme;
if (ColorScheme is null)
{
cs = new ();
}
return Enabled ? cs.HotNormal : cs.Disabled;
}
/// Determines the current based on the value.
///
/// if is or
/// if is . If it's
/// overridden can return other values.
///
public virtual Attribute GetNormalColor ()
{
ColorScheme cs = ColorScheme;
if (ColorScheme is null)
{
cs = new ();
}
return Enabled ? cs.Normal : cs.Disabled;
}
/// This moves the cursor to the specified view-relative column and row in the view.
///
/// The top-left corner of the visible content area is ViewPort.Location.
///
/// Column (viewport-relative).
/// Row (viewport-relative).
public void Move (int col, int row)
{
if (Driver is null || Driver?.Rows == 0)
{
return;
}
Rectangle screen = ViewportToScreen (new (col, row, 0, 0));
// TODO: Clamp this to stay within the View's Viewport
Driver?.Move (screen.X, screen.Y);
}
// TODO: Make this cancelable
///
/// 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.
///
///
public virtual bool OnDrawAdornments ()
{
if (!IsInitialized)
{
return false;
}
// Each of these renders lines to either this View's LineCanvas
// Those lines will be finally rendered in OnRenderLineCanvas
Margin?.OnDrawContent (Margin.Viewport);
Border?.OnDrawContent (Border.Viewport);
Padding?.OnDrawContent (Padding.Viewport);
return true;
}
///
/// Draws the view's content, including Subviews.
///
///
///
/// The parameter is provided as a convenience; it has the same values as the
/// property.
///
///
/// The Location and Size indicate what part of the View's virtual content area, defined
/// by , is visible and should be drawn. The coordinates taken by and
/// are relative to , thus if ViewPort.Location.Y is 5
/// the 5th row of the content should be drawn using MoveTo (x, 5).
///
///
/// If is larger than ViewPort.Size drawing code should use
/// to constrain drawing for better performance.
///
///
/// The may define smaller area than ; complex drawing code can be more
/// efficient by using to constrain drawing for better performance.
///
///
/// Overrides should loop through the subviews and call .
///
///
///
/// The rectangle describing the currently visible viewport into the ; has the same value as
/// .
///
public virtual void OnDrawContent (Rectangle viewport)
{
if (NeedsDisplay)
{
if (SuperView is { })
{
Clear (viewport);
}
if (!string.IsNullOrEmpty (TextFormatter.Text))
{
if (TextFormatter is { })
{
TextFormatter.NeedsFormat = true;
}
}
// This should NOT clear
TextFormatter?.Draw (
ViewportToScreen (viewport),
HasFocus ? GetFocusColor () : GetNormalColor (),
HasFocus ? ColorScheme.HotFocus : GetHotNormalColor (),
Rectangle.Empty
);
SetSubViewNeedsDisplay ();
}
// TODO: Move drawing of subviews to a separate OnDrawSubviews virtual method
// Draw subviews
// TODO: Implement OnDrawSubviews (cancelable);
if (_subviews is { } && SubViewNeedsDisplay)
{
IEnumerable subviewsNeedingDraw = _subviews.Where (
view => view.Visible
&& (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded)
);
foreach (View view in subviewsNeedingDraw)
{
//view.Frame.IntersectsWith (bounds)) {
// && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
if (view.LayoutNeeded)
{
view.LayoutSubviews ();
}
// Draw the subview
// Use the view's bounds (view-relative; Location will always be (0,0)
//if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
view.Draw ();
//}
}
}
}
///
/// Called after to enable overrides.
///
///
/// The viewport-relative rectangle describing the currently visible viewport into the
///
///
public virtual void OnDrawContentComplete (Rectangle viewport) { DrawContentComplete?.Invoke (this, new (viewport)); }
// TODO: Make this cancelable
///
/// Renders . 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 virtual bool OnRenderLineCanvas ()
{
if (!IsInitialized)
{
return false;
}
// 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
Driver.SetAttribute (p.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.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
Driver.SetAttribute (p.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.Rune);
}
LineCanvas.Clear ();
}
return true;
}
/// Sets the area of this view needing to be redrawn to .
///
/// If the view has not been initialized ( is ), this method
/// does nothing.
///
public void SetNeedsDisplay ()
{
if (IsInitialized)
{
SetNeedsDisplay (Viewport);
}
}
/// Expands the area of this view needing to be redrawn to include .
///
///
/// The location of are relative to the View's content, bound by Size.Empty and
/// .
///
///
/// If the view has not been initialized ( is ), the area to be
/// redrawn will be the .
///
///
/// The content-relative region that needs to be redrawn.
public virtual void SetNeedsDisplay (Rectangle region)
{
if (!IsInitialized)
{
_needsDisplayRect = region;
return;
}
if (_needsDisplayRect.IsEmpty)
{
_needsDisplayRect = region;
}
else
{
int x = Math.Min (_needsDisplayRect.X, region.X);
int y = Math.Min (_needsDisplayRect.Y, region.Y);
int w = Math.Max (_needsDisplayRect.Width, region.Width);
int h = Math.Max (_needsDisplayRect.Height, region.Height);
_needsDisplayRect = new (x, y, w, h);
}
_superView?.SetSubViewNeedsDisplay ();
Margin?.SetNeedsDisplay (Margin.Viewport);
Border?.SetNeedsDisplay (Border.Viewport);
Padding?.SetNeedsDisplay (Padding.Viewport);
foreach (View subview in Subviews)
{
if (subview.Frame.IntersectsWith (region))
{
Rectangle subviewRegion = Rectangle.Intersect (subview.Frame, region);
subviewRegion.X -= subview.Frame.X;
subviewRegion.Y -= subview.Frame.Y;
subview.SetNeedsDisplay (subviewRegion);
}
}
}
/// Indicates that any Subviews (in the list) need to be repainted.
public void SetSubViewNeedsDisplay ()
{
SubViewNeedsDisplay = true;
if (_superView is { } && !_superView.SubViewNeedsDisplay)
{
_superView.SetSubViewNeedsDisplay ();
}
}
/// Clears and .
protected virtual void ClearNeedsDisplay ()
{
_needsDisplayRect = Rectangle.Empty;
SubViewNeedsDisplay = false;
}
}