cranpose_ui/text/
measure.rs1use crate::text_layout_result::TextLayoutResult;
2use std::cell::RefCell;
3
4#[derive(Clone, Copy, Debug, PartialEq)]
5pub struct TextMetrics {
6 pub width: f32,
7 pub height: f32,
8 pub line_height: f32,
10 pub line_count: usize,
12}
13
14use super::style::TextStyle; pub trait TextMeasurer: 'static {
17 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics;
18
19 fn get_offset_for_position(&self, text: &str, style: &TextStyle, x: f32, y: f32) -> usize;
20
21 fn get_cursor_x_for_offset(&self, text: &str, style: &TextStyle, offset: usize) -> f32;
22
23 fn layout(&self, text: &str, style: &TextStyle) -> TextLayoutResult;
24}
25
26#[derive(Default)]
27struct MonospacedTextMeasurer;
28
29impl MonospacedTextMeasurer {
30 const DEFAULT_SIZE: f32 = 14.0;
31 const CHAR_WIDTH_RATIO: f32 = 0.6; fn get_metrics(style: &TextStyle) -> (f32, f32) {
34 let size = match style.font_size {
35 super::unit::TextUnit::Sp(v) => v,
36 super::unit::TextUnit::Em(v) => v * Self::DEFAULT_SIZE,
37 super::unit::TextUnit::Unspecified => Self::DEFAULT_SIZE,
38 };
39 (size * Self::CHAR_WIDTH_RATIO, size) }
41}
42
43impl TextMeasurer for MonospacedTextMeasurer {
44 fn measure(&self, text: &str, style: &TextStyle) -> TextMetrics {
45 let (char_width, line_height) = Self::get_metrics(style);
46
47 let lines: Vec<&str> = text.split('\n').collect();
48 let line_count = lines.len().max(1);
49
50 let width = lines
51 .iter()
52 .map(|line| line.chars().count() as f32 * char_width)
53 .fold(0.0_f32, f32::max);
54
55 TextMetrics {
56 width,
57 height: line_count as f32 * line_height,
58 line_height,
59 line_count,
60 }
61 }
62
63 fn get_offset_for_position(&self, text: &str, style: &TextStyle, x: f32, y: f32) -> usize {
64 let (char_width, line_height) = Self::get_metrics(style);
65
66 if text.is_empty() {
67 return 0;
68 }
69
70 let line_index = (y / line_height).floor().max(0.0) as usize;
71 let lines: Vec<&str> = text.split('\n').collect();
72 let target_line = line_index.min(lines.len().saturating_sub(1));
73
74 let mut line_start_byte = 0;
75 for line in lines.iter().take(target_line) {
76 line_start_byte += line.len() + 1;
77 }
78
79 let line_text = lines.get(target_line).unwrap_or(&"");
80 let char_index = (x / char_width).round() as usize;
81 let line_char_count = line_text.chars().count();
82 let clamped_index = char_index.min(line_char_count);
83
84 let offset_in_line = line_text
85 .char_indices()
86 .nth(clamped_index)
87 .map(|(i, _)| i)
88 .unwrap_or(line_text.len());
89
90 line_start_byte + offset_in_line
91 }
92
93 fn get_cursor_x_for_offset(&self, text: &str, style: &TextStyle, offset: usize) -> f32 {
94 let (char_width, _) = Self::get_metrics(style);
95
96 let clamped_offset = offset.min(text.len());
97 let char_count = text[..clamped_offset].chars().count();
98 char_count as f32 * char_width
99 }
100
101 fn layout(&self, text: &str, style: &TextStyle) -> TextLayoutResult {
102 let (char_width, line_height) = Self::get_metrics(style);
103 TextLayoutResult::monospaced(text, char_width, line_height)
104 }
105}
106
107thread_local! {
108 static TEXT_MEASURER: RefCell<Box<dyn TextMeasurer>> = RefCell::new(Box::new(MonospacedTextMeasurer));
109}
110
111pub fn set_text_measurer<M: TextMeasurer>(measurer: M) {
112 TEXT_MEASURER.with(|m| {
113 *m.borrow_mut() = Box::new(measurer);
114 });
115}
116
117pub fn measure_text(text: &str, style: &TextStyle) -> TextMetrics {
118 TEXT_MEASURER.with(|m| m.borrow().measure(text, style))
119}
120
121pub fn get_offset_for_position(text: &str, style: &TextStyle, x: f32, y: f32) -> usize {
122 TEXT_MEASURER.with(|m| m.borrow().get_offset_for_position(text, style, x, y))
123}
124
125pub fn get_cursor_x_for_offset(text: &str, style: &TextStyle, offset: usize) -> f32 {
126 TEXT_MEASURER.with(|m| m.borrow().get_cursor_x_for_offset(text, style, offset))
127}
128
129pub fn layout_text(text: &str, style: &TextStyle) -> TextLayoutResult {
130 TEXT_MEASURER.with(|m| m.borrow().layout(text, style))
131}