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