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 if run.line_i < lo_line || run.line_i > hi_line {
714 continue;
715 }
716 for (x, w) in run.highlight(c_lo, c_hi) {
717 rects.push((x, run.line_top, w, run.line_height));
718 }
719 }
720 rects
721 })
722}
723
724pub fn visual_line_byte_range(
725 text: &str,
726 byte_index: usize,
727 size: f32,
728 weight: FontWeight,
729 wrap: TextWrap,
730 available_width: Option<f32>,
731) -> (usize, usize) {
732 visual_line_byte_range_with_family(
733 text,
734 byte_index,
735 size,
736 FontFamily::default(),
737 weight,
738 wrap,
739 available_width,
740 )
741}
742
743pub fn visual_line_byte_range_with_family(
744 text: &str,
745 byte_index: usize,
746 size: f32,
747 family: FontFamily,
748 weight: FontWeight,
749 wrap: TextWrap,
750 available_width: Option<f32>,
751) -> (usize, usize) {
752 let byte_index = clamp_to_char_boundary(text, byte_index.min(text.len()));
753 let (target_line, byte_in_line) = byte_to_line_position(text, byte_index);
754 let hard_line_start = line_position_to_byte(text, target_line, 0);
755 let hard_line_end = line_end_byte(text, hard_line_start);
756 FONT_SYSTEM.with_borrow_mut(|font_system| {
757 let buffer = build_buffer(
758 font_system,
759 text,
760 size,
761 family,
762 weight,
763 wrap,
764 available_width,
765 );
766 let mut last_range = None;
767 for run in buffer.layout_runs() {
768 if run.line_i != target_line {
769 continue;
770 }
771 let Some((start, end)) = layout_run_byte_range(&run) else {
772 continue;
773 };
774 last_range = Some((start, end));
775 if start <= byte_in_line && byte_in_line < end {
776 return (
777 line_position_to_byte(text, target_line, start),
778 line_position_to_byte(text, target_line, end),
779 );
780 }
781 }
782 if let Some((start, end)) = last_range
783 && byte_index >= line_position_to_byte(text, target_line, start)
784 {
785 return (
786 line_position_to_byte(text, target_line, start),
787 line_position_to_byte(text, target_line, end),
788 );
789 }
790 (hard_line_start, hard_line_end)
791 })
792}
793
794fn byte_to_line_position(text: &str, byte_index: usize) -> (usize, usize) {
799 let byte_index = byte_index.min(text.len());
800 let mut line = 0;
801 let mut line_start = 0;
802 for (i, ch) in text.char_indices() {
803 if i >= byte_index {
804 break;
805 }
806 if ch == '\n' {
807 line += 1;
808 line_start = i + ch.len_utf8();
809 }
810 }
811 (line, byte_index - line_start)
812}
813
814fn line_position_to_byte(text: &str, line: usize, byte_in_line: usize) -> usize {
815 let mut current_line = 0;
816 let mut line_start = 0;
817 for (i, ch) in text.char_indices() {
818 if current_line == line {
819 let candidate = line_start + byte_in_line;
820 return clamp_to_char_boundary(text, candidate.min(text.len()));
821 }
822 if ch == '\n' {
823 current_line += 1;
824 line_start = i + ch.len_utf8();
825 }
826 }
827 if current_line == line {
828 clamp_to_char_boundary(text, (line_start + byte_in_line).min(text.len()))
829 } else {
830 text.len()
831 }
832}
833
834fn line_end_byte(text: &str, line_start: usize) -> usize {
835 text[line_start..]
836 .find('\n')
837 .map(|i| line_start + i)
838 .unwrap_or(text.len())
839}
840
841fn layout_run_byte_range(run: &cosmic_text::LayoutRun<'_>) -> Option<(usize, usize)> {
842 let start = run.glyphs.iter().map(|glyph| glyph.start).min()?;
843 let end = run
844 .glyphs
845 .iter()
846 .map(|glyph| glyph.end)
847 .max()
848 .unwrap_or(start);
849 Some((start, end))
850}
851
852fn clamp_to_char_boundary(text: &str, mut byte: usize) -> usize {
853 byte = byte.min(text.len());
854 while byte > 0 && !text.is_char_boundary(byte) {
855 byte -= 1;
856 }
857 byte
858}
859
860fn build_buffer(
861 font_system: &mut FontSystem,
862 text: &str,
863 size: f32,
864 family: FontFamily,
865 weight: FontWeight,
866 wrap: TextWrap,
867 available_width: Option<f32>,
868) -> Buffer {
869 let line_h = line_height(size);
870 let mut buffer = Buffer::new(font_system, Metrics::new(size, line_h));
871 buffer.set_wrap(match wrap {
872 TextWrap::NoWrap => Wrap::None,
873 TextWrap::Wrap => Wrap::WordOrGlyph,
874 });
875 buffer.set_size(
876 match wrap {
877 TextWrap::NoWrap => None,
878 TextWrap::Wrap => available_width,
879 },
880 None,
881 );
882 let attrs = Attrs::new()
883 .family(Family::Name(family.family_name()))
884 .weight(cosmic_weight(weight));
885 buffer.set_text(text, &attrs, Shaping::Advanced, None);
886 buffer.shape_until_scroll(font_system, false);
887 buffer
888}
889
890pub fn wrap_lines(
894 text: &str,
895 max_width: f32,
896 size: f32,
897 weight: FontWeight,
898 mono: bool,
899) -> Vec<String> {
900 wrap_lines_with_family(text, max_width, size, FontFamily::default(), weight, mono)
901}
902
903pub fn wrap_lines_with_family(
904 text: &str,
905 max_width: f32,
906 size: f32,
907 family: FontFamily,
908 weight: FontWeight,
909 mono: bool,
910) -> Vec<String> {
911 if !mono
912 && let Some(layout) = layout_text_cosmic(
913 text,
914 size,
915 line_height(size),
916 family,
917 weight,
918 TextWrap::Wrap,
919 Some(max_width),
920 )
921 {
922 return layout.lines.into_iter().map(|line| line.text).collect();
923 }
924 wrap_lines_by_width(text, max_width, size, family, weight, mono)
925}
926
927fn wrap_lines_by_width(
928 text: &str,
929 max_width: f32,
930 size: f32,
931 family: FontFamily,
932 weight: FontWeight,
933 mono: bool,
934) -> Vec<String> {
935 if max_width <= 0.0 {
936 return vec![String::new()];
937 }
938
939 let ctx = WrapMeasure {
940 max_width,
941 size,
942 family,
943 weight,
944 mono,
945 };
946 let mut out = Vec::new();
947 for paragraph in text.split('\n') {
948 if paragraph.is_empty() {
949 out.push(String::new());
950 continue;
951 }
952
953 let mut line = String::new();
954 for word in paragraph.split_whitespace() {
955 if line.is_empty() {
956 push_word_wrapped(&mut out, &mut line, word, ctx);
957 continue;
958 }
959
960 let candidate = format!("{line} {word}");
961 if line_width_with_family(&candidate, size, family, weight, mono) <= max_width {
962 line = candidate;
963 } else {
964 out.push(std::mem::take(&mut line));
965 push_word_wrapped(&mut out, &mut line, word, ctx);
966 }
967 }
968
969 if !line.is_empty() {
970 out.push(line);
971 }
972 }
973
974 if out.is_empty() {
975 out.push(String::new());
976 }
977 out
978}
979
980pub fn line_width(text: &str, size: f32, weight: FontWeight, mono: bool) -> f32 {
983 line_width_with_family(text, size, FontFamily::default(), weight, mono)
984}
985
986pub fn line_width_with_family(
987 text: &str,
988 size: f32,
989 family: FontFamily,
990 weight: FontWeight,
991 mono: bool,
992) -> f32 {
993 if !mono
994 && let Some(layout) = layout_text_cosmic(
995 text,
996 size,
997 line_height(size),
998 family,
999 weight,
1000 TextWrap::NoWrap,
1001 None,
1002 )
1003 {
1004 return layout.width;
1005 }
1006 line_width_by_ttf(text, size, family, weight, mono)
1007}
1008
1009fn line_width_by_ttf(
1010 text: &str,
1011 size: f32,
1012 family: FontFamily,
1013 weight: FontWeight,
1014 mono: bool,
1015) -> f32 {
1016 if mono {
1017 return text
1018 .chars()
1019 .filter(|c| *c != '\n' && *c != '\r')
1020 .map(|c| if c == '\t' { 4.0 } else { 1.0 })
1021 .sum::<f32>()
1022 * size
1023 * MONO_CHAR_WIDTH_FACTOR;
1024 }
1025
1026 let Ok(face) = ttf_parser::Face::parse(font_bytes(family, weight), 0) else {
1027 return fallback_line_width(text, size, mono);
1028 };
1029 let scale = size / face.units_per_em() as f32;
1030 let fallback_advance = face.units_per_em() as f32 * 0.5;
1031 let mut width = 0.0;
1032 let mut prev = None;
1033
1034 for c in text.chars() {
1035 if c == '\n' || c == '\r' {
1036 continue;
1037 }
1038 if c == '\t' {
1039 width += line_width_with_family(" ", size, family, weight, mono);
1040 prev = None;
1041 continue;
1042 }
1043
1044 let Some(glyph) = glyph_for(&face, c) else {
1045 continue;
1046 };
1047 if let Some(left) = prev {
1048 width += kern(&face, left, glyph) * scale;
1049 }
1050 width += face
1051 .glyph_hor_advance(glyph)
1052 .map(|advance| advance as f32)
1053 .unwrap_or(fallback_advance)
1054 * scale;
1055 prev = Some(glyph);
1056 }
1057 width
1058}
1059
1060pub fn line_height(size: f32) -> f32 {
1061 tokens::line_height_for_size(size)
1066}
1067
1068fn build_layout(
1069 lines: Vec<String>,
1070 size: f32,
1071 line_height: f32,
1072 family: FontFamily,
1073 weight: FontWeight,
1074 mono: bool,
1075) -> TextLayout {
1076 let raw_lines = if lines.is_empty() {
1077 vec![String::new()]
1078 } else {
1079 lines
1080 };
1081 let lines: Vec<TextLine> = raw_lines
1082 .into_iter()
1083 .enumerate()
1084 .map(|(i, text)| {
1085 let y = i as f32 * line_height;
1086 TextLine {
1087 width: line_width_with_family(&text, size, family, weight, mono),
1088 text,
1089 y,
1090 baseline: y + size * BASELINE_MULTIPLIER,
1091 rtl: false,
1092 }
1093 })
1094 .collect();
1095 let width = lines.iter().map(|line| line.width).fold(0.0, f32::max);
1096 TextLayout {
1097 width,
1098 height: lines.len().max(1) as f32 * line_height,
1099 line_height,
1100 lines,
1101 }
1102}
1103
1104fn layout_text_cosmic(
1105 text: &str,
1106 size: f32,
1107 line_height: f32,
1108 family: FontFamily,
1109 weight: FontWeight,
1110 wrap: TextWrap,
1111 available_width: Option<f32>,
1112) -> Option<TextLayout> {
1113 let options = CosmicLayoutOptions {
1114 size,
1115 line_height,
1116 family,
1117 weight,
1118 wrap,
1119 available_width,
1120 };
1121 FONT_SYSTEM.with_borrow_mut(|font_system| layout_text_cosmic_with(font_system, text, options))
1122}
1123
1124#[derive(Copy, Clone)]
1125struct CosmicLayoutOptions {
1126 size: f32,
1127 line_height: f32,
1128 family: FontFamily,
1129 weight: FontWeight,
1130 wrap: TextWrap,
1131 available_width: Option<f32>,
1132}
1133
1134fn layout_text_cosmic_with(
1135 font_system: &mut FontSystem,
1136 text: &str,
1137 options: CosmicLayoutOptions,
1138) -> Option<TextLayout> {
1139 let CosmicLayoutOptions {
1140 size,
1141 line_height,
1142 family,
1143 weight,
1144 wrap,
1145 available_width,
1146 } = options;
1147 let mut buffer = Buffer::new(font_system, Metrics::new(size, line_height));
1148 buffer.set_wrap(match wrap {
1149 TextWrap::NoWrap => Wrap::None,
1150 TextWrap::Wrap => Wrap::WordOrGlyph,
1151 });
1152 buffer.set_size(
1153 match wrap {
1154 TextWrap::NoWrap => None,
1155 TextWrap::Wrap => available_width,
1156 },
1157 None,
1158 );
1159 let attrs = Attrs::new()
1160 .family(Family::Name(family.family_name()))
1161 .weight(cosmic_weight(weight));
1162 buffer.set_text(text, &attrs, Shaping::Advanced, None);
1163 buffer.shape_until_scroll(font_system, false);
1164
1165 let mut lines = Vec::new();
1166 let mut height: f32 = 0.0;
1167 for run in buffer.layout_runs() {
1168 height = height.max(run.line_top + run.line_height);
1169 lines.push(TextLine {
1170 text: layout_run_text(&run),
1171 width: run.line_w,
1172 y: run.line_top,
1173 baseline: run.line_y,
1174 rtl: run.rtl,
1175 });
1176 }
1177
1178 if lines.is_empty() {
1179 return None;
1180 }
1181
1182 let width = lines.iter().map(|line| line.width).fold(0.0, f32::max);
1183 Some(TextLayout {
1184 lines,
1185 width,
1186 height: height.max(line_height),
1187 line_height,
1188 })
1189}
1190
1191thread_local! {
1197 static FONT_SYSTEM: RefCell<FontSystem> = RefCell::new(bundled_font_system());
1198}
1199
1200#[derive(Clone, PartialEq, Eq, Hash)]
1207struct ShapeKey {
1208 text: Box<str>,
1209 size_bits: u32,
1210 line_height_bits: u32,
1211 family: FontFamily,
1212 weight: FontWeight,
1213 mono: bool,
1214 wrap: TextWrap,
1215 available_width_bits: Option<u32>,
1216}
1217
1218const SHAPE_CACHE_CAPACITY: usize = 1024;
1223thread_local! {
1224 static SHAPE_CACHE: RefCell<LruCache<ShapeKey, TextLayout>> =
1225 RefCell::new(LruCache::new(NonZeroUsize::new(SHAPE_CACHE_CAPACITY).unwrap()));
1226}
1227
1228fn bundled_font_system() -> FontSystem {
1229 let mut db = fontdb::Database::new();
1230 db.set_sans_serif_family(FontFamily::default().family_name());
1231 for bytes in aetna_fonts::DEFAULT_FONTS {
1232 db.load_font_data(bytes.to_vec());
1233 }
1234 FontSystem::new_with_locale_and_db("en-US".to_string(), db)
1235}
1236
1237fn cosmic_weight(weight: FontWeight) -> Weight {
1238 match weight {
1239 FontWeight::Regular => Weight::NORMAL,
1240 FontWeight::Medium => Weight::MEDIUM,
1241 FontWeight::Semibold => Weight::SEMIBOLD,
1242 FontWeight::Bold => Weight::BOLD,
1243 }
1244}
1245
1246fn layout_run_text(run: &cosmic_text::LayoutRun<'_>) -> String {
1247 let Some(start) = run.glyphs.iter().map(|glyph| glyph.start).min() else {
1248 return String::new();
1249 };
1250 let end = run
1251 .glyphs
1252 .iter()
1253 .map(|glyph| glyph.end)
1254 .max()
1255 .unwrap_or(start);
1256 run.text
1257 .get(start..end)
1258 .unwrap_or_default()
1259 .trim_end()
1260 .to_string()
1261}
1262
1263#[derive(Copy, Clone)]
1264struct WrapMeasure {
1265 max_width: f32,
1266 size: f32,
1267 family: FontFamily,
1268 weight: FontWeight,
1269 mono: bool,
1270}
1271
1272fn push_word_wrapped(out: &mut Vec<String>, line: &mut String, word: &str, ctx: WrapMeasure) {
1273 let WrapMeasure {
1274 max_width,
1275 size,
1276 family,
1277 weight,
1278 mono,
1279 } = ctx;
1280 if line_width_with_family(word, size, family, weight, mono) <= max_width {
1281 line.push_str(word);
1282 return;
1283 }
1284
1285 for ch in word.chars() {
1286 let candidate = format!("{line}{ch}");
1287 if !line.is_empty()
1288 && line_width_with_family(&candidate, size, family, weight, mono) > max_width
1289 {
1290 out.push(std::mem::take(line));
1291 }
1292 line.push(ch);
1293 }
1294}
1295
1296fn glyph_for(face: &ttf_parser::Face<'_>, c: char) -> Option<ttf_parser::GlyphId> {
1297 face.glyph_index(c)
1298 .or_else(|| face.glyph_index('\u{FFFD}'))
1299 .or_else(|| face.glyph_index('?'))
1300 .or_else(|| face.glyph_index(' '))
1301}
1302
1303fn kern(face: &ttf_parser::Face<'_>, left: ttf_parser::GlyphId, right: ttf_parser::GlyphId) -> f32 {
1304 let Some(kern) = &face.tables().kern else {
1305 return 0.0;
1306 };
1307 kern.subtables
1308 .into_iter()
1309 .filter(|subtable| subtable.horizontal && !subtable.has_cross_stream)
1310 .find_map(|subtable| subtable.glyphs_kerning(left, right))
1311 .map(|value| value as f32)
1312 .unwrap_or(0.0)
1313}
1314
1315fn font_bytes(family: FontFamily, weight: FontWeight) -> &'static [u8] {
1316 match family {
1320 FontFamily::Inter => {
1321 #[cfg(feature = "inter")]
1322 {
1323 let _ = weight;
1324 aetna_fonts::INTER_VARIABLE
1325 }
1326 #[cfg(not(feature = "inter"))]
1327 {
1328 let _ = weight;
1329 &[]
1330 }
1331 }
1332 FontFamily::Roboto => {
1333 #[cfg(feature = "roboto")]
1334 {
1335 match weight {
1336 FontWeight::Regular => aetna_fonts::ROBOTO_REGULAR,
1337 FontWeight::Medium => aetna_fonts::ROBOTO_MEDIUM,
1338 FontWeight::Semibold | FontWeight::Bold => aetna_fonts::ROBOTO_BOLD,
1339 }
1340 }
1341 #[cfg(not(feature = "roboto"))]
1342 {
1343 let _ = weight;
1344 &[]
1345 }
1346 }
1347 FontFamily::JetBrainsMono => {
1348 #[cfg(feature = "jetbrains-mono")]
1349 {
1350 let _ = weight;
1351 aetna_fonts::JETBRAINS_MONO_VARIABLE
1352 }
1353 #[cfg(not(feature = "jetbrains-mono"))]
1354 {
1355 let _ = weight;
1356 &[]
1357 }
1358 }
1359 }
1360}
1361
1362fn fallback_line_width(text: &str, size: f32, mono: bool) -> f32 {
1363 let char_w = size * if mono { MONO_CHAR_WIDTH_FACTOR } else { 0.60 };
1364 text.chars().count() as f32 * char_w
1365}
1366
1367#[cfg(test)]
1368mod tests {
1369 use super::*;
1370
1371 #[test]
1372 fn proportional_measurement_distinguishes_narrow_and_wide_glyphs() {
1373 let narrow = line_width("iiiiii", 16.0, FontWeight::Regular, false);
1374 let wide = line_width("WWWWWW", 16.0, FontWeight::Regular, false);
1375
1376 assert!(wide > narrow * 2.0, "wide={wide} narrow={narrow}");
1377 }
1378
1379 #[cfg(feature = "roboto")]
1380 #[test]
1381 fn font_family_changes_proportional_measurement() {
1382 let roboto = line_width_with_family(
1383 "Save changes",
1384 14.0,
1385 FontFamily::Roboto,
1386 FontWeight::Semibold,
1387 false,
1388 );
1389 let inter = line_width_with_family(
1390 "Save changes",
1391 14.0,
1392 FontFamily::Inter,
1393 FontWeight::Semibold,
1394 false,
1395 );
1396
1397 assert!(
1398 (inter - roboto).abs() > 1.0,
1399 "inter={inter} roboto={roboto}"
1400 );
1401 }
1402
1403 #[test]
1404 fn wrap_lines_respects_measured_widths() {
1405 let lines = wrap_lines(
1406 "wide WWW words stay measured",
1407 120.0,
1408 16.0,
1409 FontWeight::Regular,
1410 false,
1411 );
1412
1413 assert!(lines.len() > 1);
1414 for line in lines {
1415 assert!(
1416 line_width(&line, 16.0, FontWeight::Regular, false) <= 121.0,
1417 "{line:?} overflowed"
1418 );
1419 }
1420 }
1421
1422 #[test]
1423 fn layout_text_carries_line_positions_and_measurement() {
1424 let layout = layout_text(
1425 "alpha beta gamma",
1426 16.0,
1427 FontWeight::Regular,
1428 false,
1429 TextWrap::Wrap,
1430 Some(80.0),
1431 );
1432
1433 assert!(layout.lines.len() > 1);
1434 assert_eq!(layout.measured().line_count, layout.lines.len());
1435 assert_eq!(layout.lines[0].y, 0.0);
1436 assert_eq!(layout.lines[1].y, layout.line_height);
1437 assert!(layout.lines[0].baseline > layout.lines[0].y);
1438 assert!(layout.height >= layout.line_height * 2.0);
1439 }
1440
1441 #[test]
1442 fn tokenized_line_heights_match_shadcn_scale() {
1443 assert_eq!(line_height(12.0), 16.0);
1444 assert_eq!(line_height(14.0), 20.0);
1445 assert_eq!(line_height(16.0), 24.0);
1446 assert_eq!(line_height(24.0), 32.0);
1447 assert_eq!(line_height(30.0), 36.0);
1448 }
1449
1450 #[test]
1451 fn hit_text_at_origin_lands_on_first_byte() {
1452 let hit = hit_text(
1453 "hello world",
1454 16.0,
1455 FontWeight::Regular,
1456 TextWrap::NoWrap,
1457 None,
1458 0.0,
1459 8.0,
1460 )
1461 .expect("hit at origin");
1462 assert_eq!(hit.line, 0);
1463 assert_eq!(hit.byte_index, 0);
1464 }
1465
1466 #[test]
1467 fn hit_text_past_last_glyph_clamps_to_end() {
1468 let text = "hello";
1469 let hit = hit_text(
1471 text,
1472 16.0,
1473 FontWeight::Regular,
1474 TextWrap::NoWrap,
1475 None,
1476 1000.0,
1477 8.0,
1478 )
1479 .expect("hit past end");
1480 assert_eq!(hit.line, 0);
1481 assert_eq!(hit.byte_index, text.len());
1482 }
1483
1484 #[test]
1485 fn hit_text_walks_columns_left_to_right() {
1486 let text = "abcdefghij";
1490 let mut prev = 0usize;
1491 for x in [4.0, 16.0, 32.0, 64.0, 96.0] {
1492 let hit = hit_text(
1493 text,
1494 16.0,
1495 FontWeight::Regular,
1496 TextWrap::NoWrap,
1497 None,
1498 x,
1499 8.0,
1500 );
1501 let Some(hit) = hit else { continue };
1502 assert!(
1503 hit.byte_index >= prev,
1504 "byte_index regressed at x={x}: {} < {prev}",
1505 hit.byte_index
1506 );
1507 prev = hit.byte_index;
1508 }
1509 }
1510
1511 #[test]
1512 fn text_geometry_hit_byte_maps_hard_line_offsets_to_source_bytes() {
1513 let text = "alpha\nbeta";
1514 let geometry = TextGeometry::new(
1515 text,
1516 16.0,
1517 FontWeight::Regular,
1518 false,
1519 TextWrap::NoWrap,
1520 None,
1521 );
1522 let y = geometry.line_height() * 1.5;
1523 let byte = geometry.hit_byte(1000.0, y).expect("hit on second line");
1524 assert_eq!(byte, text.len());
1525 }
1526
1527 #[test]
1528 fn text_geometry_prefix_width_matches_caret_x() {
1529 let text = "hello world";
1530 let geometry = TextGeometry::new(
1531 text,
1532 16.0,
1533 FontWeight::Regular,
1534 false,
1535 TextWrap::NoWrap,
1536 None,
1537 );
1538 let (x, _y) = geometry.caret_xy(5);
1539 assert!((geometry.prefix_width(5) - x).abs() < 0.01);
1540 }
1541
1542 #[test]
1543 fn caret_xy_at_origin_is_zero_zero() {
1544 let (x, y) = caret_xy(
1545 "hello",
1546 0,
1547 16.0,
1548 FontWeight::Regular,
1549 TextWrap::NoWrap,
1550 None,
1551 );
1552 assert!(x.abs() < 0.01, "x={x}");
1553 assert_eq!(y, 0.0);
1554 }
1555
1556 #[test]
1557 fn caret_xy_at_end_of_line_is_at_line_width() {
1558 let text = "hello";
1559 let width = line_width(text, 16.0, FontWeight::Regular, false);
1560 let (x, y) = caret_xy(
1561 text,
1562 text.len(),
1563 16.0,
1564 FontWeight::Regular,
1565 TextWrap::NoWrap,
1566 None,
1567 );
1568 assert!((x - width).abs() < 1.0, "x={x} expected~{width}");
1569 assert_eq!(y, 0.0);
1570 }
1571
1572 #[test]
1573 fn caret_xy_drops_to_next_line_after_newline() {
1574 let text = "foo\nbar";
1575 let line_h = line_height(16.0);
1576 let (x, y) = caret_xy(text, 4, 16.0, FontWeight::Regular, TextWrap::NoWrap, None);
1578 assert!(x.abs() < 0.01, "x={x}");
1579 assert!((y - line_h).abs() < 0.01, "y={y} expected~{line_h}");
1580 }
1581
1582 #[test]
1583 fn caret_xy_on_phantom_trailing_line_falls_below_text() {
1584 let text = "foo\n";
1585 let line_h = line_height(16.0);
1586 let (x, y) = caret_xy(
1587 text,
1588 text.len(),
1589 16.0,
1590 FontWeight::Regular,
1591 TextWrap::NoWrap,
1592 None,
1593 );
1594 assert!(x.abs() < 0.01, "x={x}");
1595 assert!(y >= line_h - 0.01, "y={y} expected ≥ line_h={line_h}");
1596 }
1597
1598 #[test]
1599 fn selection_rects_returns_one_per_visual_line() {
1600 let text = "alpha\nbeta\ngamma";
1601 let rects = selection_rects(
1602 text,
1603 0,
1604 text.len(),
1605 16.0,
1606 FontWeight::Regular,
1607 TextWrap::NoWrap,
1608 None,
1609 );
1610 assert_eq!(
1611 rects.len(),
1612 3,
1613 "expected one rect per BufferLine, got {rects:?}"
1614 );
1615 assert!(rects[0].1 < rects[1].1);
1617 assert!(rects[1].1 < rects[2].1);
1618 for (_x, _y, w, _h) in &rects {
1619 assert!(*w > 0.0, "empty width: {rects:?}");
1620 }
1621 }
1622
1623 #[test]
1624 fn selection_rects_for_single_line_range_do_not_highlight_other_lines() {
1625 let text = "alpha\nbeta\ngamma";
1626 let lo = text.find("et").unwrap();
1627 let hi = lo + "et".len();
1628 let rects = selection_rects(
1629 text,
1630 lo,
1631 hi,
1632 16.0,
1633 FontWeight::Regular,
1634 TextWrap::NoWrap,
1635 None,
1636 );
1637 assert_eq!(
1638 rects.len(),
1639 1,
1640 "single-line range should only highlight that line: {rects:?}"
1641 );
1642 let line_h = line_height(16.0);
1643 let y = rects[0].1;
1644 assert!(
1645 (y - line_h).abs() < 0.01,
1646 "expected second line y={line_h}, got {y}; rects={rects:?}"
1647 );
1648 }
1649
1650 #[test]
1651 fn visual_line_byte_range_respects_soft_wraps() {
1652 let text = "alpha beta gamma";
1653 let beta = text.find("beta").unwrap();
1654 let width = line_width("alpha", 16.0, FontWeight::Regular, false) + 2.0;
1655 let (lo, hi) = visual_line_byte_range(
1656 text,
1657 beta,
1658 16.0,
1659 FontWeight::Regular,
1660 TextWrap::Wrap,
1661 Some(width),
1662 );
1663 assert!(
1664 lo > 0 && hi < text.len(),
1665 "soft-wrapped visual line should be narrower than the hard line: {lo}..{hi}"
1666 );
1667 assert!(
1668 (lo..hi).contains(&beta),
1669 "range {lo}..{hi} should contain beta byte {beta}"
1670 );
1671 }
1672
1673 #[test]
1674 fn selection_rects_empty_for_collapsed_range() {
1675 let rects = selection_rects(
1676 "alpha",
1677 2,
1678 2,
1679 16.0,
1680 FontWeight::Regular,
1681 TextWrap::NoWrap,
1682 None,
1683 );
1684 assert!(rects.is_empty());
1685 }
1686
1687 #[test]
1688 fn proportional_layout_uses_cosmic_shaping_widths() {
1689 let layout = layout_text(
1690 "Roboto shaping",
1691 18.0,
1692 FontWeight::Medium,
1693 false,
1694 TextWrap::NoWrap,
1695 None,
1696 );
1697
1698 assert_eq!(layout.lines.len(), 1);
1699 assert!((layout.lines[0].width - layout.width).abs() < 0.01);
1700 assert!(layout.lines[0].baseline > layout.lines[0].y);
1701 }
1702
1703 #[test]
1704 fn ellipsize_text_shortens_to_available_width() {
1705 let source = "this is a long branch name";
1706 let available = line_width("this is a…", 14.0, FontWeight::Regular, false);
1707 let clipped = ellipsize_text(source, 14.0, FontWeight::Regular, false, available);
1708 let width = line_width(&clipped, 14.0, FontWeight::Regular, false);
1709
1710 assert!(clipped.ends_with('…'), "clipped={clipped}");
1711 assert!(clipped.len() < source.len());
1712 assert!(
1713 width <= available + 0.5,
1714 "width={width} available={available}"
1715 );
1716 }
1717
1718 #[test]
1719 fn ellipsize_text_keeps_fitting_text_unchanged() {
1720 let source = "short";
1721 let available = line_width(source, 14.0, FontWeight::Regular, false) + 4.0;
1722 assert_eq!(
1723 ellipsize_text(source, 14.0, FontWeight::Regular, false, available),
1724 source
1725 );
1726 }
1727
1728 #[test]
1729 fn clamp_text_to_lines_caps_wrapped_text_with_final_ellipsis() {
1730 let source = "alpha beta gamma delta epsilon zeta";
1731 let available = line_width("alpha beta", 14.0, FontWeight::Regular, false);
1732 let clamped = clamp_text_to_lines(source, 14.0, FontWeight::Regular, false, available, 2);
1733 let layout = layout_text(
1734 &clamped,
1735 14.0,
1736 FontWeight::Regular,
1737 false,
1738 TextWrap::Wrap,
1739 Some(available),
1740 );
1741
1742 assert!(clamped.ends_with('…'), "clamped={clamped}");
1743 assert!(layout.lines.len() <= 2, "layout={layout:?}");
1744 }
1745}