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