Skip to main content

text_typeset/
typesetter.rs

1use crate::atlas::allocator::GlyphAtlas;
2use crate::atlas::cache::GlyphCache;
3use crate::font::registry::FontRegistry;
4use crate::layout::flow::FlowLayout;
5use crate::types::{BlockVisualInfo, CursorDisplay, FontFaceId, HitTestResult, RenderFrame};
6
7/// How the content (layout) width is determined.
8///
9/// Controls whether text reflows when the viewport resizes (web/editor style)
10/// or wraps at a fixed width (page/WYSIWYG style).
11#[derive(Debug, Clone, Copy, Default)]
12pub enum ContentWidthMode {
13    /// Content width equals viewport width. Text reflows on window resize.
14    /// This is the default.typical for editors and web-style layout.
15    #[default]
16    Auto,
17    /// Content width is fixed, independent of viewport.
18    /// For page-like layout (WYSIWYG), print preview, or side panels.
19    /// If the content is wider than the viewport, horizontal scrolling is needed.
20    /// If narrower, the content is centered or left-aligned within the viewport.
21    Fixed(f32),
22}
23
24/// The main entry point for text typesetting.
25///
26/// Owns the font registry, glyph atlas, layout cache, and render state.
27/// The typical usage pattern is:
28///
29/// 1. Create with [`Typesetter::new`]
30/// 2. Register fonts with [`register_font`](Typesetter::register_font)
31/// 3. Set default font with [`set_default_font`](Typesetter::set_default_font)
32/// 4. Set viewport with [`set_viewport`](Typesetter::set_viewport)
33/// 5. Lay out content with [`layout_full`](Typesetter::layout_full) or [`layout_blocks`](Typesetter::layout_blocks)
34/// 6. Set cursor state with [`set_cursor`](Typesetter::set_cursor)
35/// 7. Render with [`render`](Typesetter::render) to get a [`RenderFrame`]
36/// 8. On edits, use [`relayout_block`](Typesetter::relayout_block) for incremental updates
37///
38/// # Thread safety
39///
40/// `Typesetter` is `!Send + !Sync` because its internal fontdb, atlas allocator,
41/// and swash scale context are not thread-safe. It lives on the adapter's render
42/// thread alongside the framework's drawing calls.
43pub struct Typesetter {
44    font_registry: FontRegistry,
45    atlas: GlyphAtlas,
46    glyph_cache: GlyphCache,
47    flow_layout: FlowLayout,
48    scale_context: swash::scale::ScaleContext,
49    render_frame: RenderFrame,
50    scroll_offset: f32,
51    viewport_width: f32,
52    viewport_height: f32,
53    content_width_mode: ContentWidthMode,
54    selection_color: [f32; 4],
55    cursor_color: [f32; 4],
56    cursors: Vec<CursorDisplay>,
57}
58
59impl Typesetter {
60    /// Create a new typesetter with no fonts loaded.
61    ///
62    /// Call [`register_font`](Self::register_font) and [`set_default_font`](Self::set_default_font)
63    /// before laying out any content.
64    pub fn new() -> Self {
65        Self {
66            font_registry: FontRegistry::new(),
67            atlas: GlyphAtlas::new(),
68            glyph_cache: GlyphCache::new(),
69            flow_layout: FlowLayout::new(),
70            scale_context: swash::scale::ScaleContext::new(),
71            render_frame: RenderFrame::new(),
72            scroll_offset: 0.0,
73            viewport_width: 0.0,
74            viewport_height: 0.0,
75            content_width_mode: ContentWidthMode::Auto,
76            selection_color: [0.26, 0.52, 0.96, 0.3],
77            cursor_color: [0.0, 0.0, 0.0, 1.0],
78            cursors: Vec::new(),
79        }
80    }
81
82    // ── Font registration ───────────────────────────────────────
83
84    /// Register a font face from raw TTF/OTF/WOFF bytes.
85    ///
86    /// Parses the font's name table to extract family, weight, and style,
87    /// then indexes it for CSS-spec font matching via [`fontdb`].
88    /// Returns the first face ID (font collections like `.ttc` may contain multiple faces).
89    ///
90    /// # Panics
91    ///
92    /// Panics if the font data contains no parseable faces.
93    pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
94        let ids = self.font_registry.register_font(data);
95        ids.into_iter()
96            .next()
97            .expect("font data contained no faces")
98    }
99
100    /// Register a font with explicit metadata, overriding the font's name table.
101    ///
102    /// Use when the font's internal metadata is unreliable or when aliasing
103    /// a font to a different family name.
104    ///
105    /// # Panics
106    ///
107    /// Panics if the font data contains no parseable faces.
108    pub fn register_font_as(
109        &mut self,
110        data: &[u8],
111        family: &str,
112        weight: u16,
113        italic: bool,
114    ) -> FontFaceId {
115        let ids = self
116            .font_registry
117            .register_font_as(data, family, weight, italic);
118        ids.into_iter()
119            .next()
120            .expect("font data contained no faces")
121    }
122
123    /// Set which font face to use as the document default.
124    ///
125    /// This is the fallback font when a fragment's `TextFormat` doesn't specify
126    /// a family or the specified family isn't found.
127    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
128        self.font_registry.set_default_font(face, size_px);
129    }
130
131    /// Map a generic family name to a registered font family.
132    ///
133    /// Common mappings: `"serif"` → `"Noto Serif"`, `"monospace"` → `"Fira Code"`.
134    /// When text-document specifies `font_family: "monospace"`, the typesetter
135    /// resolves it through this mapping before querying fontdb.
136    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
137        self.font_registry.set_generic_family(generic, family);
138    }
139
140    /// Access the font registry for advanced queries (glyph coverage, fallback, etc.).
141    pub fn font_registry(&self) -> &FontRegistry {
142        &self.font_registry
143    }
144
145    // ── Viewport & content width ───────────────────────────────
146
147    /// Set the viewport dimensions (visible area in pixels).
148    ///
149    /// The viewport controls:
150    /// - **Culling**: only blocks within the viewport are rendered.
151    /// - **Selection highlight**: multi-line selections extend to viewport width.
152    /// - **Layout width** (in [`ContentWidthMode::Auto`]): text wraps at viewport width.
153    ///
154    /// Call this when the window or container resizes.
155    pub fn set_viewport(&mut self, width: f32, height: f32) {
156        self.viewport_width = width;
157        self.viewport_height = height;
158        self.flow_layout.viewport_width = width;
159        self.flow_layout.viewport_height = height;
160    }
161
162    /// Set a fixed content width, independent of viewport.
163    ///
164    /// Text wraps at this width regardless of how wide the viewport is.
165    /// Use for page-like (WYSIWYG) layout or documents with explicit width.
166    /// Pass `f32::INFINITY` for no-wrap mode.
167    pub fn set_content_width(&mut self, width: f32) {
168        self.content_width_mode = ContentWidthMode::Fixed(width);
169    }
170
171    /// Set content width to follow viewport width (default).
172    ///
173    /// Text reflows when the viewport is resized. This is the standard
174    /// behavior for editors and web-style layout.
175    pub fn set_content_width_auto(&mut self) {
176        self.content_width_mode = ContentWidthMode::Auto;
177    }
178
179    /// The effective width used for text layout (line wrapping, table columns, etc.).
180    ///
181    /// In [`ContentWidthMode::Auto`], equals viewport width.
182    /// In [`ContentWidthMode::Fixed`], equals the set value.
183    pub fn layout_width(&self) -> f32 {
184        match self.content_width_mode {
185            ContentWidthMode::Auto => self.viewport_width,
186            ContentWidthMode::Fixed(w) => w,
187        }
188    }
189
190    /// Set the vertical scroll offset in pixels from the top of the document.
191    ///
192    /// Affects which blocks are visible (culling) and the screen-space
193    /// y coordinates in the rendered [`RenderFrame`].
194    pub fn set_scroll_offset(&mut self, offset: f32) {
195        self.scroll_offset = offset;
196    }
197
198    /// Total content height after layout, in pixels.
199    ///
200    /// Use for scrollbar range: `scrollbar.max = content_height - viewport_height`.
201    pub fn content_height(&self) -> f32 {
202        self.flow_layout.content_height
203    }
204
205    // ── Layout ──────────────────────────────────────────────────
206
207    /// Full layout from a text-document `FlowSnapshot`.
208    ///
209    /// Converts all snapshot elements (blocks, tables, frames) to internal
210    /// layout params and lays out the entire document flow. Call this on
211    /// `DocumentReset` events or initial document load.
212    ///
213    /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
214    #[cfg(feature = "text-document")]
215    pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
216        use crate::bridge::convert_flow;
217
218        let converted = convert_flow(flow);
219
220        // Merge all elements by flow index and process in order
221        let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
222        for (idx, params) in converted.blocks {
223            all_items.push((idx, FlowItemKind::Block(params)));
224        }
225        for (idx, params) in converted.tables {
226            all_items.push((idx, FlowItemKind::Table(params)));
227        }
228        for (idx, params) in converted.frames {
229            all_items.push((idx, FlowItemKind::Frame(params)));
230        }
231        all_items.sort_by_key(|(idx, _)| *idx);
232
233        let lw = self.layout_width();
234        self.flow_layout.clear();
235        self.flow_layout.viewport_width = self.viewport_width;
236        self.flow_layout.viewport_height = self.viewport_height;
237
238        for (_idx, kind) in all_items {
239            match kind {
240                FlowItemKind::Block(params) => {
241                    self.flow_layout.add_block(&self.font_registry, &params, lw);
242                }
243                FlowItemKind::Table(params) => {
244                    self.flow_layout.add_table(&self.font_registry, &params, lw);
245                }
246                FlowItemKind::Frame(params) => {
247                    self.flow_layout.add_frame(&self.font_registry, &params, lw);
248                }
249            }
250        }
251    }
252
253    /// Lay out a list of blocks from scratch (framework-agnostic API).
254    ///
255    /// Replaces all existing layout state with the given blocks.
256    /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
257    /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
258    pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
259        self.flow_layout
260            .layout_blocks(&self.font_registry, block_params, self.layout_width());
261    }
262
263    /// Add a frame to the current flow layout.
264    ///
265    /// The frame is placed after all previously laid-out content.
266    /// Frame position (inline, float, absolute) is determined by
267    /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
268    pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
269        self.flow_layout
270            .add_frame(&self.font_registry, params, self.layout_width());
271    }
272
273    /// Add a table to the current flow layout.
274    ///
275    /// The table is placed after all previously laid-out content.
276    pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
277        self.flow_layout
278            .add_table(&self.font_registry, params, self.layout_width());
279    }
280
281    /// Relayout a single block after its content or formatting changed.
282    ///
283    /// Re-shapes and re-wraps the block, then shifts subsequent blocks
284    /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
285    /// for single-block edits (typing, formatting changes).
286    ///
287    /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
288    /// the table row height is re-measured and content below the table shifts.
289    pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
290        self.flow_layout
291            .relayout_block(&self.font_registry, params, self.layout_width());
292    }
293
294    // ── Rendering ───────────────────────────────────────────────
295
296    /// Render the visible viewport and return everything needed to draw.
297    ///
298    /// Performs viewport culling (only processes blocks within the scroll window),
299    /// rasterizes any new glyphs into the atlas, and produces glyph quads,
300    /// image placeholders, and decoration rectangles.
301    ///
302    /// The returned reference borrows the `Typesetter`. The adapter should iterate
303    /// the frame for drawing, then drop the reference before calling any
304    /// layout/scroll methods on the next frame.
305    ///
306    /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
307    /// atlas to reclaim space.
308    pub fn render(&mut self) -> &RenderFrame {
309        crate::render::frame::build_render_frame(
310            &self.flow_layout,
311            &self.font_registry,
312            &mut self.atlas,
313            &mut self.glyph_cache,
314            &mut self.scale_context,
315            self.scroll_offset,
316            self.viewport_height,
317            &self.cursors,
318            self.cursor_color,
319            self.selection_color,
320            &mut self.render_frame,
321        );
322        &self.render_frame
323    }
324
325    // ── Hit testing ─────────────────────────────────────────────
326
327    /// Map a screen-space point to a document position.
328    ///
329    /// Coordinates are relative to the widget's top-left corner.
330    /// The scroll offset is accounted for internally.
331    /// Returns `None` if the flow has no content.
332    pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
333        crate::render::hit_test::hit_test(&self.flow_layout, self.scroll_offset, x, y)
334    }
335
336    /// Get the screen-space caret rectangle at a document position.
337    ///
338    /// Returns `[x, y, width, height]` in screen pixels. Use this to report
339    /// the caret position to the platform IME system for composition window
340    /// placement. For drawing the caret, use the [`crate::DecorationKind::Cursor`]
341    /// entry in [`crate::RenderFrame::decorations`] instead.
342    pub fn caret_rect(&self, position: usize) -> [f32; 4] {
343        crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position)
344    }
345
346    // ── Cursor display ──────────────────────────────────────────
347
348    /// Update the cursor display state for a single cursor.
349    ///
350    /// The adapter reads `position` and `anchor` from text-document's
351    /// `TextCursor`, toggles `visible` on a blink timer, and passes
352    /// the result here. The typesetter includes cursor and selection
353    /// decorations in the next [`render`](Self::render) call.
354    pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
355        self.cursors = vec![CursorDisplay {
356            position: cursor.position,
357            anchor: cursor.anchor,
358            visible: cursor.visible,
359        }];
360    }
361
362    /// Update multiple cursors (multi-cursor editing support).
363    ///
364    /// Each cursor independently generates a caret and optional selection highlight.
365    pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
366        self.cursors = cursors
367            .iter()
368            .map(|c| CursorDisplay {
369                position: c.position,
370                anchor: c.anchor,
371                visible: c.visible,
372            })
373            .collect();
374    }
375
376    /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
377    ///
378    /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
379    pub fn set_selection_color(&mut self, color: [f32; 4]) {
380        self.selection_color = color;
381    }
382
383    /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
384    ///
385    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
386    pub fn set_cursor_color(&mut self, color: [f32; 4]) {
387        self.cursor_color = color;
388    }
389
390    // ── Scrolling ───────────────────────────────────────────────
391
392    /// Get the visual position and height of a laid-out block.
393    ///
394    /// Returns `None` if the block ID is not in the current layout.
395    pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
396        let block = self.flow_layout.blocks.get(&block_id)?;
397        Some(BlockVisualInfo {
398            block_id,
399            y: block.y,
400            height: block.height,
401        })
402    }
403
404    /// Scroll so that the given document position is visible, placing it
405    /// roughly 1/3 from the top of the viewport.
406    ///
407    /// Returns the new scroll offset.
408    pub fn scroll_to_position(&mut self, position: usize) -> f32 {
409        let rect = self.caret_rect(position);
410        let target_y = rect[1] + self.scroll_offset - self.viewport_height / 3.0;
411        self.scroll_offset = target_y.max(0.0);
412        self.scroll_offset
413    }
414
415    /// Scroll the minimum amount needed to make the current caret visible.
416    ///
417    /// Call after cursor movement (arrow keys, click, typing) to keep
418    /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
419    /// or `None` if the caret was already visible.
420    pub fn ensure_caret_visible(&mut self) -> Option<f32> {
421        if self.cursors.is_empty() {
422            return None;
423        }
424        let pos = self.cursors[0].position;
425        let rect = self.caret_rect(pos);
426        let caret_screen_y = rect[1];
427        let caret_screen_bottom = caret_screen_y + rect[3];
428        let margin = 10.0;
429        let old_offset = self.scroll_offset;
430
431        if caret_screen_y < 0.0 {
432            self.scroll_offset += caret_screen_y - margin;
433            self.scroll_offset = self.scroll_offset.max(0.0);
434        } else if caret_screen_bottom > self.viewport_height {
435            self.scroll_offset += caret_screen_bottom - self.viewport_height + margin;
436        }
437
438        if (self.scroll_offset - old_offset).abs() > 0.001 {
439            Some(self.scroll_offset)
440        } else {
441            None
442        }
443    }
444}
445
446#[cfg(feature = "text-document")]
447enum FlowItemKind {
448    Block(crate::layout::block::BlockLayoutParams),
449    Table(crate::layout::table::TableLayoutParams),
450    Frame(crate::layout::frame::FrameLayoutParams),
451}
452
453impl Default for Typesetter {
454    fn default() -> Self {
455        Self::new()
456    }
457}