srdusr
aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--crates/core/src/lock_config.rs16
-rw-r--r--crates/srdwm/src/main.rs1
-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
9 files changed, 200 insertions, 29 deletions
diff --git a/crates/core/src/lock_config.rs b/crates/core/src/lock_config.rs
index 92ba0d0..15e4637 100644
--- a/crates/core/src/lock_config.rs
+++ b/crates/core/src/lock_config.rs
@@ -18,6 +18,21 @@
#[derive(Debug, Clone, PartialEq)]
pub struct LockConfig {
pub box_bg: (u8, u8, u8),
+ /// How opaque the password box's own panel is, `0.0`-`1.0`.
+ ///
+ /// `0.0` by default: no panel, no border, no rounded rectangle - just
+ /// the password dots and status text over whatever the lock background
+ /// already is (the blurred wallpaper, or whatever the user set).
+ /// Reported live as "there is still a weird box where password is
+ /// typed. should just be the blurred background or whatever user set.
+ /// not that horrendus box".
+ ///
+ /// Raising it brings the panel back at that opacity, using `box_bg`
+ /// and `box_border` as before, for anyone who wants a solid field to
+ /// type into. It is a float rather than a bool so a faint scrim - the
+ /// usual compromise when a wallpaper is too busy to read text against
+ /// - is reachable without a second setting.
+ pub box_opacity: f32,
pub box_border: (u8, u8, u8),
pub text_color: (u8, u8, u8),
pub error_color: (u8, u8, u8),
@@ -59,6 +74,7 @@ impl Default for LockConfig {
fn default() -> Self {
Self {
box_bg: (0x2e, 0x34, 0x40), // Nord dark, matches ThemeConfig::titlebar_bg
+ box_opacity: 0.0, // no panel at all - see the field's own doc comment
box_border: (0x88, 0xc0, 0xd0), // Nord blue, matches ThemeConfig::default_border_color
text_color: (0xec, 0xef, 0xf4), // Nord light
error_color: (0xbf, 0x61, 0x6a), // Nord red, matches the theme's own `error` colour
diff --git a/crates/srdwm/src/main.rs b/crates/srdwm/src/main.rs
index 94da20c..1cad90f 100644
--- a/crates/srdwm/src/main.rs
+++ b/crates/srdwm/src/main.rs
@@ -341,6 +341,7 @@ fn apply_general_settings(engine: &Engine, wm: &Rc<RefCell<WindowManager>>) {
if let Some(rgb) = srdwm_core::parse_hex_color(&engine.get_string("theme.lock.error_color", "#bf616a")) {
lock.error_color = rgb;
}
+ lock.box_opacity = engine.get_f64("theme.lock.box_opacity", lock.box_opacity as f64).clamp(0.0, 1.0) as f32;
lock.corner_radius = engine.get_f64("theme.lock.corner_radius", lock.corner_radius as f64).max(0.0) as u32;
lock.blur_radius = engine.get_f64("theme.lock.blur_radius", lock.blur_radius as f64).max(0.0) as u32;
lock.show_caps_lock = engine.get_bool("theme.lock.show_caps_lock", lock.show_caps_lock);
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 {