diff --git a/examples/text_box.rs b/examples/text_box.rs index 5a51d0217d585c4d016e8281949d5926673e0f1b..26e13662e37850dda4db6eefb7577b302cee4335 100644 --- a/examples/text_box.rs +++ b/examples/text_box.rs @@ -18,7 +18,7 @@ fn TextBoxExample() { let (value, set_value, _) = use_state!("I started with a value!".to_string()); let (empty_value, set_empty_value, _) = use_state!("".to_string()); let (red_value, set_red_value, _) = use_state!("This text is red".to_string()); - let (spin_value, set_spin_value, _) = use_state!("3".to_string()); + let (spin_value, set_spin_value, _) = use_state!(3.0f32); let input_styles = Style { top: StyleProp::Value(Units::Pixels(10.0)), @@ -43,7 +43,7 @@ fn TextBoxExample() { }); let on_change_spin = OnChange::new(move |event| { - set_spin_value(event.value); + set_spin_value(32.0); }); rsx! { @@ -56,7 +56,7 @@ fn TextBoxExample() { placeholder={Some("This is a placeholder".to_string())} /> <TextBox styles={Some(red_text_styles)} value={red_value} on_change={Some(on_change_red)} /> - <SpinBox styles={Some(input_styles)} value={spin_value} on_change={Some(on_change_spin)} /> + <SpinBox<f32> styles={Some(input_styles)} value={spin_value} on_change={Some(on_change_spin)} /> </Window> } } diff --git a/src/widgets/spin_box.rs b/src/widgets/spin_box.rs index fa160c1016fb440fb9b7305e4144e302375f6f84..b43aa4d157ad20fc5f239d0c5f62789f840a7f6a 100644 --- a/src/widgets/spin_box.rs +++ b/src/widgets/spin_box.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use crate::{ core::{ render_command::RenderCommand, @@ -9,13 +11,19 @@ use crate::{ }; use kayak_core::{ styles::{LayoutType, StyleProp}, - CursorIcon, OnLayout, + CursorIcon, Index, OnLayout, Widget, }; use crate::widgets::{Background, Clip, OnChange, Text}; +#[derive(Debug, PartialEq, Clone, Default)] +pub struct SpinBox<T> { + pub id: Index, + pub props: SpinBoxProps<T>, +} + #[derive(Debug, PartialEq, Clone)] -pub struct SpinBoxProps { +pub struct SpinBoxProps<T> { /// If true, prevents the widget from being focused (and consequently edited) pub disabled: bool, /// A callback for when the text value was changed @@ -26,7 +34,7 @@ pub struct SpinBoxProps { /// /// This is a controlled state. You _must_ set this to the value to you wish to be displayed. /// You can use the [`on_change`] callback to update this prop as the user types. - pub value: String, + pub value: Option<T>, pub styles: Option<Style>, /// Text on increment button defaults to `>` pub incr_str: String, @@ -38,14 +46,13 @@ pub struct SpinBoxProps { pub focusable: Option<bool>, } - -impl Default for SpinBoxProps { - fn default() -> SpinBoxProps { - SpinBoxProps { +impl<T> Default for SpinBoxProps<T> { + fn default() -> SpinBoxProps<T> { + Self { incr_str: ">".into(), decr_str: "<".into(), disabled: Default::default(), - on_change: Default::default(), + on_change: Default::default(), placeholder: Default::default(), value: Default::default(), styles: Default::default(), @@ -57,7 +64,10 @@ impl Default for SpinBoxProps { } } -impl WidgetProps for SpinBoxProps { +impl<T> WidgetProps for SpinBoxProps<T> +where + T: Widget, +{ fn get_children(&self) -> Option<Children> { self.children.clone() } @@ -86,7 +96,6 @@ impl WidgetProps for SpinBoxProps { #[derive(Debug, Clone, Copy, PartialEq)] pub struct FocusSpinbox(pub bool); -#[widget] /// A widget that displays a spinnable text field /// /// # Props @@ -101,121 +110,161 @@ pub struct FocusSpinbox(pub bool); /// | `on_layout` | ✅ | /// | `focusable` | ✅ | /// -pub fn SpinBox(props: SpinBoxProps) { - let SpinBoxProps { - on_change, - placeholder, - value, - .. - } = props.clone(); - - props.styles = Some( - Style::default() - // Required styles - .with_style(Style { - render_command: RenderCommand::Layout.into(), - ..Default::default() - }) - // Apply any prop-given styles - .with_style(&props.styles) - // If not set by props, apply these styles - .with_style(Style { - top: Units::Pixels(0.0).into(), - bottom: Units::Pixels(0.0).into(), - height: Units::Pixels(26.0).into(), - cursor: CursorIcon::Text.into(), - ..Default::default() - }), - ); - - let background_styles = Style { - background_color: Color::new(0.176, 0.196, 0.215, 1.0).into(), - border_radius: Corner::all(5.0).into(), - height: Units::Pixels(26.0).into(), - padding_left: Units::Pixels(5.0).into(), - padding_right: Units::Pixels(5.0).into(), - ..Default::default() - }; - - let has_focus = context.create_state(FocusSpinbox(false)).unwrap(); - - let mut current_value = value.clone(); - let cloned_on_change = on_change.clone(); - let cloned_has_focus = has_focus.clone(); - - props.on_event = Some(OnEvent::new(move |_, event| match event.event_type { - EventType::CharInput { c } => { - if !cloned_has_focus.get().0 { - return; - } - if is_backspace(c) { - if !current_value.is_empty() { - current_value.truncate(current_value.len() - 1); +impl<T> Widget for SpinBox<T> +where + T: Widget + ToString + FromStr + Default, +{ + type Props = SpinBoxProps<T>; + + fn constructor(props: Self::Props) -> Self + where + Self: Sized, + { + Self { + id: Index::default(), + props, + } + } + + fn get_id(&self) -> Index { + self.id + } + + fn set_id(&mut self, id: Index) { + self.id = id; + } + + fn get_props(&self) -> &Self::Props { + &self.props + } + + fn get_props_mut(&mut self) -> &mut Self::Props { + &mut self.props + } + + fn render(&mut self, context: &mut kayak_core::KayakContextRef) { + let children = self.props.get_children(); + let mut props = self.props.clone(); + let SpinBoxProps { + on_change, + placeholder, + value, + .. + } = props.clone(); + + props.styles = Some( + Style::default() + // Required styles + .with_style(Style { + render_command: RenderCommand::Layout.into(), + ..Default::default() + }) + // Apply any prop-given styles + .with_style(&props.styles) + // If not set by props, apply these styles + .with_style(Style { + top: Units::Pixels(0.0).into(), + bottom: Units::Pixels(0.0).into(), + height: Units::Pixels(26.0).into(), + cursor: CursorIcon::Text.into(), + ..Default::default() + }), + ); + + let background_styles = Style { + background_color: Color::new(0.176, 0.196, 0.215, 1.0).into(), + border_radius: Corner::all(5.0).into(), + height: Units::Pixels(26.0).into(), + padding_left: Units::Pixels(5.0).into(), + padding_right: Units::Pixels(5.0).into(), + ..Default::default() + }; + + let has_focus = context.create_state(FocusSpinbox(false)).unwrap(); + let mut current_value = value.clone().map_or(String::new(), |f| { f.to_string()}); + let cloned_on_change = on_change.clone(); + let cloned_has_focus = has_focus.clone(); + + props.on_event = Some(OnEvent::new(move |_, event| match event.event_type { + EventType::CharInput { c } => { + if !cloned_has_focus.get().0 { + return; } - } else if !c.is_control() { - current_value.push(c); - } - if let Some(on_change) = cloned_on_change.as_ref() { - if let Ok(mut on_change) = on_change.0.write() { - on_change(ChangeEvent { - value: current_value.clone(), - }); + if is_backspace(c) { + if !current_value.is_empty() { + current_value.truncate(current_value.len() - 1); + } + } else if !c.is_control() { + current_value.push(c); + } + if let Some(on_change) = cloned_on_change.as_ref() { + if let Ok(mut on_change) = on_change.0.write() { + on_change(ChangeEvent { + value: current_value.clone(), + }); + } } } - } - EventType::Focus => cloned_has_focus.set(FocusSpinbox(true)), - EventType::Blur => cloned_has_focus.set(FocusSpinbox(false)), - _ => {} - })); - - let text_styles = if value.is_empty() || (has_focus.get().0 && value.is_empty()) { - Style { - color: Color::new(0.5, 0.5, 0.5, 1.0).into(), - ..Style::default() - } - } else { - Style { - width: Units::Stretch(100.0).into(), + EventType::Focus => cloned_has_focus.set(FocusSpinbox(true)), + EventType::Blur => cloned_has_focus.set(FocusSpinbox(false)), + _ => {} + })); + + let text_styles = if value.is_none() || (has_focus.get().0 && value.is_none()) { + Style { + color: Color::new(0.5, 0.5, 0.5, 1.0).into(), + ..Style::default() + } + } else { + Style { + width: Units::Stretch(100.0).into(), + ..Style::default() + } + }; + + let button_style = Some(Style { + height: Units::Pixels(24.0).into(), + width: Units::Pixels(24.0).into(), + ..Default::default() + }); + + // if current_value.is_empty() { + // current_value = placeholder.unwrap_or(current_value); + // }; + + // let value = if current_value.is_empty() { + // None + // } else { + // T::from_str(¤t_value).ok() + // }; + + let inline_style = Style { + layout_type: StyleProp::Value(LayoutType::Row), ..Style::default() + }; + + let incr_str = props.clone().incr_str; + let decr_str = props.clone().decr_str; + let content = value.clone().map_or(String::new(), |f| { f.to_string()}); + + + rsx! { + <Background styles={Some(background_styles)}> + <Clip styles={Some(inline_style)}> + <Button styles={button_style}> + <Text content={decr_str} /> + </Button> + <Text + content={content} + size={14.0} + styles={Some(text_styles)} + /> + <Button styles={button_style}> + <Text content={incr_str} /> + </Button> + </Clip> + </Background> } - }; - - let button_style = Some(Style { - height: Units::Pixels(24.0).into(), - width: Units::Pixels(24.0).into(), - ..Default::default() - }); - - let value = if value.is_empty() { - placeholder.unwrap_or_else(|| value.clone()) - } else { - value - }; - - let inline_style = Style { - layout_type: StyleProp::Value(LayoutType::Row), - ..Style::default() - }; - - let incr_str = props.clone().incr_str; - let decr_str = props.clone().decr_str; - - rsx! { - <Background styles={Some(background_styles)}> - <Clip styles={Some(inline_style)}> - <Button styles={button_style}> - <Text content={decr_str} /> - </Button> - <Text - content={value} - size={14.0} - styles={Some(text_styles)} - /> - <Button styles={button_style}> - <Text content={incr_str} /> - </Button> - </Clip> - </Background> } }