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}