raui_core/widget/component/interactive/
button.rs

1use crate::{
2    MessageData, PropsData, pre_hooks, unpack_named_slots,
3    widget::{
4        WidgetId, WidgetIdOrRef,
5        component::interactive::navigation::{
6            NavSignal, use_nav_button, use_nav_item, use_nav_tracking, use_nav_tracking_self,
7        },
8        context::{WidgetContext, WidgetMountOrChangeContext},
9        node::WidgetNode,
10        unit::area::AreaBoxNode,
11    },
12};
13use serde::{Deserialize, Serialize};
14
15fn is_false(v: &bool) -> bool {
16    !*v
17}
18
19#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
20#[props_data(crate::props::PropsData)]
21#[prefab(crate::Prefab)]
22pub struct ButtonProps {
23    #[serde(default)]
24    #[serde(skip_serializing_if = "is_false")]
25    pub selected: bool,
26    #[serde(default)]
27    #[serde(skip_serializing_if = "is_false")]
28    pub trigger: bool,
29    #[serde(default)]
30    #[serde(skip_serializing_if = "is_false")]
31    pub context: bool,
32}
33
34#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
35#[props_data(crate::props::PropsData)]
36#[prefab(crate::Prefab)]
37pub struct ButtonNotifyProps(
38    #[serde(default)]
39    #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
40    pub WidgetIdOrRef,
41);
42
43#[derive(MessageData, Debug, Default, Clone)]
44#[message_data(crate::messenger::MessageData)]
45pub struct ButtonNotifyMessage {
46    pub sender: WidgetId,
47    pub state: ButtonProps,
48    pub prev: ButtonProps,
49}
50
51impl ButtonNotifyMessage {
52    pub fn select_start(&self) -> bool {
53        !self.prev.selected && self.state.selected
54    }
55
56    pub fn select_stop(&self) -> bool {
57        self.prev.selected && !self.state.selected
58    }
59
60    pub fn select_changed(&self) -> bool {
61        self.prev.selected != self.state.selected
62    }
63
64    pub fn trigger_start(&self) -> bool {
65        !self.prev.trigger && self.state.trigger
66    }
67
68    pub fn trigger_stop(&self) -> bool {
69        self.prev.trigger && !self.state.trigger
70    }
71
72    pub fn trigger_changed(&self) -> bool {
73        self.prev.trigger != self.state.trigger
74    }
75
76    pub fn context_start(&self) -> bool {
77        !self.prev.context && self.state.context
78    }
79
80    pub fn context_stop(&self) -> bool {
81        self.prev.context && !self.state.context
82    }
83
84    pub fn context_changed(&self) -> bool {
85        self.prev.context != self.state.context
86    }
87}
88
89pub fn use_button_notified_state(context: &mut WidgetContext) {
90    context.life_cycle.change(|context| {
91        for msg in context.messenger.messages {
92            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
93                let _ = context.state.write_with(msg.state);
94            }
95        }
96    });
97}
98
99#[pre_hooks(use_nav_item, use_nav_button)]
100pub fn use_button(context: &mut WidgetContext) {
101    fn notify(context: &WidgetMountOrChangeContext, data: ButtonNotifyMessage) {
102        if let Ok(ButtonNotifyProps(notify)) = context.props.read()
103            && let Some(to) = notify.read()
104        {
105            context.messenger.write(to, data);
106        }
107    }
108
109    context.life_cycle.mount(|context| {
110        notify(
111            &context,
112            ButtonNotifyMessage {
113                sender: context.id.to_owned(),
114                state: Default::default(),
115                prev: Default::default(),
116            },
117        );
118        let _ = context.state.write_with(ButtonProps::default());
119    });
120
121    context.life_cycle.change(|context| {
122        let mut dirty = false;
123        let mut data = context.state.read_cloned_or_default::<ButtonProps>();
124        let prev = data;
125        for msg in context.messenger.messages {
126            if let Some(msg) = msg.as_any().downcast_ref() {
127                match msg {
128                    NavSignal::Select(_) => {
129                        data.selected = true;
130                        dirty = true;
131                    }
132                    NavSignal::Unselect => {
133                        data.selected = false;
134                        dirty = true;
135                    }
136                    NavSignal::Accept(v) => {
137                        data.trigger = *v;
138                        dirty = true;
139                    }
140                    NavSignal::Context(v) => {
141                        data.context = *v;
142                        dirty = true;
143                    }
144                    _ => {}
145                }
146            }
147        }
148        if dirty {
149            notify(
150                &context,
151                ButtonNotifyMessage {
152                    sender: context.id.to_owned(),
153                    state: data.to_owned(),
154                    prev,
155                },
156            );
157            let _ = context.state.write_with(data);
158        }
159    });
160}
161
162#[pre_hooks(use_button)]
163pub fn button(mut context: WidgetContext) -> WidgetNode {
164    let WidgetContext {
165        id,
166        state,
167        named_slots,
168        ..
169    } = context;
170    unpack_named_slots!(named_slots => content);
171
172    if let Some(p) = content.props_mut() {
173        p.write(state.read_cloned_or_default::<ButtonProps>());
174    }
175
176    AreaBoxNode {
177        id: id.to_owned(),
178        slot: Box::new(content),
179    }
180    .into()
181}
182
183#[pre_hooks(use_nav_tracking)]
184pub fn tracked_button(mut context: WidgetContext) -> WidgetNode {
185    button(context)
186}
187
188#[pre_hooks(use_nav_tracking_self)]
189pub fn self_tracked_button(mut context: WidgetContext) -> WidgetNode {
190    button(context)
191}