use std::rc::Rc; use crate::{h_flex, text::Text, v_flex, ActiveTheme, AxisExt, IconName, StyledExt}; use gpui::{ div, prelude::FluentBuilder, relative, svg, AnyElement, App, Axis, Div, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, StyleRefinement, Styled, Window, }; /// A Radio element. /// /// This is not included the Radio group implementation, you can manage the group by yourself. #[derive(IntoElement)] pub struct Radio { base: Div, id: ElementId, label: Option, children: Vec, checked: bool, disabled: bool, on_click: Option>, } impl Radio { pub fn new(id: impl Into) -> Self { Self { id: id.into(), base: div(), label: None, children: Vec::new(), checked: false, disabled: false, on_click: None, } } pub fn label(mut self, label: impl Into) -> 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(&bool, &mut Window, &mut App) + 'static) -> Self { self.on_click = Some(Box::new(handler)); self } } impl Styled for Radio { fn style(&mut self) -> &mut gpui::StyleRefinement { self.base.style() } } impl InteractiveElement for Radio { fn interactivity(&mut self) -> &mut gpui::Interactivity { self.base.interactivity() } } impl StatefulInteractiveElement for Radio {} impl ParentElement for Radio { fn extend(&mut self, elements: impl IntoIterator) { self.children.extend(elements); } } impl RenderOnce for Radio { fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement { let color = if self.disabled { cx.theme().primary.opacity(0.5) } else { cx.theme().primary }; // wrap a flex to patch for let Radio display inline div().child( self.base .h_flex() .id(self.id) .gap_x_2() .text_color(cx.theme().foreground) .items_start() .line_height(relative(1.)) .child( div() .relative() .size_4() .flex_shrink_0() .rounded_full() .border_1() .border_color(color) .when(self.checked, |this| this.bg(color)) .child( svg() .absolute() .top_px() .left_px() .size_3() .text_color(color) .when(self.checked, |this| { this.text_color(cx.theme().primary_foreground) }) .map(|this| match self.checked { true => this.path(IconName::Check.path()), false => this, }), ), ) .child( v_flex() .w_full() .line_height(relative(1.2)) .gap_1() .when_some(self.label, |this, label| { this.child( div() .size_full() .overflow_hidden() .line_height(relative(1.)) .child(label), ) }) .children(self.children), ) .when_some( self.on_click.filter(|_| !self.disabled), |this, on_click| { this.on_click(move |_event, window, cx| { on_click(&!self.checked, window, cx); }) }, ), ) } } /// A Radio group element. #[derive(IntoElement)] pub struct RadioGroup { style: StyleRefinement, radios: Vec, layout: Axis, selected_index: Option, disabled: bool, on_change: Option>, } impl RadioGroup { fn new() -> Self { Self { style: StyleRefinement::default().flex_1(), on_change: None, layout: Axis::Vertical, selected_index: None, disabled: false, radios: vec![], } } /// Create a new Radio group with default Vertical layout. pub fn vertical() -> Self { Self::new() } /// Create a new Radio group with Horizontal layout. pub fn horizontal() -> Self { Self::new().layout(Axis::Horizontal) } /// Set the layout of the Radio group. Default is `Axis::Vertical`. pub fn layout(mut self, layout: Axis) -> Self { self.layout = layout; self } /// Listen to the change event. pub fn on_change(mut self, handler: impl Fn(&usize, &mut Window, &mut App) + 'static) -> Self { self.on_change = Some(Rc::new(handler)); self } /// Set the selected index. pub fn selected_index(mut self, index: Option) -> Self { self.selected_index = index; self } /// Set the disabled state. pub fn disabled(mut self, disabled: bool) -> Self { self.disabled = disabled; self } /// Add a child Radio element. pub fn child(mut self, child: impl Into) -> Self { self.radios.push(child.into()); self } /// Add multiple child Radio elements. pub fn children(mut self, children: impl IntoIterator>) -> 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 From<&'static str> for Radio { fn from(label: &'static str) -> Self { Self::new(label).label(label) } } impl From for Radio { fn from(label: SharedString) -> Self { Self::new(label.clone()).label(label) } } impl From for Radio { fn from(label: String) -> Self { Self::new(SharedString::from(label.clone())).label(SharedString::from(label)) } } 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.is_vertical() { v_flex() } else { h_flex().w_full().flex_wrap() }; let mut container = div(); *container.style() = self.style; container.child( base.gap_3() .children(self.radios.into_iter().enumerate().map(|(ix, radio)| { let checked = selected_ix == Some(ix); radio.disabled(disabled).checked(checked).when_some( on_change.clone(), |this, on_change| { this.on_click(move |_, window, cx| { on_change(&ix, window, cx); }) }, ) })), ) } }