diff options
| -rw-r--r-- | crates/core/src/lock_config.rs | 16 | ||||
| -rw-r--r-- | crates/srdwm/src/main.rs | 1 | ||||
| -rw-r--r-- | crates/wayland/src/decoration.rs | 2 | ||||
| -rw-r--r-- | crates/wayland/src/decoration/font.rs | 54 | ||||
| -rw-r--r-- | crates/wayland/src/desktop_icons.rs | 24 | ||||
| -rw-r--r-- | crates/wayland/src/input/pointer.rs | 52 | ||||
| -rw-r--r-- | crates/wayland/src/native_lock.rs | 31 | ||||
| -rw-r--r-- | crates/wayland/src/state/desktop_icons.rs | 23 | ||||
| -rw-r--r-- | crates/wayland/src/udev/render.rs | 26 |
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 { |