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