Browse Source

Fixes #4492, #4480 - Transparent shadows cause underlying wide glyph rendering issues (#4490)

* WIP - experiments in fixing shadow rendering issues based on #4465

Previously, shadow size was fixed at 1x1. This change introduces ShadowWidth and ShadowHeight properties to both Margin and View, allowing variable shadow dimensions. The Margin class now manages its own shadow sizing, enforcing valid values based on ShadowStyle (e.g., Opaque and Transparent require a minimum of 1, and Opaque only allows 1). Margin.Thickness is dynamically adjusted to account for shadow size, with original values preserved and restored as needed.

ShadowView rendering is updated to correctly handle wide graphemes (such as emojis) in the shadow area, preventing rendering errors. The View class exposes ShadowWidth and ShadowHeight, synchronizing with Margin. Extensive new unit tests verify correct behavior for shadow sizing, style changes, thickness adjustments, and rendering, including edge cases and visual output.

Additional minor bug fixes and refactoring are included, such as proper management of Margin's cached clip region and correcting a loop order bug in ShadowView. The codebase is also modernized with recent C# features.

* more merge

* added border tests

* Experiment...

* Incorporated latest wideglyphs

* Comment tweaks

* Add Adornments and ViewportSettings editors to WideGlyphs

Introduce AdornmentsEditor and ViewportSettingsEditor with custom border styles and positioning, enhancing UI editing capabilities. Also update arrangeableViewAtEven to use Color.Black and Color.Green, and adjust a commented border style from Dashed to Dotted.

* Fix scenario editors and tweak scenarios.

Enhance ShadowStyles with a second shadow window (transparent style) and a button event handler that shows a message box. In WideGlyphs, add AdornmentsEditor and ViewportSettingsEditor for view property editing, apply custom color schemes to arrangeable views, and update superView with a transparent shadow and increased shadow width. These changes improve interactivity and visualization in the demo scenarios.

* Fix scenario editors and tweak scenarios.

Enhance ShadowStyles with a second shadow window (transparent style) and a button event handler that shows a message box. In WideGlyphs, add AdornmentsEditor and ViewportSettingsEditor for view property editing, apply custom color schemes to arrangeable views, and update superView with a transparent shadow and increased shadow width. These changes improve interactivity and visualization in the demo scenarios.

* Make replacement char themeable via Glyphs.ReplacementChar

Adds Glyphs.ReplacementChar as a configurable replacement character, replacing all uses of Rune.ReplacementChar. The default is now a space (' ') and can be set via config.json. Updates all rendering, string decoding, and buffer invalidation logic to use the new property, ensuring consistency and themeability. Updates tests and comments accordingly. Also includes minor UI tweaks in WideGlyphs.cs and .DotSettings updates.

* merging

* merge errors

* merged

* merged

* Refactor shadow properties to Margin; update tests

ShadowWidth and ShadowHeight are now managed solely in the Margin class, with related properties and validation logic removed from View. All code and tests now use view.Margin.ShadowWidth/ShadowHeight. Tests and documentation were updated accordingly, and wide glyph handling in test output was improved for consistency.

* Simplify ShadowSize; remove it from View as it's infreqnetly used. Make it a Size to reduce API surface area.

Replace ShadowWidth/ShadowHeight with a single ShadowSize property (of type Size) in the Margin class and related code. Update all usages, validation logic, and tests to use ShadowSize.Width and ShadowSize.Height. Introduce TryValidateShadowSize for unified validation. Modernize code with C# features and improve clarity and maintainability by treating shadow dimensions as a single unit.

* reveted

* Fix wide glyph attribute handling for second column

Ensure the attribute for the second column of wide glyphs is set correctly when within the clip region, addressing issues #4258 and #4492. Add comprehensive unit tests to verify correct attribute assignment and output rendering, including scenarios with transparent shadows. Remove obsolete test code for clarity. This improves color/style consistency for wide glyphs, especially in overlapping UI situations.

* added url
Tig 1 day ago
parent
commit
0a9f4b8ef1

+ 18 - 21
Examples/UICatalog/Scenarios/WideGlyphs.cs

@@ -1,4 +1,4 @@
-#nullable enable
+#nullable enable
 
 using System.Text;
 
@@ -90,29 +90,16 @@ public sealed class WideGlyphs : Scenario
                     Rune codepoint = _codepoints [r, c];
                     if (codepoint != default (Rune))
                     {
-                        view.AddRune (c, r, codepoint);
+                        view.Move (c, r);
+                        Attribute attr = view.GetAttributeForRole (VisualRole.Normal);
+                        view.SetAttribute (attr with { Background = attr.Background + (r * 5) });
+                        view.AddRune (codepoint);
                     }
                 }
             }
             e.DrawContext?.AddDrawnRectangle (view.Viewport);
         };
 
-        Line verticalLineAtEven = new ()
-        {
-            X = 10,
-            Orientation = Orientation.Vertical,
-            Length = Dim.Fill ()
-        };
-        appWindow.Add (verticalLineAtEven);
-
-        Line verticalLineAtOdd = new ()
-        {
-            X = 25,
-            Orientation = Orientation.Vertical,
-            Length = Dim.Fill ()
-        };
-        appWindow.Add (verticalLineAtOdd);
-
         View arrangeableViewAtEven = new ()
         {
             CanFocus = true,
@@ -124,13 +111,16 @@ public sealed class WideGlyphs : Scenario
             //BorderStyle = LineStyle.Dashed
         };
 
+        arrangeableViewAtEven.SetScheme (new () { Normal = new (Color.Black, Color.Green) });
+
         // Proves it's not LineCanvas related
         arrangeableViewAtEven!.Border!.Thickness = new (1);
         arrangeableViewAtEven.Border.Add (new View () { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Even" });
         appWindow.Add (arrangeableViewAtEven);
 
-        View arrangeableViewAtOdd = new ()
+        Button arrangeableViewAtOdd = new ()
         {
+            Title = $"你 {Glyphs.Apple}",
             CanFocus = true,
             Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
             X = 31,
@@ -138,8 +128,12 @@ public sealed class WideGlyphs : Scenario
             Width = 15,
             Height = 5,
             BorderStyle = LineStyle.Dashed,
+            SchemeName = "error"
         };
-
+        arrangeableViewAtOdd.Accepting += (sender, args) =>
+                                          {
+                                              MessageBox.Query ((sender as View)?.App, "Button Pressed", "You Pressed it!");
+                                          };
         appWindow.Add (arrangeableViewAtOdd);
 
         var superView = new View
@@ -150,8 +144,11 @@ public sealed class WideGlyphs : Scenario
             Width = Dim.Auto (),
             Height = Dim.Auto (),
             BorderStyle = LineStyle.Single,
-            Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable
+            Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable,
+            ShadowStyle = ShadowStyle.Transparent,
         };
+        superView.Margin!.ShadowSize = superView.Margin!.ShadowSize with { Width = 2 };
+
 
         Rune codepoint = Glyphs.Apple;
 

+ 11 - 2
Terminal.Gui/Drivers/OutputBufferImpl.cs

@@ -185,8 +185,17 @@ public class OutputBufferImpl : IOutputBuffer
             if (printableGraphemeWidth > 1)
             {
                 // Skip the second column of a wide character
-                // IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
-                // See: https://github.com/gui-cs/Terminal.Gui/issues/4258
+                // See issue: https://github.com/gui-cs/Terminal.Gui/issues/4492
+                // Test: AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly
+                // Test: AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip
+                if (Clip.Contains (Col, Row))
+                {
+                    // IMPORTANT: We do NOT modify column N+1's IsDirty or Attribute here.
+                    // See: https://github.com/gui-cs/Terminal.Gui/issues/4258
+                    Contents [Row, Col].Attribute = CurrentAttribute;
+                }
+
+                // Advance cursor again for wide character
                 Col++;
             }
         }

+ 129 - 22
Terminal.Gui/ViewBase/Adornment/Margin.cs

@@ -1,8 +1,3 @@
-
-
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-
 namespace Terminal.Gui.ViewBase;
 
 /// <summary>The Margin for a <see cref="View"/>. Accessed via <see cref="View.Margin"/></summary>
@@ -21,8 +16,6 @@ namespace Terminal.Gui.ViewBase;
 /// </remarks>
 public class Margin : Adornment
 {
-    private const int SHADOW_WIDTH = 1;
-    private const int SHADOW_HEIGHT = 1;
     private const int PRESS_MOVE_HORIZONTAL = 1;
     private const int PRESS_MOVE_VERTICAL = 0;
 
@@ -35,6 +28,7 @@ public class Margin : Adornment
     public Margin (View parent) : base (parent)
     {
         SubViewLayout += Margin_LayoutStarted;
+        ThicknessChanged += OnThicknessChanged;
 
         // Margin should not be focusable
         CanFocus = false;
@@ -46,6 +40,15 @@ public class Margin : Adornment
         ViewportSettings |= ViewportSettingsFlags.TransparentMouse;
     }
 
+    private void OnThicknessChanged (object? sender, EventArgs e)
+    {
+        if (!_isThicknessChanging)
+        {
+            _originalThickness = new (Thickness.Left, Thickness.Top, Thickness.Right, Thickness.Bottom);
+            SetShadow (ShadowStyle);
+        }
+    }
+
     // When the Parent is drawn, we cache the clip region so we can draw the Margin after all other Views
     // QUESTION: Why can't this just be the NeedsDisplay region?
     private Region? _cachedClip;
@@ -56,7 +59,7 @@ public class Margin : Adornment
 
     internal void CacheClip ()
     {
-        if (Thickness != Thickness.Empty /*&& ShadowStyle != ShadowStyle.None*/)
+        if (Thickness != Thickness.Empty && ShadowStyle != ShadowStyle.None)
         {
             // PERFORMANCE: How expensive are these clones?
             _cachedClip = GetClip ()?.Clone ();
@@ -64,12 +67,15 @@ public class Margin : Adornment
     }
 
     /// <summary>
-    ///     INTERNAL API - Draws the margins for the specified views. This is called by the <see cref="Application"/> on each
+    ///     INTERNAL API - Draws the transparent margins for the specified views. This is called from <see cref="View.Draw"/> on each
     ///     iteration of the main loop after all Views have been drawn.
     /// </summary>
+    /// <remarks>
+    ///     Non-transparent margins are drawn as-normal in <see cref="View.DrawAdornments"/>.
+    /// </remarks>
     /// <param name="views"></param>
     /// <returns><see langword="true"/></returns>
-    internal static bool DrawMargins (IEnumerable<View> views)
+    internal static bool DrawTransparentMargins (IEnumerable<View> views)
     {
         Stack<View> stack = new (views);
 
@@ -77,7 +83,10 @@ public class Margin : Adornment
         {
             View view = stack.Pop ();
 
-            if (view.Margin is { } margin && margin.Thickness != Thickness.Empty && margin.GetCachedClip () != null)
+            if (view.Margin is { } margin
+                && margin.Thickness != Thickness.Empty
+                && margin.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent)
+                && margin.GetCachedClip () != null)
             {
                 margin.SetNeedsDraw ();
                 Region? saved = view.GetClip ();
@@ -87,8 +96,6 @@ public class Margin : Adornment
                 margin.ClearCachedClip ();
             }
 
-            view.ClearNeedsDraw ();
-
             foreach (View subview in view.SubViews)
             {
                 stack.Push (subview);
@@ -134,7 +141,7 @@ public class Margin : Adornment
         if (ShadowStyle != ShadowStyle.None)
         {
             // Don't clear where the shadow goes
-            screen = Rectangle.Inflate (screen, -SHADOW_WIDTH, -SHADOW_HEIGHT);
+            screen = Rectangle.Inflate (screen, -ShadowSize.Width, -ShadowSize.Height);
         }
 
         return true;
@@ -151,6 +158,8 @@ public class Margin : Adornment
     // private bool _pressed;
     private ShadowView? _bottomShadow;
     private ShadowView? _rightShadow;
+    private bool _isThicknessChanging;
+    private Thickness? _originalThickness;
 
     /// <summary>
     ///     Sets whether the Margin includes a shadow effect. The shadow is drawn on the right and bottom sides of the
@@ -172,25 +181,29 @@ public class Margin : Adornment
             _bottomShadow = null;
         }
 
+        _originalThickness ??= Thickness;
+
         if (ShadowStyle != ShadowStyle.None)
         {
             // Turn off shadow
-            Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right - SHADOW_WIDTH, Thickness.Bottom - SHADOW_HEIGHT);
+            _originalThickness = new (Thickness.Left, Thickness.Top, Math.Max (Thickness.Right - ShadowSize.Width, 0), Math.Max (Thickness.Bottom - ShadowSize.Height, 0));
         }
 
         if (style != ShadowStyle.None)
         {
             // Turn on shadow
-            Thickness = new (Thickness.Left, Thickness.Top, Thickness.Right + SHADOW_WIDTH, Thickness.Bottom + SHADOW_HEIGHT);
+            _isThicknessChanging = true;
+            Thickness = new (_originalThickness.Value.Left, _originalThickness.Value.Top, _originalThickness.Value.Right + ShadowSize.Width, _originalThickness.Value.Bottom + ShadowSize.Height);
+            _isThicknessChanging = false;
         }
 
         if (style != ShadowStyle.None)
         {
             _rightShadow = new ()
             {
-                X = Pos.AnchorEnd (SHADOW_WIDTH),
+                X = Pos.AnchorEnd (ShadowSize.Width),
                 Y = 0,
-                Width = SHADOW_WIDTH,
+                Width = ShadowSize.Width,
                 Height = Dim.Fill (),
                 ShadowStyle = style,
                 Orientation = Orientation.Vertical
@@ -199,14 +212,20 @@ public class Margin : Adornment
             _bottomShadow = new ()
             {
                 X = 0,
-                Y = Pos.AnchorEnd (SHADOW_HEIGHT),
+                Y = Pos.AnchorEnd (ShadowSize.Height),
                 Width = Dim.Fill (),
-                Height = SHADOW_HEIGHT,
+                Height = ShadowSize.Height,
                 ShadowStyle = style,
                 Orientation = Orientation.Horizontal
             };
             Add (_rightShadow, _bottomShadow);
         }
+        else if (Thickness != _originalThickness)
+        {
+            _isThicknessChanging = true;
+            Thickness = new (_originalThickness.Value.Left, _originalThickness.Value.Top, _originalThickness.Value.Right, _originalThickness.Value.Bottom);
+            _isThicknessChanging = false;
+        }
 
         return style;
     }
@@ -215,7 +234,90 @@ public class Margin : Adornment
     public override ShadowStyle ShadowStyle
     {
         get => base.ShadowStyle;
-        set => base.ShadowStyle = SetShadow (value);
+        set
+        {
+            if (value == ShadowStyle.Opaque || (value == ShadowStyle.Transparent && (ShadowSize.Width == 0 || ShadowSize.Height == 0)))
+            {
+                if (ShadowSize.Width != 1)
+                {
+                    ShadowSize = ShadowSize with { Width = 1 };
+                }
+
+                if (ShadowSize.Height != 1)
+                {
+                    ShadowSize = ShadowSize with { Height = 1 };
+                }
+            }
+
+            base.ShadowStyle = SetShadow (value);
+        }
+    }
+
+    private Size _shadowSize;
+
+    /// <summary>
+    ///     Gets or sets the size of the shadow effect.
+    /// </summary>
+    public Size ShadowSize
+    {
+        get => _shadowSize;
+        set
+        {
+            if (TryValidateShadowSize (_shadowSize, value, out Size result))
+            {
+                _shadowSize = value;
+                SetShadow (ShadowStyle);
+            }
+            else
+            {
+                _shadowSize = result;
+            }
+        }
+    }
+
+    private bool TryValidateShadowSize (Size originalValue, in Size newValue, out Size result)
+    {
+        result = newValue;
+
+        bool wasValid = true;
+
+        if (newValue.Width < 0)
+        {
+            result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Width = 1 } : originalValue;
+
+            wasValid = false;
+        }
+
+
+        if (newValue.Height < 0)
+        {
+            result = ShadowStyle is ShadowStyle.Opaque or ShadowStyle.Transparent ? result with { Height = 1 } : originalValue;
+
+            wasValid = false;
+        }
+
+        if (!wasValid)
+        {
+            return false;
+        }
+
+        bool wasUpdated = false;
+
+        if ((ShadowStyle == ShadowStyle.Opaque && newValue.Width != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Width < 1))
+        {
+            result = result with { Width = 1 };
+
+            wasUpdated = true;
+        }
+
+        if ((ShadowStyle == ShadowStyle.Opaque && newValue.Height != 1) || (ShadowStyle == ShadowStyle.Transparent && newValue.Height < 1))
+        {
+            result = result with { Height = 1 };
+
+            wasUpdated = true;
+        }
+
+        return !wasUpdated;
     }
 
     private void OnParentOnMouseStateChanged (object? sender, EventArgs<MouseState> args)
@@ -226,7 +328,7 @@ public class Margin : Adornment
         }
 
         bool pressed = args.Value.HasFlag (MouseState.Pressed) && parent.HighlightStates.HasFlag (MouseState.Pressed);
-        bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside); ;
+        bool pressedOutside = args.Value.HasFlag (MouseState.PressedOutside) && parent.HighlightStates.HasFlag (MouseState.PressedOutside);
 
         if (pressedOutside)
         {
@@ -238,11 +340,13 @@ public class Margin : Adornment
             // If the view is pressed and the highlight is being removed, move the shadow back.
             // Note, for visual effects reasons, we only move horizontally.
             // TODO: Add a setting or flag that lets the view move vertically as well.
+            _isThicknessChanging = true;
             Thickness = new (
                              Thickness.Left - PRESS_MOVE_HORIZONTAL,
                              Thickness.Top - PRESS_MOVE_VERTICAL,
                              Thickness.Right + PRESS_MOVE_HORIZONTAL,
                              Thickness.Bottom + PRESS_MOVE_VERTICAL);
+            _isThicknessChanging = false;
 
             if (_rightShadow is { })
             {
@@ -264,11 +368,14 @@ public class Margin : Adornment
             // If the view is not pressed, and we want highlight move the shadow
             // Note, for visual effects reasons, we only move horizontally.
             // TODO: Add a setting or flag that lets the view move vertically as well.
+            _isThicknessChanging = true;
             Thickness = new (
                              Thickness.Left + PRESS_MOVE_HORIZONTAL,
                              Thickness.Top + PRESS_MOVE_VERTICAL,
                              Thickness.Right - PRESS_MOVE_HORIZONTAL,
                              Thickness.Bottom - PRESS_MOVE_VERTICAL);
+            _isThicknessChanging = false;
+
             MouseState |= MouseState.Pressed;
 
             if (_rightShadow is { })

+ 21 - 5
Terminal.Gui/ViewBase/Adornment/ShadowView.cs

@@ -100,7 +100,13 @@ internal class ShadowView : View
 
                 if (c < ScreenContents?.GetLength (1) && r < ScreenContents?.GetLength (0))
                 {
-                    AddStr (ScreenContents [r, c].Grapheme);
+                    string grapheme = ScreenContents [r, c].Grapheme;
+                    AddStr (grapheme);
+
+                    if (grapheme.GetColumns () > 1)
+                    {
+                        c++;
+                    }
                 }
             }
         }
@@ -125,21 +131,31 @@ internal class ShadowView : View
         Rectangle screen = ViewportToScreen (Viewport);
 
         // Fill in the rest of the rectangle
-        for (int c = Math.Max (0, screen.X); c < screen.X + screen.Width; c++)
+        for (int r = Math.Max (0, screen.Y); r < screen.Y + viewport.Height; r++)
         {
-            for (int r = Math.Max (0, screen.Y); r < screen.Y + viewport.Height; r++)
+            for (int c = Math.Max (0, screen.X); c < screen.X + screen.Width; c++)
             {
                 Driver?.Move (c, r);
                 SetAttribute (GetAttributeUnderLocation (new (c, r)));
 
-                if (ScreenContents is { } && screen.X < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0))
+                if (ScreenContents is { } && screen.X < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0)
+                    && c < ScreenContents.GetLength (1) && r < ScreenContents.GetLength (0))
                 {
-                    AddStr (ScreenContents [r, c].Grapheme);
+                    string grapheme = ScreenContents [r, c].Grapheme;
+                    AddStr (grapheme);
+
+                    if (grapheme.GetColumns () > 1)
+                    {
+                        c++;
+                    }
                 }
             }
         }
     }
 
+    // BUGBUG: This will never really work completely right by looking at an underlying cell and trying
+    // BUGBUG: to do transparency by adjusting colors. Instead, it might be possible to use the A in argb for this.
+    // BUGBUG: See https://github.com/gui-cs/Terminal.Gui/issues/4491
     private Attribute GetAttributeUnderLocation (Point location)
     {
         if (SuperView is not Adornment

+ 20 - 4
Terminal.Gui/ViewBase/View.Drawing.cs

@@ -28,8 +28,8 @@ public partial class View // Drawing APIs
             view.Draw (context);
         }
 
-        // Draw the margins last to ensure they are drawn on top of the content.
-        Margin.DrawMargins (viewsArray);
+        // Draw Transparent margins last to ensure they are drawn on top of the content.
+        Margin.DrawTransparentMargins (viewsArray);
 
         // DrawMargins may have caused some views have NeedsDraw/NeedsSubViewDraw set; clear them all.
         foreach (View view in viewsArray)
@@ -183,7 +183,18 @@ public partial class View // Drawing APIs
 
     private void DoDrawAdornmentsSubViews ()
     {
-        // NOTE: We do not support SubViews of Margin
+        // Only SetNeedsDraw on Margin here if it is not Transparent. Transparent Margins are drawn in a separate pass in the static View.Draw
+        // via Margin.DrawTransparentMargins.
+        if (Margin is { NeedsDraw: true } && !Margin.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
+        {
+            foreach (View subview in Margin.SubViews)
+            {
+                subview.SetNeedsDraw ();
+            }
+
+            // NOTE: We do not support arbitrary SubViews of Margin (only ShadowView)
+            // NOTE: so we do not call DoDrawSubViews on Margin.
+        }
 
         if (Border?.SubViews is { } && Border.Thickness != Thickness.Empty && Border.NeedsDraw)
         {
@@ -268,7 +279,12 @@ public partial class View // Drawing APIs
     /// </remarks>
     public void DrawAdornments ()
     {
-        // We do not attempt to draw Margin. It is drawn in a separate pass.
+        // Only draw Margin here if it is not Transparent. Transparent Margins are drawn in a separate pass in the static View.Draw
+        // via Margin.DrawTransparentMargins.
+        if (Margin is { } && !Margin.ViewportSettings.HasFlag(ViewportSettingsFlags.Transparent) && Margin.Thickness != Thickness.Empty)
+        {
+            Margin?.Draw ();
+        }
 
         // Each of these renders lines to this View's LineCanvas
         // Those lines will be finally rendered in OnRenderLineCanvas

+ 2 - 0
Terminal.sln.DotSettings

@@ -382,6 +382,7 @@
 	<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/EnableEditorConfigSupport/@EntryValue">False</s:Boolean>
 	<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/ShowEditorConfigStatusBarIndicator/@EntryValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/CodeStyle/EditorConfig/SyncToVisualStudio/@EntryValue">True</s:Boolean>
+	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=BMP/@EntryIndexedValue">BMP</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=CWP/@EntryIndexedValue">CWP</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LL/@EntryIndexedValue">LL</s:String>
 	<s:String x:Key="/Default/CodeStyle/Naming/CSharpNaming/Abbreviations/=LR/@EntryIndexedValue">LR</s:String>
@@ -431,6 +432,7 @@
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Roslynator/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=RRGGBB/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=runnables/@EntryIndexedValue">True</s:Boolean>
+	<s:Boolean x:Key="/Default/UserDictionary/Words/=snek/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Toplevel/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Runnables/@EntryIndexedValue">True</s:Boolean>
 	<s:Boolean x:Key="/Default/UserDictionary/Words/=Ungrab/@EntryIndexedValue">True</s:Boolean>

+ 3 - 3
Tests/UnitTests/View/Draw/ClipTests.cs

@@ -52,13 +52,13 @@ public class ClipTests (ITestOutputHelper _output)
         Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme);
 
         // When we exit Draw, the view is excluded from the clip. So drawing at 0,0, is not valid and is clipped.
-        view.AddRune (0, 0, Rune.ReplacementChar);
+        view.AddRune (0, 0, Glyphs.WideGlyphReplacement);
         Assert.Equal (" ", Application.Driver?.Contents! [2, 2].Grapheme);
 
-        view.AddRune (-1, -1, Rune.ReplacementChar);
+        view.AddRune (-1, -1, Glyphs.WideGlyphReplacement);
         Assert.Equal ("P", Application.Driver?.Contents! [1, 1].Grapheme);
 
-        view.AddRune (1, 1, Rune.ReplacementChar);
+        view.AddRune (1, 1, Glyphs.WideGlyphReplacement);
         Assert.Equal ("P", Application.Driver?.Contents! [3, 3].Grapheme);
     }
 

+ 122 - 45
Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs

@@ -50,25 +50,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         Assert.Equal (expected, driver.Contents [0, 0].Grapheme);
         Assert.Equal (" ", driver.Contents [0, 1].Grapheme);
 
-        //		var s = "a\u0301\u0300\u0306";
-
-        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
-        //ắ", output);
-
-        //		tf.Text = "\u1eaf";
-        //		Application.Refresh ();
-        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
-        //ắ", output);
-
-        //		tf.Text = "\u0103\u0301";
-        //		Application.Refresh ();
-        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
-        //ắ", output);
-
-        //		tf.Text = "\u0061\u0306\u0301";
-        //		Application.Refresh ();
-        //		DriverAsserts.AssertDriverContentsWithFrameAre (@"
-        //ắ", output);
         driver.Dispose ();
     }
 
@@ -148,31 +129,6 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         Assert.Equal (0, driver.Row);
         Assert.Equal (2, driver.Col);
 
-        //driver.AddRune ('b');
-        //Assert.Equal ((Text)'b', driver.Contents [0, 1].Text);
-        //Assert.Equal (0, driver.Row);
-        //Assert.Equal (2, driver.Col);
-
-        //// Move to the last column of the first row
-        //var lastCol = driver.Cols - 1;
-        //driver.Move (lastCol, 0);
-        //Assert.Equal (0, driver.Row);
-        //Assert.Equal (lastCol, driver.Col);
-
-        //// Add a rune to the last column of the first row; should increment the row or col even though it's now invalid
-        //driver.AddRune ('c');
-        //Assert.Equal ((Text)'c', driver.Contents [0, lastCol].Text);
-        //Assert.Equal (lastCol + 1, driver.Col);
-
-        //// Add a rune; should succeed but do nothing as it's outside of Contents
-        //driver.AddRune ('d');
-        //Assert.Equal (lastCol + 2, driver.Col);
-        //for (var col = 0; col < driver.Cols; col++) {
-        //	for (var row = 0; row < driver.Rows; row++) {
-        //		Assert.NotEqual ((Text)'d', driver.Contents [row, col].Text);
-        //	}
-        //}
-
         driver.Dispose ();
     }
 
@@ -183,7 +139,7 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         driver.SetScreenSize (6, 3);
         driver.GetOutputBuffer ().SetWideGlyphReplacement ((Rune)'①');
 
-        driver!.Clip = new (driver.Screen);
+        driver.Clip = new (driver.Screen);
         driver.Move (1, 0);
         driver.AddStr ("┌");
         driver.Move (2, 0);
@@ -207,4 +163,125 @@ public class AddRuneTests (ITestOutputHelper output) : FakeDriverBase
         DriverAssert.AssertDriverOutputIs (@"\x1b[38;2;0;0;0m\x1b[48;2;0;0;0m①┌─┐🍎\x1b[38;2;255;255;255m\x1b[48;2;0;0;0m",
                                            output, driver);
     }
+
+    [Fact]
+    public void AddStr_WideGlyph_Second_Column_Attribute_Set_When_In_Clip ()
+    {
+        // This test verifies the fix for issue #4258
+        // When a wide glyph is added and the second column is within the clip region,
+        // the attribute for column N+1 should be set to match the current attribute.
+        // See: OutputBufferImpl.cs line 194
+        using IDriver driver = CreateFakeDriver ();
+        driver.SetScreenSize (4, 2);
+
+        // Set a specific attribute for the wide glyph
+        Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
+        driver.CurrentAttribute = wideGlyphAttr;
+
+        // Add a wide glyph at position (0, 0)
+        driver.Move (0, 0);
+        driver.AddStr ("🍎");
+
+        // Verify the wide glyph is in column 0
+        Assert.Equal ("🍎", driver.Contents! [0, 0].Grapheme);
+        Assert.Equal (wideGlyphAttr, driver.Contents [0, 0].Attribute);
+
+        // Verify column 1 (the second column of the wide glyph) has the correct attribute set
+        // This is the fix: column N+1 should have CurrentAttribute set (line 194 in OutputBufferImpl.cs)
+        Assert.Equal (wideGlyphAttr, driver.Contents [0, 1].Attribute);
+
+        // Verify cursor moved to column 2
+        Assert.Equal (2, driver.Col);
+    }
+
+    [Fact]
+    public void AddStr_WideGlyph_Second_Column_Attribute_Not_Set_When_Outside_Clip ()
+    {
+        // This test verifies that when a wide glyph's second column is outside the clip,
+        // the attribute for column N+1 is NOT modified
+        using IDriver driver = CreateFakeDriver ();
+        driver.SetScreenSize (4, 2);
+
+        // Set initial attribute for the entire contents
+        Attribute initialAttr = new (Color.White, Color.Black);
+        driver.CurrentAttribute = initialAttr;
+        driver.Move (0, 0);
+        driver.AddStr ("    ");
+        driver.Move (0, 1);
+        driver.AddStr ("    ");
+
+        // Create a clip that excludes column 1
+        driver.Clip = new (new Rectangle (0, 0, 1, 2));
+
+        // Set a different attribute for the wide glyph
+        Attribute wideGlyphAttr = new (Color.BrightRed, Color.BrightYellow);
+        driver.CurrentAttribute = wideGlyphAttr;
+
+        // Try to add a wide glyph at position (0, 0)
+        // Column 0 is in clip, but column 1 is NOT
+        driver.Move (0, 0);
+        driver.AddStr ("🍎");
+
+        // Verify column 0 has the replacement character (can't fit wide glyph)
+        Assert.NotEqual ("🍎", driver.Contents! [0, 0].Grapheme);
+
+        // Verify column 1 still has the original attribute (NOT modified)
+        Assert.Equal (initialAttr, driver.Contents [0, 1].Attribute);
+    }
+
+    [Fact]
+    public void AddStr_WideGlyph_Second_Column_Attribute_Outputs_Correctly ()
+    {
+        // This test verifies the fix for issue #4258 by checking the actual driver output
+        // This mimics what happens when TransparentShadow redraws a wide glyph from ScreenContents
+        // WITHOUT line 194, column N+1's attribute doesn't get set, causing wrong colors in output
+        // See: OutputBufferImpl.cs line ~196 (Contents [Row, Col].Attribute = CurrentAttribute;)
+        using IDriver driver = CreateFakeDriver ();
+        driver.SetScreenSize (3, 1);
+        driver.Force16Colors = true;
+
+        // Step 1: Draw initial content - a wide glyph at column 1 with white-on-black
+        driver.CurrentAttribute = new Attribute (Color.White, Color.Black);
+        driver.Move (1, 0);
+        driver.AddStr ("🍎X");  // Wide glyph at columns 1-2, 'X' at column 3 doesn't exist (off-screen)
+
+        // At this point:
+        // - Column 0: space (default) with white-on-black
+        // - Column 1: 🍎 with white-on-black
+        // - Column 2: (part of 🍎) with white-on-black (from initial ClearContents)
+
+        // Step 2: Now redraw the SAME wide glyph at column 1 but with a DIFFERENT attribute (red-on-yellow)
+        // This simulates what transparent shadow does - it redraws what's underneath with a dimmed attribute
+        driver.CurrentAttribute = new Attribute (Color.BrightRed, Color.BrightYellow);
+        driver.Move (1, 0);
+        driver.AddStr ("🍎");
+
+        // Verify internal state
+        Assert.Equal ("🍎", driver.Contents! [0, 1].Grapheme);
+        Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 1].Attribute);
+
+        // THIS is the critical assertion - column 2's attribute MUST be red-on-yellow
+        // WITHOUT line 194: column 2 retains white-on-black
+        // WITH line 194: column 2 gets red-on-yellow
+        Assert.Equal (new Attribute (Color.BrightRed, Color.BrightYellow), driver.Contents [0, 2].Attribute);
+
+        driver.Refresh ();
+
+        // Expected output:
+        // Column 0: space with white-on-black
+        // Columns 1-2: 🍎 with red-on-yellow (both columns must have same attribute!)
+        //
+        // WITHOUT line 196, the output would be:
+        // \x1b[97m\x1b[40m  (white-on-black for column 0)
+        // \x1b[91m\x1b[103m🍎 (red-on-yellow starts at column 1)
+        // \x1b[97m\x1b[40m (WRONG! Attribute changes mid-glyph because column 2 still has white-on-black)
+        //
+        // WITH line 196, the output is:
+        // \x1b[97m\x1b[40m  (white-on-black for column 0)
+        // \x1b[91m\x1b[103m🍎 (red-on-yellow for both columns 1 and 2)
+        DriverAssert.AssertDriverOutputIs (
+            "\x1b[97m\x1b[40m \x1b[91m\x1b[103m🍎",
+            output,
+            driver);
+    }
 }

+ 193 - 0
Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs

@@ -0,0 +1,193 @@
+#nullable enable
+using System.Text;
+using UnitTests;
+using Xunit.Abstractions;
+
+namespace ViewBaseTests.Adornments;
+
+[Collection ("Global Test Setup")]
+public class BorderArrangementTests (ITestOutputHelper output)
+{
+    [Fact]
+    public void Arrangement_Handles_Wide_Glyphs_Correctly ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (6, 5);
+        app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        superview.Text = """
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         """;
+
+        View view = new ()
+        {
+            X = 2, Width = 4, Height = 4, BorderStyle = LineStyle.Single,
+            Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
+        };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        Assert.Equal ("Absolute(2)", view.X.ToString ());
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              🍎┌──┐
+                                              🍎│  │
+                                              🍎│  │
+                                              🍎└──┘
+                                              🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              🍎◊──┐
+                                              🍎│  │
+                                              🍎│  │
+                                              🍎└──↘
+                                              🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
+        Assert.Equal ("Absolute(1)", view.X.ToString ());
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              �◊──┐
+                                              �│  │
+                                              �│  │
+                                              �└──↘
+                                              🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
+        Assert.Equal ("Absolute(0)", view.X.ToString ());
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              ◊──┐🍎
+                                              │  │🍎
+                                              │  │🍎
+                                              └──↘🍎
+                                              🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+    }
+
+    [Fact]
+    public void Arrangement_With_SubView_In_Border_Handles_Wide_Glyphs_Correctly ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (8, 7);
+        app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        superview.Text = """
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         🍎🍎🍎🍎
+                         """;
+
+        View view = new ()
+        {
+            X = 2, Width = 6, Height = 6, Arrangement = ViewArrangement.Movable | ViewArrangement.Resizable, CanFocus = true
+        };
+        view.Border!.Thickness = new (1);
+        view.Border.Add (new View { Height = Dim.Auto (), Width = Dim.Auto (), Text = "Hi" });
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        Assert.Equal ("Absolute(2)", view.X.ToString ());
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              🍎Hi
+                                              🍎
+                                              🍎
+                                              🍎
+                                              🍎
+                                              🍎
+                                              🍎🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              🍎◊i
+                                              🍎
+                                              🍎
+                                              🍎
+                                              🍎
+                                              🍎     ↘
+                                              🍎🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
+        Assert.Equal ("Absolute(1)", view.X.ToString ());
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              �◊i
+                                              �
+                                              �
+                                              �
+                                              �
+                                              �     ↘
+                                              🍎🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.CursorLeft));
+        Assert.Equal ("Absolute(0)", view.X.ToString ());
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              ◊i    🍎
+                                                    🍎
+                                                    🍎
+                                                    🍎
+                                                    🍎
+                                                   ↘🍎
+                                              🍎🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+    }
+}

+ 22 - 0
Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs

@@ -133,4 +133,26 @@ MMM",
         Assert.True (view.Margin!.ViewportSettings.HasFlag (ViewportSettingsFlags.Transparent), "Margin should be transparent when ShadowStyle is Opaque..");
     }
 
+    [Fact]
+    public void Margin_Layouts_Correctly ()
+    {
+        View superview = new () { Width = 10, Height = 5 };
+        View view = new () { Width = 3, Height = 1, BorderStyle = LineStyle.Single };
+        view.Margin!.Thickness = new (1);
+        View view2 = new () { X = Pos.Right (view), Width = 3, Height = 1, BorderStyle = LineStyle.Single };
+        view2.Margin!.Thickness = new (1);
+        View view3 = new () { Y = Pos.Bottom (view), Width = 3, Height = 1, BorderStyle = LineStyle.Single };
+        view3.Margin!.Thickness = new (1);
+        superview.Add (view, view2, view3);
+
+        superview.LayoutSubViews ();
+
+        Assert.Equal (new (0, 0, 10, 5), superview.Frame);
+        Assert.Equal (new (0, 0, 3, 1), view.Frame);
+        Assert.Equal (Rectangle.Empty, view.Viewport);
+        Assert.Equal (new (3, 0, 3, 1), view2.Frame);
+        Assert.Equal (Rectangle.Empty, view2.Viewport);
+        Assert.Equal (new (0, 1, 3, 1), view3.Frame);
+        Assert.Equal (Rectangle.Empty, view3.Viewport);
+    }
 }

+ 0 - 157
Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs

@@ -1,157 +0,0 @@
-using UnitTests;
-using Xunit.Abstractions;
-
-namespace ViewBaseTests.Adornments;
-
-[Collection ("Global Test Setup")]
-
-public class ShadowStyleTests (ITestOutputHelper output)
-{
-    private readonly ITestOutputHelper _output = output;
-
-    [Fact]
-    public void Default_None ()
-    {
-        var view = new View ();
-        Assert.Equal (ShadowStyle.None, view.ShadowStyle);
-        Assert.Equal (ShadowStyle.None, view.Margin!.ShadowStyle);
-        view.Dispose ();
-    }
-
-    [Theory]
-    [InlineData (ShadowStyle.None)]
-    [InlineData (ShadowStyle.Opaque)]
-    [InlineData (ShadowStyle.Transparent)]
-    public void Set_View_Sets_Margin (ShadowStyle style)
-    {
-        var view = new View ();
-
-        view.ShadowStyle = style;
-        Assert.Equal (style, view.ShadowStyle);
-        Assert.Equal (style, view.Margin!.ShadowStyle);
-        view.Dispose ();
-    }
-
-
-    [Theory]
-    [InlineData (ShadowStyle.None, 0, 0, 0, 0)]
-    [InlineData (ShadowStyle.Opaque, 0, 0, 1, 1)]
-    [InlineData (ShadowStyle.Transparent, 0, 0, 1, 1)]
-    public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
-    {
-        var superView = new View
-        {
-            Height = 10, Width = 10
-        };
-
-        View view = new ()
-        {
-            Width = Dim.Auto (),
-            Height = Dim.Auto (),
-            Text = "0123",
-            HighlightStates = MouseState.Pressed,
-            ShadowStyle = style,
-            CanFocus = true
-        };
-
-        superView.Add (view);
-        superView.BeginInit ();
-        superView.EndInit ();
-
-        Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin!.Thickness);
-    }
-
-
-    [Theory]
-    [InlineData (ShadowStyle.None, 3)]
-    [InlineData (ShadowStyle.Opaque, 4)]
-    [InlineData (ShadowStyle.Transparent, 4)]
-    public void Style_Changes_Margin_Thickness (ShadowStyle style, int expected)
-    {
-        var view = new View ();
-        view.Margin!.Thickness = new (3);
-        view.ShadowStyle = style;
-        Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
-
-        view.ShadowStyle = ShadowStyle.None;
-        Assert.Equal (new (3), view.Margin.Thickness);
-        view.Dispose ();
-    }
-
-
-    [Fact]
-    public void TransparentShadow_Draws_Transparent_At_Driver_Output ()
-    {
-        // Arrange
-        IApplication app = Application.Create ();
-        app.Init ("fake");
-        app.Driver!.SetScreenSize (5, 3);
-
-        // Force 16-bit colors off to get predictable RGB output
-        app.Driver.Force16Colors = false;
-
-        var superView = new Runnable
-        {
-            Width = Dim.Fill (),
-            Height = Dim.Fill (),
-            Text = "ABC".Repeat (40)!
-        };
-        superView.SetScheme (new (new Attribute (Color.White, Color.Blue)));
-        superView.TextFormatter.WordWrap = true;
-
-        // Create an overlapped view with transparent shadow
-        var overlappedView = new View
-        {
-            Width = 4,
-            Height = 2,
-            Text = "123",
-            Arrangement = ViewArrangement.Overlapped,
-            ShadowStyle = ShadowStyle.Transparent
-        };
-        overlappedView.SetScheme (new (new Attribute (Color.Black, Color.Green)));
-
-        superView.Add (overlappedView);
-
-        // Act
-        SessionToken? token = app.Begin (superView);
-        app.LayoutAndDraw ();
-        app.Driver.Refresh ();
-
-        // Assert
-        _output.WriteLine ("Actual driver contents:");
-        _output.WriteLine (app.Driver.ToString ());
-        _output.WriteLine ("\nActual driver output:");
-        string? output = app.Driver.GetOutput ().GetLastOutput ();
-        _output.WriteLine (output);
-
-        DriverAssert.AssertDriverOutputIs ("""
-                                           \x1b[38;2;0;0;0m\x1b[48;2;0;128;0m123\x1b[38;2;0;0;0m\x1b[48;2;189;189;189mA\x1b[38;2;0;0;255m\x1b[48;2;255;255;255mBC\x1b[38;2;0;0;0m\x1b[48;2;189;189;189mABC\x1b[38;2;0;0;255m\x1b[48;2;255;255;255mABCABC
-                                           """, _output, app.Driver);
-
-        // The output should contain ANSI color codes for the transparent shadow
-        // which will have dimmed colors compared to the original
-        Assert.Contains ("\x1b[38;2;", output); // Should have RGB foreground color codes
-        Assert.Contains ("\x1b[48;2;", output); // Should have RGB background color codes
-
-        // Verify driver contents show the background text in shadow areas
-        int shadowX = overlappedView.Frame.X + overlappedView.Frame.Width;
-        int shadowY = overlappedView.Frame.Y + overlappedView.Frame.Height;
-
-        Cell shadowCell = app.Driver.Contents! [shadowY, shadowX];
-        _output.WriteLine ($"\nShadow cell at [{shadowY},{shadowX}]: Grapheme='{shadowCell.Grapheme}', Attr={shadowCell.Attribute}");
-
-        // The grapheme should be from background text
-        Assert.NotEqual (string.Empty, shadowCell.Grapheme);
-        Assert.Contains (shadowCell.Grapheme, "ABC"); // Should be one of the background characters
-
-        // Cleanup
-        if (token is { })
-        {
-            app.End (token);
-        }
-
-        superView.Dispose ();
-        app.Dispose ();
-    }
-
-}

+ 487 - 0
Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs

@@ -0,0 +1,487 @@
+using System.Text;
+using UnitTests;
+using Xunit.Abstractions;
+
+namespace ViewBaseTests.Adornments;
+
+[Collection ("Global Test Setup")]
+
+public class ShadowTests (ITestOutputHelper output)
+{
+    private readonly ITestOutputHelper _output = output;
+
+    [Fact]
+    public void Default_None ()
+    {
+        var view = new View ();
+        Assert.Equal (ShadowStyle.None, view.ShadowStyle);
+        Assert.Equal (ShadowStyle.None, view.Margin!.ShadowStyle);
+        view.Dispose ();
+    }
+
+    [Theory]
+    [InlineData (ShadowStyle.None)]
+    [InlineData (ShadowStyle.Opaque)]
+    [InlineData (ShadowStyle.Transparent)]
+    public void Set_View_Sets_Margin (ShadowStyle style)
+    {
+        var view = new View ();
+
+        view.ShadowStyle = style;
+        Assert.Equal (style, view.ShadowStyle);
+        Assert.Equal (style, view.Margin!.ShadowStyle);
+        view.Dispose ();
+    }
+
+
+    [Theory]
+    [InlineData (ShadowStyle.None, 0, 0, 0, 0)]
+    [InlineData (ShadowStyle.Opaque, 0, 0, 1, 1)]
+    [InlineData (ShadowStyle.Transparent, 0, 0, 1, 1)]
+    public void ShadowStyle_Margin_Thickness (ShadowStyle style, int expectedLeft, int expectedTop, int expectedRight, int expectedBottom)
+    {
+        var superView = new View
+        {
+            Height = 10, Width = 10
+        };
+
+        View view = new ()
+        {
+            Width = Dim.Auto (),
+            Height = Dim.Auto (),
+            Text = "0123",
+            HighlightStates = MouseState.Pressed,
+            ShadowStyle = style,
+            CanFocus = true
+        };
+
+        superView.Add (view);
+        superView.BeginInit ();
+        superView.EndInit ();
+
+        Assert.Equal (new (expectedLeft, expectedTop, expectedRight, expectedBottom), view.Margin!.Thickness);
+    }
+
+
+    [Theory]
+    [InlineData (ShadowStyle.None, 3)]
+    [InlineData (ShadowStyle.Opaque, 4)]
+    [InlineData (ShadowStyle.Transparent, 4)]
+    public void Style_Changes_Margin_Thickness (ShadowStyle style, int expected)
+    {
+        var view = new View ();
+        view.Margin!.Thickness = new (3);
+        view.ShadowStyle = style;
+        Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
+
+        view.ShadowStyle = ShadowStyle.None;
+        Assert.Equal (new (3), view.Margin.Thickness);
+        view.Dispose ();
+    }
+
+    [Theory]
+    [InlineData (ShadowStyle.Opaque)]
+    [InlineData (ShadowStyle.Transparent)]
+    public void ShadowWidth_ShadowHeight_Defaults_To_One (ShadowStyle style)
+    {
+        View view = new () { ShadowStyle = style };
+
+        Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
+    }
+
+    [Theory]
+    [InlineData (ShadowStyle.None, 0)]
+    [InlineData (ShadowStyle.Opaque, 1)]
+    [InlineData (ShadowStyle.Transparent, 1)]
+    public void Margin_ShadowWidth_ShadowHeight_Cannot_Be_Set_Less_Than_One (ShadowStyle style, int expectedLength)
+    {
+        View view = new () { ShadowStyle = style };
+        view.Margin!.ShadowSize = new (-1, -1);
+        Assert.Equal (expectedLength, view.Margin!.ShadowSize.Width);
+        Assert.Equal (expectedLength, view.Margin!.ShadowSize.Height);
+    }
+
+    [Fact]
+    public void Changing_ShadowStyle_Correctly_Set_ShadowWidth_ShadowHeight_Thickness ()
+    {
+        View view = new () { ShadowStyle = ShadowStyle.Transparent };
+        view.Margin!.ShadowSize = new (2, 2);
+
+        Assert.Equal (new (2, 2), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 2, 2), view.Margin.Thickness);
+
+        view.ShadowStyle = ShadowStyle.None;
+        Assert.Equal (new (2, 2), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 0, 0), view.Margin.Thickness);
+
+        view.ShadowStyle = ShadowStyle.Opaque;
+        Assert.Equal (new (1, 1), view.Margin!.ShadowSize);
+        Assert.Equal (new (0, 0, 1, 1), view.Margin.Thickness);
+    }
+
+    [Fact]
+    public void ShadowStyle_Transparent_Handles_Wide_Glyphs_Correctly ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (6, 5);
+        app.Driver?.GetOutputBuffer ().SetWideGlyphReplacement (Rune.ReplacementChar);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        superview.Text = """
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         """;
+
+        View view = new () { Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single, ShadowStyle = ShadowStyle.Transparent };
+        view.Margin!.ShadowSize = view.Margin!.ShadowSize with { Width = 2 };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              ┌──┐🍎
+                                              │  │🍎
+                                              │  │🍎
+                                              └──┘🍎
+                                              � 🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        view.Margin!.ShadowSize = new (1, 2);
+
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              ┌──┐🍎
+                                              │  │�
+                                              └──┘�
+                                              � 🍎🍎
+                                              � 🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+    }
+
+    [Fact]
+    public void ShadowStyle_Opaque_Change_Thickness_On_Mouse_Pressed_Released ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (10, 4);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+        View view = new () { Width = 7, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hi |", HighlightStates = MouseState.Pressed };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              | Hi |▖
+                                              ▝▀▀▀▀▀▘
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.Button1Pressed });
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              | Hi |
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        app.Mouse.RaiseMouseEvent (new () { ScreenPosition = new (2, 0), Flags = MouseFlags.Button1Released });
+        app.LayoutAndDraw ();
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              | Hi |▖
+                                              ▝▀▀▀▀▀▘
+                                              """,
+                                              output,
+                                              app.Driver);
+    }
+
+    [Fact]
+    public void ShadowStyle_Transparent_Never_Throws_Navigating_Outside_Bounds ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (6, 5);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill () };
+
+        superview.Text = """
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         🍎🍎🍎
+                         """;
+
+        View view = new ()
+        {
+            Width = Dim.Fill (), Height = Dim.Fill (), BorderStyle = LineStyle.Single, ShadowStyle = ShadowStyle.Transparent,
+            Arrangement = ViewArrangement.Movable, CanFocus = true
+        };
+        view.Margin!.ShadowSize = view.Margin!.ShadowSize with { Width = 2 };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        Assert.Equal (new (0, 0), view.Frame.Location);
+
+        Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.F5.WithCtrl));
+
+        int i = 0;
+        DecrementValue (-10, Key.CursorLeft);
+        Assert.Equal (-10, i);
+
+        IncrementValue (0, Key.CursorRight);
+        Assert.Equal (0, i);
+
+        DecrementValue (-10, Key.CursorUp);
+        Assert.Equal (-10, i);
+
+        IncrementValue (20, Key.CursorDown);
+        Assert.Equal (20, i);
+
+        DecrementValue (0, Key.CursorUp);
+        Assert.Equal (0, i);
+
+        IncrementValue (20, Key.CursorRight);
+        Assert.Equal (20, i);
+
+        return;
+
+        void DecrementValue (int count, Key key)
+        {
+            for (; i > count; i--)
+            {
+                Assert.True (app.Keyboard.RaiseKeyDownEvent (key));
+                app.LayoutAndDraw ();
+
+                CheckAssertion (new (i - 1, 0), new (0, i - 1), key);
+            }
+        }
+
+        void IncrementValue (int count, Key key)
+        {
+            for (; i < count; i++)
+            {
+                Assert.True (app.Keyboard.RaiseKeyDownEvent (key));
+                app.LayoutAndDraw ();
+
+                CheckAssertion (new (i + 1, 0), new (0, i + 1), key);
+            }
+        }
+
+        bool? IsColumn (Key key)
+        {
+            if (key == Key.CursorLeft || key == Key.CursorRight)
+            {
+                return true;
+            }
+
+            if (key == Key.CursorUp || key == Key.CursorDown)
+            {
+                return false;
+            }
+
+            return null;
+        }
+
+        void CheckAssertion (Point colLocation, Point rowLocation, Key key)
+        {
+            bool? isCol = IsColumn (key);
+
+            switch (isCol)
+            {
+                case true:
+                    Assert.Equal (colLocation, view.Frame.Location);
+
+                    break;
+                case false:
+                    Assert.Equal (rowLocation, view.Frame.Location);
+
+                    break;
+                default:
+                    throw new InvalidOperationException ();
+            }
+        }
+    }
+
+    [Theory]
+    [InlineData (ShadowStyle.None, 3)]
+    [InlineData (ShadowStyle.Opaque, 4)]
+    [InlineData (ShadowStyle.Transparent, 4)]
+    public void Margin_Thickness_Changes_Adjust_Correctly (ShadowStyle style, int expected)
+    {
+        var view = new View ();
+        view.Margin!.Thickness = new (3);
+        view.ShadowStyle = style;
+        Assert.Equal (new (3, 3, expected, expected), view.Margin.Thickness);
+
+        view.Margin.Thickness = new (3, 3, expected + 1, expected + 1);
+        Assert.Equal (new (3, 3, expected + 1, expected + 1), view.Margin.Thickness);
+        view.ShadowStyle = ShadowStyle.None;
+        Assert.Equal (new (3, 3, 4, 4), view.Margin.Thickness);
+        view.Dispose ();
+    }
+
+    [Fact]
+    public void Runnable_View_Overlap_Other_Runnables ()
+    {
+        IApplication app = Application.Create ();
+        app.Init ("fake");
+
+        app.Driver?.SetScreenSize (10, 5);
+
+        Runnable superview = new () { Width = Dim.Fill (), Height = Dim.Fill (), Text = "🍎".Repeat (25)! };
+        View view = new () { Width = 7, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hi |" };
+        superview.Add (view);
+
+        app.Begin (superview);
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              | Hi |▖ 🍎
+                                              ▝▀▀▀▀▀▘ 🍎
+                                              🍎🍎🍎🍎🍎
+                                              🍎🍎🍎🍎🍎
+                                              🍎🍎🍎🍎🍎
+                                              """,
+                                              output,
+                                              app.Driver);
+
+        Runnable modalSuperview = new () { Y = 1, Width = Dim.Fill (), Height = 4, BorderStyle = LineStyle.Single };
+        View view1 = new () { Width = 8, Height = 2, ShadowStyle = ShadowStyle.Opaque, Text = "| Hey |" };
+        modalSuperview.Add (view1);
+
+        app.Begin (modalSuperview);
+
+        Assert.True (modalSuperview.IsModal);
+
+        DriverAssert.AssertDriverContentsAre (
+                                              """
+                                              | Hi |▖ 🍎
+                                              ┌────────┐
+                                              │| Hey |▖│
+                                              │▝▀▀▀▀▀▀▘│
+                                              └────────┘
+                                              """,
+                                              output,
+                                              app.Driver);
+
+
+        app.Dispose ();
+    }
+
+    [Fact]
+    public void TransparentShadow_Draws_Transparent_At_Driver_Output ()
+    {
+        // Arrange
+        using IApplication app = Application.Create ();
+        app.Init ("fake");
+        app.Driver!.SetScreenSize (2, 1);
+        app.Driver.Force16Colors = true;
+
+        using Runnable superView = new ();
+        superView.Width = Dim.Fill ();
+        superView.Height = Dim.Fill ();
+        superView.Text = "AB";
+        superView.TextFormatter.WordWrap = true;
+        superView.SetScheme (new (new Attribute (Color.Black, Color.White)));
+
+        // Create view with transparent shadow
+        View viewWithShadow = new ()
+        {
+            Width = Dim.Auto (),
+            Height = Dim.Auto (),
+            Text = "*",
+            ShadowStyle = ShadowStyle.Transparent
+        };
+        // Make it so the margin is only on the right for simplicity
+        viewWithShadow.Margin!.Thickness = new (0, 0, 1, 0);
+        viewWithShadow.SetScheme (new (new Attribute (Color.Black, Color.White)));
+
+        superView.Add (viewWithShadow);
+
+        // Act
+        app.Begin (superView);
+        app.LayoutAndDraw ();
+        app.Driver.Refresh ();
+
+        // Assert
+        _output.WriteLine ("Actual driver contents:");
+        _output.WriteLine (app.Driver.ToString ());
+        _output.WriteLine ("\nActual driver output:");
+        string? output = app.Driver.GetOutput ().GetLastOutput ();
+        _output.WriteLine (output);
+
+        DriverAssert.AssertDriverOutputIs ("""
+                                           \x1b[30m\x1b[107m*\x1b[90m\x1b[100mB
+                                           """, _output, app.Driver);
+    }
+
+    [Fact]
+    public void TransparentShadow_OverWide_Draws_Transparent_At_Driver_Output ()
+    {
+        // Arrange
+        using IApplication app = Application.Create ();
+        app.Init ("fake");
+        app.Driver!.SetScreenSize (2, 3);
+        app.Driver.Force16Colors = true;
+
+        using Runnable superView = new ();
+        superView.Width = Dim.Fill ();
+        superView.Height = Dim.Fill ();
+        superView.Text = "🍎🍎🍎🍎";
+        superView.TextFormatter.WordWrap = true;
+        superView.SetScheme (new (new Attribute (Color.Black, Color.White)));
+
+        // Create view with transparent shadow
+        View viewWithShadow = new ()
+        {
+            Width = Dim.Auto (),
+            Height = Dim.Auto (),
+            Text = "*",
+            ShadowStyle = ShadowStyle.Transparent
+        };
+        // Make it so the margin is only on the bottom for simplicity
+        viewWithShadow.Margin!.Thickness = new (0, 0, 0, 1);
+        viewWithShadow.SetScheme (new (new Attribute (Color.Black, Color.White)));
+
+        superView.Add (viewWithShadow);
+
+        // Act
+        app.Begin (superView);
+        app.LayoutAndDraw ();
+        app.Driver.Refresh ();
+
+        // Assert
+        _output.WriteLine ("Actual driver contents:");
+        _output.WriteLine (app.Driver.ToString ());
+        _output.WriteLine ("\nActual driver output:");
+        string? output = app.Driver.GetOutput ().GetLastOutput ();
+        _output.WriteLine (output);
+
+        DriverAssert.AssertDriverOutputIs ("""
+                                           \x1b[30m\x1b[107m*\x1b[90m\x1b[103m \x1b[97m\x1b[40m \x1b[90m\x1b[100m \x1b[97m\x1b[40m🍎
+                                           """, _output, app.Driver);
+    }
+}