diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 69e2433..5e40e77 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -184,6 +184,7 @@ struct DirectOutput { damage_tracker: OutputDamageTracker, render_metrics: RenderMetrics, frame_pending: bool, + power_off: bool, lock_frame_pending: bool, } @@ -268,6 +269,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() output.damage_tracker = OutputDamageTracker::from_output(&output.output); output.frame_pending = false; + output.power_off = false; }); }); } @@ -287,6 +289,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } } tracing::info!("direct session activated"); + state.lock_input_activity(); render_all(state); } })?; @@ -561,6 +564,45 @@ fn open_primary_device( Ok(()) } +pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { + if !state.session_lock.active || !state.session_lock.sleeping { + return; + } + let Some(backend) = state.direct_backend.as_mut() else { + return; + }; + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { + for output in device.outputs.values_mut() { + if output.power_off || output.frame_pending { + continue; + } + match output.surface.surface().clear() { + Ok(()) => output.power_off = true, + Err(error) => { + tracing::warn!(%error, output = %output.output.name(), "could not sleep locked display") + } + } + } + } +} + +pub(crate) fn wake_locked_outputs(state: &mut Ferese) { + if let Some(backend) = state.direct_backend.as_mut() { + for device in backend.devices.values_mut() { + for output in device.outputs.values_mut() { + if std::mem::take(&mut output.power_off) { + output.surface.reset_buffer_ages(); + } + } + } + } + render_all(state); +} + pub fn render_all(state: &mut Ferese) { let outputs = state .direct_backend @@ -652,6 +694,18 @@ fn lid_hides_panel(closed: bool, external_available: bool) -> bool { } fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { + if state.session_lock.active && state.session_lock.sleeping { + sleep_locked_outputs(state); + let asleep = state + .direct_backend + .as_ref() + .and_then(|backend| backend.devices.get(&node)) + .and_then(|device| device.outputs.get(&crtc)) + .is_none_or(|output| output.power_off || output.frame_pending); + if asleep { + return; + } + } let missed_deadlines = state .direct_backend .as_ref() @@ -1318,6 +1372,7 @@ fn create_direct_output( damage_tracker, render_metrics, frame_pending: false, + power_off: false, lock_frame_pending: false, }) } diff --git a/compositor/src/config.rs b/compositor/src/config.rs index 5a9ad18..73800e0 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -18,6 +18,8 @@ pub struct Config { #[serde(default)] notifications: ferese_core::notifications::NotificationConfig, #[serde(default)] + lock_screen: crate::session_lock::IdleSettings, + #[serde(default)] desktop_widgets: ferese_core::desktop::DesktopWidgets, #[serde(default)] pub(crate) autostart: Vec, @@ -292,6 +294,7 @@ impl Config { let input_settings = self.input_settings()?; let bindings = self.bindings(&input_settings)?; Ok(crate::RuntimeConfig { + lock_idle: self.lock_screen.validate()?, autostart: self.autostart.clone(), layout_mode: self.layout_mode(), gap_config: self.gap_config()?, diff --git a/compositor/src/input.rs b/compositor/src/input.rs index 14b9f89..99358a4 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -43,6 +43,14 @@ impl Ferese { pub fn process_input_event(&mut self, event: InputEvent) { let seat = self.seat.clone(); self.idle_notifier_state.notify_activity(&seat); + if !matches!( + &event, + InputEvent::DeviceAdded { .. } + | InputEvent::DeviceRemoved { .. } + | InputEvent::SwitchToggle { .. } + ) { + self.lock_input_activity(); + } if self.session_lock.active { // An already-bound IME or drag client may install a grab after the diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 463530d..42e1ab1 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -1,4 +1,8 @@ use crate::Ferese; +use smithay::reexports::calloop::{ + RegistrationToken, + timer::{TimeoutAction, Timer}, +}; use smithay::{ output::Output, reexports::{ @@ -16,7 +20,72 @@ use smithay::{ LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker, }, }; -use std::collections::{HashMap, HashSet}; +use std::{ + collections::{HashMap, HashSet}, + time::{Duration, Instant}, +}; + +#[derive(Clone, Copy, Debug, serde::Deserialize)] +#[serde(default)] +pub(crate) struct IdleSettings { + pub dim_after_seconds: u64, + pub sleep_after_seconds: u64, +} + +impl Default for IdleSettings { + fn default() -> Self { + Self { + dim_after_seconds: 30, + sleep_after_seconds: 120, + } + } +} + +impl IdleSettings { + pub(crate) fn validate(self) -> Result { + if self.dim_after_seconds > 86400 || self.sleep_after_seconds > 86400 { + return Err(crate::config::ConfigError::InvalidInputValue { + field: "lock_screen.idle_delay", + value: "must be between 0 and 86400 seconds".into(), + }); + } + Ok(self) + } + + fn next_poll(self, elapsed: Duration, animate: bool) -> Duration { + let dim_at = Duration::from_secs(self.dim_after_seconds); + if animate + && self.dim_after_seconds != 0 + && elapsed >= dim_at + && elapsed < dim_at + Duration::from_millis(500) + { + return Duration::from_millis(16); + } + [self.dim_after_seconds, self.sleep_after_seconds] + .into_iter() + .filter(|delay| *delay != 0) + .filter_map(|delay| Duration::from_secs(delay).checked_sub(elapsed)) + .filter(|delay| !delay.is_zero()) + .min() + .unwrap_or(Duration::from_secs(1)) + .min(Duration::from_secs(1)) + } + + fn appearance(self, elapsed: Duration) -> (f32, bool) { + let sleeping = self.sleep_after_seconds != 0 + && elapsed >= Duration::from_secs(self.sleep_after_seconds); + let dim = if self.dim_after_seconds == 0 { + 0.0 + } else { + elapsed + .saturating_sub(Duration::from_secs(self.dim_after_seconds)) + .as_secs_f32() + .min(0.5) + * 1.3 + }; + (if sleeping { 1.0 } else { dim }, sleeping) + } +} #[derive(Default)] pub(crate) struct Lock { @@ -27,9 +96,79 @@ pub(crate) struct Lock { pub backgrounds: HashMap, presented: HashSet, confirmed: bool, + idle_since: Option, + pub(crate) idle_opacity: f32, + pub(crate) sleeping: bool, + pub(crate) idle_overlays: + HashMap, + idle_timer: Option, +} + +impl Lock { + fn ready_for_idle<'a>(&self, outputs: impl Iterator) -> bool { + self.active + && outputs + .into_iter() + .all(|output| self.presented.contains(output)) + } + + pub(crate) fn output_added(&mut self, output: &Output) { + self.presented.remove(output); + } + + pub(crate) fn output_removed(&mut self, output: &Output) { + self.presented.remove(output); + self.idle_overlays.remove(output); + } + + fn activity(&mut self, now: Instant) -> bool { + if !self.active { + return false; + } + let changed = self.idle_opacity != 0.0 || self.sleeping; + self.idle_since = Some(now); + self.idle_opacity = 0.0; + self.sleeping = false; + changed + } } impl Ferese { + pub(crate) fn lock_input_activity(&mut self) { + if self.session_lock.activity(Instant::now()) { + crate::backends::direct::wake_locked_outputs(self); + self.cursor_redraw_pending = true; + } + } + + fn update_lock_idle(&mut self, now: Instant) { + if !self.session_lock.ready_for_idle(self.space.outputs()) { + return; + } + let since = self.session_lock.idle_since.get_or_insert(now); + let (mut opacity, sleeping) = self + .lock_idle + .appearance(now.saturating_duration_since(*since)); + if !self.animations_enabled() && opacity > 0.0 && !sleeping { + opacity = 0.65; + } + let changed = + opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping; + let was_sleeping = self.session_lock.sleeping; + self.session_lock.idle_opacity = opacity; + self.session_lock.sleeping = sleeping; + if changed { + if was_sleeping && !sleeping { + crate::backends::direct::wake_locked_outputs(self); + } else if sleeping { + crate::backends::direct::sleep_locked_outputs(self); + } else { + crate::backends::direct::render_all(self); + } + self.cursor_redraw_pending = true; + } + } + pub(crate) fn lock_surface_under( &self, position: Point, @@ -123,6 +262,28 @@ impl SessionLockHandler for Ferese { } self.cancel_logout_confirmation(); self.session_lock.active = true; + self.session_lock.idle_since = Some(Instant::now()); + match self.loop_handle.insert_source( + Timer::from_duration(Duration::from_secs(1)), + |_, _, state| { + if !state.session_lock.active { + return TimeoutAction::Drop; + } + state.update_lock_idle(Instant::now()); + let elapsed = state + .session_lock + .idle_since + .map_or(Duration::ZERO, |since| since.elapsed()); + TimeoutAction::ToDuration( + state + .lock_idle + .next_poll(elapsed, state.animations_enabled()), + ) + }, + ) { + Ok(token) => self.session_lock.idle_timer = Some(token), + Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"), + } self.input_capture.disable_all(); self.portal_session.set_locked(true); self.session_lock.owner = Some(confirmation.ext_session_lock().clone()); @@ -166,7 +327,14 @@ impl SessionLockHandler for Ferese { } fn unlock(&mut self) { + if let Some(timer) = self.session_lock.idle_timer.take() { + self.loop_handle.remove(timer); + } + let sleeping = self.session_lock.sleeping; self.session_lock = Lock::default(); + if sleeping { + crate::backends::direct::wake_locked_outputs(self); + } self.portal_session.set_locked(false); self.restore_keyboard_focus(); crate::backends::direct::render_all(self); @@ -223,3 +391,121 @@ impl Dispatch for Ferese { smithay::reexports::wayland_server::delegate_global_dispatch!(Ferese: [ExtSessionLockManagerV1: smithay::wayland::session_lock::SessionLockManagerGlobalData] => SessionLockManagerState); smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockManagerV1: ()] => SessionLockManagerState); smithay::reexports::wayland_server::delegate_dispatch!(Ferese: [ExtSessionLockSurfaceV1: smithay::wayland::session_lock::ExtLockSurfaceUserData] => SessionLockManagerState); + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn idle_policy_fades_then_sleeps_and_activity_resets_it() { + let policy = IdleSettings::default(); + assert_eq!(policy.appearance(Duration::from_secs(29)), (0.0, false)); + let halfway = policy.appearance(Duration::from_millis(30_250)); + assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1); + assert_eq!(policy.appearance(Duration::from_secs(31)), (0.65, false)); + assert_eq!(policy.appearance(Duration::from_secs(120)), (1.0, true)); + assert_eq!(policy.appearance(Duration::ZERO), (0.0, false)); + assert_eq!( + policy.next_poll(Duration::from_millis(29_998), true), + Duration::from_millis(2) + ); + assert_eq!( + policy.next_poll(Duration::from_secs(30), true), + Duration::from_millis(16) + ); + assert_eq!( + policy.next_poll(Duration::from_secs(30), false), + Duration::from_secs(1) + ); + } + + #[test] + fn protected_fallback_frames_allow_idle_after_early_locker_crash() { + let output = Output::new( + "test".into(), + smithay::output::PhysicalProperties { + size: (0, 0).into(), + subpixel: smithay::output::Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + let mut lock = Lock { + active: true, + ..Lock::default() + }; + assert!(!lock.ready_for_idle(std::iter::once(&output))); + lock.presented.insert(output.clone()); + assert!(lock.ready_for_idle(std::iter::once(&output))); + assert!(!lock.confirmed); + lock.output_added(&output); + assert!(!lock.ready_for_idle(std::iter::once(&output))); + assert!(!lock.confirmed); + } + + #[test] + fn input_wakes_a_sleeping_lock_without_unlocking_or_losing_confirmation() { + let mut lock = Lock { + active: true, + confirmed: true, + sleeping: true, + idle_opacity: 1.0, + ..Lock::default() + }; + let now = Instant::now(); + assert!(lock.activity(now)); + assert!(lock.active && lock.confirmed); + assert!(!lock.sleeping); + assert_eq!(lock.idle_opacity, 0.0); + assert_eq!(lock.idle_since, Some(now)); + assert!(!lock.activity(now)); + let mut unlocked = Lock::default(); + assert!(!unlocked.activity(now)); + assert!(unlocked.idle_since.is_none()); + } + + #[test] + fn idle_disabled_options_are_independent() { + assert_eq!( + IdleSettings { + dim_after_seconds: 0, + sleep_after_seconds: 0 + } + .appearance(Duration::from_secs(9999)), + (0.0, false) + ); + assert_eq!( + IdleSettings { + dim_after_seconds: 0, + sleep_after_seconds: 10 + } + .appearance(Duration::from_secs(10)), + (1.0, true) + ); + assert_eq!( + IdleSettings { + dim_after_seconds: 1, + sleep_after_seconds: 0 + } + .appearance(Duration::from_secs(9999)), + (0.65, false) + ); + assert!( + IdleSettings { + dim_after_seconds: 86401, + sleep_after_seconds: 0 + } + .validate() + .is_err() + ); + assert!( + crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err() + ); + assert!( + crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }") + .unwrap() + .runtime_config() + .is_err() + ); + } +} diff --git a/compositor/src/state.rs b/compositor/src/state.rs index 22f663d..60697bc 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -226,6 +226,7 @@ impl ClosingAnimation { pub struct Ferese { pub(crate) session_lock_state: smithay::wayland::session_lock::SessionLockManagerState, pub(crate) session_lock: crate::session_lock::Lock, + pub(crate) lock_idle: crate::session_lock::IdleSettings, pub(crate) config_source: Option, pub start_time: Instant, pub socket_name: OsString, @@ -353,6 +354,7 @@ pub struct Ferese { } pub struct RuntimeConfig { + pub(crate) lock_idle: crate::session_lock::IdleSettings, pub(crate) autostart: Vec, pub(crate) overview_font_family: String, pub(crate) wallpaper: crate::wallpaper::WallpaperConfig, @@ -447,6 +449,7 @@ impl Ferese { let mut state = Self { session_lock_state, session_lock: crate::session_lock::Lock::default(), + lock_idle: config.lock_idle, config_source: None, start_time, socket_name, @@ -609,6 +612,7 @@ impl Ferese { ); } + self.lock_idle = config.lock_idle; self.advance_animations(Instant::now()); let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad; @@ -677,6 +681,10 @@ impl Ferese { } pub fn register_output(&mut self, output: &Output, identity: String) { + if self.session_lock.active { + self.session_lock.output_added(output); + self.lock_input_activity(); + } let Some(geometry) = self.space.output_geometry(output) else { tracing::error!(output = %output.name(), "cannot register an unmapped output"); return; @@ -738,6 +746,7 @@ impl Ferese { self.space.unmap_output(output); self.session_lock.surfaces.remove(output); self.session_lock.backgrounds.remove(output); + self.session_lock.output_removed(output); match self.output_workspaces.disconnect(output_id) { Ok(Some(target)) => { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index cc93bbb..78958ed 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1148,6 +1148,30 @@ fn output_elements( }); background.resize(geometry.size); let mut elements = Vec::new(); + let opacity = state.session_lock.idle_opacity; + if opacity > 0.0 { + let overlay = state + .session_lock + .idle_overlays + .entry(output.clone()) + .or_insert_with(|| { + smithay::backend::renderer::element::solid::SolidColorBuffer::new( + geometry.size, + [0.0, 0.0, 0.0, 1.0], + ) + }); + overlay.resize(geometry.size); + elements.push( + SolidColorRenderElement::from_buffer( + overlay, + (0, 0), + scale, + opacity, + RenderElementKind::Unspecified, + ) + .into(), + ); + } if let Some(surface) = state .session_lock .surfaces diff --git a/docs/backlog.md b/docs/backlog.md deleted file mode 100644 index 796eb90..0000000 --- a/docs/backlog.md +++ /dev/null @@ -1,3 +0,0 @@ -# Backlog - -- [ ] Review the lock screen against swaylock for practical feature parity: dim and sleep after inactivity, and reduce the oversized password input. Keep the implementation simple. diff --git a/docs/configuration.md b/docs/configuration.md index f08a79e..faa3c2c 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -413,6 +413,11 @@ autostart { } ``` +The locked display dims after 30 seconds and sleeps after 120 seconds of +inactivity. Settings → Lock screen changes `lock-screen.dim-after-seconds` and +`lock-screen.sleep-after-seconds`; zero disables each action. Input wakes the +display without unlocking. Display sleep does not suspend the computer. + `ferese-lock` is the native PAM-authenticated locker. It follows your wallpaper, colors, font and shell corner radius. Its command returns successfully only after the compositor confirms the lock; the UI keeps running until authentication diff --git a/docs/locking.md b/docs/locking.md index 2fbe6f0..1f7e2b4 100644 --- a/docs/locking.md +++ b/docs/locking.md @@ -15,7 +15,7 @@ This is an ordinary, closable window. Submitting the field does not call PAM, lock, or unlock anything. Do not type your actual password into a visual preview. Settings → Lock screen provides clock visibility, date visibility, time format, -wallpaper dimming and softness, and a safe preview button. Your account picture is loaded from +wallpaper dimming and softness, inactivity delays, and a safe preview button. Your account picture is loaded from AccountsService or `~/.face`, with a user silhouette as a fallback. The locker reads Ferese's wallpaper, theme colors, typography, and shell radius @@ -64,3 +64,46 @@ never reports success merely because the window appeared. Validation references: [session-lock protocol](https://wayland.app/protocols/ext-session-lock-v1), [PAM authentication](https://man7.org/linux/man-pages/man3/pam_authenticate.3.html), and [PAM account checks](https://man7.org/linux/man-pages/man3/pam_acct_mgmt.3.html). + +## Inactivity and display sleep + +A locked display dims after 30 seconds without input and sleeps after 120 seconds. +Dimming fades over half a second. Any keyboard, pointer or touch activity restores +brightness and wakes the display while keeping the session locked. The compositor +owns this policy, so it remains effective if the locker crashes. Sleep starts only +after the compositor has confirmed that every display is protected. + +Settings → Lock screen adjusts both delays. Zero disables that action: + +```kdl +lock-screen { + dim-after-seconds 30 + sleep-after-seconds 120 +} +``` + +These delays are independent of automatic locking. Existing idle inhibitors from +apps cannot keep an already locked display awake. In a direct session, display +sleep uses DRM DPMS without removing outputs or rearranging workspaces; nested +previews show black instead of powering off the host display. This puts displays +to sleep, not the computer. Suspend remains a separate system power action. + +## Practical swaylock comparison + +| Behavior | Ferese Lock | +| --- | --- | +| Secure Wayland locking and protection after a locker crash | Session-lock protocol, compositor-owned fallback | +| All displays, including hotplug | Separate lock surface for each display | +| Readiness before an idle manager permits suspend | Default command waits for compositor confirmation | +| Password verification | PAM authentication and account checks on a worker | +| Password clearing and Caps Lock feedback | Escape clears; Caps Lock indicator | +| Authentication failure feedback and retry delay | Error message and bounded retry delay | +| Wallpaper, font and color customization | Shared Ferese theme and Settings | +| Inactivity and display power | Compositor dimming and DPMS; input restores brightness | + +[Swaylock](https://github.com/swaywm/swaylock/blob/master/swaylock.1.scd) provides +additional appearance flags, layout indicators and readiness-FD CLI options. +Ferese uses its shared configuration and existing startup acknowledgement instead. +Like the [swayidle example](https://github.com/swaywm/swayidle/blob/master/swayidle.1.scd), +locking and display sleep are separate from system suspend. Password changes and +interactive multi-factor prompts remain outside this password-only interface. diff --git a/scripts/tests/test_lock_isolated.py b/scripts/tests/test_lock_isolated.py index 6381598..0888251 100644 --- a/scripts/tests/test_lock_isolated.py +++ b/scripts/tests/test_lock_isolated.py @@ -1,6 +1,6 @@ """Opt-in lock protocol smoke test. Opens a temporary nested compositor. Run: FERESE_TEST_LOCK=1 python3 scripts/tests/test_lock_isolated.py -Requires release binaries and bwrap. Host PAM is never modified. +Requires built binaries and bwrap. FERESE_TEST_BIN_DIR selects debug/release. Host PAM is never modified. """ import os import unittest @@ -21,9 +21,10 @@ class NativeLockTest(unittest.TestCase): host_display=os.environ.get('WAYLAND_DISPLAY','wayland-1') if not host_display.startswith('/'):host_display=str(pathlib.Path(os.environ['XDG_RUNTIME_DIR'])/host_display) env=dict(os.environ,XDG_RUNTIME_DIR=str(runtime),XDG_CONFIG_HOME=str(root/'config'),WAYLAND_DISPLAY=host_display,FERESE_ENABLE_SCREENCOPY='1') + binary_dir = Path(os.environ.get('FERESE_TEST_BIN_DIR', 'target/release')) log=(root/'compositor.log').open('w') - compositor=subprocess.Popen(['target/release/ferese','--backend','nested'],env=env,stdout=log,stderr=subprocess.STDOUT) - ctl=pathlib.Path('target/release/feresectl') + compositor=subprocess.Popen([str(binary_dir / 'ferese'),'--backend','nested'],env=env,stdout=log,stderr=subprocess.STDOUT) + ctl=binary_dir / 'feresectl' assert ctl.is_file(), f'missing {ctl}; build it with cargo build --release -p feresectl' locker=None locker_log=(root/'locker.log').open('w') @@ -42,7 +43,7 @@ class NativeLockTest(unittest.TestCase): assert result.returncode == 0, f'Capture failed while unlocked: {result.stderr.decode()}' assert (root/'unlocked.png').stat().st_size > 0, 'Capture produced an empty file' print('Native capture succeeded while unlocked',flush=True) - locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--','target/release/ferese-lock'],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True) + locker=subprocess.Popen(['bwrap','--bind','/','/','--dev-bind','/dev','/dev','--ro-bind',str(pam),'/etc/pam.d','--unshare-user','--',str(binary_dir / 'ferese-lock')],env=childenv,stdout=subprocess.DEVNULL,stderr=locker_log,start_new_session=True) assert locker.wait(timeout=20) == 0, (root/'locker.log').read_text() print('Native locker received compositor confirmation',flush=True) result=capture(root/'locked.png') diff --git a/shell/ferese-lock/src/main.rs b/shell/ferese-lock/src/main.rs index 2ec00c4..3fba417 100644 --- a/shell/ferese-lock/src/main.rs +++ b/shell/ferese-lock/src/main.rs @@ -351,7 +351,7 @@ impl Locker { } fn screen_content(&self, size: iced::Size) -> Element<'_, Message> { - let avatar_size = (size.height * 0.09).clamp(64., 112.); + let avatar_size = (size.height * 0.08).clamp(56., 88.); let clock_size = (size.height * 0.165) .min( size.width @@ -367,7 +367,7 @@ impl Locker { let a = &self.appearance; let mut input = widget::text_input("Password", self.password.as_str()) .password() - .padding([8, 18]) + .padding([6, 12]) .style(ferese_theme::controls::lock_input( a.radius, a.accent, a.panel, )) @@ -435,8 +435,8 @@ impl Locker { .spacing(12) .align_x(iced::Alignment::Center) .width( - (size.height * 0.37) - .clamp(260., 420.) + (size.height * 0.31) + .clamp(240., 320.) .min((size.width - margin * 2.).max(160.)), ); let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center); diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index b7605e7..01c2774 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -550,6 +550,28 @@ pub fn note_fields(index: usize) -> Vec { pub fn fields(page: Page) -> Vec { match page { Page::LockScreen => vec![ + range( + "lock_screen.dim_after_seconds", + "Dim after", + "Dim the locked display after inactivity. Zero disables dimming.", + 30.0, + 0.0, + 600.0, + 5.0, + "s", + true, + ), + range( + "lock_screen.sleep_after_seconds", + "Display sleep after", + "Turn off locked displays after inactivity. Input wakes them; zero disables display sleep.", + 120.0, + 0.0, + 1800.0, + 15.0, + "s", + true, + ), toggle( "lock_screen.show_clock", "Show clock",