diff --git a/crates/story/src/switch_story.rs b/crates/story/src/switch_story.rs index bc1f9482..bd12d9cc 100644 --- a/crates/story/src/switch_story.rs +++ b/crates/story/src/switch_story.rs @@ -4,8 +4,12 @@ use gpui::{ }; use ui::{ - checkbox::Checkbox, h_flex, label::Label, radio::Radio, switch::Switch, v_flex, ActiveTheme, - Disableable as _, Side, Sizable, StyledExt, + checkbox::Checkbox, + h_flex, + label::Label, + radio::{Radio, RadioGroup}, + switch::Switch, + v_flex, ActiveTheme, Disableable as _, Side, Sizable, StyledExt, }; use crate::section; @@ -20,6 +24,7 @@ pub struct SwitchStory { check3: bool, radio_check1: bool, radio_check2: bool, + radio_group_checked: Option, } impl super::Story for SwitchStory { @@ -52,6 +57,7 @@ impl SwitchStory { check3: true, radio_check1: false, radio_check2: true, + radio_group_checked: None, } } } @@ -259,6 +265,35 @@ impl Render for SwitchStory { ), ), ), + ) + .child( + h_flex() + .items_start() + .gap_4() + .w_full() + .child( + section("Radio Group", cx).flex_1().child( + RadioGroup::horizontal() + .children(["One", "Two", "Three"]) + .selected_index(self.radio_group_checked) + .on_change(cx.listener(|this, selected_ix: &usize, _cx| { + this.radio_group_checked = Some(*selected_ix); + })), + ), + ) + .child( + section("Radio Group Vertical", cx).flex_1().child( + RadioGroup::vertical() + .disabled(true) + .child(Radio::new("one1").label("United States")) + .child(Radio::new("one2").label("Canada")) + .child(Radio::new("one3").label("Mexico")) + .selected_index(self.radio_group_checked) + .on_change(cx.listener(|this, selected_ix: &usize, _cx| { + this.radio_group_checked = Some(*selected_ix); + })), + ), + ), ), ) } diff --git a/crates/ui/src/radio.rs b/crates/ui/src/radio.rs index b8a43bfc..d9bb9b05 100644 --- a/crates/ui/src/radio.rs +++ b/crates/ui/src/radio.rs @@ -1,6 +1,8 @@ -use crate::{h_flex, ActiveTheme, IconName}; +use std::rc::Rc; + +use crate::{h_flex, v_flex, ActiveTheme, AxisExt, IconName}; use gpui::{ - div, prelude::FluentBuilder, relative, svg, ElementId, InteractiveElement, IntoElement, + div, prelude::FluentBuilder, relative, svg, Axis, ElementId, InteractiveElement, IntoElement, ParentElement, RenderOnce, SharedString, StatefulInteractiveElement, Styled, WindowContext, }; @@ -57,7 +59,7 @@ impl RenderOnce for Radio { }; // wrap a flex to patch for let Radio display inline - div().flex().child( + h_flex().child( h_flex() .id(self.id) .gap_x_2() @@ -110,3 +112,119 @@ impl RenderOnce for Radio { ) } } + +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)) + } +} + +/// A Radio group element. +#[derive(IntoElement)] +pub struct RadioGroup { + radios: Vec, + layout: Axis, + selected_index: Option, + disabled: bool, + on_change: Option>, +} + +impl RadioGroup { + fn new() -> Self { + Self { + 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 WindowContext) + '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 RenderOnce for RadioGroup { + fn render(self, _: &mut WindowContext) -> 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().flex_wrap() + }; + + div().flex().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 |_, cx| { + on_change(&ix, cx); + }) + }, + ) + })), + ) + } +}