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::{
11    cell::RefCell,
12    collections::HashMap,
13    hash::{DefaultHasher, Hash, Hasher},
14    ops::Range,
15    path::Path,
16    rc::Rc,
17};
18
19use gpui::{
20    AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
21    ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
22    StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
23    canvas, div, font, img, point, prelude::*, px, quad, size,
24};
25use theme::{TextStyle, Theme, Typeset};
26
27use crate::{
28    block,
29    doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
30    layout::Layout,
31    preview,
32    select::{Cursor, Selection},
33    typography::Typography,
34};
35
36/// Space between two ordinary blocks, and the tighter space inside a list.
37mod code;
38mod column;
39mod layouts;
40mod media;
41mod table;
42mod text;
43
44pub use code::*;
45use column::*;
46pub use layouts::*;
47use media::*;
48use table::*;
49pub use text::*;
50
51const BLOCK_GAP: f32 = 12.0;
52const LIST_GAP: f32 = 4.0;
53/// One indent level. Wide enough to clear a marker and read as a level.
54const INDENT_WIDTH: f32 = 22.0;
55/// The marker column of a list row.
56const MARKER_WIDTH: f32 = 18.0;
57const MARKER_GAP: f32 = 8.0;
58/// What a fence holds its code in, inside its border.
59const CODE_PADDING_X: f32 = 12.0;
60const CODE_PADDING_Y: f32 = 10.0;
61/// What a fence with no info string calls itself, in its header and in a
62/// picker — one spelling, so the label and the menu row cannot disagree.
63pub const PLAIN_LANGUAGE: &str = "Plain";
64/// Width of the caret. Wider than a hairline, because it has to read at a
65/// glance against the text it sits in.
66const CARET_WIDTH: f32 = 1.5;
67/// Inline code's wash is a rounded quad painted under the glyphs: a run's
68/// `background_color` can only ever be a square box.
69const INLINE_CODE_RADIUS: f32 = 4.5;
70const INLINE_CODE_PAD_X: f32 = 2.0;
71const INLINE_CODE_INSET_Y: f32 = 2.0;
72/// A mention's chip — the same quad-under-glyphs trick as inline code, with
73/// more room and an outline so the two do not read as the same thing.
74const CHIP_PAD_X: f32 = 4.0;
75const CHIP_INSET_Y: f32 = 1.0;
76/// A chip with a block to itself is a real element rather than a wash, so it
77/// has room for the favicon the inline one cannot hold.
78const CHIP_BLOCK_PAD_X: f32 = 8.0;
79const CHIP_BLOCK_PAD_Y: f32 = 3.0;
80const CHIP_ICON: f32 = 15.0;
81/// Bookmark metrics. Notion's card: 180px of image beside the text, and a
82/// height that fits a title, two lines of blurb and a footer. A cover moves
83/// that image above the text and gives it the card's full width.
84const CARD_HEIGHT: f32 = 116.0;
85const CARD_IMAGE_WIDTH: f32 = 180.0;
86const CARD_COVER_HEIGHT: f32 = 200.0;
87const CARD_PADDING: f32 = 14.0;
88const CARD_BORDER: f32 = 1.0;
89const CARD_ICON: f32 = 16.0;
90const CARD_COVER: f32 = 44.0;
91/// Image metrics.
92const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
93const CAPTION_GAP: f32 = 4.0;
94/// What an image with no URL yet says, and what its caption says while empty.
95const IMAGE_EMPTY: &str = "Add an image";
96const CAPTION_HINT: &str = "Write a caption";
97/// Table metrics. The design is frameless: hairlines between rows are the only
98/// chrome — no outer box, no header fill, no rounding.
99const TABLE_CELL_PADDING: f32 = 12.0;
100const TABLE_DIVIDER: f32 = 1.0;
101/// Floor for a column's max-content share, so a short column ("1k") beside a
102/// prose column keeps a readable width.
103const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
104/// Narrowest a column wraps down to before the table scrolls instead.
105const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
106
107/// What an image's one authored string is doing on the page.
108///
109/// SwiftUI keeps three things apart — `accessibilityLabel` for a reader that
110/// cannot see, `.help` for the pointer, and a caption you compose out of a
111/// `Text` under the picture. Markdown has one slot for all three, so this says
112/// which of them it is playing here rather than in the document, where it is
113/// the same string either way.
114///
115/// A named choice rather than a `bool`, so a surface that wants a third answer
116/// gets a variant instead of a second flag.
117#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
118pub enum Caption {
119    /// Under the picture, where a caret can sit in it. The editor's shape.
120    #[default]
121    Shown,
122    /// Kept by the document and painted nowhere — a picture on its own.
123    Hidden,
124}
125
126/// Whether a fence paints the button that copies its text.
127///
128/// A named choice rather than a `bool`, the way [`Caption`] is.
129#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
130pub enum CopyButton {
131    /// Floating at the top right of the band, on the pointer and off it.
132    #[default]
133    Shown,
134    /// Painted nowhere. A document with this and no [`Editing::toggle`] holds
135    /// no listener at all.
136    Hidden,
137}
138
139/// A range the caller wants washed, and which wash it gets.
140///
141/// None of what a comment or a highlight *says* is here: the caller keeps it
142/// and hands over the range, the way it hands over a [`crate::Preview`]. A
143/// closed set rather than a color, so the environment keeps deciding the paint.
144#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
145#[non_exhaustive]
146pub enum Annotation {
147    /// A thread still waiting on someone.
148    #[default]
149    Open,
150    /// Answered, and kept for the record.
151    Resolved,
152    /// The one whose thread the reader has in front of them.
153    Active,
154    /// A reader's highlight, in the wash [`crate::set_highlight_paint`]
155    /// gives its colour.
156    Highlight(crate::HighlightColor),
157}
158
159impl Annotation {
160    fn wash(self, theme: &Theme, highlight: crate::HighlightPaint) -> Hsla {
161        match self {
162            Self::Open => theme.warning.opacity(0.20),
163            Self::Resolved => theme.warning.opacity(0.08),
164            Self::Active => theme.warning.opacity(0.38),
165            Self::Highlight(color) => highlight(color, theme),
166        }
167    }
168}
169
170/// Handed the block whose checkbox was clicked — see [`Toggle::Handled`].
171///
172/// Shared rather than borrowed: the press listener it is cloned into outlives
173/// the frame that built it.
174pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
175
176/// Who answers a press on a task block's checkbox.
177///
178/// Either variant paints the box as a control — the pointer over it is a hand.
179/// [`Editing::toggle`] left unset paints it as a mark, and the press goes
180/// wherever it would on any other glyph.
181#[derive(Clone)]
182pub enum Toggle {
183    /// The box takes the press, stops it, and calls this with the block it
184    /// belongs to. For a caller holding the [`Doc`] it renders itself.
185    Handled(OnToggle),
186    /// The box takes no press. The caller hit-tests
187    /// [`BlockLayouts::checkbox_bounds`] in its own handler, which is what an
188    /// editor does: the press it swallows is the one that also takes focus and
189    /// closes an open menu, and the toggle belongs in the undo history beside
190    /// the rest of its edits.
191    HitTested,
192}
193
194/// What an editor paints over a document.
195///
196/// One value rather than six parameters, and the reason it is public: the
197/// caret, the selection, the comment washes and the layout sink all arrive
198/// together or not at all, and a read-only [`render`] sets none of them.
199#[derive(Clone)]
200pub struct Editing<'a> {
201    /// The caret and what it has selected. `None` paints neither — a document
202    /// nobody is editing.
203    pub selection: Option<Selection>,
204    /// The blink's lit half. A caret painted on every frame reads as frozen,
205    /// and the phase belongs to whoever owns the focus.
206    pub caret_on: bool,
207    /// Filled as the document paints, for a caller resolving clicks against it.
208    ///
209    /// Given, only the blocks near the part of the window the document shows
210    /// are built, and it answers for those alone. The rest are placed at their
211    /// last measured height, or a guess at one, and held in it across frames.
212    pub layouts: Option<&'a BlockLayouts>,
213    /// Ranges washed under the text, in the order given.
214    pub annotations: &'a [(Selection, Annotation)],
215    /// Shown on the caret's block while it holds nothing.
216    pub placeholder: Option<SharedString>,
217    pub caption: Caption,
218    /// What to set the document in. `None` takes the installed
219    /// [`Typography`] — a caller sizing one document apart from the rest
220    /// passes [`Typography::scaled`].
221    pub typography: Option<Typography>,
222    /// Makes a task block's checkbox a control, and says who answers the
223    /// press. `None` paints a mark.
224    pub toggle: Option<Toggle>,
225    /// Whether a fence offers to copy itself.
226    pub copy: CopyButton,
227    /// The directory a relative image path is joined onto. `None` leaves it
228    /// relative, which gpui reads against the process's working directory.
229    pub base: Option<&'a Path>,
230    /// Blocks built wherever they are, alongside the ones near the part of the
231    /// window the document shows — source lines, for [`render_source`]. The
232    /// caret's and the selection anchor's are built without being named. Only
233    /// read with `layouts` given.
234    pub keep: &'a [usize],
235    /// The scroll container the document sits in. When blocks above the text
236    /// showing come out taller or shorter than they were placed at, its offset
237    /// moves by the difference. Only read with `layouts` given.
238    pub scroll: Option<&'a gpui::ScrollHandle>,
239}
240
241impl Default for Editing<'_> {
242    fn default() -> Self {
243        Self {
244            selection: None,
245            // Lit, so that a caller setting a selection and nothing else gets a
246            // caret rather than a mystery.
247            caret_on: true,
248            layouts: None,
249            annotations: &[],
250            placeholder: None,
251            caption: Caption::default(),
252            typography: None,
253            toggle: None,
254            copy: CopyButton::default(),
255            base: None,
256            keep: &[],
257            scroll: None,
258        }
259    }
260}
261
262/// What the editor needs painted into one text: which text it is, where the
263/// caret sits, and where to record the layout a click resolves against.
264///
265/// One bundle rather than four parameters threaded through every block arm —
266/// a read-only render builds it with no caret and no sink, and pays nothing.
267#[derive(Clone, Copy)]
268struct Overlay<'a> {
269    block: usize,
270    part: Part,
271    selection: Option<Selection>,
272    caret_on: bool,
273    layouts: Option<&'a BlockLayouts>,
274    /// Ranges washed under the text, in the order the caller gave them.
275    annotations: &'a [(Selection, Annotation)],
276    /// Shown on the caret's block while it holds nothing. The renderer is the
277    /// only thing that knows where that text sits, so the string comes to it.
278    placeholder: Option<&'a SharedString>,
279    caption: Caption,
280    /// Borrowed so [`Overlay`] stays `Copy` — the clone is made at the one
281    /// press listener that needs an owned handle.
282    toggle: Option<&'a Toggle>,
283    copy: CopyButton,
284    base: Option<&'a Path>,
285    highlight: crate::HighlightPaint,
286}
287
288impl<'a> Overlay<'a> {
289    fn at(self, part: Part) -> Self {
290        Self { part, ..self }
291    }
292
293    fn here(&self) -> Cursor {
294        Cursor::new(self.block, self.part, 0)
295    }
296
297    /// The caret to paint: where it is, and only on the blink's lit half.
298    ///
299    /// Separate from [`Self::caret`] because the blink must not reach anything
300    /// but the quad — a block whose paint depends on holding the caret would
301    /// otherwise swap itself out twice a second.
302    fn caret_painted(&self) -> Option<usize> {
303        self.caret_on.then(|| self.caret()).flatten()
304    }
305
306    /// The caret's byte offset, if the head is in *this* text.
307    fn caret(&self) -> Option<usize> {
308        self.selection
309            .map(|selection| selection.head)
310            .filter(|head| head.block == self.block && head.part == self.part)
311            .map(|head| head.offset)
312    }
313
314    /// The selected slice of this text, clipped to it.
315    fn selected(&self, len: usize) -> Option<Range<usize>> {
316        self.clip(self.selection?, len)
317    }
318
319    /// The annotated slices of this text, already resolved to their paint —
320    /// the wash goes into a `move` closure that the theme does not travel into.
321    fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
322        self.annotations
323            .iter()
324            .filter_map(|(range, kind)| {
325                Some((self.clip(*range, len)?, kind.wash(theme, self.highlight)))
326            })
327            .collect()
328    }
329
330    /// A range clipped to this text, and `None` when it does not reach it.
331    ///
332    /// The comparison is on `(block, part)` alone: a range covers this text
333    /// entirely when it starts before and ends after, and the offsets only
334    /// matter at the two ends.
335    fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
336        if selection.is_collapsed() {
337            return None;
338        }
339        let (start, end) = selection.ordered();
340        let here = self.here();
341        let (first, last) = (
342            Cursor::new(start.block, start.part, 0),
343            Cursor::new(end.block, end.part, 0),
344        );
345        if here < first || here > last {
346            return None;
347        }
348        let from = if here == first { start.offset } else { 0 };
349        let to = if here == last { end.offset } else { len };
350        (from < to).then_some(from..to.min(len))
351    }
352
353    /// Whether a block painting something a caret cannot enter — a rule, a
354    /// picture — falls inside the selection, and so should show that it is
355    /// going to be taken.
356    fn covers_block(&self) -> bool {
357        let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
358            return false;
359        };
360        let (start, end) = selection.ordered();
361        start.block < self.block && self.block < end.block
362    }
363}
364
365/// What gpui loads for an image URL as written in a document.
366///
367/// Anything with `://` is fetched as it stands. Anything else is a file: an
368/// absolute path as it stands, a relative one joined onto `base` when there is
369/// one.
370pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
371    if url.contains("://") {
372        return SharedString::from(url.to_string()).into();
373    }
374    // gpui reads a file only from a `PathBuf` — handed a string it looks for
375    // an asset built into the binary and paints nothing.
376    match base {
377        Some(base) => base.join(url).into(),
378        None => std::path::PathBuf::from(url).into(),
379    }
380}
381
382/// Parse and render in one step — the common case for read-only content.
383pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
384    let doc = crate::parse_with(source, &crate::Marks::of(cx));
385    render(&doc, Caption::default(), window, cx)
386}
387
388/// Render a document.
389pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
390    render_with(
391        doc,
392        Editing {
393            caption,
394            ..Editing::default()
395        },
396        window,
397        cx,
398    )
399}
400
401/// Render a document with a caret and a selection in it.
402///
403/// Both are paint-time concerns and nothing else: they read their positions off
404/// the shaped text's own layout handle, the same way the inline-code wash does,
405/// so nothing about layout depends on where the caret sits. An editor supplies
406/// the selection and owns the focus and the keys; painting a caret and a few
407/// quads is not worth a second renderer.
408pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
409    let Editing {
410        selection,
411        caret_on,
412        layouts,
413        annotations,
414        placeholder,
415        caption,
416        typography,
417        toggle,
418        copy,
419        base,
420        keep,
421        scroll,
422    } = editing;
423    // Cloned once so the theme is readable while `cx` stays free for the
424    // element state the copy button needs.
425    let theme = Theme::of(cx).clone();
426    let typography = typography.unwrap_or_else(|| Typography::of(cx));
427    let highlight = crate::marks::highlight_paint_of(cx);
428    let gaps: Vec<Pixels> = doc
429        .blocks
430        .iter()
431        .enumerate()
432        .map(|(ix, block)| {
433            px(match doc.blocks.get(ix.wrapping_sub(1)) {
434                None => 0.0,
435                Some(previous) if tight(previous, block) => LIST_GAP,
436                Some(_) => BLOCK_GAP,
437            })
438        })
439        .collect();
440
441    let Some(layouts) = layouts else {
442        let mut column = div().flex().flex_col();
443        for (ix, block) in doc.blocks.iter().enumerate() {
444            let overlay = Overlay {
445                block: ix,
446                part: Part::Body,
447                selection,
448                caret_on,
449                layouts: None,
450                annotations,
451                placeholder: placeholder.as_ref(),
452                caption,
453                toggle: toggle.as_ref(),
454                copy,
455                base,
456                highlight,
457            };
458            column = column
459                .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
460        }
461        return column.into_any_element();
462    };
463
464    let keys: Vec<u64> = doc
465        .blocks
466        .iter()
467        .map(|block| block_key(block, &typography))
468        .collect();
469    layouts.prune(&keys);
470    let guesses: Vec<Guess> = doc
471        .blocks
472        .iter()
473        .map(|block| guess(block, &typography))
474        .collect();
475    let mut kept = keep.to_vec();
476    kept.extend(selection.map(|selection| selection.head.block));
477    kept.extend(selection.map(|selection| selection.anchor.block));
478    let owned = Owned {
479        blocks: doc.blocks.clone(),
480        selection,
481        caret_on,
482        layouts: layouts.clone(),
483        annotations: annotations.to_vec(),
484        placeholder,
485        caption,
486        toggle,
487        copy,
488        base: base.map(Path::to_path_buf),
489        highlight,
490        typography,
491        theme,
492    };
493    Column {
494        layouts: layouts.clone(),
495        keys: keys.into(),
496        gaps: gaps.into(),
497        guesses: guesses.into(),
498        keep: kept,
499        scroll: scroll.cloned(),
500        build: Box::new(move |ix, window, cx| {
501            let overlay = Overlay {
502                block: ix,
503                part: Part::Body,
504                selection: owned.selection,
505                caret_on: owned.caret_on,
506                layouts: Some(&owned.layouts),
507                annotations: &owned.annotations,
508                placeholder: owned.placeholder.as_ref(),
509                caption: owned.caption,
510                toggle: owned.toggle.as_ref(),
511                copy: owned.copy,
512                base: owned.base.as_deref(),
513                highlight: owned.highlight,
514            };
515            block_box(
516                &owned.blocks[ix],
517                overlay,
518                &owned.typography,
519                &owned.theme,
520                window,
521                cx,
522            )
523            .into_any_element()
524        }),
525    }
526    .into_any_element()
527}
528
529/// What [`Column`] builds a block from, owned so it can build one at prepaint.
530struct Owned {
531    blocks: Vec<Block>,
532    selection: Option<Selection>,
533    caret_on: bool,
534    layouts: BlockLayouts,
535    annotations: Vec<(Selection, Annotation)>,
536    placeholder: Option<SharedString>,
537    caption: Caption,
538    toggle: Option<Toggle>,
539    copy: CopyButton,
540    base: Option<std::path::PathBuf>,
541    highlight: crate::HighlightPaint,
542    typography: Typography,
543    theme: Theme,
544}
545
546/// A block's box: its indent outside, and inside it the recorder and the
547/// block itself.
548fn block_box(
549    block: &Block,
550    overlay: Overlay,
551    typography: &Typography,
552    theme: &Theme,
553    window: &mut Window,
554    cx: &mut App,
555) -> gpui::Div {
556    let ix = overlay.block;
557    // The block's own box, recorded for a gutter handle and a drop target.
558    // A rule and an image hold no text, so a layout would not find them.
559    let frame = overlay.layouts.map(|layouts| {
560        let layouts = layouts.clone();
561        canvas(
562            move |bounds, _, _| layouts.record_block(ix, bounds),
563            |_, _, _, _| (),
564        )
565        .absolute()
566        .size_full()
567    });
568    // The indent sits on the outside and the recorder on the inside, so what
569    // is recorded is the box the block's text actually occupies. Recorded
570    // outside the padding, every level answered with the same left edge, and a
571    // gutter handle placed from it stayed at the margin while the block it
572    // belongs to moved right.
573    div()
574        .w_full()
575        .pl(px(block.indent as f32 * INDENT_WIDTH))
576        .child(
577            div()
578                .w_full()
579                .relative()
580                .children(frame)
581                // What a caret cannot enter still has to show it is inside the
582                // selection, or a rule between two paragraphs looks untouched
583                // right up until it disappears.
584                .when(overlay.covers_block() && block.opaque(), |el| {
585                    el.rounded(px(4.0)).bg(theme.selection)
586                })
587                .child(block_element(block, overlay, typography, theme, window, cx)),
588        )
589}
590
591/// What a block's height is cached under: its content and the type it is set
592/// in, so an edit elsewhere that shifts its index keeps the height.
593fn block_key(block: &Block, typography: &Typography) -> u64 {
594    let mut hasher = DefaultHasher::new();
595    block.hash(&mut hasher);
596    typography.body.size().to_bits().hash(&mut hasher);
597    typography.body.line_height().to_bits().hash(&mut hasher);
598    hasher.finish()
599}
600
601/// What a block is placed at before it has ever been built.
602fn guess(block: &Block, typography: &Typography) -> Guess {
603    let body = px(typography.body.line_height());
604    let prose = |chars: usize, line: Pixels| Guess {
605        chars,
606        line,
607        rows: 0,
608        extra: px(0.0),
609        indent: px(0.0),
610    };
611    let guess = match &block.kind {
612        BlockKind::Paragraph(text)
613        | BlockKind::Bullet(text)
614        | BlockKind::Ordered { text, .. }
615        | BlockKind::Task { text, .. }
616        | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
617        BlockKind::Heading { level, text } => prose(
618            text.text.len(),
619            px(typography.heading(*level).line_height()),
620        ),
621        BlockKind::Code { code, .. } => Guess {
622            rows: code.text.lines().count().max(1),
623            line: px(typography.code.line_height()),
624            extra: px(2.0 * CODE_PADDING_Y) + body,
625            ..prose(0, body)
626        },
627        BlockKind::Table { rows, .. } => Guess {
628            rows: rows.len() + 1,
629            extra: px(8.0) * (rows.len() + 1) as f32,
630            ..prose(0, body)
631        },
632        BlockKind::Image { alt, .. } => Guess {
633            extra: px(240.0),
634            ..prose(alt.text.len(), px(typography.caption.line_height()))
635        },
636        BlockKind::Bookmark { .. } => Guess {
637            extra: body * 3.0,
638            ..prose(0, body)
639        },
640        BlockKind::Rule => Guess {
641            extra: body,
642            ..prose(0, px(0.0))
643        },
644    };
645    Guess {
646        indent: px(block.indent as f32 * INDENT_WIDTH),
647        ..guess
648    }
649}
650
651/// Whether two adjacent blocks belong to the same list and should sit close.
652fn tight(previous: &Block, next: &Block) -> bool {
653    let marker = |block: &Block| {
654        matches!(
655            block.kind,
656            BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
657        )
658    };
659    marker(previous) && (marker(next) || next.indent > previous.indent)
660}
661
662fn block_element(
663    block: &Block,
664    overlay: Overlay,
665    typography: &Typography,
666    theme: &Theme,
667    window: &mut Window,
668    cx: &mut App,
669) -> AnyElement {
670    let body = overlay.at(Part::Body);
671    match &block.kind {
672        BlockKind::Paragraph(text) => text_element(
673            text,
674            typography.body.size(),
675            typography.body.line_height(),
676            FontWeight::NORMAL,
677            body,
678            theme,
679            cx,
680        ),
681        BlockKind::Heading { level, text } => {
682            let heading = typography.heading(*level);
683            text_element(
684                text,
685                heading.size(),
686                heading.line_height(),
687                heading.weight,
688                body,
689                theme,
690                cx,
691            )
692        }
693        BlockKind::Bullet(text) => {
694            marker_row(disc(typography, theme), text, body, typography, theme, cx)
695        }
696        BlockKind::Ordered { number, text } => marker_row(
697            div()
698                .flex_none()
699                .w(px(MARKER_WIDTH))
700                .text_size(px(typography.body.size()))
701                .line_height(px(typography.body.line_height()))
702                .text_color(theme.text_muted)
703                .child(SharedString::from(format!("{number}.")))
704                .into_any_element(),
705            text,
706            body,
707            typography,
708            theme,
709            cx,
710        ),
711        BlockKind::Task { checked, text } => marker_row(
712            checkbox(*checked, overlay, typography, theme),
713            text,
714            body,
715            typography,
716            theme,
717            cx,
718        ),
719        BlockKind::Quote { kind, text } => div()
720            .border_l_2()
721            .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
722            .pl(px(12.0))
723            .pr(px(10.0))
724            .py(px(2.0))
725            .text_color(theme.text_muted)
726            .children(kind.map(|kind| {
727                div()
728                    .pb(px(2.0))
729                    .text_size(px(typography.body.size()))
730                    .line_height(px(typography.body.line_height()))
731                    .font_weight(FontWeight::SEMIBOLD)
732                    .text_color(alert_color(kind, theme))
733                    .child(kind.label())
734            }))
735            .child(text_element(
736                text,
737                typography.body.size(),
738                typography.body.line_height(),
739                FontWeight::NORMAL,
740                body,
741                theme,
742                cx,
743            ))
744            .into_any_element(),
745        BlockKind::Code { language, code } => {
746            let overlay = overlay.at(Part::Code);
747            // The caret in the fence gives the source back. A painted block is
748            // still an editable one, and typing into it otherwise edits what
749            // the reader cannot see.
750            let painted = overlay
751                .caret()
752                .is_none()
753                .then(|| block::render(language.as_deref(), &code.text, window, cx))
754                .flatten();
755            match painted {
756                // Painted, there is no text under the selection to carry it —
757                // the wash an opaque block gets at the container comes here.
758                Some(element) => div()
759                    .when(overlay.covers_block(), |el| {
760                        el.rounded(px(4.0)).bg(theme.selection)
761                    })
762                    .child(element)
763                    .into_any_element(),
764                None => code_block(
765                    language.as_deref(),
766                    &code.text,
767                    overlay,
768                    typography,
769                    theme,
770                    window,
771                    cx,
772                ),
773            }
774        }
775        BlockKind::Image { url, alt, width } => {
776            image(url, alt, *width, overlay, typography, theme, cx)
777        }
778        BlockKind::Bookmark { url, form } => {
779            bookmark(overlay.block, url, *form, typography, theme, cx)
780        }
781        BlockKind::Table {
782            align,
783            header,
784            rows,
785        } => table(align, header, rows, overlay, typography, theme, window, cx),
786        BlockKind::Rule => div()
787            .h(px(1.0))
788            .w_full()
789            .bg(theme.border)
790            .into_any_element(),
791    }
792}
793
794/// A real 5px disc rather than the "•" glyph, which reads too small at body size.
795fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
796    div()
797        .flex_none()
798        .w(px(MARKER_WIDTH))
799        .h(px(typography.body.line_height()))
800        .flex()
801        .items_center()
802        .child(
803            div()
804                .ml(px(1.0))
805                .w(px(5.0))
806                .h(px(5.0))
807                .rounded_full()
808                .bg(theme.text_faint),
809        )
810        .into_any_element()
811}
812
813fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
814    let ix = overlay.block;
815    let mut box_ = div()
816        .relative()
817        .w(px(13.0))
818        .h(px(13.0))
819        .rounded(px(3.5))
820        .border_1()
821        .flex()
822        .items_center()
823        .justify_center();
824    box_ = if checked {
825        box_.bg(theme.solid)
826            .border_color(theme.solid)
827            .text_style(TextStyle::Caption)
828            .text_color(theme.on_solid)
829            .child("✓")
830    } else {
831        box_.border_color(theme.border_strong)
832    };
833    // The box's own bounds rather than the marker column's: a caller hit-tests
834    // these to tell a toggle from a caret placed in the gutter beside it.
835    box_ = box_.children(overlay.layouts.map(|layouts| {
836        let layouts = layouts.clone();
837        canvas(
838            move |bounds, _, _| layouts.record_checkbox(ix, bounds),
839            |_, _, _, _| (),
840        )
841        .absolute()
842        .size_full()
843    }));
844    // The cursor answers to either variant: a box an editor hit-tests is as
845    // pressable as one the renderer listens to, and only the pointer says so.
846    if overlay.toggle.is_some() {
847        box_ = box_.cursor_pointer();
848    }
849    if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
850        box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
851            // Stopped, or the press goes on to whatever placed a caret
852            // under it and the toggle reads as a click that moved the
853            // caret as well.
854            cx.stop_propagation();
855            toggle(ix, window, cx);
856        });
857    }
858
859    div()
860        .flex_none()
861        .w(px(MARKER_WIDTH))
862        .h(px(typography.body.line_height()))
863        .flex()
864        .items_center()
865        .child(box_)
866        .into_any_element()
867}
868
869/// What an alert paints its rule and its label in.
870fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
871    match kind {
872        QuoteKind::Note => theme.accent,
873        QuoteKind::Tip => theme.success,
874        QuoteKind::Important => theme.busy,
875        QuoteKind::Warning => theme.warning,
876        QuoteKind::Caution => theme.danger,
877    }
878}
879
880fn marker_row(
881    marker: AnyElement,
882    text: &Text,
883    overlay: Overlay,
884    typography: &Typography,
885    theme: &Theme,
886    cx: &App,
887) -> AnyElement {
888    div()
889        .flex()
890        .flex_row()
891        .gap(px(MARKER_GAP))
892        .child(marker)
893        .child(div().flex_1().min_w_0().child(text_element(
894            text,
895            typography.body.size(),
896            typography.body.line_height(),
897            FontWeight::NORMAL,
898            overlay,
899            theme,
900            cx,
901        )))
902        .into_any_element()
903}