//! The checkbox: a controlled on/off value with an optional label. use gpui::prelude::FluentBuilder as _; use gpui::{ App, BoxShadow, ElementId, InteractiveElement as _, IntoElement, ParentElement as _, RenderOnce, Role, SharedString, StatefulInteractiveElement as _, Styled as _, Toggled, Window, px, }; use gpui_native_icons::{IconRole, icon}; use gpui_native_theme::ActiveTheme as _; use crate::common::h_flex; /// The change reporter, fired with the next value by pointer, Enter, and Space. type ChangeHandler = Box; /// Checkbox component. #[derive(IntoElement)] pub struct Checkbox { id: ElementId, label: Option, checked: bool, disabled: bool, a11y_label: Option, on_change: Option, } impl Checkbox { /// Creates an unchecked checkbox. pub fn new(id: I) -> Self where I: Into, { Self { id: id.into(), label: None, checked: false, disabled: false, a11y_label: None, on_change: None, } } /// Sets the visible label; clicking it toggles too. pub fn label(mut self, label: L) -> Self where L: Into, { self.label = Some(label.into()); self } /// Sets the current value; the checkbox never owns it. pub fn checked(mut self, checked: bool) -> Self { self.checked = checked; self } /// Renders the checkbox 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 } /// The change handler, fired with the next value by pointer, Enter, and Space. 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 RenderOnce for Checkbox { fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement { let theme = cx.theme(); let colors = theme.colors; let ramps = theme.ramps; let on_change = self.on_change; let next = !self.checked; let a11y_label = self.a11y_label.or_else(|| self.label.clone()); h_flex() .id(self.id) .role(Role::CheckBox) .gap_1p5() .text_sm() .when_some(a11y_label, |checkbox, label| checkbox.aria_label(label)) .aria_toggled(Toggled::from(self.checked)) .when(self.disabled, |checkbox| checkbox.opacity(0.5)) .when(!self.disabled, |checkbox| { checkbox .cursor_pointer() .focusable() .tab_stop(true) .focus_visible(|style| { // Paint-only keyboard ring; the width joins the native // metrics table when that lands. style.shadow(vec![ BoxShadow::new(px(0.), px(0.), colors.ring).spread_radius(px(3.)), ]) }) .when_some(on_change, |checkbox, on_change| { checkbox.on_click(move |_, window, cx| on_change(next, window, cx)) }) }) .child( h_flex() .justify_center() .flex_none() .size_4() .rounded(theme.radius.sm) .border_1() .when(self.checked, |indicator| { indicator .bg(ramps.primary.base) .border_color(ramps.primary.base) .hover(|style| style.bg(ramps.primary.hover)) .child(icon(IconRole::Check).color(colors.primary_foreground)) }) .when(!self.checked, |indicator| { indicator .bg(colors.input) .border_color(colors.border) .hover(|style| style.border_color(colors.muted_foreground)) }), ) .when_some(self.label, |checkbox, label| checkbox.child(label)) } }