final version

This commit is contained in:
Jonathan Spira 2021-01-26 00:31:51 -08:00
parent 2e6956a3ce
commit 72007c06b6
2 changed files with 266 additions and 214 deletions

View File

@ -42,49 +42,27 @@ bitflags!(
} }
); );
/// A drag-drop source without any payload. Typically, when dragging and dropping data in Dear ImGui, /// Creates a source for drag drop data out of the last ID created.
/// a user will attach a payload to that drag and drop, so the accepter of the drop can read or
/// otherwise react to the drop. This struct attaches no data to the drag-drop, which means that an
/// accepter will simply be notified when the payload of a given type has been dropped.
///
/// This is still probably the most useful way in imgui-rs to handle payloads.
/// Using `once_cell` or some shared data, this pattern can be very powerful:
/// ///
/// ```no_run /// ```no_run
/// fn show_ui(ui: &Ui<'_>, drop_message: &mut Option<String>) { /// # use imgui::*;
/// ui.button(im_str!("Drag me!")); /// fn show_ui(ui: &Ui<'_>) {
/// /// ui.button(im_str!("Hello, I am a drag source!"), [0.0, 0.0]);
/// let drag_drop_name = im_str!("Test Drag");
/// ///
/// // drag drop SOURCE /// // Creates an empty DragSource with no tooltip
/// if DragDropSource::new(drag_drop_name).begin(ui).is_some() { /// DragDropSource::new(im_str!("BUTTON_DRAG")).begin(ui);
/// // warning -- this would allocate every frame if `DragDropSource` has
/// // condition `Always`, which it does by default. We're okay with that for
/// // this example, but real code probably wouldn't want to allocate so much.
/// *drop_message = Some("Test Payload".to_string());
/// }
///
/// ui.button(im_str!("Target me!"));
///
/// // drag drop TARGET
/// if let Some(target) = imgui::DragDropTarget::new(ui) {
/// if target
/// .accept_drag_drop_payload(drag_drop_name, DragDropFlags::empty())
/// .is_some()
/// {
/// let msg = drop_message.take().unwrap();
/// assert_eq!(msg, "Test Payload");
/// }
///
/// target.pop();
/// }
/// } /// }
/// ``` /// ```
/// ///
/// In the above, you'll see how the payload is really just a message passing service. /// Notice especially the `"BUTTON_DRAG"` name -- this is the identifier of this
/// This method will give a user an easier time than using [DragDropSourcePayloadPod] or /// DragDropSource; [DragDropTarget]'s will specify an identifier to *receive*, and these
/// the unsafe [DragDropSourcePayloadUnsafe], as the data for a user can be kept entirely in /// names must match up. A single item should only have one [DragDropSource], though
/// Rust and never has to do a roundtrip into C++, which can cause unexpected issues. /// a target may have multiple different targets.
///
/// DropDropSources don't do anything until you use one of the three `begin_` methods
/// on this struct. Each of these methods describes how you handle the Payload which ImGui
/// will manage, and then give to a [DragDropTarget], which will received the payload. The
/// simplest and safest Payload is the empty payload, created with [begin](Self::begin).
#[derive(Debug)] #[derive(Debug)]
pub struct DragDropSource<'a> { pub struct DragDropSource<'a> {
name: &'a ImStr, name: &'a ImStr,
@ -104,24 +82,6 @@ impl<'a> DragDropSource<'a> {
} }
} }
// /// Creates a new [DragDropSource] with no flags and the `Condition::Always` with the given name.
// /// ImGui refers to this `name` field as a `type`, but really it's just an identifier to match up
// /// Source/Target for DragDrop.
// ///
// /// This payload will be passed to ImGui, which will provide it to
// /// a target when it runs [accept_drag_drop_payload](DragDropTarget::accept_drag_drop_payload).
// ///
// /// ## Safety
// /// This function is not inherently unsafe, and won't panic itself, but using it opts you into
// /// managing the lifetime yourself. When you dereference the pointer given in [accept_drag_drop_payload](DragDropTarget::accept_drag_drop_payload),
// /// you can easily create memory safety problems.
// pub unsafe fn payload<T>(name: &'a ImStr, payload: *const T) -> Self {
// let mut output = Self::new(name);
// output.payload = payload as *const ffi::c_void;
// output.size = std::mem::size_of::<T>();
// output
// }
/// Sets the flags on the [DragDropSource]. Only the flags `SOURCE_NO_PREVIEW_TOOLTIP`, /// Sets the flags on the [DragDropSource]. Only the flags `SOURCE_NO_PREVIEW_TOOLTIP`,
/// `SOURCE_NO_DISABLE_HOVER`, `SOURCE_NO_HOLD_TO_OPEN_OTHERS`, `SOURCE_ALLOW_NULL_ID`, /// `SOURCE_NO_DISABLE_HOVER`, `SOURCE_NO_HOLD_TO_OPEN_OTHERS`, `SOURCE_ALLOW_NULL_ID`,
/// `SOURCE_EXTERN`, `SOURCE_AUTO_EXPIRE_PAYLOAD` make semantic sense, but any other flags will /// `SOURCE_EXTERN`, `SOURCE_AUTO_EXPIRE_PAYLOAD` make semantic sense, but any other flags will
@ -146,78 +106,112 @@ impl<'a> DragDropSource<'a> {
/// and this returned token does nothing. Additionally, a given target may use the flag /// and this returned token does nothing. Additionally, a given target may use the flag
/// `ACCEPT_NO_PREVIEW_TOOLTIP`, which will also prevent this tooltip from being shown. /// `ACCEPT_NO_PREVIEW_TOOLTIP`, which will also prevent this tooltip from being shown.
/// ///
/// For more information on how to use payload-less drag/drops, please see [DragDropSource]'s /// This drag has no payload, but is still probably the most useful way in imgui-rs to handle payloads.
/// documentation. /// Using `once_cell` or some shared data, this pattern can be very powerful:
pub fn begin<'ui>(self, _ui: &'ui Ui) -> Option<DragDropSourceToolTip<'ui>> { ///
let should_begin = unsafe { sys::igBeginDragDropSource(self.flags.bits() as i32) }; /// ```no_run
/// # use imgui::*;
if should_begin { /// fn show_ui(ui: &Ui<'_>, drop_message: &mut Option<String>) {
unsafe { /// ui.button(im_str!("Drag me!"), [0.0, 0.0]);
sys::igSetDragDropPayload(self.name.as_ptr(), ptr::null(), 0, self.cond as i32); ///
/// let drag_drop_name = im_str!("Test Drag");
Some(DragDropSourceToolTip::push()) ///
} /// // drag drop SOURCE
} else { /// if DragDropSource::new(drag_drop_name).begin(ui).is_some() {
None /// // warning -- this would allocate every frame if `DragDropSource` has
} /// // condition `Always`, which it does by default. We're okay with that for
/// // this example, but real code probably wouldn't want to allocate so much.
/// *drop_message = Some("Test Payload".to_string());
/// }
///
/// ui.button(im_str!("Target me!"), [0.0, 0.0]);
///
/// // drag drop TARGET
/// if let Some(target) = imgui::DragDropTarget::new(ui) {
/// if target
/// .accept_payload_empty(drag_drop_name, DragDropFlags::empty())
/// .is_some()
/// {
/// let msg = drop_message.take().unwrap();
/// assert_eq!(msg, "Test Payload");
/// }
///
/// target.pop();
/// }
/// }
/// ```
///
/// In the above, you'll see how the payload is really just a message passing service.
/// If you want to pass a simple integer or other "plain old data", take a look at
/// [begin_payload_pod](Self::begin_payload_pod).
pub fn begin<'ui>(self, ui: &Ui<'ui>) -> Option<DragDropSourceToolTip<'ui>> {
unsafe { self.begin_payload_unchecked(ui, ptr::null(), 0) }
} }
}
#[derive(Debug)] /// Creates the source of a drag and returns a handle on the tooltip.
pub struct DragDropSourcePayloadPod<'a, T> { /// This handle can be immediately dropped without binding it, in which case a default empty
name: &'a ImStr, /// circle will be used for the "blank" tooltip as this item is being dragged around.
payload: &'a T, ///
flags: DragDropFlags, /// Otherwise, use this tooltip to add data which will display as this item is dragged.
cond: Condition, /// If `SOURCE_NO_PREVIEW_TOOLTIP` is enabled, however, no preview will be displayed
} /// and this returned token does nothing. Additionally, a given target may use the flag
/// `ACCEPT_NO_PREVIEW_TOOLTIP`, which will also prevent this tooltip from being shown.
impl<'a, T: bytemuck::Pod> DragDropSourcePayloadPod<'a, T> { ///
/// Creates a new [DragDropSourcePayloadPod] with no flags and the `Condition::Always` with the given name. /// This function also takes a payload in the form of `T: bytemuck::Pod`. We use this bound to
/// ImGui refers to this `name` field as a `type`, but really it's just an identifier to match up /// ensure that we can safely send and receive the given type from C++. Integers are natively
/// Source/Target for DragDrop. /// supported by this operation already, but you'll need to implement `bytemuck::Pod` for your own
pub fn new(name: &'a ImStr, payload: &'a T) -> Self { /// types to use this method.
Self { pub fn begin_payload_pod<'ui, T: bytemuck::Pod>(
name, self,
flags: DragDropFlags::empty(), ui: &Ui<'ui>,
payload, payload: &T,
cond: Condition::Always, ) -> Option<DragDropSourceToolTip<'ui>> {
unsafe {
self.begin_payload_unchecked(
ui,
payload as *const _ as *const ffi::c_void,
std::mem::size_of::<T>(),
)
} }
} }
/// Sets the flags on the [DragDropSourcePayloadPod]. Only the flags `SOURCE_NO_PREVIEW_TOOLTIP`, /// Creates the source of a drag and returns a handle on the tooltip.
/// `SOURCE_NO_DISABLE_HOVER`, `SOURCE_NO_HOLD_TO_OPEN_OTHERS`, `SOURCE_ALLOW_NULL_ID`, /// This handle can be immediately dropped without binding it, in which case a default empty
/// `SOURCE_EXTERN`, `SOURCE_AUTO_EXPIRE_PAYLOAD` make semantic sense, but any other flags will /// circle will be used for the "blank" tooltip as this item is being dragged around.
/// be accepted without panic.
/// ///
/// Defaults to empty. /// Otherwise, use this tooltip to add data which will display as this item is dragged.
pub fn flags(mut self, flags: DragDropFlags) -> Self { /// If `SOURCE_NO_PREVIEW_TOOLTIP` is enabled, however, no preview will be displayed
self.flags = flags; /// and this returned token does nothing. Additionally, a given target may use the flag
self /// `ACCEPT_NO_PREVIEW_TOOLTIP`, which will also prevent this tooltip from being shown.
}
/// Sets the condition on the [DragDropSourcePayloadPod].
/// ///
/// Defaults to [Always](Condition::Always). /// This function also takes a payload of any `*const T`. Please avoid directly using it
pub fn condition(mut self, cond: Condition) -> Self { /// if you can.
self.cond = cond; ///
self /// ## Safety
} /// This function itself will not cause a panic, but using it directly opts you into
/// managing the lifetime of the pointer provided yourself. Dear ImGui will execute a memcpy on
/// When this returns true you need to: a) call SetDragDropPayload() exactly once, b) you may render the payload visual/description, c) call EndDragDropSource() /// the data passed in with the size (in bytes) given, but this is, of course, just a copy,
pub fn begin<'ui>(self, _ui: &'ui Ui) -> Option<DragDropSourceToolTip<'ui>> { /// so if you pass in an `&String`, for example, the underlying String data will not be cloned,
let should_begin = unsafe { sys::igBeginDragDropSource(self.flags.bits() as i32) }; /// and could easily dangle if the `String` is dropped.
///
/// Moreover, if `Condition::Always` is set (as it is by default), you will be copying in your data
/// every time this function is ran in your update loop, which if it involves an allocating and then
/// handing the allocation to ImGui, would result in a significant amount of data created.
///
/// Overall, users should be very sure that this function is needed before they reach for it, and instead
/// should consider either [begin_payload](Self::begin_payload) or [begin_payload_pod](Self::begin_payload_pod).
pub unsafe fn begin_payload_unchecked<'ui>(
&self,
_ui: &Ui<'ui>,
ptr: *const ffi::c_void,
size: usize,
) -> Option<DragDropSourceToolTip<'ui>> {
let should_begin = sys::igBeginDragDropSource(self.flags.bits() as i32);
if should_begin { if should_begin {
unsafe { sys::igSetDragDropPayload(self.name.as_ptr(), ptr, size, self.cond as i32);
sys::igSetDragDropPayload(
self.name.as_ptr(),
self.payload as *const _ as *const ffi::c_void,
std::mem::size_of::<T>(),
self.cond as i32,
);
Some(DragDropSourceToolTip::push()) Some(DragDropSourceToolTip::push())
}
} else { } else {
None None
} }
@ -246,53 +240,45 @@ impl Drop for DragDropSourceToolTip<'_> {
} }
} }
#[derive(Debug)] /// Creates a target for drag drop data out of the last ID created.
pub struct DragDropPayload { ///
/// Data which is copied and owned by ImGui. If you have accepted the payload, you can /// ```no_run
/// take ownership of the data; otherwise, view it immutably. Interacting with `data` is /// # use imgui::*;
/// very unsafe. /// fn show_ui(ui: &Ui<'_>) {
/// @fixme: this doesn't make a ton of sense. /// // Drop something on this button please!
pub data: *const ffi::c_void, /// ui.button(im_str!("Hello, I am a drag Target!"), [0.0, 0.0]);
/// Set when [`accept_drag_drop_payload`](Self::accept_drag_drop_payload) was called ///
/// and mouse has been hovering the target item (nb: handle overlapping drag targets). /// if let Some(target) = DragDropTarget::new(ui) {
/// @fixme: literally what does this mean -- I believe this is false on the first /// // accepting an empty payload (which is really just raising an event)
/// frame when source hovers over target and then is subsequently true? but I'm not sure /// if let Some(_payload_data) = target.accept_payload_empty(im_str!("BUTTON_DRAG"), DragDropFlags::empty()) {
/// when this matters. If DragDropFlags::ACCEPT_NO_PREVIEW is set, it doesn't make a difference /// println!("Nice job getting on the payload!");
/// to this flag. /// }
pub preview: bool, ///
/// // and we can accept multiple, different types of payloads with one drop target.
/// Set when AcceptDragDropPayload() was called and mouse button is released over the target item. /// // this is a good pattern for handling different kinds of drag/drop situations with
/// If this is set to false, then you set DragDropFlags::ACCEPT_BEFORE_DELIVERY and shouldn't /// // different kinds of data in them.
/// mess with `data` /// if let Some(Ok(payload_data)) = target.accept_payload_pod::<usize>(im_str!("BUTTON_ID"), DragDropFlags::empty()) {
/// @fixme: obviously this isn't an impressive implementation of ffi data mutability. /// println!("Our payload's data was {}", payload_data.data);
pub delivery: bool, /// }
} /// }
/// }
#[derive(Debug)] /// ```
pub struct DragDropEmptyPayload { ///
/// @fixme add docs. /// Notice especially the `"BUTTON_DRAG"` and `"BUTTON_ID"` name -- this is the identifier of this
pub preview: bool, /// DragDropTarget; [DragDropSource]s will specify an identifier when they send a payload, and these
/// names must match up. Notice how a target can have multiple acceptances on them -- this is a good
// @fixme add docs /// pattern to handle multiple kinds of data which could be passed around.
pub delivery: bool, ///
} /// DropDropTargets don't do anything until you use one of the three `accept_` methods
/// on this struct. Each of these methods will spit out a _Payload struct with an increasing
#[derive(Debug)] /// amount of information on the Payload. The absolute safest solution is [accept_payload_empty](Self::accept_payload_empty).
pub struct DragDropPodPayload<T: bytemuck::Pod> {
/// The kind data which was requested.
pub data: T,
/// @fixme add docs.
pub preview: bool,
// @fixme add docs
pub delivery: bool,
}
#[derive(Debug)] #[derive(Debug)]
pub struct DragDropTarget<'ui>(PhantomData<Ui<'ui>>); pub struct DragDropTarget<'ui>(PhantomData<Ui<'ui>>);
impl<'ui> DragDropTarget<'ui> { impl<'ui> DragDropTarget<'ui> {
/// Creates a new DragDropTarget, holding the [Ui]'s lifetime for the duration
/// of its existence. This is required since this struct runs some code on its Drop
/// to end the DragDropTarget code.
pub fn new(_ui: &Ui<'_>) -> Option<Self> { pub fn new(_ui: &Ui<'_>) -> Option<Self> {
let should_begin = unsafe { sys::igBeginDragDropTarget() }; let should_begin = unsafe { sys::igBeginDragDropTarget() };
if should_begin { if should_begin {
@ -302,80 +288,97 @@ impl<'ui> DragDropTarget<'ui> {
} }
} }
pub fn accept_empty_payload( /// Accepts an empty payload. This is the safest option for raising named events
/// in the DragDrop API. See [DragDropSource::begin] for more information on how you
/// might use this pattern.
pub fn accept_payload_empty(
&self, &self,
name: &ImStr, name: &ImStr,
flags: DragDropFlags, flags: DragDropFlags,
) -> Option<DragDropEmptyPayload> { ) -> Option<DragDropPayloadEmpty> {
unsafe { let output = unsafe { self.accept_payload_unchecked(name, flags) };
let inner = sys::igAcceptDragDropPayload(name.as_ptr(), flags.bits() as i32);
if inner.is_null() {
None
} else {
let inner = *inner;
Some(DragDropEmptyPayload { output.map(|unsafe_pod| DragDropPayloadEmpty {
preview: inner.Preview, preview: unsafe_pod.preview,
delivery: inner.Delivery, delivery: unsafe_pod.delivery,
}) })
}
}
} }
pub fn accept_pod_payload<T: bytemuck::Pod>( /// Accepts an payload with POD in it. This returns a Result, since you can specify any
/// type, which we will try to cast the data in, and give you a failure enum if it could
/// not be cast to it. Your data must implement `bytemuck::Pod` to use this method.
pub fn accept_payload_pod<T: bytemuck::Pod>(
&self, &self,
name: &ImStr, name: &ImStr,
flags: DragDropFlags, flags: DragDropFlags,
) -> Option<Result<DragDropPodPayload<T>, bytemuck::PodCastError>> { ) -> Option<Result<DragDropPayloadPod<T>, bytemuck::PodCastError>> {
unsafe { let output = unsafe { self.accept_payload_unchecked(name, flags) };
let inner = sys::igAcceptDragDropPayload(name.as_ptr(), flags.bits() as i32);
if inner.is_null() {
None
} else {
let inner = *inner;
let data =
std::slice::from_raw_parts(inner.Data as *const u8, inner.DataSize as usize);
Some( // convert the unsafe payload to our Result
bytemuck::try_from_bytes(data).map(|data| DragDropPodPayload { output.map(|unsafe_payload| {
data: *data, let data = unsafe {
preview: inner.Preview, std::slice::from_raw_parts(
delivery: inner.Delivery, unsafe_payload.data as *const u8,
}), unsafe_payload.size as usize,
) )
} };
}
// if we succeed, convert to PayloadPod
bytemuck::try_from_bytes(data).map(|data| DragDropPayloadPod {
data: *data,
preview: unsafe_payload.preview,
delivery: unsafe_payload.delivery,
})
})
} }
/// Accepts, popping the drag_drop payload, if it exists. If `DragDropFlags::ACCEPT_BEFORE_DELIVERY` is /// Accepts a drag and drop payload, and returns a [DragDropPayload] which
/// set, this function will return `Some` even if the type is wrong as long as there is a payload to accept. /// contains a raw pointer to [c_void](std::ffi::c_void) and a size in bytes.
/// How do we possibly handle communicating that this data is somewhat immutable? /// Users should generally avoid using this function if one of the safer variants
pub fn accept_drag_drop_payload( /// is acceptable.
///
/// ## Safety
///
/// Because this pointer comes from ImGui, absolutely no promises can be made on its
/// contents, alignment, or lifetime. Interacting with it is therefore extremely unsafe.
/// **Important:** a special note needs to be made to the [ACCEPT_BEFORE_DELIVERY] flag --
/// passing this flag will make this function return `Some(DragDropPayload)` **even before
/// the user has actually "dropped" the payload by release their mouse button.**
///
/// In safe functions, this works just fine, since the data can be freely copied
/// (or doesn't exist at all!). However, if you are working with your own data, you must
/// be extremely careful with this data, as you may, effectively, only have immutable access to it.
///
/// Moreover, if the `DragDropSource` has also used `Condition::Once` or similar when they sent the data,
/// ImGui will assume its data is still valid even after your preview, so corrupting that data could
/// lead to all sorts of unsafe behvaior on ImGui's side. In summary, using this function for any data
/// which isn't truly `Copy` or "plain old data" is difficult, and requires substantial knowledge
/// of the various edge cases.
pub unsafe fn accept_payload_unchecked(
&self, &self,
name: &ImStr, name: &ImStr,
flags: DragDropFlags, flags: DragDropFlags,
) -> Option<DragDropPayload> { ) -> Option<DragDropPayload> {
unsafe { let inner = sys::igAcceptDragDropPayload(name.as_ptr(), flags.bits() as i32);
let inner = sys::igAcceptDragDropPayload(name.as_ptr(), flags.bits() as i32); if inner.is_null() {
if inner.is_null() { None
None } else {
} else { let inner = *inner;
let inner = *inner;
// @fixme: there are actually other fields on `inner` which I have shorn -- they're // @fixme: there are actually other fields on `inner` which I have shorn -- they're
// considered internal to imgui (such as id of who sent this), so i've left it for // considered internal to imgui (such as id of who sent this), so i've left it for
// now this way. // now this way.
Some(DragDropPayload { Some(DragDropPayload {
data: inner.Data, data: inner.Data,
preview: inner.Preview, size: inner.DataSize,
delivery: inner.Delivery, preview: inner.Preview,
}) delivery: inner.Delivery,
} })
} }
} }
/// Ends the current target. Ironically, this doesn't really do /// Ends the current target. Ironically, this doesn't really do anything in ImGui
/// anything in ImGui or in imgui-rs, but it might in the future. /// or in imgui-rs, but it might in the future.
pub fn pop(self) { pub fn pop(self) {
// omitted...exists just to run Drop. // omitted...exists just to run Drop.
} }
@ -386,3 +389,52 @@ impl Drop for DragDropTarget<'_> {
unsafe { sys::igEndDragDropTarget() } unsafe { sys::igEndDragDropTarget() }
} }
} }
/// An empty DragDropPayload. It has no data in it, and just includes
/// two bools with status information.
#[derive(Debug)]
pub struct DragDropPayloadEmpty {
/// Set when [`accept_payload_empty`](Self::accept_payload_empty) was called
/// and mouse has been hovering the target item.
pub preview: bool,
/// Set when [`accept_payload_empty`](Self::accept_payload_empty) was
/// called and mouse button is released over the target item.
pub delivery: bool,
}
/// A DragDropPayload with status information and some POD, or plain old data,
/// in it.
#[derive(Debug)]
pub struct DragDropPayloadPod<T: bytemuck::Pod> {
/// The kind data which was requested.
pub data: T,
/// Set when [`accept_payload_pod`](Self::accept_payload_pod) was called
/// and mouse has been hovering the target item.
pub preview: bool,
/// Set when [`accept_payload_pod`](Self::accept_payload_pod) was
/// called and mouse button is released over the target item.
pub delivery: bool,
}
#[derive(Debug)]
pub struct DragDropPayload {
/// Data which is copied and owned by ImGui. If you have accepted the payload, you can
/// take ownership of the data; otherwise, view it immutably. Interacting with `data` is
/// very unsafe.
pub data: *const ffi::c_void,
/// The size of the data in bytes.
pub size: i32,
/// Set when [`accept_payload_unchecked`](Self::accept_payload_unchecked) was called
/// and mouse has been hovering the target item.
pub preview: bool,
/// Set when [`accept_payload_unchecked`](Self::accept_payload_unchecked) was
/// called and mouse button is released over the target item. If this is set to false, then you
/// set DragDropFlags::ACCEPT_BEFORE_DELIVERY and shouldn't mutate `data`.
pub delivery: bool,
}

View File

@ -10,7 +10,7 @@ use std::thread;
pub use self::clipboard::*; pub use self::clipboard::*;
pub use self::context::*; pub use self::context::*;
pub use self::drag_drop::*; pub use self::drag_drop::{DragDropFlags, DragDropSource, DragDropTarget};
pub use self::draw_list::{ChannelsSplit, DrawListMut, ImColor}; pub use self::draw_list::{ChannelsSplit, DrawListMut, ImColor};
pub use self::fonts::atlas::*; pub use self::fonts::atlas::*;
pub use self::fonts::font::*; pub use self::fonts::font::*;