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}