use std::sync::atomic::{AtomicU64, Ordering};
use dioxus::prelude::*;
use crate::core::monitor::CancelReason;
use crate::core::session::DragCompletion;
use crate::core::types::{DragSessionId, Point, Rect};
use super::geometry::WindowKey;
use super::state::{DndWorld, WindowRecord, ZoneLocation};
static NEXT_WORLD_DRAG_GENERATION: AtomicU64 = AtomicU64::new(1);
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct ActiveDrag {
pub(super) origin: WindowKey,
pub(super) generation: u64,
pub(super) session: Option<DragSessionId>,
pub(super) origin_scale: f64,
pub(super) source_location: Option<ZoneLocation>,
pub(super) modifiers: Modifiers,
}
impl<T: Clone + 'static> DndWorld<T> {
pub fn begin_from(&self, key: WindowKey) {
let origin = self.record(key);
let active_drag = ActiveDrag {
origin: key,
generation: NEXT_WORLD_DRAG_GENERATION.fetch_add(1, Ordering::Relaxed),
session: self
.ctx
.active_session()
.filter(|session| self.ctx.session_result(*session).is_none()),
origin_scale: origin.map_or(1.0, |record| record.geometry.scale()),
source_location: self
.ctx
.source()
.map(|zone| ZoneLocation { window: key, zone }),
modifiers: Modifiers::empty(),
};
let mut active = self.active;
if *active.peek() != Some(active_drag) {
active.set(Some(active_drag));
}
let mut settle_claim = self.settle_claim;
if settle_claim.peek().is_some() {
settle_claim.set(None);
}
let mut global_pointer = self.global_pointer;
let initial_global =
origin.and_then(|record| record.geometry.to_global(self.ctx.pointer()));
if *global_pointer.peek() != initial_global {
global_pointer.set(initial_global);
}
let mut over_location = self.over_location;
if over_location.peek().is_some() {
over_location.set(None);
}
}
pub fn active_record(&self) -> Option<WindowRecord<T>> {
let origin = self.active.peek().as_ref()?.origin;
self.record(origin)
}
pub(super) fn active_drag(&self) -> Option<ActiveDrag> {
*self.active.peek()
}
pub fn global_pointer(&self) -> Option<Point> {
*self.global_pointer.read()
}
pub fn source_location(&self) -> Option<ZoneLocation> {
self.active
.read()
.as_ref()
.and_then(|active| active.source_location)
}
pub fn over_location(&self) -> Option<ZoneLocation> {
*self.over_location.read()
}
pub fn drag_session(&self) -> Option<DragSessionId> {
self.active.peek().as_ref()?.session
}
#[cfg_attr(not(feature = "desktop"), allow(dead_code))]
pub(crate) fn drag_generation(&self) -> Option<(u64, Option<DragSessionId>)> {
let active = self.active.read();
let active = active.as_ref()?;
Some((active.generation, active.session))
}
#[cfg_attr(not(feature = "desktop"), allow(dead_code))]
pub(crate) fn drag_generation_peek(&self) -> Option<(u64, Option<DragSessionId>)> {
let active = self.active_drag()?;
Some((active.generation, active.session))
}
#[cfg_attr(not(feature = "desktop"), allow(dead_code))]
pub(crate) fn is_drag_generation(
&self,
generation: u64,
session: Option<DragSessionId>,
) -> bool {
let Some(active) = self.active_drag() else {
return false;
};
if !self.ctx.dragging() || active.generation != generation || active.session != session {
return false;
}
session.is_none_or(|session| self.ctx.is_session(session))
}
pub(crate) fn is_drag_session(&self, session: DragSessionId) -> bool {
self.drag_session() == Some(session) && self.ctx.is_session(session)
}
pub(crate) fn commit_session(&self, session: DragSessionId, dropped: bool) -> bool {
if !self.is_drag_session(session) {
return false;
}
let mut ctx = self.ctx;
ctx.commit_source(session, dropped)
}
pub(crate) fn finalize_session(&self, session: DragSessionId) -> bool {
let Some(result) = self.ctx.session_result(session) else {
return false;
};
let completion = if result {
DragCompletion::Dropped
} else {
DragCompletion::Cancelled(CancelReason::User)
};
self.finish_session(session, completion)
}
pub(crate) fn finish_session(
&self,
session: DragSessionId,
completion: DragCompletion,
) -> bool {
let mut ctx = self.ctx;
if !ctx.is_session(session) {
return false;
}
let owns_metadata = self.drag_session() == Some(session);
let result = ctx
.session_result(session)
.unwrap_or_else(|| completion.dropped());
let finished = if ctx.session_result(session).is_some() {
ctx.finalize_source(session)
} else if completion.dropped() {
ctx.finish_source(session, true)
} else {
let DragCompletion::Cancelled(reason) = completion else {
unreachable!("dropped completion handled above")
};
ctx.cancel_session(session, reason)
};
if !finished {
return false;
}
if !owns_metadata || self.drag_session() != Some(session) {
return true;
}
if ctx.dragging() {
return true;
}
if result && ctx.settling().is_some() {
let mut active = self.active;
let current = *active.peek();
if let Some(mut current) = current {
current.session = None;
active.set(Some(current));
}
self.clear_hover();
} else {
self.clear_world_state();
}
true
}
pub(crate) fn finish_untracked(&self, completion: DragCompletion) {
let mut ctx = self.ctx;
if let DragCompletion::Cancelled(reason) = completion {
if ctx.dragging() {
ctx.cancel_with_reason(reason);
}
}
if ctx.dragging() {
return;
}
if completion.dropped() && ctx.settling().is_some() {
self.clear_hover();
} else {
self.clear_world_state();
}
}
pub(crate) fn active_rect_in(
&self,
destination: WindowRecord<T>,
pointer: Point,
) -> Option<Rect> {
let source = self.ctx.source_rect()?;
let active = self.active_drag()?;
let destination_scale = destination.geometry.scale();
let scale = if active.origin_scale > 0.0 && destination_scale > 0.0 {
active.origin_scale / destination_scale
} else {
1.0
};
let grab = self.ctx.grab();
Some(Rect::new(
pointer.x - grab.x * scale,
pointer.y - grab.y * scale,
source.width * scale,
source.height * scale,
))
}
pub(super) fn clear_world_state(&self) {
let mut active = self.active;
active.set(None);
let mut global_pointer = self.global_pointer;
global_pointer.set(None);
let mut over_location = self.over_location;
over_location.set(None);
let mut settle_claim = self.settle_claim;
settle_claim.set(None);
}
pub(super) fn enter_location(&self, location: ZoneLocation) {
let mut over_location = self.over_location;
if *over_location.peek() != Some(location) {
over_location.set(Some(location));
}
let mut ctx = self.ctx;
ctx.enter(location.zone);
}
pub(super) fn clear_hover(&self) {
let mut ctx = self.ctx;
if let Some(over) = ctx.over() {
ctx.leave(over);
}
let mut over_location = self.over_location;
if over_location.peek().is_some() {
over_location.set(None);
}
}
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
use crate::core::types::{DragMode, DropEffect};
thread_local! {
static WORLD: RefCell<Option<DndWorld<String>>> = const { RefCell::new(None) };
static COMPLETION: RefCell<Option<Callback<bool>>> = const { RefCell::new(None) };
static REPLACEMENT_ORIGIN: RefCell<Option<WindowKey>> = const { RefCell::new(None) };
}
fn test_app() -> Element {
let world = use_hook(DndWorld::<String>::new);
let completion = use_callback(move |dropped: bool| {
assert!(dropped);
let replacement =
REPLACEMENT_ORIGIN.with_borrow(|key| key.expect("replacement origin"));
let mut ctx = world.context();
ctx.start(
"replacement".to_string(),
None,
Point::new(20.0, 30.0),
Point::default(),
DropEffect::Move,
DragMode::Pointer,
);
world.begin_from(replacement);
});
WORLD.with_borrow_mut(|slot| *slot = Some(world));
COMPLETION.with_borrow_mut(|slot| *slot = Some(completion));
rsx! {}
}
#[test]
fn source_completion_started_drag_owns_replacement_metadata() {
let mut dom = VirtualDom::new(test_app);
dom.rebuild_in_place();
let world = WORLD.with_borrow(|slot| slot.expect("test world"));
let completion = COMPLETION.with_borrow(|slot| slot.expect("completion callback"));
dom.in_runtime(|| {
let original = WindowKey::auto();
let replacement = WindowKey::auto();
REPLACEMENT_ORIGIN.with_borrow_mut(|key| *key = Some(replacement));
let mut ctx = world.context();
let session = ctx.start_tracked(
"original".to_string(),
None,
Point::new(10.0, 10.0),
Point::default(),
DropEffect::Move,
completion,
);
world.begin_from(original);
assert_eq!(world.drag_session(), Some(session));
assert!(ctx.take().is_some());
assert!(world.finish_session(session, DragCompletion::Dropped));
assert!(ctx.dragging());
assert_eq!(ctx.payload().as_deref(), Some("replacement"));
assert_eq!(
world.active_drag().map(|drag| drag.origin),
Some(replacement)
);
assert_eq!(world.drag_session(), None);
world.finish_untracked(DragCompletion::Cancelled(CancelReason::User));
});
}
}