Forráskód Böngészése

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

* Initial plan

* Add comprehensive analysis of Line implementation status

Co-authored-by: tig <[email protected]>

* Complete Line implementation with documentation, example, and tests

Co-authored-by: tig <[email protected]>

* Add PR summary documenting Line implementation completion

Co-authored-by: tig <[email protected]>

* Add comprehensive completion report for Issue 4150

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

* Redesign Line to use Border instead of manual LineCanvas

Co-authored-by: tig <[email protected]>

* Add Line.Style property to avoid BorderStyle conflict

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

* 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 <[email protected]>

* Move ViewLayoutEventTests to parallelizable tests without AutoInitShutdown

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

* Simplify orientation handling with dimension swapping - all tests passing

Co-authored-by: tig <[email protected]>

* Add Length backing field and fix object initializer dimension handling

Co-authored-by: tig <[email protected]>

* Use CWP OnChanging events to manage dimensions instead of OnChanged

Co-authored-by: tig <[email protected]>

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

Co-authored-by: tig <[email protected]>

* Code cleanup.

* Code cleanup.

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

Co-authored-by: Copilot <[email protected]>

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

Co-authored-by: Copilot <[email protected]>

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

Co-authored-by: Copilot <[email protected]>

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

Co-authored-by: Copilot <[email protected]>

* Fixed nullable warning in test

* Removed PR files and updated copilot guidance

* Reverted .gitignore change

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: Tig <[email protected]>
Co-authored-by: tig <[email protected]>
Co-authored-by: Copilot <[email protected]>
Copilot 2 hónapja
szülő
commit
fc9c40c2a0

+ 7 - 12
AGENTS.md

@@ -102,18 +102,13 @@ This file provides instructions for GitHub Copilot when working with the Termina
 - Many existing unit tests are obtuse and not really unit tests. Anytime new tests are added or updated, strive to refactor the tests into more granular tests where each test covers the smallest area possible. 
 - Many existing unit tests in the `./Tests/UnitTests` project incorrectly require `Application.Init` and use `[AutoInitShutdown]`. Anytime new tests are added or updated, strive to remove these dependencies and make the tests parallelizable. This means not taking any dependency on static objects like `Application` and `ConfigurationManager`. 
 
-## Pull Request Checklist
-
-Before submitting a PR, ensure:
-- [ ] PR title: "Fixes #issue. Terse description."
-- [ ] Code follows style guidelines (`.editorconfig`)
-- [ ] Code follows design guidelines (`CONTRIBUTING.md`)
-- [ ] Ran `dotnet test` and all tests pass
-- [ ] Added/updated XML API documentation (`///` comments)
-- [ ] No new warnings generated
-- [ ] Checked for grammar/spelling errors
-- [ ] Conducted basic QA testing
-- [ ] Added/updated UICatalog scenario if applicable
+## Pull Request Guidelines
+
+- Titles should be of the form "Fixes #issue. Terse description." 
+- If the PR addresses multiple issues, use "Fixes #issue1, #issue2. Terse description."
+- First comment should include "- Fixes #issue" for each issue addressed. If an issue is only partially addressed, use "Partially addresses #issue".
+- First comment should include a thorough description of the change and any impact. 
+- Put temporary .md files in `/docfx/docs/drafts/` and remove before merging.
 
 ## Building and Running
 

+ 234 - 0
Examples/UICatalog/Scenarios/LineExample.cs

@@ -0,0 +1,234 @@
+namespace UICatalog.Scenarios;
+
+[ScenarioMetadata ("Line", "Demonstrates the Line view with LineCanvas integration.")]
+[ScenarioCategory ("Controls")]
+[ScenarioCategory ("Drawing")]
+[ScenarioCategory ("Adornments")]
+public class LineExample : Scenario
+{
+    public override void Main ()
+    {
+        Application.Init ();
+
+        var app = new Window
+        {
+            Title = GetQuitKeyAndName ()
+        };
+
+        // Section 1: Basic Lines
+        var basicLabel = new Label
+        {
+            X = 0,
+            Y = 0,
+            Text = "Basic Lines:"
+        };
+        app.Add (basicLabel);
+
+        // Horizontal line
+        var hLine = new Line
+        {
+            X = 0,
+            Y = 1,
+            Width = 30
+        };
+        app.Add (hLine);
+
+        // Vertical line
+        var vLine = new Line
+        {
+            X = 32,
+            Y = 0,
+            Height = 10,
+            Orientation = Orientation.Vertical
+        };
+        app.Add (vLine);
+
+        // Section 2: Different Line Styles
+        var stylesLabel = new Label
+        {
+            X = 0,
+            Y = 3,
+            Text = "Line Styles:"
+        };
+        app.Add (stylesLabel);
+
+        (LineStyle, string) [] styles = new []
+        {
+            (LineStyle.Single, "Single"),
+            (LineStyle.Double, "Double"),
+            (LineStyle.Heavy, "Heavy"),
+            (LineStyle.Rounded, "Rounded"),
+            (LineStyle.Dashed, "Dashed"),
+            (LineStyle.Dotted, "Dotted")
+        };
+
+        var yPos = 4;
+
+        foreach ((LineStyle style, string name) in styles)
+        {
+            app.Add (new Label { X = 0, Y = yPos, Width = 15, Text = name + ":" });
+            app.Add (new Line { X = 16, Y = yPos, Width = 14, Style = style });
+            yPos++;
+        }
+
+        // Section 3: Line Intersections
+        var intersectionLabel = new Label
+        {
+            X = 35,
+            Y = 3,
+            Text = "Line Intersections:"
+        };
+        app.Add (intersectionLabel);
+
+        // Create a grid of intersecting lines
+        var gridX = 35;
+        var gridY = 5;
+
+        // Horizontal lines in the grid
+        for (var i = 0; i < 5; i++)
+        {
+            app.Add (
+                     new Line
+                     {
+                         X = gridX,
+                         Y = gridY + i * 2,
+                         Width = 21,
+                         Style = LineStyle.Single
+                     });
+        }
+
+        // Vertical lines in the grid
+        for (var i = 0; i < 5; i++)
+        {
+            app.Add (
+                     new Line
+                     {
+                         X = gridX + i * 5,
+                         Y = gridY,
+                         Height = 9,
+                         Orientation = Orientation.Vertical,
+                         Style = LineStyle.Single
+                     });
+        }
+
+        // Section 4: Mixed Styles (shows how LineCanvas handles different line styles)
+        var mixedLabel = new Label
+        {
+            X = 60,
+            Y = 3,
+            Text = "Mixed Style Intersections:"
+        };
+        app.Add (mixedLabel);
+
+        // Double horizontal
+        app.Add (
+                 new Line
+                 {
+                     X = 60,
+                     Y = 5,
+                     Width = 20,
+                     Style = LineStyle.Double
+                 });
+
+        // Single vertical through double horizontal
+        app.Add (
+                 new Line
+                 {
+                     X = 70,
+                     Y = 4,
+                     Height = 3,
+                     Orientation = Orientation.Vertical,
+                     Style = LineStyle.Single
+                 });
+
+        // Heavy horizontal
+        app.Add (
+                 new Line
+                 {
+                     X = 60,
+                     Y = 8,
+                     Width = 20,
+                     Style = LineStyle.Heavy
+                 });
+
+        // Single vertical through heavy horizontal
+        app.Add (
+                 new Line
+                 {
+                     X = 70,
+                     Y = 7,
+                     Height = 3,
+                     Orientation = Orientation.Vertical,
+                     Style = LineStyle.Single
+                 });
+
+        // Section 5: Box Example (showing borders and lines working together)
+        var boxLabel = new Label
+        {
+            X = 0,
+            Y = 12,
+            Text = "Lines with Borders:"
+        };
+        app.Add (boxLabel);
+
+        var framedView = new FrameView
+        {
+            Title = "Frame",
+            X = 0,
+            Y = 13,
+            Width = 30,
+            Height = 8,
+            BorderStyle = LineStyle.Single
+        };
+
+        // Add a cross inside the frame
+        framedView.Add (
+                        new Line
+                        {
+                            X = 0,
+                            Y = 3,
+                            Width = Dim.Fill (),
+                            Style = LineStyle.Single
+                        });
+
+        framedView.Add (
+                        new Line
+                        {
+                            X = 14,
+                            Y = 0,
+                            Height = Dim.Fill (),
+                            Orientation = Orientation.Vertical,
+                            Style = LineStyle.Single
+                        });
+
+        app.Add (framedView);
+
+        // Section 6: Comparison with LineView
+        var comparisonLabel = new Label
+        {
+            X = 35,
+            Y = 15,
+            Text = "Line vs LineView Comparison:"
+        };
+        app.Add (comparisonLabel);
+
+        app.Add (new Label { X = 35, Y = 16, Text = "Line (uses LineCanvas):" });
+        app.Add (new Line { X = 35, Y = 17, Width = 20, Style = LineStyle.Single });
+
+        app.Add (new Label { X = 35, Y = 18, Text = "LineView (direct render):" });
+        app.Add (new LineView { X = 35, Y = 19, Width = 20 });
+
+        // Add help text
+        var helpLabel = new Label
+        {
+            X = Pos.Center (),
+            Y = Pos.AnchorEnd (1),
+            Text = "Line integrates with LineCanvas for automatic intersection handling"
+        };
+        app.Add (helpLabel);
+
+        Application.Run (app);
+        app.Dispose ();
+        Application.Shutdown ();
+    }
+}

+ 13 - 7
Terminal.Gui/App/CWP/CWPPropertyHelper.cs

@@ -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);

+ 5 - 5
Terminal.Gui/ViewBase/Layout/Dim.cs

@@ -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));
 

+ 35 - 56
Terminal.Gui/ViewBase/View.Drawing.Scheme.cs

@@ -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;
-
 }

+ 132 - 38
Terminal.Gui/ViewBase/View.Layout.cs

@@ -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;
+
+                                                  // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
+                                                  TextFormatter.ConstrainToHeight = null;
+                                                  PosDimSet ();
+                                              },
+                                              OnHeightChanged,
+                                              HeightChanged,
+                                              out Dim _);
+        }
+    }
 
-            _height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null");
+    /// <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; }
 
-            // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
-            TextFormatter.ConstrainToHeight = null;
+    /// <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) { }
 
-            PosDimSet ();
-        }
-    }
+    /// <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;
 
-    private Dim? _width = Dim.Absolute (0);
+    /// <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;
-            }
-
-            _width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null");
-
-            // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
-            TextFormatter.ConstrainToWidth = null;
-            PosDimSet ();
+            CWPPropertyHelper.ChangeProperty (
+                                              _width,
+                                              value,
+                                              OnWidthChanging,
+                                              WidthChanging,
+                                              newValue =>
+                                              {
+                                                  _width = newValue;
+
+                                                  // 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)

+ 9 - 8
Terminal.Gui/ViewBase/View.cs

@@ -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.

+ 184 - 27
Terminal.Gui/Views/Line.cs

@@ -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)
     {
+        // Set dimensions based on new orientation:
+        // - Primary dimension (along orientation) = Length
+        // - Perpendicular dimension = 1
+        if (newOrientation == Orientation.Horizontal)
+        {
+            Width = _length;
+            Height = 1;
+        }
+        else
+        {
+            Height = _length;
+            Width = 1;
+        }
+    }
 
-        switch (newOrientation)
+    /// <inheritdoc/>
+    protected override bool OnWidthChanging (ValueChangingEventArgs<Dim> e)
+    {
+        // If horizontal, allow width changes and update _length
+        _length = e.NewValue;
+        if (Orientation == Orientation.Horizontal)
         {
-            case Orientation.Horizontal:
-                Height = 1;
-                Width = Dim.Fill ();
+            return base.OnWidthChanging (e);
+        }
 
-                break;
-            case Orientation.Vertical:
-                Width = 1;
-                Height = Dim.Fill ();
+        // If vertical, keep width at 1 (don't allow changes to perpendicular dimension)
+        e.NewValue = 1;
 
-                break;
+        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;
     }
 }

+ 3 - 2
Terminal.sln

@@ -1,7 +1,7 @@
 
 Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 17
-VisualStudioVersion = 17.2.32427.441
+# Visual Studio Version 18
+VisualStudioVersion = 18.0.11018.127 d18.0
 MinimumVisualStudioVersion = 10.0.40219.1
 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Terminal.Gui", "Terminal.Gui\Terminal.Gui.csproj", "{00F366F8-DEE4-482C-B9FD-6DB0200B79E5}"
 EndProject
@@ -34,6 +34,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitHub", "GitHub", "{13BB2C
 		.github\workflows\api-docs.yml = .github\workflows\api-docs.yml
 		.github\workflows\build-release.yml = .github\workflows\build-release.yml
 		.github\workflows\check-duplicates.yml = .github\workflows\check-duplicates.yml
+		copilot-instructions.md = copilot-instructions.md
 		GitVersion.yml = GitVersion.yml
 		.github\workflows\integration-tests.yml = .github\workflows\integration-tests.yml
 		.github\workflows\publish.yml = .github\workflows\publish.yml

+ 8 - 4
Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs

@@ -418,7 +418,7 @@ public partial class DimAutoTests
         var otherView = new View
         {
             Text = "01234\n01234\n01234\n01234\n01234",
-            Width = Dim.Auto(),
+            Width = Dim.Auto (),
             Height = Dim.Auto ()
         };
         view.Add (otherView);
@@ -478,8 +478,8 @@ public partial class DimAutoTests
 
         var posViewView = new View
         {
-            X = Pos.Bottom(otherView),
-            Y = Pos.Right(otherView),
+            X = Pos.Bottom (otherView),
+            Y = Pos.Right (otherView),
             Width = 5,
             Height = 5,
         };
@@ -639,7 +639,11 @@ public partial class DimAutoTests
             Width = Dim.Auto (),
             Height = Dim.Auto (),
         };
-        var subview = new View { X = Pos.Func (_ => 20), Y = Pos.Func (_ => 25) };
+        var subview = new View
+        {
+            X = Pos.Func (_ => 20),
+            Y = Pos.Func (_ => 25)
+        };
         view.Add (subview);
 
         view.SetRelativeLayout (new (100, 100));

+ 19 - 0
Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs

@@ -243,6 +243,25 @@ public class FrameTests
         Assert.Equal (view.Height, frame.Height);
     }
 
+
+    [Fact]
+    public void Frame_Set_Sets_Viewport_Without_Layout ()
+    {
+        Rectangle frame = new (1, 2, 3, 4);
+
+        View v = new () { Frame = frame };
+        Assert.Equal (frame, v.Frame);
+
+        Assert.Equal (
+                      new (0, 0, frame.Width, frame.Height),
+                      v.Viewport
+                     ); // With Absolute Viewport *is* deterministic before Layout
+        Assert.Equal (Pos.Absolute (1), v.X);
+        Assert.Equal (Pos.Absolute (2), v.Y);
+        Assert.Equal (Dim.Absolute (3), v.Width);
+        Assert.Equal (Dim.Absolute (4), v.Height);
+    }
+
     [Fact]
     public void FrameChanged_Event_Raised_When_Frame_Changes ()
     {

+ 73 - 96
Tests/UnitTestsParallelizable/View/Layout/SetLayoutTests.cs → Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs

@@ -2,8 +2,61 @@
 
 namespace Terminal.Gui.LayoutTests;
 
-public class SetLayoutTests : GlobalTestSetup
+public class LayoutTests : GlobalTestSetup
 {
+    #region Constructor Tests
+
+    [Fact]
+    public void Constructor_Dispose_DoesNotThrow ()
+    {
+        var v = new View ();
+        v.Dispose ();
+    }
+
+    [Fact]
+    public void Constructor_Defaults_Are_Correct ()
+    {
+        // Tests defaults
+        View v = new ();
+        Assert.Equal (new (0, 0, 0, 0), v.Frame);
+        Assert.Equal (new (0, 0, 0, 0), v.Viewport);
+        Assert.Equal (Pos.Absolute (0), v.X);
+        Assert.Equal (Pos.Absolute (0), v.Y);
+        Assert.Equal (Dim.Absolute (0), v.Width);
+        Assert.Equal (Dim.Absolute (0), v.Height);
+
+        v.Layout ();
+        Assert.Equal (new (0, 0, 0, 0), v.Frame);
+        Assert.Equal (new (0, 0, 0, 0), v.Viewport);
+        Assert.Equal (Pos.Absolute (0), v.X);
+        Assert.Equal (Pos.Absolute (0), v.Y);
+        Assert.Equal (Dim.Absolute (0), v.Width);
+        Assert.Equal (Dim.Absolute (0), v.Height);
+    }
+
+    #endregion Constructor Tests
+
+    [Fact]
+    public void Set_All_Absolute_Sets_Correctly ()
+    {
+        Rectangle frame = new (1, 2, 3, 4);
+        View v = new () { X = frame.X, Y = frame.Y, Width = frame.Width, Height = frame.Height };
+        Assert.Equal (new (frame.X, frame.Y, 3, 4), v.Frame);
+        Assert.Equal (new (0, 0, 3, 4), v.Viewport);
+        Assert.Equal (Pos.Absolute (1), v.X);
+        Assert.Equal (Pos.Absolute (2), v.Y);
+        Assert.Equal (Dim.Absolute (3), v.Width);
+        Assert.Equal (Dim.Absolute (4), v.Height);
+
+        v.Layout ();
+        Assert.Equal (new (frame.X, frame.Y, 3, 4), v.Frame);
+        Assert.Equal (new (0, 0, 3, 4), v.Viewport);
+        Assert.Equal (Pos.Absolute (1), v.X);
+        Assert.Equal (Pos.Absolute (2), v.Y);
+        Assert.Equal (Dim.Absolute (3), v.Width);
+        Assert.Equal (Dim.Absolute (4), v.Height);
+    }
+
     [Fact]
     public void Add_Does_Not_Call_Layout ()
     {
@@ -28,12 +81,26 @@ public class SetLayoutTests : GlobalTestSetup
     }
 
     [Fact]
-    public void Change_Height_or_Width_MakesComputed ()
+    public void Set_X_Y_Does_Not_Impact_Dimensions ()
     {
-        var v = new View { Frame = Rectangle.Empty };
-        v.Height = Dim.Fill ();
-        v.Width = Dim.Fill ();
-        v.Dispose ();
+        // Tests that setting X & Y does not change Frame, Viewport, Width, or Height
+        Rectangle frame = new (1, 2, 3, 4);
+
+        View v = new () { X = frame.X, Y = frame.Y };
+        Assert.Equal (new (frame.X, frame.Y, 0, 0), v.Frame);
+        Assert.Equal (new (0, 0, 0, 0), v.Viewport);
+        Assert.Equal (Pos.Absolute (1), v.X);
+        Assert.Equal (Pos.Absolute (2), v.Y);
+        Assert.Equal (Dim.Absolute (0), v.Width);
+        Assert.Equal (Dim.Absolute (0), v.Height);
+
+        v.Layout ();
+        Assert.Equal (new (frame.X, frame.Y, 0, 0), v.Frame);
+        Assert.Equal (new (0, 0, 0, 0), v.Viewport);
+        Assert.Equal (Pos.Absolute (1), v.X);
+        Assert.Equal (Pos.Absolute (2), v.Y);
+        Assert.Equal (Dim.Absolute (0), v.Width);
+        Assert.Equal (Dim.Absolute (0), v.Height);
     }
 
     [Fact]
@@ -59,15 +126,6 @@ public class SetLayoutTests : GlobalTestSetup
         v.Dispose ();
     }
 
-    [Fact]
-    public void Change_X_or_Y_MakesComputed ()
-    {
-        var v = new View { Frame = Rectangle.Empty };
-        v.X = Pos.Center ();
-        v.Y = Pos.Center ();
-        v.Dispose ();
-    }
-
     [Fact]
     public void Change_X_Y_Height_Width_Absolute ()
     {
@@ -134,87 +192,6 @@ public class SetLayoutTests : GlobalTestSetup
         v.Dispose ();
     }
 
-    [Fact]
-    public void Constructor ()
-    {
-        var v = new View ();
-        v.Dispose ();
-
-        var frame = Rectangle.Empty;
-        v = new () { Frame = frame };
-        v.Layout ();
-        Assert.Equal (frame, v.Frame);
-
-        Assert.Equal (
-                      new (0, 0, frame.Width, frame.Height),
-                      v.Viewport
-                     ); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (0), v.X);
-        Assert.Equal (Pos.Absolute (0), v.Y);
-        Assert.Equal (Dim.Absolute (0), v.Width);
-        Assert.Equal (Dim.Absolute (0), v.Height);
-        v.Dispose ();
-
-        frame = new (1, 2, 3, 4);
-        v = new () { Frame = frame };
-        v.Layout ();
-        Assert.Equal (frame, v.Frame);
-
-        Assert.Equal (
-                      new (0, 0, frame.Width, frame.Height),
-                      v.Viewport
-                     ); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (1), v.X);
-        Assert.Equal (Pos.Absolute (2), v.Y);
-        Assert.Equal (Dim.Absolute (3), v.Width);
-        Assert.Equal (Dim.Absolute (4), v.Height);
-        v.Dispose ();
-
-        v = new () { Frame = frame, Text = "v" };
-        v.Layout ();
-        Assert.Equal (frame, v.Frame);
-
-        Assert.Equal (
-                      new (0, 0, frame.Width, frame.Height),
-                      v.Viewport
-                     ); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (1), v.X);
-        Assert.Equal (Pos.Absolute (2), v.Y);
-        Assert.Equal (Dim.Absolute (3), v.Width);
-        Assert.Equal (Dim.Absolute (4), v.Height);
-        v.Dispose ();
-
-        v = new () { X = frame.X, Y = frame.Y, Text = "v" };
-        v.Layout ();
-        Assert.Equal (new (frame.X, frame.Y, 0, 0), v.Frame);
-        Assert.Equal (new (0, 0, 0, 0), v.Viewport); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (1), v.X);
-        Assert.Equal (Pos.Absolute (2), v.Y);
-        Assert.Equal (Dim.Absolute (0), v.Width);
-        Assert.Equal (Dim.Absolute (0), v.Height);
-        v.Dispose ();
-
-        v = new ();
-        v.Layout ();
-        Assert.Equal (new (0, 0, 0, 0), v.Frame);
-        Assert.Equal (new (0, 0, 0, 0), v.Viewport); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (0), v.X);
-        Assert.Equal (Pos.Absolute (0), v.Y);
-        Assert.Equal (Dim.Absolute (0), v.Width);
-        Assert.Equal (Dim.Absolute (0), v.Height);
-        v.Dispose ();
-
-        v = new () { X = frame.X, Y = frame.Y, Width = frame.Width, Height = frame.Height };
-        v.Layout ();
-        Assert.Equal (new (frame.X, frame.Y, 3, 4), v.Frame);
-        Assert.Equal (new (0, 0, 3, 4), v.Viewport); // With Absolute Viewport *is* deterministic before Layout
-        Assert.Equal (Pos.Absolute (1), v.X);
-        Assert.Equal (Pos.Absolute (2), v.Y);
-        Assert.Equal (Dim.Absolute (3), v.Width);
-        Assert.Equal (Dim.Absolute (4), v.Height);
-        v.Dispose ();
-    }
-
     /// <summary>This is an intentionally obtuse test. See https://github.com/gui-cs/Terminal.Gui/issues/2461</summary>
     [Fact]
     public void Does_Not_Throw_If_Nested_SubViews_Ref_Topmost_SuperView ()

+ 253 - 0
Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs

@@ -0,0 +1,253 @@
+#nullable enable
+using UnitTests.Parallelizable;
+
+namespace Terminal.Gui.ViewLayoutEventTests;
+
+public class ViewLayoutEventTests : GlobalTestSetup
+{
+    [Fact]
+    public void View_WidthChanging_Event_Fires ()
+    {
+        var view = new View ();
+        bool eventFired = false;
+        Dim? oldValue = null;
+        Dim? newValue = null;
+
+        view.WidthChanging += (sender, args) =>
+        {
+            eventFired = true;
+            oldValue = args.CurrentValue;
+            newValue = args.NewValue;
+        };
+
+        view.Width = 10;
+
+        Assert.True (eventFired);
+        Assert.NotNull (oldValue);
+        Assert.NotNull (newValue);
+    }
+
+    [Fact]
+    public void View_WidthChanged_Event_Fires ()
+    {
+        var view = new View ();
+        bool eventFired = false;
+        Dim? oldValue = null;
+        Dim? newValue = null;
+
+        view.WidthChanged += (sender, args) =>
+        {
+            eventFired = true;
+            oldValue = args.OldValue;
+            newValue = args.NewValue;
+        };
+
+        view.Width = 10;
+
+        Assert.True (eventFired);
+        Assert.NotNull (oldValue);
+        Assert.NotNull (newValue);
+    }
+
+    [Fact]
+    public void View_WidthChanging_CanCancel ()
+    {
+        var view = new View ();
+        Dim? originalWidth = view.Width;
+
+        view.WidthChanging += (sender, args) =>
+        {
+            args.Handled = true; // Cancel the change
+        };
+
+        view.Width = 10;
+
+        // Width should not have changed
+        Assert.Equal (originalWidth, view.Width);
+    }
+
+    [Fact]
+    public void View_WidthChanging_CanModify ()
+    {
+        var view = new View ();
+
+        view.WidthChanging += (sender, args) =>
+        {
+            // Modify the proposed value
+            args.NewValue = 20;
+        };
+
+        view.Width = 10;
+
+        // Width should be 20 (the modified value), not 10
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (view);
+        container.Layout ();
+        Assert.Equal (20, view.Frame.Width);
+    }
+
+    [Fact]
+    public void View_HeightChanging_Event_Fires ()
+    {
+        var view = new View ();
+        bool eventFired = false;
+        Dim? oldValue = null;
+        Dim? newValue = null;
+
+        view.HeightChanging += (sender, args) =>
+        {
+            eventFired = true;
+            oldValue = args.CurrentValue;
+            newValue = args.NewValue;
+        };
+
+        view.Height = 10;
+
+        Assert.True (eventFired);
+        Assert.NotNull (oldValue);
+        Assert.NotNull (newValue);
+    }
+
+    [Fact]
+    public void View_HeightChanged_Event_Fires ()
+    {
+        var view = new View ();
+        bool eventFired = false;
+        Dim? oldValue = null;
+        Dim? newValue = null;
+
+        view.HeightChanged += (sender, args) =>
+        {
+            eventFired = true;
+            oldValue = args.OldValue;
+            newValue = args.NewValue;
+        };
+
+        view.Height = 10;
+
+        Assert.True (eventFired);
+        Assert.NotNull (oldValue);
+        Assert.NotNull (newValue);
+    }
+
+    [Fact]
+    public void View_HeightChanging_CanCancel ()
+    {
+        var view = new View ();
+        Dim? originalHeight = view.Height;
+
+        view.HeightChanging += (sender, args) =>
+        {
+            args.Handled = true; // Cancel the change
+        };
+
+        view.Height = 10;
+
+        // Height should not have changed
+        Assert.Equal (originalHeight, view.Height);
+    }
+
+    [Fact]
+    public void View_HeightChanging_CanModify ()
+    {
+        var view = new View ();
+
+        view.HeightChanging += (sender, args) =>
+        {
+            // Modify the proposed value
+            args.NewValue = 20;
+        };
+
+        view.Height = 10;
+
+        // Height should be 20 (the modified value), not 10
+        var container = new View { Width = 50, Height = 40 };
+        container.Add (view);
+        container.Layout ();
+        Assert.Equal (20, view.Frame.Height);
+    }
+
+    [Fact]
+    public void View_OnWidthChanging_CanCancel ()
+    {
+        var testView = new TestView ();
+        testView.CancelWidthChange = true;
+        Dim? originalWidth = testView.Width;
+
+        testView.Width = 10;
+
+        // Width should not have changed
+        Assert.Equal (originalWidth, testView.Width);
+    }
+
+    [Fact]
+    public void View_OnHeightChanging_CanCancel ()
+    {
+        var testView = new TestView ();
+        testView.CancelHeightChange = true;
+        Dim originalHeight = testView.Height;
+
+        testView.Height = 10;
+
+        // Height should not have changed
+        Assert.Equal (originalHeight, testView.Height);
+    }
+
+    [Fact]
+    public void View_WidthChanged_BackingFieldSetBeforeEvent ()
+    {
+        var view = new View ();
+        Dim? widthInChangedEvent = null;
+
+        view.WidthChanged += (sender, args) =>
+        {
+            // The backing field should already be set when Changed event fires
+            widthInChangedEvent = view.Width;
+        };
+
+        view.Width = 25;
+
+        // The width seen in the Changed event should be the new value
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (view);
+        container.Layout ();
+        Assert.Equal (25, view.Frame.Width);
+    }
+
+    [Fact]
+    public void View_HeightChanged_BackingFieldSetBeforeEvent ()
+    {
+        var view = new View ();
+        Dim? heightInChangedEvent = null;
+
+        view.HeightChanged += (sender, args) =>
+        {
+            // The backing field should already be set when Changed event fires
+            heightInChangedEvent = view.Height;
+        };
+
+        view.Height = 30;
+
+        // The height seen in the Changed event should be the new value
+        var container = new View { Width = 50, Height = 40 };
+        container.Add (view);
+        container.Layout ();
+        Assert.Equal (30, view.Frame.Height);
+    }
+
+    private class TestView : View
+    {
+        public bool CancelWidthChange { get; set; }
+        public bool CancelHeightChange { get; set; }
+
+        protected override bool OnWidthChanging (App.ValueChangingEventArgs<Dim> args)
+        {
+            return CancelWidthChange;
+        }
+
+        protected override bool OnHeightChanging (App.ValueChangingEventArgs<Dim> args)
+        {
+            return CancelHeightChange;
+        }
+    }
+}

+ 274 - 0
Tests/UnitTestsParallelizable/Views/LineTests.cs

@@ -0,0 +1,274 @@
+namespace Terminal.Gui.ViewsTests;
+
+public class LineTests
+{
+    [Fact]
+    public void Line_DefaultConstructor_Horizontal ()
+    {
+        var line = new Line ();
+
+        Assert.Equal (Orientation.Horizontal, line.Orientation);
+        Assert.Equal (Dim.Fill (), line.Width);
+        Assert.Equal (LineStyle.Single, line.Style);
+        Assert.True (line.SuperViewRendersLineCanvas);
+        Assert.False (line.CanFocus);
+
+        line.Layout ();
+        Assert.Equal (1, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_Horizontal_FillsWidth ()
+    {
+        var line = new Line { Orientation = Orientation.Horizontal };
+        var container = new View { Width = 50, Height = 10 };
+        container.Add (line);
+
+        container.Layout ();
+
+        Assert.Equal (50, line.Frame.Width);
+        Assert.Equal (1, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_Vertical_FillsHeight ()
+    {
+        var line = new Line { Orientation = Orientation.Vertical };
+        var container = new View { Width = 50, Height = 10 };
+        container.Add (line);
+
+        container.Layout ();
+
+        Assert.Equal (1, line.Frame.Width);
+        Assert.Equal (10, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_ChangeOrientation_UpdatesDimensions ()
+    {
+        var line = new Line { Orientation = Orientation.Horizontal };
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (line);
+        container.Layout ();
+
+        Assert.Equal (50, line.Frame.Width);
+        Assert.Equal (1, line.Frame.Height);
+
+        // Change to vertical
+        line.Orientation = Orientation.Vertical;
+        container.Layout ();
+
+        Assert.Equal (1, line.Frame.Width);
+        Assert.Equal (20, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_Style_CanBeSet ()
+    {
+        var line = new Line { Style = LineStyle.Double };
+
+        Assert.Equal (LineStyle.Double, line.Style);
+    }
+
+    [Theory]
+    [InlineData (LineStyle.Single)]
+    [InlineData (LineStyle.Double)]
+    [InlineData (LineStyle.Heavy)]
+    [InlineData (LineStyle.Rounded)]
+    [InlineData (LineStyle.Dashed)]
+    [InlineData (LineStyle.Dotted)]
+    public void Line_SupportsDifferentLineStyles (LineStyle style)
+    {
+        var line = new Line { Style = style };
+
+        Assert.Equal (style, line.Style);
+    }
+
+    [Fact]
+    public void Line_DrawsCalled_Successfully ()
+    {
+        var app = new Window ();
+        var line = new Line { Y = 1, Width = 10 };
+        app.Add (line);
+
+        app.BeginInit ();
+        app.EndInit ();
+        app.Layout ();
+
+        // Just verify the line can be drawn without errors
+        Exception exception = Record.Exception (() => app.Draw ());
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void Line_WithBorder_DrawsSuccessfully ()
+    {
+        var app = new Window { Width = 20, Height = 10, BorderStyle = LineStyle.Single };
+
+        // Add a line that intersects with the window border
+        var line = new Line { X = 5, Y = 0, Height = Dim.Fill (), Orientation = Orientation.Vertical };
+        app.Add (line);
+
+        app.BeginInit ();
+        app.EndInit ();
+        app.Layout ();
+
+        // Just verify the line and border can be drawn together without errors
+        Exception exception = Record.Exception (() => app.Draw ());
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void Line_MultipleIntersecting_DrawsSuccessfully ()
+    {
+        var app = new Window { Width = 30, Height = 15 };
+
+        // Create intersecting lines
+        var hLine = new Line { X = 5, Y = 5, Width = 15, Style = LineStyle.Single };
+
+        var vLine = new Line
+        {
+            X = 12, Y = 2, Height = 8, Orientation = Orientation.Vertical, Style = LineStyle.Single
+        };
+
+        app.Add (hLine, vLine);
+
+        app.BeginInit ();
+        app.EndInit ();
+        app.Layout ();
+
+        // Just verify multiple intersecting lines can be drawn without errors
+        Exception exception = Record.Exception (() => app.Draw ());
+        Assert.Null (exception);
+    }
+
+    [Fact]
+    public void Line_ExplicitWidthAndHeight_RespectValues ()
+    {
+        var line = new Line { Width = 10, Height = 1 };
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (line);
+
+        container.Layout ();
+
+        Assert.Equal (10, line.Frame.Width);
+        Assert.Equal (1, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_VerticalWithExplicitHeight_RespectValues ()
+    {
+        var line = new Line { Orientation = Orientation.Vertical };
+
+        // Set height AFTER orientation to avoid it being reset
+        line.Width = 1;
+        line.Height = 8;
+
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (line);
+
+        container.Layout ();
+
+        Assert.Equal (1, line.Frame.Width);
+        Assert.Equal (8, line.Frame.Height);
+    }
+
+    [Fact]
+    public void Line_SuperViewRendersLineCanvas_IsTrue ()
+    {
+        var line = new Line ();
+
+        Assert.True (line.SuperViewRendersLineCanvas);
+    }
+
+    [Fact]
+    public void Line_CannotFocus ()
+    {
+        var line = new Line ();
+
+        Assert.False (line.CanFocus);
+    }
+
+    [Fact]
+    public void Line_ImplementsIOrientation ()
+    {
+        var line = new Line ();
+
+        Assert.IsAssignableFrom<IOrientation> (line);
+    }
+
+    [Fact]
+    public void Line_Length_Get_ReturnsCorrectDimension ()
+    {
+        var line = new Line { Width = 20, Height = 1 };
+
+        // For horizontal, Length should be Width
+        line.Orientation = Orientation.Horizontal;
+        Assert.Equal (line.Width, line.Length);
+        Assert.Equal (1, line.Height.GetAnchor (0));
+
+        // For vertical, Length should be Height
+        line.Orientation = Orientation.Vertical;
+        Assert.Equal (line.Height, line.Length);
+        Assert.Equal (1, line.Width.GetAnchor (0));
+    }
+
+    [Fact]
+    public void Line_OrientationChange_SwapsDimensions ()
+    {
+        var line = new Line ();
+        var container = new View { Width = 50, Height = 20 };
+        container.Add (line);
+
+        // Start horizontal with custom dimensions
+        line.Orientation = Orientation.Horizontal;
+        line.Width = 30;
+        line.Height = 1;
+        container.Layout ();
+
+        Assert.Equal (30, line.Frame.Width);
+        Assert.Equal (1, line.Frame.Height);
+
+        // Change to vertical - dimensions should swap
+        line.Orientation = Orientation.Vertical;
+        container.Layout ();
+
+        Assert.Equal (1, line.Frame.Width);
+        Assert.Equal (30, line.Frame.Height); // Width became Height
+    }
+
+    [Fact]
+    public void Line_Dimensions_WorkSameAsInitializers ()
+    {
+        // Object initializers work same as sequential assignment
+        // Test: new Line { Width = 15, Orientation = Orientation.Horizontal }
+        // Expected: Width=15, Height=1
+        Line line = new () { Width = 15, Orientation = Orientation.Horizontal };
+
+        Assert.Equal (15, line.Width.GetAnchor (0));
+        Assert.Equal (1, line.Height.GetAnchor (0));
+        Assert.Equal (line.Length, line.Width); // Length should be Width for horizontal
+
+        line = new ();
+        line.Width = 15;
+        line.Orientation = Orientation.Horizontal;
+        Assert.Equal (15, line.Width.GetAnchor (0));
+        Assert.Equal (1, line.Height.GetAnchor (0));
+        Assert.Equal (line.Length, line.Width); // Length should be Width for horizontal
+
+        // Test: new Line { Height = 9, Orientation = Orientation.Vertical }
+        // Expected: Width=1, Height=9
+        line = new() { Height = 9, Orientation = Orientation.Vertical };
+
+        Assert.Equal (1, line.Width.GetAnchor (0));
+        Assert.Equal (9, line.Height.GetAnchor (0));
+        Assert.Equal (line.Length, line.Height); // Length should be Height for vertical
+
+        line = new ();
+        line.Height = 9;
+        line.Orientation = Orientation.Vertical;
+        Assert.Equal (1, line.Width.GetAnchor (0));
+        Assert.Equal (9, line.Height.GetAnchor (0));
+        Assert.Equal (line.Length, line.Height); // Length should be Height for vertical
+    }
+}

BIN
local_packages/Terminal.Gui.2.0.0.nupkg


BIN
local_packages/Terminal.Gui.2.0.0.snupkg