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