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