Skip to main content

markdown/render/
text.rs

1//! Inline text: flattening to runs, painting, carets and selection rects.
2
3use super::*;
4use ui::input::overdraw::Layers;
5
6/// Inline content flattened for shaping: one string, its runs, and the ranges
7/// that need painting underneath (link clicks, inline-code washes, mentions).
8///
9/// [`Self::text`] is the [`Text`] with every mention replaced by what it shows,
10/// so every range here is in its offsets; [`Self::shown`] maps the two.
11pub struct Flat {
12    pub text: SharedString,
13    pub runs: Vec<TextRun>,
14    pub links: Vec<(Range<usize>, String)>,
15    pub code: Vec<Range<usize>>,
16    /// Each run of a [`crate::MarkPaint::background`], painted over the
17    /// selection.
18    pub washes: Vec<(Range<usize>, Hsla)>,
19    pub mentions: Vec<Mention>,
20    pub shown: Shown,
21}
22
23/// A mention as it is shown: a favicon slot, then its label and title.
24#[derive(Clone, Debug)]
25pub struct Mention {
26    pub url: String,
27    /// Everything it shows, the favicon slot included.
28    pub range: Range<usize>,
29    /// The favicon slot: one em space.
30    pub icon: Range<usize>,
31    pub favicon: Option<SharedString>,
32    /// The app's mark, painted in the slot instead of a favicon.
33    pub glyph: Option<ui::icons::Icon>,
34    /// What stands in the slot while no favicon has loaded.
35    pub initial: SharedString,
36}
37
38pub use ui::input::Shown;
39
40/// What stands in a mention's favicon slot. An em space is as wide as the
41/// type is tall.
42const ICON_SLOT: &str = "\u{2003}";
43
44/// The rows a layer under the text is painted in: [`range_rects`] padded by
45/// `pad_x` and inset by `inset_y`, corners rounded by `radius`.
46#[derive(Clone, Copy, Debug)]
47struct Rows {
48    radius: f32,
49    pad_x: f32,
50    inset_y: f32,
51}
52
53impl Rows {
54    /// A mark's background.
55    const FLUSH: Self = Self {
56        radius: 0.0,
57        pad_x: 0.0,
58        inset_y: 0.0,
59    };
60    /// A selection or an annotation.
61    const ROUNDED: Self = Self {
62        radius: 2.0,
63        pad_x: 0.0,
64        inset_y: 0.0,
65    };
66    /// Inline code.
67    const CODE: Self = Self {
68        radius: INLINE_CODE_RADIUS,
69        pad_x: INLINE_CODE_PAD_X,
70        inset_y: INLINE_CODE_INSET_Y,
71    };
72}
73
74/// Marks are ranges, gpui wants consecutive runs — so cut the text at every
75/// mark boundary and ask which marks cover each piece.
76pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
77    flatten_with(text, base_weight, theme, |_| None, |_| None)
78}
79
80/// [`flatten`] with the app's own marks painted — see [`crate::MarkPaint`] —
81/// and each mention described by `preview`. A name the app does not paint
82/// reads as the text it wraps.
83pub fn flatten_with(
84    text: &Text,
85    base_weight: FontWeight,
86    theme: &Theme,
87    paint: impl Fn(&str) -> Option<crate::MarkPaint>,
88    preview: impl Fn(&str) -> Option<preview::Preview>,
89) -> Flat {
90    let mut cuts: Vec<usize> = text
91        .marks
92        .iter()
93        .flat_map(|span| [span.range.start, span.range.end])
94        .chain([0, text.text.len()])
95        .filter(|cut| *cut <= text.text.len())
96        .collect();
97    cuts.sort_unstable();
98    cuts.dedup();
99
100    // Mentions in order; one overlapping an earlier one is text.
101    let mut atoms: Vec<(Range<usize>, &str)> = text
102        .marks
103        .iter()
104        .filter_map(|span| match &span.mark {
105            Mark::Mention { url, .. } if !span.range.is_empty() => {
106                Some((span.range.clone(), url.as_str()))
107            }
108            _ => None,
109        })
110        .collect();
111    atoms.sort_by_key(|(range, _)| range.start);
112    atoms.dedup_by(|later, earlier| later.0.start < earlier.0.end);
113
114    let mut shown = String::with_capacity(text.text.len());
115    let mut runs = Vec::new();
116    let mut links: Vec<(Range<usize>, String)> = Vec::new();
117    let mut code: Vec<Range<usize>> = Vec::new();
118    let mut washes: Vec<(Range<usize>, Hsla)> = Vec::new();
119    let mut mentions: Vec<Mention> = Vec::new();
120    let mut map: Vec<(Range<usize>, Range<usize>)> = Vec::new();
121
122    for pair in cuts.windows(2) {
123        let (start, end) = (pair[0], pair[1]);
124        let atom = atoms
125            .iter()
126            .find(|(range, _)| range.start <= start && end <= range.end);
127        if atom.is_some_and(|(range, _)| range.start != start) {
128            continue;
129        }
130        let covering = text
131            .marks
132            .iter()
133            .filter(|span| span.range.start <= start && span.range.end >= end);
134
135        let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
136        let mut link = None;
137        // The app's own marks, merged in the order they cover this run: the
138        // last one to say something about a field is the one that says it.
139        let mut custom = crate::MarkPaint::default();
140        for span in covering {
141            match &span.mark {
142                Mark::Bold => bold = true,
143                Mark::Italic => italic = true,
144                Mark::Strike => strike = true,
145                Mark::Code => mono = true,
146                Mark::Mention { .. } => {}
147                Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
148                Mark::Custom(name) => {
149                    let Some(painted) = paint(name) else { continue };
150                    custom.color = painted.color.or(custom.color);
151                    custom.background = painted.background.or(custom.background);
152                    custom.weight = painted.weight.or(custom.weight);
153                    custom.italic |= painted.italic;
154                    custom.underline |= painted.underline;
155                    custom.strikethrough |= painted.strikethrough;
156                }
157            }
158        }
159        let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
160
161        let mut face = font(if mono && atom.is_none() {
162            theme.font_mono.clone()
163        } else {
164            theme.font_body.clone()
165        });
166        face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
167            FontWeight::SEMIBOLD
168        } else {
169            custom.weight.unwrap_or(base_weight)
170        };
171        face.style = if italic {
172            FontStyle::Italic
173        } else {
174            FontStyle::Normal
175        };
176        let underline = UnderlineStyle {
177            color: Some(theme.text_muted),
178            thickness: px(1.0),
179            wavy: false,
180        };
181
182        if let Some((range, url)) = atom {
183            let described = preview(url).unwrap_or_default();
184            let host = preview::host(url);
185            let own = &text.text[range.clone()];
186            let title: SharedString = match &described.title {
187                Some(title) => title.clone(),
188                None if own != *url => own.to_string().into(),
189                None => host.to_string().into(),
190            };
191            let from = shown.len();
192            let mut run = |shown: &mut String,
193                           piece: &str,
194                           color: Hsla,
195                           underline: Option<UnderlineStyle>| {
196                shown.push_str(piece);
197                runs.push(TextRun {
198                    len: piece.len(),
199                    font: face.clone(),
200                    color,
201                    background_color: None,
202                    underline,
203                    strikethrough: None,
204                });
205            };
206            run(&mut shown, ICON_SLOT, gpui::transparent_black(), None);
207            run(&mut shown, " ", theme.text, None);
208            run(&mut shown, &title, theme.text, Some(underline));
209            let to = shown.len();
210            links.push((from..to, url.to_string()));
211            mentions.push(Mention {
212                url: url.to_string(),
213                range: from..to,
214                icon: from..from + ICON_SLOT.len(),
215                favicon: described.icon,
216                glyph: described.glyph,
217                initial: host
218                    .chars()
219                    .next()
220                    .unwrap_or('?')
221                    .to_uppercase()
222                    .to_string()
223                    .into(),
224            });
225            map.push((range.clone(), from..to));
226            continue;
227        }
228
229        let (from, to) = (shown.len(), shown.len() + end - start);
230        shown.push_str(&text.text[start..end]);
231        if mono {
232            match code.last_mut() {
233                Some(range) if range.end == from => range.end = to,
234                _ => code.push(from..to),
235            }
236        }
237        if let Some(background) = custom.background {
238            match washes.last_mut() {
239                Some((range, last)) if range.end == from && *last == background => range.end = to,
240                _ => washes.push((from..to, background)),
241            }
242        }
243        if let Some(url) = &link {
244            match links.last_mut() {
245                Some((range, last)) if range.end == from && last == url => range.end = to,
246                _ => links.push((from..to, url.clone())),
247            }
248        }
249
250        runs.push(TextRun {
251            len: end - start,
252            font: face,
253            // Links stay monochrome and underlined; the accent is reserved for
254            // primary actions.
255            color: match (mono, custom.color) {
256                (_, Some(color)) => color,
257                (true, None) => theme.code_text,
258                (false, None) => theme.text,
259            },
260            background_color: None,
261            underline: (link.is_some() || custom.underline).then_some(underline),
262            strikethrough: strike.then_some(StrikethroughStyle {
263                thickness: px(1.0),
264                color: Some(theme.text_muted),
265            }),
266        });
267    }
268
269    Flat {
270        text: shown.into(),
271        runs,
272        links,
273        code,
274        washes,
275        mentions,
276        shown: Shown::new(map),
277    }
278}
279
280pub(super) fn text_element(
281    text: &Text,
282    size: f32,
283    line_height: f32,
284    weight: FontWeight,
285    overlay: Overlay,
286    theme: &Theme,
287    cx: &App,
288) -> AnyElement {
289    let flat = flatten_with(
290        text,
291        weight,
292        theme,
293        |name| crate::paint::marks::paint_of(cx, name, theme),
294        |url| crate::app::link::preview(crate::Link::new(url, overlay.base), cx),
295    );
296    painted_text(flat, text.text.len(), size, line_height, overlay, theme)
297}
298
299/// Shaped inline content with the editing overlay under it: the selection, the
300/// caret, the inline-code wash, and the layout a click resolves against.
301///
302/// Takes a [`Flat`] rather than a [`Text`] because a table has to shape every
303/// cell to measure the columns before it can paint one.
304pub(super) fn painted_text(
305    flat: Flat,
306    len: usize,
307    size: f32,
308    line_height: f32,
309    overlay: Overlay,
310    theme: &Theme,
311) -> AnyElement {
312    let (ix, part) = (overlay.block, overlay.part);
313    let shown = flat.shown.clone();
314    let caret = overlay.caret_painted().map(|offset| shown.at(offset));
315    let affinity = overlay.affinity();
316    let selected = overlay.selected(len).map(|range| shown.range(&range));
317    let span = 0..len;
318    // Only where the caret already is, and only while there is nothing to
319    // read: a hint on every empty block would be a page of grey.
320    let hint = overlay
321        .placeholder
322        // The caret's own presence, not the blink's phase — a hint that came
323        // and went twice a second would be unreadable.
324        .filter(|_| len == 0 && overlay.caret().is_some())
325        .map(|hint| {
326            div()
327                .absolute()
328                .text_color(theme.text_faint)
329                .child(hint.clone())
330        });
331    let (shape, height, hollow) = (
332        overlay.caret_shape,
333        overlay.caret_height,
334        overlay.caret_hollow(),
335    );
336    let glyph = caret
337        .filter(|_| shape.cuts_out(hollow))
338        .and_then(|offset| overlay.covered(&flat.text, offset));
339    let runs = flat.runs.clone();
340    let styled = StyledText::new(flat.text).with_runs(flat.runs);
341    let layout = styled.layout().clone();
342
343    let mentions = flat.mentions;
344    let link_ranges: Vec<Range<usize>> =
345        flat.links.iter().map(|(range, _)| range.clone()).collect();
346    let press_layout = layout.clone();
347    let painted: AnyElement = if flat.links.is_empty() {
348        styled.into_any_element()
349    } else {
350        let hovered = flat.links.clone();
351        let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
352        let jump = overlay.jump.cloned();
353        let base = overlay.base.map(Path::to_path_buf);
354        let hover_base = base.clone();
355        InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
356            .on_click(ranges, move |clicked, window, cx| {
357                if let Some(url) = urls.get(clicked) {
358                    let link = crate::Link::new(url, base.as_deref());
359                    crate::app::link::follow(link, jump.as_ref(), window, cx);
360                }
361            })
362            .hoverable_tooltip(move |at, _window, cx| {
363                let (_, url) = hovered.iter().find(|(range, _)| range.contains(&at))?;
364                Some(HoverCard::view(url, hover_base.clone(), cx))
365            })
366            .into_any_element()
367    };
368
369    // The wash is painted before the text — an earlier sibling is underneath —
370    // reading glyph geometry from the text's own layout handle. Pure paint,
371    // never part of layout.
372    let wash = theme.code_wash;
373    let code_ranges = flat.code;
374    let washes = flat.washes;
375    let (icon_color, icon_wash) = (theme.text_muted, theme.element_hover);
376    let caret_color = theme.caret;
377    let annotated: Vec<_> = overlay
378        .annotated(len, theme)
379        .into_iter()
380        .map(|(range, wash)| (shown.range(&range), wash))
381        .collect();
382    let layouts = overlay.layouts.cloned();
383    // One quad per visual row of each, so a wrapped selection is a stack of
384    // rows rather than a box around all of them.
385    let mut layers = Layers::new();
386    for (range, color) in washes {
387        layers = layers.wash(range, color, Rows::FLUSH);
388    }
389    for (range, color) in annotated {
390        layers = layers.wash(range, color, Rows::ROUNDED);
391    }
392    for range in code_ranges {
393        layers = layers.wash(range, wash, Rows::CODE);
394    }
395    if let Some(range) = selected {
396        layers = layers.select(range, theme.selection, Rows::ROUNDED);
397    }
398    let overdraws = ui::input::overdraw::overdraws(&runs, theme.bg, &layers);
399    // Over the text, which a solid block hides: the glyph it covers is painted
400    // again on top of it.
401    let caret_layout = layout.clone();
402    let cover = caret.map_or(caret_color, |offset| {
403        ui::input::caret::cover(&runs, offset, theme.bg, &layers, caret_color)
404    });
405    let over = canvas(
406        |_, _, _| (),
407        move |_, _, window, cx| {
408            ui::input::overdraw::paint(
409                caret_layout.line_layouts().iter().map(|line| &**line),
410                caret_layout.bounds().origin,
411                caret_layout.line_height(),
412                px(2.0),
413                &overdraws,
414                window,
415            );
416            let Some(offset) = caret else {
417                return;
418            };
419            let face = font(Theme::of(cx).font_body.clone());
420            paint_caret(
421                &caret_layout,
422                offset,
423                affinity,
424                glyph.as_ref(),
425                CaretPaint {
426                    shape,
427                    height,
428                    hollow,
429                    color: caret_color,
430                    cover,
431                    size,
432                    face,
433                },
434                window,
435            );
436        },
437    )
438    .absolute()
439    .size_full();
440    let underlay = canvas(
441        {
442            let mentions = mentions.clone();
443            move |_, window, cx| {
444                mentions
445                    .iter()
446                    .map(|mention| {
447                        let favicon = mention.favicon.clone()?;
448                        let source = gpui::Resource::Uri(favicon.to_string().into());
449                        window
450                            .use_asset::<gpui::ImgResourceLoader>(&source, cx)?
451                            .ok()
452                    })
453                    .collect::<Vec<_>>()
454            }
455        },
456        move |_, favicons, window, cx| {
457            if let Some(layouts) = &layouts {
458                layouts.record(ix, part, span.clone(), layout.clone(), shown.clone());
459            }
460            // Under the glyphs.
461            for (range, color, rows) in layers.painted() {
462                for rect in range_rects(&layout, range, rows.pad_x, rows.inset_y) {
463                    window.paint_quad(quad(
464                        rect,
465                        px(rows.radius),
466                        color,
467                        px(0.0),
468                        gpui::transparent_black(),
469                        BorderStyle::default(),
470                    ));
471                }
472            }
473            for (mention, favicon) in mentions.iter().zip(favicons) {
474                let Some(slot) = range_rects(&layout, &mention.icon, 0.0, 0.0).pop() else {
475                    continue;
476                };
477                let side = slot.size.width.min(px(size));
478                let icon = Bounds::new(
479                    slot.origin + point(px(0.0), (slot.size.height - side) / 2.0),
480                    gpui::size(side, side),
481                );
482                let radius = gpui::Corners::all(side / 4.0);
483                if let Some(data) = mention.glyph.as_ref().and_then(|glyph| glyph.data()) {
484                    let path = SharedString::from(format!("markdown-glyph-{:p}", data.as_ptr()));
485                    window
486                        .paint_svg(
487                            icon,
488                            path,
489                            Some(data),
490                            gpui::TransformationMatrix::unit(),
491                            icon_color,
492                            cx,
493                        )
494                        .ok();
495                    continue;
496                }
497                if let Some(favicon) = favicon {
498                    window
499                        .paint_image(icon, icon, radius, favicon, 0, false)
500                        .ok();
501                    continue;
502                }
503                window.paint_quad(quad(
504                    icon,
505                    side / 4.0,
506                    icon_wash,
507                    px(0.0),
508                    gpui::transparent_black(),
509                    BorderStyle::default(),
510                ));
511                let font = font(Theme::of(cx).font_body.clone());
512                let letter = window.text_system().shape_line(
513                    mention.initial.clone(),
514                    side * 0.55,
515                    &[TextRun {
516                        len: mention.initial.len(),
517                        font,
518                        color: icon_color,
519                        background_color: None,
520                        underline: None,
521                        strikethrough: None,
522                    }],
523                    None,
524                );
525                let at = point(icon.center().x - letter.width / 2.0, icon.origin.y);
526                letter
527                    .paint(at, side, gpui::TextAlign::Left, None, window, cx)
528                    .ok();
529            }
530        },
531    )
532    .absolute()
533    .size_full();
534
535    div()
536        .text_size(px(size))
537        .line_height(px(line_height))
538        .relative()
539        .child(underlay)
540        .children(hint)
541        .child(painted)
542        .child(over)
543        // A shift-press on a link is the link's: the host follows it on the
544        // release, and the editor around would take the press as extending
545        // the selection.
546        .on_mouse_down(MouseButton::Left, move |event, _, cx| {
547            let on_link = press_layout
548                .index_for_position(event.position)
549                .is_ok_and(|at| link_ranges.iter().any(|range| range.contains(&at)));
550            if event.modifiers.shift && on_link {
551                cx.stop_propagation();
552            }
553        })
554        .into_any_element()
555}
556
557/// The caret's quad: the text's own size, centred in the line box.
558///
559/// The leading is not the caret's to take. A document is set with air around
560/// its lines, and a caret filling all of it reads as a second, larger font
561/// standing where the text should be.
562pub(super) fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
563    let inset = (line_height - px(size)) / 2.0;
564    Bounds::new(
565        head + point(px(0.0), inset),
566        gpui::size(px(CARET_WIDTH), px(size)),
567    )
568}
569
570/// The grapheme after `offset` on its line, if there is one.
571pub(super) fn glyph_at(text: &str, offset: usize) -> Option<Range<usize>> {
572    (offset < text.len() && text.is_char_boundary(offset) && !text[offset..].starts_with('\n'))
573        .then(|| offset..ui::input::next_boundary(text, offset))
574}
575
576/// How a caret is drawn, apart from where.
577pub(super) struct CaretPaint {
578    pub shape: ui::input::CaretShape,
579    pub height: ui::input::CaretHeight,
580    pub hollow: bool,
581    pub color: Hsla,
582    /// The colour of the glyph a solid block covers.
583    pub cover: Hsla,
584    /// The text's font size, in pixels.
585    pub size: f32,
586    /// The font a wide caret measures its empty slot in.
587    pub face: gpui::Font,
588}
589
590/// Where a caret at `offset` paints in `layout`, on the row `affinity` names
591/// at a soft wrap.
592pub(super) fn caret_position(
593    layout: &TextLayout,
594    offset: usize,
595    affinity: Affinity,
596) -> Option<Point<Pixels>> {
597    if affinity == Affinity::Downstream
598        && let Some(start) = wrapped_row_start(layout, offset)
599    {
600        return Some(start);
601    }
602    layout.position_for_index(offset)
603}
604
605/// The origin of the row a soft wrap at `offset` starts, or `None` when no
606/// wrap falls there.
607fn wrapped_row_start(layout: &TextLayout, offset: usize) -> Option<Point<Pixels>> {
608    let line_height = layout.line_height();
609    let mut origin = layout.bounds().origin;
610    let mut line_start = 0;
611    for line in layout.line_layouts() {
612        if offset > line_start + line.len() {
613            origin.y += line.size(line_height).height;
614            line_start += line.len() + 1;
615            continue;
616        }
617        let shaped = &line.unwrapped_layout;
618        let row = line.wrap_boundaries().iter().position(|wrap| {
619            line_start + shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index == offset
620        })?;
621        return Some(origin + point(px(0.0), line_height * (row + 1) as f32));
622    }
623    None
624}
625
626/// Paints the caret at `offset` in `layout`, over the text. A `glyph` is what
627/// a solid block covers, on whichever row it shaped, painted again over it.
628pub(super) fn paint_caret(
629    layout: &TextLayout,
630    offset: usize,
631    affinity: Affinity,
632    glyph: Option<&Range<usize>>,
633    paint: CaretPaint,
634    window: &mut Window,
635) {
636    let line_height = layout.line_height();
637    let covered = glyph.and_then(|glyph| range_rects(layout, glyph, 0.0, 0.0).into_iter().next());
638    let (head, width) = match covered {
639        Some(rect) => (rect.origin, rect.size.width),
640        None => {
641            let Some(head) = caret_position(layout, offset, affinity) else {
642                return;
643            };
644            let width = match paint.shape {
645                ui::input::CaretShape::Bar => px(0.0),
646                _ => caret_advance(layout, offset).unwrap_or_else(|| {
647                    ui::input::caret::zero_width(paint.face, px(paint.size), window)
648                }),
649            };
650            (head, width)
651        }
652    };
653    window.paint_quad(paint.shape.quad(
654        caret_quad(head, paint.size, line_height),
655        line_height,
656        paint.height,
657        width,
658        paint.color,
659        paint.hollow,
660    ));
661    if let Some(glyph) = glyph
662        && covered.is_some()
663        && let Some((line, range)) = line_of(layout, glyph)
664    {
665        ui::input::caret::paint_covered(
666            &line.unwrapped_layout,
667            &range,
668            head,
669            line_height,
670            paint.cover,
671            window,
672        );
673    }
674}
675
676/// The shaped line `range` falls in, and `range` within that line.
677fn line_of(
678    layout: &TextLayout,
679    range: &Range<usize>,
680) -> Option<(Arc<gpui::WrappedLineLayout>, Range<usize>)> {
681    let mut start = 0;
682    for line in layout.line_layouts() {
683        let end = start + line.len();
684        if range.start <= end {
685            return (range.end <= end).then(|| (line, range.start - start..range.end - start));
686        }
687        start = end + 1;
688    }
689    None
690}
691
692/// The rectangles a byte range occupies, one per visual row.
693pub(super) fn range_rects(
694    layout: &gpui::TextLayout,
695    range: &Range<usize>,
696    pad_x: f32,
697    inset_y: f32,
698) -> Vec<Bounds<Pixels>> {
699    let mut rects = Vec::new();
700    let line_height = layout.line_height();
701    let mut origin = layout.bounds().origin;
702    let mut line_start = 0;
703    for line in layout.line_layouts() {
704        let shaped = &line.unwrapped_layout;
705        // A wrap boundary index is both the end of one row and the start of
706        // the next.
707        let row_ends = line
708            .wrap_boundaries()
709            .iter()
710            .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
711            .chain([line.len()]);
712        let mut row_start = 0;
713        for (row, row_end) in row_ends.enumerate() {
714            let from = range
715                .start
716                .saturating_sub(line_start)
717                .clamp(row_start, row_end);
718            let to = range.end.saturating_sub(line_start).min(row_end);
719            let row_x = shaped.x_for_index(row_start);
720            let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
721            if from < to && right > left {
722                rects.push(Bounds::new(
723                    origin
724                        + point(
725                            left - row_x - px(pad_x),
726                            line_height * row as f32 + px(inset_y),
727                        ),
728                    size(
729                        right - left + px(2.0 * pad_x),
730                        line_height - px(2.0 * inset_y),
731                    ),
732                ));
733            }
734            row_start = row_end;
735        }
736        origin.y += line.size(line_height).height;
737        // The newline between two lines is a byte of the text and of neither.
738        line_start += line.len() + 1;
739    }
740    rects
741}
742
743/// Resolve the advance in the same shaped line that owns the caret position.
744pub(super) fn caret_advance(layout: &TextLayout, offset: usize) -> Option<Pixels> {
745    let text = layout.text();
746    let mut start = 0;
747    for line in layout.line_layouts() {
748        let end = start + line.len();
749        if offset <= end {
750            return ui::input::caret::character_advance(&line, &text[start..end], offset - start);
751        }
752        start = end + 1;
753    }
754    None
755}