rust_widgets 2.8.2

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
// SPDX-FileCopyrightText: Copyright (c) 2026 Mike Li/Mikewolfli/Wei Li(mikewolfli@163.com)
// SPDX-License-Identifier: MIT

//! BLUE20 layer 3 — export every control as an SVG snapshot.
//!
//! # What this is for
//!
//! Layers 1 and 2 are **machine** judgements: they can assert "this control painted
//! something", "its chrome moved with the theme", "this declared property is answered".
//! None of them can say "this control looks wrong". A misplaced label, a fill that hides
//! its own border, an icon that reads as a smudge — all of those satisfy every assertion
//! and are only visible to a person looking at the picture.
//!
//! So this writes one SVG per control to `snapshots/svg/`, named by `canonical_name`:
//! the 188 files become a **reviewable artifact** rather than a number. Because they are
//! committed and regenerable, any later visual change shows up as a diff.
//!
//! # Why the geometry is fixed rather than per-control
//!
//! A size chosen per control would make each image pretty and cross-control comparison
//! impossible. A single canvas (240x120, the same `CENSUS_RECT` the census measures over)
//! means two PNGs of two controls can be laid side by side and judged against each other,
//! and it makes "does this control fill its box" a question with a visible answer.
//!
//! # Why two appearances, and why some controls have a third
//!
//! `<name>.svg` is the dark appearance (the demo's default) and `<name>.light.svg` is the
//! light one. The pair is what makes "does this control respond to the theme" checkable by
//! eye rather than only by the P3 assertion: a control whose two files are identical is
//! theme-blind in a way a person can *see*, and `tools/check_svg_snapshots.sh` requires
//! both files to exist so one cannot be quietly dropped.
//!
//! A few controls have a **state** that no static appearance can show and that a user can put
//! them into: `group_box`'s tick is the one part of it a user toggles, and its `checkable` flag
//! defaults to `false`, so an unchecked `group_box.svg` is the only picture the pair carries.
//! BLUE23 §4.1 asks for that state to enter the snapshot set as `group_box_checked` — a third
//! file, produced by [`EXTRA_APPEARANCES`], so the tick is reviewable rather than merely present
//! in the source. Extras are declared here rather than invented per control so the set is one
//! list, and `tools/check_svg_snapshots.sh` derives its expectations from the same list.
//!
//! # Regenerating
//!
//! ```text
//! cargo run --no-default-features --features desktop --example export_control_svgs
//! ```
//!
//! `tools/check_svg_snapshots.sh` runs exactly that and requires the result to match the
//! committed files byte for byte, so a change to any control's drawing appears as a diff
//! in review (rule #106).

#![cfg(all(not(feature = "mini"), not(target_arch = "wasm32")))]

use rust_widgets::theme::{theme_test_guard, AppearanceMode};
use rust_widgets::widget::census::{install_preset_appearances, CENSUS_RECT, CENSUS_TEXT};
use rust_widgets::widget::svg::render_widget_to_svg_on;
use rust_widgets::widget::{draw_bridge::draw_of, WidgetFactory};
use std::fs;
use std::path::Path;

/// Where the snapshots live, relative to the crate root.
const OUTPUT_DIR: &str = "snapshots/svg";

/// The marker every generated SVG carries.
///
/// Rule #106 asks for snapshots that a gate can recognise as **generated**: without it a
/// "skip non-generated files" check would treat these as hand-written artifacts and could
/// legitimately skip them, which is the silent-coverage loss the rule is about.
/// The `tools/check_generated_sources.sh` machinery looks for this shape.
const GENERATED_MARKER: &str = "<!-- GENERATED by examples/export_control_svgs.rs -->";

/// How long one settle tick advances a control's transition, in milliseconds.
///
/// 16 ms is one frame at 60 Hz, so settling a transition costs about as many ticks as a user would
/// see frames — and the loop exits as soon as the control reports it has stopped.
const SETTLE_TICK_MS: u32 = 16;

/// The most settle ticks the exporter will spend on one control.
///
/// A bound rather than an unbounded `while`: a perpetual animation (a spinner, a skeleton loader)
/// never reports settled, and an unbounded loop would hang the export. Perpetual animations are
/// drawn at this phase, which is what makes their snapshots reproducible.
const MAX_SETTLE_TICKS: usize = 64;

/// The controls that have a reviewable **interactive state** beyond the two appearances.
///
/// # Why this is a list and not per-control code
///
/// The snapshot set is derived from this table, so "which files exist" and "which states were
/// exercised" cannot disagree. A per-control `if` in the loop below would put the two facts in
/// different places, and the file's existence would stop being evidence that anything looked at
/// the state.
///
/// Each entry is `(control, suffix, describe)`:
///
/// * `control` — the registry name, i.e. the `<name>` half of the file name;
/// * `suffix` — appended to the file name, so `group_box` + `_checked` is `group_box_checked.svg`;
/// * `describe` — the `<!-- control: … -->` marker's state word, so a reader of one file can tell
///   which state it shows without comparing it against its siblings.
const EXTRA_APPEARANCES: &[(&str, &str, &str)] = &[
    // BLUE23 §4.1: the tick is the only part of a group box a user toggles, and `create_group_box`
    // leaves `checkable` false — so without this the checked appearance is absent from the set.
    ("group_box", "_checked", "checked"),
    // BLUE24 §10A.6 criterion 6 ("every declared face is reviewable"): `frame`'s default shape is
    // `Box`, which paints an outline and **no fill at all**, so the three shapes that *do* paint a
    // face — `styled_panel`, `win_panel`, and the 3D `panel` edge — had no snapshot anywhere in the
    // set. A control with three declaring draw paths and one exercised path is the coverage loss
    // the criterion is about: a defect inside `draw_styled_panel_frame` was invisible to every
    // automated check this crate has.
    //
    // The shape is a factory property (`FRAME_PROPERTIES`), so the extra appearances are the three
    // values of it that are otherwise unreachable from the default construction.
    ("frame", "_styled_panel", "styled_panel"),
    ("frame", "_win_panel", "win_panel"),
    ("frame", "_panel", "panel"),
    // A toggle's *on* state is the half a user actually wants to check, and it was in no snapshot.
    //
    // `switch`, `radio_button`, `check_box` and `toggle_button` all publish `set_checked`, and the
    // default construction of the first two leaves them **off** — so `radio_button.svg` showed an
    // empty ring, and the appearance a user sees when the control is on had never been rendered by
    // anything. `check_box` was the accidental exception (its constructor starts checked), which is
    // exactly the kind of accident that hides a gap: one control in the family happened to exercise
    // the state, so "the family is covered" looked true.
    //
    // This is BLUE24 §10A.6 criterion 6 — every declared state must be reviewable — applied to
    // a state a user toggles rather than to a drawing path a theme selects.
    //
    // # Why `switch` is not in this list
    //
    // It was, as `switch_on`, and the file turned out to be **byte-identical** to `switch.svg` once
    // the exporter settled animations before drawing (see `settle`): `sample_fill` already turns a
    // switch on, so the default file already shows the on state and the extra showed the same
    // picture under a different name. That is the defect this list exists to avoid — an artifact
    // that adds a file without adding information — so the entry was removed rather than kept.
    // A switch's *off* state is the one the default file cannot show, and that gap is real; it is
    // recorded here rather than papered over with a duplicate.
    ("radio_button", "_checked", "checked"),
    ("toggle_button", "_checked", "checked"),
    // A tooltip's **shown** state is the only state a user ever sees, and it was in no snapshot.
    //
    // `create_tooltip` starts hidden, and the default file exports that rest state — where the
    // bubble is composited fully toward the window by the fade, so `tooltip.svg`'s bubble fill
    // *equals* the window fill and the picture says nothing about the control. That hid a real
    // defect for several rounds: the bubble colour was taken from a field whose default is
    // `rgba(40,40,40,220)` rather than the window, so the theme's `inverse_surface` arm below it
    // was unreachable and the shown bubble painted the same near-black in **both** appearances
    // (1.27:1 against the dark window — a smudge, not a tooltip). Measured before the fix with
    // `tests/tooltip_paint_probe.rs`. The defect was invisible to the snapshot set precisely
    // because the snapshot set had no shown tooltip in it.
    ("tooltip", "_shown", "shown"),
];

/// Applies the state an extra appearance depicts. Returns `false` when the control does not have
/// the state, which is reported rather than silently skipped.
fn apply_extra_state(
    control: &str,
    suffix: &str,
    widget: &mut dyn rust_widgets::widget::Widget,
) -> bool {
    match (control, suffix) {
        ("group_box", "_checked") => {
            use rust_widgets::widget::container_widgets::groupbox::GroupBox;
            match rust_widgets::widget::capability::coercion::widget_as_mut::<GroupBox>(widget) {
                Some(group_box) => {
                    group_box.set_checkable(true);
                    group_box.set_checked(true);
                    true
                }
                None => false,
            }
        }
        // The toggles, each through its own `set_checked`. The string in `EXTRA_APPEARANCES` and the
        // state that is drawn cannot disagree, because the arm names the state and the control is
        // asked for it by its published name — a mismatch would produce a snapshot of the *off* state
        // under a name that says `on`, which is the one failure this has to rule out.
        ("radio_button", "_checked") => {
            use rust_widgets::widget::base_widgets::radiobutton::RadioButton;
            match rust_widgets::widget::capability::coercion::widget_as_mut::<RadioButton>(widget) {
                Some(radio) => {
                    radio.set_checked(true);
                    true
                }
                None => false,
            }
        }
        ("toggle_button", "_checked") => {
            use rust_widgets::widget::base_widgets::toggle_button::ToggleButton;
            match rust_widgets::widget::capability::coercion::widget_as_mut::<ToggleButton>(widget)
            {
                Some(button) => {
                    button.set_checked(true);
                    true
                }
                None => false,
            }
        }
        // The three frame shapes that paint a fill, each reached through the control's own
        // `from_name`, so the string in `EXTRA_APPEARANCES` and the shape that is drawn cannot
        // disagree — a mismatch would produce a snapshot of the *default* shape under a name that
        // claims otherwise, which is the one failure mode this arm has to rule out.
        ("frame", "_styled_panel" | "_win_panel" | "_panel") => {
            use rust_widgets::widget::base_widgets::frame::{Frame, FrameShape};
            let shape = FrameShape::from_name(&suffix[1..]).expect("a name from EXTRA_APPEARANCES");
            match rust_widgets::widget::capability::coercion::widget_as_mut::<Frame>(widget) {
                Some(frame) => {
                    frame.set_frame_shape(shape);
                    true
                }
                None => false,
            }
        }
        // A shown tooltip is the state a user sees, and it is reached through the control's own
        // `show`, so the string in `EXTRA_APPEARANCES` and the state drawn cannot disagree.
        ("tooltip", "_shown") => {
            use rust_widgets::widget::dialog::tooltip::Tooltip;
            match rust_widgets::widget::capability::coercion::widget_as_mut::<Tooltip>(widget) {
                Some(tooltip) => {
                    tooltip.show();
                    true
                }
                None => false,
            }
        }
        _ => false,
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Serialises against any other test or probe that switches the process-wide theme, so
    // this cannot observe a half-switched palette.
    let _guard = theme_test_guard();
    install_preset_appearances();

    let dir = Path::new(OUTPUT_DIR);
    fs::create_dir_all(dir)?;

    let factory = WidgetFactory::new_with_defaults();
    let names = factory.widget_names();

    let mut written = 0usize;
    let mut failed: Vec<String> = Vec::new();
    // Counted so the number of controls that received sample data is *reported* rather than assumed. A
    // control whose fill silently stopped matching its name would otherwise be indistinguishable from one
    // that never had data to begin with.
    let mut filled = 0usize;

    for name in &names {
        for (appearance, suffix) in [(AppearanceMode::Dark, ""), (AppearanceMode::Light, ".light")]
        {
            rust_widgets::theme::global_theme_manager().set_appearance(appearance);
            let Some(mut widget) = factory.create(name, CENSUS_RECT, CENSUS_TEXT) else {
                failed
                    .push(format!("{name}: the registry publishes it but `create` returned None"));
                continue;
            };
            // Give the data-bearing controls their content.
            //
            // # Why here and not in the factory
            //
            // `create` is the production path, so a host that asked for an empty table must get an empty
            // one; the sample data exists for the picture. Applying it after construction and before the
            // first draw is what keeps those two requirements apart. See
            // `widget::sample_fill`'s module docs.
            if rust_widgets::widget::sample_fill::apply(name, widget.as_mut()) {
                filled += 1;
            }

            // The theme the control renders under is applied the way the runtime applies
            // it, so the snapshot shows the colours a user would actually get. Without
            // this the two appearances produced identical drawings — the files existed and
            // proved nothing, which is precisely the failure rule #106 is about.
            //
            // # Why this runs **after** the sample fill, and not before
            //
            // `apply_active_theme` looks the control up by `"<kind>:<state>"`, and the state it
            // asks for is `widget.widget_state()`. Applying the theme first therefore resolved the
            // **constructor's** state, so every `:checked` key in the presets was invisible to the
            // snapshot: `check_box.svg` was drawn checked but painted the *resting* fill, because at
            // theme time the box was still unchecked. A state override the picture cannot show is
            // the same defect as a state the control never reports — the key is declared and
            // carries nothing.
            //
            // The runtime has the same ordering requirement, and for the same reason: a style is
            // resolved *for the state the control is in*. A host that re-applies the theme when a
            // control's state changes (which is what `request_redraw`'s callers do) reaches the same
            // answer this ordering reaches once.
            rust_widgets::theme::apply_theme_to_widget(widget.as_mut());

            // Settle any animation the sample data started; see `settle` for the defect this
            // removes (`switch.svg` captured a mid-travel frame).
            settle(widget.as_mut());

            write_one(
                dir,
                &mut written,
                name,
                suffix,
                "",
                "",
                widget.as_mut(),
                appearance,
                &mut failed,
            )?;
        }

        // The interactive states, one file each. Rendered under the dark appearance: these exist to
        // show a *shape* the default file cannot, and the light/dark question is already answered by
        // the pair above.
        for (control, state_suffix, state_word) in EXTRA_APPEARANCES {
            if control != name {
                continue;
            }
            rust_widgets::theme::global_theme_manager().set_appearance(AppearanceMode::Dark);
            let Some(mut widget) = factory.create(name, CENSUS_RECT, CENSUS_TEXT) else {
                continue;
            };
            rust_widgets::widget::sample_fill::apply(name, widget.as_mut());
            if !apply_extra_state(control, state_suffix, widget.as_mut()) {
                failed.push(format!(
                    "{name}{state_suffix}: the extra appearance names a state this control does not have"
                ));
                continue;
            }
            // After the state, for the same reason the two-appearance loop above applies it there:
            // the state is what the lookup key is built from, so theming first would resolve the
            // constructor's state and this file would show the state's *shape* in the resting
            // colours — which is exactly what it is not for.
            rust_widgets::theme::apply_theme_to_widget(widget.as_mut());
            settle(widget.as_mut());
            write_one(
                dir,
                &mut written,
                name,
                state_suffix,
                &format!(" ({state_word})"),
                state_word,
                widget.as_mut(),
                AppearanceMode::Dark,
                &mut failed,
            )?;
            // ... and the same state under the light appearance.
            //
            // The pair above settles "does this control respond to an appearance switch" for the
            // *default* shape only. A shape reachable solely through a factory property needs its
            // own pair, or the light rendering of `draw_win_panel_frame`'s white/grey bevel — whose
            // whole purpose is an illusion of relief — would never have been looked at.
            rust_widgets::theme::global_theme_manager().set_appearance(AppearanceMode::Light);
            let Some(mut widget) = factory.create(name, CENSUS_RECT, CENSUS_TEXT) else {
                continue;
            };
            rust_widgets::widget::sample_fill::apply(name, widget.as_mut());
            if !apply_extra_state(control, state_suffix, widget.as_mut()) {
                continue;
            }
            rust_widgets::theme::apply_theme_to_widget(widget.as_mut());
            settle(widget.as_mut());
            write_one(
                dir,
                &mut written,
                name,
                &format!("{state_suffix}.light"),
                &format!(" ({state_word}, light)"),
                state_word,
                widget.as_mut(),
                AppearanceMode::Light,
                &mut failed,
            )?;
        }
    }

    // The index is generated too, so the list of controls cannot drift from the files.
    fs::write(dir.join("README.md"), index(&names))?;

    println!("wrote {written} SVG snapshots for {} controls into {OUTPUT_DIR}/", names.len());
    println!(
        "checked={} skipped=0 failed={} sample-filled={}",
        names.len(),
        failed.len(),
        // Two appearances per control, so the counter is per-export; halved for the control count.
        filled / 2
    );
    if !failed.is_empty() {
        for entry in &failed {
            eprintln!("  {entry}");
        }
        return Err(format!("{} control(s) could not be exported", failed.len()).into());
    }
    Ok(())
}

/// Advances a control's transition a bounded number of times, so a settleable animation reaches its
/// rest state before the picture is taken.
///
/// # The defect this removes
///
/// `sample_fill` turns feature states *on* — `switch.set_checked(true)`, for one — and a control
/// with an animated transition therefore starts one. Drawing the very next frame captured
/// `switch.svg` **mid-travel**: measured, the track was a blend of the off and on endpoint colours
/// (travel ≈ 0.10), which is a picture of a frame no user rests in. It is neither the off state nor
/// the on state, and it moved for a reason — the A-2 fix changed the *ON endpoint* to the theme's
/// declared `switch:checked` fill — that has nothing to do with the state the file claims to show.
///
/// # Why a perpetual animation is not a failure
///
/// Some controls animate **forever** by design: a spinner, a skeleton loader's shimmer, a code
/// editor's caret, a terminal cursor. Their animation has no rest state to settle to — a spinner
/// that stopped would be a broken spinner — so "still animating after the bound" is the *correct*
/// answer for them, not a defect. The loop is therefore bounded and reaching the bound is
/// **accepted**: those controls are drawn at a defined, repeatable phase (`MAX_SETTLE_TICKS *
/// SETTLE_TICK_MS` into the animation), which is exactly what a snapshot of a perpetual animation
/// must be for the file to be regenerable byte-for-byte.
///
/// # Why the bound is the same for every control
///
/// A per-control bound would need a table of "how long is long enough", and a wrong entry would
/// silently re-introduce the mid-travel defect for one control. One bound generous enough for every
/// settleable transition (measured: the slowest is the switch's travel, well under 100 ms) is the
/// whole rule, and it needs no maintenance when a control is added.
fn settle(widget: &mut dyn rust_widgets::widget::Widget) {
    for _ in 0..MAX_SETTLE_TICKS {
        if !widget.is_animating() {
            return;
        }
        let _ = widget.tick(SETTLE_TICK_MS);
    }
}

/// Renders one widget and writes one snapshot file.
///
/// # Why this is extracted
///
/// The two-appearance loop and the extra-appearance loop both need the same four steps: attach
/// the `Draw` view, pick the backdrop, render, write. Keeping one copy is what makes an extra
/// appearance impossible to render *differently* from a default one — the failure mode a second
/// inline copy would invite.
#[allow(clippy::too_many_arguments)]
fn write_one(
    dir: &Path,
    written: &mut usize,
    name: &str,
    suffix: &str,
    caption: &str,
    state_word: &str,
    widget: &mut dyn rust_widgets::widget::Widget,
    appearance: AppearanceMode,
    failed: &mut Vec<String>,
) -> Result<(), Box<dyn std::error::Error>> {
    let Some(drawable) = draw_of(widget) else {
        failed.push(format!("{name}: the widget has no `Draw` implementation"));
        return Ok(());
    };

    // A control that paints no background of its own (a `Label`, a `Separator`) is
    // composited over the surface it would really sit on — the active theme's own
    // background. Filling the frame with a fixed white made exactly those controls
    // unreadable in the snapshot: the dark theme's near-white ink on white showed as a
    // blank rectangle while the light snapshot of the same control looked fine. The
    // difference between the two files was an artefact of the exporter, not of the
    // control, and the snapshots exist to show the control.
    let backdrop = rust_widgets::theme::global_theme_manager()
        .current_theme()
        .map(|active| active.colors.background)
        .unwrap_or(rust_widgets::core::Color::WHITE);
    let body = render_widget_to_svg_on(drawable, CENSUS_RECT, backdrop);
    let document = decorate(&body, name, appearance, caption, state_word);
    fs::write(dir.join(format!("{name}{suffix}.svg")), document)?;
    *written += 1;
    Ok(())
}

/// Wraps the renderer's `<svg …>` body with the provenance comment and a theme label.
///
/// The renderer's own output is left byte-for-byte intact after the opening tag, so a
/// diff of two snapshots is a diff of the drawing rather than of this wrapper.
///
/// # Why the caption and the state word are two arguments
///
/// They are two different readers' spellings of one fact, and collapsing them cost a snapshot.
/// `state_word` is what a **gate** reads out of the provenance line, and its contract is the bare
/// token the extra appearance was declared with — `checked`. `caption` is what a **person** reads
/// in `group_box_checked.svg` to know what it shows without diffing it against its siblings, and it
/// is free to be prose. Passing one string for both meant adding a caption to the frame extras
/// rewrote `group_box_checked.svg`'s provenance line to `state:  (checked)`, i.e. a committed
/// snapshot changed for a reason that had nothing to do with any control's drawing.
fn decorate(
    body: &str,
    name: &str,
    appearance: AppearanceMode,
    caption: &str,
    state_word: &str,
) -> String {
    let label = match appearance {
        AppearanceMode::Light => "light",
        AppearanceMode::Dark => "dark",
    };
    if state_word.is_empty() {
        format!("{GENERATED_MARKER}\n<!-- control: {name} | appearance: {label} -->\n{body}\n")
    } else {
        format!(
            "{GENERATED_MARKER}\n<!-- control: {name} | appearance: {label} | state: {state_word}{caption} -->\n{body}\n"
        )
    }
}

/// Builds `snapshots/svg/README.md`, listing every control with both of its files.
///
/// Generated rather than hand-written so a control added to the registry cannot leave the
/// index describing an incomplete set — which is exactly the drift this file would
/// otherwise acquire.
fn index(names: &[&'static str]) -> String {
    let mut out = String::new();
    out.push_str("<!-- GENERATED by examples/export_control_svgs.rs -->\n");
    out.push_str("# Control SVG snapshots (BLUE20 layer 3)\n\n");
    out.push_str(
        "One SVG per control, named by its **canonical name**. Each control appears twice:\n\
         `<name>.svg` is the dark appearance, `<name>.light.svg` the light one.\n\n",
    );
    out.push_str(
        "## Why the pair matters\n\n\
         Two files that are byte-identical mean the control renders the same in both\n\
         appearances, so it does not respond to the theme. The files exist so that is\n\
         visible to a person, not only to the `P3` assertion in\n\
         `tools/check_control_rendering.sh`.\n\n",
    );
    out.push_str(
        "## Regenerating\n\n\
         ```bash\n\
         cargo run --no-default-features --features desktop --example export_control_svgs\n\
         ```\n\n\
         `tools/check_svg_snapshots.sh` regenerates the set and requires it to match the\n\
         committed files byte for byte, so **any** change to a control's drawing shows up as\n\
         a diff. Do not hand-edit these files: a hand-edit is reverted by the next run and\n\
         fails the gate in between.\n\n",
    );
    out.push_str(
        "## Adding a control\n\n\
         1. Register it in `src/widget/capability/registration.rs` (capability + constructor).\n\
         2. Re-run the exporter above; the new control's two files and its index row are\n\
            generated automatically — this list is derived from the registry, never typed.\n\
         3. Run `bash tools/check_svg_snapshots.sh`.\n\n",
    );
    out.push_str(&format!("## Controls ({})\n\n", names.len()));
    out.push_str("| # | Control | Dark | Light |\n|---|---|---|---|\n");
    for (index, name) in names.iter().enumerate() {
        out.push_str(&format!(
            "| {} | `{name}` | `{name}.svg` | `{name}.light.svg` |\n",
            index + 1
        ));
    }
    out
}