raui-core 0.28.1

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

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

fn is_zero(v: &Vec2) -> bool {
    v.x.abs() < 1.0e-6 && v.y.abs() < 1.0e-6
}

#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)]
pub struct ButtonProps {
    #[serde(default)]
    #[serde(skip_serializing_if = "is_false")]
    pub selected: bool,
    #[serde(default)]
    #[serde(skip_serializing_if = "is_false")]
    pub trigger: bool,
    #[serde(default)]
    #[serde(skip_serializing_if = "is_false")]
    pub context: bool,
    #[serde(default)]
    #[serde(skip_serializing_if = "is_zero")]
    pub pointer: Vec2,
}
implement_props_data!(ButtonProps);

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

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

impl ButtonNotifyMessage {
    pub fn select_start(&self) -> bool {
        !self.prev.selected && self.state.selected
    }

    pub fn select_stop(&self) -> bool {
        self.prev.selected && !self.state.selected
    }

    pub fn select_changed(&self) -> bool {
        self.prev.selected != self.state.selected
    }

    pub fn trigger_start(&self) -> bool {
        !self.prev.trigger && self.state.trigger
    }

    pub fn trigger_stop(&self) -> bool {
        self.prev.trigger && !self.state.trigger
    }

    pub fn trigger_changed(&self) -> bool {
        self.prev.trigger != self.state.trigger
    }

    pub fn context_start(&self) -> bool {
        !self.prev.context && self.state.context
    }

    pub fn context_stop(&self) -> bool {
        self.prev.context && !self.state.context
    }

    pub fn context_changed(&self) -> bool {
        self.prev.context != self.state.context
    }
}

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

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

        life_cycle.mount(|context| {
            notify(&context, ButtonNotifyMessage {
                sender: context.id.to_owned(),
                state: ButtonProps::default(),
                prev: ButtonProps::default(),
            });
            drop(context.state.write_with(ButtonProps::default()));
        });

        life_cycle.change(|context| {
            let mut data = context.state.read_cloned_or_default::<ButtonProps>();
            let prev = data;
            let mut dirty = false;
            for msg in context.messenger.messages {
                if let Some(msg) = msg.as_any().downcast_ref::<NavSignal>() {
                    match msg {
                        NavSignal::Select(_) => {
                            data.selected = true;
                            dirty = true;
                        }
                        NavSignal::Unselect => {
                            data.selected = false;
                            dirty = true;
                        }
                        NavSignal::Accept(v) => {
                            data.trigger = *v;
                            dirty = true;
                        }
                        NavSignal::Context(v) => {
                            data.context = *v;
                            dirty = true;
                        }
                        NavSignal::Axis(n, v) => match n.as_str() {
                            "pointer-x" => {
                                data.pointer.x = *v;
                                dirty = true;
                            }
                            "pointer-y" => {
                                data.pointer.y = *v;
                                dirty = true;
                            }
                            _ => {}
                        }
                        _ => {}
                    }
                }
            }
            if dirty {
                notify(&context, ButtonNotifyMessage {
                    sender: context.id.to_owned(),
                    state: data.to_owned(),
                    prev,
                });
                drop(context.state.write_with(data));
            }
        });
    }
}

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

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

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