diff --git a/Examples/UICatalog/Scenarios/WideGlyphs.cs b/Examples/UICatalog/Scenarios/WideGlyphs.cs
index 771355f1a..7d95dec34 100644
--- a/Examples/UICatalog/Scenarios/WideGlyphs.cs
+++ b/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;
diff --git a/Terminal.Gui/Drivers/OutputBufferImpl.cs b/Terminal.Gui/Drivers/OutputBufferImpl.cs
index 9f0b074fe..00592f0e9 100644
--- a/Terminal.Gui/Drivers/OutputBufferImpl.cs
+++ b/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++;
}
}
diff --git a/Terminal.Gui/ViewBase/Adornment/Margin.cs b/Terminal.Gui/ViewBase/Adornment/Margin.cs
index 54e9c2a67..998478b15 100644
--- a/Terminal.Gui/ViewBase/Adornment/Margin.cs
+++ b/Terminal.Gui/ViewBase/Adornment/Margin.cs
@@ -1,8 +1,3 @@
-
-
-using System.Diagnostics;
-using System.Runtime.InteropServices;
-
namespace Terminal.Gui.ViewBase;
/// The Margin for a . Accessed via
@@ -21,8 +16,6 @@ namespace Terminal.Gui.ViewBase;
///
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
}
///
- /// INTERNAL API - Draws the margins for the specified views. This is called by the on each
+ /// INTERNAL API - Draws the transparent margins for the specified views. This is called from on each
/// iteration of the main loop after all Views have been drawn.
///
+ ///
+ /// Non-transparent margins are drawn as-normal in .
+ ///
///
///
- internal static bool DrawMargins (IEnumerable views)
+ internal static bool DrawTransparentMargins (IEnumerable views)
{
Stack 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;
///
/// 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;
+
+ ///
+ /// Gets or sets the size of the shadow effect.
+ ///
+ 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 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 { })
diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs
index 90f84219c..78eb31355 100644
--- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs
+++ b/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
diff --git a/Terminal.Gui/ViewBase/View.Drawing.cs b/Terminal.Gui/ViewBase/View.Drawing.cs
index a5e17da18..38d398c3f 100644
--- a/Terminal.Gui/ViewBase/View.Drawing.cs
+++ b/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
///
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
diff --git a/Terminal.sln.DotSettings b/Terminal.sln.DotSettings
index a2ec2772f..dfbd047be 100644
--- a/Terminal.sln.DotSettings
+++ b/Terminal.sln.DotSettings
@@ -382,6 +382,7 @@
False
True
True
+ BMP
CWP
LL
LR
@@ -431,6 +432,7 @@
True
True
True
+ True
True
True
True
diff --git a/Tests/UnitTests/View/Draw/ClipTests.cs b/Tests/UnitTests/View/Draw/ClipTests.cs
index bbeff3518..0210aa742 100644
--- a/Tests/UnitTests/View/Draw/ClipTests.cs
+++ b/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);
}
diff --git a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs b/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs
index 522f917c6..c1c4f93e6 100644
--- a/Tests/UnitTestsParallelizable/Drivers/AddRuneTests.cs
+++ b/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);
+ }
}
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs
index e69de29bb..71ad1202c 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/BorderArrangementTests.cs
+++ b/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);
+ }
+}
\ No newline at end of file
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs
index 482b2519e..eeaf1165a 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/MarginTests.cs
+++ b/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);
+ }
}
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs
deleted file mode 100644
index 7af4b777c..000000000
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowStyletests.cs
+++ /dev/null
@@ -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 ();
- }
-
-}
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
new file mode 100644
index 000000000..281fa1b2f
--- /dev/null
+++ b/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);
+ }
+}