Skip to main content

cranpose_ui/text/
measure.rs

1use 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    /// Height of a single line of text
9    pub line_height: f32,
10    /// Number of lines in the text
11    pub line_count: usize,
12}
13
14use super::style::TextStyle; // Add imports
15
16pub 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; // Width is 0.6 of Height
32
33    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) // (width, height)
40    }
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}