Skip to repository content982 lines · 36.8 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:33:13.678Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
button_like.rs
1use documented::Documented;
2use gpui::{
3 AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, FocusHandle, Hsla, MouseButton,
4 MouseClickEvent, MouseDownEvent, MouseUpEvent, Rems, Role, StyleRefinement, Toggled, relative,
5 transparent_black,
6};
7use smallvec::SmallVec;
8
9use crate::{DynamicSpacing, ElevationIndex, prelude::*};
10
11/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
12pub trait SelectableButton: Toggleable {
13 fn selected_style(self, style: ButtonStyle) -> Self;
14}
15
16/// A common set of traits all buttons must implement.
17pub trait ButtonCommon: Clickable + Disableable {
18 /// A unique element ID to identify the button.
19 fn id(&self) -> &ElementId;
20
21 /// The visual style of the button.
22 ///
23 /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
24 /// for an emphasized button.
25 fn style(self, style: ButtonStyle) -> Self;
26
27 /// The size of the button.
28 ///
29 /// Most buttons will use the default size.
30 ///
31 /// [`ButtonSize`] can also be used to help build non-button elements
32 /// that are consistently sized with buttons.
33 fn size(self, size: ButtonSize) -> Self;
34
35 /// The tooltip that shows when a user hovers over the button.
36 ///
37 /// Nearly all interactable elements should have a tooltip. Some example
38 /// exceptions might a scroll bar, or a slider.
39 fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self;
40
41 fn tab_index(self, tab_index: impl Into<isize>) -> Self;
42
43 fn layer(self, elevation: ElevationIndex) -> Self;
44
45 fn track_focus(self, focus_handle: &FocusHandle) -> Self;
46}
47
48#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
49pub enum IconPosition {
50 #[default]
51 Start,
52 End,
53}
54
55#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
56pub enum KeybindingPosition {
57 Start,
58 #[default]
59 End,
60}
61
62#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
63pub enum TintColor {
64 #[default]
65 Accent,
66 Error,
67 Warning,
68 Success,
69}
70
71impl TintColor {
72 fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles {
73 match self {
74 TintColor::Accent => ButtonLikeStyles {
75 background: cx.theme().status().info_background,
76 border_color: cx.theme().status().info_border,
77 label_color: cx.theme().colors().text,
78 icon_color: cx.theme().colors().text,
79 },
80 TintColor::Error => ButtonLikeStyles {
81 background: cx.theme().status().error_background,
82 border_color: cx.theme().status().error_border,
83 label_color: cx.theme().colors().text,
84 icon_color: cx.theme().colors().text,
85 },
86 TintColor::Warning => ButtonLikeStyles {
87 background: cx.theme().status().warning_background,
88 border_color: cx.theme().status().warning_border,
89 label_color: cx.theme().colors().text,
90 icon_color: cx.theme().colors().text,
91 },
92 TintColor::Success => ButtonLikeStyles {
93 background: cx.theme().status().success_background,
94 border_color: cx.theme().status().success_border,
95 label_color: cx.theme().colors().text,
96 icon_color: cx.theme().colors().text,
97 },
98 }
99 }
100}
101
102impl From<TintColor> for Color {
103 fn from(tint: TintColor) -> Self {
104 match tint {
105 TintColor::Accent => Color::Accent,
106 TintColor::Error => Color::Error,
107 TintColor::Warning => Color::Warning,
108 TintColor::Success => Color::Success,
109 }
110 }
111}
112
113// Used to go from ButtonStyle -> Color through tint colors.
114impl From<ButtonStyle> for Color {
115 fn from(style: ButtonStyle) -> Self {
116 match style {
117 ButtonStyle::Tinted(tint) => tint.into(),
118 _ => Color::Default,
119 }
120 }
121}
122
123/// The visual appearance of a button.
124#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
125pub enum ButtonStyle {
126 /// A filled button with a solid background color. Provides emphasis versus
127 /// the more common subtle button.
128 Filled,
129
130 /// Used to emphasize a button in some way, like a selected state, or a semantic
131 /// coloring like an error or success button.
132 Tinted(TintColor),
133
134 /// Usually used as a secondary action that should have more emphasis than
135 /// a fully transparent button.
136 Outlined,
137
138 /// A more de-emphasized version of the outlined button.
139 OutlinedGhost,
140
141 /// Like [`ButtonStyle::Outlined`], but with a caller-provided border color.
142 OutlinedCustom(Hsla),
143
144 /// The default button style, used for most buttons. Has a transparent background,
145 /// but has a background color to indicate states like hover and active.
146 #[default]
147 Subtle,
148
149 /// Used for buttons that only change foreground color on hover and active states.
150 ///
151 /// TODO: Better docs for this.
152 Transparent,
153}
154
155/// Rounding for a button that may have straight edges.
156#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
157pub(crate) struct ButtonLikeRounding {
158 /// Top-left corner rounding
159 pub top_left: bool,
160 /// Top-right corner rounding
161 pub top_right: bool,
162 /// Bottom-right corner rounding
163 pub bottom_right: bool,
164 /// Bottom-left corner rounding
165 pub bottom_left: bool,
166}
167
168impl ButtonLikeRounding {
169 pub const ALL: Self = Self {
170 top_left: true,
171 top_right: true,
172 bottom_right: true,
173 bottom_left: true,
174 };
175 pub const LEFT: Self = Self {
176 top_left: true,
177 top_right: false,
178 bottom_right: false,
179 bottom_left: true,
180 };
181 pub const RIGHT: Self = Self {
182 top_left: false,
183 top_right: true,
184 bottom_right: true,
185 bottom_left: false,
186 };
187}
188
189#[derive(Debug, Clone)]
190pub(crate) struct ButtonLikeStyles {
191 pub background: Hsla,
192 #[allow(unused)]
193 pub border_color: Hsla,
194 #[allow(unused)]
195 pub label_color: Hsla,
196 #[allow(unused)]
197 pub icon_color: Hsla,
198}
199
200fn element_bg_from_elevation(elevation: Option<ElevationIndex>, cx: &mut App) -> Hsla {
201 match elevation {
202 Some(ElevationIndex::Background) => cx.theme().colors().element_background,
203 Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
204 Some(ElevationIndex::Surface) => cx.theme().colors().surface_background,
205 Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
206 _ => cx.theme().colors().element_background,
207 }
208}
209
210impl ButtonStyle {
211 pub(crate) fn enabled(
212 self,
213 elevation: Option<ElevationIndex>,
214 cx: &mut App,
215 ) -> ButtonLikeStyles {
216 match self {
217 ButtonStyle::Filled => ButtonLikeStyles {
218 background: element_bg_from_elevation(elevation, cx),
219 border_color: transparent_black(),
220 label_color: Color::Default.color(cx),
221 icon_color: Color::Default.color(cx),
222 },
223 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
224 ButtonStyle::Outlined => ButtonLikeStyles {
225 background: element_bg_from_elevation(elevation, cx),
226 border_color: cx.theme().colors().border_variant,
227 label_color: Color::Default.color(cx),
228 icon_color: Color::Default.color(cx),
229 },
230 ButtonStyle::OutlinedGhost => ButtonLikeStyles {
231 background: transparent_black(),
232 border_color: cx.theme().colors().border_variant,
233 label_color: Color::Default.color(cx),
234 icon_color: Color::Default.color(cx),
235 },
236 ButtonStyle::OutlinedCustom(border_color) => ButtonLikeStyles {
237 background: transparent_black(),
238 border_color,
239 label_color: Color::Default.color(cx),
240 icon_color: Color::Default.color(cx),
241 },
242 ButtonStyle::Subtle => ButtonLikeStyles {
243 background: cx.theme().colors().ghost_element_background,
244 border_color: transparent_black(),
245 label_color: Color::Default.color(cx),
246 icon_color: Color::Default.color(cx),
247 },
248 ButtonStyle::Transparent => ButtonLikeStyles {
249 background: transparent_black(),
250 border_color: transparent_black(),
251 label_color: Color::Default.color(cx),
252 icon_color: Color::Default.color(cx),
253 },
254 }
255 }
256
257 pub(crate) fn hovered(
258 self,
259 elevation: Option<ElevationIndex>,
260 cx: &mut App,
261 ) -> ButtonLikeStyles {
262 match self {
263 ButtonStyle::Filled => {
264 let mut filled_background = element_bg_from_elevation(elevation, cx);
265 filled_background.fade_out(0.5);
266
267 ButtonLikeStyles {
268 background: filled_background,
269 border_color: transparent_black(),
270 label_color: Color::Default.color(cx),
271 icon_color: Color::Default.color(cx),
272 }
273 }
274 ButtonStyle::Tinted(tint) => {
275 let mut styles = tint.button_like_style(cx);
276 let theme = cx.theme();
277 styles.background = theme.darken(styles.background, 0.05, 0.2);
278 styles
279 }
280 ButtonStyle::Outlined => ButtonLikeStyles {
281 background: cx.theme().colors().ghost_element_hover,
282 border_color: cx.theme().colors().border,
283 label_color: Color::Default.color(cx),
284 icon_color: Color::Default.color(cx),
285 },
286 ButtonStyle::OutlinedGhost => ButtonLikeStyles {
287 background: cx.theme().colors().ghost_element_hover,
288 border_color: cx.theme().colors().border,
289 label_color: Color::Default.color(cx),
290 icon_color: Color::Default.color(cx),
291 },
292 ButtonStyle::OutlinedCustom(border_color) => ButtonLikeStyles {
293 background: cx.theme().colors().ghost_element_hover,
294 border_color,
295 label_color: Color::Default.color(cx),
296 icon_color: Color::Default.color(cx),
297 },
298 ButtonStyle::Subtle => ButtonLikeStyles {
299 background: cx.theme().colors().ghost_element_hover,
300 border_color: transparent_black(),
301 label_color: Color::Default.color(cx),
302 icon_color: Color::Default.color(cx),
303 },
304 ButtonStyle::Transparent => ButtonLikeStyles {
305 background: transparent_black(),
306 border_color: transparent_black(),
307 // TODO: These are not great
308 label_color: Color::Muted.color(cx),
309 // TODO: These are not great
310 icon_color: Color::Muted.color(cx),
311 },
312 }
313 }
314
315 pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
316 match self {
317 ButtonStyle::Filled => ButtonLikeStyles {
318 background: cx.theme().colors().element_active,
319 border_color: transparent_black(),
320 label_color: Color::Default.color(cx),
321 icon_color: Color::Default.color(cx),
322 },
323 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
324 ButtonStyle::Subtle => ButtonLikeStyles {
325 background: cx.theme().colors().ghost_element_active,
326 border_color: transparent_black(),
327 label_color: Color::Default.color(cx),
328 icon_color: Color::Default.color(cx),
329 },
330 ButtonStyle::Outlined => ButtonLikeStyles {
331 background: cx.theme().colors().element_active,
332 border_color: cx.theme().colors().border_variant,
333 label_color: Color::Default.color(cx),
334 icon_color: Color::Default.color(cx),
335 },
336 ButtonStyle::OutlinedGhost => ButtonLikeStyles {
337 background: transparent_black(),
338 border_color: cx.theme().colors().border_variant,
339 label_color: Color::Default.color(cx),
340 icon_color: Color::Default.color(cx),
341 },
342 ButtonStyle::OutlinedCustom(border_color) => ButtonLikeStyles {
343 background: cx.theme().colors().element_active,
344 border_color,
345 label_color: Color::Default.color(cx),
346 icon_color: Color::Default.color(cx),
347 },
348 ButtonStyle::Transparent => ButtonLikeStyles {
349 background: transparent_black(),
350 border_color: transparent_black(),
351 // TODO: These are not great
352 label_color: Color::Muted.color(cx),
353 // TODO: These are not great
354 icon_color: Color::Muted.color(cx),
355 },
356 }
357 }
358
359 #[allow(unused)]
360 pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
361 match self {
362 ButtonStyle::Filled => ButtonLikeStyles {
363 background: cx.theme().colors().element_background,
364 border_color: cx.theme().colors().border_focused,
365 label_color: Color::Default.color(cx),
366 icon_color: Color::Default.color(cx),
367 },
368 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
369 ButtonStyle::Subtle => ButtonLikeStyles {
370 background: cx.theme().colors().ghost_element_background,
371 border_color: cx.theme().colors().border_focused,
372 label_color: Color::Default.color(cx),
373 icon_color: Color::Default.color(cx),
374 },
375 ButtonStyle::Outlined => ButtonLikeStyles {
376 background: cx.theme().colors().ghost_element_background,
377 border_color: cx.theme().colors().border,
378 label_color: Color::Default.color(cx),
379 icon_color: Color::Default.color(cx),
380 },
381 ButtonStyle::OutlinedGhost => ButtonLikeStyles {
382 background: transparent_black(),
383 border_color: cx.theme().colors().border,
384 label_color: Color::Default.color(cx),
385 icon_color: Color::Default.color(cx),
386 },
387 ButtonStyle::OutlinedCustom(border_color) => ButtonLikeStyles {
388 background: cx.theme().colors().ghost_element_background,
389 border_color,
390 label_color: Color::Default.color(cx),
391 icon_color: Color::Default.color(cx),
392 },
393 ButtonStyle::Transparent => ButtonLikeStyles {
394 background: transparent_black(),
395 border_color: cx.theme().colors().border_focused,
396 label_color: Color::Accent.color(cx),
397 icon_color: Color::Accent.color(cx),
398 },
399 }
400 }
401
402 #[allow(unused)]
403 pub(crate) fn disabled(
404 self,
405 elevation: Option<ElevationIndex>,
406 window: &mut Window,
407 cx: &mut App,
408 ) -> ButtonLikeStyles {
409 match self {
410 ButtonStyle::Filled => ButtonLikeStyles {
411 background: cx.theme().colors().element_disabled,
412 border_color: cx.theme().colors().border_disabled,
413 label_color: Color::Disabled.color(cx),
414 icon_color: Color::Disabled.color(cx),
415 },
416 ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
417 ButtonStyle::Subtle => ButtonLikeStyles {
418 background: cx.theme().colors().ghost_element_disabled,
419 border_color: cx.theme().colors().border_disabled,
420 label_color: Color::Disabled.color(cx),
421 icon_color: Color::Disabled.color(cx),
422 },
423 ButtonStyle::Outlined => ButtonLikeStyles {
424 background: cx.theme().colors().element_disabled,
425 border_color: cx.theme().colors().border_disabled,
426 label_color: Color::Default.color(cx),
427 icon_color: Color::Default.color(cx),
428 },
429 ButtonStyle::OutlinedGhost => ButtonLikeStyles {
430 background: transparent_black(),
431 border_color: cx.theme().colors().border_disabled,
432 label_color: Color::Default.color(cx),
433 icon_color: Color::Default.color(cx),
434 },
435 ButtonStyle::OutlinedCustom(_) => ButtonLikeStyles {
436 background: cx.theme().colors().element_disabled,
437 border_color: cx.theme().colors().border_disabled,
438 label_color: Color::Default.color(cx),
439 icon_color: Color::Default.color(cx),
440 },
441 ButtonStyle::Transparent => ButtonLikeStyles {
442 background: transparent_black(),
443 border_color: transparent_black(),
444 label_color: Color::Disabled.color(cx),
445 icon_color: Color::Disabled.color(cx),
446 },
447 }
448 }
449}
450
451/// The height of a button.
452///
453/// Can also be used to size non-button elements to align with [`Button`]s.
454#[derive(Default, PartialEq, Clone, Copy)]
455pub enum ButtonSize {
456 Large,
457 Medium,
458 #[default]
459 Default,
460 Compact,
461 None,
462}
463
464impl ButtonSize {
465 pub fn rems(self) -> Rems {
466 match self {
467 ButtonSize::Large => rems_from_px(32.),
468 ButtonSize::Medium => rems_from_px(28.),
469 ButtonSize::Default => rems_from_px(22.),
470 ButtonSize::Compact => rems_from_px(18.),
471 ButtonSize::None => rems_from_px(16.),
472 }
473 }
474}
475
476/// A button-like element that can be used to create a custom button when
477/// prebuilt buttons are not sufficient. Use this sparingly, as it is
478/// unconstrained and may make the UI feel less consistent.
479///
480/// This is also used to build the prebuilt buttons.
481#[derive(IntoElement, Documented, RegisterComponent)]
482pub struct ButtonLike {
483 pub(super) base: Div,
484 id: ElementId,
485 pub(super) style: ButtonStyle,
486 pub(super) disabled: bool,
487 pub(super) selected: bool,
488 pub(super) selected_style: Option<ButtonStyle>,
489 pub(super) width: Option<DefiniteLength>,
490 pub(super) height: Option<DefiniteLength>,
491 pub(super) layer: Option<ElevationIndex>,
492 tab_index: Option<isize>,
493 size: ButtonSize,
494 rounding: Option<ButtonLikeRounding>,
495 pub(super) aria_label: Option<SharedString>,
496 aria_description: Option<SharedString>,
497 pub(super) aria_value: Option<SharedString>,
498 pub(super) aria_keyshortcuts: Option<SharedString>,
499 pub(super) aria_role: Option<Role>,
500 aria_expanded: Option<bool>,
501 toggled: Option<bool>,
502 tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
503 hoverable_tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
504 cursor_style: CursorStyle,
505 on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
506 on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
507 a11y_actions: Vec<(
508 gpui::accesskit::Action,
509 Box<dyn FnMut(Option<&gpui::accesskit::ActionData>, &mut Window, &mut App) + 'static>,
510 )>,
511 children: SmallVec<[AnyElement; 2]>,
512 focus_handle: Option<FocusHandle>,
513}
514
515impl ButtonLike {
516 pub fn new(id: impl Into<ElementId>) -> Self {
517 Self {
518 base: div(),
519 id: id.into(),
520 style: ButtonStyle::default(),
521 disabled: false,
522 selected: false,
523 selected_style: None,
524 width: None,
525 height: None,
526 size: ButtonSize::Default,
527 rounding: Some(ButtonLikeRounding::ALL),
528 aria_label: None,
529 aria_description: None,
530 aria_value: None,
531 aria_keyshortcuts: None,
532 aria_role: None,
533 aria_expanded: None,
534 toggled: None,
535 tooltip: None,
536 hoverable_tooltip: None,
537 children: SmallVec::new(),
538 cursor_style: CursorStyle::PointingHand,
539 on_click: None,
540 on_right_click: None,
541 a11y_actions: Vec::new(),
542 layer: None,
543 tab_index: None,
544 focus_handle: None,
545 }
546 }
547
548 pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
549 Self::new(id).rounding(ButtonLikeRounding::LEFT)
550 }
551
552 pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
553 Self::new(id).rounding(ButtonLikeRounding::RIGHT)
554 }
555
556 pub fn new_rounded_all(id: impl Into<ElementId>) -> Self {
557 Self::new(id).rounding(ButtonLikeRounding::ALL)
558 }
559
560 pub fn opacity(mut self, opacity: f32) -> Self {
561 self.base = self.base.opacity(opacity);
562 self
563 }
564
565 pub fn height(mut self, height: DefiniteLength) -> Self {
566 self.height = Some(height);
567 self
568 }
569
570 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
571 self.rounding = rounding.into();
572 self
573 }
574
575 pub fn on_right_click(
576 mut self,
577 handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
578 ) -> Self {
579 self.on_right_click = Some(Box::new(handler));
580 self
581 }
582
583 pub fn hoverable_tooltip(
584 mut self,
585 tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static,
586 ) -> Self {
587 self.hoverable_tooltip = Some(Box::new(tooltip));
588 self
589 }
590
591 /// Sets the label announced by assistive technology for this button.
592 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
593 self.aria_label = Some(label.into());
594 self
595 }
596
597 /// Sets the keyboard shortcut announced by assistive technology for this
598 /// button. Use a human-friendly display string (the accelerator shown to
599 /// sighted users), e.g. `"Ctrl-S"` - see
600 /// [`KeyBinding::keyboard_shortcut_text`](crate::KeyBinding::keyboard_shortcut_text).
601 /// [`Button`](crate::Button) sets this automatically from its displayed
602 /// keybinding.
603 pub fn aria_keyshortcuts(mut self, keyshortcuts: impl Into<SharedString>) -> Self {
604 self.aria_keyshortcuts = Some(keyshortcuts.into());
605 self
606 }
607
608 /// Sets the supplementary description announced by assistive technology
609 /// after the button's name, role, and value.
610 pub fn aria_description(mut self, description: impl Into<SharedString>) -> Self {
611 self.aria_description = Some(description.into());
612 self
613 }
614
615 /// Sets the current value reported to assistive technology. Use this when
616 /// the button represents a control with a value, such as a combobox
617 /// trigger whose value is the current selection.
618 pub fn aria_value(mut self, value: impl Into<SharedString>) -> Self {
619 self.aria_value = Some(value.into());
620 self
621 }
622
623 /// Overrides the role reported to assistive technology.
624 /// Defaults to [`Role::Button`].
625 pub fn aria_role(mut self, role: Role) -> Self {
626 self.aria_role = Some(role);
627 self
628 }
629
630 /// Sets the expanded state reported to assistive technology, for buttons
631 /// that control a popup (e.g. dropdown or disclosure triggers).
632 pub fn aria_expanded(mut self, expanded: bool) -> Self {
633 self.aria_expanded = Some(expanded);
634 self
635 }
636
637 /// Registers a handler for an accessibility action (e.g.
638 /// [`accesskit::Action::Expand`]) dispatched by assistive technology to
639 /// this button. Also advertises the action to assistive technology.
640 pub fn on_a11y_action(
641 mut self,
642 action: gpui::accesskit::Action,
643 listener: impl FnMut(Option<&gpui::accesskit::ActionData>, &mut Window, &mut App) + 'static,
644 ) -> Self {
645 self.a11y_actions.push((action, Box::new(listener)));
646 self
647 }
648}
649
650impl Disableable for ButtonLike {
651 fn disabled(mut self, disabled: bool) -> Self {
652 self.disabled = disabled;
653 self
654 }
655}
656
657impl Toggleable for ButtonLike {
658 fn toggle_state(mut self, selected: bool) -> Self {
659 self.selected = selected;
660 self.toggled = Some(selected);
661 self
662 }
663}
664
665impl SelectableButton for ButtonLike {
666 fn selected_style(mut self, style: ButtonStyle) -> Self {
667 self.selected_style = Some(style);
668 self
669 }
670}
671
672impl Clickable for ButtonLike {
673 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
674 self.on_click = Some(Box::new(handler));
675 self
676 }
677
678 fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
679 self.cursor_style = cursor_style;
680 self
681 }
682}
683
684impl FixedWidth for ButtonLike {
685 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
686 self.width = Some(width.into());
687 self
688 }
689
690 fn full_width(mut self) -> Self {
691 self.width = Some(relative(1.));
692 self
693 }
694}
695
696impl ButtonCommon for ButtonLike {
697 fn id(&self) -> &ElementId {
698 &self.id
699 }
700
701 fn style(mut self, style: ButtonStyle) -> Self {
702 self.style = style;
703 self
704 }
705
706 fn size(mut self, size: ButtonSize) -> Self {
707 self.size = size;
708 self
709 }
710
711 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
712 self.tooltip = Some(Box::new(tooltip));
713 self
714 }
715
716 fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
717 self.tab_index = Some(tab_index.into());
718 self
719 }
720
721 fn layer(mut self, elevation: ElevationIndex) -> Self {
722 self.layer = Some(elevation);
723 self
724 }
725
726 fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
727 self.focus_handle = Some(focus_handle.clone());
728 self
729 }
730}
731
732impl VisibleOnHover for ButtonLike {
733 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
734 self.base = self.base.visible_on_hover(group_name);
735 self
736 }
737}
738
739impl ParentElement for ButtonLike {
740 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
741 self.children.extend(elements)
742 }
743}
744
745impl RenderOnce for ButtonLike {
746 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
747 let style = self
748 .selected_style
749 .filter(|_| self.selected)
750 .unwrap_or(self.style);
751
752 let is_outlined = matches!(
753 self.style,
754 ButtonStyle::Outlined | ButtonStyle::OutlinedGhost | ButtonStyle::OutlinedCustom(_)
755 );
756
757 self.base
758 .h_flex()
759 .id(self.id.clone())
760 .role(self.aria_role.unwrap_or(Role::Button))
761 .when_some(self.aria_label, |this, label| this.aria_label(label))
762 .when_some(self.aria_keyshortcuts, |this, keyshortcuts| {
763 this.aria_keyshortcuts(keyshortcuts)
764 })
765 .when_some(self.aria_description, |this, description| {
766 this.aria_description(description)
767 })
768 .when_some(self.aria_value, |this, value| this.aria_value(value))
769 .when_some(self.aria_expanded, |this, expanded| {
770 this.aria_expanded(expanded)
771 })
772 .when_some(self.toggled, |this, toggled| {
773 this.aria_toggled(if toggled {
774 Toggled::True
775 } else {
776 Toggled::False
777 })
778 })
779 .when_some(self.tab_index, |this, tab_index| this.tab_index(tab_index))
780 .when_some(self.focus_handle, |this, focus_handle| {
781 this.track_focus(&focus_handle)
782 })
783 .font_ui(cx)
784 .group("")
785 .flex_none()
786 .h(self.height.unwrap_or(self.size.rems().into()))
787 .when_some(self.width, |this, width| {
788 this.w(width).justify_center().text_center()
789 })
790 .when(is_outlined, |this| this.border_1())
791 .when_some(self.rounding, |this, rounding| {
792 this.when(rounding.top_left, |this| this.rounded_tl_sm())
793 .when(rounding.top_right, |this| this.rounded_tr_sm())
794 .when(rounding.bottom_right, |this| this.rounded_br_sm())
795 .when(rounding.bottom_left, |this| this.rounded_bl_sm())
796 })
797 .gap(DynamicSpacing::Base04.rems(cx))
798 .map(|this| match self.size {
799 ButtonSize::Large | ButtonSize::Medium => this.px(DynamicSpacing::Base08.rems(cx)),
800 ButtonSize::Default | ButtonSize::Compact => {
801 this.px(DynamicSpacing::Base04.rems(cx))
802 }
803 ButtonSize::None => this.px_px(),
804 })
805 .border_color(style.enabled(self.layer, cx).border_color)
806 .bg(style.enabled(self.layer, cx).background)
807 .when(self.disabled, |this| {
808 if self.cursor_style == CursorStyle::PointingHand {
809 this.cursor_not_allowed()
810 } else {
811 this.cursor(self.cursor_style)
812 }
813 })
814 .when(!self.disabled, |this| {
815 let hovered_style = style.hovered(self.layer, cx);
816 let focus_color =
817 |refinement: StyleRefinement| refinement.bg(hovered_style.background);
818
819 this.cursor(self.cursor_style)
820 .hover(focus_color)
821 .map(|this| {
822 if is_outlined {
823 this.focus_visible(|s| {
824 s.border_color(cx.theme().colors().border_focused)
825 })
826 } else {
827 this.focus_visible(focus_color)
828 }
829 })
830 .active(|active| active.bg(style.active(cx).background))
831 })
832 .when_some(
833 self.on_right_click.filter(|_| !self.disabled),
834 |this, on_right_click| {
835 this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
836 window.prevent_default();
837 cx.stop_propagation();
838 })
839 .on_mouse_up(
840 MouseButton::Right,
841 move |event, window, cx| {
842 cx.stop_propagation();
843 let click_event = ClickEvent::Mouse(MouseClickEvent {
844 down: MouseDownEvent {
845 button: MouseButton::Right,
846 position: event.position,
847 modifiers: event.modifiers,
848 click_count: 1,
849 first_mouse: false,
850 },
851 up: MouseUpEvent {
852 button: MouseButton::Right,
853 position: event.position,
854 modifiers: event.modifiers,
855 click_count: 1,
856 },
857 });
858 (on_right_click)(&click_event, window, cx)
859 },
860 )
861 },
862 )
863 .when_some(
864 self.on_click.filter(|_| !self.disabled),
865 |this, on_click| {
866 this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
867 .on_click(move |event, window, cx| {
868 cx.stop_propagation();
869 (on_click)(event, window, cx)
870 })
871 },
872 )
873 .when_some(self.tooltip, |this, tooltip| {
874 this.tooltip(move |window, cx| tooltip(window, cx))
875 })
876 .when_some(self.hoverable_tooltip, |this, tooltip| {
877 this.hoverable_tooltip(move |window, cx| tooltip(window, cx))
878 })
879 .map(|mut this| {
880 for (action, listener) in self.a11y_actions {
881 this = this.on_a11y_action(action, listener);
882 }
883 this
884 })
885 .children(self.children)
886 }
887}
888
889impl Component for ButtonLike {
890 fn scope() -> ComponentScope {
891 ComponentScope::Input
892 }
893
894 fn sort_name() -> &'static str {
895 // ButtonLike should be at the bottom of the button list
896 "ButtonZ"
897 }
898
899 fn description() -> &'static str {
900 ButtonLike::DOCS
901 }
902
903 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
904 v_flex()
905 .gap_6()
906 .children(vec![
907 example_group(vec![
908 single_example(
909 "Default",
910 ButtonLike::new("default")
911 .child(Label::new("Default"))
912 .into_any_element(),
913 ),
914 single_example(
915 "Filled",
916 ButtonLike::new("filled")
917 .style(ButtonStyle::Filled)
918 .child(Label::new("Filled"))
919 .into_any_element(),
920 ),
921 single_example(
922 "Subtle",
923 ButtonLike::new("outline")
924 .style(ButtonStyle::Subtle)
925 .child(Label::new("Subtle"))
926 .into_any_element(),
927 ),
928 single_example(
929 "Tinted",
930 ButtonLike::new("tinted_accent_style")
931 .style(ButtonStyle::Tinted(TintColor::Accent))
932 .child(Label::new("Accent"))
933 .into_any_element(),
934 ),
935 single_example(
936 "Transparent",
937 ButtonLike::new("transparent")
938 .style(ButtonStyle::Transparent)
939 .child(Label::new("Transparent"))
940 .into_any_element(),
941 ),
942 ]),
943 example_group_with_title(
944 "Button Group Constructors",
945 vec![
946 single_example(
947 "Left Rounded",
948 ButtonLike::new_rounded_left("left_rounded")
949 .child(Label::new("Left Rounded"))
950 .style(ButtonStyle::Filled)
951 .into_any_element(),
952 ),
953 single_example(
954 "Right Rounded",
955 ButtonLike::new_rounded_right("right_rounded")
956 .child(Label::new("Right Rounded"))
957 .style(ButtonStyle::Filled)
958 .into_any_element(),
959 ),
960 single_example(
961 "Button Group",
962 h_flex()
963 .gap_px()
964 .child(
965 ButtonLike::new_rounded_left("bg_left")
966 .child(Label::new("Left"))
967 .style(ButtonStyle::Filled),
968 )
969 .child(
970 ButtonLike::new_rounded_right("bg_right")
971 .child(Label::new("Right"))
972 .style(ButtonStyle::Filled),
973 )
974 .into_any_element(),
975 ),
976 ],
977 ),
978 ])
979 .into_any_element()
980 }
981}
982