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}