Skip to main content

sequoia_view/
dnd.rs

1use std::any::{Any, TypeId};
2use std::fmt;
3use std::rc::Rc;
4
5use sequoia_core::{Point, Rect, Vec2};
6use sequoia_input::Modifiers;
7use sequoia_layout::Axis;
8
9use crate::any::AnyView;
10use crate::overlay::OverlayId;
11use crate::runtime::ElementId;
12
13pub const DRAG_THRESHOLD: f32 = 4.0;
14
15#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
16pub struct DragId(pub u64);
17
18#[derive(Clone, Copy, Debug, PartialEq, Eq)]
19pub struct DragEnd {
20    pub dropped: bool,
21
22    pub cancelled: bool,
23}
24
25#[derive(Clone)]
26pub struct DragPayload {
27    type_id: TypeId,
28    value: Rc<dyn Any>,
29    type_name: &'static str,
30}
31
32impl DragPayload {
33    pub fn new<T: 'static>(value: T) -> Self {
34        Self {
35            type_id: TypeId::of::<T>(),
36            value: Rc::new(value),
37            type_name: std::any::type_name::<T>(),
38        }
39    }
40
41    pub fn get<T: 'static>(&self) -> Option<&T> {
42        self.value.downcast_ref::<T>()
43    }
44
45    pub fn is<T: 'static>(&self) -> bool {
46        self.type_id == TypeId::of::<T>()
47    }
48
49    pub fn type_id(&self) -> TypeId {
50        self.type_id
51    }
52
53    pub fn type_name(&self) -> &'static str {
54        self.type_name
55    }
56}
57
58impl fmt::Debug for DragPayload {
59    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
60        f.debug_struct("DragPayload")
61            .field("type", &self.type_name)
62            .finish()
63    }
64}
65
66#[derive(Clone, Copy, Debug, PartialEq)]
67pub struct DropPosition {
68    pub global: Point,
69
70    pub target_rect: Rect,
71
72    pub local: Point,
73
74    pub fraction: Vec2,
75
76    pub modifiers: Modifiers,
77}
78
79impl DropPosition {
80    pub fn in_rect(global: Point, target_rect: Rect, modifiers: Modifiers) -> Self {
81        let local = Point::new(global.x - target_rect.left(), global.y - target_rect.top());
82        let fraction = Vec2::new(
83            if target_rect.size.width > 0.0 {
84                (local.x / target_rect.size.width).clamp(0.0, 1.0)
85            } else {
86                0.0
87            },
88            if target_rect.size.height > 0.0 {
89                (local.y / target_rect.size.height).clamp(0.0, 1.0)
90            } else {
91                0.0
92            },
93        );
94        Self {
95            global,
96            target_rect,
97            local: Point::new(local.x, local.y),
98            fraction,
99            modifiers,
100        }
101    }
102
103    pub fn insertion_index(&self, count: usize, axis: Axis) -> usize {
104        insertion_index(self.global, self.target_rect, count, axis)
105    }
106
107    pub fn before_midpoint(&self, axis: Axis) -> bool {
108        match axis {
109            Axis::Horizontal => self.fraction.x < 0.5,
110            Axis::Vertical => self.fraction.y < 0.5,
111        }
112    }
113}
114
115#[derive(Clone, Debug)]
116pub struct DragSession {
117    pub id: DragId,
118
119    pub payload: DragPayload,
120
121    pub source: Option<ElementId>,
122
123    pub origin: Point,
124
125    pub grab_offset: Vec2,
126
127    pub current: Point,
128
129    pub target: Option<ElementId>,
130
131    pub modifiers: Modifiers,
132
133    pub preview: Option<OverlayId>,
134}
135
136impl DragSession {
137    pub fn position_in(&self, rect: Rect) -> DropPosition {
138        DropPosition::in_rect(self.current, rect, self.modifiers)
139    }
140}
141
142pub struct DragOptions {
143    pub(crate) threshold: f32,
144    pub(crate) preview: Option<AnyView>,
145}
146
147impl Default for DragOptions {
148    fn default() -> Self {
149        Self {
150            threshold: DRAG_THRESHOLD,
151            preview: None,
152        }
153    }
154}
155
156impl DragOptions {
157    pub fn new() -> Self {
158        Self::default()
159    }
160
161    pub fn threshold(mut self, px: f32) -> Self {
162        self.threshold = px.max(0.0);
163        self
164    }
165
166    pub fn preview(mut self, view: impl crate::view::View + 'static) -> Self {
167        self.preview = Some(AnyView::new(view));
168        self
169    }
170}
171
172pub(crate) struct PendingDrag {
173    pub(crate) source: ElementId,
174
175    pub(crate) origin: Point,
176
177    pub(crate) source_rect: Rect,
178    pub(crate) payload: DragPayload,
179    pub(crate) preview: Option<AnyView>,
180    pub(crate) threshold: f32,
181}
182
183pub(crate) struct ActiveDrag {
184    pub(crate) id: DragId,
185    pub(crate) payload: DragPayload,
186
187    pub(crate) source: Option<ElementId>,
188    pub(crate) origin: Point,
189    pub(crate) grab_offset: Vec2,
190    pub(crate) current: Point,
191    pub(crate) modifiers: Modifiers,
192    pub(crate) target: Option<ElementId>,
193    pub(crate) preview: Option<OverlayId>,
194}
195
196impl ActiveDrag {
197    pub(crate) fn snapshot(&self) -> DragSession {
198        DragSession {
199            id: self.id,
200            payload: self.payload.clone(),
201            source: self.source,
202            origin: self.origin,
203            grab_offset: self.grab_offset,
204            current: self.current,
205            target: self.target,
206            modifiers: self.modifiers,
207            preview: self.preview,
208        }
209    }
210}
211
212pub fn exceeded_threshold(origin: Point, current: Point, threshold: f32) -> bool {
213    let d = current - origin;
214    if !d.x.is_finite() || !d.y.is_finite() || !threshold.is_finite() {
215        return false;
216    }
217    d.length() >= threshold.max(0.0)
218}
219
220pub fn grab_offset(origin: Point, source_rect: Rect) -> Vec2 {
221    origin - source_rect.origin
222}
223
224pub fn preview_origin(current: Point, grab: Vec2) -> Point {
225    current - grab
226}
227
228pub fn insertion_index(pos: Point, rect: Rect, count: usize, axis: Axis) -> usize {
229    if count == 0 {
230        return 0;
231    }
232    let (start, extent, p) = match axis {
233        Axis::Horizontal => (rect.left(), rect.size.width, pos.x),
234        Axis::Vertical => (rect.top(), rect.size.height, pos.y),
235    };
236    if !(extent.is_finite() && extent > 0.0) || !p.is_finite() {
237        return 0;
238    }
239    let t = ((p - start) / extent * count as f32).floor() as i64;
240    t.clamp(0, count as i64) as usize
241}
242
243pub fn move_item<T>(items: &mut Vec<T>, from: usize, slot: usize) {
244    if from >= items.len() {
245        return;
246    }
247    let item = items.remove(from);
248
249    let mut to = if slot > from { slot - 1 } else { slot };
250    to = to.min(items.len());
251    items.insert(to, item);
252}
253
254#[cfg(test)]
255mod tests {
256    use super::*;
257
258    fn rect(x: f32, y: f32, w: f32, h: f32) -> Rect {
259        Rect::from_xywh(x, y, w, h)
260    }
261
262    #[test]
263    fn threshold_is_euclidean() {
264        let o = Point::ZERO;
265        assert!(exceeded_threshold(o, Point::new(3.0, 4.0), 5.0));
266        assert!(!exceeded_threshold(o, Point::new(3.0, 4.0), 5.1));
267        assert!(exceeded_threshold(o, Point::new(-4.0, 0.0), 4.0));
268        assert!(!exceeded_threshold(o, Point::new(1.0, 1.0), 4.0));
269        assert!(!exceeded_threshold(o, Point::new(f32::NAN, 0.0), 0.0));
270    }
271
272    #[test]
273    fn grab_keeps_preview_under_cursor() {
274        let src = rect(100.0, 50.0, 60.0, 20.0);
275        let origin = Point::new(115.0, 58.0);
276        let grab = grab_offset(origin, src);
277        assert_eq!(grab, Vec2::new(15.0, 8.0));
278
279        let cur = Point::new(200.0, 100.0);
280        let pv = preview_origin(cur, grab);
281        assert_eq!(pv + grab, cur);
282    }
283
284    #[test]
285    fn drop_position_local_and_fraction() {
286        let r = rect(10.0, 20.0, 100.0, 40.0);
287        let p = DropPosition::in_rect(Point::new(35.0, 30.0), r, Modifiers::NONE);
288        assert_eq!(p.local, Point::new(25.0, 10.0));
289        assert!((p.fraction.x - 0.25).abs() < 1e-6);
290        assert!((p.fraction.y - 0.25).abs() < 1e-6);
291
292        let outside = DropPosition::in_rect(Point::new(500.0, -50.0), r, Modifiers::NONE);
293        assert_eq!(outside.fraction.x, 1.0);
294        assert_eq!(outside.fraction.y, 0.0);
295
296        let zero = DropPosition::in_rect(Point::ZERO, Rect::ZERO, Modifiers::NONE);
297        assert_eq!(zero.fraction, Vec2::ZERO);
298    }
299
300    #[test]
301    fn insertion_slots() {
302        let r = rect(0.0, 0.0, 100.0, 400.0);
303        assert_eq!(
304            insertion_index(Point::new(50.0, -10.0), r, 4, Axis::Vertical),
305            0
306        );
307        assert_eq!(
308            insertion_index(Point::new(50.0, 50.0), r, 4, Axis::Vertical),
309            0
310        );
311        assert_eq!(
312            insertion_index(Point::new(50.0, 150.0), r, 4, Axis::Vertical),
313            1
314        );
315        assert_eq!(
316            insertion_index(Point::new(50.0, 350.0), r, 4, Axis::Vertical),
317            3
318        );
319        assert_eq!(
320            insertion_index(Point::new(50.0, 500.0), r, 4, Axis::Vertical),
321            4
322        );
323        assert_eq!(insertion_index(Point::ZERO, r, 0, Axis::Vertical), 0);
324        let h = rect(0.0, 0.0, 400.0, 20.0);
325        assert_eq!(
326            insertion_index(Point::new(250.0, 10.0), h, 4, Axis::Horizontal),
327            2
328        );
329    }
330
331    #[test]
332    fn move_item_semantics() {
333        let mut v = vec!['A', 'B', 'C', 'D'];
334        move_item(&mut v, 0, 3);
335        assert_eq!(v, vec!['B', 'C', 'A', 'D']);
336        let mut v = vec!['A', 'B', 'C', 'D'];
337        move_item(&mut v, 3, 1);
338        assert_eq!(v, vec!['A', 'D', 'B', 'C']);
339        let mut v = vec!['A', 'B', 'C', 'D'];
340        move_item(&mut v, 1, 1);
341        assert_eq!(v, vec!['A', 'B', 'C', 'D']);
342        let mut v = vec!['A', 'B', 'C', 'D'];
343        move_item(&mut v, 0, 4);
344        assert_eq!(v, vec!['B', 'C', 'D', 'A']);
345        let mut v = vec!['A'];
346        move_item(&mut v, 5, 0);
347        assert_eq!(v, vec!['A']);
348    }
349
350    #[test]
351    fn payload_typed_access() {
352        let p = DragPayload::new(42u32);
353        assert!(p.is::<u32>());
354        assert!(!p.is::<String>());
355        assert_eq!(p.get::<u32>(), Some(&42u32));
356        assert_eq!(p.get::<String>(), None);
357    }
358}