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