Skip to main content

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