Integrate KenM76 PR #4987

# Conflicts:
#	crates/forms/src/author/edit.rs
#	crates/forms/src/tests.rs
This commit is contained in:
FredApps
2026-10-29 02:38:03 +01:00
6 changed files with 302 additions and 39 deletions
+26
View File
@@ -9406,3 +9406,29 @@ fn deleting_one_radio_button_through_tools_keeps_the_group() {
ok(&mut a, "edit_undo", json!({ "doc": doc }));
assert_eq!(size(&mut a)["options"], json!(["S", "M", "L"]), "one undo step brings it back");
}
/// `/I` of the top-level field `name` in the saved file `path`.
fn saved_selection_indices(path: &Path, name: &str) -> Option<Vec<i64>> {
let d = pdfcraft_cos::Document::open(std::sync::Arc::new(std::fs::read(path).unwrap())).unwrap();
let root = d.get(d.root().unwrap()).as_dict().cloned().unwrap();
let form = d.resolve(root.get(b"AcroForm").unwrap()).as_dict().cloned().unwrap();
let all = d.resolve(form.get(b"Fields").unwrap()).as_array().cloned().unwrap();
let f = all
.iter()
.filter_map(|o| d.resolve(o).as_dict().cloned())
.find(|f| f.get(b"T").and_then(|t| d.resolve(t).as_string().map(|s| s.to_text())).as_deref() == Some(name))
.unwrap();
f.get(b"I").map(|i| d.resolve(i).as_array().unwrap().iter().map(|o| o.as_int().unwrap()).collect())
}
#[test]
fn changing_a_lists_options_through_tools_keeps_its_saved_selection_on_the_same_items() {
let dir = workdir("choice-index");
let mut a = auto(&dir);
let doc = ok(&mut a, "doc_open", json!({ "path": "a.pdf" }))["doc"].as_u64().unwrap();
let args = json!({ "doc": doc, "page": 1, "type": "list", "rect": [20, 60, 180, 140], "options": ["A", "B", "C"], "multi_select": true, "name": "letters" });
ok(&mut a, "form_add_field", args);
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "letters": ["B", "C"] } }));
ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "letters", "options": ["B", "C", "D"] }));
ok(&mut a, "doc_save", json!({ "doc": doc, "path": "letters.pdf" }));
assert_eq!(saved_selection_indices(&dir.join("letters.pdf"), "letters"), Some(vec![0, 1]), "B and C are now items 0 and 1");
}
+93 -16
View File
@@ -15,6 +15,68 @@ fn swapped_around_center(r: [f64; 4]) -> Option<[f64; 4]> {
(out.iter().all(|v| v.is_finite()) && out[2] - out[0] >= MIN_FIELD_SIDE && out[3] - out[1] >= MIN_FIELD_SIDE).then_some(out)
}
/// Re-point a choice field's `/I`, positions in `/Opt`, at the new list `exports` after `/Opt` was
/// replaced (ISO 32000-2 §12.7.5.4): the positions of the selected `values` still offered. `kept`
/// are the old `/I` positions carried into the new list, which tell apart options sharing an
/// export value; a selected value none of them covers takes its first position, by `set_value`'s
/// rule. Returns them.
fn repoint_selection(d: &mut Dict, values: &[String], exports: &[String], kept: &[usize]) -> Vec<usize> {
let mut indices: Vec<usize> = kept.iter().copied().filter(|&i| exports.get(i).is_some_and(|e| values.contains(e))).collect();
for v in values {
if !indices.iter().any(|&i| exports.get(i) == Some(v)) {
indices.extend(exports.iter().position(|e| e == v));
}
}
indices.sort_unstable();
indices.dedup();
if indices.is_empty() {
d.remove(b"I");
} else {
d.set(b"I".to_vec(), Object::Array(indices.iter().map(|&i| Object::Int(i64::try_from(i).unwrap_or(i64::MAX))).collect()));
}
indices
}
/// The positions in `/Opt` that a choice field's `/I` holds, those below `count`.
fn selected_indices(doc: &Document, field: ObjRef, count: usize) -> Vec<usize> {
let Some(i) = doc.get(field).as_dict().and_then(|d| d.get(b"I").cloned()) else {
return Vec::new();
};
let positions = doc.resolve(&i);
positions.as_array().map(|a| a.iter().filter_map(|o| usize::try_from(o.as_int()?).ok()).filter(|&n| n < count).collect()).unwrap_or_default()
}
/// The items (export value, display text) "Sort items" orders: the new `props.options`, or else
/// the field's own, whose selection, `/I` included, moves with them.
fn items_to_sort(doc: &Document, f: &Field, props: &FieldProps) -> (Vec<(String, String)>, Vec<usize>) {
match &props.options {
Some(o) => (o.iter().map(|x| (x.clone(), x.clone())).collect(), Vec::new()),
None => (f.options.clone(), selected_indices(doc, f.obj, f.options.len())),
}
}
/// Sort a choice field's items (export value, display text) by display text, ignoring case, and
/// write them as `/Opt`; export values stay with their items. Returns the export values in their
/// new order and the `old` positions carried through the sort.
fn write_sorted_opt(d: &mut Dict, items: &[(String, String)], old: &[usize]) -> (Vec<String>, Vec<usize>) {
let mut order: Vec<usize> = (0..items.len()).collect();
order.sort_by_key(|&i| items.get(i).map(|(_, display)| display.to_lowercase()));
let sorted: Vec<&(String, String)> = order.iter().filter_map(|&i| items.get(i)).collect();
let opt = sorted
.iter()
.map(|(e, disp)| {
if e == disp {
Object::String(PdfString::text(disp))
} else {
Object::Array(vec![Object::String(PdfString::text(e)), Object::String(PdfString::text(disp))])
}
})
.collect();
d.set(b"Opt".to_vec(), Object::Array(opt));
let carried = old.iter().filter_map(|o| order.iter().position(|i| i == o)).collect();
(sorted.into_iter().map(|(e, _)| e.clone()).collect(), carried)
}
/// Change a field's properties (General and Options tabs) and redraw it.
pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<String, FormError> {
if props.font_size.is_some_and(|size| !size.is_finite() || size < 0.0) {
@@ -124,6 +186,8 @@ 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_picked = None;
let (sort_items, old_selection) = items_to_sort(doc, &f, props);
doc.update_dict(f.obj, |d| {
d.set(b"Ff".to_vec(), Object::Int(ff as i64));
if let Some(t) = &props.tooltip {
@@ -141,27 +205,23 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
}
}
}
// The export values in `/Opt` order once it is rewritten, for re-pointing `/I`.
let mut new_exports: Option<Vec<String>> = None;
let mut kept = Vec::new();
if let Some(opts) = &props.options {
d.set(b"Opt".to_vec(), Object::Array(opts.iter().map(|o| Object::String(PdfString::text(o))).collect()));
new_exports = Some(opts.clone());
}
// "Sort items": the list itself is kept in order (export values stay with their items).
if ff & flags::SORT != 0 && matches!(f.kind, FieldKind::Combo | FieldKind::List) {
let mut items: Vec<(String, String)> = match &props.options {
Some(o) => o.iter().map(|x| (x.clone(), x.clone())).collect(),
None => f.options.clone(),
};
items.sort_by_key(|(_, display)| display.to_lowercase());
let opt = items
.iter()
.map(|(e, disp)| {
if e == disp {
Object::String(PdfString::text(disp))
} else {
Object::Array(vec![Object::String(PdfString::text(e)), Object::String(PdfString::text(disp))])
}
})
.collect();
d.set(b"Opt".to_vec(), Object::Array(opt));
let (exports, carried) = write_sorted_opt(d, &sort_items, &old_selection);
new_exports = Some(exports);
kept = carried;
}
if let Some(exports) = &new_exports
&& matches!(f.kind, FieldKind::Combo | FieldKind::List)
{
list_picked = Some((repoint_selection(d, &f.value, exports, &kept), exports.len()));
}
if let Some(q) = props.quadding {
d.set(b"Q".to_vec(), Object::Int(q));
@@ -249,6 +309,7 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
})?;
}
let only = apply_late_widget_props(doc, &f, props, &new_da, rotated_rect)?;
scroll_list(doc, f.obj, list_picked)?;
redraw_or_join(doc, &f, name, &new_name, join.as_ref(), only)?;
if props.calculate.is_some() {
crate::recalculate(doc)?;
@@ -396,6 +457,22 @@ fn redraw_or_join(doc: &mut Document, f: &Field, name: &str, new_name: &str, joi
}
}
/// 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.
fn scroll_list(doc: &mut Document, field: ObjRef, list_picked: Option<(Vec<usize>, usize)>) -> Result<(), FormError> {
if let Some((picked, count)) = list_picked
&& let Some(g) = fields(doc).into_iter().find(|g| g.obj == field)
{
if let Some((old, new)) = crate::values::list_scroll(&g, &picked, count)
&& old != new
{
doc.update_dict(field, |d| d.set(b"TI".to_vec(), Object::Int(i64::try_from(new).unwrap_or(0))))?;
}
crate::values::scroll_widgets(doc, &g, &picked, count)?;
}
Ok(())
}
fn apply_late_widget_props(
doc: &mut Document,
f: &Field,
+142
View File
@@ -5125,4 +5125,146 @@ fn joining_by_name_is_only_for_radio_buttons_and_unlocked_groups() {
set_props(&mut doc, "Group1", &FieldProps { locked: Some(true), ..FieldProps::default() }).unwrap();
assert!(rename_to(&mut doc, "Group2", "Group1").is_err(), "a locked group");
assert_eq!(fields(&doc).len(), 3);
/// `/I` of field `name`: the selected positions in `/Opt`, or `None` when absent.
fn selection_indices(doc: &Document, name: &str) -> Option<Vec<i64>> {
let f = field(&fields(doc), name).clone();
let d = doc.get(f.obj).as_dict().cloned().unwrap();
d.get(b"I").map(|i| doc.resolve(i).as_array().unwrap().iter().map(|o| o.as_int().unwrap()).collect())
}
#[test]
fn changing_a_lists_options_repoints_its_selected_indices() {
let mut doc = one_page();
let list =
add_field(&mut doc, 0, [50.0, 600.0, 250.0, 680.0], &NewField::List { options: vec!["A".into(), "B".into(), "C".into()], multi: true }, None)
.unwrap();
set_value(&mut doc, &list, &FieldValue::Choice(vec!["C".into(), "B".into()])).unwrap();
assert_eq!(selection_indices(&doc, &list), Some(vec![1, 2]));
let f = field(&fields(&doc), &list).clone();
doc.update_dict(f.obj, |d| d.set(b"TI".to_vec(), Object::Int(2))).unwrap();
// Removing A moves B and C up one place.
let opts = |o: &[&str]| FieldProps { options: Some(o.iter().map(|s| s.to_string()).collect()), ..FieldProps::default() };
set_props(&mut doc, &list, &opts(&["B", "C", "D"])).unwrap();
assert_eq!(selection_indices(&doc, &list), Some(vec![0, 1]));
// Removing B leaves only C selected; the top row would point past the end.
set_props(&mut doc, &list, &opts(&["C"])).unwrap();
assert_eq!(selection_indices(&doc, &list), Some(vec![0]));
assert_eq!(top_row(&doc, f.obj), Some(0), "the list scrolls back to its only row");
// No selected value is offered any more.
set_props(&mut doc, &list, &opts(&["X", "Y"])).unwrap();
assert_eq!(selection_indices(&doc, &list), None);
let doc = reopen(&doc);
assert_eq!(selection_indices(&doc, &list), None);
}
fn top_row(doc: &Document, obj: ObjRef) -> Option<i64> {
doc.get(obj).as_dict().and_then(|d| d.get(b"TI")).and_then(|o| o.as_int())
}
#[test]
fn shrinking_a_lists_options_scrolls_a_widget_with_its_own_top_row() {
let objs: 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] >>".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) (F)] /V (F) /I [5] /Kids [7 0 R] >>".into(),
"<< /Type /Annot /Subtype /Widget /Parent 6 0 R /TI 5 /Rect [50 700 250 720] /P 3 0 R >>".into(),
];
let mut doc = document(&objs);
let widget = ObjRef::new(7, 0);
set_props(&mut doc, "pick", &FieldProps { options: Some(vec!["F".into(), "A".into()]), ..FieldProps::default() }).unwrap();
assert_eq!(selection_indices(&doc, "pick"), Some(vec![0]));
// The widget's own /TI is read before the field's; row 5 no longer exists.
assert_eq!(top_row(&doc, widget), Some(0), "the widget scrolls to F, now row 0");
let doc = reopen(&doc);
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");
}
/// 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();
let combo = add_field(
&mut doc,
0,
[50.0, 600.0, 250.0, 620.0],
&NewField::Combo { options: vec!["Pear".into(), "apple".into(), "Fig".into()], editable: false },
None,
)
.unwrap();
set_value(&mut doc, &combo, &FieldValue::Choice(vec!["Pear".into()])).unwrap();
assert_eq!(selection_indices(&doc, &combo), Some(vec![0]));
set_props(&mut doc, &combo, &FieldProps { flags: vec![(flags::SORT, true)], ..FieldProps::default() }).unwrap();
// apple, Fig, Pear: the selection follows Pear to the end.
assert_eq!(selection_indices(&doc, &combo), Some(vec![2]));
assert_eq!(field(&fields(&doc), &combo).value, vec!["Pear".to_string()]);
}
#[test]
fn sorting_keeps_which_of_two_items_with_one_export_value_is_selected() {
let mut doc = one_page();
let list = add_field(&mut doc, 0, [50.0, 600.0, 250.0, 680.0], &NewField::List { options: vec!["a".into()], multi: false }, None).unwrap();
let f = field(&fields(&doc), &list).clone();
let item = |e: &str, shown: &str| Object::Array(vec![Object::String(PdfString::text(e)), Object::String(PdfString::text(shown))]);
// Zeta and Alpha both export "x"; `/I` says Zeta is the one selected.
doc.update_dict(f.obj, |d| {
d.set(b"Opt".to_vec(), Object::Array(vec![item("x", "Zeta"), item("x", "Alpha"), item("y", "Mid")]));
d.set(b"V".to_vec(), PdfString::text("x"));
d.set(b"I".to_vec(), Object::Array(vec![Object::Int(0)]));
})
.unwrap();
set_props(&mut doc, &list, &FieldProps { flags: vec![(flags::SORT, true)], ..FieldProps::default() }).unwrap();
// Alpha, Mid, Zeta: the selection follows Zeta, not the first item exporting "x".
assert_eq!(selection_indices(&doc, &list), Some(vec![2]));
}
+34 -20
View File
@@ -1,8 +1,8 @@
use super::appearances::{declares_pdfa, need_appearances};
use super::read::{acroform, field_flags, fields, page_refs};
use super::{
Dict, Document, Field, FieldEvent, FieldKind, FieldValue, FormError, NoScripts, ObjRef, Object, PdfString, Scripts, af, appearance, author,
calc_order, flags, invalid, name_obj, scripting,
Dict, Document, Field, FieldEvent, FieldKind, FieldValue, FormError, NoScripts, ObjRef, Object, PdfString, Scripts, Widget, af, appearance,
author, calc_order, flags, invalid, name_obj, scripting,
};
// ── writing ─────────────────────────────────────────────────────────────────────────────────
@@ -291,14 +291,8 @@ pub(crate) fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scr
indices.sort_unstable_by_key(Object::as_int);
// A list box scrolls (`/TI`) so the picked options are among the rows it draws, as
// in other viewers: a pick below the last visible row would otherwise not show (#1524).
let top = match (f.kind, f.widgets.first()) {
(FieldKind::List, Some(w)) => 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] };
let picked: Vec<usize> = indices.iter().filter_map(|i| i.as_int()).filter_map(|i| usize::try_from(i).ok()).collect();
(rows.top, appearance::ListRows::top_showing(rows.top, &picked, rows.visible(height), f.options.len()))
}),
_ => None,
};
let picked: Vec<usize> = indices.iter().filter_map(|i| i.as_int()).filter_map(|i| usize::try_from(i).ok()).collect();
let top = list_scroll(f, &picked, f.options.len());
doc.update_dict(f.obj, |d| {
match exports.as_slice() {
[] => {
@@ -318,16 +312,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)));
}
})?;
// A widget with its own `/TI` (read before the field's) scrolls too.
if let Some((old, new)) = top
&& old != new
{
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(new).unwrap_or(0))))?;
}
}
}
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)),
@@ -336,6 +321,35 @@ pub(crate) fn write_value(doc: &mut Document, f: &Field, value: &FieldValue, scr
}
}
/// A list box's first visible row (`/TI`) before and after scrolling so the `picked` positions of
/// 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)> {
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))
})
}
/// 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"))
&& 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(())
}
/// Check boxes and radio buttons: `/V` on the field, `/AS` on each widget.
fn set_states(doc: &mut Document, f: &Field, on: Option<&str>) -> Result<(), FormError> {
let v = on.unwrap_or("Off");
+2 -2
View File
@@ -5003,8 +5003,8 @@ 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"]
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, 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"
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]]
id = "form.props-actions"
+5 -1
View File
File diff suppressed because one or more lines are too long