toast/toast.rs
1//! toast — bundle-dump demo of the runtime-managed toast stack.
2//!
3//! Apps push `ToastSpec`s by accumulating them in their state and
4//! returning them from `App::drain_toasts`. The runtime stamps each
5//! with a monotonic id + an `expires_at` deadline, queues them on
6//! `UiState::toasts`, and synthesizes a `Kind::Custom("toast_stack")`
7//! floating layer at the El root each frame. The stack is bottom-right
8//! anchored; each card carries a level-coloured leading bar, the
9//! message, and a `toast-dismiss-{id}` button the runtime intercepts.
10//!
11//! This headless example seeds four toasts directly via
12//! `UiState::push_toast` (skipping the App wiring), then runs the
13//! same `synthesize_toasts` + `layout` + `draw_ops` path the live
14//! runner uses. Inspect the bundle artifacts to see:
15//!
16//! - `out/toast.tree.txt` — `toast_stack` layer with one `toast_card`
17//! child per active toast.
18//! - `out/toast.draw_ops.txt` — surface quad + text glyph runs for
19//! each card and the trailing `toast-dismiss-{id}` button.
20//!
21//! Run: `cargo run -p damascene-core --example toast`
22
23use std::time::Duration;
24// `web_time::Instant` is what `push_toast` expects — a re-export of
25// `std::time::Instant` on native, a working clock on wasm.
26use web_time::Instant;
27
28use damascene_core::layout::assign_ids;
29use damascene_core::prelude::*;
30use damascene_core::state::UiState;
31use damascene_core::toast::synthesize_toasts;
32
33fn fixture() -> El {
34 // Apps wrap their main view in `overlays(main, [])` so the
35 // runtime can append the synthesized toast layer as an overlay
36 // sibling — same convention as for popovers and modals.
37 overlays(
38 column([
39 h2("Toasts"),
40 paragraph(
41 "Apps queue toasts by returning ToastSpec values from \
42 App::drain_toasts. The runtime stamps each with a TTL, \
43 stacks them at the bottom-right corner, and dismisses \
44 them on click or auto-expiry.",
45 )
46 .muted(),
47 row([
48 button("Save changes").key("save"),
49 button("Trigger error").key("err"),
50 button("Show info").key("info"),
51 ])
52 .gap(tokens::SPACE_2),
53 ])
54 .gap(tokens::SPACE_4)
55 .padding(tokens::SPACE_7)
56 .width(Size::Fill(1.0))
57 .height(Size::Fill(1.0)),
58 [],
59 )
60}
61
62fn main() -> std::io::Result<()> {
63 let viewport = Rect::new(0.0, 0.0, 720.0, 360.0);
64 // Seed the runtime's toast queue directly so the bundle dump
65 // shows the synthesized layer. In a live app the host calls
66 // `runner.push_toasts(app.drain_toasts())` once per frame.
67 let mut state = UiState::new();
68 let now = Instant::now();
69 let long_ttl = Duration::from_secs(60);
70 state.push_toast(ToastSpec::success("Settings saved").with_ttl(long_ttl), now);
71 state.push_toast(
72 ToastSpec::warning("Battery low — connect charger").with_ttl(long_ttl),
73 now,
74 );
75 state.push_toast(
76 ToastSpec::error("Failed to reach update server").with_ttl(long_ttl),
77 now,
78 );
79 state.push_toast(
80 ToastSpec::info("New version available").with_ttl(long_ttl),
81 now,
82 );
83
84 let mut tree = fixture();
85 assign_ids(&mut tree);
86 let _ = synthesize_toasts(&mut tree, &mut state, now);
87 let bundle = render_bundle_with(&mut tree, &mut state, viewport);
88
89 let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("out");
90 let written = write_bundle(&bundle, &out_dir, "toast")?;
91 for p in &written {
92 println!("wrote {}", p.display());
93 }
94
95 if !bundle.lint.findings.is_empty() {
96 eprintln!("\nlint findings ({}):", bundle.lint.findings.len());
97 eprint!("{}", bundle.lint.text());
98 }
99 Ok(())
100}