Skip to main content

guise/
chip.rs

1//! `Chip` — a selectable pill (controlled).
2
3use gpui::prelude::*;
4use gpui::{div, px, App, ClickEvent, ElementId, FontWeight, IntoElement, SharedString, Window};
5
6use crate::input::ClickHandler;
7use crate::reactive::Binding;
8use crate::style::ColorValue;
9use crate::theme::{theme, Size};
10
11/// A selectable chip. The Mantine `Chip`. Controlled: pass `checked` and a
12/// change handler via `cx.listener`, or two-way bind with [`Chip::bind`].
13#[derive(IntoElement)]
14pub struct Chip {
15    id: ElementId,
16    label: SharedString,
17    checked: bool,
18    color: ColorValue,
19    size: Size,
20    binding: Option<Binding<bool>>,
21    on_change: Option<ClickHandler>,
22}
23
24impl Chip {
25    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
26        Chip {
27            id: id.into(),
28            label: label.into(),
29            checked: false,
30            color: ColorValue::default(),
31            size: Size::Md,
32            binding: None,
33            on_change: None,
34        }
35    }
36
37    pub fn checked(mut self, checked: bool) -> Self {
38        self.checked = checked;
39        self
40    }
41
42    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
43        self.color = color.into();
44        self
45    }
46
47    pub fn size(mut self, size: Size) -> Self {
48        self.size = size;
49        self
50    }
51
52    /// Two-way bind the checked state. Overrides `checked`; clicks write the
53    /// toggled value back through the binding, then run any `on_change`.
54    pub fn bind(mut self, binding: Binding<bool>) -> Self {
55        self.binding = Some(binding);
56        self
57    }
58
59    pub fn on_change(
60        mut self,
61        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
62    ) -> Self {
63        self.on_change = Some(Box::new(handler));
64        self
65    }
66
67    fn metrics(&self) -> (f32, f32, f32) {
68        match self.size {
69            Size::Xs => (24.0, 10.0, 11.0),
70            Size::Sm => (28.0, 12.0, 12.0),
71            Size::Md => (32.0, 16.0, 14.0),
72            Size::Lg => (38.0, 20.0, 16.0),
73            Size::Xl => (44.0, 24.0, 18.0),
74        }
75    }
76}
77
78impl RenderOnce for Chip {
79    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
80        let t = theme(cx);
81        let (height, pad_x, font) = self.metrics();
82        let accent = self.color.accent(t);
83        let checked = self.binding.as_ref().map_or(self.checked, |b| b.get(cx));
84
85        let (bg, fg, border) = if checked {
86            (self.color.soft(t), accent, accent)
87        } else {
88            (t.surface().hsla(), t.text().hsla(), t.border().hsla())
89        };
90        let hover_bg = t.surface_hover().hsla();
91
92        let mut el = div()
93            .id(self.id)
94            .flex()
95            .items_center()
96            .gap(px(6.0))
97            .h(px(height))
98            .px(px(pad_x))
99            .rounded(px(height))
100            .border_1()
101            .border_color(border)
102            .bg(bg)
103            .text_color(fg)
104            .text_size(px(font))
105            .font_weight(FontWeight::MEDIUM);
106        if checked {
107            el = el.child(SharedString::new_static("\u{2713}"));
108        } else {
109            el = el.hover(move |s| s.bg(hover_bg));
110        }
111        el = el.child(self.label);
112        if self.binding.is_some() || self.on_change.is_some() {
113            let binding = self.binding;
114            let handler = self.on_change;
115            let next = !checked;
116            el = el.on_click(move |ev, window, cx| {
117                if let Some(binding) = &binding {
118                    binding.set(cx, next);
119                }
120                if let Some(handler) = &handler {
121                    handler(ev, window, cx);
122                }
123            });
124        }
125        el
126    }
127}