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