diff --git a/src/OpenClaw.Tray.WinUI/App.xaml b/src/OpenClaw.Tray.WinUI/App.xaml
index f0fa5ca03..97ec85d3d 100644
--- a/src/OpenClaw.Tray.WinUI/App.xaml
+++ b/src/OpenClaw.Tray.WinUI/App.xaml
@@ -34,6 +34,12 @@
+
+
+
+
+ 1
+ False
@@ -74,6 +86,12 @@
+
+
+
+
+ 2
+ True
@@ -83,6 +101,20 @@
+
+
+
+
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs
index 4496d3cc9..3ec90caf1 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawChatTimeline.cs
@@ -207,121 +207,6 @@ private static FontFamily s_chatTextFontFamily
}
}
- // Per-DispatcherQueue selection-highlight brushes for the user
- // bubble. The bubble background is the user's chosen system accent
- // (which may be red, green, purple, …), so a hardcoded color would
- // clash whenever the accent is non-blue. SystemAccentColorDark2 is
- // the OS-defined "darker shade of the current accent" — guaranteed
- // darker than the bubble's AccentFillColorDefault background and
- // high-contrast against the bubble's white foreground for every
- // accent. In High Contrast the bubble switches to
- // SystemColorHighlight (often near-black), so we fall back to the
- // OS-guaranteed SystemColorHighlightColor for the band there.
- //
- // SolidColorBrush is a DependencyObject with thread affinity, so a
- // single static instance would crash with RPC_E_WRONG_THREAD if a
- // second window on a different dispatcher ever tried to use it.
- // Keying by DispatcherQueue keeps one shared brush per window while
- // still avoiding per-render allocation. ConditionalWeakTable lets a
- // closing window's brush be collected with its dispatcher. The
- // brush's Color is mutated in place when the source color changes
- // (e.g. user switches their accent in Windows Settings) so
- // already-rendered TextBlocks update atomically.
- private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<
- Microsoft.UI.Dispatching.DispatcherQueue, SolidColorBrush> s_accentDarkByDispatcher = new();
- private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<
- Microsoft.UI.Dispatching.DispatcherQueue, SolidColorBrush> s_hcHighlightByDispatcher = new();
- // AccessibilitySettings is a WinRT object with DispatcherQueue
- // affinity: an instance created on one dispatcher cannot reliably
- // be read from another. We deliberately avoid Lazy<>: Lazy
- // permanently caches the factory's result, so a single failed
- // construction would cache null forever and silently disable the
- // High Contrast code path. Per-dispatcher cache keyed by
- // ConditionalWeakTable lets each window have its own instance,
- // collected when its dispatcher dies. On any thrown exception we
- // drop the cached instance so the next render retries from scratch.
- private static readonly System.Runtime.CompilerServices.ConditionalWeakTable<
- Microsoft.UI.Dispatching.DispatcherQueue,
- global::Windows.UI.ViewManagement.AccessibilitySettings> s_a11yByDispatcher = new();
-
- private static bool TryDetectHighContrast()
- {
- var dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
- if (dispatcher is null)
- {
- // Off-thread caller (tests, design-time). One-shot, no caching.
- try { return new global::Windows.UI.ViewManagement.AccessibilitySettings().HighContrast; }
- catch { return false; }
- }
- if (!s_a11yByDispatcher.TryGetValue(dispatcher, out var settings))
- {
- try
- {
- settings = new global::Windows.UI.ViewManagement.AccessibilitySettings();
- s_a11yByDispatcher.Add(dispatcher, settings);
- }
- catch { return false; }
- }
- try { return settings.HighContrast; }
- catch
- {
- // Drop the cached instance so the next call retries.
- s_a11yByDispatcher.Remove(dispatcher);
- return false;
- }
- }
-
- private static SolidColorBrush GetUserBubbleSelectionBrush(bool isHighContrast)
- {
- var dispatcher = Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread();
- var table = isHighContrast ? s_hcHighlightByDispatcher : s_accentDarkByDispatcher;
- var color = isHighContrast
- ? TryGetThemeColor("SystemColorHighlightColor", Microsoft.UI.Colors.Blue)
- : TryGetThemeColor("SystemAccentColorDark2", Microsoft.UI.Colors.DarkBlue);
-
- // No dispatcher means we're being called off-thread (e.g.
- // from a unit test). Allocate a one-shot brush — it can't be
- // safely cached without a dispatcher to key it on.
- if (dispatcher is null)
- return new SolidColorBrush(color);
-
- if (!table.TryGetValue(dispatcher, out var brush))
- {
- brush = new SolidColorBrush(color);
- table.Add(dispatcher, brush);
- }
- else if (brush.Color != color)
- {
- // Mutate in place rather than reallocating: TextBlocks
- // rendered earlier hold a reference to this brush, so
- // updating .Color updates them atomically without waiting
- // for the next render pass.
- brush.Color = color;
- }
- return brush;
- }
-
- private static Color TryGetThemeColor(string key, Color fallback)
- {
- try
- {
- var app = Application.Current;
- if (app is null) return fallback;
- if (app.Resources.TryGetValue(key, out var v))
- {
- // Theme dictionaries usually store Color, but a custom
- // theme override can supply a SolidColorBrush under the
- // same key. Accept either rather than silently falling
- // back to DarkBlue / Blue when the resource is present
- // but wrapped in a brush.
- if (v is Color c) return c;
- if (v is SolidColorBrush brush) return brush.Color;
- }
- }
- catch (Exception ex) { OpenClawTray.Services.Logger.Debug($"ChatTimeline: resource brush lookup failed (unpackaged/test host?): {ex.Message}"); }
- return fallback;
- }
-
private static void ApplyPlainSelectableInlines(TextBlock textBlock, string? text)
{
var normalized = text ?? string.Empty;
@@ -1020,12 +905,7 @@ static Element TimelineInset(Element child, double top = 2, double bottom = 2) =
// the bubble surface below is opaque (Mica/acrylic isn't being
// used directly), so the LayerOnAcrylic family would render
// incorrectly in dark/HC themes.
- var toolCardBgBrush = themeBrush("CardBackgroundFillColorDefaultBrush");
var toolCardBorderBrush = themeBrush("ControlStrokeColorDefaultBrush");
- // High-contrast themes need a thicker border to render at all
- // (WinUI guidance: 2px minimum). Detect once at render time so the
- // tool card border stays visible when HC is on, normal 1px otherwise.
- double toolCardBorderThickness = TryDetectHighContrast() ? 2 : 1;
// Avatar: 36×36 circle (Kenny uses circular avatars). Same constructor
// as before but radius defaults to half the size for a perfect circle.
@@ -1409,12 +1289,6 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst
var entryMeta = MetaFor(entry.Id);
if (hasMessage)
{
- // Resolve HC + selection brush once per render method call
- // rather than per Set-lambda re-run. HC state cannot change
- // mid-render, and the brush is cached per-dispatcher so
- // every user bubble in this render shares the same instance.
- bool isHighContrast = TryDetectHighContrast();
- var selectionHighlightBrush = GetUserBubbleSelectionBrush(isHighContrast);
bubbleChildren.Add(
RichTextBlock()
.Set(t =>
@@ -1431,23 +1305,7 @@ Element RenderUserEntry(ChatTimelineItem entry, bool startsBurst, bool endsBurst
t.Width = double.NaN;
t.MinWidth = 0;
t.MaxWidth = double.PositiveInfinity;
- // The default SelectionHighlightColor is the
- // system accent — which equals the user bubble's
- // background — so the highlight band is invisible
- // against the bubble, and WinUI does NOT auto-
- // invert an explicitly-set Foreground for
- // selected glyphs. Outside High Contrast, use a
- // darker shade of the current accent
- // (SystemAccentColorDark2) so the band tracks
- // whichever accent the user picked while keeping
- // the white foreground readable. In High Contrast
- // the bubble background switches to
- // SystemColorHighlight (often near-black), where
- // an accent-derived band may drop below WCAG
- // 3:1, so fall back to the system selection
- // color the OS guarantees contrasts with both
- // surfaces.
- t.SelectionHighlightColor = selectionHighlightBrush;
+ t.Style = (Style)Application.Current.Resources["ChatUserBubbleSelectionStyle"];
// Render the message as a single Paragraph (one Run)
// so the whole user message is one continuous
// selection scope — matching the assistant bubble's
@@ -1974,10 +1832,9 @@ Element PhantomChevron() => Caption("▸")
ToolName: null, ToolResult: aggregateStatus, ToolOutput: null));
Element CardOf(Element[] rowEls) => Border(VStack(0, rowEls))
- .Background(toolCardBgBrush)
- .WithBorder(toolCardBorderBrush, toolCardBorderThickness)
.Set(b =>
{
+ b.Style = (Style)Application.Current.Resources["ChatToolCardBorderStyle"];
// CornerRadius is uniform across the card; setting it
// directly works because rounding nests under the
// Border's BorderThickness.
@@ -2349,8 +2206,6 @@ Element RenderCompactionEntry(ChatTimelineItem entry)
LocalizationHelper.GetString("Chat_Compaction_Title"),
LocalizationHelper.GetString("Chat_Compaction_MetricsFormat"),
LocalizationHelper.GetString("Chat_Compaction_FallbackDetail"));
- var borderThickness = TryDetectHighContrast() ? 2 : 1;
-
return TimelineInset(
Border(
VStack(3,
@@ -2368,8 +2223,7 @@ Element RenderCompactionEntry(ChatTimelineItem entry)
t.TextAlignment = TextAlignment.Center;
}).Foreground(themeBrush("TextFillColorSecondaryBrush"))
)
- ).Background(themeBrush("CardBackgroundFillColorDefaultBrush"))
- .WithBorder(themeBrush("ControlStrokeColorDefaultBrush"), borderThickness)
+ ).Set(b => b.Style = (Style)Application.Current.Resources["ChatCompactionCardStyle"])
.CornerRadius(8)
.Padding(16, 10, 16, 10)
.HAlign(HorizontalAlignment.Stretch)
diff --git a/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs b/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
index 06fbb94a4..0d7eed6a7 100644
--- a/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
+++ b/src/OpenClaw.Tray.WinUI/Chat/OpenClawReactorChatRoot.cs
@@ -1,6 +1,7 @@
using Microsoft.UI;
using Microsoft.UI.Reactor;
using Microsoft.UI.Reactor.Core;
+using Microsoft.UI.Reactor.Hosting;
using Microsoft.UI.Reactor.Input;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
@@ -582,184 +583,11 @@ public sealed record ReactorChatComposerProps(
public sealed class ReactorChatComposer : Component
{
private static readonly string[] ThinkingLevels = ["off", "minimal", "low", "medium", "high"];
- private static readonly ConditionalWeakTable ThemeCallbacks = new();
- private static readonly global::Windows.UI.ViewManagement.AccessibilitySettings? AccessibilitySettings =
- CreateAccessibilitySettings();
-
- private sealed class ThemeCallbackState(Action apply)
- {
- public Action Apply { get; set; } = apply;
- public global::Windows.Foundation.TypedEventHandler<
- global::Windows.UI.ViewManagement.AccessibilitySettings,
- object>? HighContrastChanged { get; set; }
- public bool HighContrastEventUnavailable { get; set; }
- }
-
- private static void ApplyTheme(FrameworkElement control, Action apply)
- {
- apply();
- if (ThemeCallbacks.TryGetValue(control, out var state))
- {
- state.Apply = apply;
- EnsureHighContrastCallback(control, state);
- return;
- }
-
- state = new ThemeCallbackState(apply);
- ThemeCallbacks.Add(control, state);
- control.ActualThemeChanged += static (sender, _) =>
- {
- if (sender is FrameworkElement element
- && ThemeCallbacks.TryGetValue(element, out var callback))
- callback.Apply();
- };
- control.Loaded += static (sender, _) =>
- {
- if (sender is FrameworkElement element
- && ThemeCallbacks.TryGetValue(element, out var callback))
- {
- callback.Apply();
- EnsureHighContrastCallback(element, callback);
- }
- };
- control.Unloaded += static (sender, _) =>
- {
- if (sender is FrameworkElement element
- && ThemeCallbacks.TryGetValue(element, out var callback)
- && callback.HighContrastChanged is { } handler
- && AccessibilitySettings is { } accessibilitySettings)
- {
- try
- {
- accessibilitySettings.HighContrastChanged -= handler;
- }
- catch (System.Runtime.InteropServices.COMException)
- {
- // The optional WinRT event source can be unavailable while a view tears down.
- }
- callback.HighContrastChanged = null;
- }
- };
- EnsureHighContrastCallback(control, state);
- }
-
- private static global::Windows.UI.ViewManagement.AccessibilitySettings? CreateAccessibilitySettings()
- {
- try
- {
- return new global::Windows.UI.ViewManagement.AccessibilitySettings();
- }
- catch
- {
- return null;
- }
- }
-
- private static void EnsureHighContrastCallback(
- FrameworkElement control,
- ThemeCallbackState state)
- {
- if (AccessibilitySettings is null
- || state.HighContrastChanged is not null
- || state.HighContrastEventUnavailable)
- return;
-
- global::Windows.Foundation.TypedEventHandler<
- global::Windows.UI.ViewManagement.AccessibilitySettings,
- object> handler = (_, _) =>
- {
- control.DispatcherQueue?.TryEnqueue(() =>
- {
- if (ThemeCallbacks.TryGetValue(control, out var callback))
- callback.Apply();
- });
- };
- try
- {
- AccessibilitySettings.HighContrastChanged += handler;
- state.HighContrastChanged = handler;
- }
- catch (System.Runtime.InteropServices.COMException ex)
- {
- state.HighContrastEventUnavailable = true;
- OpenClawTray.Services.Logger.Warn(
- $"[ReactorChatComposer] High Contrast change notifications are unavailable: {ex.Message}");
- }
- }
-
- private static Brush ResolveThemeBrush(string resourceKey, ElementTheme theme)
- {
- if (FindThemedResource(resourceKey, theme) is Brush themed)
- return themed;
- if (Application.Current?.Resources.TryGetValue(resourceKey, out var value) == true
- && value is Brush brush)
- return brush;
- return new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- }
-
- private static object? FindThemedResource(string resourceKey, ElementTheme theme)
- {
- if (Application.Current?.Resources is not { } root)
- return null;
-
- var themeNames = IsHighContrast()
- ? new[] { "HighContrast" }
- : theme switch
- {
- ElementTheme.Dark => ["Dark", "Default"],
- ElementTheme.Light => ["Light"],
- _ => Array.Empty(),
- };
- return themeNames
- .Select(themeName => SearchThemeDictionaries(root, resourceKey, themeName, 0))
- .FirstOrDefault(value => value is not null);
- }
-
- private static bool IsHighContrast()
- {
- return AccessibilitySettings?.HighContrast ?? false;
- }
-
- private static object? SearchThemeDictionaries(
- ResourceDictionary dictionary,
- string resourceKey,
- string themeName,
- int depth)
- {
- if (depth > 6)
- return null;
-
- if (dictionary.ThemeDictionaries.TryGetValue(themeName, out var entry)
- && entry is ResourceDictionary themed
- && LookupResource(themed, resourceKey) is { } value)
- return value;
-
- foreach (var merged in dictionary.MergedDictionaries)
- {
- if (SearchThemeDictionaries(merged, resourceKey, themeName, depth + 1) is { } found)
- return found;
- }
-
- return null;
- }
-
- private static object? LookupResource(ResourceDictionary dictionary, string resourceKey)
- {
- if (dictionary.TryGetValue(resourceKey, out var value))
- return value;
-
- foreach (var merged in dictionary.MergedDictionaries)
- {
- if (LookupResource(merged, resourceKey) is { } found)
- return found;
- }
-
- return null;
- }
public override Element Render()
{
var props = Props;
+ var colorScheme = UseColorScheme();
var (text, setText) = UseState(string.Empty, threadSafe: true);
var (isSending, setIsSending) = UseState(false, threadSafe: true);
var (isRecording, setIsRecording) = UseState(false, threadSafe: true);
@@ -903,26 +731,6 @@ void Send()
: Localized("Chat_Composer_Tooltip_Send", "Send");
var controlCornerRadius = new CornerRadius(4);
- void ApplySubtleButtonStyle(Button button)
- {
- var transparent = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- button.Background = transparent;
- button.BorderBrush = transparent;
- button.BorderThickness = new Thickness(0);
- button.Resources["ButtonBackground"] = transparent;
- button.Resources["ButtonBorderBrush"] = transparent;
- button.Resources["ButtonBorderBrushPointerOver"] = transparent;
- button.Resources["ButtonBorderBrushPressed"] = transparent;
- ApplyTheme(button, () =>
- {
- button.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", button.ActualTheme);
- button.Resources["ButtonBackgroundPointerOver"] =
- ResolveThemeBrush("SubtleFillColorSecondaryBrush", button.ActualTheme);
- button.Resources["ButtonBackgroundPressed"] =
- ResolveThemeBrush("SubtleFillColorTertiaryBrush", button.ActualTheme);
- });
- }
-
Element IconButton(string glyph, string automationName, Action onClick, bool enabled = true)
{
return Button(
@@ -933,6 +741,14 @@ Element IconButton(string glyph, string automationName, Action onClick, bool ena
}),
onClick)
.AutomationName(automationName)
+ .Foreground(Theme.SecondaryText)
+ .Resources(resources => resources
+ .Set("ButtonBackground", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBackgroundPointerOver", Theme.SubtleFill)
+ .Set("ButtonBackgroundPressed", Theme.Ref("SubtleFillColorTertiaryBrush"))
+ .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBorderBrushPointerOver", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBorderBrushPressed", Theme.Ref("SubtleFillColorTransparentBrush")))
.Set(button =>
{
button.Width = 32;
@@ -942,7 +758,7 @@ Element IconButton(string glyph, string automationName, Action onClick, bool ena
button.Padding = new Thickness(0);
button.CornerRadius = controlCornerRadius;
button.IsEnabled = enabled;
- ApplySubtleButtonStyle(button);
+ button.BorderThickness = new Thickness(0);
ToolTipService.SetToolTip(button, automationName);
});
}
@@ -967,6 +783,14 @@ Element PickerButton(string label, string automationName, bool enabled, double m
})),
() => { })
.AutomationName(automationName)
+ .Foreground(Theme.SecondaryText)
+ .Resources(resources => resources
+ .Set("ButtonBackground", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBackgroundPointerOver", Theme.SubtleFill)
+ .Set("ButtonBackgroundPressed", Theme.Ref("SubtleFillColorTertiaryBrush"))
+ .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBorderBrushPointerOver", Theme.Ref("SubtleFillColorTransparentBrush"))
+ .Set("ButtonBorderBrushPressed", Theme.Ref("SubtleFillColorTransparentBrush")))
.Set(button =>
{
button.Height = 32;
@@ -975,7 +799,7 @@ Element PickerButton(string label, string automationName, bool enabled, double m
button.Padding = new Thickness(8, 0, 8, 0);
button.CornerRadius = controlCornerRadius;
button.IsEnabled = enabled;
- ApplySubtleButtonStyle(button);
+ button.BorderThickness = new Thickness(0);
});
}
@@ -999,8 +823,7 @@ Element PickerButton(string label, string automationName, bool enabled, double m
.Height(2 + (audioLevel * (index % 3 == 1 ? 10 : 7)))
.CornerRadius(1)
.VAlign(VerticalAlignment.Center)
- .Set(border => ApplyTheme(border, () => border.Background =
- ResolveThemeBrush("TextFillColorSecondaryBrush", border.ActualTheme))))
+ .Background(Theme.SecondaryText))
.ToArray();
Element voiceFeedback = !isRecording
? Empty()
@@ -1011,12 +834,10 @@ Element PickerButton(string label, string automationName, bool enabled, double m
.Width(6)
.Height(6)
.CornerRadius(3)
- .Set(border => ApplyTheme(border, () => border.Background =
- ResolveThemeBrush("TextFillColorSecondaryBrush", border.ActualTheme))),
+ .Background(Theme.SecondaryText),
TextBlock(voiceFeedbackText)
.FontSize(11)
- .Set(textBlock => ApplyTheme(textBlock, () => textBlock.Foreground =
- ResolveThemeBrush("TextFillColorSecondaryBrush", textBlock.ActualTheme))),
+ .Foreground(Theme.SecondaryText),
HStack(1, waveformBars)))
.Padding(8, 4)
.HAlign(HorizontalAlignment.Left);
@@ -1135,7 +956,8 @@ void CommitSlashText(string value, ReactorSlashMenuState nextState)
slashDisplay.Query,
slashDisplay.SelectedIndex,
slashDisplay.SelectableCount,
- popupCatalogKey);
+ popupCatalogKey,
+ colorScheme);
FrameworkElement? slashPopupContent;
if (!slashPopupVisible)
{
@@ -1148,27 +970,28 @@ void CommitSlashText(string value, ReactorSlashMenuState nextState)
}
else if (slashDisplay.IsLoading)
{
- slashPopupContent = BuildSlashHintPopup(
- Localized("Chat_Composer_Slash_Loading", "Loading commands..."));
+ slashPopupContent = CreateSlashPopupHost(BuildSlashHintPopup(
+ Localized("Chat_Composer_Slash_Loading", "Loading commands...")));
slashPopupContentRef.Current = (popupStateKey, slashPopupContent);
}
else if (slashDisplay.IsArgsMode && slashDisplay.ArgCommand is { } argCommand)
{
- slashPopupContent = BuildSlashArgPopup(
+ slashPopupContent = CreateSlashPopupHost(BuildSlashArgPopup(
argCommand,
slashDisplay.ArgChoices,
slashDisplay.SelectedIndex,
choice => CommitSlashText(
argCommand.BuildArgInsertionText(choice.Value),
- ReactorSlashMenuState.Closed));
+ ReactorSlashMenuState.Closed)));
slashPopupContentRef.Current = (popupStateKey, slashPopupContent);
}
else
{
- slashPopupContent = BuildSlashPopup(
+ slashPopupContent = CreateSlashPopupHost(BuildSlashPopup(
slashDisplay.Groups,
slashDisplay.SelectedIndex,
slashDisplay.Query,
+ colorScheme,
command =>
{
CommitSlashText(
@@ -1176,7 +999,7 @@ void CommitSlashText(string value, ReactorSlashMenuState nextState)
command.FirstArgChoices().Count > 0
? new ReactorSlashMenuState(true, string.Empty, 0, true)
: ReactorSlashMenuState.Closed);
- });
+ }));
slashPopupContentRef.Current = (popupStateKey, slashPopupContent);
}
@@ -1466,11 +1289,8 @@ void CommitSlashText(string value, ReactorSlashMenuState nextState)
.BorderThickness(1)
.CornerRadius(8)
.Margin(12)
- .Set(border => ApplyTheme(border, () =>
- {
- border.Background = ResolveThemeBrush("ControlFillColorDefaultBrush", border.ActualTheme);
- border.BorderBrush = ResolveThemeBrush("ControlStrokeColorDefaultBrush", border.ActualTheme);
- }))
+ .Background(Theme.ControlFill)
+ .BorderBrush(Theme.ControlStroke)
.HAlign(HorizontalAlignment.Stretch);
}
@@ -1480,10 +1300,19 @@ private static void CloseSlashPopup(Ref content);
+ return host;
+ }
+
private static void DriveSlashPopup(
Ref popupRef,
TextBox anchor,
@@ -1511,350 +1340,265 @@ private static void DriveSlashPopup(
popup.XamlRoot = anchor.XamlRoot;
popup.PlacementTarget = anchor;
popup.DesiredPlacement = Microsoft.UI.Xaml.Controls.Primitives.PopupPlacementMode.Top;
+ if (popup.Child is ReactorHostControl previousHost
+ && !ReferenceEquals(previousHost, content))
+ previousHost.Dispose();
popup.Child = content;
popup.IsOpen = true;
}
- private static Border BuildSlashHintPopup(string text)
+ private static Element BuildSlashHintPopup(string text)
{
- var label = new TextBlock
- {
- Text = text,
- FontSize = 12,
- Margin = new Thickness(8, 6, 8, 6),
- };
- ApplyTheme(label, () => label.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", label.ActualTheme));
- return SlashShell(label);
+ return SlashShell(
+ TextBlock(text)
+ .FontSize(12)
+ .Foreground(Theme.SecondaryText)
+ .Margin(8, 6, 8, 6));
}
- private static Border BuildSlashPopup(
+ private static Element BuildSlashPopup(
IReadOnlyList groups,
int selectedIndex,
string query,
+ ColorScheme colorScheme,
Action onPick)
{
- var list = new StackPanel { Orientation = Orientation.Vertical };
+ var rows = new List();
var index = 0;
foreach (var group in groups)
{
- list.Children.Add(SlashCategoryHeader(CommandCategories.Label(group.Category)));
+ rows.Add(SlashCategoryHeader(CommandCategories.Label(group.Category)));
foreach (var command in group.Commands)
{
- list.Children.Add(SlashRow(command, index == selectedIndex, query, onPick));
+ rows.Add(SlashRow(command, index == selectedIndex, query, colorScheme, onPick));
index++;
}
}
- return SlashShell(new ScrollViewer
- {
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
- MaxHeight = 280,
- Content = list,
- });
+ return SlashShell(
+ ScrollView(VStack(0, rows.ToArray()))
+ .MaxHeight(280)
+ .Set(scrollViewer =>
+ {
+ scrollViewer.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Auto;
+ scrollViewer.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden;
+ }));
}
- private static TextBlock SlashCategoryHeader(string text)
+ private static Element SlashCategoryHeader(string text)
{
- var header = new TextBlock
- {
- Text = (text ?? string.Empty).ToUpperInvariant(),
- FontSize = 11,
- FontWeight = Microsoft.UI.Text.FontWeights.Bold,
- CharacterSpacing = 60,
- Margin = new Thickness(8, 8, 8, 2),
- };
- ApplyTheme(header, () => header.Foreground = ResolveThemeBrush("TextFillColorTertiaryBrush", header.ActualTheme));
- return header;
+ return TextBlock((text ?? string.Empty).ToUpperInvariant())
+ .FontSize(11)
+ .SemiBold()
+ .CharacterSpacing(60)
+ .Foreground(Theme.TertiaryText)
+ .Margin(8, 8, 8, 2);
}
- private static Border BuildSlashArgPopup(
+ private static Element BuildSlashArgPopup(
GatewayCommand command,
IReadOnlyList choices,
int selectedIndex,
Action onPick)
{
- var list = new StackPanel { Orientation = Orientation.Vertical };
var argDescription = command.Args?.FirstOrDefault()?.Description;
var headerText = !string.IsNullOrWhiteSpace(argDescription)
? $"{command.DisplayName()} {argDescription}"
: !string.IsNullOrWhiteSpace(command.Description)
? $"{command.DisplayName()} {command.Description}"
: command.DisplayName();
- var header = new TextBlock
- {
- Text = headerText,
- FontSize = 11,
- FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
- TextTrimming = TextTrimming.CharacterEllipsis,
- MaxLines = 1,
- Margin = new Thickness(8, 6, 8, 2),
+ var rows = new List
+ {
+ TextBlock(headerText)
+ .FontSize(11)
+ .SemiBold()
+ .TextTrimming(TextTrimming.CharacterEllipsis)
+ .MaxLines(1)
+ .Foreground(Theme.TertiaryText)
+ .Margin(8, 6, 8, 2),
};
- ApplyTheme(header, () => header.Foreground = ResolveThemeBrush("TextFillColorTertiaryBrush", header.ActualTheme));
- list.Children.Add(header);
for (var index = 0; index < choices.Count; index++)
- list.Children.Add(SlashArgRow(command, choices[index], index == selectedIndex, onPick));
+ rows.Add(SlashArgRow(command, choices[index], index == selectedIndex, onPick));
- return SlashShell(new ScrollViewer
- {
- VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
- HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled,
- MaxHeight = 280,
- Content = list,
- });
+ return SlashShell(
+ ScrollView(VStack(0, rows.ToArray()))
+ .MaxHeight(280)
+ .Set(scrollViewer =>
+ {
+ scrollViewer.VerticalScrollBarVisibility = ScrollingScrollBarVisibility.Auto;
+ scrollViewer.HorizontalScrollBarVisibility = ScrollingScrollBarVisibility.Hidden;
+ }));
}
- private static Button SlashArgRow(
+ private static Element SlashArgRow(
GatewayCommand command,
GatewayCommandArgChoice choice,
bool selected,
Action onPick)
{
var label = string.IsNullOrWhiteSpace(choice.Label) ? choice.Value : choice.Label;
- var row = new StackPanel { Orientation = Orientation.Horizontal, Spacing = 8 };
- var title = new TextBlock
- {
- Text = label,
- FontSize = 13,
- FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
- VerticalAlignment = VerticalAlignment.Center,
- };
- ApplyTheme(title, () => title.Foreground = ResolveThemeBrush("TextFillColorPrimaryBrush", title.ActualTheme));
- row.Children.Add(title);
-
- var subtitle = new TextBlock
- {
- Text = $"{command.DisplayName()} {choice.Value}",
- FontSize = 12,
- VerticalAlignment = VerticalAlignment.Center,
- TextTrimming = TextTrimming.CharacterEllipsis,
- MaxLines = 1,
- };
- ApplyTheme(subtitle, () => subtitle.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", subtitle.ActualTheme));
- row.Children.Add(subtitle);
-
- var button = new Button
- {
- Content = row,
- Padding = new Thickness(8, 7, 8, 7),
- HorizontalAlignment = HorizontalAlignment.Stretch,
- HorizontalContentAlignment = HorizontalAlignment.Left,
- CornerRadius = new CornerRadius(6),
- BorderThickness = new Thickness(0),
- };
- ApplyTheme(button, () =>
- {
- button.Background = selected
- ? ResolveThemeBrush("SubtleFillColorSecondaryBrush", button.ActualTheme)
- : new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- button.BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- });
- Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(
- button,
- $"Choose {label} for {command.DisplayName()}");
- button.Click += (_, _) => onPick(choice);
- if (selected)
- {
- button.Loaded += (_, _) => button.StartBringIntoView(
- new BringIntoViewOptions { AnimationDesired = false });
- }
-
- return button;
- }
-
- private static Border SlashShell(UIElement child)
- {
- var shell = new Border
- {
- BorderThickness = new Thickness(1),
- CornerRadius = new CornerRadius(8),
- Padding = new Thickness(4),
- Child = child,
- Shadow = new ThemeShadow(),
- Translation = new System.Numerics.Vector3(0, 0, 32),
- };
- ApplyTheme(shell, () =>
- {
- shell.Background = ResolvePopupBackgroundBrush(shell.ActualTheme);
- shell.BorderBrush = ResolveThemeBrush("SurfaceStrokeColorDefaultBrush", shell.ActualTheme);
- });
- return shell;
+ var background = selected ? Theme.SubtleFill : Theme.Ref("SubtleFillColorTransparentBrush");
+ return Button(
+ HStack(
+ 8,
+ TextBlock(label)
+ .FontSize(13)
+ .SemiBold()
+ .VAlign(VerticalAlignment.Center)
+ .Foreground(Theme.PrimaryText),
+ TextBlock($"{command.DisplayName()} {choice.Value}")
+ .FontSize(12)
+ .VAlign(VerticalAlignment.Center)
+ .TextTrimming(TextTrimming.CharacterEllipsis)
+ .MaxLines(1)
+ .Foreground(Theme.SecondaryText)),
+ () => onPick(choice))
+ .Padding(8, 7, 8, 7)
+ .HAlign(HorizontalAlignment.Stretch)
+ .CornerRadius(6)
+ .AutomationName($"Choose {label} for {command.DisplayName()}")
+ .Resources(resources => resources
+ .Set("ButtonBackground", background)
+ .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush")))
+ .Set(button =>
+ {
+ button.HorizontalContentAlignment = HorizontalAlignment.Left;
+ button.BorderThickness = new Thickness(0);
+ })
+ .OnMount(element =>
+ {
+ if (selected)
+ element.StartBringIntoView(new BringIntoViewOptions { AnimationDesired = false });
+ });
}
- private static Brush ResolvePopupBackgroundBrush(ElementTheme theme)
+ private static Element SlashShell(Element child)
{
- var overlay = FindThemedResource("TextControlBackground", theme) as SolidColorBrush
- ?? Application.Current?.Resources["TextControlBackground"] as SolidColorBrush;
- var baseBrush = FindThemedResource("SolidBackgroundFillColorBaseBrush", theme) as SolidColorBrush
- ?? Application.Current?.Resources["SolidBackgroundFillColorBaseBrush"] as SolidColorBrush;
- if (overlay is null || baseBrush is null)
- return ResolveThemeBrush("SolidBackgroundFillColorBaseBrush", theme);
-
- var alpha = overlay.Color.A / 255.0;
- static byte Mix(byte background, byte foreground, double alpha) =>
- (byte)Math.Round(background * (1 - alpha) + foreground * alpha);
-
- return new SolidColorBrush(global::Windows.UI.Color.FromArgb(
- 255,
- Mix(baseBrush.Color.R, overlay.Color.R, alpha),
- Mix(baseBrush.Color.G, overlay.Color.G, alpha),
- Mix(baseBrush.Color.B, overlay.Color.B, alpha)));
+ return Border(child)
+ .Padding(4)
+ .CornerRadius(8)
+ .Background(Theme.Ref("AcrylicBackgroundFillColorDefaultBrush"))
+ .WithBorder(Theme.Ref("SurfaceStrokeColorFlyoutBrush"), 1)
+ .Translation(0, 0, 32)
+ .Set(border => border.Shadow = new ThemeShadow());
}
- private static Button SlashRow(
+ private static Element SlashRow(
GatewayCommand command,
bool selected,
string query,
+ ColorScheme colorScheme,
Action onPick)
{
- var mono = new FontFamily("Consolas");
- var grid = new Microsoft.UI.Xaml.Controls.Grid
- {
- ColumnSpacing = 8,
- VerticalAlignment = VerticalAlignment.Center,
- };
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = new GridLength(1, GridUnitType.Star) });
- grid.ColumnDefinitions.Add(new ColumnDefinition { Width = GridLength.Auto });
-
- var icon = new FontIcon
- {
- FontFamily = (FontFamily)Application.Current.Resources["SymbolThemeFontFamily"],
- Glyph = SlashGlyph(command),
- FontSize = 14,
- VerticalAlignment = VerticalAlignment.Center,
+ var cells = new List
+ {
+ TextBlock(SlashGlyph(command))
+ .FontFamily(FluentIconCatalog.SymbolThemeFontFamily)
+ .FontSize(14)
+ .VAlign(VerticalAlignment.Center)
+ .Foreground(Theme.SecondaryText)
+ .AccessibilityView(Microsoft.UI.Xaml.Automation.Peers.AccessibilityView.Raw)
+ .Grid(row: 0, column: 0),
+ TextBlock(command.DisplayName())
+ .FontSize(13)
+ .SemiBold()
+ .VAlign(VerticalAlignment.Center)
+ .Foreground(Theme.PrimaryText)
+ .Set(textBlock => ApplyQueryHighlight(textBlock, query, colorScheme))
+ .Grid(row: 0, column: 1),
};
- ApplyTheme(icon, () => icon.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", icon.ActualTheme));
- Microsoft.UI.Xaml.Automation.AutomationProperties.SetAccessibilityView(
- icon,
- Microsoft.UI.Xaml.Automation.Peers.AccessibilityView.Raw);
- Microsoft.UI.Xaml.Controls.Grid.SetColumn(icon, 0);
- grid.Children.Add(icon);
-
- var name = new TextBlock
- {
- Text = command.DisplayName(),
- FontSize = 13,
- FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
- VerticalAlignment = VerticalAlignment.Center,
- };
- ApplyTheme(name, () => name.Foreground = ResolveThemeBrush("TextFillColorPrimaryBrush", name.ActualTheme));
- Microsoft.UI.Xaml.Controls.Grid.SetColumn(name, 1);
- grid.Children.Add(name);
- ApplyQueryHighlight(name, query);
-
var args = command.ArgTemplate();
if (!string.IsNullOrWhiteSpace(args))
{
- var argBlock = new TextBlock
- {
- Text = args,
- FontSize = 12,
- FontFamily = mono,
- Opacity = 0.75,
- VerticalAlignment = VerticalAlignment.Center,
- };
- ApplyTheme(argBlock, () => argBlock.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", argBlock.ActualTheme));
- Microsoft.UI.Xaml.Controls.Grid.SetColumn(argBlock, 2);
- grid.Children.Add(argBlock);
+ cells.Add(
+ TextBlock(args)
+ .FontSize(12)
+ .FontFamily("Consolas")
+ .VAlign(VerticalAlignment.Center)
+ .Foreground(Theme.SecondaryText)
+ .Grid(row: 0, column: 2));
}
if (!string.IsNullOrWhiteSpace(command.Description))
{
- var description = new TextBlock
- {
- Text = command.Description!,
- FontSize = 12,
- VerticalAlignment = VerticalAlignment.Center,
- HorizontalAlignment = HorizontalAlignment.Right,
- TextAlignment = TextAlignment.Right,
- TextTrimming = TextTrimming.CharacterEllipsis,
- MaxLines = 1,
- };
- ApplyTheme(description, () => description.Foreground = ResolveThemeBrush("TextFillColorSecondaryBrush", description.ActualTheme));
- Microsoft.UI.Xaml.Controls.Grid.SetColumn(description, 3);
- grid.Children.Add(description);
- ApplyQueryHighlight(description, query);
+ cells.Add(
+ TextBlock(command.Description!)
+ .FontSize(12)
+ .VAlign(VerticalAlignment.Center)
+ .HAlign(HorizontalAlignment.Right)
+ .TextAlignment(TextAlignment.Right)
+ .TextTrimming(TextTrimming.CharacterEllipsis)
+ .MaxLines(1)
+ .Foreground(Theme.SecondaryText)
+ .Set(textBlock => ApplyQueryHighlight(textBlock, query, colorScheme))
+ .Grid(row: 0, column: 3));
}
var options = command.OptionCount();
if (options > 0)
{
- var badge = SlashBadge($"{options} options");
- Microsoft.UI.Xaml.Controls.Grid.SetColumn(badge, 4);
- grid.Children.Add(badge);
+ cells.Add(SlashBadge($"{options} options").Grid(row: 0, column: 4));
}
- var button = new Button
- {
- Content = grid,
- Padding = new Thickness(8, 7, 8, 7),
- HorizontalAlignment = HorizontalAlignment.Stretch,
- HorizontalContentAlignment = HorizontalAlignment.Stretch,
- CornerRadius = new CornerRadius(6),
- BorderThickness = new Thickness(0),
- };
- ApplyTheme(button, () =>
- {
- button.Background = selected
- ? ResolveThemeBrush("SubtleFillColorSecondaryBrush", button.ActualTheme)
- : new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- button.BorderBrush = new SolidColorBrush(Microsoft.UI.Colors.Transparent);
- });
- Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(button, $"Insert {command.DisplayName()}");
- button.Click += (_, _) => onPick(command);
- if (selected)
- {
- button.Loaded += (_, _) => button.StartBringIntoView(
- new BringIntoViewOptions { AnimationDesired = false });
- }
- return button;
+ var background = selected ? Theme.SubtleFill : Theme.Ref("SubtleFillColorTransparentBrush");
+ return Button(
+ Grid(
+ [GridSize.Auto, GridSize.Auto, GridSize.Auto, GridSize.Star(), GridSize.Auto],
+ [GridSize.Auto],
+ cells.ToArray())
+ .Set(grid => grid.ColumnSpacing = 8)
+ .VAlign(VerticalAlignment.Center),
+ () => onPick(command))
+ .Padding(8, 7, 8, 7)
+ .HAlign(HorizontalAlignment.Stretch)
+ .CornerRadius(6)
+ .AutomationName($"Insert {command.DisplayName()}")
+ .Resources(resources => resources
+ .Set("ButtonBackground", background)
+ .Set("ButtonBorderBrush", Theme.Ref("SubtleFillColorTransparentBrush")))
+ .Set(button =>
+ {
+ button.HorizontalContentAlignment = HorizontalAlignment.Stretch;
+ button.BorderThickness = new Thickness(0);
+ })
+ .OnMount(element =>
+ {
+ if (selected)
+ element.StartBringIntoView(new BringIntoViewOptions { AnimationDesired = false });
+ });
}
- private static Border SlashBadge(string text)
+ private static Element SlashBadge(string text)
{
- var label = new TextBlock
- {
- Text = text,
- FontSize = 10,
- FontWeight = Microsoft.UI.Text.FontWeights.SemiBold,
- };
- var badge = new Border
- {
- Padding = new Thickness(6, 1, 6, 1),
- CornerRadius = new CornerRadius(4),
- VerticalAlignment = VerticalAlignment.Center,
- Child = label,
- };
- ApplyTheme(badge, () =>
- {
- var accent = ResolveThemeBrush("AccentFillColorDefaultBrush", badge.ActualTheme);
- badge.Background = accent is SolidColorBrush solid
- ? new SolidColorBrush(solid.Color) { Opacity = 0.14 }
- : accent;
- });
- ApplyTheme(label, () => label.Foreground = ResolveThemeBrush("AccentFillColorDefaultBrush", label.ActualTheme));
- return badge;
+ return Border(
+ TextBlock(text)
+ .FontSize(10)
+ .SemiBold()
+ .Foreground(Theme.Ref("TextOnAccentFillColorPrimaryBrush")))
+ .Padding(6, 1, 6, 1)
+ .CornerRadius(4)
+ .VAlign(VerticalAlignment.Center)
+ .Background(Theme.AccentSecondary);
}
- private static void ApplyQueryHighlight(TextBlock textBlock, string? query)
+ private static void ApplyQueryHighlight(TextBlock textBlock, string? query, ColorScheme colorScheme)
{
textBlock.TextHighlighters.Clear();
var text = textBlock.Text ?? string.Empty;
var normalized = (query ?? string.Empty).Trim().TrimStart('/').Trim();
- if (normalized.Length == 0 || text.Length < normalized.Length)
+ if (normalized.Length == 0 || text.Length < normalized.Length || colorScheme == ColorScheme.HighContrast)
+ return;
+
+ var isDark = colorScheme == ColorScheme.Dark;
+ if (ThemeRef.Resolve("AccentFillColorDefaultBrush", isDark) is not SolidColorBrush accent
+ || ThemeRef.Resolve("TextFillColorPrimaryBrush", isDark) is not Brush foreground)
return;
- var accent = Application.Current.Resources["AccentFillColorDefaultBrush"] as SolidColorBrush
- ?? new SolidColorBrush(Microsoft.UI.Colors.SteelBlue);
var accentColor = accent.Color;
var highlighter = new Microsoft.UI.Xaml.Documents.TextHighlighter
{
Background = new SolidColorBrush(global::Windows.UI.Color.FromArgb(31, accentColor.R, accentColor.G, accentColor.B)),
- Foreground = Application.Current.Resources["TextFillColorPrimaryBrush"] as Brush
- ?? new SolidColorBrush(Microsoft.UI.Colors.White),
+ Foreground = foreground,
};
for (var index = 0; index <= text.Length - normalized.Length;)
diff --git a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml
index e3d118ca7..4b6220e84 100644
--- a/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml
+++ b/src/OpenClaw.Tray.WinUI/Pages/ConnectionPage.xaml
@@ -19,6 +19,42 @@
+
+
+
+
+
+
+
+
+
+
+
+
@@ -390,12 +392,6 @@
as the WinUI default once rows are bigger. -->
0,2,0,2
-
diff --git a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs
index 550f847a6..0219a8696 100644
--- a/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs
+++ b/src/OpenClaw.Tray.WinUI/Windows/HubWindow.xaml.cs
@@ -1396,12 +1396,9 @@ private void ExecuteCommand(CommandItem cmd)
#region High Contrast icon fallback
- // Maps NavigationViewItem.Tag -> Segoe Fluent Icons glyph used as fallback
- // when Windows High Contrast is active. FontIcon uses the system foreground
- // brush so it auto-adapts to every HC variant (HC Black/White/#1/#2); our
- // multi-color SVGs don't, so we swap them out at construction. This mirrors
- // the original gray Segoe Fluent Icons that were here before the colorful
- // refresh — same glyphs as those Windows users learned in earlier builds.
+ // Fixed-color SVGs provide the normal Hub presentation. In High Contrast,
+ // App.xaml resolves this path to system-foreground Fluent glyphs without
+ // constructing or subscribing to the legacy WinRT accessibility API.
private static readonly Dictionary s_highContrastGlyphFallback = new()
{
{ "chat", "\uE8BD" },
@@ -1424,36 +1421,29 @@ private void ExecuteCommand(CommandItem cmd)
{ "debug", "\uEBE8" },
};
- // Glyphs for the two parent NavigationViewItems that don't carry a Tag
- // ("Advanced" group and "Agents" group). These also feed the dynamic agent
- // items added at runtime.
private const string AdvancedGroupGlyph = "\uE950";
private const string AgentsGroupGlyph = "\uE99A";
-
private bool _isHighContrast;
private void ApplyHighContrastFallbackIfNeeded()
{
- try
- {
- var settings = new global::Windows.UI.ViewManagement.AccessibilitySettings();
- _isHighContrast = settings.HighContrast;
- }
- catch
- {
- _isHighContrast = false;
+ const string resourceKey = "HubNavigationUseHighContrastIcons";
+ _isHighContrast = Application.Current.Resources.ContainsKey(resourceKey)
+ && Application.Current.Resources[resourceKey] is true;
+ if (!_isHighContrast)
return;
- }
- if (!_isHighContrast) return;
+
SwapToFontIcons(NavView.MenuItems);
SwapToFontIcons(NavView.FooterMenuItems);
}
private void SwapToFontIcons(IList