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