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 (); + } }