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