Speed up per-frame layout, hit testing, and stacking paths

Split ScrollingLayout validation so the per-frame geometry solve only
checks scalars and keeps the full tree validation behind debug_assert,
removing a HashSet build from every frame. Replace the per-region Vec
in decode_regions with a stack array via as_chunks, and skip the window
hit test in PointerMotion when Overview is not presenting, where
visual_scale_for_window returns 1.0 regardless.

Guard sync_window_stacking behind a check that the space already matches
the target order with uniform z_index, avoiding an O(n^2) raise_element
loop on every redraw. Narrow window_under_visual to the space iterator
outside Overview, keeping the collected and sorted candidate list for
the Overview branch where front-to-back order is load-bearing.

Raise rust-version to 1.98 and clear the resulting Clippy findings
across the workspace: let-chain collapses, clamp, and while-let loops.
This commit is contained in:
Samuel Onoja committed 2026-09-29 09:49:04 +01:00
1 parent 391f6b4e37
commit 67192c0c04
25 files changed
+886 -700

No files matched your search

+8 -6
View File
@@ -109,12 +109,14 @@ impl Appearance {
} else {
ThemeBuilder::dark()
};
let mut corners = cosmic::cosmic_theme::CornerRadii::default();
corners.radius_xs = [self.radius.min(4.); 4];
corners.radius_s = [self.radius.min(8.); 4];
corners.radius_m = [self.radius; 4];
corners.radius_l = [self.radius; 4];
corners.radius_xl = [self.radius; 4];
let corners = cosmic::cosmic_theme::CornerRadii {
radius_0: Default::default(),
radius_xs: [self.radius.min(4.); 4],
radius_s: [self.radius.min(8.); 4],
radius_m: [self.radius; 4],
radius_l: [self.radius; 4],
radius_xl: [self.radius; 4],
};
let mut native = builder
.corner_radii(corners)
.bg_color(color(self.panel))
+69 -26
View File
@@ -1,3 +1,5 @@
#![feature(box_patterns)]
mod appearance;
mod auth;
mod runtime;
@@ -36,19 +38,23 @@ fn main() -> iced::Result {
} else {
ferese_config::config_path()
};
if !args.is_empty() && !preview && args != ["--foreground"] {
eprintln!("Usage: ferese-lock [--preview | --foreground]");
std::process::exit(2);
}
if !preview && !auth::available() {
eprintln!("ferese-lock: install the ferese-lock PAM policy before locking");
std::process::exit(1);
}
if !preview {
if let Err(error) = runtime::check_protocol() {
eprintln!("ferese-lock: {error}");
std::process::exit(1);
}
if args.is_empty() {
if let Err(error) = runtime::daemonize() {
eprintln!("ferese-lock: {error}");
@@ -57,14 +63,17 @@ fn main() -> iced::Result {
return Ok(());
}
}
let user = auth::username().unwrap_or_else(|error| {
eprintln!("ferese-lock: {error}");
std::process::exit(1)
});
// Password-bearing UI processes must not create core dumps.
unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0);
}
let appearance = appearance::Appearance::load(&user, config.as_deref());
cosmic::app::run::<Locker>(
Settings::default()
@@ -81,18 +90,20 @@ fn main() -> iced::Result {
#[derive(Clone)]
enum Message {
Event(Event),
Event(Box<Event>),
Input(String),
Submit,
Authenticated(bool),
Tick,
}
// Never derive Debug for password-bearing messages.
impl std::fmt::Debug for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LockerMessage")
}
}
#[derive(Default, Debug, PartialEq)]
enum AuthState {
#[default]
@@ -101,10 +112,12 @@ enum AuthState {
Rejected(Instant),
Unlocking,
}
impl AuthState {
fn may_submit(&self) -> bool {
matches!(self, Self::Ready)
}
fn complete(&mut self, success: bool) -> bool {
if *self != Self::Checking {
return false;
@@ -117,6 +130,7 @@ impl AuthState {
success
}
}
struct Locker {
core: Core,
preview: bool,
@@ -132,17 +146,22 @@ struct Locker {
confirmation: runtime::Confirmation,
caps_lock: bool,
}
impl cosmic::Application for Locker {
type Executor = cosmic::executor::Default;
type Flags = (bool, String, appearance::Appearance);
type Message = Message;
const APP_ID: &'static str = "dev.ferese.Lock";
fn core(&self) -> &Core {
&self.core
}
fn core_mut(&mut self) -> &mut Core {
&mut self.core
}
fn init(mut core: Core, (preview, user, appearance): Self::Flags) -> (Self, Task<Message>) {
core.window.show_headerbar = false;
let now = jiff::Zoned::now();
@@ -174,6 +193,7 @@ impl cosmic::Application for Locker {
},
)
}
fn subscription(&self) -> Subscription<Message> {
Subscription::batch([
event::listen_with(|event, _, _| match event {
@@ -182,22 +202,26 @@ impl cosmic::Application for Locker {
))
| Event::Window(window::Event::Opened { .. })
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => {
Some(Message::Event(event))
Some(Message::Event(Box::new(event)))
}
_ => None,
}),
iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick),
])
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::Event(Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers))) => {
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
modifiers,
))) => {
self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK);
}
Message::Tick => {
let now = jiff::Zoned::now();
self.clock = now.strftime(self.appearance.clock_format()).to_string();
self.date = now.strftime("%A, %B %-d").to_string();
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2))
{
self.state = AuthState::Ready;
@@ -217,12 +241,14 @@ impl cosmic::Application for Locker {
self.status = "Preview only · your session is not locked";
return focus();
}
if !self.confirmation.confirmed()
|| !self.state.may_submit()
|| self.password.is_empty()
{
return Task::none();
}
self.state = AuthState::Checking;
self.status = "Checking password…";
let password = Zeroizing::new(std::mem::take(&mut *self.password));
@@ -243,7 +269,7 @@ impl cosmic::Application for Locker {
self.status = "Password not accepted. Try again.";
return focus();
}
Message::Event(Event::PlatformSpecific(PlatformSpecific::Wayland(
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
wayland::Event::Output(event, output),
))) if !self.preview => {
if matches!(event, wayland::OutputEvent::Removed) {
@@ -258,7 +284,7 @@ impl cosmic::Application for Locker {
}
}
}
Message::Event(Event::PlatformSpecific(PlatformSpecific::Wayland(
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
wayland::Event::SessionLock(event),
))) if !self.preview => match event {
wayland::SessionLockEvent::Locked => {
@@ -285,25 +311,32 @@ impl cosmic::Application for Locker {
}
_ => {}
},
Message::Event(Event::Window(window::Event::Opened { .. })) => return focus(),
Message::Event(box Event::Window(window::Event::Opened { .. })) => {
return focus();
}
_ => {}
}
Task::none()
}
fn on_escape(&mut self) -> Task<Message> {
self.password.zeroize();
focus()
}
fn view(&self) -> Element<'_, Message> {
self.screen()
}
fn view_window(&self, _: window::Id) -> Element<'_, Message> {
self.screen()
}
}
fn focus() -> Task<Message> {
widget::text_input::focus(widget::Id::new("password"))
}
impl Locker {
fn label<'a>(
&self,
@@ -312,9 +345,11 @@ impl Locker {
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
widget::text(label).font(self.appearance.font).size(size)
}
fn screen(&self) -> Element<'_, Message> {
iced::widget::responsive(move |size| self.screen_content(size)).into()
}
fn screen_content(&self, size: iced::Size) -> Element<'_, Message> {
let avatar_size = (size.height * 0.09).clamp(64., 112.);
let clock_size = (size.height * 0.165)
@@ -336,12 +371,14 @@ impl Locker {
.style(password_style(a.radius, a.accent, a.panel))
.font(a.font)
.id(widget::Id::new("password"));
if self.state.may_submit() {
input = input.on_input(Message::Input).on_submit(|mut value| {
value.zeroize();
Message::Submit
});
}
let submit = widget::button::custom(widget::icon::from_name("go-next-symbolic").size(20))
.class(theme::Button::Icon)
.padding(6)
@@ -401,6 +438,7 @@ impl Locker {
.min((size.width - margin * 2.).max(160.)),
);
let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center);
if a.show_clock {
clock = clock.push(
self.label(&self.clock, clock_size)
@@ -411,12 +449,14 @@ impl Locker {
.class(iced::Color::WHITE),
);
}
if a.show_date {
clock = clock.push(
self.label(&self.date, 18)
.class(iced::Color::WHITE.scale_alpha(0.72)),
);
}
let clock_height = if a.show_clock {
clock_size as f32 * 1.3
} else {
@@ -472,26 +512,6 @@ impl Locker {
iced::widget::stack![background, foreground].into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_an_active_successful_attempt_unlocks() {
let mut state = AuthState::Ready;
assert!(!state.complete(true));
state = AuthState::Checking;
assert!(!state.complete(false));
assert!(!state.may_submit());
assert!(!state.complete(true));
state = AuthState::Checking;
assert!(state.complete(true));
assert!(!state.complete(true));
}
#[test]
fn password_messages_are_redacted() {
assert!(!format!("{:?}", Message::Input("secret".into())).contains("secret"));
}
}
fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
let svg = format!(
@@ -499,6 +519,7 @@ fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
);
widget::icon(widget::icon::from_svg_bytes(svg.into_bytes()).symbolic(true)).size(size)
}
fn ferese_symbol() -> widget::icon::Icon {
widget::icon(
widget::icon::from_svg_bytes(
@@ -533,6 +554,7 @@ fn password_style(radius: f32, accent: iced::Color, surface: iced::Color) -> the
selected_fill: accent.scale_alpha(0.5),
label_color: iced::Color::WHITE,
};
theme::TextInput::Custom {
active: Box::new(move |_| appearance(false)),
hovered: Box::new(move |_| appearance(true)),
@@ -541,3 +563,24 @@ fn password_style(radius: f32, accent: iced::Color, surface: iced::Color) -> the
disabled: Box::new(move |_| appearance(false)),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn only_an_active_successful_attempt_unlocks() {
let mut state = AuthState::Ready;
assert!(!state.complete(true));
state = AuthState::Checking;
assert!(!state.complete(false));
assert!(!state.may_submit());
assert!(!state.complete(true));
state = AuthState::Checking;
assert!(state.complete(true));
assert!(!state.complete(true));
}
#[test]
fn password_messages_are_redacted() {
assert!(!format!("{:?}", Message::Input("secret".into())).contains("secret"));
}
}