srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland/src/desktop_menu.rs
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-15 23:22:00 +0200
committersrdusr <[email protected]>2025-08-15 23:22:00 +0200
commitcc22fe69a68e0fff027d833029aea850976488c8 (patch)
treeeb1350c24c61b5d4b30d763d64149a5c6504a925 /crates/wayland/src/desktop_menu.rs
parent6694a2d9946d98d5752d84b22c45879cab5c9abe (diff)
downloadsrdwm-cc22fe69a68e0fff027d833029aea850976488c8.tar.gz
srdwm-cc22fe69a68e0fff027d833029aea850976488c8.zip
Add real desktop icons plus right-click desktop/icon context menus
Closes "right-click on bare desktop" - previously a true no-op, nothing rendered above the wallpaper at all. Requested directly: a real desktop "just like windows does" - Home/Computer/Trash plus one icon per real ~/Desktop entry, individually draggable with persisted grid positions, double-click to open, right-click menus (per-icon "Open" plus "Set as Wallpaper" for image files when general.wallpaper_command is set; bare desktop "New Folder"/"Refresh"). Architecture mirrors the existing context_menu.rs/snap_flyout.rs "compositor-owned floating UI" pattern: desktop_icons.rs (data model, grid layout, filesystem scan, hit-test), desktop_icons_state.rs (JSON persistence, same shape as monitor_layout.rs), desktop_menu.rs (the new right-click menu, reusing decoration::render_context_menu's existing rasterizer), state/desktop_icons.rs (CompState glue: rescan/select/drag/ open/persist), and a new decoration::render_desktop_icon rasterizer -- hand-drawn glyphs, since no PNG/SVG decoding capability exists anywhere in this workspace. Wired into both render loops (udev and winit) above the wallpaper and below every window, and into input/pointer.rs's button/ motion handlers for selection, drag, double-click, and both menus. Four new config keys: general.desktop_icons (default true - a directly requested, purely visual feature, unlike the opt-in-while-experimental general.gpu), general.file_manager, general.desktop_icon_single_click, general.wallpaper_command (all default off/empty). Deliberately out of scope for this pass, stated up front: move-to-trash and "Empty Trash" (destructive, no confirmation-dialog primitive to gate them on yet), filesystem watching, multi-select, per-mimetype icon art, icons on any monitor but the primary one. 124 wayland-crate tests (up from 106), full workspace build and clippy clean.
Diffstat (limited to 'crates/wayland/src/desktop_menu.rs')
-rw-r--r--crates/wayland/src/desktop_menu.rs126
1 files changed, 126 insertions, 0 deletions
diff --git a/crates/wayland/src/desktop_menu.rs b/crates/wayland/src/desktop_menu.rs
new file mode 100644
index 0000000..810e639
--- /dev/null
+++ b/crates/wayland/src/desktop_menu.rs
@@ -0,0 +1,126 @@
+//! Right-click desktop icon / bare-desktop menu - the sibling of
+//! `context_menu.rs`'s titlebar window menu, same shape (own `open`/`row_
+//! at`, rasterized via `decoration::render_context_menu`), for the two new
+//! right-click targets desktop icons add: an icon itself, or bare desktop.
+
+use crate::desktop_icons::{DesktopIcon, IconKind};
+
+#[derive(Clone)]
+pub(crate) enum DesktopMenuAction {
+ /// Open the icon with this id - same action a double-click runs.
+ OpenIcon(String),
+ /// Shell out to `general.wallpaper_command` with this icon's path --
+ /// only ever offered for an image-file icon, and only when that
+ /// config key is actually set (see `WindowManager::wallpaper_command`'s
+ /// own doc comment).
+ SetWallpaper(String),
+ NewFolder,
+ Refresh,
+}
+
+pub(crate) struct DesktopMenu {
+ pub(crate) pos: (i32, i32),
+ pub(crate) width: u32,
+ pub(crate) row_height: u32,
+ pub(crate) items: Vec<(&'static str, DesktopMenuAction)>,
+}
+
+const MENU_WIDTH: u32 = 170;
+const ROW_HEIGHT: u32 = 28;
+
+/// Extensions `render_desktop_icon`'s `IconKind::File` icons treat as an
+/// image for "Set as Wallpaper" purposes - not a real mimetype sniff (no
+/// such capability exists anywhere in this workspace, see `desktop_icons.
+/// rs`'s own doc comment on why icon art itself is hand-drawn, not
+/// decoded), just the common raster formats a wallpaper tool actually
+/// accepts.
+const IMAGE_EXTENSIONS: &[&str] = &["png", "jpg", "jpeg", "webp", "bmp", "gif"];
+
+fn is_image_path(path: &std::path::Path) -> bool {
+ path.extension().and_then(|e| e.to_str()).map(|e| IMAGE_EXTENSIONS.iter().any(|ext| e.eq_ignore_ascii_case(ext))).unwrap_or(false)
+}
+
+impl DesktopMenu {
+ /// Right-click on `icon` itself: "Open" always, plus "Set as Wallpaper"
+ /// when `icon` is an image file and `wallpaper_command` is non-empty.
+ pub(crate) fn open_for_icon(icon: &DesktopIcon, pos: (i32, i32), wallpaper_command: &str) -> Self {
+ let mut items = vec![("Open", DesktopMenuAction::OpenIcon(icon.id.clone()))];
+ if icon.kind == IconKind::File && !wallpaper_command.is_empty() && is_image_path(&icon.target) {
+ items.push(("Set as Wallpaper", DesktopMenuAction::SetWallpaper(icon.id.clone())));
+ }
+ Self { pos, width: MENU_WIDTH, row_height: ROW_HEIGHT, items }
+ }
+
+ /// Right-click on bare desktop (no icon under the pointer): "New
+ /// Folder" and "Refresh".
+ pub(crate) fn open_for_desktop(pos: (i32, i32)) -> Self {
+ let items = vec![("New Folder", DesktopMenuAction::NewFolder), ("Refresh", DesktopMenuAction::Refresh)];
+ Self { pos, width: MENU_WIDTH, row_height: ROW_HEIGHT, items }
+ }
+
+ pub(crate) fn height(&self) -> i32 {
+ self.row_height as i32 * self.items.len() as i32
+ }
+
+ /// Same shape as `ContextMenu::row_at`.
+ pub(crate) fn row_at(&self, x: i32, y: i32) -> Option<usize> {
+ if x < self.pos.0 || x >= self.pos.0 + self.width as i32 {
+ return None;
+ }
+ let rel_y = y - self.pos.1;
+ if rel_y < 0 || rel_y >= self.height() {
+ return None;
+ }
+ Some((rel_y / self.row_height as i32) as usize)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use std::path::PathBuf;
+
+ fn icon(kind: IconKind, target: &str) -> DesktopIcon {
+ DesktopIcon { id: "x".into(), label: "x".into(), kind, target: PathBuf::from(target), cell: (0, 0), selected: false }
+ }
+
+ #[test]
+ fn image_file_with_a_configured_command_gets_the_wallpaper_row() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::File, "pic.png"), (0, 0), "swww img");
+ assert_eq!(menu.items.len(), 2);
+ assert_eq!(menu.items[1].0, "Set as Wallpaper");
+ }
+
+ #[test]
+ fn image_file_with_no_configured_command_has_no_wallpaper_row() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::File, "pic.png"), (0, 0), "");
+ assert_eq!(menu.items.len(), 1, "Open only");
+ }
+
+ #[test]
+ fn non_image_file_has_no_wallpaper_row_even_with_a_command_configured() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::File, "notes.txt"), (0, 0), "swww img");
+ assert_eq!(menu.items.len(), 1, "Open only");
+ }
+
+ #[test]
+ fn a_folder_never_gets_the_wallpaper_row() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::Folder, "pic.png"), (0, 0), "swww img");
+ assert_eq!(menu.items.len(), 1, "a directory named like an image is still not a file");
+ }
+
+ #[test]
+ fn desktop_menu_offers_new_folder_and_refresh() {
+ let menu = DesktopMenu::open_for_desktop((10, 10));
+ assert_eq!(menu.items[0].0, "New Folder");
+ assert_eq!(menu.items[1].0, "Refresh");
+ }
+
+ #[test]
+ fn row_at_maps_a_point_to_the_right_row() {
+ let menu = DesktopMenu::open_for_desktop((100, 100));
+ assert_eq!(menu.row_at(150, 100), Some(0));
+ assert_eq!(menu.row_at(150, 100 + ROW_HEIGHT as i32), Some(1));
+ assert_eq!(menu.row_at(150, 100 + menu.height()), None, "just below the menu");
+ }
+}