srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2025-08-17 01:54:00 +0200
committersrdusr <[email protected]>2025-08-17 01:54:00 +0200
commitb8376bd631a72f7484c24663699a7d4c813f600e (patch)
treec8c84725e3c66598df3f619dd96aa69b6eede26e /crates
parentbd6aadf4d166c05f22f1ab6cdfe2e815e071f162 (diff)
downloadsrdwm-b8376bd631a72f7484c24663699a7d4c813f600e.tar.gz
srdwm-b8376bd631a72f7484c24663699a7d4c813f600e.zip
Fix desktop icons v1 regressions: bar overlap, wrong order; add proper menus
Live testing found v1 genuinely broken, not just rough: 1. Icons weren't rendering reliably at all - ensure_desktop_icons only ever computed the grid's origin once, on whichever render pass happened to be first. AGS's own top bar registers its exclusive zone after that first pass, so origin got permanently baked in at the pre-bar geometry. Confirmed live via a temporary diagnostic log. Fixed by re-deriving origin from the primary monitor's current geometry on every call instead of just the first. 2. Fixed icons (Home/Computer/Trash) always sorted before real files -- confirmed wrong via direct question. The whole list now sorts alphabetically by label, case-insensitive, fixed icons included. 3. "Set as Wallpaper" was the wrong feature: removed entirely (DesktopMenuAction::SetWallpaper, general.wallpaper_command, is_image_path). The user wants that handled by their real file manager once opened, not reimplemented here. Also adds real menu functionality per "where are all the options": Rename (inline text edit, new CompState::renaming_icon field and keyboard redirect mirroring NativeLock::password's existing precedent), Delete (moves to ~/.local/share/Trash per the freedesktop.org spec, new trash.rs module, same-filesystem case, no confirmation - this is the reversible move-to-trash, not a permanent delete), Empty Trash on the Trash icon, and Open Terminal Here / Open in File Manager on the bare-desktop menu (new general.terminal config key). 133 wayland-crate tests (up from 106), full workspace build and clippy clean.
Diffstat (limited to 'crates')
-rw-r--r--crates/config/src/engine/support.rs7
-rw-r--r--crates/core/src/manager/mod.rs18
-rw-r--r--crates/srdwm/src/main.rs4
-rw-r--r--crates/wayland/src/desktop_icons.rs52
-rw-r--r--crates/wayland/src/desktop_menu.rs110
-rw-r--r--crates/wayland/src/input/keyboard.rs19
-rw-r--r--crates/wayland/src/lib.rs1
-rw-r--r--crates/wayland/src/state/desktop_icons.rs200
-rw-r--r--crates/wayland/src/state/mod.rs8
-rw-r--r--crates/wayland/src/trash.rs228
-rw-r--r--crates/wayland/src/udev/platform.rs1
-rw-r--r--crates/wayland/src/udev/render.rs4
-rw-r--r--crates/wayland/src/winit/connect.rs1
13 files changed, 552 insertions, 101 deletions
diff --git a/crates/config/src/engine/support.rs b/crates/config/src/engine/support.rs
index 76fbe62..7537fce 100644
--- a/crates/config/src/engine/support.rs
+++ b/crates/config/src/engine/support.rs
@@ -166,10 +166,9 @@ pub(super) fn default_config() -> HashMap<String, ConfigValue> {
// Double-click by default - see `WindowManager::desktop_icon_single_
// click`'s own doc comment.
set("general.desktop_icon_single_click", Bool(false));
- // Empty by default - see `WindowManager::wallpaper_command`'s own doc
- // comment: empty means "Set as Wallpaper" doesn't appear at all, not
- // "configured to do nothing".
- set("general.wallpaper_command", String(std::string::String::new()));
+ // Empty by default - see `WindowManager::terminal`'s own doc comment:
+ // empty tries a short list of common terminals on `$PATH`.
+ set("general.terminal", String(std::string::String::new()));
// Deliberately *not* seeded here, unlike every other `general.*` key --
// its actual default differs by backend (GLES/winit: on; udev/Pixman:
// off, an untested-on-real-hardware CPU cost too real to default to on
diff --git a/crates/core/src/manager/mod.rs b/crates/core/src/manager/mod.rs
index c2f67e4..848eca5 100644
--- a/crates/core/src/manager/mod.rs
+++ b/crates/core/src/manager/mod.rs
@@ -231,16 +231,12 @@ pub struct WindowManager {
/// file managers) default the other way, hence this being a real
/// config option rather than a hardcoded choice.
pub desktop_icon_single_click: bool,
- /// External program a desktop image-file icon's "Set as Wallpaper"
- /// action shells out to, read from `general.wallpaper_command`. Empty
- /// (the default) means that action doesn't appear at all - srdwm
- /// never draws the wallpaper itself (an external layer-shell client's
- /// job, see `WaylandState`'s own doc comment on viewporter), so unlike
- /// `file_manager`'s `xdg-open` fallback there is no universal command
- /// to guess here; the user names their own wallpaper tool (`swww img`,
- /// `awww img`, ...) or the action stays hidden rather than silently
- /// doing nothing when clicked.
- pub wallpaper_command: String,
+ /// External program the bare-desktop menu's "Open Terminal Here"
+ /// action shells out to (with `~/Desktop` as its working directory),
+ /// read from `general.terminal`. Empty (the default) tries a short
+ /// list of common terminals on `$PATH` - there's no `xdg-open`-
+ /// equivalent for "a shell", unlike `file_manager`.
+ pub terminal: String,
/// The whole-screen colour treatment currently active (night light's
/// warm tint or reading mode's desaturation), live-settable via `srd
/// set night_light`/`srd set reading_mode` - see [`ColorFilter`]. Off
@@ -405,7 +401,7 @@ impl WindowManager {
desktop_icons_enabled: true,
file_manager: String::new(),
desktop_icon_single_click: false,
- wallpaper_command: String::new(),
+ terminal: String::new(),
color_filter: ColorFilter::None,
focus_follows_mouse: false,
auto_raise: false,
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index 0f55c00..bc5c840 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -175,7 +175,7 @@ fn apply_general_settings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
let desktop_icons = engine.get_bool("general.desktop_icons", true);
let file_manager = engine.get_string("general.file_manager", "");
let desktop_icon_single_click = engine.get_bool("general.desktop_icon_single_click", false);
- let wallpaper_command = engine.get_string("general.wallpaper_command", "");
+ let terminal = engine.get_string("general.terminal", "");
let focus_follows_mouse = engine.get_bool("general.focus_follows_mouse", false);
let auto_raise = engine.get_bool("general.auto_raise", false);
@@ -304,7 +304,7 @@ fn apply_general_settings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
wm.desktop_icons_enabled = desktop_icons;
wm.file_manager = file_manager;
wm.desktop_icon_single_click = desktop_icon_single_click;
- wm.wallpaper_command = wallpaper_command;
+ wm.terminal = terminal;
wm.focus_follows_mouse = focus_follows_mouse;
wm.auto_raise = auto_raise;
wm.theme = theme;
diff --git a/crates/wayland/src/desktop_icons.rs b/crates/wayland/src/desktop_icons.rs
index 470a965..4ba54f4 100644
--- a/crates/wayland/src/desktop_icons.rs
+++ b/crates/wayland/src/desktop_icons.rs
@@ -85,17 +85,6 @@ fn desktop_dir(home: &Path) -> PathBuf {
home.join("Desktop")
}
-/// `$XDG_DATA_HOME/Trash/files`, else `~/.local/share/Trash/files` - the
-/// freedesktop.org Trash spec's home-filesystem trash directory. Only the
-/// same-filesystem case is handled anywhere in this codebase (see this
-/// feature's own plan doc for why the per-mountpoint `.Trash-$uid`
-/// fallback is out of scope for now); this is purely where the Trash
-/// desktop icon opens to, nothing currently moves a file into it.
-fn trash_files_dir(home: &Path) -> PathBuf {
- let data_home = std::env::var("XDG_DATA_HOME").map(PathBuf::from).unwrap_or_else(|_| home.join(".local/share"));
- data_home.join("Trash/files")
-}
-
/// Rebuilds the full icon list from the real filesystem: the three fixed
/// icons first, then one per direct, non-hidden entry of `~/Desktop`
/// (creating that directory if it doesn't exist yet, matching how a real
@@ -132,7 +121,7 @@ pub(crate) fn rescan(saved: &HashMap<String, (i32, i32)>, rows_per_column: i32)
id: "trash".to_string(),
label: "Trash".to_string(),
kind: IconKind::Trash,
- target: home_dir().map(|h| trash_files_dir(&h)).unwrap_or_else(|| PathBuf::from("/")),
+ target: home_dir().map(|h| crate::trash::files_dir(&h)).unwrap_or_else(|| PathBuf::from("/")),
cell: (0, 0),
selected: false,
},
@@ -141,7 +130,7 @@ pub(crate) fn rescan(saved: &HashMap<String, (i32, i32)>, rows_per_column: i32)
let desktop = desktop_dir(&home);
if std::fs::create_dir_all(&desktop).is_ok() {
if let Ok(entries) = std::fs::read_dir(&desktop) {
- let mut files: Vec<(String, bool)> = entries
+ let files: Vec<(String, bool)> = entries
.filter_map(|e| e.ok())
.filter_map(|e| {
let name = e.file_name().to_string_lossy().into_owned();
@@ -152,7 +141,6 @@ pub(crate) fn rescan(saved: &HashMap<String, (i32, i32)>, rows_per_column: i32)
Some((name, is_dir))
})
.collect();
- files.sort_by(|a, b| a.0.cmp(&b.0));
for (name, is_dir) in files {
let target = desktop.join(&name);
icons.push(DesktopIcon {
@@ -167,6 +155,14 @@ pub(crate) fn rescan(saved: &HashMap<String, (i32, i32)>, rows_per_column: i32)
}
}
}
+ // One alphabetical list, fixed icons included - not fixed-three-then-
+ // files. Confirmed directly: the fixed shortcuts shouldn't always come
+ // first just because they're synthetic rather than real files.
+ // Case-insensitive so "computer"/"Computer" and a real lowercase
+ // filename interleave the way a user actually expects, not by raw
+ // byte value (which would put every uppercase name before any
+ // lowercase one).
+ icons.sort_by_key(|a| a.label.to_lowercase());
assign_cells(&mut icons, saved, rows_per_column);
icons
}
@@ -212,12 +208,30 @@ mod tests {
use super::*;
#[test]
- fn fixed_icons_always_come_first_in_a_stable_order() {
+ fn the_three_fixed_icons_sort_alphabetically_among_themselves() {
+ // Reported live, confirmed via direct question: fixed icons must
+ // NOT always come before real files just because they're
+ // synthetic - the whole list sorts by label together. This
+ // checks that sort using only the three fixed icons (present on
+ // any machine, unlike a specific `~/Desktop` file), whose labels
+ // - "Computer", "Home", "Trash" - already happen to be in
+ // alphabetical order, so a correct sort leaves them exactly
+ // where `rescan` built them.
let icons = rescan(&HashMap::new(), 10);
- assert!(icons.len() >= 3, "at least the three fixed icons");
- assert_eq!(icons[0].id, "home");
- assert_eq!(icons[1].id, "computer");
- assert_eq!(icons[2].id, "trash");
+ let fixed: Vec<&str> = icons.iter().filter(|i| matches!(i.kind, IconKind::Home | IconKind::Computer | IconKind::Trash)).map(|i| i.label.as_str()).collect();
+ assert_eq!(fixed, vec!["Computer", "Home", "Trash"]);
+ }
+
+ #[test]
+ fn sorting_is_case_insensitive_and_covers_the_whole_list() {
+ let mut icons = [
+ DesktopIcon { id: "zebra".into(), label: "zebra".into(), kind: IconKind::File, target: PathBuf::new(), cell: (0, 0), selected: false },
+ DesktopIcon { id: "Home".into(), label: "Home".into(), kind: IconKind::Home, target: PathBuf::new(), cell: (0, 0), selected: false },
+ DesktopIcon { id: "apple".into(), label: "apple".into(), kind: IconKind::File, target: PathBuf::new(), cell: (0, 0), selected: false },
+ ];
+ icons.sort_by_key(|a| a.label.to_lowercase());
+ let labels: Vec<&str> = icons.iter().map(|i| i.label.as_str()).collect();
+ assert_eq!(labels, vec!["apple", "Home", "zebra"], "a fixed icon's label interleaves with real filenames, not always first");
}
#[test]
diff --git a/crates/wayland/src/desktop_menu.rs b/crates/wayland/src/desktop_menu.rs
index 810e639..bd0bf34 100644
--- a/crates/wayland/src/desktop_menu.rs
+++ b/crates/wayland/src/desktop_menu.rs
@@ -9,12 +9,24 @@ use crate::desktop_icons::{DesktopIcon, IconKind};
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),
+ /// Enter inline rename mode for this real file/folder icon - see
+ /// `CompState::renaming_icon`'s own doc comment.
+ Rename(String),
+ /// Move this real file/folder into `~/.local/share/Trash` - no
+ /// confirmation, same as every mainstream file manager: this is the
+ /// reversible move-to-trash, not a permanent delete.
+ Delete(String),
+ /// Empties `~/.local/share/Trash` entirely - same no-confirmation
+ /// convention as `Delete`.
+ EmptyTrash,
NewFolder,
+ /// Spawns a terminal with `~/Desktop` as its working directory --
+ /// `general.terminal`, or a common-binary fallback list if unset.
+ OpenTerminalHere,
+ /// Opens `~/Desktop` itself in `general.file_manager`/`xdg-open` - the
+ /// concrete path to a real file manager's own richer menu (cut/copy/
+ /// paste, properties, ...), deliberately not reimplemented here.
+ OpenInFileManager,
Refresh,
}
@@ -28,33 +40,36 @@ pub(crate) struct DesktopMenu {
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())));
- }
+ /// Right-click on `icon` itself - the action set depends on what kind
+ /// of icon it is, not one fixed list: a real file/folder gets Open/
+ /// Rename/Delete (real filesystem operations); Home/Computer (fixed
+ /// shortcuts to somewhere, not real files of their own) get Open only,
+ /// since renaming or deleting the shortcut itself isn't a meaningful
+ /// action; Trash gets Open/Empty Trash instead of Rename/Delete, since
+ /// "delete the trash" and "rename the trash" aren't real trash
+ /// operations the way "empty it" is.
+ pub(crate) fn open_for_icon(icon: &DesktopIcon, pos: (i32, i32)) -> Self {
+ let items = match icon.kind {
+ IconKind::Trash => vec![("Open", DesktopMenuAction::OpenIcon(icon.id.clone())), ("Empty Trash", DesktopMenuAction::EmptyTrash)],
+ IconKind::Home | IconKind::Computer => vec![("Open", DesktopMenuAction::OpenIcon(icon.id.clone()))],
+ IconKind::Folder | IconKind::File => vec![
+ ("Open", DesktopMenuAction::OpenIcon(icon.id.clone())),
+ ("Rename", DesktopMenuAction::Rename(icon.id.clone())),
+ ("Delete", DesktopMenuAction::Delete(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".
+ /// Right-click on bare desktop (no icon under the pointer).
pub(crate) fn open_for_desktop(pos: (i32, i32)) -> Self {
- let items = vec![("New Folder", DesktopMenuAction::NewFolder), ("Refresh", DesktopMenuAction::Refresh)];
+ let items = vec![
+ ("New Folder", DesktopMenuAction::NewFolder),
+ ("Open Terminal Here", DesktopMenuAction::OpenTerminalHere),
+ ("Open in File Manager", DesktopMenuAction::OpenInFileManager),
+ ("Refresh", DesktopMenuAction::Refresh),
+ ];
Self { pos, width: MENU_WIDTH, row_height: ROW_HEIGHT, items }
}
@@ -80,40 +95,45 @@ 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 }
+ fn icon(kind: IconKind) -> DesktopIcon {
+ DesktopIcon { id: "x".into(), label: "x".into(), kind, target: PathBuf::new(), 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");
+ fn a_real_file_gets_open_rename_and_delete() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::File), (0, 0));
+ let labels: Vec<&str> = menu.items.iter().map(|(l, _)| *l).collect();
+ assert_eq!(labels, vec!["Open", "Rename", "Delete"]);
}
#[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");
+ fn a_real_folder_gets_open_rename_and_delete_too() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::Folder), (0, 0));
+ let labels: Vec<&str> = menu.items.iter().map(|(l, _)| *l).collect();
+ assert_eq!(labels, vec!["Open", "Rename", "Delete"]);
}
#[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");
+ fn home_and_computer_get_open_only() {
+ for kind in [IconKind::Home, IconKind::Computer] {
+ let menu = DesktopMenu::open_for_icon(&icon(kind), (0, 0));
+ let labels: Vec<&str> = menu.items.iter().map(|(l, _)| *l).collect();
+ assert_eq!(labels, vec!["Open"], "shortcuts aren't real files - no rename/delete");
+ }
}
#[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");
+ fn trash_gets_open_and_empty_trash_not_rename_or_delete() {
+ let menu = DesktopMenu::open_for_icon(&icon(IconKind::Trash), (0, 0));
+ let labels: Vec<&str> = menu.items.iter().map(|(l, _)| *l).collect();
+ assert_eq!(labels, vec!["Open", "Empty Trash"]);
}
#[test]
- fn desktop_menu_offers_new_folder_and_refresh() {
+ fn desktop_menu_offers_the_full_set() {
let menu = DesktopMenu::open_for_desktop((10, 10));
- assert_eq!(menu.items[0].0, "New Folder");
- assert_eq!(menu.items[1].0, "Refresh");
+ let labels: Vec<&str> = menu.items.iter().map(|(l, _)| *l).collect();
+ assert_eq!(labels, vec!["New Folder", "Open Terminal Here", "Open in File Manager", "Refresh"]);
}
#[test]
diff --git a/crates/wayland/src/input/keyboard.rs b/crates/wayland/src/input/keyboard.rs
index ae367f4..f1b117f 100644
--- a/crates/wayland/src/input/keyboard.rs
+++ b/crates/wayland/src/input/keyboard.rs
@@ -65,6 +65,25 @@ pub(crate) fn handle_keyboard_key_event<B: smithay::backend::input::InputBackend
return;
}
+ // An in-progress desktop-icon rename claims every keystroke the same
+ // way the native lock's password entry does just above - see
+ // `CompState::renaming_icon`'s own doc comment. Not gated by `state.
+ // lock.locked` (already excluded, since that branch already returned)
+ // - this only ever runs during ordinary, unlocked interactive use.
+ if state.renaming_icon.is_some() {
+ if key_state == BackendKeyState::Pressed {
+ keyboard.input::<(), _>(state, keycode, key_state, serial, time, |data, _mods, handle| {
+ let utf8 = xkbcommon::xkb::keysym_to_utf8(handle.modified_sym());
+ let name = keysym_name_for(handle).unwrap_or_default();
+ data.desktop_icon_rename_key(&name, &utf8);
+ FilterResult::Intercept(())
+ });
+ } else {
+ keyboard.input::<(), _>(state, keycode, key_state, serial, time, |_, _, _| FilterResult::Intercept(()));
+ }
+ return;
+ }
+
let bound_keys = state.bound_keys.clone();
let matched: Option<(String, Modifiers)> =
keyboard.input(state, keycode, key_state, serial, time, move |data, mods, handle| {
diff --git a/crates/wayland/src/lib.rs b/crates/wayland/src/lib.rs
index fc1aefb..91168a1 100644
--- a/crates/wayland/src/lib.rs
+++ b/crates/wayland/src/lib.rs
@@ -66,6 +66,7 @@ mod rounded_corners;
mod rounded_corners_pixman;
mod screencopy;
mod state;
+mod trash;
mod udev;
mod winit;
mod workspace;
diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs
index b1b802e..bcef073 100644
--- a/crates/wayland/src/state/desktop_icons.rs
+++ b/crates/wayland/src/state/desktop_icons.rs
@@ -3,24 +3,49 @@
//! 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_icons::{DesktopIcons, IconKind, CELL_HEIGHT, CELL_WIDTH, GRID_MARGIN};
use crate::desktop_menu::{DesktopMenu, DesktopMenuAction};
+/// Common terminal binaries tried, in order, when `general.terminal` is
+/// unset - first one actually found on `$PATH` wins. There's no `xdg-
+/// open`-equivalent dispatcher for "a shell" the way there is for a file,
+/// so unlike `file_manager`'s empty-means-`xdg-open` fallback, this needs
+/// a real candidate list.
+const TERMINAL_CANDIDATES: &[&str] = &["alacritty", "kitty", "wezterm", "foot", "gnome-terminal", "konsole", "xterm"];
+
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.
+ /// Populates `self.desktop_icons` on first call, then keeps its
+ /// `origin` continuously re-anchored to the primary monitor's own
+ /// *current* usable geometry on every later call - cheap enough
+ /// (one tuple comparison) to run unconditionally at the top of every
+ /// render pass. Does nothing at all when the config flag is off.
+ ///
+ /// The re-anchoring is the fix for a real, reported bug: this used to
+ /// return immediately once `self.desktop_icons` was `Some`, computing
+ /// `origin` exactly once, on whichever render pass happened to be
+ /// first. AGS's own top bar registers its exclusive zone (`general.
+ /// desktop_icons`' fix's own PR: `srd monitors`, `Monitor::geometry`
+ /// already excludes it) only once that separate client has connected
+ /// and committed - reliably *after* this compositor's own first
+ /// render pass, confirmed live via a temporary diagnostic log: origin
+ /// baked in at `(1936, 16)` (bar not yet registered, `geometry.y` was
+ /// still `0`) and never moved again even once `srd monitors` reported
+ /// the bar's real 34-40px reservation moments later - reported live
+ /// as "Home is still being overlapped by AGS's top bar." Re-deriving
+ /// `origin` every call (not rebuilding the icon list - render
+ /// position reads `origin` fresh at push time, never baked into a
+ /// cached glyph buffer) closes this permanently, for the bar, a dock
+ /// on any edge, or any later exclusive-zone change alike.
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);
+ if let Some(icons) = &mut self.desktop_icons {
+ icons.origin = origin;
+ return;
+ }
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);
@@ -55,11 +80,19 @@ impl CompState {
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);
+ // While this exact icon is mid-rename, show the live-edited buffer
+ // (with a trailing caret) instead of its real, on-disk label - so
+ // typing is visible without touching the filesystem until Enter
+ // actually commits it (`desktop_icon_rename_key`).
+ let label: std::borrow::Cow<str> = match &self.renaming_icon {
+ Some((rid, buf)) if rid == id => format!("{buf}_").into(),
+ _ => icon.label.as_str().into(),
+ };
let data = decoration::render_desktop_icon(
CELL_WIDTH as u32,
CELL_HEIGHT as u32,
icon.kind,
- &icon.label,
+ &label,
icon.selected,
theme.titlebar_fg_focused,
label_color,
@@ -184,15 +217,120 @@ impl CompState {
}
}
- pub(crate) fn set_desktop_icon_as_wallpaper(&mut self, id: &str) {
+ /// Enters inline rename mode for `id`, pre-filled with its current
+ /// label - only ever called for a real file/folder icon (`desktop_
+ /// menu.rs`'s own `open_for_icon` never offers Rename for anything
+ /// else), but harmless if that assumption is ever wrong: `commit_
+ /// icon_rename` re-checks the icon's kind before touching the
+ /// filesystem.
+ pub(crate) fn start_rename_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 command = self.wm.borrow().wallpaper_command.clone();
- if command.is_empty() {
+ self.renaming_icon = Some((id.to_string(), icon.label.clone()));
+ self.rebuild_icon_buffer(id);
+ }
+
+ /// Routes one keystroke into the in-progress rename buffer - same
+ /// shape as `native_lock_key`'s own `BackSpace`/`Return`/`Escape`/
+ /// printable-character handling.
+ pub(crate) fn desktop_icon_rename_key(&mut self, name: &str, utf8: &str) {
+ let Some((id, mut buffer)) = self.renaming_icon.take() else { return };
+ match name {
+ "BackSpace" => {
+ buffer.pop();
+ self.renaming_icon = Some((id.clone(), buffer));
+ self.rebuild_icon_buffer(&id);
+ }
+ "Return" | "KP_Enter" => self.commit_icon_rename(&id, &buffer),
+ "Escape" => self.rebuild_icon_buffer(&id),
+ _ => {
+ if !utf8.is_empty() && utf8.chars().all(|c| !c.is_control()) {
+ buffer.push_str(utf8);
+ }
+ self.renaming_icon = Some((id.clone(), buffer));
+ self.rebuild_icon_buffer(&id);
+ }
+ }
+ }
+
+ /// Renames the real file/folder on disk and carries its saved grid
+ /// cell forward under the new name, so a rename doesn't also bump the
+ /// icon to a fresh default position. A blank name, or renaming
+ /// anything other than a real file/folder (shouldn't happen - see
+ /// `start_rename_icon`'s own doc comment), just cancels with no
+ /// filesystem change.
+ fn commit_icon_rename(&mut self, id: &str, new_name: &str) {
+ let new_name = new_name.trim();
+ let Some(icons) = &self.desktop_icons else { return };
+ let Some(icon) = icons.icons.iter().find(|i| i.id == id) else { return };
+ if new_name.is_empty() || !matches!(icon.kind, IconKind::Folder | IconKind::File) {
+ self.rebuild_icon_buffer(id);
+ return;
+ }
+ let Some(parent) = icon.target.parent() else {
+ self.rebuild_icon_buffer(id);
+ return;
+ };
+ let new_path = parent.join(new_name);
+ if let Err(e) = std::fs::rename(&icon.target, &new_path) {
+ log::warn!("desktop_icons: couldn't rename {:?} to {new_name:?}: {e}", icon.target);
+ self.rebuild_icon_buffer(id);
+ return;
+ }
+ let cell = icon.cell;
+ crate::desktop_icons_state::save_icon(new_name, cell);
+ self.desktop_icon_buffers.remove(id);
+ self.refresh_desktop_icons();
+ }
+
+ /// Moves the real file/folder into `~/.local/share/Trash` - see
+ /// `trash.rs`'s own module doc comment for why this needs no
+ /// confirmation.
+ pub(crate) fn delete_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 };
+ if let Err(e) = crate::trash::move_to_trash(&icon.target) {
+ log::warn!("desktop_icons: couldn't move {:?} to trash: {e}", icon.target);
return;
}
- spawn_shell(&format!("{command} {}", shell_quote(&target)));
+ self.desktop_icon_buffers.remove(id);
+ self.refresh_desktop_icons();
+ }
+
+ pub(crate) fn empty_trash(&mut self) {
+ let Ok(home) = std::env::var("HOME").map(std::path::PathBuf::from) else { return };
+ crate::trash::empty(&home);
+ self.desktop_icon_buffers.remove("trash");
+ }
+
+ /// Spawns `general.terminal` (or the first of `TERMINAL_CANDIDATES`
+ /// found on `$PATH`) with `~/Desktop` as its working directory.
+ pub(crate) fn open_terminal_here(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = format!("{home}/Desktop");
+ let configured = self.wm.borrow().terminal.clone();
+ let command = if !configured.is_empty() {
+ Some(configured)
+ } else {
+ TERMINAL_CANDIDATES.iter().find(|bin| on_path(bin)).map(|s| s.to_string())
+ };
+ match command {
+ Some(command) => spawn_shell_in_dir(&command, &desktop),
+ None => log::warn!("desktop_icons: no terminal found on $PATH and general.terminal is unset"),
+ }
+ }
+
+ /// Opens `~/Desktop` itself via `file_manager`/`xdg-open` - the
+ /// concrete path to a real file manager's own richer menu.
+ pub(crate) fn open_desktop_in_file_manager(&mut self) {
+ let Ok(home) = std::env::var("HOME") else { return };
+ let desktop = format!("{home}/Desktop");
+ let file_manager = self.wm.borrow().file_manager.clone();
+ if file_manager.is_empty() {
+ spawn_shell(&format!("xdg-open {}", shell_quote(&desktop)));
+ } else {
+ spawn_shell(&format!("{file_manager} {}", shell_quote(&desktop)));
+ }
}
/// Creates `~/Desktop/New Folder`, de-duplicated as `New Folder (2)`,
@@ -217,8 +355,7 @@ impl CompState {
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);
+ let menu = DesktopMenu::open_for_icon(icon, pos);
self.build_desktop_menu_buffer(menu);
}
@@ -244,8 +381,12 @@ impl CompState {
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::Rename(id) => self.start_rename_icon(&id),
+ DesktopMenuAction::Delete(id) => self.delete_desktop_icon(&id),
+ DesktopMenuAction::EmptyTrash => self.empty_trash(),
DesktopMenuAction::NewFolder => self.new_desktop_folder(),
+ DesktopMenuAction::OpenTerminalHere => self.open_terminal_here(),
+ DesktopMenuAction::OpenInFileManager => self.open_desktop_in_file_manager(),
DesktopMenuAction::Refresh => self.refresh_desktop_icons(),
}
}
@@ -302,6 +443,29 @@ fn spawn_shell(command: &str) {
}
}
+/// Same fire-and-forget shell spawn as `spawn_shell`, but with a working
+/// directory - `open_terminal_here`'s own reason to exist as a separate
+/// function rather than reusing `spawn_shell` with a `cd` prefix, which
+/// would need its own quoting for `dir`.
+fn spawn_shell_in_dir(command: &str, dir: &str) {
+ #[cfg(unix)]
+ let result = std::process::Command::new("sh").arg("-c").arg(command).current_dir(dir).spawn();
+ #[cfg(windows)]
+ let result = std::process::Command::new("cmd").arg("/C").arg(command).current_dir(dir).spawn();
+ if let Err(e) = result {
+ log::warn!("desktop_icons: spawn '{command}' in {dir:?} failed: {e}");
+ }
+}
+
+/// Whether `bin` resolves to a real, regular file somewhere on `$PATH` --
+/// enough to pick a terminal candidate without a full `which`-equivalent
+/// (no executable-bit/`PATHEXT` check on Windows; this whole feature is
+/// Linux/BSD-desktop-shaped already, see `TERMINAL_CANDIDATES`' own
+/// entries).
+fn on_path(bin: &str) -> bool {
+ std::env::var_os("PATH").map(|paths| std::env::split_paths(&paths).any(|dir| dir.join(bin).is_file())).unwrap_or(false)
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/crates/wayland/src/state/mod.rs b/crates/wayland/src/state/mod.rs
index 66f9ec6..0fb303e 100644
--- a/crates/wayland/src/state/mod.rs
+++ b/crates/wayland/src/state/mod.rs
@@ -372,6 +372,14 @@ pub(crate) struct CompState {
/// `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)>,
+ /// An in-progress inline rename: the icon's own id, and the live-
+ /// edited name buffer - same shape as `NativeLock::password`
+ /// (`native_lock.rs`), the existing precedent for redirecting real
+ /// keyboard input into a plain string buffer instead of the normally-
+ /// focused client. `None` whenever no rename is in progress; the
+ /// keyboard handler checks this the same way it already checks
+ /// `state.lock.locked`.
+ pub(crate) renaming_icon: Option<(String, String)>,
pub(crate) wm: Rc<RefCell<WindowManager>>,
pub(crate) surface_to_id: HashMap<WlSurface, WindowId>,
pub(crate) id_to_window: HashMap<WindowId, DWindow>,
diff --git a/crates/wayland/src/trash.rs b/crates/wayland/src/trash.rs
new file mode 100644
index 0000000..a44d9c8
--- /dev/null
+++ b/crates/wayland/src/trash.rs
@@ -0,0 +1,228 @@
+//! freedesktop.org Trash specification - move-to-trash and empty-trash.
+//! Same-filesystem case only: `~/Desktop` and `~/.local/share/Trash` are
+//! virtually always the same filesystem in practice, and a cross-
+//! filesystem move needs its own per-mountpoint `<mountpoint>/.Trash-$uid`
+//! directory the spec defines separately - not attempted here.
+//! `std::fs::rename` across filesystems fails with a clear `EXDEV` error
+//! rather than silently doing the wrong thing, so this degrades to a
+//! reported failure, never silent data loss.
+//!
+//! No confirmation prompt anywhere in this module, by design: moving
+//! something to trash is the reversible operation, not the destructive
+//! one - every mainstream file manager (Nemo included) treats it exactly
+//! this way, gating only a *permanent* delete (which nothing here does)
+//! behind a dialog.
+
+use std::path::{Path, PathBuf};
+
+/// `$XDG_DATA_HOME/Trash`, else `~/.local/share/Trash`.
+fn trash_root(home: &Path) -> PathBuf {
+ let data_home = std::env::var("XDG_DATA_HOME").map(PathBuf::from).unwrap_or_else(|_| home.join(".local/share"));
+ data_home.join("Trash")
+}
+
+pub(crate) fn files_dir(home: &Path) -> PathBuf {
+ trash_root(home).join("files")
+}
+
+fn info_dir(home: &Path) -> PathBuf {
+ trash_root(home).join("info")
+}
+
+// Not called yet - reserved for the Trash icon's own full/empty glyph
+// selection, landing later in this same round alongside real icon-theme
+// rendering.
+#[allow(dead_code)]
+pub(crate) fn is_empty(home: &Path) -> bool {
+ std::fs::read_dir(files_dir(home)).map(|mut d| d.next().is_none()).unwrap_or(true)
+}
+
+/// De-duplicates `name` against whatever's already in `dir` - same
+/// `"name (2)"`, `"name (3)"`, ... scheme as `desktop_icons::new_desktop_
+/// folder`'s own "New Folder (2)". The spec requires unique names in
+/// `files/` but doesn't mandate a specific collision scheme.
+fn dedup_name(dir: &Path, name: &str) -> String {
+ if !dir.join(name).exists() {
+ return name.to_string();
+ }
+ let (stem, ext) = match name.rsplit_once('.') {
+ Some((s, e)) if !s.is_empty() => (s.to_string(), Some(e.to_string())),
+ _ => (name.to_string(), None),
+ };
+ let mut n = 2;
+ loop {
+ let candidate = match &ext {
+ Some(e) => format!("{stem} ({n}).{e}"),
+ None => format!("{stem} ({n})"),
+ };
+ if !dir.join(&candidate).exists() {
+ return candidate;
+ }
+ n += 1;
+ }
+}
+
+/// Moves `path` into the trash, writing its `.trashinfo` metadata first --
+/// the spec requires that file to exist before the item lands in `files/`,
+/// so a reader never sees a trashed item with no metadata explaining it.
+/// Rolls the info file back if the actual move then fails, so a failure
+/// here never leaves an orphaned `.trashinfo` for something that's still
+/// sitting exactly where it started.
+pub(crate) fn move_to_trash(path: &Path) -> std::io::Result<()> {
+ let home = std::env::var("HOME").map(PathBuf::from).map_err(|_| std::io::Error::other("HOME not set"))?;
+ let files = files_dir(&home);
+ let info = info_dir(&home);
+ std::fs::create_dir_all(&files)?;
+ std::fs::create_dir_all(&info)?;
+ let name = path.file_name().and_then(|n| n.to_str()).ok_or_else(|| std::io::Error::new(std::io::ErrorKind::InvalidInput, "no filename"))?;
+ let trashed_name = dedup_name(&files, name);
+ let info_content = format!("[Trash Info]\nPath={}\nDeletionDate={}\n", percent_encode_path(path), now_iso8601());
+ let info_path = info.join(format!("{trashed_name}.trashinfo"));
+ std::fs::write(&info_path, info_content)?;
+ if let Err(e) = std::fs::rename(path, files.join(&trashed_name)) {
+ let _ = std::fs::remove_file(&info_path);
+ return Err(e);
+ }
+ Ok(())
+}
+
+/// Removes every entry under both `files/` and `info/`.
+pub(crate) fn empty(home: &Path) {
+ for dir in [files_dir(home), info_dir(home)] {
+ let Ok(entries) = std::fs::read_dir(&dir) else { continue };
+ for entry in entries.flatten() {
+ let path = entry.path();
+ let result = if path.is_dir() { std::fs::remove_dir_all(&path) } else { std::fs::remove_file(&path) };
+ if let Err(e) = result {
+ log::warn!("trash: couldn't remove {path:?}: {e}");
+ }
+ }
+ }
+}
+
+/// Minimal percent-encoding for the `.trashinfo` `Path=` field - only
+/// needs to be a *safe* encoding (every byte a later parser could choke
+/// on gets escaped), not a byte-perfect implementation, since nothing in
+/// this codebase ever reads this field back; a real trash-viewing tool
+/// (Nemo) is what actually parses it.
+fn percent_encode_path(path: &Path) -> String {
+ let s = path.to_string_lossy();
+ let mut out = String::with_capacity(s.len());
+ for b in s.bytes() {
+ let safe = b.is_ascii_alphanumeric() || matches!(b, b'/' | b'-' | b'_' | b'.' | b'~');
+ if safe {
+ out.push(b as char);
+ } else {
+ out.push_str(&format!("%{b:02X}"));
+ }
+ }
+ out
+}
+
+/// `YYYY-MM-DDTHH:MM:SS`, local system clock - hand-rolled rather than
+/// pulling in `chrono`/`time` for one timestamp field. `civil_from_days`
+/// is Howard Hinnant's well-known public-domain algorithm for converting
+/// a day count into a proleptic-Gregorian (year, month, day); this
+/// codebase has no other date-formatting need that would justify a real
+/// date/time dependency.
+fn now_iso8601() -> String {
+ let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap_or_default();
+ let days = (now.as_secs() / 86400) as i64;
+ let secs_of_day = now.as_secs() % 86400;
+ let (y, m, d) = civil_from_days(days);
+ let (h, mi, s) = (secs_of_day / 3600, (secs_of_day % 3600) / 60, secs_of_day % 60);
+ format!("{y:04}-{m:02}-{d:02}T{h:02}:{mi:02}:{s:02}")
+}
+
+fn civil_from_days(z: i64) -> (i64, u32, u32) {
+ let z = z + 719468;
+ let era = if z >= 0 { z } else { z - 146096 } / 146097;
+ let doe = (z - era * 146097) as u64;
+ let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
+ let y = yoe as i64 + era * 400;
+ let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
+ let mp = (5 * doy + 2) / 153;
+ let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
+ let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
+ (if m <= 2 { y + 1 } else { y }, m, d)
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn dedup_name_keeps_an_unused_name_as_is() {
+ let dir = std::env::temp_dir().join(format!("srdwm-trash-test-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ assert_eq!(dedup_name(&dir, "report.txt"), "report.txt");
+ std::fs::remove_dir_all(&dir).unwrap();
+ }
+
+ #[test]
+ fn dedup_name_numbers_a_collision_preserving_the_extension() {
+ let dir = std::env::temp_dir().join(format!("srdwm-trash-test-ext-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("report.txt"), b"").unwrap();
+ assert_eq!(dedup_name(&dir, "report.txt"), "report (2).txt");
+ std::fs::remove_dir_all(&dir).unwrap();
+ }
+
+ #[test]
+ fn dedup_name_numbers_a_collision_with_no_extension() {
+ let dir = std::env::temp_dir().join(format!("srdwm-trash-test-noext-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&dir);
+ std::fs::create_dir_all(&dir).unwrap();
+ std::fs::write(dir.join("New Folder"), b"").unwrap();
+ assert_eq!(dedup_name(&dir, "New Folder"), "New Folder (2)");
+ std::fs::remove_dir_all(&dir).unwrap();
+ }
+
+ #[test]
+ fn percent_encode_path_leaves_ordinary_path_characters_alone() {
+ assert_eq!(percent_encode_path(Path::new("/home/x/Desktop/report.txt")), "/home/x/Desktop/report.txt");
+ }
+
+ #[test]
+ fn percent_encode_path_escapes_a_space() {
+ assert_eq!(percent_encode_path(Path::new("/home/x/New Folder")), "/home/x/New%20Folder");
+ }
+
+ #[test]
+ fn civil_from_days_matches_the_unix_epoch() {
+ assert_eq!(civil_from_days(0), (1970, 1, 1));
+ }
+
+ #[test]
+ fn civil_from_days_matches_a_known_recent_date() {
+ // 2026-08-26 is 20,691 days after the epoch (verified independently:
+ // `python3 -c "import datetime; print((datetime.date(2026,8,26) -
+ // datetime.date(1970,1,1)).days)"`).
+ assert_eq!(civil_from_days(20691), (2026, 8, 26));
+ }
+
+ #[test]
+ fn move_to_trash_and_empty_round_trip_on_a_real_temp_file() {
+ let home = std::env::temp_dir().join(format!("srdwm-trash-home-{}", std::process::id()));
+ let _ = std::fs::remove_dir_all(&home);
+ std::fs::create_dir_all(&home).unwrap();
+ // `move_to_trash` reads real $HOME/$XDG_DATA_HOME - not safely
+ // parallel-testable via env var override (same reasoning `monitor_
+ // layout.rs`'s own tests give), so this test exercises `empty`/
+ // `is_empty` directly against a fabricated trash layout instead of
+ // going through `move_to_trash`'s own env lookup.
+ let files = files_dir(&home);
+ let info = info_dir(&home);
+ std::fs::create_dir_all(&files).unwrap();
+ std::fs::create_dir_all(&info).unwrap();
+ std::fs::write(files.join("old.txt"), b"x").unwrap();
+ std::fs::write(info.join("old.txt.trashinfo"), b"[Trash Info]\n").unwrap();
+ assert!(!is_empty(&home));
+ empty(&home);
+ assert!(is_empty(&home));
+ assert_eq!(std::fs::read_dir(&info).unwrap().count(), 0);
+ std::fs::remove_dir_all(&home).unwrap();
+ }
+}
diff --git a/crates/wayland/src/udev/platform.rs b/crates/wayland/src/udev/platform.rs
index 2633f16..6f6cbe8 100644
--- a/crates/wayland/src/udev/platform.rs
+++ b/crates/wayland/src/udev/platform.rs
@@ -236,6 +236,7 @@ impl UdevPlatform {
desktop_menu: None,
desktop_menu_buffer: None,
last_icon_click: None,
+ renaming_icon: None,
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 73aaa82..2d0a063 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -925,8 +925,8 @@ impl CompState {
// shows through everywhere an icon doesn't draw). See
// `desktop_icons.rs`'s own module doc comment.
for (pos, buffer) in &desktop_icon_render_list {
- let pos = ((pos.0 - origin.x) as f64, (pos.1 - origin.y) as f64);
- match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, pos, buffer, None, None, None, Kind::Unspecified) {
+ let local_pos = ((pos.0 - origin.x) as f64, (pos.1 - origin.y) as f64);
+ match MemoryRenderBufferRenderElement::from_buffer(&mut udev.renderer, local_pos, buffer, None, None, None, Kind::Unspecified) {
Ok(elem) => custom_elements.push(crate::elements::OverlayElement::Memory(elem)),
Err(e) => log::warn!("udev: failed to import desktop icon buffer: {e}"),
}
diff --git a/crates/wayland/src/winit/connect.rs b/crates/wayland/src/winit/connect.rs
index 227cf33..140dc58 100644
--- a/crates/wayland/src/winit/connect.rs
+++ b/crates/wayland/src/winit/connect.rs
@@ -157,6 +157,7 @@ impl WaylandPlatform {
desktop_menu: None,
desktop_menu_buffer: None,
last_icon_click: None,
+ renaming_icon: None,
wm: wm.clone(),
surface_to_id: HashMap::new(),
id_to_window: HashMap::new(),