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,