text_typeset/typesetter.rs
1use crate::atlas::allocator::GlyphAtlas;
2use crate::atlas::cache::GlyphCache;
3use crate::font::registry::FontRegistry;
4use crate::layout::flow::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 viewport_width: f32,
52 viewport_height: f32,
53 content_width_mode: ContentWidthMode,
54 selection_color: [f32; 4],
55 cursor_color: [f32; 4],
56 cursors: Vec<CursorDisplay>,
57}
58
59impl Typesetter {
60 /// Create a new typesetter with no fonts loaded.
61 ///
62 /// Call [`register_font`](Self::register_font) and [`set_default_font`](Self::set_default_font)
63 /// before laying out any content.
64 pub fn new() -> Self {
65 Self {
66 font_registry: FontRegistry::new(),
67 atlas: GlyphAtlas::new(),
68 glyph_cache: GlyphCache::new(),
69 flow_layout: FlowLayout::new(),
70 scale_context: swash::scale::ScaleContext::new(),
71 render_frame: RenderFrame::new(),
72 scroll_offset: 0.0,
73 viewport_width: 0.0,
74 viewport_height: 0.0,
75 content_width_mode: ContentWidthMode::Auto,
76 selection_color: [0.26, 0.52, 0.96, 0.3],
77 cursor_color: [0.0, 0.0, 0.0, 1.0],
78 cursors: Vec::new(),
79 }
80 }
81
82 // ── Font registration ───────────────────────────────────────
83
84 /// Register a font face from raw TTF/OTF/WOFF bytes.
85 ///
86 /// Parses the font's name table to extract family, weight, and style,
87 /// then indexes it for CSS-spec font matching via [`fontdb`].
88 /// Returns the first face ID (font collections like `.ttc` may contain multiple faces).
89 ///
90 /// # Panics
91 ///
92 /// Panics if the font data contains no parseable faces.
93 pub fn register_font(&mut self, data: &[u8]) -> FontFaceId {
94 let ids = self.font_registry.register_font(data);
95 ids.into_iter()
96 .next()
97 .expect("font data contained no faces")
98 }
99
100 /// Register a font with explicit metadata, overriding the font's name table.
101 ///
102 /// Use when the font's internal metadata is unreliable or when aliasing
103 /// a font to a different family name.
104 ///
105 /// # Panics
106 ///
107 /// Panics if the font data contains no parseable faces.
108 pub fn register_font_as(
109 &mut self,
110 data: &[u8],
111 family: &str,
112 weight: u16,
113 italic: bool,
114 ) -> FontFaceId {
115 let ids = self
116 .font_registry
117 .register_font_as(data, family, weight, italic);
118 ids.into_iter()
119 .next()
120 .expect("font data contained no faces")
121 }
122
123 /// Set which font face to use as the document default.
124 ///
125 /// This is the fallback font when a fragment's `TextFormat` doesn't specify
126 /// a family or the specified family isn't found.
127 pub fn set_default_font(&mut self, face: FontFaceId, size_px: f32) {
128 self.font_registry.set_default_font(face, size_px);
129 }
130
131 /// Map a generic family name to a registered font family.
132 ///
133 /// Common mappings: `"serif"` → `"Noto Serif"`, `"monospace"` → `"Fira Code"`.
134 /// When text-document specifies `font_family: "monospace"`, the typesetter
135 /// resolves it through this mapping before querying fontdb.
136 pub fn set_generic_family(&mut self, generic: &str, family: &str) {
137 self.font_registry.set_generic_family(generic, family);
138 }
139
140 /// Access the font registry for advanced queries (glyph coverage, fallback, etc.).
141 pub fn font_registry(&self) -> &FontRegistry {
142 &self.font_registry
143 }
144
145 // ── Viewport & content width ───────────────────────────────
146
147 /// Set the viewport dimensions (visible area in pixels).
148 ///
149 /// The viewport controls:
150 /// - **Culling**: only blocks within the viewport are rendered.
151 /// - **Selection highlight**: multi-line selections extend to viewport width.
152 /// - **Layout width** (in [`ContentWidthMode::Auto`]): text wraps at viewport width.
153 ///
154 /// Call this when the window or container resizes.
155 pub fn set_viewport(&mut self, width: f32, height: f32) {
156 self.viewport_width = width;
157 self.viewport_height = height;
158 self.flow_layout.viewport_width = width;
159 self.flow_layout.viewport_height = height;
160 }
161
162 /// Set a fixed content width, independent of viewport.
163 ///
164 /// Text wraps at this width regardless of how wide the viewport is.
165 /// Use for page-like (WYSIWYG) layout or documents with explicit width.
166 /// Pass `f32::INFINITY` for no-wrap mode.
167 pub fn set_content_width(&mut self, width: f32) {
168 self.content_width_mode = ContentWidthMode::Fixed(width);
169 }
170
171 /// Set content width to follow viewport width (default).
172 ///
173 /// Text reflows when the viewport is resized. This is the standard
174 /// behavior for editors and web-style layout.
175 pub fn set_content_width_auto(&mut self) {
176 self.content_width_mode = ContentWidthMode::Auto;
177 }
178
179 /// The effective width used for text layout (line wrapping, table columns, etc.).
180 ///
181 /// In [`ContentWidthMode::Auto`], equals viewport width.
182 /// In [`ContentWidthMode::Fixed`], equals the set value.
183 pub fn layout_width(&self) -> f32 {
184 match self.content_width_mode {
185 ContentWidthMode::Auto => self.viewport_width,
186 ContentWidthMode::Fixed(w) => w,
187 }
188 }
189
190 /// Set the vertical scroll offset in pixels from the top of the document.
191 ///
192 /// Affects which blocks are visible (culling) and the screen-space
193 /// y coordinates in the rendered [`RenderFrame`].
194 pub fn set_scroll_offset(&mut self, offset: f32) {
195 self.scroll_offset = offset;
196 }
197
198 /// Total content height after layout, in pixels.
199 ///
200 /// Use for scrollbar range: `scrollbar.max = content_height - viewport_height`.
201 pub fn content_height(&self) -> f32 {
202 self.flow_layout.content_height
203 }
204
205 /// Maximum content width across all laid-out lines, in pixels.
206 ///
207 /// Use for horizontal scrollbar range when text wrapping is disabled.
208 /// Returns 0.0 if no blocks have been laid out.
209 pub fn max_content_width(&self) -> f32 {
210 self.flow_layout.cached_max_content_width
211 }
212
213 // ── Layout ──────────────────────────────────────────────────
214
215 /// Full layout from a text-document `FlowSnapshot`.
216 ///
217 /// Converts all snapshot elements (blocks, tables, frames) to internal
218 /// layout params and lays out the entire document flow. Call this on
219 /// `DocumentReset` events or initial document load.
220 ///
221 /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
222 #[cfg(feature = "text-document")]
223 pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
224 use crate::bridge::convert_flow;
225
226 let converted = convert_flow(flow);
227
228 // Merge all elements by flow index and process in order
229 let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
230 for (idx, params) in converted.blocks {
231 all_items.push((idx, FlowItemKind::Block(params)));
232 }
233 for (idx, params) in converted.tables {
234 all_items.push((idx, FlowItemKind::Table(params)));
235 }
236 for (idx, params) in converted.frames {
237 all_items.push((idx, FlowItemKind::Frame(params)));
238 }
239 all_items.sort_by_key(|(idx, _)| *idx);
240
241 let lw = self.layout_width();
242 self.flow_layout.clear();
243 self.flow_layout.viewport_width = self.viewport_width;
244 self.flow_layout.viewport_height = self.viewport_height;
245
246 for (_idx, kind) in all_items {
247 match kind {
248 FlowItemKind::Block(params) => {
249 self.flow_layout.add_block(&self.font_registry, ¶ms, lw);
250 }
251 FlowItemKind::Table(params) => {
252 self.flow_layout.add_table(&self.font_registry, ¶ms, lw);
253 }
254 FlowItemKind::Frame(params) => {
255 self.flow_layout.add_frame(&self.font_registry, ¶ms, lw);
256 }
257 }
258 }
259 }
260
261 /// Lay out a list of blocks from scratch (framework-agnostic API).
262 ///
263 /// Replaces all existing layout state with the given blocks.
264 /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
265 /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
266 pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
267 self.flow_layout
268 .layout_blocks(&self.font_registry, block_params, self.layout_width());
269 }
270
271 /// Add a frame to the current flow layout.
272 ///
273 /// The frame is placed after all previously laid-out content.
274 /// Frame position (inline, float, absolute) is determined by
275 /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
276 pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
277 self.flow_layout
278 .add_frame(&self.font_registry, params, self.layout_width());
279 }
280
281 /// Add a table to the current flow layout.
282 ///
283 /// The table is placed after all previously laid-out content.
284 pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
285 self.flow_layout
286 .add_table(&self.font_registry, params, self.layout_width());
287 }
288
289 /// Relayout a single block after its content or formatting changed.
290 ///
291 /// Re-shapes and re-wraps the block, then shifts subsequent blocks
292 /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
293 /// for single-block edits (typing, formatting changes).
294 ///
295 /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
296 /// the table row height is re-measured and content below the table shifts.
297 pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
298 self.flow_layout
299 .relayout_block(&self.font_registry, params, self.layout_width());
300 }
301
302 // ── Rendering ───────────────────────────────────────────────
303
304 /// Render the visible viewport and return everything needed to draw.
305 ///
306 /// Performs viewport culling (only processes blocks within the scroll window),
307 /// rasterizes any new glyphs into the atlas, and produces glyph quads,
308 /// image placeholders, and decoration rectangles.
309 ///
310 /// The returned reference borrows the `Typesetter`. The adapter should iterate
311 /// the frame for drawing, then drop the reference before calling any
312 /// layout/scroll methods on the next frame.
313 ///
314 /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
315 /// atlas to reclaim space.
316 pub fn render(&mut self) -> &RenderFrame {
317 crate::render::frame::build_render_frame(
318 &self.flow_layout,
319 &self.font_registry,
320 &mut self.atlas,
321 &mut self.glyph_cache,
322 &mut self.scale_context,
323 self.scroll_offset,
324 self.viewport_height,
325 &self.cursors,
326 self.cursor_color,
327 self.selection_color,
328 &mut self.render_frame,
329 );
330 &self.render_frame
331 }
332
333 /// Incremental render that only re-renders one block's glyphs.
334 ///
335 /// Reuses cached glyph/decoration data for all other blocks from the
336 /// last full `render()`. Use after `relayout_block()` when only one
337 /// block's text changed.
338 pub fn render_block_only(&mut self, block_id: usize) -> &RenderFrame {
339 // Re-render just this block's glyphs into a temporary frame
340 let mut new_glyphs = Vec::new();
341 let mut new_images = Vec::new();
342 if let Some(block) = self.flow_layout.blocks.get(&block_id) {
343 let mut tmp = crate::types::RenderFrame::new();
344 crate::render::frame::render_block_at_offset(
345 block,
346 0.0,
347 0.0,
348 &self.font_registry,
349 &mut self.atlas,
350 &mut self.glyph_cache,
351 &mut self.scale_context,
352 self.scroll_offset,
353 self.viewport_height,
354 &mut tmp,
355 );
356 new_glyphs = tmp.glyphs;
357 new_images = tmp.images;
358 }
359
360 // Re-generate this block's decorations
361 let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
362 crate::render::decoration::generate_block_decorations(
363 block,
364 &self.font_registry,
365 self.scroll_offset,
366 self.viewport_height,
367 0.0,
368 0.0,
369 self.flow_layout.viewport_width,
370 )
371 } else {
372 Vec::new()
373 };
374
375 // Replace this block's entry in the per-block caches
376 if let Some(entry) = self
377 .render_frame
378 .block_glyphs
379 .iter_mut()
380 .find(|(id, _)| *id == block_id)
381 {
382 entry.1 = new_glyphs;
383 }
384 if let Some(entry) = self
385 .render_frame
386 .block_images
387 .iter_mut()
388 .find(|(id, _)| *id == block_id)
389 {
390 entry.1 = new_images;
391 }
392 if let Some(entry) = self
393 .render_frame
394 .block_decorations
395 .iter_mut()
396 .find(|(id, _)| *id == block_id)
397 {
398 entry.1 = new_decos;
399 }
400
401 // Rebuild flat vecs from per-block cache + cursor decorations
402 self.rebuild_flat_frame();
403
404 &self.render_frame
405 }
406
407 /// Lightweight render that only updates cursor/selection decorations.
408 ///
409 /// Reuses the existing glyph quads and images from the last full `render()`.
410 /// Use this when only the cursor blinked or selection changed, not the text.
411 pub fn render_cursor_only(&mut self) -> &RenderFrame {
412 // Remove old cursor/selection decorations, keep block decorations
413 self.render_frame.decorations.retain(|d| {
414 !matches!(
415 d.kind,
416 crate::types::DecorationKind::Cursor | crate::types::DecorationKind::Selection
417 )
418 });
419
420 // Regenerate cursor/selection decorations
421 let cursor_decos = crate::render::cursor::generate_cursor_decorations(
422 &self.flow_layout,
423 &self.cursors,
424 self.scroll_offset,
425 self.cursor_color,
426 self.selection_color,
427 );
428 self.render_frame.decorations.extend(cursor_decos);
429
430 &self.render_frame
431 }
432
433 /// Rebuild flat glyphs/images/decorations from per-block caches + cursor decorations.
434 fn rebuild_flat_frame(&mut self) {
435 self.render_frame.glyphs.clear();
436 self.render_frame.images.clear();
437 self.render_frame.decorations.clear();
438 for (_, glyphs) in &self.render_frame.block_glyphs {
439 self.render_frame.glyphs.extend_from_slice(glyphs);
440 }
441 for (_, images) in &self.render_frame.block_images {
442 self.render_frame.images.extend_from_slice(images);
443 }
444 for (_, decos) in &self.render_frame.block_decorations {
445 self.render_frame.decorations.extend_from_slice(decos);
446 }
447 let cursor_decos = crate::render::cursor::generate_cursor_decorations(
448 &self.flow_layout,
449 &self.cursors,
450 self.scroll_offset,
451 self.cursor_color,
452 self.selection_color,
453 );
454 self.render_frame.decorations.extend(cursor_decos);
455
456 // Update atlas metadata
457 self.render_frame.atlas_dirty = self.atlas.dirty;
458 self.render_frame.atlas_width = self.atlas.width;
459 self.render_frame.atlas_height = self.atlas.height;
460 if self.atlas.dirty {
461 let pixels = &self.atlas.pixels;
462 let needed = (self.atlas.width * self.atlas.height * 4) as usize;
463 self.render_frame.atlas_pixels.resize(needed, 0);
464 let copy_len = needed.min(pixels.len());
465 self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
466 self.atlas.dirty = false;
467 }
468 }
469
470 // ── Hit testing ─────────────────────────────────────────────
471
472 /// Map a screen-space point to a document position.
473 ///
474 /// Coordinates are relative to the widget's top-left corner.
475 /// The scroll offset is accounted for internally.
476 /// Returns `None` if the flow has no content.
477 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
478 crate::render::hit_test::hit_test(&self.flow_layout, self.scroll_offset, x, y)
479 }
480
481 /// Get the screen-space caret rectangle at a document position.
482 ///
483 /// Returns `[x, y, width, height]` in screen pixels. Use this to report
484 /// the caret position to the platform IME system for composition window
485 /// placement. For drawing the caret, use the [`crate::DecorationKind::Cursor`]
486 /// entry in [`crate::RenderFrame::decorations`] instead.
487 pub fn caret_rect(&self, position: usize) -> [f32; 4] {
488 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position)
489 }
490
491 // ── Cursor display ──────────────────────────────────────────
492
493 /// Update the cursor display state for a single cursor.
494 ///
495 /// The adapter reads `position` and `anchor` from text-document's
496 /// `TextCursor`, toggles `visible` on a blink timer, and passes
497 /// the result here. The typesetter includes cursor and selection
498 /// decorations in the next [`render`](Self::render) call.
499 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
500 self.cursors = vec![CursorDisplay {
501 position: cursor.position,
502 anchor: cursor.anchor,
503 visible: cursor.visible,
504 }];
505 }
506
507 /// Update multiple cursors (multi-cursor editing support).
508 ///
509 /// Each cursor independently generates a caret and optional selection highlight.
510 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
511 self.cursors = cursors
512 .iter()
513 .map(|c| CursorDisplay {
514 position: c.position,
515 anchor: c.anchor,
516 visible: c.visible,
517 })
518 .collect();
519 }
520
521 /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
522 ///
523 /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
524 pub fn set_selection_color(&mut self, color: [f32; 4]) {
525 self.selection_color = color;
526 }
527
528 /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
529 ///
530 /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
531 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
532 self.cursor_color = color;
533 }
534
535 // ── Scrolling ───────────────────────────────────────────────
536
537 /// Get the visual position and height of a laid-out block.
538 ///
539 /// Returns `None` if the block ID is not in the current layout.
540 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
541 let block = self.flow_layout.blocks.get(&block_id)?;
542 Some(BlockVisualInfo {
543 block_id,
544 y: block.y,
545 height: block.height,
546 })
547 }
548
549 /// Scroll so that the given document position is visible, placing it
550 /// roughly 1/3 from the top of the viewport.
551 ///
552 /// Returns the new scroll offset.
553 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
554 let rect = self.caret_rect(position);
555 let target_y = rect[1] + self.scroll_offset - self.viewport_height / 3.0;
556 self.scroll_offset = target_y.max(0.0);
557 self.scroll_offset
558 }
559
560 /// Scroll the minimum amount needed to make the current caret visible.
561 ///
562 /// Call after cursor movement (arrow keys, click, typing) to keep
563 /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
564 /// or `None` if the caret was already visible.
565 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
566 if self.cursors.is_empty() {
567 return None;
568 }
569 let pos = self.cursors[0].position;
570 let rect = self.caret_rect(pos);
571 let caret_screen_y = rect[1];
572 let caret_screen_bottom = caret_screen_y + rect[3];
573 let margin = 10.0;
574 let old_offset = self.scroll_offset;
575
576 if caret_screen_y < 0.0 {
577 self.scroll_offset += caret_screen_y - margin;
578 self.scroll_offset = self.scroll_offset.max(0.0);
579 } else if caret_screen_bottom > self.viewport_height {
580 self.scroll_offset += caret_screen_bottom - self.viewport_height + margin;
581 }
582
583 if (self.scroll_offset - old_offset).abs() > 0.001 {
584 Some(self.scroll_offset)
585 } else {
586 None
587 }
588 }
589}
590
591#[cfg(feature = "text-document")]
592enum FlowItemKind {
593 Block(crate::layout::block::BlockLayoutParams),
594 Table(crate::layout::table::TableLayoutParams),
595 Frame(crate::layout::frame::FrameLayoutParams),
596}
597
598impl Default for Typesetter {
599 fn default() -> Self {
600 Self::new()
601 }
602}