Browse Source

WIP fixing FrameToScreen

Tig 1 year ago
parent
commit
5d85242020

+ 4 - 3
Terminal.Gui/View/Adornment/Adornment.cs

@@ -127,10 +127,11 @@ public class Adornment : View
 
         // Adornments are *Children* of a View, not SubViews. Thus View.FrameToScreen will not work.
         // To get the screen-relative coordinates of an Adornment, we need get the parent's Frame
-        // in screen coords, and add our Frame location to it.
-        Rectangle parent = Parent.FrameToScreen ();
+        // in screen coords, ...
+        Rectangle parentScreen = Parent.FrameToScreen ();
 
-        return new (new (parent.X + Frame.X, parent.Y + Frame.Y), Frame.Size);
+        // ...and add our Frame location to it.
+        return new (new (parentScreen.X + Frame.X, parentScreen.Y + Frame.Y), Frame.Size);
     }
 
     /// <inheritdoc/>

+ 18 - 13
Terminal.Gui/View/Layout/ViewLayout.cs

@@ -90,27 +90,32 @@ public partial class View
     /// <returns>The location and size of the view in screen-relative coordinates.</returns>
     public virtual Rectangle FrameToScreen ()
     {
-        Rectangle ret = Frame;
-        View super = SuperView;
+        Rectangle screen = Frame;
+        View current = SuperView;
 
-        while (super is { })
+        while (current is { })
         {
-            if (super is Adornment adornment)
+            if (current is Adornment adornment)
             {
                 // Adornments don't have SuperViews; use Adornment.FrameToScreen override
-                ret = adornment.FrameToScreen ();
-                ret.Offset (Frame.X, Frame.Y);
+                // which will give us the screen coordinates of the parent
 
-                return ret;
+                var parentScreen = adornment.FrameToScreen ();
+
+                // Now add our Frame location
+                parentScreen.Offset (Frame.X, Frame.Y);
+
+                return parentScreen;
             }
 
-            Point viewportOffset = super.GetViewportOffsetFromFrame ();
-            viewportOffset.Offset (super.Frame.X - super.Viewport.X, super.Frame.Y - super.Viewport.Y);
-            ret.X += viewportOffset.X;
-            ret.Y += viewportOffset.Y;
-            super = super.SuperView;
+            Point viewportOffset = current.GetViewportOffsetFromFrame ();
+            viewportOffset.Offset (current.Frame.X - current.Viewport.X, current.Frame.Y - current.Viewport.Y);
+            screen.X += viewportOffset.X;
+            screen.Y += viewportOffset.Y;
+            current = current.SuperView;
         }
-        return ret;
+
+        return screen;
     }
 
     /// <summary>

+ 75 - 46
Terminal.Gui/View/ViewContent.cs

@@ -9,24 +9,27 @@ namespace Terminal.Gui;
 public enum ScrollSettings
 {
     /// <summary>
-    ///     Default settings.
+    ///     No settings.
     /// </summary>
-    Default = 0,
+    None = 0,
 
     /// <summary>
-    ///     If set, does not restrict vertical scrolling to <see cref="View.ContentSize"/>.<c>Height</c>.
+    ///     If set, <c>Viewport.Location.Y</c> can be negative or greater than to <see cref="View.ContentSize"/>.<c>Height</c>,
+    ///     enabling scrolling beyond the dimensions of the content area vertically.
     /// </summary>
-    NoRestrictVertical = 1,
+    AllowViewportYBeyondContent = 1,
 
     /// <summary>
-    ///     If set, does not restrict horizontal scrolling to <see cref="View.ContentSize"/>.<c>Width</c>.
+    ///     If set, <c>Viewport.Location.X</c> can be negative or greater than to <see cref="View.ContentSize"/>.<c>Width</c>,
+    ///     enabling scrolling beyond the dimensions of the content area horizontally.
     /// </summary>
-    NoRestrictHorizontal = 2,
+    AllowViewportXBeyondContent = 2,
 
     /// <summary>
-    ///     If set, does not restrict either vertical or horizontal scrolling to <see cref="View.ContentSize"/>.
+    ///     If set, <c>Viewport.Location</c> can be negative or greater than to <see cref="View.ContentSize"/>,
+    ///     enabling scrolling beyond the dimensions of the content area either horizontally or vertically.
     /// </summary>
-    NoRestrict = NoRestrictVertical | NoRestrictHorizontal
+    AllowViewportLocationBeyondContent = AllowViewportYBeyondContent | AllowViewportXBeyondContent
 }
 
 public partial class View
@@ -129,10 +132,27 @@ public partial class View
 
     #region Viewport
 
+    private ScrollSettings _scrollSettings;
+
     /// <summary>
     ///     Gets or sets how scrolling the <see cref="View.Viewport"/> on the View's Content Area is handled.
     /// </summary>
-    public ScrollSettings ScrollSettings { get; set; }
+    public ScrollSettings ScrollSettings
+    {
+        get => _scrollSettings;
+        set
+        {
+            if (_scrollSettings == value)
+            {
+                return;
+            }
+
+            _scrollSettings = value;
+
+            // Force set Viewport to cause settings to be applied as needed
+            SetViewport (Viewport);
+        }
+    }
 
     /// <summary>
     ///     The location of the viewport into the view's content (0,0) is the top-left corner of the content. The Content
@@ -204,63 +224,72 @@ public partial class View
                              Math.Max (0, Frame.Size.Height - thickness.Vertical)
                             ));
         }
-        set
+        set => SetViewport (value);
+    }
+
+    private void SetViewport (Rectangle viewport)
+    {
+        ApplySettings (ref viewport);
+
+        Thickness thickness = GetAdornmentsThickness ();
+
+        Size newSize = new (
+                            viewport.Size.Width + thickness.Horizontal,
+                            viewport.Size.Height + thickness.Vertical);
+
+        if (newSize == Frame.Size)
         {
-            if (!ScrollSettings.HasFlag (ScrollSettings.NoRestrictVertical))
+            // The change is not changing the Frame, so we don't need to update it.
+            // Just call SetNeedsLayout to update the layout.
+            if (_viewportLocation != viewport.Location)
             {
-                if (value.Y + Viewport.Height > ContentSize.Height)
-                {
-                    value.Y = ContentSize.Height - Viewport.Height;
-                }
-
-                if (value.Y < 0)
-                {
-                    value.Y = 0;
-                }
+                _viewportLocation = viewport.Location;
+                SetNeedsLayout ();
             }
 
-            if (!ScrollSettings.HasFlag (ScrollSettings.NoRestrictHorizontal))
+            return;
+        }
+
+        _viewportLocation = viewport.Location;
+
+        // Update the Frame because we made it bigger or smaller which impacts subviews.
+        Frame = Frame with
+        {
+            Size = newSize
+        };
+
+
+        void ApplySettings (ref Rectangle location)
+        {
+            if (!ScrollSettings.HasFlag (ScrollSettings.AllowViewportYBeyondContent))
             {
-                if (value.X + Viewport.Width > ContentSize.Width)
+                if (location.Y + Viewport.Height > ContentSize.Height)
                 {
-                    value.X = ContentSize.Width - Viewport.Width;
+                    location.Y = ContentSize.Height - Viewport.Height;
                 }
 
-                if (value.X < 0)
+                if (location.Y < 0)
                 {
-                    value.X = 0;
+                    location.Y = 0;
                 }
             }
 
-            Thickness thickness = GetAdornmentsThickness ();
-
-            Size newSize = new (
-                                value.Size.Width + thickness.Horizontal,
-                                value.Size.Height + thickness.Vertical);
-
-            if (newSize == Frame.Size)
+            if (!ScrollSettings.HasFlag (ScrollSettings.AllowViewportXBeyondContent))
             {
-                // The change is not changing the Frame, so we don't need to update it.
-                // Just call SetNeedsLayout to update the layout.
-                if (_viewportLocation != value.Location)
+                if (location.X + Viewport.Width > ContentSize.Width)
                 {
-                    _viewportLocation = value.Location;
-                    SetNeedsLayout ();
+                    location.X = ContentSize.Width - Viewport.Width;
                 }
 
-                return;
+                if (location.X < 0)
+                {
+                    location.X = 0;
+                }
             }
-
-            _viewportLocation = value.Location;
-
-            // Update the Frame because we made it bigger or smaller which impacts subviews.
-            Frame = Frame with
-            {
-                Size = newSize
-            };
         }
     }
 
+
     /// <summary>
     ///     Converts a <see cref="Viewport"/>-relative location to a screen-relative location.
     /// </summary>

+ 4 - 0
Terminal.Gui/View/ViewDrawing.cs

@@ -425,6 +425,10 @@ public partial class View
     /// </param>
     public virtual void OnDrawContent (Rectangle viewport)
     {
+        if (Title == "View in Padding")
+        {
+
+        }
         if (NeedsDisplay)
         {
             if (SuperView is { })

+ 26 - 3
UICatalog/Scenarios/AdornmentExperiments.cs

@@ -42,14 +42,37 @@ public class AdornmentExperiments : Scenario
         _frameView.Padding.Thickness = new (0, 10, 0, 0);
         _frameView.Padding.ColorScheme = Colors.ColorSchemes ["Error"];
 
-        var label = new Label ()
+        var viewInPadding = new View ()
         {
-            Text = "In Padding",
+            Title = "View in Padding",
+            Text = "Text In View",
             X = Pos.Center (),
             Y = 0,
+            Width = 30,
+            Height = 10,
             BorderStyle = LineStyle.Dashed
         };
-        _frameView.Padding.Add (label);
+        viewInPadding.Border.Thickness = new (3, 3, 3, 3);
+        viewInPadding.Initialized += ViewInPadding_Initialized;
+        
+        // add a subview to the subview of padding
+        var subviewInSubview = new View ()
+        {
+            X = 0,
+            Y = 1,
+            Width = 10,
+            Height = 1,
+            Text = "Subview in Subview of Padding",
+        };
+
+        viewInPadding.Add (subviewInSubview);
+        _frameView.Padding.Add (viewInPadding);
+
+
+        void ViewInPadding_Initialized (object sender, System.EventArgs e)
+        {
+
+        }
     }
 
 }

+ 201 - 149
UICatalog/Scenarios/Buttons.cs

@@ -1,5 +1,8 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
 using System.Text;
+using JetBrains.Annotations;
 using Terminal.Gui;
 
 namespace UICatalog.Scenarios;
@@ -15,6 +18,7 @@ public class Buttons : Scenario
         {
             Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
         };
+
         // Add a label & text field so we can demo IsDefault
         var editLabel = new Label { X = 0, Y = 0, TabStop = true, Text = "TextField (to demo IsDefault):" };
         main.Add (editLabel);
@@ -32,19 +36,19 @@ public class Buttons : Scenario
         var swapButton = new Button { X = 50, Text = "S_wap Default (Absolute Layout)" };
 
         swapButton.Accept += (s, e) =>
-        {
-            defaultButton.IsDefault = !defaultButton.IsDefault;
-            swapButton.IsDefault = !swapButton.IsDefault;
-        };
+                             {
+                                 defaultButton.IsDefault = !defaultButton.IsDefault;
+                                 swapButton.IsDefault = !swapButton.IsDefault;
+                             };
         main.Add (swapButton);
 
         static void DoMessage (Button button, string txt)
         {
             button.Accept += (s, e) =>
-            {
-                string btnText = button.Text;
-                MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No");
-            };
+                             {
+                                 string btnText = button.Text;
+                                 MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No");
+                             };
         }
 
         var colorButtonsLabel = new Label { X = 0, Y = Pos.Bottom (editLabel) + 1, Text = "Color Buttons:" };
@@ -75,20 +79,20 @@ public class Buttons : Scenario
         Button button;
 
         main.Add (
-                 button = new Button
-                 {
-                     X = 2,
-                     Y = Pos.Bottom (colorButtonsLabel) + 1,
-                     Text =
-                         "A super l_öng Button that will probably expose a bug in clipping or wrapping of text. Will it?"
-                 }
-                );
+                  button = new Button
+                  {
+                      X = 2,
+                      Y = Pos.Bottom (colorButtonsLabel) + 1,
+                      Text =
+                          "A super l_öng Button that will probably expose a bug in clipping or wrapping of text. Will it?"
+                  }
+                 );
         DoMessage (button, button.Text);
 
         // Note the 'N' in 'Newline' will be the hotkey
         main.Add (
-                 button = new Button { X = 2, Y = Pos.Bottom (button) + 1, Text = "a Newline\nin the button" }
-                );
+                  button = new Button { X = 2, Y = Pos.Bottom (button) + 1, Text = "a Newline\nin the button" }
+                 );
         button.Accept += (s, e) => MessageBox.Query ("Message", "Question?", "Yes", "No");
 
         var textChanger = new Button { X = 2, Y = Pos.Bottom (button) + 1, Text = "Te_xt Changer" };
@@ -96,13 +100,13 @@ public class Buttons : Scenario
         textChanger.Accept += (s, e) => textChanger.Text += "!";
 
         main.Add (
-                 button = new Button
-                 {
-                     X = Pos.Right (textChanger) + 2,
-                     Y = Pos.Y (textChanger),
-                     Text = "Lets see if this will move as \"Text Changer\" grows"
-                 }
-                );
+                  button = new Button
+                  {
+                      X = Pos.Right (textChanger) + 2,
+                      Y = Pos.Y (textChanger),
+                      Text = "Lets see if this will move as \"Text Changer\" grows"
+                  }
+                 );
 
         var removeButton = new Button
         {
@@ -112,12 +116,12 @@ public class Buttons : Scenario
 
         // This in interesting test case because `moveBtn` and below are laid out relative to this one!
         removeButton.Accept += (s, e) =>
-        {
-            // Now this throw a InvalidOperationException on the TopologicalSort method as is expected.
-            //main.Remove (removeButton);
+                               {
+                                   // Now this throw a InvalidOperationException on the TopologicalSort method as is expected.
+                                   //main.Remove (removeButton);
 
-            removeButton.Visible = false;
-        };
+                                   removeButton.Visible = false;
+                               };
 
         var computedFrame = new FrameView
         {
@@ -142,12 +146,12 @@ public class Buttons : Scenario
         };
 
         moveBtn.Accept += (s, e) =>
-        {
-            moveBtn.X = moveBtn.Frame.X + 5;
+                          {
+                              moveBtn.X = moveBtn.Frame.X + 5;
 
-            // This is already fixed with the call to SetNeedDisplay() in the Pos Dim.
-            //computedFrame.LayoutSubviews (); // BUGBUG: This call should not be needed. View.X is not causing relayout correctly
-        };
+                              // This is already fixed with the call to SetNeedDisplay() in the Pos Dim.
+                              //computedFrame.LayoutSubviews (); // BUGBUG: This call should not be needed. View.X is not causing relayout correctly
+                          };
         computedFrame.Add (moveBtn);
 
         // Demonstrates how changing the View.Frame property can SIZE Views (#583)
@@ -163,11 +167,11 @@ public class Buttons : Scenario
         };
 
         sizeBtn.Accept += (s, e) =>
-        {
-            sizeBtn.Width = sizeBtn.Frame.Width + 5;
+                          {
+                              sizeBtn.Width = sizeBtn.Frame.Width + 5;
 
-            //computedFrame.LayoutSubviews (); // FIXED: This call should not be needed. View.X is not causing relayout correctly
-        };
+                              //computedFrame.LayoutSubviews (); // FIXED: This call should not be needed. View.X is not causing relayout correctly
+                          };
         computedFrame.Add (sizeBtn);
 
         var absoluteFrame = new FrameView
@@ -184,14 +188,14 @@ public class Buttons : Scenario
         var moveBtnA = new Button { ColorScheme = Colors.ColorSchemes ["Error"], Text = "Move This Button via Frame" };
 
         moveBtnA.Accept += (s, e) =>
-        {
-            moveBtnA.Frame = new Rectangle (
-                                       moveBtnA.Frame.X + 5,
-                                       moveBtnA.Frame.Y,
-                                       moveBtnA.Frame.Width,
-                                       moveBtnA.Frame.Height
-                                      );
-        };
+                           {
+                               moveBtnA.Frame = new Rectangle (
+                                                               moveBtnA.Frame.X + 5,
+                                                               moveBtnA.Frame.Y,
+                                                               moveBtnA.Frame.Width,
+                                                               moveBtnA.Frame.Height
+                                                              );
+                           };
         absoluteFrame.Add (moveBtnA);
 
         // Demonstrates how changing the View.Frame property can SIZE Views (#583)
@@ -201,14 +205,14 @@ public class Buttons : Scenario
         };
 
         sizeBtnA.Accept += (s, e) =>
-        {
-            sizeBtnA.Frame = new Rectangle (
-                                       sizeBtnA.Frame.X,
-                                       sizeBtnA.Frame.Y,
-                                       sizeBtnA.Frame.Width + 5,
-                                       sizeBtnA.Frame.Height
-                                      );
-        };
+                           {
+                               sizeBtnA.Frame = new Rectangle (
+                                                               sizeBtnA.Frame.X,
+                                                               sizeBtnA.Frame.Y,
+                                                               sizeBtnA.Frame.Width + 5,
+                                                               sizeBtnA.Frame.Height
+                                                              );
+                           };
         absoluteFrame.Add (sizeBtnA);
 
         var label = new Label
@@ -289,47 +293,47 @@ public class Buttons : Scenario
         main.Add (moveUnicodeHotKeyBtn);
 
         radioGroup.SelectedItemChanged += (s, args) =>
-        {
-            switch (args.SelectedItem)
-            {
-                case 0:
-                    moveBtn.TextAlignment = TextAlignment.Left;
-                    sizeBtn.TextAlignment = TextAlignment.Left;
-                    moveBtnA.TextAlignment = TextAlignment.Left;
-                    sizeBtnA.TextAlignment = TextAlignment.Left;
-                    moveHotKeyBtn.TextAlignment = TextAlignment.Left;
-                    moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Left;
-
-                    break;
-                case 1:
-                    moveBtn.TextAlignment = TextAlignment.Right;
-                    sizeBtn.TextAlignment = TextAlignment.Right;
-                    moveBtnA.TextAlignment = TextAlignment.Right;
-                    sizeBtnA.TextAlignment = TextAlignment.Right;
-                    moveHotKeyBtn.TextAlignment = TextAlignment.Right;
-                    moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Right;
-
-                    break;
-                case 2:
-                    moveBtn.TextAlignment = TextAlignment.Centered;
-                    sizeBtn.TextAlignment = TextAlignment.Centered;
-                    moveBtnA.TextAlignment = TextAlignment.Centered;
-                    sizeBtnA.TextAlignment = TextAlignment.Centered;
-                    moveHotKeyBtn.TextAlignment = TextAlignment.Centered;
-                    moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Centered;
-
-                    break;
-                case 3:
-                    moveBtn.TextAlignment = TextAlignment.Justified;
-                    sizeBtn.TextAlignment = TextAlignment.Justified;
-                    moveBtnA.TextAlignment = TextAlignment.Justified;
-                    sizeBtnA.TextAlignment = TextAlignment.Justified;
-                    moveHotKeyBtn.TextAlignment = TextAlignment.Justified;
-                    moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Justified;
-
-                    break;
-            }
-        };
+                                          {
+                                              switch (args.SelectedItem)
+                                              {
+                                                  case 0:
+                                                      moveBtn.TextAlignment = TextAlignment.Left;
+                                                      sizeBtn.TextAlignment = TextAlignment.Left;
+                                                      moveBtnA.TextAlignment = TextAlignment.Left;
+                                                      sizeBtnA.TextAlignment = TextAlignment.Left;
+                                                      moveHotKeyBtn.TextAlignment = TextAlignment.Left;
+                                                      moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Left;
+
+                                                      break;
+                                                  case 1:
+                                                      moveBtn.TextAlignment = TextAlignment.Right;
+                                                      sizeBtn.TextAlignment = TextAlignment.Right;
+                                                      moveBtnA.TextAlignment = TextAlignment.Right;
+                                                      sizeBtnA.TextAlignment = TextAlignment.Right;
+                                                      moveHotKeyBtn.TextAlignment = TextAlignment.Right;
+                                                      moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Right;
+
+                                                      break;
+                                                  case 2:
+                                                      moveBtn.TextAlignment = TextAlignment.Centered;
+                                                      sizeBtn.TextAlignment = TextAlignment.Centered;
+                                                      moveBtnA.TextAlignment = TextAlignment.Centered;
+                                                      sizeBtnA.TextAlignment = TextAlignment.Centered;
+                                                      moveHotKeyBtn.TextAlignment = TextAlignment.Centered;
+                                                      moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Centered;
+
+                                                      break;
+                                                  case 3:
+                                                      moveBtn.TextAlignment = TextAlignment.Justified;
+                                                      sizeBtn.TextAlignment = TextAlignment.Justified;
+                                                      moveBtnA.TextAlignment = TextAlignment.Justified;
+                                                      sizeBtnA.TextAlignment = TextAlignment.Justified;
+                                                      moveHotKeyBtn.TextAlignment = TextAlignment.Justified;
+                                                      moveUnicodeHotKeyBtn.TextAlignment = TextAlignment.Justified;
+
+                                                      break;
+                                              }
+                                          };
 
         label = new Label ()
         {
@@ -338,59 +342,31 @@ public class Buttons : Scenario
             Title = "_Numeric Up/Down (press-and-hold):",
         };
 
-        var downButton = new Button ()
+        var numericUpDown = new NumericUpDown ()
         {
-            CanFocus = false,
-            AutoSize = false,
-            X = Pos.Right (label) + 1,
+            Value = 1966,
+            X = Pos.Right (label),
             Y = Pos.Top (label),
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = $"{CM.Glyphs.DownArrow}",
-            WantContinuousButtonPressed = true,
+            Width = 10,
+            Height = 1
         };
+        numericUpDown.ValueChanged += NumericUpDown_ValueChanged;
 
-        var numericEdit = new TextField ()
-        {
-            Text = "1966",
-            X = Pos.Right (downButton),
-            Y = Pos.Top (downButton),
-            Width = 5,
-            Height = 1,
-        };
-        var upButton = new Button ()
-        {
-            CanFocus = false,
-            AutoSize = false,
-            X = Pos.Right (numericEdit),
-            Y = Pos.Top (numericEdit),
-            Height = 1,
-            Width = 1,
-            NoPadding = true,
-            NoDecorations = true,
-            Title = $"{CM.Glyphs.UpArrow}",
-            WantContinuousButtonPressed = true,
-        };
-        downButton.Accept += (s, e) =>
-        {
-            numericEdit.Text = $"{int.Parse (numericEdit.Text) - 1}";
-        };
-        upButton.Accept += (s, e) =>
+        void NumericUpDown_ValueChanged (object sender, PropertyChangedEventArgs e)
         {
-            numericEdit.Text = $"{int.Parse (numericEdit.Text) + 1}";
-        };
+            
+        }
 
-        main.Add (label, downButton, numericEdit, upButton);
+        main.Add (label, numericUpDown);
 
         label = new Label ()
         {
             X = 0,
-            Y = Pos.Bottom (label) + 1,
+            Y = Pos.Bottom (numericUpDown) + 1,
             Title = "_No Repeat:",
         };
         int noRepeatAcceptCount = 0;
+
         var noRepeatButton = new Button ()
         {
             X = Pos.Right (label) + 1,
@@ -398,10 +374,7 @@ public class Buttons : Scenario
             Title = $"Accept Cou_nt: {noRepeatAcceptCount}",
             WantContinuousButtonPressed = false,
         };
-        noRepeatButton.Accept += (s, e) =>
-        {
-            noRepeatButton.Title = $"Accept Cou_nt: {++noRepeatAcceptCount}";
-        };
+        noRepeatButton.Accept += (s, e) => { noRepeatButton.Title = $"Accept Cou_nt: {++noRepeatAcceptCount}"; };
         main.Add (label, noRepeatButton);
 
         label = new Label ()
@@ -411,6 +384,7 @@ public class Buttons : Scenario
             Title = "_Repeat (press-and-hold):",
         };
         int acceptCount = 0;
+
         var repeatButton = new Button ()
         {
             X = Pos.Right (label) + 1,
@@ -418,10 +392,7 @@ public class Buttons : Scenario
             Title = $"Accept Co_unt: {acceptCount}",
             WantContinuousButtonPressed = true,
         };
-        repeatButton.Accept += (s, e) =>
-        {
-            repeatButton.Title = $"Accept Co_unt: {++acceptCount}";
-        };
+        repeatButton.Accept += (s, e) => { repeatButton.Title = $"Accept Co_unt: {++acceptCount}"; };
 
         var enableCB = new CheckBox ()
         {
@@ -430,14 +401,95 @@ public class Buttons : Scenario
             Title = "Enabled",
             Checked = true,
         };
-        enableCB.Toggled += (s, e) =>
-        {
-            repeatButton.Enabled = !repeatButton.Enabled;
-        };
+        enableCB.Toggled += (s, e) => { repeatButton.Enabled = !repeatButton.Enabled; };
         main.Add (label, repeatButton, enableCB);
 
         main.Ready += (s, e) => radioGroup.Refresh ();
         Application.Run (main);
         main.Dispose ();
     }
+
+    public class NumericUpDown : View
+    {
+        private Button _downButton;
+        private TextField _numericEdit;
+        private Button _upButton;
+
+        public NumericUpDown ()
+        {
+            Height = 1;
+            Width = 7;
+            _downButton = new ()
+            {
+                CanFocus = false,
+                AutoSize = false,
+                Height = 1,
+                Width = 1,
+                NoPadding = true,
+                NoDecorations = true,
+                Title = $"{CM.Glyphs.DownArrow}",
+                WantContinuousButtonPressed = true,
+            };
+
+            _numericEdit = new TextField ()
+            {
+                Text = "0",
+                X = Pos.Right (_downButton),
+                Y = Pos.Top (_downButton),
+                Width = 5,
+                Height = 1,
+            };
+
+            _upButton = new ()
+            {
+                CanFocus = false,
+                AutoSize = false,
+                X = Pos.Right (_numericEdit),
+                Y = Pos.Top (_numericEdit),
+                Height = 1,
+                Width = 1,
+                NoPadding = true,
+                NoDecorations = true,
+                Title = $"{CM.Glyphs.UpArrow}",
+                WantContinuousButtonPressed = true,
+            };
+            _downButton.Accept += (s, e) =>
+                                  {
+                                      Button b = s as Button;
+                                      NumericUpDown superView = b!.SuperView as NumericUpDown;
+                                      superView!.Value = int.Parse (_numericEdit.Text) - 1;
+                                      _numericEdit.Text = $"{Value}";
+                                  };
+            _upButton.Accept += (s, e) =>
+                                {
+                                    Button b = s as Button;
+                                    NumericUpDown superView = b!.SuperView as NumericUpDown;
+                                    superView!.Value = int.Parse (_numericEdit.Text) + 1;
+                                    _numericEdit.Text = $"{Value}";
+                                };
+
+            Add (_downButton, _numericEdit, _upButton);
+        }
+
+        private int _value;
+
+        public int Value
+        {
+            get => _value;
+            set
+            {
+                if (_value == value)
+                {
+                    return;
+                }
+
+                _value = value;
+                _numericEdit.Text = _value.ToString ();
+                ValueChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Value)));
+            }
+        }
+
+        [CanBeNull]
+        public event EventHandler<PropertyChangedEventArgs> ValueChanged;
+    }
 }

+ 1 - 1
UICatalog/Scenarios/CharacterMap.cs

@@ -1,4 +1,4 @@
-//#define OTHER_CONTROLS
+#define OTHER_CONTROLS
 
 using System;
 using System.Collections.Generic;

+ 97 - 19
UICatalog/Scenarios/VirtualContentScrolling.cs

@@ -11,7 +11,6 @@ namespace UICatalog.Scenarios;
 public class VirtualScrolling : Scenario
 {
     private ViewDiagnosticFlags _diagnosticFlags;
-
     public class VirtualDemoView : FrameView
     {
         public VirtualDemoView ()
@@ -26,7 +25,7 @@ public class VirtualScrolling : Scenario
 
             // TODO: Add a way to set the scroll settings in the Scenario
             ContentSize = new Size (60, 40);
-            ScrollSettings = ScrollSettings.NoRestrict;
+            ScrollSettings = ScrollSettings.AllowViewportLocationBeyondContent;
 
             // Things this view knows how to do
             AddCommand (Command.ScrollDown, () => ScrollVertical (1));
@@ -104,16 +103,99 @@ public class VirtualScrolling : Scenario
 
         var view = new VirtualDemoView { Title = "Virtual Scrolling" };
 
-        var tf1 = new TextField { X = 20, Y = 7, Width = 10, Text = "TextField" };
-        var color = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (11), Y = 10 };
-        color.BorderStyle = LineStyle.RoundedDotted;
+        // Add Scroll Setting UI to Padding
+        view.Padding.Thickness = new (0, 2, 0, 0);
+        view.Padding.ColorScheme = Colors.ColorSchemes["Error"];
+
+        var cbAllowXBeyondContent = new CheckBox ()
+        {
+            Title = "Allow Viewport._X Beyond Content",
+            Y = 0,
+            CanFocus = false
+        };
+        cbAllowXBeyondContent.Checked = view.ScrollSettings.HasFlag (ScrollSettings.AllowViewportXBeyondContent);
+        cbAllowXBeyondContent.Toggled += NoRestrictHorizontal_Toggled;
+
+        void NoRestrictHorizontal_Toggled (object sender, StateEventArgs<bool?> e)
+        {
+            if (e.NewValue == true)
+            {
+                view.ScrollSettings = view.ScrollSettings | ScrollSettings.AllowViewportXBeyondContent;
+            }
+            else
+            {
+                view.ScrollSettings = view.ScrollSettings & ~ScrollSettings.AllowViewportXBeyondContent;
+            }
+        }
 
-        color.ColorChanged += (s, e) =>
+        view.Padding.Add (cbAllowXBeyondContent);
+
+        var cbAllowYBeyondContent = new CheckBox ()
+        {
+            Title = "Allow Viewport._Y Beyond Content",
+            X = Pos.Right (cbAllowXBeyondContent) + 1,
+            Y = 0,
+            CanFocus = false
+        };
+        cbAllowYBeyondContent.Checked = view.ScrollSettings.HasFlag (ScrollSettings.AllowViewportYBeyondContent);
+        cbAllowYBeyondContent.Toggled += NoRestrictVertical_Toggled;
+
+        void NoRestrictVertical_Toggled (object sender, StateEventArgs<bool?> e)
+        {
+            if (e.NewValue == true)
+            {
+                view.ScrollSettings = view.ScrollSettings | ScrollSettings.AllowViewportYBeyondContent;
+            }
+            else
+            {
+                view.ScrollSettings = view.ScrollSettings & ~ScrollSettings.AllowViewportYBeyondContent;
+            }
+        }
+
+        view.Padding.Add (cbAllowYBeyondContent);
+
+        var labelContentSize = new Label ()
+        {
+            Title = "_ContentSize:",
+            Y = 1,
+        };
+
+        var contentSizeWidth = new Buttons.NumericUpDown()
+        {
+            Value = view.ContentSize.Width,
+            X = Pos.Right (labelContentSize) + 1,
+            Y = Pos.Top (labelContentSize),
+        };
+
+        var labelComma = new Label ()
+        {
+            Title = ", ",
+            X = Pos.Right (contentSizeWidth),
+            Y = Pos.Top (labelContentSize),
+        };
+
+        var contentSizeHeight = new Buttons.NumericUpDown ()
+        {
+            Value = view.ContentSize.Height,
+            X = Pos.Right (labelComma),
+            Y = Pos.Top (labelContentSize),
+            CanFocus =false
+        };
+        view.Padding.Add (labelContentSize, contentSizeWidth, labelComma, contentSizeHeight);
+
+
+        // Add demo views to show that things work correctly
+        var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test TextField" };
+
+        var colorPicker = new ColorPicker { Title = "BG", BoxHeight = 1, BoxWidth = 1, X = Pos.AnchorEnd (11), Y = 10 };
+        colorPicker.BorderStyle = LineStyle.RoundedDotted;
+
+        colorPicker.ColorChanged += (s, e) =>
                               {
-                                  color.SuperView.ColorScheme = new (color.SuperView.ColorScheme)
+                                  colorPicker.SuperView.ColorScheme = new (colorPicker.SuperView.ColorScheme)
                                   {
                                       Normal = new (
-                                                    color.SuperView.ColorScheme.Normal.Foreground,
+                                                    colorPicker.SuperView.ColorScheme.Normal.Foreground,
                                                     e.Color
                                                    )
                                   };
@@ -142,9 +224,9 @@ public class VirtualScrolling : Scenario
         charMap.Accept += (s, e) =>
                               MessageBox.Query (20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No");
 
-        var btnButtonInWindow = new Button { X = Pos.AnchorEnd (10), Y = 0, Text = "Button" };
+        var buttonAnchoredRight = new Button { X = Pos.AnchorEnd (10), Y = 0, Text = "Button" };
 
-        var tv = new Label
+        var labelAnchoredBottomLeft = new Label
         {
             AutoSize = false,
             Y = Pos.AnchorEnd (3),
@@ -160,33 +242,29 @@ public class VirtualScrolling : Scenario
         view.Border.Thickness = new (3);
 
         view.Padding.Data = "Padding";
-        view.Padding.Thickness = new (3);
 
-        view.Add (btnButtonInWindow, tf1, color, charMap, textView, tv);
-        var label2 = new Label
+        view.Add (buttonAnchoredRight, textField, colorPicker, charMap, textView, labelAnchoredBottomLeft);
+
+        var longLabel = new Label
         {
             Id = "label2",
             X = 0,
             Y = 30,
             Text = "This label is long. It should clip to the Viewport (but not ContentArea). This is a virtual scrolling demo. Use the arrow keys and/or mouse wheel to scroll the content.",
         };
-        label2.TextFormatter.WordWrap = true;
-        view.Add (label2);
+        longLabel.TextFormatter.WordWrap = true;
+        view.Add (longLabel);
 
         var editor = new Adornments.AdornmentsEditor
         {
             Title = $"{Application.QuitKey} to Quit - Scenario: {GetName ()}",
             ColorScheme = Colors.ColorSchemes ["Dialog"]
-
-            //BorderStyle = LineStyle.None,
         };
 
         editor.Initialized += (s, e) => { editor.ViewToEdit = view; };
 
         editor.Closed += (s, e) => View.Diagnostics = _diagnosticFlags;
 
-        //button.SetFocus ();
-
         Application.Run (editor);
         editor.Dispose ();
         Application.Shutdown ();

+ 84 - 4
UnitTests/View/Layout/ToScreenTests.cs

@@ -66,10 +66,10 @@ public class ToScreenTests (ITestOutputHelper output)
         view.Frame = frame;
 
         // Act
-        var screen = view.FrameToScreen();
+        var screen = view.FrameToScreen ();
 
         // Assert
-        Assert.Equal(expectedX, screen.X);
+        Assert.Equal (expectedX, screen.X);
     }
 
     [Theory]
@@ -94,6 +94,86 @@ public class ToScreenTests (ITestOutputHelper output)
         Assert.Equal (expectedX, screen.X);
     }
 
+    [Theory]
+    [InlineData (0, 1)]
+    [InlineData (1, 2)]
+    [InlineData (-1, 0)]
+    [InlineData (11, 12)]
+    public void FrameToScreen_NoSuperView_WithAdornment_WithSubview (int x, int expectedX)
+    {
+        // We test with only X because Y is equivalent. Height/Width are irrelevant.
+        // Arrange
+        var frame = new Rectangle (x, 0, 10, 10);
+
+        var view = new View ();
+        view.BorderStyle = LineStyle.Single;
+        view.Frame = frame;
+
+        var subviewOfBorder = new View ()
+        {
+            X = 1, // screen should be 1
+            Y = 0,
+            Width = 1,
+            Height = 1
+        };
+
+        view.Border.Add (subviewOfBorder);
+        view.BeginInit ();
+        view.EndInit ();
+
+        // Act
+        var screen = subviewOfBorder.FrameToScreen ();
+
+        // Assert
+        Assert.Equal (expectedX, screen.X);
+    }
+
+    [Theory]
+    [InlineData (0, 3)]
+    [InlineData (1, 4)]
+    [InlineData (-1, 2)]
+    [InlineData (11, 14)]
+    public void FrameToScreen_Adornment_WithSubview_WithSubview (int topX, int expectedX)
+    {
+        // We test with only X because Y is equivalent. Height/Width are irrelevant.
+        // Arrange
+        var adornmentFrame = new Rectangle (topX, 0, 10, 10);
+
+        var adornment = new Adornment ();
+        adornment.Frame = adornmentFrame;
+        adornment.Thickness = new (1);
+
+        var subviewOfAdornment = new View ()
+        {
+            Id = "subviewOfAdornment",
+            X = 1, // screen should be 1
+            Y = 0,
+            Width = 1,
+            Height = 1,
+            BorderStyle = LineStyle.Single
+        };
+
+        var subviewOfSubview = new View ()
+        {
+            Id = "subviewOfSubview",
+            X = 2, // screen should be 4 (the subviewOfAdornment location is 1, and offset from frame is 1)
+            Y = 0,
+            Width = 1,
+            Height = 1
+        };
+        subviewOfAdornment.Add (subviewOfSubview);
+
+        adornment.Add (subviewOfAdornment);
+        adornment.BeginInit ();
+        adornment.EndInit ();
+
+        // Act
+        var screen = subviewOfSubview.FrameToScreen ();
+
+        // Assert
+        Assert.Equal (expectedX, screen.X);
+    }
+
     [Theory]
     [InlineData (0, 0)]
     [InlineData (1, 1)]
@@ -259,7 +339,7 @@ public class ToScreenTests (ITestOutputHelper output)
             ContentSize = new (20, 20)
         };
 
-        Point testPoint = new ( 0, 0);
+        Point testPoint = new (0, 0);
         Assert.Equal (new Point (1, 1), view.ContentToScreen (testPoint));
     }
 
@@ -551,7 +631,7 @@ public class ToScreenTests (ITestOutputHelper output)
         {
             Width = 10,
             Height = 10,
-            ScrollSettings = ScrollSettings.NoRestrict
+            ScrollSettings = ScrollSettings.AllowViewportLocationBeyondContent
         };
 
         Rectangle testRect = new Rectangle (0, 0, 1, 1);

+ 1 - 1
UnitTests/View/Layout/ViewportTests.cs

@@ -161,7 +161,7 @@ public class ViewportTests (ITestOutputHelper output)
         {
             Width = 10,
             Height = 10,
-            ScrollSettings = ScrollSettings.NoRestrict
+            ScrollSettings = ScrollSettings.AllowViewportLocationBeyondContent
         };
 
         Assert.Equal (new Rectangle (0, 0, 10, 10), view.Frame);