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};
19use ui::AppExt as _;
20use ui::widgets::Layout as _;
21
22use gpui::{
23    AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
24    ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
25    StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
26    canvas, div, font, img, point, prelude::*, px, quad, size,
27};
28use theme::{TextStyle, Theme, Typeset};
29
30use crate::{
31    block,
32    doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Run, Text, quote_runs},
33    preview,
34    select::{Affinity, Cursor, Selection},
35    typography::Typography,
36};
37
38/// Space between two ordinary blocks, and the tighter space inside a list.
39mod code;
40mod column;
41mod layouts;
42mod media;
43mod table;
44mod text;
45
46pub use code::*;
47use column::*;
48pub use layouts::*;
49use media::*;
50use table::*;
51pub use text::*;
52
53const BLOCK_GAP: f32 = 12.0;
54const LIST_GAP: f32 = 4.0;
55/// One indent level. Wide enough to clear a marker and read as a level.
56const INDENT_WIDTH: f32 = 22.0;
57/// The marker column of a list row.
58const MARKER_WIDTH: f32 = 18.0;
59const MARKER_GAP: f32 = 8.0;
60/// What a fence holds its code in, inside its border.
61const CODE_PADDING_X: f32 = 12.0;
62const CODE_PADDING_Y: f32 = 10.0;
63/// What a fence with no info string calls itself, in its header and in a
64/// picker — one spelling, so the label and the menu row cannot disagree.
65pub const PLAIN_LANGUAGE: &str = "Plain";
66/// Width of the caret. Wider than a hairline, because it has to read at a
67/// glance against the text it sits in.
68const CARET_WIDTH: f32 = 1.5;
69/// Inline code's wash is a rounded quad painted under the glyphs: a run's
70/// `background_color` can only ever be a square box.
71const INLINE_CODE_RADIUS: f32 = 4.5;
72const INLINE_CODE_PAD_X: f32 = 2.0;
73const INLINE_CODE_INSET_Y: f32 = 2.0;
74/// A mention's hover card.
75const MENTION_CARD_WIDTH: f32 = 320.0;
76/// Bookmark metrics. Notion's card: 180px of image beside the text, and a
77/// height that fits a title, two lines of blurb and a footer. A cover moves
78/// that image above the text and gives it the card's full width.
79const CARD_HEIGHT: f32 = 116.0;
80const CARD_IMAGE_WIDTH: f32 = 180.0;
81const CARD_COVER_HEIGHT: f32 = 200.0;
82const CARD_PADDING: f32 = 14.0;
83const CARD_BORDER: f32 = 1.0;
84const CARD_ICON: f32 = 16.0;
85const CARD_COVER: f32 = 44.0;
86/// Image metrics.
87const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
88const CAPTION_GAP: f32 = 4.0;
89/// What an image with no URL yet says, and what its caption says while empty.
90const IMAGE_EMPTY: &str = "Add an image";
91const CAPTION_HINT: &str = "Write a caption";
92/// Table metrics. The design is frameless: hairlines between rows are the only
93/// chrome — no outer box, no header fill, no rounding.
94const TABLE_CELL_PADDING: f32 = 12.0;
95/// Reserved lane for editor table controls, inside the block bounds.
96pub const TABLE_CONTROL_SIZE: f32 = 16.0;
97const TABLE_DIVIDER: f32 = 1.0;
98/// Floor for a column's max-content share, so a short column ("1k") beside a
99/// prose column keeps a readable width.
100const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
101/// Narrowest a column wraps down to before the table scrolls instead.
102const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
103
104/// What an image's one authored string is doing on the page.
105///
106/// SwiftUI keeps three things apart — `accessibilityLabel` for a reader that
107/// cannot see, `.help` for the pointer, and a caption you compose out of a
108/// `Text` under the picture. Markdown has one slot for all three, so this says
109/// which of them it is playing here rather than in the document, where it is
110/// the same string either way.
111///
112/// A named choice rather than a `bool`, so a surface that wants a third answer
113/// gets a variant instead of a second flag.
114#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
115pub enum Caption {
116    /// Under the picture, where a caret can sit in it. The editor's shape.
117    #[default]
118    Shown,
119    /// Kept by the document and painted nowhere — a picture on its own.
120    Hidden,
121}
122
123/// Whether a fence paints the button that copies its text.
124///
125/// A named choice rather than a `bool`, the way [`Caption`] is.
126#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
127pub enum CopyButton {
128    /// Floating at the top right of the band, on the pointer and off it.
129    #[default]
130    Shown,
131    /// Painted nowhere. A document with this and no [`Editing::toggle`] holds
132    /// no listener at all.
133    Hidden,
134}
135
136/// A range the caller wants washed, and which wash it gets.
137///
138/// None of what a comment or a highlight *says* is here: the caller keeps it
139/// and hands over the range, the way it hands over a [`crate::Preview`]. A
140/// closed set rather than a color, so the environment keeps deciding the paint.
141#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
142#[non_exhaustive]
143pub enum Annotation {
144    /// A thread still waiting on someone.
145    #[default]
146    Open,
147    /// Answered, and kept for the record.
148    Resolved,
149    /// The one whose thread the reader has in front of them.
150    Active,
151    /// A reader's highlight, in the wash [`crate::AppExt::set_highlight_paint`]
152    /// gives its colour.
153    Highlight(crate::HighlightColor),
154    /// A find hit.
155    Match,
156    /// The find hit the reader is on.
157    Current,
158}
159
160impl Annotation {
161    fn wash(self, theme: &Theme, highlight: crate::HighlightPaint, find: crate::FindPaint) -> Hsla {
162        match self {
163            Self::Open => theme.warning.opacity(0.20),
164            Self::Resolved => theme.warning.opacity(0.08),
165            Self::Active => theme.warning.opacity(0.38),
166            Self::Highlight(color) => highlight(color, theme),
167            Self::Match => find(theme).0,
168            Self::Current => find(theme).1,
169        }
170    }
171}
172
173/// Handed the block whose checkbox was clicked — see [`Toggle::Handled`].
174///
175/// Shared rather than borrowed: the press listener it is cloned into outlives
176/// the frame that built it.
177pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
178
179/// Handed the image block whose picture was clicked — see [`Editing::image`].
180pub type OnImage = Rc<dyn Fn(usize, &mut Window, &mut App)>;
181
182/// Handed the heading block a clicked `#fragment` link names — see
183/// [`Editing::jump`].
184pub type OnJump = Rc<dyn Fn(usize, &mut Window, &mut App)>;
185
186/// Builds a picture's hover control from its block index and original URL.
187pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
188
189/// Handed a fence's block index and its new code — see [`FenceHost::rewrite`].
190pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
191
192/// Handed the block a painted fence is leaving — see [`FenceHost::leave`].
193pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
194
195/// Handed a resizable painted block's index and where its grip was pressed —
196/// see [`FenceHost::resize`].
197pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
198
199/// What an editor lends the blocks an installed [`crate::BlockRenderer`]
200/// paints — see [`Editing::fence`].
201#[derive(Clone)]
202pub struct FenceHost {
203    /// Replaces the code of the fence at a block index, as an edit of the
204    /// document.
205    pub rewrite: OnRewrite,
206    /// Hands the keyboard back to the document from inside the painted block
207    /// at a block index. Dispatched by [`LeaveBlock`].
208    pub leave: OnLeave,
209    /// Starts a height drag off the grip along a resizable painted block's
210    /// foot, from the press's window `y`. `None` paints no grip.
211    pub resize: Option<OnResize>,
212}
213
214/// The key context around every painted fence. An editor's bindings stop at
215/// it, so keys typed into a control the block paints stay that control's.
216pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
217
218gpui::actions!(
219    markdown,
220    [
221        /// Return focus from inside a painted fence to the document around it.
222        LeaveBlock
223    ]
224);
225
226/// The picture corner holding an app's hover control.
227#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
228pub enum ImageOverlayCorner {
229    TopLeft,
230    TopRight,
231    BottomLeft,
232    #[default]
233    BottomRight,
234}
235
236/// Who answers a press on a task block's checkbox.
237///
238/// Either variant paints the box as a control — the pointer over it is a hand.
239/// [`Editing::toggle`] left unset paints it as a mark, and the press goes
240/// wherever it would on any other glyph.
241#[derive(Clone)]
242pub enum Toggle {
243    /// The box takes the press, stops it, and calls this with the block it
244    /// belongs to. For a caller holding the [`Doc`] it renders itself.
245    Handled(OnToggle),
246    /// The box takes no press. The caller hit-tests
247    /// [`BlockLayouts::checkbox_bounds`] in its own handler, which is what an
248    /// editor does: the press it swallows is the one that also takes focus and
249    /// closes an open menu, and the toggle belongs in the undo history beside
250    /// the rest of its edits.
251    HitTested,
252}
253
254/// What an editor paints over a document.
255///
256/// One value rather than six parameters, and the reason it is public: the
257/// caret, the selection, the comment washes and the layout sink all arrive
258/// together or not at all, and a read-only [`render`] sets none of them.
259#[derive(Clone)]
260pub struct Editing<'a> {
261    /// The caret and what it has selected. `None` paints neither — a document
262    /// nobody is editing.
263    pub selection: Option<Selection>,
264    /// The blink's lit half. A caret painted on every frame reads as frozen,
265    /// and the phase belongs to whoever owns the focus.
266    pub caret_on: bool,
267    /// Filled as the document paints, for a caller resolving clicks against it.
268    ///
269    /// Given, only the blocks near the part of the window the document shows
270    /// are built, and it answers for those alone. The rest are placed at their
271    /// last measured height, or a guess at one, and held in it across frames.
272    pub layouts: Option<&'a BlockLayouts>,
273    /// Ranges washed under the text, in the order given.
274    pub annotations: &'a [(Selection, Annotation)],
275    /// Shown on the caret's block while it holds nothing.
276    pub placeholder: Option<SharedString>,
277    pub caption: Caption,
278    /// What to set the document in. `None` takes the installed
279    /// [`Typography`] — a caller sizing one document apart from the rest
280    /// passes [`Typography::scaled`].
281    pub typography: Option<Typography>,
282    /// Makes a task block's checkbox a control, and says who answers the
283    /// press. `None` paints a mark.
284    pub toggle: Option<Toggle>,
285    /// Handles a picture click with its block index, keeping the arrow cursor. A
286    /// press and release more than a couple of pixels apart is a drag and
287    /// calls nothing. The press still reaches whatever is under the picture.
288    pub image: Option<OnImage>,
289    /// An app control inset at `image_overlay_corner`, visible on hover.
290    /// Its presses do not reach the picture. `None` adds no listeners.
291    pub image_overlay: Option<ImageOverlay>,
292    /// Corner for the picture control; defaults to bottom-right.
293    pub image_overlay_corner: ImageOverlayCorner,
294    /// Lets a painted fence rewrite its own code and give the keyboard back.
295    /// `None` paints a fence that can do neither.
296    pub fence: Option<FenceHost>,
297    /// A painted block held at a height while its resize handle is dragged:
298    /// the block index and the height, in place of the one the document says.
299    pub sizing: Option<(usize, u32)>,
300    /// Reserves lanes around tables for editor controls.
301    pub table_controls: bool,
302    /// Whether a fence offers to copy itself.
303    pub copy: CopyButton,
304    /// The directory a relative image path is joined onto. `None` leaves it
305    /// relative, which gpui reads against the process's working directory.
306    pub base: Option<&'a Path>,
307    /// Blocks built wherever they are, alongside the ones near the part of the
308    /// window the document shows — source lines, for [`render_source`]. The
309    /// caret's and the selection anchor's are built without being named. Only
310    /// read with `layouts` given.
311    pub keep: &'a [usize],
312    /// The scroll container the document sits in. When blocks above the text
313    /// showing come out taller or shorter than they were placed at, its offset
314    /// moves by the difference. Only read with `layouts` given.
315    pub scroll: Option<&'a gpui::ScrollHandle>,
316    /// Scrolls to the heading a clicked `#fragment` link names, matched by
317    /// [`crate::link::heading`]. `None`, or a fragment naming no heading,
318    /// makes the click do nothing.
319    pub jump: Option<OnJump>,
320}
321
322impl Default for Editing<'_> {
323    fn default() -> Self {
324        Self {
325            selection: None,
326            // Lit, so that a caller setting a selection and nothing else gets a
327            // caret rather than a mystery.
328            caret_on: true,
329            layouts: None,
330            annotations: &[],
331            placeholder: None,
332            caption: Caption::default(),
333            typography: None,
334            toggle: None,
335            image: None,
336            image_overlay: None,
337            image_overlay_corner: ImageOverlayCorner::BottomRight,
338            fence: None,
339            sizing: None,
340            table_controls: false,
341            copy: CopyButton::default(),
342            base: None,
343            keep: &[],
344            scroll: None,
345            jump: None,
346        }
347    }
348}
349
350/// What the editor needs painted into one text: which text it is, where the
351/// caret sits, and where to record the layout a click resolves against.
352///
353/// One bundle rather than four parameters threaded through every block arm —
354/// a read-only render builds it with no caret and no sink, and pays nothing.
355#[derive(Clone, Copy)]
356struct Overlay<'a> {
357    block: usize,
358    part: Part,
359    selection: Option<Selection>,
360    caret_on: bool,
361    caret_shape: ui::input::CaretShape,
362    caret_height: ui::input::CaretHeight,
363    caret_inactive: ui::input::InactiveCaret,
364    /// A block caret is hollow, and cuts no glyph out, while this is false.
365    window_active: bool,
366    layouts: Option<&'a BlockLayouts>,
367    /// Ranges washed under the text, in the order the caller gave them.
368    annotations: &'a [(Selection, Annotation)],
369    /// Shown on the caret's block while it holds nothing. The renderer is the
370    /// only thing that knows where that text sits, so the string comes to it.
371    placeholder: Option<&'a SharedString>,
372    caption: Caption,
373    /// Borrowed so [`Overlay`] stays `Copy` — the clone is made at the one
374    /// press listener that needs an owned handle.
375    toggle: Option<&'a Toggle>,
376    image: Option<&'a OnImage>,
377    image_overlay: Option<&'a ImageOverlay>,
378    image_overlay_corner: ImageOverlayCorner,
379    fence: Option<&'a FenceHost>,
380    sizing: Option<(usize, u32)>,
381    table_controls: bool,
382    copy: CopyButton,
383    base: Option<&'a Path>,
384    highlight: crate::HighlightPaint,
385    find: crate::FindPaint,
386    jump: Option<&'a crate::link::Jump>,
387}
388
389/// What a resizable painted block carries besides itself: where it landed,
390/// for the height drag, and the grip along its foot that starts one, shown
391/// while the block is hovered or being sized.
392fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
393    let ix = overlay.block;
394    let mut parts = Vec::new();
395    if let Some(layouts) = overlay.layouts {
396        let layouts = layouts.clone();
397        parts.push(
398            canvas(
399                move |bounds, _, _| layouts.record_painted(ix, bounds),
400                |_, _, _, _| (),
401            )
402            .absolute()
403            .size_full()
404            .into_any_element(),
405        );
406    }
407    if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
408        let dragging = overlay.held_height().is_some();
409        // Inside the block's foot rather than across its edge: the grip is
410        // shown while the block is hovered, and the pointer has to stay over
411        // the block to reach it. A strip only as wide as it needs, so the
412        // controls a block keeps in its corners stay pressable.
413        parts.push(
414            div()
415                .absolute()
416                .left_0()
417                .right_0()
418                .bottom_0()
419                .flex()
420                .justify_center()
421                .child(
422                    theme
423                        .grip_handle()
424                        .id(ElementId::named_usize("painted-grip", ix))
425                        .debug_selector(|| "painted-grip".into())
426                        .w(px(PAINTED_GRIP_WIDTH))
427                        .when(!dragging, |el| {
428                            el.invisible()
429                                .group_hover(painted_group(ix), |el| el.visible())
430                        })
431                        .on_mouse_down(MouseButton::Left, move |event, window, cx| {
432                            cx.stop_propagation();
433                            resize(ix, event.position.y, window, cx);
434                        }),
435                )
436                .into_any_element(),
437        );
438    }
439    parts
440}
441
442/// How wide the grip along a resizable painted block's foot can be pressed.
443const PAINTED_GRIP_WIDTH: f32 = 120.0;
444
445fn painted_group(ix: usize) -> SharedString {
446    SharedString::from(format!("painted-{ix}"))
447}
448
449impl<'a> Overlay<'a> {
450    fn at(self, part: Part) -> Self {
451        Self { part, ..self }
452    }
453
454    /// The height this block's resize handle is holding it at, if it is.
455    fn held_height(&self) -> Option<u32> {
456        self.sizing
457            .filter(|(ix, _)| *ix == self.block)
458            .map(|(_, height)| height)
459    }
460
461    fn here(&self) -> Cursor {
462        Cursor::new(self.block, self.part, 0)
463    }
464
465    /// The caret to paint: where it is, only on the blink's lit half, and
466    /// never over a non-empty selection.
467    ///
468    /// Separate from [`Self::caret`] because the blink must not reach anything
469    /// but the quad — a block whose paint depends on holding the caret would
470    /// otherwise swap itself out twice a second.
471    fn caret_painted(&self) -> Option<usize> {
472        let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
473        let shown = self
474            .caret_shape
475            .shown(self.window_active, self.caret_inactive);
476        (self.caret_on && collapsed && shown)
477            .then(|| self.caret())
478            .flatten()
479    }
480
481    /// The caret's row at a soft wrap.
482    fn affinity(&self) -> Affinity {
483        self.selection
484            .map_or_else(Affinity::default, |selection| selection.affinity)
485    }
486
487    /// The grapheme after the caret, which a solid block covers. None at the
488    /// end of a row before a wrap, where that grapheme starts the next row.
489    fn covered(&self, text: &str, offset: usize) -> Option<Range<usize>> {
490        (self.affinity() == Affinity::Downstream)
491            .then(|| glyph_at(text, offset))
492            .flatten()
493    }
494
495    fn caret_hollow(&self) -> bool {
496        !self.window_active
497    }
498
499    /// The caret's byte offset, if the head is in *this* text.
500    fn caret(&self) -> Option<usize> {
501        self.selection
502            .map(|selection| selection.head)
503            .filter(|head| head.block == self.block && head.part == self.part)
504            .map(|head| head.offset)
505    }
506
507    /// The selected slice of this text, clipped to it.
508    fn selected(&self, len: usize) -> Option<Range<usize>> {
509        self.clip(self.selection?, len)
510    }
511
512    /// The annotated slices of this text, already resolved to their paint —
513    /// the wash goes into a `move` closure that the theme does not travel into.
514    fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
515        self.annotations
516            .iter()
517            .filter_map(|(range, kind)| {
518                Some((
519                    self.clip(*range, len)?,
520                    kind.wash(theme, self.highlight, self.find),
521                ))
522            })
523            .collect()
524    }
525
526    /// A range clipped to this text, and `None` when it does not reach it.
527    ///
528    /// The comparison is on `(block, part)` alone: a range covers this text
529    /// entirely when it starts before and ends after, and the offsets only
530    /// matter at the two ends.
531    fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
532        if selection.is_collapsed() {
533            return None;
534        }
535        let (start, end) = selection.ordered();
536        let here = self.here();
537        let (first, last) = (
538            Cursor::new(start.block, start.part, 0),
539            Cursor::new(end.block, end.part, 0),
540        );
541        if here < first || here > last {
542            return None;
543        }
544        let from = if here == first { start.offset } else { 0 };
545        let to = if here == last { end.offset } else { len };
546        (from < to).then_some(from..to.min(len))
547    }
548
549    /// Whether a block painting something a caret cannot enter — a rule, a
550    /// picture — falls inside the selection, and so should show that it is
551    /// going to be taken.
552    fn covers_block(&self) -> bool {
553        let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
554            return false;
555        };
556        let (start, end) = selection.ordered();
557        start.block < self.block && self.block < end.block
558    }
559}
560
561/// What gpui loads for an image URL as written in a document.
562///
563/// Anything with `://` is fetched as it stands. Anything else is a file: an
564/// absolute path as it stands, a relative one joined onto `base` when there is
565/// one.
566pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
567    if url.contains("://") {
568        return SharedString::from(url.to_string()).into();
569    }
570    // gpui reads a file only from a `PathBuf` — handed a string it looks for
571    // an asset built into the binary and paints nothing.
572    match base {
573        Some(base) => base.join(url).into(),
574        None => std::path::PathBuf::from(url).into(),
575    }
576}
577
578/// Parse and render in one step — the common case for read-only content.
579pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
580    let doc = crate::parse_with(source, &cx.marks());
581    render(&doc, Caption::default(), window, cx)
582}
583
584/// Render a document.
585pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
586    render_with(
587        doc,
588        Editing {
589            caption,
590            ..Editing::default()
591        },
592        window,
593        cx,
594    )
595}
596
597/// Render a document with a caret and a selection in it.
598///
599/// Both are paint-time concerns and nothing else: they read their positions off
600/// the shaped text's own layout handle, the same way the inline-code wash does,
601/// so nothing about layout depends on where the caret sits. An editor supplies
602/// the selection and owns the focus and the keys; painting a caret and a few
603/// quads is not worth a second renderer.
604pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
605    let Editing {
606        selection,
607        caret_on,
608        layouts,
609        annotations,
610        placeholder,
611        caption,
612        typography,
613        toggle,
614        image,
615        image_overlay,
616        image_overlay_corner,
617        fence,
618        sizing,
619        table_controls,
620        copy,
621        base,
622        keep,
623        scroll,
624        jump,
625    } = editing;
626    // Cloned once so the theme is readable while `cx` stays free for the
627    // element state the copy button needs.
628    let theme = Theme::of(cx).clone();
629    let typography = typography.unwrap_or_else(|| cx.typography());
630    let highlight = crate::marks::highlight_paint_of(cx);
631    let find = crate::find::find_paint_of(cx);
632    let jump = jump.map(|to| crate::link::Jump::new(doc, to));
633    let gaps: Vec<Pixels> = doc
634        .blocks
635        .iter()
636        .enumerate()
637        .map(|(ix, block)| {
638            px(match doc.blocks.get(ix.wrapping_sub(1)) {
639                None => 0.0,
640                Some(previous) if tight(previous, block) => LIST_GAP,
641                Some(_) => BLOCK_GAP,
642            })
643        })
644        .collect();
645    let runs = quote_runs(doc.blocks.iter().map(|block| (block.quote, block.indent)));
646
647    let Some(layouts) = layouts else {
648        let mut column = div().flex().flex_col();
649        for (ix, block) in doc.blocks.iter().enumerate() {
650            let overlay = Overlay {
651                block: ix,
652                part: Part::Body,
653                selection,
654                caret_on,
655                caret_shape: cx.caret_shape(),
656                caret_height: cx.caret_height(),
657                caret_inactive: cx.inactive_caret(),
658                window_active: window.is_window_active(),
659                layouts: None,
660                annotations,
661                placeholder: placeholder.as_ref(),
662                caption,
663                toggle: toggle.as_ref(),
664                image: image.as_ref(),
665                image_overlay: image_overlay.as_ref(),
666                image_overlay_corner,
667                fence: fence.as_ref(),
668                sizing,
669                table_controls,
670                copy,
671                base,
672                highlight,
673                find,
674                jump: jump.as_ref(),
675            };
676            let quote = Quote {
677                run: runs[ix],
678                gap: gaps[ix],
679            };
680            column = column.child(
681                block_box(block, quote, overlay, &typography, &theme, window, cx).mt(gaps[ix]),
682            );
683        }
684        return column.into_any_element();
685    };
686
687    let keys: Vec<u64> = doc
688        .blocks
689        .iter()
690        .zip(&runs)
691        .map(|(block, run)| block_key(block, *run, &typography, table_controls))
692        .collect();
693    layouts.prune(&keys);
694    let guesses: Vec<Guess> = doc
695        .blocks
696        .iter()
697        .zip(&runs)
698        .map(|(block, run)| {
699            let mut guess = guess(block, *run, &typography);
700            if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
701                guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
702            }
703            guess
704        })
705        .collect();
706    let mut kept = keep.to_vec();
707    kept.extend(selection.map(|selection| selection.head.block));
708    kept.extend(selection.map(|selection| selection.anchor.block));
709    let owned = Owned {
710        blocks: doc.blocks.clone(),
711        runs,
712        gaps: gaps.clone(),
713        selection,
714        caret_on,
715        layouts: layouts.clone(),
716        annotations: annotations.to_vec(),
717        placeholder,
718        caption,
719        toggle,
720        image,
721        image_overlay,
722        image_overlay_corner,
723        fence,
724        sizing,
725        table_controls,
726        copy,
727        base: base.map(Path::to_path_buf),
728        highlight,
729        find,
730        jump,
731        typography,
732        theme,
733    };
734    Column {
735        layouts: layouts.clone(),
736        keys: keys.into(),
737        gaps: gaps.into(),
738        guesses: guesses.into(),
739        keep: kept,
740        scroll: scroll.cloned(),
741        item_of: Box::new(|at| at.block),
742        build: Box::new(move |ix, window, cx| {
743            let overlay = Overlay {
744                block: ix,
745                part: Part::Body,
746                selection: owned.selection,
747                caret_on: owned.caret_on,
748                caret_shape: cx.caret_shape(),
749                caret_height: cx.caret_height(),
750                caret_inactive: cx.inactive_caret(),
751                window_active: window.is_window_active(),
752                layouts: Some(&owned.layouts),
753                annotations: &owned.annotations,
754                placeholder: owned.placeholder.as_ref(),
755                caption: owned.caption,
756                toggle: owned.toggle.as_ref(),
757                image: owned.image.as_ref(),
758                image_overlay: owned.image_overlay.as_ref(),
759                image_overlay_corner: owned.image_overlay_corner,
760                fence: owned.fence.as_ref(),
761                sizing: owned.sizing,
762                table_controls: owned.table_controls,
763                copy: owned.copy,
764                base: owned.base.as_deref(),
765                highlight: owned.highlight,
766                find: owned.find,
767                jump: owned.jump.as_ref(),
768            };
769            let quote = Quote {
770                run: owned.runs[ix],
771                gap: owned.gaps[ix],
772            };
773            block_box(
774                &owned.blocks[ix],
775                quote,
776                overlay,
777                &owned.typography,
778                &owned.theme,
779                window,
780                cx,
781            )
782            .into_any_element()
783        }),
784    }
785    .into_any_element()
786}
787
788/// What [`Column`] builds a block from, owned so it can build one at prepaint.
789struct Owned {
790    blocks: Vec<Block>,
791    runs: Vec<Option<Run>>,
792    gaps: Vec<Pixels>,
793    selection: Option<Selection>,
794    caret_on: bool,
795    layouts: BlockLayouts,
796    annotations: Vec<(Selection, Annotation)>,
797    placeholder: Option<SharedString>,
798    caption: Caption,
799    toggle: Option<Toggle>,
800    image: Option<OnImage>,
801    image_overlay: Option<ImageOverlay>,
802    image_overlay_corner: ImageOverlayCorner,
803    fence: Option<FenceHost>,
804    sizing: Option<(usize, u32)>,
805    table_controls: bool,
806    copy: CopyButton,
807    base: Option<std::path::PathBuf>,
808    highlight: crate::HighlightPaint,
809    find: crate::FindPaint,
810    jump: Option<crate::link::Jump>,
811    typography: Typography,
812    theme: Theme,
813}
814
815/// Where a block stands in a blockquote, and the gap above it that a bar
816/// carried on from the block before has to cross.
817#[derive(Clone, Copy)]
818struct Quote {
819    run: Option<Run>,
820    gap: Pixels,
821}
822
823/// The space between a blockquote's bar and the text inside it.
824const QUOTE_INSET: f32 = 12.0;
825const QUOTE_BAR: f32 = 2.0;
826
827/// A block's box: its indent outside, and inside it the recorder and the
828/// block itself.
829fn block_box(
830    block: &Block,
831    quote: Quote,
832    overlay: Overlay,
833    typography: &Typography,
834    theme: &Theme,
835    window: &mut Window,
836    cx: &mut App,
837) -> gpui::Div {
838    let ix = overlay.block;
839    // The block's own box, recorded for a gutter handle and a drop target.
840    // A rule and an image hold no text, so a layout would not find them.
841    let frame = overlay.layouts.map(|layouts| {
842        let layouts = layouts.clone();
843        canvas(
844            move |bounds, _, _| layouts.record_block(ix, bounds),
845            |_, _, _, _| (),
846        )
847        .absolute()
848        .size_full()
849    });
850    // The indent sits on the outside and the recorder on the inside, so what
851    // is recorded is the box the block's text actually occupies. Recorded
852    // outside the padding, every level answered with the same left edge, and a
853    // gutter handle placed from it stayed at the margin while the block it
854    // belongs to moved right.
855    div()
856        .w_full()
857        // Everything a block draws — cards, bands, captions — is set in the
858        // document's face unless it names its own.
859        .font_family(theme.font_body.clone())
860        .pl(px(block.indent as f32 * INDENT_WIDTH))
861        .child(
862            div()
863                .w_full()
864                .relative()
865                .children(frame)
866                // What a caret cannot enter still has to show it is inside the
867                // selection, or a rule between two paragraphs looks untouched
868                // right up until it disappears.
869                .when(overlay.covers_block() && block.opaque(), |el| {
870                    el.rounded(px(4.0)).bg(theme.selection)
871                })
872                .child(quoted(
873                    block,
874                    quote,
875                    block_element(block, overlay, typography, theme, window, cx),
876                    typography,
877                    theme,
878                )),
879        )
880}
881
882/// A block inside a blockquote: the bar at the quote's level, the alert's
883/// label over the first block, and the block itself inset from the bar.
884///
885/// Each block paints its own piece of the bar, reaching up across the gap to
886/// the block before it, so a run shows one unbroken bar.
887fn quoted(
888    block: &Block,
889    quote: Quote,
890    element: AnyElement,
891    typography: &Typography,
892    theme: &Theme,
893) -> AnyElement {
894    let (Some(run), Some(quoted)) = (quote.run, block.quote) else {
895        return element;
896    };
897    let color = quoted
898        .alert
899        .map_or(theme.border_strong, |alert| alert_color(alert, theme));
900    let label = quoted.alert.filter(|_| run.first).map(|alert| {
901        div()
902            .pb(px(2.0))
903            .text_size(px(typography.body.size()))
904            .line_height(px(typography.body.line_height()))
905            .font_weight(FontWeight::SEMIBOLD)
906            .text_color(color)
907            .child(alert.label())
908    });
909    let bar = div()
910        .absolute()
911        .left(px(
912            -((block.indent - run.level.min(block.indent)) as f32) * INDENT_WIDTH
913        ))
914        .top(if run.first { px(0.0) } else { -quote.gap })
915        .bottom_0()
916        .w(px(QUOTE_BAR))
917        .bg(color);
918    div()
919        .child(bar)
920        .pl(px(QUOTE_BAR + QUOTE_INSET))
921        .pr(px(10.0))
922        .py(px(2.0))
923        .text_color(theme.text_muted)
924        .children(label)
925        .child(element)
926        .into_any_element()
927}
928
929/// What a block's height is cached under: its content and the type it is set
930/// in, so an edit elsewhere that shifts its index keeps the height.
931fn block_key(
932    block: &Block,
933    run: Option<Run>,
934    typography: &Typography,
935    table_controls: bool,
936) -> u64 {
937    let mut hasher = DefaultHasher::new();
938    table_controls.hash(&mut hasher);
939    block.hash(&mut hasher);
940    run.map(|run| run.first).hash(&mut hasher);
941    typography.body.size().to_bits().hash(&mut hasher);
942    typography.body.line_height().to_bits().hash(&mut hasher);
943    hasher.finish()
944}
945
946/// What a block is placed at before it has ever been built.
947fn guess(block: &Block, run: Option<Run>, typography: &Typography) -> Guess {
948    let body = px(typography.body.line_height());
949    let prose = |chars: usize, line: Pixels| Guess {
950        chars,
951        line,
952        rows: 0,
953        extra: px(0.0),
954        indent: px(0.0),
955    };
956    let guess = match &block.kind {
957        BlockKind::Paragraph(text)
958        | BlockKind::Bullet(text)
959        | BlockKind::Ordered { text, .. }
960        | BlockKind::Task { text, .. } => prose(text.text.len(), body),
961        BlockKind::Heading { level, text } => prose(
962            text.text.len(),
963            px(typography.heading(*level).line_height()),
964        ),
965        BlockKind::Code { code, .. } => Guess {
966            rows: code.text.lines().count().max(1),
967            line: px(typography.code.line_height()),
968            extra: px(2.0 * CODE_PADDING_Y) + body,
969            ..prose(0, body)
970        },
971        BlockKind::Table { rows, .. } => Guess {
972            rows: rows.len() + 1,
973            extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
974            ..prose(0, body)
975        },
976        BlockKind::Image { alt, .. } => Guess {
977            extra: px(240.0),
978            ..prose(alt.text.len(), px(typography.caption.line_height()))
979        },
980        BlockKind::Bookmark { .. } => Guess {
981            extra: body * 3.0,
982            ..prose(0, body)
983        },
984        BlockKind::Rule => Guess {
985            extra: body,
986            ..prose(0, px(0.0))
987        },
988    };
989    let quoted = run.is_some();
990    let label = run.is_some_and(|run| run.first) && block.quote.is_some_and(|q| q.alert.is_some());
991    Guess {
992        indent: px(block.indent as f32 * INDENT_WIDTH)
993            + if quoted {
994                px(QUOTE_BAR + QUOTE_INSET + 10.0)
995            } else {
996                px(0.0)
997            },
998        extra: guess.extra
999            + if quoted { px(4.0) } else { px(0.0) }
1000            + if label { body + px(2.0) } else { px(0.0) },
1001        ..guess
1002    }
1003}
1004
1005/// Whether two adjacent blocks belong to the same list and should sit close.
1006fn tight(previous: &Block, next: &Block) -> bool {
1007    let marker = |block: &Block| {
1008        matches!(
1009            block.kind,
1010            BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
1011        )
1012    };
1013    marker(previous) && (marker(next) || next.indent > previous.indent)
1014}
1015
1016fn block_element(
1017    block: &Block,
1018    overlay: Overlay,
1019    typography: &Typography,
1020    theme: &Theme,
1021    window: &mut Window,
1022    cx: &mut App,
1023) -> AnyElement {
1024    let body = overlay.at(Part::Body);
1025    match &block.kind {
1026        BlockKind::Paragraph(text) => text_element(
1027            text,
1028            typography.body.size(),
1029            typography.body.line_height(),
1030            FontWeight::NORMAL,
1031            body,
1032            theme,
1033            cx,
1034        ),
1035        BlockKind::Heading { level, text } => {
1036            let heading = typography.heading(*level);
1037            text_element(
1038                text,
1039                heading.size(),
1040                heading.line_height(),
1041                heading.weight,
1042                body,
1043                theme,
1044                cx,
1045            )
1046        }
1047        BlockKind::Bullet(text) => {
1048            marker_row(disc(typography, theme), text, body, typography, theme, cx)
1049        }
1050        BlockKind::Ordered { number, text } => marker_row(
1051            div()
1052                .flex_none()
1053                .w(px(MARKER_WIDTH))
1054                .text_size(px(typography.body.size()))
1055                .line_height(px(typography.body.line_height()))
1056                .text_color(theme.text_muted)
1057                .child(SharedString::from(format!("{number}.")))
1058                .into_any_element(),
1059            text,
1060            body,
1061            typography,
1062            theme,
1063            cx,
1064        ),
1065        BlockKind::Task { checked, text } => marker_row(
1066            checkbox(*checked, overlay, typography, theme),
1067            text,
1068            body,
1069            typography,
1070            theme,
1071            cx,
1072        ),
1073        BlockKind::Code {
1074            language,
1075            code,
1076            height,
1077        } => {
1078            let overlay = overlay.at(Part::Code);
1079            let height = overlay.held_height().or(*height);
1080            // The caret in the fence gives the source back. A painted block is
1081            // still an editable one, and typing into it otherwise edits what
1082            // the reader cannot see.
1083            let painted = overlay
1084                .caret()
1085                .is_none()
1086                .then(|| {
1087                    let fence = block::Fence {
1088                        language: language.as_deref()?,
1089                        code: &code.text,
1090                        height,
1091                        rewrite: overlay.fence.map(|host| {
1092                            let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
1093                            Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
1094                                rewrite(ix, code, window, cx)
1095                            }) as block::Rewrite
1096                        }),
1097                    };
1098                    block::render(&fence, window, cx)
1099                })
1100                .flatten();
1101            match painted {
1102                // Painted, there is no text under the selection to carry it —
1103                // the wash an opaque block gets at the container comes here.
1104                Some(element) => div()
1105                    .key_context(PAINTED_CONTEXT)
1106                    .relative()
1107                    .when_some(overlay.fence, |el, host| {
1108                        let (leave, ix) = (host.leave.clone(), overlay.block);
1109                        el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1110                    })
1111                    .when(overlay.covers_block(), |el| {
1112                        el.rounded(px(4.0)).bg(theme.selection)
1113                    })
1114                    .group(painted_group(overlay.block))
1115                    .child(element)
1116                    .children(resizable(&overlay, theme))
1117                    .into_any_element(),
1118                None => code_block(
1119                    language.as_deref(),
1120                    &code.text,
1121                    overlay,
1122                    typography,
1123                    theme,
1124                    window,
1125                    cx,
1126                ),
1127            }
1128        }
1129        BlockKind::Image { url, alt, width } => {
1130            image(url, alt, *width, overlay, typography, theme, window, cx)
1131        }
1132        BlockKind::Bookmark { url, form } => match crate::preview::card(
1133            url,
1134            match (*form, overlay.held_height()) {
1135                (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1136                (form, _) => form,
1137            },
1138            window,
1139            cx,
1140        ) {
1141            // The app's own, painted where a fence's would be and kept apart
1142            // from the editor's keys the same way.
1143            Some(element) => div()
1144                .key_context(PAINTED_CONTEXT)
1145                .relative()
1146                .when_some(overlay.fence, |el, host| {
1147                    let (leave, ix) = (host.leave.clone(), overlay.block);
1148                    el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1149                })
1150                .when(overlay.covers_block(), |el| {
1151                    el.rounded(px(4.0)).bg(theme.selection)
1152                })
1153                .group(painted_group(overlay.block))
1154                .child(element)
1155                .children(match form {
1156                    Form::Embed(_) => resizable(&overlay, theme),
1157                    _ => Vec::new(),
1158                })
1159                .into_any_element(),
1160            None => bookmark(
1161                overlay.block,
1162                url,
1163                overlay.base,
1164                *form,
1165                typography,
1166                theme,
1167                cx,
1168            ),
1169        },
1170        BlockKind::Table {
1171            align,
1172            header,
1173            rows,
1174        } => table(align, header, rows, overlay, typography, theme, window, cx),
1175        BlockKind::Rule => div()
1176            .h(px(1.0))
1177            .w_full()
1178            .bg(theme.border)
1179            .into_any_element(),
1180    }
1181}
1182
1183/// A real 5px disc rather than the "•" glyph, which reads too small at body size.
1184fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1185    div()
1186        .flex_none()
1187        .w(px(MARKER_WIDTH))
1188        .h(px(typography.body.line_height()))
1189        .flex()
1190        .items_center()
1191        .child(
1192            div()
1193                .ml(px(1.0))
1194                .w(px(5.0))
1195                .h(px(5.0))
1196                .rounded_full()
1197                .bg(theme.text_faint),
1198        )
1199        .into_any_element()
1200}
1201
1202fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1203    let ix = overlay.block;
1204    let mut box_ = div()
1205        .relative()
1206        .w(px(13.0))
1207        .h(px(13.0))
1208        .rounded(px(3.5))
1209        .border_1()
1210        .flex()
1211        .items_center()
1212        .justify_center();
1213    box_ = if checked {
1214        box_.bg(theme.solid)
1215            .border_color(theme.solid)
1216            .text_style(TextStyle::Caption)
1217            .text_color(theme.on_solid)
1218            .child("✓")
1219    } else {
1220        box_.border_color(theme.border_strong)
1221    };
1222    // The box's own bounds rather than the marker column's: a caller hit-tests
1223    // these to tell a toggle from a caret placed in the gutter beside it.
1224    box_ = box_.children(overlay.layouts.map(|layouts| {
1225        let layouts = layouts.clone();
1226        canvas(
1227            move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1228            |_, _, _, _| (),
1229        )
1230        .absolute()
1231        .size_full()
1232    }));
1233    // The cursor answers to either variant: a box an editor hit-tests is as
1234    // pressable as one the renderer listens to, and only the pointer says so.
1235    if overlay.toggle.is_some() {
1236        box_ = box_.cursor_pointer();
1237    }
1238    if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1239        box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1240            // Stopped, or the press goes on to whatever placed a caret
1241            // under it and the toggle reads as a click that moved the
1242            // caret as well.
1243            cx.stop_propagation();
1244            toggle(ix, window, cx);
1245        });
1246    }
1247
1248    div()
1249        .flex_none()
1250        .w(px(MARKER_WIDTH))
1251        .h(px(typography.body.line_height()))
1252        .flex()
1253        .items_center()
1254        .child(box_)
1255        .into_any_element()
1256}
1257
1258/// What an alert paints its rule and its label in.
1259fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1260    match kind {
1261        QuoteKind::Note => theme.accent,
1262        QuoteKind::Tip => theme.success,
1263        QuoteKind::Important => theme.busy,
1264        QuoteKind::Warning => theme.warning,
1265        QuoteKind::Caution => theme.danger,
1266    }
1267}
1268
1269fn marker_row(
1270    marker: AnyElement,
1271    text: &Text,
1272    overlay: Overlay,
1273    typography: &Typography,
1274    theme: &Theme,
1275    cx: &App,
1276) -> AnyElement {
1277    div()
1278        .flex()
1279        .flex_row()
1280        .gap(px(MARKER_GAP))
1281        .child(marker)
1282        .child(div().flex_auto().min_w_0().child(text_element(
1283            text,
1284            typography.body.size(),
1285            typography.body.line_height(),
1286            FontWeight::NORMAL,
1287            overlay,
1288            theme,
1289            cx,
1290        )))
1291        .into_any_element()
1292}