use std::any::{Any, TypeId};
use std::cell::{Ref, RefCell};
use std::collections::{HashMap, VecDeque};
use std::fmt;
use std::path::PathBuf;
use std::rc::{Rc, Weak};
use frust_core::FrameTime;
use kurbo::{Point, Rect, Vec2};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DragSourceId(u64);
impl DragSourceId {
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct DragTargetId(u64);
impl DragTargetId {
pub fn get(self) -> u64 {
self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum DragKind {
Internal,
ExternalFiles(Vec<PathBuf>),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum DragPhase {
Idle,
Armed,
Dragging,
Dropping,
Cancelled,
}
#[derive(Clone, Debug, PartialEq)]
pub struct DragSession {
pub kind: DragKind,
pub source: Option<DragSourceId>,
pub press: Point,
pub pointer: Point,
pub ghost_offset: Vec2,
pub hovered: Option<DragTargetId>,
pub payload_type: Option<TypeId>,
pub keyboard: bool,
}
impl DragSession {
pub fn ghost_origin(&self) -> Point {
self.pointer + self.ghost_offset
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum DragState {
Idle,
Armed {
source: DragSourceId,
press: Point,
keyboard: bool,
},
Dragging(DragSession),
Dropping {
session: DragSession,
target: DragTargetId,
},
}
impl DragState {
pub fn phase(&self) -> DragPhase {
match self {
DragState::Idle => DragPhase::Idle,
DragState::Armed { .. } => DragPhase::Armed,
DragState::Dragging(_) => DragPhase::Dragging,
DragState::Dropping { .. } => DragPhase::Dropping,
}
}
pub fn session(&self) -> Option<&DragSession> {
match self {
DragState::Dragging(session) | DragState::Dropping { session, .. } => Some(session),
DragState::Idle | DragState::Armed { .. } => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum DragStateChange {
Phase {
previous: DragPhase,
next: DragPhase,
},
Enter {
target: DragTargetId,
},
Leave {
target: DragTargetId,
},
Move {
pointer: Point,
},
}
type Listener = Rc<dyn Fn(DragStateChange)>;
struct Session {
kind: DragKind,
source: Option<DragSourceId>,
press: Point,
pointer: Point,
ghost_offset: Vec2,
hovered: Option<DragTargetId>,
payload: Option<Box<dyn Any>>,
payload_type: TypeId,
keyboard: bool,
}
impl Session {
fn snapshot(&self) -> DragSession {
DragSession {
kind: self.kind.clone(),
source: self.source,
press: self.press,
pointer: self.pointer,
ghost_offset: self.ghost_offset,
hovered: self.hovered,
payload_type: self.payload.as_ref().map(|_| self.payload_type),
keyboard: self.keyboard,
}
}
}
enum Machine {
Idle,
Armed {
source: DragSourceId,
press: Point,
keyboard: bool,
},
Dragging(Session),
Dropping {
session: Session,
target: DragTargetId,
},
}
impl Machine {
fn phase(&self) -> DragPhase {
match self {
Machine::Idle => DragPhase::Idle,
Machine::Armed { .. } => DragPhase::Armed,
Machine::Dragging(_) => DragPhase::Dragging,
Machine::Dropping { .. } => DragPhase::Dropping,
}
}
fn session(&self) -> Option<&Session> {
match self {
Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
Machine::Idle | Machine::Armed { .. } => None,
}
}
fn session_mut(&mut self) -> Option<&mut Session> {
match self {
Machine::Dragging(session) | Machine::Dropping { session, .. } => Some(session),
Machine::Idle | Machine::Armed { .. } => None,
}
}
}
struct Shared {
machine: Machine,
next_id: u64,
listeners: Vec<(u64, Listener)>,
next_listener: u64,
queue: VecDeque<DragStateChange>,
dispatching: bool,
targets: HashMap<DragTargetId, TargetEntry>,
next_registration: u64,
pass: u64,
pass_frame: Option<FrameTime>,
}
#[derive(Clone, Copy, Debug)]
struct TargetEntry {
registration: u64,
accepts: Option<TypeId>,
bounds: Option<Rect>,
reported: Option<u64>,
}
impl Default for Shared {
fn default() -> Self {
Self {
machine: Machine::Idle,
next_id: 1,
listeners: Vec::new(),
next_listener: 0,
queue: VecDeque::new(),
dispatching: false,
targets: HashMap::new(),
next_registration: 0,
pass: 0,
pass_frame: None,
}
}
}
impl Shared {
fn cancel(&mut self, changes: &mut Vec<DragStateChange>) -> Option<Session> {
let previous = self.machine.phase();
let (leave, session) = match std::mem::replace(&mut self.machine, Machine::Idle) {
Machine::Idle => return None,
Machine::Armed { .. } => (None, None),
Machine::Dragging(session) => (session.hovered, Some(session)),
Machine::Dropping { session, target } => (Some(target), Some(session)),
};
if let Some(target) = leave {
changes.push(DragStateChange::Leave { target });
}
changes.push(DragStateChange::Phase {
previous,
next: DragPhase::Cancelled,
});
changes.push(DragStateChange::Phase {
previous: DragPhase::Cancelled,
next: DragPhase::Idle,
});
session
}
fn live_payload_type(&self) -> Option<TypeId> {
self.machine.session().map(|session| session.payload_type)
}
fn resolvable(&self, entry: &TargetEntry, payload: Option<TypeId>) -> Option<Rect> {
self.resolvable_as_of(entry, payload, self.pass)
}
fn resolvable_as_of(
&self,
entry: &TargetEntry,
payload: Option<TypeId>,
pass: u64,
) -> Option<Rect> {
if entry.reported != Some(pass) {
return None;
}
if let (Some(accepts), Some(payload)) = (entry.accepts, payload)
&& accepts != payload
{
return None;
}
entry.bounds
}
fn target_at(&self, point: Point) -> Option<DragTargetId> {
let payload = self.live_payload_type();
self.targets
.iter()
.filter(|(_, entry)| {
self.resolvable(entry, payload)
.is_some_and(|rect| rect.contains(point))
})
.max_by_key(|(_, entry)| entry.registration)
.map(|(&id, _)| id)
}
fn target_at_mid_pass(&self, point: Point) -> Option<DragTargetId> {
let payload = self.live_payload_type();
let previous = self.pass.checked_sub(1);
self.targets
.iter()
.filter(|(_, entry)| {
self.resolvable_as_of(entry, payload, self.pass)
.or_else(|| {
previous.and_then(|pass| self.resolvable_as_of(entry, payload, pass))
})
.is_some_and(|rect| rect.contains(point))
})
.max_by_key(|(_, entry)| entry.registration)
.map(|(&id, _)| id)
}
fn record_bounds(
&mut self,
id: DragTargetId,
bounds: Option<Rect>,
frame: Option<FrameTime>,
) -> bool {
let Some(reported) = self.targets.get(&id).map(|entry| entry.reported) else {
return false;
};
let opens = match frame {
Some(frame) => self.pass_frame != Some(frame),
None => reported == Some(self.pass),
};
if opens {
self.pass += 1;
self.pass_frame = frame;
}
let pass = self.pass;
if let Some(entry) = self.targets.get_mut(&id) {
entry.bounds = bounds;
entry.reported = Some(pass);
}
true
}
fn set_hovered(&mut self, target: Option<DragTargetId>, changes: &mut Vec<DragStateChange>) {
if let Machine::Dragging(session) = &mut self.machine
&& session.hovered != target
{
if let Some(old) = session.hovered {
changes.push(DragStateChange::Leave { target: old });
}
session.hovered = target;
if let Some(target) = target {
changes.push(DragStateChange::Enter { target });
}
}
}
fn resolve_hover(&mut self, changes: &mut Vec<DragStateChange>) {
let Machine::Dragging(session) = &self.machine else {
return;
};
if session.keyboard {
return;
}
let target = self.target_at(session.pointer);
self.set_hovered(target, changes);
}
fn resolve_hover_mid_pass(&mut self, changes: &mut Vec<DragStateChange>) {
let Machine::Dragging(session) = &self.machine else {
return;
};
if session.keyboard {
return;
}
let target = self.target_at_mid_pass(session.pointer);
self.set_hovered(target, changes);
}
fn arm_from_idle(
&mut self,
source: DragSourceId,
press: Point,
keyboard: bool,
changes: &mut Vec<DragStateChange>,
) {
debug_assert!(matches!(self.machine, Machine::Idle));
self.machine = Machine::Armed {
source,
press,
keyboard,
};
changes.push(DragStateChange::Phase {
previous: DragPhase::Idle,
next: DragPhase::Armed,
});
}
fn move_target(&mut self, step: i64, changes: &mut Vec<DragStateChange>) {
let Machine::Dragging(session) = &self.machine else {
return;
};
let payload = Some(session.payload_type);
let mut ordered: Vec<(u64, DragTargetId)> = self
.targets
.iter()
.filter(|(_, entry)| self.resolvable(entry, payload).is_some())
.map(|(&id, entry)| (entry.registration, id))
.collect();
if ordered.is_empty() {
return;
}
ordered.sort_unstable();
let ids: Vec<DragTargetId> = ordered.into_iter().map(|(_, id)| id).collect();
let len = ids.len() as i64;
let next = match session
.hovered
.and_then(|current| ids.iter().position(|&id| id == current))
{
Some(index) => ids[(index as i64 + step).rem_euclid(len) as usize],
None if step >= 0 => ids[0],
None => ids[ids.len() - 1],
};
self.set_hovered(Some(next), changes);
}
}
#[derive(Clone, Default)]
pub struct DragCoordinator {
shared: Rc<RefCell<Shared>>,
}
impl fmt::Debug for DragCoordinator {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let shared = self.shared.borrow();
f.debug_struct("DragCoordinator")
.field("phase", &shared.machine.phase())
.field("listeners", &shared.listeners.len())
.finish()
}
}
impl DragCoordinator {
pub fn new() -> Self {
Self::default()
}
pub fn new_source_id(&self) -> DragSourceId {
DragSourceId(self.next_id())
}
pub fn new_target_id(&self) -> DragTargetId {
DragTargetId(self.next_id())
}
fn next_id(&self) -> u64 {
let mut shared = self.shared.borrow_mut();
let id = shared.next_id;
shared.next_id += 1;
id
}
pub fn state(&self) -> DragState {
match &self.shared.borrow().machine {
Machine::Idle => DragState::Idle,
Machine::Armed {
source,
press,
keyboard,
} => DragState::Armed {
source: *source,
press: *press,
keyboard: *keyboard,
},
Machine::Dragging(session) => DragState::Dragging(session.snapshot()),
Machine::Dropping { session, target } => DragState::Dropping {
session: session.snapshot(),
target: *target,
},
}
}
pub fn phase(&self) -> DragPhase {
self.shared.borrow().machine.phase()
}
pub fn arm(&self, source: DragSourceId, press: Point) -> bool {
self.mutate(|shared, changes| {
if matches!(shared.machine, Machine::Dragging(_)) {
log_ignored("arm", "a drag is already in flight");
return false;
}
shared.cancel(changes);
shared.arm_from_idle(source, press, false, changes);
true
})
}
pub fn lift(&self, source: DragSourceId) -> bool {
self.mutate(|shared, changes| {
if matches!(shared.machine, Machine::Dragging(_)) {
log_ignored("lift", "a drag is already in flight");
return false;
}
shared.cancel(changes);
shared.arm_from_idle(source, Point::ZERO, true, changes);
true
})
}
pub fn move_to_next_target(&self) {
self.mutate(|shared, changes| shared.move_target(1, changes));
}
pub fn move_to_previous_target(&self) {
self.mutate(|shared, changes| shared.move_target(-1, changes));
}
pub fn begin<T: Any>(&self, payload: T) -> bool {
self.mutate(|shared, changes| {
let (source, press, pointer, ghost_offset, keyboard) = match &shared.machine {
Machine::Idle => {
log_ignored("begin", "nothing is armed");
return false;
}
Machine::Dropping { .. } => {
log_ignored("begin", "a drop is still being completed");
return false;
}
Machine::Armed {
source,
press,
keyboard,
} => (*source, *press, *press, Vec2::ZERO, *keyboard),
Machine::Dragging(_) => {
let first = shared
.cancel(changes)
.expect("a Dragging machine cancels to its session");
let Some(source) = first.source else {
log_ignored("begin", "the cancelled session had no source to restart");
return false;
};
shared.arm_from_idle(source, first.press, first.keyboard, changes);
(
source,
first.press,
first.pointer,
first.ghost_offset,
first.keyboard,
)
}
};
shared.machine = Machine::Dragging(Session {
kind: DragKind::Internal,
source: Some(source),
press,
pointer,
ghost_offset,
hovered: None,
payload: Some(Box::new(payload)),
payload_type: TypeId::of::<T>(),
keyboard,
});
changes.push(DragStateChange::Phase {
previous: DragPhase::Armed,
next: DragPhase::Dragging,
});
true
})
}
pub fn begin_external(&self, paths: Vec<PathBuf>, pointer: Point) {
self.mutate(|shared, changes| {
shared.cancel(changes);
shared.machine = Machine::Dragging(Session {
kind: DragKind::ExternalFiles(paths.clone()),
source: None,
press: pointer,
pointer,
ghost_offset: Vec2::ZERO,
hovered: None,
payload: Some(Box::new(paths)),
payload_type: TypeId::of::<Vec<PathBuf>>(),
keyboard: false,
});
changes.push(DragStateChange::Phase {
previous: DragPhase::Idle,
next: DragPhase::Dragging,
});
});
}
pub fn set_external_payload(&self, paths: Vec<PathBuf>) {
if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine
&& matches!(session.kind, DragKind::ExternalFiles(_))
{
session.kind = DragKind::ExternalFiles(paths.clone());
session.payload = Some(Box::new(paths));
}
}
pub fn update_pointer(&self, pointer: Point) {
self.mutate(|shared, changes| {
let Machine::Dragging(session) = &mut shared.machine else {
return;
};
if session.keyboard {
return;
}
let moved = session.pointer != pointer;
session.pointer = pointer;
shared.resolve_hover(changes);
if moved {
changes.push(DragStateChange::Move { pointer });
}
});
}
pub fn resolve_hover(&self) {
self.mutate(|shared, changes| shared.resolve_hover_mid_pass(changes));
}
pub fn set_ghost_offset(&self, offset: Vec2) {
if let Machine::Dragging(session) = &mut self.shared.borrow_mut().machine {
session.ghost_offset = offset;
}
}
pub fn set_hovered(&self, target: Option<DragTargetId>) {
self.mutate(|shared, changes| shared.set_hovered(target, changes));
}
pub fn register_target(&self, id: DragTargetId) {
self.register(id, None);
}
pub fn register_target_for<T: Any>(&self, id: DragTargetId) {
self.register(id, Some(TypeId::of::<T>()));
}
fn register(&self, id: DragTargetId, accepts: Option<TypeId>) {
let mut shared = self.shared.borrow_mut();
if shared.targets.contains_key(&id) {
return;
}
let registration = shared.next_registration;
shared.next_registration += 1;
shared.targets.insert(
id,
TargetEntry {
registration,
accepts,
bounds: None,
reported: None,
},
);
}
pub fn unregister_target(&self, id: DragTargetId) {
self.mutate(|shared, changes| {
shared.targets.remove(&id);
match &mut shared.machine {
Machine::Dragging(session) if session.hovered == Some(id) => {
session.hovered = None;
changes.push(DragStateChange::Leave { target: id });
}
Machine::Dropping { target, .. } if *target == id => {
shared.cancel(changes);
}
_ => {}
}
});
}
pub fn set_target_bounds(&self, id: DragTargetId, bounds: Rect) {
if !self
.shared
.borrow_mut()
.record_bounds(id, Some(bounds), None)
{
log_ignored("set_target_bounds", "the target is not registered");
}
}
pub fn report_target_bounds(&self, id: DragTargetId, bounds: Option<Rect>, frame: FrameTime) {
let frame = (frame != FrameTime::ZERO).then_some(frame);
if !self.shared.borrow_mut().record_bounds(id, bounds, frame) {
log_ignored("report_target_bounds", "the target is not registered");
}
}
pub fn target_bounds(&self, id: DragTargetId) -> Option<Rect> {
self.shared
.borrow()
.targets
.get(&id)
.and_then(|entry| entry.bounds)
}
pub fn target_at(&self, point: Point) -> Option<DragTargetId> {
self.shared.borrow().target_at(point)
}
pub fn is_external(&self) -> bool {
self.shared
.borrow()
.machine
.session()
.is_some_and(|session| matches!(session.kind, DragKind::ExternalFiles(_)))
}
pub fn registered_targets(&self) -> Vec<DragTargetId> {
let shared = self.shared.borrow();
let mut targets: Vec<(u64, DragTargetId)> = shared
.targets
.iter()
.map(|(&id, entry)| (entry.registration, id))
.collect();
targets.sort_unstable();
targets.into_iter().map(|(_, id)| id).collect()
}
pub fn drop(&self) -> Option<DragTargetId> {
self.mutate(|shared, changes| match &shared.machine {
Machine::Idle | Machine::Dropping { .. } => None,
Machine::Armed { .. } => {
shared.cancel(changes);
None
}
Machine::Dragging(session) => {
let Some(target) = session.hovered else {
shared.cancel(changes);
return None;
};
let Machine::Dragging(session) =
std::mem::replace(&mut shared.machine, Machine::Idle)
else {
unreachable!("matched Dragging above");
};
shared.machine = Machine::Dropping { session, target };
changes.push(DragStateChange::Phase {
previous: DragPhase::Dragging,
next: DragPhase::Dropping,
});
Some(target)
}
})
}
pub fn complete_drop(&self) {
self.mutate(|shared, changes| {
if matches!(shared.machine, Machine::Dropping { .. }) {
shared.machine = Machine::Idle;
changes.push(DragStateChange::Phase {
previous: DragPhase::Dropping,
next: DragPhase::Idle,
});
}
});
}
pub fn cancel(&self) {
self.mutate(|shared, changes| {
shared.cancel(changes);
});
}
pub fn end_external(&self) -> bool {
self.mutate(|shared, changes| {
let ends = matches!(
&shared.machine,
Machine::Dragging(session) if matches!(session.kind, DragKind::ExternalFiles(_))
);
if ends {
shared.cancel(changes);
}
ends
})
}
pub fn payload_is<T: Any>(&self) -> bool {
self.shared
.borrow()
.machine
.session()
.is_some_and(|session| {
session.payload.is_some() && session.payload_type == TypeId::of::<T>()
})
}
pub fn payload_ref<T: Any>(&self) -> Option<Ref<'_, T>> {
Ref::filter_map(self.shared.borrow(), |shared| {
shared
.machine
.session()?
.payload
.as_ref()?
.downcast_ref::<T>()
})
.ok()
}
pub fn with_payload<T: Any, R>(&self, f: impl FnOnce(&T) -> R) -> Option<R> {
self.payload_ref::<T>().map(|payload| f(&payload))
}
pub fn take_payload<T: Any>(&self) -> Option<Box<T>> {
let mut shared = self.shared.borrow_mut();
let session = shared.machine.session_mut()?;
if session.payload_type != TypeId::of::<T>() {
return None;
}
session.payload.take()?.downcast::<T>().ok()
}
pub fn subscribe<F: Fn(DragStateChange) + 'static>(&self, listener: F) -> DragSubscription {
let mut shared = self.shared.borrow_mut();
let id = shared.next_listener;
shared.next_listener += 1;
shared.listeners.push((id, Rc::new(listener)));
DragSubscription {
shared: Rc::downgrade(&self.shared),
id,
}
}
fn mutate<R>(&self, f: impl FnOnce(&mut Shared, &mut Vec<DragStateChange>) -> R) -> R {
let mut changes = Vec::new();
let result = {
let mut shared = self.shared.borrow_mut();
let result = f(&mut shared, &mut changes);
shared.queue.extend(changes.iter().copied());
result
};
if !changes.is_empty() {
frust_core::mark_pending_result_flush();
self.dispatch();
}
result
}
fn dispatch(&self) {
{
let mut shared = self.shared.borrow_mut();
if shared.dispatching {
return;
}
shared.dispatching = true;
}
let _guard = DispatchGuard(&self.shared);
loop {
let (change, listeners) = {
let mut shared = self.shared.borrow_mut();
let Some(change) = shared.queue.pop_front() else {
break;
};
(change, shared.listeners.clone())
};
for (id, listener) in listeners {
let live = self
.shared
.borrow()
.listeners
.iter()
.any(|(other, _)| *other == id);
if live {
listener(change);
}
}
}
}
}
struct DispatchGuard<'a>(&'a RefCell<Shared>);
impl Drop for DispatchGuard<'_> {
fn drop(&mut self) {
if let Ok(mut shared) = self.0.try_borrow_mut() {
shared.dispatching = false;
if std::thread::panicking() {
shared.queue.clear();
}
}
}
}
#[must_use = "dropping a DragSubscription unsubscribes its listener at once"]
pub struct DragSubscription {
shared: Weak<RefCell<Shared>>,
id: u64,
}
impl fmt::Debug for DragSubscription {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DragSubscription")
.field("id", &self.id)
.finish()
}
}
impl Drop for DragSubscription {
fn drop(&mut self) {
if let Some(shared) = self.shared.upgrade() {
shared
.borrow_mut()
.listeners
.retain(|(id, _)| *id != self.id);
}
}
}
fn log_ignored(_call: &str, _why: &str) {
#[cfg(debug_assertions)]
eprintln!("frust-widgets: DragCoordinator::{_call} ignored: {_why}");
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use super::*;
const PRESS: Point = Point::new(10.0, 10.0);
const MOVED: Point = Point::new(20.0, 30.0);
const OTHER_PRESS: Point = Point::new(5.0, 5.0);
const FILE_ENTRY: Point = Point::new(1.0, 2.0);
struct Fixture {
drag: DragCoordinator,
source: DragSourceId,
other_source: DragSourceId,
first: DragTargetId,
second: DragTargetId,
log: Rc<RefCell<Vec<DragStateChange>>>,
_subscription: DragSubscription,
}
impl Fixture {
fn new() -> Self {
let drag = DragCoordinator::new();
let log = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&log);
let subscription = drag.subscribe(move |change| sink.borrow_mut().push(change));
Self {
source: drag.new_source_id(),
other_source: drag.new_source_id(),
first: drag.new_target_id(),
second: drag.new_target_id(),
drag,
log,
_subscription: subscription,
}
}
fn take_log(&self) -> Vec<DragStateChange> {
std::mem::take(&mut *self.log.borrow_mut())
}
}
#[derive(Clone, Copy, Debug)]
enum Start {
Idle,
Armed,
Dragging,
Hovering,
Dropping,
External,
}
impl Start {
fn reach(self, f: &Fixture) {
match self {
Start::Idle => {}
Start::Armed => {
f.drag.arm(f.source, PRESS);
}
Start::Dragging => {
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
}
Start::Hovering => {
Start::Dragging.reach(f);
f.drag.set_hovered(Some(f.first));
}
Start::Dropping => {
Start::Hovering.reach(f);
f.drag.drop();
}
Start::External => {
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
}
}
}
}
#[derive(Clone, Copy, Debug)]
enum Op {
Arm,
Begin,
BeginExternal,
Move,
HoverSecond,
HoverNone,
Drop,
Complete,
Cancel,
}
impl Op {
fn apply(self, f: &Fixture) {
match self {
Op::Arm => {
f.drag.arm(f.other_source, OTHER_PRESS);
}
Op::Begin => {
f.drag.begin("second");
}
Op::BeginExternal => {
f.drag
.begin_external(vec![PathBuf::from("/tmp/b.txt")], FILE_ENTRY);
}
Op::Move => f.drag.update_pointer(MOVED),
Op::HoverSecond => f.drag.set_hovered(Some(f.second)),
Op::HoverNone => f.drag.set_hovered(None),
Op::Drop => {
f.drag.drop();
}
Op::Complete => f.drag.complete_drop(),
Op::Cancel => f.drag.cancel(),
}
}
}
#[derive(Clone, Copy, Debug)]
enum Expect {
Phase(DragPhase, DragPhase),
EnterSecond,
LeaveFirst,
Moved,
}
impl Expect {
fn resolve(self, f: &Fixture) -> DragStateChange {
match self {
Expect::Phase(previous, next) => DragStateChange::Phase { previous, next },
Expect::EnterSecond => DragStateChange::Enter { target: f.second },
Expect::LeaveFirst => DragStateChange::Leave { target: f.first },
Expect::Moved => DragStateChange::Move { pointer: MOVED },
}
}
}
use DragPhase::{Armed, Cancelled, Dragging, Dropping, Idle};
const CANCEL_FROM_ARMED: &[Expect] = &[
Expect::Phase(Armed, Cancelled),
Expect::Phase(Cancelled, Idle),
];
const CANCEL_FROM_DRAGGING: &[Expect] = &[
Expect::Phase(Dragging, Cancelled),
Expect::Phase(Cancelled, Idle),
];
fn table() -> Vec<(Start, Op, DragPhase, Vec<Expect>)> {
use Expect::{EnterSecond, LeaveFirst, Moved, Phase};
let cat = |parts: &[&[Expect]]| parts.concat();
vec![
(Start::Idle, Op::Arm, Armed, vec![Phase(Idle, Armed)]),
(Start::Idle, Op::Begin, Idle, vec![]),
(
Start::Idle,
Op::BeginExternal,
Dragging,
vec![Phase(Idle, Dragging)],
),
(Start::Idle, Op::Move, Idle, vec![]),
(Start::Idle, Op::HoverSecond, Idle, vec![]),
(Start::Idle, Op::HoverNone, Idle, vec![]),
(Start::Idle, Op::Drop, Idle, vec![]),
(Start::Idle, Op::Complete, Idle, vec![]),
(Start::Idle, Op::Cancel, Idle, vec![]),
(
Start::Armed,
Op::Arm,
Armed,
cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Armed)]]),
),
(
Start::Armed,
Op::Begin,
Dragging,
vec![Phase(Armed, Dragging)],
),
(
Start::Armed,
Op::BeginExternal,
Dragging,
cat(&[CANCEL_FROM_ARMED, &[Phase(Idle, Dragging)]]),
),
(Start::Armed, Op::Move, Armed, vec![]),
(Start::Armed, Op::HoverSecond, Armed, vec![]),
(Start::Armed, Op::HoverNone, Armed, vec![]),
(Start::Armed, Op::Drop, Idle, CANCEL_FROM_ARMED.to_vec()),
(Start::Armed, Op::Complete, Armed, vec![]),
(Start::Armed, Op::Cancel, Idle, CANCEL_FROM_ARMED.to_vec()),
(Start::Dragging, Op::Arm, Dragging, vec![]),
(
Start::Dragging,
Op::Begin,
Dragging,
cat(&[
CANCEL_FROM_DRAGGING,
&[Phase(Idle, Armed), Phase(Armed, Dragging)],
]),
),
(
Start::Dragging,
Op::BeginExternal,
Dragging,
cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
),
(Start::Dragging, Op::Move, Dragging, vec![Moved]),
(
Start::Dragging,
Op::HoverSecond,
Dragging,
vec![EnterSecond],
),
(Start::Dragging, Op::HoverNone, Dragging, vec![]),
(
Start::Dragging,
Op::Drop,
Idle,
CANCEL_FROM_DRAGGING.to_vec(),
),
(Start::Dragging, Op::Complete, Dragging, vec![]),
(
Start::Dragging,
Op::Cancel,
Idle,
CANCEL_FROM_DRAGGING.to_vec(),
),
(Start::Hovering, Op::Arm, Dragging, vec![]),
(
Start::Hovering,
Op::Begin,
Dragging,
cat(&[
&[LeaveFirst],
CANCEL_FROM_DRAGGING,
&[Phase(Idle, Armed), Phase(Armed, Dragging)],
]),
),
(
Start::Hovering,
Op::BeginExternal,
Dragging,
cat(&[
&[LeaveFirst],
CANCEL_FROM_DRAGGING,
&[Phase(Idle, Dragging)],
]),
),
(Start::Hovering, Op::Move, Dragging, vec![LeaveFirst, Moved]),
(
Start::Hovering,
Op::HoverSecond,
Dragging,
vec![LeaveFirst, EnterSecond],
),
(Start::Hovering, Op::HoverNone, Dragging, vec![LeaveFirst]),
(
Start::Hovering,
Op::Drop,
Dropping,
vec![Phase(Dragging, Dropping)],
),
(Start::Hovering, Op::Complete, Dragging, vec![]),
(
Start::Hovering,
Op::Cancel,
Idle,
cat(&[&[LeaveFirst], CANCEL_FROM_DRAGGING]),
),
(
Start::Dropping,
Op::Arm,
Armed,
vec![
LeaveFirst,
Phase(Dropping, Cancelled),
Phase(Cancelled, Idle),
Phase(Idle, Armed),
],
),
(Start::Dropping, Op::Begin, Dropping, vec![]),
(
Start::Dropping,
Op::BeginExternal,
Dragging,
vec![
LeaveFirst,
Phase(Dropping, Cancelled),
Phase(Cancelled, Idle),
Phase(Idle, Dragging),
],
),
(Start::Dropping, Op::Move, Dropping, vec![]),
(Start::Dropping, Op::HoverSecond, Dropping, vec![]),
(Start::Dropping, Op::HoverNone, Dropping, vec![]),
(Start::Dropping, Op::Drop, Dropping, vec![]),
(
Start::Dropping,
Op::Complete,
Idle,
vec![Phase(Dropping, Idle)],
),
(
Start::Dropping,
Op::Cancel,
Idle,
vec![
LeaveFirst,
Phase(Dropping, Cancelled),
Phase(Cancelled, Idle),
],
),
(Start::External, Op::Arm, Dragging, vec![]),
(
Start::External,
Op::Begin,
Idle,
CANCEL_FROM_DRAGGING.to_vec(),
),
(
Start::External,
Op::BeginExternal,
Dragging,
cat(&[CANCEL_FROM_DRAGGING, &[Phase(Idle, Dragging)]]),
),
(Start::External, Op::Move, Dragging, vec![Moved]),
(
Start::External,
Op::HoverSecond,
Dragging,
vec![EnterSecond],
),
(Start::External, Op::HoverNone, Dragging, vec![]),
(
Start::External,
Op::Drop,
Idle,
CANCEL_FROM_DRAGGING.to_vec(),
),
(Start::External, Op::Complete, Dragging, vec![]),
(
Start::External,
Op::Cancel,
Idle,
CANCEL_FROM_DRAGGING.to_vec(),
),
]
}
#[test]
fn every_transition_lands_on_the_tabled_phase_with_the_tabled_changes() {
let rows = table();
assert_eq!(rows.len(), 6 * 9, "every start phase x every operation");
for (start, op, phase, expect) in rows {
let f = Fixture::new();
start.reach(&f);
f.take_log();
op.apply(&f);
let expected: Vec<DragStateChange> = expect.iter().map(|e| e.resolve(&f)).collect();
assert_eq!(f.take_log(), expected, "{start:?} + {op:?}: changes");
assert_eq!(f.drag.phase(), phase, "{start:?} + {op:?}: phase");
assert_eq!(
f.drag.state().phase(),
phase,
"{start:?} + {op:?}: state snapshot agrees with phase"
);
assert!(
f.drag.phase() != Cancelled,
"{start:?} + {op:?}: Cancelled is never a resting phase"
);
}
}
#[test]
fn setup_paths_report_their_own_changes() {
let f = Fixture::new();
Start::Dropping.reach(&f);
assert_eq!(
f.take_log(),
vec![
DragStateChange::Phase {
previous: Idle,
next: Armed
},
DragStateChange::Phase {
previous: Armed,
next: Dragging
},
DragStateChange::Enter { target: f.first },
DragStateChange::Phase {
previous: Dragging,
next: Dropping
},
]
);
}
#[test]
fn set_external_payload_replaces_the_held_paths_and_notifies_nothing() {
let f = Fixture::new();
f.drag.begin_external(Vec::new(), FILE_ENTRY);
f.take_log();
let real_paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.txt")];
f.drag.set_external_payload(real_paths.clone());
assert_eq!(
f.take_log(),
Vec::<DragStateChange>::new(),
"replacing the payload notifies no subscriber"
);
assert_eq!(
f.drag.with_payload::<Vec<PathBuf>, _>(Clone::clone),
Some(real_paths.clone())
);
f.drag.set_hovered(Some(f.first));
f.take_log();
assert_eq!(f.drag.drop(), Some(f.first));
assert_eq!(
f.drag.take_payload::<Vec<PathBuf>>().map(|b| *b),
Some(real_paths),
"the drop target receives the replaced paths, not the empty hover list"
);
}
#[test]
fn set_external_payload_is_a_no_op_outside_an_external_dragging_session() {
let f = Fixture::new();
f.drag
.set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
assert_eq!(f.drag.phase(), DragPhase::Idle);
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
f.drag
.set_external_payload(vec![PathBuf::from("/tmp/a.txt")]);
assert_eq!(f.drag.with_payload::<i32, _>(|v| *v), Some(7));
}
#[test]
fn a_session_snapshot_tracks_source_pointer_hover_and_drop_target() {
let f = Fixture::new();
f.drag.arm(f.source, PRESS);
assert_eq!(
f.drag.state(),
DragState::Armed {
source: f.source,
press: PRESS,
keyboard: false,
}
);
f.drag.begin(7_i32);
f.drag.set_ghost_offset(Vec2::new(-4.0, -6.0));
f.drag.update_pointer(MOVED);
f.drag.set_hovered(Some(f.first));
let DragState::Dragging(session) = f.drag.state() else {
panic!("expected Dragging");
};
assert_eq!(session.kind, DragKind::Internal);
assert_eq!(session.source, Some(f.source));
assert_eq!(session.press, PRESS);
assert_eq!(session.pointer, MOVED);
assert_eq!(session.ghost_origin(), Point::new(16.0, 24.0));
assert_eq!(session.hovered, Some(f.first));
assert_eq!(session.payload_type, Some(TypeId::of::<i32>()));
assert_eq!(f.drag.drop(), Some(f.first));
let DragState::Dropping { session, target } = f.drag.state() else {
panic!("expected Dropping");
};
assert_eq!(target, f.first);
assert_eq!(session.pointer, MOVED);
}
#[test]
fn a_second_begin_restarts_from_the_same_source_at_the_current_pointer() {
let f = Fixture::new();
f.drag.arm(f.source, PRESS);
assert!(f.drag.begin(1_u8));
f.drag.set_ghost_offset(Vec2::new(-2.0, -2.0));
f.drag.update_pointer(MOVED);
assert!(f.drag.begin("replacement"));
let DragState::Dragging(session) = f.drag.state() else {
panic!("expected Dragging");
};
assert_eq!(session.source, Some(f.source));
assert_eq!(session.press, PRESS);
assert_eq!(session.pointer, MOVED);
assert_eq!(session.ghost_offset, Vec2::new(-2.0, -2.0));
assert_eq!(session.hovered, None);
assert!(f.drag.payload_is::<&str>());
assert!(
!f.drag.payload_is::<u8>(),
"the first payload was discarded"
);
}
#[test]
fn begin_without_arm_and_arm_while_dragging_report_failure() {
let f = Fixture::new();
assert!(!f.drag.begin(1_u8), "begin from Idle is ignored");
assert!(f.drag.arm(f.source, PRESS));
assert!(f.drag.begin(1_u8));
assert!(
!f.drag.arm(f.other_source, OTHER_PRESS),
"arm while dragging is ignored"
);
assert_eq!(
f.drag.state().session().and_then(|s| s.source),
Some(f.source)
);
}
#[test]
fn a_typed_payload_round_trips_and_moves_out_once() {
#[derive(Debug, PartialEq)]
struct Card {
id: u32,
title: String,
}
let f = Fixture::new();
assert!(!f.drag.payload_is::<Card>(), "no payload while Idle");
assert!(f.drag.take_payload::<Card>().is_none());
f.drag.arm(f.source, PRESS);
assert!(
f.drag.payload_ref::<Card>().is_none(),
"no payload while Armed"
);
f.drag.begin(Card {
id: 3,
title: "three".into(),
});
assert!(f.drag.payload_is::<Card>());
assert!(!f.drag.payload_is::<u32>());
assert_eq!(f.drag.payload_ref::<Card>().map(|card| card.id), Some(3));
assert!(
f.drag.payload_ref::<String>().is_none(),
"wrong type reads None"
);
assert_eq!(
f.drag
.with_payload(|card: &Card| card.title.clone())
.as_deref(),
Some("three")
);
assert_eq!(f.drag.with_payload(|n: &u32| *n), None);
assert!(
f.drag.take_payload::<u32>().is_none(),
"a wrong-type take leaves the payload in place"
);
assert!(f.drag.payload_is::<Card>());
f.drag.set_hovered(Some(f.first));
f.drag.drop();
let taken = f
.drag
.take_payload::<Card>()
.expect("the drop target claims it");
assert_eq!(
*taken,
Card {
id: 3,
title: "three".into()
}
);
assert!(
f.drag.take_payload::<Card>().is_none(),
"a second take is None"
);
assert!(!f.drag.payload_is::<Card>());
assert_eq!(
f.drag.state().session().and_then(|s| s.payload_type),
None,
"the snapshot reports the payload gone"
);
f.drag.complete_drop();
assert_eq!(f.drag.phase(), Idle);
}
#[test]
fn a_payload_is_discarded_by_cancel_and_by_an_unclaimed_completion() {
let f = Fixture::new();
let probe = Rc::new(());
f.drag.arm(f.source, PRESS);
f.drag.begin(Rc::clone(&probe));
assert_eq!(Rc::strong_count(&probe), 2);
f.drag.cancel();
assert_eq!(Rc::strong_count(&probe), 1, "cancel drops the payload");
f.drag.arm(f.source, PRESS);
f.drag.begin(Rc::clone(&probe));
f.drag.set_hovered(Some(f.first));
f.drag.drop();
assert_eq!(Rc::strong_count(&probe), 2, "still held while Dropping");
f.drag.complete_drop();
assert_eq!(
Rc::strong_count(&probe),
1,
"an unclaimed payload is dropped on completion"
);
}
#[test]
fn subscribers_hear_every_change_in_subscription_order() {
let drag = DragCoordinator::new();
let source = drag.new_source_id();
let log = Rc::new(RefCell::new(Vec::new()));
let subscriptions: Vec<DragSubscription> = (0..3)
.map(|index| {
let sink = Rc::clone(&log);
drag.subscribe(move |change| sink.borrow_mut().push((index, change)))
})
.collect();
drag.arm(source, PRESS);
let armed = DragStateChange::Phase {
previous: Idle,
next: Armed,
};
assert_eq!(*log.borrow(), vec![(0, armed), (1, armed), (2, armed)]);
log.borrow_mut().clear();
let mut subscriptions = subscriptions;
drop(subscriptions.remove(1));
drag.cancel();
let to_cancelled = DragStateChange::Phase {
previous: Armed,
next: Cancelled,
};
let to_idle = DragStateChange::Phase {
previous: Cancelled,
next: Idle,
};
assert_eq!(
*log.borrow(),
vec![
(0, to_cancelled),
(2, to_cancelled),
(0, to_idle),
(2, to_idle),
]
);
}
#[test]
fn a_reentrant_listener_keeps_the_global_order_for_every_subscriber() {
let drag = DragCoordinator::new();
let source = drag.new_source_id();
let target = drag.new_target_id();
let log = Rc::new(RefCell::new(Vec::new()));
let handle = drag.clone();
let sink = Rc::clone(&log);
let _first = drag.subscribe(move |change| {
sink.borrow_mut().push(("first", change));
if change
== (DragStateChange::Phase {
previous: Dragging,
next: Dropping,
})
{
assert_eq!(handle.phase(), Dropping);
handle.cancel();
assert_eq!(
handle.phase(),
Idle,
"the re-entrant cancel applied at once"
);
}
});
let sink = Rc::clone(&log);
let _second = drag.subscribe(move |change| sink.borrow_mut().push(("second", change)));
drag.arm(source, PRESS);
drag.begin(());
drag.set_hovered(Some(target));
log.borrow_mut().clear();
drag.drop();
let dropping = DragStateChange::Phase {
previous: Dragging,
next: Dropping,
};
let leave = DragStateChange::Leave { target };
let to_cancelled = DragStateChange::Phase {
previous: Dropping,
next: Cancelled,
};
let to_idle = DragStateChange::Phase {
previous: Cancelled,
next: Idle,
};
assert_eq!(
*log.borrow(),
vec![
("first", dropping),
("second", dropping),
("first", leave),
("second", leave),
("first", to_cancelled),
("second", to_cancelled),
("first", to_idle),
("second", to_idle),
],
"the second subscriber hears the drop before the cancel it triggered"
);
}
#[test]
fn a_listener_unsubscribed_mid_dispatch_hears_nothing_more() {
let drag = DragCoordinator::new();
let source = drag.new_source_id();
let heard = Rc::new(RefCell::new(0_u32));
let victim: Rc<RefCell<Option<DragSubscription>>> = Rc::new(RefCell::new(None));
let slot = Rc::clone(&victim);
let _killer = drag.subscribe(move |_| {
slot.borrow_mut().take();
});
let count = Rc::clone(&heard);
*victim.borrow_mut() = Some(drag.subscribe(move |_| *count.borrow_mut() += 1));
drag.arm(source, PRESS);
assert_eq!(*heard.borrow(), 0);
}
#[test]
fn a_subscription_outliving_its_coordinator_drops_cleanly() {
let drag = DragCoordinator::new();
let subscription = drag.subscribe(|_| {});
drop(drag);
drop(subscription);
}
#[test]
fn a_panicking_listener_does_not_wedge_dispatch() {
let drag = DragCoordinator::new();
let source = drag.new_source_id();
let heard = Rc::new(RefCell::new(Vec::new()));
let sink = Rc::clone(&heard);
let armed = Rc::new(std::cell::Cell::new(true));
let trip = Rc::clone(&armed);
let _panicky = drag.subscribe(move |_| {
if trip.replace(false) {
panic!("listener failure");
}
});
let _recorder = drag.subscribe(move |change| sink.borrow_mut().push(change));
let outcome = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
drag.arm(source, PRESS);
}));
assert!(outcome.is_err());
assert_eq!(drag.phase(), Armed, "the transition itself applied");
heard.borrow_mut().clear();
drag.cancel();
assert_eq!(
*heard.borrow(),
vec![
DragStateChange::Phase {
previous: Armed,
next: Cancelled
},
DragStateChange::Phase {
previous: Cancelled,
next: Idle
},
],
"later changes are delivered, the undelivered one is not replayed"
);
}
#[test]
fn an_external_file_session_runs_its_whole_lifecycle() {
let f = Fixture::new();
let paths = vec![PathBuf::from("/tmp/a.txt"), PathBuf::from("/tmp/b.png")];
f.drag.begin_external(paths.clone(), FILE_ENTRY);
let DragState::Dragging(session) = f.drag.state() else {
panic!("expected Dragging");
};
assert_eq!(session.kind, DragKind::ExternalFiles(paths.clone()));
assert_eq!(session.source, None, "an OS drag has no source");
assert_eq!(session.press, FILE_ENTRY);
assert_eq!(session.pointer, FILE_ENTRY);
assert!(f.drag.payload_is::<Vec<PathBuf>>());
f.drag.update_pointer(MOVED);
f.drag.set_hovered(Some(f.first));
f.drag.set_hovered(Some(f.second));
assert_eq!(f.drag.drop(), Some(f.second));
assert_eq!(
f.drag.take_payload::<Vec<PathBuf>>().map(|paths| *paths),
Some(paths)
);
f.drag.complete_drop();
assert_eq!(
f.take_log(),
vec![
DragStateChange::Phase {
previous: Idle,
next: Dragging
},
DragStateChange::Move { pointer: MOVED },
DragStateChange::Enter { target: f.first },
DragStateChange::Leave { target: f.first },
DragStateChange::Enter { target: f.second },
DragStateChange::Phase {
previous: Dragging,
next: Dropping
},
DragStateChange::Phase {
previous: Dropping,
next: Idle
},
]
);
assert_eq!(f.drag.state(), DragState::Idle);
}
#[test]
fn an_external_session_cancelled_by_the_os_leaves_its_hovered_target() {
let f = Fixture::new();
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
f.drag.set_hovered(Some(f.first));
f.take_log();
f.drag.cancel();
assert_eq!(
f.take_log(),
vec![
DragStateChange::Leave { target: f.first },
DragStateChange::Phase {
previous: Dragging,
next: Cancelled
},
DragStateChange::Phase {
previous: Cancelled,
next: Idle
},
]
);
assert!(!f.drag.payload_is::<Vec<PathBuf>>());
}
#[test]
fn unchanged_pointer_and_hover_notify_nothing() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag
.set_target_bounds(f.first, Rect::from_center_size(PRESS, (20.0, 20.0)));
Start::Hovering.reach(&f);
f.take_log();
f.drag.update_pointer(PRESS);
f.drag.set_hovered(Some(f.first));
assert!(f.take_log().is_empty());
}
#[test]
fn ids_are_unique_and_every_change_raises_a_flush() {
let f = Fixture::new();
let mut ids = vec![
f.source.get(),
f.other_source.get(),
f.first.get(),
f.second.get(),
];
ids.sort_unstable();
ids.dedup();
assert_eq!(ids.len(), 4);
frust_core::take_pending_result_flush();
f.drag.update_pointer(MOVED); assert!(!frust_core::take_pending_result_flush());
f.drag.arm(f.source, PRESS);
assert!(frust_core::take_pending_result_flush());
}
#[test]
fn clones_share_one_session() {
let f = Fixture::new();
let other = f.drag.clone();
other.arm(f.source, PRESS);
assert_eq!(f.drag.phase(), Armed);
assert_eq!(
f.take_log().len(),
1,
"a clone's change reaches the original's subscriber"
);
}
#[test]
fn register_and_unregister_targets() {
let f = Fixture::new();
assert_eq!(f.drag.registered_targets().len(), 0);
f.drag.register_target(f.first);
assert_eq!(f.drag.registered_targets().len(), 1);
assert!(f.drag.registered_targets().contains(&f.first));
f.drag.register_target(f.first);
assert_eq!(f.drag.registered_targets().len(), 1);
f.drag.register_target(f.second);
assert_eq!(f.drag.registered_targets().len(), 2);
f.drag.unregister_target(f.first);
assert_eq!(f.drag.registered_targets().len(), 1);
assert!(!f.drag.registered_targets().contains(&f.first));
assert!(f.drag.registered_targets().contains(&f.second));
}
#[test]
fn unregister_while_hovered_emits_leave_and_clears_hover() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
f.drag.set_hovered(Some(f.first));
f.take_log();
f.drag.unregister_target(f.first);
let changes = f.take_log();
assert_eq!(
changes,
vec![DragStateChange::Leave { target: f.first }],
"unregister while hovered emits Leave"
);
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
None,
"hover is cleared after unregister"
);
}
#[test]
fn unregistering_the_pending_drop_target_ends_the_session() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
f.drag.set_hovered(Some(f.first));
f.drag.drop();
f.take_log();
f.drag.unregister_target(f.first);
assert_eq!(
f.take_log(),
vec![
DragStateChange::Leave { target: f.first },
DragStateChange::Phase {
previous: DragPhase::Dropping,
next: DragPhase::Cancelled,
},
DragStateChange::Phase {
previous: DragPhase::Cancelled,
next: DragPhase::Idle,
},
],
"one Leave, then the session is cancelled"
);
assert_eq!(f.drag.state(), DragState::Idle);
assert!(
!f.drag.payload_is::<i32>(),
"the unclaimed payload is discarded"
);
f.drag.complete_drop();
f.drag.cancel();
assert!(f.take_log().is_empty());
}
#[test]
fn unregistering_another_target_while_dropping_changes_nothing() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
f.drag.set_hovered(Some(f.first));
f.drag.drop();
f.take_log();
f.drag.unregister_target(f.second);
assert!(f.take_log().is_empty());
assert_eq!(f.drag.phase(), DragPhase::Dropping);
}
#[test]
fn set_and_query_target_bounds() {
let f = Fixture::new();
f.drag.register_target(f.first);
assert_eq!(f.drag.target_bounds(f.first), None, "unset bounds are None");
let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
f.drag.set_target_bounds(f.first, rect);
assert_eq!(f.drag.target_bounds(f.first), Some(rect));
let new_rect = Rect::from_origin_size(Point::new(5.0, 10.0), (200.0, 100.0));
f.drag.set_target_bounds(f.first, new_rect);
assert_eq!(f.drag.target_bounds(f.first), Some(new_rect));
}
#[test]
fn set_target_bounds_on_unknown_id_is_ignored() {
let f = Fixture::new();
let unregistered = f.drag.new_target_id();
let rect = Rect::from_origin_size(Point::new(10.0, 20.0), (100.0, 50.0));
f.drag.set_target_bounds(unregistered, rect);
assert_eq!(f.drag.target_bounds(unregistered), None);
}
#[test]
fn target_at_finds_single_containing_target() {
let f = Fixture::new();
f.drag.register_target(f.first);
let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
f.drag.set_target_bounds(f.first, rect);
let point = Point::new(50.0, 50.0);
assert_eq!(
f.drag.target_at(point),
Some(f.first),
"point inside bounds returns the target"
);
let outside = Point::new(200.0, 200.0);
assert_eq!(
f.drag.target_at(outside),
None,
"point outside all bounds returns None"
);
}
#[test]
fn a_nested_target_registered_after_its_container_wins() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
let large = Rect::from_origin_size(Point::new(0.0, 0.0), (200.0, 200.0));
f.drag.set_target_bounds(f.first, large);
let small = Rect::from_origin_size(Point::new(50.0, 50.0), (100.0, 100.0));
f.drag.set_target_bounds(f.second, small);
assert_eq!(f.drag.target_at(Point::new(100.0, 100.0)), Some(f.second));
assert_eq!(
f.drag.target_at(Point::new(10.0, 10.0)),
Some(f.first),
"outside the nested target the container still resolves"
);
}
#[test]
fn two_overlapping_targets_resolve_to_the_later_registration() {
let f = Fixture::new();
f.drag.register_target(f.second);
f.drag.register_target(f.first);
f.drag
.set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
f.drag
.set_target_bounds(f.first, Rect::new(50.0, 50.0, 300.0, 300.0));
let overlap = Point::new(75.0, 75.0);
assert_eq!(f.drag.target_at(overlap), Some(f.first));
assert_eq!(f.drag.registered_targets(), vec![f.second, f.first]);
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
f.take_log();
f.drag.update_pointer(overlap);
assert_eq!(
f.take_log(),
vec![
DragStateChange::Enter { target: f.first },
DragStateChange::Move { pointer: overlap },
]
);
f.drag.register_target(f.second);
assert_eq!(f.drag.target_at(overlap), Some(f.first));
f.drag.unregister_target(f.second);
f.drag.register_target(f.second);
f.drag
.set_target_bounds(f.second, Rect::new(0.0, 0.0, 100.0, 100.0));
assert_eq!(f.drag.target_at(overlap), Some(f.second));
}
#[test]
fn moving_across_three_targets_enters_and_leaves_each_in_turn() {
let f = Fixture::new();
let third = f.drag.new_target_id();
let columns = [
(f.first, Rect::new(0.0, 0.0, 100.0, 300.0)),
(f.second, Rect::new(100.0, 0.0, 200.0, 300.0)),
(third, Rect::new(220.0, 0.0, 320.0, 300.0)),
];
for (id, rect) in columns {
f.drag.register_target(id);
f.drag.set_target_bounds(id, rect);
}
f.drag.arm(f.source, Point::new(50.0, 10.0));
f.drag.begin(1_u8);
f.take_log();
let path = [
Point::new(50.0, 50.0), Point::new(60.0, 60.0), Point::new(150.0, 60.0), Point::new(210.0, 60.0), Point::new(250.0, 60.0), Point::new(250.0, 400.0), ];
for point in path {
f.drag.update_pointer(point);
}
let mv = |pointer: Point| DragStateChange::Move { pointer };
assert_eq!(
f.take_log(),
vec![
DragStateChange::Enter { target: f.first },
mv(path[0]),
mv(path[1]),
DragStateChange::Leave { target: f.first },
DragStateChange::Enter { target: f.second },
mv(path[2]),
DragStateChange::Leave { target: f.second },
mv(path[3]),
DragStateChange::Enter { target: third },
mv(path[4]),
DragStateChange::Leave { target: third },
mv(path[5]),
]
);
assert_eq!(
f.drag.drop(),
None,
"released outside every target: a cancel"
);
assert_eq!(f.drag.phase(), Idle);
}
#[test]
fn an_explicit_hover_stands_until_the_next_resolution() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag
.set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
Start::Dragging.reach(&f);
f.drag.set_hovered(Some(f.second));
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second),
"the override applies at once"
);
f.take_log();
f.drag.update_pointer(Point::new(40.0, 40.0));
assert_eq!(
f.take_log(),
vec![
DragStateChange::Leave { target: f.second },
DragStateChange::Enter { target: f.first },
DragStateChange::Move {
pointer: Point::new(40.0, 40.0)
},
]
);
}
#[test]
fn resolve_hover_follows_bounds_that_move_under_a_still_pointer() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag
.set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 50.0));
f.drag
.set_target_bounds(f.second, Rect::new(0.0, 50.0, 100.0, 100.0));
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
f.drag.update_pointer(Point::new(10.0, 40.0));
f.take_log();
f.drag
.set_target_bounds(f.first, Rect::new(0.0, -20.0, 100.0, 30.0));
f.drag
.set_target_bounds(f.second, Rect::new(0.0, 30.0, 100.0, 80.0));
assert!(f.take_log().is_empty(), "reporting bounds notifies nothing");
f.drag.resolve_hover();
assert_eq!(
f.take_log(),
vec![
DragStateChange::Leave { target: f.first },
DragStateChange::Enter { target: f.second },
]
);
f.drag.resolve_hover();
assert!(f.take_log().is_empty(), "an unchanged hover is silent");
f.drag.drop();
f.take_log();
f.drag.resolve_hover();
assert!(f.take_log().is_empty());
}
#[test]
fn target_at_ignores_targets_without_bounds() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
let rect = Rect::from_origin_size(Point::new(0.0, 0.0), (100.0, 100.0));
f.drag.set_target_bounds(f.second, rect);
let point = Point::new(50.0, 50.0);
assert_eq!(
f.drag.target_at(point),
Some(f.second),
"ignores registered targets without bounds set"
);
}
#[test]
fn target_bounds_persist_across_drag_sessions() {
let f = Fixture::new();
f.drag.register_target(f.first);
let rect = Rect::from_origin_size(Point::new(10.0, 10.0), (100.0, 100.0));
f.drag.set_target_bounds(f.first, rect);
f.drag.arm(f.source, PRESS);
f.drag.begin(7_i32);
f.drag.update_pointer(MOVED);
f.drag.drop();
f.drag.complete_drop();
assert_eq!(f.drag.target_bounds(f.first), Some(rect));
}
#[test]
fn lift_begins_a_keyboard_session_with_no_pointer_resolution() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag
.set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
assert!(f.drag.lift(f.source));
assert_eq!(
f.drag.state(),
DragState::Armed {
source: f.source,
press: Point::ZERO,
keyboard: true,
}
);
assert!(f.drag.begin(9_u8));
let DragState::Dragging(session) = f.drag.state() else {
panic!("expected Dragging");
};
assert!(session.keyboard, "begin carries the keyboard flag over");
assert_eq!(session.hovered, None);
f.take_log();
f.drag.update_pointer(Point::new(50.0, 50.0));
assert!(f.take_log().is_empty());
assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
f.drag.resolve_hover();
assert!(f.take_log().is_empty());
}
#[test]
fn move_to_next_and_previous_target_cycle_in_registration_order_and_wrap() {
let f = Fixture::new();
let third = f.drag.new_target_id();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.register_target(third);
for id in [f.first, f.second, third] {
f.drag
.set_target_bounds(id, Rect::new(0.0, 0.0, 10.0, 10.0));
}
f.drag.lift(f.source);
f.drag.begin(1_u8);
f.take_log();
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first),
"starts at the first registered target"
);
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(third)
);
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first),
"wraps back to the first"
);
assert_eq!(
f.take_log(),
vec![
DragStateChange::Enter { target: f.first },
DragStateChange::Leave { target: f.first },
DragStateChange::Enter { target: f.second },
DragStateChange::Leave { target: f.second },
DragStateChange::Enter { target: third },
DragStateChange::Leave { target: third },
DragStateChange::Enter { target: f.first },
]
);
f.drag.move_to_previous_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(third),
"wraps back to the last going the other way"
);
}
#[test]
fn move_to_previous_target_starts_at_the_last_when_nothing_is_hovered() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag
.set_target_bounds(f.first, Rect::new(0.0, 0.0, 10.0, 10.0));
f.drag
.set_target_bounds(f.second, Rect::new(0.0, 0.0, 10.0, 10.0));
f.drag.lift(f.source);
f.drag.begin(1_u8);
f.drag.move_to_previous_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
}
#[test]
fn move_target_is_a_no_op_outside_dragging_or_with_no_targets() {
let f = Fixture::new();
f.drag.move_to_next_target();
assert_eq!(f.drag.phase(), DragPhase::Idle);
f.drag.lift(f.source);
f.take_log();
f.drag.move_to_next_target();
assert!(
f.take_log().is_empty(),
"Armed is not Dragging yet — still a no-op"
);
f.drag.begin(1_u8);
f.take_log();
f.drag.move_to_next_target();
assert!(f.take_log().is_empty(), "no registered targets to cycle");
}
#[test]
fn lift_while_dragging_is_ignored() {
let f = Fixture::new();
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
assert!(!f.drag.lift(f.other_source));
assert_eq!(
f.drag.state().session().and_then(|s| s.source),
Some(f.source)
);
}
fn frame(n: u64) -> FrameTime {
FrameTime::from_nanos(n * 16_000_000)
}
#[test]
fn resolution_skips_a_later_registered_target_of_another_payload_type() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
f.drag.register_target_for::<u8>(f.first);
f.drag.register_target_for::<u32>(f.second);
f.drag.set_target_bounds(f.first, rect);
f.drag.set_target_bounds(f.second, rect);
let inside = Point::new(50.0, 50.0);
assert_eq!(
f.drag.target_at(inside),
Some(f.second),
"with no session, the later registration wins as before"
);
f.drag.arm(f.source, PRESS);
f.drag.begin(3_u8);
assert_eq!(f.drag.target_at(inside), Some(f.first));
f.take_log();
f.drag.update_pointer(inside);
assert_eq!(
f.take_log(),
vec![
DragStateChange::Enter { target: f.first },
DragStateChange::Move { pointer: inside },
],
"the u8 session resolves onto the u8 target under the u32 one"
);
f.drag.resolve_hover();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first)
);
}
#[test]
fn resolution_finds_nothing_when_only_other_types_are_under_the_pointer() {
let f = Fixture::new();
f.drag.register_target_for::<u32>(f.first);
f.drag
.set_target_bounds(f.first, Rect::new(0.0, 0.0, 100.0, 100.0));
f.drag.arm(f.source, PRESS);
f.drag.begin("text");
f.drag.update_pointer(Point::new(50.0, 50.0));
assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
assert_eq!(f.drag.drop(), None, "released over nothing: a cancel");
assert_eq!(f.drag.phase(), DragPhase::Idle);
}
#[test]
fn an_external_session_resolves_only_path_list_targets() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
f.drag.register_target_for::<Vec<PathBuf>>(f.first);
f.drag.register_target_for::<u32>(f.second);
f.drag.set_target_bounds(f.first, rect);
f.drag.set_target_bounds(f.second, rect);
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
f.drag.update_pointer(Point::new(50.0, 50.0));
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first)
);
}
#[test]
fn keyboard_cycling_skips_targets_of_another_payload_type() {
let f = Fixture::new();
let third = f.drag.new_target_id();
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
f.drag.register_target_for::<u32>(f.first);
f.drag.register_target_for::<u8>(f.second);
f.drag.register_target(third);
for id in [f.first, f.second, third] {
f.drag.set_target_bounds(id, rect);
}
f.drag.lift(f.source);
f.drag.begin(1_u8);
let hovered = || f.drag.state().session().and_then(|s| s.hovered);
f.drag.move_to_next_target();
assert_eq!(hovered(), Some(f.second), "the u32 target is skipped");
f.drag.move_to_next_target();
assert_eq!(hovered(), Some(third), "an untyped target accepts any");
f.drag.move_to_next_target();
assert_eq!(hovered(), Some(f.second), "wraps past the u32 target again");
f.drag.move_to_previous_target();
assert_eq!(hovered(), Some(third));
}
#[test]
fn a_target_clipped_to_nothing_is_not_resolved_or_cycled() {
let f = Fixture::new();
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag
.report_target_bounds(f.first, Some(Rect::new(0.0, 0.0, 100.0, 100.0)), frame(1));
f.drag.report_target_bounds(f.second, None, frame(1));
assert_eq!(f.drag.target_bounds(f.second), None);
assert_eq!(f.drag.target_at(Point::new(50.0, 50.0)), Some(f.first));
f.drag.lift(f.source);
f.drag.begin(1_u8);
f.drag.move_to_next_target();
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first),
"the clipped target is never cycled to"
);
}
#[test]
fn a_target_that_stopped_reporting_is_not_resolved() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let inside = Point::new(50.0, 50.0);
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.report_target_bounds(f.first, Some(rect), frame(1));
f.drag.report_target_bounds(
f.second,
Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
frame(1),
);
assert_eq!(f.drag.target_at(inside), Some(f.first));
f.drag.report_target_bounds(
f.second,
Some(Rect::new(200.0, 0.0, 300.0, 100.0)),
frame(2),
);
assert_eq!(f.drag.target_bounds(f.first), Some(rect));
assert_eq!(f.drag.target_at(inside), None);
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
f.drag.update_pointer(inside);
assert_eq!(f.drag.state().session().and_then(|s| s.hovered), None);
f.drag.report_target_bounds(f.first, Some(rect), frame(3));
f.drag.resolve_hover();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.first)
);
}
#[test]
fn resolve_hover_tolerates_a_target_one_pass_behind_but_not_two() {
let f = Fixture::new();
let hovered_rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let elsewhere = Rect::new(200.0, 0.0, 300.0, 100.0);
let point = Point::new(50.0, 50.0);
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag
.report_target_bounds(f.first, Some(elsewhere), frame(1));
f.drag
.report_target_bounds(f.second, Some(hovered_rect), frame(1));
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
f.drag.update_pointer(point);
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
f.take_log();
f.drag
.report_target_bounds(f.first, Some(elsewhere), frame(2));
f.drag.resolve_hover();
assert!(
f.take_log().is_empty(),
"a target one pass behind is tolerated, not treated as culled"
);
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
f.drag
.report_target_bounds(f.first, Some(elsewhere), frame(3));
f.drag.resolve_hover();
assert_eq!(
f.take_log(),
vec![DragStateChange::Leave { target: f.second }],
"two full passes without repainting is culled, not tolerated"
);
}
#[test]
fn a_stale_target_is_not_cycled_to() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.report_target_bounds(f.first, Some(rect), frame(1));
f.drag.report_target_bounds(f.second, Some(rect), frame(1));
f.drag.report_target_bounds(f.second, Some(rect), frame(2));
f.drag.lift(f.source);
f.drag.begin(1_u8);
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
f.drag.move_to_next_target();
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second),
"only the target painted in the latest pass is cycled"
);
}
#[test]
fn painting_twice_in_one_frame_keeps_the_pass_open() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 10.0, 10.0);
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.report_target_bounds(f.first, Some(rect), frame(1));
f.drag.report_target_bounds(f.second, Some(rect), frame(1));
f.drag.report_target_bounds(f.second, Some(rect), frame(1));
assert_eq!(
f.drag.target_at(Point::new(5.0, 5.0)),
Some(f.second),
"same frame time, same pass: both still resolve"
);
f.drag.unregister_target(f.second);
assert_eq!(f.drag.target_at(Point::new(5.0, 5.0)), Some(f.first));
}
#[test]
fn an_unclocked_repeat_report_opens_the_next_pass() {
let f = Fixture::new();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let inside = Point::new(50.0, 50.0);
f.drag.register_target(f.first);
f.drag.register_target(f.second);
f.drag.set_target_bounds(f.first, rect);
f.drag
.set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
assert_eq!(f.drag.target_at(inside), Some(f.first));
f.drag
.set_target_bounds(f.second, Rect::new(200.0, 0.0, 300.0, 100.0));
assert_eq!(f.drag.target_at(inside), None);
f.drag
.report_target_bounds(f.first, Some(rect), FrameTime::ZERO);
assert_eq!(f.drag.target_at(inside), Some(f.first));
}
#[test]
fn later_registration_still_wins_among_visible_type_matching_overlaps() {
let f = Fixture::new();
let third = f.drag.new_target_id();
let fourth = f.drag.new_target_id();
let rect = Rect::new(0.0, 0.0, 100.0, 100.0);
let inside = Point::new(50.0, 50.0);
f.drag.register_target_for::<u8>(f.first);
f.drag.register_target_for::<u8>(f.second);
f.drag.register_target_for::<u8>(third);
f.drag.register_target_for::<u32>(fourth);
f.drag.report_target_bounds(third, Some(rect), frame(1));
for id in [f.first, f.second, fourth] {
f.drag.report_target_bounds(id, Some(rect), frame(2));
}
f.drag.arm(f.source, PRESS);
f.drag.begin(1_u8);
f.drag.update_pointer(inside);
assert_eq!(
f.drag.state().session().and_then(|s| s.hovered),
Some(f.second)
);
}
#[test]
fn is_external_reports_the_kind_of_the_session_in_flight() {
let f = Fixture::new();
assert!(!f.drag.is_external());
f.drag.arm(f.source, PRESS);
assert!(!f.drag.is_external());
f.drag.begin(1_u8);
assert!(!f.drag.is_external());
f.drag.cancel();
f.drag.register_target(f.first);
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
assert!(f.drag.is_external());
f.drag.set_hovered(Some(f.first));
f.drag.drop();
assert!(f.drag.is_external(), "still external while Dropping");
f.drag.complete_drop();
assert!(!f.drag.is_external());
}
#[test]
fn end_external_ends_a_hovering_external_session_once() {
let f = Fixture::new();
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
f.drag.set_hovered(Some(f.first));
f.take_log();
assert!(f.drag.end_external(), "the first call ends it");
assert_eq!(
f.take_log(),
vec![
DragStateChange::Leave { target: f.first },
DragStateChange::Phase {
previous: Dragging,
next: Cancelled
},
DragStateChange::Phase {
previous: Cancelled,
next: Idle
},
]
);
assert_eq!(f.drag.state(), DragState::Idle);
assert!(!f.drag.end_external(), "every later call is a no-op");
assert!(f.take_log().is_empty());
}
#[test]
fn end_external_leaves_internal_armed_and_dropping_sessions_alone() {
let f = Fixture::new();
assert!(!f.drag.end_external(), "nothing in flight");
f.drag.arm(f.source, PRESS);
assert!(!f.drag.end_external());
assert_eq!(f.drag.phase(), DragPhase::Armed);
f.drag.begin(1_u8);
assert!(!f.drag.end_external());
assert_eq!(f.drag.phase(), DragPhase::Dragging);
f.drag.cancel();
assert!(f.drag.lift(f.source), "a keyboard session");
f.drag.begin(2_u8);
assert!(!f.drag.end_external());
assert!(
f.drag.payload_is::<u8>(),
"the keyboard session is untouched"
);
f.drag.cancel();
f.drag
.begin_external(vec![PathBuf::from("/tmp/a.txt")], FILE_ENTRY);
f.drag.set_hovered(Some(f.first));
assert_eq!(f.drag.drop(), Some(f.first));
f.take_log();
assert!(
!f.drag.end_external(),
"a released drop is its target's to claim"
);
assert!(f.take_log().is_empty());
assert_eq!(f.drag.phase(), DragPhase::Dropping);
}
}