Jelajahi Sumber

WIP - prototyping...

Tig 9 bulan lalu
induk
melakukan
d104a56e42

+ 8 - 12
Terminal.Gui/Application/Application.Run.cs

@@ -80,14 +80,14 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     {
         ArgumentNullException.ThrowIfNull (toplevel);
 
-//#if DEBUG_IDISPOSABLE
-//        Debug.Assert (!toplevel.WasDisposed);
+        //#if DEBUG_IDISPOSABLE
+        //        Debug.Assert (!toplevel.WasDisposed);
 
-//        if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
-//        {
-//            Debug.Assert (_cachedRunStateToplevel.WasDisposed);
-//        }
-//#endif
+        //        if (_cachedRunStateToplevel is { } && _cachedRunStateToplevel != toplevel)
+        //        {
+        //            Debug.Assert (_cachedRunStateToplevel.WasDisposed);
+        //        }
+        //#endif
 
         // Ensure the mouse is ungrabbed.
         MouseGrabView = null;
@@ -491,11 +491,7 @@ public static partial class Application // Run (Begin, Run, End, Stop)
     {
         foreach (Toplevel tl in TopLevels.Reverse ())
         {
-            if (tl.LayoutNeeded)
-            {
-                tl.LayoutSubviews ();
-            }
-
+            tl.LayoutSubviews ();
             tl.Draw ();
         }
 

+ 9 - 17
Terminal.Gui/View/Adornment/Adornment.cs

@@ -55,16 +55,9 @@ public class Adornment : View
 
             if (current != _thickness)
             {
-                if (Parent?.IsInitialized == false)
-                {
-                    // When initialized Parent.LayoutSubViews will cause a LayoutAdornments
-                    Parent?.LayoutAdornments ();
-                }
-                else
-                {
-                    Parent?.SetNeedsLayout ();
-                    Parent?.LayoutSubviews ();
-                }
+                Parent?.SetAdornmentFrames ();
+                Parent?.SetLayoutNeeded ();
+                //Parent?.LayoutSubviews ();
 
                 OnThicknessChanged ();
             }
@@ -101,10 +94,10 @@ public class Adornment : View
     //    return null;
     //}
 
-    internal override void LayoutAdornments ()
-    {
-        /* Do nothing - Adornments do not have Adornments */
-    }
+    //internal override void LayoutAdornments ()
+    //{
+    //    /* Do nothing - Adornments do not have Adornments */
+    //}
 
     /// <summary>
     ///     Gets the rectangle that describes the area of the Adornment. The Location is always (0,0).
@@ -182,10 +175,9 @@ public class Adornment : View
 
         if (Driver is { })
         {
-           Driver.Clip = prevClip;
+            Driver.Clip = prevClip;
         }
 
-        ClearLayoutNeeded ();
         ClearNeedsDisplay ();
     }
 
@@ -199,7 +191,7 @@ public class Adornment : View
     /// </summary>
     public override bool SuperViewRendersLineCanvas
     {
-        get => false; 
+        get => false;
         set => throw new InvalidOperationException (@"Adornment can only render to their Parent or Parent's Superview.");
     }
 

+ 2 - 2
Terminal.Gui/View/Adornment/Margin.cs

@@ -117,12 +117,12 @@ public class Margin : Adornment
             {
                 IEnumerable<View> subviewsNeedingDraw = Subviews.Where (
                                                                         view => view.Visible
-                                                                                && (view.NeedsDisplay || view.SubViewNeedsDisplay || view.LayoutNeeded)
+                                                                                && (view.NeedsDisplay || view.SubViewNeedsDisplay || view.IsLayoutNeeded ())
                                                                        );
 
                 foreach (View view in subviewsNeedingDraw)
                 {
-                    if (view.LayoutNeeded)
+                    if (view.IsLayoutNeeded ())
                     {
                         view.LayoutSubviews ();
                     }

+ 2 - 2
Terminal.Gui/View/Layout/Dim.cs

@@ -251,7 +251,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
         }
 
         var newDim = new DimCombine (AddOrSubtract.Add, left, right);
-        (left as DimView)?.Target?.SetNeedsLayout ();
+        (left as DimView)?.Target?.SetLayoutNeeded ();
 
         return newDim;
     }
@@ -276,7 +276,7 @@ public abstract record Dim : IEqualityOperators<Dim, Dim, bool>
         }
 
         var newDim = new DimCombine (AddOrSubtract.Subtract, left, right);
-        (left as DimView)?.Target?.SetNeedsLayout ();
+        (left as DimView)?.Target?.SetLayoutNeeded ();
 
         return newDim;
     }

+ 2 - 2
Terminal.Gui/View/Layout/Pos.cs

@@ -366,7 +366,7 @@ public abstract record Pos
 
         if (left is PosView view)
         {
-            view.Target?.SetNeedsLayout ();
+            view.Target?.SetLayoutNeeded ();
         }
 
         return newPos;
@@ -395,7 +395,7 @@ public abstract record Pos
 
         if (left is PosView view)
         {
-            view.Target?.SetNeedsLayout ();
+            view.Target?.SetLayoutNeeded ();
         }
 
         return newPos;

+ 12 - 60
Terminal.Gui/View/View.Adornments.cs

@@ -163,8 +163,8 @@ public partial class View // Adornments
         }
 
         SetBorderStyle (e.NewValue);
-        LayoutAdornments ();
-        SetNeedsLayout ();
+        SetAdornmentFrames ();
+        SetLayoutNeeded ();
     }
 
     /// <summary>
@@ -243,70 +243,22 @@ public partial class View // Adornments
         return Margin.Thickness + Border.Thickness + Padding.Thickness;
     }
 
-    /// <summary>Lays out the Adornments of the View.</summary>
-    /// <remarks>
-    ///     Overriden by <see cref="Adornment"/> to do nothing, as <see cref="Adornment"/> does not have adornments.
-    /// </remarks>
-    internal virtual void LayoutAdornments ()
+    /// <summary>Sets the Frame's of the Margin, Border, and Padding.</summary>
+    internal void SetAdornmentFrames ()
     {
-        if (Margin is null)
-        {
-            return; // CreateAdornments () has not been called yet
-        }
-
-        if (Margin.Frame.Size != Frame.Size)
-        {
-            Margin.SetFrame (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)
+        if (this is Adornment)
         {
-            Border.SetFrame (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 ();
+            // Adornments do not have Adornments
+            return;
         }
 
-        Rectangle padding = Border.Thickness.GetInside (Border.Frame);
-
-        if (padding != Padding.Frame)
+        if (Margin is null)
         {
-            Padding.SetFrame (padding);
-            Padding.X = padding.Location.X;
-            Padding.Y = padding.Location.Y;
-            Padding.Width = padding.Size.Width;
-            Padding.Height = padding.Size.Height;
+            return; // CreateAdornments () has not been called yet
         }
 
-        Padding.SetNeedsLayout ();
-        Padding.SetNeedsDisplay ();
-
-        if (IsInitialized)
-        {
-            Padding.LayoutSubviews ();
-        }
+        Margin.SetFrame (Rectangle.Empty with { Size = Frame.Size });
+        Border.SetFrame (Margin.Thickness.GetInside (Margin.Frame));
+        Padding.SetFrame (Border.Thickness.GetInside (Border.Frame));
     }
 }

+ 2 - 5
Terminal.Gui/View/View.Content.cs

@@ -151,10 +151,7 @@ public partial class View
 
         if (e.Cancel != true)
         {
-            OnResizeNeeded ();
-
-            //SetNeedsLayout ();
-            //SetNeedsDisplay ();
+            SetLayoutNeeded ();
         }
 
         return e.Cancel;
@@ -311,7 +308,7 @@ public partial class View
             if (_viewportLocation != viewport.Location)
             {
                 _viewportLocation = viewport.Location;
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
             }
 
             OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));

+ 31 - 34
Terminal.Gui/View/View.Drawing.cs

@@ -1,4 +1,6 @@
 #nullable enable
+using System.Diagnostics;
+
 namespace Terminal.Gui;
 
 public partial class View // Drawing APIs
@@ -223,7 +225,7 @@ public partial class View // Drawing APIs
             SetNeedsDisplay ();
         }
 
-        if (!NeedsDisplay && !SubViewNeedsDisplay && !LayoutNeeded)
+        if (!NeedsDisplay && !SubViewNeedsDisplay && !IsLayoutNeeded ())
         {
             return;
         }
@@ -270,8 +272,6 @@ public partial class View // Drawing APIs
         // Invoke DrawContentCompleteEvent
         OnDrawContentComplete (Viewport);
 
-        // BUGBUG: v2 - We should be able to use View.SetClip here and not have to resort to knowing Driver details.
-        ClearLayoutNeeded ();
         ClearNeedsDisplay ();
     }
 
@@ -520,39 +520,34 @@ public partial class View // Drawing APIs
     /// </param>
     public virtual void OnDrawContent (Rectangle viewport)
     {
-        if (NeedsDisplay)
+        if (!CanBeVisible (this))
         {
-            if (!CanBeVisible (this))
-            {
-                return;
-            }
+            return;
+        }
 
-            // BUGBUG: this clears way too frequently. Need to optimize this.
-            if (SuperView is { } || Arrangement.HasFlag (ViewArrangement.Overlapped))
-            {
-                Clear ();
-            }
+        // BUGBUG: this clears way too frequently. Need to optimize this.
+        if (SuperView is { } || Arrangement.HasFlag (ViewArrangement.Overlapped))
+        {
+            Clear ();
+        }
 
-            if (!string.IsNullOrEmpty (TextFormatter.Text))
-            {
-                if (TextFormatter is { })
-                {
-                    TextFormatter.NeedsFormat = true;
-                }
-            }
+        if (!string.IsNullOrEmpty (TextFormatter.Text))
+        {
+            TextFormatter.NeedsFormat = true;
+        }
 
-            // This should NOT clear 
-            // TODO: If the output is not in the Viewport, do nothing
-            var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ());
+        // This should NOT clear 
+        // TODO: If the output is not in the Viewport, do nothing
+        var drawRect = new Rectangle (ContentToScreen (Point.Empty), GetContentSize ());
+
+        TextFormatter?.Draw (
+                             drawRect,
+                             HasFocus ? GetFocusColor () : GetNormalColor (),
+                             HasFocus ? GetHotFocusColor () : GetHotNormalColor (),
+                             Rectangle.Empty
+                            );
+        SetSubViewNeedsDisplay ();
 
-            TextFormatter?.Draw (
-                                 drawRect,
-                                 HasFocus ? GetFocusColor () : GetNormalColor (),
-                                 HasFocus ? GetHotFocusColor () : GetHotNormalColor (),
-                                 Rectangle.Empty
-                                );
-            SetSubViewNeedsDisplay ();
-        }
 
         // TODO: Move drawing of subviews to a separate OnDrawSubviews virtual method
         // Draw subviews
@@ -563,14 +558,15 @@ public partial class View // Drawing APIs
                                                                      view => view.Visible
                                                                              && (view.NeedsDisplay
                                                                                  || view.SubViewNeedsDisplay
-                                                                                 || view.LayoutNeeded
+                                                                                 || view.IsLayoutNeeded ()
                                                                                  || view.Arrangement.HasFlag (ViewArrangement.Overlapped)
-                                                                    ));
+                                                                                ));
 
             foreach (View view in subviewsNeedingDraw)
             {
-                if (view.LayoutNeeded)
+                if (view.IsLayoutNeeded ())
                 {
+                    Debug.WriteLine ("Layout should be de-coupled from drawing");
                     view.LayoutSubviews ();
                 }
 
@@ -583,6 +579,7 @@ public partial class View // Drawing APIs
 
                 view.Draw ();
             }
+
         }
     }
 

+ 2 - 4
Terminal.Gui/View/View.Hierarchy.cs

@@ -86,8 +86,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         }
 
         CheckDimAuto ();
-        SetNeedsLayout ();
-        SetNeedsDisplay ();
+        view.SetRelativeLayout(GetContentSize());
 
         return view;
     }
@@ -126,7 +125,6 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
     {
         View view = e.SubView;
         view.IsAdded = true;
-        view.OnResizeNeeded ();
         view.Added?.Invoke (this, e);
     }
 
@@ -178,7 +176,7 @@ public partial class View // SuperView/SubView hierarchy management (SuperView,
         }
         view._superView = null;
 
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
         SetNeedsDisplay ();
 
         foreach (View v in _subviews)

+ 106 - 72
Terminal.Gui/View/View.Layout.cs

@@ -38,8 +38,8 @@ public partial class View // Layout APIs
         int targetY,
         out int nx,
         out int ny
-       //,
-       // out StatusBar? statusBar
+    //,
+    // out StatusBar? statusBar
     )
     {
         int maxDimension;
@@ -205,6 +205,7 @@ public partial class View // Layout APIs
                 return;
             }
 
+            // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
             SetFrame (value with { Width = Math.Max (value.Width, 0), Height = Math.Max (value.Height, 0) });
 
             // If Frame gets set, set all Pos/Dim to Absolute values.
@@ -213,17 +214,21 @@ public partial class View // Layout APIs
             _width = _frame.Width;
             _height = _frame.Height;
 
-            if (IsInitialized)
-            {
-                OnResizeNeeded ();
-            }
-
-            SetNeedsDisplay ();
+            SetLayoutNeeded ();
         }
     }
 
+    /// <summary>
+    ///     INTERNAL API - Sets _frame, calls SetsNeedsLayout, and raises OnViewportChanged/ViewportChanged
+    /// </summary>
+    /// <param name="frame"></param>
     private void SetFrame (in Rectangle frame)
     {
+        if (_frame == frame)
+        {
+            return;
+        }
+
         var oldViewport = Rectangle.Empty;
 
         if (IsInitialized)
@@ -234,6 +239,11 @@ public partial class View // Layout APIs
         // This is the only place where _frame should be set directly. Use Frame = or SetFrame instead.
         _frame = frame;
 
+        SetAdornmentFrames ();
+
+        SetLayoutNeeded ();
+
+        // BUGBUG: When SetFrame is called from Frame_set, this event gets raised BEFORE OnResizeNeeded. Is that OK?
         OnViewportChanged (new (IsInitialized ? Viewport : Rectangle.Empty, oldViewport));
     }
 
@@ -333,7 +343,13 @@ public partial class View // Layout APIs
 
             _x = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (X)} cannot be null");
 
-            OnResizeNeeded ();
+            if (!IsInitialized)
+            {
+                // Supports Unit Tests that don't call Begin/EndInit
+                SetRelativeLayout (GetBestGuessSuperViewContentSize ());
+            }
+
+            SetLayoutNeeded ();
         }
     }
 
@@ -375,7 +391,14 @@ public partial class View // Layout APIs
             }
 
             _y = value ?? throw new ArgumentNullException (nameof (value), @$"{nameof (Y)} cannot be null");
-            OnResizeNeeded ();
+
+            if (!IsInitialized)
+            {
+                // Supports Unit Tests that don't call Begin/EndInit
+                SetRelativeLayout (GetBestGuessSuperViewContentSize ());
+            }
+
+            SetLayoutNeeded ();
         }
     }
 
@@ -428,7 +451,13 @@ public partial class View // Layout APIs
             // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
             TextFormatter.ConstrainToHeight = null;
 
-            OnResizeNeeded ();
+            if (!IsInitialized)
+            {
+                // Supports Unit Tests that don't call Begin/EndInit
+                SetRelativeLayout (GetBestGuessSuperViewContentSize ());
+            }
+
+            SetLayoutNeeded ();
         }
     }
 
@@ -481,7 +510,13 @@ public partial class View // Layout APIs
             // Reset TextFormatter - Will be recalculated in SetTextFormatterSize
             TextFormatter.ConstrainToWidth = null;
 
-            OnResizeNeeded ();
+            if (!IsInitialized)
+            {
+                // Supports Unit Tests that don't call Begin/EndInit
+                SetRelativeLayout (GetBestGuessSuperViewContentSize ());
+            }
+
+            SetLayoutNeeded ();
         }
     }
 
@@ -520,6 +555,9 @@ public partial class View // Layout APIs
     ///         other subviews, <see cref="LayoutSubview"/> on
     ///         will be called for that subview.
     ///     </para>
+    ///     <para>
+    ///         Some subviews may have SetRelativeLayout called on them as a side effect, particularly in DimAuto scenarios.
+    ///     </para>
     /// </remarks>
     /// <param name="superviewContentSize">
     ///     The size of the SuperView's content (nominally the same as <c>this.SuperView.GetContentSize ()</c>).
@@ -565,6 +603,7 @@ public partial class View // Layout APIs
         if (Frame != newFrame)
         {
             // Set the frame. Do NOT use `Frame` as it overwrites X, Y, Width, and Height
+            // This will set _frame, call SetsNeedsLayout, and raise OnViewportChanged/ViewportChanged
             SetFrame (newFrame);
 
             if (_x is PosAbsolute)
@@ -591,9 +630,6 @@ public partial class View // Layout APIs
             {
                 SetTitleTextFormatterSize ();
             }
-
-            SetNeedsLayout ();
-            SetNeedsDisplay ();
         }
 
         if (TextFormatter.ConstrainToWidth is null)
@@ -607,6 +643,7 @@ public partial class View // Layout APIs
         }
     }
 
+    // TODO: remove virtual from this
     /// <summary>
     ///     Invoked when the dimensions of the view have changed, for example in response to the container view or terminal
     ///     resizing.
@@ -629,7 +666,7 @@ public partial class View // Layout APIs
             Debug.WriteLine ($"WARNING: LayoutSubviews called before view has been initialized. This is likely a bug in {this}");
         }
 
-        if (!LayoutNeeded)
+        if (!IsLayoutNeeded ())
         {
             return;
         }
@@ -639,7 +676,9 @@ public partial class View // Layout APIs
         Size contentSize = GetContentSize ();
         OnLayoutStarted (new (contentSize));
 
-        LayoutAdornments ();
+        Margin?.LayoutSubviews ();
+        Border?.LayoutSubviews ();
+        Padding?.LayoutSubviews ();
 
         // Sort out the dependencies of the X, Y, Width, Height properties
         HashSet<View> nodes = new ();
@@ -654,7 +693,7 @@ public partial class View // Layout APIs
 
         // If the 'to' is rooted to 'from' it's a special-case.
         // Use LayoutSubview with the Frame of the 'from'.
-        if (SuperView is { } && GetTopSuperView () is { } && LayoutNeeded && edges.Count > 0)
+        if (SuperView is { } && GetTopSuperView () is { } && IsLayoutNeeded () && edges.Count > 0)
         {
             foreach ((View from, View to) in edges)
             {
@@ -662,7 +701,7 @@ public partial class View // Layout APIs
             }
         }
 
-        LayoutNeeded = false;
+        _layoutNeeded = false;
 
         OnLayoutComplete (new (contentSize));
     }
@@ -672,12 +711,8 @@ public partial class View // Layout APIs
         // Note, SetRelativeLayout calls SetTextFormatterSize
         v.SetRelativeLayout (contentSize);
         v.LayoutSubviews ();
-        v.LayoutNeeded = false;
     }
 
-    /// <summary>Indicates that the view does not need to be laid out.</summary>
-    protected void ClearLayoutNeeded () { LayoutNeeded = false; }
-
     /// <summary>
     ///     Raises the <see cref="LayoutComplete"/> event. Called from  <see cref="LayoutSubviews"/> before all sub-views
     ///     have been laid out.
@@ -690,77 +725,76 @@ public partial class View // Layout APIs
     /// </summary>
     internal virtual void OnLayoutStarted (LayoutEventArgs args) { LayoutStarted?.Invoke (this, args); }
 
-    /// <summary>
-    ///     Called whenever the view needs to be resized. This is called whenever <see cref="Frame"/>,
-    ///     <see cref="View.X"/>, <see cref="View.Y"/>, <see cref="View.Width"/>, or <see cref="View.Height"/> changes.
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         Determines the relative bounds of the <see cref="View"/> and its <see cref="Frame"/>s, and then calls
-    ///         <see cref="SetRelativeLayout"/> to update the view.
-    ///     </para>
-    /// </remarks>
-    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
 
-        // Determine our container's ContentSize -
-        //  First try SuperView.Viewport, then Application.Top, then Driver.Viewport.
-        //  Finally, if none of those are valid, use 2048 (for Unit tests).
-        Size superViewContentSize = SuperView is { IsInitialized: true } ? SuperView.GetContentSize () :
-                                    Application.Top is { } && Application.Top != this && Application.Top.IsInitialized ? Application.Top.GetContentSize () :
-                                    Application.Screen.Size;
+    // We expose no setter for this to ensure that the ONLY place it's changed is in SetNeedsLayout
+    private bool _layoutNeeded = true;
 
-        SetRelativeLayout (superViewContentSize);
+    /// <summary>
+    ///     Indicates the View's Frame or the relative layout of the View's subviews (including Adornments) have
+    ///     changed since the last time the View was laid out. Used to prevent <see cref="LayoutSubviews"/> from needlessly computing
+    ///     layout.
+    /// </summary>
+    /// <returns></returns>
+    internal bool IsLayoutNeeded () => _layoutNeeded;
 
-        if (IsInitialized)
+    /// <summary>
+    ///     INTERNAL API - Sets <see cref="_layoutNeeded"/> 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 <see cref="IsLayoutNeeded"/> set.
+    /// </summary>
+    internal void SetLayoutNeeded ()
+    {
+        if (IsLayoutNeeded ())
         {
-            LayoutAdornments ();
+            // Prevent infinite recursion
+            return;
         }
 
-        SetNeedsLayout ();
+        _layoutNeeded = true;
+
+        // Use a stack to avoid recursion
+        Stack<View> stack = new Stack<View> (Subviews);
 
-        // TODO: This ensures overlapped views are drawn correctly. However, this is inefficient.
-        // TODO: The correct fix is to implement non-rectangular clip regions: https://github.com/gui-cs/Terminal.Gui/issues/3413
-        if (Arrangement.HasFlag (ViewArrangement.Overlapped))
+        while (stack.Count > 0)
         {
-            foreach (Toplevel v in Application.TopLevels)
+            View current = stack.Pop ();
+            if (!current.IsLayoutNeeded ())
             {
-                if (v.Visible && v != this)
+                current._layoutNeeded = true;
+                foreach (View subview in current.Subviews)
                 {
-                    v.SetNeedsDisplay ();
+                    stack.Push (subview);
                 }
             }
         }
-    }
 
-    internal bool LayoutNeeded { get; private set; } = true;
+        TextFormatter.NeedsFormat = true;
 
-    /// <summary>
-    ///     Sets <see cref="LayoutNeeded"/> 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 <see cref="LayoutNeeded"/> set.
-    /// </summary>
-    internal void SetNeedsLayout ()
-    {
-        if (LayoutNeeded)
+        // Use a loop to avoid recursion for superview hierarchy
+        View? superView = SuperView;
+        while (superView != null && !superView.IsLayoutNeeded ())
         {
-            return;
+            superView._layoutNeeded = true;
+            superView = superView.SuperView;
         }
+    }
 
-        LayoutNeeded = true;
-
-        foreach (View view in Subviews)
-        {
-            view.SetNeedsLayout ();
-        }
+    /// <summary>
+    ///    INTERNAL API FOR UNIT TESTS - Gets the size of the SuperView's content (nominally the same as
+    ///    the SuperView's <see cref="GetContentSize ()"/>).
+    /// </summary>
+    /// <returns></returns>
+    private Size GetBestGuessSuperViewContentSize ()
+    {
+        Size superViewContentSize = SuperView?.GetContentSize () ??
+                                    (Application.Top is { } && Application.Top != this && Application.Top.IsInitialized
+                                         ? Application.Top.GetContentSize ()
+                                         : Application.Screen.Size);
 
-        TextFormatter.NeedsFormat = true;
-        SuperView?.SetNeedsLayout ();
+        return superViewContentSize;
     }
 
     /// <summary>
-    ///     Collects all views and their dependencies from a given starting view for layout purposes. Used by
+    ///     INTERNAL API - Collects all views and their dependencies from a given starting view for layout purposes. Used by
     ///     <see cref="TopologicalSort"/> to create an ordered list of views to layout.
     /// </summary>
     /// <param name="from">The starting view from which to collect dependencies.</param>

+ 3 - 3
Terminal.Gui/View/View.Text.cs

@@ -62,7 +62,7 @@ public partial class View // Text Property APIs
             _text = value;
 
             UpdateTextFormatterText ();
-            OnResizeNeeded ();
+            SetLayoutNeeded ();
 #if DEBUG
             if (_text is { } && string.IsNullOrEmpty (Id))
             {
@@ -92,7 +92,7 @@ public partial class View // Text Property APIs
         {
             TextFormatter.Alignment = value;
             UpdateTextFormatterText ();
-            OnResizeNeeded ();
+            SetLayoutNeeded ();
         }
     }
 
@@ -229,7 +229,7 @@ public partial class View // Text Property APIs
         {
             TextFormatter.ConstrainToWidth = null;
             TextFormatter.ConstrainToHeight = null;
-            OnResizeNeeded ();
+            SetLayoutNeeded ();
         }
 
         SetNeedsDisplay ();

+ 2 - 2
Terminal.Gui/View/View.cs

@@ -122,7 +122,7 @@ public partial class View : Responder, ISupportInitializeNotification
     ///     Points to the current driver in use by the view, it is a convenience property for simplifying the development
     ///     of new views.
     /// </summary>
-    public static ConsoleDriver Driver => Application.Driver!;
+    public static ConsoleDriver? Driver => Application.Driver;
 
     /// <summary>Initializes a new instance of <see cref="View"/>.</summary>
     /// <remarks>
@@ -229,7 +229,7 @@ public partial class View : Responder, ISupportInitializeNotification
         // These calls were moved from BeginInit as they access Viewport which is indeterminate until EndInit is called.
         UpdateTextDirection (TextDirection);
         UpdateTextFormatterText ();
-        OnResizeNeeded ();
+        SetLayoutNeeded ();
 
         if (_subviews is { })
         {

+ 2 - 2
Terminal.Gui/Views/Bar.cs

@@ -119,7 +119,7 @@ public class Bar : View, IOrientation, IDesignable
     /// <param name="newOrientation"></param>
     public void OnOrientationChanged (Orientation newOrientation)
     {
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
     }
     #endregion
 
@@ -135,7 +135,7 @@ public class Bar : View, IOrientation, IDesignable
         set
         {
             _alignmentModes = value;
-            SetNeedsLayout ();
+            SetLayoutNeeded ();
         }
     }
 

+ 1 - 1
Terminal.Gui/Views/Button.cs

@@ -191,7 +191,7 @@ public class Button : View, IDesignable
             _isDefault = value;
 
             UpdateTextFormatterText ();
-            OnResizeNeeded ();
+            SetLayoutNeeded ();
         }
     }
 

+ 1 - 1
Terminal.Gui/Views/CheckBox.cs

@@ -153,7 +153,7 @@ public class CheckBox : View
 
         _checkedState = value;
         UpdateTextFormatterText ();
-        OnResizeNeeded ();
+        SetLayoutNeeded ();
 
         EventArgs<CheckState> args = new (in _checkedState);
         OnCheckedStateChanged (args);

+ 2 - 2
Terminal.Gui/Views/ColorPicker.16.cs

@@ -28,7 +28,7 @@ public class ColorPicker16 : View
                 Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
                 Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
                 SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
             }
         }
     }
@@ -45,7 +45,7 @@ public class ColorPicker16 : View
                 Width = Dim.Auto (minimumContentDim: _boxWidth * _cols);
                 Height = Dim.Auto (minimumContentDim: _boxHeight * _rows);
                 SetContentSize (new (_boxWidth * _cols, _boxHeight * _rows));
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
             }
         }
     }

+ 1 - 1
Terminal.Gui/Views/ComboBox.cs

@@ -72,7 +72,7 @@ public class ComboBox : View, IDesignable
                          }
                      }
 
-                     SetNeedsLayout ();
+                     SetLayoutNeeded ();
                      SetNeedsDisplay ();
                      ShowHideList (Text);
                  };

+ 1 - 1
Terminal.Gui/Views/ScrollBarView.cs

@@ -225,7 +225,7 @@ public class ScrollBarView : View
 
             if (IsInitialized)
             {
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
 
                 if (value)
                 {

+ 3 - 3
Terminal.Gui/Views/ScrollView.cs

@@ -287,7 +287,7 @@ public class ScrollView : View
             if (value != _showHorizontalScrollIndicator)
             {
                 _showHorizontalScrollIndicator = value;
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
 
                 if (value)
                 {
@@ -322,7 +322,7 @@ public class ScrollView : View
             if (value != _showVerticalScrollIndicator)
             {
                 _showVerticalScrollIndicator = value;
-                SetNeedsLayout ();
+                SetLayoutNeeded ();
 
                 if (value)
                 {
@@ -367,7 +367,7 @@ public class ScrollView : View
             _contentView.Add (view);
         }
 
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
         return view;
     }
 

+ 4 - 4
Terminal.Gui/Views/Shortcut.cs

@@ -412,7 +412,7 @@ public class Shortcut : View, IOrientation, IDesignable
     public void OnOrientationChanged (Orientation newOrientation)
     {
         // TODO: Determine what, if anything, is opinionated about the orientation.
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
     }
 
     #endregion
@@ -679,9 +679,9 @@ public class Shortcut : View, IOrientation, IDesignable
 
             _minimumKeyTextSize = value;
             SetKeyViewDefaultLayout ();
-            CommandView.SetNeedsLayout ();
-            HelpView.SetNeedsLayout ();
-            KeyView.SetNeedsLayout ();
+            CommandView.SetLayoutNeeded ();
+            HelpView.SetLayoutNeeded ();
+            KeyView.SetLayoutNeeded ();
             SetSubViewNeedsDisplay ();
         }
     }

+ 1 - 0
Terminal.Gui/Views/TileView.cs

@@ -155,6 +155,7 @@ public class TileView : View
     /// <returns></returns>
     public bool IsRootTileView () { return _parentTileView == null; }
 
+    // TODO: Use OnLayoutStarted instead
     /// <inheritdoc/>
     public override void LayoutSubviews ()
     {

+ 2 - 2
Terminal.Gui/Views/Toplevel.cs

@@ -272,12 +272,12 @@ public partial class Toplevel : View
         //    layoutSubviews = true;
         //}
 
-        if (superView.LayoutNeeded || layoutSubviews)
+        if (superView.IsLayoutNeeded () || layoutSubviews)
         {
             superView.LayoutSubviews ();
         }
 
-        if (LayoutNeeded)
+        if (IsLayoutNeeded ())
         {
             LayoutSubviews ();
         }

+ 2 - 2
Terminal.Gui/Views/Wizard/Wizard.cs

@@ -92,7 +92,7 @@ public class Wizard : Dialog
         Closing += Wizard_Closing;
         TitleChanged += Wizard_TitleChanged;
 
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
     }
 
     /// <summary>
@@ -545,7 +545,7 @@ public class Wizard : Dialog
 
         SizeStep (CurrentStep);
 
-        SetNeedsLayout ();
+        SetLayoutNeeded ();
         LayoutSubviews ();
 
         //Draw ();

+ 2 - 2
UnitTests/Application/ApplicationTests.cs

@@ -859,10 +859,10 @@ public class ApplicationTests
                                          Application.RaiseMouseEvent (new () { Flags = MouseFlags.ReportMousePosition });
                                          Assert.False (top.NeedsDisplay);
                                          Assert.False (top.SubViewNeedsDisplay);
-                                         Assert.False (top.LayoutNeeded);
+                                         Assert.False (top.IsLayoutNeeded ());
                                          Assert.False (d.NeedsDisplay);
                                          Assert.False (d.SubViewNeedsDisplay);
-                                         Assert.False (d.LayoutNeeded);
+                                         Assert.False (d.IsLayoutNeeded ());
                                      }
                                      else
                                      {

+ 1 - 0
UnitTests/View/Layout/Dim.AutoTests.cs

@@ -314,6 +314,7 @@ public partial class DimAutoTests (ITestOutputHelper output)
         view.SetRelativeLayout (Application.Screen.Size);
         lastSize = view.Frame.Size;
         view.Text = "*ABCD";
+        view.SetRelativeLayout (Application.Screen.Size);
         Assert.NotEqual (lastSize, view.Frame.Size);
     }
 

+ 7 - 7
UnitTests/View/Layout/Dim.Tests.cs

@@ -185,7 +185,7 @@ public class DimTests
         testValview.Dispose ();
 
         testVal = new (1, 2, 3, 4);
-        testValview = new() { Frame = testVal };
+        testValview = new () { Frame = testVal };
         dim = Height (testValview);
         Assert.Equal ($"View(Height,View(){testVal})", dim.ToString ());
         testValview.Dispose ();
@@ -369,7 +369,7 @@ public class DimTests
                        Assert.Equal (18, v6.Frame.Height); // 89*20%=18
 
                        w.Width = 200;
-                       Assert.True (t.LayoutNeeded);
+                       Assert.True (t.IsLayoutNeeded ());
                        w.Height = 200;
                        t.LayoutSubviews ();
 
@@ -582,7 +582,7 @@ public class DimTests
         Assert.NotEqual (dim1, dim2);
 
         testRect1 = new (0, 1, 2, 3);
-        view1 = new() { Frame = testRect1 };
+        view1 = new () { Frame = testRect1 };
         testRect2 = new (0, 1, 2, 3);
         dim1 = Width (view1);
         dim2 = Width (view1);
@@ -591,7 +591,7 @@ public class DimTests
         Assert.Equal (dim1, dim2);
 
         testRect1 = new (0, -1, 2, 3);
-        view1 = new() { Frame = testRect1 };
+        view1 = new () { Frame = testRect1 };
         testRect2 = new (0, -1, 2, 3);
         dim1 = Width (view1);
         dim2 = Width (view1);
@@ -600,9 +600,9 @@ public class DimTests
         Assert.Equal (dim1, dim2);
 
         testRect1 = new (0, -1, 2, 3);
-        view1 = new() { Frame = testRect1 };
+        view1 = new () { Frame = testRect1 };
         testRect2 = Rectangle.Empty;
-        view2 = new() { Frame = testRect2 };
+        view2 = new () { Frame = testRect2 };
         dim1 = Width (view1);
         dim2 = Width (view2);
         Assert.NotEqual (dim1, dim2);
@@ -632,7 +632,7 @@ public class DimTests
         testValView.Dispose ();
 
         testVal = new (1, 2, 3, 4);
-        testValView = new() { Frame = testVal };
+        testValView = new () { Frame = testVal };
         dim = Width (testValView);
         Assert.Equal ($"View(Width,View(){testVal})", dim.ToString ());
         testValView.Dispose ();

+ 2 - 0
UnitTests/View/Layout/FrameTests.cs

@@ -40,9 +40,11 @@ public class FrameTests (ITestOutputHelper output)
         var newFrame = new Rectangle (1, 2, 30, 40);
 
         var v = new View ();
+        Assert.Equal (Rectangle.Empty, v.Frame);
         v.Dispose ();
 
         v = new View { Frame = frame };
+        Assert.Equal (frame, v.Frame);
 
         v.Frame = newFrame;
         Assert.Equal (newFrame, v.Frame);

+ 61 - 0
UnitTests/View/Layout/LayoutTests.cs

@@ -34,6 +34,67 @@ public class LayoutTests (ITestOutputHelper output)
         second.Dispose ();
     }
 
+    [Fact]
+    public void Add_Does_Not_Call_LayoutSubviews ()
+    {
+        var superView = new View { Id = "superView" };
+        var view = new View { Id = "view" };
+        bool layoutStartedRaised = false;
+        bool layoutCompleteRaised = false;
+        superView.LayoutStarted += (sender, e) => layoutStartedRaised = true;
+        superView.LayoutComplete += (sender, e) => layoutCompleteRaised = true;
+
+        superView.Add (view);
+
+        Assert.False (layoutStartedRaised);
+        Assert.False (layoutCompleteRaised);
+
+        superView.Remove(view);
+
+        superView.BeginInit();
+        superView.EndInit ();
+
+        superView.Add (view);
+
+        Assert.False (layoutStartedRaised);
+        Assert.False (layoutCompleteRaised);
+
+    }
+
+    [Fact]
+    public void BeginEndInit_Do_Not_Call_LayoutSubviews ()
+    {
+        var superView = new View { Id = "superView" };
+        bool layoutStartedRaised = false;
+        bool layoutCompleteRaised = false;
+        superView.LayoutStarted += (sender, e) => layoutStartedRaised = true;
+        superView.LayoutComplete += (sender, e) => layoutCompleteRaised = true;
+        superView.BeginInit ();
+        superView.EndInit ();
+        Assert.False (layoutStartedRaised);
+        Assert.False (layoutCompleteRaised);
+    }
+
+    [Fact]
+    public void LayoutSubViews_Raises_LayoutStarted_LayoutComplete ()
+    {
+        var superView = new View { Id = "superView" };
+        int layoutStartedRaised = 0;
+        int layoutCompleteRaised = 0;
+        superView.LayoutStarted += (sender, e) => layoutStartedRaised++;
+        superView.LayoutComplete += (sender, e) => layoutCompleteRaised++;
+
+        superView.LayoutSubviews ();
+        Assert.Equal (1, layoutStartedRaised);
+        Assert.Equal (1, layoutCompleteRaised);
+
+        superView.BeginInit ();
+        superView.EndInit ();
+        superView.LayoutSubviews ();
+        Assert.Equal (2, layoutStartedRaised);
+        Assert.Equal (2, layoutCompleteRaised);
+    }
+
     [Fact]
     public void LayoutSubviews_RootHas_SuperView ()
     {

+ 1 - 2
UnitTests/View/Layout/Pos.CenterTests.cs

@@ -61,8 +61,7 @@ public class PosCenterTests (ITestOutputHelper output)
         var superView = new View { Width = 10, Height = 10 };
         var view = new View { X = Center (), Y = Center (), Width = 20, Height = 20 };
         superView.Add (view);
-        superView.BeginInit();
-        superView.EndInit();
+        superView.LayoutSubviews ();
 
         Assert.Equal (-5, view.Frame.Left);
         Assert.Equal (-5, view.Frame.Top);

+ 1 - 1
UnitTests/View/TextTests.cs

@@ -715,7 +715,7 @@ w ";
         Assert.Equal (new (0, 0, 2, 1), lbl.Frame);
         Assert.Equal (new (0, 0, 3, 1), lbl._needsDisplayRect);
         Assert.Equal (new (0, 0, 0, 0), lbl.SuperView._needsDisplayRect);
-        Assert.True (lbl.SuperView.LayoutNeeded);
+        Assert.True (lbl.SuperView.IsLayoutNeeded ());
         Application.Refresh();
         Assert.Equal ("12  ", GetContents ());
 

+ 0 - 1
UnitTests/View/ViewTests.cs

@@ -1128,7 +1128,6 @@ At 0,0
                 }
             }
 
-            ClearLayoutNeeded ();
             ClearNeedsDisplay ();
         }
 

+ 2 - 2
UnitTests/Views/LabelTests.cs

@@ -211,11 +211,11 @@ This TextFormatter (tf2) with fill will be cleared on rewritten.       ",
                                                      );
 
         Assert.False (label.NeedsDisplay);
-        Assert.False (label.LayoutNeeded);
+        Assert.False (label.IsLayoutNeeded ());
         Assert.False (label.SubViewNeedsDisplay);
         label.Text = "This label is rewritten.";
         Assert.True (label.NeedsDisplay);
-        Assert.True (label.LayoutNeeded);
+        Assert.True (label.IsLayoutNeeded ());
         //Assert.False (label.SubViewNeedsDisplay);
         label.Draw ();
 

+ 2 - 2
UnitTests/Views/ToplevelTests.cs

@@ -749,7 +749,7 @@ public partial class ToplevelTests (ITestOutputHelper output)
 
         Application.RaiseMouseEvent (new () { ScreenPosition = new (9, 9), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition });
         Assert.Equal (win.Border, Application.MouseGrabView);
-        top.SetNeedsLayout ();
+        top.SetLayoutNeeded ();
         top.LayoutSubviews ();
         Assert.Equal (new (6, 6, 191, 91), win.Frame);
         Application.Refresh ();
@@ -760,7 +760,7 @@ public partial class ToplevelTests (ITestOutputHelper output)
                                       ScreenPosition = new (5, 5), Flags = MouseFlags.Button1Pressed | MouseFlags.ReportMousePosition
                                   });
         Assert.Equal (win.Border, Application.MouseGrabView);
-        top.SetNeedsLayout ();
+        top.SetLayoutNeeded ();
         top.LayoutSubviews ();
         Assert.Equal (new (2, 2, 195, 95), win.Frame);
         Application.Refresh ();