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:
1 parent
391f6b4e37
commit
67192c0c04
25 files changed
+596
-410
No files matched your search
+1
-1
@@ -21,7 +21,7 @@ resolver = "3"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT"
|
||||
rust-version = "1.87"
|
||||
rust-version = "1.98"
|
||||
|
||||
[workspace.dependencies]
|
||||
bitflags = "2.9.4"
|
||||
|
||||
@@ -1101,7 +1101,7 @@ fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result<Out
|
||||
enabled: settings.enabled,
|
||||
profile: active_profile.map(|profile| profile.name.clone()),
|
||||
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
|
||||
.modes()
|
||||
.iter()
|
||||
|
||||
@@ -47,6 +47,7 @@ impl Runner {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// TERM/grace/reaping must not pause the compositor's event loop.
|
||||
for mut removed in previous {
|
||||
if let Some(mut child) = removed.child.take() {
|
||||
|
||||
@@ -127,21 +127,21 @@ pub(crate) fn surface_regions(surface: &WlSurface) -> 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;
|
||||
}
|
||||
bytes
|
||||
.chunks_exact(20)
|
||||
.as_chunks::<20>()
|
||||
.0
|
||||
.iter()
|
||||
.map(|tuple| {
|
||||
let values: Vec<_> = tuple
|
||||
.chunks_exact(4)
|
||||
.map(|v| i32::from_ne_bytes(v.try_into().unwrap()))
|
||||
.collect();
|
||||
let values: [i32; 5] =
|
||||
std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index]));
|
||||
(values[2] > 0
|
||||
&& values[3] > 0
|
||||
&& values[4] >= 0
|
||||
&& values.iter().all(|v| v.abs_diff(0) <= 32768))
|
||||
.then(|| [values[0], values[1], values[2], values[3], values[4]])
|
||||
.then_some(values)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
fn buffer_destroyed(&mut self, _buffer: &wl_buffer::WlBuffer) {}
|
||||
}
|
||||
@@ -148,3 +135,16 @@ impl ShmHandler for Ferese {
|
||||
&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));
|
||||
}
|
||||
}
|
||||
@@ -107,11 +107,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
let reload_handle = event_loop.handle();
|
||||
let result = event_loop.run(None, &mut state, |state| {
|
||||
if let Some(monitor) = &mut monitor {
|
||||
if let Some(result) = monitor.poll(std::time::Instant::now()) {
|
||||
if let Err(error) = result.and_then(|source| state.reload_config_source(source)) {
|
||||
if let Some(result) = monitor.poll(std::time::Instant::now())
|
||||
&& let Err(error) = result.and_then(|source| state.reload_config_source(source))
|
||||
{
|
||||
warn!(%error, "config reload rejected; retaining last working config");
|
||||
}
|
||||
}
|
||||
// At most one timer exists. Edits arriving during debounce move the
|
||||
// deadline; the old timer wakes once and is replaced if necessary.
|
||||
if !reload_timer_pending.get()
|
||||
|
||||
@@ -148,7 +148,7 @@ mod tests {
|
||||
Duration::from_millis(2),
|
||||
&[Rectangle::new((0, 0).into(), (100, 50).into())],
|
||||
0,
|
||||
FrameEffectMetrics::default(),
|
||||
FrameEffectMetrics,
|
||||
);
|
||||
|
||||
assert_eq!(metrics.rendered_frames, 0);
|
||||
|
||||
@@ -92,6 +92,14 @@ pub(crate) fn init_font_loader(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
type OverviewStateLabel = HashMap<
|
||||
(String, u64, [u32; 4]),
|
||||
(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
),
|
||||
>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct OverviewState {
|
||||
active: bool,
|
||||
@@ -103,13 +111,7 @@ pub(crate) struct OverviewState {
|
||||
font_family: String,
|
||||
font_requests: Option<FontRequests>,
|
||||
font: Option<FontArc>,
|
||||
labels: HashMap<
|
||||
(String, u64, [u32; 4]),
|
||||
(
|
||||
MemoryRenderBuffer,
|
||||
smithay::utils::Size<i32, smithay::utils::Buffer>,
|
||||
),
|
||||
>,
|
||||
labels: OverviewStateLabel,
|
||||
}
|
||||
|
||||
impl Default for OverviewState {
|
||||
@@ -480,6 +482,7 @@ impl Ferese {
|
||||
let selected = self
|
||||
.window_under_visual(point)
|
||||
.and_then(|w| self.window_ids.get(&w).copied());
|
||||
|
||||
if selected.is_some() && self.overview.selected != selected {
|
||||
self.overview.selected = selected;
|
||||
crate::backends::direct::render_all(self);
|
||||
@@ -655,8 +658,7 @@ impl Ferese {
|
||||
let card_width =
|
||||
((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64)
|
||||
/ capacity as f64)
|
||||
.min(160.0)
|
||||
.max(1.0);
|
||||
.clamp(1.0, 160.0);
|
||||
let visible_count = workspaces.len().saturating_sub(offset).min(capacity);
|
||||
let start_x = centered_row_start(strip, visible_count, card_width);
|
||||
workspaces
|
||||
|
||||
@@ -74,16 +74,14 @@ impl Ferese {
|
||||
.space
|
||||
.outputs()
|
||||
.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() {
|
||||
if confirmation.ext_session_lock().is_alive() {
|
||||
confirmation.lock();
|
||||
self.session_lock.confirmed = true;
|
||||
tracing::info!("session lock confirmed after safe output frames");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn configure_lock_surfaces(&mut self) {
|
||||
for (output, surface) in &self.session_lock.surfaces {
|
||||
@@ -163,8 +161,9 @@ impl SessionLockHandler for Ferese {
|
||||
}
|
||||
|
||||
fn new_surface(&mut self, surface: LockSurface, output: WlOutput) {
|
||||
if let Some(output) = Output::from_resource(&output) {
|
||||
if let Some(geometry) = self.space.output_geometry(&output) {
|
||||
if let Some(output) = Output::from_resource(&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())
|
||||
});
|
||||
@@ -172,7 +171,6 @@ impl SessionLockHandler for Ferese {
|
||||
self.focus_lock_surface();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Smithay delegates unlock even after posting InvalidUnlock. Check ownership
|
||||
|
||||
+281
-208
@@ -24,7 +24,7 @@ use smithay::{
|
||||
},
|
||||
desktop::{
|
||||
LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output,
|
||||
utils::send_frames_surface_tree,
|
||||
space::SpaceElement, utils::send_frames_surface_tree,
|
||||
},
|
||||
input::{
|
||||
Seat, SeatState,
|
||||
@@ -241,17 +241,205 @@ pub struct RuntimeConfig {
|
||||
}
|
||||
|
||||
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> {
|
||||
if self.output_profiles != 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
|
||||
|| self.input_settings.xkb_variant != config.input_settings.xkb_variant
|
||||
|| self.input_settings.xkb_options != config.input_settings.xkb_options;
|
||||
|
||||
if let Some(keyboard) = self.seat.get_keyboard() {
|
||||
if keyboard_changed {
|
||||
let options = (!config.input_settings.xkb_options.is_empty())
|
||||
.then(|| config.input_settings.xkb_options.join(","));
|
||||
|
||||
keyboard
|
||||
.set_xkb_config(
|
||||
self,
|
||||
@@ -269,7 +457,9 @@ impl Ferese {
|
||||
config.input_settings.repeat_delay_ms,
|
||||
);
|
||||
}
|
||||
|
||||
self.advance_animations(Instant::now());
|
||||
|
||||
let touchpad_changed = self.input_settings.touchpad != config.input_settings.touchpad;
|
||||
let outputs_changed = self.output_profiles != config.output_profiles;
|
||||
let bounds = self
|
||||
@@ -287,6 +477,7 @@ impl Ferese {
|
||||
(workspace.id, rect)
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.workspaces
|
||||
.reconfigure_live(
|
||||
config.layout_mode,
|
||||
@@ -307,20 +498,25 @@ impl Ferese {
|
||||
self.viewport_spring_config = config.viewport_spring_config;
|
||||
self.output_profiles = config.output_profiles;
|
||||
self.autostart = config.autostart;
|
||||
|
||||
if touchpad_changed {
|
||||
crate::backends::direct::reload_input_devices(self);
|
||||
}
|
||||
|
||||
if outputs_changed {
|
||||
crate::backends::direct::reload_outputs(self);
|
||||
}
|
||||
|
||||
if old_rules != self.window_rules {
|
||||
self.reapply_window_rules(&old_rules);
|
||||
}
|
||||
|
||||
self.overview.set_font_family(config.overview_font_family);
|
||||
self.wallpaper.reload(config.wallpaper);
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
self.relayout();
|
||||
crate::backends::direct::render_all(self);
|
||||
|
||||
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(
|
||||
display: Display<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> {
|
||||
let overview_active = self.overview.is_active();
|
||||
let workspace = self.workspace_under_pointer(position)?;
|
||||
let mut candidates = if self.overview.is_active() {
|
||||
self.window_ids.keys().cloned().collect::<Vec<_>>()
|
||||
} 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| {
|
||||
|
||||
let hit = |window: &Window| -> Option<Window> {
|
||||
if !self.window_content_ready(window) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let id = self.window_ids.get(window)?;
|
||||
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
|
||||
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.y >= visual.y
|
||||
&& position.x < visual.x + visual.width
|
||||
&& 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> {
|
||||
@@ -909,6 +923,9 @@ impl Ferese {
|
||||
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 {
|
||||
self.space.raise_element(&window, false);
|
||||
}
|
||||
@@ -1571,8 +1588,8 @@ impl Ferese {
|
||||
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
|
||||
});
|
||||
|
||||
if natural_pending || requested_size.is_some() || state_changed {
|
||||
if let Some(serial) = toplevel.send_pending_configure()
|
||||
if (natural_pending || requested_size.is_some() || state_changed)
|
||||
&& let Some(serial) = toplevel.send_pending_configure()
|
||||
&& requested_size.is_some()
|
||||
&& had_geometry
|
||||
&& self.animations_enabled
|
||||
@@ -1585,7 +1602,6 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.scrolling_world_x = scrolling_world_x;
|
||||
if layout_changed {
|
||||
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
|
||||
@@ -2341,8 +2357,9 @@ impl Ferese {
|
||||
}
|
||||
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
|
||||
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
|
||||
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id) {
|
||||
if let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout {
|
||||
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
|
||||
&& let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout
|
||||
{
|
||||
if enabled {
|
||||
if let Some(column) = layout
|
||||
.columns()
|
||||
@@ -2359,7 +2376,6 @@ impl Ferese {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
self.relayout();
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
pub struct ClientState {
|
||||
pub compositor_state: CompositorClientState,
|
||||
@@ -3016,4 +3044,49 @@ mod tests {
|
||||
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(), ¤t, |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(),
|
||||
¤t,
|
||||
|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
|
||||
}));
|
||||
}
|
||||
}
|
||||
@@ -1017,7 +1017,7 @@ pub(crate) fn frame_effect_metrics(
|
||||
_elements: &[AnimatedWindowRenderElement],
|
||||
_scale: f64,
|
||||
) -> FrameEffectMetrics {
|
||||
FrameEffectMetrics::default()
|
||||
FrameEffectMetrics
|
||||
}
|
||||
|
||||
fn output_elements(
|
||||
@@ -1220,8 +1220,8 @@ fn output_elements(
|
||||
output,
|
||||
&programs,
|
||||
);
|
||||
if !scale_content {
|
||||
if let Some(snapshot) = state.resize_snapshots.get(&id)
|
||||
if !scale_content
|
||||
&& let Some(snapshot) = state.resize_snapshots.get(&id)
|
||||
&& snapshot.context == renderer.context_id().erased()
|
||||
&& (snapshot.scale - scale).abs() < 0.001
|
||||
{
|
||||
@@ -1260,7 +1260,6 @@ fn output_elements(
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
elements.extend(rounded_window_elements(
|
||||
renderer,
|
||||
&window,
|
||||
|
||||
@@ -39,6 +39,7 @@ pub fn default_wallpaper() -> &'static str {
|
||||
.unwrap_or_else(|| {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")
|
||||
});
|
||||
|
||||
path.canonicalize()
|
||||
.unwrap_or(path)
|
||||
.to_string_lossy()
|
||||
@@ -50,6 +51,7 @@ fn field(name: &str, parent: &str) -> String {
|
||||
if parent == "commands" {
|
||||
return name.to_owned();
|
||||
}
|
||||
|
||||
match name {
|
||||
"binding" => "bindings".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 {
|
||||
match (parent, key) {
|
||||
("", "bindings" | "window_rules" | "output_profiles" | "autostart") => true,
|
||||
("output_profiles", "outputs") | ("desktop_widgets", "notes") => true,
|
||||
_ => false,
|
||||
}
|
||||
matches!(
|
||||
(parent, key),
|
||||
(
|
||||
"",
|
||||
"bindings" | "window_rules" | "output_profiles" | "autostart"
|
||||
) | ("output_profiles", "outputs")
|
||||
| ("desktop_widgets", "notes")
|
||||
)
|
||||
}
|
||||
|
||||
fn is_array(key: &str, parent: &str) -> bool {
|
||||
@@ -365,13 +370,13 @@ fn restore_node_formats(doc: &mut KdlDocument, formats: &mut impl Iterator<Item
|
||||
entry.set_format(format);
|
||||
}
|
||||
}
|
||||
if let Some(terminator) = terminator {
|
||||
if let Some(format) = node.format_mut() {
|
||||
if let Some(terminator) = terminator
|
||||
&& let Some(format) = node.format_mut()
|
||||
{
|
||||
format.before_terminator = " ".into();
|
||||
format.terminator = terminator;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(children) = node.children_mut() {
|
||||
restore_node_formats(children, formats);
|
||||
}
|
||||
@@ -519,8 +524,8 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) ->
|
||||
}
|
||||
let node = &mut doc.nodes_mut()[i];
|
||||
|
||||
if parts.len() == 2 {
|
||||
if let Some(entry) = node
|
||||
if parts.len() == 2
|
||||
&& let Some(entry) = node
|
||||
.entries_mut()
|
||||
.iter_mut()
|
||||
.find(|e| e.name().is_some_and(|n| field(n.value(), key) == parts[1]))
|
||||
@@ -528,7 +533,6 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) ->
|
||||
set_entry_value(entry, kdl_value(&value)?);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
set_in(
|
||||
node.children_mut().get_or_insert_with(KdlDocument::new),
|
||||
|
||||
@@ -456,7 +456,10 @@ impl ScrollingLayout {
|
||||
if let Some(focused) = focused {
|
||||
self.focus(focused)?;
|
||||
}
|
||||
self.validate()?;
|
||||
|
||||
self.validate_scalars()?;
|
||||
debug_assert!(self.validate().is_ok());
|
||||
|
||||
if self.columns.is_empty() {
|
||||
self.viewport_x = 0.0;
|
||||
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() {
|
||||
return Err(LayoutError::InvalidTree(
|
||||
"scrolling viewport offset is invalid",
|
||||
@@ -701,6 +704,11 @@ impl ScrollingLayout {
|
||||
"scrolling active column is invalid",
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn validate(&self) -> Result<(), LayoutError> {
|
||||
self.validate_scalars()?;
|
||||
|
||||
let mut windows = HashSet::new();
|
||||
for column in &self.columns {
|
||||
|
||||
@@ -109,12 +109,14 @@ impl Appearance {
|
||||
} else {
|
||||
ThemeBuilder::dark()
|
||||
};
|
||||
let mut corners = cosmic::cosmic_theme::CornerRadii::default();
|
||||
corners.radius_xs = [self.radius.min(4.); 4];
|
||||
corners.radius_s = [self.radius.min(8.); 4];
|
||||
corners.radius_m = [self.radius; 4];
|
||||
corners.radius_l = [self.radius; 4];
|
||||
corners.radius_xl = [self.radius; 4];
|
||||
let corners = cosmic::cosmic_theme::CornerRadii {
|
||||
radius_0: Default::default(),
|
||||
radius_xs: [self.radius.min(4.); 4],
|
||||
radius_s: [self.radius.min(8.); 4],
|
||||
radius_m: [self.radius; 4],
|
||||
radius_l: [self.radius; 4],
|
||||
radius_xl: [self.radius; 4],
|
||||
};
|
||||
let mut native = builder
|
||||
.corner_radii(corners)
|
||||
.bg_color(color(self.panel))
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#![feature(box_patterns)]
|
||||
|
||||
mod appearance;
|
||||
mod auth;
|
||||
mod runtime;
|
||||
@@ -36,19 +38,23 @@ fn main() -> iced::Result {
|
||||
} else {
|
||||
ferese_config::config_path()
|
||||
};
|
||||
|
||||
if !args.is_empty() && !preview && args != ["--foreground"] {
|
||||
eprintln!("Usage: ferese-lock [--preview | --foreground]");
|
||||
std::process::exit(2);
|
||||
}
|
||||
|
||||
if !preview && !auth::available() {
|
||||
eprintln!("ferese-lock: install the ferese-lock PAM policy before locking");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if !preview {
|
||||
if let Err(error) = runtime::check_protocol() {
|
||||
eprintln!("ferese-lock: {error}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if args.is_empty() {
|
||||
if let Err(error) = runtime::daemonize() {
|
||||
eprintln!("ferese-lock: {error}");
|
||||
@@ -57,14 +63,17 @@ fn main() -> iced::Result {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
let user = auth::username().unwrap_or_else(|error| {
|
||||
eprintln!("ferese-lock: {error}");
|
||||
std::process::exit(1)
|
||||
});
|
||||
|
||||
// Password-bearing UI processes must not create core dumps.
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
}
|
||||
|
||||
let appearance = appearance::Appearance::load(&user, config.as_deref());
|
||||
cosmic::app::run::<Locker>(
|
||||
Settings::default()
|
||||
@@ -81,18 +90,20 @@ fn main() -> iced::Result {
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Message {
|
||||
Event(Event),
|
||||
Event(Box<Event>),
|
||||
Input(String),
|
||||
Submit,
|
||||
Authenticated(bool),
|
||||
Tick,
|
||||
}
|
||||
|
||||
// Never derive Debug for password-bearing messages.
|
||||
impl std::fmt::Debug for Message {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("LockerMessage")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, PartialEq)]
|
||||
enum AuthState {
|
||||
#[default]
|
||||
@@ -101,10 +112,12 @@ enum AuthState {
|
||||
Rejected(Instant),
|
||||
Unlocking,
|
||||
}
|
||||
|
||||
impl AuthState {
|
||||
fn may_submit(&self) -> bool {
|
||||
matches!(self, Self::Ready)
|
||||
}
|
||||
|
||||
fn complete(&mut self, success: bool) -> bool {
|
||||
if *self != Self::Checking {
|
||||
return false;
|
||||
@@ -117,6 +130,7 @@ impl AuthState {
|
||||
success
|
||||
}
|
||||
}
|
||||
|
||||
struct Locker {
|
||||
core: Core,
|
||||
preview: bool,
|
||||
@@ -132,17 +146,22 @@ struct Locker {
|
||||
confirmation: runtime::Confirmation,
|
||||
caps_lock: bool,
|
||||
}
|
||||
|
||||
impl cosmic::Application for Locker {
|
||||
type Executor = cosmic::executor::Default;
|
||||
type Flags = (bool, String, appearance::Appearance);
|
||||
type Message = Message;
|
||||
|
||||
const APP_ID: &'static str = "dev.ferese.Lock";
|
||||
|
||||
fn core(&self) -> &Core {
|
||||
&self.core
|
||||
}
|
||||
|
||||
fn core_mut(&mut self) -> &mut Core {
|
||||
&mut self.core
|
||||
}
|
||||
|
||||
fn init(mut core: Core, (preview, user, appearance): Self::Flags) -> (Self, Task<Message>) {
|
||||
core.window.show_headerbar = false;
|
||||
let now = jiff::Zoned::now();
|
||||
@@ -174,6 +193,7 @@ impl cosmic::Application for Locker {
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn subscription(&self) -> Subscription<Message> {
|
||||
Subscription::batch([
|
||||
event::listen_with(|event, _, _| match event {
|
||||
@@ -182,22 +202,26 @@ impl cosmic::Application for Locker {
|
||||
))
|
||||
| Event::Window(window::Event::Opened { .. })
|
||||
| Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => {
|
||||
Some(Message::Event(event))
|
||||
Some(Message::Event(Box::new(event)))
|
||||
}
|
||||
_ => None,
|
||||
}),
|
||||
iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick),
|
||||
])
|
||||
}
|
||||
|
||||
fn update(&mut self, message: Message) -> Task<Message> {
|
||||
match message {
|
||||
Message::Event(Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers))) => {
|
||||
Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(
|
||||
modifiers,
|
||||
))) => {
|
||||
self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK);
|
||||
}
|
||||
Message::Tick => {
|
||||
let now = jiff::Zoned::now();
|
||||
self.clock = now.strftime(self.appearance.clock_format()).to_string();
|
||||
self.date = now.strftime("%A, %B %-d").to_string();
|
||||
|
||||
if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2))
|
||||
{
|
||||
self.state = AuthState::Ready;
|
||||
@@ -217,12 +241,14 @@ impl cosmic::Application for Locker {
|
||||
self.status = "Preview only · your session is not locked";
|
||||
return focus();
|
||||
}
|
||||
|
||||
if !self.confirmation.confirmed()
|
||||
|| !self.state.may_submit()
|
||||
|| self.password.is_empty()
|
||||
{
|
||||
return Task::none();
|
||||
}
|
||||
|
||||
self.state = AuthState::Checking;
|
||||
self.status = "Checking password…";
|
||||
let password = Zeroizing::new(std::mem::take(&mut *self.password));
|
||||
@@ -243,7 +269,7 @@ impl cosmic::Application for Locker {
|
||||
self.status = "Password not accepted. Try again.";
|
||||
return focus();
|
||||
}
|
||||
Message::Event(Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::Output(event, output),
|
||||
))) if !self.preview => {
|
||||
if matches!(event, wayland::OutputEvent::Removed) {
|
||||
@@ -258,7 +284,7 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
}
|
||||
}
|
||||
Message::Event(Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(
|
||||
wayland::Event::SessionLock(event),
|
||||
))) if !self.preview => match event {
|
||||
wayland::SessionLockEvent::Locked => {
|
||||
@@ -285,25 +311,32 @@ impl cosmic::Application for Locker {
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Message::Event(Event::Window(window::Event::Opened { .. })) => return focus(),
|
||||
Message::Event(box Event::Window(window::Event::Opened { .. })) => {
|
||||
return focus();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> Task<Message> {
|
||||
self.password.zeroize();
|
||||
focus()
|
||||
}
|
||||
|
||||
fn view(&self) -> Element<'_, Message> {
|
||||
self.screen()
|
||||
}
|
||||
|
||||
fn view_window(&self, _: window::Id) -> Element<'_, Message> {
|
||||
self.screen()
|
||||
}
|
||||
}
|
||||
|
||||
fn focus() -> Task<Message> {
|
||||
widget::text_input::focus(widget::Id::new("password"))
|
||||
}
|
||||
|
||||
impl Locker {
|
||||
fn label<'a>(
|
||||
&self,
|
||||
@@ -312,9 +345,11 @@ impl Locker {
|
||||
) -> widget::Text<'a, cosmic::Theme, cosmic::Renderer> {
|
||||
widget::text(label).font(self.appearance.font).size(size)
|
||||
}
|
||||
|
||||
fn screen(&self) -> Element<'_, Message> {
|
||||
iced::widget::responsive(move |size| self.screen_content(size)).into()
|
||||
}
|
||||
|
||||
fn screen_content(&self, size: iced::Size) -> Element<'_, Message> {
|
||||
let avatar_size = (size.height * 0.09).clamp(64., 112.);
|
||||
let clock_size = (size.height * 0.165)
|
||||
@@ -336,12 +371,14 @@ impl Locker {
|
||||
.style(password_style(a.radius, a.accent, a.panel))
|
||||
.font(a.font)
|
||||
.id(widget::Id::new("password"));
|
||||
|
||||
if self.state.may_submit() {
|
||||
input = input.on_input(Message::Input).on_submit(|mut value| {
|
||||
value.zeroize();
|
||||
Message::Submit
|
||||
});
|
||||
}
|
||||
|
||||
let submit = widget::button::custom(widget::icon::from_name("go-next-symbolic").size(20))
|
||||
.class(theme::Button::Icon)
|
||||
.padding(6)
|
||||
@@ -401,6 +438,7 @@ impl Locker {
|
||||
.min((size.width - margin * 2.).max(160.)),
|
||||
);
|
||||
let mut clock = column([]).spacing(0).align_x(iced::Alignment::Center);
|
||||
|
||||
if a.show_clock {
|
||||
clock = clock.push(
|
||||
self.label(&self.clock, clock_size)
|
||||
@@ -411,12 +449,14 @@ impl Locker {
|
||||
.class(iced::Color::WHITE),
|
||||
);
|
||||
}
|
||||
|
||||
if a.show_date {
|
||||
clock = clock.push(
|
||||
self.label(&self.date, 18)
|
||||
.class(iced::Color::WHITE.scale_alpha(0.72)),
|
||||
);
|
||||
}
|
||||
|
||||
let clock_height = if a.show_clock {
|
||||
clock_size as f32 * 1.3
|
||||
} else {
|
||||
@@ -472,26 +512,6 @@ impl Locker {
|
||||
iced::widget::stack![background, foreground].into()
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn only_an_active_successful_attempt_unlocks() {
|
||||
let mut state = AuthState::Ready;
|
||||
assert!(!state.complete(true));
|
||||
state = AuthState::Checking;
|
||||
assert!(!state.complete(false));
|
||||
assert!(!state.may_submit());
|
||||
assert!(!state.complete(true));
|
||||
state = AuthState::Checking;
|
||||
assert!(state.complete(true));
|
||||
assert!(!state.complete(true));
|
||||
}
|
||||
#[test]
|
||||
fn password_messages_are_redacted() {
|
||||
assert!(!format!("{:?}", Message::Input("secret".into())).contains("secret"));
|
||||
}
|
||||
}
|
||||
|
||||
fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
|
||||
let svg = format!(
|
||||
@@ -499,6 +519,7 @@ fn line_icon(path: &str, size: u16) -> widget::icon::Icon {
|
||||
);
|
||||
widget::icon(widget::icon::from_svg_bytes(svg.into_bytes()).symbolic(true)).size(size)
|
||||
}
|
||||
|
||||
fn ferese_symbol() -> widget::icon::Icon {
|
||||
widget::icon(
|
||||
widget::icon::from_svg_bytes(
|
||||
@@ -533,6 +554,7 @@ fn password_style(radius: f32, accent: iced::Color, surface: iced::Color) -> the
|
||||
selected_fill: accent.scale_alpha(0.5),
|
||||
label_color: iced::Color::WHITE,
|
||||
};
|
||||
|
||||
theme::TextInput::Custom {
|
||||
active: Box::new(move |_| appearance(false)),
|
||||
hovered: Box::new(move |_| appearance(true)),
|
||||
@@ -541,3 +563,24 @@ fn password_style(radius: f32, accent: iced::Color, surface: iced::Color) -> the
|
||||
disabled: Box::new(move |_| appearance(false)),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn only_an_active_successful_attempt_unlocks() {
|
||||
let mut state = AuthState::Ready;
|
||||
assert!(!state.complete(true));
|
||||
state = AuthState::Checking;
|
||||
assert!(!state.complete(false));
|
||||
assert!(!state.may_submit());
|
||||
assert!(!state.complete(true));
|
||||
state = AuthState::Checking;
|
||||
assert!(state.complete(true));
|
||||
assert!(!state.complete(true));
|
||||
}
|
||||
#[test]
|
||||
fn password_messages_are_redacted() {
|
||||
assert!(!format!("{:?}", Message::Input("secret".into())).contains("secret"));
|
||||
}
|
||||
}
|
||||
@@ -82,13 +82,14 @@ impl Appearance {
|
||||
} else {
|
||||
ThemeBuilder::dark()
|
||||
};
|
||||
|
||||
let mut corners = cosmic::cosmic_theme::CornerRadii::default();
|
||||
corners.radius_xs = [self.radius.min(4.); 4];
|
||||
corners.radius_s = [self.radius.min(8.); 4];
|
||||
corners.radius_m = [self.radius; 4];
|
||||
corners.radius_l = [self.radius; 4];
|
||||
corners.radius_xl = [self.radius; 4];
|
||||
let corners = cosmic::cosmic_theme::CornerRadii {
|
||||
radius_xs: [self.radius.min(4.); 4],
|
||||
radius_s: [self.radius.min(8.); 4],
|
||||
radius_m: [self.radius; 4],
|
||||
radius_l: [self.radius; 4],
|
||||
radius_xl: [self.radius; 4],
|
||||
radius_0: Default::default(),
|
||||
};
|
||||
|
||||
let mut native = builder
|
||||
.corner_radii(corners)
|
||||
|
||||
@@ -53,6 +53,7 @@ impl Api {
|
||||
let cancel = *agent
|
||||
.get(b"polkit_agent_session_cancel\0")
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
Ok(Self {
|
||||
_agent: agent,
|
||||
_polkit: polkit,
|
||||
@@ -199,7 +200,10 @@ fn run_session(
|
||||
connect(
|
||||
session,
|
||||
c"request",
|
||||
Some(std::mem::transmute(
|
||||
Some(std::mem::transmute::<
|
||||
unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
request as unsafe extern "C" fn(Object, *const c_char, c_int, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
@@ -207,7 +211,10 @@ fn run_session(
|
||||
connect(
|
||||
session,
|
||||
c"show-info",
|
||||
Some(std::mem::transmute(
|
||||
Some(std::mem::transmute::<
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
info as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
@@ -215,7 +222,10 @@ fn run_session(
|
||||
connect(
|
||||
session,
|
||||
c"show-error",
|
||||
Some(std::mem::transmute(
|
||||
Some(std::mem::transmute::<
|
||||
unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
error as unsafe extern "C" fn(Object, *const c_char, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
@@ -223,11 +233,15 @@ fn run_session(
|
||||
connect(
|
||||
session,
|
||||
c"completed",
|
||||
Some(std::mem::transmute(
|
||||
Some(std::mem::transmute::<
|
||||
unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
unsafe extern "C" fn(),
|
||||
>(
|
||||
completed as unsafe extern "C" fn(Object, c_int, *mut c_void),
|
||||
)),
|
||||
signal_data,
|
||||
);
|
||||
|
||||
(api.initiate)(session);
|
||||
}
|
||||
|
||||
|
||||
@@ -274,11 +274,9 @@ impl cosmic::Application for App {
|
||||
|result| cosmic::Action::App(Message::LockPreviewStarted(result)),
|
||||
);
|
||||
}
|
||||
Message::LockPreviewStarted(result) => {
|
||||
if let Err(error) = result {
|
||||
Message::LockPreviewStarted(Err(error)) => {
|
||||
self.error = Some(error);
|
||||
}
|
||||
}
|
||||
Message::Page(page) => {
|
||||
self.page = page;
|
||||
self.search.clear();
|
||||
@@ -1286,8 +1284,8 @@ impl App {
|
||||
// When a gradient exists, changing Accent also changes its
|
||||
// leading stop; otherwise the visible focus border would stay
|
||||
// on the old palette despite the control saying it changed.
|
||||
if let Edit::Set(path, value) = &edit {
|
||||
if path == "theme.colors.accent"
|
||||
if let Edit::Set(path, value) = &edit
|
||||
&& path == "theme.colors.accent"
|
||||
&& self.draft.item("theme.focus_ring.gradient").is_some()
|
||||
{
|
||||
let gradient = set("theme.focus_ring.gradient.from", value.clone());
|
||||
@@ -1295,7 +1293,7 @@ impl App {
|
||||
self.pending.push(gradient);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.pending.push(edit);
|
||||
self.error = None;
|
||||
self.flush()
|
||||
|
||||
@@ -451,7 +451,12 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> {
|
||||
let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65;
|
||||
let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65;
|
||||
let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" {
|
||||
snapshot.number("theme.material.opacity", ferese_config::DEFAULT_MATERIAL_OPACITY).clamp(0., 1.)
|
||||
snapshot
|
||||
.number(
|
||||
"theme.material.opacity",
|
||||
ferese_config::DEFAULT_MATERIAL_OPACITY,
|
||||
)
|
||||
.clamp(0., 1.)
|
||||
} else {
|
||||
1.
|
||||
};
|
||||
@@ -556,11 +561,12 @@ mod tests {
|
||||
fn default_uses_logo_blue_and_native_controls_follow_lightness() {
|
||||
let logo = include_str!("../../../packaging/icons/ferese.svg").to_ascii_uppercase();
|
||||
assert!(logo.contains(PRESETS[0].accent));
|
||||
for index in 0..PRESETS.len() {
|
||||
|
||||
for (index, item) in PRESETS.iter().enumerate() {
|
||||
let snapshot = configured_preset(index);
|
||||
assert_eq!(
|
||||
native_theme(Some(&snapshot)).cosmic().is_dark,
|
||||
PRESETS[index].name != "Ayu Light"
|
||||
item.name != "Ayu Light"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,14 +8,6 @@ mod recording;
|
||||
mod status;
|
||||
mod status_ui;
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Older libcosmic backends expose native surfaces in frame events. Keep
|
||||
// that compatibility path gated; current backends use Window::Opened and
|
||||
// window::run to retrieve handles without updates at the display refresh rate.
|
||||
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
use cosmic::Element;
|
||||
use cosmic::app::{Core, Settings, Task};
|
||||
use cosmic::iced::alignment;
|
||||
@@ -37,6 +29,8 @@ use ferese_protocols::effects::v1::client::{
|
||||
ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1},
|
||||
};
|
||||
use jiff::Zoned;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
use std::time::{Duration, Instant};
|
||||
use wayland_client::{
|
||||
Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop,
|
||||
globals::{GlobalListContents, registry_queue_init},
|
||||
@@ -48,6 +42,10 @@ use crate::control::{ShellControl, ShellSnapshot};
|
||||
|
||||
const APP_ID: &str = "dev.ferese.Shell";
|
||||
|
||||
// Older libcosmic backends expose native surfaces in frame events. Keep
|
||||
// that compatibility path gated; current backends use Window::Opened and
|
||||
// window::run to retrieve handles without updates at the display refresh rate.
|
||||
static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true);
|
||||
static SHELL_FONT: std::sync::OnceLock<std::sync::RwLock<cosmic::font::Font>> =
|
||||
std::sync::OnceLock::new();
|
||||
|
||||
@@ -117,11 +115,12 @@ fn stacked_clock_digits(label: &str) -> Option<[String; 2]> {
|
||||
let mut parts = label.split_whitespace();
|
||||
let time = parts.next()?;
|
||||
|
||||
if let Some(period) = parts.next() {
|
||||
if !period.eq_ignore_ascii_case("am") && !period.eq_ignore_ascii_case("pm") {
|
||||
if let Some(period) = parts.next()
|
||||
&& !period.eq_ignore_ascii_case("am")
|
||||
&& !period.eq_ignore_ascii_case("pm")
|
||||
{
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
if parts.next().is_some() {
|
||||
return None;
|
||||
@@ -650,8 +649,7 @@ impl cosmic::Application for FereseShell {
|
||||
let open = self.open_menu(kind, anchor);
|
||||
Task::batch([destroy, open])
|
||||
} else {
|
||||
let open = self.open_menu(kind, anchor);
|
||||
open
|
||||
self.open_menu(kind, anchor)
|
||||
}
|
||||
}
|
||||
Message::ConfirmPower(action) => {
|
||||
@@ -680,11 +678,12 @@ impl cosmic::Application for FereseShell {
|
||||
.status_service
|
||||
.send(status::Action::PowerProfile(profile))
|
||||
.err();
|
||||
if self.status_error.is_none() {
|
||||
if let Some(profiles) = &mut self.status.power_profiles {
|
||||
|
||||
if self.status_error.is_none()
|
||||
&& let Some(profiles) = &mut self.status.power_profiles
|
||||
{
|
||||
profiles.active = profile.to_owned();
|
||||
}
|
||||
}
|
||||
return Task::none();
|
||||
}
|
||||
if self.notifications.ready {
|
||||
@@ -706,22 +705,27 @@ impl cosmic::Application for FereseShell {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
self.status_error = self.status_service.send(action.clone()).err();
|
||||
|
||||
if self.status_error.is_none() {
|
||||
self.optimistic_status(&action);
|
||||
}
|
||||
|
||||
if matches!(
|
||||
action,
|
||||
status::Action::Notifications | status::Action::Settings
|
||||
) {
|
||||
return self.close_menu();
|
||||
}
|
||||
|
||||
Task::none()
|
||||
}
|
||||
Message::Event(event, id) => self.handle_event(event, id),
|
||||
Message::Tick => {
|
||||
self.recorder.poll();
|
||||
self.clock = current_time();
|
||||
|
||||
if self.config.desktop_widgets.clock.enabled {
|
||||
self.desktop_clock = self
|
||||
.config
|
||||
@@ -730,6 +734,7 @@ impl cosmic::Application for FereseShell {
|
||||
.labels(&Zoned::now())
|
||||
.unwrap_or_default();
|
||||
}
|
||||
|
||||
let mut reload_task = Task::none();
|
||||
if let Some(control) = &self.control {
|
||||
let poll = control.poll();
|
||||
@@ -737,15 +742,19 @@ impl cosmic::Application for FereseShell {
|
||||
if poll.disconnected {
|
||||
return cosmic::iced::exit();
|
||||
}
|
||||
|
||||
if let Some(active) = poll.overview_active {
|
||||
self.overview_active = active;
|
||||
}
|
||||
|
||||
if let Some(source) = poll.config {
|
||||
reload_task = self.reload_config(source);
|
||||
}
|
||||
|
||||
if let Some(snapshot) = poll.snapshot {
|
||||
self.snapshot = snapshot;
|
||||
let mut tasks = vec![reload_task];
|
||||
|
||||
for entry in &mut self.outputs {
|
||||
let output = self
|
||||
.snapshot
|
||||
@@ -754,16 +763,16 @@ impl cosmic::Application for FereseShell {
|
||||
.find(|output| Some(output.name.as_str()) == entry.name.as_deref())
|
||||
.map(|output| output.id);
|
||||
let hidden = output_bar_hidden(&self.snapshot, output);
|
||||
|
||||
if entry.hidden == hidden {
|
||||
continue;
|
||||
}
|
||||
|
||||
entry.hidden = hidden;
|
||||
if let Some(effects) = &entry.effects {
|
||||
if let Err(error) = effects.set_visible(!hidden) {
|
||||
eprintln!(
|
||||
"ferese-shell: could not update panel material: {error}"
|
||||
);
|
||||
}
|
||||
if let Some(effects) = &entry.effects
|
||||
&& let Err(error) = effects.set_visible(!hidden)
|
||||
{
|
||||
eprintln!("ferese-shell: could not update panel material: {error}");
|
||||
}
|
||||
tasks.push(set_input_zone(
|
||||
entry.bar,
|
||||
@@ -1058,7 +1067,6 @@ impl FereseShell {
|
||||
// accounts for the top margin separately.
|
||||
exclusive_zone: (bar.height.round() as i32)
|
||||
.saturating_add(shell_theme.bar_window_gap),
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(move |app| app.view_layer(bar_surface_id))),
|
||||
);
|
||||
@@ -1113,13 +1121,12 @@ impl FereseShell {
|
||||
.notes
|
||||
.iter_mut()
|
||||
.find(|note| note.id == id)
|
||||
&& (note.text != value || self.note_error.is_some())
|
||||
{
|
||||
if note.text != value || self.note_error.is_some() {
|
||||
note.text = value.clone();
|
||||
self.note_pending
|
||||
.push(note_store::Edit::Text(id.to_owned(), value));
|
||||
}
|
||||
}
|
||||
self.flush_note_changes()
|
||||
}
|
||||
|
||||
@@ -1227,7 +1234,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(dimensions.0), Some(dimensions.1))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
},
|
||||
Some(Box::new(Self::view_note_drag)),
|
||||
);
|
||||
@@ -1356,7 +1362,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(note.width), Some(note.height))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
}
|
||||
},
|
||||
Some(Box::new(move |app| app.view_note(¬e_id, id))),
|
||||
@@ -1581,7 +1586,6 @@ impl FereseShell {
|
||||
exclusive_zone: -1,
|
||||
size: Some((Some(clock.width), Some(clock.height))),
|
||||
size_limits: Limits::NONE,
|
||||
..Default::default()
|
||||
}
|
||||
},
|
||||
Some(Box::new(move |app| app.view_desktop_clock(id))),
|
||||
@@ -1854,15 +1858,14 @@ impl FereseShell {
|
||||
}
|
||||
}
|
||||
|
||||
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left) => {
|
||||
cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left)
|
||||
if self
|
||||
.note_drag
|
||||
.as_ref()
|
||||
.is_some_and(|drag| drag.source == id)
|
||||
.is_some_and(|drag| drag.source == id) =>
|
||||
{
|
||||
return self.finish_note_drag(true);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
@@ -1987,8 +1990,9 @@ impl FereseShell {
|
||||
surface,
|
||||
frame_id,
|
||||
))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => {
|
||||
if let Some(menu) = &mut self.menu {
|
||||
if menu.effects.is_none() {
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.effects.is_none()
|
||||
{
|
||||
match EffectsBinding::attach_role(
|
||||
&surface,
|
||||
menu.kind.material_role(),
|
||||
@@ -2001,7 +2005,6 @@ impl FereseShell {
|
||||
}
|
||||
EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(
|
||||
@@ -2022,6 +2025,7 @@ impl FereseShell {
|
||||
.is_some_and(|entry| entry.id == layer_id) =>
|
||||
{
|
||||
let entry = self.notification_surface.as_mut().unwrap();
|
||||
|
||||
if entry.effects.is_none() {
|
||||
match EffectsBinding::attach_role(&surface, None, 1.0) {
|
||||
Ok(binding) => entry.effects = Some(binding),
|
||||
@@ -2049,6 +2053,7 @@ impl FereseShell {
|
||||
let Some(entry) = self.outputs.iter_mut().find(|entry| entry.bar == id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
if entry.effects.is_some() {
|
||||
return;
|
||||
}
|
||||
@@ -2493,10 +2498,10 @@ fn clamp_note_position(
|
||||
cosmic::iced::Point::new(
|
||||
position
|
||||
.x
|
||||
.clamp(0., ((output.0 as f32 - size.0 as f32).max(0.)).min(8192.)),
|
||||
.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)),
|
||||
position
|
||||
.y
|
||||
.clamp(0., ((output.1 as f32 - size.1 as f32).max(0.)).min(8192.)),
|
||||
.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -520,7 +520,6 @@ impl FereseShell {
|
||||
color: color_with_opacity(palette.text_muted, 0.18 * opacity),
|
||||
width: if history { 0.0 } else { 1.0 },
|
||||
radius: palette.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
@@ -129,7 +129,9 @@ impl Server {
|
||||
title: limited(summary, 160),
|
||||
body: limited(body, 500),
|
||||
actions: actions
|
||||
.chunks_exact(2)
|
||||
.as_chunks::<2>()
|
||||
.0
|
||||
.iter()
|
||||
.take(8)
|
||||
.map(|pair| (limited(&pair[0], 128), limited(&pair[1], 40)))
|
||||
.collect(),
|
||||
|
||||
@@ -80,22 +80,24 @@ impl Recorder {
|
||||
let (send, receive) = mpsc::sync_channel(8);
|
||||
self.updates = Some(receive);
|
||||
self.state = State::Selecting;
|
||||
|
||||
std::thread::spawn(move || {
|
||||
let mut output = BufReader::new(output);
|
||||
|
||||
loop {
|
||||
let mut line = String::new();
|
||||
match output.by_ref().take(65537).read_line(&mut line) {
|
||||
Ok(0) | Err(_) => break,
|
||||
Ok(_) if line.len() > 65536 => break,
|
||||
Ok(_) => {
|
||||
if let Ok(update) = serde_json::from_str::<Update>(&line) {
|
||||
if send.send(Event::Update(update)).is_err() {
|
||||
if let Ok(update) = serde_json::from_str::<Update>(&line)
|
||||
&& send.send(Event::Update(update)).is_err()
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _ = child.wait();
|
||||
let _ = send.send(Event::Exited);
|
||||
|
||||
@@ -158,13 +158,10 @@ impl Service {
|
||||
let mut network = dbus_cache::Cache::new("org.freedesktop.NetworkManager");
|
||||
let mut bluetooth = dbus_cache::Cache::new("org.bluez");
|
||||
|
||||
loop {
|
||||
// A poll overlapping a write is discarded below. Wait for the
|
||||
// write to finish before retrying instead of launching commands
|
||||
// repeatedly while a slow control operation is still running.
|
||||
let Some(before) = wait_for_poll(&polling) else {
|
||||
break;
|
||||
};
|
||||
while let Some(before) = wait_for_poll(&polling) {
|
||||
let mut snapshot = poll(&mut system_bus, &mut session_bus);
|
||||
snapshot.network = network.read(before, network::read);
|
||||
snapshot.bluetooth = bluetooth.read(before, bluetooth::read);
|
||||
@@ -186,16 +183,14 @@ impl Service {
|
||||
});
|
||||
|
||||
thread::spawn(move || {
|
||||
loop {
|
||||
let first = match commands.recv() {
|
||||
Ok(command) => command,
|
||||
Err(_) => break,
|
||||
};
|
||||
while let Ok(first) = commands.recv() {
|
||||
let mut pending = vec![first];
|
||||
|
||||
for command in commands.try_iter() {
|
||||
pending.retain(|(_, action)| action.key() != command.1.key());
|
||||
pending.push(command);
|
||||
}
|
||||
|
||||
for (id, action) in pending {
|
||||
{
|
||||
let mut state = shared.0.lock().unwrap();
|
||||
@@ -210,6 +205,7 @@ impl Service {
|
||||
shared.1.notify_one();
|
||||
}
|
||||
}
|
||||
|
||||
shared.0.lock().unwrap().3 = true;
|
||||
shared.1.notify_one();
|
||||
});
|
||||
|
||||
@@ -89,24 +89,29 @@ impl OpenMenu {
|
||||
impl FereseShell {
|
||||
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
|
||||
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
|
||||
if let Some(menu) = &mut self.menu {
|
||||
if menu.motion.closing() {
|
||||
if let Some(menu) = &mut self.menu
|
||||
&& menu.motion.closing()
|
||||
{
|
||||
menu.motion.retarget(1.0, Instant::now());
|
||||
return Task::none();
|
||||
}
|
||||
}
|
||||
|
||||
return self.close_menu();
|
||||
}
|
||||
|
||||
if kind == Menu::Calendar {
|
||||
self.calendar_offset = 0;
|
||||
}
|
||||
|
||||
let destroy = self.destroy_menu();
|
||||
if !kind.available(&self.status) {
|
||||
return destroy;
|
||||
}
|
||||
|
||||
if kind == Menu::Notifications && self.notifications.ready {
|
||||
self.notifications.toggle_history();
|
||||
}
|
||||
|
||||
let notifications = self.sync_notification_surface();
|
||||
let id = window::Id::unique();
|
||||
self.menu = Some(OpenMenu {
|
||||
@@ -303,6 +308,7 @@ impl FereseShell {
|
||||
)]
|
||||
.spacing(4)
|
||||
.align_y(Alignment::Center);
|
||||
|
||||
if kind == Menu::Recording
|
||||
&& let Some(elapsed) = self.recorder.elapsed()
|
||||
{
|
||||
@@ -312,15 +318,17 @@ impl FereseShell {
|
||||
.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
|
||||
&& self.config.status.battery_percentage
|
||||
&& let Some(battery) = &self.status.battery
|
||||
{
|
||||
content = content.push(
|
||||
text(format!("{}%", battery.percent))
|
||||
.size(metrics.text_size)
|
||||
.class(theme::Text::Color(foreground)),
|
||||
);
|
||||
}
|
||||
}
|
||||
if kind == Menu::Notifications
|
||||
&& self
|
||||
.status
|
||||
@@ -492,6 +500,7 @@ impl FereseShell {
|
||||
let combined = menu.kind == Menu::System;
|
||||
if combined {
|
||||
let mut connections = row::with_capacity(3).spacing(8).width(Length::Fill);
|
||||
|
||||
if let Some(n) = &self.status.network {
|
||||
connections = connections.push(connection_control(
|
||||
"Wi-Fi",
|
||||
@@ -507,6 +516,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &self.status.bluetooth {
|
||||
connections = connections.push(connection_control(
|
||||
"Bluetooth",
|
||||
@@ -522,6 +532,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(b) = &self.status.battery {
|
||||
connections = connections.push(connection_control(
|
||||
"Battery",
|
||||
@@ -533,6 +544,7 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
|
||||
if self.status.network.is_some()
|
||||
|| self.status.bluetooth.is_some()
|
||||
|| self.status.battery.is_some()
|
||||
@@ -540,8 +552,10 @@ impl FereseShell {
|
||||
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 Some(n) = &self.status.network
|
||||
{
|
||||
let connected = n.enabled && n.connection.is_some();
|
||||
rows = rows.push(status_summary(
|
||||
status_icon(Menu::Network, &self.status).0,
|
||||
@@ -582,9 +596,10 @@ impl FereseShell {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Bluetooth {
|
||||
if let Some(b) = &self.status.bluetooth {
|
||||
|
||||
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 {
|
||||
@@ -605,6 +620,7 @@ impl FereseShell {
|
||||
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 {
|
||||
@@ -616,6 +632,7 @@ impl FereseShell {
|
||||
.spacing(3),
|
||||
);
|
||||
}
|
||||
|
||||
let list: Element<'_, cosmic::Action<Message>> =
|
||||
if let Some(height) = device_list_height(b.devices.len()) {
|
||||
cosmic::iced::widget::scrollable(devices)
|
||||
@@ -627,10 +644,10 @@ impl FereseShell {
|
||||
};
|
||||
rows = rows.push(list);
|
||||
}
|
||||
}
|
||||
}
|
||||
if combined || menu.kind == Menu::Audio {
|
||||
if let Some(a) = &self.status.audio {
|
||||
|
||||
if (combined || menu.kind == Menu::Audio)
|
||||
&& let Some(a) = &self.status.audio
|
||||
{
|
||||
let audio = column![
|
||||
row![
|
||||
text("Volume").size(13).width(Length::Fill),
|
||||
@@ -661,8 +678,8 @@ impl FereseShell {
|
||||
rows = rows.push(audio);
|
||||
}
|
||||
}
|
||||
if combined {
|
||||
if let Some(value) = self.status.brightness {
|
||||
|
||||
if combined && let Some(value) = self.status.brightness {
|
||||
let brightness = column![
|
||||
text("Brightness").size(13),
|
||||
slider_row(
|
||||
@@ -677,15 +694,17 @@ impl FereseShell {
|
||||
.spacing(6);
|
||||
rows = rows.push(control_card(brightness.into(), primary, p));
|
||||
}
|
||||
}
|
||||
if combined || menu.kind == Menu::Notifications {
|
||||
if let Some(n) = &self.status.notifications {
|
||||
|
||||
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(
|
||||
@@ -706,10 +725,10 @@ impl FereseShell {
|
||||
p,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
if menu.kind == Menu::Battery {
|
||||
if let Some(b) = &self.status.battery {
|
||||
|
||||
if menu.kind == Menu::Battery
|
||||
&& let Some(b) = &self.status.battery
|
||||
{
|
||||
let battery_color = if b.percent < self.config.status.low_battery_threshold
|
||||
&& b.status != "Charging"
|
||||
{
|
||||
@@ -752,20 +771,21 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if menu.kind == Menu::Battery {
|
||||
rows = rows.push(text("Power mode").size(13).class(theme::Text::Color(muted)));
|
||||
if let Some(profiles) = &self.status.power_profiles {
|
||||
let mut modes = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (index, (label, icon, profile)) in [
|
||||
(
|
||||
"Battery saver",
|
||||
"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(),
|
||||
include_bytes!("../assets/icons/status/power-balanced.svg")
|
||||
.as_slice(),
|
||||
"balanced",
|
||||
),
|
||||
(
|
||||
@@ -789,7 +809,8 @@ impl FereseShell {
|
||||
));
|
||||
}
|
||||
}
|
||||
rows = rows.push(modes);
|
||||
// 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")
|
||||
@@ -797,6 +818,7 @@ impl FereseShell {
|
||||
.class(theme::Text::Color(muted)),
|
||||
);
|
||||
}
|
||||
|
||||
let mut power = row::with_capacity(3).spacing(6).width(Length::Fill);
|
||||
for (enabled, label, icon, action) in [
|
||||
(
|
||||
@@ -822,11 +844,13 @@ impl FereseShell {
|
||||
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);
|
||||
rows = rows.push(power.align_y(Alignment::Center).width(Length::Fill));
|
||||
}
|
||||
}
|
||||
|
||||
if !menu.kind.available(&self.status) {
|
||||
rows = rows.push(
|
||||
text("Service unavailable")
|
||||
@@ -835,12 +859,15 @@ impl FereseShell {
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(error) = &self.status_error {
|
||||
rows = rows.push(text(error).size(12).class(theme::Text::Color(Color {
|
||||
a: p,
|
||||
..Color::from_rgb8(230, 172, 90)
|
||||
})));
|
||||
}
|
||||
|
||||
let compositor_material = menu.effects.is_some();
|
||||
let panel = container(rows)
|
||||
.id("ferese-blur-card")
|
||||
@@ -856,10 +883,10 @@ impl FereseShell {
|
||||
color: color_with_opacity(theme.text_muted, 0.18 * p),
|
||||
width: 1.0,
|
||||
radius: theme.material_radius.into(),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
}));
|
||||
|
||||
cosmic::widget::autosize::autosize(
|
||||
super::motion::animated(
|
||||
panel.into(),
|
||||
@@ -1111,20 +1138,20 @@ fn power_tile<'a>(
|
||||
},
|
||||
opacity,
|
||||
);
|
||||
let tile = column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
let tile = column![
|
||||
bar_icon(icon, 19, foreground),
|
||||
text(label).size(11).align_x(Alignment::Center)
|
||||
]
|
||||
.spacing(3)
|
||||
.align_x(Alignment::Center);
|
||||
motion::button(
|
||||
button::custom(tile)
|
||||
let btn = button::custom(tile.align_x(Alignment::Center))
|
||||
.width(Length::FillPortion(1))
|
||||
.padding([8, 2])
|
||||
.on_press(cosmic::Action::App(Message::Control(Action::PowerProfile(
|
||||
profile,
|
||||
)))),
|
||||
foreground,
|
||||
selected,
|
||||
opacity,
|
||||
)
|
||||
))));
|
||||
|
||||
motion::button(btn, foreground, selected, opacity)
|
||||
}
|
||||
|
||||
fn power_tile_action<'a>(
|
||||
@@ -1135,6 +1162,7 @@ fn power_tile_action<'a>(
|
||||
opacity: f32,
|
||||
) -> Element<'a, cosmic::Action<Message>> {
|
||||
let foreground = color_with_opacity(theme.text_primary, opacity);
|
||||
// TODO: confirm column align fix
|
||||
motion::button(
|
||||
button::custom(
|
||||
column![bar_icon(icon, 19, foreground), text(label).size(11)]
|
||||
@@ -1623,12 +1651,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn icons_follow_real_states() {
|
||||
let mut s = Snapshot::default();
|
||||
s.audio = Some(status::Audio {
|
||||
let mut s = Snapshot {
|
||||
audio: Some(status::Audio {
|
||||
volume: 80,
|
||||
muted: true,
|
||||
output: String::new(),
|
||||
});
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
assert_eq!(
|
||||
status_icon(Menu::Audio, &s),
|
||||
(
|
||||
@@ -1636,6 +1666,7 @@ mod tests {
|
||||
false
|
||||
)
|
||||
);
|
||||
|
||||
s.battery = Some(status::Battery {
|
||||
percent: 2,
|
||||
status: "Charging".into(),
|
||||
@@ -1644,6 +1675,7 @@ mod tests {
|
||||
status_icon(Menu::Battery, &s).0,
|
||||
include_bytes!("../assets/icons/status/battery-charging.svg")
|
||||
);
|
||||
|
||||
s.notifications = Some(status::Notifications {
|
||||
count: 3,
|
||||
dnd: true,
|
||||
@@ -1656,6 +1688,7 @@ mod tests {
|
||||
status_label(Menu::Notifications, &s),
|
||||
"Notifications: do not disturb"
|
||||
);
|
||||
|
||||
s.bluetooth = Some(status::Bluetooth {
|
||||
enabled: false,
|
||||
devices: vec!["Headphones".into()],
|
||||
|
||||
Reference in new issue
Block a user