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