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, ¶ms, lw);
290 }
291 FlowItemKind::Table(params) => {
292 self.flow_layout.add_table(&self.font_registry, ¶ms, lw);
293 }
294 FlowItemKind::Frame(params) => {
295 self.flow_layout.add_frame(&self.font_registry, ¶ms, 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}