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