use bevy::prelude::{Bundle, Color, Commands, Component, Entity, In, Query}; use kayak_ui_macros::rsx; use crate::{ context::WidgetName, event::{Event, EventType}, event_dispatcher::EventDispatcherContext, on_event::OnEvent, on_layout::OnLayout, prelude::{KChildren, OnChange, WidgetContext}, styles::{Corner, KStyle, RenderCommand, StyleProp, Units}, widget::{Widget, WidgetProps}, widget_state::WidgetState, widgets::{ text::{TextProps, TextWidgetBundle}, BackgroundBundle, ClipBundle, }, Focusable, }; /// Props used by the [`TextBox`] widget #[derive(Component, PartialEq, Default, Debug, Clone)] pub struct TextBoxProps { /// If true, prevents the widget from being focused (and consequently edited) pub disabled: bool, /// The text to display when the user input is empty pub placeholder: Option<String>, /// The user input /// /// 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, } #[derive(Component, Default, Clone, PartialEq)] pub struct TextBoxState { pub focused: bool, } pub struct TextBoxValue(pub String); impl Widget for TextBoxProps {} impl WidgetProps for TextBoxProps {} /// A widget that displays a text input field /// /// # Props /// /// __Type:__ [`TextBoxProps`] /// /// | Common Prop | Accepted | /// | :---------: | :------: | /// | `children` | ❌ | /// | `styles` | ✅ | /// | `on_event` | ✅ | /// | `on_layout` | ✅ | /// #[derive(Bundle)] pub struct TextBoxBundle { pub text_box: TextBoxProps, pub styles: KStyle, pub on_event: OnEvent, pub on_layout: OnLayout, pub on_change: OnChange, pub focusable: Focusable, pub widget_name: WidgetName, } impl Default for TextBoxBundle { fn default() -> Self { Self { text_box: Default::default(), styles: Default::default(), on_event: Default::default(), on_layout: Default::default(), on_change: Default::default(), focusable: Default::default(), widget_name: TextBoxProps::default().get_name(), } } } pub fn update_text_box( In((widget_context, entity)): In<(WidgetContext, Entity)>, mut commands: Commands, mut query: Query<(&mut KStyle, &TextBoxProps, &mut OnEvent, &OnChange)>, ) -> bool { if let Ok((mut styles, text_box, mut on_event, on_change)) = query.get_mut(entity) { let state_entity = widget_context.use_state::<TextBoxState>( &mut commands, entity, TextBoxState::default(), ); *styles = KStyle::default() // Required styles .with_style(KStyle { render_command: RenderCommand::Layout.into(), ..Default::default() }) // Apply any prop-given styles .with_style(&*styles) // If not set by props, apply these styles .with_style(KStyle { 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 = KStyle { render_command: StyleProp::Value(RenderCommand::Quad), background_color: StyleProp::Value(Color::rgba(0.176, 0.196, 0.215, 1.0)), 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 current_value = text_box.value.clone(); let cloned_on_change = on_change.clone(); *on_event = OnEvent::new( move |In((event_dispatcher_context, _, mut event, _entity)): In<( EventDispatcherContext, WidgetState, Event, Entity, )>, mut state_query: Query<&mut TextBoxState>| { match event.event_type { EventType::CharInput { c } => { let mut current_value = current_value.clone(); let cloned_on_change = cloned_on_change.clone(); if let Ok(state) = state_query.get(state_entity) { if !state.focused { return (event_dispatcher_context, event); } } else { return (event_dispatcher_context, event); } 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); } cloned_on_change.set_value(current_value); event.add_system(cloned_on_change); } EventType::Focus => { if let Ok(mut state) = state_query.get_mut(state_entity) { state.focused = true; } } EventType::Blur => { if let Ok(mut state) = state_query.get_mut(state_entity) { state.focused = false; } } _ => {} } (event_dispatcher_context, event) }, ); let parent_id = Some(entity); rsx! { <BackgroundBundle styles={background_styles}> <ClipBundle styles={KStyle { height: Units::Pixels(26.0).into(), padding_left: StyleProp::Value(Units::Stretch(0.0)), padding_right: StyleProp::Value(Units::Stretch(0.0)), padding_bottom: StyleProp::Value(Units::Stretch(1.0)), padding_top: StyleProp::Value(Units::Stretch(1.0)), ..Default::default() }}> <TextWidgetBundle text={TextProps { content: text_box.value.clone(), size: 14.0, line_height: Some(18.0), ..Default::default() }} /> </ClipBundle> </BackgroundBundle> } } true } /// Checks if the given character contains the "Backspace" sequence /// /// Context: [Wikipedia](https://en.wikipedia.org/wiki/Backspace#Common_use) fn is_backspace(c: char) -> bool { c == '\u{8}' || c == '\u{7f}' }