diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 000000000..79faf752f --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,5 @@ + + + $(DefineConstants);DIMAUTO + + \ No newline at end of file diff --git a/Terminal.Gui/Directory.Build.props b/Terminal.Gui/Directory.Build.props index 6bf2f2e2c..f22179be1 100644 --- a/Terminal.Gui/Directory.Build.props +++ b/Terminal.Gui/Directory.Build.props @@ -7,5 +7,4 @@ --> Miguel de Icaza, Charlie Kindel (@tig), @BDisp - \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosDim.cs b/Terminal.Gui/View/Layout/PosDim.cs index 6da3a0a03..42ff8d8d4 100644 --- a/Terminal.Gui/View/Layout/PosDim.cs +++ b/Terminal.Gui/View/Layout/PosDim.cs @@ -1,4 +1,4 @@ -namespace Terminal.Gui; +namespace Terminal.Gui; /// /// Describes the position of a which can be an absolute value, a percentage, centered, or @@ -43,7 +43,7 @@ /// /// /// -/// +/// /// /// /// Creates a object that is anchored to the end (right side or bottom) of @@ -132,8 +132,7 @@ public class Pos /// The view will be shifted left or up by the amount specified. /// /// This sample shows how align a to the bottom-right of a . - /// - /// // See Issue #502 + /// /// anchorButton.X = Pos.AnchorEnd () - (Pos.Right (anchorButton) - Pos.Left (anchorButton)); /// anchorButton.Y = Pos.AnchorEnd (1); /// @@ -159,19 +158,19 @@ public class Pos /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Bottom (View view) { return new PosView (view, 3); } + public static Pos Bottom (View view) { return new PosView (view, Side.Bottom); } /// Creates a object that can be used to center the . /// The center Pos. /// - /// This creates a that is centered horizontally, is 50% of the way down, is 30% the height, and + /// This creates a centered horizontally, is 50% of the way down, is 30% the height, and /// is 80% the width of the it added to. - /// + /// /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), /// }; /// /// @@ -200,7 +199,7 @@ public class Pos /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Left (View view) { return new PosView (view, 0); } + public static Pos Left (View view) { return new PosView (view, Side.X); } /// Adds a to a , yielding a new . /// The first to add. @@ -246,27 +245,27 @@ public class Pos /// Creates a percentage object /// The percent object. - /// A value between 0 and 100 representing the percentage. + /// A value between 0 and 100 representing the percentage. /// - /// This creates a that is centered horizontally, is 50% of the way down, is 30% the height, and + /// This creates a centered horizontally, is 50% of the way down, is 30% the height, and /// is 80% the width of the it added to. - /// - /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), + /// + /// var textView = new TextField { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), /// }; /// /// - public static Pos Percent (float n) + public static Pos Percent (float percent) { - if (n is < 0 or > 100) + if (percent is < 0 or > 100) { - throw new ArgumentException ("Percent value must be between 0 and 100"); + throw new ArgumentException ("Percent value must be between 0 and 100."); } - return new PosFactor (n / 100); + return new PosFactor (percent / 100); } /// @@ -275,49 +274,46 @@ public class Pos /// /// The that depends on the other view. /// The that will be tracked. - public static Pos Right (View view) { return new PosView (view, 2); } + public static Pos Right (View view) { return new PosView (view, Side.Right); } /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Top (View view) { return new PosView (view, 1); } + public static Pos Top (View view) { return new PosView (view, Side.Y); } /// Creates a object that tracks the Left (X) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos X (View view) { return new PosView (view, 0); } + public static Pos X (View view) { return new PosView (view, Side.X); } /// Creates a object that tracks the Top (Y) position of the specified . /// The that depends on the other view. /// The that will be tracked. - public static Pos Y (View view) { return new PosView (view, 1); } + public static Pos Y (View view) { return new PosView (view, Side.Y); } internal virtual int Anchor (int width) { return 0; } + // BUGBUG: newPos is never used private static void SetPosCombine (Pos left, PosCombine newPos) { - var view = left as PosView; - - if (view is { }) + if (left is PosView view) { view.Target.SetNeedsLayout (); } } - internal class PosAbsolute : Pos + internal class PosAbsolute (int n) : Pos { - private readonly int _n; - public PosAbsolute (int n) { _n = n; } + private readonly int _n = n; public override bool Equals (object other) { return other is PosAbsolute abs && abs._n == _n; } public override int GetHashCode () { return _n.GetHashCode (); } public override string ToString () { return $"Absolute({_n})"; } internal override int Anchor (int width) { return _n; } } - internal class PosAnchorEnd : Pos + internal class PosAnchorEnd (int offset) : Pos { - private readonly int _offset; - public PosAnchorEnd (int offset) { _offset = offset; } + private readonly int _offset = offset; public override bool Equals (object other) { return other is PosAnchorEnd anchorEnd && anchorEnd._offset == _offset; } public override int GetHashCode () { return _offset.GetHashCode (); } public override string ToString () { return $"AnchorEnd({_offset})"; } @@ -330,17 +326,10 @@ public class Pos internal override int Anchor (int width) { return width / 2; } } - internal class PosCombine : Pos + internal class PosCombine (bool add, Pos left, Pos right) : Pos { - internal bool _add; - internal Pos _left, _right; - - public PosCombine (bool add, Pos left, Pos right) - { - _left = left; - _right = right; - _add = add; - } + internal bool _add = add; + internal Pos _left = left, _right = right; public override string ToString () { return $"Combine({_left}{(_add ? '+' : '-')}{_right})"; } @@ -358,10 +347,9 @@ public class Pos } } - internal class PosFactor : Pos + internal class PosFactor (float factor) : Pos { - private readonly float _factor; - public PosFactor (float n) { _factor = n; } + private readonly float _factor = factor; public override bool Equals (object other) { return other is PosFactor f && f._factor == _factor; } public override int GetHashCode () { return _factor.GetHashCode (); } public override string ToString () { return $"Factor({_factor})"; } @@ -369,75 +357,57 @@ public class Pos } // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class PosFunc : Pos + internal class PosFunc (Func n) : Pos { - private readonly Func _function; - public PosFunc (Func n) { _function = n; } + private readonly Func _function = n; public override bool Equals (object other) { return other is PosFunc f && f._function () == _function (); } public override int GetHashCode () { return _function.GetHashCode (); } public override string ToString () { return $"PosFunc({_function ()})"; } internal override int Anchor (int width) { return _function (); } } - internal class PosView : Pos + internal enum Side { - public readonly View Target; + X = 0, + Y = 1, + Right = 2, + Bottom = 3, + } - private readonly int side; - - public PosView (View view, int side) - { - Target = view; - this.side = side; - } + internal class PosView (View view, Side side) : Pos + { + public readonly View Target = view; public override bool Equals (object other) { return other is PosView abs && abs.Target == Target; } public override int GetHashCode () { return Target.GetHashCode (); } public override string ToString () { - string tside; - - switch (side) - { - case 0: - tside = "x"; - - break; - case 1: - tside = "y"; - - break; - case 2: - tside = "right"; - - break; - case 3: - tside = "bottom"; - - break; - default: - tside = "unknown"; - - break; - } + string side1 = side switch + { + Side.X => "x", + Side.Y => "y", + Side.Right => "right", + Side.Bottom => "bottom", + _ => "unknown" + }; if (Target is null) { throw new NullReferenceException (nameof (Target)); } - return $"View(side={tside},target={Target})"; + return $"View(side={side1},target={Target})"; } internal override int Anchor (int width) { switch (side) { - case 0: return Target.Frame.X; - case 1: return Target.Frame.Y; - case 2: return Target.Frame.Right; - case 3: return Target.Frame.Bottom; + case Side.X: return Target.Frame.X; + case Side.Y: return Target.Frame.Y; + case Side.Right: return Target.Frame.Right; + case Side.Bottom: return Target.Frame.Bottom; default: return 0; } @@ -465,6 +435,15 @@ public class Pos /// /// /// +/// +/// +/// +/// Creates a object that automatically sizes the view to fit +/// the view's SubViews. +/// +/// +/// +/// /// /// /// @@ -486,8 +465,8 @@ public class Pos /// /// /// -/// Creates a object that fills the dimension, leaving the specified number -/// of columns for a margin. +/// Creates a object that fills the dimension from the View's X position +/// to the end of the super view's width, leaving the specified number of columns for a margin. /// /// /// @@ -514,6 +493,64 @@ public class Pos /// public class Dim { + /// + /// Specifies how will compute the dimension. + /// + public enum DimAutoStyle + { + /// + /// The dimension will be computed using both the view's and + /// . + /// The larger of the corresponding text dimension or Subview in + /// with the largest corresponding position plus dimension will determine the dimension. + /// + Auto, + + /// + /// The Subview in with the largest corresponding position plus dimension + /// will determine the dimension. + /// The corresponding dimension of the view's will be ignored. + /// + Subviews, + + /// + /// The corresponding dimension of the view's , formatted using the + /// settings, + /// will be used to determine the dimension. + /// The corresponding dimensions of the will be ignored. + /// + Text + } + + /// + /// Creates a object that automatically sizes the view to fit all of the view's SubViews and/or Text. + /// + /// + /// This initializes a with two SubViews. The view will be automatically sized to fit the two + /// SubViews. + /// + /// var button = new Button () { Text = "Click Me!", X = 1, Y = 1, Width = 10, Height = 1 }; + /// var textField = new TextField { Text = "Type here", X = 1, Y = 2, Width = 20, Height = 1 }; + /// var view = new Window () { Title = "MyWindow", X = 0, Y = 0, Width = Dim.AutoSize (), Height = Dim.AutoSize () }; + /// view.Add (button, textField); + /// + /// + /// The AutoSize object. + /// + /// Specifies how will compute the dimension. The default is . + /// + /// Specifies the minimum dimension that view will be automatically sized to. + /// Specifies the maximum dimension that view will be automatically sized to. NOT CURRENTLY SUPPORTED. + public static Dim Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim min = null, Dim max = null) + { + if (max != null) + { + throw new NotImplementedException (@"max is not implemented"); + } + + return new DimAuto (style, min, max); + } + /// Determines whether the specified object is equal to the current object. /// The object to compare with the current object. /// @@ -545,11 +582,11 @@ public class Dim /// Creates a object that tracks the Height of the specified . /// The height of the other . /// The view that will be tracked. - public static Dim Height (View view) { return new DimView (view, 0); } + public static Dim Height (View view) { return new DimView (view, Side.Height); } - /// Adds a to a , yielding a new . - /// The first to add. - /// The second to add. + /// Adds a to a , yielding a new . + /// The first to add. + /// The second to add. /// The that is the sum of the values of left and right. public static Dim operator + (Dim left, Dim right) { @@ -570,11 +607,11 @@ public class Dim public static implicit operator Dim (int n) { return new DimAbsolute (n); } /// - /// Subtracts a from a , yielding a new + /// Subtracts a from a , yielding a new /// . /// - /// The to subtract from (the minuend). - /// The to subtract (the subtrahend). + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). /// The that is the left minus right. public static Dim operator - (Dim left, Dim right) { @@ -591,31 +628,31 @@ public class Dim /// Creates a percentage object that is a percentage of the width or height of the SuperView. /// The percent object. - /// A value between 0 and 100 representing the percentage. - /// - /// If true the Percent is computed based on the remaining space after the X/Y anchor positions. If - /// false is computed based on the whole original space. + /// A value between 0 and 100 representing the percentage. + /// + /// If the dimension is computed using the View's position ( or ). + /// If the dimension is computed using the View's . /// /// - /// This initializes a that is centered horizontally, is 50% of the way down, is 30% the height, - /// and is 80% the width of the it added to. - /// - /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), + /// This initializes a that will be centered horizontally, is 50% of the way down, is 30% the height, + /// and is 80% the width of the SuperView. + /// + /// var textView = new TextField { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), /// }; /// /// - public static Dim Percent (float n, bool r = false) + public static Dim Percent (float percent, bool usePosition = false) { - if (n is < 0 or > 100) + if (percent is < 0 or > 100) { throw new ArgumentException ("Percent value must be between 0 and 100"); } - return new DimFactor (n / 100, r); + return new DimFactor (percent / 100, usePosition); } /// Creates an Absolute from the specified integer value. @@ -626,34 +663,37 @@ public class Dim /// Creates a object that tracks the Width of the specified . /// The width of the other . /// The view that will be tracked. - public static Dim Width (View view) { return new DimView (view, 1); } + public static Dim Width (View view) { return new DimView (view, Side.Width); } internal virtual int Anchor (int width) { return 0; } // BUGBUG: newPos is never used. private static void SetDimCombine (Dim left, DimCombine newPos) { (left as DimView)?.Target.SetNeedsLayout (); } - internal class DimAbsolute : Dim + internal class DimAbsolute (int n) : Dim { - private readonly int _n; - public DimAbsolute (int n) { _n = n; } + private readonly int _n = n; public override bool Equals (object other) { return other is DimAbsolute abs && abs._n == _n; } public override int GetHashCode () { return _n.GetHashCode (); } public override string ToString () { return $"Absolute({_n})"; } internal override int Anchor (int width) { return _n; } } - internal class DimCombine : Dim + internal class DimAuto (DimAutoStyle style, Dim min, Dim max) : Dim { - internal bool _add; - internal Dim _left, _right; + internal readonly Dim _max = max; + internal readonly Dim _min = min; + internal readonly DimAutoStyle _style = style; - public DimCombine (bool add, Dim left, Dim right) - { - _left = left; - _right = right; - _add = add; - } + public override bool Equals (object other) { return other is DimAuto auto && auto._min == _min && auto._max == _max && auto._style == _style; } + public override int GetHashCode () { return HashCode.Combine (base.GetHashCode (), _min, _max, _style); } + public override string ToString () { return $"Auto({_style},{_min},{_max})"; } + } + + internal class DimCombine (bool add, Dim left, Dim right) : Dim + { + internal bool _add = add; + internal Dim _left = left, _right = right; public override string ToString () { return $"Combine({_left}{(_add ? '+' : '-')}{_right})"; } @@ -671,16 +711,10 @@ public class Dim } } - internal class DimFactor : Dim + internal class DimFactor (float n, bool usePosition = false) : Dim { - private readonly float _factor; - private readonly bool _remaining; - - public DimFactor (float n, bool r = false) - { - _factor = n; - _remaining = r; - } + private readonly float _factor = n; + private readonly bool _remaining = usePosition; public override bool Equals (object other) { return other is DimFactor f && f._factor == _factor && f._remaining == _remaining; } public override int GetHashCode () { return _factor.GetHashCode (); } @@ -689,10 +723,9 @@ public class Dim internal override int Anchor (int width) { return (int)(width * _factor); } } - internal class DimFill : Dim + internal class DimFill (int margin) : Dim { - private readonly int _margin; - public DimFill (int margin) { _margin = margin; } + private readonly int _margin = margin; public override bool Equals (object other) { return other is DimFill fill && fill._margin == _margin; } public override int GetHashCode () { return _margin.GetHashCode (); } public override string ToString () { return $"Fill({_margin})"; } @@ -700,21 +733,26 @@ public class Dim } // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class DimFunc : Dim + internal class DimFunc (Func n) : Dim { - private readonly Func _function; - public DimFunc (Func n) { _function = n; } + private readonly Func _function = n; public override bool Equals (object other) { return other is DimFunc f && f._function () == _function (); } public override int GetHashCode () { return _function.GetHashCode (); } public override string ToString () { return $"DimFunc({_function ()})"; } internal override int Anchor (int width) { return _function (); } } + internal enum Side + { + Height = 0, + Width = 1 + } + internal class DimView : Dim { - private readonly int _side; + private readonly Side _side; - public DimView (View view, int side) + internal DimView (View view, Side side) { Target = view; _side = side; @@ -731,22 +769,22 @@ public class Dim throw new NullReferenceException (); } - string tside = _side switch + string side = _side switch { - 0 => "Height", - 1 => "Width", + Side.Height => "Height", + Side.Width => "Width", _ => "unknown" }; - return $"View({tside},{Target})"; + return $"View({side},{Target})"; } internal override int Anchor (int width) { return _side switch { - 0 => Target.Frame.Height, - 1 => Target.Frame.Width, + Side.Height => Target.Frame.Height, + Side.Width => Target.Frame.Width, _ => 0 }; } diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index af1e417a9..17b8e9400 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -26,9 +26,12 @@ public enum LayoutStyle /// /// Indicates one or more of the , , , or - /// objects are relative to the and are computed at layout time. - /// The position and size of the view will be computed based on these objects at layout time. - /// will provide the absolute computed values. + /// + /// objects are relative to the and are computed at layout time. The position and size of + /// the + /// view + /// will be computed based on these objects at layout time. will provide the absolute computed + /// values. /// Computed } @@ -102,7 +105,7 @@ public partial class View } Point boundsOffset = super.GetBoundsOffset (); - boundsOffset.Offset(super.Frame.X, super.Frame.Y); + boundsOffset.Offset (super.Frame.X, super.Frame.Y); ret.X += boundsOffset.X; ret.Y += boundsOffset.Y; super = super.SuperView; @@ -440,6 +443,7 @@ public partial class View } } + /// If is true, resizes the view. /// /// @@ -451,7 +455,7 @@ public partial class View } var boundsChanged = true; - Size newFrameSize = GetAutoSize (); + Size newFrameSize = GetTextAutoSize (); if (IsInitialized && newFrameSize != Frame.Size) { @@ -469,141 +473,6 @@ public partial class View return boundsChanged; } - - /// Determines if the View's can be set to a new value. - /// TrySetHeight can only be called when AutoSize is true (or being set to true). - /// - /// - /// Contains the width that would result if were set to - /// "/> - /// - /// - /// if the View's can be changed to the specified value. False - /// otherwise. - /// - internal bool TrySetHeight (int desiredHeight, out int resultHeight) - { - int h = desiredHeight; - bool canSetHeight; - - switch (Height) - { - case Dim.DimCombine _: - case Dim.DimView _: - case Dim.DimFill _: - // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored. - h = Height.Anchor (h); - canSetHeight = !ValidatePosDim; - - break; - case Dim.DimFactor factor: - // Tries to get the SuperView height otherwise the view height. - int sh = SuperView is { } ? SuperView.Frame.Height : h; - - if (factor.IsFromRemaining ()) - { - sh -= Frame.Y; - } - - h = Height.Anchor (sh); - canSetHeight = !ValidatePosDim; - - break; - default: - canSetHeight = true; - - break; - } - - resultHeight = h; - - return canSetHeight; - } - - /// Determines if the View's can be set to a new value. - /// TrySetWidth can only be called when AutoSize is true (or being set to true). - /// - /// - /// Contains the width that would result if were set to - /// "/> - /// - /// - /// if the View's can be changed to the specified value. False - /// otherwise. - /// - internal bool TrySetWidth (int desiredWidth, out int resultWidth) - { - int w = desiredWidth; - bool canSetWidth; - - switch (Width) - { - case Dim.DimCombine _: - case Dim.DimView _: - case Dim.DimFill _: - // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored. - w = Width.Anchor (w); - canSetWidth = !ValidatePosDim; - - break; - case Dim.DimFactor factor: - // Tries to get the SuperView Width otherwise the view Width. - int sw = SuperView is { } ? SuperView.Frame.Width : w; - - if (factor.IsFromRemaining ()) - { - sw -= Frame.X; - } - - w = Width.Anchor (sw); - canSetWidth = !ValidatePosDim; - - break; - default: - canSetWidth = true; - - break; - } - - resultWidth = w; - - return canSetWidth; - } - - /// Resizes the View to fit the specified size. Factors in the HotKey. - /// ResizeBoundsToFit can only be called when AutoSize is true (or being set to true). - /// - /// whether the Bounds was changed or not - private bool ResizeBoundsToFit (Size size) - { - //if (AutoSize == false) { - // throw new InvalidOperationException ("ResizeBoundsToFit can only be called when AutoSize is true"); - //} - - var boundsChanged = false; - bool canSizeW = TrySetWidth (size.Width - GetHotKeySpecifierLength (), out int rW); - bool canSizeH = TrySetHeight (size.Height - GetHotKeySpecifierLength (false), out int rH); - - if (canSizeW) - { - boundsChanged = true; - _width = rW; - } - - if (canSizeH) - { - boundsChanged = true; - _height = rH; - } - - if (boundsChanged) - { - Bounds = new (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height); - } - - return boundsChanged; - } - #endregion AutoSize #region Layout Engine @@ -886,8 +755,8 @@ public partial class View public event EventHandler LayoutStarted; /// - /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in response - /// to the container view or terminal resizing. + /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in response to + /// the container view or terminal resizing. /// /// /// @@ -900,9 +769,7 @@ public partial class View { if (!IsInitialized) { - Debug.WriteLine ( - $"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}" - ); + Debug.WriteLine ($"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}"); } if (!LayoutNeeded) @@ -910,6 +777,8 @@ public partial class View return; } + CheckDimAuto (); + LayoutAdornments (); Rectangle oldBounds = Bounds; @@ -925,7 +794,24 @@ public partial class View foreach (View v in ordered) { - LayoutSubview (v, new (GetBoundsOffset (), Bounds.Size)); + if (v.Width is Dim.DimAuto || v.Height is Dim.DimAuto) + { + // If the view is auto-sized... + Rectangle f = v.Frame; + v._frame = new (v.Frame.X, v.Frame.Y, 0, 0); + LayoutSubview (v, new (GetBoundsOffset (), Bounds.Size)); + + if (v.Frame != f) + { + // The subviews changed; do it again + v.LayoutNeeded = true; + LayoutSubview (v, new (GetBoundsOffset (), Bounds.Size)); + } + } + else + { + LayoutSubview (v, new (GetBoundsOffset (), Bounds.Size)); + } } // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case. @@ -1032,6 +918,8 @@ public partial class View Debug.Assert (_width is { }); Debug.Assert (_height is { }); + CheckDimAuto (); + int newX, newW, newY, newH; var autosize = Size.Empty; @@ -1039,7 +927,7 @@ public partial class View { // Note this is global to this function and used as such within the local functions defined // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function. - autosize = GetAutoSize (); + autosize = GetTextAutoSize (); } // TODO: Since GetNewLocationAndDimension does not depend on View, it can be moved into PosDim.cs @@ -1094,6 +982,41 @@ public partial class View break; + case Dim.DimAuto auto: + Thickness thickness = GetAdornmentsThickness (); + var text = 0; + var subviews = 0; + + int superviewSize = width ? superviewBounds.Width : superviewBounds.Height; + int autoMin = auto._min?.Anchor (superviewSize) ?? 0; + + if (superviewSize < autoMin) + { + Debug.WriteLine ($"WARNING: DimAuto specifies a min size ({autoMin}), but the SuperView's bounds are smaller ({superviewSize})."); + } + + if (auto._style is Dim.DimAutoStyle.Text or Dim.DimAutoStyle.Auto) + { + text = int.Max (width ? TextFormatter.Size.Width : TextFormatter.Size.Height, autoMin); + } + + if (auto._style is Dim.DimAutoStyle.Subviews or Dim.DimAutoStyle.Auto) + { + subviews = Subviews.Count == 0 + ? 0 + : Subviews + .Where (v => width ? v.X is not Pos.PosAnchorEnd : v.Y is not Pos.PosAnchorEnd) + .Max (v => width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height); + } + + int max = int.Max (text, subviews); + + newDimension = int.Max (width ? max + thickness.Left + thickness.Right : max + thickness.Top + thickness.Bottom, autoMin); + // If _max is set, use it. Otherwise, use superview;'s size to constrain + newDimension = int.Min (newDimension, auto._max?.Anchor (superviewSize) ?? superviewSize); + + break; + case Dim.DimAbsolute: // DimAbsolute.Anchor (int width) ignores width and returns n newDimension = Math.Max (d.Anchor (0), 0); @@ -1104,6 +1027,12 @@ public partial class View break; case Dim.DimFill: + // Fills the remaining space. + newDimension = Math.Max (d.Anchor (dimension - location), 0 /* width ? superviewBounds.Width : superviewBounds.Height*/); + newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; + + break; + default: newDimension = Math.Max (d.Anchor (dimension - location), 0); newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; @@ -1413,6 +1342,174 @@ public partial class View return result; } // TopologicalSort + /// Determines if the View's can be set to a new value. + /// TrySetHeight can only be called when AutoSize is true (or being set to true). + /// + /// + /// Contains the width that would result if were set to + /// "/> + /// + /// + /// if the View's can be changed to the specified value. False + /// otherwise. + /// + internal bool TrySetHeight (int desiredHeight, out int resultHeight) + { + int h = desiredHeight; + bool canSetHeight; + + switch (Height) + { + case Dim.DimCombine _: + case Dim.DimView _: + case Dim.DimFill _: + // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored. + h = Height.Anchor (h); + canSetHeight = !ValidatePosDim; + + break; + case Dim.DimFactor factor: + // Tries to get the SuperView height otherwise the view height. + int sh = SuperView != null ? SuperView.Frame.Height : h; + + if (factor.IsFromRemaining ()) + { + sh -= Frame.Y; + } + + h = Height.Anchor (sh); + canSetHeight = !ValidatePosDim; + + break; + default: + canSetHeight = true; + + break; + } + + resultHeight = h; + + return canSetHeight; + } + + /// Determines if the View's can be set to a new value. + /// TrySetWidth can only be called when AutoSize is true (or being set to true). + /// + /// + /// Contains the width that would result if were set to + /// "/> + /// + /// + /// if the View's can be changed to the specified value. False + /// otherwise. + /// + internal bool TrySetWidth (int desiredWidth, out int resultWidth) + { + int w = desiredWidth; + bool canSetWidth; + + switch (Width) + { + case Dim.DimCombine _: + case Dim.DimView _: + case Dim.DimFill _: + // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored. + w = Width.Anchor (w); + canSetWidth = !ValidatePosDim; + + break; + case Dim.DimFactor factor: + // Tries to get the SuperView Width otherwise the view Width. + int sw = SuperView != null ? SuperView.Frame.Width : w; + + if (factor.IsFromRemaining ()) + { + sw -= Frame.X; + } + + w = Width.Anchor (sw); + canSetWidth = !ValidatePosDim; + + break; + default: + canSetWidth = true; + + break; + } + + resultWidth = w; + + return canSetWidth; + } + + /// + /// Throws an if any SubViews are using Dim objects that depend on this + /// Views dimensions. + /// + /// + private void CheckDimAuto () + { + if (!ValidatePosDim || !IsInitialized || (Width is not Dim.DimAuto && Height is not Dim.DimAuto)) + { + return; + } + + void ThrowInvalid (View view, object checkPosDim, string name) + { + // TODO: Figure out how to make CheckDimAuto deal with PosCombine + object bad = null; + + switch (checkPosDim) + { + case Pos pos and not Pos.PosAbsolute and not Pos.PosView and not Pos.PosCombine: + bad = pos; + + break; + + case Pos pos and Pos.PosCombine: + // Recursively check for not Absolute or not View + ThrowInvalid (view, (pos as Pos.PosCombine)._left, name); + ThrowInvalid (view, (pos as Pos.PosCombine)._right, name); + + break; + + case Dim dim and not Dim.DimAbsolute and not Dim.DimView and not Dim.DimCombine: + bad = dim; + + break; + + case Dim dim and Dim.DimCombine: + // Recursively check for not Absolute or not View + ThrowInvalid (view, (dim as Dim.DimCombine)._left, name); + ThrowInvalid (view, (dim as Dim.DimCombine)._right, name); + + break; + } + + if (bad != null) + { + throw new InvalidOperationException ( + @$"{view.GetType ().Name}.{name} = {bad.GetType ().Name} which depends on the SuperView's dimensions and the SuperView uses Dim.Auto."); + } + } + + // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions. + foreach (View view in Subviews) + { + if (Width is Dim.DimAuto { _min: null }) + { + ThrowInvalid (view, view.Width, nameof (view.Width)); + ThrowInvalid (view, view.X, nameof (view.X)); + } + + if (Height is Dim.DimAuto { _min: null }) + { + ThrowInvalid (view, view.Height, nameof (view.Height)); + ThrowInvalid (view, view.Y, nameof (view.Y)); + } + } + } + private void LayoutSubview (View v, Rectangle contentArea) { //if (v.LayoutStyle == LayoutStyle.Computed) { @@ -1424,36 +1521,64 @@ public partial class View v.LayoutNeeded = false; } - #region Diagnostics - - // Diagnostics to highlight when Width or Height is read before the view has been initialized - private Dim VerifyIsInitialized (Dim dim, string member) + /// Resizes the View to fit the specified size. Factors in the HotKey. + /// ResizeBoundsToFit can only be called when AutoSize is true (or being set to true). + /// + /// whether the Bounds was changed or not + private bool ResizeBoundsToFit (Size size) { -#if DEBUG - if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) + //if (AutoSize == false) { + // throw new InvalidOperationException ("ResizeBoundsToFit can only be called when AutoSize is true"); + //} + + var boundsChanged = false; + bool canSizeW = TrySetWidth (size.Width - GetHotKeySpecifierLength (), out int rW); + bool canSizeH = TrySetHeight (size.Height - GetHotKeySpecifierLength (false), out int rH); + + if (canSizeW) { - Debug.WriteLine ( - $"WARNING: \"{this}\" has not been initialized; {member} is indeterminate: {dim}. This is potentially a bug." - ); + boundsChanged = true; + _width = rW; } -#endif // DEBUG - return dim; + + if (canSizeH) + { + boundsChanged = true; + _height = rH; + } + + if (boundsChanged) + { + Bounds = new (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height); + } + + return boundsChanged; } // Diagnostics to highlight when X or Y is read before the view has been initialized private Pos VerifyIsInitialized (Pos pos, string member) { #if DEBUG - if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) + if (pos is not Pos.PosAbsolute && LayoutStyle == LayoutStyle.Computed && !IsInitialized) { - Debug.WriteLine ( - $"WARNING: \"{this}\" has not been initialized; {member} is indeterminate {pos}. This is potentially a bug." - ); + Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate ({pos}). This is potentially a bug."); } #endif // DEBUG return pos; } + // Diagnostics to highlight when Width or Height is read before the view has been initialized + private Dim VerifyIsInitialized (Dim dim, string member) + { +#if DEBUG + if (dim is not Dim.DimAbsolute && LayoutStyle == LayoutStyle.Computed && !IsInitialized) + { + Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate: ({dim}). This is potentially a bug."); + } +#endif // DEBUG + return dim; + } + /// Gets or sets whether validation of and occurs. /// /// Setting this to will enable validation of , , @@ -1462,6 +1587,4 @@ public partial class View /// thus should only be used for debugging. /// public bool ValidatePosDim { get; set; } - - #endregion } diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs index b9cacc711..11b3abd01 100644 --- a/Terminal.Gui/View/ViewSubViews.cs +++ b/Terminal.Gui/View/ViewSubViews.cs @@ -85,6 +85,7 @@ public partial class View view.EndInit (); } + CheckDimAuto (); SetNeedsLayout (); SetNeedsDisplay (); } diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 598e2c76b..2306d02d9 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -1,13 +1,14 @@ -namespace Terminal.Gui; +namespace Terminal.Gui; public partial class View { private string _text; /// - /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved or not when - /// is enabled. If trailing spaces at the end of wrapped - /// lines will be removed when is formatted for display. The default is . + /// Gets or sets whether trailing spaces at the end of word-wrapped lines are preserved + /// or not when is enabled. + /// If trailing spaces at the end of wrapped lines will be removed when + /// is formatted for display. The default is . /// public virtual bool PreserveTrailingSpaces { @@ -22,19 +23,29 @@ public partial class View } } - /// The text displayed by the . + /// + /// The text displayed by the . + /// /// - /// The text will be drawn before any subviews are drawn. /// - /// The text will be drawn starting at the view origin (0, 0) and will be formatted according to - /// and . + /// The text will be drawn before any subviews are drawn. + /// + /// + /// The text will be drawn starting at the view origin (0, 0) and will be formatted according + /// to and . /// /// /// The text will word-wrap to additional lines if it does not fit horizontally. If 's height /// is 1, the text will be clipped. /// - /// If is true, the will be adjusted to fit the text. - /// When the text changes, the is fired. + /// + /// Set the to enable hotkey support. To disable hotkey support set + /// to + /// (Rune)0xffff. + /// + /// + /// If is true, the will be adjusted to fit the text. + /// /// public virtual string Text { @@ -80,7 +91,9 @@ public partial class View /// redisplay the . /// /// - /// If is true, the will be adjusted to fit the text. + /// + /// If is true, the will be adjusted to fit the text. + /// /// /// The text alignment. public virtual TextAlignment TextAlignment @@ -99,7 +112,9 @@ public partial class View /// . /// /// - /// If is true, the will be adjusted to fit the text. + /// + /// If is true, the will be adjusted to fit the text. + /// /// /// The text alignment. public virtual TextDirection TextDirection @@ -112,15 +127,20 @@ public partial class View } } - /// Gets the used to format . + /// + /// Gets or sets the used to format . + /// public TextFormatter TextFormatter { get; init; } = new (); /// /// Gets or sets how the View's is aligned vertically when drawn. Changing this property will - /// redisplay the . + /// redisplay + /// the . /// /// - /// If is true, the will be adjusted to fit the text. + /// + /// If is true, the will be adjusted to fit the text. + /// /// /// The text alignment. public virtual VerticalTextAlignment VerticalTextAlignment @@ -134,12 +154,47 @@ public partial class View } /// - /// Gets the Frame dimensions required to fit within using the text - /// specified by the property and accounting for any - /// characters. + /// Gets the width or height of the characters + /// in the property. /// - /// The the needs to be set to fit the text. - public Size GetAutoSize () + /// + /// Only the first HotKey specifier found in is supported. + /// + /// + /// If (the default) the width required for the HotKey specifier is returned. Otherwise the + /// height + /// is returned. + /// + /// + /// The number of characters required for the . If the text + /// direction specified + /// by does not match the parameter, 0 is returned. + /// + public int GetHotKeySpecifierLength (bool isWidth = true) + { + if (isWidth) + { + return TextFormatter.IsHorizontalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true + ? Math.Max (HotKeySpecifier.GetColumns (), 0) + : 0; + } + + return TextFormatter.IsVerticalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)HotKeySpecifier.Value) == true + ? Math.Max (HotKeySpecifier.GetColumns (), 0) + : 0; + } + + // TODO: Refactor this to return the Bounds size, not Frame. Move code that accounts for + // TODO: Thickness out to callers. + /// + /// Gets the size of the required to fit within using the + /// text + /// formatting settings of and accounting for . + /// + /// + /// + /// The of the required to fit the formatted text. + public Size GetTextAutoSize () { var x = 0; var y = 0; @@ -154,58 +209,19 @@ public partial class View int newWidth = rect.Size.Width - GetHotKeySpecifierLength () - + (Margin == null - ? 0 - : Margin.Thickness.Horizontal - + Border.Thickness.Horizontal - + Padding.Thickness.Horizontal); + + (Margin == null ? 0 : Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal); int newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) - + (Margin == null - ? 0 - : Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical); + + (Margin == null ? 0 : Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical); return new (newWidth, newHeight); } /// - /// Gets the width or height of the characters in the - /// property. + /// Can be overridden if the has + /// different format than the default. /// - /// - /// - /// This is for , not . For to show the hotkey, - /// set View. to the desired character. - /// - /// - /// Only the first HotKey specifier found in is supported. - /// - /// - /// - /// If (the default) the width required for the HotKey specifier is returned. - /// Otherwise the height is returned. - /// - /// - /// The number of characters required for the . If the text direction - /// specified by does not match the parameter, 0 is - /// returned. - /// - public int GetHotKeySpecifierLength (bool isWidth = true) - { - if (isWidth) - { - return TextFormatter.IsHorizontalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)TextFormatter.HotKeySpecifier.Value) == true - ? Math.Max (TextFormatter.HotKeySpecifier.GetColumns (), 0) - : 0; - } - - return TextFormatter.IsVerticalDirection (TextDirection) && TextFormatter.Text?.Contains ((char)TextFormatter.HotKeySpecifier.Value) == true - ? Math.Max (TextFormatter.HotKeySpecifier.GetColumns (), 0) - : 0; - } - - /// Can be overridden if the has different format than the default. protected virtual void UpdateTextFormatterText () { if (TextFormatter is { }) @@ -214,23 +230,25 @@ public partial class View } } - /// Gets the dimensions required for ignoring a . + /// + /// Gets the dimensions required for ignoring a . + /// /// internal Size GetSizeNeededForTextWithoutHotKey () { - return new ( - TextFormatter.Size.Width - GetHotKeySpecifierLength (), - TextFormatter.Size.Height - GetHotKeySpecifierLength (false) - ); + return new Size ( + TextFormatter.Size.Width - GetHotKeySpecifierLength (), + TextFormatter.Size.Height - GetHotKeySpecifierLength (false)); } /// - /// Internal API. Sets .Size to the current size, adjusted for + /// Sets .Size to the current size, adjusted for /// . /// /// - /// Use this API to set when the view has changed such that the size required to - /// fit the text has changed. changes. + /// Use this API to set when the view has changed such that the + /// size required to fit the text has changed. + /// changes. /// /// internal void SetTextFormatterSize () @@ -249,20 +267,45 @@ public partial class View return; } - TextFormatter.Size = new ( - Bounds.Size.Width + GetHotKeySpecifierLength (), - Bounds.Size.Height + GetHotKeySpecifierLength (false) - ); + int w = Bounds.Size.Width + GetHotKeySpecifierLength (); + + if (Width is Dim.DimAuto widthAuto && widthAuto._style != Dim.DimAutoStyle.Subviews) + { + if (Height is Dim.DimAuto) + { + // Both are auto. + TextFormatter.Size = new Size (SuperView?.Bounds.Width ?? 0, SuperView?.Bounds.Height ?? 0); + } + else + { + TextFormatter.Size = new Size (SuperView?.Bounds.Width ?? 0, Bounds.Size.Height + GetHotKeySpecifierLength ()); + } + + w = TextFormatter.FormatAndGetSize ().Width; + } + else + { + TextFormatter.Size = new Size (w, SuperView?.Bounds.Height ?? 0); + } + + int h = Bounds.Size.Height + GetHotKeySpecifierLength (); + + if (Height is Dim.DimAuto heightAuto && heightAuto._style != Dim.DimAutoStyle.Subviews) + { + TextFormatter.NeedsFormat = true; + h = TextFormatter.FormatAndGetSize ().Height; + } + + TextFormatter.Size = new Size (w, h); } private bool IsValidAutoSize (out Size autoSize) { Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); - autoSize = new ( - rect.Size.Width - GetHotKeySpecifierLength (), - rect.Size.Height - GetHotKeySpecifierLength (false) - ); + autoSize = new Size ( + rect.Size.Width - GetHotKeySpecifierLength (), + rect.Size.Height - GetHotKeySpecifierLength (false)); return !((ValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute))) || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength () @@ -274,8 +317,7 @@ public partial class View Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); int dimValue = height.Anchor (0); - return !((ValidatePosDim && !(height is Dim.DimAbsolute)) - || dimValue != rect.Size.Height - GetHotKeySpecifierLength (false)); + return !((ValidatePosDim && !(height is Dim.DimAbsolute)) || dimValue != rect.Size.Height - GetHotKeySpecifierLength (false)); } private bool IsValidAutoSizeWidth (Dim width) @@ -283,27 +325,24 @@ public partial class View Rectangle rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); int dimValue = width.Anchor (0); - return !((ValidatePosDim && !(width is Dim.DimAbsolute)) - || dimValue != rect.Size.Width - GetHotKeySpecifierLength ()); + return !((ValidatePosDim && !(width is Dim.DimAbsolute)) || dimValue != rect.Size.Width - GetHotKeySpecifierLength ()); } - /// Sets the size of the View to the minimum width or height required to fit . + /// + /// Sets the size of the View to the minimum width or height required to fit . + /// /// /// if the size was changed; if == - /// or will not fit. + /// or + /// will not fit. /// /// - /// Always returns if is or if - /// (Horizontal) or (Vertical) are not not set or zero. Does not take into - /// account word wrapping. + /// Always returns if is or + /// if (Horizontal) or (Vertical) are not not set or zero. + /// Does not take into account word wrapping. /// private bool SetFrameToFitText () { - if (AutoSize == false) - { - throw new InvalidOperationException ("SetFrameToFitText can only be called when AutoSize is true"); - } - // BUGBUG: This API is broken - should not assume Frame.Height == Bounds.Height // // Gets the minimum dimensions required to fit the View's , factoring in . @@ -334,7 +373,7 @@ public partial class View switch (TextFormatter.IsVerticalDirection (TextDirection)) { case true: - int colWidth = TextFormatter.GetWidestLineLength (new List { TextFormatter.Text }, 0, 1); + int colWidth = TextFormatter.GetSumMaxCharWidth (TextFormatter.Text, 0, 1); // TODO: v2 - This uses frame.Width; it should only use Bounds if (_frame.Width < colWidth @@ -374,25 +413,29 @@ public partial class View return false; } - // only called from EndInit private void UpdateTextDirection (TextDirection newDirection) { - bool directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction) - != TextFormatter.IsHorizontalDirection (newDirection); + bool directionChanged = TextFormatter.IsHorizontalDirection (TextFormatter.Direction) != TextFormatter.IsHorizontalDirection (newDirection); TextFormatter.Direction = newDirection; bool isValidOldAutoSize = AutoSize && IsValidAutoSize (out Size _); UpdateTextFormatterText (); - if ((!ValidatePosDim && directionChanged && AutoSize) - || (ValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) + if ((!ValidatePosDim && directionChanged && AutoSize) || (ValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) { OnResizeNeeded (); } - else if (AutoSize && directionChanged && IsAdded) + else if (directionChanged && IsAdded) { ResizeBoundsToFit (Bounds.Size); + + // BUGBUG: I think this call is redundant. + SetFrameToFitText (); + } + else + { + SetFrameToFitText (); } SetTextFormatterSize (); diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index b22480ccc..b333b981a 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -60,9 +60,13 @@ public class Dialog : Window Y = Pos.Center (); ValidatePosDim = true; - Width = Dim.Percent (85); // Dim.Auto (min: Dim.Percent (10)); - Height = Dim.Percent (85); //Dim.Auto (min: Dim.Percent (50)); - +#if DIMAUTO + Width = Dim.Auto (min: Dim.Percent (10)); + Height = Dim.Auto (min: Dim.Percent (50)); +#else + Width = Dim.Percent (85); + Height = Dim.Percent (85); +#endif ColorScheme = Colors.ColorSchemes ["Dialog"]; Modal = true; diff --git a/UICatalog/Scenarios/DimAutoDemo.cs b/UICatalog/Scenarios/DimAutoDemo.cs new file mode 100644 index 000000000..41a19a720 --- /dev/null +++ b/UICatalog/Scenarios/DimAutoDemo.cs @@ -0,0 +1,197 @@ +using System; +using Terminal.Gui; +using static Terminal.Gui.Dim; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("DimAuto", "Demonstrates Dim.Auto")] +[ScenarioCategory ("Layout")] +public class DimAutoDemo : Scenario +{ + public override void Main () + { + Application.Init (); + // Setup - Create a top-level application window and configure it. + Window appWindow = new () + { + Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}" + }; + + var view = new FrameView + { + Title = "Type to make View grow", + X = 1, + Y = 1, + Width = Auto (DimAutoStyle.Subviews, 40), + Height = Auto (DimAutoStyle.Subviews, 10) + }; + view.ValidatePosDim = true; + + var textEdit = new TextView { Text = "", X = 1, Y = 0, Width = 20, Height = 4 }; + view.Add (textEdit); + + var hlabel = new Label + { + Text = textEdit.Text, + X = Pos.Left (textEdit) + 1, + Y = Pos.Bottom (textEdit), + AutoSize = false, + Width = Auto (DimAutoStyle.Text, 20), + Height = 1, + ColorScheme = Colors.ColorSchemes ["Error"] + }; + view.Add (hlabel); + + var vlabel = new Label + { + Text = textEdit.Text, + X = Pos.Left (textEdit), + Y = Pos.Bottom (textEdit) + 1, + AutoSize = false, + Width = 1, + Height = Auto (DimAutoStyle.Text, 8), + ColorScheme = Colors.ColorSchemes ["Error"] + + //TextDirection = TextDirection.TopBottom_LeftRight + }; + vlabel.Id = "vlabel"; + view.Add (vlabel); + + var heightAuto = new View + { + X = Pos.Right (vlabel) + 1, + Y = Pos.Bottom (hlabel) + 1, + Width = 20, + Height = Auto (), + ColorScheme = Colors.ColorSchemes ["Error"], + Title = "W: 20, H: Auto", + BorderStyle = LineStyle.Rounded + }; + heightAuto.Id = "heightAuto"; + view.Add (heightAuto); + + var widthAuto = new View + { + X = Pos.Right (heightAuto) + 1, + Y = Pos.Bottom (hlabel) + 1, + Width = Auto (), + Height = 5, + ColorScheme = Colors.ColorSchemes ["Error"], + Title = "W: Auto, H: 5", + BorderStyle = LineStyle.Rounded + }; + widthAuto.Id = "widthAuto"; + view.Add (widthAuto); + + var bothAuto = new View + { + X = Pos.Right (widthAuto) + 1, + Y = Pos.Bottom (hlabel) + 1, + Width = Auto (), + Height = Auto (), + ColorScheme = Colors.ColorSchemes ["Error"], + Title = "W: Auto, H: Auto", + BorderStyle = LineStyle.Rounded + }; + bothAuto.Id = "bothAuto"; + view.Add (bothAuto); + + textEdit.ContentsChanged += (s, e) => + { + hlabel.Text = textEdit.Text; + vlabel.Text = textEdit.Text; + heightAuto.Text = textEdit.Text; + widthAuto.Text = textEdit.Text; + bothAuto.Text = textEdit.Text; + }; + + var movingButton = new Button + { + Text = "_Move down", + X = Pos.Right (vlabel), + Y = Pos.Bottom (vlabel), + }; + movingButton.Accept += (s, e) => { movingButton.Y = movingButton.Frame.Y + 1; }; + view.Add (movingButton); + + var resetButton = new Button + { + Text = "_Reset Button", + X = Pos.Right (movingButton), + Y = Pos.Top (movingButton) + }; + + resetButton.Accept += (s, e) => { movingButton.Y = Pos.Bottom (hlabel); }; + view.Add (resetButton); + + var dlgButton = new Button + { + Text = "Open Test _Dialog", + X = Pos.Right (view), + Y = Pos.Top (view) + }; + dlgButton.Accept += DlgButton_Clicked; + + appWindow.Add (view, dlgButton); + + // Run - Start the application. + Application.Run (appWindow); + appWindow.Dispose (); + + // Shutdown - Calling Application.Shutdown is required. + Application.Shutdown (); + + } + + private void DlgButton_Clicked (object sender, EventArgs e) + { + var dlg = new Dialog + { + Title = "Test Dialog" + }; + + //var ok = new Button ("Bye") { IsDefault = true }; + //ok.Clicked += (s, _) => Application.RequestStop (dlg); + //dlg.AddButton (ok); + + //var cancel = new Button ("Abort") { }; + //cancel.Clicked += (s, _) => Application.RequestStop (dlg); + //dlg.AddButton (cancel); + + var label = new Label + { + ValidatePosDim = true, + Text = "This is a label (AutoSize = false; Dim.Auto(3/20). Press Esc to close. Even more text.", + AutoSize = false, + X = Pos.Center (), + Y = 0, + Height = Auto (min: 3), + Width = Auto (min: 20), + ColorScheme = Colors.ColorSchemes ["Menu"] + }; + + var text = new TextField + { + ValidatePosDim = true, + Text = "TextField: X=1; Y=Pos.Bottom (label)+1, Width=Dim.Fill (0); Height=1", + TextFormatter = new TextFormatter { WordWrap = true }, + X = 0, + Y = Pos.Bottom (label) + 1, + Width = Fill (10), + Height = 1 + }; + + //var btn = new Button + //{ + // Text = "AnchorEnd", Y = Pos.AnchorEnd (1) + //}; + + //// TODO: We should really fix AnchorEnd to do this automatically. + //btn.X = Pos.AnchorEnd () - (Pos.Right (btn) - Pos.Left (btn)); + dlg.Add (label); + dlg.Add (text); + //dlg.Add (btn); + Application.Run (dlg); + dlg.Dispose (); + } +} diff --git a/UICatalog/UICatalog.csproj b/UICatalog/UICatalog.csproj index 662411daf..5ce7889ea 100644 --- a/UICatalog/UICatalog.csproj +++ b/UICatalog/UICatalog.csproj @@ -1,4 +1,4 @@ - + Exe net8.0 diff --git a/UnitTests/Dialogs/DialogTests.cs b/UnitTests/Dialogs/DialogTests.cs index 6c7eb754a..dc4793c86 100644 --- a/UnitTests/Dialogs/DialogTests.cs +++ b/UnitTests/Dialogs/DialogTests.cs @@ -33,18 +33,18 @@ public class DialogTests Width = width, Height = 1, ButtonAlignment = Dialog.ButtonAlignments.Center, - Buttons = [new Button { Text = btn1Text }] + Buttons = [new () { Text = btn1Text }] }; // Create with no top or bottom border to simplify testing button layout (no need to account for title etc..) - dlg.Border.Thickness = new Thickness (1, 0, 1, 0); + dlg.Border.Thickness = new (1, 0, 1, 0); runstate = Begin (dlg); var buttonRow = $"{CM.Glyphs.VLine} {btn1} {CM.Glyphs.VLine}"; TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); // Now add a second button buttonRow = $"{CM.Glyphs.VLine} {btn1} {btn2} {CM.Glyphs.VLine}"; - dlg.AddButton (new Button { Text = btn2Text }); + dlg.AddButton (new () { Text = btn2Text }); var first = false; RunIteration (ref runstate, ref first); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); @@ -52,24 +52,24 @@ public class DialogTests dlg.Dispose (); // Justify - dlg = new Dialog + dlg = new () { Title = title, Width = width, Height = 1, ButtonAlignment = Dialog.ButtonAlignments.Justify, - Buttons = [new Button { Text = btn1Text }] + Buttons = [new () { Text = btn1Text }] }; // Create with no top or bottom border to simplify testing button layout (no need to account for title etc..) - dlg.Border.Thickness = new Thickness (1, 0, 1, 0); + dlg.Border.Thickness = new (1, 0, 1, 0); runstate = Begin (dlg); buttonRow = $"{CM.Glyphs.VLine} {btn1}{CM.Glyphs.VLine}"; TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); // Now add a second button buttonRow = $"{CM.Glyphs.VLine}{btn1} {btn2}{CM.Glyphs.VLine}"; - dlg.AddButton (new Button { Text = btn2Text }); + dlg.AddButton (new () { Text = btn2Text }); first = false; RunIteration (ref runstate, ref first); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); @@ -77,24 +77,24 @@ public class DialogTests dlg.Dispose (); // Right - dlg = new Dialog + dlg = new () { Title = title, Width = width, Height = 1, ButtonAlignment = Dialog.ButtonAlignments.Right, - Buttons = [new Button { Text = btn1Text }] + Buttons = [new () { Text = btn1Text }] }; // Create with no top or bottom border to simplify testing button layout (no need to account for title etc..) - dlg.Border.Thickness = new Thickness (1, 0, 1, 0); + dlg.Border.Thickness = new (1, 0, 1, 0); runstate = Begin (dlg); - buttonRow = $"{CM.Glyphs.VLine}{new string (' ', width - btn1.Length - 2)}{btn1}{CM.Glyphs.VLine}"; + buttonRow = $"{CM.Glyphs.VLine}{new (' ', width - btn1.Length - 2)}{btn1}{CM.Glyphs.VLine}"; TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); // Now add a second button buttonRow = $"{CM.Glyphs.VLine} {btn1} {btn2}{CM.Glyphs.VLine}"; - dlg.AddButton (new Button { Text = btn2Text }); + dlg.AddButton (new () { Text = btn2Text }); first = false; RunIteration (ref runstate, ref first); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); @@ -102,24 +102,24 @@ public class DialogTests dlg.Dispose (); // Left - dlg = new Dialog + dlg = new () { Title = title, Width = width, Height = 1, ButtonAlignment = Dialog.ButtonAlignments.Left, - Buttons = [new Button { Text = btn1Text }] + Buttons = [new () { Text = btn1Text }] }; // Create with no top or bottom border to simplify testing button layout (no need to account for title etc..) - dlg.Border.Thickness = new Thickness (1, 0, 1, 0); + dlg.Border.Thickness = new (1, 0, 1, 0); runstate = Begin (dlg); - buttonRow = $"{CM.Glyphs.VLine}{btn1}{new string (' ', width - btn1.Length - 2)}{CM.Glyphs.VLine}"; + buttonRow = $"{CM.Glyphs.VLine}{btn1}{new (' ', width - btn1.Length - 2)}{CM.Glyphs.VLine}"; TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); // Now add a second button buttonRow = $"{CM.Glyphs.VLine}{btn1} {btn2} {CM.Glyphs.VLine}"; - dlg.AddButton (new Button { Text = btn2Text }); + dlg.AddButton (new () { Text = btn2Text }); first = false; RunIteration (ref runstate, ref first); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); @@ -153,14 +153,14 @@ public class DialogTests // Default - Center (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -170,14 +170,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -187,14 +187,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -204,14 +204,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -261,31 +261,17 @@ public class DialogTests // Justify buttonRow = - $"{ - CM.Glyphs.VLine - }{ - CM.Glyphs.LeftBracket - } yes { - CM.Glyphs.LeftBracket - } no { - CM.Glyphs.LeftBracket - } maybe { - CM.Glyphs.LeftBracket - } never { - CM.Glyphs.RightBracket - }{ - CM.Glyphs.VLine - }"; + $"{CM.Glyphs.VLine}{CM.Glyphs.LeftBracket} yes {CM.Glyphs.LeftBracket} no {CM.Glyphs.LeftBracket} maybe {CM.Glyphs.LeftBracket} never {CM.Glyphs.RightBracket}{CM.Glyphs.VLine}"; (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -294,14 +280,14 @@ public class DialogTests buttonRow = $"{CM.Glyphs.VLine}{CM.Glyphs.RightBracket} {btn2} {btn3} {btn4}{CM.Glyphs.VLine}"; (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -310,14 +296,14 @@ public class DialogTests buttonRow = $"{CM.Glyphs.VLine}{btn1} {btn2} {btn3} {CM.Glyphs.LeftBracket} n{CM.Glyphs.VLine}"; (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -351,14 +337,14 @@ public class DialogTests // Default - Center (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -368,14 +354,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -385,14 +371,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -402,14 +388,14 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -445,14 +431,14 @@ public class DialogTests // Default - Center (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -462,14 +448,14 @@ public class DialogTests Assert.Equal (width, buttonRow.GetColumns ()); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -479,14 +465,14 @@ public class DialogTests Assert.Equal (width, buttonRow.GetColumns ()); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -496,14 +482,14 @@ public class DialogTests Assert.Equal (width, buttonRow.GetColumns ()); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text }, - new Button { Text = btn4Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text }, + new Button { Text = btn4Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -528,11 +514,11 @@ public class DialogTests d.SetBufferSize (width, 1); (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btnText } + ); // Center TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); @@ -545,11 +531,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -560,11 +546,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -575,11 +561,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -592,11 +578,11 @@ public class DialogTests d.SetBufferSize (width, 1); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -607,11 +593,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -622,11 +608,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -637,11 +623,11 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btnText } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btnText } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -671,13 +657,13 @@ public class DialogTests d.SetBufferSize (buttonRow.Length, 3); (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -687,13 +673,13 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -703,13 +689,13 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -719,13 +705,13 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text }, - new Button { Text = btn3Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text }, + new Button { Text = btn3Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -753,12 +739,12 @@ public class DialogTests d.SetBufferSize (buttonRow.Length, 3); (runstate, Dialog dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Center, - new Button { Text = btn1Text }, - new Button { Text = btn2Text } - ); + title, + width, + Dialog.ButtonAlignments.Center, + new Button { Text = btn1Text }, + new Button { Text = btn2Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -768,12 +754,12 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Justify, - new Button { Text = btn1Text }, - new Button { Text = btn2Text } - ); + title, + width, + Dialog.ButtonAlignments.Justify, + new Button { Text = btn1Text }, + new Button { Text = btn2Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -783,12 +769,12 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Right, - new Button { Text = btn1Text }, - new Button { Text = btn2Text } - ); + title, + width, + Dialog.ButtonAlignments.Right, + new Button { Text = btn1Text }, + new Button { Text = btn2Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -798,12 +784,12 @@ public class DialogTests Assert.Equal (width, buttonRow.Length); (runstate, dlg) = RunButtonTestDialog ( - title, - width, - Dialog.ButtonAlignments.Left, - new Button { Text = btn1Text }, - new Button { Text = btn2Text } - ); + title, + width, + Dialog.ButtonAlignments.Left, + new Button { Text = btn1Text }, + new Button { Text = btn2Text } + ); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); dlg.Dispose (); @@ -835,8 +821,8 @@ public class DialogTests Button button1, button2; // Default (Center) - button1 = new Button { Text = btn1Text }; - button2 = new Button { Text = btn2Text }; + button1 = new () { Text = btn1Text }; + button2 = new () { Text = btn2Text }; (runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, button1, button2); button1.Visible = false; RunIteration (ref runstate, ref firstIteration); @@ -847,8 +833,8 @@ public class DialogTests // Justify Assert.Equal (width, buttonRow.Length); - button1 = new Button { Text = btn1Text }; - button2 = new Button { Text = btn2Text }; + button1 = new () { Text = btn1Text }; + button2 = new () { Text = btn2Text }; (runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Justify, button1, button2); button1.Visible = false; RunIteration (ref runstate, ref firstIteration); @@ -859,8 +845,8 @@ public class DialogTests // Right Assert.Equal (width, buttonRow.Length); - button1 = new Button { Text = btn1Text }; - button2 = new Button { Text = btn2Text }; + button1 = new () { Text = btn1Text }; + button2 = new () { Text = btn2Text }; (runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Right, button1, button2); button1.Visible = false; RunIteration (ref runstate, ref firstIteration); @@ -870,8 +856,8 @@ public class DialogTests // Left Assert.Equal (width, buttonRow.Length); - button1 = new Button { Text = btn1Text }; - button2 = new Button { Text = btn2Text }; + button1 = new () { Text = btn1Text }; + button2 = new () { Text = btn2Text }; (runstate, dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Left, button1, button2); button1.Visible = false; RunIteration (ref runstate, ref firstIteration); @@ -902,7 +888,7 @@ public class DialogTests win.Loaded += (s, a) => { - var dlg = new Dialog { Width = 18, Height = 3, Buttons = [new Button { Text = "Ok" }] }; + var dlg = new Dialog { Width = 18, Height = 3, Buttons = [new () { Text = "Ok" }] }; dlg.Loaded += (s, a) => { @@ -911,9 +897,7 @@ public class DialogTests var expected = @$" ┌──────────────────┐ │┌────────────────┐│ -││ { - btn -} ││ +││ {btn} ││ │└────────────────┘│ └──────────────────┘"; _ = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output); @@ -994,7 +978,12 @@ public class DialogTests if (iterations == 0) { - var dlg = new Dialog { Buttons = [new Button { Text = "Ok" }] }; + var dlg = new Dialog + { + Buttons = [new () { Text = "Ok" }], + Width = Dim.Percent (85), + Height = Dim.Percent (85) + }; Run (dlg); } else if (iterations == 1) @@ -1023,34 +1012,33 @@ public class DialogTests string expected = null; btn1.Accept += (s, e) => - { - btn2 = new Button { Text = "Show Sub" }; - btn3 = new Button { Text = "Close" }; - btn3.Accept += (s, e) => RequestStop (); + { + btn2 = new () { Text = "Show Sub" }; + btn3 = new () { Text = "Close" }; + btn3.Accept += (s, e) => RequestStop (); - btn2.Accept += (s, e) => - { - // Don't test MessageBox in Dialog unit tests! - var subBtn = new Button { Text = "Ok", IsDefault = true }; - var subDlg = new Dialog { Text = "ya", Width = 20, Height = 5, Buttons = [subBtn] }; - subBtn.Accept += (s, e) => RequestStop (subDlg); - Run (subDlg); - }; - var dlg = new Dialog { Buttons = [btn2, btn3] }; + btn2.Accept += (s, e) => + { + // Don't test MessageBox in Dialog unit tests! + var subBtn = new Button { Text = "Ok", IsDefault = true }; + var subDlg = new Dialog { Text = "ya", Width = 20, Height = 5, Buttons = [subBtn] }; + subBtn.Accept += (s, e) => RequestStop (subDlg); + Run (subDlg); + }; - Run (dlg); - }; + var dlg = new Dialog + { + Buttons = [btn2, btn3], + Width = Dim.Percent (85), + Height = Dim.Percent (85) + }; + + Run (dlg); + dlg.Dispose (); + }; var btn = - $"{ - CM.Glyphs.LeftBracket - }{ - CM.Glyphs.LeftDefaultIndicator - } Ok { - CM.Glyphs.RightDefaultIndicator - }{ - CM.Glyphs.RightBracket - }"; + $"{CM.Glyphs.LeftBracket}{CM.Glyphs.LeftDefaultIndicator} Ok {CM.Glyphs.RightDefaultIndicator}{CM.Glyphs.RightBracket}"; int iterations = -1; @@ -1071,15 +1059,7 @@ public class DialogTests │ │ │ │ │ │ - │{ - CM.Glyphs.LeftBracket - } Show Sub { - CM.Glyphs.RightBracket - } { - CM.Glyphs.LeftBracket - } Close { - CM.Glyphs.RightBracket - } │ + │{CM.Glyphs.LeftBracket} Show Sub {CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Close {CM.Glyphs.RightBracket} │ └───────────────────────┘"; TestHelpers.AssertDriverContentsWithFrameAre (expected, _output); @@ -1093,19 +1073,9 @@ public class DialogTests │ ┌──────────────────┐ │ │ │ya │ │ │ │ │ │ - │ │ { - btn - } │ │ + │ │ {btn} │ │ │ └──────────────────┘ │ - │{ - CM.Glyphs.LeftBracket - } Show Sub { - CM.Glyphs.RightBracket - } { - CM.Glyphs.LeftBracket - } Close { - CM.Glyphs.RightBracket - } │ + │{CM.Glyphs.LeftBracket} Show Sub {CM.Glyphs.RightBracket} {CM.Glyphs.LeftBracket} Close {CM.Glyphs.RightBracket} │ └───────────────────────┘", _output ); @@ -1149,13 +1119,17 @@ public class DialogTests [AutoInitShutdown] public void Location_Default () { - var d = new Dialog (); + var d = new Dialog () + { + Width = Dim.Percent (85), + Height = Dim.Percent (85) + }; Begin (d); ((FakeDriver)Driver).SetBufferSize (100, 100); // Default location is centered, so 100 / 2 - 85 / 2 = 7 var expected = 7; - Assert.Equal (new Point (expected, expected), (Point)d.Frame.Location); + Assert.Equal (new (expected, expected), d.Frame.Location); } [Fact] @@ -1168,7 +1142,7 @@ public class DialogTests // Default location is centered, so 100 / 2 - 85 / 2 = 7 var expected = 1; - Assert.Equal (new Point (expected, expected), (Point)d.Frame.Location); + Assert.Equal (new (expected, expected), d.Frame.Location); } [Fact] @@ -1181,7 +1155,7 @@ public class DialogTests ((FakeDriver)Driver).SetBufferSize (20, 10); // Default location is centered, so 100 / 2 - 85 / 2 = 7 - Assert.Equal (new Point (expected, expected), (Point)d.Frame.Location); + Assert.Equal (new (expected, expected), d.Frame.Location); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -1210,9 +1184,9 @@ public class DialogTests if (iterations == 0) { var d = new Dialog { X = 5, Y = 5, Height = 3, Width = 5 }; - var rs = Begin (d); + RunState rs = Begin (d); - Assert.Equal (new Point (5, 5), (Point)d.Frame.Location); + Assert.Equal (new (5, 5), d.Frame.Location); TestHelpers.AssertDriverContentsWithFrameAre ( @" @@ -1231,7 +1205,13 @@ public class DialogTests End (rs); d.Dispose (); - d = new Dialog { X = 5, Y = 5 }; + d = new () + { + X = 5, Y = 5, + Width = Dim.Percent (85), + Height = Dim.Percent (85) + + }; rs = Begin (d); // This is because of PostionTopLevels and EnsureVisibleBounds @@ -1312,12 +1292,18 @@ public class DialogTests [AutoInitShutdown] public void Size_Default () { - var d = new Dialog (); + var d = new Dialog () + { + Width = Dim.Percent (85), + Height = Dim.Percent (85) + }; + Begin (d); ((FakeDriver)Driver).SetBufferSize (100, 100); // Default size is Percent(85) Assert.Equal (new ((int)(100 * .85), (int)(100 * .85)), d.Frame.Size); + d.Dispose (); } [Fact] @@ -1331,10 +1317,11 @@ public class DialogTests // Default size is Percent(85) Assert.Equal (new (50, 50), d.Frame.Size); + d.Dispose (); } [Fact] - [AutoInitShutdown] + [SetupFakeDriver] public void Zero_Buttons_Works () { RunState runstate = null; @@ -1347,10 +1334,11 @@ public class DialogTests int width = buttonRow.Length; d.SetBufferSize (buttonRow.Length, 3); - (runstate, Dialog _) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null); + (runstate, Dialog dlg) = RunButtonTestDialog (title, width, Dialog.ButtonAlignments.Center, null); TestHelpers.AssertDriverContentsWithFrameAre ($"{buttonRow}", _output); End (runstate); + dlg.Dispose (); } private (RunState, Dialog) RunButtonTestDialog ( @@ -1372,7 +1360,7 @@ public class DialogTests }; // Create with no top or bottom border to simplify testing button layout (no need to account for title etc..) - dlg.Border.Thickness = new Thickness (1, 0, 1, 0); + dlg.Border.Thickness = new (1, 0, 1, 0); return (Begin (dlg), dlg); } @@ -1410,15 +1398,12 @@ public class DialogTests Assert.True (Top.WasDisposed); Assert.NotNull (Top); #endif - Shutdown(); + Shutdown (); Assert.Null (Top); return; - void Dlg_Ready (object sender, EventArgs e) - { - RequestStop (); - } + void Dlg_Ready (object sender, EventArgs e) { RequestStop (); } } [Fact] @@ -1446,10 +1431,11 @@ public class DialogTests #if DEBUG_IDISPOSABLE Dialog dlg2 = new (); dlg2.Ready += Dlg_Ready; - var exception = Record.Exception (() => Run (dlg2)); + Exception exception = Record.Exception (() => Run (dlg2)); Assert.NotNull (exception); - dlg.Dispose(); + dlg.Dispose (); + // Now it's possible to tun dlg2 without throw Run (dlg2); @@ -1459,6 +1445,7 @@ public class DialogTests Assert.False (dlg2.WasDisposed); dlg2.Dispose (); + // Now an assertion will throw accessing the Canceled property exception = Record.Exception (() => Assert.True (dlg.Canceled)); Assert.NotNull (exception); diff --git a/UnitTests/View/Layout/DimAutoTests.cs b/UnitTests/View/Layout/DimAutoTests.cs new file mode 100644 index 000000000..bbb3d6c56 --- /dev/null +++ b/UnitTests/View/Layout/DimAutoTests.cs @@ -0,0 +1,545 @@ +using System.Globalization; +using System.Text; +using Xunit.Abstractions; + +// Alias Console to MockConsole so we don't accidentally use Console +using Console = Terminal.Gui.FakeConsole; + +namespace Terminal.Gui.ViewTests; + +public class DimAutoTests +{ + private readonly ITestOutputHelper _output; + + public DimAutoTests (ITestOutputHelper output) + { + _output = output; + Console.OutputEncoding = Encoding.Default; + + // Change current culture + var culture = CultureInfo.CreateSpecificCulture ("en-US"); + Thread.CurrentThread.CurrentCulture = culture; + Thread.CurrentThread.CurrentUICulture = culture; + } + + // Test min - ensure that if min is specified in the DimAuto constructor it is honored + [Fact] + public void DimAuto_Min () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (min: 10), + Height = Dim.Auto (min: 10), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = 5, + Height = 5 + }; + + superView.Add (subView); + superView.BeginInit (); + superView.EndInit (); + + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + Assert.Equal (10, superView.Frame.Width); + Assert.Equal (10, superView.Frame.Height); + } + + // what happens if DimAuto (min: 10) and the subview moves to a negative coord? + [Fact] + public void DimAuto_Min_Resets_If_Subview_Moves_Negative () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (min: 10), + Height = Dim.Auto (min: 10), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = 5, + Height = 5 + }; + + superView.Add (subView); + superView.BeginInit (); + superView.EndInit (); + + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + Assert.Equal (10, superView.Frame.Width); + Assert.Equal (10, superView.Frame.Height); + + subView.X = -1; + subView.Y = -1; + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + Assert.Equal (5, subView.Frame.Width); + Assert.Equal (5, subView.Frame.Height); + + Assert.Equal (10, superView.Frame.Width); + Assert.Equal (10, superView.Frame.Height); + } + + [Fact] + public void DimAuto_Min_Resets_If_Subview_Shrinks () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (min: 10), + Height = Dim.Auto (min: 10), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = 5, + Height = 5 + }; + + superView.Add (subView); + superView.BeginInit (); + superView.EndInit (); + + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + Assert.Equal (10, superView.Frame.Width); + Assert.Equal (10, superView.Frame.Height); + + subView.Width = 3; + subView.Height = 3; + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + Assert.Equal (3, subView.Frame.Width); + Assert.Equal (3, subView.Frame.Height); + + Assert.Equal (10, superView.Frame.Width); + Assert.Equal (10, superView.Frame.Height); + } + + [Theory] + [InlineData (0, 0, 0, 0, 0)] + [InlineData (0, 0, 5, 0, 0)] + [InlineData (0, 0, 0, 5, 5)] + [InlineData (0, 0, 5, 5, 5)] + [InlineData (1, 0, 5, 0, 0)] + [InlineData (1, 0, 0, 5, 5)] + [InlineData (1, 0, 5, 5, 5)] + [InlineData (1, 1, 5, 5, 6)] + [InlineData (-1, 0, 5, 0, 0)] + [InlineData (-1, 0, 0, 5, 5)] + [InlineData (-1, 0, 5, 5, 5)] + [InlineData (-1, -1, 5, 5, 4)] + public void Height_Auto_Width_NotChanged (int subX, int subY, int subWidth, int subHeight, int expectedHeight) + { + var superView = new View + { + X = 0, + Y = 0, + Width = 10, + Height = Dim.Auto (), + ValidatePosDim = true + }; + + var subView = new View + { + X = subX, + Y = subY, + Width = subWidth, + Height = subHeight, + ValidatePosDim = true + }; + + superView.Add (subView); + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + Assert.Equal (new Rectangle (0, 0, 10, expectedHeight), superView.Frame); + } + + [Fact] + public void NoSubViews_Does_Nothing () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = Dim.Auto (), + ValidatePosDim = true + }; + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + Assert.Equal (new Rectangle (0, 0, 0, 0), superView.Frame); + + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + Assert.Equal (new Rectangle (0, 0, 0, 0), superView.Frame); + + superView.SetRelativeLayout (new Rectangle (10, 10, 10, 10)); + Assert.Equal (new Rectangle (0, 0, 0, 0), superView.Frame); + } + + [Theory] + [InlineData (0, 0, 0, 0, 0, 0)] + [InlineData (0, 0, 5, 0, 5, 0)] + [InlineData (0, 0, 0, 5, 0, 5)] + [InlineData (0, 0, 5, 5, 5, 5)] + [InlineData (1, 0, 5, 0, 6, 0)] + [InlineData (1, 0, 0, 5, 1, 5)] + [InlineData (1, 0, 5, 5, 6, 5)] + [InlineData (1, 1, 5, 5, 6, 6)] + [InlineData (-1, 0, 5, 0, 4, 0)] + [InlineData (-1, 0, 0, 5, 0, 5)] + [InlineData (-1, 0, 5, 5, 4, 5)] + [InlineData (-1, -1, 5, 5, 4, 4)] + public void SubView_ChangesSuperViewSize (int subX, int subY, int subWidth, int subHeight, int expectedWidth, int expectedHeight) + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = Dim.Auto (), + ValidatePosDim = true + }; + + var subView = new View + { + X = subX, + Y = subY, + Width = subWidth, + Height = subHeight, + ValidatePosDim = true + }; + + superView.Add (subView); + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + Assert.Equal (new Rectangle (0, 0, expectedWidth, expectedHeight), superView.Frame); + } + + // Test validation + [Fact] + public void ValidatePosDim_True_Throws_When_SubView_Uses_SuperView_Dims () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = Dim.Auto (), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = Dim.Fill (), + Height = 10, + ValidatePosDim = true + }; + + superView.BeginInit (); + superView.EndInit (); + Assert.Throws (() => superView.Add (subView)); + + subView.Width = 10; + superView.Add (subView); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + superView.LayoutSubviews (); // no throw + + subView.Width = Dim.Fill (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Width = 10; + + subView.Height = Dim.Fill (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 10; + + subView.Height = Dim.Percent (50); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 10; + + subView.X = Pos.Center (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + subView.Y = Pos.Center (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Y = 0; + + subView.Width = 10; + subView.Height = 10; + subView.X = 0; + subView.Y = 0; + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); + superView.LayoutSubviews (); + } + + // Test validation + [Fact] + public void ValidatePosDim_True_Throws_When_SubView_Uses_SuperView_Dims_Combine () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = Dim.Auto (), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10 + }; + + var subView2 = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10 + }; + + superView.Add (subView, subView2); + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); + superView.LayoutSubviews (); // no throw + + subView.Height = Dim.Fill () + 3; + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 0; + + subView.Height = 3 + Dim.Fill (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 0; + + subView.Height = 3 + 5 + Dim.Fill (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 0; + + subView.Height = 3 + 5 + Dim.Percent (10); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.Height = 0; + + // Tests nested Combine + subView.Height = 5 + new Dim.DimCombine (true, 3, new Dim.DimCombine (true, Dim.Percent (10), 9)); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + } + + [Fact] + public void ValidatePosDim_True_Throws_When_SubView_Uses_SuperView_Pos_Combine () + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = Dim.Auto (), + ValidatePosDim = true + }; + + var subView = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10 + }; + + var subView2 = new View + { + X = 0, + Y = 0, + Width = 10, + Height = 10 + }; + + superView.Add (subView, subView2); + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); + superView.LayoutSubviews (); // no throw + + subView.X = Pos.Right (subView2); + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); + superView.LayoutSubviews (); // no throw + + subView.X = Pos.Right (subView2) + 3; + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); // no throw + superView.LayoutSubviews (); // no throw + + subView.X = new Pos.PosCombine (true, Pos.Right (subView2), new Pos.PosCombine (true, 7, 9)); + superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0)); // no throw + + subView.X = Pos.Center () + 3; + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + subView.X = 3 + Pos.Center (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + subView.X = 3 + 5 + Pos.Center (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + subView.X = 3 + 5 + Pos.Percent (10); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + subView.X = Pos.Percent (10) + Pos.Center (); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + + // Tests nested Combine + subView.X = 5 + new Pos.PosCombine (true, Pos.Right (subView2), new Pos.PosCombine (true, Pos.Center (), 9)); + Assert.Throws (() => superView.SetRelativeLayout (new Rectangle (0, 0, 0, 0))); + subView.X = 0; + } + + [Theory] + [InlineData (0, 0, 0, 0, 0)] + [InlineData (0, 0, 5, 0, 5)] + [InlineData (0, 0, 0, 5, 0)] + [InlineData (0, 0, 5, 5, 5)] + [InlineData (1, 0, 5, 0, 6)] + [InlineData (1, 0, 0, 5, 1)] + [InlineData (1, 0, 5, 5, 6)] + [InlineData (1, 1, 5, 5, 6)] + [InlineData (-1, 0, 5, 0, 4)] + [InlineData (-1, 0, 0, 5, 0)] + [InlineData (-1, 0, 5, 5, 4)] + [InlineData (-1, -1, 5, 5, 4)] + public void Width_Auto_Height_NotChanged (int subX, int subY, int subWidth, int subHeight, int expectedWidth) + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (), + Height = 10, + ValidatePosDim = true + }; + + var subView = new View + { + X = subX, + Y = subY, + Width = subWidth, + Height = subHeight, + ValidatePosDim = true + }; + + superView.Add (subView); + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 10)); + Assert.Equal (new Rectangle (0, 0, expectedWidth, 10), superView.Frame); + } + + // Test that when a view has Width set to DimAuto (min: x) the width is never < x even if SetRelativeLayout is called with smaller bounds + [Theory] + [InlineData (0, 0)] + [InlineData (1, 1)] + [InlineData (3, 3)] + [InlineData (4, 4)] + [InlineData (5, 4)] // This is clearly invalid, but we choose to not throw but log a debug message + public void Width_Auto_Min (int min, int expectedWidth) + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (min: min), + Height = 1, + ValidatePosDim = true + }; + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 4, 1)); + Assert.Equal (expectedWidth, superView.Frame.Width); + } + + // Test Dim.Fill - Fill should not impact width of the DimAuto superview + [Theory] + [InlineData (0, 0, 0, 10, 10)] + [InlineData (0, 1, 0, 10, 10)] + [InlineData (0, 11, 0, 10, 10)] + [InlineData (0, 10, 0, 10, 10)] + [InlineData (0, 5, 0, 10, 10)] + [InlineData (1, 5, 0, 10, 9)] + [InlineData (1, 10, 0, 10, 9)] + [InlineData (0, 0, 1, 10, 9)] + [InlineData (0, 10, 1, 10, 9)] + [InlineData (0, 5, 1, 10, 9)] + [InlineData (1, 5, 1, 10, 8)] + [InlineData (1, 10, 1, 10, 8)] + public void Width_Fill_Fills (int subX, int superMinWidth, int fill, int expectedSuperWidth, int expectedSubWidth) + { + var superView = new View + { + X = 0, + Y = 0, + Width = Dim.Auto (min:superMinWidth), + Height = 1, + ValidatePosDim = true + }; + + var subView = new View + { + X = subX, + Y = 0, + Width = Dim.Fill (fill), + Height = 1, + ValidatePosDim = true + }; + + superView.Add (subView); + + superView.BeginInit (); + superView.EndInit (); + superView.SetRelativeLayout (new Rectangle (0, 0, 10, 1)); + Assert.Equal (expectedSuperWidth, superView.Frame.Width); + superView.LayoutSubviews (); + Assert.Equal (expectedSubWidth, subView.Frame.Width); + Assert.Equal (expectedSuperWidth, superView.Frame.Width); + } + + // Test variations of Frame +} diff --git a/UnitTests/View/Layout/DimTests.cs b/UnitTests/View/Layout/DimTests.cs index 40bcb2ecf..8e66bf17a 100644 --- a/UnitTests/View/Layout/DimTests.cs +++ b/UnitTests/View/Layout/DimTests.cs @@ -1,9 +1,7 @@ using System.Globalization; using System.Text; using Xunit.Abstractions; - -// Alias Console to MockConsole so we don't accidentally use Console -using Console = Terminal.Gui.FakeConsole; +using static Terminal.Gui.Dim; namespace Terminal.Gui.ViewTests; @@ -26,7 +24,7 @@ public class DimTests // A new test that does not depend on Application is needed. [Fact] [AutoInitShutdown] - public void Dim_Add_Operator () + public void Add_Operator () { Toplevel top = new (); @@ -76,176 +74,7 @@ public class DimTests // TODO: A new test that calls SetRelativeLayout directly is needed. [Fact] [TestRespondersDisposed] - public void Dim_Referencing_SuperView_Does_Not_Throw () - { - var super = new View { Width = 10, Height = 10, Text = "super" }; - - var view = new View - { - Width = Dim.Width (super), // this is allowed - Height = Dim.Height (super), // this is allowed - Text = "view" - }; - - super.Add (view); - super.BeginInit (); - super.EndInit (); - - Exception exception = Record.Exception (super.LayoutSubviews); - Assert.Null (exception); - super.Dispose (); - } - - // TODO: This actually a SetRelativeLayout/LayoutSubViews test and should be moved - // TODO: A new test that calls SetRelativeLayout directly is needed. - [Fact] - [AutoInitShutdown] - public void Dim_Subtract_Operator () - { - Toplevel top = new (); - - var view = new View { X = 0, Y = 0, Width = 20, Height = 0 }; - var field = new TextField { X = 0, Y = Pos.Bottom (view), Width = 20 }; - var count = 20; - List