Skip to main content

markdown/
controls.rs

1//! Minimal, raw-`gpui` replacements for `ui::{Checkbox, CopyButton,
2//! ScrollAxes, Scrollbars, Tooltip, WithScrollbar}`-style components. This
3//! crate cannot depend on `boltz-ui` (`boltz-ui` depends on `boltz-markdown`,
4//! so a `boltz-ui` dependency here would be a cycle), so these are
5//! deliberately thin: just enough for `entity.rs`/`element.rs` to render
6//! checkboxes, a code-block copy button, hover tooltips, and scrollable
7//! containers. They are not meant to visually match `boltz-ui`'s richer
8//! components pixel-for-pixel.
9
10use gpui::{
11    AnyView, App, AppContext as _, ClickEvent, ClipboardItem, Div, ElementId, InteractiveElement,
12    IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement as _, Styled,
13    Window, div, prelude::FluentBuilder as _, px, rgb,
14};
15use icons::IconName;
16
17/// A minimal checkbox: a bordered square that renders a checkmark glyph when
18/// `checked`, and invokes `on_click` (toggling is the caller's
19/// responsibility — this has no internal state, matching every other
20/// stateless-config component in this crate) on click.
21#[derive(IntoElement)]
22pub struct Checkbox {
23    id: ElementId,
24    checked: bool,
25    disabled: bool,
26    on_click: Option<Box<dyn Fn(&bool, &mut Window, &mut App) + 'static>>,
27}
28
29impl Checkbox {
30    pub fn new(id: impl Into<ElementId>, checked: bool) -> Self {
31        Self {
32            id: id.into(),
33            checked,
34            disabled: false,
35            on_click: None,
36        }
37    }
38
39    pub fn disabled(mut self, disabled: bool) -> Self {
40        self.disabled = disabled;
41        self
42    }
43
44    /// `on_click` receives the checkbox's *new* intended state (`!checked`).
45    pub fn on_click(mut self, on_click: impl Fn(&bool, &mut Window, &mut App) + 'static) -> Self {
46        self.on_click = Some(Box::new(on_click));
47        self
48    }
49}
50
51impl RenderOnce for Checkbox {
52    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
53        let checked = self.checked;
54        let disabled = self.disabled;
55        let on_click = self.on_click;
56
57        div()
58            .id(self.id)
59            .size(px(14.))
60            .flex()
61            .items_center()
62            .justify_center()
63            .rounded_xs()
64            .border_1()
65            .border_color(rgb(0x4A5568))
66            .when(checked, |this| this.bg(rgb(0x3B82F6)))
67            .when(!disabled, |this| this.cursor_pointer())
68            .when_some(on_click.filter(|_| !disabled), |this, on_click| {
69                this.on_click(move |_: &ClickEvent, window, cx| {
70                    on_click(&!checked, window, cx);
71                })
72            })
73            .when(checked, |this| {
74                this.child(
75                    div()
76                        .size(px(8.))
77                        .text_color(rgb(0xFFFFFF))
78                        .child(SharedString::from("\u{2713}")),
79                )
80            })
81    }
82}
83
84/// A small icon button that copies `text` to the clipboard on click. No
85/// internal "copied" state (that would require this to be a stateful
86/// `Entity`) — callers that want a "copied!" flash can track that externally
87/// and swap `icon`/`tooltip`.
88#[derive(IntoElement)]
89pub struct CopyButton {
90    id: ElementId,
91    text: SharedString,
92    icon: IconName,
93}
94
95impl CopyButton {
96    pub fn new(id: impl Into<ElementId>, text: impl Into<SharedString>) -> Self {
97        Self {
98            id: id.into(),
99            text: text.into(),
100            icon: IconName::Copy,
101        }
102    }
103
104    pub fn icon(mut self, icon: IconName) -> Self {
105        self.icon = icon;
106        self
107    }
108}
109
110impl RenderOnce for CopyButton {
111    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
112        let text = self.text;
113
114        div()
115            .id(self.id)
116            .flex()
117            .items_center()
118            .justify_center()
119            .size(px(20.))
120            .rounded_xs()
121            .cursor_pointer()
122            .hover(|this| this.bg(rgb(0x2D3748)))
123            .child(icon_svg(self.icon, px(14.), rgb(0xA0AEC0)))
124            .on_click(move |_: &ClickEvent, _window, cx| {
125                cx.write_to_clipboard(ClipboardItem::new_string(text.to_string()));
126            })
127    }
128}
129
130/// Renders a `boltz-icons` glyph via `gpui::svg()` directly — this crate
131/// deliberately avoids `boltz-ui`'s `Icon` component (see module docs).
132pub fn icon_svg(
133    icon: IconName,
134    size: impl Into<gpui::Pixels>,
135    color: impl Into<gpui::Hsla>,
136) -> gpui::Svg {
137    gpui::svg()
138        .path(icon.path())
139        .size(size.into())
140        .text_color(color.into())
141}
142
143/// A minimal hover tooltip carrying a single line of text, built the same
144/// way `ui::Tooltip::text` does (`cx.new(|_| ...).into()`) but without the
145/// `KeyBinding`/`meta` extras that would require depending on `boltz-menu`.
146struct SimpleTooltip {
147    text: SharedString,
148}
149
150impl gpui::Render for SimpleTooltip {
151    fn render(&mut self, _window: &mut Window, _cx: &mut gpui::Context<Self>) -> impl IntoElement {
152        div()
153            .rounded_xs()
154            .bg(rgb(0x1A202C))
155            .text_color(rgb(0xE2E8F0))
156            .px_2()
157            .py_1()
158            .text_sm()
159            .child(self.text.clone())
160    }
161}
162
163/// Builds a `.tooltip(...)` callback showing `text` on hover, usable as
164/// `div().tooltip(simple_tooltip("Copy"))`.
165pub fn simple_tooltip(text: impl Into<SharedString>) -> impl Fn(&mut Window, &mut App) -> AnyView {
166    let text = text.into();
167    move |_window, cx| cx.new(|_| SimpleTooltip { text: text.clone() }).into()
168}
169
170/// Which axes a scrollable container should scroll along. Shaped closely
171/// enough to `ui::ScrollAxes` for `element.rs`/`rendered.rs` to swap in
172/// later, but only wraps gpui's own `overflow_{x,y}_scroll` + `track_scroll`
173/// (no custom scrollbar thumb/track rendering: `boltz-gpui` exposes no
174/// primitive lower-level than `boltz-ui`'s `Scrollbars` for that, so this is
175/// a minimal passthrough fallback).
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
177pub enum ScrollAxes {
178    Vertical,
179    Horizontal,
180    Both,
181}
182
183/// Minimal `WithScrollbar` replacement: applies native `overflow_scroll` +
184/// `track_scroll` for the requested axes. Does not render a visible
185/// scrollbar thumb/track (see [`ScrollAxes`] docs). Takes a `Stateful<Div>`
186/// (i.e. after `.id(...)`) because gpui only implements
187/// `StatefulInteractiveElement` (which provides `overflow_*_scroll`/
188/// `track_scroll`) for stateful elements, not bare `Div`.
189pub trait WithScrollbar {
190    fn with_scrollbar(self, handle: &gpui::ScrollHandle, axes: ScrollAxes) -> Self;
191}
192
193impl WithScrollbar for gpui::Stateful<Div> {
194    fn with_scrollbar(self, handle: &gpui::ScrollHandle, axes: ScrollAxes) -> Self {
195        let this = match axes {
196            ScrollAxes::Vertical => self.overflow_y_scroll(),
197            ScrollAxes::Horizontal => self.overflow_x_scroll(),
198            ScrollAxes::Both => self.overflow_scroll(),
199        };
200        this.track_scroll(handle)
201    }
202}