use crate::theme::use_theme;
use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum RadioLayout {
#[default]
Vertical,
Horizontal,
}
#[derive(IntoElement)]
pub struct Radio {
base: Stateful<Div>,
id: ElementId,
label: Option<SharedString>,
checked: bool,
disabled: bool,
on_click: Option<Rc<dyn Fn(&mut Window, &mut App)>>,
style: StyleRefinement,
}
impl Radio {
pub fn new(id: impl Into<ElementId>) -> Self {
let id = id.into();
Self {
id: id.clone(),
base: div().id(id),
label: None,
checked: false,
disabled: false,
on_click: None,
style: StyleRefinement::default(),
}
}
pub fn label(mut self, label: impl Into<SharedString>) -> Self {
self.label = Some(label.into());
self
}
pub fn checked(mut self, checked: bool) -> Self {
self.checked = checked;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
self.on_click = Some(Rc::new(handler));
self
}
}
impl InteractiveElement for Radio {
fn interactivity(&mut self) -> &mut Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for Radio {}
impl Styled for Radio {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for Radio {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let theme = use_theme();
let user_style = self.style;
let focus_handle = window
.use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
.read(cx)
.clone();
let is_focused = focus_handle.is_focused(window);
let (border_color, bg, dot_opacity) = if self.checked {
(theme.tokens.primary, theme.tokens.primary, 1.0)
} else {
(theme.tokens.input, theme.tokens.background, 0.0)
};
let (border_color, bg) = if self.disabled {
(border_color.opacity(0.5), bg.opacity(0.5))
} else {
(border_color, bg)
};
let shadow_xs = BoxShadow {
offset: theme.tokens.shadow_xs.offset,
blur_radius: theme.tokens.shadow_xs.blur_radius,
spread_radius: theme.tokens.shadow_xs.spread_radius,
inset: false,
color: theme.tokens.shadow_xs.color,
};
let focus_ring = theme.tokens.focus_ring_light();
self.base
.when(!self.disabled, |this| {
this.track_focus(&focus_handle.tab_index(0).tab_stop(true))
})
.flex()
.gap(px(8.0))
.items_center()
.text_sm()
.font_family(theme.tokens.font_family.clone())
.text_color(if self.disabled {
theme.tokens.muted_foreground
} else {
theme.tokens.foreground
})
.when(is_focused && !self.disabled, |this| {
this.shadow(smallvec::smallvec![focus_ring])
})
.rounded(theme.tokens.radius_md)
.child(
div()
.relative()
.size(px(16.0))
.flex_shrink_0()
.rounded_full()
.border_1()
.border_color(border_color)
.bg(bg)
.when(!self.disabled, |this| {
this.shadow(smallvec::smallvec![shadow_xs])
})
.child(
div()
.absolute()
.top(px(3.0))
.left(px(3.0))
.size(px(8.0))
.rounded_full()
.bg(theme.tokens.primary_foreground)
.opacity(dot_opacity),
),
)
.when_some(self.label, |this, label| {
this.child(div().line_height(relative(1.0)).child(label))
})
.when(!self.disabled, |this| {
this.cursor(CursorStyle::PointingHand)
.on_mouse_down(MouseButton::Left, |_, window, _| {
window.prevent_default();
})
.when_some(self.on_click, |this, handler| {
this.on_click(move |_, window, cx| {
window.prevent_default();
cx.stop_propagation();
handler(window, cx);
})
})
})
.map(|this| {
let mut div = this;
div.style().refine(&user_style);
div
})
}
}
#[derive(IntoElement)]
pub struct RadioGroup {
id: ElementId,
radios: Vec<Radio>,
layout: RadioLayout,
selected_index: Option<usize>,
disabled: bool,
on_change: Option<Rc<dyn Fn(&usize, &mut Window, &mut App)>>,
}
impl RadioGroup {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
radios: Vec::new(),
layout: RadioLayout::default(),
selected_index: None,
disabled: false,
on_change: None,
}
}
pub fn vertical(id: impl Into<ElementId>) -> Self {
Self::new(id)
}
pub fn horizontal(id: impl Into<ElementId>) -> Self {
Self::new(id).layout(RadioLayout::Horizontal)
}
pub fn layout(mut self, layout: RadioLayout) -> Self {
self.layout = layout;
self
}
pub fn selected_index(mut self, index: Option<usize>) -> Self {
self.selected_index = index;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn on_change(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self {
self.on_change = Some(Rc::new(handler));
self
}
pub fn child(mut self, child: impl Into<Radio>) -> Self {
self.radios.push(child.into());
self
}
pub fn children(mut self, children: impl IntoIterator<Item = impl Into<Radio>>) -> Self {
self.radios.extend(children.into_iter().map(Into::into));
self
}
}
impl RenderOnce for RadioGroup {
fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let disabled = self.disabled;
let selected_ix = self.selected_index;
div()
.id(self.id)
.flex()
.when(self.layout == RadioLayout::Vertical, |this| this.flex_col())
.when(self.layout == RadioLayout::Horizontal, |this| {
this.flex_row().flex_wrap()
})
.gap(px(12.0))
.children(self.radios.into_iter().enumerate().map(|(ix, radio)| {
let checked = selected_ix == Some(ix);
radio.checked(checked).disabled(disabled).when_some(
on_change.clone(),
|this, on_change| {
this.on_click(move |window, cx| {
on_change(&ix, window, cx);
})
},
)
}))
}
}
impl From<&'static str> for Radio {
fn from(label: &'static str) -> Self {
Self::new(label).label(label)
}
}
impl From<SharedString> for Radio {
fn from(label: SharedString) -> Self {
Self::new(label.clone()).label(label)
}
}
impl From<String> for Radio {
fn from(label: String) -> Self {
let shared: SharedString = label.into();
Self::new(shared.clone()).label(shared)
}
}