Fix lock-screen input and wallpaper handling
This commit is contained in:
1 parent
bbc5b27c25
commit
cfdd2a148a
7 files changed
+350
-57
No files matched your search
+28
-12
@@ -120,18 +120,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if self.session_lock.active() {
|
if self.session_lock.active() {
|
||||||
// An already-bound IME or drag client may install a grab after the
|
self.prepare_lock_input();
|
||||||
// lock request. Never let that grab receive subsequent lock input.
|
|
||||||
if let Some(keyboard) = seat.get_keyboard() {
|
|
||||||
keyboard.unset_grab(self);
|
|
||||||
}
|
|
||||||
if let Some(pointer) = seat.get_pointer() {
|
|
||||||
pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), 0);
|
|
||||||
}
|
|
||||||
if let Some(touch) = seat.get_touch() {
|
|
||||||
touch.unset_grab(self);
|
|
||||||
}
|
|
||||||
self.focus_lock_surface();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.input_capture.active()
|
if self.input_capture.active()
|
||||||
@@ -514,6 +503,33 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||||
let serial = SERIAL_COUNTER.next_serial();
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
if self.session_lock.active() {
|
||||||
|
if event.state() == ButtonState::Pressed {
|
||||||
|
self.focus_window_at(pointer.current_location(), serial, true);
|
||||||
|
if !pointer.is_grabbed() {
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
self.lock_surface_under(pointer.current_location()),
|
||||||
|
&MotionEvent {
|
||||||
|
location: pointer.current_location(),
|
||||||
|
serial,
|
||||||
|
time: event.time_msec(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pointer.button(
|
||||||
|
self,
|
||||||
|
&ButtonEvent {
|
||||||
|
button: event.button_code(),
|
||||||
|
state: event.state(),
|
||||||
|
serial,
|
||||||
|
time: event.time_msec(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Native popup_done destroys Iced's window immediately. For
|
// Native popup_done destroys Iced's window immediately. For
|
||||||
// effects-capable shell popups, release input now but defer
|
// effects-capable shell popups, release input now but defer
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
//! logical pixels. Content, clips and decorations share those exact edges.
|
//! logical pixels. Content, clips and decorations share those exact edges.
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
use ferese_animation::AnimatedRect;
|
||||||
|
|
||||||
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates};
|
use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates};
|
||||||
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
|
use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform};
|
||||||
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
use smithay::backend::renderer::utils::{CommitCounter, DamageSet};
|
||||||
|
|||||||
@@ -273,6 +273,58 @@ impl Ferese {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn prepare_lock_input(&mut self) {
|
||||||
|
// Reject desktop grabs installed after acquisition, but keep the lock
|
||||||
|
// owner's implicit click grab intact until its button release.
|
||||||
|
if let Some(keyboard) = self.seat.get_keyboard()
|
||||||
|
&& keyboard.is_grabbed()
|
||||||
|
{
|
||||||
|
keyboard.unset_grab(self);
|
||||||
|
}
|
||||||
|
if let Some(pointer) = self.seat.get_pointer() {
|
||||||
|
let lock_click = pointer
|
||||||
|
.with_grab(|_, grab| {
|
||||||
|
grab.is::<smithay::input::pointer::ClickGrab<Ferese>>()
|
||||||
|
&& grab.start_data().focus.as_ref().is_some_and(|(surface, _)| {
|
||||||
|
self.session_lock
|
||||||
|
.surfaces
|
||||||
|
.values()
|
||||||
|
.any(|lock| lock.alive() && lock.wl_surface() == surface)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.unwrap_or(false);
|
||||||
|
if pointer.is_grabbed() && !lock_click {
|
||||||
|
pointer.unset_grab(self, SERIAL_COUNTER.next_serial(), self.last_pointer_time);
|
||||||
|
}
|
||||||
|
let stale_focus = pointer.current_focus().is_some_and(|surface| {
|
||||||
|
!self
|
||||||
|
.session_lock
|
||||||
|
.surfaces
|
||||||
|
.values()
|
||||||
|
.any(|lock| lock.alive() && lock.wl_surface() == &surface)
|
||||||
|
});
|
||||||
|
if stale_focus {
|
||||||
|
let location = pointer.current_location();
|
||||||
|
pointer.motion(
|
||||||
|
self,
|
||||||
|
self.lock_surface_under(location),
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location,
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: self.last_pointer_time,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.frame(self);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(touch) = self.seat.get_touch()
|
||||||
|
&& touch.is_grabbed()
|
||||||
|
{
|
||||||
|
touch.unset_grab(self);
|
||||||
|
}
|
||||||
|
self.focus_lock_surface();
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn focus_lock_surface(&mut self) {
|
pub(crate) fn focus_lock_surface(&mut self) {
|
||||||
let surface = self
|
let surface = self
|
||||||
.focused_output()
|
.focused_output()
|
||||||
@@ -289,10 +341,10 @@ impl Ferese {
|
|||||||
.map(|(_, surface)| surface)
|
.map(|(_, surface)| surface)
|
||||||
})
|
})
|
||||||
.map(|surface| surface.wl_surface().clone());
|
.map(|surface| surface.wl_surface().clone());
|
||||||
self.seat
|
let keyboard = self.seat.get_keyboard().unwrap();
|
||||||
.get_keyboard()
|
if keyboard.current_focus() != surface {
|
||||||
.unwrap()
|
keyboard.set_focus(self, surface, SERIAL_COUNTER.next_serial());
|
||||||
.set_focus(self, surface, SERIAL_COUNTER.next_serial());
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn lock_frame_presented(&mut self, output: &Output) {
|
pub(crate) fn lock_frame_presented(&mut self, output: &Output) {
|
||||||
|
|||||||
@@ -626,3 +626,66 @@ fn acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame() {
|
|||||||
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
|
assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_)));
|
||||||
replacement.expect_locked();
|
replacement.expect_locked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces() {
|
||||||
|
let Some(mut fixture) =
|
||||||
|
Fixture::new("session_lock::tests::lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces")
|
||||||
|
else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let output = fixture.output("lock-pointer");
|
||||||
|
let mut owner = fixture.acquire();
|
||||||
|
let surface = owner.surface(&mut fixture, &output);
|
||||||
|
let pointer = fixture.state.seat.get_pointer().unwrap();
|
||||||
|
let serial = SERIAL_COUNTER.next_serial();
|
||||||
|
pointer.motion(
|
||||||
|
&mut fixture.state,
|
||||||
|
Some((surface.wl_surface().clone(), (0.0, 0.0).into())),
|
||||||
|
&smithay::input::pointer::MotionEvent {
|
||||||
|
location: (10.0, 10.0).into(),
|
||||||
|
serial,
|
||||||
|
time: 1,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
pointer.button(
|
||||||
|
&mut fixture.state,
|
||||||
|
&smithay::input::pointer::ButtonEvent {
|
||||||
|
button: 0x110,
|
||||||
|
state: smithay::backend::input::ButtonState::Pressed,
|
||||||
|
serial,
|
||||||
|
time: 2,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(pointer.is_grabbed());
|
||||||
|
fixture.state.prepare_lock_input();
|
||||||
|
assert!(
|
||||||
|
pointer.is_grabbed(),
|
||||||
|
"the lock owner's click must survive until release"
|
||||||
|
);
|
||||||
|
pointer.button(
|
||||||
|
&mut fixture.state,
|
||||||
|
&smithay::input::pointer::ButtonEvent {
|
||||||
|
button: 0x110,
|
||||||
|
state: smithay::backend::input::ButtonState::Released,
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 3,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(!pointer.is_grabbed());
|
||||||
|
pointer.button(
|
||||||
|
&mut fixture.state,
|
||||||
|
&smithay::input::pointer::ButtonEvent {
|
||||||
|
button: 0x110,
|
||||||
|
state: smithay::backend::input::ButtonState::Pressed,
|
||||||
|
serial: SERIAL_COUNTER.next_serial(),
|
||||||
|
time: 4,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert!(pointer.is_grabbed());
|
||||||
|
fixture.state.session_lock.surfaces.clear();
|
||||||
|
fixture.state.prepare_lock_input();
|
||||||
|
assert!(!pointer.is_grabbed(), "a retired surface cannot keep a lock grab");
|
||||||
|
assert_eq!(pointer.current_focus(), None);
|
||||||
|
assert_eq!(fixture.state.seat.get_keyboard().unwrap().current_focus(), None);
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
//! One decoded image and one texture per GPU context, not double-buffered
|
//! One wallpaper texture per GPU context. Decoded pixels are staging data,
|
||||||
//! full-screen UI surfaces per monitor. Resize changes sampling only.
|
//! released after upload and decoded again when a new context needs them.
|
||||||
|
//! Resize changes sampling only.
|
||||||
use std::cell::Cell;
|
use std::cell::Cell;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
@@ -279,7 +280,7 @@ impl WallpaperState {
|
|||||||
width = pixels.width(),
|
width = pixels.width(),
|
||||||
height = pixels.height(),
|
height = pixels.height(),
|
||||||
bytes = pixels.as_raw().len(),
|
bytes = pixels.as_raw().len(),
|
||||||
"decoded compositor wallpaper once"
|
"decoded compositor wallpaper for texture upload"
|
||||||
);
|
);
|
||||||
|
|
||||||
self.pixels = Some(pixels);
|
self.pixels = Some(pixels);
|
||||||
@@ -540,6 +541,10 @@ mod tests {
|
|||||||
fn decoder_wakes_an_idle_event_loop() {
|
fn decoder_wakes_an_idle_event_loop() {
|
||||||
use smithay::reexports::calloop::EventLoop;
|
use smithay::reexports::calloop::EventLoop;
|
||||||
use smithay::reexports::calloop::timer::Timer;
|
use smithay::reexports::calloop::timer::Timer;
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let path = directory.path().join("wallpaper.png");
|
||||||
|
let expected = image::RgbaImage::from_pixel(2, 2, image::Rgba([32, 80, 160, 255]));
|
||||||
|
expected.save(&path).unwrap();
|
||||||
let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap();
|
let mut event_loop = EventLoop::<WallpaperState>::try_new().unwrap();
|
||||||
event_loop
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
@@ -548,11 +553,19 @@ mod tests {
|
|||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let signal = event_loop.get_signal();
|
let signal = event_loop.get_signal();
|
||||||
let mut state = WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone()));
|
let mut state = WallpaperState::with_wakeup(
|
||||||
|
WallpaperConfig {
|
||||||
|
path: Some(path),
|
||||||
|
mode: WallpaperMode::Fill,
|
||||||
|
},
|
||||||
|
Some(signal.clone()),
|
||||||
|
);
|
||||||
event_loop
|
event_loop
|
||||||
.run(None, &mut state, |state| {
|
.run(None, &mut state, |state| {
|
||||||
if state.poll() {
|
if state.poll() {
|
||||||
assert!(state.pixels.is_some());
|
let decoded = state.pixels.as_ref().unwrap();
|
||||||
|
assert_eq!(decoded.dimensions(), expected.dimensions());
|
||||||
|
assert_eq!(&decoded.as_raw()[..], expected.as_raw());
|
||||||
signal.stop();
|
signal.stop();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ pub struct Appearance {
|
|||||||
pub wallpaper_path: std::path::PathBuf,
|
pub wallpaper_path: std::path::PathBuf,
|
||||||
pub wallpaper_blur: f32,
|
pub wallpaper_blur: f32,
|
||||||
pub wallpaper_revision: u64,
|
pub wallpaper_revision: u64,
|
||||||
|
wallpaper_loaded: bool,
|
||||||
|
wallpaper_pending: Option<(std::path::PathBuf, bool)>,
|
||||||
pub reduce_transparency: bool,
|
pub reduce_transparency: bool,
|
||||||
pub avatar: Option<image::Handle>,
|
pub avatar: Option<image::Handle>,
|
||||||
}
|
}
|
||||||
@@ -75,10 +77,11 @@ impl Appearance {
|
|||||||
.unwrap_or(18.)
|
.unwrap_or(18.)
|
||||||
.clamp(0., 40.) as f32;
|
.clamp(0., 40.) as f32;
|
||||||
let reduce_transparency = theme.accessibility.reduce_transparency;
|
let reduce_transparency = theme.accessibility.reduce_transparency;
|
||||||
let wallpaper =
|
let loaded = load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur });
|
||||||
load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur }).unwrap_or_else(|_| {
|
let wallpaper_loaded = loaded.is_ok();
|
||||||
image::Handle::from_bytes(include_bytes!("../../../assets/wallpapers/ferese.png").as_slice())
|
let wallpaper = loaded.unwrap_or_else(|_| {
|
||||||
});
|
image::Handle::from_bytes(include_bytes!("../../../assets/wallpapers/ferese.png").as_slice())
|
||||||
|
});
|
||||||
let boolean = |key, fallback| {
|
let boolean = |key, fallback| {
|
||||||
doc.as_ref()
|
doc.as_ref()
|
||||||
.and_then(|d| d.get(key))
|
.and_then(|d| d.get(key))
|
||||||
@@ -107,11 +110,60 @@ impl Appearance {
|
|||||||
wallpaper_path,
|
wallpaper_path,
|
||||||
wallpaper_blur: blur,
|
wallpaper_blur: blur,
|
||||||
wallpaper_revision: 0,
|
wallpaper_revision: 0,
|
||||||
|
wallpaper_loaded,
|
||||||
|
wallpaper_pending: None,
|
||||||
reduce_transparency,
|
reduce_transparency,
|
||||||
avatar: account_picture(user),
|
avatar: account_picture(user),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn request_wallpaper(
|
||||||
|
&mut self,
|
||||||
|
theme: &ferese_config::theme::ResolvedTheme,
|
||||||
|
) -> Option<(u64, std::path::PathBuf, f32)> {
|
||||||
|
let path = theme
|
||||||
|
.tokens
|
||||||
|
.background
|
||||||
|
.lock_path
|
||||||
|
.as_ref()
|
||||||
|
.or(theme.tokens.background.path.as_ref())
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_else(|| ferese_config::default_wallpaper_for(theme.appearance).into());
|
||||||
|
let reduce = theme.accessibility.reduce_transparency;
|
||||||
|
let desired = (path.clone(), reduce);
|
||||||
|
if self.wallpaper_pending.as_ref() == Some(&desired) {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
if self.wallpaper_loaded && path == self.wallpaper_path && reduce == self.reduce_transparency {
|
||||||
|
if self.wallpaper_pending.take().is_some() {
|
||||||
|
self.wallpaper_revision = self.wallpaper_revision.wrapping_add(1);
|
||||||
|
}
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
self.wallpaper_pending = Some(desired);
|
||||||
|
self.wallpaper_revision = self.wallpaper_revision.wrapping_add(1);
|
||||||
|
Some((
|
||||||
|
self.wallpaper_revision,
|
||||||
|
path,
|
||||||
|
if reduce { 0.0 } else { self.wallpaper_blur },
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn finish_wallpaper(&mut self, revision: u64, result: Result<image::Handle, String>) -> Result<(), String> {
|
||||||
|
if revision != self.wallpaper_revision {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some((path, reduce)) = self.wallpaper_pending.take() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
let wallpaper = result?;
|
||||||
|
self.wallpaper = wallpaper;
|
||||||
|
self.wallpaper_path = path;
|
||||||
|
self.reduce_transparency = reduce;
|
||||||
|
self.wallpaper_loaded = true;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
pub fn clock_format(&self) -> &'static str {
|
pub fn clock_format(&self) -> &'static str {
|
||||||
if self.twelve_hour { "%I:%M %p" } else { "%H:%M" }
|
if self.twelve_hour { "%I:%M %p" } else { "%H:%M" }
|
||||||
}
|
}
|
||||||
@@ -215,6 +267,17 @@ fn decode_avatar(path: &std::path::Path) -> Result<::image::RgbaImage, String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
|
pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
|
||||||
|
let result = prepare_wallpaper(path, blur);
|
||||||
|
// Decode and resize scratch buffers have been dropped. Return their freed
|
||||||
|
// pages even when glibc would otherwise retain them in an arena.
|
||||||
|
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||||
|
unsafe {
|
||||||
|
libc::malloc_trim(0);
|
||||||
|
}
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepare_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle, String> {
|
||||||
use ::image::ImageDecoder;
|
use ::image::ImageDecoder;
|
||||||
let mut reader = ::image::ImageReader::open(path)
|
let mut reader = ::image::ImageReader::open(path)
|
||||||
.map_err(|e| e.to_string())?
|
.map_err(|e| e.to_string())?
|
||||||
@@ -225,16 +288,18 @@ pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle
|
|||||||
reader.limits(limits);
|
reader.limits(limits);
|
||||||
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
|
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
|
||||||
let (width, height) = decoder.dimensions();
|
let (width, height) = decoder.dimensions();
|
||||||
if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 {
|
if width == 0 || height == 0 || u64::from(width) * u64::from(height) > (256 * 1024 * 1024) / 4 {
|
||||||
return Err("Lock wallpaper exceeds the decode limit".into());
|
return Err("Lock wallpaper exceeds the decode limit".into());
|
||||||
}
|
}
|
||||||
let pixels = ::image::DynamicImage::from_decoder(decoder)
|
if blur <= 0. {
|
||||||
.map_err(|e| e.to_string())?
|
// Iced decodes the path and releases its host buffer after GPU upload.
|
||||||
.into_rgba8();
|
// An RGBA handle would retain those pixels for the handle's lifetime.
|
||||||
let pixels = if blur > 0. {
|
return Ok(image::Handle::from_path(path));
|
||||||
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur)
|
}
|
||||||
} else {
|
let pixels = {
|
||||||
pixels
|
let decoded = ::image::DynamicImage::from_decoder(decoder).map_err(|e| e.to_string())?;
|
||||||
|
let thumbnail = decoded.thumbnail(640, 640).into_rgba8();
|
||||||
|
::image::imageops::blur(&thumbnail, blur)
|
||||||
};
|
};
|
||||||
Ok(image::Handle::from_rgba(
|
Ok(image::Handle::from_rgba(
|
||||||
pixels.width(),
|
pixels.width(),
|
||||||
@@ -246,6 +311,76 @@ pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn wallpaper_handles_keep_only_paths_or_blurred_thumbnails() {
|
||||||
|
let file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
::image::RgbaImage::from_pixel(1280, 720, ::image::Rgba([32, 80, 160, 255]))
|
||||||
|
.save_with_format(file.path(), ::image::ImageFormat::Png)
|
||||||
|
.unwrap();
|
||||||
|
match load_wallpaper(file.path(), 0.).unwrap() {
|
||||||
|
image::Handle::Path(_, path) => assert_eq!(path, file.path()),
|
||||||
|
_ => panic!("unblurred wallpaper must not retain RGBA pixels"),
|
||||||
|
}
|
||||||
|
match load_wallpaper(file.path(), 2.).unwrap() {
|
||||||
|
image::Handle::Rgba {
|
||||||
|
width, height, pixels, ..
|
||||||
|
} => {
|
||||||
|
assert_eq!((width, height), (640, 360));
|
||||||
|
assert_eq!(pixels.len(), 640 * 360 * 4);
|
||||||
|
}
|
||||||
|
_ => panic!("blurred wallpaper must retain only the processed thumbnail"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wallpaper_path_handles_require_a_valid_image_header() {
|
||||||
|
let file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
assert!(load_wallpaper(file.path(), 0.).is_err());
|
||||||
|
std::fs::write(file.path(), b"not an image").unwrap();
|
||||||
|
assert!(load_wallpaper(file.path(), 0.).is_err());
|
||||||
|
assert!(load_wallpaper(&file.path().with_extension("missing"), 0.).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn wallpaper_requests_retry_failures_and_ignore_superseded_results() {
|
||||||
|
let directory = tempfile::tempdir().unwrap();
|
||||||
|
let initial = directory.path().join("initial.png");
|
||||||
|
::image::RgbaImage::new(2, 2).save(&initial).unwrap();
|
||||||
|
let mut theme = ferese_config::theme::default_theme();
|
||||||
|
theme.tokens.background.path = Some(initial.clone());
|
||||||
|
theme.tokens.background.lock_path = None;
|
||||||
|
let document = Document::parse("").unwrap();
|
||||||
|
let mut appearance = Appearance::from_document("no-such-test-user", Some(document), &theme);
|
||||||
|
assert!(appearance.request_wallpaper(&theme).is_none());
|
||||||
|
let next = directory.path().join("next.png");
|
||||||
|
theme.tokens.background.path = Some(next.clone());
|
||||||
|
let (failed, _, _) = appearance.request_wallpaper(&theme).unwrap();
|
||||||
|
assert!(appearance.request_wallpaper(&theme).is_none());
|
||||||
|
assert!(
|
||||||
|
appearance
|
||||||
|
.finish_wallpaper(failed, Err("decode failed".into()))
|
||||||
|
.is_err()
|
||||||
|
);
|
||||||
|
assert_eq!(appearance.wallpaper_path, initial);
|
||||||
|
let (retry, path, _) = appearance.request_wallpaper(&theme).unwrap();
|
||||||
|
assert_eq!(path, next);
|
||||||
|
let last = directory.path().join("last.png");
|
||||||
|
theme.tokens.background.path = Some(last.clone());
|
||||||
|
let (latest, _, _) = appearance.request_wallpaper(&theme).unwrap();
|
||||||
|
appearance
|
||||||
|
.finish_wallpaper(retry, Ok(image::Handle::from_rgba(1, 1, vec![255; 4])))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(appearance.wallpaper_path, initial);
|
||||||
|
appearance
|
||||||
|
.finish_wallpaper(latest, Ok(image::Handle::from_rgba(1, 1, vec![255; 4])))
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(appearance.wallpaper_path, last);
|
||||||
|
assert!(appearance.request_wallpaper(&theme).is_none());
|
||||||
|
theme.tokens.background.lock_path = Some(initial.clone());
|
||||||
|
let (_, path, _) = appearance.request_wallpaper(&theme).unwrap();
|
||||||
|
assert_eq!(path, initial, "an explicit lock image overrides the desktop wallpaper");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn avatar_decoding_bounds_dimensions_and_encoded_size() {
|
fn avatar_decoding_bounds_dimensions_and_encoded_size() {
|
||||||
let file = tempfile::NamedTempFile::new().unwrap();
|
let file = tempfile::NamedTempFile::new().unwrap();
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ enum Message {
|
|||||||
Authenticated(bool),
|
Authenticated(bool),
|
||||||
Tick,
|
Tick,
|
||||||
RetryReady,
|
RetryReady,
|
||||||
|
FocusPassword,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Never derive Debug for password-bearing messages.
|
// Never derive Debug for password-bearing messages.
|
||||||
@@ -111,6 +112,15 @@ impl AuthState {
|
|||||||
matches!(self, Self::Ready)
|
matches!(self, Self::Ready)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn retry_ready(&mut self, now: Instant) -> bool {
|
||||||
|
if matches!(self, Self::Rejected(at) if now.saturating_duration_since(*at) >= Duration::from_secs(2)) {
|
||||||
|
*self = Self::Ready;
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn complete(&mut self, success: bool) -> bool {
|
fn complete(&mut self, success: bool) -> bool {
|
||||||
if *self != Self::Checking {
|
if *self != Self::Checking {
|
||||||
return false;
|
return false;
|
||||||
@@ -228,22 +238,7 @@ impl cosmic::Application for Locker {
|
|||||||
self.appearance.radius = palette.radius;
|
self.appearance.radius = palette.radius;
|
||||||
self.appearance.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
self.appearance.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
||||||
let theme = cosmic::command::set_theme(self.appearance.theme());
|
let theme = cosmic::command::set_theme(self.appearance.theme());
|
||||||
let target = &snapshot.theme;
|
if let Some((revision, path, blur)) = self.appearance.request_wallpaper(&snapshot.theme) {
|
||||||
let path = target
|
|
||||||
.tokens
|
|
||||||
.background
|
|
||||||
.lock_path
|
|
||||||
.as_ref()
|
|
||||||
.or(target.tokens.background.path.as_ref())
|
|
||||||
.cloned()
|
|
||||||
.unwrap_or_else(|| ferese_config::default_wallpaper().into());
|
|
||||||
let reduce = target.accessibility.reduce_transparency;
|
|
||||||
if path != self.appearance.wallpaper_path || reduce != self.appearance.reduce_transparency {
|
|
||||||
self.appearance.wallpaper_path = path.clone();
|
|
||||||
self.appearance.reduce_transparency = reduce;
|
|
||||||
self.appearance.wallpaper_revision = self.appearance.wallpaper_revision.wrapping_add(1);
|
|
||||||
let revision = self.appearance.wallpaper_revision;
|
|
||||||
let blur = if reduce { 0. } else { self.appearance.wallpaper_blur };
|
|
||||||
let wallpaper = cosmic::task::future(async move {
|
let wallpaper = cosmic::task::future(async move {
|
||||||
let result = tokio::task::spawn_blocking(move || appearance::load_wallpaper(&path, blur))
|
let result = tokio::task::spawn_blocking(move || appearance::load_wallpaper(&path, blur))
|
||||||
.await
|
.await
|
||||||
@@ -255,11 +250,8 @@ impl cosmic::Application for Locker {
|
|||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
Message::WallpaperLoaded(revision, result) => {
|
Message::WallpaperLoaded(revision, result) => {
|
||||||
if revision == self.appearance.wallpaper_revision {
|
if let Err(error) = self.appearance.finish_wallpaper(revision, result) {
|
||||||
match result {
|
eprintln!("ferese-lock: retained wallpaper: {error}");
|
||||||
Ok(image) => self.appearance.wallpaper = image,
|
|
||||||
Err(error) => eprintln!("ferese-lock: retained wallpaper: {error}"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Event(event) => return self.handle_event(*event),
|
Message::Event(event) => return self.handle_event(*event),
|
||||||
@@ -269,8 +261,13 @@ impl cosmic::Application for Locker {
|
|||||||
self.date = now.strftime("%A, %B %-d").to_string();
|
self.date = now.strftime("%A, %B %-d").to_string();
|
||||||
}
|
}
|
||||||
Message::RetryReady => {
|
Message::RetryReady => {
|
||||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) {
|
if self.state.retry_ready(Instant::now()) {
|
||||||
self.state = AuthState::Ready;
|
return focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Message::FocusPassword => {
|
||||||
|
if self.state.may_submit() {
|
||||||
|
return focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Message::Input(value) => {
|
Message::Input(value) => {
|
||||||
@@ -546,7 +543,9 @@ impl Locker {
|
|||||||
.height(Length::Fill)
|
.height(Length::Fill)
|
||||||
.content_fit(iced::ContentFit::Cover)
|
.content_fit(iced::ContentFit::Cover)
|
||||||
.into();
|
.into();
|
||||||
iced::widget::stack![background, foreground].into()
|
widget::mouse_area(iced::widget::stack![background, foreground])
|
||||||
|
.on_press(Message::FocusPassword)
|
||||||
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -567,6 +566,19 @@ fn ferese_symbol() -> widget::icon::Icon {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
#[test]
|
||||||
|
fn retry_reenables_input_only_after_the_deadline() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut state = AuthState::Rejected(now);
|
||||||
|
assert!(!state.retry_ready(now + Duration::from_millis(1999)));
|
||||||
|
assert!(!state.may_submit());
|
||||||
|
assert!(state.retry_ready(now + Duration::from_secs(2)));
|
||||||
|
assert!(state.may_submit());
|
||||||
|
assert!(!state.retry_ready(now + Duration::from_secs(3)));
|
||||||
|
state = AuthState::Checking;
|
||||||
|
assert!(!state.retry_ready(now + Duration::from_secs(3)));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn only_an_active_successful_attempt_unlocks() {
|
fn only_an_active_successful_attempt_unlocks() {
|
||||||
let mut state = AuthState::Ready;
|
let mut state = AuthState::Ready;
|
||||||
|
|||||||
Reference in new issue
Block a user