use super::*;
struct OutboundStash {
live: bool,
payload: Option<crate::drag_payload::DragPayload>,
}
thread_local! {
static OUTBOUND: std::cell::RefCell<OutboundStash> =
const { std::cell::RefCell::new(OutboundStash { live: false, payload: None }) };
}
fn outbound_begin(payload: crate::drag_payload::DragPayload) {
OUTBOUND.with(|s| {
let mut s = s.borrow_mut();
s.live = true;
s.payload = Some(payload);
});
}
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_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;
});
}
impl WidgetTree {
pub(super) fn cleanup_drag_preview(&mut self) {
if let Some(ref drag) = self.active_drag {
if let Some(overlay_id) = drag.preview_overlay_id {
self.overlay_manager.dismiss(overlay_id);
}
if let Some(content_id) = drag.preview_content_id {
self.arena.destroy(content_id);
}
}
}
pub(super) fn cancel_active_drag(&mut self, ops: &mut dyn crate::window::WindowOps) {
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);
self.cleanup_drag_preview();
self.active_drag = None;
self.pointer_captured_by = None;
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);
}
}
pub fn begin_external_drag(
&mut self,
position: teksilo_canvas::Point,
data: crate::drag_payload::ExternalDropData,
ops: &mut dyn crate::window::WindowOps,
) {
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();
self.active_drag = Some(crate::drag_state::DragSession {
payload,
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.handle_drag_move(position, &mut *ops);
return;
}
self.active_drag = Some(crate::drag_state::DragSession {
payload: crate::drag_payload::DragPayload::external(data),
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.handle_drag_move(position, &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.handle_drag_move(position, &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;
self.handle_drag_drop(position, &mut *ops);
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.handle_drag_drop(position, &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) {
let Some((target_id, position)) = self
.active_drag
.as_ref()
.and_then(|d| d.current_target.map(|t| (t, d.current_position)))
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 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);
}
}
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,
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;
}
let data = 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()
}
_ => return false,
};
if !self.is_outside_window(position) {
return false;
}
if !ops.begin_os_drag(data, None) {
return false;
}
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 (matched above)");
self.pointer_captured_by = None;
self.current_cursor = crate::widget::CursorIcon::Default;
self.outbound_drag_source = drag.source_widget;
outbound_begin(drag.payload);
if let Some(prev) = prev_target {
self.fire_on_drag_leave(prev, &mut *ops);
}
true
}
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();
if let Some(drag) = self.active_drag.take() {
outbound_restash(drag.payload);
}
self.os_drag_reentered = false;
self.pointer_captured_by = None;
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,
) {
outbound_end();
self.os_drag_reentered = false;
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;
}
pub(super) fn handle_drag_move(
&mut self,
position: teksilo_canvas::Point,
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, &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 {
self.overlay_manager.update_placement(
overlay_id,
crate::overlay::OverlayPlacement::AtPointer(position),
);
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 target =
self.hit_test_excluding_overlay_and_widget(position, 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);
}
if let Some(ref mut drag) = self.active_drag {
drag.current_target = new_target;
drag.feedback = new_feedback;
}
}
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,
) {
self.cleanup_drag_preview();
if self.active_drag.is_none() {
return;
}
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 hit = self.hit_test(position);
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 = match self.active_drag.take() {
Some(d) => d,
None => return,
};
self.pointer_captured_by = None;
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);
}
}
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::PointerMove {
position: 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::PointerUp {
position: Point::new(150.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerUp {
position: Point::new(999.0, 999.0),
button: PointerButton::Primary,
modifiers: 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::PointerMove {
position: 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::PointerUp {
position: Point::new(100.0, 50.0),
button: PointerButton::Primary,
modifiers: 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"
);
}
#[test]
fn drag_arming_walks_to_an_ancestor_without_a_dead_zone() {
let mut tree = WidgetTree::new();
let button = tree.add(FillWidget::new().on_tap(|_e, _ctx| {}));
let inner = tree.add(StackWidget::new().add_child(button));
let ancestor = tree.add(
StackWidget::new()
.add_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!(
tree.drag_observers,
vec![ancestor],
"the draggable ancestor is armed 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().add_child(button).gesture_dead_zone(true));
let _ancestor = tree.add(
StackWidget::new()
.add_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!(
tree.drag_observers.is_empty(),
"a dead zone blocks the draggable ancestor from being armed"
);
tree.pointer_up_button(
Point::new(b.x + b.width / 2.0, b.y + b.height / 2.0),
PointerButton::Primary,
);
}
#[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::PointerMove {
position: 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().add_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::PointerMove {
position: 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::PointerMove {
position: 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::PointerUp {
position: Point::new(-100.0, -100.0),
button: PointerButton::Primary,
modifiers: 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::PointerUp {
position: Point::new(150.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerUp {
position: Point::new(150.0, 50.0),
button: PointerButton::Primary,
modifiers: 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().add_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::PointerUp {
position: Point::new(100.0, 50.0),
button: PointerButton::Primary,
modifiers: 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().add_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::PointerMove {
position: Point::new(100.0, 50.0),
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(100.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerUp {
position: Point::new(999.0, 999.0),
button: PointerButton::Primary,
modifiers: 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_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::PointerMove {
position: 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::PointerMove {
position: 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::PointerMove {
position: Point::new(150.0, 50.0),
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(150.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerMove {
position: Point::new(100.0, 50.0),
});
assert_eq!(leave.get(), 0, "no leave yet — target just became active");
tree.dispatch_event(WidgetEvent::PointerMove {
position: 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::PointerMove {
position: Point::new(100.0, 50.0),
});
tree.dispatch_event(WidgetEvent::PointerMove {
position: Point::new(100.0, 50.0),
});
assert_eq!(
leave.get(),
1,
"leave fires at most once per leave transition"
);
tree.dispatch_event(WidgetEvent::PointerMove {
position: 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::PointerMove {
position: Point::new(100.0, 50.0),
});
tree.dispatch_event(WidgetEvent::PointerUp {
position: Point::new(100.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerMove {
position: 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::PointerMove {
position: 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::PointerMove {
position: 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::PointerUp {
position: Point::new(100.0, 50.0),
button: PointerButton::Primary,
modifiers: 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::PointerMove {
position: 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::PointerUp {
position: Point::new(110.0, 55.0),
button: PointerButton::Primary,
modifiers: 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::PointerUp {
position: Point::new(50.0, 25.0),
button: PointerButton::Primary,
modifiers: 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::PointerMove {
position: 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::PointerMove {
position: Point::new(100.0, 50.0),
});
tree.dispatch_event(WidgetEvent::Scroll {
delta: crate::event::ScrollDelta::Pixels { x: 0.0, y: 40.0 },
modifiers: 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>>>,
succeed: bool,
cancels: std::rc::Rc<std::cell::Cell<usize>>,
}
impl RecordingWindowOps {
fn new(succeed: bool) -> Self {
Self {
started: std::rc::Rc::new(std::cell::RefCell::new(Vec::new())),
succeed,
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
}
}
}
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>,
) -> bool {
self.started.borrow_mut().push(data);
self.succeed
}
fn cancel_os_drag(&mut self) {
self.cancels.set(self.cancels.get() + 1);
}
}
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(),
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(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(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,
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
};
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(),
succeed: false,
cancels: std::rc::Rc::new(std::cell::Cell::new(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(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(),
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(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(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::PointerUp {
position: Point::new(150.0, 50.0),
button: PointerButton::Primary,
modifiers: 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,
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
};
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::OsCopy));
}
#[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,
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
};
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(),
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(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(
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,
succeed: true,
cancels: std::rc::Rc::new(std::cell::Cell::new(0)),
};
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")]);
}
#[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()));
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());
}
}