srdusr
aboutsummaryrefslogtreecommitdiffstats
path: root/crates/wayland
diff options
context:
space:
mode:
authorsrdusr <[email protected]>2026-05-11 16:44:00 +0200
committersrdusr <[email protected]>2026-05-11 16:44:00 +0200
commitcaec1e7c355bbe6437afe87cd3dab6b64fb91e0a (patch)
tree5ecab25d2ebc36949ac4129b35e35334a575d563 /crates/wayland
parentbaabd91411dacd179c690f724b6cbfe6270cb131 (diff)
downloadsrdwm-caec1e7c355bbe6437afe87cd3dab6b64fb91e0a.tar.gz
srdwm-caec1e7c355bbe6437afe87cd3dab6b64fb91e0a.zip
Three live bug reports after a restart: icon drag, lock cursor, lock box
All three reported directly after the owner restarted into today's build. Desktop icons could not be dragged at all in single-click mode. The press handler opened the icon immediately when general.desktop_icon_single_click was on, so the branch that starts a drag was unreachable and an icon could never be moved. Deciding activation on press cannot distinguish a click from the first instant of a drag. Every press on an icon now starts a potential drag and release decides which it was, using a 4px movement threshold that latches once exceeded. Double-click mode goes through the same path, so both modes now drag identically. The lock screen drew no cursor. The cursor push in the udev render loop sits inside `if !locked`, and a locked head renders only the lock element list, so nothing drew a pointer - and on a bare TTY nothing else does. The on-screen keyboard's clicks were being handled correctly the whole time (native_lock_click); they simply could not be aimed. The pointer is now prepended to the lock element list, above the UI it is used to click. The password field's opaque panel is gone. New LockConfig::box_opacity, default 0.0: no fill, no border, no rounded rectangle, just the dots and status text over the blurred background. Raising it restores the panel at that opacity for anyone who wants a solid field. Drawing text on a transparent surface needed a new blit_glyph_over: the existing blit_glyph blends against one flat opaque colour and writes alpha 255, which would have turned every glyph into a block of the assumed background - the same box with its middle removed. VERIFICATION STATUS, stated plainly: all three are code-complete and the suite passes, but none is confirmed on screen. The nested backend's capture pass does not draw the desktop icon grid (a gap already recorded in winit/capture.rs), so the icon drag cannot be checked by screenshot there, and aiming blind is what this project's own rules forbid. The two lock changes were not visually checked either. 515 tests pass, clippy clean.
Diffstat (limited to 'crates/wayland')
-rw-r--r--crates/wayland/src/decoration.rs2
-rw-r--r--crates/wayland/src/decoration/font.rs54
-rw-r--r--crates/wayland/src/desktop_icons.rs24
-rw-r--r--crates/wayland/src/input/pointer.rs52
-rw-r--r--crates/wayland/src/native_lock.rs31
-rw-r--r--crates/wayland/src/state/desktop_icons.rs23
-rw-r--r--crates/wayland/src/udev/render.rs26
7 files changed, 183 insertions, 29 deletions
diff --git a/crates/wayland/src/decoration.rs b/crates/wayland/src/decoration.rs
index 9ff43b6..70b39c4 100644
--- a/crates/wayland/src/decoration.rs
+++ b/crates/wayland/src/decoration.rs
@@ -42,7 +42,7 @@ pub(crate) use border::{border_bottom_visible_rows, border_top_visible_rows};
pub(crate) use buttons::HOVER_GLYPH_DURATION;
pub(crate) use color::{mix_rgb, rgb_to_bgra};
pub(crate) use corners::{round_bottom_corners, round_top_corners};
-pub(crate) use font::{blit_glyph, find_system_font, measure_text_width, FONT_PIXELS, TEXT_LEFT_PADDING};
+pub(crate) use font::{blit_glyph, blit_glyph_over, find_system_font, measure_text_width, FONT_PIXELS, TEXT_LEFT_PADDING};
pub use shadow::{shadow_bitmap, shadow_rect, shadow_rect_clipped};
pub(crate) use shadow::{SHADOW_MAX_ALPHA, SHADOW_SIZE};
pub use titlebar::render_titlebar;
diff --git a/crates/wayland/src/decoration/font.rs b/crates/wayland/src/decoration/font.rs
index 6a2a312..74d5047 100644
--- a/crates/wayland/src/decoration/font.rs
+++ b/crates/wayland/src/decoration/font.rs
@@ -121,6 +121,60 @@ fn find_best_font(dir: &std::path::Path, best: &mut Option<(std::path::PathBuf,
}
}
+/// Composites a glyph over whatever is already in `buf`, honouring the
+/// destination's own alpha - straight (non-premultiplied) BGRA, the same
+/// convention `rgb_to_bgra` writes everywhere else in this module.
+///
+/// [`blit_glyph`] below blends the glyph against one flat opaque colour and
+/// writes alpha 255, which is right for a surface that has an opaque
+/// background of its own (a titlebar, a menu row). It is wrong for text
+/// drawn straight onto a transparent surface: every glyph pixel would
+/// become an opaque block of the assumed background colour, so the text
+/// would sit in a solid rectangle of exactly the box this was meant to
+/// remove. Used by the lock screen's password field, which draws over the
+/// blurred wallpaper with no panel behind it.
+#[allow(clippy::too_many_arguments)]
+pub(crate) fn blit_glyph_over(
+ buf: &mut [u8],
+ width: usize,
+ height: usize,
+ glyph_x: i32,
+ glyph_y: i32,
+ metrics: &fontdue::Metrics,
+ coverage: &[u8],
+ foreground: (u8, u8, u8),
+) {
+ for row in 0..metrics.height {
+ let y = glyph_y + row as i32;
+ if y < 0 || y as usize >= height {
+ continue;
+ }
+ for col in 0..metrics.width {
+ let x = glyph_x + col as i32;
+ if x < 0 || x as usize >= width {
+ continue;
+ }
+ let sa = coverage[row * metrics.width + col] as f32 / 255.0;
+ if sa <= 0.0 {
+ continue;
+ }
+ let idx = (y as usize * width + x as usize) * 4;
+ let (db, dg, dr, da) = (buf[idx] as f32, buf[idx + 1] as f32, buf[idx + 2] as f32, buf[idx + 3] as f32 / 255.0);
+ let out_a = sa + da * (1.0 - sa);
+ if out_a <= 0.0 {
+ continue;
+ }
+ // Un-premultiplied source-over: each channel weighted by its
+ // own coverage, then divided back out by the result's alpha.
+ let mix = |fg: f32, dst: f32| -> u8 { (((fg * sa) + (dst * da * (1.0 - sa))) / out_a).round().clamp(0.0, 255.0) as u8 };
+ buf[idx] = mix(foreground.2 as f32, db);
+ buf[idx + 1] = mix(foreground.1 as f32, dg);
+ buf[idx + 2] = mix(foreground.0 as f32, dr);
+ buf[idx + 3] = (out_a * 255.0).round().clamp(0.0, 255.0) as u8;
+ }
+ }
+}
+
#[allow(clippy::too_many_arguments)]
pub(crate) fn blit_glyph(
buf: &mut [u8],
diff --git a/crates/wayland/src/desktop_icons.rs b/crates/wayland/src/desktop_icons.rs
index c3b573a..b556bef 100644
--- a/crates/wayland/src/desktop_icons.rs
+++ b/crates/wayland/src/desktop_icons.rs
@@ -79,8 +79,32 @@ pub(crate) struct DesktopIconDrag {
/// `(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))>,
+ /// The icon the press actually landed on, and where the pointer was.
+ /// Release compares against this to decide whether the gesture was a
+ /// click or a drag - see `DRAG_THRESHOLD`.
+ pub(crate) pressed: (String, (i32, i32)),
+ /// Set once the pointer leaves `DRAG_THRESHOLD` of `pressed`. A press
+ /// that never does is a click, not a move.
+ pub(crate) moved: bool,
}
+/// How far the pointer must travel from the press point before a desktop
+/// icon gesture counts as a drag rather than a click, in logical pixels.
+///
+/// Every press on an icon now starts a *potential* drag, and release
+/// decides which it was. Without that, single-click mode (`general.
+/// desktop_icon_single_click`) made dragging impossible: the press opened
+/// the icon immediately, so the drag branch was unreachable and an icon
+/// could never be moved at all. Reported live as "i can't move the desktop
+/// icons anymore since making it single click ... impossible to hold and
+/// drag move desktop icons".
+///
+/// Small, because the cost is asymmetric: too large and a genuine short
+/// drag is swallowed as a click that opens something the user did not want
+/// opened; too small only means an unusually shaky click moves an icon a
+/// cell, which is visible and trivially undone.
+pub(crate) const DRAG_THRESHOLD: i32 = 4;
+
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 e82b25f..85d1eff 100644
--- a/crates/wayland/src/input/pointer.rs
+++ b/crates/wayland/src/input/pointer.rs
@@ -729,27 +729,30 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
.and_then(|icons| icons.icon_at(pos.x as i32, pos.y as i32).map(|(i, origin)| (icons.icons[i].id.clone(), origin)));
match icon_hit {
Some((id, origin)) => {
- let single_click_opens = state.wm.borrow().desktop_icon_single_click;
- if single_click_opens || state.is_double_click_icon(&id, time) {
+ // Always a *potential* drag, whichever activation
+ // mode is configured. Opening is decided on
+ // release instead (see the release handler's own
+ // `desktop_icon_click` branch), because deciding
+ // it here cannot distinguish a click from the
+ // first instant of a drag - which is exactly what
+ // made single-click mode unable to move an icon at
+ // all.
+ //
+ // 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.open_desktop_icon(&id);
- } else {
- // 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));
}
+ state.start_desktop_icon_drag(&id, origin, (pos.x as i32, pos.y as i32));
}
// Genuinely bare desktop, not just "no icon under the
// pointer" - starts a rubber-band selection instead of
@@ -806,6 +809,17 @@ pub(crate) fn handle_pointer_button(state: &mut CompState, pos: Point<f64, Logic
// A no-op via `Option::take()` when no icon drag was active --
// always checked on release, same as `was_dragging`/`was_resizing`
// below, just for a desktop icon instead of a window.
+ // A press that never travelled far enough to be a drag is a click:
+ // decide activation here, not on press, so an icon stays draggable
+ // in single-click mode. `time` is the release's, used consistently
+ // for both clicks of a double so the interval stays comparable.
+ if let Some(id) = state.desktop_icon_click() {
+ let single_click_opens = state.wm.borrow().desktop_icon_single_click;
+ if single_click_opens || state.is_double_click_icon(&id, time) {
+ state.select_desktop_icon(Some(&id));
+ state.open_desktop_icon(&id);
+ }
+ }
state.end_desktop_icon_drag();
state.end_desktop_marquee();
// Releasing a drag over a cell of the drag-triggered Snap-Layouts
diff --git a/crates/wayland/src/native_lock.rs b/crates/wayland/src/native_lock.rs
index 14c4f21..96e4ac9 100644
--- a/crates/wayland/src/native_lock.rs
+++ b/crates/wayland/src/native_lock.rs
@@ -780,14 +780,22 @@ fn render_header_box(native: &NativeLock, theme: &srdwm_core::LockConfig) -> (Ve
/// `rgb_to_bgra`), promoted to `pub(crate)` there rather than duplicated
/// here.
fn render_ui_box(native: &NativeLock, theme: &srdwm_core::LockConfig) -> (Vec<u8>, (i32, i32)) {
- use crate::decoration::{blit_glyph, find_system_font, rgb_to_bgra, round_bottom_corners, round_top_corners, FONT_PIXELS, TEXT_LEFT_PADDING};
+ use crate::decoration::{blit_glyph_over, find_system_font, rgb_to_bgra, round_bottom_corners, round_top_corners, FONT_PIXELS, TEXT_LEFT_PADDING};
const WIDTH: usize = 340;
const HEIGHT: usize = 120;
let mut buf = vec![0u8; WIDTH * HEIGHT * 4];
- let bg = rgb_to_bgra(theme.box_bg, 255);
- for px in buf.chunks_exact_mut(4) {
- px.copy_from_slice(&bg);
+ // `box_opacity` 0 (the default) leaves the buffer fully transparent, so
+ // the dots and status text sit straight on the blurred background with
+ // no panel behind them at all - see `LockConfig::box_opacity`. Above
+ // 0 the panel comes back at that opacity, border and rounded corners
+ // included, exactly as it used to look at 1.0.
+ let panel_alpha = (theme.box_opacity.clamp(0.0, 1.0) * 255.0).round() as u8;
+ if panel_alpha > 0 {
+ let bg = rgb_to_bgra(theme.box_bg, panel_alpha);
+ for px in buf.chunks_exact_mut(4) {
+ px.copy_from_slice(&bg);
+ }
}
let font = find_system_font();
@@ -814,7 +822,7 @@ fn render_ui_box(native: &NativeLock, theme: &srdwm_core::LockConfig) -> (Vec<u8
if metrics.width > 0 && metrics.height > 0 {
let glyph_x = pen_x + metrics.xmin as f32;
let glyph_y = y - metrics.height as f32 - metrics.ymin as f32;
- blit_glyph(&mut buf, WIDTH, HEIGHT, glyph_x.round() as i32, glyph_y.round() as i32, &metrics, &coverage, theme.box_bg, color);
+ blit_glyph_over(&mut buf, WIDTH, HEIGHT, glyph_x.round() as i32, glyph_y.round() as i32, &metrics, &coverage, color);
}
pen_x += metrics.advance_width;
}
@@ -827,7 +835,11 @@ fn render_ui_box(native: &NativeLock, theme: &srdwm_core::LockConfig) -> (Vec<u8
// (GNOME, macOS): dimmer than the real text colour, never mistakable
// for an actual password once one is entered.
if native.password.is_empty() && !native.show_error {
- let placeholder = crate::decoration::mix_rgb(theme.text_color, theme.box_bg, 0.5);
+ // Dimmed toward the panel colour when there is a panel, and toward
+ // plain black otherwise - mixing toward a background that is not
+ // actually drawn would tint the placeholder for no visible reason.
+ let toward = if panel_alpha > 0 { theme.box_bg } else { (0, 0, 0) };
+ let placeholder = crate::decoration::mix_rgb(theme.text_color, toward, 0.5);
draw_line_centered("Enter Password", 65.0, placeholder);
} else {
let dots: String = std::iter::repeat_n(theme.dot_char, native.password.chars().count()).collect();
@@ -850,6 +862,13 @@ fn render_ui_box(native: &NativeLock, theme: &srdwm_core::LockConfig) -> (Vec<u8
// this box's size read as a thin, easy-to-miss hairline rather than a
// deliberate frame around the box.
const BORDER: usize = 2;
+ // Border and corner rounding belong to the panel: with no panel there
+ // is nothing to frame, and a floating rounded outline around bare text
+ // is the "horrendus box" with its middle removed rather than the box
+ // gone.
+ if panel_alpha == 0 {
+ return (buf, (WIDTH as i32, HEIGHT as i32));
+ }
let border_px = rgb_to_bgra(theme.box_border, 255);
for t in 0..BORDER {
for x in 0..WIDTH {
diff --git a/crates/wayland/src/state/desktop_icons.rs b/crates/wayland/src/state/desktop_icons.rs
index ad7796f..69b9b62 100644
--- a/crates/wayland/src/state/desktop_icons.rs
+++ b/crates/wayland/src/state/desktop_icons.rs
@@ -342,7 +342,8 @@ impl CompState {
(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 });
+ self.desktop_icon_drag =
+ Some(crate::desktop_icons::DesktopIconDrag { grab_offset, primary_pos: primary_top_left, members, pressed: (id.to_string(), pointer), moved: false });
for id in changed {
self.rebuild_icon_buffer(&id);
}
@@ -354,6 +355,14 @@ impl CompState {
/// `WindowManager::update_resize`'s own per-motion-event update.
pub(crate) fn update_desktop_icon_drag(&mut self, pointer: (i32, i32)) {
if let Some(drag) = &mut self.desktop_icon_drag {
+ // Latched, never cleared: a gesture that once travelled far
+ // enough to be a drag stays one even if the pointer wanders
+ // back over the press point before release.
+ if (pointer.0 - drag.pressed.1 .0).abs() > crate::desktop_icons::DRAG_THRESHOLD
+ || (pointer.1 - drag.pressed.1 .1).abs() > crate::desktop_icons::DRAG_THRESHOLD
+ {
+ drag.moved = true;
+ }
drag.primary_pos = (pointer.0 - drag.grab_offset.0, pointer.1 - drag.grab_offset.1);
}
}
@@ -363,6 +372,18 @@ impl CompState {
/// 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.
+ /// The icon a still-open drag was pressed on, if the pointer never
+ /// travelled far enough for it to become a real drag - i.e. the
+ /// gesture was a click. `None` once it has moved, or with no drag
+ /// open at all.
+ ///
+ /// Read on release so the open happens there rather than on press,
+ /// which is what makes an icon draggable in single-click mode at all.
+ pub(crate) fn desktop_icon_click(&self) -> Option<String> {
+ let drag = self.desktop_icon_drag.as_ref()?;
+ (!drag.moved).then(|| drag.pressed.0.clone())
+ }
+
pub(crate) fn end_desktop_icon_drag(&mut self) {
let Some(drag) = self.desktop_icon_drag.take() else { return };
// The cell math below needs the origin of whichever monitor the
diff --git a/crates/wayland/src/udev/render.rs b/crates/wayland/src/udev/render.rs
index 4170f47..aa05529 100644
--- a/crates/wayland/src/udev/render.rs
+++ b/crates/wayland/src/udev/render.rs
@@ -1145,6 +1145,24 @@ impl CompState {
Vec::new()
};
+ // The pointer, on a locked head too. A locked head renders only
+ // the lock element list below, and the cursor push further up
+ // is inside `if !locked`, so a locked screen had no pointer
+ // drawn at all - and on a bare TTY nothing else draws one.
+ // The on-screen keyboard's clicks were being handled correctly
+ // the whole time (`native_lock_click`); they simply could not
+ // be aimed. Reported live as "there is no mouse input to
+ // interact with the screen keyboard in lockscreen".
+ //
+ // Built here rather than reusing `custom_elements` because that
+ // list is assembled inside the same `if !locked` branch. First
+ // in the list, so it stays above the lock UI it is used to
+ // click on.
+ let locked_cursor: Vec<crate::elements::OverlayElement<PixmanRenderer>> = if locked {
+ crate::cursor::render_elements(&cursor_status, &cursor_buffers, &mut udev.renderer, udev.pointer_pos, origin, udev.heads[index].size)
+ } else {
+ Vec::new()
+ };
let head = &mut udev.heads[index];
let mut framebuffer = match udev.renderer.bind(&mut head.buffers[back].image) {
Ok(fb) => fb,
@@ -1161,13 +1179,17 @@ impl CompState {
// external locker's surface the same way, and nothing else;
// unlocked heads draw the normal scene.
let result = if locked && is_native {
+ let mut elements = locked_cursor;
+ elements.extend(native_elements.into_iter().map(crate::elements::OverlayElement::Memory));
head.damage_tracker
- .render_output(&mut udev.renderer, &mut framebuffer, 0, &native_elements, [0.0, 0.0, 0.0, 1.0])
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &elements, [0.0, 0.0, 0.0, 1.0])
.map(|r| (r.damage.is_some(), Vec::new()))
.map_err(|e| e.to_string())
} else if locked {
+ let mut elements = locked_cursor;
+ elements.extend(lock_elements.into_iter().map(crate::elements::OverlayElement::Surface));
head.damage_tracker
- .render_output(&mut udev.renderer, &mut framebuffer, 0, &lock_elements, [0.0, 0.0, 0.0, 1.0])
+ .render_output(&mut udev.renderer, &mut framebuffer, 0, &elements, [0.0, 0.0, 0.0, 1.0])
.map(|r| (r.damage.is_some(), Vec::new()))
.map_err(|e| e.to_string())
} else {