1use std::ops::DerefMut;
2
3use bevy::{
4 prelude::*,
5 ui::{ContentSize, FocusPolicy, RelativeCursorPosition},
6 window::{PrimaryWindow, WindowResized},
7};
8
9use sickle_ui_scaffold::{prelude::*, ui_commands::RefreshThemeExt};
10
11use crate::widgets::layout::{
12 container::UiContainerExt,
13 label::{LabelConfig, SetLabelTextExt, UiLabelExt},
14 panel::UiPanelExt,
15 resize_handles::{ResizeDirection, ResizeHandle, UiResizeHandlesExt},
16 scroll_view::UiScrollViewExt,
17};
18
19use super::column::UiColumnExt;
20
21const MIN_PANEL_SIZE: Vec2 = Vec2 { x: 150., y: 100. };
22const MIN_FLOATING_PANEL_Z_INDEX: usize = 1000;
23const PRIORITY_FLOATING_PANEL_Z_INDEX: usize = 10000;
24const WINDOW_RESIZE_PADDING: f32 = 20.;
25
26pub struct FloatingPanelPlugin;
27
28impl Plugin for FloatingPanelPlugin {
29 fn build(&self, app: &mut App) {
30 app.configure_sets(
31 Update,
32 FloatingPanelUpdate
33 .after(DroppableUpdate)
34 .after(FluxInteractionUpdate),
35 )
36 .add_plugins(ComponentThemePlugin::<FloatingPanel>::default())
37 .add_systems(PreUpdate, update_floating_panel_panel_id)
38 .add_systems(
39 Update,
40 (
41 index_floating_panels.run_if(panel_added),
42 process_panel_close_pressed,
43 process_panel_fold_pressed,
44 update_panel_size_on_resize,
45 update_panel_on_title_drag,
46 handle_window_resize.run_if(window_resized),
47 update_panel_layout,
48 touch_new_floating_panels.run_if(panel_added),
49 )
50 .chain()
51 .in_set(FloatingPanelUpdate),
52 );
53 }
54}
55
56#[derive(SystemSet, Clone, Eq, Debug, Hash, PartialEq)]
57pub struct FloatingPanelUpdate;
58
59fn update_floating_panel_panel_id(
64 mut q_floating_panels: Query<
65 (Entity, &mut FloatingPanel, &UpdateFloatingPanelPanelId),
66 Added<UpdateFloatingPanelPanelId>,
67 >,
68 mut commands: Commands,
69) {
70 for (entity, mut floating_panel, update_ref) in &mut q_floating_panels {
71 commands
72 .entity(entity)
73 .remove::<UpdateFloatingPanelPanelId>();
74
75 if update_ref.panel_id == floating_panel.content_panel {
76 warn!("Tried setting floating panel id to its current panel!");
77 continue;
78 }
79
80 commands
81 .entity(floating_panel.content_panel)
82 .despawn_recursive();
83
84 commands
85 .entity(update_ref.panel_id)
86 .set_parent(floating_panel.content_panel_container);
87
88 commands.style(update_ref.panel_id).show();
89
90 floating_panel.content_panel = update_ref.panel_id;
91 commands.entity(entity).refresh_theme::<FloatingPanel>();
92 }
93}
94
95fn panel_added(q_panels: Query<Entity, Added<FloatingPanel>>) -> bool {
96 q_panels.iter().count() > 0
97}
98
99fn index_floating_panels(mut q_panels: Query<&mut FloatingPanel>) {
100 let max = if let Some(Some(m)) = q_panels.iter().map(|p| p.z_index).max() {
101 m
102 } else {
103 0
104 };
105
106 let mut offset = 1;
107 for mut panel in &mut q_panels.iter_mut() {
108 if panel.z_index.is_none() {
109 panel.z_index = (MIN_FLOATING_PANEL_Z_INDEX + max + offset).into();
110 offset += 1;
111 }
112 }
113}
114
115fn process_panel_close_pressed(
116 q_buttons: Query<(&FloatingPanelCloseButton, &FluxInteraction), Changed<FluxInteraction>>,
117 mut commands: Commands,
118) {
119 for (button, interaction) in &q_buttons {
120 if *interaction == FluxInteraction::Released {
121 commands.entity(button.panel).despawn_recursive();
122 }
123 }
124}
125
126fn process_panel_fold_pressed(
127 q_buttons: Query<
128 (Entity, &FloatingPanelFoldButton, &FluxInteraction),
129 Changed<FluxInteraction>,
130 >,
131 mut q_panel_configs: Query<&mut FloatingPanelConfig>,
132) {
133 for (entity, button, interaction) in &q_buttons {
134 if *interaction == FluxInteraction::Released {
135 let Ok(mut config) = q_panel_configs.get_mut(button.panel) else {
136 warn!("Missing floating panel config for fold button {}", entity);
137 continue;
138 };
139
140 config.folded = !config.folded;
141 }
142 }
143}
144
145fn update_panel_size_on_resize(
146 q_draggable: Query<(&Draggable, &ResizeHandle, &FloatingPanelResizeHandle), Changed<Draggable>>,
147 mut q_panels: Query<&mut FloatingPanel>,
148) {
149 if let Some(_) = q_panels.iter().find(|p| p.priority) {
150 return;
151 }
152
153 for (draggable, handle, handle_ref) in &q_draggable {
154 let Ok(mut panel) = q_panels.get_mut(handle_ref.panel) else {
155 continue;
156 };
157
158 if draggable.state == DragState::Inactive
159 || draggable.state == DragState::MaybeDragged
160 || draggable.state == DragState::DragCanceled
161 {
162 panel.resizing = false;
163 continue;
164 }
165
166 let Some(diff) = draggable.diff else {
167 continue;
168 };
169
170 let size_diff = handle.direction().to_size_diff(diff);
171
172 let old_size = panel.size;
173 panel.resizing = true;
174 panel.size += size_diff;
175 if draggable.state == DragState::DragEnd {
176 if panel.size.x < MIN_PANEL_SIZE.x {
177 panel.size.x = MIN_PANEL_SIZE.x;
178 }
179 if panel.size.y < MIN_PANEL_SIZE.y {
180 panel.size.y = MIN_PANEL_SIZE.y;
181 }
182 }
183
184 let pos_diff = match handle.direction() {
185 ResizeDirection::North => Vec2 {
186 x: 0.,
187 y: clip_position_change(diff.y, MIN_PANEL_SIZE.y, old_size.y, panel.size.y),
188 },
189 ResizeDirection::NorthEast => Vec2 {
190 x: 0.,
191 y: clip_position_change(diff.y, MIN_PANEL_SIZE.y, old_size.y, panel.size.y),
192 },
193 ResizeDirection::East => Vec2::ZERO,
194 ResizeDirection::SouthEast => Vec2::ZERO,
195 ResizeDirection::South => Vec2::ZERO,
196 ResizeDirection::SouthWest => Vec2 {
197 x: clip_position_change(diff.x, MIN_PANEL_SIZE.x, old_size.x, panel.size.x),
198 y: 0.,
199 },
200 ResizeDirection::West => Vec2 {
201 x: clip_position_change(diff.x, MIN_PANEL_SIZE.x, old_size.x, panel.size.x),
202 y: 0.,
203 },
204 ResizeDirection::NorthWest => Vec2 {
205 x: clip_position_change(diff.x, MIN_PANEL_SIZE.x, old_size.x, panel.size.x),
206 y: clip_position_change(diff.y, MIN_PANEL_SIZE.y, old_size.y, panel.size.y),
207 },
208 };
209
210 panel.position += pos_diff;
211 }
212}
213
214fn clip_position_change(diff: f32, min: f32, old_size: f32, new_size: f32) -> f32 {
215 let mut new_diff = diff;
216 if old_size <= min && new_size <= min {
217 new_diff = 0.;
218 } else if old_size > min && new_size <= min {
219 new_diff -= min - new_size;
220 } else if old_size < min && new_size >= min {
221 new_diff += min - old_size;
222 }
223
224 new_diff
225}
226
227fn update_panel_on_title_drag(
228 q_draggable: Query<
229 (
230 &Draggable,
231 AnyOf<(&FloatingPanelTitle, &FloatingPanelDragHandle)>,
232 ),
233 Changed<Draggable>,
234 >,
235 mut q_panels: Query<(Entity, &mut FloatingPanel)>,
236) {
237 if let Some(_) = q_panels.iter().find(|(_, p)| p.priority) {
238 return;
239 }
240
241 let max_index = if let Some(Some(m)) = q_panels.iter().map(|(_, p)| p.z_index).max() {
242 m
243 } else {
244 0
245 };
246 let mut offset = 1;
247
248 let mut panel_updated = false;
249
250 for (draggable, (panel_title, drag_handle)) in &q_draggable {
251 let panel_id = if let Some(panel_title) = panel_title {
252 panel_title.panel
253 } else if let Some(drag_handle) = drag_handle {
254 drag_handle.panel
255 } else {
256 continue;
257 };
258
259 let Ok((_, mut panel)) = q_panels.get_mut(panel_id) else {
260 continue;
261 };
262
263 if panel.resizing {
264 continue;
265 }
266
267 if draggable.state == DragState::Inactive
268 || draggable.state == DragState::MaybeDragged
269 || draggable.state == DragState::DragCanceled
270 {
271 panel.moving = false;
272 continue;
273 }
274
275 panel.moving = true;
276 let Some(diff) = draggable.diff else {
277 continue;
278 };
279
280 panel.z_index = Some(max_index + offset);
281 panel.position += diff;
282 offset += 1;
283 panel_updated = true;
284 }
285
286 if !panel_updated {
287 return;
288 }
289
290 let mut panel_indices: Vec<(Entity, Option<usize>)> = q_panels
291 .iter()
292 .map(|(entity, panel)| (entity, panel.z_index))
293 .collect();
294 panel_indices.sort_by(|(_, a), (_, b)| a.cmp(b));
295
296 for (i, (entity, _)) in panel_indices.iter().enumerate() {
297 if let Some((_, mut panel)) = q_panels.iter_mut().find(|(e, _)| e == entity) {
298 panel.z_index = (MIN_FLOATING_PANEL_Z_INDEX + i + 1).into();
299 };
300 }
301}
302
303fn window_resized(e_resized: EventReader<WindowResized>) -> bool {
304 e_resized.len() > 0
305}
306
307fn handle_window_resize(
309 q_window: Query<&Window, With<PrimaryWindow>>,
310 mut q_panels: Query<(&mut FloatingPanel, &Node, &GlobalTransform)>,
311) {
312 let Ok(window) = q_window.get_single() else {
313 return;
314 };
315
316 for (mut panel, node, transform) in &mut q_panels {
317 let position = transform.translation().truncate() - (node.size() / 2.);
318
319 if position.x > window.width() - WINDOW_RESIZE_PADDING {
320 panel.position.x = (panel.position.x - panel.size.x + WINDOW_RESIZE_PADDING).max(0.);
321 if position.y > window.height() - panel.size.y {
322 let overflow = position.y - (window.height() - panel.size.y);
323 panel.position.y = (panel.position.y - overflow).max(0.);
324 }
325 }
326 if position.y > window.height() - WINDOW_RESIZE_PADDING {
327 panel.position.y = (panel.position.y - panel.size.y + WINDOW_RESIZE_PADDING).max(0.);
328
329 if position.x > window.width() - panel.size.x {
330 let overflow = position.x - (window.width() - panel.size.x);
331 panel.position.x = (panel.position.x - overflow).max(0.);
332 }
333 }
334 }
335}
336
337fn update_panel_layout(
338 q_panels: Query<
339 (Entity, &FloatingPanel, Ref<FloatingPanelConfig>),
340 Or<(Changed<FloatingPanel>, Changed<FloatingPanelConfig>)>,
341 >,
342 mut commands: Commands,
343) {
344 for (entity, panel, config) in &q_panels {
345 if config.is_changed() {
346 commands
347 .style(panel.title_container)
348 .render(config.title.is_some());
349
350 if let Some(title) = config.title.clone() {
351 commands.entity(panel.title).set_label_text(title);
352 } else {
353 commands.style(panel.drag_handle).render(config.draggable);
354 }
355
356 commands.style(panel.content_view).render(!config.folded);
357 if config.folded {
358 commands
359 .entity(entity)
360 .add_pseudo_state(PseudoState::Folded);
361 } else {
362 commands
363 .entity(entity)
364 .remove_pseudo_state(PseudoState::Folded);
365 }
366 }
367
368 let render_resize_handles = !config.folded && config.resizable && !panel.moving;
369 if render_resize_handles {
370 commands
371 .entity(panel.resize_handles)
372 .insert(PseudoStates::from(vec![
373 PseudoState::Resizable(CardinalDirection::North),
374 PseudoState::Resizable(CardinalDirection::East),
375 PseudoState::Resizable(CardinalDirection::South),
376 PseudoState::Resizable(CardinalDirection::West),
377 ]));
378 } else {
379 commands
380 .entity(panel.resize_handles)
381 .remove::<PseudoStates>();
382 }
383
384 let policy = match panel.moving {
385 true => FocusPolicy::Pass,
386 false => FocusPolicy::Block,
387 };
388
389 commands.style(entity).focus_policy(policy);
390 commands
391 .style(panel.title_container)
392 .focus_policy(policy)
393 .flux_interaction_enabled(!panel.resizing && config.draggable);
394 commands
395 .style(panel.drag_handle)
396 .focus_policy(policy)
397 .flux_interaction_enabled(!panel.resizing && config.draggable);
398
399 commands
400 .style(panel.fold_button)
401 .flux_interaction_enabled(!(panel.moving || panel.resizing));
402 commands
403 .style(panel.close_button)
404 .flux_interaction_enabled(!(panel.moving || panel.resizing));
405
406 if panel.resizing {
407 commands
408 .style(entity)
409 .width(Val::Px(panel.size.x.max(MIN_PANEL_SIZE.x)))
410 .height(Val::Px(panel.size.y.max(MIN_PANEL_SIZE.y)));
411 }
412
413 if panel.moving || panel.resizing {
414 commands.style(entity).absolute_position(panel.position);
415 }
416
417 if panel.priority {
418 commands
419 .style(entity)
420 .z_index(ZIndex::Global(PRIORITY_FLOATING_PANEL_Z_INDEX as i32));
421 } else if let Some(index) = panel.z_index {
422 commands.style(entity).z_index(ZIndex::Global(index as i32));
423 }
424 }
425}
426
427fn touch_new_floating_panels(mut q_panels: Query<&mut FloatingPanel, Added<FloatingPanel>>) {
430 for mut panel in &mut q_panels {
431 panel.deref_mut();
432 }
433}
434
435#[derive(Component, Clone, Debug, Reflect)]
436#[reflect(Component)]
437pub struct FloatingPanelResizeHandle {
438 panel: Entity,
439}
440
441impl Default for FloatingPanelResizeHandle {
442 fn default() -> Self {
443 Self {
444 panel: Entity::PLACEHOLDER,
445 }
446 }
447}
448
449#[derive(Component, Debug, Reflect)]
450#[reflect(Component)]
451pub struct FloatingPanelTitle {
452 panel: Entity,
453}
454
455impl Default for FloatingPanelTitle {
456 fn default() -> Self {
457 Self {
458 panel: Entity::PLACEHOLDER,
459 }
460 }
461}
462
463impl FloatingPanelTitle {
464 pub fn panel(&self) -> Entity {
465 self.panel
466 }
467}
468
469#[derive(Component, Debug, Reflect)]
470#[reflect(Component)]
471pub struct FloatingPanelDragHandle {
472 panel: Entity,
473}
474
475impl Default for FloatingPanelDragHandle {
476 fn default() -> Self {
477 Self {
478 panel: Entity::PLACEHOLDER,
479 }
480 }
481}
482
483#[derive(Component, Debug, Reflect)]
484#[reflect(Component)]
485pub struct FloatingPanelFoldButton {
486 panel: Entity,
487}
488
489impl Default for FloatingPanelFoldButton {
490 fn default() -> Self {
491 Self {
492 panel: Entity::PLACEHOLDER,
493 }
494 }
495}
496
497#[derive(Component, Debug, Reflect)]
498#[reflect(Component)]
499pub struct FloatingPanelCloseButton {
500 panel: Entity,
501}
502
503impl Default for FloatingPanelCloseButton {
504 fn default() -> Self {
505 Self {
506 panel: Entity::PLACEHOLDER,
507 }
508 }
509}
510
511#[derive(Component, Clone, Debug, Reflect)]
512pub struct FloatingPanelConfig {
513 pub title: Option<String>,
514 pub draggable: bool,
515 pub resizable: bool,
516 pub foldable: bool,
517 pub folded: bool,
518 pub closable: bool,
519 pub restrict_scroll: Option<ScrollAxis>,
520}
521
522impl Default for FloatingPanelConfig {
523 fn default() -> Self {
524 Self {
525 title: None,
526 draggable: true,
527 resizable: true,
528 foldable: true,
529 folded: false,
530 closable: true,
531 restrict_scroll: None,
532 }
533 }
534}
535
536impl FloatingPanelConfig {
537 pub fn title(&self) -> Option<String> {
538 self.title.clone()
539 }
540}
541
542#[derive(Component, Debug, Reflect)]
543#[reflect(Component)]
544pub struct FloatingPanel {
545 size: Vec2,
546 position: Vec2,
547 z_index: Option<usize>,
548 drag_handle: Entity,
549 fold_button: Entity,
550 title_container: Entity,
551 title: Entity,
552 close_button_container: Entity,
553 close_button: Entity,
554 content_view: Entity,
555 content_panel_container: Entity,
556 content_panel: Entity,
557 resize_handles: Entity,
558 resizing: bool,
559 moving: bool,
560 pub priority: bool,
561}
562
563impl Default for FloatingPanel {
564 fn default() -> Self {
565 Self {
566 size: Default::default(),
567 position: Default::default(),
568 z_index: Default::default(),
569 drag_handle: Entity::PLACEHOLDER,
570 fold_button: Entity::PLACEHOLDER,
571 title_container: Entity::PLACEHOLDER,
572 title: Entity::PLACEHOLDER,
573 close_button_container: Entity::PLACEHOLDER,
574 close_button: Entity::PLACEHOLDER,
575 content_view: Entity::PLACEHOLDER,
576 content_panel_container: Entity::PLACEHOLDER,
577 content_panel: Entity::PLACEHOLDER,
578 resize_handles: Entity::PLACEHOLDER,
579 resizing: Default::default(),
580 moving: Default::default(),
581 priority: Default::default(),
582 }
583 }
584}
585
586impl UiContext for FloatingPanel {
587 fn get(&self, target: &str) -> Result<Entity, String> {
588 match target {
589 FloatingPanel::DRAG_HANDLE => Ok(self.drag_handle),
590 FloatingPanel::TITLE_CONTAINER => Ok(self.title_container),
591 FloatingPanel::TITLE => Ok(self.title),
592 FloatingPanel::FOLD_BUTTON => Ok(self.fold_button),
593 FloatingPanel::CLOSE_BUTTON_CONTAINER => Ok(self.close_button_container),
594 FloatingPanel::CLOSE_BUTTON => Ok(self.close_button),
595 FloatingPanel::CONTENT_VIEW => Ok(self.content_view),
596 _ => Err(format!(
597 "{} doesn't exist for FloatingPanel. Possible contexts: {:?}",
598 target,
599 Vec::from_iter(self.contexts())
600 )),
601 }
602 }
603
604 fn contexts(&self) -> impl Iterator<Item = &str> + '_ {
605 [
606 FloatingPanel::DRAG_HANDLE,
607 FloatingPanel::TITLE_CONTAINER,
608 FloatingPanel::TITLE,
609 FloatingPanel::FOLD_BUTTON,
610 FloatingPanel::CLOSE_BUTTON_CONTAINER,
611 FloatingPanel::CLOSE_BUTTON,
612 FloatingPanel::CONTENT_VIEW,
613 ]
614 .into_iter()
615 }
616}
617
618impl DefaultTheme for FloatingPanel {
619 fn default_theme() -> Option<Theme<FloatingPanel>> {
620 FloatingPanel::theme().into()
621 }
622}
623
624impl FloatingPanel {
625 pub const DRAG_HANDLE: &'static str = "DragHandle";
626 pub const TITLE_CONTAINER: &'static str = "TitleContainer";
627 pub const TITLE: &'static str = "Title";
628 pub const FOLD_BUTTON: &'static str = "FoldButton";
629 pub const CLOSE_BUTTON_CONTAINER: &'static str = "CloseButtonContainer";
630 pub const CLOSE_BUTTON: &'static str = "CloseButton";
631 pub const CONTENT_VIEW: &'static str = "ContentView";
632
633 pub fn theme() -> Theme<FloatingPanel> {
634 let base_theme = PseudoTheme::deferred_context(None, FloatingPanel::primary_style);
635 let folded_theme =
636 PseudoTheme::deferred_context(vec![PseudoState::Folded], FloatingPanel::folded_style);
637
638 Theme::new(vec![base_theme, folded_theme])
639 }
640
641 fn primary_style(
642 style_builder: &mut StyleBuilder,
643 panel: &FloatingPanel,
644 theme_data: &ThemeData,
645 ) {
646 let theme_spacing = theme_data.spacing;
647 let colors = theme_data.colors();
648
649 style_builder
650 .absolute_position(panel.position)
651 .border(UiRect::all(Val::Px(theme_spacing.borders.extra_small)))
652 .border_color(colors.accent(Accent::Shadow))
653 .background_color(colors.surface(Surface::Surface))
654 .border_radius(BorderRadius::all(Val::Px(
655 theme_spacing.corners.extra_small,
656 )));
657
658 style_builder
659 .animated()
660 .height(AnimatedVals {
661 idle: Val::Px(panel.size.y.max(MIN_PANEL_SIZE.y)),
662 enter_from: Val::Px(theme_spacing.areas.small).into(),
663 ..default()
664 })
665 .copy_from(theme_data.enter_animation);
666
667 style_builder
668 .animated()
669 .width(AnimatedVals {
670 idle: Val::Px(panel.size.x.max(MIN_PANEL_SIZE.x)),
671 enter_from: Val::Px(theme_spacing.areas.extra_large).into(),
672 ..default()
673 })
674 .copy_from(theme_data.enter_animation);
675
676 style_builder
677 .switch_target(FloatingPanel::TITLE_CONTAINER)
678 .width(Val::Percent(100.))
679 .align_items(AlignItems::Center)
680 .justify_content(JustifyContent::Start)
681 .background_color(colors.container(Container::SurfaceMid))
682 .border_radius(BorderRadius::top(Val::Px(
683 theme_spacing.corners.extra_small,
684 )));
685
686 style_builder
687 .switch_target(FloatingPanel::TITLE)
688 .flex_grow(1.)
689 .margin(UiRect::px(
690 theme_spacing.gaps.small,
691 theme_spacing.gaps.extra_large,
692 theme_spacing.gaps.small,
693 theme_spacing.gaps.extra_small,
694 ))
695 .sized_font(
696 theme_data
697 .text
698 .get(FontStyle::Body, FontScale::Large, FontType::Regular),
699 )
700 .font_color(colors.on(OnColor::Surface));
701
702 style_builder
703 .switch_target(FloatingPanel::CLOSE_BUTTON_CONTAINER)
704 .right(Val::Px(0.))
705 .background_color(colors.container(Container::SurfaceMid))
706 .border_radius(BorderRadius::top_right(Val::Px(
707 theme_spacing.corners.extra_small,
708 )));
709
710 style_builder
711 .switch_target(FloatingPanel::CONTENT_VIEW)
712 .width(Val::Percent(100.))
713 .height(Val::Percent(100.))
714 .border_radius(BorderRadius::bottom(Val::Px(
715 theme_spacing.corners.extra_small,
716 )));
717
718 style_builder
719 .switch_context(FloatingPanel::DRAG_HANDLE, None)
720 .width(Val::Percent(100.))
721 .height(Val::Px(theme_spacing.borders.small * 2.))
722 .border(UiRect::bottom(Val::Px(theme_spacing.borders.small)))
723 .border_color(colors.accent(Accent::Shadow))
724 .animated()
725 .background_color(AnimatedVals {
726 idle: colors.surface(Surface::Surface),
727 hover: colors.surface(Surface::SurfaceVariant).into(),
728 ..default()
729 })
730 .copy_from(theme_data.interaction_animation);
731
732 style_builder
733 .switch_context(FloatingPanel::FOLD_BUTTON, None)
734 .size(Val::Px(theme_spacing.icons.small))
735 .margin(UiRect::all(Val::Px(theme_spacing.gaps.small)))
736 .icon(
737 theme_data
738 .icons
739 .expand_more
740 .with(colors.on(OnColor::Surface), theme_spacing.icons.small),
741 )
742 .animated()
743 .font_color(AnimatedVals {
744 idle: colors.on(OnColor::SurfaceVariant),
745 hover: colors.on(OnColor::Surface).into(),
746 ..default()
747 })
748 .copy_from(theme_data.interaction_animation);
749
750 style_builder
751 .switch_context(FloatingPanel::CLOSE_BUTTON, None)
752 .size(Val::Px(theme_spacing.icons.small))
753 .margin(UiRect::all(Val::Px(theme_spacing.gaps.small)))
754 .icon(
755 theme_data
756 .icons
757 .close
758 .with(colors.on(OnColor::Surface), theme_spacing.icons.small),
759 )
760 .animated()
761 .font_color(AnimatedVals {
762 idle: colors.on(OnColor::SurfaceVariant),
763 hover: colors.on(OnColor::Surface).into(),
764 ..default()
765 })
766 .copy_from(theme_data.interaction_animation);
767 }
768
769 fn folded_style(
770 style_builder: &mut StyleBuilder,
771 panel: &FloatingPanel,
772 theme_data: &ThemeData,
773 ) {
774 let theme_spacing = theme_data.spacing;
775 let colors = theme_data.colors();
776
777 style_builder
778 .height(Val::Auto)
779 .animated()
780 .width(AnimatedVals {
781 idle: Val::Px(theme_spacing.areas.extra_large),
782 enter_from: Val::Px(panel.size.x.max(MIN_PANEL_SIZE.x)).into(),
783 ..default()
784 })
785 .copy_from(theme_data.enter_animation);
786
787 style_builder
788 .switch_target(FloatingPanel::CONTENT_VIEW)
789 .animated()
790 .height(AnimatedVals {
791 idle: Val::Percent(0.),
792 enter_from: Val::Percent(100.).into(),
793 ..default()
794 })
795 .copy_from(theme_data.enter_animation);
796
797 style_builder
798 .switch_target(FloatingPanel::FOLD_BUTTON)
799 .icon(
800 theme_data
801 .icons
802 .chevron_right
803 .with(colors.on(OnColor::Surface), theme_spacing.icons.small),
804 );
805 }
806
807 pub fn content_panel_container(&self) -> Entity {
808 self.content_panel_container
809 }
810
811 pub fn content_panel_id(&self) -> Entity {
812 self.content_panel
813 }
814
815 pub fn title_container_id(&self) -> Entity {
816 self.title_container
817 }
818
819 fn frame(title: String) -> impl Bundle {
820 (
821 Name::new(format!("Floating Panel [{}]", title)),
822 NodeBundle {
823 style: Style {
824 position_type: PositionType::Absolute,
825 flex_direction: FlexDirection::Column,
826 align_items: AlignItems::Start,
827 overflow: Overflow::clip(),
828 ..default()
829 },
830 focus_policy: bevy::ui::FocusPolicy::Block,
831 ..default()
832 },
833 LockedStyleAttributes::from_vec(vec![
834 LockableStyleAttribute::PositionType,
835 LockableStyleAttribute::FlexDirection,
836 LockableStyleAttribute::AlignItems,
837 LockableStyleAttribute::Overflow,
838 ]),
839 )
840 }
841
842 fn title_container(panel: Entity) -> impl Bundle {
843 (
844 Name::new("Title Container"),
845 ButtonBundle::default(),
846 FloatingPanelTitle { panel },
847 TrackedInteraction::default(),
848 Draggable::default(),
849 RelativeCursorPosition::default(),
850 )
851 }
852
853 fn fold_button(panel: Entity) -> impl Bundle {
854 (
855 Name::new("Fold Button"),
856 ButtonBundle::default(),
857 ContentSize::default(),
858 TrackedInteraction::default(),
859 FloatingPanelFoldButton { panel },
860 )
861 }
862
863 fn drag_handle() -> impl Bundle {
864 (
865 Name::new("Drag Handle"),
866 ButtonBundle::default(),
867 TrackedInteraction::default(),
868 Draggable::default(),
869 RelativeCursorPosition::default(),
870 )
871 }
872
873 fn close_button_container() -> impl Bundle {
874 (
875 Name::new("Close Button Container"),
876 NodeBundle {
877 style: Style {
878 position_type: PositionType::Absolute,
879 ..default()
880 },
881 focus_policy: bevy::ui::FocusPolicy::Block,
882 ..default()
883 },
884 LockedStyleAttributes::from_vec(vec![
885 LockableStyleAttribute::PositionType,
886 LockableStyleAttribute::FocusPolicy,
887 ]),
888 )
889 }
890
891 fn close_button(panel: Entity) -> impl Bundle {
892 (
893 Name::new("Close Button"),
894 ButtonBundle::default(),
895 ContentSize::default(),
896 TrackedInteraction::default(),
897 FloatingPanelCloseButton { panel },
898 )
899 }
900}
901
902#[derive(Debug)]
903pub struct FloatingPanelLayout {
904 pub size: Vec2,
905 pub position: Option<Vec2>,
906 pub droppable: bool,
907}
908
909impl Default for FloatingPanelLayout {
910 fn default() -> Self {
911 Self {
912 size: Vec2 { x: 300., y: 500. },
913 position: Default::default(),
914 droppable: false,
915 }
916 }
917}
918
919impl FloatingPanelLayout {
920 pub fn min() -> Self {
921 Self {
922 size: MIN_PANEL_SIZE,
923 ..default()
924 }
925 }
926}
927
928#[derive(Component)]
929#[component(storage = "SparseSet")]
930pub struct UpdateFloatingPanelPanelId {
931 pub panel_id: Entity,
932}
933
934pub trait UiFloatingPanelExt {
935 fn floating_panel<'a>(
936 &'a mut self,
937 config: FloatingPanelConfig,
938 layout: FloatingPanelLayout,
939 spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
940 ) -> UiBuilder<Entity>;
941}
942
943impl<T: UiContainerExt> UiFloatingPanelExt for T {
944 fn floating_panel<'a>(
950 &'a mut self,
951 config: FloatingPanelConfig,
952 layout: FloatingPanelLayout,
953 spawn_children: impl FnOnce(&mut UiBuilder<Entity>),
954 ) -> UiBuilder<Entity> {
955 let restrict_to = config.restrict_scroll;
956 let title_text = if let Some(text) = config.title.clone() {
957 text
958 } else {
959 "Untitled".into()
960 };
961
962 let mut floating_panel = FloatingPanel {
963 size: layout.size.max(MIN_PANEL_SIZE),
964 position: layout.position.unwrap_or_default(),
965 z_index: None,
966 ..default()
967 };
968
969 let mut frame = self.container(FloatingPanel::frame(title_text.clone()), |container| {
970 let panel = container.id();
971 floating_panel.resize_handles = container
972 .resize_handles(FloatingPanelResizeHandle { panel }, |_| {})
973 .id();
974
975 let mut title_builder =
976 container.container(FloatingPanel::title_container(panel), |container| {
977 floating_panel.fold_button = container
978 .spawn(FloatingPanel::fold_button(panel))
979 .style()
980 .render(config.foldable)
981 .id();
982
983 floating_panel.title = container
984 .label(LabelConfig {
985 label: title_text.clone(),
986 ..default()
987 })
988 .id();
989
990 floating_panel.close_button_container = container
991 .container(
992 FloatingPanel::close_button_container(),
993 |close_button_container| {
994 floating_panel.close_button = close_button_container
995 .spawn(FloatingPanel::close_button(panel))
996 .style()
997 .render(config.closable)
998 .id();
999 },
1000 )
1001 .id();
1002 });
1003 title_builder.style().render(config.title.is_some());
1004
1005 if layout.droppable {
1006 title_builder.insert(Droppable);
1007 }
1008
1009 floating_panel.title_container = title_builder.id();
1010
1011 floating_panel.drag_handle = container
1012 .spawn((
1013 FloatingPanel::drag_handle(),
1014 FloatingPanelDragHandle { panel },
1015 ))
1016 .style()
1017 .render(config.title.is_none())
1018 .id();
1019
1020 floating_panel.content_view = container
1021 .column(|column| {
1022 column.scroll_view(restrict_to, |scroll_view| {
1023 floating_panel.content_panel_container = scroll_view.id();
1024 floating_panel.content_panel = scroll_view
1025 .panel(
1026 config.title.clone().unwrap_or("Untitled".into()),
1027 spawn_children,
1028 )
1029 .id();
1030 });
1031 })
1032 .style()
1033 .render(config.folded)
1034 .id();
1035 });
1036
1037 if config.folded {
1038 frame.insert(PseudoStates::from(vec![PseudoState::Folded]));
1039 }
1040
1041 frame.insert((config, floating_panel));
1042 frame
1043 }
1044}