srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'crates/wayland/src/state')
-rw-r--r--crates/wayland/src/state/desktop_icons.rs344
-rw-r--r--crates/wayland/src/state/mod.rs33
2 files changed, 377 insertions, 0 deletions
diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs
new file mode 100644
index 0000000..b1b802e
--- /dev/null
+++ b/crates/wayland/src/state/desktop_icons.rs
@@ -0,0 +1,344 @@
+//! `CompState` glue for desktop icons - open/rescan/select/drag/persist,
+//! plus the right-click `DesktopMenu`'s own open/close/run-action. Same
+//! shape as `state/menu.rs`'s `ContextMenu`/`SnapFlyout` glue.
+
+use super::*;
+use crate::desktop_icons::{DesktopIcons, CELL_HEIGHT, CELL_WIDTH, GRID_MARGIN};
+use crate::desktop_menu::{DesktopMenu, DesktopMenuAction};
+
+impl CompState {
+ /// Populates `self.desktop_icons` on first call (or after `general.
+ /// desktop_icons` was off and just turned on), once the primary
+ /// monitor's own geometry is actually known - a no-op every other
+ /// call, cheap enough to check unconditionally at the top of a render
+ /// pass. Does nothing at all when the config flag is off.
+ pub(crate) fn ensure_desktop_icons(&mut self) {
+ if !self.wm.borrow().desktop_icons_enabled {
+ return;
+ }
+ if self.desktop_icons.is_some() {
+ return;
+ }
+ let Some(monitor) = self.wm.borrow().monitors().iter().find(|m| m.primary).cloned() else { return };
+ let origin = (monitor.geometry.x + GRID_MARGIN, monitor.geometry.y + GRID_MARGIN);
+ let rows = ((monitor.geometry.height as i32 - 2 * GRID_MARGIN) / CELL_HEIGHT).max(1);
+ let saved = crate::desktop_icons_state::load();
+ let icons = crate::desktop_icons::rescan(&saved, rows);
+ self.desktop_icons = Some(DesktopIcons { origin, icons });
+ self.desktop_icon_buffers.clear();
+ }
+
+ /// Re-derives the icon list from the real filesystem (a new/removed
+ /// `~/Desktop` entry) without disturbing any already-persisted cell --
+ /// `rescan` itself already only assigns a fresh default cell to an
+ /// icon `saved` has no entry for.
+ pub(crate) fn refresh_desktop_icons(&mut self) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let rows = ((self.primary_monitor_height()) / CELL_HEIGHT).max(1);
+ let saved = crate::desktop_icons_state::load();
+ let origin = icons.origin;
+ let icons = crate::desktop_icons::rescan(&saved, rows);
+ self.desktop_icons = Some(DesktopIcons { origin, icons });
+ self.desktop_icon_buffers.clear();
+ }
+
+ fn primary_monitor_height(&self) -> i32 {
+ self.wm.borrow().monitors().iter().find(|m| m.primary).map(|m| m.geometry.height as i32 - 2 * GRID_MARGIN).unwrap_or(600)
+ }
+
+ /// Rasterises (or re-rasterises) one icon's buffer - called whenever
+ /// that icon's selection state or drag position changes, never per
+ /// frame; `render_udev_frame`/`render_frame` just read whatever's
+ /// already cached here.
+ fn rebuild_icon_buffer(&mut self, id: &str) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ let theme = self.wm.borrow().theme;
+ let label_color = (240, 240, 240);
+ let data = decoration::render_desktop_icon(
+ CELL_WIDTH as u32,
+ CELL_HEIGHT as u32,
+ icon.kind,
+ &icon.label,
+ icon.selected,
+ theme.titlebar_fg_focused,
+ label_color,
+ theme.default_border_color,
+ );
+ let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (CELL_WIDTH, CELL_HEIGHT), 1, Transform::Normal, None);
+ self.desktop_icon_buffers.insert(id.to_string(), buffer);
+ }
+
+ fn icon_buffer(&mut self, id: &str) -> Option<&MemoryRenderBuffer> {
+ if !self.desktop_icon_buffers.contains_key(id) {
+ self.rebuild_icon_buffer(id);
+ }
+ self.desktop_icon_buffers.get(id)
+ }
+
+ /// Every `(position, buffer)` pair the render loop needs to push this
+ /// frame - lazily rebuilds any icon whose buffer isn't cached yet
+ /// (a fresh icon, or one whose selection/drag state just changed),
+ /// then returns everything already up to date. Global-space positions;
+ /// the caller subtracts its own head origin, same as every other
+ /// `custom_elements` push site.
+ pub(crate) fn desktop_icon_render_list(&mut self) -> Vec<((i32, i32), MemoryRenderBuffer)> {
+ let Some(icons) = &self.desktop_icons else { return Vec::new() };
+ let ids: Vec<String> = icons.icons.iter().map(|i| i.id.clone()).collect();
+ let origin = icons.origin;
+ let dragging = self.desktop_icon_drag.clone();
+ let mut out = Vec::with_capacity(ids.len());
+ for id in ids {
+ let buffer = match self.icon_buffer(&id) {
+ Some(b) => b.clone(),
+ None => continue,
+ };
+ let icons = self.desktop_icons.as_ref().unwrap();
+ let icon = icons.icons.iter().find(|i| i.id == id).unwrap();
+ let pos = match &dragging {
+ Some((drag_id, _, live_pos)) if *drag_id == id => *live_pos,
+ _ => icon.top_left(origin),
+ };
+ out.push((pos, buffer));
+ }
+ out
+ }
+
+ /// Selects `id` (deselecting whatever was selected before, if
+ /// anything) - both buffers rebuilt only if their selection state
+ /// actually changed, not unconditionally.
+ pub(crate) fn select_desktop_icon(&mut self, id: Option<&str>) {
+ let Some(icons) = &mut self.desktop_icons else { return };
+ let mut changed = Vec::new();
+ for icon in &mut icons.icons {
+ let should = Some(icon.id.as_str()) == id;
+ if icon.selected != should {
+ icon.selected = should;
+ changed.push(icon.id.clone());
+ }
+ }
+ for id in changed {
+ self.rebuild_icon_buffer(&id);
+ }
+ }
+
+ /// True when this press is the second of a double-click on the same
+ /// icon - same 400ms threshold and reset-after-a-double shape as
+ /// `is_double_click`, keyed by `DesktopIcon::id` since an icon has no
+ /// `WindowId` of its own.
+ pub(crate) fn is_double_click_icon(&mut self, id: &str, time: u32) -> bool {
+ const DOUBLE_CLICK_MS: u32 = 400;
+ let doubled = match &self.last_icon_click {
+ Some((last_id, last_time)) => last_id == id && time.saturating_sub(*last_time) <= DOUBLE_CLICK_MS,
+ None => false,
+ };
+ self.last_icon_click = if doubled { None } else { Some((id.to_string(), time)) };
+ doubled
+ }
+
+ pub(crate) fn start_desktop_icon_drag(&mut self, id: &str, 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(icons.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));
+ }
+
+ /// Updates the live position of whichever icon is being dragged, 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);
+ }
+ }
+
+ /// 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.
+ pub(crate) fn end_desktop_icon_drag(&mut self) {
+ let Some((id, _, live_pos)) = self.desktop_icon_drag.take() else { return };
+ let Some(icons) = &mut self.desktop_icons else { return };
+ let origin = icons.origin;
+ 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;
+ }
+ self.rebuild_icon_buffer(&id);
+ crate::desktop_icons_state::save_icon(&id, cell);
+ }
+
+ pub(crate) fn open_desktop_icon(&mut self, id: &str) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ let target = icon.target.display().to_string();
+ let file_manager = self.wm.borrow().file_manager.clone();
+ if file_manager.is_empty() {
+ spawn_shell(&format!("xdg-open {}", shell_quote(&target)));
+ } else {
+ spawn_shell(&format!("{file_manager} {}", shell_quote(&target)));
+ }
+ }
+
+ pub(crate) fn set_desktop_icon_as_wallpaper(&mut self, id: &str) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ let target = icon.target.display().to_string();
+ let command = self.wm.borrow().wallpaper_command.clone();
+ if command.is_empty() {
+ return;
+ }
+ spawn_shell(&format!("{command} {}", shell_quote(&target)));
+ }
+
+ /// Creates `~/Desktop/New Folder`, de-duplicated as `New Folder (2)`,
+ /// `(3)`, ... against whatever's already there, then rescans so it
+ /// shows up as an icon immediately.
+ pub(crate) fn new_desktop_folder(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = std::path::PathBuf::from(home).join("Desktop");
+ let mut name = "New Folder".to_string();
+ let mut n = 2;
+ while desktop.join(&name).exists() {
+ name = format!("New Folder ({n})");
+ n += 1;
+ }
+ if let Err(e) = std::fs::create_dir(desktop.join(&name)) {
+ log::warn!("desktop_icons: couldn't create {name:?}: {e}");
+ return;
+ }
+ self.refresh_desktop_icons();
+ }
+
+ pub(crate) fn open_desktop_icon_menu(&mut self, icon_id: &str, pos: (i32, i32)) {
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == icon_id) else { return };
+ let wallpaper_command = self.wm.borrow().wallpaper_command.clone();
+ let menu = DesktopMenu::open_for_icon(icon, pos, &wallpaper_command);
+ self.build_desktop_menu_buffer(menu);
+ }
+
+ pub(crate) fn open_desktop_menu(&mut self, pos: (i32, i32)) {
+ let menu = DesktopMenu::open_for_desktop(pos);
+ self.build_desktop_menu_buffer(menu);
+ }
+
+ fn build_desktop_menu_buffer(&mut self, menu: DesktopMenu) {
+ let theme = self.wm.borrow().theme;
+ let items: Vec<(&str, bool)> = menu.items.iter().map(|&(label, _)| (label, false)).collect();
+ let data = decoration::render_context_menu(menu.width, menu.row_height, &items, theme.titlebar_bg, theme.titlebar_fg_focused, theme.titlebar_fg_unfocused, theme.default_border_color);
+ let buffer = MemoryRenderBuffer::from_slice(&data, Fourcc::Argb8888, (menu.width as i32, menu.height()), 1, Transform::Normal, None);
+ self.desktop_menu_buffer = Some(buffer);
+ self.desktop_menu = Some(menu);
+ }
+
+ pub(crate) fn close_desktop_menu(&mut self) {
+ self.desktop_menu = None;
+ self.desktop_menu_buffer = None;
+ }
+
+ pub(crate) fn run_desktop_menu_action(&mut self, action: DesktopMenuAction) {
+ match action {
+ DesktopMenuAction::OpenIcon(id) => self.open_desktop_icon(&id),
+ DesktopMenuAction::SetWallpaper(id) => self.set_desktop_icon_as_wallpaper(&id),
+ DesktopMenuAction::NewFolder => self.new_desktop_folder(),
+ DesktopMenuAction::Refresh => self.refresh_desktop_icons(),
+ }
+ }
+}
+
+trait MaxZero {
+ fn max_zero(self) -> Self;
+}
+impl MaxZero for (i32, i32) {
+ fn max_zero(self) -> Self {
+ (self.0.max(0), self.1.max(0))
+ }
+}
+
+/// Breadth-first search outward from `target` over grid cells (`target`
+/// itself first, then its 4-neighbours, then theirs, ...) for the first
+/// one not in `occupied` - a dropped icon always lands *somewhere* rather
+/// than silently failing to move when its raw target cell is already
+/// taken.
+fn nearest_free_cell(target: (i32, i32), occupied: &std::collections::HashSet<(i32, i32)>) -> (i32, i32) {
+ if !occupied.contains(&target) && target.0 >= 0 && target.1 >= 0 {
+ return target;
+ }
+ use std::collections::VecDeque;
+ let mut seen = std::collections::HashSet::new();
+ let mut queue = VecDeque::new();
+ queue.push_back(target);
+ seen.insert(target);
+ while let Some((c, r)) = queue.pop_front() {
+ if c >= 0 && r >= 0 && !occupied.contains(&(c, r)) {
+ return (c, r);
+ }
+ for (dc, dr) in [(1, 0), (-1, 0), (0, 1), (0, -1)] {
+ let next = (c + dc, r + dr);
+ if seen.insert(next) {
+ queue.push_back(next);
+ }
+ }
+ }
+ target
+}
+
+fn shell_quote(s: &str) -> String {
+ format!("'{}'", s.replace('\'', "'\\''"))
+}
+
+fn spawn_shell(command: &str) {
+ #[cfg(unix)]
+ let result = std::process::Command::new("sh").arg("-c").arg(command).spawn();
+ #[cfg(windows)]
+ let result = std::process::Command::new("cmd").arg("/C").arg(command).spawn();
+ if let Err(e) = result {
+ log::warn!("desktop_icons: spawn '{command}' failed: {e}");
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn shell_quote_wraps_in_single_quotes() {
+ assert_eq!(shell_quote("/home/x/pic.png"), "'/home/x/pic.png'");
+ }
+
+ #[test]
+ fn shell_quote_escapes_an_embedded_single_quote() {
+ assert_eq!(shell_quote("it's.png"), "'it'\\''s.png'");
+ }
+
+ #[test]
+ fn nearest_free_cell_returns_the_target_when_its_free() {
+ let occupied = std::collections::HashSet::new();
+ assert_eq!(nearest_free_cell((2, 3), &occupied), (2, 3));
+ }
+
+ #[test]
+ fn nearest_free_cell_finds_an_adjacent_slot_when_the_target_is_taken() {
+ let mut occupied = std::collections::HashSet::new();
+ occupied.insert((0, 0));
+ let (c, r) = nearest_free_cell((0, 0), &occupied);
+ assert!((c, r) != (0, 0));
+ assert!(!occupied.contains(&(c, r)));
+ assert_eq!(c.abs() + r.abs(), 1, "the nearest free cell is exactly one step away");
+ }
+
+ #[test]
+ fn nearest_free_cell_never_returns_a_negative_column_or_row() {
+ let mut occupied = std::collections::HashSet::new();
+ occupied.insert((0, 0));
+ occupied.insert((1, 0));
+ occupied.insert((0, 1));
+ let (c, r) = nearest_free_cell((0, 0), &occupied);
+ assert!(c >= 0 && r >= 0);
+ }
+}
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 0ef4346..66f9ec6 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -340,6 +340,38 @@ pub(crate) struct CompState {
/// Rasterised pixels for the currently-open `snap_flyout`, same
/// build-once-on-open pattern as `context_menu_buffer`.
pub(crate) snap_flyout_buffer: Option<MemoryRenderBuffer>,
+ /// The real desktop icons (Home/Computer/Trash plus `~/Desktop`'s own
+ /// contents) - see `desktop_icons.rs`. `None` until the first render
+ /// pass populates it (lazily, once the primary monitor's own geometry
+ /// is actually known - see `state/desktop_icons.rs::ensure_desktop_
+ /// icons`), and permanently `None` when `general.desktop_icons` is off.
+ pub(crate) desktop_icons: Option<crate::desktop_icons::DesktopIcons>,
+ /// Rasterised pixels per icon, keyed by `DesktopIcon::id` - rebuilt
+ /// only for the one icon whose selection/drag state actually changed,
+ /// 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))>,
+ /// The right-click desktop-icon/bare-desktop menu, if one is currently
+ /// open - see `desktop_menu.rs`. Same lifecycle/mutual-exclusion
+ /// story as `context_menu`/`snap_flyout` above.
+ pub(crate) desktop_menu: Option<crate::desktop_menu::DesktopMenu>,
+ /// Same build-once-on-open pattern as `context_menu_buffer`.
+ pub(crate) desktop_menu_buffer: Option<MemoryRenderBuffer>,
+ /// Same double-click bookkeeping as `last_titlebar_click`, keyed by
+ /// `DesktopIcon::id` instead of `WindowId` since a desktop icon isn't
+ /// a window - see `CompState::is_double_click`'s own doc comment.
+ pub(crate) last_icon_click: Option<(String, u32)>,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
@@ -822,6 +854,7 @@ impl CompState {
}
+mod desktop_icons;
mod focus;
mod geometry;
mod layers;