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. If [`unicode_bidi::BidiInfo`]
1328    /// detects multiple paragraphs, they will be joined.
1329    ///
1330    /// # Panics
1331    ///
1332    /// Will panic if `line` contains multiple paragraphs that do not have matching direction
1333    pub fn new(
1334        font_system: &mut FontSystem,
1335        line: &str,
1336        attrs_list: &AttrsList,
1337        shaping: Shaping,
1338        tab_width: u16,
1339        direction: Direction,
1340    ) -> Self {
1341        let mut empty = Self::empty();
1342        empty.build(font_system, line, attrs_list, shaping, tab_width, direction);
1343        empty
1344    }
1345
1346    /// See [`Self::new`].
1347    ///
1348    /// Reuses as much of the pre-existing internal allocations as possible.
1349    ///
1350    /// # Panics
1351    ///
1352    /// Will panic if `line` contains multiple paragraphs that do not have matching direction
1353    pub fn build(
1354        &mut self,
1355        font_system: &mut FontSystem,
1356        line: &str,
1357        attrs_list: &AttrsList,
1358        shaping: Shaping,
1359        tab_width: u16,
1360        direction: Direction,
1361    ) {
1362        // Clear stale ellipsis span so it gets recomputed with the current attrs.
1363        // Without this, reusing a ShapeLine from a previous text (via Cached::Unused)
1364        // would keep an ellipsis shaped with the old attrs.
1365        self.ellipsis_span = None;
1366
1367        let mut spans = mem::take(&mut self.spans);
1368
1369        // Cache the shape spans in reverse order so they can be popped for reuse in the same order.
1370        let mut cached_spans = mem::take(&mut font_system.shape_buffer.spans);
1371        cached_spans.clear();
1372        cached_spans.extend(spans.drain(..).rev());
1373
1374        let bidi = unicode_bidi::BidiInfo::new(line, direction.bidi_level());
1375        let rtl = if bidi.paragraphs.is_empty() {
1376            // No strong content to detect from, go with default base direction if it's set
1377            direction == Direction::RightToLeft
1378        } else {
1379            bidi.paragraphs[0].level.is_rtl()
1380        };
1381
1382        log::trace!("Line {}: '{}'", if rtl { "RTL" } else { "LTR" }, line);
1383
1384        for para_info in &bidi.paragraphs {
1385            let line_rtl = para_info.level.is_rtl();
1386            assert_eq!(line_rtl, rtl);
1387
1388            let line_range = para_info.range.clone();
1389            let levels = Self::adjust_levels(&unicode_bidi::Paragraph::new(&bidi, para_info));
1390
1391            // Find consecutive level runs. We use this to create Spans.
1392            // Each span is a set of characters with equal levels.
1393            let mut start = line_range.start;
1394            let mut run_level = levels[start];
1395            spans.reserve(line_range.end - start + 1);
1396
1397            for (i, &new_level) in levels
1398                .iter()
1399                .enumerate()
1400                .take(line_range.end)
1401                .skip(start + 1)
1402            {
1403                if new_level != run_level {
1404                    // End of the previous run, start of a new one.
1405                    let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
1406                    span.build(
1407                        font_system,
1408                        line,
1409                        attrs_list,
1410                        start..i,
1411                        line_rtl,
1412                        run_level,
1413                        shaping,
1414                    );
1415                    spans.push(span);
1416                    start = i;
1417                    run_level = new_level;
1418                }
1419            }
1420            let mut span = cached_spans.pop().unwrap_or_else(ShapeSpan::empty);
1421            span.build(
1422                font_system,
1423                line,
1424                attrs_list,
1425                start..line_range.end,
1426                line_rtl,
1427                run_level,
1428                shaping,
1429            );
1430            spans.push(span);
1431        }
1432
1433        // Adjust for tabs
1434        let mut x = 0.0;
1435        for span in &mut spans {
1436            for word in &mut span.words {
1437                for glyph in &mut word.glyphs {
1438                    if line.get(glyph.start..glyph.end) == Some("\t") {
1439                        // Tabs are shaped as spaces, so they will always have the x_advance of a space.
1440                        let tab_x_advance = f32::from(tab_width) * glyph.x_advance;
1441                        let tab_stop = (math::floorf(x / tab_x_advance) + 1.0) * tab_x_advance;
1442                        glyph.x_advance = tab_stop - x;
1443                    }
1444                    x += glyph.x_advance;
1445                }
1446            }
1447        }
1448
1449        self.rtl = rtl;
1450        self.spans = spans;
1451        self.metrics_opt = attrs_list.defaults().metrics_opt.map(Into::into);
1452
1453        self.ellipsis_span.get_or_insert_with(|| {
1454            let attrs = if attrs_list.spans.is_empty() {
1455                attrs_list.defaults()
1456            } else {
1457                attrs_list.get_span(0) // TODO: using the attrs from the first span for
1458                                       // ellipsis even if it's at the end. Which for rich text may look weird if the first
1459                                       // span has a different color or size than where ellipsizing is happening
1460            };
1461            let mut glyphs = shape_ellipsis(font_system, &attrs, shaping, rtl);
1462            if rtl {
1463                glyphs.reverse();
1464            }
1465            let word = ShapeWord {
1466                blank: false,
1467                glyphs,
1468            };
1469            // The level here is a placeholder; the actual level used for BiDi reordering
1470            // is set on the VlRange when the ellipsis is inserted during layout.
1471            let level = if rtl {
1472                unicode_bidi::Level::rtl()
1473            } else {
1474                unicode_bidi::Level::ltr()
1475            };
1476            ShapeSpan {
1477                level,
1478                words: vec![word],
1479                decoration_spans: Vec::new(),
1480            }
1481        });
1482
1483        // Return the buffer for later reuse.
1484        font_system.shape_buffer.spans = cached_spans;
1485    }
1486
1487    // A modified version of first part of unicode_bidi::bidi_info::visual_run
1488    fn adjust_levels(para: &unicode_bidi::Paragraph) -> Vec<unicode_bidi::Level> {
1489        use unicode_bidi::BidiClass::{B, BN, FSI, LRE, LRI, LRO, PDF, PDI, RLE, RLI, RLO, S, WS};
1490        let text = para.info.text;
1491        let levels = &para.info.levels;
1492        let original_classes = &para.info.original_classes;
1493
1494        let mut levels = levels.clone();
1495        let line_classes = &original_classes[..];
1496        let line_levels = &mut levels[..];
1497
1498        // Reset some whitespace chars to paragraph level.
1499        // <http://www.unicode.org/reports/tr9/#L1>
1500        let mut reset_from: Option<usize> = Some(0);
1501        let mut reset_to: Option<usize> = None;
1502        for (i, c) in text.char_indices() {
1503            match line_classes[i] {
1504                // Ignored by X9
1505                RLE | LRE | RLO | LRO | PDF | BN => {}
1506                // Segment separator, Paragraph separator
1507                B | S => {
1508                    assert_eq!(reset_to, None);
1509                    reset_to = Some(i + c.len_utf8());
1510                    if reset_from.is_none() {
1511                        reset_from = Some(i);
1512                    }
1513                }
1514                // Whitespace, isolate formatting
1515                WS | FSI | LRI | RLI | PDI => {
1516                    if reset_from.is_none() {
1517                        reset_from = Some(i);
1518                    }
1519                }
1520                _ => {
1521                    reset_from = None;
1522                }
1523            }
1524            if let (Some(from), Some(to)) = (reset_from, reset_to) {
1525                for level in &mut line_levels[from..to] {
1526                    *level = para.para.level;
1527                }
1528                reset_from = None;
1529                reset_to = None;
1530            }
1531        }
1532        if let Some(from) = reset_from {
1533            for level in &mut line_levels[from..] {
1534                *level = para.para.level;
1535            }
1536        }
1537        levels
1538    }
1539
1540    // A modified version of second part of unicode_bidi::bidi_info::visual run
1541    fn reorder(&self, line_range: &[VlRange]) -> Vec<Range<usize>> {
1542        let line: Vec<unicode_bidi::Level> = line_range.iter().map(|range| range.level).collect();
1543        let count = line.len();
1544        if count == 0 {
1545            return Vec::new();
1546        }
1547
1548        // Each VlRange is its own element for L2 reordering.
1549        // Using individual elements (not grouped runs) ensures that reversal
1550        // correctly reorders elements even when consecutive ranges share a level.
1551        let mut elements: Vec<Range<usize>> = (0..count).map(|i| i..i + 1).collect();
1552
1553        let mut min_level = line[0];
1554        let mut max_level = line[0];
1555        for &level in &line[1..] {
1556            min_level = min(min_level, level);
1557            max_level = max(max_level, level);
1558        }
1559
1560        // Re-order the odd runs.
1561        // <http://www.unicode.org/reports/tr9/#L2>
1562
1563        // Stop at the lowest *odd* level.
1564        min_level = min_level.new_lowest_ge_rtl().expect("Level error");
1565
1566        while max_level >= min_level {
1567            // Look for the start of a sequence of consecutive elements at max_level or higher.
1568            let mut seq_start = 0;
1569            while seq_start < count {
1570                if line[elements[seq_start].start] < max_level {
1571                    seq_start += 1;
1572                    continue;
1573                }
1574
1575                // Found the start of a sequence. Now find the end.
1576                let mut seq_end = seq_start + 1;
1577                while seq_end < count {
1578                    if line[elements[seq_end].start] < max_level {
1579                        break;
1580                    }
1581                    seq_end += 1;
1582                }
1583
1584                // Reverse the individual elements within this sequence.
1585                elements[seq_start..seq_end].reverse();
1586
1587                seq_start = seq_end;
1588            }
1589            max_level
1590                .lower(1)
1591                .expect("Lowering embedding level below zero");
1592        }
1593
1594        elements
1595    }
1596
1597    pub fn layout(
1598        &self,
1599        font_size: f32,
1600        width_opt: Option<f32>,
1601        wrap: Wrap,
1602        align: Option<Align>,
1603        match_mono_width: Option<f32>,
1604        hinting: Hinting,
1605    ) -> Vec<LayoutLine> {
1606        let mut lines = Vec::with_capacity(1);
1607        let mut scratch = ShapeBuffer::default();
1608        self.layout_to_buffer(
1609            &mut scratch,
1610            font_size,
1611            width_opt,
1612            wrap,
1613            Ellipsize::None,
1614            align,
1615            &mut lines,
1616            match_mono_width,
1617            hinting,
1618        );
1619        lines
1620    }
1621
1622    fn get_glyph_start_end(
1623        word: &ShapeWord,
1624        start: SpanWordGlyphPos,
1625        span_index: usize,
1626        word_idx: usize,
1627        _direction: LayoutDirection,
1628        congruent: bool,
1629    ) -> (usize, usize) {
1630        if span_index != start.span || word_idx != start.word {
1631            return (0, word.glyphs.len());
1632        }
1633        let (start_glyph_pos, end_glyph_pos) = if congruent {
1634            (start.glyph, word.glyphs.len())
1635        } else {
1636            (0, start.glyph)
1637        };
1638        (start_glyph_pos, end_glyph_pos)
1639    }
1640
1641    fn fit_glyphs(
1642        word: &ShapeWord,
1643        font_size: f32,
1644        start: SpanWordGlyphPos,
1645        span_index: usize,
1646        word_idx: usize,
1647        direction: LayoutDirection,
1648        congruent: bool,
1649        currently_used_width: f32,
1650        total_available_width: f32,
1651        forward: bool,
1652    ) -> (usize, f32) {
1653        let mut glyphs_w = 0.0;
1654        let (start_glyph_pos, end_glyph_pos) =
1655            Self::get_glyph_start_end(word, start, span_index, word_idx, direction, congruent);
1656
1657        if forward {
1658            let mut glyph_end = start_glyph_pos;
1659            for glyph_idx in start_glyph_pos..end_glyph_pos {
1660                let g_w = word.glyphs[glyph_idx].width(font_size);
1661                if currently_used_width + glyphs_w + g_w > total_available_width {
1662                    break;
1663                }
1664                glyphs_w += g_w;
1665                glyph_end = glyph_idx + 1;
1666            }
1667            (glyph_end, glyphs_w)
1668        } else {
1669            let mut glyph_end = word.glyphs.len();
1670            for glyph_idx in (start_glyph_pos..end_glyph_pos).rev() {
1671                let g_w = word.glyphs[glyph_idx].width(font_size);
1672                if currently_used_width + glyphs_w + g_w > total_available_width {
1673                    break;
1674                }
1675                glyphs_w += g_w;
1676                glyph_end = glyph_idx;
1677            }
1678            (glyph_end, glyphs_w)
1679        }
1680    }
1681
1682    #[inline]
1683    fn add_to_visual_line(
1684        &self,
1685        vl: &mut VisualLine,
1686        span_index: usize,
1687        start: WordGlyphPos,
1688        end: WordGlyphPos,
1689        width: f32,
1690        number_of_blanks: u32,
1691    ) {
1692        if end == start {
1693            return;
1694        }
1695
1696        vl.ranges.push(VlRange {
1697            span: span_index,
1698            start,
1699            end,
1700            level: self.spans[span_index].level,
1701        });
1702        vl.w += width;
1703        vl.spaces += number_of_blanks;
1704    }
1705
1706    fn remaining_content_exceeds(
1707        spans: &[ShapeSpan],
1708        font_size: f32,
1709        span_index: usize,
1710        word_idx: usize,
1711        word_count: usize,
1712        starting_word_index: usize,
1713        direction: LayoutDirection,
1714        congruent: bool,
1715        start_span: usize,
1716        span_count: usize,
1717        threshold: f32,
1718    ) -> bool {
1719        let mut acc: f32 = 0.0;
1720
1721        // Remaining words in the current span
1722        let word_range: Range<usize> = match (direction, congruent) {
1723            (LayoutDirection::Forward, true) => word_idx + 1..word_count,
1724            (LayoutDirection::Forward, false) => 0..word_idx,
1725            (LayoutDirection::Backward, true) => starting_word_index..word_idx,
1726            (LayoutDirection::Backward, false) => word_idx + 1..word_count,
1727        };
1728        for wi in word_range {
1729            acc += spans[span_index].words[wi].width(font_size);
1730            if acc > threshold {
1731                return true;
1732            }
1733        }
1734
1735        // Remaining spans
1736        let span_range: Range<usize> = match direction {
1737            LayoutDirection::Forward => span_index + 1..span_count,
1738            LayoutDirection::Backward => start_span..span_index,
1739        };
1740        for si in span_range {
1741            for w in &spans[si].words {
1742                acc += w.width(font_size);
1743                if acc > threshold {
1744                    return true;
1745                }
1746            }
1747        }
1748
1749        false
1750    }
1751
1752    /// This will fit as much as possible in one line
1753    /// If forward is false, it will fit as much as possible from the end of the spans
1754    /// it will stop when it gets to "start".
1755    /// If forward is true, it will start from start and keep going to the end of the spans
1756    #[inline]
1757    fn layout_spans(
1758        &self,
1759        current_visual_line: &mut VisualLine,
1760        font_size: f32,
1761        spans: &[ShapeSpan],
1762        start_opt: Option<SpanWordGlyphPos>,
1763        rtl: bool,
1764        width_opt: Option<f32>,
1765        ellipsize: Ellipsize,
1766        ellipsis_w: f32,
1767        direction: LayoutDirection,
1768    ) {
1769        let check_ellipsizing = matches!(ellipsize, Ellipsize::Start(_) | Ellipsize::End(_))
1770            && width_opt.is_some_and(|w| w.is_finite());
1771
1772        let max_width = width_opt.unwrap_or(f32::INFINITY);
1773        let span_count = spans.len();
1774
1775        let mut total_w: f32 = 0.0;
1776
1777        let start = if let Some(s) = start_opt {
1778            s
1779        } else {
1780            SpanWordGlyphPos::ZERO
1781        };
1782
1783        let span_indices: Vec<usize> = if matches!(direction, LayoutDirection::Forward) {
1784            (start.span..spans.len()).collect()
1785        } else {
1786            (start.span..spans.len()).rev().collect()
1787        };
1788
1789        'outer: for span_index in span_indices {
1790            let mut word_range_width = 0.;
1791            let mut number_of_blanks: u32 = 0;
1792
1793            let span = &spans[span_index];
1794            let word_count = span.words.len();
1795
1796            let starting_word_index = if span_index == start.span {
1797                start.word
1798            } else {
1799                0
1800            };
1801
1802            let congruent = rtl == span.level.is_rtl();
1803            let word_forward: bool = congruent == (direction == LayoutDirection::Forward);
1804
1805            let word_indices: Vec<usize> = match (direction, congruent, start_opt) {
1806                (LayoutDirection::Forward, true, _) => (starting_word_index..word_count).collect(),
1807                (LayoutDirection::Forward, false, Some(start)) => {
1808                    if span_index == start.span {
1809                        (0..start.word).rev().collect()
1810                    } else {
1811                        (0..word_count).rev().collect()
1812                    }
1813                }
1814                (LayoutDirection::Forward, false, None) => (0..word_count).rev().collect(),
1815                (LayoutDirection::Backward, true, _) => {
1816                    ((starting_word_index)..word_count).rev().collect()
1817                }
1818                (LayoutDirection::Backward, false, Some(start)) => {
1819                    if span_index == start.span {
1820                        if start.glyph > 0 {
1821                            (0..(start.word + 1)).collect()
1822                        } else {
1823                            (0..(start.word)).collect()
1824                        }
1825                    } else {
1826                        (0..word_count).collect()
1827                    }
1828                }
1829                (LayoutDirection::Backward, false, None) => (0..span.words.len()).collect(),
1830            };
1831            for word_idx in word_indices {
1832                let word = &span.words[word_idx];
1833                let word_width = if span_index == start.span && word_idx == start.word {
1834                    let (start_glyph_pos, end_glyph_pos) = Self::get_glyph_start_end(
1835                        word, start, span_index, word_idx, direction, congruent,
1836                    );
1837                    let mut w = 0.;
1838                    for glyph_idx in start_glyph_pos..end_glyph_pos {
1839                        w += word.glyphs[glyph_idx].width(font_size);
1840                    }
1841                    w
1842                } else {
1843                    word.width(font_size)
1844                };
1845
1846                let overflowing = {
1847                    // only check this if we're ellipsizing
1848                    check_ellipsizing
1849                        && (
1850                            // if this  word doesn't fit, then we have an overflow
1851                            (total_w + word_range_width + word_width > max_width)
1852                                || (Self::remaining_content_exceeds(
1853                                    spans,
1854                                    font_size,
1855                                    span_index,
1856                                    word_idx,
1857                                    word_count,
1858                                    starting_word_index,
1859                                    direction,
1860                                    congruent,
1861                                    start.span,
1862                                    span_count,
1863                                    ellipsis_w,
1864                                ) && total_w + word_range_width + word_width + ellipsis_w
1865                                    > max_width)
1866                        )
1867                };
1868
1869                if overflowing {
1870                    // overflow detected
1871                    let available = (max_width - ellipsis_w).max(0.0);
1872
1873                    let (glyph_end, glyphs_w) = Self::fit_glyphs(
1874                        word,
1875                        font_size,
1876                        start,
1877                        span_index,
1878                        word_idx,
1879                        direction,
1880                        congruent,
1881                        total_w + word_range_width,
1882                        available,
1883                        word_forward,
1884                    );
1885
1886                    let (start_pos, end_pos) = if word_forward {
1887                        if span_index == start.span {
1888                            if !congruent {
1889                                (WordGlyphPos::ZERO, WordGlyphPos::new(word_idx, glyph_end))
1890                            } else {
1891                                (
1892                                    start.word_glyph_pos(),
1893                                    WordGlyphPos::new(word_idx, glyph_end),
1894                                )
1895                            }
1896                        } else {
1897                            (WordGlyphPos::ZERO, WordGlyphPos::new(word_idx, glyph_end))
1898                        }
1899                    } else {
1900                        // For an incongruent span in the forward direction, the
1901                        // word indices are (0..start.word).rev(). Cap the VlRange
1902                        // end at start.word_glyph_pos() so it doesn't include
1903                        // words beyond start.word that belong to a previous line.
1904                        // For the backward direction (congruent span), the word
1905                        // indices are (start.word..word_count).rev() and
1906                        // span.words.len() is the correct end.
1907                        let range_end = if span_index == start.span && !congruent {
1908                            start.word_glyph_pos()
1909                        } else {
1910                            WordGlyphPos::new(span.words.len(), 0)
1911                        };
1912                        (WordGlyphPos::new(word_idx, glyph_end), range_end)
1913                    };
1914                    self.add_to_visual_line(
1915                        current_visual_line,
1916                        span_index,
1917                        start_pos,
1918                        end_pos,
1919                        word_range_width + glyphs_w,
1920                        number_of_blanks,
1921                    );
1922
1923                    // don't iterate anymore since we overflowed
1924                    current_visual_line.ellipsized = true;
1925                    break 'outer;
1926                }
1927
1928                word_range_width += word_width;
1929                if word.blank {
1930                    number_of_blanks += 1;
1931                }
1932
1933                // Backward-only: if we've reached the starting point, commit and stop.
1934                if matches!(direction, LayoutDirection::Backward)
1935                    && word_idx == start.word
1936                    && span_index == start.span
1937                {
1938                    let (start_pos, end_pos) = if word_forward {
1939                        (WordGlyphPos::ZERO, start.word_glyph_pos())
1940                    } else {
1941                        (
1942                            start.word_glyph_pos(),
1943                            WordGlyphPos::new(span.words.len(), 0),
1944                        )
1945                    };
1946
1947                    self.add_to_visual_line(
1948                        current_visual_line,
1949                        span_index,
1950                        start_pos,
1951                        end_pos,
1952                        word_range_width,
1953                        number_of_blanks,
1954                    );
1955
1956                    break 'outer;
1957                }
1958            }
1959
1960            // if we get to here that means we didn't ellipsize, so either the whole span fits,
1961            // or we don't really care
1962            total_w += word_range_width;
1963            let (start_pos, end_pos) = if congruent {
1964                if span_index == start.span {
1965                    (
1966                        start.word_glyph_pos(),
1967                        WordGlyphPos::new(span.words.len(), 0),
1968                    )
1969                } else {
1970                    (WordGlyphPos::ZERO, WordGlyphPos::new(span.words.len(), 0))
1971                }
1972            } else if span_index == start.span && (start.word, start.glyph) != (0, 0) {
1973                // Continuation of an incongruent (reversed) span after a wrap:
1974                // this visual line holds the logically-leading words [0, start).
1975                (WordGlyphPos::ZERO, start.word_glyph_pos())
1976            } else {
1977                // No continuation offset, so the whole incongruent span belongs to
1978                // this line. Without this, a fully-RTL span under a forced-LTR base
1979                // direction (start == 0) would collapse to an empty range and drop
1980                // all of its glyphs.
1981                (WordGlyphPos::ZERO, WordGlyphPos::new(span.words.len(), 0))
1982            };
1983
1984            self.add_to_visual_line(
1985                current_visual_line,
1986                span_index,
1987                start_pos,
1988                end_pos,
1989                word_range_width,
1990                number_of_blanks,
1991            );
1992        }
1993
1994        if matches!(direction, LayoutDirection::Backward) {
1995            current_visual_line.ranges.reverse();
1996        }
1997    }
1998
1999    fn layout_middle(
2000        &self,
2001        current_visual_line: &mut VisualLine,
2002        font_size: f32,
2003        spans: &[ShapeSpan],
2004        start_opt: Option<SpanWordGlyphPos>,
2005        rtl: bool,
2006        width: f32,
2007        ellipsize: Ellipsize,
2008        ellipsis_w: f32,
2009    ) {
2010        assert!(matches!(ellipsize, Ellipsize::Middle(_)));
2011
2012        // First check if all content fits without any ellipsis.
2013        {
2014            let mut test_line = VisualLine::default();
2015            self.layout_spans(
2016                &mut test_line,
2017                font_size,
2018                spans,
2019                start_opt,
2020                rtl,
2021                Some(width),
2022                Ellipsize::End(EllipsizeHeightLimit::Lines(1)),
2023                ellipsis_w,
2024                LayoutDirection::Forward,
2025            );
2026            if !test_line.ellipsized && test_line.w <= width {
2027                *current_visual_line = test_line;
2028                return;
2029            }
2030        }
2031
2032        let mut starting_line = VisualLine::default();
2033        self.layout_spans(
2034            &mut starting_line,
2035            font_size,
2036            spans,
2037            start_opt,
2038            rtl,
2039            Some(width / 2.0),
2040            Ellipsize::End(EllipsizeHeightLimit::Lines(1)),
2041            0., //pass 0 for ellipsis_w
2042            LayoutDirection::Forward,
2043        );
2044        let forward_pass_overflowed = starting_line.ellipsized;
2045        let end_range_opt = starting_line.ranges.last();
2046        match end_range_opt {
2047            Some(range) if forward_pass_overflowed => {
2048                let congruent = rtl == self.spans[range.span].level.is_rtl();
2049                // create a new range and do the other half
2050                let mut ending_line = VisualLine::default();
2051                let start = if congruent {
2052                    SpanWordGlyphPos {
2053                        span: range.span,
2054                        word: range.end.word,
2055                        glyph: range.end.glyph,
2056                    }
2057                } else {
2058                    SpanWordGlyphPos {
2059                        span: range.span,
2060                        word: range.start.word,
2061                        glyph: range.start.glyph,
2062                    }
2063                };
2064                self.layout_spans(
2065                    &mut ending_line,
2066                    font_size,
2067                    spans,
2068                    Some(start),
2069                    rtl,
2070                    Some((width - starting_line.w - ellipsis_w).max(0.0)),
2071                    Ellipsize::Start(EllipsizeHeightLimit::Lines(1)),
2072                    0., //pass 0 for ellipsis_w
2073                    LayoutDirection::Backward,
2074                );
2075                // Insert the ellipsis VlRange between the two halves.
2076                // Its BiDi level is determined by the adjacent ranges.
2077                let ellipsis_level = self.ellipsis_level_between(
2078                    starting_line.ranges.last(),
2079                    ending_line.ranges.first(),
2080                );
2081                starting_line
2082                    .ranges
2083                    .push(self.ellipsis_vlrange(ellipsis_level));
2084                starting_line.ranges.extend(ending_line.ranges);
2085                current_visual_line.ranges = starting_line.ranges;
2086                current_visual_line.ellipsized = true;
2087                current_visual_line.w = starting_line.w + ending_line.w + ellipsis_w;
2088                current_visual_line.spaces = starting_line.spaces + ending_line.spaces;
2089            }
2090            None if forward_pass_overflowed && width > ellipsis_w => {
2091                // buffer is small enough that the forward pass didn't fit
2092                // only show the ellipsis
2093                current_visual_line
2094                    .ranges
2095                    .push(self.ellipsis_vlrange(if self.rtl {
2096                        unicode_bidi::Level::rtl()
2097                    } else {
2098                        unicode_bidi::Level::ltr()
2099                    }));
2100                current_visual_line.ellipsized = true;
2101                current_visual_line.w = ellipsis_w;
2102                current_visual_line.spaces = 0;
2103            }
2104            _ => {
2105                // everything fit in the forward pass
2106                current_visual_line.ranges = starting_line.ranges;
2107                current_visual_line.w = starting_line.w;
2108                current_visual_line.spaces = starting_line.spaces;
2109                current_visual_line.ellipsized = false;
2110            }
2111        }
2112    }
2113
2114    /// Returns the words for a given span index, handling the ellipsis sentinel.
2115    fn get_span_words(&self, span_index: usize) -> &[ShapeWord] {
2116        if span_index == ELLIPSIS_SPAN {
2117            &self
2118                .ellipsis_span
2119                .as_ref()
2120                .expect("ellipsis_span not set")
2121                .words
2122        } else {
2123            &self.spans[span_index].words
2124        }
2125    }
2126
2127    fn byte_range_of_vlrange(&self, r: &VlRange) -> Option<(usize, usize)> {
2128        debug_assert_ne!(r.span, ELLIPSIS_SPAN);
2129        let words = self.get_span_words(r.span);
2130        let mut min_byte = usize::MAX;
2131        let mut max_byte = 0usize;
2132        let end_word = r.end.word + usize::from(r.end.glyph != 0);
2133        for (i, word) in words.iter().enumerate().take(end_word).skip(r.start.word) {
2134            let included_glyphs = match (i == r.start.word, i == r.end.word) {
2135                (false, false) => &word.glyphs[..],
2136                (true, false) => &word.glyphs[r.start.glyph..],
2137                (false, true) => &word.glyphs[..r.end.glyph],
2138                (true, true) => &word.glyphs[r.start.glyph..r.end.glyph],
2139            };
2140            for glyph in included_glyphs {
2141                min_byte = min_byte.min(glyph.start);
2142                max_byte = max_byte.max(glyph.end);
2143            }
2144        }
2145        if min_byte <= max_byte {
2146            Some((min_byte, max_byte))
2147        } else {
2148            None
2149        }
2150    }
2151
2152    fn compute_elided_byte_range(
2153        &self,
2154        visual_line: &VisualLine,
2155        line_len: usize,
2156    ) -> Option<(usize, usize)> {
2157        if !visual_line.ellipsized {
2158            return None;
2159        }
2160        // Find the position of the ellipsis VlRange
2161        let ellipsis_idx = visual_line
2162            .ranges
2163            .iter()
2164            .position(|r| r.span == ELLIPSIS_SPAN)?;
2165
2166        // Find the byte range of the visible content before the ellipsis
2167        let before_end = (0..ellipsis_idx)
2168            .rev()
2169            .find_map(|i| self.byte_range_of_vlrange(&visual_line.ranges[i]))
2170            .map(|(_, end)| end)
2171            .unwrap_or(0);
2172
2173        // Find the byte range of the visible content after the ellipsis
2174        let after_start = (ellipsis_idx + 1..visual_line.ranges.len())
2175            .find_map(|i| self.byte_range_of_vlrange(&visual_line.ranges[i]))
2176            .map(|(start, _)| start)
2177            .unwrap_or(line_len);
2178
2179        Some((before_end, after_start))
2180    }
2181
2182    /// Returns the maximum byte offset across all glyphs in all non-ellipsis spans.
2183    /// This effectively gives the byte length of the original shaped text.
2184    fn max_byte_offset(&self) -> usize {
2185        self.spans
2186            .iter()
2187            .flat_map(|span| span.words.iter())
2188            .flat_map(|word| word.glyphs.iter())
2189            .map(|g| g.end)
2190            .max()
2191            .unwrap_or(0)
2192    }
2193
2194    /// Returns the width of the ellipsis in the given font size.
2195    fn ellipsis_w(&self, font_size: f32) -> f32 {
2196        self.ellipsis_span
2197            .as_ref()
2198            .map_or(0.0, |s| s.words.iter().map(|w| w.width(font_size)).sum())
2199    }
2200
2201    /// Creates a `VlRange` for the ellipsis with the give`BiDi`Di level.
2202    fn ellipsis_vlrange(&self, level: unicode_bidi::Level) -> VlRange {
2203        VlRange {
2204            span: ELLIPSIS_SPAN,
2205            start: WordGlyphPos::ZERO,
2206            end: WordGlyphPos::new(1, 0),
2207            level,
2208        }
2209    }
2210
2211    /// Determines the appropriate `BiDi` level for the ellipsis based on the
2212    /// adjacent ranges, following UAX#9 N1/N2 rules for neutral characters.
2213    fn ellipsis_level_between(
2214        &self,
2215        before: Option<&VlRange>,
2216        after: Option<&VlRange>,
2217    ) -> unicode_bidi::Level {
2218        match (before, after) {
2219            (Some(a), Some(b)) if a.level == b.level => a.level,
2220            (Some(a), None) => a.level,
2221            (None, Some(b)) => b.level,
2222            _ => {
2223                if self.rtl {
2224                    unicode_bidi::Level::rtl()
2225                } else {
2226                    unicode_bidi::Level::ltr()
2227                }
2228            }
2229        }
2230    }
2231
2232    fn layout_line(
2233        &self,
2234        current_visual_line: &mut VisualLine,
2235        font_size: f32,
2236        spans: &[ShapeSpan],
2237        start_opt: Option<SpanWordGlyphPos>,
2238        rtl: bool,
2239        width_opt: Option<f32>,
2240        ellipsize: Ellipsize,
2241    ) {
2242        let ellipsis_w = self.ellipsis_w(font_size);
2243
2244        match (ellipsize, width_opt) {
2245            (Ellipsize::Start(_), Some(_)) => {
2246                self.layout_spans(
2247                    current_visual_line,
2248                    font_size,
2249                    spans,
2250                    start_opt,
2251                    rtl,
2252                    width_opt,
2253                    ellipsize,
2254                    ellipsis_w,
2255                    LayoutDirection::Backward,
2256                );
2257                // Insert ellipsis at the visual start (index 0, after backward reversal)
2258                if current_visual_line.ellipsized {
2259                    let level =
2260                        self.ellipsis_level_between(None, current_visual_line.ranges.first());
2261                    current_visual_line
2262                        .ranges
2263                        .insert(0, self.ellipsis_vlrange(level));
2264                    current_visual_line.w += ellipsis_w;
2265                }
2266            }
2267            (Ellipsize::Middle(_), Some(width)) => {
2268                self.layout_middle(
2269                    current_visual_line,
2270                    font_size,
2271                    spans,
2272                    start_opt,
2273                    rtl,
2274                    width,
2275                    ellipsize,
2276                    ellipsis_w,
2277                );
2278            }
2279            _ => {
2280                self.layout_spans(
2281                    current_visual_line,
2282                    font_size,
2283                    spans,
2284                    start_opt,
2285                    rtl,
2286                    width_opt,
2287                    ellipsize,
2288                    ellipsis_w,
2289                    LayoutDirection::Forward,
2290                );
2291                // Insert ellipsis at the visual end
2292                if current_visual_line.ellipsized {
2293                    let level =
2294                        self.ellipsis_level_between(current_visual_line.ranges.last(), None);
2295                    current_visual_line
2296                        .ranges
2297                        .push(self.ellipsis_vlrange(level));
2298                    current_visual_line.w += ellipsis_w;
2299                }
2300            }
2301        }
2302
2303        // Compute the byte range of ellipsized text so the ellipsis LayoutGlyph
2304        // can have valid start/end indices into the original line text.
2305        if current_visual_line.ellipsized {
2306            let line_len = self.max_byte_offset();
2307            current_visual_line.elided_byte_range =
2308                self.compute_elided_byte_range(current_visual_line, line_len);
2309        }
2310    }
2311
2312    pub fn layout_to_buffer(
2313        &self,
2314        scratch: &mut ShapeBuffer,
2315        font_size: f32,
2316        width_opt: Option<f32>,
2317        wrap: Wrap,
2318        ellipsize: Ellipsize,
2319        align: Option<Align>,
2320        layout_lines: &mut Vec<LayoutLine>,
2321        match_mono_width: Option<f32>,
2322        hinting: Hinting,
2323    ) {
2324        // For each visual line a list of  (span index,  and range of words in that span)
2325        // Note that a BiDi visual line could have multiple spans or parts of them
2326        // let mut vl_range_of_spans = Vec::with_capacity(1);
2327        let mut visual_lines = mem::take(&mut scratch.visual_lines);
2328        let mut cached_visual_lines = mem::take(&mut scratch.cached_visual_lines);
2329        cached_visual_lines.clear();
2330        cached_visual_lines.extend(visual_lines.drain(..).map(|mut l| {
2331            l.clear();
2332            l
2333        }));
2334
2335        // Cache glyph sets in reverse order so they will ideally be reused in exactly the same lines.
2336        let mut cached_glyph_sets = mem::take(&mut scratch.glyph_sets);
2337        cached_glyph_sets.clear();
2338        cached_glyph_sets.extend(layout_lines.drain(..).rev().map(|mut v| {
2339            v.glyphs.clear();
2340            v.glyphs
2341        }));
2342
2343        // This would keep the maximum number of spans that would fit on a visual line
2344        // If one span is too large, this variable will hold the range of words inside that span
2345        // that fits on a line.
2346        // let mut current_visual_line: Vec<VlRange> = Vec::with_capacity(1);
2347        let mut current_visual_line = cached_visual_lines.pop().unwrap_or_default();
2348
2349        if wrap == Wrap::None {
2350            self.layout_line(
2351                &mut current_visual_line,
2352                font_size,
2353                &self.spans,
2354                None,
2355                self.rtl,
2356                width_opt,
2357                ellipsize,
2358            );
2359        } else {
2360            let mut total_line_height = 0.0;
2361            let mut total_line_count = 0;
2362            let max_line_count_opt = match ellipsize {
2363                Ellipsize::Start(EllipsizeHeightLimit::Lines(lines))
2364                | Ellipsize::Middle(EllipsizeHeightLimit::Lines(lines))
2365                | Ellipsize::End(EllipsizeHeightLimit::Lines(lines)) => Some(lines.max(1)),
2366                _ => None,
2367            };
2368            let max_height_opt = match ellipsize {
2369                Ellipsize::Start(EllipsizeHeightLimit::Height(height))
2370                | Ellipsize::Middle(EllipsizeHeightLimit::Height(height))
2371                | Ellipsize::End(EllipsizeHeightLimit::Height(height)) => Some(height),
2372                _ => None,
2373            };
2374            let line_height = self
2375                .metrics_opt
2376                .map_or_else(|| font_size, |m| m.line_height);
2377
2378            let try_ellipsize_last_line = |total_line_count: usize,
2379                                           total_line_height: f32,
2380                                           current_visual_line: &mut VisualLine,
2381                                           font_size: f32,
2382                                           start_opt: Option<SpanWordGlyphPos>,
2383                                           width_opt: Option<f32>,
2384                                           ellipsize: Ellipsize|
2385             -> bool {
2386                // If Ellipsize::End, then how many lines can we fit or how much is the available height
2387                if max_line_count_opt == Some(total_line_count + 1)
2388                    || max_height_opt.is_some_and(|max_height| {
2389                        total_line_height + line_height * 2.0 > max_height
2390                    })
2391                {
2392                    self.layout_line(
2393                        current_visual_line,
2394                        font_size,
2395                        &self.spans,
2396                        start_opt,
2397                        self.rtl,
2398                        width_opt,
2399                        ellipsize,
2400                    );
2401                    return true;
2402                }
2403                false
2404            };
2405
2406            if !try_ellipsize_last_line(
2407                total_line_count,
2408                total_line_height,
2409                &mut current_visual_line,
2410                font_size,
2411                None,
2412                width_opt,
2413                ellipsize,
2414            ) {
2415                'outer: for (span_index, span) in self.spans.iter().enumerate() {
2416                    let mut word_range_width = 0.;
2417                    let mut width_before_last_blank = 0.;
2418                    let mut number_of_blanks: u32 = 0;
2419
2420                    // Create the word ranges that fits in a visual line
2421                    if self.rtl != span.level.is_rtl() {
2422                        // incongruent directions
2423                        let mut fitting_start = WordGlyphPos::new(span.words.len(), 0);
2424                        for (i, word) in span.words.iter().enumerate().rev() {
2425                            let word_width = word.width(font_size);
2426                            // Addition in the same order used to compute the final width, so that
2427                            // relayouts with that width as the `line_width` will produce the same
2428                            // wrapping results.
2429                            if current_visual_line.w + (word_range_width + word_width)
2430                            <= width_opt.unwrap_or(f32::INFINITY)
2431                            // Include one blank word over the width limit since it won't be
2432                            // counted in the final width
2433                            || (word.blank
2434                                && (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
2435                            {
2436                                // fits
2437                                if word.blank {
2438                                    number_of_blanks += 1;
2439                                    width_before_last_blank = word_range_width;
2440                                }
2441                                word_range_width += word_width;
2442                            } else if wrap == Wrap::Glyph
2443                            // Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
2444                            || (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
2445                            {
2446                                // Commit the current line so that the word starts on the next line.
2447                                if word_range_width > 0.
2448                                    && wrap == Wrap::WordOrGlyph
2449                                    && word_width > width_opt.unwrap_or(f32::INFINITY)
2450                                {
2451                                    self.add_to_visual_line(
2452                                        &mut current_visual_line,
2453                                        span_index,
2454                                        WordGlyphPos::new(i + 1, 0),
2455                                        fitting_start,
2456                                        word_range_width,
2457                                        number_of_blanks,
2458                                    );
2459
2460                                    visual_lines.push(current_visual_line);
2461                                    current_visual_line =
2462                                        cached_visual_lines.pop().unwrap_or_default();
2463
2464                                    number_of_blanks = 0;
2465                                    word_range_width = 0.;
2466
2467                                    fitting_start = WordGlyphPos::new(i, 0);
2468                                    total_line_count += 1;
2469                                    total_line_height += line_height;
2470                                    if try_ellipsize_last_line(
2471                                        total_line_count,
2472                                        total_line_height,
2473                                        &mut current_visual_line,
2474                                        font_size,
2475                                        Some(SpanWordGlyphPos::with_wordglyph(
2476                                            span_index,
2477                                            fitting_start,
2478                                        )),
2479                                        width_opt,
2480                                        ellipsize,
2481                                    ) {
2482                                        break 'outer;
2483                                    }
2484                                }
2485
2486                                for (glyph_i, glyph) in word.glyphs.iter().enumerate().rev() {
2487                                    let glyph_width = glyph.width(font_size);
2488                                    if current_visual_line.w + (word_range_width + glyph_width)
2489                                        <= width_opt.unwrap_or(f32::INFINITY)
2490                                    {
2491                                        word_range_width += glyph_width;
2492                                    } else {
2493                                        self.add_to_visual_line(
2494                                            &mut current_visual_line,
2495                                            span_index,
2496                                            WordGlyphPos::new(i, glyph_i + 1),
2497                                            fitting_start,
2498                                            word_range_width,
2499                                            number_of_blanks,
2500                                        );
2501                                        visual_lines.push(current_visual_line);
2502                                        current_visual_line =
2503                                            cached_visual_lines.pop().unwrap_or_default();
2504
2505                                        number_of_blanks = 0;
2506                                        word_range_width = glyph_width;
2507                                        fitting_start = WordGlyphPos::new(i, glyph_i + 1);
2508                                        total_line_count += 1;
2509                                        total_line_height += line_height;
2510                                        if try_ellipsize_last_line(
2511                                            total_line_count,
2512                                            total_line_height,
2513                                            &mut current_visual_line,
2514                                            font_size,
2515                                            Some(SpanWordGlyphPos::with_wordglyph(
2516                                                span_index,
2517                                                fitting_start,
2518                                            )),
2519                                            width_opt,
2520                                            ellipsize,
2521                                        ) {
2522                                            break 'outer;
2523                                        }
2524                                    }
2525                                }
2526                            } else {
2527                                // Wrap::Word, Wrap::WordOrGlyph
2528
2529                                // If we had a previous range, commit that line before the next word.
2530                                if word_range_width > 0. {
2531                                    // Current word causing a wrap is not whitespace, so we ignore the
2532                                    // previous word if it's a whitespace
2533                                    let trailing_blank = span
2534                                        .words
2535                                        .get(i + 1)
2536                                        .is_some_and(|previous_word| previous_word.blank);
2537
2538                                    if trailing_blank {
2539                                        number_of_blanks = number_of_blanks.saturating_sub(1);
2540                                        self.add_to_visual_line(
2541                                            &mut current_visual_line,
2542                                            span_index,
2543                                            WordGlyphPos::new(i + 2, 0),
2544                                            fitting_start,
2545                                            width_before_last_blank,
2546                                            number_of_blanks,
2547                                        );
2548                                    } else {
2549                                        self.add_to_visual_line(
2550                                            &mut current_visual_line,
2551                                            span_index,
2552                                            WordGlyphPos::new(i + 1, 0),
2553                                            fitting_start,
2554                                            word_range_width,
2555                                            number_of_blanks,
2556                                        );
2557                                    }
2558                                }
2559
2560                                // This fixes a bug that a long first word at the boundary of
2561                                // was overflowing
2562                                if !current_visual_line.ranges.is_empty() {
2563                                    visual_lines.push(current_visual_line);
2564                                    current_visual_line =
2565                                        cached_visual_lines.pop().unwrap_or_default();
2566                                    number_of_blanks = 0;
2567                                    total_line_count += 1;
2568                                    total_line_height += line_height;
2569
2570                                    if try_ellipsize_last_line(
2571                                        total_line_count,
2572                                        total_line_height,
2573                                        &mut current_visual_line,
2574                                        font_size,
2575                                        Some(SpanWordGlyphPos::with_wordglyph(
2576                                            span_index,
2577                                            if word.blank {
2578                                                WordGlyphPos::new(i, 0)
2579                                            } else {
2580                                                WordGlyphPos::new(i + 1, 0)
2581                                            },
2582                                        )),
2583                                        width_opt,
2584                                        ellipsize,
2585                                    ) {
2586                                        break 'outer;
2587                                    }
2588                                }
2589
2590                                if word.blank {
2591                                    word_range_width = 0.;
2592                                    fitting_start = WordGlyphPos::new(i, 0);
2593                                } else {
2594                                    word_range_width = word_width;
2595                                    fitting_start = WordGlyphPos::new(i + 1, 0);
2596                                }
2597                            }
2598                        }
2599                        self.add_to_visual_line(
2600                            &mut current_visual_line,
2601                            span_index,
2602                            WordGlyphPos::new(0, 0),
2603                            fitting_start,
2604                            word_range_width,
2605                            number_of_blanks,
2606                        );
2607                    } else {
2608                        // congruent direction
2609                        let mut fitting_start = WordGlyphPos::ZERO;
2610                        for (i, word) in span.words.iter().enumerate() {
2611                            let word_width = word.width(font_size);
2612                            if current_visual_line.w + (word_range_width + word_width)
2613                            <= width_opt.unwrap_or(f32::INFINITY)
2614                            // Include one blank word over the width limit since it won't be
2615                            // counted in the final width.
2616                            || (word.blank
2617                                && (current_visual_line.w + word_range_width) <= width_opt.unwrap_or(f32::INFINITY))
2618                            {
2619                                // fits
2620                                if word.blank {
2621                                    number_of_blanks += 1;
2622                                    width_before_last_blank = word_range_width;
2623                                }
2624                                word_range_width += word_width;
2625                            } else if wrap == Wrap::Glyph
2626                            // Make sure that the word is able to fit on it's own line, if not, fall back to Glyph wrapping.
2627                            || (wrap == Wrap::WordOrGlyph && word_width > width_opt.unwrap_or(f32::INFINITY))
2628                            {
2629                                // Commit the current line so that the word starts on the next line.
2630                                if word_range_width > 0.
2631                                    && wrap == Wrap::WordOrGlyph
2632                                    && word_width > width_opt.unwrap_or(f32::INFINITY)
2633                                {
2634                                    self.add_to_visual_line(
2635                                        &mut current_visual_line,
2636                                        span_index,
2637                                        fitting_start,
2638                                        WordGlyphPos::new(i, 0),
2639                                        word_range_width,
2640                                        number_of_blanks,
2641                                    );
2642
2643                                    visual_lines.push(current_visual_line);
2644                                    current_visual_line =
2645                                        cached_visual_lines.pop().unwrap_or_default();
2646
2647                                    number_of_blanks = 0;
2648                                    word_range_width = 0.;
2649
2650                                    fitting_start = WordGlyphPos::new(i, 0);
2651                                    total_line_count += 1;
2652                                    total_line_height += line_height;
2653                                    if try_ellipsize_last_line(
2654                                        total_line_count,
2655                                        total_line_height,
2656                                        &mut current_visual_line,
2657                                        font_size,
2658                                        Some(SpanWordGlyphPos::with_wordglyph(
2659                                            span_index,
2660                                            fitting_start,
2661                                        )),
2662                                        width_opt,
2663                                        ellipsize,
2664                                    ) {
2665                                        break 'outer;
2666                                    }
2667                                }
2668
2669                                for (glyph_i, glyph) in word.glyphs.iter().enumerate() {
2670                                    let glyph_width = glyph.width(font_size);
2671                                    if current_visual_line.w + (word_range_width + glyph_width)
2672                                        <= width_opt.unwrap_or(f32::INFINITY)
2673                                    {
2674                                        word_range_width += glyph_width;
2675                                    } else {
2676                                        self.add_to_visual_line(
2677                                            &mut current_visual_line,
2678                                            span_index,
2679                                            fitting_start,
2680                                            WordGlyphPos::new(i, glyph_i),
2681                                            word_range_width,
2682                                            number_of_blanks,
2683                                        );
2684                                        visual_lines.push(current_visual_line);
2685                                        current_visual_line =
2686                                            cached_visual_lines.pop().unwrap_or_default();
2687
2688                                        number_of_blanks = 0;
2689                                        word_range_width = glyph_width;
2690                                        fitting_start = WordGlyphPos::new(i, glyph_i);
2691                                        total_line_count += 1;
2692                                        total_line_height += line_height;
2693                                        if try_ellipsize_last_line(
2694                                            total_line_count,
2695                                            total_line_height,
2696                                            &mut current_visual_line,
2697                                            font_size,
2698                                            Some(SpanWordGlyphPos::with_wordglyph(
2699                                                span_index,
2700                                                fitting_start,
2701                                            )),
2702                                            width_opt,
2703                                            ellipsize,
2704                                        ) {
2705                                            break 'outer;
2706                                        }
2707                                    }
2708                                }
2709                            } else {
2710                                // Wrap::Word, Wrap::WordOrGlyph
2711
2712                                // If we had a previous range, commit that line before the next word.
2713                                if word_range_width > 0. {
2714                                    // Current word causing a wrap is not whitespace, so we ignore the
2715                                    // previous word if it's a whitespace.
2716                                    let trailing_blank = i > 0 && span.words[i - 1].blank;
2717
2718                                    if trailing_blank {
2719                                        number_of_blanks = number_of_blanks.saturating_sub(1);
2720                                        self.add_to_visual_line(
2721                                            &mut current_visual_line,
2722                                            span_index,
2723                                            fitting_start,
2724                                            WordGlyphPos::new(i - 1, 0),
2725                                            width_before_last_blank,
2726                                            number_of_blanks,
2727                                        );
2728                                    } else {
2729                                        self.add_to_visual_line(
2730                                            &mut current_visual_line,
2731                                            span_index,
2732                                            fitting_start,
2733                                            WordGlyphPos::new(i, 0),
2734                                            word_range_width,
2735                                            number_of_blanks,
2736                                        );
2737                                    }
2738                                }
2739
2740                                if !current_visual_line.ranges.is_empty() {
2741                                    visual_lines.push(current_visual_line);
2742                                    current_visual_line =
2743                                        cached_visual_lines.pop().unwrap_or_default();
2744                                    number_of_blanks = 0;
2745                                    total_line_count += 1;
2746                                    total_line_height += line_height;
2747                                    if try_ellipsize_last_line(
2748                                        total_line_count,
2749                                        total_line_height,
2750                                        &mut current_visual_line,
2751                                        font_size,
2752                                        Some(SpanWordGlyphPos::with_wordglyph(
2753                                            span_index,
2754                                            if i > 0 && span.words[i - 1].blank {
2755                                                WordGlyphPos::new(i - 1, 0)
2756                                            } else {
2757                                                WordGlyphPos::new(i, 0)
2758                                            },
2759                                        )),
2760                                        width_opt,
2761                                        ellipsize,
2762                                    ) {
2763                                        break 'outer;
2764                                    }
2765                                }
2766
2767                                if word.blank {
2768                                    word_range_width = 0.;
2769                                    fitting_start = WordGlyphPos::new(i + 1, 0);
2770                                } else {
2771                                    word_range_width = word_width;
2772                                    fitting_start = WordGlyphPos::new(i, 0);
2773                                }
2774                            }
2775                        }
2776                        self.add_to_visual_line(
2777                            &mut current_visual_line,
2778                            span_index,
2779                            fitting_start,
2780                            WordGlyphPos::new(span.words.len(), 0),
2781                            word_range_width,
2782                            number_of_blanks,
2783                        );
2784                    }
2785                }
2786            }
2787        }
2788
2789        if current_visual_line.ranges.is_empty() {
2790            current_visual_line.clear();
2791            cached_visual_lines.push(current_visual_line);
2792        } else {
2793            visual_lines.push(current_visual_line);
2794        }
2795
2796        // Create the LayoutLines using the ranges inside visual lines
2797        let align = align.unwrap_or(if self.rtl { Align::Right } else { Align::Left });
2798
2799        let line_width = width_opt.unwrap_or_else(|| {
2800            let mut width: f32 = 0.0;
2801            for visual_line in &visual_lines {
2802                width = width.max(visual_line.w);
2803            }
2804            width
2805        });
2806
2807        let start_x = if self.rtl { line_width } else { 0.0 };
2808
2809        let number_of_visual_lines = visual_lines.len();
2810        for (index, visual_line) in visual_lines.iter().enumerate() {
2811            if visual_line.ranges.is_empty() {
2812                continue;
2813            }
2814
2815            let new_order = self.reorder(&visual_line.ranges);
2816
2817            let mut glyphs = cached_glyph_sets
2818                .pop()
2819                .unwrap_or_else(|| Vec::with_capacity(1));
2820            let mut x = start_x;
2821            let mut y = 0.;
2822            let mut max_ascent: f32 = 0.;
2823            let mut max_descent: f32 = 0.;
2824            let alignment_correction = match (align, self.rtl) {
2825                (Align::Left, true) => (line_width - visual_line.w).max(0.),
2826                (Align::Left, false) => 0.,
2827                (Align::Right, true) => 0.,
2828                (Align::Right, false) => (line_width - visual_line.w).max(0.),
2829                (Align::Center, _) => (line_width - visual_line.w).max(0.) / 2.0,
2830                (Align::End, _) => (line_width - visual_line.w).max(0.),
2831                (Align::Justified, _) => 0.,
2832            };
2833
2834            if self.rtl {
2835                x -= alignment_correction;
2836            } else {
2837                x += alignment_correction;
2838            }
2839
2840            if hinting == Hinting::Enabled {
2841                x = x.round();
2842            }
2843
2844            // TODO: Only certain `is_whitespace` chars are typically expanded but this is what is
2845            // currently used to compute `visual_line.spaces`.
2846            //
2847            // https://www.unicode.org/reports/tr14/#Introduction
2848            // > When expanding or compressing interword space according to common
2849            // > typographical practice, only the spaces marked by U+0020 SPACE and U+00A0
2850            // > NO-BREAK SPACE are subject to compression, and only spaces marked by U+0020
2851            // > SPACE, U+00A0 NO-BREAK SPACE, and occasionally spaces marked by U+2009 THIN
2852            // > SPACE are subject to expansion. All other space characters normally have
2853            // > fixed width.
2854            //
2855            // (also some spaces aren't followed by potential linebreaks but they could
2856            //  still be expanded)
2857
2858            // Amount of extra width added to each blank space within a line.
2859            let justification_expansion = if matches!(align, Align::Justified)
2860                && visual_line.spaces > 0
2861                // Don't justify the last line in a paragraph.
2862                && index != number_of_visual_lines - 1
2863            {
2864                (line_width - visual_line.w) / visual_line.spaces as f32
2865            } else {
2866                0.
2867            };
2868
2869            let elided_byte_range = if visual_line.ellipsized {
2870                visual_line.elided_byte_range
2871            } else {
2872                None
2873            };
2874
2875            let mut decorations: Vec<DecorationSpan> = Vec::new();
2876
2877            let process_range = |range: Range<usize>,
2878                                 x: &mut f32,
2879                                 y: &mut f32,
2880                                 glyphs: &mut Vec<LayoutGlyph>,
2881                                 decorations: &mut Vec<DecorationSpan>,
2882                                 max_ascent: &mut f32,
2883                                 max_descent: &mut f32| {
2884                for r in visual_line.ranges[range.clone()].iter() {
2885                    let is_ellipsis = r.span == ELLIPSIS_SPAN;
2886                    let span_words = self.get_span_words(r.span);
2887                    let deco_spans: &[(Range<usize>, GlyphDecorationData)] = if is_ellipsis {
2888                        &[]
2889                    } else {
2890                        &self.spans[r.span].decoration_spans
2891                    };
2892                    // Cursor into deco_spans — advances forward as glyphs are
2893                    // emitted in byte order, giving amortized O(1) lookup.
2894                    let mut deco_cursor: usize = 0;
2895                    // If ending_glyph is not 0 we need to include glyphs from the ending_word
2896                    for i in r.start.word..r.end.word + usize::from(r.end.glyph != 0) {
2897                        let word = &span_words[i];
2898                        let included_glyphs = match (i == r.start.word, i == r.end.word) {
2899                            (false, false) => &word.glyphs[..],
2900                            (true, false) => &word.glyphs[r.start.glyph..],
2901                            (false, true) => &word.glyphs[..r.end.glyph],
2902                            (true, true) => &word.glyphs[r.start.glyph..r.end.glyph],
2903                        };
2904
2905                        for glyph in included_glyphs {
2906                            // Use overridden font size
2907                            let font_size = glyph.metrics_opt.map_or(font_size, |x| x.font_size);
2908
2909                            let match_mono_em_width = match_mono_width.map(|w| w / font_size);
2910
2911                            let glyph_font_size = match (
2912                                match_mono_em_width,
2913                                glyph.font_monospace_em_width,
2914                            ) {
2915                                (Some(match_em_width), Some(glyph_em_width))
2916                                    if glyph_em_width != match_em_width =>
2917                                {
2918                                    let glyph_to_match_factor = glyph_em_width / match_em_width;
2919                                    let glyph_font_size = math::roundf(glyph_to_match_factor)
2920                                        .max(1.0)
2921                                        / glyph_to_match_factor
2922                                        * font_size;
2923                                    log::trace!(
2924                                        "Adjusted glyph font size ({font_size} => {glyph_font_size})"
2925                                    );
2926                                    glyph_font_size
2927                                }
2928                                _ => font_size,
2929                            };
2930
2931                            let mut x_advance = glyph_font_size.mul_add(
2932                                glyph.x_advance,
2933                                if word.blank {
2934                                    justification_expansion
2935                                } else {
2936                                    0.0
2937                                },
2938                            );
2939                            if let Some(match_em_width) = match_mono_em_width {
2940                                // Round to nearest monospace width
2941                                x_advance = ((x_advance / match_em_width).round()) * match_em_width;
2942                            }
2943                            if hinting == Hinting::Enabled {
2944                                x_advance = x_advance.round();
2945                            }
2946                            if self.rtl {
2947                                *x -= x_advance;
2948                            }
2949                            let y_advance = glyph_font_size * glyph.y_advance;
2950                            let mut layout_glyph = glyph.layout(
2951                                glyph_font_size,
2952                                glyph.metrics_opt.map(|x| x.line_height),
2953                                *x,
2954                                *y,
2955                                x_advance,
2956                                r.level,
2957                            );
2958                            // Fix ellipsis glyph indices: point both start and
2959                            // end to the elision boundary so that hit-detection
2960                            // places the cursor at the seam between visible and
2961                            // elided text instead of selecting invisible content.
2962                            if is_ellipsis {
2963                                if let Some((elided_start, elided_end)) = elided_byte_range {
2964                                    // Use the boundary closest to the visible
2965                                    // content that is adjacent to this ellipsis:
2966                                    //   Start:  …|visible  → boundary = elided_end
2967                                    //   End:    visible|…  → boundary = elided_start
2968                                    //   Middle: vis|…|vis  → boundary = elided_start
2969                                    let boundary = if elided_start == 0 {
2970                                        elided_end
2971                                    } else {
2972                                        elided_start
2973                                    };
2974                                    layout_glyph.start = boundary;
2975                                    layout_glyph.end = boundary;
2976                                }
2977                            }
2978                            glyphs.push(layout_glyph);
2979
2980                            if deco_cursor >= deco_spans.len()
2981                                || glyph.start < deco_spans[deco_cursor].0.start
2982                            {
2983                                deco_cursor = 0;
2984                            }
2985                            while deco_cursor < deco_spans.len()
2986                                && deco_spans[deco_cursor].0.end <= glyph.start
2987                            {
2988                                deco_cursor += 1;
2989                            }
2990                            let glyph_deco = deco_spans
2991                                .get(deco_cursor)
2992                                .filter(|(range, _)| glyph.start >= range.start);
2993                            let glyph_idx = glyphs.len() - 1;
2994                            let extends = matches!(
2995                                (decorations.last(), &glyph_deco),
2996                                (Some(span), Some((_, d))) if span.data == *d
2997                            );
2998                            if extends {
2999                                if let Some(last) = decorations.last_mut() {
3000                                    last.glyph_range.end = glyph_idx + 1;
3001                                }
3002                            } else if let Some((_, d)) = glyph_deco {
3003                                decorations.push(DecorationSpan {
3004                                    glyph_range: glyph_idx..glyph_idx + 1,
3005                                    data: d.clone(),
3006                                    color_opt: glyphs[glyph_idx].color_opt,
3007                                    font_size: glyphs[glyph_idx].font_size,
3008                                });
3009                            }
3010                            if !self.rtl {
3011                                *x += x_advance;
3012                            }
3013                            *y += y_advance;
3014                            *max_ascent = max_ascent.max(glyph_font_size * glyph.ascent);
3015                            *max_descent = max_descent.max(glyph_font_size * glyph.descent);
3016                        }
3017                    }
3018                }
3019            };
3020
3021            if self.rtl {
3022                for range in new_order.into_iter().rev() {
3023                    process_range(
3024                        range,
3025                        &mut x,
3026                        &mut y,
3027                        &mut glyphs,
3028                        &mut decorations,
3029                        &mut max_ascent,
3030                        &mut max_descent,
3031                    );
3032                }
3033            } else {
3034                /* LTR */
3035                for range in new_order {
3036                    process_range(
3037                        range,
3038                        &mut x,
3039                        &mut y,
3040                        &mut glyphs,
3041                        &mut decorations,
3042                        &mut max_ascent,
3043                        &mut max_descent,
3044                    );
3045                }
3046            }
3047
3048            let mut line_height_opt: Option<f32> = None;
3049            for glyph in &glyphs {
3050                if let Some(glyph_line_height) = glyph.line_height_opt {
3051                    line_height_opt = line_height_opt
3052                        .map_or(Some(glyph_line_height), |line_height| {
3053                            Some(line_height.max(glyph_line_height))
3054                        });
3055                }
3056            }
3057
3058            layout_lines.push(LayoutLine {
3059                w: if align != Align::Justified {
3060                    visual_line.w
3061                } else if self.rtl {
3062                    start_x - x
3063                } else {
3064                    x
3065                },
3066                max_ascent,
3067                max_descent,
3068                line_height_opt,
3069                glyphs,
3070                decorations,
3071            });
3072        }
3073
3074        // This is used to create a visual line for empty lines (e.g. lines with only a <CR>)
3075        if layout_lines.is_empty() {
3076            layout_lines.push(LayoutLine {
3077                w: 0.0,
3078                max_ascent: 0.0,
3079                max_descent: 0.0,
3080                line_height_opt: self.metrics_opt.map(|x| x.line_height),
3081                glyphs: Vec::default(),
3082                decorations: Vec::new(),
3083            });
3084        }
3085
3086        // Restore the buffer to the scratch set to prevent reallocations.
3087        scratch.visual_lines = visual_lines;
3088        scratch.visual_lines.append(&mut cached_visual_lines);
3089        scratch.cached_visual_lines = cached_visual_lines;
3090        scratch.glyph_sets = cached_glyph_sets;
3091    }
3092}