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::{FlowItem, 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    rendered_scroll_offset: f32,
52    viewport_width: f32,
53    viewport_height: f32,
54    content_width_mode: ContentWidthMode,
55    selection_color: [f32; 4],
56    cursor_color: [f32; 4],
57    text_color: [f32; 4],
58    cursors: Vec<CursorDisplay>,
59    zoom: f32,
60    rendered_zoom: f32,
61}
62
63impl Typesetter {
64    /// Create a new typesetter with no fonts loaded.
65    ///
66    /// Call [`register_font`](Self::register_font) and [`set_default_font`](Self::set_default_font)
67    /// before laying out any content.
68    pub fn new() -> Self {
69        Self {
70            font_registry: FontRegistry::new(),
71            atlas: GlyphAtlas::new(),
72            glyph_cache: GlyphCache::new(),
73            flow_layout: FlowLayout::new(),
74            scale_context: swash::scale::ScaleContext::new(),
75            render_frame: RenderFrame::new(),
76            scroll_offset: 0.0,
77            rendered_scroll_offset: f32::NAN,
78            viewport_width: 0.0,
79            viewport_height: 0.0,
80            content_width_mode: ContentWidthMode::Auto,
81            selection_color: [0.26, 0.52, 0.96, 0.3],
82            cursor_color: [0.0, 0.0, 0.0, 1.0],
83            text_color: [0.0, 0.0, 0.0, 1.0],
84            cursors: Vec::new(),
85            zoom: 1.0,
86            rendered_zoom: f32::NAN,
87        }
88    }
89
90    // ── Font registration ───────────────────────────────────────
91
92    /// Register a font face from raw TTF/OTF/WOFF bytes.
93    ///
94    /// Parses the font's name table to extract family, weight, and style,
95    /// then indexes it for CSS-spec font matching via [`fontdb`].
96    /// Returns the first face ID (font collections like `.ttc` may contain multiple faces).
97    ///
98    /// # Panics
99    ///
100    /// Panics if the font data contains no parseable faces.
101    pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
102        let ids = self.font_registry.register_font(data);
103        ids.into_iter()
104            .next()
105            .expect("font data contained no faces")
106    }
107
108    /// Register a font with explicit metadata, overriding the font's name table.
109    ///
110    /// Use when the font's internal metadata is unreliable or when aliasing
111    /// a font to a different family name.
112    ///
113    /// # Panics
114    ///
115    /// Panics if the font data contains no parseable faces.
116    pub fn register_font_as(
117        &mut self,
118        data: &[u8],
119        family: &str,
120        weight: u16,
121        italic: bool,
122    ) -> FontFaceId {
123        let ids = self
124            .font_registry
125            .register_font_as(data, family, weight, italic);
126        ids.into_iter()
127            .next()
128            .expect("font data contained no faces")
129    }
130
131    /// Set which font face to use as the document default.
132    ///
133    /// This is the fallback font when a fragment's `TextFormat` doesn't specify
134    /// a family or the specified family isn't found.
135    pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
136        self.font_registry.set_default_font(face, size_px);
137    }
138
139    /// Map a generic family name to a registered font family.
140    ///
141    /// Common mappings: `"serif"` → `"Noto Serif"`, `"monospace"` → `"Fira Code"`.
142    /// When text-document specifies `font_family: "monospace"`, the typesetter
143    /// resolves it through this mapping before querying fontdb.
144    pub fn set_generic_family(&mut self, generic: &str, family: &str) {
145        self.font_registry.set_generic_family(generic, family);
146    }
147
148    /// Look up the family name of a registered font by its face ID.
149    pub fn font_family_name(&self, face_id: FontFaceId) -> Option<String> {
150        self.font_registry.font_family_name(face_id)
151    }
152
153    /// Access the font registry for advanced queries (glyph coverage, fallback, etc.).
154    pub fn font_registry(&self) -> &FontRegistry {
155        &self.font_registry
156    }
157
158    // ── Viewport & content width ───────────────────────────────
159
160    /// Set the viewport dimensions (visible area in pixels).
161    ///
162    /// The viewport controls:
163    /// - **Culling**: only blocks within the viewport are rendered.
164    /// - **Selection highlight**: multi-line selections extend to viewport width.
165    /// - **Layout width** (in [`ContentWidthMode::Auto`]): text wraps at viewport width.
166    ///
167    /// Call this when the window or container resizes.
168    pub fn set_viewport(&mut self, width: f32, height: f32) {
169        self.viewport_width = width;
170        self.viewport_height = height;
171        self.flow_layout.viewport_width = width;
172        self.flow_layout.viewport_height = height;
173    }
174
175    /// Set a fixed content width, independent of viewport.
176    ///
177    /// Text wraps at this width regardless of how wide the viewport is.
178    /// Use for page-like (WYSIWYG) layout or documents with explicit width.
179    /// Pass `f32::INFINITY` for no-wrap mode.
180    pub fn set_content_width(&mut self, width: f32) {
181        self.content_width_mode = ContentWidthMode::Fixed(width);
182    }
183
184    /// Set content width to follow viewport width (default).
185    ///
186    /// Text reflows when the viewport is resized. This is the standard
187    /// behavior for editors and web-style layout.
188    pub fn set_content_width_auto(&mut self) {
189        self.content_width_mode = ContentWidthMode::Auto;
190    }
191
192    /// The effective width used for text layout (line wrapping, table columns, etc.).
193    ///
194    /// In [`ContentWidthMode::Auto`], equals `viewport_width / zoom` so that
195    /// text reflows to fit the zoomed viewport.
196    /// In [`ContentWidthMode::Fixed`], equals the set value (zoom only magnifies).
197    pub fn layout_width(&self) -> f32 {
198        match self.content_width_mode {
199            ContentWidthMode::Auto => self.viewport_width / self.zoom,
200            ContentWidthMode::Fixed(w) => w,
201        }
202    }
203
204    /// Set the vertical scroll offset in pixels from the top of the document.
205    ///
206    /// Affects which blocks are visible (culling) and the screen-space
207    /// y coordinates in the rendered [`RenderFrame`].
208    pub fn set_scroll_offset(&mut self, offset: f32) {
209        self.scroll_offset = offset;
210    }
211
212    /// Total content height after layout, in pixels.
213    ///
214    /// Use for scrollbar range: `scrollbar.max = content_height - viewport_height`.
215    pub fn content_height(&self) -> f32 {
216        self.flow_layout.content_height
217    }
218
219    /// Maximum content width across all laid-out lines, in pixels.
220    ///
221    /// Use for horizontal scrollbar range when text wrapping is disabled.
222    /// Returns 0.0 if no blocks have been laid out.
223    pub fn max_content_width(&self) -> f32 {
224        self.flow_layout.cached_max_content_width
225    }
226
227    // -- Zoom ────────────────────────────────────────────────────
228
229    /// Set the display zoom level.
230    ///
231    /// Zoom is a pure display transform: layout stays at base size, and all
232    /// screen-space output (glyph quads, decorations, caret rects) is scaled
233    /// by the zoom factor. Hit-test input coordinates are inversely scaled.
234    ///
235    /// This is PDF-viewer-style zoom (no text reflow). For browser-style
236    /// zoom that reflows text, combine with
237    /// `set_content_width(viewport_width / zoom)`.
238    ///
239    /// Clamped to `0.1..=10.0`. Default is `1.0`.
240    pub fn set_zoom(&mut self, zoom: f32) {
241        self.zoom = zoom.clamp(0.1, 10.0);
242    }
243
244    /// The current display zoom level (default 1.0).
245    pub fn zoom(&self) -> f32 {
246        self.zoom
247    }
248
249    // ── Layout ──────────────────────────────────────────────────
250
251    /// Full layout from a text-document `FlowSnapshot`.
252    ///
253    /// Converts all snapshot elements (blocks, tables, frames) to internal
254    /// layout params and lays out the entire document flow. Call this on
255    /// `DocumentReset` events or initial document load.
256    ///
257    /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
258    #[cfg(feature = "text-document")]
259    pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
260        use crate::bridge::convert_flow;
261
262        let converted = convert_flow(flow);
263
264        // Merge all elements by flow index and process in order
265        let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
266        for (idx, params) in converted.blocks {
267            all_items.push((idx, FlowItemKind::Block(params)));
268        }
269        for (idx, params) in converted.tables {
270            all_items.push((idx, FlowItemKind::Table(params)));
271        }
272        for (idx, params) in converted.frames {
273            all_items.push((idx, FlowItemKind::Frame(params)));
274        }
275        all_items.sort_by_key(|(idx, _)| *idx);
276
277        let lw = self.layout_width();
278        self.flow_layout.clear();
279        self.flow_layout.viewport_width = self.viewport_width;
280        self.flow_layout.viewport_height = self.viewport_height;
281
282        for (_idx, kind) in all_items {
283            match kind {
284                FlowItemKind::Block(params) => {
285                    self.flow_layout.add_block(&self.font_registry, &params, lw);
286                }
287                FlowItemKind::Table(params) => {
288                    self.flow_layout.add_table(&self.font_registry, &params, lw);
289                }
290                FlowItemKind::Frame(params) => {
291                    self.flow_layout.add_frame(&self.font_registry, &params, lw);
292                }
293            }
294        }
295    }
296
297    /// Lay out a list of blocks from scratch (framework-agnostic API).
298    ///
299    /// Replaces all existing layout state with the given blocks.
300    /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
301    /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
302    pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
303        self.flow_layout
304            .layout_blocks(&self.font_registry, block_params, self.layout_width());
305    }
306
307    /// Add a frame to the current flow layout.
308    ///
309    /// The frame is placed after all previously laid-out content.
310    /// Frame position (inline, float, absolute) is determined by
311    /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
312    pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
313        self.flow_layout
314            .add_frame(&self.font_registry, params, self.layout_width());
315    }
316
317    /// Add a table to the current flow layout.
318    ///
319    /// The table is placed after all previously laid-out content.
320    pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
321        self.flow_layout
322            .add_table(&self.font_registry, params, self.layout_width());
323    }
324
325    /// Relayout a single block after its content or formatting changed.
326    ///
327    /// Re-shapes and re-wraps the block, then shifts subsequent blocks
328    /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
329    /// for single-block edits (typing, formatting changes).
330    ///
331    /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
332    /// the table row height is re-measured and content below the table shifts.
333    pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
334        self.flow_layout
335            .relayout_block(&self.font_registry, params, self.layout_width());
336    }
337
338    // ── Rendering ───────────────────────────────────────────────
339
340    /// Render the visible viewport and return everything needed to draw.
341    ///
342    /// Performs viewport culling (only processes blocks within the scroll window),
343    /// rasterizes any new glyphs into the atlas, and produces glyph quads,
344    /// image placeholders, and decoration rectangles.
345    ///
346    /// The returned reference borrows the `Typesetter`. The adapter should iterate
347    /// the frame for drawing, then drop the reference before calling any
348    /// layout/scroll methods on the next frame.
349    ///
350    /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
351    /// atlas to reclaim space.
352    pub fn render(&mut self) -> &RenderFrame {
353        let effective_vw = self.viewport_width / self.zoom;
354        let effective_vh = self.viewport_height / self.zoom;
355        crate::render::frame::build_render_frame(
356            &self.flow_layout,
357            &self.font_registry,
358            &mut self.atlas,
359            &mut self.glyph_cache,
360            &mut self.scale_context,
361            self.scroll_offset,
362            effective_vw,
363            effective_vh,
364            &self.cursors,
365            self.cursor_color,
366            self.selection_color,
367            self.text_color,
368            &mut self.render_frame,
369        );
370        self.rendered_scroll_offset = self.scroll_offset;
371        self.rendered_zoom = self.zoom;
372        apply_zoom(&mut self.render_frame, self.zoom);
373        &self.render_frame
374    }
375
376    /// Incremental render that only re-renders one block's glyphs.
377    ///
378    /// Reuses cached glyph/decoration data for all other blocks from the
379    /// last full `render()`. Use after `relayout_block()` when only one
380    /// block's text changed.
381    ///
382    /// If the block's height changed (causing subsequent blocks to shift),
383    /// this falls back to a full `render()` since cached glyph positions
384    /// for other blocks would be stale.
385    pub fn render_block_only(&mut self, block_id: usize) -> &RenderFrame {
386        // If scroll offset or zoom changed, all cached glyph positions are stale.
387        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
388            || (self.zoom - self.rendered_zoom).abs() > 0.001
389        {
390            return self.render();
391        }
392
393        // Table cell blocks are cached per-table (keyed by table_id), and
394        // frame blocks are cached per-frame (keyed by frame_id). Neither has
395        // entries in block_decorations or block_glyphs keyed by the cell
396        // block_id, so incremental rendering cannot update them in place.
397        // Fall back to a full render for both cases.
398        if !self.flow_layout.blocks.contains_key(&block_id) {
399            let in_table = self.flow_layout.tables.values().any(|table| {
400                table
401                    .cell_layouts
402                    .iter()
403                    .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
404            });
405            if in_table {
406                return self.render();
407            }
408            let in_frame = self
409                .flow_layout
410                .frames
411                .values()
412                .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
413            if in_frame {
414                return self.render();
415            }
416        }
417
418        // If the block's height changed, cached glyph positions for subsequent
419        // blocks are stale. Fall back to a full re-render.
420        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
421            let old_height = self
422                .render_frame
423                .block_heights
424                .get(&block_id)
425                .copied()
426                .unwrap_or(block.height);
427            if (block.height - old_height).abs() > 0.001 {
428                return self.render();
429            }
430        }
431
432        // Re-render just this block's glyphs into a temporary frame
433        let effective_vw = self.viewport_width / self.zoom;
434        let effective_vh = self.viewport_height / self.zoom;
435        let mut new_glyphs = Vec::new();
436        let mut new_images = Vec::new();
437        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
438            let mut tmp = crate::types::RenderFrame::new();
439            crate::render::frame::render_block_at_offset(
440                block,
441                0.0,
442                0.0,
443                &self.font_registry,
444                &mut self.atlas,
445                &mut self.glyph_cache,
446                &mut self.scale_context,
447                self.scroll_offset,
448                effective_vh,
449                self.text_color,
450                &mut tmp,
451            );
452            new_glyphs = tmp.glyphs;
453            new_images = tmp.images;
454        }
455
456        // Re-generate this block's decorations
457        let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
458            crate::render::decoration::generate_block_decorations(
459                block,
460                &self.font_registry,
461                self.scroll_offset,
462                effective_vh,
463                0.0,
464                0.0,
465                effective_vw,
466                self.text_color,
467            )
468        } else {
469            Vec::new()
470        };
471
472        // Replace this block's entry in the per-block caches
473        if let Some(entry) = self
474            .render_frame
475            .block_glyphs
476            .iter_mut()
477            .find(|(id, _)| *id == block_id)
478        {
479            entry.1 = new_glyphs;
480        }
481        if let Some(entry) = self
482            .render_frame
483            .block_images
484            .iter_mut()
485            .find(|(id, _)| *id == block_id)
486        {
487            entry.1 = new_images;
488        }
489        if let Some(entry) = self
490            .render_frame
491            .block_decorations
492            .iter_mut()
493            .find(|(id, _)| *id == block_id)
494        {
495            entry.1 = new_decos;
496        }
497
498        // Rebuild flat vecs from per-block cache + cursor decorations
499        self.rebuild_flat_frame();
500        apply_zoom(&mut self.render_frame, self.zoom);
501
502        &self.render_frame
503    }
504
505    /// Lightweight render that only updates cursor/selection decorations.
506    ///
507    /// Reuses the existing glyph quads and images from the last full `render()`.
508    /// Use this when only the cursor blinked or selection changed, not the text.
509    ///
510    /// If the scroll offset changed since the last full render, falls back to
511    /// a full [`render`](Self::render) so that glyph positions are updated.
512    pub fn render_cursor_only(&mut self) -> &RenderFrame {
513        // If scroll offset or zoom changed, glyph quads are stale - need full re-render
514        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
515            || (self.zoom - self.rendered_zoom).abs() > 0.001
516        {
517            return self.render();
518        }
519
520        // Remove old cursor/selection decorations, keep block decorations
521        self.render_frame.decorations.retain(|d| {
522            !matches!(
523                d.kind,
524                crate::types::DecorationKind::Cursor
525                    | crate::types::DecorationKind::Selection
526                    | crate::types::DecorationKind::CellSelection
527            )
528        });
529
530        // Regenerate cursor/selection decorations at 1x, then zoom
531        let effective_vw = self.viewport_width / self.zoom;
532        let effective_vh = self.viewport_height / self.zoom;
533        let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
534            &self.flow_layout,
535            &self.cursors,
536            self.scroll_offset,
537            self.cursor_color,
538            self.selection_color,
539            effective_vw,
540            effective_vh,
541        );
542        apply_zoom_decorations(&mut cursor_decos, self.zoom);
543        self.render_frame.decorations.extend(cursor_decos);
544
545        &self.render_frame
546    }
547
548    /// Rebuild flat glyphs/images/decorations from per-block caches + cursor decorations.
549    fn rebuild_flat_frame(&mut self) {
550        self.render_frame.glyphs.clear();
551        self.render_frame.images.clear();
552        self.render_frame.decorations.clear();
553        for (_, glyphs) in &self.render_frame.block_glyphs {
554            self.render_frame.glyphs.extend_from_slice(glyphs);
555        }
556        for (_, images) in &self.render_frame.block_images {
557            self.render_frame.images.extend_from_slice(images);
558        }
559        for (_, decos) in &self.render_frame.block_decorations {
560            self.render_frame.decorations.extend_from_slice(decos);
561        }
562
563        // Regenerate table and frame decorations (these are not stored in
564        // per-block caches, only in the flat decorations vec during full render).
565        for item in &self.flow_layout.flow_order {
566            match item {
567                FlowItem::Table { table_id, .. } => {
568                    if let Some(table) = self.flow_layout.tables.get(table_id) {
569                        let decos = crate::layout::table::generate_table_decorations(
570                            table,
571                            self.scroll_offset,
572                        );
573                        self.render_frame.decorations.extend(decos);
574                    }
575                }
576                FlowItem::Frame { frame_id, .. } => {
577                    if let Some(frame) = self.flow_layout.frames.get(frame_id) {
578                        crate::render::frame::append_frame_border_decorations(
579                            frame,
580                            self.scroll_offset,
581                            &mut self.render_frame.decorations,
582                        );
583                    }
584                }
585                FlowItem::Block { .. } => {}
586            }
587        }
588
589        let effective_vw = self.viewport_width / self.zoom;
590        let effective_vh = self.viewport_height / self.zoom;
591        let cursor_decos = crate::render::cursor::generate_cursor_decorations(
592            &self.flow_layout,
593            &self.cursors,
594            self.scroll_offset,
595            self.cursor_color,
596            self.selection_color,
597            effective_vw,
598            effective_vh,
599        );
600        self.render_frame.decorations.extend(cursor_decos);
601
602        // Update atlas metadata
603        self.render_frame.atlas_dirty = self.atlas.dirty;
604        self.render_frame.atlas_width = self.atlas.width;
605        self.render_frame.atlas_height = self.atlas.height;
606        if self.atlas.dirty {
607            let pixels = &self.atlas.pixels;
608            let needed = (self.atlas.width * self.atlas.height * 4) as usize;
609            self.render_frame.atlas_pixels.resize(needed, 0);
610            let copy_len = needed.min(pixels.len());
611            self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
612            self.atlas.dirty = false;
613        }
614    }
615
616    // ── Hit testing ─────────────────────────────────────────────
617
618    /// Map a screen-space point to a document position.
619    ///
620    /// Coordinates are relative to the widget's top-left corner.
621    /// The scroll offset is accounted for internally.
622    /// Returns `None` if the flow has no content.
623    pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
624        crate::render::hit_test::hit_test(
625            &self.flow_layout,
626            self.scroll_offset,
627            x / self.zoom,
628            y / self.zoom,
629        )
630    }
631
632    /// Get the screen-space caret rectangle at a document position.
633    ///
634    /// Returns `[x, y, width, height]` in screen pixels. Use this to report
635    /// the caret position to the platform IME system for composition window
636    /// placement. For drawing the caret, use the [`crate::DecorationKind::Cursor`]
637    /// entry in [`crate::RenderFrame::decorations`] instead.
638    pub fn caret_rect(&self, position: usize) -> [f32; 4] {
639        let mut rect =
640            crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
641        rect[0] *= self.zoom;
642        rect[1] *= self.zoom;
643        rect[2] *= self.zoom;
644        rect[3] *= self.zoom;
645        rect
646    }
647
648    // ── Cursor display ──────────────────────────────────────────
649
650    /// Update the cursor display state for a single cursor.
651    ///
652    /// The adapter reads `position` and `anchor` from text-document's
653    /// `TextCursor`, toggles `visible` on a blink timer, and passes
654    /// the result here. The typesetter includes cursor and selection
655    /// decorations in the next [`render`](Self::render) call.
656    pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
657        self.cursors = vec![CursorDisplay {
658            position: cursor.position,
659            anchor: cursor.anchor,
660            visible: cursor.visible,
661            selected_cells: cursor.selected_cells.clone(),
662        }];
663    }
664
665    /// Update multiple cursors (multi-cursor editing support).
666    ///
667    /// Each cursor independently generates a caret and optional selection highlight.
668    pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
669        self.cursors = cursors
670            .iter()
671            .map(|c| CursorDisplay {
672                position: c.position,
673                anchor: c.anchor,
674                visible: c.visible,
675                selected_cells: c.selected_cells.clone(),
676            })
677            .collect();
678    }
679
680    /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
681    ///
682    /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
683    pub fn set_selection_color(&mut self, color: [f32; 4]) {
684        self.selection_color = color;
685    }
686
687    /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
688    ///
689    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
690    pub fn set_cursor_color(&mut self, color: [f32; 4]) {
691        self.cursor_color = color;
692    }
693
694    /// Set the default text color (`[r, g, b, a]`, 0.0-1.0).
695    ///
696    /// This color is used for glyphs and decorations (underline, strikeout, overline)
697    /// when no per-fragment `foreground_color` is set.
698    ///
699    /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
700    pub fn set_text_color(&mut self, color: [f32; 4]) {
701        self.text_color = color;
702    }
703
704    // ── Scrolling ───────────────────────────────────────────────
705
706    /// Get the visual position and height of a laid-out block.
707    ///
708    /// Returns `None` if the block ID is not in the current layout.
709    pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
710        let block = self.flow_layout.blocks.get(&block_id)?;
711        Some(BlockVisualInfo {
712            block_id,
713            y: block.y,
714            height: block.height,
715        })
716    }
717
718    /// Check whether a block belongs to a table cell.
719    ///
720    /// Returns `true` if `block_id` is found in any table cell layout,
721    /// `false` if it is a top-level or frame block (or unknown).
722    pub fn is_block_in_table(&self, block_id: usize) -> bool {
723        self.flow_layout.tables.values().any(|table| {
724            table
725                .cell_layouts
726                .iter()
727                .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
728        })
729    }
730
731    /// Scroll so that the given document position is visible, placing it
732    /// roughly 1/3 from the top of the viewport.
733    ///
734    /// Returns the new scroll offset.
735    pub fn scroll_to_position(&mut self, position: usize) -> f32 {
736        let rect =
737            crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position);
738        let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
739        self.scroll_offset = target_y.max(0.0);
740        self.scroll_offset
741    }
742
743    /// Scroll the minimum amount needed to make the current caret visible.
744    ///
745    /// Call after cursor movement (arrow keys, click, typing) to keep
746    /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
747    /// or `None` if the caret was already visible.
748    pub fn ensure_caret_visible(&mut self) -> Option<f32> {
749        if self.cursors.is_empty() {
750            return None;
751        }
752        let pos = self.cursors[0].position;
753        // Work in 1x (document) coordinates so scroll_offset stays in document space
754        let rect = crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, pos);
755        let caret_screen_y = rect[1];
756        let caret_screen_bottom = caret_screen_y + rect[3];
757        let effective_vh = self.viewport_height / self.zoom;
758        let margin = 10.0 / self.zoom;
759        let old_offset = self.scroll_offset;
760
761        if caret_screen_y < 0.0 {
762            self.scroll_offset += caret_screen_y - margin;
763            self.scroll_offset = self.scroll_offset.max(0.0);
764        } else if caret_screen_bottom > effective_vh {
765            self.scroll_offset += caret_screen_bottom - effective_vh + margin;
766        }
767
768        if (self.scroll_offset - old_offset).abs() > 0.001 {
769            Some(self.scroll_offset)
770        } else {
771            None
772        }
773    }
774}
775
776#[cfg(feature = "text-document")]
777enum FlowItemKind {
778    Block(crate::layout::block::BlockLayoutParams),
779    Table(crate::layout::table::TableLayoutParams),
780    Frame(crate::layout::frame::FrameLayoutParams),
781}
782
783/// Scale all screen-space coordinates in a RenderFrame by the zoom factor.
784fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
785    if (zoom - 1.0).abs() <= f32::EPSILON {
786        return;
787    }
788    for q in &mut frame.glyphs {
789        q.screen[0] *= zoom;
790        q.screen[1] *= zoom;
791        q.screen[2] *= zoom;
792        q.screen[3] *= zoom;
793    }
794    for q in &mut frame.images {
795        q.screen[0] *= zoom;
796        q.screen[1] *= zoom;
797        q.screen[2] *= zoom;
798        q.screen[3] *= zoom;
799    }
800    apply_zoom_decorations(&mut frame.decorations, zoom);
801}
802
803/// Scale all screen-space coordinates in decoration rects by the zoom factor.
804fn apply_zoom_decorations(decorations: &mut [crate::types::DecorationRect], zoom: f32) {
805    if (zoom - 1.0).abs() <= f32::EPSILON {
806        return;
807    }
808    for d in decorations.iter_mut() {
809        d.rect[0] *= zoom;
810        d.rect[1] *= zoom;
811        d.rect[2] *= zoom;
812        d.rect[3] *= zoom;
813    }
814}
815
816impl Default for Typesetter {
817    fn default() -> Self {
818        Self::new()
819    }
820}