Skip to content

Commit

Permalink
Adjust colors of AvalonEdit built-in highlightings for dark themes
Browse files Browse the repository at this point in the history
  • Loading branch information
ltrzesniewski committed Dec 16, 2023
1 parent 84c5e63 commit c9a5385
Show file tree
Hide file tree
Showing 4 changed files with 159 additions and 29 deletions.
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.FromRgb(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

0 comments on commit c9a5385

Please sign in to comment.