Skip to main content

markdown/
render.rs

1//! [`Doc`] → gpui elements.
2//!
3//! Numbers drive layout (sizes, line heights, paddings — the constants here);
4//! colors are paint, read from [`Theme`]. Blocks are a flat list, so nesting is
5//! left padding rather than nested containers, and the gap between two blocks
6//! is decided by the pair: list items sit tight, everything else breathes.
7//!
8//! Ported from zeronsh/comet (MIT) and rebuilt against the flat block model.
9
10use std::{cell::RefCell, ops::Range, rc::Rc};
11
12use gpui::{
13    AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14    InteractiveText, ObjectFit, Pixels, Point, SharedString, StrikethroughStyle, StyledImage as _,
15    StyledText, TextLayout, TextRun, UnderlineStyle, Window, canvas, div, font, img, point,
16    prelude::*, px, quad, size,
17};
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{
21    block,
22    doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
23    layout::Layout,
24    preview,
25    select::{Cursor, Selection},
26    typography::Typography,
27};
28
29/// Space between two ordinary blocks, and the tighter space inside a list.
30const BLOCK_GAP: f32 = 12.0;
31const LIST_GAP: f32 = 4.0;
32/// One indent level. Wide enough to clear a marker and read as a level.
33const INDENT_WIDTH: f32 = 22.0;
34/// The marker column of a list row.
35const MARKER_WIDTH: f32 = 18.0;
36const MARKER_GAP: f32 = 8.0;
37/// What a fence holds its code in, inside its border.
38const CODE_PADDING_X: f32 = 12.0;
39const CODE_PADDING_Y: f32 = 10.0;
40/// What a fence with no info string calls itself, in its header and in a
41/// picker — one spelling, so the label and the menu row cannot disagree.
42pub const PLAIN_LANGUAGE: &str = "Plain";
43/// Width of the caret. Wider than a hairline, because it has to read at a
44/// glance against the text it sits in.
45const CARET_WIDTH: f32 = 1.5;
46/// Inline code's wash is a rounded quad painted under the glyphs: a run's
47/// `background_color` can only ever be a square box.
48const INLINE_CODE_RADIUS: f32 = 4.5;
49const INLINE_CODE_PAD_X: f32 = 2.0;
50const INLINE_CODE_INSET_Y: f32 = 2.0;
51/// A mention's chip — the same quad-under-glyphs trick as inline code, with
52/// more room and an outline so the two do not read as the same thing.
53const CHIP_PAD_X: f32 = 4.0;
54const CHIP_INSET_Y: f32 = 1.0;
55/// A chip with a block to itself is a real element rather than a wash, so it
56/// has room for the favicon the inline one cannot hold.
57const CHIP_BLOCK_PAD_X: f32 = 8.0;
58const CHIP_BLOCK_PAD_Y: f32 = 3.0;
59const CHIP_ICON: f32 = 15.0;
60/// Bookmark metrics. Notion's card: 180px of image beside the text, and a
61/// height that fits a title, two lines of blurb and a footer. A cover moves
62/// that image above the text and gives it the card's full width.
63const CARD_HEIGHT: f32 = 116.0;
64const CARD_IMAGE_WIDTH: f32 = 180.0;
65const CARD_COVER_HEIGHT: f32 = 200.0;
66const CARD_PADDING: f32 = 14.0;
67const CARD_BORDER: f32 = 1.0;
68const CARD_ICON: f32 = 16.0;
69const CARD_COVER: f32 = 44.0;
70/// Image metrics.
71const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
72const CAPTION_GAP: f32 = 4.0;
73/// What an image with no URL yet says, and what its caption says while empty.
74const IMAGE_EMPTY: &str = "Add an image";
75const CAPTION_HINT: &str = "Write a caption";
76/// Table metrics. The design is frameless: hairlines between rows are the only
77/// chrome — no outer box, no header fill, no rounding.
78const TABLE_CELL_PADDING: f32 = 12.0;
79const TABLE_DIVIDER: f32 = 1.0;
80/// Floor for a column's max-content share, so a short column ("1k") beside a
81/// prose column keeps a readable width.
82const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
83/// Narrowest a column wraps down to before the table scrolls instead.
84const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
85
86/// What an image's one authored string is doing on the page.
87///
88/// SwiftUI keeps three things apart — `accessibilityLabel` for a reader that
89/// cannot see, `.help` for the pointer, and a caption you compose out of a
90/// `Text` under the picture. Markdown has one slot for all three, so this says
91/// which of them it is playing here rather than in the document, where it is
92/// the same string either way.
93///
94/// A named choice rather than a `bool`, so a surface that wants a third answer
95/// gets a variant instead of a second flag.
96#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
97pub enum Caption {
98    /// Under the picture, where a caret can sit in it. The editor's shape.
99    #[default]
100    Shown,
101    /// Kept by the document and painted nowhere — a picture on its own.
102    Hidden,
103}
104
105/// A range the caller wants washed, and which of the three washes it gets.
106///
107/// A comment thread is what asks for this, and none of what it *says* is here:
108/// the caller keeps the thread and hands over the range, the way it hands over
109/// a [`crate::Preview`]. A closed set rather than a color, so the environment
110/// keeps deciding the paint.
111#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
112pub enum Annotation {
113    /// A thread still waiting on someone.
114    #[default]
115    Open,
116    /// Answered, and kept for the record.
117    Resolved,
118    /// The one whose thread the reader has in front of them.
119    Active,
120}
121
122impl Annotation {
123    fn wash(self, theme: &Theme) -> Hsla {
124        match self {
125            Self::Open => theme.warning.opacity(0.20),
126            Self::Resolved => theme.warning.opacity(0.08),
127            Self::Active => theme.warning.opacity(0.38),
128        }
129    }
130}
131
132/// What an editor paints over a document.
133///
134/// One value rather than six parameters, and the reason it is public: the
135/// caret, the selection, the comment washes and the layout sink all arrive
136/// together or not at all, and a read-only [`render`] sets none of them.
137#[derive(Clone)]
138pub struct Editing<'a> {
139    /// The caret and what it has selected. `None` paints neither — a document
140    /// nobody is editing.
141    pub selection: Option<Selection>,
142    /// The blink's lit half. A caret painted on every frame reads as frozen,
143    /// and the phase belongs to whoever owns the focus.
144    pub caret_on: bool,
145    /// Filled as the document paints, for a caller resolving clicks against it.
146    pub layouts: Option<&'a BlockLayouts>,
147    /// Ranges washed under the text, in the order given.
148    pub annotations: &'a [(Selection, Annotation)],
149    /// Shown on the caret's block while it holds nothing.
150    pub placeholder: Option<SharedString>,
151    pub caption: Caption,
152    /// What to set the document in. `None` takes the installed
153    /// [`Typography`] — a caller sizing one document apart from the rest
154    /// passes [`Typography::scaled`].
155    pub typography: Option<Typography>,
156}
157
158impl Default for Editing<'_> {
159    fn default() -> Self {
160        Self {
161            selection: None,
162            // Lit, so that a caller setting a selection and nothing else gets a
163            // caret rather than a mystery.
164            caret_on: true,
165            layouts: None,
166            annotations: &[],
167            placeholder: None,
168            caption: Caption::default(),
169            typography: None,
170        }
171    }
172}
173
174/// Where each block's text landed, recorded as it painted.
175///
176/// A caret has to be placeable by pointer, and only paint knows where a glyph
177/// ended up. An editor hands one of these in, the renderer fills it, and the
178/// next click resolves against it. Read-only callers pass nothing and pay
179/// nothing.
180#[derive(Clone, Default)]
181pub struct BlockLayouts(Rc<RefCell<Frames>>);
182
183#[derive(Default)]
184struct Frames {
185    texts: Vec<Painted>,
186    /// Each block's whole box, which a text layout does not give: a rule and
187    /// an image hold no text at all, and a gutter handle still has to find them.
188    blocks: Vec<(usize, Bounds<Pixels>)>,
189    /// A fenced block's language label, which a host may want to hang a
190    /// picker on.
191    languages: Vec<(usize, Bounds<Pixels>)>,
192    /// An image block's picture, which is not its block: the block runs the
193    /// full column and carries the caption, and a resize handle belongs on the
194    /// edge of the picture itself.
195    pictures: Vec<(usize, Bounds<Pixels>)>,
196}
197
198/// One shaped run and the slice of its part it covers.
199///
200/// A paragraph is one entry over all of its text; a code block is one entry per
201/// line. The range is what lets both resolve a click the same way — the layout
202/// answers in its own coordinates and the base puts the answer back into the
203/// part's.
204struct Painted {
205    block: usize,
206    part: Part,
207    range: Range<usize>,
208    layout: TextLayout,
209}
210
211impl BlockLayouts {
212    /// The position under `point`.
213    ///
214    /// Falls back to the nearest text vertically, so clicking the margin
215    /// beside a line — or below the last one — still lands somewhere useful
216    /// rather than doing nothing.
217    pub fn hit(&self, point: Point<Pixels>) -> Option<Cursor> {
218        let entries = &self.0.borrow().texts;
219        let cursor = |painted: &Painted| {
220            let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point);
221            Cursor::new(
222                painted.block,
223                painted.part,
224                painted.range.start + offset.min(painted.range.len()),
225            )
226        };
227        if let Some(painted) = entries
228            .iter()
229            .find(|painted| painted.layout.bounds().contains(&point))
230        {
231            return Some(cursor(painted));
232        }
233        entries
234            .iter()
235            .min_by_key(|painted| {
236                let bounds = painted.layout.bounds();
237                let above = (bounds.origin.y - point.y).abs();
238                let below = (bounds.origin.y + bounds.size.height - point.y).abs();
239                f32::from(above.min(below)) as i64
240            })
241            .map(cursor)
242    }
243
244    /// Where a position painted last frame, and how tall its line is.
245    ///
246    /// Vertical motion is geometry rather than arithmetic on line numbers, so
247    /// a wrapped row and a hard newline are the same case and neither needs
248    /// counting — the rule `ui::TextField` arrived at.
249    pub fn position(&self, at: Cursor) -> Option<(Point<Pixels>, Pixels)> {
250        let entries = &self.0.borrow().texts;
251        let painted = entries.iter().find(|painted| {
252            painted.block == at.block
253                && painted.part == at.part
254                && painted.range.start <= at.offset
255                && at.offset <= painted.range.end
256        })?;
257        let point = painted
258            .layout
259            .position_for_index(at.offset - painted.range.start)?;
260        Some((point, painted.layout.line_height()))
261    }
262
263    /// The painted rows of a range, in document order — what a bar centred over
264    /// a selection is placed against, and what a highlight of a caller's own is
265    /// drawn from.
266    ///
267    /// One rect per visual row rather than one box: a selection that wraps or
268    /// crosses blocks has no single box, and a caller given one would paint
269    /// over the gaps.
270    pub fn rects(&self, selection: Selection) -> Vec<Bounds<Pixels>> {
271        let (start, end) = selection.ordered();
272        self.0
273            .borrow()
274            .texts
275            .iter()
276            .filter_map(|painted| {
277                let here = Cursor::new(painted.block, painted.part, 0);
278                let (from, to) = (
279                    Cursor::new(start.block, start.part, 0),
280                    Cursor::new(end.block, end.part, 0),
281                );
282                if here < from || here > to {
283                    return None;
284                }
285                // The offsets only matter at the two ends: in between, the
286                // whole of the text is covered.
287                let len = painted.range.len();
288                let first = if here == from { start.offset } else { 0 };
289                let last = if here == to { end.offset } else { usize::MAX };
290                let range = first.saturating_sub(painted.range.start).min(len)
291                    ..last.saturating_sub(painted.range.start).min(len);
292                (range.start < range.end).then(|| range_rects(&painted.layout, &range, 0.0, 0.0))
293            })
294            .flatten()
295            .collect()
296    }
297
298    /// The position one painted row above or below `at`, and the row it landed
299    /// on. Walks the recorded runs in paint order — which is document order.
300    ///
301    /// Two things make this refuse to be a hit test. The gap between blocks
302    /// belongs to no run, so a probe there answers with whichever run is
303    /// nearest — and at a boundary that is the block being *left*, whose bottom
304    /// edge is zero pixels away while the next block's top is a whole gap. And
305    /// `from` is passed in rather than derived from `at`, because an offset at
306    /// a soft wrap belongs to two rows and `position_for_index` always answers
307    /// with the first: derive it and every step down recomputes the same row.
308    pub fn step_row(
309        &self,
310        at: Cursor,
311        from: Point<Pixels>,
312        down: bool,
313    ) -> Option<(Cursor, Pixels)> {
314        let entries = &self.0.borrow().texts;
315        let ix = entries.iter().position(|painted| {
316            painted.block == at.block
317                && painted.part == at.part
318                && painted.range.start <= at.offset
319                && at.offset <= painted.range.end
320        })?;
321        let here = &entries[ix];
322        let line = here.layout.line_height();
323        let index_at = |painted: &Painted, y: Pixels| {
324            let (Ok(offset) | Err(offset)) = painted.layout.index_for_position(point(from.x, y));
325            (
326                Cursor::new(
327                    painted.block,
328                    painted.part,
329                    painted.range.start + offset.min(painted.range.len()),
330                ),
331                y,
332            )
333        };
334
335        // A wrapped paragraph is one run holding several rows, so try to stay
336        // inside it before looking for a neighbour.
337        let bounds = here.layout.bounds();
338        let target = if down { from.y + line } else { from.y - line };
339        if target >= bounds.origin.y && target < bounds.origin.y + bounds.size.height {
340            return Some(index_at(here, target));
341        }
342
343        let next = match down {
344            true => entries.get(ix + 1)?,
345            false => entries.get(ix.checked_sub(1)?)?,
346        };
347        // Enter the neighbour on the row facing the one just left.
348        let bounds = next.layout.bounds();
349        let row = match down {
350            true => bounds.origin.y,
351            false => bounds.origin.y + bounds.size.height - next.layout.line_height(),
352        };
353        Some(index_at(next, row))
354    }
355
356    /// Whether `point` is inside painted text.
357    ///
358    /// [`Self::hit`] answers with the nearest run wherever it is asked, which
359    /// is what a click wants and what a *pointer* must not have: an I-beam
360    /// belongs where a caret would land, not everywhere an editor's box
361    /// reaches.
362    pub fn over_text(&self, point: Point<Pixels>) -> bool {
363        self.0
364            .borrow()
365            .texts
366            .iter()
367            .any(|painted| painted.layout.bounds().contains(&point))
368    }
369
370    /// The block under `point`, for a gutter handle and a drop target.
371    pub fn block_at(&self, point: Point<Pixels>) -> Option<usize> {
372        let blocks = &self.0.borrow().blocks;
373        blocks
374            .iter()
375            .find(|(_, bounds)| bounds.contains(&point))
376            .or_else(|| {
377                blocks.iter().min_by_key(|(_, bounds)| {
378                    let above = (bounds.origin.y - point.y).abs();
379                    let below = (bounds.origin.y + bounds.size.height - point.y).abs();
380                    f32::from(above.min(below)) as i64
381                })
382            })
383            .map(|(ix, _)| *ix)
384    }
385
386    /// Where a block's first painted row sits, and how tall that row is — what
387    /// a mark in the gutter has to line up with.
388    ///
389    /// [`Self::block_bounds`] is not that: it spans every row the block holds,
390    /// and a heading's single row is taller than a paragraph's, so anything
391    /// placed from the top of the box rides above the text it points at.
392    /// `None` for a block that paints no text at all, a rule being the one
393    /// that does.
394    pub fn first_row(&self, ix: usize) -> Option<(Pixels, Pixels)> {
395        let texts = &self.0.borrow().texts;
396        let painted = texts.iter().find(|painted| painted.block == ix)?;
397        Some((
398            painted.layout.bounds().origin.y,
399            painted.layout.line_height(),
400        ))
401    }
402
403    /// Where a block painted last frame, in window coordinates.
404    pub fn block_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
405        self.0
406            .borrow()
407            .blocks
408            .iter()
409            .find(|(block, _)| *block == ix)
410            .map(|(_, bounds)| *bounds)
411    }
412
413    /// Where a fenced block's language label painted — the box a host hangs its
414    /// picker on. Recorded rather than derived: the word's width is the text
415    /// system's answer, and padding arithmetic would be wrong the first time
416    /// any of it changed.
417    pub fn language_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
418        self.0
419            .borrow()
420            .languages
421            .iter()
422            .find(|(block, _)| *block == ix)
423            .map(|(_, bounds)| *bounds)
424    }
425
426    /// Where an image block's picture painted, which
427    /// [`BlockLayouts::block_bounds`] does not give: that box spans the column
428    /// and takes in the caption, so a handle placed from it sits off the edge
429    /// of any picture narrower than the page.
430    pub fn picture_bounds(&self, ix: usize) -> Option<Bounds<Pixels>> {
431        self.0
432            .borrow()
433            .pictures
434            .iter()
435            .find(|(block, _)| *block == ix)
436            .map(|(_, bounds)| *bounds)
437    }
438
439    fn record(&self, block: usize, part: Part, range: Range<usize>, layout: TextLayout) {
440        self.0.borrow_mut().texts.push(Painted {
441            block,
442            part,
443            range,
444            layout,
445        });
446    }
447
448    fn record_block(&self, ix: usize, bounds: Bounds<Pixels>) {
449        self.0.borrow_mut().blocks.push((ix, bounds));
450    }
451
452    fn record_language(&self, ix: usize, bounds: Bounds<Pixels>) {
453        self.0.borrow_mut().languages.push((ix, bounds));
454    }
455
456    fn record_picture(&self, ix: usize, bounds: Bounds<Pixels>) {
457        self.0.borrow_mut().pictures.push((ix, bounds));
458    }
459
460    fn clear(&self) {
461        let mut frames = self.0.borrow_mut();
462        frames.texts.clear();
463        frames.blocks.clear();
464        frames.languages.clear();
465        frames.pictures.clear();
466    }
467}
468
469/// What the editor needs painted into one text: which text it is, where the
470/// caret sits, and where to record the layout a click resolves against.
471///
472/// One bundle rather than four parameters threaded through every block arm —
473/// a read-only render builds it with no caret and no sink, and pays nothing.
474#[derive(Clone, Copy)]
475struct Overlay<'a> {
476    block: usize,
477    part: Part,
478    selection: Option<Selection>,
479    caret_on: bool,
480    layouts: Option<&'a BlockLayouts>,
481    /// Ranges washed under the text, in the order the caller gave them.
482    annotations: &'a [(Selection, Annotation)],
483    /// Shown on the caret's block while it holds nothing. The renderer is the
484    /// only thing that knows where that text sits, so the string comes to it.
485    placeholder: Option<&'a SharedString>,
486    caption: Caption,
487}
488
489impl<'a> Overlay<'a> {
490    fn at(self, part: Part) -> Self {
491        Self { part, ..self }
492    }
493
494    fn here(&self) -> Cursor {
495        Cursor::new(self.block, self.part, 0)
496    }
497
498    /// The caret to paint: where it is, and only on the blink's lit half.
499    ///
500    /// Separate from [`Self::caret`] because the blink must not reach anything
501    /// but the quad — a block whose paint depends on holding the caret would
502    /// otherwise swap itself out twice a second.
503    fn caret_painted(&self) -> Option<usize> {
504        self.caret_on.then(|| self.caret()).flatten()
505    }
506
507    /// The caret's byte offset, if the head is in *this* text.
508    fn caret(&self) -> Option<usize> {
509        self.selection
510            .map(|selection| selection.head)
511            .filter(|head| head.block == self.block && head.part == self.part)
512            .map(|head| head.offset)
513    }
514
515    /// The selected slice of this text, clipped to it.
516    fn selected(&self, len: usize) -> Option<Range<usize>> {
517        self.clip(self.selection?, len)
518    }
519
520    /// The annotated slices of this text, already resolved to their paint —
521    /// the wash goes into a `move` closure that the theme does not travel into.
522    fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
523        self.annotations
524            .iter()
525            .filter_map(|(range, kind)| Some((self.clip(*range, len)?, kind.wash(theme))))
526            .collect()
527    }
528
529    /// A range clipped to this text, and `None` when it does not reach it.
530    ///
531    /// The comparison is on `(block, part)` alone: a range covers this text
532    /// entirely when it starts before and ends after, and the offsets only
533    /// matter at the two ends.
534    fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
535        if selection.is_collapsed() {
536            return None;
537        }
538        let (start, end) = selection.ordered();
539        let here = self.here();
540        let (first, last) = (
541            Cursor::new(start.block, start.part, 0),
542            Cursor::new(end.block, end.part, 0),
543        );
544        if here < first || here > last {
545            return None;
546        }
547        let from = if here == first { start.offset } else { 0 };
548        let to = if here == last { end.offset } else { len };
549        (from < to).then_some(from..to.min(len))
550    }
551
552    /// Whether a block painting something a caret cannot enter — a rule, a
553    /// picture — falls inside the selection, and so should show that it is
554    /// going to be taken.
555    fn covers_block(&self) -> bool {
556        let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
557            return false;
558        };
559        let (start, end) = selection.ordered();
560        start.block < self.block && self.block < end.block
561    }
562}
563
564/// Parse and render in one step — the common case for read-only content.
565pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
566    let doc = crate::parse_with(source, &crate::Marks::of(cx));
567    render(&doc, Caption::default(), window, cx)
568}
569
570/// Render a document.
571pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
572    render_with(
573        doc,
574        Editing {
575            caption,
576            ..Editing::default()
577        },
578        window,
579        cx,
580    )
581}
582
583/// Render a document with a caret and a selection in it.
584///
585/// Both are paint-time concerns and nothing else: they read their positions off
586/// the shaped text's own layout handle, the same way the inline-code wash does,
587/// so nothing about layout depends on where the caret sits. An editor supplies
588/// the selection and owns the focus and the keys; painting a caret and a few
589/// quads is not worth a second renderer.
590pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
591    let Editing {
592        selection,
593        caret_on,
594        layouts,
595        annotations,
596        placeholder,
597        caption,
598        typography,
599    } = editing;
600    // Refilled every frame, in paint order — and emptied in *prepaint*, not
601    // here. An editor reads last frame's positions while building this frame's
602    // tree (a menu anchored at the caret, a handle beside a block), and
603    // clearing at build time takes them away before it can. Placed first in the
604    // column so it runs ahead of every recorder below it.
605    let reset = layouts.map(|layouts| {
606        let layouts = layouts.clone();
607        canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
608            .absolute()
609            .size(px(0.0))
610    });
611    // Cloned once so the theme is readable while `cx` stays free for the
612    // element state the copy button needs.
613    let theme = Theme::of(cx).clone();
614    let typography = typography.unwrap_or_else(|| Typography::of(cx));
615    let mut column = div().flex().flex_col().children(reset);
616
617    for (ix, block) in doc.blocks.iter().enumerate() {
618        let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
619            None => 0.0,
620            Some(previous) if tight(previous, block) => LIST_GAP,
621            Some(_) => BLOCK_GAP,
622        };
623        let overlay = Overlay {
624            block: ix,
625            part: Part::Body,
626            selection,
627            caret_on,
628            layouts,
629            annotations,
630            placeholder: placeholder.as_ref(),
631            caption,
632        };
633        // The block's own box, recorded for a gutter handle and a drop target.
634        // A rule and an image hold no text, so a layout would not find them.
635        let frame = layouts.map(|layouts| {
636            let layouts = layouts.clone();
637            canvas(
638                move |bounds, _, _| layouts.record_block(ix, bounds),
639                |_, _, _, _| (),
640            )
641            .absolute()
642            .size_full()
643        });
644        column = column.child(
645            // The indent sits on the outside and the recorder on the inside,
646            // so what is recorded is the box the block's text actually
647            // occupies. Recorded outside the padding, every level answered
648            // with the same left edge, and a gutter handle placed from it
649            // stayed at the margin while the block it belongs to moved right.
650            div()
651                .mt(px(gap))
652                .pl(px(block.indent as f32 * INDENT_WIDTH))
653                .child(
654                    div()
655                        .w_full()
656                        .relative()
657                        .children(frame)
658                        // What a caret cannot enter still has to show it is
659                        // inside the selection, or a rule between two
660                        // paragraphs looks untouched right up until it
661                        // disappears.
662                        .when(overlay.covers_block() && block.opaque(), |el| {
663                            el.rounded(px(4.0)).bg(theme.selection)
664                        })
665                        .child(block_element(
666                            block,
667                            overlay,
668                            &typography,
669                            &theme,
670                            window,
671                            cx,
672                        )),
673                ),
674        );
675    }
676
677    column.into_any_element()
678}
679
680/// Whether two adjacent blocks belong to the same list and should sit close.
681fn tight(previous: &Block, next: &Block) -> bool {
682    let marker = |block: &Block| {
683        matches!(
684            block.kind,
685            BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
686        )
687    };
688    marker(previous) && (marker(next) || next.indent > previous.indent)
689}
690
691fn block_element(
692    block: &Block,
693    overlay: Overlay,
694    typography: &Typography,
695    theme: &Theme,
696    window: &mut Window,
697    cx: &mut App,
698) -> AnyElement {
699    let body = overlay.at(Part::Body);
700    match &block.kind {
701        BlockKind::Paragraph(text) => text_element(
702            text,
703            typography.body.size(),
704            typography.body.line_height(),
705            FontWeight::NORMAL,
706            body,
707            theme,
708            cx,
709        ),
710        BlockKind::Heading { level, text } => {
711            let heading = typography.heading(*level);
712            text_element(
713                text,
714                heading.size(),
715                heading.line_height(),
716                heading.weight,
717                body,
718                theme,
719                cx,
720            )
721        }
722        BlockKind::Bullet(text) => {
723            marker_row(disc(typography, theme), text, body, typography, theme, cx)
724        }
725        BlockKind::Ordered { number, text } => marker_row(
726            div()
727                .flex_none()
728                .w(px(MARKER_WIDTH))
729                .text_size(px(typography.body.size()))
730                .line_height(px(typography.body.line_height()))
731                .text_color(theme.text_muted)
732                .child(SharedString::from(format!("{number}.")))
733                .into_any_element(),
734            text,
735            body,
736            typography,
737            theme,
738            cx,
739        ),
740        BlockKind::Task { checked, text } => marker_row(
741            checkbox(*checked, typography, theme),
742            text,
743            body,
744            typography,
745            theme,
746            cx,
747        ),
748        BlockKind::Quote { kind, text } => div()
749            .border_l_2()
750            .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
751            .pl(px(12.0))
752            .pr(px(10.0))
753            .py(px(2.0))
754            .text_color(theme.text_muted)
755            .children(kind.map(|kind| {
756                div()
757                    .pb(px(2.0))
758                    .text_size(px(typography.body.size()))
759                    .line_height(px(typography.body.line_height()))
760                    .font_weight(FontWeight::SEMIBOLD)
761                    .text_color(alert_color(kind, theme))
762                    .child(kind.label())
763            }))
764            .child(text_element(
765                text,
766                typography.body.size(),
767                typography.body.line_height(),
768                FontWeight::NORMAL,
769                body,
770                theme,
771                cx,
772            ))
773            .into_any_element(),
774        BlockKind::Code { language, code } => {
775            let overlay = overlay.at(Part::Code);
776            // The caret in the fence gives the source back. A painted block is
777            // still an editable one, and typing into it otherwise edits what
778            // the reader cannot see.
779            let painted = overlay
780                .caret()
781                .is_none()
782                .then(|| block::render(language.as_deref(), &code.text, window, cx))
783                .flatten();
784            match painted {
785                // Painted, there is no text under the selection to carry it —
786                // the wash an opaque block gets at the container comes here.
787                Some(element) => div()
788                    .when(overlay.covers_block(), |el| {
789                        el.rounded(px(4.0)).bg(theme.selection)
790                    })
791                    .child(element)
792                    .into_any_element(),
793                None => code_block(
794                    language.as_deref(),
795                    &code.text,
796                    overlay,
797                    typography,
798                    theme,
799                    window,
800                    cx,
801                ),
802            }
803        }
804        BlockKind::Image { url, alt, width } => {
805            image(url, alt, *width, overlay, typography, theme, cx)
806        }
807        BlockKind::Bookmark { url, form } => {
808            bookmark(overlay.block, url, *form, typography, theme, cx)
809        }
810        BlockKind::Table {
811            align,
812            header,
813            rows,
814        } => table(align, header, rows, overlay, typography, theme, window, cx),
815        BlockKind::Rule => div()
816            .h(px(1.0))
817            .w_full()
818            .bg(theme.border)
819            .into_any_element(),
820    }
821}
822
823/// A real 5px disc rather than the "•" glyph, which reads too small at body size.
824fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
825    div()
826        .flex_none()
827        .w(px(MARKER_WIDTH))
828        .h(px(typography.body.line_height()))
829        .flex()
830        .items_center()
831        .child(
832            div()
833                .ml(px(1.0))
834                .w(px(5.0))
835                .h(px(5.0))
836                .rounded_full()
837                .bg(theme.text_faint),
838        )
839        .into_any_element()
840}
841
842fn checkbox(checked: bool, typography: &Typography, theme: &Theme) -> AnyElement {
843    let mut box_ = div()
844        .w(px(13.0))
845        .h(px(13.0))
846        .rounded(px(3.5))
847        .border_1()
848        .flex()
849        .items_center()
850        .justify_center();
851    box_ = if checked {
852        box_.bg(theme.solid)
853            .border_color(theme.solid)
854            .text_style(TextStyle::Caption)
855            .text_color(theme.on_solid)
856            .child("✓")
857    } else {
858        box_.border_color(theme.border_strong)
859    };
860
861    div()
862        .flex_none()
863        .w(px(MARKER_WIDTH))
864        .h(px(typography.body.line_height()))
865        .flex()
866        .items_center()
867        .child(box_)
868        .into_any_element()
869}
870
871/// What an alert paints its rule and its label in.
872fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
873    match kind {
874        QuoteKind::Note => theme.accent,
875        QuoteKind::Tip => theme.success,
876        QuoteKind::Important => theme.busy,
877        QuoteKind::Warning => theme.warning,
878        QuoteKind::Caution => theme.danger,
879    }
880}
881
882fn marker_row(
883    marker: AnyElement,
884    text: &Text,
885    overlay: Overlay,
886    typography: &Typography,
887    theme: &Theme,
888    cx: &App,
889) -> AnyElement {
890    div()
891        .flex()
892        .flex_row()
893        .gap(px(MARKER_GAP))
894        .child(marker)
895        .child(div().flex_1().min_w_0().child(text_element(
896            text,
897            typography.body.size(),
898            typography.body.line_height(),
899            FontWeight::NORMAL,
900            overlay,
901            theme,
902            cx,
903        )))
904        .into_any_element()
905}
906
907/// Inline content flattened for shaping: one string, its runs, and the ranges
908/// that need painting underneath (link clicks, inline-code washes, chips).
909pub struct Flat {
910    pub text: SharedString,
911    pub runs: Vec<TextRun>,
912    pub links: Vec<(Range<usize>, String)>,
913    pub code: Vec<Range<usize>>,
914    pub chips: Vec<Range<usize>>,
915}
916
917/// Marks are ranges, gpui wants consecutive runs — so cut the text at every
918/// mark boundary and ask which marks cover each piece.
919pub fn flatten(text: &Text, base_weight: FontWeight, theme: &Theme) -> Flat {
920    flatten_with(text, base_weight, theme, |_| None)
921}
922
923/// [`flatten`] with the app's own marks painted — see [`crate::MarkPaint`]. A
924/// name the app does not paint reads as the text it wraps.
925pub fn flatten_with(
926    text: &Text,
927    base_weight: FontWeight,
928    theme: &Theme,
929    paint: impl Fn(&str) -> Option<crate::MarkPaint>,
930) -> Flat {
931    let mut cuts: Vec<usize> = text
932        .marks
933        .iter()
934        .flat_map(|span| [span.range.start, span.range.end])
935        .chain([0, text.text.len()])
936        .filter(|cut| *cut <= text.text.len())
937        .collect();
938    cuts.sort_unstable();
939    cuts.dedup();
940
941    let mut runs = Vec::new();
942    let mut links: Vec<(Range<usize>, String)> = Vec::new();
943    let mut code: Vec<Range<usize>> = Vec::new();
944    let mut chips: Vec<Range<usize>> = Vec::new();
945
946    for pair in cuts.windows(2) {
947        let (start, end) = (pair[0], pair[1]);
948        let covering = text
949            .marks
950            .iter()
951            .filter(|span| span.range.start <= start && span.range.end >= end);
952
953        let (mut bold, mut italic, mut mono, mut strike) = (false, false, false, false);
954        let mut chip = false;
955        let mut link = None;
956        // The app's own marks, merged in the order they cover this run: the
957        // last one to say something about a field is the one that says it.
958        let mut custom = crate::MarkPaint::default();
959        for span in covering {
960            match &span.mark {
961                Mark::Bold => bold = true,
962                Mark::Italic => italic = true,
963                Mark::Strike => strike = true,
964                Mark::Code => mono = true,
965                Mark::Mention { url, .. } => {
966                    chip = true;
967                    link = Some(url.clone());
968                }
969                Mark::Link(url) | Mark::Image(url) => link = Some(url.clone()),
970                Mark::Custom(name) => {
971                    let Some(painted) = paint(name) else { continue };
972                    custom.color = painted.color.or(custom.color);
973                    custom.background = painted.background.or(custom.background);
974                    custom.weight = painted.weight.or(custom.weight);
975                    custom.italic |= painted.italic;
976                    custom.underline |= painted.underline;
977                    custom.strikethrough |= painted.strikethrough;
978                }
979            }
980        }
981        let (italic, strike) = (italic || custom.italic, strike || custom.strikethrough);
982
983        if mono {
984            match code.last_mut() {
985                Some(range) if range.end == start => range.end = end,
986                _ => code.push(start..end),
987            }
988        }
989        if chip {
990            match chips.last_mut() {
991                Some(range) if range.end == start => range.end = end,
992                _ => chips.push(start..end),
993            }
994        }
995        if let Some(url) = &link {
996            match links.last_mut() {
997                Some((range, last)) if range.end == start && last == url => range.end = end,
998                _ => links.push((start..end, url.clone())),
999            }
1000        }
1001
1002        let mut face = font(if mono {
1003            theme.font_mono.clone()
1004        } else {
1005            theme.font_sans.clone()
1006        });
1007        face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
1008            FontWeight::SEMIBOLD
1009        } else {
1010            custom.weight.unwrap_or(base_weight)
1011        };
1012        face.style = if italic {
1013            FontStyle::Italic
1014        } else {
1015            FontStyle::Normal
1016        };
1017
1018        runs.push(TextRun {
1019            len: end - start,
1020            font: face,
1021            // Links stay monochrome and underlined; the accent is reserved for
1022            // primary actions. A chip carries its own wash, so underlining it
1023            // too would say the same thing twice.
1024            color: match (mono, custom.color) {
1025                (_, Some(color)) => color,
1026                (true, None) => theme.code_text,
1027                (false, None) => theme.text,
1028            },
1029            background_color: custom.background,
1030            underline: ((link.is_some() && !chip) || custom.underline).then_some(UnderlineStyle {
1031                color: Some(theme.text_muted),
1032                thickness: px(1.0),
1033                wavy: false,
1034            }),
1035            strikethrough: strike.then_some(StrikethroughStyle {
1036                thickness: px(1.0),
1037                color: Some(theme.text_muted),
1038            }),
1039        });
1040    }
1041
1042    Flat {
1043        text: text.text.clone().into(),
1044        runs,
1045        links,
1046        code,
1047        chips,
1048    }
1049}
1050
1051fn text_element(
1052    text: &Text,
1053    size: f32,
1054    line_height: f32,
1055    weight: FontWeight,
1056    overlay: Overlay,
1057    theme: &Theme,
1058    cx: &App,
1059) -> AnyElement {
1060    let flat = flatten_with(text, weight, theme, |name| {
1061        crate::marks::paint_of(cx, name, theme)
1062    });
1063    painted_text(flat, text.text.len(), size, line_height, overlay, theme)
1064}
1065
1066/// Shaped inline content with the editing overlay under it: the selection, the
1067/// caret, the inline-code wash, and the layout a click resolves against.
1068///
1069/// Takes a [`Flat`] rather than a [`Text`] because a table has to shape every
1070/// cell to measure the columns before it can paint one.
1071fn painted_text(
1072    flat: Flat,
1073    len: usize,
1074    size: f32,
1075    line_height: f32,
1076    overlay: Overlay,
1077    theme: &Theme,
1078) -> AnyElement {
1079    let (ix, part) = (overlay.block, overlay.part);
1080    let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
1081    let span = 0..len;
1082    // Only where the caret already is, and only while there is nothing to
1083    // read: a hint on every empty block would be a page of grey.
1084    let hint = overlay
1085        .placeholder
1086        // The caret's own presence, not the blink's phase — a hint that came
1087        // and went twice a second would be unreadable.
1088        .filter(|_| len == 0 && overlay.caret().is_some())
1089        .map(|hint| {
1090            div()
1091                .absolute()
1092                .text_color(theme.text_faint)
1093                .child(hint.clone())
1094        });
1095    let styled = StyledText::new(flat.text).with_runs(flat.runs);
1096    let layout = styled.layout().clone();
1097
1098    let painted: AnyElement = if flat.links.is_empty() {
1099        styled.into_any_element()
1100    } else {
1101        let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
1102        InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
1103            .on_click(ranges, move |clicked, _window, cx| {
1104                if let Some(url) = urls.get(clicked) {
1105                    cx.open_url(url);
1106                }
1107            })
1108            .into_any_element()
1109    };
1110
1111    // The wash is painted before the text — an earlier sibling is underneath —
1112    // reading glyph geometry from the text's own layout handle. Pure paint,
1113    // never part of layout.
1114    let wash = theme.code_wash;
1115    let code_ranges = flat.code;
1116    let chip_wash = theme.element_hover;
1117    let chip_edge = theme.border;
1118    let chip_ranges = flat.chips;
1119    let caret_color = theme.caret;
1120    let selection_color = theme.selection;
1121    let annotated = overlay.annotated(len, theme);
1122    let layouts = overlay.layouts.cloned();
1123    let underlay = canvas(
1124        |_, _, _| (),
1125        move |_, _, window, _| {
1126            if let Some(layouts) = &layouts {
1127                layouts.record(ix, part, span.clone(), layout.clone());
1128            }
1129            // Below the selection, so dragging across a comment still reads as
1130            // selected rather than as a third colour nobody chose.
1131            for (range, wash) in &annotated {
1132                for rect in range_rects(&layout, range, 0.0, 0.0) {
1133                    window.paint_quad(quad(
1134                        rect,
1135                        px(2.0),
1136                        *wash,
1137                        px(0.0),
1138                        gpui::transparent_black(),
1139                        BorderStyle::default(),
1140                    ));
1141                }
1142            }
1143            // Under the glyphs, like the inline-code wash — one quad per visual
1144            // row, so a wrapped selection is a stack of rows rather than a box
1145            // around all of them.
1146            if let Some(range) = &selected {
1147                for rect in range_rects(&layout, range, 0.0, 0.0) {
1148                    window.paint_quad(quad(
1149                        rect,
1150                        px(2.0),
1151                        selection_color,
1152                        px(0.0),
1153                        gpui::transparent_black(),
1154                        BorderStyle::default(),
1155                    ));
1156                }
1157            }
1158            if let Some(offset) = caret
1159                && let Some(head) = layout.position_for_index(offset)
1160            {
1161                window.paint_quad(quad(
1162                    caret_quad(head, size, layout.line_height()),
1163                    px(0.0),
1164                    caret_color,
1165                    px(0.0),
1166                    gpui::transparent_black(),
1167                    BorderStyle::default(),
1168                ));
1169            }
1170            for range in &code_ranges {
1171                for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1172                    window.paint_quad(quad(
1173                        rect,
1174                        px(INLINE_CODE_RADIUS),
1175                        wash,
1176                        px(0.0),
1177                        gpui::transparent_black(),
1178                        BorderStyle::default(),
1179                    ));
1180                }
1181            }
1182            // Wider, rounder and outlined, so a chip and an inline code span
1183            // never read as the same thing at a glance.
1184            for range in &chip_ranges {
1185                for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1186                    window.paint_quad(quad(
1187                        rect,
1188                        px(Theme::control_radius()),
1189                        chip_wash,
1190                        px(1.0),
1191                        chip_edge,
1192                        BorderStyle::Solid,
1193                    ));
1194                }
1195            }
1196        },
1197    )
1198    .absolute()
1199    .size_full();
1200
1201    div()
1202        .text_size(px(size))
1203        .line_height(px(line_height))
1204        .relative()
1205        .child(underlay)
1206        .children(hint)
1207        .child(painted)
1208        .into_any_element()
1209}
1210
1211/// The caret's quad: the text's own size, centred in the line box.
1212///
1213/// The leading is not the caret's to take. A document is set with air around
1214/// its lines, and a caret filling all of it reads as a second, larger font
1215/// standing where the text should be.
1216fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1217    let inset = (line_height - px(size)) / 2.0;
1218    Bounds::new(
1219        head + point(px(0.0), inset),
1220        gpui::size(px(CARET_WIDTH), px(size)),
1221    )
1222}
1223
1224/// The rectangles a byte range occupies, one per visual row.
1225fn range_rects(
1226    layout: &gpui::TextLayout,
1227    range: &Range<usize>,
1228    pad_x: f32,
1229    inset_y: f32,
1230) -> Vec<Bounds<Pixels>> {
1231    let mut rects = Vec::new();
1232    let line_height = layout.line_height();
1233    let mut origin = layout.bounds().origin;
1234    let mut line_start = 0;
1235    for line in layout.line_layouts() {
1236        let shaped = &line.unwrapped_layout;
1237        // A wrap boundary index is both the end of one row and the start of
1238        // the next.
1239        let row_ends = line
1240            .wrap_boundaries()
1241            .iter()
1242            .map(|wrap| shaped.runs[wrap.run_ix].glyphs[wrap.glyph_ix].index)
1243            .chain([line.len()]);
1244        let mut row_start = 0;
1245        for (row, row_end) in row_ends.enumerate() {
1246            let from = range
1247                .start
1248                .saturating_sub(line_start)
1249                .clamp(row_start, row_end);
1250            let to = range.end.saturating_sub(line_start).min(row_end);
1251            let row_x = shaped.x_for_index(row_start);
1252            let (left, right) = (shaped.x_for_index(from), shaped.x_for_index(to));
1253            if from < to && right > left {
1254                rects.push(Bounds::new(
1255                    origin
1256                        + point(
1257                            left - row_x - px(pad_x),
1258                            line_height * row as f32 + px(inset_y),
1259                        ),
1260                    size(
1261                        right - left + px(2.0 * pad_x),
1262                        line_height - px(2.0 * inset_y),
1263                    ),
1264                ));
1265            }
1266            row_start = row_end;
1267        }
1268        origin.y += line.size(line_height).height;
1269        // The newline between two lines is a byte of the text and of neither.
1270        line_start += line.len() + 1;
1271    }
1272    rects
1273}
1274
1275/// Paint a document's own markdown source: a fence's caret, selection and hit
1276/// testing, without a fence's box, band or copy button.
1277///
1278/// The caret is a [`Cursor`] at block 0 in [`Part::Code`] — what a document
1279/// held as one fence answers to, which is how an editor holds its source.
1280/// Wrapping is not optional here: a paragraph is one line of markdown, and a
1281/// source view that scrolled sideways would hide most of it.
1282pub fn render_source(code: &str, editing: Editing, cx: &mut App) -> AnyElement {
1283    let Editing {
1284        selection,
1285        caret_on,
1286        layouts,
1287        annotations,
1288        typography,
1289        ..
1290    } = editing;
1291    // The same reset `render_with` opens with, and for the same reason: the
1292    // positions this frame records are the ones the next click resolves
1293    // against, and last frame's have to go first.
1294    let reset = layouts.map(|layouts| {
1295        let layouts = layouts.clone();
1296        canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
1297            .absolute()
1298            .size(px(0.0))
1299    });
1300    let theme = Theme::of(cx).clone();
1301    let typography = typography.unwrap_or_else(|| Typography::of(cx));
1302    let overlay = Overlay {
1303        block: 0,
1304        part: Part::Code,
1305        selection,
1306        caret_on,
1307        layouts,
1308        annotations,
1309        placeholder: None,
1310        caption: Caption::default(),
1311    };
1312    let (underlay, lines) = code_lines(
1313        Some(crate::source::LANGUAGES[0]),
1314        code,
1315        overlay,
1316        &typography,
1317        &theme,
1318        cx,
1319    );
1320    // Keep each number beside its source line, including wrapped and empty lines.
1321    let style = crate::SourceStyle::of(cx);
1322    let digits = lines.len().to_string().len().max(style.gutter_min_digits);
1323    let gap = style.gutter_gap.max(0.0) * typography.code.size();
1324    let gutter_width = digits as f32 * typography.code.size() + gap;
1325    let lines = lines
1326        .into_iter()
1327        .enumerate()
1328        .map(|(index, line)| {
1329            if !style.line_numbers {
1330                return line;
1331            }
1332            div()
1333                .flex()
1334                .items_start()
1335                .child(
1336                    div()
1337                        .w(px(gutter_width))
1338                        .flex_shrink_0()
1339                        .pr(px(gap))
1340                        .font_family(theme.font_mono.clone())
1341                        .text_color(style.gutter_color.unwrap_or(theme.text_faint))
1342                        .text_right()
1343                        .child((index + 1).to_string()),
1344                )
1345                .child(div().flex_1().min_w_0().child(line))
1346                .into_any_element()
1347        })
1348        .collect();
1349    div()
1350        .flex()
1351        .flex_col()
1352        .children(reset)
1353        .child(code_body(0, underlay, lines, &typography, true))
1354        .into_any_element()
1355}
1356
1357/// The shaped lines of a fence, and the canvas that paints the caret, the
1358/// selection and the annotations over them.
1359fn code_lines(
1360    language: Option<&str>,
1361    code: &str,
1362    overlay: Overlay,
1363    typography: &Typography,
1364    theme: &Theme,
1365    cx: &App,
1366) -> (AnyElement, Vec<AnyElement>) {
1367    let ix = overlay.block;
1368    // Highlighting recolors runs only — layout does not move, so a build with
1369    // no highlighter installed paints the same block in one plain run.
1370    // Markdown is the one language this crate can colour on its own, which is
1371    // what a source view is painted with where no highlighter reaches.
1372    let spans = crate::highlight::spans(cx, language, code).or_else(|| {
1373        language
1374            .filter(|language| crate::source::is_markdown(language))
1375            .map(|_| crate::source::spans(code))
1376    });
1377    let mono = font(theme.font_mono.clone());
1378    let run = |len: usize, color: Hsla| TextRun {
1379        len,
1380        font: mono.clone(),
1381        color,
1382        background_color: None,
1383        underline: None,
1384        strikethrough: None,
1385    };
1386    // Each source line's own layout, with the slice of the code it covers —
1387    // the caret and a click both resolve through these. A wrapped line is
1388    // several rows of one layout, which is the case `range_rects` already
1389    // walks for a paragraph.
1390    let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1391    let mut offset = 0usize;
1392    let lines: Vec<AnyElement> = code
1393        .split('\n')
1394        .map(|line| {
1395            let start = offset;
1396            offset += line.len() + 1;
1397            let mut runs = Vec::new();
1398            // Runs are measured within the line; spans are byte ranges over the
1399            // whole block, so every span is clipped to the line and rebased.
1400            let mut pos = 0usize;
1401            if let Some(spans) = &spans {
1402                let end = start + line.len();
1403                for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1404                    let s = range.start.clamp(start, end) - start;
1405                    let e = range.end.min(end) - start;
1406                    if s > pos {
1407                        runs.push(run(s - pos, theme.text));
1408                    }
1409                    runs.push(run(e - s, theme.syntax.color(*kind)));
1410                    pos = e;
1411                }
1412            }
1413            if pos < line.len() {
1414                runs.push(run(line.len() - pos, theme.text));
1415            }
1416            if runs.is_empty() {
1417                runs.push(run(0, theme.text));
1418            }
1419            let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1420            rows.push((start..start + line.len(), styled.layout().clone()));
1421            styled.into_any_element()
1422        })
1423        .collect();
1424
1425    let caret = overlay.caret_painted();
1426    let selected = overlay.selected(code.len());
1427    let sink = overlay.layouts.cloned();
1428    let code_size = typography.code.size();
1429    let annotated = overlay.annotated(code.len(), theme);
1430    let (caret_color, selection_color) = (theme.caret, theme.selection);
1431    let underlay = canvas(
1432        |_, _, _| (),
1433        move |_, _, window, _| {
1434            for (span, layout) in &rows {
1435                if let Some(sink) = &sink {
1436                    sink.record(ix, Part::Code, span.clone(), layout.clone());
1437                }
1438                for (range, wash) in &annotated {
1439                    let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1440                    if from < to {
1441                        for rect in
1442                            range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1443                        {
1444                            window.paint_quad(quad(
1445                                rect,
1446                                px(2.0),
1447                                *wash,
1448                                px(0.0),
1449                                gpui::transparent_black(),
1450                                BorderStyle::default(),
1451                            ));
1452                        }
1453                    }
1454                }
1455                if let Some(range) = &selected {
1456                    let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1457                    if from < to {
1458                        for rect in
1459                            range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1460                        {
1461                            window.paint_quad(quad(
1462                                rect,
1463                                px(2.0),
1464                                selection_color,
1465                                px(0.0),
1466                                gpui::transparent_black(),
1467                                BorderStyle::default(),
1468                            ));
1469                        }
1470                    }
1471                }
1472                if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1473                    && let Some(head) = layout.position_for_index(offset - span.start)
1474                {
1475                    window.paint_quad(quad(
1476                        caret_quad(head, code_size, layout.line_height()),
1477                        px(0.0),
1478                        caret_color,
1479                        px(0.0),
1480                        gpui::transparent_black(),
1481                        BorderStyle::default(),
1482                    ));
1483                }
1484            }
1485        },
1486    )
1487    .absolute()
1488    .size_full();
1489
1490    (underlay.into_any_element(), lines)
1491}
1492
1493fn code_block(
1494    language: Option<&str>,
1495    code: &str,
1496    overlay: Overlay,
1497    typography: &Typography,
1498    theme: &Theme,
1499    window: &mut Window,
1500    cx: &mut App,
1501) -> AnyElement {
1502    let ix = overlay.block;
1503    let (underlay, lines) = code_lines(language, code, overlay, typography, theme, cx);
1504    let body = code_body(ix, underlay, lines, typography, Layout::of(cx).wrap_code);
1505
1506    div()
1507        .rounded(px(Theme::panel_radius()))
1508        .bg(theme.ink(0.035))
1509        .border_1()
1510        .border_color(theme.border)
1511        .overflow_hidden()
1512        .relative()
1513        // The band is unconditional: it is where the copy button already floats,
1514        // and where a host puts its language control — which needs somewhere to
1515        // sit on a block that has no language yet.
1516        .child(
1517            div()
1518                .relative()
1519                .flex()
1520                .flex_row()
1521                .items_center()
1522                .px(px(CODE_PADDING_X))
1523                .py(px(5.0))
1524                .border_b_1()
1525                .border_color(theme.border)
1526                .bg(theme.ink(0.02))
1527                .text_style(TextStyle::Subheadline)
1528                .text_color(match language {
1529                    Some(_) => theme.text_muted,
1530                    None => theme.text_faint,
1531                })
1532                // The label's own box, not the band's: a host hanging a picker
1533                // here wants it around the word, and only the word knows how
1534                // wide the word is.
1535                .child(
1536                    div()
1537                        .relative()
1538                        .children(overlay.layouts.map(|layouts| {
1539                            let layouts = layouts.clone();
1540                            canvas(
1541                                move |bounds, _, _| layouts.record_language(ix, bounds),
1542                                |_, _, _, _| (),
1543                            )
1544                            .absolute()
1545                            .size_full()
1546                        }))
1547                        .child(SharedString::from(
1548                            language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1549                        )),
1550                ),
1551        )
1552        .child(body)
1553        .child(copy_button(code, ix, theme, window, cx))
1554        .into_any_element()
1555}
1556
1557/// The lines of a fence, wrapped to the block or scrolling sideways under it.
1558fn code_body(
1559    ix: usize,
1560    underlay: AnyElement,
1561    lines: Vec<AnyElement>,
1562    typography: &Typography,
1563    wrap: bool,
1564) -> AnyElement {
1565    let column = div()
1566        .flex()
1567        .flex_col()
1568        .px(px(CODE_PADDING_X))
1569        .children(lines);
1570    let body = div()
1571        .id(ElementId::named_usize("md-code", ix))
1572        .relative()
1573        .py(px(CODE_PADDING_Y))
1574        .text_size(px(typography.code.size()))
1575        .line_height(px(typography.code.line_height()))
1576        .child(underlay);
1577    if wrap {
1578        // The column is the block's width here rather than its widest line's,
1579        // which is what gives the text something to wrap against.
1580        body.child(column.w_full()).into_any_element()
1581    } else {
1582        ui::scroll::Viewport::new(
1583            format!("md-code-scroll-{ix}"),
1584            body.flex()
1585                .flex_row()
1586                .whitespace_nowrap()
1587                // The padding belongs to the lines, not to the scroller: a scroll
1588                // container's trailing padding is not part of what it will scroll
1589                // to, so the last characters of a long line sit behind the right
1590                // edge with nowhere left to go. As a row's only item this column is
1591                // sized by its widest line, and the padding rides along inside that
1592                // width.
1593                .child(column.items_start()),
1594            gpui::Axis::Horizontal,
1595        )
1596        .into_any_element()
1597    }
1598}
1599
1600/// A copy button that owns its own feedback.
1601///
1602/// The state is the element's, not the caller's: a component library cannot ask
1603/// every host to thread a handler and a "which block is showing Copied" index
1604/// through its render tree just to put a button on a code block. It resets when
1605/// the pointer leaves, which needs no clock.
1606fn copy_button(
1607    code: &str,
1608    ix: usize,
1609    theme: &Theme,
1610    window: &mut Window,
1611    cx: &mut App,
1612) -> AnyElement {
1613    let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1614    let showing = *copied.read(cx);
1615    let text: SharedString = code.to_string().into();
1616
1617    div()
1618        .id(ElementId::named_usize("md-copy", ix))
1619        .absolute()
1620        .top(px(3.0))
1621        .right(px(5.0))
1622        .h(px(20.0))
1623        .px(px(6.0))
1624        .rounded(px(5.0))
1625        .flex()
1626        .items_center()
1627        .cursor_pointer()
1628        .text_style(TextStyle::Caption)
1629        .text_color(theme.text_muted)
1630        .hover(|el| el.bg(theme.element_hover))
1631        .child(if showing { "Copied" } else { "Copy" })
1632        .on_click({
1633            let copied = copied.clone();
1634            move |_, _, cx| {
1635                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1636                copied.update(cx, |state, cx| {
1637                    *state = true;
1638                    cx.notify();
1639                });
1640            }
1641        })
1642        .on_hover(move |hovering, _, cx| {
1643            if !*hovering && *copied.read(cx) {
1644                copied.update(cx, |state, cx| {
1645                    *state = false;
1646                    cx.notify();
1647                });
1648            }
1649        })
1650        .into_any_element()
1651}
1652
1653/// A picture and the caption under it, which is the alt text a caret can reach.
1654///
1655/// The caption row appears when there is something to read or somewhere to
1656/// type, so a document being read is not a column of pictures each trailing a
1657/// blank line. With no URL yet the picture is a dashed row instead — the shape
1658/// the slash menu makes, waiting to be told what to show.
1659fn image(
1660    url: &str,
1661    alt: &Text,
1662    width: Option<u32>,
1663    overlay: Overlay,
1664    typography: &Typography,
1665    theme: &Theme,
1666    cx: &App,
1667) -> AnyElement {
1668    let hint = SharedString::new_static(CAPTION_HINT);
1669    let overlay = Overlay {
1670        placeholder: Some(&hint),
1671        ..overlay.at(Part::Caption)
1672    };
1673    let picture = if url.is_empty() {
1674        div()
1675            .h(px(IMAGE_EMPTY_HEIGHT))
1676            .flex()
1677            .items_center()
1678            .px(px(CARD_PADDING))
1679            .rounded(px(Theme::button_radius()))
1680            .border_1()
1681            .border_dashed()
1682            .border_color(theme.border)
1683            .text_size(px(typography.body.size()))
1684            .text_color(theme.text_muted)
1685            .child(IMAGE_EMPTY)
1686    } else {
1687        // A URL is fetched; anything else is a file, and gpui reads one only
1688        // from a `PathBuf` — handed a string it looks for an asset built into
1689        // the binary and paints nothing.
1690        let picture = match url.contains("://") {
1691            true => img(SharedString::from(url.to_string())),
1692            false => img(std::path::PathBuf::from(url)),
1693        };
1694        let box_ = div()
1695            .relative()
1696            .rounded(px(Theme::button_radius()))
1697            .overflow_hidden()
1698            .border_1()
1699            .border_color(theme.border)
1700            .children(overlay.layouts.map(|layouts| {
1701                let layouts = layouts.clone();
1702                let ix = overlay.block;
1703                canvas(
1704                    move |bounds, _, _| layouts.record_picture(ix, bounds),
1705                    |_, _, _, _| (),
1706                )
1707                .absolute()
1708                .size_full()
1709            }));
1710        match width {
1711            // A stated width is the box's: it hugs, so the border is around
1712            // the picture rather than around the column beside it, and the
1713            // picture fills what the box settled on — which `max_w_full`
1714            // holds inside the page however wide the width was written.
1715            Some(width) => box_
1716                .self_start()
1717                .max_w_full()
1718                .w(px(width as f32))
1719                .child(picture.w(px(width as f32)).max_w_full()),
1720            // Unstated, the picture scales itself against the column, which
1721            // is a percentage and so needs a box that spans one to measure.
1722            None => box_.child(picture.max_w_full()),
1723        }
1724    };
1725    div()
1726        .flex()
1727        .flex_col()
1728        .gap(px(CAPTION_GAP))
1729        .child(picture)
1730        // An empty caption still paints while the caret is in it, or there
1731        // would be nothing to type into and no hint saying so.
1732        .when(
1733            overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1734            |el| {
1735                el.child(text_element(
1736                    alt,
1737                    typography.caption.size(),
1738                    typography.caption.line_height(),
1739                    FontWeight::NORMAL,
1740                    overlay,
1741                    theme,
1742                    cx,
1743                ))
1744            },
1745        )
1746        .into_any_element()
1747}
1748
1749/// A bookmark, in Notion's proportions: a fixed-height row with the text on the
1750/// left and an image panel of a fixed width on the right, all of it one click
1751/// target. [`Form::Embed`] turns the row into a column and gives the image the
1752/// card's full width instead, and [`Form::Chip`] is neither — a pill of favicon
1753/// and title, which is what an inline mention would be if shaped text had
1754/// anywhere to put a picture.
1755///
1756/// The row is a fixed height with its footer pinned to the bottom, because a
1757/// preview resolves *after* the card has painted — a blurb arriving into a box
1758/// that grows would shove every block below it down the page. An embed's cover
1759/// holds that height, so its text hugs.
1760fn bookmark(
1761    ix: usize,
1762    url: &str,
1763    form: Form,
1764    typography: &Typography,
1765    theme: &Theme,
1766    cx: &App,
1767) -> AnyElement {
1768    let preview = preview::of(cx, url).unwrap_or_default();
1769    let host = SharedString::from(preview::host(url).to_string());
1770    let label = preview.label.clone().unwrap_or_else(|| host.clone());
1771    let title = preview
1772        .title
1773        .clone()
1774        .unwrap_or_else(|| SharedString::from(url.to_string()));
1775
1776    // Owned, because the image panel's fallback outlives this call: gpui asks
1777    // for the replacement element only once the fetch has failed.
1778    let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1779    let site = host.clone();
1780    let mark = move |size: f32| {
1781        let host = site.clone();
1782        match icon.clone() {
1783            Some(icon) => img(icon)
1784                .size(px(size))
1785                .rounded(px(size / 4.0))
1786                .with_fallback(move || initial(&host, size, muted, wash))
1787                .into_any_element(),
1788            None => initial(&host, size, muted, wash),
1789        }
1790    };
1791
1792    if form == Form::Chip {
1793        let open = url.to_string();
1794        let pill = div()
1795            .id(ElementId::named_usize("md-chip", ix))
1796            .flex()
1797            .flex_row()
1798            .items_center()
1799            .gap(px(6.0))
1800            .px(px(CHIP_BLOCK_PAD_X))
1801            .py(px(CHIP_BLOCK_PAD_Y))
1802            .rounded(px(Theme::control_radius()))
1803            .border_1()
1804            .border_color(theme.border)
1805            .bg(theme.element_hover)
1806            .text_size(px(typography.body.size()))
1807            .line_height(px(typography.body.line_height()))
1808            .text_color(theme.text)
1809            .cursor(CursorStyle::PointingHand)
1810            .hover(|el| el.bg(theme.element_active))
1811            .on_click(move |_, _, cx| cx.open_url(&open))
1812            .child(mark(CHIP_ICON))
1813            // The host, not the URL, when nothing has resolved it: a chip is
1814            // the short form, and a raw URL in a pill is the long one.
1815            .child(
1816                div()
1817                    .min_w_0()
1818                    .truncate()
1819                    .child(preview.title.unwrap_or(label)),
1820            );
1821        // A block's own box is `display: block`, where a pill would take the
1822        // whole width. One flex row around it is what lets it hug its label.
1823        return div().flex().flex_row().child(pill).into_any_element();
1824    }
1825
1826    let words = div()
1827        .flex()
1828        .flex_col()
1829        .min_w_0()
1830        .px(px(CARD_PADDING))
1831        .py(px(CARD_PADDING - 2.0))
1832        .child(
1833            div()
1834                .truncate()
1835                .text_size(px(typography.body.size()))
1836                .line_height(px(typography.body.line_height()))
1837                .text_color(theme.text)
1838                .child(title),
1839        )
1840        .children(preview.description.map(|blurb| {
1841            div()
1842                .line_clamp(2)
1843                .text_size(px(typography.card.size()))
1844                .line_height(px(typography.card.line_height()))
1845                .text_color(theme.text_muted)
1846                .child(blurb)
1847        }))
1848        .child(
1849            div()
1850                .mt_auto()
1851                .pt(px(6.0))
1852                .flex()
1853                .items_center()
1854                .gap(px(6.0))
1855                .text_size(px(typography.card.size()))
1856                .text_color(theme.text_muted)
1857                .child(mark(CARD_ICON))
1858                .child(div().truncate().child(label)),
1859        );
1860
1861    let picture = corners(div(), form)
1862        .bg(theme.surface)
1863        .flex()
1864        .items_center()
1865        .justify_center()
1866        .overflow_hidden()
1867        .child(match preview.image {
1868            Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1869                .with_fallback(move || mark(CARD_COVER))
1870                .into_any_element(),
1871            None => mark(CARD_COVER),
1872        });
1873
1874    let open = url.to_string();
1875    let card = div()
1876        .id(ElementId::named_usize("md-bookmark", ix))
1877        .flex()
1878        .w_full()
1879        .overflow_hidden()
1880        .rounded(px(Theme::button_radius()))
1881        .border(px(CARD_BORDER))
1882        .border_color(theme.border)
1883        .bg(theme.surface_card)
1884        .cursor(CursorStyle::PointingHand)
1885        .hover(|el| el.bg(theme.element_hover))
1886        .on_click(move |_, _, cx| cx.open_url(&open));
1887
1888    if form == Form::Embed {
1889        card.flex_col()
1890            .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1891            .child(words.w_full())
1892    } else {
1893        card.h(px(CARD_HEIGHT))
1894            .child(words.flex_1())
1895            .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1896    }
1897    .into_any_element()
1898}
1899
1900/// The card's corners, on the panel that reaches them: a content mask is a
1901/// rectangle, so a picture paints square over a rounded card unless it carries
1902/// the radius itself, concentric inside the card's border.
1903fn corners<T: Styled>(element: T, form: Form) -> T {
1904    let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
1905    match form {
1906        Form::Embed => element.rounded_t(corner),
1907        _ => element.rounded_r(corner),
1908    }
1909}
1910
1911/// The mark a site gets before anyone has fetched its favicon: its host's first
1912/// letter, which is a placeholder no icon set has to ship.
1913fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1914    div()
1915        .flex_none()
1916        .size(px(size))
1917        .rounded(px(size / 4.0))
1918        .bg(wash)
1919        .flex()
1920        .items_center()
1921        .justify_center()
1922        .text_size(px(size * 0.55))
1923        .text_color(color)
1924        .child(SharedString::from(
1925            host.chars()
1926                .next()
1927                .unwrap_or('?')
1928                .to_uppercase()
1929                .to_string(),
1930        ))
1931        .into_any_element()
1932}
1933
1934/// A GFM table.
1935///
1936/// Columns are content-proportional with a per-column floor: each cell is
1937/// shaped unwrapped to get its max-content width, and the flex resolution does
1938/// the rest. When even the floors no longer fit, the table scrolls sideways
1939/// rather than crushing every column into per-character wrapping.
1940#[expect(
1941    clippy::too_many_arguments,
1942    reason = "a table, its overlay, and what paints them"
1943)]
1944fn table(
1945    align: &[Align],
1946    header: &[Text],
1947    rows: &[Vec<Text>],
1948    overlay: Overlay,
1949    typography: &Typography,
1950    theme: &Theme,
1951    window: &mut Window,
1952    cx: &App,
1953) -> AnyElement {
1954    let ix = overlay.block;
1955    let all: Vec<&[Text]> = std::iter::once(header)
1956        .filter(|row| !row.is_empty())
1957        .chain(rows.iter().map(|row| row.as_slice()))
1958        .collect();
1959    let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1960    if columns == 0 {
1961        return gpui::Empty.into_any_element();
1962    }
1963    let has_header = !header.is_empty();
1964
1965    let text_system = window.text_system();
1966    let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1967    let mut content = vec![0.0f32; columns];
1968    for (r, row) in all.iter().enumerate() {
1969        let weight = if has_header && r == 0 {
1970            FontWeight::BOLD
1971        } else {
1972            FontWeight::NORMAL
1973        };
1974        let mut out = Vec::with_capacity(columns);
1975        for (c, natural) in content.iter_mut().enumerate() {
1976            let Some(cell) = row.get(c) else {
1977                out.push(None);
1978                continue;
1979            };
1980            let flat = flatten_with(cell, weight, theme, |name| {
1981                crate::marks::paint_of(cx, name, theme)
1982            });
1983            if !flat.text.is_empty() {
1984                let width = f32::from(
1985                    text_system
1986                        .shape_line(
1987                            flat.text.clone(),
1988                            px(typography.body.size()),
1989                            &flat.runs,
1990                            None,
1991                        )
1992                        .width(),
1993                );
1994                *natural = natural.max(width);
1995            }
1996            out.push(Some(flat));
1997        }
1998        flats.push(out);
1999    }
2000
2001    let naturals: Vec<f32> = content
2002        .iter()
2003        .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
2004        .collect();
2005    let minimums: Vec<f32> = naturals
2006        .iter()
2007        .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
2008        .collect();
2009    let hairline = theme.hairline(0.10);
2010
2011    let mut inner = div()
2012        .flex()
2013        .flex_col()
2014        .w_full()
2015        .min_w(px(minimums.iter().sum::<f32>()));
2016    for (r, row) in flats.into_iter().enumerate() {
2017        if r > 0 {
2018            inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
2019        }
2020        let mut row_el = div().flex().flex_row();
2021        for (c, cell) in row.into_iter().enumerate() {
2022            let mut cell_el = div()
2023                .flex_grow(naturals[c])
2024                .flex_shrink(naturals[c])
2025                .flex_basis(px(0.0))
2026                .min_w(px(minimums[c]))
2027                .p(px(TABLE_CELL_PADDING))
2028                .text_size(px(typography.body.size()))
2029                .line_height(px(typography.body.line_height()));
2030            cell_el = match align.get(c).copied().unwrap_or_default() {
2031                Align::Left => cell_el,
2032                Align::Center => cell_el.text_center(),
2033                Align::Right => cell_el.text_right(),
2034            };
2035            if let Some(flat) = cell {
2036                // `all` drops an empty header, so a table without one starts at
2037                // part row 1 — row 0 is the header slot whether or not it is
2038                // filled.
2039                let row = if has_header { r } else { r + 1 };
2040                let len = flat.text.len();
2041                cell_el = cell_el.child(painted_text(
2042                    flat,
2043                    len,
2044                    typography.body.size(),
2045                    typography.body.line_height(),
2046                    overlay.at(Part::Cell { row, column: c }),
2047                    theme,
2048                ));
2049            }
2050            row_el = row_el.child(cell_el);
2051        }
2052        inner = inner.child(row_el);
2053    }
2054
2055    ui::scroll::Viewport::new(
2056        format!("md-table-scroll-{ix}"),
2057        div()
2058            .id(ElementId::named_usize("md-table", ix))
2059            .w_full()
2060            .child(inner),
2061        gpui::Axis::Horizontal,
2062    )
2063    .into_any_element()
2064}