text_typeset/types.rs
1/// Opaque handle to a registered font face.
2///
3/// Obtained from [`crate::TextFontService::register_font`] or [`crate::TextFontService::register_font_as`].
4/// Pass to [`crate::TextFontService::set_default_font`] to make it the default.
5#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
6pub struct FontFaceId(pub u32);
7
8// ── Render output ───────────────────────────────────────────────
9
10/// Everything needed to draw one frame.
11///
12/// Produced by [`crate::DocumentFlow::render`]. Contains glyph quads (textured rectangles
13/// from the atlas), inline image placeholders, and decoration rectangles
14/// (selections, cursor, underlines, table borders, etc.).
15///
16/// The adapter draws the frame in three passes:
17/// 1. Upload `atlas_pixels` as a GPU texture (only when `atlas_dirty` is true).
18/// 2. Draw each [`GlyphQuad`] as a textured rectangle from the atlas.
19/// 3. Draw each [`DecorationRect`] as a colored rectangle.
20pub struct RenderFrame {
21 /// True if the atlas texture changed since the last frame (needs re-upload).
22 pub atlas_dirty: bool,
23 /// Atlas texture width in pixels.
24 pub atlas_width: u32,
25 /// Atlas texture height in pixels.
26 pub atlas_height: u32,
27 /// RGBA pixel data, row-major. Length = `atlas_width * atlas_height * 4`.
28 pub atlas_pixels: Vec<u8>,
29 /// One textured rectangle per visible glyph.
30 pub glyphs: Vec<GlyphQuad>,
31 /// Inline image placeholders. The adapter loads the actual image data
32 /// (e.g., via `TextDocument::resource(name)`) and draws it at the given
33 /// screen position.
34 pub images: Vec<ImageQuad>,
35 /// Decoration rectangles: selections, cursor, underlines, strikeouts,
36 /// overlines, backgrounds, table borders, and cell backgrounds.
37 pub decorations: Vec<DecorationRect>,
38 /// Per-block glyph data for incremental updates. Keyed by block_id.
39 pub(crate) block_glyphs: Vec<(usize, Vec<GlyphQuad>)>,
40 /// Per-block decoration data (underlines, etc. — NOT cursor/selection).
41 pub(crate) block_decorations: Vec<(usize, Vec<DecorationRect>)>,
42 /// Per-block image data for incremental updates.
43 pub(crate) block_images: Vec<(usize, Vec<ImageQuad>)>,
44 /// Per-block height snapshot for detecting height changes in incremental render.
45 pub(crate) block_heights: std::collections::HashMap<usize, f32>,
46 /// Per-block glyph cache keys, parallel to [`Self::block_glyphs`]. Used by
47 /// [`crate::DocumentFlow::render_cursor_only`] and
48 /// [`crate::DocumentFlow::render_block_only`] to mark every cached
49 /// glyph as still-in-use in the shared `GlyphCache` — otherwise
50 /// glyphs reused via paint-cache hits (which never re-enter the
51 /// `cache.get()` path that refreshes timestamps) would age out and
52 /// their atlas slots could be reallocated for unrelated glyphs,
53 /// silently corrupting the cached `GlyphQuad`s' atlas references.
54 pub(crate) block_glyph_keys: Vec<(usize, Vec<crate::atlas::cache::GlyphCacheKey>)>,
55 /// Flat glyph cache keys, parallel to [`Self::glyphs`]. Rebuilt from
56 /// [`Self::block_glyph_keys`] by `rebuild_flat_frame`; passed to
57 /// [`crate::TextFontService::touch_glyphs`] on every cursor-only /
58 /// block-only paint so the shared atlas keeps visible glyphs alive.
59 pub(crate) glyph_keys: Vec<crate::atlas::cache::GlyphCacheKey>,
60 /// Snapshot of [`crate::TextFontService::eviction_epoch`] at the
61 /// moment this frame's atlas references were baked. Cursor-only
62 /// and block-only paths compare against the service's current
63 /// epoch and fall back to a full re-render if eviction has
64 /// happened since — defensive safety net behind the `touch_glyphs`
65 /// keep-alive mechanism.
66 pub(crate) atlas_eviction_epoch: u64,
67}
68
69/// A positioned glyph to draw as a textured quad from the atlas.
70///
71/// The adapter draws the rectangle at `screen` position, sampling from
72/// the `atlas` rectangle in the atlas texture, tinted with `color`.
73#[derive(Clone)]
74pub struct GlyphQuad {
75 /// Screen position and size: `[x, y, width, height]` in pixels.
76 pub screen: [f32; 4],
77 /// Atlas source rectangle: `[x, y, width, height]` in atlas pixel coordinates.
78 pub atlas: [f32; 4],
79 /// Glyph color: `[r, g, b, a]`, 0.0-1.0.
80 /// For normal text glyphs, this is the text color (default black).
81 /// For color emoji, this is `[1, 1, 1, 1]` (color is baked into the atlas).
82 pub color: [f32; 4],
83 /// `true` if the atlas region for this glyph holds a pre-multiplied
84 /// RGBA color bitmap (color emoji via COLR/CBDT/sbix). The renderer
85 /// must sample `texture.rgb` directly instead of using the texture
86 /// as an alpha mask tinted by [`color`](Self::color).
87 pub is_color: bool,
88}
89
90/// An inline image placeholder.
91///
92/// text-typeset computes the position and size but does NOT load or rasterize
93/// the image. The adapter retrieves the image data (e.g., from
94/// `TextDocument::resource(name)`) and draws it as a separate texture.
95#[derive(Clone)]
96pub struct ImageQuad {
97 /// Screen position and size: `[x, y, width, height]` in pixels.
98 pub screen: [f32; 4],
99 /// Image resource name (matches `FragmentContent::Image::name` from text-document).
100 pub name: String,
101}
102
103/// A colored rectangle for decorations (underlines, selections, borders, etc.).
104#[derive(Clone)]
105pub struct DecorationRect {
106 /// Screen position and size: `[x, y, width, height]` in pixels.
107 pub rect: [f32; 4],
108 /// Color: `[r, g, b, a]`, 0.0-1.0.
109 pub color: [f32; 4],
110 /// What kind of decoration this rectangle represents.
111 pub kind: DecorationKind,
112}
113
114/// The type of a [`DecorationRect`].
115#[derive(Debug, Clone, Copy, PartialEq, Eq)]
116pub enum DecorationKind {
117 /// Selection highlight (translucent background behind selected text).
118 Selection,
119 /// Cursor caret (thin vertical line at the insertion point).
120 Cursor,
121 /// Underline (below baseline, from font metrics).
122 Underline,
123 /// Strikethrough (at x-height, from font metrics).
124 Strikeout,
125 /// Overline (at ascent line).
126 Overline,
127 /// Generic background (e.g., frame borders).
128 Background,
129 /// Block-level background color.
130 BlockBackground,
131 /// Table border line.
132 TableBorder,
133 /// Table cell background color.
134 TableCellBackground,
135 /// Text-level background highlight (behind individual text runs).
136 /// Adapters should draw these before glyph quads so text appears on top.
137 TextBackground,
138 /// Cell-level selection highlight (entire cell background when cells are
139 /// selected as a rectangular region, as opposed to text within cells).
140 CellSelection,
141}
142
143/// Underline style for text decorations.
144#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
145pub enum UnderlineStyle {
146 /// No underline.
147 #[default]
148 None,
149 /// Solid single underline.
150 Single,
151 /// Dashed underline.
152 Dash,
153 /// Dotted underline.
154 Dot,
155 /// Alternating dash-dot pattern.
156 DashDot,
157 /// Alternating dash-dot-dot pattern.
158 DashDotDot,
159 /// Wavy underline.
160 Wave,
161 /// Spell-check underline (wavy, typically red).
162 SpellCheck,
163}
164
165/// Vertical alignment for characters (superscript, subscript, etc.).
166#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
167pub enum VerticalAlignment {
168 /// Normal baseline alignment.
169 #[default]
170 Normal,
171 /// Superscript: smaller size, shifted up.
172 SuperScript,
173 /// Subscript: smaller size, shifted down.
174 SubScript,
175}
176
177// ── Hit testing ─────────────────────────────────────────────────
178
179/// Disambiguates the two visual placements a single character position
180/// can have at a soft-wrap boundary. A long paragraph that wraps across
181/// lines K and K+1 has one character position N that sits at both the
182/// END of line K and the START of line K+1; affinity picks which one
183/// the caret renders at and which line `Home`/`End`-style navigation
184/// considers "current".
185///
186/// Affinity is a display concern: it makes no sense without a layout
187/// engine and a wrap width. It is never persisted with the text model
188/// (cf. Cocoa `NSSelectionAffinity` on `NSTextView`, not on
189/// `NSTextStorage`; Chromium `PositionWithAffinity` at the editing
190/// layer, not on `Position`; same in Qt and CodeMirror).
191///
192/// At positions that are NOT wrap boundaries — the interior of a line,
193/// the start of the first wrap-line, the end of the last wrap-line of
194/// a paragraph — affinity is a no-op and the rendering is identical.
195#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
196pub enum CursorAffinity {
197 /// Place the caret at the END of the previous wrap line. This is
198 /// the visual "trailing" placement and the default for any
199 /// position not produced by an upstream-side interaction.
200 #[default]
201 Downstream,
202 /// Place the caret at the START of the next wrap line.
203 Upstream,
204}
205
206/// Result of [`crate::DocumentFlow::hit_test`] - maps a screen-space point to a
207/// document position.
208pub struct HitTestResult {
209 /// Absolute character position in the document.
210 pub position: usize,
211 /// Which side of a soft-wrap boundary the click landed on. When
212 /// the matched line's Y range contained the click and `position`
213 /// equals that line's `char_range.start` AND a preceding line in
214 /// the same block ends at the same position, the click is on the
215 /// upstream side of the boundary → `Upstream`. Otherwise
216 /// `Downstream`. At non-wrap positions the value is `Downstream`
217 /// (default) and does not affect anything.
218 pub affinity: CursorAffinity,
219 /// Which block (paragraph) was hit, identified by stable block ID.
220 pub block_id: usize,
221 /// Character offset within the block (0 = start of block).
222 pub offset_in_block: usize,
223 /// What region of the layout was hit.
224 pub region: HitRegion,
225 /// Tooltip text if the hit position has a tooltip. None otherwise.
226 pub tooltip: Option<String>,
227 /// When non-None, the hit position is inside a table cell.
228 /// Identifies the table by its stable table ID.
229 /// None for hits on top-level blocks, frame blocks, or outside any table.
230 pub table_id: Option<usize>,
231}
232
233/// What region of the layout a hit test landed in.
234#[derive(Debug)]
235pub enum HitRegion {
236 /// Inside a text run (normal text content).
237 Text,
238 /// In the block's left margin area (before any text content).
239 LeftMargin,
240 /// In the block's indent area.
241 Indent,
242 /// On a table border line.
243 TableBorder,
244 /// Below all content in the document.
245 BelowContent,
246 /// Past the end of a line (to the right of the last character).
247 PastLineEnd,
248 /// On an inline image.
249 Image { name: String },
250 /// On a hyperlink.
251 Link { href: String },
252}
253
254// ── Cursor display ──────────────────────────────────────────────
255
256/// Cursor display state for rendering.
257///
258/// The adapter reads cursor position from text-document's `TextCursor`
259/// and creates this struct to feed to [`crate::DocumentFlow::set_cursor`].
260/// text-typeset uses it to generate caret and selection decorations
261/// in the next [`crate::DocumentFlow::render`] call.
262pub struct CursorDisplay {
263 /// Cursor position (character offset in the document).
264 pub position: usize,
265 /// Selection anchor. Equals `position` when there is no selection.
266 /// When different from `position`, the range `[min(anchor, position), max(anchor, position))`
267 /// is highlighted as a selection.
268 pub anchor: usize,
269 /// Which side of a soft-wrap boundary the caret renders on (see
270 /// [`CursorAffinity`]). At non-boundary positions this is a
271 /// no-op; default `Downstream` (current behavior before affinity
272 /// was introduced).
273 pub affinity: CursorAffinity,
274 /// Whether the caret is visible (false during the blink-off phase).
275 /// The adapter manages the blink timer; text-typeset just respects this flag.
276 pub visible: bool,
277 /// When non-empty, render cell-level selection highlights instead of
278 /// text-level selection. Each tuple is `(table_id, row, col)` identifying
279 /// a selected cell. The adapter fills this from `TextCursor::selected_cells()`.
280 pub selected_cells: Vec<(usize, usize, usize)>,
281}
282
283// ── Scrolling ───────────────────────────────────────────────────
284
285/// Visual position and size of a laid-out block.
286///
287/// Returned by [`crate::DocumentFlow::block_visual_info`].
288pub struct BlockVisualInfo {
289 /// Block ID (matches `BlockSnapshot::block_id`).
290 pub block_id: usize,
291 /// Y position of the block's top edge relative to the document start, in pixels.
292 pub y: f32,
293 /// Total height of the block including margins, in pixels.
294 pub height: f32,
295}
296
297// ── OpenType features ───────────────────────────────────────────
298
299/// An OpenType feature toggle applied during shaping.
300///
301/// `tag` is the 4-byte feature tag (e.g. `*b"liga"`, `*b"smcp"`,
302/// `*b"tnum"`, `*b"ss01"`); `value` is the feature value — `0` disables
303/// it, `1` enables it, and some features (e.g. `aalt`) take an index.
304///
305/// Script-mandated features (Arabic joining, Indic reordering, etc.)
306/// always apply regardless of this list; these toggles control the
307/// *discretionary* typographic features a caller wants on or off.
308#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
309pub struct FontFeature {
310 /// The 4-byte OpenType feature tag.
311 pub tag: [u8; 4],
312 /// Feature value: `0` = off, `1` = on, or a feature-specific index.
313 pub value: u32,
314}
315
316impl FontFeature {
317 /// A feature tag turned on (`value = 1`).
318 pub const fn on(tag: [u8; 4]) -> Self {
319 Self { tag, value: 1 }
320 }
321
322 /// A feature tag turned off (`value = 0`).
323 pub const fn off(tag: [u8; 4]) -> Self {
324 Self { tag, value: 0 }
325 }
326
327 /// A feature tag with an explicit value.
328 pub const fn new(tag: [u8; 4], value: u32) -> Self {
329 Self { tag, value }
330 }
331}
332
333/// Hyphenation settings for line wrapping.
334///
335/// Presence (`Some`) enables hyphenation; the `language` selects the
336/// Knuth-Liang dictionary. Soft hyphens (U+00AD) always break and render a
337/// hyphen when enabled, regardless of language; dictionary hyphenation
338/// applies only when the language's patterns are compiled in (otherwise it
339/// silently falls back to soft-hyphen-only).
340#[derive(Clone, Copy, Debug, PartialEq, Eq)]
341pub struct Hyphenation {
342 /// ISO 639-1 language code, e.g. `*b"en"`, `*b"fr"`, `*b"de"`.
343 pub language: [u8; 2],
344}
345
346impl Hyphenation {
347 /// Hyphenation in the given ISO 639-1 language.
348 pub const fn new(language: [u8; 2]) -> Self {
349 Self { language }
350 }
351
352 /// English hyphenation (`en`).
353 pub const ENGLISH: Self = Self { language: *b"en" };
354}
355
356impl Default for Hyphenation {
357 fn default() -> Self {
358 Self::ENGLISH
359 }
360}
361
362// ── Single-line API ────────────────────────────────────────────
363
364/// Text formatting parameters for the single-line layout API.
365///
366/// Controls font selection, size, and text color. All fields are optional
367/// and fall back to the typesetter's defaults (default font, default size,
368/// default text color).
369#[derive(Clone, Debug, Default)]
370pub struct TextFormat {
371 /// Font family name (e.g., "Noto Sans", "monospace").
372 /// None means use the default font.
373 pub font_family: Option<String>,
374 /// Font weight (100-900). Overrides `font_bold`.
375 pub font_weight: Option<u32>,
376 /// Shorthand for weight 700. Ignored if `font_weight` is set.
377 pub font_bold: Option<bool>,
378 /// Italic style.
379 pub font_italic: Option<bool>,
380 /// Font size in pixels. None means use the default size.
381 pub font_size: Option<f32>,
382 /// Text color (RGBA, 0.0-1.0). None means use the typesetter's text color.
383 pub color: Option<[f32; 4]>,
384 /// Discretionary OpenType features to toggle during shaping (ligatures,
385 /// small caps, tabular numerals, stylistic sets, …). Empty = font defaults.
386 pub features: Vec<FontFeature>,
387 /// Hyphenation (Knuth-Liang dictionary + soft-hyphen breaks) for line
388 /// wrapping. `None` = disabled (default); most useful for justified
389 /// prose. See [`Hyphenation`].
390 pub hyphenation: Option<Hyphenation>,
391}
392
393/// Result of [`crate::DocumentFlow::layout_single_line`].
394///
395/// Contains the measured dimensions and GPU-ready glyph quads for a
396/// single line of text. No flow layout, line breaking, or bidi analysis
397/// is performed.
398pub struct SingleLineResult {
399 /// Total advance width of the shaped text, in pixels.
400 pub width: f32,
401 /// Line height (ascent + descent + leading), in pixels.
402 pub height: f32,
403 /// Distance from the top of the line to the baseline, in pixels.
404 pub baseline: f32,
405 /// Distance from baseline to the top of the underline, in logical
406 /// pixels. Positive = below the baseline. Sourced from the primary
407 /// font's `post` table.
408 pub underline_offset: f32,
409 /// Underline line thickness in logical pixels. Sourced from the
410 /// primary font's stroke size.
411 pub underline_thickness: f32,
412 /// GPU-ready glyph quads, positioned at y=0 (no scroll offset).
413 pub glyphs: Vec<GlyphQuad>,
414 /// Per-glyph cache keys, parallel to `glyphs`. Callers that cache
415 /// glyph output externally should pass these back to
416 /// [`crate::TextFontService::touch_glyphs`] each frame to prevent the
417 /// atlas from evicting still-visible glyphs.
418 pub glyph_keys: Vec<crate::atlas::cache::GlyphCacheKey>,
419 /// Per-span bounding rectangles for markup-aware layout
420 /// ([`crate::DocumentFlow::layout_single_line_markup`]). Empty for
421 /// the plain-text layout path.
422 pub spans: Vec<LaidOutSpan>,
423}
424
425/// A single laid-out span produced by the markup-aware layout path.
426///
427/// When a link wraps across two paragraph lines, it produces two
428/// `LaidOutSpan` entries sharing the same URL and byte_range but with
429/// distinct `line_index` / `rect`.
430#[derive(Debug, Clone)]
431pub struct LaidOutSpan {
432 pub kind: LaidOutSpanKind,
433 /// Which wrapped line this span piece lives on (0 for single-line).
434 pub line_index: usize,
435 /// Local-space rect: `[x, y, width, height]`, same space as glyph quads.
436 pub rect: [f32; 4],
437 /// Byte range into the original markup source.
438 pub byte_range: std::ops::Range<usize>,
439}
440
441/// Kind discriminator for [`LaidOutSpan`].
442#[derive(Debug, Clone)]
443pub enum LaidOutSpanKind {
444 Text,
445 Link { url: String },
446}
447
448/// Result of [`crate::DocumentFlow::layout_paragraph`].
449///
450/// Contains the measured dimensions and GPU-ready glyph quads for a
451/// multi-line paragraph wrapped at a fixed width. Glyphs are positioned
452/// in paragraph-local coordinates: `x = 0` is the left edge of the
453/// paragraph, `y = 0` is the top of the first line's line box. The
454/// adapter should offset all glyph quads by the paragraph's screen
455/// position before drawing.
456pub struct ParagraphResult {
457 /// Width of the widest laid-out line, in pixels. May be less than the
458 /// `max_width` passed to `layout_paragraph` if the content is narrower.
459 pub width: f32,
460 /// Total stacked paragraph height in pixels — sum of line heights for
461 /// all emitted lines.
462 pub height: f32,
463 /// Distance from `y = 0` to the baseline of the first line, in pixels.
464 pub baseline_first: f32,
465 /// Number of lines actually emitted (respects `max_lines` when set).
466 pub line_count: usize,
467 /// Line height (single line's ascent + descent + leading), in pixels.
468 /// Useful for callers that need to reason about per-line geometry.
469 pub line_height: f32,
470 /// Distance from baseline to the top of the underline, in logical
471 /// pixels. Positive = below the baseline. Sourced from the primary
472 /// font's `post` table.
473 pub underline_offset: f32,
474 /// Underline line thickness in logical pixels. Sourced from the
475 /// primary font's stroke size.
476 pub underline_thickness: f32,
477 /// GPU-ready glyph quads in paragraph-local coordinates.
478 pub glyphs: Vec<GlyphQuad>,
479 /// Per-glyph cache keys, parallel to `glyphs`. See
480 /// [`SingleLineResult::glyph_keys`].
481 pub glyph_keys: Vec<crate::atlas::cache::GlyphCacheKey>,
482 /// Per-span bounding rectangles for markup-aware layout
483 /// ([`crate::DocumentFlow::layout_paragraph_markup`]). Empty for
484 /// the plain-text layout path.
485 pub spans: Vec<LaidOutSpan>,
486}
487
488impl RenderFrame {
489 pub(crate) fn new() -> Self {
490 Self {
491 atlas_dirty: false,
492 atlas_width: 0,
493 atlas_height: 0,
494 atlas_pixels: Vec::new(),
495 glyphs: Vec::new(),
496 images: Vec::new(),
497 decorations: Vec::new(),
498 block_glyphs: Vec::new(),
499 block_decorations: Vec::new(),
500 block_images: Vec::new(),
501 block_heights: std::collections::HashMap::new(),
502 block_glyph_keys: Vec::new(),
503 glyph_keys: Vec::new(),
504 atlas_eviction_epoch: 0,
505 }
506 }
507}
508
509// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
510// CharacterGeometry — accessibility per-character advance data
511// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
512
513/// Per-character advance geometry for a laid-out text run.
514///
515/// Consumed by accessibility layers that need to populate AccessKit's
516/// `character_positions` and `character_widths` on a `Role::TextRun`
517/// node so screen reader highlight cursors and screen magnifiers can
518/// track the caret at character granularity.
519///
520/// `position` is measured in run-local coordinates: the first
521/// character of the requested range sits at `position == 0.0`, and
522/// subsequent characters accumulate their advance widths. `width` is
523/// the advance width of each character, in the same units.
524#[derive(Debug, Clone, Copy, PartialEq)]
525pub struct CharacterGeometry {
526 pub position: f32,
527 pub width: f32,
528}