Files
LlamaCasty/Controls/TopBar.xaml
T
gramps 6b4aa99863 feat(topbar): the segmented mode switcher, to the mock
Implements the creator's mock (2026-11-12) for the world segment: pill track (#1E222B, radius 12,
padding 4) holding two equal halves; the active half sits on a lighter pill (#2E3440, radius 8) with
#E2E8F0 bold text and a coral (#F26D6D) icon; the inactive half is #8892A4 on a one-alpha black face so
the whole area hit-tests; hover tints #252A34 on the inactive half only, so the active one never looks
clickable-but-dim. Labels are "Build scenes" / "Record / go live".

Two deliberate deviations from the pasted recipe, both to honour this repo:
- State is the existing BarWorld binding plus a command, not a code-behind SetActiveMode — one source of
  truth, and the change still raises the same property-changed notification a mode event would.
- Styles live in Themes/Controls.xaml (SegmentHalf, SegmentIcon) with colours as tokens in Tokens.xaml,
  rather than inline properties on the elements: AGENTS requires shared styles, and one DataTrigger on
  each element's own Tag serves both halves.

DesignTokensTests caught the one raw literal (#01000000) and it is now Brush.SegmentHitArea, which is the
rule working as intended.

680 pass, 0 warnings. Scope justification: Assets/starting-background.jpg is outside this unit — the
creator's replacement art, still awaiting their visual check.
2026-11-12 16:20:25 -08:00

200 lines
14 KiB
XML

<UserControl x:Class="ytLive.Controls.TopBar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:helpers="clr-namespace:ytLive.Helpers"
x:Name="TopBarRoot">
<Grid>
<Grid.ColumnDefinitions>
<!-- 352 = the layers panel's right edge in the work area below: window margin 8 +
360px column = 368, minus this bar's own 16px padding. EDIT is right-aligned in
this column so the two edges line up exactly (creator ruling 2026-11-01). -->
<ColumnDefinition Width="352"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- BRAND + EDIT. The gear moved to the preview's right rail, first tab (2026-11-01):
app-level actions live with the other pull-outs now. -->
<Grid Grid.Column="0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<!-- LOGO / BRAND — left-click opens the in-app About overlay; right-click opens the app
menu (Report Bug / Feature Request / About). App Settings is no longer in this
menu: the gear rail tab opens the Settings drawer directly (creator ruling
2026-11-01). -->
<Button Grid.Column="0" Background="Transparent" BorderThickness="0" Cursor="Hand"
HorizontalAlignment="Left"
VerticalAlignment="Center" ToolTip="About LlamaCasty — right-click for the menu"
Command="{Binding OpenAboutCommand}"
PreviewMouseRightButtonDown="BrandWordmark_PreviewMouseRightButtonDown">
<Button.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
<Button.Template>
<ControlTemplate TargetType="Button">
<ContentPresenter/>
</ControlTemplate>
</Button.Template>
<!-- Wordmark: "LlamaCas" + "ty" in YouTube red (creator ruling 2026-10-30 — never
re-colour it). The Run elements MUST stay adjacent: whitespace between inline
Runs renders as a space and splits the name into "LlamaCas ty". -->
<TextBlock FontSize="22" FontWeight="Bold" VerticalAlignment="Center">
<Run Text="LlamaCas" Foreground="{StaticResource Brush.OnSurface}"/><Run Text="ty" Foreground="{StaticResource Brush.Youtube}"/><Run
x:Name="BuildIdStamp" Text="{x:Static helpers:BuildStamp.Label}" FontSize="9"
BaselineAlignment="Superscript" Foreground="{StaticResource Brush.OnSurfaceVariant}"
ToolTipService.ToolTip="{x:Static helpers:BuildStamp.Label}"/><Run Text=" " FontSize="4"/>
</TextBlock>
</Button>
</Grid>
<!-- Session surface (top bar redesign): the bar is ONE surface rendered by
the FIRST decision — Record or Stream. Each world is the whole bar; the
other world never exists on screen. While running, the bar drops to a
reality line + End so nothing mid-session can be re-armed. -->
<StackPanel Grid.Column="1" Orientation="Horizontal"
HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- THE decision (creator ruling 2026-11-12): [ Build Scenes ][ Record / Go Live ].
Build Scenes is the new edit mode — there is no separate EDIT button, so the
question "which world am I in?" and "can I edit?" cannot disagree. Clicking the
highlighted half re-enters it, which is how a creator gets back to the layers
during a session. -->
<!-- The segmented mode switcher (creator mock 2026-11-12): pill track, two equal halves,
active half on a lighter pill with off-white bold text and a coral icon. Hover tints
only the inactive half. -->
<Border Background="{StaticResource Brush.SegmentTrack}" CornerRadius="12" Padding="4"
VerticalAlignment="Center" Margin="0,0,12,0"
Visibility="{Binding IsBuildWorld, Converter={StaticResource BoolToVis}}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Button Grid.Column="0" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsBuildWorld}" Command="{Binding EnterBuildWorldCommand}"
ToolTip="Edit layers, scenes and webcam placement">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsBuildWorld}"
Data="M3,4h8v6H3V4zM13,4h8v6h-8V4zM3,12h8v8H3V12zM13,12h8v8h-8V12z"/>
<TextBlock Text="Build scenes" FontSize="12" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
<Button Grid.Column="1" Style="{StaticResource SegmentHalf}"
Tag="{Binding IsOutputWorld}" Command="{Binding EnterOutputWorldCommand}"
ToolTip="Choose the output and start the session">
<StackPanel Orientation="Horizontal">
<Path Style="{StaticResource SegmentIcon}" Tag="{Binding IsOutputWorld}"
Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Record / go live" FontSize="12" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=Button}}"/>
</StackPanel>
</Button>
</Grid>
</Border>
<!-- IDLE: the mode switch + the action(s) of the armed world. The three
controls share one height (build 77 feedback) so they read as one
family — segments host, Start/Go Live, and Test all sit at 32. -->
<!-- THE OUTPUT LINE (creator ruling 2026-11-12): which single output. Nothing else
belongs in it — the action is not part of this row. -->
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"
Visibility="{Binding ShowOutputStage, Converter={StaticResource BoolToVis}}">
<TextBlock Text="Output:" FontSize="11" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,0,8,0"/>
<StackPanel Orientation="Vertical" VerticalAlignment="Center">
<Border Background="{StaticResource Brush.SurfaceContainerLowest}" CornerRadius="4" Padding="2" Height="32" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<ToggleButton Style="{StaticResource RecordPillToggle}"
IsChecked="{Binding OnAirPillOn}">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Data="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
<TextBlock Text="YouTube" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
</StackPanel>
</ToggleButton>
<ToggleButton Style="{StaticResource RecordPillToggle}"
IsChecked="{Binding RecordPillOn}">
<StackPanel Orientation="Horizontal">
<Path Width="14" Height="14" Stretch="Uniform" VerticalAlignment="Center"
Fill="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"
Data="M12,2a10,10 0 1,0 0,20a10,10 0 1,0 0,-20zM12,4a8,8 0 1,1 0,16a8,8 0 1,1 0,-16zM12,7.5a4.5,4.5 0 1,1 0,9a4.5,4.5 0 1,1 0,-9z"/>
<TextBlock Text="Local MP4" FontSize="12" FontWeight="Bold" Margin="6,0,0,0"
VerticalAlignment="Center"
Foreground="{Binding Foreground, RelativeSource={RelativeSource AncestorType=ToggleButton}}"/>
</StackPanel>
</ToggleButton>
</StackPanel>
</Border>
<!-- Esc is the way back now that the chevron is gone (creator ruling 2026-11-12). -->
<TextBlock Text="press &lt;esc&gt; to return" FontSize="10" HorizontalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="0,2,0,0"/>
</StackPanel>
<TextBlock Text="One output at a time" FontSize="11" VerticalAlignment="Center"
Foreground="{StaticResource Brush.OnSurfaceVariant}" Margin="8,0,0,0"/>
</StackPanel>
<!-- RUNNING: one reality line (dot + word + elapsed) and the stop -->
<StackPanel Orientation="Horizontal"
Visibility="{Binding RunningVisible, Converter={StaticResource BoolToVis}}">
<Ellipse Width="12" Height="12" VerticalAlignment="Center"
Fill="{Binding RunningDotBrush}"/>
<TextBlock Text="{Binding RunningText}" FontSize="16" FontWeight="Bold"
Foreground="White" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBlock Text="{Binding LiveElapsedText}" Foreground="White" FontSize="14"
FontFamily="Consolas" VerticalAlignment="Center" Margin="8,0,0,0"/>
<!-- TASK 32·2: measured facts only — elapsed seconds and which attempt. No countdown:
YouTube's cutoff is not a constant we can promise. -->
<TextBlock x:Name="ReconnectDetail" Text="{Binding ReconnectDetailText}"
Foreground="{StaticResource Brush.Attention}" FontSize="13"
FontFamily="Consolas" VerticalAlignment="Center" Margin="10,0,0,0"
Visibility="{Binding IsReconnecting, Converter={StaticResource BoolToVis}}"/>
<Button Content="{Binding EndButtonText}" Style="{StaticResource YtButton}"
Background="{StaticResource Brush.Primary}" Margin="14,0,0,0"
Command="{Binding EndStreamCommand}"/>
</StackPanel>
</StackPanel>
<!-- Account zone: lives only in the Stream world. Signed out → Sign In;
signed in → a big avatar of the streaming account (build 77 feedback:
"make the user icon much bigger"). Record world has no YouTube
identity at all. -->
<StackPanel Grid.Column="2" Orientation="Horizontal" VerticalAlignment="Center">
<Button Content="Sign In" Style="{StaticResource YtButtonSecondary}"
Margin="0,0,8,0" Command="{Binding SignInCommand}"
ToolTip="Sign in to YouTube to go live."
Visibility="{Binding ShowSignInButton, Converter={StaticResource BoolToVis}}"/>
<Button Style="{StaticResource IconButton}" Margin="0,0,0,0"
ToolTip="{Binding AccountDisplayName}"
Visibility="{Binding ShowAvatarButton, Converter={StaticResource BoolToVis}}">
<Button.ContextMenu>
<ContextMenu>
<MenuItem Header="Change Account" Command="{Binding ChangeAccountCommand}"/>
<Separator/>
<MenuItem Header="Logout" Command="{Binding LogoutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
<Border Width="40" Height="40" CornerRadius="20" Background="{StaticResource Brush.SurfaceContainer}"
ClipToBounds="True" BorderBrush="{StaticResource Brush.Outline}" BorderThickness="1">
<Grid>
<Image x:Name="AvatarImage" Stretch="UniformToFill"/>
<TextBlock x:Name="AvatarInitial" Foreground="{StaticResource Brush.OnSurface}" FontSize="20"
FontWeight="Bold" HorizontalAlignment="Center" VerticalAlignment="Center"
Visibility="Collapsed"/>
</Grid>
</Border>
</Button>
</StackPanel>
</Grid>
</UserControl>