Skip to main content

nesting_fixed_aspect/
nesting_fixed_aspect.rs

1//! Visual demo: fixed-aspect plots composed with deeply-nested flex
2//! plots, all sharing the same outer composition row.
3//!
4//! Layout: a 1×5 outer composition with `fixed_l` and `fixed_r` at the
5//! two ends (both `.aspect(1, 1)`), and three flex plots in the middle
6//! arranged in a 3-level-deep beside chain
7//! (`beside(flex_a, beside(flex_b, flex_c))`).
8//!
9//! ```text
10//!   ┌─────────┬─────────┬──────┬──────┬─────────┐
11//!   │ fixed_l │ flex_a  │flex_b│flex_c│ fixed_r │
12//!   └─────────┴─────────┴──────┴──────┴─────────┘
13//! ```
14//!
15//! Both fixed-aspect plots and all flex plots share the **same outer
16//! row**, so all five panel tops/bottoms align. Selective respect on
17//! the outer grid couples `fixed_l` and `fixed_r`'s panel cells at
18//! 1:1; the three flex panel columns (one direct child plus a 2-deep
19//! nested composition) absorb the horizontal slack at their respective
20//! nesting levels.
21//!
22//! Compare against `nesting_deep.png` — same horizontal layout style,
23//! but with two aspect-locked anchors and chrome on the locked panels.
24//!
25//! Note on alignment: fixed-aspect plots align with their siblings
26//! when they share the outermost composition row (this example).
27//! Putting one fixed plot at the outer level and another deep inside a
28//! nested composition currently produces different panel heights —
29//! the forward/back sizer chain couples chrome rows across nesting but
30//! not the panel row. Best practice for v1.5: keep fixed-aspect plots
31//! at the same composition level so they share the same panel row.
32//!
33//! Writes `examples/nesting_fixed_aspect.png`.
34
35use hephaestus::backend::vello::VelloRenderer;
36use hephaestus::color::{rgb8, Color};
37use hephaestus::composition::{beside, Patch, Slot};
38use hephaestus::layout::Cell;
39use hephaestus::text::{draw_text_in_rect, TextRun, TextStyle};
40use hephaestus::{Affine, Brush, FillRule, Path, PickId, Renderer, SceneBuilder};
41use kurbo::Shape;
42
43fn text_cell(text: &str, size: f32) -> Cell {
44    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
45}
46
47fn plain(id: &str) -> Patch {
48    Patch::new(id).slot(Slot::Panel, Cell::empty())
49}
50
51fn fixed_square(id: &str, label: &str) -> Patch {
52    // Chrome on a fixed patch is fine — the solver's second iteration
53    // picks up the resolved Auto-row heights from iter 0 and reshapes
54    // the respected fr distribution to honour the lock anyway. The
55    // axis_top here proves it: panels still report ratio = 1.000.
56    Patch::new(id)
57        .aspect(1.0, 1.0)
58        .slot(Slot::AxisTop, text_cell(label, 12.0))
59        .slot(Slot::Panel, Cell::empty())
60}
61
62fn color_for(id: &str, region: &str) -> Color {
63    match (id, region) {
64        (_, "panel") if id.starts_with("fixed") => rgb8(80, 140, 80), // green for locked
65        (_, "panel") => rgb8(40, 60, 90),                             // blue for flex
66        (_, "axis_top") => rgb8(200, 100, 130),
67        _ => rgb8(120, 120, 120),
68    }
69}
70
71fn main() {
72    // Wide viewport so the lock-vs-flex difference is obvious.
73    let (w, h) = (1600u32, 400u32);
74    let dpi = 96.0;
75
76    // Three flex plots in a 2-level beside chain — flex_a is at the
77    // outer level beside the deeper composition; flex_b and flex_c sit
78    // one level deeper.
79    let flex_chain = beside(plain("flex_a"), beside(plain("flex_b"), plain("flex_c")));
80    // Both fixed-aspect plots sit in the SAME outermost composition row
81    // as the flex chain. `Composition::beside` extends an existing 1-row
82    // composition by appending a cell — all five end up as direct
83    // siblings of the same outer grid, sharing one panel row.
84    let composed = beside(fixed_square("fixed_l", "1:1"), flex_chain)
85        .append_col(fixed_square("fixed_r", "1:1"));
86
87    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
88
89    let mut renderer = VelloRenderer::new().expect("vello renderer init");
90    {
91        let scene = renderer.scene();
92        let stroke = hephaestus::stroke::Stroke::new(1.0);
93        let text_brush: Brush = rgb8(20, 20, 30).into();
94
95        // Chrome first (axes / etc.).
96        for (id, region, rect) in layout.iter() {
97            if region == "panel" {
98                continue;
99            }
100            let c = color_for(id, region);
101            let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
102            let path: Path = rect.to_path(0.1);
103            scene.fill(
104                FillRule::NonZero,
105                Affine::IDENTITY,
106                &Brush::Solid(tint),
107                None,
108                &path,
109                PickId::Skip,
110            );
111            scene.stroke(
112                &stroke,
113                Affine::IDENTITY,
114                &Brush::Solid(c),
115                None,
116                &path,
117                PickId::Skip,
118            );
119        }
120        // Panels (with green for locked, blue for flex).
121        for (id, region, rect) in layout.iter() {
122            if region != "panel" {
123                continue;
124            }
125            let path: Path = rect.to_path(0.1);
126            scene.fill(
127                FillRule::NonZero,
128                Affine::IDENTITY,
129                &Brush::Solid(color_for(id, region)),
130                None,
131                &path,
132                PickId::Skip,
133            );
134            scene.stroke(
135                &stroke,
136                Affine::IDENTITY,
137                &Brush::Solid(rgb8(255, 255, 255)),
138                None,
139                &path,
140                PickId::Skip,
141            );
142        }
143        // Panel-centre labels noting locked vs flex + actual width:height.
144        for (id, region, rect) in layout.iter() {
145            if region != "panel" {
146                continue;
147            }
148            let w = rect.x1 - rect.x0;
149            let h = rect.y1 - rect.y0;
150            let label = if id.starts_with("fixed") {
151                format!("{id}\nlocked 1:1\n{w:.0}×{h:.0}")
152            } else {
153                format!("{id}\nflex\n{w:.0}×{h:.0}")
154            };
155            let run = TextRun::new(&label, &TextStyle::new(13.0).weight(500), 96.0);
156            let brush: Brush = rgb8(255, 255, 255).into();
157            draw_text_in_rect(scene, &run, rect, &brush, PickId::Skip);
158        }
159        // Axis-top labels on the two fixed patches.
160        for id in &["fixed_l", "fixed_r"] {
161            if let Some(rect) = layout.get(id, Slot::AxisTop) {
162                let run = TextRun::new(&format!("{id} 1:1"), &TextStyle::new(12.0), 96.0);
163                draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
164            }
165        }
166    }
167
168    let mut pixels = vec![0u8; (w * h * 4) as usize];
169    let bg: Color = rgb8(248, 248, 252);
170    renderer
171        .render_to_buffer(w, h, bg, &mut pixels)
172        .expect("render");
173
174    let path = std::env::current_dir()
175        .unwrap()
176        .join("examples/nesting_fixed_aspect.png");
177    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
178    println!("wrote {}", path.display());
179
180    // Print per-panel widths/heights so the aspect locks are verifiable
181    // from a terminal too. All five panels should report the same
182    // height (alignment across the outer row); fixed_l and fixed_r
183    // additionally have width == height (locked 1:1).
184    for id in &["fixed_l", "flex_a", "flex_b", "flex_c", "fixed_r"] {
185        let p = layout.get(id, Slot::Panel).unwrap();
186        let pw = p.x1 - p.x0;
187        let ph = p.y1 - p.y0;
188        let ratio = pw / ph;
189        let tag = if id.starts_with("fixed") {
190            "locked 1:1"
191        } else {
192            "flex"
193        };
194        println!("{id:>8} ({tag:>10}):  w={pw:>5.1}  h={ph:>5.1}  ratio={ratio:.3}");
195    }
196    println!("(all five panels share one outer row → all heights equal;");
197    println!(" fixed_l and fixed_r additionally report ratio ≈ 1.000.)");
198}