Screen backdrop (schema v5/v6) + five-scene catalog + webcam polish: live desktop/game capture as a permanent non-deletable bottom layer (Source.IsBackdrop), auto full-screen game detection (Win32FullScreenDetector) else primary display, GraphicsCapturePicker re-designation, refcounted shared ScreenCaptureManager, Live-only backdrop by policy (HasBackdrop + v5-v6 backfill + EnforceBackdropPolicy, checkbox gone), SceneCatalog (Starting/Live/BRB/Chat/Ending) with + button re-adding missing scenes, webcam mid-session transparent-container fix (GetPreviewBitmap propagation), Chat half-screen-area cap, 'Add Webcam' always opens the picker (SwapWebcamIdentityAsync, no silent resurrect), WindowsRuntimeMarshal frame-read + 5s-throttled errors, docs updated, tests (65 passing)

This commit is contained in:
2026-08-07 14:15:50 -07:00
parent e037ba027b
commit ad9b3e1e48
27 changed files with 2232 additions and 116 deletions
+77 -13
View File
@@ -13,6 +13,8 @@
WindowStartupLocation="CenterScreen"
Closing="MainWindow_Closing"
Loaded="MainWindow_Loaded"
Activated="MainWindow_Activated"
Deactivated="MainWindow_Deactivated"
PreviewMouseLeftButtonDown="Window_PreviewMouseLeftButtonDown">
<Window.DataContext>
@@ -130,11 +132,27 @@
<RowDefinition Height="180"/>
</Grid.RowDefinitions>
<!-- Scenes -->
<!-- Scenes: the app has five canonical scenes. Work with less,
never more — the "+" only offers the missing ones, and is
hidden entirely once all five are back. -->
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Text="SCENES" Style="{StaticResource SectionHeader}" VerticalAlignment="Center"/>
<Button ToolTip="Add Scene" Command="{Binding AddSceneCommand}"
Style="{StaticResource IconButton}" Margin="4,-4,0,0">
<Button ToolTip="Add Missing Scene" Click="AddSceneButton_Click"
Style="{StaticResource IconButton}" Margin="4,-4,0,0"
Visibility="{Binding ShowAddScene, Converter={StaticResource BoolToVis}}">
<Button.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}"
ItemsSource="{Binding MissingScenes}">
<ContextMenu.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding}"/>
<Setter Property="Command"
Value="{Binding DataContext.AddSceneCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
</ContextMenu.ItemContainerStyle>
</ContextMenu>
</Button.ContextMenu>
<Path Data="M12,4 L12,20 M4,12 L20,12" Stroke="#e94560" StrokeThickness="2"
Width="14" Height="14" Stretch="Uniform"/>
</Button>
@@ -265,12 +283,25 @@
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Change Capture…" Click="BackdropMenu_ChangeCapture"
Visibility="{Binding IsBackdrop, Converter={StaticResource BoolToVis}}"/>
<MenuItem Header="Refresh Capture" Click="BackdropMenu_RefreshCapture"
Visibility="{Binding IsBackdrop, Converter={StaticResource BoolToVis}}"/>
<Separator Visibility="{Binding IsBackdrop, Converter={StaticResource BoolToVis}}"/>
<MenuItem Header="Remove Source" Click="SourceMenu_Remove"
Visibility="{Binding IsBackdrop, Converter={StaticResource InverseBoolToVis}}"/>
</ContextMenu>
</Grid.ContextMenu>
<TextBlock Grid.Column="0" Text="{Binding Name}" VerticalAlignment="Center"
Padding="4,2" TextTrimming="CharacterEllipsis"/>
<Button Grid.Column="1" ToolTip="Remove Source"
Command="{Binding DataContext.RemoveSourceCommand, RelativeSource={RelativeSource AncestorType=ListBox}}"
CommandParameter="{Binding}" Style="{StaticResource IconButton}">
CommandParameter="{Binding}" Style="{StaticResource IconButton}"
Visibility="{Binding IsBackdrop, Converter={StaticResource InverseBoolToVis}}">
<Path Data="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"
Fill="#d0d0d0" Width="13" Height="13" Stretch="Uniform"/>
</Button>
@@ -305,10 +336,31 @@
<Viewbox Stretch="Uniform">
<Grid x:Name="CanvasGrid" Width="1920" Height="1080" ClipToBounds="True" Background="Transparent">
<Grid.ContextMenu>
<ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="Show Webcam" Command="{Binding ShowWebcamCommand}"/>
<MenuItem Header="Capture Display"
Visibility="{Binding CanChangeBackdrop, Converter={StaticResource BoolToVis}}"
ItemsSource="{Binding Displays}">
<MenuItem.ItemContainerStyle>
<Style TargetType="MenuItem">
<Setter Property="Header" Value="{Binding Label}"/>
<Setter Property="Command"
Value="{Binding DataContext.SetBackdropDisplayCommand, RelativeSource={RelativeSource AncestorType=ContextMenu}}"/>
<Setter Property="CommandParameter" Value="{Binding}"/>
</Style>
</MenuItem.ItemContainerStyle>
</MenuItem>
<MenuItem Header="Change Capture…" Command="{Binding ChangeCaptureCommand}"
Visibility="{Binding CanChangeBackdrop, Converter={StaticResource BoolToVis}}"
ToolTip="Pick any window with the OS picker"/>
<MenuItem Header="Refresh Capture" Command="{Binding RefreshCaptureCommand}"
Visibility="{Binding CanChangeBackdrop, Converter={StaticResource BoolToVis}}"/>
</ContextMenu>
</Grid.ContextMenu>
<!-- Live desktop/game backdrop (bottom layer; the static
background renders above it). -->
<Image Source="{Binding BackdropImage}" Stretch="UniformToFill"
IsHitTestVisible="False"/>
<Image Source="{Binding ActiveBackgroundImage}" Stretch="UniformToFill"
IsHitTestVisible="False"/>
<Rectangle Stroke="#22c55e" StrokeThickness="6" IsHitTestVisible="False"/>
@@ -339,6 +391,12 @@
<DataTrigger Binding="{Binding IsVisible}" Value="True">
<Setter Property="Visibility" Value="Visible"/>
</DataTrigger>
<!-- The backdrop is a fixed full-frame layer (rendered separately,
below the static background): never hit-testable, so it can't be
selected, dragged, or bring up the webcam context menu. -->
<DataTrigger Binding="{Binding IsBackdrop}" Value="True">
<Setter Property="IsHitTestVisible" Value="False"/>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
@@ -368,9 +426,15 @@
</Image>
<Viewbox Stretch="Uniform">
<Grid Width="1" Height="1">
<Ellipse>
<Ellipse.Style>
<Style TargetType="Ellipse">
<!-- Round webcam: a plain Image (the same efficient WriteableBitmap
path as Traditional) clipped to a unit circle and scaled by the
Viewbox — deliberately NOT an ImageBrush, whose per-frame
re-rasterization at full scale stalls the preview. -->
<Image Stretch="UniformToFill"
Source="{Binding DisplaySource}"
RenderOptions.BitmapScalingMode="HighQuality">
<Image.Style>
<Style TargetType="Image">
<Setter Property="Visibility" Value="Collapsed"/>
<Style.Triggers>
<MultiDataTrigger>
@@ -382,11 +446,11 @@
</MultiDataTrigger>
</Style.Triggers>
</Style>
</Ellipse.Style>
<Ellipse.Fill>
<ImageBrush ImageSource="{Binding DisplaySource}" Stretch="UniformToFill"/>
</Ellipse.Fill>
</Ellipse>
</Image.Style>
<Image.Clip>
<EllipseGeometry Center="0.5,0.5" RadiusX="0.5" RadiusY="0.5"/>
</Image.Clip>
</Image>
</Grid>
</Viewbox>