mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-25 18:59:48 +00:00
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 <noreply@anthropic.com> * M3: parity notes for the mode bar without a Read tab Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 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 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user