Skip to main content

sequoia_text/
lib.rs

1#![forbid(unsafe_code)]
2
3mod rich;
4
5pub use rich::{
6    RichAlign, RichCaret, RichHit, RichLine, RichParagraph, RichRect, Squiggle, TextSpan,
7    styled_spans_to_text_spans,
8};
9
10use std::cell::{Cell, RefCell};
11use std::collections::{HashMap, VecDeque};
12use std::hash::{Hash, Hasher};
13use std::rc::Rc;
14use std::sync::atomic::{AtomicU64, Ordering};
15use std::sync::{Arc, Mutex};
16
17use cosmic_text::{
18    Attrs, Buffer, CacheKey, Family, FontSystem, Metrics, Shaping, Style as CosmicStyle,
19    SwashCache, Weight,
20};
21use sequoia_buffer::{LineIndex, StyledBuffer, StyledSpan, hash_spans_rebased, rebase_spans};
22use sequoia_core::{Color, GlyphQuad, Rect, RenderList, TextDraw};
23
24#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
25pub struct FontWeight(u16);
26
27impl FontWeight {
28    pub const THIN: Self = Self(100);
29
30    pub const EXTRA_LIGHT: Self = Self(200);
31
32    pub const LIGHT: Self = Self(300);
33
34    pub const NORMAL: Self = Self(400);
35
36    pub const MEDIUM: Self = Self(500);
37
38    pub const SEMIBOLD: Self = Self(600);
39
40    pub const BOLD: Self = Self(700);
41
42    pub const EXTRA_BOLD: Self = Self(800);
43
44    pub const BLACK: Self = Self(900);
45
46    pub const fn new(value: u16) -> Self {
47        Self(if value < 1 {
48            1
49        } else if value > 1000 {
50            1000
51        } else {
52            value
53        })
54    }
55
56    pub const fn value(self) -> u16 {
57        self.0
58    }
59}
60
61impl Default for FontWeight {
62    fn default() -> Self {
63        Self::NORMAL
64    }
65}
66
67#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
68#[non_exhaustive]
69pub enum FontStyle {
70    #[default]
71    Normal,
72    Italic,
73    Oblique,
74}
75
76impl FontStyle {
77    pub const fn is_italic(self) -> bool {
78        !matches!(self, Self::Normal)
79    }
80
81    pub(crate) fn cosmic(self) -> CosmicStyle {
82        match self {
83            Self::Normal => CosmicStyle::Normal,
84            Self::Italic => CosmicStyle::Italic,
85            Self::Oblique => CosmicStyle::Oblique,
86        }
87    }
88
89    pub(crate) fn key(self) -> u8 {
90        match self {
91            Self::Normal => 0,
92            Self::Italic => 1,
93            Self::Oblique => 2,
94        }
95    }
96}
97
98#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
99#[non_exhaustive]
100pub enum WrapMode {
101    #[default]
102    Word,
103
104    Character,
105}
106
107impl WrapMode {
108    pub(crate) fn cosmic(self) -> cosmic_text::Wrap {
109        match self {
110            Self::Word => cosmic_text::Wrap::Word,
111            Self::Character => cosmic_text::Wrap::Glyph,
112        }
113    }
114
115    pub(crate) fn key(self) -> u8 {
116        match self {
117            Self::Word => 0,
118            Self::Character => 1,
119        }
120    }
121}
122
123#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
124#[non_exhaustive]
125pub enum ElideMode {
126    #[default]
127    Right,
128
129    Left,
130
131    Middle,
132}
133
134pub const ELLIPSIS: char = '…';
135
136/// How a line box height is derived from the font size.
137#[derive(Clone, Copy, Debug, PartialEq)]
138pub enum LineHeight {
139    /// A multiplier of the font size (CSS-like unitless line height).
140    Factor(f32),
141    /// An absolute logical-pixel height.
142    Absolute(f32),
143}
144
145impl Default for LineHeight {
146    fn default() -> Self {
147        Self::Factor(1.35)
148    }
149}
150
151impl LineHeight {
152    /// Resolves the line box height for a font size.
153    pub fn resolve(self, size: f32) -> f32 {
154        let size = if size.is_finite() { size.max(0.0) } else { 0.0 };
155        let px = match self {
156            Self::Factor(factor) => size * factor,
157            Self::Absolute(px) => px,
158        };
159        if px.is_finite() {
160            px.max(0.0)
161        } else {
162            size * 1.35
163        }
164    }
165
166    pub(crate) fn key(self) -> u32 {
167        match self {
168            Self::Factor(factor) => ((factor.max(0.0) * 4.0).round() as u32).max(1),
169            Self::Absolute(px) => (px.max(0.0) * 4.0).round() as u32,
170        }
171    }
172}
173
174/// Vertical placement of the text block inside its content box.
175#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
176#[non_exhaustive]
177pub enum TextVAlign {
178    #[default]
179    Top,
180    Center,
181    Bottom,
182    /// Aligns the first text baseline with the top of the content box.
183    Baseline,
184}
185
186pub struct ShapedText {
187    glyphs: Vec<ShapedGlyph>,
188    rows: Vec<LineRow>,
189    width: f32,
190    height: f32,
191    line_height: f32,
192}
193
194impl ShapedText {
195    pub fn width(&self) -> f32 {
196        self.width
197    }
198
199    pub fn height(&self) -> f32 {
200        self.height
201    }
202
203    pub fn glyph_count(&self) -> usize {
204        self.glyphs.len()
205    }
206
207    /// Number of laid-out lines. Empty text has zero lines.
208    pub fn line_count(&self) -> usize {
209        self.rows.len()
210    }
211
212    /// Distance from the top of the first line box to its baseline.
213    pub fn first_baseline(&self) -> f32 {
214        self.rows.first().map(|row| row.baseline).unwrap_or(0.0)
215    }
216
217    /// Resolved line box height (the first row, or the configured height for empty text).
218    pub fn line_height(&self) -> f32 {
219        self.line_height
220    }
221
222    /// Per-line metrics relative to the top of the text block.
223    pub fn rows(&self) -> &[LineRow] {
224        &self.rows
225    }
226
227    pub fn row(&self, index: usize) -> Option<&LineRow> {
228        self.rows.get(index)
229    }
230}
231
232struct ShapedGlyph {
233    x: f32,
234    y: f32,
235    key: CacheKey,
236}
237
238#[derive(Clone, Copy, Debug, PartialEq)]
239pub struct LineRow {
240    pub top: f32,
241
242    pub height: f32,
243
244    pub baseline: f32,
245
246    pub width: f32,
247}
248
249impl LineRow {
250    pub fn bottom(&self) -> f32 {
251        self.top + self.height
252    }
253}
254
255pub struct ShapedLine {
256    glyphs: Vec<ShapedGlyph>,
257    rows: Vec<LineRow>,
258    width: f32,
259    height: f32,
260
261    hash: u64,
262
263    width_q: u32,
264}
265
266impl ShapedLine {
267    pub fn width(&self) -> f32 {
268        self.width
269    }
270
271    pub fn height(&self) -> f32 {
272        self.height
273    }
274
275    pub fn glyph_count(&self) -> usize {
276        self.glyphs.len()
277    }
278
279    pub fn row_count(&self) -> usize {
280        self.rows.len().max(1)
281    }
282
283    pub fn rows(&self) -> &[LineRow] {
284        &self.rows
285    }
286
287    pub fn row(&self, index: usize) -> Option<&LineRow> {
288        self.rows.get(index)
289    }
290
291    pub fn hash(&self) -> u64 {
292        self.hash
293    }
294
295    pub fn width_quant(&self) -> u32 {
296        self.width_q
297    }
298
299    pub fn is_empty(&self) -> bool {
300        self.glyphs.is_empty()
301    }
302}
303
304const LINE_CACHE_MAX: usize = 4_096;
305
306pub type LineCacheKey = (u64, u32);
307
308#[derive(Clone, Copy, Debug, PartialEq)]
309pub struct AtlasEntry {
310    pub rect: [u32; 4],
311
312    pub left: i32,
313
314    pub top: i32,
315
316    pub color: bool,
317}
318
319const ATLAS_MAX_DIM: u32 = 1024;
320
321const ATLAS_BPP: usize = 4;
322
323fn alpha_len(w: u32, h: u32) -> Option<usize> {
324    (w as usize).checked_mul(h as usize)
325}
326
327fn rgba_len(w: u32, h: u32) -> Option<usize> {
328    alpha_len(w, h)?.checked_mul(ATLAS_BPP)
329}
330
331fn checked_atlas_bytes(w: u32, h: u32) -> Option<usize> {
332    rgba_len(w, h)
333}
334
335#[derive(Clone, Copy, Debug, PartialEq, Eq)]
336pub struct AtlasDirty {
337    pub x: u32,
338    pub y: u32,
339    pub w: u32,
340    pub h: u32,
341    pub full: bool,
342}
343
344static ATLAS_SERIAL: AtomicU64 = AtomicU64::new(1);
345
346pub struct GlyphAtlas {
347    id: u64,
348    width: u32,
349    height: u32,
350    pixels: Vec<u8>,
351    entries: HashMap<CacheKey, AtlasEntry>,
352    sprites: HashMap<u64, AtlasEntry>,
353    cursor_x: u32,
354    cursor_y: u32,
355    row_h: u32,
356    version: u64,
357
358    epoch: u64,
359    dirty: Option<(u32, u32, u32, u32)>,
360    dirty_full: bool,
361
362    tick: Cell<u64>,
363    used: RefCell<HashMap<CacheKey, u64>>,
364    sprite_used: RefCell<HashMap<u64, u64>>,
365}
366
367impl GlyphAtlas {
368    pub fn new(width: u32, height: u32) -> Self {
369        let width = width.clamp(1, ATLAS_MAX_DIM);
370        let height = height.clamp(1, ATLAS_MAX_DIM);
371
372        let bytes = checked_atlas_bytes(width, height).unwrap_or(0);
373
374        Self {
375            id: ATLAS_SERIAL.fetch_add(1, Ordering::Relaxed),
376            width,
377            height,
378            pixels: vec![0; bytes],
379            entries: HashMap::new(),
380            sprites: HashMap::new(),
381            cursor_x: 0,
382            cursor_y: 0,
383            row_h: 0,
384            version: 0,
385            epoch: 0,
386            dirty: None,
387            dirty_full: false,
388            tick: Cell::new(0),
389            used: RefCell::new(HashMap::new()),
390            sprite_used: RefCell::new(HashMap::new()),
391        }
392    }
393
394    /// Unique identity for this atlas instance.
395    ///
396    /// Glyph UVs are only meaningful for the atlas that produced them; caches
397    /// must compare this id before reusing shaped output across text systems.
398    pub fn id(&self) -> u64 {
399        self.id
400    }
401
402    pub fn size(&self) -> (u32, u32) {
403        (self.width, self.height)
404    }
405
406    pub fn version(&self) -> u64 {
407        self.version
408    }
409
410    pub fn epoch(&self) -> u64 {
411        self.epoch
412    }
413
414    pub fn pixels(&self) -> &[u8] {
415        &self.pixels
416    }
417
418    /// Returns the number of bytes occupied by the atlas bitmap.
419    pub fn bytes(&self) -> usize {
420        self.pixels.len()
421    }
422
423    /// Drops the least recently used half of the atlas and repacks it.
424    ///
425    /// Entries that were not looked up recently are evicted, the survivors are
426    /// moved to the top of the bitmap, and the whole texture is marked dirty.
427    /// Glyph UVs emitted before this call become stale, so callers that keep a
428    /// render list across the call must repaint it (the renderer leaves this
429    /// until after the frame has been uploaded).
430    pub fn compact(&mut self) {
431        if self.entries.is_empty() && self.sprites.is_empty() {
432            return;
433        }
434        self.repack();
435    }
436
437    /// Compacts the atlas and shrinks the bitmap to fit the survivors.
438    ///
439    /// The texture never grows here; dimensions are rounded up to the next
440    /// power of two and clamped to the current size, with a small floor so a
441    /// single eviction does not immediately trigger another resize.
442    pub fn shrink_to_fit(&mut self) {
443        self.compact();
444        let need_w = self.cursor_x.max(1);
445        let need_h = (self.cursor_y + self.row_h).max(1);
446        let floor_w = 64.min(self.width);
447        let floor_h = 64.min(self.height);
448        let w = need_w.next_power_of_two().clamp(floor_w, self.width);
449        let h = need_h.next_power_of_two().clamp(floor_h, self.height);
450        if w != self.width || h != self.height {
451            self.resize(w, h);
452        }
453    }
454
455    fn mark_dirty(&mut self, x: u32, y: u32, w: u32, h: u32) {
456        if w == 0 || h == 0 {
457            return;
458        }
459        if self.dirty_full {
460            return;
461        }
462        match self.dirty {
463            None => self.dirty = Some((x, y, w, h)),
464            Some((dx, dy, dw, dh)) => {
465                let x0 = dx.min(x);
466                let y0 = dy.min(y);
467                let x1 = (dx + dw).max(x + w).min(self.width);
468                let y1 = (dy + dh).max(y + h).min(self.height);
469                self.dirty = Some((x0, y0, x1.saturating_sub(x0), y1.saturating_sub(y0)));
470            }
471        }
472    }
473
474    pub fn take_dirty(&mut self) -> Option<AtlasDirty> {
475        if self.dirty_full {
476            self.dirty_full = false;
477            self.dirty = None;
478            return Some(AtlasDirty {
479                x: 0,
480                y: 0,
481                w: self.width,
482                h: self.height,
483                full: true,
484            });
485        }
486        self.dirty.take().map(|(x, y, w, h)| AtlasDirty {
487            x,
488            y,
489            w: w.min(self.width.saturating_sub(x)),
490            h: h.min(self.height.saturating_sub(y)),
491            full: false,
492        })
493    }
494
495    pub fn copy_rect(&self, x: u32, y: u32, w: u32, h: u32, out: &mut Vec<u8>) {
496        out.clear();
497        if w == 0 || h == 0 {
498            return;
499        }
500        let (w, h) = (
501            w.min(self.width.saturating_sub(x)),
502            h.min(self.height.saturating_sub(y)),
503        );
504        if let Some(want) = checked_atlas_bytes(w, h) {
505            out.reserve(want);
506        }
507        let row_bytes = w as usize * ATLAS_BPP;
508        for row in 0..h {
509            let src = (((y + row) * self.width + x) as usize) * ATLAS_BPP;
510            debug_assert!(src.saturating_add(row_bytes) <= self.pixels.len());
511            out.extend_from_slice(&self.pixels[src..src + row_bytes]);
512            debug_assert_eq!(out.len() % ATLAS_BPP, 0);
513        }
514    }
515
516    pub fn entry(&self, key: &CacheKey) -> Option<AtlasEntry> {
517        let entry = self.entries.get(key).copied()?;
518        self.touch(self.used.borrow_mut().entry(*key).or_default());
519        Some(entry)
520    }
521
522    pub fn sprite(&self, key: u64) -> Option<AtlasEntry> {
523        let entry = self.sprites.get(&key).copied()?;
524        self.touch(self.sprite_used.borrow_mut().entry(key).or_default());
525        Some(entry)
526    }
527
528    fn touch(&self, slot: &mut u64) {
529        let t = self.tick.get() + 1;
530        self.tick.set(t);
531        *slot = t;
532    }
533
534    pub fn insert_sprite(&mut self, key: u64, w: u32, h: u32, alpha: &[u8]) -> AtlasEntry {
535        let (w, h) = (w.max(1), h.max(1));
536
537        let Some(count) = alpha_len(w, h).filter(|want| alpha.len() >= *want) else {
538            return AtlasEntry {
539                rect: [0, 0, 1, 1],
540                left: 0,
541                top: 0,
542                color: false,
543            };
544        };
545        let mut rgba = Vec::with_capacity(count * ATLAS_BPP);
546        for &a in &alpha[..count] {
547            rgba.extend_from_slice(&[255, 255, 255, a]);
548        }
549        if !self.blit(w, h, &rgba) {
550            log::warn!(
551                "sequoia-text: atlas full (max {ATLAS_MAX_DIM}px) - sprite skipped, icon will have a hole"
552            );
553            return AtlasEntry {
554                rect: [0, 0, 1, 1],
555                left: 0,
556                top: 0,
557                color: false,
558            };
559        }
560        let entry = AtlasEntry {
561            rect: [self.cursor_x, self.cursor_y, w, h],
562            left: 0,
563            top: 0,
564            color: false,
565        };
566        self.sprites.insert(key, entry);
567        self.touch(self.sprite_used.borrow_mut().entry(key).or_default());
568        self.cursor_x += w + 1;
569        self.row_h = self.row_h.max(h);
570        self.version += 1;
571        entry
572    }
573
574    fn blit(&mut self, w: u32, h: u32, data: &[u8]) -> bool {
575        if !self.ensure_space(w, h) {
576            return false;
577        }
578        let (bx, by) = (self.cursor_x, self.cursor_y);
579        let row_bytes = w as usize * ATLAS_BPP;
580        for row in 0..h {
581            let src = row as usize * row_bytes;
582            let dst = (((by + row) * self.width + bx) as usize) * ATLAS_BPP;
583            self.pixels[dst..dst + row_bytes].copy_from_slice(&data[src..src + row_bytes]);
584        }
585        self.mark_dirty(bx, by, w, h);
586        true
587    }
588
589    fn insert(
590        &mut self,
591        key: CacheKey,
592        w: u32,
593        h: u32,
594        data: &[u8],
595        left: i32,
596        top: i32,
597        color: bool,
598    ) -> AtlasEntry {
599        let (w, h) = (w.max(1), h.max(1));
600        if rgba_len(w, h).is_none_or(|want| data.len() < want) {
601            return AtlasEntry {
602                rect: [0, 0, 1, 1],
603                left: 0,
604                top: 0,
605                color: false,
606            };
607        }
608        if !self.blit(w, h, data) {
609            return AtlasEntry {
610                rect: [0, 0, 1, 1],
611                left: 0,
612                top: 0,
613                color: false,
614            };
615        }
616
617        let entry = AtlasEntry {
618            rect: [self.cursor_x, self.cursor_y, w, h],
619            left,
620            top,
621            color,
622        };
623        self.entries.insert(key, entry);
624        self.touch(self.used.borrow_mut().entry(key).or_default());
625        self.cursor_x += w + 1;
626        self.row_h = self.row_h.max(h);
627        self.version += 1;
628        entry
629    }
630
631    fn ensure_space(&mut self, w: u32, h: u32) -> bool {
632        if w > ATLAS_MAX_DIM || h > ATLAS_MAX_DIM {
633            return false;
634        }
635
636        if w > self.width {
637            let nw = w.next_power_of_two().min(ATLAS_MAX_DIM);
638            if nw < w {
639                return false;
640            }
641            self.resize(nw, self.height);
642        }
643        if self.cursor_x + w > self.width {
644            self.cursor_y += self.row_h + 1;
645            self.cursor_x = 0;
646            self.row_h = 0;
647        }
648        if self.cursor_y + h <= self.height {
649            return true;
650        }
651
652        let mut nh = self.height;
653        while self.cursor_y + h > nh && nh < ATLAS_MAX_DIM {
654            nh = (nh * 2).min(ATLAS_MAX_DIM);
655        }
656        if self.cursor_y + h <= nh {
657            self.resize(self.width, nh);
658            return true;
659        }
660
661        log::debug!("sequoia-text: atlas overflow - LRU compaction (newest entries kept)");
662        self.repack();
663        if self.cursor_x + w > self.width {
664            self.cursor_y += self.row_h + 1;
665            self.cursor_x = 0;
666            self.row_h = 0;
667        }
668        if self.cursor_y + h > self.height {
669            log::debug!("sequoia-text: atlas still full after compaction - full clear");
670            self.gc();
671        }
672        while self.cursor_y + h > self.height && self.height < ATLAS_MAX_DIM {
673            let nh = (self.height * 2).min(ATLAS_MAX_DIM);
674            self.resize(self.width, nh);
675        }
676        self.cursor_x + w <= self.width && self.cursor_y + h <= self.height
677    }
678
679    fn repack(&mut self) {
680        enum Kind {
681            Glyph(CacheKey),
682            Sprite(u64),
683        }
684        let mut items: Vec<(u64, Kind, AtlasEntry)> =
685            Vec::with_capacity(self.entries.len() + self.sprites.len());
686        for (key, entry) in &self.entries {
687            let used = self.used.borrow().get(key).copied().unwrap_or(0);
688            items.push((used, Kind::Glyph(*key), *entry));
689        }
690        for (key, entry) in &self.sprites {
691            let used = self.sprite_used.borrow().get(key).copied().unwrap_or(0);
692            items.push((used, Kind::Sprite(*key), *entry));
693        }
694
695        items.sort_by_key(|b| std::cmp::Reverse(b.0));
696
697        items.truncate(items.len().div_ceil(2));
698
699        let total = items.len() as u64;
700        let (width, height) = (self.width, self.height);
701        let mut pixels = vec![0u8; self.pixels.len()];
702        let mut entries = HashMap::new();
703        let mut sprites = HashMap::new();
704        let mut used = HashMap::new();
705        let mut sprite_used = HashMap::new();
706        let (mut cx, mut cy, mut rh) = (0u32, 0u32, 0u32);
707        let mut age = total;
708        for (_, kind, entry) in items {
709            age = age.saturating_sub(1);
710            let [sx, sy, w, h] = entry.rect;
711            if w == 0 || h == 0 || w > width {
712                continue;
713            }
714            if cx + w > width {
715                cy += rh + 1;
716                cx = 0;
717                rh = 0;
718            }
719            if cy + h > height {
720                break;
721            }
722            let row_bytes = w as usize * ATLAS_BPP;
723            for row in 0..h {
724                let src = (((sy + row) * width + sx) as usize) * ATLAS_BPP;
725                let dst = (((cy + row) * width + cx) as usize) * ATLAS_BPP;
726                pixels[dst..dst + row_bytes].copy_from_slice(&self.pixels[src..src + row_bytes]);
727            }
728            let moved = AtlasEntry {
729                rect: [cx, cy, w, h],
730                left: entry.left,
731                top: entry.top,
732                color: entry.color,
733            };
734            match kind {
735                Kind::Glyph(key) => {
736                    entries.insert(key, moved);
737                    used.insert(key, age);
738                }
739                Kind::Sprite(key) => {
740                    sprites.insert(key, moved);
741                    sprite_used.insert(key, age);
742                }
743            }
744            cx += w + 1;
745            rh = rh.max(h);
746        }
747        self.pixels = pixels;
748        self.entries = entries;
749        self.sprites = sprites;
750        *self.used.borrow_mut() = used;
751        *self.sprite_used.borrow_mut() = sprite_used;
752        self.tick.set(total);
753        self.cursor_x = cx;
754        self.cursor_y = cy;
755        self.row_h = rh;
756        self.version += 1;
757        self.epoch += 1;
758        self.dirty = None;
759        self.dirty_full = true;
760    }
761
762    fn gc(&mut self) {
763        self.pixels.fill(0);
764        self.entries.clear();
765        self.sprites.clear();
766        self.used.borrow_mut().clear();
767        self.sprite_used.borrow_mut().clear();
768        self.cursor_x = 0;
769        self.cursor_y = 0;
770        self.row_h = 0;
771        self.version += 1;
772        self.epoch += 1;
773        self.dirty = None;
774        self.dirty_full = true;
775    }
776
777    fn resize(&mut self, new_w: u32, new_h: u32) {
778        let new_w = new_w.clamp(1, ATLAS_MAX_DIM);
779        let new_h = new_h.clamp(1, ATLAS_MAX_DIM);
780        let bytes = checked_atlas_bytes(new_w, new_h).unwrap_or(0);
781        let mut pixels = vec![0u8; bytes];
782        let copy_w = self.width.min(new_w);
783        let row_bytes = copy_w as usize * ATLAS_BPP;
784        for row in 0..self.height.min(new_h) {
785            let src = row as usize * self.width as usize * ATLAS_BPP;
786            let dst = row as usize * new_w as usize * ATLAS_BPP;
787            pixels[dst..dst + row_bytes].copy_from_slice(&self.pixels[src..src + row_bytes]);
788        }
789        self.pixels = pixels;
790        self.width = new_w;
791        self.height = new_h;
792        self.version += 1;
793        self.epoch += 1;
794        self.dirty = None;
795        self.dirty_full = true;
796    }
797}
798
799const SHAPING_CACHE_MAX: usize = 256;
800
801/// Number of direct-mapped hot slots used before falling back to the full
802/// shaping cache. Real paint loops revisit the same labels every frame, and a
803/// slot hit avoids both key allocation and the LRU list scan.
804const SHAPE_SLOTS: usize = 4096;
805
806const CARET_CACHE_MAX: usize = 128;
807
808pub type LetterSpacing = f32;
809
810pub(crate) fn sanitize_spacing(spacing: f32) -> f32 {
811    if spacing.is_finite() { spacing } else { 0.0 }
812}
813
814pub(crate) fn spacing_key(spacing: f32) -> i32 {
815    (sanitize_spacing(spacing) * 4.0).round() as i32
816}
817
818const DEFAULT_FONT_REGULAR: &[u8] = include_bytes!("../fonts/Inter-Regular.otf");
819
820const DEFAULT_FONT_MEDIUM: &[u8] = include_bytes!("../fonts/Inter-Medium.otf");
821
822const DEFAULT_FONT_SEMIBOLD: &[u8] = include_bytes!("../fonts/Inter-SemiBold.otf");
823
824const DEFAULT_FONT_BOLD: &[u8] = include_bytes!("../fonts/Inter-Bold.otf");
825
826const DEFAULT_FAMILY: &str = "Inter";
827
828const BUNDLED_WEIGHTS: [u16; 4] = [400, 500, 600, 700];
829
830type ShapeKey = (String, u32, u16, String, u8, u32, u8, i32, u32);
831type CaretKey = (String, u32, u16, String, u8, u32, i32);
832
833#[derive(Clone)]
834struct MruShape {
835    text: String,
836    size_q: u32,
837    weight_v: u16,
838    family: Option<String>,
839    style_k: u8,
840    width_q: u32,
841    mode_k: u8,
842    spacing_k: i32,
843    line_height_k: u32,
844    shaped: Rc<ShapedText>,
845}
846
847pub struct TextSystem {
848    font_system: FontSystem,
849    swash: SwashCache,
850    atlas: GlyphAtlas,
851    shaped: HashMap<ShapeKey, Rc<ShapedText>>,
852
853    shaping_order: VecDeque<ShapeKey>,
854
855    mru: Option<MruShape>,
856
857    mru_slots: Vec<Option<MruShape>>,
858
859    carets: HashMap<CaretKey, Rc<[f32]>>,
860
861    carets_order: VecDeque<CaretKey>,
862
863    rich: HashMap<rich::RichKey, Rc<rich::RichParagraph>>,
864
865    rich_order: VecDeque<rich::RichKey>,
866
867    lines: HashMap<LineCacheKey, Rc<ShapedLine>>,
868    lines_order: VecDeque<LineCacheKey>,
869
870    line_to_key: HashMap<(usize, u32), LineCacheKey>,
871
872    line_reshapes: u64,
873
874    scratch_placed: Vec<(CacheKey, i32, i32)>,
875    shape_scratch: Option<Buffer>,
876    scale: f32,
877
878    default_family: Option<String>,
879
880    default_weights: Vec<u16>,
881
882    aliases: HashMap<String, String>,
883
884    subpixel_y: bool,
885
886    font_system_bg: Option<Arc<Mutex<Option<FontSystem>>>>,
887}
888
889impl Default for TextSystem {
890    fn default() -> Self {
891        Self::new()
892    }
893}
894
895impl TextSystem {
896    pub fn new() -> Self {
897        Self::with_atlas(512, 256)
898    }
899
900    pub fn with_atlas(width: u32, height: u32) -> Self {
901        let mut db = cosmic_text::fontdb::Database::new();
902        db.load_font_data(DEFAULT_FONT_REGULAR.to_vec());
903        db.load_font_data(DEFAULT_FONT_MEDIUM.to_vec());
904        db.load_font_data(DEFAULT_FONT_SEMIBOLD.to_vec());
905        db.load_font_data(DEFAULT_FONT_BOLD.to_vec());
906        let font_system = FontSystem::new_with_locale_and_db("en-US".into(), db);
907
908        let font_system_bg = Some(Arc::new(Mutex::new(None)));
909        let bg_slot = Arc::clone(font_system_bg.as_ref().unwrap());
910        std::thread::spawn(move || {
911            let full_fs = FontSystem::new();
912            if let Ok(mut lock) = bg_slot.lock() {
913                *lock = Some(full_fs);
914            }
915        });
916
917        let mut this = Self {
918            font_system,
919            swash: SwashCache::new(),
920            atlas: GlyphAtlas::new(width, height),
921            shaped: HashMap::new(),
922            shaping_order: VecDeque::new(),
923            mru: None,
924            mru_slots: vec![None; SHAPE_SLOTS],
925            carets: HashMap::new(),
926            carets_order: VecDeque::new(),
927            rich: HashMap::new(),
928            rich_order: VecDeque::new(),
929            lines: HashMap::new(),
930            lines_order: VecDeque::new(),
931            line_to_key: HashMap::new(),
932            line_reshapes: 0,
933            scratch_placed: Vec::new(),
934            shape_scratch: None,
935            scale: 1.0,
936            default_family: None,
937            default_weights: Vec::new(),
938            aliases: HashMap::new(),
939            subpixel_y: false,
940            font_system_bg,
941        };
942
943        this.load_font(DEFAULT_FONT_REGULAR.to_vec());
944        this.load_font(DEFAULT_FONT_MEDIUM.to_vec());
945        this.load_font(DEFAULT_FONT_SEMIBOLD.to_vec());
946        this.load_font(DEFAULT_FONT_BOLD.to_vec());
947        this.default_family = Some(DEFAULT_FAMILY.to_owned());
948        this.refresh_default_weights();
949        this
950    }
951
952    fn check_bg_font_system(&mut self) {
953        let fs_opt = self.font_system_bg.as_ref().and_then(|bg| {
954            bg.lock().ok().and_then(|mut lock| lock.take())
955        });
956        if let Some(fs) = fs_opt {
957            self.font_system = fs;
958            self.load_font(DEFAULT_FONT_REGULAR.to_vec());
959            self.load_font(DEFAULT_FONT_MEDIUM.to_vec());
960            self.load_font(DEFAULT_FONT_SEMIBOLD.to_vec());
961            self.load_font(DEFAULT_FONT_BOLD.to_vec());
962            self.clear_caches();
963            self.refresh_default_weights();
964        }
965    }
966
967    fn refresh_default_weights(&mut self) {
968        let mut weights: Vec<u16> = Vec::new();
969        if let Some(family) = self.default_family.as_deref() {
970            for face in self.font_system.db().faces() {
971                if face.families.iter().any(|(n, _)| n == family)
972                    && !weights.contains(&face.weight.0)
973                {
974                    weights.push(face.weight.0);
975                }
976            }
977            weights.sort_unstable();
978        }
979        self.default_weights = weights;
980    }
981
982    pub(crate) fn resolve_weight_for(&self, weight: FontWeight, eff_family: &str) -> FontWeight {
983        let want = weight.value();
984        if self.default_weights.contains(&want) {
985            return weight;
986        }
987        if eff_family != DEFAULT_FAMILY {
988            return weight;
989        }
990        let snapped = BUNDLED_WEIGHTS
991            .into_iter()
992            .min_by(|a, b| a.abs_diff(want).cmp(&b.abs_diff(want)).then(b.cmp(a)))
993            .unwrap_or(FontWeight::NORMAL.value());
994        FontWeight::new(snapped)
995    }
996
997    pub(crate) fn effective_family(&self, family: Option<&str>) -> String {
998        let requested = match family {
999            Some(f) if !f.is_empty() => f,
1000            _ => return self.default_family.clone().unwrap_or_default(),
1001        };
1002        if let Some(real) = self.aliases.get(requested) {
1003            return real.clone();
1004        }
1005        requested.to_owned()
1006    }
1007
1008    fn clear_caches(&mut self) {
1009        self.shaped.clear();
1010        self.shaping_order.clear();
1011        self.mru = None;
1012        self.mru_slots.fill_with(|| None);
1013        self.carets.clear();
1014        self.carets_order.clear();
1015        self.rich.clear();
1016        self.rich_order.clear();
1017        self.lines.clear();
1018        self.lines_order.clear();
1019        self.line_to_key.clear();
1020    }
1021
1022    fn clear_line_cache(&mut self) {
1023        self.lines.clear();
1024        self.lines_order.clear();
1025        self.line_to_key.clear();
1026    }
1027
1028    pub fn register_alias(&mut self, alias: impl Into<String>, family: impl Into<String>) {
1029        let (alias, family) = (alias.into(), family.into());
1030        if alias.is_empty() || family.is_empty() {
1031            return;
1032        }
1033        if self.aliases.get(&alias).is_some_and(|f| f == &family) {
1034            return;
1035        }
1036        self.aliases.insert(alias, family);
1037        self.clear_caches();
1038    }
1039
1040    pub fn font_families(&self) -> Vec<String> {
1041        let mut out: Vec<String> = Vec::new();
1042        for face in self.font_system.db().faces() {
1043            for (name, _) in face.families.iter() {
1044                if !out.contains(name) {
1045                    out.push(name.clone());
1046                }
1047            }
1048        }
1049        out.sort();
1050        out
1051    }
1052
1053    pub fn set_subpixel_y(&mut self, on: bool) {
1054        self.subpixel_y = on;
1055    }
1056
1057    pub fn subpixel_y(&self) -> bool {
1058        self.subpixel_y
1059    }
1060
1061    pub fn load_font(&mut self, data: Vec<u8>) -> Option<String> {
1062        let db = self.font_system.db_mut();
1063        let before = db.faces().count();
1064        db.load_font_data(data);
1065        self.mru = None;
1066        self.mru_slots.fill_with(|| None);
1067        self.refresh_default_weights();
1068        self.font_system
1069            .db()
1070            .faces()
1071            .nth(before)
1072            .and_then(|face| face.families.first().map(|(name, _)| name.clone()))
1073    }
1074
1075    pub fn set_default_family(&mut self, family: Option<String>) {
1076        if self.default_family != family {
1077            self.default_family = family;
1078            self.clear_caches();
1079            self.refresh_default_weights();
1080        }
1081    }
1082
1083    pub fn default_family(&self) -> Option<&str> {
1084        self.default_family.as_deref()
1085    }
1086
1087    pub fn set_scale_factor(&mut self, scale: f32) {
1088        if !scale.is_finite() || scale <= 0.0 {
1089            return;
1090        }
1091        if (scale - self.scale).abs() > f32::EPSILON {
1092            self.scale = scale;
1093            self.shaped.clear();
1094            self.shaping_order.clear();
1095            self.mru = None;
1096            self.mru_slots.fill_with(|| None);
1097            self.carets.clear();
1098            self.carets_order.clear();
1099            self.rich.clear();
1100            self.rich_order.clear();
1101            self.clear_line_cache();
1102        }
1103    }
1104
1105    pub fn scale_factor(&self) -> f32 {
1106        self.scale
1107    }
1108
1109    pub fn shape(&mut self, text: &str, size: f32) -> Rc<ShapedText> {
1110        self.shape_weighted(text, size, FontWeight::NORMAL)
1111    }
1112
1113    pub fn shape_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> Rc<ShapedText> {
1114        self.check_bg_font_system();
1115        self.shape_wrapped(text, size, weight, 0.0, WrapMode::default())
1116    }
1117
1118    pub fn shape_spaced(
1119        &mut self,
1120        text: &str,
1121        size: f32,
1122        weight: FontWeight,
1123        spacing: LetterSpacing,
1124    ) -> Rc<ShapedText> {
1125        self.shape_wrapped_spaced(text, size, weight, 0.0, WrapMode::default(), spacing)
1126    }
1127
1128    pub fn shape_wrapped(
1129        &mut self,
1130        text: &str,
1131        size: f32,
1132        weight: FontWeight,
1133        width: f32,
1134        mode: WrapMode,
1135    ) -> Rc<ShapedText> {
1136        self.shape_wrapped_spaced(text, size, weight, width, mode, 0.0)
1137    }
1138
1139    pub fn shape_wrapped_spaced(
1140        &mut self,
1141        text: &str,
1142        size: f32,
1143        weight: FontWeight,
1144        width: f32,
1145        mode: WrapMode,
1146        spacing: LetterSpacing,
1147    ) -> Rc<ShapedText> {
1148        self.shape_wrapped_styled_spaced(
1149            text,
1150            size,
1151            weight,
1152            width,
1153            mode,
1154            spacing,
1155            None,
1156            FontStyle::Normal,
1157        )
1158    }
1159
1160    pub fn shape_styled(
1161        &mut self,
1162        text: &str,
1163        size: f32,
1164        weight: FontWeight,
1165        family: Option<&str>,
1166        style: FontStyle,
1167    ) -> Rc<ShapedText> {
1168        self.shape_wrapped_styled_spaced(
1169            text,
1170            size,
1171            weight,
1172            0.0,
1173            WrapMode::default(),
1174            0.0,
1175            family,
1176            style,
1177        )
1178    }
1179
1180    pub fn shape_spaced_styled(
1181        &mut self,
1182        text: &str,
1183        size: f32,
1184        weight: FontWeight,
1185        spacing: LetterSpacing,
1186        family: Option<&str>,
1187        style: FontStyle,
1188    ) -> Rc<ShapedText> {
1189        self.shape_wrapped_styled_spaced(
1190            text,
1191            size,
1192            weight,
1193            0.0,
1194            WrapMode::default(),
1195            spacing,
1196            family,
1197            style,
1198        )
1199    }
1200
1201    pub fn shape_wrapped_styled(
1202        &mut self,
1203        text: &str,
1204        size: f32,
1205        weight: FontWeight,
1206        width: f32,
1207        mode: WrapMode,
1208        family: Option<&str>,
1209        style: FontStyle,
1210    ) -> Rc<ShapedText> {
1211        self.shape_wrapped_styled_spaced(text, size, weight, width, mode, 0.0, family, style)
1212    }
1213
1214    #[allow(clippy::too_many_arguments)]
1215    fn shape_slot_hash(
1216        text: &str,
1217        size_q: u32,
1218        weight_v: u16,
1219        family: Option<&str>,
1220        style_k: u8,
1221        width_q: u32,
1222        mode_k: u8,
1223        spacing_k: i32,
1224        line_height_k: u32,
1225    ) -> u64 {
1226        let mut h = std::collections::hash_map::DefaultHasher::new();
1227        text.hash(&mut h);
1228        size_q.hash(&mut h);
1229        weight_v.hash(&mut h);
1230        family.hash(&mut h);
1231        style_k.hash(&mut h);
1232        width_q.hash(&mut h);
1233        mode_k.hash(&mut h);
1234        spacing_k.hash(&mut h);
1235        line_height_k.hash(&mut h);
1236        h.finish()
1237    }
1238
1239    #[allow(clippy::too_many_arguments)]
1240    fn mru_hit(
1241        &self,
1242        text: &str,
1243        size_q: u32,
1244        weight_v: u16,
1245        family: Option<&str>,
1246        style_k: u8,
1247        width_q: u32,
1248        mode_k: u8,
1249        spacing_k: i32,
1250        line_height_k: u32,
1251    ) -> Option<Rc<ShapedText>> {
1252        let mru = self.mru.as_ref()?;
1253        (mru.text == text
1254            && mru.size_q == size_q
1255            && mru.weight_v == weight_v
1256            && mru.family.as_deref() == family
1257            && mru.style_k == style_k
1258            && mru.width_q == width_q
1259            && mru.mode_k == mode_k
1260            && mru.spacing_k == spacing_k
1261            && mru.line_height_k == line_height_k)
1262            .then(|| mru.shaped.clone())
1263    }
1264
1265    #[allow(clippy::too_many_arguments)]
1266    fn slot_hit(
1267        &self,
1268        hash: u64,
1269        text: &str,
1270        size_q: u32,
1271        weight_v: u16,
1272        family: Option<&str>,
1273        style_k: u8,
1274        width_q: u32,
1275        mode_k: u8,
1276        spacing_k: i32,
1277        line_height_k: u32,
1278    ) -> Option<Rc<ShapedText>> {
1279        let slots = &self.mru_slots;
1280        let slot = slots.get(hash as usize & (slots.len() - 1))?.as_ref()?;
1281        (slot.text == text
1282            && slot.size_q == size_q
1283            && slot.weight_v == weight_v
1284            && slot.family.as_deref() == family
1285            && slot.style_k == style_k
1286            && slot.width_q == width_q
1287            && slot.mode_k == mode_k
1288            && slot.spacing_k == spacing_k
1289            && slot.line_height_k == line_height_k)
1290            .then(|| slot.shaped.clone())
1291    }
1292
1293    #[allow(clippy::too_many_arguments)]
1294    fn remember_slot(
1295        &mut self,
1296        hash: u64,
1297        text: &str,
1298        size_q: u32,
1299        weight_v: u16,
1300        family: Option<&str>,
1301        style_k: u8,
1302        width_q: u32,
1303        mode_k: u8,
1304        spacing_k: i32,
1305        line_height_k: u32,
1306        shaped: Rc<ShapedText>,
1307    ) {
1308        let idx = hash as usize & (self.mru_slots.len() - 1);
1309        self.mru_slots[idx] = Some(MruShape {
1310            text: text.to_owned(),
1311            size_q,
1312            weight_v,
1313            family: family.map(str::to_owned),
1314            style_k,
1315            width_q,
1316            mode_k,
1317            spacing_k,
1318            line_height_k,
1319            shaped,
1320        });
1321    }
1322
1323    pub fn shape_wrapped_styled_spaced(
1324        &mut self,
1325        text: &str,
1326        size: f32,
1327        weight: FontWeight,
1328        width: f32,
1329        mode: WrapMode,
1330        spacing: LetterSpacing,
1331        family: Option<&str>,
1332        style: FontStyle,
1333    ) -> Rc<ShapedText> {
1334        self.shape_wrapped_styled_spaced_lh(
1335            text,
1336            size,
1337            weight,
1338            width,
1339            mode,
1340            spacing,
1341            family,
1342            style,
1343            LineHeight::default(),
1344        )
1345    }
1346
1347    #[allow(clippy::too_many_arguments)]
1348    pub fn shape_wrapped_styled_spaced_lh(
1349        &mut self,
1350        text: &str,
1351        size: f32,
1352        weight: FontWeight,
1353        width: f32,
1354        mode: WrapMode,
1355        spacing: LetterSpacing,
1356        family: Option<&str>,
1357        style: FontStyle,
1358        line_height: LineHeight,
1359    ) -> Rc<ShapedText> {
1360        let size_q = (size * 4.0).round() as u32;
1361        let width_q = (width.max(0.0) * 4.0).round() as u32;
1362        let spacing = sanitize_spacing(spacing);
1363        let spacing_k = spacing_key(spacing);
1364        let line_height_k = line_height.key();
1365
1366        let weight_raw_v = weight.value();
1367        let style_k = style.key();
1368        let mode_k = mode.key();
1369        if let Some(hit) = self.mru_hit(
1370            text,
1371            size_q,
1372            weight_raw_v,
1373            family,
1374            style_k,
1375            width_q,
1376            mode_k,
1377            spacing_k,
1378            line_height_k,
1379        ) {
1380            return hit;
1381        }
1382        let slot_hash = Self::shape_slot_hash(
1383            text,
1384            size_q,
1385            weight_raw_v,
1386            family,
1387            style_k,
1388            width_q,
1389            mode_k,
1390            spacing_k,
1391            line_height_k,
1392        );
1393        if let Some(hit) = self.slot_hit(
1394            slot_hash,
1395            text,
1396            size_q,
1397            weight_raw_v,
1398            family,
1399            style_k,
1400            width_q,
1401            mode_k,
1402            spacing_k,
1403            line_height_k,
1404        ) {
1405            return hit;
1406        }
1407        let eff_family = self.effective_family(family);
1408        let weight = self.resolve_weight_for(weight, &eff_family);
1409        let key: ShapeKey = (
1410            text.to_owned(),
1411            size_q,
1412            weight.value(),
1413            eff_family.clone(),
1414            style_k,
1415            width_q,
1416            mode_k,
1417            spacing_k,
1418            line_height_k,
1419        );
1420        if let Some(rc) = self.shaped.get(&key) {
1421            if let Some(pos) = self.shaping_order.iter().position(|k| k == &key) {
1422                self.shaping_order.remove(pos);
1423            }
1424            self.shaping_order.push_front(key.clone());
1425            let shaped = rc.clone();
1426            self.mru = Some(MruShape {
1427                text: text.to_owned(),
1428                size_q,
1429                weight_v: weight_raw_v,
1430                family: family.map(str::to_owned),
1431                style_k,
1432                width_q,
1433                mode_k,
1434                spacing_k,
1435                line_height_k,
1436                shaped: shaped.clone(),
1437            });
1438            self.remember_slot(
1439                slot_hash,
1440                text,
1441                size_q,
1442                weight_raw_v,
1443                family,
1444                style_k,
1445                width_q,
1446                mode_k,
1447                spacing_k,
1448                line_height_k,
1449                shaped.clone(),
1450            );
1451            return shaped;
1452        }
1453        let shaped = self.shape_uncached_styled(
1454            text,
1455            size,
1456            weight,
1457            &eff_family,
1458            style,
1459            width,
1460            mode,
1461            spacing,
1462            line_height,
1463        );
1464        self.shaped.insert(key.clone(), shaped.clone());
1465        self.shaping_order.push_front(key.clone());
1466        while self.shaped.len() > SHAPING_CACHE_MAX {
1467            if let Some(oldest) = self.shaping_order.pop_back() {
1468                self.shaped.remove(&oldest);
1469            } else {
1470                break;
1471            }
1472        }
1473        self.mru = Some(MruShape {
1474            text: text.to_owned(),
1475            size_q,
1476            weight_v: weight_raw_v,
1477            family: family.map(str::to_owned),
1478            style_k,
1479            width_q,
1480            mode_k,
1481            spacing_k,
1482            line_height_k,
1483            shaped: shaped.clone(),
1484        });
1485        self.remember_slot(
1486            slot_hash,
1487            text,
1488            size_q,
1489            weight_raw_v,
1490            family,
1491            style_k,
1492            width_q,
1493            mode_k,
1494            spacing_k,
1495            line_height_k,
1496            shaped.clone(),
1497        );
1498        shaped
1499    }
1500
1501    pub fn line_hash(
1502        line_text: &str,
1503        spans: &[StyledSpan],
1504        line_byte_start: usize,
1505        width: f32,
1506    ) -> u64 {
1507        sequoia_buffer::line_hash(line_text, &rebase_spans(spans, line_byte_start), width)
1508    }
1509
1510    pub fn hash_line_content(
1511        text: &str,
1512        extra_hash: u64,
1513        size: f32,
1514        weight: FontWeight,
1515        family: &str,
1516        style: FontStyle,
1517        spacing: f32,
1518        mode: WrapMode,
1519        scale_bits: u32,
1520    ) -> u64 {
1521        use std::collections::hash_map::DefaultHasher;
1522        use std::hash::{Hash, Hasher};
1523        let mut h = DefaultHasher::new();
1524        text.hash(&mut h);
1525        extra_hash.hash(&mut h);
1526        ((size * 4.0).round() as u32).hash(&mut h);
1527        weight.value().hash(&mut h);
1528        family.hash(&mut h);
1529        style.key().hash(&mut h);
1530        spacing_key(sanitize_spacing(spacing)).hash(&mut h);
1531        mode.key().hash(&mut h);
1532        scale_bits.hash(&mut h);
1533        h.finish()
1534    }
1535
1536    fn line_key_for(
1537        &self,
1538        text: &str,
1539        extra_hash: u64,
1540        size: f32,
1541        weight: FontWeight,
1542        eff_family: &str,
1543        style: FontStyle,
1544        spacing: f32,
1545        width: f32,
1546        mode: WrapMode,
1547    ) -> LineCacheKey {
1548        let hash = Self::hash_line_content(
1549            text,
1550            extra_hash,
1551            size,
1552            weight,
1553            eff_family,
1554            style,
1555            spacing,
1556            mode,
1557            self.scale.to_bits(),
1558        );
1559        let width_q = (width.max(0.0) * 4.0).round() as u32;
1560        (hash, width_q)
1561    }
1562
1563    pub fn shape_line_full(
1564        &mut self,
1565        line: usize,
1566        text: &str,
1567        size: f32,
1568        weight: FontWeight,
1569        width: f32,
1570        mode: WrapMode,
1571        spacing: LetterSpacing,
1572        family: Option<&str>,
1573        style: FontStyle,
1574        extra_hash: u64,
1575    ) -> Rc<ShapedLine> {
1576        let spacing = sanitize_spacing(spacing);
1577        let eff_family = self.effective_family(family);
1578        let weight = self.resolve_weight_for(weight, &eff_family);
1579        let width_q = (width.max(0.0) * 4.0).round() as u32;
1580        let key = self.line_key_for(
1581            text,
1582            extra_hash,
1583            size,
1584            weight,
1585            &eff_family,
1586            style,
1587            spacing,
1588            width,
1589            mode,
1590        );
1591        if let Some(rc) = self.lines.get(&key) {
1592            if let Some(pos) = self.lines_order.iter().position(|k| k == &key) {
1593                self.lines_order.remove(pos);
1594            }
1595            self.lines_order.push_front(key);
1596            self.line_to_key.insert((line, width_q), key);
1597            return rc.clone();
1598        }
1599        let shaped = self.shape_line_uncached(
1600            text,
1601            size,
1602            weight,
1603            &eff_family,
1604            style,
1605            width,
1606            mode,
1607            spacing,
1608            key.0,
1609            width_q,
1610        );
1611        self.line_reshapes += 1;
1612        self.lines.insert(key, shaped.clone());
1613        self.lines_order.push_front(key);
1614        self.line_to_key.insert((line, width_q), key);
1615        while self.lines.len() > LINE_CACHE_MAX {
1616            if let Some(oldest) = self.lines_order.pop_back() {
1617                self.lines.remove(&oldest);
1618
1619                self.line_to_key.retain(|_, k| k != &oldest);
1620            } else {
1621                break;
1622            }
1623        }
1624        shaped
1625    }
1626
1627    pub fn shape_line_wrapped(
1628        &mut self,
1629        line: usize,
1630        text: &str,
1631        size: f32,
1632        weight: FontWeight,
1633        width: f32,
1634        mode: WrapMode,
1635    ) -> Rc<ShapedLine> {
1636        self.shape_line_full(
1637            line,
1638            text,
1639            size,
1640            weight,
1641            width,
1642            mode,
1643            0.0,
1644            None,
1645            FontStyle::Normal,
1646            0,
1647        )
1648    }
1649
1650    pub fn shape_line(&mut self, line: usize, text: &str, size: f32) -> Rc<ShapedLine> {
1651        self.shape_line_wrapped(
1652            line,
1653            text,
1654            size,
1655            FontWeight::NORMAL,
1656            0.0,
1657            WrapMode::default(),
1658        )
1659    }
1660
1661    pub fn shape_lines_visible<F>(
1662        &mut self,
1663        range: std::ops::Range<usize>,
1664        get_line: F,
1665        size: f32,
1666        weight: FontWeight,
1667        width: f32,
1668        mode: WrapMode,
1669    ) -> Vec<Rc<ShapedLine>>
1670    where
1671        F: Fn(usize) -> Option<String>,
1672    {
1673        let mut out = Vec::new();
1674        let (start, end) = (range.start, range.end);
1675        out.reserve(end.saturating_sub(start));
1676        for line in start..end {
1677            if let Some(text) = get_line(line) {
1678                out.push(self.shape_line_wrapped(line, &text, size, weight, width, mode));
1679            }
1680        }
1681        out
1682    }
1683
1684    pub fn shape_lines_visible_full<F>(
1685        &mut self,
1686        range: std::ops::Range<usize>,
1687        get_line: F,
1688        size: f32,
1689        weight: FontWeight,
1690        width: f32,
1691        mode: WrapMode,
1692        spacing: LetterSpacing,
1693        family: Option<&str>,
1694        style: FontStyle,
1695        extra_hash_for: &dyn Fn(usize) -> u64,
1696    ) -> Vec<Rc<ShapedLine>>
1697    where
1698        F: Fn(usize) -> Option<String>,
1699    {
1700        let mut out = Vec::with_capacity(range.end.saturating_sub(range.start));
1701        for line in range {
1702            if let Some(text) = get_line(line) {
1703                let extra = extra_hash_for(line);
1704                out.push(self.shape_line_full(
1705                    line, &text, size, weight, width, mode, spacing, family, style, extra,
1706                ));
1707            }
1708        }
1709        out
1710    }
1711
1712    #[allow(clippy::too_many_arguments)]
1713    pub fn shape_styled_line(
1714        &mut self,
1715        line: usize,
1716        line_text: &str,
1717        line_byte_start: usize,
1718        spans: &[StyledSpan],
1719        size: f32,
1720        weight: FontWeight,
1721        width: f32,
1722        mode: WrapMode,
1723        spacing: LetterSpacing,
1724        family: Option<&str>,
1725        style: FontStyle,
1726    ) -> Rc<ShapedLine> {
1727        use std::collections::hash_map::DefaultHasher;
1728        use std::hash::Hasher;
1729
1730        let extra = if spans.iter().all(|s| s.is_empty()) {
1731            0
1732        } else {
1733            let mut h = DefaultHasher::new();
1734            hash_spans_rebased(spans, line_byte_start, &mut h);
1735            h.finish()
1736        };
1737        self.shape_line_full(
1738            line, line_text, size, weight, width, mode, spacing, family, style, extra,
1739        )
1740    }
1741
1742    #[allow(clippy::too_many_arguments)]
1743    pub fn shape_lines_visible_buffer(
1744        &mut self,
1745        buf: &StyledBuffer,
1746        range: std::ops::Range<usize>,
1747        size: f32,
1748        weight: FontWeight,
1749        width: f32,
1750        mode: WrapMode,
1751        spacing: LetterSpacing,
1752        family: Option<&str>,
1753        style: FontStyle,
1754    ) -> Vec<Rc<ShapedLine>> {
1755        let line_count = buf.lines().line_count();
1756        let end = range.end.min(line_count);
1757        let mut out = Vec::with_capacity(end.saturating_sub(range.start));
1758        for line in range.start..end {
1759            let (s, e) = buf.lines().line_range(line);
1760            let text = buf.substring(s..e);
1761            let spans = buf.spans_for_line(s..e);
1762            out.push(self.shape_styled_line(
1763                line, &text, s, &spans, size, weight, width, mode, spacing, family, style,
1764            ));
1765        }
1766        out
1767    }
1768
1769    pub fn shape_rich_from_styled(
1770        &mut self,
1771        line_text: &str,
1772        line_byte_start: usize,
1773        styled: &[StyledSpan],
1774        size: f32,
1775        width: f32,
1776        mode: WrapMode,
1777        spacing: f32,
1778        align: rich::RichAlign,
1779    ) -> Rc<rich::RichParagraph> {
1780        let spans = styled_spans_to_text_spans(line_text, line_byte_start, styled);
1781        self.shape_rich_full(&spans, size, width, mode, spacing, align)
1782    }
1783
1784    pub fn invalidate_lines(&mut self, edited: std::ops::Range<usize>) {
1785        if edited.is_empty() {
1786            return;
1787        }
1788        let mut dropped: Vec<LineCacheKey> = Vec::new();
1789        self.line_to_key.retain(|(ln, _), key| {
1790            if edited.contains(ln) {
1791                dropped.push(*key);
1792                false
1793            } else {
1794                true
1795            }
1796        });
1797        for key in dropped {
1798            let still_used = self.line_to_key.values().any(|k| k == &key);
1799            if !still_used {
1800                self.lines.remove(&key);
1801                if let Some(pos) = self.lines_order.iter().position(|k| k == &key) {
1802                    self.lines_order.remove(pos);
1803                }
1804            }
1805        }
1806    }
1807
1808    pub fn invalidate_for_edit(
1809        &mut self,
1810        index: &LineIndex,
1811        edit_byte_start: usize,
1812        edit_byte_end: usize,
1813    ) {
1814        let a = index.line_of(edit_byte_start);
1815        let b = index.line_of(edit_byte_end);
1816        let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
1817        self.invalidate_lines(lo..hi.saturating_add(1));
1818    }
1819
1820    pub fn apply_edit_invalidate(
1821        &mut self,
1822        edit_byte_start: usize,
1823        edit_byte_end: usize,
1824        line_of: &dyn Fn(usize) -> usize,
1825    ) {
1826        let a = line_of(edit_byte_start);
1827        let b = line_of(edit_byte_end);
1828        let (lo, hi) = if a <= b { (a, b) } else { (b, a) };
1829        self.invalidate_lines(lo..hi.saturating_add(1));
1830    }
1831
1832    pub fn line_reshapes(&self) -> u64 {
1833        self.line_reshapes
1834    }
1835
1836    pub fn reset_line_reshapes(&mut self) {
1837        self.line_reshapes = 0;
1838    }
1839
1840    pub fn line_cache_len(&self) -> usize {
1841        self.lines.len()
1842    }
1843
1844    fn shape_line_uncached(
1845        &mut self,
1846        text: &str,
1847        size: f32,
1848        weight: FontWeight,
1849        family: &str,
1850        style: FontStyle,
1851        wrap_width: f32,
1852        wrap_mode: WrapMode,
1853        spacing: f32,
1854        hash: u64,
1855        width_q: u32,
1856    ) -> Rc<ShapedLine> {
1857        let spacing = sanitize_spacing(spacing);
1858        let scale = self.scale;
1859        let px = size * scale;
1860        let line_h_px = px * 1.35;
1861        let mut buffer = self
1862            .shape_scratch
1863            .take()
1864            .unwrap_or_else(|| Buffer::new(&mut self.font_system, Metrics::new(px, line_h_px)));
1865        buffer.set_metrics(Metrics::new(px, line_h_px));
1866        buffer.set_wrap(wrap_mode.cosmic());
1867        buffer.set_size(
1868            if wrap_width > 0.0 {
1869                Some(wrap_width * scale)
1870            } else {
1871                None
1872            },
1873            None,
1874        );
1875        let mut attrs = Attrs::new()
1876            .weight(Weight(weight.value()))
1877            .style(style.cosmic());
1878        if !family.is_empty() {
1879            attrs = attrs.family(Family::Name(family));
1880        }
1881
1882        debug_assert!(!text.contains('\n'), "shape_line: single logical line");
1883        let single = text.replace('\n', " ");
1884        buffer.set_text(&single, &attrs, Shaping::Advanced, None);
1885        buffer.shape_until_scroll(&mut self.font_system, false);
1886
1887        let sp_px = spacing * scale;
1888        let mut glyphs = Vec::new();
1889        let mut rows: Vec<LineRow> = Vec::new();
1890        let mut width_px = 0.0f32;
1891        let mut height_px = 0.0f32;
1892        let mut first_top: Option<f32> = None;
1893        let mut first_y: Option<f32> = None;
1894        for run in buffer.layout_runs() {
1895            let base_top = *first_top.get_or_insert(run.line_top);
1896            let base_y = *first_y.get_or_insert(run.line_y);
1897            let top = (run.line_top - base_top) / scale;
1898            let baseline = (run.line_y - base_y) / scale;
1899            let h = run.line_height / scale;
1900            let extra = sp_px * run.glyphs.len().saturating_sub(1) as f32;
1901
1902            let w_phys = run.line_w + extra;
1903            let w = w_phys / scale;
1904            width_px = width_px.max(w_phys);
1905            height_px = (top + h).max(height_px);
1906            rows.push(LineRow {
1907                top,
1908                height: h,
1909                baseline,
1910                width: w,
1911            });
1912            for (j, g) in run.glyphs.iter().enumerate() {
1913                let p = g.physical((0.0, 0.0), 1.0);
1914                let x_offset = g.font_size * g.x_offset;
1915                let y_offset = g.font_size * g.y_offset;
1916                glyphs.push(ShapedGlyph {
1917                    x: g.x + x_offset + j as f32 * sp_px,
1918                    y: (run.line_y - base_y) + (g.y - y_offset),
1919                    key: p.cache_key,
1920                });
1921            }
1922        }
1923        if rows.is_empty() {
1924            rows.push(LineRow {
1925                top: 0.0,
1926                height: line_h_px / scale,
1927                baseline: (line_h_px / scale) * 0.5,
1928                width: 0.0,
1929            });
1930            height_px = line_h_px / scale;
1931        }
1932        let out = Rc::new(ShapedLine {
1933            glyphs,
1934            rows,
1935            width: width_px / scale,
1936            height: height_px,
1937            hash,
1938            width_q,
1939        });
1940        self.shape_scratch = Some(buffer);
1941        out
1942    }
1943
1944    pub fn draw_line(
1945        &mut self,
1946        line: &ShapedLine,
1947        origin: (f32, f32),
1948        color: Color,
1949        list: &mut RenderList,
1950    ) {
1951        if line.glyphs.is_empty() {
1952            return;
1953        }
1954        let scale = self.scale;
1955        let (ox, oy) = (origin.0 * scale, origin.1 * scale);
1956        let mut placed = std::mem::take(&mut self.scratch_placed);
1957        placed.clear();
1958        for g in &line.glyphs {
1959            let x = ox + g.x;
1960            let y = if self.subpixel_y {
1961                oy + g.y
1962            } else {
1963                (oy + g.y).trunc()
1964            };
1965            let (key, x_int, y_int) = CacheKey::new(
1966                g.key.font_id,
1967                g.key.glyph_id,
1968                f32::from_bits(g.key.font_size_bits),
1969                (x, y),
1970                g.key.font_weight,
1971                g.key.flags,
1972            );
1973            if self.atlas.entry(&key).is_none() {
1974                self.rasterize(key);
1975            }
1976            placed.push((key, x_int, y_int));
1977        }
1978        let (aw, ah) = (self.atlas.width as f32, self.atlas.height as f32);
1979        let mut quads = Vec::with_capacity(placed.len());
1980        for &(key, x_int, y_int) in placed.iter() {
1981            let Some(entry) = self.atlas.entry(&key) else {
1982                continue;
1983            };
1984            let [rx, ry, rw, rh] = entry.rect;
1985            if rw == 0 || rh == 0 {
1986                continue;
1987            }
1988            let x = (x_int + entry.left) as f32 / scale;
1989            let y = (y_int - entry.top) as f32 / scale;
1990            quads.push(GlyphQuad {
1991                rect: Rect::from_xywh(x, y, rw as f32 / scale, rh as f32 / scale),
1992                uv: [
1993                    rx as f32 / aw,
1994                    ry as f32 / ah,
1995                    (rx + rw) as f32 / aw,
1996                    (ry + rh) as f32 / ah,
1997                ],
1998                color_glyph: entry.color,
1999            });
2000        }
2001        self.scratch_placed = placed;
2002        list.push_text(TextDraw {
2003            color,
2004            glyphs: quads,
2005        });
2006    }
2007
2008    pub fn text_width(&mut self, text: &str, size: f32) -> f32 {
2009        self.shape(text, size).width()
2010    }
2011
2012    pub fn text_width_weighted(&mut self, text: &str, size: f32, weight: FontWeight) -> f32 {
2013        self.shape_weighted(text, size, weight).width()
2014    }
2015
2016    pub fn text_width_spaced(&mut self, text: &str, size: f32, spacing: LetterSpacing) -> f32 {
2017        self.shape_spaced(text, size, FontWeight::NORMAL, spacing)
2018            .width()
2019    }
2020
2021    pub fn text_width_weighted_spaced(
2022        &mut self,
2023        text: &str,
2024        size: f32,
2025        weight: FontWeight,
2026        spacing: LetterSpacing,
2027    ) -> f32 {
2028        self.shape_spaced(text, size, weight, spacing).width()
2029    }
2030
2031    pub fn text_width_styled_spaced(
2032        &mut self,
2033        text: &str,
2034        size: f32,
2035        weight: FontWeight,
2036        spacing: LetterSpacing,
2037        family: Option<&str>,
2038        style: FontStyle,
2039    ) -> f32 {
2040        self.shape_spaced_styled(text, size, weight, spacing, family, style)
2041            .width()
2042    }
2043
2044    pub fn caret_positions(&mut self, text: &str, size: f32, weight: FontWeight) -> Vec<f32> {
2045        self.caret_positions_spaced(text, size, weight, 0.0)
2046    }
2047
2048    pub fn caret_positions_spaced(
2049        &mut self,
2050        text: &str,
2051        size: f32,
2052        weight: FontWeight,
2053        spacing: LetterSpacing,
2054    ) -> Vec<f32> {
2055        self.caret_positions_styled_spaced(text, size, weight, spacing, None, FontStyle::Normal)
2056    }
2057
2058    pub fn caret_positions_styled_spaced(
2059        &mut self,
2060        text: &str,
2061        size: f32,
2062        weight: FontWeight,
2063        spacing: LetterSpacing,
2064        family: Option<&str>,
2065        style: FontStyle,
2066    ) -> Vec<f32> {
2067        let eff_family = self.effective_family(family);
2068        let weight = self.resolve_weight_for(weight, &eff_family);
2069        let spacing = sanitize_spacing(spacing);
2070        let key: CaretKey = (
2071            text.to_owned(),
2072            (size * 4.0).round() as u32,
2073            weight.value(),
2074            eff_family.clone(),
2075            style.key(),
2076            self.scale.to_bits(),
2077            spacing_key(spacing),
2078        );
2079        if let Some(rc) = self.carets.get(&key) {
2080            if let Some(pos) = self.carets_order.iter().position(|k| k == &key) {
2081                self.carets_order.remove(pos);
2082            }
2083            self.carets_order.push_front(key);
2084            return rc.to_vec();
2085        }
2086        let out =
2087            self.caret_positions_uncached_styled(text, size, weight, &eff_family, style, spacing);
2088        self.carets.insert(key.clone(), Rc::from(out.clone()));
2089        self.carets_order.push_front(key);
2090        while self.carets.len() > CARET_CACHE_MAX {
2091            if let Some(oldest) = self.carets_order.pop_back() {
2092                self.carets.remove(&oldest);
2093            } else {
2094                break;
2095            }
2096        }
2097        out
2098    }
2099
2100    fn caret_positions_uncached_styled(
2101        &mut self,
2102        text: &str,
2103        size: f32,
2104        weight: FontWeight,
2105        family: &str,
2106        style: FontStyle,
2107        spacing: f32,
2108    ) -> Vec<f32> {
2109        let char_count = text.chars().count();
2110        if text.is_empty() {
2111            return vec![0.0];
2112        }
2113
2114        let mut byte_offsets: Vec<usize> = Vec::with_capacity(char_count + 1);
2115        byte_offsets.push(0);
2116        for (i, _) in text.char_indices() {
2117            if i != 0 {
2118                byte_offsets.push(i);
2119            }
2120        }
2121        byte_offsets.push(text.len());
2122        debug_assert_eq!(byte_offsets.len(), char_count + 1);
2123
2124        let scale = self.scale;
2125        let px = size * scale;
2126        let mut buffer = self
2127            .shape_scratch
2128            .take()
2129            .unwrap_or_else(|| Buffer::new(&mut self.font_system, Metrics::new(px, px * 1.35)));
2130        buffer.set_metrics(Metrics::new(px, px * 1.35));
2131        buffer.set_wrap(cosmic_text::Wrap::None);
2132        buffer.set_size(None, None);
2133        let mut attrs = Attrs::new()
2134            .weight(Weight(weight.value()))
2135            .style(style.cosmic());
2136        if !family.is_empty() {
2137            attrs = attrs.family(Family::Name(family));
2138        }
2139        buffer.set_text(text, &attrs, Shaping::Advanced, None);
2140        buffer.shape_until_scroll(&mut self.font_system, false);
2141
2142        let mut out = vec![0.0f32; char_count + 1];
2143        let mut line_w_px = 0.0f32;
2144        for run in buffer.layout_runs() {
2145            line_w_px = line_w_px.max(run.line_w);
2146            for g in run.glyphs {
2147                let gx = g.x;
2148                let gw = g.w;
2149
2150                for (ci, &bo) in byte_offsets.iter().enumerate() {
2151                    if bo >= g.end {
2152                        let end_x = gx + gw;
2153                        if end_x / scale > out[ci] {
2154                            out[ci] = end_x / scale;
2155                        }
2156                    } else if bo == g.start && gx / scale > out[ci] {
2157                        out[ci] = gx / scale;
2158                    }
2159                }
2160            }
2161        }
2162
2163        let total = line_w_px / scale;
2164        let mut prev = 0.0f32;
2165        for v in out.iter_mut() {
2166            if *v < prev {
2167                *v = prev;
2168            }
2169            if *v > total {
2170                *v = total;
2171            }
2172            prev = *v;
2173        }
2174        if let Some(last) = out.last_mut() {
2175            *last = total;
2176        }
2177
2178        if spacing != 0.0 && char_count > 0 {
2179            let n = char_count;
2180            for (i, v) in out.iter_mut().enumerate() {
2181                if i < n {
2182                    *v += spacing * i as f32;
2183                }
2184            }
2185            let width = (total + spacing * n.saturating_sub(1) as f32).max(0.0);
2186            if let Some(last) = out.last_mut() {
2187                *last = width;
2188            }
2189            let mut prev = 0.0f32;
2190            for v in out.iter_mut().take(n) {
2191                if *v < prev {
2192                    *v = prev;
2193                }
2194                prev = *v;
2195            }
2196            if let Some(last) = out.last_mut() {
2197                *last = width;
2198            }
2199        }
2200        self.shape_scratch = Some(buffer);
2201        out
2202    }
2203
2204    pub fn caret_x(&mut self, text: &str, size: f32, weight: FontWeight, char_index: usize) -> f32 {
2205        self.caret_x_spaced(text, size, weight, char_index, 0.0)
2206    }
2207
2208    pub fn caret_x_spaced(
2209        &mut self,
2210        text: &str,
2211        size: f32,
2212        weight: FontWeight,
2213        char_index: usize,
2214        spacing: LetterSpacing,
2215    ) -> f32 {
2216        self.caret_x_styled_spaced(
2217            text,
2218            size,
2219            weight,
2220            char_index,
2221            spacing,
2222            None,
2223            FontStyle::Normal,
2224        )
2225    }
2226
2227    pub fn caret_x_styled_spaced(
2228        &mut self,
2229        text: &str,
2230        size: f32,
2231        weight: FontWeight,
2232        char_index: usize,
2233        spacing: LetterSpacing,
2234        family: Option<&str>,
2235        style: FontStyle,
2236    ) -> f32 {
2237        let pos = self.caret_positions_styled_spaced(text, size, weight, spacing, family, style);
2238        pos.get(char_index.min(pos.len() - 1))
2239            .copied()
2240            .unwrap_or(0.0)
2241    }
2242
2243    pub fn char_index_from_x(
2244        &mut self,
2245        text: &str,
2246        size: f32,
2247        weight: FontWeight,
2248        x: f32,
2249    ) -> usize {
2250        self.char_index_from_x_spaced(text, size, weight, x, 0.0)
2251    }
2252
2253    pub fn char_index_from_x_spaced(
2254        &mut self,
2255        text: &str,
2256        size: f32,
2257        weight: FontWeight,
2258        x: f32,
2259        spacing: LetterSpacing,
2260    ) -> usize {
2261        self.char_index_from_x_styled_spaced(
2262            text,
2263            size,
2264            weight,
2265            x,
2266            spacing,
2267            None,
2268            FontStyle::Normal,
2269        )
2270    }
2271
2272    pub fn char_index_from_x_styled_spaced(
2273        &mut self,
2274        text: &str,
2275        size: f32,
2276        weight: FontWeight,
2277        x: f32,
2278        spacing: LetterSpacing,
2279        family: Option<&str>,
2280        style: FontStyle,
2281    ) -> usize {
2282        let pos = self.caret_positions_styled_spaced(text, size, weight, spacing, family, style);
2283        if pos.len() <= 1 {
2284            return 0;
2285        }
2286        if x <= 0.0 {
2287            return 0;
2288        }
2289        if x >= *pos.last().unwrap_or(&0.0) {
2290            return pos.len() - 1;
2291        }
2292        let mut best = 0;
2293        let mut best_d = f32::MAX;
2294        for (i, &px) in pos.iter().enumerate() {
2295            let d = (px - x).abs();
2296            if d < best_d {
2297                best_d = d;
2298                best = i;
2299            }
2300        }
2301        best
2302    }
2303
2304    pub fn truncate(
2305        &mut self,
2306        text: &str,
2307        size: f32,
2308        weight: FontWeight,
2309        max_width: f32,
2310    ) -> String {
2311        self.truncate_spaced(text, size, weight, max_width, 0.0)
2312    }
2313
2314    pub fn truncate_spaced(
2315        &mut self,
2316        text: &str,
2317        size: f32,
2318        weight: FontWeight,
2319        max_width: f32,
2320        spacing: LetterSpacing,
2321    ) -> String {
2322        self.truncate_styled_spaced(
2323            text,
2324            size,
2325            weight,
2326            max_width,
2327            spacing,
2328            None,
2329            FontStyle::Normal,
2330        )
2331    }
2332
2333    pub fn truncate_styled_spaced(
2334        &mut self,
2335        text: &str,
2336        size: f32,
2337        weight: FontWeight,
2338        max_width: f32,
2339        spacing: LetterSpacing,
2340        family: Option<&str>,
2341        style: FontStyle,
2342    ) -> String {
2343        let spacing = sanitize_spacing(spacing);
2344        if max_width <= 0.0 || text.is_empty() {
2345            return String::new();
2346        }
2347        if self.measure_width_styled_spaced(text, size, weight, spacing, family, style) <= max_width
2348        {
2349            return text.to_owned();
2350        }
2351        let chars: Vec<char> = text.chars().collect();
2352        let mut lo = 0usize;
2353        let mut hi = chars.len();
2354        while lo < hi {
2355            let mid = (lo + hi).div_ceil(2);
2356            let prefix: String = chars[..mid].iter().collect();
2357            if self.measure_width_styled_spaced(&prefix, size, weight, spacing, family, style)
2358                <= max_width
2359            {
2360                lo = mid;
2361            } else {
2362                hi = mid - 1;
2363            }
2364        }
2365        chars[..lo].iter().collect()
2366    }
2367
2368    pub fn elide(
2369        &mut self,
2370        text: &str,
2371        size: f32,
2372        weight: FontWeight,
2373        max_width: f32,
2374        mode: ElideMode,
2375    ) -> String {
2376        self.elide_spaced(text, size, weight, max_width, mode, 0.0)
2377    }
2378
2379    pub fn elide_spaced(
2380        &mut self,
2381        text: &str,
2382        size: f32,
2383        weight: FontWeight,
2384        max_width: f32,
2385        mode: ElideMode,
2386        spacing: LetterSpacing,
2387    ) -> String {
2388        self.elide_styled_spaced(
2389            text,
2390            size,
2391            weight,
2392            max_width,
2393            mode,
2394            spacing,
2395            None,
2396            FontStyle::Normal,
2397        )
2398    }
2399
2400    pub fn elide_styled_spaced(
2401        &mut self,
2402        text: &str,
2403        size: f32,
2404        weight: FontWeight,
2405        max_width: f32,
2406        mode: ElideMode,
2407        spacing: LetterSpacing,
2408        family: Option<&str>,
2409        style: FontStyle,
2410    ) -> String {
2411        let spacing = sanitize_spacing(spacing);
2412        if max_width <= 0.0 || text.is_empty() {
2413            return String::new();
2414        }
2415        if self.measure_width_styled_spaced(text, size, weight, spacing, family, style) <= max_width
2416        {
2417            return text.to_owned();
2418        }
2419        let ell = ELLIPSIS.to_string();
2420        if self.measure_width_styled_spaced(&ell, size, weight, spacing, family, style) > max_width
2421        {
2422            return ell;
2423        }
2424        let chars: Vec<char> = text.chars().collect();
2425        let mut fits = |s: &str| {
2426            self.measure_width_styled_spaced(s, size, weight, spacing, family, style) <= max_width
2427        };
2428        match mode {
2429            ElideMode::Right => {
2430                let mut lo = 0usize;
2431                let mut hi = chars.len();
2432                while lo < hi {
2433                    let mid = (lo + hi).div_ceil(2);
2434                    let cand: String = chars[..mid].iter().collect::<String>() + &ell;
2435                    if fits(&cand) {
2436                        lo = mid;
2437                    } else {
2438                        hi = mid - 1;
2439                    }
2440                }
2441                chars[..lo].iter().collect::<String>() + &ell
2442            }
2443            ElideMode::Left => {
2444                let mut lo = 0usize;
2445                let mut hi = chars.len();
2446                while lo < hi {
2447                    let mid = (lo + hi).div_ceil(2);
2448                    let cand: String =
2449                        ell.clone() + &chars[chars.len() - mid..].iter().collect::<String>();
2450                    if fits(&cand) {
2451                        lo = mid;
2452                    } else {
2453                        hi = mid - 1;
2454                    }
2455                }
2456                ell + &chars[chars.len() - lo..].iter().collect::<String>()
2457            }
2458            ElideMode::Middle => {
2459                let mut lo = 0usize;
2460                let mut hi = chars.len();
2461                while lo < hi {
2462                    let mid = (lo + hi).div_ceil(2);
2463                    let front = mid.div_ceil(2);
2464                    let back = mid / 2;
2465                    let cand: String = chars[..front].iter().collect::<String>()
2466                        + &ell
2467                        + &chars[chars.len() - back..].iter().collect::<String>();
2468                    if fits(&cand) {
2469                        lo = mid;
2470                    } else {
2471                        hi = mid - 1;
2472                    }
2473                }
2474                let front = lo.div_ceil(2);
2475                let back = lo / 2;
2476                chars[..front].iter().collect::<String>()
2477                    + &ell
2478                    + &chars[chars.len() - back..].iter().collect::<String>()
2479            }
2480        }
2481    }
2482
2483    /// Returns the width of the longest unbreakable unit for the wrap mode.
2484    ///
2485    /// For word wrapping this is the longest whitespace-separated word; for
2486    /// character wrapping it is the widest single character.
2487    #[allow(clippy::too_many_arguments)]
2488    pub fn min_content_width_styled_spaced(
2489        &mut self,
2490        text: &str,
2491        size: f32,
2492        weight: FontWeight,
2493        spacing: LetterSpacing,
2494        family: Option<&str>,
2495        style: FontStyle,
2496        mode: WrapMode,
2497    ) -> f32 {
2498        match mode {
2499            WrapMode::Character => text
2500                .chars()
2501                .map(|ch| {
2502                    let mut buf = [0u8; 4];
2503                    self.measure_width_styled_spaced(
2504                        ch.encode_utf8(&mut buf),
2505                        size,
2506                        weight,
2507                        spacing,
2508                        family,
2509                        style,
2510                    )
2511                })
2512                .fold(0.0_f32, f32::max),
2513            _ => text
2514                .split_whitespace()
2515                .map(|word| {
2516                    self.measure_width_styled_spaced(word, size, weight, spacing, family, style)
2517                })
2518                .fold(0.0_f32, f32::max),
2519        }
2520    }
2521
2522    /// Elides wrapped text to at most `max_lines`, appending an ellipsis to the
2523    /// last visible line when content is cut.
2524    #[allow(clippy::too_many_arguments)]
2525    pub fn elide_lines_styled_spaced(
2526        &mut self,
2527        text: &str,
2528        size: f32,
2529        weight: FontWeight,
2530        width: f32,
2531        mode: WrapMode,
2532        spacing: LetterSpacing,
2533        family: Option<&str>,
2534        style: FontStyle,
2535        line_height: LineHeight,
2536        max_lines: usize,
2537    ) -> String {
2538        if text.is_empty() || max_lines == 0 || width <= 0.0 {
2539            return String::new();
2540        }
2541        let full = self.shape_wrapped_styled_spaced_lh(
2542            text,
2543            size,
2544            weight,
2545            width,
2546            mode,
2547            spacing,
2548            family,
2549            style,
2550            line_height,
2551        );
2552        if full.line_count() <= max_lines {
2553            return text.to_owned();
2554        }
2555        let chars: Vec<char> = text.chars().collect();
2556        let ell = ELLIPSIS.to_string();
2557        let mut lo = 0usize;
2558        let mut hi = chars.len();
2559        while lo < hi {
2560            let mid = (lo + hi).div_ceil(2);
2561            let candidate: String = chars[..mid].iter().collect::<String>() + &ell;
2562            let shaped = self.shape_wrapped_styled_spaced_lh(
2563                &candidate,
2564                size,
2565                weight,
2566                width,
2567                mode,
2568                spacing,
2569                family,
2570                style,
2571                line_height,
2572            );
2573            if shaped.line_count() <= max_lines {
2574                lo = mid;
2575            } else {
2576                hi = mid - 1;
2577            }
2578        }
2579        chars[..lo].iter().collect::<String>() + &ell
2580    }
2581
2582    fn measure_width_styled_spaced(
2583        &mut self,
2584        text: &str,
2585        size: f32,
2586        weight: FontWeight,
2587        spacing: f32,
2588        family: Option<&str>,
2589        style: FontStyle,
2590    ) -> f32 {
2591        if text.is_empty() {
2592            return 0.0;
2593        }
2594        let eff_family = self.effective_family(family);
2595        let weight = self.resolve_weight_for(weight, &eff_family);
2596        let spacing = sanitize_spacing(spacing);
2597        let scale = self.scale;
2598        let px = size * scale;
2599        let mut buffer = Buffer::new(&mut self.font_system, Metrics::new(px, px * 1.35));
2600        let mut attrs = Attrs::new()
2601            .weight(Weight(weight.value()))
2602            .style(style.cosmic());
2603        if !eff_family.is_empty() {
2604            attrs = attrs.family(Family::Name(&eff_family));
2605        }
2606        buffer.set_text(text, &attrs, Shaping::Advanced, None);
2607        buffer.shape_until_scroll(&mut self.font_system, false);
2608        let sp_px = spacing * scale;
2609        buffer
2610            .layout_runs()
2611            .map(|r| r.line_w + sp_px * r.glyphs.len().saturating_sub(1) as f32)
2612            .fold(0.0f32, f32::max)
2613            / scale
2614    }
2615
2616    #[allow(clippy::too_many_arguments)]
2617    fn shape_uncached_styled(
2618        &mut self,
2619        text: &str,
2620        size: f32,
2621        weight: FontWeight,
2622        family: &str,
2623        style: FontStyle,
2624        wrap_width: f32,
2625        wrap_mode: WrapMode,
2626        spacing: f32,
2627        line_height: LineHeight,
2628    ) -> Rc<ShapedText> {
2629        let spacing = sanitize_spacing(spacing);
2630        let scale = self.scale;
2631        let px = size * scale;
2632        let line_h = line_height.resolve(size).max(1.0);
2633        let line_h_px = line_h * scale;
2634        let mut buffer = self
2635            .shape_scratch
2636            .take()
2637            .unwrap_or_else(|| Buffer::new(&mut self.font_system, Metrics::new(px, line_h_px)));
2638        buffer.set_metrics(Metrics::new(px, line_h_px));
2639        buffer.set_wrap(wrap_mode.cosmic());
2640        buffer.set_size(
2641            if wrap_width > 0.0 {
2642                Some(wrap_width * scale)
2643            } else {
2644                None
2645            },
2646            None,
2647        );
2648        let mut attrs = Attrs::new()
2649            .weight(Weight(weight.value()))
2650            .style(style.cosmic());
2651        if !family.is_empty() {
2652            attrs = attrs.family(Family::Name(family));
2653        }
2654        buffer.set_text(text, &attrs, Shaping::Advanced, None);
2655        buffer.shape_until_scroll(&mut self.font_system, false);
2656
2657        let sp_px = spacing * scale;
2658        let mut glyphs = Vec::new();
2659        let mut rows = Vec::new();
2660        let mut width_px = 0.0_f32;
2661        let mut height_px = line_h_px;
2662
2663        let mut first_line_top: Option<f32> = None;
2664        let mut first_line_y: Option<f32> = None;
2665        for run in buffer.layout_runs() {
2666            let base_top = *first_line_top.get_or_insert(run.line_top);
2667            let extra = sp_px * run.glyphs.len().saturating_sub(1) as f32;
2668            width_px = width_px.max(run.line_w + extra);
2669            height_px = height_px.max(run.line_top - base_top + run.line_height);
2670            rows.push(LineRow {
2671                top: (run.line_top - base_top) / scale,
2672                height: run.line_height / scale,
2673                baseline: (run.line_y - base_top) / scale,
2674                width: (run.line_w + extra) / scale,
2675            });
2676            let dy_base = run.line_y - *first_line_y.get_or_insert(run.line_y);
2677            for (j, g) in run.glyphs.iter().enumerate() {
2678                let p = g.physical((0.0, 0.0), 1.0);
2679                let x_offset = g.font_size * g.x_offset;
2680                let y_offset = g.font_size * g.y_offset;
2681                glyphs.push(ShapedGlyph {
2682                    x: g.x + x_offset + j as f32 * sp_px,
2683
2684                    y: dy_base + (g.y - y_offset),
2685                    key: p.cache_key,
2686                });
2687            }
2688        }
2689        let out = Rc::new(ShapedText {
2690            glyphs,
2691            rows,
2692            width: width_px / scale,
2693            height: height_px / scale,
2694            line_height: line_h,
2695        });
2696        self.shape_scratch = Some(buffer);
2697        out
2698    }
2699
2700    pub fn draw(
2701        &mut self,
2702        shaped: &ShapedText,
2703        origin: (f32, f32),
2704        color: Color,
2705        list: &mut RenderList,
2706    ) {
2707        if shaped.glyphs.is_empty() {
2708            return;
2709        }
2710        let scale = self.scale;
2711        let (ox, oy) = (origin.0 * scale, origin.1 * scale);
2712
2713        let mut placed = std::mem::take(&mut self.scratch_placed);
2714        placed.clear();
2715        for g in &shaped.glyphs {
2716            let x = ox + g.x;
2717
2718            let y = if self.subpixel_y {
2719                oy + g.y
2720            } else {
2721                (oy + g.y).trunc()
2722            };
2723            let (key, x_int, y_int) = CacheKey::new(
2724                g.key.font_id,
2725                g.key.glyph_id,
2726                f32::from_bits(g.key.font_size_bits),
2727                (x, y),
2728                g.key.font_weight,
2729                g.key.flags,
2730            );
2731            if self.atlas.entry(&key).is_none() {
2732                self.rasterize(key);
2733            }
2734            placed.push((key, x_int, y_int));
2735        }
2736
2737        let (aw, ah) = (self.atlas.width as f32, self.atlas.height as f32);
2738        let mut quads = Vec::with_capacity(placed.len());
2739        for &(key, x_int, y_int) in placed.iter() {
2740            let Some(entry) = self.atlas.entry(&key) else {
2741                continue;
2742            };
2743            let [rx, ry, rw, rh] = entry.rect;
2744            if rw == 0 || rh == 0 {
2745                continue;
2746            }
2747
2748            let x = (x_int + entry.left) as f32 / scale;
2749            let y = (y_int - entry.top) as f32 / scale;
2750            quads.push(GlyphQuad {
2751                rect: Rect::from_xywh(x, y, rw as f32 / scale, rh as f32 / scale),
2752                uv: [
2753                    rx as f32 / aw,
2754                    ry as f32 / ah,
2755                    (rx + rw) as f32 / aw,
2756                    (ry + rh) as f32 / ah,
2757                ],
2758                color_glyph: entry.color,
2759            });
2760        }
2761
2762        self.scratch_placed = placed;
2763        list.push_text(TextDraw {
2764            color,
2765            glyphs: quads,
2766        });
2767    }
2768
2769    pub fn draw_sprite(
2770        &mut self,
2771        key: u64,
2772        w: u32,
2773        h: u32,
2774        alpha: &[u8],
2775        dst: Rect,
2776        color: Color,
2777        list: &mut RenderList,
2778    ) {
2779        if self.atlas.sprite(key).is_none() {
2780            self.atlas.insert_sprite(key, w, h, alpha);
2781        }
2782        let Some(entry) = self.atlas.sprite(key) else {
2783            return;
2784        };
2785        let [rx, ry, rw, rh] = entry.rect;
2786        if rw == 0 || rh == 0 {
2787            return;
2788        }
2789        let scale = self.scale;
2790        let rect = Rect::from_xywh(
2791            (dst.left() * scale).round() / scale,
2792            (dst.top() * scale).round() / scale,
2793            rw as f32 / scale,
2794            rh as f32 / scale,
2795        );
2796        let (aw, ah) = (self.atlas.width as f32, self.atlas.height as f32);
2797        list.push_text(TextDraw {
2798            color,
2799            glyphs: vec![GlyphQuad {
2800                rect,
2801                uv: [
2802                    rx as f32 / aw,
2803                    ry as f32 / ah,
2804                    (rx + rw) as f32 / aw,
2805                    (ry + rh) as f32 / ah,
2806                ],
2807                color_glyph: false,
2808            }],
2809        });
2810    }
2811
2812    pub fn shape_rich(&mut self, spans: &[rich::TextSpan], size: f32) -> Rc<rich::RichParagraph> {
2813        self.shape_rich_full(
2814            spans,
2815            size,
2816            0.0,
2817            WrapMode::default(),
2818            0.0,
2819            rich::RichAlign::Left,
2820        )
2821    }
2822
2823    pub fn shape_rich_wrapped(
2824        &mut self,
2825        spans: &[rich::TextSpan],
2826        size: f32,
2827        width: f32,
2828        mode: WrapMode,
2829    ) -> Rc<rich::RichParagraph> {
2830        self.shape_rich_full(spans, size, width, mode, 0.0, rich::RichAlign::Left)
2831    }
2832
2833    pub fn shape_rich_aligned(
2834        &mut self,
2835        spans: &[rich::TextSpan],
2836        size: f32,
2837        width: f32,
2838        mode: WrapMode,
2839        align: rich::RichAlign,
2840    ) -> Rc<rich::RichParagraph> {
2841        self.shape_rich_full(spans, size, width, mode, 0.0, align)
2842    }
2843
2844    pub fn shape_rich_full(
2845        &mut self,
2846        spans: &[rich::TextSpan],
2847        size: f32,
2848        width: f32,
2849        mode: WrapMode,
2850        spacing: f32,
2851        align: rich::RichAlign,
2852    ) -> Rc<rich::RichParagraph> {
2853        self.shape_rich_full_lh(
2854            spans,
2855            size,
2856            width,
2857            mode,
2858            spacing,
2859            align,
2860            LineHeight::default(),
2861        )
2862    }
2863
2864    #[allow(clippy::too_many_arguments)]
2865    pub fn shape_rich_full_lh(
2866        &mut self,
2867        spans: &[rich::TextSpan],
2868        size: f32,
2869        width: f32,
2870        mode: WrapMode,
2871        spacing: f32,
2872        align: rich::RichAlign,
2873        line_height: LineHeight,
2874    ) -> Rc<rich::RichParagraph> {
2875        let spacing = sanitize_spacing(spacing);
2876        let key = rich::RichKey::new(spans, size, width, mode.key(), spacing, align, line_height);
2877        if let Some(rc) = self.rich.get(&key) {
2878            if let Some(pos) = self.rich_order.iter().position(|k| k == &key) {
2879                self.rich_order.remove(pos);
2880            }
2881            self.rich_order.push_front(key);
2882            return rc.clone();
2883        }
2884        let shaped =
2885            self.shape_rich_uncached(spans, size, width, mode, spacing, align, line_height);
2886        self.rich.insert(key.clone(), shaped.clone());
2887        self.rich_order.push_front(key);
2888        while self.rich.len() > rich::RICH_CACHE_MAX {
2889            if let Some(oldest) = self.rich_order.pop_back() {
2890                self.rich.remove(&oldest);
2891            } else {
2892                break;
2893            }
2894        }
2895        shaped
2896    }
2897
2898    /// Elides rich text to at most `max_lines`, appending an ellipsis to the
2899    /// last visible line when content is cut.
2900    #[allow(clippy::too_many_arguments)]
2901    pub fn elide_rich_lines(
2902        &mut self,
2903        spans: &[rich::TextSpan],
2904        size: f32,
2905        width: f32,
2906        mode: WrapMode,
2907        spacing: f32,
2908        align: rich::RichAlign,
2909        line_height: LineHeight,
2910        max_lines: usize,
2911    ) -> Vec<rich::TextSpan> {
2912        if max_lines == 0 || width <= 0.0 || spans.iter().all(|s| s.text.is_empty()) {
2913            return Vec::new();
2914        }
2915        let full = self.shape_rich_full_lh(spans, size, width, mode, spacing, align, line_height);
2916        if full.line_count() <= max_lines {
2917            return spans.to_vec();
2918        }
2919        let char_count = full.len_chars();
2920        let mut lo = 0usize;
2921        let mut hi = char_count;
2922        while lo < hi {
2923            let mid = (lo + hi).div_ceil(2);
2924            let mut candidate = rich::truncate_spans(spans, mid);
2925            rich::append_ellipsis(&mut candidate);
2926            let shaped =
2927                self.shape_rich_full_lh(&candidate, size, width, mode, spacing, align, line_height);
2928            if shaped.line_count() <= max_lines {
2929                lo = mid;
2930            } else {
2931                hi = mid - 1;
2932            }
2933        }
2934        let mut out = rich::truncate_spans(spans, lo);
2935        rich::append_ellipsis(&mut out);
2936        out
2937    }
2938
2939    fn shape_rich_uncached(
2940        &mut self,
2941        spans: &[rich::TextSpan],
2942        size: f32,
2943        width: f32,
2944        mode: WrapMode,
2945        spacing: f32,
2946        align: rich::RichAlign,
2947        line_height: LineHeight,
2948    ) -> Rc<rich::RichParagraph> {
2949        use std::ops::Range;
2950
2951        let spacing = sanitize_spacing(spacing);
2952        let scale = self.scale;
2953        let px = size * scale;
2954        let line_h = line_height.resolve(size).max(1.0);
2955        let line_h_px = line_h * scale;
2956
2957        let mut full = String::new();
2958        let mut span_bytes: Vec<Range<usize>> = Vec::with_capacity(spans.len());
2959        let mut span_chars: Vec<Range<usize>> = Vec::with_capacity(spans.len());
2960        let mut char_count = 0usize;
2961        for span in spans {
2962            let normalized = span.text.replace("\r\n", "\n").replace('\r', "\n");
2963            let byte_start = full.len();
2964            let char_start = char_count;
2965            full.push_str(&normalized);
2966            char_count += normalized.chars().count();
2967            span_bytes.push(byte_start..full.len());
2968            span_chars.push(char_start..char_count);
2969        }
2970
2971        let mut byte_offsets: Vec<usize> = Vec::with_capacity(char_count + 1);
2972        byte_offsets.push(0);
2973        for (i, _) in full.char_indices() {
2974            if i != 0 {
2975                byte_offsets.push(i);
2976            }
2977        }
2978        byte_offsets.push(full.len());
2979
2980        let mut line_bases: Vec<usize> = vec![0];
2981        for (i, b) in full.bytes().enumerate() {
2982            if b == b'\n' {
2983                line_bases.push(i + 1);
2984            }
2985        }
2986        let byte_to_char = |byte: usize| -> usize {
2987            match byte_offsets.binary_search(&byte) {
2988                Ok(k) => k.min(char_count),
2989                Err(k) => k.saturating_sub(1).min(char_count),
2990            }
2991        };
2992
2993        let mut buffer = self
2994            .shape_scratch
2995            .take()
2996            .unwrap_or_else(|| Buffer::new(&mut self.font_system, Metrics::new(px, line_h_px)));
2997        buffer.set_metrics(Metrics::new(px, line_h_px));
2998        buffer.set_wrap(mode.cosmic());
2999        buffer.set_size(
3000            if width > 0.0 {
3001                Some(width * scale)
3002            } else {
3003                None
3004            },
3005            None,
3006        );
3007        let cosmic_align = match align {
3008            rich::RichAlign::Left => cosmic_text::Align::Left,
3009            rich::RichAlign::Center => cosmic_text::Align::Center,
3010            rich::RichAlign::Right => cosmic_text::Align::Right,
3011        };
3012
3013        let mut eff_families: Vec<String> = Vec::with_capacity(spans.len());
3014        for span in spans {
3015            eff_families.push(self.effective_family(span.family.as_deref()));
3016        }
3017        let mut attrs_list: Vec<(&str, Attrs)> = Vec::new();
3018        for (i, span) in spans.iter().enumerate() {
3019            if span_bytes[i].is_empty() {
3020                continue;
3021            }
3022            let weight = self.resolve_weight_for(span.weight.unwrap_or_default(), &eff_families[i]);
3023            let style = span.style.unwrap_or_default();
3024            let mut attrs = Attrs::new()
3025                .weight(Weight(weight.value()))
3026                .style(style.cosmic())
3027                .metadata(i);
3028            if !eff_families[i].is_empty() {
3029                attrs = attrs.family(Family::Name(&eff_families[i]));
3030            }
3031
3032            let delta = if span.size_delta.is_finite() {
3033                span.size_delta
3034            } else {
3035                0.0
3036            };
3037            if delta != 0.0 {
3038                let span_size = (size + delta).max(1.0);
3039                let px_i = span_size * scale;
3040                let span_line_h = line_height.resolve(span_size).max(1.0) * scale;
3041                attrs = attrs.metrics(Metrics::new(px_i, span_line_h));
3042            }
3043            attrs_list.push((&full[span_bytes[i].clone()], attrs));
3044        }
3045        buffer.set_rich_text(
3046            attrs_list,
3047            &Attrs::new(),
3048            Shaping::Advanced,
3049            Some(cosmic_align),
3050        );
3051        buffer.shape_until_scroll(&mut self.font_system, false);
3052
3053        let sp_px = spacing * scale;
3054        let mut glyphs: Vec<rich::RichGlyph> = Vec::new();
3055        let mut run_carets: Vec<Vec<f32>> = Vec::new();
3056        let mut lines: Vec<rich::RichLine> = Vec::new();
3057        let mut width_px: f32 = 0.0;
3058        let mut first_line_top: Option<f32> = None;
3059
3060        let mut cursor_char = 0usize;
3061
3062        for run in buffer.layout_runs() {
3063            let base_top = *first_line_top.get_or_insert(run.line_top);
3064            let top = (run.line_top - base_top) / scale;
3065            let height = run.line_height / scale;
3066            let baseline = (run.line_y - base_top) / scale;
3067            let extra = sp_px * run.glyphs.len().saturating_sub(1) as f32;
3068            width_px = width_px.max(run.line_w + extra);
3069
3070            let line_base = line_bases.get(run.line_i).copied().unwrap_or(0);
3071            let mut table: HashMap<usize, f32> = HashMap::new();
3072            let mut gmax = 0usize;
3073            let mut has_glyphs = false;
3074            for (j, g) in run.glyphs.iter().enumerate() {
3075                let s = line_base + g.start;
3076                let e = line_base + g.end;
3077                gmax = gmax.max(e);
3078                has_glyphs = true;
3079                let gx = g.x + g.font_size * g.x_offset + j as f32 * sp_px;
3080                let gw = g.w;
3081                table
3082                    .entry(s)
3083                    .and_modify(|v| *v = (*v).max(gx / scale))
3084                    .or_insert(gx / scale);
3085                table
3086                    .entry(e)
3087                    .and_modify(|v| *v = (*v).max((gx + gw) / scale))
3088                    .or_insert((gx + gw) / scale);
3089            }
3090
3091            let start_char = cursor_char;
3092            let mut end_char = if has_glyphs {
3093                byte_to_char(gmax).max(cursor_char)
3094            } else {
3095                cursor_char
3096            };
3097            end_char = end_char.min(char_count);
3098
3099            let line_end_x = (run.line_w + extra) / scale;
3100            let mut carets = Vec::with_capacity(end_char - start_char + 1);
3101            for &byte in byte_offsets.iter().take(end_char + 1).skip(start_char) {
3102                carets.push(table.get(&byte).copied().unwrap_or(f32::MIN));
3103            }
3104
3105            let mut prev = 0.0f32;
3106            let n = carets.len();
3107            for (k, v) in carets.iter_mut().enumerate() {
3108                if *v == f32::MIN {
3109                    *v = if k == n - 1 {
3110                        line_end_x.max(prev)
3111                    } else {
3112                        prev
3113                    };
3114                }
3115                if *v < prev {
3116                    *v = prev;
3117                }
3118                prev = *v;
3119            }
3120            if let Some(last) = carets.last_mut() {
3121                *last = line_end_x.max(prev);
3122            }
3123            if carets.is_empty() {
3124                carets.push(match align {
3125                    rich::RichAlign::Left => 0.0,
3126                    rich::RichAlign::Center => (width * 0.5).max(0.0),
3127                    rich::RichAlign::Right => width.max(0.0),
3128                });
3129            }
3130
3131            let index = lines.len();
3132            lines.push(rich::RichLine {
3133                index,
3134                top,
3135                height,
3136                baseline,
3137                width: line_end_x,
3138                chars: start_char..end_char,
3139            });
3140            run_carets.push(carets);
3141
3142            for (j, g) in run.glyphs.iter().enumerate() {
3143                let p = g.physical((0.0, 0.0), 1.0);
3144                let x_offset = g.font_size * g.x_offset;
3145                let y_offset = g.font_size * g.y_offset;
3146                let span = g.metadata.min(spans.len().saturating_sub(1));
3147                glyphs.push(rich::RichGlyph {
3148                    x: g.x + x_offset + j as f32 * sp_px,
3149                    y: (run.line_y - base_top) + (g.y - y_offset),
3150                    key: p.cache_key,
3151                    span,
3152                    color: spans.get(span).and_then(|s| s.color),
3153                });
3154            }
3155
3156            cursor_char = end_char;
3157
3158            if cursor_char < char_count
3159                && full.as_bytes().get(byte_offsets[cursor_char]).copied() == Some(b'\n')
3160            {
3161                cursor_char += 1;
3162            }
3163        }
3164
3165        for line in &mut lines {
3166            line.chars.start = line.chars.start.min(char_count);
3167            line.chars.end = line.chars.end.min(char_count);
3168        }
3169
3170        if full.ends_with('\n') && !full.is_empty() {
3171            let top = lines.last().map(|l| l.top + l.height).unwrap_or(0.0);
3172            let index = lines.len();
3173            let x = match align {
3174                rich::RichAlign::Left => 0.0,
3175                rich::RichAlign::Center => (width * 0.5).max(0.0),
3176                rich::RichAlign::Right => width.max(0.0),
3177            };
3178            lines.push(rich::RichLine {
3179                index,
3180                top,
3181                height: line_h,
3182                baseline: top + line_h * 0.5,
3183                width: 0.0,
3184                chars: char_count..char_count,
3185            });
3186            run_carets.push(vec![x]);
3187        }
3188
3189        let total_height = lines.last().map(|l| l.top + l.height).unwrap_or(0.0);
3190
3191        let mut backgrounds: Vec<rich::RichRect> = Vec::new();
3192        let mut decorations: Vec<rich::RichRect> = Vec::new();
3193        let thickness = (size * 0.07).max(1.0);
3194        for (si, span) in spans.iter().enumerate() {
3195            let range = &span_chars[si];
3196            if range.is_empty() {
3197                continue;
3198            }
3199            for (li, line) in lines.iter().enumerate() {
3200                let s0 = range.start.max(line.chars.start);
3201                let s1 = range.end.min(line.chars.end);
3202                if s1 <= s0 {
3203                    continue;
3204                }
3205                let carets = &run_carets[li];
3206                let x_of = |c: usize| -> f32 {
3207                    let k = c.saturating_sub(line.chars.start);
3208                    carets
3209                        .get(k.min(carets.len().saturating_sub(1)))
3210                        .copied()
3211                        .unwrap_or(0.0)
3212                };
3213                let x0 = x_of(s0);
3214                let x1 = x_of(s1).max(x0);
3215                if x1 <= x0 {
3216                    continue;
3217                }
3218                if let Some(bg) = span.background {
3219                    backgrounds.push(rich::RichRect {
3220                        rect: Rect::from_xywh(x0, line.top, x1 - x0, line.height),
3221                        color: bg,
3222                    });
3223                }
3224                if span.underline {
3225                    decorations.push(rich::RichRect {
3226                        rect: Rect::from_xywh(
3227                            x0,
3228                            (line.baseline + size * 0.12).round(),
3229                            x1 - x0,
3230                            thickness,
3231                        ),
3232                        color: span.color.unwrap_or(Color::WHITE),
3233                    });
3234                }
3235                if span.strikethrough {
3236                    decorations.push(rich::RichRect {
3237                        rect: Rect::from_xywh(
3238                            x0,
3239                            (line.baseline - size * 0.30).round(),
3240                            x1 - x0,
3241                            thickness,
3242                        ),
3243                        color: span.color.unwrap_or(Color::WHITE),
3244                    });
3245                }
3246            }
3247        }
3248
3249        let out = Rc::new(rich::RichParagraph {
3250            glyphs,
3251            backgrounds,
3252            decorations,
3253            lines,
3254            carets: run_carets,
3255            span_ranges: span_chars,
3256            span_links: spans.iter().map(|s| s.link.clone()).collect(),
3257            span_mentions: spans.iter().map(|s| s.mention).collect(),
3258            span_codes: spans.iter().map(|s| s.code).collect(),
3259            squiggles: Vec::new(),
3260            squiggle_rects: Vec::new(),
3261            width: width_px / scale,
3262            height: total_height,
3263            line_height: line_h,
3264            font_size: size,
3265            len_chars: char_count,
3266        });
3267        self.shape_scratch = Some(buffer);
3268        out
3269    }
3270
3271    pub fn draw_rich(
3272        &mut self,
3273        rich: &rich::RichParagraph,
3274        origin: (f32, f32),
3275        default_color: Color,
3276        list: &mut RenderList,
3277    ) {
3278        use sequoia_core::GlyphQuad;
3279
3280        for bg in rich.backgrounds() {
3281            let r = bg.rect;
3282            list.push_quad(
3283                Rect::from_xywh(
3284                    r.left() + origin.0,
3285                    r.top() + origin.1,
3286                    r.size.width,
3287                    r.size.height,
3288                ),
3289                bg.color,
3290                0.0,
3291            );
3292        }
3293        if rich.glyph_count() > 0 {
3294            let scale = self.scale;
3295            let (ox, oy) = (origin.0 * scale, origin.1 * scale);
3296
3297            let mut placed: Vec<(CacheKey, i32, i32, Color)> = Vec::new();
3298            for g in rich.glyphs.iter() {
3299                let color = g.color.unwrap_or(default_color);
3300                let x = ox + g.x;
3301                let y = if self.subpixel_y {
3302                    oy + g.y
3303                } else {
3304                    (oy + g.y).trunc()
3305                };
3306                let (key, x_int, y_int) = CacheKey::new(
3307                    g.key.font_id,
3308                    g.key.glyph_id,
3309                    f32::from_bits(g.key.font_size_bits),
3310                    (x, y),
3311                    g.key.font_weight,
3312                    g.key.flags,
3313                );
3314                if self.atlas.entry(&key).is_none() {
3315                    self.rasterize(key);
3316                }
3317                placed.push((key, x_int, y_int, color));
3318            }
3319
3320            let (aw, ah) = (self.atlas.width as f32, self.atlas.height as f32);
3321            let mut run: Vec<GlyphQuad> = Vec::new();
3322            let mut run_color = placed[0].3;
3323            let push_run = |run: &mut Vec<GlyphQuad>, color: Color, list: &mut RenderList| {
3324                if run.is_empty() {
3325                    return;
3326                }
3327                list.push_text(TextDraw {
3328                    color,
3329                    glyphs: std::mem::take(run),
3330                });
3331            };
3332            for (key, x_int, y_int, color) in placed.iter() {
3333                if *color != run_color && !run.is_empty() {
3334                    push_run(&mut run, run_color, &mut *list);
3335                    run_color = *color;
3336                }
3337                let Some(entry) = self.atlas.entry(key) else {
3338                    continue;
3339                };
3340                let [rx, ry, rw, rh] = entry.rect;
3341                if rw == 0 || rh == 0 {
3342                    continue;
3343                }
3344                let x = (*x_int + entry.left) as f32 / scale;
3345                let y = (*y_int - entry.top) as f32 / scale;
3346                run.push(GlyphQuad {
3347                    rect: Rect::from_xywh(x, y, rw as f32 / scale, rh as f32 / scale),
3348                    uv: [
3349                        rx as f32 / aw,
3350                        ry as f32 / ah,
3351                        (rx + rw) as f32 / aw,
3352                        (ry + rh) as f32 / ah,
3353                    ],
3354                    color_glyph: entry.color,
3355                });
3356            }
3357            push_run(&mut run, run_color, &mut *list);
3358        }
3359
3360        for deco in rich.decorations() {
3361            let r = deco.rect;
3362            list.push_quad(
3363                Rect::from_xywh(
3364                    r.left() + origin.0,
3365                    r.top() + origin.1,
3366                    r.size.width,
3367                    r.size.height,
3368                ),
3369                deco.color,
3370                0.0,
3371            );
3372        }
3373        for sq in rich.squiggle_rects() {
3374            let r = sq.rect;
3375            list.push_quad(
3376                Rect::from_xywh(
3377                    r.left() + origin.0,
3378                    r.top() + origin.1,
3379                    r.size.width,
3380                    r.size.height,
3381                ),
3382                sq.color,
3383                0.0,
3384            );
3385        }
3386    }
3387
3388    fn rasterize(&mut self, key: CacheKey) -> AtlasEntry {
3389        let Some(img) = self.swash.get_image_uncached(&mut self.font_system, key) else {
3390            return self.atlas.insert(key, 1, 1, &[0, 0, 0, 0], 0, 0, false);
3391        };
3392        let left = img.placement.left;
3393        let top = img.placement.top;
3394        let w = img.placement.width;
3395        let h = img.placement.height;
3396        if w == 0 || h == 0 {
3397            return self
3398                .atlas
3399                .insert(key, 1, 1, &[0, 0, 0, 0], left, top, false);
3400        }
3401
3402        let n = (w * h) as usize;
3403        let mut rgba = vec![0u8; n * ATLAS_BPP];
3404        let color;
3405        match img.data.len() {
3406            len if len == n => {
3407                for (i, px) in rgba.chunks_exact_mut(ATLAS_BPP).enumerate() {
3408                    px[0] = 255;
3409                    px[1] = 255;
3410                    px[2] = 255;
3411                    px[3] = img.data[i];
3412                }
3413                color = false;
3414            }
3415            len if len == n * 4 => {
3416                for (i, px) in rgba.chunks_exact_mut(ATLAS_BPP).enumerate() {
3417                    let a = img.data[i * 4 + 3];
3418                    if a == 0 {
3419                        continue;
3420                    }
3421                    let scale = a as f32 / 255.0;
3422                    let lin = Color::rgb(
3423                        img.data[i * 4] as f32 / 255.0,
3424                        img.data[i * 4 + 1] as f32 / 255.0,
3425                        img.data[i * 4 + 2] as f32 / 255.0,
3426                    )
3427                    .to_linear();
3428                    px[0] = (lin.r * scale * 255.0).round().clamp(0.0, 255.0) as u8;
3429                    px[1] = (lin.g * scale * 255.0).round().clamp(0.0, 255.0) as u8;
3430                    px[2] = (lin.b * scale * 255.0).round().clamp(0.0, 255.0) as u8;
3431                    px[3] = a;
3432                }
3433                color = true;
3434            }
3435            _ => {
3436                return self
3437                    .atlas
3438                    .insert(key, 1, 1, &[0, 0, 0, 0], left, top, false);
3439            }
3440        }
3441        self.atlas.insert(key, w, h, &rgba, left, top, color)
3442    }
3443
3444    pub fn atlas(&self) -> &GlyphAtlas {
3445        &self.atlas
3446    }
3447
3448    pub fn atlas_mut(&mut self) -> &mut GlyphAtlas {
3449        &mut self.atlas
3450    }
3451}
3452
3453#[cfg(test)]
3454mod tests {
3455    use super::*;
3456    use sequoia_core::RenderItem;
3457
3458    fn solid(w: u32, h: u32) -> Vec<u8> {
3459        vec![255; (w * h) as usize]
3460    }
3461
3462    #[test]
3463    fn sprite_insert_and_entry() {
3464        let mut atlas = GlyphAtlas::new(64, 64);
3465        assert!(atlas.sprite(7).is_none());
3466        let entry = atlas.insert_sprite(7, 10, 8, &solid(10, 8));
3467        assert_eq!(entry.rect, [0, 0, 10, 8]);
3468        assert_eq!(atlas.sprite(7), Some(entry));
3469        assert_eq!(atlas.version(), 1);
3470
3471        assert_eq!(atlas.pixels()[(7 * 64 + 9) * 4 + 3], 255, "alfa");
3472        assert_eq!(atlas.pixels()[(7 * 64 + 9) * 4], 255, "biel (RGB)");
3473        assert_eq!(atlas.pixels()[(7 * 64 + 10) * 4 + 3], 0, "gutter");
3474    }
3475
3476    #[test]
3477    fn atlas_construction_is_clamped_and_inserts_validate_data() {
3478        let atlas = GlyphAtlas::new(u32::MAX, u32::MAX);
3479        assert_ne!(atlas.id(), GlyphAtlas::new(8, 8).id());
3480        assert_eq!(atlas.size(), (ATLAS_MAX_DIM, ATLAS_MAX_DIM));
3481        assert_eq!(
3482            atlas.pixels().len(),
3483            ATLAS_MAX_DIM as usize * ATLAS_MAX_DIM as usize * ATLAS_BPP
3484        );
3485
3486        let mut atlas = GlyphAtlas::new(64, 64);
3487        let v0 = atlas.version();
3488        let short = vec![255u8; 10];
3489        let entry = atlas.insert_sprite(99, 10, 8, &short);
3490        assert_eq!(entry.rect, [0, 0, 1, 1], "sentinel as on full atlas");
3491        assert!(atlas.sprite(99).is_none(), "do not store the corrupt entry");
3492        assert_eq!(atlas.version(), v0, "rejection does not bump version");
3493    }
3494
3495    #[test]
3496    fn draw_sprite_pushes_glyph_quad_with_uv() {
3497        let mut text = TextSystem::with_atlas(64, 64);
3498        let mut list = RenderList::new();
3499        let dst = Rect::from_xywh(4.0, 5.0, 10.0, 10.0);
3500        text.draw_sprite(42, 10, 10, &solid(10, 10), dst, Color::RED, &mut list);
3501        assert_eq!(list.len(), 1);
3502        let item = &list.items()[0];
3503        let RenderItem::Text(t) = item else {
3504            panic!("expected text")
3505        };
3506        assert_eq!(t.color, Color::RED);
3507        assert_eq!(t.glyphs.len(), 1);
3508        assert_eq!(t.glyphs[0].rect, dst);
3509        assert!(t.glyphs[0].uv[2] > t.glyphs[0].uv[0], "UV increasing");
3510        assert!(t.glyphs[0].uv[3] > t.glyphs[0].uv[1]);
3511
3512        let v0 = text.atlas().version();
3513        text.draw_sprite(42, 10, 10, &solid(10, 10), dst, Color::RED, &mut list);
3514        assert_eq!(text.atlas().version(), v0);
3515    }
3516
3517    #[test]
3518    fn wrap_limits_width_and_grows_height() {
3519        let mut text = TextSystem::new();
3520        let content = "aaa bbb ccc ddd eee fff ggg hhh iii jjj kkk lll";
3521        let single = text.shape(content, 16.0);
3522        let wrapped = text.shape_wrapped(content, 16.0, FontWeight::NORMAL, 80.0, WrapMode::Word);
3523        assert!(single.width() > 80.0, "single line wider than the limit");
3524        assert!(
3525            wrapped.width() <= 80.0 + 0.5,
3526            "wrapped within limit: {}",
3527            wrapped.width()
3528        );
3529        assert!(
3530            wrapped.height() > single.height(),
3531            "block taller: {} > {}",
3532            wrapped.height(),
3533            single.height()
3534        );
3535        assert!(wrapped.glyph_count() > 0);
3536    }
3537
3538    #[test]
3539    fn wrap_width_is_part_of_cache_key() {
3540        let mut text = TextSystem::new();
3541        let a = text.shape_wrapped(
3542            "aaa bbb ccc",
3543            16.0,
3544            FontWeight::NORMAL,
3545            40.0,
3546            WrapMode::Word,
3547        );
3548        let b = text.shape_wrapped(
3549            "aaa bbb ccc",
3550            16.0,
3551            FontWeight::NORMAL,
3552            400.0,
3553            WrapMode::Word,
3554        );
3555        assert!(!Rc::ptr_eq(&a, &b), "different widths = different entries");
3556        let c = text.shape_wrapped(
3557            "aaa bbb ccc",
3558            16.0,
3559            FontWeight::NORMAL,
3560            40.0,
3561            WrapMode::Word,
3562        );
3563        assert!(Rc::ptr_eq(&a, &c), "same width = hit");
3564    }
3565
3566    #[test]
3567    fn wrap_mode_character_breaks_long_words() {
3568        let mut text = TextSystem::new();
3569        let word = "superlongwordwithoutspacestocheck";
3570        let word_mode = text.shape_wrapped(word, 16.0, FontWeight::NORMAL, 60.0, WrapMode::Word);
3571        let char_mode =
3572            text.shape_wrapped(word, 16.0, FontWeight::NORMAL, 60.0, WrapMode::Character);
3573        assert!(
3574            char_mode.width() <= 60.0 + 0.5,
3575            "character within limit: {}",
3576            char_mode.width()
3577        );
3578        assert!(
3579            char_mode.height() > word_mode.height(),
3580            "character breaks into more lines: {} > {}",
3581            char_mode.height(),
3582            word_mode.height()
3583        );
3584    }
3585
3586    #[test]
3587    fn line_height_sets_row_geometry() {
3588        let mut text = TextSystem::new();
3589        let shaped = text.shape_wrapped_styled_spaced_lh(
3590            "aaa\nbbb",
3591            16.0,
3592            FontWeight::NORMAL,
3593            0.0,
3594            WrapMode::Word,
3595            0.0,
3596            None,
3597            FontStyle::Normal,
3598            LineHeight::Absolute(40.0),
3599        );
3600        assert_eq!(shaped.line_count(), 2);
3601        assert!((shaped.rows()[1].top - 40.0).abs() < 0.5);
3602        assert!(shaped.height() >= 80.0);
3603        assert!(shaped.first_baseline() > 0.0);
3604        assert!(shaped.first_baseline() < 40.0);
3605    }
3606
3607    #[test]
3608    fn line_height_is_part_of_cache_key() {
3609        let mut text = TextSystem::new();
3610        let a = text.shape_wrapped_styled_spaced_lh(
3611            "aaa bbb",
3612            16.0,
3613            FontWeight::NORMAL,
3614            80.0,
3615            WrapMode::Word,
3616            0.0,
3617            None,
3618            FontStyle::Normal,
3619            LineHeight::Factor(1.0),
3620        );
3621        let b = text.shape_wrapped_styled_spaced_lh(
3622            "aaa bbb",
3623            16.0,
3624            FontWeight::NORMAL,
3625            80.0,
3626            WrapMode::Word,
3627            0.0,
3628            None,
3629            FontStyle::Normal,
3630            LineHeight::Factor(2.0),
3631        );
3632        assert!(!Rc::ptr_eq(&a, &b));
3633        assert!(b.height() > a.height());
3634    }
3635
3636    #[test]
3637    fn multiline_elision_stops_at_max_lines() {
3638        let mut text = TextSystem::new();
3639        let content = "one two three four five six seven eight nine ten";
3640        let shown = text.elide_lines_styled_spaced(
3641            content,
3642            16.0,
3643            FontWeight::NORMAL,
3644            70.0,
3645            WrapMode::Word,
3646            0.0,
3647            None,
3648            FontStyle::Normal,
3649            LineHeight::default(),
3650            2,
3651        );
3652        assert!(shown.ends_with('…'), "ellipsis appended: {shown:?}");
3653        assert!(shown.chars().count() < content.chars().count());
3654        let shaped = text.shape_wrapped_styled_spaced_lh(
3655            &shown,
3656            16.0,
3657            FontWeight::NORMAL,
3658            70.0,
3659            WrapMode::Word,
3660            0.0,
3661            None,
3662            FontStyle::Normal,
3663            LineHeight::default(),
3664        );
3665        assert!(shaped.line_count() <= 2, "lines: {}", shaped.line_count());
3666    }
3667
3668    #[test]
3669    fn min_content_width_uses_longest_word_or_char() {
3670        let mut text = TextSystem::new();
3671        let content = "a enormouslylongword b";
3672        let word = text.text_width_styled_spaced(
3673            "enormouslylongword",
3674            16.0,
3675            FontWeight::NORMAL,
3676            0.0,
3677            None,
3678            FontStyle::Normal,
3679        );
3680        let min_word = text.min_content_width_styled_spaced(
3681            content,
3682            16.0,
3683            FontWeight::NORMAL,
3684            0.0,
3685            None,
3686            FontStyle::Normal,
3687            WrapMode::Word,
3688        );
3689        assert!((min_word - word).abs() < 0.5, "{min_word} vs {word}");
3690        let min_char = text.min_content_width_styled_spaced(
3691            content,
3692            16.0,
3693            FontWeight::NORMAL,
3694            0.0,
3695            None,
3696            FontStyle::Normal,
3697            WrapMode::Character,
3698        );
3699        assert!(min_char < min_word);
3700    }
3701
3702    #[test]
3703    fn rich_elision_stops_at_max_lines() {
3704        let mut text = TextSystem::new();
3705        let spans = [rich::TextSpan::new(
3706            "one two three four five six seven eight nine ten",
3707        )];
3708        let shown = text.elide_rich_lines(
3709            &spans,
3710            16.0,
3711            70.0,
3712            WrapMode::Word,
3713            0.0,
3714            rich::RichAlign::Left,
3715            LineHeight::default(),
3716            2,
3717        );
3718        let joined: String = shown.iter().map(|s| s.text.as_str()).collect();
3719        assert!(joined.ends_with('…'), "ellipsis appended: {joined:?}");
3720        let shaped = text.shape_rich_full_lh(
3721            &shown,
3722            16.0,
3723            70.0,
3724            WrapMode::Word,
3725            0.0,
3726            rich::RichAlign::Left,
3727            LineHeight::default(),
3728        );
3729        assert!(shaped.line_count() <= 2);
3730    }
3731
3732    #[test]
3733    fn truncate_cuts_and_fits() {
3734        let mut text = TextSystem::new();
3735        assert_eq!(
3736            text.truncate("abcdef", 16.0, FontWeight::NORMAL, 10_000.0),
3737            "abcdef"
3738        );
3739        assert_eq!(text.truncate("abcdef", 16.0, FontWeight::NORMAL, 0.0), "");
3740        assert_eq!(text.truncate("", 16.0, FontWeight::NORMAL, 50.0), "");
3741        let cut = text.truncate("abcdef", 16.0, FontWeight::NORMAL, 30.0);
3742        assert!(cut.len() < 6, "cut: {cut}");
3743        assert!("abcdef".starts_with(&cut), "prefix by chars: {cut}");
3744        assert!(text.text_width(&cut, 16.0) <= 30.0 + 0.5, "fits: {cut}");
3745    }
3746
3747    #[test]
3748    fn elide_right_middle_left() {
3749        let mut text = TextSystem::new();
3750        let content = "Ann has a cat and a panda";
3751
3752        assert_eq!(
3753            text.elide(
3754                content,
3755                16.0,
3756                FontWeight::NORMAL,
3757                10_000.0,
3758                ElideMode::Right
3759            ),
3760            content
3761        );
3762        assert_eq!(
3763            text.elide(content, 16.0, FontWeight::NORMAL, 0.0, ElideMode::Right),
3764            ""
3765        );
3766
3767        let r = text.elide(content, 16.0, FontWeight::NORMAL, 60.0, ElideMode::Right);
3768        assert!(r.ends_with('…') && !r.starts_with('…'), "right: {r}");
3769        let l = text.elide(content, 16.0, FontWeight::NORMAL, 60.0, ElideMode::Left);
3770        assert!(l.starts_with('…') && !l.ends_with('…'), "left: {l}");
3771        let m = text.elide(content, 16.0, FontWeight::NORMAL, 60.0, ElideMode::Middle);
3772        assert!(
3773            m.starts_with('A') && m.ends_with('a') && m.contains('…'),
3774            "middle keeps the ends: {m}"
3775        );
3776        for (name, s) in [("R", &r), ("L", &l), ("M", &m)] {
3777            assert!(
3778                text.text_width(s, 16.0) <= 60.0 + 0.5,
3779                "{name} within limit: {s}"
3780            );
3781        }
3782    }
3783
3784    #[test]
3785    fn explicit_newlines_span_lines_without_wrap() {
3786        let mut text = TextSystem::new();
3787        let one = text.shape("aaa", 16.0);
3788        let two = text.shape("aaa\nbbb", 16.0);
3789        assert!(
3790            two.height() > one.height(),
3791            "explicit line break grows height: {} > {}",
3792            two.height(),
3793            one.height()
3794        );
3795    }
3796
3797    #[test]
3798    fn multiline_glyphs_carry_line_offsets() {
3799        let mut text = TextSystem::new();
3800        let two = text.shape("aaa\nbbb", 16.0);
3801        let ys: Vec<f32> = two.glyphs.iter().map(|g| g.y).collect();
3802        assert!(!ys.is_empty());
3803        let (mut lo, mut hi) = (f32::MAX, f32::MIN);
3804        for y in ys {
3805            lo = lo.min(y);
3806            hi = hi.max(y);
3807        }
3808        assert!(hi - lo > 10.0, "lines at different heights, span {hi}-{lo}");
3809
3810        let one = text.shape("aaa", 16.0);
3811        let one_y: Vec<f32> = one.glyphs.iter().map(|g| g.y).collect();
3812        let two_first: Vec<f32> = two.glyphs.iter().take(one_y.len()).map(|g| g.y).collect();
3813        assert_eq!(one_y, two_first, "first line without offset");
3814    }
3815
3816    #[test]
3817    fn draw_bins_glyphs_by_subpixel_position() {
3818        let mut text = TextSystem::with_atlas(256, 256);
3819        if text.shape("l", 16.0).glyph_count() == 0 {
3820            eprintln!("skip: no fonts in environment");
3821            return;
3822        }
3823        let shaped = text.shape("l", 16.0);
3824        let mut list = RenderList::new();
3825        text.draw(&shaped, (0.0, 0.0), Color::WHITE, &mut list);
3826        let v0 = text.atlas().version();
3827        text.draw(&shaped, (0.5, 0.0), Color::WHITE, &mut list);
3828        assert!(
3829            text.atlas().version() > v0,
3830            "inna faza subpikselowa = osobny raster w atlasie"
3831        );
3832
3833        let v1 = text.atlas().version();
3834        text.draw(&shaped, (0.5, 0.0), Color::WHITE, &mut list);
3835        assert_eq!(text.atlas().version(), v1, "same origin is a hit");
3836    }
3837
3838    #[test]
3839    fn subpixel_y_adds_a_new_raster_phase() {
3840        let mut text = TextSystem::with_atlas(256, 256);
3841        if text.shape("l", 16.0).glyph_count() == 0 {
3842            eprintln!("skip: no fonts in environment");
3843            return;
3844        }
3845        let shaped = text.shape("l", 16.0);
3846        let mut list = RenderList::new();
3847        text.draw(&shaped, (0.0, 10.37), Color::WHITE, &mut list);
3848        let v0 = text.atlas().version();
3849        assert!(!text.subpixel_y(), "Y hinted by default");
3850        text.set_subpixel_y(true);
3851        let mut list = RenderList::new();
3852        text.draw(&shaped, (0.0, 10.37), Color::WHITE, &mut list);
3853        assert!(
3854            text.atlas().version() > v0,
3855            "subpikselowy Y = faza rastra spoza binu Zero"
3856        );
3857    }
3858
3859    #[test]
3860    fn draw_keeps_fractional_origin_shifts() {
3861        let mut text = TextSystem::with_atlas(256, 256);
3862        if text.shape("HH", 16.0).glyph_count() == 0 {
3863            eprintln!("skip: no fonts in environment");
3864            return;
3865        }
3866        let shaped = text.shape("HH", 16.0);
3867        let rects = |t: &mut TextSystem, x: f32| -> Vec<Rect> {
3868            let mut list = RenderList::new();
3869            t.draw(&shaped, (x, 50.0), Color::WHITE, &mut list);
3870            let Some(RenderItem::Text(d)) = list.items().first() else {
3871                panic!("missing text")
3872            };
3873            d.glyphs.iter().map(|g| g.rect).collect()
3874        };
3875        let a = rects(&mut text, 10.0);
3876        let b = rects(&mut text, 11.0);
3877        assert_eq!(a.len(), b.len());
3878        for (i, (p, q)) in a.iter().zip(&b).enumerate() {
3879            assert!(
3880                ((q.left() - p.left()) - 1.0).abs() < 0.5,
3881                "glyph {i}: 1px shift gave {}",
3882                q.left() - p.left()
3883            );
3884        }
3885    }
3886
3887    #[test]
3888    fn draw_sprite_snaps_to_physical_pixels() {
3889        let mut text = TextSystem::with_atlas(64, 64);
3890        text.set_scale_factor(2.0);
3891        let mut list = RenderList::new();
3892        let dst = Rect::from_xywh(4.5, 5.25, 10.0, 10.0);
3893        text.draw_sprite(7, 20, 20, &solid(20, 20), dst, Color::WHITE, &mut list);
3894        assert_eq!(list.len(), 1);
3895        let item = &list.items()[0];
3896        let RenderItem::Text(t) = item else {
3897            panic!("expected text")
3898        };
3899        assert_eq!(t.glyphs.len(), 1);
3900        let r = t.glyphs[0].rect;
3901        assert_eq!((r.left(), r.top()), (4.5, 5.5), "snap: {r:?}");
3902        assert_eq!(
3903            (r.size.width, r.size.height),
3904            (10.0, 10.0),
3905            "1:1 with bitmap: {r:?}"
3906        );
3907
3908        assert_eq!((r.left() * 2.0, r.top() * 2.0), (9.0, 11.0));
3909        assert_eq!((r.size.width * 2.0, r.size.height * 2.0), (20.0, 20.0));
3910    }
3911
3912    #[test]
3913    fn load_font_garbage_is_none_without_panic() {
3914        let mut text = TextSystem::new();
3915        assert_eq!(text.load_font(vec![0xDE, 0xAD, 0xBE, 0xEF]), None);
3916
3917        assert_eq!(text.default_family(), Some("Inter"));
3918    }
3919
3920    #[test]
3921    fn set_default_family_takes_effect() {
3922        let mut text = TextSystem::new();
3923        text.set_default_family(Some("SequoiaTest".into()));
3924        assert_eq!(text.default_family(), Some("SequoiaTest"));
3925
3926        let shaped = text.shape("test", 12.0);
3927        assert!(shaped.glyph_count() > 0);
3928    }
3929
3930    #[test]
3931    fn shaping_cache_is_bounded_lru() {
3932        let mut text = TextSystem::new();
3933        for i in 0..(SHAPING_CACHE_MAX + 50) {
3934            text.shape(&format!("unique-line-{i}"), 12.0);
3935        }
3936        assert_eq!(
3937            text.shaped.len(),
3938            SHAPING_CACHE_MAX,
3939            "cache does not grow unbounded"
3940        );
3941        assert_eq!(text.shaping_order.len(), SHAPING_CACHE_MAX);
3942
3943        let old_key: ShapeKey = (
3944            "unique-line-0".to_owned(),
3945            (12.0f32 * 4.0).round() as u32,
3946            400,
3947            "Inter".to_owned(),
3948            FontStyle::Normal.key(),
3949            0,
3950            WrapMode::Word.key(),
3951            0,
3952            LineHeight::default().key(),
3953        );
3954        assert!(
3955            !text.shaped.contains_key(&old_key),
3956            "LRU: najstarszy wyparowany"
3957        );
3958
3959        let a = text.shape("unique-line-1", 12.0);
3960        let b = text.shape("unique-line-1", 12.0);
3961        assert!(Rc::ptr_eq(&a, &b), "hit = same Rc without reshaping");
3962        assert_eq!(
3963            text.shaped.len(),
3964            SHAPING_CACHE_MAX,
3965            "hit does not grow the cache"
3966        );
3967    }
3968
3969    #[test]
3970    fn mru_returns_same_shape_without_rehash() {
3971        let mut text = TextSystem::new();
3972        let a = text.shape("repeat me", 14.0);
3973
3974        let b = text.shape("repeat me", 14.0);
3975        assert!(Rc::ptr_eq(&a, &b), "MRU hit = same Rc");
3976        assert!(text.mru.is_some(), "MRU remembered");
3977        assert_eq!(text.shaped.len(), 1, "MRU does not duplicate cache entries");
3978
3979        let c = text.shape("something else", 14.0);
3980        assert!(!Rc::ptr_eq(&a, &c));
3981        let d = text.shape("something else", 14.0);
3982        assert!(Rc::ptr_eq(&c, &d), "MRU after switching");
3983        assert_eq!(text.shaped.len(), 2);
3984
3985        let e = text.shape("repeat me", 14.0);
3986        assert!(Rc::ptr_eq(&a, &e));
3987    }
3988
3989    #[test]
3990    fn mru_cleared_with_caches() {
3991        let mut text = TextSystem::new();
3992        let a = text.shape("trzymaj", 12.0);
3993        assert!(text.mru.is_some());
3994        text.set_scale_factor(2.0);
3995        assert!(text.mru.is_none(), "the scale also cleans the MRU");
3996        let b = text.shape("trzymaj", 12.0);
3997        assert!(!Rc::ptr_eq(&a, &b), "po zmianie skali reshape");
3998    }
3999
4000    #[test]
4001    fn atlas_repack_keeps_hot_entries_instead_of_clearing() {
4002        let mut atlas = GlyphAtlas::new(64, 64);
4003        for i in 0..10u64 {
4004            atlas.insert_sprite(i, 16, 16, &[255; 256]);
4005        }
4006        assert!(atlas.sprite(9).is_some(), "najnowszy wpis obecny");
4007        let present = (0..10u64).filter(|i| atlas.sprite(*i).is_some()).count();
4008        assert!(present >= 2, "hot set survived compaction, got {present}");
4009        assert!(
4010            atlas.pixels().iter().any(|&p| p > 0),
4011            "repacked pixels are not zeroed"
4012        );
4013        let (w, h) = atlas.size();
4014        assert_eq!(atlas.pixels().len(), (w * h) as usize * ATLAS_BPP);
4015    }
4016
4017    #[test]
4018    fn atlas_shrink_to_fit_frees_bytes() {
4019        let mut atlas = GlyphAtlas::new(256, 256);
4020        for i in 0..3u64 {
4021            atlas.insert_sprite(i, 8, 8, &solid(8, 8));
4022        }
4023        let before = atlas.bytes();
4024        atlas.shrink_to_fit();
4025        let (w, h) = atlas.size();
4026        assert!(
4027            atlas.bytes() < before,
4028            "bitmap shrank: {before} -> {}",
4029            atlas.bytes()
4030        );
4031        assert_eq!(atlas.pixels().len(), (w * h) as usize * ATLAS_BPP);
4032        assert!(atlas.sprite(2).is_some(), "hot sprite survived the shrink");
4033    }
4034
4035    #[test]
4036    fn atlas_dims_stay_bounded_under_load() {
4037        let mut atlas = GlyphAtlas::new(64, 64);
4038        for i in 0..200u64 {
4039            atlas.insert_sprite(i, 16, 16, &solid(16, 16));
4040        }
4041        let (w, h) = atlas.size();
4042        assert!(
4043            w <= ATLAS_MAX_DIM && h <= ATLAS_MAX_DIM,
4044            "atlas {w}×{h} past the limit"
4045        );
4046        assert!(atlas.pixels().len() == (w * h) as usize * ATLAS_BPP);
4047    }
4048
4049    #[test]
4050    fn letter_spacing_widens_text() {
4051        let mut text = TextSystem::new();
4052        let base = text.text_width_weighted("abc", 16.0, FontWeight::NORMAL);
4053        let wide = text.text_width_weighted_spaced("abc", 16.0, FontWeight::NORMAL, 2.0);
4054        assert!(
4055            (wide - base - 4.0).abs() < 0.5,
4056            "3 znaki + spacing 2 → +4 px, base={base} wide={wide}"
4057        );
4058
4059        let one = text.text_width_weighted("a", 16.0, FontWeight::NORMAL);
4060        let one_sp = text.text_width_weighted_spaced("a", 16.0, FontWeight::NORMAL, 5.0);
4061        assert!((one - one_sp).abs() < 1e-4, "1 znak: {one} vs {one_sp}");
4062
4063        assert_eq!(
4064            text.text_width_weighted_spaced("", 16.0, FontWeight::NORMAL, 5.0),
4065            0.0
4066        );
4067    }
4068
4069    #[test]
4070    fn letter_spacing_zero_matches_plain() {
4071        let mut text = TextSystem::new();
4072        let a = text.shape_weighted("Sequoia", 14.0, FontWeight::BOLD);
4073        let b = text.shape_spaced("Sequoia", 14.0, FontWeight::BOLD, 0.0);
4074        assert!((a.width() - b.width()).abs() < 1e-4);
4075        assert_eq!(a.glyph_count(), b.glyph_count());
4076    }
4077
4078    #[test]
4079    fn letter_spacing_cache_key_is_distinct() {
4080        let mut text = TextSystem::new();
4081        let a = text.shape_spaced("ab", 12.0, FontWeight::NORMAL, 0.0);
4082        let b = text.shape_spaced("ab", 12.0, FontWeight::NORMAL, 3.0);
4083        assert!(!Rc::ptr_eq(&a, &b), "inny spacing = osobny wpis cache");
4084        let c = text.shape_spaced("ab", 12.0, FontWeight::NORMAL, 3.0);
4085        assert!(Rc::ptr_eq(&b, &c), "same spacing = hit");
4086    }
4087
4088    #[test]
4089    fn letter_spacing_shifts_glyphs() {
4090        let mut text = TextSystem::new();
4091        let base = text.shape_spaced("ab", 20.0, FontWeight::NORMAL, 0.0);
4092        let wide = text.shape_spaced("ab", 20.0, FontWeight::NORMAL, 4.0);
4093        assert_eq!(base.glyph_count(), wide.glyph_count());
4094        if base.glyph_count() >= 2 {
4095            let dx0 = wide.glyphs[0].x - base.glyphs[0].x;
4096            let dx1 = wide.glyphs[1].x - base.glyphs[1].x;
4097            assert!(dx0.abs() < 0.5, "first glyph stays, dx={dx0}");
4098            assert!(
4099                (dx1 - 4.0 * text.scale_factor()).abs() < 1.0,
4100                "second shifted by spacing, dx={dx1}"
4101            );
4102        }
4103    }
4104
4105    #[test]
4106    fn letter_spacing_negative_tightens() {
4107        let mut text = TextSystem::new();
4108        let base = text.text_width_weighted("abc", 16.0, FontWeight::NORMAL);
4109        let tight = text.text_width_weighted_spaced("abc", 16.0, FontWeight::NORMAL, -1.0);
4110        assert!(tight < base, "negative spacing tightens: {tight} < {base}");
4111    }
4112
4113    #[test]
4114    fn letter_spacing_nonfinite_is_zero() {
4115        let mut text = TextSystem::new();
4116        let base = text.text_width_weighted("abc", 16.0, FontWeight::NORMAL);
4117        assert_eq!(
4118            text.text_width_weighted_spaced("abc", 16.0, FontWeight::NORMAL, f32::NAN),
4119            base
4120        );
4121        assert_eq!(
4122            text.text_width_weighted_spaced("abc", 16.0, FontWeight::NORMAL, f32::INFINITY),
4123            base
4124        );
4125    }
4126
4127    #[test]
4128    fn letter_spacing_carets_match_width() {
4129        let mut text = TextSystem::new();
4130        let pos = text.caret_positions_spaced("abcd", 16.0, FontWeight::NORMAL, 2.0);
4131        assert_eq!(pos.len(), 5);
4132        let w = text.text_width_weighted_spaced("abcd", 16.0, FontWeight::NORMAL, 2.0);
4133        assert!(
4134            (pos[4] - w).abs() < 0.5,
4135            "last position = width: {} vs {w}",
4136            pos[4]
4137        );
4138        assert!(
4139            pos.windows(2).all(|w| w[1] >= w[0] - 1e-4),
4140            "monotoniczne: {pos:?}"
4141        );
4142
4143        assert!(
4144            pos[1] - pos[0] >= 1.5,
4145            "step with tracking: {:?}",
4146            &pos[..2]
4147        );
4148    }
4149
4150    #[test]
4151    fn oversize_sprite_is_skipped_not_stored() {
4152        let mut atlas = GlyphAtlas::new(64, 64);
4153        let w = ATLAS_MAX_DIM + 1;
4154        let entry = atlas.insert_sprite(1, w, 8, &vec![255u8; (w * 8) as usize]);
4155        assert!(atlas.sprite(1).is_none(), "oversize entry is not stored");
4156        assert_eq!(entry.rect[2], 1, "sentinel instead of garbage UV");
4157        let (aw, ah) = atlas.size();
4158        assert!(
4159            aw <= ATLAS_MAX_DIM && ah <= ATLAS_MAX_DIM,
4160            "atlas did not grow past the limit"
4161        );
4162    }
4163
4164    #[test]
4165    fn line_cache_edit_single_line_reshapes_at_most_3() {
4166        use sequoia_buffer::StyledBuffer;
4167        let mut sys = TextSystem::new();
4168
4169        let initial: String = (0..10_000)
4170            .map(|i| format!("line {i:05} some text for shaping"))
4171            .collect::<Vec<_>>()
4172            .join("\n");
4173        let mut buf = StyledBuffer::from(initial);
4174        let line_count = buf.lines().line_count();
4175        assert_eq!(line_count, 10_000);
4176
4177        sys.reset_line_reshapes();
4178        let all = sys.shape_lines_visible_buffer(
4179            &buf,
4180            0..line_count,
4181            14.0,
4182            FontWeight::NORMAL,
4183            0.0,
4184            WrapMode::Word,
4185            0.0,
4186            None,
4187            FontStyle::Normal,
4188        );
4189        assert_eq!(all.len(), 10_000);
4190        assert_eq!(sys.line_reshapes(), 10_000);
4191        assert!(sys.line_cache_len() >= 10_000);
4192
4193        sys.reset_line_reshapes();
4194        let again = sys.shape_lines_visible_buffer(
4195            &buf,
4196            0..line_count,
4197            14.0,
4198            FontWeight::NORMAL,
4199            0.0,
4200            WrapMode::Word,
4201            0.0,
4202            None,
4203            FontStyle::Normal,
4204        );
4205        assert_eq!(sys.line_reshapes(), 0, "bez edycji wszystko trafia w cache");
4206        assert!(Rc::ptr_eq(&all[5000], &again[5000]));
4207
4208        let (s, e) = buf.lines().line_range(5000);
4209        let old_text = buf.substring(s..e);
4210        assert!(old_text.starts_with("line 05000"));
4211        buf.replace(s..e, "line 05000 EDITED single line change", None);
4212
4213        let (ns, ne) = buf.lines().line_range(5000);
4214        let spans = buf.spans_for_line(ns..ne);
4215        let line_text = buf.substring(ns..ne);
4216        let h1 = TextSystem::line_hash(&line_text, &spans, ns, 0.0);
4217        let h0 = TextSystem::line_hash(&old_text, &[], s, 0.0);
4218        assert_ne!(h0, h1, "hash changes after editing content");
4219
4220        sys.invalidate_for_edit(buf.lines(), s, e);
4221
4222        sys.reset_line_reshapes();
4223        let after = sys.shape_lines_visible_buffer(
4224            &buf,
4225            0..line_count,
4226            14.0,
4227            FontWeight::NORMAL,
4228            0.0,
4229            WrapMode::Word,
4230            0.0,
4231            None,
4232            FontStyle::Normal,
4233        );
4234        assert!(
4235            sys.line_reshapes() <= 3,
4236            "edit 1 line = ≤3 reshapes, it was {}",
4237            sys.line_reshapes()
4238        );
4239        assert!(
4240            !Rc::ptr_eq(&all[5000], &after[5000]),
4241            "edytowana linia reshape"
4242        );
4243        assert!(Rc::ptr_eq(&all[0], &after[0]), "nieedytowane linie hit");
4244        assert!(Rc::ptr_eq(&all[9999], &after[9999]));
4245    }
4246
4247    #[test]
4248    fn line_cache_apply_edit_invalidate_uses_line_of_only() {
4249        use sequoia_buffer::{LineIndex, StyledBuffer};
4250        let mut sys = TextSystem::new();
4251        let mut buf = StyledBuffer::from("aaa\nbbb\nccc");
4252        let n = buf.lines().line_count();
4253        assert_eq!(n, 3);
4254        let _ = sys.shape_lines_visible_buffer(
4255            &buf,
4256            0..n,
4257            14.0,
4258            FontWeight::NORMAL,
4259            0.0,
4260            WrapMode::Word,
4261            0.0,
4262            None,
4263            FontStyle::Normal,
4264        );
4265        sys.reset_line_reshapes();
4266
4267        let idx: &LineIndex = buf.lines();
4268        assert_eq!(idx.line_of(4), 1);
4269        assert_eq!(idx.line_of(7), 1);
4270        sys.invalidate_for_edit(idx, 4, 7);
4271
4272        sys.invalidate_for_edit(idx, 4, 7);
4273        let before = sys.line_reshapes();
4274        let (s0, e0) = idx.line_range(0);
4275        let t0 = buf.substring(s0..e0);
4276        let _ = sys.shape_line_wrapped(0, &t0, 14.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4277        assert_eq!(sys.line_reshapes(), before, "line 0 untouched = hit");
4278
4279        buf.replace(4..7, "BBB", None);
4280        sys.invalidate_for_edit(buf.lines(), 4, 7);
4281        let (s1, e1) = buf.lines().line_range(1);
4282        let t1 = buf.substring(s1..e1);
4283        assert_eq!(t1, "BBB");
4284        let _ = sys.shape_line_wrapped(1, &t1, 14.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4285        assert_eq!(sys.line_reshapes(), before + 1, "linia 1 miss = 1 reshape");
4286    }
4287
4288    #[test]
4289    fn line_cache_split_join_newline_reshapes_only_touched() {
4290        use sequoia_buffer::StyledBuffer;
4291        let mut sys = TextSystem::new();
4292        let mut buf = StyledBuffer::from("aaa\nbbb\nccc");
4293        let shape_all = |sys: &mut TextSystem, buf: &StyledBuffer| -> Vec<Rc<ShapedLine>> {
4294            let n = buf.lines().line_count();
4295            sys.shape_lines_visible_buffer(
4296                buf,
4297                0..n,
4298                14.0,
4299                FontWeight::NORMAL,
4300                0.0,
4301                WrapMode::Word,
4302                0.0,
4303                None,
4304                FontStyle::Normal,
4305            )
4306        };
4307        let before_lines = shape_all(&mut sys, &buf);
4308        assert_eq!(before_lines.len(), 3);
4309
4310        sys.reset_line_reshapes();
4311        buf.insert(5, "\n", None);
4312        assert_eq!(buf.lines().line_count(), 4, "split adds a line");
4313        assert_eq!(buf.to_string(), "aaa\nb\nbb\nccc");
4314
4315        let edited = {
4316            let idx = buf.lines();
4317
4318            idx.line_of(5)..idx.line_of(5).saturating_add(1)
4319        };
4320        sys.invalidate_lines(edited);
4321        let after_split = shape_all(&mut sys, &buf);
4322        assert_eq!(after_split.len(), 4);
4323        assert!(
4324            sys.line_reshapes() <= 3,
4325            "split 1 line = ≤3 reshapes, it was {}",
4326            sys.line_reshapes()
4327        );
4328
4329        sys.reset_line_reshapes();
4330
4331        buf.delete(5..6);
4332        assert_eq!(buf.to_string(), "aaa\nbbb\nccc");
4333        assert_eq!(buf.lines().line_count(), 3, "join removes line");
4334        sys.invalidate_lines(1..2);
4335        let after_join = shape_all(&mut sys, &buf);
4336        assert_eq!(after_join.len(), 3);
4337        assert!(
4338            sys.line_reshapes() <= 3,
4339            "join 2 lines = ≤3 reshapes, it was {}",
4340            sys.line_reshapes()
4341        );
4342
4343        let _ = (before_lines, after_split, after_join);
4344    }
4345
4346    #[test]
4347    fn line_cache_evict_clears_line_to_key() {
4348        let mut sys = TextSystem::new();
4349        let n = LINE_CACHE_MAX + 66;
4350        for i in 0..n {
4351            let t = format!("evict probe unique line {i:06} xyz");
4352            let _ = sys.shape_line_wrapped(i, &t, 12.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4353        }
4354        assert!(
4355            sys.line_cache_len() <= LINE_CACHE_MAX,
4356            "LRU bound: {} <= {LINE_CACHE_MAX}",
4357            sys.line_cache_len()
4358        );
4359        let mut checked = 0usize;
4360        for ((ln, wq), key) in sys.line_to_key.iter() {
4361            assert!(
4362                sys.lines.contains_key(key),
4363                "hanging corpse line_to_key[({ln},{wq})]"
4364            );
4365            checked += 1;
4366        }
4367        assert!(checked > 0);
4368
4369        sys.invalidate_lines(0..50);
4370        for ((ln, wq), key) in sys.line_to_key.iter() {
4371            assert!(
4372                sys.lines.contains_key(key),
4373                "trup po invalidate [({ln},{wq})]"
4374            );
4375        }
4376    }
4377
4378    #[test]
4379    fn line_hash_covers_all_span_fields_not_just_len() {
4380        use sequoia_buffer::{SpanAttr, StyledSpan};
4381
4382        let mk = |f: fn(SpanAttr) -> SpanAttr| vec![StyledSpan::new(0, 5, f(SpanAttr::plain()))];
4383        let sets = [
4384            mk(|a| SpanAttr { bold: true, ..a }),
4385            mk(|a| SpanAttr { italic: true, ..a }),
4386            mk(|a| SpanAttr {
4387                underline: true,
4388                ..a
4389            }),
4390            mk(|a| SpanAttr { strike: true, ..a }),
4391            mk(|a| SpanAttr {
4392                color: Some(Color::RED),
4393                ..a
4394            }),
4395            mk(|a| SpanAttr {
4396                color: Some(Color::BLUE),
4397                ..a
4398            }),
4399            mk(|a| SpanAttr {
4400                bg: Some(Color::GREEN),
4401                ..a
4402            }),
4403            mk(|a| SpanAttr {
4404                size_delta: 4.0,
4405                ..a
4406            }),
4407            mk(|a| SpanAttr {
4408                link: Some("https://x".into()),
4409                ..a
4410            }),
4411            mk(|a| SpanAttr {
4412                mention: Some(7),
4413                ..a
4414            }),
4415            mk(|a| SpanAttr { code: true, ..a }),
4416        ];
4417        let hashes = sets
4418            .iter()
4419            .map(|sp| TextSystem::line_hash("hello", sp, 0, 0.0))
4420            .collect::<Vec<_>>();
4421        for i in 0..hashes.len() {
4422            for j in (i + 1)..hashes.len() {
4423                assert_ne!(hashes[i], hashes[j], "distinguish fields {i} vs {j}");
4424            }
4425        }
4426
4427        let s0 = vec![StyledSpan::new(0, 5, SpanAttr::bold())];
4428        let s1 = vec![StyledSpan::new(100, 105, SpanAttr::bold())];
4429        assert_eq!(
4430            TextSystem::line_hash("hello", &s0, 0, 0.0),
4431            TextSystem::line_hash("hello", &s1, 100, 0.0),
4432            "relatywizacja do line_byte_start"
4433        );
4434
4435        let w0 = TextSystem::line_hash("hello", &s0, 0, 0.0);
4436        let w1 = TextSystem::line_hash("hello", &s0, 0, 40.0);
4437        assert_ne!(w0, w1, "width wchodzi do line_hash");
4438    }
4439
4440    #[test]
4441    fn line_hash_delegates_to_f1_exactly() {
4442        use sequoia_buffer::{SpanAttr, StyledSpan, rebase_spans};
4443
4444        let cases: Vec<(&str, Vec<StyledSpan>, usize, f32)> = vec![
4445            ("", vec![], 0, 0.0),
4446            ("hello", vec![], 0, 0.0),
4447            (
4448                "hello",
4449                vec![StyledSpan::new(0, 5, SpanAttr::bold())],
4450                0,
4451                100.0,
4452            ),
4453            (
4454                "hello",
4455                vec![StyledSpan::new(100, 105, SpanAttr::link("x"))],
4456                100,
4457                40.0,
4458            ),
4459            (
4460                "a👨‍👩‍👧b 🇵🇱 café",
4461                vec![StyledSpan::new(1, 1 + "👨‍👩‍👧".len(), SpanAttr::bold())],
4462                0,
4463                0.0,
4464            ),
4465            ("yellow the goose of the self", vec![], 57, 300.0),
4466            (
4467                "q",
4468                vec![StyledSpan::new(
4469                    0,
4470                    1,
4471                    SpanAttr {
4472                        size_delta: f32::NAN,
4473                        ..SpanAttr::plain()
4474                    },
4475                )],
4476                0,
4477                0.0,
4478            ),
4479            (
4480                "q",
4481                vec![StyledSpan::new(
4482                    0,
4483                    1,
4484                    SpanAttr {
4485                        size_delta: f32::INFINITY,
4486                        ..SpanAttr::plain()
4487                    },
4488                )],
4489                0,
4490                0.0,
4491            ),
4492            (
4493                "q",
4494                vec![StyledSpan::new(
4495                    0,
4496                    1,
4497                    SpanAttr {
4498                        size_delta: 0.12,
4499                        ..SpanAttr::plain()
4500                    },
4501                )],
4502                0,
4503                0.0,
4504            ),
4505            (
4506                "q",
4507                vec![StyledSpan::new(
4508                    0,
4509                    1,
4510                    SpanAttr {
4511                        size_delta: 0.13,
4512                        ..SpanAttr::plain()
4513                    },
4514                )],
4515                0,
4516                0.0,
4517            ),
4518        ];
4519        for (text, spans, base, width) in &cases {
4520            assert_eq!(
4521                TextSystem::line_hash(text, spans, *base, *width),
4522                sequoia_buffer::line_hash(text, &rebase_spans(spans, *base), *width),
4523                "F2==F1 dla {text:?} base={base} w={width}"
4524            );
4525        }
4526
4527        let q1 = TextSystem::line_hash(
4528            "q",
4529            &[StyledSpan::new(
4530                0,
4531                1,
4532                SpanAttr {
4533                    size_delta: 0.12,
4534                    ..SpanAttr::plain()
4535                },
4536            )],
4537            0,
4538            0.0,
4539        );
4540        let q2 = TextSystem::line_hash(
4541            "q",
4542            &[StyledSpan::new(
4543                0,
4544                1,
4545                SpanAttr {
4546                    size_delta: 0.13,
4547                    ..SpanAttr::plain()
4548                },
4549            )],
4550            0,
4551            0.0,
4552        );
4553        assert_ne!(q1, q2, "0.12 (kwant 0) vs 0.13 (kwant 1)");
4554
4555        for d in [0.0, 0.1, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
4556            let h = TextSystem::line_hash(
4557                "q",
4558                &[StyledSpan::new(
4559                    0,
4560                    1,
4561                    SpanAttr {
4562                        size_delta: d,
4563                        ..SpanAttr::plain()
4564                    },
4565                )],
4566                0,
4567                0.0,
4568            );
4569
4570            let h0 = TextSystem::line_hash(
4571                "q",
4572                &[StyledSpan::new(
4573                    0,
4574                    1,
4575                    SpanAttr {
4576                        size_delta: 0.0,
4577                        ..SpanAttr::plain()
4578                    },
4579                )],
4580                0,
4581                0.0,
4582            );
4583            assert_eq!(h, h0, "delta {d} w kwancie 0");
4584        }
4585    }
4586
4587    #[test]
4588    fn line_width_is_correct_at_scale_two() {
4589        let mut lo = TextSystem::new();
4590        let mut hi = TextSystem::new();
4591        hi.set_scale_factor(2.0);
4592        let text = "Sequoia width check";
4593        let a = lo.shape_line_wrapped(0, text, 16.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4594        let b = hi.shape_line_wrapped(0, text, 16.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4595        assert!(
4596            (a.width() - b.width()).abs() < 1.5,
4597            "scale-independent logical width: {} vs {}",
4598            a.width(),
4599            b.width()
4600        );
4601        assert!(a.width() > 0.0 && b.width() > 0.0);
4602
4603        let plain = lo.shape(text, 16.0);
4604        assert!(
4605            (a.width() - plain.width()).abs() < 2.0,
4606            "ShapedLine.width ≈ ShapedText.width: {} vs {}",
4607            a.width(),
4608            plain.width()
4609        );
4610    }
4611
4612    #[test]
4613    fn line_cache_wrap_width_is_per_call_not_global() {
4614        let mut sys = TextSystem::new();
4615        let a = sys.shape_line_wrapped(
4616            0,
4617            "aaa bbb ccc ddd",
4618            16.0,
4619            FontWeight::NORMAL,
4620            40.0,
4621            WrapMode::Word,
4622        );
4623        let b = sys.shape_line_wrapped(
4624            0,
4625            "aaa bbb ccc ddd",
4626            16.0,
4627            FontWeight::NORMAL,
4628            400.0,
4629            WrapMode::Word,
4630        );
4631        assert!(!Rc::ptr_eq(&a, &b), "different widths = separate entries");
4632        assert!(
4633            b.row_count() <= a.row_count(),
4634            "szerszy wrap = mniej wierszy"
4635        );
4636        let c = sys.shape_line_wrapped(
4637            0,
4638            "aaa bbb ccc ddd",
4639            16.0,
4640            FontWeight::NORMAL,
4641            40.0,
4642            WrapMode::Word,
4643        );
4644        assert!(Rc::ptr_eq(&a, &c), "same width = hit");
4645
4646        assert!(a.height() >= b.height());
4647        assert_eq!(a.width_quant(), (40.0f32 * 4.0).round() as u32);
4648    }
4649
4650    #[test]
4651    fn line_cache_bidi_does_not_panic_and_keeps_glyphs() {
4652        let mut sys = TextSystem::new();
4653        for text in [
4654            "Hello שלום world",
4655            "مرحبا world",
4656            "aaa בּבבּ ccc",
4657            "mixed עברית and English 123",
4658        ] {
4659            let shaped =
4660                sys.shape_line_wrapped(0, text, 16.0, FontWeight::NORMAL, 0.0, WrapMode::Word);
4661            assert!(shaped.glyph_count() > 0, "bidi: {text:?}");
4662            assert!(shaped.width() >= 0.0);
4663            assert!(shaped.height() > 0.0);
4664            assert!(shaped.row_count() >= 1);
4665        }
4666    }
4667
4668    #[test]
4669    fn bench_shape_lines_visible_100_lines() {
4670        use std::time::Instant;
4671        let mut sys = TextSystem::new();
4672        let lines: Vec<String> = (0..1000)
4673            .map(|i| format!("visible bench line {i:04} with some words to shape"))
4674            .collect();
4675
4676        let _ = sys.shape_lines_visible(
4677            0..1000,
4678            |i| lines.get(i).cloned(),
4679            14.0,
4680            FontWeight::NORMAL,
4681            300.0,
4682            WrapMode::Word,
4683        );
4684
4685        sys.reset_line_reshapes();
4686        let t0 = Instant::now();
4687        let vis = sys.shape_lines_visible(
4688            200..300,
4689            |i| lines.get(i).cloned(),
4690            14.0,
4691            FontWeight::NORMAL,
4692            300.0,
4693            WrapMode::Word,
4694        );
4695        let dt_miss_or_hit = t0.elapsed();
4696        assert_eq!(vis.len(), 100);
4697
4698        sys.reset_line_reshapes();
4699        let t1 = Instant::now();
4700        let vis2 = sys.shape_lines_visible(
4701            200..300,
4702            |i| lines.get(i).cloned(),
4703            14.0,
4704            FontWeight::NORMAL,
4705            300.0,
4706            WrapMode::Word,
4707        );
4708        let dt_hit = t1.elapsed();
4709        assert_eq!(sys.line_reshapes(), 0, "VirtualList window fully cached");
4710        assert!(Rc::ptr_eq(&vis[0], &vis2[0]));
4711        eprintln!(
4712            "bench shape_lines_visible(100): pierwsze {dt_miss_or_hit:?}, hit {dt_hit:?} ({} linii w cache)",
4713            sys.line_cache_len()
4714        );
4715    }
4716}