1use std::{
2 cell::{Cell, OnceCell, RefCell},
3 ops::Range,
4 rc::Rc,
5 sync::Arc,
6};
7
8use cranpose_core::{
9 PackedU64,
10 collections::pass_aged::{IDLE_PASSES, PassAgedCache},
11};
12use cranpose_text::{
13 CaretDirection, FontExtent, FontFamily, FontFamilyResolver, FontRequest, FontStyle, FontWeight,
14 LayoutInput, ParagraphIntrinsics, TextLayout, TextLayouter, paragraph_line_box,
15};
16use cranpose_ui_graphics::{MemoryHeadroom, Point, Size, TextLayoutHandle};
17
18use super::{
19 AnnotatedString, LineBox, RenderString, SpanStyle, TextAlign, TextDrawStyle, TextUnit,
20 layout_cache::{CacheAdmission, Cached, LayoutCache},
21 layout_input::{DEFAULT_FONT_SIZE_SP, ParagraphSpans, TextUnits},
22 layout_options::{TextLayoutOptions, TextOverflow},
23 style::TextStyle,
24 text_key::TextKey,
25};
26use crate::{density::Density, font_scale::FontScaleCurve, text_layout_result::TextLayoutResult};
27
28const TEXT_INTRINSICS_CACHE_CAPACITY: usize = 1024;
29const SCALE_DOWN_SEARCH_STEPS: usize = 14;
30
31#[derive(Clone, Copy, Debug, PartialEq)]
33pub struct TextMetrics {
34 pub width: f32,
36 pub height: f32,
38 pub line_height: f32,
40 pub line_count: usize,
42}
43
44#[derive(Clone, Debug)]
47pub struct PreparedTextLayout {
48 pub text: Rc<AnnotatedString>,
50 pub visual_style: Arc<TextStyle>,
53 pub layout: TextLayout,
55 max_width: f32,
58 min_lines: u32,
59 pub render_text: OnceCell<(Arc<RenderString>, PackedU64)>,
63 pub(crate) visual_style_hash: OnceCell<PackedU64>,
64 layout_handle: OnceCell<TextLayoutHandle>,
66}
67
68impl PreparedTextLayout {
69 fn new(
72 text: Rc<AnnotatedString>,
73 style: Arc<TextStyle>,
74 layout: TextLayout,
75 (max_width, min_lines): (f32, usize),
76 ) -> Self {
77 Self {
78 max_width,
79 min_lines: u32::try_from(min_lines).unwrap_or(u32::MAX),
80 text,
81 visual_style: style,
82 layout,
83 render_text: OnceCell::new(),
84 visual_style_hash: OnceCell::new(),
85 layout_handle: OnceCell::new(),
86 }
87 }
88
89 pub fn size(&self) -> Size {
92 Size::new(
93 self.layout.width().min(self.max_width),
94 self.layout.height(),
95 )
96 }
97
98 pub(crate) fn max_widths_kept(&self, min_width: f32) -> cranpose_ui_layout::BoundRange {
101 let width = self.layout.width();
102 if self.max_width < width {
105 return cranpose_ui_layout::BoundRange::exactly(self.max_width);
106 }
107 let (low, high) = self.layout.max_widths_kept(min_width);
108 cranpose_ui_layout::BoundRange {
109 low: low.max(width),
110 high,
111 }
112 }
113
114 pub fn metrics(&self) -> TextMetrics {
116 let layout = &self.layout;
117 let size = self.size();
118 TextMetrics {
119 width: size.width,
120 height: size.height,
121 line_height: layout.line_advance(),
122 line_count: layout
123 .line_count()
124 .max(usize::try_from(self.min_lines).unwrap_or(usize::MAX)),
125 }
126 }
127
128 pub fn alignment_lines(&self) -> cranpose_ui_layout::AlignmentLines {
130 let layout = &self.layout;
131 cranpose_ui_layout::AlignmentLines::new(
132 Some(layout.line_baseline(0)),
133 Some(layout.line_baseline(layout.line_count() - 1)),
134 )
135 }
136
137 pub fn did_overflow(&self) -> bool {
140 self.layout.did_overflow_height() || self.layout.did_overflow_width()
141 }
142
143 pub(crate) fn layout_handle(&self) -> TextLayoutHandle {
146 self.layout_handle
147 .get_or_init(|| TextLayoutHandle::from(Arc::new(self.layout.clone())))
148 .clone()
149 }
150
151 pub(crate) fn at_widths(
156 self: &Rc<Self>,
157 widths: (f32, f32),
158 align: TextAlign,
159 spare: Option<Rc<Self>>,
160 ) -> Rc<Self> {
161 let width = self.layout.width_between(widths.0, widths.1);
162 if width.to_bits() == self.layout.width().to_bits() {
163 return Rc::clone(self);
164 }
165 self.aligned_at(widths, align, spare)
166 }
167
168 #[inline(never)]
171 fn aligned_at(
172 self: &Rc<Self>,
173 widths: (f32, f32),
174 align: TextAlign,
175 spare: Option<Rc<Self>>,
176 ) -> Rc<Self> {
177 let mut aligned = Self {
178 min_lines: self.min_lines,
179 render_text: self.render_text.clone(),
180 visual_style_hash: self.visual_style_hash.clone(),
181 ..Self::new(
182 Rc::clone(&self.text),
183 Arc::clone(&self.visual_style),
184 self.layout.clone(),
185 (self.max_width, 0),
186 )
187 };
188 aligned.realign(widths, align);
189 if let Some(mut spare) = spare
190 && let Some(slot) = Rc::get_mut(&mut spare)
191 {
192 *slot = aligned;
193 return spare;
194 }
195 Rc::new(aligned)
196 }
197
198 pub(crate) fn realign(&mut self, (min_width, max_width): (f32, f32), align: TextAlign) {
201 self.layout.align(min_width, max_width, align);
202 self.max_width = max_width;
203 self.layout_handle = OnceCell::new();
204 }
205
206 pub(crate) fn try_realign(
209 &mut self,
210 (min_width, max_width): (f32, f32),
211 align: TextAlign,
212 ) -> bool {
213 let held = self.layout.try_align(min_width, max_width, align);
214 if held {
215 self.max_width = max_width;
216 self.layout_handle = OnceCell::new();
217 }
218 held
219 }
220
221 pub fn render_text(&self) -> Arc<RenderString> {
225 if let Some((converted, _)) = self.render_text.get() {
226 return Arc::clone(converted);
227 }
228 self.convert_render_text().0
229 }
230
231 pub fn render_text_hash(&self) -> u64 {
235 if let Some((_, hash)) = self.render_text.get() {
236 return hash.get();
237 }
238 self.convert_render_text().1
239 }
240
241 fn convert_render_text(&self) -> (Arc<RenderString>, u64) {
242 let converted = Arc::new(self.text.render_string());
243 let hash = converted.render_hash();
244 let _ = self
245 .render_text
246 .set((Arc::clone(&converted), PackedU64::new(hash)));
247 (converted, hash)
248 }
249
250 pub fn visual_style_hash(&self) -> u64 {
253 if let Some(hash) = self.visual_style_hash.get() {
254 return hash.get();
255 }
256 let hash = self.visual_style.render_hash();
257 let _ = self.visual_style_hash.set(PackedU64::new(hash));
258 hash
259 }
260
261 pub fn result(&self) -> TextLayoutResult {
263 TextLayoutResult::new(self.layout.clone())
264 }
265}
266
267impl PartialEq for PreparedTextLayout {
268 fn eq(&self, other: &Self) -> bool {
269 self.text == other.text
270 && self.visual_style == other.visual_style
271 && self.metrics() == other.metrics()
272 && self.alignment_lines() == other.alignment_lines()
273 && self.did_overflow() == other.did_overflow()
274 }
275}
276
277#[derive(Clone, Copy)]
280enum LayoutSource<'a> {
281 Borrowed {
282 text: &'a AnnotatedString,
283 style: &'a TextStyle,
284 },
285 Shared {
286 text: &'a SharedText<'a>,
287 shaped: Option<&'a TextLayout>,
290 },
291}
292
293pub(crate) struct SharedText<'a> {
295 pub(crate) text: &'a Rc<AnnotatedString>,
296 pub(crate) style: &'a Arc<TextStyle>,
297 pub(crate) style_hash: u64,
298}
299
300impl LayoutSource<'_> {
301 fn text(&self) -> &AnnotatedString {
302 match self {
303 Self::Borrowed { text, .. } => text,
304 Self::Shared { text, .. } => text.text,
305 }
306 }
307
308 fn style(&self) -> &TextStyle {
309 match self {
310 Self::Borrowed { style, .. } => style,
311 Self::Shared { text, .. } => text.style,
312 }
313 }
314
315 fn shared(&self) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
316 match self {
317 Self::Borrowed { text, style } => {
318 (Rc::new((*text).clone()), Arc::new((*style).clone()))
319 }
320 Self::Shared { text, .. } => (Rc::clone(text.text), Arc::clone(text.style)),
321 }
322 }
323
324 fn style_hash(&self) -> u64 {
325 match self {
326 Self::Borrowed { style, .. } => style.render_hash(),
327 Self::Shared { text, .. } => text.style_hash,
328 }
329 }
330
331 fn shaped(&self) -> Option<&TextLayout> {
332 match self {
333 Self::Borrowed { .. } => None,
334 Self::Shared { shaped, .. } => *shaped,
335 }
336 }
337}
338
339#[derive(Clone, Copy, Debug, PartialEq)]
341pub(crate) struct TextFrame {
342 pub(crate) min_width: f32,
343 pub(crate) max_width: f32,
344 pub(crate) options: TextLayoutOptions,
345}
346
347impl TextFrame {
348 fn unbounded(options: TextLayoutOptions) -> Self {
350 Self {
351 min_width: 0.0,
352 max_width: f32::INFINITY,
353 options,
354 }
355 }
356
357 pub(crate) fn up_to(max_width: Option<f32>, options: TextLayoutOptions) -> Self {
360 Self {
361 max_width: max_width
362 .filter(|width| width.is_finite() && *width > 0.0)
363 .unwrap_or(f32::INFINITY),
364 ..Self::unbounded(options)
365 }
366 }
367
368 pub(crate) fn drawn(width: f32, options: TextLayoutOptions) -> Self {
371 if width.is_finite() && width > 0.0 {
372 Self {
373 min_width: width,
374 max_width: width,
375 options,
376 }
377 } else {
378 Self::up_to(None, options)
379 }
380 }
381}
382
383pub(crate) struct TextService {
386 generation: Cell<u64>,
387 pass_owed: Cell<bool>,
390 fonts: RefCell<FontFamilyResolver>,
391 layouter: RefCell<TextLayouter>,
392 layouts: RefCell<LayoutCache>,
393 intrinsics: RefCell<PassAgedCache<TextKey, ParagraphIntrinsics>>,
394}
395
396impl TextService {
397 pub(crate) fn new() -> Self {
398 Self {
399 generation: Cell::new(1),
400 pass_owed: Cell::new(false),
401 fonts: RefCell::new(default_fonts()),
402 layouter: RefCell::new(TextLayouter::default()),
403 layouts: RefCell::new(LayoutCache::new()),
404 intrinsics: RefCell::new(PassAgedCache::with_capacity_at_least_one(
405 TEXT_INTRINSICS_CACHE_CAPACITY,
406 )),
407 }
408 }
409
410 pub(crate) fn set_fonts(&self, fonts: FontFamilyResolver) {
412 *self.fonts.borrow_mut() = fonts;
413 self.generation
414 .set(self.generation.get().wrapping_add(1).max(1));
415 self.layouts.borrow_mut().clear();
416 self.intrinsics.borrow_mut().clear();
417 }
418
419 pub(crate) fn generation(&self) -> u64 {
420 self.generation.get()
421 }
422
423 fn prepare(
427 &self,
428 source: LayoutSource<'_>,
429 frame: TextFrame,
430 (units, admission): (TextUnits, CacheAdmission),
431 ) -> Rc<PreparedTextLayout> {
432 let (text, style) = (source.text(), source.style());
433 let key = TextKey::of(text, source.style_hash(), frame.options, units);
434 let cached = key.map(|key| {
435 let cached = self
436 .layouts
437 .borrow_mut()
438 .cached(key, frame, (style, admission));
439 (key, cached)
440 });
441 let missed = match cached {
442 Some((_, Cached::Held(prepared))) => return prepared,
443 Some((key, Cached::Missed(miss))) => Some((key, miss)),
444 None => None,
445 };
446 let (text, style) = source.shared();
447 let prepared = Rc::new(self.lay_out(text, style, frame, (units, source.shaped())));
448 if let Some((key, miss)) = missed {
449 self.layouts
450 .borrow_mut()
451 .keep(key, &prepared, (miss, admission));
452 }
453 prepared
454 }
455
456 fn lay_out(
459 &self,
460 text: Rc<AnnotatedString>,
461 style: Arc<TextStyle>,
462 frame: TextFrame,
463 (units, shaped): (TextUnits, Option<&TextLayout>),
464 ) -> PreparedTextLayout {
465 let options = frame.options.normalized();
466 let min_lines = options.min_lines;
467 let Some(min_font_size_sp) = options.overflow.scale_down_min_font_size_sp() else {
468 let layout = self.layout(&text, &style, frame, (units, shaped));
469 return PreparedTextLayout::new(text, style, layout, (frame.max_width, min_lines));
470 };
471 self.lay_out_scaled_down(text, style, frame, (units, shaped), min_font_size_sp)
472 }
473
474 #[inline(never)]
478 fn lay_out_scaled_down(
479 &self,
480 text: Rc<AnnotatedString>,
481 style: Arc<TextStyle>,
482 frame: TextFrame,
483 (units, shaped): (TextUnits, Option<&TextLayout>),
484 min_font_size_sp: f32,
485 ) -> PreparedTextLayout {
486 let options = frame.options.normalized();
487 let min_lines = options.min_lines;
488 let frame = TextFrame {
489 options: TextLayoutOptions {
490 overflow: TextOverflow::Clip,
491 ..options
492 },
493 ..frame
494 };
495 let fits =
496 |layout: &TextLayout| !layout.did_overflow_height() && !layout.did_overflow_width();
497 let full = self.layout(&text, &style, frame, (units, shaped));
498 let base = style.resolve_font_size(DEFAULT_FONT_SIZE_SP);
499 let min_factor = (min_font_size_sp.min(base) / base).clamp(0.0, 1.0);
500 if fits(&full) || !frame.max_width.is_finite() || min_factor >= 1.0 {
501 return PreparedTextLayout::new(text, style, full, (frame.max_width, min_lines));
502 }
503 let at = |factor: f32| {
506 let units = TextUnits {
507 factor: units.factor * factor,
508 ..units
509 };
510 self.layout(&text, &style, frame, (units, Some(&full)))
511 };
512 let (mut best, mut factor) = (at(min_factor), min_factor);
513 if fits(&best) {
514 let (mut low, mut high) = (min_factor, 1.0);
515 for _ in 0..SCALE_DOWN_SEARCH_STEPS {
516 let mid = (low + high) * 0.5;
517 let candidate = at(mid);
518 if fits(&candidate) {
519 low = mid;
520 (best, factor) = (candidate, mid);
521 } else {
522 high = mid;
523 }
524 }
525 }
526 let (text, style) = scaled_drawing(&text, &style, factor);
527 PreparedTextLayout::new(text, style, best, (frame.max_width, min_lines))
528 }
529
530 fn layout(
533 &self,
534 text: &AnnotatedString,
535 style: &TextStyle,
536 frame: TextFrame,
537 (units, shaped): (TextUnits, Option<&TextLayout>),
538 ) -> TextLayout {
539 let paragraph = ParagraphSpans::new(text, style, units);
540 let fonts = self.fonts.borrow();
541 let spans = paragraph.spans(&fonts);
542 let input = LayoutInput {
543 text: &text.text,
544 spans: &spans,
545 paragraph: &style.paragraph_style,
546 lengths: paragraph.lengths,
547 min_width: frame.min_width,
548 max_width: frame.max_width,
549 options: frame.options,
550 };
551 let mut layouter = self.layouter.borrow_mut();
552 match shaped {
553 Some(shaped) => layouter.relayout(&input, &fonts, shaped),
554 None => layouter.layout(&input, &fonts),
555 }
556 }
557
558 fn intrinsics(
560 &self,
561 text: &AnnotatedString,
562 style: &TextStyle,
563 units: TextUnits,
564 ) -> ParagraphIntrinsics {
565 let key = TextKey::of(
566 text,
567 style.measurement_hash(),
568 TextLayoutOptions::default(),
569 units,
570 );
571 if let Some(key) = key
572 && let Some(found) = self.intrinsics.borrow_mut().get(&key).copied()
573 {
574 return found;
575 }
576 let paragraph = ParagraphSpans::new(text, style, units);
577 let fonts = self.fonts.borrow();
578 let spans = paragraph.spans(&fonts);
579 let input = LayoutInput {
580 text: &text.text,
581 spans: &spans,
582 paragraph: &style.paragraph_style,
583 lengths: paragraph.lengths,
584 min_width: 0.0,
585 max_width: f32::INFINITY,
586 options: TextLayoutOptions::default(),
587 };
588 let found = self.layouter.borrow_mut().intrinsics(&input, &fonts);
589 if let Some(key) = key {
590 self.intrinsics.borrow_mut().push(key, found);
591 }
592 found
593 }
594
595 fn extent(&self, style: &TextStyle, units: TextUnits) -> Option<FontExtent> {
598 let span = &style.span_style;
599 let size = units
600 .density
601 .font_scale_curve()
602 .sp_to_dp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP))
603 * units.factor;
604 let fonts = self.fonts.borrow();
605 let typeface = fonts.resolve(FontRequest {
606 family: span.font_family.as_ref().unwrap_or(&FontFamily::Default),
607 weight: span.font_weight.unwrap_or(FontWeight::NORMAL),
608 style: span.font_style.unwrap_or(FontStyle::Normal),
609 })?;
610 let scale = size / f32::from(typeface.units_per_em());
611 let metrics = typeface.line_metrics();
612 Some(FontExtent::new(
613 metrics.ascent * scale,
614 -metrics.descent * scale,
615 metrics.line_gap * scale,
616 ))
617 }
618
619 fn line_box(&self, style: &TextStyle, units: TextUnits) -> Option<LineBox> {
622 let extent = self.extent(style, units)?;
623 let empty = AnnotatedString::from("");
624 let paragraph = ParagraphSpans::new(&empty, style, units);
625 let asked = paragraph.lengths.line_height.unwrap_or(f32::NAN);
626 Some(paragraph_line_box(
627 &style.paragraph_style,
628 extent,
629 asked,
630 units.density.density(),
631 ))
632 }
633
634 pub(crate) fn begin_layout_pass(&self) {
636 self.pass_owed.set(false);
637 let headroom = crate::current_memory_headroom();
638 let idle_passes = match headroom {
640 MemoryHeadroom::Ample => IDLE_PASSES,
641 MemoryHeadroom::Tight => 1,
642 };
643 self.layouts.borrow_mut().begin_pass(idle_passes, headroom);
644 self.intrinsics.borrow_mut().begin_pass(idle_passes, drop);
645 self.layouter.borrow_mut().set_memory_headroom(headroom);
646 }
647
648 pub(crate) fn owe_pass(&self) {
651 self.pass_owed.set(true);
652 }
653
654 pub(crate) fn pay_owed_pass(&self) {
657 if self.pass_owed.get() {
658 self.begin_layout_pass();
659 }
660 }
661}
662
663fn default_fonts() -> FontFamilyResolver {
666 #[cfg(any(test, feature = "test-helpers"))]
667 {
668 super::test_fonts()
669 }
670 #[cfg(not(any(test, feature = "test-helpers")))]
671 {
672 FontFamilyResolver::new([])
673 }
674}
675
676fn scaled_drawing(
680 text: &Rc<AnnotatedString>,
681 style: &TextStyle,
682 factor: f32,
683) -> (Rc<AnnotatedString>, Arc<TextStyle>) {
684 let mut scaled_style = style.clone();
685 scaled_style.span_style.font_size =
686 TextUnit::Sp(style.resolve_font_size(DEFAULT_FONT_SIZE_SP) * factor);
687 scale_span_drawing(&mut scaled_style.span_style, factor);
688 let text = if text.span_styles.is_empty() {
689 Rc::clone(text)
690 } else {
691 let mut scaled = AnnotatedString::clone(text);
692 for span in &mut scaled.span_styles {
693 if let TextUnit::Sp(size) = span.item.font_size {
694 span.item.font_size = TextUnit::Sp(size * factor);
695 }
696 scale_span_drawing(&mut span.item, factor);
697 }
698 Rc::new(scaled)
699 };
700 (text, Arc::new(scaled_style))
701}
702
703fn scale_span_drawing(span: &mut SpanStyle, factor: f32) {
705 if let TextUnit::Sp(spacing) = span.letter_spacing {
706 span.letter_spacing = TextUnit::Sp(spacing * factor);
707 }
708 if let Some(shadow) = &mut span.shadow {
709 shadow.offset.x *= factor;
710 shadow.offset.y *= factor;
711 shadow.blur_radius *= factor;
712 }
713 if let Some(TextDrawStyle::Stroke { width }) = &mut span.draw_style {
714 *width *= factor;
715 }
716}
717
718fn host_units() -> TextUnits {
720 TextUnits {
721 density: Density::from_host(),
722 factor: 1.0,
723 }
724}
725
726fn resolved_units() -> TextUnits {
729 TextUnits {
730 density: Density::with_curve(Density::from_host().density(), FontScaleCurve::linear(1.0)),
731 factor: 1.0,
732 }
733}
734
735fn prepare_borrowed(
736 text: &AnnotatedString,
737 style: &TextStyle,
738 frame: TextFrame,
739 units: TextUnits,
740) -> Rc<PreparedTextLayout> {
741 crate::render_state::with_text_service(|service| {
742 service.prepare(
743 LayoutSource::Borrowed { text, style },
744 frame,
745 (units, CacheAdmission::First),
746 )
747 })
748}
749
750pub fn set_text_fonts(fonts: FontFamilyResolver) {
753 let Some(context) = crate::render_state::current_app_context() else {
754 panic!("set_text_fonts requires an active AppContext");
755 };
756 context.set_text_fonts(fonts);
757}
758
759pub fn measure_text(text: &AnnotatedString, style: &TextStyle) -> TextMetrics {
762 prepare_borrowed(
763 text,
764 style,
765 TextFrame::unbounded(TextLayoutOptions::default()),
766 host_units(),
767 )
768 .metrics()
769}
770
771pub(crate) fn prepare_resolved(text: &SharedText<'_>) -> Rc<PreparedTextLayout> {
777 crate::render_state::with_text_service(|service| {
778 service.prepare(
779 LayoutSource::Shared { text, shaped: None },
780 TextFrame::unbounded(TextLayoutOptions::default()),
781 (resolved_units(), CacheAdmission::Drawn),
782 )
783 })
784}
785
786pub fn measure_text_with_options(
789 text: &AnnotatedString,
790 style: &TextStyle,
791 options: TextLayoutOptions,
792 max_width: Option<f32>,
793) -> TextMetrics {
794 prepare_borrowed(
795 text,
796 style,
797 TextFrame::up_to(max_width, options),
798 host_units(),
799 )
800 .metrics()
801}
802
803pub fn prepare_text_layout(
805 text: &AnnotatedString,
806 style: &TextStyle,
807 options: TextLayoutOptions,
808 max_width: Option<f32>,
809) -> Rc<PreparedTextLayout> {
810 prepare_borrowed(
811 text,
812 style,
813 TextFrame::up_to(max_width, options),
814 host_units(),
815 )
816}
817
818pub(crate) fn prepare_shared(
821 text: &SharedText<'_>,
822 frame: TextFrame,
823 admission: CacheAdmission,
824 shaped: Option<&TextLayout>,
825) -> Rc<PreparedTextLayout> {
826 crate::render_state::with_text_service(|service| {
827 service.prepare(
828 LayoutSource::Shared { text, shaped },
829 frame,
830 (host_units(), admission),
831 )
832 })
833}
834
835pub fn get_offset_for_position(text: &AnnotatedString, style: &TextStyle, x: f32, y: f32) -> usize {
839 prepare_borrowed(
840 text,
841 style,
842 TextFrame::unbounded(TextLayoutOptions::default()),
843 host_units(),
844 )
845 .layout
846 .offset_for_position(x, y)
847}
848
849pub(crate) fn offset_in_drawn_text(
852 text: &AnnotatedString,
853 style: &TextStyle,
854 options: TextLayoutOptions,
855 width: f32,
856 point: Point,
857) -> usize {
858 prepare_borrowed(text, style, TextFrame::drawn(width, options), host_units())
859 .layout
860 .offset_for_position(point.x, point.y)
861}
862
863pub fn offset_for_position_wrapped(
868 text: &str,
869 style: &TextStyle,
870 wrap_width: Option<f32>,
871 x: f32,
872 y: f32,
873) -> usize {
874 prepare_borrowed(
875 &AnnotatedString::from(text),
876 style,
877 TextFrame::up_to(wrap_width, TextLayoutOptions::default()),
878 host_units(),
879 )
880 .layout
881 .offset_for_position(x, y)
882}
883
884pub fn get_cursor_x_for_offset(text: &AnnotatedString, style: &TextStyle, offset: usize) -> f32 {
887 prepare_borrowed(
888 text,
889 style,
890 TextFrame::unbounded(TextLayoutOptions::default()),
891 host_units(),
892 )
893 .layout
894 .horizontal_position(offset, CaretDirection::Primary)
895}
896
897pub fn layout_text(text: &AnnotatedString, style: &TextStyle) -> TextLayoutResult {
899 prepare_borrowed(
900 text,
901 style,
902 TextFrame::unbounded(TextLayoutOptions::default()),
903 host_units(),
904 )
905 .result()
906}
907
908pub fn wrapped_line_ranges(
911 text: &AnnotatedString,
912 style: &TextStyle,
913 options: TextLayoutOptions,
914 max_width: Option<f32>,
915) -> Vec<Range<usize>> {
916 let prepared = prepare_borrowed(
917 text,
918 style,
919 TextFrame::up_to(max_width, options),
920 host_units(),
921 );
922 let layout = &prepared.layout;
923 (0..layout.line_count())
924 .map(|line| {
925 let (start, end) = (layout.line_start(line), layout.line_end(line));
926 let line_text = text.text.get(start..end).unwrap_or_default();
927 let without_break = line_text.trim_end_matches(['\n', '\r', '\u{2028}', '\u{2029}']);
928 start..start + without_break.len()
929 })
930 .collect()
931}
932
933pub(crate) fn min_intrinsic_text_width(text: &AnnotatedString, style: &TextStyle) -> f32 {
936 crate::render_state::with_text_service(|service| service.intrinsics(text, style, host_units()))
937 .min_intrinsic_width
938}
939
940pub fn text_line_box(style: &TextStyle) -> Option<LineBox> {
943 crate::render_state::with_text_service(|service| service.line_box(style, host_units()))
944}
945
946pub(crate) fn resolved_line_box(style: &TextStyle) -> Option<LineBox> {
948 crate::render_state::current_app_context()?;
949 crate::render_state::with_text_service(|service| service.line_box(style, resolved_units()))
950}
951
952pub fn first_baseline(style: &TextStyle) -> Option<f32> {
955 text_line_box(style).map(LineBox::first_baseline)
956}
957
958pub fn glyph_line_box(style: &TextStyle, line_height: f32) -> (f32, f32) {
963 let units = host_units();
964 crate::render_state::with_text_service(|service| {
965 let extent = service.extent(style, units)?;
966 let line_box = service.line_box(style, units)?;
967 Some((
968 line_box.first_baseline() - extent.ascent,
969 extent.ascent + extent.descent,
970 ))
971 })
972 .map_or((0.0, line_height), |(top, height)| {
973 (top.clamp(0.0, line_height), height.min(line_height))
974 })
975}
976
977#[cfg(test)]
978#[path = "tests/measure_tests.rs"]
979mod tests;