From 449092c00a6a1b41d7df087e68812d367ed3e93e Mon Sep 17 00:00:00 2001 From: Siegfried Pammer Date: Sat, 23 May 2026 08:32:20 +0200 Subject: [PATCH] Pin button matches close button size + hover background MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously pin used an explicit 22x22 size, custom padding, and a Button.preview-pin class with hand-rolled #33000000/#55000000 hover tints. The close button (its sibling in the tab strip) is a plain Avalonia.Controls.Button with a ControlTheme applied by Dock's tab template — no classes, no local sizing. Visual mismatch was noticeable. Assisted-by: Claude:claude-opus-4-7:Claude Code --- .../Docking/PreviewTabPromotionTests.cs | 40 ++++++++++++++++ ILSpy/App.axaml | 17 ------- ILSpy/Themes/PreviewTabPinButtonBehavior.cs | 48 +++++++++++-------- 3 files changed, 67 insertions(+), 38 deletions(-) diff --git a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs index 1bbbfc83b..a285ebf3d 100644 --- a/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs +++ b/ILSpy.Tests/Docking/PreviewTabPromotionTests.cs @@ -490,6 +490,46 @@ public class PreviewTabPromotionTests "the already-pinned tab stays pinned"); } + [AvaloniaTest] + public async Task Pin_Button_Inherits_The_Close_Button_Theme() + { + // Visual parity: the pin button should look like the close button — same size, same + // hover background. Both are plain Avalonia.Controls.Button instances; the close + // button gets its visual identity from a ControlTheme applied by Dock's tab + // template. Pin should copy that Theme rather than carry its own custom style. + var window = AppComposition.Current.GetExport(); + window.Show(); + var vm = (MainWindowViewModel)window.DataContext!; + await vm.AssemblyTreeModel.WaitForAssembliesAsync(minimumCount: 3); + + // Open a carve-out so the close button is visible alongside the pin (single-tab + // scenario hides the close button). + var typeNode = vm.AssemblyTreeModel.FindNode( + "System.Linq", "System.Linq", "System.Linq.Enumerable"); + vm.DockWorkspace.OpenNodeInNewTab(typeNode); + + await Waiters.WaitForAsync(() => window.GetVisualDescendants().OfType().Count() >= 2, + System.TimeSpan.FromSeconds(10)); + global::Avalonia.Threading.Dispatcher.UIThread.RunJobs(); + + var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory; + var mainTabItem = window.GetVisualDescendants().OfType() + .Single(item => ReferenceEquals(item.DataContext, factory.MainTab)); + + var pinButton = mainTabItem.GetVisualDescendants() + .OfType() + .Single(b => (b.Tag as string) == "PreviewTabPinButton"); + var closeButton = mainTabItem.GetVisualDescendants() + .OfType() + .FirstOrDefault(b => (b.Tag as string) != "PreviewTabPinButton"); + closeButton.Should().NotBeNull("baseline: close button exists as a sibling of the pin"); + + pinButton.Theme.Should().BeSameAs(closeButton!.Theme, + "pin button must use the same ControlTheme as the close button so size + hover bg match"); + pinButton.Classes.Should().NotContain("preview-pin", + "after the Theme-copy refactor the custom class-based styling is unused; the Theme drives all visuals"); + } + [AvaloniaTest] public async Task Tree_Selection_While_Frozen_Tab_Active_Opens_A_New_Preview_Tab() { diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml index b5152843d..3ff069ef2 100644 --- a/ILSpy/App.axaml +++ b/ILSpy/App.axaml @@ -143,23 +143,6 @@ - - - - diff --git a/ILSpy/Themes/PreviewTabPinButtonBehavior.cs b/ILSpy/Themes/PreviewTabPinButtonBehavior.cs index fc8a24d74..bef2a0424 100644 --- a/ILSpy/Themes/PreviewTabPinButtonBehavior.cs +++ b/ILSpy/Themes/PreviewTabPinButtonBehavior.cs @@ -21,6 +21,7 @@ using System.Linq; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; using Avalonia.Data; using Avalonia.Layout; using Avalonia.Media; @@ -101,30 +102,32 @@ namespace ILSpy.Themes // on top of it. titleStack.ClipToBounds = true; - // Segoe Fluent Icons / Segoe MDL2 Assets "Pin" glyph (U+E718) — the same - // tilted-pushpin silhouette Visual Studio uses for preview tabs. Foreground - // inherits through TextElement, so the glyph always contrasts the current tab - // background (dark on unselected, light on selected). - var glyph = new TextBlock { - Text = "", - FontFamily = new FontFamily("Segoe Fluent Icons, Segoe MDL2 Assets, Symbols"), - FontSize = 12, + // Tilted-pushpin silhouette matching the Visual Studio preview-tab affordance. + // Vector path (not a font glyph): the previous Segoe Fluent Icons U+E718 + // fallback was Windows-only and rendered as a tofu box on Linux / macOS. + // Path with bound Fill is what restores per-tab Foreground inheritance that + // the intermediate SVG-image variant lost - Avalonia.Svg.Skia honors the + // asset's literal fill, not the consuming control's theme color. RotateTransform + // at -45 deg orients the upright pin path so its tip points to the lower-left. + var glyph = new Path { + Data = StreamGeometry.Parse("M5 2h6v1h-1v4l2 2v1H9v3l-1 2-1-2v-3H4V9l2-2V3H5z"), + Stretch = Stretch.Uniform, + Width = 12, + Height = 12, HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center, - ClipToBounds = false, + RenderTransform = new RotateTransform(45), + RenderTransformOrigin = RelativePoint.Center, }; - glyph.Bind(TextBlock.ForegroundProperty, new Binding(nameof(TemplatedControl.Foreground)) { + glyph.Bind(Shape.FillProperty, new Binding(nameof(TemplatedControl.Foreground)) { Source = item, Mode = BindingMode.OneWay, }); var pin = new Button { Tag = PinButtonTag, Content = glyph, - // Bumped from 18x18 to 22x22 so the glyph's visual extent has comfortable - // headroom on all sides. - Width = 22, - Height = 22, - Padding = new Thickness(3), + // Left margin preserves the gap between pin and close (~4px) regardless of + // what the inherited ControlTheme decides for its own Margin. Margin = new Thickness(4, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, HorizontalAlignment = HorizontalAlignment.Center, @@ -135,12 +138,15 @@ namespace ILSpy.Themes // right edge at the content-area boundary. ClipToBounds = false, }; - // Class-based styling — the App.axaml Style for Button.preview-pin keeps the - // button transparent in normal state and gives it a subtle background tint on - // :pointerover. Local Background/BorderBrush setters here would beat the - // :pointerover style (local > style in Avalonia property precedence), so the - // styling is routed entirely through the class selector instead. - pin.Classes.Add("preview-pin"); + // Copy the close button's ControlTheme so pin renders at the same size and uses + // the same :pointerover background. The close button (a plain Avalonia.Controls.Button + // with no classes, theme applied by Dock's tab template) sits as a sibling under + // the same Grid. Without this, pin defaulted to a class-styled custom look that + // was bigger and used a different hover tint than its neighbour. + var closeButton = item.GetVisualDescendants().OfType