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