Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Adjust colors of AvalonEdit built-in highlightings for dark themes #3138

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions ILSpy/TextView/DecompilerTextEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using ICSharpCode.AvalonEdit;
using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;

namespace ICSharpCode.ILSpy.TextView;

public class DecompilerTextEditor : TextEditor
{
protected override IVisualLineTransformer CreateColorizer(IHighlightingDefinition highlightingDefinition)
{
return new ThemeAwareHighlightingColorizer(highlightingDefinition);
}
}
55 changes: 27 additions & 28 deletions ILSpy/TextView/DecompilerTextView.xaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
<UserControl x:Class="ICSharpCode.ILSpy.TextView.DecompilerTextView" x:ClassModifier="public" x:Name="self"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
xmlns:controls="clr-namespace:ICSharpCode.ILSpy.Controls"
xmlns:local="clr-namespace:ICSharpCode.ILSpy.TextView"
xmlns:ae="clr-namespace:ICSharpCode.AvalonEdit;assembly=ICSharpCode.AvalonEdit"
xmlns:editing="clr-namespace:ICSharpCode.AvalonEdit.Editing;assembly=ICSharpCode.AvalonEdit"
xmlns:folding="clr-namespace:ICSharpCode.AvalonEdit.Folding;assembly=ICSharpCode.AvalonEdit"
xmlns:styles="urn:TomsToolbox.Wpf.Styles"
xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes">
xmlns:properties="clr-namespace:ICSharpCode.ILSpy.Properties"
xmlns:controls="clr-namespace:ICSharpCode.ILSpy.Controls"
xmlns:local="clr-namespace:ICSharpCode.ILSpy.TextView"
xmlns:editing="clr-namespace:ICSharpCode.AvalonEdit.Editing;assembly=ICSharpCode.AvalonEdit"
xmlns:folding="clr-namespace:ICSharpCode.AvalonEdit.Folding;assembly=ICSharpCode.AvalonEdit"
xmlns:styles="urn:TomsToolbox.Wpf.Styles"
xmlns:themes="clr-namespace:ICSharpCode.ILSpy.Themes">
<UserControl.Resources>
<BooleanToVisibilityConverter x:Key="boolToVisibility" />
<SolidColorBrush x:Key="waitAdornerBackgoundBrush" Color="{DynamicResource {x:Static SystemColors.WindowColorKey}}" Opacity=".75"/>
<SolidColorBrush x:Key="waitAdornerBackgoundBrush" Color="{DynamicResource {x:Static SystemColors.WindowColorKey}}" Opacity=".75" />
<Style TargetType="{x:Type editing:TextArea}">
<Setter Property="FocusVisualStyle" Value="{x:Null}" />
<Setter Property="SelectionForeground" Value="{x:Null}" />
Expand All @@ -31,15 +30,15 @@
<Grid>
<Border BorderThickness="1,1,0,1" BorderBrush="{DynamicResource {x:Static SystemColors.ControlLightBrushKey}}">
<Grid>
<ae:TextEditor Name="textEditor" AutomationProperties.Name="Decompilation" FontFamily="Consolas" FontSize="10pt" IsReadOnly="True"
Background="{DynamicResource {x:Static themes:ResourceKeys.TextBackgroundBrush}}"
Foreground="{DynamicResource {x:Static themes:ResourceKeys.TextForegroundBrush}}"
LineNumbersForeground="{DynamicResource {x:Static themes:ResourceKeys.LineNumbersForegroundBrush}}"
folding:FoldingMargin.FoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
folding:FoldingMargin.SelectedFoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
folding:FoldingMargin.FoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"
folding:FoldingMargin.SelectedFoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
<ae:TextEditor.Resources>
<local:DecompilerTextEditor x:Name="textEditor" AutomationProperties.Name="Decompilation" FontFamily="Consolas" FontSize="10pt" IsReadOnly="True"
Background="{DynamicResource {x:Static themes:ResourceKeys.TextBackgroundBrush}}"
Foreground="{DynamicResource {x:Static themes:ResourceKeys.TextForegroundBrush}}"
LineNumbersForeground="{DynamicResource {x:Static themes:ResourceKeys.LineNumbersForegroundBrush}}"
folding:FoldingMargin.FoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
folding:FoldingMargin.SelectedFoldingMarkerBackgroundBrush="{DynamicResource {x:Static SystemColors.WindowBrushKey}}"
folding:FoldingMargin.FoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlDarkBrushKey}}"
folding:FoldingMargin.SelectedFoldingMarkerBrush="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}">
<local:DecompilerTextEditor.Resources>
<!-- prevent App-wide button style from applying to the buttons in the search box -->
<Style TargetType="{x:Type Button}">
<Setter Property="Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
Expand All @@ -65,9 +64,9 @@
</Trigger>
</Style.Triggers>
</Style>
</ae:TextEditor.Resources>
<ae:TextEditor.Template>
<ControlTemplate TargetType="{x:Type ae:TextEditor}">
</local:DecompilerTextEditor.Resources>
<local:DecompilerTextEditor.Template>
<ControlTemplate TargetType="{x:Type local:DecompilerTextEditor}">
<controls:ZoomScrollViewer
Focusable="False"
x:Name="PART_ScrollViewer"
Expand All @@ -85,15 +84,15 @@
TextOptions.TextFormattingMode="{Binding CurrentZoom, ElementName=PART_ScrollViewer, Converter={x:Static local:ZoomLevelToTextFormattingModeConverter.Instance}}" />
<ControlTemplate.Triggers>
<Trigger Property="WordWrap"
Value="True">
Value="True">
<Setter TargetName="PART_ScrollViewer"
Property="HorizontalScrollBarVisibility"
Value="Disabled" />
Property="HorizontalScrollBarVisibility"
Value="Disabled" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</ae:TextEditor.Template>
</ae:TextEditor>
</local:DecompilerTextEditor.Template>
</local:DecompilerTextEditor>
<Border Name="waitAdorner" Background="{StaticResource waitAdornerBackgoundBrush}" Visibility="Collapsed">
<Grid>
<Grid.ColumnDefinitions>
Expand All @@ -102,10 +101,10 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<TextBlock Name="progressTitle" FontSize="14pt" Text="{x:Static properties:Resources.Decompiling}" Margin="3"/>
<TextBlock Name="progressTitle" FontSize="14pt" Text="{x:Static properties:Resources.Decompiling}" Margin="3" />
<ProgressBar Name="progressBar" Height="16" />
<TextBlock Name="progressText" Visibility="Collapsed" Margin="3" />
<Button Click="CancelButton_Click" HorizontalAlignment="Center" Margin="3" Content="{x:Static properties:Resources.Cancel}"/>
<Button Click="CancelButton_Click" HorizontalAlignment="Center" Margin="3" Content="{x:Static properties:Resources.Cancel}" />
</StackPanel>
</Grid>
</Border>
Expand Down
105 changes: 105 additions & 0 deletions ILSpy/TextView/ThemeAwareHighlightingColorizer.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Windows.Media;

using ICSharpCode.AvalonEdit.Highlighting;
using ICSharpCode.AvalonEdit.Rendering;
using ICSharpCode.ILSpy.Themes;

namespace ICSharpCode.ILSpy.TextView;

#nullable enable

public class ThemeAwareHighlightingColorizer : HighlightingColorizer
{
private readonly Dictionary<HighlightingColor, HighlightingColor> _darkColors = new();
private readonly bool _isHighlightingThemeAware;

public ThemeAwareHighlightingColorizer(IHighlightingDefinition highlightingDefinition)
: base(highlightingDefinition)
{
_isHighlightingThemeAware = ThemeManager.Current.IsThemeAware(highlightingDefinition);
}

protected override void ApplyColorToElement(VisualLineElement element, HighlightingColor color)
{
if (!_isHighlightingThemeAware && ThemeManager.Current.IsDarkTheme)
{
color = GetColorForDarkTheme(color);
}

base.ApplyColorToElement(element, color);
}

private HighlightingColor GetColorForDarkTheme(HighlightingColor lightColor)
{
if (lightColor.Foreground is null && lightColor.Background is null)
{
return lightColor;
}

if (!_darkColors.TryGetValue(lightColor, out var darkColor))
{
darkColor = lightColor.Clone();
darkColor.Foreground = AdjustForDarkTheme(darkColor.Foreground);
darkColor.Background = AdjustForDarkTheme(darkColor.Background);

_darkColors[lightColor] = darkColor;
}

return darkColor;
}

private static HighlightingBrush? AdjustForDarkTheme(HighlightingBrush? lightBrush)
{
if (lightBrush is SimpleHighlightingBrush simpleBrush && simpleBrush.GetBrush(null) is SolidColorBrush brush)
{
return new SimpleHighlightingBrush(AdjustForDarkTheme(brush.Color));
}

return lightBrush;
}

private static Color AdjustForDarkTheme(Color color)
{
var c = System.Drawing.Color.FromArgb(color.R, color.G, color.B);
var (h, s, l) = (c.GetHue(), c.GetSaturation(), c.GetBrightness());

// Invert the lightness, but also increase it a bit
l = 1f - MathF.Pow(l, 1.2f);

// Desaturate the colors, as they'd be too intense otherwise
if (s > 0.75f && l < 0.75f)
{
s *= 0.75f;
l *= 1.2f;
}

var (r, g, b) = HslToRgb(h, s, l);
return Color.FromArgb(color.A, r, g, b);
}

private static (byte r, byte g, byte b) HslToRgb(float h, float s, float l)
{
// https://en.wikipedia.org/wiki/HSL_and_HSV#HSL_to_RGB

var c = (1f - Math.Abs(2f * l - 1f)) * s;
h = h % 360f / 60f;
var x = c * (1f - Math.Abs(h % 2f - 1f));

var (r1, g1, b1) = (int)Math.Floor(h) switch {
0 => (c, x, 0f),
1 => (x, c, 0f),
2 => (0f, c, x),
3 => (0f, x, c),
4 => (x, 0f, c),
_ => (c, 0f, x)
};

var m = l - c / 2f;
var r = (byte)((r1 + m) * 255f);
var g = (byte)((g1 + m) * 255f);
var b = (byte)((b1 + m) * 255f);
return (r, g, b);
}
}
15 changes: 14 additions & 1 deletion ILSpy/Themes/ThemeManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@
#nullable enable

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

using ICSharpCode.AvalonEdit.Highlighting;

namespace ICSharpCode.ILSpy.Themes
{
public class ThemeManager
{
private const string _isThemeAwareKey = "ILSpy.IsThemeAware";

private string? _theme;
private readonly ResourceDictionary _themeDictionaryContainer = new();
private readonly Dictionary<string, SyntaxColor> _syntaxColors = new();
Expand All @@ -44,6 +46,8 @@ private ThemeManager()

public string DefaultTheme => "Light";

public bool IsDarkTheme { get; private set; }

public static IReadOnlyCollection<string> AllThemes => new[] {
"Light",
"Dark",
Expand Down Expand Up @@ -89,6 +93,13 @@ public void ApplyHighlightingColors(IHighlightingDefinition highlightingDefiniti
if (color is not null)
syntaxColor.ApplyTo(color);
}

highlightingDefinition.Properties[_isThemeAwareKey] = bool.TrueString;
}

public bool IsThemeAware(IHighlightingDefinition highlightingDefinition)
{
return highlightingDefinition.Properties.TryGetValue(_isThemeAwareKey, out var value) && value == bool.TrueString;
}

private void UpdateTheme(string? themeName)
Expand All @@ -109,6 +120,8 @@ private void UpdateTheme(string? themeName)
var resourceDictionary = new ResourceDictionary { Source = new Uri($"/themes/Theme.{themeFileName}.xaml", UriKind.Relative) };
_themeDictionaryContainer.MergedDictionaries.Add(resourceDictionary);

IsDarkTheme = resourceDictionary[ResourceKeys.TextBackgroundBrush] is SolidColorBrush { Color: { R: < 128, G: < 128, B: < 128 } };

// Iterate over keys first, because we don't want to instantiate all values eagerly, if we don't need them.
foreach (var item in resourceDictionary.Keys)
{
Expand Down
Loading