Skip to main content

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}