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