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    /// Maximum content width across all laid-out lines, in pixels.
206    ///
207    /// Use for horizontal scrollbar range when text wrapping is disabled.
208    /// Returns 0.0 if no blocks have been laid out.
209    pub fn max_content_width(&self) -> f32 {
210        self.flow_layout.cached_max_content_width
211    }
212
213    // ── Layout ──────────────────────────────────────────────────
214
215    /// Full layout from a text-document `FlowSnapshot`.
216    ///
217    /// Converts all snapshot elements (blocks, tables, frames) to internal
218    /// layout params and lays out the entire document flow. Call this on
219    /// `DocumentReset` events or initial document load.
220    ///
221    /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
222    #[cfg(feature = "text-document")]
223    pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
224        use crate::bridge::convert_flow;
225
226        let converted = convert_flow(flow);
227
228        // Merge all elements by flow index and process in order
229        let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
230        for (idx, params) in converted.blocks {
231            all_items.push((idx, FlowItemKind::Block(params)));
232        }
233        for (idx, params) in converted.tables {
234            all_items.push((idx, FlowItemKind::Table(params)));
235        }
236        for (idx, params) in converted.frames {
237            all_items.push((idx, FlowItemKind::Frame(params)));
238        }
239        all_items.sort_by_key(|(idx, _)| *idx);
240
241        let lw = self.layout_width();
242        self.flow_layout.clear();
243        self.flow_layout.viewport_width = self.viewport_width;
244        self.flow_layout.viewport_height = self.viewport_height;
245
246        for (_idx, kind) in all_items {
247            match kind {
248                FlowItemKind::Block(params) => {
249                    self.flow_layout.add_block(&self.font_registry, &params, lw);
250                }
251                FlowItemKind::Table(params) => {
252                    self.flow_layout.add_table(&self.font_registry, &params, lw);
253                }
254                FlowItemKind::Frame(params) => {
255                    self.flow_layout.add_frame(&self.font_registry, &params, lw);
256                }
257            }
258        }
259    }
260
261    /// Lay out a list of blocks from scratch (framework-agnostic API).
262    ///
263    /// Replaces all existing layout state with the given blocks.
264    /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
265    /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
266    pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
267        self.flow_layout
268            .layout_blocks(&self.font_registry, block_params, self.layout_width());
269    }
270
271    /// Add a frame to the current flow layout.
272    ///
273    /// The frame is placed after all previously laid-out content.
274    /// Frame position (inline, float, absolute) is determined by
275    /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
276    pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
277        self.flow_layout
278            .add_frame(&self.font_registry, params, self.layout_width());
279    }
280
281    /// Add a table to the current flow layout.
282    ///
283    /// The table is placed after all previously laid-out content.
284    pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
285        self.flow_layout
286            .add_table(&self.font_registry, params, self.layout_width());
287    }
288
289    /// Relayout a single block after its content or formatting changed.
290    ///
291    /// Re-shapes and re-wraps the block, then shifts subsequent blocks
292    /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
293    /// for single-block edits (typing, formatting changes).
294    ///
295    /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
296    /// the table row height is re-measured and content below the table shifts.
297    pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
298        self.flow_layout
299            .relayout_block(&self.font_registry, params, self.layout_width());
300    }
301
302    // ── Rendering ───────────────────────────────────────────────
303
304    /// Render the visible viewport and return everything needed to draw.
305    ///
306    /// Performs viewport culling (only processes blocks within the scroll window),
307    /// rasterizes any new glyphs into the atlas, and produces glyph quads,
308    /// image placeholders, and decoration rectangles.
309    ///
310    /// The returned reference borrows the `Typesetter`. The adapter should iterate
311    /// the frame for drawing, then drop the reference before calling any
312    /// layout/scroll methods on the next frame.
313    ///
314    /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
315    /// atlas to reclaim space.
316    pub fn render(&mut self) -> &RenderFrame {
317        crate::render::frame::build_render_frame(
318            &self.flow_layout,
319            &self.font_registry,
320            &mut self.atlas,
321            &mut self.glyph_cache,
322            &mut self.scale_context,
323            self.scroll_offset,
324            self.viewport_height,
325            &self.cursors,
326            self.cursor_color,
327            self.selection_color,
328            &mut self.render_frame,
329        );
330        &self.render_frame
331    }
332
333    /// Incremental render that only re-renders one block's glyphs.
334    ///
335    /// Reuses cached glyph/decoration data for all other blocks from the
336    /// last full `render()`. Use after `relayout_block()` when only one
337    /// block's text changed.
338    pub fn render_block_only(&mut self, block_id: usize) -> &RenderFrame {
339        // Re-render just this block's glyphs into a temporary frame
340        let mut new_glyphs = Vec::new();
341        let mut new_images = Vec::new();
342        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
343            let mut tmp = crate::types::RenderFrame::new();
344            crate::render::frame::render_block_at_offset(
345                block,
346                0.0,
347                0.0,
348                &self.font_registry,
349                &mut self.atlas,
350                &mut self.glyph_cache,
351                &mut self.scale_context,
352                self.scroll_offset,
353                self.viewport_height,
354                &mut tmp,
355            );
356            new_glyphs = tmp.glyphs;
357            new_images = tmp.images;
358        }
359
360        // Re-generate this block's decorations
361        let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
362            crate::render::decoration::generate_block_decorations(
363                block,
364                &self.font_registry,
365                self.scroll_offset,
366                self.viewport_height,
367                0.0,
368                0.0,
369                self.flow_layout.viewport_width,
370            )
371        } else {
372            Vec::new()
373        };
374
375        // Replace this block's entry in the per-block caches
376        if let Some(entry) = self
377            .render_frame
378            .block_glyphs
379            .iter_mut()
380            .find(|(id, _)| *id == block_id)
381        {
382            entry.1 = new_glyphs;
383        }
384        if let Some(entry) = self
385            .render_frame
386            .block_images
387            .iter_mut()
388            .find(|(id, _)| *id == block_id)
389        {
390            entry.1 = new_images;
391        }
392        if let Some(entry) = self
393            .render_frame
394            .block_decorations
395            .iter_mut()
396            .find(|(id, _)| *id == block_id)
397        {
398            entry.1 = new_decos;
399        }
400
401        // Rebuild flat vecs from per-block cache + cursor decorations
402        self.rebuild_flat_frame();
403
404        &self.render_frame
405    }
406
407    /// Lightweight render that only updates cursor/selection decorations.
408    ///
409    /// Reuses the existing glyph quads and images from the last full `render()`.
410    /// Use this when only the cursor blinked or selection changed, not the text.
411    pub fn render_cursor_only(&mut self) -> &RenderFrame {
412        // Remove old cursor/selection decorations, keep block decorations
413        self.render_frame.decorations.retain(|d| {
414            !matches!(
415                d.kind,
416                crate::types::DecorationKind::Cursor | crate::types::DecorationKind::Selection
417            )
418        });
419
420        // Regenerate cursor/selection decorations
421        let cursor_decos = crate::render::cursor::generate_cursor_decorations(
422            &self.flow_layout,
423            &self.cursors,
424            self.scroll_offset,
425            self.cursor_color,
426            self.selection_color,
427        );
428        self.render_frame.decorations.extend(cursor_decos);
429
430        &self.render_frame
431    }
432
433    /// Rebuild flat glyphs/images/decorations from per-block caches + cursor decorations.
434    fn rebuild_flat_frame(&mut self) {
435        self.render_frame.glyphs.clear();
436        self.render_frame.images.clear();
437        self.render_frame.decorations.clear();
438        for (_, glyphs) in &self.render_frame.block_glyphs {
439            self.render_frame.glyphs.extend_from_slice(glyphs);
440        }
441        for (_, images) in &self.render_frame.block_images {
442            self.render_frame.images.extend_from_slice(images);
443        }
444        for (_, decos) in &self.render_frame.block_decorations {
445            self.render_frame.decorations.extend_from_slice(decos);
446        }
447        let cursor_decos = crate::render::cursor::generate_cursor_decorations(
448            &self.flow_layout,
449            &self.cursors,
450            self.scroll_offset,
451            self.cursor_color,
452            self.selection_color,
453        );
454        self.render_frame.decorations.extend(cursor_decos);
455
456        // Update atlas metadata
457        self.render_frame.atlas_dirty = self.atlas.dirty;
458        self.render_frame.atlas_width = self.atlas.width;
459        self.render_frame.atlas_height = self.atlas.height;
460        if self.atlas.dirty {
461            let pixels = &self.atlas.pixels;
462            let needed = (self.atlas.width * self.atlas.height * 4) as usize;
463            self.render_frame.atlas_pixels.resize(needed, 0);
464            let copy_len = needed.min(pixels.len());
465            self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
466            self.atlas.dirty = false;
467        }
468    }
469
470    // ── Hit testing ─────────────────────────────────────────────
471
472    /// Map a screen-space point to a document position.
473    ///
474    /// Coordinates are relative to the widget's top-left corner.
475    /// The scroll offset is accounted for internally.
476    /// Returns `None` if the flow has no content.
477    pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
478        crate::render::hit_test::hit_test(&self.flow_layout, self.scroll_offset, x, y)
479    }
480
481    /// Get the screen-space caret rectangle at a document position.
482    ///
483    /// Returns `[x, y, width, height]` in screen pixels. Use this to report
484    /// the caret position to the platform IME system for composition window
485    /// placement. For drawing the caret, use the [`crate::DecorationKind::Cursor`]
486    /// entry in [`crate::RenderFrame::decorations`] instead.
487    pub fn caret_rect(&self, position: usize) -> [f32; 4] {
488        crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position)
489    }
490
491    // ── Cursor display ──────────────────────────────────────────
492
493    /// Update the cursor display state for a single cursor.
494    ///
495    /// The adapter reads `position` and `anchor` from text-document's
496    /// `TextCursor`, toggles `visible` on a blink timer, and passes
497    /// the result here. The typesetter includes cursor and selection
498    /// decorations in the next [`render`](Self::render) call.
499    pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
500        self.cursors = vec![CursorDisplay {
501            position: cursor.position,
502            anchor: cursor.anchor,
503            visible: cursor.visible,
504        }];
505    }
506
507    /// Update multiple cursors (multi-cursor editing support).
508    ///
509    /// Each cursor independently generates a caret and optional selection highlight.
510    pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
511        self.cursors = cursors
512            .iter()
513            .map(|c| CursorDisplay {
514                position: c.position,
515                anchor: c.anchor,
516                visible: c.visible,
517            })
518            .collect();
519    }
520
521    /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
522    ///
523    /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
524    pub fn set_selection_color(&mut self, color: [f32; 4]) {
525        self.selection_color = color;
526    }
527
528    /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
529    ///
530    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
531    pub fn set_cursor_color(&mut self, color: [f32; 4]) {
532        self.cursor_color = color;
533    }
534
535    // ── Scrolling ───────────────────────────────────────────────
536
537    /// Get the visual position and height of a laid-out block.
538    ///
539    /// Returns `None` if the block ID is not in the current layout.
540    pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
541        let block = self.flow_layout.blocks.get(&block_id)?;
542        Some(BlockVisualInfo {
543            block_id,
544            y: block.y,
545            height: block.height,
546        })
547    }
548
549    /// Scroll so that the given document position is visible, placing it
550    /// roughly 1/3 from the top of the viewport.
551    ///
552    /// Returns the new scroll offset.
553    pub fn scroll_to_position(&mut self, position: usize) -> f32 {
554        let rect = self.caret_rect(position);
555        let target_y = rect[1] + self.scroll_offset - self.viewport_height / 3.0;
556        self.scroll_offset = target_y.max(0.0);
557        self.scroll_offset
558    }
559
560    /// Scroll the minimum amount needed to make the current caret visible.
561    ///
562    /// Call after cursor movement (arrow keys, click, typing) to keep
563    /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
564    /// or `None` if the caret was already visible.
565    pub fn ensure_caret_visible(&mut self) -> Option<f32> {
566        if self.cursors.is_empty() {
567            return None;
568        }
569        let pos = self.cursors[0].position;
570        let rect = self.caret_rect(pos);
571        let caret_screen_y = rect[1];
572        let caret_screen_bottom = caret_screen_y + rect[3];
573        let margin = 10.0;
574        let old_offset = self.scroll_offset;
575
576        if caret_screen_y < 0.0 {
577            self.scroll_offset += caret_screen_y - margin;
578            self.scroll_offset = self.scroll_offset.max(0.0);
579        } else if caret_screen_bottom > self.viewport_height {
580            self.scroll_offset += caret_screen_bottom - self.viewport_height + margin;
581        }
582
583        if (self.scroll_offset - old_offset).abs() > 0.001 {
584            Some(self.scroll_offset)
585        } else {
586            None
587        }
588    }
589}
590
591#[cfg(feature = "text-document")]
592enum FlowItemKind {
593    Block(crate::layout::block::BlockLayoutParams),
594    Table(crate::layout::table::TableLayoutParams),
595    Frame(crate::layout::frame::FrameLayoutParams),
596}
597
598impl Default for Typesetter {
599    fn default() -> Self {
600        Self::new()
601    }
602}