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