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