Skip to main content

cosmic_text/
shape.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3#![allow(clippy::too_many_arguments)]
4
5use crate::fallback::FontFallbackIter;
6use crate::{
7    math, Align, Attrs, AttrsList, CacheKeyFlags, Color, DecorationMetrics, DecorationSpan,
8    Ellipsize, EllipsizeHeightLimit, Family, Font, FontSystem, GlyphDecorationData, Hinting,
9    LayoutGlyph, LayoutLine, Metrics, Wrap,
10};
11#[cfg(not(feature = "std"))]
12use alloc::{format, vec, vec::Vec};
13
14use alloc::collections::VecDeque;
15use core::cmp::{max, min};
16use core::fmt;
17use core::mem;
18use core::ops::Range;
19
20#[cfg(not(feature = "std"))]
21use core_maths::CoreFloat;
22use fontdb::Style;
23use unicode_script::{Script, UnicodeScript};
24use unicode_segmentation::UnicodeSegmentation;
25
26/// The shaping strategy of some text.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
28pub enum Shaping {
29    /// Basic shaping with no font fallback.
30    ///
31    /// This shaping strategy is very cheap, but it will not display complex
32    /// scripts properly nor try to find missing glyphs in your system fonts.
33    ///
34    /// You should use this strategy when you have complete control of the text
35    /// and the font you are displaying in your application.
36    #[cfg(feature = "swash")]
37    Basic,
38    /// Advanced text shaping and font fallback.
39    ///
40    /// You will need to enable this strategy if the text contains a complex
41    /// script, the font used needs it, and/or multiple fonts in your system
42    /// may be needed to display all of the glyphs.
43    Advanced,
44}
45
46impl Shaping {
47    fn run(
48        self,
49        glyphs: &mut Vec<ShapeGlyph>,
50        font_system: &mut FontSystem,
51        line: &str,
52        attrs_list: &AttrsList,
53        start_run: usize,
54        end_run: usize,
55        span_rtl: bool,
56    ) {
57        match self {
58            #[cfg(feature = "swash")]
59            Self::Basic => shape_skip(font_system, glyphs, line, attrs_list, start_run, end_run),
60            #[cfg(not(feature = "shape-run-cache"))]
61            Self::Advanced => shape_run(
62                glyphs,
63                font_system,
64                line,
65                attrs_list,
66                start_run,
67                end_run,
68                span_rtl,
69            ),
70            #[cfg(feature = "shape-run-cache")]
71            Self::Advanced => shape_run_cached(
72                glyphs,
73                font_system,
74                line,
75                attrs_list,
76                start_run,
77                end_run,
78                span_rtl,
79            ),
80        }
81    }
82}
83
84/// The base direction (paragraph level) used when shaping text.
85#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
86pub enum Direction {
87    /// Detect each paragraph's base direction from its first strong character.
88    #[default]
89    Auto,
90    /// Force a left-to-right base direction for all text.
91    LeftToRight,
92    /// Force a right-to-left base direction for all text.
93    RightToLeft,
94}
95
96impl Direction {
97    /// The base paragraph level to hand to the bidi algorithm, or `None` to let
98    /// it auto-detect the level from the text.
99    fn bidi_level(self) -> Option<unicode_bidi::Level> {
100        match self {
101            Self::Auto => None,
102            Self::LeftToRight => Some(unicode_bidi::Level::ltr()),
103            Self::RightToLeft => Some(unicode_bidi::Level::rtl()),
104        }
105    }
106}
107
108const NUM_SHAPE_PLANS: usize = 6;
109
110/// A set of buffers containing allocations for shaped text.
111#[derive(Default)]
112pub struct ShapeBuffer {
113    /// Cache for harfrust shape plans. Stores up to [`NUM_SHAPE_PLANS`] plans at once. Inserting a new one past that
114    /// will remove the one that was least recently added (not least recently used).
115    shape_plan_cache: VecDeque<(fontdb::ID, harfrust::ShapePlan)>,
116
117    /// Buffer for holding unicode text.
118    harfrust_buffer: Option<harfrust::UnicodeBuffer>,
119
120    /// Temporary buffers for scripts.
121    scripts: Vec<Script>,
122
123    /// Buffer for shape spans.
124    spans: Vec<ShapeSpan>,
125
126    /// Buffer for shape words.
127    words: Vec<ShapeWord>,
128
129    /// Buffers for visual lines.
130    visual_lines: Vec<VisualLine>,
131    cached_visual_lines: Vec<VisualLine>,
132
133    /// Buffer for sets of layout glyphs.
134    glyph_sets: Vec<Vec<LayoutGlyph>>,
135}
136
137impl fmt::Debug for ShapeBuffer {
138    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
139        f.pad("ShapeBuffer { .. }")
140    }
141}
142
143fn shape_fallback(
144    scratch: &mut ShapeBuffer,
145    glyphs: &mut Vec<ShapeGlyph>,
146    font: &Font,
147    line: &str,
148    attrs_list: &AttrsList,
149    start_run: usize,
150    end_run: usize,
151    span_rtl: bool,
152) -> Vec<usize> {
153    let run = &line[start_run..end_run];
154
155    let font_scale = font.metrics().units_per_em as f32;
156    let ascent = font.metrics().ascent / font_scale;
157    let descent = -font.metrics().descent / font_scale;
158
159    let mut buffer = scratch.harfrust_buffer.take().unwrap_or_default();
160    buffer.set_direction(if span_rtl {
161        harfrust::Direction::RightToLeft
162    } else {
163        harfrust::Direction::LeftToRight
164    });
165    if run.contains('\t') {
166        // Push string to buffer, replacing tabs with spaces
167        //TODO: Find a way to do this with minimal allocating, calling
168        // UnicodeBuffer::push_str multiple times causes issues and
169        // UnicodeBuffer::add resizes the buffer with every character
170        buffer.push_str(&run.replace('\t', " "));
171    } else {
172        buffer.push_str(run);
173    }
174    buffer.guess_segment_properties();
175
176    let rtl = matches!(buffer.direction(), harfrust::Direction::RightToLeft);
177    assert_eq!(rtl, span_rtl);
178
179    let attrs = attrs_list.get_span(start_run);
180    let mut rb_font_features = Vec::new();
181
182    // Convert attrs::Feature to harfrust::Feature
183    for feature in &attrs.font_features.features {
184        rb_font_features.push(harfrust::Feature::new(
185            harfrust::Tag::new(feature.tag.as_bytes()),
186            feature.value,
187            0..usize::MAX,
188        ));
189    }
190
191    let language = buffer.language();
192    let key = harfrust::ShapePlanKey::new(Some(buffer.script()), buffer.direction())
193        .features(&rb_font_features)
194        .instance(Some(font.shaper_instance()))
195        .language(language.as_ref());
196
197    let shape_plan = match scratch
198        .shape_plan_cache
199        .iter()
200        .find(|(id, plan)| *id == font.id() && key.matches(plan))
201    {
202        Some((_font_id, plan)) => plan,
203        None => {
204            let plan = harfrust::ShapePlan::new(
205                font.shaper(),
206                buffer.direction(),
207                Some(buffer.script()),
208                buffer.language().as_ref(),
209                &rb_font_features,
210            );
211            if scratch.shape_plan_cache.len() >= NUM_SHAPE_PLANS {
212                scratch.shape_plan_cache.pop_front();
213            }
214            scratch.shape_plan_cache.push_back((font.id(), plan));
215            &scratch
216                .shape_plan_cache
217                .back()
218                .expect("we just pushed the shape plan")
219                .1
220        }
221    };
222
223    let glyph_buffer = font
224        .shaper()
225        .shape_with_plan(shape_plan, buffer, &rb_font_features);
226    let glyph_infos = glyph_buffer.glyph_infos();
227    let glyph_positions = glyph_buffer.glyph_positions();
228
229    let mut missing = Vec::new();
230    glyphs.reserve(glyph_infos.len());
231    let glyph_start = glyphs.len();
232    for (info, pos) in glyph_infos.iter().zip(glyph_positions.iter()) {
233        let start_glyph = start_run + info.cluster as usize;
234
235        if info.glyph_id == 0 {
236            missing.push(start_glyph);
237        }
238
239        let attrs = attrs_list.get_span(start_glyph);
240        let x_advance = pos.x_advance as f32 / font_scale
241            + attrs.letter_spacing_opt.map_or(0.0, |spacing| spacing.0);
242        let y_advance = pos.y_advance as f32 / font_scale;
243        let x_offset = pos.x_offset as f32 / font_scale;
244        let y_offset = pos.y_offset as f32 / font_scale;
245
246        glyphs.push(ShapeGlyph {
247            start: start_glyph,
248            end: end_run, // Set later
249            x_advance,
250            y_advance,
251            x_offset,
252            y_offset,
253            ascent,
254            descent,
255            font_monospace_em_width: font.monospace_em_width(),
256            font_id: font.id(),
257            font_weight: attrs.weight,
258            glyph_id: info.glyph_id.try_into().expect("failed to cast glyph ID"),
259            //TODO: color should not be related to shaping
260            color_opt: attrs.color_opt,
261            metadata: attrs.metadata,
262            cache_key_flags: override_fake_italic(attrs.cache_key_flags, font, &attrs),
263            metrics_opt: attrs.metrics_opt.map(Into::into),
264        });
265    }
266
267    // Adjust end of glyphs
268    if rtl {
269        for i in glyph_start + 1..glyphs.len() {
270            let next_start = glyphs[i - 1].start;
271            let next_end = glyphs[i - 1].end;
272            let prev = &mut glyphs[i];
273            if prev.start == next_start {
274                prev.end = next_end;
275            } else {
276                prev.end = next_start;
277            }
278        }
279    } else {
280        for i in (glyph_start + 1..glyphs.len()).rev() {
281            let next_start = glyphs[i].start;
282            let next_end = glyphs[i].end;
283            let prev = &mut glyphs[i - 1];
284            if prev.start == next_start {
285                prev.end = next_end;
286            } else {
287                prev.end = next_start;
288            }
289        }
290    }
291
292    // Restore the buffer to save an allocation.
293    scratch.harfrust_buffer = Some(glyph_buffer.clear());
294
295    missing
296}
297
298fn shape_run(
299    glyphs: &mut Vec<ShapeGlyph>,
300    font_system: &mut FontSystem,
301    line: &str,
302    attrs_list: &AttrsList,
303    start_run: usize,
304    end_run: usize,
305    span_rtl: bool,
306) {
307    // Re-use the previous script buffer if possible.
308    let mut scripts = {
309        let mut scripts = mem::take(&mut font_system.shape_buffer.scripts);
310        scripts.clear();
311        scripts
312    };
313    for c in line[start_run..end_run].chars() {
314        match c.script() {
315            Script::Common | Script::Inherited | Script::Latin | Script::Unknown => (),
316            script => {
317                if !scripts.contains(&script) {
318                    scripts.push(script);
319                }
320            }
321        }
322    }
323
324    log::trace!("      Run {:?}: '{}'", &scripts, &line[start_run..end_run],);
325
326    let attrs = attrs_list.get_span(start_run);
327
328    let fonts = font_system.get_font_matches(&attrs);
329
330    let default_families = [&attrs.family];
331    let mut font_iter = FontFallbackIter::new(
332        font_system,
333        &fonts,
334        &default_families,
335        &scripts,
336        &line[start_run..end_run],
337        attrs.weight,
338    );
339
340    let font = font_iter.next().expect("no default font found");
341
342    let glyph_start = glyphs.len();
343    let mut missing = {
344        let scratch = font_iter.shape_caches();
345        shape_fallback(
346            scratch, glyphs, &font, line, attrs_list, start_run, end_run, span_rtl,
347        )
348    };
349
350    //TODO: improve performance!
351    while !missing.is_empty() {
352        let Some(font) = font_iter.next() else {
353            break;
354        };
355
356        log::trace!(
357            "Evaluating fallback with font '{}'",
358            font_iter.face_name(font.id())
359        );
360        let mut fb_glyphs = Vec::new();
361        let scratch = font_iter.shape_caches();
362        let fb_missing = shape_fallback(
363            scratch,
364            &mut fb_glyphs,
365            &font,
366            line,
367            attrs_list,
368            start_run,
369            end_run,
370            span_rtl,
371        );
372
373        // Insert all matching glyphs
374        let mut fb_i = 0;
375        while fb_i < fb_glyphs.len() {
376            let start = fb_glyphs[fb_i].start;
377            let end = fb_glyphs[fb_i].end;
378
379            // Skip clusters that are not missing, or where the fallback font is missing
380            if !missing.contains(&start) || fb_missing.contains(&start) {
381                fb_i += 1;
382                continue;
383            }
384
385            let mut missing_i = 0;
386            while missing_i < missing.len() {
387                if missing[missing_i] >= start && missing[missing_i] < end {
388                    // println!("No longer missing {}", missing[missing_i]);
389                    missing.remove(missing_i);
390                } else {
391                    missing_i += 1;
392                }
393            }
394
395            // Find prior glyphs
396            let mut i = glyph_start;
397            while i < glyphs.len() {
398                if glyphs[i].start >= start && glyphs[i].end <= end {
399                    break;
400                }
401                i += 1;
402            }
403
404            // Remove prior glyphs
405            while i < glyphs.len() {
406                if glyphs[i].start >= start && glyphs[i].end <= end {
407                    let _glyph = glyphs.remove(i);
408                    // log::trace!("Removed {},{} from {}", _glyph.start, _glyph.end, i);
409                } else {
410                    break;
411                }
412            }
413
414            while fb_i < fb_glyphs.len() {
415                if fb_glyphs[fb_i].start >= start && fb_glyphs[fb_i].end <= end {
416                    let fb_glyph = fb_glyphs.remove(fb_i);
417                    // log::trace!("Insert {},{} from font {} at {}", fb_glyph.start, fb_glyph.end, font_i, i);
418                    glyphs.insert(i, fb_glyph);
419                    i += 1;
420                } else {
421                    break;
422                }
423            }
424        }
425    }
426
427    // Debug missing font fallbacks
428    font_iter.check_missing(&line[start_run..end_run]);
429
430    /*
431    for glyph in glyphs.iter() {
432        log::trace!("'{}': {}, {}, {}, {}", &line[glyph.start..glyph.end], glyph.x_advance, glyph.y_advance, glyph.x_offset, glyph.y_offset);
433    }
434    */
435
436    // Restore the scripts buffer.
437    font_system.shape_buffer.scripts = scripts;
438}
439
440#[cfg(feature = "shape-run-cache")]
441fn shape_run_cached(
442    glyphs: &mut Vec<ShapeGlyph>,
443    font_system: &mut FontSystem,
444    line: &str,
445    attrs_list: &AttrsList,
446    start_run: usize,
447    end_run: usize,
448    span_rtl: bool,
449) {
450    use crate::{AttrsOwned, ShapeRunKey};
451
452    let run_range = start_run..end_run;
453    let mut key = ShapeRunKey {
454        text: line[run_range.clone()].to_string(),
455        default_attrs: AttrsOwned::new(&attrs_list.defaults()),
456        attrs_spans: Vec::new(),
457    };
458    for (attrs_range, attrs) in attrs_list.spans.overlapping(&run_range) {
459        if attrs == &key.default_attrs {
460            // Skip if attrs matches default attrs
461            continue;
462        }
463        let start = max(attrs_range.start, start_run).saturating_sub(start_run);
464        let end = min(attrs_range.end, end_run).saturating_sub(start_run);
465        if end > start {
466            let range = start..end;
467            key.attrs_spans.push((range, attrs.clone()));
468        }
469    }
470    if let Some(cache_glyphs) = font_system.shape_run_cache.get(&key) {
471        for mut glyph in cache_glyphs.iter().cloned() {
472            // Adjust glyph start and end to match run position
473            glyph.start += start_run;
474            glyph.end += start_run;
475            glyphs.push(glyph);
476        }
477        return;
478    }
479
480    // Fill in cache if not already set
481    let mut cache_glyphs = Vec::new();
482    shape_run(
483        &mut cache_glyphs,
484        font_system,
485        line,
486        attrs_list,
487        start_run,
488        end_run,
489        span_rtl,
490    );
491    glyphs.extend_from_slice(&cache_glyphs);
492    for glyph in cache_glyphs.iter_mut() {
493        // Adjust glyph start and end to remove run position
494        glyph.start -= start_run;
495        glyph.end -= start_run;
496    }
497    font_system.shape_run_cache.insert(key, cache_glyphs);
498}
499
500#[cfg(feature = "swash")]
501fn shape_skip(
502    font_system: &mut FontSystem,
503    glyphs: &mut Vec<ShapeGlyph>,
504    line: &str,
505    attrs_list: &AttrsList,
506    start_run: usize,
507    end_run: usize,
508) {
509    let attrs = attrs_list.get_span(start_run);
510    let fonts = font_system.get_font_matches(&attrs);
511
512    let default_families = [&attrs.family];
513    let mut font_iter = FontFallbackIter::new(
514        font_system,
515        &fonts,
516        &default_families,
517        &[],
518        "",
519        attrs.weight,
520    );
521
522    let font = font_iter.next().expect("no default font found");
523    let glyph_start = glyphs.len();
524
525    shape_skip_glyphs(glyphs, &font, line, attrs_list, start_run, end_run);
526
527    // If any glyphs are missing and the user has specified a font,
528    // fall back to a default font (SansSerif or Monospace)
529    if matches!(attrs.family, Family::Name(_))
530        && glyphs[glyph_start..].iter().any(|g| g.glyph_id == 0)
531    {
532        let is_mono = font_system
533            .db()
534            .face(font.id())
535            .is_some_and(|face| face.monospaced);
536        let fb_family = if is_mono {
537            Family::Monospace
538        } else {
539            Family::SansSerif
540        };
541        let fb_attrs = Attrs::new()
542            .family(fb_family)
543            .weight(attrs.weight)
544            .style(attrs.style)
545            .stretch(attrs.stretch);
546        let fb_fonts = font_system.get_font_matches(&fb_attrs);
547        let fb_families = [&fb_family];
548        let mut fb_iter =
549            FontFallbackIter::new(font_system, &fb_fonts, &fb_families, &[], "", attrs.weight);
550
551        if let Some(fb_font) = fb_iter.next() {
552            let fb_swash = fb_font.as_swash();
553            let fb_charmap = fb_swash.charmap();
554            let fb_metrics = fb_swash.metrics(&[]);
555            let fb_glyph_metrics = fb_swash.glyph_metrics(&[]).scale(1.0);
556            let fb_scale = f32::from(fb_metrics.units_per_em);
557
558            for glyph in glyphs[glyph_start..].iter_mut() {
559                if glyph.glyph_id != 0 {
560                    continue;
561                }
562                let codepoint = line[glyph.start..glyph.end].chars().next().unwrap_or('\0');
563                let glyph_id = fb_charmap.map(codepoint);
564                if glyph_id != 0 {
565                    let span_attrs = attrs_list.get_span(glyph.start);
566                    glyph.glyph_id = glyph_id;
567                    glyph.font_id = fb_font.id();
568                    glyph.font_monospace_em_width = fb_font.monospace_em_width();
569                    glyph.ascent = fb_metrics.ascent / fb_scale;
570                    glyph.descent = fb_metrics.descent / fb_scale;
571                    glyph.x_advance = fb_glyph_metrics.advance_width(glyph_id)
572                        + span_attrs
573                            .letter_spacing_opt
574                            .map_or(0.0, |spacing| spacing.0);
575                    glyph.cache_key_flags = override_fake_italic(
576                        span_attrs.cache_key_flags,
577                        fb_font.as_ref(),
578                        &span_attrs,
579                    );
580                }
581            }
582        }
583    }
584}
585
586#[cfg(feature = "swash")]
587fn shape_skip_glyphs(
588    glyphs: &mut Vec<ShapeGlyph>,
589    font: &Font,
590    line: &str,
591    attrs_list: &AttrsList,
592    start_run: usize,
593    end_run: usize,
594) {
595    let font_id = font.id();
596    let font_monospace_em_width = font.monospace_em_width();
597    let swash_font = font.as_swash();
598
599    let charmap = swash_font.charmap();
600    let metrics = swash_font.metrics(&[]);
601    let glyph_metrics = swash_font.glyph_metrics(&[]).scale(1.0);
602
603    let ascent = metrics.ascent / f32::from(metrics.units_per_em);
604    let descent = metrics.descent / f32::from(metrics.units_per_em);
605
606    glyphs.extend(
607        line[start_run..end_run]
608            .char_indices()
609            .map(|(chr_idx, codepoint)| {
610                let glyph_id = charmap.map(codepoint);
611                let x_advance = glyph_metrics.advance_width(glyph_id)
612                    + attrs_list
613                        .get_span(start_run + chr_idx)
614                        .letter_spacing_opt
615                        .map_or(0.0, |spacing| spacing.0);
616                let attrs = attrs_list.get_span(start_run + chr_idx);
617
618                ShapeGlyph {
619                    start: chr_idx + start_run,
620                    end: chr_idx + start_run + codepoint.len_utf8(),
621                    x_advance,
622                    y_advance: 0.0,
623                    x_offset: 0.0,
624                    y_offset: 0.0,
625                    ascent,
626                    descent,
627                    font_monospace_em_width,
628                    font_id,
629                    font_weight: attrs.weight,
630                    glyph_id,
631                    color_opt: attrs.color_opt,
632                    metadata: attrs.metadata,
633                    cache_key_flags: override_fake_italic(attrs.cache_key_flags, font, &attrs),
634                    metrics_opt: attrs.metrics_opt.map(Into::into),
635                }
636            }),
637    );
638}
639
640fn override_fake_italic(
641    cache_key_flags: CacheKeyFlags,
642    font: &Font,
643    attrs: &Attrs,
644) -> CacheKeyFlags {
645    if !font.italic_or_oblique && (attrs.style == Style::Italic || attrs.style == Style::Oblique) {
646        cache_key_flags | CacheKeyFlags::FAKE_ITALIC
647    } else {
648        cache_key_flags
649    }
650}
651
652/// A shaped glyph
653#[derive(Clone, Debug)]
654pub struct ShapeGlyph {
655    pub start: usize,
656    pub end: usize,
657    pub x_advance: f32,
658    pub y_advance: f32,
659    pub x_offset: f32,
660    pub y_offset: f32,
661    pub ascent: f32,
662    pub descent: f32,
663    pub font_monospace_em_width: Option<f32>,
664    pub font_id: fontdb::ID,
665    pub font_weight: fontdb::Weight,
666    pub glyph_id: u16,
667    pub color_opt: Option<Color>,
668    pub metadata: usize,
669    pub cache_key_flags: CacheKeyFlags,
670    pub metrics_opt: Option<Metrics>,
671}
672
673impl ShapeGlyph {
674    const fn layout(
675        &self,
676        font_size: f32,
677        line_height_opt: Option<f32>,
678        x: f32,
679        y: f32,
680        w: f32,
681        level: unicode_bidi::Level,
682    ) -> LayoutGlyph {
683        LayoutGlyph {
684            start: self.start,
685            end: self.end,
686            font_size,
687            line_height_opt,
688            font_id: self.font_id,
689            font_weight: self.font_weight,
690            glyph_id: self.glyph_id,
691            x,
692            y,
693            w,
694            level,
695            x_offset: self.x_offset,
696            y_offset: self.y_offset,
697            color_opt: self.color_opt,
698            metadata: self.metadata,
699            cache_key_flags: self.cache_key_flags,
700        }
701    }
702
703    /// Get the width of the [`ShapeGlyph`] in pixels, either using the provided font size
704    /// or the [`ShapeGlyph::metrics_opt`] override.
705    pub fn width(&self, font_size: f32) -> f32 {
706        self.metrics_opt.map_or(font_size, |x| x.font_size) * self.x_advance
707    }
708}
709
710fn decoration_metrics(font: &Font) -> (DecorationMetrics, DecorationMetrics, f32) {
711    let metrics = font.metrics();
712    let upem = metrics.units_per_em as f32;
713    if upem == 0.0 {
714        return (
715            DecorationMetrics::default(),
716            DecorationMetrics::default(),
717            0.0,
718        );
719    }
720    (
721        DecorationMetrics {
722            offset: metrics.underline.map_or(-0.125, |d| d.offset / upem),
723            thickness: metrics.underline.map_or(1.0 / 14.0, |d| d.thickness / upem),
724        },
725        DecorationMetrics {
726            offset: metrics.strikeout.map_or(0.3, |d| d.offset / upem),
727            thickness: metrics.strikeout.map_or(1.0 / 14.0, |d| d.thickness / upem),
728        },
729        metrics.ascent / upem,
730    )
731}
732
733/// span index used in `VlRange` to indicate this range is the ellipsis.
734const ELLIPSIS_SPAN: usize = usize::MAX;
735
736fn shape_ellipsis(
737    font_system: &mut FontSystem,
738    attrs: &Attrs,
739    shaping: Shaping,
740    span_rtl: bool,
741) -> Vec<ShapeGlyph> {
742    let attrs_list = AttrsList::new(attrs);
743    let level = if span_rtl {
744        unicode_bidi::Level::rtl()
745    } else {
746        unicode_bidi::Level::ltr()
747    };
748    let word = ShapeWord::new(
749        font_system,
750        "\u{2026}", // TODO: maybe do CJK ellipsis
751        &attrs_list,
752        0.."\u{2026}".len(),
753        level,
754        false,
755        shaping,
756    );
757    let mut glyphs = word.glyphs;
758
759    // did we fail to shape it?
760    if glyphs.is_empty() || glyphs.iter().all(|g| g.glyph_id == 0) {
761        let fallback = ShapeWord::new(
762            font_system,
763            "...",
764            &attrs_list,
765            0.."...".len(),
766            level,
767            false,
768            shaping,
769        );
770        glyphs = fallback.glyphs;
771    }
772    glyphs
773}
774
775/// A shaped word (for word wrapping)
776#[derive(Clone, Debug)]
777pub struct ShapeWord {
778    pub blank: bool,
779    pub glyphs: Vec<ShapeGlyph>,
780}
781
782impl ShapeWord {
783    /// Creates an empty word.
784    ///
785    /// The returned word is in an invalid state until [`Self::build_in_buffer`] is called.
786    pub(crate) fn empty() -> Self {
787        Self {
788            blank: true,
789            glyphs: Vec::default(),
790        }
791    }
792
793    /// Shape a word into a set of glyphs.
794    #[allow(clippy::too_many_arguments)]
795    pub fn new(
796        font_system: &mut FontSystem,
797        line: &str,
798        attrs_list: &AttrsList,
799        word_range: Range<usize>,
800        level: unicode_bidi::Level,
801        blank: bool,
802        shaping: Shaping,
803    ) -> Self {
804        let mut empty = Self::empty();
805        empty.build(
806            font_system,
807            line,
808            attrs_list,
809            word_range,
810            level,
811            blank,
812            shaping,
813        );
814        empty
815    }
816
817    /// See [`Self::new`].
818    ///
819    /// Reuses as much of the pre-existing internal allocations as possible.
820    #[allow(clippy::too_many_arguments)]
821    pub fn build(
822        &mut self,
823        font_system: &mut FontSystem,
824        line: &str,
825        attrs_list: &AttrsList,
826        word_range: Range<usize>,
827        level: unicode_bidi::Level,
828        blank: bool,
829        shaping: Shaping,
830    ) {
831        let word = &line[word_range.clone()];
832
833        log::trace!(
834            "      Word{}: '{}'",
835            if blank { " BLANK" } else { "" },
836            word
837        );
838
839        let mut glyphs = mem::take(&mut self.glyphs);
840        glyphs.clear();
841
842        let span_rtl = level.is_rtl();
843
844        // Fast path optimization: For simple ASCII words, skip expensive grapheme iteration
845        let is_simple_ascii =
846            word.is_ascii() && !word.chars().any(|c| c.is_ascii_control() && c != '\t');
847
848        if is_simple_ascii && !word.is_empty() && {
849            let attrs_start = attrs_list.get_span(word_range.start);
850            attrs_list.spans_iter().all(|(other_range, other_attrs)| {
851                word_range.end <= other_range.start
852                    || other_range.end <= word_range.start
853                    || attrs_start.compatible(&other_attrs.as_attrs())
854            })
855        } {
856            shaping.run(
857                &mut glyphs,
858                font_system,
859                line,
860                attrs_list,
861                word_range.start,
862                word_range.end,
863                span_rtl,
864            );
865        } else {
866            // Complex text path: Full grapheme iteration and attribute processing
867            let mut start_run = word_range.start;
868            let mut attrs = attrs_list.defaults();
869            for (egc_i, _egc) in word.grapheme_indices(true) {
870                let start_egc = word_range.start + egc_i;
871                let attrs_egc = attrs_list.get_span(start_egc);
872                if !attrs.compatible(&attrs_egc) {
873                    shaping.run(
874                        &mut glyphs,
875                        font_system,
876                        line,
877                        attrs_list,
878                        start_run,
879                        start_egc,
880                        span_rtl,
881                    );
882
883                    start_run = start_egc;
884                    attrs = attrs_egc;
885                }
886            }
887            if start_run < word_range.end {
888                shaping.run(
889                    &mut glyphs,
890                    font_system,
891                    line,
892                    attrs_list,
893                    start_run,
894                    word_range.end,
895                    span_rtl,
896                );
897            }
898        }
899
900        self.blank = blank;
901        self.glyphs = glyphs;
902    }
903
904    /// Get the width of the [`ShapeWord`] in pixels, using the [`ShapeGlyph::width`] function.
905    pub fn width(&self, font_size: f32) -> f32 {
906        let mut width = 0.0;
907        for glyph in &self.glyphs {
908            width += glyph.width(font_size);
909        }
910        width
911    }
912}
913
914/// A shaped span (for bidirectional processing)
915#[derive(Clone, Debug)]
916pub struct ShapeSpan {
917    pub level: unicode_bidi::Level,
918    pub words: Vec<ShapeWord>,
919    /// Decoration data per user-level attr span within this shape span.
920    /// Each entry maps a byte range to its decoration config and font metrics.
921    /// Empty when no decorations are active.
922    pub decoration_spans: Vec<(Range<usize>, GlyphDecorationData)>,
923}
924
925impl ShapeSpan {
926    /// Creates an empty span.
927    ///
928    /// The returned span is in an invalid state until [`Self::build_in_buffer`] is called.
929    pub(crate) fn empty() -> Self {
930        Self {
931            level: unicode_bidi::Level::ltr(),
932            words: Vec::default(),
933            decoration_spans: Vec::new(),
934        }
935    }
936
937    /// Shape a span into a set of words.
938    pub fn new(
939        font_system: &mut FontSystem,
940        line: &str,
941        attrs_list: &AttrsList,
942        span_range: Range<usize>,
943        line_rtl: bool,
944        level: unicode_bidi::Level,
945        shaping: Shaping,
946    ) -> Self {
947        let mut empty = Self::empty();
948        empty.build(
949            font_system,
950            line,
951            attrs_list,
952            span_range,
953            line_rtl,
954            level,
955            shaping,
956        );
957        empty
958    }
959
960    /// See [`Self::new`].
961    ///
962    /// Reuses as much of the pre-existing internal allocations as possible.
963    pub fn build(
964        &mut self,
965        font_system: &mut FontSystem,
966        line: &str,
967        attrs_list: &AttrsList,
968        span_range: Range<usize>,
969        line_rtl: bool,
970        level: unicode_bidi::Level,
971        shaping: Shaping,
972    ) {
973        let span = &line[span_range.start..span_range.end];
974
975        log::trace!(
976            "  Span {}: '{}'",
977            if level.is_rtl() { "RTL" } else { "LTR" },
978            span
979        );
980
981        let mut words = mem::take(&mut self.words);
982
983        // Cache the shape words in reverse order so they can be popped for reuse in the same order.
984        let mut cached_words = mem::take(&mut font_system.shape_buffer.words);
985        cached_words.clear();
986        if line_rtl != level.is_rtl() {
987            // Un-reverse previous words so the internal glyph counts match accurately when rewriting memory.
988            cached_words.append(&mut words);
989        } else {
990            cached_words.extend(words.drain(..).rev());
991        }
992
993        let mut start_word = 0;
994        for (end_lb, _) in unicode_linebreak::linebreaks(span) {
995            // Check if this break opportunity splits a likely ligature (e.g. "|>" or "!=")
996            if end_lb > 0 && end_lb < span.len() {
997                let start_idx = span_range.start;
998                let pre_char = span[..end_lb].chars().last();
999                let post_char = span[end_lb..].chars().next();
1000
1001                if let (Some(c1), Some(c2)) = (pre_char, post_char) {
1002                    // Only probe if both are punctuation (optimization for coding ligatures)
1003                    if c1.is_ascii_punctuation() && c2.is_ascii_punctuation() {
1004                        let probe_text = format!("{}{}", c1, c2);
1005                        let attrs = attrs_list.get_span(start_idx + end_lb);
1006                        let fonts = font_system.get_font_matches(&attrs);
1007                        let default_families = [&attrs.family];
1008
1009                        let mut font_iter = FontFallbackIter::new(
1010                            font_system,
1011                            &fonts,
1012                            &default_families,
1013                            &[],
1014                            &probe_text,
1015                            attrs.weight,
1016                        );
1017
1018                        if let Some(font) = font_iter.next() {
1019                            let mut glyphs = Vec::new();
1020                            let scratch = font_iter.shape_caches();
1021                            shape_fallback(
1022                                scratch,
1023                                &mut glyphs,
1024                                &font,
1025                                &probe_text,
1026                                attrs_list,
1027                                0,
1028                                probe_text.len(),
1029                                false,
1030                            );
1031
1032                            // 1. If we have fewer glyphs than chars, it's definitely a ligature (e.g. -> becoming 1 arrow).
1033                            if glyphs.len() < probe_text.chars().count() {
1034                                continue;
1035                            }
1036
1037                            // 2. If we have the same number of glyphs, they might be contextual alternates (e.g. |> becoming 2 special glyphs).
1038                            // Check if the glyphs match the standard "cmap" (character to glyph) mapping.
1039                            // If they differ, the shaper substituted them, so we should keep them together.
1040                            #[cfg(feature = "swash")]
1041                            if glyphs.len() == probe_text.chars().count() {
1042                                let charmap = font.as_swash().charmap();
1043                                let mut is_modified = false;
1044                                for (i, c) in probe_text.chars().enumerate() {
1045                                    let std_id = charmap.map(c);
1046                                    if glyphs[i].glyph_id != std_id {
1047                                        is_modified = true;
1048                                        break;
1049                                    }
1050                                }
1051
1052                                if is_modified {
1053                                    // Ligature/Contextual Alternate detected!
1054                                    continue;
1055                                }
1056                            }
1057                        }
1058                    }
1059                }
1060            }
1061
1062            let mut start_lb = end_lb;
1063            for (i, c) in span[start_word..end_lb].char_indices().rev() {
1064                // TODO: Not all whitespace characters are linebreakable, e.g. 00A0 (No-break
1065                // space)
1066                // https://www.unicode.org/reports/tr14/#GL
1067                // https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt
1068                if c.is_whitespace() {
1069                    start_lb = start_word + i;
1070                } else {
1071                    break;
1072                }
1073            }
1074            if start_word < start_lb {
1075                let mut word = cached_words.pop().unwrap_or_else(ShapeWord::empty);
1076                word.build(
1077                    font_system,
1078                    line,
1079                    attrs_list,
1080                    (span_range.start + start_word)..(span_range.start + start_lb),
1081                    level,
1082                    false,
1083                    shaping,
1084                );
1085                words.push(word);
1086            }
1087            if start_lb < end_lb {
1088                for (i, c) in span[start_lb..end_lb].char_indices() {
1089                    // assert!(c.is_whitespace());
1090                    let mut word = cached_words.pop().unwrap_or_else(ShapeWord::empty);
1091                    word.build(
1092                        font_system,
1093                        line,
1094                        attrs_list,
1095                        (span_range.start + start_lb + i)
1096                            ..(span_range.start + start_lb + i + c.len_utf8()),
1097                        level,
1098                        true,
1099                        shaping,
1100                    );
1101                    words.push(word);
1102                }
1103            }
1104            start_word = end_lb;
1105        }
1106
1107        // Reverse glyphs in RTL lines
1108        if line_rtl {
1109            for word in &mut words {
1110                word.glyphs.reverse();
1111            }
1112        }
1113
1114        // Reverse words in spans that do not match line direction
1115        if line_rtl != level.is_rtl() {
1116            words.reverse();
1117        }
1118
1119        self.level = level;
1120        self.words = words;
1121
1122        // Build decoration spans: one entry per user-level attr span that has
1123        // decorations within this shape span's byte range.  Font metrics come from
1124        // the primary font (first shaped glyph), following Pango convention.
1125        self.decoration_spans.clear();
1126
1127        // Early-out: skip font lookup and span iteration when no decorations exist.
1128        // For plain text (the common case) this is a single bool check.
1129        let any_decoration = attrs_list.defaults().text_decoration.has_decoration()
1130            || attrs_list.spans_iter().any(|(range, attr_owned)| {
1131                let start = range.start.max(span_range.start);
1132                let end = range.end.min(span_range.end);
1133                start < end && attr_owned.as_attrs().text_decoration.has_decoration()
1134            });
1135
1136        if any_decoration {
1137            // Get font metrics once from the primary glyph of this shape span
1138            let primary_metrics = self
1139                .words
1140                .iter()
1141                .flat_map(|w| w.glyphs.first())
1142                .next()
1143                .and_then(|glyph| {
1144                    font_system
1145                        .get_font(glyph.font_id, glyph.font_weight)
1146                        .map(|font| decoration_metrics(&font))
1147                });
1148
1149            if let Some((ul_metrics, st_metrics, ascent)) = primary_metrics {
1150                // Track which sub-ranges of span_range are covered by explicit spans
1151                let mut covered_end = span_range.start;
1152
1153                for (range, attr_owned) in attrs_list.spans_iter() {
1154                    // Compute intersection with our shape span's byte range
1155                    let start = range.start.max(span_range.start);
1156                    let end = range.end.min(span_range.end);
1157                    if start >= end {
1158                        continue;
1159                    }
1160
1161                    // Check the gap before this span (covered by defaults)
1162                    if covered_end < start {
1163                        let default_attrs = attrs_list.defaults();
1164                        if default_attrs.text_decoration.has_decoration() {
1165                            self.decoration_spans.push((
1166                                covered_end..start,
1167                                GlyphDecorationData {
1168                                    text_decoration: default_attrs.text_decoration,
1169                                    underline_metrics: ul_metrics,
1170                                    strikethrough_metrics: st_metrics,
1171                                    ascent,
1172                                },
1173                            ));
1174                        }
1175                    }
1176                    covered_end = end;
1177
1178                    let attrs = attr_owned.as_attrs();
1179                    if attrs.text_decoration.has_decoration() {
1180                        self.decoration_spans.push((
1181                            start..end,
1182                            GlyphDecorationData {
1183                                text_decoration: attrs.text_decoration,
1184                                underline_metrics: ul_metrics,
1185                                strikethrough_metrics: st_metrics,
1186                                ascent,
1187                            },
1188                        ));
1189                    }
1190                }
1191
1192                // Check trailing gap (covered by defaults)
1193                if covered_end < span_range.end {
1194                    let default_attrs = attrs_list.defaults();
1195                    if default_attrs.text_decoration.has_decoration() {
1196                        self.decoration_spans.push((
1197                            covered_end..span_range.end,
1198                            GlyphDecorationData {
1199                                text_decoration: default_attrs.text_decoration,
1200                                underline_metrics: ul_metrics,
1201                                strikethrough_metrics: st_metrics,
1202                                ascent,
1203                            },
1204                        ));
1205                    }
1206                }
1207            }
1208        }
1209
1210        // Cache buffer for future reuse.
1211        font_system.shape_buffer.words = cached_words;
1212    }
1213}
1214
1215/// A shaped line (or paragraph)
1216#[derive(Clone, Debug)]
1217pub struct ShapeLine {
1218    pub rtl: bool,
1219    pub spans: Vec<ShapeSpan>,
1220    pub metrics_opt: Option<Metrics>,
1221    ellipsis_span: Option<ShapeSpan>,
1222}
1223
1224#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1225struct WordGlyphPos {
1226    word: usize,
1227    glyph: usize,
1228}
1229
1230impl WordGlyphPos {
1231    const ZERO: Self = Self { word: 0, glyph: 0 };
1232    fn new(word: usize, glyph: usize) -> Self {
1233        Self { word, glyph }
1234    }
1235}
1236
1237#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
1238struct SpanWordGlyphPos {
1239    span: usize,
1240    word: usize,
1241    glyph: usize,
1242}
1243
1244impl SpanWordGlyphPos {
1245    const ZERO: Self = Self {
1246        span: 0,
1247        word: 0,
1248        glyph: 0,
1249    };
1250    fn word_glyph_pos(&self) -> WordGlyphPos {
1251        WordGlyphPos {
1252            word: self.word,
1253            glyph: self.glyph,
1254        }
1255    }
1256    fn with_wordglyph(span: usize, wordglyph: WordGlyphPos) -> Self {
1257        Self {
1258            span,
1259            word: wordglyph.word,
1260            glyph: wordglyph.glyph,
1261        }
1262    }
1263}
1264
1265/// Controls whether we layout spans forward or backward.
1266/// Backward layout is used to improve efficiency
1267#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1268enum LayoutDirection {
1269    Forward,
1270    Backward,
1271}
1272
1273// Visual Line Ranges
1274#[derive(Copy, Clone, Debug, PartialEq, Eq)]
1275struct VlRange {
1276    span: usize,
1277    start: WordGlyphPos,
1278    end: WordGlyphPos,
1279    level: unicode_bidi::Level,
1280}
1281
1282impl Default for VlRange {
1283    fn default() -> Self {
1284        Self {
1285            span: Default::default(),
1286            start: Default::default(),
1287            end: Default::default(),
1288            level: unicode_bidi::Level::ltr(),
1289        }
1290    }
1291}
1292
1293#[derive(Default, Debug)]
1294struct VisualLine {
1295    ranges: Vec<VlRange>,
1296    spaces: u32,
1297    w: f32,
1298    ellipsized: bool,
1299    /// Byte range (start, end) of the original line text that was replaced by the ellipsis.
1300    /// Only set when `ellipsized` is true.
1301    elided_byte_range: Option<(usize, usize)>,
1302}
1303
1304impl VisualLine {
1305    fn clear(&mut self) {
1306        self.ranges.clear();
1307        self.spaces = 0;
1308        self.w = 0.;
1309        self.ellipsized = false;
1310        self.elided_byte_range = None;
1311    }
1312}
1313
1314impl ShapeLine {
1315    /// Creates an empty line.
1316    ///
1317    /// The returned line is in an invalid state until [`Self::build_in_buffer`] is called.
1318    pub(crate) fn empty() -> Self {
1319        Self {
1320            rtl: false,
1321            spans: Vec::default(),
1322            metrics_opt: None,
1323            ellipsis_span: None,
1324        }
1325    }
1326
1327    /// Shape a line into a set of spans, using a scratch buffer.
1328    ///
1329    /// When [`unicode_bidi::BidiInfo`] splits `line` into multiple paragraphs (on
1330    /// any `BidiClass::B` separator, e.g. LF, CR, FS, NEL, PS), the whole line is
1331    /// laid out in the first paragraph's base direction.
1332    pub fn new(
1333        font_system: &mut FontSystem,
1334        line: &str,
1335        attrs_list: &AttrsList,
1336        shaping: Shaping,
1337        tab_width: u16,
1338        direction: Direction,
1339    ) -> Self {
1340        let mut empty = Self::empty();
1341        empty.build(font_system, line, attrs_list, shaping, tab_width, direction);
1342        empty
1343    }
1344
1345    /// See [`Self::new`].
1346    ///
1347    /// Reuses as much of the pre-existing internal allocations as possible.
1348    pub fn build(
1349        &mut self,
1350        font_system: &mut FontSystem,
1351        line: &str,
1352        attrs_list: &AttrsList,
1353        shaping: Shaping,
1354        tab_width: u16,
1355        direction: Direction,
1356    ) {
1357        // Clear stale ellipsis span so it gets recomputed with the current attrs.
1358        // Without this, reusing a ShapeLine from a previous text (via Cached::Unused)
1359        // would keep an ellipsis shaped with the old attrs.
1360        self.ellipsis_span = None;
1361
1362        let mut spans = mem::take(&mut self.spans);
1363
1364        // Cache the shape spans in reverse order so they can be popped for reuse in the same order.
1365        let mut cached_spans = mem::take(&mut font_system.shape_buffer.spans);
1366        cached_spans.clear();
1367        cached_spans.extend(spans.drain(..).rev());
1368
1369        let bidi = unicode_bidi::BidiInfo::new(line, direction.bidi_level());
1370        let rtl = if bidi.paragraphs.is_empty() {
1371            // No strong content to detect from, go with default base direction if it's set
1372            direction == Direction::RightToLeft
1373        } else {
1374            bidi.paragraphs[0].level.is_rtl()
1375        };
1376
1377        log::trace!("Line {}: '{}'", if rtl { "RTL" } else { "LTR" }, line);
1378
1379        for para_info in &bidi.paragraphs {
1380            let line_range = para_info.range.clone();
1381            let levels = Self::adjust_levels(&unicode_bidi::Paragraph::new(&bidi, para_info));
1382
1383            // Find consecutive level runs. We use this to create Spans.
1384            // Each span is a set of characters with equal levels.
1385            let mut start = line_range.start;
1386            let mut run_level = levels[start];
1387            spans.reserve(line_range.end - start + 1);
1388
1389            for (i, &new_level) in levels
1390                .iter()
1391                .enumerate()
1392                .take(line_range.end)
1393                .skip(start + 1)
1394            {
1395                if new_level != run_level {
1396                    // End of the previous run, start of a new one.
1397                    let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
1398                    span.build(
1399                        font_system,
1400                        line,
1401                        attrs_list,
1402                        start..i,
1403                        rtl,
1404                        run_level,
1405                        shaping,
1406                    );
1407                    spans.push(span);
1408                    start = i;
1409                    run_level = new_level;
1410                }
1411            }
1412            let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
1413            span.build(
1414                font_system,
1415                line,
1416                attrs_list,
1417                start..line_range.end,
1418                rtl,
1419                run_level,
1420                shaping,
1421            );
1422            spans.push(span);
1423        }
1424
1425        // Adjust for tabs
1426        let mut x = 0.0;
1427        for span in &mut spans {
1428            for word in &mut span.words {
1429                for glyph in &mut word.glyphs {
1430                    if line.get(glyph.start..glyph.end) == Some("\t") {
1431                        // Tabs are shaped as spaces, so they will always have the x_advance of a space.
1432                        let tab_x_advance = f32::from(tab_width) * glyph.x_advance;
1433                        let tab_stop = (math::floorf(x / tab_x_advance) + 1.0) * tab_x_advance;
1434                        glyph.x_advance = tab_stop - x;
1435                    }
1436                    x += glyph.x_advance;
1437                }
1438            }
1439        }
1440
1441        self.rtl = rtl;
1442        self.spans = spans;
1443        self.metrics_opt = attrs_list.defaults().metrics_opt.map(Into::into);
1444
1445        self.ellipsis_span.get_or_insert_with(|| {
1446            let attrs = if attrs_list.spans.is_empty() {
1447                attrs_list.defaults()
1448            } else {
1449                attrs_list.get_span(0) // TODO: using the attrs from the first span for
1450                                       // ellipsis even if it's at the end. Which for rich text may look weird if the first
1451                                       // span has a different color or size than where ellipsizing is happening
1452            };
1453            let mut glyphs = shape_ellipsis(font_system, &attrs, shaping, rtl);
1454            if rtl {
1455                glyphs.reverse();
1456            }
1457            let word = ShapeWord {
1458                blank: false,
1459                glyphs,
1460            };
1461            // The level here is a placeholder; the actual level used for BiDi reordering
1462            // is set on the VlRange when the ellipsis is inserted during layout.
1463            let level = if rtl {
1464                unicode_bidi::Level::rtl()
1465            } else {
1466                unicode_bidi::Level::ltr()
1467            };
1468            ShapeSpan {
1469                level,
1470                words: vec![word],
1471                decoration_spans: Vec::new(),
1472            }
1473        });
1474
1475        // Return the buffer for later reuse.
1476        font_system.shape_buffer.spans = cached_spans;
1477    }
1478
1479    // A modified version of first part of unicode_bidi::bidi_info::visual_run
1480    fn adjust_levels(para: &unicode_bidi::Paragraph) -> Vec<unicode_bidi::Level> {
1481        use unicode_bidi::BidiClass::{B, BN, FSI, LRE, LRI, LRO, PDF, PDI, RLE, RLI, RLO, S, WS};
1482        let text = para.info.text;
1483        let levels = &para.info.levels;
1484        let original_classes = &para.info.original_classes;
1485
1486        let mut levels = levels.clone();
1487        let line_classes = &original_classes[..];
1488        let line_levels = &mut levels[..];
1489
1490        // Reset some whitespace chars to paragraph level.
1491        // <http://www.unicode.org/reports/tr9/#L1>
1492        let mut reset_from: Option<usize> = Some(0);
1493        let mut reset_to: Option<usize> = None;
1494        for (i, c) in text.char_indices() {
1495            match line_classes[i] {
1496                // Ignored by X9
1497                RLE | LRE | RLO | LRO | PDF | BN => {}
1498                // Segment separator, Paragraph separator
1499                B | S => {
1500                    assert_eq!(reset_to, None);
1501                    reset_to = Some(i + c.len_utf8());
1502                    if reset_from.is_none() {
1503                        reset_from = Some(i);
1504                    }
1505                }
1506                // Whitespace, isolate formatting
1507                WS | FSI | LRI | RLI | PDI => {
1508                    if reset_from.is_none() {
1509                        reset_from = Some(i);
1510                    }
1511                }
1512                _ => {
1513                    reset_from = None;
1514                }
1515            }
1516            if let (Some(from), Some(to)) = (reset_from, reset_to) {
1517                for level in &mut line_levels[from..to] {
1518                    *level = para.para.level;
1519                }
1520                reset_from = None;
1521                reset_to = None;
1522            }
1523        }
1524        if let Some(from) = reset_from {
1525            for level in &mut line_levels[from..] {
1526                *level = para.para.level;
1527            }
1528        }
1529        levels
1530    }
1531
1532    // A modified version of second part of unicode_bidi::bidi_info::visual run
1533    fn reorder(&self, line_range: &[VlRange]) -> Vec<Range<usize>> {
1534        let line: Vec<unicode_bidi::Level> = line_range.iter().map(|range| range.level).collect();
1535        let count = line.len();
1536        if count == 0 {
1537            return Vec::new();
1538        }
1539
1540        // Each VlRange is its own element for L2 reordering.
1541        // Using individual elements (not grouped runs) ensures that reversal
1542        // correctly reorders elements even when consecutive ranges share a level.
1543        let mut elements: Vec<Range<usize>> = (0..count).map(|i| i..i + 1).collect();
1544
1545        let mut min_level = line[0];
1546        let mut max_level = line[0];
1547        for &level in &line[1..] {
1548            min_level = min(min_level, level);
1549            max_level = max(max_level, level);
1550        }
1551
1552        // Re-order the odd runs.
1553        // <http://www.unicode.org/reports/tr9/#L2>
1554
1555        // Stop at the lowest *odd* level.
1556        min_level = min_level.new_lowest_ge_rtl().expect("Level error");
1557
1558        while max_level >= min_level {
1559            // Look for the start of a sequence of consecutive elements at max_level or higher.
1560            let mut seq_start = 0;
1561            while seq_start < count {
1562                if line[elements[seq_start].start] < max_level {
1563                    seq_start += 1;
1564                    continue;
1565                }
1566
1567                // Found the start of a sequence. Now find the end.
1568                let mut seq_end = seq_start + 1;
1569                while seq_end < count {
1570                    if line[elements[seq_end].start] < max_level {
1571                        break;
1572                    }
1573                    seq_end += 1;
1574                }
1575
1576                // Reverse the individual elements within this sequence.
1577                elements[seq_start..seq_end].reverse();
1578
1579                seq_start = seq_end;
1580            }
1581            max_level
1582                .lower(1)
1583                .expect("Lowering embedding level below zero");
1584        }
1585
1586        elements
1587    }
1588
1589    pub fn layout(
1590        &self,
1591        font_size: f32,
1592        width_opt: Option<f32>,
1593        wrap: Wrap,
1594        align: Option<Align>,
1595        match_mono_width: Option<f32>,
1596        hinting: Hinting,
1597    ) -> Vec<LayoutLine> {
1598        let mut lines = Vec::with_capacity(1);
1599        let mut scratch = ShapeBuffer::default();
1600        self.layout_to_buffer(
1601            &mut scratch,
1602            font_size,
1603            width_opt,
1604            wrap,
1605            Ellipsize::None,
1606            align,
1607            &mut lines,
1608            match_mono_width,
1609            hinting,
1610        );
1611        lines
1612    }
1613
1614    fn get_glyph_start_end(
1615        word: &ShapeWord,
1616        start: SpanWordGlyphPos,
1617        span_index: usize,
1618        word_idx: usize,
1619        _direction: LayoutDirection,
1620        congruent: bool,
1621    ) -> (usize, usize) {
1622        if span_index != start.span || word_idx != start.word {
1623            return (0, word.glyphs.len());
1624        }
1625        let (start_glyph_pos, end_glyph_pos) = if congruent {
1626            (start.glyph, word.glyphs.len())
1627        } else {
1628            (0, start.glyph)
1629        };
1630        (start_glyph_pos, end_glyph_pos)
1631    }
1632
1633    fn fit_glyphs(
1634        word: &ShapeWord,
1635        font_size: f32,
1636        start: SpanWordGlyphPos,
1637        span_index: usize,
1638        word_idx: usize,
1639        direction: LayoutDirection,
1640        congruent: bool,
1641        currently_used_width: f32,
1642        total_available_width: f32,
1643        forward: bool,
1644    ) -> (usize, f32) {
1645        let mut glyphs_w = 0.0;
1646        let (start_glyph_pos, end_glyph_pos) =
1647            Self::get_glyph_start_end(word, start, span_index, word_idx, direction, congruent);
1648
1649        if forward {
1650            let mut glyph_end = start_glyph_pos;
1651            for glyph_idx in start_glyph_pos..end_glyph_pos {
1652                let g_w = word.glyphs[glyph_idx].width(font_size);
1653                if currently_used_width + glyphs_w + g_w > total_available_width {
1654                    break;
1655                }
1656                glyphs_w += g_w;
1657                glyph_end = glyph_idx + 1;
1658            }
1659            (glyph_end, glyphs_w)
1660        } else {
1661            let mut glyph_end = word.glyphs.len();
1662            for glyph_idx in (start_glyph_pos..end_glyph_pos).rev() {
1663                let g_w = word.glyphs[glyph_idx].width(font_size);
1664                if currently_used_width + glyphs_w + g_w > total_available_width {
1665                    break;
1666                }
1667                glyphs_w += g_w;
1668                glyph_end = glyph_idx;
1669            }
1670            (glyph_end, glyphs_w)
1671        }
1672    }
1673
1674    #[inline]
1675    fn add_to_visual_line(
1676        &self,
1677        vl: &mut VisualLine,
1678        span_index: usize,
1679        start: WordGlyphPos,
1680        end: WordGlyphPos,
1681        width: f32,
1682        number_of_blanks: u32,
1683    ) {
1684        if end == start {
1685            return;
1686        }
1687
1688        vl.ranges.push(VlRange {
1689            span: span_index,
1690            start,
1691            end,
1692            level: self.spans[span_index].level,
1693        });
1694        vl.w += width;
1695        vl.spaces += number_of_blanks;
1696    }
1697
1698    fn remaining_content_exceeds(
1699        spans: &[ShapeSpan],
1700        font_size: f32,
1701        span_index: usize,
1702        word_idx: usize,
1703        word_count: usize,
1704        starting_word_index: usize,
1705        direction: LayoutDirection,
1706        congruent: bool,
1707        start_span: usize,
1708        span_count: usize,
1709        threshold: f32,
1710    ) -> bool {
1711        let mut acc: f32 = 0.0;
1712
1713        // Remaining words in the current span
1714        let word_range: Range<usize> = match (direction, congruent) {
1715            (LayoutDirection::Forward, true) => word_idx + 1..word_count,
1716            (LayoutDirection::Forward, false) => 0..word_idx,
1717            (LayoutDirection::Backward, true) => starting_word_index..word_idx,
1718            (LayoutDirection::Backward, false) => word_idx + 1..word_count,
1719        };
1720        for wi in word_range {
1721            acc += spans[span_index].words[wi].width(font_size);
1722            if acc > threshold {
1723                return true;
1724            }
1725        }
1726
1727        // Remaining spans
1728        let span_range: Range<usize> = match direction {
1729            LayoutDirection::Forward => span_index + 1..span_count,
1730            LayoutDirection::Backward => start_span..span_index,
1731        };
1732        for si in span_range {
1733            for w in &spans[si].words {
1734                acc += w.width(font_size);
1735                if acc > threshold {
1736                    return true;
1737                }
1738            }
1739        }
1740
1741        false
1742    }
1743
1744    /// This will fit as much as possible in one line
1745    /// If forward is false, it will fit as much as possible from the end of the spans
1746    /// it will stop when it gets to "start".
1747    /// If forward is true, it will start from start and keep going to the end of the spans
1748    #[inline]
1749    fn layout_spans(
1750        &self,
1751        current_visual_line: &mut VisualLine,
1752        font_size: f32,
1753        spans: &[ShapeSpan],
1754        start_opt: Option<SpanWordGlyphPos>,
1755        rtl: bool,
1756        width_opt: Option<f32>,
1757        ellipsize: Ellipsize,
1758        ellipsis_w: f32,
1759        direction: LayoutDirection,
1760    ) {
1761        let check_ellipsizing = matches!(ellipsize, Ellipsize::Start(_) | Ellipsize::End(_))
1762            && width_opt.is_some_and(|w| w.is_finite());
1763
1764        let max_width = width_opt.unwrap_or(f32::INFINITY);
1765        let span_count = spans.len();
1766
1767        let mut total_w: f32 = 0.0;
1768
1769        let start = if let Some(s) = start_opt {
1770            s
1771        } else {
1772            SpanWordGlyphPos::ZERO
1773        };
1774
1775        let span_indices: Vec<usize> = if matches!(direction, LayoutDirection::Forward) {
1776            (start.span..spans.len()).collect()
1777        } else {
1778            (start.span..spans.len()).rev().collect()
1779        };
1780
1781        'outer: for span_index in span_indices {
1782            let mut word_range_width = 0.;
1783            let mut number_of_blanks: u32 = 0;
1784
1785            let span = &spans[span_index];
1786            let word_count = span.words.len();
1787
1788            let starting_word_index = if span_index == start.span {
1789                start.word
1790            } else {
1791                0
1792            };
1793
1794            let congruent = rtl == span.level.is_rtl();
1795            let word_forward: bool = congruent == (direction == LayoutDirection::Forward);
1796
1797            let word_indices: Vec<usize> = match (direction, congruent, start_opt) {
1798                (LayoutDirection::Forward, true, _) => (starting_word_index..word_count).collect(),
1799                (LayoutDirection::Forward, false, Some(start)) => {
1800                    if span_index == start.span {
1801                        (0..start.word).rev().collect()
1802                    } else {
1803                        (0..word_count).rev().collect()
1804                    }
1805                }
1806                (LayoutDirection::Forward, false, None) => (0..word_count).rev().collect(),
1807                (LayoutDirection::Backward, true, _) => {
1808                    ((starting_word_index)..word_count).rev().collect()
1809                }
1810                (LayoutDirection::Backward, false, Some(start)) => {
1811                    if span_index == start.span {
1812                        if start.glyph > 0 {
1813                            (0..(start.word + 1)).collect()
1814                        } else {
1815                            (0..(start.word)).collect()
1816                        }
1817                    } else {
1818                        (0..word_count).collect()
1819                    }
1820                }
1821                (LayoutDirection::Backward, false, None) => (0..span.words.len()).collect(),
1822            };
1823            for word_idx in word_indices {
1824                let word = &span.words[word_idx];
1825                let word_width = if span_index == start.span && word_idx == start.word {
1826                    let (start_glyph_pos, end_glyph_pos) = Self::get_glyph_start_end(
1827                        word, start, span_index, word_idx, direction, congruent,
1828                    );
1829                    let mut w = 0.;
1830                    for glyph_idx in start_glyph_pos..end_glyph_pos {
1831                        w += word.glyphs[glyph_idx].width(font_size);
1832                    }
1833                    w
1834                } else {
1835                    word.width(font_size)
1836                };
1837
1838                let overflowing = {
1839                    // only check this if we're ellipsizing
1840                    check_ellipsizing
1841                        && (
1842                            // if this  word doesn't fit, then we have an overflow
1843                            (total_w + word_range_width + word_width > max_width)
1844                                || (Self::remaining_content_exceeds(
1845                                    spans,
1846                                    font_size,
1847                                    span_index,
1848                                    word_idx,
1849                                    word_count,
1850                                    starting_word_index,
1851                                    direction,
1852                                    congruent,
1853                                    start.span,
1854                                    span_count,
1855                                    ellipsis_w,
1856                                ) && total_w + word_range_width + word_width + ellipsis_w
1857                                    > max_width)
1858                        )
1859                };
1860
1861                if overflowing {
1862                    // overflow detected
1863                    let available = (max_width - ellipsis_w).max(0.0);
1864
1865                    let (glyph_end, glyphs_w) = Self::fit_glyphs(
1866                        word,
1867                        font_size,
1868                        start,
1869                        span_index,
1870                        word_idx,
1871                        direction,
1872                        congruent,
1873                        total_w + word_range_width,
1874                        available,
1875                        word_forward,
1876                    );
1877
1878                    let (start_pos, end_pos) = if word_forward {
1879                        if span_index == start.span {
1880                            if !congruent {
1881                                (WordGlyphPos::ZERO, WordGlyphPos::new(word_idx, glyph_end))
1882                            } else {
1883                                (
1884                                    start.word_glyph_pos(),
1885                                    WordGlyphPos::new(word_idx, glyph_end),
1886                                )
1887                            }
1888                        } else {
1889                            (WordGlyphPos::ZERO, WordGlyphPos::new(word_idx, glyph_end))
1890                        }
1891                    } else {
1892                        // For an incongruent span in the forward direction, the
1893                        // word indices are (0..start.word).rev(). Cap the VlRange
1894                        // end at start.word_glyph_pos() so it doesn't include
1895                        // words beyond start.word that belong to a previous line.
1896                        // For the backward direction (congruent span), the word
1897                        // indices are (start.word..word_count).rev() and
1898                        // span.words.len() is the correct end.
1899                        let range_end = if span_index == start.span && !congruent {
1900                            start.word_glyph_pos()
1901                        } else {
1902                            WordGlyphPos::new(span.words.len(), 0)
1903                        };
1904                        (WordGlyphPos::new(word_idx, glyph_end), range_end)
1905                    };
1906                    self.add_to_visual_line(
1907                        current_visual_line,
1908                        span_index,
1909                        start_pos,
1910                        end_pos,
1911                        word_range_width + glyphs_w,
1912                        number_of_blanks,
1913                    );
1914
1915                    // don't iterate anymore since we overflowed
1916                    current_visual_line.ellipsized = true;
1917                    break 'outer;
1918                }
1919
1920                word_range_width += word_width;
1921                if word.blank {
1922                    number_of_blanks += 1;
1923                }
1924
1925                // Backward-only: if we've reached the starting point, commit and stop.
1926                if matches!(direction, LayoutDirection::Backward)
1927                    && word_idx == start.word
1928                    && span_index == start.span
1929                {
1930                    let (start_pos, end_pos) = if word_forward {
1931                        (WordGlyphPos::ZERO, start.word_glyph_pos())
1932                    } else {
1933                        (
1934                            start.word_glyph_pos(),
1935                            WordGlyphPos::new(span.words.len(), 0),
1936                        )
1937                    };
1938
1939                    self.add_to_visual_line(
1940                        current_visual_line,
1941                        span_index,
1942                        start_pos,
1943                        end_pos,
1944                        word_range_width,
1945                        number_of_blanks,
1946                    );
1947
1948                    break 'outer;
1949                }
1950            }
1951
1952            // if we get to here that means we didn't ellipsize, so either the whole span fits,
1953            // or we don't really care
1954            total_w += word_range_width;
1955            let (start_pos, end_pos) = if congruent {
1956                if span_index == start.span {
1957                    (
1958                        start.word_glyph_pos(),
1959                        WordGlyphPos::new(span.words.len(), 0),
1960                    )
1961                } else {
1962                    (WordGlyphPos::ZERO, WordGlyphPos::new(span.words.len(), 0))
1963                }
1964            } else if span_index == start.span && (start.word, start.glyph) != (0, 0) {
1965                // Continuation of an incongruent (reversed) span after a wrap:
1966                // this visual line holds the logically-leading words [0, start).
1967                (WordGlyphPos::ZERO, start.word_glyph_pos())
1968            } else {
1969                // No continuation offset, so the whole incongruent span belongs to
1970                // this line. Without this, a fully-RTL span under a forced-LTR base
1971                // direction (start == 0) would collapse to an empty range and drop
1972                // all of its glyphs.
1973                (WordGlyphPos::ZERO, WordGlyphPos::new(span.words.len(), 0))
1974            };
1975
1976            self.add_to_visual_line(
1977                current_visual_line,
1978                span_index,
1979                start_pos,
1980                end_pos,
1981                word_range_width,
1982                number_of_blanks,
1983            );
1984        }
1985
1986        if matches!(direction, LayoutDirection::Backward) {
1987            current_visual_line.ranges.reverse();
1988        }
1989    }
1990
1991    fn layout_middle(
1992        &self,
1993        current_visual_line: &mut VisualLine,
1994        font_size: f32,
1995        spans: &[ShapeSpan],
1996        start_opt: Option<SpanWordGlyphPos>,
1997        rtl: bool,
1998        width: f32,
1999        ellipsize: Ellipsize,
2000        ellipsis_w: f32,
2001    ) {
2002        assert!(matches!(ellipsize, Ellipsize::Middle(_)));
2003
2004        // First check if all content fits without any ellipsis.
2005        {
2006            let mut test_line = VisualLine::default();
2007            self.layout_spans(
2008                &mut test_line,
2009                font_size,
2010                spans,
2011                start_opt,
2012                rtl,
2013                Some(width),
2014                Ellipsize::End(EllipsizeHeightLimit::Lines(1)),
2015                ellipsis_w,
2016                LayoutDirection::Forward,
2017            );
2018            if !test_line.ellipsized && test_line.w <= width {
2019                *current_visual_line = test_line;
2020                return;
2021            }
2022        }
2023
2024        let mut starting_line = VisualLine::default();
2025        self.layout_spans(
2026            &mut starting_line,
2027            font_size,
2028            spans,
2029            start_opt,
2030            rtl,
2031            Some(width / 2.0),
2032            Ellipsize::End(EllipsizeHeightLimit::Lines(1)),
2033            0., //pass 0 for ellipsis_w
2034            LayoutDirection::Forward,
2035        );
2036        let forward_pass_overflowed = starting_line.ellipsized;
2037        let end_range_opt = starting_line.ranges.last();
2038        match end_range_opt {
2039            Some(range) if forward_pass_overflowed => {
2040                let congruent = rtl == self.spans[range.span].level.is_rtl();
2041                // create a new range and do the other half
2042                let mut ending_line = VisualLine::default();
2043                let start = if congruent {
2044                    SpanWordGlyphPos {
2045                        span: range.span,
2046                        word: range.end.word,
2047                        glyph: range.end.glyph,
2048                    }
2049                } else {
2050                    SpanWordGlyphPos {
2051                        span: range.span,
2052                        word: range.start.word,
2053                        glyph: range.start.glyph,
2054                    }
2055                };
2056                self.layout_spans(
2057                    &mut ending_line,
2058                    font_size,
2059                    spans,
2060                    Some(start),
2061                    rtl,
2062                    Some((width - starting_line.w - ellipsis_w).max(0.0)),
2063                    Ellipsize::Start(EllipsizeHeightLimit::Lines(1)),
2064                    0., //pass 0 for ellipsis_w
2065                    LayoutDirection::Backward,
2066                );
2067                // Insert the ellipsis VlRange between the two halves.
2068                // Its BiDi level is determined by the adjacent ranges.
2069                let ellipsis_level = self.ellipsis_level_between(
2070                    starting_line.ranges.last(),
2071                    ending_line.ranges.first(),
2072                );
2073                starting_line
2074                    .ranges
2075                    .push(self.ellipsis_vlrange(ellipsis_level));
2076                starting_line.ranges.extend(ending_line.ranges);
2077                current_visual_line.ranges = starting_line.ranges;
2078                current_visual_line.ellipsized = true;
2079                current_visual_line.w = starting_line.w + ending_line.w + ellipsis_w;
2080                current_visual_line.spaces = starting_line.spaces + ending_line.spaces;
2081            }
2082            None if forward_pass_overflowed && width > ellipsis_w => {
2083                // buffer is small enough that the forward pass didn't fit
2084                // only show the ellipsis
2085                current_visual_line
2086                    .ranges
2087                    .push(self.ellipsis_vlrange(if self.rtl {
2088                        unicode_bidi::Level::rtl()
2089                    } else {
2090                        unicode_bidi::Level::ltr()
2091                    }));
2092                current_visual_line.ellipsized = true;
2093                current_visual_line.w = ellipsis_w;
2094                current_visual_line.spaces = 0;
2095            }
2096            _ => {
2097                // everything fit in the forward pass
2098                current_visual_line.ranges = starting_line.ranges;
2099                current_visual_line.w = starting_line.w;
2100                current_visual_line.spaces = starting_line.spaces;
2101                current_visual_line.ellipsized = false;
2102            }
2103        }
2104    }
2105
2106    /// Returns the words for a given span index, handling the ellipsis sentinel.
2107    fn get_span_words(&self, span_index: usize) -> &[ShapeWord] {
2108        if span_index == ELLIPSIS_SPAN {
2109            &self
2110                .ellipsis_span
2111                .as_ref()
2112                .expect("ellipsis_span not set")
2113                .words
2114        } else {
2115            &self.spans[span_index].words
2116        }
2117    }
2118
2119    fn byte_range_of_vlrange(&self, r: &VlRange) -> Option<(usize, usize)> {
2120        debug_assert_ne!(r.span, ELLIPSIS_SPAN);
2121        let words = self.get_span_words(r.span);
2122        let mut min_byte = usize::MAX;
2123        let mut max_byte = 0usize;
2124        let end_word = r.end.word + usize::from(r.end.glyph != 0);
2125        for (i, word) in words.iter().enumerate().take(end_word).skip(r.start.word) {
2126            let included_glyphs = match (i == r.start.word, i == r.end.word) {
2127                (false, false) => &word.glyphs[..],
2128                (true, false) => &word.glyphs[r.start.glyph..],
2129                (false, true) => &word.glyphs[..r.end.glyph],
2130                (true, true) => &word.glyphs[r.start.glyph..r.end.glyph],
2131            };
2132            for glyph in included_glyphs {
2133                min_byte = min_byte.min(glyph.start);
2134                max_byte = max_byte.max(glyph.end);
2135            }
2136        }
2137        if min_byte <= max_byte {
2138            Some((min_byte, max_byte))
2139        } else {
2140            None
2141        }
2142    }
2143
2144    fn compute_elided_byte_range(
2145        &self,
2146        visual_line: &VisualLine,
2147        line_len: usize,
2148    ) -> Option<(usize, usize)> {
2149        if !visual_line.ellipsized {
2150            return None;
2151        }
2152        // Find the position of the ellipsis VlRange
2153        let ellipsis_idx = visual_line
2154            .ranges
2155            .iter()
2156            .position(|r| r.span == ELLIPSIS_SPAN)?;
2157
2158        // Find the byte range of the visible content before the ellipsis
2159        let before_end = (0..ellipsis_idx)
2160            .rev()
2161            .find_map(|i| self.byte_range_of_vlrange(&visual_line.ranges[i]))
2162            .map(|(_, end)| end)
2163            .unwrap_or(0);
2164
2165        // Find the byte range of the visible content after the ellipsis
2166        let after_start = (ellipsis_idx + 1..visual_line.ranges.len())
2167            .find_map(|i| self.byte_range_of_vlrange(&visual_line.ranges[i]))
2168            .map(|(start, _)| start)
2169            .unwrap_or(line_len);
2170
2171        Some((before_end, after_start))
2172    }
2173
2174    /// Returns the maximum byte offset across all glyphs in all non-ellipsis spans.
2175    /// This effectively gives the byte length of the original shaped text.
2176    fn max_byte_offset(&self) -> usize {
2177        self.spans
2178            .iter()
2179            .flat_map(|span| span.words.iter())
2180            .flat_map(|word| word.glyphs.iter())
2181            .map(|g| g.end)
2182            .max()
2183            .unwrap_or(0)
2184    }
2185
2186    /// Returns the width of the ellipsis in the given font size.
2187    fn ellipsis_w(&self, font_size: f32) -> f32 {
2188        self.ellipsis_span
2189            .as_ref()
2190            .map_or(0.0, |s| s.words.iter().map(|w| w.width(font_size)).sum())
2191    }
2192
2193    /// Creates a `VlRange` for the ellipsis with the give`BiDi`Di level.
2194    fn ellipsis_vlrange(&self, level: unicode_bidi::Level) -> VlRange {
2195        VlRange {
2196            span: ELLIPSIS_SPAN,
2197            start: WordGlyphPos::ZERO,
2198            end: WordGlyphPos::new(1, 0),
2199            level,
2200        }
2201    }
2202
2203    /// Determines the appropriate `BiDi` level for the ellipsis based on the
2204    /// adjacent ranges, following UAX#9 N1/N2 rules for neutral characters.
2205    fn ellipsis_level_between(
2206        &self,
2207        before: Option<&VlRange>,
2208        after: Option<&VlRange>,
2209    ) -> unicode_bidi::Level {
2210        match (before, after) {
2211            (Some(a), Some(b)) if a.level == b.level => a.level,
2212            (Some(a), None) => a.level,
2213            (None, Some(b)) => b.level,
2214            _ => {
2215                if self.rtl {
2216                    unicode_bidi::Level::rtl()
2217                } else {
2218                    unicode_bidi::Level::ltr()
2219                }
2220            }
2221        }
2222    }
2223
2224    fn layout_line(
2225        &self,
2226        current_visual_line: &mut VisualLine,
2227        font_size: f32,
2228        spans: &[ShapeSpan],
2229        start_opt: Option<SpanWordGlyphPos>,
2230        rtl: bool,
2231        width_opt: Option<f32>,
2232        ellipsize: Ellipsize,
2233    ) {
2234        let ellipsis_w = self.ellipsis_w(font_size);
2235
2236        match (ellipsize, width_opt) {
2237            (Ellipsize::Start(_), Some(_)) => {
2238                self.layout_spans(
2239                    current_visual_line,
2240                    font_size,
2241                    spans,
2242                    start_opt,
2243                    rtl,
2244                    width_opt,
2245                    ellipsize,
2246                    ellipsis_w,
2247                    LayoutDirection::Backward,
2248                );
2249                // Insert ellipsis at the visual start (index 0, after backward reversal)
2250                if current_visual_line.ellipsized {
2251                    let level =
2252                        self.ellipsis_level_between(None, current_visual_line.ranges.first());
2253                    current_visual_line
2254                        .ranges
2255                        .insert(0, self.ellipsis_vlrange(level));
2256                    current_visual_line.w += ellipsis_w;
2257                }
2258            }
2259            (Ellipsize::Middle(_), Some(width)) => {
2260                self.layout_middle(
2261                    current_visual_line,
2262                    font_size,
2263                    spans,
2264                    start_opt,
2265                    rtl,
2266                    width,
2267                    ellipsize,
2268                    ellipsis_w,
2269                );
2270            }
2271            _ => {
2272                self.layout_spans(
2273                    current_visual_line,
2274                    font_size,
2275                    spans,
2276                    start_opt,
2277                    rtl,
2278                    width_opt,
2279                    ellipsize,
2280                    ellipsis_w,
2281                    LayoutDirection::Forward,
2282                );
2283                // Insert ellipsis at the visual end
2284                if current_visual_line.ellipsized {
2285                    let level =
2286                        self.ellipsis_level_between(current_visual_line.ranges.last(), None);
2287                    current_visual_line
2288                        .ranges
2289                        .push(self.ellipsis_vlrange(level));
2290                    current_visual_line.w += ellipsis_w;
2291                }
2292            }
2293        }
2294
2295        // Compute the byte range of ellipsized text so the ellipsis LayoutGlyph
2296        // can have valid start/end indices into the original line text.
2297        if current_visual_line.ellipsized {
2298            let line_len = self.max_byte_offset();
2299            current_visual_line.elided_byte_range =
2300                self.compute_elided_byte_range(current_visual_line, line_len);
2301        }
2302    }
2303
2304    pub fn layout_to_buffer(
2305        &self,
2306        scratch: &mut ShapeBuffer,
2307        font_size: f32,
2308        width_opt: Option<f32>,
2309        wrap: Wrap,
2310        ellipsize: Ellipsize,
2311        align: Option<Align>,
2312        layout_lines: &mut Vec<LayoutLine>,
2313        match_mono_width: Option<f32>,
2314        hinting: Hinting,
2315    ) {
2316        // For each visual line a list of  (span index,  and range of words in that span)
2317        // Note that a BiDi visual line could have multiple spans or parts of them
2318        // let mut vl_range_of_spans = Vec::with_capacity(1);
2319        let mut visual_lines = mem::take(&mut scratch.visual_lines);
2320        let mut cached_visual_lines = mem::take(&mut scratch.cached_visual_lines);
2321        cached_visual_lines.clear();
2322        cached_visual_lines.extend(visual_lines.drain(..).map(|mut l| {
2323            l.clear();
2324            l
2325        }));
2326
2327        // Cache glyph sets in reverse order so they will ideally be reused in exactly the same lines.
2328        let mut cached_glyph_sets = mem::take(&mut scratch.glyph_sets);
2329        cached_glyph_sets.clear();
2330        cached_glyph_sets.extend(layout_lines.drain(..).rev().map(|mut v| {
2331            v.glyphs.clear();
2332            v.glyphs
2333        }));
2334
2335        // This would keep the maximum number of spans that would fit on a visual line
2336        // If one span is too large, this variable will hold the range of words inside that span
2337        // that fits on a line.
2338        // let mut current_visual_line: Vec<VlRange> = Vec::with_capacity(1);
2339        let mut current_visual_line = cached_visual_lines.pop().unwrap_or_default();
2340
2341        if wrap == Wrap::None {
2342            self.layout_line(
2343                &mut current_visual_line,
2344                font_size,
2345                &self.spans,
2346                None,
2347                self.rtl,
2348                width_opt,
2349                ellipsize,
2350            );
2351        } else {
2352            let mut total_line_height = 0.0;
2353            let mut total_line_count = 0;
2354            let max_line_count_opt = match ellipsize {
2355                Ellipsize::Start(EllipsizeHeightLimit::Lines(lines))
2356                | Ellipsize::Middle(EllipsizeHeightLimit::Lines(lines))
2357                | Ellipsize::End(EllipsizeHeightLimit::Lines(lines)) => Some(lines.max(1)),
2358                _ => None,
2359            };
2360            let max_height_opt = match ellipsize {
2361                Ellipsize::Start(EllipsizeHeightLimit::Height(height))
2362                | Ellipsize::Middle(EllipsizeHeightLimit::Height(height))
2363                | Ellipsize::End(EllipsizeHeightLimit::Height(height)) => Some(height),
2364                _ => None,
2365            };
2366            let line_height = self
2367                .metrics_opt
2368                .map_or_else(|| font_size, |m| m.line_height);
2369
2370            let try_ellipsize_last_line = |total_line_count: usize,
2371                                           total_line_height: f32,
2372                                           current_visual_line: &mut VisualLine,
2373                                           font_size: f32,
2374                                           start_opt: Option<SpanWordGlyphPos>,
2375                                           width_opt: Option<f32>,
2376                                           ellipsize: Ellipsize|
2377             -> bool {
2378                // If Ellipsize::End, then how many lines can we fit or how much is the available height
2379                if max_line_count_opt == Some(total_line_count + 1)
2380                    || max_height_opt.is_some_and(|max_height| {
2381                        total_line_height + line_height * 2.0 > max_height
2382                    })
2383                {
2384                    self.layout_line(
2385                        current_visual_line,
2386                        font_size,
2387                        &self.spans,
2388                        start_opt,
2389                        self.rtl,
2390                        width_opt,
2391                        ellipsize,
2392                    );
2393                    return true;
2394                }
2395                false
2396            };
2397
2398            if !try_ellipsize_last_line(
2399                total_line_count,
2400                total_line_height,
2401                &mut current_visual_line,
2402                font_size,
2403                None,
2404                width_opt,
2405                ellipsize,
2406            ) {
2407                'outer: for (span_index, span) in self.spans.iter().enumerate() {
2408                    let mut word_range_width = 0.;
2409                    let mut width_before_last_blank = 0.;
2410                    let mut number_of_blanks: u32 = 0;
2411
2412                    // Create the word ranges that fits in a visual line
2413                    if self.rtl != span.level.is_rtl() {
2414                        // incongruent directions
2415                        let mut fitting_start = WordGlyphPos::new(span.words.len(), 0);
2416                        for (i, word) in span.words.iter().enumerate().rev() {
2417                            let word_width = word.width(font_size);
2418                            // Addition in the same order used to compute the final width, so that
2419                            // relayouts with that width as the `line_width` will produce the same
2420                            // wrapping results.
2421                            if current_visual_line.w + (word_range_width + word_width)
2422                            <= width_opt.unwrap_or(f32::INFINITY)
2423                            // Include one blank word over the width limit since it won't be
2424                            // counted in the final width
2425                            || (word.blank
2426                                && (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
2427                            {
2428                                // fits
2429                                if word.blank {
2430                                    number_of_blanks += 1;
2431                                    width_before_last_blank = word_range_width;
2432                                }
2433                                word_range_width += word_width;
2434                            } else if wrap == Wrap::Glyph
2435                            // Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
2436                            || (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
2437                            {
2438                                // Commit the current line so that the word starts on the next line.
2439                                if word_range_width > 0.
2440                                    && wrap == Wrap::WordOrGlyph
2441                                    && word_width > width_opt.unwrap_or(f32::INFINITY)
2442                                {
2443                                    self.add_to_visual_line(
2444                                        &mut current_visual_line,
2445                                        span_index,
2446                                        WordGlyphPos::new(i + 1, 0),
2447                                        fitting_start,
2448                                        word_range_width,
2449                                        number_of_blanks,
2450                                    );
2451
2452                                    visual_lines.push(current_visual_line);
2453                                    current_visual_line =
2454                                        cached_visual_lines.pop().unwrap_or_default();
2455
2456                                    number_of_blanks = 0;
2457                                    word_range_width = 0.;
2458
2459                                    fitting_start = WordGlyphPos::new(i, 0);
2460                                    total_line_count += 1;
2461                                    total_line_height += line_height;
2462                                    if try_ellipsize_last_line(
2463                                        total_line_count,
2464                                        total_line_height,
2465                                        &mut current_visual_line,
2466                                        font_size,
2467                                        Some(SpanWordGlyphPos::with_wordglyph(
2468                                            span_index,
2469                                            fitting_start,
2470                                        )),
2471                                        width_opt,
2472                                        ellipsize,
2473                                    ) {
2474                                        break 'outer;
2475                                    }
2476                                }
2477
2478                                for (glyph_i, glyph) in word.glyphs.iter().enumerate().rev() {
2479                                    let glyph_width = glyph.width(font_size);
2480                                    if current_visual_line.w + (word_range_width + glyph_width)
2481                                        <= width_opt.unwrap_or(f32::INFINITY)
2482                                    {
2483                                        word_range_width += glyph_width;
2484                                    } else {
2485                                        self.add_to_visual_line(
2486                                            &mut current_visual_line,
2487                                            span_index,
2488                                            WordGlyphPos::new(i, glyph_i + 1),
2489                                            fitting_start,
2490                                            word_range_width,
2491                                            number_of_blanks,
2492                                        );
2493                                        visual_lines.push(current_visual_line);
2494                                        current_visual_line =
2495                                            cached_visual_lines.pop().unwrap_or_default();
2496
2497                                        number_of_blanks = 0;
2498                                        word_range_width = glyph_width;
2499                                        fitting_start = WordGlyphPos::new(i, glyph_i + 1);
2500                                        total_line_count += 1;
2501                                        total_line_height += line_height;
2502                                        if try_ellipsize_last_line(
2503                                            total_line_count,
2504                                            total_line_height,
2505                                            &mut current_visual_line,
2506                                            font_size,
2507                                            Some(SpanWordGlyphPos::with_wordglyph(
2508                                                span_index,
2509                                                fitting_start,
2510                                            )),
2511                                            width_opt,
2512                                            ellipsize,
2513                                        ) {
2514                                            break 'outer;
2515                                        }
2516                                    }
2517                                }
2518                            } else {
2519                                // Wrap::Word, Wrap::WordOrGlyph
2520
2521                                // If we had a previous range, commit that line before the next word.
2522                                if word_range_width > 0. {
2523                                    // Current word causing a wrap is not whitespace, so we ignore the
2524                                    // previous word if it's a whitespace
2525                                    let trailing_blank = span
2526                                        .words
2527                                        .get(i + 1)
2528                                        .is_some_and(|previous_word| previous_word.blank);
2529
2530                                    if trailing_blank {
2531                                        number_of_blanks = number_of_blanks.saturating_sub(1);
2532                                        self.add_to_visual_line(
2533                                            &mut current_visual_line,
2534                                            span_index,
2535                                            WordGlyphPos::new(i + 2, 0),
2536                                            fitting_start,
2537                                            width_before_last_blank,
2538                                            number_of_blanks,
2539                                        );
2540                                    } else {
2541                                        self.add_to_visual_line(
2542                                            &mut current_visual_line,
2543                                            span_index,
2544                                            WordGlyphPos::new(i + 1, 0),
2545                                            fitting_start,
2546                                            word_range_width,
2547                                            number_of_blanks,
2548                                        );
2549                                    }
2550                                }
2551
2552                                // This fixes a bug that a long first word at the boundary of
2553                                // was overflowing
2554                                if !current_visual_line.ranges.is_empty() {
2555                                    visual_lines.push(current_visual_line);
2556                                    current_visual_line =
2557                                        cached_visual_lines.pop().unwrap_or_default();
2558                                    number_of_blanks = 0;
2559                                    total_line_count += 1;
2560                                    total_line_height += line_height;
2561
2562                                    if try_ellipsize_last_line(
2563                                        total_line_count,
2564                                        total_line_height,
2565                                        &mut current_visual_line,
2566                                        font_size,
2567                                        Some(SpanWordGlyphPos::with_wordglyph(
2568                                            span_index,
2569                                            if word.blank {
2570                                                WordGlyphPos::new(i, 0)
2571                                            } else {
2572                                                WordGlyphPos::new(i + 1, 0)
2573                                            },
2574                                        )),
2575                                        width_opt,
2576                                        ellipsize,
2577                                    ) {
2578                                        break 'outer;
2579                                    }
2580                                }
2581
2582                                if word.blank {
2583                                    word_range_width = 0.;
2584                                    fitting_start = WordGlyphPos::new(i, 0);
2585                                } else {
2586                                    word_range_width = word_width;
2587                                    fitting_start = WordGlyphPos::new(i + 1, 0);
2588                                }
2589                            }
2590                        }
2591                        self.add_to_visual_line(
2592                            &mut current_visual_line,
2593                            span_index,
2594                            WordGlyphPos::new(0, 0),
2595                            fitting_start,
2596                            word_range_width,
2597                            number_of_blanks,
2598                        );
2599                    } else {
2600                        // congruent direction
2601                        let mut fitting_start = WordGlyphPos::ZERO;
2602                        for (i, word) in span.words.iter().enumerate() {
2603                            let word_width = word.width(font_size);
2604                            if current_visual_line.w + (word_range_width + word_width)
2605                            <= width_opt.unwrap_or(f32::INFINITY)
2606                            // Include one blank word over the width limit since it won't be
2607                            // counted in the final width.
2608                            || (word.blank
2609                                && (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
2610                            {
2611                                // fits
2612                                if word.blank {
2613                                    number_of_blanks += 1;
2614                                    width_before_last_blank = word_range_width;
2615                                }
2616                                word_range_width += word_width;
2617                            } else if wrap == Wrap::Glyph
2618                            // Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
2619                            || (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
2620                            {
2621                                // Commit the current line so that the word starts on the next line.
2622                                if word_range_width > 0.
2623                                    && wrap == Wrap::WordOrGlyph
2624                                    && word_width > width_opt.unwrap_or(f32::INFINITY)
2625                                {
2626                                    self.add_to_visual_line(
2627                                        &mut current_visual_line,
2628                                        span_index,
2629                                        fitting_start,
2630                                        WordGlyphPos::new(i, 0),
2631                                        word_range_width,
2632                                        number_of_blanks,
2633                                    );
2634
2635                                    visual_lines.push(current_visual_line);
2636                                    current_visual_line =
2637                                        cached_visual_lines.pop().unwrap_or_default();
2638
2639                                    number_of_blanks = 0;
2640                                    word_range_width = 0.;
2641
2642                                    fitting_start = WordGlyphPos::new(i, 0);
2643                                    total_line_count += 1;
2644                                    total_line_height += line_height;
2645                                    if try_ellipsize_last_line(
2646                                        total_line_count,
2647                                        total_line_height,
2648                                        &mut current_visual_line,
2649                                        font_size,
2650                                        Some(SpanWordGlyphPos::with_wordglyph(
2651                                            span_index,
2652                                            fitting_start,
2653                                        )),
2654                                        width_opt,
2655                                        ellipsize,
2656                                    ) {
2657                                        break 'outer;
2658                                    }
2659                                }
2660
2661                                for (glyph_i, glyph) in word.glyphs.iter().enumerate() {
2662                                    let glyph_width = glyph.width(font_size);
2663                                    if current_visual_line.w + (word_range_width + glyph_width)
2664                                        <= width_opt.unwrap_or(f32::INFINITY)
2665                                    {
2666                                        word_range_width += glyph_width;
2667                                    } else {
2668                                        self.add_to_visual_line(
2669                                            &mut current_visual_line,
2670                                            span_index,
2671                                            fitting_start,
2672                                            WordGlyphPos::new(i, glyph_i),
2673                                            word_range_width,
2674                                            number_of_blanks,
2675                                        );
2676                                        visual_lines.push(current_visual_line);
2677                                        current_visual_line =
2678                                            cached_visual_lines.pop().unwrap_or_default();
2679
2680                                        number_of_blanks = 0;
2681                                        word_range_width = glyph_width;
2682                                        fitting_start = WordGlyphPos::new(i, glyph_i);
2683                                        total_line_count += 1;
2684                                        total_line_height += line_height;
2685                                        if try_ellipsize_last_line(
2686                                            total_line_count,
2687                                            total_line_height,
2688                                            &mut current_visual_line,
2689                                            font_size,
2690                                            Some(SpanWordGlyphPos::with_wordglyph(
2691                                                span_index,
2692                                                fitting_start,
2693                                            )),
2694                                            width_opt,
2695                                            ellipsize,
2696                                        ) {
2697                                            break 'outer;
2698                                        }
2699                                    }
2700                                }
2701                            } else {
2702                                // Wrap::Word, Wrap::WordOrGlyph
2703
2704                                // If we had a previous range, commit that line before the next word.
2705                                if word_range_width > 0. {
2706                                    // Current word causing a wrap is not whitespace, so we ignore the
2707                                    // previous word if it's a whitespace.
2708                                    let trailing_blank = i > 0 && span.words[i - 1].blank;
2709
2710                                    if trailing_blank {
2711                                        number_of_blanks = number_of_blanks.saturating_sub(1);
2712                                        self.add_to_visual_line(
2713                                            &mut current_visual_line,
2714                                            span_index,
2715                                            fitting_start,
2716                                            WordGlyphPos::new(i - 1, 0),
2717                                            width_before_last_blank,
2718                                            number_of_blanks,
2719                                        );
2720                                    } else {
2721                                        self.add_to_visual_line(
2722                                            &mut current_visual_line,
2723                                            span_index,
2724                                            fitting_start,
2725                                            WordGlyphPos::new(i, 0),
2726                                            word_range_width,
2727                                            number_of_blanks,
2728                                        );
2729                                    }
2730                                }
2731
2732                                if !current_visual_line.ranges.is_empty() {
2733                                    visual_lines.push(current_visual_line);
2734                                    current_visual_line =
2735                                        cached_visual_lines.pop().unwrap_or_default();
2736                                    number_of_blanks = 0;
2737                                    total_line_count += 1;
2738                                    total_line_height += line_height;
2739                                    if try_ellipsize_last_line(
2740                                        total_line_count,
2741                                        total_line_height,
2742                                        &mut current_visual_line,
2743                                        font_size,
2744                                        Some(SpanWordGlyphPos::with_wordglyph(
2745                                            span_index,
2746                                            if i > 0 && span.words[i - 1].blank {
2747                                                WordGlyphPos::new(i - 1, 0)
2748                                            } else {
2749                                                WordGlyphPos::new(i, 0)
2750                                            },
2751                                        )),
2752                                        width_opt,
2753                                        ellipsize,
2754                                    ) {
2755                                        break 'outer;
2756                                    }
2757                                }
2758
2759                                if word.blank {
2760                                    word_range_width = 0.;
2761                                    fitting_start = WordGlyphPos::new(i + 1, 0);
2762                                } else {
2763                                    word_range_width = word_width;
2764                                    fitting_start = WordGlyphPos::new(i, 0);
2765                                }
2766                            }
2767                        }
2768                        self.add_to_visual_line(
2769                            &mut current_visual_line,
2770                            span_index,
2771                            fitting_start,
2772                            WordGlyphPos::new(span.words.len(), 0),
2773                            word_range_width,
2774                            number_of_blanks,
2775                        );
2776                    }
2777                }
2778            }
2779        }
2780
2781        if current_visual_line.ranges.is_empty() {
2782            current_visual_line.clear();
2783            cached_visual_lines.push(current_visual_line);
2784        } else {
2785            visual_lines.push(current_visual_line);
2786        }
2787
2788        // Create the LayoutLines using the ranges inside visual lines
2789        let align = align.unwrap_or(if self.rtl { Align::Right } else { Align::Left });
2790
2791        let line_width = width_opt.unwrap_or_else(|| {
2792            let mut width: f32 = 0.0;
2793            for visual_line in &visual_lines {
2794                width = width.max(visual_line.w);
2795            }
2796            width
2797        });
2798
2799        let start_x = if self.rtl { line_width } else { 0.0 };
2800
2801        let number_of_visual_lines = visual_lines.len();
2802        for (index, visual_line) in visual_lines.iter().enumerate() {
2803            if visual_line.ranges.is_empty() {
2804                continue;
2805            }
2806
2807            let new_order = self.reorder(&visual_line.ranges);
2808
2809            let mut glyphs = cached_glyph_sets
2810                .pop()
2811                .unwrap_or_else(|| Vec::with_capacity(1));
2812            let mut x = start_x;
2813            let mut y = 0.;
2814            let mut max_ascent: f32 = 0.;
2815            let mut max_descent: f32 = 0.;
2816            let alignment_correction = match (align, self.rtl) {
2817                (Align::Left, true) => (line_width - visual_line.w).max(0.),
2818                (Align::Left, false) => 0.,
2819                (Align::Right, true) => 0.,
2820                (Align::Right, false) => (line_width - visual_line.w).max(0.),
2821                (Align::Center, _) => (line_width - visual_line.w).max(0.) / 2.0,
2822                (Align::End, _) => (line_width - visual_line.w).max(0.),
2823                (Align::Justified, _) => 0.,
2824            };
2825
2826            if self.rtl {
2827                x -= alignment_correction;
2828            } else {
2829                x += alignment_correction;
2830            }
2831
2832            if hinting == Hinting::Enabled {
2833                x = x.round();
2834            }
2835
2836            // TODO: Only certain `is_whitespace` chars are typically expanded but this is what is
2837            // currently used to compute `visual_line.spaces`.
2838            //
2839            // https://www.unicode.org/reports/tr14/#Introduction
2840            // > When expanding or compressing interword space according to common
2841            // > typographical practice, only the spaces marked by U+0020 SPACE and U+00A0
2842            // > NO-BREAK SPACE are subject to compression, and only spaces marked by U+0020
2843            // > SPACE, U+00A0 NO-BREAK SPACE, and occasionally spaces marked by U+2009 THIN
2844            // > SPACE are subject to expansion. All other space characters normally have
2845            // > fixed width.
2846            //
2847            // (also some spaces aren't followed by potential linebreaks but they could
2848            //  still be expanded)
2849
2850            // Amount of extra width added to each blank space within a line.
2851            let justification_expansion = if matches!(align, Align::Justified)
2852                && visual_line.spaces > 0
2853                // Don't justify the last line in a paragraph.
2854                && index != number_of_visual_lines - 1
2855            {
2856                (line_width - visual_line.w) / visual_line.spaces as f32
2857            } else {
2858                0.
2859            };
2860
2861            let elided_byte_range = if visual_line.ellipsized {
2862                visual_line.elided_byte_range
2863            } else {
2864                None
2865            };
2866
2867            let mut decorations: Vec<DecorationSpan> = Vec::new();
2868
2869            let process_range = |range: Range<usize>,
2870                                 x: &mut f32,
2871                                 y: &mut f32,
2872                                 glyphs: &mut Vec<LayoutGlyph>,
2873                                 decorations: &mut Vec<DecorationSpan>,
2874                                 max_ascent: &mut f32,
2875                                 max_descent: &mut f32| {
2876                for r in visual_line.ranges[range.clone()].iter() {
2877                    let is_ellipsis = r.span == ELLIPSIS_SPAN;
2878                    let span_words = self.get_span_words(r.span);
2879                    let deco_spans: &[(Range<usize>, GlyphDecorationData)] = if is_ellipsis {
2880                        &[]
2881                    } else {
2882                        &self.spans[r.span].decoration_spans
2883                    };
2884                    // Cursor into deco_spans — advances forward as glyphs are
2885                    // emitted in byte order, giving amortized O(1) lookup.
2886                    let mut deco_cursor: usize = 0;
2887                    // If ending_glyph is not 0 we need to include glyphs from the ending_word
2888                    for i in r.start.word..r.end.word + usize::from(r.end.glyph != 0) {
2889                        let word = &span_words[i];
2890                        let included_glyphs = match (i == r.start.word, i == r.end.word) {
2891                            (false, false) => &word.glyphs[..],
2892                            (true, false) => &word.glyphs[r.start.glyph..],
2893                            (false, true) => &word.glyphs[..r.end.glyph],
2894                            (true, true) => &word.glyphs[r.start.glyph..r.end.glyph],
2895                        };
2896
2897                        for glyph in included_glyphs {
2898                            // Use overridden font size
2899                            let font_size = glyph.metrics_opt.map_or(font_size, |x| x.font_size);
2900
2901                            let match_mono_em_width = match_mono_width.map(|w| w / font_size);
2902
2903                            let glyph_font_size = match (
2904                                match_mono_em_width,
2905                                glyph.font_monospace_em_width,
2906                            ) {
2907                                (Some(match_em_width), Some(glyph_em_width))
2908                                    if glyph_em_width != match_em_width =>
2909                                {
2910                                    let glyph_to_match_factor = glyph_em_width / match_em_width;
2911                                    let glyph_font_size = math::roundf(glyph_to_match_factor)
2912                                        .max(1.0)
2913                                        / glyph_to_match_factor
2914                                        * font_size;
2915                                    log::trace!(
2916                                        "Adjusted glyph font size ({font_size} => {glyph_font_size})"
2917                                    );
2918                                    glyph_font_size
2919                                }
2920                                _ => font_size,
2921                            };
2922
2923                            let mut x_advance = glyph_font_size.mul_add(
2924                                glyph.x_advance,
2925                                if word.blank {
2926                                    justification_expansion
2927                                } else {
2928                                    0.0
2929                                },
2930                            );
2931                            if let Some(match_em_width) = match_mono_em_width {
2932                                // Round to nearest monospace width
2933                                x_advance = ((x_advance / match_em_width).round()) * match_em_width;
2934                            }
2935                            if hinting == Hinting::Enabled {
2936                                x_advance = x_advance.round();
2937                            }
2938                            if self.rtl {
2939                                *x -= x_advance;
2940                            }
2941                            let y_advance = glyph_font_size * glyph.y_advance;
2942                            let mut layout_glyph = glyph.layout(
2943                                glyph_font_size,
2944                                glyph.metrics_opt.map(|x| x.line_height),
2945                                *x,
2946                                *y,
2947                                x_advance,
2948                                r.level,
2949                            );
2950                            // Fix ellipsis glyph indices: point both start and
2951                            // end to the elision boundary so that hit-detection
2952                            // places the cursor at the seam between visible and
2953                            // elided text instead of selecting invisible content.
2954                            if is_ellipsis {
2955                                if let Some((elided_start, elided_end)) = elided_byte_range {
2956                                    // Use the boundary closest to the visible
2957                                    // content that is adjacent to this ellipsis:
2958                                    //   Start:  …|visible  → boundary = elided_end
2959                                    //   End:    visible|…  → boundary = elided_start
2960                                    //   Middle: vis|…|vis  → boundary = elided_start
2961                                    let boundary = if elided_start == 0 {
2962                                        elided_end
2963                                    } else {
2964                                        elided_start
2965                                    };
2966                                    layout_glyph.start = boundary;
2967                                    layout_glyph.end = boundary;
2968                                }
2969                            }
2970                            glyphs.push(layout_glyph);
2971
2972                            if deco_cursor >= deco_spans.len()
2973                                || glyph.start < deco_spans[deco_cursor].0.start
2974                            {
2975                                deco_cursor = 0;
2976                            }
2977                            while deco_cursor < deco_spans.len()
2978                                && deco_spans[deco_cursor].0.end <= glyph.start
2979                            {
2980                                deco_cursor += 1;
2981                            }
2982                            let glyph_deco = deco_spans
2983                                .get(deco_cursor)
2984                                .filter(|(range, _)| glyph.start >= range.start);
2985                            let glyph_idx = glyphs.len() - 1;
2986                            let extends = matches!(
2987                                (decorations.last(), &glyph_deco),
2988                                (Some(span), Some((_, d))) if span.data == *d
2989                            );
2990                            if extends {
2991                                if let Some(last) = decorations.last_mut() {
2992                                    last.glyph_range.end = glyph_idx + 1;
2993                                }
2994                            } else if let Some((_, d)) = glyph_deco {
2995                                decorations.push(DecorationSpan {
2996                                    glyph_range: glyph_idx..glyph_idx + 1,
2997                                    data: d.clone(),
2998                                    color_opt: glyphs[glyph_idx].color_opt,
2999                                    font_size: glyphs[glyph_idx].font_size,
3000                                });
3001                            }
3002                            if !self.rtl {
3003                                *x += x_advance;
3004                            }
3005                            *y += y_advance;
3006                            *max_ascent = max_ascent.max(glyph_font_size * glyph.ascent);
3007                            *max_descent = max_descent.max(glyph_font_size * glyph.descent);
3008                        }
3009                    }
3010                }
3011            };
3012
3013            if self.rtl {
3014                for range in new_order.into_iter().rev() {
3015                    process_range(
3016                        range,
3017                        &mut x,
3018                        &mut y,
3019                        &mut glyphs,
3020                        &mut decorations,
3021                        &mut max_ascent,
3022                        &mut max_descent,
3023                    );
3024                }
3025            } else {
3026                /* LTR */
3027                for range in new_order {
3028                    process_range(
3029                        range,
3030                        &mut x,
3031                        &mut y,
3032                        &mut glyphs,
3033                        &mut decorations,
3034                        &mut max_ascent,
3035                        &mut max_descent,
3036                    );
3037                }
3038            }
3039
3040            let mut line_height_opt: Option<f32> = None;
3041            for glyph in &glyphs {
3042                if let Some(glyph_line_height) = glyph.line_height_opt {
3043                    line_height_opt = line_height_opt
3044                        .map_or(Some(glyph_line_height), |line_height| {
3045                            Some(line_height.max(glyph_line_height))
3046                        });
3047                }
3048            }
3049
3050            layout_lines.push(LayoutLine {
3051                w: if align != Align::Justified {
3052                    visual_line.w
3053                } else if self.rtl {
3054                    start_x - x
3055                } else {
3056                    x
3057                },
3058                max_ascent,
3059                max_descent,
3060                line_height_opt,
3061                glyphs,
3062                decorations,
3063            });
3064        }
3065
3066        // This is used to create a visual line for empty lines (e.g. lines with only a <CR>)
3067        if layout_lines.is_empty() {
3068            layout_lines.push(LayoutLine {
3069                w: 0.0,
3070                max_ascent: 0.0,
3071                max_descent: 0.0,
3072                line_height_opt: self.metrics_opt.map(|x| x.line_height),
3073                glyphs: Vec::default(),
3074                decorations: Vec::new(),
3075            });
3076        }
3077
3078        // Restore the buffer to the scratch set to prevent reallocations.
3079        scratch.visual_lines = visual_lines;
3080        scratch.visual_lines.append(&mut cached_visual_lines);
3081        scratch.cached_visual_lines = cached_visual_lines;
3082        scratch.glyph_sets = cached_glyph_sets;
3083    }
3084}