Files
effectcraft/crates/ui-egui/tests/ui_frame_step.rs
Vikram Bhaskaran 341076359a Nudge selected layers with the arrow keys from any panel but Project (#961)
The viewer.nudge shortcuts bailed out unless Composition, Viewer or Timeline
had focus, so with any other panel focused the arrows silently did nothing.
After Effects nudges from almost anywhere; only the Project panel owns the
arrows for its own keyboard navigation. Typing, dialogs, menus and selected
mask vertices still keep the keys.

Tests: arrow nudge from Effect Controls, no nudge from Project, Shift+arrow
10x, time untouched, click-row path.
2026-10-13 00:39:37 +11:00

134 lines
6.6 KiB
Rust

//! Stepping the current time from the keyboard: Page Down / Page Up and Cmd+Right / Cmd+Left
//! Arrow move one frame, with Shift ten.
use effectcraft_engine::Session;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use egui::{Event, Key, Modifiers};
use egui_kittest::Harness;
use serde_json::json;
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) {
h.event(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers });
h.step();
h.event(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers });
h.step();
}
#[test]
fn cmd_arrows_step_the_current_frame() {
let mut session = Session::default();
session.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30.0, "duration": 4.0})).unwrap();
let cid = session.state.active_comp.unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(session));
h.run_steps(2);
let frame = |h: &Harness<'_, EffectcraftApp>| {
let app = h.state();
app.session.project.comp(cid).unwrap().frame_rate.frame_at(app.session.time())
};
assert_eq!(frame(&h), 0);
let cmd = Modifiers::COMMAND;
let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT;
key(&mut h, Key::ArrowRight, cmd);
assert_eq!(frame(&h), 1);
key(&mut h, Key::PageDown, Modifiers::NONE);
assert_eq!(frame(&h), 2);
key(&mut h, Key::ArrowRight, cmd_shift);
assert_eq!(frame(&h), 12);
key(&mut h, Key::ArrowLeft, cmd);
assert_eq!(frame(&h), 11);
key(&mut h, Key::ArrowLeft, cmd_shift);
assert_eq!(frame(&h), 1);
// The plain arrows still nudge layers, not the time.
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert_eq!(frame(&h), 1);
}
/// With a layer selected, the plain arrow keys nudge it (1 screen pixel at the viewer
/// magnification, 10 with Shift), as After Effects does (#961).
#[test]
fn plain_arrows_nudge_the_selected_layer() {
use effectcraft_engine::project::LayerId;
let mut session = Session::default();
session.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30.0, "duration": 4.0})).unwrap();
let lid = session.execute("layer.newSolid", json!({"name": "Box", "width": 40, "height": 40, "color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
session.execute("layer.select", json!({"layers": [lid]})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(session));
h.run_steps(2);
let x = |h: &Harness<'_, EffectcraftApp>| {
h.state().session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.prop("transform/position").unwrap().value.as_vec3()[0]
};
let frame = |h: &Harness<'_, EffectcraftApp>| {
let app = h.state();
app.session.time()
};
let (x0, t0) = (x(&h), frame(&h));
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert!(x(&h) > x0, "ArrowRight nudges the layer right (from {x0})");
assert_eq!(frame(&h), t0, "nudging does not move the time");
let x1 = x(&h);
key(&mut h, Key::ArrowRight, Modifiers::SHIFT);
let x2 = x(&h);
assert!(x2 > x1 + (x1 - x0) * 5.0, "Shift+ArrowRight nudges ten times further (from {x1})");
key(&mut h, Key::ArrowLeft, Modifiers::NONE);
assert!(x(&h) < x2, "ArrowLeft nudges back (from {x2})");
}
/// The realistic path: clicking a Timeline row selects the layer, and the arrow keys then
/// nudge it (#961).
#[test]
fn clicking_a_timeline_row_then_arrows_nudge_the_layer() {
use effectcraft_engine::project::LayerId;
let mut session = Session::default();
session.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30.0, "duration": 4.0})).unwrap();
let lid = session.execute("layer.newSolid", json!({"name": "Box", "width": 40, "height": 40, "color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
session.execute("edit.deselectAll", json!({})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(session));
h.run_steps(3);
let row = {
let e = h.state().auto.find(&format!("timeline.layer.{lid}.row")).expect("timeline row");
egui::Rect::from_min_size(egui::pos2(e.rect[0], e.rect[1]), egui::vec2(e.rect[2], e.rect[3]))
};
let p = egui::pos2(row.min.x + 210.0, row.center().y);
h.event(Event::PointerMoved(p));
h.step();
h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.step();
h.event(Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(3);
assert_eq!(h.state().session.state.selected_layers, vec![LayerId(lid)]);
let x = |h: &Harness<'_, EffectcraftApp>| {
h.state().session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.prop("transform/position").unwrap().value.as_vec3()[0]
};
let x0 = x(&h);
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert!(x(&h) > x0, "ArrowRight nudges the clicked layer (from {x0})");
}
/// The arrows nudge the selected layer from any panel except Project, which owns them for
/// its own keyboard navigation (#961).
#[test]
fn arrows_nudge_from_any_panel_but_project() {
use effectcraft_engine::project::LayerId;
let mut session = Session::default();
session.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30.0, "duration": 4.0})).unwrap();
let lid = session.execute("layer.newSolid", json!({"name": "Box", "width": 40, "height": 40, "color": "#ff0000"})).unwrap()["layer"].as_u64().unwrap();
session.execute("layer.select", json!({"layers": [lid]})).unwrap();
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(session));
h.run_steps(2);
let x = |h: &Harness<'_, EffectcraftApp>| {
h.state().session.active_comp().unwrap().layer(LayerId(lid)).unwrap().props.prop("transform/position").unwrap().value.as_vec3()[0]
};
h.state_mut().ui.focused = PanelKind::EffectControls;
h.run_steps(2);
let x0 = x(&h);
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert!(x(&h) > x0, "arrows nudge with Effect Controls focused (from {x0})");
// Project owns the arrows for its item navigation: the layer stays put.
h.state_mut().ui.focused = PanelKind::Project;
h.run_steps(2);
let x1 = x(&h);
key(&mut h, Key::ArrowRight, Modifiers::NONE);
assert_eq!(x(&h), x1, "arrows do not nudge with Project focused");
}