#![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;
const OUTPUT_DIR: &str = "snapshots/svg";
const GENERATED_MARKER: &str = "<!-- GENERATED by examples/export_control_svgs.rs -->";
const SETTLE_TICK_MS: u32 = 16;
const MAX_SETTLE_TICKS: usize = 64;
const EXTRA_APPEARANCES: &[(&str, &str, &str)] = &[
("group_box", "_checked", "checked"),
("frame", "_styled_panel", "styled_panel"),
("frame", "_win_panel", "win_panel"),
("frame", "_panel", "panel"),
("radio_button", "_checked", "checked"),
("toggle_button", "_checked", "checked"),
("tooltip", "_shown", "shown"),
];
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,
}
}
("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,
}
}
("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,
}
}
("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>> {
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();
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;
};
if rust_widgets::widget::sample_fill::apply(name, widget.as_mut()) {
filled += 1;
}
rust_widgets::theme::apply_theme_to_widget(widget.as_mut());
settle(widget.as_mut());
write_one(
dir,
&mut written,
name,
suffix,
"",
"",
widget.as_mut(),
appearance,
&mut failed,
)?;
}
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;
}
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,
)?;
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,
)?;
}
}
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(),
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(())
}
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);
}
}
#[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(());
};
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(())
}
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"
)
}
}
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
}