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
169#[derive(Default)]
170struct LayoutOptions {
171 max_width: Option<LogicalLength>,
172 max_height: Option<LogicalLength>,
173 horizontal_align: TextHorizontalAlignment,
174 vertical_align: TextVerticalAlignment,
175 text_overflow: TextOverflow,
176}
177
178impl LayoutOptions {
179 fn new_from_textinput(
180 text_input: Pin<&crate::items::TextInput>,
181 max_width: Option<LogicalLength>,
182 max_height: Option<LogicalLength>,
183 ) -> Self {
184 Self {
185 max_width,
186 max_height,
187 horizontal_align: text_input.horizontal_alignment(),
188 vertical_align: text_input.vertical_alignment(),
189 text_overflow: TextOverflow::Clip,
190 }
191 }
192}
193
194struct LayoutWithoutLineBreaksBuilder {
195 font_request: Option<FontRequest>,
196 text_wrap: TextWrap,
197 stroke: Option<TextStrokeStyle>,
198 scale_factor: ScaleFactor,
199 pixel_size: LogicalLength,
200}
201
202impl LayoutWithoutLineBreaksBuilder {
203 fn new(
204 font_request: Option<FontRequest>,
205 text_wrap: TextWrap,
206 stroke: Option<TextStrokeStyle>,
207 scale_factor: ScaleFactor,
208 ) -> Self {
209 let pixel_size = font_request
210 .as_ref()
211 .and_then(|font_request| font_request.pixel_size)
212 .unwrap_or(DEFAULT_FONT_SIZE);
213
214 Self { font_request, text_wrap, stroke, scale_factor, pixel_size }
215 }
216
217 fn ranged_builder<'a>(
218 &self,
219 layout_ctx: &'a mut parley::LayoutContext<Brush>,
220 font_ctx: &'a mut parley::FontContext,
221 text: &'a str,
222 ) -> parley::RangedBuilder<'a, Brush> {
223 let line_height_ratio = self.font_request.as_ref().and_then(|font_request| {
227 let font = font_request
228 .clone()
229 .query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
230 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).ok()?;
231 let location = face.axes().location(font.synthesis.variation_settings());
232 let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
233 let units_per_em = metrics.units_per_em as f32;
234 (units_per_em > 0.0)
235 .then(|| (metrics.ascent - metrics.descent + metrics.leading) / units_per_em)
236 });
237
238 let mut builder = layout_ctx.ranged_builder(font_ctx, text, self.scale_factor.get(), false);
239
240 if let Some(ratio) = line_height_ratio {
241 builder.push_default(parley::StyleProperty::LineHeight(
242 parley::style::LineHeight::FontSizeRelative(ratio),
243 ));
244 }
245
246 if let Some(ref font_request) = self.font_request {
247 let mut fallback_family_iter = sharedfontique::FALLBACK_FAMILIES
248 .into_iter()
249 .map(parley::style::FontFamilyName::Generic);
250
251 let font_families: &[parley::style::FontFamilyName] = if let Some(family) =
252 &font_request.family
253 {
254 let mut iter =
255 core::iter::once(parley::style::FontFamilyName::named(family.as_str()))
256 .chain(fallback_family_iter);
257 &core::array::from_fn::<
258 _,
259 { sharedfontique::FALLBACK_FAMILIES.as_slice().len() + 1 },
260 _,
261 >(|_| iter.next().unwrap())
262 } else {
263 &core::array::from_fn::<_, { sharedfontique::FALLBACK_FAMILIES.as_slice().len() }, _>(
264 |_| fallback_family_iter.next().unwrap(),
265 )
266 };
267
268 builder.push_default(parley::style::FontFamily::List(std::borrow::Cow::Borrowed(
269 font_families,
270 )));
271
272 if let Some(weight) = font_request.weight {
273 builder.push_default(parley::StyleProperty::FontWeight(
274 parley::style::FontWeight::new(weight as f32),
275 ));
276 }
277 if let Some(letter_spacing) = font_request.letter_spacing {
278 builder.push_default(parley::StyleProperty::LetterSpacing(letter_spacing.get()));
279 }
280 builder.push_default(parley::StyleProperty::FontStyle(if font_request.italic {
281 parley::style::FontStyle::Italic
282 } else {
283 parley::style::FontStyle::Normal
284 }));
285 }
286 builder.push_default(parley::StyleProperty::FontSize(self.pixel_size.get()));
287 builder.push_default(parley::StyleProperty::WordBreak(match self.text_wrap {
288 TextWrap::NoWrap => parley::style::WordBreak::KeepAll,
289 TextWrap::WordWrap => parley::style::WordBreak::Normal,
290 TextWrap::CharWrap => parley::style::WordBreak::BreakAll,
291 }));
292 builder.push_default(parley::StyleProperty::OverflowWrap(match self.text_wrap {
293 TextWrap::NoWrap => parley::style::OverflowWrap::Normal,
294 TextWrap::WordWrap | TextWrap::CharWrap => parley::style::OverflowWrap::Anywhere,
295 }));
296 if self.text_wrap == TextWrap::NoWrap {
297 builder.push_default(parley::StyleProperty::TextWrapMode(
302 parley::style::TextWrapMode::NoWrap,
303 ));
304 }
305
306 builder.push_default(parley::StyleProperty::Brush(Brush {
307 override_fill_color: None,
308 stroke: self.stroke,
309 link_color: None,
310 }));
311
312 builder
313 }
314
315 fn build(
316 &self,
317 font_context: &mut parley::FontContext,
318 text: &str,
319 selection: Option<(Range<usize>, Color)>,
320 formatting: impl IntoIterator<Item = i_slint_common::styled_text::FormattedSpan>,
321 link_color: Option<Color>,
322 ) -> parley::Layout<Brush> {
323 use i_slint_common::styled_text::Style;
324
325 LAYOUT_CONTEXT.with_borrow_mut(|layout_ctx| {
326 let mut builder = self.ranged_builder(layout_ctx, font_context, text);
327
328 if let Some((selection_range, selection_color)) = selection {
329 {
330 builder.push(
331 parley::StyleProperty::Brush(Brush {
332 override_fill_color: Some(selection_color),
333 stroke: self.stroke,
334 link_color: None,
335 }),
336 selection_range,
337 );
338 }
339 }
340
341 for span in formatting.into_iter().filter(|s| !s.range.is_empty()) {
343 match span.style {
344 Style::Emphasis => {
345 builder.push(
346 parley::StyleProperty::FontStyle(parley::style::FontStyle::Italic),
347 span.range,
348 );
349 }
350 Style::Strikethrough => {
351 builder.push(parley::StyleProperty::Strikethrough(true), span.range);
352 }
353 Style::Strong => {
354 builder.push(
355 parley::StyleProperty::FontWeight(parley::style::FontWeight::BOLD),
356 span.range,
357 );
358 }
359 Style::Code => {
360 builder.push(
361 parley::StyleProperty::FontFamily(parley::style::FontFamily::Single(
362 parley::style::FontFamilyName::Generic(
363 parley::style::GenericFamily::Monospace,
364 ),
365 )),
366 span.range,
367 );
368 }
369 Style::Underline => {
370 builder.push(parley::StyleProperty::Underline(true), span.range);
371 }
372 Style::Link => {
373 builder.push(parley::StyleProperty::Underline(true), span.range.clone());
374 builder.push(
375 parley::StyleProperty::Brush(Brush {
376 override_fill_color: None,
377 stroke: self.stroke,
378 link_color,
379 }),
380 span.range,
381 );
382 }
383 Style::Color(color) => {
384 builder.push(
385 parley::StyleProperty::Brush(Brush {
386 override_fill_color: Some(crate::Color::from_argb_encoded(color)),
387 stroke: self.stroke,
388 link_color: None,
389 }),
390 span.range,
391 );
392 }
393 }
394 }
395
396 builder.build(text)
397 })
398 }
399}
400
401fn paragraph_ranges(text: &str) -> impl Iterator<Item = Range<usize>> + '_ {
405 let mut start = 0;
406 text.split('\n').map(move |paragraph| {
407 let end = start + paragraph.len();
408 let range = if paragraph.ends_with('\r') { start..end - 1 } else { start..end };
409 start = end + 1;
410 range
411 })
412}
413
414fn create_text_paragraphs(
415 layout_builder: &LayoutWithoutLineBreaksBuilder,
416 font_context: &mut parley::FontContext,
417 text: PlainOrStyledText,
418 selection: Option<(Range<usize>, Color)>,
419 link_color: Color,
420) -> Vec<TextParagraph> {
421 let paragraph_from_text =
422 |font_context: &mut parley::FontContext,
423 text: &str,
424 range: std::ops::Range<usize>,
425 formatting: Vec<i_slint_common::styled_text::FormattedSpan>,
426 links: Vec<(std::ops::Range<usize>, std::string::String)>| {
427 let selection = selection.clone().and_then(|(selection, selection_color)| {
428 let sel_start = selection.start.max(range.start);
429 let sel_end = selection.end.min(range.end);
430
431 if sel_start < sel_end {
432 let local_selection = (sel_start - range.start)..(sel_end - range.start);
433 Some((local_selection, selection_color))
434 } else {
435 None
436 }
437 });
438
439 let layout =
440 layout_builder.build(font_context, text, selection, formatting, Some(link_color));
441
442 TextParagraph { range, y: PhysicalLength::default(), layout, links }
443 };
444
445 let mut paragraphs = Vec::with_capacity(1);
446
447 match text {
448 PlainOrStyledText::Plain(ref text) => {
449 for range in paragraph_ranges(text) {
450 paragraphs.push(paragraph_from_text(
451 font_context,
452 &text[range.clone()],
453 range,
454 Default::default(),
455 Default::default(),
456 ));
457 }
458 }
459 PlainOrStyledText::Styled(rich_text) => {
460 for paragraph in rich_text.paragraphs {
461 paragraphs.push(paragraph_from_text(
462 font_context,
463 ¶graph.text,
464 0..0,
465 paragraph.formatting,
466 paragraph.links,
467 ));
468 }
469 }
470 };
471
472 paragraphs
473}
474
475fn layout(
480 layout_builder: &LayoutWithoutLineBreaksBuilder,
481 font_context: &mut parley::FontContext,
482 mut paragraphs: Vec<TextParagraph>,
483 scale_factor: ScaleFactor,
484 options: LayoutOptions,
485) -> Layout {
486 let max_physical_width = options.max_width.map(|max_width| max_width * scale_factor);
487 let max_physical_height = options.max_height.map(|max_height| max_height * scale_factor);
488
489 let get_ellipsis_glyph = |font_context: &mut parley::FontContext| {
491 let mut layout = layout_builder.build(font_context, "…", None, None, None);
492 layout.break_all_lines(None);
493 let line = layout.lines().next()?;
494 let item = line.items().next()?;
495 let run = match item {
496 parley::layout::PositionedLayoutItem::GlyphRun(run) => Some(run),
497 _ => return None,
498 }?;
499 let glyph = run.positioned_glyphs().next()?;
500 Some((glyph, run.run().font().clone()))
501 };
502
503 let elision_info = if let (TextOverflow::Elide, Some(max_physical_width)) =
504 (options.text_overflow, max_physical_width)
505 {
506 get_ellipsis_glyph(font_context).map(|(ellipsis_glyph, font_for_ellipsis_glyph)| {
507 ElisionInfo { ellipsis_glyph, font_for_ellipsis_glyph, max_physical_width }
508 })
509 } else {
510 None
511 };
512
513 let mut para_y = 0.0;
514 for para in paragraphs.iter_mut() {
515 para.layout.break_all_lines(max_physical_width.map(|width| width.get()));
516 para.layout.align(
517 match options.horizontal_align {
518 TextHorizontalAlignment::Start | TextHorizontalAlignment::Left => {
519 parley::Alignment::Left
520 }
521 TextHorizontalAlignment::Center => parley::Alignment::Center,
522 TextHorizontalAlignment::End | TextHorizontalAlignment::Right => {
523 parley::Alignment::Right
524 }
525 },
526 parley::AlignmentOptions::default(),
527 );
528
529 para.y = PhysicalLength::new(para_y);
530 para_y += para.layout.height();
531 }
532
533 let max_width = paragraphs
534 .iter()
535 .map(|p| {
536 PhysicalLength::new(p.layout.full_width())
541 })
542 .fold(PhysicalLength::zero(), PhysicalLength::max);
543 let height = paragraphs
544 .last()
545 .map_or(PhysicalLength::zero(), |p| p.y + PhysicalLength::new(p.layout.height()));
546
547 let y_offset = match (max_physical_height, options.vertical_align) {
548 (Some(max_height), TextVerticalAlignment::Center) => (max_height - height) / 2.0,
549 (Some(max_height), TextVerticalAlignment::Bottom) => max_height - height,
550 (None, _) | (Some(_), TextVerticalAlignment::Top) => PhysicalLength::new(0.0),
551 };
552
553 Layout { paragraphs, y_offset, elision_info, max_width, height, max_physical_height }
554}
555
556struct CachedParagraphsGuard<'a> {
558 paragraphs: Option<Vec<TextParagraph>>,
559 container: Option<std::cell::RefMut<'a, Vec<TextParagraph>>>,
560}
561
562impl Drop for CachedParagraphsGuard<'_> {
563 fn drop(&mut self) {
564 if let (Some(paragraphs), Some(container)) = (self.paragraphs.take(), &mut self.container) {
565 **container = paragraphs;
566 }
567 }
568}
569
570fn shape_paragraphs(
571 text: Pin<&dyn crate::item_rendering::RenderText>,
572 item_rc: Option<&crate::item_tree::ItemRc>,
573 scale_factor: ScaleFactor,
574 font_context: &mut parley::FontContext,
575) -> Vec<TextParagraph> {
576 let (stroke_brush, _, stroke_style) = text.stroke();
577 let has_stroke = !stroke_brush.is_transparent();
578 let builder = LayoutWithoutLineBreaksBuilder::new(
579 item_rc.map(|irc| text.font_request(irc)),
580 text.wrap(),
581 has_stroke.then_some(stroke_style),
582 scale_factor,
583 );
584 create_text_paragraphs(&builder, font_context, text.text(), None, text.link_color())
585}
586
587fn get_or_create_text_paragraphs<'a>(
588 cache: Option<&'a TextLayoutCache>,
589 item_rc: Option<&crate::item_tree::ItemRc>,
590 text: Pin<&dyn crate::item_rendering::RenderText>,
591 scale_factor: ScaleFactor,
592 font_context: &mut parley::FontContext,
593) -> CachedParagraphsGuard<'a> {
594 if let (Some(cache), Some(item_rc)) = (cache, item_rc) {
595 let mut entry = cache.inner.get_or_update_cache_entry_ref(item_rc, || {
596 #[cfg(feature = "testing")]
597 cache.cache_miss_count.set(cache.cache_miss_count.get() + 1);
598 shape_paragraphs(text, Some(item_rc), scale_factor, font_context)
599 });
600 let paragraphs = std::mem::take(&mut *entry);
601 CachedParagraphsGuard { paragraphs: Some(paragraphs), container: Some(entry) }
602 } else {
603 CachedParagraphsGuard {
604 paragraphs: Some(shape_paragraphs(text, item_rc, scale_factor, font_context)),
605 container: None,
606 }
607 }
608}
609
610struct ElisionInfo {
611 ellipsis_glyph: parley::layout::Glyph,
612 font_for_ellipsis_glyph: parley::FontData,
613 max_physical_width: PhysicalLength,
614}
615
616fn line_fits_height(block_max_coord: f32, max_physical_height: PhysicalLength) -> bool {
619 max_physical_height.get().ceil() >= block_max_coord
620}
621
622struct TextParagraph {
623 range: Range<usize>,
624 y: PhysicalLength,
625 layout: parley::Layout<Brush>,
626 links: std::vec::Vec<(Range<usize>, std::string::String)>,
627}
628
629impl TextParagraph {
630 fn draw<R: GlyphRenderer>(
631 &self,
632 layout: &Layout,
633 paragraph_index: usize,
634 elision_extent: Option<ElisionCut>,
635 item_renderer: &mut R,
636 default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
637 default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
638 draw_glyphs: &mut dyn FnMut(
639 &mut R,
640 &parley::FontData,
641 PhysicalLength,
642 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
645 PhysicalLength, &mut dyn Iterator<Item = parley::layout::Glyph>,
647 ),
648 ) {
649 let para_y = layout.y_offset + self.y;
650
651 let line_count = self.layout.lines().len();
652
653 let (last_drawn, vertical_truncation) = match elision_extent {
660 Some(cut) if paragraph_index > cut.last_paragraph => return,
662 Some(cut) if paragraph_index == cut.last_paragraph => {
664 (cut.last_line, cut.needs_ellipsis)
665 }
666 _ => (line_count.saturating_sub(1), false),
669 };
670
671 for (index, line) in self.layout.lines().enumerate() {
672 if index > last_drawn {
674 break;
675 }
676 let metrics = line.metrics();
677 let last_line = index == last_drawn;
681 if !last_line
682 && !layout.paragraph_line_within_box(
683 self,
684 metrics.block_min_coord,
685 metrics.block_max_coord,
686 )
687 {
688 continue;
689 }
690 let vertically_truncated = last_line && vertical_truncation;
693 for item in line.items() {
694 match item {
695 parley::PositionedLayoutItem::GlyphRun(glyph_run) => {
696 let ellipsis = if last_line {
697 let (truncated_glyphs, ellipsis) = layout.glyphs_with_elision(
698 &glyph_run,
699 vertically_truncated,
700 metrics.trailing_whitespace,
701 );
702
703 Self::draw_glyph_run(
704 &glyph_run,
705 item_renderer,
706 default_fill_brush,
707 default_stroke_brush,
708 para_y,
709 &mut truncated_glyphs.into_iter(),
710 draw_glyphs,
711 );
712 ellipsis
713 } else {
714 Self::draw_glyph_run(
715 &glyph_run,
716 item_renderer,
717 default_fill_brush,
718 default_stroke_brush,
719 para_y,
720 &mut glyph_run.positioned_glyphs(),
721 draw_glyphs,
722 );
723 None
724 };
725
726 if let Some((ellipsis_glyph, ellipsis_font, font_size)) = ellipsis {
727 let run = glyph_run.run();
728 draw_glyphs(
729 item_renderer,
730 &ellipsis_font,
731 font_size,
732 run.normalized_coords(),
733 &run.synthesis(),
734 default_fill_brush.clone(),
735 para_y,
736 &mut core::iter::once(ellipsis_glyph),
737 );
738 }
739 }
740 parley::PositionedLayoutItem::InlineBox(_inline_box) => {}
741 };
742 }
743 }
744 }
745
746 fn draw_glyph_run<R: GlyphRenderer>(
747 glyph_run: &parley::layout::GlyphRun<Brush>,
748 item_renderer: &mut R,
749 default_fill_brush: &<R as GlyphRenderer>::PlatformBrush,
750 default_stroke_brush: &Option<<R as GlyphRenderer>::PlatformBrush>,
751 para_y: PhysicalLength,
752 glyphs_it: &mut dyn Iterator<Item = parley::layout::Glyph>,
753 draw_glyphs: &mut dyn FnMut(
754 &mut R,
755 &parley::FontData,
756 PhysicalLength,
757 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
760 PhysicalLength,
761 &mut dyn Iterator<Item = parley::layout::Glyph>,
762 ),
763 ) {
764 let run = glyph_run.run();
765 let normalized_coords = run.normalized_coords();
766 let synthesis = run.synthesis();
767 let brush = &glyph_run.style().brush;
768
769 let (fill_brush, stroke_style) = match (brush.override_fill_color, brush.link_color) {
770 (Some(color), _) => {
771 let Some(selection_brush) = item_renderer.platform_brush_for_color(&color) else {
772 return;
773 };
774 (selection_brush.clone(), &None)
775 }
776 (None, Some(color)) => {
777 let Some(link_brush) = item_renderer.platform_brush_for_color(&color) else {
778 return;
779 };
780 (link_brush.clone(), &None)
781 }
782 (None, None) => (default_fill_brush.clone(), &brush.stroke),
783 };
784
785 match stroke_style {
786 Some(TextStrokeStyle::Outside) => {
787 let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
788
789 if let Some(stroke_brush) = default_stroke_brush.clone() {
790 draw_glyphs(
791 item_renderer,
792 run.font(),
793 PhysicalLength::new(run.font_size()),
794 normalized_coords,
795 &synthesis,
796 stroke_brush,
797 para_y,
798 &mut glyphs.iter().cloned(),
799 );
800 }
801
802 draw_glyphs(
803 item_renderer,
804 run.font(),
805 PhysicalLength::new(run.font_size()),
806 normalized_coords,
807 &synthesis,
808 fill_brush.clone(),
809 para_y,
810 &mut glyphs.into_iter(),
811 );
812 }
813 Some(TextStrokeStyle::Center) => {
814 let glyphs = glyphs_it.collect::<alloc::vec::Vec<_>>();
815
816 draw_glyphs(
817 item_renderer,
818 run.font(),
819 PhysicalLength::new(run.font_size()),
820 normalized_coords,
821 &synthesis,
822 fill_brush.clone(),
823 para_y,
824 &mut glyphs.iter().cloned(),
825 );
826
827 if let Some(stroke_brush) = default_stroke_brush.clone() {
828 draw_glyphs(
829 item_renderer,
830 run.font(),
831 PhysicalLength::new(run.font_size()),
832 normalized_coords,
833 &synthesis,
834 stroke_brush,
835 para_y,
836 &mut glyphs.into_iter(),
837 );
838 }
839 }
840 None => {
841 draw_glyphs(
842 item_renderer,
843 run.font(),
844 PhysicalLength::new(run.font_size()),
845 normalized_coords,
846 &synthesis,
847 fill_brush.clone(),
848 para_y,
849 glyphs_it,
850 );
851 }
852 }
853
854 let metrics = run.metrics();
855
856 if glyph_run.style().underline.is_some() {
857 item_renderer.fill_rectangle(
858 PhysicalRect::new(
859 PhysicalPoint::from_lengths(
860 PhysicalLength::new(glyph_run.offset()),
861 para_y
862 + PhysicalLength::new(glyph_run.baseline() - metrics.underline_offset),
863 ),
864 PhysicalSize::new(glyph_run.advance(), metrics.underline_size),
865 ),
866 fill_brush.clone(),
867 );
868 }
869
870 if glyph_run.style().strikethrough.is_some() {
871 item_renderer.fill_rectangle(
872 PhysicalRect::new(
873 PhysicalPoint::from_lengths(
874 PhysicalLength::new(glyph_run.offset()),
875 para_y
876 + PhysicalLength::new(
877 glyph_run.baseline() - metrics.strikethrough_offset,
878 ),
879 ),
880 PhysicalSize::new(glyph_run.advance(), metrics.strikethrough_size),
881 ),
882 fill_brush,
883 );
884 }
885 }
886}
887
888#[derive(Clone, Copy)]
891struct ElisionCut {
892 last_paragraph: usize,
894 last_line: usize,
896 needs_ellipsis: bool,
898}
899
900struct Layout {
901 paragraphs: Vec<TextParagraph>,
902 y_offset: PhysicalLength,
903 max_width: PhysicalLength,
904 height: PhysicalLength,
905 max_physical_height: Option<PhysicalLength>,
906 elision_info: Option<ElisionInfo>,
907}
908
909impl Layout {
910 fn first_line_exceeds_height(&self) -> bool {
915 let Some(max_physical_height) = self.max_physical_height else {
916 return false;
917 };
918 self.paragraphs.first().and_then(|paragraph| paragraph.layout.lines().next()).is_some_and(
919 |line| !line_fits_height(line.metrics().block_max_coord, max_physical_height),
920 )
921 }
922
923 fn paragraph_line_within_box(
928 &self,
929 paragraph: &TextParagraph,
930 block_min: f32,
931 block_max: f32,
932 ) -> bool {
933 match self.max_physical_height {
934 Some(max_physical_height) if self.elision_info.is_some() => {
935 let para_y = self.y_offset + paragraph.y;
936 line_fits_height(para_y.get() + block_max, max_physical_height)
939 && para_y.get() + block_min >= -0.5
940 }
941 _ => true,
942 }
943 }
944
945 fn elision_extent(&self) -> Option<ElisionCut> {
952 self.max_physical_height?;
953 self.elision_info.as_ref()?;
954
955 let last_within_box = self.paragraphs.iter().enumerate().rev().find_map(|(pi, para)| {
959 para.layout
960 .lines()
961 .enumerate()
962 .rev()
963 .find(|(_, line)| {
964 let m = line.metrics();
965 self.paragraph_line_within_box(para, m.block_min_coord, m.block_max_coord)
966 })
967 .map(|(li, _)| (pi, li))
968 });
969
970 let final_line = self
973 .paragraphs
974 .iter()
975 .enumerate()
976 .rev()
977 .find_map(|(pi, para)| para.layout.lines().len().checked_sub(1).map(|li| (pi, li)));
978
979 let (last_paragraph, last_line) = last_within_box.unwrap_or((0, 0));
980 let needs_ellipsis =
981 final_line.is_some_and(|final_line| final_line != (last_paragraph, last_line));
982 Some(ElisionCut { last_paragraph, last_line, needs_ellipsis })
983 }
984
985 fn paragraph_by_byte_offset(&self, byte_offset: usize) -> Option<&TextParagraph> {
989 self.paragraphs.iter().take_while(|p| p.range.start <= byte_offset).last()
990 }
991
992 fn paragraph_by_y(&self, y: PhysicalLength) -> Option<&TextParagraph> {
993 let y = y - self.y_offset;
995
996 if y < PhysicalLength::zero() {
997 return self.paragraphs.first();
998 }
999
1000 let idx = self.paragraphs.binary_search_by(|paragraph| {
1001 if y < paragraph.y {
1002 core::cmp::Ordering::Greater
1003 } else if y >= paragraph.y + PhysicalLength::new(paragraph.layout.height()) {
1004 core::cmp::Ordering::Less
1005 } else {
1006 core::cmp::Ordering::Equal
1007 }
1008 });
1009
1010 match idx {
1011 Ok(i) => self.paragraphs.get(i),
1012 Err(_) => self.paragraphs.last(),
1013 }
1014 }
1015
1016 fn selection_geometry(
1017 &self,
1018 selection_range: Range<usize>,
1019 mut callback: impl FnMut(PhysicalRect),
1020 ) {
1021 for paragraph in &self.paragraphs {
1022 let selection_start = selection_range.start.max(paragraph.range.start);
1023 let selection_end = selection_range.end.min(paragraph.range.end);
1024
1025 if selection_start < selection_end {
1026 let local_start = selection_start - paragraph.range.start;
1027 let local_end = selection_end - paragraph.range.start;
1028
1029 let selection = parley::editing::Selection::new(
1030 parley::editing::Cursor::from_byte_index(
1031 ¶graph.layout,
1032 local_start,
1033 Default::default(),
1034 ),
1035 parley::editing::Cursor::from_byte_index(
1036 ¶graph.layout,
1037 local_end,
1038 Default::default(),
1039 ),
1040 );
1041
1042 selection.geometry_with(¶graph.layout, |rect, _| {
1043 callback(PhysicalRect::new(
1044 PhysicalPoint::from_lengths(
1045 PhysicalLength::new(rect.x0 as _),
1046 PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
1047 ),
1048 PhysicalSize::new(rect.width() as _, rect.height() as _),
1049 ));
1050 });
1051 }
1052 }
1053 }
1054
1055 fn byte_offset_from_point(&self, pos: PhysicalPoint) -> usize {
1056 let Some(paragraph) = self.paragraph_by_y(pos.y_length()) else {
1057 return 0;
1058 };
1059 let cursor = parley::editing::Cursor::from_point(
1060 ¶graph.layout,
1061 pos.x,
1062 (pos.y_length() - self.y_offset - paragraph.y).get(),
1063 );
1064 paragraph.range.start + cursor.index()
1065 }
1066
1067 fn cursor_rect_for_byte_offset(
1068 &self,
1069 byte_offset: usize,
1070 cursor_width: PhysicalLength,
1071 ) -> PhysicalRect {
1072 let Some(paragraph) = self.paragraph_by_byte_offset(byte_offset) else {
1073 return PhysicalRect::new(PhysicalPoint::default(), PhysicalSize::new(1.0, 1.0));
1074 };
1075
1076 let local_offset = (byte_offset - paragraph.range.start).min(paragraph.range.len());
1077 let cursor = parley::editing::Cursor::from_byte_index(
1078 ¶graph.layout,
1079 local_offset,
1080 Default::default(),
1081 );
1082 let rect = cursor.geometry(¶graph.layout, cursor_width.get());
1083
1084 PhysicalRect::new(
1085 PhysicalPoint::from_lengths(
1086 PhysicalLength::new(rect.x0 as _),
1087 PhysicalLength::new(rect.y0 as _) + self.y_offset + paragraph.y,
1088 ),
1089 PhysicalSize::new(rect.width() as _, rect.height() as _),
1090 )
1091 }
1092
1093 fn glyphs_with_elision<'a>(
1097 &'a self,
1098 glyph_run: &'a parley::layout::GlyphRun<Brush>,
1099 force_elision: bool,
1102 trailing_whitespace: f32,
1106 ) -> (
1107 impl Iterator<Item = parley::layout::Glyph> + Clone + 'a,
1108 Option<(parley::layout::Glyph, parley::FontData, PhysicalLength)>,
1109 ) {
1110 let ellipsis_advance =
1111 self.elision_info.as_ref().map(|info| info.ellipsis_glyph.advance).unwrap_or(0.0);
1112 let max_width = self
1113 .elision_info
1114 .as_ref()
1115 .map(|info| info.max_physical_width)
1116 .unwrap_or(PhysicalLength::new(f32::MAX));
1117
1118 let run_start = PhysicalLength::new(glyph_run.offset());
1119 let run_end = PhysicalLength::new(glyph_run.offset() + glyph_run.advance());
1120
1121 let run_beyond_elision = run_start > max_width;
1123 let needs_elision = !run_beyond_elision
1125 && (force_elision || run_end.get().floor() > max_width.get().ceil());
1126
1127 let truncated_glyphs = glyph_run.positioned_glyphs().take_while(move |glyph| {
1128 !run_beyond_elision
1129 && (!needs_elision
1130 || PhysicalLength::new(glyph.x + glyph.advance + ellipsis_advance) <= max_width)
1131 });
1132
1133 let ellipsis = if needs_elision {
1134 self.elision_info.as_ref().map(|info| {
1135 let ellipsis_x = glyph_run
1136 .positioned_glyphs()
1137 .find(|glyph| {
1138 PhysicalLength::new(glyph.x + glyph.advance + info.ellipsis_glyph.advance)
1139 > info.max_physical_width
1140 })
1141 .map(|g| g.x)
1142 .unwrap_or(run_end.get() - trailing_whitespace);
1145
1146 let mut ellipsis_glyph = info.ellipsis_glyph;
1147 ellipsis_glyph.x = ellipsis_x;
1148 ellipsis_glyph.y = glyph_run.baseline();
1151
1152 let font_size = PhysicalLength::new(glyph_run.run().font_size());
1153 (ellipsis_glyph, info.font_for_ellipsis_glyph.clone(), font_size)
1154 })
1155 } else {
1156 None
1157 };
1158
1159 (truncated_glyphs, ellipsis)
1160 }
1161
1162 fn draw<R: GlyphRenderer>(
1163 &self,
1164 item_renderer: &mut R,
1165 default_fill_brush: <R as GlyphRenderer>::PlatformBrush,
1166 default_stroke_brush: Option<<R as GlyphRenderer>::PlatformBrush>,
1167 draw_glyphs: &mut dyn FnMut(
1168 &mut R,
1169 &parley::FontData,
1170 PhysicalLength,
1171 &[i16], &fontique::Synthesis, <R as GlyphRenderer>::PlatformBrush,
1174 PhysicalLength, &mut dyn Iterator<Item = parley::layout::Glyph>,
1176 ),
1177 ) {
1178 let elision_extent = self.elision_extent();
1181 for (paragraph_index, paragraph) in self.paragraphs.iter().enumerate() {
1182 paragraph.draw(
1183 self,
1184 paragraph_index,
1185 elision_extent,
1186 item_renderer,
1187 &default_fill_brush,
1188 &default_stroke_brush,
1189 draw_glyphs,
1190 );
1191 }
1192 }
1193}
1194
1195pub fn draw_text(
1196 item_renderer: &mut impl GlyphRenderer,
1197 text: Pin<&dyn crate::item_rendering::RenderText>,
1198 item_rc: Option<&crate::item_tree::ItemRc>,
1199 size: LogicalSize,
1200 cache: Option<&TextLayoutCache>,
1201) {
1202 let max_width = size.width_length();
1203 let max_height = size.height_length();
1204
1205 if max_width.get() <= 0. || max_height.get() <= 0. {
1206 return;
1207 }
1208
1209 let Some(platform_fill_brush) = item_renderer.platform_text_fill_brush(text.color(), size)
1210 else {
1211 return;
1213 };
1214
1215 let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1216
1217 let (stroke_brush, stroke_width, stroke_style) = text.stroke();
1218 let platform_stroke_brush = if !stroke_brush.is_transparent() {
1219 let stroke_width = if stroke_width.get() != 0.0 {
1220 (stroke_width * scale_factor).get()
1221 } else {
1222 1.0
1224 };
1225 let stroke_width = match stroke_style {
1226 TextStrokeStyle::Outside => stroke_width * 2.0,
1227 TextStrokeStyle::Center => stroke_width,
1228 };
1229 item_renderer.platform_text_stroke_brush(stroke_brush, stroke_width, size)
1230 } else {
1231 None
1232 };
1233
1234 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1236 item_rc.map(|item_rc| text.font_request(item_rc)),
1237 text.wrap(),
1238 platform_stroke_brush.is_some().then_some(stroke_style),
1239 scale_factor,
1240 );
1241
1242 let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1243
1244 let mut guard =
1245 get_or_create_text_paragraphs(cache, item_rc, text, scale_factor, &mut font_ctx);
1246
1247 let (horizontal_align, vertical_align) = text.alignment();
1248 let text_overflow = text.overflow();
1249
1250 let layout = layout(
1251 &layout_builder,
1252 &mut font_ctx,
1253 guard.paragraphs.take().unwrap_or_default(),
1254 scale_factor,
1255 LayoutOptions {
1256 horizontal_align,
1257 vertical_align,
1258 max_height: Some(max_height),
1259 max_width: Some(max_width),
1260 text_overflow: text.overflow(),
1261 },
1262 );
1263
1264 drop(font_ctx);
1265
1266 let clip_overflowing_first_line =
1271 text_overflow == TextOverflow::Elide && layout.first_line_exceeds_height();
1272
1273 let render = if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1274 item_renderer.save_state();
1275
1276 item_renderer.combine_clip(
1277 LogicalRect::new(LogicalPoint::default(), size),
1278 LogicalBorderRadius::zero(),
1279 LogicalLength::zero(),
1280 )
1281 } else {
1282 true
1283 };
1284
1285 if render {
1286 layout.draw(
1287 item_renderer,
1288 platform_fill_brush,
1289 platform_stroke_brush,
1290 &mut |item_renderer: &mut _,
1291 font,
1292 font_size,
1293 normalized_coords,
1294 synthesis,
1295 brush,
1296 y_offset,
1297 glyphs_it| {
1298 item_renderer.draw_glyph_run(
1299 font,
1300 font_size,
1301 normalized_coords,
1302 synthesis,
1303 brush,
1304 y_offset,
1305 glyphs_it,
1306 );
1307 },
1308 );
1309 }
1310
1311 if text_overflow == TextOverflow::Clip || clip_overflowing_first_line {
1312 item_renderer.restore_state();
1313 }
1314
1315 guard.paragraphs = Some(layout.paragraphs);
1318}
1319
1320#[cfg(feature = "std")]
1321pub fn link_under_cursor(
1322 font_context: &mut parley::FontContext,
1323 scale_factor: ScaleFactor,
1324 text: Pin<&dyn crate::item_rendering::RenderText>,
1325 item_rc: &crate::item_tree::ItemRc,
1326 size: LogicalSize,
1327 cursor: PhysicalPoint,
1328 cache: Option<&TextLayoutCache>,
1329) -> Option<std::string::String> {
1330 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1331 Some(text.font_request(item_rc)),
1332 text.wrap(),
1333 None,
1334 scale_factor,
1335 );
1336
1337 let mut guard =
1338 get_or_create_text_paragraphs(cache, Some(item_rc), text, scale_factor, font_context);
1339
1340 let (horizontal_align, vertical_align) = text.alignment();
1341
1342 let layout = layout(
1343 &layout_builder,
1344 font_context,
1345 guard.paragraphs.take().unwrap_or_default(),
1346 scale_factor,
1347 LayoutOptions {
1348 horizontal_align,
1349 vertical_align,
1350 max_height: Some(size.height_length()),
1351 max_width: Some(size.width_length()),
1352 text_overflow: text.overflow(),
1353 },
1354 );
1355
1356 let result = layout.paragraph_by_y(cursor.y_length()).and_then(|paragraph| {
1357 let paragraph_y: f64 = paragraph.y.cast::<f64>().get();
1358
1359 paragraph
1360 .links
1361 .iter()
1362 .find(|(range, _)| {
1363 let start = parley::editing::Cursor::from_byte_index(
1364 ¶graph.layout,
1365 range.start,
1366 Default::default(),
1367 );
1368 let end = parley::editing::Cursor::from_byte_index(
1369 ¶graph.layout,
1370 range.end,
1371 Default::default(),
1372 );
1373 let mut clicked = false;
1374 let link_range = parley::Selection::new(start, end);
1375 link_range.geometry_with(¶graph.layout, |mut bounding_box, _line| {
1376 bounding_box.y0 += paragraph_y;
1377 bounding_box.y1 += paragraph_y;
1378 clicked = bounding_box.union(parley::BoundingBox::new(
1379 cursor.x.into(),
1380 cursor.y.into(),
1381 cursor.x.into(),
1382 cursor.y.into(),
1383 )) == bounding_box;
1384 });
1385 clicked
1386 })
1387 .map(|(_, link)| link.clone())
1388 });
1389
1390 guard.paragraphs = Some(layout.paragraphs);
1392
1393 result
1394}
1395
1396pub fn draw_text_input(
1397 item_renderer: &mut impl GlyphRenderer,
1398 text_input: Pin<&crate::items::TextInput>,
1399 item_rc: &crate::item_tree::ItemRc,
1400 size: LogicalSize,
1401 password_character: Option<fn() -> char>,
1402) {
1403 let width = size.width_length();
1404 let height = size.height_length();
1405 if width.get() <= 0. || height.get() <= 0. {
1406 return;
1407 }
1408
1409 let visual_representation = text_input.visual_representation(password_character);
1410
1411 let Some(platform_fill_brush) =
1412 item_renderer.platform_text_fill_brush(visual_representation.text_color, size)
1413 else {
1414 return;
1415 };
1416
1417 let selection_range = if !visual_representation.preedit_range.is_empty() {
1418 visual_representation.preedit_range.start..visual_representation.preedit_range.end
1419 } else {
1420 visual_representation.selection_range.start..visual_representation.selection_range.end
1421 };
1422
1423 let scale_factor = ScaleFactor::new(item_renderer.scale_factor());
1424
1425 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1426 Some(text_input.font_request(item_rc)),
1427 text_input.wrap(),
1428 None,
1429 scale_factor,
1430 );
1431
1432 let text = visual_representation.text.clone();
1433
1434 let selection_and_color = if !selection_range.is_empty() {
1438 Some((selection_range.clone(), text_input.selection_foreground_color()))
1439 } else {
1440 None
1441 };
1442
1443 let mut font_ctx = item_renderer.window().context().font_context().borrow_mut();
1444
1445 let paragraphs_without_linebreaks = create_text_paragraphs(
1446 &layout_builder,
1447 &mut font_ctx,
1448 PlainOrStyledText::Plain(text),
1449 selection_and_color,
1450 Color::default(),
1451 );
1452
1453 let layout = layout(
1454 &layout_builder,
1455 &mut font_ctx,
1456 paragraphs_without_linebreaks,
1457 scale_factor,
1458 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1459 );
1460
1461 drop(font_ctx);
1462
1463 layout.selection_geometry(selection_range, |selection_rect| {
1464 item_renderer
1465 .fill_rectangle_with_color(selection_rect, text_input.selection_background_color());
1466 });
1467
1468 item_renderer.save_state();
1469
1470 let render = item_renderer.combine_clip(
1471 LogicalRect::new(LogicalPoint::default(), size),
1472 LogicalBorderRadius::zero(),
1473 LogicalLength::zero(),
1474 );
1475
1476 if render {
1477 layout.draw(
1478 item_renderer,
1479 platform_fill_brush,
1480 None,
1481 &mut |item_renderer: &mut _,
1482 font,
1483 font_size,
1484 normalized_coords,
1485 synthesis,
1486 brush,
1487 y_offset,
1488 glyphs_it| {
1489 item_renderer.draw_glyph_run(
1490 font,
1491 font_size,
1492 normalized_coords,
1493 synthesis,
1494 brush,
1495 y_offset,
1496 glyphs_it,
1497 );
1498 },
1499 );
1500
1501 if let Some(cursor_pos) = visual_representation.cursor_position {
1502 let cursor_rect = layout.cursor_rect_for_byte_offset(
1503 cursor_pos,
1504 text_input.text_cursor_width() * scale_factor,
1505 );
1506 item_renderer
1507 .fill_rectangle_with_color(cursor_rect, visual_representation.cursor_color);
1508 }
1509 }
1510
1511 item_renderer.restore_state();
1512}
1513
1514pub fn text_size(
1515 renderer: &dyn RendererSealed,
1516 text_item: Pin<&dyn crate::item_rendering::RenderString>,
1517 item_rc: &crate::item_tree::ItemRc,
1518 max_width: Option<LogicalLength>,
1519 text_wrap: TextWrap,
1520 _cache: Option<&TextLayoutCache>,
1521) -> Option<LogicalSize> {
1522 let scale_factor = renderer.scale_factor()?;
1523
1524 let font_request = text_item.font_request(item_rc);
1528 let text = text_item.text();
1529
1530 let ctx = renderer.slint_context()?;
1531 let mut font_ctx = ctx.font_context().borrow_mut();
1532
1533 let layout_builder =
1534 LayoutWithoutLineBreaksBuilder::new(Some(font_request), text_wrap, None, scale_factor);
1535
1536 let paragraphs_without_linebreaks =
1537 create_text_paragraphs(&layout_builder, &mut font_ctx, text, None, Color::default());
1538
1539 let layout = layout(
1540 &layout_builder,
1541 &mut font_ctx,
1542 paragraphs_without_linebreaks,
1543 scale_factor,
1544 LayoutOptions {
1545 max_width,
1546 max_height: None,
1547 horizontal_align: TextHorizontalAlignment::Left,
1548 vertical_align: TextVerticalAlignment::Top,
1549 text_overflow: TextOverflow::Clip,
1550 },
1551 );
1552 Some(PhysicalSize::from_lengths(layout.max_width, layout.height) / scale_factor)
1553}
1554
1555pub fn char_size(
1556 font_ctx: &mut parley::FontContext,
1557 text_item: Pin<&dyn crate::item_rendering::HasFont>,
1558 item_rc: &crate::item_tree::ItemRc,
1559 ch: char,
1560) -> Option<LogicalSize> {
1561 let font_request = text_item.font_request(item_rc);
1562 let font = font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)?;
1563
1564 let char_map = font.charmap()?;
1565
1566 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1567
1568 let glyph_index = char_map.map(ch)?;
1569
1570 let pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE);
1571
1572 let location = face.axes().location(font.synthesis.variation_settings());
1573
1574 let glyph_metrics = skrifa::metrics::GlyphMetrics::new(
1575 &face,
1576 skrifa::instance::Size::new(pixel_size.get()),
1577 &location,
1578 );
1579
1580 let advance_width = LogicalLength::new(glyph_metrics.advance_width(glyph_index.into())?);
1581
1582 let font_metrics = skrifa::metrics::Metrics::new(
1583 &face,
1584 skrifa::instance::Size::new(pixel_size.get()),
1585 &location,
1586 );
1587
1588 Some(LogicalSize::from_lengths(
1589 advance_width,
1590 LogicalLength::new(font_metrics.ascent - font_metrics.descent),
1591 ))
1592}
1593
1594pub fn font_metrics(
1595 font_ctx: &mut parley::FontContext,
1596 font_request: FontRequest,
1597) -> crate::items::FontMetrics {
1598 let logical_pixel_size = font_request.pixel_size.unwrap_or(DEFAULT_FONT_SIZE).get();
1599
1600 let Some(font) =
1601 font_request.query_fontique(&mut font_ctx.collection, &mut font_ctx.source_cache)
1602 else {
1603 return crate::items::FontMetrics::default();
1604 };
1605
1606 let face = skrifa::FontRef::from_index(font.blob.data(), font.index).unwrap();
1607 let location = face.axes().location(font.synthesis.variation_settings());
1608 let metrics = face.metrics(skrifa::instance::Size::unscaled(), &location);
1609
1610 let units_per_em = metrics.units_per_em as f32;
1611
1612 crate::items::FontMetrics {
1613 ascent: metrics.ascent * logical_pixel_size / units_per_em,
1614 descent: metrics.descent * logical_pixel_size / units_per_em,
1615 x_height: metrics.x_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1616 cap_height: metrics.cap_height.unwrap_or_default() * logical_pixel_size / units_per_em,
1617 }
1618}
1619
1620pub fn text_input_byte_offset_for_position(
1621 renderer: &dyn RendererSealed,
1622 text_input: Pin<&crate::items::TextInput>,
1623 item_rc: &crate::item_tree::ItemRc,
1624 pos: LogicalPoint,
1625) -> usize {
1626 let Some(scale_factor) = renderer.scale_factor() else {
1627 return 0;
1628 };
1629 let pos: PhysicalPoint = pos * scale_factor;
1630
1631 let width = text_input.width();
1632 let height = text_input.height();
1633 if width.get() <= 0. || height.get() <= 0. || pos.y < 0. {
1634 return 0;
1635 }
1636
1637 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1638 Some(text_input.font_request(item_rc)),
1639 text_input.wrap(),
1640 None,
1641 scale_factor,
1642 );
1643 let visual_representation = text_input.visual_representation(None);
1644
1645 let Some(ctx) = renderer.slint_context() else {
1646 return 0;
1647 };
1648 let mut font_ctx = ctx.font_context().borrow_mut();
1649
1650 let paragraphs_without_linebreaks = create_text_paragraphs(
1651 &layout_builder,
1652 &mut font_ctx,
1653 PlainOrStyledText::Plain(visual_representation.text.clone()),
1654 None,
1655 Color::default(),
1656 );
1657
1658 let layout = layout(
1659 &layout_builder,
1660 &mut font_ctx,
1661 paragraphs_without_linebreaks,
1662 scale_factor,
1663 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1664 );
1665 let byte_offset = layout.byte_offset_from_point(pos);
1666 visual_representation.map_byte_offset_from_visual_text_to_actual_text(byte_offset)
1667}
1668
1669pub fn text_input_cursor_rect_for_byte_offset(
1670 renderer: &dyn RendererSealed,
1671 text_input: Pin<&crate::items::TextInput>,
1672 item_rc: &crate::item_tree::ItemRc,
1673 byte_offset: usize,
1674) -> LogicalRect {
1675 let Some(scale_factor) = renderer.scale_factor() else {
1676 return LogicalRect::default();
1677 };
1678
1679 let layout_builder = LayoutWithoutLineBreaksBuilder::new(
1680 Some(text_input.font_request(item_rc)),
1681 text_input.wrap(),
1682 None,
1683 scale_factor,
1684 );
1685
1686 let width = text_input.width();
1687 let height = text_input.height();
1688 if width.get() <= 0. || height.get() <= 0. {
1689 return LogicalRect::new(
1690 LogicalPoint::default(),
1691 LogicalSize::from_lengths(LogicalLength::new(1.0), layout_builder.pixel_size),
1692 );
1693 }
1694
1695 let visual_representation = text_input.visual_representation(None);
1696 let cursor_width = text_input.text_cursor_width() * scale_factor;
1697
1698 let Some(ctx) = renderer.slint_context() else {
1699 return LogicalRect::default();
1700 };
1701
1702 let mut font_ctx = ctx.font_context().borrow_mut();
1703
1704 let byte_offset = visual_representation.map_byte_offset_from_actual_to_visual_text(byte_offset);
1705
1706 let paragraphs_without_linebreaks = create_text_paragraphs(
1707 &layout_builder,
1708 &mut font_ctx,
1709 PlainOrStyledText::Plain(visual_representation.text),
1710 None,
1711 Color::default(),
1712 );
1713
1714 let layout = layout(
1715 &layout_builder,
1716 &mut font_ctx,
1717 paragraphs_without_linebreaks,
1718 scale_factor,
1719 LayoutOptions::new_from_textinput(text_input, Some(width), Some(height)),
1720 );
1721 let cursor_rect = layout.cursor_rect_for_byte_offset(byte_offset, cursor_width);
1722 cursor_rect / scale_factor
1723}
1724
1725#[cfg(test)]
1726mod tests {
1727 use super::*;
1728
1729 fn paragraphs(text: &str) -> Vec<&str> {
1730 paragraph_ranges(text).map(|r| &text[r]).collect()
1731 }
1732
1733 fn layout_text(text: &str) -> Layout {
1734 let mut font_ctx = parley::FontContext {
1737 collection: fontique::Collection::new(fontique::CollectionOptions {
1738 system_fonts: false,
1739 ..Default::default()
1740 }),
1741 source_cache: Default::default(),
1742 };
1743 let data = include_bytes!("../../common/sharedfontique/Inter-VariableFont.ttf");
1744 let families =
1745 font_ctx.collection.register_fonts(fontique::Blob::new(Arc::new(data)), None);
1746 font_ctx.collection.set_generic_families(
1747 fontique::GenericFamily::SansSerif,
1748 families.iter().map(|(id, _)| *id),
1749 );
1750 let builder = LayoutWithoutLineBreaksBuilder::new(
1751 None,
1752 TextWrap::NoWrap,
1753 None,
1754 ScaleFactor::new(1.0),
1755 );
1756 let paragraphs = create_text_paragraphs(
1757 &builder,
1758 &mut font_ctx,
1759 PlainOrStyledText::Plain(text.into()),
1760 None,
1761 Color::default(),
1762 );
1763 layout(&builder, &mut font_ctx, paragraphs, ScaleFactor::new(1.0), LayoutOptions::default())
1764 }
1765
1766 fn visual_line_count(text: &str) -> usize {
1767 layout_text(text).paragraphs.iter().map(|p| p.layout.lines().len()).sum()
1768 }
1769
1770 #[test]
1771 fn test_crlf_line_count() {
1772 assert_eq!(visual_line_count("hello\r\nworld"), visual_line_count("hello\nworld"));
1773 assert_eq!(visual_line_count("hello\r\nworld"), 2);
1774 }
1775
1776 #[test]
1777 fn test_cursor_between_cr_and_lf() {
1778 let layout = layout_text("hello\r\nworld");
1781 let cursor_width = PhysicalLength::new(1.0);
1782 assert_eq!(
1783 layout.cursor_rect_for_byte_offset(6, cursor_width),
1784 layout.cursor_rect_for_byte_offset(5, cursor_width)
1785 );
1786 assert_ne!(
1787 layout.cursor_rect_for_byte_offset(6, cursor_width),
1788 layout.cursor_rect_for_byte_offset(0, cursor_width)
1789 );
1790 }
1791
1792 #[test]
1793 fn test_paragraph_ranges() {
1794 assert_eq!(paragraphs(""), [""]);
1795 assert_eq!(paragraphs("hello"), ["hello"]);
1796 assert_eq!(paragraphs("hello\nworld"), ["hello", "world"]);
1797 assert_eq!(paragraphs("hello\n"), ["hello", ""]);
1798 assert_eq!(paragraphs("\n\n"), ["", "", ""]);
1799 }
1800
1801 #[test]
1802 fn test_paragraph_ranges_crlf() {
1803 assert_eq!(paragraphs("hello\r\nworld"), ["hello", "world"]);
1804 assert_eq!(paragraphs("hello\r\n"), ["hello", ""]);
1805 assert_eq!(paragraphs("\r\n\r\n"), ["", "", ""]);
1806 assert_eq!(paragraphs("a\r\n\nb"), ["a", "", "b"]);
1807 assert_eq!(paragraphs("hello\rworld"), ["hello\rworld"]);
1809 }
1810}