1use std::collections::VecDeque;
2
3use bevy::{prelude::*, ui::FocusPolicy};
4
5use sickle_ui_scaffold::{prelude::*, ui_commands::UpdateTextExt};
6
7use crate::widgets::layout::{
8 container::UiContainerExt,
9 label::{LabelConfig, UiLabelExt},
10 panel::UiPanelExt,
11 scroll_view::{ScrollView, ScrollViewLayoutUpdate, UiScrollViewExt},
12};
13
14const DROPDOWN_PANEL_Z_INDEX: usize = 11000;
15
16#[cfg(feature = "observable")]
17#[derive(Event, Copy, Clone, Debug)]
18pub struct DropdownChanged {
19 pub value: Option<usize>,
20}
21
22pub struct DropdownPlugin;
23
24impl Plugin for DropdownPlugin {
25 fn build(&self, app: &mut App) {
26 app.add_plugins((
27 ComponentThemePlugin::<Dropdown>::default(),
28 ComponentThemePlugin::<DropdownOption>::default(),
29 ))
30 .add_systems(
31 Update,
32 (
33 handle_option_press,
34 update_dropdown_label,
35 handle_click_or_touch,
36 update_drowdown_pseudo_state,
37 update_dropdown_panel_visibility,
38 )
39 .chain()
40 .after(FluxInteractionUpdate)
41 .before(ScrollViewLayoutUpdate),
42 );
43
44 #[cfg(feature = "observable")]
45 app.add_event::<DropdownChanged>();
46 }
47}
48
49fn update_dropdown_label(
50 mut q_dropdowns: Query<(&mut Dropdown, &DropdownOptions), Changed<Dropdown>>,
51 mut commands: Commands,
52) {
53 for (mut dropdown, options) in &mut q_dropdowns {
54 if let Some(value) = dropdown.value {
55 if value >= options.0.len() {
56 dropdown.value = None;
57 }
58 }
59
60 let text = if let Some(value) = dropdown.value {
61 options.0[value].clone()
62 } else {
63 String::from("---")
64 };
65
66 commands.entity(dropdown.label).update_text(text);
67 }
68}
69
70fn handle_click_or_touch(
71 r_mouse: Res<ButtonInput<MouseButton>>,
72 r_touches: Res<Touches>,
73 mut q_dropdowns: Query<(Entity, &mut Dropdown, &FluxInteraction)>,
74) {
75 if r_mouse.any_just_released([MouseButton::Left, MouseButton::Middle, MouseButton::Right])
76 || r_touches.any_just_released()
77 {
78 let mut open: Option<Entity> = None;
79 for (entity, _, interaction) in &mut q_dropdowns {
80 if *interaction == FluxInteraction::Released {
81 open = entity.into();
82 break;
83 }
84 }
85
86 for (entity, mut dropdown, _) in &mut q_dropdowns {
87 if let Some(open_dropdown) = open {
88 if entity == open_dropdown {
89 dropdown.is_open = !dropdown.is_open;
90 } else if dropdown.is_open {
91 dropdown.is_open = false;
92 }
93 } else if dropdown.is_open {
94 dropdown.is_open = false;
95 }
96 }
97 }
98}
99
100fn handle_option_press(
101 q_options: Query<(&DropdownOption, &FluxInteraction), Changed<FluxInteraction>>,
102 mut q_dropdown: Query<&mut Dropdown>,
103 mut commands: Commands,
104) {
105 for (option, interaction) in &q_options {
106 if *interaction == FluxInteraction::Released {
107 let Ok(mut dropdown) = q_dropdown.get_mut(option.dropdown) else {
108 continue;
109 };
110
111 dropdown.value = option.option.into();
112
113 #[cfg(feature = "observable")]
114 commands.trigger_targets(
115 DropdownChanged {
116 value: dropdown.value,
117 },
118 option.dropdown,
119 );
120 }
121 }
122}
123
124fn update_drowdown_pseudo_state(
125 q_panels: Query<(&DropdownPanel, &PseudoStates), Changed<PseudoStates>>,
126 mut commands: Commands,
127) {
128 for (panel, states) in &q_panels {
129 if states.has(&PseudoState::Visible) {
130 commands
131 .entity(panel.dropdown)
132 .add_pseudo_state(PseudoState::Open);
133 } else {
134 commands
135 .entity(panel.dropdown)
136 .remove_pseudo_state(PseudoState::Open);
137 }
138 }
139}
140
141fn update_dropdown_panel_visibility(
142 q_dropdowns: Query<&Dropdown, Changed<Dropdown>>,
143 mut q_scroll_view: Query<&mut ScrollView>,
144 mut commands: Commands,
145) {
146 for dropdown in &q_dropdowns {
147 if dropdown.is_open {
148 commands
149 .style_unchecked(dropdown.panel)
150 .display(Display::Flex)
151 .visibility(Visibility::Inherited)
152 .height(Val::Px(0.));
153
154 let Ok(mut scroll_view) = q_scroll_view.get_mut(dropdown.scroll_view) else {
155 continue;
156 };
157
158 scroll_view.disabled = true;
159 } else {
160 commands
161 .style_unchecked(dropdown.panel)
162 .display(Display::None)
163 .visibility(Visibility::Hidden);
164 }
165 }
166}
167
168#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
169pub enum DropdownPanelAnchor {
170 TopLeft,
171 TopRight,
172 #[default]
173 BottomLeft,
174 BottomRight,
175}
176
177#[derive(Clone, Copy, Debug, Default)]
178pub struct DropdownPanelPlacement {
179 pub anchor: DropdownPanelAnchor,
180 pub top: Val,
181 pub right: Val,
182 pub bottom: Val,
183 pub left: Val,
184 pub width: Val,
185 pub height: Val,
186 pub panel_width: f32,
187 pub button_width: f32,
188 pub wider_than_button: bool,
189}
190
191#[derive(Component, Clone, Debug, Default, Reflect)]
192#[reflect(Component)]
193pub struct DropdownOptions(Vec<String>);
194
195impl DropdownOptions {
196 pub fn labels(&self) -> &Vec<String> {
197 &self.0
198 }
199}
200
201#[derive(Component, Debug, Reflect)]
202#[reflect(Component)]
203pub struct DropdownOption {
204 dropdown: Entity,
205 label: Entity,
206 option: usize,
207}
208
209impl Default for DropdownOption {
210 fn default() -> Self {
211 Self {
212 dropdown: Entity::PLACEHOLDER,
213 label: Entity::PLACEHOLDER,
214 option: Default::default(),
215 }
216 }
217}
218
219impl UiContext for DropdownOption {
220 fn get(&self, target: &str) -> Result<Entity, String> {
221 match target {
222 DropdownOption::LABEL => Ok(self.label),
223 _ => Err(format!(
224 "{} doesn't exist for DropdownOption. Possible contexts: {:?}",
225 target,
226 Vec::from_iter(self.contexts())
227 )),
228 }
229 }
230
231 fn contexts(&self) -> impl Iterator<Item = &str> + '_ {
232 [DropdownOption::LABEL].into_iter()
233 }
234}
235
236impl DefaultTheme for DropdownOption {
237 fn default_theme() -> Option<Theme<DropdownOption>> {
238 DropdownOption::theme().into()
239 }
240}
241
242impl DropdownOption {
243 pub const LABEL: &'static str = "Label";
244
245 pub fn dropdown(&self) -> Entity {
246 self.dropdown
247 }
248
249 pub fn option(&self) -> usize {
250 self.option
251 }
252
253 pub fn theme() -> Theme<DropdownOption> {
254 let base_theme = PseudoTheme::deferred(None, DropdownOption::primary_style);
255
256 Theme::new(vec![base_theme])
257 }
258
259 fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
260 let theme_spacing = theme_data.spacing;
261 let colors = theme_data.colors();
262 let font = theme_data
263 .text
264 .get(FontStyle::Body, FontScale::Medium, FontType::Regular);
265
266 style_builder
267 .align_items(AlignItems::Center)
268 .min_width(Val::Percent(100.))
269 .padding(UiRect::axes(
270 Val::Px(theme_spacing.gaps.medium),
271 Val::Px(theme_spacing.gaps.medium),
272 ))
273 .margin(UiRect::bottom(Val::Px(theme_spacing.gaps.tiny)))
274 .animated()
275 .background_color(AnimatedVals {
276 idle: colors.container(Container::Primary),
277 hover: colors.accent(Accent::Primary).into(),
278 ..default()
279 })
280 .copy_from(theme_data.interaction_animation);
281
282 style_builder
283 .switch_target(DropdownOption::LABEL)
284 .sized_font(font)
285 .animated()
286 .font_color(AnimatedVals {
287 idle: colors.on(OnColor::PrimaryContainer),
288 hover: colors.on(OnColor::Primary).into(),
289 ..default()
290 })
291 .copy_from(theme_data.interaction_animation);
292 }
293}
294
295#[derive(Component, Debug, Reflect)]
296#[reflect(Component)]
297pub struct DropdownPanel {
298 dropdown: Entity,
299}
300
301impl Default for DropdownPanel {
302 fn default() -> Self {
303 Self {
304 dropdown: Entity::PLACEHOLDER,
305 }
306 }
307}
308
309#[derive(Component, Debug, Reflect)]
310#[reflect(Component)]
311pub struct Dropdown {
312 value: Option<usize>,
313 label: Entity,
314 icon: Entity,
315 panel: Entity,
316 scroll_view: Entity,
317 scroll_view_content: Entity,
318 is_open: bool,
319}
320
321impl Default for Dropdown {
322 fn default() -> Self {
323 Self {
324 value: Default::default(),
325 label: Entity::PLACEHOLDER,
326 icon: Entity::PLACEHOLDER,
327 panel: Entity::PLACEHOLDER,
328 scroll_view: Entity::PLACEHOLDER,
329 scroll_view_content: Entity::PLACEHOLDER,
330 is_open: false,
331 }
332 }
333}
334
335impl UiContext for Dropdown {
336 fn get(&self, target: &str) -> Result<Entity, String> {
337 match target {
338 Dropdown::LABEL => Ok(self.label),
339 Dropdown::ICON => Ok(self.icon),
340 Dropdown::PANEL => Ok(self.panel),
341 Dropdown::SCROLL_VIEW => Ok(self.scroll_view),
342 Dropdown::SCROLL_VIEW_CONTENT => Ok(self.scroll_view_content),
343 _ => Err(format!(
344 "{} doesn't exist for Dropdown. Possible contexts: {:?}",
345 target,
346 Vec::from_iter(self.contexts())
347 )),
348 }
349 }
350
351 fn contexts(&self) -> impl Iterator<Item = &str> + '_ {
352 [
353 Dropdown::LABEL,
354 Dropdown::ICON,
355 Dropdown::PANEL,
356 Dropdown::SCROLL_VIEW,
357 Dropdown::SCROLL_VIEW_CONTENT,
358 ]
359 .into_iter()
360 }
361}
362
363impl DefaultTheme for Dropdown {
364 fn default_theme() -> Option<Theme<Dropdown>> {
365 Dropdown::theme().into()
366 }
367}
368
369impl Dropdown {
370 pub const LABEL: &'static str = "Label";
371 pub const ICON: &'static str = "Icon";
372 pub const PANEL: &'static str = "Panel";
373 pub const SCROLL_VIEW: &'static str = "ScrollView";
374 pub const SCROLL_VIEW_CONTENT: &'static str = "ScrollViewContent";
375
376 pub fn value(&self) -> Option<usize> {
377 self.value
378 }
379
380 pub fn set_value(&mut self, value: impl Into<Option<usize>>) {
381 let value = value.into();
382 if self.value != value {
383 self.value = value;
384 }
385 }
386
387 pub fn options_container(&self) -> Entity {
388 self.scroll_view_content
389 }
390
391 pub fn theme() -> Theme<Dropdown> {
392 let base_theme = PseudoTheme::deferred(None, Dropdown::primary_style);
393 let open_theme = PseudoTheme::deferred_world(vec![PseudoState::Open], Dropdown::open_style);
394
395 Theme::new(vec![base_theme, open_theme])
396 }
397
398 fn primary_style(style_builder: &mut StyleBuilder, theme_data: &ThemeData) {
399 let theme_spacing = theme_data.spacing;
400 let colors = theme_data.colors();
401 let font = theme_data
402 .text
403 .get(FontStyle::Body, FontScale::Medium, FontType::Regular);
404
405 style_builder
406 .align_self(AlignSelf::Start)
407 .align_items(AlignItems::Center)
408 .justify_content(JustifyContent::SpaceBetween)
409 .height(Val::Px(theme_spacing.areas.small))
410 .padding(UiRect::axes(
411 Val::Px(theme_spacing.gaps.medium),
412 Val::Px(theme_spacing.gaps.extra_small),
413 ))
414 .border(UiRect::all(Val::Px(0.)))
415 .border_color(colors.accent(Accent::Outline))
416 .border_radius(BorderRadius::all(Val::Px(
417 theme_spacing.corners.extra_small,
418 )))
419 .animated()
420 .background_color(AnimatedVals {
421 idle: colors.accent(Accent::Primary),
422 hover: colors.container(Container::Primary).into(),
423 ..default()
424 })
425 .copy_from(theme_data.interaction_animation);
426
427 style_builder
428 .switch_target(Dropdown::LABEL)
429 .sized_font(font)
430 .animated()
431 .font_color(AnimatedVals {
432 idle: colors.on(OnColor::Primary),
433 hover: colors.on(OnColor::PrimaryContainer).into(),
434 ..default()
435 })
436 .copy_from(theme_data.interaction_animation);
437
438 style_builder
439 .switch_target(Dropdown::ICON)
440 .size(Val::Px(theme_spacing.icons.small))
441 .margin(UiRect::left(Val::Px(theme_spacing.gaps.large)))
442 .icon(
443 theme_data
444 .icons
445 .expand_more
446 .with(colors.on(OnColor::Primary), theme_spacing.icons.small),
447 )
448 .animated()
449 .font_color(AnimatedVals {
450 idle: colors.on(OnColor::Primary),
451 hover: colors.on(OnColor::PrimaryContainer).into(),
452 ..default()
453 })
454 .copy_from(theme_data.interaction_animation);
455
456 style_builder
457 .switch_target(Dropdown::PANEL)
458 .position_type(PositionType::Absolute)
459 .min_width(Val::Percent(100.))
460 .max_height(Val::Px(theme_spacing.areas.extra_large))
461 .top(Val::Px(theme_spacing.areas.medium))
462 .z_index(ZIndex::Global(DROPDOWN_PANEL_Z_INDEX as i32))
463 .border(UiRect::all(Val::Px(theme_spacing.gaps.tiny)))
464 .border_color(Color::NONE)
465 .background_color(colors.container(Container::Primary));
466
467 style_builder
468 .switch_target(Dropdown::SCROLL_VIEW_CONTENT)
469 .margin(UiRect::px(
470 0.,
471 theme_spacing.scroll_bar_size,
472 0.,
473 theme_spacing.scroll_bar_size,
474 ));
475 }
476
477 fn open_style(style_builder: &mut StyleBuilder, entity: Entity, _: &Dropdown, world: &World) {
478 let placement = match Dropdown::panel_placement_for(entity, world) {
479 Ok(placement) => placement,
480 Err(msg) => {
481 error!("Error placing Dropdown panel: {}", msg);
482 return;
483 }
484 };
485
486 let theme_data = world.resource::<ThemeData>();
487 let theme_spacing = theme_data.spacing;
488 let colors = theme_data.colors();
489 let enter_animation = theme_data.enter_animation.clone();
490 let corner_from_width =
491 placement.panel_width < placement.button_width - theme_spacing.corners.extra_small;
492 let extra_small_border = theme_spacing.borders.extra_small;
493 let extra_small_corner = theme_spacing.corners.extra_small;
494 let maybe_button_corner = match corner_from_width {
495 true => extra_small_corner,
496 false => 0.,
497 };
498
499 style_builder.background_color(colors.container(Container::Primary));
500
501 match placement.anchor {
502 DropdownPanelAnchor::TopLeft => {
503 style_builder
504 .border(UiRect::top(Val::Px(extra_small_border)))
505 .border_radius(BorderRadius::px(
506 0.,
507 maybe_button_corner,
508 extra_small_corner,
509 extra_small_corner,
510 ));
511 }
512 DropdownPanelAnchor::TopRight => {
513 style_builder
514 .border(UiRect::top(Val::Px(extra_small_border)))
515 .border_radius(BorderRadius::px(
516 maybe_button_corner,
517 0.,
518 extra_small_corner,
519 extra_small_corner,
520 ));
521 }
522 DropdownPanelAnchor::BottomLeft => {
523 style_builder
524 .border(UiRect::bottom(Val::Px(extra_small_border)))
525 .border_radius(BorderRadius::px(
526 extra_small_corner,
527 extra_small_corner,
528 maybe_button_corner,
529 0.,
530 ));
531 }
532 DropdownPanelAnchor::BottomRight => {
533 style_builder
534 .border(UiRect::bottom(Val::Px(extra_small_border)))
535 .border_radius(BorderRadius::px(
536 extra_small_corner,
537 extra_small_corner,
538 0.,
539 maybe_button_corner,
540 ));
541 }
542 }
543
544 style_builder
545 .switch_target(Dropdown::LABEL)
546 .font_color(colors.on(OnColor::PrimaryContainer));
547 style_builder
548 .switch_target(Dropdown::ICON)
549 .font_color(colors.on(OnColor::PrimaryContainer));
550
551 style_builder
552 .switch_target(Dropdown::PANEL)
553 .top(placement.top)
554 .right(placement.right)
555 .bottom(placement.bottom)
556 .left(placement.left)
557 .width(placement.width)
558 .border_color(colors.accent(Accent::Shadow))
559 .animated()
560 .height(AnimatedVals {
561 idle: placement.height,
562 enter_from: Val::Px(0.).into(),
563 ..default()
564 })
565 .copy_from(enter_animation);
566
567 let maybe_border = match placement.wider_than_button {
568 true => extra_small_border,
569 false => 0.,
570 };
571
572 let maybe_corner = match placement.wider_than_button {
573 true => extra_small_corner,
574 false => 0.,
575 };
576
577 match placement.anchor {
578 DropdownPanelAnchor::TopLeft => {
579 style_builder
580 .switch_target(Dropdown::PANEL)
581 .border(UiRect::px(0., maybe_border, extra_small_border, 0.))
582 .border_radius(BorderRadius::px(
583 extra_small_corner,
584 extra_small_corner,
585 maybe_corner,
586 0.,
587 ));
588 }
589 DropdownPanelAnchor::TopRight => {
590 style_builder
591 .switch_target(Dropdown::PANEL)
592 .border(UiRect::px(maybe_border, 0., extra_small_border, 0.))
593 .border_radius(BorderRadius::px(
594 extra_small_corner,
595 extra_small_corner,
596 0.,
597 maybe_corner,
598 ));
599 }
600 DropdownPanelAnchor::BottomLeft => {
601 style_builder
602 .switch_target(Dropdown::PANEL)
603 .border(UiRect::px(0., maybe_border, 0., extra_small_border))
604 .border_radius(BorderRadius::px(
605 0.,
606 maybe_corner,
607 extra_small_corner,
608 extra_small_corner,
609 ));
610 }
611 DropdownPanelAnchor::BottomRight => {
612 style_builder
613 .switch_target(Dropdown::PANEL)
614 .border(UiRect::px(maybe_border, 0., 0., extra_small_border))
615 .border_radius(BorderRadius::px(
616 maybe_corner,
617 0.,
618 extra_small_corner,
619 extra_small_corner,
620 ));
621 }
622 }
623
624 style_builder
625 .switch_target(Dropdown::SCROLL_VIEW)
626 .animated()
627 .tracked_style_state(TrackedStyleState::default_vals())
628 .copy_from(enter_animation);
629 }
630
631 pub fn panel_placement_for(
632 entity: Entity,
633 world: &World,
634 ) -> Result<DropdownPanelPlacement, String> {
635 let Some(dropdown) = world.get::<Dropdown>(entity) else {
636 return Err("Entity has no Dropdown component".into());
637 };
638 let dropdown_panel = dropdown.panel;
639 let scroll_view_content = dropdown.scroll_view_content;
640
641 let dropdown_node = world.get::<Node>(entity).unwrap();
643 let dropdown_size = dropdown_node.unrounded_size();
644 let dropdown_borders = UiUtils::border_as_px(entity, world);
645 let panel_borders = UiUtils::border_as_px(dropdown_panel, world);
646
647 let Some(option_list) = world.get::<Children>(scroll_view_content) else {
649 return Err("Dropdown has no options".into());
650 };
651
652 let option_list: Vec<Entity> = option_list.iter().map(|child| *child).collect();
653 let mut five_children_height = panel_borders.x + panel_borders.z;
654 let mut counted = 0;
655 for child in option_list {
656 let Some(option_node) = world.get::<Node>(child) else {
657 continue;
658 };
659
660 if counted < 5 {
661 five_children_height += option_node.unrounded_size().y;
662
663 let margin_sizes = UiUtils::margin_as_px(child, world);
664 five_children_height += margin_sizes.x + margin_sizes.z;
665 counted += 1;
666 }
667 }
668
669 let (container_size, tl_corner) = UiUtils::container_size_and_offset(entity, world);
670 let halfway_point = container_size / 2.;
671 let space_below = (container_size - tl_corner - dropdown_size).y;
672
673 let anchor = if tl_corner.x > halfway_point.x {
674 if space_below < five_children_height {
675 DropdownPanelAnchor::TopRight
676 } else {
677 DropdownPanelAnchor::BottomRight
678 }
679 } else {
680 if space_below < five_children_height {
681 DropdownPanelAnchor::TopLeft
682 } else {
683 DropdownPanelAnchor::BottomLeft
684 }
685 };
686
687 let panel_size_limit = match anchor {
688 DropdownPanelAnchor::TopLeft => Vec2::new(container_size.x - tl_corner.x, tl_corner.y),
689 DropdownPanelAnchor::TopRight => Vec2::new(tl_corner.x + dropdown_size.x, tl_corner.y),
690 DropdownPanelAnchor::BottomLeft => Vec2::new(
691 container_size.x - tl_corner.x,
692 container_size.y - (tl_corner.y + dropdown_size.y),
693 ),
694 DropdownPanelAnchor::BottomRight => Vec2::new(
695 tl_corner.x + dropdown_size.x,
696 container_size.y - (tl_corner.y + dropdown_size.y),
697 ),
698 }
699 .max(Vec2::ZERO);
700
701 let panel_width = (world
703 .get::<Node>(scroll_view_content)
704 .unwrap()
705 .unrounded_size()
706 .x
707 + panel_borders.y
708 + panel_borders.w)
709 .clamp(0., panel_size_limit.x.max(0.));
710 let idle_height = five_children_height.clamp(0., panel_size_limit.y.max(0.));
711
712 let (top, right, bottom, left) = match anchor {
713 DropdownPanelAnchor::TopLeft => (
714 Val::Auto,
715 Val::Auto,
716 Val::Px(dropdown_size.y - dropdown_borders.z),
717 Val::Px(-dropdown_borders.w),
718 ),
719 DropdownPanelAnchor::TopRight => (
720 Val::Auto,
721 Val::Px(-dropdown_borders.y),
722 Val::Px(dropdown_size.y - dropdown_borders.z),
723 Val::Auto,
724 ),
725 DropdownPanelAnchor::BottomLeft => (
726 Val::Px(dropdown_size.y - dropdown_borders.x),
727 Val::Auto,
728 Val::Auto,
729 Val::Px(-dropdown_borders.w),
730 ),
731 DropdownPanelAnchor::BottomRight => (
732 Val::Px(dropdown_size.y - dropdown_borders.x),
733 Val::Px(-dropdown_borders.y),
734 Val::Auto,
735 Val::Auto,
736 ),
737 };
738
739 Ok(DropdownPanelPlacement {
740 anchor,
741 top,
742 right,
743 bottom,
744 left,
745 width: Val::Px(panel_width),
746 height: Val::Px(idle_height),
747 panel_width,
748 button_width: dropdown_size.x,
749 wider_than_button: panel_width > dropdown_size.x,
750 })
751 }
752
753 fn button(options: Vec<String>) -> impl Bundle {
754 (
755 Name::new("Dropdown"),
756 ButtonBundle {
757 style: Style {
758 flex_direction: FlexDirection::Row,
759 overflow: Overflow::visible(),
760 ..default()
761 },
762 ..default()
763 },
764 TrackedInteraction::default(),
765 LockedStyleAttributes::from_vec(vec![
766 LockableStyleAttribute::FlexDirection,
767 LockableStyleAttribute::Overflow,
768 ]),
769 DropdownOptions(options),
770 )
771 }
772
773 fn button_icon() -> impl Bundle {
774 (
775 Name::new("Dropdown Icon"),
776 ImageBundle {
777 focus_policy: FocusPolicy::Pass,
778 ..default()
779 },
780 BorderColor::default(),
781 LockedStyleAttributes::lock(LockableStyleAttribute::FocusPolicy),
782 )
783 }
784
785 fn option_bundle(option: usize) -> impl Bundle {
786 (
787 Name::new(format!("Option {}", option)),
788 ButtonBundle {
789 focus_policy: FocusPolicy::Pass,
790 ..default()
791 },
792 TrackedInteraction::default(),
793 LockedStyleAttributes::lock(LockableStyleAttribute::FocusPolicy),
794 )
795 }
796}
797
798pub trait UiDropdownExt {
799 fn dropdown(
800 &mut self,
801 options: Vec<impl Into<String>>,
802 value: impl Into<Option<usize>>,
803 ) -> UiBuilder<Entity>;
804}
805
806impl UiDropdownExt for UiBuilder<'_, Entity> {
807 fn dropdown(
812 &mut self,
813 options: Vec<impl Into<String>>,
814 value: impl Into<Option<usize>>,
815 ) -> UiBuilder<Entity> {
816 let mut label_id = Entity::PLACEHOLDER;
817 let mut icon_id = Entity::PLACEHOLDER;
818 let mut panel_id = Entity::PLACEHOLDER;
819 let mut scroll_view_id = Entity::PLACEHOLDER;
820 let mut scroll_view_content_id = Entity::PLACEHOLDER;
821
822 let option_count = options.len();
823 let mut string_options: Vec<String> = Vec::with_capacity(option_count);
824 let mut queue = VecDeque::from(options);
825 for _ in 0..option_count {
826 let label: String = queue.pop_front().unwrap().into();
827 string_options.push(label);
828 }
829
830 let mut dropdown = self.container(Dropdown::button(string_options.clone()), |builder| {
831 let dropdown_id = builder.id();
832 label_id = builder.label(LabelConfig::default()).id();
833 icon_id = builder.spawn(Dropdown::button_icon()).id();
834 panel_id = builder
835 .panel("Dropdown Options".into(), |container| {
836 scroll_view_id = container
837 .scroll_view(None, |scroll_view| {
838 scroll_view_content_id = scroll_view.id();
839
840 for (index, label) in string_options.iter().enumerate() {
841 let mut label_id = Entity::PLACEHOLDER;
842 scroll_view.container(Dropdown::option_bundle(index), |option| {
843 label_id = option
844 .label(LabelConfig {
845 label: label.clone(),
846 ..default()
847 })
848 .id();
849
850 option.insert(DropdownOption {
851 dropdown: dropdown_id,
852 option: index,
853 label: label_id,
854 });
855 });
856 }
857 })
858 .insert(TrackedStyleState::default())
859 .id();
860 })
861 .insert((
862 DropdownPanel {
863 dropdown: dropdown_id,
864 },
865 LockedStyleAttributes::from_vec(vec![
866 LockableStyleAttribute::Visibility,
867 LockableStyleAttribute::Display,
868 LockableStyleAttribute::FocusPolicy,
869 ]),
870 PseudoStates::default(),
871 VisibilityToPseudoState,
872 ))
873 .style_unchecked()
874 .focus_policy(bevy::ui::FocusPolicy::Block)
875 .id();
876 });
877
878 dropdown.insert(Dropdown {
879 value: value.into(),
880 label: label_id,
881 icon: icon_id,
882 panel: panel_id,
883 scroll_view: scroll_view_id,
884 scroll_view_content: scroll_view_content_id,
885 ..default()
886 });
887
888 dropdown
889 }
890}