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