raui-core 0.28.1

Renderer Agnostic User Interface
Documentation
use crate::{
    messenger::MessageData,
    unpack_named_slots, widget,
    widget::{
        component::interactive::{
            button::{use_button, ButtonProps},
            navigation::{use_nav_item, use_nav_text_input, NavSignal, NavTextChange},
        },
        context::WidgetMountOrChangeContext,
        unit::area::AreaBoxNode,
        WidgetId, WidgetIdOrRef,
    },
    widget_component, widget_hook,
};
use serde::{Deserialize, Serialize};

fn is_false(v: &bool) -> bool {
    !*v
}

fn is_zero(v: &usize) -> bool {
    *v == 0
}

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct TextInputProps {
    #[serde(default)]
    #[serde(skip_serializing_if = "is_false")]
    pub focused: bool,
    #[serde(default)]
    #[serde(skip_serializing_if = "is_zero")]
    pub cursor_position: usize,
    #[serde(default)]
    #[serde(skip_serializing_if = "is_false")]
    pub allow_new_line: bool,
    #[serde(default)]
    #[serde(skip_serializing_if = "String::is_empty")]
    pub text: String,
}
implement_props_data!(TextInputProps);

#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct TextInputNotifyProps(
    #[serde(default)]
    #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
    pub WidgetIdOrRef,
);
implement_props_data!(TextInputNotifyProps);

#[derive(Debug, Clone)]
pub struct TextInputNotifyMessage {
    pub sender: WidgetId,
    pub state: TextInputProps,
}
implement_message_data!(TextInputNotifyMessage);

widget_hook! {
    pub use_text_input_notified_state(life_cycle) {
        life_cycle.change(|context| {
            for msg in context.messenger.messages {
                if let Some(msg) = msg.as_any().downcast_ref::<TextInputNotifyMessage>() {
                    drop(context.state.write_with(msg.state.to_owned()));
                }
            }
        });
    }
}

widget_hook! {
    pub use_text_input(life_cycle) [use_nav_text_input] {
        fn notify<T>(context: &WidgetMountOrChangeContext, data: T)
        where
            T: 'static + MessageData,
        {
            if let Ok(notify) = context.props.read::<TextInputNotifyProps>() {
                if let Some(to) = notify.0.read() {
                    context.messenger.write(to, data);
                }
            }
        }

        life_cycle.mount(|context| {
            let mut data = context.props.read_cloned_or_default::<TextInputProps>();
            data.focused = false;
            notify(&context, TextInputNotifyMessage {
                sender: context.id.to_owned(),
                state: data.to_owned(),
            });
            drop(context.state.write_with(data));
        });

        life_cycle.change(|context| {
            let mut data = context.state.read_cloned_or_default::<TextInputProps>();
            let mut dirty = false;
            for msg in context.messenger.messages {
                if let Some(msg) = msg.as_any().downcast_ref::<NavSignal>() {
                    match msg {
                        NavSignal::FocusTextInput(idref) => {
                            data.focused = idref.is_some();
                            dirty = true;
                        }
                        NavSignal::TextChange(change) => if data.focused {
                            match change {
                                NavTextChange::InsertCharacter(c) => if !c.is_control() {
                                    data.cursor_position = data.cursor_position.min(data.text.len());
                                    data.text.insert(data.cursor_position, *c);
                                    data.cursor_position += 1;
                                }
                                NavTextChange::MoveCursorLeft => if data.cursor_position > 0 {
                                    data.cursor_position -= 1;
                                }
                                NavTextChange::MoveCursorRight => {
                                    if data.cursor_position < data.text.len() {
                                        data.cursor_position += 1;
                                    }
                                }
                                NavTextChange::MoveCursorStart => data.cursor_position = 0,
                                NavTextChange::MoveCursorEnd => {
                                    data.cursor_position = data.text.len();
                                }
                                NavTextChange::DeleteLeft => {
                                    if data.cursor_position > 0 && data.cursor_position <= data.text.len() {
                                        data.cursor_position -= 1;
                                        data.text.remove(data.cursor_position);
                                    }
                                }
                                NavTextChange::DeleteRight => {
                                    if data.cursor_position < data.text.len() {
                                        data.text.remove(data.cursor_position);
                                    }
                                }
                                NavTextChange::NewLine => if data.allow_new_line {
                                    data.cursor_position = data.cursor_position.min(data.text.len());
                                    data.text.insert(data.cursor_position, '\n');
                                    data.cursor_position += 1;
                                }
                            }
                            data.cursor_position = data.cursor_position.min(data.text.len());
                            dirty = true;
                        }
                        _ => {}
                    }
                }
            }
            if dirty {
                notify(&context, TextInputNotifyMessage {
                    sender: context.id.to_owned(),
                    state: data.to_owned(),
                });
                drop(context.state.write_with(data));
            }
        });
    }
}

widget_hook! {
    pub use_input_field(life_cycle) [use_button, use_text_input] {
        life_cycle.change(|context| {
            let focused = context.state.map_or_default::<TextInputProps, _, _>(|s| s.focused);
            for msg in context.messenger.messages {
                if let Some(msg) = msg.as_any().downcast_ref::<NavSignal>() {
                    match msg {
                        NavSignal::Accept(true) => if !focused {
                            context.signals.write(NavSignal::FocusTextInput(
                                context.id.to_owned().into()
                            ));
                        }
                        NavSignal::Cancel(true) => if focused {
                            context.signals.write(NavSignal::FocusTextInput(().into()));
                        }
                        _ => {}
                    }
                }
            }
        });
    }
}

widget_component! {
    pub text_input(id, state, named_slots) [use_nav_item, use_text_input] {
        unpack_named_slots!(named_slots => content);

        if let Some(p) = content.props_mut() {
            p.write(state.read_cloned_or_default::<TextInputProps>());
        }

        widget! {{{
            AreaBoxNode {
                id: id.to_owned(),
                slot: Box::new(content),
            }
        }}}
    }
}

widget_component! {
    pub input_field(id, state, named_slots) [use_nav_item, use_input_field] {
        unpack_named_slots!(named_slots => content);

        if let Some(p) = content.props_mut() {
            p.write(state.read_cloned_or_default::<ButtonProps>());
            p.write(state.read_cloned_or_default::<TextInputProps>());
        }

        widget! {{{
            AreaBoxNode {
                id: id.to_owned(),
                slot: Box::new(content),
            }
        }}}
    }
}