1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
//! Text measurement for layout
//!
//! Provides a trait for measuring text dimensions during layout.
//! This allows accurate text sizing without estimation.
/// Text layout options that affect measurement
#[derive(Debug, Clone, Default)]
pub struct TextLayoutOptions {
/// Line height multiplier (1.0 = default, 1.5 = 150%)
pub line_height: f32,
/// Extra spacing between letters in pixels
pub letter_spacing: f32,
/// Extra spacing between words in pixels
pub word_spacing: f32,
/// Maximum width for wrapping (None = no wrapping)
pub max_width: Option<f32>,
/// Font family name (e.g., "Fira Code", None for default)
pub font_name: Option<String>,
/// Generic font category
pub generic_font: crate::div::GenericFont,
/// Font weight (100-900, 400 = normal, 700 = bold)
pub font_weight: u16,
/// Whether text is italic
pub italic: bool,
}
impl TextLayoutOptions {
/// Create default options
pub fn new() -> Self {
Self {
line_height: 1.2, // Default line height
letter_spacing: 0.0,
word_spacing: 0.0,
max_width: None,
font_name: None,
generic_font: crate::div::GenericFont::System,
font_weight: 400,
italic: false,
}
}
/// Set line height multiplier
pub fn with_line_height(mut self, height: f32) -> Self {
self.line_height = height;
self
}
/// Set letter spacing
pub fn with_letter_spacing(mut self, spacing: f32) -> Self {
self.letter_spacing = spacing;
self
}
/// Set word spacing
pub fn with_word_spacing(mut self, spacing: f32) -> Self {
self.word_spacing = spacing;
self
}
/// Set max width for wrapping
pub fn with_max_width(mut self, width: f32) -> Self {
self.max_width = Some(width);
self
}
/// Set font name
pub fn with_font_name(mut self, name: impl Into<String>) -> Self {
self.font_name = Some(name.into());
self
}
/// Set generic font category
pub fn with_generic_font(mut self, generic: crate::div::GenericFont) -> Self {
self.generic_font = generic;
self
}
/// Set monospace font
pub fn monospace(mut self) -> Self {
self.generic_font = crate::div::GenericFont::Monospace;
self
}
/// Set font weight (100-900, 400 = normal, 700 = bold)
pub fn with_weight(mut self, weight: u16) -> Self {
self.font_weight = weight;
self
}
/// Set bold weight (700)
pub fn bold(mut self) -> Self {
self.font_weight = 700;
self
}
/// Set italic style
pub fn with_italic(mut self, italic: bool) -> Self {
self.italic = italic;
self
}
/// Set italic style
pub fn italic(mut self) -> Self {
self.italic = true;
self
}
}
/// Text measurement result
#[derive(Debug, Clone, Copy, Default)]
pub struct TextMetrics {
/// Width in pixels
pub width: f32,
/// Height in pixels (accounts for line height and number of lines)
pub height: f32,
/// Ascender in pixels (distance from baseline to top)
pub ascender: f32,
/// Descender in pixels (distance from baseline to bottom, typically negative)
pub descender: f32,
/// Number of lines (1 for single-line text)
pub line_count: u32,
}
/// Trait for measuring text dimensions
///
/// Implement this trait to provide accurate text measurement during layout.
/// Without a text measurer, text elements will use estimated sizes.
pub trait TextMeasurer: Send + Sync {
/// Measure the dimensions of a text string with full layout options
///
/// # Arguments
/// * `text` - The text to measure
/// * `font_size` - Font size in pixels
/// * `options` - Layout options (line height, spacing, max width)
///
/// # Returns
/// `TextMetrics` with the measured dimensions
fn measure_with_options(
&self,
text: &str,
font_size: f32,
options: &TextLayoutOptions,
) -> TextMetrics;
/// Measure text with default options (convenience method)
fn measure(&self, text: &str, font_size: f32) -> TextMetrics {
self.measure_with_options(text, font_size, &TextLayoutOptions::new())
}
}
/// A dummy text measurer that uses estimates
///
/// This is used when no real text measurer is available.
/// Uses the same estimation formula as the fallback in text.rs.
#[derive(Debug, Clone, Copy, Default)]
pub struct EstimatedTextMeasurer;
impl TextMeasurer for EstimatedTextMeasurer {
fn measure_with_options(
&self,
text: &str,
font_size: f32,
options: &TextLayoutOptions,
) -> TextMetrics {
let char_count = text.chars().count() as f32;
let word_count = text.split_whitespace().count().max(1) as f32;
// Base width: ~0.55 * font_size per character (conservative for proportional fonts)
let base_char_width = font_size * 0.55;
let base_width = char_count * base_char_width;
// Add letter spacing (per character gap)
let letter_spacing_total = if char_count > 1.0 {
(char_count - 1.0) * options.letter_spacing
} else {
0.0
};
// Add word spacing (per word gap)
let word_spacing_total = if word_count > 1.0 {
(word_count - 1.0) * options.word_spacing
} else {
0.0
};
let total_width = base_width + letter_spacing_total + word_spacing_total;
// Handle wrapping if max_width is set
let (width, line_count) = if let Some(max_width) = options.max_width {
if total_width > max_width && max_width > 0.0 {
// Estimate number of lines needed
let lines = (total_width / max_width).ceil() as u32;
(max_width, lines.max(1))
} else {
(total_width, 1)
}
} else {
(total_width, 1)
};
// Height based on line height and number of lines
let line_height_px = font_size * options.line_height;
let height = line_height_px * line_count as f32;
// Ascender/descender estimates
let ascender = font_size * 0.8;
let descender = font_size * -0.2;
TextMetrics {
width,
height,
ascender,
descender,
line_count,
}
}
}
/// Global text measurer storage
///
/// This allows setting a text measurer that will be used during layout.
use std::sync::{Arc, RwLock};
static TEXT_MEASURER: RwLock<Option<Arc<dyn TextMeasurer>>> = RwLock::new(None);
/// Set the global text measurer
///
/// Call this at app initialization with a real text measurer
/// (e.g., one backed by the font rendering system).
pub fn set_text_measurer(measurer: Arc<dyn TextMeasurer>) {
let mut guard = TEXT_MEASURER.write().unwrap();
*guard = Some(measurer);
}
/// Clear the global text measurer
pub fn clear_text_measurer() {
let mut guard = TEXT_MEASURER.write().unwrap();
*guard = None;
}
/// Measure text using the global measurer, or fall back to estimation
pub fn measure_text(text: &str, font_size: f32) -> TextMetrics {
let guard = TEXT_MEASURER.read().unwrap();
if let Some(ref measurer) = *guard {
measurer.measure(text, font_size)
} else {
EstimatedTextMeasurer.measure(text, font_size)
}
}
/// Measure text with options using the global measurer, or fall back to estimation
pub fn measure_text_with_options(
text: &str,
font_size: f32,
options: &TextLayoutOptions,
) -> TextMetrics {
let guard = TEXT_MEASURER.read().unwrap();
if let Some(ref measurer) = *guard {
measurer.measure_with_options(text, font_size, options)
} else {
EstimatedTextMeasurer.measure_with_options(text, font_size, options)
}
}