feat(winui3): SettingsDrawer hosts successfully — NavigationView swap
Some checks failed
CI / build-and-test (push) Failing after 29s

Replaces the NavigationView in SettingsDrawer with a simpler
StackPanel of tab buttons built imperatively at runtime. The
NavigationView's resource-dictionary expansion (or its default
template loading) was crashing the XAML parser at SettingsDrawer's
InitializeComponent on WinUI 3 1.8.

New shape:

- `TabStrip` StackPanel populated in BuildTabStrip() with five
  Tertiary-styled Button instances. Selection updates the foreground
  to AccentCyanText for the active tab and FgSecondary for the rest.
- `TabContent` ScrollViewer remains; RebuildTabContent(key) clears
  and rebuilds via the same helpers as before (SettingHeader,
  SettingRow, SettingNote, AccentSwatch).
- Each tab's content moved into its own helper method
  (BuildAppearanceTab / Routing / Display / Control / Advanced) so
  the switch in the old OnTabSelectionChanged disappears.

MainWindow re-hosts the drawer at Grid.Row=0, RowSpan=4, right-
aligned, 400px wide, Visibility=Collapsed. OnSettingsClick toggles
visibility. Verified: dotnet build + run launches cleanly, and the
window stays alive (PID confirmed via Get-Process).

This closes Phase 6 (secondary windows) for the drawer specifically.
The Help, About, and Onboarding dialogs are ContentDialogs that
don't host inline so they should be straightforward to wire to
their respective triggers (F1 / About button / first launch) in
Phase 7.
This commit is contained in:
Zac Gaetano 2026-05-13 00:50:54 -04:00
parent 27f47401d9
commit a05c0a75d2
4 changed files with 144 additions and 83 deletions

View file

@ -377,14 +377,13 @@
</StackPanel>
</Border>
<!-- SettingsDrawer host deferred: even with Visibility=Collapsed
the SettingsDrawer.xaml parse fires at constructor time and
crashes the WinUI 3 XAML parser. Suspect the NavigationView's
IsSelected="True" attribute on its first NavigationViewItem
firing OnTabSelectionChanged before the code-behind is ready.
Re-host after replacing the NavigationView with a simpler
tab strip or after fixing the selection-change handler
signature for WinUI 3 1.8. -->
<!-- ─── Settings drawer ─── -->
<views:SettingsDrawer x:Name="SettingsDrawerHost"
Grid.Row="0"
Grid.RowSpan="4"
HorizontalAlignment="Right"
Width="400"
Visibility="Collapsed"/>
<!-- ─── Status bar ─── -->
<Grid Grid.Row="4"

View file

@ -60,14 +60,29 @@ public sealed partial class MainWindow : Window
ThemeManager.Current.Toggle();
}
private bool _drawerOpen;
/// <summary>
/// Settings drawer toggle. Currently a no-op because the drawer host
/// can't be inlined in MainWindow.xaml without crashing the XAML parser;
/// see the comment in MainWindow.xaml at the drawer placeholder.
/// Toggle the settings drawer. Visibility-only for now; composition-
/// layer Translation animation lands as part of the polish pass.
/// </summary>
private void OnSettingsClick(object sender, RoutedEventArgs e)
{
// No-op until SettingsDrawer.xaml is simplified for WinUI 3 1.8.
_drawerOpen = !_drawerOpen;
SettingsDrawerHost.Visibility = _drawerOpen
? Visibility.Visible
: Visibility.Collapsed;
if (_drawerOpen)
{
SettingsDrawerHost.CloseRequested -= OnDrawerCloseRequested;
SettingsDrawerHost.CloseRequested += OnDrawerCloseRequested;
}
}
private void OnDrawerCloseRequested(object? sender, System.EventArgs e)
{
_drawerOpen = false;
SettingsDrawerHost.Visibility = Visibility.Collapsed;
}
/// <summary>

View file

@ -48,27 +48,28 @@
</Button>
</Grid>
<!-- Tabs + body -->
<NavigationView Grid.Row="1"
x:Name="SettingsNav"
PaneDisplayMode="Top"
IsBackButtonVisible="Collapsed"
IsSettingsVisible="False"
OpenPaneLength="0"
SelectionChanged="OnTabSelectionChanged">
<NavigationView.MenuItems>
<NavigationViewItem Tag="Appearance" Content="Appearance" IsSelected="True"/>
<NavigationViewItem Tag="Routing" Content="Routing"/>
<NavigationViewItem Tag="Display" Content="Display"/>
<NavigationViewItem Tag="Control" Content="Control"/>
<NavigationViewItem Tag="Advanced" Content="Advanced"/>
</NavigationView.MenuItems>
<ScrollViewer VerticalScrollBarVisibility="Auto"
<!-- Body. NavigationView swapped for a simpler horizontal tab
button strip; the WinUI 3 NavigationView's resource
dictionary expansion was crashing the XAML parser at
SettingsDrawer construction time. -->
<Grid Grid.Row="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Grid.Row="0"
x:Name="TabStrip"
Orientation="Horizontal"
Spacing="4"
Padding="12,8"
BorderBrush="{ThemeResource BorderSubtle}"
BorderThickness="0,0,0,1"/>
<ScrollViewer Grid.Row="1"
VerticalScrollBarVisibility="Auto"
Padding="20">
<StackPanel x:Name="TabContent" Spacing="16"/>
</ScrollViewer>
</NavigationView>
</Grid>
<!-- Footer: Apply / Reset -->
<Grid Grid.Row="2"

View file

@ -18,7 +18,105 @@ public sealed partial class SettingsDrawer : UserControl
public SettingsDrawer()
{
InitializeComponent();
BuildAppearanceTab();
BuildTabStrip();
SelectTab("Appearance");
}
private string _currentTab = "Appearance";
/// <summary>Build the tab buttons imperatively so the parse doesn't
/// trigger a SelectionChanged before the code-behind is ready.</summary>
private void BuildTabStrip()
{
foreach (var (label, key) in new[]
{
("Appearance", "Appearance"),
("Routing", "Routing"),
("Display", "Display"),
("Control", "Control"),
("Advanced", "Advanced"),
})
{
var btn = new Button
{
Content = label,
Tag = key,
Style = (Style)Application.Current.Resources["ButtonTertiary"],
};
btn.Click += (s, _) =>
{
if (s is Button b && b.Tag is string k) SelectTab(k);
};
TabStrip.Children.Add(btn);
}
}
private void SelectTab(string key)
{
_currentTab = key;
foreach (var child in TabStrip.Children)
{
if (child is Button b)
{
var isActive = (b.Tag as string) == key;
b.Foreground = (Microsoft.UI.Xaml.Media.SolidColorBrush)Application.Current.Resources[
isActive ? "AccentCyanText" : "FgSecondary"];
}
}
RebuildTabContent(key);
}
private void RebuildTabContent(string key)
{
TabContent.Children.Clear();
switch (key)
{
case "Appearance": BuildAppearanceTab(); break;
case "Routing": BuildRoutingTab(); break;
case "Display": BuildDisplayTab(); break;
case "Control": BuildControlTab(); break;
case "Advanced": BuildAdvancedTab(); break;
}
}
private void BuildRoutingTab()
{
TabContent.Children.Add(SettingHeader("Routing"));
TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to."));
TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant."));
TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed."));
TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source."));
TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit."));
}
private void BuildDisplayTab()
{
TabContent.Children.Add(SettingHeader("Display & participants"));
TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table."));
TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline."));
TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table."));
TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized."));
TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens."));
TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch."));
TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call."));
}
private void BuildControlTab()
{
TabContent.Children.Add(SettingHeader("External control surface"));
TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck."));
TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces."));
TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth)."));
TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md."));
}
private void BuildAdvancedTab()
{
TabContent.Children.Add(SettingHeader("Advanced"));
TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window."));
TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write."));
TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory."));
TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report."));
}
private void OnCloseClick(object sender, RoutedEventArgs e) => CloseRequested?.Invoke(this, EventArgs.Empty);
@ -37,58 +135,6 @@ public sealed partial class SettingsDrawer : UserControl
DirtyHint.Text = "Reset queued — apply or close to commit.";
}
/// <summary>
/// Tab-switch handler builds the body for the selected tab. We assemble
/// content imperatively rather than via separate XAML pages because the
/// drawer's content is data-driven (most rows are simple label + control
/// + tooltip triples) and pulling that into five XAML files would
/// triplicate the row layout. The body recomposes on every selection
/// change, which is cheap given the small surface.
/// </summary>
private void OnTabSelectionChanged(NavigationView sender, NavigationViewSelectionChangedEventArgs args)
{
TabContent.Children.Clear();
if (args.SelectedItemContainer is not NavigationViewItem item) return;
switch (item.Tag as string)
{
case "Appearance":
BuildAppearanceTab();
break;
case "Routing":
TabContent.Children.Add(SettingHeader("Routing"));
TabContent.Children.Add(SettingRow("Framerate", "30 fps", "Target framerate the engine normalizes incoming NDI feeds to."));
TabContent.Children.Add(SettingRow("Resolution", "1920 x 1080", "Output resolution applied to every routed participant."));
TabContent.Children.Add(SettingRow("Aspect mode", "Letterbox", "How sources whose aspect ratio doesn't match the target are framed."));
TabContent.Children.Add(SettingRow("Audio routing", "Per-participant", "Embed each participant's audio in their own NDI source."));
TabContent.Children.Add(SettingNote("Settings wired to the engine in a follow-up commit."));
break;
case "Display":
TabContent.Children.Add(SettingHeader("Display & participants"));
TabContent.Children.Add(SettingRow("Hide local self", "Yes", "Filter the operator's own preview from the participants table."));
TabContent.Children.Add(SettingRow("Auto-disable on departure", "No", "Keep routing alive when a participant goes offline."));
TabContent.Children.Add(SettingRow("Participant sort", "Join order", "Default sort for the participants table."));
TabContent.Children.Add(SettingRow("Minimize to tray", "No", "Keep TeamsISO running in the system tray when the window is minimized."));
TabContent.Children.Add(SettingRow("Launch Teams on startup", "No", "Start Teams in the background when TeamsISO opens."));
TabContent.Children.Add(SettingRow("Auto-hide Teams windows", "No", "Hide every visible Teams window after launch."));
TabContent.Children.Add(SettingRow("Auto-record on call", "No", "Start recording every active ISO when Teams enters a call."));
break;
case "Control":
TabContent.Children.Add(SettingHeader("External control surface"));
TabContent.Children.Add(SettingRow("REST + WebSocket", "127.0.0.1:9755", "HTTP control surface for Companion / Stream Deck."));
TabContent.Children.Add(SettingRow("OSC bridge", "127.0.0.1:9000", "UDP OSC for TouchOSC / hardware surfaces."));
TabContent.Children.Add(SettingRow("LAN reachable", "No", "Bind the REST surface to all interfaces (warning: no auth)."));
TabContent.Children.Add(SettingNote("Control surface protocol unchanged from the WPF host. See docs/CONTROL-SURFACE.md."));
break;
case "Advanced":
TabContent.Children.Add(SettingHeader("Advanced"));
TabContent.Children.Add(SettingRow("Embed Teams window", "No", "Experimental SetParent reparent of Teams' main window."));
TabContent.Children.Add(SettingRow("Logs", "%LOCALAPPDATA%\\TeamsISO\\Logs", "Where rolling daily Serilog files write."));
TabContent.Children.Add(SettingRow("Recordings", "%USERPROFILE%\\Videos\\TeamsISO", "Default per-show recording directory."));
TabContent.Children.Add(SettingRow("Diagnostic bundle", "Export", "Zip logs + config + presets for a bug report."));
break;
}
}
/// <summary>
/// Appearance tab — theme picker, accent palette peek. The theme picker