diff --git a/AGENTS.md b/AGENTS.md index 902716bc7..eac87ad9c 100644 --- a/AGENTS.md +++ b/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 +## Pull Request Guidelines -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 +- 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 diff --git a/Examples/UICatalog/Scenarios/LineExample.cs b/Examples/UICatalog/Scenarios/LineExample.cs new file mode 100644 index 000000000..e0dc8d0ad --- /dev/null +++ b/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 (); + } +} diff --git a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs index fe8ec485c..cfe7d68a0 100644 --- a/Terminal.Gui/App/CWP/CWPPropertyHelper.cs +++ b/Terminal.Gui/App/CWP/CWPPropertyHelper.cs @@ -26,6 +26,7 @@ public static class CWPPropertyHelper /// The proposed new property value, which may be null for nullable types. /// The virtual method invoked before the change, returning true to cancel. /// The pre-change event raised to allow modification or cancellation. + /// The action that performs the actual work of setting the property (e.g., updating backing field, calling related methods). /// The virtual method invoked after the change. /// The post-change event raised to notify of the completed change. /// @@ -39,15 +40,15 @@ public static class CWPPropertyHelper /// /// /// - /// string? current = null; + /// string? current = _schemeName; /// string? proposed = "Base"; - /// Func<ValueChangingEventArgs<string?>, bool> onChanging = args => false; - /// EventHandler<ValueChangingEventArgs<string?>>? changingEvent = null; - /// Action<ValueChangedEventArgs<string?>>? onChanged = args => - /// Console.WriteLine($"SchemeName changed to {args.NewValue ?? "none"}."); - /// EventHandler<ValueChangedEventArgs<string?>>? changedEvent = null; + /// Func<ValueChangingEventArgs<string?>, bool> onChanging = OnSchemeNameChanging; + /// EventHandler<ValueChangingEventArgs<string?>>? changingEvent = SchemeNameChanging; + /// Action<string?> doWork = value => _schemeName = value; + /// Action<ValueChangedEventArgs<string?>>? onChanged = OnSchemeNameChanged; + /// EventHandler<ValueChangedEventArgs<string?>>? 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); /// /// public static bool ChangeProperty ( @@ -55,6 +56,7 @@ public static class CWPPropertyHelper T newValue, Func, bool> onChanging, EventHandler>? changingEvent, + Action doWork, Action>? onChanged, EventHandler>? 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 changedArgs = new (currentValue, finalValue); onChanged?.Invoke (changedArgs); changedEvent?.Invoke (null, changedArgs); diff --git a/Terminal.Gui/ViewBase/Layout/Dim.cs b/Terminal.Gui/ViewBase/Layout/Dim.cs index d9bdaf362..a3326e80a 100644 --- a/Terminal.Gui/ViewBase/Layout/Dim.cs +++ b/Terminal.Gui/ViewBase/Layout/Dim.cs @@ -93,7 +93,7 @@ public abstract record Dim : IEqualityOperators /// Creates an Absolute from the specified integer value. /// The Absolute . /// The value to convert to the . - public static Dim? Absolute (int size) { return new DimAbsolute (size); } + public static Dim Absolute (int size) { return new DimAbsolute (size); } /// /// Creates a 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 /// /// The minimum dimension the View's ContentSize will be constrained to. /// The maximum dimension the View's ContentSize will be fit to. - 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 /// Creates a object that fills the dimension, leaving no margin. /// /// The Fill dimension. - public static Dim? Fill () { return new DimFill (0); } + public static Dim Fill () { return new DimFill (0); } /// /// Creates a object that fills the dimension, leaving the specified margin. /// /// The Fill dimension. /// Margin to use. - public static Dim? Fill (Dim margin) { return new DimFill (margin); } + public static Dim Fill (Dim margin) { return new DimFill (margin); } /// /// Creates a function object that computes the dimension based on the passed view and by executing @@ -172,7 +172,7 @@ public abstract record Dim : IEqualityOperators /// }; /// /// - 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)); diff --git a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs b/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs index d55765e9e..e973990ed 100644 --- a/Terminal.Gui/ViewBase/View.Drawing.Scheme.cs +++ b/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 /// /// The event arguments containing the current and proposed new scheme name. /// True to cancel the change, false to proceed. - protected virtual bool OnSchemeNameChanging (ValueChangingEventArgs args) - { - return false; - } + protected virtual bool OnSchemeNameChanging (ValueChangingEventArgs args) { return false; } /// /// Called after the property changes, allowing subclasses to react to the change. /// /// The event arguments containing the old and new scheme name. - protected virtual void OnSchemeNameChanged (ValueChangedEventArgs args) - { - } + protected virtual void OnSchemeNameChanged (ValueChangedEventArgs args) { } /// /// Raised before the property changes, allowing handlers to modify or cancel the change. @@ -115,7 +104,8 @@ public partial class View /// The resolved scheme, never null. /// /// - /// This method uses the Cancellable Work Pattern (CWP) via + /// This method uses the Cancellable Work Pattern (CWP) via + /// /// to allow customization or cancellation of scheme resolution through the method /// and event. /// @@ -135,13 +125,14 @@ public partial class View ResultEventArgs 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 /// public event EventHandler>? GettingScheme; - /// /// Sets the scheme for the , marking it as explicitly set. /// @@ -190,7 +181,8 @@ public partial class View /// /// This method uses the Cancellable Work Pattern (CWP) via /// to allow customization or cancellation of the scheme change through the method - /// and event. The event is raised after a successful change. + /// and event. The event is raised after a successful + /// change. /// /// /// If set to null, will be false, and the view will inherit the scheme from its @@ -216,21 +208,15 @@ public partial class View /// 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? _); } /// @@ -238,19 +224,13 @@ public partial class View /// /// The event arguments containing the current and proposed new scheme. /// True to cancel the change, false to proceed. - protected virtual bool OnSettingScheme (ValueChangingEventArgs args) - { - return false; - } + protected virtual bool OnSettingScheme (ValueChangingEventArgs args) { return false; } /// /// Called after the scheme is set, allowing subclasses to react to the change. /// /// The event arguments containing the old and new scheme. - protected virtual void OnSchemeChanged (ValueChangedEventArgs args) - { - SetNeedsDraw (); - } + protected virtual void OnSchemeChanged (ValueChangedEventArgs args) { SetNeedsDraw (); } /// /// Raised before the scheme is set, allowing handlers to modify or cancel the change. @@ -269,5 +249,4 @@ public partial class View /// , which may be null. /// public event EventHandler>? SchemeChanged; - } diff --git a/Terminal.Gui/ViewBase/View.Layout.cs b/Terminal.Gui/ViewBase/View.Layout.cs index c3c4a407b..8f28cdba2 100644 --- a/Terminal.Gui/ViewBase/View.Layout.cs +++ b/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); /// Gets or sets the height dimension of the view. /// The object representing the height of the view (the number of rows). @@ -304,28 +309,67 @@ public partial class View // Layout APIs /// /// Changing this property will cause to be updated. /// - /// The default value is Dim.Sized (0). + /// + /// Setting this property raises pre- and post-change events via , + /// allowing customization or cancellation of the change. The event + /// is raised before the change, and is raised after. + /// + /// The default value is Dim.Absolute (0). /// - public Dim? Height + /// + /// + 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); + /// + /// Called before the property changes, allowing subclasses to cancel or modify the change. + /// + /// The event arguments containing the current and proposed new height. + /// True to cancel the change, false to proceed. + protected virtual bool OnHeightChanging (ValueChangingEventArgs args) { return false; } + + /// + /// Called after the property changes, allowing subclasses to react to the change. + /// + /// The event arguments containing the old and new height. + protected virtual void OnHeightChanged (ValueChangedEventArgs args) { } + + /// + /// Raised before the property changes, allowing handlers to modify or cancel the change. + /// + /// + /// Set to true to cancel the change or modify + /// to adjust the proposed value. + /// + public event EventHandler>? HeightChanging; + + /// + /// Raised after the property changes, allowing handlers to react to the change. + /// + public event EventHandler>? HeightChanged; + + private Dim _width = Dim.Absolute (0); /// Gets or sets the width dimension of the view. /// The object representing the width of the view (the number of columns). @@ -351,26 +395,66 @@ public partial class View // Layout APIs /// /// Changing this property will cause to be updated. /// - /// The default value is Dim.Sized (0). + /// + /// Setting this property raises pre- and post-change events via , + /// allowing customization or cancellation of the change. The event + /// is raised before the change, and is raised after. + /// + /// The default value is Dim.Absolute (0). /// - public Dim? Width + /// + /// + 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 _); } } + /// + /// Called before the property changes, allowing subclasses to cancel or modify the change. + /// + /// The event arguments containing the current and proposed new width. + /// True to cancel the change, false to proceed. + protected virtual bool OnWidthChanging (ValueChangingEventArgs args) { return false; } + + /// + /// Called after the property changes, allowing subclasses to react to the change. + /// + /// The event arguments containing the old and new width. + protected virtual void OnWidthChanged (ValueChangedEventArgs args) { } + + /// + /// Raised before the property changes, allowing handlers to modify or cancel the change. + /// + /// + /// Set to true to cancel the change or modify + /// to adjust the proposed value. + /// + public event EventHandler>? WidthChanging; + + /// + /// Raised after the property changes, allowing handlers to react to the change. + /// + public event EventHandler>? 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 } /// - /// Gets the Views that are under , including Adornments. The list is ordered by depth. The + /// Gets the Views that are under , 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). /// /// Screen-relative location. /// - /// If set, excludes Views that have the or + /// If set, excludes Views that have the or + /// /// flags set in their ViewportSettings. /// public static List GetViewsUnderLocation (in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags) @@ -1219,21 +1309,24 @@ public partial class View // Layout APIs /// /// INTERNAL: Helper for GetViewsUnderLocation that starts from a given root view. - /// Gets the Views that are under , including Adornments. The list is ordered by depth. The + /// Gets the Views that are under , 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). /// /// /// Screen-relative location. /// - /// If set, excludes Views that have the or + /// If set, excludes Views that have the or + /// /// flags set in their ViewportSettings. /// internal static List GetViewsUnderLocation (View root, in Point screenLocation, ViewportSettingsFlags excludeViewportSettingsFlags) { List 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; } + /// /// INTERNAL: Gets ALL Views (Subviews and Adornments) in the of hierarchcy that are at /// , @@ -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) diff --git a/Terminal.Gui/ViewBase/View.cs b/Terminal.Gui/ViewBase/View.cs index e875b2243..3edf1e768 100644 --- a/Terminal.Gui/ViewBase/View.cs +++ b/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 itemsToKeep = Instances.Where (view => !view.WasDisposed).ToList (); - Instances = new ConcurrentBag (itemsToKeep); + Instances = new (itemsToKeep); #endif } @@ -108,9 +109,11 @@ public partial class View : IDisposable, ISupportInitializeNotification /// The id should be unique across all Views that share a SuperView. public string Id { get; set; } = ""; - private IConsoleDriver? _driver = null; + private IConsoleDriver? _driver; + /// - /// INTERNAL: Use instead. Points to the current driver in use by the view, it is a convenience property for simplifying the development + /// INTERNAL: Use instead. Points to the current driver in use by the view, it is a + /// convenience property for simplifying the development /// of new views. /// 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 /// The title. public string Title { - get - { - return _title; - } + get { return _title; } set { #if DEBUG_IDISPOSABLE @@ -530,7 +532,6 @@ public partial class View : IDisposable, ISupportInitializeNotification /// public static bool EnableDebugIDisposableAsserts { get; set; } = true; - /// /// Gets whether was called on this view or not. /// For debug purposes to verify objects are being disposed properly. diff --git a/Terminal.Gui/Views/Line.cs b/Terminal.Gui/Views/Line.cs index 3301aaf56..d61851753 100644 --- a/Terminal.Gui/Views/Line.cs +++ b/Terminal.Gui/Views/Line.cs @@ -1,33 +1,155 @@ - +#nullable enable + namespace Terminal.Gui.Views; /// -/// Draws a single line using the specified by . +/// Draws a single line using the specified by . /// /// +/// +/// is a that renders a single horizontal or vertical line +/// using the system. Unlike , which directly renders +/// runes, integrates with the LineCanvas to enable proper box-drawing character +/// selection and line intersection handling. +/// +/// +/// The line's appearance is controlled by the property, which supports +/// various line styles including Single, Double, Heavy, Rounded, Dashed, and Dotted. +/// +/// +/// Use the property to control the extent of the line regardless of its +/// . For horizontal lines, Length controls Width; for vertical lines, +/// it controls Height. The perpendicular dimension is always 1. +/// +/// +/// When multiple instances or other LineCanvas-aware views (like ) +/// intersect, the LineCanvas automatically selects the appropriate box-drawing characters for corners, +/// T-junctions, and crosses. +/// +/// +/// sets to , +/// 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. +/// /// +/// +/// +/// // 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 }; +/// +/// public class Line : View, IOrientation { private readonly OrientationHelper _orientationHelper; + private LineStyle _style = LineStyle.Single; + private Dim _length = Dim.Fill (); - /// Constructs a Line object. + /// + /// Constructs a new instance of the class with horizontal orientation. + /// + /// + /// By default, a horizontal line fills the available width and has a height of 1. + /// The line style defaults to . + /// 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; } + /// + /// Gets or sets the length of the line along its orientation. + /// + /// + /// + /// 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. + /// + /// + /// 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. + /// + /// + /// This property provides a cleaner API for controlling the line's extent + /// without needing to know whether to use Width or Height. + /// + /// + 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; + } + } + } + + /// + /// Gets or sets the style of the line. This controls the visual appearance of the line. + /// + /// + /// Supports various line styles including Single, Double, Heavy, Rounded, Dashed, and Dotted. + /// Note: This is separate from to avoid conflicts with the View's Border. + /// + public LineStyle Style + { + get => _style; + set + { + if (_style != value) + { + _style = value; + SetNeedsDraw (); + } + } + } #region IOrientation members + /// - /// 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. /// + /// + /// + /// When orientation changes, the appropriate dimension is set to + /// and the perpendicular dimension is set to 1. + /// + /// + /// For object initializers where dimensions are set before orientation: + /// new Line { Height = 9, Orientation = Orientation.Vertical } + /// 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. + /// + /// public Orientation Orientation { get => _orientationHelper.Orientation; @@ -36,48 +158,83 @@ public class Line : View, IOrientation #pragma warning disable CS0067 // The event is never used /// - public event EventHandler> OrientationChanging; + public event EventHandler>? OrientationChanging; /// - public event EventHandler> OrientationChanged; + public event EventHandler>? OrientationChanged; #pragma warning restore CS0067 // The event is never used - /// Called when has changed. - /// + /// + /// Called when has changed. + /// + /// The new orientation value. 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; } } + + /// + protected override bool OnWidthChanging (ValueChangingEventArgs 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); + } + + /// + protected override bool OnHeightChanging (ValueChangingEventArgs 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 /// + /// + /// This method adds the line to the LineCanvas for rendering. + /// The actual rendering is performed by the parent view through . + /// 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; } } diff --git a/Terminal.sln b/Terminal.sln index d8a71beed..0e8ab1a81 100644 --- a/Terminal.sln +++ b/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 diff --git a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs b/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs index d14dfc7c0..62cfc2588 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/Dim.AutoTests.PosTypes.cs +++ b/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)); diff --git a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs b/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs index c6d1fd919..9a71c5a94 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/FrameTests.cs +++ b/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 () { diff --git a/Tests/UnitTestsParallelizable/View/Layout/SetLayoutTests.cs b/Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs similarity index 90% rename from Tests/UnitTestsParallelizable/View/Layout/SetLayoutTests.cs rename to Tests/UnitTestsParallelizable/View/Layout/LayoutTests.cs index 44a7309d9..642929e51 100644 --- a/Tests/UnitTestsParallelizable/View/Layout/SetLayoutTests.cs +++ b/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 (); - } - /// This is an intentionally obtuse test. See https://github.com/gui-cs/Terminal.Gui/issues/2461 [Fact] public void Does_Not_Throw_If_Nested_SubViews_Ref_Topmost_SuperView () diff --git a/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs b/Tests/UnitTestsParallelizable/View/Layout/ViewLayoutEventTests.cs new file mode 100644 index 000000000..7c8ad7cd6 --- /dev/null +++ b/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 args) + { + return CancelWidthChange; + } + + protected override bool OnHeightChanging (App.ValueChangingEventArgs args) + { + return CancelHeightChange; + } + } +} diff --git a/Tests/UnitTestsParallelizable/Views/LineTests.cs b/Tests/UnitTestsParallelizable/Views/LineTests.cs new file mode 100644 index 000000000..baa1ea48d --- /dev/null +++ b/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 (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 + } +} diff --git a/local_packages/Terminal.Gui.2.0.0.nupkg b/local_packages/Terminal.Gui.2.0.0.nupkg index 593e60f43..4fa237cdc 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.nupkg and b/local_packages/Terminal.Gui.2.0.0.nupkg differ diff --git a/local_packages/Terminal.Gui.2.0.0.snupkg b/local_packages/Terminal.Gui.2.0.0.snupkg index 3ed73e02a..f2ef10c3d 100644 Binary files a/local_packages/Terminal.Gui.2.0.0.snupkg and b/local_packages/Terminal.Gui.2.0.0.snupkg differ