Compare commits

...

3 Commits

Author SHA1 Message Date
Brandon Thomas be74e7b5b7 Select tool: a multi-click selects only on the frame egui reports it, so a cleared selection (or the highlighter's marked word) isn't selected again
The highlighter test now clicks at a human pace, with frames in between, which is what exposed it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:23:27 -04:00
Brandon Thomas f46cf56be3 Preferences scroll in a short window; tests scroll to controls below the fold (#487's roomier rows); the highlighter test waits for its mark under load
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 17:06:34 -04:00
Brandon Thomas 30a2c8251d M14: DATE_LANGUAGES is a static (21 entries trip clippy's large_const_arrays); rustfmt the Greek entry
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 16:37:55 -04:00
11 changed files with 75 additions and 7 deletions

View File

@ -56,7 +56,7 @@ const AR_DAYS: [&str; 7] = ["الأحد", "الاثنين", "الثلاثاء",
/// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language): /// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language):
/// the interface languages, in the same order and with the same names. /// the interface languages, in the same order and with the same names.
pub const DATE_LANGUAGES: [DateLanguage; 21] = [ pub static DATE_LANGUAGES: [DateLanguage; 21] = [
DateLanguage { DateLanguage {
code: "en", code: "en",
name: "English", name: "English",
@ -297,8 +297,34 @@ pub const DATE_LANGUAGES: [DateLanguage; 21] = [
DateLanguage { DateLanguage {
code: "el", code: "el",
name: "Ελληνικά", name: "Ελληνικά",
months: ["Ιανουάριος", "Φεβρουάριος", "Μάρτιος", "Απρίλιος", "Μάιος", "Ιούνιος", "Ιούλιος", "Αύγουστος", "Σεπτέμβριος", "Οκτώβριος", "Νοέμβριος", "Δεκέμβριος"], months: [
months_with_day: Some(["Ιανουαρίου", "Φεβρουαρίου", "Μαρτίου", "Απριλίου", "Μαΐου", "Ιουνίου", "Ιουλίου", "Αυγούστου", "Σεπτεμβρίου", "Οκτωβρίου", "Νοεμβρίου", "Δεκεμβρίου"]), "Ιανουάριος",
"Φεβρουάριος",
"Μάρτιος",
"Απρίλιος",
"Μάιος",
"Ιούνιος",
"Ιούλιος",
"Αύγουστος",
"Σεπτέμβριος",
"Οκτώβριος",
"Νοέμβριος",
"Δεκέμβριος",
],
months_with_day: Some([
"Ιανουαρίου",
"Φεβρουαρίου",
"Μαρτίου",
"Απριλίου",
"Μαΐου",
"Ιουνίου",
"Ιουλίου",
"Αυγούστου",
"Σεπτεμβρίου",
"Οκτωβρίου",
"Νοεμβρίου",
"Δεκεμβρίου",
]),
months_short: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαΐ", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"], months_short: ["Ιαν", "Φεβ", "Μαρ", "Απρ", "Μαΐ", "Ιουν", "Ιουλ", "Αυγ", "Σεπ", "Οκτ", "Νοε", "Δεκ"],
days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"], days: ["Κυριακή", "Δευτέρα", "Τρίτη", "Τετάρτη", "Πέμπτη", "Παρασκευή", "Σάββατο"],
days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"], days_short: ["Κυρ", "Δευ", "Τρί", "Τετ", "Πέμ", "Παρ", "Σάβ"],

View File

@ -2684,7 +2684,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
view.clicks = release_clicks; view.clicks = release_clicks;
} }
let clicks = if view.clicks > 2 && tool != QuickTool::Select { 1 } else { view.clicks }; let clicks = if view.clicks > 2 && tool != QuickTool::Select { 1 } else { view.clicks };
if clicks >= 2 && over_text { // Only on the frame egui reports the multi-click: `view.clicks` outlives it, and
// acting on every frame re-selected the word as soon as a selection was cleared
// (a highlight made by the double-click left its word selected).
if release_clicks > 0 && clicks >= 2 && over_text {
if view.selection.is_none() { if view.selection.is_none() {
let span = text.nearest(vx, vy).and_then(|a| match clicks { let span = text.nearest(vx, vy).and_then(|a| match clicks {
2 => text.word_at(a), 2 => text.word_at(a),

View File

@ -897,7 +897,9 @@ fn dialogs(app: &mut PdfCraftApp, ctx: &egui::Context) {
return; return;
} }
Dialog::Preferences => { Dialog::Preferences => {
close = crate::js_ui::preferences_body(ui, app, &t); // The settings outgrow a short window: scroll them rather than run off the screen.
let max = (ui.ctx().content_rect().height() - 120.0).max(200.0);
close = egui::ScrollArea::vertical().max_height(max).show(ui, |ui| crate::js_ui::preferences_body(ui, app, &t)).inner;
return; return;
} }
Dialog::RecognizeText => { Dialog::RecognizeText => {

View File

@ -1434,7 +1434,9 @@ mod tests {
assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}"); assert!(errors.is_empty(), "Turkish catalog errors: {errors:?}");
for e in &entries { for e in &entries {
assert!( assert!(
e.source != e.translation || KEEP_AS_IS.contains(&e.source.as_str()) || (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())), e.source != e.translation
|| KEEP_AS_IS.contains(&e.source.as_str())
|| (e.context == "key" && KEY_CAP_NAMES.contains(&e.source.as_str())),
"{:?} is untranslated", "{:?} is untranslated",
e.source e.source
); );

View File

@ -19,6 +19,9 @@ fn shows(h: &Harness<'static, PdfCraftApp>, text: &str) -> bool {
} }
fn retype(h: &mut Harness<'static, PdfCraftApp>, text: &str) { fn retype(h: &mut Harness<'static, PdfCraftApp>, text: &str) {
// The preferences scroll; bring the field into view before clicking it.
h.get_by_role_and_label(Role::TextInput, "Pattern").scroll_to_me();
h.run_steps(4);
h.get_by_role_and_label(Role::TextInput, "Pattern").click(); h.get_by_role_and_label(Role::TextInput, "Pattern").click();
h.run_steps(1); h.run_steps(1);
h.key_press_modifiers(Modifiers::COMMAND, Key::A); h.key_press_modifiers(Modifiers::COMMAND, Key::A);

View File

@ -94,6 +94,9 @@ fn document_scripts_and_preferences() {
assert_eq!(h.state().session.get(id).unwrap().document_scripts()[0].0, "init"); assert_eq!(h.state().session.get(id).unwrap().document_scripts()[0].0, "init");
h.state_mut().dialog = Some(Dialog::Preferences); h.state_mut().dialog = Some(Dialog::Preferences);
h.run_steps(2); h.run_steps(2);
// Below the fold in a 900-point window: the preferences scroll to it.
h.get_by_label("Enable Acrobat JavaScript").scroll_to_me();
h.run_steps(4);
h.get_by_label("Enable Acrobat JavaScript").click(); h.get_by_label("Enable Acrobat JavaScript").click();
h.run_steps(2); h.run_steps(2);
assert!(!h.state().session.javascript()); assert!(!h.state().session.javascript());

View File

@ -800,6 +800,8 @@ fn preferences_selects_default_workspace_for_next_open() {
use pdfcraft_ui_egui::Mode; use pdfcraft_ui_egui::Mode;
let mut h = harness(|app| app.set_option("dialog", "preferences").unwrap()); let mut h = harness(|app| app.set_option("dialog", "preferences").unwrap());
h.get_by_label("Default workspace mode"); h.get_by_label("Default workspace mode");
h.get_by_role_and_label(Role::RadioButton, "Read").scroll_to_me();
h.run_steps(4);
h.get_by_role_and_label(Role::RadioButton, "Read").click(); h.get_by_role_and_label(Role::RadioButton, "Read").click();
h.run_steps(2); h.run_steps(2);
assert_eq!(h.state().default_mode, Mode::Read); assert_eq!(h.state().default_mode, Mode::Read);

View File

@ -89,6 +89,9 @@ fn shared_properties_preserve_mixed_values_and_undo_the_whole_change() {
h.state_mut().bulk_field_props.as_mut().unwrap().required.value = true; h.state_mut().bulk_field_props.as_mut().unwrap().required.value = true;
h.get_by_label("Appearance").click(); h.get_by_label("Appearance").click();
h.run_steps(4); h.run_steps(4);
// Below the fold of the dialog's scroll area with the roomier rows (#487).
h.get_by_label("Line Thickness:").scroll_to_me();
h.run_steps(4);
h.get_by_label("Line Thickness:").click(); h.get_by_label("Line Thickness:").click();
h.run_steps(4); h.run_steps(4);
h.state_mut().bulk_field_props.as_mut().unwrap().width.value = 4.0; h.state_mut().bulk_field_props.as_mut().unwrap().width.value = 4.0;

View File

@ -29,7 +29,9 @@ fn the_identity_name_signs_new_comments_and_is_remembered() {
}); });
h.run_steps(4); h.run_steps(4);
h.get_by_label_contains("Name on new comments"); h.get_by_label_contains("Name on new comments");
// Replace the name in the field. // Replace the name in the field (the preferences scroll to it in a 900-point window).
h.get_by_role_and_label(Role::TextInput, "Name on new comments").scroll_to_me();
h.run_steps(4);
h.get_by_role_and_label(Role::TextInput, "Name on new comments").click(); h.get_by_role_and_label(Role::TextInput, "Name on new comments").click();
h.run_steps(1); h.run_steps(1);
h.key_press_modifiers(Modifiers::COMMAND, Key::A); h.key_press_modifiers(Modifiers::COMMAND, Key::A);

View File

@ -77,8 +77,28 @@ fn quick_clicks_with_the_highlighter_mark_only_the_word() {
let (mut h, at) = harness(|app| assert!(app.execute("comment.highlight"))); let (mut h, at) = harness(|app| assert!(app.execute("comment.highlight")));
for _ in 0..4 { for _ in 0..4 {
click(&mut h, at); click(&mut h, at);
// The double-click's highlight re-renders the page; give its text layer up to 8 frames
// to come back (still well inside egui's 0.3 s multi-click window at 1/60 s a frame), so
// a loaded machine doesn't make the later clicks land on a page with no text yet.
for _ in 0..8 {
if !h.state().render_pending() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(20));
h.run_steps(1);
}
} }
h.run_steps(3); h.run_steps(3);
// Under load the mark can land a few frames later; wait for it (egui's clock is fixed per
// frame, so waiting can't turn the clicks into a different run).
for _ in 0..200 {
let app = h.state();
if app.session.get(app.views[0].id).is_some_and(|d| !d.info.annotations.is_empty()) && !app.render_pending() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(5));
h.run_steps(1);
}
let app = h.state(); let app = h.state();
let doc = app.session.get(app.views[0].id).unwrap(); let doc = app.session.get(app.views[0].id).unwrap();
let marks: Vec<(&str, usize)> = doc.info.annotations.iter().map(|a| (a.subtype.as_str(), a.quads.len())).collect(); let marks: Vec<(&str, usize)> = doc.info.annotations.iter().map(|a| (a.subtype.as_str(), a.quads.len())).collect();

View File

@ -376,6 +376,8 @@ fn preferences_set_the_default_zoom() {
let mut h = harness(&[]); let mut h = harness(&[]);
assert!(h.state_mut().execute("app.preferences")); assert!(h.state_mut().execute("app.preferences"));
h.run_steps(3); h.run_steps(3);
h.get_by_label("Zoom to page level").scroll_to_me();
h.run_steps(4);
h.get_by_label("Zoom to page level").click(); h.get_by_label("Zoom to page level").click();
h.run_steps(2); h.run_steps(2);
assert!(h.state_mut().execute("create.blank")); assert!(h.state_mut().execute("create.blank"));