178 lines
7.6 KiB
Rust
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);
|
|
}
|