use gpui::prelude::FluentBuilder as _; use gpui::{ App, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Toggled, Window, }; use gpui_native_theme::{ActiveTheme as _, Size}; use gpui_native_utils::focus_ring; use crate::common::{Sizable, h_flex}; /// The change handler, fired with the next pressed state. type ChangeHandler = Box; /// A toggle component. #[derive(IntoElement)] pub struct Toggle { id: ElementId, label: Option, pressed: bool, size: Size, disabled: bool, a11y_label: Option, on_change: Option, } impl Toggle { /// Creates an unpressed toggle. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), label: None, pressed: false, size: Size::default(), disabled: false, a11y_label: None, on_change: None, } } /// Sets the visible label. pub fn label(mut self, label: L) -> Self where L: Into, { self.label = Some(label.into()); self } /// Sets the current pressed state; the toggle never owns it. pub fn pressed(mut self, pressed: bool) -> Self { self.pressed = pressed; self } /// Renders the toggle inert, unfocusable, and dimmed; disabled always wins. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Overrides the accessibility label, which defaults to the visible label. pub fn accessibility_label(mut self, label: L) -> Self where L: Into, { self.a11y_label = Some(label.into()); self } /// Sets the change handler, fired with the next pressed state. pub fn on_change(mut self, handler: F) -> Self where F: Fn(bool, &mut Window, &mut App) + 'static, { self.on_change = Some(Box::new(handler)); self } } impl Sizable for Toggle { fn apply_size(&mut self, size: Size) { self.size = size; } } impl RenderOnce for Toggle { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let ramps = theme.ramps; let metrics = theme.sizes.resolve(self.size); let on_change = self.on_change; let next = !self.pressed; let a11y_label = self.a11y_label.or_else(|| self.label.clone()); h_flex() .id(self.id) .justify_center() .h(metrics.control_height) .px(metrics.padding_x * 0.75) .rounded(theme.radius.control) .text_size(metrics.text_size) .text_color(colors.foreground) .role(Role::Button) .when_some(a11y_label, |toggle, label| toggle.aria_label(label)) .aria_toggled(Toggled::from(self.pressed)) .when(self.pressed, |toggle| { toggle .bg(ramps.secondary.hover) .hover(|style| style.bg(ramps.secondary.pressed)) }) .when(!self.pressed, |toggle| { toggle .hover(|style| style.bg(colors.muted)) .active(|style| style.bg(ramps.secondary.hover)) }) .when(self.disabled, |toggle| toggle.opacity(0.5)) .when(!self.disabled, |toggle| { toggle .cursor_pointer() .focusable() .tab_stop(true) .focus_visible(|style| style.shadow(focus_ring(colors.ring))) .when_some(on_change, |toggle, on_change| { toggle.on_click(move |_, window, cx| on_change(next, window, cx)) }) }) .when_some(self.label, |toggle, label| toggle.child(label)) } }