Skip to main content

markdown/
render.rs

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