diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index e01d752c4..48d1d3c44 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 e6af166d7..20fa4cb23 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 118dee8f7..a74e58879 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 9cf99021a..39ea83df7 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 7aca7d5c8..6caa46d3a 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"