Skip to repository content649 lines · 22.6 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:34:51.385Z 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.rs
1use crate::component_prelude::*;
2use gpui::{AnyElement, AnyView, DefiniteLength};
3use ui_macros::RegisterComponent;
4
5use crate::traits::animation_ext::CommonAnimationExt;
6use crate::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle, Icon, Label};
7use crate::{
8 Color, DynamicSpacing, ElevationIndex, KeyBinding, KeybindingPosition, TintColor, prelude::*,
9};
10
11/// An element that creates a button with a label and optional icons.
12///
13/// Common buttons:
14/// - Label, Icon + Label: [`Button`] (this component)
15/// - Icon only: [`IconButton`]
16/// - Custom: [`ButtonLike`]
17///
18/// To create a more complex button than what the [`Button`] or [`IconButton`] components provide, use
19/// [`ButtonLike`] directly.
20///
21/// # Examples
22///
23/// **A button with a label**, is typically used in scenarios such as a form, where the button's label
24/// indicates what action will be performed when the button is clicked.
25///
26/// ```
27/// use ui::prelude::*;
28///
29/// Button::new("button_id", "Click me!")
30/// .on_click(|event, window, cx| {
31/// // Handle click event
32/// });
33/// ```
34///
35/// **A toggleable button**, is typically used in scenarios such as a toolbar,
36/// where the button's state indicates whether a feature is enabled or not, or
37/// a trigger for a popover menu, where clicking the button toggles the visibility of the menu.
38///
39/// ```
40/// use ui::prelude::*;
41///
42/// Button::new("button_id", "Click me!")
43/// .start_icon(Icon::new(IconName::Check))
44/// .toggle_state(true)
45/// .on_click(|event, window, cx| {
46/// // Handle click event
47/// });
48/// ```
49///
50/// To change the style of the button when it is selected use the [`selected_style`][Button::selected_style] method.
51///
52/// ```
53/// use ui::prelude::*;
54/// use ui::TintColor;
55///
56/// Button::new("button_id", "Click me!")
57/// .toggle_state(true)
58/// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
59/// .on_click(|event, window, cx| {
60/// // Handle click event
61/// });
62/// ```
63/// This will create a button with a blue tinted background when selected.
64///
65/// **A full-width button**, is typically used in scenarios such as the bottom of a modal or form, where it occupies the entire width of its container.
66/// The button's content, including text and icons, is centered by default.
67///
68/// ```
69/// use ui::prelude::*;
70///
71/// let button = Button::new("button_id", "Click me!")
72/// .full_width()
73/// .on_click(|event, window, cx| {
74/// // Handle click event
75/// });
76/// ```
77///
78#[derive(IntoElement, Documented, RegisterComponent)]
79pub struct Button {
80 base: ButtonLike,
81 label: SharedString,
82 label_color: Option<Color>,
83 label_size: Option<LabelSize>,
84 selected_label: Option<SharedString>,
85 selected_label_color: Option<Color>,
86 start_icon: Option<Icon>,
87 end_icon: Option<Icon>,
88 key_binding: Option<KeyBinding>,
89 key_binding_position: KeybindingPosition,
90 alpha: Option<f32>,
91 truncate: bool,
92 loading: bool,
93}
94
95impl Button {
96 /// Creates a new [`Button`] with a specified identifier and label.
97 ///
98 /// This is the primary constructor for a [`Button`] component. It initializes
99 /// the button with the provided identifier and label text, setting all other
100 /// properties to their default values, which can be customized using the
101 /// builder pattern methods provided by this struct.
102 pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
103 Self {
104 base: ButtonLike::new(id),
105 label: label.into(),
106 label_color: None,
107 label_size: None,
108 selected_label: None,
109 selected_label_color: None,
110 start_icon: None,
111 end_icon: None,
112 key_binding: None,
113 key_binding_position: KeybindingPosition::default(),
114 alpha: None,
115 truncate: false,
116 loading: false,
117 }
118 }
119
120 /// Sets the color of the button's label.
121 pub fn color(mut self, label_color: impl Into<Option<Color>>) -> Self {
122 self.label_color = label_color.into();
123 self
124 }
125
126 /// Sets the label announced by assistive technology.
127 /// Defaults to the button's visible label.
128 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
129 self.base = self.base.aria_label(label);
130 self
131 }
132
133 /// Sets the supplementary description announced by assistive technology
134 /// after the button's name, role, and value.
135 pub fn aria_description(mut self, description: impl Into<SharedString>) -> Self {
136 self.base = self.base.aria_description(description);
137 self
138 }
139
140 /// Sets the current value reported to assistive technology. Use this when
141 /// the button represents a control with a value, such as a combobox
142 /// trigger whose value is the current selection.
143 pub fn aria_value(mut self, value: impl Into<SharedString>) -> Self {
144 self.base = self.base.aria_value(value);
145 self
146 }
147
148 /// Overrides the role reported to assistive technology.
149 /// Defaults to [`gpui::Role::Button`].
150 pub fn aria_role(mut self, role: gpui::Role) -> Self {
151 self.base = self.base.aria_role(role);
152 self
153 }
154
155 /// Sets the expanded state reported to assistive technology, for buttons
156 /// that control a popup (e.g. dropdown or disclosure triggers).
157 pub fn aria_expanded(mut self, expanded: bool) -> Self {
158 self.base = self.base.aria_expanded(expanded);
159 self
160 }
161
162 /// Registers a handler for an accessibility action (e.g.
163 /// [`gpui::accesskit::Action::Expand`]) dispatched by assistive technology.
164 pub fn on_a11y_action(
165 mut self,
166 action: gpui::accesskit::Action,
167 listener: impl FnMut(Option<&gpui::accesskit::ActionData>, &mut Window, &mut App) + 'static,
168 ) -> Self {
169 self.base = self.base.on_a11y_action(action, listener);
170 self
171 }
172
173 /// Defines the size of the button's label.
174 pub fn label_size(mut self, label_size: impl Into<Option<LabelSize>>) -> Self {
175 self.label_size = label_size.into();
176 self
177 }
178
179 /// Sets the label used when the button is in a selected state.
180 pub fn selected_label<L: Into<SharedString>>(mut self, label: impl Into<Option<L>>) -> Self {
181 self.selected_label = label.into().map(Into::into);
182 self
183 }
184
185 /// Sets the label color used when the button is in a selected state.
186 pub fn selected_label_color(mut self, color: impl Into<Option<Color>>) -> Self {
187 self.selected_label_color = color.into();
188 self
189 }
190
191 /// Sets an icon to display at the start (left) of the button label.
192 ///
193 /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
194 pub fn start_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
195 self.start_icon = icon.into();
196 self
197 }
198
199 /// Sets an icon to display at the end (right) of the button label.
200 ///
201 /// The icon's color will be overridden to `Color::Disabled` when the button is disabled.
202 pub fn end_icon(mut self, icon: impl Into<Option<Icon>>) -> Self {
203 self.end_icon = icon.into();
204 self
205 }
206
207 /// Display the keybinding that triggers the button action.
208 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
209 self.key_binding = key_binding.into();
210 self
211 }
212
213 /// Sets the position of the keybinding relative to the button label.
214 ///
215 /// This method allows you to specify where the keybinding should be displayed
216 /// in relation to the button's label.
217 pub fn key_binding_position(mut self, position: KeybindingPosition) -> Self {
218 self.key_binding_position = position;
219 self
220 }
221
222 /// Sets the alpha property of the color of label.
223 pub fn alpha(mut self, alpha: f32) -> Self {
224 self.alpha = Some(alpha);
225 self
226 }
227
228 /// Truncates overflowing labels with an ellipsis (`…`) if needed.
229 ///
230 /// Buttons with static labels should _never_ be truncated, ensure
231 /// this is only used when the label is dynamic and may overflow.
232 pub fn truncate(mut self, truncate: bool) -> Self {
233 self.truncate = truncate;
234 self
235 }
236
237 /// Displays a rotating loading spinner in place of the `start_icon`.
238 ///
239 /// When `loading` is `true`, any `start_icon` is ignored. and a rotating
240 /// loading spinner is shown instead.
241 pub fn loading(mut self, loading: bool) -> Self {
242 self.loading = loading;
243 self
244 }
245}
246
247impl Toggleable for Button {
248 /// Sets the selected state of the button.
249 ///
250 /// # Examples
251 ///
252 /// Create a toggleable button that changes appearance when selected:
253 ///
254 /// ```
255 /// use ui::prelude::*;
256 /// use ui::TintColor;
257 ///
258 /// let selected = true;
259 ///
260 /// Button::new("toggle_button", "Toggle Me")
261 /// .start_icon(Icon::new(IconName::Check))
262 /// .toggle_state(selected)
263 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent))
264 /// .on_click(|event, window, cx| {
265 /// // Toggle the selected state
266 /// });
267 /// ```
268 fn toggle_state(mut self, selected: bool) -> Self {
269 self.base = self.base.toggle_state(selected);
270 self
271 }
272}
273
274impl SelectableButton for Button {
275 /// Sets the style for the button in a selected state.
276 ///
277 /// # Examples
278 ///
279 /// Customize the selected appearance of a button:
280 ///
281 /// ```
282 /// use ui::prelude::*;
283 /// use ui::TintColor;
284 ///
285 /// Button::new("styled_button", "Styled Button")
286 /// .toggle_state(true)
287 /// .selected_style(ButtonStyle::Tinted(TintColor::Accent));
288 /// ```
289 fn selected_style(mut self, style: ButtonStyle) -> Self {
290 self.base = self.base.selected_style(style);
291 self
292 }
293}
294
295impl Disableable for Button {
296 /// Disables the button, preventing interaction and changing its appearance.
297 ///
298 /// When disabled, the button's icon and label will use `Color::Disabled`.
299 ///
300 /// # Examples
301 ///
302 /// Create a disabled button:
303 ///
304 /// ```
305 /// use ui::prelude::*;
306 ///
307 /// Button::new("disabled_button", "Can't Click Me")
308 /// .disabled(true);
309 /// ```
310 fn disabled(mut self, disabled: bool) -> Self {
311 self.base = self.base.disabled(disabled);
312 self
313 }
314}
315
316impl Clickable for Button {
317 fn on_click(
318 mut self,
319 handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
320 ) -> Self {
321 self.base = self.base.on_click(handler);
322 self
323 }
324
325 fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
326 self.base = self.base.cursor_style(cursor_style);
327 self
328 }
329}
330
331impl FixedWidth for Button {
332 /// Sets a fixed width for the button.
333 ///
334 /// # Examples
335 ///
336 /// Create a button with a fixed width of 100 pixels:
337 ///
338 /// ```
339 /// use ui::prelude::*;
340 ///
341 /// Button::new("fixed_width_button", "Fixed Width")
342 /// .width(px(100.0));
343 /// ```
344 fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
345 self.base = self.base.width(width);
346 self
347 }
348
349 /// Makes the button take up the full width of its container.
350 ///
351 /// # Examples
352 ///
353 /// Create a button that takes up the full width of its container:
354 ///
355 /// ```
356 /// use ui::prelude::*;
357 ///
358 /// Button::new("full_width_button", "Full Width")
359 /// .full_width();
360 /// ```
361 fn full_width(mut self) -> Self {
362 self.base = self.base.full_width();
363 self
364 }
365}
366
367impl ButtonCommon for Button {
368 fn id(&self) -> &ElementId {
369 self.base.id()
370 }
371
372 /// Sets the visual style of the button.
373 fn style(mut self, style: ButtonStyle) -> Self {
374 self.base = self.base.style(style);
375 self
376 }
377
378 /// Sets the size of the button.
379 fn size(mut self, size: ButtonSize) -> Self {
380 self.base = self.base.size(size);
381 self
382 }
383
384 /// Sets a tooltip that appears on hover.
385 ///
386 /// # Examples
387 ///
388 /// Add a tooltip to a button:
389 ///
390 /// ```
391 /// use ui::{Tooltip, prelude::*};
392 ///
393 /// Button::new("tooltip_button", "Hover Me")
394 /// .tooltip(Tooltip::text("This is a tooltip"));
395 /// ```
396 fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
397 self.base = self.base.tooltip(tooltip);
398 self
399 }
400
401 fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
402 self.base = self.base.tab_index(tab_index);
403 self
404 }
405
406 fn layer(mut self, elevation: ElevationIndex) -> Self {
407 self.base = self.base.layer(elevation);
408 self
409 }
410
411 fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
412 self.base = self.base.track_focus(focus_handle);
413 self
414 }
415}
416
417impl RenderOnce for Button {
418 #[allow(refining_impl_trait)]
419 fn render(mut self, window: &mut Window, cx: &mut App) -> ButtonLike {
420 let is_disabled = self.base.disabled;
421 let is_selected = self.base.selected;
422
423 let label = self
424 .selected_label
425 .filter(|_| is_selected)
426 .unwrap_or(self.label);
427
428 if self.base.aria_label.is_none() {
429 self.base.aria_label = Some(label.clone());
430 }
431
432 // Announce the displayed keybinding to assistive technology via
433 // `aria-keyshortcuts` so screen-reader users hear the same shortcut
434 // sighted users see next to the button.
435 if self.base.aria_keyshortcuts.is_none()
436 && let Some(keyshortcuts) = self
437 .key_binding
438 .as_ref()
439 .and_then(|key_binding| key_binding.keyboard_shortcut_text(window, cx))
440 {
441 self.base.aria_keyshortcuts = Some(keyshortcuts);
442 }
443
444 // A combobox's value is its visible text, so expose it as the
445 // accessible value by default. This lets combobox triggers announce
446 // their current selection without each caller wiring it up.
447 if matches!(self.base.aria_role, Some(gpui::Role::ComboBox))
448 && self.base.aria_value.is_none()
449 {
450 self.base.aria_value = Some(label.clone());
451 }
452
453 let label_color = if is_disabled {
454 Color::Disabled
455 } else if is_selected {
456 self.selected_label_color.unwrap_or(Color::Selected)
457 } else {
458 self.label_color.unwrap_or_default()
459 };
460 let loading_icon_id = (self.base.id().clone(), "loading");
461
462 self.base.child(
463 h_flex()
464 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
465 .gap(DynamicSpacing::Base04.rems(cx))
466 .when_else(
467 self.loading,
468 |this| {
469 this.child(
470 Icon::new(IconName::LoadCircle)
471 .size(IconSize::Small)
472 .color(Color::Muted)
473 .with_keyed_rotate_animation(loading_icon_id, 2),
474 )
475 },
476 |this| {
477 this.when_some(self.start_icon, |this, icon| {
478 this.child(if is_disabled {
479 icon.color(Color::Disabled)
480 } else {
481 icon
482 })
483 })
484 },
485 )
486 .child(
487 h_flex()
488 .when(self.truncate, |this| this.min_w_0().overflow_hidden())
489 .when(
490 self.key_binding_position == KeybindingPosition::Start,
491 |this| this.flex_row_reverse(),
492 )
493 .gap(DynamicSpacing::Base06.rems(cx))
494 .justify_between()
495 .child(
496 Label::new(label)
497 .color(label_color)
498 .size(self.label_size.unwrap_or_default())
499 .when_some(self.alpha, |this, alpha| this.alpha(alpha))
500 .when(self.truncate, |this| this.truncate()),
501 )
502 .children(self.key_binding),
503 )
504 .when_some(self.end_icon, |this, icon| {
505 this.child(if is_disabled {
506 icon.color(Color::Disabled)
507 } else {
508 icon
509 })
510 }),
511 )
512 }
513}
514
515impl Component for Button {
516 fn scope() -> ComponentScope {
517 ComponentScope::Input
518 }
519
520 fn sort_name() -> &'static str {
521 "ButtonA"
522 }
523
524 fn description() -> &'static str {
525 "A button triggers an event or action."
526 }
527
528 fn preview(_window: &mut Window, _cx: &mut App) -> AnyElement {
529 v_flex()
530 .gap_6()
531 .children(vec![
532 example_group_with_title(
533 "Button Styles",
534 vec![
535 single_example(
536 "Default",
537 Button::new("default", "Default").into_any_element(),
538 ),
539 single_example(
540 "Filled",
541 Button::new("filled", "Filled")
542 .style(ButtonStyle::Filled)
543 .into_any_element(),
544 ),
545 single_example(
546 "Subtle",
547 Button::new("outline", "Subtle")
548 .style(ButtonStyle::Subtle)
549 .into_any_element(),
550 ),
551 single_example(
552 "Tinted",
553 Button::new("tinted_accent_style", "Accent")
554 .style(ButtonStyle::Tinted(TintColor::Accent))
555 .into_any_element(),
556 ),
557 single_example(
558 "Transparent",
559 Button::new("transparent", "Transparent")
560 .style(ButtonStyle::Transparent)
561 .into_any_element(),
562 ),
563 ],
564 ),
565 example_group_with_title(
566 "Tint Styles",
567 vec![
568 single_example(
569 "Accent",
570 Button::new("tinted_accent", "Accent")
571 .style(ButtonStyle::Tinted(TintColor::Accent))
572 .into_any_element(),
573 ),
574 single_example(
575 "Error",
576 Button::new("tinted_negative", "Error")
577 .style(ButtonStyle::Tinted(TintColor::Error))
578 .into_any_element(),
579 ),
580 single_example(
581 "Warning",
582 Button::new("tinted_warning", "Warning")
583 .style(ButtonStyle::Tinted(TintColor::Warning))
584 .into_any_element(),
585 ),
586 single_example(
587 "Success",
588 Button::new("tinted_positive", "Success")
589 .style(ButtonStyle::Tinted(TintColor::Success))
590 .into_any_element(),
591 ),
592 ],
593 ),
594 example_group_with_title(
595 "Special States",
596 vec![
597 single_example(
598 "Default",
599 Button::new("default_state", "Default").into_any_element(),
600 ),
601 single_example(
602 "Disabled",
603 Button::new("disabled", "Disabled")
604 .disabled(true)
605 .into_any_element(),
606 ),
607 single_example(
608 "Selected",
609 Button::new("selected", "Selected")
610 .toggle_state(true)
611 .into_any_element(),
612 ),
613 ],
614 ),
615 example_group_with_title(
616 "Buttons with Icons",
617 vec![
618 single_example(
619 "Start Icon",
620 Button::new("icon_start", "Start Icon")
621 .start_icon(Icon::new(IconName::Check))
622 .into_any_element(),
623 ),
624 single_example(
625 "End Icon",
626 Button::new("icon_end", "End Icon")
627 .end_icon(Icon::new(IconName::Check))
628 .into_any_element(),
629 ),
630 single_example(
631 "Both Icons",
632 Button::new("both_icons", "Both Icons")
633 .start_icon(Icon::new(IconName::Check))
634 .end_icon(Icon::new(IconName::ChevronDown))
635 .into_any_element(),
636 ),
637 single_example(
638 "Icon Color",
639 Button::new("icon_color", "Icon Color")
640 .start_icon(Icon::new(IconName::Check).color(Color::Accent))
641 .into_any_element(),
642 ),
643 ],
644 ),
645 ])
646 .into_any_element()
647 }
648}
649