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}