Skip to main content

text_typeset/layout/
block.rs

1use crate::font::registry::FontRegistry;
2use crate::font::resolve::{ResolvedFont, resolve_font};
3use crate::layout::line::LayoutLine;
4use crate::layout::paragraph::{Alignment, Hyphenator, break_into_lines};
5use crate::shaping::run::{ShapedGlyph, ShapedRun};
6use crate::shaping::shaper::{
7    FontMetricsPx, TextDirection, font_metrics_px, shape_text, shape_text_with_fallback,
8    to_harfrust_features,
9};
10
11/// Computed layout for a single block (paragraph).
12#[derive(Clone)]
13pub struct BlockLayout {
14    pub block_id: usize,
15    /// Document character position of the block start.
16    pub position: usize,
17    /// Laid out lines within the block.
18    pub lines: Vec<LayoutLine>,
19    /// Top edge relative to document start (set by flow layout).
20    pub y: f32,
21    /// Total height: top_margin + sum(line heights) + bottom_margin.
22    pub height: f32,
23    pub top_margin: f32,
24    pub bottom_margin: f32,
25    pub left_margin: f32,
26    pub right_margin: f32,
27    /// Shaped list marker (positioned to the left of the content area).
28    /// None if the block is not a list item.
29    pub list_marker: Option<ShapedListMarker>,
30    /// Block background color (RGBA). None means transparent.
31    pub background_color: Option<[f32; 4]>,
32}
33
34/// A shaped list marker ready for rendering.
35#[derive(Clone)]
36pub struct ShapedListMarker {
37    pub run: ShapedRun,
38    /// X position of the marker (relative to block left edge, before content indent).
39    pub x: f32,
40}
41
42/// Parameters extracted from text-document's BlockFormat / TextFormat.
43/// This is a plain struct so block layout doesn't depend on text-document types.
44#[derive(Clone)]
45pub struct BlockLayoutParams {
46    pub block_id: usize,
47    pub position: usize,
48    pub text: String,
49    pub fragments: Vec<FragmentParams>,
50    pub alignment: Alignment,
51    pub top_margin: f32,
52    pub bottom_margin: f32,
53    pub left_margin: f32,
54    pub right_margin: f32,
55    pub text_indent: f32,
56    /// List marker text (e.g., "1.", "•", "a)"). Empty if not a list item.
57    pub list_marker: String,
58    /// Additional left indent for list items (in pixels).
59    pub list_indent: f32,
60    /// Tab stop positions in pixels from the left margin.
61    pub tab_positions: Vec<f32>,
62    /// Line height multiplier. 1.0 = normal (from font metrics), 1.5 = 150%, 2.0 = double.
63    /// None means use font metrics (ascent + descent + leading).
64    pub line_height_multiplier: Option<f32>,
65    /// If true, prevent line wrapping. The entire block is one long line.
66    pub non_breakable_lines: bool,
67    /// Hyphenation during line wrapping (`None` = off). See
68    /// [`crate::types::Hyphenation`].
69    pub hyphenation: Option<crate::types::Hyphenation>,
70    /// Checkbox marker: None = no checkbox, Some(false) = unchecked, Some(true) = checked.
71    pub checkbox: Option<bool>,
72    /// Block background color (RGBA). None means transparent.
73    pub background_color: Option<[f32; 4]>,
74}
75
76/// A text fragment with its formatting parameters.
77#[derive(Clone)]
78pub struct FragmentParams {
79    pub text: String,
80    /// **Byte** offset of this fragment's first character inside the
81    /// owning block's text. Lifted into glyph clusters by
82    /// `paragraph::flatten_runs` so glyph clusters
83    /// can be compared directly against `unicode-linebreak` break
84    /// positions (also bytes) and against the block-level text used
85    /// for `byte_offset_to_char_offset` conversion. Hosts threading
86    /// text-document `FragmentContent` through the bridge must
87    /// translate the char-based `FragmentContent::offset` into bytes
88    /// before assigning here.
89    pub offset: usize,
90    pub length: usize,
91    pub font_family: Option<String>,
92    pub font_weight: Option<u32>,
93    pub font_bold: Option<bool>,
94    pub font_italic: Option<bool>,
95    pub font_point_size: Option<u32>,
96    pub underline_style: crate::types::UnderlineStyle,
97    pub overline: bool,
98    pub strikeout: bool,
99    pub is_link: bool,
100    /// Extra space added after each glyph (in pixels). From TextFormat::letter_spacing.
101    pub letter_spacing: f32,
102    /// Extra space added after space glyphs (in pixels). From TextFormat::word_spacing.
103    pub word_spacing: f32,
104    /// Text foreground color (RGBA). None means default (black).
105    pub foreground_color: Option<[f32; 4]>,
106    /// Underline color (RGBA). None means use foreground_color.
107    pub underline_color: Option<[f32; 4]>,
108    /// Text-level background highlight color (RGBA). None means transparent.
109    pub background_color: Option<[f32; 4]>,
110    /// Hyperlink destination URL.
111    pub anchor_href: Option<String>,
112    /// Tooltip text.
113    pub tooltip: Option<String>,
114    /// Vertical alignment (normal, superscript, subscript).
115    pub vertical_alignment: crate::types::VerticalAlignment,
116    /// If Some, this fragment represents an inline image placeholder.
117    pub image_name: Option<String>,
118    /// Image width in pixels. Only meaningful when image_name is Some.
119    pub image_width: f32,
120    /// Image height in pixels. Only meaningful when image_name is Some.
121    pub image_height: f32,
122    /// Discretionary OpenType features to toggle during shaping. Empty =
123    /// font defaults. See [`crate::types::FontFeature`].
124    pub features: Vec<crate::types::FontFeature>,
125}
126
127/// Lay out a single block: resolve fonts, shape fragments, break into lines.
128///
129/// `scale_factor` is the device pixel ratio. Layout output is always in
130/// logical pixels; the scale factor affects shaping/rasterization precision.
131pub fn layout_block(
132    registry: &FontRegistry,
133    params: &BlockLayoutParams,
134    available_width: f32,
135    scale_factor: f32,
136    font_scale: f32,
137) -> BlockLayout {
138    let effective_left_margin = params.left_margin + params.list_indent;
139    let content_width = (available_width - effective_left_margin - params.right_margin).max(0.0);
140
141    // Resolve fonts and shape each fragment
142    let mut shaped_runs = Vec::new();
143    let mut default_metrics: Option<FontMetricsPx> = None;
144
145    for frag in &params.fragments {
146        // Inline image: create a synthetic run with one placeholder glyph
147        if let Some(ref image_name) = frag.image_name {
148            let image_glyph = ShapedGlyph {
149                glyph_id: 0,
150                cluster: 0,
151                x_advance: frag.image_width,
152                y_advance: 0.0,
153                x_offset: 0.0,
154                y_offset: 0.0,
155                font_face_id: crate::types::FontFaceId(0),
156            };
157            let run = ShapedRun {
158                font_face_id: crate::types::FontFaceId(0),
159                size_px: 0.0,
160                weight: 400,
161                glyphs: vec![image_glyph],
162                advance_width: frag.image_width,
163                text_range: frag.offset..frag.offset + frag.text.len(),
164                direction: TextDirection::LeftToRight,
165                underline_style: frag.underline_style,
166                overline: false,
167                strikeout: false,
168                is_link: frag.is_link,
169                foreground_color: None,
170                underline_color: None,
171                background_color: None,
172                anchor_href: frag.anchor_href.clone(),
173                tooltip: frag.tooltip.clone(),
174                vertical_alignment: crate::types::VerticalAlignment::Normal,
175                image_name: Some(image_name.clone()),
176                image_height: frag.image_height,
177            };
178            shaped_runs.push(run);
179            continue;
180        }
181
182        // Scale font size for superscript/subscript
183        let font_point_size = match frag.vertical_alignment {
184            crate::types::VerticalAlignment::SuperScript
185            | crate::types::VerticalAlignment::SubScript => frag
186                .font_point_size
187                .map(|s| ((s as f32 * 0.65) as u32).max(1)),
188            crate::types::VerticalAlignment::Normal => frag.font_point_size,
189        };
190
191        let resolved = resolve_font(
192            registry,
193            frag.font_family.as_deref(),
194            frag.font_weight,
195            frag.font_bold,
196            frag.font_italic,
197            font_point_size,
198            scale_factor,
199            font_scale,
200        );
201
202        if let Some(resolved) = resolved {
203            // Capture default metrics from the first resolved font
204            if default_metrics.is_none() {
205                default_metrics = font_metrics_px(registry, &resolved);
206            }
207
208            let features = to_harfrust_features(&frag.features);
209            if let Some(mut run) = shape_text_with_fallback(
210                registry,
211                &resolved,
212                &frag.text,
213                frag.offset,
214                TextDirection::Auto,
215                &features,
216            ) {
217                run.underline_style = frag.underline_style;
218                run.overline = frag.overline;
219                run.strikeout = frag.strikeout;
220                run.is_link = frag.is_link;
221                run.foreground_color = frag.foreground_color;
222                run.underline_color = frag.underline_color;
223                run.background_color = frag.background_color;
224                run.anchor_href = frag.anchor_href.clone();
225                run.tooltip = frag.tooltip.clone();
226                run.vertical_alignment = frag.vertical_alignment;
227
228                // Apply letter_spacing and word_spacing post-shaping
229                if frag.letter_spacing != 0.0 || frag.word_spacing != 0.0 {
230                    apply_spacing(&mut run, &frag.text, frag.letter_spacing, frag.word_spacing);
231                }
232
233                // Apply tab stops
234                if !params.tab_positions.is_empty() {
235                    apply_tab_stops(&mut run, &frag.text, &params.tab_positions);
236                }
237
238                shaped_runs.push(run);
239            }
240        }
241    }
242
243    // Fallback metrics if no fragments resolved
244    let metrics =
245        default_metrics.unwrap_or_else(|| get_default_metrics(registry, scale_factor, font_scale));
246
247    // Non-breakable lines: use infinite width to prevent wrapping
248    let wrap_width = if params.non_breakable_lines {
249        f32::INFINITY
250    } else {
251        content_width
252    };
253
254    // Hyphenation: a hyphen glyph shaped in the default font, supplied only
255    // when enabled and wrapping is in effect.
256    let hyphenator = params
257        .hyphenation
258        .filter(|_| !params.non_breakable_lines)
259        .and_then(|h| {
260            shape_hyphen(registry, scale_factor, font_scale).map(|glyph| Hyphenator {
261                glyph,
262                language: h.language,
263            })
264        });
265
266    // Break shaped runs into lines
267    let mut lines = break_into_lines(
268        shaped_runs,
269        &params.text,
270        wrap_width,
271        params.alignment,
272        params.text_indent,
273        &metrics,
274        hyphenator,
275    );
276
277    // Apply line height multiplier
278    let line_height_mul = params.line_height_multiplier.unwrap_or(1.0).max(0.1);
279
280    // Compute y positions for each line (relative to block content top)
281    let mut y = 0.0f32;
282    for line in &mut lines {
283        if line_height_mul != 1.0 {
284            line.line_height *= line_height_mul;
285        }
286        line.y = y + line.ascent; // y is the baseline position
287        y += line.line_height;
288    }
289
290    let content_height = y;
291    let total_height = params.top_margin + content_height + params.bottom_margin;
292
293    // Shape list marker or checkbox marker
294    let list_marker = if params.checkbox.is_some() {
295        shape_checkbox_marker(registry, &metrics, params, scale_factor, font_scale)
296    } else if !params.list_marker.is_empty() {
297        shape_list_marker(registry, &metrics, params, scale_factor, font_scale)
298    } else {
299        None
300    };
301
302    BlockLayout {
303        block_id: params.block_id,
304        position: params.position,
305        lines,
306        y: 0.0, // set by flow layout
307        height: total_height,
308        top_margin: params.top_margin,
309        bottom_margin: params.bottom_margin,
310        left_margin: effective_left_margin,
311        right_margin: params.right_margin,
312        list_marker,
313        background_color: params.background_color,
314    }
315}
316
317/// A resolved paint-only color overlay span for one character range of a block.
318///
319/// `char_start`/`char_end` are **block-relative character offsets** — the same
320/// space as the post-layout `ShapedGlyph::cluster` values (see
321/// `break_into_lines`, which converts clusters to char offsets). Each field is
322/// `None` when the overlay does not override it (the base run's value is kept).
323/// Applying paint spans never changes glyph geometry, advances, or line breaks
324/// — only color / decoration attributes — so the layout does not reflow.
325#[derive(Clone, Debug, Default, PartialEq)]
326pub struct PaintSpan {
327    pub char_start: usize,
328    pub char_end: usize,
329    pub foreground_color: Option<[f32; 4]>,
330    pub underline_color: Option<[f32; 4]>,
331    pub background_color: Option<[f32; 4]>,
332    pub underline_style: Option<crate::types::UnderlineStyle>,
333    pub overline: Option<bool>,
334    pub strikeout: Option<bool>,
335}
336
337/// The effective set of overrides for one glyph, used to group consecutive
338/// glyphs that share the same paint result into a single output run.
339#[derive(Clone, Default, PartialEq)]
340struct PaintOverride {
341    foreground_color: Option<[f32; 4]>,
342    underline_color: Option<[f32; 4]>,
343    background_color: Option<[f32; 4]>,
344    underline_style: Option<crate::types::UnderlineStyle>,
345    overline: Option<bool>,
346    strikeout: Option<bool>,
347}
348
349impl PaintOverride {
350    fn is_noop(&self) -> bool {
351        *self == PaintOverride::default()
352    }
353
354    /// Merge the overlapping spans covering `char_off` (last span wins per
355    /// field). Overlay spans from `extract_paint_spans` are already disjoint,
356    /// but last-wins keeps this correct for arbitrary inputs.
357    fn for_char(char_off: usize, spans: &[PaintSpan]) -> Self {
358        let mut o = PaintOverride::default();
359        for s in spans {
360            if s.char_start <= char_off && char_off < s.char_end {
361                if s.foreground_color.is_some() {
362                    o.foreground_color = s.foreground_color;
363                }
364                if s.underline_color.is_some() {
365                    o.underline_color = s.underline_color;
366                }
367                if s.background_color.is_some() {
368                    o.background_color = s.background_color;
369                }
370                if s.underline_style.is_some() {
371                    o.underline_style = s.underline_style;
372                }
373                if s.overline.is_some() {
374                    o.overline = s.overline;
375                }
376                if s.strikeout.is_some() {
377                    o.strikeout = s.strikeout;
378                }
379            }
380        }
381        o
382    }
383
384    /// Apply this override onto a positioned run segment, writing color /
385    /// decoration fields on BOTH the shaped run and its duplicated
386    /// `RunDecorations` (the renderer reads glyph color from the former and
387    /// decoration rects from the latter). `None` fields keep the base value.
388    fn apply(&self, run: &mut crate::layout::line::PositionedRun) {
389        if let Some(c) = self.foreground_color {
390            run.shaped_run.foreground_color = Some(c);
391            run.decorations.foreground_color = Some(c);
392        }
393        if let Some(c) = self.underline_color {
394            run.shaped_run.underline_color = Some(c);
395            run.decorations.underline_color = Some(c);
396        }
397        if let Some(c) = self.background_color {
398            run.shaped_run.background_color = Some(c);
399            run.decorations.background_color = Some(c);
400        }
401        if let Some(s) = self.underline_style {
402            run.shaped_run.underline_style = s;
403            run.decorations.underline_style = s;
404        }
405        if let Some(b) = self.overline {
406            run.shaped_run.overline = b;
407            run.decorations.overline = b;
408        }
409        if let Some(b) = self.strikeout {
410            run.shaped_run.strikeout = b;
411            run.decorations.strikeout = b;
412        }
413    }
414}
415
416/// Apply paint-only color spans to a base [`BlockLayout`], returning a recolored
417/// clone. The base is left untouched.
418///
419/// The result has byte-identical glyph positions, advances, line breaks, line
420/// widths, and block height to `base` — only color / decoration attributes
421/// differ. This is the "recolor without reshape/reflow" fast path: a run is
422/// split into segments at paint-span boundaries (snapped to glyph/cluster
423/// boundaries, never mid-cluster) and each segment's color fields are set.
424/// Splitting a run never alters any glyph advance, so line widths are preserved.
425///
426/// Empty `spans` returns an exact (color-preserving) clone of `base`.
427pub fn apply_paint_spans(base: &BlockLayout, spans: &[PaintSpan]) -> BlockLayout {
428    let mut out = base.clone();
429    if spans.is_empty() {
430        return out;
431    }
432    for line in &mut out.lines {
433        let mut new_runs: Vec<crate::layout::line::PositionedRun> =
434            Vec::with_capacity(line.runs.len());
435        for run in line.runs.drain(..) {
436            recolor_run_into(run, spans, &mut new_runs);
437        }
438        line.runs = new_runs;
439    }
440    out
441}
442
443/// Split `run` at paint-span boundaries and push the recolored segment(s) onto
444/// `out`. Image / glyph-less runs are passed through unchanged (paint overlays
445/// never recolor images).
446fn recolor_run_into(
447    run: crate::layout::line::PositionedRun,
448    spans: &[PaintSpan],
449    out: &mut Vec<crate::layout::line::PositionedRun>,
450) {
451    if run.shaped_run.glyphs.is_empty() || run.shaped_run.image_name.is_some() {
452        out.push(run);
453        return;
454    }
455
456    // Per-glyph effective override, in glyph (visual) order. Works for LTR and
457    // RTL alike: we group by adjacency in the glyph array, not by char order.
458    let overrides: Vec<PaintOverride> = run
459        .shaped_run
460        .glyphs
461        .iter()
462        .map(|g| PaintOverride::for_char(g.cluster as usize, spans))
463        .collect();
464
465    // Fast path: the whole run shares one override (the common case, and the
466    // only case when `spans` doesn't touch this run — then it's a no-op). Keep
467    // the base `advance_width` exactly so a cleared/uncovered run is identical.
468    if overrides.iter().all(|o| *o == overrides[0]) {
469        let mut seg = run;
470        overrides[0].apply(&mut seg);
471        out.push(seg);
472        return;
473    }
474
475    // Split into maximal runs of equal override.
476    let glyphs = run.shaped_run.glyphs.clone();
477    let mut seg_x = run.x;
478    let mut start = 0usize;
479    while start < glyphs.len() {
480        let ov = &overrides[start];
481        let mut end = start + 1;
482        while end < glyphs.len() && overrides[end] == *ov {
483            end += 1;
484        }
485        let seg_glyphs: Vec<crate::shaping::run::ShapedGlyph> = glyphs[start..end].to_vec();
486        let seg_advance: f32 = seg_glyphs.iter().map(|g| g.x_advance).sum();
487        let mut shaped = run.shaped_run.clone();
488        shaped.glyphs = seg_glyphs;
489        shaped.advance_width = seg_advance;
490        let mut seg = crate::layout::line::PositionedRun {
491            shaped_run: shaped,
492            x: seg_x,
493            decorations: run.decorations.clone(),
494        };
495        if !ov.is_noop() {
496            ov.apply(&mut seg);
497        }
498        out.push(seg);
499        seg_x += seg_advance;
500        start = end;
501    }
502}
503
504/// Add letter_spacing (to all glyphs) and word_spacing (to space glyphs).
505fn apply_spacing(run: &mut ShapedRun, text: &str, letter_spacing: f32, word_spacing: f32) {
506    let mut extra_advance = 0.0f32;
507    for glyph in &mut run.glyphs {
508        glyph.x_advance += letter_spacing;
509        extra_advance += letter_spacing;
510
511        // Add word_spacing to space characters.
512        // Detect spaces by mapping cluster back to the text.
513        if word_spacing != 0.0 {
514            let byte_offset = glyph.cluster as usize;
515            if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
516                && ch == ' '
517            {
518                glyph.x_advance += word_spacing;
519                extra_advance += word_spacing;
520            }
521        }
522    }
523    run.advance_width += extra_advance;
524}
525
526/// Shape a hyphen glyph (`-`) in the default font, for appending at
527/// hyphenated line breaks. Returns `None` if no default font resolves.
528pub(crate) fn shape_hyphen(
529    registry: &FontRegistry,
530    scale_factor: f32,
531    font_scale: f32,
532) -> Option<ShapedGlyph> {
533    let resolved = resolve_font(
534        registry,
535        None,
536        None,
537        None,
538        None,
539        None,
540        scale_factor,
541        font_scale,
542    )?;
543    let run = shape_text(registry, &resolved, "-", 0)?;
544    run.glyphs.into_iter().next()
545}
546
547/// Shape the list marker text and position it in the indent area.
548fn shape_list_marker(
549    registry: &FontRegistry,
550    _metrics: &FontMetricsPx,
551    params: &BlockLayoutParams,
552    scale_factor: f32,
553    font_scale: f32,
554) -> Option<ShapedListMarker> {
555    // Use the default font for the marker
556    let resolved = resolve_font(
557        registry,
558        None,
559        None,
560        None,
561        None,
562        None,
563        scale_factor,
564        font_scale,
565    )?;
566    let run = shape_text(registry, &resolved, &params.list_marker, 0)?;
567
568    // Position the marker: right-aligned within the indent area, with a small gap
569    let gap = 4.0; // pixels between marker and content
570    let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
571    let marker_x = marker_x.max(params.left_margin);
572
573    Some(ShapedListMarker { run, x: marker_x })
574}
575
576/// Expand tab character advances to reach the next tab stop position.
577fn apply_tab_stops(run: &mut ShapedRun, text: &str, tab_positions: &[f32]) {
578    let default_tab = 48.0; // default tab width if no stops defined
579    let mut pen_x = 0.0f32;
580
581    for glyph in &mut run.glyphs {
582        let byte_offset = glyph.cluster as usize;
583        if let Some(ch) = text.get(byte_offset..).and_then(|s| s.chars().next())
584            && ch == '\t'
585        {
586            // Find the next tab stop after the current pen position
587            let next_stop = tab_positions
588                .iter()
589                .find(|&&stop| stop > pen_x + 1.0)
590                .copied()
591                .unwrap_or_else(|| {
592                    // Past all defined stops: use default tab increments
593                    let last = tab_positions.last().copied().unwrap_or(0.0);
594                    let increment = if tab_positions.len() >= 2 {
595                        tab_positions[1] - tab_positions[0]
596                    } else {
597                        default_tab
598                    };
599                    let mut stop = last + increment;
600                    while stop <= pen_x + 1.0 {
601                        stop += increment;
602                    }
603                    stop
604                });
605
606            let tab_advance = next_stop - pen_x;
607            let delta = tab_advance - glyph.x_advance;
608            glyph.x_advance = tab_advance;
609            run.advance_width += delta;
610        }
611        pen_x += glyph.x_advance;
612    }
613}
614
615/// Shape a checkbox marker (unchecked or checked) for rendering in the margin.
616fn shape_checkbox_marker(
617    registry: &FontRegistry,
618    _metrics: &FontMetricsPx,
619    params: &BlockLayoutParams,
620    scale_factor: f32,
621    font_scale: f32,
622) -> Option<ShapedListMarker> {
623    let checked = params.checkbox?;
624    let marker_text = if checked { "\u{2611}" } else { "\u{2610}" }; // ballot box with/without check
625
626    let resolved = resolve_font(
627        registry,
628        None,
629        None,
630        None,
631        None,
632        None,
633        scale_factor,
634        font_scale,
635    )?;
636    let run = shape_text(registry, &resolved, marker_text, 0)?;
637
638    // If the font doesn't have the ballot box characters, use ASCII fallback
639    let run = if run.glyphs.iter().any(|g| g.glyph_id == 0) {
640        let fallback_text = if checked { "[x]" } else { "[ ]" };
641        shape_text(registry, &resolved, fallback_text, 0)?
642    } else {
643        run
644    };
645
646    let gap = 4.0;
647    let marker_x = params.left_margin + params.list_indent - run.advance_width - gap;
648    let marker_x = marker_x.max(params.left_margin);
649
650    Some(ShapedListMarker { run, x: marker_x })
651}
652
653fn get_default_metrics(
654    registry: &FontRegistry,
655    scale_factor: f32,
656    font_scale: f32,
657) -> FontMetricsPx {
658    if let Some(default_id) = registry.default_font() {
659        let resolved = ResolvedFont {
660            font_face_id: default_id,
661            size_px: registry.default_size_px() * font_scale,
662            face_index: registry.get(default_id).map(|e| e.face_index).unwrap_or(0),
663            swash_cache_key: registry
664                .get(default_id)
665                .map(|e| e.swash_cache_key)
666                .unwrap_or_default(),
667            scale_factor,
668            weight: 400,
669        };
670        if let Some(m) = font_metrics_px(registry, &resolved) {
671            return m;
672        }
673    }
674    // Absolute fallback: synthetic metrics for 16px
675    FontMetricsPx {
676        ascent: 14.0,
677        descent: 4.0,
678        leading: 0.0,
679        underline_offset: -2.0,
680        strikeout_offset: 5.0,
681        stroke_size: 1.0,
682    }
683}