From 064262bed6be1b831a22375f2546543abe34ec16 Mon Sep 17 00:00:00 2001 From: Tig Date: Mon, 11 Mar 2024 11:10:17 -0800 Subject: [PATCH] Simplified and made FindDeepestView more efficient --- Terminal.Gui/Application.cs | 4 + Terminal.Gui/View/Adornment/Adornment.cs | 15 ++ Terminal.Gui/View/Layout/ViewLayout.cs | 327 ++++------------------- Terminal.Gui/View/ViewAdornments.cs | 225 ++++++++++++++++ 4 files changed, 295 insertions(+), 276 deletions(-) create mode 100644 Terminal.Gui/View/ViewAdornments.cs diff --git a/Terminal.Gui/Application.cs b/Terminal.Gui/Application.cs index fec48df0e..b2d2cbe36 100644 --- a/Terminal.Gui/Application.cs +++ b/Terminal.Gui/Application.cs @@ -490,6 +490,8 @@ public static partial class Application Toplevel.SetRelativeLayout (Driver.Bounds); //} + + // BUGBUG: This call is likley not needed. Toplevel.LayoutSubviews (); Toplevel.PositionToplevels (); Toplevel.FocusFirst (); @@ -1029,6 +1031,8 @@ public static partial class Application runState.Toplevel?.Dispose (); runState.Toplevel = null; runState.Dispose (); + + // BUGBUG: Application.Top is now invalid?!?! } #endregion Run (Begin, Run, End) diff --git a/Terminal.Gui/View/Adornment/Adornment.cs b/Terminal.Gui/View/Adornment/Adornment.cs index 072da9ca9..2d88839ac 100644 --- a/Terminal.Gui/View/Adornment/Adornment.cs +++ b/Terminal.Gui/View/Adornment/Adornment.cs @@ -191,6 +191,21 @@ public class Adornment : View #region Mouse Support + + /// + /// Indicates whether the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. + /// + /// + /// + /// if the specified Parent's SuperView-relative coordinates are within the Adornment's Thickness. + public override bool Contains (int x, int y) + { + Rectangle frame = Frame; + frame.Offset (Parent.Frame.Location); + + return Thickness.Contains (frame, x, y); + } + private Point? _dragPosition; private Point _startGrabPoint; diff --git a/Terminal.Gui/View/Layout/ViewLayout.cs b/Terminal.Gui/View/Layout/ViewLayout.cs index 7f9fe8798..d36482ea1 100644 --- a/Terminal.Gui/View/Layout/ViewLayout.cs +++ b/Terminal.Gui/View/Layout/ViewLayout.cs @@ -92,16 +92,16 @@ public partial class View while (super is { }) { - Point boundsOffset = super.GetBoundsOffset (); if (super is Adornment adornment) { // Adornments don't have SuperViews; use Adornment.FrameToScreen override ret = adornment.FrameToScreen (); ret.Offset (Frame.X, Frame.Y); - boundsOffset = GetBoundsOffset (); - //ret.Offset(-boundsOffset.X, -boundsOffset.Y); + return ret; } + + Point boundsOffset = super.GetBoundsOffset (); ret.X += super.Frame.X + boundsOffset.X; ret.Y += super.Frame.Y + boundsOffset.Y; super = super.SuperView; @@ -286,7 +286,7 @@ public partial class View #region Bounds /// - /// The bounds represent the View-relative rectangle used for this view; the area inside of the view where + /// The bounds represent the View-relative rectangle used for this view; the area inside 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. @@ -325,14 +325,18 @@ public partial class View 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 (); - int width = Math.Max (0, Frame.Size.Width - totalThickness.Horizontal); - int height = Math.Max (0, Frame.Size.Height - totalThickness.Vertical); - return Rectangle.Empty with { Size = new (width, height) }; + return Rectangle.Empty with + { + Size = new ( + Math.Max (0, Frame.Size.Width - totalThickness.Horizontal), + Math.Max (0, Frame.Size.Height - totalThickness.Vertical)) + }; } set { @@ -350,7 +354,9 @@ public partial class View Frame = Frame with { - Size = new (value.Size.Width + totalThickness.Horizontal, value.Size.Height + totalThickness.Vertical) + Size = new ( + value.Size.Width + totalThickness.Horizontal, + value.Size.Height + totalThickness.Vertical) }; } } @@ -359,9 +365,8 @@ public partial class View public Rectangle BoundsToScreen (in Rectangle bounds) { // Translate bounds to Frame (our SuperView's Bounds-relative coordinates) - Point boundsOffset = GetBoundsOffset (); - Rectangle screen = FrameToScreen (); + Point boundsOffset = GetBoundsOffset (); screen.Offset (boundsOffset.X + bounds.X, boundsOffset.Y + bounds.Y); return new (screen.Location, bounds.Size); @@ -373,249 +378,21 @@ public partial class View /// Screen-relative row. public Point ScreenToBounds (int x, int y) { - Point screen = ScreenToFrame (x, y); Point boundsOffset = GetBoundsOffset (); + Point screen = ScreenToFrame (x, y); + screen.Offset (-boundsOffset.X, -boundsOffset.Y); - return new (screen.X - boundsOffset.X, screen.Y - boundsOffset.Y); + return screen; } /// /// 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 virtual Point GetBoundsOffset () - { - if (Padding is null) - { - return Point.Empty; - } - - return Padding.Thickness.GetInside (Padding.Frame).Location; - } + public Point GetBoundsOffset () { return Padding is null ? Point.Empty : Padding.Thickness.GetInside (Padding.Frame).Location; } #endregion Bounds - #region Adornments - - private void CreateAdornments () - { - Margin = CreateAdornment (typeof (Margin)) as Margin; - Border = CreateAdornment (typeof (Border)) as Border; - Padding = CreateAdornment (typeof (Padding)) as Padding; - } - - // TODO: Move this to Adornment as a static factory method - /// - /// 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; - } - - private void BeginInitAdornments () - { - Margin?.BeginInit (); - Border?.BeginInit (); - Padding?.BeginInit (); - } - - private void EndInitAdornments () - { - Margin?.EndInit (); - Border?.EndInit (); - Padding?.EndInit (); - } - - private void DisposeAdornments () - { - Margin?.Dispose (); - Margin = null; - Border?.Dispose (); - Border = null; - Padding?.Dispose (); - Padding = null; - } - - /// - /// The that enables separation of 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 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 ?? LineStyle.Single; - set - { - if (Border is null) - { - return; - } - - if (value != LineStyle.None) - { - Border.Thickness = new (1); - } - else - { - Border.Thickness = new (0); - } - - Border.LineStyle = value; - LayoutAdornments (); - SetNeedsLayout (); - } - } - - /// - /// The 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 the thickness describing the sum of the Adornments' thicknesses. - /// - /// A thickness that describes the sum of the Adornments' thicknesses. - public Thickness GetAdornmentsThickness () { return Margin.Thickness + Border.Thickness + Padding.Thickness; } - - /// 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 = Rectangle.Empty with { Size = Frame.Size }; - Margin.X = 0; - Margin.Y = 0; - Margin.Width = Frame.Size.Width; - Margin.Height = Frame.Size.Height; - } - Margin.SetNeedsLayout (); - Margin.SetNeedsDisplay (); - - if (IsInitialized) - { - Margin.LayoutSubviews (); - } - - Rectangle border = Margin.Thickness.GetInside (Margin.Frame); - - if (border != Border.Frame) - { - Border._frame = border; - Border.X = border.Location.X; - Border.Y = border.Location.Y; - Border.Width = border.Size.Width; - Border.Height = border.Size.Height; - } - Border.SetNeedsLayout (); - Border.SetNeedsDisplay (); - if (IsInitialized) - { - Border.LayoutSubviews (); - } - - Rectangle padding = Border.Thickness.GetInside (Border.Frame); - - if (padding != Padding.Frame) - { - Padding._frame = padding; - Padding.X = padding.Location.X; - Padding.Y = padding.Location.Y; - Padding.Width = padding.Size.Width; - Padding.Height = padding.Size.Height; - } - Padding.SetNeedsLayout (); - Padding.SetNeedsDisplay (); - if (IsInitialized) - { - Padding.LayoutSubviews (); - } - } - - #endregion Adornments - #region AutoSize private bool _autoSize; @@ -871,52 +648,51 @@ public partial class View 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 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. + /// 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) - { - return null; - } - - if (!start.Frame.Contains (x, y)) + if (start is null || !start.Visible || !start.Contains (x, y)) { return null; } Adornment? found = null; - if (start.Margin.Thickness.Contains (start.Frame, x, y)) + + if (start.Margin.Contains (x, y)) { found = start.Margin; } - else if (start.Border.Thickness.Contains ( - start.Border.Frame with - { - X = start.Frame.X + start.Border.Frame.X, - Y = start.Frame.Y + start.Border.Frame.Y - }, - x, - y)) + else if (start.Border.Contains (x, y)) { found = start.Border; } - else if (start.Padding.Thickness.Contains ( - start.Padding.Frame with - { - X = start.Frame.X + start.Padding.Frame.X, - Y = start.Frame.Y + start.Padding.Frame.Y - }, - x, - y)) + else if (start.Padding.Contains (x, y)) { found = start.Padding; } @@ -931,18 +707,17 @@ public partial class View if (start.InternalSubviews is { Count: > 0 }) { - int rx = x - (start.Frame.X + boundsOffset.X); - int ry = y - (start.Frame.Y + boundsOffset.Y); + int startOffsetX = x - (start.Frame.X + boundsOffset.X); + int startOffsetY = y - (start.Frame.Y + boundsOffset.Y); for (int i = start.InternalSubviews.Count - 1; i >= 0; i--) { - View v = start.InternalSubviews [i]; + View nextStart = start.InternalSubviews [i]; - if (v.Visible && v.Frame.Contains (rx, ry)) + if (nextStart.Visible && nextStart.Contains (startOffsetX, startOffsetY)) { - View? deep = FindDeepestView (v, rx, ry); - - return deep ?? v; + // TODO: Remove recursion + return FindDeepestView (nextStart, startOffsetX, startOffsetY) ?? nextStart; } } } diff --git a/Terminal.Gui/View/ViewAdornments.cs b/Terminal.Gui/View/ViewAdornments.cs new file mode 100644 index 000000000..91f6e1505 --- /dev/null +++ b/Terminal.Gui/View/ViewAdornments.cs @@ -0,0 +1,225 @@ +namespace Terminal.Gui; +public partial class View +{ + private void CreateAdornments () + { + Margin = CreateAdornment (typeof (Margin)) as Margin; + Border = CreateAdornment (typeof (Border)) as Border; + Padding = CreateAdornment (typeof (Padding)) as Padding; + } + + // TODO: Move this to Adornment as a static factory method + /// + /// 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; + } + + private void BeginInitAdornments () + { + Margin?.BeginInit (); + Border?.BeginInit (); + Padding?.BeginInit (); + } + + private void EndInitAdornments () + { + Margin?.EndInit (); + Border?.EndInit (); + Padding?.EndInit (); + } + + private void DisposeAdornments () + { + Margin?.Dispose (); + Margin = null; + Border?.Dispose (); + Border = null; + Padding?.Dispose (); + Padding = null; + } + + /// + /// The that enables separation of 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 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 ?? LineStyle.Single; + set + { + if (Border is null) + { + return; + } + + if (value != LineStyle.None) + { + Border.Thickness = new (1); + } + else + { + Border.Thickness = new (0); + } + + Border.LineStyle = value; + LayoutAdornments (); + SetNeedsLayout (); + } + } + + /// + /// The 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 the thickness describing the sum of the Adornments' thicknesses. + /// + /// A thickness that describes the sum of the Adornments' thicknesses. + public Thickness GetAdornmentsThickness () { return Margin.Thickness + Border.Thickness + Padding.Thickness; } + + /// 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 = Rectangle.Empty with { Size = Frame.Size }; + Margin.X = 0; + Margin.Y = 0; + Margin.Width = Frame.Size.Width; + Margin.Height = Frame.Size.Height; + } + + Margin.SetNeedsLayout (); + Margin.SetNeedsDisplay (); + + if (IsInitialized) + { + Margin.LayoutSubviews (); + } + + Rectangle border = Margin.Thickness.GetInside (Margin.Frame); + + if (border != Border.Frame) + { + Border._frame = border; + Border.X = border.Location.X; + Border.Y = border.Location.Y; + Border.Width = border.Size.Width; + Border.Height = border.Size.Height; + } + + Border.SetNeedsLayout (); + Border.SetNeedsDisplay (); + + if (IsInitialized) + { + Border.LayoutSubviews (); + } + + Rectangle padding = Border.Thickness.GetInside (Border.Frame); + + if (padding != Padding.Frame) + { + Padding._frame = padding; + Padding.X = padding.Location.X; + Padding.Y = padding.Location.Y; + Padding.Width = padding.Size.Width; + Padding.Height = padding.Size.Height; + } + + Padding.SetNeedsLayout (); + Padding.SetNeedsDisplay (); + + if (IsInitialized) + { + Padding.LayoutSubviews (); + } + } +}