diff --git a/ILSpy.Tests/Editor/DocumentationLinkTests.cs b/ILSpy.Tests/Editor/DocumentationLinkTests.cs
index 9b2ee394e..a9a44034b 100644
--- a/ILSpy.Tests/Editor/DocumentationLinkTests.cs
+++ b/ILSpy.Tests/Editor/DocumentationLinkTests.cs
@@ -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;
diff --git a/ILSpy.Tests/Editor/DocumentationRendererThemeTests.cs b/ILSpy.Tests/Editor/DocumentationRendererThemeTests.cs
new file mode 100644
index 000000000..b375c6f73
--- /dev/null
+++ b/ILSpy.Tests/Editor/DocumentationRendererThemeTests.cs
@@ -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;
+
+///
+/// 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.
+///
+[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;
+ }
+ }
+}
diff --git a/ILSpy/App.axaml b/ILSpy/App.axaml
index a54f3dcd5..3ccb14306 100644
--- a/ILSpy/App.axaml
+++ b/ILSpy/App.axaml
@@ -60,6 +60,12 @@
+
+
+
+
@@ -123,6 +129,12 @@
+
+
+
+
@@ -237,6 +249,12 @@
+
+
+
diff --git a/ILSpy/TextView/DocumentationRenderer.cs b/ILSpy/TextView/DocumentationRenderer.cs
index 4f2ef1602..011f1eecd 100644
--- a/ILSpy/TextView/DocumentationRenderer.cs
+++ b/ILSpy/TextView/DocumentationRenderer.cs
@@ -48,8 +48,6 @@ namespace ICSharpCode.ILSpy.TextView
///
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
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
// 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