feat(ui): gear to the rail, EDIT on the panel edge, preview undo/redo

Three creator directives (2026-11-01), done ahead of the surge.

**Gear moved to the preview rail** — first tab, orange (Brush.Attention), gear icon
only. Left-click opens the same app menu the top bar gear owned (Settings / Bug /
Feature / About), right-click goes straight to App Settings; both behaviours moved
with it, so nothing lost an entry point. TopBar keeps the wordmark and EDIT.

**EDIT right edge aligned to the layers panel.** The work area is margin 8 + a 360px
column, the top bar is padding 16, so the shared edge is 352 inside the bar. TopBar
column 0 is now a fixed 352 grid with EDIT right-aligned in it — alignment by
arithmetic instead of by eye.

**Preview undo/redo: Ctrl+Z / Ctrl+Shift+Z.** Models/PreviewHistory snapshots element
REFERENCES plus geometry, visibility, order and membership — a deep clone would mirror
every property of a dozen element subclasses and quietly miss the next one added.
One snapshot per gesture (pushed at drag/resize start, never in the move loop), bounded
at 50, redo survives multiple undos, cleared on layout load. Gated on EditingActive:
undo while the preview shows program output would rewrite a scene nobody is looking at.

Honest scope, in the type's doc comment: layout edits only. Colour/opacity/border/font
edits are not captured yet, so Ctrl+Z after one of those restores the last layout change.

6 tests in PreviewHistoryTests; 19 pass across it and the neighbouring UI classes; Debug
and Release both build at 0 warnings.
This commit is contained in:
2026-11-01 07:46:10 -08:00
parent bd669f2e73
commit fdb27f0758
14 changed files with 403 additions and 55 deletions
+22
View File
@@ -883,6 +883,28 @@
</Border>
<!-- The stacked white tabs -->
<StackPanel Orientation="Vertical" HorizontalAlignment="Right" VerticalAlignment="Stretch">
<!-- Settings: the first tab, orange, gear only (creator ruling 2026-11-01 —
moved off the top bar). Left-click opens the same app menu
(Settings / Bug / Feature / About); right-click goes straight
to App Settings, matching the old gear's behaviour exactly. -->
<Border Width="30" VerticalAlignment="Center" Background="{StaticResource Brush.Attention}"
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
ToolTip="App menu — right-click for Settings"
MouseLeftButtonUp="SettingsTab_Click"
PreviewMouseRightButtonDown="SettingsTab_PreviewMouseRightButtonDown">
<Border.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="App Settings" Command="{Binding OpenSettingsCommand}"/>
<Separator/>
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
</ContextMenu>
</Border.ContextMenu>
<Path Data="M19.43,12.98c0.04,-0.32 0.07,-0.65 0.07,-0.98c0,-0.33 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.38,-0.31 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98c0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.38,0.31 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64L19.43,12.98zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5c0,-1.93 1.57,-3.5 3.5,-3.5c1.93,0 3.5,1.57 3.5,3.5C15.5,13.93 13.93,15.5 12,15.5z"
Fill="{StaticResource Brush.Background}" Width="18" Height="18" Stretch="Uniform"
HorizontalAlignment="Center"/>
</Border>
<Border Width="30" VerticalAlignment="Center" Background="{StaticResource Brush.Youtube}"
CornerRadius="4,0,0,4" Padding="0,10" Cursor="Hand" Margin="0,4,0,0"
ToolTip="Broadcast text fields">
+23
View File
@@ -459,6 +459,7 @@ public partial class PreviewPane : UserControl
if (selected is { } sel && IsDraggableElement(sel) && HitHandle(e.GetPosition(grid), sel))
{
vm.PushPreviewUndo();
_isResizing = true;
_resizeAspect = sel.ClipShape == ClipShape.Round ? 1 : sel.Width / Math.Max(1, sel.Height);
grid.CaptureMouse();
@@ -470,6 +471,9 @@ public partial class PreviewPane : UserControl
if (hit != null)
{
vm.SelectedElement = hit;
// One snapshot per gesture: pushing inside the move loop would fill the stack with
// a hundred one-pixel steps.
vm.PushPreviewUndo();
_isDraggingOverlay = true;
_grabOffset = new Point(p.X - hit.X, p.Y - hit.Y);
grid.CaptureMouse();
@@ -689,4 +693,23 @@ public partial class PreviewPane : UserControl
ZoomVisualBrush.Visual = null;
}
}
/// <summary>Rail settings tab (creator ruling 2026-11-01): left-click opens the same app menu
/// the top-bar gear used to own, so Bug / Feature / About keep their only entry point.</summary>
private void SettingsTab_Click(object sender, MouseButtonEventArgs e)
{
if (sender is not FrameworkElement { ContextMenu: { } menu } anchor) return;
menu.PlacementTarget = anchor;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Left;
menu.IsOpen = true;
e.Handled = true;
}
/// <summary>Right-click goes straight to App Settings — the same shortcut the old gear had.</summary>
private void SettingsTab_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
{
e.Handled = true;
if (DataContext is MainViewModel { OpenSettingsCommand: { } open } && open.CanExecute(null))
open.Execute(null);
}
}
+21 -27
View File
@@ -5,16 +5,25 @@
x:Name="TopBarRoot">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<!-- 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>
<!-- LOGO / BRAND — clicking opens the in-app About overlay; the gear sits
a couple characters past the build stamp and opens App Settings. -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center"
Margin="0,0,24,0">
<Button Background="Transparent" BorderThickness="0" Cursor="Hand"
<!-- 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 — clicking opens the in-app About overlay. -->
<Button Grid.Column="0" Background="Transparent" BorderThickness="0" Cursor="Hand"
HorizontalAlignment="Left"
VerticalAlignment="Center" ToolTip="About LlamaCasty"
Command="{Binding OpenAboutCommand}">
<Button.Template>
@@ -32,28 +41,13 @@
ToolTipService.ToolTip="{x:Static helpers:BuildStamp.Label}"/><Run Text=" " FontSize="4"/>
</TextBlock>
</Button>
<!-- Settings gear (user directive): a couple characters beyond the brand,
nudged right a couple more places 2026-09-22 (build 77 feedback).
Moved up from the bottom bar — one menu: App Settings / Bug / Feature / About. -->
<Button Style="{StaticResource IconButton}" Margin="18,0,0,2" VerticalAlignment="Center"
ToolTip="Menu" Click="GearButton_Click" PreviewMouseRightButtonDown="GearButton_PreviewMouseRightButtonDown">
<Button.ContextMenu>
<ContextMenu DataContext="{Binding PlacementTarget.DataContext, RelativeSource={RelativeSource Self}}">
<MenuItem Header="App Settings" Command="{Binding OpenSettingsCommand}"/>
<Separator/>
<MenuItem Header="Report Bug" Command="{Binding OpenBugCommand}"/>
<MenuItem Header="Feature Request" Command="{Binding OpenFeatureCommand}"/>
<MenuItem Header="About" Command="{Binding OpenAboutCommand}"/>
</ContextMenu>
</Button.ContextMenu>
<Path Data="M19.43,12.98c0.04,-0.32 0.07,-0.65 0.07,-0.98c0,-0.33 -0.03,-0.66 -0.07,-0.98l2.11,-1.65c0.19,-0.15 0.24,-0.42 0.12,-0.64l-2,-3.46c-0.12,-0.22 -0.38,-0.31 -0.61,-0.22l-2.49,1c-0.52,-0.4 -1.08,-0.73 -1.69,-0.98l-0.38,-2.65C14.46,2.18 14.25,2 14,2h-4c-0.25,0 -0.46,0.18 -0.49,0.42l-0.38,2.65c-0.61,0.25 -1.17,0.59 -1.69,0.98l-2.49,-1c-0.23,-0.09 -0.49,0 -0.61,0.22l-2,3.46c-0.13,0.22 -0.07,0.49 0.12,0.64l2.11,1.65c-0.04,0.32 -0.07,0.65 -0.07,0.98c0,0.33 0.03,0.66 0.07,0.98l-2.11,1.65c-0.19,0.15 -0.24,0.42 -0.12,0.64l2,3.46c0.12,0.22 0.38,0.31 0.61,0.22l2.49,-1c0.52,0.4 1.08,0.73 1.69,0.98l0.38,2.65c0.03,0.24 0.24,0.42 0.49,0.42h4c0.25,0 0.46,-0.18 0.49,-0.42l0.38,-2.65c0.61,-0.25 1.17,-0.59 1.69,-0.98l2.49,1c0.23,0.09 0.49,0 0.61,-0.22l2,-3.46c0.12,-0.22 0.07,-0.49 -0.12,-0.64L19.43,12.98zM12,15.5c-1.93,0 -3.5,-1.57 -3.5,-3.5c0,-1.93 1.57,-3.5 3.5,-3.5c1.93,0 3.5,1.57 3.5,3.5C15.5,13.93 13.93,15.5 12,15.5z"
Fill="{StaticResource Brush.Attention}" Width="18" Height="18" Stretch="Uniform"/>
</Button>
<!-- EDIT: explicit toggle. Neutral = off (chat), blue = on (layers/properties). The
trigger used to set the SAME colour as the resting state, so the button never
looked pressed. -->
<ToggleButton IsChecked="{Binding EditingActive, Mode=TwoWay}" Content="EDIT"
Cursor="Hand" Height="32" Padding="14,0" Margin="18,0,0,2" VerticalAlignment="Center"
looked pressed. Right edge aligns with the layers panel below. -->
<ToggleButton Grid.Column="1" IsChecked="{Binding EditingActive, Mode=TwoWay}" Content="EDIT"
Cursor="Hand" Height="32" Padding="14,0" Margin="18,0,0,2"
HorizontalAlignment="Right" VerticalAlignment="Center"
FontWeight="Bold" FontSize="12" Foreground="{StaticResource Brush.OnAccent}" BorderThickness="0">
<ToggleButton.Template>
<ControlTemplate TargetType="ToggleButton">
@@ -68,7 +62,7 @@
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>
</StackPanel>
</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
-20
View File
@@ -26,26 +26,6 @@ public partial class TopBar : UserControl
_ = LoadAvatarImageAsync();
}
private void GearButton_PreviewMouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
// Right-click goes straight to App Settings; the menu stays on left-click.
e.Handled = true;
if (DataContext is MainViewModel { OpenSettingsCommand: { } open } && open.CanExecute(null))
open.Execute(null);
}
private void GearButton_Click(object sender, RoutedEventArgs e)
{
// One menu (Settings / Bug / Feature / About), opened on click — moved up
// from the bottom bar so app-level actions live next to the brand.
if (sender is Button { ContextMenu: { } menu } button)
{
menu.PlacementTarget = button;
menu.Placement = System.Windows.Controls.Primitives.PlacementMode.Bottom;
menu.IsOpen = true;
}
}
private async System.Threading.Tasks.Task LoadAvatarImageAsync()
{
var vm = DataContext as MainViewModel;