284 lines
11 KiB
Rust
284 lines
11 KiB
Rust
use gpui::{div, prelude::*, Div, Element, ElementId, IntoElement, Styled, WindowContext};
|
|
|
|
use crate::prelude::*;
|
|
use crate::{Color, Icon, IconElement, Selection};
|
|
|
|
pub type CheckHandler = Box<dyn Fn(&Selection, &mut WindowContext) + 'static>;
|
|
|
|
/// # Checkbox
|
|
///
|
|
/// Checkboxes are used for multiple choices, not for mutually exclusive choices.
|
|
/// Each checkbox works independently from other checkboxes in the list,
|
|
/// therefore checking an additional box does not affect any other selections.
|
|
#[derive(IntoElement)]
|
|
pub struct Checkbox {
|
|
id: ElementId,
|
|
checked: Selection,
|
|
disabled: bool,
|
|
on_click: Option<CheckHandler>,
|
|
}
|
|
|
|
impl RenderOnce for Checkbox {
|
|
type Output = gpui::Stateful<Div>;
|
|
|
|
fn render(self, cx: &mut WindowContext) -> Self::Output {
|
|
let group_id = format!("checkbox_group_{:?}", self.id);
|
|
|
|
let icon = match self.checked {
|
|
// When selected, we show a checkmark.
|
|
Selection::Selected => {
|
|
Some(
|
|
IconElement::new(Icon::Check)
|
|
.size(crate::IconSize::Small)
|
|
.color(
|
|
// If the checkbox is disabled we change the color of the icon.
|
|
if self.disabled {
|
|
Color::Disabled
|
|
} else {
|
|
Color::Selected
|
|
},
|
|
),
|
|
)
|
|
}
|
|
// In an indeterminate state, we show a dash.
|
|
Selection::Indeterminate => {
|
|
Some(
|
|
IconElement::new(Icon::Dash)
|
|
.size(crate::IconSize::Small)
|
|
.color(
|
|
// If the checkbox is disabled we change the color of the icon.
|
|
if self.disabled {
|
|
Color::Disabled
|
|
} else {
|
|
Color::Selected
|
|
},
|
|
),
|
|
)
|
|
}
|
|
// When unselected, we show nothing.
|
|
Selection::Unselected => None,
|
|
};
|
|
|
|
// A checkbox could be in an indeterminate state,
|
|
// for example the indeterminate state could represent:
|
|
// - a group of options of which only some are selected
|
|
// - an enabled option that is no longer available
|
|
// - a previously agreed to license that has been updated
|
|
//
|
|
// For the sake of styles we treat the indeterminate state as selected,
|
|
// but it's icon will be different.
|
|
let selected =
|
|
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
|
|
|
// We could use something like this to make the checkbox background when selected:
|
|
//
|
|
// ~~~rust
|
|
// ...
|
|
// .when(selected, |this| {
|
|
// this.bg(cx.theme().colors().element_selected)
|
|
// })
|
|
// ~~~
|
|
//
|
|
// But we use a match instead here because the checkbox might be disabled,
|
|
// and it could be disabled _while_ it is selected, as well as while it is not selected.
|
|
let (bg_color, border_color) = match (self.disabled, selected) {
|
|
(true, _) => (
|
|
cx.theme().colors().ghost_element_disabled,
|
|
cx.theme().colors().border_disabled,
|
|
),
|
|
(false, true) => (
|
|
cx.theme().colors().element_selected,
|
|
cx.theme().colors().border,
|
|
),
|
|
(false, false) => (
|
|
cx.theme().colors().element_background,
|
|
cx.theme().colors().border,
|
|
),
|
|
};
|
|
|
|
div()
|
|
.id(self.id)
|
|
// Rather than adding `px_1()` to add some space around the checkbox,
|
|
// we use a larger parent element to create a slightly larger
|
|
// click area for the checkbox.
|
|
.size_5()
|
|
// Because we've enlarged the click area, we need to create a
|
|
// `group` to pass down interactivity events to the checkbox.
|
|
.group(group_id.clone())
|
|
.child(
|
|
div()
|
|
.flex()
|
|
// This prevent the flex element from growing
|
|
// or shrinking in response to any size changes
|
|
.flex_none()
|
|
// The combo of `justify_center()` and `items_center()`
|
|
// is used frequently to center elements in a flex container.
|
|
//
|
|
// We use this to center the icon in the checkbox.
|
|
.justify_center()
|
|
.items_center()
|
|
.m_1()
|
|
.size_4()
|
|
.rounded_sm()
|
|
.bg(bg_color)
|
|
.border()
|
|
.border_color(border_color)
|
|
// We only want the interactivity states to fire when we
|
|
// are in a checkbox that isn't disabled.
|
|
.when(!self.disabled, |this| {
|
|
// Here instead of `hover()` we use `group_hover()`
|
|
// to pass it the group id.
|
|
this.group_hover(group_id.clone(), |el| {
|
|
el.bg(cx.theme().colors().element_hover)
|
|
})
|
|
})
|
|
.children(icon),
|
|
)
|
|
.when_some(
|
|
self.on_click.filter(|_| !self.disabled),
|
|
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
|
)
|
|
}
|
|
}
|
|
impl Checkbox {
|
|
pub fn new(id: impl Into<ElementId>, checked: Selection) -> Self {
|
|
Self {
|
|
id: id.into(),
|
|
checked,
|
|
disabled: false,
|
|
on_click: None,
|
|
}
|
|
}
|
|
|
|
pub fn disabled(mut self, disabled: bool) -> Self {
|
|
self.disabled = disabled;
|
|
self
|
|
}
|
|
|
|
pub fn on_click(
|
|
mut self,
|
|
handler: impl 'static + Fn(&Selection, &mut WindowContext) + Send + Sync,
|
|
) -> Self {
|
|
self.on_click = Some(Box::new(handler));
|
|
self
|
|
}
|
|
|
|
pub fn render(self, cx: &mut WindowContext) -> impl Element {
|
|
let group_id = format!("checkbox_group_{:?}", self.id);
|
|
|
|
let icon = match self.checked {
|
|
// When selected, we show a checkmark.
|
|
Selection::Selected => {
|
|
Some(
|
|
IconElement::new(Icon::Check)
|
|
.size(crate::IconSize::Small)
|
|
.color(
|
|
// If the checkbox is disabled we change the color of the icon.
|
|
if self.disabled {
|
|
Color::Disabled
|
|
} else {
|
|
Color::Selected
|
|
},
|
|
),
|
|
)
|
|
}
|
|
// In an indeterminate state, we show a dash.
|
|
Selection::Indeterminate => {
|
|
Some(
|
|
IconElement::new(Icon::Dash)
|
|
.size(crate::IconSize::Small)
|
|
.color(
|
|
// If the checkbox is disabled we change the color of the icon.
|
|
if self.disabled {
|
|
Color::Disabled
|
|
} else {
|
|
Color::Selected
|
|
},
|
|
),
|
|
)
|
|
}
|
|
// When unselected, we show nothing.
|
|
Selection::Unselected => None,
|
|
};
|
|
|
|
// A checkbox could be in an indeterminate state,
|
|
// for example the indeterminate state could represent:
|
|
// - a group of options of which only some are selected
|
|
// - an enabled option that is no longer available
|
|
// - a previously agreed to license that has been updated
|
|
//
|
|
// For the sake of styles we treat the indeterminate state as selected,
|
|
// but it's icon will be different.
|
|
let selected =
|
|
self.checked == Selection::Selected || self.checked == Selection::Indeterminate;
|
|
|
|
// We could use something like this to make the checkbox background when selected:
|
|
//
|
|
// ~~~rust
|
|
// ...
|
|
// .when(selected, |this| {
|
|
// this.bg(cx.theme().colors().element_selected)
|
|
// })
|
|
// ~~~
|
|
//
|
|
// But we use a match instead here because the checkbox might be disabled,
|
|
// and it could be disabled _while_ it is selected, as well as while it is not selected.
|
|
let (bg_color, border_color) = match (self.disabled, selected) {
|
|
(true, _) => (
|
|
cx.theme().colors().ghost_element_disabled,
|
|
cx.theme().colors().border_disabled,
|
|
),
|
|
(false, true) => (
|
|
cx.theme().colors().element_selected,
|
|
cx.theme().colors().border,
|
|
),
|
|
(false, false) => (
|
|
cx.theme().colors().element_background,
|
|
cx.theme().colors().border,
|
|
),
|
|
};
|
|
|
|
div()
|
|
.id(self.id)
|
|
// Rather than adding `px_1()` to add some space around the checkbox,
|
|
// we use a larger parent element to create a slightly larger
|
|
// click area for the checkbox.
|
|
.size_5()
|
|
// Because we've enlarged the click area, we need to create a
|
|
// `group` to pass down interactivity events to the checkbox.
|
|
.group(group_id.clone())
|
|
.child(
|
|
div()
|
|
.flex()
|
|
// This prevent the flex element from growing
|
|
// or shrinking in response to any size changes
|
|
.flex_none()
|
|
// The combo of `justify_center()` and `items_center()`
|
|
// is used frequently to center elements in a flex container.
|
|
//
|
|
// We use this to center the icon in the checkbox.
|
|
.justify_center()
|
|
.items_center()
|
|
.m_1()
|
|
.size_4()
|
|
.rounded_sm()
|
|
.bg(bg_color)
|
|
.border()
|
|
.border_color(border_color)
|
|
// We only want the interactivity states to fire when we
|
|
// are in a checkbox that isn't disabled.
|
|
.when(!self.disabled, |this| {
|
|
// Here instead of `hover()` we use `group_hover()`
|
|
// to pass it the group id.
|
|
this.group_hover(group_id.clone(), |el| {
|
|
el.bg(cx.theme().colors().element_hover)
|
|
})
|
|
})
|
|
.children(icon),
|
|
)
|
|
.when_some(
|
|
self.on_click.filter(|_| !self.disabled),
|
|
|this, on_click| this.on_click(move |_, cx| on_click(&self.checked.inverse(), cx)),
|
|
)
|
|
}
|
|
}
|