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, Text},
33    preview,
34    select::{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/// Builds a picture's hover control from its block index and original URL.
183pub type ImageOverlay = Rc<dyn Fn(usize, &str, &mut Window, &mut App) -> Option<AnyElement>>;
184
185/// Handed a fence's block index and its new code — see [`FenceHost::rewrite`].
186pub type OnRewrite = Rc<dyn Fn(usize, String, &mut Window, &mut App)>;
187
188/// Handed the block a painted fence is leaving — see [`FenceHost::leave`].
189pub type OnLeave = Rc<dyn Fn(usize, &mut Window, &mut App)>;
190
191/// Handed a resizable painted block's index and where its grip was pressed —
192/// see [`FenceHost::resize`].
193pub type OnResize = Rc<dyn Fn(usize, Pixels, &mut Window, &mut App)>;
194
195/// What an editor lends the blocks an installed [`crate::BlockRenderer`]
196/// paints — see [`Editing::fence`].
197#[derive(Clone)]
198pub struct FenceHost {
199    /// Replaces the code of the fence at a block index, as an edit of the
200    /// document.
201    pub rewrite: OnRewrite,
202    /// Hands the keyboard back to the document from inside the painted block
203    /// at a block index. Dispatched by [`LeaveBlock`].
204    pub leave: OnLeave,
205    /// Starts a height drag off the grip along a resizable painted block's
206    /// foot, from the press's window `y`. `None` paints no grip.
207    pub resize: Option<OnResize>,
208}
209
210/// The key context around every painted fence. An editor's bindings stop at
211/// it, so keys typed into a control the block paints stay that control's.
212pub const PAINTED_CONTEXT: &str = "MarkdownPaintedBlock";
213
214gpui::actions!(
215    markdown,
216    [
217        /// Return focus from inside a painted fence to the document around it.
218        LeaveBlock
219    ]
220);
221
222/// The picture corner holding an app's hover control.
223#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
224pub enum ImageOverlayCorner {
225    TopLeft,
226    TopRight,
227    BottomLeft,
228    #[default]
229    BottomRight,
230}
231
232/// Who answers a press on a task block's checkbox.
233///
234/// Either variant paints the box as a control — the pointer over it is a hand.
235/// [`Editing::toggle`] left unset paints it as a mark, and the press goes
236/// wherever it would on any other glyph.
237#[derive(Clone)]
238pub enum Toggle {
239    /// The box takes the press, stops it, and calls this with the block it
240    /// belongs to. For a caller holding the [`Doc`] it renders itself.
241    Handled(OnToggle),
242    /// The box takes no press. The caller hit-tests
243    /// [`BlockLayouts::checkbox_bounds`] in its own handler, which is what an
244    /// editor does: the press it swallows is the one that also takes focus and
245    /// closes an open menu, and the toggle belongs in the undo history beside
246    /// the rest of its edits.
247    HitTested,
248}
249
250/// What an editor paints over a document.
251///
252/// One value rather than six parameters, and the reason it is public: the
253/// caret, the selection, the comment washes and the layout sink all arrive
254/// together or not at all, and a read-only [`render`] sets none of them.
255#[derive(Clone)]
256pub struct Editing<'a> {
257    /// The caret and what it has selected. `None` paints neither — a document
258    /// nobody is editing.
259    pub selection: Option<Selection>,
260    /// The blink's lit half. A caret painted on every frame reads as frozen,
261    /// and the phase belongs to whoever owns the focus.
262    pub caret_on: bool,
263    /// Filled as the document paints, for a caller resolving clicks against it.
264    ///
265    /// Given, only the blocks near the part of the window the document shows
266    /// are built, and it answers for those alone. The rest are placed at their
267    /// last measured height, or a guess at one, and held in it across frames.
268    pub layouts: Option<&'a BlockLayouts>,
269    /// Ranges washed under the text, in the order given.
270    pub annotations: &'a [(Selection, Annotation)],
271    /// Shown on the caret's block while it holds nothing.
272    pub placeholder: Option<SharedString>,
273    pub caption: Caption,
274    /// What to set the document in. `None` takes the installed
275    /// [`Typography`] — a caller sizing one document apart from the rest
276    /// passes [`Typography::scaled`].
277    pub typography: Option<Typography>,
278    /// Makes a task block's checkbox a control, and says who answers the
279    /// press. `None` paints a mark.
280    pub toggle: Option<Toggle>,
281    /// Handles a picture click with its block index, keeping the arrow cursor. A
282    /// press and release more than a couple of pixels apart is a drag and
283    /// calls nothing. The press still reaches whatever is under the picture.
284    pub image: Option<OnImage>,
285    /// An app control inset at `image_overlay_corner`, visible on hover.
286    /// Its presses do not reach the picture. `None` adds no listeners.
287    pub image_overlay: Option<ImageOverlay>,
288    /// Corner for the picture control; defaults to bottom-right.
289    pub image_overlay_corner: ImageOverlayCorner,
290    /// Lets a painted fence rewrite its own code and give the keyboard back.
291    /// `None` paints a fence that can do neither.
292    pub fence: Option<FenceHost>,
293    /// A painted block held at a height while its resize handle is dragged:
294    /// the block index and the height, in place of the one the document says.
295    pub sizing: Option<(usize, u32)>,
296    /// Reserves lanes around tables for editor controls.
297    pub table_controls: bool,
298    /// Whether a fence offers to copy itself.
299    pub copy: CopyButton,
300    /// The directory a relative image path is joined onto. `None` leaves it
301    /// relative, which gpui reads against the process's working directory.
302    pub base: Option<&'a Path>,
303    /// Blocks built wherever they are, alongside the ones near the part of the
304    /// window the document shows — source lines, for [`render_source`]. The
305    /// caret's and the selection anchor's are built without being named. Only
306    /// read with `layouts` given.
307    pub keep: &'a [usize],
308    /// The scroll container the document sits in. When blocks above the text
309    /// showing come out taller or shorter than they were placed at, its offset
310    /// moves by the difference. Only read with `layouts` given.
311    pub scroll: Option<&'a gpui::ScrollHandle>,
312}
313
314impl Default for Editing<'_> {
315    fn default() -> Self {
316        Self {
317            selection: None,
318            // Lit, so that a caller setting a selection and nothing else gets a
319            // caret rather than a mystery.
320            caret_on: true,
321            layouts: None,
322            annotations: &[],
323            placeholder: None,
324            caption: Caption::default(),
325            typography: None,
326            toggle: None,
327            image: None,
328            image_overlay: None,
329            image_overlay_corner: ImageOverlayCorner::BottomRight,
330            fence: None,
331            sizing: None,
332            table_controls: false,
333            copy: CopyButton::default(),
334            base: None,
335            keep: &[],
336            scroll: None,
337        }
338    }
339}
340
341/// What the editor needs painted into one text: which text it is, where the
342/// caret sits, and where to record the layout a click resolves against.
343///
344/// One bundle rather than four parameters threaded through every block arm —
345/// a read-only render builds it with no caret and no sink, and pays nothing.
346#[derive(Clone, Copy)]
347struct Overlay<'a> {
348    block: usize,
349    part: Part,
350    selection: Option<Selection>,
351    caret_on: bool,
352    caret_shape: ui::input::CaretShape,
353    caret_height: ui::input::CaretHeight,
354    caret_inactive: ui::input::InactiveCaret,
355    /// A block caret is hollow, and cuts no glyph out, while this is false.
356    window_active: bool,
357    layouts: Option<&'a BlockLayouts>,
358    /// Ranges washed under the text, in the order the caller gave them.
359    annotations: &'a [(Selection, Annotation)],
360    /// Shown on the caret's block while it holds nothing. The renderer is the
361    /// only thing that knows where that text sits, so the string comes to it.
362    placeholder: Option<&'a SharedString>,
363    caption: Caption,
364    /// Borrowed so [`Overlay`] stays `Copy` — the clone is made at the one
365    /// press listener that needs an owned handle.
366    toggle: Option<&'a Toggle>,
367    image: Option<&'a OnImage>,
368    image_overlay: Option<&'a ImageOverlay>,
369    image_overlay_corner: ImageOverlayCorner,
370    fence: Option<&'a FenceHost>,
371    sizing: Option<(usize, u32)>,
372    table_controls: bool,
373    copy: CopyButton,
374    base: Option<&'a Path>,
375    highlight: crate::HighlightPaint,
376    find: crate::FindPaint,
377}
378
379/// What a resizable painted block carries besides itself: where it landed,
380/// for the height drag, and the grip along its foot that starts one, shown
381/// while the block is hovered or being sized.
382fn resizable(overlay: &Overlay, theme: &Theme) -> Vec<AnyElement> {
383    let ix = overlay.block;
384    let mut parts = Vec::new();
385    if let Some(layouts) = overlay.layouts {
386        let layouts = layouts.clone();
387        parts.push(
388            canvas(
389                move |bounds, _, _| layouts.record_painted(ix, bounds),
390                |_, _, _, _| (),
391            )
392            .absolute()
393            .size_full()
394            .into_any_element(),
395        );
396    }
397    if let Some(resize) = overlay.fence.and_then(|host| host.resize.clone()) {
398        let dragging = overlay.held_height().is_some();
399        // Inside the block's foot rather than across its edge: the grip is
400        // shown while the block is hovered, and the pointer has to stay over
401        // the block to reach it. A strip only as wide as it needs, so the
402        // controls a block keeps in its corners stay pressable.
403        parts.push(
404            div()
405                .absolute()
406                .left_0()
407                .right_0()
408                .bottom_0()
409                .flex()
410                .justify_center()
411                .child(
412                    theme
413                        .grip_handle()
414                        .id(ElementId::named_usize("painted-grip", ix))
415                        .debug_selector(|| "painted-grip".into())
416                        .w(px(PAINTED_GRIP_WIDTH))
417                        .when(!dragging, |el| {
418                            el.invisible()
419                                .group_hover(painted_group(ix), |el| el.visible())
420                        })
421                        .on_mouse_down(MouseButton::Left, move |event, window, cx| {
422                            cx.stop_propagation();
423                            resize(ix, event.position.y, window, cx);
424                        }),
425                )
426                .into_any_element(),
427        );
428    }
429    parts
430}
431
432/// How wide the grip along a resizable painted block's foot can be pressed.
433const PAINTED_GRIP_WIDTH: f32 = 120.0;
434
435fn painted_group(ix: usize) -> SharedString {
436    SharedString::from(format!("painted-{ix}"))
437}
438
439impl<'a> Overlay<'a> {
440    fn at(self, part: Part) -> Self {
441        Self { part, ..self }
442    }
443
444    /// The height this block's resize handle is holding it at, if it is.
445    fn held_height(&self) -> Option<u32> {
446        self.sizing
447            .filter(|(ix, _)| *ix == self.block)
448            .map(|(_, height)| height)
449    }
450
451    fn here(&self) -> Cursor {
452        Cursor::new(self.block, self.part, 0)
453    }
454
455    /// The caret to paint: where it is, only on the blink's lit half, and
456    /// never over a non-empty selection.
457    ///
458    /// Separate from [`Self::caret`] because the blink must not reach anything
459    /// but the quad — a block whose paint depends on holding the caret would
460    /// otherwise swap itself out twice a second.
461    fn caret_painted(&self) -> Option<usize> {
462        let collapsed = self.selection.is_some_and(|s| s.is_collapsed());
463        let shown = self
464            .caret_shape
465            .shown(self.window_active, self.caret_inactive);
466        (self.caret_on && collapsed && shown)
467            .then(|| self.caret())
468            .flatten()
469    }
470
471    fn caret_hollow(&self) -> bool {
472        !self.window_active
473    }
474
475    /// The caret's byte offset, if the head is in *this* text.
476    fn caret(&self) -> Option<usize> {
477        self.selection
478            .map(|selection| selection.head)
479            .filter(|head| head.block == self.block && head.part == self.part)
480            .map(|head| head.offset)
481    }
482
483    /// The selected slice of this text, clipped to it.
484    fn selected(&self, len: usize) -> Option<Range<usize>> {
485        self.clip(self.selection?, len)
486    }
487
488    /// The annotated slices of this text, already resolved to their paint —
489    /// the wash goes into a `move` closure that the theme does not travel into.
490    fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
491        self.annotations
492            .iter()
493            .filter_map(|(range, kind)| {
494                Some((
495                    self.clip(*range, len)?,
496                    kind.wash(theme, self.highlight, self.find),
497                ))
498            })
499            .collect()
500    }
501
502    /// A range clipped to this text, and `None` when it does not reach it.
503    ///
504    /// The comparison is on `(block, part)` alone: a range covers this text
505    /// entirely when it starts before and ends after, and the offsets only
506    /// matter at the two ends.
507    fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
508        if selection.is_collapsed() {
509            return None;
510        }
511        let (start, end) = selection.ordered();
512        let here = self.here();
513        let (first, last) = (
514            Cursor::new(start.block, start.part, 0),
515            Cursor::new(end.block, end.part, 0),
516        );
517        if here < first || here > last {
518            return None;
519        }
520        let from = if here == first { start.offset } else { 0 };
521        let to = if here == last { end.offset } else { len };
522        (from < to).then_some(from..to.min(len))
523    }
524
525    /// Whether a block painting something a caret cannot enter — a rule, a
526    /// picture — falls inside the selection, and so should show that it is
527    /// going to be taken.
528    fn covers_block(&self) -> bool {
529        let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
530            return false;
531        };
532        let (start, end) = selection.ordered();
533        start.block < self.block && self.block < end.block
534    }
535}
536
537/// What gpui loads for an image URL as written in a document.
538///
539/// Anything with `://` is fetched as it stands. Anything else is a file: an
540/// absolute path as it stands, a relative one joined onto `base` when there is
541/// one.
542pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
543    if url.contains("://") {
544        return SharedString::from(url.to_string()).into();
545    }
546    // gpui reads a file only from a `PathBuf` — handed a string it looks for
547    // an asset built into the binary and paints nothing.
548    match base {
549        Some(base) => base.join(url).into(),
550        None => std::path::PathBuf::from(url).into(),
551    }
552}
553
554/// Parse and render in one step — the common case for read-only content.
555pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
556    let doc = crate::parse_with(source, &cx.marks());
557    render(&doc, Caption::default(), window, cx)
558}
559
560/// Render a document.
561pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
562    render_with(
563        doc,
564        Editing {
565            caption,
566            ..Editing::default()
567        },
568        window,
569        cx,
570    )
571}
572
573/// Render a document with a caret and a selection in it.
574///
575/// Both are paint-time concerns and nothing else: they read their positions off
576/// the shaped text's own layout handle, the same way the inline-code wash does,
577/// so nothing about layout depends on where the caret sits. An editor supplies
578/// the selection and owns the focus and the keys; painting a caret and a few
579/// quads is not worth a second renderer.
580pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
581    let Editing {
582        selection,
583        caret_on,
584        layouts,
585        annotations,
586        placeholder,
587        caption,
588        typography,
589        toggle,
590        image,
591        image_overlay,
592        image_overlay_corner,
593        fence,
594        sizing,
595        table_controls,
596        copy,
597        base,
598        keep,
599        scroll,
600    } = editing;
601    // Cloned once so the theme is readable while `cx` stays free for the
602    // element state the copy button needs.
603    let theme = Theme::of(cx).clone();
604    let typography = typography.unwrap_or_else(|| cx.typography());
605    let highlight = crate::marks::highlight_paint_of(cx);
606    let find = crate::find::find_paint_of(cx);
607    let gaps: Vec<Pixels> = doc
608        .blocks
609        .iter()
610        .enumerate()
611        .map(|(ix, block)| {
612            px(match doc.blocks.get(ix.wrapping_sub(1)) {
613                None => 0.0,
614                Some(previous) if tight(previous, block) => LIST_GAP,
615                Some(_) => BLOCK_GAP,
616            })
617        })
618        .collect();
619
620    let Some(layouts) = layouts else {
621        let mut column = div().flex().flex_col();
622        for (ix, block) in doc.blocks.iter().enumerate() {
623            let overlay = Overlay {
624                block: ix,
625                part: Part::Body,
626                selection,
627                caret_on,
628                caret_shape: cx.caret_shape(),
629                caret_height: cx.caret_height(),
630                caret_inactive: cx.inactive_caret(),
631                window_active: window.is_window_active(),
632                layouts: None,
633                annotations,
634                placeholder: placeholder.as_ref(),
635                caption,
636                toggle: toggle.as_ref(),
637                image: image.as_ref(),
638                image_overlay: image_overlay.as_ref(),
639                image_overlay_corner,
640                fence: fence.as_ref(),
641                sizing,
642                table_controls,
643                copy,
644                base,
645                highlight,
646                find,
647            };
648            column = column
649                .child(block_box(block, overlay, &typography, &theme, window, cx).mt(gaps[ix]));
650        }
651        return column.into_any_element();
652    };
653
654    let keys: Vec<u64> = doc
655        .blocks
656        .iter()
657        .map(|block| block_key(block, &typography, table_controls))
658        .collect();
659    layouts.prune(&keys);
660    let guesses: Vec<Guess> = doc
661        .blocks
662        .iter()
663        .map(|block| {
664            let mut guess = guess(block, &typography);
665            if table_controls && matches!(block.kind, BlockKind::Table { .. }) {
666                guess.extra += px(2.0 * TABLE_CONTROL_SIZE);
667            }
668            guess
669        })
670        .collect();
671    let mut kept = keep.to_vec();
672    kept.extend(selection.map(|selection| selection.head.block));
673    kept.extend(selection.map(|selection| selection.anchor.block));
674    let owned = Owned {
675        blocks: doc.blocks.clone(),
676        selection,
677        caret_on,
678        layouts: layouts.clone(),
679        annotations: annotations.to_vec(),
680        placeholder,
681        caption,
682        toggle,
683        image,
684        image_overlay,
685        image_overlay_corner,
686        fence,
687        sizing,
688        table_controls,
689        copy,
690        base: base.map(Path::to_path_buf),
691        highlight,
692        find,
693        typography,
694        theme,
695    };
696    Column {
697        layouts: layouts.clone(),
698        keys: keys.into(),
699        gaps: gaps.into(),
700        guesses: guesses.into(),
701        keep: kept,
702        scroll: scroll.cloned(),
703        item_of: Box::new(|at| at.block),
704        build: Box::new(move |ix, window, cx| {
705            let overlay = Overlay {
706                block: ix,
707                part: Part::Body,
708                selection: owned.selection,
709                caret_on: owned.caret_on,
710                caret_shape: cx.caret_shape(),
711                caret_height: cx.caret_height(),
712                caret_inactive: cx.inactive_caret(),
713                window_active: window.is_window_active(),
714                layouts: Some(&owned.layouts),
715                annotations: &owned.annotations,
716                placeholder: owned.placeholder.as_ref(),
717                caption: owned.caption,
718                toggle: owned.toggle.as_ref(),
719                image: owned.image.as_ref(),
720                image_overlay: owned.image_overlay.as_ref(),
721                image_overlay_corner: owned.image_overlay_corner,
722                fence: owned.fence.as_ref(),
723                sizing: owned.sizing,
724                table_controls: owned.table_controls,
725                copy: owned.copy,
726                base: owned.base.as_deref(),
727                highlight: owned.highlight,
728                find: owned.find,
729            };
730            block_box(
731                &owned.blocks[ix],
732                overlay,
733                &owned.typography,
734                &owned.theme,
735                window,
736                cx,
737            )
738            .into_any_element()
739        }),
740    }
741    .into_any_element()
742}
743
744/// What [`Column`] builds a block from, owned so it can build one at prepaint.
745struct Owned {
746    blocks: Vec<Block>,
747    selection: Option<Selection>,
748    caret_on: bool,
749    layouts: BlockLayouts,
750    annotations: Vec<(Selection, Annotation)>,
751    placeholder: Option<SharedString>,
752    caption: Caption,
753    toggle: Option<Toggle>,
754    image: Option<OnImage>,
755    image_overlay: Option<ImageOverlay>,
756    image_overlay_corner: ImageOverlayCorner,
757    fence: Option<FenceHost>,
758    sizing: Option<(usize, u32)>,
759    table_controls: bool,
760    copy: CopyButton,
761    base: Option<std::path::PathBuf>,
762    highlight: crate::HighlightPaint,
763    find: crate::FindPaint,
764    typography: Typography,
765    theme: Theme,
766}
767
768/// A block's box: its indent outside, and inside it the recorder and the
769/// block itself.
770fn block_box(
771    block: &Block,
772    overlay: Overlay,
773    typography: &Typography,
774    theme: &Theme,
775    window: &mut Window,
776    cx: &mut App,
777) -> gpui::Div {
778    let ix = overlay.block;
779    // The block's own box, recorded for a gutter handle and a drop target.
780    // A rule and an image hold no text, so a layout would not find them.
781    let frame = overlay.layouts.map(|layouts| {
782        let layouts = layouts.clone();
783        canvas(
784            move |bounds, _, _| layouts.record_block(ix, bounds),
785            |_, _, _, _| (),
786        )
787        .absolute()
788        .size_full()
789    });
790    // The indent sits on the outside and the recorder on the inside, so what
791    // is recorded is the box the block's text actually occupies. Recorded
792    // outside the padding, every level answered with the same left edge, and a
793    // gutter handle placed from it stayed at the margin while the block it
794    // belongs to moved right.
795    div()
796        .w_full()
797        .pl(px(block.indent as f32 * INDENT_WIDTH))
798        .child(
799            div()
800                .w_full()
801                .relative()
802                .children(frame)
803                // What a caret cannot enter still has to show it is inside the
804                // selection, or a rule between two paragraphs looks untouched
805                // right up until it disappears.
806                .when(overlay.covers_block() && block.opaque(), |el| {
807                    el.rounded(px(4.0)).bg(theme.selection)
808                })
809                .child(block_element(block, overlay, typography, theme, window, cx)),
810        )
811}
812
813/// What a block's height is cached under: its content and the type it is set
814/// in, so an edit elsewhere that shifts its index keeps the height.
815fn block_key(block: &Block, typography: &Typography, table_controls: bool) -> u64 {
816    let mut hasher = DefaultHasher::new();
817    table_controls.hash(&mut hasher);
818    block.hash(&mut hasher);
819    typography.body.size().to_bits().hash(&mut hasher);
820    typography.body.line_height().to_bits().hash(&mut hasher);
821    hasher.finish()
822}
823
824/// What a block is placed at before it has ever been built.
825fn guess(block: &Block, typography: &Typography) -> Guess {
826    let body = px(typography.body.line_height());
827    let prose = |chars: usize, line: Pixels| Guess {
828        chars,
829        line,
830        rows: 0,
831        extra: px(0.0),
832        indent: px(0.0),
833    };
834    let guess = match &block.kind {
835        BlockKind::Paragraph(text)
836        | BlockKind::Bullet(text)
837        | BlockKind::Ordered { text, .. }
838        | BlockKind::Task { text, .. }
839        | BlockKind::Quote { text, .. } => prose(text.text.len(), body),
840        BlockKind::Heading { level, text } => prose(
841            text.text.len(),
842            px(typography.heading(*level).line_height()),
843        ),
844        BlockKind::Code { code, .. } => Guess {
845            rows: code.text.lines().count().max(1),
846            line: px(typography.code.line_height()),
847            extra: px(2.0 * CODE_PADDING_Y) + body,
848            ..prose(0, body)
849        },
850        BlockKind::Table { rows, .. } => Guess {
851            rows: rows.len() + 1,
852            extra: px(2.0 * TABLE_CELL_PADDING + TABLE_DIVIDER) * (rows.len() + 1) as f32,
853            ..prose(0, body)
854        },
855        BlockKind::Image { alt, .. } => Guess {
856            extra: px(240.0),
857            ..prose(alt.text.len(), px(typography.caption.line_height()))
858        },
859        BlockKind::Bookmark { .. } => Guess {
860            extra: body * 3.0,
861            ..prose(0, body)
862        },
863        BlockKind::Rule => Guess {
864            extra: body,
865            ..prose(0, px(0.0))
866        },
867    };
868    Guess {
869        indent: px(block.indent as f32 * INDENT_WIDTH),
870        ..guess
871    }
872}
873
874/// Whether two adjacent blocks belong to the same list and should sit close.
875fn tight(previous: &Block, next: &Block) -> bool {
876    let marker = |block: &Block| {
877        matches!(
878            block.kind,
879            BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
880        )
881    };
882    marker(previous) && (marker(next) || next.indent > previous.indent)
883}
884
885fn block_element(
886    block: &Block,
887    overlay: Overlay,
888    typography: &Typography,
889    theme: &Theme,
890    window: &mut Window,
891    cx: &mut App,
892) -> AnyElement {
893    let body = overlay.at(Part::Body);
894    match &block.kind {
895        BlockKind::Paragraph(text) => text_element(
896            text,
897            typography.body.size(),
898            typography.body.line_height(),
899            FontWeight::NORMAL,
900            body,
901            theme,
902            cx,
903        ),
904        BlockKind::Heading { level, text } => {
905            let heading = typography.heading(*level);
906            text_element(
907                text,
908                heading.size(),
909                heading.line_height(),
910                heading.weight,
911                body,
912                theme,
913                cx,
914            )
915        }
916        BlockKind::Bullet(text) => {
917            marker_row(disc(typography, theme), text, body, typography, theme, cx)
918        }
919        BlockKind::Ordered { number, text } => marker_row(
920            div()
921                .flex_none()
922                .w(px(MARKER_WIDTH))
923                .text_size(px(typography.body.size()))
924                .line_height(px(typography.body.line_height()))
925                .text_color(theme.text_muted)
926                .child(SharedString::from(format!("{number}.")))
927                .into_any_element(),
928            text,
929            body,
930            typography,
931            theme,
932            cx,
933        ),
934        BlockKind::Task { checked, text } => marker_row(
935            checkbox(*checked, overlay, typography, theme),
936            text,
937            body,
938            typography,
939            theme,
940            cx,
941        ),
942        BlockKind::Quote { kind, text } => div()
943            .border_l_2()
944            .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
945            .pl(px(12.0))
946            .pr(px(10.0))
947            .py(px(2.0))
948            .text_color(theme.text_muted)
949            .children(kind.map(|kind| {
950                div()
951                    .pb(px(2.0))
952                    .text_size(px(typography.body.size()))
953                    .line_height(px(typography.body.line_height()))
954                    .font_weight(FontWeight::SEMIBOLD)
955                    .text_color(alert_color(kind, theme))
956                    .child(kind.label())
957            }))
958            .child(text_element(
959                text,
960                typography.body.size(),
961                typography.body.line_height(),
962                FontWeight::NORMAL,
963                body,
964                theme,
965                cx,
966            ))
967            .into_any_element(),
968        BlockKind::Code {
969            language,
970            code,
971            height,
972        } => {
973            let overlay = overlay.at(Part::Code);
974            let height = overlay.held_height().or(*height);
975            // The caret in the fence gives the source back. A painted block is
976            // still an editable one, and typing into it otherwise edits what
977            // the reader cannot see.
978            let painted = overlay
979                .caret()
980                .is_none()
981                .then(|| {
982                    let fence = block::Fence {
983                        language: language.as_deref()?,
984                        code: &code.text,
985                        height,
986                        rewrite: overlay.fence.map(|host| {
987                            let (rewrite, ix) = (host.rewrite.clone(), overlay.block);
988                            Rc::new(move |code: String, window: &mut Window, cx: &mut App| {
989                                rewrite(ix, code, window, cx)
990                            }) as block::Rewrite
991                        }),
992                    };
993                    block::render(&fence, window, cx)
994                })
995                .flatten();
996            match painted {
997                // Painted, there is no text under the selection to carry it —
998                // the wash an opaque block gets at the container comes here.
999                Some(element) => div()
1000                    .key_context(PAINTED_CONTEXT)
1001                    .relative()
1002                    .when_some(overlay.fence, |el, host| {
1003                        let (leave, ix) = (host.leave.clone(), overlay.block);
1004                        el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1005                    })
1006                    .when(overlay.covers_block(), |el| {
1007                        el.rounded(px(4.0)).bg(theme.selection)
1008                    })
1009                    .group(painted_group(overlay.block))
1010                    .child(element)
1011                    .children(resizable(&overlay, theme))
1012                    .into_any_element(),
1013                None => code_block(
1014                    language.as_deref(),
1015                    &code.text,
1016                    overlay,
1017                    typography,
1018                    theme,
1019                    window,
1020                    cx,
1021                ),
1022            }
1023        }
1024        BlockKind::Image { url, alt, width } => {
1025            image(url, alt, *width, overlay, typography, theme, window, cx)
1026        }
1027        BlockKind::Bookmark { url, form } => match crate::preview::card(
1028            url,
1029            match (*form, overlay.held_height()) {
1030                (Form::Embed(_), Some(held)) => Form::Embed(Some(held)),
1031                (form, _) => form,
1032            },
1033            window,
1034            cx,
1035        ) {
1036            // The app's own, painted where a fence's would be and kept apart
1037            // from the editor's keys the same way.
1038            Some(element) => div()
1039                .key_context(PAINTED_CONTEXT)
1040                .relative()
1041                .when_some(overlay.fence, |el, host| {
1042                    let (leave, ix) = (host.leave.clone(), overlay.block);
1043                    el.on_action(move |_: &LeaveBlock, window, cx| leave(ix, window, cx))
1044                })
1045                .when(overlay.covers_block(), |el| {
1046                    el.rounded(px(4.0)).bg(theme.selection)
1047                })
1048                .group(painted_group(overlay.block))
1049                .child(element)
1050                .children(match form {
1051                    Form::Embed(_) => resizable(&overlay, theme),
1052                    _ => Vec::new(),
1053                })
1054                .into_any_element(),
1055            None => bookmark(overlay.block, url, *form, typography, theme, cx),
1056        },
1057        BlockKind::Table {
1058            align,
1059            header,
1060            rows,
1061        } => table(align, header, rows, overlay, typography, theme, window, cx),
1062        BlockKind::Rule => div()
1063            .h(px(1.0))
1064            .w_full()
1065            .bg(theme.border)
1066            .into_any_element(),
1067    }
1068}
1069
1070/// A real 5px disc rather than the "•" glyph, which reads too small at body size.
1071fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
1072    div()
1073        .flex_none()
1074        .w(px(MARKER_WIDTH))
1075        .h(px(typography.body.line_height()))
1076        .flex()
1077        .items_center()
1078        .child(
1079            div()
1080                .ml(px(1.0))
1081                .w(px(5.0))
1082                .h(px(5.0))
1083                .rounded_full()
1084                .bg(theme.text_faint),
1085        )
1086        .into_any_element()
1087}
1088
1089fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
1090    let ix = overlay.block;
1091    let mut box_ = div()
1092        .relative()
1093        .w(px(13.0))
1094        .h(px(13.0))
1095        .rounded(px(3.5))
1096        .border_1()
1097        .flex()
1098        .items_center()
1099        .justify_center();
1100    box_ = if checked {
1101        box_.bg(theme.solid)
1102            .border_color(theme.solid)
1103            .text_style(TextStyle::Caption)
1104            .text_color(theme.on_solid)
1105            .child("✓")
1106    } else {
1107        box_.border_color(theme.border_strong)
1108    };
1109    // The box's own bounds rather than the marker column's: a caller hit-tests
1110    // these to tell a toggle from a caret placed in the gutter beside it.
1111    box_ = box_.children(overlay.layouts.map(|layouts| {
1112        let layouts = layouts.clone();
1113        canvas(
1114            move |bounds, _, _| layouts.record_checkbox(ix, bounds),
1115            |_, _, _, _| (),
1116        )
1117        .absolute()
1118        .size_full()
1119    }));
1120    // The cursor answers to either variant: a box an editor hit-tests is as
1121    // pressable as one the renderer listens to, and only the pointer says so.
1122    if overlay.toggle.is_some() {
1123        box_ = box_.cursor_pointer();
1124    }
1125    if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
1126        box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
1127            // Stopped, or the press goes on to whatever placed a caret
1128            // under it and the toggle reads as a click that moved the
1129            // caret as well.
1130            cx.stop_propagation();
1131            toggle(ix, window, cx);
1132        });
1133    }
1134
1135    div()
1136        .flex_none()
1137        .w(px(MARKER_WIDTH))
1138        .h(px(typography.body.line_height()))
1139        .flex()
1140        .items_center()
1141        .child(box_)
1142        .into_any_element()
1143}
1144
1145/// What an alert paints its rule and its label in.
1146fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
1147    match kind {
1148        QuoteKind::Note => theme.accent,
1149        QuoteKind::Tip => theme.success,
1150        QuoteKind::Important => theme.busy,
1151        QuoteKind::Warning => theme.warning,
1152        QuoteKind::Caution => theme.danger,
1153    }
1154}
1155
1156fn marker_row(
1157    marker: AnyElement,
1158    text: &Text,
1159    overlay: Overlay,
1160    typography: &Typography,
1161    theme: &Theme,
1162    cx: &App,
1163) -> AnyElement {
1164    div()
1165        .flex()
1166        .flex_row()
1167        .gap(px(MARKER_GAP))
1168        .child(marker)
1169        .child(div().flex_1().min_w_0().child(text_element(
1170            text,
1171            typography.body.size(),
1172            typography.body.line_height(),
1173            FontWeight::NORMAL,
1174            overlay,
1175            theme,
1176            cx,
1177        )))
1178        .into_any_element()
1179}