From 583c1ec68c75024d77066983a79c05f27141f820 Mon Sep 17 00:00:00 2001 From: abc2cb Date: Thu, 22 Oct 2026 00:34:16 +0300 Subject: [PATCH] M3: the mode bar has no Read tab, as in Acrobat (#2240) * M3: the mode bar has no Read tab, as in Acrobat Acrobat's mode bar is All tools, Edit, Convert and E-Sign: reading is what the viewer does with no tool in use. Read mode (the panes hidden) stays a View command and a default workspace. Tests that clicked the Read tab as an ordinary mode-bar button use Convert instead. Co-Authored-By: Claude Opus 5.5 * M3: parity notes for the mode bar without a Read tab Co-Authored-By: Claude Opus 5.5 * M3: the Read-mode tests enter through the View command The mode bar no longer has a Read tab, so `read_mode_hides_the_quick_toolbar` (ui.rs) and `switching_modes_keeps_the_current_page_at_fixed_zoom` (view.rs) clicked a button that is gone, failing the test jobs on ubuntu-latest and windows-latest. Both run `view.read_mode` now, as the other read-mode tests in this PR do; the second is the test the `view.read-mode` parity entry cites. `cargo test -p pdfcraft-ui-egui --test ui --test view`: 56 and 55 passed. --------- Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/ui-egui/src/chrome.rs | 6 ++-- crates/ui-egui/tests/control.rs | 62 ++++++++++++++++++--------------- crates/ui-egui/tests/ui.rs | 21 +++++++++-- crates/ui-egui/tests/view.rs | 2 +- parity/acrobat-features.toml | 4 +-- 5 files changed, 57 insertions(+), 38 deletions(-) diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index e01d752c40..48d1d3c44c 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -500,9 +500,9 @@ pub fn mode_bar(app: &mut PdfCraftApp, ui: &mut egui::Ui) { ui.add_space(6.0); ui.painter().vline(ui.cursor().left(), ui.max_rect().y_range().shrink(12.0), Stroke::new(1.0, t.divider)); ui.add_space(10.0); - for (mode, label) in - [(Mode::AllTools, "All tools"), (Mode::Read, "Read"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")] - { + // Acrobat's mode bar has no Read tab: reading is what the viewer does with no tool + // in use. Read mode (the panes hidden) stays a View command and a default workspace. + for (mode, label) in [(Mode::AllTools, "All tools"), (Mode::Edit, "Edit"), (Mode::Convert, "Convert"), (Mode::Sign, "E-Sign")] { if widgets::mode_tab(ui, tl!(label), app.mode == mode).clicked() { app.switch_mode(mode, true); } diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index e6af166d77..20fa4cb230 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -119,26 +119,28 @@ fn control_resize_reports_observed_dimensions_and_rejects_invalid_sizes() { #[test] fn control_focus_traverses_real_widgets_and_keyboard_activates() { let (mut h, c) = harness(); - ok(&mut h, &c, "ui.focus", json!({"label": "Read"})); - let read = ok(&mut h, &c, "ui.inspect", json!({"query": "Read", "role": "Button"})); - assert_eq!(read["widgets"][0]["focused"], true, "{read}"); - assert_eq!(read["widgets"][0]["focusable"], true); + ok(&mut h, &c, "ui.focus", json!({"label": "Convert"})); + let convert = ok(&mut h, &c, "ui.inspect", json!({"query": "Convert", "role": "Button"})); + let focused = + |found: &serde_json::Value, label: &str| found["widgets"].as_array().unwrap().iter().any(|w| w["label"] == label && w["focused"] == true); + assert!(focused(&convert, "Convert"), "{convert}"); + assert!(convert["widgets"].as_array().unwrap().iter().any(|w| w["label"] == "Convert" && w["focusable"] == true)); ok(&mut h, &c, "ui.key", json!({"key": "Tab"})); - let edit = ok(&mut h, &c, "ui.inspect", json!({"query": "Edit", "role": "Button"})); - assert!(edit["widgets"].as_array().unwrap().iter().any(|w| w["label"] == "Edit" && w["focused"] == true), "{edit}"); + let sign = ok(&mut h, &c, "ui.inspect", json!({"query": "E-Sign", "role": "Button"})); + assert!(focused(&sign, "E-Sign"), "{sign}"); ok(&mut h, &c, "ui.key", json!({"key": "Tab", "modifiers": ["shift"]})); - let read = ok(&mut h, &c, "ui.inspect", json!({"query": "Read", "role": "Button"})); - assert_eq!(read["widgets"][0]["focused"], true); + let convert = ok(&mut h, &c, "ui.inspect", json!({"query": "Convert", "role": "Button"})); + assert!(focused(&convert, "Convert"), "{convert}"); ok(&mut h, &c, "ui.key", json!({"key": "Enter"})); let state = ok(&mut h, &c, "ui.state", json!({})); - assert_eq!(state["mode"], "Read"); + assert_eq!(state["mode"], "Convert"); assert_eq!(state["documents"][0]["dirty"], false); } #[test] fn control_button_focus_preserves_document_undo_redo_shortcuts() { let (mut h, c) = harness_pages(2); - ok(&mut h, &c, "ui.focus", json!({"label": "Read"})); + ok(&mut h, &c, "ui.focus", json!({"label": "Convert"})); ok(&mut h, &c, "ui.key", json!({"key": "Enter"})); ok(&mut h, &c, "ui.click", json!({"label": "All tools"})); assert!(h.ctx.egui_wants_keyboard_input()); @@ -176,9 +178,9 @@ fn control_focus_rejects_missing_ambiguous_disabled_and_nonfocusable_targets() { json!({"label": "does not exist"}), json!({"id": "not a number"}), json!({"id": 0}), - json!({"id": 1, "label": "Read"}), + json!({"id": 1, "label": "Convert"}), json!({"label": false}), - json!({"label": "Read", "force": true}), + json!({"label": "Convert", "force": true}), json!([]), ] { assert!(call(&mut h, &c, "ui.focus", params).is_err()); @@ -186,9 +188,10 @@ fn control_focus_rejects_missing_ambiguous_disabled_and_nonfocusable_targets() { let widgets = ok(&mut h, &c, "ui.inspect", json!({})); let nonfocusable = widgets["widgets"].as_array().unwrap().iter().find(|w| w["focusable"] == false).unwrap(); assert!(call(&mut h, &c, "ui.focus", json!({"id": nonfocusable["id"]})).is_err()); - let read = widgets["widgets"].as_array().unwrap().iter().find(|w| w["label"] == "Read").unwrap(); - ok(&mut h, &c, "ui.focus", json!({"id": read["id"]})); - assert_eq!(ok(&mut h, &c, "ui.inspect", json!({"query": "Read", "role": "Button"}))["widgets"][0]["focused"], true); + let convert = widgets["widgets"].as_array().unwrap().iter().find(|w| w["label"] == "Convert").unwrap(); + ok(&mut h, &c, "ui.focus", json!({"id": convert["id"]})); + let found = ok(&mut h, &c, "ui.inspect", json!({"query": "Convert", "role": "Button"})); + assert!(found["widgets"].as_array().unwrap().iter().any(|w| w["label"] == "Convert" && w["focused"] == true), "{found}"); // Home has no active document, so its document-only toolbar controls are disabled. ok(&mut h, &c, "ui.click", json!({"label": "Home"})); assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["home"], true); @@ -740,8 +743,8 @@ fn japanese_controls_and_search_keep_command_ids() { let (mut h, c) = harness(); ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "ja" })); assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["language"], "ja"); - ok(&mut h, &c, "ui.click", json!({ "label": "閲覧" })); - assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read"); + ok(&mut h, &c, "ui.click", json!({ "label": "変換" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Convert"); ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" })); let prefs = ok(&mut h, &c, "ui.inspect", json!({ "query": "表示言語" })); @@ -766,8 +769,8 @@ fn japanese_controls_and_search_keep_command_ids() { fn german_preferences_and_search_keep_command_ids() { let (mut h, c) = harness(); ok(&mut h, &c, "ui.set", json!({ "key": "language", "value": "de" })); - ok(&mut h, &c, "ui.click", json!({ "label": "Lesen" })); - assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read"); + ok(&mut h, &c, "ui.click", json!({ "label": "Konvertieren" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Convert"); ok(&mut h, &c, "ui.command", json!({ "id": "app.preferences" })); let prefs = ok(&mut h, &c, "ui.inspect", json!({ "query": "Sprache der Oberfläche" })); assert!(prefs["count"].as_u64().unwrap() > 0, "{prefs}"); @@ -958,23 +961,24 @@ fn simplified_chinese_signature_prompts_and_errors_keep_document_state() { #[test] fn inspect_and_click_by_label_and_id() { let (mut h, c) = harness(); - let found = ok(&mut h, &c, "ui.inspect", json!({ "query": "read" })); - let read = found["widgets"].as_array().unwrap().iter().find(|w| w["label"] == "Read" && w["clickable"] == true).cloned().expect("a Read tab"); - let rect = read["rect"].as_array().unwrap(); + let found = ok(&mut h, &c, "ui.inspect", json!({ "query": "convert" })); + let convert = + found["widgets"].as_array().unwrap().iter().find(|w| w["label"] == "Convert" && w["clickable"] == true).cloned().expect("a Convert tab"); + let rect = convert["rect"].as_array().unwrap(); assert!(rect[2].as_f64().unwrap() > rect[0].as_f64().unwrap()); - ok(&mut h, &c, "ui.click", json!({ "label": "Read" })); - assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read"); + ok(&mut h, &c, "ui.click", json!({ "label": "Convert" })); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Convert"); let all = ok(&mut h, &c, "ui.inspect", json!({ "query": "all tools" })); let id = all["widgets"].as_array().unwrap().iter().find(|w| w["label"] == "All tools" && w["clickable"] == true).unwrap()["id"].clone(); ok(&mut h, &c, "ui.click", json!({ "id": id })); assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "AllTools"); - // Clicking a point works too (here: the Read tab's centre). + // Clicking a point works too (here: the Convert tab's centre). let [x0, y0, x1, y1] = [0, 1, 2, 3].map(|i| rect[i].as_f64().unwrap()); ok(&mut h, &c, "ui.click", json!({ "x": (x0 + x1) / 2.0, "y": (y0 + y1) / 2.0 })); - assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Read"); + assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["mode"], "Convert"); let err = call(&mut h, &c, "ui.click", json!({ "label": "No such button" })).unwrap_err(); assert!(err.contains("no enabled clickable widget"), "{err}"); @@ -1622,10 +1626,10 @@ fn click_and_drag_refuse_coordinates_that_overflow_like_move() { let e = call(&mut h, &c, method, params.clone()).expect_err(&format!("{method} {params} is refused")); assert!(e.starts_with(&format!("{method}: ")) && e.contains("finite"), "{e}"); h.run_steps(4); - assert_eq!(ok(&mut h, &c, "ui.inspect", json!({ "query": "Read", "role": "Button" }))["count"], 1); + assert_eq!(ok(&mut h, &c, "ui.inspect", json!({ "query": "Convert", "role": "Button" }))["count"], 1); } - let read = ok(&mut h, &c, "ui.inspect", json!({ "query": "Read", "role": "Button" }))["widgets"][0]["rect"].clone(); - let (x, y) = (read[0].as_f64().unwrap() + 4.0, read[1].as_f64().unwrap() + 4.0); + let convert = ok(&mut h, &c, "ui.inspect", json!({ "query": "Convert", "role": "Button" }))["widgets"][0]["rect"].clone(); + let (x, y) = (convert[0].as_f64().unwrap() + 4.0, convert[1].as_f64().unwrap() + 4.0); assert_eq!(ok(&mut h, &c, "ui.click", json!({ "x": x, "y": y }))["clicked"], json!([x, y])); ok(&mut h, &c, "ui.drag", json!({ "from": [x, y], "to": [x + 10.0, y], "steps": 2 })); } diff --git a/crates/ui-egui/tests/ui.rs b/crates/ui-egui/tests/ui.rs index 118dee8f74..a74e588790 100644 --- a/crates/ui-egui/tests/ui.rs +++ b/crates/ui-egui/tests/ui.rs @@ -192,6 +192,21 @@ fn read_mode_rail_buttons_open_their_panels() { h.get_by_label("Alpha section"); } +/// The mode bar matches Acrobat's: All tools, Edit, Convert and E-Sign, with no Read tab. Read +/// mode is still a View command. +#[test] +fn the_mode_bar_has_no_read_tab() { + use pdfcraft_ui_egui::Mode; + let mut h = harness(|app| app.open_bytes("fixture.pdf", None, FIXTURE.to_vec()).expect("fixture opens")); + for label in ["All tools", "Edit", "Convert", "E-Sign"] { + assert!(h.query_all_by_label(label).next().is_some(), "the {label} tab is there"); + } + assert!(h.query_all_by_label("Read").next().is_none(), "no Read tab"); + h.state_mut().run_command("view.read_mode"); + h.run_steps(2); + assert_eq!(h.state().mode, Mode::Read); +} + /// A document that asks to open in Full Screen mode (`/PageMode /FullScreen`) is asked about /// first, as in Acrobat: "Not now" leaves the window as it is, "Full Screen" enters it. #[test] @@ -227,7 +242,7 @@ 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.state_mut().run_command("view.read_mode"); h.run_steps(3); assert_eq!(h.state().mode, Mode::Read); assert_eq!(h.state().right, None); @@ -254,7 +269,7 @@ fn read_mode_hides_the_quick_toolbar() { h.get_by_label("Hand (H)").click(); h.run_steps(2); assert_eq!(h.state().quick_tool, QuickTool::Hand); - h.get_by_label("Read").click(); + h.state_mut().run_command("view.read_mode"); h.run_steps(3); assert!(h.query_by_label("Select (V)").is_none(), "no quick toolbar in Read mode"); assert_eq!(h.state().quick_tool, QuickTool::Select); @@ -286,7 +301,7 @@ 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.state_mut().run_command("view.read_mode"); h.run_steps(3); assert_eq!(h.state().right, None); // Open Comments from the rail, then close it again. diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 9cf99021a3..39ea83df76 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -1594,7 +1594,7 @@ fn switching_modes_keeps_the_current_page_at_fixed_zoom() { h.event(wheel(MouseWheelUnit::Point, -35.0, TouchPhase::Move)); h.run_steps(8); let before = rect(&h, 17).unwrap().top() - h.state().views[0].viewport_rect().top(); - h.get_by_label("Read").click(); + h.state_mut().run_command("view.read_mode"); h.run_steps(8); assert_eq!(h.state().views[0].current, 17, "switching to Read should keep the current page"); let after = rect(&h, 17).unwrap().top() - h.state().views[0].viewport_rect().top(); diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 7aca7d5c8b..6caa46d3a2 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1094,8 +1094,8 @@ milestone = "M3" status = "shipped" commands = ["view.read_mode"] tools = [] -evidence = ["crates/ui-egui/tests/control.rs::inspect_and_click_by_label_and_id", "crates/ui-egui/tests/commands.rs::every_registered_command_is_implemented", "crates/ui-egui/tests/view.rs::switching_modes_keeps_the_current_page_at_fixed_zoom"] -notes = "Switching between Read and the docking workspace preserves the document’s current scroll position at fixed zoom; each tab keeps its own position" +evidence = ["crates/ui-egui/tests/control.rs::inspect_and_click_by_label_and_id", "crates/ui-egui/tests/commands.rs::every_registered_command_is_implemented", "crates/ui-egui/tests/ui.rs::the_mode_bar_has_no_read_tab", "crates/ui-egui/tests/view.rs::switching_modes_keeps_the_current_page_at_fixed_zoom"] +notes = "View ▸ Read mode (and the Read mode workspace) hides the panes and tool chrome. The mode bar has no Read tab, as in Acrobat, where reading is the viewer with no tool in use: its tabs are All tools, Edit, Convert and E-Sign. Switching between Read and the docking workspace preserves the document’s current scroll position at fixed zoom; each tab keeps its own position" [[feature]] id = "view.full-screen"