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