use crate::Vec2;
use crate::color::{Brush, Color};
use crate::geometry::Rect;
use crate::input::{Modifiers, PointerKind};
use crate::runtime::{Frame, HitRegion};
use crate::shortcuts::DragAction;
use crate::text::FontWeight;
use crate::view::{Scene, SceneNode, TextPaintStyle};
use std::cell::RefCell;
use std::{any::Any, path::PathBuf, rc::Rc, sync::Arc};
use web_time::Instant;
pub type DragPayload = Rc<dyn Any>;
pub fn drag_payload<T: 'static>(value: T) -> DragPayload {
Rc::new(value)
}
pub fn downcast_drag_payload<T: 'static>(payload: &DragPayload) -> Option<&T> {
payload.as_ref().downcast_ref::<T>()
}
pub fn drag_and_drop_source<T, F>(mut modifier: crate::Modifier, on_start: F) -> crate::Modifier
where
T: 'static,
F: Fn(DragStart) -> Option<T> + 'static,
{
modifier = modifier.on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
modifier
}
pub fn drag_and_drop_source_with_preview<T, F>(
mut modifier: crate::Modifier,
preview: DragPreview,
on_start: F,
) -> crate::Modifier
where
T: 'static,
F: Fn(DragStart) -> Option<T> + 'static,
{
modifier = modifier
.draw_drag_decoration_rc(preview)
.on_drag_start(move |start| on_start(start).map(drag_payload::<T>));
modifier
}
pub fn drag_and_drop_target<T, F>(mut modifier: crate::Modifier, on_drop: F) -> crate::Modifier
where
T: 'static,
F: Fn(&DropEvent, &T) -> bool + 'static,
{
modifier = modifier.on_drop(move |ev| match downcast_drag_payload::<T>(&ev.payload) {
Some(v) => on_drop(&ev, v),
None => false,
});
modifier
}
pub trait DragDropModifierExt: Sized {
fn drag_source<T>(
self,
make_payload: impl Fn(DragStart) -> Option<T> + 'static,
) -> crate::Modifier
where
T: 'static;
fn on_drag_enter_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static;
fn on_drag_over_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static;
fn on_drag_leave_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static;
fn on_drop_typed<T>(self, f: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
where
T: 'static;
fn drop_target<T>(self, on_drop: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
where
T: 'static;
}
impl DragDropModifierExt for crate::Modifier {
fn drag_source<T>(
self,
make_payload: impl Fn(DragStart) -> Option<T> + 'static,
) -> crate::Modifier
where
T: 'static,
{
self.on_drag_start(move |start| make_payload(start).map(drag_payload::<T>))
}
fn on_drag_enter_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static,
{
self.on_drag_enter(move |ev| {
if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
f(&ev, payload);
}
})
}
fn on_drag_over_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static,
{
self.on_drag_over(move |ev| {
if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
f(&ev, payload);
}
})
}
fn on_drag_leave_typed<T>(self, f: impl Fn(&DragOver, &T) + 'static) -> crate::Modifier
where
T: 'static,
{
self.on_drag_leave(move |ev| {
if let Some(payload) = downcast_drag_payload::<T>(&ev.payload) {
f(&ev, payload);
}
})
}
fn on_drop_typed<T>(self, f: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
where
T: 'static,
{
self.on_drop(move |ev| {
let Some(payload) = downcast_drag_payload::<T>(&ev.payload) else {
return false;
};
f(&ev, payload)
})
}
fn drop_target<T>(self, on_drop: impl Fn(&DropEvent, &T) -> bool + 'static) -> crate::Modifier
where
T: 'static,
{
self.on_drop_typed(on_drop)
}
}
#[derive(Clone, Debug)]
pub struct DragStart {
pub source_id: u64,
pub position: Vec2,
pub modifiers: Modifiers,
}
#[derive(Clone, Debug)]
pub struct DragOver {
pub source_id: u64,
pub target_id: u64,
pub position: Vec2,
pub modifiers: Modifiers,
pub payload: DragPayload,
}
#[derive(Clone, Debug)]
pub struct DropEvent {
pub source_id: u64,
pub target_id: u64,
pub position: Vec2,
pub modifiers: Modifiers,
pub payload: DragPayload,
}
#[derive(Clone, Copy, Debug)]
pub struct DragEnd {
pub accepted: bool,
}
#[derive(Clone)]
pub struct DragPreviewCtx {
pub pointer: Vec2,
pub start_pointer: Vec2,
pub source_rect: Rect,
pub grab_offset: Vec2,
pub payload: DragPayload,
}
pub type DragPreview = Rc<dyn Fn(&mut Scene, &DragPreviewCtx)>;
pub fn provide_drag_preview(preview: DragPreview) {
with_dnd_state(|state| state.pending_preview = Some(preview));
}
fn take_pending_preview() -> Option<DragPreview> {
with_dnd_state(|state| state.pending_preview.take())
}
pub fn drag_preview_label(label: impl Into<String>, accent: Color) -> DragPreview {
let label = label.into();
Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
draw_label_chip(scene, ctx.pointer, &label, accent, false);
})
}
pub fn drag_preview_chip(label: impl Into<String>, accent: Color) -> DragPreview {
let label = label.into();
Rc::new(move |scene: &mut Scene, ctx: &DragPreviewCtx| {
draw_label_chip(scene, ctx.pointer, &label, accent, true);
})
}
fn draw_label_chip(scene: &mut Scene, pointer: Vec2, label: &str, accent: Color, elevated: bool) {
use crate::units::{Px, UnitExt as _};
let pad_x = 10.0.dp().to_px();
let pad_y = 6.0.dp().to_px();
let font_px = 13.0.sp().to_px();
let text_w = Px((label.chars().count() as f32 * font_px.0 * 0.55).max(24.0.dp().to_px().0));
let w = text_w.0 + pad_x.0 * 2.0;
let h = font_px.0 + pad_y.0 * 2.0;
let r = 8.0.dp().to_px();
let origin = Vec2 {
x: pointer.x + 14.0.dp().to_px().0,
y: pointer.y + 14.0.dp().to_px().0,
};
let rect = Rect {
x: origin.x,
y: origin.y,
w,
h,
};
if elevated {
scene.nodes.push(SceneNode::Rect {
rect: Rect {
x: rect.x + 2.0.dp().to_px().0,
y: rect.y + 3.0.dp().to_px().0,
w: rect.w,
h: rect.h,
},
brush: Brush::Solid(Color::from_rgba(0, 0, 0, 50)),
radius: [r; 4],
});
}
let bg = accent.with_alpha(0xDD);
scene.nodes.push(SceneNode::Rect {
rect,
brush: Brush::Solid(bg),
radius: [r; 4],
});
scene.nodes.push(SceneNode::Border {
rect,
brush: Brush::Solid(accent.with_alpha(0xFF)),
width: 1.0.dp().to_px(),
radius: [r; 4],
});
scene.nodes.push(SceneNode::Text {
rect: Rect {
x: rect.x + pad_x.0,
y: rect.y + pad_y.0,
w: text_w.0,
h: font_px.0,
},
text: Arc::<str>::from(label),
color: Color::WHITE,
size: font_px,
style: TextPaintStyle {
font_weight: FontWeight::MEDIUM,
..Default::default()
},
});
}
fn draw_default_source_ghost(scene: &mut Scene, ctx: &DragPreviewCtx, accent: Color) {
use crate::units::UnitExt as _;
let w = ctx.source_rect.w.max(24.0.dp().to_px().0);
let h = ctx.source_rect.h.max(16.0.dp().to_px().0);
let rect = Rect {
x: ctx.pointer.x - ctx.grab_offset.x,
y: ctx.pointer.y - ctx.grab_offset.y,
w,
h,
};
let r = 6.0.dp().to_px();
scene.nodes.push(SceneNode::Rect {
rect,
brush: Brush::Solid(accent.with_alpha(0x55)),
radius: [r; 4],
});
scene.nodes.push(SceneNode::Border {
rect,
brush: Brush::Solid(accent.with_alpha(0xCC)),
width: 1.5.dp().to_px(),
radius: [r; 4],
});
}
#[derive(Clone, Debug)]
pub struct DroppedFile {
pub name: String,
pub path: Option<PathBuf>,
}
#[derive(Clone, Debug)]
pub struct DroppedFiles {
pub files: Vec<DroppedFile>,
}
#[derive(Clone)]
pub struct DragSession {
pub source_id: u64,
pub payload: DragPayload,
pub start_px: (f32, f32),
pub over_id: Option<u64>,
pub source_rect: Rect,
pub grab_offset: Vec2,
pub preview: Option<DragPreview>,
}
impl std::fmt::Debug for DragSession {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("DragSession")
.field("source_id", &self.source_id)
.field("start_px", &self.start_px)
.field("over_id", &self.over_id)
.field("source_rect", &self.source_rect)
.field("grab_offset", &self.grab_offset)
.field("preview", &self.preview.as_ref().map(|_| "…"))
.finish_non_exhaustive()
}
}
#[derive(Clone)]
struct MouseDownState {
position: Vec2,
capture_id: u64,
}
#[derive(Clone)]
struct TouchDownState {
time: Instant,
position: Vec2,
capture_id: u64,
long_press_pending: bool,
}
const LONG_PRESS_MS: u128 = 400;
#[derive(Default)]
struct DndStorage {
frame: Option<Rc<Frame>>,
scale: f32,
session: Option<DragSession>,
mouse_down: Option<MouseDownState>,
touch_down: Option<TouchDownState>,
pending_preview: Option<DragPreview>,
}
#[derive(Clone)]
pub struct DndContext {
storage: Rc<RefCell<DndStorage>>,
}
impl Default for DndContext {
fn default() -> Self {
Self {
storage: Rc::new(RefCell::new(DndStorage {
scale: 1.0,
..DndStorage::default()
})),
}
}
}
pub struct DndContextGuard {
previous: Option<DndContext>,
}
impl DndContext {
pub fn enter(&self) -> DndContextGuard {
let previous = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow_mut().replace(self.clone()));
DndContextGuard { previous }
}
}
impl Drop for DndContextGuard {
fn drop(&mut self) {
ACTIVE_DND_CONTEXT.with(|slot| {
*slot.borrow_mut() = self.previous.take();
});
}
}
thread_local! {
static GLOBAL_DND: RefCell<DndStorage> = RefCell::new(DndStorage {
scale: 1.0,
..DndStorage::default()
});
static ACTIVE_DND_CONTEXT: RefCell<Option<DndContext>> = const { RefCell::new(None) };
}
fn with_dnd_state<R>(f: impl FnOnce(&mut DndStorage) -> R) -> R {
if let Some(context) = ACTIVE_DND_CONTEXT.with(|slot| slot.borrow().clone()) {
let mut storage = context.storage.borrow_mut();
f(&mut storage)
} else {
GLOBAL_DND.with(|storage| f(&mut storage.borrow_mut()))
}
}
fn dnd_frame() -> Option<Rc<Frame>> {
with_dnd_state(|state| state.frame.clone())
}
fn dnd_scale() -> f32 {
with_dnd_state(|state| state.scale)
}
fn mouse_down() -> Option<MouseDownState> {
with_dnd_state(|state| state.mouse_down.clone())
}
fn set_mouse_down(state: Option<MouseDownState>) {
with_dnd_state(|storage| storage.mouse_down = state);
}
fn touch_down() -> Option<TouchDownState> {
with_dnd_state(|state| state.touch_down.clone())
}
fn set_touch_down(state: Option<TouchDownState>) {
with_dnd_state(|storage| storage.touch_down = state);
}
fn update_touch_down(f: impl FnOnce(&mut TouchDownState)) {
with_dnd_state(|state| {
if let Some(touch) = state.touch_down.as_mut() {
f(touch);
}
});
}
fn with_session_mut<R>(f: impl FnOnce(&mut Option<DragSession>) -> R) -> R {
with_dnd_state(|state| f(&mut state.session))
}
fn take_session() -> Option<DragSession> {
with_dnd_state(|state| state.session.take())
}
fn set_session(session: DragSession) {
with_dnd_state(|state| state.session = Some(session));
}
pub fn set_dnd_frame(frame: Option<Frame>) {
set_shared_dnd_frame(frame.map(Rc::new));
}
pub fn set_shared_dnd_frame(frame: Option<Rc<Frame>>) {
with_dnd_state(|state| state.frame = frame);
}
pub fn set_dnd_scale(scale: f32) {
with_dnd_state(|state| state.scale = scale);
}
pub fn is_dragging() -> bool {
with_dnd_state(|state| state.session.is_some())
}
pub fn current_drag_session() -> Option<DragSession> {
with_dnd_state(|state| state.session.clone())
}
fn touch_slop_px(scale: f32) -> f32 {
6.0 * scale
}
fn hit_index_by_id(frame: &Frame, id: u64) -> Option<usize> {
frame.hit_regions.iter().position(|h| h.id == id)
}
fn is_dnd_target(hit: &HitRegion) -> bool {
hit.on_drop.is_some()
|| hit.on_drag_enter.is_some()
|| hit.on_drag_over.is_some()
|| hit.on_drag_leave.is_some()
}
pub fn dnd_target_id_at(frame: &Frame, pos: Vec2) -> Option<u64> {
frame
.hit_regions
.iter()
.rev()
.filter(|h| h.rect.contains(pos))
.find(|h| is_dnd_target(h))
.map(|h| h.id)
}
fn dnd_update_over(frame: &Frame, session: &mut DragSession, modifiers: Modifiers, pos: Vec2) {
let new_over = dnd_target_id_at(frame, pos);
if new_over != session.over_id {
if let Some(prev) = session.over_id
&& let Some(i) = hit_index_by_id(frame, prev)
&& let Some(cb) = &frame.hit_regions[i].on_drag_leave
{
cb(DragOver {
source_id: session.source_id,
target_id: prev,
position: pos,
modifiers,
payload: session.payload.clone(),
});
}
if let Some(now) = new_over
&& let Some(i) = hit_index_by_id(frame, now)
&& let Some(cb) = &frame.hit_regions[i].on_drag_enter
{
cb(DragOver {
source_id: session.source_id,
target_id: now,
position: pos,
modifiers,
payload: session.payload.clone(),
});
}
session.over_id = new_over;
}
if let Some(over) = session.over_id
&& let Some(i) = hit_index_by_id(frame, over)
&& let Some(cb) = &frame.hit_regions[i].on_drag_over
{
cb(DragOver {
source_id: session.source_id,
target_id: over,
position: pos,
modifiers,
payload: session.payload.clone(),
});
}
}
fn dnd_finish(
frame: &Frame,
session: DragSession,
modifiers: Modifiers,
pos: Vec2,
accept_if_possible: bool,
) -> bool {
let mut accepted = false;
if accept_if_possible {
let drop_target = dnd_target_id_at(frame, pos);
if let Some(tid) = drop_target
&& let Some(i) = hit_index_by_id(frame, tid)
&& let Some(cb) = &frame.hit_regions[i].on_drop
{
accepted = cb(DropEvent {
source_id: session.source_id,
target_id: tid,
position: pos,
modifiers,
payload: session.payload.clone(),
});
}
}
if let Some(i) = hit_index_by_id(frame, session.source_id)
&& let Some(cb) = &frame.hit_regions[i].on_drag_end
{
cb(DragEnd { accepted });
}
accepted
}
fn initiate_drag(
frame: &Frame,
capture_id: u64,
start_pos: Vec2,
current_pos: Vec2,
modifiers: Modifiers,
) -> bool {
let Some(i) = hit_index_by_id(frame, capture_id) else {
return false;
};
let Some(cb) = &frame.hit_regions[i].on_drag_start else {
return false;
};
let _ = take_pending_preview();
let payload = cb(DragStart {
source_id: capture_id,
position: current_pos,
modifiers,
});
let Some(payload) = payload else {
let _ = take_pending_preview();
return false;
};
let preview = take_pending_preview().or_else(|| frame.hit_regions[i].drag_preview.clone());
let source_rect = frame.hit_regions[i].rect;
let grab_offset = Vec2 {
x: start_pos.x - source_rect.x,
y: start_pos.y - source_rect.y,
};
set_session(DragSession {
source_id: capture_id,
payload,
start_px: (start_pos.x, start_pos.y),
over_id: None,
source_rect,
grab_offset,
preview,
});
true
}
pub fn handle_drag_action(action: &DragAction) -> bool {
let scale = dnd_scale();
let slop = touch_slop_px(scale);
match *action {
DragAction::Press {
position,
capture_id,
kind,
..
} => {
match kind {
PointerKind::Mouse => {
set_mouse_down(Some(MouseDownState {
position,
capture_id,
}));
}
_ => {
set_touch_down(Some(TouchDownState {
time: web_time::Instant::now(),
position,
capture_id,
long_press_pending: true,
}));
}
}
false
}
DragAction::Move {
position,
modifiers,
} => {
if is_dragging() {
if let Some(frame) = dnd_frame() {
with_session_mut(|session| {
if let Some(session) = session.as_mut() {
dnd_update_over(&frame, session, modifiers, position);
}
});
}
return true;
}
if let Some(down) = mouse_down() {
let dx = position.x - down.position.x;
let dy = position.y - down.position.y;
let dist = (dx * dx + dy * dy).sqrt();
if dist >= slop {
if let Some(frame) = dnd_frame()
&& initiate_drag(
&frame,
down.capture_id,
down.position,
position,
modifiers,
)
{
with_session_mut(|session| {
if let Some(session) = session.as_mut() {
dnd_update_over(&frame, session, modifiers, position);
}
});
set_mouse_down(None);
return true;
}
set_mouse_down(None);
}
return true; }
if let Some(touch) = touch_down() {
if touch.long_press_pending {
let elapsed_ms = (Instant::now() - touch.time).as_millis();
let dx = position.x - touch.position.x;
let dy = position.y - touch.position.y;
let dist = (dx * dx + dy * dy).sqrt();
if elapsed_ms >= LONG_PRESS_MS
&& dist <= slop
&& let Some(frame) = dnd_frame()
{
if initiate_drag(
&frame,
touch.capture_id,
touch.position,
position,
modifiers,
) {
with_session_mut(|session| {
if let Some(session) = session.as_mut() {
dnd_update_over(&frame, session, modifiers, position);
}
});
set_touch_down(None);
return true;
}
update_touch_down(|touch| touch.long_press_pending = false);
}
if dist > slop {
update_touch_down(|touch| touch.long_press_pending = false);
}
}
let still_pending = touch_down().is_some_and(|touch| touch.long_press_pending);
if still_pending {
return true;
}
}
false
}
DragAction::Release {
position,
modifiers,
} => {
let mut consumed = false;
if let Some(session) = take_session() {
if let Some(frame) = dnd_frame() {
dnd_finish(&frame, session, modifiers, position, true);
}
consumed = true;
}
set_mouse_down(None);
set_touch_down(None);
consumed
}
DragAction::Cancel => {
let mut consumed = false;
if let Some(session) = take_session() {
if let Some(frame) = dnd_frame() {
dnd_finish(
&frame,
session,
Modifiers::default(),
Vec2::default(),
false,
);
}
consumed = true;
}
set_mouse_down(None);
set_touch_down(None);
consumed
}
}
}
pub fn overlay_drag_indicator(
scene: &mut Scene,
mouse_pos_px: (f32, f32),
external_file_drag: bool,
) {
if !is_dragging() && !external_file_drag {
return;
}
let pos = Vec2 {
x: mouse_pos_px.0,
y: mouse_pos_px.1,
};
let frame = dnd_frame();
let Some(ref f) = frame else {
return;
};
let accent = if external_file_drag {
Color::from_hex("#FFAA00")
} else {
Color::from_hex("#44AAFF")
};
if let Some(tid) = dnd_target_id_at(f, pos)
&& let Some(hit) = f.hit_regions.iter().find(|h| h.id == tid)
{
use crate::units::UnitExt as _;
let r = 8.0.dp().to_px();
scene.nodes.push(SceneNode::Border {
rect: hit.rect,
brush: Brush::Solid(accent),
width: 2.0.dp().to_px(),
radius: [r; 4],
});
}
if external_file_drag {
draw_label_chip(scene, pos, "Drop files", accent, true);
return;
}
let Some(session) = current_drag_session() else {
return;
};
let ctx = DragPreviewCtx {
pointer: pos,
start_pointer: Vec2 {
x: session.start_px.0,
y: session.start_px.1,
},
source_rect: session.source_rect,
grab_offset: session.grab_offset,
payload: session.payload.clone(),
};
if let Some(preview) = session.preview {
preview(scene, &ctx);
} else {
draw_default_source_ghost(scene, &ctx, accent);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::PointerKind;
use crate::runtime::HitRegion;
#[test]
fn contexts_isolate_drag_sessions() {
let frame = Frame {
scene: Scene::default(),
hit_regions: vec![HitRegion {
id: 1,
rect: Rect {
x: 0.0,
y: 0.0,
w: 100.0,
h: 100.0,
},
on_drag_start: Some(Rc::new(|_| Some(drag_payload(7u32)))),
..Default::default()
}],
semantics_nodes: Vec::new(),
focus_chain: Vec::new(),
};
let a = DndContext::default();
let b = DndContext::default();
{
let _guard = a.enter();
set_dnd_frame(Some(frame.clone()));
handle_drag_action(&DragAction::Press {
position: Vec2 { x: 10.0, y: 10.0 },
capture_id: 1,
kind: PointerKind::Mouse,
modifiers: Modifiers::default(),
});
handle_drag_action(&DragAction::Move {
position: Vec2 { x: 40.0, y: 40.0 },
modifiers: Modifiers::default(),
});
assert!(is_dragging());
}
{
let _guard = b.enter();
assert!(!is_dragging());
set_dnd_frame(Some(frame.clone()));
}
{
let _guard = a.enter();
assert!(is_dragging());
handle_drag_action(&DragAction::Cancel);
assert!(!is_dragging());
}
}
}