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.
121 lines
3.6 KiB
C#
121 lines
3.6 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Linq;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// Preview undo/redo (creator directive 2026-11-01). The contract that matters: one gesture is
|
|
/// one undo step, a restored layout is exactly what was there before, and redo survives several
|
|
/// undos — because a stack that only works one step deep is worse than none.
|
|
/// </summary>
|
|
public class PreviewHistoryTests
|
|
{
|
|
private static Scene NewScene(params SceneElement[] elements)
|
|
{
|
|
var scene = new Scene { Name = "Live" };
|
|
foreach (var element in elements) scene.Elements.Add(element);
|
|
return scene;
|
|
}
|
|
|
|
private static SceneElement NewElement(double x = 0, double y = 0, double w = 100, double h = 100)
|
|
=> new Source { Name = "Layer", X = x, Y = y, Width = w, Height = h };
|
|
|
|
[Fact]
|
|
public void Undo_restores_geometry_and_redo_reapplies_it()
|
|
{
|
|
var element = NewElement(10, 20);
|
|
var scenes = new ObservableCollection<Scene> { NewScene(element) };
|
|
var history = new PreviewHistory();
|
|
|
|
history.Capture(scenes);
|
|
element.X = 300;
|
|
element.Y = 400;
|
|
|
|
Assert.True(history.Undo(scenes));
|
|
Assert.Equal(10, element.X);
|
|
Assert.Equal(20, element.Y);
|
|
|
|
Assert.True(history.Redo(scenes));
|
|
Assert.Equal(300, element.X);
|
|
Assert.Equal(400, element.Y);
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_removes_a_layer_added_after_the_snapshot_and_redo_brings_it_back()
|
|
{
|
|
var first = NewElement();
|
|
var scene = NewScene(first);
|
|
var scenes = new ObservableCollection<Scene> { scene };
|
|
var history = new PreviewHistory();
|
|
|
|
history.Capture(scenes);
|
|
var added = NewElement();
|
|
scene.Elements.Add(added);
|
|
|
|
history.Undo(scenes);
|
|
Assert.Single(scene.Elements);
|
|
Assert.DoesNotContain(added, scene.Elements);
|
|
|
|
history.Redo(scenes);
|
|
Assert.Equal(2, scene.Elements.Count);
|
|
Assert.Contains(added, scene.Elements);
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_restores_a_removed_layer_in_its_original_position()
|
|
{
|
|
var top = NewElement();
|
|
var middle = NewElement();
|
|
var bottom = NewElement();
|
|
var scene = NewScene(top, middle, bottom);
|
|
var scenes = new ObservableCollection<Scene> { scene };
|
|
|
|
var history = new PreviewHistory();
|
|
history.Capture(scenes);
|
|
scene.Elements.Remove(middle);
|
|
|
|
history.Undo(scenes);
|
|
Assert.Equal(new SceneElement[] { top, middle, bottom }, scene.Elements.ToArray());
|
|
}
|
|
|
|
[Fact]
|
|
public void A_new_edit_after_undo_drops_the_redo_branch()
|
|
{
|
|
var element = NewElement();
|
|
var scenes = new ObservableCollection<Scene> { NewScene(element) };
|
|
var history = new PreviewHistory();
|
|
|
|
history.Capture(scenes);
|
|
element.X = 50;
|
|
history.Undo(scenes);
|
|
|
|
Assert.True(history.CanRedo);
|
|
history.Capture(scenes);
|
|
Assert.False(history.CanRedo);
|
|
}
|
|
|
|
[Fact]
|
|
public void The_stack_is_bounded()
|
|
{
|
|
var scenes = new ObservableCollection<Scene> { NewScene(NewElement()) };
|
|
var history = new PreviewHistory();
|
|
|
|
for (var i = 0; i < PreviewHistory.MaxDepth + 20; i++) history.Capture(scenes);
|
|
|
|
Assert.Equal(PreviewHistory.MaxDepth, history.UndoDepth);
|
|
}
|
|
|
|
[Fact]
|
|
public void Undo_with_nothing_captured_is_a_no_op()
|
|
{
|
|
var element = NewElement(5, 5);
|
|
var scenes = new ObservableCollection<Scene> { NewScene(element) };
|
|
var history = new PreviewHistory();
|
|
|
|
Assert.False(history.Undo(scenes));
|
|
Assert.Equal(5, element.X);
|
|
}
|
|
}
|