From 5539e286475bba2cfe7084cba8ae7352f6070fa5 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Fri, 23 Oct 2026 11:26:32 +0200 Subject: [PATCH] Layout: Go Back after Presentation returns to the last spread presented Presentation's spreads now count as one jump in the window's history, and leaving it as a jump back to the view it was entered from, at the place the view was. Before, the history stayed on the last presented spread while the view was put back, so Go Back couldn't reach it. docs/gaps.md: Go Back and Go Forward are two of the missing menu items, 149. --- crates/ui-egui/src/canvas.rs | 2 +- crates/ui-egui/src/lib.rs | 8 ++++ crates/ui-egui/src/menus.rs | 77 +++++++++++++++++++++++++++++++++++- docs/gaps.md | 2 +- 4 files changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index dddcfedb..e0ac7344 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -262,7 +262,7 @@ fn page_ids(doc: &Document) -> Vec { /// The page in view and the view centre's offset from that page's centre. Parent pages have none, /// nor has a view still waiting to be fitted (leaving parent pages for a document page). -fn page_in_view(app: &DesignApp) -> Option<(designcraft_doc::ids::PageId, DVec2)> { +pub(crate) fn page_in_view(app: &DesignApp) -> Option<(designcraft_doc::ids::PageId, DVec2)> { let st = app.session.active().filter(|st| !st.editing_parents)?; let (v, r) = (app.view().filter(|v| v.fitted)?, app.canvas_rect?); let abs = current_page(app)?; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 0b1add9a..e144eb41 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -194,6 +194,12 @@ pub struct UiState { /// The view (by its `views` key) Presentation was entered from; leaving puts it back. #[serde(skip)] pub presentation_view: Option<(u64, View)>, + /// The page in that view and the place on it, for Go Back after leaving Presentation. + #[serde(skip)] + pub presentation_page: Option<(designcraft_doc::ids::PageId, designcraft_geom::Vec2)>, + /// That view's Go Back / Go Forward history; leaving puts it back with one jump added. + #[serde(skip)] + pub(crate) presentation_history: Option, pub frame_edges: bool, pub rulers: bool, pub guides: bool, @@ -362,6 +368,8 @@ impl Default for UiState { screen_mode: ScreenMode::Normal, presentation_return: ScreenMode::Normal, presentation_view: None, + presentation_page: None, + presentation_history: None, frame_edges: true, rulers: true, guides: true, diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 2a3ffebc..45becf63 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -1978,13 +1978,28 @@ fn set_screen_mode(app: &mut DesignApp, mode: ScreenMode) { app.ui.presentation_return = app.ui.screen_mode; // Presentation fits the spread to the screen; leaving it puts the user's view back. app.ui.presentation_view = app.view_key().zip(app.view().copied()); + app.ui.presentation_page = crate::canvas::page_in_view(app); + app.ui.presentation_history = app.page_history().cloned(); } if mode != ScreenMode::Presentation && presenting && let Some((k, v)) = app.ui.presentation_view.take() - && let Some(view) = app.views.get_mut(&k) + && app.views.contains_key(&k) { - *view = v; + // Go Back sees the presentation as one jump to the last spread presented, and putting the + // view back as a jump from there: the spreads in between would otherwise have replaced the + // place the view was at with the presentation's. + let shown = app.ui.presentation_page.take().zip(crate::canvas::page_in_view(app)).filter(|_| app.view_key() == Some(k)); + let h = app.page_history.entry(k).or_default(); + *h = app.ui.presentation_history.take().unwrap_or_default(); + if let Some(((from, at), (to, offset))) = shown { + h.leave(from, at); + h.visit(to, offset); + h.visit(from, at); + } + if let Some(view) = app.views.get_mut(&k) { + *view = v; + } } app.ui.screen_mode = mode; } @@ -4346,6 +4361,64 @@ mod tests { assert_eq!(spread(&app), 2); } + #[test] + fn go_back_after_presentation_returns_to_the_last_spread_presented() { + let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.session.execute("file.new", &json!({"pages": 7, "facingPages": true})).unwrap(); + let ctx = egui::Context::default(); + let frame = |app: &mut crate::DesignApp, events: Vec| { + let input = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0))), + events, + ..Default::default() + }; + let mut out = ctx.run_ui(input, |ui| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }); + out.textures_delta.clear(); + }; + let key = |k: egui::Key, modifiers: egui::Modifiers| { + vec![ + egui::Event::Key { key: k, physical_key: None, pressed: true, repeat: false, modifiers }, + egui::Event::Key { key: k, physical_key: None, pressed: false, repeat: false, modifiers }, + ] + }; + let spread = |app: &crate::DesignApp| { + let st = app.session.active().unwrap(); + crate::canvas::current_slot(app, &designcraft_tools::layout::CanvasLayout::new(&st.doc, false)).unwrap() + }; + let cmd = egui::Modifiers { command: true, ctrl: true, ..Default::default() }; + frame(&mut app, vec![]); + frame(&mut app, vec![]); + // Scrolled off the page's centre, so the place Go Back returns to is not just the page. + let view = app.view_mut().unwrap(); + view.origin.x += 40.0; + view.origin.y += 60.0; + frame(&mut app, vec![]); + assert_eq!(spread(&app), 0); + let before = *app.view().unwrap(); + // Presentation goes on to spreads 1 and 2; leaving it puts spread 0 back as it was. + app.run("view.screenMode", json!({"mode": "presentation"})).unwrap(); + frame(&mut app, vec![]); + frame(&mut app, key(egui::Key::ArrowRight, egui::Modifiers::NONE)); + frame(&mut app, key(egui::Key::ArrowRight, egui::Modifiers::NONE)); + assert_eq!(spread(&app), 2); + app.run("view.screenMode", json!({"mode": "normal"})).unwrap(); + frame(&mut app, vec![]); + assert_eq!(spread(&app), 0); + assert!(!menu_enabled(&app, "view.goForward")); + frame(&mut app, key(egui::Key::PageUp, cmd)); + assert_eq!(spread(&app), 2, "Go Back shows the last spread presented"); + frame(&mut app, key(egui::Key::PageUp, cmd)); + assert_eq!(spread(&app), 0, "Go Back again, the view Presentation was entered from"); + let v = *app.view().unwrap(); + assert!((v.origin - before.origin).hypot() < 1e-6, "where it was: {:?} vs {:?}", v.origin, before.origin); + assert!(!menu_enabled(&app, "view.goBack"), "the presentation is one jump: spread 1 isn't kept"); + frame(&mut app, key(egui::Key::PageDown, cmd)); + assert_eq!(spread(&app), 2, "Go Forward"); + } + #[test] fn presentation_shows_one_spread_and_goes_through_them_with_keys_and_clicks() { let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/gaps.md b/docs/gaps.md index 1fbc07ca..4ee123d0 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -165,7 +165,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format · - **Estimate:** 30–50 h. ### 16. Menu items still missing (U) -- **Missing:** 151 of 482 in-scope items of InDesign 21.6's menu dump have no match (measured 2026-10-10; some exist under other labels): Type ▸ Notes +- **Missing:** 149 of 482 in-scope items of InDesign 21.6's menu dump have no match (measured 2026-10-10; some exist under other labels): Type ▸ Notes and Track Changes submenus (the panels and commands exist), hyperlink and cross-reference update commands, Convert Bullets/Numbering to Text, Convert Variable to Text, Object ▸ Clear Effects / Clear All Transparency / Rotate 180° / Reverse Path,