M6: a list box's top row is chosen after the edit's new font size

set_props chose /TI inside the field-dictionary update, from the field
read before the edit, so a font size or look changed in the same call
was not counted: at a larger size fewer rows fit and the selection
could end up below the visible rows. The field and widget top rows are
now chosen after every layout change, from the field read again,
before the redraw.

Test: a_list_boxs_top_row_follows_a_font_size_changed_with_its_items
(fails with the old ordering: rows 1..2 shown, selection on row 3).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
KenM76
2026-10-27 12:32:57 -04:00
co-authored by Claude Opus 5.5
parent 7a6177759c
commit 58c50f60d6
3 changed files with 36 additions and 13 deletions
+13 -12
View File
@@ -235,15 +235,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
if let Some(exports) = &new_exports
&& matches!(f.kind, FieldKind::Combo | FieldKind::List)
{
let picked = repoint_selection(d, &f.value, exports);
// A list box scrolls, as for a new value, so its first row is still an option and
// the selection is in view.
if let Some((old, new)) = crate::values::list_scroll(&f, &picked, exports.len())
&& old != new
{
d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0)));
}
list_picked = Some((picked, exports.len()));
list_picked = Some((repoint_selection(d, &f.value, exports), exports.len()));
}
if let Some(q) = props.quadding {
d.set(b"Q".to_vec(), Object::Int(q));
@@ -261,9 +253,6 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
d.set(b"DA".to_vec(), PdfString::literal(new_da.clone().into_bytes()));
}
})?;
if let Some((picked, count)) = list_picked {
crate::values::scroll_widgets(doc, &f, &picked, count)?;
}
if props.appearance.is_some() || (props.font_size.is_some() && props.look.is_none()) {
let patch = &props.appearance.unwrap_or_default();
for w in &f.widgets {
@@ -421,6 +410,18 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
}
// A label-only edit must not replace another widget's custom appearances.
let only = props.caption.as_ref().filter(|_| *props == (FieldProps { caption: props.caption.clone(), ..Default::default() })).map(|(wi, _)| *wi);
// A list box scrolls, as for a new value, so its first row is still an option and the
// selection is in view. Its rows are measured with the edited font size and rectangle.
if let Some((picked, count)) = list_picked
&& let Some(g) = fields(doc).into_iter().find(|g| g.obj == f.obj)
{
if let Some((old, new)) = crate::values::list_scroll(&g, &picked, count)
&& old != new
{
doc.update_dict(f.obj, |d| d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0))))?;
}
crate::values::scroll_widgets(doc, &g, &picked, count)?;
}
redraw_widgets(doc, &new_name, only)?;
if props.calculate.is_some() {
crate::recalculate(doc)?;
+22
View File
@@ -4663,6 +4663,28 @@ fn each_widget_with_its_own_top_row_scrolls_by_itself() {
assert_eq!(top_row(&reopen(&doc), other), Some(0), "value: the other widget scrolls to A");
}
/// A list box's top row is chosen with the font size set in the same edit: at the larger size
/// fewer rows fit, so it scrolls further to keep the selection in view.
#[test]
fn a_list_boxs_top_row_follows_a_font_size_changed_with_its_items() {
let mut doc = document(&[
"<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(),
"<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(),
"<< /Type /Page /Parent 2 0 R /Annots [6 0 R] >>".into(),
"<< /Fields [6 0 R] /DA (/Helv 10 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".into(),
"<< /FT /Ch /T (pick) /Opt [(A) (B) (C) (D) (E)] /V (C) /I [2] /Type /Annot /Subtype /Widget /Rect [50 700 250 740] /P 3 0 R >>".into(),
]);
let items = ["Z", "A", "B", "C", "D"].map(String::from).to_vec();
set_props(&mut doc, "pick", &FieldProps { options: Some(items), font_size: Some(20.0), ..FieldProps::default() }).unwrap();
let doc = reopen(&doc);
let g = field(&fields(&doc), "pick").clone();
let w = &g.widgets[0];
let rows = w.list.unwrap();
let shown = rows.top..rows.top + rows.visible(w.rect[3] - w.rect[1]);
assert!(shown.contains(&3), "C is row 3; rows {shown:?} show");
}
#[test]
fn sorting_a_combo_boxs_items_repoints_its_selected_index() {
let mut doc = one_page();
+1 -1
View File
@@ -4991,7 +4991,7 @@ milestone = "M6"
status = "shipped"
commands = ["form.field.properties"]
tools = ["form_set_props"]
evidence = ["crates/forms/src/tests.rs::button_caption_preserves_other_widgets_and_shared_dictionaries", "crates/forms/src/tests.rs::a_button_caption_is_drawn_in_its_fields_font_size_and_color", "crates/ui-egui/tests/it/prepare.rs::button_label_is_editable_without_renaming_the_field", "crates/automation/tests/it/button_caption.rs::button_caption_can_be_changed_after_creation", "crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/it/automation.rs::preparing_a_form_through_tools", "crates/forms/src/tests.rs::options_tab_flags_alignment_and_defaults", "crates/ui-egui/tests/it/prepare.rs::options_tab_sets_flags_alignment_and_defaults", "crates/automation/tests/it/form_bulk.rs::bulk_options_apply_per_field_and_refuse_an_invalid_comb_atomically", "crates/forms/src/tests.rs::changing_a_lists_options_repoints_its_selected_indices", "crates/forms/src/tests.rs::shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row", "crates/forms/src/tests.rs::each_widget_with_its_own_top_row_scrolls_by_itself", "crates/forms/src/tests.rs::sorting_a_combo_boxs_items_repoints_its_selected_index", "crates/automation/tests/it/automation.rs::changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_items"]
evidence = ["crates/forms/src/tests.rs::button_caption_preserves_other_widgets_and_shared_dictionaries", "crates/forms/src/tests.rs::a_button_caption_is_drawn_in_its_fields_font_size_and_color", "crates/ui-egui/tests/it/prepare.rs::button_label_is_editable_without_renaming_the_field", "crates/automation/tests/it/button_caption.rs::button_caption_can_be_changed_after_creation", "crates/forms/src/tests.rs::properties_rename_and_delete", "crates/automation/tests/it/automation.rs::preparing_a_form_through_tools", "crates/forms/src/tests.rs::options_tab_flags_alignment_and_defaults", "crates/ui-egui/tests/it/prepare.rs::options_tab_sets_flags_alignment_and_defaults", "crates/automation/tests/it/form_bulk.rs::bulk_options_apply_per_field_and_refuse_an_invalid_comb_atomically", "crates/forms/src/tests.rs::changing_a_lists_options_repoints_its_selected_indices", "crates/forms/src/tests.rs::shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row", "crates/forms/src/tests.rs::each_widget_with_its_own_top_row_scrolls_by_itself", "crates/forms/src/tests.rs::a_list_boxs_top_row_follows_a_font_size_changed_with_its_items", "crates/forms/src/tests.rs::sorting_a_combo_boxs_items_repoints_its_selected_index", "crates/automation/tests/it/automation.rs::changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_items"]
notes = "push buttons: per-widget normal label independent of the field name, including clearing and save/reopen, drawn in the font, size and color of the button's /DA (the form's /DR font, else Helvetica; an automatic size fits the button) and centered by Helvetica's widths; icon-only layout retained, rollover/down labels and icon layout editing remain unavailable; text: alignment, default, multi-line, scroll, rich text flag, limit, password, file selection, spell check, comb; choices: items (the selected indices /I follow their items when the list is edited or sorted), default, sort, custom text, multiple selection, commit immediately; check boxes and radios: checked by default, radios in unison. Shared GUI offers applicable text/choice flags, alignment, text limits and check styles; choices/defaults remain individual GUI edits"
[[feature]]