egui_dnd/lib.rs
1#![doc = include_str!("../README.md")]
2#![forbid(unsafe_code)]
3#![warn(missing_docs)]
4
5use egui::{Id, Ui};
6pub use state::{DragDropConfig, DragDropItem, DragDropResponse, DragUpdate, Handle};
7
8pub use crate::item_iterator::ItemIterator;
9use crate::state::DragDropUi;
10use std::hash::Hash;
11
12mod item;
13mod item_iterator;
14mod state;
15/// Helper functions to support the drag and drop functionality
16pub mod utils;
17
18/// Helper struct for ease of use.
19pub struct Dnd<'a> {
20 id: Id,
21 ui: &'a mut Ui,
22 drag_drop_ui: DragDropUi,
23}
24
25/// Main entry point for the drag and drop functionality.
26/// Loads and saves it's state from egui memory.
27/// Use either [`Dnd::show`] or [`Dnd::show_vec`] to display the drag and drop UI.
28/// You can use [`Dnd::with_mouse_config`] or [`Dnd::with_touch_config`] to configure the drag detection.
29/// Example usage:
30/// ```rust no_run
31/// use std::hash::Hash;
32/// use eframe::egui;
33/// use egui::CentralPanel;
34/// use egui_dnd::dnd;
35///
36/// pub fn main() -> eframe::Result<()> {
37/// let mut items = vec!["alfred", "bernhard", "christian"];
38///
39/// eframe::run_simple_native("DnD Simple Example", Default::default(), move |ctx, _frame| {
40/// CentralPanel::default().show(ctx, |ui| {
41///
42/// dnd(ui, "dnd_example")
43/// .show_vec(&mut items, |ui, item, handle, state| {
44/// handle.ui(ui, |ui| {
45/// ui.label("drag");
46/// });
47/// ui.label(*item);
48/// });
49///
50/// });
51/// })
52/// }
53/// ```
54pub fn dnd(ui: &mut Ui, id_source: impl Hash) -> Dnd {
55 let id = Id::new(id_source).with("dnd");
56 let mut dnd_ui: DragDropUi =
57 ui.data_mut(|data| (*data.get_temp_mut_or_default::<DragDropUi>(id)).clone());
58
59 dnd_ui.return_animation_time = ui.style().animation_time;
60 dnd_ui.swap_animation_time = ui.style().animation_time;
61
62 Dnd {
63 id,
64 ui,
65 drag_drop_ui: dnd_ui,
66 }
67}
68
69impl<'a> Dnd<'a> {
70 /// Initialize the drag and drop UI. Same as [dnd].
71 pub fn new(ui: &'a mut Ui, id_source: impl Hash) -> Self {
72 dnd(ui, id_source)
73 }
74
75 /// Sets the config used when dragging with the mouse or when no touch config is set
76 #[must_use]
77 pub fn with_mouse_config(mut self, config: DragDropConfig) -> Self {
78 self.drag_drop_ui = self.drag_drop_ui.with_mouse_config(config);
79 self
80 }
81
82 /// Sets the config used when dragging with touch
83 /// If None, the mouse config is used instead
84 /// Use [`DragDropConfig::touch`] or [`DragDropConfig::touch_scroll`] to get a config optimized for touch
85 /// The default is [`DragDropConfig::touch`]
86 /// For dragging in a `ScrollArea`, use [`DragDropConfig::touch_scroll`]
87 #[must_use]
88 pub fn with_touch_config(mut self, config: Option<DragDropConfig>) -> Self {
89 self.drag_drop_ui = self.drag_drop_ui.with_touch_config(config);
90 self
91 }
92
93 /// Sets the animation time for the return animation (after dropping an item)
94 /// The default is the same as the egui animation time
95 /// If you want to disable the animation, set it to 0
96 #[must_use]
97 pub fn with_return_animation_time(mut self, animation_time: f32) -> Self {
98 self.drag_drop_ui.return_animation_time = animation_time;
99 self
100 }
101
102 /// Sets the animation time for the swap animation (when dragging an item over another item)
103 /// The default is the same as the egui animation time
104 /// If you want to disable the animation, set it to 0
105 #[must_use]
106 pub fn with_swap_animation_time(mut self, animation_time: f32) -> Self {
107 self.drag_drop_ui.swap_animation_time = animation_time;
108 self
109 }
110
111 /// Sets the animation time for all animations
112 /// The default is the same as the egui animation time
113 /// If you want to disable all animations, set it to 0
114 #[must_use]
115 pub fn with_animation_time(mut self, animation_time: f32) -> Self {
116 self.drag_drop_ui.return_animation_time = animation_time;
117 self.drag_drop_ui.swap_animation_time = animation_time;
118 self
119 }
120
121 /// Display the drag and drop UI.
122 /// `items` should be an iterator over items that should be sortable.
123 /// Each item needs to implement [`DragDropItem`]. This is automatically implement for every type that implements [Hash].
124 ///
125 /// It can also be implemented manually. **Each item needs to have a unique id.**
126 /// If you need to allow duplicate items in your list and cannot add a id field for some reason,
127 /// you can use the index as a id, but there are some limitations. Check the [index_as_id](https://github.com/lucasmerlin/hello_egui/blob/main/crates/egui_dnd/examples/index_as_id.rs) example.
128 ///
129 /// The items won't be updated automatically, but you can use [`Dnd::show_vec`] or [`DragDropResponse::update_vec`] to do so.
130 /// If your items aren't in a vec, you have to update the order yourself.
131 ///
132 /// `item_ui` is called for each item. Display your item there.
133 /// `item_ui` gets a [Handle] that can be used to display the drag handle.
134 /// Only the handle can be used to drag the item. If you want the whole item to be draggable, put everything in the handle.
135 pub fn show<T: DragDropItem>(
136 self,
137 items: impl Iterator<Item = T>,
138 mut item_ui: impl FnMut(&mut Ui, T, Handle, ItemState),
139 ) -> DragDropResponse {
140 #[allow(clippy::used_underscore_items)]
141 self._show_with_inner(|_id, ui, drag_drop_ui| {
142 drag_drop_ui.ui(ui, |ui, iter| {
143 items.enumerate().for_each(|(i, item)| {
144 iter.next(ui, item.id(), i, true, |ui, item_handle| {
145 item_handle.ui(ui, |ui, handle, state| item_ui(ui, item, handle, state))
146 });
147 });
148 })
149 })
150 }
151
152 /// Same as [`Dnd::show`], but with a fixed size for each item.
153 /// This allows items to be placed in a `horizontal_wrapped` ui.
154 /// For more info, look at the [horizontal example](https://github.com/lucasmerlin/hello_egui/blob/main/crates/egui_dnd/examples/horizontal.rs).
155 /// If you need even more control over the size, use [`Dnd::show_custom`] instead, where you
156 /// can individually size each item. See the `sort_words` example for an example.
157 pub fn show_sized<T: DragDropItem>(
158 self,
159 items: impl Iterator<Item = T>,
160 size: egui::Vec2,
161 mut item_ui: impl FnMut(&mut Ui, T, Handle, ItemState),
162 ) -> DragDropResponse {
163 #[allow(clippy::used_underscore_items)]
164 self._show_with_inner(|_id, ui, drag_drop_ui| {
165 drag_drop_ui.ui(ui, |ui, iter| {
166 items.enumerate().for_each(|(i, item)| {
167 iter.next(ui, item.id(), i, true, |ui, item_handle| {
168 item_handle.ui_sized(ui, size, |ui, handle, state| {
169 item_ui(ui, item, handle, state);
170 })
171 });
172 });
173 })
174 })
175 }
176
177 /// Same as [`Dnd::show`], but automatically sorts the items.
178 pub fn show_vec<T>(
179 self,
180 items: &mut [T],
181 item_ui: impl FnMut(&mut Ui, &mut T, Handle, ItemState),
182 ) -> DragDropResponse
183 where
184 for<'b> &'b mut T: DragDropItem,
185 {
186 let response = self.show(items.iter_mut(), item_ui);
187 response.update_vec(items);
188 response
189 }
190
191 /// Same as [`Dnd::show_sized`], but automatically sorts the items.
192 pub fn show_vec_sized<T: DragDropItem>(
193 self,
194 items: &mut [T],
195 size: egui::Vec2,
196 item_ui: impl FnMut(&mut Ui, &mut T, Handle, ItemState),
197 ) -> DragDropResponse
198 where
199 for<'b> &'b mut T: DragDropItem,
200 {
201 let response = self.show_sized(items.iter_mut(), size, item_ui);
202 response.update_vec(items);
203 response
204 }
205
206 /// This will allow for very flexible UI. You can use it to e.g. render outlines around items
207 /// or render items in complex layouts. This is **experimental**.
208 pub fn show_custom(self, f: impl FnOnce(&mut Ui, &mut ItemIterator)) -> DragDropResponse {
209 #[allow(clippy::used_underscore_items)]
210 self._show_with_inner(|_id, ui, drag_drop_ui| drag_drop_ui.ui(ui, f))
211 }
212
213 /// Same as [`Dnd::show_custom`], but automatically sorts the items.
214 pub fn show_custom_vec<T: DragDropItem>(
215 self,
216 items: &mut [T],
217 f: impl FnOnce(&mut Ui, &mut [T], &mut ItemIterator),
218 ) -> DragDropResponse {
219 let response = self.show_custom(|ui, iter| f(ui, items, iter));
220 response.update_vec(items);
221 response
222 }
223
224 fn _show_with_inner(
225 self,
226 inner_fn: impl FnOnce(Id, &mut Ui, &mut DragDropUi) -> DragDropResponse,
227 ) -> DragDropResponse {
228 let Dnd {
229 id,
230 ui,
231 mut drag_drop_ui,
232 } = self;
233
234 let response = inner_fn(id, ui, &mut drag_drop_ui);
235
236 ui.ctx().data_mut(|data| data.insert_temp(id, drag_drop_ui));
237
238 response
239 }
240}
241
242/// State of the current item.
243pub struct ItemState {
244 /// True if the item is currently being dragged.
245 pub dragged: bool,
246 /// Index of the item in the list.
247 /// Note that when you sort the source list while the drag is still ongoing (default behaviour
248 /// of [`Dnd::show_vec`]), this index will updated while the item is being dragged.
249 /// If you sort once after the item is dropped, the index will be stable during the drag.
250 pub index: usize,
251}