Skip to main content

inset_rendering/
paragraph.rs

1//! Flutter counterpart: `rendering/paragraph.dart` (`RenderParagraph`).
2//!
3//! The paragraph is a leaf here: inline children (`WidgetSpan` placeholders), selection
4//! (`SelectionRegistrar` / `_SelectableFragment`), the fade overflow shader, and semantics
5//! wait; see `PORTING.md`.
6
7use inset_embedder::{
8    BoxHeightStyle, BoxWidthStyle, ClipOp, FontCollection, Offset, Rect, Size, TextAlign,
9    TextBaseline, TextBox, TextDirection, TextHeightBehavior, TextPosition, TextRange,
10    TextSelection,
11};
12use inset_foundation::{App, Handle};
13use inset_painting::{
14    ClipContext, InlineSpanRef, PaintingBinding, RenderComparison, TextOverflow, TextPainter,
15    TextScaler, TextWidthBasis,
16};
17
18use crate::box_::{BoxConstraints, BoxHitTestResult, RenderBox, RenderBoxData};
19use crate::object::{AnyRenderObject, Constraints, RenderHandle, RenderObject, RenderObjectData};
20use crate::painting_context::PaintingContext;
21use crate::{RelayoutWhenSystemFontsChangeData, RelayoutWhenSystemFontsChangeMixin};
22
23const K_ELLIPSIS: &str = "\u{2026}";
24
25/// A render object that displays a paragraph of text.
26///
27/// Dart's constructor arguments beyond `text` and `textDirection` are the setters. The
28/// paragraph shapes against `fonts` when given, else the app-wide collection of
29/// `PaintingBinding`.
30pub struct RenderParagraph {
31    render_object: RenderObjectData,
32    render_box: RenderBoxData,
33    text_painter: TextPainter,
34    text_intrinsics: Option<TextPainter>,
35    fonts_override: Option<Handle<FontCollection>>,
36    soft_wrap: bool,
37    overflow: TextOverflow,
38    device_pixel_ratio: f64,
39    needs_clipping: bool,
40    system_fonts: RelayoutWhenSystemFontsChangeData,
41}
42
43impl RenderParagraph {
44    /// Creates a paragraph render object.
45    pub fn new(
46        app: &mut App,
47        text: InlineSpanRef,
48        text_direction: TextDirection,
49        fonts: Option<Handle<FontCollection>>,
50    ) -> RenderHandle<Self> {
51        debug_assert!(text.debug_assert_is_valid());
52        let mut text_painter = TextPainter::new();
53        text_painter.set_text(Some(text));
54        text_painter.set_text_direction(Some(text_direction));
55        RenderHandle::new_box(
56            app,
57            RenderParagraph {
58                render_object: RenderObjectData::new(),
59                render_box: RenderBoxData::new(),
60                text_painter,
61                text_intrinsics: None,
62                fonts_override: fonts,
63                soft_wrap: true,
64                overflow: TextOverflow::Clip,
65                device_pixel_ratio: 1.0,
66                needs_clipping: false,
67                system_fonts: RelayoutWhenSystemFontsChangeData::new(),
68            },
69        )
70    }
71
72    fn painter(self: RenderHandle<Self>, app: &App) -> &TextPainter {
73        &self.get(app).text_painter
74    }
75
76    fn painter_mut(self: RenderHandle<Self>, app: &mut App) -> &mut TextPainter {
77        &mut self.get_mut(app).text_painter
78    }
79
80    /// The collection this paragraph shapes against: the override, else the app-wide one.
81    pub fn fonts(self: RenderHandle<Self>, app: &mut App) -> Handle<FontCollection> {
82        match self.get(app).fonts_override {
83            Some(fonts) => fonts,
84            None => PaintingBinding::instance(app).fonts(app),
85        }
86    }
87
88    /// The painter together with the collection it shapes against.
89    fn painter_with_fonts(
90        self: RenderHandle<Self>,
91        app: &mut App,
92    ) -> (&mut TextPainter, &mut FontCollection) {
93        let fonts = self.fonts(app);
94        let (this, fonts) = app.get_disjoint_mut(self.handle(), fonts);
95        (&mut this.text_painter, fonts)
96    }
97
98    /// The text to display.
99    pub fn text(self: RenderHandle<Self>, app: &App) -> InlineSpanRef {
100        self.painter(app)
101            .text()
102            .cloned()
103            .expect("a paragraph always has text")
104    }
105
106    /// Sets [`text`](Self::text).
107    pub fn set_text(self: RenderHandle<Self>, app: &mut App, value: InlineSpanRef) {
108        match self.text(app).compare_to(&*value) {
109            RenderComparison::Identical => {}
110            RenderComparison::Metadata => {
111                self.painter_mut(app).set_text(Some(value));
112            }
113            RenderComparison::Paint => {
114                self.painter_mut(app).set_text(Some(value));
115                self.mark_needs_paint(app);
116            }
117            RenderComparison::Layout => {
118                self.painter_mut(app).set_text(Some(value));
119                self.mark_needs_layout(app);
120            }
121        }
122    }
123
124    /// How the text should be aligned horizontally.
125    pub fn text_align(self: RenderHandle<Self>, app: &App) -> TextAlign {
126        self.painter(app).text_align()
127    }
128
129    /// Sets [`text_align`](Self::text_align).
130    pub fn set_text_align(self: RenderHandle<Self>, app: &mut App, value: TextAlign) {
131        if self.painter(app).text_align() == value {
132            return;
133        }
134        self.painter_mut(app).set_text_align(value);
135        self.mark_needs_paint(app);
136    }
137
138    /// The directionality of the text.
139    ///
140    /// This decides how the [`TextAlign::Start`], [`TextAlign::End`], and
141    /// [`TextAlign::Justify`] values of [`text_align`](Self::text_align) are interpreted.
142    ///
143    /// This is also used to disambiguate how to render bidirectional text. For example, if
144    /// the [`text`](Self::text) is an English phrase followed by a Hebrew phrase, in a
145    /// [`TextDirection::Ltr`] context the English phrase will be on the left and the Hebrew
146    /// phrase to its right, while in a [`TextDirection::Rtl`] context, the English phrase
147    /// will be on the right and the Hebrew phrase on its left.
148    pub fn text_direction(self: RenderHandle<Self>, app: &App) -> TextDirection {
149        self.painter(app)
150            .text_direction()
151            .expect("a paragraph always has a direction")
152    }
153
154    /// Sets [`text_direction`](Self::text_direction).
155    pub fn set_text_direction(self: RenderHandle<Self>, app: &mut App, value: TextDirection) {
156        if self.painter(app).text_direction() == Some(value) {
157            return;
158        }
159        self.painter_mut(app).set_text_direction(Some(value));
160        self.mark_needs_layout(app);
161    }
162
163    /// Whether the text should break at soft line breaks.
164    ///
165    /// If false, the glyphs in the text will be positioned as if there was unlimited
166    /// horizontal space.
167    ///
168    /// If the text exceeds the given number of lines, it will be truncated according to
169    /// [`overflow`](Self::overflow) and [`soft_wrap`](Self::soft_wrap).
170    pub fn soft_wrap(self: RenderHandle<Self>, app: &App) -> bool {
171        self.get(app).soft_wrap
172    }
173
174    /// Sets [`soft_wrap`](Self::soft_wrap).
175    pub fn set_soft_wrap(self: RenderHandle<Self>, app: &mut App, value: bool) {
176        if self.get(app).soft_wrap == value {
177            return;
178        }
179        self.get_mut(app).soft_wrap = value;
180        self.mark_needs_layout(app);
181    }
182
183    /// How visual overflow should be handled.
184    pub fn overflow(self: RenderHandle<Self>, app: &App) -> TextOverflow {
185        self.get(app).overflow
186    }
187
188    /// Sets [`overflow`](Self::overflow).
189    pub fn set_overflow(self: RenderHandle<Self>, app: &mut App, value: TextOverflow) {
190        if self.get(app).overflow == value {
191            return;
192        }
193        self.get_mut(app).overflow = value;
194        let ellipsis = (value == TextOverflow::Ellipsis).then(|| K_ELLIPSIS.to_owned());
195        self.painter_mut(app).set_ellipsis(ellipsis);
196        self.mark_needs_layout(app);
197    }
198
199    /// The font scaling strategy to use when laying out and rendering the text.
200    pub fn text_scaler(self: RenderHandle<Self>, app: &App) -> TextScaler {
201        self.painter(app).text_scaler().clone()
202    }
203
204    /// Sets [`text_scaler`](Self::text_scaler).
205    pub fn set_text_scaler(self: RenderHandle<Self>, app: &mut App, value: TextScaler) {
206        if *self.painter(app).text_scaler() == value {
207            return;
208        }
209        self.painter_mut(app).set_text_scaler(value);
210        self.mark_needs_layout(app);
211    }
212
213    /// The pixel ratio of the device this paragraph is being rendered on.
214    ///
215    /// Flutter repaints on change only on the web; here it is bookkeeping.
216    pub fn device_pixel_ratio(self: RenderHandle<Self>, app: &App) -> f64 {
217        self.get(app).device_pixel_ratio
218    }
219
220    /// Sets [`device_pixel_ratio`](Self::device_pixel_ratio).
221    pub fn set_device_pixel_ratio(self: RenderHandle<Self>, app: &mut App, value: f64) {
222        if self.get(app).device_pixel_ratio == value {
223            return;
224        }
225        self.get_mut(app).device_pixel_ratio = value;
226    }
227
228    /// An optional maximum number of lines for the text to span, wrapping if necessary.
229    ///
230    /// If the text exceeds the given number of lines, it will be truncated according to
231    /// [`overflow`](Self::overflow) and [`soft_wrap`](Self::soft_wrap).
232    pub fn max_lines(self: RenderHandle<Self>, app: &App) -> Option<i32> {
233        self.painter(app).max_lines()
234    }
235
236    /// Sets [`max_lines`](Self::max_lines). The value may be `None`. If it is not `None`, then
237    /// it must be greater than zero.
238    pub fn set_max_lines(self: RenderHandle<Self>, app: &mut App, value: Option<i32>) {
239        debug_assert!(value.is_none_or(|value| value > 0));
240        if self.painter(app).max_lines() == value {
241            return;
242        }
243        self.painter_mut(app).set_max_lines(value);
244        self.mark_needs_layout(app);
245    }
246
247    /// Defines how to measure the width of the rendered text.
248    pub fn text_width_basis(self: RenderHandle<Self>, app: &App) -> TextWidthBasis {
249        self.painter(app).text_width_basis()
250    }
251
252    /// Sets [`text_width_basis`](Self::text_width_basis).
253    pub fn set_text_width_basis(self: RenderHandle<Self>, app: &mut App, value: TextWidthBasis) {
254        if self.painter(app).text_width_basis() == value {
255            return;
256        }
257        self.painter_mut(app).set_text_width_basis(value);
258        self.mark_needs_layout(app);
259    }
260
261    /// Defines how to apply `TextStyle.height` over and under text.
262    pub fn text_height_behavior(self: RenderHandle<Self>, app: &App) -> Option<TextHeightBehavior> {
263        self.painter(app).text_height_behavior()
264    }
265
266    /// Sets [`text_height_behavior`](Self::text_height_behavior).
267    pub fn set_text_height_behavior(
268        self: RenderHandle<Self>,
269        app: &mut App,
270        value: Option<TextHeightBehavior>,
271    ) {
272        if self.painter(app).text_height_behavior() == value {
273            return;
274        }
275        self.painter_mut(app).set_text_height_behavior(value);
276        self.mark_needs_layout(app);
277    }
278
279    /// The height of a space in [`text`](Self::text) in logical pixels.
280    pub fn preferred_line_height(self: RenderHandle<Self>, app: &mut App) -> f64 {
281        let (painter, fonts) = self.painter_with_fonts(app);
282        painter.preferred_line_height(fonts)
283    }
284
285    /// Flutter's `_textIntrinsics`: a second painter kept in sync with this paragraph's own,
286    /// together with the collection it shapes against.
287    ///
288    /// Laying the paragraph's painter out for intrinsics would destroy the state its own layout
289    /// left behind.
290    fn text_intrinsics_with_fonts(
291        self: RenderHandle<Self>,
292        app: &mut App,
293    ) -> (&mut TextPainter, &mut FontCollection) {
294        let fonts = self.fonts(app);
295        let (this, fonts) = app.get_disjoint_mut(self.handle(), fonts);
296        let painter = &this.text_painter;
297        let intrinsics = this.text_intrinsics.get_or_insert_with(TextPainter::new);
298        intrinsics.set_text(painter.text().cloned());
299        intrinsics.set_text_align(painter.text_align());
300        intrinsics.set_text_direction(painter.text_direction());
301        intrinsics.set_text_scaler(painter.text_scaler().clone());
302        intrinsics.set_max_lines(painter.max_lines());
303        intrinsics.set_ellipsis(painter.ellipsis().map(str::to_owned));
304        intrinsics.set_text_width_basis(painter.text_width_basis());
305        intrinsics.set_text_height_behavior(painter.text_height_behavior());
306        (intrinsics, fonts)
307    }
308
309    /// Flutter's `_computeIntrinsicHeight`: the height the text takes at `width`.
310    fn compute_intrinsic_height(self: RenderHandle<Self>, app: &mut App, width: f64) -> f64 {
311        let max_width = self.adjust_max_width(app, width);
312        let (intrinsics, fonts) = self.text_intrinsics_with_fonts(app);
313        intrinsics.layout(fonts, width, max_width);
314        intrinsics.height()
315    }
316
317    fn adjust_max_width(self: RenderHandle<Self>, app: &App, max_width: f64) -> f64 {
318        if self.get(app).soft_wrap || self.get(app).overflow == TextOverflow::Ellipsis {
319            max_width
320        } else {
321            f64::INFINITY
322        }
323    }
324
325    fn layout_text_with_constraints(
326        self: RenderHandle<Self>,
327        app: &mut App,
328        constraints: BoxConstraints,
329    ) {
330        let max_width = self.adjust_max_width(app, constraints.max_width);
331        let (painter, fonts) = self.painter_with_fonts(app);
332        painter.set_placeholder_dimensions(None);
333        painter.layout(fonts, constraints.min_width, max_width);
334    }
335
336    /// Returns the offset at which to paint the caret.
337    ///
338    /// Valid only after layout.
339    pub fn get_offset_for_caret(
340        self: RenderHandle<Self>,
341        app: &mut App,
342        position: TextPosition,
343        caret_prototype: Rect,
344    ) -> Offset {
345        debug_assert!(!self.debug_needs_layout(app));
346        let constraints = self.constraints(app);
347        self.layout_text_with_constraints(app, constraints);
348        let (painter, fonts) = self.painter_with_fonts(app);
349        painter.get_offset_for_caret(fonts, position, caret_prototype)
350    }
351
352    /// Returns the full height of the caret at the given position.
353    ///
354    /// Valid only after layout.
355    pub fn get_full_height_for_caret(
356        self: RenderHandle<Self>,
357        app: &mut App,
358        position: TextPosition,
359    ) -> f64 {
360        debug_assert!(!self.debug_needs_layout(app));
361        let constraints = self.constraints(app);
362        self.layout_text_with_constraints(app, constraints);
363        let (painter, fonts) = self.painter_with_fonts(app);
364        painter.get_full_height_for_caret(fonts, position, Rect::ZERO)
365    }
366
367    /// Returns a list of rects that bound the given selection.
368    ///
369    /// The `box_height_style` and `box_width_style` arguments may be used to select the
370    /// shape of the [`TextBox`]es. These properties default to [`BoxHeightStyle::Tight`] and
371    /// [`BoxWidthStyle::Tight`] respectively.
372    ///
373    /// A given selection might have more than one rect if this text painter contains
374    /// bidirectional text because logically contiguous text might not be visually contiguous.
375    ///
376    /// Valid only after layout.
377    pub fn get_boxes_for_selection(
378        self: RenderHandle<Self>,
379        app: &mut App,
380        selection: TextSelection,
381        box_height_style: BoxHeightStyle,
382        box_width_style: BoxWidthStyle,
383    ) -> Vec<TextBox> {
384        debug_assert!(!self.debug_needs_layout(app));
385        let constraints = self.constraints(app);
386        self.layout_text_with_constraints(app, constraints);
387        self.painter(app)
388            .get_boxes_for_selection(selection, box_height_style, box_width_style)
389    }
390
391    /// Returns the position within the text for the given pixel offset.
392    ///
393    /// Valid only after layout.
394    pub fn get_position_for_offset(
395        self: RenderHandle<Self>,
396        app: &mut App,
397        offset: Offset,
398    ) -> TextPosition {
399        debug_assert!(!self.debug_needs_layout(app));
400        let constraints = self.constraints(app);
401        self.layout_text_with_constraints(app, constraints);
402        self.painter(app).get_position_for_offset(offset)
403    }
404
405    /// Returns the text range of the word at the given offset. Characters not part of a word,
406    /// such as spaces, symbols, and punctuation, have word breaks on both sides. In such
407    /// cases, this method will return a text range that contains the given text position.
408    ///
409    /// Word boundaries are defined more precisely in Unicode Standard Annex #29
410    /// <http://www.unicode.org/reports/tr29/#Word_Boundaries>.
411    ///
412    /// Valid only after layout.
413    pub fn get_word_boundary(
414        self: RenderHandle<Self>,
415        app: &mut App,
416        position: TextPosition,
417    ) -> TextRange {
418        debug_assert!(!self.debug_needs_layout(app));
419        let constraints = self.constraints(app);
420        self.layout_text_with_constraints(app, constraints);
421        self.painter(app).get_word_boundary(position)
422    }
423
424    /// Returns the size of the text as laid out.
425    ///
426    /// This can differ from [`size`](RenderBox::size) if the text overflowed or if the
427    /// [`constraints`](RenderBox::constraints) provided by the parent
428    /// [`RenderObject`](crate::RenderObject) forced the layout to be bigger than necessary
429    /// for the given [`text`](Self::text).
430    ///
431    /// This returns the [`TextPainter::size`] of the underlying [`TextPainter`].
432    ///
433    /// Valid only after layout.
434    pub fn text_size(self: RenderHandle<Self>, app: &App) -> Size {
435        debug_assert!(!self.debug_needs_layout(app));
436        self.painter(app).size()
437    }
438
439    /// Whether the text was truncated or ellipsized as laid out.
440    ///
441    /// This returns the [`TextPainter::did_exceed_max_lines`] of the underlying
442    /// [`TextPainter`].
443    ///
444    /// Valid only after layout.
445    pub fn did_exceed_max_lines(self: RenderHandle<Self>, app: &App) -> bool {
446        debug_assert!(!self.debug_needs_layout(app));
447        self.painter(app).did_exceed_max_lines()
448    }
449}
450
451impl RelayoutWhenSystemFontsChangeMixin for RenderParagraph {
452    fn relayout_when_system_fonts_change_data(
453        self: RenderHandle<Self>,
454        app: &App,
455    ) -> &RelayoutWhenSystemFontsChangeData {
456        &self.get(app).system_fonts
457    }
458
459    fn relayout_when_system_fonts_change_data_mut(
460        self: RenderHandle<Self>,
461        app: &mut App,
462    ) -> &mut RelayoutWhenSystemFontsChangeData {
463        &mut self.get_mut(app).system_fonts
464    }
465
466    fn system_fonts_did_change(self: RenderHandle<Self>, app: &mut App) {
467        self.as_render_object(app).mark_needs_layout(app);
468        self.painter_mut(app).mark_needs_layout();
469    }
470}
471
472impl RenderObject for RenderParagraph {
473    crate::render_object_accessors!();
474
475    fn visit_children(
476        self: RenderHandle<Self>,
477        _app: &App,
478        _visitor: &mut dyn FnMut(AnyRenderObject),
479    ) {
480        // A leaf until inline children (`WidgetSpan`) are ported.
481    }
482
483    fn did_attach(self: RenderHandle<Self>, app: &mut App, _owner: Handle<crate::PipelineOwner>) {
484        RelayoutWhenSystemFontsChangeMixin::attach_system_fonts(self, app);
485    }
486
487    fn did_detach(self: RenderHandle<Self>, app: &mut App) {
488        RelayoutWhenSystemFontsChangeMixin::detach_system_fonts(self, app);
489    }
490
491    fn perform_layout(self: RenderHandle<Self>, app: &mut App) {
492        let constraints = self.constraints(app);
493        self.layout_text_with_constraints(app, constraints);
494        let text_size = self.painter(app).size();
495        let size = constraints.constrain(text_size);
496        self.set_size(app, size);
497
498        let did_overflow_height =
499            size.height() < text_size.height() || self.painter(app).did_exceed_max_lines();
500        let did_overflow_width = size.width() < text_size.width();
501        // TODO(abarth): We're only measuring the sizes of the line boxes here. If
502        // the glyphs draw outside the line boxes, we might think that there isn't
503        // visual overflow when there actually is visual overflow. This can become
504        // a problem if we start having horizontal overflow and introduce a clip
505        // that affects the actual (but undetected) vertical overflow.
506        let has_visual_overflow = did_overflow_width || did_overflow_height;
507        let needs_clipping = has_visual_overflow
508            && match self.get(app).overflow {
509                TextOverflow::Visible => false,
510                // The fade shader waits on gradients; a fade clips meanwhile.
511                TextOverflow::Clip | TextOverflow::Ellipsis | TextOverflow::Fade => true,
512            };
513        self.get_mut(app).needs_clipping = needs_clipping;
514    }
515
516    fn paint(
517        self: RenderHandle<Self>,
518        app: &mut App,
519        context: &mut PaintingContext,
520        offset: Offset,
521    ) {
522        // Ideally we could compute the min/max intrinsic width/height with a
523        // non-destructive operation. However, currently, computing these values
524        // will destroy state inside the painter. If that happens, we need to get back
525        // the correct state by calling _layout again.
526        //
527        // TODO(abarth): Make computing the min/max intrinsic width/height a
528        //  non-destructive operation.
529        //
530        // If you remove this call, make sure that changing the textAlign still
531        // works properly.
532        let constraints = self.constraints(app);
533        self.layout_text_with_constraints(app, constraints);
534
535        let needs_clipping = self.get(app).needs_clipping;
536        if needs_clipping {
537            let bounds = offset & self.size(app);
538            context.canvas().save();
539            context.canvas().clip_rect(bounds, ClipOp::Intersect);
540        }
541        let (painter, fonts) = self.painter_with_fonts(app);
542        painter.paint(fonts, context.canvas(), offset);
543        if needs_clipping {
544            context.canvas().restore();
545        }
546    }
547}
548
549impl RenderBox for RenderParagraph {
550    crate::render_box_accessors!();
551
552    fn compute_min_intrinsic_width(self: RenderHandle<Self>, app: &mut App, _height: f64) -> f64 {
553        let (intrinsics, fonts) = self.text_intrinsics_with_fonts(app);
554        intrinsics.layout(fonts, 0.0, f64::INFINITY);
555        intrinsics.min_intrinsic_width()
556    }
557
558    fn compute_max_intrinsic_width(self: RenderHandle<Self>, app: &mut App, _height: f64) -> f64 {
559        let (intrinsics, fonts) = self.text_intrinsics_with_fonts(app);
560        intrinsics.layout(fonts, 0.0, f64::INFINITY);
561        intrinsics.max_intrinsic_width()
562    }
563
564    fn compute_min_intrinsic_height(self: RenderHandle<Self>, app: &mut App, width: f64) -> f64 {
565        self.compute_intrinsic_height(app, width)
566    }
567
568    fn compute_max_intrinsic_height(self: RenderHandle<Self>, app: &mut App, width: f64) -> f64 {
569        self.compute_intrinsic_height(app, width)
570    }
571
572    fn compute_dry_layout(
573        self: RenderHandle<Self>,
574        app: &mut App,
575        constraints: BoxConstraints,
576    ) -> Size {
577        let max_width = self.adjust_max_width(app, constraints.max_width);
578        let (intrinsics, fonts) = self.text_intrinsics_with_fonts(app);
579        intrinsics.layout(fonts, constraints.min_width, max_width);
580        let size = intrinsics.size();
581        constraints.constrain(size)
582    }
583
584    fn compute_distance_to_actual_baseline(
585        self: RenderHandle<Self>,
586        app: &mut App,
587        _baseline: TextBaseline,
588    ) -> Option<f64> {
589        debug_assert!(!self.as_object().debug_needs_layout(app));
590        let constraints = self.constraints(app);
591        debug_assert!(constraints.debug_assert_is_valid(false));
592        self.layout_text_with_constraints(app, constraints);
593        // Since the metric for the ideographic baseline is inaccurate and the non-alphabetic
594        // baselines are based off of the alphabetic baseline, the alphabetic one is used for
595        // now to produce correct layouts.
596        Some(
597            self.painter(app)
598                .compute_distance_to_actual_baseline(TextBaseline::Alphabetic),
599        )
600    }
601
602    fn compute_dry_baseline(
603        self: RenderHandle<Self>,
604        app: &mut App,
605        constraints: BoxConstraints,
606        _baseline: TextBaseline,
607    ) -> Option<f64> {
608        debug_assert!(constraints.debug_assert_is_valid(false));
609        let max_width = self.adjust_max_width(app, constraints.max_width);
610        let (intrinsics, fonts) = self.text_intrinsics_with_fonts(app);
611        intrinsics.layout(fonts, constraints.min_width, max_width);
612        Some(intrinsics.compute_distance_to_actual_baseline(TextBaseline::Alphabetic))
613    }
614
615    fn hit_test_self(self: RenderHandle<Self>, _app: &App, _position: Offset) -> bool {
616        true
617    }
618
619    fn hit_test_children(
620        self: RenderHandle<Self>,
621        _app: &mut App,
622        _result: &mut BoxHitTestResult<'_>,
623        _position: Offset,
624    ) -> bool {
625        // Spans are not hit-test targets, and there are no inline children.
626        false
627    }
628}
629
630#[cfg(test)]
631mod tests {
632    use inset_embedder::Color;
633    use inset_foundation::AppCell;
634    use inset_gestures::HitTestResult;
635    use inset_painting::{TextSpan, TextStyle};
636
637    use super::*;
638    use crate::layer::{ContainerLayer, ErasedLayer, OffsetLayer};
639    use crate::pipeline_owner::PipelineOwner;
640    use crate::proxy_box::RenderRepaintBoundary;
641
642    /// What the shell does at start-up: the app-wide fonts, here the OS fonts.
643    fn install_fonts(app: &mut App) {
644        let binding = PaintingBinding::instance(app);
645        if !binding.has_fonts(app) {
646            binding.install_fonts(app, |fonts| {
647                fonts.add_source(valo_system_fonts::SystemFonts::load());
648            });
649        }
650    }
651
652    fn span(text: &str) -> InlineSpanRef {
653        TextSpan::new()
654            .text(text)
655            .style(TextStyle::new().font_size(20.0))
656            .into_span()
657    }
658
659    fn laid_out(
660        app: &mut App,
661        text: &str,
662        constraints: BoxConstraints,
663    ) -> (
664        RenderHandle<RenderParagraph>,
665        RenderHandle<RenderRepaintBoundary>,
666    ) {
667        install_fonts(app);
668        let paragraph = RenderParagraph::new(app, span(text), TextDirection::Ltr, None);
669        let root = RenderRepaintBoundary::new(app, Some(paragraph.as_box()));
670        let owner = PipelineOwner::new(app, None, None);
671        owner.set_root_node(app, Some(root.as_object()));
672        root.schedule_initial_layout(app);
673        root.layout(app, constraints, false);
674        let paint_root = OffsetLayer::new(app, Offset::ZERO);
675        paint_root.as_layer().attach(app, root.as_object().id());
676        root.as_object()
677            .schedule_initial_paint(app, paint_root.as_container_layer());
678        (paragraph, root)
679    }
680
681    /// `paragraph_test.dart`: the minimum intrinsic width is the widest word, the maximum is the
682    /// whole text on one line, and the intrinsic height at a width is what the text takes there.
683    #[test]
684    fn paragraph_intrinsics_come_from_the_text() {
685        let cell = AppCell::new();
686        let mut app = cell.borrow_mut();
687        install_fonts(&mut app);
688        let paragraph = RenderParagraph::new(
689            &mut app,
690            span("one two three four"),
691            TextDirection::Ltr,
692            None,
693        );
694        let box_ = paragraph.as_box();
695        let min_width = box_.get_min_intrinsic_width(&mut app, f64::INFINITY);
696        let max_width = box_.get_max_intrinsic_width(&mut app, f64::INFINITY);
697        assert!(min_width > 0.0);
698        assert!(
699            min_width < max_width,
700            "the widest word is narrower than the whole text"
701        );
702
703        let one_line = box_.get_min_intrinsic_height(&mut app, max_width);
704        let wrapped = box_.get_min_intrinsic_height(&mut app, max_width / 2.0);
705        assert!(wrapped > one_line, "half the width takes more lines");
706        assert_eq!(box_.get_max_intrinsic_height(&mut app, max_width), one_line);
707    }
708
709    /// The dry layout of a paragraph is the size it lays out to, and its dry baseline is the
710    /// baseline that layout reports.
711    #[test]
712    fn paragraph_dry_layout_and_baseline_match_its_layout() {
713        let cell = AppCell::new();
714        let mut app = cell.borrow_mut();
715        let constraints = BoxConstraints::new().max_width(200.0).max_height(1000.0);
716        install_fonts(&mut app);
717        let paragraph =
718            RenderParagraph::new(&mut app, span("one two three"), TextDirection::Ltr, None);
719        let dry = paragraph.as_box().get_dry_layout(&mut app, constraints);
720        let dry_baseline =
721            paragraph
722                .as_box()
723                .get_dry_baseline(&mut app, constraints, TextBaseline::Alphabetic);
724
725        let (paragraph, _root) = laid_out(&mut app, "one two three", constraints);
726        assert_eq!(dry, paragraph.size(&app));
727        let baseline =
728            paragraph
729                .as_box()
730                .get_distance_to_baseline(&mut app, TextBaseline::Alphabetic, true);
731        assert_eq!(dry_baseline, baseline);
732        assert!(baseline.is_some_and(|baseline| baseline > 0.0));
733    }
734
735    #[test]
736    fn a_paragraph_sizes_itself_to_its_text_within_loose_constraints() {
737        let cell = AppCell::new();
738        let mut app = cell.borrow_mut();
739        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
740        let (paragraph, _root) = laid_out(&mut app, "Hello", loose);
741        let size = paragraph.size(&app);
742        assert!(size.width() > 0.0 && size.width() < 1000.0);
743        assert_eq!(size, paragraph.text_size(&app));
744        assert!(!paragraph.did_exceed_max_lines(&app));
745    }
746
747    #[test]
748    fn narrow_constraints_wrap_the_text() {
749        let cell = AppCell::new();
750        let mut app = cell.borrow_mut();
751        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
752        let (wide, _root) = laid_out(&mut app, "one two three four", loose);
753        let natural = wide.size(&app);
754        let narrow = BoxConstraints::new()
755            .max_width(natural.width() / 2.0)
756            .max_height(1000.0);
757        let (wrapped, _root) = laid_out(&mut app, "one two three four", narrow);
758        assert!(wrapped.size(&app).height() > natural.height());
759        assert!(wrapped.size(&app).width() <= natural.width() / 2.0);
760    }
761
762    #[test]
763    fn soft_wrap_off_lets_the_text_overflow_and_clips_it() {
764        let cell = AppCell::new();
765        let mut app = cell.borrow_mut();
766        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
767        let (wide, _root) = laid_out(&mut app, "one two three four", loose);
768        let natural = wide.size(&app);
769        install_fonts(&mut app);
770        let paragraph = RenderParagraph::new(
771            &mut app,
772            span("one two three four"),
773            TextDirection::Ltr,
774            None,
775        );
776        paragraph.set_soft_wrap(&mut app, false);
777        let root = RenderRepaintBoundary::new(&mut app, Some(paragraph.as_box()));
778        let owner = PipelineOwner::new(&mut app, None, None);
779        owner.set_root_node(&mut app, Some(root.as_object()));
780        root.schedule_initial_layout(&mut app);
781        let narrow = BoxConstraints::new()
782            .max_width(natural.width() / 2.0)
783            .max_height(1000.0);
784        root.layout(&mut app, narrow, false);
785        assert_eq!(paragraph.size(&app).width(), natural.width() / 2.0);
786        assert_eq!(
787            paragraph.text_size(&app).width(),
788            natural.width(),
789            "laid out unwrapped"
790        );
791        assert!(paragraph.get(&app).needs_clipping);
792
793        paragraph.set_overflow(&mut app, TextOverflow::Visible);
794        owner.flush_layout(&mut app);
795        assert!(!paragraph.get(&app).needs_clipping);
796    }
797
798    #[test]
799    fn ellipsis_overflow_reports_exceeded_lines() {
800        let cell = AppCell::new();
801        let mut app = cell.borrow_mut();
802        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
803        let (wide, _root) = laid_out(&mut app, "one two three four", loose);
804        let natural = wide.size(&app);
805        install_fonts(&mut app);
806        let paragraph = RenderParagraph::new(
807            &mut app,
808            span("one two three four"),
809            TextDirection::Ltr,
810            None,
811        );
812        paragraph.set_max_lines(&mut app, Some(1));
813        paragraph.set_overflow(&mut app, TextOverflow::Ellipsis);
814        let root = RenderRepaintBoundary::new(&mut app, Some(paragraph.as_box()));
815        let owner = PipelineOwner::new(&mut app, None, None);
816        owner.set_root_node(&mut app, Some(root.as_object()));
817        root.schedule_initial_layout(&mut app);
818        root.layout(
819            &mut app,
820            BoxConstraints::new()
821                .max_width(natural.width() / 2.0)
822                .max_height(1000.0),
823            false,
824        );
825        assert!(paragraph.did_exceed_max_lines(&app));
826        assert_eq!(paragraph.size(&app).height(), natural.height());
827    }
828
829    #[test]
830    fn setting_text_marks_layout_or_paint_by_the_comparison() {
831        let cell = AppCell::new();
832        let mut app = cell.borrow_mut();
833        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
834        let (paragraph, root) = laid_out(&mut app, "Hello", loose);
835        let owner = root.owner(&app).unwrap();
836        owner.flush_compositing_bits(&mut app);
837        owner.flush_paint(&mut app);
838        assert!(!paragraph.debug_needs_layout(&app));
839
840        let recolored = TextSpan::new()
841            .text("Hello")
842            .style(
843                TextStyle::new()
844                    .font_size(20.0)
845                    .color(Color::from_argb(255, 255, 0, 0)),
846            )
847            .into_span();
848        paragraph.set_text(&mut app, recolored);
849        assert!(
850            !paragraph.debug_needs_layout(&app),
851            "a color change is paint only"
852        );
853        assert!(paragraph.as_object().debug_needs_paint(&app));
854
855        paragraph.set_text(&mut app, span("Hello there"));
856        assert!(paragraph.debug_needs_layout(&app));
857    }
858
859    #[test]
860    fn painting_records_glyphs_and_hit_testing_lands_on_the_paragraph() {
861        let cell = AppCell::new();
862        let mut app = cell.borrow_mut();
863        let loose = BoxConstraints::new().max_width(1000.0).max_height(1000.0);
864        let (paragraph, root) = laid_out(&mut app, "Hello", loose);
865        let owner = root.owner(&app).unwrap();
866        owner.flush_compositing_bits(&mut app);
867        owner.flush_paint(&mut app);
868        assert!(!paragraph.as_object().debug_needs_paint(&app));
869
870        let mut result = HitTestResult::new();
871        let hit = paragraph.hit_test(
872            &mut app,
873            &mut BoxHitTestResult::wrap(&mut result),
874            Offset::new(2.0, 2.0),
875        );
876        assert!(hit);
877        assert_eq!(result.path().len(), 1);
878        let position = paragraph.get_position_for_offset(&mut app, Offset::new(0.0, 1.0));
879        assert_eq!(position.offset, 0);
880        assert_eq!(
881            paragraph.get_word_boundary(&mut app, TextPosition::new(1)),
882            TextRange::new(0, 5)
883        );
884    }
885}