From a7209bcd882ba310d52c97b2237935249e010fcb Mon Sep 17 00:00:00 2001 From: Tig Date: Tue, 26 Dec 2023 09:28:43 -0700 Subject: [PATCH] Improves robustness of `Dim`, `Pos`, and `SetRelativeLayout` (#3077) * Updated overview docs * Updated toc * Updated docs more * Updated yml via dependabot * Initial work in progress * Fixed some autosize things * Revamped Pos / Dim API docs * Removed margin * horiz->width * Updated MessageBoxes and Dialogs Scenarios to use AutoSize * AutoSize->Auxo * Adds validation * prep for Dialog to use Dim.Auto - Simplify unit tests to not depend on things not important to the unit test (like Dialog) * prep for Dialog to use Dim.Auto - Simplify unit tests * prep for Dialog to use Dim.Auto - Simplify unit tests * prep for Dialog to use Dim.Auto - Make Dialog tests not depend on MessageBox * Started on DimAuto unit tests * started impl on min/max. * started impl on min/max. * Added DimAutoStyle * Added arg checking for not implemented features * Temporarily made DimAutoStyle.Subviews default * Removed unneeded override of Anchor * Fixed GethashCode warning * Implemented DimAuto(min) * Fixed unit tests * renamed scenario * WIP * Moved ViewLayout.cs into Layout folder * Clean up cocde formatting * Renamed and moved SetFrameToFitText * Fixed API docs for SetRelativeLayout * Factored out SetRelativeLayout tests * Better documented existing SetRelativeLayout behavior + unit tess * Debugging Pos.Center + x in SetRelativeLayout - WIP * Progress on low level unit tess * Initial commit * Restored unmodified scenarios * Bump deps --- .github/workflows/api-docs.yml | 4 +- Terminal.Gui/Application.cs | 8 + Terminal.Gui/ConsoleDrivers/WindowsDriver.cs | 2 +- Terminal.Gui/Text/ViewLayout.cs | 1232 -------- Terminal.Gui/View/Layout/PosDim.cs | 1251 ++++---- Terminal.Gui/View/Layout/ViewLayout.cs | 1219 ++++++++ Terminal.Gui/View/View.cs | 1 + Terminal.Gui/View/ViewText.cs | 77 +- Terminal.Gui/Views/Dialog.cs | 448 +-- Terminal.Gui/Views/MessageBox.cs | 4 +- Terminal.Gui/Views/RadioGroup.cs | 2 +- UICatalog/Scenarios/ComputedLayout.cs | 13 + UICatalog/Scenarios/Dialogs.cs | 2 +- UICatalog/Scenarios/MessageBoxes.cs | 2 +- UnitTests/Application/ApplicationTests.cs | 48 +- UnitTests/Dialogs/DialogTests.cs | 68 +- UnitTests/Text/UnicodeTests.cs | 57 - UnitTests/UnitTests.csproj | 4 +- UnitTests/View/DrawTests.cs | 602 ++-- UnitTests/View/Layout/DimTests.cs | 1768 +++++------ UnitTests/View/Layout/LayoutTests.cs | 2659 ++++++++--------- UnitTests/View/Layout/PosTests.cs | 1816 ++++++----- .../View/Layout/SetRelativeLayoutTests.cs | 456 +++ UnitTests/View/ViewTests.cs | 33 +- UnitTests/Views/ContextMenuTests.cs | 8 +- UnitTests/Views/LabelTests.cs | 2 +- UnitTests/Views/ToplevelTests.cs | 337 +-- 27 files changed, 6252 insertions(+), 5871 deletions(-) delete mode 100644 Terminal.Gui/Text/ViewLayout.cs create mode 100644 Terminal.Gui/View/Layout/ViewLayout.cs delete mode 100644 UnitTests/Text/UnicodeTests.cs create mode 100644 UnitTests/View/Layout/SetRelativeLayoutTests.cs diff --git a/.github/workflows/api-docs.yml b/.github/workflows/api-docs.yml index ea46a750a..0386c3fef 100644 --- a/.github/workflows/api-docs.yml +++ b/.github/workflows/api-docs.yml @@ -36,14 +36,14 @@ jobs: - name: Upload artifact if: github.ref_name == 'main' || github.ref_name == 'develop' - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: docfx/_site - name: Deploy to GitHub Pages if: github.ref_name == 'main' || github.ref_name == 'develop' id: deployment - uses: actions/deploy-pages@v3 + uses: actions/deploy-pages@v4 with: token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application.cs index 785331ada..1ec014d5f 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application.cs @@ -317,6 +317,8 @@ public static partial class Application { } else if (Top != null && Toplevel != Top && _topLevels.Contains (Top)) { Top.OnLeave (Toplevel); } + // BUGBUG: We should not depend on `Id` internally. + // BUGBUG: It is super unclear what this code does anyway. if (string.IsNullOrEmpty (Toplevel.Id)) { int count = 1; string id = (_topLevels.Count + count).ToString (); @@ -836,6 +838,12 @@ public static partial class Application { #endregion Run (Begin, Run, End) #region Toplevel handling + + /// + /// Holds the stack of TopLevel views. + /// + // BUGBUG: Techncally, this is not the full lst of TopLevels. THere be dragons hwre. E.g. see how Toplevel.Id is used. What + // about TopLevels that are just a SubView of another View? static readonly Stack _topLevels = new (); /// diff --git a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs index b78d0d2dc..cda6e1359 100644 --- a/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs +++ b/Terminal.Gui/ConsoleDrivers/WindowsDriver.cs @@ -1077,7 +1077,7 @@ internal class WindowsDriver : ConsoleDriver { inputEvent.KeyEvent = FromVKPacketToKeyEventRecord (inputEvent.KeyEvent); } var keyInfo = ToConsoleKeyInfoEx (inputEvent.KeyEvent); - Debug.WriteLine ($"event: {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); + //Debug.WriteLine ($"event: {inputEvent.ToString ()} {keyInfo.ToString (keyInfo)}"); var map = MapKey (keyInfo); diff --git a/Terminal.Gui/Text/ViewLayout.cs b/Terminal.Gui/Text/ViewLayout.cs deleted file mode 100644 index faeb8e174..000000000 --- a/Terminal.Gui/Text/ViewLayout.cs +++ /dev/null @@ -1,1232 +0,0 @@ -using System; -using System.Collections.Generic; -using System.ComponentModel; -using System.Diagnostics; -using System.Linq; -using System.Reflection; -using System.Text; - -namespace Terminal.Gui { - /// - /// Determines the LayoutStyle for a , if Absolute, during , the - /// value from the will be used, if the value is Computed, then - /// will be updated from the X, Y objects and the Width and Height objects. - /// - public enum LayoutStyle { - /// - /// The position and size of the view are based . - /// - Absolute, - - /// - /// The position and size of the view will be computed based on - /// , , , and . will - /// provide the absolute computed values. - /// - Computed - } - - public partial class View { - - // The frame for the object. Relative to the SuperView's Bounds. - Rect _frame; - - /// - /// Gets or sets the frame for the view. The frame is relative to the 's . - /// - /// The frame. - /// - /// - /// Change the Frame when using the layout style to move or resize views. - /// - /// - /// Altering the Frame of a view will trigger the redrawing of the - /// view as well as the redrawing of the affected regions of the . - /// - /// - public virtual Rect Frame { - get => _frame; - set { - _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0)); - if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) { - LayoutFrames (); - TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); - SetNeedsLayout (); - SetNeedsDisplay (); - } - } - } - - /// - /// The frame (specified as a ) that separates a View from other SubViews of the same SuperView. - /// The margin offsets the from the . - /// - /// - /// - /// The frames (, , and ) are not part of the View's content - /// and are not clipped by the View's Clip Area. - /// - /// - /// Changing the size of a frame (, , or ) - /// will change the size of the and trigger to update the layout of the - /// and its . - /// - /// - public Frame Margin { get; private set; } - - /// - /// The frame (specified as a ) inside of the view that offsets the from the . - /// The Border provides the space for a visual border (drawn using line-drawing glyphs) and the Title. - /// The Border expands inward; in other words if `Border.Thickness.Top == 2` the border and - /// title will take up the first row and the second row will be filled with spaces. - /// - /// - /// - /// provides a simple helper for turning a simple border frame on or off. - /// - /// - /// The frames (, , and ) are not part of the View's content - /// and are not clipped by the View's Clip Area. - /// - /// - /// Changing the size of a frame (, , or ) - /// will change the size of the and trigger to update the layout of the - /// and its . - /// - /// - public Frame Border { get; private set; } - - /// - /// Gets or sets whether the view has a one row/col thick border. - /// - /// - /// - /// This is a helper for manipulating the view's . Setting this property to any value other than - /// is equivalent to setting 's - /// to `1` and to the value. - /// - /// - /// Setting this property to is equivalent to setting 's - /// to `0` and to . - /// - /// - /// For more advanced customization of the view's border, manipulate see directly. - /// - /// - public LineStyle BorderStyle { - get { - return Border?.BorderStyle ?? LineStyle.None; - } - set { - if (Border == null) { - throw new InvalidOperationException ("Border is null; this is likely a bug."); - } - if (value != LineStyle.None) { - Border.Thickness = new Thickness (1); - } else { - Border.Thickness = new Thickness (0); - } - Border.BorderStyle = value; - LayoutFrames (); - SetNeedsLayout (); - } - } - - /// - /// The frame (specified as a ) inside of the view that offsets the from the . - /// - /// - /// - /// The frames (, , and ) are not part of the View's content - /// and are not clipped by the View's Clip Area. - /// - /// - /// Changing the size of a frame (, , or ) - /// will change the size of the and trigger to update the layout of the - /// and its . - /// - /// - public Frame Padding { get; private set; } - - /// - /// Helper to get the total thickness of the , , and . - /// - /// A thickness that describes the sum of the Frames' thicknesses. - public Thickness GetFramesThickness () - { - var left = Margin.Thickness.Left + Border.Thickness.Left + Padding.Thickness.Left; - var top = Margin.Thickness.Top + Border.Thickness.Top + Padding.Thickness.Top; - var right = Margin.Thickness.Right + Border.Thickness.Right + Padding.Thickness.Right; - var bottom = Margin.Thickness.Bottom + Border.Thickness.Bottom + Padding.Thickness.Bottom; - return new Thickness (left, top, right, bottom); - } - - /// - /// Helper to get the X and Y offset of the Bounds from the Frame. This is the sum of the Left and Top properties of - /// , and . - /// - public Point GetBoundsOffset () => new Point (Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0); - - /// - /// Creates the view's objects. This internal method is overridden by Frame to do nothing - /// to prevent recursion during View construction. - /// - internal virtual void CreateFrames () - { - void ThicknessChangedHandler (object sender, EventArgs e) - { - LayoutFrames (); - SetNeedsLayout (); - SetNeedsDisplay (); - } - - if (Margin != null) { - Margin.ThicknessChanged -= ThicknessChangedHandler; - Margin.Dispose (); - } - Margin = new Frame () { Id = "Margin", Thickness = new Thickness (0) }; - Margin.ThicknessChanged += ThicknessChangedHandler; - Margin.Parent = this; - - if (Border != null) { - Border.ThicknessChanged -= ThicknessChangedHandler; - Border.Dispose (); - } - Border = new Frame () { Id = "Border", Thickness = new Thickness (0) }; - Border.ThicknessChanged += ThicknessChangedHandler; - Border.Parent = this; - - // TODO: Create View.AddAdornment - - if (Padding != null) { - Padding.ThicknessChanged -= ThicknessChangedHandler; - Padding.Dispose (); - } - Padding = new Frame () { Id = "Padding", Thickness = new Thickness (0) }; - Padding.ThicknessChanged += ThicknessChangedHandler; - Padding.Parent = this; - } - - LayoutStyle _layoutStyle; - - /// - /// Controls how the View's is computed during . If the style is set to - /// , - /// LayoutSubviews does not change the . If the style is - /// the is updated using - /// the , , , and properties. - /// - /// The layout style. - public LayoutStyle LayoutStyle { - get => _layoutStyle; - set { - _layoutStyle = value; - SetNeedsLayout (); - } - } - - /// - /// The view's content area. - /// - /// SubViews are positioned relative to Bounds. - /// - /// - /// Drawing is clipped to Bounds ( clips drawing to Bounds.Size). - /// - /// - /// Mouse events are reported relative to Bounds. - /// - /// - /// The view's content area. - /// - /// - /// The of Bounds is always (0, 0). To obtain the offset of the Bounds from the Frame use - /// . - /// - /// - /// When using , Bounds is not valid until after the view has been initialized (after has been called and - /// has fired). Accessing this property before the view is initialized is considered an error./> - /// - /// - public virtual Rect Bounds { - get { -#if DEBUG - if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { - Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug. View: {this}"); - Debug.WriteLine ($"The Frame is set before the View has been initialized. So it isn't a bug.Is by design."); - } -#endif // DEBUG - //var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size); - var frameRelativeBounds = FrameGetInsideBounds (); - return new Rect (default, frameRelativeBounds.Size); - } - set { - // BUGBUG: Margin etc.. can be null (if typeof(Frame)) - Frame = new Rect (Frame.Location, - new Size ( - value.Size.Width + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal, - value.Size.Height + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical - ) - ); - } - } - - private Rect FrameGetInsideBounds () - { - if (Margin == null || Border == null || Padding == null) { - return new Rect (default, Frame.Size); - } - var width = Math.Max (0, Frame.Size.Width - Margin.Thickness.Horizontal - Border.Thickness.Horizontal - Padding.Thickness.Horizontal); - var height = Math.Max (0, Frame.Size.Height - Margin.Thickness.Vertical - Border.Thickness.Vertical - Padding.Thickness.Vertical); - return new Rect (Point.Empty, new Size (width, height)); - } - - // Diagnostics to highlight when X or Y is read before the view has been initialized - Pos VerifyIsInitialized (Pos pos) - { -#if DEBUG - if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) { - Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; position is indeterminate {pos}. This is likely a bug."); - } -#endif // DEBUG - return pos; - } - - // Diagnostics to highlight when Width or Height is read before the view has been initialized - Dim VerifyIsInitialized (Dim dim) - { -#if DEBUG - if (LayoutStyle == LayoutStyle.Computed && (!IsInitialized)) { - Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; dimension is indeterminate: {dim}. This is likely a bug."); - } -#endif // DEBUG - return dim; - } - - Pos _x, _y; - - /// - /// Gets or sets the X position for the view (the column). Only used if the is . - /// - /// The X Position. - /// - /// If is changing this property has no effect and its value is indeterminate. - /// - public Pos X { - get => VerifyIsInitialized (_x); - set { - if (ForceValidatePosDim && !ValidatePosDim (_x, value)) { - throw new ArgumentException (); - } - - _x = value; - - OnResizeNeeded (); - } - } - - /// - /// Gets or sets the Y position for the view (the row). Only used if the is . - /// - /// The y position (line). - /// - /// If is changing this property has no effect and its value is indeterminate. - /// - public Pos Y { - get => VerifyIsInitialized (_y); - set { - if (ForceValidatePosDim && !ValidatePosDim (_y, value)) { - throw new ArgumentException (); - } - - _y = value; - - OnResizeNeeded (); - } - } - Dim _width, _height; - - /// - /// Gets or sets the width of the view. Only used the is . - /// - /// The width. - /// - /// If is changing this property has no effect and its value is indeterminate. - /// - public Dim Width { - get => VerifyIsInitialized (_width); - set { - if (ForceValidatePosDim && !ValidatePosDim (_width, value)) { - throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Width)); - } - - _width = value; - - if (ForceValidatePosDim) { - var isValidNewAutSize = AutoSize && IsValidAutoSizeWidth (_width); - - if (IsAdded && AutoSize && !isValidNewAutSize) { - throw new InvalidOperationException ("Must set AutoSize to false before set the Width."); - } - } - OnResizeNeeded (); - } - } - - /// - /// Gets or sets the height of the view. Only used the is . - /// - /// The height. - /// If is changing this property has no effect and its value is indeterminate. - public Dim Height { - get => VerifyIsInitialized (_height); - set { - if (ForceValidatePosDim && !ValidatePosDim (_height, value)) { - throw new ArgumentException ("ForceValidatePosDim is enabled", nameof (Height)); - } - - _height = value; - - if (ForceValidatePosDim) { - var isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height); - - if (IsAdded && AutoSize && !isValidNewAutSize) { - throw new InvalidOperationException ("Must set AutoSize to false before set the Height."); - } - } - OnResizeNeeded (); - } - } - - /// - /// Forces validation with layout - /// to avoid breaking the and settings. - /// - public bool ForceValidatePosDim { get; set; } - - bool ValidatePosDim (object oldValue, object newValue) - { - if (!IsInitialized || _layoutStyle == LayoutStyle.Absolute || oldValue == null || oldValue.GetType () == newValue.GetType () || this is Toplevel) { - return true; - } - if (_layoutStyle == LayoutStyle.Computed) { - if (oldValue.GetType () != newValue.GetType () && !(newValue is Pos.PosAbsolute || newValue is Dim.DimAbsolute)) { - return true; - } - } - return false; - } - - // BUGBUG: This API is broken - should not assume Frame.Height == Bounds.Height - // BUGBUG: this function does not belong in ViewLayout.cs - it should be in ViewText.cs - /// - /// Gets the minimum dimensions required to fit the View's , factoring in . - /// - /// The minimum dimensions required. - /// if the dimensions fit within the View's , otherwise. - /// - /// Always returns if is or - /// if (Horizontal) or (Vertical) are not not set or zero. - /// Does not take into account word wrapping. - /// - bool GetMinimumBoundsForFrame (out Size size) - { - if (!IsInitialized) { - size = new Size (0, 0); - return false; - } - size = Bounds.Size; - - if (!AutoSize && !string.IsNullOrEmpty (TextFormatter.Text)) { - switch (TextFormatter.IsVerticalDirection (TextDirection)) { - case true: - var colWidth = TextFormatter.GetSumMaxCharWidth (new List { TextFormatter.Text }, 0, 1); - // TODO: v2 - This uses frame.Width; it should only use Bounds - if (_frame.Width < colWidth && - (Width == null || - (Bounds.Width >= 0 && - Width is Dim.DimAbsolute && - Width.Anchor (0) >= 0 && - Width.Anchor (0) < colWidth))) { - size = new Size (colWidth, Bounds.Height); - return true; - } - break; - default: - if (_frame.Height < 1 && - (Height == null || - (Height is Dim.DimAbsolute && - Height.Anchor (0) == 0))) { - size = new Size (Bounds.Width, 1); - return true; - } - break; - } - } - return false; - } - - // BUGBUG: this function does not belong in ViewLayout.cs - it should be in ViewText.cs - /// - /// Sets the size of the View to the minimum width or height required to fit (see . - /// - /// if the size was changed, if - /// will not fit. - bool SetBoundsToFitFrame () - { - if (GetMinimumBoundsForFrame (out Size size)) { - _frame = new Rect (_frame.Location, size); - return true; - } - return false; - } - - /// - /// Called whenever the view needs to be resized. Sets and - /// triggers a call. - /// - /// - /// Can be overridden if the view resize behavior is different than the default. - /// - protected virtual void OnResizeNeeded () - { - var actX = _x is Pos.PosAbsolute ? _x.Anchor (0) : _frame.X; - var actY = _y is Pos.PosAbsolute ? _y.Anchor (0) : _frame.Y; - - if (AutoSize) { - //if (TextAlignment == TextAlignment.Justified) { - // throw new InvalidOperationException ("TextAlignment.Justified cannot be used with AutoSize"); - //} - var s = GetAutoSize (); - var w = _width is Dim.DimAbsolute && _width.Anchor (0) > s.Width ? _width.Anchor (0) : s.Width; - var h = _height is Dim.DimAbsolute && _height.Anchor (0) > s.Height ? _height.Anchor (0) : s.Height; - _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame! - } else { - var w = _width is Dim.DimAbsolute ? _width.Anchor (0) : _frame.Width; - var h = _height is Dim.DimAbsolute ? _height.Anchor (0) : _frame.Height; - // BUGBUG: v2 - ? - If layoutstyle is absolute, this overwrites the current frame h/w with 0. Hmmm... - // This is needed for DimAbsolute values by setting the frame before LayoutSubViews. - _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame! - } - //// BUGBUG: I think these calls are redundant or should be moved into just the AutoSize case - if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) { - SetBoundsToFitFrame (); - LayoutFrames (); - TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); - SetNeedsLayout (); - SetNeedsDisplay (); - } - } - - internal bool LayoutNeeded { get; private set; } = true; - - internal void SetNeedsLayout () - { - if (LayoutNeeded) { - return; - } - LayoutNeeded = true; - foreach (var view in Subviews) { - view.SetNeedsLayout (); - } - TextFormatter.NeedsFormat = true; - SuperView?.SetNeedsLayout (); - } - - /// - /// Indicates that the view does not need to be laid out. - /// - protected void ClearLayoutNeeded () - { - LayoutNeeded = false; - } - - /// - /// Converts a screen-relative coordinate to a Frame-relative coordinate. Frame-relative means - /// relative to the View's 's . - /// - /// The coordinate relative to the 's . - /// Screen-relative column. - /// Screen-relative row. - public Point ScreenToFrame (int x, int y) - { - Point superViewBoundsOffset = SuperView?.GetBoundsOffset () ?? Point.Empty; - var ret = new Point (x - Frame.X - superViewBoundsOffset.X, y - Frame.Y - superViewBoundsOffset.Y); - if (SuperView != null) { - var superFrame = SuperView.ScreenToFrame (x - superViewBoundsOffset.X, y - superViewBoundsOffset.Y); - ret = new Point (superFrame.X - Frame.X, superFrame.Y - Frame.Y); - } - return ret; - } - - /// - /// Converts a screen-relative coordinate to a bounds-relative coordinate. - /// - /// The coordinate relative to this view's . - /// Screen-relative column. - /// Screen-relative row. - public Point ScreenToBounds (int x, int y) - { - var screen = ScreenToFrame (x, y); - var boundsOffset = GetBoundsOffset (); - return new Point (screen.X - boundsOffset.X, screen.Y - boundsOffset.Y); - } - - /// - /// Converts a -relative coordinate to a screen-relative coordinate. The output is optionally clamped to the screen dimensions. - /// - /// -relative column. - /// -relative row. - /// Absolute column; screen-relative. - /// Absolute row; screen-relative. - /// If , and will be clamped to the - /// screen dimensions (will never be negative and will always be less than and - /// , respectively. - public virtual void BoundsToScreen (int x, int y, out int rx, out int ry, bool clamped = true) - { - var boundsOffset = GetBoundsOffset (); - rx = x + Frame.X + boundsOffset.X; - ry = y + Frame.Y + boundsOffset.Y; - - var super = SuperView; - while (super != null) { - boundsOffset = super.GetBoundsOffset (); - rx += super.Frame.X + boundsOffset.X; - ry += super.Frame.Y + boundsOffset.Y; - super = super.SuperView; - } - - // The following ensures that the cursor is always in the screen boundaries. - if (clamped) { - ry = Math.Min (ry, Driver.Rows - 1); - rx = Math.Min (rx, Driver.Cols - 1); - } - } - - /// - /// Converts a -relative region to a screen-relative region. - /// - public Rect BoundsToScreen (Rect region) - { - BoundsToScreen (region.X, region.Y, out var x, out var y, clamped: false); - return new Rect (x, y, region.Width, region.Height); - } - - /// - /// Gets the with a screen-relative location. - /// - /// The location and size of the view in screen-relative coordinates. - public virtual Rect FrameToScreen () - { - var ret = Frame; - var super = SuperView; - while (super != null) { - var boundsOffset = super.GetBoundsOffset (); - ret.X += super.Frame.X + boundsOffset.X; - ret.Y += super.Frame.Y + boundsOffset.Y; - super = super.SuperView; - } - return ret; - } - - /// - /// Sets the View's to the frame-relative coordinates if its container. The - /// container size and location are specified by and are relative to the - /// View's superview. - /// - /// The SuperView-relative rectangle describing View's container (nominally the - /// same as this.SuperView.Frame). - internal void SetRelativeLayout (Rect superviewFrame) - { - int newX, newW, newY, newH; - var autosize = Size.Empty; - - if (AutoSize) { - // Note this is global to this function and used as such within the local functions defined - // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function. - autosize = GetAutoSize (); - } - - // Returns the new dimension (width or height) and location (x or y) for the View given - // the superview's Frame.X or Frame.Y - // the superview's width or height - // the current Pos (View.X or View.Y) - // the current Dim (View.Width or View.Height) - (int newLocation, int newDimension) GetNewLocationAndDimension (int superviewLocation, int superviewDimension, Pos pos, Dim dim, int autosizeDimension) - { - int newDimension, newLocation; - - switch (pos) { - case Pos.PosCenter: - if (dim == null) { - newDimension = AutoSize ? autosizeDimension : superviewDimension; - } else { - newDimension = dim.Anchor (superviewDimension); - newDimension = AutoSize && autosizeDimension > newDimension ? autosizeDimension : newDimension; - } - newLocation = pos.Anchor (superviewDimension - newDimension); - break; - - case Pos.PosCombine combine: - int left, right; - (left, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.left, dim, autosizeDimension); - (right, newDimension) = GetNewLocationAndDimension (superviewLocation, superviewDimension, combine.right, dim, autosizeDimension); - if (combine.add) { - newLocation = left + right; - } else { - newLocation = left - right; - } - newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); - break; - - case Pos.PosAbsolute: - case Pos.PosAnchorEnd: - case Pos.PosFactor: - case Pos.PosFunc: - case Pos.PosView: - default: - newLocation = pos?.Anchor (superviewDimension) ?? 0; - newDimension = Math.Max (CalculateNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); - break; - } - return (newLocation, newDimension); - } - - // Recursively calculates the new dimension (width or height) of the given Dim given: - // the current location (x or y) - // the current dimension (width or height) - int CalculateNewDimension (Dim d, int location, int dimension, int autosize) - { - int newDimension; - switch (d) { - case null: - newDimension = AutoSize ? autosize : dimension; - break; - case Dim.DimCombine combine: - int leftNewDim = CalculateNewDimension (combine.left, location, dimension, autosize); - int rightNewDim = CalculateNewDimension (combine.right, location, dimension, autosize); - if (combine.add) { - newDimension = leftNewDim + rightNewDim; - } else { - newDimension = leftNewDim - rightNewDim; - } - newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; - break; - - case Dim.DimFactor factor when !factor.IsFromRemaining (): - newDimension = d.Anchor (dimension); - newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; - break; - - case Dim.DimFill: - default: - newDimension = Math.Max (d.Anchor (dimension - location), 0); - newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; - break; - } - - return newDimension; - } - - // horizontal - (newX, newW) = GetNewLocationAndDimension (superviewFrame.X, superviewFrame.Width, _x, _width, autosize.Width); - - // vertical - (newY, newH) = GetNewLocationAndDimension (superviewFrame.Y, superviewFrame.Height, _y, _height, autosize.Height); - - var r = new Rect (newX, newY, newW, newH); - if (Frame != r) { - Frame = r; - // BUGBUG: Why is this AFTER setting Frame? Seems duplicative. - if (!SetBoundsToFitFrame ()) { - TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); - } - } - } - - /// - /// Fired after the View's method has completed. - /// - /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. - /// - public event EventHandler LayoutStarted; - - /// - /// Raises the event. Called from before any subviews have been laid out. - /// - internal virtual void OnLayoutStarted (LayoutEventArgs args) - { - LayoutStarted?.Invoke (this, args); - } - - /// - /// Fired after the View's method has completed. - /// - /// - /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. - /// - public event EventHandler LayoutComplete; - - /// - /// Event called only once when the is being initialized for the first time. - /// Allows configurations and assignments to be performed before the being shown. - /// This derived from to allow notify all the views that are being initialized. - /// - public event EventHandler Initialized; - - /// - /// Raises the event. Called from before all sub-views have been laid out. - /// - internal virtual void OnLayoutComplete (LayoutEventArgs args) - { - LayoutComplete?.Invoke (this, args); - } - - internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) - { - switch (pos) { - case Pos.PosView pv: - // See #2461 - //if (!from.InternalSubviews.Contains (pv.Target)) { - // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}"); - //} - if (pv.Target != this) { - nEdges.Add ((pv.Target, from)); - } - return; - case Pos.PosCombine pc: - CollectPos (pc.left, from, ref nNodes, ref nEdges); - CollectPos (pc.right, from, ref nNodes, ref nEdges); - break; - } - } - - internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) - { - switch (dim) { - case Dim.DimView dv: - // See #2461 - //if (!from.InternalSubviews.Contains (dv.Target)) { - // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}"); - //} - if (dv.Target != this) { - nEdges.Add ((dv.Target, from)); - } - return; - case Dim.DimCombine dc: - CollectDim (dc.left, from, ref nNodes, ref nEdges); - CollectDim (dc.right, from, ref nNodes, ref nEdges); - break; - } - } - - internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) - { - foreach (var v in from.InternalSubviews) { - nNodes.Add (v); - if (v._layoutStyle != LayoutStyle.Computed) { - continue; - } - CollectPos (v.X, v, ref nNodes, ref nEdges); - CollectPos (v.Y, v, ref nNodes, ref nEdges); - CollectDim (v.Width, v, ref nNodes, ref nEdges); - CollectDim (v.Height, v, ref nNodes, ref nEdges); - } - } - - // https://en.wikipedia.org/wiki/Topological_sorting - internal static List TopologicalSort (View superView, IEnumerable nodes, ICollection<(View From, View To)> edges) - { - var result = new List (); - - // Set of all nodes with no incoming edges - var noEdgeNodes = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n)))); - - while (noEdgeNodes.Any ()) { - // remove a node n from S - var n = noEdgeNodes.First (); - noEdgeNodes.Remove (n); - - // add n to tail of L - if (n != superView) - result.Add (n); - - // for each node m with an edge e from n to m do - foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) { - var m = e.To; - - // remove edge e from the graph - edges.Remove (e); - - // if m has no other incoming edges then - if (edges.All (me => !me.To.Equals (m)) && m != superView) { - // insert m into S - noEdgeNodes.Add (m); - } - } - } - - if (edges.Any ()) { - foreach ((var from, var to) in edges) { - if (from == to) { - // if not yet added to the result, add it and remove from edge - if (result.Find (v => v == from) == null) { - result.Add (from); - } - edges.Remove ((from, to)); - } else if (from.SuperView == to.SuperView) { - // if 'from' is not yet added to the result, add it - if (result.Find (v => v == from) == null) { - result.Add (from); - } - // if 'to' is not yet added to the result, add it - if (result.Find (v => v == to) == null) { - result.Add (to); - } - // remove from edge - edges.Remove ((from, to)); - } else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to)) { - if (ReferenceEquals (from.SuperView, to)) { - throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{to}\" references a SubView (\"{from}\")."); - } else { - throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{from}\" linked with \"{to}\" was not found. Did you forget to add it to {superView}?"); - } - } - } - } - // return L (a topologically sorted order) - return result; - } // TopologicalSort - - /// - /// Overriden by to do nothing, as the does not have frames. - /// - internal virtual void LayoutFrames () - { - if (Margin == null) return; // CreateFrames() has not been called yet - - if (Margin.Frame.Size != Frame.Size) { - Margin._frame = new Rect (Point.Empty, Frame.Size); - Margin.X = 0; - Margin.Y = 0; - Margin.Width = Frame.Size.Width; - Margin.Height = Frame.Size.Height; - Margin.SetNeedsLayout (); - Margin.LayoutSubviews (); - Margin.SetNeedsDisplay (); - } - - var border = Margin.Thickness.GetInside (Margin.Frame); - if (border != Border.Frame) { - Border._frame = new Rect (new Point (border.Location.X, border.Location.Y), border.Size); - Border.X = border.Location.X; - Border.Y = border.Location.Y; - Border.Width = border.Size.Width; - Border.Height = border.Size.Height; - Border.SetNeedsLayout (); - Border.LayoutSubviews (); - Border.SetNeedsDisplay (); - } - - var padding = Border.Thickness.GetInside (Border.Frame); - if (padding != Padding.Frame) { - Padding._frame = new Rect (new Point (padding.Location.X, padding.Location.Y), padding.Size); - Padding.X = padding.Location.X; - Padding.Y = padding.Location.Y; - Padding.Width = padding.Size.Width; - Padding.Height = padding.Size.Height; - Padding.SetNeedsLayout (); - Padding.LayoutSubviews (); - Padding.SetNeedsDisplay (); - } - } - - /// - /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in - /// response to the container view or terminal resizing. - /// - /// - /// Raises the event) before it returns. - /// - public virtual void LayoutSubviews () - { - if (!LayoutNeeded) { - return; - } - - LayoutFrames (); - - var oldBounds = Bounds; - OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds }); - - TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); - - // Sort out the dependencies of the X, Y, Width, Height properties - var nodes = new HashSet (); - var edges = new HashSet<(View, View)> (); - CollectAll (this, ref nodes, ref edges); - var ordered = View.TopologicalSort (SuperView, nodes, edges); - foreach (var v in ordered) { - LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size)); - } - - // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case. - // Use LayoutSubview with the Frame of the 'from' - if (SuperView != null && GetTopSuperView () != null && LayoutNeeded && edges.Count > 0) { - foreach ((var from, var to) in edges) { - LayoutSubview (to, from.Frame); - } - } - - LayoutNeeded = false; - - OnLayoutComplete (new LayoutEventArgs () { OldBounds = oldBounds }); - } - - private void LayoutSubview (View v, Rect contentArea) - { - if (v.LayoutStyle == LayoutStyle.Computed) { - v.SetRelativeLayout (contentArea); - } - - v.LayoutSubviews (); - v.LayoutNeeded = false; - } - - bool _autoSize; - - /// - /// Gets or sets a flag that determines whether the View will be automatically resized to fit the - /// within - /// - /// The default is . Set to to turn on AutoSize. If then - /// and will be used if can fit; - /// if won't fit the view will be resized as needed. - /// - /// - /// In addition, if is the new values of and - /// must be of the same types of the existing one to avoid breaking the settings. - /// - /// - public virtual bool AutoSize { - get => _autoSize; - set { - var v = ResizeView (value); - TextFormatter.AutoSize = v; - if (_autoSize != v) { - _autoSize = v; - TextFormatter.NeedsFormat = true; - UpdateTextFormatterText (); - OnResizeNeeded (); - } - } - } - - bool ResizeView (bool autoSize) - { - if (!autoSize) { - return false; - } - - var boundsChanged = true; - var newFrameSize = GetAutoSize (); - if (IsInitialized && newFrameSize != Frame.Size) { - if (ForceValidatePosDim) { - // BUGBUG: This ain't right, obviously. We need to figure out how to handle this. - boundsChanged = ResizeBoundsToFit (newFrameSize); - } else { - Height = newFrameSize.Height; - Width = newFrameSize.Width; - } - } - // BUGBUG: This call may be redundant - TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); - return boundsChanged; - } - - /// - /// Resizes the View to fit the specified size. Factors in the HotKey. - /// - /// - /// whether the Bounds was changed or not - bool ResizeBoundsToFit (Size size) - { - var boundsChanged = false; - var canSizeW = TrySetWidth (size.Width - GetHotKeySpecifierLength (), out var rW); - var canSizeH = TrySetHeight (size.Height - GetHotKeySpecifierLength (false), out var rH); - if (canSizeW) { - boundsChanged = true; - _width = rW; - } - if (canSizeH) { - boundsChanged = true; - _height = rH; - } - if (boundsChanged) { - Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height); - } - - return boundsChanged; - } - - /// - /// Gets the Frame dimensions required to fit within using the text specified by the - /// property and accounting for any characters. - /// - /// The of the view required to fit the text. - public Size GetAutoSize () - { - int x = 0; - int y = 0; - if (IsInitialized) { - x = Bounds.X; - y = Bounds.Y; - } - var rect = TextFormatter.CalcRect (x, y,TextFormatter.Text, TextFormatter.Direction); - var newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal; - var newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical; - return new Size (newWidth, newHeight); - } - - bool IsValidAutoSize (out Size autoSize) - { - var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); - autoSize = new Size (rect.Size.Width - GetHotKeySpecifierLength (), - rect.Size.Height - GetHotKeySpecifierLength (false)); - return !(ForceValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute)) - || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength () - || _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false)); - } - - bool IsValidAutoSizeWidth (Dim width) - { - var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); - var dimValue = width.Anchor (0); - return !(ForceValidatePosDim && (!(width is Dim.DimAbsolute)) || dimValue != rect.Size.Width - - GetHotKeySpecifierLength ()); - } - - bool IsValidAutoSizeHeight (Dim height) - { - var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); - var dimValue = height.Anchor (0); - return !(ForceValidatePosDim && (!(height is Dim.DimAbsolute)) || dimValue != rect.Size.Height - - GetHotKeySpecifierLength (false)); - } - - /// - /// Determines if the View's can be set to a new value. - /// - /// - /// Contains the width that would result if were set to "/> - /// if the View's can be changed to the specified value. False otherwise. - internal bool TrySetWidth (int desiredWidth, out int resultWidth) - { - var w = desiredWidth; - bool canSetWidth; - switch (Width) { - case Dim.DimCombine _: - case Dim.DimView _: - case Dim.DimFill _: - // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored. - w = Width.Anchor (w); - canSetWidth = !ForceValidatePosDim; - break; - case Dim.DimFactor factor: - // Tries to get the SuperView Width otherwise the view Width. - var sw = SuperView != null ? SuperView.Frame.Width : w; - if (factor.IsFromRemaining ()) { - sw -= Frame.X; - } - w = Width.Anchor (sw); - canSetWidth = !ForceValidatePosDim; - break; - default: - canSetWidth = true; - break; - } - resultWidth = w; - - return canSetWidth; - } - - /// - /// Determines if the View's can be set to a new value. - /// - /// - /// Contains the width that would result if were set to "/> - /// if the View's can be changed to the specified value. False otherwise. - internal bool TrySetHeight (int desiredHeight, out int resultHeight) - { - var h = desiredHeight; - bool canSetHeight; - switch (Height) { - case Dim.DimCombine _: - case Dim.DimView _: - case Dim.DimFill _: - // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored. - h = Height.Anchor (h); - canSetHeight = !ForceValidatePosDim; - break; - case Dim.DimFactor factor: - // Tries to get the SuperView height otherwise the view height. - var sh = SuperView != null ? SuperView.Frame.Height : h; - if (factor.IsFromRemaining ()) { - sh -= Frame.Y; - } - h = Height.Anchor (sh); - canSetHeight = !ForceValidatePosDim; - break; - default: - canSetHeight = true; - break; - } - resultHeight = h; - - return canSetHeight; - } - - /// - /// Finds which view that belong to the superview at the provided location. - /// - /// The superview where to look for. - /// The column location in the superview. - /// The row location in the superview. - /// The found view screen relative column location. - /// The found view screen relative row location. - /// - /// The view that was found at the and coordinates. - /// if no view was found. - /// - public static View FindDeepestView (View start, int x, int y, out int resx, out int resy) - { - resy = resx = 0; - if (start == null || !start.Frame.Contains (x, y)) { - return null; - } - - var startFrame = start.Frame; - if (start.InternalSubviews != null) { - int count = start.InternalSubviews.Count; - if (count > 0) { - var boundsOffset = start.GetBoundsOffset (); - var rx = x - (startFrame.X + boundsOffset.X); - var ry = y - (startFrame.Y + boundsOffset.Y); - for (int i = count - 1; i >= 0; i--) { - View v = start.InternalSubviews [i]; - if (v.Visible && v.Frame.Contains (rx, ry)) { - var deep = FindDeepestView (v, rx, ry, out resx, out resy); - if (deep == null) - return v; - return deep; - } - } - } - } - resx = x - startFrame.X; - resy = y - startFrame.Y; - return start; - } - } -} diff --git a/Terminal.Gui/View/Layout/PosDim.cs b/Terminal.Gui/View/Layout/PosDim.cs index 4fb0cb0c1..19c0859e8 100644 --- a/Terminal.Gui/View/Layout/PosDim.cs +++ b/Terminal.Gui/View/Layout/PosDim.cs @@ -1,327 +1,346 @@ -// -// PosDim.cs: Pos and Dim objects for view dimensions. -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// +using System; +using static Terminal.Gui.Dim; + +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 { + internal virtual int Anchor (int width) => 0; -using System; -namespace Terminal.Gui { /// - /// Describes the position of a which can be an absolute value, a percentage, centered, or - /// relative to the ending dimension. Integer values are implicitly convertible to - /// an absolute . These objects are created using the static methods Percent, - /// AnchorEnd, and Center. The objects can be combined with the addition and - /// subtraction operators. + /// Creates a object that computes the position by executing the provided function. The function will be called every time the position is needed. /// - /// - /// - /// Use the objects on the X or Y properties of a view to control the position. - /// - /// - /// These can be used to set the absolute position, when merely assigning an - /// integer value (via the implicit integer to conversion), and they can be combined - /// to produce more useful layouts, like: Pos.Center - 3, which would shift the position - /// of the 3 characters to the left after centering for example. - /// - /// - /// It is possible to reference coordinates of another view by using the methods - /// Left(View), Right(View), Bottom(View), Top(View). The X(View) and Y(View) are - /// aliases to Left(View) and Top(View) respectively. - /// - /// - public class Pos { - internal virtual int Anchor (int width) + /// The function to be executed. + /// The returned from the function. + public static Pos Function (Func function) => new PosFunc (function); + + internal class PosFactor : Pos { + readonly float _factor; + + public PosFactor (float n) => _factor = n; + + internal override int Anchor (int width) => (int)(width * _factor); + + public override string ToString () => $"Factor({_factor})"; + + public override int GetHashCode () => _factor.GetHashCode (); + + public override bool Equals (object other) => other is PosFactor f && f._factor == _factor; + } + + // Helper class to provide dynamic value by the execution of a function that returns an integer. + internal class PosFunc : Pos { + readonly Func _function; + + public PosFunc (Func n) => _function = n; + + internal override int Anchor (int width) => _function (); + + public override string ToString () => $"PosFunc({_function ()})"; + + public override int GetHashCode () => _function.GetHashCode (); + + public override bool Equals (object other) => other is PosFunc f && f._function () == _function (); + } + + /// + /// Creates a percentage object + /// + /// The percent object. + /// A value between 0 and 100 representing the percentage. + /// + /// This creates a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Pos Percent (float n) + { + if (n is < 0 or > 100) { + throw new ArgumentException ("Percent value must be between 0 and 100"); + } + + return new PosFactor (n / 100); + } + + /// + /// Creates a object that is anchored to the end (right side or bottom) of the dimension, + /// useful to flush the layout from the right or bottom. + /// + /// The object anchored to the end (the bottom or the right side). + /// The view will be shifted left or up by the amount specified. + /// + /// This sample shows how align a to the bottom-right of a . + /// + /// // See Issue #502 + /// anchorButton.X = Pos.AnchorEnd () - (Pos.Right (anchorButton) - Pos.Left (anchorButton)); + /// anchorButton.Y = Pos.AnchorEnd (1); + /// + /// + public static Pos AnchorEnd (int offset = 0) + { + if (offset < 0) { + throw new ArgumentException (@"Must be positive", nameof(offset)); + } + + return new PosAnchorEnd (offset); + } + + internal class PosAnchorEnd : Pos { + readonly int _offset; + + public PosAnchorEnd (int offset) => _offset = offset; + + internal override int Anchor (int width) => width - _offset; + + public override string ToString () => $"AnchorEnd({_offset})"; + + public override int GetHashCode () => _offset.GetHashCode (); + + public override bool Equals (object other) => other is PosAnchorEnd anchorEnd && anchorEnd._offset == _offset; + } + + /// + /// Creates a object that can be used to center the . + /// + /// The center Pos. + /// + /// This creates a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Pos Center () => new PosCenter (); + + internal class PosAbsolute : Pos { + readonly int _n; + public PosAbsolute (int n) => _n = n; + + public override string ToString () => $"Absolute({_n})"; + + internal override int Anchor (int width) => _n; + + public override int GetHashCode () => _n.GetHashCode (); + + public override bool Equals (object other) => other is PosAbsolute abs && abs._n == _n; + } + + internal class PosCenter : Pos { + internal override int Anchor (int width) => width / 2; + + public override string ToString () => "Center"; + } + + /// + /// 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) => new PosAbsolute (n); + + internal class PosCombine : Pos { + internal Pos _left, _right; + internal bool _add; + + public PosCombine (bool add, Pos left, Pos right) { - return 0; + _left = left; + _right = right; + _add = add; } - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class PosFunc : Pos { - Func function; - - public PosFunc (Func n) - { - this.function = n; - } - - internal override int Anchor (int width) - { - return function (); - } - - public override string ToString () - { - return $"PosFunc({function ()})"; - } - - public override int GetHashCode () => function.GetHashCode (); - - public override bool Equals (object other) => other is PosFunc f && f.function () == function (); - } - - /// - /// Creates a "PosFunc" from the specified function. - /// - /// The function to be executed. - /// The returned from the function. - public static Pos Function (Func function) + internal override int Anchor (int width) { - return new PosFunc (function); + int la = _left.Anchor (width); + int ra = _right.Anchor (width); + if (_add) { + return la + ra; + } else { + return la - ra; + } } - internal class PosFactor : Pos { - float factor; + public override string ToString () => $"Combine({_left}{(_add ? '+' : '-')}{_right})"; + } - public PosFactor (float n) - { - this.factor = n; - } + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static implicit operator Pos (int n) => new PosAbsolute (n); - internal override int Anchor (int width) - { - return (int)(width * factor); - } - - public override string ToString () - { - return $"Factor({factor})"; - } - - public override int GetHashCode () => factor.GetHashCode (); - - public override bool Equals (object other) => other is PosFactor f && f.factor == factor; + /// + /// Adds a to a , yielding a new . + /// + /// The first to add. + /// The second to add. + /// The that is the sum of the values of left and right. + public static Pos operator + (Pos left, Pos right) + { + if (left is PosAbsolute && right is PosAbsolute) { + return new PosAbsolute (left.Anchor (0) + right.Anchor (0)); } + var newPos = new PosCombine (true, left, right); + SetPosCombine (left, newPos); + return newPos; + } - /// - /// Creates a percentage object - /// - /// The percent object. - /// A value between 0 and 100 representing the percentage. - /// - /// This creates a that is centered horizontally, is 50% of the way down, - /// is 30% the height, and is 80% the width of the it added to. - /// - /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), - /// }; - /// - /// - public static Pos Percent (float n) + /// + /// Subtracts a from a , yielding a new . + /// + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). + /// The that is the left minus right. + public static Pos operator - (Pos left, Pos right) + { + if (left is PosAbsolute && right is PosAbsolute) { + return new PosAbsolute (left.Anchor (0) - right.Anchor (0)); + } + var newPos = new PosCombine (false, left, right); + SetPosCombine (left, newPos); + return newPos; + } + + static void SetPosCombine (Pos left, PosCombine newPos) + { + var view = left as PosView; + if (view != null) { + view.Target.SetNeedsLayout (); + } + } + + internal class PosView : Pos { + public readonly View Target; + int side; + + public PosView (View view, int side) { - if (n < 0 || n > 100) - throw new ArgumentException ("Percent value must be between 0 and 100"); - - return new PosFactor (n / 100); + Target = view; + this.side = side; } - internal class PosAnchorEnd : Pos { - int n; - - public PosAnchorEnd (int n) - { - this.n = n; - } - - internal override int Anchor (int width) - { - return width - n; - } - - public override string ToString () - { - return $"AnchorEnd({n})"; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is PosAnchorEnd anchorEnd && anchorEnd.n == n; - } - - /// - /// Creates a object that is anchored to the end (right side or bottom) of the dimension, - /// useful to flush the layout from the right or bottom. - /// - /// The object anchored to the end (the bottom or the right side). - /// Optional margin to place to the right or below. - /// - /// This sample shows how align a to the bottom-right of a . - /// - /// // See Issue #502 - /// anchorButton.X = Pos.AnchorEnd () - (Pos.Right (anchorButton) - Pos.Left (anchorButton)); - /// anchorButton.Y = Pos.AnchorEnd (1); - /// - /// - public static Pos AnchorEnd (int margin = 0) + internal override int Anchor (int width) { - if (margin < 0) - throw new ArgumentException ("Margin must be positive"); - - return new PosAnchorEnd (margin); - } - - internal class PosCenter : Pos { - internal override int Anchor (int width) - { - return width / 2; - } - - public override string ToString () - { - return "Center"; + switch (side) { + case 0: return Target.Frame.X; + case 1: return Target.Frame.Y; + case 2: return Target.Frame.Right; + case 3: return Target.Frame.Bottom; + default: + return 0; } } - /// - /// Returns a object that can be used to center the - /// - /// The center Pos. - /// - /// This creates a that is centered horizontally, is 50% of the way down, - /// is 30% the height, and is 80% the width of the it added to. - /// - /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), - /// }; - /// - /// - public static Pos Center () - { - return new PosCenter (); - } - - internal class PosAbsolute : Pos { - int n; - public PosAbsolute (int n) { this.n = n; } - - public override string ToString () - { - return $"Absolute({n})"; - } - - internal override int Anchor (int width) - { - return n; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is PosAbsolute abs && abs.n == n; - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static implicit operator Pos (int n) - { - return new PosAbsolute (n); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static Pos At (int n) - { - return new PosAbsolute (n); - } - - internal class PosCombine : Pos { - internal Pos left, right; - internal bool add; - public PosCombine (bool add, Pos left, Pos right) - { - this.left = left; - this.right = right; - this.add = add; - } - - internal override int Anchor (int width) - { - var la = left.Anchor (width); - var ra = right.Anchor (width); - if (add) - return la + ra; - else - return la - ra; - } - - public override string ToString () - { - return $"Combine({left}{(add ? '+' : '-')}{right})"; - } - - } - - /// - /// Adds a to a , yielding a new . - /// - /// The first to add. - /// The second to add. - /// The that is the sum of the values of left and right. - public static Pos operator + (Pos left, Pos right) - { - if (left is PosAbsolute && right is PosAbsolute) { - return new PosAbsolute (left.Anchor (0) + right.Anchor (0)); - } - PosCombine newPos = new PosCombine (true, left, right); - SetPosCombine (left, newPos); - return newPos; - } - - /// - /// Subtracts a from a , yielding a new . - /// - /// The to subtract from (the minuend). - /// The to subtract (the subtrahend). - /// The that is the left minus right. - public static Pos operator - (Pos left, Pos right) - { - if (left is PosAbsolute && right is PosAbsolute) { - return new PosAbsolute (left.Anchor (0) - right.Anchor (0)); - } - PosCombine newPos = new PosCombine (false, left, right); - SetPosCombine (left, newPos); - return newPos; - } - - static void SetPosCombine (Pos left, PosCombine newPos) - { - var view = left as PosView; - if (view != null) { - view.Target.SetNeedsLayout (); - } - } - - internal class PosView : Pos { - public View Target; - int side; - public PosView (View view, int side) - { - Target = view; - this.side = side; - } - internal override int Anchor (int width) - { - switch (side) { - case 0: return Target.Frame.X; - case 1: return Target.Frame.Y; - case 2: return Target.Frame.Right; - case 3: return Target.Frame.Bottom; - default: - return 0; - } - } - public override string ToString () { string tside; @@ -336,365 +355,355 @@ namespace Terminal.Gui { return $"View(side={tside},target={Target.ToString ()})"; } - public override int GetHashCode () => Target.GetHashCode (); + public override int GetHashCode () => Target.GetHashCode (); - public override bool Equals (object other) => other is PosView abs && abs.Target == Target; - } + public override bool Equals (object other) => other is PosView abs && abs.Target == Target; + } /// - /// Returns a object tracks the Left (X) position of the specified . - /// + /// 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) => new PosView (view, 0); /// - /// Returns a object tracks the Left (X) position of the specified . + /// 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) => new PosView (view, 0); /// - /// Returns a object tracks the Top (Y) position of the specified . + /// 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) => new PosView (view, 1); /// - /// Returns a object tracks the Top (Y) position of the specified . + /// 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) => new PosView(view, 1); /// - /// Returns a object tracks the Right (X+Width) coordinate of the specified . + /// 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) => new PosView (view, 2); /// - /// Returns a object tracks the Bottom (Y+Height) coordinate of the specified + /// 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) => new PosView (view, 3); - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () => Anchor (0).GetHashCode (); + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode () => Anchor (0).GetHashCode (); - /// Determines whether the specified object is equal to the current object. - /// The object to compare with the current object. - /// - /// if the specified object is equal to the current object; otherwise, . - public override bool Equals (object other) => other is Pos abs && abs == this; + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + public override bool Equals (object other) => other is Pos abs && abs == this; +} + +/// +/// +/// 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 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, 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 { + internal virtual int Anchor (int width) => 0; + + /// + /// 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) => new DimFunc (function); + + // Helper class to provide dynamic value by the execution of a function that returns an integer. + internal class DimFunc : Dim { + readonly Func _function; + + public DimFunc (Func n) => _function = n; + + internal override int Anchor (int width) => _function (); + + public override string ToString () => $"DimFunc({_function ()})"; + + public override int GetHashCode () => _function.GetHashCode (); + + public override bool Equals (object other) => other is DimFunc f && f._function () == _function (); } /// - /// Dim properties of a to control the position. + /// Creates a percentage object that is a percentage of the width or height of the SuperView. /// - /// - /// - /// Use the Dim objects on the Width or Height properties of a to control the position. - /// - /// - /// These can be used to set the absolute position, when merely assigning an - /// integer value (via the implicit integer to Pos conversion), and they can be combined - /// to produce more useful layouts, like: Pos.Center - 3, which would shift the position - /// of the 3 characters to the left after centering for example. - /// - /// - public class Dim { - internal virtual int Anchor (int width) - { - return 0; + /// The percent object. + /// A value between 0 and 100 representing the percentage. + /// If true the Percent is computed based on the remaining space after the X/Y anchor positions. + /// If false is computed based on the whole original space. + /// + /// This initializes a that is centered horizontally, is 50% of the way down, + /// is 30% the height, and is 80% the width of the it added to. + /// + /// var textView = new TextView () { + /// X = Pos.Center (), + /// Y = Pos.Percent (50), + /// Width = Dim.Percent (80), + /// Height = Dim.Percent (30), + /// }; + /// + /// + public static Dim Percent (float n, bool r = false) + { + if (n is < 0 or > 100) { + throw new ArgumentException ("Percent value must be between 0 and 100"); } - // Helper class to provide dynamic value by the execution of a function that returns an integer. - internal class DimFunc : Dim { - Func function; - - public DimFunc (Func n) - { - this.function = n; - } - - internal override int Anchor (int width) - { - return function (); - } - - public override string ToString () - { - return $"DimFunc({function ()})"; - } - - public override int GetHashCode () => function.GetHashCode (); - - public override bool Equals (object other) => other is DimFunc f && f.function () == function (); - } - - /// - /// Creates a "DimFunc" from the specified function. - /// - /// The function to be executed. - /// The returned from the function. - public static Dim Function (Func function) - { - return new DimFunc (function); - } - - internal class DimFactor : Dim { - float factor; - bool remaining; - - public DimFactor (float n, bool r = false) - { - factor = n; - remaining = r; - } - - internal override int Anchor (int width) - { - return (int)(width * factor); - } - - public bool IsFromRemaining () - { - return remaining; - } - - public override string ToString () - { - return $"Factor({factor},{remaining})"; - } - - public override int GetHashCode () => factor.GetHashCode (); - - public override bool Equals (object other) => other is DimFactor f && f.factor == factor && f.remaining == remaining; - } - - /// - /// Creates a percentage object - /// - /// The percent object. - /// A value between 0 and 100 representing the percentage. - /// If true the Percent is computed based on the remaining space after the X/Y anchor positions. If false is computed based on the whole original space. - /// - /// This initializes a that is centered horizontally, is 50% of the way down, - /// is 30% the height, and is 80% the width of the it added to. - /// - /// var textView = new TextView () { - /// X = Pos.Center (), - /// Y = Pos.Percent (50), - /// Width = Dim.Percent (80), - /// Height = Dim.Percent (30), - /// }; - /// - /// - public static Dim Percent (float n, bool r = false) - { - if (n < 0 || n > 100) - throw new ArgumentException ("Percent value must be between 0 and 100"); - - return new DimFactor (n / 100, r); - } - - internal class DimAbsolute : Dim { - int n; - public DimAbsolute (int n) { this.n = n; } - - public override string ToString () - { - return $"Absolute({n})"; - } - - internal override int Anchor (int width) - { - return n; - } - - public override int GetHashCode () => n.GetHashCode (); - - public override bool Equals (object other) => other is DimAbsolute abs && abs.n == n; - } - - internal class DimFill : Dim { - int margin; - public DimFill (int margin) { this.margin = margin; } - - public override string ToString () - { - return $"Fill({margin})"; - } - - internal override int Anchor (int width) - { - return width - margin; - } - - public override int GetHashCode () => margin.GetHashCode (); - - public override bool Equals (object other) => other is DimFill fill && fill.margin == margin; - } - - /// - /// Initializes a new instance of the class that fills the dimension, but leaves the specified number of colums for a margin. - /// - /// The Fill dimension. - /// Margin to use. - public static Dim Fill (int margin = 0) - { - return new DimFill (margin); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the pos. - public static implicit operator Dim (int n) - { - return new DimAbsolute (n); - } - - /// - /// Creates an Absolute from the specified integer value. - /// - /// The Absolute . - /// The value to convert to the . - public static Dim Sized (int n) - { - return new DimAbsolute (n); - } - - internal class DimCombine : Dim { - internal Dim left, right; - internal bool add; - public DimCombine (bool add, Dim left, Dim right) - { - this.left = left; - this.right = right; - this.add = add; - } - - internal override int Anchor (int width) - { - var la = left.Anchor (width); - var ra = right.Anchor (width); - if (add) - return la + ra; - else - return la - ra; - } - - public override string ToString () - { - return $"Combine({left}{(add ? '+' : '-')}{right})"; - } - - } - - /// - /// Adds a to a , yielding a new . - /// - /// The first to add. - /// The second to add. - /// The that is the sum of the values of left and right. - public static Dim operator + (Dim left, Dim right) - { - if (left is DimAbsolute && right is DimAbsolute) { - return new DimAbsolute (left.Anchor (0) + right.Anchor (0)); - } - DimCombine newDim = new DimCombine (true, left, right); - SetDimCombine (left, newDim); - return newDim; - } - - /// - /// Subtracts a from a , yielding a new . - /// - /// The to subtract from (the minuend). - /// The to subtract (the subtrahend). - /// The that is the left minus right. - public static Dim operator - (Dim left, Dim right) - { - if (left is DimAbsolute && right is DimAbsolute) { - return new DimAbsolute (left.Anchor (0) - right.Anchor (0)); - } - DimCombine newDim = new DimCombine (false, left, right); - SetDimCombine (left, newDim); - return newDim; - } - - static void SetDimCombine (Dim left, DimCombine newPos) - { - var view = left as DimView; - if (view != null) { - view.Target.SetNeedsLayout (); - } - } - - internal class DimView : Dim { - public View Target; - int side; - public DimView (View view, int side) - { - Target = view; - this.side = side; - } - - internal override int Anchor (int width) - { - switch (side) { - case 0: return Target.Frame.Height; - case 1: return Target.Frame.Width; - default: - return 0; - } - } - - public override string ToString () - { - string tside; - switch (side) { - case 0: tside = "Height"; break; - case 1: tside = "Width"; break; - default: tside = "unknown"; break; - } - return $"View({tside},{Target.ToString ()})"; - } - - public override int GetHashCode () => Target.GetHashCode (); - - public override bool Equals (object other) => other is DimView abs && abs.Target == Target; - } - /// - /// Returns a object tracks the Width of the specified . - /// - /// The of the other . - /// The view that will be tracked. - public static Dim Width (View view) => new DimView (view, 1); - - /// - /// Returns a object tracks the Height of the specified . - /// - /// The of the other . - /// The view that will be tracked. - public static Dim Height (View view) => new DimView (view, 0); - - /// Serves as the default hash function. - /// A hash code for the current object. - public override int GetHashCode () => Anchor (0).GetHashCode (); - - /// Determines whether the specified object is equal to the current object. - /// The object to compare with the current object. - /// - /// if the specified object is equal to the current object; otherwise, . - public override bool Equals (object other) => other is Dim abs && abs == this; + return new DimFactor (n / 100, r); } -} + + internal class DimFactor : Dim { + readonly float _factor; + readonly bool _remaining; + + public DimFactor (float n, bool r = false) + { + _factor = n; + _remaining = r; + } + + internal override int Anchor (int width) => (int)(width * _factor); + + public bool IsFromRemaining () => _remaining; + + public override string ToString () => $"Factor({_factor},{_remaining})"; + + public override int GetHashCode () => _factor.GetHashCode (); + + public override bool Equals (object other) => other is DimFactor f && f._factor == _factor && f._remaining == _remaining; + } + + + internal class DimAbsolute : Dim { + readonly int _n; + public DimAbsolute (int n) => _n = n; + + public override string ToString () => $"Absolute({_n})"; + + internal override int Anchor (int width) => _n; + + public override int GetHashCode () => _n.GetHashCode (); + + public override bool Equals (object other) => other is DimAbsolute abs && abs._n == _n; + } + + internal class DimFill : Dim { + readonly int _margin; + public DimFill (int margin) => _margin = margin; + + public override string ToString () => $"Fill({_margin})"; + + internal override int Anchor (int width) => width - _margin; + + public override int GetHashCode () => _margin.GetHashCode (); + + public override bool Equals (object other) => other is DimFill fill && fill._margin == _margin; + } + + /// + /// 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) => new DimFill (margin); + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the pos. + public static implicit operator Dim (int n) => new DimAbsolute (n); + + /// + /// Creates an Absolute from the specified integer value. + /// + /// The Absolute . + /// The value to convert to the . + public static Dim Sized (int n) => new DimAbsolute (n); + + internal class DimCombine : Dim { + internal Dim _left, _right; + internal bool _add; + + public DimCombine (bool add, Dim left, Dim right) + { + _left = left; + _right = right; + _add = add; + } + + internal override int Anchor (int width) + { + int la = _left.Anchor (width); + int ra = _right.Anchor (width); + if (_add) { + return la + ra; + } else { + return la - ra; + } + } + + public override string ToString () => $"Combine({_left}{(_add ? '+' : '-')}{_right})"; + } + + /// + /// Adds a to a , yielding a new . + /// + /// The first to add. + /// The second to add. + /// The that is the sum of the values of left and right. + public static Dim operator + (Dim left, Dim right) + { + if (left is DimAbsolute && right is DimAbsolute) { + return new DimAbsolute (left.Anchor (0) + right.Anchor (0)); + } + var newDim = new DimCombine (true, left, right); + SetDimCombine (left, newDim); + return newDim; + } + + /// + /// Subtracts a from a , yielding a new . + /// + /// The to subtract from (the minuend). + /// The to subtract (the subtrahend). + /// The that is the left minus right. + public static Dim operator - (Dim left, Dim right) + { + if (left is DimAbsolute && right is DimAbsolute) { + return new DimAbsolute (left.Anchor (0) - right.Anchor (0)); + } + var newDim = new DimCombine (false, left, right); + SetDimCombine (left, newDim); + return newDim; + } + + // BUGBUG: newPos is never used. + static void SetDimCombine (Dim left, DimCombine newPos) => (left as DimView)?.Target.SetNeedsLayout (); + + internal class DimView : Dim { + public View Target { get; init; } + readonly int _side; + + public DimView (View view, int side) + { + Target = view; + _side = side; + } + + internal override int Anchor (int width) => _side switch { + 0 => Target.Frame.Height, + 1 => Target.Frame.Width, + _ => 0 + }; + + public override string ToString () + { + if (Target == null) { + throw new NullReferenceException (); + } + string tside = _side switch { + 0 => "Height", + 1 => "Width", + _ => "unknown" + }; + return $"View({tside},{Target})"; + } + + public override int GetHashCode () => Target.GetHashCode (); + + public override bool Equals (object other) => other is DimView abs && abs.Target == Target; + } + + /// + /// 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) => new DimView (view, 1); + + /// + /// 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) => new DimView (view, 0); + + /// Serves as the default hash function. + /// A hash code for the current object. + public override int GetHashCode () => Anchor (0).GetHashCode (); + + /// Determines whether the specified object is equal to the current object. + /// The object to compare with the current object. + /// + /// if the specified object is equal to the current object; otherwise, . + public override bool Equals (object other) => other is Dim abs && abs == this; +} \ No newline at end of file diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs new file mode 100644 index 000000000..0ca320c30 --- /dev/null +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -0,0 +1,1219 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics; +using System.Linq; + +namespace Terminal.Gui; + +/// +/// Determines the LayoutStyle for a , if Absolute, during , the +/// value from the will be used, if the value is Computed, then +/// will be updated from the X, Y objects and the Width and Height objects. +/// +public enum LayoutStyle { + /// + /// The position and size of the view are based . + /// + Absolute, + + /// + /// The position and size of the view will be computed based on + /// , , , and . will + /// provide the absolute computed values. + /// + Computed +} + +public partial class View { + // The frame for the object. Relative to the SuperView's Bounds. + Rect _frame; + + /// + /// Gets or sets the frame for the view. The frame is relative to the 's . + /// + /// The frame. + /// + /// + /// Change the Frame when using the layout style to move or resize views. + /// + /// + /// Altering the Frame of a view will trigger the redrawing of the + /// view as well as the redrawing of the affected regions of the . + /// + /// + public virtual Rect Frame { + get => _frame; + set { + _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0)); + if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) { + LayoutFrames (); + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); + SetNeedsLayout (); + SetNeedsDisplay (); + } + } + } + + /// + /// The frame (specified as a ) that separates a View from other SubViews of the same SuperView. + /// The margin offsets the from the . + /// + /// + /// + /// The frames (, , and ) are not part of the View's content + /// and are not clipped by the View's Clip Area. + /// + /// + /// Changing the size of a frame (, , or ) + /// will change the size of the and trigger to update the layout of the + /// and its . + /// + /// + public Frame Margin { get; private set; } + + /// + /// The frame (specified as a ) inside of the view that offsets the from the . + /// The Border provides the space for a visual border (drawn using line-drawing glyphs) and the Title. + /// The Border expands inward; in other words if `Border.Thickness.Top == 2` the border and + /// title will take up the first row and the second row will be filled with spaces. + /// + /// + /// + /// provides a simple helper for turning a simple border frame on or off. + /// + /// + /// The frames (, , and ) are not part of the View's content + /// and are not clipped by the View's Clip Area. + /// + /// + /// Changing the size of a frame (, , or ) + /// will change the size of the and trigger to update the layout of the + /// and its . + /// + /// + public Frame Border { get; private set; } + + /// + /// Gets or sets whether the view has a one row/col thick border. + /// + /// + /// + /// This is a helper for manipulating the view's . Setting this property to any value other than + /// is equivalent to setting 's + /// to `1` and to the value. + /// + /// + /// Setting this property to is equivalent to setting 's + /// to `0` and to . + /// + /// + /// For more advanced customization of the view's border, manipulate see directly. + /// + /// + public LineStyle BorderStyle { + get => Border?.BorderStyle ?? LineStyle.None; + set { + if (Border == null) { + throw new InvalidOperationException ("Border is null; this is likely a bug."); + } + if (value != LineStyle.None) { + Border.Thickness = new Thickness (1); + } else { + Border.Thickness = new Thickness (0); + } + Border.BorderStyle = value; + LayoutFrames (); + SetNeedsLayout (); + } + } + + /// + /// The frame (specified as a ) inside of the view that offsets the from the . + /// + /// + /// + /// The frames (, , and ) are not part of the View's content + /// and are not clipped by the View's Clip Area. + /// + /// + /// Changing the size of a frame (, , or ) + /// will change the size of the and trigger to update the layout of the + /// and its . + /// + /// + public Frame Padding { get; private set; } + + /// + /// Helper to get the total thickness of the , , and . + /// + /// A thickness that describes the sum of the Frames' thicknesses. + public Thickness GetFramesThickness () + { + int left = Margin.Thickness.Left + Border.Thickness.Left + Padding.Thickness.Left; + int top = Margin.Thickness.Top + Border.Thickness.Top + Padding.Thickness.Top; + int right = Margin.Thickness.Right + Border.Thickness.Right + Padding.Thickness.Right; + int bottom = Margin.Thickness.Bottom + Border.Thickness.Bottom + Padding.Thickness.Bottom; + return new Thickness (left, top, right, bottom); + } + + /// + /// Helper to get the X and Y offset of the Bounds from the Frame. This is the sum of the Left and Top properties of + /// , and . + /// + public Point GetBoundsOffset () => new Point (Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0); + + /// + /// Creates the view's objects. This internal method is overridden by Frame to do nothing + /// to prevent recursion during View construction. + /// + internal virtual void CreateFrames () + { + void ThicknessChangedHandler (object sender, EventArgs e) + { + LayoutFrames (); + SetNeedsLayout (); + SetNeedsDisplay (); + } + + if (Margin != null) { + Margin.ThicknessChanged -= ThicknessChangedHandler; + Margin.Dispose (); + } + Margin = new Frame () { Id = "Margin", Thickness = new Thickness (0) }; + Margin.ThicknessChanged += ThicknessChangedHandler; + Margin.Parent = this; + + if (Border != null) { + Border.ThicknessChanged -= ThicknessChangedHandler; + Border.Dispose (); + } + Border = new Frame () { Id = "Border", Thickness = new Thickness (0) }; + Border.ThicknessChanged += ThicknessChangedHandler; + Border.Parent = this; + + // TODO: Create View.AddAdornment + + if (Padding != null) { + Padding.ThicknessChanged -= ThicknessChangedHandler; + Padding.Dispose (); + } + Padding = new Frame () { Id = "Padding", Thickness = new Thickness (0) }; + Padding.ThicknessChanged += ThicknessChangedHandler; + Padding.Parent = this; + } + + LayoutStyle _layoutStyle; + + /// + /// Controls how the View's is computed during . If the style is set to + /// , + /// LayoutSubviews does not change the . If the style is + /// the is updated using + /// the , , , and properties. + /// + /// The layout style. + public LayoutStyle LayoutStyle { + get => _layoutStyle; + set { + _layoutStyle = value; + SetNeedsLayout (); + } + } + + /// + /// The view's content area. + /// + /// SubViews are positioned relative to Bounds. + /// + /// + /// Drawing is clipped to Bounds ( clips drawing to Bounds.Size). + /// + /// + /// Mouse events are reported relative to Bounds. + /// + /// + /// The view's content area. + /// + /// + /// The of Bounds is always (0, 0). To obtain the offset of the Bounds from the Frame use + /// . + /// + /// + /// When using , Bounds is not valid until after the view has been initialized (after has been called and + /// has fired). Accessing this property before the view is initialized is considered an error./> + /// + /// + public virtual Rect Bounds { + get { +#if DEBUG + if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { + Debug.WriteLine ($"WARNING: Bounds is being accessed before the View has been initialized. This is likely a bug in {this}"); + Debug.WriteLine ($"The Frame is set before the View has been initialized. So it isn't a bug.Is by design."); + } +#endif // DEBUG + //var frameRelativeBounds = Padding?.Thickness.GetInside (Padding.Frame) ?? new Rect (default, Frame.Size); + var frameRelativeBounds = FrameGetInsideBounds (); + return new Rect (default, frameRelativeBounds.Size); + } + set { + // BUGBUG: Margin etc.. can be null (if typeof(Frame)) + Frame = new Rect (Frame.Location, + new Size ( + value.Size.Width + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal, + value.Size.Height + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical + ) + ); + } + } + + Rect FrameGetInsideBounds () + { + if (Margin == null || Border == null || Padding == null) { + return new Rect (default, Frame.Size); + } + int width = Math.Max (0, Frame.Size.Width - Margin.Thickness.Horizontal - Border.Thickness.Horizontal - Padding.Thickness.Horizontal); + int height = Math.Max (0, Frame.Size.Height - Margin.Thickness.Vertical - Border.Thickness.Vertical - Padding.Thickness.Vertical); + return new Rect (Point.Empty, new Size (width, height)); + } + + Pos _x, _y; + + /// + /// Gets or sets the X position for the view (the column). Only used if the is . + /// + /// The X Position. + /// + /// + /// If is changing this property has no effect and its value is indeterminate. + /// + /// + /// is the same as Pos.Absolute(0). + /// + /// + public Pos X { + get => VerifyIsInitialized (_x); + set { + // BUGBUG: null is the sames a Pos.Absolute(0). Should we be explicit and set it? + + if (ValidatePosDim && LayoutStyle == LayoutStyle.Computed) { + CheckAbsolute (nameof (X), _x, value); + } + + _x = value; + + OnResizeNeeded (); + } + } + + + /// + /// Gets or sets the Y position for the view (the row). Only used if the is . + /// + /// The X Position. + /// + /// + /// If is changing this property has no effect and its value is indeterminate. + /// + /// + /// is the same as Pos.Absolute(0). + /// + /// + public Pos Y { + get => VerifyIsInitialized (_y); + set { + // BUGBUG: null is the sames a Pos.Absolute(0). Should we be explicit and set it? + + if (ValidatePosDim && LayoutStyle == LayoutStyle.Computed) { + CheckAbsolute (nameof (Y), _y, value); + } + + _y = value; + + OnResizeNeeded (); + } + } + Dim _width, _height; + + /// + /// Gets or sets the width of the view. Only used when is . + /// + /// The width. + /// + /// + /// If is changing this property + /// has no effect and its value is indeterminate. + /// + /// + /// is the same as Dim.Fill (0). + /// + /// + public Dim Width { + get => VerifyIsInitialized (_width); + set { + // BUGBUG: null is the sames a Dim.Fill(0). Should we be explicit and set it? + if (ValidatePosDim) { + if (LayoutStyle == LayoutStyle.Computed) { + CheckAbsolute (nameof (Width), _width, value); + } + } + + _width = value; + + if (ValidatePosDim) { + bool isValidNewAutSize = AutoSize && IsValidAutoSizeWidth (_width); + + if (IsAdded && AutoSize && !isValidNewAutSize) { + throw new InvalidOperationException ("Must set AutoSize to false before set the Width."); + } + } + OnResizeNeeded (); + } + } + + /// + /// Gets or sets the height of the view. Only used when is . + /// + /// The width. + /// + /// + /// If is changing this property + /// has no effect and its value is indeterminate. + /// + /// + /// is the same as Dim.Fill (0). + /// + /// + public Dim Height { + get => VerifyIsInitialized (_height); + set { + // BUGBUG: null is the sames a Dim.Fill(0). Should we be explicit and set it? + if (ValidatePosDim) { + if (LayoutStyle == LayoutStyle.Computed) { + CheckAbsolute (nameof (Height), _height, value); + } + } + + _height = value; + + if (ValidatePosDim) { + bool isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height); + + if (IsAdded && AutoSize && !isValidNewAutSize) { + throw new InvalidOperationException ("Must set AutoSize to false before setting the Height."); + } + } + OnResizeNeeded (); + } + } + + // Diagnostics to highlight when X or Y is read before the view has been initialized + Pos VerifyIsInitialized (Pos pos) + { +#if DEBUG + if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { + Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; position is indeterminate {pos}. This is likely a bug."); + } +#endif // DEBUG + return pos; + } + + // Diagnostics to highlight when Width or Height is read before the view has been initialized + Dim VerifyIsInitialized (Dim dim) + { +#if DEBUG + if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { + Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; dimension is indeterminate: {dim}. This is likely a bug."); + } +#endif // DEBUG + return dim; + } + + /// + /// Gets or sets whether validation of and occurs. + /// + /// + /// Setting this to will enable validation of , , , and + /// during set operations and in .If invalid settings are discovered exceptions will be thrown indicating the error. + /// This will impose a performance penalty and thus should only be used for debugging. + /// + public bool ValidatePosDim { get; set; } + + /// + /// Throws an if is or . + /// Used when is turned on to verify correct behavior. + /// + /// + /// Does not verify if this view is Toplevel (WHY??!?). + /// + /// The property name. + /// + /// + void CheckAbsolute (string prop, object oldValue, object newValue) + { + if (!IsInitialized || !ValidatePosDim || oldValue == null || oldValue.GetType () == newValue.GetType () || this is Toplevel) { + return; + } + + if (oldValue.GetType () != newValue.GetType () && newValue is (Pos.PosAbsolute or Dim.DimAbsolute)) { + throw new ArgumentException ($@"{prop} must not be Absolute if LayoutStyle is Computed", prop); + } + } + + /// + /// Called whenever the view needs to be resized. Sets and + /// triggers a call. + /// + /// + /// Can be overridden if the view resize behavior is different than the default. + /// + protected virtual void OnResizeNeeded () + { + int actX = _x is Pos.PosAbsolute ? _x.Anchor (0) : _frame.X; + int actY = _y is Pos.PosAbsolute ? _y.Anchor (0) : _frame.Y; + + if (AutoSize) { + //if (TextAlignment == TextAlignment.Justified) { + // throw new InvalidOperationException ("TextAlignment.Justified cannot be used with AutoSize"); + //} + var s = GetAutoSize (); + int w = _width is Dim.DimAbsolute && _width.Anchor (0) > s.Width ? _width.Anchor (0) : s.Width; + int h = _height is Dim.DimAbsolute && _height.Anchor (0) > s.Height ? _height.Anchor (0) : s.Height; + _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame! + } else { + int w = _width is Dim.DimAbsolute ? _width.Anchor (0) : _frame.Width; + int h = _height is Dim.DimAbsolute ? _height.Anchor (0) : _frame.Height; + // BUGBUG: v2 - ? - If layoutstyle is absolute, this overwrites the current frame h/w with 0. Hmmm... + // This is needed for DimAbsolute values by setting the frame before LayoutSubViews. + _frame = new Rect (new Point (actX, actY), new Size (w, h)); // Set frame, not Frame! + } + //// BUGBUG: I think these calls are redundant or should be moved into just the AutoSize case + if (IsInitialized || LayoutStyle == LayoutStyle.Absolute) { + SetFrameToFitText (); + LayoutFrames (); + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); + SetNeedsLayout (); + SetNeedsDisplay (); + } + } + + internal bool LayoutNeeded { get; private set; } = true; + + internal void SetNeedsLayout () + { + if (LayoutNeeded) { + return; + } + LayoutNeeded = true; + foreach (var view in Subviews) { + view.SetNeedsLayout (); + } + TextFormatter.NeedsFormat = true; + SuperView?.SetNeedsLayout (); + } + + /// + /// Indicates that the view does not need to be laid out. + /// + protected void ClearLayoutNeeded () => LayoutNeeded = false; + + /// + /// Converts a screen-relative coordinate to a Frame-relative coordinate. Frame-relative means + /// relative to the View's 's . + /// + /// The coordinate relative to the 's . + /// Screen-relative column. + /// Screen-relative row. + public Point ScreenToFrame (int x, int y) + { + var superViewBoundsOffset = SuperView?.GetBoundsOffset () ?? Point.Empty; + var ret = new Point (x - Frame.X - superViewBoundsOffset.X, y - Frame.Y - superViewBoundsOffset.Y); + if (SuperView != null) { + var superFrame = SuperView.ScreenToFrame (x - superViewBoundsOffset.X, y - superViewBoundsOffset.Y); + ret = new Point (superFrame.X - Frame.X, superFrame.Y - Frame.Y); + } + return ret; + } + + /// + /// Converts a screen-relative coordinate to a bounds-relative coordinate. + /// + /// The coordinate relative to this view's . + /// Screen-relative column. + /// Screen-relative row. + public Point ScreenToBounds (int x, int y) + { + var screen = ScreenToFrame (x, y); + var boundsOffset = GetBoundsOffset (); + return new Point (screen.X - boundsOffset.X, screen.Y - boundsOffset.Y); + } + + /// + /// Converts a -relative coordinate to a screen-relative coordinate. The output is optionally clamped to the screen dimensions. + /// + /// -relative column. + /// -relative row. + /// Absolute column; screen-relative. + /// Absolute row; screen-relative. + /// If , and will be clamped to the + /// screen dimensions (will never be negative and will always be less than and + /// , respectively. + public virtual void BoundsToScreen (int x, int y, out int rx, out int ry, bool clamped = true) + { + var boundsOffset = GetBoundsOffset (); + rx = x + Frame.X + boundsOffset.X; + ry = y + Frame.Y + boundsOffset.Y; + + var super = SuperView; + while (super != null) { + boundsOffset = super.GetBoundsOffset (); + rx += super.Frame.X + boundsOffset.X; + ry += super.Frame.Y + boundsOffset.Y; + super = super.SuperView; + } + + // The following ensures that the cursor is always in the screen boundaries. + if (clamped) { + ry = Math.Min (ry, Driver.Rows - 1); + rx = Math.Min (rx, Driver.Cols - 1); + } + } + + /// + /// Converts a -relative region to a screen-relative region. + /// + public Rect BoundsToScreen (Rect region) + { + BoundsToScreen (region.X, region.Y, out int x, out int y, false); + return new Rect (x, y, region.Width, region.Height); + } + + /// + /// Gets the with a screen-relative location. + /// + /// The location and size of the view in screen-relative coordinates. + public virtual Rect FrameToScreen () + { + var ret = Frame; + var super = SuperView; + while (super != null) { + var boundsOffset = super.GetBoundsOffset (); + ret.X += super.Frame.X + boundsOffset.X; + ret.Y += super.Frame.Y + boundsOffset.Y; + super = super.SuperView; + } + return ret; + } + + // TODO: Come up with a better name for this method. "SetRelativeLayout" lacks clarity and confuses. AdjustSizeAndPosition? + /// + /// Applies the view's position (, ) and dimension (, and ) to + /// , given a rectangle describing the SuperView's Bounds (nominally the same as this.SuperView.Bounds). + /// + /// The rectangle describing the SuperView's Bounds (nominally the same as this.SuperView.Bounds). + internal void SetRelativeLayout (Rect superviewBounds) + { + int newX, newW, newY, newH; + var autosize = Size.Empty; + + if (AutoSize) { + // Note this is global to this function and used as such within the local functions defined + // below. In v2 AutoSize will be re-factored to not need to be dealt with in this function. + autosize = GetAutoSize (); + } + + // Returns the new dimension (width or height) and location (x or y) for the View given + // the superview's Bounds + // the current Pos (View.X or View.Y) + // the current Dim (View.Width or View.Height) + // This method is called recursively if pos is Pos.PosCombine + (int newLocation, int newDimension) GetNewLocationAndDimension (bool width, Rect superviewBounds, Pos pos, Dim dim, int autosizeDimension) + { + // Gets the new dimension (width or height, dependent on `width`) of the given Dim given: + // location: the current location (x or y) + // dimension: the current dimension (width or height) + // autosize: the size to use if autosize = true + // This mehod is recursive if d is Dim.DimCombine + int GetNewDimension (Dim d, int location, int dimension, int autosize) + { + int newDimension; + switch (d) { + case null: + // dim == null is the same as dim == Dim.FIll (0) + newDimension = AutoSize ? autosize : dimension; + break; + + case Dim.DimCombine combine: + int leftNewDim = GetNewDimension (combine._left, location, dimension, autosize); + int rightNewDim = GetNewDimension (combine._right, location, dimension, autosize); + if (combine._add) { + newDimension = leftNewDim + rightNewDim; + } else { + newDimension = leftNewDim - rightNewDim; + } + newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; + break; + + case Dim.DimFactor factor when !factor.IsFromRemaining (): + newDimension = d.Anchor (dimension); + newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; + break; + + case Dim.DimFill: + default: + newDimension = Math.Max (d.Anchor (dimension - location), 0); + newDimension = AutoSize && autosize > newDimension ? autosize : newDimension; + break; + } + + return newDimension; + } + + int newDimension, newLocation; + int superviewDimension = width ? superviewBounds.Width : superviewBounds.Height; + + // Determine new location + switch (pos) { + case Pos.PosCenter posCenter: + // For Center, the dimension is dependent on location, but we need to force getting the dimension first + // using a location of 0 + newDimension = Math.Max (GetNewDimension (dim, 0, superviewDimension, autosizeDimension), 0); + newLocation = posCenter.Anchor (superviewDimension - newDimension); + newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); + break; + + case Pos.PosCombine combine: + int left, right; + (left, newDimension) = GetNewLocationAndDimension (width, superviewBounds, combine._left, dim, autosizeDimension); + (right, newDimension) = GetNewLocationAndDimension (width, superviewBounds, combine._right, dim, autosizeDimension); + if (combine._add) { + newLocation = left + right; + } else { + newLocation = left - right; + } + newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); + break; + + case Pos.PosAnchorEnd: + case Pos.PosAbsolute: + case null: + case Pos.PosFactor: + case Pos.PosFunc: + case Pos.PosView: + default: + newLocation = pos?.Anchor (superviewDimension) ?? 0; + newDimension = Math.Max (GetNewDimension (dim, newLocation, superviewDimension, autosizeDimension), 0); + break; + } + + + return (newLocation, newDimension); + } + + + // horizontal/width + (newX, newW) = GetNewLocationAndDimension (true, superviewBounds, _x, _width, autosize.Width); + + // vertical/height + (newY, newH) = GetNewLocationAndDimension (false, superviewBounds, _y, _height, autosize.Height); + + var r = new Rect (newX, newY, newW, newH); + if (Frame != r) { + Frame = r; + // BUGBUG: Why is this AFTER setting Frame? Seems duplicative. + if (!SetFrameToFitText ()) { + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); + } + } + } + + /// + /// Fired after the View's method has completed. + /// + /// + /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. + /// + public event EventHandler LayoutStarted; + + /// + /// Raises the event. Called from before any subviews have been laid out. + /// + internal virtual void OnLayoutStarted (LayoutEventArgs args) => LayoutStarted?.Invoke (this, args); + + /// + /// Fired after the View's method has completed. + /// + /// + /// Subscribe to this event to perform tasks when the has been resized or the layout has otherwise changed. + /// + public event EventHandler LayoutComplete; + + /// + /// Event called only once when the is being initialized for the first time. + /// Allows configurations and assignments to be performed before the being shown. + /// This derived from to allow notify all the views that are being initialized. + /// + public event EventHandler Initialized; + + /// + /// Raises the event. Called from before all sub-views have been laid out. + /// + internal virtual void OnLayoutComplete (LayoutEventArgs args) => LayoutComplete?.Invoke (this, args); + + internal void CollectPos (Pos pos, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) + { + switch (pos) { + case Pos.PosView pv: + // See #2461 + //if (!from.InternalSubviews.Contains (pv.Target)) { + // throw new InvalidOperationException ($"View {pv.Target} is not a subview of {from}"); + //} + if (pv.Target != this) { + nEdges.Add ((pv.Target, from)); + } + return; + case Pos.PosCombine pc: + CollectPos (pc._left, from, ref nNodes, ref nEdges); + CollectPos (pc._right, from, ref nNodes, ref nEdges); + break; + } + } + + internal void CollectDim (Dim dim, View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) + { + switch (dim) { + case Dim.DimView dv: + // See #2461 + //if (!from.InternalSubviews.Contains (dv.Target)) { + // throw new InvalidOperationException ($"View {dv.Target} is not a subview of {from}"); + //} + if (dv.Target != this) { + nEdges.Add ((dv.Target, from)); + } + return; + case Dim.DimCombine dc: + CollectDim (dc._left, from, ref nNodes, ref nEdges); + CollectDim (dc._right, from, ref nNodes, ref nEdges); + break; + } + } + + internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) + { + // BUGBUG: This should really only work on initialized subviews + foreach (var v in from.InternalSubviews /*.Where(v => v.IsInitialized)*/) { + nNodes.Add (v); + if (v._layoutStyle != LayoutStyle.Computed) { + continue; + } + CollectPos (v.X, v, ref nNodes, ref nEdges); + CollectPos (v.Y, v, ref nNodes, ref nEdges); + CollectDim (v.Width, v, ref nNodes, ref nEdges); + CollectDim (v.Height, v, ref nNodes, ref nEdges); + } + } + + // https://en.wikipedia.org/wiki/Topological_sorting + internal static List TopologicalSort (View superView, IEnumerable nodes, ICollection<(View From, View To)> edges) + { + var result = new List (); + + // Set of all nodes with no incoming edges + var noEdgeNodes = new HashSet (nodes.Where (n => edges.All (e => !e.To.Equals (n)))); + + while (noEdgeNodes.Any ()) { + // remove a node n from S + var n = noEdgeNodes.First (); + noEdgeNodes.Remove (n); + + // add n to tail of L + if (n != superView) { + result.Add (n); + } + + // for each node m with an edge e from n to m do + foreach (var e in edges.Where (e => e.From.Equals (n)).ToArray ()) { + var m = e.To; + + // remove edge e from the graph + edges.Remove (e); + + // if m has no other incoming edges then + if (edges.All (me => !me.To.Equals (m)) && m != superView) { + // insert m into S + noEdgeNodes.Add (m); + } + } + } + + if (!edges.Any ()) { + return result; + } + + foreach ((var from, var to) in edges) { + if (from == to) { + // if not yet added to the result, add it and remove from edge + if (result.Find (v => v == from) == null) { + result.Add (from); + } + edges.Remove ((from, to)); + } else if (from.SuperView == to.SuperView) { + // if 'from' is not yet added to the result, add it + if (result.Find (v => v == from) == null) { + result.Add (from); + } + // if 'to' is not yet added to the result, add it + if (result.Find (v => v == to) == null) { + result.Add (to); + } + // remove from edge + edges.Remove ((from, to)); + } else if (from != superView?.GetTopSuperView (to, from) && !ReferenceEquals (from, to)) { + if (ReferenceEquals (from.SuperView, to)) { + throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{to}\" references a SubView (\"{from}\")."); + } else { + throw new InvalidOperationException ($"ComputedLayout for \"{superView}\": \"{from}\" linked with \"{to}\" was not found. Did you forget to add it to {superView}?"); + } + } + } + // return L (a topologically sorted order) + return result; + } // TopologicalSort + + /// + /// Overriden by to do nothing, as the does not have frames. + /// + internal virtual void LayoutFrames () + { + if (Margin == null) { + return; // CreateFrames() has not been called yet + } + + if (Margin.Frame.Size != Frame.Size) { + Margin._frame = new Rect (Point.Empty, Frame.Size); + Margin.X = 0; + Margin.Y = 0; + Margin.Width = Frame.Size.Width; + Margin.Height = Frame.Size.Height; + Margin.SetNeedsLayout (); + Margin.LayoutSubviews (); + Margin.SetNeedsDisplay (); + } + + var border = Margin.Thickness.GetInside (Margin.Frame); + if (border != Border.Frame) { + Border._frame = new Rect (new Point (border.Location.X, border.Location.Y), border.Size); + Border.X = border.Location.X; + Border.Y = border.Location.Y; + Border.Width = border.Size.Width; + Border.Height = border.Size.Height; + Border.SetNeedsLayout (); + Border.LayoutSubviews (); + Border.SetNeedsDisplay (); + } + + var padding = Border.Thickness.GetInside (Border.Frame); + if (padding != Padding.Frame) { + Padding._frame = new Rect (new Point (padding.Location.X, padding.Location.Y), padding.Size); + Padding.X = padding.Location.X; + Padding.Y = padding.Location.Y; + Padding.Width = padding.Size.Width; + Padding.Height = padding.Size.Height; + Padding.SetNeedsLayout (); + Padding.LayoutSubviews (); + Padding.SetNeedsDisplay (); + } + } + + /// + /// Invoked when a view starts executing or when the dimensions of the view have changed, for example in + /// response to the container view or terminal resizing. + /// + /// + /// Raises the event) before it returns. + /// + public virtual void LayoutSubviews () + { + if (!IsInitialized) { + Debug.WriteLine ($"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}"); + } + + if (!LayoutNeeded) { + return; + } + + LayoutFrames (); + + var oldBounds = Bounds; + OnLayoutStarted (new LayoutEventArgs () { OldBounds = oldBounds }); + + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); + + // Sort out the dependencies of the X, Y, Width, Height properties + var nodes = new HashSet (); + var edges = new HashSet<(View, View)> (); + CollectAll (this, ref nodes, ref edges); + var ordered = TopologicalSort (SuperView, nodes, edges); + foreach (var v in ordered) { + LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size)); + } + + // If the 'to' is rooted to 'from' and the layoutstyle is Computed it's a special-case. + // Use LayoutSubview with the Frame of the 'from' + if (SuperView != null && GetTopSuperView () != null && LayoutNeeded && edges.Count > 0) { + foreach ((var from, var to) in edges) { + LayoutSubview (to, from.Frame); + } + } + + LayoutNeeded = false; + + OnLayoutComplete (new LayoutEventArgs () { OldBounds = oldBounds }); + } + + void LayoutSubview (View v, Rect contentArea) + { + if (v.LayoutStyle == LayoutStyle.Computed) { + v.SetRelativeLayout (contentArea); + } + + v.LayoutSubviews (); + v.LayoutNeeded = false; + } + + bool _autoSize; + + /// + /// Gets or sets a flag that determines whether the View will be automatically resized to fit the + /// within + /// + /// The default is . Set to to turn on AutoSize. If then + /// and will be used if can fit; + /// if won't fit the view will be resized as needed. + /// + /// + /// In addition, if is the new values of and + /// must be of the same types of the existing one to avoid breaking the settings. + /// + /// + public virtual bool AutoSize { + get => _autoSize; + set { + bool v = ResizeView (value); + TextFormatter.AutoSize = v; + if (_autoSize != v) { + _autoSize = v; + TextFormatter.NeedsFormat = true; + UpdateTextFormatterText (); + OnResizeNeeded (); + } + } + } + + bool ResizeView (bool autoSize) + { + if (!autoSize) { + return false; + } + + bool boundsChanged = true; + var newFrameSize = GetAutoSize (); + if (IsInitialized && newFrameSize != Frame.Size) { + if (ValidatePosDim) { + // BUGBUG: This ain't right, obviously. We need to figure out how to handle this. + boundsChanged = ResizeBoundsToFit (newFrameSize); + } else { + Height = newFrameSize.Height; + Width = newFrameSize.Width; + } + } + // BUGBUG: This call may be redundant + TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); + return boundsChanged; + } + + /// + /// Resizes the View to fit the specified size. Factors in the HotKey. + /// + /// + /// whether the Bounds was changed or not + bool ResizeBoundsToFit (Size size) + { + bool boundsChanged = false; + bool canSizeW = TrySetWidth (size.Width - GetHotKeySpecifierLength (), out int rW); + bool canSizeH = TrySetHeight (size.Height - GetHotKeySpecifierLength (false), out int rH); + if (canSizeW) { + boundsChanged = true; + _width = rW; + } + if (canSizeH) { + boundsChanged = true; + _height = rH; + } + if (boundsChanged) { + Bounds = new Rect (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height); + } + + return boundsChanged; + } + + /// + /// Gets the Frame dimensions required to fit within using the text specified by the + /// property and accounting for any characters. + /// + /// The of the view required to fit the text. + public Size GetAutoSize () + { + int x = 0; + int y = 0; + if (IsInitialized) { + x = Bounds.X; + y = Bounds.Y; + } + var rect = TextFormatter.CalcRect (x, y, TextFormatter.Text, TextFormatter.Direction); + int newWidth = rect.Size.Width - GetHotKeySpecifierLength () + Margin.Thickness.Horizontal + Border.Thickness.Horizontal + Padding.Thickness.Horizontal; + int newHeight = rect.Size.Height - GetHotKeySpecifierLength (false) + Margin.Thickness.Vertical + Border.Thickness.Vertical + Padding.Thickness.Vertical; + return new Size (newWidth, newHeight); + } + + bool IsValidAutoSize (out Size autoSize) + { + var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); + autoSize = new Size (rect.Size.Width - GetHotKeySpecifierLength (), + rect.Size.Height - GetHotKeySpecifierLength (false)); + return !(ValidatePosDim && (!(Width is Dim.DimAbsolute) || !(Height is Dim.DimAbsolute)) + || _frame.Size.Width != rect.Size.Width - GetHotKeySpecifierLength () + || _frame.Size.Height != rect.Size.Height - GetHotKeySpecifierLength (false)); + } + + bool IsValidAutoSizeWidth (Dim width) + { + var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); + int dimValue = width.Anchor (0); + return !(ValidatePosDim && !(width is Dim.DimAbsolute) || dimValue != rect.Size.Width + - GetHotKeySpecifierLength ()); + } + + bool IsValidAutoSizeHeight (Dim height) + { + var rect = TextFormatter.CalcRect (_frame.X, _frame.Y, TextFormatter.Text, TextDirection); + int dimValue = height.Anchor (0); + return !(ValidatePosDim && !(height is Dim.DimAbsolute) || dimValue != rect.Size.Height + - GetHotKeySpecifierLength (false)); + } + + /// + /// Determines if the View's can be set to a new value. + /// + /// + /// Contains the width that would result if were set to "/> + /// if the View's can be changed to the specified value. False otherwise. + internal bool TrySetWidth (int desiredWidth, out int resultWidth) + { + int w = desiredWidth; + bool canSetWidth; + switch (Width) { + case Dim.DimCombine _: + case Dim.DimView _: + case Dim.DimFill _: + // It's a Dim.DimCombine and so can't be assigned. Let it have it's Width anchored. + w = Width.Anchor (w); + canSetWidth = !ValidatePosDim; + break; + case Dim.DimFactor factor: + // Tries to get the SuperView Width otherwise the view Width. + int sw = SuperView != null ? SuperView.Frame.Width : w; + if (factor.IsFromRemaining ()) { + sw -= Frame.X; + } + w = Width.Anchor (sw); + canSetWidth = !ValidatePosDim; + break; + default: + canSetWidth = true; + break; + } + resultWidth = w; + + return canSetWidth; + } + + /// + /// Determines if the View's can be set to a new value. + /// + /// + /// Contains the width that would result if were set to "/> + /// if the View's can be changed to the specified value. False otherwise. + internal bool TrySetHeight (int desiredHeight, out int resultHeight) + { + int h = desiredHeight; + bool canSetHeight; + switch (Height) { + case Dim.DimCombine _: + case Dim.DimView _: + case Dim.DimFill _: + // It's a Dim.DimCombine and so can't be assigned. Let it have it's height anchored. + h = Height.Anchor (h); + canSetHeight = !ValidatePosDim; + break; + case Dim.DimFactor factor: + // Tries to get the SuperView height otherwise the view height. + int sh = SuperView != null ? SuperView.Frame.Height : h; + if (factor.IsFromRemaining ()) { + sh -= Frame.Y; + } + h = Height.Anchor (sh); + canSetHeight = !ValidatePosDim; + break; + default: + canSetHeight = true; + break; + } + resultHeight = h; + + return canSetHeight; + } + + /// + /// Finds which view that belong to the superview at the provided location. + /// + /// The superview where to look for. + /// The column location in the superview. + /// The row location in the superview. + /// The found view screen relative column location. + /// The found view screen relative row location. + /// + /// The view that was found at the and coordinates. + /// if no view was found. + /// + public static View FindDeepestView (View start, int x, int y, out int resx, out int resy) + { + resy = resx = 0; + if (start == null || !start.Frame.Contains (x, y)) { + return null; + } + + var startFrame = start.Frame; + if (start.InternalSubviews != null) { + int count = start.InternalSubviews.Count; + if (count > 0) { + var boundsOffset = start.GetBoundsOffset (); + int rx = x - (startFrame.X + boundsOffset.X); + int ry = y - (startFrame.Y + boundsOffset.Y); + for (int i = count - 1; i >= 0; i--) { + var v = start.InternalSubviews [i]; + if (v.Visible && v.Frame.Contains (rx, ry)) { + var deep = FindDeepestView (v, rx, ry, out resx, out resy); + if (deep == null) { + return v; + } + return deep; + } + } + } + } + resx = x - startFrame.X; + resy = y - startFrame.Y; + return start; + } +} \ No newline at end of file diff --git a/Terminal.Gui/View/View.cs b/Terminal.Gui/View/View.cs index 7d045079e..a22e60d94 100644 --- a/Terminal.Gui/View/View.cs +++ b/Terminal.Gui/View/View.cs @@ -280,6 +280,7 @@ namespace Terminal.Gui { _oldCanFocus = CanFocus; _oldTabIndex = _tabIndex; + // BUGBUG: These should move to EndInit as they access Bounds causing debug spew. UpdateTextDirection (TextDirection); UpdateTextFormatterText (); SetHotKey (); diff --git a/Terminal.Gui/View/ViewText.cs b/Terminal.Gui/View/ViewText.cs index 649622bbb..73f191358 100644 --- a/Terminal.Gui/View/ViewText.cs +++ b/Terminal.Gui/View/ViewText.cs @@ -1,5 +1,6 @@ using System.Text; using System; +using System.Collections.Generic; namespace Terminal.Gui { @@ -122,20 +123,88 @@ namespace Terminal.Gui { UpdateTextFormatterText (); - if ((!ForceValidatePosDim && directionChanged && AutoSize) - || (ForceValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) { + if ((!ValidatePosDim && directionChanged && AutoSize) + || (ValidatePosDim && directionChanged && AutoSize && isValidOldAutoSize)) { OnResizeNeeded (); } else if (directionChanged && IsAdded) { ResizeBoundsToFit (Bounds.Size); // BUGBUG: I think this call is redundant. - SetBoundsToFitFrame (); + SetFrameToFitText (); } else { - SetBoundsToFitFrame (); + SetFrameToFitText (); } TextFormatter.Size = GetTextFormatterSizeNeededForTextAndHotKey (); SetNeedsDisplay (); } + + /// + /// Sets the size of the View to the minimum width or height required to fit . + /// + /// if the size was changed; if == or + /// will not fit. + /// + /// Always returns if is or + /// if (Horizontal) or (Vertical) are not not set or zero. + /// Does not take into account word wrapping. + /// + bool SetFrameToFitText () + { + // BUGBUG: This API is broken - should not assume Frame.Height == Bounds.Height + // + // Gets the minimum dimensions required to fit the View's , factoring in . + // + // The minimum dimensions required. + // if the dimensions fit within the View's , otherwise. + // + // Always returns if is or + // if (Horizontal) or (Vertical) are not not set or zero. + // Does not take into account word wrapping. + // + bool GetMinimumSizeOfText (out Size sizeRequired) + { + if (!IsInitialized) { + sizeRequired = new Size (0, 0); + return false; + } + sizeRequired = Bounds.Size; + + if (!AutoSize && !string.IsNullOrEmpty (TextFormatter.Text)) { + switch (TextFormatter.IsVerticalDirection (TextDirection)) { + case true: + int colWidth = TextFormatter.GetSumMaxCharWidth (new List { TextFormatter.Text }, 0, 1); + // TODO: v2 - This uses frame.Width; it should only use Bounds + if (_frame.Width < colWidth && + (Width == null || + Bounds.Width >= 0 && + Width is Dim.DimAbsolute && + Width.Anchor (0) >= 0 && + Width.Anchor (0) < colWidth)) { + sizeRequired = new Size (colWidth, Bounds.Height); + return true; + } + break; + default: + if (_frame.Height < 1 && + (Height == null || + Height is Dim.DimAbsolute && + Height.Anchor (0) == 0)) { + sizeRequired = new Size (Bounds.Width, 1); + return true; + } + break; + } + } + return false; + } + + if (GetMinimumSizeOfText (out var size)) { + _frame = new Rect (_frame.Location, size); + return true; + } + return false; + } + /// /// Gets the width or height of the characters /// in the property. diff --git a/Terminal.Gui/Views/Dialog.cs b/Terminal.Gui/Views/Dialog.cs index 5e6165c08..1f64e19d0 100644 --- a/Terminal.Gui/Views/Dialog.cs +++ b/Terminal.Gui/Views/Dialog.cs @@ -1,243 +1,243 @@ -// -// Dialog.cs: Dialog box -// -// Authors: -// Miguel de Icaza (miguel@gnome.org) -// -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Text.Json.Serialization; -using System.Text; -using Terminal.Gui; -using static Terminal.Gui.ConfigurationManager; -namespace Terminal.Gui { +namespace Terminal.Gui; + +/// +/// The is a that by default is centered and contains one +/// or more s. It defaults to the color scheme and has a 1 cell padding around the edges. +/// +/// +/// To run the modally, create the , and pass it to . +/// This will execute the dialog until it terminates via the [ESC] or [CTRL-Q] key, or when one of the views +/// or buttons added to the dialog calls . +/// +public class Dialog : Window { /// - /// The is a that by default is centered and contains one - /// or more s. It defaults to the color scheme and has a 1 cell padding around the edges. + /// The default for . /// /// - /// To run the modally, create the , and pass it to . - /// This will execute the dialog until it terminates via the [ESC] or [CTRL-Q] key, or when one of the views - /// or buttons added to the dialog calls . + /// This property can be set in a Theme. /// - public class Dialog : Window { - /// - /// The default for . - /// - /// - /// This property can be set in a Theme. - /// - [SerializableConfigurationProperty (Scope = typeof (ThemeScope)), JsonConverter (typeof (JsonStringEnumConverter))] - public static ButtonAlignments DefaultButtonAlignment { get; set; } = ButtonAlignments.Center; + [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + [JsonConverter (typeof (JsonStringEnumConverter))] + public static ButtonAlignments DefaultButtonAlignment { get; set; } = ButtonAlignments.Center; - // TODO: Reenable once border/borderframe design is settled - /// - /// Defines the default border styling for . Can be configured via . - /// - //[SerializableConfigurationProperty (Scope = typeof (ThemeScope))] - //public static Border DefaultBorder { get; set; } = new Border () { - // LineStyle = LineStyle.Single, - //}; + // TODO: Reenable once border/borderframe design is settled + /// + /// Defines the default border styling for . Can be configured via . + /// + //[SerializableConfigurationProperty (Scope = typeof (ThemeScope))] + //public static Border DefaultBorder { get; set; } = new Border () { + // LineStyle = LineStyle.Single, + //}; + internal List