135 lines
5.6 KiB
Rust
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());
|
|
}
|