1use bevy::input::mouse::{MouseWheel, MouseScrollUnit};
4use bevy::ui_widgets::observe;
5
6use crate::{events::*, consts::*, utils::*};
7use super::*;
8
9#[derive(Resource, Default)]
12pub struct CanBeScrolled {
13 pub entity: Option<Entity>
14}
15
16#[derive(Component)]
18pub struct MakaraScroll;
19
20#[derive(Component)]
23pub struct MakaraScrollMovePanel;
24
25#[derive(Component)]
27pub struct MakaraScrollbar;
28
29#[derive(Component)]
32pub struct ScrollMovePanelEntity(pub Entity);
33
34#[derive(Component)]
37pub struct ScrollEntity(pub Entity);
38
39#[derive(Component)]
42pub struct ScrollBarEntity(pub Entity);
43
44#[derive(Component)]
47pub struct MakaraScrollList {
48 pub position: f32,
49 pub scroll_height: f32
50}
51
52impl Default for MakaraScrollList {
53 fn default() -> Self {
54 Self {
55 position: 0.0,
56 scroll_height: DEFAULT_SCROLL_HEIGHT
57 }
58 }
59}
60
61#[derive(Component)]
62pub struct ScrollChildrenNeedTransfer;
63
64#[derive(Component)]
65pub struct TempMovePanelStyle(pub ContainerStyle);
66
67#[derive(Component)]
68pub struct TempScrollBarStyle(pub ContainerStyle);
69
70pub struct ScrollWidget<'a, 'w, 's> {
72 pub entity: Entity,
73 pub panel_entity: Entity,
74 pub class: &'a mut Class,
75 pub style: WidgetStyle<'a>,
76 pub bar_style: WidgetStyle<'a>,
77 pub child_entities: Vec<Entity>, pub commands: &'a mut Commands<'w, 's>
79}
80
81impl<'a, 'w, 's> WidgetChildren for ScrollWidget<'a, 'w, 's> {
82 fn add_child(&mut self, child_bundle: impl Bundle) {
83 let child_entity = self.commands.spawn(child_bundle).id();
84 self.commands.entity(self.panel_entity).add_child(child_entity);
85 }
86
87 fn add_children(&mut self, bundles: impl IntoIterator<Item = impl Bundle>) {
88 let mut child_entities = Vec::new();
89
90 for bundle in bundles {
91 let child_entity = self.commands.spawn(bundle).id();
92 child_entities.push(child_entity);
93 }
94 self.commands.entity(self.panel_entity).add_children(&child_entities);
95 }
96
97 fn insert_at(
98 &mut self,
99 index: usize,
100 bundles: impl IntoIterator<Item = impl Bundle>
101 ) {
102 let mut child_entities = Vec::new();
103
104 for bundle in bundles {
105 let child_entity = self.commands.spawn(bundle).id();
106 child_entities.push(child_entity);
107 }
108 self.commands
109 .entity(self.panel_entity)
110 .insert_children(index, &child_entities);
111 }
112
113 fn insert_first(&mut self, bundles: impl IntoIterator<Item = impl Bundle>) {
114 self.insert_at(0, bundles);
115 }
116
117 fn insert_last(&mut self, bundles: impl IntoIterator<Item = impl Bundle>) {
118 let last_index = self.child_entities.len();
119 self.insert_at(last_index, bundles);
120 }
121
122 fn remove_at(&mut self, index: usize) {
123 if let Some(entity) = self.child_entities.get(index) {
124 self.commands.entity(self.panel_entity).detach_child(*entity);
125 self.commands.entity(*entity).despawn();
126 }
127 }
128
129 fn remove_first(&mut self) {
130 self.remove_at(0);
131 }
132
133 fn remove_last(&mut self) {
134 if let Some(last_index) = self.child_entities.len().checked_sub(1) {
136 self.remove_at(last_index);
137 }
138 }
139}
140
141type IsScrollOnly = (
142 (
143 With<MakaraScroll>,
144 Without<MakaraCheckbox>,
145 Without<MakaraCheckboxButton>,
146 Without<MakaraCircular>,
147 Without<MakaraColumn>,
148 Without<MakaraRoot>,
149 Without<MakaraButton>,
150 Without<MakaraDropdown>,
151 Without<MakaraDropdownOverlay>,
152 Without<MakaraImage>,
153 Without<MakaraLink>,
154 Without<MakaraModal>,
155 Without<MakaraModalBackdrop>,
156 ),
157 (
158 Without<MakaraProgressBar>,
159 Without<MakaraRadio>,
160 Without<MakaraRadioGroup>,
161 Without<MakaraRow>,
162 Without<MakaraScrollbar>,
163 Without<MakaraTextInput>,
164 Without<MakaraTextInputCursor>,
165 Without<MakaraSlider>,
166 Without<MakaraSliderThumb>,
167 Without<MakaraSelect>,
168 Without<MakaraSelectOverlay>,
169 )
170);
171
172type IsScrollBarOnly = (
173 (
174 With<MakaraScrollbar>,
175 Without<MakaraCheckbox>,
176 Without<MakaraCheckboxButton>,
177 Without<MakaraCircular>,
178 Without<MakaraColumn>,
179 Without<MakaraRoot>,
180 Without<MakaraButton>,
181 Without<MakaraDropdown>,
182 Without<MakaraDropdownOverlay>,
183 Without<MakaraImage>,
184 Without<MakaraLink>,
185 Without<MakaraModal>,
186 Without<MakaraModalBackdrop>,
187 ),
188 (
189 Without<MakaraProgressBar>,
190 Without<MakaraRadio>,
191 Without<MakaraRadioGroup>,
192 Without<MakaraRow>,
193 Without<MakaraScroll>,
194 Without<MakaraTextInput>,
195 Without<MakaraTextInputCursor>,
196 Without<MakaraSlider>,
197 Without<MakaraSliderThumb>,
198 Without<MakaraSelect>,
199 Without<MakaraSelectOverlay>,
200 )
201);
202
203#[derive(SystemParam)]
205pub struct ScrollQuery<'w, 's> {
206 pub id_class: Query<
207 'w, 's,
208 (Entity, &'static Id, &'static mut Class),
209 IsScrollOnly
210 >,
211 pub scroll_related: Query<'w, 's,
212 (
213 Entity,
214 &'static ScrollBarEntity,
215 &'static ScrollMovePanelEntity
216 ),
217 IsScrollOnly
218 >,
219 pub style: StyleQuery<'w, 's, IsScrollOnly>,
220 pub bar_style: StyleQuery<'w, 's, IsScrollBarOnly>,
221 pub children: Query<'w, 's, Option<&'static Children>, With<MakaraScrollMovePanel>>,
222 pub commands: Commands<'w, 's>
223}
224
225impl<'w, 's> ScrollQuery<'w, 's> {
226 pub fn id_match(&mut self, target_id: &str) -> bool {
227 self.id_class.iter().any(|(_, id, _)| id.0 == target_id)
228 }
229}
230
231impl<'w, 's> WidgetQuery<'w, 's> for ScrollQuery<'w, 's> {
232 type WidgetView<'a> = ScrollWidget<'a, 'w, 's> where Self: 'a;
233
234 fn get_components<'a>(&'a mut self, entity: Entity) -> Option<Self::WidgetView<'a>> {
235 let ScrollQuery {
236 id_class, scroll_related, style, bar_style, children, commands
237 } = self;
238
239 let (_, _, class) = id_class.get_mut(entity).ok()?;
240 let (_, bar_entity, panel_entity) = scroll_related.get_mut(entity).ok()?;
241
242 let bar_bundle = bar_style.query.get_mut(bar_entity.0).ok()?;
243 let (b_node, b_bg, b_border_color, b_shadow, b_z) = bar_bundle;
244
245 let style_bundle = style.query.get_mut(entity).ok()?;
246 let (node, bg, border_color, shadow, z_index) = style_bundle;
247
248 let mut entities: Vec<Entity> = Vec::new();
249 {
250 let children = children.get(panel_entity.0).ok()?;
251 if let Some(children) = children {
252 entities = children.iter().map(|e| e).collect();
253 }
254 }
255
256 return Some(ScrollWidget {
257 entity,
258 panel_entity: panel_entity.0,
259 class: class.into_inner(),
260 style: WidgetStyle {
261 node: node.into_inner(),
262 background_color: bg.into_inner(),
263 border_color: border_color.into_inner(),
264 shadow: shadow.into_inner(),
265 z_index: z_index.into_inner(),
266 },
267 bar_style: WidgetStyle {
268 node: b_node.into_inner(),
269 background_color: b_bg.into_inner(),
270 border_color: b_border_color.into_inner(),
271 shadow: b_shadow.into_inner(),
272 z_index: b_z.into_inner(),
273 },
274 child_entities: entities,
275 commands: commands
276 });
277 }
278
279 fn find_by_id<'a>(&'a mut self, target_id: &str) -> Option<Self::WidgetView<'a>> {
280 let entity = self.id_class.iter()
281 .find(|(_, id, _)| id.0 == target_id)
282 .map(|(e, _, _)| e)?;
283
284 self.get_components(entity)
285 }
286
287 fn find_by_entity<'a>(&'a mut self, target_entity: Entity) -> Option<Self::WidgetView<'a>> {
288 self.get_components(target_entity)
289 }
290
291 fn find_by_class(&self, target_class: &str) -> Vec<Entity> {
292 self.id_class.iter()
293 .filter(|(_, _, class)| class.0.split(" ").any(|word| word == target_class))
294 .map(|(e, _, _)| e)
295 .collect()
296 }
297}
298
299pub struct ScrollBundle {
301 pub id_class: IdAndClass,
302 pub style: ContainerStyle,
303 pub move_panel_style: ContainerStyle,
304 pub scroll_bar: ContainerStyle,
305}
306
307impl Default for ScrollBundle {
308 fn default() -> Self {
309 let style = ContainerStyle {
310 node: Node {
311 width: percent(100.0),
312 flex_direction: FlexDirection::Column,
313 align_items: AlignItems::FlexStart,
314 justify_content: JustifyContent::FlexStart,
315 height: Val::Percent(50.0),
316 overflow: Overflow::scroll_y(),
317 ..default()
318 },
319 background_color: BackgroundColor(Color::NONE),
320 shadow: BoxShadow::default(),
321 ..default()
322 };
323
324 let move_panel_style = ContainerStyle {
325 node: Node {
326 width: percent(100.0),
327 position_type: PositionType::Absolute,
328 left: px(0.0),
329 top: px(0.0),
330 flex_direction: FlexDirection::Column,
331 align_items: AlignItems::FlexStart,
332 justify_content: JustifyContent::FlexStart,
333 height: auto(),
334 padding: UiRect {
335 left: px(0.0),
336 right: px(0.0),
337 top: px(2.0),
338 bottom: px(2.0)
339 },
340 ..default()
341 },
342 background_color: BackgroundColor(Color::NONE),
343 shadow: BoxShadow::default(),
344 ..default()
345 };
346
347 let scroll_bar = ContainerStyle {
348 node: Node {
349 width: px(8),
350 height: px(10),
351 position_type: PositionType::Absolute,
352 right: px(0),
353 top: px(0),
354 border_radius: BorderRadius::all(px(8)),
355 ..default()
357 },
358 background_color: BackgroundColor(Color::srgba(0.2, 0.2, 0.2, 0.8)),
359 shadow: BoxShadow::default(),
360 ..default()
361 };
362
363 Self { style, move_panel_style, scroll_bar, id_class: IdAndClass::default() }
364 }
365}
366
367impl Widget for ScrollBundle {
368 fn build(mut self) -> impl Bundle {
370 process_built_in_spacing_class(&self.id_class.class, &mut self.style.node);
371 (
372 self.id_class,
373 self.style,
374 TempMovePanelStyle(self.move_panel_style),
375 TempScrollBarStyle(self.scroll_bar),
376 MakaraScroll,
377 ScrollChildrenNeedTransfer,
378 observe(on_mouse_over)
379 )
380 }
381}
382
383impl ScrollBundle {
384 pub fn bar_style(mut self, f: impl FnOnce(&mut ContainerStyle)) -> Self {
388 f(&mut self.scroll_bar);
389 self
390 }
391}
392
393impl SetContainerStyle for ScrollBundle {
394 fn container_style(&mut self) -> &mut ContainerStyle {
395 &mut self.style
396 }
397}
398
399impl SetIdAndClass for ScrollBundle {
400 fn id_and_class(&mut self) -> &mut IdAndClass {
401 &mut self.id_class
402 }
403}
404
405fn on_mouse_over(
406 mut over: On<Pointer<Over>>,
407 mut can_be_scrolled: ResMut<CanBeScrolled>
408) {
409 can_be_scrolled.entity = Some(over.entity);
410 over.propagate(false);
411}
412
413pub(crate) fn detect_scroll_children_added(
415 mut commands: Commands,
416 scrolls: Query<
417 (Entity, &Node, Option<&Children>, &TempMovePanelStyle, &TempScrollBarStyle),
418 With<ScrollChildrenNeedTransfer>
419 >,
420) {
421 for (scroll_entity, scroll_node, scroll_children, panel_style, bar_style) in scrolls.iter() {
422 let mut new_panel_style = panel_style.0.clone();
423 new_panel_style.node.align_items = scroll_node.align_items;
424 new_panel_style.node.justify_content = scroll_node.justify_content;
425
426 let panel_entity = commands.spawn((
427 new_panel_style,
428 MakaraScrollMovePanel,
429 MakaraScrollList::default(),
430 ScrollEntity(scroll_entity),
431 ))
432 .id();
433
434 let bar_entity = commands.spawn((
435 bar_style.0.clone(),
436 MakaraScrollbar,
437 ScrollEntity(scroll_entity),
438 Visibility::Hidden,
439 observe(on_scrollbar_drag),
440 observe(on_scrollbar_mouse_over),
441 observe(on_mouse_out)
442 ))
443 .id();
444
445 if let Some(children) = scroll_children {
446 let child_entities = children.iter().map(|e| e).collect::<Vec<Entity>>();
447 commands.entity(panel_entity).add_children(&child_entities);
448 }
449
450 commands.entity(panel_entity).insert(ScrollBarEntity(bar_entity));
451 commands
452 .entity(scroll_entity)
453 .remove::<ScrollChildrenNeedTransfer>()
454 .insert((ScrollMovePanelEntity(panel_entity), ScrollBarEntity(bar_entity)))
455 .add_children(&[panel_entity, bar_entity]);
456 }
457}
458
459pub(crate) fn detect_scroll_height_change(
460 scrolls: Query<
461 (&ComputedNode, &ScrollMovePanelEntity, &ScrollBarEntity),
462 Changed<ComputedNode>
463 >,
464 panels: Query<&ComputedNode, With<MakaraScrollMovePanel>>,
465 mut bars: Query<(&mut Visibility, &mut Node), IsScrollBarOnly>
466) {
467 for (scroll_computed, panel_entity, bar_entity) in scrolls.iter() {
468 if let Ok(panel_computed) = panels.get(panel_entity.0) {
469 if let Ok((mut vis, mut bar_node)) = bars.get_mut(bar_entity.0) {
470 let scroll_height = scroll_computed.size().y * scroll_computed.inverse_scale_factor();
471 let panel_height = panel_computed.size().y * panel_computed.inverse_scale_factor();
472
473 if panel_height <= scroll_height {
474 *vis = Visibility::Hidden;
475 }
476 else {
477 let bar_height = (scroll_height / panel_height) * scroll_height;
478 bar_node.height = px(bar_height);
479 *vis = Visibility::Visible;
480 }
481 }
482 }
483 }
484}
485
486pub(crate) fn detect_move_panel_height_change(
487 panels: Query<
488 (&ComputedNode, &ScrollEntity),
489 (With<MakaraScrollMovePanel>, Changed<ComputedNode>)
490 >,
491 scrolls: Query<(&ComputedNode, &ScrollBarEntity)>,
492 mut bars: Query<(&mut Visibility, &mut Node), IsScrollBarOnly>
493) {
494 for (panel_computed, scroll_entity) in panels.iter() {
495 if let Ok((scroll_computed, bar_entity)) = scrolls.get(scroll_entity.0) {
496 if let Ok((mut vis, mut bar_node)) = bars.get_mut(bar_entity.0) {
497 let scroll_height = scroll_computed.size().y * scroll_computed.inverse_scale_factor();
498 let panel_height = panel_computed.size().y * panel_computed.inverse_scale_factor();
499
500 if panel_height <= scroll_height {
501 *vis = Visibility::Hidden;
502 }
503 else {
504 let bar_height = (scroll_height / panel_height) * scroll_height;
505 bar_node.height = px(bar_height);
506 *vis = Visibility::Visible;
507 }
508 }
509 }
510 }
511}
512
513fn on_scrollbar_drag(
514 drag: On<Pointer<Drag>>,
515 scrolls: Query<(&ComputedNode, &ScrollMovePanelEntity)>,
516 mut bars: Query<(&mut Node, &ComputedNode, &ChildOf), Without<MakaraScrollList>>,
517 mut panels: Query<(&mut Node, &ComputedNode, &mut MakaraScrollList)>,
518 mut commands: Commands,
519) {
520 let Ok((mut bar_node, bar_computed, bar_parent)) = bars.get_mut(drag.entity) else {
521 return
522 };
523
524 let Ok((scroll_computed, panel_entity)) = scrolls.get(bar_parent.0) else {
525 return
526 };
527
528 let Ok((mut panel_node, panel_computed, mut scroll_list)) = panels.get_mut(panel_entity.0) else {
529 return
530 };
531
532 let scale = bar_computed.inverse_scale_factor();
533 let container_height = scroll_computed.size().y * scale;
534 let panel_height = panel_computed.size().y * scale;
535 let bar_height = bar_computed.size().y * scale;
536
537 let max_scroll = (panel_height - container_height).max(0.0);
538 let track_space = (container_height - bar_height).max(1.0); let current_bar_top = if let Val::Px(p) = bar_node.top {
541 p
542 }
543 else {
544 0.0
545 };
546
547 let new_bar_top = (current_bar_top + drag.delta.y).clamp(0.0, track_space);
548 bar_node.top = Val::Px(new_bar_top);
549
550 let scroll_ratio = new_bar_top / track_space;
552
553 scroll_list.position = -(scroll_ratio * max_scroll);
554 panel_node.top = Val::Px(scroll_list.position);
555
556 commands.trigger(Scrolling {
557 entity: bar_parent.0,
558 position: scroll_list.position,
559 });
560}
561
562fn on_scrollbar_mouse_over(
563 mut over: On<Pointer<Over>>,
564 mut commands: Commands,
565 window: Single<Entity, With<Window>>,
566) {
567 commands.entity(*window).insert(CursorIcon::System(SystemCursorIcon::Pointer));
568 over.propagate(false);
569}
570
571pub(crate) fn handle_scrolling(
572 mut mouse_wheel: MessageReader<MouseWheel>,
573 scrolls: Query<(&ComputedNode, &ScrollMovePanelEntity, &ScrollBarEntity)>,
574 mut panels: Query<
575 (&mut Node, &ComputedNode, &mut MakaraScrollList),
576 With<MakaraScrollMovePanel>
577 >,
578 mut bars: Query<
579 (&mut Node, &ComputedNode),
580 (With<MakaraScrollbar>, Without<MakaraScrollMovePanel>)
581 >,
582 mut commands: Commands,
583 can_be_scrolled: Res<CanBeScrolled>,
584) {
585 for e in mouse_wheel.read() {
586 let Some(hovered) = can_be_scrolled.entity else {
587 continue;
588 };
589
590 let Ok((scroll_computed, panel_entity, bar_entity)) = scrolls.get(hovered) else {
591 continue;
592 };
593
594 let Ok((mut bar_node, bar_computed)) = bars.get_mut(bar_entity.0) else {
595 continue;
596 };
597
598 if let Ok((mut panel_node, panel_computed, mut scroll_list)) = panels.get_mut(panel_entity.0) {
600 let dy = match e.unit {
601 MouseScrollUnit::Line => e.y * scroll_list.scroll_height,
602 MouseScrollUnit::Pixel => e.y,
603 };
604
605 let panel_height = panel_computed.size().y * panel_computed.inverse_scale_factor();
607 let container_height = scroll_computed.size().y * scroll_computed.inverse_scale_factor();
608 let max_scroll = (panel_height - container_height).max(0.0);
609
610 scroll_list.position = (scroll_list.position + dy).clamp(-max_scroll, 0.0);
611 panel_node.top = px(scroll_list.position);
612
613 commands.trigger(Scrolling {
614 entity: hovered,
615 position: scroll_list.position
616 });
617
618 if max_scroll > 0.0 {
619 let scroll_ratio = scroll_list.position.abs() / max_scroll;
622
623 let bar_height = bar_computed.size().y * bar_computed.inverse_scale_factor();
625 let track_space = container_height - bar_height;
626
627 bar_node.top = px(scroll_ratio * track_space);
629 } else {
630 bar_node.top = px(0.0);
631 }
632 }
633 }
634}
635
636pub(crate) fn detect_scroll_built(
637 mut commands: Commands,
638 q: Query<Entity, Added<MakaraScroll>>
639) {
640 for entity in q.iter() {
641 commands.trigger(WidgetBuilt {
642 entity
643 });
644 }
645}
646
647
648pub(crate) fn detect_scroll_class_change_for_built_in(
649 mut scrolls: Query<(&Class, &mut Node), IsScrollOnly>
650) {
651 for (class, mut node) in scrolls.iter_mut() {
652 process_built_in_spacing_class(class, &mut node);
653 }
654}
655
656pub fn scroll() -> ScrollBundle {
658 ScrollBundle::default()
659}
660
661pub(crate) fn can_run_scroll_systems(q: Query<&MakaraScroll>) -> bool {
662 q.count() > 0
663}