rust_widgets 2.8.0

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

//! BLUE25 ICON-7 — the reviewable picture of the whole icon set, in two files.
//!
//! # Why a *sheet* and not one file per icon
//!
//! The control snapshots give each control its own SVG because there are 188 of them and one
//! picture per control is the unit a reviewer judges. Icons are different: there are 31, they are
//! small, and the question a person actually has is comparative — "is `close` distinguishable from
//! `cross`?", "do the arrows point the way their names say?". Answering that from 62 separate files
//! means opening 62 files. Two sheets, one per appearance, answer it at a glance, and they are what
//! the census digest cannot show: the census proves the pictures *differ*, the sheet shows *how*.
//!
//! # What is drawn
//!
//! A grid of every [`IconName`], four per row, each labelled with its token. The label matters: the
//! whole class of defect this round is about is a name and a picture disagreeing, and an unlabelled
//! sheet cannot show that.
//!
//! # Regenerating
//!
//! ```text
//! cargo run --no-default-features --features desktop,icons --example export_icon_sheet
//! ```
//!
//! `tools/check_svg_snapshots.sh` runs exactly that and requires the result to match the committed
//! files byte for byte, so a change to how an icon draws appears as a diff in review.

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

use std::fs;
use std::path::Path;

use rust_widgets::core::{Color, Font, HorizontalAlignment, Point, Rect};
use rust_widgets::render::{PaintBackend, RenderContext, SvgPaintBackend};
use rust_widgets::theme::AppearanceMode;
use rust_widgets::widget::{Draw, Icon, IconName};

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

/// The marker every generated SVG carries, so a gate can recognise the sheet as generated.
const GENERATED_MARKER: &str = "<!-- GENERATED by examples/export_icon_sheet.rs -->";

/// Icons per row, and how much room each cell gets.
const COLUMNS: usize = 8;
const CELL: u32 = 48;
const LABEL_HEIGHT: u32 = 14;
const PADDING: u32 = 8;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let dir = Path::new(OUTPUT_DIR);
    fs::create_dir_all(dir)?;

    for (appearance, suffix, label) in
        [(AppearanceMode::Dark, "", "dark"), (AppearanceMode::Light, ".light", "light")]
    {
        rust_widgets::theme::global_theme_manager().set_appearance(appearance);
        let backdrop = rust_widgets::theme::global_theme_manager()
            .current_theme()
            .map(|active| active.colors.background)
            .unwrap_or(Color::WHITE);
        let ink = rust_widgets::theme::global_theme_manager()
            .current_theme()
            .map(|active| active.colors.foreground)
            .unwrap_or(Color::BLACK);
        let svg = render_sheet(backdrop, ink);
        let document =
            format!("{GENERATED_MARKER}\n<!-- icon sheet | appearance: {label} -->\n{svg}\n");
        let path = dir.join(format!("icon_sheet{suffix}.svg"));
        fs::write(&path, document)?;
        println!("wrote {}", path.display());
    }
    Ok(())
}

/// Renders every icon into one grid, over `backdrop` with `ink` for the labels.
fn render_sheet(backdrop: Color, ink: Color) -> String {
    let rows = IconName::ALL.len().div_ceil(COLUMNS);
    let width = PADDING + COLUMNS as u32 * (CELL + PADDING);
    let height = PADDING + rows as u32 * (CELL + LABEL_HEIGHT + PADDING);
    let mut backend = SvgPaintBackend::new(rust_widgets::core::Size::new(width, height));
    backend.begin_frame(backdrop);
    {
        let mut ctx = RenderContext::new(&mut backend);
        for (index, name) in IconName::ALL.into_iter().enumerate() {
            let column = (index % COLUMNS) as u32;
            let row = (index / COLUMNS) as u32;
            let x = PADDING + column * (CELL + PADDING);
            let y = PADDING + row * (CELL + LABEL_HEIGHT + PADDING);
            // The icon box is the top of the cell; the label sits under it.
            let mut icon = Icon::new(Rect::new(x as i32, y as i32, CELL, CELL));
            icon.set_icon_enum(name);
            icon.set_size(CELL as f32);
            icon.set_color(ink);
            icon.draw(&mut ctx);
            // The token, so a reader can see a name and its picture side by side. This is the
            // property the whole round is about: `close` and `cross` must be told apart by eye.
            ctx.draw_text(
                Point::new((x + CELL / 2) as i32, (y + CELL + 10) as i32),
                name.as_str(),
                &Font::simple("sans-serif", 8.0),
                ink,
                HorizontalAlignment::Center,
            );
        }
    }
    backend.end_frame();
    backend.finish()
}