Skip to content

Commit e2937f3

Browse files
committed
feat: improve drag-and-drop UX with smoother reordering
- hide the original icon and replace its position with an empty container while dragging - reorder items only when the drag position crosses a defined threshold: center of the target +- 4px - the cursor's position is used to calculate the exact pickup offset - reduce the drag threshold to 0 to ensure no drag events are skipped - track the dragged item so that if it is removed during a drag and was previously active, it is automatically added to the active list
1 parent a91fc32 commit e2937f3

File tree

1 file changed

+49
-60
lines changed

1 file changed

+49
-60
lines changed

cosmic-app-list/src/app.rs

Lines changed: 49 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -293,18 +293,19 @@ impl DockItem {
293293
icon_button.into()
294294
};
295295

296+
let icon_origin = iced::Vector::new(app_icon.padding.left, app_icon.padding.top);
296297
let path = desktop_info.path.clone();
297298
let icon_button = if dnd_source_enabled && interaction_enabled {
298299
dnd_source(icon_button)
299300
.window(window_id)
300-
.drag_icon(move |_| {
301+
.drag_icon(move |pointer_offset| {
301302
(
302303
cosmic_icon.clone().into(),
303304
iced::core::widget::tree::State::None,
304-
iced::Vector::ZERO,
305+
icon_origin - pointer_offset,
305306
)
306307
})
307-
.drag_threshold(16.)
308+
.drag_threshold(0.)
308309
.drag_content(move || DndPathBuf(path.clone()))
309310
.on_start(Some(Message::StartDrag(*id)))
310311
.on_cancel(Some(Message::DragFinished))
@@ -319,6 +320,15 @@ impl DockItem {
319320
icon_button.into()
320321
}
321322
}
323+
324+
fn as_draged_item(&self, applet: &Context) -> Element<'_, Message> {
325+
let app_icon = AppletIconData::new(applet);
326+
327+
container(horizontal_space())
328+
.width(app_icon.icon_size as f32 + app_icon.padding.left + app_icon.padding.right)
329+
.height(app_icon.icon_size as f32 + app_icon.padding.top + app_icon.padding.bottom)
330+
.into()
331+
}
322332
}
323333

324334
#[derive(Debug, Clone, Default)]
@@ -344,6 +354,7 @@ struct CosmicAppList {
344354
desktop_entries: Vec<DesktopEntry>,
345355
active_list: Vec<DockItem>,
346356
pinned_list: Vec<DockItem>,
357+
dragged_item: Option<DockItem>,
347358
dnd_source: Option<(window::Id, DockItem, DndAction, Option<usize>)>,
348359
config: AppListConfig,
349360
wayland_sender: Option<Sender<WaylandRequest>>,
@@ -404,40 +415,21 @@ enum Message {
404415
}
405416

406417
fn index_in_list(
407-
mut list_len: usize,
418+
list_len: usize,
408419
item_size: f32,
409-
divider_size: f32,
420+
center_threshold: f32,
410421
existing_preview: Option<usize>,
411422
pos_in_list: f32,
412423
) -> usize {
413-
if existing_preview.is_some() {
414-
list_len += 1;
415-
}
424+
let current_index = existing_preview.unwrap_or(0);
425+
let current_center = current_index as f32 * item_size + item_size / 2.0;
416426

417-
let index = if (list_len == 0) || (pos_in_list < item_size / 2.0) {
418-
0
427+
if pos_in_list > current_center + item_size - center_threshold {
428+
(current_index + 1).min(list_len)
429+
} else if pos_in_list < current_center - item_size + center_threshold {
430+
current_index.saturating_sub(1)
419431
} else {
420-
let mut i = 1;
421-
let mut pos = item_size / 2.0;
422-
while i < list_len {
423-
let next_pos = pos + item_size + divider_size;
424-
if pos < pos_in_list && pos_in_list < next_pos {
425-
break;
426-
}
427-
pos = next_pos;
428-
i += 1;
429-
}
430-
i
431-
};
432-
433-
if let Some(existing_preview) = existing_preview {
434-
if index >= existing_preview {
435-
index.saturating_sub(1)
436-
} else {
437-
index
438-
}
439-
} else {
440-
index
432+
current_index
441433
}
442434
}
443435

@@ -1083,8 +1075,18 @@ impl cosmic::Application for CosmicAppList {
10831075
})
10841076
{
10851077
let icon_id = window::Id::unique();
1078+
if let Some(pinned_pos) = pos {
1079+
let entry = self.pinned_list.remove(pinned_pos);
1080+
if !entry.toplevels.is_empty() {
1081+
self.dragged_item = Some(entry);
1082+
}
1083+
}
10861084
self.dnd_source =
10871085
Some((icon_id, toplevel_group.clone(), DndAction::empty(), pos));
1086+
self.dnd_offer = Some(DndOffer {
1087+
dock_item: Some(toplevel_group),
1088+
preview_index: pos.unwrap_or(self.pinned_list.len()),
1089+
});
10881090
}
10891091
}
10901092
Message::DragFinished => {
@@ -1127,14 +1129,20 @@ impl cosmic::Application for CosmicAppList {
11271129
};
11281130
let num_pinned = self.pinned_list.len();
11291131
let index = index_in_list(num_pinned, item_size as f32, 4.0, None, pos_in_list);
1130-
self.dnd_offer = Some(DndOffer {
1131-
preview_index: index,
1132-
..DndOffer::default()
1133-
});
11341132
if let Some(dnd_source) = self.dnd_source.as_ref() {
1135-
self.dnd_offer.as_mut().unwrap().dock_item = Some(dnd_source.1.clone());
1133+
let source_id = dnd_source.1.desktop_info.id().to_string();
1134+
self.pinned_list
1135+
.retain(|p| p.desktop_info.id() != source_id);
1136+
1137+
self.dnd_offer = Some(DndOffer {
1138+
preview_index: index,
1139+
dock_item: Some(dnd_source.1.clone()),
1140+
});
11361141
} else {
1137-
// TODO dnd
1142+
self.dnd_offer = Some(DndOffer {
1143+
preview_index: index,
1144+
..DndOffer::default()
1145+
});
11381146
return peek_dnd::<DndPathBuf>()
11391147
.map(Message::DndData)
11401148
.map(cosmic::Action::App);
@@ -1160,17 +1168,9 @@ impl cosmic::Application for CosmicAppList {
11601168
}
11611169
}
11621170
Message::DndLeave => {
1163-
if let Some((_, toplevel_group, _, pinned_pos)) = self.dnd_source.as_ref() {
1164-
let mut pos = 0;
1165-
self.pinned_list.retain_mut(|pinned| {
1166-
let matched_id =
1167-
pinned.desktop_info.id() == toplevel_group.desktop_info.id();
1168-
let pinned_match = pinned_pos.is_some_and(|pinned_pos| pinned_pos == pos);
1169-
let ret = !matched_id || pinned_match;
1170-
1171-
pos += 1;
1172-
ret
1173-
});
1171+
if let Some(dragged_item) = self.dragged_item.take() {
1172+
self.active_list.push(dragged_item);
1173+
self.dragged_item = None;
11741174
}
11751175
self.dnd_offer = None;
11761176
}
@@ -1768,18 +1768,7 @@ impl cosmic::Application for CosmicAppList {
17681768
{
17691769
favorites.insert(
17701770
index.min(favorites.len()),
1771-
item.as_icon(
1772-
&self.core.applet,
1773-
None,
1774-
false,
1775-
self.config.enable_drag_source,
1776-
self.gpus.as_deref(),
1777-
item.toplevels
1778-
.iter()
1779-
.any(|y| focused_item.contains(&y.0.foreign_toplevel)),
1780-
dot_radius,
1781-
self.core.main_window_id().unwrap(),
1782-
),
1771+
item.as_draged_item(&self.core.applet),
17831772
);
17841773
} else if self.is_listening_for_dnd && self.pinned_list.is_empty() {
17851774
// show star indicating pinned_list is drag target

0 commit comments

Comments
 (0)