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 {
899            theme.font_sans.clone()
900        });
901        face.weight = if bold && base_weight.0 < FontWeight::SEMIBOLD.0 {
902            FontWeight::SEMIBOLD
903        } else {
904            base_weight
905        };
906        face.style = if italic {
907            FontStyle::Italic
908        } else {
909            FontStyle::Normal
910        };
911
912        runs.push(TextRun {
913            len: end - start,
914            font: face,
915            // Links stay monochrome and underlined; the accent is reserved for
916            // primary actions. A chip carries its own wash, so underlining it
917            // too would say the same thing twice.
918            color: if mono { theme.code_text } else { theme.text },
919            background_color: None,
920            underline: (link.is_some() && !chip).then_some(UnderlineStyle {
921                color: Some(theme.text_muted),
922                thickness: px(1.0),
923                wavy: false,
924            }),
925            strikethrough: strike.then_some(StrikethroughStyle {
926                thickness: px(1.0),
927                color: Some(theme.text_muted),
928            }),
929        });
930    }
931
932    Flat {
933        text: text.text.clone().into(),
934        runs,
935        links,
936        code,
937        chips,
938    }
939}
940
941fn text_element(
942    text: &Text,
943    size: f32,
944    line_height: f32,
945    weight: FontWeight,
946    overlay: Overlay,
947    theme: &Theme,
948) -> AnyElement {
949    let flat = flatten(text, weight, theme);
950    painted_text(flat, text.text.len(), size, line_height, overlay, theme)
951}
952
953/// Shaped inline content with the editing overlay under it: the selection, the
954/// caret, the inline-code wash, and the layout a click resolves against.
955///
956/// Takes a [`Flat`] rather than a [`Text`] because a table has to shape every
957/// cell to measure the columns before it can paint one.
958fn painted_text(
959    flat: Flat,
960    len: usize,
961    size: f32,
962    line_height: f32,
963    overlay: Overlay,
964    theme: &Theme,
965) -> AnyElement {
966    let (ix, part) = (overlay.block, overlay.part);
967    let (caret, selected) = (overlay.caret_painted(), overlay.selected(len));
968    let span = 0..len;
969    // Only where the caret already is, and only while there is nothing to
970    // read: a hint on every empty block would be a page of grey.
971    let hint = overlay
972        .placeholder
973        // The caret's own presence, not the blink's phase — a hint that came
974        // and went twice a second would be unreadable.
975        .filter(|_| len == 0 && overlay.caret().is_some())
976        .map(|hint| {
977            div()
978                .absolute()
979                .text_color(theme.text_faint)
980                .child(hint.clone())
981        });
982    let styled = StyledText::new(flat.text).with_runs(flat.runs);
983    let layout = styled.layout().clone();
984
985    let painted: AnyElement = if flat.links.is_empty() {
986        styled.into_any_element()
987    } else {
988        let (ranges, urls): (Vec<_>, Vec<_>) = flat.links.into_iter().unzip();
989        InteractiveText::new(ElementId::named_usize("md-text", ix), styled)
990            .on_click(ranges, move |clicked, _window, cx| {
991                if let Some(url) = urls.get(clicked) {
992                    cx.open_url(url);
993                }
994            })
995            .into_any_element()
996    };
997
998    // The wash is painted before the text — an earlier sibling is underneath —
999    // reading glyph geometry from the text's own layout handle. Pure paint,
1000    // never part of layout.
1001    let wash = theme.code_wash;
1002    let code_ranges = flat.code;
1003    let chip_wash = theme.element_hover;
1004    let chip_edge = theme.border;
1005    let chip_ranges = flat.chips;
1006    let caret_color = theme.caret;
1007    let selection_color = theme.selection;
1008    let annotated = overlay.annotated(len, theme);
1009    let layouts = overlay.layouts.cloned();
1010    let underlay = canvas(
1011        |_, _, _| (),
1012        move |_, _, window, _| {
1013            if let Some(layouts) = &layouts {
1014                layouts.record(ix, part, span.clone(), layout.clone());
1015            }
1016            // Below the selection, so dragging across a comment still reads as
1017            // selected rather than as a third colour nobody chose.
1018            for (range, wash) in &annotated {
1019                for rect in range_rects(&layout, range, 0.0, 0.0) {
1020                    window.paint_quad(quad(
1021                        rect,
1022                        px(2.0),
1023                        *wash,
1024                        px(0.0),
1025                        gpui::transparent_black(),
1026                        BorderStyle::default(),
1027                    ));
1028                }
1029            }
1030            // Under the glyphs, like the inline-code wash — one quad per visual
1031            // row, so a wrapped selection is a stack of rows rather than a box
1032            // around all of them.
1033            if let Some(range) = &selected {
1034                for rect in range_rects(&layout, range, 0.0, 0.0) {
1035                    window.paint_quad(quad(
1036                        rect,
1037                        px(2.0),
1038                        selection_color,
1039                        px(0.0),
1040                        gpui::transparent_black(),
1041                        BorderStyle::default(),
1042                    ));
1043                }
1044            }
1045            if let Some(offset) = caret
1046                && let Some(head) = layout.position_for_index(offset)
1047            {
1048                window.paint_quad(quad(
1049                    caret_quad(head, size, layout.line_height()),
1050                    px(0.0),
1051                    caret_color,
1052                    px(0.0),
1053                    gpui::transparent_black(),
1054                    BorderStyle::default(),
1055                ));
1056            }
1057            for range in &code_ranges {
1058                for rect in range_rects(&layout, range, INLINE_CODE_PAD_X, INLINE_CODE_INSET_Y) {
1059                    window.paint_quad(quad(
1060                        rect,
1061                        px(INLINE_CODE_RADIUS),
1062                        wash,
1063                        px(0.0),
1064                        gpui::transparent_black(),
1065                        BorderStyle::default(),
1066                    ));
1067                }
1068            }
1069            // Wider, rounder and outlined, so a chip and an inline code span
1070            // never read as the same thing at a glance.
1071            for range in &chip_ranges {
1072                for rect in range_rects(&layout, range, CHIP_PAD_X, CHIP_INSET_Y) {
1073                    window.paint_quad(quad(
1074                        rect,
1075                        px(Theme::control_radius()),
1076                        chip_wash,
1077                        px(1.0),
1078                        chip_edge,
1079                        BorderStyle::Solid,
1080                    ));
1081                }
1082            }
1083        },
1084    )
1085    .absolute()
1086    .size_full();
1087
1088    div()
1089        .text_size(px(size))
1090        .line_height(px(line_height))
1091        .relative()
1092        .child(underlay)
1093        .children(hint)
1094        .child(painted)
1095        .into_any_element()
1096}
1097
1098/// The caret's quad: the text's own size, centred in the line box.
1099///
1100/// The leading is not the caret's to take. A document is set with air around
1101/// its lines, and a caret filling all of it reads as a second, larger font
1102/// standing where the text should be.
1103fn caret_quad(head: Point<Pixels>, size: f32, line_height: Pixels) -> Bounds<Pixels> {
1104    let inset = (line_height - px(size)) / 2.0;
1105    Bounds::new(
1106        head + point(px(0.0), inset),
1107        gpui::size(px(CARET_WIDTH), px(size)),
1108    )
1109}
1110
1111/// The rectangles a byte range occupies, one per visual row.
1112fn range_rects(
1113    layout: &gpui::TextLayout,
1114    range: &Range<usize>,
1115    pad_x: f32,
1116    inset_y: f32,
1117) -> Vec<Bounds<Pixels>> {
1118    let mut rects = Vec::new();
1119    let line_height = layout.line_height();
1120    let mut cursor = range.start;
1121    // Walk one visual row at a time. A wrapped range has no direct row query,
1122    // so the last index still on this row is found by bisection.
1123    let mut guard = 0;
1124    while cursor < range.end && guard < 256 {
1125        guard += 1;
1126        let Some(head) = layout.position_for_index(cursor) else {
1127            break;
1128        };
1129        let (row_end, next) = match layout.position_for_index(range.end) {
1130            Some(tail) if tail.y == head.y => (range.end, range.end),
1131            _ => {
1132                let (mut low, mut high) = (cursor, range.end);
1133                while high - low > 1 {
1134                    let mid = low + (high - low) / 2;
1135                    match layout.position_for_index(mid) {
1136                        Some(probe) if probe.y == head.y => low = mid,
1137                        _ => high = mid,
1138                    }
1139                }
1140                (low, high)
1141            }
1142        };
1143        if let Some(tail) = layout.position_for_index(row_end)
1144            && tail.x > head.x
1145        {
1146            rects.push(Bounds::new(
1147                point(head.x - px(pad_x), head.y + px(inset_y)),
1148                size(
1149                    tail.x - head.x + px(2.0 * pad_x),
1150                    line_height - px(2.0 * inset_y),
1151                ),
1152            ));
1153        }
1154        cursor = next.max(cursor + 1);
1155    }
1156    rects
1157}
1158
1159fn code_block(
1160    language: Option<&str>,
1161    code: &str,
1162    overlay: Overlay,
1163    typography: &Typography,
1164    theme: &Theme,
1165    window: &mut Window,
1166    cx: &mut App,
1167) -> AnyElement {
1168    let ix = overlay.block;
1169    // Per line, so the block's height is exactly `lines × line height`.
1170    // Highlighting recolors runs only — layout does not move, so a build with
1171    // no highlighter installed paints the same block in one plain run.
1172    let spans = crate::highlight::spans(cx, language, code);
1173    let mono = font(theme.font_mono.clone());
1174    let run = |len: usize, color: Hsla| TextRun {
1175        len,
1176        font: mono.clone(),
1177        color,
1178        background_color: None,
1179        underline: None,
1180        strikethrough: None,
1181    };
1182    // Each line's own layout, with the slice of the code it covers — the caret
1183    // and a click both resolve through these.
1184    let mut rows: Vec<(Range<usize>, TextLayout)> = Vec::new();
1185    let mut offset = 0usize;
1186    let lines: Vec<AnyElement> = code
1187        .split('\n')
1188        .map(|line| {
1189            let start = offset;
1190            offset += line.len() + 1;
1191            let mut runs = Vec::new();
1192            // Runs are measured within the line; spans are byte ranges over the
1193            // whole block, so every span is clipped to the line and rebased.
1194            let mut pos = 0usize;
1195            if let Some(spans) = &spans {
1196                let end = start + line.len();
1197                for (range, kind) in spans.iter().filter(|(r, _)| r.end > start && r.start < end) {
1198                    let s = range.start.clamp(start, end) - start;
1199                    let e = range.end.min(end) - start;
1200                    if s > pos {
1201                        runs.push(run(s - pos, theme.text));
1202                    }
1203                    runs.push(run(e - s, theme.syntax.color(*kind)));
1204                    pos = e;
1205                }
1206            }
1207            if pos < line.len() {
1208                runs.push(run(line.len() - pos, theme.text));
1209            }
1210            if runs.is_empty() {
1211                runs.push(run(0, theme.text));
1212            }
1213            let styled = StyledText::new(SharedString::from(line.to_string())).with_runs(runs);
1214            rows.push((start..start + line.len(), styled.layout().clone()));
1215            styled.into_any_element()
1216        })
1217        .collect();
1218
1219    let caret = overlay.caret_painted();
1220    let selected = overlay.selected(code.len());
1221    let sink = overlay.layouts.cloned();
1222    let code_size = typography.code.size();
1223    let annotated = overlay.annotated(code.len(), theme);
1224    let (caret_color, selection_color) = (theme.caret, theme.selection);
1225    let underlay = canvas(
1226        |_, _, _| (),
1227        move |_, _, window, _| {
1228            for (span, layout) in &rows {
1229                if let Some(sink) = &sink {
1230                    sink.record(ix, Part::Code, span.clone(), layout.clone());
1231                }
1232                for (range, wash) in &annotated {
1233                    let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1234                    if from < to {
1235                        for rect in
1236                            range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1237                        {
1238                            window.paint_quad(quad(
1239                                rect,
1240                                px(2.0),
1241                                *wash,
1242                                px(0.0),
1243                                gpui::transparent_black(),
1244                                BorderStyle::default(),
1245                            ));
1246                        }
1247                    }
1248                }
1249                if let Some(range) = &selected {
1250                    let (from, to) = (range.start.max(span.start), range.end.min(span.end));
1251                    if from < to {
1252                        for rect in
1253                            range_rects(layout, &(from - span.start..to - span.start), 0.0, 0.0)
1254                        {
1255                            window.paint_quad(quad(
1256                                rect,
1257                                px(2.0),
1258                                selection_color,
1259                                px(0.0),
1260                                gpui::transparent_black(),
1261                                BorderStyle::default(),
1262                            ));
1263                        }
1264                    }
1265                }
1266                if let Some(offset) = caret.filter(|at| span.contains(at) || *at == span.end)
1267                    && let Some(head) = layout.position_for_index(offset - span.start)
1268                {
1269                    window.paint_quad(quad(
1270                        caret_quad(head, code_size, layout.line_height()),
1271                        px(0.0),
1272                        caret_color,
1273                        px(0.0),
1274                        gpui::transparent_black(),
1275                        BorderStyle::default(),
1276                    ));
1277                }
1278            }
1279        },
1280    )
1281    .absolute()
1282    .size_full();
1283
1284    div()
1285        .rounded(px(Theme::panel_radius()))
1286        .bg(theme.ink(0.035))
1287        .border_1()
1288        .border_color(theme.border)
1289        .overflow_hidden()
1290        .relative()
1291        // The band is unconditional: it is where the copy button already floats,
1292        // and where a host puts its language control — which needs somewhere to
1293        // sit on a block that has no language yet.
1294        .child(
1295            div()
1296                .relative()
1297                .flex()
1298                .flex_row()
1299                .items_center()
1300                .px(px(CODE_PADDING_X))
1301                .py(px(5.0))
1302                .border_b_1()
1303                .border_color(theme.border)
1304                .bg(theme.ink(0.02))
1305                .text_style(TextStyle::Subheadline)
1306                .text_color(match language {
1307                    Some(_) => theme.text_muted,
1308                    None => theme.text_faint,
1309                })
1310                // The label's own box, not the band's: a host hanging a picker
1311                // here wants it around the word, and only the word knows how
1312                // wide the word is.
1313                .child(
1314                    div()
1315                        .relative()
1316                        .children(overlay.layouts.map(|layouts| {
1317                            let layouts = layouts.clone();
1318                            canvas(
1319                                move |bounds, _, _| layouts.record_language(ix, bounds),
1320                                |_, _, _, _| (),
1321                            )
1322                            .absolute()
1323                            .size_full()
1324                        }))
1325                        .child(SharedString::from(
1326                            language.unwrap_or(PLAIN_LANGUAGE).to_string(),
1327                        )),
1328                ),
1329        )
1330        .child(
1331            div()
1332                .id(ElementId::named_usize("md-code", ix))
1333                .overflow_x_scroll()
1334                // Without it a scroll down the page turns sideways the moment
1335                // the pointer crosses a code block: gpui remaps input to
1336                // whichever axis a container can scroll.
1337                .restrict_scroll_to_axis()
1338                .relative()
1339                .px(px(CODE_PADDING_X))
1340                .py(px(CODE_PADDING_Y))
1341                .text_size(px(typography.code.size()))
1342                .line_height(px(typography.code.line_height()))
1343                .whitespace_nowrap()
1344                .child(underlay)
1345                .children(lines),
1346        )
1347        .child(copy_button(code, ix, theme, window, cx))
1348        .into_any_element()
1349}
1350
1351/// A copy button that owns its own feedback.
1352///
1353/// The state is the element's, not the caller's: a component library cannot ask
1354/// every host to thread a handler and a "which block is showing Copied" index
1355/// through its render tree just to put a button on a code block. It resets when
1356/// the pointer leaves, which needs no clock.
1357fn copy_button(
1358    code: &str,
1359    ix: usize,
1360    theme: &Theme,
1361    window: &mut Window,
1362    cx: &mut App,
1363) -> AnyElement {
1364    let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
1365    let showing = *copied.read(cx);
1366    let text: SharedString = code.to_string().into();
1367
1368    div()
1369        .id(ElementId::named_usize("md-copy", ix))
1370        .absolute()
1371        .top(px(3.0))
1372        .right(px(5.0))
1373        .h(px(20.0))
1374        .px(px(6.0))
1375        .rounded(px(5.0))
1376        .flex()
1377        .items_center()
1378        .cursor_pointer()
1379        .text_style(TextStyle::Caption)
1380        .text_color(theme.text_muted)
1381        .hover(|el| el.bg(theme.element_hover))
1382        .child(if showing { "Copied" } else { "Copy" })
1383        .on_click({
1384            let copied = copied.clone();
1385            move |_, _, cx| {
1386                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
1387                copied.update(cx, |state, cx| {
1388                    *state = true;
1389                    cx.notify();
1390                });
1391            }
1392        })
1393        .on_hover(move |hovering, _, cx| {
1394            if !*hovering && *copied.read(cx) {
1395                copied.update(cx, |state, cx| {
1396                    *state = false;
1397                    cx.notify();
1398                });
1399            }
1400        })
1401        .into_any_element()
1402}
1403
1404/// A picture and the caption under it, which is the alt text a caret can reach.
1405///
1406/// The caption row appears when there is something to read or somewhere to
1407/// type, so a document being read is not a column of pictures each trailing a
1408/// blank line. With no URL yet the picture is a dashed row instead — the shape
1409/// the slash menu makes, waiting to be told what to show.
1410fn image(
1411    url: &str,
1412    alt: &Text,
1413    width: Option<u32>,
1414    overlay: Overlay,
1415    typography: &Typography,
1416    theme: &Theme,
1417) -> AnyElement {
1418    let hint = SharedString::new_static(CAPTION_HINT);
1419    let overlay = Overlay {
1420        placeholder: Some(&hint),
1421        ..overlay.at(Part::Caption)
1422    };
1423    let picture = if url.is_empty() {
1424        div()
1425            .h(px(IMAGE_EMPTY_HEIGHT))
1426            .flex()
1427            .items_center()
1428            .px(px(CARD_PADDING))
1429            .rounded(px(Theme::button_radius()))
1430            .border_1()
1431            .border_dashed()
1432            .border_color(theme.border)
1433            .text_size(px(typography.body.size()))
1434            .text_color(theme.text_muted)
1435            .child(IMAGE_EMPTY)
1436    } else {
1437        // A URL is fetched; anything else is a file, and gpui reads one only
1438        // from a `PathBuf` — handed a string it looks for an asset built into
1439        // the binary and paints nothing.
1440        let picture = match url.contains("://") {
1441            true => img(SharedString::from(url.to_string())),
1442            false => img(std::path::PathBuf::from(url)),
1443        };
1444        let box_ = div()
1445            .relative()
1446            .rounded(px(Theme::button_radius()))
1447            .overflow_hidden()
1448            .border_1()
1449            .border_color(theme.border)
1450            .children(overlay.layouts.map(|layouts| {
1451                let layouts = layouts.clone();
1452                let ix = overlay.block;
1453                canvas(
1454                    move |bounds, _, _| layouts.record_picture(ix, bounds),
1455                    |_, _, _, _| (),
1456                )
1457                .absolute()
1458                .size_full()
1459            }));
1460        match width {
1461            // A stated width is the box's: it hugs, so the border is around
1462            // the picture rather than around the column beside it, and the
1463            // picture fills what the box settled on — which `max_w_full`
1464            // holds inside the page however wide the width was written.
1465            Some(width) => box_
1466                .self_start()
1467                .max_w_full()
1468                .w(px(width as f32))
1469                .child(picture.w(px(width as f32)).max_w_full()),
1470            // Unstated, the picture scales itself against the column, which
1471            // is a percentage and so needs a box that spans one to measure.
1472            None => box_.child(picture.max_w_full()),
1473        }
1474    };
1475    div()
1476        .flex()
1477        .flex_col()
1478        .gap(px(CAPTION_GAP))
1479        .child(picture)
1480        // An empty caption still paints while the caret is in it, or there
1481        // would be nothing to type into and no hint saying so.
1482        .when(
1483            overlay.caption == Caption::Shown && (!alt.is_empty() || overlay.caret().is_some()),
1484            |el| {
1485                el.child(text_element(
1486                    alt,
1487                    typography.caption.size(),
1488                    typography.caption.line_height(),
1489                    FontWeight::NORMAL,
1490                    overlay,
1491                    theme,
1492                ))
1493            },
1494        )
1495        .into_any_element()
1496}
1497
1498/// A bookmark, in Notion's proportions: a fixed-height row with the text on the
1499/// left and an image panel of a fixed width on the right, all of it one click
1500/// target. [`Form::Embed`] turns the row into a column and gives the image the
1501/// card's full width instead, and [`Form::Chip`] is neither — a pill of favicon
1502/// and title, which is what an inline mention would be if shaped text had
1503/// anywhere to put a picture.
1504///
1505/// The row is a fixed height with its footer pinned to the bottom, because a
1506/// preview resolves *after* the card has painted — a blurb arriving into a box
1507/// that grows would shove every block below it down the page. An embed's cover
1508/// holds that height, so its text hugs.
1509fn bookmark(
1510    ix: usize,
1511    url: &str,
1512    form: Form,
1513    typography: &Typography,
1514    theme: &Theme,
1515    cx: &App,
1516) -> AnyElement {
1517    let preview = preview::of(cx, url).unwrap_or_default();
1518    let host = SharedString::from(preview::host(url).to_string());
1519    let label = preview.label.clone().unwrap_or_else(|| host.clone());
1520    let title = preview
1521        .title
1522        .clone()
1523        .unwrap_or_else(|| SharedString::from(url.to_string()));
1524
1525    // Owned, because the image panel's fallback outlives this call: gpui asks
1526    // for the replacement element only once the fetch has failed.
1527    let (icon, muted, wash) = (preview.icon.clone(), theme.text_muted, theme.element_hover);
1528    let site = host.clone();
1529    let mark = move |size: f32| {
1530        let host = site.clone();
1531        match icon.clone() {
1532            Some(icon) => img(icon)
1533                .size(px(size))
1534                .rounded(px(size / 4.0))
1535                .with_fallback(move || initial(&host, size, muted, wash))
1536                .into_any_element(),
1537            None => initial(&host, size, muted, wash),
1538        }
1539    };
1540
1541    if form == Form::Chip {
1542        let open = url.to_string();
1543        let pill = div()
1544            .id(ElementId::named_usize("md-chip", ix))
1545            .flex()
1546            .flex_row()
1547            .items_center()
1548            .gap(px(6.0))
1549            .px(px(CHIP_BLOCK_PAD_X))
1550            .py(px(CHIP_BLOCK_PAD_Y))
1551            .rounded(px(Theme::control_radius()))
1552            .border_1()
1553            .border_color(theme.border)
1554            .bg(theme.element_hover)
1555            .text_size(px(typography.body.size()))
1556            .line_height(px(typography.body.line_height()))
1557            .text_color(theme.text)
1558            .cursor(CursorStyle::PointingHand)
1559            .hover(|el| el.bg(theme.element_active))
1560            .on_click(move |_, _, cx| cx.open_url(&open))
1561            .child(mark(CHIP_ICON))
1562            // The host, not the URL, when nothing has resolved it: a chip is
1563            // the short form, and a raw URL in a pill is the long one.
1564            .child(
1565                div()
1566                    .min_w_0()
1567                    .truncate()
1568                    .child(preview.title.unwrap_or(label)),
1569            );
1570        // A block's own box is `display: block`, where a pill would take the
1571        // whole width. One flex row around it is what lets it hug its label.
1572        return div().flex().flex_row().child(pill).into_any_element();
1573    }
1574
1575    let words = div()
1576        .flex()
1577        .flex_col()
1578        .min_w_0()
1579        .px(px(CARD_PADDING))
1580        .py(px(CARD_PADDING - 2.0))
1581        .child(
1582            div()
1583                .truncate()
1584                .text_size(px(typography.body.size()))
1585                .line_height(px(typography.body.line_height()))
1586                .text_color(theme.text)
1587                .child(title),
1588        )
1589        .children(preview.description.map(|blurb| {
1590            div()
1591                .line_clamp(2)
1592                .text_size(px(typography.card.size()))
1593                .line_height(px(typography.card.line_height()))
1594                .text_color(theme.text_muted)
1595                .child(blurb)
1596        }))
1597        .child(
1598            div()
1599                .mt_auto()
1600                .pt(px(6.0))
1601                .flex()
1602                .items_center()
1603                .gap(px(6.0))
1604                .text_size(px(typography.card.size()))
1605                .text_color(theme.text_muted)
1606                .child(mark(CARD_ICON))
1607                .child(div().truncate().child(label)),
1608        );
1609
1610    let picture = corners(div(), form)
1611        .bg(theme.surface)
1612        .flex()
1613        .items_center()
1614        .justify_center()
1615        .overflow_hidden()
1616        .child(match preview.image {
1617            Some(image) => corners(img(image).size_full().object_fit(ObjectFit::Cover), form)
1618                .with_fallback(move || mark(CARD_COVER))
1619                .into_any_element(),
1620            None => mark(CARD_COVER),
1621        });
1622
1623    let open = url.to_string();
1624    let card = div()
1625        .id(ElementId::named_usize("md-bookmark", ix))
1626        .flex()
1627        .w_full()
1628        .overflow_hidden()
1629        .rounded(px(Theme::button_radius()))
1630        .border(px(CARD_BORDER))
1631        .border_color(theme.border)
1632        .bg(theme.surface_card)
1633        .cursor(CursorStyle::PointingHand)
1634        .hover(|el| el.bg(theme.element_hover))
1635        .on_click(move |_, _, cx| cx.open_url(&open));
1636
1637    if form == Form::Embed {
1638        card.flex_col()
1639            .child(picture.w_full().h(px(CARD_COVER_HEIGHT)))
1640            .child(words.w_full())
1641    } else {
1642        card.h(px(CARD_HEIGHT))
1643            .child(words.flex_1())
1644            .child(picture.flex_none().w(px(CARD_IMAGE_WIDTH)).h_full())
1645    }
1646    .into_any_element()
1647}
1648
1649/// The card's corners, on the panel that reaches them: a content mask is a
1650/// rectangle, so a picture paints square over a rounded card unless it carries
1651/// the radius itself, concentric inside the card's border.
1652fn corners<T: Styled>(element: T, form: Form) -> T {
1653    let corner = px(Theme::inset_radius(Theme::button_radius(), CARD_BORDER));
1654    match form {
1655        Form::Embed => element.rounded_t(corner),
1656        _ => element.rounded_r(corner),
1657    }
1658}
1659
1660/// The mark a site gets before anyone has fetched its favicon: its host's first
1661/// letter, which is a placeholder no icon set has to ship.
1662fn initial(host: &str, size: f32, color: Hsla, wash: Hsla) -> AnyElement {
1663    div()
1664        .flex_none()
1665        .size(px(size))
1666        .rounded(px(size / 4.0))
1667        .bg(wash)
1668        .flex()
1669        .items_center()
1670        .justify_center()
1671        .text_size(px(size * 0.55))
1672        .text_color(color)
1673        .child(SharedString::from(
1674            host.chars()
1675                .next()
1676                .unwrap_or('?')
1677                .to_uppercase()
1678                .to_string(),
1679        ))
1680        .into_any_element()
1681}
1682
1683/// A GFM table.
1684///
1685/// Columns are content-proportional with a per-column floor: each cell is
1686/// shaped unwrapped to get its max-content width, and the flex resolution does
1687/// the rest. When even the floors no longer fit, the table scrolls sideways
1688/// rather than crushing every column into per-character wrapping.
1689fn table(
1690    align: &[Align],
1691    header: &[Text],
1692    rows: &[Vec<Text>],
1693    overlay: Overlay,
1694    typography: &Typography,
1695    theme: &Theme,
1696    window: &mut Window,
1697) -> AnyElement {
1698    let ix = overlay.block;
1699    let all: Vec<&[Text]> = std::iter::once(header)
1700        .filter(|row| !row.is_empty())
1701        .chain(rows.iter().map(|row| row.as_slice()))
1702        .collect();
1703    let columns = all.iter().map(|row| row.len()).max().unwrap_or(0);
1704    if columns == 0 {
1705        return gpui::Empty.into_any_element();
1706    }
1707    let has_header = !header.is_empty();
1708
1709    let text_system = window.text_system();
1710    let mut flats: Vec<Vec<Option<Flat>>> = Vec::with_capacity(all.len());
1711    let mut content = vec![0.0f32; columns];
1712    for (r, row) in all.iter().enumerate() {
1713        let weight = if has_header && r == 0 {
1714            FontWeight::BOLD
1715        } else {
1716            FontWeight::NORMAL
1717        };
1718        let mut out = Vec::with_capacity(columns);
1719        for (c, natural) in content.iter_mut().enumerate() {
1720            let Some(cell) = row.get(c) else {
1721                out.push(None);
1722                continue;
1723            };
1724            let flat = flatten(cell, weight, theme);
1725            if !flat.text.is_empty() {
1726                let width = f32::from(
1727                    text_system
1728                        .shape_line(
1729                            flat.text.clone(),
1730                            px(typography.body.size()),
1731                            &flat.runs,
1732                            None,
1733                        )
1734                        .width(),
1735                );
1736                *natural = natural.max(width);
1737            }
1738            out.push(Some(flat));
1739        }
1740        flats.push(out);
1741    }
1742
1743    let naturals: Vec<f32> = content
1744        .iter()
1745        .map(|width| width.max(TABLE_MIN_COLUMN_CONTENT) + 2.0 * TABLE_CELL_PADDING)
1746        .collect();
1747    let minimums: Vec<f32> = naturals
1748        .iter()
1749        .map(|natural| natural.min(TABLE_MIN_COLUMN_WIDTH))
1750        .collect();
1751    let hairline = theme.hairline(0.10);
1752
1753    let mut inner = div()
1754        .flex()
1755        .flex_col()
1756        .w_full()
1757        .min_w(px(minimums.iter().sum::<f32>()));
1758    for (r, row) in flats.into_iter().enumerate() {
1759        if r > 0 {
1760            inner = inner.child(div().flex_none().h(px(TABLE_DIVIDER)).w_full().bg(hairline));
1761        }
1762        let mut row_el = div().flex().flex_row();
1763        for (c, cell) in row.into_iter().enumerate() {
1764            let mut cell_el = div()
1765                .flex_grow(naturals[c])
1766                .flex_shrink(naturals[c])
1767                .flex_basis(px(0.0))
1768                .min_w(px(minimums[c]))
1769                .p(px(TABLE_CELL_PADDING))
1770                .text_size(px(typography.body.size()))
1771                .line_height(px(typography.body.line_height()));
1772            cell_el = match align.get(c).copied().unwrap_or_default() {
1773                Align::Left => cell_el,
1774                Align::Center => cell_el.text_center(),
1775                Align::Right => cell_el.text_right(),
1776            };
1777            if let Some(flat) = cell {
1778                // `all` drops an empty header, so a table without one starts at
1779                // part row 1 — row 0 is the header slot whether or not it is
1780                // filled.
1781                let row = if has_header { r } else { r + 1 };
1782                let len = flat.text.len();
1783                cell_el = cell_el.child(painted_text(
1784                    flat,
1785                    len,
1786                    typography.body.size(),
1787                    typography.body.line_height(),
1788                    overlay.at(Part::Cell { row, column: c }),
1789                    theme,
1790                ));
1791            }
1792            row_el = row_el.child(cell_el);
1793        }
1794        inner = inner.child(row_el);
1795    }
1796
1797    div()
1798        .id(ElementId::named_usize("md-table", ix))
1799        .w_full()
1800        .overflow_x_scroll()
1801        .restrict_scroll_to_axis()
1802        .child(inner)
1803        .into_any_element()
1804}