Skip to main content

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}