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

+1 -1
View File
@@ -21,7 +21,7 @@ resolver = "3"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
license = "MIT" license = "MIT"
rust-version = "1.87" rust-version = "1.98"
[workspace.dependencies] [workspace.dependencies]
bitflags = "2.9.4" bitflags = "2.9.4"
+1 -1
View File
@@ -1101,7 +1101,7 @@ fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result<Out
enabled: settings.enabled, enabled: settings.enabled,
profile: active_profile.map(|profile| profile.name.clone()), profile: active_profile.map(|profile| profile.name.clone()),
physical_size: connector.size(), physical_size: connector.size(),
current_mode: selected_mode.map(|mode| connected_mode_info(mode)), current_mode: selected_mode.map(connected_mode_info),
available_modes: connector available_modes: connector
.modes() .modes()
.iter() .iter()
+1
View File
@@ -47,6 +47,7 @@ impl Runner {
}); });
} }
} }
// TERM/grace/reaping must not pause the compositor's event loop. // TERM/grace/reaping must not pause the compositor's event loop.
for mut removed in previous { for mut removed in previous {
if let Some(mut child) = removed.child.take() { if let Some(mut child) = removed.child.take() {
+7 -7
View File
@@ -127,21 +127,21 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> Option<Vec<[i32; 5]>> {
} }
fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> { fn decode_regions(bytes: &[u8]) -> Option<Vec<[i32; 5]>> {
if bytes.len() % 20 != 0 || bytes.len() > 32 * 20 { if !bytes.len().is_multiple_of(20) || bytes.len() > 32 * 20 {
return None; return None;
} }
bytes bytes
.chunks_exact(20) .as_chunks::<20>()
.0
.iter()
.map(|tuple| { .map(|tuple| {
let values: Vec<_> = tuple let values: [i32; 5] =
.chunks_exact(4) std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
.map(|v| i32::from_ne_bytes(v.try_into().unwrap()))
.collect();
(values[2] > 0 (values[2] > 0
&& values[3] > 0 && values[3] > 0
&& values[4] >= 0 && values[4] >= 0
&& values.iter().all(|v| v.abs_diff(0) <= 32768)) && values.iter().all(|v| v.abs_diff(0) <= 32768))
.then(|| [values[0], values[1], values[2], values[3], values[4]]) .then_some(values)
}) })
.collect() .collect()
} }
+13 -13
View File
@@ -110,19 +110,6 @@ impl CompositorHandler for Ferese {
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use smithay::wayland::shell::wlr_layer::Layer;
#[test]
fn panel_commits_do_not_invalidate_their_own_backdrop() {
assert!(!layer_affects_backdrop(Layer::Top));
assert!(!layer_affects_backdrop(Layer::Overlay));
assert!(layer_affects_backdrop(Layer::Bottom));
assert!(layer_affects_backdrop(Layer::Background));
}
}
impl BufferHandler for Ferese { impl BufferHandler for Ferese {
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {} fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
} }
@@ -148,3 +135,16 @@ impl ShmHandler for Ferese {
&self.shm_state &self.shm_state
} }
} }
#[cfg(test)]
mod tests {
use super::*;
use smithay::wayland::shell::wlr_layer::Layer;
#[test]
fn panel_commits_do_not_invalidate_their_own_backdrop() {
assert!(!layer_affects_backdrop(Layer::Top));
assert!(!layer_affects_backdrop(Layer::Overlay));
assert!(layer_affects_backdrop(Layer::Bottom));
assert!(layer_affects_backdrop(Layer::Background));
}
}
+4 -4
View File
@@ -107,10 +107,10 @@ fn main() -> Result<(), Box<dyn Error>> {
let reload_handle = event_loop.handle(); let reload_handle = event_loop.handle();
let result = event_loop.run(None, &mut state, |state| { let result = event_loop.run(None, &mut state, |state| {
if let Some(monitor) = &mut monitor { if let Some(monitor) = &mut monitor {
if let Some(result) = monitor.poll(std::time::Instant::now()) { if let Some(result) = monitor.poll(std::time::Instant::now())
if let Err(error) = result.and_then(|source| state.reload_config_source(source)) { && let Err(error) = result.and_then(|source| state.reload_config_source(source))
warn!(%error, "config reload rejected; retaining last working config"); {
} warn!(%error, "config reload rejected; retaining last working config");
} }
// At most one timer exists. Edits arriving during debounce move the // At most one timer exists. Edits arriving during debounce move the
// deadline; the old timer wakes once and is replaced if necessary. // deadline; the old timer wakes once and is replaced if necessary.
+1 -1
View File
@@ -148,7 +148,7 @@ mod tests {
Duration::from_millis(2), Duration::from_millis(2),
&[Rectangle::new((0, 0).into(), (100, 50).into())], &[Rectangle::new((0, 0).into(), (100, 50).into())],
0, 0,
FrameEffectMetrics::default(), FrameEffectMetrics,
); );
assert_eq!(metrics.rendered_frames, 0); assert_eq!(metrics.rendered_frames, 0);
+11 -9
View File
@@ -92,6 +92,14 @@ pub(crate) fn init_font_loader(
Ok(()) Ok(())
} }
type OverviewStateLabel = HashMap<
(String, u64, [u32; 4]),
(
MemoryRenderBuffer,
smithay::utils::Size<i32, smithay::utils::Buffer>,
),
>;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct OverviewState { pub(crate) struct OverviewState {
active: bool, active: bool,
@@ -103,13 +111,7 @@ pub(crate) struct OverviewState {
font_family: String, font_family: String,
font_requests: Option<FontRequests>, font_requests: Option<FontRequests>,
font: Option<FontArc>, font: Option<FontArc>,
labels: HashMap< labels: OverviewStateLabel,
(String, u64, [u32; 4]),
(
MemoryRenderBuffer,
smithay::utils::Size<i32, smithay::utils::Buffer>,
),
>,
} }
impl Default for OverviewState { impl Default for OverviewState {
@@ -480,6 +482,7 @@ impl Ferese {
let selected = self let selected = self
.window_under_visual(point) .window_under_visual(point)
.and_then(|w| self.window_ids.get(&w).copied()); .and_then(|w| self.window_ids.get(&w).copied());
if selected.is_some() && self.overview.selected != selected { if selected.is_some() && self.overview.selected != selected {
self.overview.selected = selected; self.overview.selected = selected;
crate::backends::direct::render_all(self); crate::backends::direct::render_all(self);
@@ -655,8 +658,7 @@ impl Ferese {
let card_width = let card_width =
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64) ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
/ capacity as f64) / capacity as f64)
.min(160.0) .clamp(1.0, 160.0);
.max(1.0);
let visible_count = workspaces.len().saturating_sub(offset).min(capacity); let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
let start_x = centered_row_start(strip, visible_count, card_width); let start_x = centered_row_start(strip, visible_count, card_width);
workspaces workspaces
+13 -15
View File
@@ -74,14 +74,12 @@ impl Ferese {
.space .space
.outputs() .outputs()
.all(|output| self.session_lock.presented.contains(output)) .all(|output| self.session_lock.presented.contains(output))
&& let Some(confirmation) = self.session_lock.confirmation.take()
&& confirmation.ext_session_lock().is_alive()
{ {
if let Some(confirmation) = self.session_lock.confirmation.take() { confirmation.lock();
if confirmation.ext_session_lock().is_alive() { self.session_lock.confirmed = true;
confirmation.lock(); tracing::info!("session lock confirmed after safe output frames");
self.session_lock.confirmed = true;
tracing::info!("session lock confirmed after safe output frames");
}
}
} }
} }
@@ -163,14 +161,14 @@ impl SessionLockHandler for Ferese {
} }
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) { fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
if let Some(output) = Output::from_resource(&output) { if let Some(output) = Output::from_resource(&output)
if let Some(geometry) = self.space.output_geometry(&output) { && let Some(geometry) = self.space.output_geometry(&output)
surface.with_pending_state(|state| { {
state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into()) surface.with_pending_state(|state| {
}); state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into())
self.session_lock.surfaces.insert(output, surface); });
self.focus_lock_surface(); self.session_lock.surfaces.insert(output, surface);
} self.focus_lock_surface();
} }
} }
} }
+302 -229
View File
@@ -24,7 +24,7 @@ use smithay::{
}, },
desktop::{ desktop::{
LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output, LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output,
utils::send_frames_surface_tree, space::SpaceElement, utils::send_frames_surface_tree,
}, },
input::{ input::{
Seat, SeatState, Seat, SeatState,
@@ -241,17 +241,205 @@ pub struct RuntimeConfig {
} }
impl Ferese { impl Ferese {
pub fn new(
event_loop: &mut EventLoop<'static, Self>,
display: Display<Self>,
config: RuntimeConfig,
) -> Result<Self, Box<dyn Error>> {
let display_handle = display.handle();
crate::handlers::screencopy::init_global(&display_handle);
crate::effects::init_global(&display_handle);
crate::shell_control::init_global(&display_handle);
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
let compositor_state = CompositorState::new::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let decoration_state = XdgDecorationState::new::<Self>(&display_handle);
let dmabuf_state = DmabufState::new();
let fractional_scale_state = FractionalScaleManagerState::new::<Self>(&display_handle);
let idle_inhibit_state = IdleInhibitManagerState::new::<Self>(&display_handle);
let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle());
let session_lock_state = smithay::wayland::session_lock::SessionLockManagerState::new::<
Self,
_,
>(&display_handle, |_| true);
let input_method_enabled =
std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1");
let input_method_manager_state =
InputMethodManagerState::new::<Self, _>(&display_handle, move |_| input_method_enabled);
let keyboard_shortcuts_inhibit_state =
KeyboardShortcutsInhibitState::new::<Self>(&display_handle);
let shortcut_inhibit_enabled =
std::env::var_os("FERESE_ENABLE_SHORTCUT_INHIBIT").is_some_and(|value| value == "1");
if !shortcut_inhibit_enabled {
display_handle.disable_global::<Self>(keyboard_shortcuts_inhibit_state.global());
}
let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
let single_pixel_buffer_state = SinglePixelBufferState::new::<Self>(&display_handle);
let text_input_manager_state = TextInputManagerState::new::<Self>(&display_handle);
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let presentation_state =
PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
let xdg_foreign_state = XdgForeignState::new::<Self>(&display_handle);
let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::<Self>(&display_handle);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit");
let xkb_options = (!config.input_settings.xkb_options.is_empty())
.then(|| config.input_settings.xkb_options.join(","));
seat.add_keyboard(
XkbConfig {
layout: &config.input_settings.xkb_layout,
variant: &config.input_settings.xkb_variant,
options: xkb_options,
..XkbConfig::default()
},
config.input_settings.repeat_delay_ms,
config.input_settings.repeat_rate,
)?;
seat.add_pointer();
seat.add_touch();
let socket_name = Self::init_wayland_listener(display, event_loop)?;
let start_time = Instant::now();
let cursor_theme = crate::cursor::cursor_theme();
let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default);
let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]);
let mut state = Self {
session_lock_state,
session_lock: crate::session_lock::Lock::default(),
config_source: None,
start_time,
socket_name,
display_handle,
loop_signal: event_loop.get_signal(),
space: Space::default(),
workspaces: WorkspaceSet::new(
config.layout_mode,
config.default_column_width,
config.scrolling_focus_strategy,
),
output_workspaces: OutputWorkspaceMap::default(),
output_ids: HashMap::new(),
output_identity_ids: HashMap::new(),
window_ids: HashMap::new(),
window_geometry: HashMap::new(),
resize_transactions: HashMap::new(),
resize_snapshots: HashMap::new(),
nested_backend: None,
wallpaper: crate::wallpaper::WallpaperState::with_wakeup(
config.wallpaper,
Some(event_loop.get_signal()),
),
maximized_windows: HashSet::new(),
maximized_column_widths: HashMap::new(),
window_stack: crate::stacking::WindowStack::default(),
natural_floating_pending: HashSet::new(),
window_borders: HashMap::new(),
window_dims: HashMap::new(),
window_resize_fills: HashMap::new(),
window_dimming: HashMap::new(),
window_focus: HashMap::new(),
window_shadows: HashMap::new(),
rounded_clip_programs: HashMap::new(),
overview_scrims: HashMap::new(),
material_programs: HashMap::new(),
material_buffers: HashMap::new(),
blur_programs: HashMap::new(),
backdrop_generation: 0,
closing_windows: HashMap::new(),
viewport_animations: HashMap::new(),
scrolling_world_x: HashMap::new(),
viewport_coupled_widths: HashMap::new(),
pending_column_width_cycles: HashSet::new(),
focused_window: None,
column_width_presets: config.column_width_presets,
gap_config: config.gap_config,
input_settings: config.input_settings,
bindings: config.bindings,
window_rules: config.window_rules,
window_rules_applied: HashSet::new(),
theme_settings: config.theme_settings,
animations_enabled: config.animations_enabled,
animation_speed: config.animation_speed,
spring_config: config.spring_config,
viewport_spring_config: config.viewport_spring_config,
output_profiles: config.output_profiles,
autostart: config.autostart,
cursor_status: CursorImageStatus::default_named(),
cursor_redraw_pending: false,
cursor_theme,
named_cursors,
intercepted_keys: HashSet::new(),
swipe: crate::gestures::Swipe::default(),
idle_inhibitors: HashMap::new(),
active_shortcuts_inhibitor: None,
direct_backend: None,
_ipc_socket: None,
pending_dmabuf_imports: Vec::new(),
pending_screencopies: Vec::new(),
shell_resources: Vec::new(),
shell_snapshot_serial: 0,
last_shell_snapshot: None,
overview: crate::overview::OverviewState::with_font_family(config.overview_font_family),
next_window_id: 1,
next_output_id: 1,
last_animation_tick: start_time,
popups: PopupManager::default(),
dismissing_popups: Vec::new(),
seat,
alpha_modifier_state,
compositor_state,
cursor_shape_state,
data_device_state,
decoration_state,
dmabuf_state,
fractional_scale_state,
idle_inhibit_state,
idle_notifier_state,
input_method_manager_state,
keyboard_shortcuts_inhibit_state,
output_manager_state,
pointer_constraints_state,
presentation_state,
primary_selection_state,
relative_pointer_state,
seat_state,
shm_state,
single_pixel_buffer_state,
text_input_manager_state,
viewporter_state,
layer_shell_state,
xdg_activation_state,
xdg_foreign_state,
xdg_shell_state,
xdg_toplevel_icon_manager,
};
state._ipc_socket = Some(crate::ipc::init(event_loop)?);
Ok(state)
}
pub(crate) fn apply_runtime_config(&mut self, config: RuntimeConfig) -> Result<(), String> { pub(crate) fn apply_runtime_config(&mut self, config: RuntimeConfig) -> Result<(), String> {
if self.output_profiles != config.output_profiles { if self.output_profiles != config.output_profiles {
crate::backends::direct::validate_live_outputs(self, &config.output_profiles)?; crate::backends::direct::validate_live_outputs(self, &config.output_profiles)?;
} }
let keyboard_changed = self.input_settings.xkb_layout != config.input_settings.xkb_layout let keyboard_changed = self.input_settings.xkb_layout != config.input_settings.xkb_layout
|| self.input_settings.xkb_variant != config.input_settings.xkb_variant || self.input_settings.xkb_variant != config.input_settings.xkb_variant
|| self.input_settings.xkb_options != config.input_settings.xkb_options; || self.input_settings.xkb_options != config.input_settings.xkb_options;
if let Some(keyboard) = self.seat.get_keyboard() { if let Some(keyboard) = self.seat.get_keyboard() {
if keyboard_changed { if keyboard_changed {
let options = (!config.input_settings.xkb_options.is_empty()) let options = (!config.input_settings.xkb_options.is_empty())
.then(|| config.input_settings.xkb_options.join(",")); .then(|| config.input_settings.xkb_options.join(","));
keyboard keyboard
.set_xkb_config( .set_xkb_config(
self, self,
@@ -269,7 +457,9 @@ impl Ferese {
config.input_settings.repeat_delay_ms, config.input_settings.repeat_delay_ms,
); );
} }
self.advance_animations(Instant::now()); self.advance_animations(Instant::now());
let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad; let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad;
let outputs_changed = self.output_profiles != config.output_profiles; let outputs_changed = self.output_profiles != config.output_profiles;
let bounds = self let bounds = self
@@ -287,6 +477,7 @@ impl Ferese {
(workspace.id, rect) (workspace.id, rect)
}) })
.collect(); .collect();
self.workspaces self.workspaces
.reconfigure_live( .reconfigure_live(
config.layout_mode, config.layout_mode,
@@ -307,20 +498,25 @@ impl Ferese {
self.viewport_spring_config = config.viewport_spring_config; self.viewport_spring_config = config.viewport_spring_config;
self.output_profiles = config.output_profiles; self.output_profiles = config.output_profiles;
self.autostart = config.autostart; self.autostart = config.autostart;
if touchpad_changed { if touchpad_changed {
crate::backends::direct::reload_input_devices(self); crate::backends::direct::reload_input_devices(self);
} }
if outputs_changed { if outputs_changed {
crate::backends::direct::reload_outputs(self); crate::backends::direct::reload_outputs(self);
} }
if old_rules != self.window_rules { if old_rules != self.window_rules {
self.reapply_window_rules(&old_rules); self.reapply_window_rules(&old_rules);
} }
self.overview.set_font_family(config.overview_font_family); self.overview.set_font_family(config.overview_font_family);
self.wallpaper.reload(config.wallpaper); self.wallpaper.reload(config.wallpaper);
self.backdrop_generation = self.backdrop_generation.wrapping_add(1); self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.relayout(); self.relayout();
crate::backends::direct::render_all(self); crate::backends::direct::render_all(self);
Ok(()) Ok(())
} }
@@ -509,191 +705,6 @@ impl Ferese {
} }
} }
pub fn new(
event_loop: &mut EventLoop<'static, Self>,
display: Display<Self>,
config: RuntimeConfig,
) -> Result<Self, Box<dyn Error>> {
let display_handle = display.handle();
crate::handlers::screencopy::init_global(&display_handle);
crate::effects::init_global(&display_handle);
crate::shell_control::init_global(&display_handle);
let alpha_modifier_state = AlphaModifierState::new::<Self>(&display_handle);
let compositor_state = CompositorState::new::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let decoration_state = XdgDecorationState::new::<Self>(&display_handle);
let dmabuf_state = DmabufState::new();
let fractional_scale_state = FractionalScaleManagerState::new::<Self>(&display_handle);
let idle_inhibit_state = IdleInhibitManagerState::new::<Self>(&display_handle);
let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle());
let session_lock_state = smithay::wayland::session_lock::SessionLockManagerState::new::<
Self,
_,
>(&display_handle, |_| true);
let input_method_enabled =
std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1");
let input_method_manager_state =
InputMethodManagerState::new::<Self, _>(&display_handle, move |_| input_method_enabled);
let keyboard_shortcuts_inhibit_state =
KeyboardShortcutsInhibitState::new::<Self>(&display_handle);
let shortcut_inhibit_enabled =
std::env::var_os("FERESE_ENABLE_SHORTCUT_INHIBIT").is_some_and(|value| value == "1");
if !shortcut_inhibit_enabled {
display_handle.disable_global::<Self>(keyboard_shortcuts_inhibit_state.global());
}
let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
let single_pixel_buffer_state = SinglePixelBufferState::new::<Self>(&display_handle);
let text_input_manager_state = TextInputManagerState::new::<Self>(&display_handle);
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let presentation_state =
PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
let xdg_foreign_state = XdgForeignState::new::<Self>(&display_handle);
let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::<Self>(&display_handle);
let mut seat_state = SeatState::new();
let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit");
let xkb_options = (!config.input_settings.xkb_options.is_empty())
.then(|| config.input_settings.xkb_options.join(","));
seat.add_keyboard(
XkbConfig {
layout: &config.input_settings.xkb_layout,
variant: &config.input_settings.xkb_variant,
options: xkb_options,
..XkbConfig::default()
},
config.input_settings.repeat_delay_ms,
config.input_settings.repeat_rate,
)?;
seat.add_pointer();
seat.add_touch();
let socket_name = Self::init_wayland_listener(display, event_loop)?;
let start_time = Instant::now();
let cursor_theme = crate::cursor::cursor_theme();
let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default);
let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]);
let mut state = Self {
session_lock_state,
session_lock: crate::session_lock::Lock::default(),
config_source: None,
start_time,
socket_name,
display_handle,
loop_signal: event_loop.get_signal(),
space: Space::default(),
workspaces: WorkspaceSet::new(
config.layout_mode,
config.default_column_width,
config.scrolling_focus_strategy,
),
output_workspaces: OutputWorkspaceMap::default(),
output_ids: HashMap::new(),
output_identity_ids: HashMap::new(),
window_ids: HashMap::new(),
window_geometry: HashMap::new(),
resize_transactions: HashMap::new(),
resize_snapshots: HashMap::new(),
nested_backend: None,
wallpaper: crate::wallpaper::WallpaperState::with_wakeup(
config.wallpaper,
Some(event_loop.get_signal()),
),
maximized_windows: HashSet::new(),
maximized_column_widths: HashMap::new(),
window_stack: crate::stacking::WindowStack::default(),
natural_floating_pending: HashSet::new(),
window_borders: HashMap::new(),
window_dims: HashMap::new(),
window_resize_fills: HashMap::new(),
window_dimming: HashMap::new(),
window_focus: HashMap::new(),
window_shadows: HashMap::new(),
rounded_clip_programs: HashMap::new(),
overview_scrims: HashMap::new(),
material_programs: HashMap::new(),
material_buffers: HashMap::new(),
blur_programs: HashMap::new(),
backdrop_generation: 0,
closing_windows: HashMap::new(),
viewport_animations: HashMap::new(),
scrolling_world_x: HashMap::new(),
viewport_coupled_widths: HashMap::new(),
pending_column_width_cycles: HashSet::new(),
focused_window: None,
column_width_presets: config.column_width_presets,
gap_config: config.gap_config,
input_settings: config.input_settings,
bindings: config.bindings,
window_rules: config.window_rules,
window_rules_applied: HashSet::new(),
theme_settings: config.theme_settings,
animations_enabled: config.animations_enabled,
animation_speed: config.animation_speed,
spring_config: config.spring_config,
viewport_spring_config: config.viewport_spring_config,
output_profiles: config.output_profiles,
autostart: config.autostart,
cursor_status: CursorImageStatus::default_named(),
cursor_redraw_pending: false,
cursor_theme,
named_cursors,
intercepted_keys: HashSet::new(),
swipe: crate::gestures::Swipe::default(),
idle_inhibitors: HashMap::new(),
active_shortcuts_inhibitor: None,
direct_backend: None,
_ipc_socket: None,
pending_dmabuf_imports: Vec::new(),
pending_screencopies: Vec::new(),
shell_resources: Vec::new(),
shell_snapshot_serial: 0,
last_shell_snapshot: None,
overview: crate::overview::OverviewState::with_font_family(config.overview_font_family),
next_window_id: 1,
next_output_id: 1,
last_animation_tick: start_time,
popups: PopupManager::default(),
dismissing_popups: Vec::new(),
seat,
alpha_modifier_state,
compositor_state,
cursor_shape_state,
data_device_state,
decoration_state,
dmabuf_state,
fractional_scale_state,
idle_inhibit_state,
idle_notifier_state,
input_method_manager_state,
keyboard_shortcuts_inhibit_state,
output_manager_state,
pointer_constraints_state,
presentation_state,
primary_selection_state,
relative_pointer_state,
seat_state,
shm_state,
single_pixel_buffer_state,
text_input_manager_state,
viewporter_state,
layer_shell_state,
xdg_activation_state,
xdg_foreign_state,
xdg_shell_state,
xdg_toplevel_icon_manager,
};
state._ipc_socket = Some(crate::ipc::init(event_loop)?);
Ok(state)
}
fn init_wayland_listener( fn init_wayland_listener(
display: Display<Self>, display: Display<Self>,
event_loop: &mut EventLoop<'static, Self>, event_loop: &mut EventLoop<'static, Self>,
@@ -836,35 +847,38 @@ impl Ferese {
} }
pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> { pub fn window_under_visual(&self, position: Point<f64, Logical>) -> Option<Window> {
let overview_active = self.overview.is_active();
let workspace = self.workspace_under_pointer(position)?; let workspace = self.workspace_under_pointer(position)?;
let mut candidates = if self.overview.is_active() {
self.window_ids.keys().cloned().collect::<Vec<_>>() let hit = |window: &Window| -> Option<Window> {
} else {
self.space.elements().rev().cloned().collect::<Vec<_>>()
};
if self.overview.is_active() {
// Match Overview's front-to-back render order during overlapping motion.
candidates.sort_by_key(|window| {
std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0))
});
}
candidates.iter().find_map(|window| {
if !self.window_content_ready(window) { if !self.window_content_ready(window) {
return None; return None;
} }
let id = self.window_ids.get(window)?; let id = self.window_ids.get(window)?;
if self.workspaces.workspace_for_window(*id) != Some(workspace) { if self.workspaces.workspace_for_window(*id) != Some(workspace) {
return None; return None;
} }
let visual = self.presented_window_rect(*id)?;
let caption_height = if self.overview.is_active() { 34.0 } else { 0.0 }; let visual = self.presented_window_rect(*id)?;
let caption_height = if overview_active { 34.0 } else { 0.0 };
(position.x >= visual.x (position.x >= visual.x
&& position.y >= visual.y && position.y >= visual.y
&& position.x < visual.x + visual.width && position.x < visual.x + visual.width
&& position.y < visual.y + visual.height + caption_height) && position.y < visual.y + visual.height + caption_height)
.then(|| window.clone()) .then_some(window.clone())
}) };
if overview_active {
let mut candidates = self.window_ids.keys().cloned().collect::<Vec<_>>();
// Match Overview's front-to-back render order during overlapping motion.
candidates.sort_by_key(|window| {
std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0))
});
candidates.iter().find_map(hit)
} else {
self.space.elements().rev().find_map(hit)
}
} }
fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> { fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> {
@@ -909,6 +923,9 @@ impl Ferese {
id.map_or(usize::MAX, |id| self.window_stack.rank(id)), id.map_or(usize::MAX, |id| self.window_stack.rank(id)),
) )
}); });
if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) {
return;
}
for window in windows { for window in windows {
self.space.raise_element(&window, false); self.space.raise_element(&window, false);
} }
@@ -1571,18 +1588,17 @@ impl Ferese {
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
}); });
if natural_pending || requested_size.is_some() || state_changed { if (natural_pending || requested_size.is_some() || state_changed)
if let Some(serial) = toplevel.send_pending_configure() && let Some(serial) = toplevel.send_pending_configure()
&& requested_size.is_some() && requested_size.is_some()
&& had_geometry && had_geometry
&& self.animations_enabled && self.animations_enabled
{ {
self.resize_transactions.insert( self.resize_transactions.insert(
id, id,
crate::resize_transaction::ResizeTransaction::new(serial, now) crate::resize_transaction::ResizeTransaction::new(serial, now)
.with_source_geometry(window.geometry()), .with_source_geometry(window.geometry()),
); );
}
} }
} }
} }
@@ -2341,22 +2357,22 @@ impl Ferese {
} }
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) { if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
let workspace_id = self.workspaces.workspace_for_window(window).unwrap(); let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id) { if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
if let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout { && let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
if enabled { {
if let Some(column) = layout if enabled {
.columns() if let Some(column) = layout
.iter() .columns()
.find(|column| column.windows.contains(&window)) .iter()
{ .find(|column| column.windows.contains(&window))
self.maximized_column_widths {
.entry(window) self.maximized_column_widths
.or_insert(column.width); .entry(window)
} .or_insert(column.width);
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
let _ = layout.set_column_width(window, width);
} }
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
let _ = layout.set_column_width(window, width);
} }
} }
} }
@@ -2870,6 +2886,18 @@ fn window_has_buffer(window: &Window) -> bool {
}) })
} }
fn stacking_order_settled<'a, T, I>(current: I, target: &'a [T], z_index: impl Fn(&T) -> u8) -> bool
where
I: IntoIterator<Item = &'a T>,
T: PartialEq,
{
let Some((first, rest)) = target.split_first() else {
return current.into_iter().next().is_none();
};
let expected = z_index(first);
rest.iter().all(|window| z_index(window) == expected) && current.into_iter().eq(target)
}
#[derive(Default)] #[derive(Default)]
pub struct ClientState { pub struct ClientState {
pub compositor_state: CompositorClientState, pub compositor_state: CompositorClientState,
@@ -3016,4 +3044,49 @@ mod tests {
1_000.0 1_000.0
); );
} }
#[derive(Clone, Copy, PartialEq, Debug)]
struct StackedWindow {
id: u32,
z_index: u8,
}
fn stacked(id: u32, z_index: u8) -> StackedWindow {
StackedWindow { id, z_index }
}
#[test]
fn stacking_is_settled_when_order_matches_and_z_index_is_uniform() {
let current = [stacked(1, 30), stacked(2, 30), stacked(3, 30)];
assert!(stacking_order_settled(current.iter(), &current, |window| {
window.z_index
}));
}
#[test]
fn stacking_is_not_settled_when_order_differs() {
let current = [stacked(2, 30), stacked(1, 30)];
let target = [stacked(1, 30), stacked(2, 30)];
assert!(!stacking_order_settled(current.iter(), &target, |window| {
window.z_index
}));
}
#[test]
fn stacking_is_not_settled_when_z_index_is_mixed_even_with_matching_order() {
let current = [stacked(1, 30), stacked(2, 40)];
assert!(!stacking_order_settled(
current.iter(),
&current,
|window| window.z_index
));
}
#[test]
fn stacking_is_settled_for_an_empty_space() {
let empty: [StackedWindow; 0] = [];
assert!(stacking_order_settled(empty.iter(), &empty, |window| {
window.z_index
}));
}
} }
+39 -40
View File
@@ -1017,7 +1017,7 @@ pub(crate) fn frame_effect_metrics(
_elements: &[AnimatedWindowRenderElement], _elements: &[AnimatedWindowRenderElement],
_scale: f64, _scale: f64,
) -> FrameEffectMetrics { ) -> FrameEffectMetrics {
FrameEffectMetrics::default() FrameEffectMetrics
} }
fn output_elements( fn output_elements(
@@ -1220,45 +1220,44 @@ fn output_elements(
output, output,
&programs, &programs,
); );
if !scale_content { if !scale_content
if let Some(snapshot) = state.resize_snapshots.get(&id) && let Some(snapshot) = state.resize_snapshots.get(&id)
&& snapshot.context == renderer.context_id().erased() && snapshot.context == renderer.context_id().erased()
&& (snapshot.scale - scale).abs() < 0.001 && (snapshot.scale - scale).abs() < 0.001
{ {
let size = snapshot.texture.size(); let size = snapshot.texture.size();
let visible = let visible =
Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels); Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels);
if let Some(visible) = visible { if let Some(visible) = visible {
let clip = framebuffer_clip_rect( let clip = framebuffer_clip_rect(
pixels, pixels,
output.current_mode().unwrap().size, output.current_mode().unwrap().size,
output.current_transform().invert(), output.current_transform().invert(),
); );
elements.push( elements.push(
NativeTextureElement { NativeTextureElement {
id: snapshot.id.clone(), id: snapshot.id.clone(),
commit: snapshot.commit, commit: snapshot.commit,
texture: snapshot.texture.clone(), texture: snapshot.texture.clone(),
geometry: visible, geometry: visible,
source: Rectangle::from_size(Size::from(( source: Rectangle::from_size(Size::from((
f64::from(visible.size.w), f64::from(visible.size.w),
f64::from(visible.size.h), f64::from(visible.size.h),
))), ))),
alpha: crate::presentation::handoff_alpha(snapshot.elapsed) alpha: crate::presentation::handoff_alpha(snapshot.elapsed)
* close_alpha, * close_alpha,
program: Some(programs.texture.clone()), program: Some(programs.texture.clone()),
uniforms: vec![ uniforms: vec![
Uniform::new("clip_rect", clip).into_owned(), Uniform::new("clip_rect", clip).into_owned(),
Uniform::new( Uniform::new(
"radius", "radius",
scaled_effect_value(window_radius, constrain, scale), scaled_effect_value(window_radius, constrain, scale),
) )
.into_owned(), .into_owned(),
], ],
} }
.into(), .into(),
); );
}
} }
} }
elements.extend(rounded_window_elements( elements.extend(rounded_window_elements(
+20 -16
View File
@@ -39,6 +39,7 @@ pub fn default_wallpaper() -> &'static str {
.unwrap_or_else(|| { .unwrap_or_else(|| {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png") PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")
}); });
path.canonicalize() path.canonicalize()
.unwrap_or(path) .unwrap_or(path)
.to_string_lossy() .to_string_lossy()
@@ -50,6 +51,7 @@ fn field(name: &str, parent: &str) -> String {
if parent == "commands" { if parent == "commands" {
return name.to_owned(); return name.to_owned();
} }
match name { match name {
"binding" => "bindings".into(), "binding" => "bindings".into(),
"window-rule" => "window_rules".into(), "window-rule" => "window_rules".into(),
@@ -61,11 +63,14 @@ fn field(name: &str, parent: &str) -> String {
} }
fn is_records(key: &str, parent: &str) -> bool { fn is_records(key: &str, parent: &str) -> bool {
match (parent, key) { matches!(
("", "bindings" | "window_rules" | "output_profiles" | "autostart") => true, (parent, key),
("output_profiles", "outputs") | ("desktop_widgets", "notes") => true, (
_ => false, "",
} "bindings" | "window_rules" | "output_profiles" | "autostart"
) | ("output_profiles", "outputs")
| ("desktop_widgets", "notes")
)
} }
fn is_array(key: &str, parent: &str) -> bool { fn is_array(key: &str, parent: &str) -> bool {
@@ -365,11 +370,11 @@ fn restore_node_formats(doc: &mut KdlDocument, formats: &mut impl Iterator<Item
entry.set_format(format); entry.set_format(format);
} }
} }
if let Some(terminator) = terminator { if let Some(terminator) = terminator
if let Some(format) = node.format_mut() { && let Some(format) = node.format_mut()
format.before_terminator = " ".into(); {
format.terminator = terminator; format.before_terminator = " ".into();
} format.terminator = terminator;
} }
} }
if let Some(children) = node.children_mut() { if let Some(children) = node.children_mut() {
@@ -519,15 +524,14 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) ->
} }
let node = &mut doc.nodes_mut()[i]; let node = &mut doc.nodes_mut()[i];
if parts.len() == 2 { if parts.len() == 2
if let Some(entry) = node && let Some(entry) = node
.entries_mut() .entries_mut()
.iter_mut() .iter_mut()
.find(|e| e.name().is_some_and(|n| field(n.value(), key) == parts[1])) .find(|e| e.name().is_some_and(|n| field(n.value(), key) == parts[1]))
{ {
set_entry_value(entry, kdl_value(&value)?); set_entry_value(entry, kdl_value(&value)?);
return Ok(()); return Ok(());
}
} }
set_in( set_in(
+10 -2
View File
@@ -456,7 +456,10 @@ impl ScrollingLayout {
if let Some(focused) = focused { if let Some(focused) = focused {
self.focus(focused)?; self.focus(focused)?;
} }
self.validate()?;
self.validate_scalars()?;
debug_assert!(self.validate().is_ok());
if self.columns.is_empty() { if self.columns.is_empty() {
self.viewport_x = 0.0; self.viewport_x = 0.0;
return Ok(LayoutResult::default()); return Ok(LayoutResult::default());
@@ -673,7 +676,7 @@ impl ScrollingLayout {
}) })
} }
pub fn validate(&self) -> Result<(), LayoutError> { fn validate_scalars(&self) -> Result<(), LayoutError> {
if !self.viewport_x.is_finite() { if !self.viewport_x.is_finite() {
return Err(LayoutError::InvalidTree( return Err(LayoutError::InvalidTree(
"scrolling viewport offset is invalid", "scrolling viewport offset is invalid",
@@ -701,6 +704,11 @@ impl ScrollingLayout {
"scrolling active column is invalid", "scrolling active column is invalid",
)); ));
} }
Ok(())
}
pub fn validate(&self) -> Result<(), LayoutError> {
self.validate_scalars()?;
let mut windows = HashSet::new(); let mut windows = HashSet::new();
for column in &self.columns { for column in &self.columns {
+8 -6
View File
@@ -109,12 +109,14 @@ impl Appearance {
} else { } else {
ThemeBuilder::dark() ThemeBuilder::dark()
}; };
let mut corners = cosmic::cosmic_theme::CornerRadii::default(); let corners = cosmic::cosmic_theme::CornerRadii {
corners.radius_xs = [self.radius.min(4.); 4]; radius_0: Default::default(),
corners.radius_s = [self.radius.min(8.); 4]; radius_xs: [self.radius.min(4.); 4],
corners.radius_m = [self.radius; 4]; radius_s: [self.radius.min(8.); 4],
corners.radius_l = [self.radius; 4]; radius_m: [self.radius; 4],
corners.radius_xl = [self.radius; 4]; radius_l: [self.radius; 4],
radius_xl: [self.radius; 4],
};
let mut native = builder let mut native = builder
.corner_radii(corners) .corner_radii(corners)
.bg_color(color(self.panel)) .bg_color(color(self.panel))
+69 -26
View File
@@ -1,3 +1,5 @@
#![feature(box_patterns)]
mod appearance; mod appearance;
mod auth; mod auth;
mod runtime; mod runtime;
@@ -36,19 +38,23 @@ fn main() -> iced::Result {
} else { } else {
ferese_config::config_path() ferese_config::config_path()
}; };
if !args.is_empty() && !preview && args != ["--foreground"] { if !args.is_empty() && !preview && args != ["--foreground"] {
eprintln!("Usage: ferese-lock [--preview | --foreground]"); eprintln!("Usage: ferese-lock [--preview | --foreground]");
std::process::exit(2); std::process::exit(2);
} }
if !preview && !auth::available() { if !preview && !auth::available() {
eprintln!("ferese-lock: install the ferese-lock PAM policy before locking"); eprintln!("ferese-lock: install the ferese-lock PAM policy before locking");
std::process::exit(1); std::process::exit(1);
} }
if !preview { if !preview {
if let Err(error) = runtime::check_protocol() { if let Err(error) = runtime::check_protocol() {
eprintln!("ferese-lock: {error}"); eprintln!("ferese-lock: {error}");
std::process::exit(1); std::process::exit(1);
} }
if args.is_empty() { if args.is_empty() {
if let Err(error) = runtime::daemonize() { if let Err(error) = runtime::daemonize() {
eprintln!("ferese-lock: {error}"); eprintln!("ferese-lock: {error}");
@@ -57,14 +63,17 @@ fn main() -> iced::Result {
return Ok(()); return Ok(());
} }
} }
let user = auth::username().unwrap_or_else(|error| { let user = auth::username().unwrap_or_else(|error| {
eprintln!("ferese-lock: {error}"); eprintln!("ferese-lock: {error}");
std::process::exit(1) std::process::exit(1)
}); });
// Password-bearing UI processes must not create core dumps. // Password-bearing UI processes must not create core dumps.
unsafe { unsafe {
libc::prctl(libc::PR_SET_DUMPABLE, 0); libc::prctl(libc::PR_SET_DUMPABLE, 0);
} }
let appearance = appearance::Appearance::load(&user, config.as_deref()); let appearance = appearance::Appearance::load(&user, config.as_deref());
cosmic::app::run::<Locker>( cosmic::app::run::<Locker>(
Settings::default() Settings::default()
@@ -81,18 +90,20 @@ fn main() -> iced::Result {
#[derive(Clone)] #[derive(Clone)]
enum Message { enum Message {
Event(Event), Event(Box<Event>),
Input(String), Input(String),
Submit, Submit,
Authenticated(bool), Authenticated(bool),
Tick, Tick,
} }
// Never derive Debug for password-bearing messages. // Never derive Debug for password-bearing messages.
impl std::fmt::Debug for Message { impl std::fmt::Debug for Message {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("LockerMessage") f.write_str("LockerMessage")
} }
} }
#[derive(Default, Debug, PartialEq)] #[derive(Default, Debug, PartialEq)]
enum AuthState { enum AuthState {
#[default] #[default]
@@ -101,10 +112,12 @@ enum AuthState {
Rejected(Instant), Rejected(Instant),
Unlocking, Unlocking,
} }
impl AuthState { impl AuthState {
fn may_submit(&self) -> bool { fn may_submit(&self) -> bool {
matches!(self, Self::Ready) matches!(self, Self::Ready)
} }
fn complete(&mut self, success: bool) -> bool { fn complete(&mut self, success: bool) -> bool {
if *self != Self::Checking { if *self != Self::Checking {
return false; return false;
@@ -117,6 +130,7 @@ impl AuthState {
success success
} }
} }
struct Locker { struct Locker {
core: Core, core: Core,
preview: bool, preview: bool,
@@ -132,17 +146,22 @@ struct Locker {
confirmation: runtime::Confirmation, confirmation: runtime::Confirmation,
caps_lock: bool, caps_lock: bool,
} }
impl cosmic::Application for Locker { impl cosmic::Application for Locker {
type Executor = cosmic::executor::Default; type Executor = cosmic::executor::Default;
type Flags = (bool, String, appearance::Appearance); type Flags = (bool, String, appearance::Appearance);
type Message = Message; type Message = Message;
const APP_ID: &'static str = "dev.ferese.Lock"; const APP_ID: &'static str = "dev.ferese.Lock";
fn core(&self) -> &Core { fn core(&self) -> &Core {
&self.core &self.core
} }
fn core_mut(&mut self) -> &mut Core { fn core_mut(&mut self) -> &mut Core {
&mut self.core &mut self.core
} }
fn init(mut core: Core, (preview, user, appearance): Self::Flags) -> (Self, Task<Message>) { fn init(mut core: Core, (preview, user, appearance): Self::Flags) -> (Self, Task<Message>) {
core.window.show_headerbar = false; core.window.show_headerbar = false;
let now = jiff::Zoned::now(); let now = jiff::Zoned::now();
@@ -174,6 +193,7 @@ impl cosmic::Application for Locker {
}, },
) )
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
Subscription::batch([ Subscription::batch([
event::listen_with(|event, _, _| match event { event::listen_with(|event, _, _| match event {
@@ -182,22 +202,26 @@ impl cosmic::Application for Locker {
)) ))
| Event::Window(window::Event::Opened { .. }) | Event::Window(window::Event::Opened { .. })
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => { | Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => {
Some(Message::Event(event)) Some(Message::Event(Box::new(event)))
} }
_ => None, _ => None,
}), }),
iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick), iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick),
]) ])
} }
fn update(&mut self, message: Message) -> Task<Message> { fn update(&mut self, message: Message) -> Task<Message> {
match 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); self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK);
} }
Message::Tick => { Message::Tick => {
let now = jiff::Zoned::now(); let now = jiff::Zoned::now();
self.clock = now.strftime(self.appearance.clock_format()).to_string(); self.clock = now.strftime(self.appearance.clock_format()).to_string();
self.date = now.strftime("%A, %B %-d").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)) if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2))
{ {
self.state = AuthState::Ready; self.state = AuthState::Ready;
@@ -217,12 +241,14 @@ impl cosmic::Application for Locker {
self.status = "Preview only · your session is not locked"; self.status = "Preview only · your session is not locked";
return focus(); return focus();
} }
if !self.confirmation.confirmed() if !self.confirmation.confirmed()
|| !self.state.may_submit() || !self.state.may_submit()
|| self.password.is_empty() || self.password.is_empty()
{ {
return Task::none(); return Task::none();
} }
self.state = AuthState::Checking; self.state = AuthState::Checking;
self.status = "Checking password…"; self.status = "Checking password…";
let password = Zeroizing::new(std::mem::take(&mut *self.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."; self.status = "Password not accepted. Try again.";
return focus(); return focus();
} }
Message::Event(Event::PlatformSpecific(PlatformSpecific::Wayland( Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
wayland::Event::Output(event, output), wayland::Event::Output(event, output),
))) if !self.preview => { ))) if !self.preview => {
if matches!(event, wayland::OutputEvent::Removed) { 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), wayland::Event::SessionLock(event),
))) if !self.preview => match event { ))) if !self.preview => match event {
wayland::SessionLockEvent::Locked => { 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() Task::none()
} }
fn on_escape(&mut self) -> Task<Message> { fn on_escape(&mut self) -> Task<Message> {
self.password.zeroize(); self.password.zeroize();
focus() focus()
} }
fn view(&self) -> Element<'_, Message> { fn view(&self) -> Element<'_, Message> {
self.screen() self.screen()
} }
fn view_window(&self, _: window::Id) -> Element<'_, Message> { fn view_window(&self, _: window::Id) -> Element<'_, Message> {
self.screen() self.screen()
} }
} }
fn focus() -> Task<Message> { fn focus() -> Task<Message> {
widget::text_input::focus(widget::Id::new("password")) widget::text_input::focus(widget::Id::new("password"))
} }
impl Locker { impl Locker {
fn label<'a>( fn label<'a>(
&self, &self,
@@ -312,9 +345,11 @@ impl Locker {
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> { ) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
widget::text(label).font(self.appearance.font).size(size) widget::text(label).font(self.appearance.font).size(size)
} }
fn screen(&self) -> Element<'_, Message> { fn screen(&self) -> Element<'_, Message> {
iced::widget::responsive(move |size| self.screen_content(size)).into() iced::widget::responsive(move |size| self.screen_content(size)).into()
} }
fn screen_content(&self, size: iced::Size) -> Element<'_, Message> { 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.09).clamp(64., 112.);
let clock_size = (size.height * 0.165) let clock_size = (size.height * 0.165)
@@ -336,12 +371,14 @@ impl Locker {
.style(password_style(a.radius, a.accent, a.panel)) .style(password_style(a.radius, a.accent, a.panel))
.font(a.font) .font(a.font)
.id(widget::Id::new("password")); .id(widget::Id::new("password"));
if self.state.may_submit() { if self.state.may_submit() {
input = input.on_input(Message::Input).on_submit(|mut value| { input = input.on_input(Message::Input).on_submit(|mut value| {
value.zeroize(); value.zeroize();
Message::Submit Message::Submit
}); });
} }
let submit = widget::button::custom(widget::icon::from_name("go-next-symbolic").size(20)) let submit = widget::button::custom(widget::icon::from_name("go-next-symbolic").size(20))
.class(theme::Button::Icon) .class(theme::Button::Icon)
.padding(6) .padding(6)
@@ -401,6 +438,7 @@ impl Locker {
.min((size.width - margin * 2.).max(160.)), .min((size.width - margin * 2.).max(160.)),
); );
let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center); let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center);
if a.show_clock { if a.show_clock {
clock = clock.push( clock = clock.push(
self.label(&self.clock, clock_size) self.label(&self.clock, clock_size)
@@ -411,12 +449,14 @@ impl Locker {
.class(iced::Color::WHITE), .class(iced::Color::WHITE),
); );
} }
if a.show_date { if a.show_date {
clock = clock.push( clock = clock.push(
self.label(&self.date, 18) self.label(&self.date, 18)
.class(iced::Color::WHITE.scale_alpha(0.72)), .class(iced::Color::WHITE.scale_alpha(0.72)),
); );
} }
let clock_height = if a.show_clock { let clock_height = if a.show_clock {
clock_size as f32 * 1.3 clock_size as f32 * 1.3
} else { } else {
@@ -472,26 +512,6 @@ impl Locker {
iced::widget::stack![background, foreground].into() 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 { fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
let svg = format!( 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) widget::icon(widget::icon::from_svg_bytes(svg.into_bytes()).symbolic(true)).size(size)
} }
fn ferese_symbol() -> widget::icon::Icon { fn ferese_symbol() -> widget::icon::Icon {
widget::icon( widget::icon(
widget::icon::from_svg_bytes( 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), selected_fill: accent.scale_alpha(0.5),
label_color: iced::Color::WHITE, label_color: iced::Color::WHITE,
}; };
theme::TextInput::Custom { theme::TextInput::Custom {
active: Box::new(move |_| appearance(false)), active: Box::new(move |_| appearance(false)),
hovered: Box::new(move |_| appearance(true)), 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)), 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 { } else {
ThemeBuilder::dark() ThemeBuilder::dark()
}; };
let corners = cosmic::cosmic_theme::CornerRadii {
let mut corners = cosmic::cosmic_theme::CornerRadii::default(); radius_xs: [self.radius.min(4.); 4],
corners.radius_xs = [self.radius.min(4.); 4]; radius_s: [self.radius.min(8.); 4],
corners.radius_s = [self.radius.min(8.); 4]; radius_m: [self.radius; 4],
corners.radius_m = [self.radius; 4]; radius_l: [self.radius; 4],
corners.radius_l = [self.radius; 4]; radius_xl: [self.radius; 4],
corners.radius_xl = [self.radius; 4]; radius_0: Default::default(),
};
let mut native = builder let mut native = builder
.corner_radii(corners) .corner_radii(corners)
+18 -4
View File
@@ -53,6 +53,7 @@ impl Api {
let cancel = *agent let cancel = *agent
.get(b"polkit_agent_session_cancel\0") .get(b"polkit_agent_session_cancel\0")
.map_err(|e| e.to_string())?; .map_err(|e| e.to_string())?;
Ok(Self { Ok(Self {
_agent: agent, _agent: agent,
_polkit: polkit, _polkit: polkit,
@@ -199,7 +200,10 @@ fn run_session(
connect( connect(
session, session,
c"request", 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), request as unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
)), )),
signal_data, signal_data,
@@ -207,7 +211,10 @@ fn run_session(
connect( connect(
session, session,
c"show-info", 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), info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)), )),
signal_data, signal_data,
@@ -215,7 +222,10 @@ fn run_session(
connect( connect(
session, session,
c"show-error", 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), error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
)), )),
signal_data, signal_data,
@@ -223,11 +233,15 @@ fn run_session(
connect( connect(
session, session,
c"completed", 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), completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
)), )),
signal_data, signal_data,
); );
(api.initiate)(session); (api.initiate)(session);
} }
+10 -12
View File
@@ -274,10 +274,8 @@ impl cosmic::Application for App {
|result| cosmic::Action::App(Message::LockPreviewStarted(result)), |result| cosmic::Action::App(Message::LockPreviewStarted(result)),
); );
} }
Message::LockPreviewStarted(result) => { Message::LockPreviewStarted(Err(error)) => {
if let Err(error) = result { self.error = Some(error);
self.error = Some(error);
}
} }
Message::Page(page) => { Message::Page(page) => {
self.page = page; self.page = page;
@@ -1286,16 +1284,16 @@ impl App {
// When a gradient exists, changing Accent also changes its // When a gradient exists, changing Accent also changes its
// leading stop; otherwise the visible focus border would stay // leading stop; otherwise the visible focus border would stay
// on the old palette despite the control saying it changed. // on the old palette despite the control saying it changed.
if let Edit::Set(path, value) = &edit { if let Edit::Set(path, value) = &edit
if path == "theme.colors.accent" && path == "theme.colors.accent"
&& self.draft.item("theme.focus_ring.gradient").is_some() && self.draft.item("theme.focus_ring.gradient").is_some()
{ {
let gradient = set("theme.focus_ring.gradient.from", value.clone()); let gradient = set("theme.focus_ring.gradient.from", value.clone());
if self.draft.edit(&gradient).is_ok() { if self.draft.edit(&gradient).is_ok() {
self.pending.push(gradient); self.pending.push(gradient);
}
} }
} }
self.pending.push(edit); self.pending.push(edit);
self.error = None; self.error = None;
self.flush() 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_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65;
let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65; let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65;
let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" { 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 { } else {
1. 1.
}; };
@@ -556,11 +561,12 @@ mod tests {
fn default_uses_logo_blue_and_native_controls_follow_lightness() { fn default_uses_logo_blue_and_native_controls_follow_lightness() {
let logo = include_str!("../../../packaging/icons/ferese.svg").to_ascii_uppercase(); let logo = include_str!("../../../packaging/icons/ferese.svg").to_ascii_uppercase();
assert!(logo.contains(PRESETS[0].accent)); assert!(logo.contains(PRESETS[0].accent));
for index in 0..PRESETS.len() {
for (index, item) in PRESETS.iter().enumerate() {
let snapshot = configured_preset(index); let snapshot = configured_preset(index);
assert_eq!( assert_eq!(
native_theme(Some(&snapshot)).cosmic().is_dark, 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;
mod status_ui; 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::Element;
use cosmic::app::{Core, Settings, Task}; use cosmic::app::{Core, Settings, Task};
use cosmic::iced::alignment; use cosmic::iced::alignment;
@@ -37,6 +29,8 @@ use ferese_protocols::effects::v1::client::{
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1}, ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
}; };
use jiff::Zoned; use jiff::Zoned;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use wayland_client::{ use wayland_client::{
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop, Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
globals::{GlobalListContents, registry_queue_init}, globals::{GlobalListContents, registry_queue_init},
@@ -48,6 +42,10 @@ use crate::control::{ShellControl, ShellSnapshot};
const APP_ID: &str = "dev.ferese.Shell"; 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>> = static SHELL_FONT: std::sync::OnceLock<std::sync::RwLock<cosmic::font::Font>> =
std::sync::OnceLock::new(); std::sync::OnceLock::new();
@@ -117,10 +115,11 @@ fn stacked_clock_digits(label: &str) -> Option<[String; 2]> {
let mut parts = label.split_whitespace(); let mut parts = label.split_whitespace();
let time = parts.next()?; let time = parts.next()?;
if let Some(period) = parts.next() { if let Some(period) = parts.next()
if !period.eq_ignore_ascii_case("am") && !period.eq_ignore_ascii_case("pm") { && !period.eq_ignore_ascii_case("am")
return None; && !period.eq_ignore_ascii_case("pm")
} {
return None;
} }
if parts.next().is_some() { if parts.next().is_some() {
@@ -650,8 +649,7 @@ impl cosmic::Application for FereseShell {
let open = self.open_menu(kind, anchor); let open = self.open_menu(kind, anchor);
Task::batch([destroy, open]) Task::batch([destroy, open])
} else { } else {
let open = self.open_menu(kind, anchor); self.open_menu(kind, anchor)
open
} }
} }
Message::ConfirmPower(action) => { Message::ConfirmPower(action) => {
@@ -680,10 +678,11 @@ impl cosmic::Application for FereseShell {
.status_service .status_service
.send(status::Action::PowerProfile(profile)) .send(status::Action::PowerProfile(profile))
.err(); .err();
if self.status_error.is_none() {
if let Some(profiles) = &mut self.status.power_profiles { if self.status_error.is_none()
profiles.active = profile.to_owned(); && let Some(profiles) = &mut self.status.power_profiles
} {
profiles.active = profile.to_owned();
} }
return Task::none(); return Task::none();
} }
@@ -706,22 +705,27 @@ impl cosmic::Application for FereseShell {
_ => {} _ => {}
} }
} }
self.status_error = self.status_service.send(action.clone()).err(); self.status_error = self.status_service.send(action.clone()).err();
if self.status_error.is_none() { if self.status_error.is_none() {
self.optimistic_status(&action); self.optimistic_status(&action);
} }
if matches!( if matches!(
action, action,
status::Action::Notifications | status::Action::Settings status::Action::Notifications | status::Action::Settings
) { ) {
return self.close_menu(); return self.close_menu();
} }
Task::none() Task::none()
} }
Message::Event(event, id) => self.handle_event(event, id), Message::Event(event, id) => self.handle_event(event, id),
Message::Tick => { Message::Tick => {
self.recorder.poll(); self.recorder.poll();
self.clock = current_time(); self.clock = current_time();
if self.config.desktop_widgets.clock.enabled { if self.config.desktop_widgets.clock.enabled {
self.desktop_clock = self self.desktop_clock = self
.config .config
@@ -730,6 +734,7 @@ impl cosmic::Application for FereseShell {
.labels(&Zoned::now()) .labels(&Zoned::now())
.unwrap_or_default(); .unwrap_or_default();
} }
let mut reload_task = Task::none(); let mut reload_task = Task::none();
if let Some(control) = &self.control { if let Some(control) = &self.control {
let poll = control.poll(); let poll = control.poll();
@@ -737,15 +742,19 @@ impl cosmic::Application for FereseShell {
if poll.disconnected { if poll.disconnected {
return cosmic::iced::exit(); return cosmic::iced::exit();
} }
if let Some(active) = poll.overview_active { if let Some(active) = poll.overview_active {
self.overview_active = active; self.overview_active = active;
} }
if let Some(source) = poll.config { if let Some(source) = poll.config {
reload_task = self.reload_config(source); reload_task = self.reload_config(source);
} }
if let Some(snapshot) = poll.snapshot { if let Some(snapshot) = poll.snapshot {
self.snapshot = snapshot; self.snapshot = snapshot;
let mut tasks = vec![reload_task]; let mut tasks = vec![reload_task];
for entry in &mut self.outputs { for entry in &mut self.outputs {
let output = self let output = self
.snapshot .snapshot
@@ -754,16 +763,16 @@ impl cosmic::Application for FereseShell {
.find(|output| Some(output.name.as_str()) == entry.name.as_deref()) .find(|output| Some(output.name.as_str()) == entry.name.as_deref())
.map(|output| output.id); .map(|output| output.id);
let hidden = output_bar_hidden(&self.snapshot, output); let hidden = output_bar_hidden(&self.snapshot, output);
if entry.hidden == hidden { if entry.hidden == hidden {
continue; continue;
} }
entry.hidden = hidden; entry.hidden = hidden;
if let Some(effects) = &entry.effects { if let Some(effects) = &entry.effects
if let Err(error) = effects.set_visible(!hidden) { && let Err(error) = effects.set_visible(!hidden)
eprintln!( {
"ferese-shell: could not update panel material: {error}" eprintln!("ferese-shell: could not update panel material: {error}");
);
}
} }
tasks.push(set_input_zone( tasks.push(set_input_zone(
entry.bar, entry.bar,
@@ -1058,7 +1067,6 @@ impl FereseShell {
// accounts for the top margin separately. // accounts for the top margin separately.
exclusive_zone: (bar.height.round() as i32) exclusive_zone: (bar.height.round() as i32)
.saturating_add(shell_theme.bar_window_gap), .saturating_add(shell_theme.bar_window_gap),
..Default::default()
}, },
Some(Box::new(move |app| app.view_layer(bar_surface_id))), Some(Box::new(move |app| app.view_layer(bar_surface_id))),
); );
@@ -1113,12 +1121,11 @@ impl FereseShell {
.notes .notes
.iter_mut() .iter_mut()
.find(|note| note.id == id) .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();
note.text = value.clone(); self.note_pending
self.note_pending .push(note_store::Edit::Text(id.to_owned(), value));
.push(note_store::Edit::Text(id.to_owned(), value));
}
} }
self.flush_note_changes() self.flush_note_changes()
} }
@@ -1227,7 +1234,6 @@ impl FereseShell {
exclusive_zone: -1, exclusive_zone: -1,
size: Some((Some(dimensions.0), Some(dimensions.1))), size: Some((Some(dimensions.0), Some(dimensions.1))),
size_limits: Limits::NONE, size_limits: Limits::NONE,
..Default::default()
}, },
Some(Box::new(Self::view_note_drag)), Some(Box::new(Self::view_note_drag)),
); );
@@ -1356,7 +1362,6 @@ impl FereseShell {
exclusive_zone: -1, exclusive_zone: -1,
size: Some((Some(note.width), Some(note.height))), size: Some((Some(note.width), Some(note.height))),
size_limits: Limits::NONE, size_limits: Limits::NONE,
..Default::default()
} }
}, },
Some(Box::new(move |app| app.view_note(&note_id, id))), Some(Box::new(move |app| app.view_note(&note_id, id))),
@@ -1581,7 +1586,6 @@ impl FereseShell {
exclusive_zone: -1, exclusive_zone: -1,
size: Some((Some(clock.width), Some(clock.height))), size: Some((Some(clock.width), Some(clock.height))),
size_limits: Limits::NONE, size_limits: Limits::NONE,
..Default::default()
} }
}, },
Some(Box::new(move |app| app.view_desktop_clock(id))), 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 if self
.note_drag .note_drag
.as_ref() .as_ref()
.is_some_and(|drag| drag.source == id) .is_some_and(|drag| drag.source == id) =>
{ {
return self.finish_note_drag(true); return self.finish_note_drag(true);
}
} }
_ => {} _ => {}
} }
@@ -1987,20 +1990,20 @@ impl FereseShell {
surface, surface,
frame_id, frame_id,
))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => { ))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => {
if let Some(menu) = &mut self.menu { if let Some(menu) = &mut self.menu
if menu.effects.is_none() { && menu.effects.is_none()
match EffectsBinding::attach_role( {
&surface, match EffectsBinding::attach_role(
menu.kind.material_role(), &surface,
menu.progress(), menu.kind.material_role(),
) { menu.progress(),
Ok(binding) => menu.effects = Some(binding), ) {
Err(error) => { Ok(binding) => menu.effects = Some(binding),
eprintln!("ferese-shell: popover material unavailable: {error}") 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() Task::none()
} }
@@ -2022,6 +2025,7 @@ impl FereseShell {
.is_some_and(|entry| entry.id == layer_id) => .is_some_and(|entry| entry.id == layer_id) =>
{ {
let entry = self.notification_surface.as_mut().unwrap(); let entry = self.notification_surface.as_mut().unwrap();
if entry.effects.is_none() { if entry.effects.is_none() {
match EffectsBinding::attach_role(&surface, None, 1.0) { match EffectsBinding::attach_role(&surface, None, 1.0) {
Ok(binding) => entry.effects = Some(binding), 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 { let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
return; return;
}; };
if entry.effects.is_some() { if entry.effects.is_some() {
return; return;
} }
@@ -2493,10 +2498,10 @@ fn clamp_note_position(
cosmic::iced::Point::new( cosmic::iced::Point::new(
position position
.x .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 position
.y .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), color: color_with_opacity(palette.text_muted, 0.18 * opacity),
width: if history { 0.0 } else { 1.0 }, width: if history { 0.0 } else { 1.0 },
radius: palette.material_radius.into(), radius: palette.material_radius.into(),
..Default::default()
}, },
..Default::default() ..Default::default()
})); }));
+3 -1
View File
@@ -129,7 +129,9 @@ impl Server {
title: limited(summary, 160), title: limited(summary, 160),
body: limited(body, 500), body: limited(body, 500),
actions: actions actions: actions
.chunks_exact(2) .as_chunks::<2>()
.0
.iter()
.take(8) .take(8)
.map(|pair| (limited(&pair[0], 128), limited(&pair[1], 40))) .map(|pair| (limited(&pair[0], 128), limited(&pair[1], 40)))
.collect(), .collect(),
+6 -4
View File
@@ -80,18 +80,20 @@ impl Recorder {
let (send, receive) = mpsc::sync_channel(8); let (send, receive) = mpsc::sync_channel(8);
self.updates = Some(receive); self.updates = Some(receive);
self.state = State::Selecting; self.state = State::Selecting;
std::thread::spawn(move || { std::thread::spawn(move || {
let mut output = BufReader::new(output); let mut output = BufReader::new(output);
loop { loop {
let mut line = String::new(); let mut line = String::new();
match output.by_ref().take(65537).read_line(&mut line) { match output.by_ref().take(65537).read_line(&mut line) {
Ok(0) | Err(_) => break, Ok(0) | Err(_) => break,
Ok(_) if line.len() > 65536 => break, Ok(_) if line.len() > 65536 => break,
Ok(_) => { Ok(_) => {
if let Ok(update) = serde_json::from_str::<Update>(&line) { if let Ok(update) = serde_json::from_str::<Update>(&line)
if send.send(Event::Update(update)).is_err() { && send.send(Event::Update(update)).is_err()
break; {
} break;
} }
} }
} }
+8 -12
View File
@@ -158,13 +158,10 @@ impl Service {
let mut network = dbus_cache::Cache::new("org.freedesktop.NetworkManager"); let mut network = dbus_cache::Cache::new("org.freedesktop.NetworkManager");
let mut bluetooth = dbus_cache::Cache::new("org.bluez"); let mut bluetooth = dbus_cache::Cache::new("org.bluez");
loop { // A poll overlapping a write is discarded below. Wait for the
// A poll overlapping a write is discarded below. Wait for the // write to finish before retrying instead of launching commands
// write to finish before retrying instead of launching commands // repeatedly while a slow control operation is still running.
// repeatedly while a slow control operation is still running. while let Some(before) = wait_for_poll(&polling) {
let Some(before) = wait_for_poll(&polling) else {
break;
};
let mut snapshot = poll(&mut system_bus, &mut session_bus); let mut snapshot = poll(&mut system_bus, &mut session_bus);
snapshot.network = network.read(before, network::read); snapshot.network = network.read(before, network::read);
snapshot.bluetooth = bluetooth.read(before, bluetooth::read); snapshot.bluetooth = bluetooth.read(before, bluetooth::read);
@@ -186,16 +183,14 @@ impl Service {
}); });
thread::spawn(move || { thread::spawn(move || {
loop { while let Ok(first) = commands.recv() {
let first = match commands.recv() {
Ok(command) => command,
Err(_) => break,
};
let mut pending = vec![first]; let mut pending = vec![first];
for command in commands.try_iter() { for command in commands.try_iter() {
pending.retain(|(_, action)| action.key() != command.1.key()); pending.retain(|(_, action)| action.key() != command.1.key());
pending.push(command); pending.push(command);
} }
for (id, action) in pending { for (id, action) in pending {
{ {
let mut state = shared.0.lock().unwrap(); let mut state = shared.0.lock().unwrap();
@@ -210,6 +205,7 @@ impl Service {
shared.1.notify_one(); shared.1.notify_one();
} }
} }
shared.0.lock().unwrap().3 = true; shared.0.lock().unwrap().3 = true;
shared.1.notify_one(); shared.1.notify_one();
}); });
+267 -234
View File
@@ -89,24 +89,29 @@ impl OpenMenu {
impl FereseShell { impl FereseShell {
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> { 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 self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
if let Some(menu) = &mut self.menu { if let Some(menu) = &mut self.menu
if menu.motion.closing() { && menu.motion.closing()
menu.motion.retarget(1.0, Instant::now()); {
return Task::none(); menu.motion.retarget(1.0, Instant::now());
} return Task::none();
} }
return self.close_menu(); return self.close_menu();
} }
if kind == Menu::Calendar { if kind == Menu::Calendar {
self.calendar_offset = 0; self.calendar_offset = 0;
} }
let destroy = self.destroy_menu(); let destroy = self.destroy_menu();
if !kind.available(&self.status) { if !kind.available(&self.status) {
return destroy; return destroy;
} }
if kind == Menu::Notifications && self.notifications.ready { if kind == Menu::Notifications && self.notifications.ready {
self.notifications.toggle_history(); self.notifications.toggle_history();
} }
let notifications = self.sync_notification_surface(); let notifications = self.sync_notification_surface();
let id = window::Id::unique(); let id = window::Id::unique();
self.menu = Some(OpenMenu { self.menu = Some(OpenMenu {
@@ -303,6 +308,7 @@ impl FereseShell {
)] )]
.spacing(4) .spacing(4)
.align_y(Alignment::Center); .align_y(Alignment::Center);
if kind == Menu::Recording if kind == Menu::Recording
&& let Some(elapsed) = self.recorder.elapsed() && let Some(elapsed) = self.recorder.elapsed()
{ {
@@ -312,14 +318,16 @@ impl FereseShell {
.class(theme::Text::Color(foreground)), .class(theme::Text::Color(foreground)),
); );
} }
if kind == Menu::Battery && self.config.status.battery_percentage {
if let Some(battery) = &self.status.battery { if kind == Menu::Battery
content = content.push( && self.config.status.battery_percentage
text(format!("{}%", battery.percent)) && let Some(battery) = &self.status.battery
.size(metrics.text_size) {
.class(theme::Text::Color(foreground)), content = content.push(
); text(format!("{}%", battery.percent))
} .size(metrics.text_size)
.class(theme::Text::Color(foreground)),
);
} }
if kind == Menu::Notifications if kind == Menu::Notifications
&& self && self
@@ -492,6 +500,7 @@ impl FereseShell {
let combined = menu.kind == Menu::System; let combined = menu.kind == Menu::System;
if combined { if combined {
let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill); let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill);
if let Some(n) = &self.status.network { if let Some(n) = &self.status.network {
connections = connections.push(connection_control( connections = connections.push(connection_control(
"Wi-Fi", "Wi-Fi",
@@ -507,6 +516,7 @@ impl FereseShell {
p, p,
)); ));
} }
if let Some(b) = &self.status.bluetooth { if let Some(b) = &self.status.bluetooth {
connections = connections.push(connection_control( connections = connections.push(connection_control(
"Bluetooth", "Bluetooth",
@@ -522,6 +532,7 @@ impl FereseShell {
p, p,
)); ));
} }
if let Some(b) = &self.status.battery { if let Some(b) = &self.status.battery {
connections = connections.push(connection_control( connections = connections.push(connection_control(
"Battery", "Battery",
@@ -533,6 +544,7 @@ impl FereseShell {
p, p,
)); ));
} }
if self.status.network.is_some() if self.status.network.is_some()
|| self.status.bluetooth.is_some() || self.status.bluetooth.is_some()
|| self.status.battery.is_some() || self.status.battery.is_some()
@@ -540,97 +552,102 @@ impl FereseShell {
rows = rows.push(control_card(connections.into(), primary, p)); rows = rows.push(control_card(connections.into(), primary, p));
} }
} }
if menu.kind == Menu::Network {
if let Some(n) = &self.status.network { if menu.kind == Menu::Network
let connected = n.enabled && n.connection.is_some(); && let Some(n) = &self.status.network
rows = rows.push(status_summary( {
status_icon(Menu::Network, &self.status).0, let connected = n.enabled && n.connection.is_some();
if !n.enabled { rows = rows.push(status_summary(
"Wi-Fi is off" status_icon(Menu::Network, &self.status).0,
} else { if !n.enabled {
n.connection.as_deref().unwrap_or("Not connected") "Wi-Fi is off"
}, } else {
if connected { n.connection.as_deref().unwrap_or("Not 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 connected { if connected {
let strength = match n.signal { "Connected"
75.. => "Excellent", } else if n.enabled {
50..=74 => "Good", "No active wireless connection"
25..=49 => "Fair", } else {
_ => "Weak", "Wireless connections are paused"
}; },
rows = rows.push( 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![ column![
row![ text(device).size(14),
text("Signal strength").size(12).width(Length::Fill), text("Connected").size(12).class(theme::Text::Color(muted)),
text(strength).size(12)
],
level_meter(n.signal, primary, p),
] ]
.spacing(8), .spacing(3),
); );
} }
}
} let list: Element<'_, cosmic::Action<Message>> =
if menu.kind == Menu::Bluetooth { if let Some(height) = device_list_height(b.devices.len()) {
if let Some(b) = &self.status.bluetooth { cosmic::iced::widget::scrollable(devices)
rows = rows.push(status_summary( .height(height)
status_icon(Menu::Bluetooth, &self.status).0, .width(Length::Fill)
if b.enabled { .into()
"Bluetooth is on"
} else { } else {
"Bluetooth is off" devices.into()
}, };
if !b.enabled { rows = rows.push(list);
"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);
}
} }
}
if combined || menu.kind == Menu::Audio { if (combined || menu.kind == Menu::Audio)
if let Some(a) = &self.status.audio { && let Some(a) = &self.status.audio
{
let audio = column![ let audio = column![
row![ row![
text("Volume").size(13).width(Length::Fill), text("Volume").size(13).width(Length::Fill),
@@ -661,55 +678,57 @@ impl FereseShell {
rows = rows.push(audio); rows = rows.push(audio);
} }
} }
if combined {
if let Some(value) = self.status.brightness { if combined && let Some(value) = self.status.brightness {
let brightness = column![ let brightness = column![
text("Brightness").size(13), text("Brightness").size(13),
slider_row( slider_row(
include_bytes!("../assets/icons/status/brightness.svg"), include_bytes!("../assets/icons/status/brightness.svg"),
value, value,
true, true,
primary, primary,
color_with_opacity(theme.accent, p), color_with_opacity(theme.accent, p),
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)),
) )
] .push(menu_button(
.spacing(6); "Open notification center",
rows = rows.push(control_card(brightness.into(), primary, p)); Message::Control(Action::Notifications),
primary,
p,
));
} }
}
if combined || menu.kind == Menu::Notifications { if menu.kind == Menu::Battery
if let Some(n) = &self.status.notifications { && let Some(b) = &self.status.battery
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 {
let battery_color = if b.percent < self.config.status.low_battery_threshold let battery_color = if b.percent < self.config.status.low_battery_threshold
&& b.status != "Charging" && b.status != "Charging"
{ {
@@ -752,95 +771,103 @@ impl FereseShell {
.class(theme::Text::Color(muted)), .class(theme::Text::Color(muted)),
); );
} }
}
if menu.kind == Menu::Battery { if menu.kind == Menu::Battery {
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted))); rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
if let Some(profiles) = &self.status.power_profiles { if let Some(profiles) = &self.status.power_profiles {
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill); let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
for (index, (label, icon, profile)) in [ 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", self.status.reboot,
include_bytes!("../assets/icons/status/power-saver.svg").as_slice(), "Restart",
"power-saver", include_bytes!("../assets/icons/status/restart.svg").as_slice(),
Action::Reboot,
), ),
( (
"Balanced", self.status.poweroff,
include_bytes!("../assets/icons/status/power-balanced.svg").as_slice(), "Power off",
"balanced", include_bytes!("../assets/icons/status/power-off.svg").as_slice(),
Action::Poweroff,
), ),
( (
"Performance", self.status.suspend,
include_bytes!("../assets/icons/status/power-performance.svg") "Suspend",
.as_slice(), include_bytes!("../assets/icons/status/suspend.svg").as_slice(),
"performance", Action::Suspend,
), ),
] ] {
.into_iter() if enabled {
.enumerate() power = power.push(power_tile_action(label, icon, action, theme, p));
{
if profiles.available[index] {
modes = modes.push(power_tile(
label,
icon,
profile,
profiles.active == profile,
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( rows = rows.push(
text("Power Profiles service unavailable") text("Service unavailable")
.size(12) .size(13)
.class(theme::Text::Color(muted)), .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 { if let Some(error) = &self.status_error {
rows = rows.push(text(error).size(12).class(theme::Text::Color(Color { rows = rows.push(text(error).size(12).class(theme::Text::Color(Color {
a: p, a: p,
..Color::from_rgb8(230, 172, 90) ..Color::from_rgb8(230, 172, 90)
}))); })));
} }
let compositor_material = menu.effects.is_some(); let compositor_material = menu.effects.is_some();
let panel = container(rows) let panel = container(rows)
.id("ferese-blur-card") .id("ferese-blur-card")
@@ -856,10 +883,10 @@ impl FereseShell {
color: color_with_opacity(theme.text_muted, 0.18 * p), color: color_with_opacity(theme.text_muted, 0.18 * p),
width: 1.0, width: 1.0,
radius: theme.material_radius.into(), radius: theme.material_radius.into(),
..Default::default()
}, },
..Default::default() ..Default::default()
})); }));
cosmic::widget::autosize::autosize( cosmic::widget::autosize::autosize(
super::motion::animated( super::motion::animated(
panel.into(), panel.into(),
@@ -1111,20 +1138,20 @@ fn power_tile<'a>(
}, },
opacity, opacity,
); );
let tile = column![bar_icon(icon, 19, foreground), text(label).size(11)] let tile = column![
.spacing(3) bar_icon(icon, 19, foreground),
.align_x(Alignment::Center); text(label).size(11).align_x(Alignment::Center)
motion::button( ]
button::custom(tile) .spacing(3)
.width(Length::FillPortion(1)) .align_x(Alignment::Center);
.padding([8, 2]) let btn = button::custom(tile.align_x(Alignment::Center))
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile( .width(Length::FillPortion(1))
profile, .padding([8, 2])
)))), .on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
foreground, profile,
selected, ))));
opacity,
) motion::button(btn, foreground, selected, opacity)
} }
fn power_tile_action<'a>( fn power_tile_action<'a>(
@@ -1135,6 +1162,7 @@ fn power_tile_action<'a>(
opacity: f32, opacity: f32,
) -> Element<'a, cosmic::Action<Message>> { ) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity); let foreground = color_with_opacity(theme.text_primary, opacity);
// TODO: confirm column align fix
motion::button( motion::button(
button::custom( button::custom(
column![bar_icon(icon, 19, foreground), text(label).size(11)] column![bar_icon(icon, 19, foreground), text(label).size(11)]
@@ -1623,12 +1651,14 @@ mod tests {
#[test] #[test]
fn icons_follow_real_states() { fn icons_follow_real_states() {
let mut s = Snapshot::default(); let mut s = Snapshot {
s.audio = Some(status::Audio { audio: Some(status::Audio {
volume: 80, volume: 80,
muted: true, muted: true,
output: String::new(), output: String::new(),
}); }),
..Default::default()
};
assert_eq!( assert_eq!(
status_icon(Menu::Audio, &s), status_icon(Menu::Audio, &s),
( (
@@ -1636,6 +1666,7 @@ mod tests {
false false
) )
); );
s.battery = Some(status::Battery { s.battery = Some(status::Battery {
percent: 2, percent: 2,
status: "Charging".into(), status: "Charging".into(),
@@ -1644,6 +1675,7 @@ mod tests {
status_icon(Menu::Battery, &s).0, status_icon(Menu::Battery, &s).0,
include_bytes!("../assets/icons/status/battery-charging.svg") include_bytes!("../assets/icons/status/battery-charging.svg")
); );
s.notifications = Some(status::Notifications { s.notifications = Some(status::Notifications {
count: 3, count: 3,
dnd: true, dnd: true,
@@ -1656,6 +1688,7 @@ mod tests {
status_label(Menu::Notifications, &s), status_label(Menu::Notifications, &s),
"Notifications: do not disturb" "Notifications: do not disturb"
); );
s.bluetooth = Some(status::Bluetooth { s.bluetooth = Some(status::Bluetooth {
enabled: false, enabled: false,
devices: vec!["Headphones".into()], devices: vec!["Headphones".into()],