Skip to content
Snippets Groups Projects
text_box.rs 5.15 KiB
Newer Older
use crate::{core::{
MrGVSV's avatar
MrGVSV committed
    rsx,
MrGVSV's avatar
MrGVSV committed
    styles::{Corner, Style, Units},
MrGVSV's avatar
MrGVSV committed
    widget, Bound, Children, Color, EventType, MutableBound, OnEvent, WidgetProps,
}, widgets::ChangeEvent};
Ygg01's avatar
Ygg01 committed
use kayak_core::{CursorIcon, OnLayout};
StarArawn's avatar
StarArawn committed

use crate::widgets::{Background, Clip, Text, OnChange};
StarArawn's avatar
StarArawn committed

MrGVSV's avatar
MrGVSV committed
/// Props used by the [`TextBox`] widget
MrGVSV's avatar
MrGVSV committed
#[derive(Default, Debug, PartialEq, 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
    /// A callback for when the text value was changed
    pub on_change: Option<OnChange>,
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,
    pub styles: Option<Style>,
    pub children: Option<Children>,
    pub on_event: Option<OnEvent>,
Ygg01's avatar
Ygg01 committed
    pub on_layout: Option<OnLayout>,
    pub focusable: Option<bool>,
}

MrGVSV's avatar
MrGVSV committed
impl WidgetProps for TextBoxProps {
    fn get_children(&self) -> Option<Children> {
        self.children.clone()
    }

    fn set_children(&mut self, children: Option<Children>) {
        self.children = children;
    }

    fn get_styles(&self) -> Option<Style> {
        self.styles.clone()
    }

    fn get_on_event(&self) -> Option<OnEvent> {
        self.on_event.clone()
    }

Ygg01's avatar
Ygg01 committed
    fn get_on_layout(&self) -> Option<OnLayout> {
        self.on_layout.clone()
    }
Gino Valente's avatar
Gino Valente committed

MrGVSV's avatar
MrGVSV committed
    fn get_focusable(&self) -> Option<bool> {
        Some(!self.disabled)
    }
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Focus(pub bool);

MrGVSV's avatar
MrGVSV committed
#[widget]
MrGVSV's avatar
MrGVSV committed
/// A widget that displays a text input field
///
/// # Props
///
/// __Type:__ [`TextBoxProps`]
///
/// | Common Prop | Accepted |
/// | :---------: | :------: |
/// | `children`  | ✅        |
/// | `styles`    | ✅        |
/// | `on_event`  | ✅        |
/// | `on_layout` | ✅        |
MrGVSV's avatar
MrGVSV committed
/// | `focusable` | ✅        |
///
pub fn TextBox(props: TextBoxProps) {
MrGVSV's avatar
MrGVSV committed
    let TextBoxProps {
        on_change,
        placeholder,
        value,
        ..
    } = props.clone();
MrGVSV's avatar
MrGVSV committed

    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)
MrGVSV's avatar
MrGVSV committed
            // 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(),
MrGVSV's avatar
MrGVSV committed
                ..Default::default()
MrGVSV's avatar
MrGVSV committed
            }),
MrGVSV's avatar
MrGVSV committed
    );
StarArawn's avatar
StarArawn committed
    let background_styles = Style {
MrGVSV's avatar
MrGVSV committed
        background_color: Color::new(0.176, 0.196, 0.215, 1.0).into(),
MrGVSV's avatar
MrGVSV committed
        border_radius: Corner::all(5.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(),
        ..Default::default()
StarArawn's avatar
StarArawn committed
    };

    let has_focus = context.create_state(Focus(false)).unwrap();
StarArawn's avatar
StarArawn committed

StarArawn's avatar
StarArawn committed
    let mut current_value = value.clone();
StarArawn's avatar
StarArawn committed
    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 {
StarArawn's avatar
StarArawn committed
        EventType::CharInput { c } => {
            if is_backspace(c) {
                if !current_value.is_empty() {
StarArawn's avatar
StarArawn committed
                    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(),
                    });
                }
            }
        }
StarArawn's avatar
StarArawn committed
        EventType::Focus => cloned_has_focus.set(Focus(true)),
        EventType::Blur => cloned_has_focus.set(Focus(false)),
StarArawn's avatar
StarArawn committed
        _ => {}
    }));

    let text_styles = if value.is_empty() || (has_focus.get().0 && value.is_empty()) {
        Style {
MrGVSV's avatar
MrGVSV committed
            color: Color::new(0.5, 0.5, 0.5, 1.0).into(),
            ..Style::default()
        }
    } else {
MrGVSV's avatar
MrGVSV committed
        Style::default()
StarArawn's avatar
StarArawn committed
    let value = if value.is_empty() {
        placeholder.unwrap_or_else(|| value.clone())
StarArawn's avatar
StarArawn committed
    } else {
StarArawn's avatar
StarArawn committed
    };
StarArawn's avatar
StarArawn committed
    rsx! {
        <Background styles={Some(background_styles)}>
            <Clip>
                <Text
                    content={value}
                    size={14.0}
StarArawn's avatar
StarArawn committed
                    line_height={Some(22.0)}
                    styles={Some(text_styles)}
                />
StarArawn's avatar
StarArawn committed
            </Clip>
        </Background>
    }
}

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