Browse Source

Fix #3994: theme the documentation hover popup's chrome

The rich hover popup hardcoded a near-white background and border while
its signature text is coloured by the active highlighting theme, so in
dark mode light-on-dark syntax colours landed on a light box and were
unreadable. The chrome and the doc-link colour now route through
theme-variant brushes; light mode keeps the established near-white look.

Assisted-by: Claude:claude-fable-5:Claude Code
pull/4013/head
Christoph Wille 1 month ago
parent
commit
deb1da33c1
  1. 4
      ILSpy.Tests/Editor/DocumentationLinkTests.cs
  2. 88
      ILSpy.Tests/Editor/DocumentationRendererThemeTests.cs
  3. 18
      ILSpy/App.axaml
  4. 14
      ILSpy/TextView/DocumentationRenderer.cs

4
ILSpy.Tests/Editor/DocumentationLinkTests.cs

@ -80,6 +80,10 @@ public class DocumentationLinkTests @@ -80,6 +80,10 @@ public class DocumentationLinkTests
.FirstOrDefault(tb => tb.Classes.Contains("doc-link"));
link.Should().NotBeNull("a resolvable cref must render as a clickable link");
link!.Cursor.Should().NotBeNull("links show the hand cursor as a click affordance");
window.TryFindResource("ILSpy.DocLinkForeground", window.ActualThemeVariant, out var linkBrush)
.Should().BeTrue("doc links route their colour through a themed brush");
link.Foreground.Should().Be(linkBrush,
"a hardcoded link colour is unreadable on the dark popup background (issue #3994)");
object? navigated = null;
var linkClickedRaised = false;

88
ILSpy.Tests/Editor/DocumentationRendererThemeTests.cs

@ -0,0 +1,88 @@ @@ -0,0 +1,88 @@
// Copyright (c) 2026 Christoph Wille
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless.NUnit;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.Threading;
using AvaloniaEdit.Highlighting;
using AwesomeAssertions;
using ICSharpCode.Decompiler.CSharp.OutputVisitor;
using ICSharpCode.ILSpy.TextView;
using NUnit.Framework;
namespace ICSharpCode.ILSpy.Tests.TextView;
/// <summary>
/// The hover popup's signature text is coloured by the active highlighting theme, so its
/// chrome must follow the same theme variant: dark-theme (light-on-dark) text on a
/// hardcoded near-white background is unreadable (issue #3994). Light mode keeps the
/// established near-white look.
/// </summary>
[TestFixture]
public class DocumentationRendererThemeTests
{
[AvaloniaTest]
public void Popup_Chrome_Follows_The_Active_Theme_Variant()
{
var renderer = new DocumentationRenderer(
new CSharpAmbience(),
new FontFamily("Consolas, Menlo, Monospace"),
12);
renderer.AddSignatureBlock(new RichText("(parameter) string matchText"));
var view = (Border)renderer.CreateView();
var window = new Window { Content = view };
var app = Application.Current!;
var originalVariant = app.RequestedThemeVariant;
try
{
window.Show();
app.RequestedThemeVariant = ThemeVariant.Dark;
Dispatcher.UIThread.RunJobs();
window.TryFindResource("ILSpy.DocTooltipBackground", ThemeVariant.Dark, out var darkBackground)
.Should().BeTrue("the popup chrome must route through themed brushes");
view.Background.Should().Be(darkBackground,
"dark-theme signature colours are unreadable on a light popup background");
window.TryFindResource("ILSpy.DocTooltipBorder", ThemeVariant.Dark, out var darkBorder)
.Should().BeTrue();
view.BorderBrush.Should().Be(darkBorder);
app.RequestedThemeVariant = ThemeVariant.Light;
Dispatcher.UIThread.RunJobs();
((ISolidColorBrush)view.Background!).Color.Should().Be(Color.FromRgb(0xFC, 0xFC, 0xFC),
"light mode keeps the established near-white popup chrome");
((ISolidColorBrush)view.BorderBrush!).Color.Should().Be(Color.FromRgb(0xAA, 0xAA, 0xAA));
}
finally
{
window.Close();
app.RequestedThemeVariant = originalVariant;
}
}
}

18
ILSpy/App.axaml

@ -60,6 +60,12 @@ @@ -60,6 +60,12 @@
<SolidColorBrush x:Key="ILSpy.TooltipBackground" Color="#FFFFE1" />
<SolidColorBrush x:Key="ILSpy.TooltipForeground" Color="Black" />
<SolidColorBrush x:Key="ILSpy.TooltipBorder" Color="#FF767676" />
<!-- Documentation hover popup (signature + XML docs). Kept separate from the
plain ILSpy.Tooltip* brushes: the doc popup uses a near-white surface for
syntax-coloured code, not the classic yellow tooltip fill. -->
<SolidColorBrush x:Key="ILSpy.DocTooltipBackground" Color="#FCFCFC" />
<SolidColorBrush x:Key="ILSpy.DocTooltipBorder" Color="#AAAAAA" />
<SolidColorBrush x:Key="ILSpy.DocLinkForeground" Color="#0066CC" />
<SolidColorBrush x:Key="ILSpy.WindowBackground" Color="#F0F0F0" />
<SolidColorBrush x:Key="ILSpy.WindowForeground" Color="Black" />
<SolidColorBrush x:Key="ILSpy.PaneBackground" Color="White" />
@ -123,6 +129,12 @@ @@ -123,6 +129,12 @@
<SolidColorBrush x:Key="ILSpy.TooltipBackground" Color="#252526" />
<SolidColorBrush x:Key="ILSpy.TooltipForeground" Color="#DCDCDC" />
<SolidColorBrush x:Key="ILSpy.TooltipBorder" Color="#3F3F46" />
<!-- Dark counterpart of the doc hover popup: slightly lighter than the #1E1E1E
editor canvas so the popup reads as a raised surface, with the link blue
brightened for contrast on the dark fill. -->
<SolidColorBrush x:Key="ILSpy.DocTooltipBackground" Color="#252526" />
<SolidColorBrush x:Key="ILSpy.DocTooltipBorder" Color="#3F3F46" />
<SolidColorBrush x:Key="ILSpy.DocLinkForeground" Color="#6FA8DC" />
<SolidColorBrush x:Key="ILSpy.WindowBackground" Color="#252526" />
<SolidColorBrush x:Key="ILSpy.WindowForeground" Color="#DCDCDC" />
<SolidColorBrush x:Key="ILSpy.PaneBackground" Color="#1E1E1E" />
@ -237,6 +249,12 @@ @@ -237,6 +249,12 @@
<Setter Property="BorderThickness" Value="1" />
</Style>
<!-- Links inside the documentation hover popup (DocumentationRenderer tags them with
the doc-link class). Colour lives here so it swaps with the theme variant. -->
<Style Selector="TextBlock.doc-link">
<Setter Property="Foreground" Value="{DynamicResource ILSpy.DocLinkForeground}" />
</Style>
<!-- Window-level chrome: every top-level window picks up these brushes for the
canvas behind the menu / toolbar / dock area. Foreground propagates by
inheritance so default-styled text inside the window goes light on dark. -->

14
ILSpy/TextView/DocumentationRenderer.cs

@ -48,8 +48,6 @@ namespace ICSharpCode.ILSpy.TextView @@ -48,8 +48,6 @@ namespace ICSharpCode.ILSpy.TextView
/// </summary>
public sealed class DocumentationRenderer
{
static readonly IBrush HyperlinkBrush = new SolidColorBrush(Color.FromRgb(0x00, 0x66, 0xCC));
readonly IAmbience ambience;
readonly FontFamily codeFont;
readonly double fontSize;
@ -118,14 +116,18 @@ namespace ICSharpCode.ILSpy.TextView @@ -118,14 +116,18 @@ namespace ICSharpCode.ILSpy.TextView
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
MaxHeight = maxHeight,
};
return new Border {
var border = new Border {
BorderThickness = new Thickness(1),
BorderBrush = new SolidColorBrush(Color.FromRgb(0xAA, 0xAA, 0xAA)),
Background = new SolidColorBrush(Color.FromRgb(0xFC, 0xFC, 0xFC)),
Padding = new Thickness(6),
MaxWidth = maxWidth,
Child = scroll,
};
// The signature text is coloured by the active highlighting theme, so the chrome
// must follow the same theme variant: dark-theme text on a fixed light fill is
// unreadable. DynamicResource-style bindings keep it in sync on theme switches.
border.Bind(Border.BackgroundProperty, border.GetResourceObservable("ILSpy.DocTooltipBackground"));
border.Bind(Border.BorderBrushProperty, border.GetResourceObservable("ILSpy.DocTooltipBorder"));
return border;
}
public void AddSignatureBlock(RichText signature)
@ -392,8 +394,8 @@ namespace ICSharpCode.ILSpy.TextView @@ -392,8 +394,8 @@ namespace ICSharpCode.ILSpy.TextView
// embedded TextBlock inside an InlineUIContainer.
static TextBlock CreateLinkTextBlock()
{
// Foreground comes from the App.axaml "doc-link" style so it follows the theme.
return new TextBlock {
Foreground = HyperlinkBrush,
TextDecorations = TextDecorations.Underline,
Cursor = new Cursor(StandardCursorType.Hand),
// A null background makes the TextBlock hit-test invisible — clicks would

Loading…
Cancel
Save