Skip to content
Snippets Groups Projects
text_box.rs 7.73 KiB
Newer Older
use bevy::prelude::{Bundle, Color, Commands, Component, Entity, In, Query};
StarToaster's avatar
StarToaster committed
use kayak_ui_macros::rsx;

Ygg01's avatar
Ygg01 committed
use crate::{
    context::WidgetName,
StarToaster's avatar
StarToaster committed
    event::{Event, EventType},
    event_dispatcher::EventDispatcherContext,
    on_event::OnEvent,
    on_layout::OnLayout,
    prelude::{KChildren, KayakWidgetContext, OnChange},
    styles::{Edge, KStyle, RenderCommand, StyleProp, Units},
    widget::Widget,
    widget_state::WidgetState,
StarToaster's avatar
StarToaster committed
    widgets::{
        text::{TextProps, TextWidgetBundle},
        BackgroundBundle, ClipBundle,
Ygg01's avatar
Ygg01 committed
    },
StarToaster's avatar
StarToaster committed
    Focusable,
Ygg01's avatar
Ygg01 committed
};
StarArawn's avatar
StarArawn committed

MrGVSV's avatar
MrGVSV committed
/// Props used by the [`TextBox`] widget
#[derive(Component, PartialEq, Default, Debug, Clone)]
pub struct TextBoxProps {
MrGVSV's avatar
MrGVSV committed
    /// If true, prevents the widget from being focused (and consequently edited)
MrGVSV's avatar
MrGVSV committed
    pub disabled: bool,
MrGVSV's avatar
MrGVSV committed
    /// The text to display when the user input is empty
    pub placeholder: Option<String>,
MrGVSV's avatar
MrGVSV committed
    /// 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)]
StarToaster's avatar
StarToaster committed
pub struct TextBoxState {
    pub focused: bool,
StarToaster's avatar
StarToaster committed
pub struct TextBoxValue(pub String);

impl Widget for TextBoxProps {}
MrGVSV's avatar
MrGVSV committed
/// A widget that displays a text input field
/// A text box allows users to input text.
/// This text box is fairly simple and only supports basic input.
MrGVSV's avatar
MrGVSV committed
///
StarToaster's avatar
StarToaster committed
#[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 text_box_render(
    In((widget_context, entity)): In<(KayakWidgetContext, Entity)>,
StarToaster's avatar
StarToaster committed
    mut commands: Commands,
    mut query: Query<(&mut KStyle, &TextBoxProps, &mut OnEvent, &OnChange)>,
    state_query: Query<&TextBoxState>,
StarToaster's avatar
StarToaster committed
) -> 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(),
        );

        if let Ok(state) = state_query.get(state_entity) {
            *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: Color::rgba(0.160, 0.172, 0.235, 1.0).into(),
                border_color: if state.focused {
                    Color::rgba(0.933, 0.745, 0.745, 1.0).into()
                } else {
                    Color::rgba(0.360, 0.380, 0.474, 1.0).into()
                },
                border: Edge::new(0.0, 0.0, 0.0, 2.0).into(),
MrGVSV's avatar
MrGVSV committed
                height: Units::Pixels(26.0).into(),
                padding_left: Units::Pixels(5.0).into(),
                padding_right: Units::Pixels(5.0).into(),
MrGVSV's avatar
MrGVSV committed
                ..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 {
StarToaster's avatar
StarToaster committed
                                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);
StarToaster's avatar
StarToaster committed
                            }
                            cloned_on_change.set_value(current_value);
                            event.add_system(cloned_on_change);
StarToaster's avatar
StarToaster committed
                        }
                        EventType::Focus => {
                            if let Ok(mut state) = state_query.get_mut(state_entity) {
                                state.focused = true;
                            }
StarToaster's avatar
StarToaster committed
                        }
                        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),
                                user_styles: KStyle {
                                    top: Units::Stretch(1.0).into(),
                                    bottom: Units::Stretch(1.0).into(),
                                    ..Default::default()
                                },
                                ..Default::default()
                            }}
                        />
                    </ClipBundle>
                </BackgroundBundle>
            }
StarArawn's avatar
StarArawn committed
        }
    }
StarArawn's avatar
StarArawn committed
}

/// 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}'