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).
2026-05-13 00:13:58 -04:00
|
|
|
using System;
|
|
|
|
|
using Microsoft.UI;
|
|
|
|
|
using Microsoft.UI.Xaml;
|
|
|
|
|
using Windows.UI;
|
|
|
|
|
using Windows.UI.ViewManagement;
|
|
|
|
|
|
|
|
|
|
namespace TeamsISO.App.WinUI.Services;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Owns the active theme for the WinUI 3 host. Three preferences:
|
|
|
|
|
/// <c>System</c> follows the Windows app-mode setting (default for new
|
|
|
|
|
/// users); <c>Dark</c> and <c>Light</c> 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 <see cref="Themed"/> 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).
|
|
|
|
|
/// </summary>
|
|
|
|
|
public sealed class ThemeManager
|
|
|
|
|
{
|
|
|
|
|
public static ThemeManager Current { get; } = new();
|
|
|
|
|
|
|
|
|
|
private ThemeManager()
|
|
|
|
|
{
|
|
|
|
|
_uiSettings = new UISettings();
|
|
|
|
|
_uiSettings.ColorValuesChanged += OnSystemColorsChanged;
|
2026-05-13 21:35:31 -04:00
|
|
|
|
|
|
|
|
// Hydrate the preference from disk so the operator's choice
|
|
|
|
|
// survives across launches. Defaults to "System" if the prefs
|
|
|
|
|
// file is missing or unreadable (Load() catches its own errors).
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var prefs = UIPreferences.Load();
|
|
|
|
|
if (prefs.Theme == "System" || prefs.Theme == "Dark" || prefs.Theme == "Light")
|
|
|
|
|
{
|
|
|
|
|
_preference = prefs.Theme;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
// Defensive — ThemeManager.Current is a static singleton; a
|
|
|
|
|
// throw here would prevent the app from getting any theme.
|
|
|
|
|
}
|
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).
2026-05-13 00:13:58 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private readonly UISettings _uiSettings;
|
|
|
|
|
private string _preference = "System";
|
|
|
|
|
|
|
|
|
|
public string Preference => _preference;
|
|
|
|
|
|
|
|
|
|
public event EventHandler<ElementTheme>? Themed;
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Resolve the preference to an absolute <see cref="ElementTheme"/>
|
|
|
|
|
/// suitable for <see cref="FrameworkElement.RequestedTheme"/>.
|
|
|
|
|
/// <c>System</c> resolves to the OS app-mode.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Cycle dark ↔ light from the title-bar toggle. If the current
|
|
|
|
|
/// preference is <c>System</c>, the cycle pins to the opposite of the
|
|
|
|
|
/// currently-resolved theme so the click has a visible effect.
|
|
|
|
|
/// </summary>
|
|
|
|
|
public ElementTheme Toggle()
|
|
|
|
|
{
|
|
|
|
|
var current = ResolveTheme();
|
|
|
|
|
Set(current == ElementTheme.Dark ? "Light" : "Dark");
|
|
|
|
|
return ResolveTheme();
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-13 21:35:31 -04:00
|
|
|
/// <summary>Set the preference, persist to disk, broadcast the resolved theme.</summary>
|
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).
2026-05-13 00:13:58 -04:00
|
|
|
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;
|
2026-05-13 21:35:31 -04:00
|
|
|
try { UIPreferences.SetTheme(preference); }
|
|
|
|
|
catch { /* persistence is best-effort */ }
|
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).
2026-05-13 00:13:58 -04:00
|
|
|
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());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
|
/// Compute the AppWindow title-bar foreground for the given resolved
|
|
|
|
|
/// theme so the system min/max/close buttons stay readable.
|
|
|
|
|
/// </summary>
|
|
|
|
|
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);
|
|
|
|
|
}
|