use gpui::{
App, Component, IntoElement, MouseButton, RenderOnce, SharedString, Window, div, prelude::*, px,
};
use liora_core::Config;
use std::sync::Arc;
type ToggleCallback = dyn Fn(bool, &mut Window, &mut App) + 'static;
type ToggleGroupCallback = dyn Fn(SharedString, &mut Window, &mut App) + 'static;
pub struct Toggle {
label: SharedString,
selected: bool,
disabled: bool,
on_change: Option<Arc<ToggleCallback>>,
}
impl Toggle {
pub fn new(label: impl Into<SharedString>, selected: bool) -> Self {
Self {
label: label.into(),
selected,
disabled: false,
on_change: None,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_change(mut self, callback: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Arc::new(callback));
self
}
pub fn selected(&self) -> bool {
self.selected
}
}
impl IntoElement for Toggle {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
impl RenderOnce for Toggle {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
let selected = self.selected;
let disabled = self.disabled;
let callback = self.on_change.clone();
div()
.px_3()
.py_2()
.rounded(px(theme.radius.sm))
.border_1()
.border_color(if selected {
theme.primary.base
} else {
theme.neutral.border
})
.bg(if selected {
theme.primary.light_9
} else {
theme.neutral.card
})
.text_color(if selected {
theme.primary.base
} else {
theme.neutral.text_2
})
.text_sm()
.when(disabled, |s| s.opacity(0.55).cursor_not_allowed())
.when(!disabled, |s| {
s.cursor_pointer()
.hover(|s| s.bg(theme.neutral.hover))
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
if let Some(callback) = &callback {
callback(!selected, window, cx);
}
})
})
.child(self.label)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ToggleOption {
pub value: SharedString,
pub label: SharedString,
pub disabled: bool,
}
impl ToggleOption {
pub fn new(value: impl Into<SharedString>, label: impl Into<SharedString>) -> Self {
Self {
value: value.into(),
label: label.into(),
disabled: false,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
pub struct ToggleGroup {
options: Vec<ToggleOption>,
selected: Option<SharedString>,
on_change: Option<Arc<ToggleGroupCallback>>,
}
impl ToggleGroup {
pub fn new(options: impl IntoIterator<Item = ToggleOption>) -> Self {
Self {
options: options.into_iter().collect(),
selected: None,
on_change: None,
}
}
pub fn selected(mut self, value: impl Into<SharedString>) -> Self {
self.selected = Some(value.into());
self
}
pub fn on_change(
mut self,
callback: impl Fn(SharedString, &mut Window, &mut App) + 'static,
) -> Self {
self.on_change = Some(Arc::new(callback));
self
}
pub fn len(&self) -> usize {
self.options.len()
}
pub fn is_empty(&self) -> bool {
self.options.is_empty()
}
}
impl IntoElement for ToggleGroup {
type Element = Component<Self>;
fn into_element(self) -> Self::Element {
Component::new(self)
}
}
impl RenderOnce for ToggleGroup {
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = cx.global::<Config>().theme.clone();
div()
.flex()
.flex_row()
.rounded(px(theme.radius.sm))
.border_1()
.border_color(theme.neutral.border)
.overflow_hidden()
.children(self.options.into_iter().enumerate().map(|(index, option)| {
let selected = self.selected.as_ref() == Some(&option.value);
let disabled = option.disabled;
let value = option.value.clone();
let callback = self.on_change.clone();
div()
.px_3()
.py_2()
.text_sm()
.when(index > 0, |s| {
s.border_l_1().border_color(theme.neutral.border)
})
.bg(if selected {
theme.primary.light_9
} else {
theme.neutral.card
})
.text_color(if selected {
theme.primary.base
} else {
theme.neutral.text_2
})
.when(disabled, |s| s.opacity(0.55).cursor_not_allowed())
.when(!disabled, |s| {
s.cursor_pointer()
.hover(|s| s.bg(theme.neutral.hover))
.on_mouse_down(MouseButton::Left, move |_, window, cx| {
if let Some(callback) = &callback {
callback(value.clone(), window, cx);
}
})
})
.child(option.label)
.into_any_element()
}))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn toggle_group_tracks_options_and_selection() {
let group = ToggleGroup::new([
ToggleOption::new("left", "Left"),
ToggleOption::new("right", "Right"),
])
.selected("right");
assert_eq!(group.len(), 2);
assert_eq!(group.selected.as_ref().map(|v| v.as_ref()), Some("right"));
assert!(Toggle::new("Bold", true).selected());
}
}