review/review.rs
1//! review — the headless review loop, as a copy-paste scaffold.
2//!
3//! Damascene apps don't need a GPU or a window to be looked at. Any
4//! [`App`] builds through layout, paint, and the lint pass headlessly,
5//! producing an SVG you can open and a lint report you can read. That is
6//! the cheapest way to catch the layout and styling slips that otherwise
7//! only show up at paint — raw colors, text overflow, missing surface
8//! fills, focus rings clipped by a scroll viewport, duplicate ids.
9//!
10//! Treat it the way you'd treat a compiler warning: render after each
11//! change, open the SVG, and keep the lint clean. Run it here with
12//!
13//! ```text
14//! cargo run -p damascene-core --example review
15//! ```
16//!
17//! and it writes `out/review.{svg,tree.txt,draw_ops.txt,lint.txt}` next
18//! to the crate, prints where, then prints the lint report. It exits
19//! non-zero if the lint finds anything, so the same `main` works as a
20//! CI gate.
21//!
22//! ## Copy this into your own crate
23//!
24//! Drop a near-identical file under your crate's `examples/` (or fold
25//! the body into a `#[test]` and `assert!(bundle.lint.findings
26//! .is_empty(), "{}", bundle.lint.text())`). Swap `ReviewDemo` for your
27//! own [`App`] and point it at the canned states worth reviewing —
28//! empty states, long lists, error states. The whole loop is the three
29//! calls in `main`: build the tree, [`render_bundle_themed`], then read
30//! `bundle.lint`.
31
32use damascene_core::prelude::*;
33
34/// A small, idiomatic settings panel — enough of a real tree for the
35/// lint pass to have something to say. Note the named widgets (`card`,
36/// `field_row`, `switch`, `badge`) over hand-styled `column`/`row`, and
37/// the explicit `gap` on the content column so stacked interactive rows
38/// don't overlap their focus rings.
39struct ReviewDemo {
40 two_factor: bool,
41 weekly_digest: bool,
42}
43
44impl App for ReviewDemo {
45 fn build(&self, _cx: &BuildCx) -> El {
46 let account = card([
47 card_header([card_title("Account")]),
48 card_content([
49 field_row(
50 "Two-factor authentication",
51 row([
52 if self.two_factor {
53 badge("Enabled").success()
54 } else {
55 badge("Off").muted()
56 },
57 switch("two_factor", self.two_factor),
58 ])
59 .gap(tokens::SPACE_3)
60 .align(Align::Center)
61 .width(Size::Hug),
62 ),
63 separator(),
64 field_row(
65 "Weekly email digest",
66 switch("weekly_digest", self.weekly_digest),
67 ),
68 ])
69 .gap(tokens::SPACE_3),
70 ])
71 .width(Size::Fixed(420.0));
72
73 let body = column([
74 column([
75 h1("Settings"),
76 text("Headless review fixture — open out/review.svg to see it.").muted(),
77 ])
78 .gap(tokens::SPACE_1)
79 .width(Size::Hug),
80 account,
81 ])
82 .gap(tokens::SPACE_5)
83 .width(Size::Hug);
84
85 page([row([spacer(), body, spacer()]).width(Size::Fill(1.0))])
86 }
87
88 fn on_event(&mut self, event: UiEvent, _cx: &EventCx) {
89 switch::apply_event(&mut self.two_factor, &event, "two_factor");
90 switch::apply_event(&mut self.weekly_digest, &event, "weekly_digest");
91 }
92}
93
94fn main() -> std::io::Result<()> {
95 let app = ReviewDemo {
96 two_factor: true,
97 weekly_digest: false,
98 };
99
100 // The three-call loop. `theme()` defaults to `Theme::default()`;
101 // override `App::theme` to review a different palette.
102 let theme = app.theme();
103 let viewport = Rect::new(0.0, 0.0, 720.0, 540.0);
104 let mut root = app.build(&BuildCx::new(&theme).with_viewport(viewport.w, viewport.h));
105 let bundle = render_bundle_themed(&mut root, viewport, &theme);
106
107 // Write the artifacts to look at.
108 let out_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("out");
109 let written = write_bundle(&bundle, &out_dir, "review")?;
110 for p in &written {
111 println!("wrote {}", p.display());
112 }
113
114 // Read the lint. Anything here is a layout/styling slip worth fixing
115 // before the UI is trusted — so we surface it and fail the run.
116 if bundle.lint.findings.is_empty() {
117 println!("\nlint: clean");
118 } else {
119 eprintln!("\nlint findings ({}):", bundle.lint.findings.len());
120 eprint!("{}", bundle.lint.text());
121 std::process::exit(1);
122 }
123
124 Ok(())
125}