Fixes #4150 - Finish implementing Line View (#4260)

* Initial plan

* Add comprehensive analysis of Line implementation status

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Complete Line implementation with documentation, example, and tests

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add PR summary documenting Line implementation completion

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add comprehensive completion report for Issue 4150

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix Line rendering: use SuperView's LineCanvas instead of own

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Redesign Line to use Border instead of manual LineCanvas

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add Line.Style property to avoid BorderStyle conflict

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add SetWidth/SetHeight methods to preserve dimensions on Orientation change

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Implement CWP events for Width/Height properties; update Line to use events

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* WIP: Updating Line.
Cleaned up Layout tests.

* Made Height/Width non-nullable

* Add doWork stage to CWPPropertyHelper to execute between Changing and Changed events

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Move ViewLayoutEventTests to parallelizable tests without AutoInitShutdown

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Replace tracking fields with Length property for thread-safe Line implementation

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Fix orientation handling to preserve user-set dimensions in object initializers

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Simplify orientation handling with dimension swapping - all tests passing

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Add Length backing field and fix object initializer dimension handling

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Use CWP OnChanging events to manage dimensions instead of OnChanged

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Move LineTests to parallelizable; simplify tests with GetAnchor; fix Length property

Co-authored-by: tig <585482+tig@users.noreply.github.com>

* Code cleanup.

* Code cleanup.

* Update Terminal.Gui/ViewBase/View.Layout.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Layout.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Layout.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update Terminal.Gui/ViewBase/View.Layout.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Fixed nullable warning in test

* Removed PR files and updated copilot guidance

* Reverted .gitignore change

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: Tig <tig@users.noreply.github.com>
Co-authored-by: tig <585482+tig@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Copilot
2025-10-03 12:26:45 -07:00
committed by GitHub
parent 1822216130
commit fc9c40c2a0
16 changed files with 1251 additions and 257 deletions

View File

@@ -26,6 +26,7 @@ public static class CWPPropertyHelper
/// <param name="newValue">The proposed new property value, which may be null for nullable types.</param>
/// <param name="onChanging">The virtual method invoked before the change, returning true to cancel.</param>
/// <param name="changingEvent">The pre-change event raised to allow modification or cancellation.</param>
/// <param name="doWork">The action that performs the actual work of setting the property (e.g., updating backing field, calling related methods).</param>
/// <param name="onChanged">The virtual method invoked after the change.</param>
/// <param name="changedEvent">The post-change event raised to notify of the completed change.</param>
/// <param name="finalValue">
@@ -39,15 +40,15 @@ public static class CWPPropertyHelper
/// </exception>
/// <example>
/// <code>
/// string? current = null;
/// string? current = _schemeName;
/// string? proposed = "Base";
/// Func&lt;ValueChangingEventArgs&lt;string?&gt;, bool&gt; onChanging = args =&gt; false;
/// EventHandler&lt;ValueChangingEventArgs&lt;string?&gt;&gt;? changingEvent = null;
/// Action&lt;ValueChangedEventArgs&lt;string?&gt;&gt;? onChanged = args =&gt;
/// Console.WriteLine($"SchemeName changed to {args.NewValue ?? "none"}.");
/// EventHandler&lt;ValueChangedEventArgs&lt;string?&gt;&gt;? changedEvent = null;
/// Func&lt;ValueChangingEventArgs&lt;string?&gt;, bool&gt; onChanging = OnSchemeNameChanging;
/// EventHandler&lt;ValueChangingEventArgs&lt;string?&gt;&gt;? changingEvent = SchemeNameChanging;
/// Action&lt;string?&gt; doWork = value => _schemeName = value;
/// Action&lt;ValueChangedEventArgs&lt;string?&gt;&gt;? onChanged = OnSchemeNameChanged;
/// EventHandler&lt;ValueChangedEventArgs&lt;string?&gt;&gt;? changedEvent = SchemeNameChanged;
/// bool changed = CWPPropertyHelper.ChangeProperty(
/// current, proposed, onChanging, changingEvent, onChanged, changedEvent, out string? final);
/// current, proposed, onChanging, changingEvent, doWork, onChanged, changedEvent, out string? final);
/// </code>
/// </example>
public static bool ChangeProperty<T> (
@@ -55,6 +56,7 @@ public static class CWPPropertyHelper
T newValue,
Func<ValueChangingEventArgs<T>, bool> onChanging,
EventHandler<ValueChangingEventArgs<T>>? changingEvent,
Action<T> doWork,
Action<ValueChangedEventArgs<T>>? onChanged,
EventHandler<ValueChangedEventArgs<T>>? changedEvent,
out T finalValue
@@ -93,6 +95,10 @@ public static class CWPPropertyHelper
}
finalValue = args.NewValue;
// Do the work (set backing field, update related properties, etc.) BEFORE raising Changed events
doWork (finalValue);
ValueChangedEventArgs<T> changedArgs = new (currentValue, finalValue);
onChanged?.Invoke (changedArgs);
changedEvent?.Invoke (null, changedArgs);

View File

@@ -93,7 +93,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
/// <summary>Creates an Absolute <see cref="Dim"/> from the specified integer value.</summary>
/// <returns>The Absolute <see cref="Dim"/>.</returns>
/// <param name="size">The value to convert to the <see cref="Dim"/>.</param>
public static Dim? Absolute (int size) { return new DimAbsolute (size); }
public static Dim Absolute (int size) { return new DimAbsolute (size); }
/// <summary>
/// Creates a <see cref="Dim"/> object that automatically sizes the view to fit all the view's Content, SubViews, and/or Text.
@@ -119,7 +119,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
/// </param>
/// <param name="minimumContentDim">The minimum dimension the View's ContentSize will be constrained to.</param>
/// <param name="maximumContentDim">The maximum dimension the View's ContentSize will be fit to.</param>
public static Dim? Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim? minimumContentDim = null, Dim? maximumContentDim = null)
public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim? minimumContentDim = null, Dim? maximumContentDim = null)
{
return new DimAuto (
MinimumContentDim: minimumContentDim,
@@ -131,14 +131,14 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
/// Creates a <see cref="Dim"/> object that fills the dimension, leaving no margin.
/// </summary>
/// <returns>The Fill dimension.</returns>
public static Dim? Fill () { return new DimFill (0); }
public static Dim Fill () { return new DimFill (0); }
/// <summary>
/// Creates a <see cref="Dim"/> object that fills the dimension, leaving the specified margin.
/// </summary>
/// <returns>The Fill dimension.</returns>
/// <param name="margin">Margin to use.</param>
public static Dim? Fill (Dim margin) { return new DimFill (margin); }
public static Dim Fill (Dim margin) { return new DimFill (margin); }
/// <summary>
/// Creates a function <see cref="Dim"/> object that computes the dimension based on the passed view and by executing
@@ -172,7 +172,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
/// };
/// </code>
/// </example>
public static Dim? Percent (int percent, DimPercentMode mode = DimPercentMode.ContentSize)
public static Dim Percent (int percent, DimPercentMode mode = DimPercentMode.ContentSize)
{
ArgumentOutOfRangeException.ThrowIfNegative (percent, nameof (percent));

View File

@@ -1,6 +1,4 @@
#nullable enable
using System.ComponentModel;
namespace Terminal.Gui.ViewBase;
public partial class View
@@ -27,19 +25,15 @@ public partial class View
get => _schemeName;
set
{
bool changed = CWPPropertyHelper.ChangeProperty (
_schemeName,
value,
OnSchemeNameChanging,
SchemeNameChanging,
OnSchemeNameChanged,
SchemeNameChanged,
out string? finalValue);
if (changed)
{
_schemeName = finalValue;
}
CWPPropertyHelper.ChangeProperty (
_schemeName,
value,
OnSchemeNameChanging,
SchemeNameChanging,
newValue => _schemeName = newValue,
OnSchemeNameChanged,
SchemeNameChanged,
out string? _);
}
}
@@ -48,18 +42,13 @@ public partial class View
/// </summary>
/// <param name="args">The event arguments containing the current and proposed new scheme name.</param>
/// <returns>True to cancel the change, false to proceed.</returns>
protected virtual bool OnSchemeNameChanging (ValueChangingEventArgs<string?> args)
{
return false;
}
protected virtual bool OnSchemeNameChanging (ValueChangingEventArgs<string?> args) { return false; }
/// <summary>
/// Called after the <see cref="SchemeName"/> property changes, allowing subclasses to react to the change.
/// </summary>
/// <param name="args">The event arguments containing the old and new scheme name.</param>
protected virtual void OnSchemeNameChanged (ValueChangedEventArgs<string?> args)
{
}
protected virtual void OnSchemeNameChanged (ValueChangedEventArgs<string?> args) { }
/// <summary>
/// Raised before the <see cref="SchemeName"/> property changes, allowing handlers to modify or cancel the change.
@@ -115,7 +104,8 @@ public partial class View
/// <returns>The resolved scheme, never null.</returns>
/// <remarks>
/// <para>
/// This method uses the Cancellable Work Pattern (CWP) via <see cref="CWPWorkflowHelper.ExecuteWithResult{TResult}"/>
/// This method uses the Cancellable Work Pattern (CWP) via
/// <see cref="CWPWorkflowHelper.ExecuteWithResult{TResult}"/>
/// to allow customization or cancellation of scheme resolution through the <see cref="OnGettingScheme"/> method
/// and <see cref="GettingScheme"/> event.
/// </para>
@@ -135,13 +125,14 @@ public partial class View
ResultEventArgs<Scheme?> args = new ();
return CWPWorkflowHelper.ExecuteWithResult (
onMethod: args =>
{
bool cancelled = OnGettingScheme (out Scheme? newScheme);
args.Result = newScheme;
return cancelled;
},
eventHandler: GettingScheme,
args =>
{
bool cancelled = OnGettingScheme (out Scheme? newScheme);
args.Result = newScheme;
return cancelled;
},
GettingScheme,
args,
DefaultAction);
@@ -170,6 +161,7 @@ public partial class View
protected virtual bool OnGettingScheme (out Scheme? scheme)
{
scheme = null;
return false;
}
@@ -180,7 +172,6 @@ public partial class View
/// </summary>
public event EventHandler<ResultEventArgs<Scheme?>>? GettingScheme;
/// <summary>
/// Sets the scheme for the <see cref="View"/>, marking it as explicitly set.
/// </summary>
@@ -190,7 +181,8 @@ public partial class View
/// <para>
/// This method uses the Cancellable Work Pattern (CWP) via <see cref="CWPPropertyHelper.ChangeProperty{T}"/>
/// to allow customization or cancellation of the scheme change through the <see cref="OnSettingScheme"/> method
/// and <see cref="SchemeChanging"/> event. The <see cref="SchemeChanged"/> event is raised after a successful change.
/// and <see cref="SchemeChanging"/> event. The <see cref="SchemeChanged"/> event is raised after a successful
/// change.
/// </para>
/// <para>
/// If set to null, <see cref="HasScheme"/> will be false, and the view will inherit the scheme from its
@@ -216,21 +208,15 @@ public partial class View
/// </example>
public bool SetScheme (Scheme? scheme)
{
bool changed = CWPPropertyHelper.ChangeProperty (
_scheme,
scheme,
OnSettingScheme,
SchemeChanging,
OnSchemeChanged,
SchemeChanged,
out Scheme? finalValue);
if (changed)
{
_scheme = finalValue;
return true;
}
return false;
return CWPPropertyHelper.ChangeProperty (
_scheme,
scheme,
OnSettingScheme,
SchemeChanging,
newValue => _scheme = newValue,
OnSchemeChanged,
SchemeChanged,
out Scheme? _);
}
/// <summary>
@@ -238,19 +224,13 @@ public partial class View
/// </summary>
/// <param name="args">The event arguments containing the current and proposed new scheme.</param>
/// <returns>True to cancel the change, false to proceed.</returns>
protected virtual bool OnSettingScheme (ValueChangingEventArgs<Scheme?> args)
{
return false;
}
protected virtual bool OnSettingScheme (ValueChangingEventArgs<Scheme?> args) { return false; }
/// <summary>
/// Called after the scheme is set, allowing subclasses to react to the change.
/// </summary>
/// <param name="args">The event arguments containing the old and new scheme.</param>
protected virtual void OnSchemeChanged (ValueChangedEventArgs<Scheme?> args)
{
SetNeedsDraw ();
}
protected virtual void OnSchemeChanged (ValueChangedEventArgs<Scheme?> args) { SetNeedsDraw (); }
/// <summary>
/// Raised before the scheme is set, allowing handlers to modify or cancel the change.
@@ -269,5 +249,4 @@ public partial class View
/// <see cref="ValueChangedEventArgs{T}.NewValue"/>, which may be null.
/// </remarks>
public event EventHandler<ValueChangedEventArgs<Scheme?>>? SchemeChanged;
}

View File

@@ -56,6 +56,11 @@ public partial class View // Layout APIs
// This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
if (SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) }))
{
// BUGBUG: We set the internal fields here to avoid recursion. However, this means that
// BUGBUG: other logic in the property setters does not get executed. Specifically:
// BUGBUG: - Reset TextFormatter
// BUGBUG: - SetLayoutNeeded (not an issue as we explictly call Layout below)
// BUGBUG: - If we add property change events for X/Y/Width/Height they will not be invoked
// If Frame gets set, set all Pos/Dim to Absolute values.
_x = _frame!.Value.X;
_y = _frame!.Value.Y;
@@ -279,7 +284,7 @@ public partial class View // Layout APIs
}
}
private Dim? _height = Dim.Absolute (0);
private Dim _height = Dim.Absolute (0);
/// <summary>Gets or sets the height dimension of the view.</summary>
/// <value>The <see cref="Dim"/> object representing the height of the view (the number of rows).</value>
@@ -304,28 +309,67 @@ public partial class View // Layout APIs
/// <para>
/// Changing this property will cause <see cref="Frame"/> to be updated.
/// </para>
/// <para>The default value is <c>Dim.Sized (0)</c>.</para>
/// <para>
/// Setting this property raises pre- and post-change events via <see cref="CWPPropertyHelper"/>,
/// allowing customization or cancellation of the change. The <see cref="HeightChanging"/> event
/// is raised before the change, and <see cref="HeightChanged"/> is raised after.
/// </para>
/// <para>The default value is <c>Dim.Absolute (0)</c>.</para>
/// </remarks>
public Dim? Height
/// <seealso cref="HeightChanging"/>
/// <seealso cref="HeightChanged"/>
public Dim Height
{
get => VerifyIsInitialized (_height, nameof (Height));
set
{
if (Equals (_height, value))
{
return;
}
CWPPropertyHelper.ChangeProperty (
_height,
value,
OnHeightChanging,
HeightChanging,
newValue =>
{
_height = newValue;
_height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null");
// Reset TextFormatter - Will be recalculated in SetTextFormatterSize
TextFormatter.ConstrainToHeight = null;
PosDimSet ();
// Reset TextFormatter - Will be recalculated in SetTextFormatterSize
TextFormatter.ConstrainToHeight = null;
PosDimSet ();
},
OnHeightChanged,
HeightChanged,
out Dim _);
}
}
private Dim? _width = Dim.Absolute (0);
/// <summary>
/// Called before the <see cref="Height"/> property changes, allowing subclasses to cancel or modify the change.
/// </summary>
/// <param name="args">The event arguments containing the current and proposed new height.</param>
/// <returns>True to cancel the change, false to proceed.</returns>
protected virtual bool OnHeightChanging (ValueChangingEventArgs<Dim> args) { return false; }
/// <summary>
/// Called after the <see cref="Height"/> property changes, allowing subclasses to react to the change.
/// </summary>
/// <param name="args">The event arguments containing the old and new height.</param>
protected virtual void OnHeightChanged (ValueChangedEventArgs<Dim> args) { }
/// <summary>
/// Raised before the <see cref="Height"/> property changes, allowing handlers to modify or cancel the change.
/// </summary>
/// <remarks>
/// Set <see cref="ValueChangingEventArgs{T}.Handled"/> to true to cancel the change or modify
/// <see cref="ValueChangingEventArgs{T}.NewValue"/> to adjust the proposed value.
/// </remarks>
public event EventHandler<ValueChangingEventArgs<Dim>>? HeightChanging;
/// <summary>
/// Raised after the <see cref="Height"/> property changes, allowing handlers to react to the change.
/// </summary>
public event EventHandler<ValueChangedEventArgs<Dim>>? HeightChanged;
private Dim _width = Dim.Absolute (0);
/// <summary>Gets or sets the width dimension of the view.</summary>
/// <value>The <see cref="Dim"/> object representing the width of the view (the number of columns).</value>
@@ -351,26 +395,66 @@ public partial class View // Layout APIs
/// <para>
/// Changing this property will cause <see cref="Frame"/> to be updated.
/// </para>
/// <para>The default value is <c>Dim.Sized (0)</c>.</para>
/// <para>
/// Setting this property raises pre- and post-change events via <see cref="CWPPropertyHelper"/>,
/// allowing customization or cancellation of the change. The <see cref="WidthChanging"/> event
/// is raised before the change, and <see cref="WidthChanged"/> is raised after.
/// </para>
/// <para>The default value is <c>Dim.Absolute (0)</c>.</para>
/// </remarks>
public Dim? Width
/// <seealso cref="WidthChanging"/>
/// <seealso cref="WidthChanged"/>
public Dim Width
{
get => VerifyIsInitialized (_width, nameof (Width));
set
{
if (Equals (_width, value))
{
return;
}
CWPPropertyHelper.ChangeProperty (
_width,
value,
OnWidthChanging,
WidthChanging,
newValue =>
{
_width = newValue;
_width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null");
// Reset TextFormatter - Will be recalculated in SetTextFormatterSize
TextFormatter.ConstrainToWidth = null;
PosDimSet ();
// Reset TextFormatter - Will be recalculated in SetTextFormatterSize
TextFormatter.ConstrainToWidth = null;
PosDimSet ();
},
OnWidthChanged,
WidthChanged,
out Dim _);
}
}
/// <summary>
/// Called before the <see cref="Width"/> property changes, allowing subclasses to cancel or modify the change.
/// </summary>
/// <param name="args">The event arguments containing the current and proposed new width.</param>
/// <returns>True to cancel the change, false to proceed.</returns>
protected virtual bool OnWidthChanging (ValueChangingEventArgs<Dim> args) { return false; }
/// <summary>
/// Called after the <see cref="Width"/> property changes, allowing subclasses to react to the change.
/// </summary>
/// <param name="args">The event arguments containing the old and new width.</param>
protected virtual void OnWidthChanged (ValueChangedEventArgs<Dim> args) { }
/// <summary>
/// Raised before the <see cref="Width"/> property changes, allowing handlers to modify or cancel the change.
/// </summary>
/// <remarks>
/// Set <see cref="ValueChangingEventArgs{T}.Handled"/> to true to cancel the change or modify
/// <see cref="ValueChangingEventArgs{T}.NewValue"/> to adjust the proposed value.
/// </remarks>
public event EventHandler<ValueChangingEventArgs<Dim>>? WidthChanging;
/// <summary>
/// Raised after the <see cref="Width"/> property changes, allowing handlers to react to the change.
/// </summary>
public event EventHandler<ValueChangedEventArgs<Dim>>? WidthChanged;
#endregion Frame/Position/Dimension
#region Core Layout API
@@ -474,8 +558,7 @@ public partial class View // Layout APIs
{
Debug.Assert (_x is { });
Debug.Assert (_y is { });
Debug.Assert (_width is { });
Debug.Assert (_height is { });
CheckDimAuto ();
@@ -532,10 +615,15 @@ public partial class View // Layout APIs
if (Frame != newFrame)
{
// Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height
// This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
// Set the frame. Do NOT use `Frame = newFrame` as it overwrites X, Y, Width, and Height
// SetFrame will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
SetFrame (newFrame);
// BUGBUG: We set the internal fields here to avoid recursion. However, this means that
// BUGBUG: other logic in the property setters does not get executed. Specifically:
// BUGBUG: - Reset TextFormatter
// BUGBUG: - SetLayoutNeeded (not an issue as we explicitly call Layout below)
// BUGBUG: - If we add property change events for X/Y/Width/Height they will not be invoked
if (_x is PosAbsolute)
{
_x = Frame.X;
@@ -1152,13 +1240,15 @@ public partial class View // Layout APIs
}
/// <summary>
/// Gets the Views that are under <paramref name="screenLocation"/>, including Adornments. The list is ordered by depth. The
/// Gets the Views that are under <paramref name="screenLocation"/>, including Adornments. The list is ordered by
/// depth. The
/// deepest
/// View is at the end of the list (the top most View is at element 0).
/// </summary>
/// <param name="screenLocation">Screen-relative location.</param>
/// <param name="excludeViewportSettingsFlags">
/// If set, excludes Views that have the <see cref="ViewportSettingsFlags.Transparent"/> or <see cref="ViewportSettingsFlags.TransparentMouse"/>
/// If set, excludes Views that have the <see cref="ViewportSettingsFlags.Transparent"/> or
/// <see cref="ViewportSettingsFlags.TransparentMouse"/>
/// flags set in their ViewportSettings.
/// </param>
public static List<View?> GetViewsUnderLocation (in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags)
@@ -1219,21 +1309,24 @@ public partial class View // Layout APIs
/// <summary>
/// INTERNAL: Helper for GetViewsUnderLocation that starts from a given root view.
/// Gets the Views that are under <paramref name="screenLocation"/>, including Adornments. The list is ordered by depth. The
/// Gets the Views that are under <paramref name="screenLocation"/>, including Adornments. The list is ordered by
/// depth. The
/// deepest
/// View is at the end of the list (the topmost View is at element 0).
/// </summary>
/// <param name="root"></param>
/// <param name="screenLocation">Screen-relative location.</param>
/// <param name="excludeViewportSettingsFlags">
/// If set, excludes Views that have the <see cref="ViewportSettingsFlags.Transparent"/> or <see cref="ViewportSettingsFlags.TransparentMouse"/>
/// If set, excludes Views that have the <see cref="ViewportSettingsFlags.Transparent"/> or
/// <see cref="ViewportSettingsFlags.TransparentMouse"/>
/// flags set in their ViewportSettings.
/// </param>
internal static List<View?> GetViewsUnderLocation (View root, in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags)
{
List<View?> viewsUnderLocation = GetViewsAtLocation (root, screenLocation);
if (!excludeViewportSettingsFlags.HasFlag (ViewportSettingsFlags.Transparent) && !excludeViewportSettingsFlags.HasFlag (ViewportSettingsFlags.TransparentMouse))
if (!excludeViewportSettingsFlags.HasFlag (ViewportSettingsFlags.Transparent)
&& !excludeViewportSettingsFlags.HasFlag (ViewportSettingsFlags.TransparentMouse))
{
// Only filter views if we are excluding transparent views.
return viewsUnderLocation;
@@ -1241,8 +1334,7 @@ public partial class View // Layout APIs
// Remove all views that have an adornment with ViewportSettings.TransparentMouse; they are in the list
// because the point was in their adornment, and if the adornment is transparent, they should be removed.
viewsUnderLocation.RemoveAll (
v =>
viewsUnderLocation.RemoveAll (v =>
{
if (v is null or Adornment)
{
@@ -1277,6 +1369,7 @@ public partial class View // Layout APIs
return viewsUnderLocation;
}
/// <summary>
/// INTERNAL: Gets ALL Views (Subviews and Adornments) in the of <see cref="SuperView"/> hierarchcy that are at
/// <paramref name="location"/>,
@@ -1320,6 +1413,7 @@ public partial class View // Layout APIs
for (int i = currentView.InternalSubViews.Count - 1; i >= 0; i--)
{
View subview = currentView.InternalSubViews [i];
if (subview.Visible && subview.FrameToScreen ().Contains (location))
{
viewsToProcess.Push (subview);
@@ -1350,7 +1444,7 @@ public partial class View // Layout APIs
}
// Diagnostics to highlight when Width or Height is read before the view has been initialized
private Dim? VerifyIsInitialized (Dim? dim, string member)
private Dim VerifyIsInitialized (Dim dim, string member)
{
//#if DEBUG
// if (dim.ReferencesOtherViews () && !IsInitialized)

View File

@@ -38,9 +38,10 @@ public partial class View : IDisposable, ISupportInitializeNotification
#if DEBUG_IDISPOSABLE
WasDisposed = true;
// Safely remove any disposed views from the Instances list
List<View> itemsToKeep = Instances.Where (view => !view.WasDisposed).ToList ();
Instances = new ConcurrentBag<View> (itemsToKeep);
Instances = new (itemsToKeep);
#endif
}
@@ -108,9 +109,11 @@ public partial class View : IDisposable, ISupportInitializeNotification
/// <remarks>The id should be unique across all Views that share a SuperView.</remarks>
public string Id { get; set; } = "";
private IConsoleDriver? _driver = null;
private IConsoleDriver? _driver;
/// <summary>
/// INTERNAL: Use <see cref="Application.Driver"/> instead. Points to the current driver in use by the view, it is a convenience property for simplifying the development
/// INTERNAL: Use <see cref="Application.Driver"/> instead. Points to the current driver in use by the view, it is a
/// convenience property for simplifying the development
/// of new views.
/// </summary>
internal IConsoleDriver? Driver
@@ -121,6 +124,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
{
return _driver;
}
return Application.Driver;
}
set => _driver = value;
@@ -345,6 +349,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
{
// BUGBUG: Ideally we'd reset _previouslyFocused to the first focusable subview
_previouslyFocused = SubViews.FirstOrDefault (v => v.CanFocus);
if (HasFocus)
{
HasFocus = false;
@@ -449,10 +454,7 @@ public partial class View : IDisposable, ISupportInitializeNotification
/// <value>The title.</value>
public string Title
{
get
{
return _title;
}
get { return _title; }
set
{
#if DEBUG_IDISPOSABLE
@@ -530,7 +532,6 @@ public partial class View : IDisposable, ISupportInitializeNotification
/// </summary>
public static bool EnableDebugIDisposableAsserts { get; set; } = true;
/// <summary>
/// Gets whether <see cref="View.Dispose"/> was called on this view or not.
/// For debug purposes to verify objects are being disposed properly.

View File

@@ -1,33 +1,155 @@

#nullable enable
namespace Terminal.Gui.Views;
/// <summary>
/// Draws a single line using the <see cref="LineStyle"/> specified by <see cref="View.BorderStyle"/>.
/// Draws a single line using the <see cref="LineStyle"/> specified by <see cref="Line.Style"/>.
/// </summary>
/// <remarks>
/// <para>
/// <see cref="Line"/> is a <see cref="View"/> that renders a single horizontal or vertical line
/// using the <see cref="LineCanvas"/> system. Unlike <see cref="LineView"/>, which directly renders
/// runes, <see cref="Line"/> integrates with the LineCanvas to enable proper box-drawing character
/// selection and line intersection handling.
/// </para>
/// <para>
/// The line's appearance is controlled by the <see cref="Style"/> property, which supports
/// various line styles including Single, Double, Heavy, Rounded, Dashed, and Dotted.
/// </para>
/// <para>
/// Use the <see cref="Length"/> property to control the extent of the line regardless of its
/// <see cref="Orientation"/>. For horizontal lines, Length controls Width; for vertical lines,
/// it controls Height. The perpendicular dimension is always 1.
/// </para>
/// <para>
/// When multiple <see cref="Line"/> instances or other LineCanvas-aware views (like <see cref="Border"/>)
/// intersect, the LineCanvas automatically selects the appropriate box-drawing characters for corners,
/// T-junctions, and crosses.
/// </para>
/// <para>
/// <see cref="Line"/> sets <see cref="View.SuperViewRendersLineCanvas"/> to <see langword="true"/>,
/// meaning its parent view is responsible for rendering the line. This allows for proper intersection
/// handling when multiple views contribute lines to the same canvas.
/// </para>
/// </remarks>
/// <example>
/// <code>
/// // Create a horizontal line
/// var hLine = new Line { Y = 5 };
///
/// // Create a vertical line with specific length
/// var vLine = new Line { X = 10, Orientation = Orientation.Vertical, Length = 15 };
///
/// // Create a double-line style horizontal line
/// var doubleLine = new Line { Y = 10, Style = LineStyle.Double };
/// </code>
/// </example>
public class Line : View, IOrientation
{
private readonly OrientationHelper _orientationHelper;
private LineStyle _style = LineStyle.Single;
private Dim _length = Dim.Fill ();
/// <summary>Constructs a Line object.</summary>
/// <summary>
/// Constructs a new instance of the <see cref="Line"/> class with horizontal orientation.
/// </summary>
/// <remarks>
/// By default, a horizontal line fills the available width and has a height of 1.
/// The line style defaults to <see cref="LineStyle.Single"/>.
/// </remarks>
public Line ()
{
CanFocus = false;
base.SuperViewRendersLineCanvas = true;
_orientationHelper = new (this);
_orientationHelper.Orientation = Orientation.Horizontal;
OnOrientationChanged(Orientation);
// Set default dimensions for horizontal orientation
// Set Height first (this will update _length, but we'll override it next)
Height = 1;
// Now set Width and _length to Fill
_length = Dim.Fill ();
Width = _length;
}
/// <summary>
/// Gets or sets the length of the line along its orientation.
/// </summary>
/// <remarks>
/// <para>
/// This is the "source of truth" for the line's primary dimension.
/// For a horizontal line, Length controls Width.
/// For a vertical line, Length controls Height.
/// </para>
/// <para>
/// When Width or Height is set directly, Length is updated to match the primary dimension.
/// When Orientation changes, the appropriate dimension is set to Length and the perpendicular
/// dimension is set to 1.
/// </para>
/// <para>
/// This property provides a cleaner API for controlling the line's extent
/// without needing to know whether to use Width or Height.
/// </para>
/// </remarks>
public Dim Length
{
get => Orientation == Orientation.Horizontal ? Width : Height;
set
{
_length = value;
// Update the appropriate dimension based on current orientation
if (Orientation == Orientation.Horizontal)
{
Width = _length;
}
else
{
Height = _length;
}
}
}
/// <summary>
/// Gets or sets the style of the line. This controls the visual appearance of the line.
/// </summary>
/// <remarks>
/// Supports various line styles including Single, Double, Heavy, Rounded, Dashed, and Dotted.
/// Note: This is separate from <see cref="View.BorderStyle"/> to avoid conflicts with the View's Border.
/// </remarks>
public LineStyle Style
{
get => _style;
set
{
if (_style != value)
{
_style = value;
SetNeedsDraw ();
}
}
}
#region IOrientation members
/// <summary>
/// The direction of the line. If you change this you will need to manually update the Width/Height of the
/// control to cover a relevant area based on the new direction.
/// The direction of the line.
/// </summary>
/// <remarks>
/// <para>
/// When orientation changes, the appropriate dimension is set to <see cref="Length"/>
/// and the perpendicular dimension is set to 1.
/// </para>
/// <para>
/// For object initializers where dimensions are set before orientation:
/// <code>new Line { Height = 9, Orientation = Orientation.Vertical }</code>
/// Setting Height=9 updates Length to 9 (since default orientation is Horizontal and Height is perpendicular).
/// Then when Orientation is set to Vertical, Height is set to Length (9) and Width is set to 1,
/// resulting in the expected Width=1, Height=9.
/// </para>
/// </remarks>
public Orientation Orientation
{
get => _orientationHelper.Orientation;
@@ -36,48 +158,83 @@ public class Line : View, IOrientation
#pragma warning disable CS0067 // The event is never used
/// <inheritdoc/>
public event EventHandler<CancelEventArgs<Orientation>> OrientationChanging;
public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
/// <inheritdoc/>
public event EventHandler<EventArgs<Orientation>> OrientationChanged;
public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
#pragma warning restore CS0067 // The event is never used
/// <summary>Called when <see cref="Orientation"/> has changed.</summary>
/// <param name="newOrientation"></param>
/// <summary>
/// Called when <see cref="Orientation"/> has changed.
/// </summary>
/// <param name="newOrientation">The new orientation value.</param>
public void OnOrientationChanged (Orientation newOrientation)
{
switch (newOrientation)
// Set dimensions based on new orientation:
// - Primary dimension (along orientation) = Length
// - Perpendicular dimension = 1
if (newOrientation == Orientation.Horizontal)
{
case Orientation.Horizontal:
Height = 1;
Width = Dim.Fill ();
break;
case Orientation.Vertical:
Width = 1;
Height = Dim.Fill ();
break;
Width = _length;
Height = 1;
}
else
{
Height = _length;
Width = 1;
}
}
/// <inheritdoc/>
protected override bool OnWidthChanging (ValueChangingEventArgs<Dim> e)
{
// If horizontal, allow width changes and update _length
_length = e.NewValue;
if (Orientation == Orientation.Horizontal)
{
return base.OnWidthChanging (e);
}
// If vertical, keep width at 1 (don't allow changes to perpendicular dimension)
e.NewValue = 1;
return base.OnWidthChanging (e);
}
/// <inheritdoc/>
protected override bool OnHeightChanging (ValueChangingEventArgs<Dim> e)
{
// If vertical, allow height changes and update _length
_length = e.NewValue;
if (Orientation == Orientation.Vertical)
{
return base.OnHeightChanging (e);
}
e.NewValue = 1;
return base.OnHeightChanging (e);
}
#endregion
/// <inheritdoc/>
/// <remarks>
/// This method adds the line to the LineCanvas for rendering.
/// The actual rendering is performed by the parent view through <see cref="View.RenderLineCanvas"/>.
/// </remarks>
protected override bool OnDrawingContent ()
{
Point pos = ViewportToScreen (Viewport).Location;
int length = Orientation == Orientation.Horizontal ? Frame.Width : Frame.Height;
LineCanvas?.AddLine (
pos,
length,
Orientation,
BorderStyle
);
LineCanvas.AddLine (
pos,
length,
Orientation,
Style
);
//SuperView?.SetNeedsDraw ();
return true;
}
}