Tig 1 年之前
父节点
当前提交
a355f34821

+ 15 - 76
Terminal.Gui/View/Layout/Pos.cs

@@ -135,6 +135,11 @@ namespace Terminal.Gui;
 /// </remarks>
 public abstract class Pos
 {
+    #region static Pos creation methods
+    /// <summary>Creates a <see cref="Pos"/> object that is an absolute position based on the specified integer value.</summary>
+    /// <returns>The Absolute <see cref="Pos"/>.</returns>
+    /// <param name="position">The value to convert to the <see cref="Pos"/>.</param>
+    public static Pos Absolute (int position) { return new PosAbsolute (position); }
 
     /// <summary>
     ///     Creates a <see cref="Pos"/> object that aligns a set of views according to the specified alignment setting.
@@ -318,85 +323,20 @@ public abstract class Pos
     /// </summary>
     /// <returns></returns>
     internal virtual bool ReferencesOtherViews () { return false; }
-}
 
-/// <summary>
-///    Represents an absolute position in the layout. This is used to specify a fixed position in the layout.
-/// </summary>
-/// <remarks>
-///     <para>
-///     This is a low-level API that is typically used internally by the layout system. Use the various static
-///     methods on the <see cref="Pos"/> class to create <see cref="Pos"/> objects instead.
-///     </para>
-/// </remarks>
-/// <param name="position"></param>
-public class PosAbsolute (int position) : Pos
-{
-    /// <summary>
-    ///    The position of the <see cref="View"/> in the layout.
-    /// </summary>
-    public int Position { get; } = position;
+    #endregion virtual methods
 
-    /// <inheritdoc />
-    public override bool Equals (object other) { return other is PosAbsolute abs && abs.Position == Position; }
+    #region operators
 
-    /// <inheritdoc />
-    public override int GetHashCode () { return Position.GetHashCode (); }
-
-    /// <inheritdoc />
-    public override string ToString () { return $"Absolute({Position})"; }
-
-    internal override int Anchor (int width) { return Position; }
-}
-
-/// <summary>
-///     Represents a position anchored to the end (right side or bottom).
-/// </summary>
-/// <remarks>
-///     <para>
-///     This is a low-level API that is typically used internally by the layout system. Use the various static
-///     methods on the <see cref="Pos"/> class to create <see cref="Pos"/> objects instead.
-///     </para>
-/// </remarks>
-public class PosAnchorEnd : Pos
-{
-    /// <summary>
-    /// Gets the offset of the position from the right/bottom.
-    /// </summary>
-    public int Offset { get; }
-
-    /// <summary>
-    ///     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 <see cref="PosAnchorEnd(int)"/>,
-    ///     with an offset equivalent to the View's respective dimension.
-    /// </summary>
-    public PosAnchorEnd () { UseDimForOffset = true; }
-
-    /// <summary>
-    ///     Constructs a new position anchored to the end (right side or bottom) of the SuperView,
-    /// </summary>
-    /// <param name="offset"></param>
-    public PosAnchorEnd (int offset) { Offset = offset; }
-
-    /// <inheritdoc />
-    public override bool Equals (object other) { return other is PosAnchorEnd anchorEnd && anchorEnd.Offset == Offset; }
-
-    /// <inheritdoc />
-    public override int GetHashCode () { return Offset.GetHashCode (); }
-
-    /// <summary>
-    ///     If true, the offset is the width of the view, if false, the offset is the offset value.
-    /// </summary>
-    public bool UseDimForOffset { get; }
-
-    /// <inheritdoc />
-    public override string ToString () { return UseDimForOffset ? "AnchorEnd()" : $"AnchorEnd({Offset})"; }
-
-    internal override int Anchor (int width)
+    /// <summary>Adds a <see cref="Terminal.Gui.Pos"/> to a <see cref="Terminal.Gui.Pos"/>, yielding a new <see cref="Pos"/>.</summary>
+    /// <param name="left">The first <see cref="Terminal.Gui.Pos"/> to add.</param>
+    /// <param name="right">The second <see cref="Terminal.Gui.Pos"/> to add.</param>
+    /// <returns>The <see cref="Pos"/> that is the sum of the values of <c>left</c> and <c>right</c>.</returns>
+    public static Pos operator + (Pos left, Pos right)
     {
-        if (UseDimForOffset)
+        if (left is PosAbsolute && right is PosAbsolute)
         {
-            return width;
+            return new PosAbsolute (left.GetAnchor (0) + right.GetAnchor (0));
         }
 
         var newPos = new PosCombine (AddOrSubtract.Add, left, right);
@@ -439,5 +379,4 @@ public class PosAnchorEnd : Pos
     }
 
     #endregion operators
-
-}
+}

+ 162 - 0
Terminal.Gui/View/Layout/PosAlign.cs

@@ -0,0 +1,162 @@
+#nullable enable
+
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+/// <summary>
+///     Enables alignment of a set of views.
+/// </summary>
+/// <remarks>
+///     <para>
+///         The Group ID is used to identify a set of views that should be alignment together. When only a single
+///         set of views is aligned, setting the Group ID is not needed because it defaults to 0.
+///     </para>
+///     <para>
+///         The first view added to the Superview with a given Group ID is used to determine the alignment of the group.
+///         The alignment is applied to all views with the same Group ID.
+///     </para>
+/// </remarks>
+public class PosAlign : Pos
+{
+    /// <summary>
+    ///     The cached location. Used to store the calculated location to avoid recalculating it.
+    /// </summary>
+    private int? _location;
+
+    /// <summary>
+    ///     Gets the identifier of a set of views that should be aligned together. When only a single
+    ///     set of views is aligned, setting the <see cref="_groupId"/> is not needed because it defaults to 0.
+    /// </summary>
+    private readonly int _groupId;
+
+    /// <summary>
+    ///     Gets the alignment settings.
+    /// </summary>
+    public Aligner Aligner { get; } = new ();
+
+    /// <summary>
+    ///     Aligns the views in <paramref name="views"/> that have the same group ID as <paramref name="groupId"/>.
+    /// </summary>
+    /// <param name="groupId"></param>
+    /// <param name="views"></param>
+    /// <param name="dimension"></param>
+    /// <param name="size"></param>
+    private static void AlignGroup (int groupId, IList<View> views, Dimension dimension, int size)
+    {
+        if (views is null)
+        {
+            return;
+        }
+
+        Aligner firstInGroup = null;
+        List<int> dimensionsList = new ();
+
+        List<View> viewsInGroup = views.Where (
+                                               v =>
+                                               {
+                                                   if (dimension == Dimension.Width && v.X is PosAlign alignX)
+                                                   {
+                                                       return alignX._groupId == groupId;
+                                                   }
+
+                                                   if (dimension == Dimension.Height && v.Y is PosAlign alignY)
+                                                   {
+                                                       return alignY._groupId == groupId;
+                                                   }
+
+                                                   return false;
+                                               })
+                                       .ToList ();
+
+        if (viewsInGroup.Count == 0)
+        {
+            return;
+        }
+
+        foreach (View view in viewsInGroup)
+        {
+            PosAlign posAlign = dimension == Dimension.Width ? view.X as PosAlign : view.Y as PosAlign;
+
+            if (posAlign is { })
+            {
+                if (firstInGroup is null)
+                {
+                    firstInGroup = posAlign.Aligner;
+                }
+
+                dimensionsList.Add (dimension == Dimension.Width ? view.Frame.Width : view.Frame.Height);
+            }
+        }
+
+        if (firstInGroup is null)
+        {
+            return;
+        }
+
+        firstInGroup.ContainerSize = size;
+        int [] locations = firstInGroup.Align (dimensionsList.ToArray ());
+
+        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._location = locations [index];
+            }
+        }
+    }
+
+    /// <summary>
+    ///     Enables alignment of a set of views.
+    /// </summary>
+    /// <param name="alignment"></param>
+    /// <param name="groupId">The unique identifier for the set of views to align according to <paramref name="alignment"/>.</param>
+    public PosAlign (Alignment alignment, int groupId = 0)
+    {
+        Aligner.SpaceBetweenItems = true;
+        Aligner.Alignment = alignment;
+        _groupId = groupId;
+        Aligner.PropertyChanged += Aligner_PropertyChanged;
+    }
+
+    private void Aligner_PropertyChanged (object? sender, PropertyChangedEventArgs e) { _location = null; }
+
+    /// <inheritdoc/>
+    public override bool Equals (object other)
+    {
+        return other is PosAlign align && _groupId == align._groupId && _location == align._location && align.Aligner.Alignment == Aligner.Alignment;
+    }
+
+    /// <inheritdoc/>
+    public override int GetHashCode () { return Aligner.GetHashCode () ^ _groupId.GetHashCode (); }
+
+    /// <inheritdoc/>
+    public override string ToString () { return $"Align(groupId={_groupId}, alignment={Aligner.Alignment})"; }
+
+    internal override int GetAnchor (int width) { return _location ?? 0 - width; }
+
+    internal override int Calculate (int superviewDimension, Dim dim, View us, Dimension dimension)
+    {
+        if (_location.HasValue && Aligner.ContainerSize == superviewDimension)
+        {
+            return _location.Value;
+        }
+
+        if (us?.SuperView is null)
+        {
+            return 0;
+        }
+
+        AlignGroup (_groupId, us.SuperView.Subviews, dimension, superviewDimension);
+
+        if (_location.HasValue)
+        {
+            return _location.Value;
+        }
+
+        return 0;
+    }
+}

+ 0 - 6
Terminal.Gui/Views/Dialog.cs

@@ -64,12 +64,6 @@ public class Dialog : Window
                     });
         KeyBindings.Add (Key.Esc, Command.QuitToplevel);
 
-        Initialized += Dialog_Initialized; ;
-    }
-
-    private void Dialog_Initialized (object sender, EventArgs e)
-    {
-        LayoutButtons ();
     }
 
     private bool _canceled;

+ 2 - 2
Terminal.Gui/Views/Slider.cs

@@ -1365,7 +1365,7 @@ public class Slider<T> : View
                     switch (_config._legendsOrientation)
                     {
                         case Orientation.Horizontal:
-                            text = AlignText (text, _config._cachedInnerSpacing + 1, TextAlignment.Centered);
+                            text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Centered);
 
                             break;
                         case Orientation.Vertical:
@@ -1383,7 +1383,7 @@ public class Slider<T> : View
 
                             break;
                         case Orientation.Vertical:
-                            text = AlignText (text, _config._cachedInnerSpacing + 1, TextAlignment.Centered);
+                            text = AlignText (text, _config._cachedInnerSpacing + 1, Alignment.Centered);
 
                             break;
                     }

+ 1 - 1
UnitTests/View/Layout/Pos.AlignTests.cs

@@ -43,7 +43,7 @@ public class PosAlignTests ()
         var width = 50;
         var expectedAnchor = -width;
 
-        Assert.Equal (expectedAnchor, posAlign.Anchor (width));
+        Assert.Equal (expectedAnchor, posAlign.GetAnchor (width));
     }
 
     [Fact]

+ 0 - 26
UnitTests/View/Layout/Pos.Tests.cs

@@ -222,32 +222,6 @@ public class PosTests ()
     [TestRespondersDisposed]
     public void LeftTopBottomRight_Win_ShouldNotThrow ()
     {
-        // Setup Fake driver
-        (Toplevel top, Window win, Button button) Setup ()
-        {
-            Application.Init (new FakeDriver ());
-            Application.Iteration += (s, a) => { Application.RequestStop (); };
-            var win = new Window { X = 0, Y = 0, Width = Dim.Fill (), Height = Dim.Fill () };
-            var top = new Toplevel ();
-            top.Add (win);
-
-            var button = new Button { X = Pos.Center (), Text = "button" };
-            win.Add (button);
-
-            return (top, win, button);
-        }
-
-        void Cleanup (RunState rs)
-        {
-            // Cleanup
-            Application.End (rs);
-
-            Application.Top.Dispose ();
-
-            // Shutdown must be called to safely clean up Application if Init has been called
-            Application.Shutdown ();
-        }
-
         // Test cases:
         (Toplevel top, Window win, Button button) app = Setup ();
         app.button.Y = Pos.Left (app.win);