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