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