Skip to main content

text_typeset/
document_flow.rs

1//! Per-widget document flow state.
2//!
3//! A [`DocumentFlow`] is everything that describes **what a specific
4//! widget is showing** — viewport, zoom, scroll offset, wrap mode,
5//! the laid-out flow (blocks / tables / frames), the rendered frame
6//! cache, the cursor(s), and the selection / caret / text colors.
7//!
8//! Flows do not own font data. Every layout and render call takes a
9//! [`TextFontService`] by reference and reads the font registry,
10//! glyph atlas, and glyph cache through it. This split lets many
11//! widgets in the same window share one atlas (and one GPU upload
12//! per frame) while each owns an independent view onto its own
13//! document.
14//!
15//! # Lifecycle
16//!
17//! ```rust,no_run
18//! use text_typeset::{DocumentFlow, TextFontService};
19//!
20//! let mut service = TextFontService::new();
21//! let face = service.register_font(include_bytes!("../test-fonts/NotoSans-Variable.ttf"));
22//! service.set_default_font(face, 16.0);
23//!
24//! let mut flow = DocumentFlow::new();
25//! flow.set_viewport(800.0, 600.0);
26//!
27//! # #[cfg(feature = "text-document")]
28//! # {
29//! let doc = text_document::TextDocument::new();
30//! doc.set_plain_text("Hello, world!").unwrap();
31//! flow.layout_full(&service, &doc.snapshot_flow());
32//! # }
33//!
34//! let frame = flow.render(&mut service);
35//! // frame.glyphs     -> glyph quads (textured rects from the shared atlas)
36//! // frame.decorations -> cursor, selection, underlines, borders
37//! ```
38//!
39//! The caller's pattern for a multi-widget UI is the same, plus one
40//! rule: each widget owns its own `DocumentFlow` and must re-push
41//! its view state (viewport, zoom, scroll, cursor, colors) before
42//! its own `layout_*` / `render` call, because those fields live on
43//! the flow itself, not on the shared service.
44
45use crate::TextFontService;
46use crate::font::resolve::resolve_font;
47use crate::layout::block::BlockLayoutParams;
48use crate::layout::flow::{FlowItem, FlowLayout};
49use crate::layout::frame::FrameLayoutParams;
50use crate::layout::inline_markup::{InlineAttrs, InlineMarkup};
51use crate::layout::paragraph::{Alignment, Hyphenator, break_into_lines};
52use crate::layout::table::TableLayoutParams;
53use crate::shaping::run::{ShapedGlyph, ShapedRun};
54use crate::shaping::shaper::{
55    bidi_runs, font_metrics_px, shape_text, shape_text_with_fallback, to_harfrust_features,
56};
57use crate::types::{
58    BlockVisualInfo, CharacterGeometry, CursorDisplay, DecorationKind, DecorationRect, GlyphQuad,
59    HitTestResult, LaidOutSpan, LaidOutSpanKind, ParagraphResult, RenderFrame, SingleLineResult,
60    TextFormat,
61};
62
63/// Reasons [`DocumentFlow::relayout_block`] may refuse an
64/// incremental update.
65///
66/// Both variants describe invariant violations the caller can
67/// detect structurally ahead of time by asking
68/// [`DocumentFlow::has_layout`] and
69/// [`DocumentFlow::layout_dirty_for_scale`]. Returned as a
70/// `Result` rather than panicking so a misbehaving caller
71/// produces a recoverable error at the exact call site instead
72/// of corrupting the flow with a partial relayout.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum RelayoutError {
75    /// No `layout_*` method has been called on this flow yet.
76    /// The caller must run [`DocumentFlow::layout_full`] or
77    /// [`DocumentFlow::layout_blocks`] first to establish a
78    /// baseline layout before incremental updates make sense.
79    NoLayout,
80    /// The backing [`TextFontService`] has had its HiDPI scale
81    /// factor changed since this flow was last laid out, so the
82    /// existing block layouts hold advances at the old ppem.
83    /// Re-shaping a single block would leave it at the new ppem
84    /// while neighbors stay at the old, producing an inconsistent
85    /// flow. The caller must re-run `layout_full` /
86    /// `layout_blocks` to rebuild everything at the new scale.
87    ScaleDirty,
88}
89
90impl std::fmt::Display for RelayoutError {
91    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92        match self {
93            RelayoutError::NoLayout => {
94                f.write_str("relayout_block called before any layout_* method")
95            }
96            RelayoutError::ScaleDirty => f.write_str(
97                "relayout_block called after a scale-factor change without a fresh layout_*",
98            ),
99        }
100    }
101}
102
103impl std::error::Error for RelayoutError {}
104
105/// How the content (layout) width is determined.
106///
107/// Controls whether text reflows when the viewport resizes (web or
108/// editor style) or wraps at a fixed width (page / WYSIWYG style).
109#[derive(Debug, Clone, Copy, Default)]
110pub enum ContentWidthMode {
111    /// Content width equals viewport width (divided by zoom). Text
112    /// reflows on window resize — the default, typical for editors
113    /// and web layout.
114    #[default]
115    Auto,
116    /// Content width is fixed at a specific value, independent of
117    /// the viewport. Useful for page-like WYSIWYG layout, print
118    /// preview, or side panels with their own column width.
119    Fixed(f32),
120}
121
122/// Per-widget document flow state.
123///
124/// See the module-level docs for the shape of the split and for
125/// lifecycle examples. Every layout/render method here takes a
126/// [`TextFontService`] reference so flows can share one atlas across
127/// an entire window.
128pub struct DocumentFlow {
129    flow_layout: FlowLayout,
130    render_frame: RenderFrame,
131    scroll_offset: f32,
132    rendered_scroll_offset: f32,
133    viewport_width: f32,
134    viewport_height: f32,
135    content_width_mode: ContentWidthMode,
136    selection_color: [f32; 4],
137    cursor_color: [f32; 4],
138    text_color: [f32; 4],
139    /// Background used by the text-document bridge when a code block
140    /// carries no explicit `background_color`. Overrides the bridge's
141    /// historical light-grey default. Threaded into every
142    /// `convert_flow_with` / `convert_block_with` call kicked off
143    /// from `layout_full`. See [`Self::set_code_block_background`].
144    code_block_background: [f32; 4],
145    /// Foreground used by the text-document bridge for monospaced runs
146    /// (markdown inline `code`, fenced code blocks) that carry no
147    /// explicit `foreground_color`. `None` keeps the engine's default
148    /// `text_color`. See [`Self::set_code_block_foreground`].
149    code_block_foreground: Option<[f32; 4]>,
150    /// Echo / masking character for secure (password) fields. When
151    /// `Some(c)`, every character laid out by `layout_full` is replaced
152    /// with `c` before shaping, so the real text never reaches the
153    /// shaper or the glyph atlas. `None` (default) lays text out
154    /// verbatim. Threaded into the bridge via [`crate::bridge::BridgeOptions::echo_char`]
155    /// from `layout_full`. See [`set_echo_char`](Self::set_echo_char).
156    echo_char: Option<char>,
157    /// Auto-hyphenate justified blocks that don't set `hyphenate`
158    /// explicitly. Threaded into the bridge via
159    /// [`crate::bridge::BridgeOptions::hyphenate_justified`] from
160    /// `layout_full`. Enable on prose surfaces only. `false` by default.
161    /// See [`set_hyphenate_justified`](Self::set_hyphenate_justified).
162    hyphenate_justified: bool,
163    cursors: Vec<CursorDisplay>,
164    zoom: f32,
165    rendered_zoom: f32,
166    /// Raster densification for content drawn under a scale transform.
167    /// `1.0` = unscaled UI. Rasterization happens at
168    /// `size × scale_factor × raster_scale` physical pixels while layout
169    /// and glyph `screen` rects stay in logical pixels. Unlike `zoom`
170    /// (a post-layout coordinate transform) this changes which bitmaps
171    /// the quads sample, so the incremental `render_block_only` path
172    /// falls back to a full render when it changed. See
173    /// [`set_raster_scale`](Self::set_raster_scale).
174    raster_scale: f32,
175    rendered_raster_scale: f32,
176    /// `TextFontService::scale_generation` at the time of the last
177    /// `layout_*` call. Used by
178    /// [`layout_dirty_for_scale`](DocumentFlow::layout_dirty_for_scale)
179    /// so the framework can detect HiDPI transitions and re-run
180    /// layout without having to track them itself.
181    layout_scale_generation: u64,
182    /// Whether any `layout_*` call has been made at least once.
183    has_layout: bool,
184}
185
186impl DocumentFlow {
187    /// Create an empty flow with no content.
188    ///
189    /// After construction the caller typically calls
190    /// [`set_viewport`](Self::set_viewport) and one of the
191    /// `layout_*` methods before the first render.
192    pub fn new() -> Self {
193        Self {
194            flow_layout: FlowLayout::new(),
195            render_frame: RenderFrame::new(),
196            scroll_offset: 0.0,
197            rendered_scroll_offset: f32::NAN,
198            viewport_width: 0.0,
199            viewport_height: 0.0,
200            content_width_mode: ContentWidthMode::Auto,
201            selection_color: [0.26, 0.52, 0.96, 0.3],
202            cursor_color: [0.0, 0.0, 0.0, 1.0],
203            text_color: [0.0, 0.0, 0.0, 1.0],
204            code_block_background: [0.95, 0.95, 0.95, 1.0],
205            code_block_foreground: None,
206            echo_char: None,
207            hyphenate_justified: false,
208            cursors: Vec::new(),
209            zoom: 1.0,
210            rendered_zoom: f32::NAN,
211            raster_scale: 1.0,
212            rendered_raster_scale: f32::NAN,
213            layout_scale_generation: 0,
214            has_layout: false,
215        }
216    }
217
218    // ── Viewport & content width ───────────────────────────────
219
220    /// Set the visible area dimensions in logical pixels.
221    ///
222    /// The viewport controls:
223    ///
224    /// - **Culling**: only blocks within the viewport are rendered.
225    /// - **Selection highlight**: multi-line selection extends to
226    ///   the viewport width.
227    /// - **Layout width** (in [`ContentWidthMode::Auto`]): text
228    ///   wraps at `viewport_width / zoom`.
229    ///
230    /// Call this when the widget's container resizes. A resize by
231    /// itself does not relayout — re-run `layout_full` /
232    /// `layout_blocks` if the wrap width changed.
233    pub fn set_viewport(&mut self, width: f32, height: f32) {
234        self.viewport_width = width;
235        self.viewport_height = height;
236        self.flow_layout.viewport_width = width;
237        self.flow_layout.viewport_height = height;
238    }
239
240    /// Current viewport width in logical pixels.
241    pub fn viewport_width(&self) -> f32 {
242        self.viewport_width
243    }
244
245    /// Current viewport height in logical pixels.
246    pub fn viewport_height(&self) -> f32 {
247        self.viewport_height
248    }
249
250    /// Pin content width at a fixed value, independent of viewport.
251    ///
252    /// Text wraps at this width regardless of how wide the viewport
253    /// is. Use for page-like (WYSIWYG) layout or documents with an
254    /// explicit column width. Pass `f32::INFINITY` for no-wrap mode.
255    pub fn set_content_width(&mut self, width: f32) {
256        self.content_width_mode = ContentWidthMode::Fixed(width);
257    }
258
259    /// Reflow content width to follow the viewport (the default).
260    ///
261    /// Text re-wraps on every viewport resize. Standard editor and
262    /// web-style layout.
263    pub fn set_content_width_auto(&mut self) {
264        self.content_width_mode = ContentWidthMode::Auto;
265    }
266
267    /// The effective width used for text layout (line wrapping,
268    /// table columns, etc.).
269    ///
270    /// In [`ContentWidthMode::Auto`], equals `viewport_width / zoom`
271    /// so that text reflows to fit the zoomed viewport. In
272    /// [`ContentWidthMode::Fixed`], equals the set value (zoom only
273    /// magnifies the rendered output).
274    pub fn layout_width(&self) -> f32 {
275        match self.content_width_mode {
276            ContentWidthMode::Auto => self.viewport_width / self.zoom,
277            ContentWidthMode::Fixed(w) => w,
278        }
279    }
280
281    /// The currently configured content-width mode.
282    pub fn content_width_mode(&self) -> ContentWidthMode {
283        self.content_width_mode
284    }
285
286    /// Set the vertical scroll offset in logical pixels from the
287    /// top of the document. Affects culling and screen-space `y`
288    /// coordinates in the rendered frame.
289    pub fn set_scroll_offset(&mut self, offset: f32) {
290        self.scroll_offset = offset;
291    }
292
293    /// Current vertical scroll offset.
294    pub fn scroll_offset(&self) -> f32 {
295        self.scroll_offset
296    }
297
298    /// Total content height after layout, in logical pixels.
299    pub fn content_height(&self) -> f32 {
300        self.flow_layout.content_height
301    }
302
303    /// Maximum content width across all laid-out lines, in logical
304    /// pixels. Used for horizontal scrollbar range when wrapping
305    /// is disabled.
306    pub fn max_content_width(&self) -> f32 {
307        self.flow_layout.cached_max_content_width
308    }
309
310    // ── Zoom ────────────────────────────────────────────────────
311
312    /// Set the display zoom level (PDF-style, no reflow).
313    ///
314    /// Zoom is a pure display transform: layout stays at base size
315    /// and all screen-space output (glyph quads, decorations, caret
316    /// rects) is scaled by this factor. Hit-test inputs are
317    /// inversely scaled.
318    ///
319    /// For browser-style zoom that reflows text, combine with
320    /// `set_content_width(viewport_width / zoom)`.
321    ///
322    /// Clamped to `0.1..=10.0`. Default is `1.0`.
323    pub fn set_zoom(&mut self, zoom: f32) {
324        self.zoom = zoom.clamp(0.1, 10.0);
325    }
326
327    /// Current display zoom level.
328    pub fn zoom(&self) -> f32 {
329        self.zoom
330    }
331
332    /// Set the raster densification scale for content drawn under a
333    /// scale transform (a zoomed scene viewport).
334    ///
335    /// Orthogonal to [`set_zoom`](Self::set_zoom): zoom multiplies the
336    /// emitted screen coordinates, `raster_scale` only changes the
337    /// physical ppem glyphs are rasterized at — layout, metrics, and
338    /// `screen` rects are identical at every raster scale, so no
339    /// relayout is needed after changing it. The next [`render`](Self::render)
340    /// rasterizes missing glyphs at the new density;
341    /// old-density entries age out of the atlas via the normal LRU.
342    /// Scaled rasters (`!= 1.0`) are unhinted.
343    ///
344    /// Clamped to `0.1..=16.0`. Default is `1.0`.
345    pub fn set_raster_scale(&mut self, raster_scale: f32) {
346        self.raster_scale = raster_scale.clamp(0.1, 16.0);
347    }
348
349    /// Current raster densification scale.
350    pub fn raster_scale(&self) -> f32 {
351        self.raster_scale
352    }
353
354    // ── Scale factor sync ───────────────────────────────────────
355
356    /// Whether any `layout_*` method has run on this flow at least
357    /// once. Callers that need to distinguish "never laid out"
358    /// from "laid out against a stale scale factor" read this
359    /// alongside [`layout_dirty_for_scale`](Self::layout_dirty_for_scale).
360    pub fn has_layout(&self) -> bool {
361        self.has_layout
362    }
363
364    /// Returns `true` when the backing [`TextFontService`] has had
365    /// its HiDPI scale factor changed since this flow was last laid
366    /// out, meaning stored shaped advances and cached ppem values
367    /// are stale.
368    ///
369    /// Call after every `service.set_scale_factor(...)` to decide
370    /// whether to re-run `layout_full` / `layout_blocks` before the
371    /// next render. Returns `false` for flows that have never been
372    /// laid out at all (nothing to invalidate).
373    pub fn layout_dirty_for_scale(&self, service: &TextFontService) -> bool {
374        self.has_layout && self.layout_scale_generation != service.scale_generation()
375    }
376
377    // ── Layout ──────────────────────────────────────────────────
378
379    /// Full layout from a text-document `FlowSnapshot`.
380    ///
381    /// Clears any existing flow state and lays out every element
382    /// (blocks, tables, frames) from the snapshot in flow order.
383    /// Call on document load or `DocumentReset`. For single-block
384    /// edits prefer [`relayout_block`](Self::relayout_block).
385    #[cfg(feature = "text-document")]
386    pub fn layout_full(&mut self, service: &TextFontService, flow: &text_document::FlowSnapshot) {
387        use crate::bridge::{BridgeOptions, convert_flow_with};
388
389        let opts = BridgeOptions {
390            code_block_background: self.code_block_background,
391            code_block_foreground: self.code_block_foreground,
392            echo_char: self.echo_char,
393            hyphenate_justified: self.hyphenate_justified,
394        };
395        let converted = convert_flow_with(flow, &opts);
396
397        // Merge all elements by flow index and process in order.
398        let mut all_items: Vec<(usize, FlowItemKind)> = Vec::new();
399        for (idx, params) in converted.blocks {
400            all_items.push((idx, FlowItemKind::Block(params)));
401        }
402        for (idx, params) in converted.tables {
403            all_items.push((idx, FlowItemKind::Table(params)));
404        }
405        for (idx, params) in converted.frames {
406            all_items.push((idx, FlowItemKind::Frame(params)));
407        }
408        all_items.sort_by_key(|(idx, _)| *idx);
409
410        let lw = self.layout_width();
411        self.flow_layout.clear();
412        self.flow_layout.viewport_width = self.viewport_width;
413        self.flow_layout.viewport_height = self.viewport_height;
414        self.flow_layout.scale_factor = service.scale_factor;
415
416        for (_idx, kind) in all_items {
417            match kind {
418                FlowItemKind::Block(params) => {
419                    self.flow_layout
420                        .add_block(&service.font_registry, &params, lw);
421                }
422                FlowItemKind::Table(params) => {
423                    self.flow_layout
424                        .add_table(&service.font_registry, &params, lw);
425                }
426                FlowItemKind::Frame(params) => {
427                    self.flow_layout
428                        .add_frame(&service.font_registry, &params, lw);
429                }
430            }
431        }
432
433        // Capture the freshly-shaped blocks as the paint-overlay base. The
434        // engine applies paint spans afterward (recolor without reshape).
435        self.flow_layout.refresh_base_blocks();
436
437        self.note_layout_done(service);
438    }
439
440    /// Lay out a list of blocks from scratch.
441    ///
442    /// Framework-agnostic entry point — the caller assembles
443    /// [`BlockLayoutParams`] directly without going through
444    /// text-document. Replaces any existing flow state.
445    pub fn layout_blocks(
446        &mut self,
447        service: &TextFontService,
448        block_params: Vec<BlockLayoutParams>,
449    ) {
450        self.flow_layout.scale_factor = service.scale_factor;
451        self.flow_layout
452            .layout_blocks(&service.font_registry, block_params, self.layout_width());
453        self.note_layout_done(service);
454    }
455
456    /// Append a frame to the current flow. The frame's position
457    /// (inline, float, absolute) is carried in `params`.
458    pub fn add_frame(&mut self, service: &TextFontService, params: &FrameLayoutParams) {
459        self.flow_layout.scale_factor = service.scale_factor;
460        self.flow_layout
461            .add_frame(&service.font_registry, params, self.layout_width());
462        self.note_layout_done(service);
463    }
464
465    /// Append a table to the current flow.
466    pub fn add_table(&mut self, service: &TextFontService, params: &TableLayoutParams) {
467        self.flow_layout.scale_factor = service.scale_factor;
468        self.flow_layout
469            .add_table(&service.font_registry, params, self.layout_width());
470        self.note_layout_done(service);
471    }
472
473    /// Relayout a single block after its content or formatting
474    /// changed.
475    ///
476    /// Re-shapes and re-wraps just that block, then shifts
477    /// subsequent items if the height changed. Much cheaper than a
478    /// full layout for single-block edits (typing, format toggles).
479    /// If the block lives inside a table cell, the row height is
480    /// re-measured and content below the table shifts.
481    ///
482    /// # Invariants
483    ///
484    /// This is an incremental operation and only makes sense when
485    /// a valid layout is already installed on this flow, laid out
486    /// against the same HiDPI scale factor the service currently
487    /// reports. Violations produce a [`RelayoutError`]:
488    ///
489    /// - [`RelayoutError::NoLayout`] if no `layout_*` method has
490    ///   run on this flow yet — there is nothing to update.
491    /// - [`RelayoutError::ScaleDirty`] if the service's scale
492    ///   factor has changed since the last layout — reshaping a
493    ///   single block would leave neighbors at the old ppem and
494    ///   produce an inconsistent flow. The caller must re-run
495    ///   [`layout_full`](Self::layout_full) / [`layout_blocks`](Self::layout_blocks)
496    ///   first.
497    ///
498    /// Both conditions are detected structurally from
499    /// [`has_layout`](Self::has_layout) and
500    /// [`layout_dirty_for_scale`](Self::layout_dirty_for_scale),
501    /// so callers that already guard those don't need to handle
502    /// the error.
503    pub fn relayout_block(
504        &mut self,
505        service: &TextFontService,
506        params: &BlockLayoutParams,
507    ) -> Result<(), RelayoutError> {
508        if !self.has_layout {
509            return Err(RelayoutError::NoLayout);
510        }
511        if self.layout_scale_generation != service.scale_generation() {
512            return Err(RelayoutError::ScaleDirty);
513        }
514        self.flow_layout.scale_factor = service.scale_factor;
515        self.flow_layout
516            .relayout_block(&service.font_registry, params, self.layout_width());
517        self.note_layout_done(service);
518        Ok(())
519    }
520
521    /// Replace the paint-only color overlay for the whole flow, re-derived from
522    /// the captured base layout. Recolors without reshaping or reflowing — the
523    /// fast path for search / spell / paint-only syntax highlights. Call
524    /// `render` afterward to refresh the GPU frame.
525    pub fn apply_paint_spans_for(
526        &mut self,
527        spans_by_block: std::collections::HashMap<usize, Vec<crate::layout::block::PaintSpan>>,
528    ) {
529        self.flow_layout.apply_paint_spans_for(spans_by_block);
530    }
531
532    /// Apply (or clear) the paint overlay for a single block. Returns `false`
533    /// if the block has no captured base (no full layout yet).
534    pub fn apply_block_paint_spans(
535        &mut self,
536        block_id: usize,
537        spans: &[crate::layout::block::PaintSpan],
538    ) -> bool {
539        self.flow_layout.apply_block_paint_spans(block_id, spans)
540    }
541
542    fn note_layout_done(&mut self, service: &TextFontService) {
543        self.has_layout = true;
544        self.layout_scale_generation = service.scale_generation();
545    }
546
547    // ── Rendering ──────────────────────────────────────────────
548
549    /// Render the visible viewport and return the produced frame.
550    ///
551    /// Performs viewport culling, rasterizes any glyphs missing
552    /// from the atlas into it, and emits glyph quads, image quads,
553    /// and decoration rectangles. The returned reference borrows
554    /// both `self` and `service`; drop it before the next mutation.
555    ///
556    /// On every call, stale glyphs (unused for ~120 frames) are
557    /// evicted from the atlas to reclaim slot space.
558    pub fn render(&mut self, service: &mut TextFontService) -> &RenderFrame {
559        let effective_vw = self.viewport_width / self.zoom;
560        let effective_vh = self.viewport_height / self.zoom;
561        crate::render::frame::build_render_frame(
562            &self.flow_layout,
563            &service.font_registry,
564            &mut service.atlas,
565            &mut service.glyph_cache,
566            &mut service.scale_context,
567            self.scroll_offset,
568            effective_vw,
569            effective_vh,
570            &self.cursors,
571            self.cursor_color,
572            self.selection_color,
573            self.text_color,
574            self.raster_scale,
575            &mut self.render_frame,
576            &mut service.eviction_epoch,
577        );
578        self.rendered_scroll_offset = self.scroll_offset;
579        self.rendered_zoom = self.zoom;
580        self.rendered_raster_scale = self.raster_scale;
581        apply_zoom(&mut self.render_frame, self.zoom);
582        &self.render_frame
583    }
584
585    /// Incremental render that only re-renders one block's glyphs.
586    ///
587    /// Reuses cached glyph / decoration data for all other blocks
588    /// from the last full `render()`. Call after
589    /// [`relayout_block`](Self::relayout_block) when only one block's
590    /// text changed.
591    ///
592    /// Falls back to a full [`render`](Self::render) if the block's
593    /// height changed (subsequent glyph positions would be stale),
594    /// if scroll offset or zoom changed since the last full render,
595    /// or if the block lives inside a table / frame (those are
596    /// cached with a different key).
597    pub fn render_block_only(
598        &mut self,
599        service: &mut TextFontService,
600        block_id: usize,
601    ) -> &RenderFrame {
602        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
603            || (self.zoom - self.rendered_zoom).abs() > 0.001
604            || (self.raster_scale - self.rendered_raster_scale).abs() > 0.001
605        {
606            return self.render(service);
607        }
608
609        // Defensive: if the atlas has dropped any entry since the last
610        // full render, our cached per-block glyph quads may now point
611        // at slots owned by unrelated glyphs. Fall back to a full
612        // re-render — `touch_glyphs` in `rebuild_flat_frame` is the
613        // primary keep-alive mechanism, this is the safety net.
614        if service.eviction_epoch != self.render_frame.atlas_eviction_epoch {
615            return self.render(service);
616        }
617
618        if !self.flow_layout.blocks.contains_key(&block_id) {
619            let in_table = self.flow_layout.tables.values().any(|table| {
620                table
621                    .cell_layouts
622                    .iter()
623                    .any(|c| c.blocks.iter().any(|b| b.block_id == block_id))
624            });
625            if in_table {
626                return self.render(service);
627            }
628            let in_frame = self
629                .flow_layout
630                .frames
631                .values()
632                .any(|frame| crate::layout::flow::frame_contains_block(frame, block_id));
633            if in_frame {
634                return self.render(service);
635            }
636        }
637
638        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
639            let old_height = self
640                .render_frame
641                .block_heights
642                .get(&block_id)
643                .copied()
644                .unwrap_or(block.height);
645            if (block.height - old_height).abs() > 0.001 {
646                return self.render(service);
647            }
648        }
649
650        let effective_vw = self.viewport_width / self.zoom;
651        let effective_vh = self.viewport_height / self.zoom;
652        let scale_factor = service.scale_factor;
653        let mut new_glyphs = Vec::new();
654        let mut new_images = Vec::new();
655        let mut new_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
656        if let Some(block) = self.flow_layout.blocks.get(&block_id) {
657            let mut tmp = RenderFrame::new();
658            crate::render::frame::render_block_at_offset(
659                block,
660                0.0,
661                0.0,
662                &service.font_registry,
663                &mut service.atlas,
664                &mut service.glyph_cache,
665                &mut service.scale_context,
666                self.scroll_offset,
667                effective_vh,
668                self.text_color,
669                scale_factor,
670                self.raster_scale,
671                &mut tmp,
672                &mut new_keys,
673                &mut service.eviction_epoch,
674            );
675            new_glyphs = tmp.glyphs;
676            new_images = tmp.images;
677        }
678
679        let new_decos = if let Some(block) = self.flow_layout.blocks.get(&block_id) {
680            crate::render::decoration::generate_block_decorations(
681                block,
682                &service.font_registry,
683                self.scroll_offset,
684                effective_vh,
685                0.0,
686                0.0,
687                effective_vw,
688                self.text_color,
689                scale_factor,
690            )
691        } else {
692            Vec::new()
693        };
694
695        if let Some(entry) = self
696            .render_frame
697            .block_glyphs
698            .iter_mut()
699            .find(|(id, _)| *id == block_id)
700        {
701            entry.1 = new_glyphs;
702        }
703        if let Some(entry) = self
704            .render_frame
705            .block_images
706            .iter_mut()
707            .find(|(id, _)| *id == block_id)
708        {
709            entry.1 = new_images;
710        }
711        if let Some(entry) = self
712            .render_frame
713            .block_decorations
714            .iter_mut()
715            .find(|(id, _)| *id == block_id)
716        {
717            entry.1 = new_decos;
718        }
719        if let Some(entry) = self
720            .render_frame
721            .block_glyph_keys
722            .iter_mut()
723            .find(|(id, _)| *id == block_id)
724        {
725            entry.1 = new_keys;
726        }
727
728        self.rebuild_flat_frame(service);
729        apply_zoom(&mut self.render_frame, self.zoom);
730        &self.render_frame
731    }
732
733    /// Lightweight render that only updates cursor/selection
734    /// decorations.
735    ///
736    /// Reuses the existing glyph quads and images from the last
737    /// full `render()`. Use when only the cursor blinked or the
738    /// selection changed. Falls back to a full [`render`](Self::render)
739    /// if the scroll offset or zoom changed in the meantime.
740    pub fn render_cursor_only(&mut self, service: &mut TextFontService) -> &RenderFrame {
741        if (self.scroll_offset - self.rendered_scroll_offset).abs() > 0.001
742            || (self.zoom - self.rendered_zoom).abs() > 0.001
743        {
744            return self.render(service);
745        }
746
747        // Defensive: if any atlas eviction has happened since the last
748        // full render, our cached glyph quads' atlas coordinates may
749        // refer to slots reallocated for unrelated glyphs. Fall back
750        // to a fresh render rather than painting garbled text.
751        if service.eviction_epoch != self.render_frame.atlas_eviction_epoch {
752            return self.render(service);
753        }
754
755        // Keep cached glyphs alive in the shared atlas. Cursor blinks
756        // and selection-only changes paint the cached `render_frame.glyphs`
757        // without ever calling `cache.get`, so the LRU sees those
758        // glyphs as idle and ages them out under sustained activity
759        // in *other* widgets sharing the atlas. Touching here closes
760        // that gap so the eviction fallback above stays a safety net
761        // rather than a hot path.
762        service.touch_glyphs(&self.render_frame.glyph_keys);
763
764        self.render_frame.decorations.retain(|d| {
765            !matches!(
766                d.kind,
767                DecorationKind::Cursor | DecorationKind::Selection | DecorationKind::CellSelection
768            )
769        });
770
771        let effective_vw = self.viewport_width / self.zoom;
772        let effective_vh = self.viewport_height / self.zoom;
773        let mut cursor_decos = crate::render::cursor::generate_cursor_decorations(
774            &self.flow_layout,
775            &self.cursors,
776            self.scroll_offset,
777            self.cursor_color,
778            self.selection_color,
779            effective_vw,
780            effective_vh,
781        );
782        apply_zoom_decorations(&mut cursor_decos, self.zoom);
783        self.render_frame.decorations.extend(cursor_decos);
784
785        &self.render_frame
786    }
787
788    fn rebuild_flat_frame(&mut self, service: &mut TextFontService) {
789        self.render_frame.glyphs.clear();
790        self.render_frame.images.clear();
791        self.render_frame.decorations.clear();
792        self.render_frame.glyph_keys.clear();
793        for (_, glyphs) in &self.render_frame.block_glyphs {
794            self.render_frame.glyphs.extend_from_slice(glyphs);
795        }
796        for (_, images) in &self.render_frame.block_images {
797            self.render_frame.images.extend_from_slice(images);
798        }
799        for (_, decos) in &self.render_frame.block_decorations {
800            self.render_frame.decorations.extend_from_slice(decos);
801        }
802        for (_, keys) in &self.render_frame.block_glyph_keys {
803            self.render_frame.glyph_keys.extend_from_slice(keys);
804        }
805        // Keep the cached glyphs alive in the shared atlas. Without
806        // this, blocks that are still visible through cached quads
807        // but never re-rasterized this frame would age out under the
808        // 120-generation LRU and have their atlas slots reallocated
809        // to unrelated glyphs — corrupting every paint that reuses
810        // these quads (the editor-and-viewer-mangled-together bug).
811        service.touch_glyphs(&self.render_frame.glyph_keys);
812
813        for item in &self.flow_layout.flow_order {
814            match item {
815                FlowItem::Table { table_id, .. } => {
816                    if let Some(table) = self.flow_layout.tables.get(table_id) {
817                        let decos = crate::layout::table::generate_table_decorations(
818                            table,
819                            self.scroll_offset,
820                        );
821                        self.render_frame.decorations.extend(decos);
822                    }
823                }
824                FlowItem::Frame { frame_id, .. } => {
825                    if let Some(frame) = self.flow_layout.frames.get(frame_id) {
826                        crate::render::frame::append_frame_border_decorations(
827                            frame,
828                            self.scroll_offset,
829                            &mut self.render_frame.decorations,
830                        );
831                    }
832                }
833                FlowItem::Block { .. } => {}
834            }
835        }
836
837        let effective_vw = self.viewport_width / self.zoom;
838        let effective_vh = self.viewport_height / self.zoom;
839        let cursor_decos = crate::render::cursor::generate_cursor_decorations(
840            &self.flow_layout,
841            &self.cursors,
842            self.scroll_offset,
843            self.cursor_color,
844            self.selection_color,
845            effective_vw,
846            effective_vh,
847        );
848        self.render_frame.decorations.extend(cursor_decos);
849
850        self.render_frame.atlas_dirty = service.atlas.dirty;
851        self.render_frame.atlas_width = service.atlas.width;
852        self.render_frame.atlas_height = service.atlas.height;
853        if service.atlas.dirty {
854            let pixels = &service.atlas.pixels;
855            let needed = (service.atlas.width * service.atlas.height * 4) as usize;
856            self.render_frame.atlas_pixels.resize(needed, 0);
857            let copy_len = needed.min(pixels.len());
858            self.render_frame.atlas_pixels[..copy_len].copy_from_slice(&pixels[..copy_len]);
859            service.atlas.dirty = false;
860        }
861    }
862
863    // ── Single-line layout ──────────────────────────────────────
864
865    /// Lay out a single line of text and return GPU-ready glyph
866    /// quads. Fast path for labels, tooltips, overlays — anything
867    /// that doesn't need the full document pipeline.
868    ///
869    /// If `max_width` is set and the shaped text exceeds it, the
870    /// output is truncated with an ellipsis character. Glyph quads
871    /// are positioned with the top-left at `(0, 0)`.
872    ///
873    /// `raster_scale` densifies glyph bitmaps for content drawn under
874    /// a scale transform (pass `1.0` for unscaled UI): rasterization
875    /// happens at `size × scale_factor × raster_scale` physical pixels
876    /// while every returned metric and `screen` rect stays in logical
877    /// pixels — layout is identical at every raster scale.
878    pub fn layout_single_line(
879        &mut self,
880        service: &mut TextFontService,
881        text: &str,
882        format: &TextFormat,
883        max_width: Option<f32>,
884        raster_scale: f32,
885    ) -> SingleLineResult {
886        let empty = SingleLineResult {
887            width: 0.0,
888            height: 0.0,
889            baseline: 0.0,
890            underline_offset: 0.0,
891            underline_thickness: 0.0,
892            glyphs: Vec::new(),
893            glyph_keys: Vec::new(),
894            spans: Vec::new(),
895        };
896
897        if text.is_empty() {
898            return empty;
899        }
900
901        let font_point_size = format.font_size.map(|s| s as u32);
902        let resolved = match resolve_font(
903            &service.font_registry,
904            format.font_family.as_deref(),
905            format.font_weight,
906            format.font_bold,
907            format.font_italic,
908            font_point_size,
909            service.scale_factor,
910        ) {
911            Some(r) => r,
912            None => return empty,
913        };
914
915        let metrics = match font_metrics_px(&service.font_registry, &resolved) {
916            Some(m) => m,
917            None => return empty,
918        };
919        let line_height = metrics.ascent + metrics.descent + metrics.leading;
920        let baseline = metrics.ascent;
921
922        let features = to_harfrust_features(&format.features);
923        let runs: Vec<_> = bidi_runs(text)
924            .into_iter()
925            .filter_map(|br| {
926                let slice = text.get(br.byte_range.clone())?;
927                shape_text_with_fallback(
928                    &service.font_registry,
929                    &resolved,
930                    slice,
931                    br.byte_range.start,
932                    br.direction,
933                    &features,
934                )
935            })
936            .collect();
937
938        if runs.is_empty() {
939            return empty;
940        }
941
942        let total_advance: f32 = runs.iter().map(|r| r.advance_width).sum();
943
944        let (truncate_at_visual_index, final_width, ellipsis_run) = if let Some(max_w) = max_width
945            && total_advance > max_w
946        {
947            let ellipsis_run = shape_text(&service.font_registry, &resolved, "\u{2026}", 0);
948            let ellipsis_width = ellipsis_run
949                .as_ref()
950                .map(|r| r.advance_width)
951                .unwrap_or(0.0);
952            let budget = (max_w - ellipsis_width).max(0.0);
953
954            let mut used = 0.0f32;
955            let mut count = 0usize;
956            'outer: for run in &runs {
957                for g in &run.glyphs {
958                    if used + g.x_advance > budget {
959                        break 'outer;
960                    }
961                    used += g.x_advance;
962                    count += 1;
963                }
964            }
965
966            (Some(count), used + ellipsis_width, ellipsis_run)
967        } else {
968            (None, total_advance, None)
969        };
970
971        let text_color = format.color.unwrap_or(self.text_color);
972        let glyph_capacity: usize = runs.iter().map(|r| r.glyphs.len()).sum();
973        let mut quads = Vec::with_capacity(glyph_capacity + 1);
974        let mut keys = Vec::with_capacity(glyph_capacity + 1);
975        let mut pen_x = 0.0f32;
976        let mut emitted = 0usize;
977
978        'emit: for run in &runs {
979            for glyph in &run.glyphs {
980                if let Some(limit) = truncate_at_visual_index
981                    && emitted >= limit
982                {
983                    break 'emit;
984                }
985                rasterize_glyph_quad(
986                    service,
987                    glyph,
988                    run,
989                    pen_x,
990                    baseline,
991                    text_color,
992                    raster_scale,
993                    &mut quads,
994                    &mut keys,
995                );
996                pen_x += glyph.x_advance;
997                emitted += 1;
998            }
999        }
1000
1001        if let Some(ref e_run) = ellipsis_run {
1002            for glyph in &e_run.glyphs {
1003                rasterize_glyph_quad(
1004                    service,
1005                    glyph,
1006                    e_run,
1007                    pen_x,
1008                    baseline,
1009                    text_color,
1010                    raster_scale,
1011                    &mut quads,
1012                    &mut keys,
1013                );
1014                pen_x += glyph.x_advance;
1015            }
1016        }
1017
1018        SingleLineResult {
1019            width: final_width,
1020            height: line_height,
1021            baseline,
1022            underline_offset: metrics.underline_offset,
1023            underline_thickness: metrics.stroke_size,
1024            glyphs: quads,
1025            glyph_keys: keys,
1026            spans: Vec::new(),
1027        }
1028    }
1029
1030    /// Lay out a multi-line paragraph by wrapping text at `max_width`.
1031    ///
1032    /// Multi-line counterpart to
1033    /// [`layout_single_line`](Self::layout_single_line). Shapes the
1034    /// input, breaks it at Unicode line-break opportunities
1035    /// (greedy, left-aligned), and rasterizes each line's glyphs
1036    /// into paragraph-local coordinates starting at `(0, 0)`.
1037    ///
1038    /// If `max_lines` is `Some(n)`, at most `n` lines are emitted
1039    /// and any remainder is silently dropped.
1040    ///
1041    /// See [`layout_single_line`](Self::layout_single_line) for the
1042    /// `raster_scale` contract (pass `1.0` for unscaled UI).
1043    pub fn layout_paragraph(
1044        &mut self,
1045        service: &mut TextFontService,
1046        text: &str,
1047        format: &TextFormat,
1048        max_width: f32,
1049        max_lines: Option<usize>,
1050        raster_scale: f32,
1051    ) -> ParagraphResult {
1052        let empty = ParagraphResult {
1053            width: 0.0,
1054            height: 0.0,
1055            baseline_first: 0.0,
1056            line_count: 0,
1057            line_height: 0.0,
1058            underline_offset: 0.0,
1059            underline_thickness: 0.0,
1060            glyphs: Vec::new(),
1061            glyph_keys: Vec::new(),
1062            spans: Vec::new(),
1063        };
1064
1065        if text.is_empty() || max_width <= 0.0 {
1066            return empty;
1067        }
1068
1069        let font_point_size = format.font_size.map(|s| s as u32);
1070        let resolved = match resolve_font(
1071            &service.font_registry,
1072            format.font_family.as_deref(),
1073            format.font_weight,
1074            format.font_bold,
1075            format.font_italic,
1076            font_point_size,
1077            service.scale_factor,
1078        ) {
1079            Some(r) => r,
1080            None => return empty,
1081        };
1082
1083        let metrics = match font_metrics_px(&service.font_registry, &resolved) {
1084            Some(m) => m,
1085            None => return empty,
1086        };
1087
1088        let features = to_harfrust_features(&format.features);
1089        let runs: Vec<_> = bidi_runs(text)
1090            .into_iter()
1091            .filter_map(|br| {
1092                let slice = text.get(br.byte_range.clone())?;
1093                shape_text_with_fallback(
1094                    &service.font_registry,
1095                    &resolved,
1096                    slice,
1097                    br.byte_range.start,
1098                    br.direction,
1099                    &features,
1100                )
1101            })
1102            .collect();
1103
1104        if runs.is_empty() {
1105            return empty;
1106        }
1107
1108        let hyphenator = format.hyphenation.and_then(|h| {
1109            shape_text(&service.font_registry, &resolved, "-", 0)
1110                .and_then(|r| r.glyphs.into_iter().next())
1111                .map(|glyph| Hyphenator {
1112                    glyph,
1113                    language: h.language,
1114                })
1115        });
1116        let lines = break_into_lines(
1117            runs,
1118            text,
1119            max_width,
1120            Alignment::Left,
1121            0.0,
1122            &metrics,
1123            hyphenator,
1124        );
1125
1126        let line_count = match max_lines {
1127            Some(n) => lines.len().min(n),
1128            None => lines.len(),
1129        };
1130
1131        let text_color = format.color.unwrap_or(self.text_color);
1132        let mut quads: Vec<GlyphQuad> = Vec::new();
1133        let mut keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1134        let mut y_top = 0.0f32;
1135        let mut max_line_width = 0.0f32;
1136        let baseline_first = metrics.ascent;
1137
1138        for line in lines.iter().take(line_count) {
1139            if line.width > max_line_width {
1140                max_line_width = line.width;
1141            }
1142            let baseline_y = y_top + metrics.ascent;
1143            for run in &line.runs {
1144                let mut pen_x = run.x;
1145                let run_copy = run.shaped_run.clone();
1146                for glyph in &run_copy.glyphs {
1147                    rasterize_glyph_quad(
1148                        service,
1149                        glyph,
1150                        &run_copy,
1151                        pen_x,
1152                        baseline_y,
1153                        text_color,
1154                        raster_scale,
1155                        &mut quads,
1156                        &mut keys,
1157                    );
1158                    pen_x += glyph.x_advance;
1159                }
1160            }
1161            y_top += metrics.ascent + metrics.descent + metrics.leading;
1162        }
1163
1164        let line_height = metrics.ascent + metrics.descent + metrics.leading;
1165        ParagraphResult {
1166            width: max_line_width,
1167            height: y_top,
1168            baseline_first,
1169            line_count,
1170            line_height,
1171            underline_offset: metrics.underline_offset,
1172            underline_thickness: metrics.stroke_size,
1173            glyphs: quads,
1174            glyph_keys: keys,
1175            spans: Vec::new(),
1176        }
1177    }
1178
1179    /// Single-line layout with inline markup. See
1180    /// [`layout_single_line`](Self::layout_single_line) for the plain
1181    /// variant. Accepts parsed `[label](url)`, `*italic*`, and
1182    /// `**bold**` spans and annotates the output with per-span
1183    /// bounding rectangles for hit-testing.
1184    pub fn layout_single_line_markup(
1185        &mut self,
1186        service: &mut TextFontService,
1187        markup: &InlineMarkup,
1188        format: &TextFormat,
1189        max_width: Option<f32>,
1190        raster_scale: f32,
1191    ) -> SingleLineResult {
1192        if markup.spans.is_empty() {
1193            return SingleLineResult {
1194                width: 0.0,
1195                height: 0.0,
1196                baseline: 0.0,
1197                underline_offset: 0.0,
1198                underline_thickness: 0.0,
1199                glyphs: Vec::new(),
1200                glyph_keys: Vec::new(),
1201                spans: Vec::new(),
1202            };
1203        }
1204
1205        let per_span: Vec<(SingleLineResult, &crate::layout::inline_markup::InlineSpan)> = markup
1206            .spans
1207            .iter()
1208            .map(|sp| {
1209                let fmt = merge_format(format, sp.attrs);
1210                let r = if sp.text.is_empty() {
1211                    SingleLineResult {
1212                        width: 0.0,
1213                        height: 0.0,
1214                        baseline: 0.0,
1215                        underline_offset: 0.0,
1216                        underline_thickness: 0.0,
1217                        glyphs: Vec::new(),
1218                        glyph_keys: Vec::new(),
1219                        spans: Vec::new(),
1220                    }
1221                } else {
1222                    self.layout_single_line(service, &sp.text, &fmt, None, raster_scale)
1223                };
1224                (r, sp)
1225            })
1226            .collect();
1227
1228        let total_width: f32 = per_span.iter().map(|(r, _)| r.width).sum();
1229        let line_height = per_span
1230            .iter()
1231            .map(|(r, _)| r.height)
1232            .fold(0.0f32, f32::max);
1233        let baseline = per_span
1234            .iter()
1235            .map(|(r, _)| r.baseline)
1236            .fold(0.0f32, f32::max);
1237        // Carry underline metrics from the first non-empty span. Spans may
1238        // use different fonts but a single line only has one underline, so
1239        // the first span wins.
1240        let (underline_offset, underline_thickness) = per_span
1241            .iter()
1242            .map(|(r, _)| (r.underline_offset, r.underline_thickness))
1243            .find(|(_, t)| *t > 0.0)
1244            .unwrap_or((0.0, 0.0));
1245
1246        let truncate = match max_width {
1247            Some(mw) if total_width > mw => Some(mw),
1248            _ => None,
1249        };
1250
1251        let mut glyphs: Vec<GlyphQuad> = Vec::new();
1252        let mut all_keys: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1253        let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1254        let mut pen_x: f32 = 0.0;
1255        let effective_width = truncate.unwrap_or(total_width);
1256
1257        for (r, sp) in &per_span {
1258            let remaining = (effective_width - pen_x).max(0.0);
1259            let span_visible_width = r.width.min(remaining);
1260            if span_visible_width <= 0.0 && r.width > 0.0 {
1261                spans_out.push(LaidOutSpan {
1262                    kind: if let Some(url) = sp.link_url.clone() {
1263                        LaidOutSpanKind::Link { url }
1264                    } else {
1265                        LaidOutSpanKind::Text
1266                    },
1267                    line_index: 0,
1268                    rect: [pen_x, 0.0, 0.0, line_height],
1269                    byte_range: sp.byte_range.clone(),
1270                });
1271                continue;
1272            }
1273
1274            for (gi, g) in r.glyphs.iter().enumerate() {
1275                let g_right = pen_x + g.screen[0] + g.screen[2];
1276                if g_right > effective_width + 0.5 {
1277                    break;
1278                }
1279                let mut gq = g.clone();
1280                gq.screen[0] += pen_x;
1281                glyphs.push(gq);
1282                if let Some(k) = r.glyph_keys.get(gi) {
1283                    all_keys.push(*k);
1284                }
1285            }
1286
1287            spans_out.push(LaidOutSpan {
1288                kind: if let Some(url) = sp.link_url.clone() {
1289                    LaidOutSpanKind::Link { url }
1290                } else {
1291                    LaidOutSpanKind::Text
1292                },
1293                line_index: 0,
1294                rect: [pen_x, 0.0, span_visible_width, line_height],
1295                byte_range: sp.byte_range.clone(),
1296            });
1297
1298            pen_x += r.width;
1299            if truncate.is_some() && pen_x >= effective_width {
1300                break;
1301            }
1302        }
1303
1304        SingleLineResult {
1305            width: effective_width,
1306            height: line_height,
1307            baseline,
1308            underline_offset,
1309            underline_thickness,
1310            glyphs,
1311            glyph_keys: all_keys,
1312            spans: spans_out,
1313        }
1314    }
1315
1316    /// Paragraph layout with inline markup. Multi-line counterpart
1317    /// to [`layout_single_line_markup`](Self::layout_single_line_markup).
1318    /// Emits a [`LaidOutSpan`] for every link segment so the caller
1319    /// can hit-test against wrapped links.
1320    pub fn layout_paragraph_markup(
1321        &mut self,
1322        service: &mut TextFontService,
1323        markup: &InlineMarkup,
1324        format: &TextFormat,
1325        max_width: f32,
1326        max_lines: Option<usize>,
1327        raster_scale: f32,
1328    ) -> ParagraphResult {
1329        let empty = ParagraphResult {
1330            width: 0.0,
1331            height: 0.0,
1332            baseline_first: 0.0,
1333            line_count: 0,
1334            line_height: 0.0,
1335            underline_offset: 0.0,
1336            underline_thickness: 0.0,
1337            glyphs: Vec::new(),
1338            glyph_keys: Vec::new(),
1339            spans: Vec::new(),
1340        };
1341
1342        if markup.spans.is_empty() || max_width <= 0.0 {
1343            return empty;
1344        }
1345
1346        let mut flat = String::new();
1347        let mut span_flat_offsets: Vec<usize> = Vec::with_capacity(markup.spans.len());
1348        for sp in &markup.spans {
1349            span_flat_offsets.push(flat.len());
1350            flat.push_str(&sp.text);
1351        }
1352        if flat.is_empty() {
1353            return empty;
1354        }
1355
1356        let base_point_size = format.font_size.map(|s| s as u32);
1357        let base_resolved = match resolve_font(
1358            &service.font_registry,
1359            format.font_family.as_deref(),
1360            format.font_weight,
1361            format.font_bold,
1362            format.font_italic,
1363            base_point_size,
1364            service.scale_factor,
1365        ) {
1366            Some(r) => r,
1367            None => return empty,
1368        };
1369        let metrics = match font_metrics_px(&service.font_registry, &base_resolved) {
1370            Some(m) => m,
1371            None => return empty,
1372        };
1373
1374        let mut all_runs: Vec<ShapedRun> = Vec::new();
1375        for (span_idx, sp) in markup.spans.iter().enumerate() {
1376            if sp.text.is_empty() {
1377                continue;
1378            }
1379            let fmt = merge_format(format, sp.attrs);
1380            let span_point_size = fmt.font_size.map(|s| s as u32);
1381            let Some(resolved) = resolve_font(
1382                &service.font_registry,
1383                fmt.font_family.as_deref(),
1384                fmt.font_weight,
1385                fmt.font_bold,
1386                fmt.font_italic,
1387                span_point_size,
1388                service.scale_factor,
1389            ) else {
1390                continue;
1391            };
1392
1393            let flat_start = span_flat_offsets[span_idx];
1394            let features = to_harfrust_features(&fmt.features);
1395            for br in bidi_runs(&sp.text) {
1396                let slice = match sp.text.get(br.byte_range.clone()) {
1397                    Some(s) => s,
1398                    None => continue,
1399                };
1400                let Some(mut run) = shape_text_with_fallback(
1401                    &service.font_registry,
1402                    &resolved,
1403                    slice,
1404                    flat_start + br.byte_range.start,
1405                    br.direction,
1406                    &features,
1407                ) else {
1408                    continue;
1409                };
1410                if let Some(url) = sp.link_url.as_ref() {
1411                    run.is_link = true;
1412                    run.anchor_href = Some(url.clone());
1413                }
1414                all_runs.push(run);
1415            }
1416        }
1417
1418        if all_runs.is_empty() {
1419            return empty;
1420        }
1421
1422        let hyphenator = format.hyphenation.and_then(|h| {
1423            shape_text(&service.font_registry, &base_resolved, "-", 0)
1424                .and_then(|r| r.glyphs.into_iter().next())
1425                .map(|glyph| Hyphenator {
1426                    glyph,
1427                    language: h.language,
1428                })
1429        });
1430        let lines = break_into_lines(
1431            all_runs,
1432            &flat,
1433            max_width,
1434            Alignment::Left,
1435            0.0,
1436            &metrics,
1437            hyphenator,
1438        );
1439
1440        let line_count = match max_lines {
1441            Some(n) => lines.len().min(n),
1442            None => lines.len(),
1443        };
1444
1445        let text_color = format.color.unwrap_or(self.text_color);
1446        let mut glyphs_out: Vec<GlyphQuad> = Vec::new();
1447        let mut keys_out: Vec<crate::atlas::cache::GlyphCacheKey> = Vec::new();
1448        let mut spans_out: Vec<LaidOutSpan> = Vec::new();
1449        let line_height = metrics.ascent + metrics.descent + metrics.leading;
1450        let mut y_top: f32 = 0.0;
1451        let mut max_line_width: f32 = 0.0;
1452        let baseline_first = metrics.ascent;
1453
1454        for (line_idx, line) in lines.iter().take(line_count).enumerate() {
1455            if line.width > max_line_width {
1456                max_line_width = line.width;
1457            }
1458            let baseline_y = y_top + metrics.ascent;
1459
1460            for pr in &line.runs {
1461                let run_copy = pr.shaped_run.clone();
1462                let mut pen_x = pr.x;
1463                for glyph in &run_copy.glyphs {
1464                    rasterize_glyph_quad(
1465                        service,
1466                        glyph,
1467                        &run_copy,
1468                        pen_x,
1469                        baseline_y,
1470                        text_color,
1471                        raster_scale,
1472                        &mut glyphs_out,
1473                        &mut keys_out,
1474                    );
1475                    pen_x += glyph.x_advance;
1476                }
1477
1478                if pr.decorations.is_link
1479                    && let Some(url) = pr.decorations.anchor_href.clone()
1480                {
1481                    let width = pr.shaped_run.advance_width;
1482                    spans_out.push(LaidOutSpan {
1483                        kind: LaidOutSpanKind::Link { url },
1484                        line_index: line_idx,
1485                        rect: [pr.x, y_top, width, line_height],
1486                        byte_range: pr.shaped_run.text_range.clone(),
1487                    });
1488                }
1489            }
1490
1491            y_top += line_height;
1492        }
1493
1494        ParagraphResult {
1495            width: max_line_width,
1496            height: y_top,
1497            baseline_first,
1498            line_count,
1499            line_height,
1500            underline_offset: metrics.underline_offset,
1501            underline_thickness: metrics.stroke_size,
1502            glyphs: glyphs_out,
1503            glyph_keys: keys_out,
1504            spans: spans_out,
1505        }
1506    }
1507
1508    // ── Hit testing & character geometry ───────────────────────
1509
1510    /// Map a screen-space point to a document position. Coordinates
1511    /// are relative to the widget's top-left corner; the scroll
1512    /// offset is applied internally. Returns `None` when the flow
1513    /// has no content.
1514    pub fn hit_test(&self, x: f32, y: f32) -> Option<HitTestResult> {
1515        crate::render::hit_test::hit_test(
1516            &self.flow_layout,
1517            self.scroll_offset,
1518            x / self.zoom,
1519            y / self.zoom,
1520        )
1521    }
1522
1523    /// Per-character advance geometry within a laid-out block.
1524    ///
1525    /// Used by accessibility layers that need to expose character
1526    /// positions to screen readers (AccessKit's `character_positions`
1527    /// / `character_widths` on `Role::TextRun`). `char_start` and
1528    /// `char_end` are block-relative character offsets. Returns one
1529    /// entry per character in the range, with `position` measured
1530    /// in run-local coordinates (the first character sits at `0`).
1531    pub fn character_geometry(
1532        &self,
1533        block_id: usize,
1534        char_start: usize,
1535        char_end: usize,
1536    ) -> Vec<CharacterGeometry> {
1537        if char_start >= char_end {
1538            return Vec::new();
1539        }
1540        let block = match self.flow_layout.blocks.get(&block_id) {
1541            Some(b) => b,
1542            None => return Vec::new(),
1543        };
1544
1545        let mut absolute: Vec<(usize, f32)> = Vec::with_capacity(char_end - char_start);
1546        for line in &block.lines {
1547            if line.char_range.end <= char_start || line.char_range.start >= char_end {
1548                continue;
1549            }
1550            let local_start = char_start.max(line.char_range.start);
1551            let local_end = char_end.min(line.char_range.end);
1552            for c in local_start..local_end {
1553                let x = line.x_for_offset(c);
1554                absolute.push((c, x));
1555            }
1556            if local_end == char_end {
1557                let x_end = line.x_for_offset(local_end);
1558                absolute.push((local_end, x_end));
1559            }
1560        }
1561
1562        if absolute.is_empty() {
1563            return Vec::new();
1564        }
1565
1566        absolute.sort_by_key(|(c, _)| *c);
1567
1568        let base_x = absolute.first().map(|(_, x)| *x).unwrap_or(0.0);
1569        let mut out: Vec<CharacterGeometry> = Vec::with_capacity(absolute.len());
1570        for window in absolute.windows(2) {
1571            let (c, x) = window[0];
1572            let (_, x_next) = window[1];
1573            if c >= char_end {
1574                break;
1575            }
1576            out.push(CharacterGeometry {
1577                position: x - base_x,
1578                width: (x_next - x).max(0.0),
1579            });
1580        }
1581        out
1582    }
1583
1584    /// Screen-space caret rectangle at a document position with the
1585    /// given affinity, as `[x, y, width, height]`. Feed this to the
1586    /// platform IME for composition window placement. For drawing the
1587    /// caret itself, use the `DecorationKind::Cursor` entry in
1588    /// [`RenderFrame::decorations`] instead.
1589    ///
1590    /// Affinity only changes the result at soft-wrap boundaries; at
1591    /// every other position the two affinities return the same rect.
1592    /// `CursorAffinity::Downstream` matches the pre-affinity behavior.
1593    pub fn caret_rect(&self, position: usize, affinity: crate::types::CursorAffinity) -> [f32; 4] {
1594        let mut rect = crate::render::hit_test::caret_rect(
1595            &self.flow_layout,
1596            self.scroll_offset,
1597            position,
1598            affinity,
1599        );
1600        rect[0] *= self.zoom;
1601        rect[1] *= self.zoom;
1602        rect[2] *= self.zoom;
1603        rect[3] *= self.zoom;
1604        rect
1605    }
1606
1607    // ── Cursor & colors ────────────────────────────────────────
1608
1609    /// Replace the cursor display with a single cursor.
1610    pub fn set_cursor(&mut self, cursor: &CursorDisplay) {
1611        self.cursors = vec![CursorDisplay {
1612            position: cursor.position,
1613            anchor: cursor.anchor,
1614            affinity: cursor.affinity,
1615            visible: cursor.visible,
1616            selected_cells: cursor.selected_cells.clone(),
1617        }];
1618    }
1619
1620    /// Replace the cursor display with multiple cursors (multi-caret
1621    /// editing). Each cursor independently generates a caret and
1622    /// optional selection highlight.
1623    pub fn set_cursors(&mut self, cursors: &[CursorDisplay]) {
1624        self.cursors = cursors
1625            .iter()
1626            .map(|c| CursorDisplay {
1627                position: c.position,
1628                anchor: c.anchor,
1629                affinity: c.affinity,
1630                visible: c.visible,
1631                selected_cells: c.selected_cells.clone(),
1632            })
1633            .collect();
1634    }
1635
1636    /// Set the selection highlight color `[r, g, b, a]` in 0..=1
1637    /// space. Default: `[0.26, 0.52, 0.96, 0.3]` (translucent blue).
1638    pub fn set_selection_color(&mut self, color: [f32; 4]) {
1639        self.selection_color = color;
1640    }
1641
1642    /// Set the caret color `[r, g, b, a]`. Default: black.
1643    pub fn set_cursor_color(&mut self, color: [f32; 4]) {
1644        self.cursor_color = color;
1645    }
1646
1647    /// Set the default text color `[r, g, b, a]`, used when a
1648    /// fragment has no explicit `foreground_color`. Default: black.
1649    pub fn set_text_color(&mut self, color: [f32; 4]) {
1650        self.text_color = color;
1651    }
1652
1653    /// Current default text color.
1654    pub fn text_color(&self) -> [f32; 4] {
1655        self.text_color
1656    }
1657
1658    /// Set the background painted behind fenced code blocks when the
1659    /// block carries no explicit `background_color`. Hosts wire this
1660    /// from the active theme so dark / light swaps reach the cards.
1661    /// Default `[0.95, 0.95, 0.95, 1.0]` (light grey). Affects future
1662    /// `layout_full` / `relayout_block` calls; existing layouts keep
1663    /// their already-converted background until they next re-shape.
1664    pub fn set_code_block_background(&mut self, color: [f32; 4]) {
1665        self.code_block_background = color;
1666    }
1667
1668    /// Current code-block background default.
1669    pub fn code_block_background(&self) -> [f32; 4] {
1670        self.code_block_background
1671    }
1672
1673    /// Auto-hyphenate justified blocks (that don't set `hyphenate`
1674    /// explicitly) on future `layout_full` / `relayout_block` calls.
1675    /// Enable on prose/rich-text surfaces; leave off for single-line or
1676    /// label widgets. Default `false`.
1677    pub fn set_hyphenate_justified(&mut self, enabled: bool) {
1678        self.hyphenate_justified = enabled;
1679    }
1680
1681    /// Whether justified blocks are auto-hyphenated.
1682    pub fn hyphenate_justified(&self) -> bool {
1683        self.hyphenate_justified
1684    }
1685
1686    /// Set the foreground used for monospaced runs (inline `code`,
1687    /// fenced code blocks) that carry no explicit `foreground_color`.
1688    /// `None` (default) keeps the engine's `text_color`. Hosts wire
1689    /// this from the active theme alongside `set_code_block_background`.
1690    pub fn set_code_block_foreground(&mut self, color: Option<[f32; 4]>) {
1691        self.code_block_foreground = color;
1692    }
1693
1694    /// Current code-block foreground override.
1695    pub fn code_block_foreground(&self) -> Option<[f32; 4]> {
1696        self.code_block_foreground
1697    }
1698
1699    /// Set the echo / masking character for secure (password) fields.
1700    ///
1701    /// When `Some(c)`, every character laid out by future `layout_full`
1702    /// calls is replaced with `c` before shaping, so the real text never
1703    /// reaches the shaper or the glyph atlas. `None` (default) lays text
1704    /// out verbatim. One echo char is emitted per source `char`,
1705    /// preserving char counts so caret / selection / hit-test (all
1706    /// char-indexed) stay aligned with the host document's positions.
1707    ///
1708    /// Affects future `layout_full` calls; existing layouts keep their
1709    /// already-converted glyphs until they next re-shape. The incremental
1710    /// `relayout_block` path takes pre-converted [`BlockLayoutParams`], so
1711    /// hosts driving that path must thread the same echo char through
1712    /// their own [`crate::bridge::BridgeOptions`].
1713    pub fn set_echo_char(&mut self, echo: Option<char>) {
1714        self.echo_char = echo;
1715    }
1716
1717    /// Current echo / masking character, if any.
1718    pub fn echo_char(&self) -> Option<char> {
1719        self.echo_char
1720    }
1721
1722    // ── Scrolling helpers ──────────────────────────────────────
1723
1724    /// Visual position and height of a laid-out block. Returns
1725    /// `None` if `block_id` is not in the current layout.
1726    pub fn block_visual_info(&self, block_id: usize) -> Option<BlockVisualInfo> {
1727        let block = self.flow_layout.blocks.get(&block_id)?;
1728        Some(BlockVisualInfo {
1729            block_id,
1730            y: block.y,
1731            height: block.height,
1732        })
1733    }
1734
1735    /// Whether a block lives inside any table cell.
1736    pub fn is_block_in_table(&self, block_id: usize) -> bool {
1737        self.flow_layout.tables.values().any(|table| {
1738            table
1739                .cell_layouts
1740                .iter()
1741                .any(|cell| cell.blocks.iter().any(|b| b.block_id == block_id))
1742        })
1743    }
1744
1745    /// Scroll so that `position` is visible, placing it roughly one
1746    /// third from the top of the viewport. Returns the new offset.
1747    /// Affinity defaults to `Downstream` since scroll targeting picks
1748    /// any acceptable line for the position.
1749    pub fn scroll_to_position(&mut self, position: usize) -> f32 {
1750        let rect = crate::render::hit_test::caret_rect(
1751            &self.flow_layout,
1752            self.scroll_offset,
1753            position,
1754            crate::types::CursorAffinity::Downstream,
1755        );
1756        let target_y = rect[1] + self.scroll_offset - self.viewport_height / (3.0 * self.zoom);
1757        self.scroll_offset = target_y.max(0.0);
1758        self.scroll_offset
1759    }
1760
1761    /// Scroll the minimum amount needed to make the current caret
1762    /// visible. Call after arrow-key / click / typing. Returns
1763    /// `Some(new_offset)` if the scroll moved, `None` otherwise.
1764    pub fn ensure_caret_visible(&mut self) -> Option<f32> {
1765        if self.cursors.is_empty() {
1766            return None;
1767        }
1768        let pos = self.cursors[0].position;
1769        let affinity = self.cursors[0].affinity;
1770        let rect = crate::render::hit_test::caret_rect(
1771            &self.flow_layout,
1772            self.scroll_offset,
1773            pos,
1774            affinity,
1775        );
1776        let caret_screen_y = rect[1];
1777        let caret_screen_bottom = caret_screen_y + rect[3];
1778        let effective_vh = self.viewport_height / self.zoom;
1779        let margin = 10.0 / self.zoom;
1780        let old_offset = self.scroll_offset;
1781
1782        if caret_screen_y < 0.0 {
1783            self.scroll_offset += caret_screen_y - margin;
1784            self.scroll_offset = self.scroll_offset.max(0.0);
1785        } else if caret_screen_bottom > effective_vh {
1786            self.scroll_offset += caret_screen_bottom - effective_vh + margin;
1787        }
1788
1789        if (self.scroll_offset - old_offset).abs() > 0.001 {
1790            Some(self.scroll_offset)
1791        } else {
1792            None
1793        }
1794    }
1795}
1796
1797impl Default for DocumentFlow {
1798    fn default() -> Self {
1799        Self::new()
1800    }
1801}
1802
1803#[cfg(feature = "text-document")]
1804enum FlowItemKind {
1805    Block(BlockLayoutParams),
1806    Table(TableLayoutParams),
1807    Frame(FrameLayoutParams),
1808}
1809
1810/// Rasterize a single glyph into the service's atlas and append a
1811/// `GlyphQuad` to the output vec. Shared between
1812/// [`DocumentFlow::layout_single_line`] and
1813/// [`DocumentFlow::layout_paragraph`] (plus the markup variants).
1814///
1815/// `raster_scale` densifies the bitmap without touching layout: the
1816/// glyph is rasterized at `size × scale_factor × raster_scale`
1817/// physical pixels while the emitted `screen` rect stays in logical
1818/// pixels (divided by the *total* scale), so content drawn under a
1819/// scale transform (scene zoom) samples a matching-resolution bitmap
1820/// instead of stretching a 1× raster. Scaled rasters are unhinted —
1821/// glyph positions come from shaping at the logical ppem.
1822#[allow(clippy::too_many_arguments)]
1823fn rasterize_glyph_quad(
1824    service: &mut TextFontService,
1825    glyph: &ShapedGlyph,
1826    run: &ShapedRun,
1827    pen_x: f32,
1828    baseline: f32,
1829    text_color: [f32; 4],
1830    raster_scale: f32,
1831    quads: &mut Vec<GlyphQuad>,
1832    glyph_keys: &mut Vec<crate::atlas::cache::GlyphCacheKey>,
1833) {
1834    use crate::atlas::cache::GlyphCacheKey;
1835    use crate::atlas::rasterizer::rasterize_glyph;
1836
1837    if glyph.glyph_id == 0 {
1838        return;
1839    }
1840
1841    let entry = match service.font_registry.get(glyph.font_face_id) {
1842        Some(e) => e,
1843        None => return,
1844    };
1845
1846    let raster_scale = if raster_scale > 0.0 {
1847        raster_scale
1848    } else {
1849        1.0
1850    };
1851    let hinted = raster_scale == 1.0;
1852    let sf = service.scale_factor.max(f32::MIN_POSITIVE);
1853    let inv_total = 1.0 / (sf * raster_scale);
1854    let physical_size_px = run.size_px * sf * raster_scale;
1855    let cache_key = GlyphCacheKey::with_weight(
1856        glyph.font_face_id,
1857        glyph.glyph_id,
1858        physical_size_px,
1859        run.weight as u32,
1860        hinted,
1861    );
1862
1863    if service.glyph_cache.peek(&cache_key).is_none()
1864        && let Some(image) = rasterize_glyph(
1865            &mut service.scale_context,
1866            entry.bytes(),
1867            entry.face_index,
1868            entry.swash_cache_key,
1869            glyph.glyph_id,
1870            physical_size_px,
1871            run.weight as u32,
1872            hinted,
1873        )
1874        && image.width > 0
1875        && image.height > 0
1876    {
1877        let (alloc, evicted) = crate::atlas::allocate_or_evict(
1878            &mut service.atlas,
1879            &mut service.glyph_cache,
1880            image.width,
1881            image.height,
1882        );
1883        if evicted {
1884            service.eviction_epoch = service.eviction_epoch.wrapping_add(1);
1885        }
1886        if let Some(alloc) = alloc {
1887            let rect = alloc.rectangle;
1888            let atlas_x = rect.min.x as u32;
1889            let atlas_y = rect.min.y as u32;
1890            if image.is_color {
1891                service
1892                    .atlas
1893                    .blit_rgba(atlas_x, atlas_y, image.width, image.height, &image.data);
1894            } else {
1895                service
1896                    .atlas
1897                    .blit_mask(atlas_x, atlas_y, image.width, image.height, &image.data);
1898            }
1899            service.glyph_cache.insert(
1900                cache_key,
1901                crate::atlas::cache::CachedGlyph {
1902                    alloc_id: alloc.id,
1903                    atlas_x,
1904                    atlas_y,
1905                    width: image.width,
1906                    height: image.height,
1907                    placement_left: image.placement_left,
1908                    placement_top: image.placement_top,
1909                    is_color: image.is_color,
1910                    last_used: 0,
1911                },
1912            );
1913        }
1914    }
1915
1916    if let Some(cached) = service.glyph_cache.get(&cache_key) {
1917        let logical_w = cached.width as f32 * inv_total;
1918        let logical_h = cached.height as f32 * inv_total;
1919        let logical_left = cached.placement_left as f32 * inv_total;
1920        let logical_top = cached.placement_top as f32 * inv_total;
1921        let screen_x = pen_x + glyph.x_offset + logical_left;
1922        let screen_y = baseline - glyph.y_offset - logical_top;
1923        let color = if cached.is_color {
1924            [1.0, 1.0, 1.0, 1.0]
1925        } else {
1926            text_color
1927        };
1928        quads.push(GlyphQuad {
1929            screen: [screen_x, screen_y, logical_w, logical_h],
1930            atlas: [
1931                cached.atlas_x as f32,
1932                cached.atlas_y as f32,
1933                cached.width as f32,
1934                cached.height as f32,
1935            ],
1936            color,
1937            is_color: cached.is_color,
1938        });
1939        glyph_keys.push(cache_key);
1940    }
1941}
1942
1943/// Scale all screen-space coordinates in a RenderFrame by `zoom`.
1944fn apply_zoom(frame: &mut RenderFrame, zoom: f32) {
1945    if (zoom - 1.0).abs() <= f32::EPSILON {
1946        return;
1947    }
1948    for q in &mut frame.glyphs {
1949        q.screen[0] *= zoom;
1950        q.screen[1] *= zoom;
1951        q.screen[2] *= zoom;
1952        q.screen[3] *= zoom;
1953    }
1954    for q in &mut frame.images {
1955        q.screen[0] *= zoom;
1956        q.screen[1] *= zoom;
1957        q.screen[2] *= zoom;
1958        q.screen[3] *= zoom;
1959    }
1960    apply_zoom_decorations(&mut frame.decorations, zoom);
1961}
1962
1963/// Scale all screen-space coordinates in decoration rects by `zoom`.
1964fn apply_zoom_decorations(decorations: &mut [DecorationRect], zoom: f32) {
1965    if (zoom - 1.0).abs() <= f32::EPSILON {
1966        return;
1967    }
1968    for d in decorations.iter_mut() {
1969        d.rect[0] *= zoom;
1970        d.rect[1] *= zoom;
1971        d.rect[2] *= zoom;
1972        d.rect[3] *= zoom;
1973    }
1974}
1975
1976/// Derive a per-span [`TextFormat`] from a base format and inline
1977/// markup attributes (bold / italic).
1978fn merge_format(base: &TextFormat, attrs: InlineAttrs) -> TextFormat {
1979    let mut fmt = base.clone();
1980    if attrs.is_bold() {
1981        fmt.font_bold = Some(true);
1982        if let Some(w) = fmt.font_weight
1983            && w < 600
1984        {
1985            fmt.font_weight = Some(700);
1986        } else if fmt.font_weight.is_none() {
1987            fmt.font_weight = Some(700);
1988        }
1989    }
1990    if attrs.is_italic() {
1991        fmt.font_italic = Some(true);
1992    }
1993    fmt
1994}
1995
1996#[cfg(test)]
1997mod tests {
1998    use super::*;
1999    use crate::layout::block::{BlockLayoutParams, FragmentParams};
2000    use crate::layout::paragraph::Alignment;
2001    use crate::types::{UnderlineStyle, VerticalAlignment};
2002
2003    const NOTO_SANS: &[u8] = include_bytes!("../test-fonts/NotoSans-Variable.ttf");
2004
2005    fn service() -> TextFontService {
2006        // Hermetic: don't pull in the host machine's fonts.
2007        let mut s = TextFontService::new_without_system_fonts();
2008        let face = s.register_font(NOTO_SANS);
2009        s.set_default_font(face, 16.0);
2010        s
2011    }
2012
2013    fn block(id: usize, text: &str) -> BlockLayoutParams {
2014        BlockLayoutParams {
2015            block_id: id,
2016            position: 0,
2017            text: text.to_string(),
2018            fragments: vec![FragmentParams {
2019                text: text.to_string(),
2020                offset: 0,
2021                length: text.len(),
2022                font_family: None,
2023                font_weight: None,
2024                font_bold: None,
2025                font_italic: None,
2026                font_point_size: None,
2027                underline_style: UnderlineStyle::None,
2028                overline: false,
2029                strikeout: false,
2030                is_link: false,
2031                letter_spacing: 0.0,
2032                word_spacing: 0.0,
2033                foreground_color: None,
2034                underline_color: None,
2035                background_color: None,
2036                anchor_href: None,
2037                tooltip: None,
2038                vertical_alignment: VerticalAlignment::Normal,
2039                image_name: None,
2040                image_width: 0.0,
2041                image_height: 0.0,
2042                features: Vec::new(),
2043            }],
2044            alignment: Alignment::Left,
2045            top_margin: 0.0,
2046            bottom_margin: 0.0,
2047            left_margin: 0.0,
2048            right_margin: 0.0,
2049            text_indent: 0.0,
2050            list_marker: String::new(),
2051            list_indent: 0.0,
2052            tab_positions: vec![],
2053            line_height_multiplier: None,
2054            non_breakable_lines: false,
2055            hyphenation: None,
2056            checkbox: None,
2057            background_color: None,
2058        }
2059    }
2060
2061    #[test]
2062    fn relayout_block_returns_no_layout_when_never_laid_out() {
2063        let svc = service();
2064        let mut flow = DocumentFlow::new();
2065        flow.set_viewport(400.0, 200.0);
2066        let err = flow.relayout_block(&svc, &block(1, "Hello")).unwrap_err();
2067        assert_eq!(err, RelayoutError::NoLayout);
2068    }
2069
2070    #[test]
2071    fn relayout_block_returns_scale_dirty_after_scale_factor_change() {
2072        let mut svc = service();
2073        let mut flow = DocumentFlow::new();
2074        flow.set_viewport(400.0, 200.0);
2075        flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2076        assert!(flow.has_layout());
2077
2078        // Simulate a HiDPI transition on the shared service.
2079        svc.set_scale_factor(2.0);
2080        assert!(flow.layout_dirty_for_scale(&svc));
2081
2082        let err = flow
2083            .relayout_block(&svc, &block(1, "Hello world"))
2084            .unwrap_err();
2085        assert_eq!(err, RelayoutError::ScaleDirty);
2086    }
2087
2088    #[test]
2089    fn relayout_block_succeeds_after_fresh_layout_post_scale_change() {
2090        let mut svc = service();
2091        let mut flow = DocumentFlow::new();
2092        flow.set_viewport(400.0, 200.0);
2093        flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2094
2095        svc.set_scale_factor(2.0);
2096        // Caller is expected to re-run a full layout at the new
2097        // scale before issuing incremental updates.
2098        flow.layout_blocks(&svc, vec![block(1, "Hello")]);
2099        assert!(!flow.layout_dirty_for_scale(&svc));
2100
2101        // Now the incremental path succeeds.
2102        flow.relayout_block(&svc, &block(1, "Hello world"))
2103            .expect("relayout_block must succeed after a fresh post-scale layout");
2104    }
2105}