use std::sync::Arc;
use crate::elements::checkbox::checkbox_check_icon;
use crate::prelude::FluentBuilder as _;
use crate::{
ActiveTheme, AnyElement, App, Axis, ComponentText, Disableable, ElementId, ElementSize,
FocusableExt as _, InteractiveElement, IntoElement, ParentElement, RenderOnce, Selectable,
SharedString, Sizable, StatefulInteractiveElement, StyleRefinement, Styled, StyledExt as _,
Window, div, h_flex, px, relative, rems, v_flex,
};
#[derive(IntoElement)]
pub struct Radio {
base: crate::Div,
style: StyleRefinement,
id: ElementId,
label: Option<ComponentText>,
children: Vec<AnyElement>,
checked: bool,
disabled: bool,
tab_stop: bool,
tab_index: isize,
size: ElementSize,
on_change: Option<Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync + 'static>>,
tooltip: Option<SharedString>,
}
impl Radio {
pub fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
base: div(),
style: StyleRefinement::default(),
label: None,
children: Vec::new(),
checked: false,
disabled: false,
tab_index: 0,
tab_stop: true,
size: ElementSize::default(),
on_change: None,
tooltip: None,
}
}
pub fn tooltip(mut self, tooltip: impl Into<SharedString>) -> Self {
self.tooltip = Some(tooltip.into());
self
}
pub fn label(mut self, label: impl Into<ComponentText>) -> 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 tab_index(mut self, tab_index: isize) -> Self {
self.tab_index = tab_index;
self
}
pub fn tab_stop(mut self, tab_stop: bool) -> Self {
self.tab_stop = tab_stop;
self
}
pub fn on_change(
mut self,
handler: impl Fn(bool, &mut Window, &mut App) + Send + Sync + 'static,
) -> Self {
self.on_change = Some(Arc::new(handler));
self
}
fn handle_change(
on_change: &Option<Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync + 'static>>,
checked: bool,
window: &mut Window,
cx: &mut App,
) {
let new_checked = !checked;
if let Some(f) = on_change {
(f)(new_checked, window, cx);
}
}
}
impl Sizable for Radio {
fn with_size(mut self, size: impl Into<ElementSize>) -> Self {
self.size = size.into();
self
}
}
impl Styled for Radio {
fn style(&mut self) -> &mut crate::StyleRefinement {
&mut self.style
}
}
impl InteractiveElement for Radio {
fn interactivity(&mut self) -> &mut crate::Interactivity {
self.base.interactivity()
}
}
impl StatefulInteractiveElement for Radio {}
impl ParentElement for Radio {
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
self.children.extend(elements);
}
}
impl Disableable for Radio {
fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl Selectable for Radio {
fn selected(self, selected: bool) -> Self {
self.checked(selected)
}
fn is_selected(&self) -> bool {
self.checked
}
}
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 {
Self::new(SharedString::from(label.clone())).label(SharedString::from(label))
}
}
impl RenderOnce for Radio {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let checked = self.checked;
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 disabled = self.disabled;
let (border_color, bg) = if checked {
(cx.theme().primary, cx.theme().primary)
} else {
(cx.theme().input, cx.theme().input.opacity(0.5))
};
let (border_color, bg) = if disabled {
(border_color.opacity(0.5), bg.opacity(0.5))
} else {
(border_color, bg)
};
div().child(
self.base
.id(self.id.clone())
.when(!self.disabled, |this| {
this.track_focus(
&focus_handle
.tab_stop(self.tab_stop)
.tab_index(self.tab_index),
)
})
.h_flex()
.gap_x_2()
.text_color(cx.theme().foreground)
.items_start()
.line_height(relative(1.))
.rounded(cx.theme().radius * 0.5)
.focus_ring(is_focused, px(2.), window, cx)
.map(|this| match self.size {
ElementSize::XSmall => this.text_xs(),
ElementSize::Small => this.text_sm(),
ElementSize::Medium => this.text_base(),
ElementSize::Large => this.text_lg(),
_ => this,
})
.refine_style(&self.style)
.child(
div()
.relative()
.map(|this| match self.size {
ElementSize::XSmall => this.size_3(),
ElementSize::Small => this.size_3p5(),
ElementSize::Medium => this.size_4(),
ElementSize::Large => this.size(rems(1.125)),
_ => this.size_4(),
})
.flex_shrink_0()
.rounded_full()
.border_1()
.border_color(border_color)
.when(cx.theme().shadow && !disabled, |this| this.shadow_xs())
.map(|this| match self.checked {
false => this.bg(cx.theme().input_background()),
true if disabled => this.bg(bg),
true => this.bg(cx.theme().tokens.primary),
})
.child(checkbox_check_icon(
self.id, self.size, checked, disabled, window, cx,
)),
)
.when(!self.children.is_empty() || self.label.is_some(), |this| {
this.child(
v_flex()
.w_full()
.line_height(relative(1.2))
.gap_1()
.when_some(self.label, |this, label| {
this.child(
div()
.size_full()
.line_height(relative(1.))
.when(self.disabled, |this| {
this.text_color(cx.theme().muted_foreground)
})
.child(label),
)
})
.children(self.children),
)
})
.on_mouse_down(crate::MouseButton::Left, |_, window, _| {
window.prevent_default();
})
.when(!self.disabled, |this| {
this.on_click({
let on_change = self.on_change.clone();
move |_, window, cx| {
window.prevent_default();
Self::handle_change(&on_change, checked, window, cx);
}
})
}),
)
}
}
#[derive(IntoElement)]
pub struct RadioGroup {
id: ElementId,
style: StyleRefinement,
radios: Vec<Radio>,
layout: Axis,
selected_index: Option<usize>,
disabled: bool,
on_change: Option<Arc<dyn Fn(usize, &mut Window, &mut App) + Send + Sync + 'static>>,
}
impl RadioGroup {
fn new(id: impl Into<ElementId>) -> Self {
Self {
id: id.into(),
style: StyleRefinement::default().flex_1(),
on_change: None,
layout: Axis::Vertical,
selected_index: None,
disabled: false,
radios: vec![],
}
}
pub fn vertical(id: impl Into<ElementId>) -> Self {
Self::new(id)
}
pub fn horizontal(id: impl Into<ElementId>) -> Self {
Self::new(id).layout(Axis::Horizontal)
}
pub fn layout(mut self, layout: Axis) -> Self {
self.layout = layout;
self
}
pub fn on_change(
mut self,
handler: impl Fn(usize, &mut Window, &mut App) + Send + Sync + 'static,
) -> Self {
self.on_change = Some(Arc::new(handler));
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 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 Styled for RadioGroup {
fn style(&mut self) -> &mut StyleRefinement {
&mut self.style
}
}
impl RenderOnce for RadioGroup {
fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
let on_change = self.on_change;
let disabled = self.disabled;
let selected_ix = self.selected_index;
let base = if self.layout == Axis::Vertical {
v_flex()
} else {
h_flex().w_full().flex_wrap()
};
let mut container = div().id(self.id);
*container.style() = self.style;
container.child(
base.gap_3()
.children(self.radios.into_iter().enumerate().map(|(ix, mut radio)| {
let checked = selected_ix == Some(ix);
radio.id = ix.into();
radio.disabled(disabled).checked(checked).when_some(
on_change.clone(),
|this, on_change| {
this.on_change(move |_, window, cx| {
on_change(ix, window, cx);
})
},
)
})),
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_radio_build() {
let r = Radio::new("test-radio")
.label("选项 A")
.checked(true)
.with_size(ElementSize::Small);
assert!(r.checked);
assert!(r.label.is_some());
}
#[test]
fn test_radio_from_str() {
let r: Radio = "选项 B".into();
assert_eq!(r.id, ElementId::from("选项 B"));
assert!(r.label.is_some());
}
#[test]
fn test_radio_group_build() {
let group = RadioGroup::horizontal("test-group")
.selected_index(Some(1))
.child(Radio::new("r1").label("A"))
.child(Radio::new("r2").label("B"));
assert_eq!(group.radios.len(), 2);
assert_eq!(group.selected_index, Some(1));
assert_eq!(group.layout, Axis::Horizontal);
}
}