1pub use parley;
5pub use parley::fontique;
6
7use alloc::vec::Vec;
8use core::ops::Range;
9use core::pin::Pin;
10use euclid::num::Zero;
11use skrifa::MetadataProvider as _;
12use std::cell::RefCell;
13
14use crate::{
15 Color, SharedString,
16 graphics::FontRequest,
17 item_rendering::PlainOrStyledText,
18 items::TextStrokeStyle,
19 lengths::{
20 LogicalBorderRadius, LogicalLength, LogicalPoint, LogicalRect, LogicalSize, PhysicalPx,
21 PointLengths, ScaleFactor, SizeLengths,
22 },
23 renderer::RendererSealed,
24 textlayout::{TextHorizontalAlignment, TextOverflow, TextVerticalAlignment, TextWrap},
25};
26
27type InnerTextLayoutCache = crate::item_rendering::ItemCache<Vec<TextParagraph>>;
28
29pub struct TextLayoutCache {
31 inner: InnerTextLayoutCache,
32 #[cfg(feature = "testing")]
33 cache_miss_count: std::cell::Cell<u64>,
34}
35
36#[allow(clippy::derivable_impls)] impl Default for TextLayoutCache {
38 fn default() -> Self {
39 Self {
40 inner: Default::default(),
41 #[cfg(feature = "testing")]
42 cache_miss_count: std::cell::Cell::new(0),
43 }
44 }
45}
46
47impl TextLayoutCache {
48 pub fn clear_cache_if_scale_factor_changed(&self, window: &crate::api::Window) {
49 self.inner.clear_cache_if_scale_factor_changed(window);
50 }
51 pub fn component_destroyed(&self, component: crate::item_tree::ItemTreeRef) {
52 self.inner.component_destroyed(component);
53 }
54 pub fn clear_all(&self) {
55 self.inner.clear_all();
56 }
57}
58
59#[cfg(feature = "testing")]
60impl TextLayoutCache {
61 pub fn cache_miss_count(&self) -> u64 {
62 self.cache_miss_count.get()
63 }
64 pub fn reset_cache_miss_count(&self) {
65 self.cache_miss_count.set(0);
66 }
67}
68
69pub type PhysicalLength = euclid::Length<f32, PhysicalPx>;
70pub type PhysicalRect = euclid::Rect<f32, PhysicalPx>;
71type PhysicalSize = euclid::Size2D<f32, PhysicalPx>;
72type PhysicalPoint = euclid::Point2D<f32, PhysicalPx>;
73
74use i_slint_common::sharedfontique;
75
76pub trait GlyphRenderer: crate::item_rendering::ItemRenderer {
79 type PlatformBrush: Clone;
81
82 fn platform_text_fill_brush(
84 &mut self,
85 brush: crate::Brush,
86 size: LogicalSize,
87 ) -> Option<Self::PlatformBrush>;
88
89 fn platform_brush_for_color(&mut self, color: &Color) -> Option<Self::PlatformBrush>;
91
92 fn platform_text_stroke_brush(
94 &mut self,
95 brush: crate::Brush,
96 physical_stroke_width: f32,
97 size: LogicalSize,
98 ) -> Option<Self::PlatformBrush>;
99
100 fn draw_glyph_run(
105 &mut self,
106 font: &parley::FontData,
107 font_size: PhysicalLength,
108 normalized_coords: &[i16],
109 synthesis: &fontique::Synthesis,
110 brush: Self::PlatformBrush,
111 y_offset: PhysicalLength,
112 glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
113 );
114
115 fn fill_rectange_with_color(&mut self, physical_rect: PhysicalRect, color: Color) {
116 if let Some(platform_brush) = self.platform_brush_for_color(&color) {
117 self.fill_rectangle(physical_rect, platform_brush);
118 }
119 }
120
121 fn fill_rectangle(&mut self, physical_rect: PhysicalRect, brush: Self::PlatformBrush);
124}
125
126pub const DEFAULT_FONT_SIZE: LogicalLength = LogicalLength::new(12.);
127
128std::thread_local! {
129 static LAYOUT_CONTEXT: RefCell<parley::LayoutContext<Brush>> = Default::default();
130}
131
132#[derive(Debug, Default, PartialEq, Clone, Copy)]
133struct Brush {
134 override_fill_color: Option<Color>,
136 stroke: Option<TextStrokeStyle>,
137 link_color: Option<Color>,
138}
139
140struct LayoutOptions {
141 max_width: Option<LogicalLength>,
142 max_height: Option<LogicalLength>,
143 horizontal_align: TextHorizontalAlignment,
144 vertical_align: TextVerticalAlignment,
145 text_overflow: TextOverflow,
146}
147
148impl LayoutOptions {
149 fn new_from_textinput(
150 text_input: Pin<&crate::items::TextInput>,
151 max_width: Option<LogicalLength>,
152 max_height: Option<LogicalLength>,
153 ) -> Self {
154 Self {
155 max_width,
156 max_height,
157 horizontal_align: text_input.horizontal_alignment(),
158 vertical_align: text_input.vertical_alignment(),
159 text_overflow: TextOverflow::Clip,
160 }
161 }
162}
163
164struct LayoutWithoutLineBreaksBuilder {
165 font_request: Option<FontRequest>,
166 text_wrap: TextWrap,
167 stroke: Option<TextStrokeStyle>,
168 scale_factor: ScaleFactor,
169 pixel_size: LogicalLength,
170}
171
172impl LayoutWithoutLineBreaksBuilder {
173 fn new(
174 font_request: Option<FontRequest>,
175 text_wrap: TextWrap,
176 stroke: Option<TextStrokeStyle>,
177 scale_factor: ScaleFactor,
178 ) -> Self {
179 let pixel_size = font_request
180 .as_ref()
181 .and_then(|font_request| font_request.pixel_size)
182 .unwrap_or(DEFAULT_FONT_SIZE);
183
184 Self { font_request, text_wrap, stroke, scale_factor, pixel_size }
185 }
186
187 fn ranged_builder<'a>(
188 &self,
189 layout_ctx: &'a mut parley::LayoutContext<Brush>,
190 font_ctx: &'a mut parley::FontContext,
191 text: &'a str,
192 ) -> parley::RangedBuilder<'a, Brush> {
193 let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
194
195 if let Some(ref font_request) = self.font_request {
196 let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
197 .into_iter()
198 .map(parley::style::FontFamilyName::Generic);
199
200 let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
201 &font_request.family
202 {
203 let mut iter =
204 core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
205 .chain(fallback_family_iter);
206 &core::array::from_fn::<
207 _,
208 { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
209 _,
210 >(|_| iter.next().unwrap())
211 } else {
212 &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
213 |_| fallback_family_iter.next().unwrap(),
214 )
215 };
216
217 builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
218 font_families,
219 )));
220
221 if let Some(weight) = font_request.weight {
222 builder.push_default(parley::StyleProperty::FontWeight(
223 parley::style::FontWeight::new(weight as f32),
224 ));
225 }
226 if let Some(letter_spacing) = font_request.letter_spacing {
227 builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
228 }
229 builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
230 parley::style::FontStyle::Italic
231 } else {
232 parley::style::FontStyle::Normal
233 }));
234 }
235 builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
236 builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
237 TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
238 TextWrap::WordWrap => parley::style::WordBreak::Normal,
239 TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
240 }));
241 builder.push_default(parley::StyleProperty::OverflowWrap(match self.text_wrap {
242 TextWrap::NoWrap => parley::style::OverflowWrap::Normal,
243 TextWrap::WordWrap | TextWrap::CharWrap => parley::style::OverflowWrap::Anywhere,
244 }));
245
246 builder.push_default(parley::StyleProperty::Brush(Brush {
247 override_fill_color: None,
248 stroke: self.stroke,
249 link_color: None,
250 }));
251
252 builder
253 }
254
255 fn build(
256 &self,
257 font_context: &mut parley::FontContext,
258 text: &str,
259 selection: Option<(Range<usize>, Color)>,
260 formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
261 link_color: Option<Color>,
262 ) -> parley::Layout<Brush> {
263 use i_slint_common::styled_text::Style;
264
265 LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
266 let mut builder = self.ranged_builder(layout_ctx, font_context, text);
267
268 if let Some((selection_range, selection_color)) = selection {
269 {
270 builder.push(
271 parley::StyleProperty::Brush(Brush {
272 override_fill_color: Some(selection_color),
273 stroke: self.stroke,
274 link_color: None,
275 }),
276 selection_range,
277 );
278 }
279 }
280
281 for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
283 match span.style {
284 Style::Emphasis => {
285 builder.push(
286 parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
287 span.range,
288 );
289 }
290 Style::Strikethrough => {
291 builder.push(parley::StyleProperty::Strikethrough(true), span.range);
292 }
293 Style::Strong => {
294 builder.push(
295 parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
296 span.range,
297 );
298 }
299 Style::Code => {
300 builder.push(
301 parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
302 parley::style::FontFamilyName::Generic(
303 parley::style::GenericFamily::Monospace,
304 ),
305 )),
306 span.range,
307 );
308 }
309 Style::Underline => {
310 builder.push(parley::StyleProperty::Underline(true), span.range);
311 }
312 Style::Link => {
313 builder.push(parley::StyleProperty::Underline(true), span.range.clone());
314 builder.push(
315 parley::StyleProperty::Brush(Brush {
316 override_fill_color: None,
317 stroke: self.stroke,
318 link_color,
319 }),
320 span.range,
321 );
322 }
323 Style::Color(color) => {
324 builder.push(
325 parley::StyleProperty::Brush(Brush {
326 override_fill_color: Some(crate::Color::from_argb_encoded(color)),
327 stroke: self.stroke,
328 link_color: None,
329 }),
330 span.range,
331 );
332 }
333 }
334 }
335
336 builder.build(text)
337 })
338 }
339}
340
341fn create_text_paragraphs(
342 layout_builder: &LayoutWithoutLineBreaksBuilder,
343 font_context: &mut parley::FontContext,
344 text: PlainOrStyledText,
345 selection: Option<(Range<usize>, Color)>,
346 link_color: Color,
347) -> Vec<TextParagraph> {
348 let paragraph_from_text =
349 |font_context: &mut parley::FontContext,
350 text: &str,
351 range: std::ops::Range<usize>,
352 formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
353 links: Vec<(std::ops::Range<usize>, std::string::String)>| {
354 let selection = selection.clone().and_then(|(selection, selection_color)| {
355 let sel_start = selection.start.max(range.start);
356 let sel_end = selection.end.min(range.end);
357
358 if sel_start < sel_end {
359 let local_selection = (sel_start - range.start)..(sel_end - range.start);
360 Some((local_selection, selection_color))
361 } else {
362 None
363 }
364 });
365
366 let layout =
367 layout_builder.build(font_context, text, selection, formatting, Some(link_color));
368
369 TextParagraph { range, y: PhysicalLength::default(), layout, links }
370 };
371
372 let mut paragraphs = Vec::with_capacity(1);
373
374 match text {
375 PlainOrStyledText::Plain(ref text) => {
376 let paragraph_ranges = core::iter::from_fn({
377 let mut start = 0;
378 let mut char_it = text.char_indices().peekable();
379 let mut eot = false;
380 move || {
381 for (idx, ch) in char_it.by_ref() {
382 if ch == '\n' {
383 let next_range = start..idx;
384 start = idx + ch.len_utf8();
385 return Some(next_range);
386 }
387 }
388
389 if eot {
390 return None;
391 }
392 eot = true;
393 Some(start..text.len())
394 }
395 });
396
397 for range in paragraph_ranges {
398 paragraphs.push(paragraph_from_text(
399 font_context,
400 &text[range.clone()],
401 range,
402 Default::default(),
403 Default::default(),
404 ));
405 }
406 }
407 PlainOrStyledText::Styled(rich_text) => {
408 for paragraph in rich_text.paragraphs {
409 paragraphs.push(paragraph_from_text(
410 font_context,
411 ¶graph.text,
412 0..0,
413 paragraph.formatting,
414 paragraph.links,
415 ));
416 }
417 }
418 };
419
420 paragraphs
421}
422
423fn layout(
430 layout_builder: &LayoutWithoutLineBreaksBuilder,
431 font_context: &mut parley::FontContext,
432 mut paragraphs: Vec<TextParagraph>,
433 scale_factor: ScaleFactor,
434 text_wrap: TextWrap,
435 options: LayoutOptions,
436) -> Layout {
437 let max_physical_width = options.max_width.map(|max_width| max_width * scale_factor);
438 let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor);
439
440 let get_elipsis_glyph = |font_context: &mut parley::FontContext| {
442 let mut layout = layout_builder.build(font_context, "…", None, None, None);
443 layout.break_all_lines(None);
444 let line = layout.lines().next()?;
445 let item = line.items().next()?;
446 let run = match item {
447 parley::layout::PositionedLayoutItem::GlyphRun(run) => Some(run),
448 _ => return None,
449 }?;
450 let glyph = run.positioned_glyphs().next()?;
451 Some((glyph, run.run().font().clone()))
452 };
453
454 let elision_info =
455 if let (TextOverflow::Elide, Some(max_physical_width)) =
456 (options.text_overflow, max_physical_width)
457 {
458 get_elipsis_glyph(font_context).map(|(elipsis_glyph, font_for_elipsis_glyph)| {
459 ElisionInfo { elipsis_glyph, font_for_elipsis_glyph, max_physical_width }
460 })
461 } else {
462 None
463 };
464
465 let mut para_y = 0.0;
466 for para in paragraphs.iter_mut() {
467 para.layout.break_all_lines(
468 max_physical_width.filter(|_| text_wrap != TextWrap::NoWrap).map(|width| width.get()),
469 );
470 para.layout.align(
471 max_physical_width.map(|width| width.get()),
472 match options.horizontal_align {
473 TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
474 parley::Alignment::Left
475 }
476 TextHorizontalAlignment::Center => parley::Alignment::Center,
477 TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
478 parley::Alignment::Right
479 }
480 },
481 parley::AlignmentOptions::default(),
482 );
483
484 para.y = PhysicalLength::new(para_y);
485 para_y += para.layout.height();
486 }
487
488 let max_width = paragraphs
489 .iter()
490 .map(|p| {
491 PhysicalLength::new(p.layout.full_width())
496 })
497 .fold(PhysicalLength::zero(), PhysicalLength::max);
498 let height = paragraphs
499 .last()
500 .map_or(PhysicalLength::zero(), |p| p.y + PhysicalLength::new(p.layout.height()));
501
502 let y_offset = match (max_physical_height, options.vertical_align) {
503 (Some(max_height), TextVerticalAlignment::Center) => (max_height - height) / 2.0,
504 (Some(max_height), TextVerticalAlignment::Bottom) => max_height - height,
505 (None, _) | (Some(_), TextVerticalAlignment::Top) => PhysicalLength::new(0.0),
506 };
507
508 Layout { paragraphs, y_offset, elision_info, max_width, height, max_physical_height }
509}
510
511struct CachedParagraphsGuard<'a> {
513 paragraphs: Option<Vec<TextParagraph>>,
514 container: Option<std::cell::RefMut<'a, Vec<TextParagraph>>>,
515}
516
517impl Drop for CachedParagraphsGuard<'_> {
518 fn drop(&mut self) {
519 if let (Some(paragraphs), Some(container)) = (self.paragraphs.take(), &mut self.container) {
520 **container = paragraphs;
521 }
522 }
523}
524
525fn shape_paragraphs(
526 text: Pin<&dyn crate::item_rendering::RenderText>,
527 item_rc: Option<&crate::item_tree::ItemRc>,
528 scale_factor: ScaleFactor,
529 font_context: &mut parley::FontContext,
530) -> Vec<TextParagraph> {
531 let (stroke_brush, _, stroke_style) = text.stroke();
532 let has_stroke = !stroke_brush.is_transparent();
533 let builder = LayoutWithoutLineBreaksBuilder::new(
534 item_rc.map(|irc| text.font_request(irc)),
535 text.wrap(),
536 has_stroke.then_some(stroke_style),
537 scale_factor,
538 );
539 create_text_paragraphs(&builder, font_context, text.text(), None, text.link_color())
540}
541
542fn get_or_create_text_paragraphs<'a>(
543 cache: Option<&'a TextLayoutCache>,
544 item_rc: Option<&crate::item_tree::ItemRc>,
545 text: Pin<&dyn crate::item_rendering::RenderText>,
546 scale_factor: ScaleFactor,
547 font_context: &mut parley::FontContext,
548) -> CachedParagraphsGuard<'a> {
549 if let (Some(cache), Some(item_rc)) = (cache, item_rc) {
550 let mut entry = cache.inner.get_or_update_cache_entry_ref(item_rc, || {
551 #[cfg(feature = "testing")]
552 cache.cache_miss_count.set(cache.cache_miss_count.get() + 1);
553 shape_paragraphs(text, Some(item_rc), scale_factor, font_context)
554 });
555 let paragraphs = std::mem::take(&mut *entry);
556 CachedParagraphsGuard { paragraphs: Some(paragraphs), container: Some(entry) }
557 } else {
558 CachedParagraphsGuard {
559 paragraphs: Some(shape_paragraphs(text, item_rc, scale_factor, font_context)),
560 container: None,
561 }
562 }
563}
564
565struct ElisionInfo {
566 elipsis_glyph: parley::layout::Glyph,
567 font_for_elipsis_glyph: parley::FontData,
568 max_physical_width: PhysicalLength,
569}
570
571struct TextParagraph {
572 range: Range<usize>,
573 y: PhysicalLength,
574 layout: parley::Layout<Brush>,
575 links: std::vec::Vec<(Range<usize>, std::string::String)>,
576}
577
578impl TextParagraph {
579 fn draw<R: GlyphRenderer>(
580 &self,
581 layout: &Layout,
582 item_renderer: &mut R,
583 default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
584 default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
585 draw_glyphs: &mut dyn FnMut(
586 &mut R,
587 &parley::FontData,
588 PhysicalLength,
589 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
592 PhysicalLength, &mut dyn Iterator<Item = parley::layout::Glyph>,
594 ),
595 ) {
596 let para_y = layout.y_offset + self.y;
597
598 let mut lines = self
599 .layout
600 .lines()
601 .take_while(|line| {
602 let metrics = line.metrics();
603 match layout.max_physical_height {
604 Some(max_physical_height) if layout.elision_info.is_some() => {
608 max_physical_height.get().ceil() >= metrics.max_coord
609 }
610 _ => true,
611 }
612 })
613 .peekable();
614
615 while let Some(line) = lines.next() {
616 let last_line = lines.peek().is_none();
617 for item in line.items() {
618 match item {
619 parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
620 let elipsis = if last_line {
621 let (truncated_glyphs, elipsis) =
622 layout.glyphs_with_elision(&glyph_run);
623
624 Self::draw_glyph_run(
625 &glyph_run,
626 item_renderer,
627 default_fill_brush,
628 default_stroke_brush,
629 para_y,
630 &mut truncated_glyphs.into_iter(),
631 draw_glyphs,
632 );
633 elipsis
634 } else {
635 Self::draw_glyph_run(
636 &glyph_run,
637 item_renderer,
638 default_fill_brush,
639 default_stroke_brush,
640 para_y,
641 &mut glyph_run.positioned_glyphs(),
642 draw_glyphs,
643 );
644 None
645 };
646
647 if let Some((elipsis_glyph, elipsis_font, font_size)) = elipsis {
648 let run = glyph_run.run();
649 draw_glyphs(
650 item_renderer,
651 &elipsis_font,
652 font_size,
653 run.normalized_coords(),
654 &run.synthesis(),
655 default_fill_brush.clone(),
656 para_y,
657 &mut core::iter::once(elipsis_glyph),
658 );
659 }
660 }
661 parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
662 };
663 }
664 }
665 }
666
667 fn draw_glyph_run<R: GlyphRenderer>(
668 glyph_run: &parley::layout::GlyphRun<Brush>,
669 item_renderer: &mut R,
670 default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
671 default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
672 para_y: PhysicalLength,
673 glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
674 draw_glyphs: &mut dyn FnMut(
675 &mut R,
676 &parley::FontData,
677 PhysicalLength,
678 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
681 PhysicalLength,
682 &mut dyn Iterator<Item = parley::layout::Glyph>,
683 ),
684 ) {
685 let run = glyph_run.run();
686 let normalized_coords = run.normalized_coords();
687 let synthesis = run.synthesis();
688 let brush = &glyph_run.style().brush;
689
690 let (fill_brush, stroke_style) = match (brush.override_fill_color, brush.link_color) {
691 (Some(color), _) => {
692 let Some(selection_brush) = item_renderer.platform_brush_for_color(&color) else {
693 return;
694 };
695 (selection_brush.clone(), &None)
696 }
697 (None, Some(color)) => {
698 let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
699 return;
700 };
701 (link_brush.clone(), &None)
702 }
703 (None, None) => (default_fill_brush.clone(), &brush.stroke),
704 };
705
706 match stroke_style {
707 Some(TextStrokeStyle::Outside) => {
708 let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
709
710 if let Some(stroke_brush) = default_stroke_brush.clone() {
711 draw_glyphs(
712 item_renderer,
713 run.font(),
714 PhysicalLength::new(run.font_size()),
715 normalized_coords,
716 &synthesis,
717 stroke_brush,
718 para_y,
719 &mut glyphs.iter().cloned(),
720 );
721 }
722
723 draw_glyphs(
724 item_renderer,
725 run.font(),
726 PhysicalLength::new(run.font_size()),
727 normalized_coords,
728 &synthesis,
729 fill_brush.clone(),
730 para_y,
731 &mut glyphs.into_iter(),
732 );
733 }
734 Some(TextStrokeStyle::Center) => {
735 let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
736
737 draw_glyphs(
738 item_renderer,
739 run.font(),
740 PhysicalLength::new(run.font_size()),
741 normalized_coords,
742 &synthesis,
743 fill_brush.clone(),
744 para_y,
745 &mut glyphs.iter().cloned(),
746 );
747
748 if let Some(stroke_brush) = default_stroke_brush.clone() {
749 draw_glyphs(
750 item_renderer,
751 run.font(),
752 PhysicalLength::new(run.font_size()),
753 normalized_coords,
754 &synthesis,
755 stroke_brush,
756 para_y,
757 &mut glyphs.into_iter(),
758 );
759 }
760 }
761 None => {
762 draw_glyphs(
763 item_renderer,
764 run.font(),
765 PhysicalLength::new(run.font_size()),
766 normalized_coords,
767 &synthesis,
768 fill_brush.clone(),
769 para_y,
770 glyphs_it,
771 );
772 }
773 }
774
775 let metrics = run.metrics();
776
777 if glyph_run.style().underline.is_some() {
778 item_renderer.fill_rectangle(
779 PhysicalRect::new(
780 PhysicalPoint::from_lengths(
781 PhysicalLength::new(glyph_run.offset()),
782 para_y
783 + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
784 ),
785 PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
786 ),
787 fill_brush.clone(),
788 );
789 }
790
791 if glyph_run.style().strikethrough.is_some() {
792 item_renderer.fill_rectangle(
793 PhysicalRect::new(
794 PhysicalPoint::from_lengths(
795 PhysicalLength::new(glyph_run.offset()),
796 para_y
797 + PhysicalLength::new(
798 glyph_run.baseline() - metrics.strikethrough_offset,
799 ),
800 ),
801 PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
802 ),
803 fill_brush,
804 );
805 }
806 }
807}
808
809struct Layout {
810 paragraphs: Vec<TextParagraph>,
811 y_offset: PhysicalLength,
812 max_width: PhysicalLength,
813 height: PhysicalLength,
814 max_physical_height: Option<PhysicalLength>,
815 elision_info: Option<ElisionInfo>,
816}
817
818impl Layout {
819 fn paragraph_by_byte_offset(&self, byte_offset: usize) -> Option<&TextParagraph> {
820 self.paragraphs.iter().find(|p| byte_offset >= p.range.start && byte_offset <= p.range.end)
821 }
822
823 fn paragraph_by_y(&self, y: PhysicalLength) -> Option<&TextParagraph> {
824 let y = y - self.y_offset;
826
827 if y < PhysicalLength::zero() {
828 return self.paragraphs.first();
829 }
830
831 let idx = self.paragraphs.binary_search_by(|paragraph| {
832 if y < paragraph.y {
833 core::cmp::Ordering::Greater
834 } else if y >= paragraph.y + PhysicalLength::new(paragraph.layout.height()) {
835 core::cmp::Ordering::Less
836 } else {
837 core::cmp::Ordering::Equal
838 }
839 });
840
841 match idx {
842 Ok(i) => self.paragraphs.get(i),
843 Err(_) => self.paragraphs.last(),
844 }
845 }
846
847 fn selection_geometry(
848 &self,
849 selection_range: Range<usize>,
850 mut callback: impl FnMut(PhysicalRect),
851 ) {
852 for paragraph in &self.paragraphs {
853 let selection_start = selection_range.start.max(paragraph.range.start);
854 let selection_end = selection_range.end.min(paragraph.range.end);
855
856 if selection_start < selection_end {
857 let local_start = selection_start - paragraph.range.start;
858 let local_end = selection_end - paragraph.range.start;
859
860 let selection = parley::editing::Selection::new(
861 parley::editing::Cursor::from_byte_index(
862 ¶graph.layout,
863 local_start,
864 Default::default(),
865 ),
866 parley::editing::Cursor::from_byte_index(
867 ¶graph.layout,
868 local_end,
869 Default::default(),
870 ),
871 );
872
873 selection.geometry_with(¶graph.layout, |rect, _| {
874 callback(PhysicalRect::new(
875 PhysicalPoint::from_lengths(
876 PhysicalLength::new(rect.x0 as _),
877 PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
878 ),
879 PhysicalSize::new(rect.width() as _, rect.height() as _),
880 ));
881 });
882 }
883 }
884 }
885
886 fn byte_offset_from_point(&self, pos: PhysicalPoint) -> usize {
887 let Some(paragraph) = self.paragraph_by_y(pos.y_length()) else {
888 return 0;
889 };
890 let cursor = parley::editing::Cursor::from_point(
891 ¶graph.layout,
892 pos.x,
893 (pos.y_length() - self.y_offset - paragraph.y).get(),
894 );
895 paragraph.range.start + cursor.index()
896 }
897
898 fn cursor_rect_for_byte_offset(
899 &self,
900 byte_offset: usize,
901 cursor_width: PhysicalLength,
902 ) -> PhysicalRect {
903 let Some(paragraph) = self.paragraph_by_byte_offset(byte_offset) else {
904 return PhysicalRect::new(PhysicalPoint::default(), PhysicalSize::new(1.0, 1.0));
905 };
906
907 let local_offset = byte_offset - paragraph.range.start;
908 let cursor = parley::editing::Cursor::from_byte_index(
909 ¶graph.layout,
910 local_offset,
911 Default::default(),
912 );
913 let rect = cursor.geometry(¶graph.layout, cursor_width.get());
914
915 PhysicalRect::new(
916 PhysicalPoint::from_lengths(
917 PhysicalLength::new(rect.x0 as _),
918 PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
919 ),
920 PhysicalSize::new(rect.width() as _, rect.height() as _),
921 )
922 }
923
924 fn glyphs_with_elision<'a>(
928 &'a self,
929 glyph_run: &'a parley::layout::GlyphRun<Brush>,
930 ) -> (
931 impl Iterator<Item = parley::layout::Glyph> + Clone + 'a,
932 Option<(parley::layout::Glyph, parley::FontData, PhysicalLength)>,
933 ) {
934 let elipsis_advance =
935 self.elision_info.as_ref().map(|info| info.elipsis_glyph.advance).unwrap_or(0.0);
936 let max_width = self
937 .elision_info
938 .as_ref()
939 .map(|info| info.max_physical_width)
940 .unwrap_or(PhysicalLength::new(f32::MAX));
941
942 let run_start = PhysicalLength::new(glyph_run.offset());
943 let run_end = PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
944
945 let run_beyond_elision = run_start > max_width;
947 let needs_elision = !run_beyond_elision && run_end.get().floor() > max_width.get().ceil();
949
950 let truncated_glyphs = glyph_run.positioned_glyphs().take_while(move |glyph| {
951 !run_beyond_elision
952 && (!needs_elision
953 || PhysicalLength::new(glyph.x + glyph.advance + elipsis_advance) <= max_width)
954 });
955
956 let elipsis = if needs_elision {
957 self.elision_info.as_ref().map(|info| {
958 let elipsis_x = glyph_run
959 .positioned_glyphs()
960 .find(|glyph| {
961 PhysicalLength::new(glyph.x + glyph.advance + info.elipsis_glyph.advance)
962 > info.max_physical_width
963 })
964 .map(|g| g.x)
965 .unwrap_or(0.0);
966
967 let mut elipsis_glyph = info.elipsis_glyph;
968 elipsis_glyph.x = elipsis_x;
969
970 let font_size = PhysicalLength::new(glyph_run.run().font_size());
971 (elipsis_glyph, info.font_for_elipsis_glyph.clone(), font_size)
972 })
973 } else {
974 None
975 };
976
977 (truncated_glyphs, elipsis)
978 }
979
980 fn draw<R: GlyphRenderer>(
981 &self,
982 item_renderer: &mut R,
983 default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
984 default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
985 draw_glyphs: &mut dyn FnMut(
986 &mut R,
987 &parley::FontData,
988 PhysicalLength,
989 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
992 PhysicalLength, &mut dyn Iterator<Item = parley::layout::Glyph>,
994 ),
995 ) {
996 for paragraph in &self.paragraphs {
997 paragraph.draw(
998 self,
999 item_renderer,
1000 &default_fill_brush,
1001 &default_stroke_brush,
1002 draw_glyphs,
1003 );
1004 }
1005 }
1006}
1007
1008pub fn draw_text(
1009 item_renderer: &mut impl GlyphRenderer,
1010 text: Pin<&dyn crate::item_rendering::RenderText>,
1011 item_rc: Option<&crate::item_tree::ItemRc>,
1012 size: LogicalSize,
1013 cache: Option<&TextLayoutCache>,
1014) {
1015 let max_width = size.width_length();
1016 let max_height = size.height_length();
1017
1018 if max_width.get() <= 0. || max_height.get() <= 0. {
1019 return;
1020 }
1021
1022 let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
1023 else {
1024 return;
1026 };
1027
1028 let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1029
1030 let (stroke_brush, stroke_width, stroke_style) = text.stroke();
1031 let platform_stroke_brush = if !stroke_brush.is_transparent() {
1032 let stroke_width = if stroke_width.get() != 0.0 {
1033 (stroke_width * scale_factor).get()
1034 } else {
1035 1.0
1037 };
1038 let stroke_width = match stroke_style {
1039 TextStrokeStyle::Outside => stroke_width * 2.0,
1040 TextStrokeStyle::Center => stroke_width,
1041 };
1042 item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
1043 } else {
1044 None
1045 };
1046
1047 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1049 item_rc.map(|item_rc| text.font_request(item_rc)),
1050 text.wrap(),
1051 platform_stroke_brush.is_some().then_some(stroke_style),
1052 scale_factor,
1053 );
1054
1055 let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1056
1057 let mut guard =
1058 get_or_create_text_paragraphs(cache, item_rc, text, scale_factor, &mut font_ctx);
1059
1060 let (horizontal_align, vertical_align) = text.alignment();
1061 let text_overflow = text.overflow();
1062 let text_wrap = text.wrap();
1063
1064 let layout = layout(
1065 &layout_builder,
1066 &mut font_ctx,
1067 guard.paragraphs.take().unwrap_or_default(),
1068 scale_factor,
1069 text_wrap,
1070 LayoutOptions {
1071 horizontal_align,
1072 vertical_align,
1073 max_height: Some(max_height),
1074 max_width: Some(max_width),
1075 text_overflow: text.overflow(),
1076 },
1077 );
1078
1079 drop(font_ctx);
1080
1081 let render = if text_overflow == TextOverflow::Clip {
1082 item_renderer.save_state();
1083
1084 item_renderer.combine_clip(
1085 LogicalRect::new(LogicalPoint::default(), size),
1086 LogicalBorderRadius::zero(),
1087 LogicalLength::zero(),
1088 )
1089 } else {
1090 true
1091 };
1092
1093 if render {
1094 layout.draw(
1095 item_renderer,
1096 platform_fill_brush,
1097 platform_stroke_brush,
1098 &mut |item_renderer: &mut _,
1099 font,
1100 font_size,
1101 normalized_coords,
1102 synthesis,
1103 brush,
1104 y_offset,
1105 glyphs_it| {
1106 item_renderer.draw_glyph_run(
1107 font,
1108 font_size,
1109 normalized_coords,
1110 synthesis,
1111 brush,
1112 y_offset,
1113 glyphs_it,
1114 );
1115 },
1116 );
1117 }
1118
1119 if text_overflow == TextOverflow::Clip {
1120 item_renderer.restore_state();
1121 }
1122
1123 guard.paragraphs = Some(layout.paragraphs);
1126}
1127
1128#[cfg(feature = "std")]
1129pub fn link_under_cursor(
1130 font_context: &mut parley::FontContext,
1131 scale_factor: ScaleFactor,
1132 text: Pin<&dyn crate::item_rendering::RenderText>,
1133 item_rc: &crate::item_tree::ItemRc,
1134 size: LogicalSize,
1135 cursor: PhysicalPoint,
1136 cache: Option<&TextLayoutCache>,
1137) -> Option<std::string::String> {
1138 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1139 Some(text.font_request(item_rc)),
1140 text.wrap(),
1141 None,
1142 scale_factor,
1143 );
1144
1145 let mut guard =
1146 get_or_create_text_paragraphs(cache, Some(item_rc), text, scale_factor, font_context);
1147
1148 let (horizontal_align, vertical_align) = text.alignment();
1149
1150 let layout = layout(
1151 &layout_builder,
1152 font_context,
1153 guard.paragraphs.take().unwrap_or_default(),
1154 scale_factor,
1155 text.wrap(),
1156 LayoutOptions {
1157 horizontal_align,
1158 vertical_align,
1159 max_height: Some(size.height_length()),
1160 max_width: Some(size.width_length()),
1161 text_overflow: text.overflow(),
1162 },
1163 );
1164
1165 let result = layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
1166 let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
1167
1168 paragraph
1169 .links
1170 .iter()
1171 .find(|(range, _)| {
1172 let start = parley::editing::Cursor::from_byte_index(
1173 ¶graph.layout,
1174 range.start,
1175 Default::default(),
1176 );
1177 let end = parley::editing::Cursor::from_byte_index(
1178 ¶graph.layout,
1179 range.end,
1180 Default::default(),
1181 );
1182 let mut clicked = false;
1183 let link_range = parley::Selection::new(start, end);
1184 link_range.geometry_with(¶graph.layout, |mut bounding_box, _line| {
1185 bounding_box.y0 += paragraph_y;
1186 bounding_box.y1 += paragraph_y;
1187 clicked = bounding_box.union(parley::BoundingBox::new(
1188 cursor.x.into(),
1189 cursor.y.into(),
1190 cursor.x.into(),
1191 cursor.y.into(),
1192 )) == bounding_box;
1193 });
1194 clicked
1195 })
1196 .map(|(_, link)| link.clone())
1197 });
1198
1199 guard.paragraphs = Some(layout.paragraphs);
1201
1202 result
1203}
1204
1205pub fn draw_text_input(
1206 item_renderer: &mut impl GlyphRenderer,
1207 text_input: Pin<&crate::items::TextInput>,
1208 item_rc: &crate::item_tree::ItemRc,
1209 size: LogicalSize,
1210 password_character: Option<fn() -> char>,
1211) {
1212 let width = size.width_length();
1213 let height = size.height_length();
1214 if width.get() <= 0. || height.get() <= 0. {
1215 return;
1216 }
1217
1218 let visual_representation = text_input.visual_representation(password_character);
1219
1220 let Some(platform_fill_brush) =
1221 item_renderer.platform_text_fill_brush(visual_representation.text_color, size)
1222 else {
1223 return;
1224 };
1225
1226 let selection_range = if !visual_representation.preedit_range.is_empty() {
1227 visual_representation.preedit_range.start..visual_representation.preedit_range.end
1228 } else {
1229 visual_representation.selection_range.start..visual_representation.selection_range.end
1230 };
1231
1232 let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1233
1234 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1235 Some(text_input.font_request(item_rc)),
1236 text_input.wrap(),
1237 None,
1238 scale_factor,
1239 );
1240
1241 let text: SharedString = visual_representation.text.into();
1242
1243 let selection_and_color = if !selection_range.is_empty() {
1247 Some((selection_range.clone(), text_input.selection_foreground_color()))
1248 } else {
1249 None
1250 };
1251
1252 let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1253
1254 let paragraphs_without_linebreaks = create_text_paragraphs(
1255 &layout_builder,
1256 &mut font_ctx,
1257 PlainOrStyledText::Plain(text),
1258 selection_and_color,
1259 Color::default(),
1260 );
1261
1262 let layout = layout(
1263 &layout_builder,
1264 &mut font_ctx,
1265 paragraphs_without_linebreaks,
1266 scale_factor,
1267 text_input.wrap(),
1268 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1269 );
1270
1271 drop(font_ctx);
1272
1273 layout.selection_geometry(selection_range, |selection_rect| {
1274 item_renderer
1275 .fill_rectange_with_color(selection_rect, text_input.selection_background_color());
1276 });
1277
1278 item_renderer.save_state();
1279
1280 let render = item_renderer.combine_clip(
1281 LogicalRect::new(LogicalPoint::default(), size),
1282 LogicalBorderRadius::zero(),
1283 LogicalLength::zero(),
1284 );
1285
1286 if render {
1287 layout.draw(
1288 item_renderer,
1289 platform_fill_brush,
1290 None,
1291 &mut |item_renderer: &mut _,
1292 font,
1293 font_size,
1294 normalized_coords,
1295 synthesis,
1296 brush,
1297 y_offset,
1298 glyphs_it| {
1299 item_renderer.draw_glyph_run(
1300 font,
1301 font_size,
1302 normalized_coords,
1303 synthesis,
1304 brush,
1305 y_offset,
1306 glyphs_it,
1307 );
1308 },
1309 );
1310
1311 if let Some(cursor_pos) = visual_representation.cursor_position {
1312 let cursor_rect = layout.cursor_rect_for_byte_offset(
1313 cursor_pos,
1314 text_input.text_cursor_width() * scale_factor,
1315 );
1316 item_renderer.fill_rectange_with_color(cursor_rect, visual_representation.cursor_color);
1317 }
1318 }
1319
1320 item_renderer.restore_state();
1321}
1322
1323pub fn text_size(
1324 renderer: &dyn RendererSealed,
1325 text_item: Pin<&dyn crate::item_rendering::RenderString>,
1326 item_rc: &crate::item_tree::ItemRc,
1327 max_width: Option<LogicalLength>,
1328 text_wrap: TextWrap,
1329 _cache: Option<&TextLayoutCache>,
1330) -> Option<LogicalSize> {
1331 let scale_factor = renderer.scale_factor()?;
1332
1333 let ctx = renderer.slint_context()?;
1334 let mut font_ctx = ctx.font_context().borrow_mut();
1335
1336 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1337 Some(text_item.font_request(item_rc)),
1338 text_wrap,
1339 None,
1340 scale_factor,
1341 );
1342
1343 let text = text_item.text();
1344
1345 let paragraphs_without_linebreaks =
1346 create_text_paragraphs(&layout_builder, &mut font_ctx, text, None, Color::default());
1347
1348 let layout = layout(
1349 &layout_builder,
1350 &mut font_ctx,
1351 paragraphs_without_linebreaks,
1352 scale_factor,
1353 text_wrap,
1354 LayoutOptions {
1355 max_width,
1356 max_height: None,
1357 horizontal_align: TextHorizontalAlignment::Left,
1358 vertical_align: TextVerticalAlignment::Top,
1359 text_overflow: TextOverflow::Clip,
1360 },
1361 );
1362 Some(PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor)
1363}
1364
1365pub fn char_size(
1366 font_ctx: &mut parley::FontContext,
1367 text_item: Pin<&dyn crate::item_rendering::HasFont>,
1368 item_rc: &crate::item_tree::ItemRc,
1369 ch: char,
1370) -> Option<LogicalSize> {
1371 let font_request = text_item.font_request(item_rc);
1372 let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
1373
1374 let char_map = font.charmap()?;
1375
1376 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1377
1378 let glyph_index = char_map.map(ch)?;
1379
1380 let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
1381
1382 let location = face.axes().location(font.synthesis.variation_settings());
1383
1384 let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
1385 &face,
1386 skrifa::instance::Size::new(pixel_size.get()),
1387 &location,
1388 );
1389
1390 let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
1391
1392 let font_metrics = skrifa::metrics::Metrics::new(
1393 &face,
1394 skrifa::instance::Size::new(pixel_size.get()),
1395 &location,
1396 );
1397
1398 Some(LogicalSize::from_lengths(
1399 advance_width,
1400 LogicalLength::new(font_metrics.ascent - font_metrics.descent),
1401 ))
1402}
1403
1404pub fn font_metrics(
1405 font_ctx: &mut parley::FontContext,
1406 font_request: FontRequest,
1407) -> crate::items::FontMetrics {
1408 let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
1409
1410 let Some(font) =
1411 font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
1412 else {
1413 return crate::items::FontMetrics::default();
1414 };
1415
1416 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1417 let location = face.axes().location(font.synthesis.variation_settings());
1418 let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
1419
1420 let units_per_em = metrics.units_per_em as f32;
1421
1422 crate::items::FontMetrics {
1423 ascent: metrics.ascent * logical_pixel_size / units_per_em,
1424 descent: metrics.descent * logical_pixel_size / units_per_em,
1425 x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1426 cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1427 }
1428}
1429
1430pub fn text_input_byte_offset_for_position(
1431 renderer: &dyn RendererSealed,
1432 text_input: Pin<&crate::items::TextInput>,
1433 item_rc: &crate::item_tree::ItemRc,
1434 pos: LogicalPoint,
1435) -> usize {
1436 let Some(scale_factor) = renderer.scale_factor() else {
1437 return 0;
1438 };
1439 let pos: PhysicalPoint = pos * scale_factor;
1440
1441 let width = text_input.width();
1442 let height = text_input.height();
1443 if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
1444 return 0;
1445 }
1446
1447 let Some(ctx) = renderer.slint_context() else {
1448 return 0;
1449 };
1450 let mut font_ctx = ctx.font_context().borrow_mut();
1451
1452 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1453 Some(text_input.font_request(item_rc)),
1454 text_input.wrap(),
1455 None,
1456 scale_factor,
1457 );
1458
1459 let text = text_input.text();
1460 let paragraphs_without_linebreaks = create_text_paragraphs(
1461 &layout_builder,
1462 &mut font_ctx,
1463 PlainOrStyledText::Plain(text),
1464 None,
1465 Color::default(),
1466 );
1467
1468 let layout = layout(
1469 &layout_builder,
1470 &mut font_ctx,
1471 paragraphs_without_linebreaks,
1472 scale_factor,
1473 text_input.wrap(),
1474 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1475 );
1476 let byte_offset = layout.byte_offset_from_point(pos);
1477 let visual_representation = text_input.visual_representation(None);
1478 visual_representation.map_byte_offset_from_byte_offset_in_visual_text(byte_offset)
1479}
1480
1481pub fn text_input_cursor_rect_for_byte_offset(
1482 renderer: &dyn RendererSealed,
1483 text_input: Pin<&crate::items::TextInput>,
1484 item_rc: &crate::item_tree::ItemRc,
1485 byte_offset: usize,
1486) -> LogicalRect {
1487 let Some(scale_factor) = renderer.scale_factor() else {
1488 return LogicalRect::default();
1489 };
1490
1491 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1492 Some(text_input.font_request(item_rc)),
1493 text_input.wrap(),
1494 None,
1495 scale_factor,
1496 );
1497
1498 let width = text_input.width();
1499 let height = text_input.height();
1500 if width.get() <= 0. || height.get() <= 0. {
1501 return LogicalRect::new(
1502 LogicalPoint::default(),
1503 LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
1504 );
1505 }
1506
1507 let Some(ctx) = renderer.slint_context() else {
1508 return LogicalRect::default();
1509 };
1510 let mut font_ctx = ctx.font_context().borrow_mut();
1511
1512 let text = text_input.text();
1513 let paragraphs_without_linebreaks = create_text_paragraphs(
1514 &layout_builder,
1515 &mut font_ctx,
1516 PlainOrStyledText::Plain(text),
1517 None,
1518 Color::default(),
1519 );
1520
1521 let layout = layout(
1522 &layout_builder,
1523 &mut font_ctx,
1524 paragraphs_without_linebreaks,
1525 scale_factor,
1526 text_input.wrap(),
1527 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1528 );
1529 let cursor_rect = layout
1530 .cursor_rect_for_byte_offset(byte_offset, text_input.text_cursor_width() * scale_factor);
1531 cursor_rect / scale_factor
1532}