From 48ca16bc5e8f6f9c2ee52707413613e0bd4c4ad4 Mon Sep 17 00:00:00 2001 From: Zac Gaetano Date: Wed, 13 May 2026 00:13:58 -0400 Subject: [PATCH] feat(winui3): ThemeManager service + Settings drawer + Help/About/Onboarding Builds out the secondary surfaces of the redesigned WinUI 3 host. ThemeManager (Services/ThemeManager.cs) Single-source-of-truth for the active theme. Holds the user preference (System / Dark / Light), resolves it to ElementTheme at request, and raises a Themed event when it changes so the MainWindow can push the AppWindow title-bar button colors. Uses Windows.UI.ViewManagement UISettings to follow the OS app-mode when preference is System. Persistence to UIPreferences lands in the engine-wiring commit. MainWindow theme wiring Replaces the per-handler theme toggle with a ThemeManager subscription: click the title-bar sun/moon -> Toggle() -> Themed event -> ApplyResolvedTheme on the visual tree + the title-bar buttons. Glyph cue: sun = "current is Light, click to Dark"; moon = "current is Dark, click to Light." Initial state applied at construction so the first frame matches the preference. SettingsDrawer (Views/SettingsDrawer.xaml + .cs) UserControl that slides in from the right over the participants table. 56px header, NavigationView with five tabs (Appearance, Routing, Display, Control, Advanced), footer with Reset-to-defaults + Apply/Close. Appearance tab has the theme tri-state picker (System / Dark / Light radio group) and an "Accent peek" row showing the four brand accents (cyan / coral / live / warn) as swatches so the operator can verify Wild Dragon brand is respected on a light desk. CloseRequested event signals the host to collapse the drawer. HelpDialog (Views/HelpDialog.xaml + .cs) ContentDialog with the keyboard shortcut cheat sheet, grouped by category (Global / Participants / Look / Control surface). 540px max height with scroll, mono-spaced shortcut labels at left, body text at right. Replaces the WPF host's HelpWindow at parity. AboutDialog (Views/AboutDialog.xaml + .cs) ContentDialog with the Wild Dragon mark, version + host + engine + brand info as label/value rows, and three quick action buttons (open logs folder, open recordings, check for updates). Mirrors the WPF host's AboutWindow. OnboardingDialog (Views/OnboardingDialog.xaml + .cs) Three numbered steps (Install NDI Runtime / Enable Teams NDI / Pick transcoder topology), no carousel, operator-tone copy ("Don't show this again" defaults checked). PrimaryButtonText "Get started", SecondaryButtonText "Skip" so the dialog is skippable from the first frame as the PRODUCT.md anti-references demand. Build clean: dotnet build TeamsISO.App.WinUI -c Debug -> 0 / 0. Next: wire the drawer's CloseRequested into MainWindow (so the settings icon actually opens / collapses the drawer), then attack the runtime activation blocker (Phase 3 of the migration plan). --- .../Services/ThemeManager.cs | 108 +++++++++ src/TeamsISO.App.WinUI/Views/AboutDialog.xaml | 75 ++++++ .../Views/AboutDialog.xaml.cs | 11 + src/TeamsISO.App.WinUI/Views/HelpDialog.xaml | 110 +++++++++ .../Views/HelpDialog.xaml.cs | 11 + .../Views/MainWindow.xaml.cs | 67 +++-- .../Views/OnboardingDialog.xaml | 104 ++++++++ .../Views/OnboardingDialog.xaml.cs | 18 ++ .../Views/SettingsDrawer.xaml | 99 ++++++++ .../Views/SettingsDrawer.xaml.cs | 229 ++++++++++++++++++ 10 files changed, 797 insertions(+), 35 deletions(-) create mode 100644 src/TeamsISO.App.WinUI/Services/ThemeManager.cs create mode 100644 src/TeamsISO.App.WinUI/Views/AboutDialog.xaml create mode 100644 src/TeamsISO.App.WinUI/Views/AboutDialog.xaml.cs create mode 100644 src/TeamsISO.App.WinUI/Views/HelpDialog.xaml create mode 100644 src/TeamsISO.App.WinUI/Views/HelpDialog.xaml.cs create mode 100644 src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml create mode 100644 src/TeamsISO.App.WinUI/Views/OnboardingDialog.xaml.cs create mode 100644 src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml create mode 100644 src/TeamsISO.App.WinUI/Views/SettingsDrawer.xaml.cs diff --git a/src/TeamsISO.App.WinUI/Services/ThemeManager.cs b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs new file mode 100644 index 0000000..7cc683a --- /dev/null +++ b/src/TeamsISO.App.WinUI/Services/ThemeManager.cs @@ -0,0 +1,108 @@ +using System; +using Microsoft.UI; +using Microsoft.UI.Xaml; +using Windows.UI; +using Windows.UI.ViewManagement; + +namespace TeamsISO.App.WinUI.Services; + +/// +/// Owns the active theme for the WinUI 3 host. Three preferences: +/// System follows the Windows app-mode setting (default for new +/// users); Dark and Light pin one regardless of the OS choice. +/// The persistence path will land alongside the existing UIPreferences in +/// the next commit — for now state lives in-process. +/// +/// All public mutations push to subscribers so the +/// host (MainWindow) can update the AppWindow title-bar button colors +/// (system buttons aren't part of the visual tree and need a separate +/// poke when ElementTheme changes). +/// +public sealed class ThemeManager +{ + public static ThemeManager Current { get; } = new(); + + private ThemeManager() + { + _uiSettings = new UISettings(); + _uiSettings.ColorValuesChanged += OnSystemColorsChanged; + } + + private readonly UISettings _uiSettings; + private string _preference = "System"; + + public string Preference => _preference; + + public event EventHandler? Themed; + + /// + /// Resolve the preference to an absolute + /// suitable for . + /// System resolves to the OS app-mode. + /// + public ElementTheme ResolveTheme() => _preference switch + { + "Dark" => ElementTheme.Dark, + "Light" => ElementTheme.Light, + _ => IsSystemDark() ? ElementTheme.Dark : ElementTheme.Light, + }; + + public bool PreferenceMatches(string value) => string.Equals(_preference, value, StringComparison.Ordinal); + + /// + /// Cycle dark ↔ light from the title-bar toggle. If the current + /// preference is System, the cycle pins to the opposite of the + /// currently-resolved theme so the click has a visible effect. + /// + public ElementTheme Toggle() + { + var current = ResolveTheme(); + Set(current == ElementTheme.Dark ? "Light" : "Dark"); + return ResolveTheme(); + } + + /// Set the preference and broadcast the resolved theme. + public void Set(string preference) + { + if (preference != "System" && preference != "Dark" && preference != "Light") + { + throw new ArgumentException("Preference must be System, Dark, or Light.", nameof(preference)); + } + + _preference = preference; + Themed?.Invoke(this, ResolveTheme()); + } + + private bool IsSystemDark() + { + // UISettings.GetColorValue(UIColorType.Background) returns + // black-ish in dark mode, white-ish in light mode — the most + // reliable cross-version check for app mode on desktop WinUI 3. + var bg = _uiSettings.GetColorValue(UIColorType.Background); + return ((5 * bg.G) + (2 * bg.R) + bg.B) < 8 * 128; + } + + private void OnSystemColorsChanged(UISettings sender, object args) + { + // Only re-broadcast if the operator hasn't pinned a preference — + // otherwise the explicit choice wins regardless of what the OS does. + if (_preference == "System") + { + Themed?.Invoke(this, ResolveTheme()); + } + } + + /// + /// Compute the AppWindow title-bar foreground for the given resolved + /// theme so the system min/max/close buttons stay readable. + /// + public static Color TitleBarForegroundFor(ElementTheme theme) => + theme == ElementTheme.Dark + ? Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6) + : Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A); + + public static Color TitleBarHoverBgFor(ElementTheme theme) => + theme == ElementTheme.Dark + ? Color.FromArgb(0xFF, 0x33, 0x34, 0x3A) + : Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1); +} diff --git a/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml new file mode 100644 index 0000000..143430d --- /dev/null +++ b/src/TeamsISO.App.WinUI/Views/AboutDialog.xaml @@ -0,0 +1,75 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +