Skip to main content

composition_demo/
composition_demo.rs

1//! Visual smoke test for the high-level composition layer with real text.
2//!
3//! Builds an outer patch wrapping a row of two inner patches, fills each
4//! anatomical slot with [`hephaestus::text::TextRun`] content (so the slots
5//! actually size themselves from font metrics), then renders every resolved
6//! rect plus the text into the rect.
7//!
8//! Writes `examples/composition_demo.png` next to this file.
9
10use hephaestus::backend::vello::VelloRenderer;
11use hephaestus::color::{rgb8, Color};
12use hephaestus::composition::{beside, Composition, Patch, Slot, Span};
13use hephaestus::layout::{Cell, Track};
14use hephaestus::stroke::Stroke;
15use hephaestus::text::{draw_text_in_rect, TextRun, TextStyle};
16use hephaestus::{Affine, Brush, FillRule, Path, PickId, Renderer, SceneBuilder};
17use kurbo::Shape;
18
19fn text_cell(text: &str, size: f32) -> Cell {
20    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
21}
22
23fn weighted_text_cell(text: &str, size: f32, weight: u16) -> Cell {
24    Cell::measured(TextRun::new(
25        text,
26        &TextStyle::new(size).weight(weight),
27        96.0,
28    ))
29}
30
31fn color_for_region(region: &str) -> Color {
32    match region {
33        "panel" => rgb8(40, 50, 70),
34        "title" => rgb8(220, 180, 90),
35        "subtitle" => rgb8(180, 150, 70),
36        "caption" => rgb8(180, 130, 90),
37        "axis_left" | "axis_right" | "axis_top" | "axis_bottom" => rgb8(160, 100, 130),
38        "axis_left_title" | "axis_right_title" | "axis_top_title" | "axis_bottom_title" => {
39            rgb8(200, 120, 160)
40        }
41        "strip_left" | "strip_right" | "strip_top" | "strip_bottom" => rgb8(90, 140, 180),
42        "legend_left" | "legend_right" | "legend_top" | "legend_bottom" => rgb8(110, 180, 140),
43        _ => rgb8(120, 120, 120),
44    }
45}
46
47/// Build an inner patch with axis labels and a panel-axis title. Each
48/// `TextRun` provides its own intrinsic width/height to the layout solver.
49fn build_inner_patch(id: &str, y_axis_text: &str, y_axis_title: &str, x_axis_title: &str) -> Patch {
50    Patch::new(id)
51        .slot(Slot::AxisLeft, text_cell(y_axis_text, 12.0))
52        .slot(
53            Slot::AxisLeftTitle,
54            weighted_text_cell(y_axis_title, 13.0, 500),
55        )
56        .slot(Slot::AxisBottom, text_cell("0  25  50  75  100", 12.0))
57        .slot(
58            Slot::AxisBottomTitle,
59            weighted_text_cell(x_axis_title, 13.0, 500),
60        )
61        .slot(Slot::Panel, Cell::empty())
62}
63
64fn main() {
65    let (w, h) = (1200u32, 700u32);
66    let dpi = 96.0;
67
68    let inner_a = build_inner_patch("plot_a", "0\n50\n100", "Pressure (kPa)", "Time (s)");
69    let inner_b = build_inner_patch(
70        "plot_b",
71        "0\n10000\n20000\n30000",
72        "Particle count",
73        "Time (s)",
74    );
75
76    // Header carries title + subtitle; footer carries caption. Both have no
77    // panel content — their composition rows are sized to chrome height only
78    // (Fr(0.0) panel weight so the middle row absorbs all leftover height).
79    let header = Patch::new("header")
80        .slot(
81            Slot::Title,
82            weighted_text_cell("Reactor diagnostics", 28.0, 700),
83        )
84        .slot(
85            Slot::Subtitle,
86            text_cell("Pressure and particle count over the morning run", 16.0),
87        );
88    let footer = Patch::new("footer").slot(
89        Slot::Caption,
90        text_cell("Source: in-line telemetry, smoothed at 1s intervals", 11.0),
91    );
92    let composed = Composition::empty(3, 1)
93        .heights(vec![Track::Fr(0.0), Track::Fr(1.0), Track::Fr(0.0)])
94        .place(1, 1, Span::cell(), header)
95        .place(2, 1, Span::cell(), beside(inner_a, inner_b))
96        .place(3, 1, Span::cell(), footer);
97
98    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
99
100    let mut renderer = VelloRenderer::new().expect("vello renderer init");
101    {
102        let scene = renderer.scene();
103        let stroke = Stroke::new(1.0);
104        let text_brush: Brush = rgb8(30, 30, 40).into();
105
106        // Background colour rectangles for chrome.
107        for (_id, region, rect) in layout.iter() {
108            if region == "panel" {
109                continue;
110            }
111            let color = color_for_region(region);
112            let tinted = Color::new([
113                color.components[0],
114                color.components[1],
115                color.components[2],
116                0.18,
117            ]);
118            let path: Path = rect.to_path(0.1);
119            scene.fill(
120                FillRule::NonZero,
121                Affine::IDENTITY,
122                &Brush::Solid(tinted),
123                None,
124                &path,
125                PickId::Skip,
126            );
127            scene.stroke(
128                &stroke,
129                Affine::IDENTITY,
130                &Brush::Solid(color),
131                None,
132                &path,
133                PickId::Skip,
134            );
135        }
136
137        // Panels on top.
138        for (_id, region, rect) in layout.iter() {
139            if region != "panel" {
140                continue;
141            }
142            let path: Path = rect.to_path(0.1);
143            scene.fill(
144                FillRule::NonZero,
145                Affine::IDENTITY,
146                &Brush::Solid(color_for_region(region)),
147                None,
148                &path,
149                PickId::Skip,
150            );
151            scene.stroke(
152                &stroke,
153                Affine::IDENTITY,
154                &Brush::Solid(rgb8(255, 255, 255)),
155                None,
156                &path,
157                PickId::Skip,
158            );
159        }
160
161        // Draw the actual text. We reach into the patches again to get a
162        // `TextRun` reference — the composition layout doesn't hand us the
163        // measure values back. For the demo, just rebuild a separate TextRun
164        // for each slot keyed by the same text so we can render it.
165        for (id, region, rect) in layout.iter() {
166            if region == "panel" {
167                continue;
168            }
169            let text = match (id, region) {
170                ("header", "title") => "Reactor diagnostics",
171                ("header", "subtitle") => "Pressure and particle count over the morning run",
172                ("footer", "caption") => "Source: in-line telemetry, smoothed at 1s intervals",
173                ("plot_a", "axis_left") => "0\n50\n100",
174                ("plot_a", "axis_left_title") => "Pressure (kPa)",
175                ("plot_a", "axis_bottom") => "0  25  50  75  100",
176                ("plot_a", "axis_bottom_title") => "Time (s)",
177                ("plot_b", "axis_left") => "0\n10000\n20000\n30000",
178                ("plot_b", "axis_left_title") => "Particle count",
179                ("plot_b", "axis_bottom") => "0  25  50  75  100",
180                ("plot_b", "axis_bottom_title") => "Time (s)",
181                _ => continue,
182            };
183            let size = match region {
184                "title" => 28.0,
185                "subtitle" => 16.0,
186                "caption" => 11.0,
187                "axis_left_title" | "axis_bottom_title" => 13.0,
188                _ => 12.0,
189            };
190            let weight = match region {
191                "title" => 700,
192                "axis_left_title" | "axis_bottom_title" => 500,
193                _ => 400,
194            };
195            let style = TextStyle::new(size).weight(weight);
196            let run = TextRun::new(text, &style, 96.0);
197            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
198        }
199    }
200
201    let mut pixels = vec![0u8; (w * h * 4) as usize];
202    let bg: Color = rgb8(248, 248, 252);
203    renderer
204        .render_to_buffer(w, h, bg, &mut pixels)
205        .expect("render");
206
207    let path = std::env::current_dir()
208        .unwrap()
209        .join("examples/composition_demo.png");
210    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
211    println!("wrote {}", path.display());
212
213    let panel_a = layout.get("plot_a", Slot::Panel).unwrap();
214    let panel_b = layout.get("plot_b", Slot::Panel).unwrap();
215    let title = layout.get("header", Slot::Title).unwrap();
216    println!("plot_a.panel = {:?}", panel_a);
217    println!("plot_b.panel = {:?}", panel_b);
218    println!("header.title = {:?}", title);
219}