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, ¶ms, lw);
337 }
338 FlowItemKind::Table(params) => {
339 self.flow_layout.add_table(&self.font_registry, ¶ms, lw);
340 }
341 FlowItemKind::Frame(params) => {
342 self.flow_layout.add_frame(&self.font_registry, ¶ms, 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}