Skip to main content

fui/
drag_drop.rs

1use crate::ffi::CursorStyle;
2use crate::ffi::PointerEventType;
3use crate::node::NodeRef;
4use std::cell::RefCell;
5use std::collections::HashMap;
6use std::rc::Rc;
7
8const DRAG_DROP_TEXT_FORMAT: &str = "text/plain";
9
10#[repr(u32)]
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum DragDropEffects {
13    #[default]
14    None = 0,
15    Copy = 1,
16    Move = 2,
17    Link = 4,
18}
19
20fn normalize_effect(candidate: DragDropEffects, allowed: DragDropEffects) -> DragDropEffects {
21    let masked = (candidate as u32) & (allowed as u32);
22    if masked == DragDropEffects::None as u32 {
23        return DragDropEffects::None;
24    }
25    if (masked & DragDropEffects::Move as u32) != 0 {
26        return DragDropEffects::Move;
27    }
28    if (masked & DragDropEffects::Copy as u32) != 0 {
29        return DragDropEffects::Copy;
30    }
31    if (masked & DragDropEffects::Link as u32) != 0 {
32        return DragDropEffects::Link;
33    }
34    DragDropEffects::None
35}
36
37#[derive(Clone, Debug, Default, PartialEq, Eq)]
38pub struct DragDataObject {
39    formats: HashMap<String, String>,
40}
41
42impl DragDataObject {
43    pub fn new() -> Self {
44        Self::default()
45    }
46
47    pub fn set_text(mut self, value: impl Into<String>) -> Self {
48        self.formats
49            .insert(String::from(DRAG_DROP_TEXT_FORMAT), value.into());
50        self
51    }
52
53    pub fn set_format(mut self, format: impl Into<String>, value: impl Into<String>) -> Self {
54        self.formats.insert(format.into(), value.into());
55        self
56    }
57
58    pub fn has_format(&self, format: &str) -> bool {
59        self.formats.contains_key(format)
60    }
61
62    pub fn get_text(&self) -> Option<String> {
63        self.get_format(DRAG_DROP_TEXT_FORMAT)
64    }
65
66    pub fn get_format(&self, format: &str) -> Option<String> {
67        self.formats.get(format).cloned()
68    }
69}
70
71#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
72pub struct DragCompletedEventArgs {
73    pub effect: DragDropEffects,
74}
75
76#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
77pub struct DropProposal {
78    pub effect: DragDropEffects,
79    pub show_insertion_marker: bool,
80}
81
82impl DropProposal {
83    pub fn new(effect: DragDropEffects, show_insertion_marker: bool) -> Self {
84        Self {
85            effect,
86            show_insertion_marker,
87        }
88    }
89
90    pub fn none() -> Self {
91        Self::default()
92    }
93}
94
95type DragCompletedCallback = Rc<dyn Fn(DragCompletedEventArgs)>;
96
97struct DragSessionState {
98    source: NodeRef,
99    current_effect: DragDropEffects,
100    active: bool,
101    completed_callback: Option<DragCompletedCallback>,
102}
103
104#[derive(Clone)]
105pub struct DragSession {
106    pub data: DragDataObject,
107    pub allowed_effects: DragDropEffects,
108    inner: Rc<RefCell<DragSessionState>>,
109}
110
111impl DragSession {
112    pub(crate) fn new(
113        source: NodeRef,
114        data: DragDataObject,
115        allowed_effects: DragDropEffects,
116    ) -> Self {
117        Self {
118            data,
119            allowed_effects,
120            inner: Rc::new(RefCell::new(DragSessionState {
121                source,
122                current_effect: DragDropEffects::None,
123                active: true,
124                completed_callback: None,
125            })),
126        }
127    }
128
129    pub fn current_effect(&self) -> DragDropEffects {
130        self.inner.borrow().current_effect
131    }
132
133    pub fn is_active(&self) -> bool {
134        self.inner.borrow().active
135    }
136
137    pub fn on_completed(&self, callback: impl Fn(DragCompletedEventArgs) + 'static) -> &Self {
138        self.inner.borrow_mut().completed_callback = Some(Rc::new(callback));
139        self
140    }
141
142    pub fn cancel(&self) {
143        if !self.is_active() {
144            return;
145        }
146        crate::event::cancel_drag_session(self.clone());
147    }
148
149    pub(crate) fn source(&self) -> NodeRef {
150        self.inner.borrow().source.clone()
151    }
152
153    pub(crate) fn set_current_effect(&self, effect: DragDropEffects) {
154        self.inner.borrow_mut().current_effect = effect;
155    }
156
157    pub(crate) fn complete(&self, effect: DragDropEffects) {
158        let callback = {
159            let mut state = self.inner.borrow_mut();
160            if !state.active {
161                return;
162            }
163            state.active = false;
164            state.current_effect = effect;
165            state.completed_callback.clone()
166        };
167        if let Some(callback) = callback {
168            callback(DragCompletedEventArgs { effect });
169        }
170    }
171}
172
173#[derive(Clone)]
174pub struct DragEventArgs {
175    pub session: DragSession,
176    pub x: f32,
177    pub y: f32,
178    pub modifiers: u32,
179}
180
181impl DragEventArgs {
182    pub fn new(session: DragSession, x: f32, y: f32, modifiers: u32) -> Self {
183        Self {
184            session,
185            x,
186            y,
187            modifiers,
188        }
189    }
190}
191
192#[derive(Default)]
193struct DragDropState {
194    active_session: Option<DragSession>,
195    active_target: Option<NodeRef>,
196}
197
198thread_local! {
199    static STATE: RefCell<DragDropState> = RefCell::new(DragDropState::default());
200}
201
202fn is_default_proposal(proposal: DropProposal) -> bool {
203    proposal.effect == DragDropEffects::None && !proposal.show_insertion_marker
204}
205
206fn with_state<T>(callback: impl FnOnce(&mut DragDropState) -> T) -> T {
207    STATE.with(|slot| callback(&mut slot.borrow_mut()))
208}
209
210pub(crate) fn cursor_override_style() -> CursorStyle {
211    STATE.with(|slot| {
212        let state = slot.borrow();
213        let Some(session) = state.active_session.as_ref() else {
214            return CursorStyle::Default;
215        };
216        if !session.is_active() {
217            return CursorStyle::Default;
218        }
219        if session.current_effect() == DragDropEffects::None {
220            CursorStyle::Grabbing
221        } else {
222            CursorStyle::Move
223        }
224    })
225}
226
227pub(crate) fn begin_session(source: NodeRef) -> bool {
228    let existing = STATE.with(|slot| slot.borrow().active_session.clone());
229    if let Some(existing) = existing {
230        finish_session(existing, DragDropEffects::None, 0.0, 0.0, 0, true);
231    }
232    if !source.has_drag_source() {
233        return false;
234    }
235    let Some(data) = source.create_drag_data_object() else {
236        return false;
237    };
238    let allowed = source.get_drag_allowed_effects();
239    if allowed == DragDropEffects::None {
240        return false;
241    }
242    with_state(|state| {
243        state.active_target = None;
244        state.active_session = Some(DragSession::new(source, data, allowed));
245    });
246    true
247}
248
249pub(crate) fn cancel_session(session: DragSession) {
250    let is_active = STATE.with(|slot| {
251        slot.borrow()
252            .active_session
253            .as_ref()
254            .map(|active| Rc::ptr_eq(&active.inner, &session.inner))
255            .unwrap_or(false)
256    });
257    if !is_active {
258        return;
259    }
260    finish_session(session, DragDropEffects::None, 0.0, 0.0, 0, true);
261}
262
263pub(crate) fn cancel_session_for_source(source: &NodeRef) {
264    let active = STATE.with(|slot| slot.borrow().active_session.clone());
265    let Some(session) = active else {
266        return;
267    };
268    if !session.source().ptr_eq(source) {
269        return;
270    }
271    finish_session(session, DragDropEffects::None, 0.0, 0.0, 0, true);
272}
273
274pub(crate) fn handle_node_destroyed(node: NodeRef) {
275    let active = STATE.with(|slot| {
276        let state = slot.borrow();
277        (state.active_session.clone(), state.active_target.clone())
278    });
279    if let Some(session) = active.0 {
280        if session.source().ptr_eq(&node) {
281            finish_session(session, DragDropEffects::None, 0.0, 0.0, 0, true);
282            return;
283        }
284    }
285    if let Some(target) = active.1 {
286        if target.ptr_eq(&node) {
287            with_state(|state| {
288                state.active_target = None;
289                if let Some(session) = state.active_session.as_ref() {
290                    session.set_current_effect(DragDropEffects::None);
291                }
292            });
293        }
294    }
295}
296
297pub(crate) fn handle_pointer_event(
298    pointed_node: Option<NodeRef>,
299    event_type: PointerEventType,
300    x: f32,
301    y: f32,
302    modifiers: u32,
303) {
304    let active = STATE.with(|slot| slot.borrow().active_session.clone());
305    let Some(session) = active else {
306        return;
307    };
308    if !session.is_active() {
309        return;
310    }
311    match event_type {
312        PointerEventType::Down
313        | PointerEventType::Enter
314        | PointerEventType::Move
315        | PointerEventType::Leave => {
316            update_target(pointed_node, session, x, y, modifiers);
317        }
318        PointerEventType::Up => {
319            let effect = update_target(pointed_node, session.clone(), x, y, modifiers);
320            let target = STATE.with(|slot| slot.borrow().active_target.clone());
321            if let Some(target) = target {
322                if effect != DragDropEffects::None {
323                    target.handle_drop_event(DragEventArgs::new(session.clone(), x, y, modifiers));
324                }
325            }
326            finish_session(session, effect, x, y, modifiers, true);
327        }
328        PointerEventType::Cancel => {}
329    }
330}
331
332pub(crate) fn reset() {
333    with_state(|state| {
334        state.active_session = None;
335        state.active_target = None;
336    });
337}
338
339fn update_target(
340    pointed_node: Option<NodeRef>,
341    session: DragSession,
342    x: f32,
343    y: f32,
344    modifiers: u32,
345) -> DragDropEffects {
346    let target = resolve_drop_target(pointed_node);
347    let args = DragEventArgs::new(session.clone(), x, y, modifiers);
348    let mut proposal = DropProposal::none();
349    let previous_target = STATE.with(|slot| slot.borrow().active_target.clone());
350    let target_changed = match (&target, &previous_target) {
351        (Some(left), Some(right)) => !left.ptr_eq(right),
352        (None, None) => false,
353        _ => true,
354    };
355    if target_changed {
356        if let Some(previous_target) = previous_target {
357            previous_target.handle_drag_leave(args.clone());
358        }
359        with_state(|state| {
360            state.active_target = target.clone();
361        });
362        if let Some(target) = target.as_ref() {
363            if target.has_drag_enter_handler() {
364                proposal = target.handle_drag_enter(args.clone());
365            }
366        }
367    }
368    let Some(target) = target else {
369        session.set_current_effect(DragDropEffects::None);
370        return DragDropEffects::None;
371    };
372    if target.has_drag_over_handler() {
373        proposal = target.handle_drag_over(args);
374    } else if !target_changed && is_default_proposal(proposal) {
375        return session.current_effect();
376    }
377    let effect = normalize_effect(proposal.effect, session.allowed_effects);
378    session.set_current_effect(effect);
379    effect
380}
381
382fn resolve_drop_target(pointed_node: Option<NodeRef>) -> Option<NodeRef> {
383    let mut current = pointed_node;
384    while let Some(node) = current {
385        if node.allows_drop() {
386            return Some(node);
387        }
388        current = node.parent();
389    }
390    None
391}
392
393fn finish_session(
394    session: DragSession,
395    effect: DragDropEffects,
396    x: f32,
397    y: f32,
398    modifiers: u32,
399    notify_target_leave: bool,
400) {
401    let target = with_state(|state| {
402        let active = state.active_session.as_ref()?;
403        if !Rc::ptr_eq(&active.inner, &session.inner) {
404            return None;
405        }
406        let target = state.active_target.clone();
407        state.active_session = None;
408        state.active_target = None;
409        Some(target)
410    });
411    let Some(target) = target else {
412        return;
413    };
414    session.set_current_effect(effect);
415    if notify_target_leave {
416        if let Some(target) = target {
417            target.handle_drag_leave(DragEventArgs::new(session.clone(), x, y, modifiers));
418        }
419    }
420    session.complete(effect);
421    session.source().notify_drag_completed(effect);
422}