Skip to main content

markdown/fence/
chrome.rs

1//! The panel a fence is drawn in, painted or as code, and the copy button.
2
3use gpui::{AnyElement, App, ElementId, SharedString, Window, div, prelude::*, px};
4use theme::{TextStyle, Theme, Typeset};
5
6/// What a fence holds its code in, inside its border.
7pub(crate) const CODE_PADDING_X: f32 = 12.0;
8pub(crate) const CODE_PADDING_Y: f32 = 10.0;
9
10/// The box a fence is drawn in, painted or as code.
11pub fn fence_panel(theme: &Theme) -> gpui::Div {
12    div()
13        .rounded(px(Theme::panel_radius()))
14        .bg(theme.ink(0.035))
15        .border_1()
16        .border_color(theme.border)
17        .overflow_hidden()
18        .relative()
19}
20
21/// The debug selector on a [`fence_note`].
22pub const NOTE_SELECTOR: &str = "fence-note";
23
24/// A note across the bottom of a [`fence_panel`] under its code — why the
25/// code was not painted — its lines kept as written.
26pub fn fence_note(note: SharedString, theme: &Theme) -> AnyElement {
27    div()
28        .debug_selector(|| NOTE_SELECTOR.into())
29        .flex()
30        .flex_col()
31        .px(px(CODE_PADDING_X))
32        .py(px(CODE_PADDING_Y))
33        .border_t_1()
34        .border_color(theme.border)
35        .text_color(theme.danger)
36        .font_family(theme.font_mono.clone())
37        .text_style(TextStyle::Subheadline)
38        .children(
39            note.lines()
40                .map(|line| SharedString::from(line.to_string()))
41                .collect::<Vec<_>>(),
42        )
43        .into_any_element()
44}
45
46/// The band across the top of a [`fence_panel`], where its label and copy
47/// button sit.
48pub fn fence_band(theme: &Theme) -> gpui::Div {
49    div()
50        .relative()
51        .flex()
52        .flex_row()
53        .items_center()
54        .px(px(CODE_PADDING_X))
55        .py(px(5.0))
56        .border_b_1()
57        .border_color(theme.border)
58        .bg(theme.ink(0.02))
59        .text_style(TextStyle::Subheadline)
60}
61
62/// A copy button that owns its own feedback.
63///
64/// The state is the element's, not the caller's: a component library cannot ask
65/// every host to thread a handler and a "which block is showing Copied" index
66/// through its render tree just to put a button on a code block. It resets when
67/// the pointer leaves, which needs no clock.
68pub(crate) fn copy_button(
69    code: &str,
70    ix: usize,
71    theme: &Theme,
72    window: &mut Window,
73    cx: &mut App,
74) -> AnyElement {
75    let copied = window.use_keyed_state(ElementId::named_usize("md-copied", ix), cx, |_, _| false);
76    let showing = *copied.read(cx);
77    let text: SharedString = code.to_string().into();
78
79    div()
80        .id(ElementId::named_usize("md-copy", ix))
81        .absolute()
82        .top(px(3.0))
83        .right(px(5.0))
84        .h(px(20.0))
85        .px(px(6.0))
86        .rounded(px(5.0))
87        .flex()
88        .items_center()
89        .cursor_pointer()
90        .text_style(TextStyle::Caption)
91        .text_color(theme.text_muted)
92        .hover(|el| el.bg(theme.element_hover))
93        .child(if showing { "Copied" } else { "Copy" })
94        .on_click({
95            let copied = copied.clone();
96            move |_, _, cx| {
97                cx.write_to_clipboard(gpui::ClipboardItem::new_string(text.to_string()));
98                copied.update(cx, |state, cx| {
99                    *state = true;
100                    cx.notify();
101                });
102            }
103        })
104        .on_hover(move |hovering, _, cx| {
105            if !*hovering && *copied.read(cx) {
106                copied.update(cx, |state, cx| {
107                    *state = false;
108                    cx.notify();
109                });
110            }
111        })
112        .into_any_element()
113}