1use crate::cache_keys::{
2 TextMeasureKey, TextMeasureShapingKey, hash_text, spans_shaping_fingerprint,
3};
4use crate::cache_tuning;
5use crate::geometry::{metrics_for_uniform_lines, metrics_from_wrapped_lines};
6use crate::parley_shaper;
7use crate::parley_shaper::ParleyShaper;
8use crate::wrapper;
9use fret_core::{
10 AttributedText, TextConstraints, TextInputRef, TextMetrics, TextOverflow, TextSlant, TextSpan,
11 TextStyle, TextWrap,
12};
13use std::collections::{HashMap, VecDeque};
14use std::sync::Arc;
15
16#[derive(Debug, Clone)]
17struct TextMeasureEntry {
18 text_hash: u64,
19 spans_hash: u64,
20 text: Arc<str>,
21 spans: Option<Arc<[TextSpan]>>,
22 metrics: TextMetrics,
23}
24
25#[derive(Debug, Clone)]
26struct TextMeasureShapingEntry {
27 text: Arc<str>,
28 spans: Option<Arc<[TextSpan]>>,
29 width_px: f32,
30 baseline_px: f32,
31 line_height_px: f32,
32 clusters: Arc<[parley_shaper::ShapedCluster]>,
33}
34
35#[derive(Debug)]
36pub struct TextMeasureCaches {
37 measure_cache: HashMap<TextMeasureKey, VecDeque<TextMeasureEntry>>,
38 measure_shaping_cache: HashMap<TextMeasureShapingKey, TextMeasureShapingEntry>,
39 measure_shaping_fifo: VecDeque<TextMeasureShapingKey>,
40}
41
42impl Default for TextMeasureCaches {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48impl TextMeasureCaches {
49 pub fn new() -> Self {
50 let shaping_entries = cache_tuning::measure_shaping_cache_entries();
51 Self {
52 measure_cache: HashMap::new(),
53 measure_shaping_cache: HashMap::with_capacity(shaping_entries.min(65_536)),
56 measure_shaping_fifo: VecDeque::with_capacity(shaping_entries.min(65_536)),
57 }
58 }
59
60 pub fn clear(&mut self) {
61 self.measure_cache.clear();
62 self.measure_shaping_cache.clear();
63 self.measure_shaping_fifo.clear();
64 }
65
66 pub fn buckets_len(&self) -> usize {
67 self.measure_cache.len()
68 }
69
70 pub fn shaping_entries_len(&self) -> usize {
71 self.measure_shaping_cache.len()
72 }
73
74 pub fn measure_plain(
75 &mut self,
76 shaper: &mut ParleyShaper,
77 text: &str,
78 style: &TextStyle,
79 constraints: TextConstraints,
80 font_stack_key: u64,
81 ) -> TextMetrics {
82 const MEASURE_CACHE_PER_BUCKET_LIMIT: usize = 256;
83 const MEASURE_CACHE_PER_BUCKET_LIMIT_WRAP_NONE: usize = 2048;
84
85 let mut normalized_constraints = constraints;
86 if normalized_constraints.wrap == TextWrap::None {
87 normalized_constraints.max_width = None;
88 }
89
90 let key = TextMeasureKey::new(style, normalized_constraints, font_stack_key);
91 let text_hash = hash_text(text);
92 if let Some(bucket) = self.measure_cache.get_mut(&key)
93 && let Some(idx) = bucket.iter().position(|e| {
94 e.text_hash == text_hash && e.spans_hash == 0 && e.text.as_ref() == text
95 })
96 && let Some(hit) = bucket.remove(idx)
97 {
98 let mut metrics = hit.metrics;
99 bucket.push_back(hit);
100 if constraints.wrap == TextWrap::None
101 && constraints.overflow == TextOverflow::Ellipsis
102 && let Some(max_width) = constraints.max_width
103 {
104 metrics.size.width = max_width;
105 }
106 return metrics;
107 }
108
109 let scale = crate::effective_text_scale_factor(constraints.scale_factor);
110 let allow_fast_wrap_measure =
111 constraints.scale_factor.is_finite() && constraints.scale_factor.fract().abs() <= 1e-4;
112 let max_width_for_fast = match constraints {
113 TextConstraints {
114 max_width: Some(max_width),
115 wrap: TextWrap::Word | TextWrap::Balance | TextWrap::WordBreak | TextWrap::Grapheme,
116 overflow: TextOverflow::Clip,
117 ..
118 } if allow_fast_wrap_measure && !text.contains('\n') => Some(max_width),
119 _ => None,
120 };
121
122 let metrics = if let Some(max_width) = max_width_for_fast {
123 let allow_shaping_cache =
124 text.len() >= cache_tuning::measure_shaping_cache_min_text_len_bytes();
125
126 let shaping_key = TextMeasureShapingKey {
127 text_hash,
128 text_len: text.len(),
129 spans_shaping_key: 0,
130 font: style.font.clone(),
131 font_stack_key,
132 size_bits: style.size.0.to_bits(),
133 weight: style.weight.0,
134 slant: match style.slant {
135 TextSlant::Normal => 0,
136 TextSlant::Italic => 1,
137 TextSlant::Oblique => 2,
138 },
139 line_height_bits: style.line_height.map(|px| px.0.to_bits()),
140 line_height_em_bits: style.line_height_em.map(|v| v.to_bits()),
141 line_height_policy: match style.line_height_policy {
142 fret_core::TextLineHeightPolicy::ExpandToFit => 0,
143 fret_core::TextLineHeightPolicy::FixedFromStyle => 1,
144 },
145 leading_distribution: match style.leading_distribution {
146 fret_core::text::TextLeadingDistribution::Even => 0,
147 fret_core::text::TextLeadingDistribution::Proportional => 1,
148 },
149 strut_force: style
150 .strut_style
151 .as_ref()
152 .map(|s| if s.force { 1 } else { 0 })
153 .unwrap_or(0),
154 strut_font: style.strut_style.as_ref().and_then(|s| s.font.clone()),
155 strut_size_bits: style
156 .strut_style
157 .as_ref()
158 .and_then(|s| s.size.map(|px| px.0.to_bits())),
159 strut_line_height_bits: style
160 .strut_style
161 .as_ref()
162 .and_then(|s| s.line_height.map(|px| px.0.to_bits())),
163 strut_line_height_em_bits: style
164 .strut_style
165 .as_ref()
166 .and_then(|s| s.line_height_em.map(|v| v.to_bits())),
167 strut_leading_distribution: style.strut_style.as_ref().and_then(|s| {
168 s.leading_distribution.map(|d| match d {
169 fret_core::text::TextLeadingDistribution::Even => 0,
170 fret_core::text::TextLeadingDistribution::Proportional => 1,
171 })
172 }),
173 letter_spacing_bits: style.letter_spacing_em.map(|v| v.to_bits()),
174 scale_bits: constraints.scale_factor.to_bits(),
175 };
176
177 let max_width_px = max_width.0 * scale;
178
179 if allow_shaping_cache {
180 let (width_px, baseline_px, line_height_px, _clusters) = if let Some(hit) =
181 self.measure_shaping_cache.get(&shaping_key)
182 && hit.text.as_ref() == text
183 && hit.spans.is_none()
184 {
185 (
186 hit.width_px,
187 hit.baseline_px,
188 hit.line_height_px,
189 hit.clusters.clone(),
190 )
191 } else {
192 let mut line =
193 shaper.shape_single_line_metrics(TextInputRef::plain(text, style), scale);
194 let clusters: Arc<[parley_shaper::ShapedCluster]> =
195 Arc::from(line.take_clusters());
196
197 let existed = self
198 .measure_shaping_cache
199 .insert(
200 shaping_key.clone(),
201 TextMeasureShapingEntry {
202 text: Arc::<str>::from(text),
203 spans: None,
204 width_px: line.width(),
205 baseline_px: line.baseline(),
206 line_height_px: line.line_height(),
207 clusters: clusters.clone(),
208 },
209 )
210 .is_some();
211 if !existed {
212 self.measure_shaping_fifo.push_back(shaping_key.clone());
213 let limit = cache_tuning::measure_shaping_cache_entries();
214 while self.measure_shaping_fifo.len() > limit {
215 let Some(evict) = self.measure_shaping_fifo.pop_front() else {
216 break;
217 };
218 self.measure_shaping_cache.remove(&evict);
219 }
220 }
221
222 (line.width(), line.baseline(), line.line_height(), clusters)
223 };
224
225 if width_px <= max_width_px + 0.5 {
226 metrics_for_uniform_lines(
227 width_px.max(0.0),
228 1,
229 baseline_px,
230 line_height_px,
231 scale,
232 )
233 } else {
234 let wrapped = wrapper::wrap_with_constraints_measure_only(
235 shaper,
236 TextInputRef::plain(text, style),
237 normalized_constraints,
238 );
239 metrics_from_wrapped_lines(wrapped.lines(), scale)
240 }
241 } else {
242 let mut line =
243 shaper.shape_single_line_metrics(TextInputRef::plain(text, style), scale);
244 let width_px = line.width();
245 let baseline_px = line.baseline();
246 let line_height_px = line.line_height();
247 let _clusters = line.take_clusters();
248
249 if width_px <= max_width_px + 0.5 {
250 metrics_for_uniform_lines(
251 width_px.max(0.0),
252 1,
253 baseline_px,
254 line_height_px,
255 scale,
256 )
257 } else {
258 let wrapped = wrapper::wrap_with_constraints_measure_only(
259 shaper,
260 TextInputRef::plain(text, style),
261 normalized_constraints,
262 );
263 metrics_from_wrapped_lines(wrapped.lines(), scale)
264 }
265 }
266 } else {
267 let wrapped = wrapper::wrap_with_constraints_measure_only(
271 shaper,
272 TextInputRef::plain(text, style),
273 normalized_constraints,
274 );
275 metrics_from_wrapped_lines(wrapped.lines(), scale)
276 };
277
278 let bucket = self.measure_cache.entry(key).or_default();
279 bucket.push_back(TextMeasureEntry {
280 text_hash,
281 spans_hash: 0,
282 text: Arc::<str>::from(text),
283 spans: None,
284 metrics,
285 });
286 let limit = match normalized_constraints.wrap {
287 TextWrap::None => MEASURE_CACHE_PER_BUCKET_LIMIT_WRAP_NONE,
288 TextWrap::Word | TextWrap::Balance | TextWrap::WordBreak | TextWrap::Grapheme => {
289 MEASURE_CACHE_PER_BUCKET_LIMIT
290 }
291 };
292 while bucket.len() > limit {
293 bucket.pop_front();
294 }
295
296 let mut metrics = metrics;
297 if constraints.wrap == TextWrap::None
298 && constraints.overflow == TextOverflow::Ellipsis
299 && let Some(max_width) = constraints.max_width
300 {
301 metrics.size.width = max_width;
302 }
303 metrics
304 }
305
306 pub fn measure_attributed(
307 &mut self,
308 shaper: &mut ParleyShaper,
309 rich: &AttributedText,
310 base_style: &TextStyle,
311 constraints: TextConstraints,
312 font_stack_key: u64,
313 ) -> TextMetrics {
314 const MEASURE_CACHE_PER_BUCKET_LIMIT: usize = 256;
315 const MEASURE_CACHE_PER_BUCKET_LIMIT_WRAP_NONE: usize = 2048;
316
317 let mut normalized_constraints = constraints;
318 if normalized_constraints.wrap == TextWrap::None {
319 normalized_constraints.max_width = None;
320 }
321
322 let key = TextMeasureKey::new(base_style, normalized_constraints, font_stack_key);
323 let text_hash = hash_text(rich.text.as_ref());
324 let spans_hash = spans_shaping_fingerprint(rich.spans.as_ref());
325
326 if let Some(bucket) = self.measure_cache.get_mut(&key)
327 && let Some(idx) = bucket.iter().position(|e| {
328 e.text_hash == text_hash
329 && e.spans_hash == spans_hash
330 && e.text.as_ref() == rich.text.as_ref()
331 && e.spans.as_ref().is_some_and(|s| {
332 Arc::ptr_eq(s, &rich.spans) || s.as_ref() == rich.spans.as_ref()
333 })
334 })
335 && let Some(hit) = bucket.remove(idx)
336 {
337 let mut metrics = hit.metrics;
338 bucket.push_back(hit);
339 if constraints.wrap == TextWrap::None
340 && constraints.overflow == TextOverflow::Ellipsis
341 && let Some(max_width) = constraints.max_width
342 {
343 metrics.size.width = max_width;
344 }
345 return metrics;
346 }
347
348 let scale = crate::effective_text_scale_factor(constraints.scale_factor);
349 let allow_fast_wrap_measure =
350 constraints.scale_factor.is_finite() && constraints.scale_factor.fract().abs() <= 1e-4;
351 let max_width_for_fast = match constraints {
352 TextConstraints {
353 max_width: Some(max_width),
354 wrap: TextWrap::Word | TextWrap::Balance | TextWrap::WordBreak | TextWrap::Grapheme,
355 overflow: TextOverflow::Clip,
356 ..
357 } if allow_fast_wrap_measure && !rich.text.as_ref().contains('\n') => Some(max_width),
358 _ => None,
359 };
360
361 let metrics = if let Some(max_width) = max_width_for_fast {
362 let allow_shaping_cache =
363 rich.text.len() >= cache_tuning::measure_shaping_cache_min_text_len_bytes();
364
365 let shaping_key = TextMeasureShapingKey {
366 text_hash,
367 text_len: rich.text.len(),
368 spans_shaping_key: spans_hash,
369 font: base_style.font.clone(),
370 font_stack_key,
371 size_bits: base_style.size.0.to_bits(),
372 weight: base_style.weight.0,
373 slant: match base_style.slant {
374 TextSlant::Normal => 0,
375 TextSlant::Italic => 1,
376 TextSlant::Oblique => 2,
377 },
378 line_height_bits: base_style.line_height.map(|px| px.0.to_bits()),
379 line_height_em_bits: base_style.line_height_em.map(|v| v.to_bits()),
380 line_height_policy: match base_style.line_height_policy {
381 fret_core::TextLineHeightPolicy::ExpandToFit => 0,
382 fret_core::TextLineHeightPolicy::FixedFromStyle => 1,
383 },
384 leading_distribution: match base_style.leading_distribution {
385 fret_core::text::TextLeadingDistribution::Even => 0,
386 fret_core::text::TextLeadingDistribution::Proportional => 1,
387 },
388 strut_force: base_style
389 .strut_style
390 .as_ref()
391 .map(|s| if s.force { 1 } else { 0 })
392 .unwrap_or(0),
393 strut_font: base_style.strut_style.as_ref().and_then(|s| s.font.clone()),
394 strut_size_bits: base_style
395 .strut_style
396 .as_ref()
397 .and_then(|s| s.size.map(|px| px.0.to_bits())),
398 strut_line_height_bits: base_style
399 .strut_style
400 .as_ref()
401 .and_then(|s| s.line_height.map(|px| px.0.to_bits())),
402 strut_line_height_em_bits: base_style
403 .strut_style
404 .as_ref()
405 .and_then(|s| s.line_height_em.map(|v| v.to_bits())),
406 strut_leading_distribution: base_style.strut_style.as_ref().and_then(|s| {
407 s.leading_distribution.map(|d| match d {
408 fret_core::text::TextLeadingDistribution::Even => 0,
409 fret_core::text::TextLeadingDistribution::Proportional => 1,
410 })
411 }),
412 letter_spacing_bits: base_style.letter_spacing_em.map(|v| v.to_bits()),
413 scale_bits: constraints.scale_factor.to_bits(),
414 };
415
416 let max_width_px = max_width.0 * scale;
417 let text = rich.text.as_ref();
418
419 if allow_shaping_cache {
420 let (width_px, baseline_px, line_height_px, _clusters) = if let Some(hit) =
421 self.measure_shaping_cache.get(&shaping_key)
422 && hit.text.as_ref() == rich.text.as_ref()
423 && hit.spans.as_ref().is_some_and(|s| {
424 Arc::ptr_eq(s, &rich.spans) || s.as_ref() == rich.spans.as_ref()
425 }) {
426 (
427 hit.width_px,
428 hit.baseline_px,
429 hit.line_height_px,
430 hit.clusters.clone(),
431 )
432 } else {
433 let mut line = shaper.shape_single_line_metrics(
434 TextInputRef::Attributed {
435 text: rich.text.as_ref(),
436 base: base_style,
437 spans: rich.spans.as_ref(),
438 },
439 scale,
440 );
441 let clusters: Arc<[parley_shaper::ShapedCluster]> =
442 Arc::from(line.take_clusters());
443
444 let existed = self
445 .measure_shaping_cache
446 .insert(
447 shaping_key.clone(),
448 TextMeasureShapingEntry {
449 text: rich.text.clone(),
450 spans: Some(rich.spans.clone()),
451 width_px: line.width(),
452 baseline_px: line.baseline(),
453 line_height_px: line.line_height(),
454 clusters: clusters.clone(),
455 },
456 )
457 .is_some();
458 if !existed {
459 self.measure_shaping_fifo.push_back(shaping_key.clone());
460 let limit = cache_tuning::measure_shaping_cache_entries();
461 while self.measure_shaping_fifo.len() > limit {
462 let Some(evict) = self.measure_shaping_fifo.pop_front() else {
463 break;
464 };
465 self.measure_shaping_cache.remove(&evict);
466 }
467 }
468
469 (line.width(), line.baseline(), line.line_height(), clusters)
470 };
471
472 if width_px <= max_width_px + 0.5 {
473 metrics_for_uniform_lines(
474 width_px.max(0.0),
475 1,
476 baseline_px,
477 line_height_px,
478 scale,
479 )
480 } else {
481 let wrapped = wrapper::wrap_with_constraints_measure_only(
482 shaper,
483 TextInputRef::Attributed {
484 text,
485 base: base_style,
486 spans: rich.spans.as_ref(),
487 },
488 normalized_constraints,
489 );
490 metrics_from_wrapped_lines(wrapped.lines(), scale)
491 }
492 } else {
493 let mut line = shaper.shape_single_line_metrics(
494 TextInputRef::Attributed {
495 text,
496 base: base_style,
497 spans: rich.spans.as_ref(),
498 },
499 scale,
500 );
501 let width_px = line.width();
502 let baseline_px = line.baseline();
503 let line_height_px = line.line_height();
504 let _clusters = line.take_clusters();
505
506 if width_px <= max_width_px + 0.5 {
507 metrics_for_uniform_lines(
508 width_px.max(0.0),
509 1,
510 baseline_px,
511 line_height_px,
512 scale,
513 )
514 } else {
515 let wrapped = wrapper::wrap_with_constraints_measure_only(
516 shaper,
517 TextInputRef::Attributed {
518 text,
519 base: base_style,
520 spans: rich.spans.as_ref(),
521 },
522 normalized_constraints,
523 );
524 metrics_from_wrapped_lines(wrapped.lines(), scale)
525 }
526 }
527 } else {
528 let wrapped = wrapper::wrap_with_constraints_measure_only(
529 shaper,
530 TextInputRef::Attributed {
531 text: rich.text.as_ref(),
532 base: base_style,
533 spans: rich.spans.as_ref(),
534 },
535 normalized_constraints,
536 );
537 metrics_from_wrapped_lines(wrapped.lines(), scale)
538 };
539
540 let bucket = self.measure_cache.entry(key).or_default();
541 bucket.push_back(TextMeasureEntry {
542 text_hash,
543 spans_hash,
544 text: rich.text.clone(),
545 spans: Some(rich.spans.clone()),
546 metrics,
547 });
548 let limit = match normalized_constraints.wrap {
549 TextWrap::None => MEASURE_CACHE_PER_BUCKET_LIMIT_WRAP_NONE,
550 TextWrap::Word | TextWrap::Balance | TextWrap::WordBreak | TextWrap::Grapheme => {
551 MEASURE_CACHE_PER_BUCKET_LIMIT
552 }
553 };
554 while bucket.len() > limit {
555 bucket.pop_front();
556 }
557
558 let mut metrics = metrics;
559 if constraints.wrap == TextWrap::None
560 && constraints.overflow == TextOverflow::Ellipsis
561 && let Some(max_width) = constraints.max_width
562 {
563 metrics.size.width = max_width;
564 }
565 metrics
566 }
567}