Skip to main content

devpage_design/
lib.rs

1//! The design engine: parses `.design` files, resolves them into scenes, lays
2//! them out and paints them. It does no I/O, so the same code runs natively
3//! in devpage and compiled to WebAssembly in the browser.
4//!
5//! **The core** links no rasterizer: text → syntax tree ([`parse`]) → scene
6//! ([`resolve`], [`fonts`]) → layout, the painter's commands and a kept,
7//! updatable drawing ([`render`]) → chunks under property trees with their
8//! index, the one paint entry point and hit testing ([`display`]) → tiles,
9//! their cache and the damage that makes them stale ([`tiles`]); with the
10//! shadows' sampling ([`shadow`]), the GPU atlas's model ([`atlas`]), the
11//! wire form of scenes and patches ([`wire`], [`proto`]) and the rule two
12//! drawings are compared by ([`compare`]). Its geometry and colours are
13//! kurbo's and peniko's, depended on directly.
14//!
15//! **Adapters** plug into its one port, [`display::Target`] — paint this area
16//! at this scale: [`cpu`], `vello_cpu`; the module's `vello` on WebGPU. And
17//! [`bands`] writes the PNG band by band from whatever gives it tiles. The
18//! core names no adapter; a test holds it to that.
19
20// The core.
21pub mod atlas;
22pub mod compare;
23pub mod display;
24pub mod fonts;
25pub mod ids;
26pub mod parse;
27pub mod proto;
28pub mod render;
29pub mod resolve;
30pub mod shadow;
31pub mod sums;
32pub mod tiles;
33pub mod tree;
34pub mod wire;
35
36// Adapters.
37pub mod bands;
38pub mod cpu;
39
40use std::fmt;
41
42pub use kurbo::Rect as KRect;
43pub use render::{Damage, Drawing, Renderer};
44pub use resolve::{Files, Scene, Theme, resolve};
45
46/// Something wrong with a design, at a place in one of its files.
47#[derive(Debug, Clone, PartialEq)]
48pub struct Problem {
49    pub file: String,
50    pub pos: parse::Pos,
51    pub message: String,
52}
53
54impl fmt::Display for Problem {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        write!(
57            f,
58            "{}:{}:{}: {}",
59            self.file, self.pos.line, self.pos.col, self.message
60        )
61    }
62}
63
64/// Paints a scene's whole drawing at 1× in `theme` and encodes it as a PNG,
65/// band by band, or says why it can't: see [`Renderer::drawing`]. A drawing
66/// made afresh and kept by nothing: the server's PNG comes from its kept
67/// drawings and tiles instead. This is the reference the engine's, the
68/// module's and the canvas's tests compare a drawing against — one call,
69/// from a scene, with nothing kept.
70pub fn png(renderer: &mut Renderer, scene: &Scene, theme: Theme) -> Result<Vec<u8>, String> {
71    let drawing = renderer
72        .drawing(scene, theme, None)
73        .map_err(|e| e.message)?;
74    let mut png = Vec::new();
75    bands::png(&drawing.list, &mut png).map_err(|e| format!("encoding the PNG: {e}"))?;
76    Ok(png)
77}
78
79// SAFETY: a drawing isn't `Send` only because taffy's `CompactLength`, in
80// the layout tree it keeps, packs a length into a `*const ()`. That value is
81// a pointer only for a `calc()` length, behind taffy's `calc` feature, which
82// the engine neither enables nor makes — every length it gives taffy is a
83// number or a keyword, bits in the word — so nothing in a drawing points
84// anywhere, and a server can keep one behind a lock and paint it from any
85// thread.
86unsafe impl Send for render::Drawing {}
87
88/// The core names no adapter and links no rasterizer: no module of it
89/// mentions `vello_cpu`, `vello`, `wgpu`, the PNG encoder, or [`cpu`] and
90/// [`bands`] — outside its tests. Dependencies point inward.
91#[cfg(test)]
92#[test]
93fn the_core_names_no_adapter() {
94    let src = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
95    let adapters = ["lib.rs", "cpu", "bands"];
96    let forbidden = [
97        "vello_cpu",
98        "vello::",
99        "wgpu",
100        "png::Encoder",
101        "png::Stream",
102        "crate::cpu",
103        "crate::bands",
104    ];
105    let mut stack = vec![src.clone()];
106    while let Some(dir) = stack.pop() {
107        for entry in std::fs::read_dir(&dir).unwrap() {
108            let path = entry.unwrap().path();
109            let rel = path.strip_prefix(&src).unwrap();
110            let top = rel
111                .components()
112                .next()
113                .unwrap()
114                .as_os_str()
115                .to_string_lossy();
116            let name = path.file_name().unwrap().to_string_lossy().into_owned();
117            if adapters.contains(&top.trim_end_matches(".rs"))
118                || adapters.contains(&top.as_ref())
119                || name == "tests.rs"
120                || name == "testing.rs"
121                || name == "examples.rs"
122            {
123                continue;
124            }
125            if path.is_dir() {
126                stack.push(path);
127                continue;
128            }
129            let text = std::fs::read_to_string(&path).unwrap();
130            // A module's own unit tests sit at its end.
131            let text = text.split("#[cfg(test)]\nmod tests {").next().unwrap();
132            for word in forbidden {
133                assert!(!text.contains(word), "{} names {word}", rel.display());
134            }
135        }
136    }
137}