rust_widgets 2.5.2

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
// 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
//!
//! `<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.
//!
//! # 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 -->";

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();

    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;
            };
            // 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.
            rust_widgets::theme::apply_theme_to_widget(widget.as_mut());

            let Some(drawable) = draw_of(widget.as_mut()) else {
                failed.push(format!("{name}: the widget has no `Draw` implementation"));
                continue;
            };

            // 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);
            fs::write(dir.join(format!("{name}{suffix}.svg")), document)?;
            written += 1;
        }
    }

    // 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={}", names.len(), failed.len());
    if !failed.is_empty() {
        for entry in &failed {
            eprintln!("  {entry}");
        }
        return Err(format!("{} control(s) could not be exported", failed.len()).into());
    }
    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.
fn decorate(body: &str, name: &str, appearance: AppearanceMode) -> String {
    let label = match appearance {
        AppearanceMode::Light => "light",
        AppearanceMode::Dark => "dark",
    };
    format!("{GENERATED_MARKER}\n<!-- control: {name} | appearance: {label} -->\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
}