Skip to content

Commit 4b702dc

Browse files
bkudiessCopilot
andcommitted
fix(theme): restore high contrast navigation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
1 parent 7be4403 commit 4b702dc

5 files changed

Lines changed: 238 additions & 8 deletions

File tree

src/OpenClaw.Tray.WinUI/App.xaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemFillColorCritical}" Opacity="0.14" />
4040
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemAccentColorDark2}" />
4141
<x:Double x:Key="ChatAccessibleBorderThickness">1</x:Double>
42+
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">False</x:Boolean>
4243

4344
<!-- Composer scrim: the chat timeline dissolves into the composer dock via a
4445
vertical fade from transparent to the base surface fill. FunctionalUI
@@ -67,6 +68,7 @@
6768
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemFillColorCritical}" Opacity="0.14" />
6869
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemAccentColorDark2}" />
6970
<x:Double x:Key="ChatAccessibleBorderThickness">1</x:Double>
71+
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">False</x:Boolean>
7072

7173
<!-- Light mirror of ChatComposerFadeBrush (SolidBackgroundFillColorBase light #F3F3F3). -->
7274
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">
@@ -89,6 +91,7 @@
8991
<SolidColorBrush x:Key="ConnectionCapabilityPillCriticalBrush" Color="{ThemeResource SystemColorWindowColor}" />
9092
<SolidColorBrush x:Key="ChatUserBubbleSelectionHighlightBrush" Color="{ThemeResource SystemColorHighlightColor}" />
9193
<x:Double x:Key="ChatAccessibleBorderThickness">2</x:Double>
94+
<x:Boolean x:Key="HubNavigationUseHighContrastIcons">True</x:Boolean>
9295

9396
<!-- High-contrast follows the system window color so the scrim stays legible. -->
9497
<LinearGradientBrush x:Key="ChatComposerFadeBrush" StartPoint="0,0" EndPoint="0,1">

src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ public HubWindow()
9898
InitializeComponent();
9999
Title = AppIdentity.DisplayName;
100100
RefreshDiagnosticsNavVisibility();
101+
ApplyHighContrastFallbackIfNeeded();
101102
ExtendsContentIntoTitleBar = true;
102103
SetTitleBar(AppTitleBar);
103104
Closed += (s, e) =>
@@ -1393,11 +1394,90 @@ private void ExecuteCommand(CommandItem cmd)
13931394
}
13941395
}
13951396

1397+
#region High Contrast icon fallback
1398+
1399+
// Fixed-color SVGs provide the normal Hub presentation. In High Contrast,
1400+
// App.xaml resolves this path to system-foreground Fluent glyphs without
1401+
// constructing or subscribing to the legacy WinRT accessibility API.
1402+
private static readonly Dictionary<string, string> s_highContrastGlyphFallback = new()
1403+
{
1404+
{ "chat", "\uE8BD" },
1405+
{ "connection", "\uE839" },
1406+
{ "sessions", "\uE8F2" },
1407+
{ "skills", "\uE945" },
1408+
{ "channels", "\uEC05" },
1409+
{ "instances", "\uE977" },
1410+
{ "agentevents", "\uE943" },
1411+
{ "bindings", "\uE8AD" },
1412+
{ "config", "\uE90F" },
1413+
{ "usage", "\uE9D9" },
1414+
{ "cron", "\uE787" },
1415+
{ "voice", "\uE720" },
1416+
{ "settings", "\uE713" },
1417+
{ "permissions", "\uEA18" },
1418+
{ "sandbox", "\uE72E" },
1419+
{ "activity", "\uEA95" },
1420+
{ "notifications", "\uE7F4" },
1421+
{ "debug", "\uEBE8" },
1422+
};
1423+
1424+
private const string AdvancedGroupGlyph = "\uE950";
1425+
private const string AgentsGroupGlyph = "\uE99A";
1426+
private bool _isHighContrast;
1427+
1428+
private void ApplyHighContrastFallbackIfNeeded()
1429+
{
1430+
const string resourceKey = "HubNavigationUseHighContrastIcons";
1431+
_isHighContrast = Application.Current.Resources.ContainsKey(resourceKey)
1432+
&& Application.Current.Resources[resourceKey] is true;
1433+
if (!_isHighContrast)
1434+
return;
1435+
1436+
SwapToFontIcons(NavView.MenuItems);
1437+
SwapToFontIcons(NavView.FooterMenuItems);
1438+
}
1439+
1440+
private void SwapToFontIcons(IList<object> items)
1441+
{
1442+
foreach (var value in items)
1443+
{
1444+
if (value is not NavigationViewItem item)
1445+
continue;
1446+
1447+
item.Icon = ResolveHighContrastIcon(item);
1448+
if (item.MenuItems.Count > 0)
1449+
SwapToFontIcons(item.MenuItems);
1450+
}
1451+
}
1452+
1453+
private IconElement ResolveHighContrastIcon(NavigationViewItem item)
1454+
{
1455+
if (item.Tag is string tag)
1456+
{
1457+
if (s_highContrastGlyphFallback.TryGetValue(tag, out var glyph))
1458+
return FluentIconCatalog.Build(glyph, 20);
1459+
if (tag.StartsWith("agent:", StringComparison.Ordinal))
1460+
return FluentIconCatalog.Build(AgentsGroupGlyph, 20);
1461+
}
1462+
1463+
if (item == AgentsNavItem)
1464+
return FluentIconCatalog.Build(AgentsGroupGlyph, 20);
1465+
if (item == NavAdvanced)
1466+
return FluentIconCatalog.Build(AdvancedGroupGlyph, 20);
1467+
1468+
return FluentIconCatalog.Build("\uE700", 20);
1469+
}
1470+
13961471
private IconElement BuildAgentItemIcon()
13971472
{
1473+
if (_isHighContrast)
1474+
return FluentIconCatalog.Build(AgentsGroupGlyph, 20);
1475+
13981476
return new ImageIcon
13991477
{
14001478
Source = (ImageSource)NavView.Resources["Agents_Icon"]
14011479
};
14021480
}
1481+
1482+
#endregion
14031483
}

tests/OpenClaw.Tray.Tests/AccessibilityThemeResourceSourceTests.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public void TrayThemeChanges_AreOwnedByXamlResourcesInsteadOfAccessibilitySettin
1616
Assert.DoesNotContain("HighContrastChanged", connection);
1717
Assert.DoesNotContain("TrySubscribeAccessibilitySettings", connection);
1818
Assert.DoesNotContain("AccessibilitySettings", hub);
19-
Assert.DoesNotContain("ApplyHighContrastFallbackIfNeeded", hub);
19+
Assert.DoesNotContain("HighContrastChanged", hub);
2020
Assert.DoesNotContain("AccessibilitySettings", timeline);
2121
Assert.DoesNotContain("TryDetectHighContrast", timeline);
2222

@@ -27,6 +27,13 @@ public void TrayThemeChanges_AreOwnedByXamlResourcesInsteadOfAccessibilitySettin
2727
Assert.Contains("ImageIcon Source=\"{StaticResource Chat_Icon}\"", hubXaml);
2828
Assert.Contains("new ImageIcon", hub);
2929
Assert.Contains("NavView.Resources[\"Agents_Icon\"]", hub);
30+
Assert.Contains("ApplyHighContrastFallbackIfNeeded", hub);
31+
Assert.Contains("HubNavigationUseHighContrastIcons", hub);
32+
Assert.Contains("SwapToFontIcons", hub);
33+
Assert.Contains("FluentIconCatalog.Build", hub);
34+
Assert.Contains("item == NavAdvanced", hub);
35+
Assert.DoesNotContain("content.Equals(\"Advanced\"", hub);
36+
Assert.Contains("return FluentIconCatalog.Build(\"\\uE700\", 20);", hub);
3037
Assert.DoesNotContain("<IconSourceElement", hubXaml);
3138
Assert.DoesNotContain("FontIconSource", hubXaml);
3239
var stateIconBlock = connection[
@@ -40,6 +47,7 @@ public void TrayThemeChanges_AreOwnedByXamlResourcesInsteadOfAccessibilitySettin
4047
Assert.Contains("ChatUserBubbleSelectionHighlightBrush", resources);
4148
Assert.Contains("SystemColorHighlightColor", resources);
4249
Assert.Contains("<x:Double x:Key=\"ChatAccessibleBorderThickness\">2</x:Double>", resources);
50+
Assert.Contains("<x:Boolean x:Key=\"HubNavigationUseHighContrastIcons\">True</x:Boolean>", resources);
4351
}
4452

4553
private static string ReadSource(params string[] relativePathParts)
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
using Microsoft.UI.Xaml;
2+
using Microsoft.UI.Xaml.Controls;
3+
using OpenClaw.Chat;
4+
using OpenClawTray.Chat;
5+
using OpenClawTray.FunctionalUI.Hosting;
6+
using static OpenClawTray.FunctionalUI.Factories;
7+
using static OpenClaw.Tray.UITests.TestSupport;
8+
9+
namespace OpenClaw.Tray.UITests;
10+
11+
[Collection(UICollection.Name)]
12+
public sealed class ChatTimelineThemeResourceProofTests
13+
{
14+
private readonly UIThreadFixture _ui;
15+
16+
public ChatTimelineThemeResourceProofTests(UIThreadFixture ui) => _ui = ui;
17+
18+
[Fact]
19+
public async Task ToolBurst_RendersWithTestHostBorderStyle()
20+
{
21+
var props = BuildProps(
22+
[
23+
new ChatTimelineItem("user-1", ChatTimelineItemKind.User, "Check status"),
24+
new ChatTimelineItem("assistant-1", ChatTimelineItemKind.Assistant, "Checking."),
25+
new ChatTimelineItem(
26+
"tool-1",
27+
ChatTimelineItemKind.ToolCall,
28+
"{}",
29+
ToolName: "session_status",
30+
ToolResult: ChatToolCallStatus.Success,
31+
ToolOutput: "ok"),
32+
]);
33+
34+
var host = await MountAsync(props);
35+
36+
await _ui.RunOnUIAsync(() =>
37+
{
38+
var style = Assert.IsType<Style>(
39+
Application.Current.Resources["ChatToolCardBorderStyle"]);
40+
Assert.Equal(typeof(Border), style.TargetType);
41+
Assert.Contains(
42+
FindDescendants<Border>(host),
43+
border => ReferenceEquals(border.Style, style));
44+
host.Dispose();
45+
});
46+
}
47+
48+
[Fact]
49+
public async Task CompactionEntry_RendersWithTestHostBorderStyle()
50+
{
51+
const string entryId = "compaction-1";
52+
var metadata = new Dictionary<string, ChatEntryMetadata>(StringComparer.Ordinal)
53+
{
54+
[entryId] = new(
55+
Timestamp: null,
56+
Model: null,
57+
OpenClawKind: "compaction",
58+
CompactionTokensBefore: 42_000,
59+
CompactionTokensAfter: 12_000),
60+
};
61+
var props = BuildProps(
62+
[new ChatTimelineItem(entryId, ChatTimelineItemKind.Status, "Context compacted")],
63+
metadata);
64+
65+
var host = await MountAsync(props);
66+
67+
await _ui.RunOnUIAsync(() =>
68+
{
69+
var style = Assert.IsType<Style>(
70+
Application.Current.Resources["ChatCompactionCardStyle"]);
71+
Assert.Equal(typeof(Border), style.TargetType);
72+
Assert.Contains(
73+
FindDescendants<Border>(host),
74+
border => ReferenceEquals(border.Style, style));
75+
host.Dispose();
76+
});
77+
}
78+
79+
private async Task<FunctionalHostControl> MountAsync(OpenClawChatTimelineProps props)
80+
{
81+
await _ui.ResetContainerAsync();
82+
83+
FunctionalHostControl? host = null;
84+
await _ui.RunOnUIAsync(() =>
85+
{
86+
TestApp.EnsureFluentBrushFallbacks(Application.Current.Resources);
87+
host = new FunctionalHostControl
88+
{
89+
Width = 860,
90+
Height = 560,
91+
SuppressAutoDispose = true,
92+
};
93+
_ui.Container.Children.Add(host);
94+
host.Mount(_ => Component<OpenClawChatTimeline, OpenClawChatTimelineProps>(props));
95+
});
96+
97+
for (var pass = 0; pass < 4; pass++)
98+
{
99+
await _ui.RunOnUIAsync(() => _ui.Container.UpdateLayout());
100+
await _ui.YieldToRenderAsync();
101+
await Task.Delay(40);
102+
}
103+
104+
return host!;
105+
}
106+
107+
private static OpenClawChatTimelineProps BuildProps(
108+
IReadOnlyList<ChatTimelineItem> entries,
109+
IReadOnlyDictionary<string, ChatEntryMetadata>? metadata = null) =>
110+
new(
111+
SessionId: "theme-resource-proof",
112+
Entries: entries,
113+
HasMoreHistory: false,
114+
OnLoadMoreHistory: null,
115+
EntryMetadata: metadata,
116+
ShowToolCalls: true);
117+
}

tests/OpenClaw.Tray.UITests/TestApp.cs

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,6 @@ public void MergeStandardResources()
7777
"<Setter Property='Foreground' Value='White' />" +
7878
"<Setter Property='CornerRadius' Value='4' />" +
7979
"</Style>");
80-
AddChatUserBubbleSelectionStyle(resources);
81-
8280
EnsureFluentBrushFallbacks(resources);
8381
}
8482

@@ -89,7 +87,7 @@ internal static void EnsureFluentBrushFallbacks(ResourceDictionary resources)
8987
TryAddBrushResource(resources, key, color);
9088
}
9189

92-
AddChatUserBubbleSelectionStyle(resources);
90+
AddChatTimelineStyles(resources);
9391
}
9492

9593
private bool TryGetResources(out ResourceDictionary resources)
@@ -138,14 +136,38 @@ private static void TryAddBrushResource(ResourceDictionary resources, string key
138136
}
139137
}
140138

141-
private static void AddChatUserBubbleSelectionStyle(ResourceDictionary resources)
139+
private static void AddChatTimelineStyles(ResourceDictionary resources)
140+
{
141+
if (!resources.ContainsKey("ChatUserBubbleSelectionStyle"))
142+
{
143+
resources["ChatUserBubbleSelectionStyle"] = new Style
144+
{
145+
TargetType = typeof(RichTextBlock),
146+
};
147+
}
148+
149+
AddChatBorderStyle(resources, "ChatToolCardBorderStyle");
150+
AddChatBorderStyle(resources, "ChatCompactionCardStyle");
151+
}
152+
153+
private static void AddChatBorderStyle(ResourceDictionary resources, string key)
142154
{
143-
if (resources.ContainsKey("ChatUserBubbleSelectionStyle"))
155+
if (resources.ContainsKey(key))
144156
return;
145157

146-
resources["ChatUserBubbleSelectionStyle"] = new Style
158+
var style = new Style
147159
{
148-
TargetType = typeof(RichTextBlock),
160+
TargetType = typeof(Border),
149161
};
162+
style.Setters.Add(new Setter(
163+
Border.BackgroundProperty,
164+
resources["CardBackgroundFillColorDefaultBrush"]));
165+
style.Setters.Add(new Setter(
166+
Border.BorderBrushProperty,
167+
resources["ControlStrokeColorDefaultBrush"]));
168+
style.Setters.Add(new Setter(
169+
Border.BorderThicknessProperty,
170+
new Thickness(1)));
171+
resources[key] = style;
150172
}
151173
}

0 commit comments

Comments
 (0)