diff --git a/Terminal.Gui/Drawing/Aligner.cs b/Terminal.Gui/Drawing/Aligner.cs new file mode 100644 index 0000000000..1c96a2ac0c --- /dev/null +++ b/Terminal.Gui/Drawing/Aligner.cs @@ -0,0 +1,369 @@ +using System.ComponentModel; + +namespace Terminal.Gui; + +/// +/// Aligns items within a container based on the specified . Both horizontal and vertical +/// alignments are supported. +/// +public class Aligner : INotifyPropertyChanged +{ + private Alignment _alignment; + + /// + /// Gets or sets how the aligns items within a container. + /// + /// + /// + /// provides additional options for aligning items in a container. + /// + /// + public Alignment Alignment + { + get => _alignment; + set + { + _alignment = value; + PropertyChanged?.Invoke (this, new (nameof (Alignment))); + } + } + + private AlignmentModes _alignmentMode = AlignmentModes.StartToEnd; + + /// + /// Gets or sets the modes controlling . + /// + public AlignmentModes AlignmentModes + { + get => _alignmentMode; + set + { + _alignmentMode = value; + PropertyChanged?.Invoke (this, new (nameof (AlignmentModes))); + } + } + + private int _containerSize; + + /// + /// The size of the container. + /// + public int ContainerSize + { + get => _containerSize; + set + { + _containerSize = value; + PropertyChanged?.Invoke (this, new (nameof (ContainerSize))); + } + } + + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Takes a list of item sizes and returns a list of the positions of those items when aligned within + /// + /// using the and settings. + /// + /// The sizes of the items to align. + /// The locations of the items, from left/top to right/bottom. + public int [] Align (int [] sizes) { return Align (Alignment, AlignmentModes, ContainerSize, sizes); } + + /// + /// Takes a list of item sizes and returns a list of the positions of those items when aligned within + /// + /// using specified parameters. + /// + /// Specifies how the items will be aligned. + /// + /// The size of the container. + /// The sizes of the items to align. + /// The positions of the items, from left/top to right/bottom. + public static int [] Align (in Alignment alignment, in AlignmentModes alignmentMode, in int containerSize, in int [] sizes) + { + if (sizes.Length == 0) + { + return []; + } + + var sizesCopy = sizes; + if (alignmentMode.FastHasFlags (AlignmentModes.EndToStart)) + { + sizesCopy = sizes.Reverse ().ToArray (); + } + + int maxSpaceBetweenItems = alignmentMode.FastHasFlags (AlignmentModes.AddSpaceBetweenItems) ? 1 : 0; + int totalItemsSize = sizes.Sum (); + int totalGaps = sizes.Length - 1; // total gaps between items + int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spacesToGive if we had enough room + int spacesToGive = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out + + if (totalItemsSize >= containerSize) + { + spacesToGive = 0; + } + else if (totalItemsAndSpaces > containerSize) + { + spacesToGive = containerSize - totalItemsSize; + } + + switch (alignment) + { + case Alignment.Start: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: + return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + + case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: + return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; + } + + break; + + case Alignment.End: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return End (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.StartToEnd | AlignmentModes.IgnoreFirstOrLast: + return IgnoreFirst (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return Start (in sizesCopy, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + + case AlignmentModes.EndToStart | AlignmentModes.IgnoreFirstOrLast: + return IgnoreLast (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); ; + } + + break; + + case Alignment.Center: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive); + + case AlignmentModes.EndToStart: + return Center (in sizesCopy, containerSize, totalItemsSize, maxSpaceBetweenItems, spacesToGive).Reverse ().ToArray (); + } + + break; + + case Alignment.Fill: + switch (alignmentMode & ~AlignmentModes.AddSpaceBetweenItems) + { + case AlignmentModes.StartToEnd: + return Fill (in sizesCopy, containerSize, totalItemsSize); + + case AlignmentModes.EndToStart: + return Fill (in sizesCopy, containerSize, totalItemsSize).Reverse ().ToArray (); + } + + break; + + default: + throw new ArgumentOutOfRangeException (nameof (alignment), alignment, null); + } + + return []; + } + + internal static int [] Start (ref readonly int [] sizes, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == 0) + { + positions [0] = 0; // first item position + + continue; + } + + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + // subsequent items are placed one space after the previous item + positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; + } + + return positions; + } + + internal static int [] IgnoreFirst ( + ref readonly int [] sizes, + int containerSize, + int totalItemsSize, + int maxSpaceBetweenItems, + int spacesToGive + ) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + var currentPosition = 0; + positions [0] = currentPosition; // first item is flush left + + for (int i = sizes.Length - 1; i >= 0; i--) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == sizes.Length - 1) + { + // start at right + currentPosition = Math.Max (totalItemsSize, containerSize) - sizes [i]; + positions [i] = currentPosition; + } + + if (i < sizes.Length - 1 && i > 0) + { + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition - sizes [i] - spaceBefore; + currentPosition = positions [i]; + } + } + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + positions [0] = 0; // single item is flush left + } + + return positions; + } + + internal static int [] IgnoreLast ( + ref readonly int [] sizes, + int containerSize, + int totalItemsSize, + int maxSpaceBetweenItems, + int spacesToGive + ) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + var currentPosition = 0; + if (totalItemsSize > containerSize) + { + currentPosition = containerSize - totalItemsSize - spacesToGive; + } + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i < sizes.Length - 1) + { + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition; + currentPosition += sizes [i] + spaceBefore; + } + } + + positions [sizes.Length - 1] = containerSize - sizes [^1]; + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + + positions [0] = containerSize - sizes [0]; // single item is flush right + } + + return positions; + } + + internal static int [] Fill (ref readonly int [] sizes, int containerSize, int totalItemsSize) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0; + int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0; + var currentPosition = 0; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + positions [i] = currentPosition; + int extraSpace = i < remainder ? 1 : 0; + currentPosition += sizes [i] + spaceBetween + extraSpace; + } + + return positions; + } + + internal static int [] Center (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + + if (sizes.Length > 1) + { + // remaining space to be distributed before first and after the items + int remainingSpace = containerSize - totalItemsSize - spacesToGive; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + + if (i == 0) + { + positions [i] = remainingSpace / 2; // first item position + + continue; + } + + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + // subsequent items are placed one space after the previous item + positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; + } + } + else if (sizes.Length == 1) + { + CheckSizeCannotBeNegative (0, in sizes); + positions [0] = (containerSize - sizes [0]) / 2; // single item is centered + } + + return positions; + } + + internal static int [] End (ref readonly int [] sizes, int containerSize, int totalItemsSize, int maxSpaceBetweenItems, int spacesToGive) + { + var positions = new int [sizes.Length]; // positions of the items. the return value. + int currentPosition = containerSize - totalItemsSize - spacesToGive; + + for (var i = 0; i < sizes.Length; i++) + { + CheckSizeCannotBeNegative (i, in sizes); + int spaceBefore = spacesToGive-- > 0 ? maxSpaceBetweenItems : 0; + + positions [i] = currentPosition; + currentPosition += sizes [i] + spaceBefore; + } + + return positions; + } + + private static void CheckSizeCannotBeNegative (int i, ref readonly int [] sizes) + { + if (sizes [i] < 0) + { + throw new ArgumentException ("The size of an item cannot be negative."); + } + } +} diff --git a/Terminal.Gui/Drawing/Alignment.cs b/Terminal.Gui/Drawing/Alignment.cs new file mode 100644 index 0000000000..40061a8c19 --- /dev/null +++ b/Terminal.Gui/Drawing/Alignment.cs @@ -0,0 +1,82 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Determines the position of items when arranged in a container. +/// +[GenerateEnumExtensionMethods (FastHasFlags = true)] + +public enum Alignment +{ + /// + /// The items will be aligned to the start (left or top) of the container. + /// + /// + /// + /// If the container is smaller than the total size of the items, the end items will be clipped (their locations + /// will be greater than the container size). + /// + /// + /// The enumeration provides additional options for aligning items in a container. + /// + /// + /// + /// + /// |111 2222 33333 | + /// + /// + Start = 0, + + /// + /// The items will be aligned to the end (right or bottom) of the container. + /// + /// + /// + /// If the container is smaller than the total size of the items, the start items will be clipped (their locations + /// will be negative). + /// + /// + /// The enumeration provides additional options for aligning items in a container. + /// + /// + /// + /// + /// | 111 2222 33333| + /// + /// + End, + + /// + /// Center in the available space. + /// + /// + /// + /// If centering is not possible, the group will be left-aligned. + /// + /// + /// Extra space will be distributed between the items, biased towards the left. + /// + /// + /// + /// + /// | 111 2222 33333 | + /// + /// + Center, + + /// + /// The items will fill the available space. + /// + /// + /// + /// Extra space will be distributed between the items, biased towards the end. + /// + /// + /// + /// + /// |111 2222 33333| + /// + /// + Fill, +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/AlignmentModes.cs b/Terminal.Gui/Drawing/AlignmentModes.cs new file mode 100644 index 0000000000..4de4d5c988 --- /dev/null +++ b/Terminal.Gui/Drawing/AlignmentModes.cs @@ -0,0 +1,52 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Determines alignment modes for . +/// +[Flags] +[GenerateEnumExtensionMethods (FastHasFlags = true)] +public enum AlignmentModes +{ + /// + /// The items will be arranged from start (left/top) to end (right/bottom). + /// + StartToEnd = 0, + + /// + /// The items will be arranged from end (right/bottom) to start (left/top). + /// + /// + /// Not implemented. + /// + EndToStart = 1, + + /// + /// At least one space will be added between items. Useful for justifying text where at least one space is needed. + /// + /// + /// + /// If the total size of the items is greater than the container size, the space between items will be ignored + /// starting from the end. + /// + /// + AddSpaceBetweenItems = 2, + + /// + /// When aligning via or , the item opposite to the alignment (the first or last item) will be ignored. + /// + /// + /// + /// If the container is smaller than the total size of the items, the end items will be clipped (their locations + /// will be greater than the container size). + /// + /// + /// + /// + /// Start: |111 2222 33333| + /// End: |111 2222 33333| + /// + /// + IgnoreFirstOrLast = 4, +} \ No newline at end of file diff --git a/Terminal.Gui/Drawing/Justification.cs b/Terminal.Gui/Drawing/Justification.cs deleted file mode 100644 index f1fba56a83..0000000000 --- a/Terminal.Gui/Drawing/Justification.cs +++ /dev/null @@ -1,333 +0,0 @@ -namespace Terminal.Gui; - -/// -/// Controls how the justifies items within a container. -/// -public enum Justification -{ - /// - /// The items will be aligned to the left. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Left, - - /// - /// The items will be aligned to the right. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Right, - - /// - /// The group will be centered in the container. - /// If centering is not possible, the group will be left-justified. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Centered, - - /// - /// The items will be justified. Space will be added between the items such that the first item - /// is at the start and the right side of the last item against the end. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - Justified, - - /// - /// The first item will be aligned to the left and the remaining will aligned to the right. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - FirstLeftRestRight, - - /// - /// The last item will be aligned to the right and the remaining will aligned to the left. - /// Set to to ensure at least one space between - /// each item. - /// - /// - /// - /// 111 2222 33333 - /// - /// - LastRightRestLeft -} - -/// -/// Justifies items within a container based on the specified . -/// -public class Justifier -{ - /// - /// Gets or sets how the justifies items within a container. - /// - public Justification Justification { get; set; } - - /// - /// The size of the container. - /// - public int ContainerSize { get; set; } - - /// - /// Gets or sets whether puts a space is placed between items. Default is . If , a space will be - /// placed between each item, which is useful for justifying text. - /// - public bool PutSpaceBetweenItems { get; set; } - - /// - /// Takes a list of items and returns their positions when justified within a container wide based on the specified - /// . - /// - /// The sizes of the items to justify. - /// The locations of the items, from left to right. - public int [] Justify (int [] sizes) - { - return Justify (Justification, PutSpaceBetweenItems, ContainerSize, sizes); - } - - /// - /// Takes a list of items and returns their positions when justified within a container wide based on the specified - /// . - /// - /// The sizes of the items to justify. - /// The justification style. - /// - /// The size of the container. - /// The locations of the items, from left to right. - public static int [] Justify (Justification justification, bool putSpaceBetweenItems, int containerSize, int [] sizes) - { - if (sizes.Length == 0) - { - return new int [] { }; - } - - int maxSpaceBetweenItems = putSpaceBetweenItems ? 1 : 0; - - var positions = new int [sizes.Length]; // positions of the items. the return value. - int totalItemsSize = sizes.Sum (); - int totalGaps = sizes.Length - 1; // total gaps between items - int totalItemsAndSpaces = totalItemsSize + totalGaps * maxSpaceBetweenItems; // total size of items and spaces if we had enough room - - int spaces = totalGaps * maxSpaceBetweenItems; // We'll decrement this below to place one space between each item until we run out - if (totalItemsSize >= containerSize) - { - spaces = 0; - } - else if (totalItemsAndSpaces > containerSize) - { - spaces = containerSize - totalItemsSize; - } - - switch (justification) - { - case Justification.Left: - var currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == 0) - { - positions [0] = 0; // first item position - - continue; - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - // subsequent items are placed one space after the previous item - positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; - } - - break; - case Justification.Right: - currentPosition = Math.Max (0, containerSize - totalItemsSize - spaces); - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition; - currentPosition += sizes [i] + spaceBefore; - } - - break; - - case Justification.Centered: - if (sizes.Length > 1) - { - // remaining space to be distributed before first and after the items - int remainingSpace = Math.Max (0, containerSize - totalItemsSize - spaces); - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == 0) - { - positions [i] = remainingSpace / 2; // first item position - - continue; - } - - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - // subsequent items are placed one space after the previous item - positions [i] = positions [i - 1] + sizes [i - 1] + spaceBefore; - } - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = (containerSize - sizes [0]) / 2; // single item is centered - } - - break; - - case Justification.Justified: - int spaceBetween = sizes.Length > 1 ? (containerSize - totalItemsSize) / (sizes.Length - 1) : 0; - int remainder = sizes.Length > 1 ? (containerSize - totalItemsSize) % (sizes.Length - 1) : 0; - currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [i] = currentPosition; - int extraSpace = i < remainder ? 1 : 0; - currentPosition += sizes [i] + spaceBetween + extraSpace; - } - - break; - - // 111 2222 33333 - case Justification.LastRightRestLeft: - if (sizes.Length > 1) - { - currentPosition = 0; - - for (var i = 0; i < sizes.Length; i++) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i < sizes.Length - 1) - { - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition; - currentPosition += sizes [i] + spaceBefore; - } - } - - positions [sizes.Length - 1] = containerSize - sizes [sizes.Length - 1]; - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = containerSize - sizes [0]; // single item is flush right - } - - break; - - // 111 2222 33333 - case Justification.FirstLeftRestRight: - if (sizes.Length > 1) - { - currentPosition = 0; - positions [0] = currentPosition; // first item is flush left - - for (int i = sizes.Length - 1; i >= 0; i--) - { - if (sizes [i] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - if (i == sizes.Length - 1) - { - // start at right - currentPosition = containerSize - sizes [i]; - positions [i] = currentPosition; - } - - if (i < sizes.Length - 1 && i > 0) - { - int spaceBefore = spaces-- > 0 ? maxSpaceBetweenItems : 0; - - positions [i] = currentPosition - sizes [i] - spaceBefore; - currentPosition = positions [i]; - } - } - } - else if (sizes.Length == 1) - { - if (sizes [0] < 0) - { - throw new ArgumentException ("The size of an item cannot be negative."); - } - - positions [0] = 0; // single item is flush left - } - - break; - - default: - throw new ArgumentOutOfRangeException (nameof (justification), justification, null); - } - - return positions; - } -} diff --git a/Terminal.Gui/Drawing/Thickness.cs b/Terminal.Gui/Drawing/Thickness.cs index 6070e6cbe3..ad684470b8 100644 --- a/Terminal.Gui/Drawing/Thickness.cs +++ b/Terminal.Gui/Drawing/Thickness.cs @@ -230,8 +230,8 @@ rect with var tf = new TextFormatter { Text = label is null ? string.Empty : $"{label} {this}", - Alignment = TextAlignment.Centered, - VerticalAlignment = VerticalTextAlignment.Bottom, + Alignment = Alignment.Center, + VerticalAlignment = Alignment.End, AutoSize = true }; tf.Draw (rect, Application.Driver.CurrentAttribute, Application.Driver.CurrentAttribute, rect); diff --git a/Terminal.Gui/Resources/config.json b/Terminal.Gui/Resources/config.json index 368ccd8bf5..8380a14f5e 100644 --- a/Terminal.Gui/Resources/config.json +++ b/Terminal.Gui/Resources/config.json @@ -24,7 +24,8 @@ "Themes": [ { "Default": { - "Dialog.DefaultButtonAlignment": "Center", + "Dialog.DefaultButtonAlignment": "End", + "Dialog.DefaultButtonAlignmentModes": "AddSpaceBetweenItems", "FrameView.DefaultBorderStyle": "Single", "Window.DefaultBorderStyle": "Single", "ColorSchemes": [ diff --git a/Terminal.Gui/Text/TextAlignment.cs b/Terminal.Gui/Text/TextAlignment.cs deleted file mode 100644 index 44950cfd5b..0000000000 --- a/Terminal.Gui/Text/TextAlignment.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Terminal.Gui; - -/// Text alignment enumeration, controls how text is displayed. -public enum TextAlignment -{ - /// The text will be left-aligned. - Left, - - /// The text will be right-aligned. - Right, - - /// The text will be centered horizontally. - Centered, - - /// - /// The text will be justified (spaces will be added to existing spaces such that the text fills the container - /// horizontally). - /// - Justified -} \ No newline at end of file diff --git a/Terminal.Gui/Text/TextFormatter.cs b/Terminal.Gui/Text/TextFormatter.cs index 925f89a389..bee37de67a 100644 --- a/Terminal.Gui/Text/TextFormatter.cs +++ b/Terminal.Gui/Text/TextFormatter.cs @@ -1,3 +1,5 @@ +using System.Diagnostics; + namespace Terminal.Gui; /// @@ -15,14 +17,14 @@ public class TextFormatter private Size _size; private int _tabWidth = 4; private string _text; - private TextAlignment _textAlignment; + private Alignment _textAlignment = Alignment.Start; private TextDirection _textDirection; - private VerticalTextAlignment _textVerticalAlignment; + private Alignment _textVerticalAlignment = Alignment.Start; private bool _wordWrap = true; - /// Controls the horizontal text-alignment property. + /// Get or sets the horizontal text alignment. /// The text alignment. - public TextAlignment Alignment + public Alignment Alignment { get => _textAlignment; set => _textAlignment = EnableNeedsFormat (value); @@ -32,8 +34,7 @@ public TextAlignment Alignment /// /// Used when is using to resize the view's to fit . /// - /// AutoSize is ignored if and - /// are used. + /// AutoSize is ignored if is used. /// /// public bool AutoSize @@ -50,7 +51,7 @@ public bool AutoSize } } - private Size GetAutoSize () + internal Size GetAutoSize () { Size size = CalcRect (0, 0, Text, Direction, TabWidth).Size; return size with @@ -68,9 +69,8 @@ private 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. + /// 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 @@ -97,8 +97,8 @@ public int GetHotKeySpecifierLength (bool isWidth = true) /// public int CursorPosition { get; internal set; } - /// Controls the text-direction property. - /// The text vertical alignment. + /// Gets or sets the text-direction. + /// The text direction. public TextDirection Direction { get => _textDirection; @@ -112,8 +112,7 @@ public TextDirection Direction } } } - - + /// /// Determines if the viewport width will be used or only the text width will be used, /// If all the viewport area will be filled with whitespaces and the same background color @@ -223,9 +222,9 @@ public virtual string Text } } - /// Controls the vertical text-alignment property. + /// Gets or sets the vertical text-alignment. /// The text vertical alignment. - public VerticalTextAlignment VerticalAlignment + public Alignment VerticalAlignment { get => _textVerticalAlignment; set => _textVerticalAlignment = EnableNeedsFormat (value); @@ -318,10 +317,10 @@ public void Draw ( // When text is justified, we lost left or right, so we use the direction to align. - int x, y; + int x = 0, y = 0; // Horizontal Alignment - if (Alignment is TextAlignment.Right) + if (Alignment is Alignment.End) { if (isVertical) { @@ -336,7 +335,7 @@ public void Draw ( CursorPosition = screen.Width - runesWidth + (_hotKeyPos > -1 ? _hotKeyPos : 0); } } - else if (Alignment is TextAlignment.Left) + else if (Alignment is Alignment.Start) { if (isVertical) { @@ -352,7 +351,7 @@ public void Draw ( CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else if (Alignment is TextAlignment.Justified) + else if (Alignment is Alignment.Fill) { if (isVertical) { @@ -375,7 +374,7 @@ public void Draw ( CursorPosition = _hotKeyPos > -1 ? _hotKeyPos : 0; } - else if (Alignment is TextAlignment.Centered) + else if (Alignment is Alignment.Center) { if (isVertical) { @@ -395,11 +394,13 @@ public void Draw ( } else { - throw new ArgumentOutOfRangeException ($"{nameof (Alignment)}"); + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return; } // Vertical Alignment - if (VerticalAlignment is VerticalTextAlignment.Bottom) + if (VerticalAlignment is Alignment.End) { if (isVertical) { @@ -410,7 +411,7 @@ public void Draw ( y = screen.Bottom - linesFormatted.Count + line; } } - else if (VerticalAlignment is VerticalTextAlignment.Top) + else if (VerticalAlignment is Alignment.Start) { if (isVertical) { @@ -421,7 +422,7 @@ public void Draw ( y = screen.Top + line; } } - else if (VerticalAlignment is VerticalTextAlignment.Justified) + else if (VerticalAlignment is Alignment.Fill) { if (isVertical) { @@ -435,7 +436,7 @@ public void Draw ( line < linesFormatted.Count - 1 ? screen.Height - interval <= 1 ? screen.Top + 1 : screen.Top + line * interval : screen.Bottom - 1; } } - else if (VerticalAlignment is VerticalTextAlignment.Middle) + else if (VerticalAlignment is Alignment.Center) { if (isVertical) { @@ -450,7 +451,9 @@ public void Draw ( } else { - throw new ArgumentOutOfRangeException ($"{nameof (VerticalAlignment)}"); + Debug.WriteLine ($"Unsupported Alignment: {nameof (VerticalAlignment)}"); + + return; } int colOffset = screen.X < 0 ? Math.Abs (screen.X) : 0; @@ -471,8 +474,8 @@ public void Draw ( { if (idx < 0 || (isVertical - ? VerticalAlignment != VerticalTextAlignment.Bottom && current < 0 - : Alignment != TextAlignment.Right && x + current + colOffset < 0)) + ? VerticalAlignment != Alignment.End && current < 0 + : Alignment != Alignment.End && x + current + colOffset < 0)) { current++; @@ -561,7 +564,7 @@ public void Draw ( if (HotKeyPos > -1 && idx == HotKeyPos) { - if ((isVertical && VerticalAlignment == VerticalTextAlignment.Justified) || (!isVertical && Alignment == TextAlignment.Justified)) + if ((isVertical && VerticalAlignment == Alignment.Fill) || (!isVertical && Alignment == Alignment.Fill)) { CursorPosition = idx - start; } @@ -699,7 +702,7 @@ public List GetLines () _lines = Format ( text, Size.Height, - VerticalAlignment == VerticalTextAlignment.Justified, + VerticalAlignment == Alignment.Fill, Size.Width > colsWidth && WordWrap, PreserveTrailingSpaces, TabWidth, @@ -723,7 +726,7 @@ public List GetLines () _lines = Format ( text, Size.Width, - Alignment == TextAlignment.Justified, + Alignment == Alignment.Fill, Size.Height > 1 && WordWrap, PreserveTrailingSpaces, TabWidth, @@ -977,7 +980,7 @@ public static string ClipOrPad (string text, int width) // if value is not wide enough if (text.EnumerateRunes ().Sum (c => c.GetColumns ()) < width) { - // pad it out with spaces to the given alignment + // pad it out with spaces to the given Alignment int toPad = width - text.EnumerateRunes ().Sum (c => c.GetColumns ()); return text + new string (' ', toPad); @@ -999,7 +1002,7 @@ public static string ClipOrPad (string text, int width) /// instance to access any of his objects. /// A list of word wrapped lines. /// - /// This method does not do any justification. + /// This method does not do any alignment. /// This method strips Newline ('\n' and '\r\n') sequences before processing. /// /// If is at most one space will be preserved @@ -1031,7 +1034,7 @@ public static List WordWrapText ( List runes = StripCRLF (text).ToRuneList (); int start = Math.Max ( - !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom } && IsVerticalDirection (textDirection) + !runes.Contains ((Rune)' ') && textFormatter is { VerticalAlignment: Alignment.End } && IsVerticalDirection (textDirection) ? runes.Count - width : 0, 0); @@ -1249,7 +1252,7 @@ int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = /// The number of columns to clip the text to. Text longer than will be /// clipped. /// - /// Alignment. + /// Alignment. /// The text direction. /// The number of columns used for a tab. /// instance to access any of his objects. @@ -1257,13 +1260,13 @@ int GetNextWhiteSpace (int from, int cWidth, out bool incomplete, int cLength = public static string ClipAndJustify ( string text, int width, - TextAlignment talign, + Alignment textAlignment, TextDirection textDirection = TextDirection.LeftRight_TopBottom, int tabWidth = 0, TextFormatter textFormatter = null ) { - return ClipAndJustify (text, width, talign == TextAlignment.Justified, textDirection, tabWidth, textFormatter); + return ClipAndJustify (text, width, textAlignment == Alignment.Fill, textDirection, tabWidth, textFormatter); } /// Justifies text within a specified width. @@ -1304,12 +1307,12 @@ public static string ClipAndJustify ( { if (IsHorizontalDirection (textDirection)) { - if (textFormatter is { Alignment: TextAlignment.Right }) + if (textFormatter is { Alignment: Alignment.End }) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } - if (textFormatter is { Alignment: TextAlignment.Centered }) + if (textFormatter is { Alignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1319,12 +1322,12 @@ public static string ClipAndJustify ( if (IsVerticalDirection (textDirection)) { - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + if (textFormatter is { VerticalAlignment: Alignment.End }) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + if (textFormatter is { VerticalAlignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1342,14 +1345,14 @@ public static string ClipAndJustify ( if (IsHorizontalDirection (textDirection)) { - if (textFormatter is { Alignment: TextAlignment.Right }) + if (textFormatter is { Alignment: Alignment.End }) { if (GetRuneWidth (text, tabWidth, textDirection) > width) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } } - else if (textFormatter is { Alignment: TextAlignment.Centered }) + else if (textFormatter is { Alignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1361,14 +1364,14 @@ public static string ClipAndJustify ( if (IsVerticalDirection (textDirection)) { - if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Bottom }) + if (textFormatter is { VerticalAlignment: Alignment.End }) { if (runes.Count - zeroLength > width) { return GetRangeThatFits (runes, runes.Count - width, text, width, tabWidth, textDirection); } } - else if (textFormatter is { VerticalAlignment: VerticalTextAlignment.Middle }) + else if (textFormatter is { VerticalAlignment: Alignment.Center }) { return GetRangeThatFits (runes, Math.Max ((runes.Count - width) / 2, 0), text, width, tabWidth, textDirection); } @@ -1475,7 +1478,7 @@ public static string Justify ( /// Formats text into lines, applying text alignment and optionally wrapping text to new lines on word boundaries. /// /// The number of columns to constrain the text to for word wrapping and clipping. - /// Specifies how the text will be aligned horizontally. + /// Specifies how the text will be aligned horizontally. /// /// If , the text will be wrapped to new lines no longer than /// . If , forces text to fit a single line. Line breaks are converted @@ -1498,7 +1501,7 @@ public static string Justify ( public static List Format ( string text, int width, - TextAlignment talign, + Alignment textAlignment, bool wordWrap, bool preserveTrailingSpaces = false, int tabWidth = 0, @@ -1510,7 +1513,7 @@ public static List Format ( return Format ( text, width, - talign == TextAlignment.Justified, + textAlignment == Alignment.Fill, wordWrap, preserveTrailingSpaces, tabWidth, @@ -1884,7 +1887,7 @@ public static int GetMaxColsForWidth (List lines, int width, int tabWidt return lineIdx; } - /// Calculates the rectangle required to hold text, assuming no word wrapping or justification. + /// Calculates the rectangle required to hold text, assuming no word wrapping or alignment. /// /// This API will return incorrect results if the text includes glyphs who's width is dependent on surrounding /// glyphs (e.g. Arabic). diff --git a/Terminal.Gui/Text/VerticalTextAlignment.cs b/Terminal.Gui/Text/VerticalTextAlignment.cs deleted file mode 100644 index ef77885772..0000000000 --- a/Terminal.Gui/Text/VerticalTextAlignment.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace Terminal.Gui; - -/// Vertical text alignment enumeration, controls how text is displayed. -public enum VerticalTextAlignment -{ - /// The text will be top-aligned. - Top, - - /// The text will be bottom-aligned. - Bottom, - - /// The text will centered vertically. - Middle, - - /// - /// The text will be justified (spaces will be added to existing spaces such that the text fills the container - /// vertically). - /// - Justified -} \ No newline at end of file diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index 611b06d9ce..c810310d97 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -242,7 +242,7 @@ public override bool Contains (in Point location) return base.OnMouseEnter (mouseEvent); } - /// + /// protected internal override bool OnMouseLeave (MouseEvent mouseEvent) { // Invert Normal diff --git a/Terminal.Gui/View/Layout/AddOrSubtract.cs b/Terminal.Gui/View/Layout/AddOrSubtract.cs new file mode 100644 index 0000000000..e03cfbcfd2 --- /dev/null +++ b/Terminal.Gui/View/Layout/AddOrSubtract.cs @@ -0,0 +1,20 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Describes whether an operation should add or subtract values. +/// +[GenerateEnumExtensionMethods] +public enum AddOrSubtract +{ + /// + /// The operation should use addition. + /// + Add = 0, + + /// + /// The operation should use subtraction. + /// + Subtract = 1 +} diff --git a/Terminal.Gui/View/Layout/Dim.cs b/Terminal.Gui/View/Layout/Dim.cs new file mode 100644 index 0000000000..cc31d41b1a --- /dev/null +++ b/Terminal.Gui/View/Layout/Dim.cs @@ -0,0 +1,273 @@ +#nullable enable +using System.Diagnostics; + +namespace Terminal.Gui; + +/// +/// +/// A Dim object describes the dimensions of a . Dim is the type of the +/// and properties of . Dim objects enable +/// Computed Layout (see ) to automatically manage the dimensions of a view. +/// +/// +/// Integer values are implicitly convertible to an absolute . These objects are created using +/// the static methods described below. The objects can be combined with the addition and +/// subtraction operators. +/// +/// +/// +/// +/// +/// +/// Dim Object Description +/// +/// +/// +/// +/// +/// +/// Creates a object that automatically sizes the view to fit +/// the view's Text, SubViews, or ContentArea. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that computes the dimension by executing the provided +/// function. The function will be called every time the dimension is needed. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that is a percentage of the width or height of the +/// SuperView. +/// +/// +/// +/// +/// +/// +/// +/// 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. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Width of the specified +/// . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Height of the specified +/// . +/// +/// +/// +/// +/// +/// +public abstract class Dim +{ + #region static Dim creation methods + + /// Creates an Absolute from the specified integer value. + /// The Absolute . + /// The value to convert to the . + public static Dim? Absolute (int size) { return new DimAbsolute (size); } + + /// + /// Creates a object that automatically sizes the view to fit all the view's Content, Subviews, and/or Text. + /// + /// + /// + /// See . + /// + /// + /// + /// 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.Auto (), Height = Dim.Auto () }; + /// view.Add (button, textField); + /// + /// + /// The object. + /// + /// Specifies how will compute the dimension. The default is . + /// + /// The minimum dimension the View's ContentSize will be constrained to. + /// The maximum dimension the View's ContentSize will be fit to. NOT CURRENTLY SUPPORTED. + public static Dim? Auto (DimAutoStyle style = DimAutoStyle.Auto, Dim? minimumContentDim = null, Dim? maximumContentDim = null) + { + if (maximumContentDim is { }) + { + Debug.WriteLine (@"WARNING: maximumContentDim is not fully implemented."); + } + + return new DimAuto () + { + MinimumContentDim = minimumContentDim, + MaximumContentDim = maximumContentDim, + Style = style + }; + } + + /// + /// Creates a object that fills the dimension, leaving the specified margin. + /// + /// The Fill dimension. + /// Margin to use. + public static Dim? Fill (int margin = 0) { return new DimFill (margin); } + + /// + /// Creates a function object that computes the dimension by executing the provided function. + /// The function will be called every time the dimension is needed. + /// + /// The function to be executed. + /// The returned from the function. + public static Dim Func (Func function) { return new DimFunc (function); } + + /// 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, Dimension.Height); } + + /// 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. + /// the mode. Defaults to . + /// + /// 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 (int percent, DimPercentMode mode = DimPercentMode.ContentSize) + { + if (percent is < 0 /*or > 100*/) + { + throw new ArgumentException ("Percent value must be positive."); + } + + return new DimPercent (percent, mode); + } + + /// 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, Dimension.Width); } + + #endregion static Dim creation methods + + #region virtual methods + + /// + /// Gets a dimension that is anchored to a certain point in the layout. + /// This method is typically used internally by the layout system to determine the size of a View. + /// + /// The width of the area where the View is being sized (Superview.ContentSize). + /// + /// An integer representing the calculated dimension. The way this dimension is calculated depends on the specific + /// subclass of Dim that is used. For example, DimAbsolute returns a fixed dimension, DimFactor returns a + /// dimension that is a certain percentage of the super view's size, and so on. + /// + internal virtual int GetAnchor (int size) { return 0; } + + /// + /// Calculates and returns the dimension of a object. It takes into account the location of the + /// , it's SuperView's ContentSize, and whether it should automatically adjust its size based on its + /// content. + /// + /// + /// The starting point from where the size calculation begins. It could be the left edge for width calculation or the + /// top edge for height calculation. + /// + /// The size of the SuperView's content. It could be width or height. + /// The View that holds this Pos object. + /// Width or Height + /// + /// The calculated size of the View. The way this size is calculated depends on the specific subclass of Dim that + /// is used. + /// + internal virtual int Calculate (int location, int superviewContentSize, View us, Dimension dimension) + { + return Math.Max (GetAnchor (superviewContentSize - location), 0); + } + + /// + /// Diagnostics API to determine if this Dim object references other views. + /// + /// + internal virtual bool ReferencesOtherViews () { return false; } + + #endregion virtual methods + + #region operators + + /// 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.GetAnchor (0) + right.GetAnchor (0)); + } + + var newDim = new DimCombine (AddOrSubtract.Add, left, right); + (left as DimView)?.Target.SetNeedsLayout (); + + return newDim; + } + + /// 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); } + + /// + /// 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.GetAnchor (0) - right.GetAnchor (0)); + } + + var newDim = new DimCombine (AddOrSubtract.Subtract, left, right); + (left as DimView)?.Target.SetNeedsLayout (); + + return newDim; + } + + #endregion operators + +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimAbsolute.cs b/Terminal.Gui/View/Layout/DimAbsolute.cs new file mode 100644 index 0000000000..72d4e12f76 --- /dev/null +++ b/Terminal.Gui/View/Layout/DimAbsolute.cs @@ -0,0 +1,36 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that is a fixed size. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// +public class DimAbsolute (int size) : Dim +{ + /// + public override bool Equals (object? other) { return other is DimAbsolute abs && abs.Size == Size; } + + /// + public override int GetHashCode () { return Size.GetHashCode (); } + + /// + /// Gets the size of the dimension. + /// + public int Size { get; } = size; + + /// + public override string ToString () { return $"Absolute({Size})"; } + + internal override int GetAnchor (int size) { return Size; } + + internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) + { + return Math.Max (GetAnchor (0), 0); + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimAuto.cs b/Terminal.Gui/View/Layout/DimAuto.cs new file mode 100644 index 0000000000..e6d07f1a6e --- /dev/null +++ b/Terminal.Gui/View/Layout/DimAuto.cs @@ -0,0 +1,247 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that automatically sizes the view to fit all the view's Content, SubViews, and/or Text. +/// +/// +/// +/// See . +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +public class DimAuto () : Dim +{ + private readonly Dim? _maximumContentDim; + + /// + /// Gets the maximum dimension the View's ContentSize will be fit to. NOT CURRENTLY SUPPORTED. + /// + // ReSharper disable once ConvertToAutoProperty + public required Dim? MaximumContentDim + { + get => _maximumContentDim; + init => _maximumContentDim = value; + } + + private readonly Dim? _minimumContentDim; + + /// + /// Gets the minimum dimension the View's ContentSize will be constrained to. + /// + // ReSharper disable once ConvertToAutoProperty + public required Dim? MinimumContentDim + { + get => _minimumContentDim; + init => _minimumContentDim = value; + } + + private readonly DimAutoStyle _style; + + /// + /// Gets the style of the DimAuto. + /// + // ReSharper disable once ConvertToAutoProperty + public required DimAutoStyle Style + { + get => _style; + init => _style = value; + } + + /// + public override string ToString () { return $"Auto({Style},{MinimumContentDim},{MaximumContentDim})"; } + + internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) + { + var textSize = 0; + var subviewsSize = 0; + + int autoMin = MinimumContentDim?.GetAnchor (superviewContentSize) ?? 0; + int autoMax = MaximumContentDim?.GetAnchor (superviewContentSize) ?? int.MaxValue; + + if (Style.FastHasFlags (DimAutoStyle.Text)) + { + textSize = int.Max (autoMin, dimension == Dimension.Width ? us.TextFormatter.Size.Width : us.TextFormatter.Size.Height); + } + + if (Style.FastHasFlags (DimAutoStyle.Content)) + { + if (us._contentSize is { }) + { + subviewsSize = dimension == Dimension.Width ? us.ContentSize.Width : us.ContentSize.Height; + } + else + { + // TODO: This whole body of code is a WIP (for https://github.com/gui-cs/Terminal.Gui/pull/3451). + subviewsSize = 0; + + List subviews; + + #region Not Anchored and Are Not Dependent + // Start with subviews that are not anchored to the end, aligned, or dependent on content size + // [x] PosAnchorEnd + // [x] PosAlign + // [ ] PosCenter + // [ ] PosPercent + // [ ] PosView + // [ ] PosFunc + // [x] DimFill + // [ ] DimPercent + // [ ] DimFunc + // [ ] DimView + if (dimension == Dimension.Width) + { + subviews = us.Subviews.Where (v => v.X is not PosAnchorEnd + && v.X is not PosAlign + && v.X is not PosCenter + && v.Width is not DimFill).ToList (); + } + else + { + subviews = us.Subviews.Where (v => v.Y is not PosAnchorEnd + && v.Y is not PosAlign + && v.Y is not PosCenter + && v.Height is not DimFill).ToList (); + } + + for (var i = 0; i < subviews.Count; i++) + { + View v = subviews [i]; + + int size = dimension == Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; + + if (size > subviewsSize) + { + // BUGBUG: Should we break here? Or choose min/max? + subviewsSize = size; + } + } + #endregion Not Anchored and Are Not Dependent + + #region Anchored + // Now, handle subviews that are anchored to the end + // [x] PosAnchorEnd + if (dimension == Dimension.Width) + { + subviews = us.Subviews.Where (v => v.X is PosAnchorEnd).ToList (); + } + else + { + subviews = us.Subviews.Where (v => v.Y is PosAnchorEnd).ToList (); + } + + int maxAnchorEnd = 0; + for (var i = 0; i < subviews.Count; i++) + { + View v = subviews [i]; + maxAnchorEnd = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height; + } + + subviewsSize += maxAnchorEnd; + #endregion Anchored + + #region Center + // Now, handle subviews that are Centered + if (dimension == Dimension.Width) + { + subviews = us.Subviews.Where (v => v.X is PosCenter).ToList (); + } + else + { + subviews = us.Subviews.Where (v => v.Y is PosCenter).ToList (); + } + + int maxCenter = 0; + for (var i = 0; i < subviews.Count; i++) + { + View v = subviews [i]; + maxCenter = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height; + } + + subviewsSize += maxCenter; + #endregion Center + + #region Are Dependent + // Now, go back to those that are dependent on content size + // [x] DimFill + // [ ] DimPercent + if (dimension == Dimension.Width) + { + subviews = us.Subviews.Where (v => v.Width is DimFill).ToList (); + } + else + { + subviews = us.Subviews.Where (v => v.Height is DimFill).ToList (); + } + + int maxFill = 0; + for (var i = 0; i < subviews.Count; i++) + { + View v = subviews [i]; + + if (dimension == Dimension.Width) + { + v.SetRelativeLayout (new Size (autoMax - subviewsSize, 0)); + } + else + { + v.SetRelativeLayout (new Size (0, autoMax - subviewsSize)); + } + maxFill = dimension == Dimension.Width ? v.Frame.Width : v.Frame.Height; + } + + subviewsSize += maxFill; + #endregion Are Dependent + } + } + + // All sizes here are content-relative; ignoring adornments. + // We take the largest of text and content. + int max = int.Max (textSize, subviewsSize); + + // And, if min: is set, it wins if larger + max = int.Max (max, autoMin); + + // Factor in adornments + Thickness thickness = us.GetAdornmentsThickness (); + + max += dimension switch + { + Dimension.Width => thickness.Horizontal, + Dimension.Height => thickness.Vertical, + Dimension.None => 0, + _ => throw new ArgumentOutOfRangeException (nameof (dimension), dimension, null) + }; + + return int.Min (max, autoMax); + } + + internal override bool ReferencesOtherViews () + { + // BUGBUG: This is not correct. _contentSize may be null. + return false; //_style.HasFlag (DimAutoStyle.Content); + } + + /// + public override bool Equals (object? other) + { + if (other is not DimAuto auto) + { + return false; + } + + return auto.MinimumContentDim == MinimumContentDim && + auto.MaximumContentDim == MaximumContentDim && + auto.Style == Style; + } + + /// + public override int GetHashCode () + { + return HashCode.Combine (MinimumContentDim, MaximumContentDim, Style); + } + +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimAutoStyle.cs b/Terminal.Gui/View/Layout/DimAutoStyle.cs new file mode 100644 index 0000000000..4a3ecbb66c --- /dev/null +++ b/Terminal.Gui/View/Layout/DimAutoStyle.cs @@ -0,0 +1,46 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Specifies how will compute the dimension. +/// +[Flags] +[GenerateEnumExtensionMethods (FastHasFlags = true)] +public enum DimAutoStyle +{ + /// + /// The dimensions will be computed based on the View's non-Text content. + /// + /// If is explicitly set (is not ) then + /// + /// will be used to determine the dimension. + /// + /// + /// Otherwise, the Subview in with the largest corresponding position plus dimension + /// will determine the dimension. + /// + /// + /// The corresponding dimension of the view's will be ignored. + /// + /// + Content = 1, + + /// + /// + /// 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 = 2, + + /// + /// The dimension will be computed using both the view's and + /// (whichever is larger). + /// + Auto = Content | Text, +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimCombine.cs b/Terminal.Gui/View/Layout/DimCombine.cs new file mode 100644 index 0000000000..5baf00f639 --- /dev/null +++ b/Terminal.Gui/View/Layout/DimCombine.cs @@ -0,0 +1,83 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that is a combination of two other dimensions. +/// +/// +/// Indicates whether the two dimensions are added or subtracted. +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// The left dimension. +/// The right dimension. +public class DimCombine (AddOrSubtract add, Dim? left, Dim? right) : Dim +{ + /// + /// Gets whether the two dimensions are added or subtracted. + /// + public AddOrSubtract Add { get; } = add; + + /// + /// Gets the left dimension. + /// + public Dim? Left { get; } = left; + + /// + /// Gets the right dimension. + /// + public Dim? Right { get; } = right; + + /// + public override string ToString () { return $"Combine({Left}{(Add == AddOrSubtract.Add ? '+' : '-')}{Right})"; } + + internal override int GetAnchor (int size) + { + if (Add == AddOrSubtract.Add) + { + return Left!.GetAnchor (size) + Right!.GetAnchor (size); + } + + return Left!.GetAnchor (size) - Right!.GetAnchor (size); + } + + internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) + { + int newDimension; + + if (Add == AddOrSubtract.Add) + { + newDimension = Left!.Calculate (location, superviewContentSize, us, dimension) + Right!.Calculate (location, superviewContentSize, us, dimension); + } + else + { + newDimension = Math.Max ( + 0, + Left!.Calculate (location, superviewContentSize, us, dimension) + - Right!.Calculate (location, superviewContentSize, us, dimension)); + } + + return newDimension; + } + + /// + /// Diagnostics API to determine if this Dim object references other views. + /// + /// + internal override bool ReferencesOtherViews () + { + if (Left!.ReferencesOtherViews ()) + { + return true; + } + + if (Right!.ReferencesOtherViews ()) + { + return true; + } + + return false; + } +} diff --git a/Terminal.Gui/View/Layout/DimFill.cs b/Terminal.Gui/View/Layout/DimFill.cs new file mode 100644 index 0000000000..03cf6f3d2a --- /dev/null +++ b/Terminal.Gui/View/Layout/DimFill.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that fills the dimension, leaving the specified margin. +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// The margin to not fill. +public class DimFill (int margin) : Dim +{ + /// + public override bool Equals (object? other) { return other is DimFill fill && fill.Margin == Margin; } + + /// + public override int GetHashCode () { return Margin.GetHashCode (); } + + /// + /// Gets the margin to not fill. + /// + public int Margin { get; } = margin; + + /// + public override string ToString () { return $"Fill({Margin})"; } + + internal override int GetAnchor (int size) { return size - Margin; } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimFunc.cs b/Terminal.Gui/View/Layout/DimFunc.cs new file mode 100644 index 0000000000..c15e9fc8c8 --- /dev/null +++ b/Terminal.Gui/View/Layout/DimFunc.cs @@ -0,0 +1,29 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a function object that computes the dimension by executing the provided function. +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +public class DimFunc (Func dim) : Dim +{ + /// + public override bool Equals (object? other) { return other is DimFunc f && f.Func () == Func (); } + + /// + /// Gets the function that computes the dimension. + /// + public new Func Func { get; } = dim; + + /// + public override int GetHashCode () { return Func.GetHashCode (); } + + /// + public override string ToString () { return $"DimFunc({Func ()})"; } + + internal override int GetAnchor (int size) { return Func (); } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimPercent.cs b/Terminal.Gui/View/Layout/DimPercent.cs new file mode 100644 index 0000000000..af849c53fe --- /dev/null +++ b/Terminal.Gui/View/Layout/DimPercent.cs @@ -0,0 +1,45 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that is a percentage of the width or height of the SuperView. +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// The percentage. +/// +/// If the dimension is computed using the View's position ( or +/// ); otherwise, the dimension is computed using the View's . +/// +public class DimPercent (int percent, DimPercentMode mode = DimPercentMode.ContentSize) : Dim +{ + /// + public override bool Equals (object? other) { return other is DimPercent f && f.Percent == Percent && f.Mode == Mode; } + + /// + public override int GetHashCode () { return Percent.GetHashCode (); } + + /// + /// Gets the percentage. + /// + public new int Percent { get; } = percent; + + /// + /// + /// + public override string ToString () { return $"Percent({Percent},{Mode})"; } + + /// + /// Gets whether the dimension is computed using the View's position or ContentSize. + /// + public DimPercentMode Mode { get; } = mode; + + internal override int GetAnchor (int size) { return (int)(size * (Percent / 100f)); } + + internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) + { + return Mode == DimPercentMode.Position ? Math.Max (GetAnchor (superviewContentSize - location), 0) : GetAnchor (superviewContentSize); + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimPercentMode.cs b/Terminal.Gui/View/Layout/DimPercentMode.cs new file mode 100644 index 0000000000..74d64b77ce --- /dev/null +++ b/Terminal.Gui/View/Layout/DimPercentMode.cs @@ -0,0 +1,21 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Indicates the mode for a object. +/// +[GenerateEnumExtensionMethods] + +public enum DimPercentMode +{ + /// + /// The dimension is computed using the View's position ( or ). + /// + Position = 0, + + /// + /// The dimension is computed using the View's . + /// + ContentSize = 1 +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/DimView.cs b/Terminal.Gui/View/Layout/DimView.cs new file mode 100644 index 0000000000..22c0d1f709 --- /dev/null +++ b/Terminal.Gui/View/Layout/DimView.cs @@ -0,0 +1,62 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a dimension that tracks the Height or Width of the specified View. +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +public class DimView : Dim +{ + /// + /// Initializes a new instance of the class. + /// + /// The view the dimension is anchored to. + /// Indicates which dimension is tracked. + public DimView (View view, Dimension dimension) + { + Target = view; + Dimension = dimension; + } + + /// + /// Gets the indicated dimension of the View. + /// + public Dimension Dimension { get; } + + /// + public override bool Equals (object? other) { return other is DimView abs && abs.Target == Target && abs.Dimension == Dimension; } + + /// + public override int GetHashCode () { return Target.GetHashCode (); } + + /// + /// Gets the View the dimension is anchored to. + /// + public View Target { get; init; } + + /// + public override string ToString () + { + if (Target == null) + { + throw new NullReferenceException (); + } + + return $"View({Dimension},{Target})"; + } + + internal override int GetAnchor (int size) + { + return Dimension switch + { + Dimension.Height => Target.Frame.Height, + Dimension.Width => Target.Frame.Width, + _ => 0 + }; + } + + internal override bool ReferencesOtherViews () { return true; } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/Dimension.cs b/Terminal.Gui/View/Layout/Dimension.cs new file mode 100644 index 0000000000..cc56ffd4b6 --- /dev/null +++ b/Terminal.Gui/View/Layout/Dimension.cs @@ -0,0 +1,26 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Indicates the dimension for operations. +/// + +[GenerateEnumExtensionMethods] +public enum Dimension +{ + /// + /// No dimension specified. + /// + None = 0, + + /// + /// The height dimension. + /// + Height = 1, + + /// + /// The width dimension. + /// + Width = 2 +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/LayoutStyle.cs b/Terminal.Gui/View/Layout/LayoutStyle.cs new file mode 100644 index 0000000000..81883bfccf --- /dev/null +++ b/Terminal.Gui/View/Layout/LayoutStyle.cs @@ -0,0 +1,38 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Indicates the LayoutStyle for the . +/// +/// If Absolute, the , , , and +/// objects are all absolute values and are not relative. The position and size of the +/// view is described by . +/// +/// +/// If Computed, one or more of the , , , or +/// objects are relative to the and are computed at layout +/// time. +/// +/// +[GenerateEnumExtensionMethods] +public enum LayoutStyle +{ + /// + /// Indicates the , , , and + /// objects are all absolute values and are not relative. The position and size of the view + /// is described by . + /// + Absolute, + + /// + /// 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. + /// + Computed +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/Pos.cs b/Terminal.Gui/View/Layout/Pos.cs new file mode 100644 index 0000000000..609c1ffd7d --- /dev/null +++ b/Terminal.Gui/View/Layout/Pos.cs @@ -0,0 +1,394 @@ +#nullable enable +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. +/// +/// +/// 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. +/// +/// +/// +/// +/// Pos Object Description +/// +/// +/// +/// +/// +/// +/// Creates a object that aligns a set of views. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that computes the position by executing the provided +/// function. The function will be called every time the position is needed. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that is a percentage of the width or height of the +/// SuperView. +/// +/// +/// +/// +/// +/// +/// +/// 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. +/// +/// +/// +/// +/// +/// +/// Creates a object that can be used to center the . +/// +/// +/// +/// +/// +/// +/// Creates a object that is an absolute position based on the specified +/// integer value. +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Left (X) position of the specified +/// . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Left (X) position of the specified +/// . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Top (Y) position of the specified +/// . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Top (Y) position of the specified +/// . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Right (X+Width) coordinate of the +/// specified . +/// +/// +/// +/// +/// +/// +/// +/// Creates a object that tracks the Bottom (Y+Height) coordinate of the +/// specified +/// +/// +/// +/// +/// +public abstract class Pos +{ + #region static Pos creation methods + + /// Creates a object that is an absolute position based on the specified integer value. + /// The Absolute . + /// The value to convert to the . + public static Pos Absolute (int position) { return new PosAbsolute (position); } + + /// + /// Creates a object that aligns a set of views according to the specified + /// and . + /// + /// The alignment. The default includes . + /// The optional alignment modes. + /// + /// The optional identifier of a set of views that should be aligned together. When only a single + /// set of views in a SuperView is aligned, this parameter is optional. + /// + /// + public static Pos Align (Alignment alignment, AlignmentModes modes = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems, int groupId = 0) + { + return new PosAlign + { + Aligner = new () + { + Alignment = alignment, + AlignmentModes = modes + }, + GroupId = groupId + }; + } + + /// + /// Creates a object that is anchored to the end (right side or + /// bottom) of the SuperView's Content Area, minus the respective size of the View. This is equivalent to using + /// , + /// with an offset equivalent to the View's respective dimension. + /// + /// The object anchored to the end (the bottom or the right side) minus the View's dimension. + /// + /// This sample shows how align a to the bottom-right the SuperView. + /// + /// anchorButton.X = Pos.AnchorEnd (); + /// anchorButton.Y = Pos.AnchorEnd (); + /// + /// + public static Pos AnchorEnd () { return new PosAnchorEnd (); } + + /// + /// Creates a object that is anchored to the end (right side or bottom) of the SuperView's Content + /// Area, + /// useful to flush the layout from the right or bottom. See also , which uses the view + /// dimension to ensure the view is fully visible. + /// + /// The object anchored to the end (the bottom or the right side). + /// The view will be shifted left or up by the amount specified. + /// + /// This sample shows how align a 10 column wide to the bottom-right the SuperView. + /// + /// anchorButton.X = Pos.AnchorEnd (10); + /// anchorButton.Y = 1 + /// + /// + public static Pos AnchorEnd (int offset) + { + if (offset < 0) + { + throw new ArgumentException (@"Must be positive", nameof (offset)); + } + + return new PosAnchorEnd (offset); + } + + /// Creates a object that can be used to center the . + /// The center Pos. + /// + /// 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), + /// }; + /// + /// + public static Pos Center () { return new PosCenter (); } + + /// + /// Creates a object that computes the position by executing the provided function. The function + /// will be called every time the position is needed. + /// + /// The function to be executed. + /// The returned from the function. + public static Pos Func (Func function) { return new PosFunc (function); } + + /// Creates a percentage object + /// The percent object. + /// A value between 0 and 100 representing the percentage. + /// + /// 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 TextField { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Pos Percent (int percent) + { + if (percent is < 0) + { + throw new ArgumentException ("Percent value must be positive."); + } + + return new PosPercent (percent); + } + + /// 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, Side.Top); } + + /// 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, Side.Top); } + + /// 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, Side.Left); } + + /// 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, Side.Left); } + + /// + /// Creates a object that 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) { return new PosView (view, Side.Bottom); } + + /// + /// Creates a object that 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) { return new PosView (view, Side.Right); } + + #endregion static Pos creation methods + + #region virtual methods + + /// + /// Gets the starting point of an element based on the size of the parent element (typically + /// Superview.ContentSize). + /// This method is meant to be overridden by subclasses to provide different ways of calculating the starting point. + /// This method is used + /// internally by the layout system to determine where a View should be positioned. + /// + /// The size of the parent element (typically Superview.ContentSize). + /// + /// An integer representing the calculated position. The way this position is calculated depends on the specific + /// subclass of Pos that is used. For example, PosAbsolute returns a fixed position, PosAnchorEnd returns a + /// position that is anchored to the end of the layout, and so on. + /// + internal virtual int GetAnchor (int size) { return 0; } + + /// + /// Calculates and returns the final position of a object. It takes into account the dimension of + /// the + /// superview and the dimension of the view itself. + /// + /// + /// + /// + /// The dimension of the superview. This could be the width for x-coordinate calculation or the + /// height for y-coordinate calculation. + /// + /// The dimension of the View. It could be the current width or height. + /// The View that holds this Pos object. + /// Width or Height + /// + /// The calculated position of the View. The way this position is calculated depends on the specific subclass of Pos + /// that + /// is used. + /// + internal virtual int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) { return GetAnchor (superviewDimension); } + + /// + /// Diagnostics API to determine if this Pos object references other views. + /// + /// + internal virtual bool ReferencesOtherViews () { return false; } + + #endregion virtual methods + + #region operators + + /// 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.GetAnchor (0) + right.GetAnchor (0)); + } + + var newPos = new PosCombine (AddOrSubtract.Add, left, right); + + if (left is PosView view) + { + view.Target.SetNeedsLayout (); + } + + return newPos; + } + + /// 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); } + + /// + /// 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.GetAnchor (0) - right.GetAnchor (0)); + } + + var newPos = new PosCombine (AddOrSubtract.Subtract, left, right); + + if (left is PosView view) + { + view.Target.SetNeedsLayout (); + } + + return newPos; + } + + #endregion operators +} diff --git a/Terminal.Gui/View/Layout/PosAbsolute.cs b/Terminal.Gui/View/Layout/PosAbsolute.cs new file mode 100644 index 0000000000..44afdac970 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosAbsolute.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents an absolute position in the layout. This is used to specify a fixed position in the layout. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// +public class PosAbsolute (int position) : Pos +{ + /// + /// The position of the in the layout. + /// + public int Position { get; } = position; + + /// + public override bool Equals (object? other) { return other is PosAbsolute abs && abs.Position == Position; } + + /// + public override int GetHashCode () { return Position.GetHashCode (); } + + /// + public override string ToString () { return $"Absolute({Position})"; } + + internal override int GetAnchor (int size) { return Position; } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosAlign.cs b/Terminal.Gui/View/Layout/PosAlign.cs new file mode 100644 index 0000000000..5d6901f540 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosAlign.cs @@ -0,0 +1,169 @@ +#nullable enable + +using System.ComponentModel; + +namespace Terminal.Gui; + +/// +/// Enables alignment of a set of views. +/// +/// +/// +/// Updating the properties of is supported, but will not automatically cause re-layout to +/// happen. +/// must be called on the SuperView. +/// +/// +/// Views that should be aligned together must have a distinct . When only a single +/// set of views is aligned within a SuperView, setting is optional because it defaults to 0. +/// +/// +/// The first view added to the Superview with a given is used to determine the alignment of +/// the group. +/// The alignment is applied to all views with the same . +/// +/// +public class PosAlign : Pos +{ + /// + /// The cached location. Used to store the calculated location to minimize recalculating it. + /// + private int? _cachedLocation; + + /// + /// Gets the identifier of a set of views that should be aligned together. When only a single + /// set of views in a SuperView is aligned, setting is not needed because it defaults to 0. + /// + public int GroupId { get; init; } + + private readonly Aligner? _aligner; + + /// + /// Gets the alignment settings. + /// + public required Aligner Aligner + { + get => _aligner!; + init + { + if (_aligner is { }) + { + _aligner.PropertyChanged -= Aligner_PropertyChanged; + } + + _aligner = value; + _aligner.PropertyChanged += Aligner_PropertyChanged; + } + } + + /// + /// Aligns the views in that have the same group ID as . + /// Updates each view's cached _location. + /// + /// + /// + /// + /// + private static void AlignAndUpdateGroup (int groupId, IList views, Dimension dimension, int size) + { + List dimensionsList = new (); + + // PERF: If this proves a perf issue, consider caching a ref to this list in each item + List viewsInGroup = views.Where ( + v => + { + return dimension switch + { + Dimension.Width when v.X is PosAlign alignX => alignX.GroupId == groupId, + Dimension.Height when v.Y is PosAlign alignY => alignY.GroupId == groupId, + _ => false + }; + }) + .ToList (); + + if (viewsInGroup.Count == 0) + { + return; + } + + // PERF: We iterate over viewsInGroup multiple times here. + + Aligner? firstInGroup = null; + + // Update the dimensionList with the sizes of the views + for (var index = 0; index < viewsInGroup.Count; index++) + { + View view = viewsInGroup [index]; + PosAlign? posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign; + + if (posAlign is { }) + { + if (index == 0) + { + firstInGroup = posAlign.Aligner; + } + + dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height); + } + } + + // Update the first item in the group with the new container size. + firstInGroup!.ContainerSize = size; + + // Align + int [] locations = firstInGroup.Align (dimensionsList.ToArray ()); + + // Update the cached location for each item + for (var index = 0; index < viewsInGroup.Count; index++) + { + View view = viewsInGroup [index]; + PosAlign? align = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign; + + if (align is { }) + { + align._cachedLocation = locations [index]; + } + } + } + + private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _cachedLocation = null; } + + /// + public override bool Equals (object? other) + { + return other is PosAlign align + && GroupId == align.GroupId + && align.Aligner.Alignment == Aligner.Alignment + && align.Aligner.AlignmentModes == Aligner.AlignmentModes; + } + + /// + public override int GetHashCode () { return HashCode.Combine (Aligner, GroupId); } + + /// + public override string ToString () { return $"Align(alignment={Aligner.Alignment},modes={Aligner.AlignmentModes},groupId={GroupId})"; } + + internal override int GetAnchor (int width) { return _cachedLocation ?? 0 - width; } + + internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) + { + if (_cachedLocation.HasValue && Aligner.ContainerSize == superviewDimension) + { + return _cachedLocation.Value; + } + + if (us?.SuperView is null) + { + return 0; + } + + AlignAndUpdateGroup (GroupId, us.SuperView.Subviews, dimension, superviewDimension); + + if (_cachedLocation.HasValue) + { + return _cachedLocation.Value; + } + + return 0; + } +} diff --git a/Terminal.Gui/View/Layout/PosAnchorEnd.cs b/Terminal.Gui/View/Layout/PosAnchorEnd.cs new file mode 100644 index 0000000000..e4641c2b55 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosAnchorEnd.cs @@ -0,0 +1,68 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position anchored to the end (right side or bottom). +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +public class PosAnchorEnd : Pos +{ + /// + /// Gets the offset of the position from the right/bottom. + /// + public int Offset { get; } + + /// + /// Constructs a new position anchored to the end (right side or bottom) of the SuperView, + /// minus the respective dimension of the View. This is equivalent to using , + /// with an offset equivalent to the View's respective dimension. + /// + public PosAnchorEnd () { UseDimForOffset = true; } + + /// + /// Constructs a new position anchored to the end (right side or bottom) of the SuperView, + /// + /// + public PosAnchorEnd (int offset) { Offset = offset; } + + /// + public override bool Equals (object? other) { return other is PosAnchorEnd anchorEnd && anchorEnd.Offset == Offset; } + + /// + public override int GetHashCode () { return Offset.GetHashCode (); } + + /// + /// If true, the offset is the width of the view, if false, the offset is the offset value. + /// + public bool UseDimForOffset { get; } + + /// + public override string ToString () { return UseDimForOffset ? "AnchorEnd()" : $"AnchorEnd({Offset})"; } + + internal override int GetAnchor (int size) + { + if (UseDimForOffset) + { + return size; + } + + return size - Offset; + } + + internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) + { + int newLocation = GetAnchor (superviewDimension); + + if (UseDimForOffset) + { + newLocation -= dim.GetAnchor (superviewDimension); + } + + return newLocation; + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosCenter.cs b/Terminal.Gui/View/Layout/PosCenter.cs new file mode 100644 index 0000000000..04c7958bbc --- /dev/null +++ b/Terminal.Gui/View/Layout/PosCenter.cs @@ -0,0 +1,20 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position that is centered. +/// +public class PosCenter : Pos +{ + /// + public override string ToString () { return "Center"; } + + internal override int GetAnchor (int size) { return size / 2; } + + internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) + { + int newDimension = Math.Max (dim.Calculate (0, superviewDimension, us, dimension), 0); + + return GetAnchor (superviewDimension - newDimension); + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosCombine.cs b/Terminal.Gui/View/Layout/PosCombine.cs new file mode 100644 index 0000000000..63ff1814f9 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosCombine.cs @@ -0,0 +1,72 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position that is a combination of two other positions. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// +/// Indicates whether the two positions are added or subtracted. +/// +/// The left position. +/// The right position. +public class PosCombine (AddOrSubtract add, Pos left, Pos right) : Pos +{ + /// + /// Gets whether the two positions are added or subtracted. + /// + public AddOrSubtract Add { get; } = add; + + /// + /// Gets the left position. + /// + public new Pos Left { get; } = left; + + /// + /// Gets the right position. + /// + public new Pos Right { get; } = right; + + /// + public override string ToString () { return $"Combine({Left}{(Add == AddOrSubtract.Add ? '+' : '-')}{Right})"; } + + internal override int GetAnchor (int size) + { + if (Add == AddOrSubtract.Add) + { + return Left.GetAnchor (size) + Right.GetAnchor (size); + } + + return Left.GetAnchor (size) - Right.GetAnchor (size); + } + + internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension) + { + if (Add == AddOrSubtract.Add) + { + return Left.Calculate (superviewDimension, dim, us, dimension) + Right.Calculate (superviewDimension, dim, us, dimension); + } + + return Left.Calculate (superviewDimension, dim, us, dimension) - Right.Calculate (superviewDimension, dim, us, dimension); + } + + internal override bool ReferencesOtherViews () + { + if (Left.ReferencesOtherViews ()) + { + return true; + } + + if (Right.ReferencesOtherViews ()) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosDim.cs b/Terminal.Gui/View/Layout/PosDim.cs deleted file mode 100644 index 2bd0cb0add..0000000000 --- a/Terminal.Gui/View/Layout/PosDim.cs +++ /dev/null @@ -1,1178 +0,0 @@ -using System.Diagnostics; - -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. -/// -/// -/// 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. -/// -/// -/// -/// -/// Pos Object Description -/// -/// -/// -/// -/// -/// -/// Creates a object that computes the position by executing the provided -/// function. The function will be called every time the position is needed. -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that is a percentage of the width or height of the -/// SuperView. -/// -/// -/// -/// -/// -/// -/// -/// 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. -/// -/// -/// -/// -/// -/// -/// Creates a object that can be used to center the . -/// -/// -/// -/// -/// -/// -/// Creates a object that is an absolute position based on the specified -/// integer value. -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Left (X) position of the specified -/// . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Left (X) position of the specified -/// . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Top (Y) position of the specified -/// . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Top (Y) position of the specified -/// . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Right (X+Width) coordinate of the -/// specified . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Bottom (Y+Height) coordinate of the -/// specified -/// -/// -/// -/// -/// -public class Pos -{ - /// - /// Creates a object that is anchored to the end (right side or - /// bottom) of the SuperView, minus the respective dimension of the View. This is equivalent to using - /// , - /// with an offset equivalent to the View's respective dimension. - /// - /// The object anchored to the end (the bottom or the right side) minus the View's dimension. - /// - /// This sample shows how align a to the bottom-right the SuperView. - /// - /// anchorButton.X = Pos.AnchorEnd (); - /// anchorButton.Y = Pos.AnchorEnd (); - /// - /// - public static Pos AnchorEnd () { return new PosAnchorEnd (); } - - /// - /// Creates a object that is anchored to the end (right side or bottom) of the SuperView, - /// useful to flush the layout from the right or bottom. See also , which uses the view - /// dimension to ensure the view is fully visible. - /// - /// The object anchored to the end (the bottom or the right side). - /// The view will be shifted left or up by the amount specified. - /// - /// This sample shows how align a 10 column wide to the bottom-right the SuperView. - /// - /// anchorButton.X = Pos.AnchorEnd (10); - /// anchorButton.Y = 1 - /// - /// - public static Pos AnchorEnd (int offset) - { - if (offset < 0) - { - throw new ArgumentException (@"Must be positive", nameof (offset)); - } - - return new PosAnchorEnd (offset); - } - - /// Creates a object that is an absolute position based on the specified integer value. - /// The Absolute . - /// The value to convert to the . - public static Pos At (int n) { return new PosAbsolute (n); } - - /// Creates a object that can be used to center the . - /// The center Pos. - /// - /// 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), - /// }; - /// - /// - public static Pos Center () { return new PosCenter (); } - - /// 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) { return other is Pos abs && abs == this; } - - /// - /// Creates a object that computes the position by executing the provided function. The function - /// will be called every time the position is needed. - /// - /// The function to be executed. - /// The returned from the function. - public static Pos Function (Func function) { return new PosFunc (function); } - - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () { return Anchor (0).GetHashCode (); } - - /// 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); - - if (left is PosView view) - { - view.Target.SetNeedsLayout (); - } - - return newPos; - } - - /// 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); } - - /// - /// 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); - - if (left is PosView view) - { - view.Target.SetNeedsLayout (); - } - - return newPos; - } - - /// Creates a percentage object - /// The percent object. - /// A value between 0 and 100 representing the percentage. - /// - /// 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 TextField { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), - /// }; - /// - /// - public static Pos Percent (float percent) - { - if (percent is < 0 or > 100) - { - throw new ArgumentException ("Percent value must be between 0 and 100."); - } - - return new PosFactor (percent / 100); - } - - /// 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, Side.Top); } - - /// 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, Side.Top); } - - /// 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, Side.Left); } - - /// 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, Side.Left); } - - /// - /// Creates a object that 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) { return new PosView (view, Side.Bottom); } - - /// - /// Creates a object that 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) { return new PosView (view, Side.Right); } - - /// - /// Gets a position that is anchored to a certain point in the layout. This method is typically used - /// internally by the layout system to determine where a View should be positioned. - /// - /// The width of the area where the View is being positioned (Superview.ContentSize). - /// - /// An integer representing the calculated position. The way this position is calculated depends on the specific - /// subclass of Pos that is used. For example, PosAbsolute returns a fixed position, PosAnchorEnd returns a - /// position that is anchored to the end of the layout, and so on. - /// - internal virtual int Anchor (int width) { return 0; } - - /// - /// Calculates and returns the position of a object. It takes into account the dimension of the - /// superview and the dimension of the view itself. - /// - /// - /// The dimension of the superview. This could be the width for x-coordinate calculation or the - /// height for y-coordinate calculation. - /// - /// The dimension of the View. It could be the current width or height. - /// The View that holds this Pos object. - /// Width or Height - /// - /// The calculated position of the View. The way this position is calculated depends on the specific subclass of Pos - /// that - /// is used. - /// - internal virtual int Calculate (int superviewDimension, Dim dim, View us, Dim.Dimension dimension) - { - return Anchor (superviewDimension); - } - - - /// - /// Diagnostics API to determine if this Pos object references other views. - /// - /// - internal virtual bool ReferencesOtherViews () - { - return false; - } - - internal class PosAbsolute (int n) : Pos - { - 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 - { - private readonly int _offset; - public PosAnchorEnd () { UseDimForOffset = true; } - public PosAnchorEnd (int offset) { _offset = offset; } - public override bool Equals (object other) { return other is PosAnchorEnd anchorEnd && anchorEnd._offset == _offset; } - public override int GetHashCode () { return _offset.GetHashCode (); } - - /// - /// If true, the offset is the width of the view, if false, the offset is the offset value. - /// - internal bool UseDimForOffset { get; set; } - - public override string ToString () { return UseDimForOffset ? "AnchorEnd()" : $"AnchorEnd({_offset})"; } - - internal override int Anchor (int width) - { - if (UseDimForOffset) - { - return width; - } - - return width - _offset; - } - - internal override int Calculate (int superviewDimension, Dim dim, View us, Dim.Dimension dimension) - { - int newLocation = Anchor (superviewDimension); - - if (UseDimForOffset) - { - newLocation -= dim.Anchor (superviewDimension); - } - - return newLocation; - } - } - - internal class PosCenter : Pos - { - public override string ToString () { return "Center"; } - internal override int Anchor (int width) { return width / 2; } - - internal override int Calculate (int superviewDimension, Dim dim, View us, Dim.Dimension dimension) - { - int newDimension = Math.Max (dim.Calculate (0, superviewDimension, us, dimension), 0); - - return Anchor (superviewDimension - newDimension); - } - } - - internal class PosCombine (bool add, Pos left, Pos right) : Pos - { - internal bool _add = add; - internal Pos _left = left, _right = right; - - public override string ToString () { return $"Combine({_left}{(_add ? '+' : '-')}{_right})"; } - - internal override int Anchor (int width) - { - int la = _left.Anchor (width); - int ra = _right.Anchor (width); - - if (_add) - { - return la + ra; - } - - return la - ra; - } - - internal override int Calculate (int superviewDimension, Dim dim, View us, Dim.Dimension dimension) - { - int newDimension = dim.Calculate (0, superviewDimension, us, dimension); - int left = _left.Calculate (superviewDimension, dim, us, dimension); - int right = _right.Calculate (superviewDimension, dim, us, dimension); - - if (_add) - { - return left + right; - } - - return left - right; - } - - /// - /// Diagnostics API to determine if this Pos object references other views. - /// - /// - internal override bool ReferencesOtherViews () - { - if (_left.ReferencesOtherViews ()) - { - return true; - } - - if (_right.ReferencesOtherViews ()) - { - return true; - } - - return false; - } - } - - internal class PosFactor (float factor) : Pos - { - 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})"; } - internal override int Anchor (int width) { return (int)(width * _factor); } - } - - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class PosFunc (Func n) : Pos - { - 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 (); } - } - - /// - /// Describes which side of the view to use for the position. - /// - public enum Side - { - /// - /// The left (X) side of the view. - /// - Left = 0, - - /// - /// The top (Y) side of the view. - /// - Top = 1, - - /// - /// The right (X + Width) side of the view. - /// - Right = 2, - - /// - /// The bottom (Y + Height) side of the view. - /// - Bottom = 3 - } - - 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 sideString = side switch - { - Side.Left => "left", - Side.Top => "top", - Side.Right => "right", - Side.Bottom => "bottom", - _ => "unknown" - }; - - if (Target == null) - { - throw new NullReferenceException (nameof (Target)); - } - - return $"View(side={sideString},target={Target})"; - } - - internal override int Anchor (int width) - { - return side switch - { - Side.Left => Target.Frame.X, - Side.Top => Target.Frame.Y, - Side.Right => Target.Frame.Right, - Side.Bottom => Target.Frame.Bottom, - _ => 0 - }; - } - - /// - /// Diagnostics API to determine if this Pos object references other views. - /// - /// - internal override bool ReferencesOtherViews () - { - return true; - } - } -} - -/// -/// -/// A Dim object describes the dimensions of a . Dim is the type of the -/// and properties of . Dim objects enable -/// Computed Layout (see ) to automatically manage the dimensions of a view. -/// -/// -/// Integer values are implicitly convertible to an absolute . These objects are created using -/// the static methods described below. The objects can be combined with the addition and -/// subtraction operators. -/// -/// -/// -/// -/// -/// -/// Dim Object Description -/// -/// -/// -/// -/// -/// -/// Creates a object that automatically sizes the view to fit -/// the view's Text, SubViews, or ContentArea. -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that computes the dimension by executing the provided -/// function. The function will be called every time the dimension is needed. -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that is a percentage of the width or height of the -/// SuperView. -/// -/// -/// -/// -/// -/// -/// -/// 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. -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Width of the specified -/// . -/// -/// -/// -/// -/// -/// -/// -/// Creates a object that tracks the Height of the specified -/// . -/// -/// -/// -/// -/// -/// -public class Dim -{ - /// - /// Specifies how will compute the dimension. - /// - [Flags] - public enum DimAutoStyle - { - /// - /// The dimension will be computed using both the view's and - /// (whichever is larger). - /// - Auto = Content | Text, - - /// - /// The dimensions will be computed based on the View's non-Text content. - /// - /// If is explicitly set (is not ) then - /// will be used to determine the dimension. - /// - /// - /// Otherwise, the Subview in with the largest corresponding position plus dimension - /// will determine the dimension. - /// - /// - /// The corresponding dimension of the view's will be ignored. - /// - /// - Content = 1, - - /// - /// - /// 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 = 2 - } - - - /// - /// - /// - public enum Dimension - { - /// - /// No dimension specified. - /// - None = 0, - - /// - /// The height dimension. - /// - Height = 1, - - /// - /// The width dimension. - /// - Width = 2 - } - - - /// - /// Creates a object that automatically sizes the view to fit all the view's SubViews and/or Text. - /// - /// - /// - /// See . - /// - /// - /// - /// 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.Auto (), Height = Dim.Auto () }; - /// view.Add (button, textField); - /// - /// - /// The 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. - /// - /// if the specified object is equal to the current object; otherwise, - /// . - /// - public override bool Equals (object other) { return other is Dim abs && abs == this; } - - /// - /// Creates a object that fills the dimension, leaving the specified number of columns for a - /// margin. - /// - /// The Fill dimension. - /// Margin to use. - public static Dim Fill (int margin = 0) { return new DimFill (margin); } - - /// - /// Creates a function object that computes the dimension by executing the provided function. - /// The function will be called every time the dimension is needed. - /// - /// The function to be executed. - /// The returned from the function. - public static Dim Function (Func function) { return new DimFunc (function); } - - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () { return Anchor (0).GetHashCode (); } - - /// 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, Dimension.Height); } - - /// 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); - (left as DimView)?.Target.SetNeedsLayout (); - - return newDim; - } - - /// 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); } - - /// - /// 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); - (left as DimView)?.Target.SetNeedsLayout (); - - return newDim; - } - - /// 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 the dimension is computed using the View's position ( or - /// ). - /// If the dimension is computed using the View's . - /// - /// - /// 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 percent, bool usePosition = false) - { - if (percent is < 0 or > 100) - { - throw new ArgumentException ("Percent value must be between 0 and 100"); - } - - return new DimFactor (percent / 100, usePosition); - } - - /// 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); } - - /// 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, Dimension.Width); } - - /// - /// Gets a dimension that is anchored to a certain point in the layout. - /// This method is typically used internally by the layout system to determine the size of a View. - /// - /// The width of the area where the View is being sized (Superview.ContentSize). - /// - /// An integer representing the calculated dimension. The way this dimension is calculated depends on the specific - /// subclass of Dim that is used. For example, DimAbsolute returns a fixed dimension, DimFactor returns a - /// dimension that is a certain percentage of the super view's size, and so on. - /// - internal virtual int Anchor (int width) { return 0; } - - /// - /// Calculates and returns the dimension of a object. It takes into account the location of the - /// , it's SuperView's ContentSize, and whether it should automatically adjust its size based on its content. - /// - /// - /// The starting point from where the size calculation begins. It could be the left edge for width calculation or the - /// top edge for height calculation. - /// - /// The size of the SuperView's content. It could be width or height. - /// The View that holds this Pos object. - /// Width or Height - /// - /// The calculated size of the View. The way this size is calculated depends on the specific subclass of Dim that - /// is used. - /// - internal virtual int Calculate (int location, int superviewContentSize, View us, Dimension dimension) - { - return Math.Max (Anchor (superviewContentSize - location), 0); - } - - /// - /// Diagnostics API to determine if this Dim object references other views. - /// - /// - internal virtual bool ReferencesOtherViews () - { - return false; - } - - internal class DimAbsolute (int n) : Dim - { - 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 override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) - { - // DimAbsolute.Anchor (int width) ignores width and returns n - return Math.Max (Anchor (0), 0); - } - } - - /// - /// A object that automatically sizes the view to fit all the view's SubViews and/or Text. - /// - /// - /// - /// See . - /// - /// - /// - /// 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 class DimAuto (DimAutoStyle style, Dim min, Dim max) : Dim - { - internal readonly Dim _max = max; - internal readonly Dim _min = min; - internal readonly DimAutoStyle _style = style; - internal int _size; - - /// - 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 override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) - { - if (us == null) - { - return _max?.Anchor (0) ?? 0; - } - - var textSize = 0; - var subviewsSize = 0; - - int autoMin = _min?.Anchor (superviewContentSize) ?? 0; - - if (superviewContentSize < autoMin) - { - Debug.WriteLine ($"WARNING: DimAuto specifies a min size ({autoMin}), but the SuperView's bounds are smaller ({superviewContentSize})."); - - return superviewContentSize; - } - - if (_style.HasFlag (Dim.DimAutoStyle.Text)) - { - textSize = int.Max (autoMin, dimension == Dimension.Width ? us.TextFormatter.Size.Width : us.TextFormatter.Size.Height); - } - - if (_style.HasFlag (DimAutoStyle.Content)) - { - if (us._contentSize is { }) - { - subviewsSize = dimension == Dimension.Width ? us.ContentSize!.Value.Width : us.ContentSize!.Value.Height; - } - else - { - // TODO: AnchorEnd needs work - // TODO: If _min > 0 we can SetRelativeLayout for the subviews? - subviewsSize = 0; - if (us.Subviews.Count > 0) - { - for (int i = 0; i < us.Subviews.Count; i++) - { - var v = us.Subviews [i]; - bool isNotPosAnchorEnd = dimension == Dim.Dimension.Width ? v.X is not Pos.PosAnchorEnd : v.Y is not Pos.PosAnchorEnd; - - //if (!isNotPosAnchorEnd) - //{ - // v.SetRelativeLayout(dimension == Dim.Dimension.Width ? (new Size (autoMin, 0)) : new Size (0, autoMin)); - //} - - if (isNotPosAnchorEnd) - { - int size = dimension == Dim.Dimension.Width ? v.Frame.X + v.Frame.Width : v.Frame.Y + v.Frame.Height; - if (size > subviewsSize) - { - subviewsSize = size; - } - } - } - } - - } - } - - int max = int.Max (textSize, subviewsSize); - - Thickness thickness = us.GetAdornmentsThickness (); - - if (dimension == Dimension.Width) - { - max += thickness.Horizontal; - } - else - { - max += thickness.Vertical; - } - - max = int.Max (max, autoMin); - return int.Min (max, _max?.Anchor (superviewContentSize) ?? superviewContentSize); - } - - /// - /// Diagnostics API to determine if this Dim object references other views. - /// - /// - internal override bool ReferencesOtherViews () - { - // BUGBUG: This is not correct. _contentSize may be null. - return _style.HasFlag (Dim.DimAutoStyle.Content); - } - - } - 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})"; } - - internal override int Anchor (int width) - { - int la = _left.Anchor (width); - int ra = _right.Anchor (width); - - if (_add) - { - return la + ra; - } - - return la - ra; - } - - internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) - { - int leftNewDim = _left.Calculate (location, superviewContentSize, us, dimension); - int rightNewDim = _right.Calculate (location, superviewContentSize, us, dimension); - - int newDimension; - - if (_add) - { - newDimension = leftNewDim + rightNewDim; - } - else - { - newDimension = Math.Max (0, leftNewDim - rightNewDim); - } - - return newDimension; - } - - - /// - /// Diagnostics API to determine if this Dim object references other views. - /// - /// - internal override bool ReferencesOtherViews () - { - if (_left.ReferencesOtherViews ()) - { - return true; - } - - if (_right.ReferencesOtherViews ()) - { - return true; - } - - return false; - } - } - - internal class DimFactor (float factor, bool remaining = false) : Dim - { - private readonly float _factor = factor; - private readonly bool _remaining = remaining; - - public override bool Equals (object other) { return other is DimFactor f && f._factor == _factor && f._remaining == _remaining; } - public override int GetHashCode () { return _factor.GetHashCode (); } - public bool IsFromRemaining () { return _remaining; } - public override string ToString () { return $"Factor({_factor},{_remaining})"; } - internal override int Anchor (int width) { return (int)(width * _factor); } - - internal override int Calculate (int location, int superviewContentSize, View us, Dimension dimension) - { - return _remaining ? Math.Max (Anchor (superviewContentSize - location), 0) : Anchor (superviewContentSize); - } - } - - internal class DimFill (int margin) : Dim - { - 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})"; } - internal override int Anchor (int width) { return width - _margin; } - } - - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class DimFunc (Func n) : Dim - { - 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 class DimView : Dim - { - private readonly Dimension _side; - - internal DimView (View view, Dimension side) - { - Target = view; - _side = side; - } - - public View Target { get; init; } - public override bool Equals (object other) { return other is DimView abs && abs.Target == Target; } - public override int GetHashCode () { return Target.GetHashCode (); } - - public override string ToString () - { - if (Target == null) - { - throw new NullReferenceException (); - } - - string sideString = _side switch - { - Dimension.Height => "Height", - Dimension.Width => "Width", - _ => "unknown" - }; - - return $"View({sideString},{Target})"; - } - - internal override int Anchor (int width) - { - return _side switch - { - Dimension.Height => Target.Frame.Height, - Dimension.Width => Target.Frame.Width, - _ => 0 - }; - } - - internal override bool ReferencesOtherViews () - { - return true; - } - } -} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosFunc.cs b/Terminal.Gui/View/Layout/PosFunc.cs new file mode 100644 index 0000000000..24344f9a85 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosFunc.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position that is computed by executing a function that returns an integer position. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// The position. +public class PosFunc (Func pos) : Pos +{ + /// + /// Gets the function that computes the position. + /// + public new Func Func { get; } = pos; + + /// + public override bool Equals (object? other) { return other is PosFunc f && f.Func () == Func (); } + + /// + public override int GetHashCode () { return Func.GetHashCode (); } + + /// + public override string ToString () { return $"PosFunc({Func ()})"; } + + internal override int GetAnchor (int size) { return Func (); } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosPercent.cs b/Terminal.Gui/View/Layout/PosPercent.cs new file mode 100644 index 0000000000..384ba1fda9 --- /dev/null +++ b/Terminal.Gui/View/Layout/PosPercent.cs @@ -0,0 +1,31 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position that is a percentage of the width or height of the SuperView. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// +public class PosPercent (int percent) : Pos +{ + /// + /// Gets the percentage of the width or height of the SuperView. + /// + public new int Percent { get; } = percent; + + /// + public override bool Equals (object? other) { return other is PosPercent i && i.Percent == Percent; } + + /// + public override int GetHashCode () { return Percent.GetHashCode (); } + + /// + public override string ToString () { return $"Percent({Percent})"; } + + internal override int GetAnchor (int size) { return (int)(size * (Percent / 100f)); } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/PosView.cs b/Terminal.Gui/View/Layout/PosView.cs new file mode 100644 index 0000000000..b48613307c --- /dev/null +++ b/Terminal.Gui/View/Layout/PosView.cs @@ -0,0 +1,59 @@ +#nullable enable +namespace Terminal.Gui; + +/// +/// Represents a position that is anchored to the side of another view. +/// +/// +/// +/// This is a low-level API that is typically used internally by the layout system. Use the various static +/// methods on the class to create objects instead. +/// +/// +/// The View the position is anchored to. +/// The side of the View the position is anchored to. +public class PosView (View view, Side side) : Pos +{ + /// + /// Gets the View the position is anchored to. + /// + public View Target { get; } = view; + + /// + /// Gets the side of the View the position is anchored to. + /// + public Side Side { get; } = side; + + /// + public override bool Equals (object? other) { return other is PosView abs && abs.Target == Target && abs.Side == Side; } + + /// + public override int GetHashCode () { return Target.GetHashCode (); } + + /// + public override string ToString () + { + string sideString = Side.ToString (); + + if (Target == null) + { + throw new NullReferenceException (nameof (Target)); + } + + return $"View(Side={sideString},Target={Target})"; + } + + internal override int GetAnchor (int size) + { + return Side switch + { + Side.Left => Target.Frame.X, + Side.Top => Target.Frame.Y, + Side.Right => Target.Frame.Right, + Side.Bottom => Target.Frame.Bottom, + _ => 0 + }; + } + + internal override bool ReferencesOtherViews () { return true; } +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/Side.cs b/Terminal.Gui/View/Layout/Side.cs new file mode 100644 index 0000000000..6708904dae --- /dev/null +++ b/Terminal.Gui/View/Layout/Side.cs @@ -0,0 +1,31 @@ +using Terminal.Gui.Analyzers.Internal.Attributes; + +namespace Terminal.Gui; + +/// +/// Indicates the side for operations. +/// +/// +[GenerateEnumExtensionMethods] +public enum Side +{ + /// + /// The left (X) side of the view. + /// + Left = 0, + + /// + /// The top (Y) side of the view. + /// + Top = 1, + + /// + /// The right (X + Width) side of the view. + /// + Right = 2, + + /// + /// The bottom (Y + Height) side of the view. + /// + Bottom = 3 +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index 2c623c5067..94348fc75a 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -1,42 +1,9 @@ +#nullable enable using System.Diagnostics; using Microsoft.CodeAnalysis; namespace Terminal.Gui; -/// -/// Indicates the LayoutStyle for the . -/// -/// If Absolute, the , , , and -/// objects are all absolute values and are not relative. The position and size of the -/// view is described by . -/// -/// -/// If Computed, one or more of the , , , or -/// objects are relative to the and are computed at layout -/// time. -/// -/// -public enum LayoutStyle -{ - /// - /// Indicates the , , , and - /// objects are all absolute values and are not relative. The position and size of the view - /// is described by . - /// - Absolute, - - /// - /// 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. - /// - Computed -} - public partial class View { #region Frame @@ -101,12 +68,9 @@ private void SetFrame (in Rectangle frame) // This is the only place where _frame should be set directly. Use Frame = or SetFrame instead. _frame = frame; - OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); + SetTextFormatterSize (); - if (!TextFormatter.AutoSize) - { - TextFormatter.Size = ContentSize.GetValueOrDefault (); - } + OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport)); } /// Gets the with a screen-relative location. @@ -155,10 +119,10 @@ public virtual Point ScreenToFrame (in Point location) } Point superViewViewportOffset = SuperView.GetViewportOffsetFromFrame (); - superViewViewportOffset.Offset(-SuperView.Viewport.X, -SuperView.Viewport.Y); + superViewViewportOffset.Offset (-SuperView.Viewport.X, -SuperView.Viewport.Y); Point frame = location; - frame.Offset(-superViewViewportOffset.X, -superViewViewportOffset.Y); + frame.Offset (-superViewViewportOffset.X, -superViewViewportOffset.Y); frame = SuperView.ScreenToFrame (frame); frame.Offset (-Frame.X, -Frame.Y); @@ -166,7 +130,7 @@ public virtual Point ScreenToFrame (in Point location) return frame; } - private Pos _x = Pos.At (0); + private Pos _x = Pos.Absolute (0); /// Gets or sets the X position for the view (the column). /// The object representing the X position. @@ -185,7 +149,7 @@ public virtual Point ScreenToFrame (in Point location) /// /// /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// the will change to . /// /// The default value is Pos.At (0). /// @@ -205,7 +169,7 @@ public Pos X } } - private Pos _y = Pos.At (0); + private Pos _y = Pos.Absolute (0); /// Gets or sets the Y position for the view (the row). /// The object representing the Y position. @@ -224,7 +188,7 @@ public Pos X /// /// /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// the will change to . /// /// The default value is Pos.At (0). /// @@ -243,7 +207,7 @@ public Pos Y } } - private Dim _height = Dim.Sized (0); + private Dim? _height = Dim.Absolute (0); /// Gets or sets the height dimension of the view. /// The object representing the height of the view (the number of rows). @@ -263,11 +227,11 @@ public Pos Y /// /// /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// the will change to . /// /// The default value is Dim.Sized (0). /// - public Dim Height + public Dim? Height { get => VerifyIsInitialized (_height, nameof (Height)); set @@ -277,7 +241,7 @@ public Dim Height return; } - if (_height is Dim.DimAuto) + if (_height is DimAuto) { // Reset ContentSize to Viewport _contentSize = null; @@ -289,7 +253,7 @@ public Dim Height } } - private Dim _width = Dim.Sized (0); + private Dim? _width = Dim.Absolute (0); /// Gets or sets the width dimension of the view. /// The object representing the width of the view (the number of columns). @@ -309,11 +273,11 @@ public Dim Height /// /// /// Changing this property will cause to be updated. If the new value is not of type - /// the will change to . + /// the will change to . /// /// The default value is Dim.Sized (0). /// - public Dim Width + public Dim? Width { get => VerifyIsInitialized (_width, nameof (Width)); set @@ -323,7 +287,7 @@ public Dim Width return; } - if (_width is Dim.DimAuto) + if (_width is DimAuto) { // Reset ContentSize to Viewport _contentSize = null; @@ -360,15 +324,15 @@ public Dim Width /// /// /// Setting this property to will cause to determine the - /// size and position of the view. and will be set to + /// size and position of the view. and will be set to /// using . /// /// /// Setting this property to will cause the view to use the /// method to size and position of the view. If either of the and - /// properties are `null` they will be set to using the current value + /// properties are `null` they will be set to using the current value /// of . If either of the and properties are `null` - /// they will be set to using . + /// they will be set to using . /// /// /// The layout style. @@ -376,10 +340,10 @@ public LayoutStyle LayoutStyle { get { - if (_x is Pos.PosAbsolute - && _y is Pos.PosAbsolute - && _width is Dim.DimAbsolute - && _height is Dim.DimAbsolute) + if (_x is PosAbsolute + && _y is PosAbsolute + && _width is DimAbsolute + && _height is DimAbsolute) { return LayoutStyle.Absolute; } @@ -397,7 +361,6 @@ public LayoutStyle LayoutStyle /// if the specified SuperView-relative coordinates are within the View. public virtual bool Contains (in Point location) { return Frame.Contains (location); } -#nullable enable /// Finds the first Subview of that is visible at the provided location. /// /// @@ -472,8 +435,6 @@ public LayoutStyle LayoutStyle return null; } -#nullable restore - /// /// Gets a new location of the that is within the Viewport of the 's /// (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates. @@ -504,7 +465,7 @@ out StatusBar statusBar { int maxDimension; View superView; - statusBar = null; + statusBar = null!; if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { @@ -514,11 +475,11 @@ out StatusBar statusBar else { // Use the SuperView's Viewport, not Frame - maxDimension = viewToMove.SuperView.Viewport.Width; + maxDimension = viewToMove!.SuperView.Viewport.Width; superView = viewToMove.SuperView; } - if (superView?.Margin is { } && superView == viewToMove.SuperView) + if (superView?.Margin is { } && superView == viewToMove!.SuperView) { maxDimension -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; } @@ -548,7 +509,7 @@ out StatusBar statusBar } else { - View t = viewToMove.SuperView; + View t = viewToMove!.SuperView; while (t is { } and not Toplevel) { @@ -575,21 +536,21 @@ out StatusBar statusBar if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { statusVisible = Application.Top?.StatusBar?.Visible == true; - statusBar = Application.Top?.StatusBar; + statusBar = Application.Top?.StatusBar!; } else { - View t = viewToMove.SuperView; + View t = viewToMove!.SuperView; while (t is { } and not Toplevel) { t = t.SuperView; } - if (t is Toplevel toplevel) + if (t is Toplevel topLevel) { - statusVisible = toplevel.StatusBar?.Visible == true; - statusBar = toplevel.StatusBar; + statusVisible = topLevel.StatusBar?.Visible == true; + statusBar = topLevel.StatusBar!; } } @@ -599,7 +560,7 @@ out StatusBar statusBar } else { - maxDimension = statusVisible ? viewToMove.SuperView.Viewport.Height - 1 : viewToMove.SuperView.Viewport.Height; + maxDimension = statusVisible ? viewToMove!.SuperView.Viewport.Height - 1 : viewToMove!.SuperView.Viewport.Height; } if (superView?.Margin is { } && superView == viewToMove?.SuperView) @@ -623,7 +584,7 @@ out StatusBar statusBar //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); - return superView; + return superView!; } /// Fired after the View's method has completed. @@ -665,7 +626,7 @@ public virtual void LayoutSubviews () CheckDimAuto (); - var contentSize = ContentSize.GetValueOrDefault (); + var contentSize = ContentSize; OnLayoutStarted (new (contentSize)); LayoutAdornments (); @@ -689,7 +650,7 @@ public virtual void LayoutSubviews () { foreach ((View from, View to) in edges) { - LayoutSubview (to, from.ContentSize.GetValueOrDefault ()); + LayoutSubview (to, from.ContentSize); } } @@ -739,15 +700,16 @@ internal void OnResizeNeeded () // TODO: Identify a real-world use-case where this API should be virtual. // TODO: Until then leave it `internal` and non-virtual - // First try SuperView.Viewport, then Application.Top, then Driver.Viewport. - // Finally, if none of those are valid, use int.MaxValue (for Unit tests). - Size? contentSize = SuperView is { IsInitialized: true } ? SuperView.ContentSize : + // Determine our container's ContentSize - + // First try SuperView.Viewport, then Application.Top, then Driver.Viewport. + // Finally, if none of those are valid, use int.MaxValue (for Unit tests). + Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.ContentSize : Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.ContentSize : Application.Driver?.Screen.Size ?? new (int.MaxValue, int.MaxValue); SetTextFormatterSize (); - SetRelativeLayout (contentSize.GetValueOrDefault ()); + SetRelativeLayout (superViewContentSize); if (IsInitialized) { @@ -798,23 +760,37 @@ internal void SetNeedsLayout () /// /// The size of the SuperView's content (nominally the same as this.SuperView.ContentSize). /// - internal void SetRelativeLayout (Size? superviewContentSize) + internal void SetRelativeLayout (Size superviewContentSize) { Debug.Assert (_x is { }); Debug.Assert (_y is { }); Debug.Assert (_width is { }); Debug.Assert (_height is { }); - if (superviewContentSize is null) + CheckDimAuto (); + int newX, newW, newY, newH; + + if (_width is DimAuto) { - return; + newW = _width.Calculate (0, superviewContentSize.Width, this, Dimension.Width); + newX = _x.Calculate (superviewContentSize.Width, newW, this, Dimension.Width); + } + else + { + newX = _x.Calculate (superviewContentSize.Width, _width, this, Dimension.Width); + newW = _width.Calculate (newX, superviewContentSize.Width, this, Dimension.Width); } - CheckDimAuto (); - int newX = _x.Calculate (superviewContentSize.Value.Width, _width, this, Dim.Dimension.Width); - int newW = _width.Calculate (newX, superviewContentSize.Value.Width, this, Dim.Dimension.Width); - int newY = _y.Calculate (superviewContentSize.Value.Height, _height, this, Dim.Dimension.Height); - int newH = _height.Calculate (newY, superviewContentSize.Value.Height, this, Dim.Dimension.Height); + if (_height is DimAuto) + { + newH = _height.Calculate (0, superviewContentSize.Height, this, Dimension.Height); + newY = _y.Calculate (superviewContentSize.Height, newH, this, Dimension.Height); + } + else + { + newY = _y.Calculate (superviewContentSize.Height, _height, this, Dimension.Height); + newH = _height.Calculate (newY, superviewContentSize.Height, this, Dimension.Height); + } Rectangle newFrame = new (newX, newY, newW, newH); @@ -824,22 +800,22 @@ internal void SetRelativeLayout (Size? superviewContentSize) // the view LayoutStyle.Absolute. SetFrame (newFrame); - if (_x is Pos.PosAbsolute) + if (_x is PosAbsolute) { _x = Frame.X; } - if (_y is Pos.PosAbsolute) + if (_y is PosAbsolute) { _y = Frame.Y; } - if (_width is Dim.DimAbsolute) + if (_width is DimAbsolute) { _width = Frame.Width; } - if (_height is Dim.DimAbsolute) + if (_height is DimAbsolute) { _height = Frame.Height; } @@ -856,8 +832,7 @@ internal void SetRelativeLayout (Size? superviewContentSize) internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { - // BUGBUG: This should really only work on initialized subviews - foreach (View v in from.InternalSubviews /*.Where(v => v.IsInitialized)*/) + foreach (View? v in from.InternalSubviews) { nNodes.Add (v); @@ -873,11 +848,11 @@ internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View } } - internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) + internal void CollectDim (Dim? dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { switch (dim) { - case Dim.DimView dv: + case DimView dv: // See #2461 //if (!from.InternalSubviews.Contains (dv.Target)) { // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}"); @@ -888,9 +863,9 @@ internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref Hash } return; - case Dim.DimCombine dc: - CollectDim (dc._left, from, ref nNodes, ref nEdges); - CollectDim (dc._right, from, ref nNodes, ref nEdges); + case DimCombine dc: + CollectDim (dc.Left, from, ref nNodes, ref nEdges); + CollectDim (dc.Right, from, ref nNodes, ref nEdges); break; } @@ -900,7 +875,7 @@ internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref Hash { switch (pos) { - case Pos.PosView pv: + case PosView pv: // See #2461 //if (!from.InternalSubviews.Contains (pv.Target)) { // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}"); @@ -911,9 +886,9 @@ internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref Hash } return; - case Pos.PosCombine pc: - CollectPos (pc._left, from, ref nNodes, ref nEdges); - CollectPos (pc._right, from, ref nNodes, ref nEdges); + case PosCombine pc: + CollectPos (pc.Left, from, ref nNodes, ref nEdges); + CollectPos (pc.Right, from, ref nNodes, ref nEdges); break; } @@ -1018,31 +993,30 @@ internal static List TopologicalSort ( // 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 ((pos.ReferencesOtherViews () || pos.ReferencesOtherViews ()) && !IsInitialized) - { - Debug.WriteLine ( - $"WARNING: The {pos} of {this} is dependent on other views and {member} " - + $"is being accessed before the View has been initialized. This is likely a bug." - ); - } -#endif // DEBUG + //#if DEBUG + // if (pos.ReferencesOtherViews () && !IsInitialized) + // { + // Debug.WriteLine ( + // $"WARNING: {member} = {pos} of {this} is dependent on other views and {member} " + // + $"is being accessed before the View has been initialized. This is likely 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) + private Dim? VerifyIsInitialized (Dim? dim, string member) { -#if DEBUG - if ((dim.ReferencesOtherViews () || dim.ReferencesOtherViews ()) && !IsInitialized) - { - Debug.WriteLine ( - $"WARNING: The {member} of {this} is dependent on other views and is " - + $"is being accessed before the View has been initialized. This is likely a bug. " - + $"{member} is {dim}" - ); - } -#endif // DEBUG + //#if DEBUG + // if (dim.ReferencesOtherViews () && !IsInitialized) + // { + // Debug.WriteLine ( + // $"WARNING: {member} = {dim} of {this} is dependent on other views and {member} " + // + $"is being accessed before the View has been initialized. This is likely a bug." + // ); + // } + //#endif // DEBUG return dim; } @@ -1064,21 +1038,24 @@ private Dim VerifyIsInitialized (Dim dim, string member) /// private void CheckDimAuto () { - if (!ValidatePosDim || !IsInitialized || (Width is not Dim.DimAuto && Height is not Dim.DimAuto)) + if (!ValidatePosDim || !IsInitialized) { return; } + DimAuto? widthAuto = Width as DimAuto; + DimAuto? heightAuto = Height as DimAuto; + // 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 }) + if (widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null) { ThrowInvalid (view, view.Width, nameof (view.Width)); ThrowInvalid (view, view.X, nameof (view.X)); } - if (Height is Dim.DimAuto { _min: null }) + if (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Content) && _contentSize is null) { ThrowInvalid (view, view.Height, nameof (view.Height)); ThrowInvalid (view, view.Y, nameof (view.Y)); @@ -1087,33 +1064,42 @@ private void CheckDimAuto () return; - void ThrowInvalid (View view, object checkPosDim, string name) + void ThrowInvalid (View view, object? checkPosDim, string name) { - object bad = null; + object? bad = null; switch (checkPosDim) { - case Pos pos and not Pos.PosAbsolute and not Pos.PosView and not Pos.PosCombine: + case Pos pos and PosAnchorEnd: + break; + + case Pos pos and not PosAbsolute and not PosView and not PosCombine: bad = pos; break; - case Pos pos and Pos.PosCombine: + case Pos pos and 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); + ThrowInvalid (view, (pos as PosCombine)?.Left, name); + ThrowInvalid (view, (pos as PosCombine)?.Right, name); + + break; + + case Dim dim and DimAuto: + break; + case Dim dim and DimFill: break; - case Dim dim and not Dim.DimAbsolute and not Dim.DimView and not Dim.DimCombine: + case Dim dim and not DimAbsolute and not DimView and not DimCombine: bad = dim; break; - case Dim dim and Dim.DimCombine: + case Dim dim and 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); + ThrowInvalid (view, (dim as DimCombine)?.Left, name); + ThrowInvalid (view, (dim as DimCombine)?.Right, name); break; } diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/ViewAdornments.cs index 37355a2cf4..54b4609b1a 100644 --- a/Terminal.Gui/View/ViewAdornments.cs +++ b/Terminal.Gui/View/ViewAdornments.cs @@ -135,6 +135,11 @@ public LineStyle BorderStyle /// /// Gets the thickness describing the sum of the Adornments' thicknesses. /// + /// + /// + /// The is offset from the by the thickness returned by this method. + /// + /// /// A thickness that describes the sum of the Adornments' thicknesses. public Thickness GetAdornmentsThickness () { diff --git a/Terminal.Gui/View/ViewContent.cs b/Terminal.Gui/View/ViewContent.cs index 0a18ec0816..7fa48ed387 100644 --- a/Terminal.Gui/View/ViewContent.cs +++ b/Terminal.Gui/View/ViewContent.cs @@ -2,165 +2,73 @@ namespace Terminal.Gui; -/// -/// Settings for how the behaves relative to the View's Content area. -/// -[Flags] -public enum ViewportSettings +public partial class View { - /// - /// No settings. - /// - None = 0, - - /// - /// If set, .X can be set to negative values enabling scrolling beyond the left of - /// the - /// content area. - /// - /// - /// - /// When not set, .X is constrained to positive values. - /// - /// - AllowNegativeX = 1, + #region Content Area - /// - /// If set, .Y can be set to negative values enabling scrolling beyond the top of the - /// content area. - /// - /// - /// - /// When not set, .Y is constrained to positive values. - /// - /// - AllowNegativeY = 2, + internal Size? _contentSize; /// - /// If set, .Size can be set to negative coordinates enabling scrolling beyond the - /// top-left of the - /// content area. + /// Sets the size of the View's content. /// /// /// - /// When not set, .Size is constrained to positive coordinates. + /// By default, the content size is set to . /// /// - AllowNegativeLocation = AllowNegativeX | AllowNegativeY, - - /// - /// If set, .X can be set values greater than - /// .Width enabling scrolling beyond the right - /// of the content area. - /// - /// + /// /// - /// When not set, .X is constrained to - /// .Width - 1. - /// This means the last column of the content will remain visible even if there is an attempt to scroll the - /// Viewport past the last column. + /// If , and the View has no visible subviews, will track the size of . /// /// - /// The practical effect of this is that the last column of the content will always be visible. + /// If , and the View has visible subviews, will track the maximum position plus size of any + /// visible Subviews + /// and Viewport.Location will track the minimum position and size of any visible Subviews. /// - /// - AllowXGreaterThanContentWidth = 4, - - /// - /// If set, .Y can be set values greater than - /// .Height enabling scrolling beyond the right - /// of the content area. - /// - /// /// - /// When not set, .Y is constrained to - /// .Height - 1. - /// This means the last row of the content will remain visible even if there is an attempt to scroll the Viewport - /// past the last row. + /// If not , is set to the passed value and describes the portion of the content currently visible + /// to the user. This enables virtual scrolling. /// /// - /// The practical effect of this is that the last row of the content will always be visible. + /// If not , is set to the passed value and the behavior of will be to use the ContentSize + /// to determine the size of the view. /// - /// - AllowYGreaterThanContentHeight = 8, - - /// - /// If set, .Size can be set values greater than - /// enabling scrolling beyond the bottom-right - /// of the content area. - /// - /// /// - /// When not set, is constrained to -1. - /// This means the last column and row of the content will remain visible even if there is an attempt to - /// scroll the Viewport past the last column or row. + /// Negative sizes are not supported. /// - /// - AllowLocationGreaterThanContentSize = AllowXGreaterThanContentWidth | AllowYGreaterThanContentHeight, - - /// - /// By default, clipping is applied to the . Setting this flag will cause clipping to be - /// applied to the visible content area. - /// - ClipContentOnly = 16, - - /// - /// If set will clear only the portion of the content - /// area that is visible within the . This is useful for views that have a - /// content area larger than the Viewport and want the area outside the content to be visually distinct. - /// - /// - /// must be set for this setting to work (clipping beyond the visible area must be - /// disabled). - /// - ClearContentOnly = 32 -} + /// + public void SetContentSize (Size? contentSize) + { + if (ContentSize.Width < 0 || ContentSize.Height < 0) + { + throw new ArgumentException (@"ContentSize cannot be negative.", nameof (contentSize)); + } -public partial class View -{ - #region Content Area + if (contentSize == _contentSize) + { + return; + } - internal Size? _contentSize; + _contentSize = contentSize; + OnContentSizeChanged (new (_contentSize)); + } /// - /// Gets or sets the size of the View's content. If , the value will be the same as the size of , - /// and Viewport.Location will always be 0, 0. + /// Gets the size of the View's content. /// /// /// - /// If a size is provided, describes the portion of the content currently visible - /// to the view. This enables virtual scrolling. + /// Use to change to change the content size. /// /// - /// If a size is provided, the behavior of will be to use the ContentSize - /// to determine the size of the view. - /// - /// - /// Negative sizes are not supported. + /// If the content size has not been explicitly set with , the value tracks + /// . /// /// - public Size? ContentSize - { - get => _contentSize ?? Viewport.Size; - set - { - if (value?.Width < 0 || value?.Height < 0) - { - throw new ArgumentException (@"ContentSize cannot be negative.", nameof (value)); - } - - if (value == _contentSize) - { - return; - } - - _contentSize = value; - OnContentSizeChanged (new (_contentSize)); - } - } + public Size ContentSize => _contentSize ?? Viewport.Size; /// - /// Called when changes. Invokes the event. + /// Called when has changed. /// /// /// @@ -292,40 +200,12 @@ public virtual Rectangle Viewport { get { -#if DEBUG - if ((_width.ReferencesOtherViews () || _height.ReferencesOtherViews ()) && !IsInitialized) - { - Debug.WriteLine ( - $"WARNING: The dimensions of {this} are dependent on other views and Viewport is being accessed before the View has been initialized. This is likely a bug." - ); - } -#endif // DEBUG - if (Margin is null || Border is null || Padding is null) { // CreateAdornments has not been called yet. return new (_viewportLocation, Frame.Size); } - // BUGBUG: This is a hack. Viewport_get should not have side effects. - if (Frame.Size == Size.Empty) - { - // The Frame has not been set yet (e.g. the view has not been added to a SuperView yet). - // - if ((Width is Dim.DimAuto widthAuto && widthAuto._style.HasFlag(Dim.DimAutoStyle.Text)) - || (Height is Dim.DimAuto heightAuto && heightAuto._style.HasFlag (Dim.DimAutoStyle.Text))) - { - if (TextFormatter.NeedsFormat) - { - // This updates TextFormatter.Size to the text size - TextFormatter.AutoSize = true; - - // Whenever DimAutoStyle.Text is set, ContentSize will match TextFormatter.Size. - ContentSize = TextFormatter.Size == Size.Empty ? null : TextFormatter.Size; - } - } - } - Thickness thickness = GetAdornmentsThickness (); return new ( _viewportLocation, @@ -374,9 +254,9 @@ void ApplySettings (ref Rectangle newViewport) { if (!ViewportSettings.HasFlag (ViewportSettings.AllowXGreaterThanContentWidth)) { - if (newViewport.X >= ContentSize.GetValueOrDefault ().Width) + if (newViewport.X >= ContentSize.Width) { - newViewport.X = ContentSize.GetValueOrDefault ().Width - 1; + newViewport.X = ContentSize.Width - 1; } } @@ -391,9 +271,9 @@ void ApplySettings (ref Rectangle newViewport) if (!ViewportSettings.HasFlag (ViewportSettings.AllowYGreaterThanContentHeight)) { - if (newViewport.Y >= ContentSize.GetValueOrDefault().Height) + if (newViewport.Y >= ContentSize.Height) { - newViewport.Y = ContentSize.GetValueOrDefault ().Height - 1; + newViewport.Y = ContentSize.Height - 1; } } diff --git a/Terminal.Gui/View/ViewDrawing.cs b/Terminal.Gui/View/ViewDrawing.cs index 7c8c83dddb..95232fa8c2 100644 --- a/Terminal.Gui/View/ViewDrawing.cs +++ b/Terminal.Gui/View/ViewDrawing.cs @@ -106,7 +106,7 @@ public void Clear () if (ViewportSettings.HasFlag (ViewportSettings.ClearContentOnly)) { - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize.GetValueOrDefault ())); + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize)); toClear = Rectangle.Intersect (toClear, visibleContent); } @@ -172,7 +172,7 @@ public Rectangle SetClip () if (ViewportSettings.HasFlag (ViewportSettings.ClipContentOnly)) { // Clamp the Clip to the just content area that is within the viewport - Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize.GetValueOrDefault ())); + Rectangle visibleContent = ViewportToScreen (new Rectangle (new (-Viewport.X, -Viewport.Y), ContentSize)); clip = Rectangle.Intersect (clip, visibleContent); } @@ -475,7 +475,7 @@ public virtual void OnDrawContent (Rectangle viewport) // This should NOT clear // TODO: If the output is not in the Viewport, do nothing - var drawRect = new Rectangle (ContentToScreen (Point.Empty), ContentSize.GetValueOrDefault ()); + var drawRect = new Rectangle (ContentToScreen (Point.Empty), ContentSize); TextFormatter?.Draw ( drawRect, diff --git a/Terminal.Gui/View/ViewSubViews.cs b/Terminal.Gui/View/ViewSubViews.cs index 06bdcf5517..05d332e304 100644 --- a/Terminal.Gui/View/ViewSubViews.cs +++ b/Terminal.Gui/View/ViewSubViews.cs @@ -872,7 +872,7 @@ private View GetMostFocused (View view) /// Viewport-relative cursor position. Return to ensure the cursor is not visible. public virtual Point? PositionCursor () { - if (IsInitialized && CanFocus && HasFocus && ContentSize.HasValue) + if (IsInitialized && CanFocus && HasFocus) { // By default, position the cursor at the hotkey (if any) or 0, 0. Move (TextFormatter.HotKeyPos == -1 ? 0 : TextFormatter.CursorPosition, 0); diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 63ad748c75..4b203091b1 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -40,7 +40,7 @@ public virtual bool PreserveTrailingSpaces /// 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 or are using , + /// If or are using , /// the will be adjusted to fit the text. /// When the text changes, the is fired. /// @@ -84,10 +84,10 @@ public void OnTextChanged (string oldValue, string newValue) /// redisplay the . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// /// The text alignment. - public virtual TextAlignment TextAlignment + public virtual Alignment TextAlignment { get => TextFormatter.Alignment; set @@ -103,9 +103,9 @@ public virtual TextAlignment TextAlignment /// . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// - /// The text alignment. + /// The text direction. public virtual TextDirection TextDirection { get => TextFormatter.Direction; @@ -127,10 +127,10 @@ public virtual TextDirection TextDirection /// the . /// /// - /// or are using , the will be adjusted to fit the text. + /// or are using , the will be adjusted to fit the text. /// - /// The text alignment. - public virtual VerticalTextAlignment VerticalTextAlignment + /// The vertical text alignment. + public virtual Alignment VerticalTextAlignment { get => TextFormatter.VerticalAlignment; set @@ -175,23 +175,34 @@ internal Size GetSizeNeededForTextWithoutHotKey () /// internal void SetTextFormatterSize () { + // View subclasses can override UpdateTextFormatterText to modify the Text it holds (e.g. Checkbox and Button). + // We need to ensure TextFormatter is accurate by calling it here. UpdateTextFormatterText (); + // Default is to use ContentSize. + var size = ContentSize; + // TODO: This is a hack. Figure out how to move this into DimDimAuto // Use _width & _height instead of Width & Height to avoid debug spew - if ((_width is Dim.DimAuto widthAuto && widthAuto._style.HasFlag (Dim.DimAutoStyle.Text)) - || (_height is Dim.DimAuto heightAuto && heightAuto._style.HasFlag (Dim.DimAutoStyle.Text))) + DimAuto widthAuto = _width as DimAuto; + DimAuto heightAuto = _height as DimAuto; + if ((widthAuto is { } && widthAuto.Style.FastHasFlags (DimAutoStyle.Text)) + || (heightAuto is { } && heightAuto.Style.FastHasFlags (DimAutoStyle.Text))) { - // This updates TextFormatter.Size to the text size - TextFormatter.AutoSize = true; + size = TextFormatter.GetAutoSize (); + + if (widthAuto is null || !widthAuto.Style.FastHasFlags (DimAutoStyle.Text)) + { + size.Width = ContentSize.Width; + } - // Whenever DimAutoStyle.Text is set, ContentSize will match TextFormatter.Size. - ContentSize = TextFormatter.Size == Size.Empty ? null : TextFormatter.Size; - return; + if (heightAuto is null || !heightAuto.Style.FastHasFlags (DimAutoStyle.Text)) + { + size.Height = ContentSize.Height; + } } - TextFormatter.AutoSize = false; - TextFormatter.Size = new Size (ContentSize.GetValueOrDefault ().Width, ContentSize.GetValueOrDefault ().Height); + TextFormatter.Size = size; } private void UpdateTextDirection (TextDirection newDirection) diff --git a/Terminal.Gui/View/ViewportSettings.cs b/Terminal.Gui/View/ViewportSettings.cs new file mode 100644 index 0000000000..443d1b0caa --- /dev/null +++ b/Terminal.Gui/View/ViewportSettings.cs @@ -0,0 +1,115 @@ +namespace Terminal.Gui; + +/// +/// Settings for how the behaves relative to the View's Content area. +/// +[Flags] +public enum ViewportSettings +{ + /// + /// No settings. + /// + None = 0, + + /// + /// If set, .X can be set to negative values enabling scrolling beyond the left of + /// the + /// content area. + /// + /// + /// + /// When not set, .X is constrained to positive values. + /// + /// + AllowNegativeX = 1, + + /// + /// If set, .Y can be set to negative values enabling scrolling beyond the top of the + /// content area. + /// + /// + /// + /// When not set, .Y is constrained to positive values. + /// + /// + AllowNegativeY = 2, + + /// + /// If set, .Size can be set to negative coordinates enabling scrolling beyond the + /// top-left of the + /// content area. + /// + /// + /// + /// When not set, .Size is constrained to positive coordinates. + /// + /// + AllowNegativeLocation = AllowNegativeX | AllowNegativeY, + + /// + /// If set, .X can be set values greater than + /// .Width enabling scrolling beyond the right + /// of the content area. + /// + /// + /// + /// When not set, .X is constrained to + /// .Width - 1. + /// This means the last column of the content will remain visible even if there is an attempt to scroll the + /// Viewport past the last column. + /// + /// + /// The practical effect of this is that the last column of the content will always be visible. + /// + /// + AllowXGreaterThanContentWidth = 4, + + /// + /// If set, .Y can be set values greater than + /// .Height enabling scrolling beyond the right + /// of the content area. + /// + /// + /// + /// When not set, .Y is constrained to + /// .Height - 1. + /// This means the last row of the content will remain visible even if there is an attempt to scroll the Viewport + /// past the last row. + /// + /// + /// The practical effect of this is that the last row of the content will always be visible. + /// + /// + AllowYGreaterThanContentHeight = 8, + + /// + /// If set, .Size can be set values greater than + /// enabling scrolling beyond the bottom-right + /// of the content area. + /// + /// + /// + /// When not set, is constrained to -1. + /// This means the last column and row of the content will remain visible even if there is an attempt to + /// scroll the Viewport past the last column or row. + /// + /// + AllowLocationGreaterThanContentSize = AllowXGreaterThanContentWidth | AllowYGreaterThanContentHeight, + + /// + /// By default, clipping is applied to the . Setting this flag will cause clipping to be + /// applied to the visible content area. + /// + ClipContentOnly = 16, + + /// + /// If set will clear only the portion of the content + /// area that is visible within the . This is useful for views that have a + /// content area larger than the Viewport and want the area outside the content to be visually distinct. + /// + /// + /// must be set for this setting to work (clipping beyond the visible area must be + /// disabled). + /// + ClearContentOnly = 32 +} \ No newline at end of file diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs index 3639866ff4..5b1cfcf3d1 100644 --- a/Terminal.Gui/Views/Button.cs +++ b/Terminal.Gui/Views/Button.cs @@ -37,16 +37,16 @@ public class Button : View /// The width of the is computed based on the text length. The height will always be 1. public Button () { - TextAlignment = TextAlignment.Centered; - VerticalTextAlignment = VerticalTextAlignment.Middle; + TextAlignment = Alignment.Center; + VerticalTextAlignment = Alignment.Center; _leftBracket = Glyphs.LeftBracket; _rightBracket = Glyphs.RightBracket; _leftDefault = Glyphs.LeftDefaultIndicator; _rightDefault = Glyphs.RightDefaultIndicator; - Height = 1; - Width = Dim.Auto (Dim.DimAutoStyle.Text); + Width = Dim.Auto (DimAutoStyle.Text); + Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1); CanFocus = true; HighlightStyle |= HighlightStyle.Pressed; diff --git a/Terminal.Gui/Views/CheckBox.cs b/Terminal.Gui/Views/CheckBox.cs index 0535f5f438..cf0adeefc4 100644 --- a/Terminal.Gui/Views/CheckBox.cs +++ b/Terminal.Gui/Views/CheckBox.cs @@ -20,8 +20,8 @@ public CheckBox () _charChecked = Glyphs.Checked; _charUnChecked = Glyphs.UnChecked; - Height = 1; - Width = Dim.Auto (Dim.DimAutoStyle.Text); + Width = Dim.Auto (DimAutoStyle.Text); + Height = Dim.Auto (DimAutoStyle.Text, minimumContentDim: 1); CanFocus = true; @@ -155,14 +155,14 @@ protected override void UpdateTextFormatterText () { switch (TextAlignment) { - case TextAlignment.Left: - case TextAlignment.Centered: - case TextAlignment.Justified: - TextFormatter.Text = $"{GetCheckedState ()} {GetFormatterText ()}"; + case Alignment.Start: + case Alignment.Center: + case Alignment.Fill: + TextFormatter.Text = $"{GetCheckedState ()} {Text}"; break; - case TextAlignment.Right: - TextFormatter.Text = $"{GetFormatterText ()} {GetCheckedState ()}"; + case Alignment.End: + TextFormatter.Text = $"{Text} {GetCheckedState ()}"; break; } @@ -177,14 +177,4 @@ private Rune GetCheckedState () var _ => _charNullChecked }; } - - private string GetFormatterText () - { - if (Width is Dim.DimAuto || string.IsNullOrEmpty (Title) || ContentSize?.Width <= 2) - { - return Text; - } - - return ContentSize is null ? Text : Text [..Math.Min (ContentSize.Value.Width - 2, Text.GetRuneCount ())]; - } } diff --git a/Terminal.Gui/Views/ColorPicker.cs b/Terminal.Gui/Views/ColorPicker.cs index 91be0e666f..c61fdc5f3e 100644 --- a/Terminal.Gui/Views/ColorPicker.cs +++ b/Terminal.Gui/Views/ColorPicker.cs @@ -37,12 +37,9 @@ private void SetInitialProperties () AddCommands (); AddKeyBindings (); - LayoutStarted += (o, a) => - { - Thickness thickness = GetAdornmentsThickness (); - Width = _cols * BoxWidth + thickness.Vertical; - Height = _rows * BoxHeight + thickness.Horizontal; - }; + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize(new (_boxWidth * _cols, _boxHeight * _rows)); MouseClick += ColorPicker_MouseClick; } @@ -68,6 +65,9 @@ public int BoxHeight if (_boxHeight != value) { _boxHeight = value; + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); SetNeedsLayout (); } } @@ -82,6 +82,9 @@ public int BoxWidth if (_boxWidth != value) { _boxWidth = value; + Width = Dim.Auto (minimumContentDim: _boxWidth * _cols); + Height = Dim.Auto (minimumContentDim: _boxHeight * _rows); + SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows)); SetNeedsLayout (); } } @@ -175,9 +178,9 @@ public override void OnDrawContent (Rectangle viewport) Driver.SetAttribute (HasFocus ? ColorScheme.Focus : GetNormalColor ()); var colorIndex = 0; - for (var y = 0; y < Viewport.Height / BoxHeight; y++) + for (var y = 0; y < Math.Max(2, viewport.Height / BoxHeight); y++) { - for (var x = 0; x < Viewport.Width / BoxWidth; x++) + for (var x = 0; x < Math.Max(8, viewport.Width / BoxWidth); x++) { int foregroundColorIndex = y == 0 ? colorIndex + _cols : colorIndex - _cols; Driver.SetAttribute (new Attribute ((ColorName)foregroundColorIndex, (ColorName)colorIndex)); diff --git a/Terminal.Gui/Views/ComboBox.cs b/Terminal.Gui/Views/ComboBox.cs index 84cba0b4f0..3eb630e644 100644 --- a/Terminal.Gui/Views/ComboBox.cs +++ b/Terminal.Gui/Views/ComboBox.cs @@ -605,9 +605,10 @@ private bool PageUp () return true; } + // TODO: Upgrade Combobox to use Dim.Auto instead of all this stuff. private void ProcessLayout () { - if (Viewport.Height < _minimumHeight && (Height is null || Height is Dim.DimAbsolute)) + if (Viewport.Height < _minimumHeight && (Height is null || Height is DimAbsolute)) { Height = _minimumHeight; } @@ -618,8 +619,8 @@ private void ProcessLayout () { _search.Width = _listview.Width = _autoHide ? Viewport.Width - 1 : Viewport.Width; _listview.Height = CalculatetHeight (); - _search.SetRelativeLayout (ContentSize.GetValueOrDefault()); - _listview.SetRelativeLayout (ContentSize.GetValueOrDefault ()); + _search.SetRelativeLayout (ContentSize); + _listview.SetRelativeLayout (ContentSize); } } diff --git a/Terminal.Gui/Views/DatePicker.cs b/Terminal.Gui/Views/DatePicker.cs index 075c9f890b..d0cb41330a 100644 --- a/Terminal.Gui/Views/DatePicker.cs +++ b/Terminal.Gui/Views/DatePicker.cs @@ -65,8 +65,6 @@ protected override void Dispose (bool disposing) base.Dispose (disposing); } - private int CalculateCalendarWidth () { return _calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7; } - private void ChangeDayDate (int day) { _date = new DateTime (_date.Year, _date.Month, day); @@ -160,7 +158,8 @@ private void GenerateCalendarLabels () _table.Columns.Add (abbreviatedDayName); } - _calendar.Width = CalculateCalendarWidth (); + // TODO: Get rid of the +7 which is hackish + _calendar.Width = _calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7; } private string GetBackButtonText () { return Glyphs.LeftArrow + Glyphs.LeftArrow.ToString (); } @@ -189,15 +188,6 @@ private void SetInitialProperties (DateTime date) Date = date; _dateLabel = new Label { X = 0, Y = 0, Text = "Date: " }; - _dateField = new DateField (DateTime.Now) - { - X = Pos.Right (_dateLabel), - Y = 0, - Width = Dim.Fill (1), - Height = 1, - Culture = Culture - }; - _calendar = new TableView { X = 0, @@ -212,11 +202,19 @@ private void SetInitialProperties (DateTime date) } }; + _dateField = new DateField (DateTime.Now) + { + X = Pos.Right (_dateLabel), + Y = 0, + Width = Dim.Width (_calendar) - Dim.Width (_dateLabel), + Height = 1, + Culture = Culture + }; + _previousMonthButton = new Button { X = Pos.Center () - 2, Y = Pos.Bottom (_calendar) - 1, - Height = 1, Width = 2, Text = GetBackButtonText (), WantContinuousButtonPressed = true, @@ -235,9 +233,8 @@ private void SetInitialProperties (DateTime date) { X = Pos.Right (_previousMonthButton) + 2, Y = Pos.Bottom (_calendar) - 1, - Height = 1, Width = 2, - Text = GetForwardButtonText(), + Text = GetForwardButtonText (), WantContinuousButtonPressed = true, NoPadding = true, NoDecorations = true @@ -274,8 +271,11 @@ private void SetInitialProperties (DateTime date) Text = _date.ToString (Format); }; - Width = CalculateCalendarWidth () + 2; - Height = _calendar.Height + 3; + Width = Dim.Auto (DimAutoStyle.Content); + Height = Dim.Auto (DimAutoStyle.Content); + + // BUGBUG: Remove when Dim.Auto(subviews) fully works + SetContentSize (new (_calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7, _calendar.Frame.Height + 1)); _dateField.DateChanged += DateField_DateChanged; @@ -285,35 +285,35 @@ private void SetInitialProperties (DateTime date) private static string StandardizeDateFormat (string format) { return format switch - { - "MM/dd/yyyy" => "MM/dd/yyyy", - "yyyy-MM-dd" => "yyyy-MM-dd", - "yyyy/MM/dd" => "yyyy/MM/dd", - "dd/MM/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy" => "dd/MM/yyyy", - "dd.MM.yyyy" => "dd.MM.yyyy", - "dd-MM-yyyy" => "dd-MM-yyyy", - "dd/MM yyyy" => "dd/MM/yyyy", - "d. M. yyyy" => "dd.MM.yyyy", - "yyyy.MM.dd" => "yyyy.MM.dd", - "g yyyy/M/d" => "yyyy/MM/dd", - "d/M/yyyy" => "dd/MM/yyyy", - "d?/M?/yyyy g" => "dd/MM/yyyy", - "d-M-yyyy" => "dd-MM-yyyy", - "d.MM.yyyy" => "dd.MM.yyyy", - "d.MM.yyyy '?'." => "dd.MM.yyyy", - "M/d/yyyy" => "MM/dd/yyyy", - "d. M. yyyy." => "dd.MM.yyyy", - "d.M.yyyy." => "dd.MM.yyyy", - "g yyyy-MM-dd" => "yyyy-MM-dd", - "d.M.yyyy" => "dd.MM.yyyy", - "d/MM/yyyy" => "dd/MM/yyyy", - "yyyy/M/d" => "yyyy/MM/dd", - "dd. MM. yyyy." => "dd.MM.yyyy", - "yyyy. MM. dd." => "yyyy.MM.dd", - "yyyy. M. d." => "yyyy.MM.dd", - "d. MM. yyyy" => "dd.MM.yyyy", - _ => "dd/MM/yyyy" - }; + { + "MM/dd/yyyy" => "MM/dd/yyyy", + "yyyy-MM-dd" => "yyyy-MM-dd", + "yyyy/MM/dd" => "yyyy/MM/dd", + "dd/MM/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy" => "dd/MM/yyyy", + "dd.MM.yyyy" => "dd.MM.yyyy", + "dd-MM-yyyy" => "dd-MM-yyyy", + "dd/MM yyyy" => "dd/MM/yyyy", + "d. M. yyyy" => "dd.MM.yyyy", + "yyyy.MM.dd" => "yyyy.MM.dd", + "g yyyy/M/d" => "yyyy/MM/dd", + "d/M/yyyy" => "dd/MM/yyyy", + "d?/M?/yyyy g" => "dd/MM/yyyy", + "d-M-yyyy" => "dd-MM-yyyy", + "d.MM.yyyy" => "dd.MM.yyyy", + "d.MM.yyyy '?'." => "dd.MM.yyyy", + "M/d/yyyy" => "MM/dd/yyyy", + "d. M. yyyy." => "dd.MM.yyyy", + "d.M.yyyy." => "dd.MM.yyyy", + "g yyyy-MM-dd" => "yyyy-MM-dd", + "d.M.yyyy" => "dd.MM.yyyy", + "d/MM/yyyy" => "dd/MM/yyyy", + "yyyy/M/d" => "yyyy/MM/dd", + "dd. MM. yyyy." => "dd.MM.yyyy", + "yyyy. MM. dd." => "yyyy.MM.dd", + "yyyy. M. d." => "yyyy.MM.dd", + "d. MM. yyyy" => "dd.MM.yyyy", + _ => "dd/MM/yyyy" + }; } } diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 5fcfd6b072..d0376ca84b 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -15,21 +15,32 @@ namespace Terminal.Gui; /// public class Dialog : Window { - /// Determines the horizontal alignment of the Dialog buttons. - public enum ButtonAlignments - { - /// Center-aligns the buttons (the default). - Center = 0, + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static Alignment DefaultButtonAlignment { get; set; } = Alignment.End; - /// Justifies the buttons - Justify, + /// The default for . + /// This property can be set in a Theme. + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static AlignmentModes DefaultButtonAlignmentModes { get; set; } = AlignmentModes.StartToEnd | AlignmentModes.AddSpaceBetweenItems; - /// Left-aligns the buttons - Left, + /// + /// Defines the default minimum Dialog width, as a percentage of the container width. Can be configured via + /// . + /// + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + public static int DefaultMinimumWidth { get; set; } = 25; + + /// + /// Defines the default minimum Dialog height, as a percentage of the container width. Can be configured via + /// . + /// + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + public static int DefaultMinimumHeight { get; set; } = 25; - /// Right-aligns the buttons - Right - } // TODO: Reenable once border/borderframe design is settled /// @@ -43,8 +54,6 @@ public enum ButtonAlignments //}; private readonly List