1use gpui::prelude::*;
7use gpui::*;
8
9#[derive(Debug, Clone)]
11pub struct IconButtonTheme {
12 pub ghost_bg: Rgba,
14 pub ghost_hover_bg: Rgba,
16 pub selected_bg: Rgba,
18 pub selected_hover_bg: Rgba,
20 pub filled_bg: Rgba,
22 pub filled_hover_bg: Rgba,
24 pub accent: Rgba,
26 pub accent_hover: Rgba,
28 pub text: Rgba,
30 pub text_on_accent: Rgba,
32 pub border: Rgba,
34}
35
36impl Default for IconButtonTheme {
37 fn default() -> Self {
38 Self {
39 ghost_bg: rgba(0x00000000),
40 ghost_hover_bg: rgba(0x3a3a3aff),
41 selected_bg: rgba(0x3a3a3aff),
42 selected_hover_bg: rgba(0x4a4a4aff),
43 filled_bg: rgba(0x3a3a3aff),
44 filled_hover_bg: rgba(0x4a4a4aff),
45 accent: rgba(0x007accff),
46 accent_hover: rgba(0x0098ffff),
47 text: rgba(0xccccccff),
48 text_on_accent: rgba(0xffffffff),
49 border: rgba(0x555555ff),
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum IconButtonSize {
57 Xs,
59 Sm,
61 #[default]
63 Md,
64 Lg,
66 Xl,
68 Custom(u32),
70}
71
72impl IconButtonSize {
73 pub fn size(&self) -> Pixels {
75 match self {
76 IconButtonSize::Xs => px(16.0),
77 IconButtonSize::Sm => px(20.0),
78 IconButtonSize::Md => px(24.0),
79 IconButtonSize::Lg => px(32.0),
80 IconButtonSize::Xl => px(48.0),
81 IconButtonSize::Custom(size) => px(*size as f32),
82 }
83 }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum IconButtonVariant {
89 #[default]
91 Ghost,
92 Filled,
94 Outline,
96}
97
98enum IconContent {
100 Text(SharedString),
101 Element(AnyElement),
102}
103
104pub struct IconButton {
121 id: ElementId,
122 content: IconContent,
123 size: IconButtonSize,
124 variant: IconButtonVariant,
125 disabled: bool,
126 selected: bool,
127 rounded_full: bool,
128 padding: Option<Pixels>,
129 theme: Option<IconButtonTheme>,
130 on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
131}
132
133impl IconButton {
134 pub fn new(id: impl Into<ElementId>, icon: impl Into<SharedString>) -> Self {
136 Self {
137 id: id.into(),
138 content: IconContent::Text(icon.into()),
139 size: IconButtonSize::default(),
140 variant: IconButtonVariant::default(),
141 disabled: false,
142 selected: false,
143 rounded_full: false,
144 padding: None,
145 theme: None,
146 on_click: None,
147 }
148 }
149
150 pub fn with_child(id: impl Into<ElementId>, child: impl IntoElement) -> Self {
152 Self {
153 id: id.into(),
154 content: IconContent::Element(child.into_any_element()),
155 size: IconButtonSize::default(),
156 variant: IconButtonVariant::default(),
157 disabled: false,
158 selected: false,
159 rounded_full: false,
160 padding: None,
161 theme: None,
162 on_click: None,
163 }
164 }
165
166 pub fn size(mut self, size: IconButtonSize) -> Self {
168 self.size = size;
169 self
170 }
171
172 pub fn variant(mut self, variant: IconButtonVariant) -> Self {
174 self.variant = variant;
175 self
176 }
177
178 pub fn disabled(mut self, disabled: bool) -> Self {
180 self.disabled = disabled;
181 self
182 }
183
184 pub fn selected(mut self, selected: bool) -> Self {
186 self.selected = selected;
187 self
188 }
189
190 pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
192 self.on_click = Some(Box::new(handler));
193 self
194 }
195
196 pub fn rounded_full(mut self) -> Self {
198 self.rounded_full = true;
199 self
200 }
201
202 pub fn padding(mut self, padding: Pixels) -> Self {
204 self.padding = Some(padding);
205 self
206 }
207
208 pub fn theme(mut self, theme: IconButtonTheme) -> Self {
210 self.theme = Some(theme);
211 self
212 }
213
214 pub fn compute_colors(&self) -> (Rgba, Rgba, Rgba, Option<Rgba>) {
216 let default_theme = IconButtonTheme::default();
217 let theme = self.theme.as_ref().unwrap_or(&default_theme);
218
219 match self.variant {
220 IconButtonVariant::Ghost => {
221 if self.selected {
222 (
223 theme.selected_bg,
224 theme.selected_hover_bg,
225 theme.text_on_accent,
226 None,
227 )
228 } else {
229 (theme.ghost_bg, theme.ghost_hover_bg, theme.text, None)
230 }
231 }
232 IconButtonVariant::Filled => {
233 if self.selected {
234 (theme.accent, theme.accent_hover, theme.text_on_accent, None)
235 } else {
236 (theme.filled_bg, theme.filled_hover_bg, theme.text, None)
237 }
238 }
239 IconButtonVariant::Outline => {
240 if self.selected {
241 (
242 theme.selected_bg,
243 theme.selected_hover_bg,
244 theme.text_on_accent,
245 Some(theme.accent),
246 )
247 } else {
248 (
249 theme.ghost_bg,
250 theme.ghost_hover_bg,
251 theme.text,
252 Some(theme.border),
253 )
254 }
255 }
256 }
257 }
258
259 pub fn build(self) -> Stateful<Div> {
261 let size = self.size.size();
262 let (bg, bg_hover, text_color, border) = self.compute_colors();
263
264 let mut el = div()
265 .id(self.id)
266 .flex()
267 .items_center()
268 .justify_center()
269 .w(size)
270 .h(size)
271 .bg(bg)
272 .text_color(text_color)
273 .cursor_pointer();
274
275 if let Some(padding) = self.padding {
277 el = el.p(padding);
278 }
279
280 if self.rounded_full {
282 el = el.rounded_full();
283 } else {
284 el = el.rounded_md();
285 }
286
287 if let Some(border_color) = border {
288 el = el.border_1().border_color(border_color);
289 }
290
291 if self.disabled {
292 el = el.opacity(0.5).cursor_not_allowed();
293 } else {
294 el = el.hover(|s| s.bg(bg_hover));
295
296 if let Some(handler) = self.on_click {
297 el = el.on_mouse_up(MouseButton::Left, move |_event, window, cx| {
298 handler(window, cx);
299 });
300 }
301 }
302
303 match self.content {
305 IconContent::Text(text) => el.child(text),
306 IconContent::Element(element) => el.child(element),
307 }
308 }
309}
310
311impl IntoElement for IconButton {
312 type Element = Stateful<Div>;
313
314 fn into_element(self) -> Self::Element {
315 self.build()
316 }
317}