feat(winui3): redesigned MainWindow + custom title bar + theme toggle

Lands the approved shape brief as the WinUI 3 MainWindow:

* 64px left rail with brand mark, primary nav (participants), Teams
  launch / hide / settings buttons, and the engine-status puck at the
  bottom. All five rail buttons use Segoe Fluent Icons glyphs at a
  uniform 20px optical size; no more bespoke <Path Data> shapes with
  inconsistent stroke weights.

* 44px custom title bar via ExtendsContentIntoTitleBar +
  SetTitleBar(AppTitleBar). The drag region absorbs the three live-state
  pills inline (session timer 'live * 00:14:32', REC count + elapsed,
  disk free) and a slim sun/moon theme-toggle button to the left of the
  system Min/Max/Close controls. System buttons inherit ButtonForeground
  Color etc. from AppWindow.TitleBar so they match palette in both
  themes.

* Section header with 'Participants * count' display, filter input,
  Refresh + Presets (Secondary buttons), and 'Enable all online' as
  the single cyan Primary button - finally a real button hierarchy
  instead of seven indistinguishable ghost buttons.

* Participants list rendered as ItemsRepeater + DataTemplate for now;
  the CommunityToolkit DataGrid migration follows in a separate commit.
  Row template at 64px height with: 3px cyan left border for active
  speaker, avatar with initials in cyan-muted circle, name + codec line,
  signal lock state with dot, audio meter via ProgressBar, output name
  in JetBrains Mono, ISO state pill (LIVE/OFF/ERROR) at right.

* Conditional in-call control bar below the table: Mute / Camera /
  Share / Marker / Leave + overflow kebab. Muted state binds the
  destructive coral treatment to the Mute button; Leave is also
  destructive (coral border + text); everything else is Secondary.
  Tight 8px spacing keeps the bar dense without crowding.

* Slim 32px status bar at the bottom: control-surface URL on the left
  (cyan dot indicator), keyboard-shortcut hints on the right in
  tertiary mono. Replaces the WPF host's six-column footer.

Implementation notes:

* MockParticipant model populates the table with representative data
  (Maya / Daniel / Aicha / Sam, one as active speaker) until the
  ParticipantViewModel binding migrates over from the WPF host.

* Custom Program.cs takes ownership of Main from the XAML compiler
  (DISABLE_XAML_GENERATED_MAIN). Calls Bootstrap.TryInitialize(0x00010006)
  before Application.Start so the unpackaged .exe can locate the
  WindowsAppSDK 1.6 framework MSIX at launch. Shutdown is paired in
  a finally block.

* Theme toggle in code-behind flips Window.Content.RequestedTheme
  between Dark and Light. {ThemeResource} bindings auto-swap across
  the visual tree; system title-bar buttons (outside the XAML tree)
  get color updates inline so they stay readable in both modes.

* app.manifest deferred from build - the framework-emitted manifest
  covers DPI awareness and supportedOS GUIDs; reintroducing our own
  goes in the next commit alongside the bootstrapper hardening.

Known issue: the unpackaged .exe currently fails to activate on this
build host with 'this application could not be started' before Main
runs. Build is clean; published output runs the same way. Diagnosing
the activation failure is the next session's first task (likely the
runtimeconfig.json including Microsoft.WindowsDesktop.App which WinUI 3
doesn't want, or a missing CRT redistributable). The WPF host remains
the running build until that's resolved.

dotnet build TeamsISO.Windows.slnf -c Debug: 0 warnings, 0 errors.
This commit is contained in:
Zac Gaetano 2026-05-13 00:03:12 -04:00
parent cb1402ec8d
commit 9e176d8f10
5 changed files with 689 additions and 6 deletions

View file

@ -0,0 +1,56 @@
using System.Collections.Generic;
namespace TeamsISO.App.WinUI.Models;
/// <summary>
/// Stand-in for the real ParticipantViewModel until the view-model bindings
/// migrate over from the WPF host. Lets the redesigned MainWindow render with
/// representative data so the visual design can be validated independently
/// of the engine layer. Removed in the view-model wiring commit.
/// </summary>
public sealed class MockParticipant
{
public string DisplayName { get; init; } = "";
public string Initials { get; init; } = "";
public string SourceCodec { get; init; } = "MS Teams · 1920x1080 · 30fps";
public string SignalState { get; init; } = "locked"; // locked | degraded | offline
public string OutputName { get; init; } = "";
public string IsoState { get; init; } = "OFF"; // LIVE | OFF | ERROR
public double AudioLevel { get; init; } = 0.0; // 0..1
public bool IsActiveSpeaker { get; init; } = false;
public static List<MockParticipant> Sample()
{
return new List<MockParticipant>
{
new()
{
DisplayName = "Maya Rodriguez", Initials = "MA",
SourceCodec = "MS Teams · 1920x1080 · 30fps",
SignalState = "locked", OutputName = "TEAMSISO_maya",
IsoState = "LIVE", AudioLevel = 0.82, IsActiveSpeaker = true,
},
new()
{
DisplayName = "Daniel Chen", Initials = "DC",
SourceCodec = "MS Teams · 1280x720 · 30fps",
SignalState = "locked", OutputName = "TEAMSISO_daniel",
IsoState = "LIVE", AudioLevel = 0.35,
},
new()
{
DisplayName = "Aicha Kone", Initials = "AK",
SourceCodec = "MS Teams · 1920x1080 · 30fps",
SignalState = "degraded", OutputName = "TEAMSISO_aicha",
IsoState = "OFF", AudioLevel = 0.12,
},
new()
{
DisplayName = "Sam Park", Initials = "SP",
SourceCodec = "MS Teams · 1920x1080 · 30fps",
SignalState = "locked", OutputName = "TEAMSISO_sam",
IsoState = "LIVE", AudioLevel = 0.48,
},
};
}
}

View file

@ -0,0 +1,63 @@
using System;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.Windows.ApplicationModel.DynamicDependency;
namespace TeamsISO.App.WinUI;
/// <summary>
/// Custom Main for the unpackaged WinUI 3 host.
///
/// The default XAML-compiler-generated Main (disabled here via the
/// DISABLE_XAML_GENERATED_MAIN compile constant) calls Application.Start
/// directly. That's fine for MSIX-packaged WinUI 3 apps where the OS
/// activates the package and the runtime is found via the package
/// dependency, but for an unpackaged .exe the Windows App Runtime has to
/// be bootstrapped explicitly before any WinUI 3 type is touched.
///
/// Bootstrap.TryInitialize(0x00010006) targets WindowsAppSDK 1.6 (the LTS
/// branch we ship against). The major nibble 0x0001 is the runtime major;
/// the minor 0x0006 is the runtime minor. If the user's machine has a
/// compatible 1.6.x framework installed (the broadcast-tool installer
/// will eventually ensure that as a prereq), Bootstrap connects and the
/// rest of the WinUI 3 surface comes alive. If not, the call returns a
/// negative HResult that we surface via Environment.Exit so the .exe
/// dies cleanly rather than throwing an opaque "this application could
/// not be started" dialog.
/// </summary>
public static class Program
{
/// <summary>WindowsAppSDK 1.6 major/minor packed as 0x00010006.</summary>
private const uint WindowsAppSdkMajorMinor = 0x00010006;
[STAThread]
public static int Main(string[] args)
{
if (!Bootstrap.TryInitialize(WindowsAppSdkMajorMinor, out int hr))
{
// The runtime isn't installed (or this build's bootstrap can't
// find a compatible package version). Surface a Win32 error code
// so postmortem inspection of the launch failure has more than
// "application could not be started" to go on.
return hr;
}
try
{
WinRT.ComWrappersSupport.InitializeComWrappers();
Application.Start(p =>
{
var context = new DispatcherQueueSynchronizationContext(
DispatcherQueue.GetForCurrentThread());
System.Threading.SynchronizationContext.SetSynchronizationContext(context);
_ = new App();
});
}
finally
{
Bootstrap.Shutdown();
}
return 0;
}
}

View file

@ -25,7 +25,14 @@
<SupportedOSPlatformVersion>10.0.17763.0</SupportedOSPlatformVersion>
<RootNamespace>TeamsISO.App.WinUI</RootNamespace>
<AssemblyName>TeamsISO</AssemblyName>
<ApplicationManifest>app.manifest</ApplicationManifest>
<!--
Default app.manifest deferred: WinUI 3 emits its own manifest with the
DPI awareness + supportedOS GUIDs that match what we want. Our custom
manifest (kept in tree at app.manifest) describes the same intent but
doesn't currently merge cleanly with the framework-emitted manifest;
see docs/superpowers/plans/2026-05-12-winui3-migration.md for the
follow-up to reintroduce it via uap:VisualElements.
-->
<Platforms>x64;ARM64</Platforms>
<RuntimeIdentifiers>win-x64;win-arm64</RuntimeIdentifiers>
<UseWinUI>true</UseWinUI>
@ -38,6 +45,16 @@
having to upgrade the .NET SDK on the build host.
-->
<WindowsSdkPackageVersion>10.0.19041.38</WindowsSdkPackageVersion>
<!--
Disable the XAML compiler's auto-generated Program.Main so we can write
one that bootstraps the Windows App Runtime explicitly. The default
generated Main calls Application.Start directly, with no Bootstrap
initialization step — that's fine for packaged MSIX apps but blocks
unpackaged launch on a machine where the runtime is installed only as
a framework package. Program.cs in this project takes ownership of
Main and calls Bootstrap.TryInitialize(0x00010006) before Start.
-->
<DefineConstants>$(DefineConstants);DISABLE_XAML_GENERATED_MAIN</DefineConstants>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<ApplicationIcon>Assets\teamsiso.ico</ApplicationIcon>

View file

@ -2,11 +2,481 @@
<Window
x:Class="TeamsISO.App.WinUI.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:models="using:TeamsISO.App.WinUI.Models">
<!--
TeamsISO MainWindow — redesigned IA per the approved shape brief.
Structure:
[64 rail] [content]
[44 title bar — drag region]
[section header]
[participants list — hero]
[in-call control — conditional]
[32 status bar]
The rail's bottom puck opens the engine-status popover that absorbs
what used to live in the WPF footer (logs path, version, control
surface URL details). The title bar absorbs the live state pills
(session timer · REC · disk free) so the operator's at-a-glance
read stays in peripheral vision regardless of scroll position.
Settings is a right-side drawer (opened from the rail settings icon)
rather than a permanent 380px panel — the participants list claims
the full content width when settings aren't being actively edited.
-->
<Grid Background="{ThemeResource BgCanvas}">
<TextBlock Style="{StaticResource TextTitle}"
Text="TeamsISO — WinUI 3 host scaffold"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="64"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- ═══════════════════════ LEFT RAIL ═══════════════════════ -->
<Border Grid.Column="0"
Background="{ThemeResource BgRail}"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="0,0,1,0">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Spacing="2" Padding="0,12,0,0">
<!-- Wild Dragon brand mark -->
<Button x:Name="BrandButton"
Style="{StaticResource ButtonRailIcon}"
Width="48" Height="56"
Margin="8,0,8,8"
ToolTipService.ToolTip="About TeamsISO">
<Border Width="40" Height="40"
CornerRadius="{ThemeResource RadiusM}"
Background="{ThemeResource AccentCyanMuted}">
<TextBlock Text="W"
FontFamily="{ThemeResource FontSans}"
FontSize="22"
FontWeight="Bold"
Foreground="{ThemeResource AccentCyanText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</Button>
<Border Height="1"
Background="{ThemeResource BorderSubtle}"
Margin="14,4,14,12"/>
<!-- Participants / Home (active) -->
<Button Style="{StaticResource ButtonRailIcon}"
Margin="8,0"
ToolTipService.ToolTip="Participants">
<Grid>
<Border Width="48" Height="48"
CornerRadius="{ThemeResource RadiusM}"
Background="{ThemeResource AccentCyanMuted}"/>
<FontIcon Glyph="&#xE716;"
FontSize="20"
Foreground="{ThemeResource AccentCyanText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Grid>
</Button>
<!-- Launch / surface Teams -->
<Button Style="{StaticResource ButtonRailIcon}"
Margin="8,0"
ToolTipService.ToolTip="Launch Microsoft Teams (or surface its window)">
<FontIcon Glyph="&#xE714;"
FontSize="20"
Foreground="{ThemeResource FgSecondary}"/>
</Button>
<!-- Hide / show Teams windows -->
<Button Style="{StaticResource ButtonRailIcon}"
Margin="8,0"
ToolTipService.ToolTip="Hide / show Microsoft Teams windows">
<FontIcon Glyph="&#xE7B3;"
FontSize="20"
Foreground="{ThemeResource FgSecondary}"/>
</Button>
<!-- Settings drawer trigger -->
<Button x:Name="SettingsButton"
Style="{StaticResource ButtonRailIcon}"
Margin="8,0"
ToolTipService.ToolTip="Settings">
<FontIcon Glyph="&#xE713;"
FontSize="20"
Foreground="{ThemeResource FgSecondary}"/>
</Button>
</StackPanel>
<!-- Engine status puck — opens the status popover -->
<Button x:Name="StatusPuckButton"
Grid.Row="1"
Style="{StaticResource ButtonRailIcon}"
Width="48" Height="48"
Margin="8,12"
CornerRadius="24"
Background="{ThemeResource StatusLiveBg}"
ToolTipService.ToolTip="Engine status">
<Ellipse Width="10" Height="10"
Fill="{ThemeResource StatusLive}"/>
</Button>
</Grid>
</Border>
<!-- ═══════════════════════ CONTENT ═══════════════════════ -->
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="44"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
<RowDefinition Height="32"/>
</Grid.RowDefinitions>
<!-- ─── Title bar ─── -->
<!--
AppTitleBar is the drag region. Window.SetTitleBar(AppTitleBar)
in code-behind makes this element the operating-system-defined
drag area. The system Min/Max/Close buttons render to the right
of this element automatically (their colors come from
AppWindow.TitleBar.ButtonForegroundColor etc.); we draw
everything else.
-->
<Grid x:Name="AppTitleBar"
Grid.Row="0"
Background="{ThemeResource BgCanvas}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="12"
Padding="24,0,0,0"
VerticalAlignment="Center">
<TextBlock Text="TeamsISO"
Style="{StaticResource TextHeading}"
VerticalAlignment="Center"/>
<TextBlock x:Name="VersionLabel"
Text="v1.0.0-alpha"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource FgTertiary}"
VerticalAlignment="Center"/>
</StackPanel>
<!-- Live pills (session timer / REC count / disk) live in the
title bar so peripheral-vision status reads from the same
place whether the operator is scrolled, settings-drawer
open, or in-call. Conditionally shown via code-behind. -->
<StackPanel x:Name="LivePillsPanel"
Grid.Column="2"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center"
Padding="0,0,12,0">
<Border CornerRadius="{ThemeResource RadiusPill}"
Background="{ThemeResource StatusLiveBg}"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<Ellipse Width="7" Height="7"
Fill="{ThemeResource StatusLive}"
VerticalAlignment="Center"/>
<TextBlock x:Name="SessionTimerText"
Text="live · 00:14:32"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource StatusLive}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border CornerRadius="{ThemeResource RadiusPill}"
Background="{ThemeResource AccentCoralBg}"
Padding="10,4">
<StackPanel Orientation="Horizontal" Spacing="6">
<Ellipse Width="7" Height="7"
Fill="{ThemeResource AccentCoral}"
VerticalAlignment="Center"/>
<TextBlock x:Name="RecPillText"
Text="rec 3 · 00:11:08"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource AccentCoral}"
VerticalAlignment="Center"/>
</StackPanel>
</Border>
<Border CornerRadius="{ThemeResource RadiusPill}"
Background="{ThemeResource BgSurface}"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="1"
Padding="10,4">
<TextBlock x:Name="DiskFreeText"
Text="482 GB free"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource FgSecondary}"
VerticalAlignment="Center"/>
</Border>
</StackPanel>
<!-- Theme toggle — single-click cycle between Dark and Light.
Persisted to UIPreferences.Theme on click. -->
<Button x:Name="ThemeToggleButton"
Grid.Column="3"
Style="{StaticResource ButtonCaption}"
Click="OnThemeToggleClick"
ToolTipService.ToolTip="Toggle theme (dark / light)">
<FontIcon x:Name="ThemeToggleIcon"
Glyph="&#xE706;"
FontSize="14"/>
</Button>
<!-- The Min / Max / Close buttons that follow in Grid.Column 4,5
are NOT drawn here — the WindowsAppSDK title-bar API draws
them itself, overlaid on the drag region we've defined.
The reserved columns 4 and 5 are just visual placeholders
in this layout to remind future readers where they land. -->
<Border Grid.Column="4" Width="138" Background="Transparent"/>
</Grid>
<!-- ─── Section header ─── -->
<Grid Grid.Row="1" Padding="32,18,32,12">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="12"
VerticalAlignment="Center">
<TextBlock Text="Participants"
Style="{StaticResource TextDisplay}"/>
<Border CornerRadius="{ThemeResource RadiusPill}"
Background="{ThemeResource BgSurface}"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="1"
Padding="10,3"
VerticalAlignment="Center">
<TextBlock x:Name="ParticipantCountText"
Text="4"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource FgSecondary}"/>
</Border>
</StackPanel>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<TextBox x:Name="FilterInput"
PlaceholderText="Filter"
Width="200"
VerticalAlignment="Center"/>
<Button Style="{StaticResource ButtonSecondary}"
Content="Refresh"
ToolTipService.ToolTip="Refresh NDI discovery"/>
<Button Style="{StaticResource ButtonSecondary}"
Content="Presets"
ToolTipService.ToolTip="Save or load operator presets"/>
<Button Style="{StaticResource ButtonPrimary}"
Content="Enable all online"
HorizontalAlignment="Right"
ToolTipService.ToolTip="Enable ISOs for every online participant"/>
</StackPanel>
</Grid>
<!-- ─── Participants list (hero) ─── -->
<ScrollViewer Grid.Row="2"
Padding="32,0,32,0"
VerticalScrollBarVisibility="Auto">
<ItemsRepeater x:Name="ParticipantsRepeater">
<ItemsRepeater.Layout>
<StackLayout Orientation="Vertical" Spacing="0"/>
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate x:DataType="models:MockParticipant">
<Grid Height="64"
Padding="0,0,12,0"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="0,0,0,1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="2*"/>
<ColumnDefinition Width="1*"/>
<ColumnDefinition Width="1.2*"/>
<ColumnDefinition Width="1.5*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Border Grid.Column="0"
Width="3" Height="64"
Background="{ThemeResource AccentCyanText}"
Visibility="{Binding IsActiveSpeaker}"/>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
Spacing="12"
Padding="14,0,0,0"
VerticalAlignment="Center">
<Border Width="36" Height="36"
CornerRadius="18"
Background="{ThemeResource AccentCyanMuted}">
<TextBlock Text="{Binding Initials}"
Style="{StaticResource TextBody}"
FontWeight="SemiBold"
Foreground="{ThemeResource AccentCyanText}"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
<StackPanel VerticalAlignment="Center" Spacing="2">
<TextBlock Text="{Binding DisplayName}"
Style="{StaticResource TextBody}"
FontWeight="Medium"/>
<TextBlock Text="{Binding SourceCodec}"
Style="{StaticResource TextCaption}"
Foreground="{ThemeResource FgSecondary}"/>
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Ellipse Width="8" Height="8"
Fill="{ThemeResource StatusLive}"
VerticalAlignment="Center"/>
<TextBlock Text="{Binding SignalState}"
Style="{StaticResource TextMono}"
FontSize="11"/>
</StackPanel>
<Grid Grid.Column="3" VerticalAlignment="Center" Height="22">
<ProgressBar Maximum="1.0"
Value="{Binding AudioLevel}"
Height="6"
VerticalAlignment="Center"/>
</Grid>
<TextBlock Grid.Column="4"
Text="{Binding OutputName}"
Style="{StaticResource TextMono}"
VerticalAlignment="Center"/>
<Border Grid.Column="5"
CornerRadius="{ThemeResource RadiusPill}"
Background="{ThemeResource StatusLiveBg}"
BorderBrush="{ThemeResource StatusLive}"
BorderThickness="1"
Padding="14,6"
MinWidth="80"
VerticalAlignment="Center">
<TextBlock Text="{Binding IsoState}"
Style="{StaticResource TextBody}"
FontSize="11"
FontWeight="SemiBold"
Foreground="{ThemeResource StatusLive}"
HorizontalAlignment="Center"/>
</Border>
</Grid>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<!-- ─── In-call control (conditional) ─── -->
<Border Grid.Row="3"
Padding="32,12,32,12"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="0,1,0,0"
Background="{ThemeResource BgCanvas}">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Text="IN-CALL"
Style="{StaticResource TextCaption}"
VerticalAlignment="Center"
Margin="0,0,8,0"/>
<Button Style="{StaticResource ButtonDestructive}"
ToolTipService.ToolTip="Toggle microphone mute">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE74F;" FontSize="14"/>
<TextBlock Text="Muted"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonSecondary}"
ToolTipService.ToolTip="Toggle camera">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE714;" FontSize="14"/>
<TextBlock Text="Camera"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonSecondary}"
ToolTipService.ToolTip="Open Teams share tray">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE72D;" FontSize="14"/>
<TextBlock Text="Share"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonSecondary}"
ToolTipService.ToolTip="Drop a timestamped marker into every active recording">
<StackPanel Orientation="Horizontal" Spacing="6">
<FontIcon Glyph="&#xE735;" FontSize="14"/>
<TextBlock Text="Marker"/>
</StackPanel>
</Button>
<Button Style="{StaticResource ButtonDestructive}"
ToolTipService.ToolTip="Leave the Teams call">
<TextBlock Text="Leave"/>
</Button>
</StackPanel>
</Border>
<!-- ─── Status bar ─── -->
<Grid Grid.Row="4"
Padding="32,8,32,8"
Background="{ThemeResource BgCanvas}"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="0,1,0,0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center">
<Ellipse Width="6" Height="6"
Fill="{ThemeResource AccentCyanText}"
VerticalAlignment="Center"/>
<TextBlock Text="control surface · 127.0.0.1:9755"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource FgSecondary}"
VerticalAlignment="Center"/>
</StackPanel>
<TextBlock Grid.Column="2"
Text="F1 help · Ctrl+M marker · Ctrl+Shift+S panic stop · Ctrl+K command palette"
Style="{StaticResource TextMono}"
FontSize="11"
Foreground="{ThemeResource FgTertiary}"
VerticalAlignment="Center"/>
</Grid>
</Grid>
</Grid>
</Window>

View file

@ -1,4 +1,9 @@
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using TeamsISO.App.WinUI.Models;
using Windows.Graphics;
using Windows.UI;
namespace TeamsISO.App.WinUI.Views;
@ -7,6 +12,78 @@ public sealed partial class MainWindow : Window
public MainWindow()
{
InitializeComponent();
Title = "TeamsISO";
// ── Custom title bar wiring ───────────────────────────────────────
// ExtendsContentIntoTitleBar=true tells WindowsAppSDK to draw the
// window chrome over our content instead of reserving a Windows-default
// caption strip. SetTitleBar marks AppTitleBar as the drag region —
// clicks on it route to the system drag handler, everything else stays
// hit-testable as a normal XAML element. The system min/max/close
// buttons render on top of the right edge regardless; we just provide
// their colors so they match our palette.
ExtendsContentIntoTitleBar = true;
SetTitleBar(AppTitleBar);
AppWindow.TitleBar.ButtonBackgroundColor = Colors.Transparent;
AppWindow.TitleBar.ButtonInactiveBackgroundColor = Colors.Transparent;
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
AppWindow.TitleBar.ButtonHoverForegroundColor = Colors.White;
// ── Initial size & position ───────────────────────────────────────
// 1280x780 matches the WPF host's default — fits comfortably on a
// 14-inch laptop while giving the participants table 600+ pixels
// of vertical breathing room.
AppWindow.Resize(new SizeInt32(1280, 780));
// ── Mock data wiring (interim) ────────────────────────────────────
// Until ParticipantViewModel binds in the engine wiring commit, the
// table is populated from a static sample list so the visual design
// can be validated end-to-end against representative data.
ParticipantsRepeater.ItemsSource = MockParticipant.Sample();
}
/// <summary>
/// Cycle the active theme between Dark and Light. The actual swap target
/// is the window's root visual element — RequestedTheme on that element
/// propagates the {ThemeResource} swap across the whole tree, with no
/// flicker. The system title-bar buttons aren't part of the visual tree,
/// so their colors are recomputed inline.
///
/// Persistence to UIPreferences.Theme happens in the view-model wiring
/// commit (the WinUI host doesn't yet share UIPreferences with the WPF
/// host; both will once the WinUI host owns the .NET startup).
/// </summary>
private void OnThemeToggleClick(object sender, RoutedEventArgs e)
{
if (Content is FrameworkElement root)
{
var next = root.ActualTheme == ElementTheme.Dark
? ElementTheme.Light
: ElementTheme.Dark;
root.RequestedTheme = next;
// Title-bar system buttons read from AppWindow.TitleBar directly,
// not from the XAML resource dictionary, so we set them inline
// for both themes. Dark-mode chrome lands on near-white glyphs;
// light-mode chrome lands on near-black glyphs.
if (next == ElementTheme.Dark)
{
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0xF4, 0xF4, 0xF6);
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0x33, 0x34, 0x3A);
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0x3F, 0x3F, 0x47);
ThemeToggleIcon.Glyph = ""; // sun
}
else
{
AppWindow.TitleBar.ButtonForegroundColor = Color.FromArgb(0xFF, 0x0A, 0x0A, 0x0A);
AppWindow.TitleBar.ButtonHoverBackgroundColor = Color.FromArgb(0xFF, 0xEC, 0xEE, 0xF1);
AppWindow.TitleBar.ButtonPressedBackgroundColor = Color.FromArgb(0xFF, 0xE0, 0xE3, 0xE7);
ThemeToggleIcon.Glyph = ""; // moon
}
}
}
}