1use crate::tokens;
11use crate::tree::{FontFamily, FontWeight, TextWrap};
12use cosmic_text::{
13 Attrs, Buffer, Cursor, Family, FontSystem, Metrics, Shaping, Weight, Wrap, fontdb,
14};
15use lru::LruCache;
16use std::cell::RefCell;
17use std::num::NonZeroUsize;
18
19const MONO_CHAR_WIDTH_FACTOR: f32 = 0.62;
20
21const BASELINE_MULTIPLIER: f32 = 0.93;
22
23#[derive(Clone, Debug, PartialEq)]
24pub struct TextLine {
25 pub text: String,
26 pub width: f32,
27 pub y: f32,
29 pub baseline: f32,
31 pub rtl: bool,
33}
34
35#[derive(Clone, Debug, PartialEq)]
36pub struct TextLayout {
37 pub lines: Vec<TextLine>,
38 pub width: f32,
39 pub height: f32,
40 pub line_height: f32,
41}
42
43impl TextLayout {
44 pub fn line_count(&self) -> usize {
45 self.lines.len().max(1)
46 }
47
48 pub fn measured(&self) -> MeasuredText {
49 MeasuredText {
50 width: self.width,
51 height: self.height,
52 line_count: self.line_count(),
53 }
54 }
55}
56
57#[derive(Clone, Debug, PartialEq)]
58pub struct MeasuredText {
59 pub width: f32,
60 pub height: f32,
61 pub line_count: usize,
62}
63
64#[derive(Clone, Debug, PartialEq)]
74pub struct TextGeometry<'a> {
75 text: &'a str,
76 size: f32,
77 family: FontFamily,
78 weight: FontWeight,
79 mono: bool,
80 wrap: TextWrap,
81 available_width: Option<f32>,
82 layout: TextLayout,
83}
84
85impl<'a> TextGeometry<'a> {
86 pub fn new(
87 text: &'a str,
88 size: f32,
89 weight: FontWeight,
90 mono: bool,
91 wrap: TextWrap,
92 available_width: Option<f32>,
93 ) -> Self {
94 Self::new_with_family(
95 text,
96 size,
97 FontFamily::default(),
98 weight,
99 mono,
100 wrap,
101 available_width,
102 )
103 }
104
105 pub fn new_with_family(
106 text: &'a str,
107 size: f32,
108 family: FontFamily,
109 weight: FontWeight,
110 mono: bool,
111 wrap: TextWrap,
112 available_width: Option<f32>,
113 ) -> Self {
114 let layout =
115 layout_text_with_family(text, size, family, weight, mono, wrap, available_width);
116 Self {
117 text,
118 size,
119 family,
120 weight,
121 mono,
122 wrap,
123 available_width,
124 layout,
125 }
126 }
127
128 pub fn text(&self) -> &'a str {
129 self.text
130 }
131
132 pub fn layout(&self) -> &TextLayout {
133 &self.layout
134 }
135
136 pub fn measured(&self) -> MeasuredText {
137 self.layout.measured()
138 }
139
140 pub fn line_height(&self) -> f32 {
141 self.layout.line_height
142 }
143
144 pub fn width(&self) -> f32 {
145 self.layout.width
146 }
147
148 pub fn height(&self) -> f32 {
149 self.layout.height
150 }
151
152 pub fn hit(&self, x: f32, y: f32) -> Option<TextHit> {
153 hit_text_with_family(
154 self.text,
155 self.size,
156 self.family,
157 self.weight,
158 self.wrap,
159 self.available_width,
160 x,
161 y,
162 )
163 }
164
165 pub fn hit_byte(&self, x: f32, y: f32) -> Option<usize> {
170 let hit = self.hit(x, y)?;
171 Some(self.byte_from_line_position(hit.line, hit.byte_index))
172 }
173
174 pub fn caret_xy(&self, byte_index: usize) -> (f32, f32) {
175 caret_xy_with_family(
176 self.text,
177 byte_index,
178 self.size,
179 self.family,
180 self.weight,
181 self.wrap,
182 self.available_width,
183 )
184 }
185
186 pub fn prefix_width(&self, byte_index: usize) -> f32 {
190 self.caret_xy(byte_index).0
191 }
192
193 pub fn selection_rects(&self, lo: usize, hi: usize) -> Vec<(f32, f32, f32, f32)> {
194 selection_rects_with_family(
195 self.text,
196 lo,
197 hi,
198 self.size,
199 self.family,
200 self.weight,
201 self.wrap,
202 self.available_width,
203 )
204 }
205
206 fn byte_from_line_position(&self, line: usize, byte_in_line: usize) -> usize {
207 line_position_to_byte(self.text, line, byte_in_line)
208 }
209}
210
211pub fn measure_text(
214 text: &str,
215 size: f32,
216 weight: FontWeight,
217 mono: bool,
218 wrap: TextWrap,
219 available_width: Option<f32>,
220) -> MeasuredText {
221 layout_text(text, size, weight, mono, wrap, available_width).measured()
222}
223
224pub fn layout_text(
228 text: &str,
229 size: f32,
230 weight: FontWeight,
231 mono: bool,
232 wrap: TextWrap,
233 available_width: Option<f32>,
234) -> TextLayout {
235 layout_text_with_family(
236 text,
237 size,
238 FontFamily::default(),
239 weight,
240 mono,
241 wrap,
242 available_width,
243 )
244}
245
246pub fn layout_text_with_family(
248 text: &str,
249 size: f32,
250 family: FontFamily,
251 weight: FontWeight,
252 mono: bool,
253 wrap: TextWrap,
254 available_width: Option<f32>,
255) -> TextLayout {
256 layout_text_with_line_height_and_family(
257 text,
258 size,
259 line_height(size),
260 family,
261 weight,
262 mono,
263 wrap,
264 available_width,
265 )
266}
267
268#[allow(clippy::too_many_arguments)]
272pub fn layout_text_with_line_height(
273 text: &str,
274 size: f32,
275 line_height: f32,
276 weight: FontWeight,
277 mono: bool,
278 wrap: TextWrap,
279 available_width: Option<f32>,
280) -> TextLayout {
281 layout_text_with_line_height_and_family(
282 text,
283 size,
284 line_height,
285 FontFamily::default(),
286 weight,
287 mono,
288 wrap,
289 available_width,
290 )
291}
292
293#[allow(clippy::too_many_arguments)]
294pub fn layout_text_with_line_height_and_family(
295 text: &str,
296 size: f32,
297 line_height: f32,
298 family: FontFamily,
299 weight: FontWeight,
300 mono: bool,
301 wrap: TextWrap,
302 available_width: Option<f32>,
303) -> TextLayout {
304 let key = ShapeKey {
315 text: Box::from(text),
316 size_bits: size.to_bits(),
317 line_height_bits: line_height.to_bits(),
318 family,
319 weight,
320 mono,
321 wrap,
322 available_width_bits: available_width.map(f32::to_bits),
323 };
324 if let Some(cached) = SHAPE_CACHE.with_borrow_mut(|c| c.get(&key).cloned()) {
325 return cached;
326 }
327 let layout = layout_text_uncached(
328 text,
329 size,
330 line_height,
331 family,
332 weight,
333 mono,
334 wrap,
335 available_width,
336 );
337 SHAPE_CACHE.with_borrow_mut(|c| {
338 c.put(key, layout.clone());
339 });
340 layout
341}
342
343#[allow(clippy::too_many_arguments)]
344fn layout_text_uncached(
345 text: &str,
346 size: f32,
347 line_height: f32,
348 family: FontFamily,
349 weight: FontWeight,
350 mono: bool,
351 wrap: TextWrap,
352 available_width: Option<f32>,
353) -> TextLayout {
354 if !mono
355 && let Some(layout) = layout_text_cosmic(
356 text,
357 size,
358 line_height,
359 family,
360 weight,
361 wrap,
362 available_width,
363 )
364 {
365 return layout;
366 }
367
368 let raw_lines = match (wrap, available_width) {
369 (TextWrap::Wrap, Some(width)) => {
370 wrap_lines_by_width(text, width, size, family, weight, mono)
371 }
372 _ => text.split('\n').map(str::to_string).collect(),
373 };
374 build_layout(raw_lines, size, line_height, family, weight, mono)
375}
376
377pub fn ellipsize_text(
380 text: &str,
381 size: f32,
382 weight: FontWeight,
383 mono: bool,
384 available_width: f32,
385) -> String {
386 ellipsize_text_with_family(
387 text,
388 size,
389 FontFamily::default(),
390 weight,
391 mono,
392 available_width,
393 )
394}
395
396pub fn ellipsize_text_with_family(
397 text: &str,
398 size: f32,
399 family: FontFamily,
400 weight: FontWeight,
401 mono: bool,
402 available_width: f32,
403) -> String {
404 if available_width <= 0.0 || text.is_empty() {
405 return String::new();
406 }
407 let full = layout_text_with_family(text, size, family, weight, mono, TextWrap::NoWrap, None);
408 if full.width <= available_width + 0.5 {
409 return text.to_string();
410 }
411
412 let ellipsis = "…";
413 let ellipsis_w =
414 layout_text_with_family(ellipsis, size, family, weight, mono, TextWrap::NoWrap, None).width;
415 if ellipsis_w > available_width + 0.5 {
416 return ellipsis.to_string();
417 }
418
419 let chars: Vec<char> = text.chars().collect();
420 let mut lo = 0usize;
421 let mut hi = chars.len();
422 while lo < hi {
423 let mid = (lo + hi).div_ceil(2);
424 let candidate: String = chars[..mid].iter().collect();
425 let candidate = format!("{candidate}{ellipsis}");
426 let width = layout_text_with_family(
427 &candidate,
428 size,
429 family,
430 weight,
431 mono,
432 TextWrap::NoWrap,
433 None,
434 )
435 .width;
436 if width <= available_width + 0.5 {
437 lo = mid;
438 } else {
439 hi = mid - 1;
440 }
441 }
442
443 let prefix: String = chars[..lo].iter().collect();
444 format!("{prefix}{ellipsis}")
445}
446
447pub fn clamp_text_to_lines(
450 text: &str,
451 size: f32,
452 weight: FontWeight,
453 mono: bool,
454 available_width: f32,
455 max_lines: usize,
456) -> String {
457 clamp_text_to_lines_with_family(
458 text,
459 size,
460 FontFamily::default(),
461 weight,
462 mono,
463 available_width,
464 max_lines,
465 )
466}
467
468pub fn clamp_text_to_lines_with_family(
469 text: &str,
470 size: f32,
471 family: FontFamily,
472 weight: FontWeight,
473 mono: bool,
474 available_width: f32,
475 max_lines: usize,
476) -> String {
477 if text.is_empty() || available_width <= 0.0 || max_lines == 0 {
478 return String::new();
479 }
480
481 let layout = layout_text_with_family(
482 text,
483 size,
484 family,
485 weight,
486 mono,
487 TextWrap::Wrap,
488 Some(available_width),
489 );
490 if layout.lines.len() <= max_lines {
491 return text.to_string();
492 }
493
494 let mut lines: Vec<String> = layout
495 .lines
496 .iter()
497 .take(max_lines)
498 .map(|line| line.text.clone())
499 .collect();
500 if let Some(last) = lines.last_mut() {
501 let marked = format!("{last}…");
502 *last = ellipsize_text_with_family(&marked, size, family, weight, mono, available_width);
503 }
504 lines.join("\n")
505}
506
507#[derive(Clone, Copy, Debug, PartialEq, Eq)]
511pub struct TextHit {
512 pub line: usize,
517 pub byte_index: usize,
520}
521
522pub fn hit_text(
533 text: &str,
534 size: f32,
535 weight: FontWeight,
536 wrap: TextWrap,
537 available_width: Option<f32>,
538 x: f32,
539 y: f32,
540) -> Option<TextHit> {
541 hit_text_with_family(
542 text,
543 size,
544 FontFamily::default(),
545 weight,
546 wrap,
547 available_width,
548 x,
549 y,
550 )
551}
552
553#[allow(clippy::too_many_arguments)]
554pub fn hit_text_with_family(
555 text: &str,
556 size: f32,
557 family: FontFamily,
558 weight: FontWeight,
559 wrap: TextWrap,
560 available_width: Option<f32>,
561 x: f32,
562 y: f32,
563) -> Option<TextHit> {
564 FONT_SYSTEM.with_borrow_mut(|font_system| {
565 let line_height = line_height(size);
566 let mut buffer = Buffer::new(font_system, Metrics::new(size, line_height));
567 buffer.set_wrap(match wrap {
568 TextWrap::NoWrap => Wrap::None,
569 TextWrap::Wrap => Wrap::WordOrGlyph,
570 });
571 buffer.set_size(
572 match wrap {
573 TextWrap::NoWrap => None,
574 TextWrap::Wrap => available_width,
575 },
576 None,
577 );
578 let attrs = Attrs::new()
579 .family(Family::Name(family.family_name()))
580 .weight(cosmic_weight(weight));
581 buffer.set_text(text, &attrs, Shaping::Advanced, None);
582 buffer.shape_until_scroll(font_system, false);
583 let cursor = buffer.hit(x, y)?;
584 Some(TextHit {
585 line: cursor.line,
586 byte_index: cursor.index,
587 })
588 })
589}
590
591pub fn caret_xy(
603 text: &str,
604 byte_index: usize,
605 size: f32,
606 weight: FontWeight,
607 wrap: TextWrap,
608 available_width: Option<f32>,
609) -> (f32, f32) {
610 caret_xy_with_family(
611 text,
612 byte_index,
613 size,
614 FontFamily::default(),
615 weight,
616 wrap,
617 available_width,
618 )
619}
620
621pub fn caret_xy_with_family(
622 text: &str,
623 byte_index: usize,
624 size: f32,
625 family: FontFamily,
626 weight: FontWeight,
627 wrap: TextWrap,
628 available_width: Option<f32>,
629) -> (f32, f32) {
630 let (target_line, byte_in_line) = byte_to_line_position(text, byte_index);
631 FONT_SYSTEM.with_borrow_mut(|font_system| {
632 let line_h = line_height(size);
633 let buffer = build_buffer(
634 font_system,
635 text,
636 size,
637 family,
638 weight,
639 wrap,
640 available_width,
641 );
642 let cursor = Cursor::new(target_line, byte_in_line);
643 if let Some((x, y)) = buffer.cursor_position(&cursor) {
647 return (x, y);
648 }
649 (0.0, target_line as f32 * line_h)
652 })
653}
654
655pub fn selection_rects(
663 text: &str,
664 lo: usize,
665 hi: usize,
666 size: f32,
667 weight: FontWeight,
668 wrap: TextWrap,
669 available_width: Option<f32>,
670) -> Vec<(f32, f32, f32, f32)> {
671 selection_rects_with_family(
672 text,
673 lo,
674 hi,
675 size,
676 FontFamily::default(),
677 weight,
678 wrap,
679 available_width,
680 )
681}
682
683#[allow(clippy::too_many_arguments)]
684pub fn selection_rects_with_family(
685 text: &str,
686 lo: usize,
687 hi: usize,
688 size: f32,
689 family: FontFamily,
690 weight: FontWeight,
691 wrap: TextWrap,
692 available_width: Option<f32>,
693) -> Vec<(f32, f32, f32, f32)> {
694 if lo >= hi {
695 return Vec::new();
696 }
697 let (lo_line, lo_in_line) = byte_to_line_position(text, lo);
698 let (hi_line, hi_in_line) = byte_to_line_position(text, hi);
699 FONT_SYSTEM.with_borrow_mut(|font_system| {
700 let buffer = build_buffer(
701 font_system,
702 text,
703 size,
704 family,
705 weight,
706 wrap,
707 available_width,
708 );
709 let c_lo = Cursor::new(lo_line, lo_in_line);
710 let c_hi = Cursor::new(hi_line, hi_in_line);
711 let mut rects = Vec::new();
712 for run in buffer.layout_runs() {
713 for (x, w) in run.highlight(c_lo, c_hi) {
714 rects.push((x, run.line_top, w, run.line_height));
715 }
716 }
717 rects
718 })
719}
720
721fn byte_to_line_position(text: &str, byte_index: usize) -> (usize, usize) {
726 let byte_index = byte_index.min(text.len());
727 let mut line = 0;
728 let mut line_start = 0;
729 for (i, ch) in text.char_indices() {
730 if i >= byte_index {
731 break;
732 }
733 if ch == '\n' {
734 line += 1;
735 line_start = i + ch.len_utf8();
736 }
737 }
738 (line, byte_index - line_start)
739}
740
741fn line_position_to_byte(text: &str, line: usize, byte_in_line: usize) -> usize {
742 let mut current_line = 0;
743 let mut line_start = 0;
744 for (i, ch) in text.char_indices() {
745 if current_line == line {
746 let candidate = line_start + byte_in_line;
747 return clamp_to_char_boundary(text, candidate.min(text.len()));
748 }
749 if ch == '\n' {
750 current_line += 1;
751 line_start = i + ch.len_utf8();
752 }
753 }
754 if current_line == line {
755 clamp_to_char_boundary(text, (line_start + byte_in_line).min(text.len()))
756 } else {
757 text.len()
758 }
759}
760
761fn clamp_to_char_boundary(text: &str, mut byte: usize) -> usize {
762 byte = byte.min(text.len());
763 while byte > 0 && !text.is_char_boundary(byte) {
764 byte -= 1;
765 }
766 byte
767}
768
769fn build_buffer(
770 font_system: &mut FontSystem,
771 text: &str,
772 size: f32,
773 family: FontFamily,
774 weight: FontWeight,
775 wrap: TextWrap,
776 available_width: Option<f32>,
777) -> Buffer {
778 let line_h = line_height(size);
779 let mut buffer = Buffer::new(font_system, Metrics::new(size, line_h));
780 buffer.set_wrap(match wrap {
781 TextWrap::NoWrap => Wrap::None,
782 TextWrap::Wrap => Wrap::WordOrGlyph,
783 });
784 buffer.set_size(
785 match wrap {
786 TextWrap::NoWrap => None,
787 TextWrap::Wrap => available_width,
788 },
789 None,
790 );
791 let attrs = Attrs::new()
792 .family(Family::Name(family.family_name()))
793 .weight(cosmic_weight(weight));
794 buffer.set_text(text, &attrs, Shaping::Advanced, None);
795 buffer.shape_until_scroll(font_system, false);
796 buffer
797}
798
799pub fn wrap_lines(
803 text: &str,
804 max_width: f32,
805 size: f32,
806 weight: FontWeight,
807 mono: bool,
808) -> Vec<String> {
809 wrap_lines_with_family(text, max_width, size, FontFamily::default(), weight, mono)
810}
811
812pub fn wrap_lines_with_family(
813 text: &str,
814 max_width: f32,
815 size: f32,
816 family: FontFamily,
817 weight: FontWeight,
818 mono: bool,
819) -> Vec<String> {
820 if !mono
821 && let Some(layout) = layout_text_cosmic(
822 text,
823 size,
824 line_height(size),
825 family,
826 weight,
827 TextWrap::Wrap,
828 Some(max_width),
829 )
830 {
831 return layout.lines.into_iter().map(|line| line.text).collect();
832 }
833 wrap_lines_by_width(text, max_width, size, family, weight, mono)
834}
835
836fn wrap_lines_by_width(
837 text: &str,
838 max_width: f32,
839 size: f32,
840 family: FontFamily,
841 weight: FontWeight,
842 mono: bool,
843) -> Vec<String> {
844 if max_width <= 0.0 {
845 return vec![String::new()];
846 }
847
848 let ctx = WrapMeasure {
849 max_width,
850 size,
851 family,
852 weight,
853 mono,
854 };
855 let mut out = Vec::new();
856 for paragraph in text.split('\n') {
857 if paragraph.is_empty() {
858 out.push(String::new());
859 continue;
860 }
861
862 let mut line = String::new();
863 for word in paragraph.split_whitespace() {
864 if line.is_empty() {
865 push_word_wrapped(&mut out, &mut line, word, ctx);
866 continue;
867 }
868
869 let candidate = format!("{line} {word}");
870 if line_width_with_family(&candidate, size, family, weight, mono) <= max_width {
871 line = candidate;
872 } else {
873 out.push(std::mem::take(&mut line));
874 push_word_wrapped(&mut out, &mut line, word, ctx);
875 }
876 }
877
878 if !line.is_empty() {
879 out.push(line);
880 }
881 }
882
883 if out.is_empty() {
884 out.push(String::new());
885 }
886 out
887}
888
889pub fn line_width(text: &str, size: f32, weight: FontWeight, mono: bool) -> f32 {
892 line_width_with_family(text, size, FontFamily::default(), weight, mono)
893}
894
895pub fn line_width_with_family(
896 text: &str,
897 size: f32,
898 family: FontFamily,
899 weight: FontWeight,
900 mono: bool,
901) -> f32 {
902 if !mono
903 && let Some(layout) = layout_text_cosmic(
904 text,
905 size,
906 line_height(size),
907 family,
908 weight,
909 TextWrap::NoWrap,
910 None,
911 )
912 {
913 return layout.width;
914 }
915 line_width_by_ttf(text, size, family, weight, mono)
916}
917
918fn line_width_by_ttf(
919 text: &str,
920 size: f32,
921 family: FontFamily,
922 weight: FontWeight,
923 mono: bool,
924) -> f32 {
925 if mono {
926 return text
927 .chars()
928 .filter(|c| *c != '\n' && *c != '\r')
929 .map(|c| if c == '\t' { 4.0 } else { 1.0 })
930 .sum::<f32>()
931 * size
932 * MONO_CHAR_WIDTH_FACTOR;
933 }
934
935 let Ok(face) = ttf_parser::Face::parse(font_bytes(family, weight), 0) else {
936 return fallback_line_width(text, size, mono);
937 };
938 let scale = size / face.units_per_em() as f32;
939 let fallback_advance = face.units_per_em() as f32 * 0.5;
940 let mut width = 0.0;
941 let mut prev = None;
942
943 for c in text.chars() {
944 if c == '\n' || c == '\r' {
945 continue;
946 }
947 if c == '\t' {
948 width += line_width_with_family(" ", size, family, weight, mono);
949 prev = None;
950 continue;
951 }
952
953 let Some(glyph) = glyph_for(&face, c) else {
954 continue;
955 };
956 if let Some(left) = prev {
957 width += kern(&face, left, glyph) * scale;
958 }
959 width += face
960 .glyph_hor_advance(glyph)
961 .map(|advance| advance as f32)
962 .unwrap_or(fallback_advance)
963 * scale;
964 prev = Some(glyph);
965 }
966 width
967}
968
969pub fn line_height(size: f32) -> f32 {
970 tokens::line_height_for_size(size)
975}
976
977fn build_layout(
978 lines: Vec<String>,
979 size: f32,
980 line_height: f32,
981 family: FontFamily,
982 weight: FontWeight,
983 mono: bool,
984) -> TextLayout {
985 let raw_lines = if lines.is_empty() {
986 vec![String::new()]
987 } else {
988 lines
989 };
990 let lines: Vec<TextLine> = raw_lines
991 .into_iter()
992 .enumerate()
993 .map(|(i, text)| {
994 let y = i as f32 * line_height;
995 TextLine {
996 width: line_width_with_family(&text, size, family, weight, mono),
997 text,
998 y,
999 baseline: y + size * BASELINE_MULTIPLIER,
1000 rtl: false,
1001 }
1002 })
1003 .collect();
1004 let width = lines.iter().map(|line| line.width).fold(0.0, f32::max);
1005 TextLayout {
1006 width,
1007 height: lines.len().max(1) as f32 * line_height,
1008 line_height,
1009 lines,
1010 }
1011}
1012
1013fn layout_text_cosmic(
1014 text: &str,
1015 size: f32,
1016 line_height: f32,
1017 family: FontFamily,
1018 weight: FontWeight,
1019 wrap: TextWrap,
1020 available_width: Option<f32>,
1021) -> Option<TextLayout> {
1022 let options = CosmicLayoutOptions {
1023 size,
1024 line_height,
1025 family,
1026 weight,
1027 wrap,
1028 available_width,
1029 };
1030 FONT_SYSTEM.with_borrow_mut(|font_system| layout_text_cosmic_with(font_system, text, options))
1031}
1032
1033#[derive(Copy, Clone)]
1034struct CosmicLayoutOptions {
1035 size: f32,
1036 line_height: f32,
1037 family: FontFamily,
1038 weight: FontWeight,
1039 wrap: TextWrap,
1040 available_width: Option<f32>,
1041}
1042
1043fn layout_text_cosmic_with(
1044 font_system: &mut FontSystem,
1045 text: &str,
1046 options: CosmicLayoutOptions,
1047) -> Option<TextLayout> {
1048 let CosmicLayoutOptions {
1049 size,
1050 line_height,
1051 family,
1052 weight,
1053 wrap,
1054 available_width,
1055 } = options;
1056 let mut buffer = Buffer::new(font_system, Metrics::new(size, line_height));
1057 buffer.set_wrap(match wrap {
1058 TextWrap::NoWrap => Wrap::None,
1059 TextWrap::Wrap => Wrap::WordOrGlyph,
1060 });
1061 buffer.set_size(
1062 match wrap {
1063 TextWrap::NoWrap => None,
1064 TextWrap::Wrap => available_width,
1065 },
1066 None,
1067 );
1068 let attrs = Attrs::new()
1069 .family(Family::Name(family.family_name()))
1070 .weight(cosmic_weight(weight));
1071 buffer.set_text(text, &attrs, Shaping::Advanced, None);
1072 buffer.shape_until_scroll(font_system, false);
1073
1074 let mut lines = Vec::new();
1075 let mut height: f32 = 0.0;
1076 for run in buffer.layout_runs() {
1077 height = height.max(run.line_top + run.line_height);
1078 lines.push(TextLine {
1079 text: layout_run_text(&run),
1080 width: run.line_w,
1081 y: run.line_top,
1082 baseline: run.line_y,
1083 rtl: run.rtl,
1084 });
1085 }
1086
1087 if lines.is_empty() {
1088 return None;
1089 }
1090
1091 let width = lines.iter().map(|line| line.width).fold(0.0, f32::max);
1092 Some(TextLayout {
1093 lines,
1094 width,
1095 height: height.max(line_height),
1096 line_height,
1097 })
1098}
1099
1100thread_local! {
1106 static FONT_SYSTEM: RefCell<FontSystem> = RefCell::new(bundled_font_system());
1107}
1108
1109#[derive(Clone, PartialEq, Eq, Hash)]
1116struct ShapeKey {
1117 text: Box<str>,
1118 size_bits: u32,
1119 line_height_bits: u32,
1120 family: FontFamily,
1121 weight: FontWeight,
1122 mono: bool,
1123 wrap: TextWrap,
1124 available_width_bits: Option<u32>,
1125}
1126
1127const SHAPE_CACHE_CAPACITY: usize = 1024;
1132thread_local! {
1133 static SHAPE_CACHE: RefCell<LruCache<ShapeKey, TextLayout>> =
1134 RefCell::new(LruCache::new(NonZeroUsize::new(SHAPE_CACHE_CAPACITY).unwrap()));
1135}
1136
1137fn bundled_font_system() -> FontSystem {
1138 let mut db = fontdb::Database::new();
1139 db.set_sans_serif_family(FontFamily::default().family_name());
1140 for bytes in aetna_fonts::DEFAULT_FONTS {
1141 db.load_font_data(bytes.to_vec());
1142 }
1143 FontSystem::new_with_locale_and_db("en-US".to_string(), db)
1144}
1145
1146fn cosmic_weight(weight: FontWeight) -> Weight {
1147 match weight {
1148 FontWeight::Regular => Weight::NORMAL,
1149 FontWeight::Medium => Weight::MEDIUM,
1150 FontWeight::Semibold => Weight::SEMIBOLD,
1151 FontWeight::Bold => Weight::BOLD,
1152 }
1153}
1154
1155fn layout_run_text(run: &cosmic_text::LayoutRun<'_>) -> String {
1156 let Some(start) = run.glyphs.iter().map(|glyph| glyph.start).min() else {
1157 return String::new();
1158 };
1159 let end = run
1160 .glyphs
1161 .iter()
1162 .map(|glyph| glyph.end)
1163 .max()
1164 .unwrap_or(start);
1165 run.text
1166 .get(start..end)
1167 .unwrap_or_default()
1168 .trim_end()
1169 .to_string()
1170}
1171
1172#[derive(Copy, Clone)]
1173struct WrapMeasure {
1174 max_width: f32,
1175 size: f32,
1176 family: FontFamily,
1177 weight: FontWeight,
1178 mono: bool,
1179}
1180
1181fn push_word_wrapped(out: &mut Vec<String>, line: &mut String, word: &str, ctx: WrapMeasure) {
1182 let WrapMeasure {
1183 max_width,
1184 size,
1185 family,
1186 weight,
1187 mono,
1188 } = ctx;
1189 if line_width_with_family(word, size, family, weight, mono) <= max_width {
1190 line.push_str(word);
1191 return;
1192 }
1193
1194 for ch in word.chars() {
1195 let candidate = format!("{line}{ch}");
1196 if !line.is_empty()
1197 && line_width_with_family(&candidate, size, family, weight, mono) > max_width
1198 {
1199 out.push(std::mem::take(line));
1200 }
1201 line.push(ch);
1202 }
1203}
1204
1205fn glyph_for(face: &ttf_parser::Face<'_>, c: char) -> Option<ttf_parser::GlyphId> {
1206 face.glyph_index(c)
1207 .or_else(|| face.glyph_index('\u{FFFD}'))
1208 .or_else(|| face.glyph_index('?'))
1209 .or_else(|| face.glyph_index(' '))
1210}
1211
1212fn kern(face: &ttf_parser::Face<'_>, left: ttf_parser::GlyphId, right: ttf_parser::GlyphId) -> f32 {
1213 let Some(kern) = &face.tables().kern else {
1214 return 0.0;
1215 };
1216 kern.subtables
1217 .into_iter()
1218 .filter(|subtable| subtable.horizontal && !subtable.has_cross_stream)
1219 .find_map(|subtable| subtable.glyphs_kerning(left, right))
1220 .map(|value| value as f32)
1221 .unwrap_or(0.0)
1222}
1223
1224fn font_bytes(family: FontFamily, weight: FontWeight) -> &'static [u8] {
1225 match family {
1229 FontFamily::Inter => {
1230 #[cfg(feature = "inter")]
1231 {
1232 let _ = weight;
1233 aetna_fonts::INTER_VARIABLE
1234 }
1235 #[cfg(not(feature = "inter"))]
1236 {
1237 let _ = weight;
1238 &[]
1239 }
1240 }
1241 FontFamily::Roboto => {
1242 #[cfg(feature = "roboto")]
1243 {
1244 match weight {
1245 FontWeight::Regular => aetna_fonts::ROBOTO_REGULAR,
1246 FontWeight::Medium => aetna_fonts::ROBOTO_MEDIUM,
1247 FontWeight::Semibold | FontWeight::Bold => aetna_fonts::ROBOTO_BOLD,
1248 }
1249 }
1250 #[cfg(not(feature = "roboto"))]
1251 {
1252 let _ = weight;
1253 &[]
1254 }
1255 }
1256 FontFamily::JetBrainsMono => {
1257 #[cfg(feature = "jetbrains-mono")]
1258 {
1259 let _ = weight;
1260 aetna_fonts::JETBRAINS_MONO_VARIABLE
1261 }
1262 #[cfg(not(feature = "jetbrains-mono"))]
1263 {
1264 let _ = weight;
1265 &[]
1266 }
1267 }
1268 }
1269}
1270
1271fn fallback_line_width(text: &str, size: f32, mono: bool) -> f32 {
1272 let char_w = size * if mono { MONO_CHAR_WIDTH_FACTOR } else { 0.60 };
1273 text.chars().count() as f32 * char_w
1274}
1275
1276#[cfg(test)]
1277mod tests {
1278 use super::*;
1279
1280 #[test]
1281 fn proportional_measurement_distinguishes_narrow_and_wide_glyphs() {
1282 let narrow = line_width("iiiiii", 16.0, FontWeight::Regular, false);
1283 let wide = line_width("WWWWWW", 16.0, FontWeight::Regular, false);
1284
1285 assert!(wide > narrow * 2.0, "wide={wide} narrow={narrow}");
1286 }
1287
1288 #[cfg(feature = "roboto")]
1289 #[test]
1290 fn font_family_changes_proportional_measurement() {
1291 let roboto = line_width_with_family(
1292 "Save changes",
1293 14.0,
1294 FontFamily::Roboto,
1295 FontWeight::Semibold,
1296 false,
1297 );
1298 let inter = line_width_with_family(
1299 "Save changes",
1300 14.0,
1301 FontFamily::Inter,
1302 FontWeight::Semibold,
1303 false,
1304 );
1305
1306 assert!(
1307 (inter - roboto).abs() > 1.0,
1308 "inter={inter} roboto={roboto}"
1309 );
1310 }
1311
1312 #[test]
1313 fn wrap_lines_respects_measured_widths() {
1314 let lines = wrap_lines(
1315 "wide WWW words stay measured",
1316 120.0,
1317 16.0,
1318 FontWeight::Regular,
1319 false,
1320 );
1321
1322 assert!(lines.len() > 1);
1323 for line in lines {
1324 assert!(
1325 line_width(&line, 16.0, FontWeight::Regular, false) <= 121.0,
1326 "{line:?} overflowed"
1327 );
1328 }
1329 }
1330
1331 #[test]
1332 fn layout_text_carries_line_positions_and_measurement() {
1333 let layout = layout_text(
1334 "alpha beta gamma",
1335 16.0,
1336 FontWeight::Regular,
1337 false,
1338 TextWrap::Wrap,
1339 Some(80.0),
1340 );
1341
1342 assert!(layout.lines.len() > 1);
1343 assert_eq!(layout.measured().line_count, layout.lines.len());
1344 assert_eq!(layout.lines[0].y, 0.0);
1345 assert_eq!(layout.lines[1].y, layout.line_height);
1346 assert!(layout.lines[0].baseline > layout.lines[0].y);
1347 assert!(layout.height >= layout.line_height * 2.0);
1348 }
1349
1350 #[test]
1351 fn tokenized_line_heights_match_shadcn_scale() {
1352 assert_eq!(line_height(12.0), 16.0);
1353 assert_eq!(line_height(14.0), 20.0);
1354 assert_eq!(line_height(16.0), 24.0);
1355 assert_eq!(line_height(24.0), 32.0);
1356 assert_eq!(line_height(30.0), 36.0);
1357 }
1358
1359 #[test]
1360 fn hit_text_at_origin_lands_on_first_byte() {
1361 let hit = hit_text(
1362 "hello world",
1363 16.0,
1364 FontWeight::Regular,
1365 TextWrap::NoWrap,
1366 None,
1367 0.0,
1368 8.0,
1369 )
1370 .expect("hit at origin");
1371 assert_eq!(hit.line, 0);
1372 assert_eq!(hit.byte_index, 0);
1373 }
1374
1375 #[test]
1376 fn hit_text_past_last_glyph_clamps_to_end() {
1377 let text = "hello";
1378 let hit = hit_text(
1380 text,
1381 16.0,
1382 FontWeight::Regular,
1383 TextWrap::NoWrap,
1384 None,
1385 1000.0,
1386 8.0,
1387 )
1388 .expect("hit past end");
1389 assert_eq!(hit.line, 0);
1390 assert_eq!(hit.byte_index, text.len());
1391 }
1392
1393 #[test]
1394 fn hit_text_walks_columns_left_to_right() {
1395 let text = "abcdefghij";
1399 let mut prev = 0usize;
1400 for x in [4.0, 16.0, 32.0, 64.0, 96.0] {
1401 let hit = hit_text(
1402 text,
1403 16.0,
1404 FontWeight::Regular,
1405 TextWrap::NoWrap,
1406 None,
1407 x,
1408 8.0,
1409 );
1410 let Some(hit) = hit else { continue };
1411 assert!(
1412 hit.byte_index >= prev,
1413 "byte_index regressed at x={x}: {} < {prev}",
1414 hit.byte_index
1415 );
1416 prev = hit.byte_index;
1417 }
1418 }
1419
1420 #[test]
1421 fn text_geometry_hit_byte_maps_hard_line_offsets_to_source_bytes() {
1422 let text = "alpha\nbeta";
1423 let geometry = TextGeometry::new(
1424 text,
1425 16.0,
1426 FontWeight::Regular,
1427 false,
1428 TextWrap::NoWrap,
1429 None,
1430 );
1431 let y = geometry.line_height() * 1.5;
1432 let byte = geometry.hit_byte(1000.0, y).expect("hit on second line");
1433 assert_eq!(byte, text.len());
1434 }
1435
1436 #[test]
1437 fn text_geometry_prefix_width_matches_caret_x() {
1438 let text = "hello world";
1439 let geometry = TextGeometry::new(
1440 text,
1441 16.0,
1442 FontWeight::Regular,
1443 false,
1444 TextWrap::NoWrap,
1445 None,
1446 );
1447 let (x, _y) = geometry.caret_xy(5);
1448 assert!((geometry.prefix_width(5) - x).abs() < 0.01);
1449 }
1450
1451 #[test]
1452 fn caret_xy_at_origin_is_zero_zero() {
1453 let (x, y) = caret_xy(
1454 "hello",
1455 0,
1456 16.0,
1457 FontWeight::Regular,
1458 TextWrap::NoWrap,
1459 None,
1460 );
1461 assert!(x.abs() < 0.01, "x={x}");
1462 assert_eq!(y, 0.0);
1463 }
1464
1465 #[test]
1466 fn caret_xy_at_end_of_line_is_at_line_width() {
1467 let text = "hello";
1468 let width = line_width(text, 16.0, FontWeight::Regular, false);
1469 let (x, y) = caret_xy(
1470 text,
1471 text.len(),
1472 16.0,
1473 FontWeight::Regular,
1474 TextWrap::NoWrap,
1475 None,
1476 );
1477 assert!((x - width).abs() < 1.0, "x={x} expected~{width}");
1478 assert_eq!(y, 0.0);
1479 }
1480
1481 #[test]
1482 fn caret_xy_drops_to_next_line_after_newline() {
1483 let text = "foo\nbar";
1484 let line_h = line_height(16.0);
1485 let (x, y) = caret_xy(text, 4, 16.0, FontWeight::Regular, TextWrap::NoWrap, None);
1487 assert!(x.abs() < 0.01, "x={x}");
1488 assert!((y - line_h).abs() < 0.01, "y={y} expected~{line_h}");
1489 }
1490
1491 #[test]
1492 fn caret_xy_on_phantom_trailing_line_falls_below_text() {
1493 let text = "foo\n";
1494 let line_h = line_height(16.0);
1495 let (x, y) = caret_xy(
1496 text,
1497 text.len(),
1498 16.0,
1499 FontWeight::Regular,
1500 TextWrap::NoWrap,
1501 None,
1502 );
1503 assert!(x.abs() < 0.01, "x={x}");
1504 assert!(y >= line_h - 0.01, "y={y} expected ≥ line_h={line_h}");
1505 }
1506
1507 #[test]
1508 fn selection_rects_returns_one_per_visual_line() {
1509 let text = "alpha\nbeta\ngamma";
1510 let rects = selection_rects(
1511 text,
1512 0,
1513 text.len(),
1514 16.0,
1515 FontWeight::Regular,
1516 TextWrap::NoWrap,
1517 None,
1518 );
1519 assert_eq!(
1520 rects.len(),
1521 3,
1522 "expected one rect per BufferLine, got {rects:?}"
1523 );
1524 assert!(rects[0].1 < rects[1].1);
1526 assert!(rects[1].1 < rects[2].1);
1527 for (_x, _y, w, _h) in &rects {
1528 assert!(*w > 0.0, "empty width: {rects:?}");
1529 }
1530 }
1531
1532 #[test]
1533 fn selection_rects_empty_for_collapsed_range() {
1534 let rects = selection_rects(
1535 "alpha",
1536 2,
1537 2,
1538 16.0,
1539 FontWeight::Regular,
1540 TextWrap::NoWrap,
1541 None,
1542 );
1543 assert!(rects.is_empty());
1544 }
1545
1546 #[test]
1547 fn proportional_layout_uses_cosmic_shaping_widths() {
1548 let layout = layout_text(
1549 "Roboto shaping",
1550 18.0,
1551 FontWeight::Medium,
1552 false,
1553 TextWrap::NoWrap,
1554 None,
1555 );
1556
1557 assert_eq!(layout.lines.len(), 1);
1558 assert!((layout.lines[0].width - layout.width).abs() < 0.01);
1559 assert!(layout.lines[0].baseline > layout.lines[0].y);
1560 }
1561
1562 #[test]
1563 fn ellipsize_text_shortens_to_available_width() {
1564 let source = "this is a long branch name";
1565 let available = line_width("this is a…", 14.0, FontWeight::Regular, false);
1566 let clipped = ellipsize_text(source, 14.0, FontWeight::Regular, false, available);
1567 let width = line_width(&clipped, 14.0, FontWeight::Regular, false);
1568
1569 assert!(clipped.ends_with('…'), "clipped={clipped}");
1570 assert!(clipped.len() < source.len());
1571 assert!(
1572 width <= available + 0.5,
1573 "width={width} available={available}"
1574 );
1575 }
1576
1577 #[test]
1578 fn ellipsize_text_keeps_fitting_text_unchanged() {
1579 let source = "short";
1580 let available = line_width(source, 14.0, FontWeight::Regular, false) + 4.0;
1581 assert_eq!(
1582 ellipsize_text(source, 14.0, FontWeight::Regular, false, available),
1583 source
1584 );
1585 }
1586
1587 #[test]
1588 fn clamp_text_to_lines_caps_wrapped_text_with_final_ellipsis() {
1589 let source = "alpha beta gamma delta epsilon zeta";
1590 let available = line_width("alpha beta", 14.0, FontWeight::Regular, false);
1591 let clamped = clamp_text_to_lines(source, 14.0, FontWeight::Regular, false, available, 2);
1592 let layout = layout_text(
1593 &clamped,
1594 14.0,
1595 FontWeight::Regular,
1596 false,
1597 TextWrap::Wrap,
1598 Some(available),
1599 );
1600
1601 assert!(clamped.ends_with('…'), "clamped={clamped}");
1602 assert!(layout.lines.len() <= 2, "layout={layout:?}");
1603 }
1604}