Skip to main content

rich_text_marquee_parity/
rich_text_marquee_parity.rs

1//! Exercises every sheet selector in a single rendered block: headings
2//! `h1..h6`, `em` / `strong` / `del` / `code`, `sup` / `sub`, inline
3//! `link`, custom colour spans, fenced code block, blockquote, ordered
4//! and unordered lists (nested), a horizontal rule, and a fenced div.
5//!
6//! Renders one rich-text block directly (no plot) so the styling is the
7//! only thing to inspect. A dashed guide-box shows the wrap column.
8//!
9//! Writes `examples/rich_text_marquee_parity.png`.
10
11use hephaestus::backend::vello::VelloRenderer;
12use hephaestus::brush::Brush;
13use hephaestus::color::{rgb8, Color};
14use hephaestus::geometry::{Affine, Rect};
15use hephaestus::pick::PickId;
16use hephaestus::plot::theme::{HAlign, Palette, ThemeColor};
17use hephaestus::primitives::rect as rect_path;
18use hephaestus::scales::value::LinetypeStep;
19use hephaestus::stroke::Stroke;
20use hephaestus::text::rich::{
21    draw_rich_text, pt, relative, Direction, RichAnchor, RichMargin, RichTextRun,
22    RichTextStyleSheet, RichTextWidth, StyleDelta,
23};
24use hephaestus::text::{FontFeatureSetting, TextStyle};
25use hephaestus::{Renderer, SceneBuilder};
26use std::sync::Arc;
27
28const SOURCE: &str = "# Rich text at a glance
29
30A one-block tour of the marquee-flavoured styling vocabulary.
31
32## Inline formatting
33
34Plain text can carry **strong emphasis**, *soft emphasis*, _underline_,
35~~struck through~~ fragments, and inline `code` spans. Underscore emphasis
36underlines rather than slanting, matching marquee. Braces that are meant
37literally get escaped: \\{like so\\}, or doubled as {{like so}}. Named or hex
38colour spans work anywhere:
39{.crimson red}, {#3369e8 hex-blue}. Combine styles by **nesting**: {.royalblue *slanted*}.
40Superscript ^like this^ and subscript ~like this~ require whitespace around the
41outer markers (pulldown-cmark's grammar) — chemistry / physics notation with
42tightly-glued markers falls back to literal.
43
44Per-glyph outlines paint through the sheet's `text_stroke` field. A
45{.haloed HALOED} span sets a coloured outline behind its fill; combine it
46with a distinct `color` on the same class so parley splits the run at the
47span boundary (the outline scope is defined by the resulting glyph run).
48
49OpenType features are per-span too: {.smcp small caps here} route through
50the `smcp` feature via a sheet class; {.tnum 0123456789} pushes `tnum` for
51tabular numerals so digits share a fixed advance.
52
53## Lists
54
55- Unordered items get bullets from the sheet's cycle.
56- Second item, showing that tight lists stack tighter than loose lists.
57  - Nested items indent under their parent's continuation position.
58  - The nested marker cycles to `◦`.
59
60Ordered lists number themselves, and multi-digit ordinals right-align on
61their period because markers live in the list's start gutter rather than
62in the item's own text flow:
63
641. First step
652. Second step
663. Third step
674. Fourth
685. Fifth
696. Sixth
707. Seventh
718. Eighth
729. Ninth
7310. Tenth — note the right-aligned period
74
75## Blockquotes and code
76
77> Blockquote content indents past a left-edge bar. Multiple lines fit into the
78> wrapped column just like regular paragraphs.
79
80```rust
81fn code_block() {
82    println!(\"backgrounded and monospaced\");
83}
84```
85
86---
87
88A horizontal rule sits above this paragraph, drawn as the rule block's\nbottom-only border.
89
90:::note
91Fenced divs pick up a custom class; users can theme them via the sheet.
92:::
93
94## Custom indent + hanging + justification
95
96:::first-line-indent
97Sheet-defined class `.first-line-indent` sets a `Rel(2)` first-line
98indent — this paragraph's first line steps in by two em, while continuation
99lines start flush with the block's left. Try wrapping a few sentences at
100this width and see the effect apply to each paragraph inside the div.
101:::
102
103:::hanging-block
104Sheet-defined class `.hanging-block` sets a `Rel(2.5)` hanging indent —
105the first line stays flush at the left while every wrapped continuation
106line steps in. Handy for a definition-list-style layout where each
107paragraph's first word acts as the term.
108:::
109
110:::justified
111Sheet-defined class `.justified` sets `align: HAlign::Justify` — parley
112distributes trailing whitespace across each line so both the left and
113the right edge align. Effect is most visible on paragraphs that wrap
114across multiple lines with mixed word lengths.
115:::
116
117:::dashed-note
118`border_type` sets a dash pattern on any block border — this div has
119a dashed rectangle around it. Adjacent same-width sides share one
120polyline stroke so the corner is a single mitred join, not two abutting
121segments meeting at the same point.
122:::
123
124:::l-shape
125`.l-shape` sets top + left borders only. They collapse into one
126polyline through the top-left corner rather than rendering as two
127independent segments.
128:::
129
130:::stamped-note
131`border_type` also accepts `LinetypeStep::Marker` — small shape stamps
132spaced along the border. This div's pattern alternates dashes with
133`circle` marker stamps, walked by the same primitive `LineGeom` uses.
134:::
135
136:::rtl-quote
137> نص عربي مع اقتباس — the blockquote bar flips to the right edge under
138> `Direction::Rtl`, first-line indent applies from the right, and
139> `HAlign::Start` right-aligns text. Latin words inside still shape
140> left-to-right (parley's UBA is untouched).
141:::";
142
143fn main() {
144    let (w, h) = (960u32, 1600u32);
145    let dpi = 96.0;
146    let bg: Color = rgb8(252, 252, 254);
147    // Sheet with three custom div classes — each demonstrates a
148    // different block-level styling axis.
149    let mut sheet = RichTextStyleSheet::new();
150    sheet.set(
151        "first-line-indent",
152        StyleDelta {
153            indent: Some(relative(2.0)),
154            ..StyleDelta::empty()
155        },
156    );
157    sheet.set(
158        "hanging-block",
159        StyleDelta {
160            hanging: Some(relative(2.5)),
161            ..StyleDelta::empty()
162        },
163    );
164    sheet.set(
165        "justified",
166        StyleDelta {
167            align: Some(HAlign::Justify),
168            ..StyleDelta::empty()
169        },
170    );
171    // Bright fill + contrasting halo. `color` forces parley to split
172    // the run at the span's edges so the outline stays contained.
173    sheet.set(
174        "haloed",
175        StyleDelta {
176            weight: Some(700),
177            color: Some(ThemeColor::Fixed(rgb8(230, 60, 60))),
178            text_stroke: Some(ThemeColor::Fixed(rgb8(255, 235, 205))),
179            text_stroke_width: Some(pt(2.0)),
180            ..StyleDelta::empty()
181        },
182    );
183    sheet.set(
184        "smcp",
185        StyleDelta {
186            features: Some(vec![FontFeatureSetting {
187                tag: *b"smcp",
188                value: 1,
189            }]),
190            ..StyleDelta::empty()
191        },
192    );
193    sheet.set(
194        "tnum",
195        StyleDelta {
196            features: Some(vec![FontFeatureSetting {
197                tag: *b"tnum",
198                value: 1,
199            }]),
200            ..StyleDelta::empty()
201        },
202    );
203    sheet.set(
204        "dashed-note",
205        StyleDelta {
206            border_color: Some(ThemeColor::Fixed(rgb8(160, 90, 40))),
207            border_width: Some(RichMargin::all(pt(1.5))),
208            border_type: Some(Arc::from(vec![
209                LinetypeStep::Dash(6.0),
210                LinetypeStep::Gap(3.0),
211            ])),
212            border_radius: Some(pt(4.0)),
213            padding: Some(RichMargin::all(pt(8.0))),
214            margin: Some(RichMargin {
215                top: pt(6.0),
216                right: pt(0.0),
217                bottom: pt(6.0),
218                left: pt(0.0),
219            }),
220            ..StyleDelta::empty()
221        },
222    );
223    sheet.set(
224        "stamped-note",
225        StyleDelta {
226            border_color: Some(ThemeColor::Fixed(rgb8(80, 130, 90))),
227            border_width: Some(RichMargin::all(pt(1.0))),
228            border_type: Some(Arc::from(vec![
229                LinetypeStep::Dash(6.0),
230                LinetypeStep::Gap(3.0),
231                LinetypeStep::Marker(Arc::from("circle")),
232                LinetypeStep::Gap(3.0),
233            ])),
234            padding: Some(RichMargin::all(pt(8.0))),
235            margin: Some(RichMargin {
236                top: pt(6.0),
237                right: pt(0.0),
238                bottom: pt(6.0),
239                left: pt(0.0),
240            }),
241            ..StyleDelta::empty()
242        },
243    );
244    sheet.set(
245        "rtl-quote",
246        StyleDelta {
247            text_direction: Some(Direction::Rtl),
248            padding: Some(RichMargin::all(pt(6.0))),
249            margin: Some(RichMargin {
250                top: pt(6.0),
251                right: pt(0.0),
252                bottom: pt(6.0),
253                left: pt(0.0),
254            }),
255            ..StyleDelta::empty()
256        },
257    );
258    sheet.set(
259        "l-shape",
260        StyleDelta {
261            border_color: Some(ThemeColor::Fixed(rgb8(60, 100, 160))),
262            // Top + left only. Same width on both so they collapse
263            // into one polyline through the top-left corner.
264            border_width: Some(RichMargin {
265                top: pt(2.0),
266                right: pt(0.0),
267                bottom: pt(0.0),
268                left: pt(2.0),
269            }),
270            padding: Some(RichMargin::all(pt(8.0))),
271            margin: Some(RichMargin {
272                top: pt(6.0),
273                right: pt(0.0),
274                bottom: pt(6.0),
275                left: pt(0.0),
276            }),
277            ..StyleDelta::empty()
278        },
279    );
280    let palette = Palette::default();
281    let base_style = TextStyle::new(13.0);
282    let base_brush: Color = rgb8(24, 24, 30);
283    // Column width for wrapping. Leave a 40px gutter on each side.
284    let column = (w as f32) - 80.0;
285    let run = RichTextRun::new_with_width(
286        SOURCE,
287        &base_style,
288        base_brush,
289        &sheet,
290        &palette,
291        dpi,
292        RichTextWidth::Fixed(column),
293    );
294    let mut renderer = VelloRenderer::new().expect("vello renderer init");
295    {
296        let scene = renderer.scene();
297        scene.clear();
298        // Faint dashed guide box showing the column bounds.
299        let guide_rect = Rect::new(
300            40.0,
301            40.0,
302            40.0 + column as f64,
303            40.0 + run.current_height() + 8.0,
304        );
305        let guide_path = rect_path(guide_rect);
306        let guide_stroke = Stroke::new(1.0);
307        scene.stroke(
308            &guide_stroke,
309            Affine::IDENTITY,
310            &Brush::Solid(rgb8(220, 220, 230)),
311            None,
312            &guide_path,
313            PickId::Skip,
314        );
315        // The block itself.
316        draw_rich_text(
317            scene,
318            &run,
319            40.0,
320            48.0,
321            RichAnchor::top_left(),
322            Affine::IDENTITY,
323            PickId::Skip,
324        );
325    }
326    let mut pixels = vec![0u8; (w * h * 4) as usize];
327    renderer
328        .render_to_buffer(w, h, bg, &mut pixels)
329        .expect("render");
330    let path = std::env::current_dir()
331        .unwrap()
332        .join("examples/rich_text_marquee_parity.png");
333    hephaestus::image::write_png(&path, w, h, &pixels).expect("write png");
334    println!("wrote {}", path.display());
335}