Skip to main content

max / makeover-webview

Say where each option landed, completing the placed set field_html_placed is field_html_into plus a record of each `<option>`'s offsets. Third of the three, on the same terms as cells and bars: two options with the same label are the same bytes, so a caller compiling the field into a template cannot find one by searching for it.
Co-Authored-By
Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session
https://claude.ai/code/session_01P8ostB2UmZJGj5WjSHRSot
Author: Max Johnson <me@maxj.phd> · 2026-09-08 20:08 UTC
Signed with PGP, not checked
Commit: 9fe53b5452dac014dcdf66e384da64240605f76c
Parent: 0d2cdc1
1 file changed, +50 insertions, -4 deletions
M src/form.rs +50 -4
@@ -688,7 +688,13 @@
688 688 /// nobody chose. goingson hit exactly that with a backup-retention default of
689 689 /// 10 against a 1/3/7/14/0 list, and grew this stray-option fix locally; it is
690 690 /// here so the second app gets it without hitting the bug first.
691 - fn push_options(out: &mut String, field: &Field<'_>, options: &[Choice<'_>], value: &str) {
691 + fn push_options(
692 + out: &mut String,
693 + field: &Field<'_>,
694 + options: &[Choice<'_>],
695 + value: &str,
696 + mut placed: Option<&mut Vec<core::ops::Range<usize>>>,
697 + ) {
692 698 // The unanswered state, which HTML has no attribute for: `placeholder` is
693 699 // not a `<select>` attribute, and the idiom is an empty option that cannot
694 700 // be chosen back. `disabled` is what stops it being re-selected once the
@@ -721,6 +727,7 @@
721 727 );
722 728 }
723 729 for opt in options {
730 + let at = out.len();
724 731 out.push_str("<option value=\"");
725 732 escape_into(opt.value, out);
726 733 out.push('"');
@@ -762,6 +769,9 @@
762 769 escape_into(reason, out);
763 770 }
764 771 out.push_str("</option>");
772 + if let Some(placed) = placed.as_deref_mut() {
773 + placed.push(at..out.len());
774 + }
765 775 }
766 776 }
767 777
@@ -855,7 +865,13 @@
855 865 }
856 866
857 867 /// The control itself, without its label, hint or error.
858 - fn push_control(out: &mut String, field: &Field<'_>, filling: &Filling<'_>, opts: &Emit) {
868 + fn push_control(
869 + out: &mut String,
870 + field: &Field<'_>,
871 + filling: &Filling<'_>,
872 + opts: &Emit,
873 + placed: Option<&mut Vec<core::ops::Range<usize>>>,
874 + ) {
859 875 // Emitted before anything else is computed: a radio group carries its
860 876 // descriptions on the group rather than on a control, so none of the
861 877 // attributes below belong to it.
@@ -921,7 +937,7 @@
921 937 // A select described with no options emits an empty select, which
922 938 // says so on screen rather than in a log. That is the description's
923 939 // own position on `Field::options`, not a fallback invented here.
924 - push_options(out, field, field.options, filling.value.as_text());
940 + push_options(out, field, field.options, filling.value.as_text(), placed);
925 941 out.push_str("</select>");
926 942 }
927 943 // The one place this renderer emits `<optgroup>`, and it emits it
@@ -1308,6 +1324,36 @@
1308 1324 /// these, so a host building one should hold a single buffer and append each
1309 1325 /// field into it rather than take a `String` per field and concatenate.
1310 1326 pub fn field_html_into(field: &Field<'_>, filling: &Filling<'_>, opts: &Emit, out: &mut String) {
1327 + emit_field(field, filling, opts, out, None);
1328 + }
1329 +
1330 + /// One field, saying where each of its options landed.
1331 + ///
1332 + /// Byte-identical to [`field_html_into`], and it appends one entry to `placed`
1333 + /// per option of a select, in order: the offsets in `out` between which that
1334 + /// `<option>` was written. Nothing is appended for a field that offers no
1335 + /// options.
1336 + ///
1337 + /// Same reason as [`crate::list::cells_html_placed`]: a caller compiling a
1338 + /// described screen into a template has to know which bytes one option
1339 + /// produced, and two options with the same label are the same bytes.
1340 + pub fn field_html_placed(
1341 + field: &Field<'_>,
1342 + filling: &Filling<'_>,
1343 + opts: &Emit,
1344 + out: &mut String,
1345 + placed: &mut Vec<core::ops::Range<usize>>,
1346 + ) {
1347 + emit_field(field, filling, opts, out, Some(placed));
1348 + }
1349 +
1350 + fn emit_field(
1351 + field: &Field<'_>,
1352 + filling: &Filling<'_>,
1353 + opts: &Emit,
1354 + out: &mut String,
1355 + placed: Option<&mut Vec<core::ops::Range<usize>>>,
1356 + ) {
1311 1357 let id = filling.id_for(field.name);
1312 1358
1313 1359 if !field.kind.visible() {
@@ -1351,7 +1397,7 @@
1351 1397 out.push_str("</label>");
1352 1398 }
1353 1399
1354 - push_control(out, field, filling, opts);
1400 + push_control(out, field, filling, opts, placed);
1355 1401
1356 1402 // Adjacent text, because HTML has no unit attribute and inventing one would
1357 1403 // be markup nothing reads. Pointed at by `aria-describedby` so it is not