photocraft/crates/ui-egui/src/input_tests.rs

135 lines
5.6 KiB
Rust

//! Input routing through the real canvas, dialogs and shortcuts (#44, #45): keyboard zoom hits the
//! canvas (never egui's UI scale), and an open dialog keeps the canvas pannable and zoomable but
//! is not cancelled by a click outside it.
use egui::{Key, Modifiers, Pos2, pos2, vec2};
use egui_kittest::Harness;
use serde_json::json;
use crate::PhotocraftApp;
fn harness() -> Harness<'static, PhotocraftApp> {
let mut app = PhotocraftApp::new(photocraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.sync_views();
let mut h = Harness::builder().with_size(vec2(1200.0, 800.0)).build_ui_state(
|ui, app: &mut PhotocraftApp| {
let ctx = ui.ctx().clone();
// Fonts set up after the first frame only apply from the next one.
if !ctx.fonts(|f| f.families().contains(&egui::FontFamily::Name("medium".into()))) {
return;
}
crate::shortcuts::handle(app, &ctx);
egui::CentralPanel::default().show(ui, |ui| crate::canvas::document_area(app, ui));
crate::dialogs::show(app, &ctx);
},
app,
);
PhotocraftApp::setup_context(&h.ctx, crate::theme::ThemeKind::ALL[0]);
h.run_steps(4);
h
}
fn zoom(h: &Harness<'static, PhotocraftApp>) -> f32 {
h.state().current_zoom()
}
fn open_dialog(h: &mut Harness<'static, PhotocraftApp>) {
crate::dialogs::open_command_dialog(h.state_mut(), "image.adjustments.brightnessContrast", "Brightness/Contrast…");
h.run_steps(3);
assert_eq!(h.state().ui.dialogs.len(), 1);
}
/// A point on the canvas well away from the centred dialog.
fn free_canvas(h: &Harness<'static, PhotocraftApp>) -> Pos2 {
let r = h.state().last_canvas_rect;
pos2(r.left() + 60.0, r.bottom() - 60.0)
}
#[test]
fn ctrl_plus_zooms_the_canvas_not_the_interface() {
let mut h = harness();
let z0 = zoom(&h);
// ⌘+ / Ctrl+'+' as typed with ⇧ on a US layout, and as the numpad / Nordic `+`.
h.key_press_modifiers(Modifiers::COMMAND | Modifiers::SHIFT, Key::Plus);
h.run_steps(2);
let z1 = zoom(&h);
assert!(z1 > z0, "⌘+ should zoom the canvas in ({z0} -> {z1})");
h.key_press_modifiers(Modifiers::COMMAND, Key::Plus);
h.run_steps(2);
assert!(zoom(&h) > z1);
h.key_press_modifiers(Modifiers::COMMAND, Key::Minus);
h.run_steps(2);
assert!((zoom(&h) - z1).abs() < 1e-4);
// ⌘1 is 100%, ⌘0 fits on screen again.
h.key_press_modifiers(Modifiers::COMMAND, Key::Num1);
h.run_steps(2);
assert_eq!(zoom(&h), 1.0);
h.key_press_modifiers(Modifiers::COMMAND, Key::Num0);
h.run_steps(3);
assert!((zoom(&h) - z0).abs() < 1e-3, "⌘0 fits on screen: {} vs {z0}", zoom(&h));
assert_eq!(h.ctx.zoom_factor(), 1.0, "the interface must never scale");
}
#[test]
fn keyboard_zoom_works_with_a_dialog_open() {
let mut h = harness();
open_dialog(&mut h);
let z0 = zoom(&h);
h.key_press_modifiers(Modifiers::COMMAND, Key::Equals);
h.run_steps(2);
assert!(zoom(&h) > z0);
h.key_press_modifiers(Modifiers::COMMAND | Modifiers::SHIFT, Key::Plus);
h.run_steps(2);
assert_eq!(h.ctx.zoom_factor(), 1.0);
assert_eq!(h.state().ui.dialogs.len(), 1);
// Other commands stay blocked while the dialog is open (⌘J would duplicate the layer).
let layers = h.state().session.active().unwrap().doc.layers.len();
h.key_press_modifiers(Modifiers::COMMAND, Key::J);
h.run_steps(2);
assert_eq!(h.state().session.active().unwrap().doc.layers.len(), layers);
}
#[test]
fn clicking_outside_a_dialog_keeps_it_open_and_the_canvas_pans_and_zooms() {
let mut h = harness();
open_dialog(&mut h);
let p = free_canvas(&h);
h.hover_at(p);
h.run_steps(1);
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.run_steps(1);
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.run_steps(3);
assert_eq!(h.state().ui.dialogs.len(), 1, "a click outside must not cancel the dialog");
// Scrolling over the canvas pans it.
let c0 = h.state().ui.views[0].center;
h.event(egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta: vec2(0.0, -40.0), phase: egui::TouchPhase::Move, modifiers: Modifiers::NONE });
h.run_steps(8);
let c1 = h.state().ui.views[0].center;
assert!(c1[1] > c0[1], "scroll pans under a dialog: {c0:?} -> {c1:?}");
// Space-drag pans too.
h.event(egui::Event::Key { key: Key::Space, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
h.run_steps(1);
h.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
h.run_steps(1);
for i in 1..=5 {
h.hover_at(p + vec2(10.0 * i as f32, 0.0));
h.run_steps(1);
}
h.event(egui::Event::PointerButton { pos: p + vec2(50.0, 0.0), button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
h.event(egui::Event::Key { key: Key::Space, physical_key: None, pressed: false, repeat: false, modifiers: Modifiers::NONE });
h.run_steps(2);
let c2 = h.state().ui.views[0].center;
let z = zoom(&h);
assert!((c2[0] - (c1[0] - 50.0 / z)).abs() < 0.5, "space-drag pans by 50 pt: {c1:?} -> {c2:?}");
assert_eq!(h.state().ui.dialogs.len(), 1);
// Esc still cancels.
h.key_press(Key::Escape);
h.run_steps(2);
assert!(h.state().ui.dialogs.is_empty());
}