using System; using System; using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; using System.Linq; namespace Terminal.Gui; /// /// /// Indicates the LayoutStyle for the . /// /// /// If Absolute, the , , , and /// /// objects are all absolute values and are not relative. The position and size of the view is described by /// . /// /// /// If Computed, one or more of the , , , or /// /// objects are relative to the and are computed at layout time. /// /// public enum LayoutStyle { /// /// Indicates the , , , and /// objects are all absolute values and are not relative. The position and size of the view is described by /// . /// Absolute, /// /// Indicates one or more of the , , , or /// /// objects are relative to the and are computed at layout time. The position and size of the /// view /// will be computed based on these objects at layout time. will provide the absolute computed /// values. /// Computed } public partial class View { bool _autoSize; Rect _frame; Dim _height = Dim.Sized (0); Dim _width = Dim.Sized (0); Pos _x = Pos.At (0); Pos _y = Pos.At (0); /// /// Gets or sets the absolute location and dimension of the view. /// /// /// The rectangle describing absolute location and dimension of the view, /// in coordinates relative to the 's . /// /// /// /// Frame is relative to the 's . /// /// /// Setting Frame will set , , , and /// to the values of the corresponding properties of the parameter. /// /// /// This causes to be . /// /// /// Altering the Frame will eventually (when the view hierarchy is next laid out via see cref="LayoutSubviews"/>) /// cause and methods to be called. /// /// public Rect Frame { get => _frame; set { _frame = new Rect (value.X, value.Y, Math.Max (value.Width, 0), Math.Max (value.Height, 0)); // If Frame gets set, by definition, the View is now LayoutStyle.Absolute, so // set all Pos/Dim to Absolute values. _x = _frame.X; _y = _frame.Y; _width = _frame.Width; _height = _frame.Height; // TODO: Figure out if the below can be optimized. if (IsInitialized /*|| LayoutStyle == LayoutStyle.Absolute*/) { LayoutFrames (); SetTextFormatterSize (); 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; } /// /// /// Gets the LayoutStyle for the . /// /// /// If Absolute, the , , , and /// /// objects are all absolute values and are not relative. The position and size of the view is described by /// . /// /// /// If Computed, one or more of the , , , or /// /// objects are relative to the and are computed at layout time. /// /// /// The layout style. public LayoutStyle LayoutStyle { get { if (_x is Pos.PosAbsolute && _y is Pos.PosAbsolute && _width is Dim.DimAbsolute && _height is Dim.DimAbsolute) { return LayoutStyle.Absolute; } return LayoutStyle.Computed; } } /// /// The bounds represent the View-relative rectangle used for this view; the area inside of the view where subviews and /// content are presented. /// /// The rectangle describing the location and size of the area where the views' subviews and content are drawn. /// /// /// If is the value of Bounds is indeterminate until /// the view has been initialized ( is true) and has been /// called. /// /// /// Updates to the Bounds updates , and has the same effect as updating the /// . /// /// /// Altering the Bounds will eventually (when the view is next laid out) cause the /// /// and methods to be called. /// /// /// Because coordinates are relative to the upper-left corner of the , /// the coordinates of the upper-left corner of the rectangle returned by this property are (0,0). /// Use this property to obtain the size of the area of the view for tasks such as drawing the view's contents. /// /// 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}"); } #endif // DEBUG var frameRelativeBounds = FrameGetInsideBounds (); return new Rect (default, frameRelativeBounds.Size); } set { // TODO: Should we enforce Bounds.X/Y == 0? The code currently ignores value.X/Y which is // TODO: correct behavior, but is silent. Perhaps an exception? #if DEBUG if (value.Location != Point.Empty) { Debug.WriteLine ($"WARNING: Bounds.Location must always be 0,0. Location ({value.Location}) is ignored. {this}"); } #endif // DEBUG 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 ) ); } } /// /// Gets or sets the X position for the view (the column). /// /// The object representing the X position. /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the /// view has been initialized ( is true) and has been /// called. /// /// /// Changing this property will eventually (when the view is next drawn) cause the /// and /// methods to be called. /// /// /// Changing this property will cause to be updated. If /// the new value is not of type the will change to /// . /// /// /// The default value is Pos.At (0). /// /// public Pos X { get => VerifyIsInitialized (_x, nameof (X)); set { _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null"); OnResizeNeeded (); } } /// /// Gets or sets the Y position for the view (the row). /// /// The object representing the Y position. /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the /// view has been initialized ( is true) and has been /// called. /// /// /// Changing this property will eventually (when the view is next drawn) cause the /// and /// methods to be called. /// /// /// Changing this property will cause to be updated. If /// the new value is not of type the will change to /// . /// /// /// The default value is Pos.At (0). /// /// public Pos Y { get => VerifyIsInitialized (_y, nameof (Y)); set { _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null"); OnResizeNeeded (); } } /// /// Gets or sets the width dimension of the view. /// /// The object representing the width of the view (the number of columns). /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the /// view has been initialized ( is true) and has been /// called. /// /// /// Changing this property will eventually (when the view is next drawn) cause the /// /// and methods to be called. /// /// /// Changing this property will cause to be updated. If /// the new value is not of type the will change to /// . /// /// /// The default value is Dim.Sized (0). /// /// public Dim Width { get => VerifyIsInitialized (_width, nameof (Width)); set { _width = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Width)} cannot be null"); if (ValidatePosDim) { CheckDimAuto (); 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 dimension of the view. /// /// The object representing the height of the view (the number of rows). /// /// /// If set to a relative value (e.g. ) the value is indeterminate until the /// view has been initialized ( is true) and has been /// called. /// /// /// Changing this property will eventually (when the view is next drawn) cause the /// /// and methods to be called. /// /// /// Changing this property will cause to be updated. If /// the new value is not of type the will change to /// . /// /// /// The default value is Dim.Sized (0). /// /// public Dim Height { get => VerifyIsInitialized (_height, nameof (Height)); set { _height = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Height)} cannot be null"); if (ValidatePosDim) { CheckDimAuto (); var isValidNewAutSize = AutoSize && IsValidAutoSizeHeight (_height); if (IsAdded && AutoSize && !isValidNewAutSize) { throw new InvalidOperationException ("Must set AutoSize to false before setting the Height."); } } OnResizeNeeded (); } } /// /// 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 any SubViews are using Dim objects that depend on this /// Views dimensions. /// /// void CheckDimAuto () { if (!ValidatePosDim && Width is not Dim.DimAuto && Height is not Dim.DimAuto) { return; } void ThrowInvalid (View view, object checkPosDim, string name) { // TODO: Figure out how to make CheckDimAuto deal with PosCombine object bad = null; switch (checkPosDim) { case Pos pos and not Pos.PosAbsolute and not Pos.PosView and not Pos.PosCombine: bad = pos; break; case Pos pos and Pos.PosCombine: // Recursively check for not Absolute or not View ThrowInvalid (view, (pos as Pos.PosCombine)._left, name); ThrowInvalid (view, (pos as Pos.PosCombine)._right, name); break; case Dim dim and not Dim.DimAbsolute and not Dim.DimView and not Dim.DimCombine: bad = dim; break; case Dim dim and Dim.DimCombine: // Recursively check for not Absolute or not View ThrowInvalid (view, (dim as Dim.DimCombine)._left, name); ThrowInvalid (view, (dim as Dim.DimCombine)._right, name); break; } if (bad != null) { throw new InvalidOperationException ( @$"{view.GetType ().Name}.{name} = {bad.GetType ().Name} which depends on the SuperView's dimensions and the SuperView uses Dim.Auto."); } } // Verify none of the subviews are using Dim objects that depend on the SuperView's dimensions. foreach (var view in Subviews) { if (Width is Dim.DimAuto { _min: null }) { ThrowInvalid (view, view.Width, nameof (view.Width)); ThrowInvalid (view, view.X, nameof (view.X)); } if (Height is Dim.DimAuto { _min: null }) { ThrowInvalid (view, view.Height, nameof (view.Height)); ThrowInvalid (view, view.Y, nameof (view.Y)); } } } internal bool LayoutNeeded { get; private set; } = true; /// /// 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. /// /// /// If is set to then and /// will be changed to if they are not already. /// /// /// If is set to then and /// will left unchanged. /// /// public virtual bool AutoSize { get => _autoSize; set { var v = ResizeView (value); TextFormatter.AutoSize = v; if (_autoSize != v) { _autoSize = v; TextFormatter.NeedsFormat = true; UpdateTextFormatterText (); OnResizeNeeded (); } } } /// /// 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; /// /// 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 (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) { if (IsInitialized) { 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; } 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, string member) { #if DEBUG if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate {pos}. This is potentially a bug."); } #endif // DEBUG return pos; } // Diagnostics to highlight when Width or Height is read before the view has been initialized Dim VerifyIsInitialized (Dim dim, string member) { #if DEBUG if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { Debug.WriteLine ($"WARNING: \"{this}\" has not been initialized; {member} is indeterminate: {dim}. This is potentially a bug."); } #endif // DEBUG return dim; } /// /// Called whenever the view needs to be resized. This is called whenever , /// , , , or changes. /// /// /// /// Determines the relative bounds of the and its s, and then calls /// to update the view. /// /// internal void OnResizeNeeded () { SuperView?.CheckDimAuto (); // TODO: Identify a real-world use-case where this API should be virtual. // TODO: Until then leave it `internal` and non-virtual // First try SuperView.Bounds, then Application.Top, then Driver.Bounds. // Finally, if none of those are valid, use int.MaxValue (for Unit tests). var relativeBounds = SuperView is { IsInitialized: true } ? SuperView.Bounds : Application.Top != null && Application.Top.IsInitialized ? Application.Top.Bounds : Application.Driver?.Bounds ?? new Rect (0, 0, int.MaxValue, int.MaxValue); SetRelativeLayout (relativeBounds); // TODO: Determine what, if any of the below is actually needed here. if (IsInitialized) { SetFrameToFitText (); LayoutFrames (); SetTextFormatterSize (); SetNeedsLayout (); SetNeedsDisplay (); } //if (IsInitialized && SuperView != null && LayoutStyle == LayoutStyle.Computed && (SuperView?.Height is Dim.DimAuto || SuperView?.Width is Dim.DimAuto)) { // // DimAuto is in play, force a layout. // // BUGBUG: This can cause LayoutSubviews to be called recursively resulting in a deadlock. // // SetNeedsLayout should be sufficient, but it's not. // SuperView.LayoutSubviews (); //} } /// /// Sets the internal flag for this View and all of it's /// subviews and it's SuperView. The main loop will call SetRelativeLayout and LayoutSubviews /// for any view with set. /// 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 var x, out var 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; } /// /// 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) { Debug.Assert (_x != null); Debug.Assert (_y != null); Debug.Assert (_width != null); Debug.Assert (_height != null); 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 (); } // TODO: Since GetNewLocationAndDimension does not depend on View, it can be moved into PosDim.cs // TODO: to make architecture more clean. Do this after DimAuto is implemented and the // TODO: View.AutoSize stuff is removed. // 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 new dimension (width or height) (if relevant for Dim type) // autosize: the size to use if autosize = true // This method is recursive if d is Dim.DimCombine int GetNewDimension (Dim d, int location, int dimension, int autosize) { int newDimension; switch (d) { case Dim.DimCombine combine: // TODO: Move combine logic into DimCombine? var leftNewDim = GetNewDimension (combine._left, location, dimension, autosize); var 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.DimAuto auto: var thickness = GetFramesThickness (); //newDimension = GetNewDimension (auto._min, location, dimension, autosize); if (width) { var furthestRight = Subviews.Count == 0 ? 0 : Subviews.Where (v => v.X is not Pos.PosAnchorEnd).Max (v => v.Frame.X + v.Frame.Width); //Debug.Assert(superviewBounds.Width == (SuperView?.Bounds.Width ?? 0)); newDimension = int.Max (furthestRight + thickness.Left + thickness.Right, auto._min?.Anchor (superviewBounds.Width) ?? 0); } else { var furthestBottom = Subviews.Count == 0 ? 0 : Subviews.Max (v => v.Frame.Y + v.Frame.Height); //Debug.Assert (superviewBounds.Height == (SuperView?.Bounds.Height ?? 0)); newDimension = int.Max (furthestBottom + thickness.Top + thickness.Bottom, auto._min?.Anchor (superviewBounds.Height) ?? 0); } break; case Dim.DimAbsolute: // DimAbsoulte.Anchor (int width) ignores width and returns n newDimension = Math.Max (d.Anchor (0), 0); 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; var 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: // TODO: Move combine logic into PosCombine? 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 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) { // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height, making // the view LayoutStyle.Absolute. _frame = r; if (_x is Pos.PosAbsolute) { _x = Frame.X; } if (_y is Pos.PosAbsolute) { _y = Frame.Y; } if (_width is Dim.DimAbsolute) { _width = Frame.Width; } if (_height is Dim.DimAbsolute) { _height = Frame.Height; } if (IsInitialized) { // TODO: Figure out what really is needed here. All unit tests (except AutoSize) pass as-is //LayoutFrames (); SetTextFormatterSize (); SetNeedsLayout (); //SetNeedsDisplay (); } // BUGBUG: Why is this AFTER setting Frame? Seems duplicative. if (!SetFrameToFitText ()) { SetTextFormatterSize (); } } } /// /// 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; /// /// 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}\")."); } 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.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.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.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. /// /// /// /// The position and dimensions of the view are indeterminate until the view has been initialized. Therefore, /// the behavior of this method is indeterminate if is . /// /// /// 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; } CheckDimAuto (); LayoutFrames (); var oldBounds = Bounds; OnLayoutStarted (new LayoutEventArgs { OldBounds = oldBounds }); SetTextFormatterSize (); // 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) { if (v.Width is Dim.DimAuto || v.Height is Dim.DimAuto) { // If the view is auto-sized... var f = v.Frame; v._frame = new Rect (v.Frame.X, v.Frame.Y, 0, 0); LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size)); if (v.Frame != f) { // The subviews changed; do it again v.LayoutNeeded = true; LayoutSubview (v, new Rect (GetBoundsOffset (), Bounds.Size)); } } else { 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 ResizeView (bool autoSize) { if (!autoSize) { return false; } var 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; } } 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; } /// /// 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 = !ValidatePosDim; 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 = !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) { 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 = !ValidatePosDim; 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 = !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) { var 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 (var 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; } }