mirror of
https://github.com/storytold/wordcraft.git
synced 2026-11-09 20:00:11 -06:00
fix(ui): keep Page Setup fields bounded after long paste (#5696)
* M14: keep Page Setup margin and grid editors within their width (#5650) Use the pinned egui DragValue clipping option for four margins and both grid counts. Canonical actual WordApp regression and 12 Page Setup controls pass; strict all-target UI Clippy and scoped rustfmt pass. * M14: keep Page Setup paper-size editors within their width (#5650) Apply the same bounded text editing to paper width and height while preserving their existing limits, units and speed. Actual Page Setup regression and existing controls pass. * M14: regress Page Setup paste, blur and reopen widths (#5650) Exercise all eight numeric fields through the actual WordApp harness. Baseline Bottom paste reproduced width growth from 354 to 2973 pt; the fixed canonical 12-test Page Setup suite passes. Private rendering hooks are excluded. * M14: record bounded Page Setup number editors (#5650) Add the focused UI fix to the progress and revision logs and status line. Readiness estimates remain unchanged. * M14: note the focused Page Setup paste fix in gaps (#5650) Record bounded numeric editing in the gap note, status and revision history; no ranked gap or readiness estimate changes. * M14: document bounded Page Setup numeric input (#5650) Update the UI parity note, status and revision history for long input scrolling inside the existing fields. Readiness estimates remain unchanged. * M14: bound unfocused Page Setup margin values (#5650) Keep the unfocused DragValue inside an allocated, clipped field without changing numeric parsing, units, speed or accepted values. Regress a finite long paste followed by Tab in all four margins. The actual baseline grew from 354 to 458.53125 pt after Tab; the canonical 13 Page Setup tests and scoped formatting checks pass, and the inspected fixed renders stay 354 pt. Hosted validation remains pending in draft PR #5696. * M14: record unfocused Page Setup bounds (#5650) Update the gap and UI parity notes for the accepted-value paste followed by Tab regression. Retain the existing progress history, value ranges and readiness estimates. * M14: log the Page Setup blur regression (#5650) Record the bounded unfocused margin display and accepted-value regression. Preserve every upstream progress entry and the previous validation history. * M14: regress RTL margin re-entry (#5650) Cover re-editing the visible portion of a wide blurred margin in the real Page Setup dialog under an asserted RTL fixture. Raw pointer events restore editing and accept a normal value; the existing thirteen tests remain unchanged. Fourteen Page Setup tests, package formatting, strict all-target UI Clippy and source-integrity checks passed. No production or dependency changes.
This commit is contained in:
@@ -9,6 +9,8 @@
|
||||
> **Recipient CSV update:** 2026-10-27 · Complete quoted header records choose the delimiter (#4249); detection and regressions follow the current mailings module split. Recipient limits and readiness estimates are unchanged.
|
||||
>
|
||||
> **Headered CSV prefix update:** 2026-10-31 · Unquoted whitespace-only prefix lines are skipped before choosing recipient headers (#4249); quoted whitespace headers, headerless records, the shared CSV reader and recipient limits remain unchanged.
|
||||
|
||||
> **Page Setup update:** 2026-11-07 · Page Setup number boxes keep long input inside the field, including accepted margin values after focus leaves (#5650); accepted ranges and readiness estimates are unchanged.
|
||||
> **Printing update:** 2026-10-12 · direct desktop printing through craft-print; details below.
|
||||
> **Legacy form update:** 2026-10-22 · trivial: numeric XML references decode in field names and labels; readiness estimates unchanged.
|
||||
|
||||
@@ -464,6 +466,8 @@ Imported Word 97–2003 `.doc`/`.dot` files now ask for a supported DOCX target
|
||||
| 2026-10-17 | Non-overlapping body floating tables clear header tables. Original native controls distinguish never-overlap from allowed overlap. |
|
||||
| 2026-10-14 | Comment text being typed is no longer lost or misplaced (#1348): an edit or reply the document refuses (for example read-only protection) keeps the typed text in its field and says why on the status line; drafts belong to their document, so after New or Open a comment with the same number shows its own text; Save, Export, Print, Close, New and Open write pending comment text first; if the document refuses it, Save, Export and Print carry on with the stored comments and show why on the status line (a refused draft is dropped when Save is the only change), while Close, New and Open show the Save Changes prompt (Don't Save drops text the document refuses); Escape discards what was typed; in a document protected as read only or for forms the comment fields can't be typed in |
|
||||
|---|---|
|
||||
| 2026-11-07 | Page Setup also bounds the unfocused margin display after a long numeric paste; the accepted value is retained (#5650). |
|
||||
| 2026-11-05 | Page Setup's margin, paper-size and document-grid number boxes scroll long text inside the field instead of enlarging the dialog (#5650). Readiness estimates unchanged. |
|
||||
| 2026-10-29 | Print Markup discovers comment balloon anchors in one forward paragraph-object walk; anchor positions, filtering and the existing cap stay unchanged (#3614) |
|
||||
| 2026-10-26 | Spelling & Grammar opens a right-side Proofing task pane observed in Word for Windows: sentence context, directly clickable suggestion cards and ignore/dictionary actions. Current-story traversal and stale-target checks are retained. Right-click keeps its compact menu. |
|
||||
| 2026-10-22 | Documents that keep content in another format (`w:altChunk`: a web page, RTF, text or another Word document, as generators and mail-merge tools write them) show it where it stands, as Word does, instead of leaving it out; one such file in the docxide-pdf corpus opened as an empty page and now matches Word's 8 pages. `.mht` web archives open, and web pages open the way Word opens them (style sheets, paragraph margins, Times New Roman, single lines, automatic spacing). Insert › Text from File keeps the inserted file's pictures |
|
||||
@@ -812,6 +816,8 @@ MATH glyphs that reference the same construction now share its immutable variant
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-07 | trivial | Page Setup also bounds the unfocused margin display after a long numeric paste; the accepted value is retained (#5650). |
|
||||
| 2026-11-05 | trivial | Page Setup's margin, paper-size and document-grid number boxes scroll long text inside the field instead of enlarging the dialog (#5650). Readiness estimates unchanged. |
|
||||
| 2026-11-06 | trivial | Print helpers remove loaded PDF/settings files before driver work, tolerate prior removal and report cleanup errors; a held-driver regression checks the files are already absent (#5710) |
|
||||
| 2026-11-06 | trivial | Windows print submissions isolated per helper process; cancelled driver work cannot block a retry (#5710) |
|
||||
| 2026-11-06 | trivial | Progress log: print cancellation before sending, including without a document, and the Windows repeated-conversion limitation (#5710) |
|
||||
|
||||
@@ -581,8 +581,20 @@ fn insert_table_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
}
|
||||
|
||||
/// A margin's number box, in the unit the interface measures in.
|
||||
fn margin(value: &mut f32) -> egui::DragValue<'_> {
|
||||
egui::DragValue::new(value).speed(3.6 / crate::options::unit().pt_per_unit()).measure(crate::options::unit()).max_decimals(2)
|
||||
fn margin(value: &mut f32) -> impl egui::Widget + '_ {
|
||||
move |ui: &mut Ui| {
|
||||
// DragValue's unfocused button extends to fit its value; keep it inside the field too.
|
||||
let (_, rect) = ui.allocate_space(egui::vec2(80.0, ui.spacing().interact_size.y));
|
||||
let mut field = ui.new_child(egui::UiBuilder::new().max_rect(rect).layout(crate::rtl::row_layout(ui, egui::Align::Center)));
|
||||
field.shrink_clip_rect(rect);
|
||||
field.add(
|
||||
egui::DragValue::new(value)
|
||||
.speed(3.6 / crate::options::unit().pt_per_unit())
|
||||
.measure(crate::options::unit())
|
||||
.max_decimals(2)
|
||||
.clip_text(true),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn page_setup_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
@@ -640,9 +652,9 @@ fn page_setup_dialog(app: &mut WordApp, ui: &mut Ui, d: &mut Dialog) -> bool {
|
||||
crate::rtl::Table::new("ps_grid").columns(4).show(ui, |t| {
|
||||
t.row(|r| {
|
||||
let chars_label = r.cell(|ui| ui.add_enabled(grid.0 >= 2, egui::Label::new(tl!("Characters per line:"))));
|
||||
r.cell(|ui| ui.add_enabled(grid.0 >= 2, egui::DragValue::new(&mut grid.2).range(1..=1000)).labelled_by(chars_label.id));
|
||||
r.cell(|ui| ui.add_enabled(grid.0 >= 2, egui::DragValue::new(&mut grid.2).range(1..=1000).clip_text(true)).labelled_by(chars_label.id));
|
||||
let lines_label = r.cell(|ui| ui.add_enabled(grid.0 >= 1, egui::Label::new(tl!("Lines per page:"))));
|
||||
r.cell(|ui| ui.add_enabled(grid.0 >= 1, egui::DragValue::new(&mut grid.1).range(1..=1000)).labelled_by(lines_label.id));
|
||||
r.cell(|ui| ui.add_enabled(grid.0 >= 1, egui::DragValue::new(&mut grid.1).range(1..=1000).clip_text(true)).labelled_by(lines_label.id));
|
||||
});
|
||||
});
|
||||
ui.horizontal(|ui| {
|
||||
|
||||
@@ -112,12 +112,13 @@ pub(super) fn page_paper_group(ui: &mut Ui, paper: &mut (f32, f32), landscape: &
|
||||
t.row(|r| {
|
||||
let width_label = r.cell(|ui| ui.label(tl!(l.width)));
|
||||
let w = r.cell(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut paper.0).speed(3.6 / ppu).range(range.clone()).measure(unit).max_decimals(2))
|
||||
ui.add(egui::DragValue::new(&mut paper.0).speed(3.6 / ppu).range(range.clone()).measure(unit).max_decimals(2).clip_text(true))
|
||||
.labelled_by(width_label.id)
|
||||
});
|
||||
let height_label = r.cell(|ui| ui.label(tl!(l.height)));
|
||||
let h = r.cell(|ui| {
|
||||
ui.add(egui::DragValue::new(&mut paper.1).speed(3.6 / ppu).range(range).measure(unit).max_decimals(2)).labelled_by(height_label.id)
|
||||
ui.add(egui::DragValue::new(&mut paper.1).speed(3.6 / ppu).range(range).measure(unit).max_decimals(2).clip_text(true))
|
||||
.labelled_by(height_label.id)
|
||||
});
|
||||
if (w.changed() || h.changed()) && paper.0 != paper.1 {
|
||||
*landscape = paper.0 > paper.1;
|
||||
|
||||
@@ -985,6 +985,145 @@ fn opened_paper(app: &mut WordApp) -> ((f32, f32), Option<&'static str>) {
|
||||
((paper.0 * ppu, paper.1 * ppu), paper_entry(paper))
|
||||
}
|
||||
|
||||
/// #5650: a long paste scrolls inside Page Setup's number boxes instead of enlarging the
|
||||
/// dialog, including after leaving the field and reopening the dialog.
|
||||
#[test]
|
||||
fn page_setup_number_fields_do_not_expand_after_paste() {
|
||||
use egui::accesskit::Role;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
for label in ["Bottom:", "Top:", "Left:", "Right:", "Width:", "Height:", "Characters per line:", "Lines per page:"] {
|
||||
let mut h = app_harness(app_with("Page"));
|
||||
h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
|
||||
if let Some(Dialog::PageSetup { grid, .. }) = h.state_mut().dialog.as_mut() {
|
||||
grid.0 = 2; // Enable both document-grid number boxes.
|
||||
}
|
||||
h.run_steps(3);
|
||||
let width =
|
||||
|h: &egui_kittest::Harness<'_, WordApp>| h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Page Setup")))).unwrap().width();
|
||||
let before = width(&h);
|
||||
h.get_by_role_and_label(Role::SpinButton, label).click();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Paste("a".repeat(400)));
|
||||
h.run_steps(3);
|
||||
assert!(width(&h) <= before + 2.0, "{label}: pasted text grew the dialog from {before} to {}", width(&h));
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
assert!(width(&h) <= before + 2.0, "{label}: leaving the field kept a larger dialog");
|
||||
h.get_by_role_and_label(Role::Button, "Cancel").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().dialog.is_none());
|
||||
h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
|
||||
h.run_steps(3);
|
||||
assert!(width(&h) <= before + 2.0, "{label}: reopening kept a larger dialog");
|
||||
}
|
||||
}
|
||||
|
||||
/// A finite value can outgrow DragValue's unfocused button even when its editor clips.
|
||||
#[test]
|
||||
fn page_setup_large_numeric_margins_do_not_expand_after_leaving_field() {
|
||||
use egui::accesskit::Role;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
for label in ["Bottom:", "Top:", "Left:", "Right:"] {
|
||||
let mut h = app_harness(app_with("Page"));
|
||||
h.state_mut().run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
|
||||
h.run_steps(3);
|
||||
let width =
|
||||
|h: &egui_kittest::Harness<'_, WordApp>| h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Page Setup")))).unwrap().width();
|
||||
let before = width(&h);
|
||||
h.get_by_role_and_label(Role::SpinButton, label).click();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Paste(format!("1{}", "0".repeat(36))));
|
||||
h.run_steps(3);
|
||||
let Some(Dialog::PageSetup { top, bottom, left, right, .. }) = h.state().dialog.as_ref() else { panic!("no page setup dialog") };
|
||||
let value = match label {
|
||||
"Bottom:" => *bottom,
|
||||
"Top:" => *top,
|
||||
"Left:" => *left,
|
||||
_ => *right,
|
||||
};
|
||||
assert!(value.is_finite() && value > 1e35, "{label}: the large finite paste was accepted: {value}");
|
||||
assert!(width(&h) <= before + 2.0, "{label}: editing grew the dialog from {before} to {}", width(&h));
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
let Some(Dialog::PageSetup { top, bottom, left, right, .. }) = h.state().dialog.as_ref() else { panic!("no page setup dialog") };
|
||||
let after = match label {
|
||||
"Bottom:" => *bottom,
|
||||
"Top:" => *top,
|
||||
"Left:" => *left,
|
||||
_ => *right,
|
||||
};
|
||||
assert_eq!(after, value, "{label}: leaving the field keeps the accepted value");
|
||||
assert!(width(&h) <= before + 2.0, "{label}: leaving the large numeric field grew the dialog from {before} to {}", width(&h));
|
||||
h.get_by_role_and_label(Role::Button, "Cancel").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.state().dialog.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
/// A clipped wide number in a mirrored form can still be clicked where it is visible.
|
||||
#[test]
|
||||
fn page_setup_rtl_large_margin_can_be_edited_again_after_blur() {
|
||||
use egui::accesskit::Role;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut app = app_with("Page");
|
||||
app.run("ui.dialog", json!({"name": "pageSetup"})).unwrap();
|
||||
let ar = crate::i18n::fixture_lang("ar", "\tMargins\tMargins\n");
|
||||
let mut h = egui_kittest::Harness::builder().with_size(vec2(1440.0, 900.0)).build_ui_state(
|
||||
move |ui, app: &mut WordApp| {
|
||||
let ctx = ui.ctx().clone();
|
||||
app.logic(&ctx);
|
||||
// Draw the actual dialog in a fixture language even without embedded catalogs.
|
||||
crate::i18n::set_current(ar);
|
||||
crate::rtl::sync(&ctx);
|
||||
assert!(crate::rtl::on() && ctx.global_style().right_to_left);
|
||||
show(app, &ctx);
|
||||
crate::i18n::set_current(crate::i18n::Lang::EN);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(4);
|
||||
let initial = h.get_by_role_and_label(Role::SpinButton, "Bottom:").rect();
|
||||
assert!(h.get_by_role_and_label(Role::SpinButton, "Top:").rect().center().x > initial.center().x, "the form is mirrored");
|
||||
let window = |h: &egui_kittest::Harness<'_, WordApp>| h.ctx.memory(|m| m.area_rect(egui::Id::unique(("dialog", "Page Setup")))).unwrap();
|
||||
let before = window(&h).width();
|
||||
h.get_by_role_and_label(Role::SpinButton, "Bottom:").click();
|
||||
h.run_steps(2);
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Paste(format!("1{}", "0".repeat(36))));
|
||||
h.run_steps(3);
|
||||
let value = match h.state().dialog.as_ref() {
|
||||
Some(Dialog::PageSetup { bottom, .. }) => *bottom,
|
||||
_ => panic!("no page setup dialog"),
|
||||
};
|
||||
assert!(value.is_finite() && value > 1e35);
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
assert!(matches!(h.state().dialog.as_ref(), Some(Dialog::PageSetup { bottom, .. }) if *bottom == value));
|
||||
let field = h.get_by_role_and_label(Role::SpinButton, "Bottom:");
|
||||
assert!(!field.is_focused());
|
||||
let wide = field.rect();
|
||||
let visible = wide.intersect(initial).intersect(window(&h));
|
||||
assert!(wide.width() > 80.0 && visible.is_positive() && !visible.contains(wide.center()));
|
||||
assert!(window(&h).width() <= before + 2.0);
|
||||
// The AccessKit bounds include clipped text: click the visible part, not their centre.
|
||||
let pos = visible.center();
|
||||
h.event(egui::Event::PointerMoved(pos));
|
||||
for pressed in [true, false] {
|
||||
h.event(egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
|
||||
}
|
||||
h.run_steps(3);
|
||||
assert!(h.get_by_role_and_label(Role::SpinButton, "Bottom:").is_focused(), "the visible field enters editing");
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::A);
|
||||
h.event(egui::Event::Paste("2.5".into()));
|
||||
h.run_steps(3);
|
||||
h.key_press(egui::Key::Tab);
|
||||
h.run_steps(3);
|
||||
assert!(matches!(h.state().dialog.as_ref(), Some(Dialog::PageSetup { bottom, .. }) if *bottom == 2.5), "the field accepts a normal value again");
|
||||
assert!(window(&h).width() <= before + 2.0);
|
||||
}
|
||||
|
||||
/// #1433: Page Setup's Paper size shows the named size the section has, in either
|
||||
/// orientation, and Custom size (None) for anything else.
|
||||
#[test]
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user