diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 4903b2226..3367bce9b 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -364,7 +364,7 @@ pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) { [(Mode::AllTools, "All tools"), (Mode::Read, "Read"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")] { if widgets::mode_tab(ui, tl!(label), app.mode == mode).clicked() { - app.select_mode(mode); + app.switch_mode(mode, true); } } ui.with_layout(Layout::right_to_left(Align::Center), |ui| { diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 546ea74c3..17d2b7c56 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -242,7 +242,7 @@ impl PdfCraftApp { None => self.full_screen = on, } } - "view.read_mode" => self.mode = if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, + "view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false), "view.theme" => { let next = if self.theme == ThemeKind::Light { ThemePreference::Dark } else { ThemePreference::Light }; self.set_theme_preference(next); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c6886a9f3..07f3ab58a 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -408,6 +408,10 @@ pub struct PdfCraftApp { pub left: LeftPanel, pub left_open: bool, pub right: Option, + /// The right panel that was open when the user switched to Read mode, which starts with the + /// panes closed; leaving Read mode brings it back unless the user opened or closed a pane in + /// Read mode meanwhile. + right_before_read: Option, /// The user closed the Comments panel, so picking a comment tool leaves it closed until they /// open it again (#225). Remembered across restarts. pub comments_panel_closed: bool, @@ -730,6 +734,7 @@ impl PdfCraftApp { pending_panel_reveals: Vec::new(), left_open: true, right: None, + right_before_read: None, comments_panel_closed: false, quick_tool: QuickTool::Select, space_hand: None, @@ -957,7 +962,9 @@ impl PdfCraftApp { let doc = self.session.get(id).ok_or("the document could not be opened")?; let pages = doc.info.pages.len(); // Acrobat opens straight to the Comments panel when a document has comments. - if activate && self.right.is_none() { + // Read mode opens with the panes closed; the rail still opens them (#932). + let opening_mode = self.mode_override.unwrap_or(self.default_mode); + if activate && self.right.is_none() && opening_mode != Mode::Read { self.right = if !doc.info.annotations.is_empty() { Some(RightPanel::Comments) } else if !doc.info.outline.is_empty() { @@ -1421,9 +1428,36 @@ impl PdfCraftApp { }; } + /// Switches workspace because the user chose to (mode bar, Read mode command). Entering Read + /// mode closes the right panel, as Acrobat's Read mode hides its panes, and leaving it reopens + /// that panel. The rail stays in Read mode, and its buttons open panels there too (#932). + pub(crate) fn switch_mode(&mut self, mode: Mode, select_tools: bool) { + let was_read = self.mode == Mode::Read; + if select_tools { + self.select_mode(mode); + } else { + self.mode = mode; + } + if mode == Mode::Read && !was_read { + self.right_before_read = self.right.take(); + } else if mode != Mode::Read && was_read { + let before = self.right_before_read.take(); + if self.right.is_none() { + self.right = before; + } + if self.right.is_some() { + self.pending_panel_reveals.push(docking::Panel::Inspector); + } + } + } + /// Opens a right panel, or closes it with `None`, because the user chose to. Closing Comments - /// keeps comment tools from reopening it; opening it again lets them (#225). + /// keeps comment tools from reopening it; opening it again lets them (#225). A choice made in + /// Read mode replaces the pane saved on the way in, so leaving Read mode doesn't undo it. pub fn choose_right_panel(&mut self, panel: Option) { + if self.mode == Mode::Read { + self.right_before_read = None; + } if panel == Some(RightPanel::Comments) { self.comments_panel_closed = false; } else if panel.is_none() && self.right == Some(RightPanel::Comments) { @@ -2165,6 +2199,17 @@ impl eframe::App for PdfCraftApp { chrome::mode_bar(self, ui); if self.active.is_some() { chrome::right_rail(self, ui); + // Read mode hides the tools, not the navigation panes: the rail stays visible + // there, so its buttons (Bookmarks, Page thumbnails, ...) must open their panels. + // Outside Read mode the docking workspace draws the inspector. + if self.mode == Mode::Read && self.right.is_some() { + egui::Panel::right("read_right_panel") + .resizable(true) + .default_size(330.0) + .size_range(260.0..=520.0) + .frame(egui::Frame::NONE) + .show(ui, |ui| panels::right_panel(self, ui)); + } } let t = theme::Tokens::get(&ctx); egui::CentralPanel::default().frame(egui::Frame::NONE.fill(t.pasteboard)).show(ui, |ui| { diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 2a94e78d0..b35743693 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -114,6 +114,99 @@ fn opening_a_pdf_shows_comments_and_bookmarks() { h.get_by_label("Beta section"); } +/// Read mode hides the tools, not the navigation panes: the rail's Bookmarks button opens the +/// bookmarks when Read is the default workspace (#932), and the left tool pane stays hidden. +#[test] +fn read_mode_rail_buttons_open_their_panels() { + use pdfcraft_ui_egui::{Mode, RightPanel}; + let mut h = harness(|app| { + app.default_mode = Mode::Read; + app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens"); + }); + assert_eq!(h.state().mode, Mode::Read); + assert_eq!(h.state().right, None, "Read mode opens with the panes closed"); + assert!(h.query_by_label("Alpha section").is_none()); + h.get_by_label("Bookmarks").click(); + h.run_steps(3); + assert_eq!(h.state().right, Some(RightPanel::Bookmarks)); + h.get_by_label("Alpha section"); + h.get_by_label("Beta section"); + // The same button closes them again (the open panel's title is also "Bookmarks"). + h.get_by_role_and_label(egui::accesskit::Role::Button, "Bookmarks").click(); + h.run_steps(3); + assert_eq!(h.state().right, None); + assert!(h.query_by_label("Alpha section").is_none()); + // A document that asks for its outline (/PageMode /UseOutlines) shows it in Read mode too. + let pdf = String::from_utf8(FIXTURE.to_vec()).unwrap().replace("/Outlines 6 0 R", "/Outlines 6 0 R /PageMode /UseOutlines"); + h.state_mut().open_bytes("outline.pdf", None, pdf.into_bytes()).unwrap(); + h.run_steps(3); + assert_eq!(h.state().mode, Mode::Read); + h.get_by_label("Alpha section"); +} + +/// Switching to Read mode closes the open pane, and switching back reopens it. +#[test] +fn read_mode_closes_the_panes_and_leaving_it_brings_them_back() { + use pdfcraft_ui_egui::{Mode, RightPanel}; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + assert_eq!(h.state().right, Some(RightPanel::Comments)); + h.get_by_label("Read").click(); + h.run_steps(3); + assert_eq!(h.state().mode, Mode::Read); + assert_eq!(h.state().right, None); + assert!(h.query_by_label_contains("Check the numbers").is_none(), "the Comments panel is hidden"); + h.state_mut().run_command("view.read_mode"); + h.run_steps(3); + assert_eq!(h.state().mode, Mode::AllTools); + assert_eq!(h.state().right, Some(RightPanel::Comments)); + // A pane opened in Read mode stays open on the way out. + h.state_mut().run_command("view.read_mode"); + h.run_steps(2); + h.get_by_label("Page thumbnails").click(); + h.run_steps(2); + h.get_by_label("All tools").click(); + h.run_steps(3); + assert_eq!(h.state().right, Some(RightPanel::Pages)); +} + +/// A pane closed in Read mode stays closed on the way out: leaving Read mode doesn't bring back the +/// pane saved on the way in, and closing Comments keeps its close preference. +#[test] +fn a_pane_closed_in_read_mode_stays_closed_when_leaving_it() { + use pdfcraft_ui_egui::{Mode, RightPanel}; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + assert_eq!(h.state().right, Some(RightPanel::Comments)); + h.get_by_label("Read").click(); + h.run_steps(3); + assert_eq!(h.state().right, None); + // Open Comments from the rail, then close it again. + h.get_by_role_and_label(egui::accesskit::Role::Button, "Comments").click(); + h.run_steps(3); + assert_eq!(h.state().right, Some(RightPanel::Comments)); + h.get_by_role_and_label(egui::accesskit::Role::Button, "Comments").click(); + h.run_steps(3); + assert_eq!(h.state().right, None); + assert!(h.state().comments_panel_closed); + // Leave Read mode through the toolbar. + h.get_by_label("All tools").click(); + h.run_steps(3); + assert_eq!(h.state().mode, Mode::AllTools); + assert_eq!(h.state().right, None, "the pane closed in Read mode stays closed"); + assert!(h.state().comments_panel_closed); + // The same through the Read mode command. + h.state_mut().choose_right_panel(Some(RightPanel::Comments)); + h.state_mut().run_command("view.read_mode"); + h.run_steps(2); + assert_eq!(h.state().mode, Mode::Read); + h.state_mut().choose_right_panel(Some(RightPanel::Comments)); + h.state_mut().choose_right_panel(None); + h.state_mut().run_command("view.read_mode"); + h.run_steps(3); + assert_ne!(h.state().mode, Mode::Read); + assert_eq!(h.state().right, None); + assert!(h.state().comments_panel_closed); +} + /// A right-to-left file name opens, lays out and paints; the tab's accessible name keeps the /// logical text (only the painted label is put in visual order). #[test]