raui_core/widget/component/containers/
float_box.rs

1use crate::{
2    MessageData, PropsData, Scalar, make_widget, pre_hooks,
3    widget::{
4        WidgetId, WidgetIdOrRef,
5        component::{
6            containers::content_box::{ContentBoxProps, content_box},
7            interactive::navigation::{
8                NavContainerActive, NavItemActive, NavJumpActive, use_nav_container_active,
9                use_nav_item, use_nav_jump_direction_active,
10            },
11        },
12        context::{WidgetContext, WidgetMountOrChangeContext},
13        node::WidgetNode,
14        unit::content::ContentBoxContentReposition,
15        utils::Vec2,
16    },
17};
18use serde::{Deserialize, Serialize};
19
20#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
21#[props_data(crate::props::PropsData)]
22#[prefab(crate::Prefab)]
23pub struct FloatBoxProps {
24    #[serde(default)]
25    pub bounds_left: Option<Scalar>,
26    #[serde(default)]
27    pub bounds_right: Option<Scalar>,
28    #[serde(default)]
29    pub bounds_top: Option<Scalar>,
30    #[serde(default)]
31    pub bounds_bottom: Option<Scalar>,
32}
33
34#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
35#[props_data(crate::props::PropsData)]
36#[prefab(crate::Prefab)]
37pub struct FloatBoxNotifyProps(
38    #[serde(default)]
39    #[serde(skip_serializing_if = "WidgetIdOrRef::is_none")]
40    pub WidgetIdOrRef,
41);
42
43#[derive(PropsData, Debug, Copy, Clone, Serialize, Deserialize)]
44#[props_data(crate::props::PropsData)]
45#[prefab(crate::Prefab)]
46pub struct FloatBoxState {
47    #[serde(default)]
48    pub position: Vec2,
49    #[serde(default = "FloatBoxState::default_zoom")]
50    pub zoom: Scalar,
51}
52
53impl Default for FloatBoxState {
54    fn default() -> Self {
55        Self {
56            position: Default::default(),
57            zoom: Self::default_zoom(),
58        }
59    }
60}
61
62impl FloatBoxState {
63    fn default_zoom() -> Scalar {
64        1.0
65    }
66}
67
68#[derive(MessageData, Debug, Default, Clone)]
69#[message_data(crate::messenger::MessageData)]
70pub struct FloatBoxNotifyMessage {
71    pub sender: WidgetId,
72    pub state: FloatBoxState,
73    pub prev: FloatBoxState,
74}
75
76#[derive(MessageData, Debug, Clone)]
77#[message_data(crate::messenger::MessageData)]
78pub struct FloatBoxChangeMessage {
79    pub sender: WidgetId,
80    pub change: FloatBoxChange,
81}
82
83#[derive(Debug, Clone)]
84pub enum FloatBoxChange {
85    Absolute(FloatBoxState),
86    RelativePosition(Vec2),
87    RelativeZoom(Scalar),
88}
89
90pub fn use_float_box(context: &mut WidgetContext) {
91    fn notify(context: &WidgetMountOrChangeContext, data: FloatBoxNotifyMessage) {
92        if let Ok(FloatBoxNotifyProps(notify)) = context.props.read()
93            && let Some(to) = notify.read()
94        {
95            context.messenger.write(to, data);
96        }
97    }
98
99    context.life_cycle.mount(|context| {
100        let props = context.props.read_cloned_or_default::<FloatBoxProps>();
101        let mut data = context.props.read_cloned_or_default::<FloatBoxState>();
102        if let Some(limit) = props.bounds_left {
103            data.position.x = data.position.x.max(limit);
104        }
105        if let Some(limit) = props.bounds_right {
106            data.position.x = data.position.x.min(limit);
107        }
108        if let Some(limit) = props.bounds_top {
109            data.position.y = data.position.y.max(limit);
110        }
111        if let Some(limit) = props.bounds_bottom {
112            data.position.y = data.position.y.min(limit);
113        }
114        notify(
115            &context,
116            FloatBoxNotifyMessage {
117                sender: context.id.to_owned(),
118                state: data,
119                prev: data,
120            },
121        );
122        let _ = context.state.write_with(data);
123    });
124
125    context.life_cycle.change(|context| {
126        let props = context.props.read_cloned_or_default::<FloatBoxProps>();
127        let mut dirty = false;
128        let mut data = context.state.read_cloned_or_default::<FloatBoxState>();
129        let prev = data;
130        for msg in context.messenger.messages {
131            if let Some(msg) = msg.as_any().downcast_ref::<FloatBoxChangeMessage>() {
132                match msg.change {
133                    FloatBoxChange::Absolute(value) => {
134                        data = value;
135                        dirty = true;
136                    }
137                    FloatBoxChange::RelativePosition(delta) => {
138                        data.position.x -= delta.x;
139                        data.position.y -= delta.y;
140                        dirty = true;
141                    }
142                    FloatBoxChange::RelativeZoom(delta) => {
143                        data.zoom *= delta;
144                        dirty = true;
145                    }
146                }
147            }
148        }
149        if dirty {
150            if let Some(limit) = props.bounds_left {
151                data.position.x = data.position.x.max(limit);
152            }
153            if let Some(limit) = props.bounds_right {
154                data.position.x = data.position.x.min(limit);
155            }
156            if let Some(limit) = props.bounds_top {
157                data.position.y = data.position.y.max(limit);
158            }
159            if let Some(limit) = props.bounds_bottom {
160                data.position.y = data.position.y.min(limit);
161            }
162            notify(
163                &context,
164                FloatBoxNotifyMessage {
165                    sender: context.id.to_owned(),
166                    state: data.to_owned(),
167                    prev,
168                },
169            );
170            let _ = context.state.write_with(data);
171        }
172    });
173}
174
175#[pre_hooks(use_nav_container_active, use_nav_jump_direction_active, use_nav_item)]
176pub fn nav_float_box(mut context: WidgetContext) -> WidgetNode {
177    let WidgetContext {
178        key,
179        props,
180        listed_slots,
181        ..
182    } = context;
183
184    let props = props
185        .clone()
186        .without::<NavContainerActive>()
187        .without::<NavJumpActive>()
188        .without::<NavItemActive>();
189
190    make_widget!(float_box)
191        .key(key)
192        .merge_props(props)
193        .listed_slots(listed_slots)
194        .into()
195}
196
197#[pre_hooks(use_float_box)]
198pub fn float_box(mut context: WidgetContext) -> WidgetNode {
199    let WidgetContext {
200        key,
201        props,
202        state,
203        mut listed_slots,
204        ..
205    } = context;
206
207    let mut props = props.read_cloned_or_default::<ContentBoxProps>();
208    let state = state.read_cloned_or_default::<FloatBoxState>();
209    props.content_reposition = ContentBoxContentReposition {
210        offset: Vec2 {
211            x: -state.position.x,
212            y: -state.position.y,
213        },
214        scale: Vec2 {
215            x: state.zoom,
216            y: state.zoom,
217        },
218    };
219
220    for item in listed_slots.iter_mut() {
221        if let Some(p) = item.props_mut() {
222            p.write(state);
223            if !p.has::<FloatBoxNotifyProps>() {
224                p.write(FloatBoxNotifyProps(context.id.to_owned().into()));
225            }
226        }
227    }
228
229    make_widget!(content_box)
230        .key(key)
231        .with_props(props)
232        .listed_slots(listed_slots)
233        .into()
234}