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