ui/dnd.rs
1// dnd -- drag and drop: widgets as drag sources and drop targets, the hover of a drag over a
2// window, drops of other programs onto widgets, dragging out of the app
3//
4// Sources (the app's own drags):
5// drag_source(id, ev) -> DS_* after the widget's behave (ev = its EV_*): DS_START when it
6// starts being dragged (EV_DRAG; hand the content to drag_set
7// in the same pass, else nothing is dragged), DS_ACTIVE while
8// it is dragged, DS_END once when the drag ended
9// drag_set(data) what is dragged (DragData: text, files, URLs, HTML, a PNG,
10// the app's own kind DT_USER0 << k with bytes, a label shown
11// next to the pointer, the effects the source allows)
12// drag_out(data) -> started (left button held) hand data to the platform at once
13// drag_effect() -> DE_* how the app's last drag ended (DE_COPY / DE_MOVE /
14// DE_LINK; 0 = cancelled or refused); UIC_DRAG_END says it too
15// dragging() -> bool one of the app's drags runs
16// Targets:
17// drop_target(id, types) -> DS_* the widget at last_rect takes drags of these DT_* kinds:
18// DS_HOVER while one is over it (the style highlights it),
19// DS_DROP in the pass of the drop (drop_data(), drop_effect():
20// one of the target's effects that the source allows; none in
21// common: refused, no DS_DROP)
22// drop_effect_set(effects) the DE_* effects the target of the last drop_target call
23// takes in this pass (default DE_ALL): the platform answers a
24// drag over it with one of them (the system's pointer shows
25// copy, move or link), 0 refuses the drag there (no drop, no
26// highlight); a drag of the app picks from them too
27// drop_accept(types) what the windows take outside every drop target (default
28// DT_FILES); such drops come as UIC_DROP (drop.rs, as before)
29// drag_over() -> (types, x, y) the drag over the live window: its kinds and position (DIP);
30// (0, 0, 0) when none; drag_over_px() the position in px of
31// the window, exact (as drop_pos_px, drop.rs)
32// drag_allowed() -> DE_* the effects its source allows (DE_ALL where the platform does
33// not say); after a drop its source's, from E_DROP to the end
34// of its passes and while drop_defer waits; 0 when none
35// drop_defer() -> bool (in the pass of a drop: DS_DROP or UIC_DROP) the drop's answer
36// to its source waits for drop_finish (at most DROP_DEFER_MS);
37// false: the platform answered at the drop already
38// drop_finish(effect) -> bool the answer: the DE_* effect it was taken with, 0 = not taken
39//
40// An internal drag stays in the app while the pointer stays in its window: the drop target under
41// the pointer is highlighted, a label follows the pointer, Esc cancels, a release over a matching
42// target drops there (else nothing happens). The target is the innermost drop target at the
43// point (the smallest of the topmost layer); targets under a popup or a modal layer do not count.
44// When the pointer leaves the window with content other programs know (DT_STD), the platform
45// takes over (plat::drag_start: DoDragDrop, Xdnd, wl_data_device.start_drag); dropped back onto
46// the app the drop carries the app's own content again (its kind and bytes). Headless runs keep
47// what would have left the app for the scripts (`dragout expect`, `dragout end`).
48// Other programs' drags: E_DRAG (hover: the targets highlight), E_DROP (onto the drop target at
49// the point, else UIC_DROP when drop_accept takes it). The platform asks the published targets
50// synchronously (uiw_drag_pw, win.rs) to answer the source with an effect: the effects the target
51// under the point took in the last pass (drop_effect_set), DE_ALL for drop_accept's window --
52// IDropTarget::DragOver's pdwEffect, XdndStatus' action, wl_data_offer.set_actions, macOS'
53// NSDragOperation (plat::drag_query). The answer follows the app's pass: an effect set for the
54// drag's point in one pass answers the moves until the next pass. The effects the source allows
55// come with E_DRAG (forgotten when it leaves or the drag ends) and with the drop (plat::
56// drop_allowed; without them those of the hover before it, else every effect): the drop's effect
57// is one the target takes that the source allows -- the platform's, else the first of copy,
58// move, link -- and a target that takes none of them refuses the drop as the platform refuses
59// such a drag over it.
60// The answer of a drop to its source (the effect, or that it was not taken) goes after the
61// passes of E_DROP: the effect it was delivered with -- unless the app called drop_defer in them
62// and answers later with drop_finish (a web page decides asynchronously whether it took a drop).
63// Where the platform allows a later answer (plat::drop_hold: X11, Wayland, Windows with a source
64// in async mode) it then reaches the source; elsewhere drop_defer says false and the source got
65// the effect at the drop. A deferred drop without drop_finish is answered after DROP_DEFER_MS as
66// it was delivered (a UIWARN); so is one a newer drop overtakes. Headless runs keep the answer
67// for the scripts (`drop expect`).
68// Positions stay logical in mirrored (RTL) windows: E_DRAG / E_DROP points are mirrored on the
69// way in like every pointer event; the label next to the pointer is drawn through the mirror.
70
71use alloc::string::String;
72
73use super::consts::*;
74use super::keys::*;
75use super::{Sp, Ui};
76use plat::{DE_ALL, DE_COPY, DE_MOVE, DT_FILES, DT_STD, DragData, Event};
77
78const UIT_DROP: u32 = 0x7FFE_0060; // timer: a deferred drop's time is up
79
80const PH_NONE: u8 = 0;
81const PH_START: u8 = 1; // drag_source said DS_START, drag_set not called yet
82const PH_IN: u8 = 2; // inside the source window
83const PH_OUT: u8 = 3; // the platform carries it (or, headless, it left the window)
84const LABEL_MAX: usize = 48; // characters of the label next to the pointer
85
86/// a drop target of a pass (clipped, physical px of the window; x1 / y1 exclusive)
87#[derive(Clone, Copy, Default)]
88pub(crate) struct Zone {
89 pub(crate) id: u32,
90 pub(crate) types: u32,
91 /// the DE_* effects it takes (drop_effect_set; DE_ALL by default, 0 refuses)
92 pub(crate) effects: u32,
93 pub(crate) layer: u8,
94 pub(crate) x0: i32,
95 pub(crate) y0: i32,
96 pub(crate) x1: i32,
97 pub(crate) y1: i32,
98}
99
100/// the drop target at (x, y) that takes one of `types`: of the topmost layer that has a hit
101/// box or a target there (never below `modal`), the smallest one
102pub(crate) fn zone_find(zones: &[Zone], hits: &[super::ctx::Hit], modal: u32, x: i32, y: i32, types: u32) -> Option<Zone> {
103 if types == 0 || (x == -1 && y == -1) {
104 return None;
105 }
106 let inside = |x0: i32, y0: i32, x1: i32, y1: i32| x >= x0 && x < x1 && y >= y0 && y < y1;
107 // a popup over the target hides it
108 let top = hits.iter().filter(|h| inside(h.x0, h.y0, h.x1, h.y1)).map(|h| h.layer as u32).max().unwrap_or(0).max(modal);
109 let area = |z: &Zone| (z.x1 - z.x0) as i64 * (z.y1 - z.y0) as i64;
110 let mut best: Option<Zone> = None;
111 for z in zones.iter().filter(|z| (z.layer as u32) >= top && z.types & types != 0 && inside(z.x0, z.y0, z.x1, z.y1)) {
112 let better = match &best {
113 None => true,
114 Some(b) => z.layer > b.layer || (z.layer == b.layer && area(z) <= area(b)),
115 };
116 if better {
117 best = Some(*z);
118 }
119 }
120 best
121}
122
123/// what the platform's hook answers at (x, y) logical px of a window (its targets, hit boxes and
124/// modal layer of the last pass) for a drag offering `offered` (DT_STD bits) -> (accepted DT_*,
125/// DE_* effects): the drop target there with its effects, else `accept` with every effect. `own`:
126/// the kinds of the app's own drag while the platform carries it
127#[allow(clippy::too_many_arguments)]
128pub(crate) fn drag_answer(zones: &[Zone], hits: &[super::ctx::Hit], modal: u32, accept: u32, own: u32, x: i32, y: i32, offered: u32) -> (u32, u32) {
129 let offered = offered & DT_STD;
130 let all = offered | own; // the app's own drag: its own kinds too
131 let (t, fx) = match zone_find(zones, hits, modal, x, y, all) {
132 Some(z) => (z.types & all, z.effects & DE_ALL),
133 None => (accept & all, DE_ALL),
134 };
135 // the platform knows the standard kinds: a target of the app's own kind takes them all
136 let t = if t != 0 && t & offered == 0 { offered } else { t & offered };
137 if t == 0 || fx == 0 { (0, 0) } else { (t, fx) }
138}
139
140/// a drop whose answer to its source has not gone yet
141#[derive(Clone, Copy)]
142struct Wait {
143 /// plat::drop_waiting's serial (headless: one of our own)
144 serial: u32,
145 /// the effect it was delivered with (0: refused): the answer without drop_finish
146 fallback: u32,
147 /// the effects its source allows (files never moved)
148 allowed: u32,
149 /// drop_defer was called: when (ms)
150 deferred: Option<u32>,
151}
152
153/// the drag and drop state shared by the windows
154pub(crate) struct S {
155 phase: u8,
156 src: u32,
157 src_win: u32,
158 /// what the app drags
159 data: DragData,
160 /// DS_END for `end_src` in the next pass
161 end_src: u32,
162 end_win: u32,
163 end_live: bool,
164 last_effect: u32,
165 /// another program's drag over window ext_win (logical px)
166 ext: bool,
167 ext_win: u32,
168 ext_x: i32,
169 ext_y: i32,
170 ext_types: u32,
171 /// the DE_* effects its source allows (E_DRAG; 0: not said); forgotten when it leaves, is
172 /// dropped or the app's own drag ends
173 ext_allowed: u32,
174 /// the effects the source of the last drop allows (0: nobody said) and that its passes run
175 /// (E_DROP .. dnd_drop_settled): drag_allowed then, and while its deferred answer waits
176 drop_allowed: u32,
177 drop_now: bool,
178 /// the zone of the last drop_target call (window, index in its pass's list, id), for
179 /// drop_effect_set
180 last_zone: Option<(u32, usize, u32)>,
181 /// the drop target under the drag (this pass) and its highlight (window, layer, rect),
182 /// drawn at the end of the window's pass over the target's content
183 over: u32,
184 over_win: u32,
185 hl: Option<(u32, u8, [i32; 4])>,
186 /// DS_DROP for drop_id of drop_win in the next pass
187 drop_id: u32,
188 drop_win: u32,
189 drop_live: bool,
190 pub(crate) drop_effect: u32,
191 /// the content of the last drop
192 pub(crate) dropped: DragData,
193 /// what a window takes outside the drop targets
194 pub(crate) accept: u32,
195 /// headless: what a drag out handed over (scripts)
196 pub(crate) out: Option<DragData>,
197 /// the last drop's answer to its source while it has not gone; what it was once it went
198 /// (None: none yet) -- `drop expect`
199 wait: Option<Wait>,
200 pub(crate) answered: Option<u32>,
201 /// headless: the next drop is answered at its drop, as on a platform without a later answer
202 /// (script `drop ... sync`); the serials of the headless drops
203 pub(crate) sync_next: bool,
204 serial: u32,
205}
206
207impl Default for S {
208 fn default() -> S {
209 S {
210 phase: PH_NONE,
211 src: 0,
212 src_win: 0,
213 data: DragData::default(),
214 end_src: 0,
215 end_win: 0,
216 end_live: false,
217 last_effect: 0,
218 ext: false,
219 ext_win: 0,
220 ext_x: 0,
221 ext_y: 0,
222 ext_types: 0,
223 ext_allowed: 0,
224 drop_allowed: 0,
225 drop_now: false,
226 last_zone: None,
227 over: 0,
228 over_win: 0,
229 hl: None,
230 drop_id: 0,
231 drop_win: 0,
232 drop_live: false,
233 drop_effect: 0,
234 dropped: DragData::default(),
235 accept: DT_FILES,
236 out: None,
237 wait: None,
238 answered: None,
239 sync_next: false,
240 serial: 0,
241 }
242 }
243}
244
245// the label next to the pointer: the app's, else from the content
246fn label_of(d: &DragData) -> String {
247 let cut = |s: &str| -> String {
248 let line = s.lines().find(|l| !l.trim().is_empty()).unwrap_or("").trim();
249 let mut out: String = line.chars().take(LABEL_MAX).collect();
250 if line.chars().count() > LABEL_MAX {
251 out.push('…');
252 }
253 out
254 };
255 if !d.label.is_empty() {
256 return cut(&d.label);
257 }
258 if let Some(t) = &d.text {
259 return cut(t);
260 }
261 if let Some(f) = d.files.first() {
262 let name = f.rsplit(['/', '\\']).find(|s| !s.is_empty()).unwrap_or(f);
263 let mut s = cut(name);
264 if d.files.len() > 1 {
265 s.push_str(&alloc::format!(" +{}", d.files.len() - 1));
266 }
267 return s;
268 }
269 if let Some(u) = d.urls.first() {
270 return cut(u);
271 }
272 String::new()
273}
274
275impl Ui {
276 // ------------------------------------------------------------------ sources
277
278 /// drag_source(id, ev) -> DS_START / DS_ACTIVE / DS_END / 0 for the widget `id` whose
279 /// behave returned ev: DS_START when a drag of it begins (call drag_set in this pass),
280 /// DS_ACTIVE while its content is dragged, DS_END once afterwards (drag_effect())
281 pub fn drag_source(&mut self, id: u32, ev: u32) -> u32 {
282 let cur = self.uiw_cur;
283 let d = &self.dnd;
284 if id == 0 {
285 return 0;
286 }
287 if d.end_live && d.end_src == id && d.end_win == cur {
288 return DS_END;
289 }
290 if d.phase != PH_NONE && d.src == id && d.src_win == cur {
291 return if d.phase == PH_START { DS_START } else { DS_ACTIVE };
292 }
293 if d.phase == PH_NONE && ev & EV_DRAG != 0 && self.wp.ctx.active == id && self.wp.ctx.mbtn & (1 << BTN_LEFT) != 0 {
294 let d = &mut self.dnd;
295 d.phase = PH_START;
296 d.src = id;
297 d.src_win = cur;
298 d.data = DragData::default();
299 self.wp.ctx.active = 0; // the drag owns the pointer: the release is no click
300 return DS_START;
301 }
302 0
303 }
304
305 /// drag_set(data): the content of the drag that starts (after DS_START, same pass)
306 pub fn drag_set(&mut self, data: DragData) {
307 let d = &mut self.dnd;
308 if d.phase == PH_START && d.src_win == self.uiw_cur && !data.is_empty() {
309 d.data = data;
310 d.phase = PH_IN;
311 self.repass = true; // the targets see it in this frame
312 }
313 }
314
315 /// drag_out(data) -> started: (the left button held) the platform drags data out of the
316 /// app right away (DoDragDrop, Xdnd, wl_data_device.start_drag); UIC_DRAG_END at the end
317 pub fn drag_out(&mut self, data: DragData) -> bool {
318 if self.dnd.phase != PH_NONE || data.types() & DT_STD == 0 {
319 return false;
320 }
321 self.dnd.src = 0;
322 self.dnd.src_win = self.uiw_cur;
323 self.dnd.data = data;
324 self.dnd_go_out()
325 }
326
327 /// the effect of the app's last drag: DE_COPY / DE_MOVE / DE_LINK, 0 = cancelled / refused
328 pub fn drag_effect(&self) -> u32 {
329 self.dnd.last_effect
330 }
331
332 /// one of the app's drags runs (inside the app or carried by the platform)
333 pub fn dragging(&self) -> bool {
334 self.dnd.phase >= PH_IN
335 }
336
337 // the platform takes the drag over (headless: kept for the scripts) -> it did
338 fn dnd_go_out(&mut self) -> bool {
339 let data = self.dnd.data.clone();
340 // (hot reload: the replay keeps the drag inside; recording, it ends the replays)
341 let ok = if self.headless || self.hr_outside("a drag out of the app") {
342 self.dnd.out = Some(data);
343 true
344 } else {
345 plat::drag_start(data)
346 };
347 if ok {
348 self.dnd.phase = PH_OUT;
349 self.dnd_publish();
350 }
351 ok
352 }
353
354 // the app's drag is over: DS_END in the next pass, UIC_DRAG_END
355 fn dnd_end(&mut self, effect: u32) {
356 let d = &mut self.dnd;
357 d.end_src = d.src;
358 d.end_win = d.src_win;
359 d.end_live = d.src != 0;
360 d.last_effect = effect;
361 d.phase = PH_NONE;
362 d.src = 0;
363 d.data = DragData::default();
364 if d.over_win == d.src_win {
365 d.over = 0;
366 }
367 self.dnd_publish();
368 self.cmd_run(UIC_DRAG_END, effect);
369 self.repass = true;
370 }
371
372 // ------------------------------------------------------------------ targets
373
374 /// drop_target(id, types) -> DS_HOVER / DS_DROP / 0: the widget at last_rect takes drags
375 /// of these DT_* kinds (DT_USER0 << k: the app's own). DS_HOVER while such a drag is over
376 /// it (the style draws the highlight), DS_DROP in the pass of the drop (drop_data(),
377 /// drop_effect(): one of the target's effects that the source allows; none in common: the
378 /// drop is refused, no DS_DROP)
379 pub fn drop_target(&mut self, id: u32, types: u32) -> u32 {
380 if id == 0 || types == 0 {
381 return 0;
382 }
383 let c = &mut self.wp.ctx;
384 if c.disabled_depth != 0 {
385 return 0;
386 }
387 let r = c.last_rect;
388 let k = c.uiclip;
389 let cur = c.hit_cur;
390 let mut z = Zone {
391 id,
392 types,
393 effects: DE_ALL,
394 layer: c.cmd_layer_ui as u8,
395 x0: r[0].max(k[0]),
396 y0: r[1].max(k[1]),
397 x1: r[0].wrapping_add(r[2]).min(k[2]),
398 y1: r[1].wrapping_add(r[3]).min(k[3]),
399 };
400 let full = c.zones[cur].len() >= super::ctx::HITS_MAX;
401 let win = self.uiw_cur;
402 if z.x1 > z.x0 && z.y1 > z.y0 && !full {
403 self.dnd.last_zone = Some((win, c.zones[cur].len(), id));
404 c.zones[cur].push(z);
405 } else {
406 self.dnd.last_zone = None;
407 z.x1 = z.x0; // clipped away (or no room): no highlight
408 if full {
409 self.limit_hit(super::ctx::LIM_ZONES);
410 }
411 }
412 let d = &self.dnd;
413 if d.drop_live && d.drop_win == win && d.drop_id == id {
414 return DS_DROP;
415 }
416 if d.over == id && d.over_win == win && self.dnd_types_in(win) & types != 0 {
417 self.dnd.hl = Some((win, z.layer, [z.x0, z.y0, z.x1 - z.x0, z.y1 - z.y0]));
418 return DS_HOVER;
419 }
420 0
421 }
422
423 /// drop_effect_set(effects): the DE_* effects (DE_COPY | DE_MOVE | DE_LINK) the drop target
424 /// of the last drop_target call takes in this pass; DE_ALL until it is called. The platform
425 /// answers a drag of another program over the target with one of them (the modifier keys
426 /// choose, else copy, move, link in this order; files are never moved): the system's pointer
427 /// shows that effect and the drop carries it (drop_effect()). 0 refuses the drag there: no
428 /// drop, no highlight, the pointer shows "no" -- a page that takes drops only in some places;
429 /// so is a drag that allows none of them (a link target, a source that only copies or moves).
430 /// A drag of the app inside its window picks from them the same way (a plain drag moves when
431 /// both allow it). Call it after drop_target in every pass the effect should hold, typically
432 /// while drop_target says DS_HOVER, for the point of drag_over_px: the platform's answers
433 /// follow the passes, one pass late while the drag moves
434 pub fn drop_effect_set(&mut self, effects: u32) {
435 let (cur, win) = (self.wp.ctx.hit_cur, self.uiw_cur);
436 let Some((w, i, id)) = self.dnd.last_zone else { return };
437 if w != win {
438 return;
439 }
440 let Some(z) = self.wp.ctx.zones[cur].get_mut(i).filter(|z| z.id == id) else { return };
441 z.effects = effects & DE_ALL;
442 let r = [z.x0, z.y0, z.x1 - z.x0, z.y1 - z.y0];
443 if effects & self.dnd_allowed_in(win) == 0 && self.dnd.over == id && self.dnd.hl.is_some_and(|h| h.0 == win && h.2 == r) {
444 self.dnd.hl = None; // a target that refuses the drag over it is not highlighted
445 }
446 }
447
448 // the DE_* effects the drag over window win allows (0 = none): the app's own its source's
449 // (DragData::allowed), another program's what it offers (0 there: copy | move, as
450 // plat::drag_decide), never a move of another program's files
451 fn dnd_allowed_in(&self, win: u32) -> u32 {
452 let d = &self.dnd;
453 if d.phase == PH_IN && d.src_win == win {
454 d.data.allowed()
455 } else if d.ext && d.ext_win == win {
456 let a = if d.ext_allowed == 0 { DE_COPY | DE_MOVE } else { d.ext_allowed };
457 if d.ext_types & DT_FILES != 0 { a & !DE_MOVE } else { a }
458 } else {
459 0
460 }
461 }
462
463 /// drop_accept(types): the DT_* kinds a window takes outside every drop target (default
464 /// DT_FILES); those drops arrive as UIC_DROP
465 pub fn drop_accept(&mut self, types: u32) {
466 if self.dnd.accept != types {
467 self.dnd.accept = types;
468 self.dnd_publish();
469 }
470 }
471
472 /// drag_over() -> (DT_* kinds, x, y DIP): the drag over the live window, (0, 0, 0) = none
473 pub fn drag_over(&self) -> (u32, i32, i32) {
474 self.dnd_over().map_or((0, 0, 0), |(types, x, y)| (types, self.px2dip(x), self.px2dip(y)))
475 }
476
477 /// drag_over_px() -> (DT_* kinds, x, y px): drag_over with the position in px of the live
478 /// window, exact (logical like `last_rect`; minus a canvas's `UiInput::rect`: the point in
479 /// the canvas's own pixels); (0, 0, 0) = none
480 pub fn drag_over_px(&self) -> (u32, i32, i32) {
481 self.dnd_over().unwrap_or((0, 0, 0))
482 }
483
484 /// drag_allowed() -> the DE_* effects (DE_COPY | DE_MOVE | DE_LINK) the source of the drag over
485 /// the live window allows: another program's as its platform says -- Windows the effects of its
486 /// DoDragDrop, X11 the source's XdndActionList with the action it asks for (DE_ALL without a
487 /// list), Wayland its source_actions (copy and move: Wayland has no link), macOS the drag's
488 /// operation mask (narrowed by the modifier keys, as AppKit hands it on) -- the app's own drag
489 /// its DragData::allowed(); DE_ALL where the platform does not say. After a drop, the effects
490 /// its source allows -- as the platform gives them with the drop (Windows Drop's pdwEffect, X11
491 /// the list with the last XdndPosition's action, Wayland source_actions, macOS the operation
492 /// mask), else as the hover before it said, else DE_ALL (the browser, Android) -- from its
493 /// E_DROP to the end of the passes it starts (DS_DROP, App::command's UIC_DROP) and, when the
494 /// app called drop_defer, until its answer went (drop_finish, DROP_DEFER_MS, a newer drop): in
495 /// every window over which no drag is, and drop_finish answers within them. 0 otherwise: no
496 /// drag over the window, no drop in its passes or waiting. A page that decides an effect (a
497 /// web page's effectAllowed and dropEffect) picks from these, then drop_effect_set; the
498 /// platform never moves another program's files whatever this says
499 pub fn drag_allowed(&self) -> u32 {
500 let win = self.uiw_cur;
501 let d = &self.dnd;
502 let said = |a: u32| if a & DE_ALL != 0 { a & DE_ALL } else { DE_ALL };
503 if d.phase == PH_IN && d.src_win == win {
504 d.data.allowed()
505 } else if d.ext && d.ext_win == win {
506 said(d.ext_allowed)
507 } else if d.drop_now || d.wait.is_some_and(|w| w.deferred.is_some()) {
508 said(d.drop_allowed)
509 } else {
510 0
511 }
512 }
513
514 /// drop_defer() -> waits: (in the pass of a drop: while drop_target says DS_DROP, or in
515 /// App::command's UIC_DROP) the drop's answer to its source -- the effect it was taken with,
516 /// or that it was not taken -- waits for drop_finish, at most DROP_DEFER_MS; then it goes as
517 /// the drop was delivered (drop_effect()) with a UIWARN. For a target that learns later
518 /// whether it took the drop (a web page's drop handler in another process). false: the answer
519 /// went already (the platform answers at the drop: macOS, the browser, Android, Windows with a
520 /// source whose data object has no async mode; an internal drag of the app; outside the pass
521 /// of a drop): drop_finish has nothing to do then. What the source sees, per platform: docs/
522 /// GUIDE.md section 7, "Drag and drop"
523 pub fn drop_defer(&mut self) -> bool {
524 let now = self.now_ms;
525 let Some(w) = self.dnd.wait.as_mut() else { return false };
526 if w.deferred.is_none() {
527 w.deferred = Some(now);
528 self.timer(UIT_DROP, DROP_DEFER_MS);
529 }
530 true
531 }
532
533 /// drop_finish(effect) -> answered: the answer of the drop that waits (drop_defer; or, in the
534 /// pass of a drop, instead of the effect it was delivered with): the DE_* effect it was taken
535 /// with, 0 = not taken. An effect the source does not allow (drag_allowed says them until
536 /// then) becomes the first it does (copy, move, link): taken stays taken. false: no drop
537 /// waits (none was deferred, it was answered, or its DROP_DEFER_MS were up)
538 pub fn drop_finish(&mut self, effect: u32) -> bool {
539 let Some(w) = self.dnd.wait else { return false };
540 let e = effect & DE_ALL;
541 let e = if e == 0 {
542 0
543 } else if e & w.allowed != 0 {
544 plat::drag_effect_pick(e & w.allowed, 0)
545 } else {
546 plat::drag_effect_pick(w.allowed, 0)
547 };
548 self.dnd_answer(e);
549 true
550 }
551
552 // the drop that waits gets its answer: the platform's source (headless: kept for `drop
553 // expect`)
554 fn dnd_answer(&mut self, effect: u32) {
555 let Some(w) = self.dnd.wait.take() else { return };
556 if w.deferred.is_some() {
557 self.timers.retain(|t| t.0 != UIT_DROP);
558 self.need = true; // drag_allowed says the drop's effects no more
559 }
560 if !self.headless {
561 plat::drop_finish(w.serial, effect);
562 }
563 self.dnd.answered = Some(effect);
564 }
565
566 // a new drop (serial: the platform's while its answer waits, 0: it went at the drop) delivered
567 // with `effect`, its source allowing `allowed`: an older one that still waits is answered as
568 // it was delivered
569 fn dnd_wait_begin(&mut self, serial: u32, effect: u32, allowed: u32) {
570 if let Some(w) = self.dnd.wait {
571 self.dnd_answer(w.fallback);
572 }
573 if serial == 0 {
574 self.dnd.answered = Some(effect);
575 return;
576 }
577 self.dnd.answered = None;
578 self.dnd.wait = Some(Wait { serial, fallback: effect, allowed, deferred: None });
579 }
580
581 /// (runtime, after the passes of E_DROP) a drop the app did not defer is answered now; its
582 /// passes are over (drag_allowed: the drop's effects only while its deferred answer waits)
583 pub(crate) fn dnd_drop_settled(&mut self) {
584 if let Some(w) = self.dnd.wait.filter(|w| w.deferred.is_none()) {
585 self.dnd_answer(w.fallback);
586 }
587 if core::mem::take(&mut self.dnd.drop_now) && !self.dnd.wait.is_some_and(|w| w.deferred.is_some()) {
588 self.need = true; // a frame without the drop's effects
589 }
590 }
591
592 /// (runtime, the window loop ends) a drop whose answer still waits: answered as it was
593 /// delivered
594 pub(crate) fn dnd_quit(&mut self) {
595 if let Some(w) = self.dnd.wait {
596 self.dnd_answer(w.fallback);
597 }
598 }
599
600 /// (scripts: `drop expect`) the last drop's answer to its source: None while it waits
601 pub(crate) fn dnd_answered(&self) -> Option<Option<u32>> {
602 if self.dnd.wait.is_some() { Some(None) } else { self.dnd.answered.map(Some) }
603 }
604
605 // the drag over the live window: its kinds and position (px)
606 fn dnd_over(&self) -> Option<(u32, i32, i32)> {
607 let win = self.uiw_cur;
608 let d = &self.dnd;
609 if d.phase == PH_IN && d.src_win == win {
610 let c = &self.wp.ctx;
611 return Some((d.data.types(), c.mx, c.my));
612 }
613 if d.ext && d.ext_win == win {
614 return Some((d.ext_types, d.ext_x, d.ext_y));
615 }
616 None
617 }
618
619 /// the effect of the last drop (DE_*): one its target takes (drop_effect_set; outside the
620 /// targets every one) that its source allows (drag_allowed in its passes), files never moved
621 /// -- the platform's choice, else the first of copy, move, link. A drop with none in common
622 /// is refused (no DS_DROP, no UIC_DROP; its source is answered "not taken")
623 pub fn drop_effect(&self) -> u32 {
624 self.dnd.drop_effect
625 }
626
627 // the kinds of the drag over window win (0 = none)
628 fn dnd_types_in(&self, win: u32) -> u32 {
629 let d = &self.dnd;
630 if d.phase == PH_IN && d.src_win == win {
631 d.data.types()
632 } else if d.ext && d.ext_win == win {
633 d.ext_types
634 } else {
635 0
636 }
637 }
638
639 // ------------------------------------------------------------------ the runtime's calls
640
641 /// uiw_pass_begin: the drop target under the drag (the previous pass's targets)
642 pub(crate) fn dnd_pass_begin(&mut self) {
643 let win = self.uiw_cur;
644 self.dnd.last_zone = None;
645 if self.dnd.hl.is_some_and(|h| h.0 == win) {
646 self.dnd.hl = None;
647 }
648 let d = &self.dnd;
649 let at = if d.phase == PH_IN && d.src_win == win {
650 Some((self.wp.ctx.mx, self.wp.ctx.my, d.data.types()))
651 } else if d.ext && d.ext_win == win {
652 Some((d.ext_x, d.ext_y, d.ext_types))
653 } else {
654 None
655 };
656 if let Some((x, y, types)) = at {
657 let c = &self.wp.ctx;
658 let z = if self.uiw_inert_now() { None } else { zone_find(&c.zones[c.hit_cur ^ 1], &c.hits[c.hit_cur ^ 1], c.modal_prev, x, y, types) };
659 self.dnd.over = z.map_or(0, |z| z.id);
660 self.dnd.over_win = win;
661 } else if self.dnd.over_win == win {
662 self.dnd.over = 0;
663 }
664 }
665
666 /// uiw_pass_end: the highlight of the drop target under a drag, the label next to the
667 /// pointer of the app's drag
668 pub(crate) fn dnd_pass_end(&mut self) {
669 if let Some((w, layer, r)) = self.dnd.hl {
670 if w == self.uiw_cur && r[2] > 0 {
671 let prev = self.layer_set(layer as u32);
672 let st = self.style;
673 st.drop_highlight(self, r);
674 self.layer_set(prev);
675 }
676 }
677 let d = &self.dnd;
678 if d.phase != PH_IN || d.src_win != self.uiw_cur {
679 return;
680 }
681 let s = label_of(&d.data);
682 if s.is_empty() {
683 return;
684 }
685 let (mx, my) = (self.wp.ctx.mx, self.wp.ctx.my);
686 if mx == -1 && my == -1 {
687 return;
688 }
689 let lr = self.wp.ctx.last_rect;
690 let prev = self.layer_set(L_TIP);
691 let st = self.style;
692 let mut sp = Sp { label: Some(&s), ..Sp::default() };
693 let (w, h) = st.tooltip_size(self, &mut sp);
694 // right of and below the pointer, inside the window
695 let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
696 let gap = self.dp(12);
697 let x = (mx + gap).min(fw - w).max(0);
698 let y = (my + gap).min(fh - h).max(0);
699 sp.place([x, y, w, h]);
700 st.tooltip(self, &sp);
701 self.layer_set(prev);
702 self.wp.ctx.last_rect = lr;
703 }
704
705 /// uiw_clear_edges (after the first pass): DS_DROP / DS_END were seen
706 pub(crate) fn dnd_clear_edges(&mut self) {
707 self.dnd.drop_live = false;
708 self.dnd.end_live = false;
709 }
710
711 /// after the passes: a DS_START nobody answered with drag_set is no drag; a drag that
712 /// started outside its window (a fast pointer) goes to the platform now
713 pub(crate) fn dnd_after_passes(&mut self) {
714 // a deferred drop whose time is up: answered as it was delivered
715 if let Some(w) = self.dnd.wait {
716 if w.deferred.is_some_and(|t| self.now_ms.wrapping_sub(t) >= DROP_DEFER_MS) {
717 self.warn("a deferred drop got no drop_finish within DROP_DEFER_MS: answered as it was delivered");
718 self.dnd_answer(w.fallback);
719 }
720 }
721 if self.dnd.phase == PH_START {
722 self.dnd.phase = PH_NONE;
723 self.dnd.src = 0;
724 }
725 if self.dnd.phase == PH_IN {
726 let (cur, w) = (self.uiw_cur, self.dnd.src_win);
727 self.uiw_use(w);
728 if self.wp.ctx.mbtn & (1 << BTN_LEFT) == 0 {
729 self.dnd_end(0); // the button went without a release (focus lost): cancelled
730 } else {
731 self.dnd_moved();
732 }
733 self.uiw_use(cur);
734 }
735 }
736
737 /// needs_move_pass: a drag of the app follows the pointer
738 pub(crate) fn dnd_follows_pointer(&self) -> bool {
739 self.dnd.phase == PH_IN && self.dnd.src_win == self.uiw_cur
740 }
741
742 /// input_event, E_MOVE of the live window: the drag left the window -> the platform
743 /// takes over (content other programs know)
744 pub(crate) fn dnd_moved(&mut self) {
745 if !self.dnd_follows_pointer() || self.dnd.data.types() & DT_STD == 0 {
746 return;
747 }
748 let c = &self.wp.ctx;
749 let (x, y) = (c.mx, c.my);
750 let fb = &self.wp.rt.fb;
751 if x >= 0 && y >= 0 && x < fb.w && y < fb.h {
752 return;
753 }
754 if c.mbtn & (1 << BTN_LEFT) == 0 {
755 return;
756 }
757 if self.dnd_go_out() {
758 self.dnd.over = 0;
759 self.repass = true;
760 }
761 }
762
763 /// input_event, E_UP of the left button / Esc: the app's drag inside its window ends
764 /// -> true when the event belonged to the drag
765 pub(crate) fn dnd_input(&mut self, ev: &Event) -> bool {
766 let d = &self.dnd;
767 if d.src_win != self.uiw_cur || !(d.phase == PH_START || d.phase == PH_IN) {
768 return false;
769 }
770 match ev.typ {
771 plat::E_KEY_DOWN if ev.data & 0xFFFF == VK_ESCAPE => {
772 self.dnd_end(0); // Esc cancels
773 true
774 }
775 plat::E_UP if ev.btn == BTN_LEFT as u8 => {
776 if self.dnd.phase == PH_START {
777 self.dnd_after_passes();
778 return false;
779 }
780 // the drop target under the pointer (previous pass)
781 let (x, y) = if ev.x != -1 { (ev.x, ev.y) } else { (self.wp.ctx.mx, self.wp.ctx.my) };
782 let c = &self.wp.ctx;
783 let types = self.dnd.data.types();
784 let z = zone_find(&c.zones[c.hit_cur ^ 1], &c.hits[c.hit_cur ^ 1], c.modal_prev, x, y, types);
785 let mut effect = 0;
786 if let Some(z) = z {
787 // inside the app a plain drag moves (when the source and the target allow
788 // it); a target that takes none of the source's effects refuses it
789 let a = self.dnd.data.allowed() & z.effects;
790 effect = if self.mods & (MOD_CTRL | MOD_SHIFT) == 0 && a & DE_MOVE != 0 {
791 DE_MOVE
792 } else {
793 plat::drag_effect_pick(a, self.mods as u8)
794 };
795 if effect != 0 {
796 let win = self.uiw_cur;
797 let mut data = self.dnd.data.clone();
798 data.effects = effect;
799 self.dnd_deliver(z.id, win, x, y, data);
800 }
801 }
802 self.dnd_end(effect);
803 false // the release still reaches the widgets
804 }
805 _ => false,
806 }
807 }
808
809 // a drop for target id of window win at (x, y) logical px: DS_DROP in the next pass
810 fn dnd_deliver(&mut self, id: u32, win: u32, x: i32, y: i32, data: DragData) {
811 let files = data.files.clone();
812 self.dr.list = files;
813 self.dr.win = self.uiw_rec_id(win);
814 self.dr.px = (x, y);
815 self.dr.x = self.px2dip_win(win, x);
816 self.dr.y = self.px2dip_win(win, y);
817 let d = &mut self.dnd;
818 d.drop_effect = data.effects;
819 d.dropped = data;
820 d.drop_id = id;
821 d.drop_win = win;
822 d.drop_live = true;
823 self.repass = true;
824 }
825
826 // px -> DIP in window win
827 fn px2dip_win(&mut self, win: u32, v: i32) -> i32 {
828 let cur = self.uiw_cur;
829 self.uiw_use(win);
830 let r = self.px2dip(v);
831 self.uiw_use(cur);
832 r
833 }
834
835 /// E_DRAG of window win (UI index): another program's drag (or the app's own one coming
836 /// back) is over it at (x, y) px; data 0: it left
837 pub(crate) fn dnd_hover(&mut self, ev: &Event) {
838 let win = ev.win as u32;
839 if ev.data == 0 {
840 if self.dnd.ext_win == win {
841 self.dnd.ext = false;
842 self.dnd.ext_allowed = 0; // the next drag says its own
843 }
844 return;
845 }
846 let mut types = ev.data & DT_STD;
847 if self.dnd.phase == PH_OUT {
848 types |= self.dnd.data.types(); // the app's own drag: its own kinds too
849 }
850 let cur = self.uiw_cur;
851 self.uiw_use(win);
852 let x = if self.rtl { self.wp.rt.fb.w - 1 - ev.x } else { ev.x }; // logical
853 self.uiw_use(cur);
854 let d = &mut self.dnd;
855 d.ext = true;
856 d.ext_win = win;
857 d.ext_x = x;
858 d.ext_y = ev.y;
859 d.ext_types = types;
860 d.ext_allowed = (ev.data >> 8) & DE_ALL;
861 }
862
863 /// (scripts: `dragover expect`) what the platform would answer the other program's drag over
864 /// its window now, with the modifier keys mods (plat::drag_decide over the targets of the
865 /// last pass, as uiw_drag_pw does with the published ones) -> (DT_* taken, the DE_* effect,
866 /// every effect taken); None: no drag hovers
867 pub(crate) fn dnd_answer_now(&mut self, mods: u8) -> Option<(u32, u32, u32)> {
868 let d = &self.dnd;
869 if !d.ext {
870 return None;
871 }
872 let (win, x, y, offered, allowed) = (d.ext_win, d.ext_x, d.ext_y, d.ext_types & DT_STD, d.ext_allowed);
873 let own = if d.phase == PH_OUT { d.data.types() } else { 0 };
874 let accept = d.accept;
875 let cur = self.uiw_cur;
876 self.uiw_use(win);
877 let c = &self.wp.ctx;
878 let (t, e) = if self.uiw_inert_now() {
879 (0, 0)
880 } else {
881 drag_answer(&c.zones[c.hit_cur ^ 1], &c.hits[c.hit_cur ^ 1], c.modal_prev, accept, own, x, y, offered)
882 };
883 self.uiw_use(cur);
884 Some(plat::drag_decide(t, e, offered, allowed, mods))
885 }
886
887 /// E_DROP (drop.rs): the drop of another program (or the app's own drag) at (x, y) logical
888 /// px of window win -> true when a drop target took it or it was refused (else UIC_DROP
889 /// decides). serial: the platform's while the drop's answer waits (plat::drop_waiting), 0
890 /// when it went at the drop; given: the effects its source allows as the platform gave them
891 /// with the drop (plat::drop_allowed; 0: not said)
892 pub(crate) fn dnd_dropped(&mut self, win: u32, x: i32, y: i32, mut data: DragData, serial: u32, given: u32) -> bool {
893 // what the source allows: the app's own drag its data's; another program's as the drop
894 // says, else as the hover over the window before it said (the same drag), else nobody
895 // said (every effect)
896 let d = &self.dnd;
897 let src = if d.phase == PH_OUT {
898 d.data.allowed()
899 } else if given & DE_ALL != 0 {
900 given & DE_ALL
901 } else if d.ext && d.ext_win == win {
902 d.ext_allowed & DE_ALL
903 } else {
904 0
905 };
906 let mut allowed = if src == 0 { DE_ALL } else { src };
907 self.dnd.ext = false;
908 self.dnd.ext_allowed = 0;
909 self.dnd.drop_allowed = src;
910 self.dnd.drop_now = true;
911 if self.dnd.phase == PH_OUT {
912 // the app's own drag came back: its full content
913 let effect = data.effects;
914 data = self.dnd.data.clone();
915 data.effects = effect;
916 }
917 if data.effects & DE_ALL == 0 || (data.effects == DE_MOVE && !data.files.is_empty()) {
918 data.effects = DE_COPY; // files are never moved (plat::drag_query)
919 }
920 if !data.files.is_empty() {
921 allowed &= !DE_MOVE; // files are never moved
922 }
923 let types = data.types();
924 let cur = self.uiw_cur;
925 self.uiw_use(win);
926 let c = &self.wp.ctx;
927 let z = if self.uiw_inert_now() { None } else { zone_find(&c.zones[c.hit_cur ^ 1], &c.hits[c.hit_cur ^ 1], c.modal_prev, x, y, types) };
928 self.uiw_use(cur);
929 let serial = if self.headless && serial == 0 && !core::mem::take(&mut self.dnd.sync_next) {
930 self.dnd.serial = self.dnd.serial.wrapping_add(1).max(1); // headless: a platform that waits
931 self.dnd.serial
932 } else {
933 serial
934 };
935 // the effects the drop may have there: the target's (drop_effect_set; the window's
936 // drop_accept outside them: every one) that the source allows, files never moved. The
937 // platform picked one of them already; a scripted drop (or one that raced the pass) with
938 // another effect gets the first of them, and with none in common nothing happens (no
939 // DS_DROP, no UIC_DROP; the source is answered "not taken"), as the platform refuses
940 // such a drag over the target
941 let fx = z.map_or(DE_ALL, |z| z.effects) & allowed;
942 if data.effects & fx == 0 {
943 data.effects = plat::drag_effect_pick(fx, 0);
944 }
945 let e = data.effects;
946 match z {
947 Some(z) => {
948 if e != 0 {
949 self.dnd.drop_effect = e;
950 self.dnd_deliver(z.id, win, x, y, data);
951 }
952 self.dnd_wait_begin(serial, e, allowed);
953 true
954 }
955 None => {
956 if e != 0 {
957 self.dnd.drop_effect = e;
958 self.dnd.dropped = data;
959 }
960 self.dnd_wait_begin(serial, e, allowed);
961 e == 0
962 }
963 }
964 }
965
966 /// E_DRAG_END: the platform finished the app's drag (data = the effect)
967 pub(crate) fn dnd_out_end(&mut self, effect: u32) {
968 if self.dnd.phase == PH_OUT {
969 self.dnd.out = None;
970 // its hover over a window of the app (it came back) ended with it
971 self.dnd.ext = false;
972 self.dnd.ext_allowed = 0;
973 self.dnd_end(effect & DE_ALL);
974 }
975 }
976
977 /// (scripts) the content a headless drag out handed over
978 pub(crate) fn dnd_out_data(&self) -> Option<&DragData> {
979 if self.dnd.phase == PH_OUT { self.dnd.out.as_ref() } else { None }
980 }
981
982 // what the platform's synchronous hook needs: the accepted kinds and the app's own drag
983 fn dnd_publish(&self) {
984 let own = if self.dnd.phase == PH_OUT { self.dnd.data.types() } else { 0 };
985 super::win::uiw_publish_dnd(self.dnd.accept, own);
986 }
987}