use bevy::ecs::lifecycle::HookContext;
use bevy::ecs::resource::IsResource;
use bevy::ecs::world::DeferredWorld;
use bevy::input_focus::{InputFocus, InputFocusVisible};
use bevy::picking::events::{PointerCancel, PointerDrag, PointerDragEnd, PointerDragStart};
use bevy::picking::hover::HoverMap;
use bevy::picking::pointer::PointerId;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, InteractionDisabled, UiGlobalTransform, UiScale};
use crate::model::{Toward, target_step, target_toward};
use crate::nav::show_focus;
use crate::shortcut::{Modifiers, Shortcut};
use crate::skin::Role;
pub const DRAG_LIFT_PX: f32 = 6.0;
const DRAG_Z: i32 = 900;
const GHOST_OFFSET_PX: f32 = 14.0;
#[derive(Component, Clone, Debug, Default, PartialEq, Eq, Reflect)]
#[reflect(Component, Default, Debug, PartialEq, Clone)]
pub struct DragLabel(pub String);
#[derive(Resource, Clone, Debug, PartialEq, Reflect)]
#[reflect(Resource, Default, Debug, PartialEq, Clone)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(default)
)]
pub struct DragSettings {
pub pick_up: Shortcut,
pub pick_up_button: GamepadButton,
}
impl Default for DragSettings {
fn default() -> Self {
Self {
pick_up: Shortcut::new(Modifiers::ALT, KeyCode::KeyM),
pick_up_button: GamepadButton::West,
}
}
}
#[derive(Component, Clone, Debug, PartialEq, Eq)]
#[component(on_add = payload_added::<T>, on_remove = payload_removed)]
#[require(DragSource)]
pub struct DragPayload<T: Clone + Send + Sync + 'static>(pub T);
#[derive(Component, Clone, Debug)]
#[component(on_add = target_added::<T>)]
#[require(DropZone)]
pub struct DropTarget<T: Clone + Send + Sync + 'static> {
accepts: Option<fn(&T) -> bool>,
judge: Option<fn(EntityRef, EntityRef, &T) -> bool>,
}
impl<T: Clone + Send + Sync + 'static> DropTarget<T> {
pub const fn new() -> Self {
Self {
accepts: None,
judge: None,
}
}
pub const fn accepting(accepts: fn(&T) -> bool) -> Self {
Self {
accepts: Some(accepts),
judge: None,
}
}
pub(crate) const fn judged(judge: fn(EntityRef, EntityRef, &T) -> bool) -> Self {
Self {
accepts: None,
judge: Some(judge),
}
}
}
impl<T: Clone + Send + Sync + 'static> Default for DropTarget<T> {
fn default() -> Self {
Self::new()
}
}
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct Dropped<T: Clone + Send + Sync + 'static> {
#[event_target]
pub target: Entity,
pub source: Entity,
pub payload: T,
pub pointer: Vec2,
}
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct DropRefused<T: Clone + Send + Sync + 'static> {
#[event_target]
pub target: Entity,
pub source: Entity,
pub payload: T,
pub pointer: Vec2,
}
#[derive(Event, Clone, Debug, PartialEq)]
pub struct DroppedOnWorld<T: Clone + Send + Sync + 'static> {
pub source: Entity,
pub payload: T,
pub pointer: Vec2,
}
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct Dragging;
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct DropCandidate;
#[derive(Component, Clone, Copy, Debug, Default, Reflect)]
#[reflect(Component, Default, Debug, Clone)]
pub struct DropRefusing;
#[derive(Component, Clone, Copy, Default)]
pub(crate) struct DragSource {
world_drop: Option<fn(&mut Commands, EntityRef, Vec2)>,
refused: Option<fn(&mut Commands, Entity, EntityRef, Vec2)>,
}
#[derive(Clone, Copy)]
struct Handler {
accepts: fn(EntityRef, EntityRef) -> Option<bool>,
deliver: fn(&mut Commands, EntityRef, EntityRef, Vec2) -> bool,
}
#[derive(Component, Default, Clone)]
pub(crate) struct DropZone {
handlers: Vec<Handler>,
}
impl DropZone {
fn takes(&self, zone: EntityRef, source: EntityRef) -> bool {
self.handlers
.iter()
.any(|h| (h.accepts)(zone, source) == Some(true))
}
}
fn payload_added<T: Clone + Send + Sync + 'static>(
mut world: DeferredWorld,
HookContext { entity, .. }: HookContext,
) {
if let Some(mut source) = world.get_mut::<DragSource>(entity) {
source.world_drop = Some(world_drop::<T>);
source.refused = Some(refused::<T>);
}
}
fn payload_removed(mut world: DeferredWorld, HookContext { entity, .. }: HookContext) {
world.commands().entity(entity).try_remove::<DragSource>();
}
fn target_added<T: Clone + Send + Sync + 'static>(
mut world: DeferredWorld,
HookContext { entity, .. }: HookContext,
) {
if let Some(mut zone) = world.get_mut::<DropZone>(entity) {
zone.handlers.push(Handler {
accepts: accepts::<T>,
deliver: deliver::<T>,
});
}
}
fn accepts<T: Clone + Send + Sync + 'static>(zone: EntityRef, source: EntityRef) -> Option<bool> {
let payload = source.get::<DragPayload<T>>()?;
let target = zone.get::<DropTarget<T>>()?;
Some(
target.accepts.is_none_or(|accepts| accepts(&payload.0))
&& target
.judge
.is_none_or(|judge| judge(zone, source, &payload.0)),
)
}
fn deliver<T: Clone + Send + Sync + 'static>(
commands: &mut Commands,
zone: EntityRef,
source: EntityRef,
pointer: Vec2,
) -> bool {
if accepts::<T>(zone, source) != Some(true) {
return false;
}
let Some(payload) = source.get::<DragPayload<T>>() else {
return false;
};
commands.trigger(Dropped {
target: zone.id(),
source: source.id(),
payload: payload.0.clone(),
pointer,
});
true
}
fn refused<T: Clone + Send + Sync + 'static>(
commands: &mut Commands,
target: Entity,
source: EntityRef,
pointer: Vec2,
) {
if let Some(payload) = source.get::<DragPayload<T>>() {
commands.trigger(DropRefused {
target,
source: source.id(),
payload: payload.0.clone(),
pointer,
});
}
}
fn world_drop<T: Clone + Send + Sync + 'static>(
commands: &mut Commands,
source: EntityRef,
pointer: Vec2,
) {
if let Some(payload) = source.get::<DragPayload<T>>() {
commands.trigger(DroppedOnWorld {
source: source.id(),
payload: payload.0.clone(),
pointer,
});
}
}
#[derive(Resource, Clone, Debug, Default)]
pub struct DragState {
drag: Option<Drag>,
}
#[derive(Clone, Debug)]
struct Drag {
source: Entity,
pointer_id: PointerId,
pointer: Vec2,
lifted: bool,
label: String,
over: Option<Entity>,
refused: bool,
cancelled: bool,
keyboard: bool,
}
impl DragState {
pub fn source(&self) -> Option<Entity> {
self.drag.as_ref().filter(|d| d.lifted).map(|d| d.source)
}
pub fn over(&self) -> Option<Entity> {
self.drag.as_ref().filter(|d| d.lifted).and_then(|d| d.over)
}
pub fn refused(&self) -> bool {
self.drag.as_ref().is_some_and(|d| d.lifted && d.refused)
}
pub fn is_dragging(&self) -> bool {
self.drag.as_ref().is_some_and(|d| d.lifted)
}
pub fn carried_by_key(&self) -> bool {
self.drag.as_ref().is_some_and(|d| d.lifted && d.keyboard)
}
}
pub fn is_dragging(state: Option<Res<DragState>>) -> bool {
state.is_some_and(|s| s.is_dragging())
}
pub(crate) fn drag_on_start(
mut start: On<PointerDragStart>,
sources: Query<(), With<DragSource>>,
mut state: ResMut<DragState>,
) {
if !sources.contains(start.entity) || start.button != PointerButton::Primary {
return;
}
start.propagate(false);
if state.drag.is_none() {
state.drag = Some(Drag {
source: start.entity,
pointer_id: start.pointer.id,
pointer: start.pointer.position,
lifted: false,
label: String::new(),
over: None,
refused: false,
cancelled: false,
keyboard: false,
});
}
}
pub(crate) fn drag_on_drag(
mut drag: On<PointerDrag>,
mut state: ResMut<DragState>,
labels: Query<&DragLabel>,
texts: Query<&Text>,
kids: Query<&Children>,
mut commands: Commands,
) {
let Some(held) = state.drag.as_mut().filter(|d| d.source == drag.entity) else {
return;
};
drag.propagate(false);
held.pointer = drag.pointer.position;
if !held.lifted && drag.distance.length() >= DRAG_LIFT_PX {
held.lifted = true;
held.label = labels
.get(held.source)
.map(|label| label.0.clone())
.ok()
.or_else(|| first_text(held.source, &texts, &kids))
.unwrap_or_default();
commands.entity(held.source).insert(Dragging);
}
}
fn first_text(entity: Entity, texts: &Query<&Text>, kids: &Query<&Children>) -> Option<String> {
if let Ok(text) = texts.get(entity) {
return Some(text.0.clone());
}
kids.get(entity)
.ok()?
.iter()
.find_map(|kid| first_text(kid, texts, kids))
}
fn front_hit(hover: &HoverMap, pointer: PointerId) -> Option<Entity> {
hover
.get(&pointer)?
.iter()
.min_by(|a, b| {
a.1.depth
.partial_cmp(&b.1.depth)
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|(entity, _)| *entity)
}
fn zone_of(entity: Entity, zones: &Query<&DropZone>, parents: &Query<&ChildOf>) -> Option<Entity> {
std::iter::once(entity)
.chain(parents.iter_ancestors(entity))
.find(|&e| zones.contains(e))
}
pub(crate) fn track_drag(
mut state: ResMut<DragState>,
hover: Option<Res<HoverMap>>,
zones: Query<&DropZone>,
parents: Query<&ChildOf>,
entities: Query<EntityRef, Without<IsResource>>,
mut commands: Commands,
) {
if state.drag.as_ref().is_some_and(|d| d.cancelled)
&& let Some(held) = state.drag.take()
{
commands.entity(held.source).remove::<Dragging>();
return;
}
let Some(held) = state.drag.as_mut() else {
return;
};
if held.keyboard {
return;
}
if !held.lifted {
return;
}
let over = hover
.as_ref()
.and_then(|hover| front_hit(hover, held.pointer_id))
.and_then(|front| zone_of(front, &zones, &parents));
let refused = over.is_some_and(|zone| {
let (Ok(zone_ref), Ok(source), Ok(handlers)) = (
entities.get(zone),
entities.get(held.source),
zones.get(zone),
) else {
return true;
};
!handlers.takes(zone_ref, source)
});
if held.over != over {
held.over = over;
}
if held.refused != refused {
held.refused = refused;
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn drag_on_end(
mut end: On<PointerDragEnd>,
mut state: ResMut<DragState>,
hover: Option<Res<HoverMap>>,
zones: Query<&DropZone>,
parents: Query<&ChildOf>,
nodes: Query<(), With<Node>>,
entities: Query<EntityRef, Without<IsResource>>,
mut commands: Commands,
) {
if state.drag.as_ref().is_none_or(|d| d.source != end.entity) {
return;
}
end.propagate(false);
let Some(held) = state.drag.take() else {
return;
};
commands.entity(held.source).remove::<Dragging>();
if !held.lifted {
return;
}
let pointer = end.pointer.position;
let Ok(source) = entities.get(held.source) else {
return;
};
let Some(drag_source) = source.get::<DragSource>().copied() else {
return;
};
let front = hover
.as_ref()
.and_then(|hover| front_hit(hover, held.pointer_id));
match front {
Some(front) => {
if let Some(zone) = zone_of(front, &zones, &parents)
&& let (Ok(zone_ref), Ok(handlers)) = (entities.get(zone), zones.get(zone))
{
release_on(
&mut commands,
zone,
zone_ref,
handlers,
source,
drag_source,
pointer,
);
} else if !nodes.contains(front)
&& let Some(world_drop) = drag_source.world_drop
{
world_drop(&mut commands, source, pointer);
}
}
None => {
if let Some(world_drop) = drag_source.world_drop {
world_drop(&mut commands, source, pointer);
}
}
}
}
fn release_on(
commands: &mut Commands,
zone: Entity,
zone_ref: EntityRef,
handlers: &DropZone,
source: EntityRef,
drag_source: DragSource,
pointer: Vec2,
) {
let delivered = handlers
.handlers
.iter()
.any(|h| (h.deliver)(commands, zone_ref, source, pointer));
if !delivered && let Some(refused) = drag_source.refused {
refused(commands, zone, source, pointer);
}
}
pub(crate) fn drag_on_pointer_cancel(
cancel: On<PointerCancel>,
mut state: ResMut<DragState>,
mut commands: Commands,
) {
if let Some(held) = state.drag.as_ref()
&& held.source == cancel.entity
{
commands.entity(held.source).remove::<Dragging>();
state.drag = None;
}
}
pub fn cancel_drags(
keys: Res<ButtonInput<KeyCode>>,
gamepads: Query<&Gamepad>,
mut state: ResMut<DragState>,
) {
if !state.is_dragging() {
return;
}
if (keys.just_pressed(KeyCode::Escape)
|| gamepads.iter().any(|g| g.just_pressed(GamepadButton::East)))
&& let Some(held) = state.drag.as_mut()
{
held.cancelled = true;
}
}
fn rect_of(node: &ComputedNode, at: &UiGlobalTransform) -> Rect {
let inverse = node.inverse_scale_factor();
Rect::from_center_size(at.translation * inverse, node.size() * inverse)
}
fn rect_array(rect: Rect) -> [f32; 4] {
[rect.min.x, rect.min.y, rect.max.x, rect.max.y]
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn carry_by_input(
keys: Res<ButtonInput<KeyCode>>,
gamepads: Query<&Gamepad>,
settings: Res<DragSettings>,
focus: Res<InputFocus>,
mut shown: ResMut<InputFocusVisible>,
mut state: ResMut<DragState>,
ui_scale: Option<Res<UiScale>>,
parents: Query<&ChildOf>,
words: (Query<&DragLabel>, Query<&Text>, Query<&Children>),
zones: Query<
(
Entity,
&DropZone,
&ComputedNode,
&UiGlobalTransform,
&InheritedVisibility,
),
Without<InteractionDisabled>,
>,
frames: Query<(&ComputedNode, &UiGlobalTransform)>,
entities: Query<EntityRef, Without<IsResource>>,
mut commands: Commands,
) {
let pad = |button: GamepadButton| gamepads.iter().any(|g| g.just_pressed(button));
let key = |code: KeyCode| keys.just_pressed(code);
let pick = settings.pick_up.just_pressed(&keys) || pad(settings.pick_up_button);
let scale = ui_scale.as_ref().map_or(1.0, |s| s.0).max(f32::EPSILON);
let Some(held) = state.drag.as_ref() else {
if !pick {
return;
}
let (labels, texts, kids) = &words;
let Some(source) = focus.get().and_then(|focused| {
std::iter::once(focused)
.chain(parents.iter_ancestors(focused))
.find(|&e| {
entities.get(e).is_ok_and(|e| {
e.contains::<DragSource>() && !e.contains::<InteractionDisabled>()
})
})
}) else {
return;
};
let label = labels
.get(source)
.map(|label| label.0.clone())
.ok()
.or_else(|| first_text(source, texts, kids))
.unwrap_or_default();
let pointer = frames
.get(source)
.map_or(Vec2::ZERO, |(node, at)| rect_of(node, at).center() * scale);
state.drag = Some(Drag {
source,
pointer_id: PointerId::Mouse,
pointer,
lifted: true,
label,
over: None,
refused: false,
cancelled: false,
keyboard: true,
});
commands.entity(source).insert(Dragging);
show_focus(&mut shown);
return;
};
if !held.keyboard || held.cancelled {
return;
}
let source = held.source;
let (Ok(source_ref), Ok((source_node, source_at))) = (entities.get(source), frames.get(source))
else {
state.drag = None;
return;
};
if pick || pad(GamepadButton::South) {
let over = held.over;
state.drag = None;
commands.entity(source).remove::<Dragging>();
let pointer = over
.and_then(|over| frames.get(over).ok())
.map_or(Vec2::ZERO, |(node, at)| rect_of(node, at).center() * scale);
if let Some(zone) = over
&& let (Ok(zone_ref), Ok((_, handlers, ..))) = (entities.get(zone), zones.get(zone))
&& let Some(drag_source) = source_ref.get::<DragSource>().copied()
{
release_on(
&mut commands,
zone,
zone_ref,
handlers,
source_ref,
drag_source,
pointer,
);
}
return;
}
let toward = [
(KeyCode::ArrowUp, GamepadButton::DPadUp, Toward::Up),
(KeyCode::ArrowDown, GamepadButton::DPadDown, Toward::Down),
(KeyCode::ArrowLeft, GamepadButton::DPadLeft, Toward::Left),
(KeyCode::ArrowRight, GamepadButton::DPadRight, Toward::Right),
]
.into_iter()
.find(|&(k, b, _)| key(k) || pad(b))
.map(|(_, _, toward)| toward);
let step = if key(KeyCode::PageDown) || pad(GamepadButton::RightTrigger) {
Some(true)
} else if key(KeyCode::PageUp) || pad(GamepadButton::LeftTrigger) {
Some(false)
} else {
None
};
if toward.is_none() && step.is_none() {
return;
}
let mut candidates: Vec<(Entity, Rect)> = zones
.iter()
.filter(|&(entity, handlers, node, _, visible)| {
entity != source
&& visible.get()
&& node.size().cmpgt(Vec2::ZERO).all()
&& entities
.get(entity)
.is_ok_and(|zone_ref| handlers.takes(zone_ref, source_ref))
})
.map(|(entity, _, node, at, _)| (entity, rect_of(node, at)))
.collect();
candidates.sort_by_key(|&(entity, _)| entity);
let rects: Vec<[f32; 4]> = candidates.iter().map(|&(_, r)| rect_array(r)).collect();
let current = state
.drag
.as_ref()
.and_then(|d| d.over)
.and_then(|over| candidates.iter().position(|&(e, _)| e == over));
let chosen = match (toward, step) {
(Some(toward), _) => {
let from =
current.map_or_else(|| rect_array(rect_of(source_node, source_at)), |i| rects[i]);
target_toward(from, &rects, toward)
}
(None, Some(forward)) => target_step(&rects, current, forward),
(None, None) => None,
};
if let Some(index) = chosen
&& let Some(held) = state.drag.as_mut()
{
let (entity, rect) = candidates[index];
held.over = Some(entity);
held.refused = false;
held.pointer = rect.center() * scale;
}
}
#[derive(Component)]
pub(crate) struct GhostPart;
#[derive(Component)]
pub(crate) struct MarkPart;
type GhostParts = (
Entity,
&'static mut Node,
&'static mut Role,
&'static mut Text,
);
type MarkParts = (Entity, &'static mut Node, &'static mut Role);
type IsGhost = (With<GhostPart>, Without<MarkPart>);
type IsMark = (With<MarkPart>, Without<GhostPart>);
type Laid = (Without<GhostPart>, Without<MarkPart>);
pub(crate) fn update_drag_visuals(
state: Option<Res<DragState>>,
ui_scale: Option<Res<UiScale>>,
mut ghosts: Query<GhostParts, IsGhost>,
mut marks: Query<MarkParts, IsMark>,
frames: Query<(&ComputedNode, &UiGlobalTransform), Laid>,
mut commands: Commands,
) {
let held = state
.as_ref()
.and_then(|state| state.drag.as_ref())
.filter(|d| d.lifted);
let Some(held) = held else {
for (entity, ..) in &ghosts {
commands.entity(entity).despawn();
}
for (entity, ..) in &marks {
commands.entity(entity).despawn();
}
return;
};
let scale = ui_scale.as_ref().map_or(1.0, |s| s.0).max(f32::EPSILON);
let at = held.pointer / scale + Vec2::splat(GHOST_OFFSET_PX);
let (ghost_role, mark_role) = if held.refused {
(Role::DragGhostRefused, Role::DropMarkRefused)
} else {
(Role::DragGhost, Role::DropMark)
};
match ghosts.iter_mut().next() {
Some((_, mut node, mut role, mut text)) => {
let mut next = node.clone();
next.left = Val::Px(at.x);
next.top = Val::Px(at.y);
node.set_if_neq(next);
role.set_if_neq(ghost_role);
text.set_if_neq(Text::new(held.label.clone()));
}
None => {
commands.spawn((
Node {
position_type: PositionType::Absolute,
left: Val::Px(at.x),
top: Val::Px(at.y),
..default()
},
Text::new(held.label.clone()),
ghost_role,
Pickable::IGNORE,
GlobalZIndex(DRAG_Z),
GhostPart,
));
}
}
let rect = held.over.and_then(|over| {
let (node, at) = frames.get(over).ok()?;
Some(rect_of(node, at))
});
match marks.iter_mut().next() {
Some((_, mut node, mut role)) => {
let mut next = node.clone();
match rect {
Some(rect) => {
next.display = Display::Flex;
next.left = Val::Px(rect.min.x);
next.top = Val::Px(rect.min.y);
next.width = Val::Px(rect.width());
next.height = Val::Px(rect.height());
}
None => next.display = Display::None,
}
node.set_if_neq(next);
role.set_if_neq(mark_role);
}
None => {
commands.spawn((
Node {
position_type: PositionType::Absolute,
display: Display::None,
..default()
},
mark_role,
Pickable::IGNORE,
GlobalZIndex(DRAG_Z - 1),
MarkPart,
));
}
}
}
type IsMarked = Or<(With<DropCandidate>, With<DropRefusing>)>;
pub(crate) fn update_drop_candidates(
state: Option<Res<DragState>>,
zones: Query<(Entity, &DropZone)>,
marked: Query<Entity, IsMarked>,
entities: Query<EntityRef, Without<IsResource>>,
mut commands: Commands,
) {
let source = state
.as_ref()
.and_then(|state| state.source())
.and_then(|source| entities.get(source).ok());
let Some(source) = source else {
for entity in &marked {
commands
.entity(entity)
.remove::<(DropCandidate, DropRefusing)>();
}
return;
};
for (entity, handlers) in &zones {
let Ok(zone) = entities.get(entity) else {
continue;
};
let live = entity != source.id()
&& !zone.contains::<InteractionDisabled>()
&& zone
.get::<InheritedVisibility>()
.is_none_or(|visible| visible.get());
let takes = live && handlers.takes(zone, source);
let refuses = live && !takes;
if takes != zone.contains::<DropCandidate>() {
if takes {
commands.entity(entity).insert(DropCandidate);
} else {
commands.entity(entity).remove::<DropCandidate>();
}
}
if refuses != zone.contains::<DropRefusing>() {
if refuses {
commands.entity(entity).insert(DropRefusing);
} else {
commands.entity(entity).remove::<DropRefusing>();
}
}
}
}