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