From 1f42e4ceebb33cc3f85e5ded17ad4cda09a32478 Mon Sep 17 00:00:00 2001 From: Tig Kindel Date: Sun, 17 Dec 2023 23:26:43 -0700 Subject: [PATCH] Initial work in progress --- Terminal.Gui/View/Layout/PosDim.cs | 1284 ++++++++++----------- Terminal.Gui/{Text => View}/ViewLayout.cs | 41 +- UICatalog/Scenarios/DimAutoSize.cs | 67 ++ UnitTests/View/Layout/DimTests.cs | 4 +- 4 files changed, 708 insertions(+), 688 deletions(-) rename Terminal.Gui/{Text => View}/ViewLayout.cs (95%) create mode 100644 UICatalog/Scenarios/DimAutoSize.cs diff --git a/Terminal.Gui/View/Layout/PosDim.cs b/Terminal.Gui/View/Layout/PosDim.cs index adeda1840..79b7acd79 100644 --- a/Terminal.Gui/View/Layout/PosDim.cs +++ b/Terminal.Gui/View/Layout/PosDim.cs @@ -6,695 +6,635 @@ // using System; -namespace Terminal.Gui { - /// - /// Describes the position of a which can be an absolute value, a percentage, centered, or - /// relative to the ending dimension. Integer values are implicitly convertible to - /// an absolute . These objects are created using the static methods Percent, - /// AnchorEnd, and Center. The objects can be combined with the addition and - /// subtraction operators. - /// - /// - /// - /// Use the objects on the X or Y properties of a view to control the position. - /// - /// - /// These can be used to set the absolute position, when merely assigning an - /// integer value (via the implicit integer to conversion), and they can be combined - /// to produce more useful layouts, like: Pos.Center - 3, which would shift the position - /// of the 3 characters to the left after centering for example. - /// - /// - /// It is possible to reference coordinates of another view by using the methods - /// Left(View), Right(View), Bottom(View), Top(View). The X(View) and Y(View) are - /// aliases to Left(View) and Top(View) respectively. - /// - /// - public class Pos { - internal virtual int Anchor (int width) - { - return 0; - } - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class PosFunc : Pos { - Func function; +namespace Terminal.Gui; - public PosFunc (Func n) - { - this.function = n; - } +/// +/// Describes the position of a which can be an absolute value, a percentage, centered, or +/// relative to the ending dimension. Integer values are implicitly convertible to +/// an absolute . These objects are created using the static methods Percent, +/// AnchorEnd, and Center. The objects can be combined with the addition and +/// subtraction operators. +/// +/// +/// +/// Use the objects on the X or Y properties of a view to control the position. +/// +/// +/// These can be used to set the absolute position, when merely assigning an +/// integer value (via the implicit integer to conversion), and they can be combined +/// to produce more useful layouts, like: Pos.Center - 3, which would shift the position +/// of the 3 characters to the left after centering for example. +/// +/// +/// It is possible to reference coordinates of another view by using the methods +/// Left(View), Right(View), Bottom(View), Top(View). The X(View) and Y(View) are +/// aliases to Left(View) and Top(View) respectively. +/// +/// +public class Pos { + internal virtual int Anchor (int width) => 0; - internal override int Anchor (int width) - { - return function (); - } + // Helper class to provide dynamic value by the execution of a function that returns an integer. + internal class PosFunc : Pos { + Func function; - public override string ToString () - { - return $"PosFunc({function ()})"; - } + public PosFunc (Func n) => function = n; - public override int GetHashCode () => function.GetHashCode (); + internal override int Anchor (int width) => function (); - public override bool Equals (object other) => other is PosFunc f && f.function () == function (); - } + public override string ToString () => $"PosFunc({function ()})"; - /// - /// Creates a "PosFunc" from the specified function. - /// - /// The function to be executed. - /// The returned from the function. - public static Pos Function (Func function) - { - return new PosFunc (function); - } + public override int GetHashCode () => function.GetHashCode (); - internal class PosFactor : Pos { - float factor; - - public PosFactor (float n) - { - this.factor = n; - } - - internal override int Anchor (int width) - { - return (int)(width * factor); - } - - public override string ToString () - { - return $"Factor({factor})"; - } - - public override int GetHashCode () => factor.GetHashCode (); - - public override bool Equals (object other) => other is PosFactor f && f.factor == factor; - } - - /// - /// Creates a percentage object - /// - /// The percent object. - /// 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 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), - /// }; - /// - /// - public static Pos Percent (float n) - { - if (n < 0 || n > 100) - throw new ArgumentException ("Percent value must be between 0 and 100"); - - return new PosFactor (n / 100); - } - - internal class PosAnchorEnd : Pos { - int n; - - public PosAnchorEnd (int n) - { - this.n = n; - } - - internal override int Anchor (int width) - { - return width - n; - } - - public override string ToString () - { - return $"AnchorEnd({n})"; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is PosAnchorEnd anchorEnd && anchorEnd.n == n; - } - - /// - /// Creates a object that is anchored to the end (right side or bottom) of the dimension, - /// useful to flush the layout from the right or bottom. - /// - /// The object anchored to the end (the bottom or the right side). - /// Optional margin to place to the right or below. - /// - /// 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); - /// - /// - public static Pos AnchorEnd (int margin = 0) - { - if (margin < 0) - throw new ArgumentException ("Margin must be positive"); - - return new PosAnchorEnd (margin); - } - - internal class PosCenter : Pos { - internal override int Anchor (int width) - { - return width / 2; - } - - public override string ToString () - { - return "Center"; - } - } - - /// - /// Returns 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 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), - /// }; - /// - /// - public static Pos Center () - { - return new PosCenter (); - } - - internal class PosAbsolute : Pos { - int n; - public PosAbsolute (int n) { this.n = n; } - - public override string ToString () - { - return $"Absolute({n})"; - } - - internal override int Anchor (int width) - { - return n; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is PosAbsolute abs && abs.n == n; - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static implicit operator Pos (int n) - { - return new PosAbsolute (n); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static Pos At (int n) - { - return new PosAbsolute (n); - } - - internal class PosCombine : Pos { - internal Pos left, right; - internal bool add; - public PosCombine (bool add, Pos left, Pos right) - { - this.left = left; - this.right = right; - this.add = add; - } - - internal override int Anchor (int width) - { - var la = left.Anchor (width); - var ra = right.Anchor (width); - if (add) - return la + ra; - else - return la - ra; - } - - public override string ToString () - { - return $"Combine({left}{(add ? '+' : '-')}{right})"; - } - - } - - /// - /// 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 Pos operator + (Pos left, Pos right) - { - if (left is PosAbsolute && right is PosAbsolute) { - return new PosAbsolute (left.Anchor (0) + right.Anchor (0)); - } - PosCombine newPos = new PosCombine (true, left, right); - SetPosCombine (left, newPos); - return newPos; - } - - /// - /// Subtracts a from a , yielding a new . - /// - /// The to subtract from (the minuend). - /// The to subtract (the subtrahend). - /// The that is the left minus right. - public static Pos operator - (Pos left, Pos right) - { - if (left is PosAbsolute && right is PosAbsolute) { - return new PosAbsolute (left.Anchor (0) - right.Anchor (0)); - } - PosCombine newPos = new PosCombine (false, left, right); - SetPosCombine (left, newPos); - return newPos; - } - - static void SetPosCombine (Pos left, PosCombine newPos) - { - var view = left as PosView; - if (view != null) { - view.Target.SetNeedsLayout (); - } - } - - internal class PosView : Pos { - public View Target; - int side; - public PosView (View view, int side) - { - Target = view; - this.side = side; - } - 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; - default: - return 0; - } - } - - 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; - } - // Note: We do not checkt `Target` for null here to intentionally throw if so - return $"View({tside},{Target.ToString ()})"; - } - - public override int GetHashCode () => Target.GetHashCode (); - - public override bool Equals (object other) => other is PosView abs && abs.Target == Target; - } - - /// - /// Returns a object 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) => new PosCombine (true, new PosView (view, 0), new Pos.PosAbsolute (0)); - - /// - /// Returns a object 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) => new PosCombine (true, new PosView (view, 0), new Pos.PosAbsolute (0)); - - /// - /// Returns a object 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) => new PosCombine (true, new PosView (view, 1), new Pos.PosAbsolute (0)); - - /// - /// Returns a object 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) => new PosCombine (true, new PosView (view, 1), new Pos.PosAbsolute (0)); - - /// - /// Returns a object tracks the Right (X+Width) coordinate of the specified . - /// - /// The that depends on the other view. - /// The that will be tracked. - public static Pos Right (View view) => new PosCombine (true, new PosView (view, 2), new Pos.PosAbsolute (0)); - - /// - /// Returns a object tracks the Bottom (Y+Height) coordinate of the specified - /// - /// The that depends on the other view. - /// The that will be tracked. - public static Pos Bottom (View view) => new PosCombine (true, new PosView (view, 3), new Pos.PosAbsolute (0)); - - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () => Anchor (0).GetHashCode (); - - /// Determines whether the specified object is equal to the current object. - /// The object to compare with the current object. - /// - /// if the specified object is equal to the current object; otherwise, . - public override bool Equals (object other) => other is Pos abs && abs == this; + public override bool Equals (object other) => other is PosFunc f && f.function () == function (); } /// - /// Dim properties of a to control the position. + /// Creates a "PosFunc" from the specified function. /// - /// - /// - /// Use the Dim objects on the Width or Height properties of a to control the position. - /// - /// - /// These can be used to set the absolute position, when merely assigning an - /// integer value (via the implicit integer to Pos conversion), and they can be combined - /// to produce more useful layouts, like: Pos.Center - 3, which would shift the position - /// of the 3 characters to the left after centering for example. - /// - /// - public class Dim { - internal virtual int Anchor (int width) - { - return 0; - } + /// The function to be executed. + /// The returned from the function. + public static Pos Function (Func function) => new PosFunc (function); - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class DimFunc : Dim { - Func function; + internal class PosFactor : Pos { + float factor; - public DimFunc (Func n) - { - this.function = n; - } + public PosFactor (float n) => factor = n; - internal override int Anchor (int width) - { - return function (); - } + internal override int Anchor (int width) => (int)(width * factor); - public override string ToString () - { - return $"DimFunc({function ()})"; - } + public override string ToString () => $"Factor({factor})"; - public override int GetHashCode () => function.GetHashCode (); + public override int GetHashCode () => factor.GetHashCode (); - public override bool Equals (object other) => other is DimFunc f && f.function () == function (); - } - - /// - /// Creates a "DimFunc" from the specified function. - /// - /// The function to be executed. - /// The returned from the function. - public static Dim Function (Func function) - { - return new DimFunc (function); - } - - internal class DimFactor : Dim { - float factor; - bool remaining; - - public DimFactor (float n, bool r = false) - { - factor = n; - remaining = r; - } - - internal override int Anchor (int width) - { - return (int)(width * factor); - } - - public bool IsFromRemaining () - { - return remaining; - } - - public override string ToString () - { - return $"Factor({factor},{remaining})"; - } - - public override int GetHashCode () => factor.GetHashCode (); - - public override bool Equals (object other) => other is DimFactor f && f.factor == factor && f.remaining == remaining; - } - - /// - /// Creates a percentage object - /// - /// 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. - /// - /// 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), - /// }; - /// - /// - public static Dim Percent (float n, bool r = false) - { - if (n < 0 || n > 100) - throw new ArgumentException ("Percent value must be between 0 and 100"); - - return new DimFactor (n / 100, r); - } - - internal class DimAbsolute : Dim { - int n; - public DimAbsolute (int n) { this.n = n; } - - public override string ToString () - { - return $"Absolute({n})"; - } - - internal override int Anchor (int width) - { - return n; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is DimAbsolute abs && abs.n == n; - } - - internal class DimFill : Dim { - int margin; - public DimFill (int margin) { this.margin = margin; } - - public override string ToString () - { - return $"Fill({margin})"; - } - - internal override int Anchor (int width) - { - return width - margin; - } - - public override int GetHashCode () => margin.GetHashCode (); - - public override bool Equals (object other) => other is DimFill fill && fill.margin == margin; - } - - /// - /// Initializes a new instance of the class that fills the dimension, but leaves the specified number of colums for a margin. - /// - /// The Fill dimension. - /// Margin to use. - public static Dim Fill (int margin = 0) - { - return new DimFill (margin); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the pos. - public static implicit operator Dim (int n) - { - return new DimAbsolute (n); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static Dim Sized (int n) - { - return new DimAbsolute (n); - } - - internal class DimCombine : Dim { - internal Dim left, right; - internal bool add; - public DimCombine (bool add, Dim left, Dim right) - { - this.left = left; - this.right = right; - this.add = add; - } - - internal override int Anchor (int width) - { - var la = left.Anchor (width); - var ra = right.Anchor (width); - if (add) - return la + ra; - else - return la - ra; - } - - public override string ToString () - { - return $"Combine({left}{(add ? '+' : '-')}{right})"; - } - - } - - /// - /// 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) - { - if (left is DimAbsolute && right is DimAbsolute) { - return new DimAbsolute (left.Anchor (0) + right.Anchor (0)); - } - DimCombine newDim = new DimCombine (true, left, right); - SetDimCombine (left, newDim); - return newDim; - } - - /// - /// Subtracts a from a , yielding a new . - /// - /// 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) - { - if (left is DimAbsolute && right is DimAbsolute) { - return new DimAbsolute (left.Anchor (0) - right.Anchor (0)); - } - DimCombine newDim = new DimCombine (false, left, right); - SetDimCombine (left, newDim); - return newDim; - } - - static void SetDimCombine (Dim left, DimCombine newPos) - { - var view = left as DimView; - if (view != null) { - view.Target.SetNeedsLayout (); - } - } - - internal class DimView : Dim { - public View Target; - int side; - public DimView (View view, int side) - { - Target = view; - this.side = side; - } - - internal override int Anchor (int width) - { - switch (side) { - case 0: return Target.Frame.Height; - case 1: return Target.Frame.Width; - default: - return 0; - } - } - - public override string ToString () - { - string tside; - switch (side) { - case 0: tside = "Height"; break; - case 1: tside = "Width"; break; - default: tside = "unknown"; break; - } - return $"View({tside},{Target.ToString ()})"; - } - - public override int GetHashCode () => Target.GetHashCode (); - - public override bool Equals (object other) => other is DimView abs && abs.Target == Target; - } - /// - /// Returns a object tracks the Width of the specified . - /// - /// The of the other . - /// The view that will be tracked. - public static Dim Width (View view) => new DimView (view, 1); - - /// - /// Returns a object tracks the Height of the specified . - /// - /// The of the other . - /// The view that will be tracked. - public static Dim Height (View view) => new DimView (view, 0); - - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () => Anchor (0).GetHashCode (); - - /// Determines whether the specified object is equal to the current object. - /// The object to compare with the current object. - /// - /// if the specified object is equal to the current object; otherwise, . - public override bool Equals (object other) => other is Dim abs && abs == this; + public override bool Equals (object other) => other is PosFactor f && f.factor == factor; } + + /// + /// Creates a percentage object + /// + /// The percent object. + /// 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 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), + /// }; + /// + /// + public static Pos Percent (float n) + { + if (n < 0 || n > 100) { + throw new ArgumentException ("Percent value must be between 0 and 100"); + } + + return new PosFactor (n / 100); + } + + internal class PosAnchorEnd : Pos { + int n; + + public PosAnchorEnd (int n) => this.n = n; + + internal override int Anchor (int width) => width - n; + + public override string ToString () => $"AnchorEnd({n})"; + + public override int GetHashCode () => n.GetHashCode (); + + public override bool Equals (object other) => other is PosAnchorEnd anchorEnd && anchorEnd.n == n; + } + + /// + /// Creates a object that is anchored to the end (right side or bottom) of the dimension, + /// useful to flush the layout from the right or bottom. + /// + /// The object anchored to the end (the bottom or the right side). + /// Optional margin to place to the right or below. + /// + /// 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); + /// + /// + public static Pos AnchorEnd (int margin = 0) + { + if (margin < 0) { + throw new ArgumentException ("Margin must be positive"); + } + + return new PosAnchorEnd (margin); + } + + internal class PosCenter : Pos { + internal override int Anchor (int width) => width / 2; + + public override string ToString () => "Center"; + } + + /// + /// Returns 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 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), + /// }; + /// + /// + public static Pos Center () => new PosCenter (); + + internal class PosAbsolute : Pos { + int n; + public PosAbsolute (int n) => this.n = n; + + public override string ToString () => $"Absolute({n})"; + + internal override int Anchor (int width) => n; + + public override int GetHashCode () => n.GetHashCode (); + + public override bool Equals (object other) => other is PosAbsolute abs && abs.n == n; + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static implicit operator Pos (int n) => new PosAbsolute (n); + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static Pos At (int n) => new PosAbsolute (n); + + internal class PosCombine : Pos { + internal Pos left, right; + internal bool add; + + public PosCombine (bool add, Pos left, Pos right) + { + this.left = left; + this.right = right; + this.add = add; + } + + internal override int Anchor (int width) + { + int la = left.Anchor (width); + int ra = right.Anchor (width); + if (add) { + return la + ra; + } else { + return la - ra; + } + } + + public override string ToString () => $"Combine({left}{(add ? '+' : '-')}{right})"; + } + + /// + /// 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 Pos operator + (Pos left, Pos right) + { + if (left is PosAbsolute && right is PosAbsolute) { + return new PosAbsolute (left.Anchor (0) + right.Anchor (0)); + } + var newPos = new PosCombine (true, left, right); + SetPosCombine (left, newPos); + return newPos; + } + + /// + /// Subtracts a from a , yielding a new . + /// + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). + /// The that is the left minus right. + public static Pos operator - (Pos left, Pos right) + { + if (left is PosAbsolute && right is PosAbsolute) { + return new PosAbsolute (left.Anchor (0) - right.Anchor (0)); + } + var newPos = new PosCombine (false, left, right); + SetPosCombine (left, newPos); + return newPos; + } + + static void SetPosCombine (Pos left, PosCombine newPos) + { + var view = left as PosView; + if (view != null) { + view.Target.SetNeedsLayout (); + } + } + + internal class PosView : Pos { + public View Target; + int side; + + public PosView (View view, int side) + { + Target = view; + this.side = side; + } + + 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; + default: + return 0; + } + } + + 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; + } + // Note: We do not checkt `Target` for null here to intentionally throw if so + return $"View({tside},{Target.ToString ()})"; + } + + public override int GetHashCode () => Target.GetHashCode (); + + public override bool Equals (object other) => other is PosView abs && abs.Target == Target; + } + + /// + /// Returns a object 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) => new PosCombine (true, new PosView (view, 0), new PosAbsolute (0)); + + /// + /// Returns a object 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) => new PosCombine (true, new PosView (view, 0), new PosAbsolute (0)); + + /// + /// Returns a object 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) => new PosCombine (true, new PosView (view, 1), new PosAbsolute (0)); + + /// + /// Returns a object 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) => new PosCombine (true, new PosView (view, 1), new PosAbsolute (0)); + + /// + /// Returns a object tracks the Right (X+Width) coordinate of the specified . + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Right (View view) => new PosCombine (true, new PosView (view, 2), new PosAbsolute (0)); + + /// + /// Returns a object tracks the Bottom (Y+Height) coordinate of the specified + /// + /// The that depends on the other view. + /// The that will be tracked. + public static Pos Bottom (View view) => new PosCombine (true, new PosView (view, 3), new PosAbsolute (0)); + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode () => Anchor (0).GetHashCode (); + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + public override bool Equals (object other) => other is Pos abs && abs == this; } + +/// +/// Dim properties of a to control the dimensions. +/// +/// +/// +/// Use the Dim objects on the Width or Height properties of a to control the dimensions. +/// +/// +/// +/// +public class Dim { + internal virtual int Anchor (int width) => 0; + + // Helper class to provide dynamic value by the execution of a function that returns an integer. + internal class DimFunc : Dim { + Func function; + + public DimFunc (Func n) => function = n; + + internal override int Anchor (int width) => function (); + + public override string ToString () => $"DimFunc({function ()})"; + + public override int GetHashCode () => function.GetHashCode (); + + public override bool Equals (object other) => other is DimFunc f && f.function () == function (); + } + + /// + /// Creates a "DimFunc" from the specified function. + /// + /// The function to be executed. + /// The returned from the function. + public static Dim Function (Func function) => new DimFunc (function); + + internal class DimFactor : Dim { + float factor; + bool remaining; + + public DimFactor (float n, bool r = false) + { + factor = n; + remaining = r; + } + + internal override int Anchor (int width) => (int)(width * factor); + + public bool IsFromRemaining () => remaining; + + public override string ToString () => $"Factor({factor},{remaining})"; + + public override int GetHashCode () => factor.GetHashCode (); + + public override bool Equals (object other) => other is DimFactor f && f.factor == factor && f.remaining == remaining; + } + + /// + /// 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. + /// + /// 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), + /// }; + /// + /// + public static Dim Percent (float n, bool r = false) + { + if (n is < 0 or > 100) { + throw new ArgumentException ("Percent value must be between 0 and 100"); + } + + return new DimFactor (n / 100, r); + } + + internal class DimAbsolute : Dim { + readonly int _n; + public DimAbsolute (int n) => _n = n; + + public override string ToString () => $"Absolute({_n})"; + + internal override int Anchor (int width) => _n; + + public override int GetHashCode () => _n.GetHashCode (); + + public override bool Equals (object other) => other is DimAbsolute abs && abs._n == _n; + } + + internal class DimFill : Dim { + readonly int _margin; + public DimFill (int margin) => _margin = margin; + + public override string ToString () => $"Fill({_margin})"; + + internal override int Anchor (int width) => width - _margin; + + public override int GetHashCode () => _margin.GetHashCode (); + + public override bool Equals (object other) => other is DimFill fill && fill._margin == _margin; + } + + /// + /// Initializes a new instance of the class that fills the dimension, but leaves the specified number of columns for a margin. + /// + /// The Fill dimension. + /// Margin to use. + public static Dim Fill (int margin = 0) => new DimFill (margin); + + internal class DimAutoSize : Dim { + readonly int _margin; + public DimAutoSize (int margin) + { + _margin = margin; + } + + public override string ToString () => $"AutoSize({_margin})"; + + internal override int Anchor (int width) + { + return width - _margin; + } + + public override int GetHashCode () => _margin.GetHashCode (); + + public override bool Equals (object other) => other is DimAutoSize autoSize && autoSize._margin == _margin; + } + + /// + /// Creates an AutoSize object that is the size required to fit all of the view's SubViews. + /// + /// The AutoSize object. + /// Margin to use. + /// + /// 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); + /// + /// + public static Dim AutoSize (int margin = 0) + { + return new DimAutoSize (margin); + } + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the pos. + public static implicit operator Dim (int n) => new DimAbsolute (n); + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static Dim Sized (int n) => new DimAbsolute (n); + + internal class DimCombine : Dim { + internal Dim _left, _right; + internal bool _add; + + public DimCombine (bool add, Dim left, Dim right) + { + _left = left; + _right = right; + _add = add; + } + + internal override int Anchor (int width) + { + int la = _left.Anchor (width); + int ra = _right.Anchor (width); + if (_add) { + return la + ra; + } else { + return la - ra; + } + } + + public override string ToString () => $"Combine({_left}{(_add ? '+' : '-')}{_right})"; + } + + /// + /// 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) + { + if (left is DimAbsolute && right is DimAbsolute) { + return new DimAbsolute (left.Anchor (0) + right.Anchor (0)); + } + var newDim = new DimCombine (true, left, right); + SetDimCombine (left, newDim); + return newDim; + } + + /// + /// Subtracts a from a , yielding a new . + /// + /// 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) + { + if (left is DimAbsolute && right is DimAbsolute) { + return new DimAbsolute (left.Anchor (0) - right.Anchor (0)); + } + var newDim = new DimCombine (false, left, right); + SetDimCombine (left, newDim); + return newDim; + } + + // BUGBUG: newPos is never used. + static void SetDimCombine (Dim left, DimCombine newPos) => (left as DimView)?.Target.SetNeedsLayout (); + + internal class DimView : Dim { + public View Target { get; init; } + readonly int _side; + + public DimView (View view, int side) + { + Target = view; + _side = side; + } + + internal override int Anchor (int width) => _side switch { + 0 => Target.Frame.Height, + 1 => Target.Frame.Width, + _ => 0 + }; + + public override string ToString () + { + string tside = _side switch { + 0 => "Height", + 1 => "Width", + _ => "unknown" + }; + return $"View({tside},{Target})"; + } + + public override int GetHashCode () => Target.GetHashCode (); + + public override bool Equals (object other) => other is DimView abs && abs.Target == Target; + } + + /// + /// Returns a object tracks the Width of the specified . + /// + /// The of the other . + /// The view that will be tracked. + public static Dim Width (View view) => new DimView (view, 1); + + /// + /// Returns a object tracks the Height of the specified . + /// + /// The of the other . + /// The view that will be tracked. + public static Dim Height (View view) => new DimView (view, 0); + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode () => Anchor (0).GetHashCode (); + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + public override bool Equals (object other) => other is Dim abs && abs == this; +} \ No newline at end of file diff --git a/Terminal.Gui/Text/ViewLayout.cs b/Terminal.Gui/View/ViewLayout.cs similarity index 95% rename from Terminal.Gui/Text/ViewLayout.cs rename to Terminal.Gui/View/ViewLayout.cs index faeb8e174..0485569ef 100644 --- a/Terminal.Gui/Text/ViewLayout.cs +++ b/Terminal.Gui/View/ViewLayout.cs @@ -652,7 +652,7 @@ namespace Terminal.Gui { // the superview's width or height // the current Pos (View.X or View.Y) // the current Dim (View.Width or View.Height) - (int newLocation, int newDimension) GetNewLocationAndDimension (int superviewLocation, int superviewDimension, Pos pos, Dim dim, int autosizeDimension) + (int newLocation, int newDimension) GetNewLocationAndDimension (bool horiz, int superviewLocation, int superviewDimension, Pos pos, Dim dim, int autosizeDimension) { int newDimension, newLocation; @@ -669,14 +669,14 @@ namespace Terminal.Gui { case Pos.PosCombine combine: int left, right; - (left, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.left, dim, autosizeDimension); - (right, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.right, dim, autosizeDimension); + (left, newDimension) = GetNewLocationAndDimension (horiz, superviewLocation, superviewDimension, combine.left, dim, autosizeDimension); + (right, newDimension) = GetNewLocationAndDimension (horiz, superviewLocation, superviewDimension, combine.right, dim, autosizeDimension); if (combine.add) { newLocation = left + right; } else { newLocation = left - right; } - newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); + newDimension = Math.Max (CalculateNewDimension (horiz, dim, newLocation, superviewDimension, autosizeDimension), 0); break; case Pos.PosAbsolute: @@ -686,7 +686,7 @@ namespace Terminal.Gui { case Pos.PosView: default: newLocation = pos?.Anchor (superviewDimension) ?? 0; - newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); + newDimension = Math.Max (CalculateNewDimension (horiz, dim, newLocation, superviewDimension, autosizeDimension), 0); break; } return (newLocation, newDimension); @@ -695,7 +695,7 @@ namespace Terminal.Gui { // Recursively calculates the new dimension (width or height) of the given Dim given: // the current location (x or y) // the current dimension (width or height) - int CalculateNewDimension (Dim d, int location, int dimension, int autosize) + int CalculateNewDimension (bool horiz, Dim d, int location, int dimension, int autosize) { int newDimension; switch (d) { @@ -703,20 +703,33 @@ namespace Terminal.Gui { newDimension = AutoSize ? autosize : dimension; break; case Dim.DimCombine combine: - int leftNewDim = CalculateNewDimension (combine.left, location, dimension, autosize); - int rightNewDim = CalculateNewDimension (combine.right, location, dimension, autosize); - if (combine.add) { + int leftNewDim = CalculateNewDimension (horiz, combine._left, location, dimension, autosize); + int rightNewDim = CalculateNewDimension (horiz, combine._right, location, dimension, autosize); + if (combine._add) { newDimension = leftNewDim + rightNewDim; } else { newDimension = leftNewDim - rightNewDim; } newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; break; - + case Dim.DimFactor factor when !factor.IsFromRemaining (): newDimension = d.Anchor (dimension); newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; break; + + case Dim.DimAutoSize: + var thickness = GetFramesThickness (); + if (horiz) { + var furthestLeft = Subviews.Where (v => v.Frame.X >= 0).Min (v => v.Frame.X); + var furthestRight = Subviews.Where (v => v.Frame.X >= 0).Max (v => v.Frame.X + v.Frame.Width); + newDimension = furthestLeft + furthestRight + thickness.Left + thickness.Right; + } else { + var furthestTop = Subviews.Where (v => v.Frame.Y >= 0).Min (v => v.Frame.Y); + var furthestBottom = Subviews.Where (v => v.Frame.Y >= 0).Max (v => v.Frame.Y + v.Frame.Height); + newDimension = furthestTop + furthestBottom + thickness.Top + thickness.Bottom; + } + break; case Dim.DimFill: default: @@ -729,10 +742,10 @@ namespace Terminal.Gui { } // horizontal - (newX, newW) = GetNewLocationAndDimension (superviewFrame.X, superviewFrame.Width, _x, _width, autosize.Width); + (newX, newW) = GetNewLocationAndDimension (true, superviewFrame.X, superviewFrame.Width, _x, _width, autosize.Width); // vertical - (newY, newH) = GetNewLocationAndDimension (superviewFrame.Y, superviewFrame.Height, _y, _height, autosize.Height); + (newY, newH) = GetNewLocationAndDimension (false, superviewFrame.Y, superviewFrame.Height, _y, _height, autosize.Height); var r = new Rect (newX, newY, newW, newH); if (Frame != r) { @@ -815,8 +828,8 @@ namespace Terminal.Gui { } return; case Dim.DimCombine dc: - CollectDim (dc.left, from, ref nNodes, ref nEdges); - CollectDim (dc.right, from, ref nNodes, ref nEdges); + CollectDim (dc._left, from, ref nNodes, ref nEdges); + CollectDim (dc._right, from, ref nNodes, ref nEdges); break; } } diff --git a/UICatalog/Scenarios/DimAutoSize.cs b/UICatalog/Scenarios/DimAutoSize.cs new file mode 100644 index 000000000..34d428c74 --- /dev/null +++ b/UICatalog/Scenarios/DimAutoSize.cs @@ -0,0 +1,67 @@ +using Terminal.Gui; + +namespace UICatalog.Scenarios; + +[ScenarioMetadata ("DimAutoSize", "Demonstrates Dim.AutoSize")] +[ScenarioCategory ("Layout")] +public class DimAutoSize : Scenario { + public override void Init () + { + // The base `Scenario.Init` implementation: + // - Calls `Application.Init ()` + // - Adds a full-screen Window to Application.Top with a title + // that reads "Press to Quit". Access this Window with `this.Win`. + // - Sets the Theme & the ColorScheme property of `this.Win` to `colorScheme`. + // To override this, implement an override of `Init`. + + //base.Init (); + + // A common, alternate, implementation where `this.Win` is not used is below. This code + // leverages ConfigurationManager to borrow the color scheme settings from UICatalog: + + Application.Init (); + ConfigurationManager.Themes.Theme = Theme; + ConfigurationManager.Apply (); + Application.Top.ColorScheme = Colors.ColorSchemes [TopLevelColorScheme]; + } + + public override void Setup () + { + // Put scenario code here (in a real app, this would be the code + // that would setup the app before `Application.Run` is called`). + // With a Scenario, after UI Catalog calls `Scenario.Setup` it calls + // `Scenario.Run` which calls `Application.Run`. Example: + + var textField = new TextField { Text = "Type here", X = 1, Y = 0, Width = 20, Height = 1 }; + + var label = new Label { + X = Pos.Left (textField), + Y = Pos.Bottom (textField), + AutoSize = true, + }; + + textField.TextChanged += (s, e) => { + label.Text = textField.Text; + }; + + var button = new Button () { Text = "Press to make button move down.", + X = Pos.Center(), + Y = Pos.Bottom (label), + }; + button.Clicked += (s, e) => { + button.Y = button.Frame.Y + 1; + }; + + var view = new FrameView () { + Title = "Type in the TextField to make it grow.", + X = 3, + Y = 3, + Width = Dim.AutoSize (), + Height = Dim.AutoSize () + }; + + view.Add (textField, label, button); + + Application.Top.Add (view); + } +} \ No newline at end of file diff --git a/UnitTests/View/Layout/DimTests.cs b/UnitTests/View/Layout/DimTests.cs index 356934744..1dbc82115 100644 --- a/UnitTests/View/Layout/DimTests.cs +++ b/UnitTests/View/Layout/DimTests.cs @@ -1251,8 +1251,8 @@ namespace Terminal.Gui.ViewTests { Assert.Equal (99, dimFill.Anchor (100)); var dimCombine = new Dim.DimCombine (true, dimFactor, dimAbsolute); - Assert.Equal (dimCombine.left, dimFactor); - Assert.Equal (dimCombine.right, dimAbsolute); + Assert.Equal (dimCombine._left, dimFactor); + Assert.Equal (dimCombine._right, dimAbsolute); Assert.Equal (20, dimCombine.Anchor (100)); var view = new View (new Rect (20, 10, 20, 1));