mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 18:31:10 -06:00
M6: each list box widget with its own top row scrolls by itself
A widget's own /TI is read before the field's, so each one scrolls by its own top row and size (ListRows::top_showing). Before, the widgets were only moved, all to the first widget's new row, when the first widget itself scrolled: a second widget scrolled past the remaining options, or away from a new value the first already showed, kept its stale row. Test each_widget_with_its_own_top_row_scrolls_by_itself covers both set_props (options shrink) and set_value; restoring the first-widget gate in either caller makes it fail. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SqmHYx8FiWM5PcsoiErgp9
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3ed4f2379e
commit
7a6177759c
@@ -188,7 +188,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
||||
if props.check_style.is_some() && !matches!(f.kind, FieldKind::CheckBox | FieldKind::Radio) {
|
||||
return invalid("only check boxes and radio buttons have a check style");
|
||||
}
|
||||
let mut list_top = None;
|
||||
let mut list_picked = None;
|
||||
doc.update_dict(f.obj, |d| {
|
||||
d.set(b"Ff".to_vec(), Object::Int(ff as i64));
|
||||
if let Some(t) = &props.tooltip {
|
||||
@@ -242,8 +242,8 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
|
||||
&& old != new
|
||||
{
|
||||
d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0)));
|
||||
list_top = Some(new);
|
||||
}
|
||||
list_picked = Some((picked, exports.len()));
|
||||
}
|
||||
if let Some(q) = props.quadding {
|
||||
d.set(b"Q".to_vec(), Object::Int(q));
|
||||
@@ -261,8 +261,8 @@ 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(top) = list_top {
|
||||
crate::values::scroll_widgets(doc, &f, top)?;
|
||||
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();
|
||||
|
||||
@@ -4635,6 +4635,34 @@ fn shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row() {
|
||||
assert_eq!(top_row(&doc, widget), Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_widget_with_its_own_top_row_scrolls_by_itself() {
|
||||
// Widget 7 shows from the field's top row; widget 8 keeps its own /TI 4.
|
||||
let objs = |v: &str| -> Vec<String> {
|
||||
vec![
|
||||
"<< /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 [7 0 R 8 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(),
|
||||
format!("<< /FT /Ch /T (pick) /Opt [(A) (B) (C) (D) (E) (F)] {v} /Kids [7 0 R 8 0 R] >>"),
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /Rect [50 700 250 740] /P 3 0 R >>".into(),
|
||||
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /TI 4 /Rect [50 600 250 620] /P 3 0 R >>".into(),
|
||||
]
|
||||
};
|
||||
let (first, other) = (ObjRef::new(7, 0), ObjRef::new(8, 0));
|
||||
// The options shrink: the first widget already shows A, the other's row 4 is gone.
|
||||
let mut doc = document(&objs("/V (A) /I [0]"));
|
||||
set_props(&mut doc, "pick", &FieldProps { options: Some(vec!["A".into(), "B".into()]), ..FieldProps::default() }).unwrap();
|
||||
assert_eq!(top_row(&doc, first), None);
|
||||
assert_eq!(top_row(&reopen(&doc), other), Some(0), "options: the other widget scrolls to A");
|
||||
// A new value the first widget already shows, below the other's view.
|
||||
let mut doc = document(&objs("/V (B) /I [1]"));
|
||||
set_value(&mut doc, "pick", &FieldValue::Choice(vec!["A".into()])).unwrap();
|
||||
assert_eq!(top_row(&doc, ObjRef::new(6, 0)), None, "the field's top row did not move");
|
||||
assert_eq!(top_row(&reopen(&doc), other), Some(0), "value: the other widget scrolls to A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sorting_a_combo_boxs_items_repoints_its_selected_index() {
|
||||
let mut doc = one_page();
|
||||
|
||||
+14
-10
@@ -309,11 +309,7 @@ pub(crate) fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scr
|
||||
d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0)));
|
||||
}
|
||||
})?;
|
||||
if let Some((old, new)) = top
|
||||
&& old != new
|
||||
{
|
||||
scroll_widgets(doc, f, new)?;
|
||||
}
|
||||
scroll_widgets(doc, f, &picked, f.options.len())?;
|
||||
redraw(doc, f, &exports, scripts)
|
||||
}
|
||||
(FieldKind::PushButton, _) => invalid(format!("{:?} is a button; it has no value", f.name)),
|
||||
@@ -326,18 +322,26 @@ pub(crate) fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scr
|
||||
/// its `count` options are among the rows it draws, as in other viewers: a pick below the last
|
||||
/// visible row would otherwise not show (#1524). `None` for any other field.
|
||||
pub(crate) fn list_scroll(f: &Field, picked: &[usize], count: usize) -> Option<(usize, usize)> {
|
||||
let w = f.widgets.first().filter(|_| f.kind == FieldKind::List)?;
|
||||
f.widgets.first().filter(|_| f.kind == FieldKind::List).and_then(|w| widget_scroll(w, picked, count))
|
||||
}
|
||||
|
||||
/// A list box widget's first row before and after scrolling so `picked` shows.
|
||||
fn widget_scroll(w: &Widget, picked: &[usize], count: usize) -> Option<(usize, usize)> {
|
||||
w.list.map(|rows| {
|
||||
let height = if matches!(w.rotation, 90 | 270) { w.rect[2] - w.rect[0] } else { w.rect[3] - w.rect[1] };
|
||||
(rows.top, appearance::ListRows::top_showing(rows.top, picked, rows.visible(height), count))
|
||||
})
|
||||
}
|
||||
|
||||
/// Give every widget of `f` with its own `/TI` (read before the field's) the first row `top`.
|
||||
pub(crate) fn scroll_widgets(doc: &mut Document, f: &Field, top: usize) -> Result<(), FormError> {
|
||||
/// Scroll every widget of `f` with its own `/TI` (read before the field's) by its own rows and
|
||||
/// size, so `picked` shows in each.
|
||||
pub(crate) fn scroll_widgets(doc: &mut Document, f: &Field, picked: &[usize], count: usize) -> Result<(), FormError> {
|
||||
for w in f.widgets.iter().filter(|w| w.obj != f.obj) {
|
||||
if doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"TI")) {
|
||||
doc.update_dict(w.obj, |d| d.set(b"TI".to_vec(), Object::Int(i64::try_from(top).unwrap_or(0))))?;
|
||||
if doc.get(w.obj).as_dict().is_some_and(|d| d.contains(b"TI"))
|
||||
&& let Some((old, new)) = widget_scroll(w, picked, count)
|
||||
&& old != new
|
||||
{
|
||||
doc.update_dict(w.obj, |d| d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0))))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -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::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::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]]
|
||||
|
||||
Reference in New Issue
Block a user