Fix lock-screen input and wallpaper handling
This commit is contained in:
1 parent
bbc5b27c25
commit
cfdd2a148a
7 files changed
+348
-55
No files matched your search
+28
-12
@@ -120,18 +120,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
if self.session_lock.active() {
|
||||
// An already-bound IME or drag client may install a grab after the
|
||||
// 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();
|
||||
self.prepare_lock_input();
|
||||
}
|
||||
|
||||
if self.input_capture.active()
|
||||
@@ -514,6 +503,33 @@ impl Ferese {
|
||||
}
|
||||
let pointer = self.seat.get_pointer().expect("seat has a pointer");
|
||||
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
|
||||
// effects-capable shell popups, release input now but defer
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
//! logical pixels. Content, clips and decorations share those exact edges.
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use ferese_animation::AnimatedRect;
|
||||
|
||||
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::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) {
|
||||
let surface = self
|
||||
.focused_output()
|
||||
@@ -289,10 +341,10 @@ impl Ferese {
|
||||
.map(|(_, surface)| surface)
|
||||
})
|
||||
.map(|surface| surface.wl_surface().clone());
|
||||
self.seat
|
||||
.get_keyboard()
|
||||
.unwrap()
|
||||
.set_focus(self, surface, SERIAL_COUNTER.next_serial());
|
||||
let keyboard = self.seat.get_keyboard().unwrap();
|
||||
if keyboard.current_focus() != surface {
|
||||
keyboard.set_focus(self, surface, SERIAL_COUNTER.next_serial());
|
||||
}
|
||||
}
|
||||
|
||||
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(_)));
|
||||
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
|
||||
//! full-screen UI surfaces per monitor. Resize changes sampling only.
|
||||
//! One wallpaper texture per GPU context. Decoded pixels are staging data,
|
||||
//! released after upload and decoded again when a new context needs them.
|
||||
//! Resize changes sampling only.
|
||||
use std::cell::Cell;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
@@ -279,7 +280,7 @@ impl WallpaperState {
|
||||
width = pixels.width(),
|
||||
height = pixels.height(),
|
||||
bytes = pixels.as_raw().len(),
|
||||
"decoded compositor wallpaper once"
|
||||
"decoded compositor wallpaper for texture upload"
|
||||
);
|
||||
|
||||
self.pixels = Some(pixels);
|
||||
@@ -540,6 +541,10 @@ mod tests {
|
||||
fn decoder_wakes_an_idle_event_loop() {
|
||||
use smithay::reexports::calloop::EventLoop;
|
||||
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();
|
||||
event_loop
|
||||
.handle()
|
||||
@@ -548,11 +553,19 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
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
|
||||
.run(None, &mut state, |state| {
|
||||
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();
|
||||
}
|
||||
})
|
||||
|
||||
@@ -20,6 +20,8 @@ pub struct Appearance {
|
||||
pub wallpaper_path: std::path::PathBuf,
|
||||
pub wallpaper_blur: f32,
|
||||
pub wallpaper_revision: u64,
|
||||
wallpaper_loaded: bool,
|
||||
wallpaper_pending: Option<(std::path::PathBuf, bool)>,
|
||||
pub reduce_transparency: bool,
|
||||
pub avatar: Option<image::Handle>,
|
||||
}
|
||||
@@ -75,8 +77,9 @@ impl Appearance {
|
||||
.unwrap_or(18.)
|
||||
.clamp(0., 40.) as f32;
|
||||
let reduce_transparency = theme.accessibility.reduce_transparency;
|
||||
let wallpaper =
|
||||
load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur }).unwrap_or_else(|_| {
|
||||
let loaded = load_wallpaper(&wallpaper_path, if reduce_transparency { 0. } else { blur });
|
||||
let wallpaper_loaded = loaded.is_ok();
|
||||
let wallpaper = loaded.unwrap_or_else(|_| {
|
||||
image::Handle::from_bytes(include_bytes!("../../../assets/wallpapers/ferese.png").as_slice())
|
||||
});
|
||||
let boolean = |key, fallback| {
|
||||
@@ -107,11 +110,60 @@ impl Appearance {
|
||||
wallpaper_path,
|
||||
wallpaper_blur: blur,
|
||||
wallpaper_revision: 0,
|
||||
wallpaper_loaded,
|
||||
wallpaper_pending: None,
|
||||
reduce_transparency,
|
||||
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 {
|
||||
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> {
|
||||
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;
|
||||
let mut reader = ::image::ImageReader::open(path)
|
||||
.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);
|
||||
let decoder = reader.into_decoder().map_err(|e| e.to_string())?;
|
||||
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());
|
||||
}
|
||||
let pixels = ::image::DynamicImage::from_decoder(decoder)
|
||||
.map_err(|e| e.to_string())?
|
||||
.into_rgba8();
|
||||
let pixels = if blur > 0. {
|
||||
::image::imageops::blur(&::image::imageops::thumbnail(&pixels, 640, 640), blur)
|
||||
} else {
|
||||
pixels
|
||||
if blur <= 0. {
|
||||
// Iced decodes the path and releases its host buffer after GPU upload.
|
||||
// An RGBA handle would retain those pixels for the handle's lifetime.
|
||||
return Ok(image::Handle::from_path(path));
|
||||
}
|
||||
let 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(
|
||||
pixels.width(),
|
||||
@@ -246,6 +311,76 @@ pub fn load_wallpaper(path: &std::path::Path, blur: f32) -> Result<image::Handle
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
fn avatar_decoding_bounds_dimensions_and_encoded_size() {
|
||||
let file = tempfile::NamedTempFile::new().unwrap();
|
||||
|
||||
@@ -88,6 +88,7 @@ enum Message {
|
||||
Authenticated(bool),
|
||||
Tick,
|
||||
RetryReady,
|
||||
FocusPassword,
|
||||
}
|
||||
|
||||
// Never derive Debug for password-bearing messages.
|
||||
@@ -111,6 +112,15 @@ impl AuthState {
|
||||
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 {
|
||||
if *self != Self::Checking {
|
||||
return false;
|
||||
@@ -228,22 +238,7 @@ impl cosmic::Application for Locker {
|
||||
self.appearance.radius = palette.radius;
|
||||
self.appearance.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family));
|
||||
let theme = cosmic::command::set_theme(self.appearance.theme());
|
||||
let target = &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 };
|
||||
if let Some((revision, path, blur)) = self.appearance.request_wallpaper(&snapshot.theme) {
|
||||
let wallpaper = cosmic::task::future(async move {
|
||||
let result = tokio::task::spawn_blocking(move || appearance::load_wallpaper(&path, blur))
|
||||
.await
|
||||
@@ -255,11 +250,8 @@ impl cosmic::Application for Locker {
|
||||
return theme;
|
||||
}
|
||||
Message::WallpaperLoaded(revision, result) => {
|
||||
if revision == self.appearance.wallpaper_revision {
|
||||
match result {
|
||||
Ok(image) => self.appearance.wallpaper = image,
|
||||
Err(error) => eprintln!("ferese-lock: retained wallpaper: {error}"),
|
||||
}
|
||||
if let Err(error) = self.appearance.finish_wallpaper(revision, result) {
|
||||
eprintln!("ferese-lock: retained wallpaper: {error}");
|
||||
}
|
||||
}
|
||||
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();
|
||||
}
|
||||
Message::RetryReady => {
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) {
|
||||
self.state = AuthState::Ready;
|
||||
if self.state.retry_ready(Instant::now()) {
|
||||
return focus();
|
||||
}
|
||||
}
|
||||
Message::FocusPassword => {
|
||||
if self.state.may_submit() {
|
||||
return focus();
|
||||
}
|
||||
}
|
||||
Message::Input(value) => {
|
||||
@@ -546,7 +543,9 @@ impl Locker {
|
||||
.height(Length::Fill)
|
||||
.content_fit(iced::ContentFit::Cover)
|
||||
.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)]
|
||||
mod tests {
|
||||
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]
|
||||
fn only_an_active_successful_attempt_unlocks() {
|
||||
let mut state = AuthState::Ready;
|
||||
|
||||
Reference in new issue
Block a user