raui_core/widget/component/containers/
scroll_box.rs

1use crate::{
2    PropsData, Scalar, make_widget, pre_hooks,
3    props::Props,
4    unpack_named_slots,
5    widget::{
6        WidgetId,
7        component::{
8            ResizeListenerSignal,
9            containers::{
10                content_box::{ContentBoxProps, content_box},
11                size_box::{SizeBoxProps, size_box},
12            },
13            image_box::{ImageBoxProps, image_box},
14            interactive::{
15                button::{
16                    ButtonNotifyMessage, ButtonNotifyProps, ButtonProps, button,
17                    self_tracked_button,
18                },
19                navigation::{
20                    NavItemActive, NavJump, NavScroll, NavSignal, NavTrackingNotifyMessage,
21                    NavTrackingNotifyProps, use_nav_container_active, use_nav_item,
22                    use_nav_item_active, use_nav_scroll_view_content,
23                },
24                scroll_view::{ScrollViewState, use_scroll_view},
25            },
26            use_resize_listener,
27        },
28        context::WidgetContext,
29        node::WidgetNode,
30        unit::{
31            area::AreaBoxNode, content::ContentBoxItemLayout, image::ImageBoxMaterial,
32            size::SizeBoxSizeValue,
33        },
34        utils::{Rect, Vec2, lerp},
35    },
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                && let Ok(data) = context.props.read::<ScrollBoxOwner>()
84            {
85                context
86                    .messenger
87                    .write(data.0.to_owned(), ResizeListenerSignal::Change(*size));
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                && let Ok(data) = context.state.read::<ScrollViewState>()
118            {
119                context
120                    .signals
121                    .write(NavSignal::Jump(NavJump::Scroll(NavScroll::Factor(
122                        data.value, false,
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.unmount(|context| {
207        context.signals.write(NavSignal::Unlock);
208    });
209
210    context.life_cycle.change(|context| {
211        let mut dirty = false;
212        let mut notify = false;
213        let mut state = context
214            .state
215            .read_cloned_or_default::<SideScrollbarsState>();
216        let mut props = context.props.read_cloned_or_default::<ScrollViewState>();
217        for msg in context.messenger.messages {
218            if let Some(msg) = msg.as_any().downcast_ref::<ButtonNotifyMessage>() {
219                if msg.trigger_start() {
220                    context.signals.write(NavSignal::Lock);
221                }
222                if msg.trigger_stop() {
223                    context.signals.write(NavSignal::Unlock);
224                }
225                if msg.sender.key() == "hbar" {
226                    state.horizontal_state = msg.state;
227                    dirty = true;
228                } else if msg.sender.key() == "vbar" {
229                    state.vertical_state = msg.state;
230                    dirty = true;
231                }
232            }
233            if let Some(msg) = msg.as_any().downcast_ref::<NavTrackingNotifyMessage>() {
234                if msg.sender.key() == "hbar"
235                    && state.horizontal_state.selected
236                    && (state.horizontal_state.trigger || state.horizontal_state.context)
237                {
238                    props.value.x = msg.state.factor.x;
239                    notify = true;
240                } else if msg.sender.key() == "vbar"
241                    && state.vertical_state.selected
242                    && (state.vertical_state.trigger || state.vertical_state.context)
243                {
244                    props.value.y = msg.state.factor.y;
245                    notify = true;
246                }
247            }
248        }
249        if dirty {
250            let _ = context.state.write_with(state);
251        }
252        if notify {
253            let view = context.props.read_cloned_or_default::<ScrollBoxOwner>().0;
254            context
255                .signals
256                .write(NavSignal::Jump(NavJump::Scroll(NavScroll::DirectFactor(
257                    view.into(),
258                    props.value,
259                    false,
260                ))));
261        }
262    });
263}
264
265#[pre_hooks(
266    use_nav_item_active,
267    use_nav_container_active,
268    use_nav_scroll_box_side_scrollbars
269)]
270pub fn nav_scroll_box_side_scrollbars(mut context: WidgetContext) -> WidgetNode {
271    let WidgetContext { id, key, props, .. } = context;
272
273    let view_props = props.read_cloned_or_default::<ScrollViewState>();
274
275    let SideScrollbarsProps {
276        size,
277        back_material,
278        front_material,
279    } = props.read_cloned_or_default();
280
281    let hbar = if view_props.size_factor.x > 1.0 {
282        let length = 1.0 / view_props.size_factor.y;
283        let rest = 1.0 - length;
284
285        let button_props = Props::new(NavItemActive)
286            .with(ButtonNotifyProps(id.to_owned().into()))
287            .with(NavTrackingNotifyProps(id.to_owned().into()))
288            .with(ContentBoxItemLayout {
289                anchors: Rect {
290                    left: 0.0,
291                    right: 1.0,
292                    top: 1.0,
293                    bottom: 1.0,
294                },
295                margin: Rect {
296                    left: 0.0,
297                    right: size,
298                    top: -size,
299                    bottom: 0.0,
300                },
301                align: Vec2 { x: 0.0, y: 1.0 },
302                ..Default::default()
303            });
304
305        let front_props = Props::new(ImageBoxProps {
306            material: front_material.clone(),
307            ..Default::default()
308        })
309        .with(ContentBoxItemLayout {
310            anchors: Rect {
311                left: lerp(0.0, rest, view_props.value.x),
312                right: lerp(length, 1.0, view_props.value.x),
313                top: 0.0,
314                bottom: 1.0,
315            },
316            ..Default::default()
317        });
318
319        let back = if let Some(material) = back_material.clone() {
320            let props = ImageBoxProps {
321                material,
322                ..Default::default()
323            };
324
325            make_widget!(image_box).key("back").with_props(props).into()
326        } else {
327            WidgetNode::default()
328        };
329
330        make_widget!(self_tracked_button)
331            .key("hbar")
332            .merge_props(button_props)
333            .named_slot(
334                "content",
335                make_widget!(content_box)
336                    .key("container")
337                    .listed_slot(back)
338                    .listed_slot(
339                        make_widget!(image_box)
340                            .key("front")
341                            .merge_props(front_props),
342                    ),
343            )
344            .into()
345    } else {
346        WidgetNode::default()
347    };
348
349    let vbar = if view_props.size_factor.y > 1.0 {
350        let length = 1.0 / view_props.size_factor.y;
351        let rest = 1.0 - length;
352
353        let button_props = Props::new(NavItemActive)
354            .with(ButtonNotifyProps(id.to_owned().into()))
355            .with(NavTrackingNotifyProps(id.to_owned().into()))
356            .with(ContentBoxItemLayout {
357                anchors: Rect {
358                    left: 1.0,
359                    right: 1.0,
360                    top: 0.0,
361                    bottom: 1.0,
362                },
363                margin: Rect {
364                    left: -size,
365                    right: 0.0,
366                    top: 0.0,
367                    bottom: size,
368                },
369                align: Vec2 { x: 1.0, y: 0.0 },
370                ..Default::default()
371            });
372
373        let back = if let Some(material) = back_material {
374            let props = ImageBoxProps {
375                material,
376                ..Default::default()
377            };
378
379            make_widget!(image_box).key("back").with_props(props).into()
380        } else {
381            WidgetNode::default()
382        };
383
384        let front_props = Props::new(ImageBoxProps {
385            material: front_material,
386            ..Default::default()
387        })
388        .with(ContentBoxItemLayout {
389            anchors: Rect {
390                left: 0.0,
391                right: 1.0,
392                top: lerp(0.0, rest, view_props.value.y),
393                bottom: lerp(length, 1.0, view_props.value.y),
394            },
395            ..Default::default()
396        });
397
398        make_widget!(self_tracked_button)
399            .key("vbar")
400            .merge_props(button_props)
401            .named_slot(
402                "content",
403                make_widget!(content_box)
404                    .key("container")
405                    .listed_slot(back)
406                    .listed_slot(
407                        make_widget!(image_box)
408                            .key("front")
409                            .merge_props(front_props),
410                    ),
411            )
412            .into()
413    } else {
414        WidgetNode::default()
415    };
416
417    make_widget!(content_box)
418        .key(key)
419        .listed_slot(hbar)
420        .listed_slot(vbar)
421        .into()
422}