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