Skip to main content

dear_imgui_rs/
drag_drop.rs

1#![allow(
2    clippy::cast_possible_truncation,
3    clippy::cast_sign_loss,
4    clippy::as_conversions,
5    // We intentionally keep explicit casts for FFI clarity; avoid auto-fix churn.
6    clippy::unnecessary_cast
7)]
8//! Drag and Drop functionality for Dear ImGui
9//!
10//! This module provides a complete drag and drop system that allows users to transfer
11//! data between UI elements. The system consists of drag sources and drop targets,
12//! with type-safe payload management.
13//!
14//! # Basic Usage
15//!
16//! ```no_run
17//! # use dear_imgui_rs::*;
18//! # let mut ctx = Context::create();
19//! # let ui = ctx.frame();
20//! // Create a drag source
21//! ui.button("Drag me!");
22//! if let Some(source) = ui.drag_drop_source_config("MY_DATA").begin() {
23//!     ui.text("Dragging...");
24//!     source.end();
25//! }
26//!
27//! // Create a drop target
28//! ui.button("Drop here!");
29//! if let Some(target) = ui.drag_drop_target() {
30//!     if target.accept_payload_empty("MY_DATA", DragDropFlags::empty()).is_some() {
31//!         println!("Data dropped!");
32//!     }
33//!     target.pop();
34//! }
35//! ```
36
37use crate::{Condition, Ui, sys};
38use std::{any, ffi};
39
40bitflags::bitflags! {
41    /// Flags for drag and drop operations
42    #[repr(transparent)]
43    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44    pub struct DragDropFlags: u32 {
45        /// No flags
46        const NONE = 0;
47
48        // Source flags
49        /// Disable preview tooltip during drag
50        const SOURCE_NO_PREVIEW_TOOLTIP = sys::ImGuiDragDropFlags_SourceNoPreviewTooltip as u32;
51        /// Don't disable hover during drag
52        const SOURCE_NO_DISABLE_HOVER = sys::ImGuiDragDropFlags_SourceNoDisableHover as u32;
53        /// Don't open tree nodes/headers when hovering during drag
54        const SOURCE_NO_HOLD_TO_OPEN_OTHERS = sys::ImGuiDragDropFlags_SourceNoHoldToOpenOthers as u32;
55        /// Allow items without unique ID to be drag sources
56        const SOURCE_ALLOW_NULL_ID = sys::ImGuiDragDropFlags_SourceAllowNullID as u32;
57        /// External drag source (from outside ImGui)
58        const SOURCE_EXTERN = sys::ImGuiDragDropFlags_SourceExtern as u32;
59        /// Automatically expire payload if source stops being submitted
60        const PAYLOAD_AUTO_EXPIRE = sys::ImGuiDragDropFlags_PayloadAutoExpire as u32;
61
62        // Target flags
63        /// Accept payload before mouse button is released
64        const ACCEPT_BEFORE_DELIVERY = sys::ImGuiDragDropFlags_AcceptBeforeDelivery as u32;
65        /// Don't draw default highlight rectangle when hovering
66        const ACCEPT_NO_DRAW_DEFAULT_RECT = sys::ImGuiDragDropFlags_AcceptNoDrawDefaultRect as u32;
67        /// Don't show preview tooltip from source
68        const ACCEPT_NO_PREVIEW_TOOLTIP = sys::ImGuiDragDropFlags_AcceptNoPreviewTooltip as u32;
69        /// Render accepting target as hovered (e.g. allow Button() as drop target)
70        const ACCEPT_DRAW_AS_HOVERED = sys::ImGuiDragDropFlags_AcceptDrawAsHovered as u32;
71        /// Convenience flag for peeking (ACCEPT_BEFORE_DELIVERY | ACCEPT_NO_DRAW_DEFAULT_RECT)
72        const ACCEPT_PEEK_ONLY = sys::ImGuiDragDropFlags_AcceptPeekOnly as u32;
73    }
74}
75
76impl Ui {
77    /// Creates a new drag drop source configuration
78    ///
79    /// # Arguments
80    /// * `name` - Identifier for this drag source (must match target name)
81    ///
82    /// # Example
83    /// ```no_run
84    /// # use dear_imgui_rs::*;
85    /// # let mut ctx = Context::create();
86    /// # let ui = ctx.frame();
87    /// ui.button("Drag me!");
88    /// if let Some(source) = ui.drag_drop_source_config("MY_DATA")
89    ///     .flags(DragDropFlags::SOURCE_NO_PREVIEW_TOOLTIP)
90    ///     .begin() {
91    ///     ui.text("Custom drag tooltip");
92    ///     source.end();
93    /// }
94    /// ```
95    pub fn drag_drop_source_config<T: AsRef<str>>(&self, name: T) -> DragDropSource<'_, T> {
96        DragDropSource {
97            name,
98            flags: DragDropFlags::NONE,
99            cond: Condition::Always,
100            ui: self,
101        }
102    }
103
104    /// Creates a drag drop target for the last item
105    ///
106    /// Returns `Some(DragDropTarget)` if the last item can accept drops,
107    /// `None` otherwise.
108    ///
109    /// # Example
110    /// ```no_run
111    /// # use dear_imgui_rs::*;
112    /// # let mut ctx = Context::create();
113    /// # let ui = ctx.frame();
114    /// ui.button("Drop target");
115    /// if let Some(target) = ui.drag_drop_target() {
116    ///     if target.accept_payload_empty("MY_DATA", DragDropFlags::NONE).is_some() {
117    ///         println!("Received drop!");
118    ///     }
119    ///     target.pop();
120    /// }
121    /// ```
122    #[doc(alias = "BeginDragDropTarget")]
123    pub fn drag_drop_target(&self) -> Option<DragDropTarget<'_>> {
124        let should_begin = unsafe { sys::igBeginDragDropTarget() };
125        if should_begin {
126            Some(DragDropTarget(self))
127        } else {
128            None
129        }
130    }
131
132    /// Returns the current drag and drop payload, if any.
133    ///
134    /// This is a convenience wrapper over `ImGui::GetDragDropPayload`.
135    ///
136    /// The returned payload is owned and managed by Dear ImGui and may become invalid
137    /// after the drag operation completes. Do not cache it beyond the current frame.
138    #[doc(alias = "GetDragDropPayload")]
139    pub fn drag_drop_payload(&self) -> Option<DragDropPayload> {
140        unsafe {
141            let ptr = sys::igGetDragDropPayload();
142            if ptr.is_null() {
143                return None;
144            }
145            let inner = *ptr;
146            let size = if inner.DataSize <= 0 || inner.Data.is_null() {
147                0
148            } else {
149                inner.DataSize as usize
150            };
151            Some(DragDropPayload {
152                data: inner.Data,
153                size,
154                preview: inner.Preview,
155                delivery: inner.Delivery,
156            })
157        }
158    }
159}
160
161/// Builder for creating drag drop sources
162///
163/// This struct is created by [`Ui::drag_drop_source_config`] and provides
164/// a fluent interface for configuring drag sources.
165#[derive(Debug)]
166pub struct DragDropSource<'ui, T> {
167    name: T,
168    flags: DragDropFlags,
169    cond: Condition,
170    ui: &'ui Ui,
171}
172
173impl<'ui, T: AsRef<str>> DragDropSource<'ui, T> {
174    /// Set flags for this drag source
175    ///
176    /// # Arguments
177    /// * `flags` - Combination of source-related `DragDropFlags`
178    #[inline]
179    pub fn flags(mut self, flags: DragDropFlags) -> Self {
180        self.flags = flags;
181        self
182    }
183
184    /// Set condition for when to update the payload
185    ///
186    /// # Arguments
187    /// * `cond` - When to update the payload data
188    #[inline]
189    pub fn condition(mut self, cond: Condition) -> Self {
190        self.cond = cond;
191        self
192    }
193
194    /// Begin drag source with empty payload
195    ///
196    /// This is the safest option for simple drag and drop operations.
197    /// Use shared state or other mechanisms to transfer actual data.
198    ///
199    /// Returns a tooltip token if dragging started, `None` otherwise.
200    #[inline]
201    pub fn begin(self) -> Option<DragDropSourceTooltip<'ui>> {
202        self.begin_payload(())
203    }
204
205    /// Begin drag source with typed payload
206    ///
207    /// The payload data will be copied and managed by ImGui.
208    /// The data must be `Copy + 'static` for safety.
209    ///
210    /// # Arguments
211    /// * `payload` - Data to transfer (must be Copy + 'static)
212    ///
213    /// Returns a tooltip token if dragging started, `None` otherwise.
214    #[inline]
215    pub fn begin_payload<P: Copy + 'static>(
216        self,
217        payload: P,
218    ) -> Option<DragDropSourceTooltip<'ui>> {
219        unsafe {
220            let payload = make_typed_payload(payload);
221            self.begin_payload_unchecked(
222                &payload as *const _ as *const ffi::c_void,
223                std::mem::size_of::<TypedPayload<P>>(),
224            )
225        }
226    }
227
228    /// Begin drag source with raw payload data (unsafe)
229    ///
230    /// # Safety
231    /// The caller must ensure:
232    /// - `ptr` points to valid data of `size` bytes
233    /// - The data remains valid for the duration of the drag operation
234    /// - The data layout matches what targets expect
235    ///
236    /// # Arguments
237    /// * `ptr` - Pointer to payload data
238    /// * `size` - Size of payload data in bytes
239    pub unsafe fn begin_payload_unchecked(
240        &self,
241        ptr: *const ffi::c_void,
242        size: usize,
243    ) -> Option<DragDropSourceTooltip<'ui>> {
244        unsafe {
245            let should_begin = sys::igBeginDragDropSource(self.flags.bits() as i32);
246
247            if should_begin {
248                sys::igSetDragDropPayload(
249                    self.ui.scratch_txt(&self.name),
250                    ptr,
251                    size,
252                    self.cond as i32,
253                );
254
255                Some(DragDropSourceTooltip::new(self.ui))
256            } else {
257                None
258            }
259        }
260    }
261}
262
263/// Token representing an active drag source tooltip
264///
265/// While this token exists, you can add UI elements that will be shown
266/// as a tooltip during the drag operation.
267#[derive(Debug)]
268pub struct DragDropSourceTooltip<'ui> {
269    _ui: &'ui Ui,
270}
271
272impl<'ui> DragDropSourceTooltip<'ui> {
273    fn new(ui: &'ui Ui) -> Self {
274        Self { _ui: ui }
275    }
276
277    /// End the drag source tooltip manually
278    ///
279    /// This is called automatically when the token is dropped.
280    pub fn end(self) {
281        // Drop will handle cleanup
282    }
283}
284
285impl Drop for DragDropSourceTooltip<'_> {
286    fn drop(&mut self) {
287        unsafe {
288            sys::igEndDragDropSource();
289        }
290    }
291}
292
293/// Drag drop target for accepting payloads
294///
295/// This struct is created by [`Ui::drag_drop_target`] and provides
296/// methods for accepting different types of payloads.
297#[derive(Debug)]
298pub struct DragDropTarget<'ui>(&'ui Ui);
299
300impl<'ui> DragDropTarget<'ui> {
301    /// Accept an empty payload
302    ///
303    /// This is the safest option for drag and drop operations.
304    /// Use this when you only need to know that a drop occurred,
305    /// not transfer actual data.
306    ///
307    /// # Arguments
308    /// * `name` - Payload type name (must match source name)
309    /// * `flags` - Accept flags
310    ///
311    /// Returns payload info if accepted, `None` otherwise.
312    pub fn accept_payload_empty(
313        &self,
314        name: impl AsRef<str>,
315        flags: DragDropFlags,
316    ) -> Option<DragDropPayloadEmpty> {
317        self.accept_payload(name, flags)?
318            .ok()
319            .map(|payload_pod: DragDropPayloadPod<()>| DragDropPayloadEmpty {
320                preview: payload_pod.preview,
321                delivery: payload_pod.delivery,
322            })
323    }
324
325    /// Accept a typed payload
326    ///
327    /// Attempts to accept a payload with the specified type.
328    /// Returns `Ok(payload)` if the type matches, `Err(PayloadIsWrongType)` if not.
329    ///
330    /// # Arguments
331    /// * `name` - Payload type name (must match source name)
332    /// * `flags` - Accept flags
333    ///
334    /// Returns `Some(Result<payload, error>)` if payload exists, `None` otherwise.
335    pub fn accept_payload<T: 'static + Copy, Name: AsRef<str>>(
336        &self,
337        name: Name,
338        flags: DragDropFlags,
339    ) -> Option<Result<DragDropPayloadPod<T>, PayloadIsWrongType>> {
340        let output = unsafe { self.accept_payload_unchecked(name, flags) };
341
342        output.map(|payload| {
343            if payload.data.is_null() || payload.size < std::mem::size_of::<TypedPayload<T>>() {
344                return Err(PayloadIsWrongType);
345            }
346
347            // Dear ImGui stores payload data in an unaligned byte buffer, so always read unaligned.
348            let typed_payload: TypedPayload<T> =
349                unsafe { std::ptr::read_unaligned(payload.data as *const TypedPayload<T>) };
350
351            if typed_payload.type_id == any::TypeId::of::<T>() {
352                Ok(DragDropPayloadPod {
353                    data: typed_payload.data,
354                    preview: payload.preview,
355                    delivery: payload.delivery,
356                })
357            } else {
358                Err(PayloadIsWrongType)
359            }
360        })
361    }
362
363    /// Accept raw payload data (unsafe)
364    ///
365    /// # Safety
366    /// The returned pointer and size are managed by ImGui and may become
367    /// invalid at any time. The caller must not access the data after
368    /// the drag operation completes.
369    ///
370    /// # Arguments
371    /// * `name` - Payload type name
372    /// * `flags` - Accept flags
373    pub unsafe fn accept_payload_unchecked(
374        &self,
375        name: impl AsRef<str>,
376        flags: DragDropFlags,
377    ) -> Option<DragDropPayload> {
378        let inner =
379            unsafe { sys::igAcceptDragDropPayload(self.0.scratch_txt(name), flags.bits() as i32) };
380
381        if inner.is_null() {
382            None
383        } else {
384            let inner = unsafe { *inner };
385            let size = if inner.DataSize <= 0 || inner.Data.is_null() {
386                0
387            } else {
388                inner.DataSize as usize
389            };
390            Some(DragDropPayload {
391                data: inner.Data,
392                size,
393                preview: inner.Preview,
394                delivery: inner.Delivery,
395            })
396        }
397    }
398
399    /// End the drag drop target
400    ///
401    /// This is called automatically when the token is dropped.
402    pub fn pop(self) {
403        // Drop will handle cleanup
404    }
405}
406
407impl Drop for DragDropTarget<'_> {
408    fn drop(&mut self) {
409        unsafe {
410            sys::igEndDragDropTarget();
411        }
412    }
413}
414
415// Payload types and utilities
416
417/// Wrapper for typed payloads with runtime type checking.
418///
419/// Important: payload memory is copied and stored by Dear ImGui in an unaligned byte buffer.
420/// Never take `&TypedPayload<T>` from the raw pointer returned by `AcceptDragDropPayload()`.
421/// Always copy out using `ptr::read_unaligned`.
422#[repr(C)]
423#[derive(Copy, Clone)]
424struct TypedPayload<T: Copy> {
425    type_id: any::TypeId,
426    data: T,
427}
428
429fn make_typed_payload<T: Copy + 'static>(data: T) -> TypedPayload<T> {
430    // Ensure we do not pass uninitialized padding bytes across the C++ boundary.
431    let mut out = std::mem::MaybeUninit::<TypedPayload<T>>::zeroed();
432    unsafe {
433        let ptr = out.as_mut_ptr();
434        std::ptr::addr_of_mut!((*ptr).type_id).write(any::TypeId::of::<T>());
435        std::ptr::addr_of_mut!((*ptr).data).write(data);
436        out.assume_init()
437    }
438}
439
440/// Empty payload (no data, just notification)
441#[derive(Debug, Clone, Copy)]
442pub struct DragDropPayloadEmpty {
443    /// True when hovering over target
444    pub preview: bool,
445    /// True when payload should be delivered
446    pub delivery: bool,
447}
448
449/// Typed payload with data
450#[derive(Debug, Clone, Copy)]
451pub struct DragDropPayloadPod<T> {
452    /// The payload data
453    pub data: T,
454    /// True when hovering over target
455    pub preview: bool,
456    /// True when payload should be delivered
457    pub delivery: bool,
458}
459
460/// Raw payload data
461#[derive(Debug)]
462pub struct DragDropPayload {
463    /// Pointer to payload data (managed by ImGui)
464    pub data: *const ffi::c_void,
465    /// Size of payload data in bytes
466    pub size: usize,
467    /// True when hovering over target
468    pub preview: bool,
469    /// True when payload should be delivered
470    pub delivery: bool,
471}
472
473/// Error type for payload type mismatches
474#[derive(Debug, Clone, Copy, PartialEq, Eq)]
475pub struct PayloadIsWrongType;
476
477impl std::fmt::Display for PayloadIsWrongType {
478    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
479        write!(f, "drag drop payload has wrong type")
480    }
481}
482
483impl std::error::Error for PayloadIsWrongType {}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488
489    fn payload_bytes<T: Copy + 'static>(value: T) -> Vec<u8> {
490        let payload = make_typed_payload(value);
491        let size = std::mem::size_of::<TypedPayload<T>>();
492        let mut out = vec![0u8; size];
493        unsafe {
494            std::ptr::copy_nonoverlapping(
495                std::ptr::from_ref(&payload).cast::<u8>(),
496                out.as_mut_ptr(),
497                size,
498            );
499        }
500        out
501    }
502
503    #[test]
504    fn typed_payload_bytes_are_deterministic() {
505        // If we accidentally leak uninitialized padding bytes, these can become nondeterministic.
506        assert_eq!(payload_bytes(7u8), payload_bytes(7u8));
507        assert_eq!(payload_bytes(0x1122_3344u32), payload_bytes(0x1122_3344u32));
508    }
509
510    #[test]
511    fn typed_payload_can_be_read_unaligned() {
512        let bytes = payload_bytes(7u8);
513        let mut buf = vec![0u8; 1 + bytes.len()];
514        buf[1..].copy_from_slice(&bytes);
515        let ptr = unsafe { buf.as_ptr().add(1) } as *const TypedPayload<u8>;
516        let decoded = unsafe { std::ptr::read_unaligned(ptr) };
517        assert_eq!(decoded.type_id, any::TypeId::of::<u8>());
518        assert_eq!(decoded.data, 7u8);
519    }
520}