diff options
Diffstat (limited to 'crates')
| -rw-r--r-- | crates/wayland/src/desktop_icons.rs | 24 | ||||
| -rw-r--r-- | crates/wayland/src/input/pointer.rs | 15 | ||||
| -rw-r--r-- | crates/wayland/src/state/desktop_icons.rs | 121 | ||||
| -rw-r--r-- | crates/wayland/src/state/mod.rs | 16 |
4 files changed, 131 insertions, 45 deletions
diff --git a/crates/wayland/src/desktop_icons.rs b/crates/wayland/src/desktop_icons.rs index 7cfcbd1..c3b573a 100644 --- a/crates/wayland/src/desktop_icons.rs +++ b/crates/wayland/src/desktop_icons.rs @@ -57,6 +57,30 @@ impl DesktopIcon { } } +/// An in-progress icon drag that may carry more than one icon along +/// together. `primary` is whichever icon the pointer actually grabbed; +/// `members` is every icon moving with it (always includes `primary`, at +/// offset `(0, 0)`), each recorded as a fixed offset from `primary`'s own +/// top-left at the moment the drag started - the whole group moves as +/// one rigid unit regardless of where each member's own cell happens to +/// be, the same way dragging one file in a multi-selection in Windows/ +/// GNOME/macOS/KDE carries every other selected file along with it. +/// Reported live as missing: "try move desktop items all at once +/// somewhere else" didn't work at all before this - `members` used to +/// not exist, a drag only ever carried the one icon it grabbed no matter +/// how many were selected. +pub(crate) struct DesktopIconDrag { + /// Pointer's grab offset from `primary`'s own top-left at drag start, + /// so the icon tracks the pointer smoothly rather than snapping its + /// top-left corner straight to the cursor. + pub(crate) grab_offset: (i32, i32), + /// `primary`'s own live top-left this frame. + pub(crate) primary_pos: (i32, i32), + /// `(icon id, fixed offset from primary's own top-left at drag + /// start)` - primary included at offset `(0, 0)`. + pub(crate) members: Vec<(String, (i32, i32))>, +} + pub(crate) struct DesktopIcons { /// Top-left of the grid's own `(0, 0)` cell, in global space, one per /// participating monitor - each monitor's own usable-area origin plus diff --git a/crates/wayland/src/input/pointer.rs b/crates/wayland/src/input/pointer.rs index 602f613..44b0bd7 100644 --- a/crates/wayland/src/input/pointer.rs +++ b/crates/wayland/src/input/pointer.rs @@ -663,7 +663,20 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic state.select_desktop_icon(Some(&id)); state.open_desktop_icon(&id); } else { - state.select_desktop_icon(Some(&id)); + // Don't collapse an existing multi-selection + // just because the drag grabbed one of its own + // members - `start_desktop_icon_drag` itself + // carries every currently-selected icon along + // when the one grabbed is already selected + // (see its own doc comment), the same "drag one + // of several selected files, they all move" + // convention every real desktop uses. Grabbing + // an icon *outside* the current selection still + // replaces it, same as before. + let already_selected = state.desktop_icons.as_ref().is_some_and(|icons| icons.icons.iter().any(|i| i.id == id && i.selected)); + if !already_selected { + state.select_desktop_icon(Some(&id)); + } state.start_desktop_icon_drag(&id, origin, (pos.x as i32, pos.y as i32)); } } diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs index f111336..81a8ed8 100644 --- a/crates/wayland/src/state/desktop_icons.rs +++ b/crates/wayland/src/state/desktop_icons.rs @@ -170,7 +170,13 @@ impl CompState { let Some(icons) = &self.desktop_icons else { return Vec::new() }; let ids: Vec<String> = icons.icons.iter().map(|i| i.id.clone()).collect(); let origins = icons.origins.clone(); - let dragging = self.desktop_icon_drag.clone(); + // `(primary_pos, members)` - see `DesktopIconDrag`'s own doc + // comment. Every dragged icon (not just the one actually grabbed) + // follows the live pointer as a rigid group; every other mirror + // (if any) and every non-dragged icon stays put at its own + // origin's cell position, same as before this could carry more + // than one icon. + let dragging = self.desktop_icon_drag.as_ref().map(|d| (d.primary_pos, d.members.clone())); let mut out = Vec::with_capacity(ids.len() * origins.len()); for id in ids { let buffer = match self.icon_buffer(&id) { @@ -179,14 +185,12 @@ impl CompState { }; let icons = self.desktop_icons.as_ref().unwrap(); let icon = icons.icons.iter().find(|i| i.id == id).unwrap(); - // The dragged copy follows the live pointer on whichever - // monitor it's actually being dragged over; every other - // mirror (if any) stays put at its own origin's cell position - // - dragging on one monitor doesn't yank the icon's other - // mirrors around mid-gesture, only the one actually grabbed. - match &dragging { - Some((drag_id, _, live_pos)) if *drag_id == id => out.push((*live_pos, buffer)), - _ => { + let drag_pos = dragging + .as_ref() + .and_then(|(primary_pos, members)| members.iter().find(|(mid, _)| *mid == id).map(|(_, offset)| (primary_pos.0 + offset.0, primary_pos.1 + offset.1))); + match drag_pos { + Some(pos) => out.push((pos, buffer)), + None => { for &origin in &origins { out.push((icon.top_left(origin), buffer.clone())); } @@ -294,54 +298,105 @@ impl CompState { /// under the pointer, not always the primary monitor's, is what makes /// the drag track the cursor instead of jumping to a different /// monitor's copy the instant the drag starts. + /// Starts a drag on `id`. If `id` is already part of a multi- + /// selection, every other selected icon comes along as a `member` + /// (see `DesktopIconDrag`'s own doc comment); otherwise this starts a + /// fresh single-icon drag and selects just `id`, the same "grabbing + /// something outside the current selection replaces it" convention + /// real desktops use. pub(crate) fn start_desktop_icon_drag(&mut self, id: &str, origin: (i32, i32), pointer: (i32, i32)) { - let Some(icons) = &self.desktop_icons else { return }; - let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return }; - let top_left = icon.top_left(origin); - let grab_offset = (pointer.0 - top_left.0, pointer.1 - top_left.1); - self.desktop_icon_drag = Some((id.to_string(), grab_offset, top_left)); + let Some(icons) = &mut self.desktop_icons else { return }; + let Some(primary) = icons.icons.iter().find(|i| i.id == id) else { return }; + let primary_top_left = primary.top_left(origin); + let primary_selected = primary.selected; + let grab_offset = (pointer.0 - primary_top_left.0, pointer.1 - primary_top_left.1); + + let mut changed = Vec::new(); + if !primary_selected { + for icon in &mut icons.icons { + let should = icon.id == id; + if icon.selected != should { + icon.selected = should; + changed.push(icon.id.clone()); + } + } + } + let members: Vec<(String, (i32, i32))> = icons + .icons + .iter() + .filter(|i| i.id == id || i.selected) + .map(|i| { + let top_left = i.top_left(origin); + (i.id.clone(), (top_left.0 - primary_top_left.0, top_left.1 - primary_top_left.1)) + }) + .collect(); + self.desktop_icon_drag = Some(crate::desktop_icons::DesktopIconDrag { grab_offset, primary_pos: primary_top_left, members }); + for id in changed { + self.rebuild_icon_buffer(&id); + } } - /// Updates the live position of whichever icon is being dragged, if + /// Updates the live position of every icon in the current drag (the + /// one actually grabbed, plus every icon carried along with it), if /// any - called from every pointer-motion event, same as /// `WindowManager::update_resize`'s own per-motion-event update. pub(crate) fn update_desktop_icon_drag(&mut self, pointer: (i32, i32)) { - if let Some((_, grab_offset, live_pos)) = &mut self.desktop_icon_drag { - *live_pos = (pointer.0 - grab_offset.0, pointer.1 - grab_offset.1); + if let Some(drag) = &mut self.desktop_icon_drag { + drag.primary_pos = (pointer.0 - drag.grab_offset.0, pointer.1 - drag.grab_offset.1); } } - /// Ends an in-progress drag (if any): snaps to the nearest free grid - /// cell (occupied cells other than the dragged icon's own previous one - /// are avoided by walking outward from the raw target, closest first) - /// and persists it. + /// Ends an in-progress drag (if any): snaps every dragged icon (the + /// one actually grabbed, plus every icon carried along with it) to + /// its own nearest free grid cell, independently but never colliding + /// with each other - walking outward from each one's own raw target, + /// closest first - and persists all of them. pub(crate) fn end_desktop_icon_drag(&mut self) { - let Some((id, _, live_pos)) = self.desktop_icon_drag.take() else { return }; + let Some(drag) = self.desktop_icon_drag.take() else { return }; // The cell math below needs the origin of whichever monitor the - // icon was actually dropped on, not always the first mirror -- + // group was actually dropped on, not always the first mirror -- // recomputed fresh (same formula `desktop_icon_origins` uses) // rather than searched for in `icons.origins`, since a drop // just past every monitor's own strict icon-grid rect (but still // on-screen) should still resolve to that monitor's grid, not fall - // through to a stale/wrong one. + // through to a stale/wrong one. Based on the primary's own drop + // position - the group drops together onto whichever monitor the + // pointer is actually over. let origin = self .wm .borrow() .monitors() .iter() - .find(|m| m.full_geometry.contains_point(live_pos.0, live_pos.1)) + .find(|m| m.full_geometry.contains_point(drag.primary_pos.0, drag.primary_pos.1)) .map(|m| (m.geometry.x + GRID_MARGIN, m.geometry.y + GRID_MARGIN)) .unwrap_or(self.desktop_icons.as_ref().map(|i| i.origins.first().copied().unwrap_or((0, 0))).unwrap_or((0, 0))); let Some(icons) = &mut self.desktop_icons else { return }; - let raw = (live_pos.0 - origin.0, live_pos.1 - origin.1); - let raw_cell = ((raw.0 as f64 / CELL_WIDTH as f64).round() as i32, (raw.1 as f64 / CELL_HEIGHT as f64).round() as i32).max_zero(); - let occupied: std::collections::HashSet<(i32, i32)> = icons.icons.iter().filter(|i| i.id != id).map(|i| i.cell).collect(); - let cell = nearest_free_cell(raw_cell, &occupied); - if let Some(icon) = icons.icons.iter_mut().find(|i| i.id == id) { - icon.cell = cell; + let dragged_ids: std::collections::HashSet<&str> = drag.members.iter().map(|(id, _)| id.as_str()).collect(); + // Each member's own nearest free cell, in recorded order (primary + // first) - `newly_occupied` accumulates across the loop so two + // dragged icons landing near each other never both claim the same + // cell, on top of every cell a *non*-dragged icon already holds. + let mut newly_occupied: std::collections::HashSet<(i32, i32)> = std::collections::HashSet::new(); + let mut placements: Vec<(String, (i32, i32))> = Vec::with_capacity(drag.members.len()); + for (id, offset) in &drag.members { + let live_pos = (drag.primary_pos.0 + offset.0, drag.primary_pos.1 + offset.1); + let raw = (live_pos.0 - origin.0, live_pos.1 - origin.1); + let raw_cell = ((raw.0 as f64 / CELL_WIDTH as f64).round() as i32, (raw.1 as f64 / CELL_HEIGHT as f64).round() as i32).max_zero(); + let occupied: std::collections::HashSet<(i32, i32)> = + icons.icons.iter().filter(|i| !dragged_ids.contains(i.id.as_str())).map(|i| i.cell).chain(newly_occupied.iter().copied()).collect(); + let cell = nearest_free_cell(raw_cell, &occupied); + newly_occupied.insert(cell); + placements.push((id.clone(), cell)); + } + for (id, cell) in &placements { + if let Some(icon) = icons.icons.iter_mut().find(|i| &i.id == id) { + icon.cell = *cell; + } + } + for (id, cell) in &placements { + self.rebuild_icon_buffer(id); + crate::desktop_icons_state::save_icon(id, *cell); } - self.rebuild_icon_buffer(&id); - crate::desktop_icons_state::save_icon(&id, cell); } pub(crate) fn open_desktop_icon(&mut self, id: &str) { diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs index 3519297..629b58c 100644 --- a/crates/wayland/src/state/mod.rs +++ b/crates/wayland/src/state/mod.rs @@ -353,17 +353,11 @@ pub(crate) struct CompState { /// same cached-until-dirty convention as every other decoration /// buffer in this codebase. pub(crate) desktop_icon_buffers: HashMap<String, MemoryRenderBuffer>, - /// An in-progress icon drag: the icon's own id, the pointer's grab - /// offset from that icon's cell origin at the moment the drag started - /// (so the icon tracks the pointer smoothly rather than snapping its - /// top-left corner straight to the cursor), and the icon's own live - /// top-left position this frame - updated on every pointer-motion - /// event by `update_desktop_icon_drag`, read straight back by - /// `desktop_icon_render_list` with no separate "current pointer - /// position" field needed anywhere on `CompState`. `None` whenever no - /// drag is active. - #[allow(clippy::type_complexity)] - pub(crate) desktop_icon_drag: Option<(String, (i32, i32), (i32, i32))>, + /// An in-progress icon drag - see `desktop_icons::DesktopIconDrag`'s + /// own doc comment for the full shape (it may carry more than one + /// icon, when the drag started on an already-multi-selected icon). + /// `None` whenever no drag is active. + pub(crate) desktop_icon_drag: Option<crate::desktop_icons::DesktopIconDrag>, /// An active rubber-band/marquee selection drag on bare desktop -- /// `(start, current)`, both global-space pointer positions. The one /// "click and drag" desktop interaction this compositor never had at |