use crate::core::{DragSourceId, DropId, Font, LayoutNode, drag_source_at, drop_at, find_drop};
use crate::geometry::{Point, Rect};
use crate::platform::{DragOffer, DropData};
use crate::render::{Color, Scene};
use crate::style::Theme;
use crate::{App, Pane};
const DRAG_THRESHOLD: f64 = 4.0;
#[derive(Debug, Default)]
pub(crate) struct DndState {
offer: Option<DragOffer>,
pub(crate) over: Option<(DropId, bool, Point)>,
pub(crate) target: Option<DropId>,
pub(crate) highlight: Option<DropId>,
pub(crate) painted: Option<DropId>,
pub(crate) fades: crate::core::DropFades,
pub(crate) queried: Vec<DropId>,
press: Option<(DragSourceId, Point)>,
internal: Option<DropData>,
}
impl DndState {
pub(crate) fn dragging_internally(&self) -> bool {
self.internal.is_some()
}
}
impl<S> Pane<S> {
pub(crate) fn pointer_latched(&self) -> bool {
self.text_selecting.is_some() || self.dragging.is_some() || self.dnd.dragging_internally()
}
}
pub(crate) fn paint_drop_target(tree: &LayoutNode, id: DropId, scene: &mut Scene, theme: &Theme) {
let Some(node) = find_drop(tree, id) else {
return;
};
let fill = theme.palette.hover_overlay;
let ring = theme.palette.focus_ring;
let b = node.bounds;
let r = node.decoration.radius;
if r > 0.0 {
scene.fill_round_rect(b, r, fill);
} else {
scene.fill_rect(b, fill);
}
let w = 2.0f64.min(b.width() / 2.0).min(b.height() / 2.0);
let bars = [
Rect::from_xywh(b.min_x(), b.min_y(), b.width(), w),
Rect::from_xywh(b.min_x(), b.max_y() - w, b.width(), w),
Rect::from_xywh(b.min_x(), b.min_y() + w, w, b.height() - 2.0 * w),
Rect::from_xywh(b.max_x() - w, b.min_y() + w, w, b.height() - 2.0 * w),
];
for bar in bars {
scene.fill_rect(bar, Color { a: 0xff, ..ring });
}
}
impl<S: 'static> Pane<S> {
fn dnd_hover(&mut self, state: &mut S, theme: &Theme, font: Option<&Font>, pos: Point) -> bool {
let Some(offer) = self.dnd.offer else {
return false;
};
self.last_pointer = pos;
self.ensure_tree(state, theme, font);
let over = self
.tree
.as_ref()
.and_then(|t| drop_at(t, pos))
.map(|(id, b)| {
let local = Point::new(pos.x - b.min_x(), pos.y - b.min_y());
(id, self.handlers.drop_accepts(id, offer), local)
});
let target = over.filter(|o| o.1).map(|o| o.0);
if target != self.dnd.target {
if let Some(old) = self.dnd.target {
self.handlers.dispatch_drop_hover(old, false, state);
}
if let Some(new) = target {
self.handlers.dispatch_drop_hover(new, true, state);
}
self.dnd.target = target;
self.dirty = true;
}
let key = |o: Option<(DropId, bool, Point)>| o.map(|(id, ok, _)| (id, ok));
let moved = over != self.dnd.over && over.is_some_and(|o| self.dnd.queried.contains(&o.0));
if key(over) != key(self.dnd.over) || moved {
self.dirty = true;
}
self.dnd.over = over;
target.is_some()
}
pub(crate) fn drag_enter(
&mut self,
state: &mut S,
theme: &Theme,
font: Option<&Font>,
pos: Point,
offer: DragOffer,
) -> bool {
self.dnd.offer = Some(offer);
self.dnd_hover(state, theme, font, pos)
}
pub(crate) fn drag_over(
&mut self,
state: &mut S,
theme: &Theme,
font: Option<&Font>,
pos: Point,
) -> bool {
self.dnd_hover(state, theme, font, pos)
}
pub(crate) fn drag_leave(&mut self, state: &mut S) {
if let Some(old) = self.dnd.target.take() {
self.handlers.dispatch_drop_hover(old, false, state);
}
if self.dnd.over.take().is_some() {
self.dirty = true;
}
self.dnd.offer = None;
}
pub(crate) fn drop_data(
&mut self,
state: &mut S,
theme: &Theme,
font: Option<&Font>,
pos: Point,
data: &DropData,
) -> bool {
self.dnd.offer = Some(data.kind());
self.dnd_hover(state, theme, font, pos);
let target = self.dnd.target;
self.drag_leave(state);
let Some(id) = target else {
return false;
};
self.handlers.dispatch_drop(id, data, pos, state);
self.dirty = true;
true
}
pub(crate) fn source_press(&mut self, pos: Point) {
self.dnd.press = self
.tree
.as_ref()
.and_then(|t| drag_source_at(t, pos))
.map(|id| (id, pos));
}
pub(crate) fn source_move(
&mut self,
state: &mut S,
theme: &Theme,
font: Option<&Font>,
pos: Point,
) -> bool {
if self.dnd.internal.is_some() {
self.dnd_hover(state, theme, font, pos);
return true;
}
let Some((id, origin)) = self.dnd.press else {
return false;
};
if (pos.x - origin.x).hypot(pos.y - origin.y) < DRAG_THRESHOLD {
return false;
}
self.dnd.press = None;
let Some(data) = self.handlers.dispatch_drag_source(id, state) else {
return false;
};
self.pressed = None;
self.dragging = None;
self.dirty = true;
self.dnd.offer = Some(data.kind());
self.dnd.internal = Some(data);
self.dnd_hover(state, theme, font, pos);
true
}
pub(crate) fn source_release(
&mut self,
state: &mut S,
theme: &Theme,
font: Option<&Font>,
pos: Point,
) -> bool {
self.dnd.press = None;
match self.dnd.internal.take() {
Some(data) => {
self.drop_data(state, theme, font, pos, &data);
true
}
None => false,
}
}
pub(crate) fn source_cancel(&mut self, state: &mut S) -> bool {
self.dnd.press = None;
if self.dnd.internal.take().is_some() {
self.drag_leave(state);
true
} else {
false
}
}
}
impl<S: 'static> App<S> {
pub fn drag_enter(&mut self, pos: Point, offer: DragOffer) -> bool {
self.pane_drag_enter(0, pos, offer)
}
pub fn drag_over(&mut self, pos: Point) -> bool {
self.pane_drag_over(0, pos)
}
pub fn drag_leave(&mut self) {
let App { state, panes, .. } = self;
panes[0].drag_leave(state);
}
pub fn drop_data(&mut self, pos: Point, data: &DropData) -> bool {
self.pane_drop(0, pos, data)
}
pub fn drop_target(&self) -> Option<DropId> {
self.panes[0].dnd.target
}
pub fn needs_frame(&self) -> bool {
self.panes[0].dirty || self.panes[0].animating
}
pub(crate) fn pane_drag_enter(&mut self, idx: usize, pos: Point, offer: DragOffer) -> bool {
let App {
state,
theme,
font,
panes,
..
} = self;
panes[idx].drag_enter(state, theme, font.as_ref(), pos, offer)
}
pub(crate) fn pane_drag_over(&mut self, idx: usize, pos: Point) -> bool {
let App {
state,
theme,
font,
panes,
..
} = self;
panes[idx].drag_over(state, theme, font.as_ref(), pos)
}
pub(crate) fn pane_drag_leave(&mut self, idx: usize) {
let App { state, panes, .. } = self;
panes[idx].drag_leave(state);
}
pub(crate) fn pane_drop(&mut self, idx: usize, pos: Point, data: &DropData) -> bool {
let App {
state,
theme,
font,
panes,
..
} = self;
panes[idx].drop_data(state, theme, font.as_ref(), pos, data)
}
pub(crate) fn pane_source_move(&mut self, idx: usize, pos: Point) -> bool {
let App {
state,
theme,
font,
panes,
..
} = self;
panes[idx].source_move(state, theme, font.as_ref(), pos)
}
pub(crate) fn pane_source_release(&mut self, idx: usize, pos: Point) -> bool {
let App {
state,
theme,
font,
panes,
..
} = self;
panes[idx].source_release(state, theme, font.as_ref(), pos)
}
pub(crate) fn pane_source_cancel(&mut self, idx: usize) -> bool {
let App { state, panes, .. } = self;
panes[idx].source_cancel(state)
}
pub fn pointer_press(&mut self, pos: Point) {
self.pane_pointer_down(0, pos);
}
pub fn pointer_drag_to(&mut self, pos: Point) -> bool {
self.pane_pointer_move(0, pos)
}
pub fn pointer_release(&mut self, pos: Point) {
self.pane_pointer_up(0, pos);
}
}
#[cfg(test)]
mod tests {
use crate::App;
use crate::core::{BoxStyle, Cx, DropState, Element};
use crate::geometry::{Point, Size};
use crate::platform::{DragOffer, DropData};
use crate::widgets::column;
use std::cell::Cell;
use std::path::PathBuf;
#[derive(Default)]
struct St {
files: Vec<PathBuf>,
moved: Vec<String>,
hovering: bool,
taps: u32,
}
thread_local! {
static SEEN: Cell<DropState> = Cell::new(DropState::default());
}
fn block() -> Element {
Element::boxed(BoxStyle::default())
.width(200.0)
.height(40.0)
}
fn app() -> App<St> {
App::new(St::default(), |_: &St, cx: &mut Cx<St>| {
let item = block()
.on_tap(cx, |s: &mut St| s.taps += 1)
.drag_source(cx, |_| Some(DropData::Internal("item-1".into())));
let accepts = DragOffer {
files: true,
text: false,
internal: true,
};
let zone = cx.register_drop(accepts, |s: &mut St, data, _| match data {
DropData::Files(f) => s.files.extend(f.iter().cloned()),
DropData::Internal(id) => s.moved.push(id.clone()),
DropData::Text(_) => unreachable!("refused"),
});
cx.set_drop_hover(zone, |s: &mut St, on| s.hovering = on);
SEEN.with(|c| c.set(cx.drop_state(zone)));
column(vec![item, block().drop_target(zone)])
})
.logical_size(Size::new(200.0, 80.0))
}
#[test]
fn os_drag_hovers_accepts_and_drops_files() {
let mut app = app();
let _ = app.render_frame();
assert!(!app.drag_enter(Point::new(10.0, 10.0), DragOffer::FILES));
assert_eq!(app.drop_target(), None);
assert!(app.drag_over(Point::new(30.0, 50.0)));
assert!(app.state().hovering);
assert!(app.needs_frame(), "entering rebuilds");
let _ = app.render_frame();
let seen = SEEN.with(Cell::get);
assert!(seen.over && seen.accepted);
assert_eq!(seen.position, Some(Point::new(30.0, 10.0)));
assert!(app.drag_over(Point::new(35.0, 55.0)));
assert!(app.needs_frame());
let _ = app.render_frame();
assert_eq!(SEEN.with(Cell::get).position, Some(Point::new(35.0, 15.0)));
let dropped = DropData::Files(vec![PathBuf::from("/tmp/a.txt")]);
assert!(app.drop_data(Point::new(35.0, 55.0), &dropped));
assert_eq!(app.state().files, vec![PathBuf::from("/tmp/a.txt")]);
assert!(!app.state().hovering);
assert!(app.needs_frame());
let _ = app.render_frame();
assert!(!SEEN.with(Cell::get).over);
}
#[test]
fn refused_kind_is_over_but_not_accepted() {
let mut app = app();
let _ = app.render_frame();
assert!(!app.drag_enter(Point::new(30.0, 50.0), DragOffer::TEXT));
assert!(!app.state().hovering);
let _ = app.render_frame();
let seen = SEEN.with(Cell::get);
assert!(seen.over && !seen.accepted, "{seen:?}");
assert!(!app.drop_data(Point::new(30.0, 50.0), &DropData::Text("x".into())));
app.drag_leave();
let _ = app.render_frame();
assert!(!SEEN.with(Cell::get).over);
}
#[test]
fn in_app_drag_moves_an_item_without_tapping() {
let mut app = app();
let _ = app.render_frame();
app.pointer_press(Point::new(20.0, 20.0));
app.pointer_drag_to(Point::new(22.0, 21.0));
assert_eq!(app.drop_target(), None);
app.pointer_drag_to(Point::new(20.0, 30.0));
app.pointer_drag_to(Point::new(20.0, 60.0));
assert!(app.drop_target().is_some());
assert!(app.state().hovering);
app.pointer_release(Point::new(20.0, 60.0));
assert_eq!(app.state().moved, vec!["item-1".to_string()]);
assert_eq!(app.state().taps, 0, "a press that drags doesn't tap");
assert!(!app.state().hovering);
app.pointer_press(Point::new(20.0, 20.0));
app.pointer_release(Point::new(20.0, 20.0));
assert_eq!(app.state().taps, 1);
}
#[test]
fn an_unread_target_gets_the_app_highlight() {
let mut app = App::new((), |_: &(), cx: &mut Cx<()>| {
block().height(80.0).on_drop(cx, |_, _, _| {})
})
.logical_size(Size::new(200.0, 80.0));
let (before, _) = app.render_frame();
assert!(app.drag_enter(Point::new(10.0, 10.0), DragOffer::TEXT));
let (during, damage) = app.render_frame();
assert_eq!(damage, crate::core::Damage::Full);
assert_ne!(before.as_bytes(), during.as_bytes(), "highlighted");
app.drag_leave();
let (after, _) = app.render_frame();
assert_eq!(before.as_bytes(), after.as_bytes(), "highlight gone");
}
}