use bevy::picking::events::{PointerClick, PointerPress, PointerRelease};
use bevy::picking::hover::HoverMap;
use bevy::picking::pointer::{PointerAction, PointerId, PointerInput, PointerLocation};
use bevy::prelude::*;
use bevy::ui::{FixedNode, UiScale};
use bevy::ui_widgets::popover::{Popover, PopoverAlign, PopoverPlacement, PopoverSide};
use crate::cancel::Cancel;
use crate::menu::{MenuPanel, close_menu};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, Reflect)]
#[reflect(Debug, Default, Clone, PartialEq)]
pub enum Placement {
#[default]
Below,
Above,
Right,
Left,
AtPointer,
}
#[derive(Component, Clone, Debug, Reflect)]
#[reflect(Component, Debug, Clone)]
pub struct PopoverPanel {
pub anchor: Entity,
pub placement: Placement,
pub modal: bool,
armed: bool,
shown: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Reflect)]
#[reflect(Debug, Clone, PartialEq)]
pub enum CloseReason {
Cancel,
Outside,
}
#[derive(EntityEvent, Clone, Debug, Reflect)]
#[reflect(Event, Clone, Debug)]
pub struct PopoverClosed {
#[event_target]
pub entity: Entity,
pub anchor: Entity,
pub reason: CloseReason,
}
#[derive(Component, Clone, Copy, Debug)]
pub struct PopoverPoint;
impl PopoverPanel {
pub(crate) fn new(anchor: Entity, placement: Placement, modal: bool) -> Self {
Self {
anchor,
placement,
modal,
armed: false,
shown: false,
}
}
pub(crate) fn menu(anchor: Entity, placement: Placement) -> Self {
Self::new(anchor, placement, true)
}
}
pub(crate) fn candidates(placement: Placement, gap: f32) -> Vec<PopoverPlacement> {
use PopoverSide::{Bottom, Left, Right, Top};
let (side, mirror) = match placement {
Placement::Below | Placement::AtPointer => (Bottom, Top),
Placement::Above => (Top, Bottom),
Placement::Right => (Right, Left),
Placement::Left => (Left, Right),
};
[side, mirror]
.into_iter()
.flat_map(|side| {
[PopoverAlign::Start, PopoverAlign::End, PopoverAlign::Center]
.into_iter()
.map(move |align| PopoverPlacement { side, align, gap })
})
.collect()
}
pub(crate) fn placement_of(placement: Placement, gap: f32, margin: f32) -> Popover {
Popover {
positions: candidates(
placement,
if placement == Placement::AtPointer {
0.0
} else {
gap
},
),
window_margin: margin,
}
}
fn close(commands: &mut Commands, popover: Entity, anchor: Entity, reason: CloseReason) {
commands.queue(move |world: &mut World| {
world.trigger(PopoverClosed {
entity: popover,
anchor,
reason,
});
if world.get::<MenuPanel>(popover).is_some() {
close_menu(world, popover);
} else if let Ok(entity) = world.get_entity_mut(popover) {
entity.despawn();
}
});
}
pub(crate) fn popover_on_cancel(
mut cancel: On<Cancel>,
panels: Query<&PopoverPanel>,
mut commands: Commands,
) {
if let Ok(panel) = panels.get(cancel.entity) {
cancel.propagate(false);
close(
&mut commands,
cancel.entity,
panel.anchor,
CloseReason::Cancel,
);
}
}
pub(crate) fn stop_press(mut press: On<PointerPress>) {
press.propagate(false);
}
pub(crate) fn stop_release(mut release: On<PointerRelease>) {
release.propagate(false);
}
pub(crate) fn stop_click(mut click: On<PointerClick>) {
click.propagate(false);
}
pub(crate) fn stop_acquire(mut acquire: On<bevy::input_focus::AcquireFocus>) {
acquire.propagate(false);
}
pub fn anchor_pointer_popovers(
new: Query<(Entity, &PopoverPanel), Added<PopoverPanel>>,
pointers: Query<&PointerLocation>,
anchors: Query<(), With<Node>>,
ui_scale: Option<Res<UiScale>>,
mut commands: Commands,
) {
for (panel, info) in &new {
if info.placement != Placement::AtPointer {
continue;
}
let scale = ui_scale.as_ref().map_or(1.0, |s| s.0).max(f32::EPSILON);
let Some(at) = pointers
.iter()
.find_map(|p| p.location.as_ref())
.map(|l| l.position / scale)
else {
continue;
};
let mut point = commands.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(at.x),
top: Val::Px(at.y),
width: Val::Px(0.0),
height: Val::Px(0.0),
..default()
},
PopoverPoint,
));
if anchors.contains(info.anchor) {
point.insert((FixedNode, ChildOf(info.anchor)));
}
let point = point.id();
commands.entity(panel).insert(ChildOf(point));
}
}
pub fn drop_pointer_anchors(
points: Query<Entity, (With<PopoverPoint>, Without<Children>)>,
mut commands: Commands,
) {
for point in &points {
commands.entity(point).try_despawn();
}
}
pub fn reveal_popovers(mut panels: Query<(&Node, &mut PopoverPanel, &mut Visibility)>) {
for (node, mut panel, mut visibility) in &mut panels {
if !panel.shown && node.display == Display::Flex {
panel.shown = true;
*visibility = Visibility::Inherited;
}
}
}
type Open = (Entity, &'static mut PopoverPanel);
pub fn close_popovers_on_press(
mut input: MessageReader<PointerInput>,
hover: Option<Res<HoverMap>>,
mut panels: Query<Open>,
parents: Query<&ChildOf>,
mut commands: Commands,
) {
let pressed: Vec<PointerId> = input
.read()
.filter(|i| matches!(i.action, PointerAction::Press(_)))
.map(|i| i.pointer_id)
.collect();
let mut closed: Vec<Entity> = Vec::new();
let mut open: Vec<(Entity, Entity, Placement, bool)> = Vec::new();
for (entity, mut panel) in &mut panels {
let armed = panel.armed;
if !armed {
panel.armed = true;
}
open.push((entity, panel.anchor, panel.placement, armed));
}
let Some(hover) = hover else {
return;
};
open.sort_by_key(|&(entity, ..)| parents.iter_ancestors(entity).count());
for (entity, anchor, placement, armed) in open {
if !armed || parents.iter_ancestors(entity).any(|a| closed.contains(&a)) {
continue;
}
let outside = pressed.iter().any(|pointer| {
let hits = hover.get(pointer);
let within = |root: Entity| {
hits.is_some_and(|hits| {
hits.keys()
.any(|&e| e == root || parents.iter_ancestors(e).any(|a| a == root))
})
};
!within(entity) && !(placement != Placement::AtPointer && within(anchor))
});
if outside {
closed.push(entity);
close(&mut commands, entity, anchor, CloseReason::Outside);
}
}
}