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