raui_core/widget/component/containers/
scroll_box.rs

1use crate::{
2    make_widget, pre_hooks,
3    props::Props,
4    unpack_named_slots,
5    widget::{
6        component::{
7            containers::{
8                content_box::{content_box, ContentBoxProps},
9                size_box::{size_box, SizeBoxProps},
10            },
11            image_box::{image_box, ImageBoxProps},
12            interactive::{
13                button::{
14                    button, self_tracked_button, ButtonNotifyMessage, ButtonNotifyProps,
15                    ButtonProps,
16                },
17                navigation::{
18                    use_nav_container_active, use_nav_item, use_nav_item_active,
19                    use_nav_scroll_view_content, NavItemActive, NavJump, NavScroll, NavSignal,
20                    NavTrackingNotifyMessage, NavTrackingNotifyProps,
21                },
22                scroll_view::{use_scroll_view, ScrollViewState},
23            },
24            use_resize_listener, ResizeListenerSignal,
25        },
26        context::WidgetContext,
27        node::WidgetNode,
28        unit::{
29            area::AreaBoxNode, content::ContentBoxItemLayout, image::ImageBoxMaterial,
30            size::SizeBoxSizeValue,
31        },
32        utils::{lerp, Rect, Vec2},
33        WidgetId,
34    },
35    PropsData, Scalar,
36};
37use serde::{Deserialize, Serialize};
38
39#[derive(PropsData, Debug, Default, Clone, Serialize, Deserialize)]
40#[props_data(crate::props::PropsData)]
41#[prefab(crate::Prefab)]
42pub struct ScrollBoxOwner(
43    #[serde(default)]
44    #[serde(skip_serializing_if = "WidgetId::is_none")]
45    pub WidgetId,
46);
47
48#[derive(PropsData, Debug, Clone, Serialize, Deserialize)]
49#[props_data(crate::props::PropsData)]
50#[prefab(crate::Prefab)]
51pub struct SideScrollbarsProps {
52    #[serde(default)]
53    pub size: Scalar,
54    #[serde(default)]
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub back_material: Option<ImageBoxMaterial>,
57    #[serde(default)]
58    pub front_material: ImageBoxMaterial,
59}
60
61impl Default for SideScrollbarsProps {
62    fn default() -> Self {
63        Self {
64            size: 10.0,
65            back_material: None,
66            front_material: Default::default(),
67        }
68    }
69}
70
71#[derive(PropsData, Debug, Default, Copy, Clone, Serialize, Deserialize)]
72#[props_data(crate::props::PropsData)]
73#[prefab(crate::Prefab)]
74pub struct SideScrollbarsState {
75    pub horizontal_state: ButtonProps,
76    pub vertical_state: ButtonProps,
77}
78
79pub fn use_nav_scroll_box_content(context: &mut WidgetContext) {
80    context.life_cycle.change(|context| {
81        for msg in context.messenger.messages {
82            if let Some(ResizeListenerSignal::Change(size)) = msg.as_any().downcast_ref() {
83                if let Ok(data) = context.props.read::<ScrollBoxOwner>() {
84                    context
85                        .messenger
86                        .write(data.0.to_owned(), ResizeListenerSignal::Change(*size));
87                }
88            }
89        }
90    });
91}
92
93#[pre_hooks(
94    use_resize_listener,
95    use_nav_item_active,
96    use_nav_container_active,
97    use_nav_scroll_view_content,
98    use_nav_scroll_box_content
99)]
100pub fn nav_scroll_box_content(mut context: WidgetContext) -> WidgetNode {
101    let WidgetContext {
102        id, named_slots, ..
103    } = context;
104    unpack_named_slots!(named_slots => content);
105
106    AreaBoxNode {
107        id: id.to_owned(),
108        slot: Box::new(content),
109    }
110    .into()
111}
112
113pub fn use_nav_scroll_box(context: &mut WidgetContext) {
114    context.life_cycle.change(|context| {
115        for msg in context.messenger.messages {
116            if let Some(ResizeListenerSignal::Change(_)) = msg.as_any().downcast_ref() {
117                if let Ok(data) = context.state.read::<ScrollViewState>() {
118                    context
119                        .signals
120                        .write(NavSignal::Jump(NavJump::Scroll(NavScroll::Factor(
121                            data.value, false,
122                        ))));
123                }
124            }
125        }
126    });
127}
128
129#[pre_hooks(
130    use_resize_listener,
131    use_nav_item,
132    use_nav_container_active,
133    use_scroll_view,
134    use_nav_scroll_box
135)]
136pub fn nav_scroll_box(mut context: WidgetContext) -> WidgetNode {
137    let WidgetContext {
138        id,
139        key,
140        props,
141        state,
142        named_slots,
143        ..
144    } = context;
145    unpack_named_slots!(named_slots => {content, scrollbars});
146
147    let scroll_props = state.read_cloned_or_default::<ScrollViewState>();
148
149    let content_props = Props::new(ContentBoxItemLayout {
150        align: scroll_props.value,
151        ..Default::default()
152    })
153    .with(ScrollBoxOwner(id.to_owned()));
154
155    if let Some(props) = scrollbars.props_mut() {
156        props.write(ScrollBoxOwner(id.to_owned()));
157        props.write(scroll_props);
158    }
159
160    if !props.has::<ContentBoxProps>() {
161        props.write(ContentBoxProps {
162            clipping: true,
163            ..Default::default()
164        });
165    }
166
167    let size_props = SizeBoxProps {
168        width: SizeBoxSizeValue::Fill,
169        height: SizeBoxSizeValue::Fill,
170        ..Default::default()
171    };
172
173    let content = make_widget!(content_box)
174        .key(key)
175        .merge_props(props.clone())
176        .listed_slot(
177            make_widget!(button)
178                .key("input-consumer")
179                .with_props(NavItemActive)
180                .named_slot(
181                    "content",
182                    make_widget!(size_box).key("size").with_props(size_props),
183                ),
184        )
185        .listed_slot(
186            make_widget!(nav_scroll_box_content)
187                .key("content")
188                .merge_props(content_props)
189                .named_slot("content", content),
190        )
191        .listed_slot(scrollbars)
192        .into();
193
194    AreaBoxNode {
195        id: id.to_owned(),
196        slot: Box::new(content),
197    }
198    .into()
199}
200
201pub fn use_nav_scroll_box_side_scrollbars(context: &mut WidgetContext) {
202    context.life_cycle.mount(|context| {
203        let _ = context.state.write_with(SideScrollbarsState::default());
204    });
205
206    context.life_cycle.change(|context| {
207        let mut dirty = false;
208        let mut notify = false;
209        let mut state = context
210            .state
211            .read_cloned_or_default::<SideScrollbarsState>();
212        let mut props = context.props.read_cloned_or_default::<ScrollViewState>();
213        for msg in context.messenger.messages {
214            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
215                if msg.trigger_start() {
216                    context.signals.write(NavSignal::Lock);
217                }
218                if msg.trigger_stop() {
219                    context.signals.write(NavSignal::Unlock);
220                }
221                if msg.sender.key() == "hbar" {
222                    state.horizontal_state = msg.state;
223                    dirty = true;
224                } else if msg.sender.key() == "vbar" {
225                    state.vertical_state = msg.state;
226                    dirty = true;
227                }
228            }
229            if let Some(msg) = msg.as_any().downcast_ref::<NavTrackingNotifyMessage>() {
230                if msg.sender.key() == "hbar"
231                    && state.horizontal_state.selected
232                    && (state.horizontal_state.trigger || state.horizontal_state.context)
233                {
234                    props.value.x = msg.state.0.x;
235                    notify = true;
236                } else if msg.sender.key() == "vbar"
237                    && state.vertical_state.selected
238                    && (state.vertical_state.trigger || state.vertical_state.context)
239                {
240                    props.value.y = msg.state.0.y;
241                    notify = true;
242                }
243            }
244        }
245        if dirty {
246            let _ = context.state.write_with(state);
247        }
248        if notify {
249            let view = context.props.read_cloned_or_default::<ScrollBoxOwner>().0;
250            context
251                .signals
252                .write(NavSignal::Jump(NavJump::Scroll(NavScroll::DirectFactor(
253                    view.into(),
254                    props.value,
255                    false,
256                ))));
257        }
258    });
259}
260
261#[pre_hooks(
262    use_nav_item_active,
263    use_nav_container_active,
264    use_nav_scroll_box_side_scrollbars
265)]
266pub fn nav_scroll_box_side_scrollbars(mut context: WidgetContext) -> WidgetNode {
267    let WidgetContext { id, key, props, .. } = context;
268
269    let view_props = props.read_cloned_or_default::<ScrollViewState>();
270
271    let SideScrollbarsProps {
272        size,
273        back_material,
274        front_material,
275    } = props.read_cloned_or_default();
276
277    let hbar = if view_props.size_factor.x > 1.0 {
278        let length = 1.0 / view_props.size_factor.y;
279        let rest = 1.0 - length;
280
281        let button_props = Props::new(NavItemActive)
282            .with(ButtonNotifyProps(id.to_owned().into()))
283            .with(NavTrackingNotifyProps(id.to_owned().into()))
284            .with(ContentBoxItemLayout {
285                anchors: Rect {
286                    left: 0.0,
287                    right: 1.0,
288                    top: 1.0,
289                    bottom: 1.0,
290                },
291                margin: Rect {
292                    left: 0.0,
293                    right: size,
294                    top: -size,
295                    bottom: 0.0,
296                },
297                align: Vec2 { x: 0.0, y: 1.0 },
298                ..Default::default()
299            });
300
301        let front_props = Props::new(ImageBoxProps {
302            material: front_material.clone(),
303            ..Default::default()
304        })
305        .with(ContentBoxItemLayout {
306            anchors: Rect {
307                left: lerp(0.0, rest, view_props.value.x),
308                right: lerp(length, 1.0, view_props.value.x),
309                top: 0.0,
310                bottom: 1.0,
311            },
312            ..Default::default()
313        });
314
315        let back = if let Some(material) = back_material.clone() {
316            let props = ImageBoxProps {
317                material,
318                ..Default::default()
319            };
320
321            make_widget!(image_box).key("back").with_props(props).into()
322        } else {
323            WidgetNode::default()
324        };
325
326        make_widget!(self_tracked_button)
327            .key("hbar")
328            .merge_props(button_props)
329            .named_slot(
330                "content",
331                make_widget!(content_box)
332                    .key("container")
333                    .listed_slot(back)
334                    .listed_slot(
335                        make_widget!(image_box)
336                            .key("front")
337                            .merge_props(front_props),
338                    ),
339            )
340            .into()
341    } else {
342        WidgetNode::default()
343    };
344
345    let vbar = if view_props.size_factor.y > 1.0 {
346        let length = 1.0 / view_props.size_factor.y;
347        let rest = 1.0 - length;
348
349        let button_props = Props::new(NavItemActive)
350            .with(ButtonNotifyProps(id.to_owned().into()))
351            .with(NavTrackingNotifyProps(id.to_owned().into()))
352            .with(ContentBoxItemLayout {
353                anchors: Rect {
354                    left: 1.0,
355                    right: 1.0,
356                    top: 0.0,
357                    bottom: 1.0,
358                },
359                margin: Rect {
360                    left: -size,
361                    right: 0.0,
362                    top: 0.0,
363                    bottom: size,
364                },
365                align: Vec2 { x: 1.0, y: 0.0 },
366                ..Default::default()
367            });
368
369        let back = if let Some(material) = back_material {
370            let props = ImageBoxProps {
371                material,
372                ..Default::default()
373            };
374
375            make_widget!(image_box).key("back").with_props(props).into()
376        } else {
377            WidgetNode::default()
378        };
379
380        let front_props = Props::new(ImageBoxProps {
381            material: front_material,
382            ..Default::default()
383        })
384        .with(ContentBoxItemLayout {
385            anchors: Rect {
386                left: 0.0,
387                right: 1.0,
388                top: lerp(0.0, rest, view_props.value.y),
389                bottom: lerp(length, 1.0, view_props.value.y),
390            },
391            ..Default::default()
392        });
393
394        make_widget!(self_tracked_button)
395            .key("vbar")
396            .merge_props(button_props)
397            .named_slot(
398                "content",
399                make_widget!(content_box)
400                    .key("container")
401                    .listed_slot(back)
402                    .listed_slot(
403                        make_widget!(image_box)
404                            .key("front")
405                            .merge_props(front_props),
406                    ),
407            )
408            .into()
409    } else {
410        WidgetNode::default()
411    };
412
413    make_widget!(content_box)
414        .key(key)
415        .listed_slot(hbar)
416        .listed_slot(vbar)
417        .into()
418}