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