using System.Diagnostics; namespace Terminal.Gui; /// /// Indicates the LayoutStyle for the . /// /// If Absolute, the , , , and /// objects are all absolute values and are not relative. The position and size of the /// view is described by . /// /// /// If Computed, one or more of the , , , or /// objects are relative to the and are computed at layout /// time. /// /// public enum LayoutStyle { /// /// Indicates the , , , and /// objects are all absolute values and are not relative. The position and size of the view /// is described by . /// Absolute, /// /// Indicates one or more of the , , , or /// objects are relative to the and are computed at layout time. /// The position and size of the view will be computed based on these objects at layout time. /// will provide the absolute computed values. /// Computed } public partial class View { #region Frame private Rectangle _frame; /// 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 Rectangle Frame { get => _frame; set { _frame = value with { Width = Math.Max (value.Width, 0), Height = 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*/) { LayoutAdornments (); SetTextFormatterSize (); SetNeedsLayout (); SetNeedsDisplay (); } } } /// Gets the with a screen-relative location. /// The location and size of the view in screen-relative coordinates. public virtual Rectangle FrameToScreen () { Rectangle ret = Frame; View super = SuperView; while (super is { }) { if (super is Adornment adornment) { // Adornments don't have SuperViews; use Adornment.FrameToScreen override ret = adornment.FrameToScreen (); ret.Offset (Frame.X, Frame.Y); return ret; } Point viewportOffset = super.GetViewportOffset (); viewportOffset.Offset(super.Frame.X, super.Frame.Y); ret.X += viewportOffset.X; ret.Y += viewportOffset.Y; super = super.SuperView; } return ret; } /// /// 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 virtual Point ScreenToFrame (int x, int y) { Point superViewBoundsOffset = SuperView?.GetViewportOffset () ?? Point.Empty; if (SuperView is null) { superViewBoundsOffset.Offset (x - Frame.X, y - Frame.Y); return superViewBoundsOffset; } var frame = SuperView.ScreenToFrame (x - superViewBoundsOffset.X, y - superViewBoundsOffset.Y); frame.Offset (-Frame.X, -Frame.Y); return frame; } private Pos _x = Pos.At (0); /// 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 (); } } private Pos _y = Pos.At (0); /// 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 (); } } private Dim _height = Dim.Sized (0); /// 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 (AutoSize) { throw new InvalidOperationException (@$"Must set AutoSize to false before setting {nameof (Height)}."); } //if (ValidatePosDim) { bool isValidNewAutoSize = AutoSize && IsValidAutoSizeHeight (_height); if (IsAdded && AutoSize && !isValidNewAutoSize) { throw new InvalidOperationException ( @$"Must set AutoSize to false before setting the {nameof (Height)}." ); } //} OnResizeNeeded (); } } private Dim _width = Dim.Sized (0); /// 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 (AutoSize) { throw new InvalidOperationException (@$"Must set AutoSize to false before setting {nameof (Width)}."); } bool isValidNewAutoSize = AutoSize && IsValidAutoSizeWidth (_width); if (IsAdded && AutoSize && !isValidNewAutoSize) { throw new InvalidOperationException (@$"Must set AutoSize to false before setting {nameof (Width)}."); } OnResizeNeeded (); } } #endregion Frame #region Viewport /// /// The viewport represents the location and size of the View's content that can be seen by the end-user at a given time. /// The location is specified in coordinates relative to the top-left corner of the area of the View within the /// , and and is normally 0, 0. Non-zero /// values for the location indicate the visible area is offset into the View's virtual . /// /// The rectangle describing the location and size of the area where the views' subviews and content are visible. /// /// /// If is the value of Viewport is indeterminate until /// the view has been initialized ( is true) and has been /// called. /// /// /// Updates to the Viewport size updates , and has the same effect as updating the /// . /// /// /// Altering the Viewport size will eventually (when the view is next laid out) cause the /// and methods to be called. /// /// public virtual Rectangle Viewport { get { #if DEBUG if (LayoutStyle == LayoutStyle.Computed && !IsInitialized) { Debug.WriteLine ( $"WARNING: Viewport is being accessed before the View has been initialized. This is likely a bug in {this}" ); } #endif // DEBUG if (Margin is null || Border is null || Padding is null) { // CreateAdornments has not been called yet. return Rectangle.Empty with { Size = Frame.Size }; } Thickness totalThickness = GetAdornmentsThickness (); return Rectangle.Empty with { Size = new ( Math.Max (0, Frame.Size.Width - totalThickness.Horizontal), Math.Max (0, Frame.Size.Height - totalThickness.Vertical)) }; } set { // TODO: Should we enforce Viewport.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: Viewport.Location must always be 0,0. Location ({value.Location}) is ignored. {this}" ); } #endif // DEBUG Thickness totalThickness = GetAdornmentsThickness (); Frame = Frame with { Size = new ( value.Size.Width + totalThickness.Horizontal, value.Size.Height + totalThickness.Vertical) }; } } /// Converts a -relative rectangle to a screen-relative rectangle. public Rectangle ViewportToScreen (in Rectangle bounds) { // Translate bounds to Frame (our SuperView's Viewport-relative coordinates) Rectangle screen = FrameToScreen (); Point viewportOffset = GetViewportOffset (); screen.Offset (viewportOffset.X + bounds.X, viewportOffset.Y + bounds.Y); return new (screen.Location, bounds.Size); } /// Converts a screen-relative coordinate to a Viewport-relative coordinate. /// The coordinate relative to this view's . /// Screen-relative column. /// Screen-relative row. public Point ScreenToViewport (int x, int y) { Point viewportOffset = GetViewportOffset (); Point screen = ScreenToFrame (x, y); screen.Offset (-viewportOffset.X, -viewportOffset.Y); return screen; } /// /// Helper to get the X and Y offset of the Viewport from the Frame. This is the sum of the Left and Top properties /// of , and . /// public Point GetViewportOffset () { return Padding is null ? Point.Empty : Padding.Thickness.GetInside (Padding.Frame).Location; } #endregion Viewport #region AutoSize private 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. /// /// /// 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 { if (Width != Dim.Sized (0) && Height != Dim.Sized (0)) { Debug.WriteLine ( $@"WARNING: {GetType ().Name} - Setting {nameof (AutoSize)} invalidates {nameof (Width)} and {nameof (Height)}." ); } bool v = ResizeView (value); TextFormatter.AutoSize = v; if (_autoSize != v) { _autoSize = v; TextFormatter.NeedsFormat = true; UpdateTextFormatterText (); OnResizeNeeded (); } } } /// If is true, resizes the view. /// /// private bool ResizeView (bool autoSize) { if (!autoSize) { return false; } var boundsChanged = true; Size 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; } /// Determines if the View's can be set to a new value. /// TrySetHeight can only be called when AutoSize is true (or being set to true). /// /// /// 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 is { } ? 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; } /// Determines if the View's can be set to a new value. /// TrySetWidth can only be called when AutoSize is true (or being set to true). /// /// /// 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 is { } ? 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; } /// Resizes the View to fit the specified size. Factors in the HotKey. /// ResizeBoundsToFit can only be called when AutoSize is true (or being set to true). /// /// whether the Viewport was changed or not private bool ResizeBoundsToFit (Size size) { //if (AutoSize == false) { // throw new InvalidOperationException ("ResizeBoundsToFit can only be called when AutoSize is true"); //} var 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) { Viewport = new (Viewport.X, Viewport.Y, canSizeW ? rW : Viewport.Width, canSizeH ? rH : Viewport.Height); } return boundsChanged; } #endregion AutoSize #region Layout Engine /// /// 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. /// /// /// /// Setting this property to will cause to determine the /// size and position of the view. and will be set to /// using . /// /// /// Setting this property to will cause the view to use the /// method to size and position of the view. If either of the and /// properties are `null` they will be set to using the current value /// of . If either of the and properties are `null` /// they will be set to using . /// /// /// 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; } } #endregion Layout Engine internal bool LayoutNeeded { get; private set; } = true; /// /// Indicates whether the specified SuperView-relative coordinates are within the View's . /// /// SuperView-relative X coordinate. /// SuperView-relative Y coordinate. /// if the specified SuperView-relative coordinates are within the View. public virtual bool Contains (int x, int y) { return Frame.Contains (x, y); } #nullable enable /// Finds the first Subview of that is visible at the provided location. /// /// /// Used to determine what view the mouse is over. /// /// /// The view to scope the search by. /// .SuperView-relative X coordinate. /// .SuperView-relative Y coordinate. /// /// The view that was found at the and coordinates. /// if no view was found. /// // CONCURRENCY: This method is not thread-safe. Undefined behavior and likely program crashes are exposed by unsynchronized access to InternalSubviews. internal static View? FindDeepestView (View? start, int x, int y) { if (start is null || !start.Visible || !start.Contains (x, y)) { return null; } Adornment? found = null; if (start.Margin.Contains (x, y)) { found = start.Margin; } else if (start.Border.Contains (x, y)) { found = start.Border; } else if (start.Padding.Contains (x, y)) { found = start.Padding; } Point viewportOffset = start.GetViewportOffset (); if (found is { }) { start = found; viewportOffset = found.Parent.Frame.Location; } if (start.InternalSubviews is { Count: > 0 }) { int startOffsetX = x - (start.Frame.X + viewportOffset.X); int startOffsetY = y - (start.Frame.Y + viewportOffset.Y); for (int i = start.InternalSubviews.Count - 1; i >= 0; i--) { View nextStart = start.InternalSubviews [i]; if (nextStart.Visible && nextStart.Contains (startOffsetX, startOffsetY)) { // TODO: Remove recursion return FindDeepestView (nextStart, startOffsetX, startOffsetY) ?? nextStart; } } } return start; } #nullable restore /// /// Gets a new location of the that is within the Viewport of the 's /// (e.g. for dragging a Window). The `out` parameters are the new X and Y coordinates. /// /// /// If does not have a or it's SuperView is not /// the position will be bound by the and /// . /// /// The View that is to be moved. /// The target x location. /// The target y location. /// The new x location that will ensure will be fully visible. /// The new y location that will ensure will be fully visible. /// The new top most statusBar /// /// Either (if does not have a Super View) or /// 's SuperView. This can be used to ensure LayoutSubviews is called on the correct View. /// internal static View GetLocationEnsuringFullVisibility ( View viewToMove, int targetX, int targetY, out int nx, out int ny, out StatusBar statusBar ) { int maxDimension; View superView; if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { maxDimension = Driver.Cols; superView = Application.Top; } else { // Use the SuperView's Viewport, not Frame maxDimension = viewToMove.SuperView.Viewport.Width; superView = viewToMove.SuperView; } if (superView.Margin is { } && superView == viewToMove.SuperView) { maxDimension -= superView.GetAdornmentsThickness ().Left + superView.GetAdornmentsThickness ().Right; } if (viewToMove.Frame.Width <= maxDimension) { nx = Math.Max (targetX, 0); nx = nx + viewToMove.Frame.Width > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Width, 0) : nx; if (nx > viewToMove.Frame.X + viewToMove.Frame.Width) { nx = Math.Max (viewToMove.Frame.Right, 0); } } else { nx = targetX; } //System.Diagnostics.Debug.WriteLine ($"nx:{nx}, rWidth:{rWidth}"); bool menuVisible, statusVisible; if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { menuVisible = Application.Top.MenuBar?.Visible == true; } else { View t = viewToMove.SuperView; while (t is not Toplevel) { t = t.SuperView; } menuVisible = ((Toplevel)t).MenuBar?.Visible == true; } if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { maxDimension = menuVisible ? 1 : 0; } else { maxDimension = 0; } ny = Math.Max (targetY, maxDimension); if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { statusVisible = Application.Top.StatusBar?.Visible == true; statusBar = Application.Top.StatusBar; } else { View t = viewToMove.SuperView; while (t is not Toplevel) { t = t.SuperView; } statusVisible = ((Toplevel)t).StatusBar?.Visible == true; statusBar = ((Toplevel)t).StatusBar; } if (viewToMove?.SuperView is null || viewToMove == Application.Top || viewToMove?.SuperView == Application.Top) { maxDimension = statusVisible ? Driver.Rows - 1 : Driver.Rows; } else { maxDimension = statusVisible ? viewToMove.SuperView.Frame.Height - 1 : viewToMove.SuperView.Frame.Height; } if (superView.Margin is { } && superView == viewToMove.SuperView) { maxDimension -= superView.GetAdornmentsThickness ().Top + superView.GetAdornmentsThickness ().Bottom; } ny = Math.Min (ny, maxDimension); if (viewToMove.Frame.Height <= maxDimension) { ny = ny + viewToMove.Frame.Height > maxDimension ? Math.Max (maxDimension - viewToMove.Frame.Height, menuVisible ? 1 : 0) : ny; if (ny > viewToMove.Frame.Y + viewToMove.Frame.Height) { ny = Math.Max (viewToMove.Frame.Bottom, 0); } } //System.Diagnostics.Debug.WriteLine ($"ny:{ny}, rHeight:{rHeight}"); return superView; } /// 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; /// 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; /// /// 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; } LayoutAdornments (); Rectangle oldBounds = Viewport; OnLayoutStarted (new () { OldBounds = oldBounds }); SetTextFormatterSize (); // Sort out the dependencies of the X, Y, Width, Height properties HashSet nodes = new (); HashSet<(View, View)> edges = new (); CollectAll (this, ref nodes, ref edges); List ordered = TopologicalSort (SuperView, nodes, edges); foreach (View v in ordered) { LayoutSubview (v, new (GetViewportOffset (), Viewport.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 is { } && GetTopSuperView () is { } && LayoutNeeded && edges.Count > 0) { foreach ((View from, View to) in edges) { LayoutSubview (to, from.Frame); } } LayoutNeeded = false; OnLayoutComplete (new () { OldBounds = oldBounds }); } /// Indicates that the view does not need to be laid out. protected void ClearLayoutNeeded () { LayoutNeeded = false; } /// /// Raises the event. Called from before all sub-views /// have been laid out. /// internal virtual void OnLayoutComplete (LayoutEventArgs args) { LayoutComplete?.Invoke (this, args); } /// /// Raises the event. Called from before any subviews /// have been laid out. /// internal virtual void OnLayoutStarted (LayoutEventArgs args) { LayoutStarted?.Invoke (this, args); } /// /// 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 () { // TODO: Identify a real-world use-case where this API should be virtual. // TODO: Until then leave it `internal` and non-virtual // First try SuperView.Viewport, then Application.Top, then Driver.Viewport. // Finally, if none of those are valid, use int.MaxValue (for Unit tests). Rectangle relativeBounds = SuperView is { IsInitialized: true } ? SuperView.Viewport : Application.Top is { } && Application.Top.IsInitialized ? Application.Top.Viewport : Application.Driver?.Viewport ?? new Rectangle (0, 0, int.MaxValue, int.MaxValue); SetRelativeLayout (relativeBounds); // TODO: Determine what, if any of the below is actually needed here. if (IsInitialized) { if (AutoSize) { SetFrameToFitText (); SetTextFormatterSize (); } LayoutAdornments (); SetNeedsDisplay (); SetNeedsLayout (); } } /// /// 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 (View view in Subviews) { view.SetNeedsLayout (); } TextFormatter.NeedsFormat = true; SuperView?.SetNeedsLayout (); } /// /// Applies the view's position (, ) and dimension (, and /// ) to , given a rectangle describing the SuperView's Viewport (nominally the /// same as this.SuperView.Viewport). /// /// /// The rectangle describing the SuperView's Viewport (nominally the same as /// this.SuperView.Viewport). /// internal void SetRelativeLayout (Rectangle superviewBounds) { Debug.Assert (_x is { }); Debug.Assert (_y is { }); Debug.Assert (_width is { }); Debug.Assert (_height is { }); 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 Viewport // 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, Rectangle 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? 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.DimAbsolute: // DimAbsolute.Anchor (int width) ignores width and returns n newDimension = Math.Max (d.Anchor (0), 0); // BUGBUG: AutoSize does two things: makes text fit AND changes the view's dimensions 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: // 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); Rectangle r = new (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; } SetNeedsLayout (); SetNeedsDisplay (); } if (AutoSize) { if (autosize.Width == 0 || autosize.Height == 0) { // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height, making // the view LayoutStyle.Absolute. _frame = _frame with { Size = autosize }; if (autosize.Width == 0) { _width = 0; } if (autosize.Height == 0) { _height = 0; } } else if (!SetFrameToFitText ()) { SetTextFormatterSize (); } SetNeedsLayout (); SetNeedsDisplay (); } } internal void CollectAll (View from, ref HashSet nNodes, ref HashSet<(View, View)> nEdges) { // BUGBUG: This should really only work on initialized subviews foreach (View v in from.InternalSubviews /*.Where(v => v.IsInitialized)*/) { 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); } } 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 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; } } // https://en.wikipedia.org/wiki/Topological_sorting internal static List TopologicalSort ( View superView, IEnumerable nodes, ICollection<(View From, View To)> edges ) { List result = new (); // Set of all nodes with no incoming edges HashSet noEdgeNodes = new (nodes.Where (n => edges.All (e => !e.To.Equals (n)))); while (noEdgeNodes.Any ()) { // remove a node n from S View 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 ((View From, View To) e in edges.Where (e => e.From.Equals (n)).ToArray ()) { View 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 ((View from, View 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) is 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) is null) { result.Add (from); } // if 'to' is not yet added to the result, add it if (result.Find (v => v == to) is 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 private void LayoutSubview (View v, Rectangle viewport) { //if (v.LayoutStyle == LayoutStyle.Computed) { v.SetRelativeLayout (viewport); //} v.LayoutSubviews (); v.LayoutNeeded = false; } #region Diagnostics // Diagnostics to highlight when Width or Height is read before the view has been initialized private 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; } // Diagnostics to highlight when X or Y is read before the view has been initialized private Pos VerifyIsInitialized (Pos pos, string member) { #if DEBUG if (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; } /// 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; } #endregion }