Skip to main content

nesting_chrome_coupling/
nesting_chrome_coupling.rs

1//! Visual stress test: bidirectional sizer coupling between an outer
2//! composition cell and a nested composition's inner border chrome.
3//!
4//! Layout: outer 1×2 composition.
5//! - Cell (1,1): a plain patch with NO axis_top.
6//! - Cell (1,2): a nested 1×3 composition where the MIDDLE inner patch
7//!   has a tall axis_top; the others don't.
8//!
9//! The outer-grid row band 1..16 is shared across BOTH outer blocks (they
10//! sit in the same composition row). With patchwork-style sizer coupling:
11//! 1. The sub-Grid's inner row 8 (AxisTop anatomical row) resolves to the
12//!    max of its inner cells — so it grows to fit the middle patch's tall
13//!    axis_top.
14//! 2. The forward sizer in the outer points at that resolved size; outer
15//!    row 8 grows to match.
16//! 3. The back sizer in the sub points at outer row 8; ALL inner blocks'
17//!    row-8 tracks grow to that same height.
18//!
19//! Visually: the plain patch's panel starts at the same y as all three
20//! inner panels, even though only one of four plots actually has an
21//! axis_top contributing content. The chrome row is empty on the plain
22//! side (white space above its panel) — that empty space is the
23//! propagated max chrome height.
24//!
25//! Writes `examples/nesting_chrome_coupling.png`.
26
27use hephaestus::backend::vello::VelloRenderer;
28use hephaestus::color::{rgb8, Color};
29use hephaestus::composition::{beside, grid, Patch, Slot};
30use hephaestus::layout::Cell;
31use hephaestus::text::{draw_text_in_rect, TextRun, TextStyle};
32use hephaestus::{Affine, Brush, FillRule, Path, PickId, Renderer, SceneBuilder};
33use kurbo::Shape;
34
35fn text_cell(text: &str, size: f32) -> Cell {
36    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
37}
38
39fn plain_plot(id: &str) -> Patch {
40    Patch::new(id).slot(Slot::Panel, Cell::empty())
41}
42
43fn plot_with_axis_top(id: &str, axis_top_text: &str) -> Patch {
44    Patch::new(id)
45        .slot(Slot::AxisTop, text_cell(axis_top_text, 14.0))
46        .slot(Slot::Panel, Cell::empty())
47}
48
49fn color_for_region(region: &str) -> Color {
50    match region {
51        "panel" => rgb8(40, 60, 90),
52        "axis_top" => rgb8(200, 100, 130),
53        _ => rgb8(120, 120, 120),
54    }
55}
56
57fn main() {
58    let (w, h) = (1400u32, 400u32);
59    let dpi = 96.0;
60
61    let plain = plain_plot("plain");
62    let nested = grid(
63        1,
64        3,
65        vec![
66            plain_plot("c1").into(),
67            plot_with_axis_top("c2", "this is a TALL axis_top label\nspanning multiple lines\nto stress chrome propagation").into(),
68            plain_plot("c3").into(),
69        ],
70    );
71    let composed = beside(plain, nested);
72    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
73
74    let mut renderer = VelloRenderer::new().expect("vello renderer init");
75    {
76        let scene = renderer.scene();
77        let stroke = hephaestus::stroke::Stroke::new(1.0);
78        let text_brush: Brush = rgb8(20, 20, 30).into();
79
80        for (_id, region, rect) in layout.iter() {
81            if region == "panel" {
82                continue;
83            }
84            let c = color_for_region(region);
85            let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
86            let path: Path = rect.to_path(0.1);
87            scene.fill(
88                FillRule::NonZero,
89                Affine::IDENTITY,
90                &Brush::Solid(tint),
91                None,
92                &path,
93                PickId::Skip,
94            );
95            scene.stroke(
96                &stroke,
97                Affine::IDENTITY,
98                &Brush::Solid(c),
99                None,
100                &path,
101                PickId::Skip,
102            );
103        }
104        for (_id, region, rect) in layout.iter() {
105            if region != "panel" {
106                continue;
107            }
108            let path: Path = rect.to_path(0.1);
109            scene.fill(
110                FillRule::NonZero,
111                Affine::IDENTITY,
112                &Brush::Solid(color_for_region(region)),
113                None,
114                &path,
115                PickId::Skip,
116            );
117            scene.stroke(
118                &stroke,
119                Affine::IDENTITY,
120                &Brush::Solid(rgb8(255, 255, 255)),
121                None,
122                &path,
123                PickId::Skip,
124            );
125        }
126        // Label the only chrome that has text content.
127        if let Some(rect) = layout.get("c2", Slot::AxisTop) {
128            let run = TextRun::new(
129                "this is a TALL axis_top label\nspanning multiple lines\nto stress chrome propagation",
130                &TextStyle::new(14.0),
131                96.0,
132            );
133            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
134        }
135    }
136
137    let mut pixels = vec![0u8; (w * h * 4) as usize];
138    let bg: Color = rgb8(248, 248, 252);
139    renderer
140        .render_to_buffer(w, h, bg, &mut pixels)
141        .expect("render");
142
143    let path = std::env::current_dir()
144        .unwrap()
145        .join("examples/nesting_chrome_coupling.png");
146    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
147    println!("wrote {}", path.display());
148
149    // Print the panel y-coordinates so the user can sanity-check that
150    // the plain panel starts at the same y as the inner panels even
151    // though it has no axis_top.
152    let plain_y = layout.get("plain", Slot::Panel).unwrap();
153    let c1_y = layout.get("c1", Slot::Panel).unwrap();
154    let c2_y = layout.get("c2", Slot::Panel).unwrap();
155    let c3_y = layout.get("c3", Slot::Panel).unwrap();
156    println!(
157        "panel y0: plain={}, c1={}, c2={}, c3={}",
158        plain_y.y0, c1_y.y0, c2_y.y0, c3_y.y0
159    );
160    println!("(all four should be equal — bidirectional sizer coupling at work)");
161}