using System.ComponentModel; 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 { private bool _autoSize; private Rectangle _frame; private Dim _height = Dim.Sized (0); private Dim _width = Dim.Sized (0); private Pos _x = Pos.At (0); private Pos _y = Pos.At (0); /// /// 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 (); } } } /// /// The adornment (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 adornments (, , 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 Border 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.LineStyle; set { if (value != LineStyle.None) { Border.Thickness = new Thickness (1); } else { Border.Thickness = new Thickness (0); } Border.LineStyle = value; LayoutAdornments (); SetNeedsLayout (); } } /// /// 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 Rectangle 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 // BUGBUG: I think there's a bug here. This should be && not || if (Margin is null || Border is null || Padding is null) { return new Rectangle (default (Point), 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 Rectangle (Point.Empty, new Size (width, height)); } 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 Rectangle ( 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 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 = new Rectangle (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*/) { LayoutAdornments (); SetTextFormatterSize (); SetNeedsLayout (); SetNeedsDisplay (); } } } /// 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 (); } } /// /// 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; } } /// /// The frame (specified as a ) that separates a View from other SubViews of the same /// SuperView. The margin offsets the from the . /// /// /// /// The adornments (, , and ) are not part of the /// View's content and are not clipped by the View's Clip Area. /// /// /// Changing the size of an adornment (, , or ) will /// change the size of and trigger to update the layout of the /// and its . /// /// public Margin Margin { get; private set; } /// /// The frame (specified as a ) inside of the view that offsets the /// from the . /// /// /// /// The adornments (, , 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 Padding Padding { get; private set; } /// 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; } /// 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 (); } } /// 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 (); } } internal bool LayoutNeeded { get; private set; } = true; /// /// 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; /// Converts a -relative region to a screen-relative region. public Rectangle BoundsToScreen (Rectangle region) { BoundsToScreen (region.X, region.Y, out int x, out int y, false); return new Rectangle (x, y, region.Width, region.Height); } /// /// 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) { Point boundsOffset = GetBoundsOffset (); rx = x + Frame.X + boundsOffset.X; ry = y + Frame.Y + boundsOffset.Y; View super = SuperView; while (super is { }) { 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); } } #nullable enable /// 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 resultX, out int resultY) { resultY = resultX = 0; if (start is null || !start.Frame.Contains (x, y)) { return null; } Rectangle startFrame = start.Frame; if (start.InternalSubviews is { }) { int count = start.InternalSubviews.Count; if (count > 0) { Point 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--) { View v = start.InternalSubviews [i]; if (v.Visible && v.Frame.Contains (rx, ry)) { View? deep = FindDeepestView (v, rx, ry, out resultX, out resultY); if (deep is null) { return v; } return deep; } } } } resultX = x - startFrame.X; resultY = y - startFrame.Y; return start; } #nullable restore /// 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 { }) { Point boundsOffset = super.GetBoundsOffset (); ret.X += super.Frame.X + boundsOffset.X; ret.Y += super.Frame.Y + boundsOffset.Y; super = super.SuperView; } return ret; } /// /// Gets the thickness describing the sum of the Adornments' thicknesses. /// /// A thickness that describes the sum of the Adornments' thicknesses. public Thickness GetAdornmentsThickness () { 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 () { return new Point ( Padding?.Thickness.GetInside (Padding.Frame).X ?? 0, Padding?.Thickness.GetInside (Padding.Frame).Y ?? 0 ); } /// 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 = Bounds; OnLayoutStarted (new LayoutEventArgs { 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 Rectangle (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 is { } && GetTopSuperView () is { } && LayoutNeeded && edges.Count > 0) { foreach ((View from, View to) in edges) { LayoutSubview (to, from.Frame); } } LayoutNeeded = false; OnLayoutComplete (new LayoutEventArgs { OldBounds = oldBounds }); } /// 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) { Point screen = ScreenToFrame (x, y); Point boundsOffset = GetBoundsOffset (); return new Point (screen.X - boundsOffset.X, screen.Y - boundsOffset.Y); } /// /// 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 is { }) { Point superFrame = SuperView.ScreenToFrame (x - superViewBoundsOffset.X, y - superViewBoundsOffset.Y); ret = new Point (superFrame.X - Frame.X, superFrame.Y - Frame.Y); } return ret; } /// Indicates that the view does not need to be laid out. protected void ClearLayoutNeeded () { LayoutNeeded = false; } 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; } } /// /// This internal method is overridden by Adornment to do nothing to prevent recursion during View construction. /// And, because Adornments don't have Adornments. It's internal to support unit tests. /// /// /// /// internal virtual Adornment CreateAdornment (Type adornmentType) { void ThicknessChangedHandler (object sender, EventArgs e) { if (IsInitialized) { LayoutAdornments (); } SetNeedsLayout (); SetNeedsDisplay (); } Adornment adornment; adornment = Activator.CreateInstance (adornmentType, this) as Adornment; adornment.ThicknessChanged += ThicknessChangedHandler; return adornment; } /// Overriden by to do nothing, as the does not have adornments. internal virtual void LayoutAdornments () { if (Margin is null) { return; // CreateAdornments () has not been called yet } if (Margin.Frame.Size != Frame.Size) { Margin._frame = new Rectangle (Point.Empty, Frame.Size); Margin.X = 0; Margin.Y = 0; Margin.Width = Frame.Size.Width; Margin.Height = Frame.Size.Height; Margin.SetNeedsLayout (); Margin.SetNeedsDisplay (); } Rectangle border = Margin.Thickness.GetInside (Margin.Frame); if (border != Border.Frame) { Border._frame = new Rectangle (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 (); } Rectangle padding = Border.Thickness.GetInside (Border.Frame); if (padding != Padding.Frame) { Padding._frame = new Rectangle (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 (); } } /// /// 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.Bounds, then Application.Top, then Driver.Bounds. // Finally, if none of those are valid, use int.MaxValue (for Unit tests). Rectangle relativeBounds = SuperView is { IsInitialized: true } ? SuperView.Bounds : Application.Top is { } && Application.Top.IsInitialized ? Application.Top.Bounds : Application.Driver?.Bounds ?? 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 (); SetNeedsLayout (); SetNeedsDisplay (); } } /// /// 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 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 (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 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, 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); var r = new Rectangle (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 = new Rectangle (Frame.Location, autosize); if (autosize.Width == 0) { _width = 0; } if (autosize.Height == 0) { _height = 0; } } else if (!SetFrameToFitText ()) { SetTextFormatterSize (); } SetNeedsLayout (); SetNeedsDisplay (); } } // 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 /// 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; } private void LayoutSubview (View v, Rectangle contentArea) { //if (v.LayoutStyle == LayoutStyle.Computed) { v.SetRelativeLayout (contentArea); //} v.LayoutSubviews (); v.LayoutNeeded = false; } /// 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 Bounds 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) { Bounds = new Rectangle (Bounds.X, Bounds.Y, canSizeW ? rW : Bounds.Width, canSizeH ? rH : Bounds.Height); } return boundsChanged; } /// 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; } // 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; } }