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