Parcourir la source

Fixed clipping - mostly: Still an issue w/ subviews of adornments

Tig il y a 1 an
Parent
commit
88222c4dfd

+ 21 - 3
Terminal.Gui/ConsoleDrivers/ConsoleDriver.cs

@@ -22,12 +22,27 @@ public abstract class ConsoleDriver
     /// <summary>Gets the location and size of the terminal screen.</summary>
     public Rectangle Screen => new (0, 0, Cols, Rows);
 
+    private Rectangle _clip;
+
     /// <summary>
     ///     Gets or sets the clip rectangle that <see cref="AddRune(Rune)"/> and <see cref="AddStr(string)"/> are subject
     ///     to.
     /// </summary>
     /// <value>The rectangle describing the of <see cref="Clip"/> region.</value>
-    public Rectangle Clip { get; set; }
+    public Rectangle Clip
+    {
+        get => _clip;
+        set
+        {
+            if (_clip == value)
+            {
+                return;
+            }
+
+            // Don't ever let Clip be bigger than Screen
+            _clip = Rectangle.Intersect (Screen, value);
+        }
+    }
 
     /// <summary>Get the operating system clipboard.</summary>
     public IClipboard Clipboard { get; internal set; }
@@ -229,7 +244,7 @@ public abstract class ConsoleDriver
 
                 _dirtyLines [Row] = true;
             }
-        } 
+        }
 
         if (runeWidth is < 0 or > 0)
         {
@@ -376,7 +391,10 @@ public abstract class ConsoleDriver
     ///     <see langword="false"/> if the coordinate is outside the screen bounds or outside of <see cref="Clip"/>.
     ///     <see langword="true"/> otherwise.
     /// </returns>
-    public bool IsValidLocation (int col, int row) { return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row); }
+    public bool IsValidLocation (int col, int row)
+    {
+        return col >= 0 && row >= 0 && col < Cols && row < Rows && Clip.Contains (col, row);
+    }
 
     /// <summary>
     ///     Updates <see cref="Col"/> and <see cref="Row"/> to the specified column and row in <see cref="Contents"/>.

+ 7 - 0
Terminal.Gui/View/Adornment/Adornment.cs

@@ -149,6 +149,8 @@ public class Adornment : View
             return;
         }
 
+        //Rectangle prevClip = SetClip ();
+
         Rectangle screen = ViewportToScreen (viewport);
         Attribute normalAttr = GetNormalColor ();
         Driver.SetAttribute (normalAttr);
@@ -172,6 +174,11 @@ public class Adornment : View
             base.OnDrawContent (viewport);
         }
 
+        if (Driver is { })
+        {
+           // Driver.Clip = prevClip;
+        }
+
         ClearLayoutNeeded ();
         ClearNeedsDisplay ();
     }

+ 0 - 7
Terminal.Gui/View/Adornment/Border.cs

@@ -362,11 +362,6 @@ public class Border : Adornment
     /// <inheritdoc/>
     public override void OnDrawContent (Rectangle viewport)
     {
-        if (Parent?.Title == "Title")
-        {
-
-        }
-
         base.OnDrawContent (viewport);
 
         if (Thickness == Thickness.Empty)
@@ -646,7 +641,5 @@ public class Border : Adornment
                 }
             }
         }
-
-        //base.OnDrawContent (viewport);
     }
 }

+ 14 - 3
Terminal.Gui/View/ViewContent.cs

@@ -84,10 +84,21 @@ public enum ViewportSettings
     AllowLocationGreaterThanContentSize = AllowXGreaterThanContentWidth | AllowYGreaterThanContentHeight,
 
     /// <summary>
-    /// If set, the default <see cref="View.OnDrawContent(Rectangle)"/> implementation will clear only the portion of the content
-    /// area that is visible within the <see cref="View.Viewport"/>. See also <see cref="View.ClearVisibleContent()"/>.
+    /// By default, clipping is applied to just the visible content within <see cref="View.Viewport"/>. In cases where the viewport is
+    /// larger than the content area (e.g. when <see cref="AllowNegativeLocation"/> is enabled), setting this flag will allow content
+    /// outside the <see cref="View.Viewport"/> to be drawn.
     /// </summary>
-    ClearVisibleContentOnly = 16,
+    ClipVisibleContentOnly = 16,
+
+    /// <summary>
+    /// If set <see cref="View.Clear()"/> will clear only the portion of the content
+    /// area that is visible within the <see cref="View.Viewport"/>. This is useful for views that have a
+    /// content area larger than the Viewport and want the area outside the content to be visually distinct.
+    /// </summary>
+    /// <remarks>
+    /// <see cref="ClipVisibleContentOnly"/> must be set for this setting to work (clipping beyond the visible area must be disabled).
+    /// </remarks>
+    ClearVisibleContentOnly = 32,
 }
 
 public partial class View

+ 83 - 61
Terminal.Gui/View/ViewDrawing.cs

@@ -1,4 +1,6 @@
-namespace Terminal.Gui;
+using System.Drawing;
+
+namespace Terminal.Gui;
 
 public partial class View
 {
@@ -54,7 +56,7 @@ public partial class View
     public bool SubViewNeedsDisplay { get; private set; }
 
     /// <summary>
-    ///     Gets or sets whether this View will use it's SuperView's <see cref="LineCanvas"/> for rendering any border
+    ///     Gets or sets whether this View will use it's SuperView's <see cref="LineCanvas"/> for rendering any 
     ///     lines. If <see langword="true"/> the rendering of any borders drawn by this Frame will be done by its parent's
     ///     SuperView. If <see langword="false"/> (the default) this View's <see cref="OnDrawAdornments"/> method will be
     ///     called to render the borders.
@@ -73,66 +75,83 @@ public partial class View
     /// <param name="rune">The Rune.</param>
     public void AddRune (int col, int row, Rune rune)
     {
-        if (row < 0 || col < 0 || row >= Viewport.Height || col >= Viewport.Width)
+        if (Move (col, row))
         {
-            // TODO: Change return type to bool so callers can determine success?
-            return;
+            Driver.AddRune (rune);
         }
-
-        Move (col, row);
-        Driver.AddRune (rune);
     }
 
     /// <summary>Clears <see cref="Viewport"/> with the normal background.</summary>
-    /// <remarks></remarks>
-    public void Clear () { Clear (new (Point.Empty, Viewport.Size)); }
-
-    /// <summary>Clears the portion of the content that is visible with the normal background. If the content does not fill the Viewport,
-    /// the area not filled will be cleared with DarkGray.</summary>
-    /// <remarks></remarks>
-    public void ClearVisibleContent ()
+    /// <remarks>
+    ///     <para>
+    ///         If <see cref="ViewportSettings"/> has <see cref="ViewportSettings.ClearVisibleContentOnly"/> only
+    ///         the portion of the content
+    ///         area that is visible within the <see cref="View.Viewport"/> will be cleared. This is useful for views that have a
+    ///         content area larger than the Viewport (e.g. when <see cref="ViewportSettings.AllowNegativeLocation"/> is
+    ///         enabled) and want
+    ///         the area outside the content to be visually distinct.
+    ///     </para>
+    /// </remarks>
+    public void Clear ()
     {
         if (Driver is null)
         {
             return;
         }
 
-        Rectangle toClear = new (-Viewport.Location.X, -Viewport.Location.Y, ContentSize.Width, ContentSize.Height);
+        // Get screen-relative coords
+        Rectangle toClear = ViewportToScreen (Viewport with { Location = new (0, 0) });
+
+        Rectangle prevClip = Driver.Clip;
+
+        if (ViewportSettings.HasFlag (ViewportSettings.ClearVisibleContentOnly))
+        {
+            Rectangle visibleContent = ViewportToScreen (new (new (-Viewport.X, -Viewport.Y), ContentSize));
+            toClear = Rectangle.Intersect (toClear, visibleContent);
+        }
 
+        Attribute prev = Driver.SetAttribute (GetNormalColor());
+        Driver.FillRect (toClear);
+        Driver.SetAttribute (prev);
 
-        Clear (toClear);
+        Driver.Clip = prevClip;
     }
 
-    /// <summary>Clears the specified <see cref="Viewport"/>-relative rectangle with the normal background.</summary>
-    /// <remarks></remarks>
-    /// <param name="viewport">The Viewport-relative rectangle to clear.</param>
-    public void Clear (Rectangle viewport)
+    /// <summary>Fills the specified <see cref="Viewport"/>-relative rectangle with the specified color.</summary>
+    /// <param name="rect">The Viewport-relative rectangle to clear.</param>
+    /// <param name="color">The color to use to fill the rectangle. If not provided, the Normal background color will be used.</param>
+    public void FillRect (Rectangle rect, Color? color = null)
     {
         if (Driver is null)
         {
             return;
         }
 
-        // Clamp the region to the bounds of the view
-        viewport = Rectangle.Intersect (viewport, new (Point.Empty, Viewport.Size));
+        // Get screen-relative coords
+        Rectangle toClear = ViewportToScreen (rect);
+
+        Rectangle prevClip = Driver.Clip;
 
-        Attribute prev = Driver.SetAttribute (GetNormalColor ());
-        Driver.FillRect (ViewportToScreen (viewport));
+        Driver.Clip = Rectangle.Intersect (prevClip, ViewportToScreen (Viewport with { Location = new (0, 0) }));
+
+        Attribute prev = Driver.SetAttribute (new (color ?? GetNormalColor().Background));
+        Driver.FillRect (toClear);
         Driver.SetAttribute (prev);
+
+        Driver.Clip = prevClip;
     }
 
-    /// <summary>Expands the <see cref="ConsoleDriver"/>'s clip region to include <see cref="Viewport"/>.</summary>
+    /// <summary>Sets the <see cref="ConsoleDriver"/>'s clip region to <see cref="Viewport"/>.</summary>
+    /// <remarks>
+    ///     The clip region is set to the intersection of the current clip region and the
+    ///     <see cref="Viewport"/>. This ensures that drawing is constrained to the visible
+    ///     viewport of the view.
+    /// </remarks>
     /// <returns>
     ///     The current screen-relative clip region, which can be then re-applied by setting
     ///     <see cref="ConsoleDriver.Clip"/>.
     /// </returns>
-    /// <remarks>
-    ///     <para>
-    ///         If <see cref="ConsoleDriver.Clip"/> and <see cref="Viewport"/> do not intersect, the clip region will be set to
-    ///         <see cref="Rectangle.Empty"/>.
-    ///     </para>
-    /// </remarks>
-    public Rectangle ClipToViewport ()
+    public Rectangle SetClip ()
     {
         if (Driver is null)
         {
@@ -140,7 +159,18 @@ public partial class View
         }
 
         Rectangle previous = Driver.Clip;
-        Driver.Clip = Rectangle.Intersect (previous, ViewportToScreen (Viewport with { Location = Point.Empty }));
+
+        // Clamp the Clip to the entire visible area
+        Rectangle clip = Rectangle.Intersect (ViewportToScreen (Viewport with { Location = Point.Empty }), previous);
+
+        if (ViewportSettings.HasFlag (ViewportSettings.ClipVisibleContentOnly))
+        {
+            // Clamp the Clip to the just content area that is within the viewport
+            Rectangle visibleContent = ViewportToScreen (new (new (-Viewport.X, -Viewport.Y), ContentSize));
+            clip = Rectangle.Intersect (clip, visibleContent);
+        }
+
+        Driver.Clip = clip;
 
         return previous;
     }
@@ -173,14 +203,17 @@ public partial class View
 
         OnDrawAdornments ();
 
-        Rectangle prevClip = ClipToViewport ();
-
         if (ColorScheme is { })
         {
             //Driver.SetAttribute (HasFocus ? GetFocusColor () : GetNormalColor ());
             Driver?.SetAttribute (GetNormalColor ());
         }
 
+        // By default, we clip to the viewport preventing drawing outside the viewport
+        // We also clip to the content, but if a developer wants to draw outside the viewport, they can do
+        // so via settings. SetClip honors the ViewportSettings.DisableVisibleContentClipping flag.
+        Rectangle prevClip = SetClip ();
+
         // Invoke DrawContentEvent
         var dev = new DrawEventArgs (Viewport, Rectangle.Empty);
         DrawContent?.Invoke (this, dev);
@@ -346,21 +379,22 @@ public partial class View
     /// </remarks>
     /// <param name="col">Column (viewport-relative).</param>
     /// <param name="row">Row (viewport-relative).</param>
-    public void Move (int col, int row)
+    public bool Move (int col, int row)
     {
         if (Driver is null || Driver?.Rows == 0)
         {
-            return;
+            return false;
         }
 
-        if (col < 0 || row < 0 || col >= Viewport.Size.Width || row >= Viewport.Size.Height)
+        if (col < 0 || row < 0 || col >= Viewport.Width || row >= Viewport.Height)
         {
-            // TODO: Change return type to bool so callers can determine success?
-            return;
+            return false;
         }
 
         Rectangle screen = ViewportToScreen (new (col, row, 0, 0));
         Driver?.Move (screen.X, screen.Y);
+
+        return true;
     }
 
     // TODO: Make this cancelable
@@ -405,7 +439,8 @@ public partial class View
     ///         to constrain drawing for better performance.
     ///     </para>
     ///     <para>
-    ///         The <see cref="ConsoleDriver.Clip"/> may define smaller area than <see cref="Viewport"/>; complex drawing code can be more
+    ///         The <see cref="ConsoleDriver.Clip"/> may define smaller area than <see cref="Viewport"/>; complex drawing code
+    ///         can be more
     ///         efficient by using <see cref="ConsoleDriver.Clip"/> to constrain drawing for better performance.
     ///     </para>
     ///     <para>
@@ -422,14 +457,7 @@ public partial class View
         {
             if (SuperView is { })
             {
-                if (ViewportSettings.HasFlag(ViewportSettings.ClearVisibleContentOnly))
-                {
-                    ClearVisibleContent ();
-                }
-                else
-                {
-                    Clear ();
-                }
+                Clear ();
             }
 
             if (!string.IsNullOrEmpty (TextFormatter.Text))
@@ -465,19 +493,16 @@ public partial class View
 
             foreach (View view in subviewsNeedingDraw)
             {
-                //view.Frame.IntersectsWith (bounds)) {
-                // && (view.Frame.IntersectsWith (bounds) || bounds.X < 0 || bounds.Y < 0)) {
                 if (view.LayoutNeeded)
                 {
                     view.LayoutSubviews ();
                 }
 
                 // Draw the subview
-                // Use the view's Viewport (view-relative; Location will always be (0,0)
-                //if (view.Visible && view.Frame.Width > 0 && view.Frame.Height > 0) {
-                view.Draw ();
+                if (view.Title.Contains ("This label"))
+                { }
 
-                //}
+                view.Draw ();
             }
         }
     }
@@ -489,10 +514,7 @@ public partial class View
     ///     The viewport-relative rectangle describing the currently visible viewport into the
     ///     <see cref="View"/>
     /// </param>
-    public virtual void OnDrawContentComplete (Rectangle viewport)
-    {
-        DrawContentComplete?.Invoke (this, new (viewport, Rectangle.Empty));
-    }
+    public virtual void OnDrawContentComplete (Rectangle viewport) { DrawContentComplete?.Invoke (this, new (viewport, Rectangle.Empty)); }
 
     // TODO: Make this cancelable
     /// <summary>
@@ -503,7 +525,7 @@ public partial class View
     /// <returns></returns>
     public virtual bool OnRenderLineCanvas ()
     {
-        if (!IsInitialized)
+        if (!IsInitialized || Driver is null)
         {
             return false;
         }

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

@@ -491,7 +491,7 @@ public class ComboBox : View
         }
 
         Reset (true);
-        _listview.Clear (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty);
+        _listview.Clear ();
         _listview.TabStop = false;
         SuperView?.SendSubviewToBack (this);
         Rectangle rect = _listview.ViewportToScreen (_listview.IsInitialized ? _listview.Viewport : Rectangle.Empty);
@@ -761,7 +761,7 @@ public class ComboBox : View
     private void ShowList ()
     {
         _listview.SetSource (_searchset);
-        _listview.Clear (Viewport); // Ensure list shrinks in Dialog as you type
+        _listview.Clear (); 
         _listview.Height = CalculatetHeight ();
         SuperView?.BringSubviewToFront (this);
     }

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

@@ -370,7 +370,7 @@ public class ScrollView : View
         SetViewsNeedsDisplay ();
 
         // TODO: It's bad practice for views to always clear a view. It negates clipping.
-        ClearVisibleContent();
+        Clear ();
 
         if (!string.IsNullOrEmpty (_contentView.Text) || _contentView.Subviews.Count > 0)
         {

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

@@ -1073,7 +1073,7 @@ public class Slider<T> : View
     private void DrawSlider ()
     {
         // TODO: be more surgical on clear
-        ClearVisibleContent ();
+        Clear ();
 
         // Attributes
 

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

@@ -317,7 +317,7 @@ public class TabView : View
 
         if (Tabs.Any ())
         {
-            Rectangle savedClip = ClipToViewport ();
+            Rectangle savedClip = SetClip ();
             _tabsBar.OnDrawContent (viewport);
             _contentView.SetNeedsDisplay ();
             _contentView.Draw ();
@@ -662,7 +662,7 @@ public class TabView : View
             _host._tabLocations = _host.CalculateViewport (Viewport).ToArray ();
 
             // clear any old text
-            ClearVisibleContent ();
+            Clear ();
 
             RenderTabLine ();
 

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

@@ -183,7 +183,7 @@ public class TileView : View
     {
         Driver.SetAttribute (ColorScheme.Normal);
 
-        ClearVisibleContent ();
+        Clear ();
 
         base.OnDrawContent (viewport);
 

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

@@ -260,7 +260,7 @@ public partial class Toplevel : View
         {
             //Driver.SetAttribute (GetNormalColor ());
             // TODO: It's bad practice for views to always clear. Defeats the purpose of clipping etc...
-            ClearVisibleContent ();
+            Clear ();
             LayoutSubviews ();
             PositionToplevels ();
 

+ 1 - 1
UICatalog/Scenarios/CharacterMap.cs

@@ -670,7 +670,7 @@ internal class CharMap : View
             return;
         }
 
-        ClearVisibleContent ();
+        Clear ();
 
         int cursorCol = Cursor.X + Viewport.X - RowLabelWidth - 1;
         int cursorRow = Cursor.Y + Viewport.Y - 1;

+ 1 - 1
UICatalog/Scenarios/Snake.cs

@@ -314,7 +314,7 @@ public class Snake : Scenario
             base.OnDrawContent (viewport);
 
             Driver.SetAttribute (white);
-            Clear (viewport);
+            Clear ();
 
             var canvas = new LineCanvas ();
 

+ 37 - 4
UICatalog/Scenarios/VirtualContent.cs

@@ -25,6 +25,7 @@ public class VirtualContent : Scenario
 
             ContentSize = new (60, 40);
             ViewportSettings |= ViewportSettings.ClearVisibleContentOnly;
+            ViewportSettings |= ViewportSettings.ClipVisibleContentOnly;
 
             // Things this view knows how to do
             AddCommand (Command.ScrollDown, () => ScrollVertical (1));
@@ -85,7 +86,13 @@ public class VirtualContent : Scenario
 
             SetNeedsDisplay ();
         }
+
+        public override void OnDrawContent (Rectangle viewport)
+        {
+            base.OnDrawContent (viewport);
+        }
     }
+
     public override void Main ()
     {
         Application.Init ();
@@ -232,7 +239,7 @@ public class VirtualContent : Scenario
 
         var cbClearOnlyVisible = new CheckBox
         {
-            Title = "Clear only Visible Content",
+            Title = "ClearVisibleContentOnly",
             X = Pos.Right (contentSizeHeight) + 1,
             Y = Pos.Top (labelContentSize),
             CanFocus = false
@@ -252,7 +259,30 @@ public class VirtualContent : Scenario
             }
         }
 
-        view.Padding.Add (labelContentSize, contentSizeWidth, labelComma, contentSizeHeight, cbClearOnlyVisible);
+
+        var cbDoNotClipContent = new CheckBox
+        {
+            Title = "ClipVisibleContentOnly",
+            X = Pos.Right (cbClearOnlyVisible) + 1,
+            Y = Pos.Top (labelContentSize),
+            CanFocus = false
+        };
+        cbDoNotClipContent.Checked = view.ViewportSettings.HasFlag (ViewportSettings.ClipVisibleContentOnly);
+        cbDoNotClipContent.Toggled += ClipVisibleContentOnly_Toggled;
+
+        void ClipVisibleContentOnly_Toggled (object sender, StateEventArgs<bool?> e)
+        {
+            if (e.NewValue == true)
+            {
+                view.ViewportSettings |= ViewportSettings.ClipVisibleContentOnly;
+            }
+            else
+            {
+                view.ViewportSettings &= ~ViewportSettings.ClipVisibleContentOnly;
+            }
+        }
+
+        view.Padding.Add (labelContentSize, contentSizeWidth, labelComma, contentSizeHeight, cbClearOnlyVisible, cbDoNotClipContent);
 
         // Add demo views to show that things work correctly
         var textField = new TextField { X = 20, Y = 7, Width = 15, Text = "Test TextField" };
@@ -294,7 +324,10 @@ public class VirtualContent : Scenario
         charMap.Accept += (s, e) =>
                               MessageBox.Query (20, 7, "Hi", $"Am I a {view.GetType ().Name}?", "Yes", "No");
 
-        var buttonAnchoredRight = new Button { X = Pos.AnchorEnd (10), Y = 0, Text = "Button" };
+        var buttonAnchoredRight = new Button
+        {
+            X = Pos.AnchorEnd (10), Y = 0, Text = "Button"
+        };
 
         var labelAnchoredBottomLeft = new Label
         {
@@ -321,7 +354,7 @@ public class VirtualContent : Scenario
             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."
+                "This label is long. It should clip to the ContentArea * if ClearVisibleContentOnly is not set. This is a virtual scrolling demo. Use the arrow keys and/or mouse wheel to scroll the content."
         };
         longLabel.TextFormatter.WordWrap = true;
         view.Add (longLabel);

+ 1 - 1
UnitTests/Drawing/LineCanvasTests.cs

@@ -1313,7 +1313,7 @@ public class LineCanvasTests
 
         v.DrawContentComplete += (s, e) =>
                                  {
-                                     v.Clear (v.Viewport);
+                                     v.FillRect (v.Viewport);
 
                                      foreach (KeyValuePair<Point, Rune> p in canvasCopy.GetMap ())
                                      {

+ 1 - 1
UnitTests/UICatalog/ScenarioTests.cs

@@ -239,7 +239,7 @@ public class ScenarioTests
                                                       _hostPane.Remove (_curView);
                                                       _curView.Dispose ();
                                                       _curView = null;
-                                                      _hostPane.Clear (_hostPane.Viewport);
+                                                      _hostPane.FillRect (_hostPane.Viewport);
                                                   }
 
                                                   _curView = CreateClass (_viewClasses.Values.ToArray () [_classListView.SelectedItem]);

+ 162 - 15
UnitTests/View/DrawTests.cs

@@ -1,6 +1,7 @@
 #nullable enable
 using System.Text;
 using Xunit.Abstractions;
+using static System.Net.Mime.MediaTypeNames;
 
 namespace Terminal.Gui.ViewTests;
 
@@ -22,7 +23,7 @@ public class DrawTests (ITestOutputHelper output)
         // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen)
 
         view.Move (0, 0);
-        Assert.Equal(new Point(2, 2), new Point (Application.Driver.Col, Application.Driver.Row));
+        Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row));
 
         view.Move (-1, -1);
         Assert.Equal (new Point (2, 2), new Point (Application.Driver.Col, Application.Driver.Row));
@@ -43,14 +44,14 @@ public class DrawTests (ITestOutputHelper output)
         };
         view.Margin.Thickness = new Thickness (1);
         View.Diagnostics = ViewDiagnosticFlags.Padding;
-        view.BeginInit();
-        view.EndInit();
-        view.Draw();
+        view.BeginInit ();
+        view.EndInit ();
+        view.Draw ();
 
         // Only valid location w/in Viewport is 0, 0 (view) - 2, 2 (screen)
         Assert.Equal ((Rune)' ', Application.Driver.Contents [2, 2].Rune);
 
-        view.AddRune(0, 0, Rune.ReplacementChar);
+        view.AddRune (0, 0, Rune.ReplacementChar);
         Assert.Equal (Rune.ReplacementChar, Application.Driver.Contents [2, 2].Rune);
 
         view.AddRune (-1, -1, Rune.ReplacementChar);
@@ -67,7 +68,7 @@ public class DrawTests (ITestOutputHelper output)
     [InlineData (0, 0, 2, 2)]
     [InlineData (-1, -1, 2, 2)]
     [SetupFakeDriver]
-    public void Clear_Clears_Only_Viewport (int x, int y, int width, int height)
+    public void FillRect_Fills_HonorsClip (int x, int y, int width, int height)
     {
         var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () };
 
@@ -91,15 +92,16 @@ public class DrawTests (ITestOutputHelper output)
  └─┘",
                                                       output);
 
-        Rectangle toClear = new (x, y, width, height);
-        view.Clear (toClear);
+        Rectangle toFill = new (x, y, width, height);
+        view.FillRect (toFill);
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
  ┌─┐
  │ │
  └─┘",
                                                       output);
-        // Now try to clear beyond Viewport (invalid)
+
+        // Now try to clear beyond Viewport (invalid; clipping should prevent)
         superView.SetNeedsDisplay ();
         superView.Draw ();
         TestHelpers.AssertDriverContentsWithFrameAre (
@@ -108,8 +110,8 @@ public class DrawTests (ITestOutputHelper output)
  │X│
  └─┘",
                                                       output);
-        toClear = new (-width, -height, width, height);
-        view.Clear (toClear);
+        toFill = new (-width, -height, width, height);
+        view.FillRect (toFill);
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
  ┌─┐
@@ -126,8 +128,8 @@ public class DrawTests (ITestOutputHelper output)
  │X│
  └─┘",
                                                       output);
-        toClear = new (-1, -1, width + 1, height + 1);
-        view.Clear (toClear);
+        toFill = new (-1, -1, width + 1, height + 1);
+        view.FillRect (toFill);
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
  ┌─┐
@@ -144,8 +146,85 @@ public class DrawTests (ITestOutputHelper output)
  │X│
  └─┘",
                                                       output);
-        toClear = new (0, 0, width * 2, height * 2);
-        view.Clear (toClear);
+        toFill = new (0, 0, width * 2, height * 2);
+        view.FillRect (toFill);
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+ ┌─┐
+ │ │
+ └─┘",
+                                                      output);
+    }
+
+    [Theory]
+    [InlineData (0, 0, 1, 1)]
+    [InlineData (0, 0, 2, 2)]
+    [InlineData (-1, -1, 2, 2)]
+    [SetupFakeDriver]
+    public void Clear_ClearsEntireViewport (int x, int y, int width, int height)
+    {
+        var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        var view = new View
+        {
+            Text = "X",
+            X = 1, Y = 1,
+            Width = 3, Height = 3,
+            BorderStyle = LineStyle.Single
+        };
+        superView.Add (view);
+        superView.BeginInit ();
+        superView.EndInit ();
+        superView.LayoutSubviews ();
+
+        superView.Draw ();
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+ ┌─┐
+ │X│
+ └─┘",
+                                                      output);
+
+        view.Clear ();
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+ ┌─┐
+ │ │
+ └─┘",
+                                                      output);
+    }
+
+    [Theory]
+    [InlineData (0, 0, 1, 1)]
+    [InlineData (0, 0, 2, 2)]
+    [InlineData (-1, -1, 2, 2)]
+    [SetupFakeDriver]
+    public void Clear_WithClearVisibleContentOnly_ClearsVisibleContentOnly (int x, int y, int width, int height)
+    {
+        var superView = new View { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        var view = new View
+        {
+            Text = "X",
+            X = 1, Y = 1,
+            Width = 3, Height = 3,
+            BorderStyle = LineStyle.Single,
+            ViewportSettings = ViewportSettings.ClearVisibleContentOnly
+        };
+        superView.Add (view);
+        superView.BeginInit ();
+        superView.EndInit ();
+        superView.LayoutSubviews ();
+
+        superView.Draw ();
+        TestHelpers.AssertDriverContentsWithFrameAre (
+                                                      @"
+ ┌─┐
+ │X│
+ └─┘",
+                                                      output);
+
+        view.Clear ();
         TestHelpers.AssertDriverContentsWithFrameAre (
                                                       @"
  ┌─┐
@@ -872,4 +951,72 @@ public class DrawTests (ITestOutputHelper output)
                                                expectedColors
                                               );
     }
+
+    [Fact]
+    [SetupFakeDriver]
+    public void SetClip_ClipVisibleContentOnly_VisibleContentIsClipped ()
+    {
+        // Screen is 25x25
+        // View is 25x25
+        // Viewport is (0, 0, 23, 23)
+        // ContentSize is (10, 10)
+        // ViewportToScreen is (1, 1, 23, 23)
+        // Visible content is (1, 1, 10, 10)
+        // Expected clip is (1, 1, 10, 10) - same as visible content
+        Rectangle expectedClip = new (1, 1, 10, 10);
+        // Arrange
+        var view = new View ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            ContentSize = new Size (10, 10),
+            ViewportSettings = ViewportSettings.ClipVisibleContentOnly
+        };
+        view.Border.Thickness = new Thickness (1);
+        view.BeginInit ();
+        view.EndInit ();
+        Assert.Equal (view.Frame, Application.Driver.Clip);
+
+        // Act
+        view.SetClip ();
+
+        // Assert
+        Assert.Equal (expectedClip, Application.Driver.Clip);
+        view.Dispose ();
+    }
+
+    [Fact]
+    [SetupFakeDriver]
+    public void SetClip_Default_ClipsToViewport ()
+    {
+        // Screen is 25x25
+        // View is 25x25
+        // Viewport is (0, 0, 23, 23)
+        // ContentSize is (10, 10)
+        // ViewportToScreen is (1, 1, 23, 23)
+        // Visible content is (1, 1, 10, 10)
+        // Expected clip is (1, 1, 23, 23) - same as Viewport
+        Rectangle expectedClip = new (1, 1, 23, 23);
+        // Arrange
+        var view = new View ()
+        {
+            Width = Dim.Fill (),
+            Height = Dim.Fill (),
+            ContentSize = new Size (10, 10),
+        };
+        view.Border.Thickness = new Thickness (1);
+        view.BeginInit ();
+        view.EndInit ();
+        Assert.Equal (view.Frame, Application.Driver.Clip);
+        view.Viewport = view.Viewport with { X = 1, Y = 1 };
+
+        // Act
+        view.SetClip ();
+
+        // Assert
+        Assert.Equal (expectedClip, Application.Driver.Clip);
+        view.Dispose ();
+    }
+
+
 }

+ 2 - 2
UnitTests/View/ViewTests.cs

@@ -54,7 +54,7 @@ public class ViewTests
         Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
         Assert.Equal (new Rectangle (0, 0, 20, 10), pos);
 
-        view.Clear (view.Viewport);
+        view.FillRect (view.Viewport);
 
         expected = @"
 ┌──────────────────┐
@@ -118,7 +118,7 @@ public class ViewTests
         Rectangle pos = TestHelpers.AssertDriverContentsWithFrameAre (expected, _output);
         Assert.Equal (new Rectangle (0, 0, 20, 10), pos);
 
-        view.Clear (view.Viewport);
+        view.FillRect (view.Viewport);
 
         expected = @"
 ┌──────────────────┐