Skip to main content

incular_text/
engine.rs

1//! Parley-backed text shaping, fallback, bidi ordering, and line breaking.
2//!
3//! Incular deliberately keeps its renderer-facing glyph protocol small. Parley
4//! owns all font discovery, fallback selection and shaping; this module only
5//! translates its positioned glyph runs into `incular-rendering` commands.
6
7use super::{FontFamily, FontStyle, TextAffinity, TextAlign, TextOverflow, TextStyle};
8use icu_segmenter::GraphemeClusterSegmenter;
9pub use incular_assets::FontId;
10use incular_assets::{FontBytes, FontHandle};
11use incular_core::{Offset, Size};
12use incular_rendering::{GlyphPosition, GlyphRun};
13use parley::{
14    Alignment, AlignmentOptions, FontContext, FontStack, FontWeight as ParleyFontWeight, Layout,
15    LayoutContext, PositionedLayoutItem, StyleProperty,
16};
17use std::{
18    borrow::Cow,
19    collections::{HashMap, HashSet, VecDeque, hash_map::DefaultHasher},
20    hash::{Hash, Hasher},
21    sync::{Arc, OnceLock},
22};
23
24#[derive(Clone, Copy, Debug, PartialEq)]
25pub struct TextMetrics {
26    pub size: Size,
27    pub baseline: f32,
28    pub line_height: f32,
29}
30
31/// One shaped cluster's visual span on a line, in text-area coordinates
32/// with `left <= right`. Spans follow visual order, so a logical byte
33/// range can cover several disjoint spans in mixed-direction text.
34/// Consumers merge spans intersecting a selection instead of assuming
35/// the range maps to one rectangle. Zero-advance clusters are kept because
36/// they still own caret stops; selection ignores them.
37#[derive(Clone, Copy, Debug)]
38pub struct TextClusterSpan {
39    pub start: usize,
40    pub end: usize,
41    pub left: f32,
42    pub right: f32,
43    /// Whether the cluster belongs to a right-to-left run.
44    pub rtl: bool,
45}
46
47impl TextLine {
48    /// Visual x-intervals covered by a half-open logical byte range on
49    /// this line. Collapsed and reversed ranges yield nothing by
50    /// policy; callers pass normalized ranges and never rely on
51    /// rejection. Caret rendering stays separate: collapsed positions
52    /// highlight nothing here and resolve through caret stops instead.
53    ///
54    /// Four granularities meet here and are not interchangeable: the
55    /// range arrives in UTF-8 bytes (clamped to char boundaries
56    /// upstream, so mid-char bytes never arrive); shaping clusters are
57    /// the coverage granularity (a range splitting a cluster expands
58    /// to the whole shaped cluster, since no visual edge exists
59    /// inside it); visual order comes from the stored spans, never
60    /// from re-sorting bytes or glyphs downstream. Touching spans
61    /// merge; genuinely disjoint spans stay separate. An empty line
62    /// covered by a nonempty range yields its caret point. Observed
63    /// clusters here are char-or-finer, so wider-than-grapheme
64    /// clusters would follow the same rule untested.
65    #[must_use]
66    pub fn selection_spans(&self, start: usize, end: usize) -> Vec<(f32, f32)> {
67        if start >= end {
68            return Vec::new();
69        }
70        let mut spans: Vec<(f32, f32)> = self
71            .clusters
72            .iter()
73            .filter(|span| span.left < span.right && span.start < end && start < span.end)
74            .map(|span| (span.left, span.right))
75            .collect();
76        if spans.is_empty() && self.start == self.end && start <= self.start && self.end <= end {
77            spans.push((self.offset, self.offset));
78        }
79        spans.sort_by(|left, right| left.0.total_cmp(&right.0));
80        let mut merged: Vec<(f32, f32)> = Vec::with_capacity(spans.len());
81        for (left, right) in spans {
82            if let Some(last) = merged.last_mut()
83                && left <= last.1
84            {
85                last.1 = last.1.max(right);
86            } else {
87                merged.push((left, right));
88            }
89        }
90        merged
91    }
92}
93
94#[derive(Clone, Debug)]
95pub struct TextLine {
96    pub runs: Arc<[Arc<GlyphRun>]>,
97    pub glyphs: Arc<[GlyphPosition]>,
98    pub clusters: Arc<[TextClusterSpan]>,
99    pub width: f32,
100    /// Horizontal alignment offset in text-area coordinates. Glyph and
101    /// caret x positions already include it; `width` stays the unshifted
102    /// advance so extent checks keep working. Consumers mapping absolute
103    /// x positions must add this to `width` instead of re-deriving the
104    /// alignment.
105    pub offset: f32,
106    pub baseline: f32,
107    pub start: usize,
108    /// End of selectable/caret text on this visual line. This excludes a
109    /// trailing hard line-break which has no shaped glyph of its own.
110    pub caret_end: usize,
111    pub end: usize,
112}
113
114#[derive(Clone, Debug)]
115pub struct FontRunDebug {
116    pub range: std::ops::Range<usize>,
117    pub font: FontId,
118    pub direction: &'static str,
119}
120
121/// A visual insertion point emitted by the shaping engine.
122///
123/// A logical byte offset can have two distinct visual positions at a bidi run
124/// boundary. `affinity` keeps those positions separate, matching the concept
125/// used by Parley's cursor implementation and preventing an arrow key from
126/// jumping through a right-to-left run in logical UTF-8 order.
127#[derive(Clone, Copy, Debug, PartialEq)]
128pub struct TextCaretPosition {
129    pub offset: usize,
130    pub affinity: TextAffinity,
131    pub x: f32,
132    pub line: usize,
133}
134
135#[derive(Clone, Debug)]
136struct ClusterPosition {
137    range: std::ops::Range<usize>,
138    left: f32,
139    right: f32,
140    rtl: bool,
141    ligature_start: bool,
142    ligature_continuation: bool,
143}
144
145impl ClusterPosition {
146    fn include(&mut self, other: &Self) {
147        self.range.start = self.range.start.min(other.range.start);
148        self.range.end = self.range.end.max(other.range.end);
149        self.left = self.left.min(other.left);
150        self.right = self.right.max(other.right);
151        self.ligature_start |= other.ligature_start;
152        self.ligature_continuation |= other.ligature_continuation;
153    }
154
155    fn can_join(&self, other: &Self) -> bool {
156        self.rtl == other.rtl
157            && if self.rtl {
158                other.range.end == self.range.start
159            } else {
160                self.range.end == other.range.start
161            }
162    }
163}
164
165#[derive(Clone, Debug)]
166pub struct TextLayout {
167    pub lines: Arc<[TextLine]>,
168    pub metrics: TextMetrics,
169    pub font_runs: Arc<[FontRunDebug]>,
170    /// Caret stops of every line, derived from the line clusters on first
171    /// use: only editing and selection need them.
172    caret_positions: OnceLock<Arc<[TextCaretPosition]>>,
173    /// Whether the source had content outside the retained layout's visible
174    /// bounds. `Visible` layouts deliberately leave this false.
175    pub overflowed: bool,
176}
177
178/// Paragraph-level layout controls. They deliberately describe behavior in
179/// Incular terms and do not expose Parley builder types.
180#[derive(Clone, Copy, Debug, PartialEq)]
181pub struct TextLayoutOptions {
182    pub max_width: Option<f32>,
183    pub align: TextAlign,
184    pub soft_wrap: bool,
185    pub max_lines: Option<usize>,
186    pub overflow: TextOverflow,
187}
188
189impl TextLayoutOptions {
190    #[must_use]
191    pub const fn new(max_width: Option<f32>, align: TextAlign) -> Self {
192        Self {
193            max_width,
194            align,
195            soft_wrap: true,
196            max_lines: None,
197            overflow: TextOverflow::Clip,
198        }
199    }
200
201    #[must_use]
202    pub const fn soft_wrap(mut self, soft_wrap: bool) -> Self {
203        self.soft_wrap = soft_wrap;
204        self
205    }
206
207    #[must_use]
208    pub const fn max_lines(mut self, max_lines: Option<usize>) -> Self {
209        self.max_lines = max_lines;
210        self
211    }
212
213    #[must_use]
214    pub const fn overflow(mut self, overflow: TextOverflow) -> Self {
215        self.overflow = overflow;
216        self
217    }
218}
219
220impl Default for TextLayoutOptions {
221    fn default() -> Self {
222        Self::new(None, TextAlign::Start)
223    }
224}
225
226impl TextLayout {
227    #[must_use]
228    pub fn glyph_count(&self) -> usize {
229        self.lines.iter().map(|line| line.glyphs.len()).sum()
230    }
231
232    /// Returns the visual insertion points for one laid-out line in
233    /// left-to-right screen order.
234    #[must_use]
235    pub fn line_caret_positions(&self, line: usize) -> &[TextCaretPosition] {
236        let caret_positions = self
237            .caret_positions
238            .get_or_init(|| self.derive_caret_positions());
239        let Some(start) = caret_positions
240            .iter()
241            .position(|position| position.line == line)
242        else {
243            return &[];
244        };
245        let end = start
246            + caret_positions
247                .iter()
248                .skip(start)
249                .take_while(|position| position.line == line)
250                .count();
251        &caret_positions[start..end]
252    }
253
254    /// Two stops per cluster (logical start and end, placed by direction),
255    /// plus the line's start and caret end when no cluster supplies them,
256    /// sorted into screen order.
257    fn derive_caret_positions(&self) -> Arc<[TextCaretPosition]> {
258        let mut positions = Vec::new();
259        for (index, line) in self.lines.iter().enumerate() {
260            let stop = |offset, affinity, x| TextCaretPosition {
261                offset,
262                affinity,
263                x,
264                line: index,
265            };
266            let first = positions.len();
267            for cluster in line.clusters.iter() {
268                let (left, right) = if cluster.rtl {
269                    (cluster.end, cluster.start)
270                } else {
271                    (cluster.start, cluster.end)
272                };
273                let (left_affinity, right_affinity) = if cluster.rtl {
274                    (TextAffinity::Upstream, TextAffinity::Downstream)
275                } else {
276                    (TextAffinity::Downstream, TextAffinity::Upstream)
277                };
278                positions.push(stop(left, left_affinity, cluster.left));
279                positions.push(stop(right, right_affinity, cluster.right));
280            }
281            let line_stops = &positions[first..];
282            if !line_stops.iter().any(|stop| stop.offset == line.start) {
283                positions.push(stop(line.start, TextAffinity::Downstream, line.offset));
284            }
285            if line.caret_end > line.start
286                && !positions[first..]
287                    .iter()
288                    .any(|stop| stop.offset == line.caret_end)
289            {
290                positions.push(stop(
291                    line.caret_end,
292                    TextAffinity::Upstream,
293                    line.offset + line.width,
294                ));
295            }
296            positions[first..].sort_by(|left, right| {
297                left.x
298                    .total_cmp(&right.x)
299                    .then_with(|| left.offset.cmp(&right.offset))
300            });
301        }
302        positions.into()
303    }
304}
305
306/// Layout/cache counters. Legacy field names are retained for source
307/// compatibility; Parley performs fallback and coverage resolution internally.
308#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
309pub struct TextDiagnostics {
310    pub layouts_requested: u64,
311    pub cache_hits: u64,
312    pub cache_misses: u64,
313    pub font_resolution_requests: u64,
314    pub font_resolution_cache_hits: u64,
315    pub font_resolution_cache_misses: u64,
316    pub coverage_queries: u64,
317    pub fallback_runs: u64,
318    pub fallback_fonts_used: u64,
319    pub missing_clusters: u64,
320    pub shaping_runs: u64,
321    // Task 15 document-granularity instrumentation.
322    /// Paragraphs examined by the document path.
323    pub paragraphs_considered: u64,
324    /// Paragraphs that required fresh shaping (cache misses at paragraph
325    /// granularity).
326    pub paragraphs_reshaped: u64,
327    /// Parley layouts satisfied entirely from cache.
328    pub parley_layouts_reused: u64,
329    /// Multi-paragraph documents assembled from retained paragraphs.
330    pub documents_composed: u64,
331}
332
333#[derive(Hash, PartialEq, Eq)]
334struct LayoutKey {
335    text: String,
336    family: FontFamily,
337    fallbacks: Arc<[FontFamily]>,
338    size: u32,
339    weight: u16,
340    style: FontStyle,
341    line_height: Option<(u8, u32)>,
342    letter_spacing: u32,
343    font_variations: Option<Arc<str>>,
344    font_features: Option<Arc<str>>,
345    width: Option<u32>,
346    align: TextAlign,
347    soft_wrap: bool,
348    max_lines: Option<usize>,
349    overflow: TextOverflow,
350    generation: u64,
351}
352
353impl LayoutKey {
354    fn new(text: &str, style: &TextStyle, options: TextLayoutOptions, generation: u64) -> Self {
355        Self {
356            text: text.into(),
357            family: style.family.clone(),
358            fallbacks: style.fallback_families.clone(),
359            size: style.size.to_bits(),
360            weight: style.weight.0,
361            style: style.style,
362            line_height: style.line_height.map(|lh| match lh {
363                crate::LineHeight::Normal => (0, 0),
364                crate::LineHeight::Multiplier(m) => (1, m.to_bits()),
365                crate::LineHeight::Absolute(a) => (2, a.to_bits()),
366            }),
367            letter_spacing: style.letter_spacing.to_bits(),
368            font_variations: style.font_variations.clone(),
369            font_features: style.font_features.clone(),
370            width: options.max_width.map(f32::to_bits),
371            align: options.align,
372            soft_wrap: options.soft_wrap,
373            max_lines: options.max_lines,
374            overflow: options.overflow,
375            generation,
376        }
377    }
378}
379
380/// The sole owner of font discovery and app-registered fonts. Parley owns the
381/// actual font collection and resolves scripts, bidi direction and fallback.
382pub struct TextEngine {
383    /// One `FontHandle` per distinct font blob. Handles share the font bytes
384    /// through a single `Arc`, so every cached text layout references the
385    /// same allocation instead of holding a private copy of the whole font
386    /// file (the Task 15 root cause of ~500 KB retained per shaped string
387    /// and a full-file memcpy plus full-file hash per glyph run).
388    font_handles: HashMap<(usize, usize, u32), FontHandle>,
389    font_context: FontContext,
390    layout_context: LayoutContext<()>,
391    generation: u64,
392    cache: HashMap<Arc<LayoutKey>, Arc<TextLayout>>,
393    order: VecDeque<Arc<LayoutKey>>,
394    diagnostics: TextDiagnostics,
395}
396
397impl Default for TextEngine {
398    fn default() -> Self {
399        Self::new()
400    }
401}
402
403impl TextEngine {
404    #[must_use]
405    pub fn new() -> Self {
406        Self {
407            font_handles: HashMap::new(),
408            font_context: FontContext::new(),
409            layout_context: LayoutContext::new(),
410            generation: 1,
411            cache: HashMap::new(),
412            order: VecDeque::new(),
413            diagnostics: TextDiagnostics::default(),
414        }
415    }
416
417    #[must_use]
418    pub const fn diagnostics(&self) -> TextDiagnostics {
419        self.diagnostics
420    }
421
422    #[must_use]
423    pub const fn font_database_generation(&self) -> u64 {
424        self.generation
425    }
426
427    /// Registers all faces in a TrueType/OpenType byte buffer with Parley's
428    /// Fontique collection. Invalid buffers are harmless: Fontique ignores
429    /// them, while the generation still invalidates stale cached layouts.
430    pub fn register_font_bytes(&mut self, bytes: impl Into<Vec<u8>>) {
431        self.font_context
432            .collection
433            .register_fonts(parley::fontique::Blob::from(bytes.into()), None);
434        self.generation = self
435            .generation
436            .checked_add(1)
437            .expect("font database generation exhausted");
438        self.cache.clear();
439        self.order.clear();
440    }
441
442    pub fn layout(
443        &mut self,
444        text: &str,
445        style: &TextStyle,
446        max_width: Option<f32>,
447        align: TextAlign,
448    ) -> Arc<TextLayout> {
449        self.layout_with_options(text, style, TextLayoutOptions::new(max_width, align))
450    }
451
452    /// Shapes a paragraph with retained wrapping and overflow policy.
453    pub fn layout_with_options(
454        &mut self,
455        text: &str,
456        style: &TextStyle,
457        options: TextLayoutOptions,
458    ) -> Arc<TextLayout> {
459        self.diagnostics.layouts_requested += 1;
460        // Documents with hard line breaks compose from retained per-paragraph
461        // layouts so editing one paragraph never reshapes the rest. Overflow
462        // policies keep the exact monolithic semantics and stay on the
463        // whole-string path.
464        // With soft wrapping, `Clip` can only trigger on single-line
465        // horizontal overflow, which a hard-break document never has; so this
466        // route preserves monolithic semantics exactly.
467        if text.contains('\n')
468            && options.max_lines.is_none()
469            && (options.soft_wrap || options.overflow == TextOverflow::Visible)
470        {
471            let layout = self.layout_document(text, style, options);
472            return Arc::new(layout);
473        }
474        let key = LayoutKey::new(text, style, options, self.generation);
475        if let Some(layout) = self.cache.get(&key) {
476            self.diagnostics.cache_hits += 1;
477            return layout.clone();
478        }
479
480        self.diagnostics.cache_misses += 1;
481        let layout = Arc::new(self.shape_and_wrap(text, style, options));
482        self.store(key, layout.clone());
483        layout
484    }
485
486    /// FIFO insertion with the measured pool size. The previous 256-entry cap
487    /// thrashed under large-document workloads (Task 15 profiling).
488    fn store(&mut self, key: LayoutKey, layout: Arc<TextLayout>) {
489        const LAYOUT_CACHE_CAPACITY: usize = 2048;
490        if self.order.len() == LAYOUT_CACHE_CAPACITY
491            && let Some(old) = self.order.pop_front()
492        {
493            self.cache.remove(old.as_ref());
494        }
495        // The queue and map own the same key allocation. Cloning LayoutKey here
496        // used to duplicate its text (and any owned family) for every entry.
497        let key = Arc::new(key);
498        self.order.push_back(key.clone());
499        self.cache.insert(key, layout);
500    }
501
502    /// Composes a multi-paragraph document from per-paragraph retained
503    /// layouts. Shaping reuse is exact (paragraph slice == cache key);
504    /// assembly copies line data with vertical and byte-range rebasing.
505    fn layout_document(
506        &mut self,
507        text: &str,
508        style: &TextStyle,
509        options: TextLayoutOptions,
510    ) -> TextLayout {
511        self.diagnostics.documents_composed += 1;
512        let paragraph_options = TextLayoutOptions {
513            max_lines: None,
514            overflow: TextOverflow::Visible,
515            ..options
516        };
517        let mut byte_start = 0usize;
518        let mut lines: Vec<TextLine> = Vec::new();
519        let mut font_runs = Vec::new();
520        let mut width = 0f32;
521        let mut height = 0f32;
522        let mut line_height = 0f32;
523        let mut overflowed = false;
524        for paragraph in text.split('\n') {
525            self.diagnostics.paragraphs_considered += 1;
526            let key = LayoutKey::new(paragraph, style, paragraph_options, self.generation);
527            let paragraph_layout = if let Some(existing) = self.cache.get(&key) {
528                self.diagnostics.parley_layouts_reused += 1;
529                self.diagnostics.cache_hits += 1;
530                existing.clone()
531            } else {
532                self.diagnostics.paragraphs_reshaped += 1;
533                self.diagnostics.cache_misses += 1;
534                let built = Arc::new(self.shape_and_wrap(paragraph, style, paragraph_options));
535                self.store(key, built.clone());
536                built
537            };
538            // Font-run metadata belongs to the paragraph, not each visual line.
539            // Rebase it once even when soft wrapping creates multiple lines.
540            for run in paragraph_layout.font_runs.iter() {
541                let mut rebased = run.clone();
542                rebased.range.start += byte_start;
543                rebased.range.end += byte_start;
544                font_runs.push(rebased);
545            }
546            for line in paragraph_layout.lines.iter() {
547                let glyphs: Vec<GlyphPosition> = line
548                    .glyphs
549                    .iter()
550                    .map(|glyph| {
551                        let mut shifted = *glyph;
552                        // Stored glyph y is negative parley space; moving the
553                        // paragraph down by `height` subtracts from it.
554                        shifted.offset.y -= height;
555                        // Clusters index the full document string, so caret
556                        // and selection mapping stay globally consistent.
557                        shifted.cluster += byte_start as u32;
558                        shifted
559                    })
560                    .collect();
561                let shifted_runs: Vec<Arc<GlyphRun>> = line
562                    .runs
563                    .iter()
564                    .map(|run| {
565                        let mut shifted = (**run).clone();
566                        shifted.origin.y += height;
567                        Arc::new(shifted)
568                    })
569                    .collect();
570                lines.push(TextLine {
571                    runs: shifted_runs.into(),
572                    glyphs: glyphs.into(),
573                    clusters: line
574                        .clusters
575                        .iter()
576                        .map(|span| TextClusterSpan {
577                            start: span.start + byte_start,
578                            end: span.end + byte_start,
579                            left: span.left,
580                            right: span.right,
581                            rtl: span.rtl,
582                        })
583                        .collect(),
584                    width: line.width,
585                    offset: line.offset,
586                    baseline: line.baseline + height,
587                    start: line.start + byte_start,
588                    caret_end: line.caret_end + byte_start,
589                    end: line.end + byte_start,
590                });
591            }
592            width = width.max(paragraph_layout.metrics.size.width);
593            height += paragraph_layout.metrics.size.height;
594            line_height = paragraph_layout.metrics.line_height;
595            overflowed |= paragraph_layout.overflowed;
596            byte_start += paragraph.len() + 1; // include the hard break
597        }
598        if lines.is_empty() {
599            lines.push(TextLine {
600                runs: Arc::new([]),
601                glyphs: Arc::new([]),
602                clusters: Arc::new([]),
603                width: 0.,
604                offset: 0.,
605                baseline: 0.,
606                start: 0,
607                caret_end: 0,
608                end: 0,
609            });
610        }
611        let baseline = lines[0].baseline;
612        TextLayout {
613            lines: lines.into(),
614            metrics: TextMetrics {
615                size: Size::new(width, height),
616                baseline,
617                line_height,
618            },
619            font_runs: font_runs.into(),
620            caret_positions: OnceLock::new(),
621            overflowed,
622        }
623    }
624
625    fn font_stack(style: &TextStyle) -> FontStack<'static> {
626        let mut names = vec![family_name(&style.family)];
627        names.extend(style.fallback_families.iter().map(family_name));
628        FontStack::Source(names.join(", ").into())
629    }
630
631    fn shape_and_wrap(
632        &mut self,
633        text: &str,
634        style: &TextStyle,
635        options: TextLayoutOptions,
636    ) -> TextLayout {
637        let wrap_width = options.soft_wrap.then_some(options.max_width).flatten();
638        let layout = self.shape_raw(text, style, wrap_width, options.max_width, options.align);
639        let horizontal_overflow = !options.soft_wrap
640            && options
641                .max_width
642                .is_some_and(|width| layout.metrics.size.width > width);
643        let line_limit = options
644            .max_lines
645            .or_else(|| horizontal_overflow.then_some(1));
646        let line_overflow = line_limit.is_some_and(|limit| layout.lines.len() > limit);
647        if (!horizontal_overflow && !line_overflow) || options.overflow == TextOverflow::Visible {
648            return layout;
649        }
650
651        let limit = line_limit.unwrap_or(layout.lines.len());
652        match options.overflow {
653            TextOverflow::Visible => layout,
654            TextOverflow::Clip | TextOverflow::Fade => clipped_layout(layout, limit),
655            TextOverflow::Ellipsis => self.ellipsized_layout(text, style, options, limit),
656        }
657    }
658
659    fn ellipsized_layout(
660        &mut self,
661        text: &str,
662        style: &TextStyle,
663        options: TextLayoutOptions,
664        limit: usize,
665    ) -> TextLayout {
666        if limit == 0 {
667            return clipped_layout(
668                self.shape_raw("", style, None, options.max_width, options.align),
669                0,
670            );
671        }
672        let wrap_width = options.soft_wrap.then_some(options.max_width).flatten();
673        let initial = self.shape_raw(text, style, wrap_width, options.max_width, options.align);
674        let end = initial
675            .lines
676            .get(limit.saturating_sub(1))
677            .map_or(0, |line| line.caret_end.min(text.len()));
678        let mut end = grapheme_boundary_at_or_before(text, end);
679
680        loop {
681            let mut candidate = text[..end].to_owned();
682            candidate.push('\u{2026}');
683            // The ellipsis always enters Parley as source text and is shaped
684            // with the selected/fallback font stack, never appended as a raw
685            // renderer glyph or a byte-truncated source suffix.
686            let shaped = self.shape_raw(
687                &candidate,
688                style,
689                wrap_width,
690                options.max_width,
691                options.align,
692            );
693            let fits_width = options.max_width.is_none_or(|width| {
694                shaped
695                    .lines
696                    .iter()
697                    .all(|line| line.width <= width + f32::EPSILON)
698            });
699            if shaped.lines.len() <= limit && fits_width {
700                return TextLayout {
701                    overflowed: true,
702                    ..shaped
703                };
704            }
705            let previous = previous_grapheme_boundary(text, end);
706            if previous == end {
707                return TextLayout {
708                    overflowed: true,
709                    ..shaped
710                };
711            }
712            end = previous;
713        }
714    }
715
716    fn shape_raw(
717        &mut self,
718        text: &str,
719        style: &TextStyle,
720        max_width: Option<f32>,
721        align_width: Option<f32>,
722        align: TextAlign,
723    ) -> TextLayout {
724        let mut builder =
725            self.layout_context
726                .ranged_builder(&mut self.font_context, text, 1.0, true);
727        builder.push_default(StyleProperty::FontStack(Self::font_stack(style)));
728        builder.push_default(StyleProperty::FontSize(style.size.max(0.0)));
729        builder.push_default(StyleProperty::FontWeight(ParleyFontWeight::new(
730            style.weight.0 as f32,
731        )));
732        builder.push_default(StyleProperty::FontStyle(match style.style {
733            FontStyle::Normal => parley::FontStyle::Normal,
734            FontStyle::Italic => parley::FontStyle::Italic,
735        }));
736        builder.push_default(StyleProperty::LetterSpacing(style.letter_spacing));
737        if let Some(settings) = &style.font_variations {
738            builder.push_default(StyleProperty::FontVariations(parley::FontSettings::Source(
739                Cow::Owned(settings.to_string()),
740            )));
741        }
742        if let Some(settings) = &style.font_features {
743            builder.push_default(StyleProperty::FontFeatures(parley::FontSettings::Source(
744                Cow::Owned(settings.to_string()),
745            )));
746        }
747        if let Some(line_height) = style.line_height
748            && let Some(parley_lh) = line_height.to_parley(style.size)
749        {
750            builder.push_default(StyleProperty::LineHeight(parley_lh));
751        }
752
753        let mut layout: Layout<()> = builder.build(text);
754        layout.break_all_lines(max_width);
755        // Alignment resolves against the measured width even when the text
756        // does not wrap: single-line content otherwise ignores alignment
757        // because the wrap width is empty.
758        layout.align(
759            align_width,
760            match align {
761                TextAlign::Start => Alignment::Start,
762                TextAlign::Center => Alignment::Center,
763                TextAlign::End => Alignment::End,
764                TextAlign::Justify => Alignment::Justify,
765            },
766            AlignmentOptions::default(),
767        );
768
769        self.translate_layout(&layout, text)
770    }
771
772    fn font_handle_for(&mut self, font: &parley::FontData) -> FontHandle {
773        font_handle(self, font)
774    }
775
776    fn translate_layout(&mut self, layout: &Layout<()>, text: &str) -> TextLayout {
777        let mut lines = Vec::new();
778        let mut font_runs = Vec::new();
779        let mut used_fonts = HashSet::new();
780        // First glyph run decides the primary font; resolve its handle before
781        // the mutable translation loop so borrowck stays trivial.
782        let primary_handle = layout
783            .lines()
784            .flat_map(|line| line.runs())
785            .next()
786            .map(|run| self.font_handle_for(run.font()));
787        let primary = primary_handle.as_ref().map(FontHandle::id);
788
789        for line in layout.lines() {
790            let metrics = line.metrics();
791            let range = line.text_range();
792            let mut runs: Vec<Arc<GlyphRun>> = Vec::new();
793            let mut glyphs = Vec::new();
794            let mut caret_end = range.start;
795            // Walk Parley's visual cluster advances once for the whole line.
796            // This is the same cell geometry Parley uses for hit testing and
797            // accounts for per-cluster spacing and justified advances. A
798            // source cluster can be split across font/style glyph runs, so
799            // merge duplicate byte ranges into one logical span and caret
800            // pair here rather than deriving cells separately per glyph run.
801            let mut cluster_positions: Vec<ClusterPosition> = Vec::new();
802            let mut cluster_position_index: HashMap<(usize, usize), usize> = HashMap::new();
803            let mut cluster_x = metrics.offset;
804            for run in line.runs() {
805                let rtl = run.is_rtl();
806                for cluster in run.visual_clusters() {
807                    let cluster_range = cluster.text_range();
808                    // Invisible continuation clusters still own source bytes.
809                    // Keep the logical line endpoint through their ranges so
810                    // a final ligature component does not lose its caret stop.
811                    caret_end = caret_end.max(cluster_range.end);
812                    let edge = cluster_x + cluster.advance();
813                    let ligature_start = cluster.is_ligature_start();
814                    let ligature_continuation = cluster.is_ligature_continuation();
815                    let mut cluster_glyphs = cluster.glyphs();
816                    let Some(first_glyph) = cluster_glyphs.next() else {
817                        // A shaping cluster can carry advance for an
818                        // invisible continuation. Ligature components remain
819                        // part of the start cluster's selectable range, while
820                        // standalone join controls have no visible selection
821                        // geometry of their own.
822                        let is_join_control =
823                            text.get(cluster_range.clone()).is_some_and(|component| {
824                                !component.is_empty()
825                                    && component.chars().all(|character| {
826                                        matches!(character, '\u{200C}' | '\u{200D}')
827                                    })
828                            });
829                        if ligature_continuation && !is_join_control {
830                            let position = ClusterPosition {
831                                range: cluster_range.clone(),
832                                left: cluster_x.min(edge),
833                                right: cluster_x.max(edge),
834                                rtl,
835                                ligature_start,
836                                ligature_continuation,
837                            };
838                            let key = (cluster_range.start, cluster_range.end);
839                            if let Some(index) = cluster_position_index.get(&key).copied() {
840                                cluster_positions[index].include(&position);
841                            } else {
842                                cluster_position_index.insert(key, cluster_positions.len());
843                                cluster_positions.push(position);
844                            }
845                        }
846                        cluster_x = edge;
847                        continue;
848                    };
849                    let glyph_advance = std::iter::once(first_glyph)
850                        .chain(cluster_glyphs)
851                        .map(|glyph| glyph.advance)
852                        .sum::<f32>();
853                    // Ligature components can share a larger glyph advance
854                    // than their divided cluster advances; retain the glyph's
855                    // full cell so no caret or selection edge lands inside
856                    // that ligature. Justification and letter spacing can
857                    // also grow the cluster cell beyond its raw glyphs.
858                    let visible_edge = cluster_x + cluster.advance().max(glyph_advance);
859                    let left = cluster_x.min(visible_edge);
860                    let right = cluster_x.max(visible_edge);
861                    let position = ClusterPosition {
862                        range: cluster_range.clone(),
863                        left,
864                        right,
865                        rtl,
866                        ligature_start,
867                        ligature_continuation,
868                    };
869                    let key = (cluster_range.start, cluster_range.end);
870                    if let Some(index) = cluster_position_index.get(&key).copied() {
871                        cluster_positions[index].include(&position);
872                    } else {
873                        cluster_position_index.insert(key, cluster_positions.len());
874                        cluster_positions.push(position);
875                    }
876                    cluster_x = edge;
877                }
878            }
879            for item in line.items() {
880                let PositionedLayoutItem::GlyphRun(parley_run) = item else {
881                    continue;
882                };
883                let run = parley_run.run();
884                let font = self.font_handle_for(run.font());
885                let font_id = font.id();
886                if Some(font_id) != primary {
887                    self.diagnostics.fallback_runs += 1;
888                }
889                used_fonts.insert(font_id);
890                self.diagnostics.shaping_runs += 1;
891
892                // `PositionedLayoutItem::GlyphRun` is a line-local slice of
893                // the underlying shaping run. Parley's renderer-facing Glyph
894                // intentionally has no byte index, so recover each glyph's
895                // cluster from the authoritative visual-cluster sequence
896                // before translating it. This keeps caret movement and
897                // selection on shaped cluster boundaries instead of assigning
898                // every glyph to the run's first byte.
899                //
900                // Order and cardinality correspond structurally, not by
901                // length coincidence: `glyphs()` and `positioned_glyphs()`
902                // both iterate `Run::visual_clusters().flat_map(glyphs)`,
903                // the latter only adding running x (which already carries
904                // the line offset). The window below re-locates this
905                // style-uniform slice within that same sequence, so the
906                // zip pairs each positioned glyph with its own cluster
907                // for RTL runs, ligatures, and multi-glyph clusters alike.
908                let visible_glyphs = parley_run.glyphs().collect::<Vec<_>>();
909                let all_clustered_glyphs = run
910                    .visual_clusters()
911                    .flat_map(|cluster| {
912                        let cluster_range = cluster.text_range();
913                        cluster
914                            .glyphs()
915                            .map(move |glyph| (glyph, cluster_range.clone()))
916                    })
917                    .collect::<Vec<_>>();
918                let first_glyph = all_clustered_glyphs
919                    .windows(visible_glyphs.len())
920                    .position(|candidate| {
921                        candidate
922                            .iter()
923                            .zip(&visible_glyphs)
924                            .all(|((glyph, _), visible)| glyph == visible)
925                    })
926                    .unwrap_or(0);
927                let clusters = all_clustered_glyphs
928                    .iter()
929                    .skip(first_glyph)
930                    .map(|(_, range)| range.clone())
931                    .take(visible_glyphs.len())
932                    .collect::<Vec<_>>();
933                let positions: Vec<_> = parley_run
934                    .positioned_glyphs()
935                    .zip(
936                        clusters
937                            .iter()
938                            .map(|range| range.start as u32)
939                            .chain(std::iter::repeat(run.text_range().start as u32)),
940                    )
941                    .map(|(glyph, cluster)| GlyphPosition {
942                        id: glyph.id as u16,
943                        // Incular's renderer uses `origin.y - offset.y`; Parley
944                        // exposes glyph y from its top-left layout coordinate.
945                        // Parley seeds positioned glyph x with the line
946                        // alignment offset, so adding it again would translate
947                        // aligned text twice.
948                        offset: Offset::new(glyph.x, -glyph.y),
949                        advance: glyph.advance,
950                        cluster,
951                    })
952                    .collect();
953                if runs.len() == 1 {
954                    glyphs.extend(runs[0].glyphs.iter().copied());
955                }
956                if !runs.is_empty() {
957                    glyphs.extend(positions.iter().copied());
958                }
959                runs.push(Arc::new(GlyphRun {
960                    font,
961                    font_size: run.font_size(),
962                    origin: Offset::ZERO,
963                    glyphs: positions.into(),
964                }));
965                font_runs.push(FontRunDebug {
966                    range: run.text_range(),
967                    font: font_id,
968                    direction: if run.is_rtl() { "rtl" } else { "ltr" },
969                });
970            }
971            // Every merged cluster, including zero-advance ones that only
972            // carry caret stops; the layout derives its stops from these.
973            let cluster_spans: Vec<_> = merge_ligature_components(cluster_positions)
974                .iter()
975                .map(|position| TextClusterSpan {
976                    start: position.range.start,
977                    end: position.range.end,
978                    left: position.left,
979                    right: position.right,
980                    rtl: position.rtl,
981                })
982                .collect();
983            // The common one-run line has exactly the same positioned glyphs
984            // for caret handling and painting. Share the immutable allocation
985            // instead of retaining a second copy in TextLine.
986            let glyphs = if runs.len() == 1 {
987                runs[0].glyphs.clone()
988            } else {
989                glyphs.into()
990            };
991            lines.push(TextLine {
992                runs: runs.into(),
993                glyphs,
994                clusters: cluster_spans.into(),
995                width: metrics.advance,
996                offset: metrics.offset,
997                baseline: metrics.baseline,
998                start: range.start,
999                caret_end,
1000                end: range.end,
1001            });
1002        }
1003
1004        if lines.is_empty() {
1005            lines.push(TextLine {
1006                runs: Arc::new([]),
1007                glyphs: Arc::new([]),
1008                clusters: Arc::new([]),
1009                width: 0.0,
1010                offset: 0.0,
1011                baseline: 0.0,
1012                start: 0,
1013                caret_end: 0,
1014                end: 0,
1015            });
1016        }
1017        self.diagnostics.fallback_fonts_used += used_fonts.len().saturating_sub(1) as u64;
1018        let baseline = lines[0].baseline;
1019        let line_height = layout
1020            .lines()
1021            .next()
1022            .map_or(0.0, |line| line.metrics().line_height);
1023        TextLayout {
1024            lines: lines.into(),
1025            metrics: TextMetrics {
1026                size: Size::new(layout.width(), layout.height()),
1027                baseline,
1028                line_height,
1029            },
1030            font_runs: font_runs.into(),
1031            caret_positions: OnceLock::new(),
1032            overflowed: false,
1033        }
1034    }
1035}
1036
1037/// Coalesces Parley's glyphless ligature components with their rendered
1038/// cluster. Visual cluster iteration is forward for LTR runs and reversed for
1039/// RTL runs, so the component sequence starts at opposite ends of a ligature
1040/// in each direction.
1041fn merge_ligature_components(positions: Vec<ClusterPosition>) -> Vec<ClusterPosition> {
1042    let mut merged = Vec::with_capacity(positions.len());
1043    let mut pending: Option<ClusterPosition> = None;
1044    let mut pending_has_start = false;
1045
1046    for position in positions {
1047        let is_start = position.ligature_start;
1048        let is_continuation = position.ligature_continuation;
1049        if !is_start && !is_continuation {
1050            if let Some(pending) = pending.take() {
1051                merged.push(pending);
1052            }
1053            pending_has_start = false;
1054            merged.push(position);
1055            continue;
1056        }
1057        // A single source cluster can already cover the whole ligature.
1058        if is_start && is_continuation {
1059            if let Some(pending) = pending.take() {
1060                merged.push(pending);
1061            }
1062            pending_has_start = false;
1063            merged.push(position);
1064            continue;
1065        }
1066
1067        if position.rtl {
1068            if is_continuation {
1069                // In visual RTL order, continuations precede their start. A
1070                // continuation after a completed group begins the next one.
1071                if pending_has_start {
1072                    if let Some(pending) = pending.take() {
1073                        merged.push(pending);
1074                    }
1075                    pending_has_start = false;
1076                }
1077                let can_extend = pending
1078                    .as_ref()
1079                    .is_some_and(|pending| pending.can_join(&position));
1080                if can_extend {
1081                    pending
1082                        .as_mut()
1083                        .expect("checked pending ligature group")
1084                        .include(&position);
1085                } else {
1086                    if let Some(pending) = pending.take() {
1087                        merged.push(pending);
1088                    }
1089                    pending_has_start = false;
1090                    pending = Some(position);
1091                }
1092            } else {
1093                // The start completes the pending continuation group.
1094                match pending.take() {
1095                    Some(mut pending_group)
1096                        if !pending_has_start && pending_group.can_join(&position) =>
1097                    {
1098                        pending_group.include(&position);
1099                        merged.push(pending_group);
1100                    }
1101                    Some(pending_group) => {
1102                        merged.push(pending_group);
1103                        merged.push(position);
1104                    }
1105                    None => merged.push(position),
1106                }
1107                pending_has_start = false;
1108            }
1109        } else if is_start {
1110            if let Some(pending) = pending.take() {
1111                merged.push(pending);
1112            }
1113            pending = Some(position);
1114            pending_has_start = true;
1115        } else if pending.is_some() {
1116            // LTR starts precede their continuations in visual order. The
1117            // byte check prevents an excluded join control from being
1118            // bridged by a later glyphless continuation.
1119            if pending
1120                .as_ref()
1121                .is_some_and(|pending_group| pending_group.can_join(&position))
1122            {
1123                pending
1124                    .as_mut()
1125                    .expect("checked pending ligature group")
1126                    .include(&position);
1127            } else {
1128                let pending_group = pending.take().expect("pending ligature group");
1129                merged.push(pending_group);
1130                pending = Some(position);
1131                pending_has_start = false;
1132            }
1133        } else {
1134            // A run can begin in the middle of a shaped ligature if a prior
1135            // fallback/style run owns its start.
1136            pending = Some(position);
1137            pending_has_start = false;
1138        }
1139    }
1140
1141    if let Some(pending) = pending {
1142        merged.push(pending);
1143    }
1144    merged
1145}
1146
1147fn clipped_layout(mut layout: TextLayout, limit: usize) -> TextLayout {
1148    let lines = layout.lines.iter().take(limit).cloned().collect::<Vec<_>>();
1149    let width = lines
1150        .iter()
1151        .fold(0.0_f32, |width, line| width.max(line.width));
1152    layout.metrics.size = Size::new(width, layout.metrics.line_height * lines.len() as f32);
1153    layout.metrics.baseline = lines.first().map_or(0.0, |line| line.baseline);
1154    layout.lines = lines.into();
1155    layout.overflowed = true;
1156    layout
1157}
1158
1159fn grapheme_boundary_at_or_before(text: &str, offset: usize) -> usize {
1160    GraphemeClusterSegmenter::new()
1161        .segment_str(text)
1162        .take_while(|boundary| *boundary <= offset)
1163        .last()
1164        .unwrap_or(0)
1165}
1166
1167fn previous_grapheme_boundary(text: &str, offset: usize) -> usize {
1168    GraphemeClusterSegmenter::new()
1169        .segment_str(text)
1170        .take_while(|boundary| *boundary < offset)
1171        .last()
1172        .unwrap_or(0)
1173}
1174
1175fn family_name(family: &FontFamily) -> String {
1176    match family {
1177        FontFamily::SansSerif => "sans-serif".into(),
1178        FontFamily::Serif => "serif".into(),
1179        FontFamily::Monospace => "monospace".into(),
1180        FontFamily::Cursive => "cursive".into(),
1181        FontFamily::SystemUi => "system-ui".into(),
1182        FontFamily::Named(name) => format!("'{name}'"),
1183    }
1184}
1185
1186/// Identity for one font blob sighting. Pointer identity is stable because
1187/// `Blob` clones share the same backing allocation, so repeated sightings of
1188/// the same face never re-copy or re-hash the bytes.
1189type FontBlobKey = (usize, usize, u32);
1190
1191fn font_blob_key(font: &parley::FontData) -> FontBlobKey {
1192    (
1193        font.data.as_ref().as_ptr() as usize,
1194        font.data.as_ref().len(),
1195        font.index,
1196    )
1197}
1198
1199/// Returns the shared handle for this font blob, building it (one full-file
1200/// hash, no copy) on first sight only.
1201fn font_handle(engine: &mut TextEngine, font: &parley::FontData) -> FontHandle {
1202    let key = font_blob_key(font);
1203    if let Some(handle) = engine.font_handles.get(&key) {
1204        return handle.clone();
1205    }
1206    let mut hasher = DefaultHasher::new();
1207    font.data.as_ref().hash(&mut hasher);
1208    hasher.write_u64(u64::from(font.index));
1209    let id = FontId(hasher.finish());
1210    // Share discovery's (typically memory-mapped) source instead of copying
1211    // the whole font file into a private heap allocation.
1212    let handle = FontHandle::with_face_index(
1213        id,
1214        FontBytes::from_shared(font.data.clone().into_raw_parts().0),
1215        font.index,
1216    );
1217    engine.font_handles.insert(key, handle.clone());
1218    handle
1219}