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 std::{cell::RefCell, ops::Range, path::Path, rc::Rc};
11
12use gpui::{
13    AnyElement, App, BorderStyle, Bounds, CursorStyle, ElementId, FontStyle, FontWeight, Hsla,
14    ImageSource, InteractiveText, MouseButton, ObjectFit, Pixels, Point, SharedString,
15    StrikethroughStyle, StyledImage as _, StyledText, TextLayout, TextRun, UnderlineStyle, Window,
16    canvas, div, font, img, point, prelude::*, px, quad, size,
17};
18use theme::{TextStyle, Theme, Typeset};
19
20use crate::{
21    block,
22    doc::{Align, Block, BlockKind, Doc, Form, Mark, Part, QuoteKind, Text},
23    layout::Layout,
24    preview,
25    select::{Cursor, Selection},
26    typography::Typography,
27};
28
29/// Space between two ordinary blocks, and the tighter space inside a list.
30mod code;
31mod layouts;
32mod media;
33mod table;
34mod text;
35
36pub use code::*;
37pub use layouts::*;
38use media::*;
39use table::*;
40pub use text::*;
41
42const BLOCK_GAP: f32 = 12.0;
43const LIST_GAP: f32 = 4.0;
44/// One indent level. Wide enough to clear a marker and read as a level.
45const INDENT_WIDTH: f32 = 22.0;
46/// The marker column of a list row.
47const MARKER_WIDTH: f32 = 18.0;
48const MARKER_GAP: f32 = 8.0;
49/// What a fence holds its code in, inside its border.
50const CODE_PADDING_X: f32 = 12.0;
51const CODE_PADDING_Y: f32 = 10.0;
52/// What a fence with no info string calls itself, in its header and in a
53/// picker — one spelling, so the label and the menu row cannot disagree.
54pub const PLAIN_LANGUAGE: &str = "Plain";
55/// Width of the caret. Wider than a hairline, because it has to read at a
56/// glance against the text it sits in.
57const CARET_WIDTH: f32 = 1.5;
58/// Inline code's wash is a rounded quad painted under the glyphs: a run's
59/// `background_color` can only ever be a square box.
60const INLINE_CODE_RADIUS: f32 = 4.5;
61const INLINE_CODE_PAD_X: f32 = 2.0;
62const INLINE_CODE_INSET_Y: f32 = 2.0;
63/// A mention's chip — the same quad-under-glyphs trick as inline code, with
64/// more room and an outline so the two do not read as the same thing.
65const CHIP_PAD_X: f32 = 4.0;
66const CHIP_INSET_Y: f32 = 1.0;
67/// A chip with a block to itself is a real element rather than a wash, so it
68/// has room for the favicon the inline one cannot hold.
69const CHIP_BLOCK_PAD_X: f32 = 8.0;
70const CHIP_BLOCK_PAD_Y: f32 = 3.0;
71const CHIP_ICON: f32 = 15.0;
72/// Bookmark metrics. Notion's card: 180px of image beside the text, and a
73/// height that fits a title, two lines of blurb and a footer. A cover moves
74/// that image above the text and gives it the card's full width.
75const CARD_HEIGHT: f32 = 116.0;
76const CARD_IMAGE_WIDTH: f32 = 180.0;
77const CARD_COVER_HEIGHT: f32 = 200.0;
78const CARD_PADDING: f32 = 14.0;
79const CARD_BORDER: f32 = 1.0;
80const CARD_ICON: f32 = 16.0;
81const CARD_COVER: f32 = 44.0;
82/// Image metrics.
83const IMAGE_EMPTY_HEIGHT: f32 = 52.0;
84const CAPTION_GAP: f32 = 4.0;
85/// What an image with no URL yet says, and what its caption says while empty.
86const IMAGE_EMPTY: &str = "Add an image";
87const CAPTION_HINT: &str = "Write a caption";
88/// Table metrics. The design is frameless: hairlines between rows are the only
89/// chrome — no outer box, no header fill, no rounding.
90const TABLE_CELL_PADDING: f32 = 12.0;
91const TABLE_DIVIDER: f32 = 1.0;
92/// Floor for a column's max-content share, so a short column ("1k") beside a
93/// prose column keeps a readable width.
94const TABLE_MIN_COLUMN_CONTENT: f32 = 48.0;
95/// Narrowest a column wraps down to before the table scrolls instead.
96const TABLE_MIN_COLUMN_WIDTH: f32 = 96.0;
97
98/// What an image's one authored string is doing on the page.
99///
100/// SwiftUI keeps three things apart — `accessibilityLabel` for a reader that
101/// cannot see, `.help` for the pointer, and a caption you compose out of a
102/// `Text` under the picture. Markdown has one slot for all three, so this says
103/// which of them it is playing here rather than in the document, where it is
104/// the same string either way.
105///
106/// A named choice rather than a `bool`, so a surface that wants a third answer
107/// gets a variant instead of a second flag.
108#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
109pub enum Caption {
110    /// Under the picture, where a caret can sit in it. The editor's shape.
111    #[default]
112    Shown,
113    /// Kept by the document and painted nowhere — a picture on its own.
114    Hidden,
115}
116
117/// Whether a fence paints the button that copies its text.
118///
119/// A named choice rather than a `bool`, the way [`Caption`] is.
120#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
121pub enum CopyButton {
122    /// Floating at the top right of the band, on the pointer and off it.
123    #[default]
124    Shown,
125    /// Painted nowhere. A document with this and no [`Editing::toggle`] holds
126    /// no listener at all.
127    Hidden,
128}
129
130/// A range the caller wants washed, and which wash it gets.
131///
132/// None of what a comment or a highlight *says* is here: the caller keeps it
133/// and hands over the range, the way it hands over a [`crate::Preview`]. A
134/// closed set rather than a color, so the environment keeps deciding the paint.
135#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
136#[non_exhaustive]
137pub enum Annotation {
138    /// A thread still waiting on someone.
139    #[default]
140    Open,
141    /// Answered, and kept for the record.
142    Resolved,
143    /// The one whose thread the reader has in front of them.
144    Active,
145    /// A reader's highlight, in the wash [`crate::set_highlight_paint`]
146    /// gives its colour.
147    Highlight(crate::HighlightColor),
148}
149
150impl Annotation {
151    fn wash(self, theme: &Theme, highlight: crate::HighlightPaint) -> Hsla {
152        match self {
153            Self::Open => theme.warning.opacity(0.20),
154            Self::Resolved => theme.warning.opacity(0.08),
155            Self::Active => theme.warning.opacity(0.38),
156            Self::Highlight(color) => highlight(color, theme),
157        }
158    }
159}
160
161/// Handed the block whose checkbox was clicked — see [`Toggle::Handled`].
162///
163/// Shared rather than borrowed: the press listener it is cloned into outlives
164/// the frame that built it.
165pub type OnToggle = Rc<dyn Fn(usize, &mut Window, &mut App)>;
166
167/// Who answers a press on a task block's checkbox.
168///
169/// Either variant paints the box as a control — the pointer over it is a hand.
170/// [`Editing::toggle`] left unset paints it as a mark, and the press goes
171/// wherever it would on any other glyph.
172#[derive(Clone)]
173pub enum Toggle {
174    /// The box takes the press, stops it, and calls this with the block it
175    /// belongs to. For a caller holding the [`Doc`] it renders itself.
176    Handled(OnToggle),
177    /// The box takes no press. The caller hit-tests
178    /// [`BlockLayouts::checkbox_bounds`] in its own handler, which is what an
179    /// editor does: the press it swallows is the one that also takes focus and
180    /// closes an open menu, and the toggle belongs in the undo history beside
181    /// the rest of its edits.
182    HitTested,
183}
184
185/// What an editor paints over a document.
186///
187/// One value rather than six parameters, and the reason it is public: the
188/// caret, the selection, the comment washes and the layout sink all arrive
189/// together or not at all, and a read-only [`render`] sets none of them.
190#[derive(Clone)]
191pub struct Editing<'a> {
192    /// The caret and what it has selected. `None` paints neither — a document
193    /// nobody is editing.
194    pub selection: Option<Selection>,
195    /// The blink's lit half. A caret painted on every frame reads as frozen,
196    /// and the phase belongs to whoever owns the focus.
197    pub caret_on: bool,
198    /// Filled as the document paints, for a caller resolving clicks against it.
199    pub layouts: Option<&'a BlockLayouts>,
200    /// Ranges washed under the text, in the order given.
201    pub annotations: &'a [(Selection, Annotation)],
202    /// Shown on the caret's block while it holds nothing.
203    pub placeholder: Option<SharedString>,
204    pub caption: Caption,
205    /// What to set the document in. `None` takes the installed
206    /// [`Typography`] — a caller sizing one document apart from the rest
207    /// passes [`Typography::scaled`].
208    pub typography: Option<Typography>,
209    /// Makes a task block's checkbox a control, and says who answers the
210    /// press. `None` paints a mark.
211    pub toggle: Option<Toggle>,
212    /// Whether a fence offers to copy itself.
213    pub copy: CopyButton,
214    /// The directory a relative image path is joined onto. `None` leaves it
215    /// relative, which gpui reads against the process's working directory.
216    pub base: Option<&'a Path>,
217}
218
219impl Default for Editing<'_> {
220    fn default() -> Self {
221        Self {
222            selection: None,
223            // Lit, so that a caller setting a selection and nothing else gets a
224            // caret rather than a mystery.
225            caret_on: true,
226            layouts: None,
227            annotations: &[],
228            placeholder: None,
229            caption: Caption::default(),
230            typography: None,
231            toggle: None,
232            copy: CopyButton::default(),
233            base: None,
234        }
235    }
236}
237
238/// What the editor needs painted into one text: which text it is, where the
239/// caret sits, and where to record the layout a click resolves against.
240///
241/// One bundle rather than four parameters threaded through every block arm —
242/// a read-only render builds it with no caret and no sink, and pays nothing.
243#[derive(Clone, Copy)]
244struct Overlay<'a> {
245    block: usize,
246    part: Part,
247    selection: Option<Selection>,
248    caret_on: bool,
249    layouts: Option<&'a BlockLayouts>,
250    /// Ranges washed under the text, in the order the caller gave them.
251    annotations: &'a [(Selection, Annotation)],
252    /// Shown on the caret's block while it holds nothing. The renderer is the
253    /// only thing that knows where that text sits, so the string comes to it.
254    placeholder: Option<&'a SharedString>,
255    caption: Caption,
256    /// Borrowed so [`Overlay`] stays `Copy` — the clone is made at the one
257    /// press listener that needs an owned handle.
258    toggle: Option<&'a Toggle>,
259    copy: CopyButton,
260    base: Option<&'a Path>,
261    highlight: crate::HighlightPaint,
262}
263
264impl<'a> Overlay<'a> {
265    fn at(self, part: Part) -> Self {
266        Self { part, ..self }
267    }
268
269    fn here(&self) -> Cursor {
270        Cursor::new(self.block, self.part, 0)
271    }
272
273    /// The caret to paint: where it is, and only on the blink's lit half.
274    ///
275    /// Separate from [`Self::caret`] because the blink must not reach anything
276    /// but the quad — a block whose paint depends on holding the caret would
277    /// otherwise swap itself out twice a second.
278    fn caret_painted(&self) -> Option<usize> {
279        self.caret_on.then(|| self.caret()).flatten()
280    }
281
282    /// The caret's byte offset, if the head is in *this* text.
283    fn caret(&self) -> Option<usize> {
284        self.selection
285            .map(|selection| selection.head)
286            .filter(|head| head.block == self.block && head.part == self.part)
287            .map(|head| head.offset)
288    }
289
290    /// The selected slice of this text, clipped to it.
291    fn selected(&self, len: usize) -> Option<Range<usize>> {
292        self.clip(self.selection?, len)
293    }
294
295    /// The annotated slices of this text, already resolved to their paint —
296    /// the wash goes into a `move` closure that the theme does not travel into.
297    fn annotated(&self, len: usize, theme: &Theme) -> Vec<(Range<usize>, Hsla)> {
298        self.annotations
299            .iter()
300            .filter_map(|(range, kind)| {
301                Some((self.clip(*range, len)?, kind.wash(theme, self.highlight)))
302            })
303            .collect()
304    }
305
306    /// A range clipped to this text, and `None` when it does not reach it.
307    ///
308    /// The comparison is on `(block, part)` alone: a range covers this text
309    /// entirely when it starts before and ends after, and the offsets only
310    /// matter at the two ends.
311    fn clip(&self, selection: Selection, len: usize) -> Option<Range<usize>> {
312        if selection.is_collapsed() {
313            return None;
314        }
315        let (start, end) = selection.ordered();
316        let here = self.here();
317        let (first, last) = (
318            Cursor::new(start.block, start.part, 0),
319            Cursor::new(end.block, end.part, 0),
320        );
321        if here < first || here > last {
322            return None;
323        }
324        let from = if here == first { start.offset } else { 0 };
325        let to = if here == last { end.offset } else { len };
326        (from < to).then_some(from..to.min(len))
327    }
328
329    /// Whether a block painting something a caret cannot enter — a rule, a
330    /// picture — falls inside the selection, and so should show that it is
331    /// going to be taken.
332    fn covers_block(&self) -> bool {
333        let Some(selection) = self.selection.filter(|s| !s.is_collapsed()) else {
334            return false;
335        };
336        let (start, end) = selection.ordered();
337        start.block < self.block && self.block < end.block
338    }
339}
340
341/// What gpui loads for an image URL as written in a document.
342///
343/// Anything with `://` is fetched as it stands. Anything else is a file: an
344/// absolute path as it stands, a relative one joined onto `base` when there is
345/// one.
346pub fn image_source(url: &str, base: Option<&Path>) -> ImageSource {
347    if url.contains("://") {
348        return SharedString::from(url.to_string()).into();
349    }
350    // gpui reads a file only from a `PathBuf` — handed a string it looks for
351    // an asset built into the binary and paints nothing.
352    match base {
353        Some(base) => base.join(url).into(),
354        None => std::path::PathBuf::from(url).into(),
355    }
356}
357
358/// Parse and render in one step — the common case for read-only content.
359pub fn markdown(source: &str, window: &mut Window, cx: &mut App) -> AnyElement {
360    let doc = crate::parse_with(source, &crate::Marks::of(cx));
361    render(&doc, Caption::default(), window, cx)
362}
363
364/// Render a document.
365pub fn render(doc: &Doc, caption: Caption, window: &mut Window, cx: &mut App) -> AnyElement {
366    render_with(
367        doc,
368        Editing {
369            caption,
370            ..Editing::default()
371        },
372        window,
373        cx,
374    )
375}
376
377/// Render a document with a caret and a selection in it.
378///
379/// Both are paint-time concerns and nothing else: they read their positions off
380/// the shaped text's own layout handle, the same way the inline-code wash does,
381/// so nothing about layout depends on where the caret sits. An editor supplies
382/// the selection and owns the focus and the keys; painting a caret and a few
383/// quads is not worth a second renderer.
384pub fn render_with(doc: &Doc, editing: Editing, window: &mut Window, cx: &mut App) -> AnyElement {
385    let Editing {
386        selection,
387        caret_on,
388        layouts,
389        annotations,
390        placeholder,
391        caption,
392        typography,
393        toggle,
394        copy,
395        base,
396    } = editing;
397    // Refilled every frame, in paint order — and emptied in *prepaint*, not
398    // here. An editor reads last frame's positions while building this frame's
399    // tree (a menu anchored at the caret, a handle beside a block), and
400    // clearing at build time takes them away before it can. Placed first in the
401    // column so it runs ahead of every recorder below it.
402    let reset = layouts.map(|layouts| {
403        let layouts = layouts.clone();
404        canvas(move |_, _, _| layouts.clear(), |_, _, _, _| ())
405            .absolute()
406            .size(px(0.0))
407    });
408    // Cloned once so the theme is readable while `cx` stays free for the
409    // element state the copy button needs.
410    let theme = Theme::of(cx).clone();
411    let typography = typography.unwrap_or_else(|| Typography::of(cx));
412    let highlight = crate::marks::highlight_paint_of(cx);
413    let mut column = div().flex().flex_col().children(reset);
414
415    for (ix, block) in doc.blocks.iter().enumerate() {
416        let gap = match doc.blocks.get(ix.wrapping_sub(1)) {
417            None => 0.0,
418            Some(previous) if tight(previous, block) => LIST_GAP,
419            Some(_) => BLOCK_GAP,
420        };
421        let overlay = Overlay {
422            block: ix,
423            part: Part::Body,
424            selection,
425            caret_on,
426            layouts,
427            annotations,
428            placeholder: placeholder.as_ref(),
429            caption,
430            toggle: toggle.as_ref(),
431            copy,
432            base,
433            highlight,
434        };
435        // The block's own box, recorded for a gutter handle and a drop target.
436        // A rule and an image hold no text, so a layout would not find them.
437        let frame = layouts.map(|layouts| {
438            let layouts = layouts.clone();
439            canvas(
440                move |bounds, _, _| layouts.record_block(ix, bounds),
441                |_, _, _, _| (),
442            )
443            .absolute()
444            .size_full()
445        });
446        column = column.child(
447            // The indent sits on the outside and the recorder on the inside,
448            // so what is recorded is the box the block's text actually
449            // occupies. Recorded outside the padding, every level answered
450            // with the same left edge, and a gutter handle placed from it
451            // stayed at the margin while the block it belongs to moved right.
452            div()
453                .mt(px(gap))
454                .pl(px(block.indent as f32 * INDENT_WIDTH))
455                .child(
456                    div()
457                        .w_full()
458                        .relative()
459                        .children(frame)
460                        // What a caret cannot enter still has to show it is
461                        // inside the selection, or a rule between two
462                        // paragraphs looks untouched right up until it
463                        // disappears.
464                        .when(overlay.covers_block() && block.opaque(), |el| {
465                            el.rounded(px(4.0)).bg(theme.selection)
466                        })
467                        .child(block_element(
468                            block,
469                            overlay,
470                            &typography,
471                            &theme,
472                            window,
473                            cx,
474                        )),
475                ),
476        );
477    }
478
479    column.into_any_element()
480}
481
482/// Whether two adjacent blocks belong to the same list and should sit close.
483fn tight(previous: &Block, next: &Block) -> bool {
484    let marker = |block: &Block| {
485        matches!(
486            block.kind,
487            BlockKind::Bullet(_) | BlockKind::Ordered { .. } | BlockKind::Task { .. }
488        )
489    };
490    marker(previous) && (marker(next) || next.indent > previous.indent)
491}
492
493fn block_element(
494    block: &Block,
495    overlay: Overlay,
496    typography: &Typography,
497    theme: &Theme,
498    window: &mut Window,
499    cx: &mut App,
500) -> AnyElement {
501    let body = overlay.at(Part::Body);
502    match &block.kind {
503        BlockKind::Paragraph(text) => text_element(
504            text,
505            typography.body.size(),
506            typography.body.line_height(),
507            FontWeight::NORMAL,
508            body,
509            theme,
510            cx,
511        ),
512        BlockKind::Heading { level, text } => {
513            let heading = typography.heading(*level);
514            text_element(
515                text,
516                heading.size(),
517                heading.line_height(),
518                heading.weight,
519                body,
520                theme,
521                cx,
522            )
523        }
524        BlockKind::Bullet(text) => {
525            marker_row(disc(typography, theme), text, body, typography, theme, cx)
526        }
527        BlockKind::Ordered { number, text } => marker_row(
528            div()
529                .flex_none()
530                .w(px(MARKER_WIDTH))
531                .text_size(px(typography.body.size()))
532                .line_height(px(typography.body.line_height()))
533                .text_color(theme.text_muted)
534                .child(SharedString::from(format!("{number}.")))
535                .into_any_element(),
536            text,
537            body,
538            typography,
539            theme,
540            cx,
541        ),
542        BlockKind::Task { checked, text } => marker_row(
543            checkbox(*checked, overlay, typography, theme),
544            text,
545            body,
546            typography,
547            theme,
548            cx,
549        ),
550        BlockKind::Quote { kind, text } => div()
551            .border_l_2()
552            .border_color(kind.map_or(theme.border_strong, |kind| alert_color(kind, theme)))
553            .pl(px(12.0))
554            .pr(px(10.0))
555            .py(px(2.0))
556            .text_color(theme.text_muted)
557            .children(kind.map(|kind| {
558                div()
559                    .pb(px(2.0))
560                    .text_size(px(typography.body.size()))
561                    .line_height(px(typography.body.line_height()))
562                    .font_weight(FontWeight::SEMIBOLD)
563                    .text_color(alert_color(kind, theme))
564                    .child(kind.label())
565            }))
566            .child(text_element(
567                text,
568                typography.body.size(),
569                typography.body.line_height(),
570                FontWeight::NORMAL,
571                body,
572                theme,
573                cx,
574            ))
575            .into_any_element(),
576        BlockKind::Code { language, code } => {
577            let overlay = overlay.at(Part::Code);
578            // The caret in the fence gives the source back. A painted block is
579            // still an editable one, and typing into it otherwise edits what
580            // the reader cannot see.
581            let painted = overlay
582                .caret()
583                .is_none()
584                .then(|| block::render(language.as_deref(), &code.text, window, cx))
585                .flatten();
586            match painted {
587                // Painted, there is no text under the selection to carry it —
588                // the wash an opaque block gets at the container comes here.
589                Some(element) => div()
590                    .when(overlay.covers_block(), |el| {
591                        el.rounded(px(4.0)).bg(theme.selection)
592                    })
593                    .child(element)
594                    .into_any_element(),
595                None => code_block(
596                    language.as_deref(),
597                    &code.text,
598                    overlay,
599                    typography,
600                    theme,
601                    window,
602                    cx,
603                ),
604            }
605        }
606        BlockKind::Image { url, alt, width } => {
607            image(url, alt, *width, overlay, typography, theme, cx)
608        }
609        BlockKind::Bookmark { url, form } => {
610            bookmark(overlay.block, url, *form, typography, theme, cx)
611        }
612        BlockKind::Table {
613            align,
614            header,
615            rows,
616        } => table(align, header, rows, overlay, typography, theme, window, cx),
617        BlockKind::Rule => div()
618            .h(px(1.0))
619            .w_full()
620            .bg(theme.border)
621            .into_any_element(),
622    }
623}
624
625/// A real 5px disc rather than the "•" glyph, which reads too small at body size.
626fn disc(typography: &Typography, theme: &Theme) -> AnyElement {
627    div()
628        .flex_none()
629        .w(px(MARKER_WIDTH))
630        .h(px(typography.body.line_height()))
631        .flex()
632        .items_center()
633        .child(
634            div()
635                .ml(px(1.0))
636                .w(px(5.0))
637                .h(px(5.0))
638                .rounded_full()
639                .bg(theme.text_faint),
640        )
641        .into_any_element()
642}
643
644fn checkbox(checked: bool, overlay: Overlay, typography: &Typography, theme: &Theme) -> AnyElement {
645    let ix = overlay.block;
646    let mut box_ = div()
647        .relative()
648        .w(px(13.0))
649        .h(px(13.0))
650        .rounded(px(3.5))
651        .border_1()
652        .flex()
653        .items_center()
654        .justify_center();
655    box_ = if checked {
656        box_.bg(theme.solid)
657            .border_color(theme.solid)
658            .text_style(TextStyle::Caption)
659            .text_color(theme.on_solid)
660            .child("✓")
661    } else {
662        box_.border_color(theme.border_strong)
663    };
664    // The box's own bounds rather than the marker column's: a caller hit-tests
665    // these to tell a toggle from a caret placed in the gutter beside it.
666    box_ = box_.children(overlay.layouts.map(|layouts| {
667        let layouts = layouts.clone();
668        canvas(
669            move |bounds, _, _| layouts.record_checkbox(ix, bounds),
670            |_, _, _, _| (),
671        )
672        .absolute()
673        .size_full()
674    }));
675    // The cursor answers to either variant: a box an editor hit-tests is as
676    // pressable as one the renderer listens to, and only the pointer says so.
677    if overlay.toggle.is_some() {
678        box_ = box_.cursor_pointer();
679    }
680    if let Some(Toggle::Handled(toggle)) = overlay.toggle.cloned() {
681        box_ = box_.on_mouse_down(MouseButton::Left, move |_, window, cx| {
682            // Stopped, or the press goes on to whatever placed a caret
683            // under it and the toggle reads as a click that moved the
684            // caret as well.
685            cx.stop_propagation();
686            toggle(ix, window, cx);
687        });
688    }
689
690    div()
691        .flex_none()
692        .w(px(MARKER_WIDTH))
693        .h(px(typography.body.line_height()))
694        .flex()
695        .items_center()
696        .child(box_)
697        .into_any_element()
698}
699
700/// What an alert paints its rule and its label in.
701fn alert_color(kind: QuoteKind, theme: &Theme) -> Hsla {
702    match kind {
703        QuoteKind::Note => theme.accent,
704        QuoteKind::Tip => theme.success,
705        QuoteKind::Important => theme.busy,
706        QuoteKind::Warning => theme.warning,
707        QuoteKind::Caution => theme.danger,
708    }
709}
710
711fn marker_row(
712    marker: AnyElement,
713    text: &Text,
714    overlay: Overlay,
715    typography: &Typography,
716    theme: &Theme,
717    cx: &App,
718) -> AnyElement {
719    div()
720        .flex()
721        .flex_row()
722        .gap(px(MARKER_GAP))
723        .child(marker)
724        .child(div().flex_1().min_w_0().child(text_element(
725            text,
726            typography.body.size(),
727            typography.body.line_height(),
728            FontWeight::NORMAL,
729            overlay,
730            theme,
731            cx,
732        )))
733        .into_any_element()
734}