Files
effectcraft/crates/ui-egui/tests/ui_preview_align.rs
T
Brandon ThomasandClaude Opus 5.5 553a51316c M13.8: Preview panel shortcuts and Align panel commands
Preview panel: five shortcuts (Spacebar, Shift+Spacebar, Numpad 0, Shift+Numpad 0,
Alt+Numpad 0) each with saved options (include video/audio/overlays/layer controls, loop,
cache before playback, range incl. play around current time, play from, frame rate, skip,
resolution, full screen, play cached frames, move time to preview time), persisted in
prefs; playback.settings.get/set; the playback loop follows the preview plan.

Align panel: layer.align {edge, to} and layer.distribute {mode} engine commands (bounds
through transform and parents, one undo step); Align Layers to dropdown and Distribute row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 07:38:46 -04:00

187 lines
8.3 KiB
Rust

//! Headless checks for M13.8: the Preview panel's per-shortcut options drive playback (range,
//! play from, resolution, overlays, full screen, move time), the preview shortcuts pick their
//! options, and the Align panel runs `layer.align` / `layer.distribute`.
use effectcraft_engine::Session;
use effectcraft_engine::preview::PreviewShortcut;
use effectcraft_engine::time::Tick;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use egui::{Event, Pos2, pos2};
use egui_kittest::Harness;
use serde_json::json;
fn harness() -> Harness<'static, EffectcraftApp> {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "Preview", "width": 320, "height": 180, "frameRate": 24, "duration": 10})).unwrap();
s.execute("layer.newSolid", json!({"name": "A", "color": "#406080", "width": 40, "height": 30})).unwrap();
s.execute("layer.newSolid", json!({"name": "B", "color": "#806040", "width": 60, "height": 20})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
h.run_steps(3);
h
}
fn invoke(h: &mut Harness<'_, EffectcraftApp>, id: &str, p: serde_json::Value) -> serde_json::Value {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, id, p).unwrap()
}
fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}")).clone();
let p: Pos2 = pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0);
h.input_mut().events.push(Event::PointerMoved(p));
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Default::default() });
h.step();
h.input_mut().events.push(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Default::default() });
h.run_steps(2);
}
fn set(h: &mut Harness<'_, EffectcraftApp>, p: serde_json::Value) {
h.state_mut().session.execute("playback.settings.set", p).unwrap();
}
#[test]
fn preview_panel_edits_the_shown_shortcut() {
let mut h = harness();
invoke(&mut h, "window.panel", json!({"panel": "preview"}));
// The default workspace keeps the Preview panel short: maximize it.
h.state_mut().toggle_maximize(PanelKind::Preview);
h.run_steps(3);
for id in [
"preview.shortcut",
"preview.includeVideo",
"preview.includeAudio",
"preview.includeOverlays",
"preview.includeLayerControls",
"preview.loop",
"preview.cacheBeforePlayback",
"preview.range",
"preview.playFrom",
"preview.frameRate",
"preview.skip",
"preview.resolution",
"preview.fullScreen",
"preview.playCachedFrames",
"preview.moveTimeToPreviewTime",
] {
assert!(h.state().auto.find(id).is_some(), "{id}");
}
assert!(h.state().session.prefs.preview.spacebar.loop_);
click(&mut h, "preview.loop");
assert!(!h.state().session.prefs.preview.spacebar.loop_);
// Shortcut popup: Shift+Numpad 0; its options are shown and edited from now on.
click(&mut h, "preview.shortcut");
click(&mut h, "preview.shortcut.3");
assert_eq!(h.state().session.prefs.preview.current, PreviewShortcut::ShiftNumpad0);
click(&mut h, "preview.fullScreen");
assert!(h.state().session.prefs.preview.shift_numpad0.full_screen);
assert!(!h.state().session.prefs.preview.spacebar.full_screen);
// Range popup → Play Around Current Time shows the pre/post-roll.
click(&mut h, "preview.range");
click(&mut h, "preview.range.3");
h.run_steps(2);
assert!(h.state().auto.find("preview.preRoll").is_some());
}
#[test]
fn playback_follows_the_shortcut_options() {
let mut h = harness();
// Numpad 0: Work Area from its start; the time returns when the preview stops.
h.state_mut().session.execute("comp.workArea", json!({"start": 2.0, "end": 4.0})).unwrap();
set(&mut h, json!({"shortcut": "numpad0", "moveTimeToPreviewTime": false, "resolution": "quarter", "includeOverlays": false}));
h.state_mut().session.set_time(Tick::from_seconds_f64(7.0));
invoke(&mut h, "playback.ramPreview", json!({}));
let app = h.state();
assert!(app.playback.playing);
assert_eq!(app.playback.shortcut, PreviewShortcut::Numpad0);
let pl = app.playback.plan.unwrap();
assert_eq!((pl.start, pl.end, pl.first), (48, 95, 48));
assert_eq!(app.session.time(), Tick::from_seconds_f64(2.0));
assert_eq!(app.viewer_scale(1.0, 1.0), 0.25);
assert!(!app.overlays_visible());
invoke(&mut h, "playback.stop", json!({}));
assert!(!h.state().playback.playing);
assert_eq!(h.state().session.time(), Tick::from_seconds_f64(7.0));
assert!(h.state().overlays_visible());
// Spacebar with Full Screen: the viewer is maximized while it plays.
set(&mut h, json!({"shortcut": "spacebar", "fullScreen": true, "skip": 2}));
invoke(&mut h, "playback.toggle", json!({}));
assert_eq!(h.state().ui.maximized, Some(PanelKind::Composition));
assert_eq!(h.state().playback.plan.unwrap().step, 3);
invoke(&mut h, "playback.toggle", json!({}));
assert_eq!(h.state().ui.maximized, None);
// Shift+Spacebar from the keyboard plays with its own options.
let ev = Event::Key { key: egui::Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::SHIFT };
h.input_mut().events.push(ev);
h.step();
assert!(h.state().playback.playing);
assert_eq!(h.state().playback.shortcut, PreviewShortcut::ShiftSpacebar);
assert_eq!(h.state().playback.plan.unwrap().step, 2);
}
#[test]
fn cache_before_playback_waits_then_plays_cached() {
let mut h = harness();
set(&mut h, json!({"shortcut": "spacebar", "cacheBeforePlayback": true, "range": "entireDuration", "playFrom": "rangeStart"}));
invoke(&mut h, "playback.toggle", json!({}));
assert!(h.state().playback.caching);
// Pressing again while caching ("If caching, play cached frames") stops only when nothing
// is cached yet, otherwise plays the cached frames.
h.run_steps(4);
invoke(&mut h, "playback.toggle", json!({}));
let st = &h.state().playback;
assert!(!st.caching);
if st.playing {
let pl = st.plan.unwrap();
assert!(pl.end < 239);
}
}
#[test]
fn align_panel_runs_align_and_distribute() {
let mut h = harness();
invoke(&mut h, "window.panel", json!({"panel": "align"}));
h.state_mut().toggle_maximize(PanelKind::Align);
h.run_steps(3);
for id in ["align.to", "align.left", "align.vcenter", "distribute.hcenter", "distribute.bottom"] {
assert!(h.state().auto.find(id).is_some(), "{id}");
}
let ids: Vec<u64> = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id.0).collect();
h.state_mut().session.execute("layer.select", json!({"layers": ids})).unwrap();
h.run_steps(2);
// (x position, width) of A and B (anchors in the middle of the solids).
let pos = |h: &Harness<'_, EffectcraftApp>| -> Vec<(f64, f64)> {
let c = h.state().session.active_comp().unwrap();
["A", "B"]
.iter()
.map(|n| {
let l = c.layers.iter().find(|l| l.name == *n).unwrap();
let x = l.transform().unwrap().get("position").unwrap().value_at(Tick::ZERO).to_json()[0].as_f64().unwrap();
(x, if *n == "A" { 40.0 } else { 60.0 })
})
.collect()
};
let orig = pos(&h);
click(&mut h, "align.left");
assert_eq!(pos(&h), vec![(20.0, 40.0), (30.0, 60.0)]);
// One undo step for the whole alignment.
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
assert_eq!(pos(&h), orig);
// Align Layers to: Selection — right edges meet at the rightmost.
click(&mut h, "align.to");
click(&mut h, "align.to");
h.state_mut().ui.align_to_selection = true;
h.run_steps(2);
assert_eq!(h.state().auto.find("align.to").unwrap().label, "Selection");
click(&mut h, "align.right");
let p = pos(&h);
let right = |(x, w): (f64, f64)| x + w / 2.0;
let goal = orig.iter().map(|v| right(*v)).fold(f64::NEG_INFINITY, f64::max);
assert!((right(p[0]) - goal).abs() < 1e-9 && (right(p[1]) - goal).abs() < 1e-9, "{p:?}");
// Distribute needs three layers: nothing moves.
click(&mut h, "distribute.hcenter");
assert_eq!(pos(&h), p);
}