Skip to main content

freya_core/elements/
paragraph.rs

1//! [paragraph()] makes it possible to render rich text with different styles. Its a more customizable API than [crate::elements::label].
2
3use std::{
4    any::Any,
5    borrow::Cow,
6    cell::RefCell,
7    fmt::{
8        Debug,
9        Display,
10    },
11    rc::Rc,
12};
13
14use freya_engine::prelude::{
15    BlendMode,
16    Canvas,
17    FontStyle,
18    Paint,
19    PaintStyle,
20    ParagraphBuilder,
21    ParagraphStyle,
22    RectHeightStyle,
23    RectWidthStyle,
24    SaveLayerRec,
25    SkParagraph,
26    SkRect,
27    TextStyle,
28};
29use rustc_hash::FxHashMap;
30use torin::prelude::{
31    Area,
32    Point2D,
33    Size2D,
34};
35
36use crate::{
37    data::{
38        AccessibilityData,
39        CursorStyleData,
40        EffectData,
41        LayoutData,
42        StyleState,
43        TextStyleData,
44        TextStyleState,
45    },
46    diff_key::DiffKey,
47    element::{
48        Element,
49        ElementExt,
50        EventHandlerType,
51        LayoutContext,
52        RenderContext,
53    },
54    events::name::EventName,
55    layers::Layer,
56    prelude::{
57        AccessibilityExt,
58        Color,
59        ContainerExt,
60        EventHandlersExt,
61        Fill,
62        KeyExt,
63        LayerExt,
64        LayoutExt,
65        MaybeExt,
66        TextAlign,
67        TextStyleExt,
68        VerticalAlign,
69    },
70    style::cursor::{
71        CursorMode,
72        CursorStyle,
73    },
74    text_cache::CachedParagraph,
75    tree::DiffModifies,
76};
77
78/// [paragraph()] makes it possible to render rich text with different styles. Its a more customizable API than [crate::elements::label].
79///
80/// See the available methods in [Paragraph].
81///
82/// ```rust
83/// # use freya::prelude::*;
84/// fn app() -> impl IntoElement {
85///     paragraph()
86///         .span(Span::new("Hello").font_size(24.0))
87///         .span(Span::new("World").font_size(16.0))
88/// }
89/// ```
90pub fn paragraph() -> Paragraph {
91    Paragraph {
92        key: DiffKey::None,
93        element: ParagraphElement::default(),
94    }
95}
96
97pub struct ParagraphHolderInner {
98    pub paragraph: Rc<SkParagraph>,
99    pub scale_factor: f64,
100}
101
102/// A shared slot that receives the laid-out paragraph, so callers can hit-test and measure
103/// text after layout. Pass it to a [`paragraph()`] with [`Paragraph::holder`].
104#[derive(Clone)]
105pub struct ParagraphHolder(pub Rc<RefCell<Option<ParagraphHolderInner>>>);
106
107impl PartialEq for ParagraphHolder {
108    fn eq(&self, other: &Self) -> bool {
109        Rc::ptr_eq(&self.0, &other.0)
110    }
111}
112
113impl Debug for ParagraphHolder {
114    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115        f.write_str("ParagraphHolder")
116    }
117}
118
119impl Default for ParagraphHolder {
120    fn default() -> Self {
121        Self(Rc::new(RefCell::new(None)))
122    }
123}
124
125#[derive(PartialEq, Clone)]
126pub struct ParagraphElement {
127    pub layout: LayoutData,
128    pub spans: Vec<Span<'static>>,
129    pub accessibility: AccessibilityData,
130    pub text_style_data: TextStyleData,
131    pub cursor_style_data: CursorStyleData,
132    pub event_handlers: FxHashMap<EventName, EventHandlerType>,
133    pub sk_paragraph: ParagraphHolder,
134    pub cursor_index: Option<usize>,
135    pub highlights: Vec<(usize, usize)>,
136    pub max_lines: Option<usize>,
137    pub line_height: Option<f32>,
138    pub relative_layer: Layer,
139    pub cursor_style: CursorStyle,
140    pub cursor_mode: CursorMode,
141    pub vertical_align: VerticalAlign,
142}
143
144impl Default for ParagraphElement {
145    fn default() -> Self {
146        let mut accessibility = AccessibilityData::default();
147        accessibility.builder.set_role(accesskit::Role::Paragraph);
148        Self {
149            layout: Default::default(),
150            spans: Default::default(),
151            accessibility,
152            text_style_data: Default::default(),
153            cursor_style_data: Default::default(),
154            event_handlers: Default::default(),
155            sk_paragraph: Default::default(),
156            cursor_index: Default::default(),
157            highlights: Default::default(),
158            max_lines: Default::default(),
159            line_height: Default::default(),
160            relative_layer: Default::default(),
161            cursor_style: CursorStyle::default(),
162            cursor_mode: CursorMode::default(),
163            vertical_align: VerticalAlign::default(),
164        }
165    }
166}
167
168impl Display for ParagraphElement {
169    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170        f.write_str(
171            &self
172                .spans
173                .iter()
174                .map(|s| s.text.clone())
175                .collect::<Vec<_>>()
176                .join("\n"),
177        )
178    }
179}
180
181impl ElementExt for ParagraphElement {
182    fn changed(&self, other: &Rc<dyn ElementExt>) -> bool {
183        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
184        else {
185            return false;
186        };
187        self != paragraph
188    }
189
190    fn diff(&self, other: &Rc<dyn ElementExt>) -> DiffModifies {
191        let Some(paragraph) = (other.as_ref() as &dyn Any).downcast_ref::<ParagraphElement>()
192        else {
193            return DiffModifies::all();
194        };
195
196        let mut diff = DiffModifies::empty();
197
198        if self.spans != paragraph.spans {
199            diff.insert(DiffModifies::STYLE);
200            diff.insert(DiffModifies::LAYOUT);
201        }
202
203        if self.accessibility != paragraph.accessibility {
204            diff.insert(DiffModifies::ACCESSIBILITY);
205        }
206
207        if self.relative_layer != paragraph.relative_layer {
208            diff.insert(DiffModifies::LAYER);
209        }
210
211        if self.text_style_data != paragraph.text_style_data {
212            diff.insert(DiffModifies::STYLE);
213        }
214
215        if self.event_handlers != paragraph.event_handlers {
216            diff.insert(DiffModifies::EVENT_HANDLERS);
217        }
218
219        if self.cursor_index != paragraph.cursor_index
220            || self.highlights != paragraph.highlights
221            || self.cursor_mode != paragraph.cursor_mode
222            || self.cursor_style != paragraph.cursor_style
223            || self.cursor_style_data != paragraph.cursor_style_data
224            || self.vertical_align != paragraph.vertical_align
225        {
226            diff.insert(DiffModifies::STYLE);
227        }
228
229        if self.text_style_data != paragraph.text_style_data
230            || self.line_height != paragraph.line_height
231            || self.max_lines != paragraph.max_lines
232        {
233            diff.insert(DiffModifies::TEXT_STYLE);
234            diff.insert(DiffModifies::LAYOUT);
235        }
236
237        if self.layout != paragraph.layout {
238            diff.insert(DiffModifies::STYLE);
239            diff.insert(DiffModifies::LAYOUT);
240        }
241
242        diff
243    }
244
245    fn layout(&'_ self) -> Cow<'_, LayoutData> {
246        Cow::Borrowed(&self.layout)
247    }
248    fn effect(&'_ self) -> Option<Cow<'_, EffectData>> {
249        None
250    }
251
252    fn style(&'_ self) -> Cow<'_, StyleState> {
253        Cow::Owned(StyleState::default())
254    }
255
256    fn text_style(&'_ self) -> Cow<'_, TextStyleData> {
257        Cow::Borrowed(&self.text_style_data)
258    }
259
260    fn accessibility(&'_ self) -> Cow<'_, AccessibilityData> {
261        Cow::Borrowed(&self.accessibility)
262    }
263
264    fn layer(&self) -> Layer {
265        self.relative_layer
266    }
267
268    fn measure(&self, context: LayoutContext) -> Option<(Size2D, Rc<dyn Any>)> {
269        let cached_paragraph = CachedParagraph {
270            text_style_state: context.text_style_state,
271            spans: &self.spans,
272            max_lines: self.max_lines,
273            line_height: self.line_height,
274            width: context.area_size.width,
275        };
276        let paragraph = context
277            .text_cache
278            .utilize(context.node_id, &cached_paragraph)
279            .unwrap_or_else(|| {
280                let mut paragraph_style = ParagraphStyle::default();
281                let mut text_style = TextStyle::default();
282
283                let mut font_families = context.text_style_state.font_families.clone();
284                font_families.extend_from_slice(context.fallback_fonts);
285
286                text_style.set_color(
287                    context
288                        .text_style_state
289                        .color
290                        .as_color()
291                        .unwrap_or(Color::WHITE),
292                );
293                text_style.set_font_size(
294                    f32::from(context.text_style_state.font_size) * context.scale_factor as f32,
295                );
296                text_style.set_font_families(&font_families);
297                text_style.set_font_style(FontStyle::new(
298                    context.text_style_state.font_weight.into(),
299                    context.text_style_state.font_width.into(),
300                    context.text_style_state.font_slant.into(),
301                ));
302
303                if context.text_style_state.text_height.needs_custom_height() {
304                    text_style.set_height_override(true);
305                    text_style.set_half_leading(true);
306                }
307
308                if let Some(line_height) = self.line_height {
309                    text_style.set_height_override(true);
310                    text_style.set_height(line_height);
311                }
312
313                for text_shadow in context.text_style_state.text_shadows.iter() {
314                    text_style.add_shadow((*text_shadow).into());
315                }
316
317                if let Some(ellipsis) = context.text_style_state.text_overflow.get_ellipsis() {
318                    paragraph_style.set_ellipsis(ellipsis);
319                }
320
321                paragraph_style.set_text_style(&text_style);
322                paragraph_style.set_max_lines(self.max_lines);
323                paragraph_style.set_text_align(context.text_style_state.text_align.into());
324
325                let mut paragraph_builder =
326                    ParagraphBuilder::new(&paragraph_style, &*context.font_collection);
327
328                for span in &self.spans {
329                    let text_style_state =
330                        TextStyleState::from_data(context.text_style_state, &span.text_style_data);
331                    let mut text_style = TextStyle::new();
332                    let mut font_families = context.text_style_state.font_families.clone();
333                    font_families.extend_from_slice(context.fallback_fonts);
334
335                    for text_shadow in text_style_state.text_shadows.iter() {
336                        text_style.add_shadow((*text_shadow).into());
337                    }
338
339                    text_style.set_color(text_style_state.color.as_color().unwrap_or(Color::WHITE));
340                    text_style.set_font_size(
341                        f32::from(text_style_state.font_size) * context.scale_factor as f32,
342                    );
343                    text_style.set_font_families(&font_families);
344                    text_style.set_font_style(FontStyle::new(
345                        text_style_state.font_weight.into(),
346                        text_style_state.font_width.into(),
347                        text_style_state.font_slant.into(),
348                    ));
349                    text_style.set_decoration_type(text_style_state.text_decoration.into());
350                    if let Some(line_height) = self.line_height {
351                        text_style.set_height_override(true);
352                        text_style.set_height(line_height);
353                    }
354                    paragraph_builder.push_style(&text_style);
355                    paragraph_builder.add_text(&span.text);
356                }
357
358                let mut paragraph = paragraph_builder.build();
359                paragraph.layout(
360                    if self.max_lines == Some(1)
361                        && context.text_style_state.text_align == TextAlign::default()
362                        && !paragraph_style.ellipsized()
363                    {
364                        f32::MAX
365                    } else {
366                        context.area_size.width + 1.0
367                    },
368                );
369                context
370                    .text_cache
371                    .insert(context.node_id, &cached_paragraph, paragraph)
372            });
373
374        let size = Size2D::new(paragraph.longest_line(), paragraph.height());
375
376        self.sk_paragraph
377            .0
378            .borrow_mut()
379            .replace(ParagraphHolderInner {
380                paragraph,
381                scale_factor: context.scale_factor,
382            });
383
384        Some((size, Rc::new(())))
385    }
386
387    fn should_hook_measurement(&self) -> bool {
388        true
389    }
390
391    fn should_measure_inner_children(&self) -> bool {
392        false
393    }
394
395    fn events_handlers(&'_ self) -> Option<Cow<'_, FxHashMap<EventName, EventHandlerType>>> {
396        Some(Cow::Borrowed(&self.event_handlers))
397    }
398
399    fn render(&self, context: RenderContext) {
400        let paragraph = self.sk_paragraph.0.borrow();
401        let ParagraphHolderInner { paragraph, .. } = paragraph.as_ref().unwrap();
402        let visible_area = context.layout_node.visible_area();
403
404        let cursor_area = match self.cursor_mode {
405            CursorMode::Fit => visible_area,
406            CursorMode::Expanded => context.layout_node.area,
407        };
408
409        let paragraph_height = paragraph.height();
410        let area_height = visible_area.height();
411        let vertical_offset = match self.vertical_align {
412            VerticalAlign::Start => 0.0,
413            VerticalAlign::Center => (area_height - paragraph_height).max(0.0) / 2.0,
414        };
415
416        let cursor_vertical_offset = match self.cursor_mode {
417            CursorMode::Fit => vertical_offset,
418            CursorMode::Expanded => 0.0,
419        };
420        let cursor_vertical_size_offset = match self.cursor_mode {
421            CursorMode::Fit => 0.,
422            CursorMode::Expanded => vertical_offset * 2.,
423        };
424
425        // Draw highlights
426        for (from, to) in self.highlights.iter() {
427            if from == to {
428                continue;
429            }
430            let (from, to) = { if from < to { (from, to) } else { (to, from) } };
431            let rects = paragraph.get_rects_for_range(
432                *from..*to,
433                RectHeightStyle::Tight,
434                RectWidthStyle::Tight,
435            );
436
437            let mut highlights_paint = Paint::default();
438            highlights_paint.set_anti_alias(true);
439            highlights_paint.set_style(PaintStyle::Fill);
440            highlights_paint.set_color(self.cursor_style_data.highlight_color);
441
442            if rects.is_empty() && *from == 0 {
443                let avg_line_height =
444                    paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
445                let rect = SkRect::new(
446                    cursor_area.min_x(),
447                    cursor_area.min_y() + cursor_vertical_offset,
448                    cursor_area.min_x() + 6.,
449                    cursor_area.min_y() + avg_line_height + cursor_vertical_size_offset,
450                );
451
452                context.canvas.draw_rect(rect, &highlights_paint);
453            }
454
455            for rect in rects {
456                let rect = SkRect::new(
457                    cursor_area.min_x() + rect.rect.left,
458                    cursor_area.min_y() + rect.rect.top + cursor_vertical_offset,
459                    cursor_area.min_x() + rect.rect.right.max(6.),
460                    cursor_area.min_y() + rect.rect.bottom + cursor_vertical_size_offset,
461                );
462                context.canvas.draw_rect(rect, &highlights_paint);
463            }
464        }
465
466        // We exclude those highlights that on the same start and end (e.g the user just started dragging)
467        let visible_highlights = self
468            .highlights
469            .iter()
470            .filter(|highlight| highlight.0 != highlight.1)
471            .count()
472            > 0;
473
474        // Draw block cursor behind text if needed
475        if let Some(cursor_index) = self.cursor_index
476            && self.cursor_style == CursorStyle::Block
477            && let Some(cursor_rect) = paragraph
478                .get_rects_for_range(
479                    cursor_index..cursor_index + 1,
480                    RectHeightStyle::Tight,
481                    RectWidthStyle::Tight,
482                )
483                .first()
484                .map(|text| text.rect)
485                .or_else(|| {
486                    // Show the cursor at the end of the text if possible
487                    let text_len = paragraph
488                        .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
489                        .position as usize;
490                    let last_rects = paragraph.get_rects_for_range(
491                        text_len.saturating_sub(1)..text_len,
492                        RectHeightStyle::Tight,
493                        RectWidthStyle::Tight,
494                    );
495
496                    if let Some(last_rect) = last_rects.first() {
497                        let mut caret = last_rect.rect;
498                        caret.left = caret.right;
499                        Some(caret)
500                    } else {
501                        let avg_line_height =
502                            paragraph.height() / paragraph.get_line_metrics().len().max(1) as f32;
503                        Some(SkRect::new(0., 0., 6., avg_line_height))
504                    }
505                })
506        {
507            let width = (cursor_rect.right - cursor_rect.left).max(6.0);
508            let cursor_rect = SkRect::new(
509                cursor_area.min_x() + cursor_rect.left,
510                cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
511                cursor_area.min_x() + cursor_rect.left + width,
512                cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
513            );
514
515            let mut paint = Paint::default();
516            paint.set_anti_alias(true);
517            paint.set_style(PaintStyle::Fill);
518            paint.set_color(self.cursor_style_data.color);
519
520            context.canvas.draw_rect(cursor_rect, &paint);
521        }
522
523        // Draw text (always uses visible_area with vertical_offset)
524        paint_paragraph_with_fill(
525            paragraph,
526            context.canvas,
527            Point2D::new(visible_area.min_x(), visible_area.min_y() + vertical_offset),
528            &context.text_style_state.color,
529        );
530
531        // Draw cursor
532        if let Some(cursor_index) = self.cursor_index
533            && !visible_highlights
534        {
535            let cursor_rects = paragraph.get_rects_for_range(
536                cursor_index..cursor_index + 1,
537                RectHeightStyle::Tight,
538                RectWidthStyle::Tight,
539            );
540            if let Some(cursor_rect) = cursor_rects.first().map(|text| text.rect).or_else(|| {
541                // Show the cursor at the end of the text if possible
542                let text_len = paragraph
543                    .get_glyph_position_at_coordinate((f32::MAX, f32::MAX))
544                    .position as usize;
545                let last_rects = paragraph.get_rects_for_range(
546                    text_len.saturating_sub(1)..text_len,
547                    RectHeightStyle::Tight,
548                    RectWidthStyle::Tight,
549                );
550
551                if let Some(last_rect) = last_rects.first() {
552                    let mut caret = last_rect.rect;
553                    caret.left = caret.right;
554                    Some(caret)
555                } else {
556                    None
557                }
558            }) {
559                let paint_color = self.cursor_style_data.color;
560                match self.cursor_style {
561                    CursorStyle::Underline => {
562                        let thickness = 2.0;
563                        let underline_rect = SkRect::new(
564                            cursor_area.min_x() + cursor_rect.left,
565                            cursor_area.min_y() + cursor_rect.bottom - thickness
566                                + cursor_vertical_offset,
567                            cursor_area.min_x() + cursor_rect.right,
568                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
569                        );
570
571                        let mut paint = Paint::default();
572                        paint.set_anti_alias(true);
573                        paint.set_style(PaintStyle::Fill);
574                        paint.set_color(paint_color);
575
576                        context.canvas.draw_rect(underline_rect, &paint);
577                    }
578                    CursorStyle::Line => {
579                        let cursor_rect = SkRect::new(
580                            cursor_area.min_x() + cursor_rect.left,
581                            cursor_area.min_y() + cursor_rect.top + cursor_vertical_offset,
582                            cursor_area.min_x() + cursor_rect.left + 2.,
583                            cursor_area.min_y() + cursor_rect.bottom + cursor_vertical_size_offset,
584                        );
585
586                        let mut paint = Paint::default();
587                        paint.set_anti_alias(true);
588                        paint.set_style(PaintStyle::Fill);
589                        paint.set_color(paint_color);
590
591                        context.canvas.draw_rect(cursor_rect, &paint);
592                    }
593                    _ => {}
594                }
595            }
596        }
597    }
598}
599
600impl From<Paragraph> for Element {
601    fn from(value: Paragraph) -> Self {
602        Element::Element {
603            key: value.key,
604            element: Rc::new(value.element),
605            elements: vec![],
606        }
607    }
608}
609
610/// Paints a paragraph with a [Fill] as the text color. Non-color fills are masked
611/// onto the rendered glyph alpha via an offscreen layer + [BlendMode::SrcIn].
612pub(crate) fn paint_paragraph_with_fill(
613    paragraph: &SkParagraph,
614    canvas: &Canvas,
615    origin: Point2D,
616    fill: &Fill,
617) {
618    if matches!(fill, Fill::Color(_)) {
619        paragraph.paint(canvas, origin.to_tuple());
620        return;
621    }
622
623    let width = paragraph.longest_line().max(paragraph.max_width());
624    let height = paragraph.height();
625    let area = Area::new(origin, Size2D::new(width, height));
626    let bounds_rect = SkRect::from_xywh(area.min_x(), area.min_y(), width, height);
627
628    let layer = canvas.save_layer(&SaveLayerRec::default().bounds(&bounds_rect));
629
630    paragraph.paint(canvas, origin.to_tuple());
631
632    let mut paint = Paint::default();
633    paint.set_anti_alias(true);
634    paint.set_style(PaintStyle::Fill);
635    paint.set_blend_mode(BlendMode::SrcIn);
636    fill.apply_to_paint(&mut paint, area);
637
638    canvas.draw_rect(bounds_rect, &paint);
639
640    canvas.restore_to_count(layer);
641}
642
643impl KeyExt for Paragraph {
644    fn write_key(&mut self) -> &mut DiffKey {
645        &mut self.key
646    }
647}
648
649impl EventHandlersExt for Paragraph {
650    fn get_event_handlers(&mut self) -> &mut FxHashMap<EventName, EventHandlerType> {
651        &mut self.element.event_handlers
652    }
653}
654
655impl MaybeExt for Paragraph {}
656
657impl LayerExt for Paragraph {
658    fn get_layer(&mut self) -> &mut Layer {
659        &mut self.element.relative_layer
660    }
661}
662
663pub struct Paragraph {
664    key: DiffKey,
665    element: ParagraphElement,
666}
667
668impl LayoutExt for Paragraph {
669    fn get_layout(&mut self) -> &mut LayoutData {
670        &mut self.element.layout
671    }
672}
673
674impl ContainerExt for Paragraph {}
675
676impl AccessibilityExt for Paragraph {
677    fn get_accessibility_data(&mut self) -> &mut AccessibilityData {
678        &mut self.element.accessibility
679    }
680}
681
682impl TextStyleExt for Paragraph {
683    fn get_text_style_data(&mut self) -> &mut TextStyleData {
684        &mut self.element.text_style_data
685    }
686}
687
688impl Paragraph {
689    pub fn try_downcast(element: &dyn ElementExt) -> Option<ParagraphElement> {
690        (element as &dyn Any)
691            .downcast_ref::<ParagraphElement>()
692            .cloned()
693    }
694
695    /// Append every [`Span`] yielded by the iterator to the paragraph.
696    pub fn spans_iter(mut self, spans: impl Iterator<Item = Span<'static>>) -> Self {
697        let spans = spans.collect::<Vec<Span>>();
698        // TODO: Accessible paragraphs
699        // self.element.accessibility.builder.set_value(text.clone());
700        self.element.spans.extend(spans);
701        self
702    }
703
704    /// Append a single [`Span`] of styled text to the paragraph.
705    pub fn span(mut self, span: impl Into<Span<'static>>) -> Self {
706        let span = span.into();
707        // TODO: Accessible paragraphs
708        // self.element.accessibility.builder.set_value(text.clone());
709        self.element.spans.push(span);
710        self
711    }
712
713    /// Set the color of the text cursor. See [`Color`].
714    pub fn cursor_color(mut self, cursor_color: impl Into<Color>) -> Self {
715        self.element.cursor_style_data.color = cursor_color.into();
716        self
717    }
718
719    /// Set the color used to highlight selected text. See [`Color`].
720    pub fn highlight_color(mut self, highlight_color: impl Into<Color>) -> Self {
721        self.element.cursor_style_data.highlight_color = highlight_color.into();
722        self
723    }
724
725    /// Set the shape of the text cursor. See [`CursorStyle`].
726    pub fn cursor_style(mut self, cursor_style: impl Into<CursorStyle>) -> Self {
727        self.element.cursor_style = cursor_style.into();
728        self
729    }
730
731    /// Attach a [`ParagraphHolder`] that receives the laid-out paragraph for hit-testing and measurement.
732    pub fn holder(mut self, holder: ParagraphHolder) -> Self {
733        self.element.sk_paragraph = holder;
734        self
735    }
736
737    /// Place the text cursor at the given character index. Pass `None` to hide it.
738    pub fn cursor_index(mut self, cursor_index: impl Into<Option<usize>>) -> Self {
739        self.element.cursor_index = cursor_index.into();
740        self
741    }
742
743    /// Highlight the given `(start, end)` character ranges, used for text selection.
744    pub fn highlights(mut self, highlights: impl Into<Option<Vec<(usize, usize)>>>) -> Self {
745        if let Some(highlights) = highlights.into() {
746            self.element.highlights = highlights;
747        }
748        self
749    }
750
751    /// Limit the paragraph to at most this many lines, truncating the rest. Pass `None` for no limit.
752    pub fn max_lines(mut self, max_lines: impl Into<Option<usize>>) -> Self {
753        self.element.max_lines = max_lines.into();
754        self
755    }
756
757    /// Override the height of each line as a multiple of the font size. Pass `None` for the default.
758    pub fn line_height(mut self, line_height: impl Into<Option<f32>>) -> Self {
759        self.element.line_height = line_height.into();
760        self
761    }
762
763    /// Set the cursor mode for the paragraph.
764    /// - `CursorMode::Fit`: cursor/highlights use the paragraph's visible_area. VerticalAlign affects cursor positions.
765    /// - `CursorMode::Expanded`: cursor/highlights use the paragraph's inner_area. VerticalAlign does NOT affect cursor positions.
766    pub fn cursor_mode(mut self, cursor_mode: impl Into<CursorMode>) -> Self {
767        self.element.cursor_mode = cursor_mode.into();
768        self
769    }
770
771    /// Set the vertical alignment for the paragraph text.
772    /// This affects how the text is rendered within the paragraph area, but cursor/highlight behavior
773    /// depends on the `cursor_mode` setting.
774    pub fn vertical_align(mut self, vertical_align: impl Into<VerticalAlign>) -> Self {
775        self.element.vertical_align = vertical_align.into();
776        self
777    }
778}
779
780/// A run of text with its own style, used to build a [`paragraph()`].
781///
782/// Create it with [`Span::new`] (or from a `&str`/`String`) and style it with the
783/// [`TextStyleExt`] methods such as [`font_size`](TextStyleExt::font_size) and
784/// [`color`](TextStyleExt::color):
785///
786/// ```
787/// # use freya_core::prelude::*;
788/// let span = Span::new("Hello").font_size(24.0).color(Color::RED);
789/// ```
790#[derive(Clone, PartialEq, Hash)]
791pub struct Span<'a> {
792    pub text_style_data: TextStyleData,
793    pub text: Cow<'a, str>,
794}
795
796impl From<&'static str> for Span<'static> {
797    fn from(text: &'static str) -> Self {
798        Span {
799            text_style_data: TextStyleData::default(),
800            text: text.into(),
801        }
802    }
803}
804
805impl From<String> for Span<'static> {
806    fn from(text: String) -> Self {
807        Span {
808            text_style_data: TextStyleData::default(),
809            text: text.into(),
810        }
811    }
812}
813
814impl<'a> Span<'a> {
815    /// Create a [`Span`] from the given text, with the default text style.
816    pub fn new(text: impl Into<Cow<'a, str>>) -> Self {
817        Self {
818            text: text.into(),
819            text_style_data: TextStyleData::default(),
820        }
821    }
822}
823
824impl<'a> TextStyleExt for Span<'a> {
825    fn get_text_style_data(&mut self) -> &mut TextStyleData {
826        &mut self.text_style_data
827    }
828}