Skip to main content

TextRun

Struct TextRun 

Source
pub struct TextRun { /* private fields */ }
Expand description

Shaped text — built once, re-laid-out cheaply on width changes.

Implements Measure so it can be dropped into a crate::composition::Patch::slot via crate::layout::Cell::measured.

Implementations§

Source§

impl TextRun

Source

pub fn new(text: &str, style: &TextStyle, dpi: f64) -> Self

Shape text with style at dpi (typically 96 for screen output). The point-size on style is converted to pixels via size_px = size_pt * dpi / 72 before parley shapes the glyphs. Full shaping cost is paid here; later calls to Measure::height_at and draw_text only re-break lines.

Examples found in repository?
examples/composition_demo.rs (line 20)
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}
More examples
Hide additional examples
examples/nesting_asymmetric.rs (line 22)
21fn text_cell(text: &str, size: f32) -> Cell {
22    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
23}
24
25fn plot(id: &str, title: &str, axis_left: &str, axis_bottom: &str) -> Patch {
26    Patch::new(id)
27        .slot(Slot::Title, text_cell(title, 16.0))
28        .slot(Slot::AxisLeft, text_cell(axis_left, 11.0))
29        .slot(Slot::AxisBottom, text_cell(axis_bottom, 11.0))
30        .slot(Slot::Panel, Cell::empty())
31}
32
33fn color_for_region(region: &str) -> Color {
34    match region {
35        "panel" => rgb8(40, 60, 90),
36        "title" => rgb8(220, 180, 90),
37        "axis_left" | "axis_bottom" => rgb8(160, 100, 130),
38        _ => rgb8(120, 120, 120),
39    }
40}
41
42fn main() {
43    let (w, h) = (1200u32, 700u32);
44    let dpi = 96.0;
45
46    let row_three = grid(
47        1,
48        3,
49        vec![
50            plot("a1", "Series A1", "0\n10\n20", "0  5  10").into(),
51            plot("a2", "Series A2", "0\n50\n100", "0  5  10").into(),
52            plot("a3", "Series A3", "0\n500\n1000", "0  5  10").into(),
53        ],
54    );
55    let row_two = grid(
56        1,
57        2,
58        vec![
59            plot("b1", "Series B1 (wider)", "0\n100\n200\n300", "0   25   50").into(),
60            plot("b2", "Series B2 (wider)", "0\n5000\n10000", "0   25   50").into(),
61        ],
62    );
63    let composed = stack(row_three, row_two);
64    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
65
66    let mut renderer = VelloRenderer::new().expect("vello renderer init");
67    {
68        let scene = renderer.scene();
69        let stroke = hephaestus::stroke::Stroke::new(1.0);
70        let text_brush: Brush = rgb8(20, 20, 30).into();
71
72        // Chrome rects, tinted background.
73        for (_id, region, rect) in layout.iter() {
74            if region == "panel" {
75                continue;
76            }
77            let c = color_for_region(region);
78            let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
79            let path: Path = rect.to_path(0.1);
80            scene.fill(
81                FillRule::NonZero,
82                Affine::IDENTITY,
83                &Brush::Solid(tint),
84                None,
85                &path,
86                PickId::Skip,
87            );
88            scene.stroke(
89                &stroke,
90                Affine::IDENTITY,
91                &Brush::Solid(c),
92                None,
93                &path,
94                PickId::Skip,
95            );
96        }
97
98        // Panels solid.
99        for (_id, region, rect) in layout.iter() {
100            if region != "panel" {
101                continue;
102            }
103            let path: Path = rect.to_path(0.1);
104            scene.fill(
105                FillRule::NonZero,
106                Affine::IDENTITY,
107                &Brush::Solid(color_for_region(region)),
108                None,
109                &path,
110                PickId::Skip,
111            );
112            scene.stroke(
113                &stroke,
114                Affine::IDENTITY,
115                &Brush::Solid(rgb8(255, 255, 255)),
116                None,
117                &path,
118                PickId::Skip,
119            );
120        }
121
122        // Text labels in chrome.
123        for (id, region, rect) in layout.iter() {
124            if region == "panel" {
125                continue;
126            }
127            let text = match (id, region) {
128                ("a1", "title") => "Series A1",
129                ("a2", "title") => "Series A2",
130                ("a3", "title") => "Series A3",
131                ("b1", "title") => "Series B1 (wider)",
132                ("b2", "title") => "Series B2 (wider)",
133                ("a1", "axis_left") => "0\n10\n20",
134                ("a2", "axis_left") => "0\n50\n100",
135                ("a3", "axis_left") => "0\n500\n1000",
136                ("b1", "axis_left") => "0\n100\n200\n300",
137                ("b2", "axis_left") => "0\n5000\n10000",
138                (_, "axis_bottom") if id.starts_with('a') => "0  5  10",
139                (_, "axis_bottom") if id.starts_with('b') => "0   25   50",
140                _ => continue,
141            };
142            let size = if region == "title" { 16.0 } else { 11.0 };
143            let run = TextRun::new(text, &TextStyle::new(size), 96.0);
144            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
145        }
146    }
147
148    let mut pixels = vec![0u8; (w * h * 4) as usize];
149    let bg: Color = rgb8(248, 248, 252);
150    renderer
151        .render_to_buffer(w, h, bg, &mut pixels)
152        .expect("render");
153
154    let path = std::env::current_dir()
155        .unwrap()
156        .join("examples/nesting_asymmetric.png");
157    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
158    println!("wrote {}", path.display());
159}
examples/nesting_chrome_coupling.rs (line 36)
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}
examples/nesting_deep.rs (line 26)
25fn text_cell(text: &str, size: f32) -> Cell {
26    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
27}
28
29fn plain(id: &str) -> Patch {
30    Patch::new(id).slot(Slot::Panel, Cell::empty())
31}
32
33fn color_for_region(region: &str) -> Color {
34    match region {
35        "panel" => rgb8(40, 60, 90),
36        "axis_top" => rgb8(200, 100, 130),
37        "axis_bottom" => rgb8(160, 100, 130),
38        _ => rgb8(120, 120, 120),
39    }
40}
41
42fn main() {
43    let (w, h) = (1400u32, 400u32);
44    let dpi = 96.0;
45
46    // Deepest level: two plots, the first carries the chrome that needs
47    // to propagate all the way up to the root composition.
48    let leaf = beside(
49        Patch::new("leaf_l")
50            .slot(Slot::AxisTop, text_cell("axis_top from deepest leaf", 14.0))
51            .slot(Slot::AxisBottom, text_cell("axis_bottom from leaf", 11.0))
52            .slot(Slot::Panel, Cell::empty()),
53        plain("leaf_r"),
54    );
55    // Mid level: a plain plot beside the leaf composition.
56    let mid = beside(plain("mid"), leaf);
57    // Root level: a plain plot beside the mid composition.
58    let composed = beside(plain("root"), mid);
59
60    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
61
62    let mut renderer = VelloRenderer::new().expect("vello renderer init");
63    {
64        let scene = renderer.scene();
65        let stroke = hephaestus::stroke::Stroke::new(1.0);
66        let text_brush: Brush = rgb8(20, 20, 30).into();
67
68        for (_id, region, rect) in layout.iter() {
69            if region == "panel" {
70                continue;
71            }
72            let c = color_for_region(region);
73            let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
74            let path: Path = rect.to_path(0.1);
75            scene.fill(
76                FillRule::NonZero,
77                Affine::IDENTITY,
78                &Brush::Solid(tint),
79                None,
80                &path,
81                PickId::Skip,
82            );
83            scene.stroke(
84                &stroke,
85                Affine::IDENTITY,
86                &Brush::Solid(c),
87                None,
88                &path,
89                PickId::Skip,
90            );
91        }
92        for (_id, region, rect) in layout.iter() {
93            if region != "panel" {
94                continue;
95            }
96            let path: Path = rect.to_path(0.1);
97            scene.fill(
98                FillRule::NonZero,
99                Affine::IDENTITY,
100                &Brush::Solid(color_for_region(region)),
101                None,
102                &path,
103                PickId::Skip,
104            );
105            scene.stroke(
106                &stroke,
107                Affine::IDENTITY,
108                &Brush::Solid(rgb8(255, 255, 255)),
109                None,
110                &path,
111                PickId::Skip,
112            );
113        }
114
115        if let Some(rect) = layout.get("leaf_l", Slot::AxisTop) {
116            let run = TextRun::new("axis_top from deepest leaf", &TextStyle::new(14.0), 96.0);
117            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
118        }
119        if let Some(rect) = layout.get("leaf_l", Slot::AxisBottom) {
120            let run = TextRun::new("axis_bottom from leaf", &TextStyle::new(11.0), 96.0);
121            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
122        }
123    }
124
125    let mut pixels = vec![0u8; (w * h * 4) as usize];
126    let bg: Color = rgb8(248, 248, 252);
127    renderer
128        .render_to_buffer(w, h, bg, &mut pixels)
129        .expect("render");
130
131    let path = std::env::current_dir()
132        .unwrap()
133        .join("examples/nesting_deep.png");
134    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
135    println!("wrote {}", path.display());
136
137    // Print panel y0s so the user can verify alignment across all 3 nesting levels.
138    let root_panel = layout.get("root", Slot::Panel).unwrap();
139    let mid_panel = layout.get("mid", Slot::Panel).unwrap();
140    let leaf_l_panel = layout.get("leaf_l", Slot::Panel).unwrap();
141    let leaf_r_panel = layout.get("leaf_r", Slot::Panel).unwrap();
142    println!(
143        "panel y0: root={}, mid={}, leaf_l={}, leaf_r={}",
144        root_panel.y0, mid_panel.y0, leaf_l_panel.y0, leaf_r_panel.y0
145    );
146    println!("(all four should be equal — propagation across 3 nesting levels)");
147}
examples/nesting_faceted_title.rs (line 33)
32fn text_cell(text: &str, size: f32) -> Cell {
33    Cell::measured(TextRun::new(text, &TextStyle::new(size), 96.0))
34}
35
36fn weighted_text_cell(text: &str, size: f32, weight: u16) -> Cell {
37    Cell::measured(TextRun::new(
38        text,
39        &TextStyle::new(size).weight(weight),
40        96.0,
41    ))
42}
43
44fn facet(id: &str, axis_left: &str, axis_bottom: &str) -> Patch {
45    Patch::new(id)
46        .slot(Slot::AxisLeft, text_cell(axis_left, 11.0))
47        .slot(Slot::AxisBottom, text_cell(axis_bottom, 11.0))
48        .slot(Slot::Panel, Cell::empty())
49}
50
51fn color_for_region(region: &str) -> Color {
52    match region {
53        "panel" => rgb8(40, 60, 90),
54        "title" => rgb8(220, 180, 90),
55        "subtitle" => rgb8(180, 150, 70),
56        "caption" => rgb8(180, 130, 90),
57        "axis_left" | "axis_bottom" => rgb8(160, 100, 130),
58        _ => rgb8(120, 120, 120),
59    }
60}
61
62fn main() {
63    let (w, h) = (1400u32, 800u32);
64    let dpi = 96.0;
65
66    // A 2×3 grid of facets, each with its own axis chrome. The
67    // composition is then annotated with shared chrome via `.slot(...)`
68    // directly — no wrapper composition, no manual span calculations.
69    let mut facet_cells = Vec::new();
70    for r in 1..=2 {
71        for c in 1..=3 {
72            let id = format!("f{}_{}", r, c);
73            facet_cells.push(facet(&id, "0\n50\n100", "0  25  50").into());
74        }
75    }
76    let composed = grid(2, 3, facet_cells)
77        .id("plot")
78        .slot(
79            Slot::Title,
80            weighted_text_cell("Reactor diagnostics across runs", 26.0, 700),
81        )
82        .slot(
83            Slot::Subtitle,
84            text_cell(
85                "Each panel: a separate 60-minute run; same scales throughout",
86                15.0,
87            ),
88        )
89        .slot(
90            Slot::AxisLeftTitle,
91            weighted_text_cell("Pressure (kPa)", 14.0, 500),
92        )
93        .slot(
94            Slot::AxisBottomTitle,
95            weighted_text_cell("Elapsed time (minutes)", 14.0, 500),
96        )
97        .slot(
98            Slot::Caption,
99            text_cell(
100                "Source: in-line telemetry, smoothed at 1s; runs ordered chronologically.",
101                11.0,
102            ),
103        );
104
105    let layout = composed.solve(hephaestus::Size::new(w as f64, h as f64), dpi);
106
107    let mut renderer = VelloRenderer::new().expect("vello renderer init");
108    {
109        let scene = renderer.scene();
110        let stroke = hephaestus::stroke::Stroke::new(1.0);
111        let text_brush: Brush = rgb8(20, 20, 30).into();
112
113        for (_id, region, rect) in layout.iter() {
114            if region == "panel" {
115                continue;
116            }
117            let c = color_for_region(region);
118            let tint = Color::new([c.components[0], c.components[1], c.components[2], 0.20]);
119            let path: Path = rect.to_path(0.1);
120            scene.fill(
121                FillRule::NonZero,
122                Affine::IDENTITY,
123                &Brush::Solid(tint),
124                None,
125                &path,
126                PickId::Skip,
127            );
128            scene.stroke(
129                &stroke,
130                Affine::IDENTITY,
131                &Brush::Solid(c),
132                None,
133                &path,
134                PickId::Skip,
135            );
136        }
137        for (_id, region, rect) in layout.iter() {
138            if region != "panel" {
139                continue;
140            }
141            let path: Path = rect.to_path(0.1);
142            scene.fill(
143                FillRule::NonZero,
144                Affine::IDENTITY,
145                &Brush::Solid(color_for_region(region)),
146                None,
147                &path,
148                PickId::Skip,
149            );
150            scene.stroke(
151                &stroke,
152                Affine::IDENTITY,
153                &Brush::Solid(rgb8(255, 255, 255)),
154                None,
155                &path,
156                PickId::Skip,
157            );
158        }
159
160        for (id, region, rect) in layout.iter() {
161            let (text, size, weight) = match (id, region) {
162                ("plot", "title") => ("Reactor diagnostics across runs", 26.0, 700),
163                ("plot", "subtitle") => (
164                    "Each panel: a separate 60-minute run; same scales throughout",
165                    15.0,
166                    400,
167                ),
168                ("plot", "caption") => (
169                    "Source: in-line telemetry, smoothed at 1s; runs ordered chronologically.",
170                    11.0,
171                    400,
172                ),
173                ("plot", "axis_left_title") => ("Pressure (kPa)", 14.0, 500),
174                ("plot", "axis_bottom_title") => ("Elapsed time (minutes)", 14.0, 500),
175                (_, "axis_left") => ("0\n50\n100", 11.0, 400),
176                (_, "axis_bottom") => ("0  25  50", 11.0, 400),
177                _ => continue,
178            };
179            let run = TextRun::new(text, &TextStyle::new(size).weight(weight), 96.0);
180            draw_text_in_rect(scene, &run, rect, &text_brush, PickId::Skip);
181        }
182    }
183
184    let mut pixels = vec![0u8; (w * h * 4) as usize];
185    let bg: Color = rgb8(248, 248, 252);
186    renderer
187        .render_to_buffer(w, h, bg, &mut pixels)
188        .expect("render");
189
190    let path = std::env::current_dir()
191        .unwrap()
192        .join("examples/nesting_faceted_title.png");
193    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
194    println!("wrote {}", path.display());
195
196    // Sanity check: chrome rects line up with the facet panel band.
197    let title = layout.get("plot", Slot::Title).unwrap();
198    let caption = layout.get("plot", Slot::Caption).unwrap();
199    let axis_left_title = layout.get("plot", Slot::AxisLeftTitle).unwrap();
200    let axis_bottom_title = layout.get("plot", Slot::AxisBottomTitle).unwrap();
201    let f1_1_panel = layout.get("f1_1", Slot::Panel).unwrap();
202    let f1_3_panel = layout.get("f1_3", Slot::Panel).unwrap();
203    let f2_3_panel = layout.get("f2_3", Slot::Panel).unwrap();
204    println!(
205        "plot.title:             x0={:>5.0}  x1={:>5.0}  y0={:>5.0}  y1={:>5.0}",
206        title.x0, title.x1, title.y0, title.y1
207    );
208    println!(
209        "plot.caption:           x0={:>5.0}  x1={:>5.0}  y0={:>5.0}  y1={:>5.0}",
210        caption.x0, caption.x1, caption.y0, caption.y1
211    );
212    println!(
213        "plot.axis_left_title:   x0={:>5.0}  x1={:>5.0}  y0={:>5.0}  y1={:>5.0}",
214        axis_left_title.x0, axis_left_title.x1, axis_left_title.y0, axis_left_title.y1
215    );
216    println!(
217        "plot.axis_bottom_title: x0={:>5.0}  x1={:>5.0}  y0={:>5.0}  y1={:>5.0}",
218        axis_bottom_title.x0, axis_bottom_title.x1, axis_bottom_title.y0, axis_bottom_title.y1
219    );
220    println!(
221        "facets panel band:      x0={:>5.0}  x1={:>5.0}  y0={:>5.0}  y1={:>5.0}",
222        f1_1_panel.x0, f1_3_panel.x1, f1_1_panel.y0, f2_3_panel.y1
223    );
224}
examples/nesting_fixed_aspect.rs (line 44)
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}
Source

pub fn set_max_width(&self, max_width: f32, alignment: HAlign) -> f32

Re-break lines at max_width pixels, applying alignment to the resulting layout. Equivalent to Measure::height_at(max_width, _) but exposed for callers that want to draw without first running through a composition solve.

alignment controls justification within the wrap box: HAlign::Start leaves every line flush against the leading edge; Center / End / Justify apply the matching direction-aware alignment.

Source

pub fn natural_width(&self) -> f64

Natural unwrapped content width in pixels — the width the text would occupy if laid out on a single line per paragraph break in the source. Computed once at construction; stable regardless of subsequent Self::set_max_width calls. Used by label-style geoms to anchor the text against its intrinsic dimensions.

Source

pub fn natural_height(&self) -> f64

Natural unwrapped content height in pixels. Stable across Self::set_max_width calls.

Source

pub fn current_height(&self) -> f64

Current laid-out height in pixels — reflects the most recent Self::set_max_width / Measure::height_at call. Equals Self::natural_height when no wrap has been requested.

Source

pub fn content_width(&self) -> f64

Actual rendered content width in pixels — the widest line in the current layout. Reflects the most recent line-break, so when Self::set_max_width has been called the result is the actual wrapped width (usually less than the constraint, since parley breaks at word boundaries). When no wrap has been requested the layout is single-line and this matches Self::natural_width.

Source

pub fn baseline_offset(&self) -> f64

Offset from the layout’s top edge to the baseline of the first line, in pixels. Differs from the font’s ascent metric when the resolved line height includes leading — the baseline sits below the typographic ascent by half the leading. Used by chrome labels to convert between baseline-anchored and top-anchored positioning.

Source

pub fn cap_height(&self) -> f64

Cap-height of the first run, in pixels — distance from the baseline to the top of capital letters. Falls back to x_height (and then 0.7 × ascent as a last resort) when the font doesn’t report cap-height. Used by axis / legend label centering: a numeric or uppercase label centered on cap_height looks visually balanced against its tick or swatch, whereas centering on the full natural_height reserves descender space the glyphs don’t occupy and shifts the visual centre off-target.

Source

pub fn last_line_descender(&self) -> f64

Font descender of the last line in the current layout, in pixels. Used by background-rect geoms to apply the ggplot2 geom_label-style padding rebalance — bump top padding up to at least the descender and reduce bottom padding by the same — so visible glyphs centre vertically in the rect even when the last line has no descenders (“men” vs “jay”).

Source

pub fn first_line_ascender_offset(&self) -> f64

Y position of the first line’s ascender top, relative to the layout’s top edge. Equivalent to the top half-leading on the first line — the empty pixels above the visible glyphs that the line-box reserves on its way to line-height.

Source

pub fn last_line_descender_offset_from_top(&self) -> f64

Y position of the last line’s descender bottom, relative to the layout’s top edge. Equivalent to current_height - bottom half-leading on the last line.

Source

pub fn inked_height(&self) -> f64

Inked height of the current layout — from the first line’s ascender top to the last line’s descender bottom, with leading appearing only between lines (not above the first or below the last). The natural text box.

Trait Implementations§

Source§

impl Measure for TextRun

Source§

fn width_hint(&self, _dpi: f64) -> WidthHint

Report this leaf’s intrinsic width — either a stable minimum (WidthHint::Min) or a height-dependent value that opts the leaf into iteration (WidthHint::NeedsHeight).
Source§

fn height_at(&self, width: f64, _dpi: f64) -> f64

Report this leaf’s intrinsic height when allocated width pixels.
Source§

fn width_at(&self, _height: f64, _dpi: f64) -> f64

Report a width given a resolved height. Consulted only during iteration for cells that returned WidthHint::NeedsHeight. Default 0.0 is correct for content that uses WidthHint::Min.

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> ErasedDestructor for T
where T: 'static,

Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T, S> SimdFrom<T, S> for T
where S: Simd,

Source§

fn simd_from(_simd: S, value: T) -> T

Source§

impl<F, T, S> SimdInto<T, S> for F
where T: SimdFrom<F, S>, S: Simd,

Source§

fn simd_into(self, simd: S) -> T

Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<S, T> Upcast<T> for S
where T: UpcastFrom<S> + ?Sized, S: ?Sized,

Source§

fn upcast(&self) -> &T
where Self: ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider ref type within the Wasm bindgen generics type system. Read more
Source§

fn upcast_into(self) -> T
where Self: Sized + ErasableGeneric, T: Sized + ErasableGeneric<Repr = Self::Repr>,

Perform a zero-cost type-safe upcast to a wider type within the Wasm bindgen generics type system. Read more
Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more