Skip to main content

loam_egui/
dnd.rs

1use egui::{
2    self, emath::TSTransform, vec2, DragAndDrop, Id, LayerId, Order, Rect, Response, Sense, Ui,
3    UiBuilder,
4};
5
6pub fn drag_source_collapsing<P>(
7    ui: &mut Ui,
8    id: Id,
9    payload: P,
10    body: impl FnOnce(&mut Ui),
11) -> Response
12where
13    P: 'static + Send + Sync,
14{
15    let ctx = ui.ctx().clone();
16    let is_dragged = ctx.is_being_dragged(id);
17    if !is_dragged {
18        return ui.dnd_drag_source(id, payload, body).response;
19    }
20    DragAndDrop::set_payload(&ctx, payload);
21    let layer_id = LayerId::new(Order::Tooltip, id);
22    let mut child = ui.new_child(UiBuilder::new().layer_id(layer_id));
23    body(&mut child);
24    let body_rect = child.min_rect();
25    if let Some(pos) = ctx.pointer_interact_pos() {
26        let delta = pos - body_rect.center();
27        ctx.transform_layer_shapes(layer_id, TSTransform::from_translation(delta));
28    }
29    ui.interact(body_rect, id, Sense::hover())
30}
31
32/// `true` on a pointer release over the targeted gap.
33pub fn make_room_gap(
34    ui: &mut Ui,
35    is_target: bool,
36    slot_id: Id,
37    height: f32,
38    open_width: f32,
39) -> bool {
40    let target_w = if is_target { open_width } else { 0.0 };
41    let smooth_w = ui.ctx().animate_value_with_time(slot_id, target_w, 0.12);
42    if smooth_w >= 0.5 {
43        let _ = ui.allocate_exact_size(vec2(smooth_w, height), Sense::hover());
44    }
45    let dropped = is_target && ui.ctx().input(|i| i.pointer.any_released());
46    if dropped {
47        let _ = ui.ctx().animate_value_with_time(slot_id, 0.0, 0.0);
48    }
49    dropped
50}
51
52/// `None` when not dragging or more than 40 pt off the row.
53pub fn drop_target_idx(
54    ctx: &egui::Context,
55    is_dragging: bool,
56    row_rect: Rect,
57    item_count: usize,
58) -> Option<usize> {
59    if !is_dragging {
60        return None;
61    }
62    let cursor = ctx.input(|i| i.pointer.hover_pos())?;
63    let band = row_rect.expand2(vec2(0.0, 40.0));
64    if !band.x_range().contains(cursor.x) || !band.y_range().contains(cursor.y) {
65        return None;
66    }
67    let n_slots = item_count + 1;
68    let slot_w = (row_rect.width() / n_slots as f32).max(1.0);
69    let rel = (cursor.x - row_rect.left()).max(0.0);
70    Some(((rel / slot_w) as usize).min(item_count))
71}
72
73/// The Tooltip layer never registers hover, so the dragged body dims without this.
74pub fn force_opaque_active(ui: &mut Ui) {
75    let active = ui.visuals().widgets.active;
76    let v = ui.visuals_mut();
77    v.widgets.inactive.bg_fill = active.bg_fill;
78    v.widgets.inactive.weak_bg_fill = active.weak_bg_fill;
79    v.widgets.inactive.fg_stroke = active.fg_stroke;
80    v.widgets.inactive.bg_stroke = active.bg_stroke;
81    v.widgets.noninteractive.bg_fill = active.bg_fill;
82    v.widgets.noninteractive.weak_bg_fill = active.weak_bg_fill;
83}
84
85/// In `[0.0, 1.0]`, animated over 120 ms at drag start and drag end.
86pub fn pickup_t(ctx: &egui::Context, drag_id: Id) -> f32 {
87    let target = if ctx.is_being_dragged(drag_id) {
88        1.0
89    } else {
90        0.0
91    };
92    ctx.animate_value_with_time(drag_id.with("pickup"), target, 0.12)
93}
94
95/// Call before rendering the row, with the same gap and card IDs.
96pub fn apply_drop_pre_pass<T, P>(
97    ui: &mut Ui,
98    vec: &mut Vec<T>,
99    drop_idx: Option<usize>,
100    filter: impl FnOnce(&P) -> Option<usize>,
101    gap_id_prefix: &'static str,
102    card_id_prefix: &'static str,
103    max_count: usize,
104) -> bool
105where
106    P: 'static + Send + Sync,
107{
108    if !ui.ctx().input(|i| i.pointer.any_released()) {
109        return false;
110    }
111    let Some(to) = drop_idx else {
112        return false;
113    };
114    let Some(arc) = DragAndDrop::payload::<P>(ui.ctx()) else {
115        return false;
116    };
117    let Some(from) = filter(&arc) else {
118        return false;
119    };
120    let _ = DragAndDrop::take_payload::<P>(ui.ctx());
121    if from == to || from >= vec.len() {
122        return false;
123    }
124    let item = vec.remove(from);
125    let dest = if to > from { to - 1 } else { to };
126    vec.insert(dest.min(vec.len()), item);
127    let ctx = ui.ctx();
128    for i in 0..=max_count {
129        let gap_id = ui.make_persistent_id((gap_id_prefix, i));
130        let _ = ctx.animate_value_with_time(gap_id, 0.0, 0.0);
131        let card_id = ui.make_persistent_id((card_id_prefix, i));
132        let _ = ctx.animate_value_with_time(card_id.with("pickup"), 0.0, 0.0);
133    }
134    true
135}