diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 613f18f98..f851adb58 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -122,6 +122,8 @@ mod panel_reorder_tests; #[cfg(test)] mod panel_width_tests; #[cfg(test)] +mod pen_mode_tests; +#[cfg(test)] mod perf_tests; #[cfg(test)] mod plugin_panel_tests; @@ -280,6 +282,8 @@ pub struct UiState { pub left_width: f32, pub right_width: f32, pub right_tab: RightTab, + /// The tab of the previous frame, to see the right panel change mode. + pub last_right_tab: RightTab, pub left_tab: LeftTab, pub pages_open: bool, pub expanded: BTreeSet, @@ -400,6 +404,7 @@ impl Default for UiState { left_width: PANEL_DEFAULT, right_width: PANEL_DEFAULT, right_tab: RightTab::Design, + last_right_tab: RightTab::Design, left_tab: LeftTab::File, pages_open: true, expanded: BTreeSet::new(), @@ -610,6 +615,19 @@ impl FigApp { } } + /// Leaving Design ends a pen path as Escape does and goes back to Move: Prototype and Inspect + /// don't draw (#307). Checked as the frame starts (the control channel) and after the panels + /// and shortcuts (the mode switcher, the tabs, Shift+E/Shift+D), before the canvas sees more. + fn end_pen_outside_design(&mut self) { + if self.ui.right_tab == self.ui.last_right_tab { + return; + } + self.ui.last_right_tab = self.ui.right_tab; + if self.ui.right_tab != RightTab::Design && self.session.tool.tool == figcraft_engine::tools::ToolId::Pen { + self.run("tool.set", json!({"tool": "move"})); + } + } + /// Once a frame, after the panels: a preview nothing asked for this frame ends. fn end_unwanted_text_preview(&mut self, ctx: &egui::Context) { if !std::mem::take(&mut self.ui.text_preview_wanted) { @@ -984,6 +1002,7 @@ impl FigApp { widgets::set_nudge(&ctx, self.session.prefs.nudge_small, self.session.prefs.nudge_big); // The Inspect tab measures on hover without Alt. self.session.tool.redlines = self.ui.right_tab == RightTab::Inspect; + self.end_pen_outside_design(); if self.ui.present.is_some() { present::show(self, ui); // What went wrong while presenting (a widget's plugin failing) shows over it. @@ -1086,6 +1105,7 @@ impl FigApp { variable_ui::picker(self, &ctx); style_picker::show(self, &ctx); self.end_unwanted_text_preview(&ctx); + self.end_pen_outside_design(); dialogs::show(self, &ctx); self.toast_ui(&ctx); // A rename started after the layers panel drew (Cmd/Ctrl+R, a double-click) opens its field diff --git a/crates/ui-egui/src/pen_mode_tests.rs b/crates/ui-egui/src/pen_mode_tests.rs new file mode 100644 index 000000000..6fa54959d --- /dev/null +++ b/crates/ui-egui/src/pen_mode_tests.rs @@ -0,0 +1,94 @@ +//! Leaving Design with a pen path open (#307): the path ends as Escape ends it, the tool goes back +//! to Move, and the canvas of Prototype or Inspect takes no more pen points. Driven by the panel +//! keys and by the toolbar's mode switcher. + +use egui::accesskit::Role; +use egui::{Event, Key, Modifiers, Pos2, pos2}; +use figcraft_engine::Session; +use figcraft_engine::tools::ToolId; +use serde_json::json; + +use crate::RightTab; +use crate::layers_drag_tests::Ui; + +fn press(u: &mut Ui, key: Key, modifiers: Modifiers) { + u.frame(vec![ + Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }, + Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers }, + ]); +} + +fn click(u: &mut Ui, p: Pos2) { + u.button(p, true); + u.button(p, false); + u.frame(vec![]); +} + +fn layers(u: &Ui) -> usize { + let doc = u.app.session.doc().unwrap(); + doc.node(u.app.session.page().unwrap()).unwrap().children.len() +} + +/// The middle of the toolbar's mode button named `label`, found in the AccessKit tree. +fn mode_button(u: &mut Ui, label: &str) -> Pos2 { + u.ctx.enable_accesskit(); + u.frame(vec![]); + let out = u.frame_output(vec![]); + let nodes = out.platform_output.accesskit_update.expect("an AccessKit update").nodes; + let n = nodes.iter().map(|(_, n)| n).find(|n| n.role() == Role::Button && n.label() == Some(label)).expect(label); + let b = n.bounds().expect("bounds"); + pos2(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32) +} + +/// A pen path with two anchors still open on an empty page. +fn drawing() -> Ui { + let mut u = Ui::new(Session::new()); + u.app.run("tool.set", json!({"tool": "pen"})); + click(&mut u, pos2(600.0, 400.0)); + click(&mut u, pos2(700.0, 450.0)); + assert_eq!(u.app.session.tool.pen.as_ref().map(|p| p.points.len()), Some(2), "the path is open"); + u +} + +#[test] +fn switching_mode_ends_the_pen_path_as_escape_does() { + let mut esc = drawing(); + press(&mut esc, Key::Escape, Modifiers::NONE); + assert!(esc.app.session.tool.pen.is_none()); + let finished = layers(&esc); + assert_eq!(finished, 1, "Escape keeps the two-anchor path"); + + // Shift+E is Prototype, Shift+D is Inspect. + for key in [Key::E, Key::D] { + let mut u = drawing(); + press(&mut u, key, Modifiers::SHIFT); + assert_ne!(u.app.ui.right_tab, RightTab::Design, "{key:?}"); + assert!(u.app.session.tool.pen.is_none(), "{key:?}: the path is finished"); + assert_eq!(u.app.session.tool.tool, ToolId::Move, "{key:?}: back to Move"); + assert_eq!(layers(&u), finished, "{key:?}: the path is kept as Escape keeps it"); + // The canvas takes no more pen points. + click(&mut u, pos2(800.0, 300.0)); + assert!(u.app.session.tool.pen.is_none(), "{key:?}: a click starts no path"); + assert_eq!(layers(&u), finished, "{key:?}: a click adds nothing"); + } +} + +#[test] +fn the_mode_switcher_ends_the_pen_path_too() { + let finished = { + let mut esc = drawing(); + press(&mut esc, Key::Escape, Modifiers::NONE); + layers(&esc) + }; + // Design is already the mode while the pen draws: pressing it still leaves the pen, as it leaves + // the pencil of Draw. + for (label, tab) in [("Prototype", RightTab::Prototype), ("Inspect", RightTab::Inspect), ("Design", RightTab::Design)] { + let mut u = drawing(); + let at = mode_button(&mut u, label); + click(&mut u, at); + assert_eq!(u.app.ui.right_tab, tab, "{label}"); + assert!(u.app.session.tool.pen.is_none(), "{label}: the path is finished"); + assert_eq!(u.app.session.tool.tool, ToolId::Move, "{label}: back to Move"); + assert_eq!(layers(&u), finished, "{label}: the path is kept as Escape keeps it"); + } +} diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 1e6e38aa1..24af4a640 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -174,7 +174,7 @@ pub fn show(app: &mut FigApp, ui: &mut Ui) { } 1 => { app.ui.right_tab = crate::RightTab::Design; - if app.session.tool.tool == ToolId::Pencil { + if matches!(app.session.tool.tool, ToolId::Pencil | ToolId::Pen) { app.run("tool.set", json!({"tool": "move"})); } }