diff --git a/crates/ui-egui/src/canvas/input.rs b/crates/ui-egui/src/canvas/input.rs index 5f06eb980d..879f86dd1e 100644 --- a/crates/ui-egui/src/canvas/input.rs +++ b/crates/ui-egui/src/canvas/input.rs @@ -12,7 +12,9 @@ use super::{Color32, Drag, Mods, Point, PointerEvent, PointerKind, Pos2, Shape, /// Dispatch canvas pointer, keyboard and drop events through the active document view. pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect) { - let (pointer, m, space, hover) = wheel_view(app, ui, resp, rect); + // A modal dialog takes the canvas's presses, finger taps, long touches and view navigation, from the frame it opens. + let modal = crate::panel_docking::modal_open(app, ui.ctx()); + let (pointer, m, space, hover) = wheel_view(app, ui, resp, rect, modal); let v = *app.view().unwrap_or(&View::default()); let xf = Xf::at(rect, &v, app.canvas_ui_zoom); // A lifted pen or finger takes the pointer away in the frame it lifts (egui's `PointerGone`), @@ -21,11 +23,11 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo let at = hover.or_else(|| pointer.interact_pos().filter(|p| rect.contains(*p))); let view = app.view_info(); let (_, backquote_before) = backquote_seen(ui); - let f = Pointing { resp, rect, pointer: &pointer, m, space, xf, v, view, backquote_before }; - touch_gestures(app, ui, rect, view); + let f = Pointing { resp, rect, pointer: &pointer, m, space, xf, v, view, backquote_before, modal }; + touch_gestures(app, ui, rect, view, modal); // Pressed and held still, a pen or a finger opens the context menu (egui's long touch): the - // tool's press is dropped, as a right-click presses nothing. - if resp.long_touched() { + // tool's press is dropped, as a right-click presses nothing. Under a modal dialog it drops nothing. + if resp.long_touched() && !modal { drop_press(app, ui, view); } let drag: Option = ui.data(|d| d.get_temp(drag_id())); @@ -84,9 +86,11 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo // egui counts a press a few pixels outside the canvas as on it (its interaction radius): only a // press on the canvas itself reaches the tools, else it would land at the canvas's centre. // A finger pressing with others down, or with another landing in its frame, presses nothing - // (`touch_gestures` made the press a gesture, #4198). + // (`touch_gestures` made the press a gesture, #4198). While a modal dialog is open no press + // reaches the tools or pans, also in the frame it opens, before its backdrop covers the canvas. if pointer.primary_pressed() && resp.hovered() + && !f.modal && !middle_pan && drag != Some(Drag::Gesture) && let Some(p) = at @@ -110,6 +114,7 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo } } else if drag.is_none() && resp.hovered() + && !f.modal && pointer.button_pressed(egui::PointerButton::Middle) && let Some(start) = hover { @@ -169,6 +174,7 @@ pub(super) fn handle_input(app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Respo select_named_artboard(app, i, shift || release_shift); } if resp.double_clicked() + && !f.modal && let Some(p) = at { double_click(app, ui, &f, p); @@ -190,21 +196,37 @@ struct Pointing<'a> { view: vectorcraft_engine::ViewInfo, /// Whether Backtick was held at the end of the frame before ([`backquote_seen`]). backquote_before: Option, + /// Whether a modal dialog is open ([`crate::panel_docking::modal_open`]). + modal: bool, } -/// Zoom around the pointer, and scroll ([`wheel`]); a trackpad rotation turns the view. Returns the pointer, the +/// Zoom around the pointer, and scroll ([`wheel`]); a trackpad rotation turns the view. While a modal dialog is open +/// (`modal`), and for the rest of a wheel turn made under it, the view does not move. Returns the pointer, the /// modifiers, whether Space is held and where the pointer hovers over the canvas. fn wheel_view( app: &mut VectorcraftApp, ui: &Ui, resp: &egui::Response, rect: egui::Rect, + modal: bool, ) -> (egui::PointerState, egui::Modifiers, bool, Option) { let line = ui.ctx().options(|o| o.input_options.line_scroll_speed); let wheel_zooms = app.session.prefs.zoom_with_mouse_wheel; + // A wheel turn that goes on under a modal dialog moves nothing after the dialog closes either: what egui lets out + // of it later is dropped until egui ends the turn. + // `carried` is set only while a turn that went on under the dialog is still scrolling, so a new turn + // made after it closes moves the view at once. + let carried_id = egui::Id::unique("canvas-wheel-muted"); + let was_carried = ui.data(|d| d.get_temp::(carried_id)).unwrap_or(false); + let carried = (modal || was_carried) && ui.input(egui::InputState::is_scrolling); + if carried != was_carried { + ui.data_mut(|d| d.insert_temp(carried_id, carried)); + } + let muted = modal || carried; + let navigates = resp.hovered() && !muted; // A docked field may be drawn later and has not consumed its wheel event yet. Only canvas // hover may start a zoom glide; existing glides still decay outside the canvas in `wheel`. - let animate = animated_zoom(&app.session.prefs) && resp.hovered(); + let animate = animated_zoom(&app.session.prefs) && navigates; let turn_id = egui::Id::unique("canvas-wheel-turn"); let turn_before: WheelTurn = ui.data(|d| d.get_temp(turn_id)).unwrap_or_default(); let mut turn = turn_before; @@ -216,6 +238,10 @@ fn wheel_view( wheel(i, &app.session.prefs, wheel_zooms, animate, line, rect.height(), &mut turn), ) }); + // A zoom glide still running when a modal dialog opens stops. + if muted { + turn.glide = 0.0; + } if turn != turn_before { ui.data_mut(|d| d.insert_temp(turn_id, turn)); } @@ -226,7 +252,7 @@ fn wheel_view( // Zoom around the pointer, and scroll ([`wheel`]): fingers pinching on a touch screen may also // slide. Then turn the view about the pointer for a trackpad rotation ([`trackpad_turn`]). The // tool's events below take their document points from the view this moves to. - if resp.hovered() { + if navigates { let ui_zoom = app.canvas_ui_zoom; if (factor - 1.0).abs() > 1e-6 && let (Some(p), Some(vm)) = (hover, app.view_mut()) @@ -247,10 +273,11 @@ fn wheel_view( } /// In Presentation Mode a click anywhere goes to the next artboard. The tool receives no press, drag or hover, and a -/// drag does not pan the view. A drag that began before the mode was turned on ends as usual. +/// drag does not pan the view. A drag that began before the mode was turned on ends as usual. While a modal dialog is +/// open, a click does not step. fn present_step(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, drag: Option, at: Option) { let (resp, pointer) = (f.resp, f.pointer); - let pressed = drag.is_none() && pointer.primary_pressed() && resp.hovered() && at.is_some(); + let pressed = drag.is_none() && pointer.primary_pressed() && resp.hovered() && at.is_some() && !f.modal; // `press` is this frame's new press, or else the press held so far. A quick tap can be // pressed and released in one frame. let press = if pressed { Some(Drag::Present) } else { drag }; @@ -258,7 +285,7 @@ fn present_step(app: &mut VectorcraftApp, ui: &Ui, f: &Pointing, drag: Option(drag_id())); // A tap of several fingers leaves `Drag::Gesture` (`touch_gestures`), and its release // does not move the view to the next artboard. - if press == Some(Drag::Present) && resp.clicked() { + if press == Some(Drag::Present) && resp.clicked() && !f.modal { crate::shortcuts::presentation_step(app, true); } } else if pressed { diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index 6bf8a6fa97..d6df378c87 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -1030,6 +1030,278 @@ fn two_finger_tap_in_touch_switches_selection_and_drawing_modes() { assert_eq!(count(&app), before - 1, "Essentials: undone"); } +/// One frame of the canvas with `events`, with the modal backdrop drawn over it when `backdrop` is set, as the app +/// draws it for an open dialog. +fn modal_frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec, backdrop: bool) { + let raw = egui::RawInput { screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + show(app, ui); + if backdrop { + crate::dialogs::modal::show(ui.ctx(), "Test", egui::Id::unique("test-dialog"), 0.0, 8, |ui| ui.label("A dialog")); + } + }); + out.textures_delta.clear(); +} + +/// Finger `n` touching at `pos`. +fn finger(n: u64, phase: egui::TouchPhase, pos: Pos2) -> egui::Event { + egui::Event::Touch { device_id: egui::TouchDeviceId(1), id: egui::TouchId(n), phase, pos, force: None } +} + +/// The primary button pressed or released at `pos`. +fn finger_button(pos: Pos2, pressed: bool) -> egui::Event { + egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() } +} + +/// A tap of `fingers` fingers beside `c`, the first also pressing as the pointer and moving a little, in frames drawn +/// with the modal backdrop when `backdrop` is set ([`modal_frame`]). +fn finger_tap(app: &mut VectorcraftApp, ctx: &egui::Context, c: Pos2, fingers: u64, backdrop: bool) { + use egui::TouchPhase::{End, Move, Start}; + let at = |n: u64| c + vec2(n as f32 * 50.0, 0.0); + let moved = at(0) + vec2(6.0, 6.0); + modal_frame(app, ctx, vec![finger(0, Start, at(0)), egui::Event::PointerMoved(at(0)), finger_button(at(0), true)], backdrop); + modal_frame(app, ctx, vec![finger(0, Move, moved), egui::Event::PointerMoved(moved)], backdrop); + modal_frame(app, ctx, (1..fingers).map(|n| finger(n, Start, at(n))).collect(), backdrop); + modal_frame(app, ctx, vec![finger(0, End, at(0)), finger_button(moved, false), egui::Event::PointerGone], backdrop); + modal_frame(app, ctx, (1..fingers).map(|n| finger(n, End, at(n))).collect(), backdrop); + modal_frame(app, ctx, vec![], backdrop); +} + +/// While a modal is open, a two-finger tap on the canvas does not undo, a three-finger tap does not redo, and in the +/// Touch workspace a two-finger tap does not switch modes: with the app's dialog open, before its backdrop is drawn +/// (the frame it opens) and after, and under a modal backdrop alone. +#[test] +fn modal_blocks_two_and_three_finger_canvas_taps() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 40, "height": 40})).unwrap(); + app.select_tool("selection"); + let ctx = egui::Context::default(); + modal_frame(&mut app, &ctx, vec![], false); + let count = |app: &VectorcraftApp| app.session.active().unwrap().doc.layers[0].children().unwrap().len(); + let c = app.canvas_rect.unwrap().center(); + let settle = |app: &mut VectorcraftApp| { + app.ui.dialog = None; + modal_frame(app, &ctx, vec![], false); + modal_frame(app, &ctx, vec![], false); + }; + for (dialog, backdrop) in [(true, true), (true, false), (false, true)] { + let open = |app: &mut VectorcraftApp| app.ui.dialog = dialog.then(|| crate::state::Dialog::new("preferences", json!({}))); + open(&mut app); + finger_tap(&mut app, &ctx, c, 2, backdrop); + assert_eq!(count(&app), 1, "dialog {dialog}, backdrop {backdrop}: two fingers undo nothing"); + settle(&mut app); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(count(&app), 0); + open(&mut app); + finger_tap(&mut app, &ctx, c, 3, backdrop); + assert_eq!(count(&app), 0, "dialog {dialog}, backdrop {backdrop}: three fingers redo nothing"); + settle(&mut app); + app.run("edit.redo", json!({})).unwrap(); + assert_eq!(count(&app), 1); + } + crate::workspaces::switch(&mut app.ui, crate::workspaces::TOUCH).unwrap(); + app.select_tool("selection"); + modal_frame(&mut app, &ctx, vec![], false); + app.ui.dialog = Some(crate::state::Dialog::new("preferences", json!({}))); + finger_tap(&mut app, &ctx, c, 2, true); + assert_eq!((app.session.tool_id(), count(&app)), ("selection", 1), "Touch: no switch to drawing mode under the dialog"); + settle(&mut app); + finger_tap(&mut app, &ctx, c, 2, false); + assert_ne!(app.session.tool_id(), "selection", "closed: two fingers switch to drawing mode again"); +} + +/// Fingers down on the canvas when a modal dialog opens make no tap: lifting them while the dialog is open, or after it +/// closes, runs nothing. A tap begun after it closes undoes again. +#[test] +fn opening_a_modal_discards_a_pending_canvas_tap() { + use egui::TouchPhase::{End, Start}; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 50, "y": 50, "width": 40, "height": 40})).unwrap(); + app.select_tool("selection"); + let ctx = egui::Context::default(); + modal_frame(&mut app, &ctx, vec![], false); + let count = |app: &VectorcraftApp| app.session.active().unwrap().doc.layers[0].children().unwrap().len(); + let c = app.canvas_rect.unwrap().center(); + let at = |n: u64| c + vec2(n as f32 * 50.0, 0.0); + modal_frame(&mut app, &ctx, vec![finger(0, Start, at(0)), egui::Event::PointerMoved(at(0)), finger_button(at(0), true)], false); + modal_frame(&mut app, &ctx, vec![finger(1, Start, at(1))], false); + // The dialog opens in a frame with no touch, and one finger lifts while it is open. + app.ui.dialog = Some(crate::state::Dialog::new("preferences", json!({}))); + modal_frame(&mut app, &ctx, vec![], true); + modal_frame(&mut app, &ctx, vec![finger(0, End, at(0)), finger_button(at(0), false), egui::Event::PointerGone], true); + assert_eq!(count(&app), 1, "nothing undone while the dialog is open"); + // The other finger lifts after it closes. + app.ui.dialog = None; + modal_frame(&mut app, &ctx, vec![], false); + modal_frame(&mut app, &ctx, vec![], false); + modal_frame(&mut app, &ctx, vec![finger(1, End, at(1))], false); + modal_frame(&mut app, &ctx, vec![], false); + assert_eq!(count(&app), 1, "the tap begun before the dialog runs nothing after it closes"); + finger_tap(&mut app, &ctx, c, 2, false); + assert_eq!(count(&app), 0, "a tap begun after it closes undoes"); +} + +/// While a modal is open and the canvas is not yet covered by its backdrop (the frame a dialog opens), a click in +/// Presentation Mode does not step to the next artboard, and a pinch or a wheel does not move the view. Closed, both +/// act again. +#[test] +fn modal_blocks_presentation_tap_and_touch_navigation() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + app.session.execute("artboard.new", &json!({"x": 500, "y": 0, "width": 100, "height": 80, "name": "Far"})).unwrap(); + let ctx = egui::Context::default(); + let dialog = || Some(crate::state::Dialog::new("preferences", json!({}))); + app.ui.screen_mode = 3; + modal_frame(&mut app, &ctx, vec![], false); + let c = app.canvas_rect.unwrap().center(); + let click = |app: &mut VectorcraftApp| { + modal_frame(app, &ctx, vec![egui::Event::PointerMoved(c)], false); + modal_frame(app, &ctx, vec![finger_button(c, true)], false); + modal_frame(app, &ctx, vec![finger_button(c, false)], false); + modal_frame(app, &ctx, vec![], false); + }; + let at = |app: &VectorcraftApp| crate::panels::artboards::selected(app, 2); + assert_eq!(at(&app), 0); + app.ui.dialog = dialog(); + click(&mut app); + assert_eq!(at(&app), 0, "no step under the dialog"); + app.ui.dialog = None; + click(&mut app); + assert_eq!(at(&app), 1, "closed: a click steps"); + + app.ui.screen_mode = 0; + modal_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(c)], false); + let view = |app: &VectorcraftApp| { + let v = app.view().unwrap(); + (v.zoom, v.center) + }; + let wheel = egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: vec2(0.0, 40.0), + phase: egui::TouchPhase::Move, + modifiers: Default::default(), + source: egui::MouseWheelSource::Unknown, + }; + let before = view(&app); + app.ui.dialog = dialog(); + modal_frame(&mut app, &ctx, vec![egui::Event::Zoom(2.0)], false); + modal_frame(&mut app, &ctx, vec![wheel.clone()], false); + for _ in 0..30 { + modal_frame(&mut app, &ctx, vec![], false); + } + assert_eq!(view(&app), before, "the view does not move under the dialog"); + // A wheel turn of whole lines, which egui lets out over several frames, made just before the dialog closes. + let lines = egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Line, + delta: vec2(0.0, 3.0), + phase: egui::TouchPhase::Move, + modifiers: Default::default(), + source: egui::MouseWheelSource::Unknown, + }; + modal_frame(&mut app, &ctx, vec![lines], false); + app.ui.dialog = None; + for _ in 0..30 { + modal_frame(&mut app, &ctx, vec![], false); + } + assert_eq!(view(&app), before, "the rest of a wheel turn made under the dialog does not move the view after it closes"); + modal_frame(&mut app, &ctx, vec![egui::Event::Zoom(2.0)], false); + assert_ne!(view(&app).0, before.0, "closed: a pinch zooms"); + let zoomed = view(&app); + modal_frame(&mut app, &ctx, vec![wheel.clone()], false); + for _ in 0..30 { + modal_frame(&mut app, &ctx, vec![], false); + } + assert_ne!(view(&app), zoomed, "closed: a new wheel turn moves the view"); + // A dialog opened and closed with no wheel turn under it: a wheel turn in the first frame after it closes moves + // the view. + let scrolled = view(&app); + app.ui.dialog = dialog(); + modal_frame(&mut app, &ctx, vec![], false); + app.ui.dialog = None; + modal_frame(&mut app, &ctx, vec![wheel], false); + for _ in 0..30 { + modal_frame(&mut app, &ctx, vec![], false); + } + assert_ne!(view(&app), scrolled, "a wheel turn just after the dialog closes moves the view"); +} + +/// While a modal is open, a pen or finger held still on the canvas does not drop the press it began. +#[test] +fn modal_long_touch_does_not_cancel_canvas_press() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width":800,"height":600})).unwrap(); + app.select_tool("rectangle"); + let ctx = egui::Context::default(); + let tick = |app: &mut VectorcraftApp, time, events| { + let raw = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), + time: Some(time), + events, + ..Default::default() + }; + let mut out = ctx.run_ui(raw, |ui| show(app, ui)); + out.textures_delta.clear(); + }; + tick(&mut app, 0.0, vec![]); + let pos = app.canvas_rect.unwrap().center(); + tick(&mut app, 0.1, pen(egui::TouchPhase::Start, pos, 0.5)); + assert!(matches!(ctx.data(|d| d.get_temp::(drag_id())), Some(Drag::Tool))); + app.ui.dialog = Some(crate::state::Dialog::new("preferences", json!({}))); + tick(&mut app, 1.5, vec![]); + assert!(matches!(ctx.data(|d| d.get_temp::(drag_id())), Some(Drag::Tool)), "the press is kept under the dialog"); +} + +/// While a modal is open and its backdrop does not yet cover the canvas (the frame a dialog opens), a drag on the canvas +/// draws nothing, a two-finger tap with a drawing tool draws nothing, a middle-button drag does not pan and a +/// double-click on type does not start editing it. Closed, a drag draws again. +#[test] +fn modal_blocks_new_canvas_presses() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 100, "y": 150, "text": "Hello world", "size": 24})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.none", &json!({})).unwrap(); + app.select_tool("rectangle"); + let ctx = egui::Context::default(); + modal_frame(&mut app, &ctx, vec![], false); + let count = |app: &VectorcraftApp| app.session.active().unwrap().doc.layers[0].children().unwrap().len(); + let from = app.canvas_rect.unwrap().center() + vec2(0.0, 80.0); + let drag = |app: &mut VectorcraftApp, button: egui::PointerButton| { + let to = from + vec2(60.0, 40.0); + let b = |pos, pressed| egui::Event::PointerButton { pos, button, pressed, modifiers: Default::default() }; + modal_frame(app, &ctx, vec![egui::Event::PointerMoved(from), b(from, true)], false); + modal_frame(app, &ctx, vec![egui::Event::PointerMoved(from + vec2(30.0, 20.0))], false); + modal_frame(app, &ctx, vec![egui::Event::PointerMoved(to)], false); + modal_frame(app, &ctx, vec![b(to, false)], false); + modal_frame(app, &ctx, vec![], false); + }; + let before = count(&app); + app.ui.dialog = Some(crate::state::Dialog::new("preferences", json!({}))); + drag(&mut app, egui::PointerButton::Primary); + assert_eq!(count(&app), before, "a drag with the Rectangle tool draws nothing under the dialog"); + finger_tap(&mut app, &ctx, from, 2, false); + assert_eq!(count(&app), before, "a two-finger tap with the Rectangle tool draws nothing under the dialog"); + let center = app.view().unwrap().center; + drag(&mut app, egui::PointerButton::Middle); + assert_eq!(app.view().unwrap().center, center, "a middle-button drag does not pan under the dialog"); + app.select_tool("selection"); + modal_frame(&mut app, &ctx, vec![], false); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let b = app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().geometric_bounds().unwrap(); + let at = xf.to_screen(b.center()); + let button = |pressed| finger_button(at, pressed); + modal_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at), button(true), button(false), button(true), button(false)], false); + modal_frame(&mut app, &ctx, vec![], false); + assert_eq!(app.session.tool_id(), "selection", "a double-click on type does not edit it under the dialog"); + app.ui.dialog = None; + app.select_tool("rectangle"); + modal_frame(&mut app, &ctx, vec![], false); + modal_frame(&mut app, &ctx, vec![], false); + drag(&mut app, egui::PointerButton::Primary); + assert_eq!(count(&app), before + 1, "closed: a drag draws"); +} + /// Switch Selection and Drawing Modes from the command: Pencil when no drawing tool was chosen yet, /// a view tool is left for Selection without becoming the drawing tool, and every selection tool /// switches to drawing mode. diff --git a/crates/ui-egui/src/canvas/view.rs b/crates/ui-egui/src/canvas/view.rs index 0304e28c17..0e43e6a545 100644 --- a/crates/ui-egui/src/canvas/view.rs +++ b/crates/ui-egui/src/canvas/view.rs @@ -534,10 +534,18 @@ pub(super) fn wheel( /// finger drops the press the first one began (its tool interaction rolled back; the rest of the /// gesture presses nothing), and a tap of two fingers undoes, one of three redoes. In the Touch /// workspace a tap of two fingers switches between selection and drawing modes instead. -pub(super) fn touch_gestures(app: &mut VectorcraftApp, ui: &Ui, rect: egui::Rect, view: vectorcraft_engine::ViewInfo) { +/// +/// While a modal dialog is open (`modal`) the canvas takes no finger contacts: no tap runs a command and no gesture +/// begins. The fingers already down are forgotten, so lifting them, during the dialog or after it closes, runs nothing. +pub(super) fn touch_gestures(app: &mut VectorcraftApp, ui: &Ui, rect: egui::Rect, view: vectorcraft_engine::ViewInfo, modal: bool) { if !app.session.prefs.touch_gestures { return; } + let key = egui::Id::unique("canvas-touch"); + if modal { + ui.data_mut(|d| d.remove::(key)); + return; + } let (touches, now): (Vec<_>, f64) = ui.input(|i| { let t = i.events.iter().filter_map(|e| match e { egui::Event::Touch { id, phase, pos, .. } => Some((*id, *phase, *pos)), @@ -548,7 +556,6 @@ pub(super) fn touch_gestures(app: &mut VectorcraftApp, ui: &Ui, rect: egui::Rect if touches.is_empty() { return; } - let key = egui::Id::unique("canvas-touch"); let mut taps: crate::touch::Taps = ui.data(|d| d.get_temp(key)).unwrap_or_default(); let mut tap = None; for (id, phase, pos) in touches { diff --git a/docs/mcp.md b/docs/mcp.md index c2e825d8c6..6bebc86bab 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -5804,7 +5804,9 @@ From the Selection, Direct Selection, Group Selection, Magic Wand, Lasso or Pers drawing tool chosen last in the window, or the Pencil tool when none has been chosen yet; from any other tool it chooses the Selection tool, and the tool left becomes the drawing tool unless it is the Hand, Rotate View, Zoom or Print Tiling tool. It returns `{tool}` and adds no undo step. Pencil as the first drawing tool and the list of selection tools -are VectorCraft's own rules. A tap in Presentation Mode runs nothing. +are VectorCraft's own rules. A tap in Presentation Mode runs nothing. Under VectorCraft's own rule, a tap on the canvas +while a modal dialog is open runs nothing, and fingers already down when it opens make no tap when they lift, during +the dialog or after it closes. `gridlineEvery` (72 pt by default) and `gridSubdivisions` (8 by default) set the document grid that View › Show Grid draws, and View › Snap to Grid snaps to its subdivisions. Changing either sets the grid of every open document. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index b553a76c8f..7fdc7ef81f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1196,6 +1196,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A two-finger rotation on a Mac trackpad turns the view about the pointer once it reaches 5° (Preferences › General › Trackpad Gesture to Rotate View, on by default). On a touch screen a second finger cancels the tool press the first finger began, in the frame it lands or later: the Pencil's stroke or a Selection move is rolled back with no undo entry, and a finger that lands while another is down presses nothing, so a pinch or two-finger pan only zooms and pans (#4198; canvas test `a_second_finger_cancels_the_tool_gesture_the_first_began`). In the Touch workspace a two-finger tap switches between the Selection tool and the drawing tool chosen last (the Pencil tool when none has been chosen yet) and undoes nothing; outside Touch it still undoes, and a three-finger tap still redoes ([the help page](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/touch-workspace-in-illustrator.html); `tool.toggleTouchMode`; canvas test `two_finger_tap_in_touch_switches_selection_and_drawing_modes`). + While a modal dialog is open, from the frame it opens, the canvas takes no new presses, double-clicks, finger taps, long touches, pinches or wheel turns, and a click in Presentation Mode does not step to the next artboard: a two-finger tap undoes nothing, a three-finger tap redoes nothing, a two-finger tap in the Touch workspace switches no mode, and fingers already down when the dialog opens make no tap when they lift, during the dialog or after it closes. This is VectorCraft's own rule (canvas tests `modal_blocks_two_and_three_finger_canvas_taps`, `opening_a_modal_discards_a_pending_canvas_tap`, `modal_blocks_presentation_tap_and_touch_navigation`, `modal_long_touch_does_not_cancel_canvas_press` and `modal_blocks_new_canvas_presses`). A tool press dropped by a second finger, a long touch or a canceled touch cancels the tool's drag as Escape does: the tool gets back the options it had when the press began (the Polygon tool's sides, set with ↑ and ↓) and they are not stored, a press held still opens no size dialog, a Selection or Artboard tool marquee selects nothing, and the Pen goes on drawing its path without the dropped anchor (canvas tests `a_canceled_touch_drops_the_drag_without_keeping_its_options` and `long_touch_drops_a_tool_press_without_committing_art`; engine tests in `tests::toolsettings`). The status bar navigator's field shows the active artboard's number and name, and its drop-down lists every artboard by number and name, as the Artboards panel does. Choosing an artboard in the list makes it the active artboard and fits it in the window (`view.goToArtboard`). The list opens on the active artboard's row and lays out only the rows in view and at most one more. - **Geometry and effects:**