effectcraft/crates/ui-egui/tests/ui_layer_panel_zoom.rs

178 lines
7.6 KiB
Rust

//! #753: the Layer panel zooms and pans (magnification menu, View ▸ Zoom In / Zoom Out while it
//! is focused, the wheel, the Zoom and Hand tools), and the Roto Brush explains itself in the
//! Composition panel (egui_kittest, UI logic only).
use effectcraft_engine::Session;
use effectcraft_ui_egui::EffectcraftApp;
use effectcraft_ui_egui::dock::PanelKind;
use effectcraft_ui_egui::state::Tool;
use egui::{Event, Pos2, Rect, pos2, vec2};
use egui_kittest::Harness;
use serde_json::json;
fn app() -> EffectcraftApp {
let mut s = Session::default();
s.execute("comp.new", json!({"name": "View", "width": 640, "height": 360, "duration": 4})).unwrap();
s.execute("layer.newSolid", json!({"name": "Box", "color": "#e04020", "width": 80, "height": 80})).unwrap();
EffectcraftApp::new(s)
}
fn box_id(h: &Harness<'_, EffectcraftApp>) -> u64 {
h.state().session.active_comp().unwrap().layers.iter().find(|l| l.name == "Box").unwrap().id.0
}
/// The harness with Box open in the Layer panel (focused).
fn harness() -> Harness<'static, EffectcraftApp> {
let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| app());
h.run_steps(3);
let id = box_id(&h);
h.state_mut().session.execute("layer.select", json!({"layers": [id]})).unwrap();
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "layer.openLayer", json!({"layer": id})).unwrap();
h.run_steps(3);
assert_eq!(h.state().ui.focused, PanelKind::Layer);
h
}
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
}
fn press(h: &mut Harness<'_, EffectcraftApp>, p: Pos2, button: egui::PointerButton, pressed: bool) {
h.input_mut().events.push(Event::PointerButton { pos: p, button, pressed, modifiers: Default::default() });
}
fn click(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
h.input_mut().events.push(Event::PointerMoved(p));
h.step();
press(h, p, egui::PointerButton::Primary, true);
h.step();
press(h, p, egui::PointerButton::Primary, false);
h.run_steps(2);
}
fn drag_with(h: &mut Harness<'_, EffectcraftApp>, button: egui::PointerButton, from: Pos2, to: Pos2) {
h.input_mut().events.push(Event::PointerMoved(from));
press(h, from, button, true);
h.step();
for i in 1..=8 {
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 8.0)));
h.step();
}
press(h, to, button, false);
h.run_steps(2);
}
fn command(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
let ctx = h.ctx.clone();
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, id, json!({})).unwrap();
h.run_steps(3);
}
/// Shown magnification: the canvas width per layer pixel (Box is 80 px wide).
fn shown(h: &Harness<'_, EffectcraftApp>) -> f32 {
rect(h, "layerPanel.canvas").width() / 80.0
}
#[test]
fn zoom_commands_act_on_the_focused_layer_panel() {
let mut h = harness();
assert!(h.state().auto.find("layerPanel.magnification").is_some(), "the Layer panel has a magnification menu");
let fit = shown(&h);
let comp_zoom = h.state().ui.viewer.zoom;
command(&mut h, "view.zoomOut");
let z = h.state().ui.layer_zoom.expect("Zoom Out sets the Layer panel's magnification");
assert!(z < fit, "{z} < {fit}");
assert!((shown(&h) - z).abs() < 0.01, "the canvas is drawn at the new magnification");
assert_eq!(h.state().ui.viewer.zoom, comp_zoom, "the Composition viewer keeps its magnification");
command(&mut h, "view.zoomIn");
assert!(h.state().ui.layer_zoom.unwrap() > z);
command(&mut h, "view.fit");
assert_eq!(h.state().ui.layer_zoom, None);
assert!((shown(&h) - fit).abs() < 0.01);
// With the Composition panel focused, the same commands zoom it instead.
h.state_mut().show_panel(PanelKind::Composition);
h.run_steps(2);
command(&mut h, "view.zoomIn");
assert!(h.state().ui.viewer.zoom.is_some());
assert_eq!(h.state().ui.layer_zoom, None);
}
#[test]
fn wheel_zooms_around_the_pointer_and_hand_tool_pans() {
let mut h = harness();
let canvas = rect(&h, "layerPanel.canvas");
// A point a quarter of the way into the layer stays under the pointer.
let at = canvas.min + canvas.size() * 0.25;
h.input_mut().events.push(Event::PointerMoved(at));
h.input_mut().events.push(Event::MouseWheel {
unit: egui::MouseWheelUnit::Point,
delta: vec2(0.0, 120.0),
modifiers: Default::default(),
phase: egui::TouchPhase::Move,
});
h.run_steps(20);
let after = rect(&h, "layerPanel.canvas");
assert!(after.width() > canvas.width(), "the wheel zooms in: {} → {}", canvas.width(), after.width());
let still = after.min + after.size() * 0.25;
assert!(still.distance(at) < 2.0, "the point under the pointer stays put: {at:?} vs {still:?}");
// Hand tool drag pans; no paint or Roto stroke is made.
h.state_mut().ui.tool = Tool::Hand;
h.run_steps(2);
let before = rect(&h, "layerPanel.canvas").min;
let c = rect(&h, "layerPanel.area").center();
drag_with(&mut h, egui::PointerButton::Primary, c, c + vec2(60.0, 30.0));
let moved = rect(&h, "layerPanel.canvas").min - before;
assert!((moved.x - 60.0).abs() < 1.0 && (moved.y - 30.0).abs() < 1.0, "{moved:?}");
// Middle-drag pans with a Roto Brush tool, painting nothing.
h.state_mut().ui.tool = Tool::RotoBrush;
h.run_steps(2);
let before = rect(&h, "layerPanel.canvas").min;
drag_with(&mut h, egui::PointerButton::Middle, c, c - vec2(40.0, 20.0));
let moved = rect(&h, "layerPanel.canvas").min - before;
assert!((moved.x + 40.0).abs() < 1.0 && (moved.y + 20.0).abs() < 1.0, "{moved:?}");
let layer = h.state().session.active_comp().unwrap().layer(effectcraft_engine::project::LayerId(box_id(&h))).unwrap().clone();
assert!(layer.effects().is_none_or(|fx| fx.groups().count() == 0), "panning adds no Roto Brush effect");
}
#[test]
fn zoom_tool_click_zooms_in_the_layer_panel() {
let mut h = harness();
let fit = shown(&h);
h.state_mut().ui.tool = Tool::Zoom;
h.run_steps(2);
let c = rect(&h, "layerPanel.canvas").center();
click(&mut h, c);
let z = h.state().ui.layer_zoom.expect("the Zoom tool sets the Layer panel's magnification");
assert!((z - fit * 2.0).abs() < 0.01, "{fit} → {z}");
assert_eq!(h.state().ui.viewer.zoom, None, "the Composition viewer keeps Fit");
}
#[test]
fn roto_brush_in_the_composition_panel_says_where_it_works() {
let mut h = harness();
h.state_mut().show_panel(PanelKind::Composition);
h.state_mut().ui.layer_panel = None;
h.state_mut().ui.tool = Tool::RotoBrush;
h.run_steps(3);
let comp = rect(&h, "viewer.comp");
let c = comp.center();
click(&mut h, c);
assert!(h.state().ui.status.contains("Layer panel"), "a hint, not silence: {:?}", h.state().ui.status);
// Double-clicking the layer opens it in the Layer panel.
h.run_steps(30);
h.input_mut().events.push(Event::PointerMoved(c));
h.step();
// Both clicks inside egui's double-click time; the second release (its own frame, as with a
// real mouse) opens the layer after the press has focused the Composition panel.
press(&mut h, c, egui::PointerButton::Primary, true);
press(&mut h, c, egui::PointerButton::Primary, false);
press(&mut h, c, egui::PointerButton::Primary, true);
h.step();
press(&mut h, c, egui::PointerButton::Primary, false);
h.run_steps(3);
assert_eq!(h.state().ui.layer_panel, Some(box_id(&h)));
assert_eq!(h.state().ui.focused, PanelKind::Layer);
}