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