From 1e2e7cc79fcda2358e1c5809502faf9e55437cc8 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 00:39:18 +0800 Subject: [PATCH 1/5] M3: full screen shows one fitted page at a time, as Acrobat's Full Screen Mode (#1459) Full screen is the page on black and nothing else: one page (or one spread, when two-page view was on, #1353) fitted to the screen, from the current page; a click, Space, Enter, the arrows and Page Up/Down turn pages, a shift-click and Backspace go back; Esc brings the display from before back at the page reached. The quick tools are not drawn in full screen. Transitions, auto-advance, loop and the cursor hiding stay planned. Refs #379. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01RUNQSPVXx3dDBabRZHAnjz --- ROADMAP.md | 1 + crates/ui-egui/src/canvas.rs | 81 ++++++++++++++++++++++++++++++++- crates/ui-egui/src/commands.rs | 2 +- crates/ui-egui/src/create_ui.rs | 6 ++- crates/ui-egui/src/files.rs | 6 ++- crates/ui-egui/src/lib.rs | 43 +++++++++++------ crates/ui-egui/tests/view.rs | 79 ++++++++++++++++++++++++++++++++ docs/ui-parity.md | 5 +- parity/acrobat-features.toml | 12 ++--- 9 files changed, 208 insertions(+), 27 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index c6bcf08c3..3531a82b3 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -249,6 +249,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-14 (M3, control-click candidate):** Coordinate clicks retain hover warmup and pair press/release in one input pass so slow frames do not turn a click into a hold. Text multi-click selection accepts the actual press event when egui has already cleared its origin. Regression cases cover slow-frame button activation, disabled controls and coalesced text selection; estimates unchanged. - **2026-10-14 (community, M7, #128 Hebrew in Add text):** Add text and `page_add_text` draw Hebrew with the craft-fonts Hebr face (Noto Sans Hebrew), through the same shaped path as Arabic and Thai: right-to-left display order with numbers and Latin in place, points and cantillation kept on their letter as one Type 3 glyph, ToUnicode and per-line ActualText so extraction, search and the editor get typing order. Builds without the face refuse Hebrew rather than write `?`. Regular style only. Estimates unchanged. - **2026-10-14 (M9, #1787):** Changes after signing are judged by the role an object held when signed for a page's resources too: a rewritten font, XObject, graphics state or resources dictionary is page content however its new version is labelled, so a `/FT` or `/Subtype /Widget` added by the attacker no longer reads as a permitted form fill. A category dictionary that only gained a name is judged as before. +- **2026-10-14 (M3, full screen as presentation, #379/#1459/#1353):** Full Screen Mode shows one page (or one spread) at a time fitted to the screen on black, with no tools; click, Space, Enter and the arrows turn pages; Esc restores the display from before. Transitions, auto-advance and loop remain planned. - **2026-10-13 (M3, native-opening candidate):** Chosen tools survive preparation, conversion and password retries; startup intake waits for pending preparation. Regression tests cover completion, cancellation and document identity. Estimates unchanged. - **2026-10-13 (community, M3, #531 touchpad momentum):** On Wayland a two-finger swipe keeps gliding after the fingers lift and slows to a stop, measured from the gesture's last 0.1 s and decaying with a 0.35 s time constant through the #759 1:1 path. Any wheel input, click, key, zoom or edge ends it (fingers resting on the touchpad don't yet: the vendored winit binds no Wayland hold gestures); single-page paging, macOS (which sends its own momentum), the web, and Animation effects Off never glide. Unit tests for the physics and two headless view tests. Estimates unchanged. - **2026-10-13 (M3, native-open admission):** Later prepared replies stay queued while a modal or native picker owns the foreground. Picker completion, cancellation and abandonment wake retained opens. Regressions cover recipient ownership, exactly-once admission, source/history preservation and one-shot repaint requests. Estimates unchanged. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 16ae63490..eb539d985 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -81,6 +81,17 @@ pub enum Fit { None, } +/// The page display as it was before full screen, put back on leaving it +/// ([`DocView::enter_presentation`]). +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct SavedDisplay { + pub layout: PageLayout, + pub continuous: bool, + pub cover: bool, + pub fit: Fit, + pub zoom: f32, +} + /// The base page display: one page across, or a two-page spread. Whether it scrolls continuously /// is a separate flag (`continuous`), as in Acrobat, so every combination is reachable (#752). #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -414,6 +425,9 @@ pub struct DocView { pub flash: Option<(usize, [f32; 4], f64)>, /// Compare files: differences shaded on this document's pages (page, user-space box, colour). pub compare_marks: Vec<(usize, [f32; 4], Color32)>, + /// Full screen (Acrobat's Full Screen Mode): the display saved on entering it, while the view + /// shows one page or spread at a time, fitted to the screen. `None` outside full screen. + presentation: Option, pages: HashMap, /// Pages the renderer could not draw, with the reason (never re-requested). errors: HashMap, @@ -723,6 +737,7 @@ impl DocView { single_edges: None, flash: None, compare_marks: Vec::new(), + presentation: None, pages: HashMap::new(), errors: HashMap::new(), waiting_since: HashMap::new(), @@ -1372,6 +1387,37 @@ impl DocView { self.reset_coarse_state(); } + /// Full screen, as Acrobat's Full Screen Mode (#379, #1459): one page at a time (or one + /// spread, when two-page view was on: #1353), fitted to the screen, from the current page. + /// The display it replaces is kept for [`Self::leave_presentation`]. A no-op while presenting. + pub fn enter_presentation(&mut self) { + if self.presentation.is_some() { + return; + } + self.presentation = + Some(SavedDisplay { layout: self.layout, continuous: self.continuous, cover: self.cover, fit: self.fit, zoom: self.zoom }); + self.continuous = false; + self.fit = Fit::Page; + self.selection = None; + self.goto = Some((self.current, 0.0)); + } + + /// Leave full screen: the display from before it comes back, at the page reached meanwhile. + pub fn leave_presentation(&mut self) { + let Some(saved) = self.presentation.take() else { return }; + self.layout = saved.layout; + self.continuous = saved.continuous; + self.cover = saved.cover; + self.fit = saved.fit; + self.zoom = saved.zoom; + self.goto = Some((self.current, 0.0)); + } + + /// Whether the view is in full screen's one-page-at-a-time display. + pub fn presenting(&self) -> bool { + self.presentation.is_some() + } + /// Switch the base page display (Single or Two-page), leaving continuous scrolling as it is. /// A no-op when the layout is already current, so re-selecting it never yanks a scrolled view /// back to the top. @@ -2300,6 +2346,16 @@ pub fn shortcuts(view: &mut DocView, ctx: &egui::Context, speed: crate::scroll_s if key(Key::End) { view.go_to_page(usize::MAX); } + // Full screen: Space and Enter go forward a page and Backspace back, as in Acrobat (the + // arrows and Page Up / Page Down already turn pages where nothing scrolls, below). + if view.presentation.is_some() { + if key(Key::Space) || key(Key::Enter) { + view.step_page(true); + } + if key(Key::Backspace) { + view.step_page(false); + } + } // As in Acrobat: → / ← go to the next / previous page in every layout (#185), and so do // ⌘Page Down / ⌘Page Up, or plain Page Down / Page Up in single-page view. let command = ctx.input(|i| i.modifiers.command); @@ -2469,6 +2525,22 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { // Exclude the previous frame's scrollbar tracks from gesture activation. let navigation_rect = if view.viewport_screen.is_positive() { avail.intersect(view.viewport_screen) } else { avail }; let navigation_enabled = app.dialog.is_none() && app.close_request.is_none() && !app.palette_open && ui.is_enabled() && ui.input(|i| i.focused); + // Full screen: a click on the document goes forward a page and a shift-click back, as in + // Acrobat, unless the click did something else (a link, a word selected). Decided below, + // once the pages have had the click. + let present_click = view.presentation.is_some() + && navigation_enabled + && ui.rect_contains_pointer(navigation_rect) + && ui.input(|i| i.pointer.primary_clicked()) + && app.quick_tool == QuickTool::Select; + // Shift as held now, or as the click's own event carried it (a synthetic click). + let present_back = present_click + && ui.input(|i| { + i.modifiers.shift + || i.events.iter().any(|e| { + matches!(e, egui::Event::PointerButton { button: egui::PointerButton::Primary, pressed: false, modifiers, .. } if modifiers.shift) + }) + }); if zoom_delta.is_finite() && zoom_delta > 0.0 && zoom_delta != 1.0 && navigation_enabled && ui.rect_contains_pointer(navigation_rect) { let z = view.zoom * zoom_delta; match ui.input(|i| i.pointer.hover_pos()) { @@ -3953,6 +4025,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { Some(comments::CanvasAction::CopyImageFile(p, i)) => image_action = Some(crate::edit_text_ui::ImageAction::CopyFile(p, i)), None => {} } + // Full screen: the click turned the page unless it did something else first. + if present_click && clicked_link.is_none() && view.selection.is_none() && view.clicks < 2 { + view.step_page(!present_back); + } match clicked_link { Some(LinkTarget::Page(p, dest)) => view.go_to_dest(p, dest, info), Some(LinkTarget::Uri(u)) => app.request_document_url(&u, crate::LinkOrigin::Link), @@ -4071,8 +4147,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { if copy_selected { app.execute("edit.copy"); } - // Read mode hides the tools (#2646). - if app.mode != crate::Mode::Read { + // Read mode hides the tools (#2646), and full screen is the page and nothing else (#1459); + // the quick tools come back with the window. + if app.mode != crate::Mode::Read && !app.full_screen { quick_bar(app, avail, ui); } if let Some((source, file, destination)) = remote_open { diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index b769c0c00..e9d3b43d3 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -270,7 +270,7 @@ impl PdfCraftApp { let on = !self.full_screen; match self.ctx.clone() { Some(ctx) => self.set_full_screen(&ctx, on), - None => self.full_screen = on, + None => self.apply_full_screen(on), } } "view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false), diff --git a/crates/ui-egui/src/create_ui.rs b/crates/ui-egui/src/create_ui.rs index 63f803525..1e4e69603 100644 --- a/crates/ui-egui/src/create_ui.rs +++ b/crates/ui-egui/src/create_ui.rs @@ -103,7 +103,11 @@ impl PdfCraftApp { // The reading-colours preference is app-wide: the newly created document starts with it. self.session.set_reading_colors(id, self.reading_colors); let info = &self.session.get(id).ok_or("the new document could not be opened")?.info; - self.views.push(crate::DocView::new(id, info, self.view_defaults)); + let mut view = crate::DocView::new(id, info, self.view_defaults); + if self.full_screen { + view.enter_presentation(); + } + self.views.push(view); self.active = Some(self.views.len() - 1); Ok(()) } diff --git a/crates/ui-egui/src/files.rs b/crates/ui-egui/src/files.rs index 03c773807..1ec9d383f 100644 --- a/crates/ui-egui/src/files.rs +++ b/crates/ui-egui/src/files.rs @@ -816,7 +816,11 @@ impl PdfCraftApp { // The reading-colours preference is app-wide: the newly created document starts with it. self.session.set_reading_colors(id, self.reading_colors); let Some(doc) = self.session.get(id) else { return }; - self.views.push(crate::DocView::new(id, &doc.info, self.view_defaults)); + let mut view = crate::DocView::new(id, &doc.info, self.view_defaults); + if self.full_screen { + view.enter_presentation(); + } + self.views.push(view); self.active = Some(self.views.len() - 1); self.notify_tr(message); } diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 7b421263d..40be115df 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1338,7 +1338,11 @@ impl PdfCraftApp { }; } let initial = doc.initial_view(); - self.views.push(DocView::new(id, &doc.info, self.view_defaults)); + let mut view = DocView::new(id, &doc.info, self.view_defaults); + if self.full_screen { + view.enter_presentation(); + } + self.views.push(view); if activate { self.active = Some(self.views.len() - 1); } @@ -1582,10 +1586,24 @@ impl PdfCraftApp { /// Enter or leave full-screen reading (Acrobat: View ▸ Full Screen Mode, ⌘L). pub fn set_full_screen(&mut self, ctx: &egui::Context, on: bool) { - self.full_screen = on; + self.apply_full_screen(on); ctx.send_viewport_cmd(egui::ViewportCommand::Fullscreen(on)); } + /// The full-screen state without the window: every document shows one page (or spread) at a + /// time fitted to the screen while it is on (Acrobat's Full Screen Mode, #379, #1459), and + /// gets its display back when it is off. + pub fn apply_full_screen(&mut self, on: bool) { + self.full_screen = on; + for view in &mut self.views { + if on { + view.enter_presentation(); + } else { + view.leave_presentation(); + } + } + } + /// Answer the Full Screen mode prompt a document raised on opening: enter it, or leave the /// window as it is. pub fn resolve_full_screen_prompt(&mut self, ctx: &egui::Context, allow: bool) { @@ -3131,19 +3149,16 @@ impl eframe::App for PdfCraftApp { self.window_title = title; } if self.full_screen && self.active.is_some() { - // Full screen: the page, nothing else (Esc or ⌘L to leave). - let t = theme::Tokens::get(&ctx); - egui::CentralPanel::default().frame(egui::Frame::NONE.fill(if t.dark() { t.pasteboard } else { egui::Color32::from_gray(32) })).show( - ui, - |ui| { - if let Some(i) = self.active { - match compare_ui::side_by_side(self, i) { - Some((old, new)) => compare_ui::compare_area(self, old, new, ui), - None => canvas::document_area(self, i, ui), - } + // Full screen: the page on black, nothing else, as Acrobat's Full Screen Mode (Esc + // or ⌘L to leave). + egui::CentralPanel::default().frame(egui::Frame::NONE.fill(egui::Color32::BLACK)).show(ui, |ui| { + if let Some(i) = self.active { + match compare_ui::side_by_side(self, i) { + Some((old, new)) => compare_ui::compare_area(self, old, new, ui), + None => canvas::document_area(self, i, ui), } - }, - ); + } + }); dialogs::show(self, &ctx); // Notices too: a refused field value or a failed save must be seen in full screen. self.show_progress(&ctx); diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 9cf99021a..f3d5717db 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -2,6 +2,7 @@ use egui::{Key, Modifiers, MouseWheelUnit, TouchPhase}; use egui_kittest::Harness; +use egui_kittest::kittest::Queryable; use pdfcraft_ui_egui::PdfCraftApp; /// Three 300×400 pt pages. Page 1 links to page 3. @@ -1645,3 +1646,81 @@ fn long_view_menu_fits_a_short_window() { h.run_steps(4); assert_eq!(h.state().right, Some(pdfcraft_ui_egui::RightPanel::Bookmarks)); } + +fn click_with(h: &mut Harness<'static, PdfCraftApp>, pos: egui::Pos2, modifiers: Modifiers) { + h.hover_at(pos); + h.run_steps(1); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers }); + } + h.run_steps(4); +} + +/// Full screen is Acrobat's Full Screen Mode (#379, #1459): one page at a time, fitted to the +/// screen, with no tools; a click, Space, Enter or → go forward a page and a ⇧-click, Backspace +/// or ← back; Esc brings the display from before it back, at the page reached. +#[test] +fn full_screen_shows_one_fitted_page_and_clicks_turn_it() { + use pdfcraft_ui_egui::canvas::Fit; + let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]); + h.run_steps(4); + assert!(h.state().views[0].continuous(), "continuous before"); + h.key_press_modifiers(Modifiers::COMMAND, Key::L); + h.run_steps(6); + assert!(h.state().full_screen); + let v = &h.state().views[0]; + assert!(!v.continuous(), "one page at a time"); + assert_eq!(v.fit, Fit::Page, "fitted to the screen"); + assert!(rect(&h, 0).is_some() && rect(&h, 1).is_none(), "only page 1 is on screen"); + let r = rect(&h, 0).unwrap(); + let vp = h.state().views[0].viewport_rect(); + assert!(r.height() <= vp.height() + 1.0 && r.width() <= vp.width() + 1.0, "the page fits: {r:?} in {vp:?}"); + assert!(h.query_by_label("Select (V)").is_none(), "no quick tools in full screen"); + click_with(&mut h, r.center(), Modifiers::NONE); + assert_eq!(h.state().views[0].current, 1, "a click goes forward a page"); + h.key_press(Key::ArrowRight); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 2, "→ goes forward"); + h.key_press(Key::Backspace); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 1, "Backspace goes back"); + let r = rect(&h, 1).expect("page 2 on screen"); + click_with(&mut h, r.center(), Modifiers::SHIFT); + assert_eq!(h.state().views[0].current, 0, "a shift-click goes back"); + h.key_press(Key::Space); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 1, "Space goes forward"); + h.key_press(Key::Escape); + h.run_steps(4); + assert!(!h.state().full_screen, "Escape leaves full screen"); + let v = &h.state().views[0]; + assert!(v.continuous(), "the display from before comes back"); + assert_eq!(v.fit, Fit::None); + assert!((v.zoom - 0.5).abs() < 1e-3, "zoom back to 50%: {}", v.zoom); + assert_eq!(v.current, 1, "at the page reached in full screen"); + h.run_steps(2); + assert!(h.query_by_label("Select (V)").is_some(), "the quick tools are back"); +} + +/// Two-page view stays a spread in full screen (#1353): both pages of the spread, fitted, and +/// → turns the spread. +#[test] +fn full_screen_keeps_two_page_view_as_spreads() { + use pdfcraft_ui_egui::canvas::PageLayout; + let mut h = harness(&[("layout", "two-up")]); + h.run_steps(4); + h.key_press_modifiers(Modifiers::COMMAND, Key::L); + h.run_steps(6); + let v = &h.state().views[0]; + assert_eq!(v.layout, PageLayout::TwoUp); + assert!(!v.continuous(), "one spread at a time"); + assert!(rect(&h, 0).is_some() && rect(&h, 1).is_some() && rect(&h, 2).is_none(), "the first spread only"); + h.key_press(Key::ArrowRight); + h.run_steps(4); + assert_eq!(h.state().views[0].current, 2, "→ turns the spread"); + assert!(rect(&h, 2).is_some() && rect(&h, 0).is_none()); + h.key_press(Key::Escape); + h.run_steps(4); + let v = &h.state().views[0]; + assert!(v.continuous() && v.layout == PageLayout::TwoUp, "two-page continuous view comes back"); +} diff --git a/docs/ui-parity.md b/docs/ui-parity.md index a78409fad..a6fc3c3de 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (column select #740, multi-object move #844, Shift-⌘-G #810 fixed) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (full screen presents one page at a time #1459) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the 2023+ "new Acrobat": All tools pane on the left, quick-action toolbar, Home view). Behaviour is @@ -31,7 +31,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24) | Document tabs | yes | yes, with unsaved marker | shipped | | Multiple windows, drag a tab out | yes | no | planned | | Split window, spreadsheet split | yes | no | planned | -| Read mode, full screen | yes | yes | shipped; presentation options planned | +| Read mode, full screen | yes | yes; full screen is one fitted page at a time on black, click/keys turn pages (#1459) | shipped; auto-advance, loop and transitions planned | | Light, dark, system theme | yes | yes | shipped | | Side panels: thumbnails, bookmarks, attachments, layers, comments, fields, signatures | yes | yes; panels keep their width (#704) | shipped | | Destinations, articles, model tree, Tags, Order, Content panels | yes | no | planned | @@ -81,5 +81,6 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24) | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | Full screen shows one fitted page (or spread) at a time on black with click and key paging, as Acrobat's Full Screen Mode (#379, #1459, #1353) | | 2026-10-11 | minor | Column select shipped (#740, PR #765); several objects move together on one page (#844, PR #907); Shift-⌘-G goes back (#810, PR #835) | | 2026-10-10 | major | Created from the command registry, automation tools, the View area of the checklist and open UI issues | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index e19efb0a0..2868a64ec 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1106,8 +1106,8 @@ milestone = "M3" status = "shipped" commands = ["view.full_screen"] tools = [] -evidence = ["crates/ui-egui/tests/commands.rs::full_screen_toggles_by_shortcut_and_command_and_escape_leaves"] -notes = "" +evidence = ["crates/ui-egui/tests/commands.rs::full_screen_toggles_by_shortcut_and_command_and_escape_leaves", "crates/ui-egui/tests/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it", "crates/ui-egui/tests/view.rs::full_screen_keeps_two_page_view_as_spreads"] +notes = "Acrobat's Full Screen Mode (#1459): the page on black and nothing else, one page at a time fitted to the screen (one spread when two-page view was on, #1353), from the current page; a click, Space, Enter, \u2192 or Page Down go forward, a shift-click, Backspace, \u2190 or Page Up back, Home/End to the ends; Esc or \u2318L bring the display from before back at the page reached. Links still open on click. Missing: the cursor is not hidden, no transitions, auto-advance or loop (view.full-screen-presentation), and no full-screen preferences such as the background colour" [[feature]] id = "view.full-screen-presentation" @@ -1115,11 +1115,11 @@ title = "Full-screen presentation options (auto-advance, loop, click to advance) area = "B" tier = "P2" milestone = "M12" -status = "planned" -commands = [] +status = "partial" +commands = ["view.full_screen"] tools = [] -evidence = [] -notes = "" +evidence = ["crates/ui-egui/tests/view.rs::full_screen_shows_one_fitted_page_and_clicks_turn_it"] +notes = "Click to advance (and shift-click back) ships with view.full-screen; auto-advance every N seconds, loop after the last page, page transitions and the full-screen preferences are not done" [[feature]] id = "view.reflow" From 3ed0dff80793acfa59c9ae5c1e3c897be5e50de0 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 02:21:22 +0800 Subject: [PATCH 2/5] M12: full screen survives a reload and starts after the document's own initial view (#379) Review follow-ups on #2070. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/ui-egui/src/canvas.rs | 8 +++++--- crates/ui-egui/src/lib.rs | 11 +++++++---- crates/ui-egui/tests/view.rs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 47 insertions(+), 7 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index eb539d985..51112a814 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -859,9 +859,9 @@ impl DocView { } /// The document was replaced by a newer version of its file (#431). The reader's place - /// stays: zoom, layout, rotation, page, scroll, view history and the find bar, the snapshot - /// marker and its pending close, and the page images on screen until their replacements - /// are drawn, so nothing flashes. Everything that pointed into the old version (a selected + /// stays: zoom, layout, rotation, page, scroll, full screen, view history and the find bar, + /// the snapshot marker and its pending close, and the page images on screen until their + /// replacements are drawn, so nothing flashes. Everything that pointed into the old version (a selected /// comment, a field being typed in, text being edited, a drag, page selection) starts /// afresh, so no stale index reaches the new one. pub fn document_replaced(&mut self, info: &DocInfo) { @@ -881,6 +881,8 @@ impl DocView { fresh.snapshot = self.snapshot.clone(); fresh.close_requested = self.close_requested; fresh.cover = self.cover; + // Still presenting, with the display to go back to on leaving. + fresh.presentation = self.presentation.take(); fresh.back = std::mem::take(&mut self.back); fresh.forward = std::mem::take(&mut self.forward); fresh.find = self.find.take(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 40be115df..cc7a486fc 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1338,15 +1338,18 @@ impl PdfCraftApp { }; } let initial = doc.initial_view(); - let mut view = DocView::new(id, &doc.info, self.view_defaults); - if self.full_screen { - view.enter_presentation(); - } + let view = DocView::new(id, &doc.info, self.view_defaults); self.views.push(view); if activate { self.active = Some(self.views.len() - 1); } self.apply_initial_view(self.views.len() - 1, &initial); + // After the document's own initial view, so leaving full screen comes back to it. + if self.full_screen + && let Some(view) = self.views.last_mut() + { + view.enter_presentation(); + } // Full screen is asked for, never taken: a document can't put the app in full screen on // its own (the prompt is `dialogs::full_screen_prompt`). if activate && initial.full_screen { diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index f3d5717db..74ea5965b 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -1702,6 +1702,41 @@ fn full_screen_shows_one_fitted_page_and_clicks_turn_it() { assert!(h.query_by_label("Select (V)").is_some(), "the quick tools are back"); } +/// #2070 review: full screen survives the file being reloaded (Esc still brings the display from +/// before back), and a document opened in full screen takes its own initial view first, so Esc +/// comes back to that and full screen stays one spread at a time. +#[test] +fn full_screen_survives_a_reload_and_keeps_a_new_documents_own_view() { + use pdfcraft_ui_egui::canvas::{Fit, PageLayout}; + let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]); + h.run_steps(4); + h.key_press_modifiers(Modifiers::COMMAND, Key::L); + h.run_steps(6); + { + let app = h.state_mut(); + let info = app.session.get(app.views[0].id).unwrap().info.clone(); + app.views[0].document_replaced(&info); + } + h.run_steps(4); + assert!(h.state().views[0].presenting(), "still presenting after the reload"); + h.key_press(Key::Escape); + h.run_steps(4); + let v = &h.state().views[0]; + assert!(v.continuous() && v.fit == Fit::None && (v.zoom - 0.5).abs() < 1e-3, "the display from before comes back"); + + h.key_press_modifiers(Modifiers::COMMAND, Key::L); + h.run_steps(6); + let two_columns = String::from_utf8_lossy(PAGES).replace("/Type /Catalog /Pages 2 0 R", "/Type /Catalog /Pages 2 0 R /PageLayout /TwoColumnLeft"); + h.state_mut().open_bytes("two.pdf", None, two_columns.into_bytes()).expect("opens"); + h.run_steps(6); + let v = h.state().views.last().unwrap(); + assert!(v.presenting() && v.layout == PageLayout::TwoUp && !v.continuous(), "one spread at a time"); + h.key_press(Key::Escape); + h.run_steps(4); + let v = h.state().views.last().unwrap(); + assert!(v.layout == PageLayout::TwoUp && v.continuous(), "the document's own two-column view comes back"); +} + /// Two-page view stays a spread in full screen (#1353): both pages of the spread, fitted, and /// → turns the spread. #[test] From 42e63703d91b1fe453f25fe94342accd0cb7ed90 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 12:51:54 +0800 Subject: [PATCH 3/5] M12: full screen in two-page view fits the spread it shows (#379) Review on #2070: fit_zoom fitted the document's largest page in every two-page display, so a smaller spread stayed small in full screen. While presenting, one spread at a time, it now fits the spread shown, the cover page on its own included; in the window, and while scrolling, the zoom stays one for the document. The spread arithmetic is one helper, shared with step_page. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/ui-egui/src/canvas.rs | 74 +++++++++++++++++++++++++++++++++--- 1 file changed, 69 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 51112a814..2c2ee35d7 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1292,8 +1292,7 @@ impl DocView { let last = self.page_count.saturating_sub(1); let (target, first) = match self.layout { PageLayout::TwoUp => { - // Spreads are [0, 1], [2, 3], … or, with a cover page, [0], [1, 2], [3, 4], …. - let first = if self.cover { c.saturating_sub((c + 1) % 2) } else { c - c % 2 }; + let first = self.spread_first(c); let target = match (forward, self.cover && c == 0) { (true, true) => 1, (true, false) => first.saturating_add(2), @@ -1989,14 +1988,30 @@ impl DocView { } } + /// The first page of the two-page spread page `c` is in: spreads are [0, 1], [2, 3], … or, + /// with a cover page, [0], [1, 2], [3, 4], …. + fn spread_first(&self, c: usize) -> usize { + if self.cover { c.saturating_sub((c + 1) % 2) } else { c - c % 2 } + } + fn fit_zoom(&mut self, info: &DocInfo) { let largest = |side: fn((f32, f32)) -> f32| info.pages.iter().map(|p| side(self.display_size(p))).fold(1.0, f32::max); let max_w = largest(|s| s.0); - // A single page shown on its own fits that page. Every other display (scrolling, or a - // two-page spread) fits its largest page, so the zoom holds still while pages of other - // sizes scroll or page past. + // A single page shown on its own fits that page, and full screen in two-page view fits + // the spread it shows (review on #2070). Every other display (scrolling, or two-page + // view in the window) fits its largest page, so the zoom holds still while pages of + // other sizes scroll or page past. let (w, h) = if self.layout == PageLayout::Single && !self.continuous { info.pages.get(self.current).map_or_else(|| (max_w, largest(|s| s.1)), |p| self.display_size(p)) + } else if self.presentation.is_some() && self.layout == PageLayout::TwoUp && !self.continuous { + let first = self.spread_first(self.current); + let last = if self.cover && first == 0 { 0 } else { first.saturating_add(1) }; + let spread: Vec<(f32, f32)> = info.pages.iter().skip(first).take(last - first + 1).map(|p| self.display_size(p)).collect(); + if spread.is_empty() { + (max_w, largest(|s| s.1)) + } else { + (spread.iter().map(|s| s.0).fold(1.0, f32::max), spread.iter().map(|s| s.1).fold(1.0, f32::max)) + } } else { (max_w, largest(|s| s.1)) }; @@ -6432,6 +6447,55 @@ trailer << /Root 1 0 R >> } } + /// Review on #2070: full screen in two-page view fits the spread it shows, the cover page + /// on its own included; two-page view in the window keeps one zoom for the document. + #[test] + fn full_screen_two_page_view_fits_the_spread_it_shows() { + let page = + |width: f32, height: f32| pdfcraft_render::PageInfo { width, height, label: String::new(), crop: [0.0, 0.0, width, height], rotation: 0 }; + // Spreads [0, 1] small, [2, 3] large; with a cover page [0], [1, 2] mixed, [3]. + let info = DocInfo { pages: vec![page(300.0, 400.0), page(300.0, 400.0), page(600.0, 800.0), page(600.0, 800.0)], ..Default::default() }; + let mut v = DocView::new(DocId(1), &info, ViewDefaults::default()); + (v.layout, v.continuous, v.viewport_w, v.viewport_h) = (PageLayout::TwoUp, false, 1000.0, 800.0); + v.fit = Fit::Page; + v.fit_zoom(&info); + let windowed = v.zoom; + v.enter_presentation(); + v.fit_zoom(&info); + let small = v.zoom; + assert!(small > windowed * 1.9, "the small spread fills the screen: {small} vs {windowed}"); + v.current = 3; + v.fit_zoom(&info); + assert!((v.zoom - windowed).abs() < 1e-4, "the large spread fits as before"); + v.current = 1; + v.fit_zoom(&info); + assert_eq!(v.zoom, small, "page 2 is in the first spread"); + // With a cover page: [0] alone, then [1, 2] (the large page decides), then [3]. + v.cover = true; + v.current = 0; + v.fit_zoom(&info); + assert_eq!(v.zoom, small, "the cover page alone"); + v.current = 1; + v.fit_zoom(&info); + assert!((v.zoom - windowed).abs() < 1e-4, "page 2 shares a spread with the large page 3"); + // Scrolling while in full screen fits the document, as scrolling does in the window. + (v.continuous, v.current) = (true, 0); + v.fit_zoom(&info); + assert!((v.zoom - windowed).abs() < 1e-4, "continuous full screen keeps one zoom"); + v.continuous = false; + // The cover page is a spread of its own even when the next page is larger. + let mixed = DocInfo { pages: vec![page(300.0, 400.0), page(600.0, 800.0), page(600.0, 800.0)], ..Default::default() }; + v.current = 0; + v.fit_zoom(&mixed); + assert_eq!(v.zoom, small, "the small cover alone, not with page 2"); + // Leaving full screen brings the document-wide fit back. + v.leave_presentation(); + (v.fit, v.current) = (Fit::Page, 0); + (v.layout, v.continuous) = (PageLayout::TwoUp, false); + v.fit_zoom(&info); + assert!((v.zoom - windowed).abs() < 1e-4, "in the window, one zoom for the document"); + } + #[test] fn fit_width_fits_the_shown_page_in_single_page_view_only() { let page = From 14e17ece3bfdf6dfe95bed7fa56ed02be1ccd88c Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 15:56:30 +0800 Subject: [PATCH 4/5] M3: in full screen a double-click selects a word, and full screen selects the Select tool (#1459) Review on #2070 (CodeRabbit): - A click turned the page on its release, so a double-click's second click, meant to select a word as the PR promises, landed on the next page. The turn now waits out egui's double-click time, and is dropped when a selection appears or full screen is left; a double-click on blank page area still turns once. - Entering full screen from another tool left it on, and clicks only turn pages with Select while the quick tools are hidden. Full screen now selects Select and gives the earlier tool back on leaving, as Space's temporary Hand tool does. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/ui-egui/src/canvas.rs | 23 +++++++++- crates/ui-egui/src/lib.rs | 11 +++++ crates/ui-egui/tests/ui_group/text_select.rs | 47 ++++++++++++++++++++ crates/ui-egui/tests/view.rs | 23 ++++++++++ 4 files changed, 102 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 2c2ee35d7..0bd62621b 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -470,6 +470,9 @@ pub struct DocView { clicks: u32, last_press_time: f64, last_press_pos: Option, + /// Full screen: a click's page turn, when it falls due (input time) and which way. It waits + /// out the double-click time, as the click may be the first of a double-click selecting a word. + present_turn: Option<(f64, bool)>, last_queue: Vec, /// Finished renders wait in the pool instead of reaching the view (tests): what the view /// shows before a raster arrives can be looked at however fast the renderer is. @@ -761,6 +764,7 @@ impl DocView { clicks: 0, last_press_time: 0.0, last_press_pos: None, + present_turn: None, last_queue: Vec::new(), hold_rasters: false, viewport_w: 800.0, @@ -4042,9 +4046,24 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { Some(comments::CanvasAction::CopyImageFile(p, i)) => image_action = Some(crate::edit_text_ui::ImageAction::CopyFile(p, i)), None => {} } - // Full screen: the click turned the page unless it did something else first. + // Full screen: the click turns the page unless it did something else first. It waits out the + // double-click time: turning on the first click's release sent a double-click's second click, + // meant to select a word, to the next page (review on #2070). A selection cancels it, and so + // does leaving full screen; a double-click elsewhere still turns once. + let now = ui.input(|i| i.time); if present_click && clicked_link.is_none() && view.selection.is_none() && view.clicks < 2 { - view.step_page(!present_back); + let delay = ui.ctx().options(|o| o.input_options.max_double_click_delay); + view.present_turn = Some((now + delay, !present_back)); + } + if let Some((due, forward)) = view.present_turn { + if view.selection.is_some() || view.presentation.is_none() { + view.present_turn = None; + } else if now >= due { + view.present_turn = None; + view.step_page(forward); + } else { + ui.ctx().request_repaint_after(std::time::Duration::from_secs_f64((due - now).max(0.0))); + } } match clicked_link { Some(LinkTarget::Page(p, dest)) => view.go_to_dest(p, dest, info), diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index cc7a486fc..d6c208083 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -521,6 +521,8 @@ pub struct PdfCraftApp { pub quick_tool: QuickTool, /// The tool to go back to when Space, held for a temporary Hand, is released. space_hand: Option, + /// The tool before full screen, given back on leaving it (see [`Self::apply_full_screen`]). + full_screen_tool: Option, /// Comment author, per-tool colours and widths, pin. pub comment_prefs: comments::CommentPrefs, /// Resolved colours, including the current OS theme when following the system. @@ -910,6 +912,7 @@ impl PdfCraftApp { right_panel_closed: false, quick_tool: QuickTool::Select, space_hand: None, + full_screen_tool: None, comment_prefs: Default::default(), theme: ThemeKind::Light, theme_preference: ThemePreference::Light, @@ -1597,6 +1600,14 @@ impl PdfCraftApp { /// time fitted to the screen while it is on (Acrobat's Full Screen Mode, #379, #1459), and /// gets its display back when it is off. pub fn apply_full_screen(&mut self, on: bool) { + // A click turns the page only with the Select tool, and full screen hides the tools: + // select it there, and give the tool back on leaving (review on #2070). + if on && !self.full_screen { + self.full_screen_tool = Some(self.quick_tool); + self.quick_tool = QuickTool::Select; + } else if !on && let Some(tool) = self.full_screen_tool.take() { + self.quick_tool = tool; + } self.full_screen = on; for view in &mut self.views { if on { diff --git a/crates/ui-egui/tests/ui_group/text_select.rs b/crates/ui-egui/tests/ui_group/text_select.rs index 9ed4e9d5d..cf484785d 100644 --- a/crates/ui-egui/tests/ui_group/text_select.rs +++ b/crates/ui-egui/tests/ui_group/text_select.rs @@ -544,3 +544,50 @@ fn coalesced_primary_clicks_select_line_then_spaced_click_clears() { assert_eq!(doc.saved_bytes(), saved); } } + +/// Review on #2070: in full screen a click goes forward a page, but a double-click selects the +/// word and stays: the click waits out the double-click time before turning. Turning on the first +/// click's release had sent the second click to the next page. Leaving full screen meanwhile +/// drops the pending turn. +#[test] +fn full_screen_double_click_selects_a_word_and_stays_on_the_page() { + // The same page twice. + let two_pages = String::from_utf8(two_lines()) + .unwrap() + .replace("/Kids [3 0 R] /Count 1", "/Kids [3 0 R 6 0 R] /Count 2") + .replace( + "trailer", + "6 0 obj << /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >> endobj\ntrailer", + ) + .into_bytes(); + let (mut h, _) = harness_with(two_pages, |_| {}); + h.state_mut().apply_full_screen(true); + crate::support::wait_until(&mut h, "the page in full screen", |app| app.views[0].glyph_screen_pos(0, 5).is_some() && !app.render_pending()); + let at = h.state().views[0].glyph_screen_pos(0, 5).expect("text layer"); + h.hover_at(at); + h.run_steps(1); + + // A double-click selects "quick" and stays on page 1, also once the double-click time is over. + click(&mut h, at); + click(&mut h, at); + assert_eq!(h.state().views[0].selected_text().as_deref(), Some("quick")); + h.run_steps(30); + assert_eq!(h.state().views[0].current, 0, "the double-click didn't turn the page"); + + // A single click turns it once the double-click time is over. + // Entering full screen again clears the selection. + h.state_mut().apply_full_screen(false); + h.state_mut().apply_full_screen(true); + h.run_steps(30); + click(&mut h, at); + h.run_steps(30); + assert_eq!(h.state().views[0].current, 1, "a click goes forward"); + + // A click right before leaving full screen doesn't turn the page after it. + h.state_mut().views[0].go_to_page(0); + h.run_steps(30); + click(&mut h, at); + h.state_mut().apply_full_screen(false); + h.run_steps(30); + assert_eq!(h.state().views[0].current, 0, "no page turn after leaving"); +} diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 74ea5965b..df653bf6e 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -1759,3 +1759,26 @@ fn full_screen_keeps_two_page_view_as_spreads() { let v = &h.state().views[0]; assert!(v.continuous() && v.layout == PageLayout::TwoUp, "two-page continuous view comes back"); } + +/// Review on #2070: full screen hides the tools and a click turns the page only with Select, so +/// entering it from another tool selects Select, and leaving gives the tool back. +#[test] +fn full_screen_clicks_turn_pages_whatever_tool_was_on() { + use pdfcraft_ui_egui::QuickTool; + let mut h = harness(&[]); + h.run_steps(4); + h.state_mut().quick_tool = QuickTool::Hand; + h.key_press_modifiers(Modifiers::COMMAND, Key::L); + h.run_steps(6); + assert!(h.state().full_screen); + assert_eq!(h.state().quick_tool, QuickTool::Select, "Select in full screen"); + // Entering again (a second document asking for it) keeps the tool to give back. + h.state_mut().apply_full_screen(true); + let r = rect(&h, 0).expect("page 1 on screen"); + click_with(&mut h, r.center(), Modifiers::NONE); + assert_eq!(h.state().views[0].current, 1, "a click goes forward"); + h.key_press(Key::Escape); + h.run_steps(4); + assert!(!h.state().full_screen); + assert_eq!(h.state().quick_tool, QuickTool::Hand, "the tool from before is back"); +} From 3fff309dd388d4b850a758ce054f49db513f8a32 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 19 Oct 2026 16:21:20 +0800 Subject: [PATCH 5/5] M3: a link followed in full screen cancels the click's pending page turn (#1459) Review on #2070: a click on the page schedules a turn after the double-click time, and a link clicked before then jumped and then the page still moved on. Following a link now drops the pending turn. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Gv2WwdgjmBz33GWkwgNn3Q --- crates/ui-egui/src/canvas.rs | 5 +-- crates/ui-egui/tests/ui_group/text_select.rs | 34 ++++++++++++++++++++ 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 0bd62621b..5ebc539ec 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -4049,14 +4049,15 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { // Full screen: the click turns the page unless it did something else first. It waits out the // double-click time: turning on the first click's release sent a double-click's second click, // meant to select a word, to the next page (review on #2070). A selection cancels it, and so - // does leaving full screen; a double-click elsewhere still turns once. + // do a link followed meanwhile and leaving full screen; a double-click elsewhere still turns + // once. let now = ui.input(|i| i.time); if present_click && clicked_link.is_none() && view.selection.is_none() && view.clicks < 2 { let delay = ui.ctx().options(|o| o.input_options.max_double_click_delay); view.present_turn = Some((now + delay, !present_back)); } if let Some((due, forward)) = view.present_turn { - if view.selection.is_some() || view.presentation.is_none() { + if clicked_link.is_some() || view.selection.is_some() || view.presentation.is_none() { view.present_turn = None; } else if now >= due { view.present_turn = None; diff --git a/crates/ui-egui/tests/ui_group/text_select.rs b/crates/ui-egui/tests/ui_group/text_select.rs index cf484785d..509775f6a 100644 --- a/crates/ui-egui/tests/ui_group/text_select.rs +++ b/crates/ui-egui/tests/ui_group/text_select.rs @@ -545,6 +545,40 @@ fn coalesced_primary_clicks_select_line_then_spaced_click_clears() { } } +/// Review on #2070: a click on the page in full screen waits out the double-click time before +/// it turns the page, and a link clicked meanwhile used to leave that turn pending: the link +/// jumped to page 3, then the page moved on to page 4. +#[test] +fn full_screen_link_click_cancels_the_pending_page_turn() { + let page = "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 300 200] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>"; + let four_pages = String::from_utf8(two_lines()) + .unwrap() + .replace("/Kids [3 0 R] /Count 1", "/Kids [3 0 R 6 0 R 7 0 R 8 0 R] /Count 4") + .replace("/Contents 4 0 R /Resources", "/Annots [9 0 R] /Contents 4 0 R /Resources") + .replace( + "trailer", + &format!( + "6 0 obj {page} endobj\n7 0 obj {page} endobj\n8 0 obj {page} endobj\n9 0 obj << /Type /Annot /Subtype /Link /Rect [200 20 280 60] /Border [0 0 0] /Dest [7 0 R /Fit] >> endobj\ntrailer" + ), + ) + .into_bytes(); + let (mut h, _) = harness_with(four_pages, |_| {}); + h.state_mut().apply_full_screen(true); + crate::support::wait_until(&mut h, "the page in full screen", |app| app.views[0].glyph_screen_pos(0, 5).is_some() && !app.render_pending()); + let r = h.state().views[0].page_screen_rect(0).expect("page 1 on screen"); + let at = |x: f32, y: f32| egui::pos2(r.left() + x / 300.0 * r.width(), r.top() + (200.0 - y) / 200.0 * r.height()); + // A click on blank page area, then, within the double-click time, a click on the link. + h.hover_at(at(150.0, 90.0)); + h.run_steps(1); + click(&mut h, at(150.0, 90.0)); + h.run_steps(3); + h.hover_at(at(240.0, 40.0)); + h.run_steps(1); + click(&mut h, at(240.0, 40.0)); + h.run_steps(30); + assert_eq!(h.state().views[0].current, 2, "the link's page, and no turn after it"); +} + /// Review on #2070: in full screen a click goes forward a page, but a double-click selects the /// word and stays: the click waits out the double-click time before turning. Turning on the first /// click's release had sent the second click to the next page. Leaving full screen meanwhile