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 // ── Layout ──────────────────────────────────────────────────
206
207 /// Full layout from a text-document `FlowSnapshot`.
208 ///
209 /// Converts all snapshot elements (blocks, tables, frames) to internal
210 /// layout params and lays out the entire document flow. Call this on
211 /// `DocumentReset` events or initial document load.
212 ///
213 /// For incremental updates after small edits, prefer [`relayout_block`](Self::relayout_block).
214 #[cfg(feature = "text-document")]
215 pub fn layout_full(&mut self, flow: &text_document::FlowSnapshot) {
216 use crate::bridge::convert_flow;
217
218 let converted = convert_flow(flow);
219
220 // Merge all elements by flow index and process in order
221 let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
222 for (idx, params) in converted.blocks {
223 all_items.push((idx, FlowItemKind::Block(params)));
224 }
225 for (idx, params) in converted.tables {
226 all_items.push((idx, FlowItemKind::Table(params)));
227 }
228 for (idx, params) in converted.frames {
229 all_items.push((idx, FlowItemKind::Frame(params)));
230 }
231 all_items.sort_by_key(|(idx, _)| *idx);
232
233 let lw = self.layout_width();
234 self.flow_layout.clear();
235 self.flow_layout.viewport_width = self.viewport_width;
236 self.flow_layout.viewport_height = self.viewport_height;
237
238 for (_idx, kind) in all_items {
239 match kind {
240 FlowItemKind::Block(params) => {
241 self.flow_layout.add_block(&self.font_registry, ¶ms, lw);
242 }
243 FlowItemKind::Table(params) => {
244 self.flow_layout.add_table(&self.font_registry, ¶ms, lw);
245 }
246 FlowItemKind::Frame(params) => {
247 self.flow_layout.add_frame(&self.font_registry, ¶ms, lw);
248 }
249 }
250 }
251 }
252
253 /// Lay out a list of blocks from scratch (framework-agnostic API).
254 ///
255 /// Replaces all existing layout state with the given blocks.
256 /// This is the non-text-document equivalent of [`layout_full`](Self::layout_full).
257 /// the caller converts snapshot types to [`BlockLayoutParams`](crate::layout::block::BlockLayoutParams).
258 pub fn layout_blocks(&mut self, block_params: Vec<crate::layout::block::BlockLayoutParams>) {
259 self.flow_layout
260 .layout_blocks(&self.font_registry, block_params, self.layout_width());
261 }
262
263 /// Add a frame to the current flow layout.
264 ///
265 /// The frame is placed after all previously laid-out content.
266 /// Frame position (inline, float, absolute) is determined by
267 /// [`FrameLayoutParams::position`](crate::layout::frame::FrameLayoutParams).
268 pub fn add_frame(&mut self, params: &crate::layout::frame::FrameLayoutParams) {
269 self.flow_layout
270 .add_frame(&self.font_registry, params, self.layout_width());
271 }
272
273 /// Add a table to the current flow layout.
274 ///
275 /// The table is placed after all previously laid-out content.
276 pub fn add_table(&mut self, params: &crate::layout::table::TableLayoutParams) {
277 self.flow_layout
278 .add_table(&self.font_registry, params, self.layout_width());
279 }
280
281 /// Relayout a single block after its content or formatting changed.
282 ///
283 /// Re-shapes and re-wraps the block, then shifts subsequent blocks
284 /// if the height changed. Much cheaper than [`layout_full`](Self::layout_full)
285 /// for single-block edits (typing, formatting changes).
286 ///
287 /// If the block is inside a table cell (`BlockSnapshot::table_cell` is `Some`),
288 /// the table row height is re-measured and content below the table shifts.
289 pub fn relayout_block(&mut self, params: &crate::layout::block::BlockLayoutParams) {
290 self.flow_layout
291 .relayout_block(&self.font_registry, params, self.layout_width());
292 }
293
294 // ── Rendering ───────────────────────────────────────────────
295
296 /// Render the visible viewport and return everything needed to draw.
297 ///
298 /// Performs viewport culling (only processes blocks within the scroll window),
299 /// rasterizes any new glyphs into the atlas, and produces glyph quads,
300 /// image placeholders, and decoration rectangles.
301 ///
302 /// The returned reference borrows the `Typesetter`. The adapter should iterate
303 /// the frame for drawing, then drop the reference before calling any
304 /// layout/scroll methods on the next frame.
305 ///
306 /// On each call, stale glyphs (unused for ~120 frames) are evicted from the
307 /// atlas to reclaim space.
308 pub fn render(&mut self) -> &RenderFrame {
309 crate::render::frame::build_render_frame(
310 &self.flow_layout,
311 &self.font_registry,
312 &mut self.atlas,
313 &mut self.glyph_cache,
314 &mut self.scale_context,
315 self.scroll_offset,
316 self.viewport_height,
317 &self.cursors,
318 self.cursor_color,
319 self.selection_color,
320 &mut self.render_frame,
321 );
322 &self.render_frame
323 }
324
325 // ── Hit testing ─────────────────────────────────────────────
326
327 /// Map a screen-space point to a document position.
328 ///
329 /// Coordinates are relative to the widget's top-left corner.
330 /// The scroll offset is accounted for internally.
331 /// Returns `None` if the flow has no content.
332 pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
333 crate::render::hit_test::hit_test(&self.flow_layout, self.scroll_offset, x, y)
334 }
335
336 /// Get the screen-space caret rectangle at a document position.
337 ///
338 /// Returns `[x, y, width, height]` in screen pixels. Use this to report
339 /// the caret position to the platform IME system for composition window
340 /// placement. For drawing the caret, use the [`DecorationKind::Cursor`]
341 /// entry in [`RenderFrame::decorations`] instead.
342 pub fn caret_rect(&self, position: usize) -> [f32; 4] {
343 crate::render::hit_test::caret_rect(&self.flow_layout, self.scroll_offset, position)
344 }
345
346 // ── Cursor display ──────────────────────────────────────────
347
348 /// Update the cursor display state for a single cursor.
349 ///
350 /// The adapter reads `position` and `anchor` from text-document's
351 /// `TextCursor`, toggles `visible` on a blink timer, and passes
352 /// the result here. The typesetter includes cursor and selection
353 /// decorations in the next [`render`](Self::render) call.
354 pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
355 self.cursors = vec![CursorDisplay {
356 position: cursor.position,
357 anchor: cursor.anchor,
358 visible: cursor.visible,
359 }];
360 }
361
362 /// Update multiple cursors (multi-cursor editing support).
363 ///
364 /// Each cursor independently generates a caret and optional selection highlight.
365 pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
366 self.cursors = cursors
367 .iter()
368 .map(|c| CursorDisplay {
369 position: c.position,
370 anchor: c.anchor,
371 visible: c.visible,
372 })
373 .collect();
374 }
375
376 /// Set the selection highlight color (`[r, g, b, a]`, 0.0-1.0).
377 ///
378 /// Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
379 pub fn set_selection_color(&mut self, color: [f32; 4]) {
380 self.selection_color = color;
381 }
382
383 /// Set the cursor caret color (`[r, g, b, a]`, 0.0-1.0).
384 ///
385 /// Default: `[0.0, 0.0, 0.0, 1.0]` (black).
386 pub fn set_cursor_color(&mut self, color: [f32; 4]) {
387 self.cursor_color = color;
388 }
389
390 // ── Scrolling ───────────────────────────────────────────────
391
392 /// Get the visual position and height of a laid-out block.
393 ///
394 /// Returns `None` if the block ID is not in the current layout.
395 pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
396 let block = self.flow_layout.blocks.get(&block_id)?;
397 Some(BlockVisualInfo {
398 block_id,
399 y: block.y,
400 height: block.height,
401 })
402 }
403
404 /// Scroll so that the given document position is visible, placing it
405 /// roughly 1/3 from the top of the viewport.
406 ///
407 /// Returns the new scroll offset.
408 pub fn scroll_to_position(&mut self, position: usize) -> f32 {
409 let rect = self.caret_rect(position);
410 let target_y = rect[1] + self.scroll_offset - self.viewport_height / 3.0;
411 self.scroll_offset = target_y.max(0.0);
412 self.scroll_offset
413 }
414
415 /// Scroll the minimum amount needed to make the current caret visible.
416 ///
417 /// Call after cursor movement (arrow keys, click, typing) to keep
418 /// the caret in view. Returns `Some(new_offset)` if scrolling occurred,
419 /// or `None` if the caret was already visible.
420 pub fn ensure_caret_visible(&mut self) -> Option<f32> {
421 if self.cursors.is_empty() {
422 return None;
423 }
424 let pos = self.cursors[0].position;
425 let rect = self.caret_rect(pos);
426 let caret_screen_y = rect[1];
427 let caret_screen_bottom = caret_screen_y + rect[3];
428 let margin = 10.0;
429 let old_offset = self.scroll_offset;
430
431 if caret_screen_y < 0.0 {
432 self.scroll_offset += caret_screen_y - margin;
433 self.scroll_offset = self.scroll_offset.max(0.0);
434 } else if caret_screen_bottom > self.viewport_height {
435 self.scroll_offset += caret_screen_bottom - self.viewport_height + margin;
436 }
437
438 if (self.scroll_offset - old_offset).abs() > 0.001 {
439 Some(self.scroll_offset)
440 } else {
441 None
442 }
443 }
444}
445
446#[cfg(feature = "text-document")]
447enum FlowItemKind {
448 Block(crate::layout::block::BlockLayoutParams),
449 Table(crate::layout::table::TableLayoutParams),
450 Frame(crate::layout::frame::FrameLayoutParams),
451}
452
453impl Default for Typesetter {
454 fn default() -> Self {
455 Self::new()
456 }
457}