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//   drop_accept(types)               what the windows take outside every drop target (default
21//                                    DT_FILES); such drops come as UIC_DROP (drop.rs, as before)
22//   drag_over() -> (types, x, y)     the drag over the live window: its kinds and position (DIP);
23//                                    (0, 0, 0) when none
24//
25// An internal drag stays in the app while the pointer stays in its window: the drop target under
26// the pointer is highlighted, a label follows the pointer, Esc cancels, a release over a matching
27// target drops there (else nothing happens).  The target is the innermost drop target at the
28// point (the smallest of the topmost layer); targets under a popup or a modal layer do not count.
29// When the pointer leaves the window with content other programs know (DT_STD), the platform
30// takes over (plat::drag_start: DoDragDrop, Xdnd, wl_data_device.start_drag); dropped back onto
31// the app the drop carries the app's own content again (its kind and bytes).  Headless runs keep
32// what would have left the app for the scripts (`dragout expect`, `dragout end`).
33// Other programs' drags: E_DRAG (hover: the targets highlight), E_DROP (onto the drop target at
34// the point, else UIC_DROP when drop_accept takes it).  The platform asks the published targets
35// synchronously (uiw_drag_pw, win.rs) to answer the source with an effect.
36// Positions stay logical in mirrored (RTL) windows: E_DRAG / E_DROP points are mirrored on the
37// way in like every pointer event; the label next to the pointer is drawn through the mirror.
38
39use alloc::string::String;
40
41use super::consts::*;
42use super::keys::*;
43use super::{Sp, Ui};
44use plat::{DE_ALL, DE_COPY, DE_MOVE, DT_FILES, DT_STD, DragData, Event};
45
46const PH_NONE: u8 = 0;
47const PH_START: u8 = 1; // drag_source said DS_START, drag_set not called yet
48const PH_IN: u8 = 2; // inside the source window
49const PH_OUT: u8 = 3; // the platform carries it (or, headless, it left the window)
50const LABEL_MAX: usize = 48; // characters of the label next to the pointer
51
52/// a drop target of a pass (clipped, physical px of the window; x1 / y1 exclusive)
53#[derive(Clone, Copy, Default)]
54pub(crate) struct Zone {
55    pub(crate) id: u32,
56    pub(crate) types: u32,
57    pub(crate) layer: u8,
58    pub(crate) x0: i32,
59    pub(crate) y0: i32,
60    pub(crate) x1: i32,
61    pub(crate) y1: i32,
62}
63
64/// the drop target at (x, y) that takes one of `types`: of the topmost layer that has a hit
65/// box or a target there (never below `modal`), the smallest one
66pub(crate) fn zone_find(zones: &[Zone], hits: &[super::ctx::Hit], modal: u32, x: i32, y: i32, types: u32) -> Option<Zone> {
67    if types == 0 || (x == -1 && y == -1) {
68        return None;
69    }
70    let inside = |x0: i32, y0: i32, x1: i32, y1: i32| x >= x0 && x < x1 && y >= y0 && y < y1;
71    // a popup over the target hides it
72    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);
73    let area = |z: &Zone| (z.x1 - z.x0) as i64 * (z.y1 - z.y0) as i64;
74    let mut best: Option<Zone> = None;
75    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)) {
76        let better = match &best {
77            None => true,
78            Some(b) => z.layer > b.layer || (z.layer == b.layer && area(z) <= area(b)),
79        };
80        if better {
81            best = Some(*z);
82        }
83    }
84    best
85}
86
87/// the drag and drop state shared by the windows
88pub(crate) struct S {
89    phase: u8,
90    src: u32,
91    src_win: u32,
92    /// what the app drags
93    data: DragData,
94    /// DS_END for `end_src` in the next pass
95    end_src: u32,
96    end_win: u32,
97    end_live: bool,
98    last_effect: u32,
99    /// another program's drag over window ext_win (logical px)
100    ext: bool,
101    ext_win: u32,
102    ext_x: i32,
103    ext_y: i32,
104    ext_types: u32,
105    /// the drop target under the drag (this pass) and its highlight (window, layer, rect),
106    /// drawn at the end of the window's pass over the target's content
107    over: u32,
108    over_win: u32,
109    hl: Option<(u32, u8, [i32; 4])>,
110    /// DS_DROP for drop_id of drop_win in the next pass
111    drop_id: u32,
112    drop_win: u32,
113    drop_live: bool,
114    pub(crate) drop_effect: u32,
115    /// the content of the last drop
116    pub(crate) dropped: DragData,
117    /// what a window takes outside the drop targets
118    pub(crate) accept: u32,
119    /// headless: what a drag out handed over (scripts)
120    pub(crate) out: Option<DragData>,
121}
122
123impl Default for S {
124    fn default() -> S {
125        S {
126            phase: PH_NONE,
127            src: 0,
128            src_win: 0,
129            data: DragData::default(),
130            end_src: 0,
131            end_win: 0,
132            end_live: false,
133            last_effect: 0,
134            ext: false,
135            ext_win: 0,
136            ext_x: 0,
137            ext_y: 0,
138            ext_types: 0,
139            over: 0,
140            over_win: 0,
141            hl: None,
142            drop_id: 0,
143            drop_win: 0,
144            drop_live: false,
145            drop_effect: 0,
146            dropped: DragData::default(),
147            accept: DT_FILES,
148            out: None,
149        }
150    }
151}
152
153// the label next to the pointer: the app's, else from the content
154fn label_of(d: &DragData) -> String {
155    let cut = |s: &str| -> String {
156        let line = s.lines().find(|l| !l.trim().is_empty()).unwrap_or("").trim();
157        let mut out: String = line.chars().take(LABEL_MAX).collect();
158        if line.chars().count() > LABEL_MAX {
159            out.push('…');
160        }
161        out
162    };
163    if !d.label.is_empty() {
164        return cut(&d.label);
165    }
166    if let Some(t) = &d.text {
167        return cut(t);
168    }
169    if let Some(f) = d.files.first() {
170        let name = f.rsplit(['/', '\\']).find(|s| !s.is_empty()).unwrap_or(f);
171        let mut s = cut(name);
172        if d.files.len() > 1 {
173            s.push_str(&alloc::format!(" +{}", d.files.len() - 1));
174        }
175        return s;
176    }
177    if let Some(u) = d.urls.first() {
178        return cut(u);
179    }
180    String::new()
181}
182
183impl Ui {
184    // ------------------------------------------------------------------ sources
185
186    /// drag_source(id, ev) -> DS_START / DS_ACTIVE / DS_END / 0 for the widget `id` whose
187    /// behave returned ev: DS_START when a drag of it begins (call drag_set in this pass),
188    /// DS_ACTIVE while its content is dragged, DS_END once afterwards (drag_effect())
189    pub fn drag_source(&mut self, id: u32, ev: u32) -> u32 {
190        let cur = self.uiw_cur;
191        let d = &self.dnd;
192        if id == 0 {
193            return 0;
194        }
195        if d.end_live && d.end_src == id && d.end_win == cur {
196            return DS_END;
197        }
198        if d.phase != PH_NONE && d.src == id && d.src_win == cur {
199            return if d.phase == PH_START { DS_START } else { DS_ACTIVE };
200        }
201        if d.phase == PH_NONE && ev & EV_DRAG != 0 && self.wp.ctx.active == id && self.wp.ctx.mbtn & (1 << BTN_LEFT) != 0 {
202            let d = &mut self.dnd;
203            d.phase = PH_START;
204            d.src = id;
205            d.src_win = cur;
206            d.data = DragData::default();
207            self.wp.ctx.active = 0; // the drag owns the pointer: the release is no click
208            return DS_START;
209        }
210        0
211    }
212
213    /// drag_set(data): the content of the drag that starts (after DS_START, same pass)
214    pub fn drag_set(&mut self, data: DragData) {
215        let d = &mut self.dnd;
216        if d.phase == PH_START && d.src_win == self.uiw_cur && !data.is_empty() {
217            d.data = data;
218            d.phase = PH_IN;
219            self.repass = true; // the targets see it in this frame
220        }
221    }
222
223    /// drag_out(data) -> started: (the left button held) the platform drags data out of the
224    /// app right away (DoDragDrop, Xdnd, wl_data_device.start_drag); UIC_DRAG_END at the end
225    pub fn drag_out(&mut self, data: DragData) -> bool {
226        if self.dnd.phase != PH_NONE || data.types() & DT_STD == 0 {
227            return false;
228        }
229        self.dnd.src = 0;
230        self.dnd.src_win = self.uiw_cur;
231        self.dnd.data = data;
232        self.dnd_go_out()
233    }
234
235    /// the effect of the app's last drag: DE_COPY / DE_MOVE / DE_LINK, 0 = cancelled / refused
236    pub fn drag_effect(&self) -> u32 {
237        self.dnd.last_effect
238    }
239
240    /// one of the app's drags runs (inside the app or carried by the platform)
241    pub fn dragging(&self) -> bool {
242        self.dnd.phase >= PH_IN
243    }
244
245    // the platform takes the drag over (headless: kept for the scripts) -> it did
246    fn dnd_go_out(&mut self) -> bool {
247        let data = self.dnd.data.clone();
248        // (hot reload: the replay keeps the drag inside; recording, it ends the replays)
249        let ok = if self.headless || self.hr_outside("a drag out of the app") {
250            self.dnd.out = Some(data);
251            true
252        } else {
253            plat::drag_start(data)
254        };
255        if ok {
256            self.dnd.phase = PH_OUT;
257            self.dnd_publish();
258        }
259        ok
260    }
261
262    // the app's drag is over: DS_END in the next pass, UIC_DRAG_END
263    fn dnd_end(&mut self, effect: u32) {
264        let d = &mut self.dnd;
265        d.end_src = d.src;
266        d.end_win = d.src_win;
267        d.end_live = d.src != 0;
268        d.last_effect = effect;
269        d.phase = PH_NONE;
270        d.src = 0;
271        d.data = DragData::default();
272        if d.over_win == d.src_win {
273            d.over = 0;
274        }
275        self.dnd_publish();
276        self.cmd_run(UIC_DRAG_END, effect);
277        self.repass = true;
278    }
279
280    // ------------------------------------------------------------------ targets
281
282    /// drop_target(id, types) -> DS_HOVER / DS_DROP / 0: the widget at last_rect takes drags
283    /// of these DT_* kinds (DT_USER0 << k: the app's own).  DS_HOVER while such a drag is over
284    /// it (the style draws the highlight), DS_DROP in the pass of the drop (drop_data())
285    pub fn drop_target(&mut self, id: u32, types: u32) -> u32 {
286        if id == 0 || types == 0 {
287            return 0;
288        }
289        let c = &mut self.wp.ctx;
290        if c.disabled_depth != 0 {
291            return 0;
292        }
293        let r = c.last_rect;
294        let k = c.uiclip;
295        let cur = c.hit_cur;
296        let mut z = Zone {
297            id,
298            types,
299            layer: c.cmd_layer_ui as u8,
300            x0: r[0].max(k[0]),
301            y0: r[1].max(k[1]),
302            x1: r[0].wrapping_add(r[2]).min(k[2]),
303            y1: r[1].wrapping_add(r[3]).min(k[3]),
304        };
305        let full = c.zones[cur].len() >= super::ctx::HITS_MAX;
306        if z.x1 > z.x0 && z.y1 > z.y0 && !full {
307            c.zones[cur].push(z);
308        } else {
309            z.x1 = z.x0; // clipped away (or no room): no highlight
310            if full {
311                self.limit_hit(super::ctx::LIM_ZONES);
312            }
313        }
314        let win = self.uiw_cur;
315        let d = &self.dnd;
316        if d.drop_live && d.drop_win == win && d.drop_id == id {
317            return DS_DROP;
318        }
319        if d.over == id && d.over_win == win && self.dnd_types_in(win) & types != 0 {
320            self.dnd.hl = Some((win, z.layer, [z.x0, z.y0, z.x1 - z.x0, z.y1 - z.y0]));
321            return DS_HOVER;
322        }
323        0
324    }
325
326    /// drop_accept(types): the DT_* kinds a window takes outside every drop target (default
327    /// DT_FILES); those drops arrive as UIC_DROP
328    pub fn drop_accept(&mut self, types: u32) {
329        if self.dnd.accept != types {
330            self.dnd.accept = types;
331            self.dnd_publish();
332        }
333    }
334
335    /// drag_over() -> (DT_* kinds, x, y DIP): the drag over the live window, (0, 0, 0) = none
336    pub fn drag_over(&self) -> (u32, i32, i32) {
337        let win = self.uiw_cur;
338        let d = &self.dnd;
339        if d.phase == PH_IN && d.src_win == win {
340            let c = &self.wp.ctx;
341            return (d.data.types(), self.px2dip(c.mx), self.px2dip(c.my));
342        }
343        if d.ext && d.ext_win == win {
344            return (d.ext_types, self.px2dip(d.ext_x), self.px2dip(d.ext_y));
345        }
346        (0, 0, 0)
347    }
348
349    /// the effect of the last drop (DE_*)
350    pub fn drop_effect(&self) -> u32 {
351        self.dnd.drop_effect
352    }
353
354    // the kinds of the drag over window win (0 = none)
355    fn dnd_types_in(&self, win: u32) -> u32 {
356        let d = &self.dnd;
357        if d.phase == PH_IN && d.src_win == win {
358            d.data.types()
359        } else if d.ext && d.ext_win == win {
360            d.ext_types
361        } else {
362            0
363        }
364    }
365
366    // ------------------------------------------------------------------ the runtime's calls
367
368    /// uiw_pass_begin: the drop target under the drag (the previous pass's targets)
369    pub(crate) fn dnd_pass_begin(&mut self) {
370        let win = self.uiw_cur;
371        if self.dnd.hl.is_some_and(|h| h.0 == win) {
372            self.dnd.hl = None;
373        }
374        let d = &self.dnd;
375        let at = if d.phase == PH_IN && d.src_win == win {
376            Some((self.wp.ctx.mx, self.wp.ctx.my, d.data.types()))
377        } else if d.ext && d.ext_win == win {
378            Some((d.ext_x, d.ext_y, d.ext_types))
379        } else {
380            None
381        };
382        if let Some((x, y, types)) = at {
383            let c = &self.wp.ctx;
384            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) };
385            self.dnd.over = z.map_or(0, |z| z.id);
386            self.dnd.over_win = win;
387        } else if self.dnd.over_win == win {
388            self.dnd.over = 0;
389        }
390    }
391
392    /// uiw_pass_end: the highlight of the drop target under a drag, the label next to the
393    /// pointer of the app's drag
394    pub(crate) fn dnd_pass_end(&mut self) {
395        if let Some((w, layer, r)) = self.dnd.hl {
396            if w == self.uiw_cur && r[2] > 0 {
397                let prev = self.layer_set(layer as u32);
398                let st = self.style;
399                st.drop_highlight(self, r);
400                self.layer_set(prev);
401            }
402        }
403        let d = &self.dnd;
404        if d.phase != PH_IN || d.src_win != self.uiw_cur {
405            return;
406        }
407        let s = label_of(&d.data);
408        if s.is_empty() {
409            return;
410        }
411        let (mx, my) = (self.wp.ctx.mx, self.wp.ctx.my);
412        if mx == -1 && my == -1 {
413            return;
414        }
415        let lr = self.wp.ctx.last_rect;
416        let prev = self.layer_set(L_TIP);
417        let st = self.style;
418        let mut sp = Sp { label: Some(&s), ..Sp::default() };
419        let (w, h) = st.tooltip_size(self, &mut sp);
420        // right of and below the pointer, inside the window
421        let (fw, fh) = (self.wp.rt.fb.w, self.wp.rt.fb.h);
422        let gap = self.dp(12);
423        let x = (mx + gap).min(fw - w).max(0);
424        let y = (my + gap).min(fh - h).max(0);
425        sp.place([x, y, w, h]);
426        st.tooltip(self, &sp);
427        self.layer_set(prev);
428        self.wp.ctx.last_rect = lr;
429    }
430
431    /// uiw_clear_edges (after the first pass): DS_DROP / DS_END were seen
432    pub(crate) fn dnd_clear_edges(&mut self) {
433        self.dnd.drop_live = false;
434        self.dnd.end_live = false;
435    }
436
437    /// after the passes: a DS_START nobody answered with drag_set is no drag; a drag that
438    /// started outside its window (a fast pointer) goes to the platform now
439    pub(crate) fn dnd_after_passes(&mut self) {
440        if self.dnd.phase == PH_START {
441            self.dnd.phase = PH_NONE;
442            self.dnd.src = 0;
443        }
444        if self.dnd.phase == PH_IN {
445            let (cur, w) = (self.uiw_cur, self.dnd.src_win);
446            self.uiw_use(w);
447            if self.wp.ctx.mbtn & (1 << BTN_LEFT) == 0 {
448                self.dnd_end(0); // the button went without a release (focus lost): cancelled
449            } else {
450                self.dnd_moved();
451            }
452            self.uiw_use(cur);
453        }
454    }
455
456    /// needs_move_pass: a drag of the app follows the pointer
457    pub(crate) fn dnd_follows_pointer(&self) -> bool {
458        self.dnd.phase == PH_IN && self.dnd.src_win == self.uiw_cur
459    }
460
461    /// input_event, E_MOVE of the live window: the drag left the window -> the platform
462    /// takes over (content other programs know)
463    pub(crate) fn dnd_moved(&mut self) {
464        if !self.dnd_follows_pointer() || self.dnd.data.types() & DT_STD == 0 {
465            return;
466        }
467        let c = &self.wp.ctx;
468        let (x, y) = (c.mx, c.my);
469        let fb = &self.wp.rt.fb;
470        if x >= 0 && y >= 0 && x < fb.w && y < fb.h {
471            return;
472        }
473        if c.mbtn & (1 << BTN_LEFT) == 0 {
474            return;
475        }
476        if self.dnd_go_out() {
477            self.dnd.over = 0;
478            self.repass = true;
479        }
480    }
481
482    /// input_event, E_UP of the left button / Esc: the app's drag inside its window ends
483    /// -> true when the event belonged to the drag
484    pub(crate) fn dnd_input(&mut self, ev: &Event) -> bool {
485        let d = &self.dnd;
486        if d.src_win != self.uiw_cur || !(d.phase == PH_START || d.phase == PH_IN) {
487            return false;
488        }
489        match ev.typ {
490            plat::E_KEY_DOWN if ev.data & 0xFFFF == VK_ESCAPE => {
491                self.dnd_end(0); // Esc cancels
492                true
493            }
494            plat::E_UP if ev.btn == BTN_LEFT as u8 => {
495                if self.dnd.phase == PH_START {
496                    self.dnd_after_passes();
497                    return false;
498                }
499                // the drop target under the pointer (previous pass)
500                let (x, y) = if ev.x != -1 { (ev.x, ev.y) } else { (self.wp.ctx.mx, self.wp.ctx.my) };
501                let c = &self.wp.ctx;
502                let types = self.dnd.data.types();
503                let z = zone_find(&c.zones[c.hit_cur ^ 1], &c.hits[c.hit_cur ^ 1], c.modal_prev, x, y, types);
504                let mut effect = 0;
505                if let Some(z) = z {
506                    // inside the app a plain drag moves (when the source allows it)
507                    let a = self.dnd.data.allowed();
508                    effect = if self.mods & (MOD_CTRL | MOD_SHIFT) == 0 && a & DE_MOVE != 0 {
509                        DE_MOVE
510                    } else {
511                        plat::drag_effect_pick(a, self.mods as u8)
512                    };
513                    let win = self.uiw_cur;
514                    let mut data = self.dnd.data.clone();
515                    data.effects = effect;
516                    self.dnd_deliver(z.id, win, x, y, data);
517                }
518                self.dnd_end(effect);
519                false // the release still reaches the widgets
520            }
521            _ => false,
522        }
523    }
524
525    // a drop for target id of window win at (x, y) logical px: DS_DROP in the next pass
526    fn dnd_deliver(&mut self, id: u32, win: u32, x: i32, y: i32, data: DragData) {
527        let files = data.files.clone();
528        self.dr.list = files;
529        self.dr.win = self.uiw_rec_id(win);
530        self.dr.x = self.px2dip_win(win, x);
531        self.dr.y = self.px2dip_win(win, y);
532        let d = &mut self.dnd;
533        d.drop_effect = data.effects;
534        d.dropped = data;
535        d.drop_id = id;
536        d.drop_win = win;
537        d.drop_live = true;
538        self.repass = true;
539    }
540
541    // px -> DIP in window win
542    fn px2dip_win(&mut self, win: u32, v: i32) -> i32 {
543        let cur = self.uiw_cur;
544        self.uiw_use(win);
545        let r = self.px2dip(v);
546        self.uiw_use(cur);
547        r
548    }
549
550    /// E_DRAG of window win (UI index): another program's drag (or the app's own one coming
551    /// back) is over it at (x, y) px; data 0: it left
552    pub(crate) fn dnd_hover(&mut self, ev: &Event) {
553        let win = ev.win as u32;
554        if ev.data == 0 {
555            if self.dnd.ext_win == win {
556                self.dnd.ext = false;
557            }
558            return;
559        }
560        let mut types = ev.data & DT_STD;
561        if self.dnd.phase == PH_OUT {
562            types |= self.dnd.data.types(); // the app's own drag: its own kinds too
563        }
564        let cur = self.uiw_cur;
565        self.uiw_use(win);
566        let x = if self.rtl { self.wp.rt.fb.w - 1 - ev.x } else { ev.x }; // logical
567        self.uiw_use(cur);
568        let d = &mut self.dnd;
569        d.ext = true;
570        d.ext_win = win;
571        d.ext_x = x;
572        d.ext_y = ev.y;
573        d.ext_types = types;
574    }
575
576    /// E_DROP (drop.rs): the drop of another program (or the app's own drag) at (x, y) logical
577    /// px of window win -> true when a drop target took it (else UIC_DROP decides)
578    pub(crate) fn dnd_dropped(&mut self, win: u32, x: i32, y: i32, mut data: DragData) -> bool {
579        self.dnd.ext = false;
580        if self.dnd.phase == PH_OUT {
581            // the app's own drag came back: its full content
582            let effect = data.effects;
583            data = self.dnd.data.clone();
584            data.effects = effect;
585        }
586        if data.effects & DE_ALL == 0 || (data.effects == DE_MOVE && !data.files.is_empty()) {
587            data.effects = DE_COPY; // files are never moved (plat::drag_query)
588        }
589        let types = data.types();
590        let cur = self.uiw_cur;
591        self.uiw_use(win);
592        let c = &self.wp.ctx;
593        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) };
594        self.uiw_use(cur);
595        self.dnd.drop_effect = data.effects;
596        match z {
597            Some(z) => {
598                self.dnd_deliver(z.id, win, x, y, data);
599                true
600            }
601            None => {
602                self.dnd.dropped = data;
603                false
604            }
605        }
606    }
607
608    /// E_DRAG_END: the platform finished the app's drag (data = the effect)
609    pub(crate) fn dnd_out_end(&mut self, effect: u32) {
610        if self.dnd.phase == PH_OUT {
611            self.dnd.out = None;
612            self.dnd_end(effect & DE_ALL);
613        }
614    }
615
616    /// (scripts) the content a headless drag out handed over
617    pub(crate) fn dnd_out_data(&self) -> Option<&DragData> {
618        if self.dnd.phase == PH_OUT { self.dnd.out.as_ref() } else { None }
619    }
620
621    // what the platform's synchronous hook needs: the accepted kinds and the app's own drag
622    fn dnd_publish(&self) {
623        let own = if self.dnd.phase == PH_OUT { self.dnd.data.types() } else { 0 };
624        super::win::uiw_publish_dnd(self.dnd.accept, own);
625    }
626}