Skip to main content

markdown/
render.rs

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