Forms: check box and radio button styles (#94)

Field Properties > Options > Check Box Style (Button Style for radio
buttons): Check, Circle, Cross, Diamond, Square or Star, as Acrobat
offers. The choice is stored in each widget's /MK /CA with Acrobat's
ZapfDingbats codes (4 l 8 u n H), so other viewers draw the same mark,
and PrintCraft draws it as paths, needing no symbol font. Styles set
by other apps are read back too; the defaults stay a check for check
boxes and a circle for radio buttons.

- forms: CheckStyle, check_style(), FieldProps::check_style; set_props
  writes /MK /CA (resolving an indirect /MK so its colours survive) and
  redraws; text and choice fields refuse a style.
- engine: Document::field_check_style.
- automation: form_set_props takes check_style, form_fields reports it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
bflatastic 2026-10-07 09:42:39 +07:00
parent 55c581760a
commit cdb3f3d106
11 changed files with 305 additions and 49 deletions

View File

@ -201,6 +201,9 @@ impl Automation {
if let Some(t) = &f.tooltip {
o.insert("tooltip".into(), json!(t));
}
if let Some(s) = doc.field_check_style(&f.name) {
o.insert("check_style".into(), json!(s.label().to_lowercase()));
}
if f.actions.format != Format::None {
o.insert("format".into(), format_json(&f.actions.format));
o.insert("display".into(), json!(f.value.first().map(|v| printcraft_engine::form_scripts::format_value(&f.actions.format, v))));
@ -391,6 +394,15 @@ impl Automation {
}
},
actions: None,
check_style: match a.opt_str("check_style")? {
None => None,
Some(s) => Some(
printcraft_engine::CheckStyle::ALL
.into_iter()
.find(|c| c.label().eq_ignore_ascii_case(s))
.ok_or_else(|| bad(format!("unknown check style {s:?} (check, circle, cross, diamond, square, star)")))?,
),
},
};
if props == FieldProps::default() {
return Err(ToolError::InvalidArgs("nothing to change".into()));

View File

@ -274,7 +274,7 @@ pub fn tools() -> Vec<ToolDef> {
t(
"form_set_props",
"Field properties",
"Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }. Only the given ones change. Undoable.",
"Change a field's properties (General and Options tabs): name (renames it), tooltip, read_only, required, multiline, max_length (0 = no limit), options (combo/list items), font_size (0 = auto), rect (position), format, validate, calculate (Acrobat's Format/Validate/Calculate tabs, run natively: values are checked, formatted and recalculated as in Acrobat). Options tab: align (left, center, right), default (the value Reset form restores; for check boxes and radio buttons their on state, \"\" for off), flags { scroll, rich_text, password, file_select, spell_check, comb (needs max_length), sort, editable, multi_select, commit_immediately }, check_style (check boxes and radio buttons: check, circle, cross, diamond, square, star). Only the given ones change. Undoable.",
)
.with(schema(
json!({
@ -290,6 +290,7 @@ pub fn tools() -> Vec<ToolDef> {
"align": { "type": "string", "enum": ["left", "center", "right"] },
"locked": { "type": "boolean", "description": "Lock the field's properties (only unlocking is accepted while locked)." },
"default": { "type": "string" },
"check_style": { "type": "string", "enum": ["check", "circle", "cross", "diamond", "square", "star"], "description": "Check boxes and radio buttons: the mark shown when on." },
"flags": { "type": "object", "additionalProperties": { "type": "boolean" } },
"font_size": { "type": "number", "minimum": 0 },
"rect": { "type": "array", "items": { "type": "number" }, "minItems": 4, "maxItems": 4, "description": "Move/resize: points from the top-left of the displayed page." },

View File

@ -823,6 +823,12 @@ fn preparing_a_form_through_tools() {
assert_eq!((f[0]["required"].as_bool(), f[0]["tooltip"].as_str()), (Some(true), Some("Your name")));
assert_eq!(f[0]["rect"], json!([30.0, 20.0, 190.0, 42.0]), "moved; the rect round-trips in view coordinates");
assert_eq!(f[1]["options"], json!(["S", "L"]));
// #94: the mark a check box or radio button shows.
assert_eq!(f[1]["check_style"], "circle", "radio buttons default to a circle");
ok(&mut a, "form_set_props", json!({ "doc": doc, "field": "size", "check_style": "star" }));
assert_eq!(ok(&mut a, "form_fields", json!({ "doc": doc }))["fields"][1]["check_style"], "star");
assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "size", "check_style": "heart" })), Err(ToolError::InvalidArgs(_))));
assert!(matches!(a.call("form_set_props", &json!({ "doc": doc, "field": "full name", "check_style": "star" })), Err(ToolError::Failed(_))));
ok(&mut a, "form_fill", json!({ "doc": doc, "values": { "full name": "Ada", "size": "L" } }));
assert!(page_text(&mut a, doc)[0].contains("Ada"));
// Options tab: alignment, default, flags (comb needs a limit).

View File

@ -32,8 +32,8 @@ pub use printcraft_edit::{
Added, AddedImage, AddedText, Align as TextAlign, Background, Content as AddedContent, Family as FontFamily, HeaderFooter, MarkKind, Watermark,
};
pub use printcraft_forms::{
BorderStyle, Field as FormField, FieldAction, FieldFont, FieldKind as FormFieldKind, FieldProps, FieldValue, Look as FieldLook, NewField,
TabOrder, Trigger as FieldTrigger, Widget as FormWidget, af as form_scripts, flags as field_flags,
BorderStyle, CheckStyle, Field as FormField, FieldAction, FieldFont, FieldKind as FormFieldKind, FieldProps, FieldValue, Look as FieldLook,
NewField, TabOrder, Trigger as FieldTrigger, Widget as FormWidget, af as form_scripts, flags as field_flags,
};
pub use printcraft_a11y as a11y;
@ -386,6 +386,13 @@ impl Document {
Some(printcraft_forms::look(&e.cos, f))
}
/// A check box's or radio button's mark (Field Properties ▸ Options), `None` for other fields.
pub fn field_check_style(&self, name: &str) -> Option<CheckStyle> {
let e = self.editor.as_ref()?;
let f = self.form.iter().find(|f| f.name == name)?;
matches!(f.kind, FormFieldKind::CheckBox | FormFieldKind::Radio).then(|| printcraft_forms::check_style(&e.cos, f))
}
/// Remove Hidden Information: what each category would remove.
pub fn hidden_info(&self) -> Vec<(Hidden, usize)> {
self.editor.as_ref().map(|e| printcraft_redact::sanitize::scan(&e.cos)).unwrap_or_default()

View File

@ -320,6 +320,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
let wd = wobj.as_dict().cloned().unwrap_or_default();
let (width, height) = ((w.rect[2] - w.rect[0]).max(1.0), (w.rect[3] - w.rect[1]).max(1.0));
let (frame_c, _) = frame(doc, &wd, width, height);
let style = crate::author::CheckStyle::of_widget(doc, &wd, kind);
let mut form = |content: String| -> Object {
let mut d = Dict::new();
d.set(b"Type".to_vec(), Object::name("XObject"));
@ -328,51 +329,7 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
Object::Ref(doc.add(Object::Stream(Stream::flate(d, content.as_bytes()))))
};
let s = width.min(height);
let (cx, cy) = (width / 2.0, height / 2.0);
let mark = if kind == FieldKind::Radio {
let r = s * 0.25;
let k = 0.5523 * r;
format!(
"0 g\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
n(cx + r),
n(cy),
n(cx + r),
n(cy + k),
n(cx + k),
n(cy + r),
n(cx),
n(cy + r),
n(cx - k),
n(cy + r),
n(cx - r),
n(cy + k),
n(cx - r),
n(cy),
n(cx - r),
n(cy - k),
n(cx - k),
n(cy - r),
n(cx),
n(cy - r),
n(cx + k),
n(cy - r),
n(cx + r),
n(cy - k),
n(cx + r),
n(cy)
)
} else {
format!(
"0 G\n{} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
n((s * 0.1).max(1.0)),
n(cx - s * 0.28),
n(cy),
n(cx - s * 0.08),
n(cy - s * 0.22),
n(cx + s * 0.3),
n(cy + s * 0.25)
)
};
let mark = check_mark(style, width / 2.0, height / 2.0, s);
let mut nd = Dict::new();
nd.set(on_name.as_bytes().to_vec(), form(format!("{frame_c}{mark}")));
nd.set(b"Off".to_vec(), form(frame_c));
@ -381,6 +338,100 @@ pub fn check_box_states(doc: &mut Document, w: &Widget, kind: FieldKind, on_name
ap
}
/// The on-state mark for `style` (#94), centred on (`cx`, `cy`) in a box whose smaller side is
/// `s`, drawn in black.
fn check_mark(style: crate::author::CheckStyle, cx: f64, cy: f64, s: f64) -> String {
use crate::author::CheckStyle;
// A filled polygon through `pts`.
let polygon = |pts: &[(f64, f64)]| {
let mut c = String::from("0 g\n");
for (i, (x, y)) in pts.iter().enumerate() {
c.push_str(&format!("{} {} {} ", n(*x), n(*y), if i == 0 { "m" } else { "l" }));
}
c.push_str("h f\n");
c
};
let line = n((s * 0.1).max(1.0));
match style {
CheckStyle::Check => format!(
"0 G\n{line} w 1 J 1 j\n{} {} m {} {} l {} {} l S\n",
n(cx - s * 0.28),
n(cy),
n(cx - s * 0.08),
n(cy - s * 0.22),
n(cx + s * 0.3),
n(cy + s * 0.25)
),
CheckStyle::Cross => {
let d = s * 0.25;
format!(
"0 G\n{line} w 1 J\n{} {} m {} {} l S\n{} {} m {} {} l S\n",
n(cx - d),
n(cy - d),
n(cx + d),
n(cy + d),
n(cx - d),
n(cy + d),
n(cx + d),
n(cy - d)
)
}
CheckStyle::Square => {
let d = s * 0.22;
format!("0 g\n{} {} {} {} re f\n", n(cx - d), n(cy - d), n(2.0 * d), n(2.0 * d))
}
CheckStyle::Diamond => {
let d = s * 0.3;
polygon(&[(cx, cy + d), (cx + d, cy), (cx, cy - d), (cx - d, cy)])
}
CheckStyle::Star => {
// Five points: outer and inner radii alternate, the first point straight up.
let (outer, inner) = (s * 0.32, s * 0.32 * 0.382);
let pts: Vec<(f64, f64)> = (0..10)
.map(|i| {
let a = std::f64::consts::FRAC_PI_2 + i as f64 * std::f64::consts::PI / 5.0;
let r = if i % 2 == 0 { outer } else { inner };
(cx + r * a.cos(), cy + r * a.sin())
})
.collect();
polygon(&pts)
}
CheckStyle::Circle => {
let r = s * 0.25;
let k = 0.5523 * r;
format!(
"0 g\n{} {} m {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c {} {} {} {} {} {} c f\n",
n(cx + r),
n(cy),
n(cx + r),
n(cy + k),
n(cx + k),
n(cy + r),
n(cx),
n(cy + r),
n(cx - k),
n(cy + r),
n(cx - r),
n(cy + k),
n(cx - r),
n(cy),
n(cx - r),
n(cy - k),
n(cx - k),
n(cy - r),
n(cx),
n(cy - r),
n(cx + k),
n(cy - r),
n(cx + r),
n(cy - k),
n(cx + r),
n(cy)
)
}
}
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -89,6 +89,8 @@ pub struct FieldProps {
pub locked: Option<bool>,
/// Actions tab: every trigger's action (replacing the field's).
pub actions: Option<Vec<(crate::Trigger, crate::FieldAction)>>,
/// Options tab: the mark of a check box or radio button (every widget).
pub check_style: Option<CheckStyle>,
}
fn invalid<T>(m: impl Into<String>) -> Result<T, FormError> {
@ -215,6 +217,63 @@ impl FieldFont {
}
}
/// The mark a check box or radio button shows when on (Field Properties ▸ Options ▸ style).
/// Stored as `/MK /CA`, the ZapfDingbats character Acrobat uses for it; PrintCraft draws the
/// mark as paths, so no symbol font is needed.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum CheckStyle {
Check,
Circle,
Cross,
Diamond,
Square,
Star,
}
impl CheckStyle {
pub const ALL: [CheckStyle; 6] =
[CheckStyle::Check, CheckStyle::Circle, CheckStyle::Cross, CheckStyle::Diamond, CheckStyle::Square, CheckStyle::Star];
fn code(self) -> &'static str {
match self {
CheckStyle::Check => "4",
CheckStyle::Circle => "l",
CheckStyle::Cross => "8",
CheckStyle::Diamond => "u",
CheckStyle::Square => "n",
CheckStyle::Star => "H",
}
}
pub fn label(self) -> &'static str {
match self {
CheckStyle::Check => "Check",
CheckStyle::Circle => "Circle",
CheckStyle::Cross => "Cross",
CheckStyle::Diamond => "Diamond",
CheckStyle::Square => "Square",
CheckStyle::Star => "Star",
}
}
/// A widget's style: its `/MK /CA`, else a check for check boxes and a circle for radios.
pub(crate) fn of_widget(doc: &Document, wd: &Dict, kind: FieldKind) -> Self {
let mk = wd.get(b"MK").and_then(|m| doc.resolve(m).as_dict().cloned()).unwrap_or_default();
let ca = mk.get(b"CA").and_then(|c| doc.resolve(c).as_string().map(|s| s.to_text()));
CheckStyle::ALL.into_iter().find(|s| ca.as_deref() == Some(s.code())).unwrap_or(if kind == FieldKind::Radio {
CheckStyle::Circle
} else {
CheckStyle::Check
})
}
}
/// A check box's or radio button's style (from its first widget).
pub fn check_style(doc: &Document, f: &Field) -> CheckStyle {
let wd = f.widgets.first().and_then(|w| doc.get(w.obj).as_dict().cloned()).unwrap_or_default();
CheckStyle::of_widget(doc, &wd, f.kind)
}
fn rgb_of(doc: &Document, o: Option<&Object>) -> Option<[f64; 3]> {
let v: Vec<f64> = doc.resolve(o?).as_array()?.iter().filter_map(Object::as_f64).collect();
match v.len() {
@ -702,6 +761,9 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
if props.options.as_ref().is_some_and(|o| o.is_empty()) && matches!(f.kind, FieldKind::Combo | FieldKind::List) {
return invalid("a list needs at least one option");
}
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");
}
doc.update_dict(f.obj, |d| {
d.set(b"Ff".to_vec(), Object::Int(ff as i64));
if let Some(t) = &props.tooltip {
@ -785,6 +847,14 @@ pub fn set_props(doc: &mut Document, name: &str, props: &FieldProps) -> Result<S
})?;
}
}
if let Some(style) = props.check_style {
for w in &f.widgets {
// Resolved first: an indirect /MK keeps its colours.
let mut mk = doc.get(w.obj).as_dict().and_then(|d| d.get(b"MK").map(|m| doc.resolve(m).as_dict().cloned())).flatten().unwrap_or_default();
mk.set(b"CA".to_vec(), PdfString::text(style.code()));
doc.update_dict(w.obj, |d| d.set(b"MK".to_vec(), Object::Dict(mk)))?;
}
}
if let Some((wi, r)) = props.rect {
let w = f.widgets.get(wi).ok_or_else(|| FormError::Invalid(format!("{name} has no widget {}", wi + 1)))?;
let r = [r[0].min(r[2]), r[1].min(r[3]), r[0].max(r[2]), r[1].max(r[3])];

View File

@ -23,7 +23,8 @@ pub mod detect;
mod scripting;
pub use actions::{FieldAction, Trigger, field_actions, set_field_actions};
pub use author::{
BorderStyle, FieldFont, FieldProps, Look, NewField, add_field, delete_field, duplicate_field, look, redraw_field, set_button_icon, set_props,
BorderStyle, CheckStyle, FieldFont, FieldProps, Look, NewField, add_field, check_style, delete_field, duplicate_field, look, redraw_field,
set_button_icon, set_props,
};
pub use scripting::{
FieldChange, FieldEvent, NoScripts, ScriptResult, Scripts, apply_script_changes, document_scripts, document_scripts_named, set_document_script,

View File

@ -523,6 +523,58 @@ fn options_tab_flags_alignment_and_defaults() {
assert!(set_props(&mut doc, &text, &FieldProps { quadding: Some(5), ..FieldProps::default() }).is_err());
}
#[test]
fn check_box_styles_change_the_mark() {
// #94: Field Properties ▸ Options ▸ Check Box Style (stored as /MK /CA).
let mut doc = one_page();
let check = add_field(&mut doc, 0, [50.0, 700.0, 70.0, 720.0], &NewField::CheckBox, None).unwrap();
let radio = add_field(&mut doc, 0, [50.0, 600.0, 70.0, 620.0], &NewField::Radio { group: None, export: "A".into() }, None).unwrap();
let text = add_field(&mut doc, 0, [50.0, 500.0, 250.0, 520.0], &NewField::Text { multiline: false }, None).unwrap();
let style = |doc: &Document, name: &str| check_style(doc, field(&fields(doc), name));
assert_eq!((style(&doc, &check), style(&doc, &radio)), (CheckStyle::Check, CheckStyle::Circle), "the defaults");
// The "on" appearance: the widget's /AP /N entry for its on state.
let on_ap = |doc: &Document, name: &str| {
let all = fields(doc);
let w = &field(&all, name).widgets[0];
let on = w.on_state.clone().unwrap();
let n = doc
.get(w.obj)
.as_dict()
.unwrap()
.get(b"AP")
.unwrap()
.as_dict()
.unwrap()
.get(b"N")
.unwrap()
.as_dict()
.unwrap()
.reference(on.as_bytes())
.unwrap();
let Object::Stream(s) = &*doc.get(n) else { panic!() };
String::from_utf8_lossy(&s.decoded().unwrap()).into_owned()
};
let before = on_ap(&doc, &check);
for (s, code) in
[(CheckStyle::Cross, "8"), (CheckStyle::Square, "n"), (CheckStyle::Star, "H"), (CheckStyle::Diamond, "u"), (CheckStyle::Circle, "l")]
{
set_props(&mut doc, &check, &FieldProps { check_style: Some(s), ..FieldProps::default() }).unwrap();
let doc2 = reopen(&doc);
assert_eq!(style(&doc2, &check), s, "saved and read back");
let mk = doc2.get(field(&fields(&doc2), &check).widgets[0].obj).as_dict().unwrap().get(b"MK").unwrap().as_dict().cloned().unwrap();
assert_eq!(mk.get(b"CA").and_then(|c| c.as_string()).map(|c| c.to_text()).as_deref(), Some(code));
assert!(mk.get(b"BC").is_some(), "the border colour is kept");
assert_ne!(on_ap(&doc, &check), before, "{s:?} draws a different mark");
}
assert!(on_ap(&doc, &check).contains(" c f"), "a circle is a filled curve");
set_props(&mut doc, &radio, &FieldProps { check_style: Some(CheckStyle::Square), ..FieldProps::default() }).unwrap();
assert!(on_ap(&doc, &radio).contains(" re f"), "a square");
assert!(
set_props(&mut doc, &text, &FieldProps { check_style: Some(CheckStyle::Star), ..FieldProps::default() }).is_err(),
"text fields have no style"
);
}
#[test]
fn ordering_tabs_manually() {
let mut doc = one_page();

View File

@ -541,8 +541,10 @@ impl crate::PrintCraftApp {
let Some(f) = self.session.get(id).and_then(|d| d.form.iter().find(|f| f.name == name).cloned()) else { return };
let mut d = FieldDraft::new(&f, widget);
d.look = self.session.get(id).and_then(|doc| doc.field_look(name));
d.check_style = self.session.get(id).and_then(|doc| doc.field_check_style(name));
if let Some(o) = d.original.as_mut() {
o.look = d.look;
o.check_style = d.check_style;
}
let others: Vec<String> =
self.session.get(id).map(|doc| doc.form.iter().filter(|x| x.name != name).map(|x| x.name.clone()).collect()).unwrap_or_default();
@ -647,6 +649,8 @@ pub struct FieldDraft {
/// Left, bottom, width, height in points.
pub position: [f64; 4],
pub look: Option<FieldLook>,
/// Check boxes and radio buttons: the mark when on (Options tab).
pub check_style: Option<printcraft_engine::CheckStyle>,
pub format: Format,
pub validate: Validate,
pub calculate: Calculate,
@ -688,6 +692,7 @@ impl FieldDraft {
font_size: da_size(&f.da),
position: [r[0], r[1], r[2] - r[0], r[3] - r[1]],
look: None,
check_style: None,
format: f.actions.format.clone(),
validate: f.actions.validate.clone(),
calculate: f.actions.calculate.clone(),
@ -742,6 +747,7 @@ impl FieldDraft {
(self.widget, [x, y, x + w.max(4.0), y + h.max(4.0)])
}),
look: (self.look != o.look).then_some(self.look).flatten(),
check_style: (self.check_style != o.check_style).then_some(self.check_style).flatten(),
format: (self.format != o.format).then(|| self.format.clone()),
validate: (self.validate != o.validate).then(|| self.validate.clone()),
calculate: (self.calculate != o.calculate).then(|| self.calculate.clone()),
@ -963,6 +969,20 @@ pub(crate) fn body(ui: &mut egui::Ui, d: &mut FieldDraft, t: &crate::theme::Toke
if ui.checkbox(&mut checked, label).changed() {
d.default = if checked { on } else { String::new() };
}
if let Some(style) = d.check_style.as_mut() {
ui.horizontal(|ui| {
let l = ui.label(if d.kind == FormFieldKind::CheckBox { "Check Box Style:" } else { "Button Style:" });
egui::ComboBox::from_id_salt("check-style")
.selected_text(style.label())
.show_ui(ui, |ui| {
for s in printcraft_engine::CheckStyle::ALL {
ui.selectable_value(style, s, s.label());
}
})
.response
.labelled_by(l.id);
});
}
if d.kind == FormFieldKind::Radio {
flag_box(ui, &mut d.flags, ff::RADIOS_IN_UNISON, false, "Buttons with the same name and value are selected in unison");
}

View File

@ -170,6 +170,30 @@ fn appearance_tab_restyles_the_field() {
assert_eq!((l.style, l.font, l.fill), (BorderStyle::Dashed, FieldFont::Courier, Some([1.0, 1.0, 0.8])));
}
#[test]
fn options_tab_sets_the_check_box_style() {
// #94: Check Box Style on the Options tab.
use printcraft_engine::CheckStyle;
let mut h = harness();
h.state_mut().apply_edit(printcraft_engine::Edit::AddField {
page: 0,
rect: [40.0, 300.0, 56.0, 316.0],
kind: printcraft_engine::NewField::CheckBox,
name: Some("agree".into()),
});
h.state_mut().open_field_props("agree", 0);
h.run_steps(2);
h.get_by_label("Options").click();
h.run_steps(2);
h.get_by_label("Check Box Style:");
assert_eq!(h.state().field_props.as_ref().unwrap().check_style, Some(CheckStyle::Check));
h.state_mut().field_props.as_mut().unwrap().check_style = Some(CheckStyle::Cross);
h.get_by_label("OK").click();
h.run_steps(3);
let s = h.state();
assert_eq!(s.session.get(s.views[0].id).unwrap().field_check_style("agree"), Some(CheckStyle::Cross));
}
#[test]
fn options_tab_sets_flags_alignment_and_defaults() {
use printcraft_engine::field_flags as ff;

View File

@ -4689,6 +4689,18 @@ tools = ["form_add_field"]
evidence = ["crates/forms/src/tests.rs::every_field_type_can_be_added_named_and_drawn", "crates/automation/tests/automation.rs::preparing_a_form_through_tools"]
notes = "new group Group1… or join the selected group with the next ChoiceN"
[[feature]]
id = "form.check-box-style"
title = "Check box and radio button styles"
area = "F"
tier = "P1"
milestone = "M6"
status = "shipped"
commands = ["form.field.properties"]
tools = ["form_set_props", "form_fields"]
evidence = ["crates/forms/src/tests.rs::check_box_styles_change_the_mark", "crates/ui-egui/tests/prepare.rs::options_tab_sets_the_check_box_style", "crates/automation/tests/automation.rs::preparing_a_form_through_tools"]
notes = "Field Properties ▸ Options: check, circle, cross, diamond, square, star (/MK /CA, Acrobat's codes), drawn as paths; defaults check (check boxes) and circle (radio buttons)"
[[feature]]
id = "form.add-dropdown"
title = "Add drop-down lists"