Browse Source

Pin button matches close button size + hover background

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
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
449092c00a
  1. 40
      ILSpy.Tests/Docking/PreviewTabPromotionTests.cs
  2. 17
      ILSpy/App.axaml
  3. 48
      ILSpy/Themes/PreviewTabPinButtonBehavior.cs

40
ILSpy.Tests/Docking/PreviewTabPromotionTests.cs

@ -490,6 +490,46 @@ public class PreviewTabPromotionTests @@ -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<MainWindow>();
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<TypeTreeNode>(
"System.Linq", "System.Linq", "System.Linq.Enumerable");
vm.DockWorkspace.OpenNodeInNewTab(typeNode);
await Waiters.WaitForAsync(() => window.GetVisualDescendants().OfType<DocumentTabStripItem>().Count() >= 2,
System.TimeSpan.FromSeconds(10));
global::Avalonia.Threading.Dispatcher.UIThread.RunJobs();
var factory = (ILSpyDockFactory)vm.DockWorkspace.Factory;
var mainTabItem = window.GetVisualDescendants().OfType<DocumentTabStripItem>()
.Single(item => ReferenceEquals(item.DataContext, factory.MainTab));
var pinButton = mainTabItem.GetVisualDescendants()
.OfType<global::Avalonia.Controls.Button>()
.Single(b => (b.Tag as string) == "PreviewTabPinButton");
var closeButton = mainTabItem.GetVisualDescendants()
.OfType<global::Avalonia.Controls.Button>()
.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()
{

17
ILSpy/App.axaml

@ -143,23 +143,6 @@ @@ -143,23 +143,6 @@
<Setter Property="themes:PreviewTabPinButtonBehavior.Enable" Value="True" />
</Style>
<!-- Styling for the injected pin button. Routed through a class selector (not
local setters on the Button) so the :pointerover style still applies — local
property values beat Style setters in Avalonia precedence. -->
<Style Selector="Button.preview-pin">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.preview-pin:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="#33000000" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="Button.preview-pin:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="#55000000" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<!-- Override the presenter template so we can layer classic Windows-Explorer tree
lines behind the indent / expander / content. -->

48
ILSpy/Themes/PreviewTabPinButtonBehavior.cs

@ -21,6 +21,7 @@ using System.Linq; @@ -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 @@ -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 @@ -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<Button>()
.FirstOrDefault(b => (b.Tag as string) != PinButtonTag);
if (closeButton?.Theme is { } closeTheme)
pin.Theme = closeTheme;
ToolTip.SetTip(pin, "Pin tab");
// IsVisible follows IsPreview — the button hides once the tab is pinned.

Loading…
Cancel
Save