diff --git a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
index cd9b1fe775..58c5b108ab 100644
--- a/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
+++ b/Terminal.Gui/ViewBase/Adornment/ArrangerButton.cs
@@ -74,7 +74,6 @@ public ArrangerButton ()
Height = 1;
NoDecorations = true;
NoPadding = true;
- base.ShadowStyle = null;
base.Visible = false;
AddCommand (Command.Up, DefaultAcceptHandler);
@@ -85,6 +84,13 @@ public ArrangerButton ()
_orientationHelper = new OrientationHelper (this);
}
+ ///
+ ///
+ /// Sets to so that no shadow infrastructure is
+ /// allocated by default for arranger buttons.
+ ///
+ protected override void OnInitializingShadowStyle (ValueChangingEventArgs args) => args.NewValue = null;
+
private ArrangeButtons _buttonType = (ArrangeButtons)(-1);
///
diff --git a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs
index 191fc2081f..c59b4ff595 100644
--- a/Terminal.Gui/ViewBase/Adornment/ShadowView.cs
+++ b/Terminal.Gui/ViewBase/Adornment/ShadowView.cs
@@ -184,7 +184,7 @@ private Attribute GetAttributeUnderLocation (Point location)
Attribute attr = attribute.Value;
var newAttribute = new Attribute (ShadowStyle == ShadowStyles.Opaque ? Color.Black : attr.Foreground.GetDimmerColor (),
- ShadowStyle == ShadowStyles.Opaque ? attr.Background : attr.Background.GetDimmerColor (0.05),
+ ShadowStyle == ShadowStyles.Opaque ? attr.Background : attr.Background.GetDimmerColor (0.9),
attr.Style);
// If the BG is DarkGray, GetDimmerColor gave up. Instead of using the attribute in the Driver under the shadow,
@@ -198,7 +198,7 @@ private Attribute GetAttributeUnderLocation (Point location)
attr = underView?.GetAttributeForRole (VisualRole.Normal) ?? Attribute.Default;
newAttribute = new Attribute (ShadowStyle == ShadowStyles.Opaque ? Color.Black : attr.Background.GetDimmerColor (),
- ShadowStyle == ShadowStyles.Opaque ? attr.Background : attr.Foreground.GetDimmerColor (0.25),
+ ShadowStyle == ShadowStyles.Opaque ? attr.Background : attr.Foreground.GetDimmerColor (0.9),
attr.Style);
return newAttribute;
diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs
index cbe872a2dc..3e4d1b61bb 100644
--- a/Terminal.Gui/Views/Button.cs
+++ b/Terminal.Gui/Views/Button.cs
@@ -1,8 +1,9 @@
namespace Terminal.Gui.Views;
///
-/// Raises the and events when the user presses ,
-/// Enter, or Space or clicks with the mouse.
+/// Raises the and events when the user presses
+/// ,
+/// Enter, or Space or clicks with the mouse.
///
///
/// Use to change the hot key specifier from the default of ('_').
@@ -84,10 +85,51 @@ public Button ()
TitleChanged += Button_TitleChanged;
- base.ShadowStyle = DefaultShadow;
+ // Determine and apply the initial shadow via the CWP InitializingShadowStyle event, so that
+ // subclasses and external subscribers can change or suppress the default allocation
+ // before any shadow infrastructure is created.
+ RaiseInitializingShadowStyle ();
MouseHighlightStates = DefaultMouseHighlightStates;
}
+ ///
+ /// Called before the Button's initial is applied during construction.
+ /// Override to change or suppress the default shadow — set
+ /// to the desired style, or set to
+ /// to skip applying any shadow.
+ ///
+ ///
+ /// Event args whose is pre-set to
+ /// . Subclasses that never display a shadow
+ /// (e.g. or internal buttons used by arrangement UI)
+ /// should set args.NewValue = null to avoid the create-then-destroy allocation pattern.
+ ///
+ protected virtual void OnInitializingShadowStyle (ValueChangingEventArgs args) { }
+
+ ///
+ /// Fired before the Button's initial is applied during construction.
+ /// Subscribers can modify or set
+ /// to to suppress the shadow.
+ ///
+ public event EventHandler>? InitializingShadowStyle;
+
+ private void RaiseInitializingShadowStyle ()
+ {
+ ValueChangingEventArgs args = new (null, DefaultShadow);
+
+ // 1. Virtual method — subclasses override to change/suppress the default shadow.
+ OnInitializingShadowStyle (args);
+
+ // 2. Event — external subscribers get a chance to customize.
+ InitializingShadowStyle?.Invoke (this, args);
+
+ // 3. Apply the (potentially modified) shadow style unless already handled.
+ if (!args.Handled)
+ {
+ base.ShadowStyle = args.NewValue;
+ }
+ }
+
///
protected override void OnMouseHoldRepeatChanged (ValueChangedEventArgs args) => SetMouseBindings (args.NewValue);
@@ -118,11 +160,8 @@ private void SetMouseBindings (MouseFlags? mouseHoldRepeat)
}
}
- ///
- protected override void OnHotKeyCommand (ICommandContext? commandContext)
- {
- InvokeCommand (Command.Accept);
- }
+ ///
+ protected override void OnHotKeyCommand (ICommandContext? commandContext) => InvokeCommand (Command.Accept);
private void Button_TitleChanged (object? sender, EventArgs e)
{
@@ -136,7 +175,7 @@ private void Button_TitleChanged (object? sender, EventArgs e)
///
public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; }
- ///
+ ///
public bool IsDefault
{
get;
diff --git a/Terminal.Gui/Views/ScrollBar/ScrollButton.cs b/Terminal.Gui/Views/ScrollBar/ScrollButton.cs
index 283af27cec..737fbd3358 100644
--- a/Terminal.Gui/Views/ScrollBar/ScrollButton.cs
+++ b/Terminal.Gui/Views/ScrollBar/ScrollButton.cs
@@ -60,7 +60,6 @@ public ScrollButton ()
CanFocus = false;
NoDecorations = true;
NoPadding = true;
- base.ShadowStyle = null;
MouseHoldRepeat = MouseFlags.LeftButtonReleased;
// ReSharper disable once UseObjectOrCollectionInitializer
@@ -68,6 +67,10 @@ public ScrollButton ()
SetGlyph ();
}
+ ///
+ /// Sets to so that no shadow infrastructure is allocated by default for scroll buttons.
+ protected override void OnInitializingShadowStyle (ValueChangingEventArgs args) => args.NewValue = null;
+
///
/// Gets or sets the direction this scrolls.
///
diff --git a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
index 55cb627370..6d4d55f6d4 100644
--- a/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
+++ b/Tests/UnitTestsParallelizable/ViewBase/Adornment/ShadowTests.cs
@@ -572,7 +572,7 @@ public void TransparentShadow_OverWide_Draws_Transparent_At_Driver_Output ()
output.WriteLine (output1);
DriverAssert.AssertDriverOutputIs ("""
- \x1b[30m\x1b[107m*\x1b[90m\x1b[107m \x1b[97m\x1b[40m \x1b[93m\x1b[100m \x1b[97m\x1b[40m🍎
+ \x1b[30m\x1b[107m*\x1b[90m\x1b[40m \x1b[97m\x1b[40m \x1b[93m\x1b[100m \x1b[97m\x1b[40m🍎
""",
output,
app.Driver);
diff --git a/Tests/UnitTestsParallelizable/Views/ButtonTests.cs b/Tests/UnitTestsParallelizable/Views/ButtonTests.cs
index 3b5f9f2445..a4cdbc7132 100644
--- a/Tests/UnitTestsParallelizable/Views/ButtonTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/ButtonTests.cs
@@ -1005,4 +1005,79 @@ public void Mouse_Click_Does_Not_Raise_Activating ()
button.Dispose ();
}
+
+ // Copilot
+ ///
+ /// Verifies that a newly-constructed Button has shadow infrastructure allocated by default,
+ /// because fires with
+ /// and no handler suppresses it.
+ ///
+ [Fact]
+ public void ShadowStyle_Applied_In_Constructor_By_Default ()
+ {
+ Button button = new ();
+
+ // Default shadow is applied in the constructor via the InitializingShadowStyle CWP event.
+ Assert.Equal (Button.DefaultShadow, button.ShadowStyle);
+ Assert.NotNull (button.Margin.View); // MarginView was created.
+
+ button.Dispose ();
+ }
+
+ // Copilot
+ ///
+ /// Verifies that setting to after construction
+ /// clears the shadow views while keeping the existing Margin.View allocated.
+ ///
+ [Fact]
+ public void ShadowStyle_Null_After_Construction_Clears_ShadowViews_But_Keeps_MarginView ()
+ {
+ Button button = new ();
+ View marginView = button.Margin.View;
+
+ Assert.NotNull (marginView);
+ Assert.NotEmpty (marginView.SubViews);
+
+ button.ShadowStyle = null;
+
+ Assert.Null (button.ShadowStyle);
+ Assert.Same (marginView, button.Margin.View);
+ Assert.Empty (button.Margin.View.SubViews);
+
+ button.Dispose ();
+ }
+
+ ///
+ /// Verifies that does not create shadow infrastructure during construction
+ /// because its override sets
+ /// args.NewValue = null, eliminating the create-then-destroy allocation pattern.
+ ///
+ [Fact]
+ public void ScrollButton_OnInitializingShadowStyle_Suppresses_Shadow ()
+ {
+ ScrollButton btn = new ();
+
+ // OnInitializingShadowStyle sets args.NewValue = null
+ // → base.ShadowStyle = null is a no-op → no shadow infrastructure created.
+ Assert.Null (btn.ShadowStyle);
+ Assert.Null (btn.Margin.View);
+
+ btn.Dispose ();
+ }
+
+ // Copilot
+ ///
+ /// Verifies that explicitly setting to a non-default value
+ /// after construction correctly replaces the initial default.
+ ///
+ [Fact]
+ public void ShadowStyle_ExplicitValue_After_Construction_IsApplied ()
+ {
+ Button button = new ();
+ button.ShadowStyle = ShadowStyles.Transparent;
+
+ Assert.Equal (ShadowStyles.Transparent, button.ShadowStyle);
+
+ button.Dispose ();
+ }
}