Skip to main content

text_typeset/
typesetter.rs

1use crate::atlas::allocator::GlyphAtlas;
2use crate::atlas::cache::{GlyphCache, GlyphCacheKey};
3use crate::atlas::rasterizer::rasterize_glyph;
4use crate::font::registry::FontRegistry;
5use crate::layout::flow::{FlowItem, FlowLayout};
6use crate::types::{
7    BlockVisualInfo, CursorDisplay, FontFaceId, GlyphQuad, HitTestResult, ParagraphResult,
8    RenderFrame, SingleLineResult, TextFormat,
9};
10
11/// How the content (layout) width is determined.
12///
13/// Controls whether text reflows when the viewport resizes (web/editor style)
14/// or wraps at a fixed width (page/WYSIWYG style).
15#[derive(Debug, Clone, Copy, Default)]
16pub enum ContentWidthMode {
17    /// Content width equals viewport width. Text reflows on window resize.
18    /// This is the default.typical for editors and web-style layout.
19    #[default]
20    Auto,
21    /// Content width is fixed, independent of viewport.
22    /// For page-like layout (WYSIWYG), print preview, or side panels.
23    /// If the content is wider than the viewport, horizontal scrolling is needed.
24    /// If narrower, the content is centered or left-aligned within the viewport.
25    Fixed(f32),
26}
27
28/// The main entry point for text typesetting.
29///
30/// Owns the font registry, glyph atlas, layout cache, and render state.
31/// The typical usage pattern is:
32///
33/// 1. Create with [`Typesetter::new`]
34/// 2. Register fonts with [`register_font`](Typesetter::register_font)
35/// 3. Set default font with [`set_default_font`](Typesetter::set_default_font)
36/// 4. Set viewport with [`set_viewport`](Typesetter::set_viewport)
37/// 5. Lay out content with [`layout_full`](Typesetter::layout_full) or [`layout_blocks`](Typesetter::layout_blocks)
38/// 6. Set cursor state with [`set_cursor`](Typesetter::set_cursor)
39/// 7. Render with [`render`](Typesetter::render) to get a [`RenderFrame`]
40/// 8. On edits, use [`relayout_block`](Typesetter::relayout_block) for incremental updates
41///
42/// # Thread safety
43///
44/// `Typesetter` is `!Send + !Sync` because its internal fontdb, atlas allocator,
45/// and swash scale context are not thread-safe. It lives on the adapter's render
46/// thread alongside the framework's drawing calls.
47pub struct Typesetter {
48    font_registry: FontRegistry,
49    atlas: GlyphAtlas,
50    glyph_cache: GlyphCache,
51    flow_layout: FlowLayout,
52    scale_context: swash::scale::ScaleContext,
53    render_frame: RenderFrame,
54    scroll_offset: f32,
55    rendered_scroll_offset: f32,
56    viewport_width: f32,
57    viewport_height: f32,
58    content_width_mode: ContentWidthMode,
59    selection_color: [f32; 4],
60    cursor_color: [f32; 4],
61    text_color: [f32; 4],
62    cursors: Vec<CursorDisplay>,
63    zoom: f32,
64    rendered_zoom: f32,
65    scale_factor: f32,
66}
67
68impl Typesetter {
69    /// Create a new typesetter with no fonts loaded.
70    ///
71    /// Call [`register_font`](Self::register_font) and [`set_default_font`](Self::set_default_font)
72    /// before laying out any content.
73    pub fn new() -> Self {
74        Self {
75            font_registry: FontRegistry::new(),
76            atlas: GlyphAtlas::new(),
77            glyph_cache: GlyphCache::new(),
78            flow_layout: FlowLayout::new(),
79            scale_context: swash::scale::ScaleContext::new(),
80            render_frame: RenderFrame::new(),
81            scroll_offset: 0.0,
82            rendered_scroll_offset: f32::NAN,
83            viewport_width: 0.0,
84            viewport_height: 0.0,
85            content_width_mode: ContentWidthMode::Auto,
86            selection_color: [0.26, 0.52, 0.96, 0.3],
87            cursor_color: [0.0, 0.0, 0.0, 1.0],
88            text_color: [0.0, 0.0, 0.0, 1.0],
89            cursors: Vec::new(),
90            zoom: 1.0,
91            rendered_zoom: f32::NAN,
92            scale_factor: 1.0,
93        }
94    }
95
96    // ── Font registration ───────────────────────────────────────
97
98    /// Register a font face from raw TTF/OTF/WOFF bytes.
99    ///
100    /// Parses the font's name table to extract family, weight, and style,
101    /// then indexes it for CSS-spec font matching via [`fontdb`].
102    /// Returns the first face ID (font collections like `.ttc` may contain multiple faces).
103    ///
104    /// # Panics
105    ///
106    /// Panics if the font data contains no parseable faces.
107    pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
108        let ids = self.font_registry.register_font(data);
109        ids.into_iter()
110            .next()
111            .expect("font data contained no faces")
112    }
113
114    /// Register a font with explicit metadata, overriding the font's name table.
115    ///
116    /// Use when the font's internal metadata is unreliable or when aliasing
117    /// a font to a different family name.
118    ///
119    /// # Panics
120    ///
121    /// Panics if the font data contains no parseable faces.
122    pub fn register_font_as(
123        &mut self,
124        data: &[u8],
125        family: &str,
126        weight: u16,
127        italic: bool,
128    ) -> FontFaceId {
129        let ids = self
130            .font_registry
131            .register_font_as(data, family, weight, italic);
132        ids.into_iter()
133            .next()
134            .expect("font data contained no faces")
135    }
136
137    /// Set which font face to use as the document default.
138    ///
139    /// This is the fallback font when a fragment's `TextFormat` doesn't specify
140    /// a family or the specified family isn't found.
141    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
142        self.font_registry.set_default_font(face, size_px);
143    }
144
145    /// Map a generic family name to a registered font family.
146    ///
147    /// Common mappings: `"serif"` → `"Noto Serif"`, `"monospace"` → `"Fira Code"`.
148    /// When text-document specifies `font_family: "monospace"`, the typesetter
149    /// resolves it through this mapping before querying fontdb.
150    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
151        self.font_registry.set_generic_family(generic, family);
152    }
153
154    /// Look up the family name of a registered font by its face ID.
155    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
156        self.font_registry.font_family_name(face_id)
157    }
158
159    /// Access the font registry for advanced queries (glyph coverage, fallback, etc.).
160    pub fn font_registry(&self) -> &FontRegistry {
161        &self.font_registry
162    }
163
164    // ── Viewport & content width ───────────────────────────────
165
166    /// Set the viewport dimensions (visible area in pixels).
167    ///
168    /// The viewport controls:
169    /// - **Culling**: only blocks within the viewport are rendered.
170    /// - **Selection highlight**: multi-line selections extend to viewport width.
171    /// - **Layout width** (in [`ContentWidthMode::Auto`]): text wraps at viewport width.
172    ///
173    /// Call this when the window or container resizes.
174    pub fn set_viewport(&mut self, width: f32, height: f32) {
175        self.viewport_width = width;
176        self.viewport_height = height;
177        self.flow_layout.viewport_width = width;
178        self.flow_layout.viewport_height = height;
179    }
180
181    /// Set a fixed content width, independent of viewport.
182    ///
183    /// Text wraps at this width regardless of how wide the viewport is.
184    /// Use for page-like (WYSIWYG) layout or documents with explicit width.
185    /// Pass `f32::INFINITY` for no-wrap mode.
186    pub fn set_content_width(&mut self, width: f32) {
187        self.content_width_mode = ContentWidthMode::Fixed(width);
188    }
189
190    /// Set content width to follow viewport width (default).
191    ///
192    /// Text reflows when the viewport is resized. This is the standard
193    /// behavior for editors and web-style layout.
194    pub fn set_content_width_auto(&mut self) {
195        self.content_width_mode = ContentWidthMode::Auto;
196    }
197
198    /// The effective width used for text layout (line wrapping, table columns, etc.).
199    ///
200    /// In [`ContentWidthMode::Auto`], equals `viewport_width / zoom` so that
201    /// text reflows to fit the zoomed viewport.
202    /// In [`ContentWidthMode::Fixed`], equals the set value (zoom only magnifies).
203    pub fn layout_width(&self) -> f32 {
204        match self.content_width_mode {
205            ContentWidthMode::Auto => self.viewport_width / self.zoom,
206            ContentWidthMode::Fixed(w) => w,
207        }
208    }
209
210    /// Set the vertical scroll offset in pixels from the top of the document.
211    ///
212    /// Affects which blocks are visible (culling) and the screen-space
213    /// y coordinates in the rendered [`RenderFrame`].
214    pub fn set_scroll_offset(&mut self, offset: f32) {
215        self.scroll_offset = offset;
216    }
217
218    /// Total content height after layout, in pixels.
219    ///
220    /// Use for scrollbar range: `scrollbar.max = content_height - viewport_height`.
221    pub fn content_height(&self) -> f32 {
222        self.flow_layout.content_height
223    }
224
225    /// Maximum content width across all laid-out lines, in pixels.
226    ///
227    /// Use for horizontal scrollbar range when text wrapping is disabled.
228    /// Returns 0.0 if no blocks have been laid out.
229    pub fn max_content_width(&self) -> f32 {
230        self.flow_layout.cached_max_content_width
231    }
232
233    // -- Zoom ────────────────────────────────────────────────────
234
235    /// Set the display zoom level.
236    ///
237    /// Zoom is a pure display transform: layout stays at base size, and all
238    /// screen-space output (glyph quads, decorations, caret rects) is scaled
239    /// by the zoom factor. Hit-test input coordinates are inversely scaled.
240    ///
241    /// This is PDF-viewer-style zoom (no text reflow). For browser-style
242    /// zoom that reflows text, combine with
243    /// `set_content_width(viewport_width / zoom)`.
244    ///
245    /// Clamped to `0.1..=10.0`. Default is `1.0`.
246    pub fn set_zoom(&mut self, zoom: f32) {
247        self.zoom = zoom.clamp(0.1, 10.0);
248    }
249
250    /// The current display zoom level (default 1.0).
251    pub fn zoom(&self) -> f32 {
252        self.zoom
253    }
254
255    // ── Scale factor (HiDPI) ────────────────────────────────────
256
257    /// Set the device pixel ratio for HiDPI rasterization.
258    ///
259    /// Layout stays in logical pixels, but glyphs are shaped and rasterized
260    /// at `size_px * scale_factor` so text is sharp on HiDPI displays.
261    /// This is orthogonal to [`set_zoom`](Self::set_zoom), which is a pure
262    /// display transform applied after rendering.
263    ///
264    /// Changing this value invalidates cached layout and cached glyphs:
265    /// the flow layout is cleared, the glyph cache is emptied, and the
266    /// atlas is reset. After calling this, re-run `layout_blocks` /
267    /// `layout_full` and then `render()`.
268    ///
269    /// Clamped to `0.25..=8.0`. Default is `1.0`.
270    pub fn set_scale_factor(&mut self, scale_factor: f32) {
271        let sf = scale_factor.clamp(0.25, 8.0);
272        if (self.scale_factor - sf).abs() <= f32::EPSILON {
273            return;
274        }
275        self.scale_factor = sf;
276        self.flow_layout.scale_factor = sf;
277        // Shaped advances and line metrics depended on the previous scale
278        // factor's ppem rounding; drop everything.
279        self.flow_layout.clear();
280        // Evict every cached glyph: rasters were produced at the old
281        // physical size and would be wrong at the new one.
282        self.glyph_cache.entries.clear();
283        // The atlas allocator still holds rects for glyphs we just
284        // evicted. Start from a fresh allocator to reclaim the space.
285        self.atlas = GlyphAtlas::new();
286        // Force the next incremental render path (`render_block_only` /
287        // `render_cursor_only`) to fall back to a full `render()`, since
288        // the per-block glyph caches in `render_frame` reference the
289        // now-discarded atlas. A sentinel outside the clamp range works;
290        // NaN would not (NaN comparisons always return false).
291        self.rendered_zoom = -1.0;
292        self.rendered_scroll_offset = -1.0e30;
293    }
294
295    /// The current scale factor (device pixel ratio). Default is `1.0`.
296    pub fn scale_factor(&self) -> f32 {
297        self.scale_factor
298    }
299
300    // ── Layout ──────────────────────────────────────────────────
301
302    /// Full layout from a text-document `FlowSnapshot`.
303    ///
304    /// Converts all snapshot elements (blocks, tables, frames) to internal
305    /// layout params and lays out the entire document flow. Call this on
306    /// `DocumentReset` events or initial document load.
307    ///
308    /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
309    #[cfg(feature = "text-document")]
310    pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
311        use crate::bridge::convert_flow;
312
313        let converted = convert_flow(flow);
314
315        // Merge all elements by flow index and process in order
316        let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
317        for (idx, params) in converted.blocks {
318            all_items.push((idx, FlowItemKind::Block(params)));
319        }
320        for (idx, params) in converted.tables {
321            all_items.push((idx, FlowItemKind::Table(params)));
322        }
323        for (idx, params) in converted.frames {
324            all_items.push((idx, FlowItemKind::Frame(params)));
325        }
326        all_items.sort_by_key(|(idx, _)| *idx);
327
328        let lw = self.layout_width();
329        self.flow_layout.clear();
330        self.flow_layout.viewport_width = self.viewport_width;
331        self.flow_layout.viewport_height = self.viewport_height;
332
333        for (_idx, kind) in all_items {
334            match kind {
335                FlowItemKind::Block(params) => {
336                    self.flow_layout.add_block(&self.font_registry, &params, lw);
337                }
338                FlowItemKind::Table(params) => {
339                    self.flow_layout.add_table(&self.font_registry, &params, lw);
340                }
341                FlowItemKind::Frame(params) => {
342                    self.flow_layout.add_frame(&self.font_registry, &params, lw);
343                }
344            }
345        }
346    }
347
348    /// Lay out a list of blocks from scratch (framework-agnostic API).
349    ///
350    /// Replaces all existing layout state with the given blocks.
351    /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
352    /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
353    pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
354        self.flow_layout
355            .layout_blocks(&self.font_registry, block_params, self.layout_width());
356    }
357
358    /// Add a frame to the current flow layout.
359    ///
360    /// The frame is placed after all previously laid-out content.
361    /// Frame position (inline, float, absolute) is determined by
362    /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
363    pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
364        self.flow_layout
365            .add_frame(&self.font_registry, params, self.layout_width());
366    }
367
368    /// Add a table to the current flow layout.
369    ///
370    /// The table is placed after all previously laid-out content.
371    pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
372        self.flow_layout
373            .add_table(&self.font_registry, params, self.layout_width());
374    }
375
376    /// Relayout a single block after its content or formatting changed.
377    ///
378    /// Re-shapes and re-wraps the block, then shifts subsequent blocks
379    /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
380    /// for single-block edits (typing, formatting changes).
381    ///
382    /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
383    /// the table row height is re-measured and content below the table shifts.
384    pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
385        self.flow_layout
386            .relayout_block(&self.font_registry, params, self.layout_width());
387    }
388
389    // ── Rendering ───────────────────────────────────────────────
390
391    /// Render the visible viewport and return everything needed to draw.
392    ///
393    /// Performs viewport culling (only processes blocks within the scroll window),
394    /// rasterizes any new glyphs into the atlas, and produces glyph quads,
395    /// image placeholders, and decoration rectangles.
396    ///
397    /// The returned reference borrows the `Typesetter`. The adapter should iterate
398    /// the frame for drawing, then drop the reference before calling any
399    /// layout/scroll methods on the next frame.
400    ///
401    /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
402    /// atlas to reclaim space.
403    pub fn render(&mut self) -> &RenderFrame {
404        let effective_vw = self.viewport_width / self.zoom;
405        let effective_vh = self.viewport_height / self.zoom;
406        crate::render::frame::build_render_frame(
407            &self.flow_layout,
408            &self.font_registry,
409            &mut self.atlas,
410            &mut self.glyph_cache,
411            &mut self.scale_context,
412            self.scroll_offset,
413            effective_vw,
414            effective_vh,
415            &self.cursors,
416            self.cursor_color,
417            self.selection_color,
418            self.text_color,
419            &mut self.render_frame,
420        );
421        self.rendered_scroll_offset = self.scroll_offset;
422        self.rendered_zoom = self.zoom;
423        apply_zoom(&mut self.render_frame, self.zoom);
424        &self.render_frame
425    }
426
427    /// Incremental render that only re-renders one block's glyphs.
428    ///
429    /// Reuses cached glyph/decoration data for all other blocks from the
430    /// last full `render()`. Use after `relayout_block()` when only one
431    /// block's text changed.
432    ///
433    /// If the block's height changed (causing subsequent blocks to shift),
434    /// this falls back to a full `render()` since cached glyph positions
435    /// for other blocks would be stale.
436    pub fn render_block_only(&mut self, block_id: usize) -> &RenderFrame {
437        // If scroll offset or zoom changed, all cached glyph positions are stale.
438        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
439            || (self.zoom - self.rendered_zoom).abs() > 0.001
440        {
441            return self.render();
442        }
443
444        // Table cell blocks are cached per-table (keyed by table_id), and
445        // frame blocks are cached per-frame (keyed by frame_id). Neither has
446        // entries in block_decorations or block_glyphs keyed by the cell
447        // block_id, so incremental rendering cannot update them in place.
448        // Fall back to a full render for both cases.
449        if !self.flow_layout.blocks.contains_key(&block_id) {
450            let in_table = self.flow_layout.tables.values().any(|table| {
451                table
452                    .cell_layouts
453                    .iter()
454                    .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
455            });
456            if in_table {
457                return self.render();
458            }
459            let in_frame = self
460                .flow_layout
461                .frames
462                .values()
463                .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
464            if in_frame {
465                return self.render();
466            }
467        }
468
469        // If the block's height changed, cached glyph positions for subsequent
470        // blocks are stale. Fall back to a full re-render.
471        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
472            let old_height = self
473                .render_frame
474                .block_heights
475                .get(&block_id)
476                .copied()
477                .unwrap_or(block.height);
478            if (block.height - old_height).abs() > 0.001 {
479                return self.render();
480            }
481        }
482
483        // Re-render just this block's glyphs into a temporary frame
484        let effective_vw = self.viewport_width / self.zoom;
485        let effective_vh = self.viewport_height / self.zoom;
486        let mut new_glyphs = Vec::new();
487        let mut new_images = Vec::new();
488        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
489            let mut tmp = crate::types::RenderFrame::new();
490            crate::render::frame::render_block_at_offset(
491                block,
492                0.0,
493                0.0,
494                &self.font_registry,
495                &mut self.atlas,
496                &mut self.glyph_cache,
497                &mut self.scale_context,
498                self.scroll_offset,
499                effective_vh,
500                self.text_color,
501                self.scale_factor,
502                &mut tmp,
503            );
504            new_glyphs = tmp.glyphs;
505            new_images = tmp.images;
506        }
507
508        // Re-generate this block's decorations
509        let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
510            crate::render::decoration::generate_block_decorations(
511                block,
512                &self.font_registry,
513                self.scroll_offset,
514                effective_vh,
515                0.0,
516                0.0,
517                effective_vw,
518                self.text_color,
519                self.scale_factor,
520            )
521        } else {
522            Vec::new()
523        };
524
525        // Replace this block's entry in the per-block caches
526        if let Some(entry) = self
527            .render_frame
528            .block_glyphs
529            .iter_mut()
530            .find(|(id, _)| *id == block_id)
531        {
532            entry.1 = new_glyphs;
533        }
534        if let Some(entry) = self
535            .render_frame
536            .block_images
537            .iter_mut()
538            .find(|(id, _)| *id == block_id)
539        {
540            entry.1 = new_images;
541        }
542        if let Some(entry) = self
543            .render_frame
544            .block_decorations
545            .iter_mut()
546            .find(|(id, _)| *id == block_id)
547        {
548            entry.1 = new_decos;
549        }
550
551        // Rebuild flat vecs from per-block cache + cursor decorations
552        self.rebuild_flat_frame();
553        apply_zoom(&mut self.render_frame, self.zoom);
554
555        &self.render_frame
556    }
557
558    /// Lightweight render that only updates cursor/selection decorations.
559    ///
560    /// Reuses the existing glyph quads and images from the last full `render()`.
561    /// Use this when only the cursor blinked or selection changed, not the text.
562    ///
563    /// If the scroll offset changed since the last full render, falls back to
564    /// a full [`render`](Self::render) so that glyph positions are updated.
565    pub fn render_cursor_only(&mut self) -> &RenderFrame {
566        // If scroll offset or zoom changed, glyph quads are stale - need full re-render
567        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
568            || (self.zoom - self.rendered_zoom).abs() > 0.001
569        {
570            return self.render();
571        }
572
573        // Remove old cursor/selection decorations, keep block decorations
574        self.render_frame.decorations.retain(|d| {
575            !matches!(
576                d.kind,
577                crate::types::DecorationKind::Cursor
578                    | crate::types::DecorationKind::Selection
579                    | crate::types::DecorationKind::CellSelection
580            )
581        });
582
583        // Regenerate cursor/selection decorations at 1x, then zoom
584        let effective_vw = self.viewport_width / self.zoom;
585        let effective_vh = self.viewport_height / self.zoom;
586        let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
587            &self.flow_layout,
588            &self.cursors,
589            self.scroll_offset,
590            self.cursor_color,
591            self.selection_color,
592            effective_vw,
593            effective_vh,
594        );
595        apply_zoom_decorations(&mut cursor_decos, self.zoom);
596        self.render_frame.decorations.extend(cursor_decos);
597
598        &self.render_frame
599    }
600
601    /// Rebuild flat glyphs/images/decorations from per-block caches + cursor decorations.
602    fn rebuild_flat_frame(&mut self) {
603        self.render_frame.glyphs.clear();
604        self.render_frame.images.clear();
605        self.render_frame.decorations.clear();
606        for (_, glyphs) in &self.render_frame.block_glyphs {
607            self.render_frame.glyphs.extend_from_slice(glyphs);
608        }
609        for (_, images) in &self.render_frame.block_images {
610            self.render_frame.images.extend_from_slice(images);
611        }
612        for (_, decos) in &self.render_frame.block_decorations {
613            self.render_frame.decorations.extend_from_slice(decos);
614        }
615
616        // Regenerate table and frame decorations (these are not stored in
617        // per-block caches, only in the flat decorations vec during full render).
618        for item in &self.flow_layout.flow_order {
619            match item {
620                FlowItem::Table { table_id, .. } => {
621                    if let Some(table) = self.flow_layout.tables.get(table_id) {
622                        let decos = crate::layout::table::generate_table_decorations(
623                            table,
624                            self.scroll_offset,
625                        );
626                        self.render_frame.decorations.extend(decos);
627                    }
628                }
629                FlowItem::Frame { frame_id, .. } => {
630                    if let Some(frame) = self.flow_layout.frames.get(frame_id) {
631                        crate::render::frame::append_frame_border_decorations(
632                            frame,
633                            self.scroll_offset,
634                            &mut self.render_frame.decorations,
635                        );
636                    }
637                }
638                FlowItem::Block { .. } => {}
639            }
640        }
641
642        let effective_vw = self.viewport_width / self.zoom;
643        let effective_vh = self.viewport_height / self.zoom;
644        let cursor_decos = crate::render::cursor::generate_cursor_decorations(
645            &self.flow_layout,
646            &self.cursors,
647            self.scroll_offset,
648            self.cursor_color,
649            self.selection_color,
650            effective_vw,
651            effective_vh,
652        );
653        self.render_frame.decorations.extend(cursor_decos);
654
655        // Update atlas metadata
656        self.render_frame.atlas_dirty = self.atlas.dirty;
657        self.render_frame.atlas_width = self.atlas.width;
658        self.render_frame.atlas_height = self.atlas.height;
659        if self.atlas.dirty {
660            let pixels = &self.atlas.pixels;
661            let needed = (self.atlas.width * self.atlas.height * 4) as usize;
662            self.render_frame.atlas_pixels.resize(needed, 0);
663            let copy_len = needed.min(pixels.len());
664            self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
665            self.atlas.dirty = false;
666        }
667    }
668
669    /// Read the glyph atlas state without triggering the full document
670    /// render pipeline. Advances the cache generation and runs eviction
671    /// (to reclaim atlas space), but does NOT re-render document content.
672    ///
673    /// Returns `(dirty, width, height, pixels, glyphs_evicted)`.
674    /// When `glyphs_evicted` is true, callers that cache glyph output
675    /// (e.g. paint caches) must invalidate — evicted atlas space may be
676    /// reused by future glyph allocations.
677    pub fn atlas_snapshot(&mut self, advance_generation: bool) -> (bool, u32, u32, &[u8], bool) {
678        // Only advance generation and run eviction when text work happened.
679        // Skipping this on idle frames prevents aging out glyphs that are
680        // still visible but not re-measured (paint cache reuse scenario).
681        let mut glyphs_evicted = false;
682        if advance_generation {
683            self.glyph_cache.advance_generation();
684            let evicted = self.glyph_cache.evict_unused();
685            glyphs_evicted = !evicted.is_empty();
686            for alloc_id in evicted {
687                self.atlas.deallocate(alloc_id);
688            }
689        }
690
691        let dirty = self.atlas.dirty;
692        let w = self.atlas.width;
693        let h = self.atlas.height;
694        let pixels = &self.atlas.pixels[..];
695        if dirty {
696            self.atlas.dirty = false;
697        }
698        (dirty, w, h, pixels, glyphs_evicted)
699    }
700
701    // ── Single-line layout ───────────────────────────────────────
702
703    /// Lay out a single line of text and return GPU-ready glyph quads.
704    ///
705    /// This is the fast path for simple labels, tooltips, overlays, and other
706    /// single-line text that does not need the full document layout pipeline.
707    ///
708    /// What it does:
709    /// - Resolves the font from `format` (family, weight, italic, size).
710    /// - Shapes the text with rustybuzz (including glyph fallback).
711    /// - Rasterizes glyphs into the atlas (same path as the full pipeline).
712    /// - If `max_width` is provided and the text exceeds it, truncates with
713    ///   an ellipsis character.
714    ///
715    /// What it skips:
716    /// - Line breaking (there is only one line).
717    /// - Bidi analysis (assumes a single direction run).
718    /// - Flow layout, margins, indents, block stacking.
719    ///
720    /// Glyph quads are positioned with the top-left at (0, 0).
721    pub fn layout_single_line(
722        &mut self,
723        text: &str,
724        format: &TextFormat,
725        max_width: Option<f32>,
726    ) -> SingleLineResult {
727        use crate::font::resolve::resolve_font;
728        use crate::shaping::shaper::{
729            bidi_runs, font_metrics_px, shape_text, shape_text_with_fallback,
730        };
731
732        let empty = SingleLineResult {
733            width: 0.0,
734            height: 0.0,
735            baseline: 0.0,
736            glyphs: Vec::new(),
737        };
738
739        if text.is_empty() {
740            return empty;
741        }
742
743        // Resolve font from TextFormat fields
744        let font_point_size = format.font_size.map(|s| s as u32);
745        let resolved = match resolve_font(
746            &self.font_registry,
747            format.font_family.as_deref(),
748            format.font_weight,
749            format.font_bold,
750            format.font_italic,
751            font_point_size,
752            self.scale_factor,
753        ) {
754            Some(r) => r,
755            None => return empty,
756        };
757
758        // Get font metrics for line height
759        let metrics = match font_metrics_px(&self.font_registry, &resolved) {
760            Some(m) => m,
761            None => return empty,
762        };
763        let line_height = metrics.ascent + metrics.descent + metrics.leading;
764        let baseline = metrics.ascent;
765
766        // Shape the text, split into bidi runs in visual order.
767        //
768        // Each directional run is shaped with its own explicit direction
769        // so rustybuzz cannot infer RTL from a strong Arabic/Hebrew char
770        // and reverse an embedded Latin cluster (UAX #9, rule L2).
771        //
772        // Runs are already in visual order — concatenating their glyphs
773        // left-to-right produces the correct visual line.
774        let runs: Vec<_> = bidi_runs(text)
775            .into_iter()
776            .filter_map(|br| {
777                let slice = text.get(br.byte_range.clone())?;
778                shape_text_with_fallback(
779                    &self.font_registry,
780                    &resolved,
781                    slice,
782                    br.byte_range.start,
783                    br.direction,
784                )
785            })
786            .collect();
787
788        if runs.is_empty() {
789            return empty;
790        }
791
792        let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
793
794        // Determine which glyphs to render (truncation with ellipsis if needed).
795        // Truncation operates on the visual-order glyph stream and cuts from
796        // the visual-end (right side), matching the pre-bidi behavior for the
797        // common single-direction case.
798        let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
799            && total_advance > max_w
800        {
801            let ellipsis_run = shape_text(&self.font_registry, &resolved, "\u{2026}", 0);
802            let ellipsis_width = ellipsis_run
803                .as_ref()
804                .map(|r| r.advance_width)
805                .unwrap_or(0.0);
806            let budget = (max_w - ellipsis_width).max(0.0);
807
808            let mut used = 0.0f32;
809            let mut count = 0usize;
810            'outer: for run in &runs {
811                for g in &run.glyphs {
812                    if used + g.x_advance > budget {
813                        break 'outer;
814                    }
815                    used += g.x_advance;
816                    count += 1;
817                }
818            }
819
820            (Some(count), used + ellipsis_width, ellipsis_run)
821        } else {
822            (None, total_advance, None)
823        };
824
825        // Rasterize glyphs in visual order and build GlyphQuads
826        let text_color = format.color.unwrap_or(self.text_color);
827        let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
828        let mut quads = Vec::with_capacity(glyph_capacity + 1);
829        let mut pen_x = 0.0f32;
830        let mut emitted = 0usize;
831
832        'emit: for run in &runs {
833            for glyph in &run.glyphs {
834                if let Some(limit) = truncate_at_visual_index
835                    && emitted >= limit
836                {
837                    break 'emit;
838                }
839                self.rasterize_glyph_quad(glyph, run, pen_x, baseline, text_color, &mut quads);
840                pen_x += glyph.x_advance;
841                emitted += 1;
842            }
843        }
844
845        // Render ellipsis glyphs if truncated
846        if let Some(ref e_run) = ellipsis_run {
847            for glyph in &e_run.glyphs {
848                self.rasterize_glyph_quad(glyph, e_run, pen_x, baseline, text_color, &mut quads);
849                pen_x += glyph.x_advance;
850            }
851        }
852
853        SingleLineResult {
854            width: final_width,
855            height: line_height,
856            baseline,
857            glyphs: quads,
858        }
859    }
860
861    /// Lay out a multi-line paragraph by wrapping text at `max_width`.
862    ///
863    /// This is the multi-line counterpart to [`layout_single_line`]. It shapes
864    /// the input text, breaks it into lines at unicode line-break opportunities
865    /// (greedy, left-aligned), and rasterizes each line's glyphs into
866    /// paragraph-local coordinates starting at `(0, 0)`.
867    ///
868    /// If `max_lines` is `Some(n)`, at most `n` lines are emitted — any
869    /// remaining lines are silently dropped. (A future extension may insert a
870    /// trailing ellipsis on the last emitted line; this is currently plain
871    /// truncation.)
872    ///
873    /// The caller is responsible for offsetting the returned glyph quads by
874    /// the paragraph's screen position.
875    ///
876    /// [`layout_single_line`]: Self::layout_single_line
877    pub fn layout_paragraph(
878        &mut self,
879        text: &str,
880        format: &TextFormat,
881        max_width: f32,
882        max_lines: Option<usize>,
883    ) -> ParagraphResult {
884        use crate::font::resolve::resolve_font;
885        use crate::layout::paragraph::{Alignment, break_into_lines};
886        use crate::shaping::shaper::{bidi_runs, font_metrics_px, shape_text_with_fallback};
887
888        let empty = ParagraphResult {
889            width: 0.0,
890            height: 0.0,
891            baseline_first: 0.0,
892            line_count: 0,
893            glyphs: Vec::new(),
894        };
895
896        if text.is_empty() || max_width <= 0.0 {
897            return empty;
898        }
899
900        // Resolve font and metrics (identical to layout_single_line's prelude).
901        let font_point_size = format.font_size.map(|s| s as u32);
902        let resolved = match resolve_font(
903            &self.font_registry,
904            format.font_family.as_deref(),
905            format.font_weight,
906            format.font_bold,
907            format.font_italic,
908            font_point_size,
909            self.scale_factor,
910        ) {
911            Some(r) => r,
912            None => return empty,
913        };
914
915        let metrics = match font_metrics_px(&self.font_registry, &resolved) {
916            Some(m) => m,
917            None => return empty,
918        };
919
920        // Shape the text into bidi-aware runs in visual order.
921        let runs: Vec<_> = bidi_runs(text)
922            .into_iter()
923            .filter_map(|br| {
924                let slice = text.get(br.byte_range.clone())?;
925                shape_text_with_fallback(
926                    &self.font_registry,
927                    &resolved,
928                    slice,
929                    br.byte_range.start,
930                    br.direction,
931                )
932            })
933            .collect();
934
935        if runs.is_empty() {
936            return empty;
937        }
938
939        // Break into lines using the existing greedy wrapper.
940        let lines = break_into_lines(runs, text, max_width, Alignment::Left, 0.0, &metrics);
941
942        // Cap at max_lines.
943        let line_count = match max_lines {
944            Some(n) => lines.len().min(n),
945            None => lines.len(),
946        };
947
948        // Walk the emitted lines, stacking them vertically and rasterizing
949        // glyphs into paragraph-local coordinates.
950        let text_color = format.color.unwrap_or(self.text_color);
951        let mut quads: Vec<GlyphQuad> = Vec::new();
952        let mut y_top = 0.0f32;
953        let mut max_line_width = 0.0f32;
954        let baseline_first = metrics.ascent;
955
956        for line in lines.iter().take(line_count) {
957            if line.width > max_line_width {
958                max_line_width = line.width;
959            }
960            let baseline_y = y_top + metrics.ascent;
961            for run in &line.runs {
962                let mut pen_x = run.x;
963                // Snapshot the glyph list so `rasterize_glyph_quad` can borrow
964                // `&mut self` without aliasing the shaped-run reference.
965                let run_copy = run.shaped_run.clone();
966                for glyph in &run_copy.glyphs {
967                    self.rasterize_glyph_quad(
968                        glyph, &run_copy, pen_x, baseline_y, text_color, &mut quads,
969                    );
970                    pen_x += glyph.x_advance;
971                }
972            }
973            y_top += metrics.ascent + metrics.descent + metrics.leading;
974        }
975
976        ParagraphResult {
977            width: max_line_width,
978            height: y_top,
979            baseline_first,
980            line_count,
981            glyphs: quads,
982        }
983    }
984
985    /// Rasterize a single glyph and append a GlyphQuad to the output vec.
986    ///
987    /// Shared helper for `layout_single_line`. Handles cache lookup,
988    /// rasterization on miss, and atlas allocation.
989    fn rasterize_glyph_quad(
990        &mut self,
991        glyph: &crate::shaping::run::ShapedGlyph,
992        run: &crate::shaping::run::ShapedRun,
993        pen_x: f32,
994        baseline: f32,
995        text_color: [f32; 4],
996        quads: &mut Vec<GlyphQuad>,
997    ) {
998        if glyph.glyph_id == 0 {
999            return;
1000        }
1001
1002        let entry = match self.font_registry.get(glyph.font_face_id) {
1003            Some(e) => e,
1004            None => return,
1005        };
1006
1007        let sf = self.scale_factor.max(f32::MIN_POSITIVE);
1008        let inv_sf = 1.0 / sf;
1009        let physical_size_px = run.size_px * sf;
1010        let cache_key = GlyphCacheKey::new(glyph.font_face_id, glyph.glyph_id, physical_size_px);
1011
1012        // Ensure glyph is cached (rasterize on miss)
1013        if self.glyph_cache.peek(&cache_key).is_none()
1014            && let Some(image) = rasterize_glyph(
1015                &mut self.scale_context,
1016                &entry.data,
1017                entry.face_index,
1018                entry.swash_cache_key,
1019                glyph.glyph_id,
1020                physical_size_px,
1021            )
1022            && image.width > 0
1023            && image.height > 0
1024            && let Some(alloc) = self.atlas.allocate(image.width, image.height)
1025        {
1026            let rect = alloc.rectangle;
1027            let atlas_x = rect.min.x as u32;
1028            let atlas_y = rect.min.y as u32;
1029            if image.is_color {
1030                self.atlas
1031                    .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1032            } else {
1033                self.atlas
1034                    .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1035            }
1036            self.glyph_cache.insert(
1037                cache_key,
1038                crate::atlas::cache::CachedGlyph {
1039                    alloc_id: alloc.id,
1040                    atlas_x,
1041                    atlas_y,
1042                    width: image.width,
1043                    height: image.height,
1044                    placement_left: image.placement_left,
1045                    placement_top: image.placement_top,
1046                    is_color: image.is_color,
1047                    last_used: 0,
1048                },
1049            );
1050        }
1051
1052        if let Some(cached) = self.glyph_cache.get(&cache_key) {
1053            // CachedGlyph dims are physical; quad screen rect is logical.
1054            let logical_w = cached.width as f32 * inv_sf;
1055            let logical_h = cached.height as f32 * inv_sf;
1056            let logical_left = cached.placement_left as f32 * inv_sf;
1057            let logical_top = cached.placement_top as f32 * inv_sf;
1058            let screen_x = pen_x + glyph.x_offset + logical_left;
1059            let screen_y = baseline - glyph.y_offset - logical_top;
1060            let color = if cached.is_color {
1061                [1.0, 1.0, 1.0, 1.0]
1062            } else {
1063                text_color
1064            };
1065            quads.push(GlyphQuad {
1066                screen: [screen_x, screen_y, logical_w, logical_h],
1067                atlas: [
1068                    cached.atlas_x as f32,
1069                    cached.atlas_y as f32,
1070                    cached.width as f32,
1071                    cached.height as f32,
1072                ],
1073                color,
1074            });
1075        }
1076    }
1077
1078    // ── Hit testing ─────────────────────────────────────────────
1079
1080    /// Map a screen-space point to a document position.
1081    ///
1082    /// Coordinates are relative to the widget's top-left corner.
1083    /// The scroll offset is accounted for internally.
1084    /// Returns `None` if the flow has no content.
1085    pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1086        crate::render::hit_test::hit_test(
1087            &self.flow_layout,
1088            self.scroll_offset,
1089            x / self.zoom,
1090            y / self.zoom,
1091        )
1092    }
1093
1094    /// Get the screen-space caret rectangle at a document position.
1095    ///
1096    /// Returns `[x, y, width, height]` in screen pixels. Use this to report
1097    /// the caret position to the platform IME system for composition window
1098    /// placement. For drawing the caret, use the [`crate::DecorationKind::Cursor`]
1099    /// entry in [`crate::RenderFrame::decorations`] instead.
1100    pub fn caret_rect(&self, position: usize) -> [f32; 4] {
1101        let mut rect =
1102            crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1103        rect[0] *= self.zoom;
1104        rect[1] *= self.zoom;
1105        rect[2] *= self.zoom;
1106        rect[3] *= self.zoom;
1107        rect
1108    }
1109
1110    // ── Cursor display ──────────────────────────────────────────
1111
1112    /// Update the cursor display state for a single cursor.
1113    ///
1114    /// The adapter reads `position` and `anchor` from text-document's
1115    /// `TextCursor`, toggles `visible` on a blink timer, and passes
1116    /// the result here. The typesetter includes cursor and selection
1117    /// decorations in the next [`render`](Self::render) call.
1118    pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1119        self.cursors = vec![CursorDisplay {
1120            position: cursor.position,
1121            anchor: cursor.anchor,
1122            visible: cursor.visible,
1123            selected_cells: cursor.selected_cells.clone(),
1124        }];
1125    }
1126
1127    /// Update multiple cursors (multi-cursor editing support).
1128    ///
1129    /// Each cursor independently generates a caret and optional selection highlight.
1130    pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1131        self.cursors = cursors
1132            .iter()
1133            .map(|c| CursorDisplay {
1134                position: c.position,
1135                anchor: c.anchor,
1136                visible: c.visible,
1137                selected_cells: c.selected_cells.clone(),
1138            })
1139            .collect();
1140    }
1141
1142    /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
1143    ///
1144    /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
1145    pub fn set_selection_color(&mut self, color: [f32; 4]) {
1146        self.selection_color = color;
1147    }
1148
1149    /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
1150    ///
1151    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
1152    pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1153        self.cursor_color = color;
1154    }
1155
1156    /// Set the default text color (`[r, g, b, a]`, 0.0-1.0).
1157    ///
1158    /// This color is used for glyphs and decorations (underline, strikeout, overline)
1159    /// when no per-fragment `foreground_color` is set.
1160    ///
1161    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
1162    pub fn set_text_color(&mut self, color: [f32; 4]) {
1163        self.text_color = color;
1164    }
1165
1166    // ── Scrolling ───────────────────────────────────────────────
1167
1168    /// Get the visual position and height of a laid-out block.
1169    ///
1170    /// Returns `None` if the block ID is not in the current layout.
1171    pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1172        let block = self.flow_layout.blocks.get(&block_id)?;
1173        Some(BlockVisualInfo {
1174            block_id,
1175            y: block.y,
1176            height: block.height,
1177        })
1178    }
1179
1180    /// Check whether a block belongs to a table cell.
1181    ///
1182    /// Returns `true` if `block_id` is found in any table cell layout,
1183    /// `false` if it is a top-level or frame block (or unknown).
1184    pub fn is_block_in_table(&self, block_id: usize) -> bool {
1185        self.flow_layout.tables.values().any(|table| {
1186            table
1187                .cell_layouts
1188                .iter()
1189                .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1190        })
1191    }
1192
1193    /// Scroll so that the given document position is visible, placing it
1194    /// roughly 1/3 from the top of the viewport.
1195    ///
1196    /// Returns the new scroll offset.
1197    pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1198        let rect =
1199            crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
1200        let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1201        self.scroll_offset = target_y.max(0.0);
1202        self.scroll_offset
1203    }
1204
1205    /// Scroll the minimum amount needed to make the current caret visible.
1206    ///
1207    /// Call after cursor movement (arrow keys, click, typing) to keep
1208    /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
1209    /// or `None` if the caret was already visible.
1210    pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1211        if self.cursors.is_empty() {
1212            return None;
1213        }
1214        let pos = self.cursors[0].position;
1215        // Work in 1x (document) coordinates so scroll_offset stays in document space
1216        let rect = crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, pos);
1217        let caret_screen_y = rect[1];
1218        let caret_screen_bottom = caret_screen_y + rect[3];
1219        let effective_vh = self.viewport_height / self.zoom;
1220        let margin = 10.0 / self.zoom;
1221        let old_offset = self.scroll_offset;
1222
1223        if caret_screen_y < 0.0 {
1224            self.scroll_offset += caret_screen_y - margin;
1225            self.scroll_offset = self.scroll_offset.max(0.0);
1226        } else if caret_screen_bottom > effective_vh {
1227            self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1228        }
1229
1230        if (self.scroll_offset - old_offset).abs() > 0.001 {
1231            Some(self.scroll_offset)
1232        } else {
1233            None
1234        }
1235    }
1236}
1237
1238#[cfg(feature = "text-document")]
1239enum FlowItemKind {
1240    Block(crate::layout::block::BlockLayoutParams),
1241    Table(crate::layout::table::TableLayoutParams),
1242    Frame(crate::layout::frame::FrameLayoutParams),
1243}
1244
1245/// Scale all screen-space coordinates in a RenderFrame by the zoom factor.
1246fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1247    if (zoom - 1.0).abs() <= f32::EPSILON {
1248        return;
1249    }
1250    for q in &mut frame.glyphs {
1251        q.screen[0] *= zoom;
1252        q.screen[1] *= zoom;
1253        q.screen[2] *= zoom;
1254        q.screen[3] *= zoom;
1255    }
1256    for q in &mut frame.images {
1257        q.screen[0] *= zoom;
1258        q.screen[1] *= zoom;
1259        q.screen[2] *= zoom;
1260        q.screen[3] *= zoom;
1261    }
1262    apply_zoom_decorations(&mut frame.decorations, zoom);
1263}
1264
1265/// Scale all screen-space coordinates in decoration rects by the zoom factor.
1266fn apply_zoom_decorations(decorations: &mut [crate::types::DecorationRect], zoom: f32) {
1267    if (zoom - 1.0).abs() <= f32::EPSILON {
1268        return;
1269    }
1270    for d in decorations.iter_mut() {
1271        d.rect[0] *= zoom;
1272        d.rect[1] *= zoom;
1273        d.rect[2] *= zoom;
1274        d.rect[3] *= zoom;
1275    }
1276}
1277
1278impl Default for Typesetter {
1279    fn default() -> Self {
1280        Self::new()
1281    }
1282}