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