Skip to main content

i_slint_core/items/
drag_n_drop.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4use super::{
5    DragAction, DragActionArg, DropEvent, Item, ItemConsts, ItemRc, MouseCursor,
6    PointerEventButton, RenderingResult,
7};
8use crate::Coord;
9use crate::data_transfer::DataTransfer;
10use crate::graphics::Image;
11use crate::input::{
12    FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, InternalKeyEvent,
13    KeyEventResult, KeyboardModifiers, MouseEvent,
14};
15use crate::item_rendering::{CachedRenderingData, ItemRenderer};
16use crate::layout::{LayoutInfo, Orientation};
17use crate::lengths::{LogicalPoint, LogicalRect, LogicalSize};
18#[cfg(feature = "rtti")]
19use crate::rtti::*;
20use crate::window::WindowAdapter;
21use crate::{Callback, Property};
22use alloc::rc::Rc;
23use const_field_offset::FieldOffsets;
24use core::cell::Cell;
25use core::pin::Pin;
26use i_slint_core_macros::*;
27
28pub type DropEventArg = (DropEvent,);
29
30/// The set of actions a drag source permits, captured when the drag starts.
31#[repr(C)]
32#[derive(Clone, Copy, Default, PartialEq, Eq, Debug)]
33pub struct AllowedDragActions {
34    pub copy: bool,
35    pub move_: bool,
36    pub link: bool,
37}
38
39impl AllowedDragActions {
40    /// True if at least one action is permitted.
41    pub fn any(self) -> bool {
42        self.copy || self.move_ || self.link
43    }
44}
45
46#[repr(C)]
47#[derive(FieldOffsets, Default, SlintElement)]
48#[pin]
49/// The implementation of the `DragArea` element
50pub struct DragArea {
51    pub enabled: Property<bool>,
52    pub data: Property<DataTransfer>,
53    pub drag_image: Property<Image>,
54    pub drag_image_offset_x: Property<i32>,
55    pub drag_image_offset_y: Property<i32>,
56    pub allow_copy: Property<bool>,
57    pub allow_move: Property<bool>,
58    pub allow_link: Property<bool>,
59    pub dragging: Property<bool>,
60    pub drag_finished: Callback<DragActionArg, ()>,
61    pressed: Cell<bool>,
62    pressed_position: Cell<LogicalPoint>,
63    pub cached_rendering_data: CachedRenderingData,
64}
65
66impl Item for DragArea {
67    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
68
69    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
70
71    fn layout_info(
72        self: Pin<&Self>,
73        _: Orientation,
74        _cross_axis_constraint: Coord,
75        _window_adapter: &Rc<dyn WindowAdapter>,
76        _self_rc: &ItemRc,
77    ) -> LayoutInfo {
78        LayoutInfo { stretch: 1., ..LayoutInfo::default() }
79    }
80
81    fn input_event_filter_before_children(
82        self: Pin<&Self>,
83        event: &MouseEvent,
84        _window_adapter: &Rc<dyn WindowAdapter>,
85        _self_rc: &ItemRc,
86        _: &mut MouseCursor,
87    ) -> InputEventFilterResult {
88        if !self.enabled() || !self.allowed_actions().any() || self.data().is_empty() {
89            self.cancel();
90            return InputEventFilterResult::ForwardAndIgnore;
91        }
92
93        match event {
94            MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => {
95                self.pressed_position.set(*position);
96                self.pressed.set(true);
97                InputEventFilterResult::ForwardAndInterceptGrab
98            }
99            MouseEvent::Exit => {
100                self.cancel();
101                InputEventFilterResult::ForwardAndIgnore
102            }
103            MouseEvent::Released { button: PointerEventButton::Left, .. } => {
104                self.pressed.set(false);
105                InputEventFilterResult::ForwardAndIgnore
106            }
107
108            MouseEvent::Moved { position, .. } => {
109                if !self.pressed.get() {
110                    InputEventFilterResult::ForwardEvent
111                } else {
112                    let pressed_pos = self.pressed_position.get();
113                    let dx = (position.x - pressed_pos.x).abs();
114                    let dy = (position.y - pressed_pos.y).abs();
115                    let threshold = super::flickable::DISTANCE_THRESHOLD.get();
116                    if dy > threshold || dx > threshold {
117                        InputEventFilterResult::Intercept
118                    } else {
119                        InputEventFilterResult::ForwardAndInterceptGrab
120                    }
121                }
122            }
123            MouseEvent::Wheel { .. } => InputEventFilterResult::ForwardAndIgnore,
124            // Not the left button
125            MouseEvent::Pressed { .. } | MouseEvent::Released { .. } => {
126                InputEventFilterResult::ForwardAndIgnore
127            }
128            MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
129                InputEventFilterResult::ForwardAndIgnore
130            }
131            MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => {
132                InputEventFilterResult::ForwardAndIgnore
133            }
134        }
135    }
136
137    fn input_event(
138        self: Pin<&Self>,
139        event: &MouseEvent,
140        _window_adapter: &Rc<dyn WindowAdapter>,
141        _self_rc: &ItemRc,
142        _: &mut MouseCursor,
143    ) -> InputEventResult {
144        match event {
145            MouseEvent::Pressed { .. } => InputEventResult::EventAccepted,
146            MouseEvent::Exit => {
147                self.cancel();
148                InputEventResult::EventIgnored
149            }
150            MouseEvent::Released { .. } => {
151                self.cancel();
152                InputEventResult::EventIgnored
153            }
154            MouseEvent::Moved { position, .. } => {
155                if !self.pressed.get()
156                    || !self.enabled()
157                    || !self.allowed_actions().any()
158                    || self.data().is_empty()
159                {
160                    return InputEventResult::EventIgnored;
161                }
162                let pressed_pos = self.pressed_position.get();
163                let dx = (position.x - pressed_pos.x).abs();
164                let dy = (position.y - pressed_pos.y).abs();
165                let threshold = super::flickable::DISTANCE_THRESHOLD.get();
166                let start_drag = dx > threshold || dy > threshold;
167                if start_drag {
168                    self.pressed.set(false);
169                    InputEventResult::StartDrag
170                } else {
171                    InputEventResult::EventAccepted
172                }
173            }
174            MouseEvent::Wheel { .. } => InputEventResult::EventIgnored,
175            MouseEvent::PinchGesture { .. } | MouseEvent::RotationGesture { .. } => {
176                InputEventResult::EventIgnored
177            }
178            MouseEvent::DragMove { .. } | MouseEvent::Drop { .. } => InputEventResult::EventIgnored,
179        }
180    }
181
182    fn capture_key_event(
183        self: Pin<&Self>,
184        _: &InternalKeyEvent,
185        _window_adapter: &Rc<dyn WindowAdapter>,
186        _self_rc: &ItemRc,
187    ) -> KeyEventResult {
188        KeyEventResult::EventIgnored
189    }
190
191    fn key_event(
192        self: Pin<&Self>,
193        _: &InternalKeyEvent,
194        _window_adapter: &Rc<dyn WindowAdapter>,
195        _self_rc: &ItemRc,
196    ) -> KeyEventResult {
197        KeyEventResult::EventIgnored
198    }
199
200    fn focus_event(
201        self: Pin<&Self>,
202        _: &FocusEvent,
203        _window_adapter: &Rc<dyn WindowAdapter>,
204        _self_rc: &ItemRc,
205    ) -> FocusEventResult {
206        FocusEventResult::FocusIgnored
207    }
208
209    fn render(
210        self: Pin<&Self>,
211        _: &mut &mut dyn ItemRenderer,
212        _self_rc: &ItemRc,
213        _size: LogicalSize,
214    ) -> RenderingResult {
215        RenderingResult::ContinueRenderingChildren
216    }
217
218    fn bounding_rect(
219        self: core::pin::Pin<&Self>,
220        _window_adapter: &Rc<dyn WindowAdapter>,
221        _self_rc: &ItemRc,
222        mut geometry: LogicalRect,
223    ) -> LogicalRect {
224        geometry.size = LogicalSize::zero();
225        geometry
226    }
227
228    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
229        false
230    }
231}
232
233impl ItemConsts for DragArea {
234    const cached_rendering_data_offset: const_field_offset::FieldOffset<
235        DragArea,
236        CachedRenderingData,
237    > = DragArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
238}
239
240impl DragArea {
241    fn cancel(self: Pin<&Self>) {
242        self.pressed.set(false)
243    }
244
245    pub(crate) fn allowed_actions(self: Pin<&Self>) -> AllowedDragActions {
246        AllowedDragActions {
247            copy: self.allow_copy(),
248            move_: self.allow_move(),
249            link: self.allow_link(),
250        }
251    }
252
253    /// Build the initial DropEvent for a drag starting on this DragArea, together with the
254    /// source's allowed actions. `proposed_action` is seeded from the default action (the first
255    /// allowed of move, copy, link).
256    pub(crate) fn initial_drop_event(self: Pin<&Self>) -> (DropEvent, AllowedDragActions) {
257        let allowed = self.allowed_actions();
258        let event = DropEvent {
259            data: self.data(),
260            position: Default::default(),
261            proposed_action: compute_proposed_action(KeyboardModifiers::default(), allowed),
262        };
263        (event, allowed)
264    }
265}
266
267#[repr(C)]
268#[derive(FieldOffsets, Default, SlintElement)]
269#[pin]
270/// The implementation of the `DropArea` element
271pub struct DropArea {
272    pub enabled: Property<bool>,
273    pub has_drag: Property<bool>,
274    pub current_action: Property<DragAction>,
275    pub can_drop: Callback<DropEventArg, DragAction>,
276    pub dropped: Callback<DropEventArg, DragAction>,
277
278    pub cached_rendering_data: CachedRenderingData,
279}
280
281impl Item for DropArea {
282    fn init(self: Pin<&Self>, _self_rc: &ItemRc) {}
283
284    fn deinit(self: Pin<&Self>, _window_adapter: &Rc<dyn WindowAdapter>) {}
285
286    fn layout_info(
287        self: Pin<&Self>,
288        _: Orientation,
289        _cross_axis_constraint: Coord,
290        _window_adapter: &Rc<dyn WindowAdapter>,
291        _self_rc: &ItemRc,
292    ) -> LayoutInfo {
293        LayoutInfo { stretch: 1., ..LayoutInfo::default() }
294    }
295
296    fn input_event_filter_before_children(
297        self: Pin<&Self>,
298        _: &MouseEvent,
299        _window_adapter: &Rc<dyn WindowAdapter>,
300        _self_rc: &ItemRc,
301        _: &mut MouseCursor,
302    ) -> InputEventFilterResult {
303        InputEventFilterResult::ForwardEvent
304    }
305
306    fn input_event(
307        self: Pin<&Self>,
308        event: &MouseEvent,
309        _: &Rc<dyn WindowAdapter>,
310        _self_rc: &ItemRc,
311        cursor: &mut MouseCursor,
312    ) -> InputEventResult {
313        if !self.enabled() {
314            return InputEventResult::EventIgnored;
315        }
316        match event {
317            MouseEvent::DragMove { event, allowed } => {
318                let raw = Self::FIELD_OFFSETS.can_drop().apply_pin(self).call(&(event.clone(),));
319                let chosen = clamp_action_to_allowed(raw, *allowed);
320                self.current_action.set(chosen);
321                if chosen != DragAction::None {
322                    self.has_drag.set(true);
323                    *cursor = cursor_for_action(chosen);
324                    InputEventResult::EventAccepted
325                } else {
326                    self.has_drag.set(false);
327                    InputEventResult::EventIgnored
328                }
329            }
330            MouseEvent::Drop { event, allowed } => {
331                self.has_drag.set(false);
332                let returned =
333                    Self::FIELD_OFFSETS.dropped().apply_pin(self).call(&(event.clone(),));
334                // The target's `dropped` return value is the final action reported back to
335                // the source. Clamp against the source's allowed set and stash on
336                // `current_action` so the post-dispatch step in `window.rs` can read it.
337                self.current_action.set(clamp_action_to_allowed(returned, *allowed));
338                InputEventResult::EventAccepted
339            }
340            MouseEvent::Exit => {
341                self.has_drag.set(false);
342                self.current_action.set(DragAction::None);
343                InputEventResult::EventIgnored
344            }
345            _ => InputEventResult::EventIgnored,
346        }
347    }
348
349    fn capture_key_event(
350        self: Pin<&Self>,
351        _: &InternalKeyEvent,
352        _window_adapter: &Rc<dyn WindowAdapter>,
353        _self_rc: &ItemRc,
354    ) -> KeyEventResult {
355        KeyEventResult::EventIgnored
356    }
357
358    fn key_event(
359        self: Pin<&Self>,
360        _: &InternalKeyEvent,
361        _window_adapter: &Rc<dyn WindowAdapter>,
362        _self_rc: &ItemRc,
363    ) -> KeyEventResult {
364        KeyEventResult::EventIgnored
365    }
366
367    fn focus_event(
368        self: Pin<&Self>,
369        _: &FocusEvent,
370        _window_adapter: &Rc<dyn WindowAdapter>,
371        _self_rc: &ItemRc,
372    ) -> FocusEventResult {
373        FocusEventResult::FocusIgnored
374    }
375
376    fn render(
377        self: Pin<&Self>,
378        _: &mut &mut dyn ItemRenderer,
379        _self_rc: &ItemRc,
380        _size: LogicalSize,
381    ) -> RenderingResult {
382        RenderingResult::ContinueRenderingChildren
383    }
384
385    fn bounding_rect(
386        self: core::pin::Pin<&Self>,
387        _window_adapter: &Rc<dyn WindowAdapter>,
388        _self_rc: &ItemRc,
389        mut geometry: LogicalRect,
390    ) -> LogicalRect {
391        geometry.size = LogicalSize::zero();
392        geometry
393    }
394
395    fn clips_children(self: core::pin::Pin<&Self>) -> bool {
396        false
397    }
398}
399
400impl ItemConsts for DropArea {
401    const cached_rendering_data_offset: const_field_offset::FieldOffset<
402        DropArea,
403        CachedRenderingData,
404    > = DropArea::FIELD_OFFSETS.cached_rendering_data().as_unpinned_projection();
405}
406
407/// Compute the action proposed by the user's current modifier state, clamped to the source's
408/// allowed actions. Ctrl alone → copy, Shift alone → move, Ctrl+Shift → link, no modifier →
409/// the first allowed of move/copy/link.
410pub(crate) fn compute_proposed_action(
411    modifiers: KeyboardModifiers,
412    allowed_actions: AllowedDragActions,
413) -> DragAction {
414    let allowed = |a| match a {
415        DragAction::Copy => allowed_actions.copy,
416        DragAction::Move => allowed_actions.move_,
417        DragAction::Link => allowed_actions.link,
418        DragAction::None => false,
419    };
420    let modifier_request = match (modifiers.control, modifiers.shift) {
421        (true, true) => Some(DragAction::Link),
422        (true, false) => Some(DragAction::Copy),
423        (false, true) => Some(DragAction::Move),
424        (false, false) => None,
425    };
426    if let Some(req) = modifier_request
427        && allowed(req)
428    {
429        return req;
430    }
431    for fallback in [DragAction::Move, DragAction::Copy, DragAction::Link] {
432        if allowed(fallback) {
433            return fallback;
434        }
435    }
436    DragAction::None
437}
438
439/// Clamp a `can-drop` return value against the source's allowed actions.
440/// A concrete action the source did not allow becomes `None`.
441pub(crate) fn clamp_action_to_allowed(
442    action: DragAction,
443    allowed: AllowedDragActions,
444) -> DragAction {
445    match action {
446        DragAction::None => DragAction::None,
447        DragAction::Copy if allowed.copy => DragAction::Copy,
448        DragAction::Move if allowed.move_ => DragAction::Move,
449        DragAction::Link if allowed.link => DragAction::Link,
450        _ => DragAction::None,
451    }
452}
453
454/// The cursor shown while a DropArea is hovering an accepted drag.
455pub(crate) fn cursor_for_action(action: DragAction) -> MouseCursor {
456    match action {
457        DragAction::Move => MouseCursor::Move,
458        DragAction::Copy => MouseCursor::Copy,
459        DragAction::Link => MouseCursor::Alias,
460        DragAction::None => MouseCursor::NoDrop,
461    }
462}
463
464#[cfg(test)]
465mod tests {
466    use super::*;
467
468    fn modifiers(control: bool, shift: bool) -> KeyboardModifiers {
469        KeyboardModifiers { control, shift, alt: false, meta: false }
470    }
471
472    const ALL: AllowedDragActions = AllowedDragActions { copy: true, move_: true, link: true };
473    const COPY_ONLY: AllowedDragActions =
474        AllowedDragActions { copy: true, move_: false, link: false };
475    const MOVE_ONLY: AllowedDragActions =
476        AllowedDragActions { copy: false, move_: true, link: false };
477    const LINK_ONLY: AllowedDragActions =
478        AllowedDragActions { copy: false, move_: false, link: true };
479    const COPY_AND_MOVE: AllowedDragActions =
480        AllowedDragActions { copy: true, move_: true, link: false };
481
482    #[test]
483    fn compute_proposed_action_modifier_table() {
484        // All actions allowed.
485        let a = |m| compute_proposed_action(m, ALL);
486        assert_eq!(a(modifiers(false, false)), DragAction::Move);
487        assert_eq!(a(modifiers(true, false)), DragAction::Copy);
488        assert_eq!(a(modifiers(false, true)), DragAction::Move);
489        assert_eq!(a(modifiers(true, true)), DragAction::Link);
490    }
491
492    #[test]
493    fn compute_proposed_action_falls_back_when_modifier_action_not_allowed() {
494        // Source only allows move; user holds Ctrl (asking for copy).
495        assert_eq!(compute_proposed_action(modifiers(true, false), MOVE_ONLY), DragAction::Move);
496        // User holds Ctrl+Shift asking for link, only copy allowed.
497        assert_eq!(compute_proposed_action(modifiers(true, true), COPY_ONLY), DragAction::Copy);
498    }
499
500    #[test]
501    fn compute_proposed_action_default_is_first_allowed() {
502        // No modifiers: the first allowed of move, copy, link wins.
503        assert_eq!(
504            compute_proposed_action(modifiers(false, false), COPY_AND_MOVE),
505            DragAction::Move
506        );
507        assert_eq!(compute_proposed_action(modifiers(false, false), COPY_ONLY), DragAction::Copy);
508        assert_eq!(compute_proposed_action(modifiers(false, false), LINK_ONLY), DragAction::Link);
509        // Nothing allowed at all.
510        assert_eq!(
511            compute_proposed_action(modifiers(false, false), AllowedDragActions::default()),
512            DragAction::None
513        );
514    }
515}