From 4e3a82fa30e88c649b19297816201ffd3f17e70d Mon Sep 17 00:00:00 2001 From: Dave Tseng Date: Thu, 6 Aug 2026 14:52:27 +0800 Subject: [PATCH] feat: scale panel fonts with VS's Environment Font size Replace hardcoded FontSize values in the panel and compose dialog with bindings relative to VsFonts.EnvironmentFontSizeKey, so text follows the user's Tools > Options > Environment > Fonts and Colors setting live instead of staying fixed regardless of DPI/accessibility preferences. Co-Authored-By: Claude Sonnet 5 --- .../Ui/ClaudeToolWindowControl.cs | 59 +++++++++---------- src/ClaudeCodeVS/Ui/ComposeDialog.cs | 7 ++- src/ClaudeCodeVS/Ui/FontScale.cs | 43 ++++++++++++++ 3 files changed, 78 insertions(+), 31 deletions(-) create mode 100644 src/ClaudeCodeVS/Ui/FontScale.cs diff --git a/src/ClaudeCodeVS/Ui/ClaudeToolWindowControl.cs b/src/ClaudeCodeVS/Ui/ClaudeToolWindowControl.cs index 5d3be96..546f466 100644 --- a/src/ClaudeCodeVS/Ui/ClaudeToolWindowControl.cs +++ b/src/ClaudeCodeVS/Ui/ClaudeToolWindowControl.cs @@ -69,6 +69,7 @@ public ClaudeToolWindowControl() SetResourceReference(BackgroundProperty, VsBrushes.ToolWindowBackgroundKey); SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); SetResourceReference(TextBlock.ForegroundProperty, VsBrushes.ToolWindowTextKey); + FontScale.BindRoot(this); var root = new Grid { Margin = new Thickness(10, 8, 10, 8) }; for (int i = 0; i < 7; i++) @@ -77,16 +78,16 @@ public ClaudeToolWindowControl() // ---- Row 0: header (title + status pill) ---- var header = new StackPanel { Margin = new Thickness(0, 0, 0, 8) }; - header.Children.Add(new TextBlock { Text = "Claude Code", FontSize = 15, FontWeight = FontWeights.SemiBold }); + header.Children.Add(Font(new TextBlock { Text = "Claude Code", FontWeight = FontWeights.SemiBold }, 1.25)); var statusRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 3, 0, 0) }; _dot = new Ellipse { Width = 9, Height = 9, Fill = DotIdle, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(0, 0, 6, 0) }; - _statusLine = new TextBlock { VerticalAlignment = VerticalAlignment.Center, FontSize = 12 }; + _statusLine = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center }, 1.0); statusRow.Children.Add(_dot); statusRow.Children.Add(_statusLine); header.Children.Add(statusRow); - _endpointLine = new TextBlock { FontSize = 11, Opacity = 0.65, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 1, 0, 0) }; + _endpointLine = Font(new TextBlock { Opacity = 0.65, TextTrimming = TextTrimming.CharacterEllipsis, Margin = new Thickness(0, 1, 0, 0) }, 0.92); header.Children.Add(_endpointLine); Grid.SetRow(header, 0); @@ -178,16 +179,15 @@ public ClaudeToolWindowControl() // Surfaces the otherwise-silent gap where the IDE WebSocket connected but vs-debug/vs-semantic/ // tests never loaded (Claude launched outside the workspace, or project servers unapproved). var warnStack = new StackPanel(); - _toolsWarningTitle = new TextBlock + _toolsWarningTitle = Font(new TextBlock { Text = "⚠ Workspace hooks & tools didn't load for this session", - FontSize = 12, FontWeight = FontWeights.SemiBold, Foreground = WarnText, TextWrapping = TextWrapping.Wrap, - }; + }, 1.0); warnStack.Children.Add(_toolsWarningTitle); - _toolsWarningText = new TextBlock { FontSize = 11.5, Opacity = 0.85, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0) }; + _toolsWarningText = Font(new TextBlock { Opacity = 0.85, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 3, 0, 0) }, 0.96); warnStack.Children.Add(_toolsWarningText); var warnButtons = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 6, 0, 0) }; warnButtons.Children.Add(MakeButton("Relaunch Claude Code", () => { _ = BridgeStatus.LaunchAction?.Invoke(); })); @@ -206,14 +206,14 @@ public ClaudeToolWindowControl() Grid.SetRow(_toolsWarningCard, 2); // ---- Row 3: stats card ---- - _editsLine = new TextBlock { FontSize = 12, TextWrapping = TextWrapping.Wrap }; - _debugLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }; - _latestLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 0) }; - _sessionLine = new TextBlock { FontSize = 12, Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }; + _editsLine = Font(new TextBlock { TextWrapping = TextWrapping.Wrap }, 1.0); + _debugLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }, 1.0); + _latestLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 4, 0, 0) }, 1.0); + _sessionLine = Font(new TextBlock { Opacity = 0.9, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 2, 0, 0) }, 1.0); // Cost is an estimate, so it's gated behind a toggle rather than shown by default. _costButton = MakeButton("ā‰ˆ Show est. cost", ToggleCost); - _costText = new TextBlock { FontSize = 12, VerticalAlignment = VerticalAlignment.Center, Opacity = 0.9, Margin = new Thickness(8, 0, 0, 0), Visibility = Visibility.Collapsed }; + _costText = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.9, Margin = new Thickness(8, 0, 0, 0), Visibility = Visibility.Collapsed }, 1.0); _costRow = new StackPanel { Orientation = Orientation.Horizontal, Margin = new Thickness(0, 6, 0, 0), Visibility = Visibility.Collapsed }; _costRow.Children.Add(_costButton); _costRow.Children.Add(_costText); @@ -235,7 +235,7 @@ public ClaudeToolWindowControl() Grid.SetRow(statsCard, 3); // ---- Row 4: pending diffs (collapsed when none) ---- - _pendingText = new TextBlock { FontSize = 12, TextWrapping = TextWrapping.Wrap }; + _pendingText = Font(new TextBlock { TextWrapping = TextWrapping.Wrap }, 1.0); _pendingCard = new Border { Background = Chip, @@ -251,13 +251,12 @@ public ClaudeToolWindowControl() // Screenshots can't be pasted into the CLI on Windows (open upstream gap), so the panel is the // paste/drop point: stage the file, then push an at_mentioned so the reference lands in the // CLI's composer with no path typing. Chips show what's staged; Ɨ removes, click re-mentions. - var attachHint = new TextBlock + var attachHint = Font(new TextBlock { Text = "šŸ“Ž Attach — drop images/files here, or", - FontSize = 11.5, Opacity = 0.75, VerticalAlignment = VerticalAlignment.Center, - }; + }, 0.96); var attachHeader = new StackPanel { Orientation = Orientation.Horizontal }; attachHeader.Children.Add(attachHint); var pasteBtn = MakeButton("Paste", PasteFromClipboard); @@ -272,14 +271,13 @@ public ClaudeToolWindowControl() _attachClear.Visibility = Visibility.Collapsed; attachHeader.Children.Add(_attachClear); // A live "what will this cost" readout for the staged set - same estimate language as the cost row. - _attachSummary = new TextBlock + _attachSummary = Font(new TextBlock { - FontSize = 11.5, Opacity = 0.65, VerticalAlignment = VerticalAlignment.Center, Margin = new Thickness(2, 0, 0, 0), Visibility = Visibility.Collapsed, - }; + }, 0.96); _attachSummary.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); attachHeader.Children.Add(_attachSummary); @@ -305,7 +303,7 @@ public ClaudeToolWindowControl() CommandBindings.Add(new CommandBinding(ApplicationCommands.Paste, (s, e) => PasteFromClipboard())); // ---- Row 6: feed label ---- - var feedLabel = new TextBlock { Text = "ACTIVITY", FontSize = 10, FontWeight = FontWeights.SemiBold, Opacity = 0.55, Margin = new Thickness(2, 0, 0, 4) }; + var feedLabel = Font(new TextBlock { Text = "ACTIVITY", FontWeight = FontWeights.SemiBold, Opacity = 0.55, Margin = new Thickness(2, 0, 0, 4) }, 0.83); Grid.SetRow(feedLabel, 6); // ---- Row 7: curated activity feed ---- @@ -386,7 +384,7 @@ private void AddFeedLine(LogLevel level, string text) // Raw JSON frames and notification noise stay in the Output pane; keep the panel readable. if (level == LogLevel.Frame || level == LogLevel.Event) return; - var tb = new TextBlock { Text = text, FontFamily = Mono, FontSize = 11.5, TextWrapping = TextWrapping.NoWrap }; + var tb = Font(new TextBlock { Text = text, FontFamily = Mono, TextWrapping = TextWrapping.NoWrap }, 0.96); if (level == LogLevel.Error) tb.Foreground = ErrText; else if (level == LogLevel.Warn) tb.Foreground = WarnText; else tb.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); @@ -664,29 +662,27 @@ private void RefreshAttachments() var it = item; // capture per chip var est = it.EstTokens is long e ? $"\nā‰ˆ {Tok(e)} tokens when read (estimate)" : ""; var toolNote = it.NeedsTool ? "\nNot a format Claude reads directly - it will use a script/tool on it." : ""; - var name = new TextBlock + var name = Font(new TextBlock { Text = (it.IsImage ? "šŸ–¼ " : it.NeedsTool ? "🧰 " : "šŸ“„ ") + it.FileName + (it.Sent ? "" : " ā³"), - FontSize = 11.5, VerticalAlignment = VerticalAlignment.Center, Cursor = Cursors.Hand, ToolTip = it.MentionPath + est + toolNote + (it.Sent ? "\nClick to @-mention it again." : "\nStaged - sends when Claude connects. Click to retry."), - }; + }, 0.96); name.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); name.MouseLeftButtonUp += (s, e) => _ = Task.Run(() => Attachments.AttachmentService.ResendAsync(it)); - var close = new TextBlock + var close = Font(new TextBlock { Text = "āœ•", - FontSize = 11.5, Margin = new Thickness(7, 0, 0, 0), VerticalAlignment = VerticalAlignment.Center, Cursor = Cursors.Hand, Opacity = 0.65, ToolTip = it.WasCopied ? "Remove (deletes the staged copy)" : "Remove from the tray", - }; + }, 0.96); close.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); close.MouseLeftButtonUp += (s, e) => Attachments.AttachmentService.Remove(it); @@ -731,6 +727,10 @@ private static string ShortModel(string model) return model; } + /// Scales an element's FontSize relative to VS's Environment Font size (see ). + private static T Font(T el, double ratio) where T : FrameworkElement + => FontScale.Bind(el, typeof(ClaudeToolWindowControl), ratio); + /// A flat, non-selectable list item (so the feed reads like a log, not a selectable list). private static Style FlatItemStyle() { @@ -747,16 +747,15 @@ private static Style FlatItemStyle() /// A flat, theme-aware button: a rounded chip that lightens on hover, themed text. private static Button MakeButton(string text, Action onClick) { - var b = new Button + var b = Font(new Button { Content = text, Margin = new Thickness(0, 0, 6, 0), Padding = new Thickness(10, 3, 10, 3), - FontSize = 12, Cursor = Cursors.Hand, Background = Chip, BorderThickness = new Thickness(0), - }; + }, 1.0); b.SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); var border = new System.Windows.FrameworkElementFactory(typeof(Border)); diff --git a/src/ClaudeCodeVS/Ui/ComposeDialog.cs b/src/ClaudeCodeVS/Ui/ComposeDialog.cs index c0305cd..4ab6a97 100644 --- a/src/ClaudeCodeVS/Ui/ComposeDialog.cs +++ b/src/ClaudeCodeVS/Ui/ComposeDialog.cs @@ -33,6 +33,7 @@ private ComposeDialog(string initialText) SetResourceReference(BackgroundProperty, VsBrushes.ToolWindowBackgroundKey); SetResourceReference(ForegroundProperty, VsBrushes.ToolWindowTextKey); + FontScale.BindRoot(this); var grid = new Grid { Margin = new Thickness(12) }; grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto }); @@ -69,7 +70,7 @@ private ComposeDialog(string initialText) Grid.SetRow(_box, 1); var footer = new DockPanel { Margin = new Thickness(0, 8, 0, 0) }; - _estimate = new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.65, FontSize = 11.5 }; + _estimate = Font(new TextBlock { VerticalAlignment = VerticalAlignment.Center, Opacity = 0.65 }, 0.96); _estimate.SetResourceReference(TextBlock.ForegroundProperty, VsBrushes.ToolWindowTextKey); footer.Children.Add(_estimate); @@ -115,6 +116,10 @@ private void UpdateEstimate() return string.IsNullOrWhiteSpace(text) ? null : text; } + /// Scales an element's FontSize relative to VS's Environment Font size (see ). + private static T Font(T el, double ratio) where T : FrameworkElement + => FontScale.Bind(el, typeof(ComposeDialog), ratio); + /// Minimal ICommand for the Ctrl+Enter accelerator (no framework helper in scope). private sealed class RelayCommand : ICommand { diff --git a/src/ClaudeCodeVS/Ui/FontScale.cs b/src/ClaudeCodeVS/Ui/FontScale.cs new file mode 100644 index 0000000..6ea7cdf --- /dev/null +++ b/src/ClaudeCodeVS/Ui/FontScale.cs @@ -0,0 +1,43 @@ +using System; +using System.Globalization; +using System.Windows; +using System.Windows.Data; +using System.Windows.Documents; +using Microsoft.VisualStudio.Shell; + +namespace ClaudeCodeVs.Ui; + +/// +/// Ties this extension's hand-built WPF panels to VS's own Environment Font size (Tools > Options > +/// Environment > Fonts and Colors), instead of the hardcoded point values every element used before. +/// makes a panel's own FontSize track VsFonts.EnvironmentFontSizeKey; +/// scales a descendant relative to that root so the panel's size hierarchy +/// (title vs. body vs. caption) survives, and both update live if the user changes the VS setting. +/// +internal static class FontScale +{ + private static readonly IValueConverter Ratio = new RatioConverter(); + + public static void BindRoot(FrameworkElement root) + => root.SetResourceReference(TextElement.FontSizeProperty, VsFonts.EnvironmentFontSizeKey); + + public static T Bind(T el, Type rootType, double ratio = 1.0) where T : FrameworkElement + { + el.SetBinding(TextElement.FontSizeProperty, new Binding + { + Path = new PropertyPath(TextElement.FontSizeProperty), + RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, rootType, 1), + Converter = Ratio, + ConverterParameter = ratio, + }); + return el; + } + + private sealed class RatioConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, CultureInfo culture) + => value is double d && parameter is double r ? d * r : value; + public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture) + => throw new NotSupportedException(); + } +}