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
133/// Builder for creating drag drop sources
134///
135/// This struct is created by [`Ui::drag_drop_source_config`] and provides
136/// a fluent interface for configuring drag sources.
137#[derive(Debug)]
138pub struct DragDropSource<'ui, T> {
139 name: T,
140 flags: DragDropFlags,
141 cond: Condition,
142 ui: &'ui Ui,
143}
144
145impl<'ui, T: AsRef<str>> DragDropSource<'ui, T> {
146 /// Set flags for this drag source
147 ///
148 /// # Arguments
149 /// * `flags` - Combination of source-related `DragDropFlags`
150 #[inline]
151 pub fn flags(mut self, flags: DragDropFlags) -> Self {
152 self.flags = flags;
153 self
154 }
155
156 /// Set condition for when to update the payload
157 ///
158 /// # Arguments
159 /// * `cond` - When to update the payload data
160 #[inline]
161 pub fn condition(mut self, cond: Condition) -> Self {
162 self.cond = cond;
163 self
164 }
165
166 /// Begin drag source with empty payload
167 ///
168 /// This is the safest option for simple drag and drop operations.
169 /// Use shared state or other mechanisms to transfer actual data.
170 ///
171 /// Returns a tooltip token if dragging started, `None` otherwise.
172 #[inline]
173 pub fn begin(self) -> Option<DragDropSourceTooltip<'ui>> {
174 self.begin_payload(())
175 }
176
177 /// Begin drag source with typed payload
178 ///
179 /// The payload data will be copied and managed by ImGui.
180 /// The data must be `Copy + 'static` for safety.
181 ///
182 /// # Arguments
183 /// * `payload` - Data to transfer (must be Copy + 'static)
184 ///
185 /// Returns a tooltip token if dragging started, `None` otherwise.
186 #[inline]
187 pub fn begin_payload<P: Copy + 'static>(
188 self,
189 payload: P,
190 ) -> Option<DragDropSourceTooltip<'ui>> {
191 unsafe {
192 let payload = make_typed_payload(payload);
193 self.begin_payload_unchecked(
194 &payload as *const _ as *const ffi::c_void,
195 std::mem::size_of::<TypedPayload<P>>(),
196 )
197 }
198 }
199
200 /// Begin drag source with raw payload data (unsafe)
201 ///
202 /// # Safety
203 /// The caller must ensure:
204 /// - `ptr` points to valid data of `size` bytes
205 /// - The data remains valid for the duration of the drag operation
206 /// - The data layout matches what targets expect
207 ///
208 /// # Arguments
209 /// * `ptr` - Pointer to payload data
210 /// * `size` - Size of payload data in bytes
211 pub unsafe fn begin_payload_unchecked(
212 &self,
213 ptr: *const ffi::c_void,
214 size: usize,
215 ) -> Option<DragDropSourceTooltip<'ui>> {
216 unsafe {
217 let should_begin = sys::igBeginDragDropSource(self.flags.bits() as i32);
218
219 if should_begin {
220 sys::igSetDragDropPayload(
221 self.ui.scratch_txt(&self.name),
222 ptr,
223 size,
224 self.cond as i32,
225 );
226
227 Some(DragDropSourceTooltip::new(self.ui))
228 } else {
229 None
230 }
231 }
232 }
233}
234
235/// Token representing an active drag source tooltip
236///
237/// While this token exists, you can add UI elements that will be shown
238/// as a tooltip during the drag operation.
239#[derive(Debug)]
240pub struct DragDropSourceTooltip<'ui> {
241 _ui: &'ui Ui,
242}
243
244impl<'ui> DragDropSourceTooltip<'ui> {
245 fn new(ui: &'ui Ui) -> Self {
246 Self { _ui: ui }
247 }
248
249 /// End the drag source tooltip manually
250 ///
251 /// This is called automatically when the token is dropped.
252 pub fn end(self) {
253 // Drop will handle cleanup
254 }
255}
256
257impl Drop for DragDropSourceTooltip<'_> {
258 fn drop(&mut self) {
259 unsafe {
260 sys::igEndDragDropSource();
261 }
262 }
263}
264
265/// Drag drop target for accepting payloads
266///
267/// This struct is created by [`Ui::drag_drop_target`] and provides
268/// methods for accepting different types of payloads.
269#[derive(Debug)]
270pub struct DragDropTarget<'ui>(&'ui Ui);
271
272impl<'ui> DragDropTarget<'ui> {
273 /// Accept an empty payload
274 ///
275 /// This is the safest option for drag and drop operations.
276 /// Use this when you only need to know that a drop occurred,
277 /// not transfer actual data.
278 ///
279 /// # Arguments
280 /// * `name` - Payload type name (must match source name)
281 /// * `flags` - Accept flags
282 ///
283 /// Returns payload info if accepted, `None` otherwise.
284 pub fn accept_payload_empty(
285 &self,
286 name: impl AsRef<str>,
287 flags: DragDropFlags,
288 ) -> Option<DragDropPayloadEmpty> {
289 self.accept_payload(name, flags)?
290 .ok()
291 .map(|payload_pod: DragDropPayloadPod<()>| DragDropPayloadEmpty {
292 preview: payload_pod.preview,
293 delivery: payload_pod.delivery,
294 })
295 }
296
297 /// Accept a typed payload
298 ///
299 /// Attempts to accept a payload with the specified type.
300 /// Returns `Ok(payload)` if the type matches, `Err(PayloadIsWrongType)` if not.
301 ///
302 /// # Arguments
303 /// * `name` - Payload type name (must match source name)
304 /// * `flags` - Accept flags
305 ///
306 /// Returns `Some(Result<payload, error>)` if payload exists, `None` otherwise.
307 pub fn accept_payload<T: 'static + Copy, Name: AsRef<str>>(
308 &self,
309 name: Name,
310 flags: DragDropFlags,
311 ) -> Option<Result<DragDropPayloadPod<T>, PayloadIsWrongType>> {
312 let output = unsafe { self.accept_payload_unchecked(name, flags) };
313
314 output.map(|payload| {
315 if payload.data.is_null() || payload.size < std::mem::size_of::<TypedPayload<T>>() {
316 return Err(PayloadIsWrongType);
317 }
318
319 // Dear ImGui stores payload data in an unaligned byte buffer, so always read unaligned.
320 let typed_payload: TypedPayload<T> =
321 unsafe { std::ptr::read_unaligned(payload.data as *const TypedPayload<T>) };
322
323 if typed_payload.type_id == any::TypeId::of::<T>() {
324 Ok(DragDropPayloadPod {
325 data: typed_payload.data,
326 preview: payload.preview,
327 delivery: payload.delivery,
328 })
329 } else {
330 Err(PayloadIsWrongType)
331 }
332 })
333 }
334
335 /// Accept raw payload data (unsafe)
336 ///
337 /// # Safety
338 /// The returned pointer and size are managed by ImGui and may become
339 /// invalid at any time. The caller must not access the data after
340 /// the drag operation completes.
341 ///
342 /// # Arguments
343 /// * `name` - Payload type name
344 /// * `flags` - Accept flags
345 pub unsafe fn accept_payload_unchecked(
346 &self,
347 name: impl AsRef<str>,
348 flags: DragDropFlags,
349 ) -> Option<DragDropPayload> {
350 let inner =
351 unsafe { sys::igAcceptDragDropPayload(self.0.scratch_txt(name), flags.bits() as i32) };
352
353 if inner.is_null() {
354 None
355 } else {
356 let inner = unsafe { *inner };
357 let size = if inner.DataSize <= 0 || inner.Data.is_null() {
358 0
359 } else {
360 inner.DataSize as usize
361 };
362 Some(DragDropPayload {
363 data: inner.Data,
364 size,
365 preview: inner.Preview,
366 delivery: inner.Delivery,
367 })
368 }
369 }
370
371 /// End the drag drop target
372 ///
373 /// This is called automatically when the token is dropped.
374 pub fn pop(self) {
375 // Drop will handle cleanup
376 }
377}
378
379impl Drop for DragDropTarget<'_> {
380 fn drop(&mut self) {
381 unsafe {
382 sys::igEndDragDropTarget();
383 }
384 }
385}
386
387// Payload types and utilities
388
389/// Wrapper for typed payloads with runtime type checking.
390///
391/// Important: payload memory is copied and stored by Dear ImGui in an unaligned byte buffer.
392/// Never take `&TypedPayload<T>` from the raw pointer returned by `AcceptDragDropPayload()`.
393/// Always copy out using `ptr::read_unaligned`.
394#[repr(C)]
395#[derive(Copy, Clone)]
396struct TypedPayload<T: Copy> {
397 type_id: any::TypeId,
398 data: T,
399}
400
401fn make_typed_payload<T: Copy + 'static>(data: T) -> TypedPayload<T> {
402 // Ensure we do not pass uninitialized padding bytes across the C++ boundary.
403 let mut out = std::mem::MaybeUninit::<TypedPayload<T>>::zeroed();
404 unsafe {
405 let ptr = out.as_mut_ptr();
406 std::ptr::addr_of_mut!((*ptr).type_id).write(any::TypeId::of::<T>());
407 std::ptr::addr_of_mut!((*ptr).data).write(data);
408 out.assume_init()
409 }
410}
411
412/// Empty payload (no data, just notification)
413#[derive(Debug, Clone, Copy)]
414pub struct DragDropPayloadEmpty {
415 /// True when hovering over target
416 pub preview: bool,
417 /// True when payload should be delivered
418 pub delivery: bool,
419}
420
421/// Typed payload with data
422#[derive(Debug, Clone, Copy)]
423pub struct DragDropPayloadPod<T> {
424 /// The payload data
425 pub data: T,
426 /// True when hovering over target
427 pub preview: bool,
428 /// True when payload should be delivered
429 pub delivery: bool,
430}
431
432/// Raw payload data
433#[derive(Debug)]
434pub struct DragDropPayload {
435 /// Pointer to payload data (managed by ImGui)
436 pub data: *const ffi::c_void,
437 /// Size of payload data in bytes
438 pub size: usize,
439 /// True when hovering over target
440 pub preview: bool,
441 /// True when payload should be delivered
442 pub delivery: bool,
443}
444
445/// Error type for payload type mismatches
446#[derive(Debug, Clone, Copy, PartialEq, Eq)]
447pub struct PayloadIsWrongType;
448
449impl std::fmt::Display for PayloadIsWrongType {
450 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
451 write!(f, "drag drop payload has wrong type")
452 }
453}
454
455impl std::error::Error for PayloadIsWrongType {}
456
457#[cfg(test)]
458mod tests {
459 use super::*;
460
461 fn payload_bytes<T: Copy + 'static>(value: T) -> Vec<u8> {
462 let payload = make_typed_payload(value);
463 let size = std::mem::size_of::<TypedPayload<T>>();
464 let mut out = vec![0u8; size];
465 unsafe {
466 std::ptr::copy_nonoverlapping(
467 std::ptr::from_ref(&payload).cast::<u8>(),
468 out.as_mut_ptr(),
469 size,
470 );
471 }
472 out
473 }
474
475 #[test]
476 fn typed_payload_bytes_are_deterministic() {
477 // If we accidentally leak uninitialized padding bytes, these can become nondeterministic.
478 assert_eq!(payload_bytes(7u8), payload_bytes(7u8));
479 assert_eq!(payload_bytes(0x1122_3344u32), payload_bytes(0x1122_3344u32));
480 }
481
482 #[test]
483 fn typed_payload_can_be_read_unaligned() {
484 let bytes = payload_bytes(7u8);
485 let mut buf = vec![0u8; 1 + bytes.len()];
486 buf[1..].copy_from_slice(&bytes);
487 let ptr = unsafe { buf.as_ptr().add(1) } as *const TypedPayload<u8>;
488 let decoded = unsafe { std::ptr::read_unaligned(ptr) };
489 assert_eq!(decoded.type_id, any::TypeId::of::<u8>());
490 assert_eq!(decoded.data, 7u8);
491 }
492}