use super::*;
struct OutboundStash {
live: bool,
payload: Option<crate::drag_payload::DragPayload>,
pointer: Option<crate::pointer::PointerInfo>,
}
thread_local! {
static OUTBOUND: std::cell::RefCell<OutboundStash> = const {
std::cell::RefCell::new(OutboundStash {
live: false,
payload: None,
pointer: None,
})
};
}
fn outbound_begin(payload: crate::drag_payload::DragPayload, pointer: crate::pointer::PointerInfo) {
OUTBOUND.with(|s| {
let mut s = s.borrow_mut();
s.live = true;
s.payload = Some(payload);
s.pointer = Some(pointer);
});
}
fn outbound_take_if_live() -> Option<crate::drag_payload::DragPayload> {
OUTBOUND.with(|s| {
let mut s = s.borrow_mut();
if s.live { s.payload.take() } else { None }
})
}
fn outbound_pointer_if_live() -> Option<crate::pointer::PointerInfo> {
OUTBOUND.with(|s| {
let s = s.borrow();
if s.live { s.pointer } else { None }
})
}
fn outbound_is_live() -> bool {
OUTBOUND.with(|s| s.borrow().live)
}
fn outbound_restash(payload: crate::drag_payload::DragPayload) {
OUTBOUND.with(|s| {
let mut s = s.borrow_mut();
if s.live {
s.payload = Some(payload);
}
});
}
#[cfg(test)]
fn has_outbound_typed() -> bool {
OUTBOUND.with(|s| s.borrow().payload.is_some())
}
fn outbound_end() {
OUTBOUND.with(|s| {
let mut s = s.borrow_mut();
s.live = false;
s.payload = None;
s.pointer = None;
});
}
fn unknown_external_pointer() -> crate::pointer::PointerInfo {
let mut info = crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO);
info.kind = teksilo_tokens::PointerKind::Unknown;
info
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
enum DragDrive {
Move,
Drop,
}
impl WidgetTree {
fn drive_drag_session(
&mut self,
position: teksilo_canvas::Point,
which: DragDrive,
ops: &mut dyn crate::window::WindowOps,
) -> Option<crate::drag_payload::DropOutcome> {
let pointer = self.active_drag.as_ref().map(|d| d.pointer)?;
let saved = std::mem::replace(
&mut self.current_input,
crate::pointer::InputSnapshot::for_drag_session(pointer),
);
let outcome = match which {
DragDrive::Move => {
self.handle_drag_move(position, ops);
None
}
DragDrive::Drop => self.handle_drag_drop(position, ops),
};
self.current_input = saved;
outcome
}
pub(super) fn cleanup_drag_preview(&mut self) {
let Some((overlay_id, content_id)) = self
.active_drag
.as_ref()
.map(|drag| (drag.preview_overlay_id, drag.preview_content_id))
else {
return;
};
if let Some(overlay_id) = overlay_id {
self.overlay_manager.dismiss(overlay_id);
}
if let Some(content_id) = content_id {
self.destroy_subtree(content_id);
}
}
pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
if self.os_drag_pending.take().is_some() {
ops.cancel_os_drag();
}
let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
let saved = self.active_drag.as_ref().map(|d| d.pointer).map(|p| {
std::mem::replace(
&mut self.current_input,
crate::pointer::InputSnapshot::for_drag_session(p),
)
});
self.cleanup_drag_preview();
self.active_drag = None;
self.os_drop_accepted = None;
self.release_drag_capture(source);
self.current_cursor = crate::widget::CursorIcon::Default;
if let Some(prev) = prev_target {
self.fire_on_drag_leave(prev, &mut *ops);
}
if let Some(src) = source {
self.fire_on_drag_ended(src, crate::drag_payload::DropOutcome::Cancelled, &mut *ops);
}
if let Some(saved) = saved {
self.current_input = saved;
}
}
pub fn begin_external_drag(
&mut self,
position: teksilo_canvas::Point,
data: crate::drag_payload::ExternalDropData,
ops: &mut dyn crate::window::WindowOps,
) {
if let Some(pending) = self.os_drag_pending {
self.confirm_os_drag(pending, &mut *ops);
}
if self.active_drag.is_some() {
self.cancel_active_drag(&mut *ops);
}
if let Some(mut payload) = outbound_take_if_live() {
payload.enrich_external_from_mime();
let pointer = outbound_pointer_if_live().unwrap_or_else(unknown_external_pointer);
self.active_drag = Some(crate::drag_state::DragSession {
payload,
pointer,
source_widget: None,
is_external: false,
current_position: position,
current_target: None,
feedback: crate::drag_state::DropFeedback::NoFeedback,
preview_content_id: None,
preview_overlay_id: None,
});
self.os_drag_reentered = true;
self.drive_drag_session(position, DragDrive::Move, &mut *ops);
return;
}
self.active_drag = Some(crate::drag_state::DragSession {
payload: crate::drag_payload::DragPayload::external(data),
pointer: unknown_external_pointer(),
source_widget: None,
is_external: true,
current_position: position,
current_target: None,
feedback: crate::drag_state::DropFeedback::NoFeedback,
preview_content_id: None,
preview_overlay_id: None,
});
self.drive_drag_session(position, DragDrive::Move, &mut *ops);
}
pub fn update_external_drag(
&mut self,
position: teksilo_canvas::Point,
ops: &mut dyn crate::window::WindowOps,
) {
if self.active_drag.as_ref().is_some_and(|d| d.is_external) || self.os_drag_reentered {
self.drive_drag_session(position, DragDrive::Move, &mut *ops);
}
}
pub fn end_external_drag(
&mut self,
position: teksilo_canvas::Point,
data: crate::drag_payload::ExternalDropData,
ops: &mut dyn crate::window::WindowOps,
) {
if self.os_drag_reentered {
self.os_drag_reentered = false;
let outcome = self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
if self.outbound_drag_source.is_some()
&& let Some(in_app @ crate::drag_payload::DropOutcome::InApp { .. }) = outcome
{
self.reentered_drop_outcome = Some(in_app);
}
outbound_end();
return;
}
if !self.active_drag.as_ref().is_some_and(|d| d.is_external) {
return;
}
if !data.is_empty()
&& let Some(drag) = self.active_drag.as_mut()
{
drag.payload = crate::drag_payload::DragPayload::external(data);
}
self.drive_drag_session(position, DragDrive::Drop, &mut *ops);
}
pub fn abort_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
if self.active_drag.is_none() {
self.os_drag_reentered = false;
return;
}
self.os_drag_reentered = false;
self.cancel_active_drag(&mut *ops);
}
pub fn cancel_external_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
if self.os_drag_reentered {
self.reexit_outbound(&mut *ops);
} else if self.active_drag.as_ref().is_some_and(|d| d.is_external) {
self.cancel_active_drag(&mut *ops);
}
}
pub(super) fn process_drag_tick(&mut self, ops: &mut dyn crate::window::WindowOps) {
self.reap_dead_reentered_drag(&mut *ops);
let Some((target_id, position, pointer)) = self
.active_drag
.as_ref()
.and_then(|d| d.current_target.map(|t| (t, d.current_position, d.pointer)))
else {
return;
};
if !self.arena.is_active(target_id) {
return;
}
let bounds = self.arena.bounds(target_id);
let local = teksilo_canvas::Point::new(position.x - bounds.x, position.y - bounds.y);
let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
Some(node) => (
node.external_handlers.on_drag_tick.take(),
node.handlers.on_drag_tick.take(),
),
None => return,
};
if ext_handler.is_none() && own_handler.is_none() {
return;
}
let saved = std::mem::replace(
&mut self.current_input,
crate::pointer::InputSnapshot::for_drag_session(pointer),
);
let mut ctx = self.make_event_context(&mut *ops);
if let Some(h) = ext_handler.as_mut() {
h(local, &mut ctx);
}
if let Some(h) = own_handler.as_mut() {
h(local, &mut ctx);
}
if let Some(node) = self.arena.get_mut(target_id) {
node.external_handlers.on_drag_tick = ext_handler;
node.handlers.on_drag_tick = own_handler;
}
self.collect_from_ctx(ctx, target_id);
if self.active_drag.is_some() {
self.handle_drag_move(position, &mut *ops);
}
self.current_input = saved;
}
fn reap_dead_reentered_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
if !self.os_drag_reentered || outbound_is_live() {
return;
}
crate::trace_input!(
Gestures,
"the OS drag this window held as re-entered has ended elsewhere: dropping the session"
);
self.abort_external_drag(&mut *ops);
}
pub(super) fn fire_on_drag_leave(
&mut self,
target_id: WidgetId,
ops: &mut dyn crate::window::WindowOps,
) {
if !self.arena.is_active(target_id) {
return;
}
let (mut ext_handler, mut own_handler) = match self.arena.get_mut(target_id) {
Some(node) => (
node.external_handlers.on_drag_leave.take(),
node.handlers.on_drag_leave.take(),
),
None => return,
};
if ext_handler.is_none() && own_handler.is_none() {
self.arena.mark_needs_paint(target_id);
return;
}
let mut ctx = self.make_event_context(&mut *ops);
if let Some(h) = ext_handler.as_mut() {
h(&mut ctx);
}
if let Some(h) = own_handler.as_mut() {
h(&mut ctx);
}
if let Some(node) = self.arena.get_mut(target_id) {
node.external_handlers.on_drag_leave = ext_handler;
node.handlers.on_drag_leave = own_handler;
}
self.collect_from_ctx(ctx, target_id);
self.arena.mark_needs_paint(target_id);
}
pub(super) fn fire_on_drag_ended(
&mut self,
source_id: WidgetId,
outcome: crate::drag_payload::DropOutcome,
ops: &mut dyn crate::window::WindowOps,
) {
if !self.arena.is_active(source_id) {
return;
}
let (mut ext_handler, mut own_handler) = match self.arena.get_mut(source_id) {
Some(node) => (
node.external_handlers.on_drag_ended.take(),
node.handlers.on_drag_ended.take(),
),
None => return,
};
if ext_handler.is_none() && own_handler.is_none() {
return;
}
let mut ctx = self.make_event_context(&mut *ops);
if let Some(h) = ext_handler.as_mut() {
h(outcome, &mut ctx);
}
if let Some(h) = own_handler.as_mut() {
h(outcome, &mut ctx);
}
if let Some(node) = self.arena.get_mut(source_id) {
node.external_handlers.on_drag_ended = ext_handler;
node.handlers.on_drag_ended = own_handler;
}
self.collect_from_ctx(ctx, source_id);
}
fn window_content_size(&self) -> Option<(f32, f32)> {
Some((self.last_proposal.width?, self.last_proposal.height?))
}
fn is_outside_window(&self, position: teksilo_canvas::Point) -> bool {
match self.window_content_size() {
Some((w, h)) => {
position.x < 0.0 || position.y < 0.0 || position.x > w || position.y > h
}
None => false,
}
}
fn try_escalate_to_os_drag(
&mut self,
position: teksilo_canvas::Point,
sample: bool,
ops: &mut dyn crate::window::WindowOps,
) -> bool {
if self.os_drag_reentered {
if self.is_outside_window(position) {
self.reexit_outbound(&mut *ops);
return true;
}
return false;
}
if self.os_drag_pending.is_some() {
return false;
}
let (data, handoff, dragging) = match self.active_drag.as_ref() {
Some(d)
if !d.is_external && d.source_widget.is_some() && d.payload.is_os_exportable() =>
{
(d.payload.to_outbound(), d.payload.os_handoff(), d.pointer)
}
_ => return false,
};
let eager = handoff == crate::drag_payload::OsHandoff::Eager && sample;
if !eager && !self.is_outside_window(position) {
return false;
}
match ops.begin_os_drag(data, None, dragging.kind) {
crate::drag_payload::OsDragStart::Declined => false,
crate::drag_payload::OsDragStart::Pending(drag) => {
self.os_drag_pending = Some(drag);
false
}
crate::drag_payload::OsDragStart::Started => {
self.hand_session_to_os(None, &mut *ops);
true
}
}
}
pub fn confirm_os_drag(
&mut self,
drag: crate::drag_payload::OsDragId,
ops: &mut dyn crate::window::WindowOps,
) {
if self.os_drag_pending == Some(drag) {
self.os_drag_pending = None;
if self.active_drag.is_none() {
ops.cancel_os_drag();
return;
}
self.hand_session_to_os(Some(drag), &mut *ops);
return;
}
if self.os_drag_pending.is_none()
&& self.outbound_drag_source.is_none()
&& !self.os_drag_reentered
{
ops.cancel_os_drag();
}
}
fn hand_session_to_os(
&mut self,
drag: Option<crate::drag_payload::OsDragId>,
ops: &mut dyn crate::window::WindowOps,
) {
let Some(dragging) = self.active_drag.as_ref().map(|d| d.pointer) else {
return;
};
self.os_drag_pending = None;
self.outbound_drag_id = drag;
self.reentered_drop_outcome = None;
let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
self.cleanup_drag_preview();
let drag = self
.active_drag
.take()
.expect("active_drag present (checked above)");
self.os_drop_accepted = None;
self.release_drag_capture(drag.source_widget);
self.current_cursor = crate::widget::CursorIcon::Default;
self.outbound_drag_source = drag.source_widget;
outbound_begin(drag.payload, dragging);
if let Some(prev) = prev_target {
self.fire_on_drag_leave(prev, &mut *ops);
}
self.cancel_pointer_to(
dragging.id,
crate::pointer::CancelReason::OsDragStarted,
self.outbound_drag_source,
&mut *ops,
);
}
fn reexit_outbound(&mut self, ops: &mut dyn crate::window::WindowOps) {
let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
self.cleanup_drag_preview();
let source = self.active_drag.as_ref().and_then(|d| d.source_widget);
if let Some(drag) = self.active_drag.take() {
outbound_restash(drag.payload);
}
self.os_drag_reentered = false;
self.reentered_drop_outcome = None;
self.os_drop_accepted = None;
self.release_drag_capture(source);
self.current_cursor = crate::widget::CursorIcon::Default;
if let Some(prev) = prev_target {
self.fire_on_drag_leave(prev, &mut *ops);
}
}
pub fn handle_os_drag_ended(
&mut self,
outcome: crate::drag_payload::DropOutcome,
ops: &mut dyn crate::window::WindowOps,
) {
self.handle_os_drag_ended_for(None, outcome, ops);
}
pub fn handle_os_drag_ended_for(
&mut self,
drag: Option<crate::drag_payload::OsDragId>,
outcome: crate::drag_payload::DropOutcome,
ops: &mut dyn crate::window::WindowOps,
) {
let waiting = match drag {
Some(id) if self.os_drag_pending == Some(id) => true,
Some(id) if self.outbound_drag_id == Some(id) => false,
Some(id) => {
crate::trace_input!(Gestures, "a stale end of OS drag {} is ignored", id.raw());
return;
}
None => self.os_drag_pending.is_some(),
};
if waiting {
self.os_drag_pending = None;
self.reentered_drop_outcome = None;
self.cancel_active_drag(&mut *ops);
return;
}
outbound_end();
self.os_drag_reentered = false;
self.outbound_drag_id = None;
let outcome = self.reentered_drop_outcome.take().unwrap_or(outcome);
if let Some(source) = self.outbound_drag_source.take() {
self.fire_on_drag_ended(source, outcome, &mut *ops);
}
}
pub fn abort_outbound_drag(&mut self) {
if self.outbound_drag_source.take().is_some() {
outbound_end();
} else if self.os_drag_reentered
&& let Some(drag) = self.active_drag.take()
{
outbound_restash(drag.payload);
}
self.os_drag_reentered = false;
self.os_drag_pending = None;
self.outbound_drag_id = None;
self.reentered_drop_outcome = None;
self.os_drop_accepted = None;
}
pub(super) fn handle_drag_move(
&mut self,
position: teksilo_canvas::Point,
ops: &mut dyn crate::window::WindowOps,
) {
self.drag_move(position, false, ops);
}
pub(super) fn handle_drag_sample(
&mut self,
position: teksilo_canvas::Point,
ops: &mut dyn crate::window::WindowOps,
) {
self.drag_move(position, true, ops);
}
fn drag_move(
&mut self,
position: teksilo_canvas::Point,
sample: bool,
ops: &mut dyn crate::window::WindowOps,
) {
if let Some(ref mut drag) = self.active_drag {
drag.current_position = position;
}
if self.try_escalate_to_os_drag(position, sample, &mut *ops) {
return;
}
let preview_content = self
.active_drag
.as_ref()
.and_then(|d| Some((d.preview_overlay_id?, d.preview_content_id?)));
if let Some((overlay_id, content_id)) = preview_content {
let dragging = self.active_drag.as_ref().map(|d| d.pointer);
let placement = match dragging {
Some(pointer) => {
crate::overlay::OverlayPlacement::at_pointer_for(position, &pointer)
}
None => crate::overlay::OverlayPlacement::AtPointer(position),
};
self.overlay_manager.update_placement(overlay_id, placement);
self.arena.mark_needs_layout(content_id);
}
let exclude_overlay = self.active_drag.as_ref().and_then(|d| d.preview_overlay_id);
let exclude_widget = self.active_drag.as_ref().and_then(|d| d.preview_content_id);
let dragging = self.current_input.pointer;
let target =
self.hit_test_for_excluding(position, &dragging, exclude_overlay, exclude_widget);
let mut candidate = target.and_then(|t| self.find_drop_target_at_or_above(t));
let mut engaged: Option<WidgetId> = None;
let mut engaged_feedback = crate::drag_state::DropFeedback::NoFeedback;
let mut bubbled_past: Vec<WidgetId> = Vec::new();
while let Some(cand) = candidate {
let fb = self.fire_on_drag_hover(cand, position, &mut *ops);
if fb.is_engaged() {
engaged = Some(cand);
engaged_feedback = fb;
break;
}
bubbled_past.push(cand);
candidate = self.next_drop_target_above(cand);
}
let (new_target, new_feedback) = if engaged.is_some() {
for cand in &bubbled_past {
self.fire_on_drag_leave(*cand, &mut *ops);
}
(engaged, engaged_feedback)
} else if let Some((&deepest, rest)) = bubbled_past.split_first() {
for cand in rest {
self.fire_on_drag_leave(*cand, &mut *ops);
}
(Some(deepest), crate::drag_state::DropFeedback::NoFeedback)
} else {
(None, crate::drag_state::DropFeedback::NoFeedback)
};
let prev_target = self.active_drag.as_ref().and_then(|d| d.current_target);
if prev_target != new_target
&& let Some(prev) = prev_target
&& !bubbled_past.contains(&prev)
{
self.fire_on_drag_leave(prev, &mut *ops);
}
let engaged_now = new_feedback.is_engaged();
let is_os_drag =
self.os_drag_reentered || self.active_drag.as_ref().is_some_and(|d| d.is_external);
if let Some(ref mut drag) = self.active_drag {
drag.current_target = new_target;
drag.feedback = new_feedback;
}
if is_os_drag && self.os_drop_accepted != Some(engaged_now) {
self.os_drop_accepted = Some(engaged_now);
ops.set_drop_accepted(engaged_now);
}
}
fn fire_on_drag_hover(
&mut self,
target_id: WidgetId,
position: teksilo_canvas::Point,
ops: &mut dyn crate::window::WindowOps,
) -> crate::drag_state::DropFeedback {
use crate::drag_state::DropFeedback;
let target_bounds = self.arena.bounds(target_id);
let local =
teksilo_canvas::Point::new(position.x - target_bounds.x, position.y - target_bounds.y);
let (mut ext_handler, mut own_handler, has_on_drop) = match self.arena.get_mut(target_id) {
Some(node) => {
let has_on_drop = node.any_handler(|h| h.on_drop.is_some());
let ext = node.external_handlers.on_drag_hover.take();
let own = node.handlers.on_drag_hover.take();
(ext, own, has_on_drop)
}
None => return DropFeedback::NoFeedback,
};
if ext_handler.is_none() && own_handler.is_none() {
return if has_on_drop {
DropFeedback::Accept
} else {
DropFeedback::NoFeedback
};
}
let mut feedback = DropFeedback::NoFeedback;
if self.active_drag.is_some() {
let mut ctx = self.make_event_context(&mut *ops);
if let Some(ref drag) = self.active_drag {
if let Some(h) = ext_handler.as_mut() {
feedback = h(&drag.payload, local, &mut ctx);
}
if let Some(h) = own_handler.as_mut() {
feedback = h(&drag.payload, local, &mut ctx);
}
}
if let Some(node) = self.arena.get_mut(target_id) {
node.external_handlers.on_drag_hover = ext_handler;
node.handlers.on_drag_hover = own_handler;
}
self.collect_from_ctx(ctx, target_id);
self.arena.mark_needs_paint(target_id);
} else if let Some(node) = self.arena.get_mut(target_id) {
node.external_handlers.on_drag_hover = ext_handler;
node.handlers.on_drag_hover = own_handler;
}
feedback
}
fn next_drop_target_above(&self, id: WidgetId) -> Option<WidgetId> {
let parent = self.arena.parent(id)?;
self.find_drop_target_at_or_above(parent)
}
pub(super) fn handle_drag_drop(
&mut self,
position: teksilo_canvas::Point,
ops: &mut dyn crate::window::WindowOps,
) -> Option<crate::drag_payload::DropOutcome> {
if self.os_drag_pending.is_some() && self.active_drag.is_some() {
self.cancel_active_drag(&mut *ops);
return Some(crate::drag_payload::DropOutcome::Cancelled);
}
self.cleanup_drag_preview();
self.active_drag.as_ref()?;
let mut drop_target = self
.active_drag
.as_ref()
.and_then(|d| d.current_target)
.filter(|&t| self.arena.is_active(t));
if drop_target.is_none() {
let dropping = self.current_input.pointer;
let hit = self.hit_test_for(position, &dropping);
let mut candidate = hit.and_then(|t| self.find_drop_target_at_or_above(t));
while let Some(cand) = candidate {
if self
.fire_on_drag_hover(cand, position, &mut *ops)
.is_engaged()
{
drop_target = Some(cand);
break;
}
self.fire_on_drag_leave(cand, &mut *ops);
candidate = self.next_drop_target_above(cand);
}
}
let drag = self.active_drag.take()?;
self.os_drop_accepted = None;
self.release_drag_capture(drag.source_widget);
self.current_cursor = crate::widget::CursorIcon::Default;
let source = drag.source_widget;
let mut outcome = crate::drag_payload::DropOutcome::Cancelled;
if let Some(prev) = drop_target {
self.fire_on_drag_leave(prev, &mut *ops);
}
if let Some(target_id) = drop_target {
let target_bounds = self.arena.bounds(target_id);
let local = teksilo_canvas::Point::new(
position.x - target_bounds.x,
position.y - target_bounds.y,
);
let (taken_own, taken_ext) = match self.arena.get_mut(target_id) {
Some(node) => {
let own = node.handlers.on_drop.take();
let ext = if own.is_none() {
node.external_handlers.on_drop.take()
} else {
None
};
(own, ext)
}
None => (None, None),
};
let picked = if let Some(h) = taken_own {
Some((h, true))
} else {
taken_ext.map(|h| (h, false))
};
if let Some((mut handler, is_own)) = picked {
let mut ctx = self.make_event_context(&mut *ops);
let accepted = handler(drag.payload, local, &mut ctx);
outcome = crate::drag_payload::DropOutcome::InApp { accepted };
if let Some(node) = self.arena.get_mut(target_id) {
if is_own {
node.handlers.on_drop = Some(handler);
} else {
node.external_handlers.on_drop = Some(handler);
}
}
self.collect_from_ctx(ctx, target_id);
self.arena.mark_needs_paint(target_id);
}
}
if let Some(src) = source {
self.fire_on_drag_ended(src, outcome, &mut *ops);
}
Some(outcome)
}
fn find_drop_target_at_or_above(&self, start: WidgetId) -> Option<WidgetId> {
let mut current = Some(start);
while let Some(id) = current {
if let Some(node) = self.arena.get(id)
&& node.any_handler(|h| h.on_drop.is_some() || h.on_drag_hover.is_some())
{
return Some(id);
}
current = self.arena.parent(id);
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_widgets::{FillWidget, StackWidget};
use crate::widget::CursorIcon;
use crate::widget_builder::WidgetBuilder;
#[test]
fn start_drag_creates_session() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new().on_tap({
move |_pos, ctx: &mut crate::widget::EventContext| {
ctx.start_drag(
ctx.focus_requests.first().copied().unwrap_or_default(),
crate::drag_payload::DragPayload::typed(42_u32),
);
}
}));
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
let drag = tree.active_drag.as_ref().unwrap();
assert_eq!(drag.source_widget, Some(source));
assert!(!drag.is_external);
assert!(drag.payload.has_typed::<u32>());
}
#[test]
fn drag_move_updates_position() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed("hello"));
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(50.0, 30.0)));
let drag = tree.active_drag.as_ref().unwrap();
assert!((drag.current_position.x - 50.0).abs() < 0.01);
assert!((drag.current_position.y - 30.0).abs() < 0.01);
}
#[test]
fn escape_cancels_drag() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(99_i32));
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
tree.press_key(Key::Escape, Modifiers::NONE);
assert!(tree.active_drag.is_none(), "drag should be cancelled");
}
#[test]
fn drop_on_target_fires_handler() {
use std::cell::Cell;
use std::rc::Rc;
let dropped = Rc::new(Cell::new(false));
let dropped_value = Rc::new(Cell::new(0_u32));
let d = dropped.clone();
let dv = dropped_value.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(FillWidget::new().on_drop(move |mut payload, _pos, _ctx| {
d.set(true);
if let Some(val) = payload.take_typed::<u32>() {
dv.set(val);
}
true
}));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none(), "drag session should be cleared");
assert!(dropped.get(), "on_drop should have been called");
assert_eq!(dropped_value.get(), 42);
}
#[test]
fn drop_on_no_target_cancels() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(999.0, 999.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none(), "drag session should be cleared");
}
#[test]
fn drop_falls_through_a_destroyed_current_target() {
use std::cell::Cell;
use std::rc::Rc;
let bg_dropped = Rc::new(Cell::new(false));
let bg_sink = bg_dropped.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _bg = tree.add(FillWidget::new().on_drop(move |_p, _pos, _ctx| {
bg_sink.set(true);
true
}));
let fg = tree.add(
FillWidget::new()
.on_drag_hover(|_payload, _pos, _ctx| {
crate::drag_state::DropFeedback::InsertionLine {
y: 50.0,
width: 200.0,
}
})
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
assert_eq!(
tree.active_drag.as_ref().unwrap().current_target,
Some(fg),
"fg engaged as the current drop target"
);
tree.arena.destroy(fg);
assert!(!tree.arena.is_active(fg));
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none(), "drag session cleared");
assert!(
bg_dropped.get(),
"drop landed on the live background target, not the destroyed one"
);
}
fn mouse_members(
tree: &WidgetTree,
) -> Vec<(
WidgetId,
crate::gesture::MemberRole,
crate::gesture::MemberState,
)> {
tree.sequence_members(crate::pointer::PointerId::MOUSE)
}
#[test]
fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
use crate::gesture::{MemberRole, MemberState};
let mut tree = WidgetTree::new();
let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
let inner = tree.add(StackWidget::new().child(button));
let ancestor = tree.add(StackWidget::new().child(inner).on_drag(|_phase, _ctx| {}));
tree.layout(SizeProposal::exact(100.0, 100.0));
let b = tree.bounds(button);
tree.pointer_down_button(
Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
PointerButton::Primary,
);
assert_eq!(
mouse_members(&tree),
vec![(ancestor, MemberRole::Gesture, MemberState::Possible)],
"the draggable ancestor competes when the button press is not in a dead zone"
);
tree.pointer_up_button(
Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
PointerButton::Primary,
);
}
#[test]
fn gesture_dead_zone_blocks_ancestor_drag_arming() {
use crate::widget_builder::WidgetBuilder;
let mut tree = WidgetTree::new();
let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
let _ancestor = tree.add(
StackWidget::new()
.child(dead_zone)
.on_drag(|_phase, _ctx| {}),
);
tree.layout(SizeProposal::exact(100.0, 100.0));
let b = tree.bounds(button);
tree.pointer_down_button(
Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
PointerButton::Primary,
);
assert!(
mouse_members(&tree).is_empty(),
"a dead zone blocks the draggable ancestor from competing"
);
tree.pointer_up_button(
Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
PointerButton::Primary,
);
}
#[test]
fn a_dead_zone_boundary_blocks_a_mouse_exactly_as_it_blocks_a_finger() {
use crate::pointer::{
BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
};
use crate::widget_builder::WidgetBuilder;
let mut tree = WidgetTree::new();
let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
let dead_zone = tree.add(StackWidget::new().child(button).gesture_dead_zone(true));
tree.add(
StackWidget::new()
.child(dead_zone)
.on_drag(|_phase, _ctx| {}),
);
tree.layout(SizeProposal::exact(100.0, 100.0));
let b = tree.bounds(button);
let at = Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0);
tree.pointer_down_button(at, PointerButton::Primary);
assert!(
mouse_members(&tree).is_empty(),
"the mouse enrols no ancestor across the dead zone"
);
tree.pointer_up_button(at, PointerButton::Primary);
let contact = PointerIdAllocator::global().begin(BackendDeviceKey::DEFAULT, 41);
let sample = |phase| PointerSample {
pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
phase,
position: at,
button: None,
modifiers: Modifiers::NONE,
coalesced: Vec::new(),
};
tree.dispatch_pointer(sample(PointerPhase::Down));
assert!(
tree.sequence_members(contact).is_empty(),
"and neither does a finger"
);
tree.dispatch_pointer(sample(PointerPhase::Up));
}
#[test]
fn drag_hover_calls_on_drag_hover() {
use std::cell::Cell;
use std::rc::Rc;
let hover_count = Rc::new(Cell::new(0));
let hc = hover_count.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(move |_payload, _pos, _ctx| {
hc.set(hc.get() + 1);
crate::drag_state::DropFeedback::InsertionLine {
y: 50.0,
width: 200.0,
}
})
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed("test"));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
assert!(
hover_count.get() > 0,
"on_drag_hover should have been called"
);
}
#[test]
fn rejecter_under_engaging_ancestor_leaves_once() {
use std::cell::Cell;
use std::rc::Rc;
let leaves = Rc::new(Cell::new(0));
let lv = leaves.clone();
let engage = Rc::new(Cell::new(false));
let eg = engage.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let child = tree.add(
FillWidget::new()
.on_drag_hover(|_payload, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
.on_drag_leave(move |_ctx| lv.set(lv.get() + 1)),
);
let _ancestor = tree.add(StackWidget::new().child(child).on_drag_hover(
move |_payload, _pos, _ctx| {
if eg.get() {
crate::drag_state::DropFeedback::Accept
} else {
crate::drag_state::DropFeedback::NoFeedback
}
},
));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
assert_eq!(
tree.active_drag.as_ref().unwrap().current_target,
Some(child)
);
assert_eq!(leaves.get(), 0, "no leave yet — child is freshly tracked");
engage.set(true);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(101.0, 50.0)));
assert_eq!(
leaves.get(),
1,
"child must receive exactly one on_drag_leave, not two"
);
}
#[test]
fn drop_outside_window_cancels() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(-100.0, -100.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none(), "drag should be cleared");
}
#[test]
fn drop_target_rejects_wrong_type() {
use std::cell::Cell;
use std::rc::Rc;
let accepted = Rc::new(Cell::new(false));
let a = accepted.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(|payload, _pos, _ctx| {
if payload.has_typed::<String>() {
crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 100.0,
}
} else {
crate::drag_state::DropFeedback::NoFeedback
}
})
.on_drop(move |payload, _pos, _ctx| {
if payload.has_typed::<String>() {
a.set(true);
true
} else {
false
}
}),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(!accepted.get(), "on_drop should reject wrong payload type");
}
#[test]
fn inter_widget_drop_transfers_payload() {
use std::cell::Cell;
use std::rc::Rc;
let received_value = Rc::new(Cell::new(0_u32));
let rv = received_value.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(|_payload, _pos, _ctx| {
crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 100.0,
}
})
.on_drop(move |mut payload, _pos, _ctx| {
if let Some(val) = payload.take_typed::<u32>() {
rv.set(val);
true
} else {
false
}
}),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(777_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert_eq!(
received_value.get(),
777,
"Target should receive the typed payload from source"
);
}
#[test]
fn drop_on_child_walks_up_to_ancestor_drop_target() {
use crate::test_widgets::StackWidget;
use std::cell::Cell;
use std::rc::Rc;
let parent_fired = Rc::new(Cell::new(false));
let pf = parent_fired.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let child = tree.add(FillWidget::new());
let _parent = tree.add(StackWidget::new().child(child).on_drop(
move |_payload, _pos, _ctx| {
pf.set(true);
true
},
));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(
parent_fired.get(),
"Parent's on_drop should fire via ancestor walk"
);
}
#[test]
fn drop_bubbles_past_a_rejecting_child_to_ancestor() {
use crate::test_widgets::StackWidget;
use std::cell::Cell;
use std::rc::Rc;
let child_drop = Rc::new(Cell::new(false));
let parent_drop = Rc::new(Cell::new(false));
let cd = child_drop.clone();
let pd = parent_drop.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let child = tree.add(
FillWidget::new()
.on_drag_hover(|_p, _pos, _ctx| crate::drag_state::DropFeedback::NoFeedback)
.on_drop(move |_p, _pos, _ctx| {
cd.set(true);
false }),
);
let _parent = tree.add(
StackWidget::new()
.child(child)
.on_drop(move |_p, _pos, _ctx| {
pd.set(true);
true
}),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(7_u8));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(parent_drop.get(), "drop bubbles to the accepting ancestor");
assert!(
!child_drop.get(),
"the rejecting child must not receive the drop"
);
}
#[test]
fn drag_preview_overlay_created_and_dismissed() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let overlay_count_before = tree.overlay_manager().len();
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some(), "drag session should be active");
assert!(
tree.active_drag
.as_ref()
.unwrap()
.preview_overlay_id
.is_some(),
"preview overlay id should be recorded"
);
assert_eq!(
tree.overlay_manager().len(),
overlay_count_before + 1,
"overlay count should increase by one for the preview"
);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(999.0, 999.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none(), "drag session should be cleared");
assert_eq!(
tree.overlay_manager().len(),
overlay_count_before,
"preview overlay should be dismissed on drop"
);
}
#[test]
fn drag_preview_teardown_releases_what_its_content_registered() {
#[derive(Debug)]
struct SpinningPreview;
impl Widget for SpinningPreview {
fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
let _ = ctx.animated_quad(crate::animated_quad::AnimatedQuadKind::SpinnerArc {
period: std::time::Duration::from_secs(1),
arc_fraction: 0.25,
stroke_fraction: 0.12,
color: teksilo_tokens::Color::RED.into(),
});
Vec::new()
}
fn layout_response(
&self,
_: SizeProposal,
_: &crate::widget::LayoutContext,
) -> crate::widget::LayoutResponse {
teksilo_canvas::Size::new(20.0, 20.0).into()
}
}
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(SpinningPreview),
);
tree.collect_from_ctx(ctx, source);
assert_eq!(tree.animated_quad_count(), 1, "the preview's build ran");
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(999.0, 999.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(tree.active_drag.is_none());
assert_eq!(
tree.animated_quad_count(),
0,
"tearing the preview down must release its animated-quad slot"
);
}
#[test]
fn drag_preview_follows_pointer_position() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed("p"),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(73.0, 41.0)));
let drag = tree.active_drag.as_ref().expect("active drag");
assert!(
(drag.current_position.x - 73.0).abs() < 0.01
&& (drag.current_position.y - 41.0).abs() < 0.01,
"drag session position should track the pointer"
);
let overlay_id = drag.preview_overlay_id.expect("preview overlay");
let overlay = tree
.overlay_manager()
.overlay(overlay_id)
.expect("overlay looked up by id");
match &overlay.placement {
crate::overlay::OverlayPlacement::AtPointer(p) => {
assert!(
(p.x - 73.0).abs() < 0.01 && (p.y - 41.0).abs() < 0.01,
"preview overlay placement should follow pointer"
);
}
other => panic!("expected AtPointer placement, got {:?}", other),
}
}
#[test]
fn escape_during_hover_dismisses_preview() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(|_payload, _pos, _ctx| {
crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 100.0,
}
})
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let overlay_count_before = tree.overlay_manager().len();
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
assert!(tree.active_drag.is_some());
assert_eq!(tree.overlay_manager().len(), overlay_count_before + 1);
tree.press_key(Key::Escape, Modifiers::NONE);
assert!(tree.active_drag.is_none(), "drag must be cancelled");
assert_eq!(
tree.overlay_manager().len(),
overlay_count_before,
"preview overlay must be dismissed after Escape"
);
}
#[test]
fn active_drag_blocks_on_tap_on_other_widgets() {
use std::cell::Cell;
use std::rc::Rc;
let tap_fired = Rc::new(Cell::new(false));
let tf = tap_fired.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _other = tree.add(FillWidget::new().on_tap(move |_pos, _ctx| {
tf.set(true);
}));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(150.0, 50.0)));
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert!(
!tap_fired.get(),
"on_tap must not fire during an active drag"
);
}
#[test]
fn on_drag_leave_fires_when_pointer_leaves_target_bounds() {
use crate::test_widgets::InsetWidget;
use std::cell::Cell;
use std::rc::Rc;
let leave = Rc::new(Cell::new(0_u32));
let l = leave.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
assert_eq!(leave.get(), 0, "no leave yet — target just became active");
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
assert_eq!(
leave.get(),
1,
"on_drag_leave fires when pointer exits the target's bounds"
);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
assert_eq!(
leave.get(),
1,
"leave fires at most once per leave transition"
);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(10.0, 10.0)));
assert_eq!(leave.get(), 2);
}
#[test]
fn on_drag_leave_fires_on_drop() {
use std::cell::Cell;
use std::rc::Rc;
let leave = Rc::new(Cell::new(0_u32));
let l = leave.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert_eq!(leave.get(), 1, "on_drag_leave fires exactly once on drop");
}
#[test]
fn on_drag_leave_fires_on_escape_cancel() {
use std::cell::Cell;
use std::rc::Rc;
let leave = Rc::new(Cell::new(0_u32));
let l = leave.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.press_key(Key::Escape, Modifiers::NONE);
assert_eq!(
leave.get(),
1,
"Escape cancel must fire on_drag_leave on the current target"
);
}
#[test]
fn on_drag_leave_fires_when_source_destroyed_mid_drag() {
use std::cell::Cell;
use std::rc::Rc;
let leave = Rc::new(Cell::new(0_u32));
let l = leave.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.arena.destroy(source);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert!(
tree.active_drag.is_none(),
"active drag should have been cancelled"
);
assert_eq!(
leave.get(),
1,
"on_drag_leave fires on the drop target when the source is torn down"
);
}
#[test]
fn on_drag_tick_fires_per_layout_pass() {
use std::cell::Cell;
use std::rc::Rc;
let ticks = Rc::new(Cell::new(0_u32));
let t = ticks.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_drag_tick(move |_pos, _ctx| t.set(t.get() + 1))
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
assert_eq!(ticks.get(), 0, "tick shouldn't have fired yet");
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(ticks.get(), 1);
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(ticks.get(), 3);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
let after_drop = ticks.get();
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(
ticks.get(),
after_drop,
"on_drag_tick must not fire after drag ends"
);
}
#[test]
fn on_drag_hover_and_on_drop_receive_widget_local_coordinates() {
use crate::test_widgets::InsetWidget;
use std::cell::Cell;
use std::rc::Rc;
let hover_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
let h = hover_local.clone();
let d = drop_local.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let target = tree.add(
FillWidget::new()
.on_drag_hover(move |_p, pos, _ctx| {
h.set(pos);
crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
}
})
.on_drop(move |_payload, pos, _ctx| {
d.set(pos);
true
}),
);
let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 60.0)));
let hov = hover_local.get();
assert!(
(hov.x - 60.0).abs() < 0.01 && (hov.y - 20.0).abs() < 0.01,
"on_drag_hover should receive local coords, got {:?}",
hov,
);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(110.0, 55.0),
PointerButton::Primary,
Modifiers::NONE,
));
let drp = drop_local.get();
assert!(
(drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
"on_drop should receive local coords, got {:?}",
drp,
);
}
#[test]
fn active_drag_sets_grabbing_cursor() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
assert_eq!(tree.current_cursor(), CursorIcon::Default);
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(50.0, 25.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert_eq!(tree.current_cursor(), CursorIcon::Default);
}
#[test]
fn escape_cancel_resets_cursor() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
assert_eq!(tree.current_cursor(), CursorIcon::Grabbing);
tree.press_key(Key::Escape, Modifiers::NONE);
assert_eq!(tree.current_cursor(), CursorIcon::Default);
}
#[test]
fn drag_preview_composite_gets_built() {
use std::cell::Cell;
use std::rc::Rc;
let built = Rc::new(Cell::new(false));
let b = built.clone();
#[derive(Debug)]
struct CheckingWidget {
built: Rc<Cell<bool>>,
}
impl Widget for CheckingWidget {
fn build(&mut self, _ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
self.built.set(true);
Vec::new()
}
fn layout_response(
&self,
_: SizeProposal,
_: &crate::widget::LayoutContext,
) -> crate::widget::LayoutResponse {
teksilo_canvas::Size::new(50.0, 20.0).into()
}
}
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(CheckingWidget { built: b }),
);
tree.collect_from_ctx(ctx, source);
assert!(built.get(), "preview's build() must fire on drag start");
}
#[test]
fn preview_placement_drives_layout_needs() {
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 200.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(0_u32),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
assert!(
tree.needs_layout(),
"drag start must mark preview content for layout"
);
tree.layout(SizeProposal::exact(200.0, 200.0));
assert!(
!tree.needs_layout(),
"layout should have cleared dirty flag"
);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(75.0, 120.0)));
assert!(
tree.needs_layout(),
"PointerMove during drag must mark preview for layout"
);
}
#[test]
fn scroll_during_drag_routes_to_drop_target() {
use std::cell::Cell;
use std::rc::Rc;
let scroll_count = Rc::new(Cell::new(0_u32));
let sc = scroll_count.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::InsertionLine {
y: 0.0,
width: 10.0,
},
)
.on_scroll(move |event, _ctx| match event {
WidgetEvent::Scroll { .. } => {
sc.set(sc.get() + 1);
EventResponse::Handled
}
_ => EventResponse::Ignored,
})
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_move(Point::new(100.0, 50.0)));
tree.dispatch_event(WidgetEvent::scroll(
crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
Default::default(),
));
assert_eq!(
scroll_count.get(),
1,
"Scroll during drag must route to the current drop target"
);
}
#[test]
fn external_drop_delivers_files_and_marks_external() {
use crate::drag_payload::ExternalDropData;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
let dropped_files: Rc<RefCell<Vec<PathBuf>>> = Rc::new(RefCell::new(Vec::new()));
let was_external = Rc::new(std::cell::Cell::new(false));
let df = dropped_files.clone();
let we = was_external.clone();
let mut tree = WidgetTree::new();
let _target = tree.add(
FillWidget::new()
.on_drag_hover(|payload, _pos, _ctx| {
if payload.is_external() && !payload.files().is_empty() {
crate::drag_state::DropFeedback::HighlightRect {
rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
color: teksilo_tokens::Color::WHITE,
}
} else {
crate::drag_state::DropFeedback::NoFeedback
}
})
.on_drop(move |payload, _pos, _ctx| {
we.set(payload.is_external());
*df.borrow_mut() = payload.files().to_vec();
true
}),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut noop = crate::window::NoopWindowOps;
let data = ExternalDropData {
files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
..Default::default()
};
tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
assert!(tree.active_drag.is_some());
assert!(tree.active_drag.as_ref().unwrap().is_external);
tree.update_external_drag(Point::new(110.0, 55.0), &mut noop);
let drop_data = ExternalDropData {
files: vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
..Default::default()
};
tree.end_external_drag(Point::new(110.0, 55.0), drop_data, &mut noop);
assert!(
tree.active_drag.is_none(),
"external drag must clear on drop"
);
assert!(was_external.get(), "payload should report external origin");
assert_eq!(
*dropped_files.borrow(),
vec![PathBuf::from("/tmp/a.png"), PathBuf::from("/tmp/b.png")],
);
}
#[test]
fn external_drop_passes_local_coordinates() {
use crate::drag_payload::ExternalDropData;
use crate::test_widgets::InsetWidget;
use std::cell::Cell;
use std::path::PathBuf;
use std::rc::Rc;
let drop_local = Rc::new(Cell::new(Point::new(-1.0, -1.0)));
let d = drop_local.clone();
let mut tree = WidgetTree::new();
let target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
color: teksilo_tokens::Color::WHITE,
},
)
.on_drop(move |_payload, pos, _ctx| {
d.set(pos);
true
}),
);
let _wrapper = tree.add(InsetWidget::new(40.0).set_child(target));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut noop = crate::window::NoopWindowOps;
let data = ExternalDropData {
files: vec![PathBuf::from("/tmp/x")],
..Default::default()
};
tree.begin_external_drag(Point::new(110.0, 55.0), data, &mut noop);
tree.end_external_drag(
Point::new(110.0, 55.0),
ExternalDropData::default(),
&mut noop,
);
let drp = drop_local.get();
assert!(
(drp.x - 70.0).abs() < 0.01 && (drp.y - 15.0).abs() < 0.01,
"external on_drop should receive local coords, got {:?}",
drp,
);
}
#[test]
fn cancel_external_drag_clears_session_and_fires_leave() {
use crate::drag_payload::ExternalDropData;
use std::cell::Cell;
use std::path::PathBuf;
use std::rc::Rc;
let left = Rc::new(Cell::new(0_u32));
let l = left.clone();
let mut tree = WidgetTree::new();
let _target = tree.add(
FillWidget::new()
.on_drag_hover(
|_p, _pos, _ctx| crate::drag_state::DropFeedback::HighlightRect {
rect: teksilo_canvas::Rect::new(0.0, 0.0, 10.0, 10.0),
color: teksilo_tokens::Color::WHITE,
},
)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut noop = crate::window::NoopWindowOps;
let data = ExternalDropData {
files: vec![PathBuf::from("/tmp/x")],
..Default::default()
};
tree.begin_external_drag(Point::new(100.0, 50.0), data, &mut noop);
assert!(tree.active_drag.is_some());
tree.cancel_external_drag(&mut noop);
assert!(tree.active_drag.is_none(), "cancel must clear the session");
assert_eq!(
left.get(),
1,
"cancel must fire on_drag_leave on the target"
);
}
#[test]
fn external_drag_helpers_noop_without_session() {
let mut tree = WidgetTree::new();
let _t = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(100.0, 50.0));
let mut noop = crate::window::NoopWindowOps;
tree.update_external_drag(Point::new(10.0, 10.0), &mut noop);
tree.end_external_drag(
Point::new(10.0, 10.0),
crate::drag_payload::ExternalDropData::default(),
&mut noop,
);
tree.cancel_external_drag(&mut noop);
assert!(tree.active_drag.is_none());
}
struct RecordingWindowOps {
started: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OutboundDragData>>>,
started_kinds: std::rc::Rc<std::cell::RefCell<Vec<teksilo_tokens::PointerKind>>>,
start: crate::drag_payload::OsDragStart,
pending_ids: std::rc::Rc<std::cell::RefCell<Vec<crate::drag_payload::OsDragId>>>,
cancels: std::rc::Rc<std::cell::Cell<usize>>,
accepts: std::rc::Rc<std::cell::RefCell<Vec<bool>>>,
}
impl RecordingWindowOps {
fn new(succeed: bool) -> Self {
Self::answering(if succeed {
crate::drag_payload::OsDragStart::Started
} else {
crate::drag_payload::OsDragStart::Declined
})
}
fn answering(start: crate::drag_payload::OsDragStart) -> Self {
Self {
started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
}
}
}
impl crate::window::WindowOps for RecordingWindowOps {
fn open_window(
&mut self,
_c: crate::window::WindowConfig,
) -> crate::window::TeksiloWindowId {
panic!("not used in these tests")
}
fn find_window(&self, _s: &str) -> Option<crate::window::TeksiloWindowId> {
None
}
fn window_state(
&self,
_id: crate::window::TeksiloWindowId,
) -> Option<crate::window::WindowState> {
None
}
fn windows(&self) -> Vec<crate::window::WindowState> {
Vec::new()
}
fn focus_window(&mut self, _id: crate::window::TeksiloWindowId) {}
fn close_window_by_id(&mut self, _id: crate::window::TeksiloWindowId) {}
fn begin_os_drag(
&mut self,
data: crate::drag_payload::OutboundDragData,
_image: Option<crate::drag_payload::DragImageData>,
pointer: teksilo_tokens::PointerKind,
) -> crate::drag_payload::OsDragStart {
self.started.borrow_mut().push(data);
self.started_kinds.borrow_mut().push(pointer);
match self.start {
crate::drag_payload::OsDragStart::Pending(_) => {
let id = crate::drag_payload::OsDragId::next();
self.pending_ids.borrow_mut().push(id);
crate::drag_payload::OsDragStart::Pending(id)
}
other => other,
}
}
fn cancel_os_drag(&mut self) {
self.cancels.set(self.cancels.get() + 1);
}
fn set_drop_accepted(&mut self, accepted: bool) {
self.accepts.borrow_mut().push(accepted);
}
}
fn exportable_payload() -> crate::drag_payload::DragPayload {
crate::drag_payload::DragPayload::typed(7_u32).with_mime("text/plain", b"hi".to_vec())
}
#[test]
fn internal_exportable_drag_escalates_when_leaving_window() {
let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started: started.clone(),
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, exportable_payload());
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
assert!(started.borrow().is_empty());
assert!(tree.active_drag.is_some());
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "begin_os_drag called once");
assert!(
started.borrow()[0].mime.contains_key("text/plain"),
"outbound data carries the payload's mime"
);
assert!(tree.active_drag.is_none(), "in-app session torn down");
assert_eq!(tree.outbound_drag_source, Some(source));
}
#[test]
fn os_drag_ended_fires_source_on_drag_ended() {
use crate::drag_payload::DropOutcome;
use std::cell::Cell;
use std::rc::Rc;
let outcome = Rc::new(Cell::new(None));
let o = outcome.clone();
let started = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started,
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source =
tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, exportable_payload());
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
tree.handle_os_drag_ended(DropOutcome::OsMove, &mut ops);
assert_eq!(outcome.get(), Some(DropOutcome::OsMove));
assert!(
tree.outbound_drag_source.is_none(),
"cleared after delivery"
);
}
#[test]
fn escape_during_an_escalated_drag_asks_the_platform_to_cancel() {
let mut ops = RecordingWindowOps::new(true);
let cancels = ops.cancels.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, exportable_payload());
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert_eq!(tree.outbound_drag_source, Some(source));
tree.dispatch_event_with_ops(
crate::event::WidgetEvent::KeyDown {
key: crate::event::Key::Escape,
modifiers: crate::event::Modifiers::NONE,
text: None,
},
&mut ops,
);
assert_eq!(cancels.get(), 1, "the platform must be asked to cancel");
assert_eq!(
tree.outbound_drag_source,
Some(source),
"the session stays until the backend reports its terminal outcome — \
tearing it down here would drop the source's on_drag_ended"
);
}
#[test]
fn escape_without_an_os_drag_does_not_touch_the_platform() {
let mut ops = RecordingWindowOps::new(true);
let cancels = ops.cancels.clone();
let mut tree = WidgetTree::new();
tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.dispatch_event_with_ops(
crate::event::WidgetEvent::KeyDown {
key: crate::event::Key::Escape,
modifiers: crate::event::Modifiers::NONE,
text: None,
},
&mut ops,
);
assert_eq!(cancels.get(), 0);
}
#[test]
fn no_backend_keeps_session_active_on_leave() {
let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started: started.clone(),
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Declined,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, exportable_payload());
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "escalation was attempted");
assert!(tree.active_drag.is_some(), "session kept (no backend)");
assert!(tree.outbound_drag_source.is_none());
}
#[test]
fn non_exportable_drag_does_not_escalate() {
let started = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started: started.clone(),
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u32));
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(started.borrow().is_empty(), "no escalation attempt");
assert!(tree.active_drag.is_some(), "session unaffected");
}
#[test]
fn in_app_drop_fires_source_on_drag_ended_with_accepted() {
use crate::drag_payload::DropOutcome;
use std::cell::Cell;
use std::rc::Rc;
let outcome = Rc::new(Cell::new(None));
let o = outcome.clone();
let mut tree = WidgetTree::new();
let source =
tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(42_u32));
tree.collect_from_ctx(ctx, source);
tree.dispatch_event(WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
));
assert_eq!(outcome.get(), Some(DropOutcome::InApp { accepted: true }));
}
#[test]
fn escape_fires_source_on_drag_ended_cancelled() {
use crate::drag_payload::DropOutcome;
use std::cell::Cell;
use std::rc::Rc;
let outcome = Rc::new(Cell::new(None));
let o = outcome.clone();
let mut tree = WidgetTree::new();
let source =
tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| o.set(Some(outcome))));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(0_u32));
tree.collect_from_ctx(ctx, source);
tree.press_key(Key::Escape, Modifiers::NONE);
assert_eq!(outcome.get(), Some(DropOutcome::Cancelled));
}
#[test]
fn os_drag_reentry_recovers_typed_payload_for_in_app_drop() {
use crate::drag_payload::{DragPayload, DropOutcome};
use std::cell::Cell;
use std::rc::Rc;
let started = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started,
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let got_typed = Rc::new(Cell::new(0_u32));
let ended = Rc::new(Cell::new(0_u32));
let last_outcome = Rc::new(Cell::new(None));
let g = got_typed.clone();
let e = ended.clone();
let lo = last_outcome.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new().on_drag_ended(move |outcome, _ctx| {
e.set(e.get() + 1);
lo.set(Some(outcome));
}));
let _target =
tree.add(
FillWidget::new().on_drop(move |mut p, _, _| match p.take_typed::<u32>() {
Some(v) => {
g.set(v);
true
}
None => false,
}),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
source,
DragPayload::typed(123_u32).with_mime("text/plain", b"x".to_vec()),
);
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(tree.active_drag.is_none());
assert!(
super::has_outbound_typed(),
"typed payload stashed globally"
);
tree.begin_external_drag(
Point::new(100.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
let d = tree.active_drag.as_ref().expect("re-entered session");
assert!(!d.is_external, "re-entry is an internal session");
assert!(d.payload.has_typed::<u32>(), "typed payload recovered");
assert_eq!(
d.payload.text(),
Some("x"),
"external view enriched from MIME so DropZone-style targets also accept"
);
assert!(!super::has_outbound_typed(), "stash taken by the re-entry");
tree.end_external_drag(
Point::new(150.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(got_typed.get(), 123, "target received the typed payload");
assert_eq!(
ended.get(),
0,
"source on_drag_ended not fired by the drop itself"
);
tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
assert_eq!(ended.get(), 1, "on_drag_ended fired exactly once");
assert_eq!(
last_outcome.get(),
Some(DropOutcome::InApp { accepted: true })
);
}
#[test]
fn os_drag_reentry_recovers_typed_payload_across_windows() {
use crate::drag_payload::{DragPayload, DropOutcome};
use std::cell::Cell;
use std::rc::Rc;
let started = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started,
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree_a = WidgetTree::new();
let src = tree_a.add(FillWidget::new());
tree_a.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
src,
DragPayload::typed(77_u32).with_mime("text/plain", b"x".to_vec()),
);
tree_a.collect_from_ctx(ctx, src);
tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(super::has_outbound_typed());
assert_eq!(tree_a.outbound_drag_source, Some(src));
let got = Rc::new(Cell::new(0_u32));
let g = got.clone();
let mut tree_b = WidgetTree::new();
let _t = tree_b.add(FillWidget::new().on_drop(
move |mut p, _, _| match p.take_typed::<u32>() {
Some(v) => {
g.set(v);
true
}
None => false,
},
));
tree_b.layout(SizeProposal::exact(200.0, 100.0));
tree_b.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(
tree_b
.active_drag
.as_ref()
.is_some_and(|d| d.payload.has_typed::<u32>()),
"window B recovered the typed payload"
);
tree_b.end_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(
got.get(),
77,
"window B's target received the typed payload"
);
tree_a.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
}
#[test]
fn os_drag_reexit_restashes_payload() {
use crate::drag_payload::DragPayload;
use std::rc::Rc;
let started = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started: started.clone(),
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
source,
DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
);
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "OS drag started once");
tree.begin_external_drag(
Point::new(100.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree.os_drag_reentered);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(!tree.os_drag_reentered, "re-exited");
assert!(tree.active_drag.is_none(), "session torn down on re-exit");
assert!(super::has_outbound_typed(), "payload re-stashed");
assert_eq!(
started.borrow().len(),
1,
"no second OS drag started on re-exit"
);
}
#[test]
fn source_window_close_clears_stash_no_hijack() {
use crate::drag_payload::{DragPayload, ExternalDropData};
use std::path::PathBuf;
use std::rc::Rc;
let started = Rc::new(std::cell::RefCell::new(Vec::new()));
let mut ops = RecordingWindowOps {
started,
started_kinds: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
start: crate::drag_payload::OsDragStart::Started,
pending_ids: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
accepts: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
};
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
let _target = tree.add(FillWidget::new().on_drop(|_, _, _| true));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
source,
DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
);
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(-5.0, 50.0), &mut ops); assert!(super::has_outbound_typed());
assert_eq!(tree.outbound_drag_source, Some(source));
tree.abort_outbound_drag();
assert!(
!super::has_outbound_typed(),
"stash cleared when the source window closes"
);
assert!(tree.outbound_drag_source.is_none());
tree.begin_external_drag(
Point::new(50.0, 50.0),
ExternalDropData {
files: vec![PathBuf::from("/tmp/real")],
..Default::default()
},
&mut ops,
);
let d = tree.active_drag.as_ref().expect("external session");
assert!(
d.is_external,
"stale stash did not hijack the new external drag"
);
assert!(
!d.payload.has_typed::<u32>(),
"no stale typed payload leaked in"
);
assert_eq!(d.payload.files(), &[PathBuf::from("/tmp/real")]);
}
use std::cell::{Cell, RefCell};
use std::rc::Rc;
fn drag_arming_leaf(
slot: Rc<Cell<Option<WidgetId>>>,
payload: impl Fn() -> crate::drag_payload::DragPayload + 'static,
) -> impl crate::widget::Widget {
let armed = Cell::new(false);
FillWidget::new().on_pointer_event(move |event, ctx| {
if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
&& !armed.replace(true)
&& let Some(id) = slot.get()
{
ctx.start_drag(id, payload());
}
crate::event::EventResponse::Ignored
})
}
fn touch_press(
tree: &mut WidgetTree,
at: Point,
ops: &mut dyn crate::window::WindowOps,
) -> crate::pointer::PointerId {
use crate::pointer::{
BackendDeviceKey, PointerIdAllocator, PointerInfo, PointerPhase, PointerSample,
};
use std::sync::atomic::{AtomicU64, Ordering};
static NEXT: AtomicU64 = AtomicU64::new(91_000);
let contact = PointerIdAllocator::global().begin(
BackendDeviceKey::DEFAULT,
NEXT.fetch_add(1, Ordering::Relaxed),
);
tree.dispatch_pointer_with_ops(
PointerSample {
pointer: PointerInfo::touch(contact, crate::pointer::EventTime::from_millis(1)),
phase: PointerPhase::Down,
position: at,
button: None,
modifiers: Modifiers::NONE,
coalesced: Vec::new(),
},
ops,
);
contact
}
fn placement_of(
tree: &WidgetTree,
id: crate::overlay::OverlayId,
) -> crate::overlay::OverlayPlacement {
tree.overlay_manager
.stack
.iter()
.find(|o| o.id == id)
.map(|o| o.placement.clone())
.expect("the overlay is in the stack")
}
#[test]
fn a_drag_tick_reports_the_device_that_started_the_drag() {
let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
let s = seen.clone();
let mut tree = WidgetTree::new();
let target = tree.add(
FillWidget::new()
.on_drop(|_, _, _| true)
.on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(target, crate::drag_payload::DragPayload::typed(1_u8));
tree.collect_from_ctx(ctx, target);
tree.handle_drag_move(Point::new(50.0, 50.0), &mut crate::window::NoopWindowOps);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(
*seen.borrow(),
vec![teksilo_tokens::PointerKind::Mouse],
"a mouse drag still reports the mouse"
);
tree.cancel_active_drag(&mut crate::window::NoopWindowOps);
seen.borrow_mut().clear();
let mut ops = crate::window::NoopWindowOps;
let s = seen.clone();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let armed = Cell::new(false);
let s2 = slot.clone();
let mut tree = WidgetTree::new();
let row = tree.add(
FillWidget::new()
.on_pointer_event(move |event, ctx| {
if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
&& !armed.replace(true)
&& let Some(id) = s2.get()
{
ctx.start_drag(id, crate::drag_payload::DragPayload::typed(2_u8));
}
crate::event::EventResponse::Ignored
})
.on_drop(|_, _, _| true)
.on_drag_tick(move |_pos, ctx| s.borrow_mut().push(ctx.pointer_kind())),
);
slot.set(Some(row));
tree.layout(SizeProposal::exact(200.0, 100.0));
touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
assert!(tree.active_drag.is_some(), "the finger armed a drag");
tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert_eq!(
*seen.borrow(),
vec![teksilo_tokens::PointerKind::Touch],
"and a finger drag reports the finger, from a tick that has no sample"
);
}
#[test]
fn the_preview_avoids_a_coarse_contact_and_still_pins_a_cursor() {
use crate::overlay::OverlayPlacement;
let mut ops = crate::window::NoopWindowOps;
let at = Point::new(60.0, 40.0);
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag_with_preview(
source,
crate::drag_payload::DragPayload::typed(1_u8),
Box::new(FillWidget::new()),
);
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(at, &mut ops);
let overlay = tree
.active_drag
.as_ref()
.and_then(|d| d.preview_overlay_id)
.expect("the preview overlay exists");
assert!(
matches!(placement_of(&tree, overlay), OverlayPlacement::AtPointer(p) if p == at),
"a mouse keeps AtPointer at the reported point, unchanged",
);
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let source = tree.add(drag_arming_leaf_with_preview(slot.clone()));
slot.set(Some(source));
tree.layout(SizeProposal::exact(200.0, 100.0));
touch_press(&mut tree, at, &mut ops);
assert_eq!(
tree.active_drag.as_ref().map(|d| d.pointer.kind),
Some(teksilo_tokens::PointerKind::Touch),
"the session recorded the finger",
);
tree.handle_drag_move(at, &mut ops);
let overlay = tree
.active_drag
.as_ref()
.and_then(|d| d.preview_overlay_id)
.expect("the preview overlay exists");
match placement_of(&tree, overlay) {
OverlayPlacement::AtPointerAvoiding { point, avoid } => {
assert_eq!(point, at);
assert!(
avoid.contains(at),
"the rectangle to clear is centred on the contact, so no \
placement that honours it can put the preview under the hand",
);
}
other => panic!("a coarse pointer must avoid its own contact, got {other:?}"),
}
}
fn drag_arming_leaf_with_preview(
slot: Rc<Cell<Option<WidgetId>>>,
) -> impl crate::widget::Widget {
let armed = Cell::new(false);
FillWidget::new().on_pointer_event(move |event, ctx| {
if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
&& !armed.replace(true)
&& let Some(id) = slot.get()
{
ctx.start_drag_with_preview(
id,
crate::drag_payload::DragPayload::typed(1_u8),
Box::new(FillWidget::new()),
);
}
crate::event::EventResponse::Ignored
})
}
#[test]
fn an_os_drag_started_by_a_finger_cancels_that_finger() {
let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
Rc::new(RefCell::new(Vec::new()));
let c = cancelled.clone();
let mut ops = RecordingWindowOps::new(true);
let kinds = ops.started_kinds.clone();
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let armed = Cell::new(false);
let s2 = slot.clone();
let source = tree.add(
FillWidget::new()
.on_pointer_event(move |event, ctx| {
if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
&& !armed.replace(true)
&& let Some(id) = s2.get()
{
ctx.start_drag(id, exportable_payload());
}
crate::event::EventResponse::Ignored
})
.on_pointer_cancel(move |pointer, reason, _ctx| {
c.borrow_mut().push((pointer.id, reason));
}),
);
slot.set(Some(source));
tree.layout(SizeProposal::exact(200.0, 100.0));
let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
assert!(tree.active_drag.is_some());
tree.handle_drag_move(Point::new(-40.0, 50.0), &mut ops);
assert_eq!(tree.outbound_drag_source, Some(source));
assert_eq!(
*kinds.borrow(),
vec![teksilo_tokens::PointerKind::Touch],
"the platform is told which device is dragging — Wayland needs the \
touch-down serial, not a button serial"
);
assert_eq!(
*cancelled.borrow(),
vec![(contact, crate::pointer::CancelReason::OsDragStarted)],
"the finger is the pointer revoked"
);
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
#[test]
fn an_escape_cancelled_finger_drag_reports_the_finger_to_its_source() {
let seen: Rc<RefCell<Vec<teksilo_tokens::PointerKind>>> = Rc::new(RefCell::new(Vec::new()));
let s = seen.clone();
let mut ops = crate::window::NoopWindowOps;
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let armed = Cell::new(false);
let s2 = slot.clone();
let source = tree.add(
FillWidget::new()
.on_pointer_event(move |event, ctx| {
if matches!(event, crate::event::WidgetEvent::PointerDown { .. })
&& !armed.replace(true)
&& let Some(id) = s2.get()
{
ctx.start_drag(id, crate::drag_payload::DragPayload::typed(1_u8));
}
crate::event::EventResponse::Ignored
})
.on_drag_ended(move |_outcome, ctx| s.borrow_mut().push(ctx.pointer_kind())),
);
slot.set(Some(source));
tree.layout(SizeProposal::exact(200.0, 100.0));
touch_press(&mut tree, Point::new(50.0, 50.0), &mut ops);
assert!(tree.active_drag.is_some());
tree.dispatch_event_with_ops(
crate::event::WidgetEvent::KeyDown {
key: crate::event::Key::Escape,
modifiers: crate::event::Modifiers::NONE,
text: None,
},
&mut ops,
);
assert_eq!(*seen.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
}
#[test]
fn the_accept_setter_receives_the_widget_verdict() {
use crate::drag_state::DropFeedback;
let verdict = Rc::new(Cell::new(false));
let v = verdict.clone();
let mut ops = RecordingWindowOps::new(true);
let accepts = ops.accepts.clone();
let mut tree = WidgetTree::new();
tree.add(
FillWidget::new()
.on_drag_hover(move |_p, _pos, _ctx| {
if v.get() {
DropFeedback::Accept
} else {
DropFeedback::NoFeedback
}
})
.on_drop(|_, _, _| true),
);
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData {
files: vec![std::path::PathBuf::from("/tmp/a.png")],
..Default::default()
},
&mut ops,
);
assert_eq!(
*accepts.borrow(),
vec![false],
"a refusing target is reported to the OS as a refusal"
);
tree.update_external_drag(Point::new(52.0, 50.0), &mut ops);
assert_eq!(
*accepts.borrow(),
vec![false],
"an unchanged answer is not re-sent"
);
verdict.set(true);
tree.update_external_drag(Point::new(54.0, 50.0), &mut ops);
assert_eq!(
*accepts.borrow(),
vec![false, true],
"and a change is pushed once"
);
}
#[test]
fn an_in_app_drag_pushes_no_os_accept_state() {
let mut ops = RecordingWindowOps::new(true);
let accepts = ops.accepts.clone();
let mut tree = WidgetTree::new();
let source = tree.add(FillWidget::new());
tree.add(FillWidget::new().on_drop(|_, _, _| true));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, crate::drag_payload::DragPayload::typed(1_u8));
tree.collect_from_ctx(ctx, source);
tree.handle_drag_move(Point::new(50.0, 50.0), &mut ops);
assert!(accepts.borrow().is_empty());
}
#[test]
fn a_cancelled_os_drag_tears_the_source_down_exactly_once() {
use crate::drag_payload::{DragPayload, DropOutcome};
let ended = Rc::new(RefCell::new(Vec::new()));
let e = ended.clone();
let mut ops = RecordingWindowOps::new(true);
let mut tree_a = WidgetTree::new();
let src = tree_a.add(
FillWidget::new().on_drag_ended(move |outcome, _ctx| e.borrow_mut().push(outcome)),
);
tree_a.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
src,
DragPayload::typed(5_u32).with_mime("text/plain", b"x".to_vec()),
);
tree_a.collect_from_ctx(ctx, src);
tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(super::outbound_is_live());
let left = Rc::new(Cell::new(0_u32));
let l = left.clone();
let mut tree_b = WidgetTree::new();
tree_b.add(
FillWidget::new()
.on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree_b.layout(SizeProposal::exact(200.0, 100.0));
let accepts = ops.accepts.clone();
accepts.borrow_mut().clear();
tree_b.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree_b.os_drag_reentered);
assert_eq!(
*accepts.borrow(),
vec![true],
"a re-entered app drag still negotiates with the OS: its offer is \
live and a refusal must still reach the compositor's cursor",
);
tree_b.abort_external_drag(&mut ops);
assert!(tree_b.active_drag.is_none(), "B's session is gone");
assert!(!tree_b.os_drag_reentered);
assert_eq!(left.get(), 1, "B's highlighted target was cleared");
assert!(
ended.borrow().is_empty(),
"the abort must not fire the source's on_drag_ended — the source \
window owns that"
);
tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
assert_eq!(
*ended.borrow(),
vec![DropOutcome::Cancelled],
"exactly once"
);
}
#[test]
fn a_reentered_session_is_reaped_when_the_os_drag_ends_elsewhere() {
use crate::drag_payload::{DragPayload, DropOutcome};
let mut ops = RecordingWindowOps::new(true);
let mut tree_a = WidgetTree::new();
let src = tree_a.add(FillWidget::new());
tree_a.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(
src,
DragPayload::typed(9_u32).with_mime("text/plain", b"x".to_vec()),
);
tree_a.collect_from_ctx(ctx, src);
tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
let left = Rc::new(Cell::new(0_u32));
let l = left.clone();
let mut tree_b = WidgetTree::new();
tree_b.add(
FillWidget::new()
.on_drag_hover(|_, _, _| crate::drag_state::DropFeedback::Accept)
.on_drag_leave(move |_ctx| l.set(l.get() + 1))
.on_drop(|_, _, _| true),
);
tree_b.layout(SizeProposal::exact(200.0, 100.0));
tree_b.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree_b.active_drag.is_some() && tree_b.os_drag_reentered);
tree_a.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
assert!(
tree_b.active_drag.is_some(),
"B has not been told anything yet"
);
tree_b.layout(SizeProposal::exact(200.0, 100.0));
assert!(tree_b.active_drag.is_none(), "the dead session was reaped");
assert!(!tree_b.os_drag_reentered);
assert_eq!(left.get(), 1, "and its highlighted target was cleared");
}
#[test]
fn a_reentered_app_drag_recovers_the_device_that_started_it() {
let mut ops = RecordingWindowOps::new(true);
let mut tree_a = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let src = tree_a.add(drag_arming_leaf(slot.clone(), exportable_payload));
slot.set(Some(src));
tree_a.layout(SizeProposal::exact(200.0, 100.0));
touch_press(&mut tree_a, Point::new(20.0, 50.0), &mut ops);
tree_a.handle_drag_move(Point::new(-5.0, 50.0), &mut ops);
assert!(super::outbound_is_live());
let mut tree_b = WidgetTree::new();
tree_b.add(FillWidget::new().on_drop(|_, _, _| true));
tree_b.layout(SizeProposal::exact(200.0, 100.0));
tree_b.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(
tree_b.active_drag.as_ref().map(|d| d.pointer.kind),
Some(teksilo_tokens::PointerKind::Touch),
);
let mut tree_c = WidgetTree::new();
tree_c.add(FillWidget::new().on_drop(|_, _, _| true));
tree_c.layout(SizeProposal::exact(200.0, 100.0));
tree_a.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
tree_c.begin_external_drag(
Point::new(50.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(
tree_c.active_drag.as_ref().map(|d| d.pointer.kind),
Some(teksilo_tokens::PointerKind::Unknown),
"no OS names the source's device to a destination",
);
}
#[test]
fn restash_after_drag_ended_is_noop() {
use crate::drag_payload::DragPayload;
super::outbound_begin(
DragPayload::typed(1_u32).with_mime("text/plain", b"x".to_vec()),
crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO),
);
assert!(super::has_outbound_typed());
let held = super::outbound_take_if_live().expect("payload taken while live");
assert!(!super::has_outbound_typed());
super::outbound_end();
super::outbound_restash(held);
assert!(
!super::has_outbound_typed(),
"ended drag is not resurrected by a racing re-stash"
);
assert!(super::outbound_take_if_live().is_none());
}
fn pending() -> crate::drag_payload::OsDragStart {
crate::drag_payload::OsDragStart::Pending(crate::drag_payload::OsDragId::next())
}
fn last_pending(ops: &RecordingWindowOps) -> crate::drag_payload::OsDragId {
*ops.pending_ids.borrow().last().expect("a pending start")
}
fn eager_payload() -> crate::drag_payload::DragPayload {
exportable_payload().with_os_handoff(crate::drag_payload::OsHandoff::Eager)
}
fn tree_dragging(
payload: crate::drag_payload::DragPayload,
) -> (
WidgetTree,
WidgetId,
Rc<RefCell<Vec<crate::drag_payload::DropOutcome>>>,
) {
let ended: Rc<RefCell<Vec<crate::drag_payload::DropOutcome>>> =
Rc::new(RefCell::new(Vec::new()));
let e = ended.clone();
let mut tree = WidgetTree::new();
let source =
tree.add(FillWidget::new().on_drag_ended(move |o, _ctx| e.borrow_mut().push(o)));
tree.layout(SizeProposal::exact(200.0, 100.0));
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, payload);
tree.collect_from_ctx(ctx, source);
assert!(tree.active_drag.is_some());
(tree, source, ended)
}
#[test]
fn an_eager_drag_hands_off_on_the_first_pointer_move_inside_the_window() {
let mut ops = RecordingWindowOps::new(true);
let started = ops.started.clone();
let (mut tree, source, ended) = tree_dragging(eager_payload());
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(100.0, 50.0)), &mut ops);
assert_eq!(started.borrow().len(), 1, "begin_os_drag asked once");
assert!(tree.active_drag.is_none(), "in-app session handed over");
assert_eq!(tree.outbound_drag_source, Some(source));
assert!(super::has_outbound_typed(), "typed payload parked");
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(120.0, 50.0)), &mut ops);
assert_eq!(started.borrow().len(), 1);
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::OsCopy, &mut ops);
assert_eq!(
*ended.borrow(),
vec![crate::drag_payload::DropOutcome::OsCopy]
);
}
#[test]
fn an_at_window_edge_drag_stays_in_app_while_the_pointer_is_inside() {
let mut ops = RecordingWindowOps::new(true);
let started = ops.started.clone();
let (mut tree, source, _ended) = tree_dragging(exportable_payload());
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(100.0, 50.0)), &mut ops);
assert!(started.borrow().is_empty());
assert!(tree.active_drag.is_some());
tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "the window edge still hands off");
assert_eq!(tree.outbound_drag_source, Some(source));
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
#[test]
fn an_eager_drag_waits_for_a_pointer_sample_rather_than_a_drag_tick() {
let mut ops = RecordingWindowOps::new(true);
let started = ops.started.clone();
let (mut tree, _source, _ended) = tree_dragging(eager_payload());
tree.handle_drag_move(Point::new(100.0, 50.0), &mut ops);
tree.layout(SizeProposal::exact(200.0, 100.0));
assert!(started.borrow().is_empty(), "no handoff from a tick");
assert!(tree.active_drag.is_some());
tree.handle_drag_sample(Point::new(101.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "the next sample hands off");
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
#[test]
fn a_refused_eager_handoff_is_asked_again_on_the_next_sample() {
let mut ops = RecordingWindowOps::new(false);
let started = ops.started.clone();
let (mut tree, source, _ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1);
assert!(
tree.active_drag.is_some(),
"a refusal keeps the in-app drag"
);
tree.handle_drag_sample(Point::new(101.0, 50.0), &mut ops);
assert_eq!(
started.borrow().len(),
2,
"no latch: the next move asks again"
);
ops.start = crate::drag_payload::OsDragStart::Started;
tree.handle_drag_sample(Point::new(102.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 3);
assert_eq!(tree.outbound_drag_source, Some(source), "handed off once");
tree.handle_drag_sample(Point::new(103.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 3);
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
#[test]
fn eager_without_anything_to_export_never_hands_off() {
let mut ops = RecordingWindowOps::new(true);
let started = ops.started.clone();
let (mut tree, _source, _ended) = tree_dragging(
crate::drag_payload::DragPayload::typed(1_u32)
.with_os_handoff(crate::drag_payload::OsHandoff::Eager),
);
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
assert!(started.borrow().is_empty());
assert!(tree.active_drag.is_some());
let mut tree = WidgetTree::new();
tree.add(FillWidget::new().on_drop(|_, _, _| true));
tree.layout(SizeProposal::exact(200.0, 100.0));
let data = crate::drag_payload::ExternalDropData {
text: Some("x".into()),
..Default::default()
};
tree.begin_external_drag(Point::new(10.0, 10.0), data, &mut ops);
tree.update_external_drag(Point::new(-5.0, 10.0), &mut ops);
assert!(started.borrow().is_empty());
}
#[test]
fn a_pending_start_keeps_the_in_app_drag_until_the_platform_confirms_it() {
let mut ops = RecordingWindowOps::answering(pending());
let started = ops.started.clone();
let (mut tree, source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1);
assert!(
tree.active_drag.is_some(),
"the session waits for the platform"
);
assert!(tree.outbound_drag_source.is_none());
assert!(
!super::has_outbound_typed(),
"nothing parked before the start"
);
tree.handle_drag_sample(Point::new(110.0, 50.0), &mut ops);
tree.handle_drag_sample(Point::new(-5.0, 50.0), &mut ops);
assert_eq!(started.borrow().len(), 1, "one request per start");
assert!(tree.active_drag.is_some());
tree.confirm_os_drag(last_pending(&ops), &mut ops);
assert!(
tree.active_drag.is_none(),
"handed over at the confirmation"
);
assert_eq!(tree.outbound_drag_source, Some(source));
assert!(super::has_outbound_typed());
assert!(ended.borrow().is_empty());
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::OsCopy, &mut ops);
assert_eq!(
*ended.borrow(),
vec![crate::drag_payload::DropOutcome::OsCopy]
);
}
#[test]
fn a_release_before_the_native_drag_starts_ends_the_drag_as_cancelled() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::answering(pending());
let cancels = ops.cancels.clone();
let dropped = Rc::new(Cell::new(0_u32));
let d = dropped.clone();
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.add(FillWidget::new().on_drop(move |_, _, _| {
d.set(d.get() + 1);
true
}));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
tree.dispatch_event_with_ops(
WidgetEvent::pointer_up(
Point::new(150.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
),
&mut ops,
);
assert!(tree.active_drag.is_none());
assert_eq!(dropped.get(), 0, "no drop is delivered");
assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
assert_eq!(cancels.get(), 1, "the platform is told to drop the start");
tree.confirm_os_drag(last_pending(&ops), &mut ops);
assert_eq!(cancels.get(), 2);
assert!(tree.outbound_drag_source.is_none());
tree.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
assert_eq!(ended.borrow().len(), 1, "on_drag_ended fired once");
tree.assert_no_leaked_pointer_state();
}
#[test]
fn escape_before_the_native_drag_starts_abandons_the_start() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::answering(pending());
let cancels = ops.cancels.clone();
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
tree.dispatch_event_with_ops(
WidgetEvent::KeyDown {
key: Key::Escape,
modifiers: Modifiers::NONE,
text: None,
},
&mut ops,
);
assert_eq!(cancels.get(), 1);
assert!(tree.os_drag_pending.is_none());
assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
}
#[test]
fn a_start_that_fails_on_the_platform_ends_the_drag_once() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::answering(pending());
let cancels = ops.cancels.clone();
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
tree.handle_os_drag_ended(DropOutcome::Cancelled, &mut ops);
assert!(tree.active_drag.is_none());
assert!(tree.os_drag_pending.is_none());
assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
assert_eq!(cancels.get(), 0, "the backend ended it already");
}
#[test]
fn closing_the_window_drops_a_pending_start() {
let mut ops = RecordingWindowOps::answering(pending());
let (mut tree, _source, _ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
assert!(tree.os_drag_pending.is_some());
tree.abort_outbound_drag();
assert!(tree.os_drag_pending.is_none());
assert!(!super::outbound_is_live());
}
#[test]
fn an_inbound_drag_before_the_confirmation_is_the_native_drag_itself() {
let mut ops = RecordingWindowOps::answering(pending());
let (mut tree, source, _ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
tree.begin_external_drag(
Point::new(100.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(tree.outbound_drag_source, Some(source), "handed over first");
assert!(tree.os_drag_reentered, "then re-entered as our own drag");
assert!(
tree.active_drag
.as_ref()
.is_some_and(|d| d.payload.has_typed::<u32>()),
"with the typed payload recovered"
);
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
#[test]
fn a_leave_while_the_pointer_is_inside_the_window_keeps_the_os_drag_alive() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::new(true);
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
tree.begin_external_drag(
Point::new(100.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree.os_drag_reentered);
tree.update_external_drag(Point::new(110.0, 50.0), &mut ops);
tree.cancel_external_drag(&mut ops);
assert!(!tree.os_drag_reentered);
assert!(tree.active_drag.is_none());
assert!(super::has_outbound_typed(), "payload parked again");
assert!(ended.borrow().is_empty(), "the drag is not over");
tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
assert_eq!(*ended.borrow(), vec![DropOutcome::OsCopy]);
}
#[test]
fn an_eager_drag_dropped_back_on_its_own_window_reports_the_in_app_outcome() {
use crate::drag_payload::DropOutcome;
for accepts in [true, false] {
let mut ops = RecordingWindowOps::new(true);
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.add(FillWidget::new().on_drop(move |_, _, _| accepts));
tree.layout(SizeProposal::exact(200.0, 100.0));
tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
tree.begin_external_drag(
Point::new(150.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
tree.end_external_drag(
Point::new(150.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(
ended.borrow().is_empty(),
"the platform still has to end it"
);
tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
assert_eq!(
*ended.borrow(),
vec![DropOutcome::InApp { accepted: accepts }]
);
}
let mut ops = RecordingWindowOps::new(true);
let (mut tree, _source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(150.0, 50.0), &mut ops);
tree.begin_external_drag(
Point::new(150.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
tree.cancel_external_drag(&mut ops);
tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
assert_eq!(*ended.borrow(), vec![DropOutcome::OsCopy]);
}
#[test]
fn an_eager_drag_asks_the_platform_once_however_often_it_comes_back() {
let mut ops = RecordingWindowOps::new(true);
let started = ops.started.clone();
for round in 1..=2 {
let (mut tree, _source, _ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
for _ in 0..2 {
tree.begin_external_drag(
Point::new(100.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
tree.update_external_drag(Point::new(-5.0, 50.0), &mut ops);
assert!(!tree.os_drag_reentered, "re-exited");
}
assert_eq!(started.borrow().len(), round, "one start per drag");
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
}
#[test]
fn an_eager_finger_drag_tells_the_platform_it_is_a_finger_and_revokes_it() {
let cancelled: Rc<RefCell<Vec<(crate::pointer::PointerId, crate::pointer::CancelReason)>>> =
Rc::new(RefCell::new(Vec::new()));
let c = cancelled.clone();
let mut ops = RecordingWindowOps::answering(pending());
let kinds = ops.started_kinds.clone();
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let source = tree.add(
drag_arming_leaf(slot.clone(), eager_payload).on_pointer_cancel(
move |pointer, reason, _ctx| {
c.borrow_mut().push((pointer.id, reason));
},
),
);
slot.set(Some(source));
tree.layout(SizeProposal::exact(200.0, 100.0));
let contact = touch_press(&mut tree, Point::new(20.0, 50.0), &mut ops);
assert!(tree.active_drag.is_some());
tree.dispatch_pointer_with_ops(
crate::pointer::PointerSample {
pointer: crate::pointer::PointerInfo::touch(
contact,
crate::pointer::EventTime::from_millis(30),
),
phase: crate::pointer::PointerPhase::Move,
position: Point::new(40.0, 50.0),
button: None,
modifiers: Modifiers::NONE,
coalesced: Vec::new(),
},
&mut ops,
);
assert_eq!(*kinds.borrow(), vec![teksilo_tokens::PointerKind::Touch]);
assert!(
cancelled.borrow().is_empty(),
"nothing revoked before the start"
);
tree.confirm_os_drag(last_pending(&ops), &mut ops);
assert_eq!(
*cancelled.borrow(),
vec![(contact, crate::pointer::CancelReason::OsDragStarted)]
);
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
tree.assert_no_leaked_pointer_state();
}
#[test]
fn a_late_event_of_an_earlier_drag_does_not_touch_the_next_one() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::answering(pending());
let cancels = ops.cancels.clone();
let (mut tree, source, ended) = tree_dragging(eager_payload());
tree.handle_drag_sample(Point::new(100.0, 50.0), &mut ops);
let first = last_pending(&ops);
tree.dispatch_event_with_ops(
WidgetEvent::pointer_up(
Point::new(100.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
),
&mut ops,
);
assert_eq!(*ended.borrow(), vec![DropOutcome::Cancelled]);
assert_eq!(cancels.get(), 1);
let mut ctx = crate::widget::EventContext::new();
ctx.start_drag(source, eager_payload());
tree.collect_from_ctx(ctx, source);
tree.handle_drag_sample(Point::new(110.0, 50.0), &mut ops);
let second = last_pending(&ops);
assert_ne!(first, second);
tree.confirm_os_drag(first, &mut ops);
tree.handle_os_drag_ended_for(Some(first), DropOutcome::Cancelled, &mut ops);
assert_eq!(tree.os_drag_pending, Some(second), "drag 2 still waits");
assert!(tree.active_drag.is_some(), "with its in-app session");
assert_eq!(cancels.get(), 1, "and its start is not cancelled");
assert_eq!(ended.borrow().len(), 1);
tree.confirm_os_drag(second, &mut ops);
assert_eq!(tree.outbound_drag_source, Some(source));
tree.handle_os_drag_ended_for(Some(first), DropOutcome::OsCopy, &mut ops);
assert_eq!(ended.borrow().len(), 1, "a stale end does not end drag 2");
tree.handle_os_drag_ended_for(Some(second), DropOutcome::OsCopy, &mut ops);
assert_eq!(
*ended.borrow(),
vec![DropOutcome::Cancelled, DropOutcome::OsCopy]
);
}
#[test]
fn a_reentered_drag_ignores_the_windows_own_pointer_samples() {
use crate::drag_payload::DropOutcome;
let mut ops = RecordingWindowOps::new(true);
let dropped = Rc::new(Cell::new(0_u32));
let d = dropped.clone();
let ended: Rc<RefCell<Vec<DropOutcome>>> = Rc::new(RefCell::new(Vec::new()));
let e = ended.clone();
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let row = tree.add(
drag_arming_leaf(slot.clone(), eager_payload)
.on_drop(move |_, _, _| {
d.set(d.get() + 1);
true
})
.on_drag_ended(move |o, _| e.borrow_mut().push(o)),
);
slot.set(Some(row));
tree.layout(SizeProposal::exact(200.0, 100.0));
let at = Point::new(50.0, 50.0);
tree.dispatch_event_with_ops(
WidgetEvent::pointer_down(at, PointerButton::Primary, Modifiers::NONE),
&mut ops,
);
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(60.0, 50.0)), &mut ops);
assert_eq!(
tree.outbound_drag_source,
Some(row),
"handed to the platform"
);
tree.begin_external_drag(
at,
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree.os_drag_reentered);
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(70.0, 50.0)), &mut ops);
tree.dispatch_event_with_ops(
WidgetEvent::pointer_up(
Point::new(70.0, 50.0),
PointerButton::Primary,
Modifiers::NONE,
),
&mut ops,
);
assert_eq!(dropped.get(), 0, "the window's release is not the drop");
assert!(
tree.os_drag_reentered,
"the re-entered session is still there"
);
tree.end_external_drag(
Point::new(70.0, 50.0),
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert_eq!(dropped.get(), 1);
tree.handle_os_drag_ended(DropOutcome::OsCopy, &mut ops);
assert_eq!(*ended.borrow(), vec![DropOutcome::InApp { accepted: true }]);
tree.assert_no_leaked_pointer_state();
}
#[test]
fn a_reentered_drag_leaves_other_pointers_alone() {
let mut ops = RecordingWindowOps::new(true);
let hovered = Rc::new(Cell::new(0_u32));
let h = hovered.clone();
let mut tree = WidgetTree::new();
let slot: Rc<Cell<Option<WidgetId>>> = Rc::new(Cell::new(None));
let row = tree.add(drag_arming_leaf(slot.clone(), eager_payload).on_hover(
move |entered, _| {
if entered {
h.set(h.get() + 1);
}
},
));
slot.set(Some(row));
tree.layout(SizeProposal::exact(200.0, 100.0));
let at = Point::new(50.0, 50.0);
tree.dispatch_event_with_ops(
WidgetEvent::pointer_down(at, PointerButton::Primary, Modifiers::NONE),
&mut ops,
);
tree.dispatch_event_with_ops(WidgetEvent::pointer_move(Point::new(60.0, 50.0)), &mut ops);
tree.begin_external_drag(
at,
crate::drag_payload::ExternalDropData::default(),
&mut ops,
);
assert!(tree.os_drag_reentered);
let before = hovered.get();
tree.pen_hover(Point::new(70.0, 50.0));
assert!(
hovered.get() > before,
"the pen's hover reaches the widget under it"
);
assert!(tree.os_drag_reentered, "and leaves the session alone");
tree.handle_os_drag_ended(crate::drag_payload::DropOutcome::Cancelled, &mut ops);
}
}