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