rich_text_marquee_parity/
rich_text_marquee_parity.rs1use 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 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 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 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 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 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 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}