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"));
}
}
+8 -7
View File
@@ -82,13 +82,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_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],
radius_0: Default::default(),
};
let mut native = builder
.corner_radii(corners)
+18 -4
View File
@@ -53,6 +53,7 @@ impl Api {
let cancel = *agent
.get(b"polkit_agent_session_cancel\0")
.map_err(|e| e.to_string())?;
Ok(Self {
_agent: agent,
_polkit: polkit,
@@ -199,7 +200,10 @@ fn run_session(
connect(
session,
c"request",
Some(std::mem::transmute(
Some(std::mem::transmute::<
unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
unsafe extern "C" fn(),
>(
request as unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
)),
signal_data,
@@ -207,7 +211,10 @@ fn run_session(
connect(
session,
c"show-info",
Some(std::mem::transmute(
Some(std::mem::transmute::<
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
unsafe extern "C" fn(),
>(
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)),
signal_data,
@@ -215,7 +222,10 @@ fn run_session(
connect(
session,
c"show-error",
Some(std::mem::transmute(
Some(std::mem::transmute::<
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
unsafe extern "C" fn(),
>(
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)),
signal_data,
@@ -223,11 +233,15 @@ fn run_session(
connect(
session,
c"completed",
Some(std::mem::transmute(
Some(std::mem::transmute::<
unsafe extern "C" fn(Object, c_int, *mut c_void),
unsafe extern "C" fn(),
>(
completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
)),
signal_data,
);
(api.initiate)(session);
}
+10 -12
View File
@@ -274,10 +274,8 @@ impl cosmic::Application for App {
|result| cosmic::Action::App(Message::LockPreviewStarted(result)),
);
}
Message::LockPreviewStarted(result) => {
if let Err(error) = result {
self.error = Some(error);
}
Message::LockPreviewStarted(Err(error)) => {
self.error = Some(error);
}
Message::Page(page) => {
self.page = page;
@@ -1286,16 +1284,16 @@ impl App {
// When a gradient exists, changing Accent also changes its
// leading stop; otherwise the visible focus border would stay
// on the old palette despite the control saying it changed.
if let Edit::Set(path, value) = &edit {
if path == "theme.colors.accent"
&& self.draft.item("theme.focus_ring.gradient").is_some()
{
let gradient = set("theme.focus_ring.gradient.from", value.clone());
if self.draft.edit(&gradient).is_ok() {
self.pending.push(gradient);
}
if let Edit::Set(path, value) = &edit
&& path == "theme.colors.accent"
&& self.draft.item("theme.focus_ring.gradient").is_some()
{
let gradient = set("theme.focus_ring.gradient.from", value.clone());
if self.draft.edit(&gradient).is_ok() {
self.pending.push(gradient);
}
}
self.pending.push(edit);
self.error = None;
self.flush()
+9 -3
View File
@@ -451,7 +451,12 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65;
let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65;
let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" {
snapshot.number("theme.material.opacity", ferese_config::DEFAULT_MATERIAL_OPACITY).clamp(0., 1.)
snapshot
.number(
"theme.material.opacity",
ferese_config::DEFAULT_MATERIAL_OPACITY,
)
.clamp(0., 1.)
} else {
1.
};
@@ -556,11 +561,12 @@ mod tests {
fn default_uses_logo_blue_and_native_controls_follow_lightness() {
let logo = include_str!("../../../packaging/icons/ferese.svg").to_ascii_uppercase();
assert!(logo.contains(PRESETS[0].accent));
for index in 0..PRESETS.len() {
for (index, item) in PRESETS.iter().enumerate() {
let snapshot = configured_preset(index);
assert_eq!(
native_theme(Some(&snapshot)).cosmic().is_dark,
PRESETS[index].name != "Ayu Light"
item.name != "Ayu Light"
);
}
}
+57 -52
View File
@@ -8,14 +8,6 @@ mod recording;
mod status;
mod status_ui;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
// Older libcosmic backends expose native surfaces in frame events. Keep
// that compatibility path gated; current backends use Window::Opened and
// window::run to retrieve handles without updates at the display refresh rate.
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
use cosmic::Element;
use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment;
@@ -37,6 +29,8 @@ use ferese_protocols::effects::v1::client::{
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
};
use jiff::Zoned;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
globals::{GlobalListContents, registry_queue_init},
@@ -48,6 +42,10 @@ use crate::control::{ShellControl, ShellSnapshot};
const APP_ID: &str = "dev.ferese.Shell";
// Older libcosmic backends expose native surfaces in frame events. Keep
// that compatibility path gated; current backends use Window::Opened and
// window::run to retrieve handles without updates at the display refresh rate.
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
static SHELL_FONT: std::sync::OnceLock<std::sync::RwLock<cosmic::font::Font>> =
std::sync::OnceLock::new();
@@ -117,10 +115,11 @@ fn stacked_clock_digits(label: &str) -> Option<[String; 2]> {
let mut parts = label.split_whitespace();
let time = parts.next()?;
if let Some(period) = parts.next() {
if !period.eq_ignore_ascii_case("am") && !period.eq_ignore_ascii_case("pm") {
return None;
}
if let Some(period) = parts.next()
&& !period.eq_ignore_ascii_case("am")
&& !period.eq_ignore_ascii_case("pm")
{
return None;
}
if parts.next().is_some() {
@@ -650,8 +649,7 @@ impl cosmic::Application for FereseShell {
let open = self.open_menu(kind, anchor);
Task::batch([destroy, open])
} else {
let open = self.open_menu(kind, anchor);
open
self.open_menu(kind, anchor)
}
}
Message::ConfirmPower(action) => {
@@ -680,10 +678,11 @@ impl cosmic::Application for FereseShell {
.status_service
.send(status::Action::PowerProfile(profile))
.err();
if self.status_error.is_none() {
if let Some(profiles) = &mut self.status.power_profiles {
profiles.active = profile.to_owned();
}
if self.status_error.is_none()
&& let Some(profiles) = &mut self.status.power_profiles
{
profiles.active = profile.to_owned();
}
return Task::none();
}
@@ -706,22 +705,27 @@ impl cosmic::Application for FereseShell {
_ => {}
}
}
self.status_error = self.status_service.send(action.clone()).err();
if self.status_error.is_none() {
self.optimistic_status(&action);
}
if matches!(
action,
status::Action::Notifications | status::Action::Settings
) {
return self.close_menu();
}
Task::none()
}
Message::Event(event, id) => self.handle_event(event, id),
Message::Tick => {
self.recorder.poll();
self.clock = current_time();
if self.config.desktop_widgets.clock.enabled {
self.desktop_clock = self
.config
@@ -730,6 +734,7 @@ impl cosmic::Application for FereseShell {
.labels(&Zoned::now())
.unwrap_or_default();
}
let mut reload_task = Task::none();
if let Some(control) = &self.control {
let poll = control.poll();
@@ -737,15 +742,19 @@ impl cosmic::Application for FereseShell {
if poll.disconnected {
return cosmic::iced::exit();
}
if let Some(active) = poll.overview_active {
self.overview_active = active;
}
if let Some(source) = poll.config {
reload_task = self.reload_config(source);
}
if let Some(snapshot) = poll.snapshot {
self.snapshot = snapshot;
let mut tasks = vec![reload_task];
for entry in &mut self.outputs {
let output = self
.snapshot
@@ -754,16 +763,16 @@ impl cosmic::Application for FereseShell {
.find(|output| Some(output.name.as_str()) == entry.name.as_deref())
.map(|output| output.id);
let hidden = output_bar_hidden(&self.snapshot, output);
if entry.hidden == hidden {
continue;
}
entry.hidden = hidden;
if let Some(effects) = &entry.effects {
if let Err(error) = effects.set_visible(!hidden) {
eprintln!(
"ferese-shell: could not update panel material: {error}"
);
}
if let Some(effects) = &entry.effects
&& let Err(error) = effects.set_visible(!hidden)
{
eprintln!("ferese-shell: could not update panel material: {error}");
}
tasks.push(set_input_zone(
entry.bar,
@@ -1058,7 +1067,6 @@ impl FereseShell {
// accounts for the top margin separately.
exclusive_zone: (bar.height.round() as i32)
.saturating_add(shell_theme.bar_window_gap),
..Default::default()
},
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
);
@@ -1113,12 +1121,11 @@ impl FereseShell {
.notes
.iter_mut()
.find(|note| note.id == id)
&& (note.text != value || self.note_error.is_some())
{
if note.text != value || self.note_error.is_some() {
note.text = value.clone();
self.note_pending
.push(note_store::Edit::Text(id.to_owned(), value));
}
note.text = value.clone();
self.note_pending
.push(note_store::Edit::Text(id.to_owned(), value));
}
self.flush_note_changes()
}
@@ -1227,7 +1234,6 @@ impl FereseShell {
exclusive_zone: -1,
size: Some((Some(dimensions.0), Some(dimensions.1))),
size_limits: Limits::NONE,
..Default::default()
},
Some(Box::new(Self::view_note_drag)),
);
@@ -1356,7 +1362,6 @@ impl FereseShell {
exclusive_zone: -1,
size: Some((Some(note.width), Some(note.height))),
size_limits: Limits::NONE,
..Default::default()
}
},
Some(Box::new(move |app| app.view_note(&note_id, id))),
@@ -1581,7 +1586,6 @@ impl FereseShell {
exclusive_zone: -1,
size: Some((Some(clock.width), Some(clock.height))),
size_limits: Limits::NONE,
..Default::default()
}
},
Some(Box::new(move |app| app.view_desktop_clock(id))),
@@ -1854,14 +1858,13 @@ impl FereseShell {
}
}
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left) => {
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left)
if self
.note_drag
.as_ref()
.is_some_and(|drag| drag.source == id)
{
return self.finish_note_drag(true);
}
.is_some_and(|drag| drag.source == id) =>
{
return self.finish_note_drag(true);
}
_ => {}
}
@@ -1987,20 +1990,20 @@ impl FereseShell {
surface,
frame_id,
))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => {
if let Some(menu) = &mut self.menu {
if menu.effects.is_none() {
match EffectsBinding::attach_role(
&surface,
menu.kind.material_role(),
menu.progress(),
) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => {
eprintln!("ferese-shell: popover material unavailable: {error}")
}
if let Some(menu) = &mut self.menu
&& menu.effects.is_none()
{
match EffectsBinding::attach_role(
&surface,
menu.kind.material_role(),
menu.progress(),
) {
Ok(binding) => menu.effects = Some(binding),
Err(error) => {
eprintln!("ferese-shell: popover material unavailable: {error}")
}
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
}
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
}
Task::none()
}
@@ -2022,6 +2025,7 @@ impl FereseShell {
.is_some_and(|entry| entry.id == layer_id) =>
{
let entry = self.notification_surface.as_mut().unwrap();
if entry.effects.is_none() {
match EffectsBinding::attach_role(&surface, None, 1.0) {
Ok(binding) => entry.effects = Some(binding),
@@ -2049,6 +2053,7 @@ impl FereseShell {
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
return;
};
if entry.effects.is_some() {
return;
}
@@ -2493,10 +2498,10 @@ fn clamp_note_position(
cosmic::iced::Point::new(
position
.x
.clamp(0., ((output.0 as f32 - size.0 as f32).max(0.)).min(8192.)),
.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)),
position
.y
.clamp(0., ((output.1 as f32 - size.1 as f32).max(0.)).min(8192.)),
.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)),
)
}
@@ -520,7 +520,6 @@ impl FereseShell {
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
width: if history { 0.0 } else { 1.0 },
radius: palette.material_radius.into(),
..Default::default()
},
..Default::default()
}));
+3 -1
View File
@@ -129,7 +129,9 @@ impl Server {
title: limited(summary, 160),
body: limited(body, 500),
actions: actions
.chunks_exact(2)
.as_chunks::<2>()
.0
.iter()
.take(8)
.map(|pair| (limited(&pair[0], 128), limited(&pair[1], 40)))
.collect(),
+6 -4
View File
@@ -80,18 +80,20 @@ impl Recorder {
let (send, receive) = mpsc::sync_channel(8);
self.updates = Some(receive);
self.state = State::Selecting;
std::thread::spawn(move || {
let mut output = BufReader::new(output);
loop {
let mut line = String::new();
match output.by_ref().take(65537).read_line(&mut line) {
Ok(0) | Err(_) => break,
Ok(_) if line.len() > 65536 => break,
Ok(_) => {
if let Ok(update) = serde_json::from_str::<Update>(&line) {
if send.send(Event::Update(update)).is_err() {
break;
}
if let Ok(update) = serde_json::from_str::<Update>(&line)
&& send.send(Event::Update(update)).is_err()
{
break;
}
}
}
+8 -12
View File
@@ -158,13 +158,10 @@ impl Service {
let mut network = dbus_cache::Cache::new("org.freedesktop.NetworkManager");
let mut bluetooth = dbus_cache::Cache::new("org.bluez");
loop {
// A poll overlapping a write is discarded below. Wait for the
// write to finish before retrying instead of launching commands
// repeatedly while a slow control operation is still running.
let Some(before) = wait_for_poll(&polling) else {
break;
};
// A poll overlapping a write is discarded below. Wait for the
// write to finish before retrying instead of launching commands
// repeatedly while a slow control operation is still running.
while let Some(before) = wait_for_poll(&polling) {
let mut snapshot = poll(&mut system_bus, &mut session_bus);
snapshot.network = network.read(before, network::read);
snapshot.bluetooth = bluetooth.read(before, bluetooth::read);
@@ -186,16 +183,14 @@ impl Service {
});
thread::spawn(move || {
loop {
let first = match commands.recv() {
Ok(command) => command,
Err(_) => break,
};
while let Ok(first) = commands.recv() {
let mut pending = vec![first];
for command in commands.try_iter() {
pending.retain(|(_, action)| action.key() != command.1.key());
pending.push(command);
}
for (id, action) in pending {
{
let mut state = shared.0.lock().unwrap();
@@ -210,6 +205,7 @@ impl Service {
shared.1.notify_one();
}
}
shared.0.lock().unwrap().3 = true;
shared.1.notify_one();
});
+267 -234
View File
@@ -89,24 +89,29 @@ impl OpenMenu {
impl FereseShell {
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
if let Some(menu) = &mut self.menu {
if menu.motion.closing() {
menu.motion.retarget(1.0, Instant::now());
return Task::none();
}
if let Some(menu) = &mut self.menu
&& menu.motion.closing()
{
menu.motion.retarget(1.0, Instant::now());
return Task::none();
}
return self.close_menu();
}
if kind == Menu::Calendar {
self.calendar_offset = 0;
}
let destroy = self.destroy_menu();
if !kind.available(&self.status) {
return destroy;
}
if kind == Menu::Notifications && self.notifications.ready {
self.notifications.toggle_history();
}
let notifications = self.sync_notification_surface();
let id = window::Id::unique();
self.menu = Some(OpenMenu {
@@ -303,6 +308,7 @@ impl FereseShell {
)]
.spacing(4)
.align_y(Alignment::Center);
if kind == Menu::Recording
&& let Some(elapsed) = self.recorder.elapsed()
{
@@ -312,14 +318,16 @@ impl FereseShell {
.class(theme::Text::Color(foreground)),
);
}
if kind == Menu::Battery && self.config.status.battery_percentage {
if let Some(battery) = &self.status.battery {
content = content.push(
text(format!("{}%", battery.percent))
.size(metrics.text_size)
.class(theme::Text::Color(foreground)),
);
}
if kind == Menu::Battery
&& self.config.status.battery_percentage
&& let Some(battery) = &self.status.battery
{
content = content.push(
text(format!("{}%", battery.percent))
.size(metrics.text_size)
.class(theme::Text::Color(foreground)),
);
}
if kind == Menu::Notifications
&& self
@@ -492,6 +500,7 @@ impl FereseShell {
let combined = menu.kind == Menu::System;
if combined {
let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill);
if let Some(n) = &self.status.network {
connections = connections.push(connection_control(
"Wi-Fi",
@@ -507,6 +516,7 @@ impl FereseShell {
p,
));
}
if let Some(b) = &self.status.bluetooth {
connections = connections.push(connection_control(
"Bluetooth",
@@ -522,6 +532,7 @@ impl FereseShell {
p,
));
}
if let Some(b) = &self.status.battery {
connections = connections.push(connection_control(
"Battery",
@@ -533,6 +544,7 @@ impl FereseShell {
p,
));
}
if self.status.network.is_some()
|| self.status.bluetooth.is_some()
|| self.status.battery.is_some()
@@ -540,97 +552,102 @@ impl FereseShell {
rows = rows.push(control_card(connections.into(), primary, p));
}
}
if menu.kind == Menu::Network {
if let Some(n) = &self.status.network {
let connected = n.enabled && n.connection.is_some();
rows = rows.push(status_summary(
status_icon(Menu::Network, &self.status).0,
if !n.enabled {
"Wi-Fi is off"
} else {
n.connection.as_deref().unwrap_or("Not connected")
},
if connected {
"Connected"
} else if n.enabled {
"No active wireless connection"
} else {
"Wireless connections are paused"
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
Some((n.enabled, Action::Wifi(!n.enabled))),
));
if menu.kind == Menu::Network
&& let Some(n) = &self.status.network
{
let connected = n.enabled && n.connection.is_some();
rows = rows.push(status_summary(
status_icon(Menu::Network, &self.status).0,
if !n.enabled {
"Wi-Fi is off"
} else {
n.connection.as_deref().unwrap_or("Not connected")
},
if connected {
let strength = match n.signal {
75.. => "Excellent",
50..=74 => "Good",
25..=49 => "Fair",
_ => "Weak",
};
rows = rows.push(
"Connected"
} else if n.enabled {
"No active wireless connection"
} else {
"Wireless connections are paused"
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
Some((n.enabled, Action::Wifi(!n.enabled))),
));
if connected {
let strength = match n.signal {
75.. => "Excellent",
50..=74 => "Good",
25..=49 => "Fair",
_ => "Weak",
};
rows = rows.push(
column![
row![
text("Signal strength").size(12).width(Length::Fill),
text(strength).size(12)
],
level_meter(n.signal, primary, p),
]
.spacing(8),
);
}
}
if menu.kind == Menu::Bluetooth
&& let Some(b) = &self.status.bluetooth
{
rows = rows.push(status_summary(
status_icon(Menu::Bluetooth, &self.status).0,
if b.enabled {
"Bluetooth is on"
} else {
"Bluetooth is off"
},
if !b.enabled {
"Device connections are paused"
} else if b.devices.is_empty() {
"No devices connected"
} else {
"Your connected devices"
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
Some((b.enabled, Action::Bluetooth(!b.enabled))),
));
if b.enabled && !b.devices.is_empty() {
let mut devices = column::with_capacity(b.devices.len()).spacing(8);
for device in &b.devices {
devices = devices.push(
column![
row![
text("Signal strength").size(12).width(Length::Fill),
text(strength).size(12)
],
level_meter(n.signal, primary, p),
text(device).size(14),
text("Connected").size(12).class(theme::Text::Color(muted)),
]
.spacing(8),
.spacing(3),
);
}
}
}
if menu.kind == Menu::Bluetooth {
if let Some(b) = &self.status.bluetooth {
rows = rows.push(status_summary(
status_icon(Menu::Bluetooth, &self.status).0,
if b.enabled {
"Bluetooth is on"
let list: Element<'_, cosmic::Action<Message>> =
if let Some(height) = device_list_height(b.devices.len()) {
cosmic::iced::widget::scrollable(devices)
.height(height)
.width(Length::Fill)
.into()
} else {
"Bluetooth is off"
},
if !b.enabled {
"Device connections are paused"
} else if b.devices.is_empty() {
"No devices connected"
} else {
"Your connected devices"
},
primary,
muted,
color_with_opacity(theme.accent, p),
p,
Some((b.enabled, Action::Bluetooth(!b.enabled))),
));
if b.enabled && !b.devices.is_empty() {
let mut devices = column::with_capacity(b.devices.len()).spacing(8);
for device in &b.devices {
devices = devices.push(
column![
text(device).size(14),
text("Connected").size(12).class(theme::Text::Color(muted)),
]
.spacing(3),
);
}
let list: Element<'_, cosmic::Action<Message>> =
if let Some(height) = device_list_height(b.devices.len()) {
cosmic::iced::widget::scrollable(devices)
.height(height)
.width(Length::Fill)
.into()
} else {
devices.into()
};
rows = rows.push(list);
}
devices.into()
};
rows = rows.push(list);
}
}
if combined || menu.kind == Menu::Audio {
if let Some(a) = &self.status.audio {
if (combined || menu.kind == Menu::Audio)
&& let Some(a) = &self.status.audio
{
let audio = column![
row![
text("Volume").size(13).width(Length::Fill),
@@ -661,55 +678,57 @@ impl FereseShell {
rows = rows.push(audio);
}
}
if combined {
if let Some(value) = self.status.brightness {
let brightness = column![
text("Brightness").size(13),
slider_row(
include_bytes!("../assets/icons/status/brightness.svg"),
value,
true,
primary,
color_with_opacity(theme.accent, p),
p,
if combined && let Some(value) = self.status.brightness {
let brightness = column![
text("Brightness").size(13),
slider_row(
include_bytes!("../assets/icons/status/brightness.svg"),
value,
true,
primary,
color_with_opacity(theme.accent, p),
p,
)
]
.spacing(6);
rows = rows.push(control_card(brightness.into(), primary, p));
}
if (combined || menu.kind == Menu::Notifications)
&& let Some(n) = &self.status.notifications
{
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
rows = rows.push(if combined {
control_card(toggle, primary, p)
} else {
toggle
});
if menu.kind == Menu::Notifications {
rows = rows
.push(
text(if n.count == 0 {
"No notifications".to_owned()
} else if n.count == 1 {
"1 notification".to_owned()
} else {
format!("{} notifications", n.count)
})
.size(13)
.class(theme::Text::Color(muted)),
)
]
.spacing(6);
rows = rows.push(control_card(brightness.into(), primary, p));
.push(menu_button(
"Open notification center",
Message::Control(Action::Notifications),
primary,
p,
));
}
}
if combined || menu.kind == Menu::Notifications {
if let Some(n) = &self.status.notifications {
let toggle = toggle_row("Do Not Disturb", n.dnd, Action::Dnd(!n.dnd), theme, p);
rows = rows.push(if combined {
control_card(toggle, primary, p)
} else {
toggle
});
if menu.kind == Menu::Notifications {
rows = rows
.push(
text(if n.count == 0 {
"No notifications".to_owned()
} else if n.count == 1 {
"1 notification".to_owned()
} else {
format!("{} notifications", n.count)
})
.size(13)
.class(theme::Text::Color(muted)),
)
.push(menu_button(
"Open notification center",
Message::Control(Action::Notifications),
primary,
p,
));
}
}
}
if menu.kind == Menu::Battery {
if let Some(b) = &self.status.battery {
if menu.kind == Menu::Battery
&& let Some(b) = &self.status.battery
{
let battery_color = if b.percent < self.config.status.low_battery_threshold
&& b.status != "Charging"
{
@@ -752,95 +771,103 @@ impl FereseShell {
.class(theme::Text::Color(muted)),
);
}
}
if menu.kind == Menu::Battery {
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
if let Some(profiles) = &self.status.power_profiles {
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
for (index, (label, icon, profile)) in [
if menu.kind == Menu::Battery {
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
if let Some(profiles) = &self.status.power_profiles {
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
for (index, (label, icon, profile)) in [
(
"Power saver",
include_bytes!("../assets/icons/status/power-saver.svg").as_slice(),
"power-saver",
),
(
"Balanced",
include_bytes!("../assets/icons/status/power-balanced.svg")
.as_slice(),
"balanced",
),
(
"Performance",
include_bytes!("../assets/icons/status/power-performance.svg")
.as_slice(),
"performance",
),
]
.into_iter()
.enumerate()
{
if profiles.available[index] {
modes = modes.push(power_tile(
label,
icon,
profile,
profiles.active == profile,
theme,
p,
));
}
}
// TODO: confirm column align fix
rows = rows.push(modes.width(Length::Fill).align_y(Alignment::Center));
} else {
rows = rows.push(
text("Power Profiles service unavailable")
.size(12)
.class(theme::Text::Color(muted)),
);
}
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
for (enabled, label, icon, action) in [
(
"Battery saver",
include_bytes!("../assets/icons/status/power-saver.svg").as_slice(),
"power-saver",
self.status.reboot,
"Restart",
include_bytes!("../assets/icons/status/restart.svg").as_slice(),
Action::Reboot,
),
(
"Balanced",
include_bytes!("../assets/icons/status/power-balanced.svg").as_slice(),
"balanced",
self.status.poweroff,
"Power off",
include_bytes!("../assets/icons/status/power-off.svg").as_slice(),
Action::Poweroff,
),
(
"Performance",
include_bytes!("../assets/icons/status/power-performance.svg")
.as_slice(),
"performance",
self.status.suspend,
"Suspend",
include_bytes!("../assets/icons/status/suspend.svg").as_slice(),
Action::Suspend,
),
]
.into_iter()
.enumerate()
{
if profiles.available[index] {
modes = modes.push(power_tile(
label,
icon,
profile,
profiles.active == profile,
theme,
p,
));
] {
if enabled {
power = power.push(power_tile_action(label, icon, action, theme, p));
}
}
rows = rows.push(modes);
} else {
if self.status.reboot || self.status.poweroff || self.status.suspend {
rows = rows.push(text("Power").size(13).class(theme::Text::Color(muted)));
rows = rows.push(power.align_y(Alignment::Center).width(Length::Fill));
}
}
if !menu.kind.available(&self.status) {
rows = rows.push(
text("Power Profiles service unavailable")
.size(12)
text("Service unavailable")
.size(13)
.class(theme::Text::Color(muted)),
);
}
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
for (enabled, label, icon, action) in [
(
self.status.reboot,
"Restart",
include_bytes!("../assets/icons/status/restart.svg").as_slice(),
Action::Reboot,
),
(
self.status.poweroff,
"Power off",
include_bytes!("../assets/icons/status/power-off.svg").as_slice(),
Action::Poweroff,
),
(
self.status.suspend,
"Suspend",
include_bytes!("../assets/icons/status/suspend.svg").as_slice(),
Action::Suspend,
),
] {
if enabled {
power = power.push(power_tile_action(label, icon, action, theme, p));
}
}
if self.status.reboot || self.status.poweroff || self.status.suspend {
rows = rows.push(text("Power").size(13).class(theme::Text::Color(muted)));
rows = rows.push(power);
}
}
if !menu.kind.available(&self.status) {
rows = rows.push(
text("Service unavailable")
.size(13)
.class(theme::Text::Color(muted)),
);
}
}
if let Some(error) = &self.status_error {
rows = rows.push(text(error).size(12).class(theme::Text::Color(Color {
a: p,
..Color::from_rgb8(230, 172, 90)
})));
}
let compositor_material = menu.effects.is_some();
let panel = container(rows)
.id("ferese-blur-card")
@@ -856,10 +883,10 @@ impl FereseShell {
color: color_with_opacity(theme.text_muted, 0.18 * p),
width: 1.0,
radius: theme.material_radius.into(),
..Default::default()
},
..Default::default()
}));
cosmic::widget::autosize::autosize(
super::motion::animated(
panel.into(),
@@ -1111,20 +1138,20 @@ fn power_tile<'a>(
},
opacity,
);
let tile = column![bar_icon(icon, 19, foreground), text(label).size(11)]
.spacing(3)
.align_x(Alignment::Center);
motion::button(
button::custom(tile)
.width(Length::FillPortion(1))
.padding([8, 2])
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
profile,
)))),
foreground,
selected,
opacity,
)
let tile = column![
bar_icon(icon, 19, foreground),
text(label).size(11).align_x(Alignment::Center)
]
.spacing(3)
.align_x(Alignment::Center);
let btn = button::custom(tile.align_x(Alignment::Center))
.width(Length::FillPortion(1))
.padding([8, 2])
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
profile,
))));
motion::button(btn, foreground, selected, opacity)
}
fn power_tile_action<'a>(
@@ -1135,6 +1162,7 @@ fn power_tile_action<'a>(
opacity: f32,
) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity);
// TODO: confirm column align fix
motion::button(
button::custom(
column![bar_icon(icon, 19, foreground), text(label).size(11)]
@@ -1623,12 +1651,14 @@ mod tests {
#[test]
fn icons_follow_real_states() {
let mut s = Snapshot::default();
s.audio = Some(status::Audio {
volume: 80,
muted: true,
output: String::new(),
});
let mut s = Snapshot {
audio: Some(status::Audio {
volume: 80,
muted: true,
output: String::new(),
}),
..Default::default()
};
assert_eq!(
status_icon(Menu::Audio, &s),
(
@@ -1636,6 +1666,7 @@ mod tests {
false
)
);
s.battery = Some(status::Battery {
percent: 2,
status: "Charging".into(),
@@ -1644,6 +1675,7 @@ mod tests {
status_icon(Menu::Battery, &s).0,
include_bytes!("../assets/icons/status/battery-charging.svg")
);
s.notifications = Some(status::Notifications {
count: 3,
dnd: true,
@@ -1656,6 +1688,7 @@ mod tests {
status_label(Menu::Notifications, &s),
"Notifications: do not disturb"
);
s.bluetooth = Some(status::Bluetooth {
enabled: false,
devices: vec!["Headphones".into()],