Skip to content

Commit f4d52b3

Browse files
committed
Theme / Dark Mode support
1 parent 100fc51 commit f4d52b3

13 files changed

Lines changed: 448 additions & 142 deletions

File tree

PilotAIAssistantControl/ChatItem.cs

Lines changed: 102 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,40 @@
1414
namespace PilotAIAssistantControl {
1515

1616
public class ChatItem {
17+
private const string UserSender = "You";
18+
private const string AiSender = "AI Assistant";
19+
private const string SystemSender = "System";
20+
21+
#if !WPF
22+
// Application.Current.RequestedTheme is set once at startup and does NOT update
23+
// when the system theme changes at runtime. The UI layer sets this from ActualTheme
24+
// so SearchThemeDictionaries resolves the correct theme dictionary.
25+
//internal static bool IsDarkTheme { get; set { } } = true;
26+
internal static bool IsDarkTheme { get; set; }
27+
#endif
28+
1729
public string Message { get; set; } = string.Empty;
1830
public string Sender { get; set; } = string.Empty;
31+
public bool IsSystemError { get; set; }
1932

20-
// Default properties using the helper
21-
public Brush BackgroundColor { get; set; } = GetBrush(Colors.White);
22-
public Brush SenderColor { get; set; } = GetBrush(Colors.Gray);
33+
// Default properties use theme-aware brushes (with safe fallbacks)
34+
public Brush BackgroundColor { get; set; } = GetThemeBrush("CardBackgroundFillColorDefaultBrush", Colors.White);
35+
public Brush SenderColor { get; set; } = GetThemeBrush("TextFillColorSecondaryBrush", Colors.Gray);
2336

2437
public HorizontalAlignment Alignment { get; set; } = HorizontalAlignment.Left;
2538
public List<CodeBlock> CodeBlocks { get; set; } = new();
2639
public bool HasCodeBlocks => CodeBlocks.Count > 0;
27-
public bool IsAI => Sender == "AI Assistant";
40+
public bool IsAI => Sender == AiSender;
41+
public bool IsUser => Sender == UserSender;
2842

2943
// --- Factory Methods ---
3044

3145
public static ChatItem CreateUserMessage(string message) {
32-
return new ChatItem {
46+
return ApplyThemeBrushes(new ChatItem {
3347
Message = message,
34-
Sender = "You",
35-
// AliceBlue is a standard "Light Blue"
36-
BackgroundColor = GetBrush(Colors.AliceBlue),
37-
// DarkSlateBlue is readable against light backgrounds
38-
SenderColor = GetBrush(Colors.DarkSlateBlue),
48+
Sender = UserSender,
3949
Alignment = HorizontalAlignment.Right
40-
};
50+
});
4151
}
4252

4353
private static Regex FindCodeBlockEnd = new(@"^```$", RegexOptions.Multiline);
@@ -46,29 +56,31 @@ public static ChatItem CreateAiMessage(string message) {
4656
// bit hacky to make sure scrollbar doesn't make readability hard
4757
message = FindCodeBlockEnd.Replace(message, "\n```");
4858

49-
return new ChatItem {
59+
return ApplyThemeBrushes(new ChatItem {
5060
Message = message,
51-
Sender = "AI Assistant",
52-
// WhiteSmoke is a standard "Light Gray" perfect for message bubbles
53-
BackgroundColor = GetBrush(Colors.WhiteSmoke),
54-
SenderColor = GetBrush(Colors.SeaGreen),
61+
Sender = AiSender,
5562
Alignment = HorizontalAlignment.Left,
5663
CodeBlocks = CodeBlock.ExtractCodeBlocks(message)
57-
};
64+
});
5865
}
5966

6067
public static ChatItem CreateSystemMessage(string message, bool isError = false) {
61-
// Setup colors based on error state using Named Colors
62-
Color bgColor = isError ? Colors.MistyRose : Colors.Cornsilk;
63-
Color txtColor = isError ? Colors.DarkRed : Colors.DarkOrange;
64-
65-
return new ChatItem {
68+
return ApplyThemeBrushes(new ChatItem {
6669
Message = message,
67-
Sender = "System",
68-
BackgroundColor = GetBrush(bgColor),
69-
SenderColor = GetBrush(txtColor),
70+
Sender = SystemSender,
71+
IsSystemError = isError,
7072
Alignment = HorizontalAlignment.Stretch
71-
};
73+
});
74+
}
75+
76+
public ChatItem CreateRethemedCopy() {
77+
return ApplyThemeBrushes(new ChatItem {
78+
Message = Message,
79+
Sender = Sender,
80+
IsSystemError = IsSystemError,
81+
Alignment = Alignment,
82+
CodeBlocks = CodeBlocks
83+
});
7284
}
7385

7486
// --- Helpers ---
@@ -79,13 +91,75 @@ public static ChatItem CreateSystemMessage(string message, bool isError = false)
7991
private static Brush GetBrush(Color color) {
8092
#if WPF
8193
var brush = new SolidColorBrush(color);
82-
// Freezing is important in WPF for performance and thread safety
94+
// Freezing is important in WPF for performance and thread safety
8395
// (similar to how Brushes.White works)
84-
brush.Freeze();
96+
brush.Freeze();
8597
return brush;
8698
#else
8799
return new SolidColorBrush(color);
88100
#endif
89101
}
102+
103+
internal static Brush GetThemeBrush(string key, Color fallbackColor) {
104+
#if WPF
105+
if (Application.Current?.TryFindResource(key) is Brush resourceBrush) {
106+
return resourceBrush;
107+
}
108+
#else
109+
// TryGetValue doesn't search ThemeDictionaries inside MergedDictionaries,
110+
// so search explicitly based on the current app theme.
111+
var resources = Application.Current?.Resources;
112+
if (resources != null) {
113+
string theme = IsDarkTheme ? "Dark" : "Light";
114+
if (SearchThemeDictionaries(resources, key, theme) is Brush found)
115+
return found;
116+
if (resources.TryGetValue(key, out object? resource) && resource is Brush resourceBrush)
117+
return resourceBrush;
118+
}
119+
#endif
120+
return GetBrush(fallbackColor);
121+
}
122+
123+
#if !WPF
124+
private static Brush? SearchThemeDictionaries(ResourceDictionary dict, string key, string theme) {
125+
if (dict.ThemeDictionaries.Count > 0 &&
126+
dict.ThemeDictionaries.TryGetValue(theme, out object? td) &&
127+
td is ResourceDictionary themed &&
128+
themed.TryGetValue(key, out object? resource) &&
129+
resource is Brush brush) {
130+
return brush;
131+
}
132+
foreach (var merged in dict.MergedDictionaries) {
133+
if (SearchThemeDictionaries(merged, key, theme) is Brush found)
134+
return found;
135+
}
136+
return null;
137+
}
138+
#endif
139+
140+
private static ChatItem ApplyThemeBrushes(ChatItem item) {
141+
if (item.Sender == UserSender) {
142+
item.BackgroundColor = GetThemeBrush("TextFillColorInverseBrush", Colors.Orange);
143+
item.SenderColor = GetThemeBrush("TextOnAccentFillColorSecondary", Colors.Gray);
144+
return item;
145+
}
146+
147+
if (item.Sender == AiSender) {
148+
item.BackgroundColor = GetThemeBrush("CardBackgroundFillColorSecondaryBrush", Colors.WhiteSmoke);
149+
item.SenderColor = GetThemeBrush("TextFillColorSecondaryBrush", Colors.SeaGreen);
150+
return item;
151+
}
152+
153+
if (item.Sender == SystemSender) {
154+
string bgKey = item.IsSystemError ? "InfoBarErrorSeverityBackgroundBrush" : "InfoBarWarningSeverityBackgroundBrush";
155+
item.BackgroundColor = GetThemeBrush(bgKey, item.IsSystemError ? Colors.MistyRose : Colors.Cornsilk);
156+
item.SenderColor = GetThemeBrush("TextFillColorPrimaryBrush", item.IsSystemError ? Colors.DarkRed : Colors.DarkOrange);
157+
return item;
158+
}
159+
160+
item.BackgroundColor = GetThemeBrush("CardBackgroundFillColorDefaultBrush", Colors.White);
161+
item.SenderColor = GetThemeBrush("TextFillColorSecondaryBrush", Colors.Gray);
162+
return item;
163+
}
90164
}
91165
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using System.Globalization;
3+
4+
#if WPF
5+
using System.Windows;
6+
using System.Windows.Data;
7+
#else
8+
using Microsoft.UI.Xaml;
9+
using Microsoft.UI.Xaml.Data;
10+
#endif
11+
12+
namespace PilotAIAssistantControl {
13+
14+
#if !WPF
15+
/// <summary>
16+
/// Converts a ChatItem to the appropriate Border Style for chat bubbles.
17+
/// Each Style uses {ThemeResource} for backgrounds, so they auto-update on theme changes.
18+
/// </summary>
19+
public class ChatItemToBubbleStyleConverter : IValueConverter {
20+
public Style? UserStyle { get; set; }
21+
public Style? AiStyle { get; set; }
22+
public Style? SystemWarningStyle { get; set; }
23+
public Style? SystemErrorStyle { get; set; }
24+
25+
public object? Convert(object value, Type targetType, object parameter, string language) {
26+
if (value is ChatItem item) {
27+
if (item.IsUser) return UserStyle;
28+
if (item.IsAI) return AiStyle;
29+
if (item.IsSystemError) return SystemErrorStyle;
30+
return SystemWarningStyle;
31+
}
32+
return AiStyle;
33+
}
34+
35+
public object ConvertBack(object value, Type targetType, object parameter, string language)
36+
=> throw new NotImplementedException();
37+
}
38+
#endif
39+
}

PilotAIAssistantControl/MarkdownViewer.cs

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@
88
#else
99
using CommunityToolkit.WinUI.UI.Controls;
1010
//using CommunityToolkit.WinUI.Controls;
11+
using Microsoft.UI;
1112
using Microsoft.UI.Xaml;
1213
using Microsoft.UI.Xaml.Controls;
14+
using Microsoft.UI.Xaml.Media;
15+
using Windows.UI;
1316
#endif
1417

1518
namespace PilotAIAssistantControl {
@@ -32,6 +35,8 @@ public MarkdownViewer() {
3235
public class MarkdownViewer : UserControl {
3336
private readonly MarkdownTextBlock _markdownBlock;
3437

38+
// --- Dependency Properties ---
39+
3540
public static readonly DependencyProperty MarkdownProperty =
3641
DependencyProperty.Register(
3742
nameof(Markdown),
@@ -44,16 +49,88 @@ public string Markdown {
4449
set => SetValue(MarkdownProperty, value);
4550
}
4651

52+
public static readonly DependencyProperty CodeBackgroundBrushProperty =
53+
DependencyProperty.Register(
54+
nameof(CodeBackgroundBrush),
55+
typeof(Brush),
56+
typeof(MarkdownViewer),
57+
new PropertyMetadata(null, OnCodeBrushChanged));
58+
59+
public Brush? CodeBackgroundBrush {
60+
get => (Brush?)GetValue(CodeBackgroundBrushProperty);
61+
set => SetValue(CodeBackgroundBrushProperty, value);
62+
}
63+
64+
public static readonly DependencyProperty CodeForegroundBrushProperty =
65+
DependencyProperty.Register(
66+
nameof(CodeForegroundBrush),
67+
typeof(Brush),
68+
typeof(MarkdownViewer),
69+
new PropertyMetadata(null, OnCodeBrushChanged));
70+
71+
public Brush? CodeForegroundBrush {
72+
get => (Brush?)GetValue(CodeForegroundBrushProperty);
73+
set => SetValue(CodeForegroundBrushProperty, value);
74+
}
75+
76+
public static readonly DependencyProperty CodeBorderBrushProperty =
77+
DependencyProperty.Register(
78+
nameof(CodeBorderBrush),
79+
typeof(Brush),
80+
typeof(MarkdownViewer),
81+
new PropertyMetadata(null, OnCodeBrushChanged));
82+
83+
public Brush? CodeBorderBrush {
84+
get => (Brush?)GetValue(CodeBorderBrushProperty);
85+
set => SetValue(CodeBorderBrushProperty, value);
86+
}
87+
88+
// --- Callbacks ---
89+
4790
private static void OnMarkdownChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
4891
var viewer = (MarkdownViewer)d;
4992
viewer._markdownBlock.Text = e.NewValue as string ?? string.Empty;
5093
}
5194

95+
private static void OnCodeBrushChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) {
96+
var viewer = (MarkdownViewer)d;
97+
viewer.ApplyCodeBrushes();
98+
}
99+
100+
// --- Constructor ---
101+
52102
public MarkdownViewer() {
53-
//_markdownBlock = new MarkdownTextBlock(){UseAutoLinks=true,UseEmphasisExtras=true,UseListExtras=true,UseTaskLists=true,UsePipeTables=true, };
54-
_markdownBlock = new MarkdownTextBlock(){UseSyntaxHighlighting=true};
103+
_markdownBlock = new MarkdownTextBlock {
104+
UseSyntaxHighlighting = false,
105+
IsTextSelectionEnabled = true,
106+
Background = new SolidColorBrush(Colors.Transparent),
107+
Margin = new Thickness(0),
108+
Padding = new Thickness(0),
109+
// Static styling (non-theme-dependent)
110+
CodeBorderThickness = new Thickness(1),
111+
CodePadding = new Thickness(8),
112+
CodeMargin = new Thickness(0, 6, 0, 6),
113+
InlineCodeBorderThickness = new Thickness(1),
114+
InlineCodePadding = new Thickness(4, 2, 4, 2)
115+
};
116+
55117
Content = _markdownBlock;
56118
}
119+
120+
private void ApplyCodeBrushes() {
121+
if (CodeBackgroundBrush != null) {
122+
_markdownBlock.CodeBackground = CodeBackgroundBrush;
123+
_markdownBlock.InlineCodeBackground = CodeBackgroundBrush;
124+
}
125+
if (CodeForegroundBrush != null) {
126+
_markdownBlock.CodeForeground = CodeForegroundBrush;
127+
_markdownBlock.InlineCodeForeground = CodeForegroundBrush;
128+
}
129+
if (CodeBorderBrush != null) {
130+
_markdownBlock.CodeBorderBrush = CodeBorderBrush;
131+
_markdownBlock.InlineCodeBorderBrush = CodeBorderBrush;
132+
}
133+
}
57134
}
58135
#endif
59136
}

PilotAIAssistantControl/PilotAIAssistantControl.projitems

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
<Compile Include="$(MSBuildThisFileDirectory)AIUserConfig.cs" />
1616
<Compile Include="$(MSBuildThisFileDirectory)BaseBoolNullConverter.cs" />
1717
<Compile Include="$(MSBuildThisFileDirectory)ChatItem.cs" />
18+
<Compile Include="$(MSBuildThisFileDirectory)ChatItemToBubbleStyleConverter.cs" />
1819
<Compile Include="$(MSBuildThisFileDirectory)CodeBlock.cs" />
1920
<Compile Include="$(MSBuildThisFileDirectory)CopilotTokenHelper.cs" />
2021
<Compile Include="$(MSBuildThisFileDirectory)GithubCopilotProvider.cs" />

PilotAIAssistantControlWPF/UCAI.xaml

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,17 @@
2626
<Style x:Key="StatusText" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
2727
<Setter Property="TextWrapping" Value="Wrap"/>
2828
</Style>
29+
<Style x:Key="StatusConnected" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
30+
<Setter Property="FontWeight" Value="Bold"/>
31+
<Setter Property="Foreground" Value="{DynamicResource AccentTextFillColorPrimaryBrush}"/>
32+
</Style>
33+
<Style x:Key="StatusDisconnected" TargetType="TextBlock" BasedOn="{StaticResource BodyTextBlockStyle}">
34+
<Setter Property="FontWeight" Value="Bold"/>
35+
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}"/>
36+
</Style>
2937
<!-- Chat bubbles need specific colors for context -->
3038
<Style x:Key="ChatBubbleUser" TargetType="Border">
31-
<Setter Property="Background" Value="{DynamicResource AccentFillColorDefaultBrush}"/>
39+
<Setter Property="Background" Value="{DynamicResource TextFillColorInverseBrush}"/>
3240
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}"/>
3341
<Setter Property="Padding" Value="10"/>
3442
<Setter Property="Margin" Value="20,4,4,4"/>
@@ -69,26 +77,31 @@
6977
<Border Grid.Row="0" Style="{StaticResource SectionCard}">
7078
<StackPanel>
7179
<TextBlock Name="TxtCurrentModel" VerticalAlignment="Center"
72-
Foreground="{DynamicResource TextFillColorSecondaryBrush}" FontWeight="Bold"/>
80+
Style="{StaticResource StatusDisconnected}"/>
7381
<StackPanel Orientation="Horizontal">
7482
<Button Content="🗑 Clear" Click="ClearChat_Click"
7583
ToolTip="Clear conversation history"/>
76-
<Button Content="⚙ Settings" Click="GoToSettings_Click"/>
7784
</StackPanel>
7885
</StackPanel>
7986
</Border>
8087

8188
<!-- Chat Messages Area with Markdown Support -->
8289
<ScrollViewer Grid.Row="1" Name="ChatScrollViewer" VerticalScrollBarVisibility="Auto"
83-
Background="{DynamicResource LayerFillColorDefaultBrush}">
90+
Background="{DynamicResource LayerFillColorDefaultBrush}"
91+
Padding="2">
8492
<ItemsControl Name="ChatMessages" ItemsSource="{Binding Messages}">
8593
<ItemsControl.ItemTemplate>
8694
<DataTemplate>
8795
<Border Background="{Binding BackgroundColor}"
96+
BorderBrush="{DynamicResource CardStrokeColorDefaultBrush}"
97+
BorderThickness="1"
98+
CornerRadius="12"
99+
Padding="12,10"
100+
Margin="6,4"
88101
HorizontalAlignment="{Binding Alignment}"
89-
MaxWidth="500">
102+
MaxWidth="560">
90103
<StackPanel>
91-
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
104+
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,0,6">
92105
<TextBlock Text="{Binding Sender}" FontWeight="SemiBold" VerticalAlignment="Center"
93106
Style="{StaticResource CaptionTextBlockStyle}"
94107
Foreground="{Binding SenderColor}"/>

0 commit comments

Comments
 (0)