Skip to main content

markdown/render/
text.rs

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