From 610478f8c78a6e287a5c5e34f2f62ca7c926e303 Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Thu, 22 Oct 2026 00:45:28 +0100 Subject: [PATCH] Format Rust sources at 100 columns --- compositor/src/activities.rs | 118 +- compositor/src/activities/deadline.rs | 28 +- compositor/src/activities/pomodoro.rs | 40 +- compositor/src/activities/publication.rs | 23 +- compositor/src/activities_process.rs | 41 +- compositor/src/backends/direct.rs | 583 ++++++--- compositor/src/backends/direct/capture.rs | 113 +- .../src/backends/direct/crtc_assignment.rs | 73 +- compositor/src/backends/direct/diagnostics.rs | 25 +- compositor/src/backends/direct/feedback.rs | 102 +- compositor/src/backends/direct/gamma.rs | 100 +- compositor/src/backends/direct/lid.rs | 17 +- compositor/src/backends/direct/mirror.rs | 61 +- compositor/src/backends/direct/recovery.rs | 77 +- compositor/src/backends/direct/redraw.rs | 85 +- compositor/src/backends/direct/scheduling.rs | 11 +- .../src/backends/direct/session_device.rs | 15 +- compositor/src/backends/direct/topology.rs | 185 ++- compositor/src/backends/direct/transaction.rs | 121 +- compositor/src/backends/mod.rs | 46 +- compositor/src/backends/power.rs | 17 +- compositor/src/config.rs | 275 ++-- compositor/src/config/appearance.rs | 70 +- compositor/src/config/bindings.rs | 169 ++- compositor/src/config/bindings/index.rs | 36 +- compositor/src/config/bindings/modes.rs | 62 +- compositor/src/config/input_motion.rs | 11 +- compositor/src/config/layout.rs | 24 +- compositor/src/config/loading.rs | 16 +- compositor/src/config/outputs.rs | 23 +- compositor/src/config/tab_indicator.rs | 44 +- compositor/src/cursor.rs | 105 +- compositor/src/cursor/tests.rs | 71 +- compositor/src/daemon.rs | 50 +- compositor/src/display_presentation.rs | 7 +- compositor/src/dmabuf_feedback.rs | 16 +- compositor/src/dmabuf_feedback/tests.rs | 42 +- compositor/src/dmabuf_imports.rs | 63 +- compositor/src/effects.rs | 67 +- compositor/src/floating.rs | 84 +- compositor/src/focus_effect.rs | 22 +- compositor/src/frame_scheduler.rs | 41 +- compositor/src/gamma.rs | 26 +- compositor/src/gestures.rs | 16 +- compositor/src/grabs/move_grab.rs | 21 +- compositor/src/grabs/resize_grab.rs | 85 +- compositor/src/handlers/activation.rs | 72 +- compositor/src/handlers/compositor.rs | 3 +- compositor/src/handlers/dispatch.rs | 3 +- compositor/src/handlers/gamma_control.rs | 4 +- compositor/src/handlers/input_method.rs | 10 +- compositor/src/handlers/layer_shell.rs | 78 +- .../src/handlers/layer_shell/output_tests.rs | 18 +- compositor/src/handlers/mod.rs | 56 +- compositor/src/handlers/screencopy.rs | 51 +- compositor/src/handlers/screenshot.rs | 422 +++++-- compositor/src/handlers/screenshot_worker.rs | 39 +- compositor/src/handlers/security_context.rs | 25 +- .../src/handlers/toplevel_icon_tests.rs | 25 +- compositor/src/handlers/window_capture.rs | 128 +- compositor/src/handlers/xdg_shell.rs | 69 +- compositor/src/idle_inhibition.rs | 40 +- compositor/src/idle_inhibition/media.rs | 42 +- compositor/src/idle_inhibition/media/bus.rs | 82 +- .../src/idle_inhibition/media/policy.rs | 5 +- compositor/src/idle_inhibition/media/tests.rs | 9 +- compositor/src/input.rs | 212 +++- compositor/src/input/binding_mode_tests.rs | 32 +- compositor/src/input/layer_focus_tests.rs | 103 +- compositor/src/input_capture.rs | 95 +- compositor/src/ipc.rs | 884 ++++++++----- compositor/src/ipc/activity_owner.rs | 47 +- compositor/src/ipc/transport.rs | 21 +- compositor/src/ipc_events.rs | 86 +- compositor/src/main.rs | 17 +- compositor/src/metrics.rs | 23 +- compositor/src/monitor_identity.rs | 31 +- compositor/src/output_policy.rs | 109 +- compositor/src/output_transaction.rs | 17 +- compositor/src/overview.rs | 345 +++-- compositor/src/portal_session.rs | 105 +- compositor/src/portal_shortcuts.rs | 55 +- compositor/src/presentation.rs | 131 +- compositor/src/presentation_policy.rs | 19 +- compositor/src/private_client.rs | 14 +- compositor/src/process.rs | 39 +- compositor/src/process/supervisor.rs | 88 +- compositor/src/reload.rs | 148 ++- compositor/src/render/backdrop.rs | 74 +- compositor/src/render/capture.rs | 45 +- compositor/src/render/closing.rs | 25 +- compositor/src/render/corner_tests.rs | 143 ++- compositor/src/render/decorations.rs | 118 +- compositor/src/render/materials.rs | 232 +++- compositor/src/render/mod.rs | 210 +++- compositor/src/render/overview.rs | 78 +- compositor/src/render/resources.rs | 39 +- compositor/src/render/scene.rs | 127 +- compositor/src/render/shader.rs | 60 +- compositor/src/render/tab_indicator.rs | 79 +- compositor/src/render/tabs.rs | 38 +- compositor/src/render/window_blur.rs | 32 +- compositor/src/render/window_content.rs | 68 +- compositor/src/resize_metrics.rs | 30 +- compositor/src/resize_transaction.rs | 3 +- compositor/src/resume.rs | 13 +- compositor/src/resume/tests.rs | 51 +- compositor/src/session_environment.rs | 28 +- compositor/src/session_lock.rs | 62 +- compositor/src/session_lock/tests.rs | 274 ++-- compositor/src/shell_control.rs | 218 +++- compositor/src/stacking.rs | 23 +- compositor/src/startup_tests.rs | 108 +- compositor/src/state.rs | 234 +++- compositor/src/state/animation.rs | 237 +++- compositor/src/state/capture_privacy.rs | 11 +- compositor/src/state/capture_privacy_tests.rs | 435 +++++-- compositor/src/state/floating.rs | 98 +- compositor/src/state/focus.rs | 63 +- compositor/src/state/hit_testing.rs | 52 +- compositor/src/state/layout.rs | 215 +++- compositor/src/state/lifecycle.rs | 34 +- compositor/src/state/navigation.rs | 201 ++- compositor/src/state/outputs.rs | 51 +- compositor/src/state/panel_clearance_tests.rs | 13 +- compositor/src/state/prediction.rs | 316 +++-- .../src/state/presentation_dependencies.rs | 256 ++-- compositor/src/state/reconciliation.rs | 161 ++- compositor/src/state/tab_indicator.rs | 102 +- compositor/src/state/tabs.rs | 50 +- compositor/src/state/viewport.rs | 57 +- .../src/state/window_lifecycle_tests.rs | 332 +++-- compositor/src/state/window_registry.rs | 92 +- compositor/src/state/windows.rs | 278 +++- compositor/src/theme.rs | 159 ++- compositor/src/wallpaper.rs | 70 +- compositor/src/wallpaper/decoder.rs | 27 +- compositor/src/wallpaper/pixels.rs | 38 +- compositor/src/window_rules.rs | 49 +- compositor/src/winit.rs | 71 +- compositor/src/xwayland/auth.rs | 34 +- compositor/src/xwayland/child.rs | 56 +- compositor/src/xwayland/integration_tests.rs | 392 ++++-- compositor/src/xwayland/lifecycle.rs | 51 +- compositor/src/xwayland/mod.rs | 267 ++-- compositor/src/xwayland/readiness.rs | 70 +- compositor/src/xwayland/sockets.rs | 72 +- crates/ferese-activities/src/lib.rs | 107 +- crates/ferese-activities/src/pomodoro.rs | 14 +- crates/ferese-activities/src/tests.rs | 84 +- crates/ferese-animation/src/gesture.rs | 22 +- crates/ferese-animation/src/lib.rs | 152 ++- crates/ferese-animation/src/settings.rs | 16 +- crates/ferese-animation/src/spring.rs | 30 +- crates/ferese-animation/src/traces.rs | 121 +- crates/ferese-config/src/activities.rs | 63 +- crates/ferese-config/src/families.rs | 42 +- crates/ferese-config/src/file.rs | 25 +- crates/ferese-config/src/lib.rs | 223 +++- crates/ferese-config/src/theme.rs | 409 ++++-- crates/ferese-config/tests/panel.rs | 95 +- crates/ferese-core/src/focus.rs | 33 +- crates/ferese-core/src/lib.rs | 7 +- crates/ferese-core/src/output.rs | 443 +++++-- crates/ferese-core/src/workspace.rs | 498 ++++++-- crates/ferese-core/src/workspace_policy.rs | 95 +- crates/ferese-ipc/src/activities.rs | 5 +- crates/ferese-ipc/src/connection.rs | 26 +- crates/ferese-ipc/src/lib.rs | 5 +- crates/ferese-ipc/src/media.rs | 10 +- crates/ferese-ipc/src/socket.rs | 21 +- crates/ferese-ipc/src/theme.rs | 15 +- crates/ferese-ipc/src/xwayland.rs | 5 +- crates/ferese-layout/src/lib.rs | 311 ++++- crates/ferese-layout/src/scrolling.rs | 732 ++++++++--- crates/ferese-panel/src/adaptive.rs | 3 +- crates/ferese-panel/src/morph.rs | 54 +- crates/ferese-panel/src/motion.rs | 30 +- crates/ferese-panel/src/outline.rs | 86 +- crates/ferese-panel/src/panel.rs | 60 +- crates/ferese-panel/src/panel/geometry.rs | 25 +- crates/ferese-panel/src/panel/layout.rs | 148 ++- crates/ferese-panel/src/panel/radius.rs | 13 +- crates/ferese-panel/src/scene.rs | 102 +- crates/ferese-panel/src/shape.rs | 115 +- crates/ferese-protocols/src/lib.rs | 16 +- crates/ferese-theme-client/src/material.rs | 19 +- crates/ferese-theme-client/src/service.rs | 12 +- .../ferese-theme-client/tests/theme_wire.rs | 17 +- crates/ferese-theme-model/src/color.rs | 65 +- crates/ferese-theme-model/src/lib.rs | 6 +- crates/ferese-theme/src/button.rs | 6 +- crates/ferese-theme/src/calendar.rs | 71 +- crates/ferese-theme/src/contrast.rs | 50 +- crates/ferese-theme/src/controls.rs | 43 +- crates/ferese-theme/src/gallery.rs | 134 +- crates/ferese-theme/src/icons.rs | 38 +- crates/ferese-theme/src/menus.rs | 38 +- crates/ferese-theme/src/palette.rs | 57 +- crates/ferese-theme/src/panel.rs | 112 +- crates/ferese-theme/src/panel_surface.rs | 185 ++- crates/ferese-theme/src/workspaces.rs | 66 +- docs/configuration/activities.md | 1 + feresectl/src/activities.rs | 70 +- feresectl/src/activities/progress.rs | 27 +- feresectl/src/activities/setup.rs | 66 +- feresectl/src/activities/transport.rs | 12 +- feresectl/src/autostart.rs | 21 +- feresectl/src/cli.rs | 25 +- feresectl/src/main.rs | 87 +- feresectl/src/output.rs | 5 +- feresectl/tests/cli.rs | 98 +- portal/src/backend.rs | 235 +++- portal/src/bridge.rs | 27 +- portal/src/bridge/tests.rs | 41 +- portal/src/capture.rs | 109 +- portal/src/consent.rs | 17 +- portal/src/desktop.rs | 243 +++- portal/src/eis.rs | 110 +- portal/src/inhibit.rs | 85 +- portal/src/input_capture.rs | 152 ++- portal/src/lockdown.rs | 35 +- portal/src/main.rs | 12 +- portal/src/parent.rs | 27 +- portal/src/permissions.rs | 98 +- portal/src/picker.rs | 81 +- portal/src/record_main.rs | 8 +- portal/src/recorder.rs | 207 ++- portal/src/recording_region.rs | 22 +- portal/src/restore.rs | 77 +- portal/src/settings.rs | 44 +- portal/src/shortcuts.rs | 78 +- portal/src/stream.rs | 53 +- rustfmt.toml | 2 +- shell/ferese-lock/src/appearance.rs | 70 +- shell/ferese-lock/src/auth.rs | 27 +- shell/ferese-lock/src/main.rs | 143 ++- shell/ferese-lock/src/runtime.rs | 32 +- shell/ferese-polkit-agent/src/main.rs | 68 +- shell/ferese-polkit-agent/src/prompt.rs | 134 +- shell/ferese-polkit-agent/src/session.rs | 145 ++- .../ferese-settings/src/connections/agent.rs | 104 +- .../src/connections/bluetooth.rs | 35 +- .../src/connections/bluetooth_ui.rs | 47 +- .../src/connections/contracts.rs | 110 +- .../src/connections/controller.rs | 67 +- shell/ferese-settings/src/connections/mod.rs | 24 +- shell/ferese-settings/src/connections/ui.rs | 42 +- shell/ferese-settings/src/connections/wifi.rs | 96 +- .../src/connections/wifi_ui.rs | 68 +- shell/ferese-settings/src/displays.rs | 66 +- shell/ferese-settings/src/displays_ui.rs | 50 +- shell/ferese-settings/src/form_controls.rs | 55 +- shell/ferese-settings/src/main.rs | 362 ++++-- shell/ferese-settings/src/navigation.rs | 10 +- shell/ferese-settings/src/pages.rs | 260 ++-- shell/ferese-settings/src/panel_controls.rs | 337 +++-- shell/ferese-settings/src/panel_drag.rs | 156 ++- shell/ferese-settings/src/panel_edit.rs | 299 +++-- shell/ferese-settings/src/panel_inspector.rs | 129 +- shell/ferese-settings/src/panel_preview.rs | 199 ++- shell/ferese-settings/src/schema.rs | 41 +- shell/ferese-settings/src/scroll_tests.rs | 256 +++- shell/ferese-settings/src/store.rs | 55 +- shell/ferese-settings/src/theme_controls.rs | 41 +- shell/ferese-settings/src/visuals.rs | 106 +- .../ferese-settings/src/wallpaper_controls.rs | 125 +- shell/ferese-settings/src/watch.rs | 28 +- shell/ferese-shell/src/activities.rs | 341 +++-- shell/ferese-shell/src/activities/compact.rs | 28 +- shell/ferese-shell/src/activities/pomodoro.rs | 61 +- shell/ferese-shell/src/activities/ring.rs | 29 +- shell/ferese-shell/src/bar.rs | 312 +++-- .../src/bar/binding_mode_tests.rs | 60 +- shell/ferese-shell/src/bar/presentation.rs | 50 +- shell/ferese-shell/src/battery_indicator.rs | 21 +- shell/ferese-shell/src/clock.rs | 106 +- shell/ferese-shell/src/compositor_ipc.rs | 43 +- shell/ferese-shell/src/config.rs | 106 +- shell/ferese-shell/src/control.rs | 206 ++- shell/ferese-shell/src/display_mode.rs | 164 ++- shell/ferese-shell/src/ipc_subscription.rs | 64 +- shell/ferese-shell/src/keybinding_guide.rs | 9 +- shell/ferese-shell/src/launcher/files.rs | 21 +- shell/ferese-shell/src/launcher/launch.rs | 14 +- shell/ferese-shell/src/launcher/mod.rs | 104 +- shell/ferese-shell/src/launcher/sources.rs | 19 +- shell/ferese-shell/src/launcher/tests.rs | 33 +- shell/ferese-shell/src/launcher/thumbnails.rs | 35 +- shell/ferese-shell/src/launcher/view.rs | 87 +- shell/ferese-shell/src/main.rs | 365 ++++-- shell/ferese-shell/src/media.rs | 37 +- shell/ferese-shell/src/media/art.rs | 10 +- shell/ferese-shell/src/media/subscription.rs | 4 +- shell/ferese-shell/src/motion.rs | 206 ++- shell/ferese-shell/src/motion/frames.rs | 96 +- shell/ferese-shell/src/notification_ui.rs | 183 ++- shell/ferese-shell/src/notifications.rs | 129 +- shell/ferese-shell/src/panel_hub.rs | 1116 ++++++++++++----- shell/ferese-shell/src/panel_hub/activity.rs | 45 +- shell/ferese-shell/src/panel_hub/battery.rs | 5 +- .../src/panel_hub/content_frame.rs | 10 +- shell/ferese-shell/src/panel_hub/equalizer.rs | 29 +- shell/ferese-shell/src/panel_hub/scroll.rs | 40 +- .../ferese-shell/src/panel_hub/transition.rs | 6 +- .../src/panel_hub/workspace_grid.rs | 168 ++- .../src/panel_hub/workspace_grid/drag.rs | 74 +- .../src/panel_hub/workspace_grid/thumbnail.rs | 7 +- .../ferese-shell/src/panel_hub/workspaces.rs | 107 +- shell/ferese-shell/src/panel_runtime.rs | 25 +- shell/ferese-shell/src/recording.rs | 47 +- shell/ferese-shell/src/renderer.rs | 12 +- shell/ferese-shell/src/status.rs | 134 +- shell/ferese-shell/src/status/audio_cache.rs | 22 +- .../src/status/audio_cache/signals.rs | 58 +- shell/ferese-shell/src/status/bluetooth.rs | 26 +- shell/ferese-shell/src/status/dbus_cache.rs | 62 +- shell/ferese-shell/src/status/hardware.rs | 7 +- shell/ferese-shell/src/status/network.rs | 7 +- shell/ferese-shell/src/status/night_light.rs | 11 +- shell/ferese-shell/src/status_ui/audio.rs | 14 +- shell/ferese-shell/src/status_ui/bar.rs | 71 +- shell/ferese-shell/src/status_ui/battery.rs | 52 +- shell/ferese-shell/src/status_ui/bluetooth.rs | 13 +- shell/ferese-shell/src/status_ui/calendar.rs | 15 +- shell/ferese-shell/src/status_ui/controls.rs | 29 +- shell/ferese-shell/src/status_ui/lifecycle.rs | 47 +- shell/ferese-shell/src/status_ui/media.rs | 269 ++-- shell/ferese-shell/src/status_ui/mod.rs | 104 +- shell/ferese-shell/src/status_ui/network.rs | 20 +- .../ferese-shell/src/status_ui/night_light.rs | 20 +- shell/ferese-shell/src/status_ui/system.rs | 43 +- shell/ferese-shell/src/surfaces.rs | 237 +++- shell/ferese-shell/src/surfaces/tests.rs | 19 +- shell/ferese-shell/src/system_modal.rs | 203 ++- shell/ferese-shell/src/workspace_ui.rs | 179 ++- 336 files changed, 23497 insertions(+), 7892 deletions(-) diff --git a/compositor/src/activities.rs b/compositor/src/activities.rs index bf393fa..37b7f3f 100644 --- a/compositor/src/activities.rs +++ b/compositor/src/activities.rs @@ -48,10 +48,13 @@ impl Default for Engine { pomodoro: None, pomodoro_settings: Default::default(), alerts: None, - store: Store::new(ferese_ipc::activities::now_ms() ^ u64::from(std::process::id()) << 32), + store: Store::new( + ferese_ipc::activities::now_ms() ^ u64::from(std::process::id()) << 32, + ), rules: Arc::new(RwLock::new(Arc::new(Rules { enabled: false, - matcher: ferese_config::activities::Matcher::new(&settings).expect("built-in rules"), + matcher: ferese_config::activities::Matcher::new(&settings) + .expect("built-in rules"), }))), monitor: None, process_enabled: false, @@ -141,7 +144,9 @@ impl Engine { { match crate::activities_process::Watcher::start(self.rules.clone(), sender.clone()) { Ok(watcher) => self.process_watcher = Some(watcher), - Err(error) => tracing::warn!(%error, "could not start optional activity process source"), + Err(error) => { + tracing::warn!(%error, "could not start optional activity process source") + } } } } @@ -160,17 +165,22 @@ impl Engine { .cloned() .chain(self.pomodoro.iter().map(|timer| timer.activity())) .collect(), - pomodoro: self.pomodoro.as_ref().map(|timer| ferese_ipc::activities::Pomodoro { - id: timer.id.clone(), - timer: timer.session.clone(), - }), + pomodoro: self + .pomodoro + .as_ref() + .map(|timer| ferese_ipc::activities::Pomodoro { + id: timer.id.clone(), + timer: timer.session.clone(), + }), windows: self .windows .iter() - .map(|(activity, window)| ferese_ipc::activities::ActivityWindow { - activity: activity.clone(), - window: *window, - }) + .map( + |(activity, window)| ferese_ipc::activities::ActivityWindow { + activity: activity.clone(), + window: *window, + }, + ) .collect(), }); self.cached_snapshot = Some(snapshot.clone()); @@ -206,7 +216,13 @@ impl Engine { pub fn get(&mut self, request: u64, sender: SyncSender) { self.deliver(vec![(request, sender)]); } - pub fn watch(&mut self, connection: u64, request: u64, since: u64, sender: SyncSender) { + pub fn watch( + &mut self, + connection: u64, + request: u64, + since: u64, + sender: SyncSender, + ) { if self.locked || since != self.revision { self.get(request, sender); } else if self.watchers.len() < 32 { @@ -258,7 +274,11 @@ impl Engine { self.store .next_deadline(now) .into_iter() - .chain(self.pomodoro.as_ref().and_then(|timer| timer.session.deadline)) + .chain( + self.pomodoro + .as_ref() + .and_then(|timer| timer.session.deadline), + ) .min(), ) { tracing::error!(%error, "Activity deadline could not be armed"); @@ -307,16 +327,17 @@ impl Engine { window: Option, ) -> Result { let started = match &publish { - Publish::Start { local, .. } | Publish::Run { local, .. } | Publish::Hook { local, .. } => { - Some(local.clone()) - } + Publish::Start { local, .. } + | Publish::Run { local, .. } + | Publish::Hook { local, .. } => Some(local.clone()), _ => None, }; let now = ferese_ipc::activities::now_ms(); if matches!(publish, Publish::Hook { .. }) && matched.is_none() { return Ok(json!({"matched":false})); } - self.rates.retain(|_, (at, _)| now.saturating_sub(*at) < 60_000); + self.rates + .retain(|_, (at, _)| now.saturating_sub(*at) < 60_000); if self.rates.len() >= 128 && !self.rates.contains_key(&owner) { return Err("owner publication quota exceeded"); } @@ -392,7 +413,9 @@ impl Engine { value: Value::Indeterminate, template: Template::Progress, }; - let id = self.store.start(owner, local, data, now, 0, None, true, true, None)?; + let id = self + .store + .start(owner, local, data, now, 0, None, true, true, None)?; json!({"handle":id,"progress":"indeterminate"}) } } @@ -486,7 +509,10 @@ impl Engine { } impl crate::Ferese { - pub(crate) fn activity_window(&self, owner: &crate::ipc::activity_owner::VerifiedOwner) -> Option { + pub(crate) fn activity_window( + &self, + owner: &crate::ipc::activity_owner::VerifiedOwner, + ) -> Option { use smithay::reexports::wayland_server::Resource; let mut ancestors = owner.ancestor_pids().peekable(); ancestors.peek()?; @@ -495,7 +521,8 @@ impl crate::Ferese { .records() .filter_map(|(id, record)| { let client = record.surface.as_ref()?.client()?; - let pid = u32::try_from(client.get_credentials(&self.display_handle).ok()?.pid).ok()?; + let pid = + u32::try_from(client.get_credentials(&self.display_handle).ok()?.pid).ok()?; Some((pid, id.0)) }) .collect(); @@ -546,15 +573,27 @@ mod tests { for since in [revision, engine.revision] { let (send, receive) = std::sync::mpsc::sync_channel(1); engine.watch(2, 43, since, send); - assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); + assert_eq!( + receive.recv().unwrap().error.unwrap().code, + "session_locked" + ); } let (send, receive) = std::sync::mpsc::sync_channel(1); engine.get(44, send); - assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); + assert_eq!( + receive.recv().unwrap().error.unwrap().code, + "session_locked" + ); engine.set_locked(false); let (send, receive) = std::sync::mpsc::sync_channel(1); engine.watch(3, 45, revision, send); - assert!(receive.recv_timeout(Duration::from_secs(2)).unwrap().result.is_some()); + assert!( + receive + .recv_timeout(Duration::from_secs(2)) + .unwrap() + .result + .is_some() + ); } #[test] @@ -605,7 +644,10 @@ mod tests { fn snapshots_are_lazy_shared_and_immutable_across_publications() { let mut engine = Engine::default(); engine.changed(); - assert!(engine.cached_snapshot.is_none(), "no subscriber needs a snapshot"); + assert!( + engine.cached_snapshot.is_none(), + "no subscriber needs a snapshot" + ); let before = engine.snapshot(); assert!(Arc::ptr_eq(&before, &engine.snapshot())); let (first, first_reply) = std::sync::mpsc::sync_channel(1); @@ -637,13 +679,19 @@ mod tests { } let (sender, receiver) = std::sync::mpsc::sync_channel(1); engine.watch(3, 33, before.revision, sender); - assert_eq!(receiver.recv_timeout(Duration::from_secs(2)).unwrap().id, 33); + assert_eq!( + receiver.recv_timeout(Duration::from_secs(2)).unwrap().id, + 33 + ); assert!(Arc::ptr_eq(&after, &engine.snapshot())); } #[test] fn process_that_exits_before_detection_dispatch_is_not_published() { - let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); + let mut child = std::process::Command::new("sleep") + .arg("30") + .spawn() + .unwrap(); let (owner, fd, _) = crate::ipc::activity_owner::inspect(child.id()).unwrap(); let matched = ferese_config::activities::Matcher::new(&Default::default()) .unwrap() @@ -681,7 +729,10 @@ mod tests { #[test] fn terminal_matching_uses_nearest_unique_ancestor_and_never_guesses() { let ancestry = [400, 300, 200, 100]; - assert_eq!(origin_window(ancestry.into_iter(), &[(200, 7), (100, 8)]), Some(7)); + assert_eq!( + origin_window(ancestry.into_iter(), &[(200, 7), (100, 8)]), + Some(7) + ); assert_eq!( origin_window(ancestry.into_iter(), &[(200, 7), (200, 9), (100, 8)]), None @@ -739,7 +790,9 @@ mod tests { .unwrap(); engine.owner_exit(owner()); assert_eq!(engine.snapshot().windows[0].activity, id); - engine.store.advance(ferese_ipc::activities::now_ms() + 2_000); + engine + .store + .advance(ferese_ipc::activities::now_ms() + 2_000); engine.changed(); assert!(engine.snapshot().windows.is_empty()); } @@ -776,7 +829,14 @@ mod tests { ); } assert_eq!( - engine.store.entries().next().unwrap().data.output.as_deref(), + engine + .store + .entries() + .next() + .unwrap() + .data + .output + .as_deref(), Some("first") ); engine diff --git a/compositor/src/activities/deadline.rs b/compositor/src/activities/deadline.rs index d549a2a..6221c33 100644 --- a/compositor/src/activities/deadline.rs +++ b/compositor/src/activities/deadline.rs @@ -1,6 +1,8 @@ //! Activity deadlines share CLOCK_BOOTTIME with the published countdowns. A //! monotonic calloop timer would otherwise keep waiting for time spent asleep. -use smithay::reexports::calloop::{Interest, LoopHandle, Mode, PostAction, RegistrationToken, generic::Generic}; +use smithay::reexports::calloop::{ + Interest, LoopHandle, Mode, PostAction, RegistrationToken, generic::Generic, +}; use std::{ io, os::fd::{AsRawFd, FromRawFd, OwnedFd}, @@ -13,9 +15,13 @@ pub(crate) struct Deadline { token: RegistrationToken, } impl Deadline { - pub fn new(handle: LoopHandle<'static, crate::Ferese>) -> Result> { + pub fn new( + handle: LoopHandle<'static, crate::Ferese>, + ) -> Result> { // SAFETY: timerfd_create returns a new owned descriptor or -1. - let fd = unsafe { libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) }; + let fd = unsafe { + libc::timerfd_create(libc::CLOCK_BOOTTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) + }; if fd < 0 { return Err(io::Error::last_os_error().into()); } @@ -25,7 +31,8 @@ impl Deadline { |_, fd, state| { let mut expirations = 0_u64; // SAFETY: expirations is a writable eight-byte timerfd counter. - let result = unsafe { libc::read(fd.as_raw_fd(), (&raw mut expirations).cast(), 8) }; + let result = + unsafe { libc::read(fd.as_raw_fd(), (&raw mut expirations).cast(), 8) }; if result < 0 { let error = io::Error::last_os_error(); if error.kind() != io::ErrorKind::WouldBlock { @@ -50,7 +57,10 @@ impl Deadline { } let ms = deadline.unwrap_or(0); let spec = libc::itimerspec { - it_interval: libc::timespec { tv_sec: 0, tv_nsec: 0 }, + it_interval: libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }, it_value: libc::timespec { tv_sec: (ms / 1_000) as _, tv_nsec: ((ms % 1_000) * 1_000_000) as _, @@ -86,13 +96,17 @@ mod tests { fn deadline_uses_boottime_and_disarming_leaves_no_periodic_wakeups() { let events = smithay::reexports::calloop::EventLoop::::try_new().unwrap(); let deadline = Deadline::new(events.handle()).unwrap(); - let info = std::fs::read_to_string(format!("/proc/self/fdinfo/{}", deadline.fd.as_raw_fd())).unwrap(); + let info = + std::fs::read_to_string(format!("/proc/self/fdinfo/{}", deadline.fd.as_raw_fd())) + .unwrap(); assert!( info.lines() .any(|line| line.split_whitespace().collect::>() == ["clockid:", "7"]), "{info}" ); - deadline.update(Some(ferese_ipc::activities::now_ms() + 20)).unwrap(); + deadline + .update(Some(ferese_ipc::activities::now_ms() + 20)) + .unwrap(); let mut pollfd = libc::pollfd { fd: deadline.fd.as_raw_fd(), events: libc::POLLIN, diff --git a/compositor/src/activities/pomodoro.rs b/compositor/src/activities/pomodoro.rs index 8868ad1..c7b48aa 100644 --- a/compositor/src/activities/pomodoro.rs +++ b/compositor/src/activities/pomodoro.rs @@ -69,7 +69,9 @@ impl Engine { return Err("Pomodoro session changed; refresh its controls".into()); } let now = ferese_ipc::activities::now_ms(); - if !matches!(request.action, PomodoroAction::Stop | PomodoroAction::Reset) && self.advance_pomodoro(now) { + if !matches!(request.action, PomodoroAction::Stop | PomodoroAction::Reset) + && self.advance_pomodoro(now) + { self.changed(); } match request.action { @@ -85,8 +87,8 @@ impl Engine { } let session = Session::start(self.pomodoro_settings.clone(), now)?; // Verify the long-lived host, never the short-lived CLI, as owner. - let (owner, _pidfd, _) = - crate::ipc::activity_owner::inspect(std::process::id()).map_err(|error| error.to_string())?; + let (owner, _pidfd, _) = crate::ipc::activity_owner::inspect(std::process::id()) + .map_err(|error| error.to_string())?; let id = Id { owner, local: "pomodoro".into(), @@ -104,7 +106,10 @@ impl Engine { self.pomodoro = None; } action => { - let timer = self.pomodoro.as_mut().ok_or("No Pomodoro session; start one first")?; + let timer = self + .pomodoro + .as_mut() + .ok_or("No Pomodoro session; start one first")?; match action { PomodoroAction::Pause => timer.session.pause(now)?, PomodoroAction::Resume => timer.session.resume(now)?, @@ -182,14 +187,24 @@ impl Alerts { let connection = zbus::blocking::connection::Builder::session()? .method_timeout(Duration::from_secs(2)) .build()?; - let mut hints = std::collections::HashMap::<&str, zbus::zvariant::Value<'_>>::new(); + let mut hints = + std::collections::HashMap::<&str, zbus::zvariant::Value<'_>>::new(); hints.insert("suppress-sound", zbus::zvariant::Value::Bool(true)); let reply = connection.call_method( Some("org.freedesktop.Notifications"), "/org/freedesktop/Notifications", Some("org.freedesktop.Notifications"), "Notify", - &("Ferese", 0_u32, "", title, body, Vec::::new(), hints, -1_i32), + &( + "Ferese", + 0_u32, + "", + title, + body, + Vec::::new(), + hints, + -1_i32, + ), )?; let _: u32 = reply.body().deserialize()?; Ok(()) @@ -218,8 +233,7 @@ mod tests { } #[test] fn completion_uses_the_standard_notification_service_without_sound() { - const TEST: &str = - "activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound"; + const TEST: &str = "activities::pomodoro::tests::completion_uses_the_standard_notification_service_without_sound"; if !crate::startup_tests::private_runtime(TEST) { return; } @@ -296,7 +310,10 @@ mod tests { assert_eq!(first.id.owner.pid, std::process::id()); assert!(control(&mut engine, PomodoroAction::Start).is_err()); engine.remove_connection(10); - engine.owner_exit(Owner { pid: 999_999, birth: 0 }); + engine.owner_exit(Owner { + pid: 999_999, + birth: 0, + }); assert_eq!(engine.snapshot().pomodoro.as_ref().unwrap().id, first.id); assert_eq!(engine.snapshot().activities.len(), 1); control(&mut engine, PomodoroAction::Reset).unwrap(); @@ -355,7 +372,10 @@ mod tests { engine.pomodoro_remove_output("test", None); assert!(engine.pomodoro_attach_output("new")); engine.changed(); - assert_eq!(engine.snapshot().activities[0].data.output.as_deref(), Some("new")); + assert_eq!( + engine.snapshot().activities[0].data.output.as_deref(), + Some("new") + ); } #[test] fn disabled_alerts_and_schema_errors_have_no_side_effects() { diff --git a/compositor/src/activities/publication.rs b/compositor/src/activities/publication.rs index dc7a225..8cab6fe 100644 --- a/compositor/src/activities/publication.rs +++ b/compositor/src/activities/publication.rs @@ -26,7 +26,10 @@ impl Publisher { .as_ref() .is_some_and(|(snapshot, _)| Arc::ptr_eq(snapshot, &job.snapshot)) { - cached = Some((job.snapshot.clone(), serde_json::json!(job.snapshot.as_ref()))); + cached = Some(( + job.snapshot.clone(), + serde_json::json!(job.snapshot.as_ref()), + )); } let value = &cached.as_ref().unwrap().1; // Lock acquisition revokes queued/in-flight serialization. @@ -54,7 +57,12 @@ impl Publisher { } } } -fn deliver(epoch: u64, current: &RwLock, recipients: Vec, value: &serde_json::Value) { +fn deliver( + epoch: u64, + current: &RwLock, + recipients: Vec, + value: &serde_json::Value, +) { let current = current.read().expect("activity publication epoch"); if epoch != *current { locked(recipients); @@ -116,9 +124,16 @@ mod tests { let publisher = Publisher::new().unwrap(); let (slow, _slow_receive) = mpsc::sync_channel(1); - slow.send(Response::success(1, serde_json::json!({}))).unwrap(); + slow.send(Response::success(1, serde_json::json!({}))) + .unwrap(); let (fast, fast_receive) = mpsc::sync_channel(1); publisher.send(Arc::new(Snapshot::default()), vec![(2, slow), (3, fast)]); - assert_eq!(fast_receive.recv_timeout(Duration::from_secs(1)).unwrap().id, 3); + assert_eq!( + fast_receive + .recv_timeout(Duration::from_secs(1)) + .unwrap() + .id, + 3 + ); } } diff --git a/compositor/src/activities_process.rs b/compositor/src/activities_process.rs index f94f1c1..d865a44 100644 --- a/compositor/src/activities_process.rs +++ b/compositor/src/activities_process.rs @@ -27,13 +27,20 @@ impl Watcher { std::thread::Builder::new() .name("ferese-activity-processes".into()) .spawn(move || { - let mut observed = - BTreeMap::, ferese_config::activities::Matched)>::new(); + let mut observed = BTreeMap::< + Owner, + ( + Arc, + ferese_config::activities::Matched, + ), + >::new(); while !worker_stop.load(Ordering::Acquire) { let rules = rules.read().expect("activity rules").clone(); let dead: Vec<_> = observed .iter() - .filter_map(|(owner, (fd, _))| crate::ipc::activity_owner::exited(fd).then_some(*owner)) + .filter_map(|(owner, (fd, _))| { + crate::ipc::activity_owner::exited(fd).then_some(*owner) + }) .collect(); for owner in dead { observed.remove(&owner); @@ -54,21 +61,30 @@ impl Watcher { if worker_stop.load(Ordering::Acquire) || observed.len() >= 128 { break; } - let Some(pid) = process.file_name().to_str().and_then(|name| name.parse::().ok()) + let Some(pid) = process + .file_name() + .to_str() + .and_then(|name| name.parse::().ok()) else { continue; }; - let Ok((owner, fd, parents)) = crate::ipc::activity_owner::inspect(pid) else { + let Ok((owner, fd, parents)) = crate::ipc::activity_owner::inspect(pid) + else { continue; }; - if observed.contains_key(&owner) || parents.iter().any(|parent| parent == "feresectl") { + if observed.contains_key(&owner) + || parents.iter().any(|parent| parent == "feresectl") + { continue; } - let Ok(file) = std::fs::File::open(process.path().join("cmdline")) else { + let Ok(file) = std::fs::File::open(process.path().join("cmdline")) + else { continue; }; let mut bytes = Vec::new(); - if file.take(4_097).read_to_end(&mut bytes).is_err() || bytes.len() > 4_096 { + if file.take(4_097).read_to_end(&mut bytes).is_err() + || bytes.len() > 4_096 + { continue; } let Ok(command) = String::from_utf8(bytes) else { @@ -79,14 +95,19 @@ impl Watcher { continue; }; let cwd = cwd.to_string_lossy(); - let Some(matched) = rules.matcher.matches(&command, &cwd, &parents) else { + let Some(matched) = rules.matcher.matches(&command, &cwd, &parents) + else { continue; }; let fd = Arc::new(fd); // Keep the inspected process identity alive through dispatch. // No raw command, cwd or environment crosses the dispatcher. if sender - .try_send(IpcEvent::ActivityDetected(owner, Box::new(matched.clone()), fd.clone())) + .try_send(IpcEvent::ActivityDetected( + owner, + Box::new(matched.clone()), + fd.clone(), + )) .is_ok() { observed.insert(owner, (fd, matched)); diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 3fb054e..e9f99d8 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -113,13 +113,24 @@ impl DirectBackendState { self.active && !self.topology.reconciling && !self.lid.pending() } - pub(crate) fn process_dmabuf_imports(&mut self, imports: &mut crate::dmabuf_imports::ImportManager) { + pub(crate) fn process_dmabuf_imports( + &mut self, + imports: &mut crate::dmabuf_imports::ImportManager, + ) { if !self.active || self.topology.reconciling { return; } - for device in self.devices.values_mut().filter(|device| device.drm.is_active()) { - imports.process(device.dmabuf_global, &mut device.renderer, Some(device.render_node)); + for device in self + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { + imports.process( + device.dmabuf_global, + &mut device.renderer, + Some(device.render_node), + ); } } @@ -147,12 +158,14 @@ impl DirectBackendState { geometry: smithay::utils::Rectangle, output: &Output, remaining: usize, - ) -> Result, smithay::backend::renderer::gles::GlesError> { - let Some(device) = self - .devices - .values_mut() - .find(|device| device.outputs.values().any(|candidate| &candidate.output == output)) - else { + ) -> Result, smithay::backend::renderer::gles::GlesError> + { + let Some(device) = self.devices.values_mut().find(|device| { + device + .outputs + .values() + .any(|candidate| &candidate.output == output) + }) else { return Ok(None); }; crate::render::capture_resize_snapshot( @@ -173,7 +186,12 @@ impl DirectBackendState { let device = self .devices .values_mut() - .find(|device| device.outputs.values().any(|candidate| &candidate.output == output)) + .find(|device| { + device + .outputs + .values() + .any(|candidate| &candidate.output == output) + }) .ok_or("Window output is unavailable")?; crate::render::capture_window_buffer( &mut device.renderer, @@ -193,7 +211,12 @@ impl DirectBackendState { let device = self .devices .values_mut() - .find(|device| device.outputs.values().any(|candidate| &candidate.output == output)) + .find(|device| { + device + .outputs + .values() + .any(|candidate| &candidate.output == output) + }) .ok_or("Window output is unavailable")?; crate::render::capture_window_frame( &mut device.renderer, @@ -314,7 +337,12 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() let seat_name = session.seat(); let udev_backend = UdevBackend::new(&seat_name)?; let primary_path = primary_gpu(&seat_name)? - .or_else(|| udev_backend.device_list().next().map(|(_, path)| path.to_owned())) + .or_else(|| { + udev_backend + .device_list() + .next() + .map(|(_, path)| path.to_owned()) + }) .ok_or_else(|| io::Error::new(io::ErrorKind::NotFound, "no DRM device found"))?; let mut libinput_context = Libinput::new_with_udev::>(session.clone().into()); @@ -361,7 +389,10 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() .as_mut() .unwrap() .topology - .observe_devices(&[(primary_path.clone(), topology::device_identity(&primary_path))]); + .observe_devices(&[( + primary_path.clone(), + topology::device_identity(&primary_path), + )]); lid::request_refresh(state, false); event_loop @@ -376,7 +407,9 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() InputEvent::DeviceRemoved { device } => { if let Some(backend) = state.direct_backend.as_mut() { - backend.input_devices.retain(|candidate| candidate != &device); + backend + .input_devices + .retain(|candidate| candidate != &device); } } @@ -451,18 +484,20 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() tracing::info!("direct session reacquired; refreshing external state"); } })?; - event_loop.handle().insert_source(udev_backend, |event, _, state| { - if let UdevEvent::Removed { device_id } = event - && let Some(node) = direct_node_for_device(state, device_id) - && let Some(backend) = state.direct_backend.as_mut() - { - // A remove/re-add may reuse the same dev_t. Retire the old fd - // even if fresh enumeration already sees its replacement. - backend.topology.invalidated.insert(node); - } + event_loop + .handle() + .insert_source(udev_backend, |event, _, state| { + if let UdevEvent::Removed { device_id } = event + && let Some(node) = direct_node_for_device(state, device_id) + && let Some(backend) = state.direct_backend.as_mut() + { + // A remove/re-add may reuse the same dev_t. Retire the old fd + // even if fresh enumeration already sees its replacement. + backend.topology.invalidated.insert(node); + } - topology::hotplug(state); - })?; + topology::hotplug(state); + })?; tracing::info!(seat = %seat_name, "initialized direct session input and device discovery"); Ok(()) @@ -505,10 +540,12 @@ pub(crate) fn reload_input_devices(state: &mut Ferese) { fn automatic_refresh_active(backend: &DirectBackendState) -> bool { backend.active - && backend - .devices - .values() - .any(|device| device.outputs.values().any(|output| output.settings.auto_refresh)) + && backend.devices.values().any(|device| { + device + .outputs + .values() + .any(|output| output.settings.auto_refresh) + }) } fn sync_battery_timer(state: &mut Ferese) { @@ -525,23 +562,28 @@ fn sync_battery_timer(state: &mut Ferese) { return; } - match state - .loop_handle - .insert_source(Timer::from_duration(Duration::from_secs(5)), |_, _, state| { + match state.loop_handle.insert_source( + Timer::from_duration(Duration::from_secs(5)), + |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.battery_timer = None; } update_power_policy(state); sync_battery_timer(state); TimeoutAction::Drop - }) { + }, + ) { Ok(token) => backend.battery_timer = Some(token), Err(error) => tracing::warn!(%error, "could not arm automatic refresh battery timer"), } } fn update_power_policy(state: &mut Ferese) { - if !state.direct_backend.as_ref().is_some_and(automatic_refresh_active) { + if !state + .direct_backend + .as_ref() + .is_some_and(automatic_refresh_active) + { return; } let Some(backend) = state.direct_backend.as_mut() else { @@ -575,7 +617,12 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S if !backend.active || backend.lid.pending() { return Err("output session is inactive or refreshing lid state".into()); } - if name != "auto" && !state.output_profiles.iter().any(|profile| profile.name == name) { + if name != "auto" + && !state + .output_profiles + .iter() + .any(|profile| profile.name == name) + { return Err(format!("unknown output profile {name:?}")); } let previous = backend.manual_outputs.clone(); @@ -620,7 +667,10 @@ pub(crate) fn set_output_profile(state: &mut Ferese, name: &str) -> Result<(), S Ok(()) } -pub(crate) fn set_output_layout(state: &mut Ferese, layout: crate::config::OutputLayout) -> Result<(), String> { +pub(crate) fn set_output_layout( + state: &mut Ferese, + layout: crate::config::OutputLayout, +) -> Result<(), String> { let backend = state .direct_backend .as_mut() @@ -636,7 +686,9 @@ pub(crate) fn set_output_layout(state: &mut Ferese, layout: crate::config::Outpu let available = match layout { crate::config::OutputLayout::InternalOnly => usable.iter().any(|monitor| monitor.internal), crate::config::OutputLayout::ExternalOnly => usable.iter().any(|monitor| !monitor.internal), - crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => usable.len() >= 2, + crate::config::OutputLayout::Extend | crate::config::OutputLayout::Mirror => { + usable.len() >= 2 + } }; if !available { return Err("this display mode requires monitors that are not connected".into()); @@ -694,7 +746,11 @@ fn confirmation_baseline(pending: Option, previous: T) -> T { pending.unwrap_or(previous) } -fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfiguration, manual: ManualOverride) { +fn arm_output_confirmation( + state: &mut Ferese, + previous: DesiredOutputConfiguration, + manual: ManualOverride, +) { let backend = state.direct_backend.as_mut().unwrap(); if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); @@ -703,7 +759,12 @@ fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfigurat .desired_outputs .profile .as_ref() - .and_then(|name| state.output_profiles.iter().find(|profile| &profile.name == name)) + .and_then(|name| { + state + .output_profiles + .iter() + .find(|profile| &profile.name == name) + }) .map_or(15, |profile| profile.confirm_timeout); let baseline = confirmation_baseline(backend.confirmation.take(), (previous, manual)); if timeout == 0 || baseline.0.outputs.is_empty() { @@ -729,8 +790,14 @@ fn arm_output_confirmation(state: &mut Ferese, previous: DesiredOutputConfigurat } } -pub(crate) fn confirm_output_configuration(state: &mut Ferese, confirm: bool) -> Result<(), String> { - let backend = state.direct_backend.as_mut().ok_or("direct backend unavailable")?; +pub(crate) fn confirm_output_configuration( + state: &mut Ferese, + confirm: bool, +) -> Result<(), String> { + let backend = state + .direct_backend + .as_mut() + .ok_or("direct backend unavailable")?; if let Some(token) = backend.confirmation_timer.take() { state.loop_handle.remove(token); } @@ -751,7 +818,10 @@ pub(crate) fn confirm_output_configuration(state: &mut Ferese, confirm: bool) -> Ok(()) } -pub(crate) fn validate_live_outputs(state: &Ferese, profiles: &[OutputProfile]) -> Result<(), String> { +pub(crate) fn validate_live_outputs( + state: &Ferese, + profiles: &[OutputProfile], +) -> Result<(), String> { let Some(backend) = state.direct_backend.as_ref() else { return Ok(()); }; @@ -1018,11 +1088,19 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { // Apply runtime requests without changing output globals or workspace ownership. fn apply_output_power(state: &mut Ferese) { - let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.can_render()) else { + let Some(backend) = state + .direct_backend + .as_mut() + .filter(|backend| backend.can_render()) + else { return; }; - for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { for output in device.outputs.values_mut() { if !output.power.needs_power_off() || output.frame_pending { continue; @@ -1057,15 +1135,16 @@ fn apply_output_power(state: &mut Ferese) { state.loop_handle.remove(token); } } else if backend.power_retry.is_none() { - match state - .loop_handle - .insert_source(Timer::from_duration(Duration::from_secs(1)), |_, _, state| { + match state.loop_handle.insert_source( + Timer::from_duration(Duration::from_secs(1)), + |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.power_retry = None; } apply_output_power(state); TimeoutAction::Drop - }) { + }, + ) { Ok(token) => backend.power_retry = Some(token), Err(error) => tracing::warn!(%error, "could not arm failed power-off retry"), } @@ -1121,10 +1200,9 @@ pub(crate) fn cursor_animation_visible(state: &Ferese) -> bool { if let Some(backend) = state.direct_backend.as_ref() { backend.can_render() && backend.devices.values().any(|device| { - device - .outputs - .values() - .any(|output| output.power.can_render() && redraw::cursor_on_output(state, &output.output)) + device.outputs.values().any(|output| { + output.power.can_render() && redraw::cursor_on_output(state, &output.output) + }) }) } else { state @@ -1155,7 +1233,11 @@ pub(crate) fn render_surface( render_on(state, &outputs); } -fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std::panic::Location<'static>) { +fn redraw( + state: &mut Ferese, + selected: Option<&[Output]>, + source: &'static std::panic::Location<'static>, +) { if let Some(transition) = state.desktop_transition.as_mut() { transition.redraw.extend( selected @@ -1190,7 +1272,11 @@ fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std: let affected = outputs .iter() .map(|(_, _, output)| { - let scene = state.direct_backend.as_ref().unwrap().scene_output(state, output); + let scene = state + .direct_backend + .as_ref() + .unwrap() + .scene_output(state, output); selected.is_none_or(|selected| selected.contains(output) || selected.contains(&scene)) || state.output_has_animations(&scene) || state.output_has_pending_visual_changes(&scene) @@ -1205,12 +1291,12 @@ fn redraw(state: &mut Ferese, selected: Option<&[Output]>, source: &'static std: } } - state.direct_backend.as_mut().unwrap().scheduling_metrics.redraw( - source, - selected.is_none(), - requested, - outputs.len(), - ); + state + .direct_backend + .as_mut() + .unwrap() + .scheduling_metrics + .redraw(source, selected.is_none(), requested, outputs.len()); arm_animation_timer(state); } @@ -1236,7 +1322,11 @@ pub(crate) fn set_lid_closed(state: &mut Ferese, closed: bool) { } pub(crate) fn system_resumed(state: &mut Ferese) { - if state.direct_backend.as_ref().is_some_and(|backend| backend.active) { + if state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.active) + { let seat = state.seat.clone(); state.idle_notifier_state.notify_activity(&seat); } @@ -1250,7 +1340,9 @@ pub(crate) fn reconcile_after_monitor_recovery(state: &mut Ferese) { fn internal_connector(interface: connector::Interface) -> bool { matches!( interface, - connector::Interface::EmbeddedDisplayPort | connector::Interface::LVDS | connector::Interface::DSI + connector::Interface::EmbeddedDisplayPort + | connector::Interface::LVDS + | connector::Interface::DSI ) } @@ -1319,15 +1411,25 @@ fn render_output( let rendered = { (|| -> Result> { let elements = if output.mirror_source.is_some() { - mirror::elements(state, &mut device.renderer, &scene_output, &frame, &mut output)? + mirror::elements( + state, + &mut device.renderer, + &scene_output, + &frame, + &mut output, + )? } else { sampled_output_elements(state, &mut device.renderer, &output.output, true, &frame) }; - let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); - let fullscreen = state.output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()); + let effects = + frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); + let fullscreen = state + .output_has_fullscreen_for_frame(&output.output, frame.overview.is_presenting()); let locked = state.session_lock.active(); let flags = planes::frame_flags(fullscreen, frame.animating, locked); - output.diagnostics.attempt(fullscreen, frame.animating, locked); + output + .diagnostics + .attempt(fullscreen, frame.animating, locked); let result = output .surface .render_frame( @@ -1344,12 +1446,16 @@ fn render_output( let primary_format = match &result.primary_element { PrimaryPlaneElement::Swapchain(primary) => Some(primary.buffer().format()), - PrimaryPlaneElement::Element(element) => match element.underlying_storage(&mut device.renderer) { - Some(UnderlyingStorage::Wayland(buffer)) => smithay::wayland::dmabuf::get_dmabuf(buffer) - .ok() - .map(|buffer| buffer.format()), - _ => None, - }, + PrimaryPlaneElement::Element(element) => { + match element.underlying_storage(&mut device.renderer) { + Some(UnderlyingStorage::Wayland(buffer)) => { + smithay::wayland::dmabuf::get_dmabuf(buffer) + .ok() + .map(|buffer| buffer.format()) + } + _ => None, + } + } }; let feedback_candidates = feedback::candidates( &mut device.renderer, @@ -1361,7 +1467,10 @@ fn render_output( if result.needs_sync() && let PrimaryPlaneElement::Swapchain(primary) = &result.primary_element { - primary.sync.wait().inspect_err(|_| output.diagnostics.sync_failed())?; + primary + .sync + .wait() + .inspect_err(|_| output.diagnostics.sync_failed())?; } if result.is_empty { @@ -1371,11 +1480,19 @@ fn render_output( .into_iter() .any(|cursor| state.has_pending_screencopy(&output.output, cursor)) }); - output - .dmabuf_feedback - .observed_frame(primary_format, &result.states, feedback_candidates); + output.dmabuf_feedback.observed_frame( + primary_format, + &result.states, + feedback_candidates, + ); if output.mirror_source.is_some() { - capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output); + capture::capture_mirror( + state, + &mut device.renderer, + &scene_output, + &frame, + &mut output, + ); } else { capture::capture_output( state, @@ -1447,16 +1564,23 @@ fn render_output( flags.contains(FrameFlags::ALLOW_PRIMARY_PLANE_SCANOUT), ); capture::after_queue(queued, || { - output - .dmabuf_feedback - .observed_frame(primary_format, &visibility, feedback_candidates); + output.dmabuf_feedback.observed_frame( + primary_format, + &visibility, + feedback_candidates, + ); if output.mirror_source.is_none() { - state.display_presentation.queued(&output.output, &visibility); + state + .display_presentation + .queued(&output.output, &visibility); } output.primary_commit = primary_commit; - output - .render_metrics - .record_frame(render_started.elapsed(), &damage, missed_deadlines, effects); + output.render_metrics.record_frame( + render_started.elapsed(), + &damage, + missed_deadlines, + effects, + ); output.frame_pending = true; if !recovery::arm_completion(&state.loop_handle, node, crtc, &mut output) { recovery::reconcile_device(state, node); @@ -1470,7 +1594,13 @@ fn render_output( }); if output.mirror_source.is_some() { - capture::capture_mirror(state, &mut device.renderer, &scene_output, &frame, &mut output); + capture::capture_mirror( + state, + &mut device.renderer, + &scene_output, + &frame, + &mut output, + ); } else { capture::capture_output( state, @@ -1510,7 +1640,10 @@ fn render_output( device.outputs.insert(crtc, output); restore_device(state, node, device); - if !submitted && scene_output == state.direct_backend.as_ref().unwrap().devices[&node].outputs[&crtc].output { + if !submitted + && scene_output + == state.direct_backend.as_ref().unwrap().devices[&node].outputs[&crtc].output + { schedule_frame_callbacks(state, node, crtc, plan.present_at); } @@ -1534,13 +1667,22 @@ fn direct_node_for_device(state: &Ferese, device_id: libc::dev_t) -> Option Result { - let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.active) else { + let Some(backend) = state + .direct_backend + .as_mut() + .filter(|backend| backend.active) + else { return Err(()); }; let Some(mut device) = backend.devices.remove(&node) else { return Err(()); }; - let scan = select_outputs(&device, &backend.monitors, &backend.desired_outputs, backend.low_power); + let scan = select_outputs( + &device, + &backend.monitors, + &backend.desired_outputs, + backend.low_power, + ); let result = scan .map_err(|error| error.to_string()) .and_then(|scan| transaction::apply_device(state, node, &mut device, scan)); @@ -1556,7 +1698,12 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) -> Result { fn refresh_connected_info(state: &mut Ferese) { let backend = state.direct_backend.as_mut().unwrap(); for device in backend.devices.values_mut() { - if let Ok(scan) = select_outputs(device, &backend.monitors, &backend.desired_outputs, backend.low_power) { + if let Ok(scan) = select_outputs( + device, + &backend.monitors, + &backend.desired_outputs, + backend.low_power, + ) { device.connected_outputs = scan.connected_outputs; } transaction::update_applied_info(device); @@ -1570,7 +1717,10 @@ fn refresh_connected_info(state: &mut Ferese) { .flat_map(|device| device.connected_outputs.clone()) .collect::>(); for previous in &backend.connected_outputs { - if !inventory.iter().any(|output| output.identity == previous.identity) { + if !inventory + .iter() + .any(|output| output.identity == previous.identity) + { let mut missing = previous.clone(); missing.connected = false; missing.enabled = false; @@ -1598,7 +1748,10 @@ fn remember_applied_configuration(state: &mut Ferese) { } let mut applied = backend.desired_outputs.clone(); for desired in &mut applied.outputs { - let monitor = backend.monitors.iter().find(|monitor| monitor.key == desired.key); + let monitor = backend + .monitors + .iter() + .find(|monitor| monitor.key == desired.key); let output = monitor.and_then(|monitor| { backend .devices @@ -1624,7 +1777,10 @@ fn remember_applied_configuration(state: &mut Ferese) { } fn validate_desired_outputs(state: &Ferese) -> Result<(), String> { - let backend = state.direct_backend.as_ref().ok_or("direct backend unavailable")?; + let backend = state + .direct_backend + .as_ref() + .ok_or("direct backend unavailable")?; validate_output_configuration(backend, &backend.desired_outputs).map(|_| ()) } @@ -1676,7 +1832,10 @@ fn validate_selected_outputs<'a>( ) -> Result<(), String> { for output in outputs.into_iter().filter(|output| output.settings.enabled) { if !selected.contains(&output.key) { - return Err(format!("enabled output {} has no usable CRTC assignment", output.key)); + return Err(format!( + "enabled output {} has no usable CRTC assignment", + output.key + )); } } Ok(()) @@ -1739,10 +1898,9 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan .into_iter() .flat_map(|backend| backend.devices.iter()) .flat_map(|(node, device)| { - device - .outputs - .iter() - .map(move |(crtc, output)| (*node, *crtc, output.output.clone(), device.drm.is_active())) + device.outputs.iter().map(move |(crtc, output)| { + (*node, *crtc, output.output.clone(), device.drm.is_active()) + }) }) .collect::>(); let mut fallbacks = Vec::new(); @@ -1753,14 +1911,26 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan .direct_backend .as_ref() .is_some_and(|backend| backend.can_render()); - let scene = state.direct_backend.as_ref().unwrap().scene_output(state, &identity); + let scene = state + .direct_backend + .as_ref() + .unwrap() + .scene_output(state, &identity); let animating = enabled && state.output_has_animations(&scene); let backend = state.direct_backend.as_mut().unwrap(); - let output = backend.devices.get_mut(&node).unwrap().outputs.get_mut(&crtc).unwrap(); + let output = backend + .devices + .get_mut(&node) + .unwrap() + .outputs + .get_mut(&crtc) + .unwrap(); // Preserve recovery of a requested final frame if another output // advances this animation to rest before the watchdog is dispatched. let active = enabled - && (animating || (output.animation_fallback.waiting_for_frame() && output.scheduler.has_pending_request())); + && (animating + || (output.animation_fallback.waiting_for_frame() + && output.scheduler.has_pending_request())); let chained = output.render_timer.is_some() || output.frame_pending; if active && chained { backend.scheduling_metrics.chained_animation_observations += 1; @@ -1783,7 +1953,11 @@ fn animation_fallbacks(state: &mut Ferese) -> Vec<(DrmNode, crtc::Handle, Instan // from current output identities and waits so commit, replacement and teardown // cancel stale wakeups, including mirrored and sleeping outputs. fn resize_deadlines(state: &Ferese) -> Vec<(Output, Instant)> { - let Some(backend) = state.direct_backend.as_ref().filter(|backend| backend.can_render()) else { + let Some(backend) = state + .direct_backend + .as_ref() + .filter(|backend| backend.can_render()) + else { return Vec::new(); }; if state.session_lock.sleeping { @@ -1809,7 +1983,11 @@ fn arm_animation_timer(state: &mut Ferese) { let deadline = animation_fallbacks(state) .iter() .map(|(_, _, deadline)| *deadline) - .chain(resize_deadlines(state).into_iter().map(|(_, deadline)| deadline)) + .chain( + resize_deadlines(state) + .into_iter() + .map(|(_, deadline)| deadline), + ) .min(); let Some(backend) = state.direct_backend.as_mut() else { return; @@ -1887,7 +2065,12 @@ fn arm_animation_timer(state: &mut Ferese) { } } -fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, target: Duration) { +fn schedule_frame_callbacks( + state: &mut Ferese, + node: DrmNode, + crtc: crtc::Handle, + target: Duration, +) { if state.session_lock.sleeping { return; } @@ -1941,7 +2124,12 @@ fn schedule_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handl } } -fn deliver_frame_callbacks(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identity: &Output) { +fn deliver_frame_callbacks( + state: &mut Ferese, + node: DrmNode, + crtc: crtc::Handle, + identity: &Output, +) { let Some(backend) = state.direct_backend.as_mut() else { return; }; @@ -2005,7 +2193,10 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) { eligible.get(&surface.into()).cloned() }); }); - let layers = layer_map_for_output(output).layers().cloned().collect::>(); + let layers = layer_map_for_output(output) + .layers() + .cloned() + .collect::>(); layers.iter().for_each(|layer| { layer.send_frame(output, state.start_time.elapsed(), None, |surface, _| { eligible.get(&surface.into()).cloned() @@ -2083,7 +2274,8 @@ fn select_outputs( ))); } if let Some(mode) = selected_mode { - let size = output_transform(settings.transform).transform_size(OutputMode::from(mode).size); + let size = + output_transform(settings.transform).transform_size(OutputMode::from(mode).size); let width = (size.w as f64 / settings.scale).round(); let height = (size.h as f64 / settings.scale).round(); if width < 1.0 || height < 1.0 || width > i32::MAX as f64 || height > i32::MAX as f64 { @@ -2114,7 +2306,12 @@ fn select_outputs( auto_refresh: settings.auto_refresh, physical_size: connector.size(), current_mode: selected_mode.map(connected_mode_info), - available_modes: connector.modes().iter().copied().map(connected_mode_info).collect(), + available_modes: connector + .modes() + .iter() + .copied() + .map(connected_mode_info) + .collect(), scale: settings.scale, transform: settings.transform, configured_position: settings.position, @@ -2160,8 +2357,9 @@ fn select_outputs( )); } - for ((connector, mode, settings, identity, mirror_source), crtc) in - candidates.into_iter().zip(crtc_assignment::assign(&requests)) + for ((connector, mode, settings, identity, mirror_source), crtc) in candidates + .into_iter() + .zip(crtc_assignment::assign(&requests)) { if let Some(crtc) = crtc { selections.push(OutputSelection { @@ -2183,7 +2381,10 @@ fn select_outputs( // Also guard the rescan immediately before applying a device transaction: // hotplug can invalidate the earlier configuration-wide validation. validate_selected_outputs( - desired.outputs.iter().filter(|output| output.key.starts_with(&prefix)), + desired + .outputs + .iter() + .filter(|output| output.key.starts_with(&prefix)), &selected, ) .map_err(io::Error::other)?; @@ -2205,7 +2406,10 @@ fn connected_mode_info(mode: DrmMode) -> ConnectedModeInfo { } } -fn select_mode(connector: &connector::Info, requested: Option) -> Option { +fn select_mode( + connector: &connector::Info, + requested: Option, +) -> Option { let fallback = || { connector .modes() @@ -2224,11 +2428,24 @@ fn select_mode(connector: &connector::Info, requested: Option .filter(|mode| mode.size() == (requested.width, requested.height)); if let Some(refresh) = requested.refresh_millihertz { matching - .min_by_key(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh)) - .filter(|mode| OutputMode::from(**mode).refresh.unsigned_abs().abs_diff(refresh) <= 1_000) + .min_by_key(|mode| { + OutputMode::from(**mode) + .refresh + .unsigned_abs() + .abs_diff(refresh) + }) + .filter(|mode| { + OutputMode::from(**mode) + .refresh + .unsigned_abs() + .abs_diff(refresh) + <= 1_000 + }) .copied() } else { - matching.max_by_key(|mode| OutputMode::from(**mode).refresh).copied() + matching + .max_by_key(|mode| OutputMode::from(**mode).refresh) + .copied() } } @@ -2249,7 +2466,10 @@ fn create_direct_output( mirror_source, } = selection; let drm_surface = drm.create_surface(crtc, mode, &[connector.handle()])?; - let allocator = GbmAllocator::new(gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT); + let allocator = GbmAllocator::new( + gbm.clone(), + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ); let node = DrmNode::from_file(gbm.as_fd())?; let render_node = node .node_with_type(NodeType::Render) @@ -2402,18 +2622,20 @@ fn output_transform(transform: OutputTransform) -> Transform { } fn connector_edid(drm: &DrmDevice, connector: &connector::Info) -> Option> { - drm.get_properties(connector.handle()).ok().and_then(|properties| { - properties.iter().find_map(|(handle, raw)| { - let info = drm.get_property(*handle).ok()?; - if info.name().to_bytes() != b"EDID" { - return None; - } - match info.value_type().convert_value(*raw) { - property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(), - _ => None, - } + drm.get_properties(connector.handle()) + .ok() + .and_then(|properties| { + properties.iter().find_map(|(handle, raw)| { + let info = drm.get_property(*handle).ok()?; + if info.name().to_bytes() != b"EDID" { + return None; + } + match info.value_type().convert_value(*raw) { + property::Value::Blob(blob) if blob != 0 => drm.get_property_blob(blob).ok(), + _ => None, + } + }) }) - }) } fn drm_device_key(drm: &DrmDevice) -> String { @@ -2437,11 +2659,11 @@ fn resolve_output_positions( ) -> Result<(), String> { let mut placements = Vec::new(); for device in backend.devices.values() { - let scan = - select_outputs(device, &backend.monitors, desired, backend.low_power).map_err(|error| error.to_string())?; + let scan = select_outputs(device, &backend.monitors, desired, backend.low_power) + .map_err(|error| error.to_string())?; for selection in scan.selections { - let size = - output_transform(selection.settings.transform).transform_size(OutputMode::from(selection.mode).size); + let size = output_transform(selection.settings.transform) + .transform_size(OutputMode::from(selection.mode).size); let key = format!("{}/{}", drm_device_key(&device.drm), selection.connector); let previous = device .outputs @@ -2466,13 +2688,22 @@ fn resolve_output_positions( } fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> { - let backend = state.direct_backend.as_mut().ok_or("direct backend unavailable")?; + let backend = state + .direct_backend + .as_mut() + .ok_or("direct backend unavailable")?; let mut discovered = Vec::new(); for device in backend.devices.values() { let drm = &device.drm; let key = drm_device_key(drm); - for handle in drm.resource_handles().map_err(|error| error.to_string())?.connectors() { - let connector = drm.get_connector(*handle, true).map_err(|error| error.to_string())?; + for handle in drm + .resource_handles() + .map_err(|error| error.to_string())? + .connectors() + { + let connector = drm + .get_connector(*handle, true) + .map_err(|error| error.to_string())?; if connector.state() != connector::State::Connected { continue; } @@ -2529,9 +2760,11 @@ fn refresh_monitor_policy(state: &mut Ferese) -> Result<(), String> { backend.manual_outputs.internal, backend.manual_outputs.layout, ); - backend - .manual_outputs - .observe(&backend.monitors, backend.lid.closed(), &state.output_profiles); + backend.manual_outputs.observe( + &backend.monitors, + backend.lid.closed(), + &state.output_profiles, + ); if previous_override != ( backend.manual_outputs.profile.clone(), @@ -2596,16 +2829,23 @@ impl PresentationClock { fn set_refresh(&mut self, refresh_millihertz: i32) { if refresh_millihertz > 0 { - self.refresh_interval = Some(Duration::from_nanos(1_000_000_000_000_u64 / refresh_millihertz as u64)); + self.refresh_interval = Some(Duration::from_nanos( + 1_000_000_000_000_u64 / refresh_millihertz as u64, + )); } } fn record(&mut self, time: DrmEventTime, sequence: u32) -> Option { let DrmEventTime::Monotonic(time) = time else { - tracing::warn!(sequence, "DRM driver reported a realtime page-flip timestamp"); + tracing::warn!( + sequence, + "DRM driver reported a realtime page-flip timestamp" + ); return None; }; - let delta = self.last_presentation.map(|previous| time.saturating_sub(previous)); + let delta = self + .last_presentation + .map(|previous| time.saturating_sub(previous)); self.last_presentation = Some(time); self.presented_frames = self.presented_frames.saturating_add(1); @@ -2640,7 +2880,12 @@ mod tests { lid_closed: None, mirror_source: None, }; - crate::output_policy::select_profile(&monitors, false, &[profile], &ManualOverride::default()) + crate::output_policy::select_profile( + &monitors, + false, + &[profile], + &ManualOverride::default(), + ) } #[test] @@ -2649,7 +2894,11 @@ mod tests { let selected = vec!["A".into(), "B".into(), "C".into()]; assert!(validate_output_plan(&desired, &selected, || Ok(())).is_ok()); assert_eq!( - validate_output_plan(&desired, &selected, || Err("atomic TEST_ONLY failed".into())), + validate_output_plan( + &desired, + &selected, + || Err("atomic TEST_ONLY failed".into()) + ), Err("atomic TEST_ONLY failed".into()) ); } @@ -2658,7 +2907,8 @@ mod tests { fn requiredness_does_not_allow_a_connected_enabled_output_to_be_omitted() { let mut desired = three_monitor_plan(crate::config::OutputLayout::Extend); desired.outputs[2].settings.required = false; - let error = validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).unwrap_err(); + let error = + validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).unwrap_err(); assert!(error.contains("C")); desired.outputs[2].settings.enabled = false; assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok()); @@ -2687,9 +2937,16 @@ mod tests { }; for required in [true, false] { profile.outputs[2].required = required; - let desired = - crate::output_policy::select_profile(&connected, false, &[profile.clone()], &ManualOverride::default()); - assert_eq!(desired.profile.as_deref(), if required { None } else { Some("dock") }); + let desired = crate::output_policy::select_profile( + &connected, + false, + &[profile.clone()], + &ManualOverride::default(), + ); + assert_eq!( + desired.profile.as_deref(), + if required { None } else { Some("dock") } + ); assert_eq!(desired.outputs.len(), 2); assert!(validate_output_plan(&desired, &["A".into(), "B".into()], || Ok(())).is_ok()); } @@ -2740,14 +2997,15 @@ mod tests { #[test] fn feasible_three_monitor_assignment_passes_live_plan_validation() { let desired = three_monitor_plan(crate::config::OutputLayout::Extend); - let requests = [&[1, 2][..], &[1, 3][..], &[1, 3][..]].map(|compatible| crtc_assignment::Candidates { - applied: None, - kernel: None, - compatible: compatible - .iter() - .map(|value| std::num::NonZeroU32::new(*value).unwrap().into()) - .collect(), - }); + let requests = + [&[1, 2][..], &[1, 3][..], &[1, 3][..]].map(|compatible| crtc_assignment::Candidates { + applied: None, + kernel: None, + compatible: compatible + .iter() + .map(|value| std::num::NonZeroU32::new(*value).unwrap().into()) + .collect(), + }); let selected = desired .outputs .iter() @@ -2761,7 +3019,10 @@ mod tests { #[test] fn live_plan_rejects_partial_extend_and_mirror_before_hardware_validation() { - for layout in [crate::config::OutputLayout::Extend, crate::config::OutputLayout::Mirror] { + for layout in [ + crate::config::OutputLayout::Extend, + crate::config::OutputLayout::Mirror, + ] { let desired = three_monitor_plan(layout); for omitted in ["A", "B", "C"] { let selected = ["A", "B", "C"] @@ -2807,7 +3068,9 @@ mod tests { #[test] fn panel_detection_uses_connector_type_not_monitor_name() { - assert!(internal_connector(connector::Interface::EmbeddedDisplayPort)); + assert!(internal_connector( + connector::Interface::EmbeddedDisplayPort + )); assert!(internal_connector(connector::Interface::LVDS)); assert!(internal_connector(connector::Interface::DSI)); assert!(!internal_connector(connector::Interface::HDMIA)); @@ -2840,7 +3103,10 @@ mod tests { fn presentation_clock_rejects_realtime_timestamps() { let mut clock = PresentationClock::default(); - assert_eq!(clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), None); + assert_eq!( + clock.record(DrmEventTime::Realtime(SystemTime::now()), 1), + None + ); assert_eq!(clock.presented_frames, 0); } @@ -2863,7 +3129,10 @@ mod tests { clock.reset_timing(); - assert_eq!(clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), None); + assert_eq!( + clock.record(DrmEventTime::Monotonic(Duration::from_secs(10)), 2), + None + ); assert_eq!(clock.presented_frames, 2); assert_eq!(clock.missed_deadlines, 0); } diff --git a/compositor/src/backends/direct/capture.rs b/compositor/src/backends/direct/capture.rs index 67916fb..8d731ec 100644 --- a/compositor/src/backends/direct/capture.rs +++ b/compositor/src/backends/direct/capture.rs @@ -17,31 +17,32 @@ pub(crate) fn touch_cache(state: &mut Ferese) { if state.capture_last_used.replace(Instant::now()).is_some() { return; } - if let Err(error) = state - .loop_handle - .insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| { - let Some(last_used) = state.capture_last_used else { - return TimeoutAction::Drop; - }; - let idle = last_used.elapsed(); - if idle < CACHE_IDLE { - return TimeoutAction::ToDuration(CACHE_IDLE - idle); - } - // A stream can pause or fail without another repaint. Release its - // offscreen targets and filtered-scene caches from the event loop. - state.capture_last_used = None; - state.capture_render = Default::default(); - state.nested_capture_texture = None; - if let Some(backend) = state.direct_backend.as_mut() { - for device in backend.devices.values_mut() { - for output in device.outputs.values_mut() { - output.capture_texture = None; - output.mirror_capture_texture = None; + if let Err(error) = + state + .loop_handle + .insert_source(Timer::from_duration(CACHE_IDLE), |_, _, state| { + let Some(last_used) = state.capture_last_used else { + return TimeoutAction::Drop; + }; + let idle = last_used.elapsed(); + if idle < CACHE_IDLE { + return TimeoutAction::ToDuration(CACHE_IDLE - idle); + } + // A stream can pause or fail without another repaint. Release its + // offscreen targets and filtered-scene caches from the event loop. + state.capture_last_used = None; + state.capture_render = Default::default(); + state.nested_capture_texture = None; + if let Some(backend) = state.direct_backend.as_mut() { + for device in backend.devices.values_mut() { + for output in device.outputs.values_mut() { + output.capture_texture = None; + output.mirror_capture_texture = None; + } } } - } - TimeoutAction::Drop - }) + TimeoutAction::Drop + }) { state.capture_last_used = None; tracing::warn!(%error, "could not schedule capture cache cleanup"); @@ -85,9 +86,18 @@ pub(super) fn capture_mirror( } let elements = capture_output_elements(state, renderer, source, include_cursor, scene); // Never sample mirror_texture: it contains the unfiltered display. - let canvas = output.mirror_canvas.as_ref().expect("mirror display composed"); + let canvas = output + .mirror_canvas + .as_ref() + .expect("mirror display composed"); let Some(()) = capture_request(state, &output.output, include_cursor, || { - super::mirror::compose(renderer, &mut output.mirror_capture_texture, canvas, source, &elements) + super::mirror::compose( + renderer, + &mut output.mirror_capture_texture, + canvas, + source, + &elements, + ) }) else { continue; }; @@ -98,7 +108,12 @@ pub(super) fn capture_mirror( Default::default(), ); let Some(target) = capture_request(state, &output.output, include_cursor, || { - render_capture(renderer, &mut output.capture_texture, &output.output, &[element]) + render_capture( + renderer, + &mut output.capture_texture, + &output.output, + &[element], + ) }) else { continue; }; @@ -168,7 +183,10 @@ mod tests { #[test] #[ignore = "requires an EGL rendering device"] fn capture_recomposes_cursor_without_touching_the_display_target() { - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; @@ -223,18 +241,32 @@ mod tests { ] { let target = render_capture(&mut renderer, &mut capture, &output, &elements).unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((32, 24).into()), + Fourcc::Abgr8888, + ) .unwrap(); let pixels = renderer.map_texture(&mapping).unwrap(); assert_eq!(&pixels[..4], &expected); - assert_eq!(&pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], &[255, 0, 0, 255]); + assert_eq!( + &pixels[(12 * 32 + 16) * 4..(12 * 32 + 16) * 4 + 4], + &[255, 0, 0, 255] + ); drop(target); let target = renderer.bind(displayed.as_mut().unwrap()).unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((32, 24).into()), + Fourcc::Abgr8888, + ) .unwrap(); - assert_eq!(&renderer.map_texture(&mapping).unwrap()[..4], &[255, 0, 0, 255]); + assert_eq!( + &renderer.map_texture(&mapping).unwrap()[..4], + &[255, 0, 0, 255] + ); } output.change_current_state( @@ -246,7 +278,9 @@ mod tests { None, None, ); - let _ = render_capture::(&mut renderer, &mut capture, &output, &[]).unwrap(); + let _ = + render_capture::(&mut renderer, &mut capture, &output, &[]) + .unwrap(); assert_eq!(capture.as_ref().unwrap().size(), (48, 16).into()); } } @@ -318,7 +352,10 @@ mod failure_tests { Ok::<(), ()>(()) }; after_queue(queued, || { - assert!(submitted.get(), "capture must run after the display queue succeeds"); + assert!( + submitted.get(), + "capture must run after the display queue succeeds" + ); assert!( capture_request(&mut state, &output, false, || Err::<(), _>( "injected allocation failure" @@ -328,14 +365,20 @@ mod failure_tests { }) .unwrap(); - assert!(submitted.get(), "capture failure must not undo queued display work"); + assert!( + submitted.get(), + "capture failure must not undo queued display work" + ); assert_eq!(state.pending_screencopies.len(), 1); assert_eq!(state.pending_screencopies[0].output, other_output); let outcome = receiver.try_recv().unwrap(); assert_eq!(outcome.request, 41); assert_eq!(outcome.part, 0); assert!(outcome.result.is_err()); - assert!(receiver.try_recv().is_err(), "request should fail exactly once"); + assert!( + receiver.try_recv().is_err(), + "request should fail exactly once" + ); assert!( other_receiver.try_recv().is_err(), "other output's request must stay pending" diff --git a/compositor/src/backends/direct/crtc_assignment.rs b/compositor/src/backends/direct/crtc_assignment.rs index 3df6e60..527c3d7 100644 --- a/compositor/src/backends/direct/crtc_assignment.rs +++ b/compositor/src/backends/direct/crtc_assignment.rs @@ -130,17 +130,31 @@ mod tests { #[test] fn every_connector_order_finds_a_complete_matching() { let graph = [&[1, 2][..], &[1, 3][..], &[1, 3][..]]; - for order in [[0, 1, 2], [0, 2, 1], [1, 0, 2], [1, 2, 0], [2, 0, 1], [2, 1, 0]] { + for order in [ + [0, 1, 2], + [0, 2, 1], + [1, 0, 2], + [1, 2, 0], + [2, 0, 1], + [2, 1, 0], + ] { let requests = order.map(|index| candidates(None, None, graph[index])); let result = assign(&requests); assert_eq!(result.iter().flatten().count(), 3, "order {order:?}"); - assert_eq!(assign(&requests), result, "deterministic for the same inventory"); + assert_eq!( + assign(&requests), + result, + "deterministic for the same inventory" + ); } } #[test] fn equal_preferences_use_connector_order() { - let result = assign(&[candidates(Some(1), None, &[1, 2]), candidates(Some(1), None, &[1, 2])]); + let result = assign(&[ + candidates(Some(1), None, &[1, 2]), + candidates(Some(1), None, &[1, 2]), + ]); assert_eq!(result, vec![Some(crtc(1)), Some(crtc(2))]); } @@ -159,18 +173,29 @@ mod tests { #[test] fn maximum_cardinality_and_preference_objective_match_exhaustive_oracle() { - fn score(requests: &[Candidates], assignment: &[Option]) -> (usize, usize, usize) { + fn score( + requests: &[Candidates], + assignment: &[Option], + ) -> (usize, usize, usize) { let retained = [true, false].map(|applied| { requests .iter() .zip(assignment) .filter(|(request, actual)| { - let preferred = if applied { request.applied } else { request.kernel }; + let preferred = if applied { + request.applied + } else { + request.kernel + }; preferred.is_some() && preferred == **actual }) .count() }); - (assignment.iter().flatten().count(), retained[0], retained[1]) + ( + assignment.iter().flatten().count(), + retained[0], + retained[1], + ) } fn enumerate( @@ -204,7 +229,8 @@ mod tests { .filter(|crtc| graph & (1 << (index * 3 + crtc - 1)) != 0) .collect::>(); let applied = ((seed + index) % 4 != 0).then_some((seed + index) % 4); - let kernel = ((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4); + let kernel = + ((seed * 3 + index) % 4 != 0).then_some((seed * 3 + index) % 4); candidates(applied, kernel, &compatible) }) .collect::>(); @@ -213,13 +239,14 @@ mod tests { let actual = assign(&requests); let used = actual.iter().flatten().collect::>(); assert_eq!(used.len(), actual.iter().flatten().count()); - assert!( - requests - .iter() - .zip(&actual) - .all(|(request, crtc)| crtc.is_none_or(|crtc| request.compatible.contains(&crtc))) + assert!(requests.iter().zip(&actual).all(|(request, crtc)| { + crtc.is_none_or(|crtc| request.compatible.contains(&crtc)) + })); + assert_eq!( + score(&requests, &actual), + best, + "graph {graph}, preferences {seed}" ); - assert_eq!(score(&requests, &actual), best, "graph {graph}, preferences {seed}"); } } } @@ -250,10 +277,16 @@ mod tests { // The internal panel appears first in the DRM connector inventory. let internal = candidates(None, None, &[151, 270]); let external = candidates(Some(151), Some(151), &[151, 270]); - assert_eq!(assign(&[internal, external]), vec![Some(crtc(270)), Some(crtc(151))]); + assert_eq!( + assign(&[internal, external]), + vec![Some(crtc(270)), Some(crtc(151))] + ); let internal = candidates(None, None, &[151, 270]); let external = candidates(Some(151), Some(151), &[151, 270]); - assert_eq!(assign(&[external, internal]), vec![Some(crtc(151)), Some(crtc(270))]); + assert_eq!( + assign(&[external, internal]), + vec![Some(crtc(151)), Some(crtc(270))] + ); } #[test] @@ -304,7 +337,10 @@ mod tests { #[test] fn restricted_connector_gets_its_only_free_crtc() { assert_eq!( - assign(&[candidates(None, None, &[151, 270]), candidates(None, None, &[151])]), + assign(&[ + candidates(None, None, &[151, 270]), + candidates(None, None, &[151]) + ]), vec![Some(crtc(270)), Some(crtc(151))] ); } @@ -312,7 +348,10 @@ mod tests { #[test] fn exhausted_crtcs_do_not_displace_surviving_output_or_assign_twice() { assert_eq!( - assign(&[candidates(None, None, &[151]), candidates(Some(151), Some(151), &[151])]), + assign(&[ + candidates(None, None, &[151]), + candidates(Some(151), Some(151), &[151]) + ]), vec![None, Some(crtc(151))] ); assert!(assign(&[]).is_empty()); diff --git a/compositor/src/backends/direct/diagnostics.rs b/compositor/src/backends/direct/diagnostics.rs index 283d3e7..9467388 100644 --- a/compositor/src/backends/direct/diagnostics.rs +++ b/compositor/src/backends/direct/diagnostics.rs @@ -54,7 +54,10 @@ pub(super) struct Diagnostics { impl Diagnostics { pub fn from_environment(output: String) -> Self { - Self::new(output, std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some()) + Self::new( + output, + std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some(), + ) } fn new(output: String, enabled: bool) -> Self { @@ -191,9 +194,18 @@ mod tests { #[test] fn primary_classification_does_not_infer_scanout_from_zero_damage() { - assert_eq!(PrimaryOutcome::classify(true, false), PrimaryOutcome::DirectScanout); - assert_eq!(PrimaryOutcome::classify(false, true), PrimaryOutcome::Composed); - assert_eq!(PrimaryOutcome::classify(false, false), PrimaryOutcome::ReusedSwapchain); + assert_eq!( + PrimaryOutcome::classify(true, false), + PrimaryOutcome::DirectScanout + ); + assert_eq!( + PrimaryOutcome::classify(false, true), + PrimaryOutcome::Composed + ); + assert_eq!( + PrimaryOutcome::classify(false, false), + PrimaryOutcome::ReusedSwapchain + ); } #[test] @@ -224,7 +236,10 @@ mod tests { let mut d = Diagnostics::new("test".into(), true); let queued = Err::<(), _>("queue failed"); d.queue_result(&queued, PrimaryOutcome::DirectScanout, true, true); - assert!(super::super::capture::after_queue(queued, || panic!("failed queues must not capture")).is_err()); + assert!( + super::super::capture::after_queue(queued, || panic!("failed queues must not capture")) + .is_err() + ); assert_eq!(d.counters.submissions, 0); assert_eq!(d.counters.scanout_submissions, 0); assert_eq!(d.counters.cursor_plane_submissions, 0); diff --git a/compositor/src/backends/direct/feedback.rs b/compositor/src/backends/direct/feedback.rs index 1410473..d733b1c 100644 --- a/compositor/src/backends/direct/feedback.rs +++ b/compositor/src/backends/direct/feedback.rs @@ -72,17 +72,27 @@ impl OutputFeedback { // Restrict the preference to the observed primary format/modifier: // without ALLOW_PRIMARY_PLANE_SCANOUT_ANY Smithay compares the full // format with the swapchain. Renderer-only formats remain in fallback. - match DmabufFeedbackBuilder::new(self.render_device, self.render_formats.iter().copied()) - .add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6) - .build() + match DmabufFeedbackBuilder::new( + self.render_device, + self.render_formats.iter().copied(), + ) + .add_preference_tranche(self.scanout_device, TrancheFlags::Scanout, [format], 4..=6) + .build() { Ok(feedback) => self.preferred = Some(feedback), - Err(error) => tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback"), + Err(error) => { + tracing::warn!(%error, "output DMA-BUF feedback unavailable; retaining render fallback") + } } } } - pub fn observed_frame(&mut self, format: Option, states: &RenderElementStates, candidates: HashSet) { + pub fn observed_frame( + &mut self, + format: Option, + states: &RenderElementStates, + candidates: HashSet, + ) { self.observe_format(format); self.visible = states .states @@ -203,13 +213,17 @@ pub(crate) fn refresh(state: &mut Ferese) { .filter(|id| { state .current_window_presentation(*id) - .is_some_and(|presentation| presentation.alpha() == 1.0 && presentation.dim == 0.0) + .is_some_and(|presentation| { + presentation.alpha() == 1.0 && presentation.dim == 0.0 + }) }) .and_then(|id| state.windows.window(id)) .and_then(|window| window.toplevel()) .filter(|toplevel| { - with_renderer_surface_state(toplevel.wl_surface(), |state| state.buffer().is_some()) - .unwrap_or(false) + with_renderer_surface_state(toplevel.wl_surface(), |state| { + state.buffer().is_some() + }) + .unwrap_or(false) }) .map(|toplevel| Id::from(toplevel.wl_surface())) }) @@ -251,8 +265,10 @@ mod tests { let feedback = DmabufFeedbackBuilder::new(10, [format(Fourcc::Argb8888, Modifier::Linear)]) .build() .unwrap(); - let global = state.create_global_with_default_feedback::(&display.handle(), &feedback); - let other = state.create_global_with_default_feedback::(&display.handle(), &feedback); + let global = + state.create_global_with_default_feedback::(&display.handle(), &feedback); + let other = + state.create_global_with_default_feedback::(&display.handle(), &feedback); (display, global, other) } fn visible(output: &mut OutputFeedback, id: &Id, f: Format) { @@ -273,11 +289,31 @@ mod tests { let linear = format(Fourcc::Argb8888, Modifier::Linear); let implicit = format(Fourcc::Argb8888, Modifier::Invalid); let opaque = format(Fourcc::Xrgb8888, Modifier::Linear); - assert!(compatible(&formats(&[linear, implicit]), &formats(&[linear]), linear)); - assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), linear)); - assert!(!compatible(&formats(&[linear]), &formats(&[opaque]), linear)); - assert!(!compatible(&formats(&[linear]), &formats(&[implicit]), implicit)); - assert!(!compatible(&formats(&[opaque]), &formats(&[linear]), linear)); + assert!(compatible( + &formats(&[linear, implicit]), + &formats(&[linear]), + linear + )); + assert!(!compatible( + &formats(&[linear]), + &formats(&[implicit]), + linear + )); + assert!(!compatible( + &formats(&[linear]), + &formats(&[opaque]), + linear + )); + assert!(!compatible( + &formats(&[linear]), + &formats(&[implicit]), + implicit + )); + assert!(!compatible( + &formats(&[opaque]), + &formats(&[linear]), + linear + )); } #[test] @@ -307,7 +343,11 @@ mod tests { let mut b = output(&[f], &[f]); visible(&mut a, &id, f); visible(&mut b, &id, f); - fn view(global: DmabufGlobal, feedback: &OutputFeedback, eligible: Option) -> OutputView<'_> { + fn view( + global: DmabufGlobal, + feedback: &OutputFeedback, + eligible: Option, + ) -> OutputView<'_> { OutputView { global, feedback, @@ -320,7 +360,10 @@ mod tests { select( global, &id, - [view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))] + [ + view(global, &a, Some(id.clone())), + view(global, &b, Some(id.clone())) + ] ) .is_none() ); @@ -331,7 +374,10 @@ mod tests { select( global, &id, - [view(global, &a, Some(id.clone())), view(global, &b, Some(id.clone()))] + [ + view(global, &a, Some(id.clone())), + view(global, &b, Some(id.clone())) + ] ) .is_some() ); @@ -347,7 +393,12 @@ mod tests { let mut o = output(&[f], &[f]); visible(&mut o, &id, f); o.invalidate(); - assert!(o.observed.is_none() && o.preferred.is_none() && o.visible.is_empty() && o.candidates.is_empty()); + assert!( + o.observed.is_none() + && o.preferred.is_none() + && o.visible.is_empty() + && o.candidates.is_empty() + ); o.observe_format(None); assert!(o.preferred.is_none()); visible(&mut o, &id, f); @@ -390,9 +441,12 @@ mod tests { "clipped storage must remain hidden" ); let faded = solid(bounds, 0.8); - assert!(!candidate_geometry(&faded, bounds, scale.into(), || panic!( - "faded elements must not probe storage" - ))); + assert!(!candidate_geometry( + &faded, + bounds, + scale.into(), + || panic!("faded elements must not probe storage") + )); let mut partial = bounds; partial.size.w -= 1; assert!(!candidate_geometry( @@ -474,7 +528,9 @@ mod tests { let f = format(Fourcc::Argb8888, Modifier::Linear); let id = Id::new(); let mut o = output(&[f], &[f]); - let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || visible(&mut o, &id, f)); + let failed = super::super::capture::after_queue(Err::<(), _>("queue failed"), || { + visible(&mut o, &id, f) + }); assert!(failed.is_err()); assert!(o.visible.is_empty() && o.preferred.is_none()); super::super::capture::after_queue(Ok::<(), ()>(()), || visible(&mut o, &id, f)).unwrap(); diff --git a/compositor/src/backends/direct/gamma.rs b/compositor/src/backends/direct/gamma.rs index eeb0cfb..162fba3 100644 --- a/compositor/src/backends/direct/gamma.rs +++ b/compositor/src/backends/direct/gamma.rs @@ -24,8 +24,14 @@ struct Transition { // A single owner determines both the desired ramp and who loses control on failure. enum Source { Original, - Client { control: ZwlrGammaControlV1, ramp: Ramp }, - NightLight { temperature: u16, ramp: Ramp }, + Client { + control: ZwlrGammaControlV1, + ramp: Ramp, + }, + NightLight { + temperature: u16, + ramp: Ramp, + }, } #[derive(Default)] @@ -136,7 +142,8 @@ impl Gamma { fn configure(&mut self, settings: Settings, duration: Duration, now: Instant) { if settings.enabled { - if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature) { + if matches!(self.source, Source::NightLight { temperature, .. } if temperature == settings.temperature) + { return; } self.replace_source(Source::NightLight { @@ -152,11 +159,14 @@ impl Gamma { Source::Original => &self.original, Source::NightLight { ramp, .. } | Source::Client { ramp, .. } => ramp, }; - self.transition = (!duration.is_zero() && !self.suspended && self.current != *target).then(|| Transition { - from: self.current.clone(), - started: now, - duration, - }); + self.transition = + (!duration.is_zero() && !self.suspended && self.current != *target).then(|| { + Transition { + from: self.current.clone(), + started: now, + duration, + } + }); } fn owns(&self, owner: &ZwlrGammaControlV1) -> bool { @@ -181,8 +191,10 @@ impl Gamma { }; let animated; let ramp = if let Some(transition) = &self.transition { - let progress = - now.saturating_duration_since(transition.started).as_secs_f64() / transition.duration.as_secs_f64(); + let progress = now + .saturating_duration_since(transition.started) + .as_secs_f64() + / transition.duration.as_secs_f64(); if progress >= 1. { self.transition = None; ramp @@ -222,15 +234,26 @@ impl Drop for Gamma { } } -pub(crate) fn acquire(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option { +pub(crate) fn acquire( + state: &mut Ferese, + target: &Output, + owner: &ZwlrGammaControlV1, +) -> Option { // Reconcile a just-reloaded configuration before assigning ownership. sync_night_light(state); if state.night_light.enabled { return None; } let duration = state.animation_duration(TRANSITION_DURATION); - let backend = state.direct_backend.as_mut().filter(|backend| backend.active)?; - for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { + let backend = state + .direct_backend + .as_mut() + .filter(|backend| backend.active)?; + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { for (crtc, output) in &mut device.outputs { if &output.output != target || output.global.is_none() { continue; @@ -255,28 +278,41 @@ pub(crate) fn sync_night_light(state: &mut Ferese) { if !state.gamma_dirty { return; } - if !state.direct_backend.as_ref().is_some_and(|backend| backend.active) { + if !state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.active) + { return; } state.gamma_dirty = false; if advance_night_light(state, true) && !state.gamma_timer_pending { - let result = state - .loop_handle - .insert_source(Timer::from_duration(TRANSITION_INTERVAL), |_, _, state| { + let result = state.loop_handle.insert_source( + Timer::from_duration(TRANSITION_INTERVAL), + |_, _, state| { if advance_night_light(state, false) { TimeoutAction::ToDuration(TRANSITION_INTERVAL) } else { state.gamma_timer_pending = false; TimeoutAction::Drop } - }); + }, + ); match result { Ok(_) => state.gamma_timer_pending = true, Err(error) => { tracing::warn!(%error, "could not schedule night-light transition"); if let Some(backend) = &mut state.direct_backend { - for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { - for output in device.outputs.values_mut().filter(|output| !output.power.needs_wake()) { + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { + for output in device + .outputs + .values_mut() + .filter(|output| !output.power.needs_wake()) + { if let Some(gamma) = output.gamma.controller_mut() { gamma.transition = None; gamma.resume(Instant::now()); @@ -292,11 +328,19 @@ pub(crate) fn sync_night_light(state: &mut Ferese) { fn advance_night_light(state: &mut Ferese, force: bool) -> bool { let duration = state.animation_duration(TRANSITION_DURATION); let now = Instant::now(); - let Some(backend) = state.direct_backend.as_mut().filter(|backend| backend.active) else { + let Some(backend) = state + .direct_backend + .as_mut() + .filter(|backend| backend.active) + else { return false; }; let mut pending = false; - for device in backend.devices.values_mut().filter(|device| device.drm.is_active()) { + for device in backend + .devices + .values_mut() + .filter(|device| device.drm.is_active()) + { for (crtc, output) in &mut device.outputs { let ready = !output.power.needs_wake(); let gamma = if ready { @@ -346,7 +390,11 @@ pub(crate) fn night_light_status(state: &Ferese) -> Status { status } -fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option<(&'a mut Gamma, bool)> { +fn owned<'a>( + state: &'a mut Ferese, + target: &Output, + owner: &ZwlrGammaControlV1, +) -> Option<(&'a mut Gamma, bool)> { let duration = state.animation_duration(TRANSITION_DURATION); let backend = state.direct_backend.as_mut()?; for device in backend.devices.values_mut() { @@ -362,7 +410,11 @@ fn owned<'a>(state: &'a mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) None } -pub(crate) fn size(state: &mut Ferese, target: &Output, owner: &ZwlrGammaControlV1) -> Option { +pub(crate) fn size( + state: &mut Ferese, + target: &Output, + owner: &ZwlrGammaControlV1, +) -> Option { owned(state, target, owner).map(|(gamma, _)| gamma.original.size()) } diff --git a/compositor/src/backends/direct/lid.rs b/compositor/src/backends/direct/lid.rs index a1e1bd5..a8a4168 100644 --- a/compositor/src/backends/direct/lid.rs +++ b/compositor/src/backends/direct/lid.rs @@ -182,9 +182,13 @@ impl Reader { "block", ), ) - .and_then(|reply| reply.body().deserialize::()); + .and_then(|reply| { + reply.body().deserialize::() + }); match inhibitor { - Ok(descriptor) => worker_lease.lock().unwrap().accept(generation, descriptor), + Ok(descriptor) => { + worker_lease.lock().unwrap().accept(generation, descriptor) + } Err(error) => tracing::warn!(%error, "could not own logind lid policy"), } continue; @@ -302,16 +306,17 @@ fn request(state: &mut Ferese, reactivate: bool, wake: bool) { backend.lid_reader.lease.lock().unwrap().suspend = false; let probe = backend.lid.begin(reactivate, wake); let sent = backend.lid_reader.requests.send(probe).is_ok(); - let timer = state - .loop_handle - .insert_source(Timer::from_duration(Duration::from_secs(3)), move |_, _, state| { + let timer = state.loop_handle.insert_source( + Timer::from_duration(Duration::from_secs(3)), + move |_, _, state| { if let Some(backend) = state.direct_backend.as_mut() { backend.lid_reader.deadline = None; } tracing::warn!("lid refresh deadline elapsed; retaining the latest observed state"); complete_refresh(state, probe, None); TimeoutAction::Drop - }); + }, + ); if sent && let Ok(token) = timer { state.direct_backend.as_mut().unwrap().lid_reader.deadline = Some(token); diff --git a/compositor/src/backends/direct/mirror.rs b/compositor/src/backends/direct/mirror.rs index 3c04a25..60c5e7d 100644 --- a/compositor/src/backends/direct/mirror.rs +++ b/compositor/src/backends/direct/mirror.rs @@ -6,7 +6,8 @@ use smithay::utils::{Buffer, Physical, Rectangle}; /// Fit the complete source into the transformed target; black letterbox, never crop. fn fit(source: (i32, i32), target: (i32, i32)) -> Rectangle { - let scale = (target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64); + let scale = + (target.0 as f64 / source.0.max(1) as f64).min(target.1 as f64 / source.1.max(1) as f64); let width = (source.0 as f64 * scale).round().max(1.0) as i32; let height = (source.1 as f64 * scale).round().max(1.0) as i32; Rectangle::new( @@ -21,7 +22,10 @@ pub(super) fn elements( source: &Output, frame: &crate::state::FrameScene, target: &mut DirectOutput, -) -> Result, smithay::backend::renderer::gles::GlesError> { +) -> Result< + Vec, + smithay::backend::renderer::gles::GlesError, +> { let elements = sampled_output_elements(state, renderer, source, true, frame); // Target renderer owns this texture, including when source and target use different GPUs. let canvas = target.mirror_canvas.get_or_insert_with(|| { @@ -36,7 +40,13 @@ pub(super) fn elements( }, ) }); - compose(renderer, &mut target.mirror_texture, canvas, source, &elements)?; + compose( + renderer, + &mut target.mirror_texture, + canvas, + source, + &elements, + )?; let texture = target .mirror_texture .as_ref() @@ -44,7 +54,13 @@ pub(super) fn elements( .clone(); target.mirror_commit.increment(); Ok(vec![ - fitted_texture(texture, &target.output, target.mirror_id.clone(), target.mirror_commit).into(), + fitted_texture( + texture, + &target.output, + target.mirror_id.clone(), + target.mirror_commit, + ) + .into(), ]) } @@ -141,19 +157,20 @@ mod tests { None, ); let canvas = output("canvas"); - let colors: Vec = [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])] - .into_iter() - .map(|(y, color)| { - SolidColorRenderElement::new( - Id::new(), - Rectangle::new((0, y).into(), (32, 32).into()), - CommitCounter::default(), - Color32F::from(color), - Kind::Unspecified, - ) - .into() - }) - .collect(); + let colors: Vec = + [(0, [1., 0., 0., 1.]), (32, [0., 0., 1., 1.])] + .into_iter() + .map(|(y, color)| { + SolidColorRenderElement::new( + Id::new(), + Rectangle::new((0, y).into(), (32, 32).into()), + CommitCounter::default(), + Color32F::from(color), + Kind::Unspecified, + ) + .into() + }) + .collect(); let mut texture = None; compose(&mut renderer, &mut texture, &canvas, &source, &colors).unwrap(); assert_eq!(texture.as_ref().unwrap().size(), (32, 64).into()); @@ -175,9 +192,15 @@ mod tests { CommitCounter::default(), ); let mut target_texture = None; - let framebuffer = capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]).unwrap(); + let framebuffer = + capture::render_capture(&mut renderer, &mut target_texture, &target, &[element]) + .unwrap(); let mapping = renderer - .copy_framebuffer(&framebuffer, Rectangle::from_size((96, 64).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &framebuffer, + Rectangle::from_size((96, 64).into()), + Fourcc::Abgr8888, + ) .unwrap(); let pixels = renderer.map_texture(&mapping).unwrap(); let red = pixels diff --git a/compositor/src/backends/direct/recovery.rs b/compositor/src/backends/direct/recovery.rs index e0aad1a..b874669 100644 --- a/compositor/src/backends/direct/recovery.rs +++ b/compositor/src/backends/direct/recovery.rs @@ -8,7 +8,8 @@ pub(super) fn inject(mode: &str) -> bool { use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; - static MODE: LazyLock> = LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok()); + static MODE: LazyLock> = + LazyLock::new(|| std::env::var("FERESE_TEST_DRM_FAILURE").ok()); static USED: AtomicBool = AtomicBool::new(false); MODE.as_deref() == Some(mode) && !USED.swap(true, Ordering::AcqRel) } @@ -57,31 +58,38 @@ pub(super) fn arm_completion( } let identity = output.output.clone(); - match handle.insert_source(Timer::from_duration(COMPLETION_TIMEOUT), move |_, _, state| { - let Some(output) = state - .direct_backend - .as_mut() - .and_then(|backend| backend.devices.get_mut(&node)) - .and_then(|device| device.outputs.get_mut(&crtc)) - .filter(|output| output.output == identity) - else { - return TimeoutAction::Drop; - }; + match handle.insert_source( + Timer::from_duration(COMPLETION_TIMEOUT), + move |_, _, state| { + let Some(output) = state + .direct_backend + .as_mut() + .and_then(|backend| backend.devices.get_mut(&node)) + .and_then(|device| device.outputs.get_mut(&crtc)) + .filter(|output| output.output == identity) + else { + return TimeoutAction::Drop; + }; - if let Some(remaining) = output.completion.remaining(Instant::now()) { - if !remaining.is_zero() { - return TimeoutAction::ToDuration(remaining); + if let Some(remaining) = output.completion.remaining(Instant::now()) { + if !remaining.is_zero() { + return TimeoutAction::ToDuration(remaining); + } + + output.completion_timer = None; + tracing::error!( + ?node, + ?crtc, + "DRM frame completion timed out; reconciling device" + ); + reconcile_device(state, node); + } else { + output.completion_timer = None; } - output.completion_timer = None; - tracing::error!(?node, ?crtc, "DRM frame completion timed out; reconciling device"); - reconcile_device(state, node); - } else { - output.completion_timer = None; - } - - TimeoutAction::Drop - }) { + TimeoutAction::Drop + }, + ) { Ok(token) => { output.completion_timer = Some(token); true @@ -106,10 +114,12 @@ pub(super) fn reconcile_device(state: &mut Ferese, node: DrmNode) { // sessions retain the invalidation for fresh enumeration on activation. let active = backend.active; if active { - match state.loop_handle.insert_source(Timer::immediate(), |_, _, state| { - reconcile_outputs(state, false); - TimeoutAction::Drop - }) { + match state + .loop_handle + .insert_source(Timer::immediate(), |_, _, state| { + reconcile_outputs(state, false); + TimeoutAction::Drop + }) { Ok(_) => (), Err(error) => tracing::error!(%error, "could not schedule DRM device recovery"), } @@ -125,10 +135,14 @@ mod tests { let now = Instant::now(); let mut completion = Completion::default(); completion.queued(now); - let (feedback, retired, error) = retirement::<(), _>(Err("injected KMS retirement failure")); + let (feedback, retired, error) = + retirement::<(), _>(Err("injected KMS retirement failure")); assert_eq!(feedback, None); assert!(!retired); - assert!(error.is_some(), "the production handler must reconcile the device"); + assert!( + error.is_some(), + "the production handler must reconcile the device" + ); assert_eq!(completion.remaining(now), Some(COMPLETION_TIMEOUT)); assert_eq!(retirement::<(), &str>(Ok(None)), (None, false, None)); assert_eq!(retirement::<(), &str>(Ok(Some(()))), (Some(()), true, None)); @@ -148,7 +162,10 @@ mod tests { Some(Duration::from_millis(1)) ); // A timeout observes uncertainty; it never frees the current buffer. - assert_eq!(completion.remaining(now + COMPLETION_TIMEOUT), Some(Duration::ZERO)); + assert_eq!( + completion.remaining(now + COMPLETION_TIMEOUT), + Some(Duration::ZERO) + ); assert_eq!( completion.remaining(now + COMPLETION_TIMEOUT + Duration::from_secs(1)), Some(Duration::ZERO) diff --git a/compositor/src/backends/direct/redraw.rs b/compositor/src/backends/direct/redraw.rs index f2f68be..96ba9b1 100644 --- a/compositor/src/backends/direct/redraw.rs +++ b/compositor/src/backends/direct/redraw.rs @@ -13,7 +13,13 @@ impl AnimationFallback { self.orphaned_since.is_some() } - pub fn deadline(&mut self, now: Instant, interval: Duration, active: bool, chained: bool) -> Option { + pub fn deadline( + &mut self, + now: Instant, + interval: Duration, + active: bool, + chained: bool, + ) -> Option { if !active || chained { self.orphaned_since = None; return None; @@ -55,12 +61,21 @@ impl SchedulingMetrics { } } - pub fn redraw(&mut self, source: &'static Location<'static>, global: bool, requested: usize, connected: usize) { + pub fn redraw( + &mut self, + source: &'static Location<'static>, + global: bool, + requested: usize, + connected: usize, + ) { if !self.enabled { return; } - let count = self.redraws.entry((source.file(), source.line(), global)).or_default(); + let count = self + .redraws + .entry((source.file(), source.line(), global)) + .or_default(); count.calls += 1; count.requested_outputs += requested as u64; count.connected_outputs += connected as u64; @@ -119,8 +134,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output:: .map(|attributes| attributes.lock().unwrap().hotspot) .unwrap_or_default() }); - smithay::desktop::utils::bbox_from_surface_tree(surface, (location - hotspot.to_f64()).to_i32_round()) - .to_f64() + smithay::desktop::utils::bbox_from_surface_tree( + surface, + (location - hotspot.to_f64()).to_i32_round(), + ) + .to_f64() } CursorImageStatus::Named(_) => { @@ -135,7 +153,11 @@ pub(super) fn cursor_on_output(state: &crate::Ferese, output: &smithay::output:: // One logical pixel covers physical rounding at fractional scales. let bounds = Rectangle::::new( (f64::from(bounds.loc.x) - 1.0, f64::from(bounds.loc.y) - 1.0).into(), - (f64::from(bounds.size.w) + 2.0, f64::from(bounds.size.h) + 2.0).into(), + ( + f64::from(bounds.size.w) + 2.0, + f64::from(bounds.size.h) + 2.0, + ) + .into(), ); rect.overlaps(bounds) } @@ -152,7 +174,10 @@ mod tests { let mut idle_fast = AnimationFallback::default(); let mut moving_slow = AnimationFallback::default(); assert_eq!(idle_fast.deadline(now, fast, false, false), None); - assert_eq!(moving_slow.deadline(now, slow, true, false), Some(now + slow * 2)); + assert_eq!( + moving_slow.deadline(now, slow, true, false), + Some(now + slow * 2) + ); assert_eq!( moving_slow.deadline(now + slow, slow, true, false), Some(now + slow * 2) @@ -166,7 +191,10 @@ mod tests { let interval = Duration::from_millis(16); let mut output = AnimationFallback::default(); output.deadline(now, interval, true, false); - assert_eq!(output.deadline(now + interval, interval, false, false), None); + assert_eq!( + output.deadline(now + interval, interval, false, false), + None + ); let reconnected = now + Duration::from_secs(1); assert_eq!( output.deadline(reconnected, interval, true, false), @@ -192,7 +220,10 @@ mod tests { last_tick_us = now_us; } - assert_eq!(policy.deadline(start, Duration::from_micros(16_667), true, true), None); + assert_eq!( + policy.deadline(start, Duration::from_micros(16_667), true, true), + None + ); } assert_eq!(old_wakes, 60); @@ -210,9 +241,17 @@ mod tests { assert_eq!(fallback.deadline(start, interval, true, true), None); let plan = scheduler.begin().unwrap(); assert_eq!(plan, scheduled); - scheduler.rendered(plan, Duration::from_millis(2), Duration::from_millis(2), true); + scheduler.rendered( + plan, + Duration::from_millis(2), + Duration::from_millis(2), + true, + ); assert_eq!(scheduler.plan(interval), None); - assert_eq!(fallback.deadline(start + interval, interval, true, true), None); + assert_eq!( + fallback.deadline(start + interval, interval, true, true), + None + ); scheduler.presented(interval); assert_eq!( fallback.deadline(start + interval, interval, true, false), @@ -220,7 +259,10 @@ mod tests { ); scheduler.request(interval); assert!(scheduler.plan(interval).is_some()); - assert_eq!(fallback.deadline(start + interval, interval, true, true), None); + assert_eq!( + fallback.deadline(start + interval, interval, true, true), + None + ); } #[test] @@ -237,13 +279,17 @@ mod tests { assert!(scheduler.has_pending_request()); let deadline = fallback.deadline(start, interval, true, false).unwrap(); let animation_active = false; - let needs_final_frame = animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request()); + let needs_final_frame = + animation_active || (fallback.waiting_for_frame() && scheduler.has_pending_request()); assert_eq!( fallback.deadline(deadline, interval, needs_final_frame, false), Some(deadline) ); // A successful retry installs the normal timer and cancels fallback. - assert_eq!(fallback.deadline(deadline, interval, needs_final_frame, true), None); + assert_eq!( + fallback.deadline(deadline, interval, needs_final_frame, true), + None + ); scheduler.begin().unwrap(); assert!(!scheduler.has_pending_request()); } @@ -296,7 +342,11 @@ mod tests { refresh, }), None, - Some(smithay::output::Scale::Fractional(if index == 0 { 2.0 } else { 1.0 })), + Some(smithay::output::Scale::Fractional(if index == 0 { + 2.0 + } else { + 1.0 + })), Some(((index as i32) * 1920, 0).into()), ); state.space.map_output(&output, ((index as i32) * 1920, 0)); @@ -342,7 +392,10 @@ mod tests { }, ); assert!(cursor_on_output(&state, &outputs[0])); - assert!(cursor_on_output(&state, &outputs[1]), "cursor image straddles the seam"); + assert!( + cursor_on_output(&state, &outputs[1]), + "cursor image straddles the seam" + ); if let Some(cursor) = state.named_cursor_frame() { let x = 1920.0 - f64::from(cursor.size.w) / f64::from(cursor.buffer_scale) * 0.75 + f64::from(cursor.hotspot.x) / f64::from(cursor.buffer_scale); diff --git a/compositor/src/backends/direct/scheduling.rs b/compositor/src/backends/direct/scheduling.rs index 27e44b9..9cd4e22 100644 --- a/compositor/src/backends/direct/scheduling.rs +++ b/compositor/src/backends/direct/scheduling.rs @@ -138,8 +138,15 @@ fn dispatch_frame(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle, identit arm_animation_timer(state); } -pub(super) fn record_frame_schedule(output: &mut DirectOutput, plan: FramePlan, cost: Duration, submitted: bool) { - output.scheduler.rendered(plan, monotonic_now(), cost, submitted); +pub(super) fn record_frame_schedule( + output: &mut DirectOutput, + plan: FramePlan, + cost: Duration, + submitted: bool, +) { + output + .scheduler + .rendered(plan, monotonic_now(), cost, submitted); if std::env::var_os("FERESE_TRACE_PERFORMANCE").is_some() { let started = monotonic_now().saturating_sub(cost); tracing::debug!(target: "ferese::render", output = %output.output.name(), diff --git a/compositor/src/backends/direct/session_device.rs b/compositor/src/backends/direct/session_device.rs index 3218a93..532a849 100644 --- a/compositor/src/backends/direct/session_device.rs +++ b/compositor/src/backends/direct/session_device.rs @@ -8,8 +8,14 @@ pub(super) struct SessionDevice { impl SessionDevice { pub fn open(mut session: S, path: &Path) -> Result { - let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; - Ok(Self { session, fd: Some(fd) }) + let fd = session.open( + path, + OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, + )?; + Ok(Self { + session, + fd: Some(fd), + }) } pub fn device_fd(&self) -> io::Result { @@ -20,7 +26,10 @@ impl SessionDevice { impl Drop for SessionDevice { fn drop(&mut self) { if let Err(error) = self.session.close(self.fd.take().unwrap()) { - tracing::warn!(?error, "could not release DRM device through the session manager"); + tracing::warn!( + ?error, + "could not release DRM device through the session manager" + ); } } } diff --git a/compositor/src/backends/direct/topology.rs b/compositor/src/backends/direct/topology.rs index 6ec943e..0df8850 100644 --- a/compositor/src/backends/direct/topology.rs +++ b/compositor/src/backends/direct/topology.rs @@ -31,11 +31,20 @@ impl Default for DeviceState { pub(super) fn device_identity(path: &Path) -> Option { // The parent hardware identity survives cardN renumbering after unplug. - std::fs::canonicalize(Path::new("/sys/class/drm").join(path.file_name()?).join("device")).ok() + std::fs::canonicalize( + Path::new("/sys/class/drm") + .join(path.file_name()?) + .join("device"), + ) + .ok() } impl DeviceState { - fn current_path<'a>(&self, original: &Path, observed: &'a [(PathBuf, Option)]) -> Option<&'a Path> { + fn current_path<'a>( + &self, + original: &Path, + observed: &'a [(PathBuf, Option)], + ) -> Option<&'a Path> { observed .iter() .find(|(path, identity)| match &self.identity { @@ -48,7 +57,8 @@ impl DeviceState { pub fn failed(&mut self, now: Instant) { self.availability = Availability::Unavailable; self.failures = self.failures.saturating_add(1); - self.retry_at = Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5))); + self.retry_at = + Some(now + Duration::from_secs(1u64 << self.failures.saturating_sub(1).min(5))); } pub fn ready(&mut self, node: Node) { @@ -128,7 +138,8 @@ impl Topology { ) -> HashSet { let mut retired = HashSet::new(); for (key, slot) in &mut self.devices { - if let Availability::Ready(previous) | Availability::Degraded(previous) = slot.availability + if let Availability::Ready(previous) | Availability::Degraded(previous) = + slot.availability && slot.current_path(key, observed).and_then(&node_for_path) != Some(previous) { retired.insert(previous); @@ -145,7 +156,12 @@ impl Topology { pub fn retry_deadline(&self, active: bool) -> Option { active - .then(|| self.devices.values().filter_map(|device| device.retry_at).min()) + .then(|| { + self.devices + .values() + .filter_map(|device| device.retry_at) + .min() + }) .flatten() } } @@ -166,7 +182,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { let Some(backend) = state.direct_backend.as_mut() else { return; }; - if !backend.topology.request(backend.active && !backend.lid.pending()) { + if !backend + .topology + .request(backend.active && !backend.lid.pending()) + { return; } @@ -200,8 +219,11 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { .output_profiles .iter() .any(|profile| profile.outputs.iter().any(|output| output.auto_refresh)); - backend.low_power = - automatic && super::super::power::low_power(backend.low_power, super::super::power::battery_percent()); + backend.low_power = automatic + && super::super::power::low_power( + backend.low_power, + super::super::power::battery_percent(), + ); if let Ok(observed) = &observed { backend.topology.observe_devices(observed); @@ -230,7 +252,14 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { .and_then(|devices| slot.current_path(&path, devices)); let existing = current_path .and_then(|path| DrmNode::from_path(path).ok()) - .filter(|node| state.direct_backend.as_ref().unwrap().devices.contains_key(node)); + .filter(|node| { + state + .direct_backend + .as_ref() + .unwrap() + .devices + .contains_key(node) + }); let previous = match slot.availability { Availability::Ready(node) | Availability::Degraded(node) => Some(node), _ => existing, @@ -269,7 +298,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { let mut failed_outputs = Vec::new(); let activation = if reactivate && existing.is_some() { - let device = state.direct_backend.as_mut().unwrap().devices.get_mut(&node).unwrap(); + let device = state + .direct_backend + .as_mut() + .unwrap() + .devices + .get_mut(&node) + .unwrap(); device.drm.activate(true).map(|()| { for (crtc, output) in &mut device.outputs { cancel_output_timers(&state.loop_handle, output); @@ -333,7 +368,12 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { ready.push((path, node)); } - let known_good = state.direct_backend.as_ref().unwrap().applied_outputs.clone(); + let known_good = state + .direct_backend + .as_ref() + .unwrap() + .applied_outputs + .clone(); if let Err(error) = refresh_monitor_policy(state).and_then(|()| { transaction::retire_disconnected(state); validate_desired_outputs(state) @@ -353,7 +393,10 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { .outputs .iter() .filter(|output| output.settings.enabled && output.key.starts_with(&prefix)); - let logical_sources = desired.clone().filter(|output| output.mirror_source.is_none()).count(); + let logical_sources = desired + .clone() + .filter(|output| output.mirror_source.is_none()) + .count(); std::cmp::Reverse((logical_sources, desired.count())) }); let mut succeeded = Vec::new(); @@ -380,7 +423,13 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { break; } } - if state.direct_backend.as_ref().unwrap().output_error.is_none() { + if state + .direct_backend + .as_ref() + .unwrap() + .output_error + .is_none() + { remember_applied_configuration(state); } } @@ -390,7 +439,9 @@ pub(super) fn reconcile_outputs(state: &mut Ferese, reactivate: bool) { if std::mem::take(&mut state.direct_backend.as_mut().unwrap().topology.layout_dirty) { desktop_changes.layout = true; desktop_changes.shell = true; - desktop_changes.outputs.extend(state.space.outputs().cloned()); + desktop_changes + .outputs + .extend(state.space.outputs().cloned()); } let backend = state.direct_backend.as_mut().unwrap(); backend.topology.reconciling = false; @@ -453,7 +504,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges { }); let mode = OutputMode::from(mode); let transform = output_transform(settings.transform); - let width = (transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32; + let width = + (transform.transform_size(mode.size).w as f64 / settings.scale).ceil() as i32; right_edge = right_edge.max(position[0].saturating_add(width)); crate::state::DesktopOutput { output, @@ -472,7 +524,8 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges { for output in device.outputs.values_mut() { if output.mirror_source.is_none() { if output.global.is_none() { - output.global = Some(output.output.create_global::(&state.display_handle)); + output.global = + Some(output.output.create_global::(&state.display_handle)); } } else { // Mirrors are physical scanouts, not workspace-owning outputs. @@ -483,7 +536,9 @@ fn publish_runtime_desktop(state: &mut Ferese) -> crate::state::DesktopChanges { None, ); } - output.surface.set_output_mode_source((&output.output).into()); + output + .surface + .set_output_mode_source((&output.output).into()); } } changes @@ -542,7 +597,11 @@ mod tests { topology.devices.get_mut(&a).unwrap().ready(node); // A policy failure keeps the device open, so it must retain ownership // of its node until replacement retirement runs. - topology.devices.get_mut(&a).unwrap().degraded(node, Instant::now()); + topology + .devices + .get_mut(&a) + .unwrap() + .degraded(node, Instant::now()); let observed = [(card.clone(), Some(b.clone()))]; topology.observe_devices(&observed); assert_eq!( @@ -551,9 +610,16 @@ mod tests { ); assert_eq!(topology.devices[&a].availability, Availability::Missing); assert_eq!(topology.devices[&a].current_path(&a, &observed), None); - assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card.as_path())); + assert_eq!( + topology.devices[&b].current_path(&b, &observed), + Some(card.as_path()) + ); topology.devices.get_mut(&b).unwrap().ready(node); - assert!(topology.retire_changed_devices(&observed, |_| Some(node)).is_empty()); + assert!( + topology + .retire_changed_devices(&observed, |_| Some(node)) + .is_empty() + ); } #[test] @@ -567,15 +633,28 @@ mod tests { let mut topology = Topology::::default(); topology.observe_devices(&[(card0.clone(), Some(a.clone()))]); topology.devices.get_mut(&a).unwrap().ready(node0); - let observed = [(card0.clone(), Some(b.clone())), (card2.clone(), Some(a.clone()))]; + let observed = [ + (card0.clone(), Some(b.clone())), + (card2.clone(), Some(a.clone())), + ]; for _ in 0..2 { topology.observe_devices(&observed); } assert_eq!(topology.devices.len(), 2); - assert_eq!(topology.devices[&a].current_path(&a, &observed), Some(card2.as_path())); - assert_eq!(topology.devices[&b].current_path(&b, &observed), Some(card0.as_path())); assert_eq!( - topology.retire_changed_devices(&observed, |path| Some(if path == card0 { node0 } else { node2 })), + topology.devices[&a].current_path(&a, &observed), + Some(card2.as_path()) + ); + assert_eq!( + topology.devices[&b].current_path(&b, &observed), + Some(card0.as_path()) + ); + assert_eq!( + topology.retire_changed_devices(&observed, |path| Some(if path == card0 { + node0 + } else { + node2 + })), HashSet::from([node0]) ); } @@ -590,7 +669,10 @@ mod tests { topology.devices.get_mut(&card).unwrap().ready(node); topology.observe_devices(&[(card.clone(), Some(identity.clone()))]); assert_eq!(topology.devices.len(), 1); - assert_eq!(topology.devices[&identity].availability, Availability::Ready(node)); + assert_eq!( + topology.devices[&identity].availability, + Availability::Ready(node) + ); } #[test] @@ -608,7 +690,10 @@ mod tests { topology.retire_changed_devices(&unknown, |_| Some(1)), HashSet::from([1]) ); - assert_eq!(topology.devices[&identity].availability, Availability::Missing); + assert_eq!( + topology.devices[&identity].availability, + Availability::Missing + ); assert_eq!( topology.devices[&card].current_path(&card, &unknown), Some(card.as_path()) @@ -624,9 +709,17 @@ mod tests { Some(card.as_path()) ); // The reopened node is still valid and can be reused for this identity. - assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty()); + assert!( + topology + .retire_changed_devices(&known, |_| Some(1)) + .is_empty() + ); topology.devices.get_mut(&identity).unwrap().ready(1); - assert!(topology.retire_changed_devices(&known, |_| Some(1)).is_empty()); + assert!( + topology + .retire_changed_devices(&known, |_| Some(1)) + .is_empty() + ); } #[test] @@ -636,10 +729,16 @@ mod tests { let card0 = PathBuf::from("/dev/dri/card0"); let card1 = PathBuf::from("/dev/dri/card1"); let mut topology = Topology::::default(); - topology.observe_devices(&[(card0.clone(), Some(a.clone())), (card1.clone(), Some(b.clone()))]); + topology.observe_devices(&[ + (card0.clone(), Some(a.clone())), + (card1.clone(), Some(b.clone())), + ]); topology.devices.get_mut(&a).unwrap().ready(0); topology.devices.get_mut(&b).unwrap().ready(1); - let swapped = [(card0.clone(), Some(b.clone())), (card1.clone(), Some(a.clone()))]; + let swapped = [ + (card0.clone(), Some(b.clone())), + (card1.clone(), Some(a.clone())), + ]; let node_for_path = |path: &Path| Some(u64::from(path == card1)); topology.observe_devices(&swapped); assert_eq!( @@ -650,7 +749,11 @@ mod tests { assert_eq!(topology.devices[&b].availability, Availability::Missing); topology.devices.get_mut(&a).unwrap().ready(1); topology.devices.get_mut(&b).unwrap().ready(0); - assert!(topology.retire_changed_devices(&swapped, node_for_path).is_empty()); + assert!( + topology + .retire_changed_devices(&swapped, node_for_path) + .is_empty() + ); } #[test] @@ -693,12 +796,17 @@ mod tests { fn unavailable_devices_retry_independently_with_capped_backoff() { let now = Instant::now(); let mut topology = Topology::::default(); - topology.devices.insert("/dev/dri/card0".into(), DeviceState::default()); + topology + .devices + .insert("/dev/dri/card0".into(), DeviceState::default()); let healthy = 1; let mut second = DeviceState::default(); second.ready(healthy); topology.devices.insert("/dev/dri/card1".into(), second); - let device = topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap(); + let device = topology + .devices + .get_mut(Path::new("/dev/dri/card0")) + .unwrap(); device.failed(now); assert_eq!(device.retry_at, Some(now + Duration::from_secs(1))); for _ in 0..20 { @@ -710,8 +818,15 @@ mod tests { Availability::Ready(healthy) ); assert!(topology.retry_deadline(false).is_none()); - assert_eq!(topology.retry_deadline(true), Some(now + Duration::from_secs(32))); - topology.devices.get_mut(Path::new("/dev/dri/card0")).unwrap().missing(); + assert_eq!( + topology.retry_deadline(true), + Some(now + Duration::from_secs(32)) + ); + topology + .devices + .get_mut(Path::new("/dev/dri/card0")) + .unwrap() + .missing(); assert!(topology.retry_deadline(true).is_none()); } } diff --git a/compositor/src/backends/direct/transaction.rs b/compositor/src/backends/direct/transaction.rs index 1352326..bb62748 100644 --- a/compositor/src/backends/direct/transaction.rs +++ b/compositor/src/backends/direct/transaction.rs @@ -6,7 +6,11 @@ use smithay::backend::allocator::Allocator; use smithay::backend::drm::exporter::{ExportBuffer, ExportFramebuffer}; use smithay::reexports::drm::control::{AtomicCommitFlags, ResourceHandle, atomic::AtomicModeReq}; -fn property(drm: &D, handle: H, name: &str) -> io::Result { +fn property( + drm: &D, + handle: H, + name: &str, +) -> io::Result { for (property, _) in drm.get_properties(handle)?.iter() { if drm.get_property(*property)?.name().to_bytes() == name.as_bytes() { return Ok(*property); @@ -35,8 +39,9 @@ fn clear_secondary_planes( for plane in planes.cursor.iter().chain(&planes.overlay) { let properties = drm.get_properties(plane.handle)?; let owned = properties.iter().any(|(handle, value)| { - drm.get_property(*handle) - .is_ok_and(|info| info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64) + drm.get_property(*handle).is_ok_and(|info| { + info.name().to_bytes() == b"CRTC_ID" && *value == u32::from(crtc) as u64 + }) }); if owned { add(drm, request, plane.handle, "FB_ID", 0)?; @@ -59,12 +64,14 @@ impl Drop for Blobs { } } -pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Result<(), Box> { +pub(super) fn validate_device( + device: &DirectDevice, + scan: &OutputScan, +) -> Result<(), Box> { if scan.selections.iter().all(|selection| { - device - .outputs - .get(&selection.crtc) - .is_some_and(|output| output.connector == selection.connector.handle() && output.mode == selection.mode) + device.outputs.get(&selection.crtc).is_some_and(|output| { + output.connector == selection.connector.handle() && output.mode == selection.mode + }) }) && scan.selections.len() == device.outputs.len() { return Ok(()); @@ -82,7 +89,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul }; let mut buffers = Vec::new(); let mut framebuffers = Vec::new(); - let mut allocator = GbmAllocator::new(device.gbm.clone(), GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT); + let mut allocator = GbmAllocator::new( + device.gbm.clone(), + GbmBufferFlags::RENDERING | GbmBufferFlags::SCANOUT, + ); let exporter = GbmFramebufferExporter::new(device.gbm.clone(), Some(device.render_node).into()); let mut used_planes = HashSet::new(); let desired_crtcs = scan @@ -151,16 +161,33 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul Fourcc::Argb8888 | Fourcc::Abgr8888 | Fourcc::Xrgb8888 | Fourcc::Xbgr8888 ) }) { - if let Ok(buffer) = allocator.create_buffer(width.into(), height.into(), format.code, &[format.modifier]) - && let Ok(Some(fb)) = exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false) + if let Ok(buffer) = allocator.create_buffer( + width.into(), + height.into(), + format.code, + &[format.modifier], + ) && let Ok(Some(fb)) = + exporter.add_framebuffer(drm.device_fd(), ExportBuffer::Allocator(&buffer), false) { allocated = Some((buffer, fb)); break; } } let (buffer, fb) = allocated.ok_or("unable to allocate a DRM validation buffer")?; - add(drm, &mut request, primary, "FB_ID", u32::from(*fb.as_ref()) as u64)?; - add(drm, &mut request, primary, "CRTC_ID", u32::from(selection.crtc) as u64)?; + add( + drm, + &mut request, + primary, + "FB_ID", + u32::from(*fb.as_ref()) as u64, + )?; + add( + drm, + &mut request, + primary, + "CRTC_ID", + u32::from(selection.crtc) as u64, + )?; for (name, value) in [ ("SRC_X", 0), ("SRC_Y", 0), @@ -176,7 +203,10 @@ pub(super) fn validate_device(device: &DirectDevice, scan: &OutputScan) -> Resul buffers.push(buffer); framebuffers.push(fb); } - drm.atomic_commit(AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, request)?; + drm.atomic_commit( + AtomicCommitFlags::TEST_ONLY | AtomicCommitFlags::ALLOW_MODESET, + request, + )?; Ok(()) } @@ -348,13 +378,19 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> { let previous = self.device.outputs.get_mut(&crtc).unwrap(); previous.surface = replacement.surface; previous.dmabuf_feedback = replacement.dmabuf_feedback; - previous.surface.commit_frame().map_err(|error| error.to_string())?; + previous + .surface + .commit_frame() + .map_err(|error| error.to_string())?; } let Some(output) = self.device.outputs.get_mut(&crtc) else { return Ok(()); }; output.dmabuf_feedback.invalidate(); - output.surface.reset_state().map_err(|error| error.to_string())?; + output + .surface + .reset_state() + .map_err(|error| error.to_string())?; let restored = output .surface .set_connectors(&[output.connector]) @@ -368,9 +404,16 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> { ) .map_err(|error| error.to_string()) }) - .and_then(|()| output.surface.commit_frame().map_err(|error| error.to_string())); + .and_then(|()| { + output + .surface + .commit_frame() + .map_err(|error| error.to_string()) + }); self.state.session_lock.output_added(&output.output); - self.state.display_presentation.remove_output(&output.output); + self.state + .display_presentation + .remove_output(&output.output); self.state .desktop_transition .as_mut() @@ -378,7 +421,9 @@ impl crate::output_transaction::HardwareTransaction for DeviceTransaction<'_> { .redraw .insert(output.output.clone()); output.lock_frame_pending = false; - output.surface.set_output_mode_source((&output.output).into()); + output + .surface + .set_output_mode_source((&output.output).into()); cancel_output_timers(&self.state.loop_handle, output); output.frame_pending = false; restored @@ -447,8 +492,12 @@ pub(super) fn apply_device( } for crtc in &changed { let selection = &selections[crtc]; - let mut output = staged.remove(crtc).or_else(|| device.outputs.remove(crtc)).unwrap(); - if output.connector != selection.connector.handle() || output.identity != selection.identity { + let mut output = staged + .remove(crtc) + .or_else(|| device.outputs.remove(crtc)) + .unwrap(); + if output.connector != selection.connector.handle() || output.identity != selection.identity + { retire(state, &mut output, node, *crtc); let (handle, _) = create_output( state, @@ -482,7 +531,9 @@ pub(super) fn apply_device( state.display_presentation.remove_output(&output.output); } output.lock_frame_pending = false; - output.surface.set_output_mode_source((&output.output).into()); + output + .surface + .set_output_mode_source((&output.output).into()); cancel_output_timers(&state.loop_handle, &mut output); output.frame_pending = false; output.primary_commit = None; @@ -513,7 +564,11 @@ pub(super) fn update_applied_info(device: &mut DirectDevice) { for info in &mut device.connected_outputs { info.enabled = false; info.current_mode = None; - if let Some(output) = device.outputs.values().find(|output| output.identity == info.identity) { + if let Some(output) = device + .outputs + .values() + .find(|output| output.identity == info.identity) + { info.enabled = true; info.current_mode = Some(connected_mode_info(output.mode)); info.scale = output.settings.scale; @@ -568,7 +623,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) { .unwrap() .devices .values() - .any(|device| device.outputs.values().any(|output| output.mirror_source.is_none())) + .any(|device| { + device + .outputs + .values() + .any(|output| output.mirror_source.is_none()) + }) { return; } @@ -604,7 +664,11 @@ pub(super) fn recover_last_output(state: &mut Ferese) { let nodes = backend .devices .iter() - .filter(|(_, device)| candidate.key.starts_with(&format!("{}/", drm_device_key(&device.drm)))) + .filter(|(_, device)| { + candidate + .key + .starts_with(&format!("{}/", drm_device_key(&device.drm))) + }) .map(|(node, _)| *node) .collect::>(); if validate_desired_outputs(state).is_ok() { @@ -617,7 +681,12 @@ pub(super) fn recover_last_output(state: &mut Ferese) { .unwrap() .devices .values() - .any(|device| device.outputs.values().any(|output| output.mirror_source.is_none())) + .any(|device| { + device + .outputs + .values() + .any(|output| output.mirror_source.is_none()) + }) { remember_applied_configuration(state); state.direct_backend.as_mut().unwrap().output_error = original_error; diff --git a/compositor/src/backends/mod.rs b/compositor/src/backends/mod.rs index 0491909..dcb3b57 100644 --- a/compositor/src/backends/mod.rs +++ b/compositor/src/backends/mod.rs @@ -26,12 +26,16 @@ pub struct LaunchConfig { impl LaunchConfig { pub fn from_environment() -> Result { - let graphical_session = std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some(); + let graphical_session = + std::env::var_os("WAYLAND_DISPLAY").is_some() || std::env::var_os("DISPLAY").is_some(); Self::parse(std::env::args_os().skip(1), graphical_session) } - fn parse(args: impl IntoIterator, graphical_session: bool) -> Result { + fn parse( + args: impl IntoIterator, + graphical_session: bool, + ) -> Result { let mut args = args.into_iter().peekable(); let mut requested_backend = None; let mut client = Vec::new(); @@ -113,7 +117,10 @@ fn parse_backend(value: &OsString) -> Result, String> { Some("auto") => Ok(None), Some("nested") => Ok(Some(BackendKind::Nested)), Some("drm") => Ok(Some(BackendKind::Drm)), - _ => Err(format!("unknown backend {:?}; expected auto, nested, or drm", value)), + _ => Err(format!( + "unknown backend {:?}; expected auto, nested, or drm", + value + )), } } @@ -148,7 +155,11 @@ mod tests { .unwrap(); assert!(config.supervise_client); assert_eq!(config.client, [OsString::from("ferese-session-shell")]); - assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL)); + assert!( + config + .client_capabilities + .contains(ClientCapabilities::SHELL_CONTROL) + ); } #[test] @@ -194,25 +205,42 @@ mod tests { .unwrap(); assert_eq!(config.client, [OsString::from("ferese-effects-probe")]); - assert!(config.client_capabilities.contains(ClientCapabilities::EFFECTS)); + assert!( + config + .client_capabilities + .contains(ClientCapabilities::EFFECTS) + ); } #[test] fn explicitly_grants_shell_control_to_the_launched_private_client() { let config = LaunchConfig::parse( - [OsString::from("--grant-shell-control"), OsString::from("ferese-shell")], + [ + OsString::from("--grant-shell-control"), + OsString::from("ferese-shell"), + ], true, ) .unwrap(); assert_eq!(config.client, [OsString::from("ferese-shell")]); - assert!(config.client_capabilities.contains(ClientCapabilities::SHELL_CONTROL)); - assert!(!config.client_capabilities.contains(ClientCapabilities::EFFECTS)); + assert!( + config + .client_capabilities + .contains(ClientCapabilities::SHELL_CONTROL) + ); + assert!( + !config + .client_capabilities + .contains(ClientCapabilities::EFFECTS) + ); } #[test] fn rejects_an_unknown_backend() { - let error = LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true).unwrap_err(); + let error = + LaunchConfig::parse([OsString::from("--backend"), OsString::from("other")], true) + .unwrap_err(); assert!(error.contains("unknown backend")); } diff --git a/compositor/src/backends/power.rs b/compositor/src/backends/power.rs index b586ae3..02cf2d0 100644 --- a/compositor/src/backends/power.rs +++ b/compositor/src/backends/power.rs @@ -17,10 +17,10 @@ fn read_battery_percent(root: &Path) -> Option { .ok() .map(|text| text.trim().to_owned()) }; - if entries - .iter() - .any(|path| read(path, "online").as_deref() == Some("1") && read(path, "type").as_deref() != Some("Battery")) - { + if entries.iter().any(|path| { + read(path, "online").as_deref() == Some("1") + && read(path, "type").as_deref() != Some("Battery") + }) { return None; } entries.iter().find_map(|path| { @@ -57,7 +57,9 @@ pub(super) fn low_refresh_mode(size: (u16, u16), modes: &[(u16, u16, i32)]) -> O modes .iter() .enumerate() - .filter(|(_, (width, height, refresh))| (*width, *height) == size && (59_000..=61_000).contains(refresh)) + .filter(|(_, (width, height, refresh))| { + (*width, *height) == size && (59_000..=61_000).contains(refresh) + }) .min_by_key(|(_, (_, _, refresh))| refresh.abs_diff(60_000)) .map(|(index, _)| index) } @@ -96,7 +98,10 @@ mod tests { assert_eq!(low_refresh_mode((2880, 1800), &modes), Some(3)); assert_eq!(low_refresh_mode((2880, 1800), &modes[..3]), Some(2)); assert_eq!(low_refresh_mode((2880, 1800), &modes[..2]), None); - assert_eq!(low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]), None); + assert_eq!( + low_refresh_mode((2880, 1800), &[(2880, 1800, -60_000)]), + None + ); } #[test] diff --git a/compositor/src/config.rs b/compositor/src/config.rs index bdd3537..e70bd0d 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -26,7 +26,9 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy}; pub use input_motion::InputSettings; use input_motion::*; use outputs::*; -pub use outputs::{LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; +pub use outputs::{ + LidPolicy, OutputLayout, OutputModeRequest, OutputProfile, OutputSettings, OutputTransform, +}; use serde::Deserialize; use smithay::input::keyboard::{Keycode, keysyms, xkb}; pub(crate) use tab_indicator::TabIndicatorSettings; @@ -128,11 +130,19 @@ impl Config { ferese_panel::validate(panels).map_err(ConfigError::Binding)?; } self.activities.validate().map_err(ConfigError::Binding)?; - self.notifications.validate().map_err(ConfigError::Binding)?; + self.notifications + .validate() + .map_err(ConfigError::Binding)?; self.xwayland.validate().map_err(ConfigError::Binding)?; for daemon in &self.autostart { - if daemon.command.first().is_none_or(|program| program.trim().is_empty()) { - return Err(ConfigError::Binding("autostart command must contain a program".into())); + if daemon + .command + .first() + .is_none_or(|program| program.trim().is_empty()) + { + return Err(ConfigError::Binding( + "autostart command must contain a program".into(), + )); } } let input_settings = self.input_settings()?; @@ -451,7 +461,10 @@ mod tests { let runtime = config.runtime_config().unwrap(); assert_eq!(runtime.panel_background_opacity, Some(0.8)); assert!((runtime.theme_settings.shell_opacity - 0.3).abs() < 0.001); - let inherited = Config::parse_source("panel main;").unwrap().runtime_config().unwrap(); + let inherited = Config::parse_source("panel main;") + .unwrap() + .runtime_config() + .unwrap(); assert!(inherited.panel_background_opacity.is_none()); } @@ -475,7 +488,12 @@ mod tests { #[test] fn validates_shell_composition_before_publishing_config() { let source = r#"panel "main" { end { group "status" { item "network" kind="network"; item "clock" kind="clock"; }; }; }"#; - assert!(Config::parse_source(source).unwrap().runtime_config().is_ok()); + assert!( + Config::parse_source(source) + .unwrap() + .runtime_config() + .is_ok() + ); assert!( Config::parse_source(&source.replace("item \"clock\"", "item \"network\"")) .unwrap() @@ -498,10 +516,14 @@ mod tests { "panel main {{ style {}; end {{ group status {{ item clock kind=clock; }}; }}; }}", style.key() ); - Config::parse_source(&source).unwrap().runtime_config().unwrap(); + Config::parse_source(&source) + .unwrap() + .runtime_config() + .unwrap(); } for surface in ["none", "inset", "island"] { - let invalid = format!("panel main {{ end {{ group status {{ surface {surface}; }}; }}; }}"); + let invalid = + format!("panel main {{ end {{ group status {{ surface {surface}; }}; }}; }}"); assert!(Config::parse_source(&invalid).is_err()); } assert!(Config::parse_source("panel main { group-surface invalid; }").is_err()); @@ -512,11 +534,18 @@ mod tests { #[test] fn group_padding_is_validated_before_live_publication() { for padding in ["0", "4.5", "12", "32"] { - let source = format!("panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"); - Config::parse_source(&source).unwrap().runtime_config().unwrap(); + let source = format!( + "panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}" + ); + Config::parse_source(&source) + .unwrap() + .runtime_config() + .unwrap(); } for padding in ["-1", "32.5", "\"small\""] { - let source = format!("panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}"); + let source = format!( + "panel main {{ end {{ group status {{ island-padding {padding}; }}; }}; }}" + ); assert!( Config::parse_source(&source) .map(|config| config.runtime_config().is_err()) @@ -529,7 +558,8 @@ mod tests { fn rejects_removed_physics_keys_before_publication() { for property in ["spring", "viewport-spring"] { for coefficient in ["mass", "stiffness", "damping", "damping-ratio"] { - let source = format!("animations {{ {property} {{ {coefficient} 0; overshoot #true; }}; }}"); + let source = + format!("animations {{ {property} {{ {coefficient} 0; overshoot #true; }}; }}"); let error = Config::parse_source(&source).unwrap_err().to_string(); assert!(error.contains("unknown field"), "{error}"); } @@ -541,10 +571,9 @@ mod tests { #[test] fn display_key_has_a_default_binding_and_can_be_rebound() { let bindings = parse("").runtime_config().unwrap().bindings; - assert!(bindings.iter().any( - |binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display) - && binding.action == BindingAction::ToggleDisplayMode - )); + assert!(bindings.iter().any(|binding| binding.trigger + == BindingTrigger::Keysym(keysyms::KEY_XF86Display) + && binding.action == BindingAction::ToggleDisplayMode)); let bindings = parse( "binding keys=\"XF86Display\" disabled=#true\nbinding keys=\"Super+P\" action=\"toggle-display-mode\"\n", ) @@ -557,9 +586,8 @@ mod tests { .any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_XF86Display)) ); assert!( - bindings - .iter() - .any(|binding| binding.modifiers.logo && binding.action == BindingAction::ToggleDisplayMode) + bindings.iter().any(|binding| binding.modifiers.logo + && binding.action == BindingAction::ToggleDisplayMode) ); } @@ -580,12 +608,9 @@ mod tests { .iter() .any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F1)) ); - assert!( - bindings - .iter() - .any(|binding| binding.trigger == BindingTrigger::Keysym(keysyms::KEY_F2) - && binding.action == BindingAction::ToggleKeybindingGuide) - ); + assert!(bindings.iter().any(|binding| binding.trigger + == BindingTrigger::Keysym(keysyms::KEY_F2) + && binding.action == BindingAction::ToggleKeybindingGuide)); } #[test] fn workspace_back_and_forth_config_is_optional_and_bindable() { @@ -635,7 +660,12 @@ mod tests { BindingAction::FocusMru(false), BindingAction::FocusMru(true), ] { - assert!(defaults.bindings.iter().any(|binding| binding.action == action)); + assert!( + defaults + .bindings + .iter() + .any(|binding| binding.action == action) + ); } for (name, action) in [ ("focus-last-window", BindingAction::FocusLastWindow), @@ -654,7 +684,8 @@ mod tests { false, true, false - ) && binding.action == BindingAction::FocusMru(false))); + ) && binding.action + == BindingAction::FocusMru(false))); assert!(bindings.iter().any(|binding| binding.matches( 23_u32.into(), &[keysyms::KEY_Tab], @@ -662,7 +693,8 @@ mod tests { false, true, true - ) && binding.action == BindingAction::FocusMru(true))); + ) && binding.action + == BindingAction::FocusMru(true))); } #[test] @@ -677,8 +709,14 @@ mod tests { let source = "input {\n touchpad {\n swipe-threshold 96\n }\n}\nbinding keys=\"Swipe3Up\" action=\"toggle-overview\"\noutput-profile name=\"desk\" {\n output match=\"DP-1\" scale=1.5 {\n position 0 0\n }\n}\nautostart {\n command \"program\" \"argument with space\"\n}\n"; let config = Config::parse_source(source).unwrap(); config.runtime_config().unwrap(); - assert_eq!(config.input_settings().unwrap().touchpad.swipe_threshold, 96); - assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0])); + assert_eq!( + config.input_settings().unwrap().touchpad.swipe_threshold, + 96 + ); + assert_eq!( + config.output_profiles().unwrap()[0].outputs[0].position, + Some([0, 0]) + ); } #[test] @@ -716,7 +754,9 @@ mod tests { ); assert_eq!(config.xwayland, XwaylandConfig::default()); assert!(config.xwayland.enabled); - let runtime = config.runtime_config().expect("the existing config stays valid"); + let runtime = config + .runtime_config() + .expect("the existing config stays valid"); assert!(runtime.xwayland.enabled); } @@ -725,7 +765,10 @@ mod tests { let config = parse(""); assert_eq!(config.layout_mode(), LayoutMode::Scrolling); - assert_eq!(config.default_column_width().unwrap(), ColumnWidth::Proportion(0.5)); + assert_eq!( + config.default_column_width().unwrap(), + ColumnWidth::Proportion(0.5) + ); assert_eq!(config.gap_config().unwrap(), GapConfig::default()); } @@ -735,7 +778,10 @@ mod tests { let numeric = parse("scrolling {\n default-column-width 1.0\n}\n"); assert_eq!(full.default_column_width().unwrap(), ColumnWidth::Full); - assert_eq!(numeric.default_column_width().unwrap(), ColumnWidth::Proportion(1.0)); + assert_eq!( + numeric.default_column_width().unwrap(), + ColumnWidth::Proportion(1.0) + ); } #[test] @@ -759,10 +805,19 @@ mod tests { let minimal = parse(""); let centered = parse("scrolling {\n focus-strategy \"center_on_focus\"\n}\n"); let paged = parse("scrolling {\n focus-strategy \"paged\"\n}\n"); - assert_eq!(paged.scrolling_focus_strategy(), ViewportFocusStrategy::Paged); + assert_eq!( + paged.scrolling_focus_strategy(), + ViewportFocusStrategy::Paged + ); - assert_eq!(minimal.scrolling_focus_strategy(), ViewportFocusStrategy::Minimal); - assert_eq!(centered.scrolling_focus_strategy(), ViewportFocusStrategy::Center); + assert_eq!( + minimal.scrolling_focus_strategy(), + ViewportFocusStrategy::Minimal + ); + assert_eq!( + centered.scrolling_focus_strategy(), + ViewportFocusStrategy::Center + ); } #[test] @@ -804,7 +859,8 @@ mod tests { #[test] fn parses_and_validates_layout_gaps() { - let configured = parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n"); + let configured = + parse("layout {\n inner-gap 6.0\n outer-gap 14.0\n smart-gaps #true\n}\n"); let invalid = parse("layout {\n outer-gap -1.0\n}\n"); assert_eq!( @@ -842,9 +898,11 @@ mod tests { "theme {\n colors {\n border \"#11223344\"\n accent \"#AABBCC\"\n shadow \"#01020380\"\n }\n geometry {\n border-width 1.5\n focus-ring-width 3.0\n window-radius 12.0\n }\n shadow {\n soft {\n offset-y -2.0\n blur 24.0\n opacity 0.4\n }\n }\n material {\n style \"translucent\"\n }\n}\n", ); let invalid_color = parse("theme {\n colors {\n accent \"blue\"\n }\n}\n"); - let invalid_radius = parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n"); - let invalid_opacity = - parse("theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n"); + let invalid_radius = + parse("theme {\n geometry {\n window-radius -1.0\n }\n}\n"); + let invalid_opacity = parse( + "theme {\n shadow {\n soft {\n opacity 1.1\n }\n }\n}\n", + ); assert_eq!( configured.theme_settings().unwrap(), @@ -877,15 +935,24 @@ mod tests { assert!(invalid_radius.theme_settings().is_err()); assert!(invalid_opacity.theme_settings().is_err()); assert!( - ferese_config::from_str::("theme {\n material {\n style \"mist\"\n }\n}\n").is_err() + ferese_config::from_str::( + "theme {\n material {\n style \"mist\"\n }\n}\n" + ) + .is_err() ); } #[test] fn materials_default_to_solid_and_reject_removed_style() { - assert_eq!(parse("").theme_settings().unwrap().material_style, MaterialStyle::Solid); + assert_eq!( + parse("").theme_settings().unwrap().material_style, + MaterialStyle::Solid + ); assert!( - ferese_config::from_str::("theme {\n material {\n style \"glass\"\n }\n}\n").is_err() + ferese_config::from_str::( + "theme {\n material {\n style \"glass\"\n }\n}\n" + ) + .is_err() ); } @@ -932,9 +999,11 @@ mod tests { "from \"#112233\"\nto \"#445566\"\nangle #inf\n", ] { assert!( - Config::parse_source(&format!("theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}")) - .and_then(|config| config.theme_settings()) - .is_err() + Config::parse_source(&format!( + "theme {{ focus-ring {{ gradient {{\n{settings}\n}} }} }}" + )) + .and_then(|config| config.theme_settings()) + .is_err() ); } assert!( @@ -1023,17 +1092,21 @@ mod tests { for key in ["active-opacity", "inactive-opacity", "inactive-dim"] { for value in ["-0.1", "1.1", "#nan", "#inf"] { assert!( - Config::parse_source(&format!("appearance {{ focus-effect {{ {key} {value}; }} }}")) - .and_then(|config| config.runtime_config()) - .is_err() + Config::parse_source(&format!( + "appearance {{ focus-effect {{ {key} {value}; }} }}" + )) + .and_then(|config| config.runtime_config()) + .is_err() ); } } for value in ["-1", "#nan", "#inf"] { assert!( - Config::parse_source(&format!("appearance {{ focus-effect {{ duration-ms {value}; }} }}")) - .and_then(|config| config.runtime_config()) - .is_err() + Config::parse_source(&format!( + "appearance {{ focus-effect {{ duration-ms {value}; }} }}" + )) + .and_then(|config| config.runtime_config()) + .is_err() ); } let disabled = parse("appearance { focus-effect { enabled #false; active-opacity 0.9; inactive-opacity 0.8; inactive-dim 0.25; }; }") @@ -1109,9 +1182,11 @@ mod tests { ); for threshold in [0, 15, 1001] { assert!( - parse(&format!("input {{ touchpad {{ swipe-threshold {threshold}; }} }}")) - .input_settings() - .is_err() + parse(&format!( + "input {{ touchpad {{ swipe-threshold {threshold}; }} }}" + )) + .input_settings() + .is_err() ); } } @@ -1133,8 +1208,9 @@ mod tests { #[test] fn guide_uses_effective_bindings_including_overrides_and_unbindings() { - let config = - parse("binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n"); + let config = parse( + "binding keys=\"Super+Q\" action=\"none\"\nbinding keys=\"Super+Tab\" action=\"toggle-floating\"\n", + ); let input = config.input_settings().unwrap(); let map = physical_keymap(&input).unwrap(); let entries = config @@ -1143,12 +1219,13 @@ mod tests { .iter() .filter_map(|binding| binding.guide_entry(Some(&map))) .collect::>(); - assert!(!entries.iter().any(|entry| entry["description"] == "Close window")); assert!( - entries + !entries .iter() - .any(|entry| entry["keys"] == "Super + Tab" && entry["description"] == "Toggle floating window") + .any(|entry| entry["description"] == "Close window") ); + assert!(entries.iter().any(|entry| entry["keys"] == "Super + Tab" + && entry["description"] == "Toggle floating window")); assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter")); } @@ -1157,7 +1234,11 @@ mod tests { let defaults = Config::default().runtime_config().unwrap(); for (key, name, action) in [ (keysyms::KEY_XF86AudioPlay, "media-play-pause", "play-pause"), - (keysyms::KEY_XF86AudioPause, "media-play-pause", "play-pause"), + ( + keysyms::KEY_XF86AudioPause, + "media-play-pause", + "play-pause", + ), (keysyms::KEY_XF86AudioNext, "media-next", "next"), (keysyms::KEY_XF86AudioPrev, "media-previous", "previous"), ] { @@ -1186,7 +1267,9 @@ mod tests { parse("scrolling { default-column-display \"tabbed\"; }\n").default_column_display(), ferese_layout::ColumnDisplay::Tabbed ); - assert!(Config::parse_source("scrolling { default-column-display \"unknown\"; }\n").is_err()); + assert!( + Config::parse_source("scrolling { default-column-display \"unknown\"; }\n").is_err() + ); } #[test] @@ -1224,7 +1307,11 @@ mod tests { && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_f) && binding.action == action)); } - assert!(bindings.iter().any(|binding| binding.action == BindingAction::Exit)); + assert!( + bindings + .iter() + .any(|binding| binding.action == BindingAction::Exit) + ); assert!(bindings.iter().any(|binding| binding.modifiers.logo && !binding.modifiers.shift && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_space) @@ -1242,13 +1329,16 @@ mod tests { binding.modifiers.logo && binding.modifiers.shift && binding.trigger == BindingTrigger::Keysym(keysyms::KEY_s) - && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into()) + && binding.action + == BindingAction::Spawn(vec!["ferese-screenshot".to_owned()].into()) })); assert!(bindings.iter().any(|binding| { binding.trigger == BindingTrigger::Keysym(keysyms::KEY_Print) && binding.modifiers == BindingModifiers::default() && binding.action - == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into()) + == BindingAction::Spawn( + vec!["ferese-screenshot".to_owned(), "--full".to_owned()].into(), + ) })); assert!(bindings.iter().any(|binding| { binding.modifiers.logo @@ -1269,18 +1359,26 @@ mod tests { assert_eq!(bindings.len(), 63); assert!(bindings.iter().any(|binding| { binding.action - == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into()) + == BindingAction::Spawn( + vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()].into(), + ) })); let input = unbound.input_settings().unwrap(); let bindings = unbound.bindings(&input).unwrap(); assert_eq!(bindings.len(), 62); - assert!(!bindings.iter().any(|binding| binding.action == BindingAction::Close)); + assert!( + !bindings + .iter() + .any(|binding| binding.action == BindingAction::Close) + ); } #[test] fn accepts_physical_xkb_key_names() { - let config = parse("binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n"); + let config = parse( + "binding keys=\"Super+AD06\" match=\"physical\" action=\"focus\" argument=\"left\"\n", + ); let input = config.input_settings().unwrap(); let bindings = config.bindings(&input).unwrap(); @@ -1292,9 +1390,13 @@ mod tests { #[test] fn rejects_duplicate_or_invalid_user_bindings() { - let duplicate = parse("binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n"); - let missing_command = parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n"); - let invalid_argument = parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n"); + let duplicate = parse( + "binding keys=\"Super+Q\" action=\"close\"\nbinding keys=\"logo+q\" action=\"close\"\n", + ); + let missing_command = + parse("binding keys=\"Super+Enter\" action=\"spawn\" argument=\"missing\"\n"); + let invalid_argument = + parse("binding keys=\"Super+Q\" action=\"close\" argument=\"left\"\n"); for config in [duplicate, missing_command, invalid_argument] { let input = config.input_settings().unwrap(); @@ -1308,7 +1410,12 @@ mod tests { "window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 min-width=500 min-height=400 fullscreen=#false block-out-from-screencasts=#true\n", ); let rules = config.window_rules().unwrap(); - let result = window_rules::resolve(&rules, Some("org.example.editor.desktop"), Some("Document"), false); + let result = window_rules::resolve( + &rules, + Some("org.example.editor.desktop"), + Some("Document"), + false, + ); assert_eq!(result.workspace, Some(3)); assert_eq!(result.floating, Some(true)); @@ -1406,24 +1513,34 @@ mod tests { ] { assert!(parse(source).output_profiles().is_err(), "{source}"); } - assert!(Config::parse_source("output-profile a layout=\"docked\" { output eDP-1; }").is_err()); + assert!( + Config::parse_source("output-profile a layout=\"docked\" { output eDP-1; }").is_err() + ); } #[test] fn automatic_refresh_is_opt_in_and_rejects_wrong_types() { - let config = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@120\" auto-refresh=#true; }"); + let config = parse( + "output-profile laptop { output eDP-1 mode=\"2880x1800@120\" auto-refresh=#true; }", + ); assert!(config.output_profiles().unwrap()[0].outputs[0].auto_refresh); let manual = parse("output-profile laptop { output eDP-1 mode=\"2880x1800@60\"; }"); assert!(!manual.output_profiles().unwrap()[0].outputs[0].auto_refresh); - assert!(Config::parse_source("output-profile laptop { output eDP-1 auto-refresh=\"yes\"; }").is_err()); + assert!( + Config::parse_source("output-profile laptop { output eDP-1 auto-refresh=\"yes\"; }") + .is_err() + ); } #[test] fn rejects_invalid_output_profiles() { - let invalid_scale = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n"); - let invalid_mode = parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n"); - let duplicate = - parse("output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n"); + let invalid_scale = + parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" scale=0.0\n}\n"); + let invalid_mode = + parse("output-profile name=\"bad\" {\n output match=\"eDP-1\" mode=\"native\"\n}\n"); + let duplicate = parse( + "output-profile name=\"bad\" {\n output match=\"eDP-1\"\n output match=\"eDP-1\"\n}\n", + ); assert!(invalid_scale.output_profiles().is_err()); assert!(invalid_mode.output_profiles().is_err()); diff --git a/compositor/src/config/appearance.rs b/compositor/src/config/appearance.rs index 6a0b019..8e33ddc 100644 --- a/compositor/src/config/appearance.rs +++ b/compositor/src/config/appearance.rs @@ -6,21 +6,39 @@ impl Config { Ok(FocusEffectSettings { enabled: effect.enabled, blur: effect.blur, - active_opacity: unit_theme_value(effect.active_opacity, "appearance.focus_effect.active_opacity")?, - inactive_opacity: unit_theme_value(effect.inactive_opacity, "appearance.focus_effect.inactive_opacity")?, - inactive_dim: unit_theme_value(effect.inactive_dim, "appearance.focus_effect.inactive_dim")?, - duration_ms: nonnegative_theme_value(effect.duration_ms, "appearance.focus_effect.duration_ms")?, + active_opacity: unit_theme_value( + effect.active_opacity, + "appearance.focus_effect.active_opacity", + )?, + inactive_opacity: unit_theme_value( + effect.inactive_opacity, + "appearance.focus_effect.inactive_opacity", + )?, + inactive_dim: unit_theme_value( + effect.inactive_dim, + "appearance.focus_effect.inactive_dim", + )?, + duration_ms: nonnegative_theme_value( + effect.duration_ms, + "appearance.focus_effect.duration_ms", + )?, }) } pub fn theme_settings(&self) -> Result { - let border_width = nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?; - let focus_ring_width = - nonnegative_theme_value(self.theme.geometry.focus_ring_width, "geometry.focus_ring_width")?; - let window_radius = nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?; - let shadow_offset_y = finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?; + let border_width = + nonnegative_theme_value(self.theme.geometry.border_width, "geometry.border_width")?; + let focus_ring_width = nonnegative_theme_value( + self.theme.geometry.focus_ring_width, + "geometry.focus_ring_width", + )?; + let window_radius = + nonnegative_theme_value(self.theme.geometry.window_radius, "geometry.window_radius")?; + let shadow_offset_y = + finite_theme_value(self.theme.shadow.soft.offset_y, "shadow.soft.offset_y")?; let shadow_blur = nonnegative_theme_value(self.theme.shadow.soft.blur, "shadow.soft.blur")?; - let shadow_opacity = unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?; + let shadow_opacity = + unit_theme_value(self.theme.shadow.soft.opacity, "shadow.soft.opacity")?; let material_radius = nonnegative_theme_value( self.theme.geometry.shell_radius.unwrap_or(14.0), @@ -30,13 +48,22 @@ impl Config { border_width, focus_ring_width, border_color: parse_color(&self.theme.colors.border, "colors.border")?, - text_primary_color: parse_color(&self.theme.colors.text_primary, "colors.text_primary")?, + text_primary_color: parse_color( + &self.theme.colors.text_primary, + "colors.text_primary", + )?, accent_color: parse_color(&self.theme.colors.accent, "colors.accent")?, border_gradient: self.theme.border.settings("border")?, focus_ring_gradient: self.theme.focus_ring.settings("focus_ring")?, shadow_color: parse_color(&self.theme.colors.shadow, "colors.shadow")?, - surface_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?, - surface_raised_color: parse_color(&self.theme.colors.surface_raised, "colors.surface_raised")?, + surface_base_color: parse_color( + &self.theme.colors.surface_base, + "colors.surface_base", + )?, + surface_raised_color: parse_color( + &self.theme.colors.surface_raised, + "colors.surface_raised", + )?, bar_background_color: parse_color( self.theme .surface @@ -52,8 +79,15 @@ impl Config { shadow_blur, shadow_opacity, material_style: self.theme.material.style, - backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0), - material_tint_strength: unit_theme_value(self.theme.material.tint_strength, "material.tint_strength")?, + backdrop_blur: nonnegative_theme_value( + self.theme.material.blur_radius, + "material.blur_radius", + )? + .min(32.0), + material_tint_strength: unit_theme_value( + self.theme.material.tint_strength, + "material.tint_strength", + )?, material_radius, panel_radius: material_radius, reduce_transparency: false, @@ -189,7 +223,11 @@ impl BorderPaintConfig { "focus_ring.gradient.angle", ) } else { - ("border.gradient.from", "border.gradient.to", "border.gradient.angle") + ( + "border.gradient.from", + "border.gradient.to", + "border.gradient.angle", + ) }; let gradient = self .gradient diff --git a/compositor/src/config/bindings.rs b/compositor/src/config/bindings.rs index 6715f63..4d02de2 100644 --- a/compositor/src/config/bindings.rs +++ b/compositor/src/config/bindings.rs @@ -12,7 +12,10 @@ impl Config { fn binding_commands(&self) -> HashMap> { let mut commands = HashMap::from([ ("terminal".to_owned(), vec!["foot".to_owned()]), - ("screenshot".to_owned(), vec!["ferese-screenshot".to_owned()]), + ( + "screenshot".to_owned(), + vec!["ferese-screenshot".to_owned()], + ), ( "screenshot-full".to_owned(), vec!["ferese-screenshot".to_owned(), "--full".to_owned()], @@ -36,7 +39,10 @@ impl Config { for configured in &self.bindings { let identity = binding_identity(configured, &keymap)?; if !supplied.insert(identity.clone()) { - return Err(ConfigError::Binding(format!("duplicate binding {:?}", configured.keys))); + return Err(ConfigError::Binding(format!( + "duplicate binding {:?}", + configured.keys + ))); } bindings.retain(|binding| binding_identity_for_runtime(binding) != identity); @@ -56,13 +62,19 @@ impl Config { } } -pub(super) fn validate_commands(commands: &HashMap>) -> Result<(), ConfigError> { +pub(super) fn validate_commands( + commands: &HashMap>, +) -> Result<(), ConfigError> { for (name, argv) in commands { if name.trim().is_empty() { - return Err(ConfigError::Binding("command names cannot be empty".to_owned())); + return Err(ConfigError::Binding( + "command names cannot be empty".to_owned(), + )); } if argv.is_empty() || argv[0].is_empty() { - return Err(ConfigError::Binding(format!("command {name:?} must contain a program"))); + return Err(ConfigError::Binding(format!( + "command {name:?} must contain a program" + ))); } } @@ -98,10 +110,9 @@ pub(super) fn parse_binding( } let identity = binding_identity(configured, keymap)?; - let action = configured - .action - .as_deref() - .ok_or_else(|| ConfigError::Binding(format!("binding {:?} has no action", configured.keys)))?; + let action = configured.action.as_deref().ok_or_else(|| { + ConfigError::Binding(format!("binding {:?} has no action", configured.keys)) + })?; let action = parse_action(action, configured.argument.as_deref(), commands)?; let trigger = match identity.match_mode { BindingMatch::Keysym => BindingTrigger::Keysym(identity.key), @@ -130,7 +141,9 @@ pub(super) fn binding_identity( ) -> Result { let (modifiers, key) = parse_chord(&configured.keys)?; if let Some((fingers, direction)) = parse_swipe(&key) { - if configured.match_mode == BindingMatch::Physical || modifiers != BindingModifiers::default() { + if configured.match_mode == BindingMatch::Physical + || modifiers != BindingModifiers::default() + { return Err(ConfigError::Binding( "swipes cannot use keyboard modifiers or physical key matching".into(), )); @@ -170,7 +183,9 @@ pub(super) fn binding_identity_for_runtime(binding: &Binding) -> BindingIdentity let (match_mode, key) = match binding.trigger { BindingTrigger::Keysym(key) => (BindingMatch::Keysym, key), BindingTrigger::Physical(key) => (BindingMatch::Physical, key.raw()), - BindingTrigger::Swipe { fingers, direction } => (BindingMatch::Swipe, fingers * 4 + direction as u32), + BindingTrigger::Swipe { fingers, direction } => { + (BindingMatch::Swipe, fingers * 4 + direction as u32) + } }; BindingIdentity { @@ -198,7 +213,9 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con }; if let Some(slot) = slot { if *slot { - return Err(ConfigError::Binding(format!("duplicate modifier in {chord:?}"))); + return Err(ConfigError::Binding(format!( + "duplicate modifier in {chord:?}" + ))); } *slot = true; } else if key.replace(component.to_owned()).is_some() { @@ -208,7 +225,9 @@ pub(super) fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), Con } } - let key = key.ok_or_else(|| ConfigError::Binding(format!("key chord {chord:?} does not contain a key")))?; + let key = key.ok_or_else(|| { + ConfigError::Binding(format!("key chord {chord:?} does not contain a key")) + })?; Ok((modifiers, key)) } @@ -264,8 +283,10 @@ pub(super) fn parse_action( Ok(()) } }; - let required_argument = - || argument.ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument"))); + let required_argument = || { + argument + .ok_or_else(|| ConfigError::Binding(format!("action {action:?} requires an argument"))) + }; match action { "binding-mode" => Ok(BindingAction::BindingMode(required_argument()?.to_owned())), @@ -275,7 +296,9 @@ pub(super) fn parse_action( } "workspace-next" | "workspace-previous" => { no_argument()?; - Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next")) + Ok(BindingAction::SwitchRelativeWorkspace( + action == "workspace-next", + )) } "workspace-back-and-forth" => { no_argument()?; @@ -310,9 +333,15 @@ pub(super) fn parse_action( } "focus" => Ok(BindingAction::Focus(parse_direction(required_argument()?)?)), "move" => Ok(BindingAction::Move(parse_direction(required_argument()?)?)), - "resize" => Ok(BindingAction::Resize(parse_direction(required_argument()?)?)), - "workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace(required_argument()?)?)), - "move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace(required_argument()?)?)), + "resize" => Ok(BindingAction::Resize( + parse_direction(required_argument()?)?, + )), + "workspace" => Ok(BindingAction::SwitchWorkspace(parse_workspace( + required_argument()?, + )?)), + "move-to-workspace" => Ok(BindingAction::MoveToWorkspace(parse_workspace( + required_argument()?, + )?)), "toggle-maximized" => { no_argument()?; Ok(BindingAction::ToggleMaximized) @@ -337,7 +366,9 @@ pub(super) fn parse_action( no_argument()?; Ok(BindingAction::ToggleColumnTabbed) } - "join-tab" => Ok(BindingAction::JoinTab(parse_direction(required_argument()?)?)), + "join-tab" => Ok(BindingAction::JoinTab(parse_direction( + required_argument()? + )?)), "leave-tab" => { no_argument()?; Ok(BindingAction::LeaveTab) @@ -396,14 +427,18 @@ pub(super) fn parse_direction(argument: &str) -> Result "right" => Ok(Direction::Right), "up" => Ok(Direction::Up), "down" => Ok(Direction::Down), - _ => Err(ConfigError::Binding(format!("invalid direction {argument:?}"))), + _ => Err(ConfigError::Binding(format!( + "invalid direction {argument:?}" + ))), } } pub(super) fn parse_workspace(argument: &str) -> Result { match argument.parse() { Ok(workspace) if workspace > 0 => Ok(workspace), - _ => Err(ConfigError::Binding(format!("invalid workspace {argument:?}"))), + _ => Err(ConfigError::Binding(format!( + "invalid workspace {argument:?}" + ))), } } @@ -446,13 +481,25 @@ pub(super) fn default_bindings() -> Vec { for (key, direction) in [("H", "left"), ("J", "down"), ("K", "up"), ("L", "right")] { bindings.push(binding("Super+".to_owned() + key, "focus", Some(direction))); - bindings.push(binding("Super+Shift+".to_owned() + key, "move", Some(direction))); - bindings.push(binding("Super+Ctrl+".to_owned() + key, "resize", Some(direction))); + bindings.push(binding( + "Super+Shift+".to_owned() + key, + "move", + Some(direction), + )); + bindings.push(binding( + "Super+Ctrl+".to_owned() + key, + "resize", + Some(direction), + )); } for workspace in 1..=9 { let workspace = workspace.to_string(); - bindings.push(binding(format!("Super+{workspace}"), "workspace", Some(&workspace))); + bindings.push(binding( + format!("Super+{workspace}"), + "workspace", + Some(&workspace), + )); bindings.push(binding( format!("Super+Shift+{workspace}"), "move-to-workspace", @@ -463,7 +510,11 @@ pub(super) fn default_bindings() -> Vec { bindings } -pub(super) fn binding(keys: impl Into, action: impl Into, argument: Option<&str>) -> BindingConfig { +pub(super) fn binding( + keys: impl Into, + action: impl Into, + argument: Option<&str>, +) -> BindingConfig { BindingConfig { keys: keys.into(), match_mode: BindingMatch::Keysym, @@ -557,7 +608,10 @@ impl Binding { let symbol = parse_keysym(&key)?; if (modifiers.ctrl && modifiers.alt - && matches!(symbol, keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12)) + && matches!( + symbol, + keysyms::KEY_Escape | keysyms::KEY_F1..=keysyms::KEY_F12 + )) || (keysyms::KEY_XF86Switch_VT_1..=keysyms::KEY_XF86Switch_VT_12).contains(&symbol) { return Err(ConfigError::Binding("Reserved system shortcut".into())); @@ -576,19 +630,34 @@ impl Binding { match (&self.trigger, &other.trigger) { (BindingTrigger::Keysym(left), BindingTrigger::Keysym(right)) => left == right, (BindingTrigger::Keysym(symbol), BindingTrigger::Physical(code)) - | (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => (0..map.num_layouts_for_key(*code)) - .any(|layout| { + | (BindingTrigger::Physical(code), BindingTrigger::Keysym(symbol)) => { + (0..map.num_layouts_for_key(*code)).any(|layout| { map.key_get_syms_by_level(*code, layout, 0) .iter() .any(|key| key.raw() == *symbol) - }), + }) + } (BindingTrigger::Physical(left), BindingTrigger::Physical(right)) => left == right, _ => false, } } - pub fn matches(&self, keycode: Keycode, keysyms: &[u32], logo: bool, ctrl: bool, alt: bool, shift: bool) -> bool { - self.modifiers == BindingModifiers { logo, ctrl, alt, shift } + pub fn matches( + &self, + keycode: Keycode, + keysyms: &[u32], + logo: bool, + ctrl: bool, + alt: bool, + shift: bool, + ) -> bool { + self.modifiers + == BindingModifiers { + logo, + ctrl, + alt, + shift, + } && match self.trigger { BindingTrigger::Keysym(expected) => keysyms.contains(&expected), BindingTrigger::Physical(expected) => expected == keycode, @@ -600,7 +669,11 @@ impl Binding { let description = match &self.action { BindingAction::BindingMode(name) => format!("Switch to binding mode {name}"), BindingAction::None => return None, - BindingAction::Spawn(argv) if argv.first().is_some_and(|program| program == "ferese-screenshot") => { + BindingAction::Spawn(argv) + if argv + .first() + .is_some_and(|program| program == "ferese-screenshot") => + { if argv.iter().any(|arg| arg == "--full") { "Capture whole screen" } else { @@ -619,7 +692,9 @@ impl Binding { BindingAction::Exit => "Log out (with confirmation)".into(), BindingAction::Focus(direction) => format!("Focus window {direction:?}"), BindingAction::FocusLastWindow => "Return to last focused window".into(), - BindingAction::FocusFloating => "Switch focus between tiled and floating windows".into(), + BindingAction::FocusFloating => { + "Switch focus between tiled and floating windows".into() + } BindingAction::FocusMru(reverse) => if *reverse { "Previous window in focus history" } else { @@ -629,11 +704,16 @@ impl Binding { BindingAction::Move(direction) => format!("Move window {direction:?}"), BindingAction::Resize(direction) => format!("Resize window {direction:?}"), BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"), - BindingAction::WorkspaceBackAndForth => "Return to last workspace on this monitor".into(), - BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"), - BindingAction::SwitchRelativeWorkspace(next) => { - if *next { "Next workspace" } else { "Previous workspace" }.into() + BindingAction::WorkspaceBackAndForth => { + "Return to last workspace on this monitor".into() } + BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"), + BindingAction::SwitchRelativeWorkspace(next) => if *next { + "Next workspace" + } else { + "Previous workspace" + } + .into(), BindingAction::ToggleFullscreen => "Toggle fullscreen".into(), BindingAction::ToggleMaximized => "Toggle full-width window".into(), BindingAction::ToggleLayout => "Switch workspace layout".into(), @@ -642,7 +722,12 @@ impl Binding { BindingAction::ToggleColumnTabbed => "Toggle stacked / tabbed column".into(), BindingAction::JoinTab(direction) => format!("Join tab group {direction:?}"), BindingAction::LeaveTab => "Leave tab group".into(), - BindingAction::CycleTab(previous) => if *previous { "Previous tab" } else { "Next tab" }.into(), + BindingAction::CycleTab(previous) => if *previous { + "Previous tab" + } else { + "Next tab" + } + .into(), BindingAction::Consume => "Join window into column or tab group".into(), BindingAction::Expel => "Leave column or tab group".into(), BindingAction::ToggleFloating => "Toggle floating window".into(), @@ -694,7 +779,11 @@ impl Binding { matches!(self.trigger, BindingTrigger::Swipe { fingers, .. } if fingers == count) } - pub(crate) fn matches_swipe(&self, count: u32, target: crate::gestures::SwipeDirection) -> bool { + pub(crate) fn matches_swipe( + &self, + count: u32, + target: crate::gestures::SwipeDirection, + ) -> bool { matches!(self.trigger, BindingTrigger::Swipe { fingers, direction } if fingers == count && direction == target) } } diff --git a/compositor/src/config/bindings/index.rs b/compositor/src/config/bindings/index.rs index 603c9e8..621b8d0 100644 --- a/compositor/src/config/bindings/index.rs +++ b/compositor/src/config/bindings/index.rs @@ -42,7 +42,12 @@ impl BindingSet { alt: bool, shift: bool, ) -> Option<&Binding> { - let modifiers = BindingModifiers { logo, ctrl, alt, shift }; + let modifiers = BindingModifiers { + logo, + ctrl, + alt, + shift, + }; let physical = self.keyboard.get(&BindingIdentity { modifiers, match_mode: BindingMatch::Physical, @@ -96,7 +101,10 @@ mod tests { let code = Keycode::new(38); let bindings = vec![ binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::Close), - binding_for(BindingTrigger::Physical(code), BindingAction::ToggleFloating), + binding_for( + BindingTrigger::Physical(code), + BindingAction::ToggleFloating, + ), binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Exit), binding_for(BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None), ]; @@ -125,7 +133,10 @@ mod tests { let config = Config::default(); let mut bindings = config.bindings(&config.input_settings().unwrap()).unwrap(); bindings.extend([ - binding_for(BindingTrigger::Physical(Keycode::new(38)), BindingAction::None), + binding_for( + BindingTrigger::Physical(Keycode::new(38)), + BindingAction::None, + ), binding_for(BindingTrigger::Keysym(keysyms::KEY_a), BindingAction::Close), binding_for( BindingTrigger::Swipe { @@ -138,7 +149,8 @@ mod tests { let index = BindingSet::from(bindings); for bits in 0..16 { - let (logo, ctrl, alt, shift) = (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0); + let (logo, ctrl, alt, shift) = + (bits & 1 != 0, bits & 2 != 0, bits & 4 != 0, bits & 8 != 0); for code in 8..100 { for symbols in [ @@ -164,7 +176,10 @@ mod tests { #[test] fn replacing_bindings_discards_old_index_entries() { let code = Keycode::new(38); - let mut bindings = BindingSet::from(vec![binding_for(BindingTrigger::Physical(code), BindingAction::Close)]); + let mut bindings = BindingSet::from(vec![binding_for( + BindingTrigger::Physical(code), + BindingAction::Close, + )]); assert_eq!( bindings.action_for_key(code, &[], false, false, false, false), Some(&BindingAction::Close) @@ -173,7 +188,10 @@ mod tests { BindingTrigger::Keysym(keysyms::KEY_b), BindingAction::None, )]); - assert_eq!(bindings.action_for_key(code, &[], false, false, false, false), None); + assert_eq!( + bindings.action_for_key(code, &[], false, false, false, false), + None + ); assert_eq!( bindings.action_for_key(code, &[keysyms::KEY_b], false, false, false, false), Some(&BindingAction::None) @@ -182,9 +200,11 @@ mod tests { #[test] fn cloning_spawn_actions_shares_the_command_storage() { - let action = BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into()); + let action = + BindingAction::Spawn(vec!["foot".into(), "--app-id".into(), "work".into()].into()); let cloned = action.clone(); - let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) else { + let (BindingAction::Spawn(original), BindingAction::Spawn(cloned)) = (&action, cloned) + else { unreachable!(); }; assert!(Arc::ptr_eq(original, &cloned)); diff --git a/compositor/src/config/bindings/modes.rs b/compositor/src/config/bindings/modes.rs index 648032a..bb4402d 100644 --- a/compositor/src/config/bindings/modes.rs +++ b/compositor/src/config/bindings/modes.rs @@ -75,16 +75,24 @@ impl Config { if configured.disabled || identity.match_mode == BindingMatch::Swipe || !identities.insert(identity.clone()) - || (identity.match_mode == BindingMatch::Keysym && identity.key == keysyms::KEY_Escape) + || (identity.match_mode == BindingMatch::Keysym + && identity.key == keysyms::KEY_Escape) || (identity.match_mode == BindingMatch::Physical - && (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any(|layout| { - (0..keymap.num_levels_for_key(Keycode::new(identity.key), layout)).any(|level| { - keymap - .key_get_syms_by_level(Keycode::new(identity.key), layout, level) - .iter() - .any(|symbol| symbol.raw() == keysyms::KEY_Escape) - }) - })) + && (0..keymap.num_layouts_for_key(Keycode::new(identity.key))).any( + |layout| { + (0..keymap.num_levels_for_key(Keycode::new(identity.key), layout)) + .any(|level| { + keymap + .key_get_syms_by_level( + Keycode::new(identity.key), + layout, + level, + ) + .iter() + .any(|symbol| symbol.raw() == keysyms::KEY_Escape) + }) + }, + )) { return Err(ConfigError::Binding(format!( "invalid, duplicate or reserved mode binding {:?}", @@ -95,12 +103,17 @@ impl Config { } modes.maps.insert(mode.name.clone(), bindings.into()); } - for binding in default.iter().chain(modes.maps.values().flat_map(|map| map.iter())) { + for binding in default + .iter() + .chain(modes.maps.values().flat_map(|map| map.iter())) + { if let BindingAction::BindingMode(name) = &binding.action && name != "default" && !modes.maps.contains_key(name) { - return Err(ConfigError::Binding(format!("unknown binding mode {name:?}"))); + return Err(ConfigError::Binding(format!( + "unknown binding mode {name:?}" + ))); } } Ok(modes) @@ -131,7 +144,14 @@ mod tests { assert!(modes.set("resize")); let selected = modes.bindings(&default); assert_eq!( - selected.action_for_key(Keycode::new(43), &[keysyms::KEY_h], false, false, false, false), + selected.action_for_key( + Keycode::new(43), + &[keysyms::KEY_h], + false, + false, + false, + false + ), Some(&BindingAction::Resize(Direction::Left)) ); assert_eq!( @@ -139,11 +159,25 @@ mod tests { Some(&BindingAction::Resize(Direction::Up)) ); assert_eq!( - selected.action_for_key(Keycode::new(43), &[keysyms::KEY_h], true, false, false, false), + selected.action_for_key( + Keycode::new(43), + &[keysyms::KEY_h], + true, + false, + false, + false + ), None ); assert_eq!( - selected.action_for_key(Keycode::new(36), &[keysyms::KEY_Return], true, false, false, false), + selected.action_for_key( + Keycode::new(36), + &[keysyms::KEY_Return], + true, + false, + false, + false + ), None ); assert!(!modes.set("missing")); diff --git a/compositor/src/config/input_motion.rs b/compositor/src/config/input_motion.rs index dad8bbd..6b550c8 100644 --- a/compositor/src/config/input_motion.rs +++ b/compositor/src/config/input_motion.rs @@ -57,11 +57,15 @@ impl Config { } pub fn spring_config(&self) -> Result { - self.animations.spring_config().map_err(ConfigError::Binding) + self.animations + .spring_config() + .map_err(ConfigError::Binding) } pub fn viewport_spring_config(&self) -> Result { - self.animations.viewport_config().map_err(ConfigError::Binding) + self.animations + .viewport_config() + .map_err(ConfigError::Binding) } } @@ -156,7 +160,8 @@ mod tests { #[test] fn compositor_speed_uses_shared_motion_bounds() { for speed in [1e-100, 0.009, 101.0, 1e100] { - let config = Config::parse_source(&format!("animations {{ speed {speed:e}; }}")).unwrap(); + let config = + Config::parse_source(&format!("animations {{ speed {speed:e}; }}")).unwrap(); assert!(config.animation_speed().is_err()); } } diff --git a/compositor/src/config/layout.rs b/compositor/src/config/layout.rs index b07b001..c6d25d9 100644 --- a/compositor/src/config/layout.rs +++ b/compositor/src/config/layout.rs @@ -58,7 +58,11 @@ impl Config { } pub fn scrolling_focus_strategy(&self) -> ViewportFocusStrategy { - match self.scrolling.focus_strategy.unwrap_or(FocusStrategyValue::Minimal) { + match self + .scrolling + .focus_strategy + .unwrap_or(FocusStrategyValue::Minimal) + { FocusStrategyValue::Minimal => ViewportFocusStrategy::Minimal, FocusStrategyValue::CenterOnFocus => ViewportFocusStrategy::Center, FocusStrategyValue::Paged => ViewportFocusStrategy::Paged, @@ -66,7 +70,10 @@ impl Config { } } -pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Result { +pub(super) fn nonnegative_layout_value( + value: f64, + field: &'static str, +) -> Result { if value.is_finite() && value >= 0.0 { Ok(value) } else { @@ -74,10 +81,17 @@ pub(super) fn nonnegative_layout_value(value: f64, field: &'static str) -> Resul } } -pub(super) fn parse_column_width(value: &ColumnWidthValue, field: &'static str) -> Result { +pub(super) fn parse_column_width( + value: &ColumnWidthValue, + field: &'static str, +) -> Result { match value { - ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => Ok(ColumnWidth::Proportion(*value)), - ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => Ok(ColumnWidth::Full), + ColumnWidthValue::Proportion(value) if value.is_finite() && *value > 0.0 => { + Ok(ColumnWidth::Proportion(*value)) + } + ColumnWidthValue::Named(value) if value.eq_ignore_ascii_case("full") => { + Ok(ColumnWidth::Full) + } value => Err(ConfigError::ColumnWidth { field, value: match value { diff --git a/compositor/src/config/loading.rs b/compositor/src/config/loading.rs index 8bba48a..8fa9bee 100644 --- a/compositor/src/config/loading.rs +++ b/compositor/src/config/loading.rs @@ -20,7 +20,10 @@ pub(crate) fn read_source(path: &Path) -> Result { Ok(source) } -pub(crate) fn prepare(source: &str, directory: &Path) -> Result<(Config, RuntimeConfig, Candidate), String> { +pub(crate) fn prepare( + source: &str, + directory: &Path, +) -> Result<(Config, RuntimeConfig, Candidate), String> { let document = Document::parse(source).map_err(|e| e.to_string())?; prepare_document(&document, directory) } @@ -68,9 +71,11 @@ pub(crate) fn prepare_document( ) -> Result<(Config, RuntimeConfig, Candidate), String> { let candidate = crate::theme::resolve_candidate(document, directory)?; let config: Config = - serde_json::from_value(document.with_theme(&candidate.theme).value().clone()).map_err(|e| e.to_string())?; + serde_json::from_value(document.with_theme(&candidate.theme).value().clone()) + .map_err(|e| e.to_string())?; let mut runtime = config.runtime_config().map_err(|e| e.to_string())?; - runtime.theme_settings = Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?; + runtime.theme_settings = + Config::resolved_theme_settings(&candidate.theme).map_err(|e| e.to_string())?; Ok((config, runtime, candidate)) } @@ -119,7 +124,10 @@ mod tests { let valid = startup(Some(&path)).unwrap(); assert!(valid.error.is_none()); assert_eq!(valid.source.as_deref(), Some(source)); - assert_eq!(valid.runtime.panel_style, ferese_panel::PanelStyle::Floating); + assert_eq!( + valid.runtime.panel_style, + ferese_panel::PanelStyle::Floating + ); assert_eq!( Document::parse(valid.source.as_deref().unwrap()) .unwrap() diff --git a/compositor/src/config/outputs.rs b/compositor/src/config/outputs.rs index 3426cba..b9c7d79 100644 --- a/compositor/src/config/outputs.rs +++ b/compositor/src/config/outputs.rs @@ -9,13 +9,19 @@ impl Config { .map(|profile| { let name = profile.name.trim(); if name.is_empty() { - return Err(ConfigError::OutputProfile("profile names cannot be empty".to_owned())); + return Err(ConfigError::OutputProfile( + "profile names cannot be empty".to_owned(), + )); } if !names.insert(name.to_owned()) { - return Err(ConfigError::OutputProfile(format!("duplicate profile name {name:?}"))); + return Err(ConfigError::OutputProfile(format!( + "duplicate profile name {name:?}" + ))); } if profile.outputs.is_empty() { - return Err(ConfigError::OutputProfile(format!("profile {name:?} has no outputs"))); + return Err(ConfigError::OutputProfile(format!( + "profile {name:?} has no outputs" + ))); } let mut matchers = HashSet::new(); @@ -70,13 +76,16 @@ impl Config { ))); } if let Some(source) = &profile.mirror_source - && !outputs.iter().any(|output| &output.matcher == source && output.enabled) + && !outputs + .iter() + .any(|output| &output.matcher == source && output.enabled) { return Err(ConfigError::OutputProfile(format!( "profile {name:?}: mirror source must name an enabled output" ))); } - if profile.layout == OutputLayout::Mirror && outputs.iter().filter(|output| output.enabled).count() < 2 + if profile.layout == OutputLayout::Mirror + && outputs.iter().filter(|output| output.enabled).count() < 2 { return Err(ConfigError::OutputProfile(format!( "profile {name:?}: mirror needs at least two enabled outputs" @@ -101,7 +110,9 @@ pub(super) fn parse_output_mode(value: &str) -> Result, field| value.as_ref().map(|s| parse_color(s, field).map(|c| c.0)).transpose(); + let color = |value: &Option, field| { + value + .as_ref() + .map(|s| parse_color(s, field).map(|c| c.0)) + .transpose() + }; let width = range(c.width, 1.0, 16.0, "tab_indicator.width")?; let hit_width = range(c.hit_width, width, 64.0, "tab_indicator.hit_width")?; let font_size = range(c.font_size, 8.0, 32.0, "tab_indicator.font_size")?; @@ -85,11 +89,21 @@ impl Config { font_size, outline_width: range(c.outline_width, 0.0, 8.0, "tab_indicator.outline_width")?, outline_color: color(&c.outline_color, "tab_indicator.outline_color")?, - segment_height: range(c.segment_height, width, 128.0, "tab_indicator.segment_height")?, + segment_height: range( + c.segment_height, + width, + 128.0, + "tab_indicator.segment_height", + )?, segment_gap: range(c.segment_gap, 0.0, 64.0, "tab_indicator.segment_gap")?, gap_threshold: range(c.gap_threshold, width, 128.0, "tab_indicator.gap_threshold")?, list_width: range(c.list_width, 80.0, 640.0, "tab_indicator.list_width")?, - row_height: range(c.row_height, font_size + 6.0, 80.0, "tab_indicator.row_height")?, + row_height: range( + c.row_height, + font_size + 6.0, + 80.0, + "tab_indicator.row_height", + )?, padding: range(c.padding, 0.0, 24.0, "tab_indicator.padding")?, flash_duration: std::time::Duration::from_millis(range( c.flash_duration_ms as f64, @@ -111,9 +125,18 @@ mod tests { #[test] fn indicator_defaults_overrides_and_validation() { - let c = Config::parse_source("").unwrap().tab_indicator_settings().unwrap(); + let c = Config::parse_source("") + .unwrap() + .tab_indicator_settings() + .unwrap(); assert_eq!( - (c.width, c.segment_height, c.segment_gap, c.hit_width, c.gap_threshold), + ( + c.width, + c.segment_height, + c.segment_gap, + c.hit_width, + c.gap_threshold + ), (4., 24., 4., 12., 8.) ); assert_eq!(c.flash_duration.as_millis(), 1000); @@ -123,7 +146,10 @@ mod tests { ) .unwrap(); let c = config.tab_indicator_settings().unwrap(); - assert_eq!((c.width, c.hit_width, c.flash_duration.as_millis()), (3., 14., 750)); + assert_eq!( + (c.width, c.hit_width, c.flash_duration.as_millis()), + (3., 14., 750) + ); assert_eq!(c.active_color, Some([1., 0., 0., 1.])); for source in [ "width 0", @@ -133,7 +159,9 @@ mod tests { "flash-duration-ms 60001", "active-color \"invalid\"", ] { - let config = Config::parse_source(&format!("scrolling {{ tab-indicator {{ {source}; }}; }}")).unwrap(); + let config = + Config::parse_source(&format!("scrolling {{ tab-indicator {{ {source}; }}; }}")) + .unwrap(); assert!(config.tab_indicator_settings().is_err(), "{source}"); } assert!(Config::parse_source("scrolling { tab-indicator { typo 1; }; }").is_err()); diff --git a/compositor/src/cursor.rs b/compositor/src/cursor.rs index fcbc305..855aeed 100644 --- a/compositor/src/cursor.rs +++ b/compositor/src/cursor.rs @@ -42,7 +42,11 @@ impl CursorFrame { ) } - pub(crate) fn physical_location(&self, pointer: Point, scale: f64) -> Point { + pub(crate) fn physical_location( + &self, + pointer: Point, + scale: f64, + ) -> Point { self.logical_rect(pointer).loc.to_physical(scale) } } @@ -64,7 +68,10 @@ impl NamedCursor { let mut offset = elapsed.as_nanos() % period.as_nanos(); for (index, frame) in self.frames.iter().enumerate() { if offset < frame.delay.as_nanos() { - return (index, Some(frame.delay - Duration::from_nanos(offset as u64))); + return ( + index, + Some(frame.delay - Duration::from_nanos(offset as u64)), + ); } offset -= frame.delay.as_nanos(); } @@ -97,7 +104,9 @@ impl Loader { let mut theme = None; move |(icon, scale)| { let theme = theme.get_or_insert_with(|| { - CursorTheme::load(&std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into())) + CursorTheme::load( + &std::env::var("XCURSOR_THEME").unwrap_or_else(|_| "default".into()), + ) }); load_named_cursor(theme, icon, scale) } @@ -135,7 +144,14 @@ impl Loader { drop(requests); let cursor = load(request); let (icon, scale) = request; - if sender.send(Loaded { icon, scale, cursor }).is_err() { + if sender + .send(Loaded { + icon, + scale, + cursor, + }) + .is_err() + { break; } signal.wakeup(); @@ -218,7 +234,12 @@ impl crate::Ferese { } .unwrap_or(1) .clamp(1, 8); - while let Ok(Loaded { icon, scale, cursor }) = self.cursor_loader.results.try_recv() { + while let Ok(Loaded { + icon, + scale, + cursor, + }) = self.cursor_loader.results.try_recv() + { let mut cursor = cursor.unwrap_or_else(fallback_cursor); cursor.requested_scale = scale; let active = match self.cursor_status { @@ -230,12 +251,9 @@ impl crate::Ferese { && active.is_some_and(|active| !self.named_cursors.contains_key(&active))); let mut bytes: usize = self.named_cursors.values().map(|cursor| cursor.bytes).sum(); while bytes + cursor.bytes > MAX_CACHE_BYTES { - let Some(evict) = self - .named_cursors - .keys() - .copied() - .find(|key| *key != CursorIcon::Default && *key != icon && Some(*key) != active) - else { + let Some(evict) = self.named_cursors.keys().copied().find(|key| { + *key != CursorIcon::Default && *key != icon && Some(*key) != active + }) else { break; }; bytes -= self.named_cursors.remove(&evict).unwrap().bytes; @@ -243,7 +261,8 @@ impl crate::Ferese { self.named_cursors.insert(icon, cursor); if affects_current { self.cursor_animation.reset = true; - self.cursor_redraw_pending |= crate::backends::direct::cursor_animation_visible(self); + self.cursor_redraw_pending |= + crate::backends::direct::cursor_animation_visible(self); } } @@ -257,7 +276,8 @@ impl crate::Ferese { .get(&icon) .is_none_or(|cursor| cursor.requested_scale != self.cursor_animation.buffer_scale) { - self.cursor_loader.request(icon, self.cursor_animation.buffer_scale); + self.cursor_loader + .request(icon, self.cursor_animation.buffer_scale); } let animated = icon .and_then(|icon| { @@ -285,7 +305,8 @@ impl crate::Ferese { .get(&icon) .or_else(|| self.named_cursors.get(&CursorIcon::Default)) .unwrap(); - let (frame, remaining) = cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch)); + let (frame, remaining) = + cursor.sample(now.saturating_duration_since(self.cursor_animation.epoch)); if self.cursor_animation.frame != frame { self.cursor_animation.frame = frame; self.cursor_redraw_pending = true; @@ -293,13 +314,14 @@ impl crate::Ferese { if let Some(remaining) = remaining && self.cursor_animation.timer.is_none() { - match self - .loop_handle - .insert_source(Timer::from_deadline(now + remaining), |_, _, state| { + match self.loop_handle.insert_source( + Timer::from_deadline(now + remaining), + |_, _, state| { state.cursor_animation.timer = None; state.cursor_redraw_pending = true; TimeoutAction::Drop - }) { + }, + ) { Ok(token) => self.cursor_animation.timer = Some(token), Err(error) => tracing::warn!(%error, "could not schedule animated cursor"), } @@ -321,7 +343,9 @@ fn load_named_cursor(theme: &CursorTheme, icon: CursorIcon, scale: i32) -> Optio return None; } let mut bytes = Vec::new(); - file.take((MAX_FILE_BYTES + 1) as u64).read_to_end(&mut bytes).ok()?; + file.take((MAX_FILE_BYTES + 1) as u64) + .read_to_end(&mut bytes) + .ok()?; parse_cursor(&bytes, requested, scale) }) } @@ -348,7 +372,10 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option } let header = word(bytes, 4)? as usize; let count = word(bytes, 12)? as usize; - if header < 16 || count > MAX_FRAMES || header.checked_add(count.checked_mul(12)?)? > bytes.len() { + if header < 16 + || count > MAX_FRAMES + || header.checked_add(count.checked_mul(12)?)? > bytes.len() + { return None; } let mut images = Vec::new(); @@ -371,7 +398,14 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option let height = word(bytes, pos + 20)?; let xhot = word(bytes, pos + 24)?; let yhot = word(bytes, pos + 28)?; - if nominal == 0 || width == 0 || height == 0 || width > 1024 || height > 1024 || xhot > width || yhot > height { + if nominal == 0 + || width == 0 + || height == 0 + || width > 1024 + || height > 1024 + || xhot > width + || yhot > height + { return None; } let pixels = (width as usize).checked_mul(height as usize)?; @@ -382,7 +416,15 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option let start = pos.checked_add(36)?; let end = start.checked_add(pixels.checked_mul(4)?)?; let rgba = bytes.get(start..end)?; - images.push((nominal, width, height, xhot, yhot, word(bytes, pos + 32)?, rgba)); + images.push(( + nominal, + width, + height, + xhot, + yhot, + word(bytes, pos + 32)?, + rgba, + )); } let base = images .iter() @@ -396,7 +438,9 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option .min_by_key(|size| (size.abs_diff(desired), *size))?; let mut frames = Vec::new(); let mut asset_bytes = 0usize; - for (_, width, height, xhot, yhot, delay, rgba) in images.into_iter().filter(|image| image.0 == nominal) { + for (_, width, height, xhot, yhot, delay, rgba) in + images.into_iter().filter(|image| image.0 == nominal) + { let ratio = f64::from(desired) / f64::from(nominal); let w = (f64::from(width) * ratio).round().max(1.0) as u32; let h = (f64::from(height) * ratio).round().max(1.0) as u32; @@ -404,20 +448,29 @@ fn parse_cursor(bytes: &[u8], requested: u32, scale: i32) -> Option return None; } let size: Size = (w as i32, h as i32).into(); - asset_bytes = asset_bytes.checked_add((w as usize).checked_mul(h as usize)?.checked_mul(4)?)?; + asset_bytes = + asset_bytes.checked_add((w as usize).checked_mul(h as usize)?.checked_mul(4)?)?; if asset_bytes > MAX_PIXELS * 4 { return None; } let resized; let pixels = if w != width || h != height { let original = image::RgbaImage::from_raw(width, height, rgba.to_vec())?; - resized = image::imageops::resize(&original, w, h, image::imageops::FilterType::Triangle); + resized = + image::imageops::resize(&original, w, h, image::imageops::FilterType::Triangle); resized.as_raw().as_slice() } else { rgba }; frames.push(CursorFrame { - buffer: MemoryRenderBuffer::from_slice(pixels, Fourcc::Abgr8888, size, scale, Transform::Normal, None), + buffer: MemoryRenderBuffer::from_slice( + pixels, + Fourcc::Abgr8888, + size, + scale, + Transform::Normal, + None, + ), hotspot: ( (f64::from(xhot) * ratio).round() as i32, (f64::from(yhot) * ratio).round() as i32, diff --git a/compositor/src/cursor/tests.rs b/compositor/src/cursor/tests.rs index a4991bf..89a56a5 100644 --- a/compositor/src/cursor/tests.rs +++ b/compositor/src/cursor/tests.rs @@ -7,17 +7,27 @@ use smithay::utils::SERIAL_COUNTER; fn file(frames: &[(u32, u32, u32, u32)]) -> Vec { let mut bytes = b"Xcur".to_vec(); - bytes.extend([16u32, 1, frames.len() as u32].into_iter().flat_map(u32::to_le_bytes)); + bytes.extend( + [16u32, 1, frames.len() as u32] + .into_iter() + .flat_map(u32::to_le_bytes), + ); let mut pos = 16 + frames.len() * 12; for &(size, width, _, _) in frames { - bytes.extend([0xfffd0002, size, pos as u32].into_iter().flat_map(u32::to_le_bytes)); + bytes.extend( + [0xfffd0002, size, pos as u32] + .into_iter() + .flat_map(u32::to_le_bytes), + ); pos += 36 + width as usize * width as usize * 4; } for &(size, width, hotspot, delay) in frames { bytes.extend( - [36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay] - .into_iter() - .flat_map(u32::to_le_bytes), + [ + 36, 0xfffd0002, size, 1, width, width, hotspot, hotspot, delay, + ] + .into_iter() + .flat_map(u32::to_le_bytes), ); bytes.extend([255, 255, 255, 255].repeat(width as usize * width as usize)); } @@ -49,12 +59,20 @@ fn unavailable_hidpi_size_preserves_logical_size_and_hotspot() { assert_eq!(frame.buffer_scale, 2); assert_eq!(frame.size.w, 64); assert_eq!(frame.hotspot.x, 32); - assert_eq!(frame.logical_rect((100.0, 100.0).into()).size, (32.0, 32.0).into()); + assert_eq!( + frame.logical_rect((100.0, 100.0).into()).size, + (32.0, 32.0).into() + ); } #[test] fn selects_a_nominal_sequence_and_preserves_frame_delays() { - let asset = parse_cursor(&file(&[(48, 48, 24, 70), (24, 24, 12, 30), (24, 24, 12, 80)]), 24, 1).unwrap(); + let asset = parse_cursor( + &file(&[(48, 48, 24, 70), (24, 24, 12, 30), (24, 24, 12, 80)]), + 24, + 1, + ) + .unwrap(); assert_eq!(asset.frames.len(), 2); for (time, index, remaining) in [ (0, 0, 30), @@ -93,7 +111,11 @@ fn rejects_oversized_and_malformed_assets_before_buffer_allocation() { bytes.truncate(16); bytes[12..16].copy_from_slice(&3u32.to_le_bytes()); for _ in 0..3 { - bytes.extend([0xfffd0002, 1024, 52].into_iter().flat_map(u32::to_le_bytes)); + bytes.extend( + [0xfffd0002, 1024, 52] + .into_iter() + .flat_map(u32::to_le_bytes), + ); } bytes.extend(frame); assert!(parse_cursor(&bytes, 24, 1).is_none()); @@ -143,9 +165,15 @@ fn delayed_loading_does_not_block_cursor_callbacks_or_pointer_dispatch() { state.named_cursor_frame().is_some(), "cached fallback must remain visible" ); - assert!(calls.try_recv().is_err(), "decoder is still deliberately blocked"); + assert!( + calls.try_recv().is_err(), + "decoder is still deliberately blocked" + ); release.send(()).unwrap(); - assert_eq!(calls.recv_timeout(Duration::from_secs(2)).unwrap().0, CursorIcon::Text); + assert_eq!( + calls.recv_timeout(Duration::from_secs(2)).unwrap().0, + CursorIcon::Text + ); } #[test] @@ -188,7 +216,9 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() { assert!(state.cursor_animation.timer.is_some()); // Invoke the same deadline callback by dispatching the real timer. while Instant::now() < now + Duration::from_millis(35) { - events.dispatch(Duration::from_millis(5), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(5), &mut state) + .unwrap(); } state.update_named_cursor(now + Duration::from_millis(30)); assert_eq!(state.cursor_animation.frame, 1); @@ -200,7 +230,9 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() { state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Wait)); state.update_named_cursor(now + Duration::from_millis(32)); assert!(state.cursor_animation.timer.is_some()); - state.named_cursors.insert(CursorIcon::Text, fallback_cursor()); + state + .named_cursors + .insert(CursorIcon::Text, fallback_cursor()); state.cursor_image(&seat, CursorImageStatus::Named(CursorIcon::Text)); state.update_named_cursor(now + Duration::from_millis(33)); assert!(state.cursor_animation.timer.is_none()); @@ -208,9 +240,18 @@ fn cursor_animation_advances_and_cancels_on_hide_and_shape_change() { #[test] fn supplies_legacy_fallback_names_for_common_cursors() { - assert_eq!(cursor_names(CursorIcon::Default), ["default", "left_ptr", "arrow"]); - assert_eq!(cursor_names(CursorIcon::Pointer), ["pointer", "hand2", "left_ptr"]); - assert_eq!(cursor_names(CursorIcon::Text), ["text", "xterm", "left_ptr"]); + assert_eq!( + cursor_names(CursorIcon::Default), + ["default", "left_ptr", "arrow"] + ); + assert_eq!( + cursor_names(CursorIcon::Pointer), + ["pointer", "hand2", "left_ptr"] + ); + assert_eq!( + cursor_names(CursorIcon::Text), + ["text", "xterm", "left_ptr"] + ); assert_eq!( cursor_names(CursorIcon::Crosshair), ["crosshair", "default", "left_ptr"] diff --git a/compositor/src/daemon.rs b/compositor/src/daemon.rs index 36d6a3c..23eb595 100644 --- a/compositor/src/daemon.rs +++ b/compositor/src/daemon.rs @@ -98,15 +98,16 @@ impl Runner { } if let Some(deadline) = deadline { let weak = Rc::downgrade(owner); - match state - .loop_handle - .insert_source(Timer::from_deadline(deadline), move |_, _, state| { + match state.loop_handle.insert_source( + Timer::from_deadline(deadline), + move |_, _, state| { if let Some(owner) = weak.upgrade() { owner.borrow_mut().2 = None; Self::refresh(&owner, state); } TimeoutAction::Drop - }) { + }, + ) { Ok(token) => runner.2 = Some((token, deadline)), Err(error) => tracing::warn!(%error, "could not arm daemon restart deadline"), } @@ -119,7 +120,11 @@ impl Runner { .iter() .filter(|c| c.enabled && policy.allows_autostart(c.nested)) { - if self.0.iter().any(|service| service.config.command == config.command) { + if self + .0 + .iter() + .any(|service| service.config.command == config.command) + { continue; } if let Some(index) = previous @@ -187,7 +192,11 @@ impl Runner { } } if !service.finished && now >= service.next_start { - service.child = spawn_client(state, service.config.command.iter(), ClientCapabilities::default()); + service.child = spawn_client( + state, + service.config.command.iter(), + ClientCapabilities::default(), + ); service.next_start = now + Duration::from_secs(5); if service.child.is_none() && !service.config.restart { service.finished = true; @@ -225,14 +234,19 @@ mod tests { let mut events = smithay::reexports::calloop::EventLoop::::try_new().unwrap(); events .handle() - .insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, count| { - assert_eq!(child.wait().unwrap().code(), Some(7)); - *count += 1; - Ok(PostAction::Remove) - }) + .insert_source( + Generic::new(fd, Interest::READ, Mode::Level), + move |_, _, count| { + assert_eq!(child.wait().unwrap().code(), Some(7)); + *count += 1; + Ok(PostAction::Remove) + }, + ) .unwrap(); let mut count = 0; - events.dispatch(Some(Duration::from_secs(2)), &mut count).unwrap(); + events + .dispatch(Some(Duration::from_secs(2)), &mut count) + .unwrap(); assert_eq!(count, 1); events.dispatch(Some(Duration::ZERO), &mut count).unwrap(); assert_eq!(count, 1); @@ -247,7 +261,12 @@ mod tests { nested: false, }; let mut runner = Runner::new(&[config.clone()], SessionPolicy::Desktop); - runner.0[0].child = Some(std::process::Command::new("/bin/sleep").arg("30").spawn().unwrap()); + runner.0[0].child = Some( + std::process::Command::new("/bin/sleep") + .arg("30") + .spawn() + .unwrap(), + ); let pid = runner.0[0].child.as_ref().unwrap().id(); config.restart = true; runner.reconcile(&[config.clone()], SessionPolicy::Desktop); @@ -260,7 +279,10 @@ mod tests { assert!(runner.0.is_empty()); let deadline = Instant::now() + Duration::from_secs(2); while unsafe { libc::kill(pid as i32, 0) } == 0 { - assert!(Instant::now() < deadline, "removed service was not stopped/reaped"); + assert!( + Instant::now() < deadline, + "removed service was not stopped/reaped" + ); std::thread::sleep(Duration::from_millis(10)); } } diff --git a/compositor/src/display_presentation.rs b/compositor/src/display_presentation.rs index ffc7e4b..109e7a9 100644 --- a/compositor/src/display_presentation.rs +++ b/compositor/src/display_presentation.rs @@ -46,7 +46,9 @@ impl DisplayPresentation { #[cfg(test)] pub fn visible(&self, surface: &Id) -> bool { - self.outputs.values().any(|record| record.displayed.contains(surface)) + self.outputs + .values() + .any(|record| record.displayed.contains(surface)) } pub fn outputs_for<'a>(&'a self, surface: &'a Id) -> impl Iterator { @@ -143,7 +145,8 @@ mod tests { states.states.get_mut(&id).unwrap().visible_area = 0; assert!(visible(&states).is_empty()); states.states.get_mut(&id).unwrap().visible_area = 20; - states.states.get_mut(&id).unwrap().presentation_state = RenderElementPresentationState::Skipped; + states.states.get_mut(&id).unwrap().presentation_state = + RenderElementPresentationState::Skipped; assert!(visible(&states).is_empty()); } } diff --git a/compositor/src/dmabuf_feedback.rs b/compositor/src/dmabuf_feedback.rs index 501f333..c5849d7 100644 --- a/compositor/src/dmabuf_feedback.rs +++ b/compositor/src/dmabuf_feedback.rs @@ -22,7 +22,11 @@ impl FeedbackManager { self.globals.insert(global, render); } - pub fn new_surface(&mut self, surface: &WlSurface, global: DmabufGlobal) -> Option { + pub fn new_surface( + &mut self, + surface: &WlSurface, + global: DmabufGlobal, + ) -> Option { let fallback = self.globals.get(&global)?.clone(); self.surfaces.insert( surface.clone(), @@ -35,7 +39,9 @@ impl FeedbackManager { } pub fn requested(&self) -> impl Iterator { - self.surfaces.iter().map(|(surface, record)| (surface, record.global)) + self.surfaces + .iter() + .map(|(surface, record)| (surface, record.global)) } pub fn refresh(&mut self, preferred: &HashMap) { @@ -57,7 +63,11 @@ impl FeedbackManager { pub fn remove(&mut self, global: DmabufGlobal) { self.globals.remove(&global); - for (surface, record) in self.surfaces.iter().filter(|(_, record)| record.global == global) { + for (surface, record) in self + .surfaces + .iter() + .filter(|(_, record)| record.global == global) + { // V4 requires a main-device tranche. Retain last-known render // capabilities, withdraw scanout hints and retire the global. Bound // resources cannot import through the retired global or another GPU. diff --git a/compositor/src/dmabuf_feedback/tests.rs b/compositor/src/dmabuf_feedback/tests.rs index 422bde8..04c8d63 100644 --- a/compositor/src/dmabuf_feedback/tests.rs +++ b/compositor/src/dmabuf_feedback/tests.rs @@ -108,8 +108,13 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() { if *object != 7 || *opcode != 0 { return None; } - let bytes = args[2..].iter().flat_map(|word| word.to_ne_bytes()).collect::>(); - bytes.starts_with(b"zwp_linux_dmabuf_v1\0").then_some(args[0]) + let bytes = args[2..] + .iter() + .flat_map(|word| word.to_ne_bytes()) + .collect::>(); + bytes + .starts_with(b"zwp_linux_dmabuf_v1\0") + .then_some(args[0]) }) .expect("DMA-BUF v4 global"); let name = b"zwp_linux_dmabuf_v1\0"; @@ -131,7 +136,10 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() { assert_eq!(flags(&initial, 9), [0]); assert_eq!(done(&initial, 9), 1); assert_eq!(state.dmabuf_feedback.requested().count(), 1); - let main = initial.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 2).unwrap(); + let main = initial + .iter() + .find(|(id, opcode, _)| *id == 9 && *opcode == 2) + .unwrap(); assert_eq!(main.2[0] as usize, std::mem::size_of::()); assert_eq!(main.2[1], 10, "main device remains the renderer"); @@ -141,11 +149,20 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() { let changed = messages(&mut wire); assert_eq!(flags(&changed, 9), [TrancheFlags::Scanout.bits(), 0]); assert_eq!(done(&changed, 9), 1); - let target = changed.iter().find(|(id, opcode, _)| *id == 9 && *opcode == 4).unwrap(); - assert_eq!(target.2[1], 20, "scanout tranche targets the primary DRM device"); + let target = changed + .iter() + .find(|(id, opcode, _)| *id == 9 && *opcode == 4) + .unwrap(); + assert_eq!( + target.2[1], 20, + "scanout tranche targets the primary DRM device" + ); state.dmabuf_feedback.refresh(&selected); state.display_handle.flush_clients().unwrap(); - assert!(messages(&mut wire).is_empty(), "Smithay suppresses unchanged feedback"); + assert!( + messages(&mut wire).is_empty(), + "Smithay suppresses unchanged feedback" + ); request(&mut wire, 8, 3, &[10, 2], None); dispatch(&mut events, &mut state); @@ -167,7 +184,11 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() { request(&mut wire, 8, 2, &[12], None); // get_default_feedback dispatch(&mut events, &mut state); let renewed = messages(&mut wire); - assert_eq!(flags(&renewed, 11), [0], "renewal starts with safe render feedback"); + assert_eq!( + flags(&renewed, 11), + [0], + "renewal starts with safe render feedback" + ); assert_eq!(flags(&renewed, 12), [0]); state.dmabuf_feedback.refresh(&selected); state.display_handle.flush_clients().unwrap(); @@ -198,7 +219,12 @@ fn subscriptions_update_deduplicate_and_retire_device_preferences() { state.dmabuf_feedback.refresh(&selected); // A stale choice cannot resurrect a retired preference. state.display_handle.flush_clients().unwrap(); assert!(messages(&mut wire).is_empty()); - assert!(state.dmabuf_feedback.new_surface(&surface, global).is_none()); + assert!( + state + .dmabuf_feedback + .new_surface(&surface, global) + .is_none() + ); drop(wire); dispatch(&mut events, &mut state); state.dmabuf_feedback.refresh(&HashMap::new()); diff --git a/compositor/src/dmabuf_imports.rs b/compositor/src/dmabuf_imports.rs index 8db4bd7..03fe1cd 100644 --- a/compositor/src/dmabuf_imports.rs +++ b/compositor/src/dmabuf_imports.rs @@ -37,7 +37,12 @@ impl Default for Queue { impl Queue { fn enqueue(&mut self, owner: K, client: C, value: T, now: Instant) -> Result<(), T> { if self.pending.len() >= MAX_PENDING - || self.pending.iter().filter(|entry| entry.client == client).count() >= MAX_PER_CLIENT + || self + .pending + .iter() + .filter(|entry| entry.client == client) + .count() + >= MAX_PER_CLIENT { return Err(value); } @@ -89,7 +94,8 @@ pub(crate) struct ImportManager { impl Drop for ImportManager { fn drop(&mut self) { - self.queue.remove_if(|_| true, |entry| entry.value.notifier.failed()); + self.queue + .remove_if(|_| true, |entry| entry.value.notifier.failed()); } } @@ -100,10 +106,17 @@ impl ImportManager { pub(crate) fn remove(&mut self, global: DmabufGlobal) { self.globals.remove(&global); - self.queue.process(&global, |request| request.notifier.failed()); + self.queue + .process(&global, |request| request.notifier.failed()); } - pub(crate) fn enqueue(&mut self, global: DmabufGlobal, buffer: Dmabuf, notifier: ImportNotifier, now: Instant) { + pub(crate) fn enqueue( + &mut self, + global: DmabufGlobal, + buffer: Dmabuf, + notifier: ImportNotifier, + now: Instant, + ) { let Some(client) = notifier.client() else { notifier.failed(); return; @@ -114,16 +127,23 @@ impl ImportManager { return; } - if let Err(request) = self - .queue - .enqueue(global, client.id(), Request { buffer, notifier }, now) + if let Err(request) = + self.queue + .enqueue(global, client.id(), Request { buffer, notifier }, now) { request.notifier.failed(); } } - pub(crate) fn process(&mut self, global: DmabufGlobal, renderer: &mut R, node: Option) { - self.process_with(global, node, |buffer| renderer.import_dmabuf(buffer, None).is_ok()); + pub(crate) fn process( + &mut self, + global: DmabufGlobal, + renderer: &mut R, + node: Option, + ) { + self.process_with(global, node, |buffer| { + renderer.import_dmabuf(buffer, None).is_ok() + }); } pub(crate) fn process_with( @@ -171,16 +191,17 @@ impl Ferese { let mut backend = backend.borrow_mut(); // Nested Ferese publishes exactly one DMA-BUF global. if let Some(global) = self.dmabuf_imports.globals.iter().next().copied() { - self.dmabuf_imports.process(global, backend.renderer(), None); + self.dmabuf_imports + .process(global, backend.renderer(), None); } } } if let Some(deadline) = self.dmabuf_imports.queue.deadline() { if self.dmabuf_imports.timer.is_none() { - match self - .loop_handle - .insert_source(Timer::from_deadline(deadline), |_, _, state| { + match self.loop_handle.insert_source( + Timer::from_deadline(deadline), + |_, _, state| { state.dmabuf_imports.expire(Instant::now()); if let Some(deadline) = state.dmabuf_imports.queue.deadline() { TimeoutAction::ToInstant(deadline) @@ -188,7 +209,8 @@ impl Ferese { state.dmabuf_imports.timer = None; TimeoutAction::Drop } - }) { + }, + ) { Ok(token) => self.dmabuf_imports.timer = Some(token), Err(error) => { tracing::warn!(%error, "could not schedule DMA-BUF import deadline"); @@ -234,7 +256,9 @@ mod tests { let now = Instant::now(); let mut queue = Queue::default(); for index in 0..MAX_PENDING { - queue.enqueue(1, index / MAX_PER_CLIENT, index, now).unwrap(); + queue + .enqueue(1, index / MAX_PER_CLIENT, index, now) + .unwrap(); } assert_eq!(queue.enqueue(1, 99, 100, now), Err(100)); @@ -275,13 +299,18 @@ mod tests { let fd_removed = removed.as_raw_fd(); let fd_expired = expired.as_raw_fd(); queue.enqueue(1, 1, removed, now).unwrap(); - queue.enqueue(2, 1, expired, now + Duration::from_secs(1)).unwrap(); + queue + .enqueue(2, 1, expired, now + Duration::from_secs(1)) + .unwrap(); let mut failures = 0; queue.process(&1, |_| failures += 1); // SAFETY: F_GETFD observes descriptor validity without modifying it. assert_eq!(unsafe { libc::fcntl(fd_removed, libc::F_GETFD) }, -1); assert_ne!(unsafe { libc::fcntl(fd_expired, libc::F_GETFD) }, -1); - queue.remove_if(|entry| entry.deadline <= now + MAX_WAIT, |_| panic!("expired early")); + queue.remove_if( + |entry| entry.deadline <= now + MAX_WAIT, + |_| panic!("expired early"), + ); queue.remove_if( |entry| entry.deadline <= now + MAX_WAIT + Duration::from_secs(1), |_| failures += 1, diff --git a/compositor/src/effects.rs b/compositor/src/effects.rs index 7d52724..06a9c01 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -6,7 +6,9 @@ use ferese_protocols::effects::v1::server::ferese_surface_effects_v1::FereseSurf use ferese_protocols::effects::v1::server::{ferese_effects_manager_v1, ferese_surface_effects_v1}; use ferese_protocols::material::v1::server::ferese_material_manager_v1::FereseMaterialManagerV1; use ferese_protocols::material::v1::server::ferese_surface_material_v1::FereseSurfaceMaterialV1; -use ferese_protocols::material::v1::server::{ferese_material_manager_v1, ferese_surface_material_v1}; +use ferese_protocols::material::v1::server::{ + ferese_material_manager_v1, ferese_surface_material_v1, +}; use smithay::reexports::wayland_server::backend::ClientId; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{ @@ -37,10 +39,16 @@ pub(crate) struct ResolvedMaterial { pub shadow: [f64; 3], } -pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity: f32) -> ResolvedMaterial { +pub(crate) fn resolve_material( + role: SemanticRole, + style: MaterialStyle, + opacity: f32, +) -> ResolvedMaterial { let shadow = match role { SemanticRole::Panel => [1.0, 5.0, 0.04], - SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => [2.0, 8.0, 0.07], + SemanticRole::PanelElevated | SemanticRole::Popover | SemanticRole::Menu => { + [2.0, 8.0, 0.07] + } SemanticRole::Hud => [1.0, 4.0, 0.04], SemanticRole::Notification | SemanticRole::Modal => [3.0, 10.0, 0.09], }; @@ -48,7 +56,11 @@ pub(crate) fn resolve_material(role: SemanticRole, style: MaterialStyle, opacity MaterialStyle::Solid => 1.0, MaterialStyle::Translucent => opacity.clamp(0.0, 1.0), }; - ResolvedMaterial { style, opacity, shadow } + ResolvedMaterial { + style, + opacity, + shadow, + } } #[derive(Clone, Debug, PartialEq)] @@ -167,7 +179,9 @@ impl Dispatch for Ferese { match request { ferese_material_manager_v1::Request::GetSurfaceMaterial { id, surface } => { let attached = with_states(&surface, |states| { - states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new); + states + .data_map + .insert_if_missing_threadsafe(SurfaceEffectsState::new); let effects = states.data_map.get::().unwrap(); effects.attached.swap(true, Ordering::AcqRel) }); @@ -288,7 +302,9 @@ pub(crate) fn surface_region_opacities(surface: &WlSurface) -> Vec { } fn decode_region_opacities(bytes: &[u8]) -> Option> { - if !bytes.len().is_multiple_of(4) || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 4 { + if !bytes.len().is_multiple_of(4) + || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 4 + { return None; } bytes @@ -303,7 +319,9 @@ fn decode_region_opacities(bytes: &[u8]) -> Option> { } fn decode_regions(bytes: &[u8]) -> Option> { - if !bytes.len().is_multiple_of(20) || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 20 { + if !bytes.len().is_multiple_of(20) + || bytes.len() > ferese_protocols::effects::v1::MAX_REGIONS * 20 + { return None; } bytes @@ -311,7 +329,8 @@ fn decode_regions(bytes: &[u8]) -> Option> { .0 .iter() .map(|tuple| { - let values: [f64; 5] = std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i]))); + let values: [f64; 5] = + std::array::from_fn(|i| f64::from(f32::from_ne_bytes(tuple.as_chunks::<4>().0[i]))); (values[2] > 0.0 && values[3] > 0.0 && values[4] >= 0.0 @@ -363,7 +382,9 @@ impl Dispatch for Ferese { match request { ferese_effects_manager_v1::Request::GetSurfaceEffects { id, surface } => { let already_attached = with_states(&surface, |states| { - states.data_map.insert_if_missing_threadsafe(SurfaceEffectsState::new); + states + .data_map + .insert_if_missing_threadsafe(SurfaceEffectsState::new); states .data_map .get::() @@ -416,8 +437,10 @@ impl Dispatch for Ferese { fusion, opacity, } => { - let (Some(regions), Some(panel)) = (decode_regions(®ions), decode_panel_shapes(&shapes, fusion)) - else { + let (Some(regions), Some(panel)) = ( + decode_regions(®ions), + decode_panel_shapes(&shapes, fusion), + ) else { return; }; if panel.shapes.is_empty() != regions.is_empty() { @@ -600,17 +623,22 @@ pub(crate) fn surface_opacity(surface: &WlSurface) -> f32 { states .data_map .get::() - .map_or(1.0, |effects| *effects.opacity.lock().unwrap() as f32 / 1000.0) + .map_or(1.0, |effects| { + *effects.opacity.lock().unwrap() as f32 / 1000.0 + }) }) } pub(crate) fn begin_surface_dismiss(surface: &WlSurface) -> bool { with_states(surface, |states| { - states.data_map.get::().is_some_and(|effects| { - effects.presentation_supported.load(Ordering::Acquire) - && effects.attached.load(Ordering::Acquire) - && !effects.dismissing.swap(true, Ordering::AcqRel) - }) + states + .data_map + .get::() + .is_some_and(|effects| { + effects.presentation_supported.load(Ordering::Acquire) + && effects.attached.load(Ordering::Acquire) + && !effects.dismissing.swap(true, Ordering::AcqRel) + }) }) } @@ -733,7 +761,10 @@ mod tests { #[test] fn region_opacities_validate_independent_fades() { - let bytes: Vec<_> = [0u32, 500, 1000].into_iter().flat_map(u32::to_ne_bytes).collect(); + let bytes: Vec<_> = [0u32, 500, 1000] + .into_iter() + .flat_map(u32::to_ne_bytes) + .collect(); assert_eq!(decode_region_opacities(&bytes), Some(vec![0, 500, 1000])); assert_eq!(decode_region_opacities(&[]), Some(vec![])); assert!(decode_region_opacities(&bytes[..11]).is_none()); diff --git a/compositor/src/floating.rs b/compositor/src/floating.rs index 0125d90..0ebe343 100644 --- a/compositor/src/floating.rs +++ b/compositor/src/floating.rs @@ -19,8 +19,12 @@ pub fn intersection(a: Rect, b: Rect) -> Option { pub fn clamp_to_work(mut rect: Rect, work: Rect) -> Rect { // Preserve client minimum sizes even when the work area is smaller. - rect.x = rect.x.clamp(work.x, work.x + (work.width - rect.width).max(0.0)); - rect.y = rect.y.clamp(work.y, work.y + (work.height - rect.height).max(0.0)); + rect.x = rect + .x + .clamp(work.x, work.x + (work.width - rect.width).max(0.0)); + rect.y = rect + .y + .clamp(work.y, work.y + (work.height - rect.height).max(0.0)); rect } @@ -35,14 +39,27 @@ pub fn centered(size: (f64, f64), anchor: Rect) -> Rect { pub fn anchored_size(rect: Rect, size: (f64, f64), left: bool, top: bool) -> Rect { Rect::new( - if left { rect.x + rect.width - size.0 } else { rect.x }, - if top { rect.y + rect.height - size.1 } else { rect.y }, + if left { + rect.x + rect.width - size.0 + } else { + rect.x + }, + if top { + rect.y + rect.height - size.1 + } else { + rect.y + }, size.0, size.1, ) } -pub fn min_overlap(size: (f64, f64), work: Rect, others: &[Rect], anchor: (f64, f64)) -> Option { +pub fn min_overlap( + size: (f64, f64), + work: Rect, + others: &[Rect], + anchor: (f64, f64), +) -> Option { let (width, height) = size; if !width.is_finite() || !height.is_finite() @@ -77,12 +94,15 @@ pub fn min_overlap(size: (f64, f64), work: Rect, others: &[Rect], anchor: (f64, .filter_map(|other| intersection(rect, *other)) .map(|r| r.width * r.height) .sum::(); - let distance = (x + width / 2.0 - anchor.0).powi(2) + (y + height / 2.0 - anchor.1).powi(2); + let distance = + (x + width / 2.0 - anchor.0).powi(2) + (y + height / 2.0 - anchor.1).powi(2); let cost = (overlap, distance); - if best - .as_ref() - .is_none_or(|(old, _)| cost.0.total_cmp(&old.0).then(cost.1.total_cmp(&old.1)).is_lt()) - { + if best.as_ref().is_none_or(|(old, _)| { + cost.0 + .total_cmp(&old.0) + .then(cost.1.total_cmp(&old.1)) + .is_lt() + }) { best = Some((cost, rect)); } } @@ -97,7 +117,10 @@ pub struct Cascade { impl Cascade { pub fn place(&mut self, output: &str, work: Rect, size: (f64, f64)) -> Rect { - let position = self.positions.entry(output.to_owned()).or_insert((work.x, work.y)); + let position = self + .positions + .entry(output.to_owned()) + .or_insert((work.x, work.y)); if position.0 < work.x || position.1 < work.y || position.0 + size.0 > work.x + work.width @@ -132,7 +155,10 @@ impl Remembered { pub fn restore(&self, outputs: &[(String, Rect)]) -> Option { let work = outputs.iter().find(|(name, _)| *name == self.output)?.1; - if !self.fractions.iter().all(|v| v.is_finite()) || self.fractions[2] <= 0.0 || self.fractions[3] <= 0.0 { + if !self.fractions.iter().all(|v| v.is_finite()) + || self.fractions[2] <= 0.0 + || self.fractions[3] <= 0.0 + { return None; } let [x, y, w, h] = self.fractions; @@ -142,11 +168,15 @@ impl Remembered { w * work.width, h * work.height, ); - if ![rect.x, rect.y, rect.width, rect.height].iter().all(|v| v.is_finite()) { + if ![rect.x, rect.y, rect.width, rect.height] + .iter() + .all(|v| v.is_finite()) + { return None; } let visible = outputs.iter().any(|(_, area)| { - intersection(rect, *area).is_some_and(|r| r.width * r.height >= rect.width * rect.height * 0.25) + intersection(rect, *area) + .is_some_and(|r| r.width * r.height >= rect.width * rect.height * 0.25) }); visible.then_some(rect) } @@ -169,8 +199,13 @@ struct SaveThread { } impl SaveThread { - fn start(mut persist: impl FnMut(&Path, &Memory) -> std::io::Result<()> + Send + 'static) -> std::io::Result { - let shared = std::sync::Arc::new((std::sync::Mutex::new(SaveState::default()), std::sync::Condvar::new())); + fn start( + mut persist: impl FnMut(&Path, &Memory) -> std::io::Result<()> + Send + 'static, + ) -> std::io::Result { + let shared = std::sync::Arc::new(( + std::sync::Mutex::new(SaveState::default()), + std::sync::Condvar::new(), + )); let worker = shared.clone(); let thread = std::thread::Builder::new() .name("floating-geometry-save".into()) @@ -355,7 +390,9 @@ pub fn snap_lines(raw: Rect, work: Rect, others: &[(u64, Rect)]) -> (Vec= rect.y { + if raw.y - SNAP_DISTANCE <= rect.y + rect.height + && raw.y + raw.height + SNAP_DISTANCE >= rect.y + { xs.extend([ SnapLine { target, @@ -367,7 +404,9 @@ pub fn snap_lines(raw: Rect, work: Rect, others: &[(u64, Rect)]) -> (Vec= rect.x { + if raw.x - SNAP_DISTANCE <= rect.x + rect.width + && raw.x + raw.width + SNAP_DISTANCE >= rect.x + { ys.extend([ SnapLine { target, @@ -420,7 +459,9 @@ mod tests { memory }; worker.queue(PathBuf::from("unused"), snapshot("first")); - started_rx.recv_timeout(std::time::Duration::from_secs(2)).unwrap(); + started_rx + .recv_timeout(std::time::Duration::from_secs(2)) + .unwrap(); worker.queue(PathBuf::from("unused"), snapshot("superseded")); worker.queue(PathBuf::from("unused"), snapshot("last")); assert_eq!(*written.lock().unwrap(), vec!["first"]); @@ -511,7 +552,10 @@ mod tests { }; let placed = min_overlap(size, Rect::new(0., 0., 16., 16.), &others, (8., 8.)).unwrap(); let brute = (0..=16 - size.0 as i32) - .flat_map(|x| (0..=16 - size.1 as i32).map(move |y| Rect::new(x as f64, y as f64, size.0, size.1))) + .flat_map(|x| { + (0..=16 - size.1 as i32) + .map(move |y| Rect::new(x as f64, y as f64, size.0, size.1)) + }) .map(overlap) .min_by(f64::total_cmp) .unwrap(); diff --git a/compositor/src/focus_effect.rs b/compositor/src/focus_effect.rs index 4afbbaf..1bd1c95 100644 --- a/compositor/src/focus_effect.rs +++ b/compositor/src/focus_effect.rs @@ -3,7 +3,11 @@ use std::time::Duration; use crate::config::FocusEffectSettings; use ferese_layout::WindowId; -pub(crate) fn target(settings: FocusEffectSettings, active: Option, window: WindowId) -> f64 { +pub(crate) fn target( + settings: FocusEffectSettings, + active: Option, + window: WindowId, +) -> f64 { if !settings.has_effects() || active == Some(window) { 1.0 } else { @@ -16,12 +20,18 @@ impl FocusEffectSettings { self.enabled && (self.active_opacity != self.inactive_opacity || self.inactive_dim != 0.0) } - pub(crate) fn resolve(self, progress: f64, has_active: bool, overview_opacity: f32) -> (f32, f64) { + pub(crate) fn resolve( + self, + progress: f64, + has_active: bool, + overview_opacity: f32, + ) -> (f32, f64) { if !self.enabled { return (1.0, 0.0); } let progress = progress.clamp(0.0, 1.0); - let alpha = self.inactive_opacity + (self.active_opacity - self.inactive_opacity) * progress; + let alpha = + self.inactive_opacity + (self.active_opacity - self.inactive_opacity) * progress; let dim = if has_active { self.inactive_dim * (1.0 - progress) * (1.0 - f64::from(overview_opacity)) } else { @@ -87,7 +97,11 @@ impl BoundedFade { pub fn advance_visual(&mut self, target: f64, delta: Duration, duration_ms: f64) -> bool { self.advance( target, - if self.target != target { Duration::ZERO } else { delta }, + if self.target != target { + Duration::ZERO + } else { + delta + }, duration_ms, ) } diff --git a/compositor/src/frame_scheduler.rs b/compositor/src/frame_scheduler.rs index 3c71389..cdff45c 100644 --- a/compositor/src/frame_scheduler.rs +++ b/compositor/src/frame_scheduler.rs @@ -68,7 +68,8 @@ impl FrameScheduler { // A cold GPU is less predictable. Start immediately on idle wakeup. self.interval } else { - (self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin).min(self.interval) + (self.render_estimate + self.render_variance * 2 + TIMER_MARGIN + self.extra_margin) + .min(self.interval) } } @@ -95,7 +96,10 @@ impl FrameScheduler { } let requested_at = self.requested_at?; - if self.last_presented.is_some_and(|last| last > now + self.interval) { + if self + .last_presented + .is_some_and(|last| last > now + self.interval) + { self.last_presented = None; self.last_target = None; } @@ -148,8 +152,12 @@ impl FrameScheduler { let ratio = (elapsed.as_secs_f64() / 0.5).clamp(0.01, 1.0); let deviation = sample.saturating_sub(self.render_estimate); let variance_ratio = (elapsed.as_secs_f64() / 6.0).clamp(0.001, 0.1); - self.render_variance = self.render_variance.mul_f64(1.0 - variance_ratio).max(deviation); - self.render_estimate = self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio); + self.render_variance = self + .render_variance + .mul_f64(1.0 - variance_ratio) + .max(deviation); + self.render_estimate = + self.render_estimate.mul_f64(1.0 - ratio) + sample.mul_f64(ratio); } } @@ -168,7 +176,8 @@ impl FrameScheduler { self.last_target = Some(timestamp); let late = timestamp.saturating_sub(plan.present_at); if plan.phase_known && late > self.interval / 2 { - let missed = (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos(); + let missed = + (late.as_nanos() + self.interval.as_nanos() / 2) / self.interval.as_nanos(); self.missed_deadlines = self.missed_deadlines.saturating_add(missed as u64); // CPU submission timings may omit asynchronous GPU work. Increase // the margin promptly after a real miss; relax it slowly afterward. @@ -226,7 +235,8 @@ impl FrameScheduler { // the next refresh look too early and halve a client's frame rate. self.last_callback = Some(self.last_presented.map_or(now, |last| { let tolerance = Duration::from_micros(500).min(self.interval / 8); - let slots = (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos(); + let slots = + (now.saturating_sub(last) + tolerance).as_nanos() / self.interval.as_nanos(); last + self.interval.mul_f64(slots as f64) })); } @@ -308,7 +318,10 @@ mod tests { scheduler.callback_sent(ms(110)); scheduler.request(ms(108)); assert_eq!(scheduler.plan(ms(108)).unwrap().present_at, ms(120)); - assert_eq!(scheduler.callback_deadline(ms(111), ms(110)).unwrap(), ms(120)); + assert_eq!( + scheduler.callback_deadline(ms(111), ms(110)).unwrap(), + ms(120) + ); } #[test] @@ -319,7 +332,9 @@ mod tests { let unchanged = scheduler.plan(ms(101)).unwrap(); scheduler.begin(); scheduler.rendered(unchanged, ms(108), ms(1), false); - let callback_at = scheduler.callback_deadline(ms(108), unchanged.present_at).unwrap(); + let callback_at = scheduler + .callback_deadline(ms(108), unchanged.present_at) + .unwrap(); assert_eq!(callback_at, ms(110)); scheduler.request(ms(109)); @@ -330,13 +345,19 @@ mod tests { // The old timer is dispatched late, after a new frame was queued. // Neither that timer nor the predicted refresh can release callbacks. assert_eq!(scheduler.callback_deadline(ms(118), callback_at), None); - assert_eq!(scheduler.callback_deadline(ms(120), changed.present_at), None); + assert_eq!( + scheduler.callback_deadline(ms(120), changed.present_at), + None + ); assert_eq!(scheduler.callback_deadline(ms(129), callback_at), None); scheduler.presented(ms(130)); assert_eq!(scheduler.callback_deadline(ms(130), ms(130)), Some(ms(130))); scheduler.callback_sent(ms(130)); - assert_eq!(scheduler.callback_deadline(ms(130), callback_at), Some(ms(140))); + assert_eq!( + scheduler.callback_deadline(ms(130), callback_at), + Some(ms(140)) + ); } #[test] diff --git a/compositor/src/gamma.rs b/compositor/src/gamma.rs index ef1367d..5545a9a 100644 --- a/compositor/src/gamma.rs +++ b/compositor/src/gamma.rs @@ -30,7 +30,11 @@ impl Ramp { pub(crate) fn at_temperature(&self, temperature: u16) -> Self { let mut ramp = self.clone(); - for (channel, gain) in ramp.0.chunks_exact_mut(self.size()).zip(temperature_gains(temperature)) { + for (channel, gain) in ramp + .0 + .chunks_exact_mut(self.size()) + .zip(temperature_gains(temperature)) + { for value in channel { *value = (f64::from(*value) * gain).round() as u16; } @@ -45,14 +49,18 @@ impl Ramp { self.0 .iter() .zip(&target.0) - .map(|(&from, &to)| (f64::from(from) + (f64::from(to) - f64::from(from)) * progress).round() as u16) + .map(|(&from, &to)| { + (f64::from(from) + (f64::from(to) - f64::from(from)) * progress).round() as u16 + }) .collect(), ) } pub(crate) fn read(file: File, size: usize) -> io::Result { let metadata = file.metadata()?; - let length = size.checked_mul(6).filter(|_| size > 0 && size <= MAX_GAMMA_SIZE); + let length = size + .checked_mul(6) + .filter(|_| size > 0 && size <= MAX_GAMMA_SIZE); let Some(length) = length else { return Err(io::ErrorKind::InvalidData.into()); }; @@ -73,7 +81,11 @@ impl Ramp { pub(crate) fn channels(&self) -> (&[u16], &[u16], &[u16]) { let size = self.0.len() / 3; - (&self.0[..size], &self.0[size..2 * size], &self.0[2 * size..]) + ( + &self.0[..size], + &self.0[size..2 * size], + &self.0[2 * size..], + ) } } @@ -89,5 +101,9 @@ fn temperature_gains(kelvin: u16) -> [f64; 3] { } }; let temperature = f64::from(kelvin.clamp(1000, 6500)) / 100.; - [1., green(temperature) / green(65.), blue(temperature) / blue(65.)] + [ + 1., + green(temperature) / green(65.), + blue(temperature) / blue(65.), + ] } diff --git a/compositor/src/gestures.rs b/compositor/src/gestures.rs index 2391e97..fd1b1f4 100644 --- a/compositor/src/gestures.rs +++ b/compositor/src/gestures.rs @@ -49,7 +49,11 @@ impl Swipe { } } - pub fn finish_at(&mut self, cancelled: bool, time: std::time::Duration) -> Option { + pub fn finish_at( + &mut self, + cancelled: bool, + time: std::time::Duration, + ) -> Option { let Some(direction) = self.direction else { return self.finish(cancelled); }; @@ -76,11 +80,14 @@ impl Swipe { }; // The preview is bounded. Its visible velocity is zero when pushing // farther beyond either endpoint, even if the fingers keep moving. - if (progress == 0.0 && self.release_velocity < 0.0) || (progress == 1.0 && self.release_velocity > 0.0) { + if (progress == 0.0 && self.release_velocity < 0.0) + || (progress == 1.0 && self.release_velocity > 0.0) + { self.release_velocity = 0.0; } self.delta = None; - (!cancelled && ferese_animation::gesture::project(progress, self.release_velocity, 0.997) >= 0.5) + (!cancelled + && ferese_animation::gesture::project(progress, self.release_velocity, 0.997) >= 0.5) .then_some(direction) } @@ -139,7 +146,8 @@ impl Swipe { if cancelled { return None; } - dominant_direction(x, y, self.threshold).filter(|direction| Some(*direction) == self.direction) + dominant_direction(x, y, self.threshold) + .filter(|direction| Some(*direction) == self.direction) } } diff --git a/compositor/src/grabs/move_grab.rs b/compositor/src/grabs/move_grab.rs index a8e25fb..982bb9f 100644 --- a/compositor/src/grabs/move_grab.rs +++ b/compositor/src/grabs/move_grab.rs @@ -1,8 +1,9 @@ use smithay::desktop::Window; use smithay::input::pointer::{ - AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, - GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, - MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, }; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::utils::{Logical, Point, Size}; @@ -61,7 +62,12 @@ impl PointerGrab for MoveSurfaceGrab { handle.relative_motion(data, focus, event); } - fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) { + fn button( + &mut self, + data: &mut Ferese, + handle: &mut PointerInnerHandle<'_, Ferese>, + event: &ButtonEvent, + ) { handle.button(data, event); if handle.current_pressed().is_empty() { self.finished = true; @@ -70,7 +76,12 @@ impl PointerGrab for MoveSurfaceGrab { } } - fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) { + fn axis( + &mut self, + data: &mut Ferese, + handle: &mut PointerInnerHandle<'_, Ferese>, + frame: AxisFrame, + ) { handle.axis(data, frame); } diff --git a/compositor/src/grabs/resize_grab.rs b/compositor/src/grabs/resize_grab.rs index df80094..9623034 100644 --- a/compositor/src/grabs/resize_grab.rs +++ b/compositor/src/grabs/resize_grab.rs @@ -1,8 +1,9 @@ use smithay::desktop::Window; use smithay::input::pointer::{ - AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, GesturePinchEndEvent, - GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, - MotionEvent, PointerGrab, PointerInnerHandle, RelativeMotionEvent, + AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, + GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, PointerInnerHandle, + RelativeMotionEvent, }; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; @@ -48,7 +49,9 @@ impl ResizeEdge { fn left(self) -> bool { matches!( self.0, - xdg_toplevel::ResizeEdge::Left | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::BottomLeft + xdg_toplevel::ResizeEdge::Left + | xdg_toplevel::ResizeEdge::TopLeft + | xdg_toplevel::ResizeEdge::BottomLeft ) } @@ -64,7 +67,9 @@ impl ResizeEdge { fn top(self) -> bool { matches!( self.0, - xdg_toplevel::ResizeEdge::Top | xdg_toplevel::ResizeEdge::TopLeft | xdg_toplevel::ResizeEdge::TopRight + xdg_toplevel::ResizeEdge::Top + | xdg_toplevel::ResizeEdge::TopLeft + | xdg_toplevel::ResizeEdge::TopRight ) } @@ -122,8 +127,12 @@ impl PointerGrab for ResizeSurfaceGrab { let initial = self.initial_rect; let raw_left = initial.loc.x as f64 + if self.edges.left() { delta.x } else { 0. }; let raw_top = initial.loc.y as f64 + if self.edges.top() { delta.y } else { 0. }; - let raw_right = initial.loc.x as f64 + initial.size.w as f64 + if self.edges.right() { delta.x } else { 0. }; - let raw_bottom = initial.loc.y as f64 + initial.size.h as f64 + if self.edges.bottom() { delta.y } else { 0. }; + let raw_right = initial.loc.x as f64 + + initial.size.w as f64 + + if self.edges.right() { delta.x } else { 0. }; + let raw_bottom = initial.loc.y as f64 + + initial.size.h as f64 + + if self.edges.bottom() { delta.y } else { 0. }; let raw = ferese_layout::Rect::new( raw_left, raw_top, @@ -136,22 +145,36 @@ impl PointerGrab for ResizeSurfaceGrab { self.snap_y.clear(); } else { if self.edges.left() || self.edges.right() { - let moving = if self.edges.left() { raw_left } else { raw_right }; + let moving = if self.edges.left() { + raw_left + } else { + raw_right + }; delta.x += self.snap_x.apply(moving, 0., &xs) - moving; } if self.edges.top() || self.edges.bottom() { - let moving = if self.edges.top() { raw_top } else { raw_bottom }; + let moving = if self.edges.top() { + raw_top + } else { + raw_bottom + }; delta.y += self.snap_y.apply(moving, 0., &ys) - moving; } } - let surface = self.window.toplevel().expect("managed window has a toplevel"); + let surface = self + .window + .toplevel() + .expect("managed window has a toplevel"); let (minimum, maximum) = with_states(surface.wl_surface(), |states| { let mut cached = states.cached_state.get::(); let state = cached.current(); (state.min_size, state.max_size) }); - let (minimum, maximum) = - data.effective_size_constraints(&self.window, (minimum.w, minimum.h), (maximum.w, maximum.h)); + let (minimum, maximum) = data.effective_size_constraints( + &self.window, + (minimum.w, minimum.h), + (maximum.w, maximum.h), + ); self.last_size = constrained_size( self.initial_rect.size, delta, @@ -193,13 +216,21 @@ impl PointerGrab for ResizeSurfaceGrab { handle.relative_motion(data, focus, event); } - fn button(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, event: &ButtonEvent) { + fn button( + &mut self, + data: &mut Ferese, + handle: &mut PointerInnerHandle<'_, Ferese>, + event: &ButtonEvent, + ) { handle.button(data, event); if handle.current_pressed().is_empty() { self.finished = true; data.remember_floating(&self.window); handle.unset_grab(self, data, event.serial, event.time, true); - let surface = self.window.toplevel().expect("managed window has a toplevel"); + let surface = self + .window + .toplevel() + .expect("managed window has a toplevel"); surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Resizing); state.size = Some(self.last_size); @@ -208,7 +239,12 @@ impl PointerGrab for ResizeSurfaceGrab { } } - fn axis(&mut self, data: &mut Ferese, handle: &mut PointerInnerHandle<'_, Ferese>, frame: AxisFrame) { + fn axis( + &mut self, + data: &mut Ferese, + handle: &mut PointerInnerHandle<'_, Ferese>, + frame: AxisFrame, + ) { handle.axis(data, frame); } @@ -297,13 +333,20 @@ impl PointerGrab for ResizeSurfaceGrab { return; } - let surface = self.window.toplevel().expect("managed window has a toplevel"); + let surface = self + .window + .toplevel() + .expect("managed window has a toplevel"); surface.with_pending_state(|state| { state.states.unset(xdg_toplevel::State::Resizing); state.size = Some(self.initial_rect.size); }); surface.send_pending_configure(); - data.set_floating_window_geometry(&self.window, self.initial_rect.loc, self.initial_rect.size); + data.set_floating_window_geometry( + &self.window, + self.initial_rect.loc, + self.initial_rect.size, + ); } } @@ -357,11 +400,15 @@ fn resized_rect( let mut location = initial.loc; if edges.left() { - location.x = location.x.saturating_add(initial.size.w.saturating_sub(size.w)); + location.x = location + .x + .saturating_add(initial.size.w.saturating_sub(size.w)); } if edges.top() { - location.y = location.y.saturating_add(initial.size.h.saturating_sub(size.h)); + location.y = location + .y + .saturating_add(initial.size.h.saturating_sub(size.h)); } Rectangle::new(location, size) diff --git a/compositor/src/handlers/activation.rs b/compositor/src/handlers/activation.rs index a1fe670..b622c12 100644 --- a/compositor/src/handlers/activation.rs +++ b/compositor/src/handlers/activation.rs @@ -40,7 +40,9 @@ impl InputHistory { fn validate(&self, serial: Serial, client: &C, now: Instant) -> Option { self.events.iter().rev().find_map(|(event, owner, time)| { - (*event == serial && owner == client && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE) + (*event == serial + && owner == client + && now.saturating_duration_since(*time) <= TOKEN_MAX_AGE) .then_some(*time) }) } @@ -55,7 +57,8 @@ impl Ferese { } if let Some(client) = surface.and_then(|surface| surface.client()) { - self.activation_inputs.record(serial, client.id(), Instant::now()); + self.activation_inputs + .record(serial, client.id(), Instant::now()); } } } @@ -86,14 +89,23 @@ impl XdgActivationHandler for Ferese { return false; } - let Some(time) = self.activation_inputs.validate(*serial, client, Instant::now()) else { + let Some(time) = self + .activation_inputs + .validate(*serial, client, Instant::now()) + else { return false; }; - data.user_data.insert_if_missing(|| ApprovedInteraction(time)); + data.user_data + .insert_if_missing(|| ApprovedInteraction(time)); true } - fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) { + fn request_activation( + &mut self, + token: XdgActivationToken, + data: XdgActivationTokenData, + surface: WlSurface, + ) { self.xdg_activation_state.remove_token(&token); if self.session_lock.active() || self.input_capture.captures(1) { return; @@ -152,7 +164,9 @@ mod tests { wire.write_all(&bytes).unwrap(); }; send(&mut wire, 1, 1, &[2]); // wl_display.get_registry - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); state.display_handle.flush_clients().unwrap(); wire.set_nonblocking(true).unwrap(); let mut globals = Vec::new(); @@ -162,10 +176,16 @@ mod tests { let mut seat_name = None; while offset < globals.len() { let header = u32::from_ne_bytes(globals[offset + 4..offset + 8].try_into().unwrap()); - if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 && header & 0xffff == 0 { - let length = u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) as usize; + if u32::from_ne_bytes(globals[offset..offset + 4].try_into().unwrap()) == 2 + && header & 0xffff == 0 + { + let length = + u32::from_ne_bytes(globals[offset + 12..offset + 16].try_into().unwrap()) + as usize; if &globals[offset + 16..offset + 16 + length] == b"wl_seat\0" { - seat_name = Some(u32::from_ne_bytes(globals[offset + 8..offset + 12].try_into().unwrap())); + seat_name = Some(u32::from_ne_bytes( + globals[offset + 8..offset + 12].try_into().unwrap(), + )); } } @@ -180,7 +200,9 @@ mod tests { ); args.extend([7, 3]); send(&mut wire, 2, 0, &args); - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); let seat: WlSeat = state .seat .client_seats(&launcher) @@ -208,7 +230,9 @@ mod tests { !state.token_created(token.clone(), data()), "fresh token renewed stale input" ); - state.activation_inputs.record(serial, launcher.id(), Instant::now()); + state + .activation_inputs + .record(serial, launcher.id(), Instant::now()); let approved = data(); assert!(state.token_created(token.clone(), approved.clone())); assert!(approved.user_data.get::().is_some()); @@ -227,7 +251,15 @@ mod tests { // The proof is attached to the token, not the recipient identity. This // is what lets a validated launcher hand it to a different application. assert_ne!(approved.client_id, Some(recipient.id())); - assert!(approved.user_data.get::().unwrap().0.elapsed() < TOKEN_MAX_AGE); + assert!( + approved + .user_data + .get::() + .unwrap() + .0 + .elapsed() + < TOKEN_MAX_AGE + ); // A valid handoff must not focus a recipient before its first buffer. use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase; @@ -241,12 +273,18 @@ mod tests { ) .unwrap(); let shell = recipient - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); send(&mut recipient_wire, compositor.id().protocol_id(), 0, &[2]); send(&mut recipient_wire, shell.id().protocol_id(), 2, &[3, 2]); send(&mut recipient_wire, 3, 1, &[4]); - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); let target = state .space .elements() @@ -272,7 +310,11 @@ mod tests { assert_eq!(history.validate(Serial::from(13), &"launcher", now), None); assert_eq!(history.validate(serial, &"other-client", now), None); assert_eq!( - history.validate(serial, &"launcher", now + TOKEN_MAX_AGE + Duration::from_millis(1)), + history.validate( + serial, + &"launcher", + now + TOKEN_MAX_AGE + Duration::from_millis(1) + ), None ); // An approved token retains the interaction time across client handoff. diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index e35156e..6c98838 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -5,7 +5,8 @@ use smithay::reexports::wayland_server::protocol::wl_buffer; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::wayland::buffer::BufferHandler; use smithay::wayland::compositor::{ - CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, is_sync_subsurface, + CompositorClientState, CompositorHandler, CompositorState, get_parent, get_role, + is_sync_subsurface, }; use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; use smithay::wayland::shm::{ShmHandler, ShmState}; diff --git a/compositor/src/handlers/dispatch.rs b/compositor/src/handlers/dispatch.rs index bb4735c..4eea0c1 100644 --- a/compositor/src/handlers/dispatch.rs +++ b/compositor/src/handlers/dispatch.rs @@ -1,6 +1,7 @@ use crate::Ferese; use smithay::reexports::wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, protocol as wl, + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, backend::ClientId, + protocol as wl, }; use smithay::reexports::{ wayland_protocols as protocols, wayland_protocols_misc as misc, wayland_protocols_wlr as wlr, diff --git a/compositor/src/handlers/gamma_control.rs b/compositor/src/handlers/gamma_control.rs index 5d1be4f..41390fa 100644 --- a/compositor/src/handlers/gamma_control.rs +++ b/compositor/src/handlers/gamma_control.rs @@ -5,7 +5,9 @@ use smithay::reexports::wayland_protocols_wlr::gamma_control::v1::server::{ zwlr_gamma_control_manager_v1::{self, ZwlrGammaControlManagerV1}, zwlr_gamma_control_v1::{self, ZwlrGammaControlV1}, }; -use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; use crate::{Ferese, backends::direct::gamma, gamma::Ramp}; diff --git a/compositor/src/handlers/input_method.rs b/compositor/src/handlers/input_method.rs index c324245..66eb86a 100644 --- a/compositor/src/handlers/input_method.rs +++ b/compositor/src/handlers/input_method.rs @@ -7,7 +7,10 @@ use crate::Ferese; impl InputMethodHandler for Ferese { fn new_popup(&mut self, surface: PopupSurface) { - if let Err(error) = self.popups.track_popup(PopupKind::InputMethod(surface.clone())) { + if let Err(error) = self + .popups + .track_popup(PopupKind::InputMethod(surface.clone())) + { tracing::warn!(?error, "failed to track input method popup"); } crate::backends::direct::render_surface(self, surface.wl_surface()); @@ -34,7 +37,10 @@ impl InputMethodHandler for Ferese { return geometry; } - let Some(layer) = self.space.layer_for_surface(parent, WindowSurfaceType::TOPLEVEL) else { + let Some(layer) = self + .space + .layer_for_surface(parent, WindowSurfaceType::TOPLEVEL) + else { return Rectangle::default(); }; diff --git a/compositor/src/handlers/layer_shell.rs b/compositor/src/handlers/layer_shell.rs index a72be11..7179720 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -1,4 +1,6 @@ -use smithay::desktop::{LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output}; +use smithay::desktop::{ + LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output, +}; use smithay::output::Output; use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; @@ -14,7 +16,13 @@ impl WlrLayerShellHandler for Ferese { &mut self.layer_shell_state } - fn new_layer_surface(&mut self, surface: LayerSurface, output: Option, _layer: Layer, namespace: String) { + fn new_layer_surface( + &mut self, + surface: LayerSurface, + output: Option, + _layer: Layer, + namespace: String, + ) { // A wl_output resource can outlive its registered desktop output. // Explicit requests retain their identity; only unspecified requests // may use the focused/default output policy. @@ -82,7 +90,11 @@ impl WlrLayerShellHandler for Ferese { let outputs = self .space .outputs() - .filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some()) + .filter(|output| { + layer_map_for_output(output) + .layer_geometry(&layer) + .is_some() + }) .cloned() .collect::>(); @@ -134,13 +146,20 @@ impl LayoutState { } pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { - let Some(layer) = state.space.layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) else { + let Some(layer) = state + .space + .layer_for_surface(surface, WindowSurfaceType::TOPLEVEL) + else { return; }; let outputs = state .space .outputs() - .filter(|output| layer_map_for_output(output).layer_geometry(&layer).is_some()) + .filter(|output| { + layer_map_for_output(output) + .layer_geometry(&layer) + .is_some() + }) .cloned() .collect::>(); @@ -154,9 +173,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { } let policy = layer.cached_state(); let current = LayoutState::current(&layer); - let previous = layer - .user_data() - .get_or_insert_threadsafe(|| std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>)); + let previous = layer.user_data().get_or_insert_threadsafe(|| { + std::sync::Mutex::new(None::<(LayoutState, KeyboardInteractivity)>) + }); let (changed, requested_focus) = { let mut previous = previous.lock().unwrap(); let changed = previous.as_ref().map(|(layout, _)| layout) != Some(¤t); @@ -191,7 +210,10 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { }; state.reconcile_layer_keyboard_focus(); if opening_interaction - && !state.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) + && !state + .seat + .get_keyboard() + .is_some_and(|keyboard| keyboard.is_grabbed()) && !state.session_lock.active() && !state.input_capture.captures(1) && state.layer_keyboard_eligible(&layer) @@ -199,11 +221,15 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { && matches!(policy.layer, Layer::Top | Layer::Overlay) && !layer_has_keyboard_focus(state, &layer) { - state.seat.get_keyboard().expect("seat has a keyboard").set_focus( - state, - Some(layer.wl_surface().clone()), - smithay::utils::SERIAL_COUNTER.next_serial(), - ); + state + .seat + .get_keyboard() + .expect("seat has a keyboard") + .set_focus( + state, + Some(layer.wl_surface().clone()), + smithay::utils::SERIAL_COUNTER.next_serial(), + ); } if let Err(error) = state.display_handle.flush_clients() { @@ -220,9 +246,10 @@ impl Ferese { /// Use the renderer state updated by on_commit_buffer_handler, as for windows. fn layer_mapped_and_visible(&self, layer: &DesktopLayerSurface) -> bool { layer.layer_surface().alive() - && smithay::backend::renderer::utils::with_renderer_surface_state(layer.wl_surface(), |state| { - state.buffer().is_some() - }) + && smithay::backend::renderer::utils::with_renderer_surface_state( + layer.wl_surface(), + |state| state.buffer().is_some(), + ) .unwrap_or(false) && self.space.outputs().any(|output| { self.layer_visible_on_output(output, layer.layer()) @@ -239,8 +266,9 @@ impl Ferese { .find_popup(&surface) .and_then(|popup| smithay::desktop::find_popup_root_surface(&popup).ok()) .unwrap_or(surface); - (smithay::wayland::compositor::get_role(&root) == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE)) - .then_some(root) + (smithay::wayland::compositor::get_role(&root) + == Some(smithay::wayland::shell::wlr_layer::LAYER_SURFACE_ROLE)) + .then_some(root) } pub(crate) fn keyboard_focused_layer(&self) -> Option { @@ -255,7 +283,8 @@ impl Ferese { for priority in [Layer::Overlay, Layer::Top] { if let Some(layer) = focused.as_ref().filter(|layer| { layer.layer() == priority - && layer.cached_state().keyboard_interactivity == KeyboardInteractivity::Exclusive + && layer.cached_state().keyboard_interactivity + == KeyboardInteractivity::Exclusive && self.layer_keyboard_eligible(layer) }) { return Some(layer.clone()); @@ -313,7 +342,9 @@ impl Ferese { } fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool { - state.keyboard_focused_layer().is_some_and(|focused| focused == *layer) + state + .keyboard_focused_layer() + .is_some_and(|focused| focused == *layer) } #[cfg(test)] @@ -349,7 +380,10 @@ mod tests { original, LayoutState::from_policy(policy, Rectangle::from_size((800, 33).into())) ); - assert_ne!(original, LayoutState::from_policy(policy, Rectangle::default())); + assert_ne!( + original, + LayoutState::from_policy(policy, Rectangle::default()) + ); } } diff --git a/compositor/src/handlers/layer_shell/output_tests.rs b/compositor/src/handlers/layer_shell/output_tests.rs index 3ddf8ec..acc411c 100644 --- a/compositor/src/handlers/layer_shell/output_tests.rs +++ b/compositor/src/handlers/layer_shell/output_tests.rs @@ -151,7 +151,13 @@ impl Client { ) -> Vec> { if output == 0 { // Reserve the output slot with a spare surface for unspecified requests. - request(&mut self.wire, self.compositor.id().protocol_id(), 0, &[5], None); + request( + &mut self.wire, + self.compositor.id().protocol_id(), + 0, + &[5], + None, + ); } request( &mut self.wire, @@ -199,8 +205,9 @@ fn assert_closed_without_configure(messages: &[Vec]) { #[test] fn zero_outputs_close_new_layer_roles() { - if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles") - { + if !crate::startup_tests::private_runtime( + "handlers::layer_shell::output_tests::zero_outputs_close_new_layer_roles", + ) { return; } let (mut state, mut events) = fixture(); @@ -259,8 +266,9 @@ fn unspecified_output_uses_focused_registered_output() { #[test] fn valid_explicit_output_is_preserved() { - if !crate::startup_tests::private_runtime("handlers::layer_shell::output_tests::valid_explicit_output_is_preserved") - { + if !crate::startup_tests::private_runtime( + "handlers::layer_shell::output_tests::valid_explicit_output_is_preserved", + ) { return; } let (mut state, mut events) = fixture(); diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index fa2b732..fc1fb04 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -28,7 +28,9 @@ use smithay::wayland::keyboard_shortcuts_inhibit::{ KeyboardShortcutsInhibitorSeat, }; use smithay::wayland::output::OutputHandler; -use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint}; +use smithay::wayland::pointer_constraints::{ + PointerConstraint, PointerConstraintsHandler, with_pointer_constraint, +}; use smithay::wayland::selection::SelectionHandler; use smithay::wayland::selection::data_device::{ DataDeviceHandler, DataDeviceState, WaylandDndGrabHandler, set_data_device_focus, @@ -54,12 +56,17 @@ impl SeatHandler for Ferese { if let CursorImageStatus::Named(icon) = &image && !self.named_cursors.contains_key(icon) { - self.cursor_loader.request(*icon, self.cursor_animation.buffer_scale); + self.cursor_loader + .request(*icon, self.cursor_animation.buffer_scale); } self.cursor_animation.reset |= match (&self.cursor_status, &image) { - (CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => previous != next, + (CursorImageStatus::Named(previous), CursorImageStatus::Named(next)) => { + previous != next + } (CursorImageStatus::Hidden, CursorImageStatus::Hidden) => false, - (CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => previous != next, + (CursorImageStatus::Surface(previous), CursorImageStatus::Surface(next)) => { + previous != next + } _ => true, }; self.cursor_status = image; @@ -74,7 +81,10 @@ impl SeatHandler for Ferese { self.pending_layer_focus = None; // Smithay has already sent enter. last_enter uses a separate mutex; // querying current_focus here would reenter the held keyboard lock. - if let Some(serial) = seat.get_keyboard().and_then(|keyboard| keyboard.last_enter()) { + if let Some(serial) = seat + .get_keyboard() + .and_then(|keyboard| keyboard.last_enter()) + { self.record_activation_input(serial, focused.cloned()); } @@ -86,7 +96,9 @@ impl SeatHandler for Ferese { { inhibitor.inactivate(); } - if let Some(inhibitor) = focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface)) { + if let Some(inhibitor) = + focused.and_then(|surface| seat.keyboard_shortcuts_inhibitor_for_surface(surface)) + { inhibitor.activate(); self.active_shortcuts_inhibitor = Some(inhibitor); } @@ -136,7 +148,10 @@ impl KeyboardShortcutsInhibitHandler for Ferese { } fn new_inhibitor(&mut self, inhibitor: KeyboardShortcutsInhibitor) { - let focused = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus()); + let focused = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()); if focused.as_ref() == Some(inhibitor.wl_surface()) { inhibitor.activate(); @@ -216,7 +231,9 @@ impl Ferese { let output = output .or_else(|| self.focused_output()) .or_else(|| self.space.outputs().next()); - with_states(surface, |states| set_surface_preferences(surface, states, output)); + with_states(surface, |states| { + set_surface_preferences(surface, states, output) + }); } } @@ -226,9 +243,18 @@ fn set_surface_preferences( output: Option<&smithay::output::Output>, ) { let scale = output.map_or(1.0, |output| output.current_scale().fractional_scale()); - let transform = output.map_or(smithay::utils::Transform::Normal, |output| output.current_transform()); - smithay::wayland::compositor::send_surface_state(surface, states, scale.ceil() as i32, transform); - with_fractional_scale(states, |surface_scale| surface_scale.set_preferred_scale(scale)); + let transform = output.map_or(smithay::utils::Transform::Normal, |output| { + output.current_transform() + }); + smithay::wayland::compositor::send_surface_state( + surface, + states, + scale.ceil() as i32, + transform, + ); + with_fractional_scale(states, |surface_scale| { + surface_scale.set_preferred_scale(scale) + }); } pub(crate) fn set_surface_tree_output(surface: &WlSurface, output: &smithay::output::Output) { @@ -337,13 +363,17 @@ impl PrimarySelectionHandler for Ferese { } impl smithay::wayland::selection::wlr_data_control::DataControlHandler for Ferese { - fn data_control_state(&mut self) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState { + fn data_control_state( + &mut self, + ) -> &mut smithay::wayland::selection::wlr_data_control::DataControlState { &mut self.wlr_data_control_state } } impl smithay::wayland::selection::ext_data_control::DataControlHandler for Ferese { - fn data_control_state(&mut self) -> &mut smithay::wayland::selection::ext_data_control::DataControlState { + fn data_control_state( + &mut self, + ) -> &mut smithay::wayland::selection::ext_data_control::DataControlState { &mut self.ext_data_control_state } } diff --git a/compositor/src/handlers/screencopy.rs b/compositor/src/handlers/screencopy.rs index d44809d..b2e49f4 100644 --- a/compositor/src/handlers/screencopy.rs +++ b/compositor/src/handlers/screencopy.rs @@ -11,7 +11,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{ }; use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; use smithay::reexports::wayland_server::protocol::wl_shm; -use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; use smithay::utils::{Buffer, Logical, Rectangle, Size}; use smithay::wayland::shm::with_buffer_contents_mut; @@ -157,7 +159,8 @@ impl PendingScreencopy { pub(crate) fn capture_allowed() -> bool { static ALLOWED: std::sync::OnceLock = std::sync::OnceLock::new(); - *ALLOWED.get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref())) + *ALLOWED + .get_or_init(|| capture_opted_in(std::env::var_os("FERESE_ENABLE_SCREENCOPY").as_deref())) } fn capture_opted_in(value: Option<&std::ffi::OsStr>) -> bool { @@ -321,7 +324,9 @@ impl Ferese { let captures = std::mem::take(&mut self.pending_screencopies); for capture in captures { - if &capture.output != output || !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) { + if &capture.output != output + || !cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) + { remaining.push(capture); continue; } @@ -332,7 +337,8 @@ impl Ferese { } framebuffer_binding_changed = true; - if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err() { + if complete_capture(renderer, framebuffer, &capture, self.start_time.elapsed()).is_err() + { capture.fail(); } } @@ -342,15 +348,18 @@ impl Ferese { } pub(crate) fn has_pending_screencopy(&self, output: &Output, overlay_cursor: bool) -> bool { - self.pending_screencopies - .iter() - .any(|capture| capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor)) + self.pending_screencopies.iter().any(|capture| { + capture.output == *output + && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) + }) } pub(crate) fn fail_screencopies(&mut self, output: &Output, overlay_cursor: bool) { let mut remaining = Vec::new(); for capture in std::mem::take(&mut self.pending_screencopies) { - if capture.output == *output && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) { + if capture.output == *output + && cursor_overlay_matches(capture.overlay_cursor, overlay_cursor) + { capture.sink.fail(); } else { remaining.push(capture); @@ -422,7 +431,10 @@ fn create_frame( } } -fn capture_region(output: &Output, requested: Option>) -> Option> { +fn capture_region( + output: &Output, + requested: Option>, +) -> Option> { let mode = output.current_mode()?; let scale = output.current_scale().fractional_scale(); let transform = output.current_transform(); @@ -458,7 +470,9 @@ pub(crate) fn capture_region_for_geometry( } let logical_size = output_logical_size(mode_size, scale, transform); let output_region = Rectangle::from_size(logical_size); - let logical_region = requested.unwrap_or(output_region).intersection(output_region)?; + let logical_region = requested + .unwrap_or(output_region) + .intersection(output_region)?; if logical_region.size.w <= 0 || logical_region.size.h <= 0 { return None; @@ -509,7 +523,9 @@ where tracing::warn!(?error, "failed to map output framebuffer copy"); })?; let expected = capture.region.size; - let stride = (expected.w as usize).checked_mul(BYTES_PER_PIXEL).ok_or(())?; + let stride = (expected.w as usize) + .checked_mul(BYTES_PER_PIXEL) + .ok_or(())?; let bytes = stride.checked_mul(expected.h as usize).ok_or(())?; if source.len() < bytes { return Err(()); @@ -567,7 +583,11 @@ where frame.flags(zwlr_screencopy_frame_v1::Flags::empty()); let seconds = timestamp.as_secs(); - frame.ready((seconds >> 32) as u32, seconds as u32, timestamp.subsec_nanos()); + frame.ready( + (seconds >> 32) as u32, + seconds as u32, + timestamp.subsec_nanos(), + ); } Ok(()) } @@ -596,7 +616,8 @@ mod tests { let mode = Size::::from((1_920, 1_080)); let requested = Rectangle::new((-10, 20).into(), (110, 50).into()); - let region = capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap(); + let region = + capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested)).unwrap(); assert_eq!(region, Rectangle::new((0, 40).into(), (200, 100).into())); } @@ -606,7 +627,9 @@ mod tests { let mode = Size::::from((1_920, 1_080)); let requested = Rectangle::new((1_000, 600).into(), (100, 100).into()); - assert!(capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none()); + assert!( + capture_region_for_geometry(mode, 2.0, Transform::Normal, Some(requested),).is_none() + ); } #[test] diff --git a/compositor/src/handlers/screenshot.rs b/compositor/src/handlers/screenshot.rs index 6a114b2..0e86774 100644 --- a/compositor/src/handlers/screenshot.rs +++ b/compositor/src/handlers/screenshot.rs @@ -40,7 +40,9 @@ const MAX_CANVAS_BYTES: usize = 128 * 1024 * 1024; impl Canvas { fn new(width: u32, height: u32) -> Option { - let length = (width as usize).checked_mul(height as usize)?.checked_mul(4)?; + let length = (width as usize) + .checked_mul(height as usize)? + .checked_mul(4)?; if width == 0 || height == 0 || length > MAX_CANVAS_BYTES { return None; } @@ -83,7 +85,11 @@ impl Canvas { } } -fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: usize) -> Option> { +fn scale_and_convert( + frame: &OutputFrame, + target_width: usize, + target_height: usize, +) -> Option> { let (upright_width, upright_height) = upright_size(frame); if upright_width <= 0 || upright_height <= 0 || target_width == 0 || target_height == 0 { return None; @@ -95,8 +101,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us if upright_width as usize == target_width && upright_height as usize == target_height { for row in 0..target_height { for column in 0..target_width { - let (x, y) = - upright_source_pixel(column as i32, row as i32, frame.width, frame.height, frame.transform); + let (x, y) = upright_source_pixel( + column as i32, + row as i32, + frame.width, + frame.height, + frame.transform, + ); let from = (y as usize) .checked_mul(frame.stride)? .checked_add(x as usize * BYTES_PER_PIXEL)?; @@ -147,7 +158,13 @@ fn scale_and_convert(frame: &OutputFrame, target_width: usize, target_height: us Some(out) } -fn upright_source_pixel(x: i32, y: i32, width: i32, height: i32, transform: Transform) -> (i32, i32) { +fn upright_source_pixel( + x: i32, + y: i32, + width: i32, + height: i32, + transform: Transform, +) -> (i32, i32) { match transform { Transform::_90 => (y, height - 1 - x), Transform::_180 => (width - 1 - x, height - 1 - y), @@ -173,7 +190,10 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result { } let (left, top) = logical_origin(frames).ok_or("Screenshot region is empty")?; - let scale = frames.iter().map(|frame| frame.scale).fold(0.0_f64, f64::max); + let scale = frames + .iter() + .map(|frame| frame.scale) + .fold(0.0_f64, f64::max); if !(scale.is_finite() && scale > 0.0) { return Err("Screenshot outputs have an invalid scale".into()); } @@ -200,15 +220,22 @@ pub(crate) fn compose(frames: &[OutputFrame]) -> Result { if width <= 0 || height <= 0 { return Err("Screenshot region is empty".into()); } - let mut canvas = Canvas::new(width as u32, height as u32).ok_or("Screenshot image is too large to encode")?; + let mut canvas = Canvas::new(width as u32, height as u32) + .ok_or("Screenshot image is too large to encode")?; for (frame, (x0, y0, target_width, target_height)) in frames.iter().zip(placement) { if target_width == 0 || target_height == 0 { continue; } - let scaled = - scale_and_convert(frame, target_width as usize, target_height as usize).ok_or("Invalid capture size")?; - canvas.blit(&scaled, target_width as usize, target_height as usize, x0, y0); + let scaled = scale_and_convert(frame, target_width as usize, target_height as usize) + .ok_or("Invalid capture size")?; + canvas.blit( + &scaled, + target_width as usize, + target_height as usize, + x0, + y0, + ); } Ok(canvas) @@ -269,7 +296,12 @@ fn logical_origin(frames: &[OutputFrame]) -> Option<(i32, i32)> { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum Geometry { All, - Region { x: i32, y: i32, width: i32, height: i32 }, + Region { + x: i32, + y: i32, + width: i32, + height: i32, + }, } impl Geometry { @@ -279,9 +311,16 @@ impl Geometry { if width <= 0 || height <= 0 { return Err("Screenshot region must have a positive size".into()); } - x.checked_add(width).ok_or("screenshot geometry overflows")?; - y.checked_add(height).ok_or("screenshot geometry overflows")?; - Ok(Geometry::Region { x, y, width, height }) + x.checked_add(width) + .ok_or("screenshot geometry overflows")?; + y.checked_add(height) + .ok_or("screenshot geometry overflows")?; + Ok(Geometry::Region { + x, + y, + width, + height, + }) } } @@ -402,7 +441,8 @@ impl Budget { fn reserve(self: &Arc, bytes: usize) -> Result { self.used .fetch_update(Ordering::AcqRel, Ordering::Acquire, |used| { - used.checked_add(bytes).filter(|total| *total <= MAX_PENDING_BYTES) + used.checked_add(bytes) + .filter(|total| *total <= MAX_PENDING_BYTES) }) .map_err(|_| "Screenshot memory budget is exhausted".to_string())?; Ok(BudgetPermit { @@ -492,7 +532,10 @@ impl Coordinator { response_id, reply, state: RequestState::Collecting, - parts: specs.into_iter().map(|spec| (spec, PartState::Pending)).collect(), + parts: specs + .into_iter() + .map(|spec| (spec, PartState::Pending)) + .collect(), received: 0, deadline: now + REQUEST_TIMEOUT, permit: Some(permit), @@ -503,7 +546,9 @@ impl Coordinator { } pub(crate) fn permit(&self, request: u64) -> Option { - self.requests.get(&request).and_then(|entry| entry.permit.clone()) + self.requests + .get(&request) + .and_then(|entry| entry.permit.clone()) } // Used by the completion tests to assert that a request is still tracked. @@ -519,7 +564,12 @@ impl Coordinator { ) } - pub(crate) fn on_part(&mut self, request: u64, part: usize, result: Result) -> Action { + pub(crate) fn on_part( + &mut self, + request: u64, + part: usize, + result: Result, + ) -> Action { let Some(entry) = self.requests.get_mut(&request) else { return Action::None; }; @@ -572,7 +622,10 @@ impl Coordinator { Action::Encode { request, frames, - permit: entry.permit.take().expect("collecting request owns its reservation"), + permit: entry + .permit + .take() + .expect("collecting request owns its reservation"), } } @@ -613,7 +666,10 @@ impl Coordinator { .map(|(id, _)| *id) .collect(); for id in &expired { - self.reject_inner(*id, "Screenshot timed out: an output was not redrawn in time"); + self.reject_inner( + *id, + "Screenshot timed out: an output was not redrawn in time", + ); } expired } @@ -728,7 +784,10 @@ impl PlannedPart { // Turns a requested logical region into the per-output reads that cover it. // The logical-to-buffer conversion is shared with the screencopy path so both // agree on rounding at fractional scales. -pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result, String> { +pub(crate) fn plan( + geometry: &Geometry, + outputs: &[OutputLayout], +) -> Result, String> { if outputs.is_empty() { return Err("No outputs are enabled".into()); } @@ -739,7 +798,8 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result> = outputs.iter().map(OutputLayout::logical_size).collect(); + let logical_sizes: Vec> = + outputs.iter().map(OutputLayout::logical_size).collect(); let requested = match geometry { Geometry::All => { @@ -751,8 +811,10 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result { - let top_left = - Point::::new(current.loc.x.min(rect.loc.x), current.loc.y.min(rect.loc.y)); + let top_left = Point::::new( + current.loc.x.min(rect.loc.x), + current.loc.y.min(rect.loc.y), + ); let bottom_right = Point::::new( (current.loc.x + current.size.w).max(rect.loc.x + rect.size.w), (current.loc.y + current.size.h).max(rect.loc.y + rect.size.h), @@ -764,7 +826,12 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result Rectangle { + Geometry::Region { + x, + y, + width, + height, + } => Rectangle { loc: (*x, *y).into(), size: (*width, *height).into(), }, @@ -792,7 +859,11 @@ pub(crate) fn plan(geometry: &Geometry, outputs: &[OutputLayout]) -> Result Geometry { - Geometry::Region { x, y, width, height } + Geometry::Region { + x, + y, + width, + height, + } } #[test] fn a_crop_crossing_a_seam_splits_into_one_part_per_output() { - let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)]; + let outputs = [ + output(0, 0, 1920, 1080, 1.0), + output(1920, 0, 1920, 1080, 1.0), + ]; let parts = plan(®ion(1800, 0, 200, 100), &outputs).unwrap(); assert_eq!(parts.len(), 2); assert_eq!(parts[0].index, 0); - assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (1800, 120)); assert_eq!( - (parts[1].index, parts[1].logical.loc.x, parts[1].logical.size.w), + (parts[0].logical.loc.x, parts[0].logical.size.w), + (1800, 120) + ); + assert_eq!( + ( + parts[1].index, + parts[1].logical.loc.x, + parts[1].logical.size.w + ), (1, 1920, 80) ); // The clipped widths must add back up to the request. @@ -884,8 +970,14 @@ mod planning { let outputs = [output(0, 0, 800, 600, 1.0), output(800, 0, 1600, 1200, 1.5)]; let parts = plan(®ion(600, 0, 400, 300), &outputs).unwrap(); assert_eq!(parts.len(), 2); - assert_eq!((parts[0].logical.loc.x, parts[0].logical.size.w), (600, 200)); - assert_eq!((parts[1].logical.loc.x, parts[1].logical.size.w), (800, 200)); + assert_eq!( + (parts[0].logical.loc.x, parts[0].logical.size.w), + (600, 200) + ); + assert_eq!( + (parts[1].logical.loc.x, parts[1].logical.size.w), + (800, 200) + ); // The 1.5 output reads back at 1.5x while still reporting logical 200. let spec = parts[1].spec(&outputs[1]); assert_eq!(spec.logical_width, 200); @@ -895,7 +987,10 @@ mod planning { #[test] fn all_geometry_unions_every_output() { - let outputs = [output(0, 0, 1920, 1080, 1.0), output(-1280, -400, 1280, 800, 1.0)]; + let outputs = [ + output(0, 0, 1920, 1080, 1.0), + output(-1280, -400, 1280, 800, 1.0), + ]; let parts = plan(&Geometry::All, &outputs).unwrap(); assert_eq!(parts.len(), 2); // Neither output is moved into a normalised origin at plan time. @@ -906,7 +1001,10 @@ mod planning { #[test] fn a_crop_misses_outputs_that_do_not_intersect() { - let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)]; + let outputs = [ + output(0, 0, 1920, 1080, 1.0), + output(1920, 0, 1920, 1080, 1.0), + ]; let parts = plan(®ion(4000, 0, 100, 100), &outputs); assert!(parts.is_err(), "a region off the desktop is reported"); } @@ -916,11 +1014,17 @@ mod planning { // The second output starts exactly where the region ends. It contributes // nothing, so the capture must still succeed with just the first output // rather than failing on a region it does not overlap. - let outputs = [output(0, 0, 1920, 1080, 1.0), output(1920, 0, 1920, 1080, 1.0)]; + let outputs = [ + output(0, 0, 1920, 1080, 1.0), + output(1920, 0, 1920, 1080, 1.0), + ]; let parts = plan(®ion(100, 100, 1820, 980), &outputs).expect("captures the overlap"); assert_eq!(parts.len(), 1, "only the overlapping output is read"); assert_eq!(parts[0].index, 0); - assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1820, 980)); + assert_eq!( + (parts[0].logical.size.w, parts[0].logical.size.h), + (1820, 980) + ); } #[test] @@ -930,10 +1034,16 @@ mod planning { // A 90-degree transform turns the 1920x1080 mode into a 1080x1920 // logical area, and the readback follows the same rotation. let parts = plan(®ion(0, 0, 1080, 400), std::slice::from_ref(&rotated)).unwrap(); - assert_eq!((parts[0].logical.size.w, parts[0].logical.size.h), (1080, 400)); + assert_eq!( + (parts[0].logical.size.w, parts[0].logical.size.h), + (1080, 400) + ); let spec = parts[0].spec(&rotated); assert_eq!(spec.transform, Transform::_90); - assert!(spec.buffer_height > spec.buffer_width, "buffer axes are swapped"); + assert!( + spec.buffer_height > spec.buffer_width, + "buffer axes are swapped" + ); } #[test] @@ -1013,9 +1123,17 @@ mod completion { let (reply, received) = sync_channel(8); let mut coordinator = Coordinator::new(); coordinator - .admit(1, 42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect()) + .admit( + 1, + 42, + reply.clone(), + (0..parts).map(|_| spec(2, 2)).collect(), + ) .expect("admitted"); - Self { coordinator, received } + Self { + coordinator, + received, + } } fn response(&self) -> Option { @@ -1036,7 +1154,11 @@ mod completion { }; assert_eq!(frames.len(), 1); - assert_eq!(frames[0].pixels.as_ptr(), allocation, "readback pixels were copied"); + assert_eq!( + frames[0].pixels.as_ptr(), + allocation, + "readback pixels were copied" + ); assert_eq!(frames[0].pixels, vec![137; 16]); } @@ -1049,13 +1171,21 @@ mod completion { harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))), Action::None )); - assert!(harness.coordinator.is_live(id), "still collecting after part 0"); + assert!( + harness.coordinator.is_live(id), + "still collecting after part 0" + ); assert!(matches!( - harness.coordinator.on_part(id, 1, Err("readback failed".into())), + harness + .coordinator + .on_part(id, 1, Err("readback failed".into())), Action::None )); - assert!(!harness.coordinator.is_live(id), "one failed part ends the request"); + assert!( + !harness.coordinator.is_live(id), + "one failed part ends the request" + ); let response = harness.response().expect("caller is always answered"); assert_eq!(response.id, 42); @@ -1070,7 +1200,9 @@ mod completion { harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); // Output 1 is removed from the layout, so its pending capture is failed. - harness.coordinator.on_part(id, 1, Err("output was removed".into())); + harness + .coordinator + .on_part(id, 1, Err("output was removed".into())); assert!(!harness.coordinator.is_live(id)); assert!(harness.response().unwrap().error.is_some()); @@ -1109,14 +1241,26 @@ mod completion { fn admission_enforces_one_budget_across_requests_and_late_readbacks() { let (reply, _received) = sync_channel(8); let mut coordinator = Coordinator::new(); - let first = coordinator.admit(1, 1, reply.clone(), vec![spec(4096, 8192)]).unwrap(); - let second = coordinator.admit(1, 2, reply.clone(), vec![spec(4096, 8192)]).unwrap(); - assert!(coordinator.admit(1, 3, reply.clone(), vec![spec(2, 2)]).is_err()); + let first = coordinator + .admit(1, 1, reply.clone(), vec![spec(4096, 8192)]) + .unwrap(); + let second = coordinator + .admit(1, 2, reply.clone(), vec![spec(4096, 8192)]) + .unwrap(); + assert!( + coordinator + .admit(1, 3, reply.clone(), vec![spec(2, 2)]) + .is_err() + ); // A cancelled request can still have a readback retained by a sink or channel. let late_readback = coordinator.permit(first).unwrap(); coordinator.reject(first, "cancelled"); - assert!(coordinator.admit(1, 4, reply.clone(), vec![spec(2, 2)]).is_err()); + assert!( + coordinator + .admit(1, 4, reply.clone(), vec![spec(2, 2)]) + .is_err() + ); drop(late_readback); assert!(coordinator.admit(1, 5, reply, vec![spec(2, 2)]).is_ok()); coordinator.reject(second, "finished"); @@ -1127,19 +1271,48 @@ mod completion { let mut harness = Harness::new(1); let id = harness.coordinator.next_id; let bytes = harness.coordinator.budget.used.load(Ordering::Acquire); - let Action::Encode { frames, permit, .. } = harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) else { + let Action::Encode { frames, permit, .. } = + harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))) + else { panic!("ready request should encode"); }; - harness.coordinator.reject(id, "cancelled while encoder owns pixels"); - assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), bytes); - assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_err()); + harness + .coordinator + .reject(id, "cancelled while encoder owns pixels"); + assert_eq!( + harness.coordinator.budget.used.load(Ordering::Acquire), + bytes + ); + assert!( + harness + .coordinator + .budget + .reserve(MAX_PENDING_BYTES) + .is_err() + ); drop(frames); drop(permit); assert_eq!(harness.coordinator.budget.used.load(Ordering::Acquire), 0); - let first = harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2 + 1).unwrap(); - assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES / 2).is_err()); + let first = harness + .coordinator + .budget + .reserve(MAX_PENDING_BYTES / 2 + 1) + .unwrap(); + assert!( + harness + .coordinator + .budget + .reserve(MAX_PENDING_BYTES / 2) + .is_err() + ); drop(first); - assert!(harness.coordinator.budget.reserve(MAX_PENDING_BYTES).is_ok()); + assert!( + harness + .coordinator + .budget + .reserve(MAX_PENDING_BYTES) + .is_ok() + ); } #[test] @@ -1152,7 +1325,10 @@ mod completion { Action::None )); let action = harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))); - let Action::Encode { request, frames, .. } = action else { + let Action::Encode { + request, frames, .. + } = action + else { panic!("expected the request to move to encoding"); }; assert_eq!(request, id); @@ -1177,7 +1353,9 @@ mod completion { // Unlocking does not resurrect the request; the file the worker // eventually produces is ours to delete. assert!(!harness.coordinator.is_live(id)); - let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/late.png"))); + let action = harness + .coordinator + .on_encoded(id, Ok(PathBuf::from("/tmp/late.png"))); assert!(matches!(action, Action::DiscardFile(ref path) if path.ends_with("late.png"))); assert!(harness.response().is_none(), "no late success is delivered"); } @@ -1189,7 +1367,9 @@ mod completion { harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); assert!(harness.coordinator.is_encoding(id)); assert_eq!( - harness.coordinator.terminate_all_with_reason("Capture privacy changed"), + harness + .coordinator + .terminate_all_with_reason("Capture privacy changed"), vec![id] ); assert!(harness.response().unwrap().error.is_some()); @@ -1208,7 +1388,9 @@ mod completion { let id = harness.coordinator.next_id; harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); - harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/shot.png"))); + harness + .coordinator + .on_encoded(id, Ok(PathBuf::from("/tmp/shot.png"))); let response = harness.response().expect("success is answered"); assert_eq!(response.id, 42); assert!(response.error.is_none()); @@ -1216,7 +1398,9 @@ mod completion { // A duplicate completion for the same request has nowhere to go, and // its file is an orphan rather than a second delivery. - let action = harness.coordinator.on_encoded(id, Ok(PathBuf::from("/tmp/again.png"))); + let action = harness + .coordinator + .on_encoded(id, Ok(PathBuf::from("/tmp/again.png"))); assert!(matches!(action, Action::DiscardFile(_))); // A delivered path is reported separately so the caller cannot @@ -1239,7 +1423,9 @@ mod completion { let id = harness.coordinator.next_id; harness.coordinator.on_part(id, 0, Ok(buffer(2, 2))); - harness.coordinator.on_encoded(id, Err("encoding failed".into())); + harness + .coordinator + .on_encoded(id, Err("encoding failed".into())); let response = harness.response().expect("failure is answered"); assert_eq!(response.error.unwrap().message, "encoding failed"); assert!(!harness.coordinator.is_live(id)); @@ -1252,13 +1438,23 @@ mod completion { // client would never be answered. let mut harness = Harness::new(2); let id = harness.coordinator.next_id; - harness.coordinator.requests.get_mut(&id).expect("tracked").deadline = Instant::now() - Duration::from_secs(1); + harness + .coordinator + .requests + .get_mut(&id) + .expect("tracked") + .deadline = Instant::now() - Duration::from_secs(1); assert_eq!(harness.coordinator.expire_at(Instant::now()), vec![id]); let response = harness.response().expect("the caller is answered"); assert_eq!(response.error.as_ref().unwrap().code, "screenshot_failed"); assert!( - response.error.as_ref().unwrap().message.contains("timed out"), + response + .error + .as_ref() + .unwrap() + .message + .contains("timed out"), "the failure explains itself: {:?}", response.error.as_ref().unwrap().message ); @@ -1275,7 +1471,10 @@ mod completion { fn a_live_request_is_not_expired_early() { let mut harness = Harness::new(1); let id = harness.coordinator.next_id; - assert!(harness.coordinator.expire_at(Instant::now()).is_empty(), "not yet due"); + assert!( + harness.coordinator.expire_at(Instant::now()).is_empty(), + "not yet due" + ); assert!(harness.coordinator.is_live(id)); assert!(harness.response().is_none(), "still unanswered, not failed"); let remaining = harness @@ -1334,7 +1533,9 @@ mod completion { ); } - fn observe_deadlines(coordinator: &mut Coordinator) -> std::rc::Rc>>> { + fn observe_deadlines( + coordinator: &mut Coordinator, + ) -> std::rc::Rc>>> { let changes = std::rc::Rc::new(std::cell::RefCell::new(Vec::new())); let observed = changes.clone(); coordinator.set_deadline_observer(move |deadline| observed.borrow_mut().push(deadline)); @@ -1356,15 +1557,29 @@ mod completion { .admit_at(1, 42, reply.clone(), vec![spec(2, 2)], late) .unwrap(); let second = coordinator - .admit_at(2, 43, reply.clone(), vec![spec(2, 2)], late + Duration::from_secs(5)) + .admit_at( + 2, + 43, + reply.clone(), + vec![spec(2, 2)], + late + Duration::from_secs(5), + ) .unwrap(); assert_eq!( changes.borrow().len(), 2, "a later request does not rearm the earliest timer" ); - assert!(coordinator.admit_at(1, 44, reply, Vec::new(), late).is_err()); - assert_eq!(changes.borrow().len(), 2, "rejected admission does not touch the timer"); + assert!( + coordinator + .admit_at(1, 44, reply, Vec::new(), late) + .is_err() + ); + assert_eq!( + changes.borrow().len(), + 2, + "rejected admission does not touch the timer" + ); assert_eq!(coordinator.terminate_owner(1), vec![first]); assert!(coordinator.is_live(second)); assert_eq!(coordinator.terminate_owner(99), Vec::::new()); @@ -1382,7 +1597,10 @@ mod completion { #[test] fn deadline_survives_readback_and_encoding_then_disarms_on_completion() { - for result in [Ok(PathBuf::from("/tmp/shot.png")), Err("encoder failed".into())] { + for result in [ + Ok(PathBuf::from("/tmp/shot.png")), + Err("encoder failed".into()), + ] { let mut harness = Harness::new(2); let changes = observe_deadlines(&mut harness.coordinator); let id = harness.coordinator.next_id; @@ -1392,11 +1610,19 @@ mod completion { harness.coordinator.on_part(id, 1, Ok(buffer(2, 2))), Action::Encode { .. } )); - assert_eq!(&*changes.borrow(), &[deadline], "encoding keeps the original deadline"); + assert_eq!( + &*changes.borrow(), + &[deadline], + "encoding keeps the original deadline" + ); harness.coordinator.on_encoded(id, result); assert_eq!(&*changes.borrow(), &[deadline, None]); - harness.coordinator.on_part(id, 0, Err("late readback".into())); - harness.coordinator.on_encoded(id, Err("late encode".into())); + harness + .coordinator + .on_part(id, 0, Err("late readback".into())); + harness + .coordinator + .on_encoded(id, Err("late encode".into())); assert_eq!( changes.borrow().len(), 2, @@ -1440,7 +1666,11 @@ mod completion { } _ => unreachable!(), } - assert_eq!(changes.borrow().last(), Some(&None), "{cancellation} must disarm"); + assert_eq!( + changes.borrow().last(), + Some(&None), + "{cancellation} must disarm" + ); assert_eq!(changes.borrow().len(), 2); assert!(harness.response().unwrap().error.is_some()); assert!(!harness.coordinator.is_live(id)); @@ -1458,7 +1688,13 @@ mod completion { .admit_at(1, 42, reply.clone(), vec![spec(2, 2)], now) .unwrap(); let second = coordinator - .admit_at(2, 43, reply.clone(), vec![spec(2, 2)], now + Duration::from_secs(5)) + .admit_at( + 2, + 43, + reply.clone(), + vec![spec(2, 2)], + now + Duration::from_secs(5), + ) .unwrap(); coordinator.on_part(first, 0, Ok(buffer(2, 2))); assert!( @@ -1487,7 +1723,13 @@ mod completion { Action::DiscardFile(_) )); coordinator - .admit_at(3, 44, reply, vec![spec(2, 2)], now + Duration::from_secs(3600)) + .admit_at( + 3, + 44, + reply, + vec![spec(2, 2)], + now + Duration::from_secs(3600), + ) .unwrap(); assert_eq!( changes.borrow().last(), @@ -1505,7 +1747,10 @@ mod completion { assert!(!sink.publish(buffer(64, 64))); let PartOutcome { - request, part, result, .. + request, + part, + result, + .. } = receiver.try_recv().expect("published once"); assert_eq!((request, part), (7, 1)); assert_eq!(result.unwrap().width, 2); @@ -1522,7 +1767,10 @@ mod completion { assert!(!sink.publish(buffer(2, 2))); let PartOutcome { - request, part, result, .. + request, + part, + result, + .. } = receiver.try_recv().expect("answered"); assert_eq!((request, part), (3, 0)); assert!(result.is_err()); @@ -1913,7 +2161,11 @@ mod tests { // 9 logical at scale 1.5. assert_eq!((canvas.width, canvas.height), (14, 5)); for column in 0..canvas.width as usize { - assert_eq!(canvas.pixels[column * 4 + 3], 255, "column {column} has a gap"); + assert_eq!( + canvas.pixels[column * 4 + 3], + 255, + "column {column} has a gap" + ); } } @@ -1945,7 +2197,11 @@ mod tests { let canvas = compose(&[left, right]).unwrap(); assert_eq!(canvas.width, 5); - assert_eq!(&canvas.pixels[2 * 4..3 * 4], &[0, 0, 0, 0], "gap is transparent"); + assert_eq!( + &canvas.pixels[2 * 4..3 * 4], + &[0, 0, 0, 0], + "gap is transparent" + ); assert_eq!(canvas.pixels[3], 255); assert_eq!(canvas.pixels[3 * 4 + 3], 255); } @@ -1974,7 +2230,11 @@ mod tests { #[test] fn geometry_tolerates_surrounding_whitespace_from_slurp() { - for raw in ["100,200 800x600\n", " 100,200 800x600 ", "100,200 800x600\r\n"] { + for raw in [ + "100,200 800x600\n", + " 100,200 800x600 ", + "100,200 800x600\r\n", + ] { assert_eq!( parse_geometry(raw).unwrap(), Geometry::Region { diff --git a/compositor/src/handlers/screenshot_worker.rs b/compositor/src/handlers/screenshot_worker.rs index 5336fe0..c2eefb9 100644 --- a/compositor/src/handlers/screenshot_worker.rs +++ b/compositor/src/handlers/screenshot_worker.rs @@ -113,8 +113,9 @@ fn encode_png(canvas: &Canvas) -> Result, String> { if width == 0 || height == 0 { return Err("Screenshot is empty".into()); } - let image = image::ImageBuffer::, _>::from_raw(width, height, canvas.pixels.as_slice()) - .ok_or("Screenshot buffer does not match its dimensions")?; + let image = + image::ImageBuffer::, _>::from_raw(width, height, canvas.pixels.as_slice()) + .ok_or("Screenshot buffer does not match its dimensions")?; let mut out = Vec::new(); let encoder = image::codecs::png::PngEncoder::new_with_quality( @@ -122,8 +123,14 @@ fn encode_png(canvas: &Canvas) -> Result, String> { image::codecs::png::CompressionType::Fast, image::codecs::png::FilterType::Adaptive, ); - image::ImageEncoder::write_image(encoder, image.as_raw(), width, height, image::ExtendedColorType::Rgba8) - .map_err(|error| format!("Could not encode screenshot: {error}"))?; + image::ImageEncoder::write_image( + encoder, + image.as_raw(), + width, + height, + image::ExtendedColorType::Rgba8, + ) + .map_err(|error| format!("Could not encode screenshot: {error}"))?; tracing::debug!(encoded = out.len(), "encoded screenshot"); Ok(out) } @@ -178,8 +185,8 @@ fn write_private_file_in( } fn private_directory() -> Result { - let runtime = - std::env::var_os("XDG_RUNTIME_DIR").ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?; + let runtime = std::env::var_os("XDG_RUNTIME_DIR") + .ok_or("XDG_RUNTIME_DIR is not set, so screenshots cannot be staged")?; let root = PathBuf::from(runtime); let directory = root.join("ferese-screenshots"); match fs::symlink_metadata(&directory) { @@ -194,7 +201,9 @@ fn private_directory() -> Result { } Err(error) if error.kind() == io::ErrorKind::NotFound => { fs::create_dir(&directory) - .and_then(|()| fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE))) + .and_then(|()| { + fs::set_permissions(&directory, fs::Permissions::from_mode(DIR_MODE)) + }) .map_err(|error| format!("Could not create screenshot directory: {error}"))?; } Err(error) => return Err(format!("Could not inspect screenshot directory: {error}")), @@ -248,7 +257,11 @@ mod tests { .expect("decodes back") .to_rgba8(); assert_eq!(decoded.dimensions(), (4, 3)); - assert_eq!(decoded.as_raw(), &canvas.pixels, "encoding changed the pixel data"); + assert_eq!( + decoded.as_raw(), + &canvas.pixels, + "encoding changed the pixel data" + ); // The opaque alpha we force is preserved through the round trip. assert_eq!(decoded.get_pixel(0, 0)[3], 255); } @@ -279,7 +292,10 @@ mod tests { let path = write_private_file(&png).expect("staged"); use std::os::unix::fs::PermissionsExt; let mode = fs::metadata(&path).unwrap().permissions().mode() & 0o777; - assert_eq!(mode, FILE_MODE, "staged screenshots are not group/world readable"); + assert_eq!( + mode, FILE_MODE, + "staged screenshots are not group/world readable" + ); fs::remove_file(&path).unwrap(); } @@ -293,7 +309,10 @@ mod tests { fs::write(&other, b"not a screenshot").unwrap(); sweep_stale_files(); - assert!(fresh.exists(), "a freshly staged screenshot is still in use"); + assert!( + fresh.exists(), + "a freshly staged screenshot is still in use" + ); assert!(other.exists(), "unrelated files are left alone"); fs::remove_file(&fresh).unwrap(); diff --git a/compositor/src/handlers/security_context.rs b/compositor/src/handlers/security_context.rs index 79ee424..5d46243 100644 --- a/compositor/src/handlers/security_context.rs +++ b/compositor/src/handlers/security_context.rs @@ -1,7 +1,9 @@ use std::sync::Arc; use smithay::reexports::wayland_server::Client; -use smithay::wayland::security_context::{SecurityContext, SecurityContextHandler, SecurityContextListenerSource}; +use smithay::wayland::security_context::{ + SecurityContext, SecurityContextHandler, SecurityContextListenerSource, +}; use crate::Ferese; use crate::state::ClientState; @@ -14,15 +16,18 @@ pub(crate) fn unrestricted_client(client: &Client) -> bool { impl SecurityContextHandler for Ferese { fn context_created(&mut self, source: SecurityContextListenerSource, context: SecurityContext) { - if let Err(error) = self.loop_handle.insert_source(source, move |stream, _, state| { - let data = ClientState { - security_context: Some(context.clone()), - ..Default::default() - }; - if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) { - tracing::warn!(%error, "failed to register sandboxed Wayland client"); - } - }) { + if let Err(error) = self + .loop_handle + .insert_source(source, move |stream, _, state| { + let data = ClientState { + security_context: Some(context.clone()), + ..Default::default() + }; + if let Err(error) = state.display_handle.insert_client(stream, Arc::new(data)) { + tracing::warn!(%error, "failed to register sandboxed Wayland client"); + } + }) + { tracing::warn!(%error, "failed to register security context listener"); } } diff --git a/compositor/src/handlers/toplevel_icon_tests.rs b/compositor/src/handlers/toplevel_icon_tests.rs index ee71db2..7301f79 100644 --- a/compositor/src/handlers/toplevel_icon_tests.rs +++ b/compositor/src/handlers/toplevel_icon_tests.rs @@ -28,19 +28,28 @@ fn icon_cleanup_messages(destroy_icon: bool) -> Vec<(u32, u16, Vec)> { .create_resource::(&state.display_handle, 6, GlobalData) .unwrap(); let shell = client - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); let shm = client .create_resource::(&state.display_handle, 1, GlobalData) .unwrap(); let manager = client - .create_resource::(&state.display_handle, 1, GlobalData) + .create_resource::( + &state.display_handle, + 1, + GlobalData, + ) .unwrap(); request(&mut wire, compositor.id().protocol_id(), 0, &[2], None); request(&mut wire, shell.id().protocol_id(), 2, &[3, 2], None); request(&mut wire, 3, 1, &[4], None); let mut file = tempfile::tempfile().unwrap(); - file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32)).unwrap(); + file.write_all(&0xff112233u32.to_ne_bytes().repeat(32 * 32)) + .unwrap(); request( &mut wire, shm.id().protocol_id(), @@ -85,9 +94,15 @@ fn destroying_an_assigned_icon_before_its_buffer_keeps_the_client_connected() { return; } let messages = icon_cleanup_messages(true); - assert!(!messages.iter().any(|(object, opcode, _)| *object == 1 && *opcode == 0)); assert!( - messages.iter().any(|(object, opcode, _)| *object == 8 && *opcode == 0), + !messages + .iter() + .any(|(object, opcode, _)| *object == 1 && *opcode == 0) + ); + assert!( + messages + .iter() + .any(|(object, opcode, _)| *object == 8 && *opcode == 0), "valid icon cleanup disconnected the client before its sync reply: {messages:?}" ); } diff --git a/compositor/src/handlers/window_capture.rs b/compositor/src/handlers/window_capture.rs index 508ea2f..d922b73 100644 --- a/compositor/src/handlers/window_capture.rs +++ b/compositor/src/handlers/window_capture.rs @@ -8,7 +8,9 @@ use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_scre use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1; use smithay::reexports::wayland_server::protocol::wl_buffer::WlBuffer; use smithay::reexports::wayland_server::protocol::wl_shm; -use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; use smithay::utils::Rectangle; use smithay::wayland::shm::with_buffer_contents_mut; @@ -34,7 +36,10 @@ struct Permit { impl Permit { fn reserve(budget: &Arc>, bytes: usize) -> Option { let mut available = budget.lock().unwrap(); - if bytes == 0 || bytes > MAX_BYTES.saturating_sub(available.bytes) || available.frames >= MAX_FRAMES { + if bytes == 0 + || bytes > MAX_BYTES.saturating_sub(available.bytes) + || available.frames >= MAX_FRAMES + { return None; } available.bytes += bytes; @@ -77,7 +82,11 @@ struct Conversion { pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool { use std::os::fd::AsRawFd; - let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 }; + let mut credentials = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; let mut length = std::mem::size_of_val(&credentials) as libc::socklen_t; // SAFETY: the connected socket is live and both out pointers cover their // initialized stack allocations. SO_PEERCRED identifies the connecting process. @@ -95,7 +104,10 @@ pub(crate) fn is_portal(stream: &std::os::unix::net::UnixStream) -> bool { } let expected = std::env::current_exe() .ok() - .and_then(|exe| exe.parent().map(|dir| dir.join("xdg-desktop-portal-ferese"))) + .and_then(|exe| { + exe.parent() + .map(|dir| dir.join("xdg-desktop-portal-ferese")) + }) .and_then(|exe| std::fs::metadata(exe).ok()); let actual = std::fs::metadata(format!("/proc/{}/exe", credentials.pid)).ok(); matches!((expected, actual), (Some(expected), Some(actual)) if expected.dev() == actual.dev() && expected.ino() == actual.ino()) @@ -178,11 +190,13 @@ fn finish_conversion(state: &Ferese, result: Conversion) { impl GlobalDispatch for Ferese { fn can_view(client: Client, _: &Global) -> bool { - client.get_data::().is_some_and(|state| { - state - .capabilities - .contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE) - }) + client + .get_data::() + .is_some_and(|state| { + state + .capabilities + .contains(crate::private_client::ClientCapabilities::WINDOW_CAPTURE) + }) } fn bind( @@ -248,7 +262,12 @@ impl Dispatch for Ferese { }) .is_err() { - *resource.data::>().unwrap().used.lock().unwrap() = true; + *resource + .data::>() + .unwrap() + .used + .lock() + .unwrap() = true; manager.busy(&resource); resource.failed(); } @@ -287,7 +306,8 @@ impl Ferese { .clone(); let geometry = window.geometry(); let scale = output.current_scale().fractional_scale(); - let size: smithay::utils::Size = geometry.size.to_physical_precise_round(scale); + let size: smithay::utils::Size = + geometry.size.to_physical_precise_round(scale); let bytes = usize::try_from(size.w) .ok()? .checked_mul(usize::try_from(size.h).ok()?)? @@ -299,26 +319,29 @@ impl Ferese { *busy = true; return None; }; - let cursor = if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected() { - self.seat.get_pointer().and_then(|pointer| { - let location = pointer.current_location(); - if self.window_under_visual(location).as_ref() != Some(&window) { - return None; - } - let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?; - let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64(); - let (surface, _) = window.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?; - if pointer.current_focus().as_ref() != Some(&surface) { - return None; - } - Some(Rectangle::new( - (location - smithay::utils::Point::from((x, y))).to_i32_round(), - geometry.size, - )) - }) - } else { - None - }; + let cursor = + if overlay_cursor && !self.overview.is_presenting() && !self.capture_cursor_protected() + { + self.seat.get_pointer().and_then(|pointer| { + let location = pointer.current_location(); + if self.window_under_visual(location).as_ref() != Some(&window) { + return None; + } + let (x, y) = self.inverse_presented_window_point(id, location.x, location.y)?; + let surface_point = smithay::utils::Point::from((x, y)) + geometry.loc.to_f64(); + let (surface, _) = window + .surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?; + if pointer.current_focus().as_ref() != Some(&surface) { + return None; + } + Some(Rectangle::new( + (location - smithay::utils::Point::from((x, y))).to_i32_round(), + geometry.size, + )) + }) + } else { + None + }; let result = if let Some(backend) = self.nested_backend.clone() { let mut backend = backend.try_borrow_mut().ok()?; crate::render::capture_window_frame( @@ -357,10 +380,18 @@ impl Ferese { } } -pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, data: &FrameData, buffer: &WlBuffer) { +pub(super) fn copy_snapshot( + state: &Ferese, + resource: &ZwlrScreencopyFrameV1, + data: &FrameData, + buffer: &WlBuffer, +) { let mut used = data.used.lock().unwrap(); if *used { - resource.post_error(frame::Error::AlreadyUsed, "window capture frame has already been used"); + resource.post_error( + frame::Error::AlreadyUsed, + "window capture frame has already been used", + ); return; } *used = true; @@ -393,7 +424,11 @@ pub(super) fn copy_snapshot(state: &Ferese, resource: &ZwlrScreencopyFrameV1, da // SAFETY: Smithay owns the writable mapping, validated format/stride and // checked offset cover the complete snapshot; the source is separately owned. unsafe { - std::ptr::copy_nonoverlapping(snapshot.pixels.pixels.as_ptr(), destination.add(offset), bytes); + std::ptr::copy_nonoverlapping( + snapshot.pixels.pixels.as_ptr(), + destination.add(offset), + bytes, + ); } true }) @@ -459,7 +494,11 @@ pub(crate) mod tests { .unwrap(); let data = frame_data(); let frame = client - .create_resource::, Ferese>(&state.display_handle, 3, data.clone()) + .create_resource::, Ferese>( + &state.display_handle, + 3, + data.clone(), + ) .unwrap(); finish_conversion( state, @@ -475,20 +514,27 @@ pub(crate) mod tests { let data = frame_data(); *data.snapshot.lock().unwrap() = Some(snapshot()); let frame = client - .create_resource::, Ferese>(&state.display_handle, 3, data.clone()) + .create_resource::, Ferese>( + &state.display_handle, + 3, + data.clone(), + ) .unwrap(); let buffer = client .object_from_protocol_id::(&state.display_handle, 6) .unwrap(); let before = with_buffer_contents_mut(&buffer, |ptr, _, layout| { // SAFETY: the fixture allocates and validates this 64x48 SHM buffer. - unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec() } + unsafe { + std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4).to_vec() + } }) .unwrap(); copy_snapshot(state, &frame, &data, &buffer); with_buffer_contents_mut(&buffer, |ptr, _, layout| { // SAFETY: same fixture-owned buffer as above. - let after = unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) }; + let after = + unsafe { std::slice::from_raw_parts(ptr.add(layout.offset as usize), 64 * 48 * 4) }; if allowed { assert!(after.iter().all(|byte| *byte == 0)); } else { @@ -496,7 +542,11 @@ pub(crate) mod tests { } // Restore the client's colored buffer for subsequent renderer checks. unsafe { - std::ptr::copy_nonoverlapping(before.as_ptr(), ptr.add(layout.offset as usize), before.len()); + std::ptr::copy_nonoverlapping( + before.as_ptr(), + ptr.add(layout.offset as usize), + before.len(), + ); } }) .unwrap(); diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index c56e6aa..954809a 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -60,7 +60,9 @@ impl XdgShellHandler for Ferese { fn parent_changed(&mut self, surface: ToplevelSurface) { self.refresh_capture_privacy(); self.refresh_idle_inhibition(); - let parent = surface.parent().and_then(|parent| self.windows.id_for_surface(&parent)); + let parent = surface + .parent() + .and_then(|parent| self.windows.id_for_surface(&parent)); let child = self .windows .id_for_surface(surface.wl_surface()) @@ -95,7 +97,12 @@ impl XdgShellHandler for Ferese { } } - fn reposition_request(&mut self, surface: PopupSurface, positioner: PositionerState, token: u32) { + fn reposition_request( + &mut self, + surface: PopupSurface, + positioner: PositionerState, + token: u32, + ) { surface.with_pending_state(|state| { state.geometry = positioner.get_geometry(); state.positioner = positioner; @@ -252,7 +259,12 @@ impl XdgShellHandler for Ferese { }; if let Some(pointer) = seat.get_pointer() { - pointer.set_grab(self, PopupPointerGrab::new(&popup_grab), serial, Focus::Keep); + pointer.set_grab( + self, + PopupPointerGrab::new(&popup_grab), + serial, + Focus::Keep, + ); } if let Some(keyboard) = seat.get_keyboard() { keyboard.set_grab(self, PopupKeyboardGrab::new(&popup_grab), serial); @@ -264,14 +276,21 @@ impl XdgShellHandler for Ferese { } } -fn check_grab(seat: &Seat, surface: &WlSurface, serial: Serial) -> Option> { +fn check_grab( + seat: &Seat, + surface: &WlSurface, + serial: Serial, +) -> Option> { let pointer = seat.get_pointer()?; if !pointer.has_grab(serial) { return None; } let start_data = pointer.grab_start_data()?; let (focus, _) = start_data.focus.as_ref()?; - focus.id().same_client_as(&surface.id()).then_some(start_data) + focus + .id() + .same_client_as(&surface.id()) + .then_some(start_data) } impl XdgDecorationHandler for Ferese { @@ -325,12 +344,14 @@ pub(crate) fn validate_size_constraints(toplevel: &ToplevelSurface) -> bool { let mut cached = states.cached_state.get::(); let state = cached.current(); let valid_axis = |min: i32, max: i32| min >= 0 && max >= 0 && (max == 0 || min <= max); - valid_axis(state.min_size.w, state.max_size.w) && valid_axis(state.min_size.h, state.max_size.h) + valid_axis(state.min_size.w, state.max_size.w) + && valid_axis(state.min_size.h, state.max_size.h) }); if !valid { - toplevel - .xdg_toplevel() - .post_error(xdg_toplevel::Error::InvalidSize, "invalid minimum/maximum window size"); + toplevel.xdg_toplevel().post_error( + xdg_toplevel::Error::InvalidSize, + "invalid minimum/maximum window size", + ); } valid @@ -359,7 +380,9 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { if let Some(PopupKind::Xdg(popup)) = state.popups.find_popup(surface) && !popup.is_initial_configure_sent() { - popup.send_configure().expect("initial popup configure is valid"); + popup + .send_configure() + .expect("initial popup configure is valid"); } } @@ -415,10 +438,11 @@ impl Ferese { let Ok(root) = find_popup_root_surface(&PopupKind::Xdg(popup.clone())) else { return; }; - let window = self - .space - .elements() - .find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root)); + let window = self.space.elements().find(|window| { + window + .toplevel() + .is_some_and(|toplevel| toplevel.wl_surface() == &root) + }); let geometry = (|| { if let Some(window) = window { let id = *self.windows.ids().get(window)?; @@ -432,11 +456,14 @@ impl Ferese { Some((output_geometry, location)) } else { - let layer = self.space.layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; + let layer = self + .space + .layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; self.space.outputs().find_map(|output| { let output_geometry = self.space.output_geometry(output)?; - let layer_geometry = smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?; + let layer_geometry = + smithay::desktop::layer_map_for_output(output).layer_geometry(&layer)?; Some((output_geometry, output_geometry.loc + layer_geometry.loc)) }) @@ -490,7 +517,9 @@ impl Dispatch for Ferese { return; } - smithay::wayland::Dispatch2::request(data, state, client, resource, request, dhandle, data_init); + smithay::wayland::Dispatch2::request( + data, state, client, resource, request, dhandle, data_init, + ); } fn destroyed( @@ -524,7 +553,11 @@ fn size_limits_usable( (pending.max_size.w, pending.max_size.h), ) }); - let (min, max) = if minimum { (proposed, max) } else { (min, proposed) }; + let (min, max) = if minimum { + (proposed, max) + } else { + (min, proposed) + }; (max.0 == 0 || min.0 <= max.0) && (max.1 == 0 || min.1 <= max.1) } diff --git a/compositor/src/idle_inhibition.rs b/compositor/src/idle_inhibition.rs index 7b97192..cd14221 100644 --- a/compositor/src/idle_inhibition.rs +++ b/compositor/src/idle_inhibition.rs @@ -77,16 +77,21 @@ fn player_window(player: &media::Player, windows: &[AppWindow<'_>]) -> Option) { + pub(crate) fn watch( + &mut self, + owner: u64, + id: u64, + since: u64, + response: mpsc::SyncSender, + ) { if self.locked { let _ = response.try_send(Response::error(id, "session_locked", "Session is locked")); } else if since != self.snapshot.revision { @@ -98,8 +105,15 @@ impl Engine { self.waiters.remove(&owner); } - pub(crate) fn action(&self, args: Value, reply: Option<(u64, mpsc::SyncSender)>) -> Result<(), String> { - let commands = self.commands.as_ref().ok_or("Media service is unavailable")?; + pub(crate) fn action( + &self, + args: Value, + reply: Option<(u64, mpsc::SyncSender)>, + ) -> Result<(), String> { + let commands = self + .commands + .as_ref() + .ok_or("Media service is unavailable")?; commands .try_send(Command { args, @@ -113,16 +127,18 @@ impl Engine { pub(crate) fn init(event_loop: &mut EventLoop) -> Result<(), Box> { let (sender, receiver) = sync_channel::(1); let (commands, receive_commands) = async_channel::bounded(16); - event_loop.handle().insert_source(receiver, move |event, _, state| { - if let Event::Msg(update) = event { - let changed = inhibition_changed(&state.media_players, &update.players); - state.media_players = update.players; - state.media_engine.update(update.snapshot); - if changed { - state.refresh_idle_inhibition(); + event_loop + .handle() + .insert_source(receiver, move |event, _, state| { + if let Event::Msg(update) = event { + let changed = inhibition_changed(&state.media_players, &update.players); + state.media_players = update.players; + state.media_engine.update(update.snapshot); + if changed { + state.refresh_idle_inhibition(); + } } - } - })?; + })?; event_loop .handle() .insert_idle(move |state| state.media_engine.commands = Some(commands)); diff --git a/compositor/src/idle_inhibition/media/bus.rs b/compositor/src/idle_inhibition/media/bus.rs index f00143c..cf3d015 100644 --- a/compositor/src/idle_inhibition/media/bus.rs +++ b/compositor/src/idle_inhibition/media/bus.rs @@ -38,7 +38,11 @@ async fn bus_call( .deserialize() } -async fn properties(connection: &Connection, owner: &str, interface: &str) -> zbus::Result { +async fn properties( + connection: &Connection, + owner: &str, + interface: &str, +) -> zbus::Result { connection .call_method(Some(owner), PATH, Some(PROPERTIES), "GetAll", &(interface,)) .await? @@ -47,7 +51,9 @@ async fn properties(connection: &Connection, owner: &str, interface: &str) -> zb } fn string<'a>(properties: &'a Properties, name: &str) -> Option<&'a str> { - properties.get(name).and_then(|value| <&str>::try_from(value).ok()) + properties + .get(name) + .and_then(|value| <&str>::try_from(value).ok()) } pub(super) fn clean(value: &str, length: usize) -> String { @@ -163,7 +169,11 @@ async fn discover(connection: &Connection, name: String, owner: String) -> Playe player } -async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_json::Value) -> Result<(), String> { +async fn action( + connection: &Connection, + state: &mut PlayersState, + args: &serde_json::Value, +) -> Result<(), String> { let kind = args["action"].as_str().ok_or("Missing media action")?; if kind == "auto" { state.pinned = None; @@ -176,7 +186,10 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_ } .cloned() .ok_or("No active media player")?; - if args["owner"].as_str().is_some_and(|owner| owner != player.owner) { + if args["owner"] + .as_str() + .is_some_and(|owner| owner != player.owner) + { return Err("Media player was replaced".into()); } @@ -227,12 +240,15 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_ if args["track_id"].as_str() != view.track_id.as_deref() { return Err("Media track changed".into()); } - let position = args["position_us"].as_u64().ok_or("Invalid media position")?; + let position = args["position_us"] + .as_u64() + .ok_or("Invalid media position")?; let position = view .length_us .map_or(position, |length| position.min(length)) .min(i64::MAX as u64) as i64; - let track = OwnedObjectPath::try_from(view.track_id.clone().unwrap()).map_err(|error| error.to_string())?; + let track = OwnedObjectPath::try_from(view.track_id.clone().unwrap()) + .map_err(|error| error.to_string())?; connection .call_method( Some(player.owner.as_str()), @@ -279,7 +295,11 @@ async fn action(connection: &Connection, state: &mut PlayersState, args: &serde_ Ok(()) } -fn publish(state: &PlayersState, previous: &mut Option, sender: &SyncSender) -> bool { +fn publish( + state: &PlayersState, + previous: &mut Option, + sender: &SyncSender, +) -> bool { let update = state.update_snapshot(); if previous .as_ref() @@ -336,7 +356,10 @@ async fn monitor( .deserialize()?; let mut names: Vec<_> = names .into_iter() - .filter(|name| name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.'))) + .filter(|name| { + name.strip_prefix(PREFIX) + .is_some_and(|suffix| suffix.starts_with('.')) + }) .collect(); names.sort_unstable(); for name in names.into_iter().take(MAX_PLAYERS) { @@ -367,7 +390,9 @@ async fn monitor( }; match input { Input::Action(None) => return Ok(()), - Input::Signal(None) => return Err(zbus::Error::Failure("MPRIS signal stream ended".into())), + Input::Signal(None) => { + return Err(zbus::Error::Failure("MPRIS signal stream ended".into())); + } Input::Action(Some(command)) => { let result = if command.sent.elapsed() > Duration::from_secs(2) { Err("Media command expired".into()) @@ -389,8 +414,12 @@ async fn monitor( .member() .is_some_and(|member| member.as_str() == "NameOwnerChanged") { - let (name, _, owner): (String, String, String) = message.body().deserialize()?; - if !name.strip_prefix(PREFIX).is_some_and(|suffix| suffix.starts_with('.')) { + let (name, _, owner): (String, String, String) = + message.body().deserialize()?; + if !name + .strip_prefix(PREFIX) + .is_some_and(|suffix| suffix.starts_with('.')) + { continue; } state.remove(&name); @@ -402,7 +431,10 @@ async fn monitor( state.update(player); } } else if let Some(owner) = header.sender() { - if header.member().is_some_and(|member| member.as_str() == "Seeked") { + if header + .member() + .is_some_and(|member| member.as_str() == "Seeked") + { let names: Vec<_> = state .players .values() @@ -417,14 +449,17 @@ async fn monitor( } } else { let Ok((interface, changed, invalidated)) = - message.body().deserialize::<(String, Properties, Vec)>() + message + .body() + .deserialize::<(String, Properties, Vec)>() else { continue; }; let identity = interface == PREFIX - && ["Identity", "DesktopEntry", "CanRaise"] - .iter() - .any(|key| changed.contains_key(*key) || invalidated.iter().any(|field| field == key)); + && ["Identity", "DesktopEntry", "CanRaise"].iter().any(|key| { + changed.contains_key(*key) + || invalidated.iter().any(|field| field == key) + }); let playback = interface == PLAYER && [ "PlaybackStatus", @@ -439,7 +474,10 @@ async fn monitor( "CanControl", ] .iter() - .any(|key| changed.contains_key(*key) || invalidated.iter().any(|field| field == key)); + .any(|key| { + changed.contains_key(*key) + || invalidated.iter().any(|field| field == key) + }); if !identity && !playback { continue; } @@ -483,10 +521,16 @@ pub(super) fn run(sender: SyncSender, commands: async_channel::Receiver< if matches!(result, Ok(())) || commands.is_closed() { break; } - if sender.send(PlayersState::default().update_snapshot()).is_err() { + if sender + .send(PlayersState::default().update_snapshot()) + .is_err() + { break; } - tracing::debug!(?result, "MPRIS disconnected; clearing playback and retrying"); + tracing::debug!( + ?result, + "MPRIS disconnected; clearing playback and retrying" + ); std::thread::sleep(Duration::from_secs(5)); } } diff --git a/compositor/src/idle_inhibition/media/policy.rs b/compositor/src/idle_inhibition/media/policy.rs index b2f35aa..23c2b70 100644 --- a/compositor/src/idle_inhibition/media/policy.rs +++ b/compositor/src/idle_inhibition/media/policy.rs @@ -47,8 +47,9 @@ impl PlayersState { } pub fn selected(&self) -> Option<&Player> { - let eligible = - |player: &&Player| player.view.status != Playback::Stopped && !self.ignored.contains(&player.name); + let eligible = |player: &&Player| { + player.view.status != Playback::Stopped && !self.ignored.contains(&player.name) + }; if let Some(player) = self .pinned .as_ref() diff --git a/compositor/src/idle_inhibition/media/tests.rs b/compositor/src/idle_inhibition/media/tests.rs index 7b2b60e..b2cc35c 100644 --- a/compositor/src/idle_inhibition/media/tests.rs +++ b/compositor/src/idle_inhibition/media/tests.rs @@ -55,7 +55,9 @@ fn pin_ignore_and_departure_do_not_leave_a_ghost_player() { fn initial_ties_are_deterministic_and_stopped_players_are_hidden() { let mut state = PlayersState::default(); for name in ["z", "a"] { - state.players.insert(name.into(), player(name, Playback::Playing)); + state + .players + .insert(name.into(), player(name, Playback::Playing)); } assert_eq!(state.selected().unwrap().name, "a"); state.update(player("a", Playback::Stopped)); @@ -132,7 +134,10 @@ fn session_lock_cancels_pending_and_rejects_new_media_watches() { for since in [0, engine.snapshot.revision] { let (send, receive) = mpsc::sync_channel(1); engine.watch(2, 43, since, send); - assert_eq!(receive.recv().unwrap().error.unwrap().code, "session_locked"); + assert_eq!( + receive.recv().unwrap().error.unwrap().code, + "session_locked" + ); } engine.set_locked(false); let (send, receive) = mpsc::sync_channel(1); diff --git a/compositor/src/input.rs b/compositor/src/input.rs index a49e93d..2299758 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -1,13 +1,14 @@ use ferese_layout::Direction; use smithay::backend::input::{ - AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, GestureEndEvent, - GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, KeyboardKeyEvent, PointerAxisEvent, - PointerButtonEvent, PointerMotionEvent, Switch, SwitchState, SwitchToggleEvent, TouchEvent, + AbsolutePositionEvent, Axis, AxisSource, ButtonState, Event, GestureBeginEvent, + GestureEndEvent, GestureSwipeUpdateEvent as _, InputBackend, InputEvent, KeyState, + KeyboardKeyEvent, PointerAxisEvent, PointerButtonEvent, PointerMotionEvent, Switch, + SwitchState, SwitchToggleEvent, TouchEvent, }; use smithay::input::keyboard::{FilterResult, keysyms}; use smithay::input::pointer::{ - AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, GestureSwipeUpdateEvent, - GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent, + AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, + GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle, RelativeMotionEvent, }; use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}; use smithay::reexports::wayland_server::Resource; @@ -63,7 +64,8 @@ impl Ferese { let layers = smithay::desktop::layer_map_for_output(output); for layer in layers.layers() { - let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) else { + let Some(local) = surface_tree_position(layer.wl_surface(), surface, (0, 0).into()) + else { continue; }; let geometry = layers.layer_geometry(layer)?; @@ -135,7 +137,9 @@ impl Ferese { if !matches!( &event, - InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. } + InputEvent::DeviceAdded { .. } + | InputEvent::DeviceRemoved { .. } + | InputEvent::SwitchToggle { .. } ) { self.lock_input_activity(); } @@ -221,32 +225,39 @@ impl Ferese { } BindingAction::Focus(focus @ (Direction::Left | Direction::Right)) => { self.swipe.mark_momentum_navigation(); - let raw = self.swipe.unbounded_preview().map_or(progress, |(_, value)| value); + let raw = self + .swipe + .unbounded_preview() + .map_or(progress, |(_, value)| value); self.preview_focus_swipe(focus, direction, raw); } _ => {} } } } else { - seat.get_pointer().expect("seat has a pointer").gesture_swipe_update( - self, - &GestureSwipeUpdateEvent { - time: event.time(), - delta: event.delta(), - }, - ); + seat.get_pointer() + .expect("seat has a pointer") + .gesture_swipe_update( + self, + &GestureSwipeUpdateEvent { + time: event.time(), + delta: event.delta(), + }, + ); } } InputEvent::GestureSwipeEnd { event } => { if self.swipe.active() { let fingers = self.swipe.fingers(); let cancelled = event.cancelled() || self.swipe_navigation_blocked(); - let direction = self - .swipe - .finish_at(cancelled, std::time::Duration::from_micros(event.time().micros())); + let direction = self.swipe.finish_at( + cancelled, + std::time::Duration::from_micros(event.time().micros()), + ); let workspace_handled = self.finish_workspace_swipe(direction); let focus_handled = self.finish_focus_swipe(direction); - let handled = workspace_handled || focus_handled || self.swipe.preview_started(); + let handled = + workspace_handled || focus_handled || self.swipe.preview_started(); if !handled && let Some(direction) = direction && let Some(action) = self @@ -256,7 +267,9 @@ impl Ferese { .map(|binding| binding.action.clone()) { match action { - BindingAction::Focus(direction @ (Direction::Left | Direction::Right)) => { + BindingAction::Focus( + direction @ (Direction::Left | Direction::Right), + ) => { self.focus_direction_from_swipe(direction); } BindingAction::SwitchRelativeWorkspace(next) => { @@ -266,14 +279,16 @@ impl Ferese { } } } else { - seat.get_pointer().expect("seat has a pointer").gesture_swipe_end( - self, - &GestureSwipeEndEvent { - serial: SERIAL_COUNTER.next_serial(), - time: event.time(), - cancelled: event.cancelled(), - }, - ); + seat.get_pointer() + .expect("seat has a pointer") + .gesture_swipe_end( + self, + &GestureSwipeEndEvent { + serial: SERIAL_COUNTER.next_serial(), + time: event.time(), + cancelled: event.cancelled(), + }, + ); } } InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => { @@ -544,7 +559,8 @@ impl Ferese { focus, &RelativeMotionEvent { delta: (delta.x / scale_x, delta.y / scale_y).into(), - delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y).into(), + delta_unaccel: (delta_unaccel.x / scale_x, delta_unaccel.y / scale_y) + .into(), time: event.time(), }, ); @@ -574,7 +590,8 @@ impl Ferese { } return; } - if event.state() == ButtonState::Released && self.consumed_pointer_buttons.remove(&event.button_code()) + if event.state() == ButtonState::Released + && self.consumed_pointer_buttons.remove(&event.button_code()) { return; } @@ -733,12 +750,12 @@ impl Ferese { InputEvent::PointerAxis { event, .. } => { self.last_pointer_time = event.time(); let source = event.source(); - let horizontal = event - .amount(Axis::Horizontal) - .unwrap_or_else(|| event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0); - let vertical = event - .amount(Axis::Vertical) - .unwrap_or_else(|| event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0); + let horizontal = event.amount(Axis::Horizontal).unwrap_or_else(|| { + event.amount_v120(Axis::Horizontal).unwrap_or(0.0) * 15.0 / 120.0 + }); + let vertical = event.amount(Axis::Vertical).unwrap_or_else(|| { + event.amount_v120(Axis::Vertical).unwrap_or(0.0) * 15.0 / 120.0 + }); if self.input_capture.active() { if self.input_capture.captures(2) { self.input_capture.push(serde_json::json!({"type":"scroll", "x":horizontal, "y":vertical, "v120_x":event.amount_v120(Axis::Horizontal), "v120_y":event.amount_v120(Axis::Vertical), "stop_x":source == AxisSource::Finger && event.amount(Axis::Horizontal) == Some(0.0), "stop_y":source == AxisSource::Finger && event.amount(Axis::Vertical) == Some(0.0)})); @@ -748,13 +765,21 @@ impl Ferese { let pointer = self.seat.get_pointer().expect("seat has a pointer"); if self.scroll_tab_indicator( pointer.current_location(), - if vertical != 0.0 { vertical } else { horizontal }, + if vertical != 0.0 { + vertical + } else { + horizontal + }, ) { return; } if self.scroll_overview_strip( pointer.current_location(), - if horizontal != 0.0 { horizontal } else { vertical }, + if horizontal != 0.0 { + horizontal + } else { + vertical + }, ) { return; } @@ -791,7 +816,8 @@ impl Ferese { let Some(location) = self.absolute_event_position(&event) else { return; }; - if !self.consumed_touch_slots.is_empty() || self.dismiss_launcher_outside(location) { + if !self.consumed_touch_slots.is_empty() || self.dismiss_launcher_outside(location) + { self.consumed_touch_slots.insert(event.slot()); return; } @@ -800,7 +826,10 @@ impl Ferese { let serial = SERIAL_COUNTER.next_serial(); if !touch.is_grabbed() { - self.record_activation_input(serial, self.surface_under(location).map(|(surface, _)| surface)); + self.record_activation_input( + serial, + self.surface_under(location).map(|(surface, _)| surface), + ); } touch.down( @@ -865,7 +894,11 @@ impl Ferese { if self.session_lock.active() || self.input_capture.active() { return false; } - let Some(surface) = self.seat.get_keyboard().and_then(|keyboard| keyboard.current_focus()) else { + let Some(surface) = self + .seat + .get_keyboard() + .and_then(|keyboard| keyboard.current_focus()) + else { return false; }; if self @@ -890,7 +923,12 @@ impl Ferese { true } - fn start_floating_pointer_grab(&mut self, pointer: &PointerHandle, button: u32, serial: Serial) -> bool { + fn start_floating_pointer_grab( + &mut self, + pointer: &PointerHandle, + button: u32, + serial: Serial, + ) -> bool { if self.session_lock.active() || pointer.is_grabbed() || !matches!(button, BTN_LEFT | BTN_RIGHT) @@ -956,7 +994,9 @@ impl Ferese { I: InputBackend, E: AbsolutePositionEvent, { - let output = self.focused_output().or_else(|| self.space.outputs().next())?; + let output = self + .focused_output() + .or_else(|| self.space.outputs().next())?; let geometry = self.space.output_geometry(output)?; Some(event.position_transformed(geometry.size) + geometry.loc.to_f64()) @@ -965,8 +1005,14 @@ impl Ferese { fn swipe_navigation_blocked(&self) -> bool { self.session_lock.active() || self.active_shortcuts_inhibitor.is_some() - || self.seat.get_pointer().is_some_and(|pointer| pointer.is_grabbed()) - || self.seat.get_keyboard().is_some_and(|keyboard| keyboard.is_grabbed()) + || self + .seat + .get_pointer() + .is_some_and(|pointer| pointer.is_grabbed()) + || self + .seat + .get_keyboard() + .is_some_and(|keyboard| keyboard.is_grabbed()) } fn switch_relative_workspace(&mut self, next: bool, slide: Option) { @@ -982,7 +1028,9 @@ impl Ferese { pub(crate) fn relative_workspace_target(&self, next: bool) -> Option { let output = self.output_workspaces.focused_output()?; let current = self.output_workspaces.active_workspace(output)?; - let candidates = self.output_workspaces.ordered_workspaces(&self.workspaces, output); + let candidates = self + .output_workspaces + .ordered_workspaces(&self.workspaces, output); let index = candidates.iter().position(|id| *id == current)?; let neighbor = if next { index.checked_add(1)? @@ -1009,7 +1057,9 @@ impl Ferese { if self.session_lock.active() { // Repeats retain press ownership and never run configurable commands // on the lock screen, including keys held before locking. - if let Some(step) = raw_symbols.iter().find_map(|symbol| locked_brightness_step(*symbol)) + if let Some(step) = raw_symbols + .iter() + .find_map(|symbol| locked_brightness_step(*symbol)) && let Some(child) = spawn_client( self, ["brightnessctl", "set", step], @@ -1087,7 +1137,8 @@ impl Ferese { { // The client may enable OnDemand only after receiving this // opening press. Touch does not establish pointer focus. - self.pending_layer_focus = Some((layer.wl_surface().clone(), std::time::Instant::now())); + self.pending_layer_focus = + Some((layer.wl_surface().clone(), std::time::Instant::now())); } return; } @@ -1183,7 +1234,11 @@ impl Ferese { } } - fn focus_window_under_pointer(&mut self, pointer: &PointerHandle, position: Point) { + fn focus_window_under_pointer( + &mut self, + pointer: &PointerHandle, + position: Point, + ) { if self.session_lock.active() { return; } @@ -1193,7 +1248,10 @@ impl Ferese { } // Keep the explicitly selected window focused until Overview's exit // animation settles. Moving previews must not steal focus on pointer jitter. - if self.overview.is_presenting() || !self.input_settings.focus_follows_mouse || pointer.is_grabbed() { + if self.overview.is_presenting() + || !self.input_settings.focus_follows_mouse + || pointer.is_grabbed() + { return; } if self.layer_under(position).is_some() { @@ -1323,9 +1381,11 @@ impl Ferese { BindingAction::None => {} BindingAction::BindingMode(name) => self.set_binding_mode(&name), BindingAction::Spawn(argv) => { - if let Some(child) = - spawn_client(self, argv.iter(), crate::private_client::ClientCapabilities::default()) - { + if let Some(child) = spawn_client( + self, + argv.iter(), + crate::private_client::ClientCapabilities::default(), + ) { crate::process::watch_client_exit(&self.loop_handle, child); } } @@ -1337,7 +1397,9 @@ impl Ferese { BindingAction::FocusMru(reverse) => self.cycle_focus(reverse, false), BindingAction::Move(direction) => self.move_direction(direction), BindingAction::Resize(direction) => self.resize_direction(direction), - BindingAction::SwitchRelativeWorkspace(next) => self.switch_relative_workspace(next, None), + BindingAction::SwitchRelativeWorkspace(next) => { + self.switch_relative_workspace(next, None) + } BindingAction::WorkspaceBackAndForth => self.workspace_back_and_forth(), BindingAction::SwitchWorkspace(workspace) => { self.switch_workspace_from_binding(u32::from(workspace)); @@ -1359,7 +1421,10 @@ impl Ferese { BindingAction::ToggleFloating => self.toggle_focused_floating(), BindingAction::ToggleOverview => self.toggle_overview(), BindingAction::Media(action) => { - if let Err(error) = self.media_engine.action(serde_json::json!({"action": action}), None) { + if let Err(error) = self + .media_engine + .action(serde_json::json!({"action": action}), None) + { tracing::debug!(%error, "media action unavailable"); } } @@ -1474,17 +1539,25 @@ mod tests { #[test] fn binding_commands_are_reaped_while_the_compositor_runs() { - if !crate::startup_tests::private_runtime("input::tests::binding_commands_are_reaped_while_the_compositor_runs") - { + if !crate::startup_tests::private_runtime( + "input::tests::binding_commands_are_reaped_while_the_compositor_runs", + ) { return; } let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap(); let mut state = crate::startup_tests::state(&mut events); let children_path = format!("/proc/self/task/{}/children", unsafe { libc::gettid() }); - assert!(std::fs::read_to_string(&children_path).unwrap().trim().is_empty()); + assert!( + std::fs::read_to_string(&children_path) + .unwrap() + .trim() + .is_empty() + ); for _ in 0..24 { - state.execute_binding(crate::config::BindingAction::Spawn(vec!["/bin/true".into()].into())); + state.execute_binding(crate::config::BindingAction::Spawn( + vec!["/bin/true".into()].into(), + )); } let children: Vec = std::fs::read_to_string(&children_path) @@ -1494,8 +1567,15 @@ mod tests { .collect(); assert_eq!(children.len(), 24); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); - while !std::fs::read_to_string(&children_path).unwrap().trim().is_empty() { - assert!(std::time::Instant::now() < deadline, "binding children were not reaped"); + while !std::fs::read_to_string(&children_path) + .unwrap() + .trim() + .is_empty() + { + assert!( + std::time::Instant::now() < deadline, + "binding children were not reaped" + ); events .dispatch(Some(std::time::Duration::from_millis(20)), &mut state) .unwrap(); @@ -1504,8 +1584,14 @@ mod tests { for pid in children { let mut status = 0; - assert_eq!(unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }, -1); - assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD)); + assert_eq!( + unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) }, + -1 + ); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::ECHILD) + ); } } diff --git a/compositor/src/input/binding_mode_tests.rs b/compositor/src/input/binding_mode_tests.rs index 62948ef..5ff0de6 100644 --- a/compositor/src/input/binding_mode_tests.rs +++ b/compositor/src/input/binding_mode_tests.rs @@ -60,7 +60,11 @@ pub(crate) fn key(state: &mut Ferese, code: u32, pressed: bool) { state.process_input_event::(InputEvent::Keyboard { event: TestKey( Keycode::new(code), - if pressed { KeyState::Pressed } else { KeyState::Released }, + if pressed { + KeyState::Pressed + } else { + KeyState::Released + }, ), }); } @@ -78,7 +82,10 @@ fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese { config.wallpaper.path = None; let mut state = Ferese::new(events, Display::new().unwrap(), config).unwrap(); state.config_source = Some(SOURCE.into()); - let mut sections = ferese_config::Document::parse(SOURCE).unwrap().value().clone(); + let mut sections = ferese_config::Document::parse(SOURCE) + .unwrap() + .value() + .clone(); sections.as_object_mut().unwrap().remove("theme"); state.config_sections = Some(sections); state @@ -149,7 +156,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() { let mut state = state(&mut events); state.set_binding_mode("resize"); key(&mut state, 43, true); - assert!(state.reload_config_source("binding-mode default {}".into()).is_err()); + assert!( + state + .reload_config_source("binding-mode default {}".into()) + .is_err() + ); assert_eq!(state.binding_modes.active(), Some("resize")); assert!(state.intercepted_keys.contains(&Keycode::new(43))); state @@ -163,7 +174,11 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() { state .reload_config_source(format!("{SOURCE}\ninput {{ repeat-rate 30; }}")) .unwrap(); - assert_eq!(state.binding_modes.active(), None, "input reload exits mode"); + assert_eq!( + state.binding_modes.active(), + None, + "input reload exits mode" + ); state.set_binding_mode("resize"); state .reload_config_source("binding-mode launch { binding t spawn terminal; }".into()) @@ -176,8 +191,9 @@ fn reload_transitions_cancel_modes_without_losing_key_ownership() { #[test] fn escape_exits_with_every_modifier_combination() { - if !crate::startup_tests::private_runtime("input::binding_mode_tests::escape_exits_with_every_modifier_combination") - { + if !crate::startup_tests::private_runtime( + "input::binding_mode_tests::escape_exits_with_every_modifier_combination", + ) { return; } let mut events = EventLoop::try_new().unwrap(); @@ -220,7 +236,9 @@ fn input_capture_exits_modes_and_prevents_reentry() { y: 0, }]); let id = state.input_capture.register(1, 3).unwrap(); - let zone_set = state.input_capture.zones(id).unwrap()["zone_set"].as_u64().unwrap() as u32; + let zone_set = state.input_capture.zones(id).unwrap()["zone_set"] + .as_u64() + .unwrap() as u32; state .input_capture .set_barriers( diff --git a/compositor/src/input/layer_focus_tests.rs b/compositor/src/input/layer_focus_tests.rs index 7cb896e..26a9489 100644 --- a/compositor/src/input/layer_focus_tests.rs +++ b/compositor/src/input/layer_focus_tests.rs @@ -1,6 +1,8 @@ use super::*; use crate::state::ClientState; -use crate::state::{dispatch_test_client as dispatch, test_client_request as request, test_window as window}; +use crate::state::{ + dispatch_test_client as dispatch, test_client_request as request, test_window as window, +}; use smithay::desktop::{LayerSurface, layer_map_for_output}; use smithay::output::{Mode, Output, PhysicalProperties, Subpixel}; use smithay::reexports::calloop::EventLoop; @@ -11,7 +13,11 @@ use std::os::fd::AsRawFd; use std::os::unix::net::UnixStream; use std::sync::Arc; -fn layer(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, output: &Output) -> (LayerSurface, UnixStream) { +fn layer( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + output: &Output, +) -> (LayerSurface, UnixStream) { layer_with_policy(state, events, output, 0, 2) } @@ -81,7 +87,9 @@ fn layer_with_policy( let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); if object == 3 && header & 0xffff == 0 { - serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); + serial = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); } offset += (header >> 16) as usize; } @@ -93,7 +101,8 @@ fn layer_with_policy( "acknowledging configure without mapping must not acquire focus" ); let mut file = tempfile::tempfile().unwrap(); - file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48)).unwrap(); + file.write_all(&0xff112233u32.to_ne_bytes().repeat(64 * 48)) + .unwrap(); request( &mut wire, shm.id().protocol_id(), @@ -198,7 +207,11 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() { }, ); state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); - assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel"); + assert_eq!( + keyboard.current_focus(), + None, + "empty-desktop clicks dismiss the panel" + ); request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); assert_eq!( @@ -225,7 +238,9 @@ fn on_demand_layer_focus_follows_opening_and_outside_clicks() { let (second, _second_wire) = window(&mut state, &mut events, 0xff334455); let first_id = state.windows.ids()[&first]; assert!(state.activate_managed_window(first_id)); - let second_bounds = state.presented_window_rect(state.windows.ids()[&second]).unwrap(); + let second_bounds = state + .presented_window_rect(state.windows.ids()[&second]) + .unwrap(); let on_window = Point::from((second_bounds.x + 20., second_bounds.y + 20.)); state.input_settings.focus_follows_mouse = true; keyboard.set_focus( @@ -297,7 +312,12 @@ fn fixture_with_animations(animations: bool) -> (Ferese, EventLoop<'static, Fere (state, events, output) } -fn commit_policy(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, wire: &mut UnixStream, keyboard: u32) { +fn commit_policy( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + wire: &mut UnixStream, + keyboard: u32, +) { request(wire, 3, 4, &[keyboard], None); request(wire, 2, 6, &[], None); dispatch(events, state); @@ -345,17 +365,24 @@ fn null_buffer_commit_revokes_layer_focus_without_destroying_role() { request(&mut wire, 2, 1, &[0, 0, 0], None); // wl_surface.attach(NULL). request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); - assert!(panel.layer_surface().alive(), "the layer role survives unmapping"); + assert!( + panel.layer_surface().alive(), + "the layer role survives unmapping" + ); assert!( state .space - .layer_for_surface(panel.wl_surface(), smithay::desktop::WindowSurfaceType::TOPLEVEL) + .layer_for_surface( + panel.wl_surface(), + smithay::desktop::WindowSurfaceType::TOPLEVEL + ) .is_some() ); assert!( - smithay::backend::renderer::utils::with_renderer_surface_state(panel.wl_surface(), |s| s - .buffer() - .is_none()) + smithay::backend::renderer::utils::with_renderer_surface_state( + panel.wl_surface(), + |s| s.buffer().is_none() + ) .unwrap() ); assert_eq!( @@ -392,7 +419,11 @@ fn null_buffer_commit_revokes_layer_focus_without_destroying_role() { ); request(&mut wire, 3, 7, &[], None); // Destroy the role. dispatch(&mut events, &mut state); - assert_eq!(keyboard.current_focus(), fallback, "destruction restores window focus"); + assert_eq!( + keyboard.current_focus(), + fallback, + "destruction restores window focus" + ); } } @@ -410,7 +441,9 @@ fn ack_latest_configure(wire: &mut UnixStream) { let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); if object == 3 && header & 0xffff == 0 { - serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); + serial = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); } offset += (header >> 16) as usize; } @@ -495,7 +528,11 @@ fn layer_mapping_commits_do_not_override_session_lock() { request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); remap_layer(&mut state, &mut events, &mut wire, 1, 3); - assert_eq!(keyboard.current_focus(), None, "layer remapping cannot bypass the lock"); + assert_eq!( + keyboard.current_focus(), + None, + "layer remapping cannot bypass the lock" + ); request(&mut wire, 3, 7, &[], None); dispatch(&mut events, &mut state); assert_eq!( @@ -631,7 +668,11 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() { ) .unwrap(); let shell = client - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); request(&mut wire, shell.id().protocol_id(), 1, &[6], None); // Positioner. request(&mut wire, 6, 1, &[32, 24], None); @@ -657,7 +698,12 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() { let seat = state.seat.clone(); let grab = state .popups - .grab_popup(top.wl_surface().clone(), PopupKind::Xdg(popup.clone()), &seat, serial) + .grab_popup( + top.wl_surface().clone(), + PopupKind::Xdg(popup.clone()), + &seat, + serial, + ) .unwrap(); let keyboard = state.seat.get_keyboard().unwrap(); keyboard.set_grab(&mut state, PopupKeyboardGrab::new(&grab), serial); @@ -680,7 +726,10 @@ fn valid_layer_popup_grab_survives_commits_but_not_parent_unmapping() { request(&mut wire, 2, 1, &[0, 0, 0], None); request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); - assert!(!keyboard.is_grabbed(), "unmapping revokes the parent popup grab"); + assert!( + !keyboard.is_grabbed(), + "unmapping revokes the parent popup grab" + ); assert_ne!(keyboard.current_focus(), popup_focus); assert_ne!(keyboard.current_focus(), Some(top.wl_surface().clone())); } @@ -699,8 +748,16 @@ fn fullscreen_layer_eligibility_is_per_output_and_respects_overview() { state.set_window_fullscreen(id, true); let (top, _top_wire) = layer_with_policy(&mut state, &mut events, &output, 1, 2); assert!(!state.layer_keyboard_eligible(&top)); - assert!(!state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, false)); - assert!(state.layer_visible_for_frame(&output, smithay::wayland::shell::wlr_layer::Layer::Top, true)); + assert!(!state.layer_visible_for_frame( + &output, + smithay::wayland::shell::wlr_layer::Layer::Top, + false + )); + assert!(state.layer_visible_for_frame( + &output, + smithay::wayland::shell::wlr_layer::Layer::Top, + true + )); state.set_overview_active(true); assert!(state.layer_keyboard_eligible(&top)); assert_eq!( @@ -837,7 +894,11 @@ fn touch_opened_on_demand_layer_does_not_need_pointer_focus() { }, ); state.focus_window_at(outside, SERIAL_COUNTER.next_serial(), true); - assert_eq!(keyboard.current_focus(), None, "empty-desktop clicks dismiss the panel"); + assert_eq!( + keyboard.current_focus(), + None, + "empty-desktop clicks dismiss the panel" + ); request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); assert_eq!( diff --git a/compositor/src/input_capture.rs b/compositor/src/input_capture.rs index b6292d5..ba61564 100644 --- a/compositor/src/input_capture.rs +++ b/compositor/src/input_capture.rs @@ -89,14 +89,24 @@ fn edge(barrier: &Barrier, zones: &[Zone]) -> Option { } match found { Edge::Top | Edge::Bottom => { - let outside = y1 + if matches!(found, Edge::Top) { -0.5 } else { 0.5 }; + let outside = y1 + + if matches!(found, Edge::Top) { + -0.5 + } else { + 0.5 + }; outside >= f64::from(other.y) && outside < other.bottom() && x1 < other.right() && x2 + 1.0 > f64::from(other.x) } Edge::Left | Edge::Right => { - let outside = x1 + if matches!(found, Edge::Left) { -0.5 } else { 0.5 }; + let outside = x1 + + if matches!(found, Edge::Left) { + -0.5 + } else { + 0.5 + }; outside >= f64::from(other.x) && outside < other.right() && y1 < other.bottom() @@ -174,7 +184,10 @@ impl InputCapture { if self.sessions.len() >= MAX_SESSIONS || capabilities == 0 || capabilities & !3 != 0 { return Err("Unsupported capabilities or input capture session limit reached".into()); } - self.next_session = self.next_session.checked_add(1).ok_or("Input capture IDs exhausted")?; + self.next_session = self + .next_session + .checked_add(1) + .ok_or("Input capture IDs exhausted")?; let id = self.next_session; self.sessions.insert( id, @@ -193,15 +206,24 @@ impl InputCapture { pub(crate) fn zones(&self, id: u64) -> Result { self.session(id)?; - Ok(json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone| - (zone.width, zone.height, zone.x, zone.y)).collect::>()})) + Ok( + json!({"zone_set":self.zone_set, "zones":self.zones.iter().map(|zone| + (zone.width, zone.height, zone.x, zone.y)).collect::>()}), + ) } fn session(&self, id: u64) -> Result<&Session, String> { - self.sessions.get(&id).ok_or("Unknown input capture session".into()) + self.sessions + .get(&id) + .ok_or("Unknown input capture session".into()) } - pub(crate) fn set_barriers(&mut self, id: u64, zone_set: u32, barriers: Vec) -> Result, String> { + pub(crate) fn set_barriers( + &mut self, + id: u64, + zone_set: u32, + barriers: Vec, + ) -> Result, String> { self.session(id)?; if barriers.len() > MAX_BARRIERS { return Err("Too many pointer barriers".into()); @@ -229,7 +251,10 @@ impl InputCapture { } pub(crate) fn enable(&mut self, id: u64) -> Result<(), String> { - let session = self.sessions.get_mut(&id).ok_or("Unknown input capture session")?; + let session = self + .sessions + .get_mut(&id) + .ok_or("Unknown input capture session")?; if session.closed { return Err("Input capture session is closed".into()); } @@ -255,7 +280,11 @@ impl InputCapture { if self.active() { return true; } - if !allowed || ![from.0, from.1, to.0, to.1].iter().all(|value| value.is_finite()) { + if !allowed + || ![from.0, from.1, to.0, to.1] + .iter() + .all(|value| value.is_finite()) + { return false; } let hit = self @@ -340,7 +369,10 @@ impl InputCapture { self.restore_focus = true; let session = self.sessions.get_mut(&id).unwrap(); let activation = session.activation.take(); - self.enqueue(id, json!({"type":"deactivated", "activation_id":activation})); + self.enqueue( + id, + json!({"type":"deactivated", "activation_id":activation}), + ); self.flush(); } Ok(()) @@ -365,10 +397,18 @@ impl InputCapture { } pub(crate) fn valid_position(&self, point: (f64, f64)) -> bool { - point.0.is_finite() && point.1.is_finite() && self.zones.iter().any(|zone| zone.contains(point)) + point.0.is_finite() + && point.1.is_finite() + && self.zones.iter().any(|zone| zone.contains(point)) } - pub(crate) fn watch(&mut self, owner: u64, request: u64, id: u64, response: SyncSender) { + pub(crate) fn watch( + &mut self, + owner: u64, + request: u64, + id: u64, + response: SyncSender, + ) { if !self.sessions.contains_key(&id) || self.waiters.len() >= MAX_SESSIONS || self.waiters.values().any(|(_, token, _)| *token == id) @@ -479,10 +519,19 @@ impl crate::Ferese { && !self.overview.is_presenting() && !pointer.is_grabbed() && !self.swipe.active() - && self.seat.get_keyboard().is_none_or(|keyboard| !keyboard.is_grabbed()) - && self.seat.get_touch().is_none_or(|touch| !touch.is_grabbed()) + && self + .seat + .get_keyboard() + .is_none_or(|keyboard| !keyboard.is_grabbed()) + && self + .seat + .get_touch() + .is_none_or(|touch| !touch.is_grabbed()) && !self.seat.keyboard_shortcuts_inhibited(); - if !self.input_capture.motion((from.x, from.y), (to.x, to.y), allowed) { + if !self + .input_capture + .motion((from.x, from.y), (to.x, to.y), allowed) + { return false; } let delta = if absolute { @@ -553,7 +602,10 @@ mod tests { [3840, 0, 3840, 1079], [0, 0, 0, 1079], ] { - assert!(edge(&Barrier { id: 1, position }, &zones).is_some(), "{position:?}"); + assert!( + edge(&Barrier { id: 1, position }, &zones).is_some(), + "{position:?}" + ); } for position in [ [1920, 0, 1920, 1079], @@ -562,7 +614,10 @@ mod tests { [50, 50, 50, 200], [0, 5, 0, 4], ] { - assert!(edge(&Barrier { id: 1, position }, &zones).is_none(), "{position:?}"); + assert!( + edge(&Barrier { id: 1, position }, &zones).is_none(), + "{position:?}" + ); } let barrier = Barrier { id: 1, @@ -651,7 +706,11 @@ mod tests { capture.enable(id).unwrap(); assert!(capture.motion((3839.0, 50.0), (3900.0, 50.0), true)); let activation = capture.sessions[&id].activation.unwrap(); - assert!(capture.release(id, Some(activation.wrapping_add(1))).is_err()); + assert!( + capture + .release(id, Some(activation.wrapping_add(1))) + .is_err() + ); capture.release(id, Some(activation)).unwrap(); assert!(capture.sessions[&id].enabled); capture.update_zones(vec![zones()[0]]); diff --git a/compositor/src/ipc.rs b/compositor/src/ipc.rs index d5c8e1e..d98ff63 100644 --- a/compositor/src/ipc.rs +++ b/compositor/src/ipc.rs @@ -18,12 +18,16 @@ mod transport; use ferese_layout::Direction; use serde_json::{Value, json}; use smithay::output::Output; -use smithay::reexports::calloop::{EventLoop, LoopHandle, LoopSignal, RegistrationToken, channel, timer}; +use smithay::reexports::calloop::{ + EventLoop, LoopHandle, LoopSignal, RegistrationToken, channel, timer, +}; use smithay::utils::Transform; use crate::Ferese; use crate::config::OutputTransform; -use crate::handlers::screenshot::{Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan}; +use crate::handlers::screenshot::{ + Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan, +}; use crate::handlers::screenshot_worker::{Encoded, Job, Worker}; use crate::handlers::{screencopy, screenshot_worker}; @@ -107,7 +111,10 @@ impl Endpoint { fn desktop_at(path: PathBuf) -> io::Result { prepare_parent(&path)?; - Ok(Self { path, instance: None }) + Ok(Self { + path, + instance: None, + }) } pub(crate) fn instance() -> io::Result { @@ -115,7 +122,9 @@ impl Endpoint { } fn instance_at(default: PathBuf) -> io::Result { - let root = default.parent().expect("runtime control socket has a parent"); + let root = default + .parent() + .expect("runtime control socket has a parent"); prepare_directory(root)?; let instances = root.join("instances"); prepare_directory(&instances)?; @@ -133,7 +142,9 @@ impl Endpoint { impl Drop for IpcSocketGuard { fn drop(&mut self) { if self.path.symlink_metadata().is_ok_and(|metadata| { - metadata.file_type().is_socket() && metadata.dev() == self.device && metadata.ino() == self.inode + metadata.file_type().is_socket() + && metadata.dev() == self.device + && metadata.ino() == self.inode }) { let _ = fs::remove_file(&self.path); } @@ -158,58 +169,13 @@ pub(crate) fn init( let (sender, receiver): (channel::SyncSender, channel::Channel) = channel::sync_channel(REQUEST_QUEUE_CAPACITY); - event_loop.handle().insert_source(receiver, |event, _, state| { - if let channel::Event::Msg(event) = event { - let call = match event { - IpcEvent::Call(call) => *call, - IpcEvent::ActivityDetected(owner, matched, fd) => { - let output = state - .outputs_json() - .as_array() - .and_then(|outputs| { - outputs - .iter() - .find(|output| output["focused"] == true) - .or_else(|| outputs.first()) - }) - .and_then(|output| output["name"].as_str()) - .map(str::to_owned); - state.activities.detect_process(owner, *matched, output, &fd); - return; - } - IpcEvent::ActivityOwnerExited(owner) => { - state.activities.owner_exit(owner); - return; - } - IpcEvent::Closed(owner) => { - state.activities.remove_connection(owner); - state.ipc_events.remove(owner); - for request in state.screenshot.terminate_owner(owner) { - state - .pending_screencopies - .retain(|capture| capture.request_id() != Some(request)); - } - let captured = state.input_capture.active(); - state.input_capture.remove_owner(owner); - if captured && !state.input_capture.active() { - state.restore_input_capture_focus(); - } - state.portal_shortcuts.remove(owner); - state.portal_session.remove(owner); - state.theme_engine.remove(owner); - state.media_engine.remove(owner); - state.refresh_idle_inhibition(); - return; - } - }; - if call.request.command == "activity-publish" { - let result = validate_request(&call.request) - .map_err(|error| error.message) - .and_then(|()| { - if state.session_lock.active() { - return Err("Session locked".into()); - } - let (owner, publish, matched) = call.activity.ok_or("Unverified activity owner")?; + event_loop + .handle() + .insert_source(receiver, |event, _, state| { + if let channel::Event::Msg(event) = event { + let call = match event { + IpcEvent::Call(call) => *call, + IpcEvent::ActivityDetected(owner, matched, fd) => { let output = state .outputs_json() .as_array() @@ -221,225 +187,313 @@ pub(crate) fn init( }) .and_then(|output| output["name"].as_str()) .map(str::to_owned); - let window = state.activity_window(&owner); state .activities - .publish_verified(owner, publish, matched, output, window) - }); - state.activities.schedule(); - let response = match result { - Ok(result) => Response::success(call.request.id, result), - Err(error) => Response::error(call.request.id, "activity_rejected", error), - }; - let _ = call.response.try_send(response); - } else if call.request.command == "pomodoro" { - let result = validate_request(&call.request) - .map_err(|error| error.message) - .and_then(|()| { - if state.session_lock.active() { - return Err("Session locked".into()); + .detect_process(owner, *matched, output, &fd); + return; + } + IpcEvent::ActivityOwnerExited(owner) => { + state.activities.owner_exit(owner); + return; + } + IpcEvent::Closed(owner) => { + state.activities.remove_connection(owner); + state.ipc_events.remove(owner); + for request in state.screenshot.terminate_owner(owner) { + state + .pending_screencopies + .retain(|capture| capture.request_id() != Some(request)); } - let request: ferese_ipc::activities::PomodoroRequest = - serde_json::from_value(call.request.args).map_err(|error| error.to_string())?; - let output = state - .outputs_json() - .as_array() - .and_then(|outputs| { - outputs - .iter() - .find(|output| output["focused"] == true) - .or_else(|| outputs.first()) - }) - .and_then(|output| output["name"].as_str()) - .map(str::to_owned); - state.activities.pomodoro_control(request, output) - }); - let response = match result { - Ok(value) => Response::success(call.request.id, value), - Err(error) => Response::error(call.request.id, "pomodoro_rejected", error), - }; - let _ = call.response.try_send(response); - } else if call.request.command == "activity-get" { - if state.session_lock.active() { - let _ = call.response.try_send(Response::error( - call.request.id, - "session_locked", - "IPC unavailable while session is locked", - )); - } else if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else { - state.activities.get(call.request.id, call.response); - } - } else if call.request.command == "activity-watch" { - if state.session_lock.active() { - let _ = call.response.try_send(Response::error( - call.request.id, - "session_locked", - "IPC unavailable while session is locked", - )); - } else if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else if let Some(since) = call.request.args["since"].as_u64() { - state - .activities - .watch(call.owner, call.request.id, since, call.response); - } else { - let _ = call.response.try_send(Response::error( - call.request.id, - "invalid_argument", - "Missing activity revision", - )); - } - } else if call.request.command == "event-stream" { - let result = validate_request(&call.request).and_then(|()| { - if call.request.args["version"].as_u64() != Some(u64::from(ferese_ipc::events::VERSION)) { - return Err(CommandError::new( - "unsupported_event_version", - "Unsupported event schema version", - )); + let captured = state.input_capture.active(); + state.input_capture.remove_owner(owner); + if captured && !state.input_capture.active() { + state.restore_input_capture_focus(); + } + state.portal_shortcuts.remove(owner); + state.portal_session.remove(owner); + state.theme_engine.remove(owner); + state.media_engine.remove(owner); + state.refresh_idle_inhibition(); + return; } - state - .ipc_events - .subscribe(call.owner, call.events.expect("event stream sender")) - .map_err(|error| CommandError::new("subscriber_limit", error)) - }); - let response = match result { - Ok(()) => Response::success(call.request.id, json!({"version": ferese_ipc::events::VERSION})), - Err(error) => Response::error(call.request.id, error.code, error.message), }; - let _ = call.response.try_send(response); - } else if call.request.command.starts_with("input-capture-") && !call.native_portal { - let _ = call.response.try_send(Response::error( - call.request.id, - "access_denied", - "Input capture is restricted to the native portal", - )); - } else if call.request.command == "input-capture-watch" { - if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else if let Some(id) = call.request.args["session"].as_u64() { - state - .input_capture - .watch(call.owner, call.request.id, id, call.response); - } else { - let _ = call.response.try_send(Response::error( - call.request.id, - "invalid_argument", - "Missing capture session", - )); - } - } else if matches!(call.request.command.as_str(), "media-watch" | "media-action") { - if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else if state.session_lock.active() { - let _ = call.response.try_send(Response::error( - call.request.id, - "session_locked", - "IPC unavailable while session is locked", - )); - } else if call.request.command == "media-watch" { - if let Some(since) = call.request.args["since"].as_u64() { + if call.request.command == "activity-publish" { + let result = validate_request(&call.request) + .map_err(|error| error.message) + .and_then(|()| { + if state.session_lock.active() { + return Err("Session locked".into()); + } + let (owner, publish, matched) = + call.activity.ok_or("Unverified activity owner")?; + let output = state + .outputs_json() + .as_array() + .and_then(|outputs| { + outputs + .iter() + .find(|output| output["focused"] == true) + .or_else(|| outputs.first()) + }) + .and_then(|output| output["name"].as_str()) + .map(str::to_owned); + let window = state.activity_window(&owner); + state + .activities + .publish_verified(owner, publish, matched, output, window) + }); + state.activities.schedule(); + let response = match result { + Ok(result) => Response::success(call.request.id, result), + Err(error) => Response::error(call.request.id, "activity_rejected", error), + }; + let _ = call.response.try_send(response); + } else if call.request.command == "pomodoro" { + let result = validate_request(&call.request) + .map_err(|error| error.message) + .and_then(|()| { + if state.session_lock.active() { + return Err("Session locked".into()); + } + let request: ferese_ipc::activities::PomodoroRequest = + serde_json::from_value(call.request.args) + .map_err(|error| error.to_string())?; + let output = state + .outputs_json() + .as_array() + .and_then(|outputs| { + outputs + .iter() + .find(|output| output["focused"] == true) + .or_else(|| outputs.first()) + }) + .and_then(|output| output["name"].as_str()) + .map(str::to_owned); + state.activities.pomodoro_control(request, output) + }); + let response = match result { + Ok(value) => Response::success(call.request.id, value), + Err(error) => Response::error(call.request.id, "pomodoro_rejected", error), + }; + let _ = call.response.try_send(response); + } else if call.request.command == "activity-get" { + if state.session_lock.active() { + let _ = call.response.try_send(Response::error( + call.request.id, + "session_locked", + "IPC unavailable while session is locked", + )); + } else if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else { + state.activities.get(call.request.id, call.response); + } + } else if call.request.command == "activity-watch" { + if state.session_lock.active() { + let _ = call.response.try_send(Response::error( + call.request.id, + "session_locked", + "IPC unavailable while session is locked", + )); + } else if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if let Some(since) = call.request.args["since"].as_u64() { state - .media_engine + .activities .watch(call.owner, call.request.id, since, call.response); } else { let _ = call.response.try_send(Response::error( call.request.id, "invalid_argument", - "Missing media revision", + "Missing activity revision", )); } - } else if let Err(error) = state - .media_engine - .action(call.request.args, Some((call.request.id, call.response.clone()))) + } else if call.request.command == "event-stream" { + let result = validate_request(&call.request).and_then(|()| { + if call.request.args["version"].as_u64() + != Some(u64::from(ferese_ipc::events::VERSION)) + { + return Err(CommandError::new( + "unsupported_event_version", + "Unsupported event schema version", + )); + } + state + .ipc_events + .subscribe(call.owner, call.events.expect("event stream sender")) + .map_err(|error| CommandError::new("subscriber_limit", error)) + }); + let response = match result { + Ok(()) => Response::success( + call.request.id, + json!({"version": ferese_ipc::events::VERSION}), + ), + Err(error) => Response::error(call.request.id, error.code, error.message), + }; + let _ = call.response.try_send(response); + } else if call.request.command.starts_with("input-capture-") && !call.native_portal { - let _ = call - .response - .try_send(Response::error(call.request.id, "media_unavailable", error)); - } - } else if call.request.command == "theme-watch" { - if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else if let Some(since) = call.request.args["since"].as_u64() { - state - .theme_engine - .watch(call.owner, call.request.id, since, call.response); - } else { let _ = call.response.try_send(Response::error( call.request.id, - "invalid_argument", - "Missing theme revision", + "access_denied", + "Input capture is restricted to the native portal", )); - } - } else if call.request.command == "session-watch" { - if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .send(Response::error(call.request.id, error.code, error.message)); - } else if let Some(since) = call.request.args["since"].as_u64() { - state - .portal_session - .watch(call.owner, call.request.id, since, call.response); + } else if call.request.command == "input-capture-watch" { + if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if let Some(id) = call.request.args["session"].as_u64() { + state + .input_capture + .watch(call.owner, call.request.id, id, call.response); + } else { + let _ = call.response.try_send(Response::error( + call.request.id, + "invalid_argument", + "Missing capture session", + )); + } + } else if matches!( + call.request.command.as_str(), + "media-watch" | "media-action" + ) { + if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if state.session_lock.active() { + let _ = call.response.try_send(Response::error( + call.request.id, + "session_locked", + "IPC unavailable while session is locked", + )); + } else if call.request.command == "media-watch" { + if let Some(since) = call.request.args["since"].as_u64() { + state.media_engine.watch( + call.owner, + call.request.id, + since, + call.response, + ); + } else { + let _ = call.response.try_send(Response::error( + call.request.id, + "invalid_argument", + "Missing media revision", + )); + } + } else if let Err(error) = state.media_engine.action( + call.request.args, + Some((call.request.id, call.response.clone())), + ) { + let _ = call.response.try_send(Response::error( + call.request.id, + "media_unavailable", + error, + )); + } + } else if call.request.command == "theme-watch" { + if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if let Some(since) = call.request.args["since"].as_u64() { + state + .theme_engine + .watch(call.owner, call.request.id, since, call.response); + } else { + let _ = call.response.try_send(Response::error( + call.request.id, + "invalid_argument", + "Missing theme revision", + )); + } + } else if call.request.command == "session-watch" { + if let Err(error) = validate_request(&call.request) { + let _ = call.response.send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if let Some(since) = call.request.args["since"].as_u64() { + state.portal_session.watch( + call.owner, + call.request.id, + since, + call.response, + ); + } else { + let _ = call.response.send(Response::error( + call.request.id, + "invalid_argument", + "Missing session revision", + )); + } + } else if call.request.command == "reload-config" { + if let Err(error) = validate_request(&call.request) { + let _ = call.response.try_send(Response::error( + call.request.id, + error.code, + error.message, + )); + } else if state.session_lock.active() { + let _ = call.response.try_send(Response::error( + call.request.id, + "session_locked", + "IPC unavailable while session is locked", + )); + } else if let Err(error) = + state.queue_config_reload(false, Some((call.request.id, call.response))) + { + tracing::warn!(%error, "cannot queue configuration reload"); + } + } else if call.request.command == "screenshot-window" { + state.start_window_screenshot(call.owner, call.request, call.response); + } else if call.request.command == "screenshot" { + // Deferred: this path answers the caller itself, exactly + // once, whenever the request finishes or is terminated. + state.start_screenshot(call.owner, call.request, call.response); } else { - let _ = call.response.send(Response::error( - call.request.id, - "invalid_argument", - "Missing session revision", - )); + let response = state.handle_ipc_request(call.owner, call.request); + let _ = call.response.send(response); } - } else if call.request.command == "reload-config" { - if let Err(error) = validate_request(&call.request) { - let _ = call - .response - .try_send(Response::error(call.request.id, error.code, error.message)); - } else if state.session_lock.active() { - let _ = call.response.try_send(Response::error( - call.request.id, - "session_locked", - "IPC unavailable while session is locked", - )); - } else if let Err(error) = state.queue_config_reload(false, Some((call.request.id, call.response))) { - tracing::warn!(%error, "cannot queue configuration reload"); - } - } else if call.request.command == "screenshot-window" { - state.start_window_screenshot(call.owner, call.request, call.response); - } else if call.request.command == "screenshot" { - // Deferred: this path answers the caller itself, exactly - // once, whenever the request finishes or is terminated. - state.start_screenshot(call.owner, call.request, call.response); - } else { - let response = state.handle_ipc_request(call.owner, call.request); - let _ = call.response.send(response); } - } - })?; + })?; // Screenshot readbacks publish parts here, so the loop wakes as soon as a // readback lands, whichever backend produced it. let (parts, part_events) = channel::channel::(); - event_loop.handle().insert_source(part_events, |event, _, state| { - if let channel::Event::Msg(outcome) = event { - state.on_screenshot_part(outcome); - } - })?; + event_loop + .handle() + .insert_source(part_events, |event, _, state| { + if let channel::Event::Msg(outcome) = event { + state.on_screenshot_part(outcome); + } + })?; let (encoded, encoded_events) = channel::sync_channel::(RESULT_QUEUE_CAPACITY); - event_loop.handle().insert_source(encoded_events, |event, _, state| { - if let channel::Event::Msg(result) = event { - state.on_screenshot_encoded(result); - } - })?; + event_loop + .handle() + .insert_source(encoded_events, |event, _, state| { + if let channel::Event::Msg(result) = event { + state.on_screenshot_encoded(result); + } + })?; let worker = Worker::spawn(encoded); let activity_timer = crate::activities::deadline::Deadline::new(event_loop.handle())?; @@ -589,7 +643,10 @@ fn accept_connections( } let Some(permit) = try_acquire_connection(&active_connections) else { - tracing::warn!(limit = MAX_CONNECTIONS, "rejected IPC connection at worker limit"); + tracing::warn!( + limit = MAX_CONNECTIONS, + "rejected IPC connection at worker limit" + ); continue; }; let sender = sender.clone(); @@ -601,7 +658,13 @@ fn accept_connections( .name("ferese-ipc-client".to_owned()) .spawn(move || { let _permit = permit; - serve_connection_with_activities(stream, sender, signal, owner, Some((monitor, rules))); + serve_connection_with_activities( + stream, + sender, + signal, + owner, + Some((monitor, rules)), + ); }) { tracing::warn!(%error, "could not start IPC connection worker"); @@ -621,11 +684,18 @@ fn try_acquire_connection(active: &Arc) -> Option } } - Some(ConnectionPermit { active: active.clone() }) + Some(ConnectionPermit { + active: active.clone(), + }) } #[cfg(test)] -fn serve_connection(stream: UnixStream, sender: channel::SyncSender, signal: LoopSignal, owner: u64) { +fn serve_connection( + stream: UnixStream, + sender: channel::SyncSender, + signal: LoopSignal, + owner: u64, +) { serve_connection_with_activities(stream, sender, signal, owner, None) } fn serve_connection_with_activities( @@ -648,7 +718,9 @@ fn serve_connection_with_activities( Err(ferese_ipc::FrameError::Io(error)) if matches!( error.kind(), - io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset | io::ErrorKind::BrokenPipe + io::ErrorKind::UnexpectedEof + | io::ErrorKind::ConnectionReset + | io::ErrorKind::BrokenPipe ) => { return; @@ -675,7 +747,8 @@ fn serve_connection_with_activities( let verified = (|| -> Result<_, String> { validate_request(&request).map_err(|error| error.message)?; let wire: ferese_ipc::activities::Request = - serde_json::from_value(std::mem::take(&mut request.args)).map_err(|error| error.to_string())?; + serde_json::from_value(std::mem::take(&mut request.args)) + .map_err(|error| error.to_string())?; if wire.version != ferese_ipc::activities::VERSION { return Err("Unsupported activity schema".into()); } @@ -688,25 +761,31 @@ fn serve_connection_with_activities( .map_err(|error| error.to_string())?; let mut publish = wire.publish; let is_run = matches!(publish, ferese_ipc::activities::Publish::Run { .. }); - let matched = if let ferese_ipc::activities::Publish::Hook { command, cwd, .. } - | ferese_ipc::activities::Publish::Run { command, cwd, .. } = &mut publish - { - let rules = rules.read().map_err(|_| "Activity rules unavailable")?.clone(); - let matched = if rules.enabled || is_run { - rules.matcher.matches(command, cwd, &parents) + let matched = + if let ferese_ipc::activities::Publish::Hook { command, cwd, .. } + | ferese_ipc::activities::Publish::Run { command, cwd, .. } = &mut publish + { + let rules = rules + .read() + .map_err(|_| "Activity rules unavailable")? + .clone(); + let matched = if rules.enabled || is_run { + rules.matcher.matches(command, cwd, &parents) + } else { + None + }; + command.clear(); + cwd.clear(); + matched } else { None }; - command.clear(); - cwd.clear(); - matched - } else { - None - }; if matches!( publish, - ferese_ipc::activities::Publish::Start { .. } | ferese_ipc::activities::Publish::Run { .. } - ) || (matches!(publish, ferese_ipc::activities::Publish::Hook { .. }) && matched.is_some()) + ferese_ipc::activities::Publish::Start { .. } + | ferese_ipc::activities::Publish::Run { .. } + ) || (matches!(publish, ferese_ipc::activities::Publish::Hook { .. }) + && matched.is_some()) { identity.capture_ancestors(); } @@ -715,8 +794,11 @@ fn serve_connection_with_activities( match verified { Ok(verified) => Some(verified), Err(error) => { - if transport::write_response(&stream, &Response::error(request.id, "owner_rejected", error)) - .is_err() + if transport::write_response( + &stream, + &Response::error(request.id, "owner_rejected", error), + ) + .is_err() { return; } @@ -738,7 +820,8 @@ fn serve_connection_with_activities( | "input-capture-register" | "begin-session-end" ); - let screenshot_requested = matches!(request.command.as_str(), "screenshot" | "screenshot-window"); + let screenshot_requested = + matches!(request.command.as_str(), "screenshot" | "screenshot-window"); let call = IpcEvent::Call(Box::new(IpcCall { native_portal, activity, @@ -812,7 +895,10 @@ fn serve_connection_with_activities( } // Socket I/O stays on the bounded connection worker, never the compositor loop. -fn relay_events(stream: &mut UnixStream, events: std::sync::mpsc::Receiver>) { +fn relay_events( + stream: &mut UnixStream, + events: std::sync::mpsc::Receiver>, +) { loop { match events.recv_timeout(Duration::from_millis(250)) { Ok(event) => { @@ -855,7 +941,10 @@ fn u32_arg(args: &Value, key: &str) -> Result { fn discard_undelivered_screenshot(response: &Response) { if response.error.is_none() - && let Some(path) = response.result.as_ref().and_then(|result| result["path"].as_str()) + && let Some(path) = response + .result + .as_ref() + .and_then(|result| result["path"].as_str()) && let Err(error) = fs::remove_file(path) && error.kind() != io::ErrorKind::NotFound { @@ -882,7 +971,11 @@ impl Ferese { | "cancel-session-end" ) { - return Response::error(request.id, "session_locked", "IPC unavailable while session is locked"); + return Response::error( + request.id, + "session_locked", + "IPC unavailable while session is locked", + ); } if let Err(error) = validate_request(&request) { return Response::error(request.id, error.code, error.message); @@ -894,7 +987,12 @@ impl Ferese { } } - fn dispatch_ipc_command(&mut self, owner: u64, command: &str, args: &Value) -> Result { + fn dispatch_ipc_command( + &mut self, + owner: u64, + command: &str, + args: &Value, + ) -> Result { match command { "theme-get" | "theme-status" => return Ok(self.theme_engine.value()), "theme-set-mode" => { @@ -905,7 +1003,9 @@ impl Ferese { .map_err(|error| CommandError::new("invalid_config", error)); } "night-light" => { - return Ok(json!(crate::backends::direct::gamma::night_light_status(self))); + return Ok(json!(crate::backends::direct::gamma::night_light_status( + self + ))); } "portal-inhibit" => { let inhibition = serde_json::from_value(args.clone()) @@ -923,7 +1023,8 @@ impl Ferese { return Ok(crate::xwayland::status_snapshot(self).to_value()); } ferese_ipc::xwayland::RETRY_COMMAND => { - crate::xwayland::retry(self).map_err(|error| CommandError::new("x11_unavailable", error))?; + crate::xwayland::retry(self) + .map_err(|error| CommandError::new("x11_unavailable", error))?; return Ok(crate::xwayland::status_snapshot(self).to_value()); } @@ -955,9 +1056,9 @@ impl Ferese { .validate_end( u32_arg(args, "token")?, u32_arg(args, "inhibitor-revision")?, - args["force"] - .as_bool() - .ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?, + args["force"].as_bool().ok_or_else(|| { + CommandError::new("invalid_argument", "Missing force confirmation") + })?, ) .map_err(|error| CommandError::new("inhibited", error))?; return Ok(json!({})); @@ -967,17 +1068,17 @@ impl Ferese { .commit_end( u32_arg(args, "token")?, u32_arg(args, "inhibitor-revision")?, - args["force"] - .as_bool() - .ok_or_else(|| CommandError::new("invalid_argument", "Missing force confirmation"))?, + args["force"].as_bool().ok_or_else(|| { + CommandError::new("invalid_argument", "Missing force confirmation") + })?, ) .map_err(|error| CommandError::new("inhibited", error))?; return Ok(json!({})); } "logind-session-ending" => { - let ending = args["ending"] - .as_bool() - .ok_or_else(|| CommandError::new("invalid_argument", "Missing shutdown state"))?; + let ending = args["ending"].as_bool().ok_or_else(|| { + CommandError::new("invalid_argument", "Missing shutdown state") + })?; self.portal_session.set_phase(if ending { 3 } else { 1 }); return Ok(json!({})); } @@ -990,7 +1091,9 @@ impl Ferese { return Ok(json!({})); } "portal-shortcuts-poll" => { - return Ok(self.portal_shortcuts.poll(owner, self.session_lock.active())); + return Ok(self + .portal_shortcuts + .poll(owner, self.session_lock.active())); } "input-capture-register" => { if self.session_lock.active() { @@ -1006,7 +1109,8 @@ impl Ferese { let xkb = state.xkb().lock().unwrap(); // SAFETY: the ref-counted keymap does not escape the locked Xkb; // only its owned string is retained for the EIS keyboard. - unsafe { xkb.keymap() }.get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1) + unsafe { xkb.keymap() } + .get_as_string(smithay::input::keyboard::xkb::KEYMAP_FORMAT_TEXT_V1) }); return Ok(json!({"session":id, "keymap":keymap})); } @@ -1015,9 +1119,9 @@ impl Ferese { return self .input_capture .zones( - args["session"] - .as_u64() - .ok_or_else(|| CommandError::new("invalid_capture", "Missing session"))?, + args["session"].as_u64().ok_or_else(|| { + CommandError::new("invalid_capture", "Missing session") + })?, ) .map_err(|e| CommandError::new("invalid_capture", e)); } @@ -1074,8 +1178,12 @@ impl Ferese { let result = if command == "input-capture-disable" { self.input_capture.disable(id) } else { - self.input_capture - .release(id, args["activation_id"].as_u64().and_then(|id| u32::try_from(id).ok())) + self.input_capture.release( + id, + args["activation_id"] + .as_u64() + .and_then(|id| u32::try_from(id).ok()), + ) }; result.map_err(|e| CommandError::new("invalid_capture", e))?; if active && !self.input_capture.active() { @@ -1166,7 +1274,9 @@ impl Ferese { .get("pid") .and_then(Value::as_u64) .and_then(|pid| u32::try_from(pid).ok()) - .ok_or_else(|| CommandError::new("invalid_argument", "pid must be an unsigned process ID"))?; + .ok_or_else(|| { + CommandError::new("invalid_argument", "pid must be an unsigned process ID") + })?; return Ok(json!(self.has_client_surfaces(pid))); } "get-workspaces" => return Ok(self.workspaces_json()), @@ -1185,29 +1295,38 @@ impl Ferese { })); } "output-confirm" | "output-revert" => { - crate::backends::direct::confirm_output_configuration(self, command == "output-confirm") - .map_err(|error| CommandError::new("output_configuration_failed", error))?; + crate::backends::direct::confirm_output_configuration( + self, + command == "output-confirm", + ) + .map_err(|error| CommandError::new("output_configuration_failed", error))?; return Ok(self.outputs_json()); } "output-profile" => { - let name = args["name"] - .as_str() - .ok_or_else(|| CommandError::new("invalid_arguments", "output-profile requires a name or auto"))?; + let name = args["name"].as_str().ok_or_else(|| { + CommandError::new( + "invalid_arguments", + "output-profile requires a name or auto", + ) + })?; crate::backends::direct::set_output_profile(self, name) .map_err(|error| CommandError::new("output_configuration_failed", error))?; return Ok(self.outputs_json()); } "output-layout" => { - let layout = serde_json::from_value::(args["layout"].clone()) - .map_err(|_| CommandError::new("invalid_arguments", "unknown display layout"))?; + let layout = + serde_json::from_value::(args["layout"].clone()) + .map_err(|_| { + CommandError::new("invalid_arguments", "unknown display layout") + })?; crate::backends::direct::set_output_layout(self, layout) .map_err(|error| CommandError::new("output_configuration_failed", error))?; return Ok(self.outputs_json()); } "output-internal" => { - let enabled = args["enabled"] - .as_bool() - .ok_or_else(|| CommandError::new("invalid_arguments", "output-internal requires on or off"))?; + let enabled = args["enabled"].as_bool().ok_or_else(|| { + CommandError::new("invalid_arguments", "output-internal requires on or off") + })?; crate::backends::direct::set_internal_output(self, enabled) .map_err(|error| CommandError::new("output_configuration_failed", error))?; return Ok(self.outputs_json()); @@ -1225,7 +1344,12 @@ impl Ferese { // Screenshot replies are deferred: this either answers the caller now, or // hands the reply to the coordinator, which answers exactly once. - pub(crate) fn start_screenshot(&mut self, owner: u64, request: Request, response: SyncSender) { + pub(crate) fn start_screenshot( + &mut self, + owner: u64, + request: Request, + response: SyncSender, + ) { // A macro rather than a closure: the message may be a borrowed str or an // owned String, and Response::error already accepts either. macro_rules! reject { @@ -1292,7 +1416,10 @@ impl Ferese { return; } }; - let specs = planned.iter().map(|part| part.spec(&layouts[part.index])).collect(); + let specs = planned + .iter() + .map(|part| part.spec(&layouts[part.index])) + .collect(); // admit stores the reply only on success, so a clone survives the // rejection path and every caller is answered exactly once. @@ -1300,7 +1427,8 @@ impl Ferese { let id = match self.screenshot.admit(owner, request.id, response, specs) { Ok(id) => id, Err(message) => { - let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", message)); + let _ = + fallback.try_send(Response::error(request.id, "screenshot_rejected", message)); return; } }; @@ -1328,9 +1456,18 @@ impl Ferese { crate::backends::direct::render_on(self, &outputs); } - fn start_window_screenshot(&mut self, owner: u64, request: Request, response: SyncSender) { + fn start_window_screenshot( + &mut self, + owner: u64, + request: Request, + response: SyncSender, + ) { let reject = |message: String| { - let _ = response.try_send(Response::error(request.id, "window_capture_failed", message)); + let _ = response.try_send(Response::error( + request.id, + "window_capture_failed", + message, + )); }; if self.session_lock.active() || !screencopy::capture_allowed() { reject("Screen capture is unavailable".into()); @@ -1386,16 +1523,25 @@ impl Ferese { buffer_height: size.h, }; let fallback = response.clone(); - let capture = match self.screenshot.admit(owner, request.id, response, vec![spec]) { + let capture = match self + .screenshot + .admit(owner, request.id, response, vec![spec]) + { Ok(capture) => capture, Err(error) => { - let _ = fallback.try_send(Response::error(request.id, "screenshot_rejected", error)); + let _ = + fallback.try_send(Response::error(request.id, "screenshot_rejected", error)); return; } }; let result = if let Some(backend) = &self.nested_backend { match backend.try_borrow_mut() { - Ok(mut backend) => crate::render::capture_window_buffer(backend.renderer(), &window, geometry, scale), + Ok(mut backend) => crate::render::capture_window_buffer( + backend.renderer(), + &window, + geometry, + scale, + ), Err(_) => Err("Window renderer is busy".into()), } } else if let Some(backend) = &mut self.direct_backend { @@ -1447,7 +1593,8 @@ impl Ferese { .is_ok() }); if !submitted { - self.screenshot.reject(request, "Screenshot encoding queue is full"); + self.screenshot + .reject(request, "Screenshot encoding queue is full"); } } Action::Delivered(path) => { @@ -1500,7 +1647,11 @@ impl Ferese { }); ferese_ipc::events::Snapshot { outputs, - workspaces: if locked { json!([]) } else { self.workspaces_json() }, + workspaces: if locked { + json!([]) + } else { + self.workspaces_json() + }, windows, focus: if locked { Value::Null @@ -1528,12 +1679,13 @@ impl Ferese { fn has_client_surfaces(&self, pid: u32) -> bool { use smithay::reexports::wayland_server::Resource; - let belongs_to = |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| { - surface - .client() - .and_then(|client| client.get_credentials(&self.display_handle).ok()) - .is_some_and(|credentials| credentials.pid as u32 == pid) - }; + let belongs_to = + |surface: &smithay::reexports::wayland_server::protocol::wl_surface::WlSurface| { + surface + .client() + .and_then(|client| client.get_credentials(&self.display_handle).ok()) + .is_some_and(|credentials| credentials.pid as u32 == pid) + }; self.windows .ids() .keys() @@ -1589,7 +1741,10 @@ impl Ferese { .workspace_views(&self.workspaces) .into_iter() .map(|view| { - let workspace = self.workspaces.workspace(view.id).expect("workspace view exists"); + let workspace = self + .workspaces + .workspace(view.id) + .expect("workspace view exists"); let mode = match workspace.layout.mode() { LayoutMode::Scrolling => "scrolling", LayoutMode::Tree => "tree", @@ -1686,7 +1841,9 @@ impl Ferese { }) }) .collect::>(); - outputs.sort_by(|left, right| left["connector"].as_str().cmp(&right["connector"].as_str())); + outputs.sort_by(|left, right| { + left["connector"].as_str().cmp(&right["connector"].as_str()) + }); return Value::Array(outputs); } @@ -1742,7 +1899,9 @@ fn geometry_arg(args: &Value) -> Result { match args.get("geometry") { None | Some(Value::Null) => Ok(Geometry::All), Some(value) => { - let text = value.as_str().ok_or("geometry must be a string like \"x,y WxH\"")?; + let text = value + .as_str() + .ok_or("geometry must be a string like \"x,y WxH\"")?; parse_geometry(text) } } @@ -1756,7 +1915,10 @@ fn validate_request(request: &Request) -> Result<(), CommandError> { )); } if request.kind != "command" { - return Err(CommandError::new("invalid_type", "expected type \"command\"")); + return Err(CommandError::new( + "invalid_type", + "expected type \"command\"", + )); } Ok(()) @@ -1821,11 +1983,18 @@ fn workspace_arg(args: &Value) -> Result { .and_then(Value::as_u64) .and_then(|index| u32::try_from(index).ok()) .filter(|index| *index > 0) - .ok_or_else(|| CommandError::new("invalid_argument", "index must be a positive 32-bit integer")) + .ok_or_else(|| { + CommandError::new( + "invalid_argument", + "index must be a positive 32-bit integer", + ) + }) } fn prepare_parent(socket: &Path) -> Result<(), io::Error> { - let parent = socket.parent().expect("the Ferese control socket always has a parent"); + let parent = socket + .parent() + .expect("the Ferese control socket always has a parent"); prepare_directory(parent) } @@ -1878,7 +2047,11 @@ fn bind_listener(path: &Path) -> Result { } fn peer_uid(stream: &UnixStream) -> Result { - let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 }; + let mut credentials = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; let mut length = std::mem::size_of::() as libc::socklen_t; // SAFETY: `credentials` and `length` point to initialized, correctly sized @@ -1940,7 +2113,10 @@ mod tests { let (mut event_loop, mut state) = deadline_loop(); let timer = state.timer.as_ref().unwrap(); timer.update(None); - assert!(timer.armed.borrow().is_none(), "idle registers no timeout source"); + assert!( + timer.armed.borrow().is_none(), + "idle registers no timeout source" + ); event_loop.dispatch(Duration::ZERO, &mut state).unwrap(); assert_eq!(state.fired, 0); @@ -1977,7 +2153,10 @@ mod tests { state.timer.as_ref().unwrap().update(Some(due)); state.timer.as_ref().unwrap().update(None); event_loop.dispatch(Duration::ZERO, &mut state).unwrap(); - assert_eq!(state.fired, 0, "cancelling the due timer removes its wakeup"); + assert_eq!( + state.fired, 0, + "cancelling the due timer removes its wakeup" + ); assert!(state.timer.as_ref().unwrap().armed.borrow().is_none()); } @@ -1991,7 +2170,10 @@ mod tests { .update(Some(Instant::now() - Duration::from_secs(1))); drop(state.timer.take()); event_loop.dispatch(Duration::ZERO, &mut state).unwrap(); - assert_eq!(state.fired, 0, "dropping the observer removes its timer callback"); + assert_eq!( + state.fired, 0, + "dropping the observer removes its timer callback" + ); } #[test] @@ -2034,7 +2216,10 @@ mod tests { #[test] fn parses_direction_arguments() { - assert_eq!(direction_arg(&json!({ "direction": "left" })).unwrap(), Direction::Left); + assert_eq!( + direction_arg(&json!({ "direction": "left" })).unwrap(), + Direction::Left + ); assert!(direction_arg(&json!({ "direction": "diagonal" })).is_err()); } @@ -2084,7 +2269,10 @@ mod tests { assert!(try_acquire_connection(&active).is_none()); let deadline = Instant::now() + transport::FRAME_TIMEOUT + Duration::from_secs(3); while active.load(Ordering::Acquire) != 0 { - assert!(Instant::now() < deadline, "silent peers retained connection slots"); + assert!( + Instant::now() < deadline, + "silent peers retained connection slots" + ); thread::sleep(Duration::from_millis(20)); } assert!(try_acquire_connection(&active).is_some()); @@ -2104,7 +2292,9 @@ mod tests { let worker = thread::spawn(move || serve_connection(server, sender, signal, 1)); let invalid = b"not-json"; - client.write_all(&(invalid.len() as u32).to_be_bytes()).unwrap(); + client + .write_all(&(invalid.len() as u32).to_be_bytes()) + .unwrap(); client.write_all(invalid).unwrap(); client.shutdown(Shutdown::Write).unwrap(); client @@ -2173,7 +2363,10 @@ mod tests { assert!(!first_directory.exists()); assert!(second.path.parent().unwrap().is_dir()); assert!(default.symlink_metadata().unwrap().file_type().is_socket()); - assert_eq!(bind_listener(&default).unwrap_err().kind(), io::ErrorKind::AddrInUse); + assert_eq!( + bind_listener(&default).unwrap_err().kind(), + io::ErrorKind::AddrInUse + ); drop(host); } @@ -2213,7 +2406,10 @@ mod tests { fs::set_permissions(&outside, fs::Permissions::from_mode(0o755)).unwrap(); std::os::unix::fs::symlink(&outside, default.parent().unwrap().join("instances")).unwrap(); assert!(Endpoint::instance_at(default).is_err()); - assert_eq!(fs::metadata(outside).unwrap().permissions().mode() & 0o777, 0o755); + assert_eq!( + fs::metadata(outside).unwrap().permissions().mode() & 0o777, + 0o755 + ); } #[test] @@ -2245,7 +2441,10 @@ mod tests { let socket = root.join("control.sock"); let first = bind_listener(&socket).unwrap(); - assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse); + assert_eq!( + bind_listener(&socket).unwrap_err().kind(), + io::ErrorKind::AddrInUse + ); drop(first); let replacement = bind_listener(&socket).unwrap(); @@ -2261,7 +2460,10 @@ mod tests { let socket = root.join("control.sock"); fs::write(&socket, b"keep").unwrap(); - assert_eq!(bind_listener(&socket).unwrap_err().kind(), io::ErrorKind::AddrInUse); + assert_eq!( + bind_listener(&socket).unwrap_err().kind(), + io::ErrorKind::AddrInUse + ); assert_eq!(fs::read(&socket).unwrap(), b"keep"); fs::remove_dir_all(root).unwrap(); } @@ -2296,7 +2498,9 @@ mod tests { #[test] fn stalled_event_socket_write_expires_on_the_worker() { let (_client, mut server) = UnixStream::pair().unwrap(); - server.set_write_timeout(Some(Duration::from_millis(50))).unwrap(); + server + .set_write_timeout(Some(Duration::from_millis(50))) + .unwrap(); let (sender, receiver) = sync_channel(1); let (finished, completion) = sync_channel(1); sender diff --git a/compositor/src/ipc/activity_owner.rs b/compositor/src/ipc/activity_owner.rs index e04032b..e53784f 100644 --- a/compositor/src/ipc/activity_owner.rs +++ b/compositor/src/ipc/activity_owner.rs @@ -25,7 +25,9 @@ impl VerifiedOwner { let mut next = self.owner.pid; let mut child = None; for _ in 0..16 { - let Ok((birth, parent)) = identity(next) else { break }; + let Ok((birth, parent)) = identity(next) else { + break; + }; if next == self.owner.pid && birth != self.owner.birth { break; } @@ -59,7 +61,10 @@ impl VerifiedOwner { pub(crate) struct Monitor(Arc<(Mutex>, Condvar)>); impl Monitor { pub fn new(sender: channel::SyncSender) -> Self { - let shared = Arc::new((Mutex::new(BTreeMap::::new()), Condvar::new())); + let shared = Arc::new(( + Mutex::new(BTreeMap::::new()), + Condvar::new(), + )); let worker = shared.clone(); std::thread::Builder::new() .name("ferese-activity-owners".into()) @@ -104,7 +109,11 @@ impl Monitor { .expect("activity owners") .retain(|owner, _| owners.contains(owner)); } - pub fn verify(&self, stream: &UnixStream, parent: bool) -> io::Result<(VerifiedOwner, Vec)> { + pub fn verify( + &self, + stream: &UnixStream, + parent: bool, + ) -> io::Result<(VerifiedOwner, Vec)> { let peer = credentials(stream)?; let peer_pid = u32::try_from(peer.pid).map_err(|_| io::Error::other("invalid peer PID"))?; let peer_identity = identity(peer_pid)?; @@ -115,7 +124,10 @@ impl Monitor { let pid = if parent { peer_identity.1 } else { peer_pid }; let (birth, _) = identity(pid)?; let fd = pidfd(pid)?; - if identity(pid)?.0 != birth || exited(&fd) || (parent && identity(peer_pid)? != peer_identity) { + if identity(pid)?.0 != birth + || exited(&fd) + || (parent && identity(peer_pid)? != peer_identity) + { return Err(io::Error::other("owner identity changed")); } let metadata = std::fs::metadata(format!("/proc/{pid}"))?; @@ -224,7 +236,11 @@ fn parse_stat(stat: &str) -> io::Result<(u64, u32)> { Ok((birth, parent)) } fn credentials(stream: &UnixStream) -> io::Result { - let mut credentials = libc::ucred { pid: 0, uid: 0, gid: 0 }; + let mut credentials = libc::ucred { + pid: 0, + uid: 0, + gid: 0, + }; let mut len = std::mem::size_of::() as libc::socklen_t; // SAFETY: both output buffers have the sizes expected by SO_PEERCRED. if unsafe { @@ -274,7 +290,10 @@ mod tests { use super::*; #[test] fn captured_ancestry_stops_at_an_exited_process() { - let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); + let mut child = std::process::Command::new("sleep") + .arg("30") + .spawn() + .unwrap(); let (owner, fd, _) = inspect(child.id()).unwrap(); let mut verified = VerifiedOwner { owner, @@ -283,7 +302,11 @@ mod tests { }; verified.capture_ancestors(); assert_eq!(verified.ancestor_pids().next(), Some(child.id())); - assert!(verified.ancestor_pids().any(|pid| pid == std::process::id())); + assert!( + verified + .ancestor_pids() + .any(|pid| pid == std::process::id()) + ); child.kill().unwrap(); child.wait().unwrap(); assert!(verified.ancestor_pids().next().is_none()); @@ -334,7 +357,10 @@ mod tests { fn owner_that_exits_in_the_dispatch_queue_is_rejected() { let (sender, _receiver) = channel::sync_channel(16); let monitor = Monitor::new(sender); - let mut child = std::process::Command::new("sleep").arg("30").spawn().unwrap(); + let mut child = std::process::Command::new("sleep") + .arg("30") + .spawn() + .unwrap(); let (owner, fd, _) = inspect(child.id()).unwrap(); child.kill().unwrap(); child.wait().unwrap(); @@ -370,7 +396,10 @@ mod tests { assert_eq!(peer.owner.pid, std::process::id()); let (parent, _) = monitor.verify(&left, true).unwrap(); assert_eq!(parent.owner.pid, unsafe { libc::getppid() } as u32); - let mut child = std::process::Command::new("sleep").arg("0.05").spawn().unwrap(); + let mut child = std::process::Command::new("sleep") + .arg("0.05") + .spawn() + .unwrap(); let fd = pidfd(child.id()).unwrap(); assert!(!exited(&fd)); child.wait().unwrap(); diff --git a/compositor/src/ipc/transport.rs b/compositor/src/ipc/transport.rs index 0931699..bb75ad2 100644 --- a/compositor/src/ipc/transport.rs +++ b/compositor/src/ipc/transport.rs @@ -41,7 +41,10 @@ impl Write for DeadlineIo<'_> { } } -pub(super) fn read_request(stream: &UnixStream, idle: Option) -> Result { +pub(super) fn read_request( + stream: &UnixStream, + idle: Option, +) -> Result { read_request_with_budget(stream, idle, FRAME_TIMEOUT) } @@ -60,7 +63,10 @@ fn read_request_with_budget( ferese_ipc::read_frame(&mut first.as_slice().chain(reader)) } -pub(super) fn write_response(stream: &UnixStream, value: &impl serde::Serialize) -> Result<(), FrameError> { +pub(super) fn write_response( + stream: &UnixStream, + value: &impl serde::Serialize, +) -> Result<(), FrameError> { // Tests may install a tighter socket timeout. let previous = stream.write_timeout()?; let budget = previous.unwrap_or(WRITE_TIMEOUT).min(WRITE_TIMEOUT); @@ -88,7 +94,8 @@ mod tests { assert!(read_request_with_budget(&server, Some(short), short).is_err()); let (mut client, server) = UnixStream::pair().unwrap(); - let worker = std::thread::spawn(move || read_request_with_budget(&server, None, short).unwrap()); + let worker = + std::thread::spawn(move || read_request_with_budget(&server, None, short).unwrap()); std::thread::sleep(short * 3); let request = Request { version: 1, @@ -112,8 +119,12 @@ mod tests { std::thread::sleep(Duration::from_millis(30)); } }); - let error = - read_request_with_budget(&server, Some(Duration::from_secs(1)), Duration::from_millis(70)).unwrap_err(); + let error = read_request_with_budget( + &server, + Some(Duration::from_secs(1)), + Duration::from_millis(70), + ) + .unwrap_err(); assert!( matches!(error, FrameError::Io(error) if matches!(error.kind(), io::ErrorKind::TimedOut | io::ErrorKind::WouldBlock)) ); diff --git a/compositor/src/ipc_events.rs b/compositor/src/ipc_events.rs index 51ad365..c4fe561 100644 --- a/compositor/src/ipc_events.rs +++ b/compositor/src/ipc_events.rs @@ -21,11 +21,21 @@ pub(crate) struct Subscribers { } impl Subscribers { - pub(crate) fn subscribe(&mut self, owner: u64, sender: SyncSender>) -> Result<(), &'static str> { + pub(crate) fn subscribe( + &mut self, + owner: u64, + sender: SyncSender>, + ) -> Result<(), &'static str> { if self.subscribers.len() >= MAX_SUBSCRIBERS { return Err("Event subscriber limit reached"); } - self.subscribers.insert(owner, Subscriber { sender, initial: true }); + self.subscribers.insert( + owner, + Subscriber { + sender, + initial: true, + }, + ); Ok(()) } @@ -47,7 +57,12 @@ impl Subscribers { .as_ref() .map(|previous| snapshot.changes_since(previous)) .unwrap_or_default(); - if changes.is_empty() && !self.subscribers.values().any(|subscriber| subscriber.initial) { + if changes.is_empty() + && !self + .subscribers + .values() + .any(|subscriber| subscriber.initial) + { return; } self.generation = self.generation.wrapping_add(1); @@ -65,19 +80,26 @@ impl Subscribers { if let Some(last) = events.last_mut() { Arc::get_mut(last).unwrap().last = true; } - let initial = self.subscribers.values().any(|subscriber| subscriber.initial).then(|| { - Arc::new(Event { - version: VERSION, - generation: self.generation, - last: true, - change: Change::Snapshot { - desktop: Box::new(snapshot.clone()), - }, - }) - }); + let initial = self + .subscribers + .values() + .any(|subscriber| subscriber.initial) + .then(|| { + Arc::new(Event { + version: VERSION, + generation: self.generation, + last: true, + change: Change::Snapshot { + desktop: Box::new(snapshot.clone()), + }, + }) + }); self.subscribers.retain(|_, subscriber| { let delivered = if subscriber.initial { - subscriber.sender.try_send(initial.as_ref().unwrap().clone()).is_ok() + subscriber + .sender + .try_send(initial.as_ref().unwrap().clone()) + .is_ok() } else { events .iter() @@ -137,7 +159,10 @@ mod tests { hub.subscribe(1, sender).unwrap(); let mut state = snapshot(); hub.publish(state.clone()); - assert!(matches!(receive.try_recv().unwrap().change, Change::Snapshot { .. })); + assert!(matches!( + receive.try_recv().unwrap().change, + Change::Snapshot { .. } + )); hub.publish(state.clone()); assert!(matches!(receive.try_recv(), Err(TryRecvError::Empty))); state.outputs = json!([{"id": 2}]); @@ -146,7 +171,11 @@ mod tests { hub.publish(state); let events = receive.try_iter().collect::>(); assert_eq!(events.len(), 3); - assert!(events.iter().all(|event| event.generation == events[0].generation)); + assert!( + events + .iter() + .all(|event| event.generation == events[0].generation) + ); assert!(!events[0].last && !events[1].last && events[2].last); } @@ -171,7 +200,10 @@ mod tests { )); assert_eq!(hub.subscribers.len(), 1); slow_receive.try_recv().unwrap(); - assert!(matches!(slow_receive.try_recv(), Err(TryRecvError::Disconnected))); + assert!(matches!( + slow_receive.try_recv(), + Err(TryRecvError::Disconnected) + )); hub.remove(2); assert!(!hub.active()); assert!(hub.previous.is_none()); @@ -209,7 +241,11 @@ mod tests { hub.publish(state); let events = receive.try_iter().collect::>(); assert_eq!(events.len(), 7); - assert!(events.iter().all(|event| event.generation == events[0].generation)); + assert!( + events + .iter() + .all(|event| event.generation == events[0].generation) + ); assert!(events[..6].iter().all(|event| !event.last)); assert!(events[6].last); } @@ -229,7 +265,8 @@ mod tests { let mut event_loop = EventLoop::try_new().unwrap(); let mut config = crate::config::Config::default().runtime_config().unwrap(); config.wallpaper.path = None; - let mut state = crate::Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap(); + let mut state = + crate::Ferese::new(&mut event_loop, Display::new().unwrap(), config).unwrap(); let output = |name: &str, x| DesktopOutput { output: Output::new( name.into(), @@ -273,7 +310,11 @@ mod tests { state.publish_ipc_events(); let events = receive.try_iter().collect::>(); assert!(events.last().unwrap().last); - assert!(events.iter().all(|event| event.generation == events[0].generation)); + assert!( + events + .iter() + .all(|event| event.generation == events[0].generation) + ); let final_state = state.ipc_event_snapshot(); assert_eq!(final_state.outputs.as_array().unwrap().len(), 2); for workspace in final_state.workspaces.as_array().unwrap() { @@ -336,7 +377,10 @@ mod tests { }); let deadline = std::time::Instant::now() + std::time::Duration::from_secs(3); while completion.try_recv().is_err() { - assert!(std::time::Instant::now() < deadline, "subscription worker timed out"); + assert!( + std::time::Instant::now() < deadline, + "subscription worker timed out" + ); event_loop .dispatch(std::time::Duration::from_millis(10), &mut state) .unwrap(); diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 1ff7063..45de6d1 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -80,8 +80,8 @@ fn main() -> Result<(), Box> { return Ok(()); } - let launch = - LaunchConfig::from_environment().map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; + let launch = LaunchConfig::from_environment() + .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?; let config_path = config::config_path(); let startup = config::startup(config_path.as_deref())?; if let Some(error) = &startup.error { @@ -120,7 +120,9 @@ fn main() -> Result<(), Box> { value }); if let Some(error) = startup.error { - state.theme_engine.startup_failed(config_path.as_deref(), error); + state + .theme_engine + .startup_failed(config_path.as_deref(), error); } theme::init(&mut event_loop, &mut state, startup.theme)?; let _resume_monitor = resume::init(&mut event_loop)?; @@ -201,7 +203,10 @@ fn after_dispatch(state: &mut Ferese) { }); } -fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>)) { +fn after_dispatch_with_redraw( + state: &mut Ferese, + mut redraw: impl FnMut(&mut Ferese, RedrawRequest<'_>), +) { crate::backends::direct::gamma::sync_night_light(state); state.process_pending_dmabuf_imports(); state.update_named_cursor(std::time::Instant::now()); @@ -213,7 +218,9 @@ fn after_dispatch_with_redraw(state: &mut Ferese, mut redraw: impl FnMut(&mut Fe // when no output has a pending frame. let wallpaper_changed = state.wallpaper.poll(); // All input/Wayland callbacks have returned, releasing seat locks. - if state.focus_cycle.is_some() && (state.session_lock.active() || state.input_capture.captures(1)) { + if state.focus_cycle.is_some() + && (state.session_lock.active() || state.input_capture.captures(1)) + { state.cancel_focus_cycle(); } diff --git a/compositor/src/metrics.rs b/compositor/src/metrics.rs index 7a19da6..c3c2f9c 100644 --- a/compositor/src/metrics.rs +++ b/compositor/src/metrics.rs @@ -108,8 +108,14 @@ impl RenderMetrics { .unwrap_or(0); self.frame_times.make_contiguous().sort_unstable(); let percentile = |percent: usize| { - let index = (self.frame_times.len() * percent).div_ceil(100).saturating_sub(1); - self.frame_times.get(index).copied().unwrap_or_default().as_micros() + let index = (self.frame_times.len() * percent) + .div_ceil(100) + .saturating_sub(1); + self.frame_times + .get(index) + .copied() + .unwrap_or_default() + .as_micros() }; tracing::info!( target: "ferese::render", @@ -145,7 +151,8 @@ impl RenderMetrics { } fn rectangle_area(rectangle: &Rectangle) -> u64 { - u64::try_from(rectangle.size.w.max(0)).unwrap_or(0) * u64::try_from(rectangle.size.h.max(0)).unwrap_or(0) + u64::try_from(rectangle.size.w.max(0)).unwrap_or(0) + * u64::try_from(rectangle.size.h.max(0)).unwrap_or(0) } #[cfg(test)] @@ -240,8 +247,14 @@ mod tests { for micros in 0..600 { metrics.record_frame(Duration::from_micros(micros), &[], 0, FrameEffectMetrics); } - assert_eq!(metrics.frame_times.front(), Some(&Duration::from_micros(88))); - assert_eq!(metrics.frame_times.back(), Some(&Duration::from_micros(599))); + assert_eq!( + metrics.frame_times.front(), + Some(&Duration::from_micros(88)) + ); + assert_eq!( + metrics.frame_times.back(), + Some(&Duration::from_micros(599)) + ); assert!( metrics .frame_times diff --git a/compositor/src/monitor_identity.rs b/compositor/src/monitor_identity.rs index 4a2c927..a59214c 100644 --- a/compositor/src/monitor_identity.rs +++ b/compositor/src/monitor_identity.rs @@ -26,7 +26,10 @@ impl Identity { pub fn from_edid(edid: &[u8]) -> Option { if edid.len() < 128 || edid[..8] != [0, 255, 255, 255, 255, 255, 255, 0] - || edid[..128].iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte)) != 0 + || edid[..128] + .iter() + .fold(0u8, |sum, byte| sum.wrapping_add(*byte)) + != 0 { return None; } @@ -62,7 +65,11 @@ impl Identity { if serial.is_none() && numeric_serial != 0 && numeric_serial != u32::MAX { serial = Some(format!("{numeric_serial:08X}")); } - Some(Self { make, model, serial }) + Some(Self { + make, + model, + serial, + }) } pub fn base(&self) -> String { @@ -90,8 +97,12 @@ impl IdentityRegistry { *counts.entry(identity.base()).or_insert(0) += 1; } } - self.ambiguous - .extend(counts.into_iter().filter(|(_, count)| *count > 1).map(|(base, _)| base)); + self.ambiguous.extend( + counts + .into_iter() + .filter(|(_, count)| *count > 1) + .map(|(base, _)| base), + ); // Prefer already-known identities, but never permit duplicate runtime IDs. let mut used = HashSet::new(); let mut ordered = monitors.iter().enumerate().collect::>(); @@ -107,12 +118,15 @@ impl IdentityRegistry { .known .get(key) .filter(|known| { - base.as_ref().is_some_and(|base| *known == base || *known == &qualified) + base.as_ref() + .is_some_and(|base| *known == base || *known == &qualified) || (base.is_none() && *known == &qualified) }) .cloned() .unwrap_or_else(|| { - if identity.as_ref().is_some_and(|identity| identity.serial.is_some()) + if identity + .as_ref() + .is_some_and(|identity| identity.serial.is_some()) && !self.ambiguous.contains(base.as_ref().unwrap()) { base.clone().unwrap() @@ -208,7 +222,10 @@ mod tests { edid[10..12].copy_from_slice(&0x1234u16.to_le_bytes()); edid[12..16].copy_from_slice(&42u32.to_le_bytes()); edid[127] = 0u8.wrapping_sub(edid.iter().fold(0u8, |sum, byte| sum.wrapping_add(*byte))); - assert_eq!(Identity::from_edid(&edid).unwrap().base(), "edid:DEL:1234:0000002A"); + assert_eq!( + Identity::from_edid(&edid).unwrap().base(), + "edid:DEL:1234:0000002A" + ); edid[127] ^= 1; assert!(Identity::from_edid(&edid).is_none()); } diff --git a/compositor/src/output_policy.rs b/compositor/src/output_policy.rs index a3dc5f3..ae592eb 100644 --- a/compositor/src/output_policy.rs +++ b/compositor/src/output_policy.rs @@ -16,7 +16,9 @@ pub(crate) struct Monitor { impl Monitor { pub fn matches(&self, selector: &str) -> bool { - selector == self.connector || selector == self.identity || self.aliases.iter().any(|alias| alias == selector) + selector == self.connector + || selector == self.identity + || self.aliases.iter().any(|alias| alias == selector) } } @@ -59,9 +61,9 @@ impl ManualOverride { self.layout = None; } if self.profile.as_ref().is_some_and(|name| { - !profiles - .iter() - .any(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed)) + !profiles.iter().any(|profile| { + profile.name == *name && profile_matches(profile, monitors, lid_closed) + }) }) { self.profile = None; } @@ -79,7 +81,9 @@ fn profile_matches(profile: &OutputProfile, monitors: &[Monitor], lid_closed: bo }) { return false; } - profile.lid_closed.is_none_or(|required| required == lid_closed) + profile + .lid_closed + .is_none_or(|required| required == lid_closed) && profile .outputs .iter() @@ -119,9 +123,9 @@ pub(crate) fn select_profile( .profile .as_ref() .and_then(|name| { - profiles - .iter() - .find(|profile| profile.name == *name && profile_matches(profile, monitors, lid_closed)) + profiles.iter().find(|profile| { + profile.name == *name && profile_matches(profile, monitors, lid_closed) + }) }) .or_else(|| { profiles @@ -130,7 +134,11 @@ pub(crate) fn select_profile( .filter(|(_, profile)| profile_matches(profile, monitors, lid_closed)) .max_by_key(|(index, profile)| { ( - profile.outputs.iter().filter(|output| output.required).count(), + profile + .outputs + .iter() + .filter(|output| output.required) + .count(), std::cmp::Reverse(*index), ) }) @@ -140,7 +148,9 @@ pub(crate) fn select_profile( .layout .unwrap_or_else(|| selected.map_or(OutputLayout::Extend, |profile| profile.layout)); let manage_lid = selected.is_none_or(|profile| profile.lid_policy == LidPolicy::DockOrSuspend); - let external = monitors.iter().any(|monitor| monitor.usable && !monitor.internal); + let external = monitors + .iter() + .any(|monitor| monitor.usable && !monitor.internal); let mut outputs = monitors .iter() .map(|monitor| { @@ -229,7 +239,11 @@ pub(crate) fn restore_configuration( .outputs .retain(|output| monitors.iter().any(|monitor| monitor.key == output.key)); for monitor in monitors { - if !restored.outputs.iter().any(|output| output.key == monitor.key) { + if !restored + .outputs + .iter() + .any(|output| output.key == monitor.key) + { let mut settings = defaults(monitor.identity.clone()); settings.enabled = false; restored.outputs.push(DesiredOutput { @@ -254,7 +268,11 @@ pub(crate) fn restore_configuration( output.mirror_source = None; } } - if !restored.outputs.iter().any(|output| output.settings.enabled) { + if !restored + .outputs + .iter() + .any(|output| output.settings.enabled) + { return select_profile(monitors, false, &[], &ManualOverride::default()); } restored @@ -278,7 +296,8 @@ pub(crate) fn changed_outputs<'a>( let next = new.outputs.iter().find(|candidate| candidate.key == key); let same = match (previous, next) { (Some(previous), Some(next)) => { - previous.settings.same_configuration(&next.settings) && previous.mirror_source == next.mirror_source + previous.settings.same_configuration(&next.settings) + && previous.mirror_source == next.mirror_source } (None, None) => true, _ => false, @@ -294,7 +313,11 @@ pub(crate) struct Debouncer { } impl Debouncer { - pub fn request(&mut self, now: std::time::Duration, active: bool) -> Option { + pub fn request( + &mut self, + now: std::time::Duration, + active: bool, + ) -> Option { self.deadline = active.then(|| now + std::time::Duration::from_millis(150)); self.deadline } @@ -341,7 +364,9 @@ pub(crate) fn arrange_positions( automatic.push((index, placement)); } } - automatic.sort_by(|(_, a), (_, b)| (a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key))); + automatic.sort_by(|(_, a), (_, b)| { + (a.previous.is_none(), &a.key).cmp(&(b.previous.is_none(), &b.key)) + }); for (index, placement) in automatic { let fits = |position: [i32; 2]| { let (x, y, w, h) = ( @@ -351,7 +376,10 @@ pub(crate) fn arrange_positions( placement.size.1 as i64, ); !occupied.iter().any(|&(ox, oy, ow, oh)| { - x < ox as i64 + ow as i64 && x + w > ox as i64 && y < oy as i64 + oh as i64 && y + h > oy as i64 + x < ox as i64 + ow as i64 + && x + w > ox as i64 + && y < oy as i64 + oh as i64 + && y + h > oy as i64 }) }; let previous = placement.previous.filter(|position| fits(*position)); @@ -463,7 +491,10 @@ mod tests { fn profile(name: &str, outputs: &[&str], layout: OutputLayout) -> OutputProfile { OutputProfile { name: name.into(), - outputs: outputs.iter().map(|name| defaults((*name).into())).collect(), + outputs: outputs + .iter() + .map(|name| defaults((*name).into())) + .collect(), layout, confirm_timeout: 15, lid_policy: LidPolicy::DockOrSuspend, @@ -559,7 +590,11 @@ mod tests { &ManualOverride::default(), ); assert_eq!( - desired.outputs.iter().filter(|output| output.settings.enabled).count(), + desired + .outputs + .iter() + .filter(|output| output.settings.enabled) + .count(), count ); assert_eq!( @@ -574,7 +609,12 @@ mod tests { let missing = [profile("absent", &["DP-9"], OutputLayout::ExternalOnly)]; let fallback = select_profile(&monitors, false, &missing, &ManualOverride::default()); assert_eq!(fallback.profile, None); - assert!(fallback.outputs.iter().all(|output| output.settings.enabled)); + assert!( + fallback + .outputs + .iter() + .all(|output| output.settings.enabled) + ); let internal = &monitors[..1]; let mut disabled = profile("disabled", &["eDP-1"], OutputLayout::Extend); disabled.outputs[0].enabled = false; @@ -631,13 +671,23 @@ mod tests { let monitors = [monitor("eDP-1", true), monitor("DP-1", false)]; let mut mirror = profile("mirror", &["eDP-1", "DP-1"], OutputLayout::Mirror); mirror.mirror_source = Some("eDP-1".into()); - let old = select_profile(&monitors, false, &[mirror.clone()], &ManualOverride::default()); + let old = select_profile( + &monitors, + false, + &[mirror.clone()], + &ManualOverride::default(), + ); assert_eq!(old.outputs[1].mirror_source.as_deref(), Some("eDP-1")); let removed = select_profile(&monitors[..1], false, &[mirror], &ManualOverride::default()); assert!(removed.outputs[0].mirror_source.is_none()); assert!(removed.outputs[0].settings.enabled); let extended = select_profile(&monitors, false, &[], &ManualOverride::default()); - assert!(extended.outputs.iter().all(|output| output.mirror_source.is_none())); + assert!( + extended + .outputs + .iter() + .all(|output| output.mirror_source.is_none()) + ); assert_eq!(changed_outputs(&old, &extended), vec!["DP-1"]); } @@ -648,9 +698,14 @@ mod tests { mobile.outputs[1].required = false; mobile.lid_closed = Some(false); assert_eq!( - select_profile(&monitors, false, &[mobile.clone()], &ManualOverride::default()) - .profile - .as_deref(), + select_profile( + &monitors, + false, + &[mobile.clone()], + &ManualOverride::default() + ) + .profile + .as_deref(), Some("mobile") ); assert_eq!( @@ -679,7 +734,11 @@ mod tests { assert_eq!(restored.outputs.len(), 1); assert!(restored.outputs[0].settings.enabled); let unknown = [monitor("HDMI-A-2", false)]; - assert!(restore_configuration(&previous, &unknown).outputs[0].settings.enabled); + assert!( + restore_configuration(&previous, &unknown).outputs[0] + .settings + .enabled + ); } } diff --git a/compositor/src/output_transaction.rs b/compositor/src/output_transaction.rs index ffe6c5a..ae626ee 100644 --- a/compositor/src/output_transaction.rs +++ b/compositor/src/output_transaction.rs @@ -152,7 +152,12 @@ mod tests { let inventory = |active: &BTreeSet| { active .iter() - .map(|id| (OutputId(*id as u64), OutputGeometry::new(*id as i32 * 800, 0, 800, 600))) + .map(|id| { + ( + OutputId(*id as u64), + OutputGeometry::new(*id as i32 * 800, 0, 800, 600), + ) + }) .collect::>() }; for rollback_fails in [false, true] { @@ -184,8 +189,14 @@ mod tests { ) .unwrap(); if rollback_fails { - assert_eq!(plan.outputs.connected_outputs().collect::>(), vec![OutputId(2)]); - assert_eq!(plan.outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2))); + assert_eq!( + plan.outputs.connected_outputs().collect::>(), + vec![OutputId(2)] + ); + assert_eq!( + plan.outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(2)) + ); } else { assert_eq!(plan.outputs, published); assert!(plan.affected_outputs.is_empty()); diff --git a/compositor/src/overview.rs b/compositor/src/overview.rs index 503807c..26a974d 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -41,14 +41,17 @@ pub(crate) fn init_font_loader( ) -> Result<(), Box> { use smithay::reexports::calloop::channel; let (results, source) = channel::sync_channel::<(String, Option)>(1); - event_loop.handle().insert_source(source, |event, _, state| { - if let channel::Event::Msg((family, font)) = event - && state.overview.complete_font_load(&family, font) - && (state.overview.is_presenting() || state.tab_bars.values().any(|bars| !bars.is_empty())) - { - crate::backends::direct::render_all(state); - } - })?; + event_loop + .handle() + .insert_source(source, |event, _, state| { + if let channel::Event::Msg((family, font)) = event + && state.overview.complete_font_load(&family, font) + && (state.overview.is_presenting() + || state.tab_bars.values().any(|bars| !bars.is_empty())) + { + crate::backends::direct::render_all(state); + } + })?; let latest = Arc::new(Mutex::new(state.overview.font_family.clone())); let worker_latest = latest.clone(); let (wake, requests) = mpsc::sync_channel(1); @@ -98,7 +101,10 @@ fn label_font(database: &fontdb::Database, family: &str) -> Option { }) } -type LabelBuffer = (MemoryRenderBuffer, smithay::utils::Size); +type LabelBuffer = ( + MemoryRenderBuffer, + smithay::utils::Size, +); type LabelStyle = (u64, u64, [u32; 4]); type OverviewStateLabel = HashMap>; @@ -170,11 +176,19 @@ pub(crate) fn workspace_strip(bounds: Rect) -> Rect { fn window_area(bounds: Rect) -> Rect { let strip = workspace_strip(bounds); let top = strip.y + strip.height + 16.0; - Rect::new(bounds.x, top, bounds.width, (bounds.y + bounds.height - top).max(1.0)) + Rect::new( + bounds.x, + top, + bounds.width, + (bounds.y + bounds.height - top).max(1.0), + ) } fn contains(rect: Rect, point: Point) -> bool { - point.x >= rect.x && point.x < rect.x + rect.width && point.y >= rect.y && point.y < rect.y + rect.height + point.x >= rect.x + && point.x < rect.x + rect.width + && point.y >= rect.y + && point.y < rect.y + rect.height } fn strip_capacity(strip: Rect) -> usize { @@ -192,11 +206,10 @@ impl OverviewState { pub(crate) fn needs_tick(&self) -> bool { self.motion.opacity.current != self.motion.opacity.target || self.motion.opacity.velocity != 0.0 - || self - .motion - .presentations - .values() - .any(|rect| rect.current != rect.target || rect.velocity != ferese_animation::RectVelocity::default()) + || self.motion.presentations.values().any(|rect| { + rect.current != rect.target + || rect.velocity != ferese_animation::RectVelocity::default() + }) } pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { @@ -246,12 +259,25 @@ impl OverviewState { text: &str, scale: f64, color: [f32; 4], - ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { + ) -> Option<( + MemoryRenderBuffer, + smithay::utils::Size, + )> { self.label_with_size(text, scale, color, 13.0) } - fn label_with_size(&mut self, text: &str, scale: f64, color: [f32; 4], font_size: f64) -> Option { - let key = (scale.to_bits(), font_size.to_bits(), color.map(f32::to_bits)); + fn label_with_size( + &mut self, + text: &str, + scale: f64, + color: [f32; 4], + font_size: f64, + ) -> Option { + let key = ( + scale.to_bits(), + font_size.to_bits(), + color.map(f32::to_bits), + ); if let Some(buffer) = self.labels.get(text).and_then(|styles| styles.get(&key)) { return Some(buffer.clone()); } @@ -269,7 +295,10 @@ impl OverviewState { let mut pen = 2.0; for ch in text.chars() { let id = font.glyph_id(ch); - let glyph = id.with_scale_and_position(scaled.scale(), ab_glyph::point(pen, scaled.ascent() + 1.0)); + let glyph = id.with_scale_and_position( + scaled.scale(), + ab_glyph::point(pen, scaled.ascent() + 1.0), + ); pen += scaled.h_advance(id); if let Some(outline) = font.outline_glyph(glyph) { let bounds = outline.px_bounds(); @@ -289,8 +318,14 @@ impl OverviewState { }); } } - let buffer = - MemoryRenderBuffer::from_slice(&pixels, Fourcc::Abgr8888, (width, height), 1, Transform::Normal, None); + let buffer = MemoryRenderBuffer::from_slice( + &pixels, + Fourcc::Abgr8888, + (width, height), + 1, + Transform::Normal, + None, + ); if self.label_count >= 128 { self.labels.clear(); self.label_count = 0; @@ -318,12 +353,19 @@ impl OverviewState { } fn select_workspace_windows(&mut self, windows: &[WindowId], preferred: Option) { - let visible = |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id); + let visible = + |id: &WindowId| windows.contains(id) && self.motion.presentations.contains_key(id); self.selected = self .selected .filter(visible) .or_else(|| preferred.filter(visible)) - .or_else(|| windows.iter().copied().filter(visible).min_by_key(|id| id.0)); + .or_else(|| { + windows + .iter() + .copied() + .filter(visible) + .min_by_key(|id| id.0) + }); } pub(crate) fn selection_state(&self) -> (Option, Option) { @@ -363,9 +405,13 @@ impl OverviewState { .filter(|(id, _)| Some(**id) != self.selected) .filter_map(|(id, presentation)| { let center = rect_center(presentation.current); - directional_distance(direction, current_center, center).map(|distance| (*id, distance)) + directional_distance(direction, current_center, center) + .map(|distance| (*id, distance)) + }) + .min_by(|(left_id, left), (right_id, right)| { + left.total_cmp(right) + .then_with(|| left_id.0.cmp(&right_id.0)) }) - .min_by(|(left_id, left), (right_id, right)| left.total_cmp(right).then_with(|| left_id.0.cmp(&right_id.0))) .map(|(id, _)| id); let Some(next) = next else { return false; @@ -385,26 +431,33 @@ impl OverviewState { &self.motion } - fn seed_presentations(&mut self, windows: impl IntoIterator) { + fn seed_presentations( + &mut self, + windows: impl IntoIterator, + ) { for window in windows { - self.motion.presentations.entry(window.id).or_insert_with(|| { - // Opening scales the final presentation after overview. Remove - // it here so the same opening motion is not applied twice. - let scale = 0.97 + 0.03 * window.opacity.current; - let mut bounds = window.bounds; - bounds.velocity = crate::presentation::scaled_visual_velocity( - bounds.current, - bounds.velocity, - 1.0 / scale, - -0.03 * window.opacity.velocity / (scale * scale), - ); - bounds.current = crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale); - bounds.velocity.x /= OVERVIEW_MOTION_SPEED; - bounds.velocity.y /= OVERVIEW_MOTION_SPEED; - bounds.velocity.width /= OVERVIEW_MOTION_SPEED; - bounds.velocity.height /= OVERVIEW_MOTION_SPEED; - bounds - }); + self.motion + .presentations + .entry(window.id) + .or_insert_with(|| { + // Opening scales the final presentation after overview. Remove + // it here so the same opening motion is not applied twice. + let scale = 0.97 + 0.03 * window.opacity.current; + let mut bounds = window.bounds; + bounds.velocity = crate::presentation::scaled_visual_velocity( + bounds.current, + bounds.velocity, + 1.0 / scale, + -0.03 * window.opacity.velocity / (scale * scale), + ); + bounds.current = + crate::presentation::scaled_visual_rect(bounds.current, 1.0 / scale); + bounds.velocity.x /= OVERVIEW_MOTION_SPEED; + bounds.velocity.y /= OVERVIEW_MOTION_SPEED; + bounds.velocity.width /= OVERVIEW_MOTION_SPEED; + bounds.velocity.height /= OVERVIEW_MOTION_SPEED; + bounds + }); } } @@ -428,7 +481,9 @@ impl OverviewState { } fn retarget(&mut self, targets: HashMap, animations_enabled: bool) { - self.motion.presentations.retain(|id, _| targets.contains_key(id)); + self.motion + .presentations + .retain(|id, _| targets.contains_key(id)); for (id, (normal, target)) in targets { let presentation = self @@ -446,7 +501,12 @@ impl OverviewState { .selected .is_none_or(|id| !self.motion.presentations.contains_key(&id)) { - self.selected = self.motion.presentations.keys().copied().min_by_key(|id| id.0); + self.selected = self + .motion + .presentations + .keys() + .copied() + .min_by_key(|id| id.0); } } @@ -474,7 +534,12 @@ impl OverviewState { } } - pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool { + pub(crate) fn advance( + &mut self, + delta: Duration, + spring: SpringConfig, + animations_enabled: bool, + ) -> bool { self.motion.advance(delta, spring, animations_enabled) } } @@ -496,7 +561,10 @@ impl Ferese { } } - pub(crate) fn redraw_overview_selection(&mut self, previous: (Option, Option)) { + pub(crate) fn redraw_overview_selection( + &mut self, + previous: (Option, Option), + ) { let outputs = self .space .outputs() @@ -509,7 +577,8 @@ impl Ferese { .into_iter() .flatten() .any(|workspace| { - self.output_workspaces.output_for_workspace(workspace) == self.output_id(output) + self.output_workspaces.output_for_workspace(workspace) + == self.output_id(output) }) }) .cloned() @@ -523,7 +592,12 @@ impl Ferese { .retain(|id, _| self.windows.window(*id).is_some()); } - pub(crate) fn overview_window_label(&mut self, id: WindowId, scale: f64, width: f64) -> Option { + pub(crate) fn overview_window_label( + &mut self, + id: WindowId, + scale: f64, + width: f64, + ) -> Option { self.window_title_label(id, scale, width, 13.0) } @@ -537,7 +611,11 @@ impl Ferese { let record = self.windows.record(id)?; let limit = ((width - 16.0) / 8.0).clamp(1.0, 48.0) as usize; let color = self.theme_settings.text_primary_color.0; - let style = (scale.to_bits(), font_size.to_bits(), color.map(f32::to_bits)); + let style = ( + scale.to_bits(), + font_size.to_bits(), + color.map(f32::to_bits), + ); if let Some(label) = self.overview.window_labels.get(&id) && label.revision == record.metadata_revision @@ -563,7 +641,9 @@ impl Ferese { } let revision = record.metadata_revision; - let buffer = self.overview.label_with_size(&text, scale, color, font_size)?; + let buffer = self + .overview + .label_with_size(&text, scale, color, font_size)?; self.overview.window_labels.insert( id, WindowLabel { @@ -604,9 +684,15 @@ impl Ferese { &mut self, index: u32, scale: f64, - ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { - self.overview - .label(&index.to_string(), scale, self.theme_settings.text_primary_color.0) + ) -> Option<( + MemoryRenderBuffer, + smithay::utils::Size, + )> { + self.overview.label( + &index.to_string(), + scale, + self.theme_settings.text_primary_color.0, + ) } pub(crate) fn set_overview_active(&mut self, active: bool) { @@ -676,14 +762,17 @@ impl Ferese { if !self.overview.is_active() { if self.overview.is_presenting() { let normal = self.normal_window_rects(); - self.overview.motion.presentations.retain(|id, presentation| { - if let Some(target) = normal.get(id) { - presentation.set_target(*target); - true - } else { - false - } - }); + self.overview + .motion + .presentations + .retain(|id, presentation| { + if let Some(target) = normal.get(id) { + presentation.set_target(*target); + true + } else { + false + } + }); } return; } @@ -691,7 +780,13 @@ impl Ferese { let targets = self.overview_targets(); self.overview.retarget(targets, self.animations_enabled()); if let Some(cycle) = self.focus_cycle.as_mut() { - let available = self.overview.motion.presentations.keys().copied().collect::>(); + let available = self + .overview + .motion + .presentations + .keys() + .copied() + .collect::>(); if let Some(id) = cycle.reconcile(&available) { self.overview.select_window(id); } @@ -711,7 +806,8 @@ impl Ferese { .collect::>() }) .unwrap_or_default(); - self.overview.select_workspace_windows(&windows, self.focused_window); + self.overview + .select_workspace_windows(&windows, self.focused_window); } } @@ -719,7 +815,12 @@ impl Ferese { Some(self.current_window_presentation(id)?.bounds.current) } - pub(crate) fn inverse_presented_window_point(&self, id: WindowId, x: f64, y: f64) -> Option<(f64, f64)> { + pub(crate) fn inverse_presented_window_point( + &self, + id: WindowId, + x: f64, + y: f64, + ) -> Option<(f64, f64)> { let geometry = self.windows.geometry(&id)?; if !self.overview.is_presenting() && self.windows.record(id)?.opening.is_none() { let presented = self.presented_window_rect(id)?; @@ -732,7 +833,11 @@ impl Ferese { }; let presented = self.presented_window_rect(id)?; - if source.width <= 0 || source.height <= 0 || presented.width <= 0.0 || presented.height <= 0.0 { + if source.width <= 0 + || source.height <= 0 + || presented.width <= 0.0 + || presented.height <= 0.0 + { return None; } @@ -780,9 +885,10 @@ impl Ferese { .copied() .unwrap_or_else(|| selected_index.saturating_sub(capacity - 1)) .min(max_offset); - let card_width = ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64) - / capacity as f64) - .clamp(1.0, 160.0); + let card_width = + ((strip.width - 24.0 - WORKSPACE_CARD_GAP * capacity.saturating_sub(1) as f64) + / capacity as f64) + .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 @@ -791,7 +897,10 @@ impl Ferese { .take(capacity) .enumerate() .map(|(index, view)| { - let workspace = self.workspaces.workspace(view.id).expect("workspace view exists"); + let workspace = self + .workspaces + .workspace(view.id) + .expect("workspace view exists"); let rect = Rect::new( start_x + index as f64 * (card_width + WORKSPACE_CARD_GAP), @@ -861,8 +970,11 @@ impl Ferese { self.advance_animations(std::time::Instant::now()); let previous = self.output_workspaces.active_workspace(output_id); if let Ok( - ferese_core::WorkspaceSwitch::Activated(owner) | ferese_core::WorkspaceSwitch::FocusedExisting(owner), - ) = self.output_workspaces.switch_workspace(output_id, card.workspace) + ferese_core::WorkspaceSwitch::Activated(owner) + | ferese_core::WorkspaceSwitch::FocusedExisting(owner), + ) = self + .output_workspaces + .switch_workspace(output_id, card.workspace) { self.update_workspace_slide(output_id, owner, previous, card.workspace, None); // Existing monitor ownership wins; never steal another output's workspace. @@ -889,7 +1001,9 @@ impl Ferese { }; let capacity = strip_capacity(workspace_strip(bounds)); let cards = self.overview_workspace_cards(&output); - let ordered = self.output_workspaces.ordered_workspaces(&self.workspaces, output_id); + let ordered = self + .output_workspaces + .ordered_workspaces(&self.workspaces, output_id); let current = cards .first() .and_then(|card| ordered.iter().position(|id| *id == card.workspace)) @@ -933,12 +1047,17 @@ impl Ferese { }) .collect::>(); for (id, target) in overview_layout(window_area(bounds), &windows) { - targets.insert(id, (self.windows.geometry(&id).unwrap().visual.current, target)); + targets.insert( + id, + (self.windows.geometry(&id).unwrap().visual.current, target), + ); } continue; } let mut visible = vec![workspace]; - let sliding = self.workspace_slide_workspaces(output_id).collect::>(); + let sliding = self + .workspace_slide_workspaces(output_id) + .collect::>(); visible.extend(sliding.iter().copied().filter(|id| *id != workspace)); for workspace in visible { let Some(workspace) = self.workspaces.workspace(workspace) else { @@ -983,7 +1102,12 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap Vec<(WindowId, Rect)> { +fn preview_layout( + bounds: Rect, + windows: &[(WindowId, Rect)], + margin: f64, + gap: f64, +) -> Vec<(WindowId, Rect)> { if windows.is_empty() { return Vec::new(); } @@ -996,8 +1120,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: let cell_size = |columns: usize| { let rows = windows.len().div_ceil(columns); ( - ((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64).clamp(1.0, 480.0), - ((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64).clamp(1.0, 420.0), + ((content_width - gap * columns.saturating_sub(1) as f64) / columns as f64) + .clamp(1.0, 480.0), + ((content_height - gap * rows.saturating_sub(1) as f64) / rows as f64) + .clamp(1.0, 420.0), ) }; let score = |columns: usize| { @@ -1039,8 +1165,10 @@ fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: let width = (source.width * scale).max(1.0); let height = (source.height * scale).max(1.0); let row_count = (windows.len() - row * columns).min(columns); - let row_width = cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64; - let cell_x = bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap); + let row_width = + cell_width * row_count as f64 + gap * row_count.saturating_sub(1) as f64; + let cell_x = + bounds.x + (bounds.width - row_width) * 0.5 + column as f64 * (cell_width + gap); let cell_y = origin_y + row as f64 * (cell_height + gap); let target = Rect::new( cell_x + (cell_width - width) / 2.0, @@ -1058,7 +1186,11 @@ fn rect_center(rect: Rect) -> (f64, f64) { (rect.x + rect.width / 2.0, rect.y + rect.height / 2.0) } -fn directional_distance(direction: Direction, current: (f64, f64), candidate: (f64, f64)) -> Option { +fn directional_distance( + direction: Direction, + current: (f64, f64), + candidate: (f64, f64), +) -> Option { let delta_x = candidate.0 - current.0; let delta_y = candidate.1 - current.1; let (primary, secondary) = match direction { @@ -1086,7 +1218,10 @@ impl OverviewMotion { pub(crate) fn is_animating(&self, spring: SpringConfig) -> bool { (self.opacity.current - self.opacity.target).abs() > 0.001 || self.opacity.velocity.abs() > 0.005 - || self.presentations.values().any(|rect| !rect.is_settled(spring)) + || self + .presentations + .values() + .any(|rect| !rect.is_settled(spring)) } pub(crate) fn is_presenting(&self) -> bool { @@ -1113,7 +1248,12 @@ impl OverviewMotion { self.presentations.get(&id).map_or(normal, |p| p.current) } - pub(crate) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations_enabled: bool) -> bool { + pub(crate) fn advance( + &mut self, + delta: Duration, + spring: SpringConfig, + animations_enabled: bool, + ) -> bool { let delta = delta.mul_f64(OVERVIEW_MOTION_SPEED); let mut active_animation = false; @@ -1194,7 +1334,9 @@ mod tests { Some(id), true, ); - let sampled = overview.motion.presented_bounds(id, AnimatedRect::new(normal)); + let sampled = overview + .motion + .presented_bounds(id, AnimatedRect::new(normal)); assert!((sampled.current.x - normal.x).abs() < 1e-10); assert!((sampled.current.width - normal.width).abs() < 1e-10); assert!((sampled.velocity.x - velocity.x).abs() < 1e-10); @@ -1265,7 +1407,10 @@ mod tests { requests.request(family); } receiver.try_recv().unwrap(); - assert!(matches!(receiver.try_recv(), Err(mpsc::TryRecvError::Empty))); + assert!(matches!( + receiver.try_recv(), + Err(mpsc::TryRecvError::Empty) + )); assert_eq!(*requests.latest.lock().unwrap(), "third"); drop(requests); assert!(matches!(receiver.recv(), Err(mpsc::RecvError))); @@ -1285,7 +1430,9 @@ mod tests { let strip = Rect::new(origin + 24.0, 64.0, 1552.0, 132.0); for count in [1, 3, 8] { let start = centered_row_start(strip, count, 160.0); - let end = start + count as f64 * 160.0 + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP; + let end = start + + count as f64 * 160.0 + + count.saturating_sub(1) as f64 * WORKSPACE_CARD_GAP; assert!((start - strip.x - (strip.x + strip.width - end)).abs() < 0.001); } } @@ -1302,7 +1449,11 @@ mod tests { assert!(strip.height < bounds.height / 2.0); assert!(area.y > strip.y + strip.height); let windows = [(WindowId(1), Rect::new(0.0, 0.0, 800.0, 900.0))]; - assert!(overview_layout(area, &windows).values().all(|rect| rect.y >= area.y)); + assert!( + overview_layout(area, &windows) + .values() + .all(|rect| rect.y >= area.y) + ); } } @@ -1386,7 +1537,11 @@ mod tests { let mut reference = AnimatedRect::new(normal); reference.set_target(thumbnail); - reference.advance_with_policy(Duration::from_millis(60), spring, CrossingPolicy::NoCrossing); + reference.advance_with_policy( + Duration::from_millis(60), + spring, + CrossingPolicy::NoCrossing, + ); assert_eq!(predicted.presented_rect(id, normal), reference.current); overview.advance(Duration::from_millis(100), spring, true); @@ -1467,7 +1622,10 @@ mod tests { #[test] fn overview_grid_preserves_preview_aspect_ratio() { let source = Rect::new(0.0, 0.0, 1200.0, 800.0); - let layout = overview_layout(Rect::new(0.0, 0.0, 1920.0, 1080.0), &[(WindowId(1), source)]); + let layout = overview_layout( + Rect::new(0.0, 0.0, 1920.0, 1080.0), + &[(WindowId(1), source)], + ); let preview = layout[&WindowId(1)]; assert!((preview.width / preview.height - source.width / source.height).abs() < 0.001); @@ -1579,7 +1737,10 @@ mod tests { let mut overview = OverviewState::default(); overview.enter( - HashMap::from([(left, (left_rect, left_rect)), (right, (right_rect, right_rect))]), + HashMap::from([ + (left, (left_rect, left_rect)), + (right, (right_rect, right_rect)), + ]), Some(left), false, ); diff --git a/compositor/src/portal_session.rs b/compositor/src/portal_session.rs index f708107..ed42f06 100644 --- a/compositor/src/portal_session.rs +++ b/compositor/src/portal_session.rs @@ -103,21 +103,31 @@ impl PortalSession { pub(crate) fn remove(&mut self, owner: u64) { self.waiters.remove(&owner); - if self.phase == 2 && self.query.as_ref().is_some_and(|query| query.owner == Some(owner)) { + if self.phase == 2 + && self + .query + .as_ref() + .is_some_and(|query| query.owner == Some(owner)) + { self.set_phase(1); } if self.inhibitors.remove(&owner).is_some() { self.inhibitor_revision = self.inhibitor_revision.wrapping_add(1); } let monitor_removed = self.monitors.remove(&owner); - let acknowledged = self.query.as_mut().is_some_and(|query| query.pending.remove(&owner)); + let acknowledged = self + .query + .as_mut() + .is_some_and(|query| query.pending.remove(&owner)); if monitor_removed || acknowledged { self.changed(); } } pub(crate) fn idle_inhibited(&self) -> bool { - self.inhibitors.values().any(|inhibitor| inhibitor.flags & 8 != 0) + self.inhibitors + .values() + .any(|inhibitor| inhibitor.flags & 8 != 0) } pub(crate) fn begin_query(&mut self) -> u32 { @@ -141,7 +151,11 @@ impl PortalSession { } pub(crate) fn cancel_query(&mut self, token: u32) { - if self.query.as_ref().is_some_and(|query| query.token == token) { + if self + .query + .as_ref() + .is_some_and(|query| query.token == token) + { self.set_phase(1); } } @@ -154,19 +168,37 @@ impl PortalSession { .is_some_and(|query| query.pending.is_empty() || Instant::now() >= query.deadline) } - pub(crate) fn validate_end(&self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> { + pub(crate) fn validate_end( + &self, + token: u32, + inhibitor_revision: u32, + force: bool, + ) -> Result<(), String> { if !self.query_ready() || self.query.as_ref().is_none_or(|query| query.token != token) { - return Err("Session-ending confirmation expired or applications are still responding".into()); + return Err( + "Session-ending confirmation expired or applications are still responding".into(), + ); } if self.inhibitor_revision != inhibitor_revision - || (!force && self.inhibitors.values().any(|inhibitor| inhibitor.flags & 1 != 0)) + || (!force + && self + .inhibitors + .values() + .any(|inhibitor| inhibitor.flags & 1 != 0)) { - return Err("Applications changed their inhibitors; review the confirmation again".into()); + return Err( + "Applications changed their inhibitors; review the confirmation again".into(), + ); } Ok(()) } - pub(crate) fn commit_end(&mut self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> { + pub(crate) fn commit_end( + &mut self, + token: u32, + inhibitor_revision: u32, + force: bool, + ) -> Result<(), String> { self.validate_end(token, inhibitor_revision, force)?; self.set_phase(3); Ok(()) @@ -209,7 +241,13 @@ impl PortalSession { } } - pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: SyncSender) { + pub(crate) fn watch( + &mut self, + owner: u64, + id: u64, + since: u64, + response: SyncSender, + ) { if since != self.revision { let _ = response.try_send(Response::success(id, self.snapshot())); } else { @@ -224,7 +262,10 @@ impl PortalSession { .map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})) .collect::>(); let remaining = self.query.as_ref().map_or(0, |query| { - query.deadline.saturating_duration_since(Instant::now()).as_millis() as u64 + query + .deadline + .saturating_duration_since(Instant::now()) + .as_millis() as u64 }); json!({"screensaver-active": self.locked, "session-state": self.phase, "revision": self.revision, "inhibitors": inhibitors, "inhibitor-revision": self.inhibitor_revision, @@ -236,16 +277,17 @@ impl PortalSession { impl crate::Ferese { pub(crate) fn end_portal_session(&mut self) { let token = self.portal_session.query.as_ref().map(|query| query.token); - let result = - self.loop_handle - .insert_source(Timer::from_duration(Duration::from_millis(200)), move |_, _, state| { - if state.portal_session.phase == 3 - && state.portal_session.query.as_ref().map(|query| query.token) == token - { - state.loop_signal.stop(); - } - TimeoutAction::Drop - }); + let result = self.loop_handle.insert_source( + Timer::from_duration(Duration::from_millis(200)), + move |_, _, state| { + if state.portal_session.phase == 3 + && state.portal_session.query.as_ref().map(|query| query.token) == token + { + state.loop_signal.stop(); + } + TimeoutAction::Drop + }, + ); if let Err(error) = result { tracing::error!(%error, "failed to schedule session ending"); self.loop_signal.stop(); @@ -304,10 +346,20 @@ mod tests { let approved = session.inhibitor_revision; session.register(1, inhibition(1)).unwrap(); assert!(session.commit_end(token, approved, true).is_err()); - assert!(session.commit_end(token, session.inhibitor_revision, false).is_err()); - session.commit_end(token, session.inhibitor_revision, true).unwrap(); + assert!( + session + .commit_end(token, session.inhibitor_revision, false) + .is_err() + ); + session + .commit_end(token, session.inhibitor_revision, true) + .unwrap(); assert_eq!(session.snapshot()["session-state"], 3); - assert!(session.commit_end(token, session.inhibitor_revision, true).is_err()); + assert!( + session + .commit_end(token, session.inhibitor_revision, true) + .is_err() + ); } #[test] @@ -350,7 +402,10 @@ mod tests { let response = receiver.try_recv().unwrap(); assert_eq!(response.result.unwrap()["screensaver-active"], true); session.set_locked(false); - assert_eq!(session.snapshot()["transitions"].as_array().unwrap().len(), 2); + assert_eq!( + session.snapshot()["transitions"].as_array().unwrap().len(), + 2 + ); let (sender, receiver) = std::sync::mpsc::sync_channel(1); session.watch(1, 11, session.revision, sender); session.remove(1); diff --git a/compositor/src/portal_shortcuts.rs b/compositor/src/portal_shortcuts.rs index 5d3f83a..a7d9108 100644 --- a/compositor/src/portal_shortcuts.rs +++ b/compositor/src/portal_shortcuts.rs @@ -26,7 +26,9 @@ struct Registration { impl Registration { fn timestamp(&self) -> u64 { - self.latest.0.saturating_add(self.latest.1.elapsed().as_millis() as u64) + self.latest + .0 + .saturating_add(self.latest.1.elapsed().as_millis() as u64) } fn release_all(&mut self) { @@ -53,7 +55,11 @@ impl PortalShortcuts { return Err("Shortcut limit reached".into()); } let keymap = crate::config::physical_keymap(input).map_err(|error| error.to_string())?; - if self.0.get(&owner).is_some_and(|registration| registration.closed) { + if self + .0 + .get(&owner) + .is_some_and(|registration| registration.closed) + { return Err("Shortcut session has ended".into()); } let mut parsed: Vec<(Shortcut, Binding)> = Vec::new(); @@ -62,11 +68,17 @@ impl PortalShortcuts { .trigger .split('+') .any(|part| part.trim().eq_ignore_ascii_case("num")); - if shortcut.id.is_empty() || shortcut.id.len() > 256 || shortcut.id.chars().any(char::is_control) { + if shortcut.id.is_empty() + || shortcut.id.len() > 256 + || shortcut.id.chars().any(char::is_control) + { return Err("Invalid shortcut identifier".into()); } - let binding = Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?; - if configured.iter().any(|other| binding.conflicts(other, &keymap)) + let binding = + Binding::portal_trigger(&shortcut.trigger).map_err(|error| error.to_string())?; + if configured + .iter() + .any(|other| binding.conflicts(other, &keymap)) || parsed .iter() .any(|(other, key)| other.id == shortcut.id || binding.conflicts(key, &keymap)) @@ -106,9 +118,9 @@ impl PortalShortcuts { return; }; for registration in self.0.values_mut() { - registration - .shortcuts - .retain(|(_, binding)| !configured.iter().any(|key| binding.conflicts(key, &keymap))); + registration.shortcuts.retain(|(_, binding)| { + !configured.iter().any(|key| binding.conflicts(key, &keymap)) + }); let active_ids = registration .shortcuts .iter() @@ -144,7 +156,13 @@ impl PortalShortcuts { }) } - pub(crate) fn press(&mut self, key: Keycode, symbols: &[u32], modifiers: &ModifiersState, timestamp: u64) -> bool { + pub(crate) fn press( + &mut self, + key: Keycode, + symbols: &[u32], + modifiers: &ModifiersState, + timestamp: u64, + ) -> bool { for registration in self.0.values_mut() { let Some((shortcut, _)) = registration.shortcuts.iter().find(|(shortcut, binding)| { (!shortcut.num_required || modifiers.num_lock) @@ -225,7 +243,10 @@ mod tests { .register(1, vec![shortcut("one", "Super+q")], &configured, &input) .is_err() ); - assert_eq!(shortcuts.poll(1, false)["shortcuts"][0]["trigger"], "Ctrl+Alt+a"); + assert_eq!( + shortcuts.poll(1, false)["shortcuts"][0]["trigger"], + "Ctrl+Alt+a" + ); shortcuts.remove(1); assert_eq!(shortcuts.poll(1, false)["closed"], true); shortcuts @@ -274,7 +295,12 @@ mod tests { .register(1, vec![shortcut("one", "Ctrl+Alt+a")], &[], &input) .unwrap(); shortcuts.reconcile(&[Binding::portal_trigger("Ctrl+Alt+a").unwrap()], &input); - assert!(shortcuts.poll(1, false)["shortcuts"].as_array().unwrap().is_empty()); + assert!( + shortcuts.poll(1, false)["shortcuts"] + .as_array() + .unwrap() + .is_empty() + ); assert!(Binding::portal_trigger("Ctrl+Alt+Escape").is_err()); assert!(Binding::portal_trigger("Ctrl+Alt+F3").is_err()); } @@ -305,9 +331,10 @@ mod tests { #[test] fn physical_conflicts_include_secondary_layouts_and_num_modifier_is_honored() { - let config = - crate::config::Config::parse_source("binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n") - .unwrap(); + let config = crate::config::Config::parse_source( + "binding keys=\"Ctrl+AD01\" match=\"physical\" action=\"none\"\n", + ) + .unwrap(); let mut input = config.input_settings().unwrap(); input.xkb_layout = "us,fr".into(); let configured = config.bindings(&input).unwrap(); diff --git a/compositor/src/presentation.rs b/compositor/src/presentation.rs index 6d5e757..65bfbfa 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -5,7 +5,9 @@ use std::time::{Duration, Instant}; use ferese_animation::AnimatedRect; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement, RenderElementStates}; -use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform}; +use smithay::backend::renderer::gles::{ + GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, +}; use smithay::backend::renderer::utils::{CommitCounter, DamageSet}; use smithay::desktop::utils::OutputPresentationFeedback; use smithay::output::Output; @@ -52,7 +54,8 @@ impl WindowPresentation { } pub fn close(&mut self) { - self.bounds.set_target(scaled_visual_rect(self.bounds.current, 0.98)); + self.bounds + .set_target(scaled_visual_rect(self.bounds.current, 0.98)); self.opacity.set_target(0.0); self.emphasis.set_target(0.0); self.shadow.set_target(0.0); @@ -64,21 +67,32 @@ impl WindowPresentation { /// always follows the window's shared rounded geometry. pub fn shadow_factors(self) -> (f64, f64, f64) { let strength = self.shadow.current.clamp(0.0, 1.0); - (0.75 + 0.25 * strength, 0.9 + 0.1 * strength, 0.8 + 0.2 * strength) + ( + 0.75 + 0.25 * strength, + 0.9 + 0.1 * strength, + 0.8 + 0.2 * strength, + ) } } // Response multipliers preserve damping ratio and the user's crossing policy. // Emphasis responds sooner; the shadow settles a little later than the bounds. -pub(crate) fn emphasis_spring(base: ferese_animation::SpringConfig) -> ferese_animation::SpringConfig { +pub(crate) fn emphasis_spring( + base: ferese_animation::SpringConfig, +) -> ferese_animation::SpringConfig { visual_response(base, 0.8) } -pub(crate) fn shadow_spring(base: ferese_animation::SpringConfig) -> ferese_animation::SpringConfig { +pub(crate) fn shadow_spring( + base: ferese_animation::SpringConfig, +) -> ferese_animation::SpringConfig { visual_response(base, 1.2) } -fn visual_response(base: ferese_animation::SpringConfig, response: f64) -> ferese_animation::SpringConfig { +fn visual_response( + base: ferese_animation::SpringConfig, + response: f64, +) -> ferese_animation::SpringConfig { ferese_animation::SpringConfig { stiffness: base.stiffness / (response * response), damping: base.damping / response, @@ -95,11 +109,14 @@ pub(crate) fn take_output_feedback( flags: wp_presentation_feedback::Kind, ) -> OutputPresentationFeedback { use smithay::desktop::layer_map_for_output; - use smithay::desktop::utils::{OutputPresentationFeedback, surface_presentation_feedback_flags_from_states}; + use smithay::desktop::utils::{ + OutputPresentationFeedback, surface_presentation_feedback_flags_from_states, + }; let mut feedback = OutputPresentationFeedback::new(output); - let visible_output = - |surface: &WlSurface, _: &SurfaceData| rendered_feedback_output(output, surface.into(), rendered); + let visible_output = |surface: &WlSurface, _: &SurfaceData| { + rendered_feedback_output(output, surface.into(), rendered) + }; let surface_flags = |surface: &WlSurface, _: &SurfaceData| { flags | surface_presentation_feedback_flags_from_states(surface, None, rendered) }; @@ -121,7 +138,11 @@ pub(crate) fn take_output_feedback( feedback } -fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementStates) -> Option { +fn rendered_feedback_output( + output: &Output, + id: Id, + rendered: &RenderElementStates, +) -> Option { rendered .element_render_state(id) .is_some_and(crate::display_presentation::is_visible) @@ -153,7 +174,11 @@ pub(crate) fn physical_rect( pub(crate) use ferese_shape::Shape as CornerShape; /// Conservative extent of the corner shoulders, in physical pixels. -pub(crate) fn corner_extent(radius: f32, size: smithay::utils::Size, shape: CornerShape) -> f32 { +pub(crate) fn corner_extent( + radius: f32, + size: smithay::utils::Size, + shape: CornerShape, +) -> f32 { if shape == CornerShape::Circular { return radius; } @@ -175,7 +200,12 @@ impl RoundedRect { self } - pub(crate) fn new(rect: ferese_layout::Rect, origin: Point, scale: f64, radius: f64) -> Self { + pub(crate) fn new( + rect: ferese_layout::Rect, + origin: Point, + scale: f64, + radius: f64, + ) -> Self { let rect = physical_rect(rect, origin, scale); Self { radius: clamp_radius(radius * scale, rect.size), @@ -271,7 +301,11 @@ impl Element for PhysicalShaderElement { self.geometry } - fn damage_since(&self, _: Scale, commit: Option) -> DamageSet { + fn damage_since( + &self, + _: Scale, + commit: Option, + ) -> DamageSet { if commit == Some(self.current_commit()) { DamageSet::default() } else { @@ -333,7 +367,11 @@ impl Element for NativeTextureElement { Kind::Unspecified } - fn damage_since(&self, _: Scale, commit: Option) -> DamageSet { + fn damage_since( + &self, + _: Scale, + commit: Option, + ) -> DamageSet { if commit == Some(self.commit) { DamageSet::default() } else { @@ -454,7 +492,8 @@ mod tests { fn layered_responses_preserve_damping_and_settle_independently() { use ferese_animation::{AnimatedValue, SpringConfig}; let base = SpringConfig::default(); - let damping_ratio = |spring: SpringConfig| spring.damping / (2.0 * (spring.mass * spring.stiffness).sqrt()); + let damping_ratio = + |spring: SpringConfig| spring.damping / (2.0 * (spring.mass * spring.stiffness).sqrt()); for spring in [emphasis_spring(base), shadow_spring(base)] { assert!((damping_ratio(spring) - damping_ratio(base)).abs() < 1e-12); assert_eq!(spring.crossing, base.crossing); @@ -482,7 +521,12 @@ mod tests { #[test] fn rounded_outline_clamps_after_physical_snapping() { // Logical clamping would give 1.125; the snapped 3px rect allows radius 1.5. - let outline = RoundedRect::new(ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0), (0, 0).into(), 1.5, 20.0); + let outline = RoundedRect::new( + ferese_layout::Rect::new(0.2, 0.2, 1.5, 4.0), + (0, 0).into(), + 1.5, + 20.0, + ); assert_eq!(outline.rect, Rectangle::new((0, 0).into(), (3, 6).into())); assert_eq!(outline.radius, 1.5); for scale in [1.0, 1.25, 1.5, 2.0] { @@ -504,7 +548,12 @@ mod tests { assert_eq!(clamp_radius(0.0, size), 0.0); assert_eq!(clamp_radius(-1.0, size), 0.0); assert_eq!(clamp_radius(2.0, (0, 10).into()), 0.0); - let outline = RoundedRect::new(ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0), (0, 0).into(), 1.5, 1.5); + let outline = RoundedRect::new( + ferese_layout::Rect::new(0.0, 0.0, 10.0, 10.0), + (0, 0).into(), + 1.5, + 1.5, + ); assert_eq!(outline.radius, 2.25); } @@ -528,7 +577,9 @@ mod tests { #[test] fn presentation_feedback_excludes_missing_and_occluded_surfaces() { - use smithay::backend::renderer::element::{RenderElementPresentationState, RenderElementState}; + use smithay::backend::renderer::element::{ + RenderElementPresentationState, RenderElementState, + }; use smithay::output::{PhysicalProperties, Subpixel}; let output = Output::new( @@ -580,9 +631,24 @@ mod tests { #[test] fn reversing_a_grow_replaces_the_smaller_snapshot() { - assert!(!snapshot_covers_source((500, 600).into(), (1000, 600).into(), 1.0, 1.0)); - assert!(snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.0)); - assert!(!snapshot_covers_source((1000, 600).into(), (500, 600).into(), 1.0, 1.8)); + assert!(!snapshot_covers_source( + (500, 600).into(), + (1000, 600).into(), + 1.0, + 1.0 + )); + assert!(snapshot_covers_source( + (1000, 600).into(), + (500, 600).into(), + 1.0, + 1.0 + )); + assert!(!snapshot_covers_source( + (1000, 600).into(), + (500, 600).into(), + 1.0, + 1.8 + )); } #[test] @@ -660,11 +726,13 @@ mod tests { fast.callback_sent(start); fast.callback_sent(start + Duration::from_millis(8)); assert_eq!( - slow.callback_deadline(start + Duration::from_millis(8), start).unwrap(), + slow.callback_deadline(start + Duration::from_millis(8), start) + .unwrap(), start + Duration::from_millis(16) ); assert_eq!( - fast.callback_deadline(start + Duration::from_millis(8), start).unwrap(), + fast.callback_deadline(start + Duration::from_millis(8), start) + .unwrap(), start + Duration::from_millis(16) ); } @@ -686,14 +754,25 @@ mod tests { fn fractional_motion_does_not_quantize_to_logical_pixels() { let rect = ferese_layout::Rect::new(0.3, 0.3, 100.2, 80.2); assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).loc, (1, 1).into()); - assert_eq!(physical_rect(rect, (0, 0).into(), 1.8).size, (180, 144).into()); + assert_eq!( + physical_rect(rect, (0, 0).into(), 1.8).size, + (180, 144).into() + ); } #[test] fn adjacent_frames_share_edges_at_all_output_scales() { for scale in [1.0, 1.25, 1.5, 1.8, 2.0] { - let left = physical_rect(ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0), (0, 0).into(), scale); - let right = physical_rect(ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), (0, 0).into(), scale); + let left = physical_rect( + ferese_layout::Rect::new(13.3, 0.0, 500.4, 800.0), + (0, 0).into(), + scale, + ); + let right = physical_rect( + ferese_layout::Rect::new(513.7, 0.0, 500.4, 800.0), + (0, 0).into(), + scale, + ); assert_eq!(left.loc.x + left.size.w, right.loc.x); } } diff --git a/compositor/src/presentation_policy.rs b/compositor/src/presentation_policy.rs index f51c8b4..ce3819e 100644 --- a/compositor/src/presentation_policy.rs +++ b/compositor/src/presentation_policy.rs @@ -15,7 +15,8 @@ fn mapped_tree(surface: &WlSurface) -> bool { let mut current = Some(surface.clone()); while let Some(surface) = current { if !surface.is_alive() - || !with_renderer_surface_state(&surface, |state| state.buffer().is_some()).unwrap_or(false) + || !with_renderer_surface_state(&surface, |state| state.buffer().is_some()) + .unwrap_or(false) { return false; } @@ -29,9 +30,12 @@ impl Ferese { let mut surfaces = Vec::new(); if self.session_lock.active() { for lock in self.session_lock.surfaces.values() { - smithay::desktop::utils::with_surfaces_surface_tree(lock.wl_surface(), |surface, _| { - surfaces.push(surface.clone()); - }); + smithay::desktop::utils::with_surfaces_surface_tree( + lock.wl_surface(), + |surface, _| { + surfaces.push(surface.clone()); + }, + ); } } else { for window in self.space.elements() { @@ -55,7 +59,12 @@ impl Ferese { let output = self .display_presentation .outputs_for(&id) - .max_by_key(|output| (output.current_mode().map_or(0, |mode| mode.refresh), output.name()))? + .max_by_key(|output| { + ( + output.current_mode().map_or(0, |mode| mode.refresh), + output.name(), + ) + })? .clone(); Some((id, output)) }) diff --git a/compositor/src/private_client.rs b/compositor/src/private_client.rs index 77f11e2..7095e27 100644 --- a/compositor/src/private_client.rs +++ b/compositor/src/private_client.rs @@ -34,8 +34,8 @@ pub(crate) fn prepare_command( command: &mut Command, capabilities: ClientCapabilities, ) -> io::Result> { - let split_shell_control = - capabilities.contains(ClientCapabilities::EFFECTS) && capabilities.contains(ClientCapabilities::SHELL_CONTROL); + let split_shell_control = capabilities.contains(ClientCapabilities::EFFECTS) + && capabilities.contains(ClientCapabilities::SHELL_CONTROL); let primary_capabilities = if split_shell_control { ClientCapabilities::EFFECTS } else { @@ -54,7 +54,10 @@ pub(crate) fn prepare_command( if split_shell_control { let control = insert_private_client(state, ClientCapabilities::SHELL_CONTROL)?; - command.env("FERESE_SHELL_CONTROL_SOCKET", control.as_raw_fd().to_string()); + command.env( + "FERESE_SHELL_CONTROL_SOCKET", + control.as_raw_fd().to_string(), + ); clients.push(control); } @@ -76,7 +79,10 @@ pub(crate) fn prepare_command( Ok(clients) } -fn insert_private_client(state: &mut Ferese, capabilities: ClientCapabilities) -> io::Result { +fn insert_private_client( + state: &mut Ferese, + capabilities: ClientCapabilities, +) -> io::Result { let (server, client) = UnixStream::pair()?; state.display_handle.insert_client( diff --git a/compositor/src/process.rs b/compositor/src/process.rs index d9ea4ee..3730fac 100644 --- a/compositor/src/process.rs +++ b/compositor/src/process.rs @@ -133,32 +133,41 @@ fn reap(child: &RefCell>) -> bool { } } -pub(crate) fn watch_client_exit(handle: &LoopHandle<'static, Ferese>, child: Child) -> Rc>> { +pub(crate) fn watch_client_exit( + handle: &LoopHandle<'static, Ferese>, + child: Child, +) -> Rc>> { let pid = child.id(); let owner = Rc::new(RefCell::new(Some(child))); let watched = owner.clone(); let result = pidfd(pid).and_then(|fd| { handle - .insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, _| { - Ok(if reap(&watched) { - PostAction::Remove - } else { - PostAction::Continue - }) - }) + .insert_source( + Generic::new(fd, Interest::READ, Mode::Level), + move |_, _, _| { + Ok(if reap(&watched) { + PostAction::Remove + } else { + PostAction::Continue + }) + }, + ) .map_err(io::Error::other) }); if let Err(error) = result { warn!(%error, pid, "could not watch client exit with pidfd; polling instead"); let watched = owner.clone(); - if let Err(error) = handle.insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, _| { - if reap(&watched) { - TimeoutAction::Drop - } else { - TimeoutAction::ToDuration(Duration::from_millis(250)) - } - }) { + if let Err(error) = handle.insert_source( + Timer::from_duration(Duration::from_millis(250)), + move |_, _, _| { + if reap(&watched) { + TimeoutAction::Drop + } else { + TimeoutAction::ToDuration(Duration::from_millis(250)) + } + }, + ) { warn!(%error, pid, "could not schedule client exit polling; stopping child"); if let Some(mut child) = owner.borrow_mut().take() { terminate_child(&mut child); diff --git a/compositor/src/process/supervisor.rs b/compositor/src/process/supervisor.rs index c11385c..e104e8c 100644 --- a/compositor/src/process/supervisor.rs +++ b/compositor/src/process/supervisor.rs @@ -51,13 +51,16 @@ impl Supervisor { let watched = pidfd(pid).and_then(|fd| { state .loop_handle - .insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, state| { - if let Some(owner) = weak.upgrade() { - owner.borrow_mut().source = None; - Self::exited(&owner, state); - } - Ok(PostAction::Remove) - }) + .insert_source( + Generic::new(fd, Interest::READ, Mode::Level), + move |_, _, state| { + if let Some(owner) = weak.upgrade() { + owner.borrow_mut().source = None; + Self::exited(&owner, state); + } + Ok(PostAction::Remove) + }, + ) .map_err(io::Error::other) }); if let Ok(token) = watched { @@ -69,23 +72,26 @@ impl Supervisor { let weak = Rc::downgrade(owner); let token = state .loop_handle - .insert_source(Timer::from_duration(Duration::from_millis(250)), move |_, _, state| { - let Some(owner) = weak.upgrade() else { - return TimeoutAction::Drop; - }; - let done = owner - .borrow_mut() - .child - .as_mut() - .is_none_or(|child| child.try_wait().ok().flatten().is_some()); - if done { - owner.borrow_mut().source = None; - Self::exited(&owner, state); - TimeoutAction::Drop - } else { - TimeoutAction::ToDuration(Duration::from_millis(250)) - } - }) + .insert_source( + Timer::from_duration(Duration::from_millis(250)), + move |_, _, state| { + let Some(owner) = weak.upgrade() else { + return TimeoutAction::Drop; + }; + let done = owner + .borrow_mut() + .child + .as_mut() + .is_none_or(|child| child.try_wait().ok().flatten().is_some()); + if done { + owner.borrow_mut().source = None; + Self::exited(&owner, state); + TimeoutAction::Drop + } else { + TimeoutAction::ToDuration(Duration::from_millis(250)) + } + }, + ) .expect("register desktop child watcher"); owner.borrow_mut().source = Some(token); } else { @@ -177,7 +183,8 @@ mod tests { if !crate::startup_tests::private_runtime(test) { return; } - crate::xwayland::test_hooks::PIDFD_UNAVAILABLE.store(polling, std::sync::atomic::Ordering::SeqCst); + crate::xwayland::test_hooks::PIDFD_UNAVAILABLE + .store(polling, std::sync::atomic::Ordering::SeqCst); let mut events = calloop::EventLoop::try_new().unwrap(); let mut state = crate::startup_tests::state(&mut events); let directory = tempfile::tempdir().unwrap(); @@ -200,30 +207,42 @@ if os.environ['FERESE_CLIENT_RESTART'] == '1': ); let deadline = Instant::now() + Duration::from_secs(5); let rows = loop { - events.dispatch(Duration::from_millis(20), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(20), &mut state) + .unwrap(); let text = std::fs::read_to_string(&record).unwrap_or_default(); let rows: Vec<_> = text.lines().map(str::to_owned).collect(); if rows.len() == 2 { break rows; } - assert!(Instant::now() < deadline, "supervised child did not restart: {text}"); + assert!( + Instant::now() < deadline, + "supervised child did not restart: {text}" + ); }; assert!(rows[0].ends_with(" 0")); assert!(rows[1].ends_with(" 1")); - assert_ne!(rows[0].split_whitespace().next(), rows[1].split_whitespace().next()); + assert_ne!( + rows[0].split_whitespace().next(), + rows[1].split_whitespace().next() + ); let child_pid = owner.borrow().child.as_ref().unwrap().id(); unsafe { libc::kill(child_pid as i32, libc::SIGKILL); } while owner.borrow().child.is_some() { - events.dispatch(Duration::from_millis(20), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(20), &mut state) + .unwrap(); assert!(Instant::now() < deadline); } assert!(owner.borrow().source.is_some(), "restart timer armed"); owner.borrow_mut().stop(&state.loop_handle); let stopped = Instant::now(); while stopped.elapsed() < Duration::from_millis(2200) { - events.dispatch(Duration::from_millis(20), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(20), &mut state) + .unwrap(); } assert_eq!(std::fs::read_to_string(record).unwrap().lines().count(), 2); assert!(owner.borrow().source.is_none()); @@ -265,7 +284,9 @@ time.sleep(60) break pid; } assert!(Instant::now() < deadline); - events.dispatch(Duration::from_millis(20), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(20), &mut state) + .unwrap(); }; owner.borrow_mut().stop(&state.loop_handle); let status_path = format!("/proc/{pid}/status"); @@ -279,7 +300,10 @@ time.sleep(60) if !running { break; } - assert!(Instant::now() < deadline, "helper descendant survived shutdown"); + assert!( + Instant::now() < deadline, + "helper descendant survived shutdown" + ); std::thread::sleep(Duration::from_millis(10)); } assert!(owner.borrow().child.is_none()); diff --git a/compositor/src/reload.rs b/compositor/src/reload.rs index 2c0e46e..fca4e4a 100644 --- a/compositor/src/reload.rs +++ b/compositor/src/reload.rs @@ -14,14 +14,19 @@ impl Prepared { Self::with_watch(source, directory, &mut Vec::new()) } - fn with_watch(source: String, directory: &std::path::Path, watched: &mut Vec) -> Result { + fn with_watch( + source: String, + directory: &std::path::Path, + watched: &mut Vec, + ) -> Result { if source.len() > 60 * 1024 { return Err("configuration exceeds the 60 KiB live-reload limit".into()); } let document = ferese_config::Document::parse(&source).map_err(|e| e.to_string())?; if let Some(value) = document.get("theme") - && let Ok(policy) = serde_json::from_value::(value.clone()) + && let Ok(policy) = + serde_json::from_value::(value.clone()) { watched.extend( [&policy.file, &policy.light.file, &policy.dark.file] @@ -76,24 +81,27 @@ impl Worker { event_loop: &mut calloop::EventLoop<'static, crate::Ferese>, ) -> Result> { let (requests, receiver) = std::sync::mpsc::sync_channel::(8); - let (results, events) = calloop::channel::channel::<(Result, Vec, Option)>(); - event_loop.handle().insert_source(events, |event, _, state| { - if let calloop::channel::Event::Msg((result, watched, reply)) = event { - state.theme_engine.watch_files(&watched); - let result = result.and_then(|prepared| state.apply_prepared_config(prepared)); - if let Err(error) = &result { - state.theme_engine.reject(error.clone()); - } + let (results, events) = + calloop::channel::channel::<(Result, Vec, Option)>(); + event_loop + .handle() + .insert_source(events, |event, _, state| { + if let calloop::channel::Event::Msg((result, watched, reply)) = event { + state.theme_engine.watch_files(&watched); + let result = result.and_then(|prepared| state.apply_prepared_config(prepared)); + if let Err(error) = &result { + state.theme_engine.reject(error.clone()); + } - if let Some((id, reply)) = reply { - let response = match result { - Ok(()) => ferese_ipc::Response::success(id, serde_json::json!({})), - Err(error) => ferese_ipc::Response::error(id, "invalid_config", error), - }; - let _ = reply.try_send(response); + if let Some((id, reply)) = reply { + let response = match result { + Ok(()) => ferese_ipc::Response::success(id, serde_json::json!({})), + Err(error) => ferese_ipc::Response::error(id, "invalid_config", error), + }; + let _ = reply.try_send(response); + } } - } - })?; + })?; std::thread::Builder::new() .name("ferese-config".into()) .spawn(move || { @@ -110,7 +118,11 @@ impl Worker { let result = path .as_ref() .map_or_else( - || request.fallback.ok_or_else(|| "No configuration available".into()), + || { + request + .fallback + .ok_or_else(|| "No configuration available".into()) + }, |path| { if path.exists() || !request.force { crate::config::read_source(path) @@ -124,7 +136,8 @@ impl Worker { // Auto mode still resolves time and system appearance. if let Some(previous) = &cached && previous.source == source - && previous.candidate.policy.mode != ferese_config::theme::Mode::Auto + && previous.candidate.policy.mode + != ferese_config::theme::Mode::Auto && file_stamps(&previous.candidate.files) == stamps { return Ok(previous.clone()); @@ -149,16 +162,23 @@ impl Worker { } impl crate::Ferese { - pub(crate) fn queue_config_reload(&self, force: bool, reply: Option) -> Result<(), String> { + pub(crate) fn queue_config_reload( + &self, + force: bool, + reply: Option, + ) -> Result<(), String> { let request = Request { force, fallback: self.config_source.clone(), reply, }; - let worker = self.config_worker.as_ref().ok_or("configuration worker unavailable")?; + let worker = self + .config_worker + .as_ref() + .ok_or("configuration worker unavailable")?; if let Err(error) = worker.requests.try_send(request) { - let (std::sync::mpsc::TrySendError::Full(request) | std::sync::mpsc::TrySendError::Disconnected(request)) = - error; + let (std::sync::mpsc::TrySendError::Full(request) + | std::sync::mpsc::TrySendError::Disconnected(request)) = error; if let Some((id, reply)) = request.reply { let _ = reply.try_send(ferese_ipc::Response::error( id, @@ -276,8 +296,9 @@ mod tests { #[test] fn wallpaper_only_reload_applies_path_and_mode_changes() { - if !crate::startup_tests::private_runtime("reload::tests::wallpaper_only_reload_applies_path_and_mode_changes") - { + if !crate::startup_tests::private_runtime( + "reload::tests::wallpaper_only_reload_applies_path_and_mode_changes", + ) { return; } let mut events = calloop::EventLoop::try_new().unwrap(); @@ -286,8 +307,12 @@ mod tests { let image_path = directory.path().join("wallpaper.png"); image::RgbaImage::new(2, 2).save(&image_path).unwrap(); for mode in ["fit", "fill"] { - let source = format!("wallpaper {{ path \"{}\"; mode \"{mode}\"; }}", image_path.display()); - let (_, runtime, candidate) = crate::config::prepare(&source, directory.path()).unwrap(); + let source = format!( + "wallpaper {{ path \"{}\"; mode \"{mode}\"; }}", + image_path.display() + ); + let (_, runtime, candidate) = + crate::config::prepare(&source, directory.path()).unwrap(); // Model a settled theme; changing only the wallpaper must still apply. state.theme_engine.live.theme = candidate.theme.clone(); state.theme_engine.live.presented = candidate.theme; @@ -310,8 +335,9 @@ mod tests { #[test] fn rejected_keymap_reload_preserves_other_configuration() { - if !crate::startup_tests::private_runtime("reload::tests::rejected_keymap_reload_preserves_other_configuration") - { + if !crate::startup_tests::private_runtime( + "reload::tests::rejected_keymap_reload_preserves_other_configuration", + ) { return; } let mut events = calloop::EventLoop::try_new().unwrap(); @@ -326,7 +352,9 @@ mod tests { // Exercise application failure after preparation, for example when an // XKB file disappears before the compositor installs the new keymap. runtime.input_settings.xkb_layout = "ferese_missing_keymap_for_reload_test".into(); - let error = state.apply_runtime_config(runtime, &serde_json::json!({})).unwrap_err(); + let error = state + .apply_runtime_config(runtime, &serde_json::json!({})) + .unwrap_err(); assert!(error.contains("keymap reload failed"), "{error}"); assert_eq!(state.xwayland_config.enabled, before_xwayland); assert_eq!(state.activities.snapshot().revision, before_activities); @@ -336,9 +364,14 @@ mod tests { let mut runtime = crate::config::Config::default().runtime_config().unwrap(); runtime.xwayland.enabled = !before_xwayland; runtime.layout_mode = ferese_core::LayoutMode::Tree; - state.apply_runtime_config(runtime, &serde_json::json!({})).unwrap(); + state + .apply_runtime_config(runtime, &serde_json::json!({})) + .unwrap(); assert_eq!(state.xwayland_config.enabled, !before_xwayland); - assert_eq!(state.workspaces.active().layout.mode(), ferese_core::LayoutMode::Tree); + assert_eq!( + state.workspaces.active().layout.mode(), + ferese_core::LayoutMode::Tree + ); assert!(state.activities.snapshot().revision > before_activities); } @@ -379,9 +412,14 @@ mod tests { std::fs::create_dir_all(path.parent().unwrap()).unwrap(); let valid = "animations { speed 0.5; }"; std::fs::write(&path, valid).unwrap(); - for (index, source) in [Some(valid), Some(valid), Some("animations { speed -1.0; }"), None] - .into_iter() - .enumerate() + for (index, source) in [ + Some(valid), + Some(valid), + Some("animations { speed -1.0; }"), + None, + ] + .into_iter() + .enumerate() { if let Some(source) = source { std::fs::write(&path, source).unwrap(); @@ -392,7 +430,9 @@ mod tests { let before = state.ipc_events.config_revision; let theme_before = state.theme_engine.live.theme.clone(); let (reply, received) = std::sync::mpsc::sync_channel(1); - state.queue_config_reload(index == 1, Some((42, reply))).unwrap(); + state + .queue_config_reload(index == 1, Some((42, reply))) + .unwrap(); if index == 1 { // Present a frame while the duplicate reload is queued. Theme // revisions include these frames; config revisions do not. @@ -401,7 +441,9 @@ mod tests { } let deadline = Instant::now() + Duration::from_secs(5); let response = loop { - event_loop.dispatch(Duration::from_millis(10), &mut state).unwrap(); + event_loop + .dispatch(Duration::from_millis(10), &mut state) + .unwrap(); if let Ok(response) = received.try_recv() { break response; } @@ -415,7 +457,10 @@ mod tests { ); assert_eq!(state.config_source.as_deref(), Some(valid)); let applied = source == Some(valid) && !already_applied; - assert_eq!(state.ipc_events.config_revision, before + u64::from(applied)); + assert_eq!( + state.ipc_events.config_revision, + before + u64::from(applied) + ); if already_applied { assert_eq!(state.theme_engine.live.theme, theme_before); } @@ -460,8 +505,9 @@ mod tests { state.theme_engine.live.presented = candidate.theme; for inactive_opacity in [1.0, 0.75, 0.9] { - let source = - format!("appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}"); + let source = format!( + "appearance {{ focus-effect {{ inactive-opacity {inactive_opacity}; inactive-dim 0.25; }}; }}" + ); state.reload_config_source(source).unwrap(); assert_eq!(state.focus_effect.inactive_opacity, inactive_opacity); let (alpha, dim) = state.focus_effect.resolve(0.0, true, 0.0); @@ -486,7 +532,10 @@ mod tests { ); assert_eq!(state.focus_effect, previous); state - .reload_config_source("appearance { focus-effect { inactive-dim 0; }; }; theme { mode \"light\"; }".into()) + .reload_config_source( + "appearance { focus-effect { inactive-dim 0; }; }; theme { mode \"light\"; }" + .into(), + ) .unwrap(); state.poll_theme(); assert_eq!(state.focus_effect.inactive_dim, 0.0); @@ -514,12 +563,17 @@ mod tests { .env_remove("FERESE_SOCKET") .output() .unwrap(); - assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr)); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); return; } let directory = Directory::new(); - let (_, runtime, _) = crate::config::prepare("xwayland { enabled #false; }", &directory.0).unwrap(); + let (_, runtime, _) = + crate::config::prepare("xwayland { enabled #false; }", &directory.0).unwrap(); let mut event_loop = smithay::reexports::calloop::EventLoop::try_new().unwrap(); let display = smithay::reexports::wayland_server::Display::new().unwrap(); let mut state = crate::Ferese::new(&mut event_loop, display, runtime).unwrap(); @@ -563,7 +617,11 @@ mod tests { .env_remove("FERESE_SOCKET") .output() .unwrap(); - assert!(result.status.success(), "{}", String::from_utf8_lossy(&result.stderr)); + assert!( + result.status.success(), + "{}", + String::from_utf8_lossy(&result.stderr) + ); return; } let directory = Directory::new(); diff --git a/compositor/src/render/backdrop.rs b/compositor/src/render/backdrop.rs index 3e897e3..fd1e568 100644 --- a/compositor/src/render/backdrop.rs +++ b/compositor/src/render/backdrop.rs @@ -39,7 +39,10 @@ impl Backdrop { continue; } - let previous = self.sources.iter().find(|source| &source.id == element.id()); + let previous = self + .sources + .iter() + .find(|source| &source.id == element.id()); changed |= previous.is_none_or(|source| { source.geometry != geometry || source.alpha != element.alpha() @@ -48,10 +51,13 @@ impl Backdrop { || source.visible != visible }); if let Some(previous) = previous { - changed |= element.damage_since(scale, Some(previous.commit)).iter().any(|damage| { - let damage = Rectangle::new(damage.loc + geometry.loc, damage.size); - visible.iter().any(|rect| rect.overlaps(damage)) - }); + changed |= element + .damage_since(scale, Some(previous.commit)) + .iter() + .any(|damage| { + let damage = Rectangle::new(damage.loc + geometry.loc, damage.size); + visible.iter().any(|rect| rect.overlaps(damage)) + }); } sources.push(Source { @@ -163,7 +169,9 @@ mod tests { } fn current_commit(&self) -> CommitCounter { if let Some(backdrop) = &self.probe { - let _guard = backdrop.try_lock().expect("backdrop lock held during an element query"); + let _guard = backdrop + .try_lock() + .expect("backdrop lock held during an element query"); } self.commit } @@ -173,7 +181,11 @@ mod tests { fn geometry(&self, _: RenderScale) -> Rectangle { self.rect } - fn damage_since(&self, _: RenderScale, commit: Option) -> DamageSet { + fn damage_since( + &self, + _: RenderScale, + commit: Option, + ) -> DamageSet { if commit == Some(self.commit) { DamageSet::default() } else { @@ -209,13 +221,31 @@ mod tests { // Open: ignore a repeated instance of the cached popup material, and // allow dependencies to query backdrop state without a nested lock. - assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); + assert!(update_shared( + &backdrop, + &elements, + &owner, + region, + 1.0.into() + )); assert_eq!(backdrop.lock().unwrap().sources.len(), 1); - assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into())); + assert!(!update_shared( + &backdrop, + &elements, + &owner, + region, + 1.0.into() + )); // Update both content and bounds while the popup remains visible. elements[1].repaint(vec![rect(10, 10, 5, 5)]); - assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); + assert!(update_shared( + &backdrop, + &elements, + &owner, + region, + 1.0.into() + )); assert!(update_shared( &backdrop, &elements, @@ -225,10 +255,28 @@ mod tests { )); // Close and reopen: dependencies disappear, then are tracked again. - assert!(update_shared::(&backdrop, &[], &owner, region, 1.0.into())); + assert!(update_shared::( + &backdrop, + &[], + &owner, + region, + 1.0.into() + )); assert!(backdrop.lock().unwrap().sources.is_empty()); - assert!(update_shared(&backdrop, &elements, &owner, region, 1.0.into())); - assert!(!update_shared(&backdrop, &elements, &owner, region, 1.0.into())); + assert!(update_shared( + &backdrop, + &elements, + &owner, + region, + 1.0.into() + )); + assert!(!update_shared( + &backdrop, + &elements, + &owner, + region, + 1.0.into() + )); } #[test] diff --git a/compositor/src/render/capture.rs b/compositor/src/render/capture.rs index d99ed4b..a9cf8aa 100644 --- a/compositor/src/render/capture.rs +++ b/compositor/src/render/capture.rs @@ -33,10 +33,14 @@ fn render_window_texture( if bytes == 0 || bytes > remaining { return Ok(None); } - let content = render_elements_from_surface_tree::>( + let content = render_elements_from_surface_tree::< + GlesRenderer, + WaylandSurfaceRenderElement, + >( renderer, toplevel.wl_surface(), - Point::::from((-geometry.loc.x, -geometry.loc.y)).to_physical_precise_round(scale), + Point::::from((-geometry.loc.x, -geometry.loc.y)) + .to_physical_precise_round(scale), scale, 1.0, RenderElementKind::Unspecified, @@ -46,7 +50,11 @@ fn render_window_texture( } let mut texture = match reusable.filter(|texture| texture.size() == (size.w, size.h).into()) { Some(texture) => texture, - None => Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, (size.w, size.h).into())?, + None => Offscreen::::create_buffer( + renderer, + Fourcc::Abgr8888, + (size.w, size.h).into(), + )?, }; { let mut target = renderer.bind(&mut texture)?; @@ -93,13 +101,22 @@ pub(crate) fn capture_window_frame( .get_or_insert_threadsafe(|| Arc::new(CaptureTarget::default())) .clone(); let reusable = cache.0.lock().unwrap().take(); - let mut snapshot = render_window_texture(renderer, window, geometry, scale, 128 * 1024 * 1024, reusable) - .map_err(|error| error.to_string())? - .ok_or("Window has no capturable content")?; + let mut snapshot = render_window_texture( + renderer, + window, + geometry, + scale, + 128 * 1024 * 1024, + reusable, + ) + .map_err(|error| error.to_string())? + .ok_or("Window has no capturable content")?; let size = snapshot.texture.size(); if let Some((state, cursor_geometry)) = cursor { let elements = cursor_elements(state, renderer, cursor_geometry, scale); - let mut target = renderer.bind(&mut snapshot.texture).map_err(|e| e.to_string())?; + let mut target = renderer + .bind(&mut snapshot.texture) + .map_err(|e| e.to_string())?; let mut frame = renderer .render(&mut target, (size.w, size.h).into(), Transform::Normal) .map_err(|e| e.to_string())?; @@ -113,7 +130,11 @@ pub(crate) fn capture_window_frame( let _ = frame.finish().map_err(|e| e.to_string())?; } let mapping = renderer - .copy_texture(&snapshot.texture, Rectangle::from_size(size), Fourcc::Abgr8888) + .copy_texture( + &snapshot.texture, + Rectangle::from_size(size), + Fourcc::Abgr8888, + ) .map_err(|error| error.to_string())?; if mapping.format() != Some(Fourcc::Abgr8888) { return Err("Unsupported window readback format".into()); @@ -125,7 +146,9 @@ pub(crate) fn capture_window_frame( // Smithay maps its PBO here and can wait for the GPU. Nonblocking readback // needs a retained mapping plus an EGL-context-owned fence, polled before // mapping; moving this call to the conversion worker is not context-safe. - let source = renderer.map_texture(&mapping).map_err(|error| error.to_string())?; + let source = renderer + .map_texture(&mapping) + .map_err(|error| error.to_string())?; if source.len() < bytes { return Err("Incomplete window readback".into()); } @@ -164,7 +187,9 @@ mod tests { convert_window_pixels(&mut pixels); assert_eq!( pixels, - [30, 20, 10, 255, 255, 128, 64, 128, 0, 0, 0, 0, 255, 255, 255, 1] + [ + 30, 20, 10, 255, 255, 128, 64, 128, 0, 0, 0, 0, 255, 255, 255, 1 + ] ); } } diff --git a/compositor/src/render/closing.rs b/compositor/src/render/closing.rs index da346f1..eaefacf 100644 --- a/compositor/src/render/closing.rs +++ b/compositor/src/render/closing.rs @@ -26,7 +26,11 @@ pub(crate) struct ClosedWindow { impl ClosedWindow { pub fn bytes(&self) -> usize { - self.snapshot.bytes() + self.handoff.as_ref().map_or(0, |(snapshot, _)| snapshot.bytes()) + self.snapshot.bytes() + + self + .handoff + .as_ref() + .map_or(0, |(snapshot, _)| snapshot.bytes()) } pub fn advance(&mut self, delta: Duration, spring: SpringConfig) -> bool { @@ -82,7 +86,9 @@ pub(super) fn grouped_elements( .render .closing .iter() - .filter(|window| window.output == output_id && window.snapshot.context == renderer.context_id().erased()) + .filter(|window| { + window.output == output_id && window.snapshot.context == renderer.context_id().erased() + }) .cloned() .collect(); if windows.is_empty() { @@ -97,7 +103,8 @@ pub(super) fn grouped_elements( window.advance(delta, state.spring_config); } let visual = window.presentation.bounds.current; - let corners = RoundedRect::new(visual, output_geometry.loc, scale, window.radius).with_shape(window.shape); + let corners = RoundedRect::new(visual, output_geometry.loc, scale, window.radius) + .with_shape(window.shape); let constrain = rounded_visual_rect(visual, output_geometry.loc); let Some(programs) = corner_program(&mut state.render, renderer, window.shape) else { continue; @@ -124,7 +131,8 @@ pub(super) fn grouped_elements( constrain, corners, scale, - theme.border_width + (theme.focus_ring_width - theme.border_width) * window.presentation.focus(), + theme.border_width + + (theme.focus_ring_width - theme.border_width) * window.presentation.focus(), theme.border_color.0, theme.border_gradient, window.presentation.focus() as f32, @@ -196,8 +204,13 @@ pub(super) fn grouped_elements( elements.push(material); } if window.material.is_none() - && let Some(blur) = - super::window_blur::window_blur_element(state, renderer, output, window.presentation, corners) + && let Some(blur) = super::window_blur::window_blur_element( + state, + renderer, + output, + window.presentation, + corners, + ) { elements.push(blur); } diff --git a/compositor/src/render/corner_tests.rs b/compositor/src/render/corner_tests.rs index e21ae14..10707c3 100644 --- a/compositor/src/render/corner_tests.rs +++ b/compositor/src/render/corner_tests.rs @@ -219,7 +219,9 @@ fn window_corner_shader_timing_sample() { Uniform::new("clip_rect", [0.0f32, 0.0, 1024.0, 768.0]), Uniform::new("radius", 16.0f32), ]; - let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap(); + let mut texture: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (1024, 768).into()) + .unwrap(); for shape in [CornerShape::Circular, CornerShape::Continuous] { let programs = corner_program(&mut resources, &mut renderer, shape).unwrap(); @@ -228,7 +230,9 @@ fn window_corner_shader_timing_sample() { for iteration in 0..110 { let started = std::time::Instant::now(); let mut target = renderer.bind(&mut texture).unwrap(); - let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); + let mut frame = renderer + .render(&mut target, size, Transform::Normal) + .unwrap(); frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap(); frame .render_pixel_shader_to( @@ -256,7 +260,11 @@ fn window_corner_shader_timing_sample() { } } -fn rasterize(renderer: &mut GlesRenderer, program: &GlesPixelProgram, uniforms: &[Uniform<'_>]) -> Vec { +fn rasterize( + renderer: &mut GlesRenderer, + program: &GlesPixelProgram, + uniforms: &[Uniform<'_>], +) -> Vec { let size = (64, 64).into(); let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); let rect = Rectangle::::from_size((64, 64).into()); @@ -294,7 +302,8 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() { use smithay::backend::renderer::gles::UniformValue; let mut renderer = renderer(); let mut resources = RenderResources::default(); - let program = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); + let program = + material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let bounds = ferese_layout::Rect::new(0., 0., 64., 64.); let groups = [ GroupBounds { @@ -346,7 +355,19 @@ fn panel_shader_matches_core_outlines_and_resets_for_popovers() { let matrices = scene .packets() .into_iter() - .map(|p| [p[0], p[1], p[2], p[3], p[4], p[5], p[6], p[7], p[8] * 4. + p[9]]) + .map(|p| { + [ + p[0], + p[1], + p[2], + p[3], + p[4], + p[5], + p[6], + p[7], + p[8] * 4. + p[9], + ] + }) .collect::>(); let mut uniforms = vec![ Uniform::new("visible_rect", [0.0f32, 0., 64., 64.]), @@ -441,7 +462,10 @@ fn apple_window_fill_and_inset_border_match_path_reference_at_fractional_scales( let inner = coverage(distance + f64::from(width)); let index = (y * 64 + x) * 4; - for (actual, expected) in [(fill[index + 3], outer), (edge[index + 3], (outer - inner).max(0.0))] { + for (actual, expected) in [ + (fill[index + 3], outer), + (edge[index + 3], (outer - inner).max(0.0)), + ] { let expected = (expected * 255.0).round() as u8; assert!( actual.abs_diff(expected) <= 2, @@ -510,9 +534,12 @@ fn apple_shoulder_extent_never_claims_transparent_pixels_as_opaque() { fn docked_panel_fill_covers_the_entire_attached_edge() { let mut renderer = renderer(); let mut resources = RenderResources::default(); - let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); + let material = + material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); for edge in [ferese_panel::Edge::Top, ferese_panel::Edge::Bottom] { - let radii = ferese_panel::CornerRadii([14., 8., 12., 20.]).at_edge(edge, true).0; + let radii = ferese_panel::CornerRadii([14., 8., 12., 20.]) + .at_edge(edge, true) + .0; let pixels = rasterize( &mut renderer, &material.0, @@ -537,7 +564,11 @@ fn docked_panel_fill_covers_the_entire_attached_edge() { "wallpaper exposed at {edge:?} pixel {x}" ); } - assert_eq!(pixels[free_row * 64 * 4 + 3], 0, "opposite corners remain rounded"); + assert_eq!( + pixels[free_row * 64 * 4 + 3], + 0, + "opposite corners remain rounded" + ); } } @@ -546,7 +577,8 @@ fn docked_panel_fill_covers_the_entire_attached_edge() { fn asymmetric_panel_fill_and_shadow_match_each_corner() { let mut renderer = renderer(); let mut resources = RenderResources::default(); - let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); + let material = + material_program_for_corners(&mut resources, &mut renderer, CornerShape::Circular).unwrap(); let rect = [2.0f32, 2.0, 60.0, 60.0]; let radii = [0.0f32, 4.0, 12.0, 20.0]; for paint_mode in [0.0f32, 2.0] { @@ -596,7 +628,9 @@ fn apple_shadow_and_window_tint_match_the_same_outline() { let mut renderer = renderer(); let mut resources = RenderResources::default(); let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); - let material = material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); + let material = + material_program_for_corners(&mut resources, &mut renderer, CornerShape::Continuous) + .unwrap(); let rect = [1.25f32, 2.5, 61.5, 59.0]; let radius = 12.375f32; let shadow = rasterize( @@ -634,7 +668,10 @@ fn apple_shadow_and_window_tint_match_the_same_outline() { let shadow_alpha = (-0.5 * (distance.max(0.0) / 4.0).powi(2)).exp(); let index = (y * 64 + x) * 4; - for (actual, expected) in [(tint[index + 3], coverage(distance)), (shadow[index + 3], shadow_alpha)] { + for (actual, expected) in [ + (tint[index + 3], coverage(distance)), + (shadow[index + 3], shadow_alpha), + ] { assert!( actual.abs_diff((expected * 255.0).round() as u8) <= 2, "outline mismatch at ({x}, {y})" @@ -680,8 +717,16 @@ fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot( .unwrap() .borders .contexts - .insert(context.clone(), CachedBorder { element, parameters }); - let texture = renderer.create_buffer(Fourcc::Abgr8888, (64, 64).into()).unwrap(); + .insert( + context.clone(), + CachedBorder { + element, + parameters, + }, + ); + let texture = renderer + .create_buffer(Fourcc::Abgr8888, (64, 64).into()) + .unwrap(); resources.set_snapshot( id, ResizeSnapshot { @@ -711,7 +756,10 @@ fn window_role_change_invalidates_decorations_and_preserves_the_resize_snapshot( fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) { if end { ( - [3.0 * (points[3][0] - points[2][0]), 3.0 * (points[3][1] - points[2][1])], + [ + 3.0 * (points[3][0] - points[2][0]), + 3.0 * (points[3][1] - points[2][1]), + ], [ 6.0 * (points[3][0] - 2.0 * points[2][0] + points[1][0]), 6.0 * (points[3][1] - 2.0 * points[2][1] + points[1][1]), @@ -719,7 +767,10 @@ fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) { ) } else { ( - [3.0 * (points[1][0] - points[0][0]), 3.0 * (points[1][1] - points[0][1])], + [ + 3.0 * (points[1][0] - points[0][0]), + 3.0 * (points[1][1] - points[0][1]), + ], [ 6.0 * (points[2][0] - 2.0 * points[1][0] + points[0][0]), 6.0 * (points[2][1] - 2.0 * points[1][1] + points[0][1]), @@ -729,7 +780,8 @@ fn endpoint_derivatives(points: Cubic, end: bool) -> (Point2, Point2) { } fn curvature(tangent: Point2, acceleration: Point2) -> f64 { - (tangent[0] * acceleration[1] - tangent[1] * acceleration[0]).abs() / tangent[0].hypot(tangent[1]).powi(3) + (tangent[0] * acceleration[1] - tangent[1] * acceleration[0]).abs() + / tangent[0].hypot(tangent[1]).powi(3) } #[test] @@ -745,10 +797,12 @@ fn measured_uikit_profile_has_flat_shoulders_but_small_internal_join_discontinui assert_eq!(pair[0][3], pair[1][0], "join must be position-continuous"); let (before, before_acceleration) = endpoint_derivatives(pair[0], true); let (after, after_acceleration) = endpoint_derivatives(pair[1], false); - let cosine = - (before[0] * after[0] + before[1] * after[1]) / before[0].hypot(before[1]) / after[0].hypot(after[1]); + let cosine = (before[0] * after[0] + before[1] * after[1]) + / before[0].hypot(before[1]) + / after[0].hypot(after[1]); let angle = cosine.clamp(-1.0, 1.0).acos().to_degrees(); - let curvature_jump = (curvature(before, before_acceleration) - curvature(after, after_acceleration)).abs(); + let curvature_jump = + (curvature(before, before_acceleration) - curvature(after, after_acceleration)).abs(); // This is a measured Apple-style approximation, not a globally G1/G2 // curve. Preserve and document its limitation instead of falsely // passing a continuity test, or silently changing the source profile. @@ -767,7 +821,9 @@ fn corner_heavy_shader_timing_sample() { let current = include_str!("shaders/window_corners.glsl"); let size = (1024, 768).into(); let rect = Rectangle::::from_size(size); - let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (1024, 768).into()).unwrap(); + let mut texture: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (1024, 768).into()) + .unwrap(); let body = "\nvoid main() { float d = rounded_rect_distance(mod(gl_FragCoord.xy, vec2(64.0)), vec4(0.0, 0.0, 64.0, 64.0), radius); gl_FragColor = vec4(edge_coverage(d)); }"; for (name, source) in reference .as_deref() @@ -784,7 +840,9 @@ fn corner_heavy_shader_timing_sample() { for i in 0..110 { let start = std::time::Instant::now(); let mut target = renderer.bind(&mut texture).unwrap(); - let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); + let mut frame = renderer + .render(&mut target, size, Transform::Normal) + .unwrap(); frame.clear(Color32F::TRANSPARENT, &[rect]).unwrap(); frame .render_pixel_shader_to( @@ -817,7 +875,9 @@ fn window_tint_does_not_inherit_panel_geometry() { use smithay::backend::renderer::gles::UniformValue; use smithay::output::{Mode, PhysicalProperties, Subpixel}; fn paint(renderer: &mut GlesRenderer, element: &impl RenderElement) -> Vec { - let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (96, 96).into()).unwrap(); + let mut texture: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (96, 96).into()) + .unwrap(); let mut target = renderer.bind(&mut texture).unwrap(); let rect = Rectangle::from_size((96, 96).into()); let mut frame = renderer @@ -837,7 +897,11 @@ fn window_tint_does_not_inherit_panel_geometry() { .unwrap(); let _ = frame.finish().unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((96, 96).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((96, 96).into()), + Fourcc::Abgr8888, + ) .unwrap(); renderer.map_texture(&mapping).unwrap().to_vec() } @@ -863,7 +927,8 @@ fn window_tint_does_not_inherit_panel_geometry() { Some((0, 0).into()), ); let geometry = Rectangle::new((6, 6).into(), (80, 80).into()); - let corners = RoundedRect::from_logical(geometry, 1.0, 12.0).with_shape(CornerShape::Continuous); + let corners = + RoundedRect::from_logical(geometry, 1.0, 12.0).with_shape(CornerShape::Continuous); for resize_fill in [false, true] { let tint = window_tint_element( &mut resources, @@ -949,14 +1014,23 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() { ]; let rect = Rectangle::::new( (capture[0] as i32, capture[1] as i32).into(), - ((capture[2] - capture[0]) as i32, (capture[3] - capture[1]) as i32).into(), + ( + (capture[2] - capture[0]) as i32, + (capture[3] - capture[1]) as i32, + ) + .into(), ); let mut pixels = vec![128_u8; (rect.size.w * rect.size.h * 4) as usize]; for p in pixels.chunks_exact_mut(4) { p[3] = 255; } let source = renderer - .import_memory(&pixels, Fourcc::Abgr8888, (rect.size.w, rect.size.h).into(), false) + .import_memory( + &pixels, + Fourcc::Abgr8888, + (rect.size.w, rect.size.h).into(), + false, + ) .unwrap(); let uniforms = vec![ Uniform::new("visible_rect", visible), @@ -981,10 +1055,15 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() { .create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()) .unwrap(); let mut target = renderer.bind(&mut texture).unwrap(); - let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); + let mut frame = renderer + .render(&mut target, size, Transform::Normal) + .unwrap(); frame.clear([0.; 4].into(), &[full]).unwrap(); // OpenGL coordinates have the opposite Y direction from the frame. - let dst = Rectangle::new((rect.loc.x, size.h - rect.loc.y - rect.size.h).into(), rect.size); + let dst = Rectangle::new( + (rect.loc.x, size.h - rect.loc.y - rect.size.h).into(), + rect.size, + ); frame .render_texture_from_to( &source, @@ -1000,7 +1079,11 @@ fn floating_blur_sampling_halo_does_not_paint_neighboring_islands() { .unwrap(); let _ = frame.finish().unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((size.w, size.h).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((size.w, size.h).into()), + Fourcc::Abgr8888, + ) .unwrap(); let rendered = renderer.map_texture(&mapping).unwrap().to_vec(); let mut painted = 0; diff --git a/compositor/src/render/decorations.rs b/compositor/src/render/decorations.rs index ec6397e..55cdbe0 100644 --- a/compositor/src/render/decorations.rs +++ b/compositor/src/render/decorations.rs @@ -13,10 +13,14 @@ pub(super) fn corner_program( shape: CornerShape, ) -> Option { let context = renderer.context_id().erased(); - if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape { - CornerShape::Circular => programs.rounded.as_ref(), - CornerShape::Continuous => programs.continuous.as_ref(), - }) { + if let Some(program) = resources + .contexts + .get(&context) + .and_then(|programs| match shape { + CornerShape::Circular => programs.rounded.as_ref(), + CornerShape::Continuous => programs.continuous.as_ref(), + }) + { return Some(program.clone()); } @@ -43,11 +47,18 @@ pub(super) fn corner_program( UniformName::new("opacity", UniformType::_1f), UniformName::new("shadow_color", UniformType::_4f), ]; - let texture = - renderer.compile_custom_texture_shader(corner_shader_for(ROUNDED_TEXTURE_SHADER, shape), &texture_uniforms); - let border = - renderer.compile_custom_pixel_shader(corner_shader_for(ROUNDED_BORDER_SHADER, shape), &border_uniforms); - let shadow = renderer.compile_custom_pixel_shader(corner_shader_for(WINDOW_SHADOW_SHADER, shape), &shadow_uniforms); + let texture = renderer.compile_custom_texture_shader( + corner_shader_for(ROUNDED_TEXTURE_SHADER, shape), + &texture_uniforms, + ); + let border = renderer.compile_custom_pixel_shader( + corner_shader_for(ROUNDED_BORDER_SHADER, shape), + &border_uniforms, + ); + let shadow = renderer.compile_custom_pixel_shader( + corner_shader_for(WINDOW_SHADOW_SHADER, shape), + &shadow_uniforms, + ); let solid = renderer.compile_custom_pixel_shader( corner_shader_for(ROUNDED_SOLID_SHADER, shape), &[ @@ -57,7 +68,9 @@ pub(super) fn corner_program( ], ); let compiled = texture.and_then(|texture| { - border.and_then(|border| shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid)))) + border.and_then(|border| { + shadow.and_then(|shadow| solid.map(|solid| (texture, border, shadow, solid))) + }) }); match compiled { Ok((texture, border, shadow, solid)) => { @@ -130,13 +143,22 @@ pub(super) fn window_border_element( Some(g) => ( g.from.0, g.to.0, - border_gradient_line(corners.rect, mode.size, output.current_transform().invert(), g.angle), + border_gradient_line( + corners.rect, + mode.size, + output.current_transform().invert(), + g.angle, + ), ), None => (theme.accent_color.0, theme.accent_color.0, [0.0; 4]), }; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + clip_rect: framebuffer_clip_rect( + corners.rect, + mode.size, + output.current_transform().invert(), + ), radius: corners.radius, width, color: color_with_alpha(from, opacity), @@ -235,7 +257,11 @@ pub(super) fn window_tint_element( let program = material_program_for_corners(resources, renderer, corners.shape)?; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + clip_rect: framebuffer_clip_rect( + corners.rect, + mode.size, + output.current_transform().invert(), + ), radius: corners.radius, width: 0.0, color, @@ -253,17 +279,20 @@ pub(super) fn window_tint_element( } else { &mut resources.windows.entry(id).or_default().dim }; - let cached = buffers.contexts.entry(context).or_insert_with(|| CachedBorder { - element: SharedPixelShaderElement::new( - program.0, - geometry, - None, - 1.0, - uniforms(¶meters), - RenderElementKind::Unspecified, - ), - parameters: parameters.clone(), - }); + let cached = buffers + .contexts + .entry(context) + .or_insert_with(|| CachedBorder { + element: SharedPixelShaderElement::new( + program.0, + geometry, + None, + 1.0, + uniforms(¶meters), + RenderElementKind::Unspecified, + ), + parameters: parameters.clone(), + }); if cached.parameters != parameters { if cached.parameters.geometry != geometry { cached.element.resize(geometry, None); @@ -311,7 +340,11 @@ pub(super) fn window_shadow_element( let parameters = ShadowParameters { blur: (blur * scale) as f32, bounds, - shadow_rect: framebuffer_clip_rect(shadow_geometry, mode.size, output.current_transform().invert()), + shadow_rect: framebuffer_clip_rect( + shadow_geometry, + mode.size, + output.current_transform().invert(), + ), radius: corners.radius, opacity: opacity as f32, color, @@ -349,8 +382,16 @@ pub(super) fn window_shadow_element( Some(PhysicalShaderElement { inner: cached.element.clone(), geometry: Rectangle::new( - (shadow_geometry.loc.x - extent, shadow_geometry.loc.y - extent).into(), - (shadow_geometry.size.w + 2 * extent, shadow_geometry.size.h + 2 * extent).into(), + ( + shadow_geometry.loc.x - extent, + shadow_geometry.loc.y - extent, + ) + .into(), + ( + shadow_geometry.size.w + 2 * extent, + shadow_geometry.size.h + 2 * extent, + ) + .into(), ), }) } @@ -365,7 +406,11 @@ pub(super) fn shadow_uniforms(parameters: &ShadowParameters) -> Vec, offset_y: f64, blur: f64) -> Rectangle { +pub(super) fn shadow_bounds( + geometry: Rectangle, + offset_y: f64, + blur: f64, +) -> Rectangle { let extent = (blur * 2.0).ceil() as i32; let offset_y = offset_y.round() as i32; @@ -399,21 +444,30 @@ pub(super) fn rounded_window_elements( let geometry = window.geometry(); let mut reference = geometry.to_physical_precise_round(scale); let behavior = if let Some(size) = native_size { - reference.size = Size::::from((size.width, size.height)).to_physical_precise_round(scale); + reference.size = + Size::::from((size.width, size.height)).to_physical_precise_round(scale); ConstrainScaleBehavior::Stretch } else { behavior }; let location = physical_constrain.loc - geometry.loc.to_physical_precise_round(scale); - let clip = framebuffer_clip_rect(physical_constrain, mode.size, output.current_transform().invert()); + let clip = framebuffer_clip_rect( + physical_constrain, + mode.size, + output.current_transform().invert(), + ); let radius = corners.radius; let surface = toplevel.wl_surface(); let mut content = PopupManager::popups_for_surface(surface) .flat_map(|(popup, popup_offset)| { - let offset = (geometry.loc + popup_offset - popup.geometry().loc).to_physical_precise_round(scale); + let offset = (geometry.loc + popup_offset - popup.geometry().loc) + .to_physical_precise_round(scale); - render_elements_from_surface_tree::>( + render_elements_from_surface_tree::< + GlesRenderer, + WaylandSurfaceRenderElement, + >( renderer, popup.wl_surface(), location + offset, diff --git a/compositor/src/render/materials.rs b/compositor/src/render/materials.rs index dfc0c8f..f8fd2f8 100644 --- a/compositor/src/render/materials.rs +++ b/compositor/src/render/materials.rs @@ -12,27 +12,34 @@ pub(super) fn append_material_surface( output_crop: Rectangle, elements: &mut Vec, ) { - let radius = - if crate::effects::surface_role(surface).is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel) { - state.theme_settings.panel_radius - } else { - state.theme_settings.material_radius - }; + let radius = if crate::effects::surface_role(surface) + .is_some_and(|(role, _)| role == crate::effects::SemanticRole::Panel) + { + state.theme_settings.panel_radius + } else { + state.theme_settings.material_radius + }; let regions = crate::effects::surface_regions(surface); let opacities = crate::effects::surface_region_opacities(surface); let targets: Vec<_> = match ®ions { - None => vec![(geometry, RoundedRect::from_logical(geometry, scale, radius), 1.0)], + None => vec![( + geometry, + RoundedRect::from_logical(geometry, scale, radius), + 1.0, + )], Some(regions) => regions .iter() .enumerate() .filter_map(|(index, r)| { - material_region_geometry(*r, geometry, content_origin, scale).map(|(rect, corners)| { - ( - rect, - corners, - opacities.get(index).copied().unwrap_or(1000) as f32 / 1000.0, - ) - }) + material_region_geometry(*r, geometry, content_origin, scale).map( + |(rect, corners)| { + ( + rect, + corners, + opacities.get(index).copied().unwrap_or(1000) as f32 / 1000.0, + ) + }, + ) }) .collect(), }; @@ -63,8 +70,12 @@ pub(super) fn append_material_surface( }) .collect(); if let Some(buffers) = state.render.surfaces.get_mut(surface) { - buffers.contexts.retain(|(_, _, index), _| *index < targets.len()); - buffers.captures.retain(|(_, _, index), _| *index < targets.len()); + buffers + .contexts + .retain(|(_, _, index), _| *index < targets.len()); + buffers + .captures + .retain(|(_, _, index), _| *index < targets.len()); } let material = (!materials.is_empty() && regions.is_none()).then_some(()); let clip = material.as_ref().and_then(|_| { @@ -78,7 +89,10 @@ pub(super) fn append_material_surface( )) }); // Smithay element lists are front to back: content, material, shadow. - let content = render_elements_from_surface_tree::>( + let content = render_elements_from_surface_tree::< + GlesRenderer, + WaylandSurfaceRenderElement, + >( renderer, surface, content_origin.to_physical_precise_round(scale), @@ -118,7 +132,11 @@ fn material_region_geometry( scale: f64, ) -> Option<(Rectangle, RoundedRect)> { let rect = Rectangle::::new( - (f64::from(origin.x) + region[0], f64::from(origin.y) + region[1]).into(), + ( + f64::from(origin.x) + region[0], + f64::from(origin.y) + region[1], + ) + .into(), (region[2], region[3]).into(), ) .intersection(geometry.to_f64())?; @@ -135,7 +153,10 @@ fn material_region_geometry( fn framebuffer_corner_radii(radii: [f32; 4], transform: Transform) -> [f32; 4] { let size = Size::::from((1., 1.)); let mut transformed = [0.; 4]; - for (radius, point) in radii.into_iter().zip([(0., 0.), (1., 0.), (1., 1.), (0., 1.)]) { + for (radius, point) in radii + .into_iter() + .zip([(0., 0.), (1., 0.), (1., 1.), (0., 1.)]) + { let point = transform.transform_point_in(Point::::from(point), &size); let index = match (point.x > 0.5, point.y > 0.5) { (false, false) => 0, @@ -181,7 +202,11 @@ fn resolve_surface_material( panel_opacity: Option, panel_style: ferese_panel::PanelStyle, ) -> crate::effects::ResolvedMaterial { - let mut material = crate::effects::resolve_material(role, settings.material_style, settings.shell_opacity as f32); + let mut material = crate::effects::resolve_material( + role, + settings.material_style, + settings.shell_opacity as f32, + ); if role == crate::effects::SemanticRole::Panel { material.opacity = ferese_config::theme::effective_panel_opacity( material.opacity, @@ -226,7 +251,8 @@ pub(super) fn material_element_with_role( } else { state.theme_settings.surface_base_color }; - let morph = role == crate::effects::SemanticRole::Panel && state.panel_style == ferese_panel::PanelStyle::Morph; + let morph = role == crate::effects::SemanticRole::Panel + && state.panel_style == ferese_panel::PanelStyle::Morph; let background = if morph { state.theme_settings.surface_raised_color } else { @@ -250,8 +276,8 @@ pub(super) fn material_element_with_role( .to_logical(scale) .to_i32_ceil(); let touches_top = role == crate::effects::SemanticRole::Panel && geometry.loc.y == 0; - let touches_bottom = - role == crate::effects::SemanticRole::Panel && geometry.loc.y + geometry.size.h >= output_size.h; + let touches_bottom = role == crate::effects::SemanticRole::Panel + && geometry.loc.y + geometry.size.h >= output_size.h; let radii = ferese_panel::CornerRadii(radii) .at_top_edge(touches_top) .at_edge(ferese_panel::Edge::Bottom, touches_bottom) @@ -279,14 +305,21 @@ pub(super) fn material_element_with_role( corners.rect.size, ); let background_opacity = material.opacity; - let blur = material_blur_radius(material.style, material.opacity, state.theme_settings.backdrop_blur); + let blur = material_blur_radius( + material.style, + material.opacity, + state.theme_settings.backdrop_blur, + ); let sample_geometry = expanded_blur_region(capture_geometry, blur.ceil() as i32, output_size); let sample_physical = sample_geometry.to_physical_precise_round(scale); let panel = (role == crate::effects::SemanticRole::Panel) .then(|| crate::effects::surface_panel_presentation(surface)) .flatten() - .map(|presentation| panel_matrices(&presentation, shape_origin, scale, mode.size, transform)); - let opaque = panel.is_none() && material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0; + .map(|presentation| { + panel_matrices(&presentation, shape_origin, scale, mode.size, transform) + }); + let opaque = + panel.is_none() && material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0; let mut parameters = MaterialParameters { panel, corner_radii, @@ -321,12 +354,16 @@ pub(super) fn material_element_with_role( .get(&capture_key) .is_none_or(|c| c.geometry != sample_geometry || c.texture.size() != capture_size) { - let texture = Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, capture_size) - .map_err(|error| { - tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed"); - error - }) - .ok(); + let texture = Offscreen::::create_buffer( + renderer, + Fourcc::Abgr8888, + capture_size, + ) + .map_err(|error| { + tracing::warn!(%error, ?sample_physical, "backdrop capture allocation failed"); + error + }) + .ok(); if let Some(texture) = texture { buffers.captures.insert( capture_key.clone(), @@ -364,7 +401,9 @@ pub(super) fn material_element_with_role( MaterialElement::Fill(SharedPixelShaderElement::new( program.0.clone(), geometry, - parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), + parameters + .opaque + .then(|| vec![Rectangle::from_size(geometry.size)]), 1.0, material_uniforms(¶meters), RenderElementKind::Unspecified, @@ -404,13 +443,17 @@ pub(super) fn material_element_with_role( || cached.parameters.panel != parameters.panel { cached.shadow.resize(parameters.shadow_bounds, None); - cached.shadow.update_uniforms(decoration_uniforms(¶meters, 2.0)); + cached + .shadow + .update_uniforms(decoration_uniforms(¶meters, 2.0)); } match &mut cached.element { MaterialElement::Fill(element) => { element.resize( geometry, - parameters.opaque.then(|| vec![Rectangle::from_size(geometry.size)]), + parameters + .opaque + .then(|| vec![Rectangle::from_size(geometry.size)]), ); element.update_uniforms(material_uniforms(¶meters)); } @@ -421,7 +464,9 @@ pub(super) fn material_element_with_role( let background = match &cached.element { // Keep the shader's opaque regions when its canvas already has the // shared edges. Animated modal geometry may need a physical override. - MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => element.clone().into(), + MaterialElement::Fill(element) if element.geometry(scale.into()) == corners.rect => { + element.clone().into() + } MaterialElement::Fill(element) => PhysicalShaderElement { inner: element.clone(), geometry: corners.rect, @@ -432,7 +477,11 @@ pub(super) fn material_element_with_role( Some((background, cached.shadow.clone().into())) } -pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity: f32, radius: f64) -> f64 { +pub(super) fn material_blur_radius( + style: crate::config::MaterialStyle, + opacity: f32, + radius: f64, +) -> f64 { if style == crate::config::MaterialStyle::Translucent && opacity > 0.0 { radius } else { @@ -440,7 +489,10 @@ pub(super) fn material_blur_radius(style: crate::config::MaterialStyle, opacity: } } -pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesRenderer) -> Option { +pub(super) fn blur_program( + resources: &mut RenderResources, + renderer: &mut GlesRenderer, +) -> Option { let context = renderer.context_id().erased(); if let Some(program) = resources .contexts @@ -449,7 +501,8 @@ pub(super) fn blur_program(resources: &mut RenderResources, renderer: &mut GlesR { return Some(program.clone()); } - match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) { + match renderer.compile_custom_texture_shader(corner_shader(BLUR_SHADER), &blur_uniform_names()) + { Ok(program) => { let program = BlurProgram(program); resources.contexts.entry(context).or_default().blur = Some(program.clone()); @@ -481,8 +534,14 @@ pub(super) fn blur_uniforms(p: &MaterialParameters) -> Vec> { let mut uniforms = vec![ Uniform::new("visible_rect", p.visible_framebuffer), Uniform::new("material_radii", p.corner_radii), - Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]), - Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]), + Uniform::new( + "texture_size", + [p.sample_framebuffer[2], p.sample_framebuffer[3]], + ), + Uniform::new( + "capture_origin", + [p.sample_framebuffer[0], p.sample_framebuffer[1]], + ), Uniform::new("blur_radius", p.blur), Uniform::new("presentation_alpha", p.presentation_alpha), Uniform::new("background_opacity", p.background_opacity), @@ -524,10 +583,14 @@ pub(super) fn material_program_for_corners( shape: CornerShape, ) -> Option { let context = renderer.context_id().erased(); - if let Some(program) = resources.contexts.get(&context).and_then(|programs| match shape { - CornerShape::Circular => programs.material.as_ref(), - CornerShape::Continuous => programs.window_material.as_ref(), - }) { + if let Some(program) = resources + .contexts + .get(&context) + .and_then(|programs| match shape { + CornerShape::Circular => programs.material.as_ref(), + CornerShape::Continuous => programs.window_material.as_ref(), + }) + { return Some(program.clone()); } @@ -540,7 +603,8 @@ pub(super) fn material_program_for_corners( UniformName::new("shadow_values", UniformType::_2f), ]; uniforms.extend(panel_uniform_names()); - match renderer.compile_custom_pixel_shader(corner_shader_for(MATERIAL_SHADER, shape), &uniforms) { + match renderer.compile_custom_pixel_shader(corner_shader_for(MATERIAL_SHADER, shape), &uniforms) + { Ok(program) => { let program = MaterialProgram(program); let context = resources.contexts.entry(context).or_default(); @@ -561,7 +625,10 @@ pub(super) fn material_uniforms(parameters: &MaterialParameters) -> Vec Vec> { +pub(super) fn decoration_uniforms( + parameters: &MaterialParameters, + paint_mode: f32, +) -> Vec> { let mut uniforms = vec![ Uniform::new("paint_mode", paint_mode), Uniform::new("shadow_rect", parameters.shadow_rect), @@ -587,9 +654,15 @@ fn panel_uniforms(parameters: &MaterialParameters) -> Vec> { let mut uniforms = vec![ Uniform::new( "panel_count", - parameters.panel.as_ref().map_or(0., |p| p.shapes.len() as f32), + parameters + .panel + .as_ref() + .map_or(0., |p| p.shapes.len() as f32), + ), + Uniform::new( + "panel_fusion", + parameters.panel.as_ref().map_or(0., |p| p.fusion), ), - Uniform::new("panel_fusion", parameters.panel.as_ref().map_or(0., |p| p.fusion)), ]; if let Some(panel) = ¶meters.panel && !panel.shapes.is_empty() @@ -643,7 +716,11 @@ fn panel_matrices( 4 => 1, _ => 0, }; - match permutation.iter().position(|i| *i == old_corner as f32).unwrap() { + match permutation + .iter() + .position(|i| *i == old_corner as f32) + .unwrap() + { 0 => 5, 1 => 4, 2 => 3, @@ -707,7 +784,10 @@ mod region_geometry_tests { runtime.panel_style, ); assert_eq!(material.opacity, if reduce { 1. } else { opacity }); - assert_eq!(material.opacity, candidate.theme.panel_opacity(Some(opacity))); + assert_eq!( + material.opacity, + candidate.theme.panel_opacity(Some(opacity)) + ); assert_eq!( material.style, if reduce { @@ -738,9 +818,14 @@ mod region_geometry_tests { let source = format!( "theme {{ material {{ style translucent; opacity 0.6; blur-radius 20; }}; accessibility {{ reduce-transparency #{reduce}; }}; }}; panel main {{ style morph; }}" ); - let (_, runtime, candidate) = crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap(); - let material = - resolve_surface_material(SemanticRole::Panel, runtime.theme_settings, None, runtime.panel_style); + let (_, runtime, candidate) = + crate::config::prepare(&source, std::path::Path::new("/tmp")).unwrap(); + let material = resolve_surface_material( + SemanticRole::Panel, + runtime.theme_settings, + None, + runtime.panel_style, + ); assert_eq!(material.shadow, [0.; 3]); assert_eq!(material.opacity, candidate.theme.panel_opacity(None)); assert_eq!( @@ -751,7 +836,11 @@ mod region_geometry_tests { crate::config::MaterialStyle::Translucent } ); - let blur = material_blur_radius(material.style, material.opacity, runtime.theme_settings.backdrop_blur); + let blur = material_blur_radius( + material.style, + material.opacity, + runtime.theme_settings.backdrop_blur, + ); assert_eq!(blur > 0., !reduce); } } @@ -760,7 +849,10 @@ mod region_geometry_tests { fn framebuffer_transforms_preserve_each_logical_corner() { let radii = [1., 2., 3., 4.]; assert_eq!(framebuffer_corner_radii(radii, Transform::Normal), radii); - assert_eq!(framebuffer_corner_radii(radii, Transform::Flipped180), [4., 3., 2., 1.]); + assert_eq!( + framebuffer_corner_radii(radii, Transform::Flipped180), + [4., 3., 2., 1.] + ); for (transform, expected) in [ (Transform::Normal, [1., 2., 3., 4.]), (Transform::_90, [4., 1., 2., 3.]), @@ -781,7 +873,9 @@ mod region_geometry_tests { use ferese_panel::shape::PixelGrid; for scale in [1., 1.25, 1.5, 2.] { for origin in [-3, 0, 1, 3, 101] { - for width in [149.99, 150., 150.01, 199.99, 200., 200.01, 259.99, 260., 260.01] { + for width in [ + 149.99, 150., 150.01, 199.99, 200., 200.01, 259.99, 260., 260.01, + ] { let scene = derive( Parameters { style: ferese_panel::PanelStyle::Morph, @@ -814,7 +908,8 @@ mod region_geometry_tests { let [body, left, right] = shapes.as_slice() else { panic!("expected body and shoulders") }; - let client_left = (scene.paint[0].bounds.x * scale).round() + (f64::from(origin) * scale).round(); + let client_left = (scene.paint[0].bounds.x * scale).round() + + (f64::from(origin) * scale).round(); assert_eq!( body[0], client_left as f32, "native fill must align with the shell outline" @@ -864,7 +959,9 @@ mod region_geometry_tests { attached: true, edge, fillet: 12., - radius: Some(AnchoredRadii(sample.geometry.radii.map(|r| r as f32))), + radius: Some(AnchoredRadii( + sample.geometry.radii.map(|r| r as f32), + )), ..Default::default() }, bounds, @@ -916,14 +1013,23 @@ mod region_geometry_tests { #[test] fn fractional_regions_snap_once_in_output_pixels() { let bounds = Rectangle::from_size((200, 100).into()); - let (_, corners) = material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5).unwrap(); - assert_eq!(corners.rect, Rectangle::new((1, 1).into(), (150, 60).into())); + let (_, corners) = + material_region_geometry([0.4, 0.4, 100.4, 40.4, 14.0], bounds, (0, 0).into(), 1.5) + .unwrap(); + assert_eq!( + corners.rect, + Rectangle::new((1, 1).into(), (150, 60).into()) + ); assert_eq!(corners.radius, 21.0); // The old logical rounding would put this at physical (0, 0). let (_, clipped) = - material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5).unwrap(); + material_region_geometry([-0.4, -0.4, 100.4, 40.4, 100.0], bounds, (0, 0).into(), 1.5) + .unwrap(); assert_eq!(clipped.rect, Rectangle::from_size((150, 60).into())); assert_eq!(clipped.radius, 30.0); - assert!(material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5).is_none()); + assert!( + material_region_geometry([300.0, 0.0, 10.0, 10.0, 2.0], bounds, (0, 0).into(), 1.5) + .is_none() + ); } } diff --git a/compositor/src/render/mod.rs b/compositor/src/render/mod.rs index 27cbf28..ab9983b 100644 --- a/compositor/src/render/mod.rs +++ b/compositor/src/render/mod.rs @@ -19,14 +19,17 @@ use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::Duration; -pub(crate) use capture::{capture_resize_snapshot, capture_window_buffer, capture_window_frame, convert_window_pixels}; +pub(crate) use capture::{ + capture_resize_snapshot, capture_window_buffer, capture_window_frame, convert_window_pixels, +}; use decorations::*; use materials::*; use overview::*; pub(crate) use resources::RenderResources; use scene::cursor_elements; pub(crate) use scene::{ - animated_window_elements, capture_output_elements, frame_effect_metrics, layer_surfaces, sampled_output_elements, + animated_window_elements, capture_output_elements, frame_effect_metrics, layer_surfaces, + sampled_output_elements, }; pub(crate) use shader::SharedPixelShaderElement; use smithay::backend::allocator::Fourcc; @@ -36,40 +39,50 @@ use smithay::backend::renderer::element::surface::{ WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree, }; use smithay::backend::renderer::element::utils::{ - ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement, - constrain_render_elements, + ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, + RescaleRenderElement, constrain_render_elements, }; use smithay::backend::renderer::element::{ Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, render_elements, }; use smithay::backend::renderer::gles::{ - GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, UniformName, - UniformType, + GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, + UniformName, UniformType, +}; +use smithay::backend::renderer::utils::{ + CommitCounter, DamageSet, OpaqueRegions, draw_render_elements, +}; +use smithay::backend::renderer::{ + Bind, Color32F, ErasedContextId, ExportMem, Frame, Offscreen, Renderer, Texture, }; -use smithay::backend::renderer::utils::{CommitCounter, DamageSet, OpaqueRegions, draw_render_elements}; -use smithay::backend::renderer::{Bind, Color32F, ErasedContextId, ExportMem, Frame, Offscreen, Renderer, Texture}; use smithay::desktop::space::{ConstrainBehavior, ConstrainReference, constrain_space_element}; use smithay::desktop::{LayerSurface, PopupManager, layer_map_for_output}; use smithay::input::pointer::{CursorImageStatus, CursorImageSurfaceData}; use smithay::output::Output; use smithay::reexports::wayland_server::Resource; -use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform}; +use smithay::utils::{ + Buffer, Logical, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform, +}; use smithay::wayland::compositor::with_states; use smithay::wayland::shell::wlr_layer::Layer; use crate::Ferese; use crate::metrics::FrameEffectMetrics; use crate::presentation::{ - CornerShape, NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, corner_extent, physical_rect, + CornerShape, NativeTextureElement, PhysicalShaderElement, RoundedRect, clamp_radius, + corner_extent, physical_rect, }; -type SurfaceRenderElement = - CropRenderElement>>>; +type SurfaceRenderElement = CropRenderElement< + RelocateRenderElement>>, +>; -type WindowRenderElement = CropRenderElement>>; +type WindowRenderElement = + CropRenderElement>>; -type MemoryRenderElement = - CropRenderElement>>>; +type MemoryRenderElement = CropRenderElement< + RelocateRenderElement>>, +>; render_elements! { pub(crate) AnimatedWindowRenderElement<=GlesRenderer>; @@ -127,7 +140,9 @@ fn corner_shader(source: &str) -> String { fn corner_shader_for(source: &str, shape: CornerShape) -> String { match shape { - CornerShape::Circular => source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")), + CornerShape::Circular => { + source.replace("//_CORNERS_", include_str!("shaders/corners.glsl")) + } CornerShape::Continuous => source .replace( "//_CORNERS_", @@ -138,7 +153,10 @@ fn corner_shader_for(source: &str, shape: CornerShape) -> String { ) .replace("precision mediump float;", "precision highp float;"), } - .replace("//_MATERIAL_CORNERS_", include_str!("shaders/material_corners.glsl")) + .replace( + "//_MATERIAL_CORNERS_", + include_str!("shaders/material_corners.glsl"), + ) .replace("//_PANEL_MASK_", include_str!("shaders/panel_mask.glsl")) } @@ -216,7 +234,11 @@ impl Element for BlurRenderElement { self.geometry.to_physical_precise_round(scale) } - fn damage_since(&self, scale: RenderScale, commit: Option) -> DamageSet { + fn damage_since( + &self, + scale: RenderScale, + commit: Option, + ) -> DamageSet { blur_damage(self.geometry(scale).size, self.current_commit(), commit) } @@ -517,7 +539,11 @@ impl Element for RoundedSurfaceRenderElement { self.inner.src() } - fn damage_since(&self, scale: RenderScale, commit: Option) -> DamageSet { + fn damage_since( + &self, + scale: RenderScale, + commit: Option, + ) -> DamageSet { clipped_surface_damage(self.inner.geometry(scale).size, self.clip_changed, || { self.inner.damage_since(scale, commit) }) @@ -634,8 +660,8 @@ mod tests { use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; use super::{ - border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior, rounded_visual_rect, - scaled_visual_rect, shadow_bounds, + border_gradient_line, color_with_alpha, framebuffer_clip_rect, resize_content_behavior, + rounded_visual_rect, scaled_visual_rect, shadow_bounds, }; #[test] @@ -643,9 +669,14 @@ mod tests { fn solid_corner_mask_preserves_premultiplied_color() { use smithay::backend::allocator::Fourcc; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; - use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform, UniformName, UniformType}; + use smithay::backend::renderer::gles::{ + GlesRenderer, GlesTexture, Uniform, UniformName, UniformType, + }; use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, Offscreen, Renderer}; - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; @@ -662,7 +693,9 @@ mod tests { let size = (32, 32).into(); let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); let rect = Rectangle::::from_size((32, 32).into()); - for (radius, corner_alpha, inset) in [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] { + for (radius, corner_alpha, inset) in + [(12.0f32, 0u8, 0.0f32), (0.0, 64, 0.0), (0.0, 48, 0.25)] + { let mut target = renderer.bind(&mut texture).unwrap(); { let mut frame = renderer @@ -678,7 +711,10 @@ mod tests { Some(&[rect]), 0.5, &[ - Uniform::new("clip_rect", [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset]), + Uniform::new( + "clip_rect", + [inset, inset, 32.0 - 2.0 * inset, 32.0 - 2.0 * inset], + ), Uniform::new("radius", radius), Uniform::new("color", [0.5f32, 0.0, 0.0, 0.5]), ], @@ -702,7 +738,10 @@ mod tests { fn shared_corner_shaders_compile() { use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::renderer::gles::GlesRenderer; - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; @@ -729,22 +768,33 @@ mod tests { use smithay::backend::allocator::Fourcc; use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture, Uniform}; - use smithay::backend::renderer::{Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer}; + use smithay::backend::renderer::{ + Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, + }; - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let program = renderer - .compile_custom_texture_shader(super::corner_shader(super::BLUR_SHADER), &super::blur_uniform_names()) + .compile_custom_texture_shader( + super::corner_shader(super::BLUR_SHADER), + &super::blur_uniform_names(), + ) .unwrap(); let size = (32, 32).into(); let mut pixels = vec![128u8; 32 * 32 * 4]; for pixel in pixels.as_chunks_mut::<4>().0 { pixel[3] = 255; } - let blurred = renderer.import_memory(&pixels, Fourcc::Abgr8888, size, false).unwrap(); - let mut target_texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); + let blurred = renderer + .import_memory(&pixels, Fourcc::Abgr8888, size, false) + .unwrap(); + let mut target_texture: GlesTexture = + renderer.create_buffer(Fourcc::Abgr8888, size).unwrap(); let damage = Rectangle::::from_size((32, 32).into()); for (opacity, strength, tint) in [ @@ -768,7 +818,9 @@ mod tests { let mut frame = renderer .render(&mut target, (32, 32).into(), Transform::Normal) .unwrap(); - frame.clear(Color32F::new(0.0, 0.0, 0.0, 1.0), &[damage]).unwrap(); + frame + .clear(Color32F::new(0.0, 0.0, 0.0, 1.0), &[damage]) + .unwrap(); frame .draw_solid( Rectangle::new((0, 0).into(), (16, 32).into()), @@ -801,7 +853,8 @@ mod tests { left.abs_diff(right) <= 2, "opacity {opacity} leaked the original backdrop: {left} versus {right}" ); - let expected = (128.0 * (1.0 - strength * opacity) + tint * 255.0 * strength * opacity).round() as u8; + let expected = (128.0 * (1.0 - strength * opacity) + tint * 255.0 * strength * opacity) + .round() as u8; assert!( left.abs_diff(expected) <= 2, "opacity {opacity} failed to control tint: {left} versus {expected}" @@ -823,7 +876,8 @@ mod tests { Transform::Flipped270, ] { let area = transform.transform_size(mode).to_f64(); - let rect: Rectangle = Rectangle::new((110, 60).into(), (800, 600).into()); + let rect: Rectangle = + Rectangle::new((110, 60).into(), (800, 600).into()); for (angle, first, last) in [ (0.0, (110.0, 360.0), (910.0, 360.0)), (90.0, (510.0, 60.0), (510.0, 660.0)), @@ -836,8 +890,14 @@ mod tests { (point.x - f64::from(line[0])) * f64::from(line[2]) + (point.y - f64::from(line[1])) * f64::from(line[3]) }; - assert!(progress(first.into()).abs() < 0.00001, "{transform:?}, {angle}"); - assert!((progress(last.into()) - 1.0).abs() < 0.00001, "{transform:?}, {angle}"); + assert!( + progress(first.into()).abs() < 0.00001, + "{transform:?}, {angle}" + ); + assert!( + (progress(last.into()) - 1.0).abs() < 0.00001, + "{transform:?}, {angle}" + ); } } } @@ -888,7 +948,12 @@ mod tests { } fn src(&self) -> Rectangle { - Rectangle::from_size(self.geometry.size.to_f64().to_buffer(1.0, Transform::Normal)) + Rectangle::from_size( + self.geometry + .size + .to_f64() + .to_buffer(1.0, Transform::Normal), + ) } fn geometry(&self, scale: Scale) -> Rectangle { @@ -904,9 +969,9 @@ mod tests { if commit == Some(self.current_commit()) { smithay::backend::renderer::utils::DamageSet::default() } else { - smithay::backend::renderer::utils::DamageSet::from_slice(&[Rectangle::from_size( - self.geometry(scale).size, - )]) + smithay::backend::renderer::utils::DamageSet::from_slice(&[ + Rectangle::from_size(self.geometry(scale).size), + ]) } }) } @@ -933,7 +998,12 @@ mod tests { element.clip_changed = resources.thumbnail_corner_changed(window, output, shape); assert_eq!(element.commit, client_commit); assert!(element.clip_changed); - let damage = tracker.damage_output(1, &[&element]).unwrap().0.cloned().unwrap(); + let damage = tracker + .damage_output(1, &[&element]) + .unwrap() + .0 + .cloned() + .unwrap(); assert!(damage.iter().any(|rect| rect.contains((100, 100)))); assert!(damage.iter().any(|rect| rect.contains((299, 259)))); element.clip_changed = resources.thumbnail_corner_changed(window, output, shape); @@ -949,7 +1019,9 @@ mod tests { #[test] fn resize_keeps_source_pixels_native_when_growing_and_shrinking() { - use smithay::backend::renderer::element::utils::{ConstrainAlign, constrain_render_elements}; + use smithay::backend::renderer::element::utils::{ + ConstrainAlign, constrain_render_elements, + }; for scale in [1.0, 1.5, 1.8, 2.0] { for width in [300, 600, 1200] { let source = Rectangle::::from_size((600, 800).into()); @@ -1020,14 +1092,18 @@ mod tests { Transform::Flipped180, Transform::Flipped270, ] { - let output_size = - smithay::utils::Size::::from(((1280.0 * scale) as i32, (800.0 * scale) as i32)); + let output_size = smithay::utils::Size::::from(( + (1280.0 * scale) as i32, + (800.0 * scale) as i32, + )); let element_size = transform.transform_size(output_size); let logical_height = f64::from(element_size.h) / scale; // Bar and exclusion end at 40; the 10px outer gap ends at 50. - let geometry = - Rectangle::::new((10.0, 50.0).into(), (400.0, logical_height - 60.0).into()) - .to_physical_precise_round(scale); + let geometry = Rectangle::::new( + (10.0, 50.0).into(), + (400.0, logical_height - 60.0).into(), + ) + .to_physical_precise_round(scale); let clip = framebuffer_clip_rect(geometry, output_size, transform); // Independently reproduce GlesFrame's orthographic projection, // transform matrix, GL flip, and viewport mapping for each corner. @@ -1048,7 +1124,10 @@ mod tests { let max_x = xs.iter().copied().fold(f32::NEG_INFINITY, f32::max); let min_y = ys.iter().copied().fold(f32::INFINITY, f32::min); let max_y = ys.iter().copied().fold(f32::NEG_INFINITY, f32::max); - for (actual, expected) in clip.into_iter().zip([min_x, min_y, max_x - min_x, max_y - min_y]) { + for (actual, expected) in + clip.into_iter() + .zip([min_x, min_y, max_x - min_x, max_y - min_y]) + { assert!( (actual - expected).abs() < 0.001, "{transform:?}, scale={scale}: {actual} != {expected}" @@ -1077,11 +1156,17 @@ mod tests { let old_window = Rectangle::::new((100, 80).into(), (400, 300).into()); let new_window = Rectangle::::new((300, 180).into(), (400, 300).into()); let mut shadow = DamageElement::new(shadow_bounds(old_window, 4.0, 18.0)); - let mut tracker = OutputDamageTracker::new((1_000, 800), Scale::from(1.0), Transform::Normal); + let mut tracker = + OutputDamageTracker::new((1_000, 800), Scale::from(1.0), Transform::Normal); tracker.damage_output(0, &[&shadow]).unwrap(); shadow.geometry = shadow_bounds(new_window, 4.0, 18.0); - let damage = tracker.damage_output(1, &[&shadow]).unwrap().0.cloned().unwrap(); + let damage = tracker + .damage_output(1, &[&shadow]) + .unwrap() + .0 + .cloned() + .unwrap(); assert!(damage.iter().any(|rect| rect.contains((64, 48)))); assert!(damage.iter().any(|rect| rect.contains((735, 519)))); @@ -1098,13 +1183,22 @@ mod tests { #[test] fn translucent_materials_blur_at_full_opacity() { use crate::config::MaterialStyle; - assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 0.0, 18.0), 0.0); - assert_eq!(super::material_blur_radius(MaterialStyle::Translucent, 1.0, 18.0), 18.0); + assert_eq!( + super::material_blur_radius(MaterialStyle::Translucent, 0.0, 18.0), + 0.0 + ); + assert_eq!( + super::material_blur_radius(MaterialStyle::Translucent, 1.0, 18.0), + 18.0 + ); assert_eq!( super::material_blur_radius(MaterialStyle::Translucent, 0.79, 18.0), 18.0 ); - assert_eq!(super::material_blur_radius(MaterialStyle::Solid, 0.79, 18.0), 0.0); + assert_eq!( + super::material_blur_radius(MaterialStyle::Solid, 0.79, 18.0), + 0.0 + ); } #[test] @@ -1120,7 +1214,10 @@ mod tests { #[test] fn close_opacity_only_changes_the_alpha_channel() { - assert_eq!(color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5), [0.2, 0.4, 0.6, 0.4]); + assert_eq!( + color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5), + [0.2, 0.4, 0.6, 0.4] + ); } #[test] @@ -1134,7 +1231,10 @@ mod tests { for tenth in 0..=10 { let left = 100.0 + f64::from(tenth) / 10.0; - let frame = rounded_visual_rect(ferese_layout::Rect::new(left, 50.0, 500.0 - left, 300.0), (0, 0).into()); + let frame = rounded_visual_rect( + ferese_layout::Rect::new(left, 50.0, 500.0 - left, 300.0), + (0, 0).into(), + ); assert_eq!(frame.loc.x + frame.size.w, 500); } diff --git a/compositor/src/render/overview.rs b/compositor/src/render/overview.rs index c5d7471..620d28a 100644 --- a/compositor/src/render/overview.rs +++ b/compositor/src/render/overview.rs @@ -19,7 +19,11 @@ pub(super) fn overview_chrome_element( let mode = output.current_mode()?; let parameters = BorderParameters { geometry, - clip_rect: framebuffer_clip_rect(corners.rect, mode.size, output.current_transform().invert()), + clip_rect: framebuffer_clip_rect( + corners.rect, + mode.size, + output.current_transform().invert(), + ), radius: corners.radius, width: (width * scale) as f32, color, @@ -30,13 +34,19 @@ pub(super) fn overview_chrome_element( focus_gradient_line: [0.0; 4], focus_mix: 0.0, }; - let program = if matches!(key.1, OverviewChromePart::Outline | OverviewChromePart::TabListOutline) { + let program = if matches!( + key.1, + OverviewChromePart::Outline | OverviewChromePart::TabListOutline + ) { rounded_clip_program(&mut state.render, renderer)?.border } else { material_program(&mut state.render, renderer)?.0 }; let uniforms = |p: &BorderParameters| { - if matches!(key.1, OverviewChromePart::Outline | OverviewChromePart::TabListOutline) { + if matches!( + key.1, + OverviewChromePart::Outline | OverviewChromePart::TabListOutline + ) { border_uniforms(p) } else { tint_uniforms(p) @@ -47,7 +57,9 @@ pub(super) fn overview_chrome_element( .render .outputs .entry(output_id) - .or_insert_with(|| OverviewScrim { chrome: HashMap::new() }) + .or_insert_with(|| OverviewScrim { + chrome: HashMap::new(), + }) .chrome; let cached = chrome .entry(key) @@ -129,7 +141,9 @@ pub(super) fn overview_strip_elements( | OverviewChromePart::TabListOutline | OverviewChromePart::TabList | OverviewChromePart::TabRow - | OverviewChromePart::TabFlash => state.windows.window(ferese_layout::WindowId(*id)).is_some(), + | OverviewChromePart::TabFlash => { + state.windows.window(ferese_layout::WindowId(*id)).is_some() + } OverviewChromePart::Card | OverviewChromePart::Outline => card_ids.contains(id), OverviewChromePart::Strip => true, }); @@ -149,7 +163,11 @@ pub(super) fn overview_strip_elements( { continue; } - let Some(rect) = frame.windows.get(&id).map(|sample| sample.presentation.bounds.current) else { + let Some(rect) = frame + .windows + .get(&id) + .map(|sample| sample.presentation.bounds.current) + else { continue; }; let Some((buffer, size)) = state.overview_window_label(id, scale, rect.width) else { @@ -163,7 +181,9 @@ pub(super) fn overview_strip_elements( 26.0, ); let location = Point::::from(( - ((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round() as i32 - size.w / 2, + ((caption.x - f64::from(output_geometry.loc.x) + caption.width * 0.5) * scale).round() + as i32 + - size.w / 2, ((caption.y - f64::from(output_geometry.loc.y) + 4.0) * scale).round() as i32, )); if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( @@ -171,7 +191,9 @@ pub(super) fn overview_strip_elements( location.to_f64(), &buffer, Some(alpha), - Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), Some( ( (f64::from(size.w) / scale).round() as i32, @@ -183,9 +205,11 @@ pub(super) fn overview_strip_elements( ) { let element = RescaleRenderElement::from_element(element, location, 1.0); let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); - if let Some(element) = - CropRenderElement::from_element(element, scale, physical_rect(caption, output_geometry.loc, scale)) - { + if let Some(element) = CropRenderElement::from_element( + element, + scale, + physical_rect(caption, output_geometry.loc, scale), + ) { elements.push(element.into()); } } @@ -235,13 +259,20 @@ pub(super) fn overview_strip_elements( let shape = window_corner_shape(window); if let Some(programs) = corner_program(&mut state.render, renderer, shape) { - let clip_changed = state - .output_id(output) - .is_some_and(|output| state.render.thumbnail_corner_changed(*id, output, shape)); - let Some(sample) = frame.windows.get(id) else { continue }; + let clip_changed = state.output_id(output).is_some_and(|output| { + state.render.thumbnail_corner_changed(*id, output, shape) + }); + let Some(sample) = frame.windows.get(id) else { + continue; + }; let presentation = sample.presentation.thumbnail(*rect); - let corners = RoundedRect::new(presentation.bounds.current, output_geometry.loc, scale, 4.0) - .with_shape(shape); + let corners = RoundedRect::new( + presentation.bounds.current, + output_geometry.loc, + scale, + 4.0, + ) + .with_shape(shape); if let Some(snapshot) = state.render.snapshot(id) && snapshot.context == renderer.context_id().erased() && (snapshot.scale - scale).abs() < 0.001 @@ -277,16 +308,20 @@ pub(super) fn overview_strip_elements( } if let Some((buffer, size)) = state.overview_workspace_label(card.index, scale) { let location = Point::::from(( - ((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale).round() as i32 + ((card.rect.x - f64::from(output_geometry.loc.x) + card.rect.width * 0.5) * scale) + .round() as i32 - size.w / 2, - ((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale).round() as i32, + ((card.rect.y - f64::from(output_geometry.loc.y) + card.rect.height - 20.0) * scale) + .round() as i32, )); if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( renderer, location.to_f64(), &buffer, Some(alpha), - Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), Some( ( (f64::from(size.w) / scale).round() as i32, @@ -297,7 +332,8 @@ pub(super) fn overview_strip_elements( RenderElementKind::Unspecified, ) { let element = RescaleRenderElement::from_element(element, location, 1.0); - let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); + let element = + RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); if let Some(element) = CropRenderElement::from_element( element, scale, diff --git a/compositor/src/render/resources.rs b/compositor/src/render/resources.rs index 39e601c..3c98a9a 100644 --- a/compositor/src/render/resources.rs +++ b/compositor/src/render/resources.rs @@ -60,7 +60,12 @@ impl RenderResources { changed } - pub(super) fn thumbnail_corner_changed(&mut self, id: WindowId, output: OutputId, shape: CornerShape) -> bool { + pub(super) fn thumbnail_corner_changed( + &mut self, + id: WindowId, + output: OutputId, + shape: CornerShape, + ) -> bool { self.windows .entry(id) .or_default() @@ -117,7 +122,9 @@ impl RenderResources { } pub fn snapshots(&self) -> impl Iterator { - self.windows.values().filter_map(|window| window.snapshot.as_ref()) + self.windows + .values() + .filter_map(|window| window.snapshot.as_ref()) } pub fn set_snapshot(&mut self, id: WindowId, snapshot: ResizeSnapshot) { @@ -130,7 +137,10 @@ impl RenderResources { } } - pub fn retain_snapshots(&mut self, mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool) { + pub fn retain_snapshots( + &mut self, + mut keep: impl FnMut(&WindowId, &mut ResizeSnapshot) -> bool, + ) { for (id, window) in &mut self.windows { if let Some(snapshot) = &mut window.snapshot && !keep(id, snapshot) @@ -159,7 +169,8 @@ impl RenderResources { .filter(|window| &window.snapshot.context == context) .map(|window| window.presentation.id) .collect(); - self.closing.retain(|window| &window.snapshot.context != context); + self.closing + .retain(|window| &window.snapshot.context != context); for id in removed { self.remove_window(id); } @@ -201,8 +212,12 @@ mod tests { #[test] fn window_removal_releases_all_its_caches_without_removing_other_windows() { let mut resources = RenderResources::default(); - resources.windows.insert(WindowId(1), WindowBuffers::default()); - resources.windows.insert(WindowId(2), WindowBuffers::default()); + resources + .windows + .insert(WindowId(1), WindowBuffers::default()); + resources + .windows + .insert(WindowId(2), WindowBuffers::default()); resources.remove_window(WindowId(1)); resources.clear_snapshot(&WindowId(1)); @@ -217,7 +232,10 @@ mod tests { fn context_removal_evicts_programs_and_snapshots_without_touching_another_gpu() { use smithay::backend::egl::{EGLContext, EGLDevice, EGLDisplay}; - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let mut first = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let mut second = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; @@ -229,7 +247,12 @@ mod tests { rounded_clip_program(&mut resources, renderer).unwrap(); material_program(&mut resources, renderer).unwrap(); blur_program(&mut resources, renderer).unwrap(); - let texture = Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, (16, 16).into()).unwrap(); + let texture = Offscreen::::create_buffer( + renderer, + Fourcc::Abgr8888, + (16, 16).into(), + ) + .unwrap(); resources.set_snapshot( id, ResizeSnapshot { diff --git a/compositor/src/render/scene.rs b/compositor/src/render/scene.rs index aa1fe84..01e6271 100644 --- a/compositor/src/render/scene.rs +++ b/compositor/src/render/scene.rs @@ -10,7 +10,10 @@ pub(crate) fn animated_window_elements( output_elements(state, renderer, output, true) } -pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics { +pub(crate) fn frame_effect_metrics( + _elements: &[AnimatedWindowRenderElement], + _scale: f64, +) -> FrameEffectMetrics { FrameEffectMetrics } @@ -69,9 +72,16 @@ fn scene_elements( } else { Vec::new() }; - let background = state.session_lock.backgrounds.entry(output.clone()).or_insert_with(|| { - smithay::backend::renderer::element::solid::SolidColorBuffer::new(geometry.size, [0.0, 0.0, 0.0, 1.0]) - }); + let background = state + .session_lock + .backgrounds + .entry(output.clone()) + .or_insert_with(|| { + smithay::backend::renderer::element::solid::SolidColorBuffer::new( + geometry.size, + [0.0, 0.0, 0.0, 1.0], + ) + }); background.resize(geometry.size); let opacity = state.session_lock.idle_opacity; if opacity > 0.0 { @@ -87,8 +97,14 @@ fn scene_elements( }); overlay.resize(geometry.size); elements.push( - SolidColorRenderElement::from_buffer(overlay, (0, 0), scale, opacity, RenderElementKind::Unspecified) - .into(), + SolidColorRenderElement::from_buffer( + overlay, + (0, 0), + scale, + opacity, + RenderElementKind::Unspecified, + ) + .into(), ); } if let Some(surface) = state @@ -98,7 +114,10 @@ fn scene_elements( .filter(|surface| surface.alive()) { elements.extend( - render_elements_from_surface_tree::>( + render_elements_from_surface_tree::< + GlesRenderer, + WaylandSurfaceRenderElement, + >( renderer, surface.wl_surface(), (0, 0), @@ -111,7 +130,14 @@ fn scene_elements( ); } elements.push( - SolidColorRenderElement::from_buffer(background, (0, 0), scale, 1.0, RenderElementKind::Unspecified).into(), + SolidColorRenderElement::from_buffer( + background, + (0, 0), + scale, + 1.0, + RenderElementKind::Unspecified, + ) + .into(), ); return elements; } @@ -126,11 +152,12 @@ fn scene_elements( } else { Vec::new() }; - let upper_layers: &[Layer] = if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) { - &[Layer::Overlay] - } else { - &[Layer::Overlay, Layer::Top] - }; + let upper_layers: &[Layer] = + if !state.layer_visible_for_frame(output, Layer::Top, frame.overview.is_presenting()) { + &[Layer::Overlay] + } else { + &[Layer::Overlay, Layer::Top] + }; elements.extend(layer_elements(state, renderer, output, upper_layers)); if frame.overview.is_presenting() { elements.extend(overview_strip_elements( @@ -196,7 +223,9 @@ fn scene_elements( if let Some(group) = closing.remove(&Some(id)) { elements.extend(group); } - elements.extend(super::tabs::tab_elements(state, renderer, output, frame, id, capture)); + elements.extend(super::tabs::tab_elements( + state, renderer, output, frame, id, capture, + )); let constrain = rounded_visual_rect(visual, output_geometry.loc); let material_surface = window .toplevel() @@ -211,7 +240,8 @@ fn scene_elements( state.theme_settings.window_radius } * decoration_progress; let shape = window_corner_shape(&window); - let corners = RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape); + let corners = + RoundedRect::new(visual, output_geometry.loc, scale, window_radius).with_shape(shape); let corner_shape_changed = state.render.prepare_window_corners(id, shape); let rounded_clip_program = corner_program(&mut state.render, renderer, shape); // Only overview/close intentionally scale the complete application. @@ -241,7 +271,8 @@ fn scene_elements( let shadow_color = state.theme_settings.shadow_color.0; let focus = sample.presentation.focus(); let border_width = state.theme_settings.border_width - + (state.theme_settings.focus_ring_width - state.theme_settings.border_width) * focus; + + (state.theme_settings.focus_ring_width - state.theme_settings.border_width) + * focus; let border_color = state.theme_settings.border_color.0; let gradient = state.theme_settings.border_gradient; @@ -273,7 +304,10 @@ fn scene_elements( scale, shadow_offset_y * offset_factor, shadow_blur * blur_factor, - shadow_opacity * opacity_factor * f64::from(presentation_alpha) * decoration_progress, + shadow_opacity + * opacity_factor + * f64::from(presentation_alpha) + * decoration_progress, shadow_color, output, &programs, @@ -313,9 +347,16 @@ fn scene_elements( { let mut color = state.theme_settings.surface_base_color.0; color[3] = presentation_alpha; - if let Some(fill) = - window_tint_element(&mut state.render, renderer, id, constrain, corners, color, true, output) - { + if let Some(fill) = window_tint_element( + &mut state.render, + renderer, + id, + constrain, + corners, + color, + true, + output, + ) { // Front-to-back: fill uncovered strips behind the native // content instead of stretching it or exposing wallpaper. elements.push(fill.into()); @@ -342,8 +383,13 @@ fn scene_elements( elements.push(background); } if material_surface.is_none() - && let Some(blur) = - super::window_blur::window_blur_element(state, renderer, output, sample.presentation, corners) + && let Some(blur) = super::window_blur::window_blur_element( + state, + renderer, + output, + sample.presentation, + corners, + ) { elements.push(blur); } @@ -420,20 +466,22 @@ pub(super) fn layer_elements( requested_layers .iter() .flat_map(|requested| { - map.layers_on(*requested) - .rev() - .filter_map(|layer| map.layer_geometry(layer).map(|geometry| (geometry, layer.clone()))) + map.layers_on(*requested).rev().filter_map(|layer| { + map.layer_geometry(layer) + .map(|geometry| (geometry, layer.clone())) + }) }) .collect::>() }; state.render.surfaces.retain(|surface, _| { surface.is_alive() - || state - .render - .closing - .iter() - .any(|window| window.material.as_ref().is_some_and(|(key, ..)| key == surface)) + || state.render.closing.iter().any(|window| { + window + .material + .as_ref() + .is_some_and(|(key, ..)| key == surface) + }) }); let mut elements = Vec::new(); @@ -486,7 +534,8 @@ pub(super) fn cursor_elements( let Some(pointer) = state.seat.get_pointer() else { return Vec::new(); }; - let output_crop = Rectangle::::from_size(output_geometry.size).to_physical_precise_round(scale); + let output_crop = + Rectangle::::from_size(output_geometry.size).to_physical_precise_round(scale); let origin = Point::::default(); let pointer_location = pointer.current_location() - output_geometry.loc.to_f64(); match &state.cursor_status { @@ -498,9 +547,13 @@ pub(super) fn cursor_elements( .map(|attributes| attributes.lock().unwrap().hotspot) .unwrap_or_default() }); - let physical_location = (pointer_location - hotspot.to_f64()).to_physical_precise_round(scale); + let physical_location = + (pointer_location - hotspot.to_f64()).to_physical_precise_round(scale); - render_elements_from_surface_tree::>( + render_elements_from_surface_tree::< + GlesRenderer, + WaylandSurfaceRenderElement, + >( renderer, surface, physical_location, @@ -511,7 +564,8 @@ pub(super) fn cursor_elements( .into_iter() .filter_map(|element| { let element = RescaleRenderElement::from_element(element, origin, 1.0); - let element = RelocateRenderElement::from_element(element, origin, Relocate::Relative); + let element = + RelocateRenderElement::from_element(element, origin, Relocate::Relative); CropRenderElement::from_element(element, scale, output_crop).map(Into::into) }) @@ -573,8 +627,9 @@ mod layer_crop_tests { [1.0; 4], RenderElementKind::Unspecified, ); - let crop = CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform)) - .expect("bottom panel must remain visible"); + let crop = + CropRenderElement::from_element(panel, 1.0, layer_output_crop(mode, transform)) + .expect("bottom panel must remain visible"); assert_eq!(crop.geometry(1.0.into()), geometry, "{transform:?}"); } } diff --git a/compositor/src/render/shader.rs b/compositor/src/render/shader.rs index 692d30a..a46bb90 100644 --- a/compositor/src/render/shader.rs +++ b/compositor/src/render/shader.rs @@ -3,7 +3,9 @@ use std::sync::Arc; use smithay::backend::renderer::element::{Element, Id, Kind, RenderElement}; -use smithay::backend::renderer::gles::{GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform}; +use smithay::backend::renderer::gles::{ + GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, Uniform, +}; use smithay::backend::renderer::utils::{CommitCounter, OpaqueRegions}; use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; @@ -47,7 +49,11 @@ impl SharedPixelShaderElement { } } - pub(crate) fn resize(&mut self, area: Rectangle, opaque: Option>>) { + pub(crate) fn resize( + &mut self, + area: Rectangle, + opaque: Option>>, + ) { let opaque = opaque.unwrap_or_default(); if self.state.area != area || self.state.opaque != opaque { @@ -75,7 +81,13 @@ impl Element for SharedPixelShaderElement { } fn src(&self) -> Rectangle { - Rectangle::from_size(self.state.area.size.to_f64().to_buffer(1.0, Transform::Normal)) + Rectangle::from_size( + self.state + .area + .size + .to_f64() + .to_buffer(1.0, Transform::Normal), + ) } fn geometry(&self, scale: Scale) -> Rectangle { @@ -159,9 +171,13 @@ mod tests { #[test] #[ignore = "requires an EGL rendering device"] fn shared_shader_matches_smithay_and_keeps_old_frames_immutable() { - let device = EGLDevice::enumerate().unwrap().last().expect("an EGL device"); + let device = EGLDevice::enumerate() + .unwrap() + .last() + .expect("an EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; - let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; + let mut renderer = + unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let program = renderer .compile_custom_pixel_shader( super::super::corner_shader(super::super::ROUNDED_SOLID_SHADER), @@ -191,7 +207,14 @@ mod tests { uniforms(red), Kind::Unspecified, ); - let mut shared = SharedPixelShaderElement::new(program, area, opaque, 0.5, uniforms(red), Kind::Unspecified); + let mut shared = SharedPixelShaderElement::new( + program, + area, + opaque, + 0.5, + uniforms(red), + Kind::Unspecified, + ); let old = shared.clone(); let old_commit = old.current_commit(); @@ -205,14 +228,20 @@ mod tests { ); for scale in [1.0, 1.25, 2.0] { - assert_eq!(shared.geometry(scale.into()), baseline.geometry(scale.into())); + assert_eq!( + shared.geometry(scale.into()), + baseline.geometry(scale.into()) + ); assert_eq!( &*shared.opaque_regions(scale.into()), &*baseline.opaque_regions(scale.into()) ); } - assert_eq!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); + assert_eq!( + pixels(&mut renderer, &shared), + pixels(&mut renderer, &baseline) + ); shared.resize(area, Some(vec![Rectangle::from_size((20, 20).into())])); assert!(Arc::ptr_eq(&shared.state, &old.state)); shared.update_uniforms(uniforms(green)); @@ -220,12 +249,21 @@ mod tests { assert_eq!(shared.id(), old.id()); assert_eq!(old.current_commit(), old_commit); assert_ne!(shared.current_commit(), old_commit); - assert_eq!(pixels(&mut renderer, &old), pixels(&mut renderer, &baseline)); - assert_ne!(pixels(&mut renderer, &shared), pixels(&mut renderer, &baseline)); + assert_eq!( + pixels(&mut renderer, &old), + pixels(&mut renderer, &baseline) + ); + assert_ne!( + pixels(&mut renderer, &shared), + pixels(&mut renderer, &baseline) + ); let retained = shared.clone(); shared.resize(Rectangle::from_size((16, 16).into()), None); - assert_eq!(retained.geometry(1.0.into()), area.to_physical_precise_round(1.0)); + assert_eq!( + retained.geometry(1.0.into()), + area.to_physical_precise_round(1.0) + ); assert_eq!(shared.geometry(1.0.into()).size, (16, 16).into()); assert!(shared.opaque_regions(1.0.into()).is_empty()); assert_eq!(shared.alpha(), baseline.alpha()); diff --git a/compositor/src/render/tab_indicator.rs b/compositor/src/render/tab_indicator.rs index 4be5ec2..4918bd3 100644 --- a/compositor/src/render/tab_indicator.rs +++ b/compositor/src/render/tab_indicator.rs @@ -13,14 +13,23 @@ fn text_element( centered: bool, ) -> Option { let settings = state.tab_indicator_settings; - let color = settings.text_color.unwrap_or(state.theme_settings.text_primary_color.0); + let color = settings + .text_color + .unwrap_or(state.theme_settings.text_primary_color.0); let available = (rect.width - 16.0).max(0.0); - let mut title: String = text.chars().filter(|c| !c.is_control()).take(1024).collect(); - if state.indicator_text_width(&title, settings.font_size) > available || text.chars().count() > 1024 { + let mut title: String = text + .chars() + .filter(|c| !c.is_control()) + .take(1024) + .collect(); + if state.indicator_text_width(&title, settings.font_size) > available + || text.chars().count() > 1024 + { let mut boundaries: Vec<_> = title.char_indices().map(|(index, _)| index).collect(); boundaries.push(title.len()); let fits = boundaries.partition_point(|&end| { - state.indicator_text_width(&format!("{}…", &title[..end]), settings.font_size) <= available + state.indicator_text_width(&format!("{}…", &title[..end]), settings.font_size) + <= available }); if fits == 0 { title.clear(); @@ -37,14 +46,17 @@ fn text_element( }; let location = Point::::from(( ((rect.x - f64::from(output_origin.x) + x) * scale).round() as i32, - ((rect.y - f64::from(output_origin.y) + rect.height * 0.5) * scale).round() as i32 - size.h / 2, + ((rect.y - f64::from(output_origin.y) + rect.height * 0.5) * scale).round() as i32 + - size.h / 2, )); let element = MemoryRenderBufferRenderElement::from_buffer( renderer, location.to_f64(), &buffer, Some(alpha), - Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), Some( ( (f64::from(size.w) / scale).round() as i32, @@ -57,12 +69,23 @@ fn text_element( .ok()?; let element = RescaleRenderElement::from_element(element, location, 1.0); let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); - let clip = Rect::new(rect.x + 8.0, rect.y, (rect.width - 16.0).max(0.0), rect.height); - CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale)).map(Into::into) + let clip = Rect::new( + rect.x + 8.0, + rect.y, + (rect.width - 16.0).max(0.0), + rect.height, + ); + CropRenderElement::from_element(element, scale, physical_rect(clip, output_origin, scale)) + .map(Into::into) } fn title(state: &Ferese, id: WindowId, capture: bool) -> String { - if capture && state.windows.window(id).is_none_or(|w| state.capture_protected(w)) { + if capture + && state + .windows + .window(id) + .is_none_or(|w| state.capture_protected(w)) + { return "Protected window".into(); } state @@ -99,8 +122,12 @@ pub(super) fn elements( let scale = output.current_scale().fractional_scale(); let settings = state.tab_indicator_settings; let anchor = bar.group.windows[0]; - let accent = settings.active_color.unwrap_or(state.theme_settings.accent_color.0); - let inactive = settings.inactive_color.unwrap_or(state.theme_settings.border_color.0); + let accent = settings + .active_color + .unwrap_or(state.theme_settings.accent_color.0); + let inactive = settings + .inactive_color + .unwrap_or(state.theme_settings.border_color.0); let mut background = settings .background_color .unwrap_or(state.theme_settings.surface_raised_color.0); @@ -109,7 +136,8 @@ pub(super) fn elements( state.theme_settings.material_style, state.theme_settings.shell_opacity as f32, ); - background[3] = if state.theme_settings.reduce_transparency || material.style == crate::config::MaterialStyle::Solid + background[3] = if state.theme_settings.reduce_transparency + || material.style == crate::config::MaterialStyle::Solid { 1.0 } else { @@ -135,7 +163,15 @@ pub(super) fn elements( .tab_indicator .first_row .min(bar.group.windows.len().saturating_sub(g.rows)); - for (row, id) in bar.group.windows.iter().copied().skip(first).take(g.rows).enumerate() { + for (row, id) in bar + .group + .windows + .iter() + .copied() + .skip(first) + .take(g.rows) + .enumerate() + { let rect = g.row(row); let label = title(state, id, capture); if let Some(element) = text_element( @@ -153,7 +189,9 @@ pub(super) fn elements( if id == bar.group.active { // Match the control center's selected shell buttons: a quiet // foreground tint, with accent reserved for the active rail. - let mut fill = settings.text_color.unwrap_or(state.theme_settings.text_primary_color.0); + let mut fill = settings + .text_color + .unwrap_or(state.theme_settings.text_primary_color.0); fill[3] = 0.14 * alpha * hover; if let Some(element) = overview_chrome_element( state, @@ -172,7 +210,9 @@ pub(super) fn elements( } } if settings.outline_width > 0.0 { - let color = settings.outline_color.unwrap_or(state.theme_settings.border_color.0); + let color = settings + .outline_color + .unwrap_or(state.theme_settings.border_color.0); if let Some(element) = overview_chrome_element( state, renderer, @@ -224,7 +264,8 @@ pub(super) fn elements( output_geometry.size.h as f64, ); let rect = Rect::new( - (content.x + (content.width - width) * 0.5).clamp(bounds.x, bounds.x + bounds.width - width), + (content.x + (content.width - width) * 0.5) + .clamp(bounds.x, bounds.x + bounds.width - width), (content.y + settings.padding).clamp(bounds.y, bounds.y + bounds.height - g.row_height), width, g.row_height, @@ -263,7 +304,11 @@ pub(super) fn elements( g.rail.width, g.segment_height, ); - let color = if id == bar.group.active { accent } else { inactive }; + let color = if id == bar.group.active { + accent + } else { + inactive + }; if let Some(element) = overview_chrome_element( state, renderer, diff --git a/compositor/src/render/tabs.rs b/compositor/src/render/tabs.rs index c0feeb8..e8ffa59 100644 --- a/compositor/src/render/tabs.rs +++ b/compositor/src/render/tabs.rs @@ -48,7 +48,12 @@ pub(super) fn tab_elements( }; let scale = output.current_scale().fractional_scale(); let width = content.width / bar.group.windows.len() as f64; - let shell = ferese_layout::Rect::new(content.x, content.y - bar.rect.height, content.width, bar.rect.height); + let shell = ferese_layout::Rect::new( + content.x, + content.y - bar.rect.height, + content.width, + bar.rect.height, + ); let radius = state .theme_settings .material_radius @@ -89,14 +94,18 @@ pub(super) fn tab_elements( + ((rect.width - f64::from(size.w) / scale) * 0.5).max(8.0)) * scale) .round() as i32, - ((rect.y - f64::from(output_geometry.loc.y) + rect.height * 0.5) * scale).round() as i32 - size.h / 2, + ((rect.y - f64::from(output_geometry.loc.y) + rect.height * 0.5) * scale).round() + as i32 + - size.h / 2, )); if let Ok(element) = MemoryRenderBufferRenderElement::from_buffer( renderer, location.to_f64(), &buffer, Some(alpha), - Some(Rectangle::from_size((f64::from(size.w), f64::from(size.h)).into())), + Some(Rectangle::from_size( + (f64::from(size.w), f64::from(size.h)).into(), + )), Some( ( (f64::from(size.w) / scale).round() as i32, @@ -107,11 +116,19 @@ pub(super) fn tab_elements( RenderElementKind::Unspecified, ) { let element = RescaleRenderElement::from_element(element, location, 1.0); - let element = RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); - let clip = ferese_layout::Rect::new(rect.x + 8.0, rect.y, (rect.width - 16.0).max(0.0), rect.height); - if let Some(element) = - CropRenderElement::from_element(element, scale, physical_rect(clip, output_geometry.loc, scale)) - { + let element = + RelocateRenderElement::from_element(element, (0, 0), Relocate::Relative); + let clip = ferese_layout::Rect::new( + rect.x + 8.0, + rect.y, + (rect.width - 16.0).max(0.0), + rect.height, + ); + if let Some(element) = CropRenderElement::from_element( + element, + scale, + physical_rect(clip, output_geometry.loc, scale), + ) { elements.push(element.into()); } } @@ -126,7 +143,10 @@ pub(super) fn tab_elements( // The shell palette supplies the surface and accent. Keep selection // inside the shell so its corners follow the same continuous profile. let mut fill = state.theme_settings.surface_raised_color.0; - for (channel, accent) in fill[..3].iter_mut().zip(state.theme_settings.accent_color.0) { + for (channel, accent) in fill[..3] + .iter_mut() + .zip(state.theme_settings.accent_color.0) + { *channel += (accent - *channel) * 0.16; } fill[3] = alpha * opacity; diff --git a/compositor/src/render/window_blur.rs b/compositor/src/render/window_blur.rs index 7bb3c87..4d92429 100644 --- a/compositor/src/render/window_blur.rs +++ b/compositor/src/render/window_blur.rs @@ -68,22 +68,34 @@ pub(super) fn window_blur_element( }; let key = (renderer.context_id().erased(), state.output_id(output)?); let program = blur_program(&mut state.render, renderer)?; - let buffers = &mut state.render.windows.entry(presentation.id).or_default().blur; + let buffers = &mut state + .render + .windows + .entry(presentation.id) + .or_default() + .blur; let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); let capture_size = Size::from((capture_rect[2], capture_rect[3])); if buffers .get(&key) .is_none_or(|cached| cached.element.texture.size() != capture_size) { - let texture = match Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, capture_size) { - Ok(texture) => texture, - Err(error) => { - tracing::warn!(%error, "window blur allocation failed"); - buffers.remove(&key); - return None; - } - }; - let element = BlurRenderElement::new(texture, program.0, ¶meters, Arc::new(AtomicBool::new(true))); + let texture = + match Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, capture_size) + { + Ok(texture) => texture, + Err(error) => { + tracing::warn!(%error, "window blur allocation failed"); + buffers.remove(&key); + return None; + } + }; + let element = BlurRenderElement::new( + texture, + program.0, + ¶meters, + Arc::new(AtomicBool::new(true)), + ); buffers.insert( key.clone(), WindowBlur { diff --git a/compositor/src/render/window_content.rs b/compositor/src/render/window_content.rs index 79eb577..e5f43a2 100644 --- a/compositor/src/render/window_content.rs +++ b/compositor/src/render/window_content.rs @@ -34,7 +34,11 @@ pub(super) fn snapshot_element( .into(), ) .intersection(pixels)?; - let clip = framebuffer_clip_rect(pixels, output.current_mode()?.size, output.current_transform().invert()); + let clip = framebuffer_clip_rect( + pixels, + output.current_mode()?.size, + output.current_transform().invert(), + ); Some(NativeTextureElement { id: snapshot.id.clone(), commit: snapshot.commit, @@ -46,7 +50,10 @@ pub(super) fn snapshot_element( ))), alpha: presentation.alpha() * handoff, program: Some(programs.texture.clone()), - uniforms: vec![Uniform::new("clip_rect", clip), Uniform::new("radius", corners.radius)], + uniforms: vec![ + Uniform::new("clip_rect", clip), + Uniform::new("radius", corners.radius), + ], }) } @@ -84,7 +91,8 @@ mod tests { Some((0, 0).into()), ); let mut resources = RenderResources::default(); - let programs = corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); + let programs = + corner_program(&mut resources, &mut renderer, CornerShape::Continuous).unwrap(); for (scale, focus_alpha) in [1.0, 1.25, 1.5, 2.0] .into_iter() .flat_map(|scale| [1.0, 0.8].map(|alpha| (scale, alpha))) @@ -100,17 +108,22 @@ mod tests { scale_content: false, native_size: None, }; - let corners = - RoundedRect::new(live.bounds.current, (0, 0).into(), scale, 8.0).with_shape(CornerShape::Continuous); + let corners = RoundedRect::new(live.bounds.current, (0, 0).into(), scale, 8.0) + .with_shape(CornerShape::Continuous); let size = corners.rect.size; let mut texture: GlesTexture = renderer .create_buffer(Fourcc::Abgr8888, (size.w, size.h).into()) .unwrap(); { let mut target = renderer.bind(&mut texture).unwrap(); - let mut frame = renderer.render(&mut target, size, Transform::Normal).unwrap(); + let mut frame = renderer + .render(&mut target, size, Transform::Normal) + .unwrap(); frame - .clear(Color32F::new(1.0, 0.0, 0.0, 1.0), &[Rectangle::from_size(size)]) + .clear( + Color32F::new(1.0, 0.0, 0.0, 1.0), + &[Rectangle::from_size(size)], + ) .unwrap(); let _ = frame.finish().unwrap(); } @@ -127,10 +140,20 @@ mod tests { closing.close(); let mut pixels = Vec::new(); for presentation in [live, closing] { - let element = - snapshot_element(&snapshot, presentation, corners, scale, &output, &programs, 1.0).unwrap(); + let element = snapshot_element( + &snapshot, + presentation, + corners, + scale, + &output, + &programs, + 1.0, + ) + .unwrap(); assert_eq!(element.geometry, corners.rect); - let mut texture: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (128, 128).into()).unwrap(); + let mut texture: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (128, 128).into()) + .unwrap(); { let mut target = renderer.bind(&mut texture).unwrap(); let mut frame = renderer @@ -148,14 +171,23 @@ mod tests { let _ = frame.finish().unwrap(); } let mapping = renderer - .copy_texture(&texture, Rectangle::from_size((128, 128).into()), Fourcc::Abgr8888) + .copy_texture( + &texture, + Rectangle::from_size((128, 128).into()), + Fourcc::Abgr8888, + ) .unwrap(); pixels.push(renderer.map_texture(&mapping).unwrap().to_vec()); } - assert_eq!(pixels[0], pixels[1], "close handoff changed pixels at scale {scale}"); + assert_eq!( + pixels[0], pixels[1], + "close handoff changed pixels at scale {scale}" + ); let expected = (focus_alpha * 255.0).round() as u8; assert!( - pixels[0].chunks_exact(4).any(|pixel| pixel[3].abs_diff(expected) <= 1), + pixels[0] + .chunks_exact(4) + .any(|pixel| pixel[3].abs_diff(expected) <= 1), "snapshot alpha must be applied once" ); assert!( @@ -183,7 +215,10 @@ mod tests { assert_eq!(predicted.presentation.focus_alpha, focus_alpha); let faded = &predicted.handoff.as_ref().unwrap().0; assert_eq!(faded.elapsed, Duration::from_millis(40)); - assert_eq!(retained.handoff.as_ref().unwrap().0.elapsed, Duration::from_millis(20)); + assert_eq!( + retained.handoff.as_ref().unwrap().0.elapsed, + Duration::from_millis(20) + ); let element = snapshot_element( faded, predicted.presentation, @@ -200,7 +235,10 @@ mod tests { assert!(retained.handoff.is_none()); let thumbnail = live.thumbnail(ferese_layout::Rect::new(4.25, 3.75, 16.5, 12.25)); let corners = RoundedRect::new(thumbnail.bounds.current, (0, 0).into(), scale, 4.0); - let element = snapshot_element(&snapshot, thumbnail, corners, scale, &output, &programs, 1.0).unwrap(); + let element = snapshot_element( + &snapshot, thumbnail, corners, scale, &output, &programs, 1.0, + ) + .unwrap(); assert_eq!(element.geometry, corners.rect); assert!((element.source.size.w - f64::from(size.w)).abs() < 1e-9); assert!((element.source.size.h - f64::from(size.h)).abs() < 1e-9); diff --git a/compositor/src/resize_metrics.rs b/compositor/src/resize_metrics.rs index 0874f3b..489fc15 100644 --- a/compositor/src/resize_metrics.rs +++ b/compositor/src/resize_metrics.rs @@ -7,7 +7,9 @@ use std::time::Duration; use ferese_core::WorkspaceId; use ferese_layout::WindowId; -const BUCKETS_MS: [u64; 16] = [16, 32, 64, 75, 80, 85, 100, 150, 250, 290, 299, 300, 301, 310, 350, 500]; +const BUCKETS_MS: [u64; 16] = [ + 16, 32, 64, 75, 80, 85, 100, 150, 250, 290, 299, 300, 301, 310, 350, 500, +]; #[derive(Debug, Default)] struct Histogram { @@ -64,7 +66,13 @@ pub(crate) struct ResizeMetrics { } impl ResizeMetrics { - pub(crate) fn begin(&mut self, id: WindowId, workspace: Option, app_id: &str, now: Duration) { + pub(crate) fn begin( + &mut self, + id: WindowId, + workspace: Option, + app_id: &str, + now: Duration, + ) { // Keep an overlapping workspace barrier continuous through retargets. let previous_workspace = self.end_client(id, now, End::Replaced); if previous_workspace != workspace { @@ -123,7 +131,13 @@ impl ResizeMetrics { } } - pub(crate) fn observe(&mut self, id: WindowId, now: Duration, viewport_active: bool, other_active: bool) { + pub(crate) fn observe( + &mut self, + id: WindowId, + now: Duration, + viewport_active: bool, + other_active: bool, + ) { let Some(client) = self.clients.get_mut(&id) else { return; }; @@ -151,7 +165,11 @@ impl ResizeMetrics { pub(crate) fn pause_clock(&mut self, now: Duration) { // Activity overlap excludes globally paused presentation time. Barrier // and target-to-handoff durations remain actual wall-clock durations. - for client in self.clients.values_mut().filter(|client| client.started.is_some()) { + for client in self + .clients + .values_mut() + .filter(|client| client.started.is_some()) + { client.last_observation = now; } } @@ -181,7 +199,9 @@ impl ResizeMetrics { pub(crate) fn dump(&self) { // Enabling the feature explicitly opts into the exit report. This does // not depend on a tracing filter and adds no per-frame log messages. - eprintln!("resize-metrics: histogram upper bounds ms {BUCKETS_MS:?}, final bucket overflow"); + eprintln!( + "resize-metrics: histogram upper bounds ms {BUCKETS_MS:?}, final bucket overflow" + ); eprintln!( "resize-metrics: workspace barrier durations {:?}", self.workspace_duration diff --git a/compositor/src/resize_transaction.rs b/compositor/src/resize_transaction.rs index b6202cb..d84fa61 100644 --- a/compositor/src/resize_transaction.rs +++ b/compositor/src/resize_transaction.rs @@ -102,7 +102,8 @@ mod tests { #[test] fn shrinking_commit_retains_the_previous_full_frame_geometry() { let source = Rectangle::new((4, 8).into(), (1200, 800).into()); - let transaction = ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source); + let transaction = + ResizeTransaction::new(1.into(), Duration::ZERO).with_source_geometry(source); assert_eq!(transaction.source_geometry(), Some(source)); } } diff --git a/compositor/src/resume.rs b/compositor/src/resume.rs index bb6d7cc..0448ea5 100644 --- a/compositor/src/resume.rs +++ b/compositor/src/resume.rs @@ -109,9 +109,11 @@ pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result { monitor.source = Some((handle, token)); Ok(monitor) @@ -255,7 +257,10 @@ async fn listen( // Owner changes queued during subscription setup are already reflected in the // GetNameOwner reply. Only a later change interrupts this recovered listener. -async fn owner_changed_after(owners: &mut zbus::proxy::OwnerChangedStream<'_>, baseline: zbus::message::Sequence) { +async fn owner_changed_after( + owners: &mut zbus::proxy::OwnerChangedStream<'_>, + baseline: zbus::message::Sequence, +) { use zbus::export::ordered_stream::OrderedStreamExt; while let Some((sequence, _)) = owners.next_before(None).await.into_tuple() { if sequence > baseline { diff --git a/compositor/src/resume/tests.rs b/compositor/src/resume/tests.rs index 91b4555..0c03e05 100644 --- a/compositor/src/resume/tests.rs +++ b/compositor/src/resume/tests.rs @@ -8,12 +8,18 @@ use super::*; #[test] fn warning_suppression_resets_at_subscription_not_at_next_resume() { let mut recovery = Recovery::default(); - assert!(!recovery.subscribed(), "startup is not a recovery or a resume"); + assert!( + !recovery.subscribed(), + "startup is not a recovery or a resume" + ); assert!(recovery.unavailable()); assert!(!recovery.unavailable(), "one warning per outage"); assert!(recovery.subscribed()); assert!(!recovery.subscribed(), "one reconciliation per recovery"); - assert!(recovery.unavailable(), "another outage must warn even without a resume"); + assert!( + recovery.unavailable(), + "another outage must warn even without a resume" + ); assert!(!recovery.unavailable()); } @@ -25,7 +31,10 @@ fn stop_and_join(mut monitor: Monitor) { std::thread::sleep(Duration::from_millis(2)); } - assert!(worker.is_finished(), "cancellation did not stop the worker promptly"); + assert!( + worker.is_finished(), + "cancellation did not stop the worker promptly" + ); worker.join().unwrap(); } @@ -130,7 +139,9 @@ fn confirm_subscription(manager: &zbus::blocking::Connection, events: &Receiver< #[test] fn reconnect_reconciles_a_resume_missed_during_the_outage() { - if !crate::startup_tests::private_runtime("resume::tests::reconnect_reconciles_a_resume_missed_during_the_outage") { + if !crate::startup_tests::private_runtime( + "resume::tests::reconnect_reconciles_a_resume_missed_during_the_outage", + ) { return; } @@ -159,18 +170,26 @@ fn reconnect_reconciles_a_resume_missed_during_the_outage() { let manager = replacement.manager(); signal(&manager, false); // No listener can receive this resume. *address.lock().unwrap() = replacement.address.clone(); - assert_eq!(events.recv_timeout(Duration::from_secs(2)).unwrap(), Refresh::Reconcile); + assert_eq!( + events.recv_timeout(Duration::from_secs(2)).unwrap(), + Refresh::Reconcile + ); assert!(events.recv_timeout(Duration::from_millis(80)).is_err()); signal(&manager, true); assert!(events.recv_timeout(Duration::from_millis(40)).is_err()); signal(&manager, false); - assert_eq!(events.recv_timeout(Duration::from_secs(1)).unwrap(), Refresh::Resumed); + assert_eq!( + events.recv_timeout(Duration::from_secs(1)).unwrap(), + Refresh::Resumed + ); stop_and_join(monitor); } #[test] fn shutdown_interrupts_a_healthy_signal_wait() { - if !crate::startup_tests::private_runtime("resume::tests::shutdown_interrupts_a_healthy_signal_wait") { + if !crate::startup_tests::private_runtime( + "resume::tests::shutdown_interrupts_a_healthy_signal_wait", + ) { return; } @@ -190,7 +209,9 @@ fn shutdown_interrupts_a_healthy_signal_wait() { #[test] fn logind_replacement_reconciles_without_a_bus_disconnect() { - if !crate::startup_tests::private_runtime("resume::tests::logind_replacement_reconciles_without_a_bus_disconnect") { + if !crate::startup_tests::private_runtime( + "resume::tests::logind_replacement_reconciles_without_a_bus_disconnect", + ) { return; } @@ -211,15 +232,23 @@ fn logind_replacement_reconciles_without_a_bus_disconnect() { "absent logind is not recovery" ); let replacement = bus.manager(); - assert_eq!(events.recv_timeout(Duration::from_secs(2)).unwrap(), Refresh::Reconcile); + assert_eq!( + events.recv_timeout(Duration::from_secs(2)).unwrap(), + Refresh::Reconcile + ); signal(&replacement, false); - assert_eq!(events.recv_timeout(Duration::from_secs(1)).unwrap(), Refresh::Resumed); + assert_eq!( + events.recv_timeout(Duration::from_secs(1)).unwrap(), + Refresh::Resumed + ); stop_and_join(monitor); } #[test] fn shutdown_interrupts_connection_authentication() { - if !crate::startup_tests::private_runtime("resume::tests::shutdown_interrupts_connection_authentication") { + if !crate::startup_tests::private_runtime( + "resume::tests::shutdown_interrupts_connection_authentication", + ) { return; } diff --git a/compositor/src/session_environment.rs b/compositor/src/session_environment.rs index b92126a..a9a455a 100644 --- a/compositor/src/session_environment.rs +++ b/compositor/src/session_environment.rs @@ -50,7 +50,9 @@ impl SessionEnvironment { .env_remove("XAUTHORITY"); if let Some(x11) = &self.x11 { - command.env("DISPLAY", &x11.display).env("XAUTHORITY", &x11.authority); + command + .env("DISPLAY", &x11.display) + .env("XAUTHORITY", &x11.authority); } if self.policy == SessionPolicy::Embedded { command.env("FERESE_SESSION_IMPORT_ENV", "0"); @@ -125,7 +127,10 @@ mod tests { environment(None).apply_public(&mut command); assert_eq!(value_of(&command, "WAYLAND_SOCKET"), Some(None)); - assert_eq!(value_of(&command, "FERESE_SHELL_CONTROL_SOCKET"), Some(None)); + assert_eq!( + value_of(&command, "FERESE_SHELL_CONTROL_SOCKET"), + Some(None) + ); } #[test] @@ -171,7 +176,9 @@ mod tests { environment(None).apply_public(&mut command); assert_eq!( value_of(&command, "FERESE_SOCKET"), - Some(Some("/run/user/1000/ferese/instances/preview/control.sock".into())) + Some(Some( + "/run/user/1000/ferese/instances/preview/control.sock".into() + )) ); } @@ -183,8 +190,14 @@ mod tests { let mut embedded = environment(None); embedded.policy = SessionPolicy::Embedded; embedded.apply_public(&mut command); - assert_eq!(value_of(&command, "FERESE_SESSION_IMPORT_ENV"), Some(Some("0".into()))); - assert_eq!(value_of(&command, "FERESE_SESSION_MODE"), Some(Some("embedded".into()))); + assert_eq!( + value_of(&command, "FERESE_SESSION_IMPORT_ENV"), + Some(Some("0".into())) + ); + assert_eq!( + value_of(&command, "FERESE_SESSION_MODE"), + Some(Some("embedded".into())) + ); } #[test] @@ -198,7 +211,10 @@ mod tests { value_of(&command, "FERESE_SESSION_IMPORT_ENV"), Some(Some(choice.into())) ); - assert_eq!(value_of(&command, "FERESE_SESSION_MODE"), Some(Some("desktop".into()))); + assert_eq!( + value_of(&command, "FERESE_SESSION_MODE"), + Some(Some("desktop".into())) + ); } } } diff --git a/compositor/src/session_lock.rs b/compositor/src/session_lock.rs index 5fba7dd..62e5fe6 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -45,7 +45,8 @@ impl IdleSettings { } fn next_deadline(self, elapsed: Duration, fade_duration: Duration) -> Option { - if self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds) { + if self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds) + { return None; } let dim_at = Duration::from_secs(self.dim_after_seconds); @@ -65,14 +66,16 @@ impl IdleSettings { } fn appearance(self, elapsed: Duration, fade_duration: Duration) -> (f32, bool) { - let sleeping = self.sleep_after_seconds != 0 && elapsed >= Duration::from_secs(self.sleep_after_seconds); + let sleeping = self.sleep_after_seconds != 0 + && elapsed >= Duration::from_secs(self.sleep_after_seconds); let dim_at = Duration::from_secs(self.dim_after_seconds); let dim = if self.dim_after_seconds == 0 || elapsed < dim_at { 0.0 } else if fade_duration.is_zero() { 0.65 } else { - (elapsed.saturating_sub(dim_at).as_secs_f32() / fade_duration.as_secs_f32()).min(1.0) * 0.65 + (elapsed.saturating_sub(dim_at).as_secs_f32() / fade_duration.as_secs_f32()).min(1.0) + * 0.65 }; (if sleeping { 1.0 } else { dim }, sleeping) } @@ -110,7 +113,8 @@ pub(crate) struct Lock { idle_since: Option, pub(crate) idle_opacity: f32, pub(crate) sleeping: bool, - pub(crate) idle_overlays: HashMap, + pub(crate) idle_overlays: + HashMap, idle_timer: Option<(RegistrationToken, Instant)>, } @@ -133,7 +137,10 @@ impl Lock { } fn ready_for_idle<'a>(&self, outputs: impl Iterator) -> bool { - self.active() && outputs.into_iter().all(|output| self.presented.contains(output)) + self.active() + && outputs + .into_iter() + .all(|output| self.presented.contains(output)) } pub(crate) fn output_added(&mut self, output: &Output) { @@ -253,7 +260,8 @@ impl Ferese { let (opacity, sleeping) = self .lock_idle .appearance(now.saturating_duration_since(*since), fade_duration); - let changed = opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping; + let changed = + opacity != self.session_lock.idle_opacity || sleeping != self.session_lock.sleeping; let was_sleeping = self.session_lock.sleeping; self.session_lock.idle_opacity = opacity; self.session_lock.sleeping = sleeping; @@ -298,12 +306,16 @@ impl Ferese { let lock_click = pointer .with_grab(|_, grab| { grab.is::>() - && grab.start_data().focus.as_ref().is_some_and(|(surface, _)| { - self.session_lock - .surfaces - .values() - .any(|lock| lock.alive() && lock.wl_surface() == surface) - }) + && grab + .start_data() + .focus + .as_ref() + .is_some_and(|(surface, _)| { + self.session_lock + .surfaces + .values() + .any(|lock| lock.alive() && lock.wl_surface() == surface) + }) }) .unwrap_or(false); if pointer.is_grabbed() && !lock_click { @@ -408,7 +420,12 @@ impl Ferese { pub(crate) fn lock_frame_callbacks(&self, output: &Output) { let eligible = self.callback_outputs(); - if let Some(surface) = self.session_lock.surfaces.get(output).filter(|surface| surface.alive()) { + if let Some(surface) = self + .session_lock + .surfaces + .get(output) + .filter(|surface| surface.alive()) + { smithay::desktop::utils::send_frames_surface_tree( surface.wl_surface(), output, @@ -452,7 +469,10 @@ impl SessionLockHandler for Ferese { // already encoding has no pending capture left, so it is terminated // here and its staged file is discarded when the worker reports back. for request in self.screenshot.terminate_all() { - tracing::debug!(request, "cancelled an in-flight screenshot for the session lock"); + tracing::debug!( + request, + "cancelled an in-flight screenshot for the session lock" + ); } self.set_overview_active(false); let serial = SERIAL_COUNTER.next_serial(); @@ -503,8 +523,9 @@ impl SessionLockHandler for Ferese { 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())); + surface.with_pending_state(|state| { + state.size = Some((geometry.size.w as u32, geometry.size.h as u32).into()) + }); crate::handlers::set_surface_tree_output(surface.wl_surface(), &output); self.session_lock.surfaces.insert(output, surface); self.focus_lock_surface(); @@ -535,10 +556,15 @@ impl Dispatch for Ferese { return; } if matches!(request, ext_session_lock_v1::Request::GetLockSurface { .. }) && !owner { - lock.post_error(ext_session_lock_v1::Error::InvalidUnlock, "not the active lock owner"); + lock.post_error( + ext_session_lock_v1::Error::InvalidUnlock, + "not the active lock owner", + ); return; } - smithay::wayland::Dispatch2::request(data, state, client, lock, request, display, data_init); + smithay::wayland::Dispatch2::request( + data, state, client, lock, request, display, data_init, + ); } fn destroyed( diff --git a/compositor/src/session_lock/tests.rs b/compositor/src/session_lock/tests.rs index ac00bdc..7b41fa8 100644 --- a/compositor/src/session_lock/tests.rs +++ b/compositor/src/session_lock/tests.rs @@ -5,20 +5,29 @@ fn dim_fade_respects_speed_without_changing_idle_deadlines() { let policy = IdleSettings::default(); for (duration, half_at) in [(250, 30_125), (1000, 30_500)] { let fade = Duration::from_millis(duration); - assert_eq!(policy.appearance(Duration::from_secs(29), fade), (0.0, false)); + assert_eq!( + policy.appearance(Duration::from_secs(29), fade), + (0.0, false) + ); let halfway = policy.appearance(Duration::from_millis(half_at), fade); assert!((halfway.0 - 0.325).abs() < 0.001 && !halfway.1); assert_eq!( policy.next_deadline(Duration::from_millis(half_at), fade), Some(Duration::from_millis(16)) ); - assert_eq!(policy.appearance(Duration::from_secs(120), fade), (1.0, true)); + assert_eq!( + policy.appearance(Duration::from_secs(120), fade), + (1.0, true) + ); } assert_eq!( policy.appearance(Duration::from_secs(30), Duration::ZERO), (0.65, false) ); - assert_eq!(policy.appearance(Duration::from_secs(120), Duration::ZERO), (1.0, true)); + assert_eq!( + policy.appearance(Duration::from_secs(120), Duration::ZERO), + (1.0, true) + ); } #[test] @@ -54,7 +63,10 @@ fn idle_policy_fades_then_sleeps_and_activity_resets_it() { policy.next_deadline(Duration::from_secs(30), Duration::ZERO), Some(Duration::from_secs(90)) ); - assert_eq!(policy.next_deadline(Duration::from_secs(120), Duration::ZERO), None); + assert_eq!( + policy.next_deadline(Duration::from_secs(120), Duration::ZERO), + None + ); assert_eq!( IdleSettings { dim_after_seconds: 0, @@ -215,7 +227,9 @@ impl Fixture { } fn dispatch(&mut self) { - self.events.dispatch(Duration::from_millis(5), &mut self.state).unwrap(); + self.events + .dispatch(Duration::from_millis(5), &mut self.state) + .unwrap(); crate::after_dispatch(&mut self.state); } @@ -282,11 +296,16 @@ impl Locker { } fn expect_locked(&mut self) { - self.wire.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + self.wire + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); let mut event = [0u8; 8]; self.wire.read_exact(&mut event).unwrap(); assert_eq!(u32::from_ne_bytes(event[..4].try_into().unwrap()), 2); - assert_eq!(u32::from_ne_bytes(event[4..].try_into().unwrap()), 8u32 << 16); + assert_eq!( + u32::from_ne_bytes(event[4..].try_into().unwrap()), + 8u32 << 16 + ); } fn surface(&mut self, fixture: &mut Fixture, output: &Output) -> LockSurface { @@ -304,10 +323,13 @@ impl Locker { let object = u32::from_ne_bytes(events[offset..offset + 4].try_into().unwrap()); let header = u32::from_ne_bytes(events[offset + 4..offset + 8].try_into().unwrap()); if object == registry && header & 0xffff == 0 { - let len = u32::from_ne_bytes(events[offset + 12..offset + 16].try_into().unwrap()) as usize; + let len = u32::from_ne_bytes(events[offset + 12..offset + 16].try_into().unwrap()) + as usize; if &events[offset + 16..offset + 16 + len] == b"wl_output\0" { - let name = u32::from_ne_bytes(events[offset + 8..offset + 12].try_into().unwrap()); - output_name = Some(output_name.map_or(name, |previous: u32| previous.max(name))); + let name = + u32::from_ne_bytes(events[offset + 8..offset + 12].try_into().unwrap()); + output_name = + Some(output_name.map_or(name, |previous: u32| previous.max(name))); } } @@ -332,7 +354,12 @@ impl Locker { ) .unwrap(); let surface_id = self.id(); - request(&mut self.wire, compositor.id().protocol_id(), 0, &[surface_id]); + request( + &mut self.wire, + compositor.id().protocol_id(), + 0, + &[surface_id], + ); let lock_surface = self.id(); request(&mut self.wire, 2, 1, &[lock_surface, surface_id, output_id]); fixture.dispatch(); @@ -357,9 +384,9 @@ fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32]) { #[test] fn locking_exits_binding_modes_and_retains_consumed_release_ownership() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::locking_exits_binding_modes_and_retains_consumed_release_ownership") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::locking_exits_binding_modes_and_retains_consumed_release_ownership", + ) else { return; }; fixture @@ -382,9 +409,9 @@ fn locking_exits_binding_modes_and_retains_consumed_release_ownership() { #[test] fn acquiring_lock_revokes_activity_and_media_subscriptions() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::acquiring_lock_revokes_activity_and_media_subscriptions") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::acquiring_lock_revokes_activity_and_media_subscriptions", + ) else { return; }; fixture.output("pending"); @@ -392,10 +419,18 @@ fn acquiring_lock_revokes_activity_and_media_subscriptions() { let (activities, activity_reply) = std::sync::mpsc::sync_channel(1); let (media, media_reply) = std::sync::mpsc::sync_channel(1); fixture.state.activities.watch(1, 11, revision, activities); - let media_revision = fixture.state.media_engine.value()["revision"].as_u64().unwrap(); - fixture.state.media_engine.watch(2, 22, media_revision, media); + let media_revision = fixture.state.media_engine.value()["revision"] + .as_u64() + .unwrap(); + fixture + .state + .media_engine + .watch(2, 22, media_revision, media); let _locker = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); for (id, receiver) in [(11, activity_reply), (22, media_reply)] { let response = receiver.recv_timeout(Duration::from_secs(1)).unwrap(); assert_eq!(response.id, id); @@ -406,37 +441,57 @@ fn acquiring_lock_revokes_activity_and_media_subscriptions() { #[test] fn lock_confirms_without_outputs() { - let Some(mut fixture) = Fixture::new("session_lock::tests::lock_confirms_without_outputs") else { + let Some(mut fixture) = Fixture::new("session_lock::tests::lock_confirms_without_outputs") + else { return; }; let epoch = fixture.state.capture_epoch; let mut locker = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); - assert_ne!(fixture.state.capture_epoch, epoch, "lock revokes deferred snapshots"); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); + assert_ne!( + fixture.state.capture_epoch, epoch, + "lock revokes deferred snapshots" + ); locker.expect_locked(); } #[test] fn lock_waits_for_each_output_before_confirmation() { - let Some(mut fixture) = Fixture::new("session_lock::tests::lock_waits_for_each_output_before_confirmation") else { + let Some(mut fixture) = + Fixture::new("session_lock::tests::lock_waits_for_each_output_before_confirmation") + else { return; }; let first = fixture.output("first"); let second = fixture.output("second"); let mut locker = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); fixture.state.lock_frame_presented(&first); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); assert!(fixture.state.session_lock.idle_timer.is_none()); fixture.state.lock_frame_presented(&second); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); fixture.dispatch(); locker.expect_locked(); } #[test] fn live_owner_rejects_competitor_unlock() { - let Some(mut fixture) = Fixture::new("session_lock::tests::live_owner_rejects_competitor_unlock") else { + let Some(mut fixture) = + Fixture::new("session_lock::tests::live_owner_rejects_competitor_unlock") + else { return; }; let _owner_wire = fixture.acquire(); @@ -451,22 +506,31 @@ fn live_owner_rejects_competitor_unlock() { #[test] fn pending_owner_cannot_unlock_before_confirmation() { - let Some(mut fixture) = Fixture::new("session_lock::tests::pending_owner_cannot_unlock_before_confirmation") else { + let Some(mut fixture) = + Fixture::new("session_lock::tests::pending_owner_cannot_unlock_before_confirmation") + else { return; }; fixture.output("pending"); let mut pending = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); request(&mut pending.wire, 2, 2, &[]); fixture.dispatch(); assert!(fixture.state.session_lock.active()); fixture.dispatch(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned)); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Orphaned + )); } #[test] fn owner_disconnect_stays_locked_and_allows_replacement() { - let Some(mut fixture) = Fixture::new("session_lock::tests::owner_disconnect_stays_locked_and_allows_replacement") + let Some(mut fixture) = + Fixture::new("session_lock::tests::owner_disconnect_stays_locked_and_allows_replacement") else { return; }; @@ -474,31 +538,46 @@ fn owner_disconnect_stays_locked_and_allows_replacement() { let owner = fixture.state.session_lock.lifecycle.owner().cloned(); drop(owner_wire); fixture.dispatch(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned)); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Orphaned + )); assert!(fixture.state.session_lock.active()); let mut replacement = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); assert_ne!(fixture.state.session_lock.lifecycle.owner(), owner.as_ref()); replacement.expect_locked(); } #[test] fn removing_last_output_confirms_pending_lock() { - let Some(mut fixture) = Fixture::new("session_lock::tests::removing_last_output_confirms_pending_lock") else { + let Some(mut fixture) = + Fixture::new("session_lock::tests::removing_last_output_confirms_pending_lock") + else { return; }; let output = fixture.output("last"); let mut pending = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); fixture.state.unregister_output(&output); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); fixture.dispatch(); pending.expect_locked(); } #[test] fn refresh_after_removal_confirms_and_starts_idle_timer() { - let Some(mut fixture) = Fixture::new("session_lock::tests::refresh_after_removal_confirms_and_starts_idle_timer") + let Some(mut fixture) = + Fixture::new("session_lock::tests::refresh_after_removal_confirms_and_starts_idle_timer") else { return; }; @@ -506,9 +585,15 @@ fn refresh_after_removal_confirms_and_starts_idle_timer() { let mut pending = fixture.acquire(); fixture.state.space.unmap_output(&output); fixture.state.session_lock.output_removed(&output); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); fixture.state.refresh_lock_outputs(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); assert!(fixture.state.session_lock.idle_timer.is_some()); fixture.dispatch(); pending.expect_locked(); @@ -516,9 +601,9 @@ fn refresh_after_removal_confirms_and_starts_idle_timer() { #[test] fn activity_reuses_timer_and_expired_timer_rechecks_latest_activity() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::activity_reuses_timer_and_expired_timer_rechecks_latest_activity") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::activity_reuses_timer_and_expired_timer_rechecks_latest_activity", + ) else { return; }; fixture.state.session_lock.lifecycle = Lifecycle::Orphaned; @@ -556,9 +641,9 @@ fn activity_reuses_timer_and_expired_timer_rechecks_latest_activity() { #[test] fn earlier_policy_replaces_timer_and_sleeping_activity_arms_one() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::earlier_policy_replaces_timer_and_sleeping_activity_arms_one") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::earlier_policy_replaces_timer_and_sleeping_activity_arms_one", + ) else { return; }; fixture.state.session_lock.lifecycle = Lifecycle::Orphaned; @@ -585,9 +670,9 @@ fn earlier_policy_replaces_timer_and_sleeping_activity_arms_one() { #[test] fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::dead_preferred_surface_uses_live_fallback_in_stable_output_order") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::dead_preferred_surface_uses_live_fallback_in_stable_output_order", + ) else { return; }; let preferred = fixture.output("preferred"); @@ -598,12 +683,25 @@ fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() { let fallback = locker.surface(&mut fixture, &second); let later = locker.surface(&mut fixture, &third); let preferred_id = fixture.state.output_id(&preferred).unwrap(); - fixture.state.output_workspaces.focus_output(preferred_id).unwrap(); - request(&mut locker.wire, dead.wl_surface().id().protocol_id(), 0, &[]); + fixture + .state + .output_workspaces + .focus_output(preferred_id) + .unwrap(); + request( + &mut locker.wire, + dead.wl_surface().id().protocol_id(), + 0, + &[], + ); fixture.dispatch(); assert!(!dead.alive()); // Exercise the defensive path even though destruction normally removes it. - fixture.state.session_lock.surfaces.insert(preferred.clone(), dead); + fixture + .state + .session_lock + .surfaces + .insert(preferred.clone(), dead); for _ in 0..5 { fixture.state.session_lock.surfaces.remove(&second); fixture @@ -611,7 +709,11 @@ fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() { .session_lock .surfaces .insert(second.clone(), fallback.clone()); - fixture.state.session_lock.surfaces.insert(third.clone(), later.clone()); + fixture + .state + .session_lock + .surfaces + .insert(third.clone(), later.clone()); fixture.state.focus_lock_surface(); assert_eq!( fixture.state.seat.get_keyboard().unwrap().current_focus(), @@ -622,14 +724,18 @@ fn dead_preferred_surface_uses_live_fallback_in_stable_output_order() { #[test] fn output_removed_clears_all_lock_resources() { - let Some(mut fixture) = Fixture::new("session_lock::tests::output_removed_clears_all_lock_resources") else { + let Some(mut fixture) = + Fixture::new("session_lock::tests::output_removed_clears_all_lock_resources") + else { return; }; let output = fixture.output("removed"); let mut locker = fixture.acquire(); locker.surface(&mut fixture, &output); - let background = - smithay::backend::renderer::element::solid::SolidColorBuffer::new((800, 600), [0.0, 0.0, 0.0, 1.0]); + let background = smithay::backend::renderer::element::solid::SolidColorBuffer::new( + (800, 600), + [0.0, 0.0, 0.0, 1.0], + ); fixture .state .session_lock @@ -652,7 +758,8 @@ fn output_removed_clears_all_lock_resources() { #[test] fn confirmed_owner_can_unlock_and_cancel_idle_timer() { - let Some(mut fixture) = Fixture::new("session_lock::tests::confirmed_owner_can_unlock_and_cancel_idle_timer") + let Some(mut fixture) = + Fixture::new("session_lock::tests::confirmed_owner_can_unlock_and_cancel_idle_timer") else { return; }; @@ -661,37 +768,52 @@ fn confirmed_owner_can_unlock_and_cancel_idle_timer() { assert!(fixture.state.session_lock.idle_timer.is_some()); request(&mut locker.wire, 2, 2, &[]); fixture.dispatch(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Unlocked)); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Unlocked + )); assert!(fixture.state.session_lock.idle_timer.is_none()); } #[test] fn acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::acquiring_owner_disconnect_keeps_lock_and_replacement_waits_for_frame", + ) else { return; }; let output = fixture.output("pending"); let pending = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); drop(pending); fixture.dispatch(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Orphaned)); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Orphaned + )); assert!(fixture.state.session_lock.active()); let mut replacement = fixture.acquire(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Acquiring(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Acquiring(_) + )); fixture.state.lock_frame_presented(&output); fixture.dispatch(); - assert!(matches!(fixture.state.session_lock.lifecycle, Lifecycle::Locked(_))); + assert!(matches!( + fixture.state.session_lock.lifecycle, + Lifecycle::Locked(_) + )); replacement.expect_locked(); } #[test] fn lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces", + ) else { return; }; let output = fixture.output("lock-pointer"); @@ -745,16 +867,22 @@ fn lock_input_preserves_owner_click_grabs_and_rejects_retired_surfaces() { assert!(pointer.is_grabbed()); fixture.state.session_lock.surfaces.clear(); fixture.state.prepare_lock_input(); - assert!(!pointer.is_grabbed(), "a retired surface cannot keep a lock grab"); + assert!( + !pointer.is_grabbed(), + "a retired surface cannot keep a lock grab" + ); assert_eq!(pointer.current_focus(), None); - assert_eq!(fixture.state.seat.get_keyboard().unwrap().current_focus(), None); + assert_eq!( + fixture.state.seat.get_keyboard().unwrap().current_focus(), + None + ); } #[test] fn ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects() { - let Some(mut fixture) = - Fixture::new("session_lock::tests::ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects") - else { + let Some(mut fixture) = Fixture::new( + "session_lock::tests::ipc_events_follow_lock_lifecycle_without_exposing_desktop_objects", + ) else { return; }; let output = fixture.output("lock-events"); diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index 4a0f20a..bc55c37 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -3,7 +3,9 @@ use ferese_layout::WindowId; use ferese_protocols::shell::v1::server::ferese_shell_manager_v1::FereseShellManagerV1; use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use ferese_protocols::shell::v1::server::{ferese_shell_manager_v1, ferese_shell_v1}; -use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource}; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, +}; use crate::Ferese; use crate::private_client::ClientCapabilities; @@ -48,9 +50,11 @@ impl GlobalDispatch for Ferese { } fn can_view(client: Client, _global_data: &()) -> bool { - client - .get_data::() - .is_some_and(|state| state.capabilities.contains(ClientCapabilities::SHELL_CONTROL)) + client.get_data::().is_some_and(|state| { + state + .capabilities + .contains(ClientCapabilities::SHELL_CONTROL) + }) } } @@ -103,20 +107,28 @@ impl Dispatch for Ferese { window_hi, window_lo, } => { - if let Some((width, height, file)) = state.shell_thumbnail(join_id(window_hi, window_lo)) { + if let Some((width, height, file)) = + state.shell_thumbnail(join_id(window_hi, window_lo)) + { use std::os::fd::AsFd; shell.thumbnail(token, window_hi, window_lo, width, height, file.as_fd()); } else { shell.thumbnail_unavailable(token, window_hi, window_lo); } } - ferese_shell_v1::Request::ActivateWindow { window_hi, window_lo } => state.handle_shell_window_request( + ferese_shell_v1::Request::ActivateWindow { + window_hi, + window_lo, + } => state.handle_shell_window_request( shell, ferese_shell_v1::FailedRequest::ActivateWindow, join_id(window_hi, window_lo), ShellWindowAction::Activate, ), - ferese_shell_v1::Request::CloseWindow { window_hi, window_lo } => state.handle_shell_window_request( + ferese_shell_v1::Request::CloseWindow { + window_hi, + window_lo, + } => state.handle_shell_window_request( shell, ferese_shell_v1::FailedRequest::CloseWindow, join_id(window_hi, window_lo), @@ -129,7 +141,11 @@ impl Dispatch for Ferese { let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo)); if !state.activate_managed_workspace(workspace) { - send_request_failed(shell, ferese_shell_v1::FailedRequest::ActivateWorkspace, workspace.0); + send_request_failed( + shell, + ferese_shell_v1::FailedRequest::ActivateWorkspace, + workspace.0, + ); } } ferese_shell_v1::Request::MoveWindowToWorkspace { @@ -141,7 +157,11 @@ impl Dispatch for Ferese { let window = join_id(window_hi, window_lo); let workspace = WorkspaceId(join_u64(workspace_hi, workspace_lo)); if !state.move_managed_window_to_workspace(window, workspace) { - send_request_failed(shell, ferese_shell_v1::FailedRequest::MoveWindowToWorkspace, window.0); + send_request_failed( + shell, + ferese_shell_v1::FailedRequest::MoveWindowToWorkspace, + window.0, + ); } } ferese_shell_v1::Request::ConfirmLogout { serial } => { @@ -157,21 +177,36 @@ impl Dispatch for Ferese { force, } => { if force <= 1 { - state.confirm_portal_logout(shell, serial, query_token, inhibitor_revision, force == 1); + state.confirm_portal_logout( + shell, + serial, + query_token, + inhibitor_revision, + force == 1, + ); } } ferese_shell_v1::Request::CancelLogout { serial } => { - if state.logout_owner.as_ref() == Some(&shell.id()) && state.pending_logout == Some(serial) { + if state.logout_owner.as_ref() == Some(&shell.id()) + && state.pending_logout == Some(serial) + { state.cancel_logout_confirmation(); } } ferese_shell_v1::Request::EnterOverview => state.set_overview_active(true), ferese_shell_v1::Request::ExitOverview => state.set_overview_active(false), - ferese_shell_v1::Request::SelectOverviewWindow { window_hi, window_lo } => { + ferese_shell_v1::Request::SelectOverviewWindow { + window_hi, + window_lo, + } => { let id = join_id(window_hi, window_lo); if !state.select_overview_window(id) { - send_request_failed(shell, ferese_shell_v1::FailedRequest::SelectOverviewWindow, id.0); + send_request_failed( + shell, + ferese_shell_v1::FailedRequest::SelectOverviewWindow, + id.0, + ); } } ferese_shell_v1::Request::Destroy => { @@ -195,7 +230,12 @@ impl Dispatch for Ferese { } } -fn logout_confirmation_matches(pending: Option, query: Option, serial: u32, token: u32) -> bool { +fn logout_confirmation_matches( + pending: Option, + query: Option, + serial: u32, + token: u32, +) -> bool { pending == Some(serial) && query == Some(token) } @@ -209,7 +249,14 @@ fn consume_logout_confirmation(pending: &mut Option, serial: u32) -> bool { } impl Ferese { - fn confirm_portal_logout(&mut self, shell: &FereseShellV1, serial: u32, token: u32, revision: u32, force: bool) { + fn confirm_portal_logout( + &mut self, + shell: &FereseShellV1, + serial: u32, + token: u32, + revision: u32, + force: bool, + ) { if self.logout_owner.as_ref() != Some(&shell.id()) || !logout_confirmation_matches(self.pending_logout, self.logout_query, serial, token) { @@ -254,7 +301,10 @@ impl Ferese { tracing::warn!("display mode chooser requires the updated Ferese shell"); return false; }; - shell.toggle_display_mode(self.focused_output().map_or_else(String::new, |output| output.name())); + shell.toggle_display_mode( + self.focused_output() + .map_or_else(String::new, |output| output.name()), + ); true } @@ -270,7 +320,10 @@ impl Ferese { else { return false; }; - shell.toggle_launcher(self.focused_output().map_or_else(String::new, |output| output.name())); + shell.toggle_launcher( + self.focused_output() + .map_or_else(String::new, |output| output.name()), + ); true } @@ -287,7 +340,9 @@ impl Ferese { tracing::warn!("shortcut hint requires the updated Ferese shell"); return false; }; - let output = self.focused_output().map_or_else(String::new, |output| output.name()); + let output = self + .focused_output() + .map_or_else(String::new, |output| output.name()); shell.toggle_keybinding_guide(output); true } @@ -312,7 +367,9 @@ impl Ferese { self.logout_query = Some(self.portal_session.begin_query()); self.pending_logout = Some(serial); self.logout_owner = Some(shell.id()); - let output_name = self.focused_output().map_or_else(String::new, |output| output.name()); + let output_name = self + .focused_output() + .map_or_else(String::new, |output| output.name()); shell.logout_requested(serial, output_name); } @@ -321,7 +378,11 @@ impl Ferese { self.portal_session.cancel_query(token); } if let Some(serial) = self.pending_logout.take() { - for shell in self.shell_resources.iter().filter_map(|shell| shell.upgrade().ok()) { + for shell in self + .shell_resources + .iter() + .filter_map(|shell| shell.upgrade().ok()) + { if self.logout_owner.as_ref() == Some(&shell.id()) && shell.version() >= 3 { shell.logout_cancelled(serial); } @@ -343,7 +404,8 @@ impl Ferese { return; } - self.shell_resources.retain(|resource| resource.upgrade().is_ok()); + self.shell_resources + .retain(|resource| resource.upgrade().is_ok()); if self.shell_resources.is_empty() { self.last_shell_snapshot = None; return; @@ -377,14 +439,16 @@ impl Ferese { for previous in &snapshot.outputs { if outputs.next() != Some(previous.id) || self.output_names.get(&previous.id) != Some(&previous.name) - || self.output_workspaces.active_workspace(previous.id) != Some(previous.active_workspace) + || self.output_workspaces.active_workspace(previous.id) + != Some(previous.active_workspace) || self .output_ids .iter() .find(|(_, id)| **id == previous.id) .and_then(|(output, _)| self.output_bounds_for(output)) != previous.bounds - || (self.output_workspaces.focused_output() == Some(previous.id)) != previous.focused + || (self.output_workspaces.focused_output() == Some(previous.id)) + != previous.focused { return false; } @@ -394,7 +458,9 @@ impl Ferese { return false; } - let mut workspaces = self.output_workspaces.workspace_views_iter(&self.workspaces); + let mut workspaces = self + .output_workspaces + .workspace_views_iter(&self.workspaces); for previous in &snapshot.workspaces { let Some(view) = workspaces.next() else { @@ -416,7 +482,10 @@ impl Ferese { return false; } - let mut windows = self.windows.ordered_ids().filter_map(|id| self.shell_window_data(id)); + let mut windows = self + .windows + .ordered_ids() + .filter_map(|id| self.shell_window_data(id)); for previous in &snapshot.windows { let Some((id, workspace, app_id, title, state)) = windows.next() else { @@ -456,7 +525,8 @@ impl Ferese { // New subscribers always receive a complete snapshot. Do not update the // broadcast cache here: existing subscribers may still need this state. self.shell_snapshot_serial = self.shell_snapshot_serial.wrapping_add(1); - self.shell_snapshot().send(shell, self.shell_snapshot_serial); + self.shell_snapshot() + .send(shell, self.shell_snapshot_serial); } fn output_snapshots(&self) -> Vec { @@ -494,7 +564,10 @@ impl Ferese { .collect() } - fn shell_window_data(&self, id: WindowId) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> { + fn shell_window_data( + &self, + id: WindowId, + ) -> Option<(WindowId, u64, &str, &str, ferese_shell_v1::WindowState)> { let workspace = self.workspaces.workspace_for_window(id)?; let record = self.windows.record(id)?; self.windows.window(id)?.toplevel()?; @@ -535,15 +608,20 @@ impl Ferese { self.windows .ordered_ids() .filter_map(|id| self.shell_window_data(id)) - .map(|(id, workspace, app_id, title, state)| ManagedWindowSnapshot { - id, - workspace, - app_id: app_id.to_owned(), - title: title.to_owned(), - state, - source_geometry: self.shell_window_geometry(id), - geometry: previews.get(&id).copied().or_else(|| self.shell_window_geometry(id)), - }) + .map( + |(id, workspace, app_id, title, state)| ManagedWindowSnapshot { + id, + workspace, + app_id: app_id.to_owned(), + title: title.to_owned(), + state, + source_geometry: self.shell_window_geometry(id), + geometry: previews + .get(&id) + .copied() + .or_else(|| self.shell_window_geometry(id)), + }, + ) .collect() } @@ -612,7 +690,10 @@ impl ShellSnapshot { } for workspace in &self.workspaces { let (workspace_hi, workspace_lo) = split_id(workspace.id.0); - let (output_hi, output_lo) = workspace.output.map(|output| split_id(output.0)).unwrap_or_default(); + let (output_hi, output_lo) = workspace + .output + .map(|output| split_id(output.0)) + .unwrap_or_default(); shell.workspace( workspace_hi, @@ -642,7 +723,14 @@ impl ShellSnapshot { if shell.version() >= 8 && let Some(rect) = snapshot.geometry { - shell.window_geometry(window_hi, window_lo, rect.x, rect.y, rect.width, rect.height); + shell.window_geometry( + window_hi, + window_lo, + rect.x, + rect.y, + rect.width, + rect.height, + ); } } shell.snapshot_end(serial); @@ -833,20 +921,42 @@ mod tests { assert!(state.shell_snapshot_matches(&occupied)); let output_id = state.output_ids[&output]; let next = state.workspaces.create_workspace(); - state.output_workspaces.assign_workspace(output_id, next).unwrap(); + state + .output_workspaces + .assign_workspace(output_id, next) + .unwrap(); assert!(!state.shell_snapshot_matches(&occupied)); let assigned = state.shell_snapshot(); - state.output_workspaces.switch_workspace(output_id, next).unwrap(); + state + .output_workspaces + .switch_workspace(output_id, next) + .unwrap(); assert!(!state.shell_snapshot_matches(&assigned)); let selected = state.shell_snapshot(); assert!(state.shell_snapshot_matches(&selected)); - assert_eq!(state.output_workspaces.active_workspace(output_id), Some(next)); - assert_eq!(state.workspaces.workspace(next).unwrap().layout.window_ids().count(), 0); + assert_eq!( + state.output_workspaces.active_workspace(output_id), + Some(next) + ); + assert_eq!( + state + .workspaces + .workspace(next) + .unwrap() + .layout + .window_ids() + .count(), + 0 + ); let hidden_before = state.workspace_preview_rects()[&WindowId(99)]; let before = state.preview_cache_stats(); state.output_names.insert(output_id, "Renamed".into()); state.shell_snapshot(); - assert_eq!(state.preview_cache_stats(), before, "names do not affect previews"); + assert_eq!( + state.preview_cache_stats(), + before, + "names do not affect previews" + ); state.output_names.insert(output_id, "snapshot-test".into()); // The second workspace is empty; modifying its layout should not solve the hidden one. state @@ -868,13 +978,18 @@ mod tests { state.workspace_preview_rects(); let selected = state.shell_snapshot(); state - .reload_config_source("layout { inner-gap 30; outer-gap 40; smart-gaps #false; }".into()) + .reload_config_source( + "layout { inner-gap 30; outer-gap 40; smart-gaps #false; }".into(), + ) .unwrap(); assert!( !state.shell_snapshot_matches(&selected), "hidden previews must invalidate even with no visible window geometry to change" ); - assert_ne!(state.workspace_preview_rects()[&WindowId(99)], hidden_before); + assert_ne!( + state.workspace_preview_rects()[&WindowId(99)], + hidden_before + ); let refreshed = state.shell_snapshot(); assert!(state.shell_snapshot_matches(&refreshed)); state @@ -899,7 +1014,12 @@ mod tests { #[test] fn logout_serial_cannot_authorize_a_different_session_query() { assert!(super::logout_confirmation_matches(Some(42), Some(9), 42, 9)); - assert!(!super::logout_confirmation_matches(Some(42), Some(9), 42, 10)); + assert!(!super::logout_confirmation_matches( + Some(42), + Some(9), + 42, + 10 + )); assert!(!super::logout_confirmation_matches(None, Some(9), 42, 9)); } @@ -976,7 +1096,11 @@ mod tests { fn config_chunks_are_bounded_and_preserve_unicode() { let source = "abc🌲é".repeat(9000); let chunks = config_chunks(&source); - assert!(chunks.iter().all(|chunk| !chunk.is_empty() && chunk.len() <= 1024)); + assert!( + chunks + .iter() + .all(|chunk| !chunk.is_empty() && chunk.len() <= 1024) + ); assert_eq!(chunks.concat(), source); assert!(config_chunks("").is_empty()); } diff --git a/compositor/src/stacking.rs b/compositor/src/stacking.rs index fca7eec..4e7b9c9 100644 --- a/compositor/src/stacking.rs +++ b/compositor/src/stacking.rs @@ -2,7 +2,12 @@ use std::collections::HashMap; use ferese_layout::WindowId; -pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, above_fullscreen: bool) -> u8 { +pub(crate) fn layer_priority( + floating: bool, + zooming: bool, + fullscreen: bool, + above_fullscreen: bool, +) -> u8 { if above_fullscreen { 4 } else if fullscreen { @@ -201,10 +206,18 @@ mod tests { #[test] fn floating_windows_stay_above_tiled_and_maximized_windows() { - assert!(layer_priority(true, false, false, false) > layer_priority(false, true, false, false)); - assert!(layer_priority(false, true, false, false) > layer_priority(false, false, false, false)); - assert!(layer_priority(false, false, true, false) > layer_priority(true, false, false, false)); - assert!(layer_priority(true, false, false, true) > layer_priority(false, false, true, false)); + assert!( + layer_priority(true, false, false, false) > layer_priority(false, true, false, false) + ); + assert!( + layer_priority(false, true, false, false) > layer_priority(false, false, false, false) + ); + assert!( + layer_priority(false, false, true, false) > layer_priority(true, false, false, false) + ); + assert!( + layer_priority(true, false, false, true) > layer_priority(false, false, true, false) + ); } #[test] diff --git a/compositor/src/startup_tests.rs b/compositor/src/startup_tests.rs index bcd4105..050001b 100644 --- a/compositor/src/startup_tests.rs +++ b/compositor/src/startup_tests.rs @@ -10,7 +10,9 @@ use calloop::EventLoop; use calloop::signals::{Signal, Signals}; use smithay::input::SeatHandler; use smithay::input::pointer::{CursorImageStatus, MotionEvent}; -use smithay::reexports::wayland_server::protocol::{wl_compositor::WlCompositor, wl_surface::WlSurface}; +use smithay::reexports::wayland_server::protocol::{ + wl_compositor::WlCompositor, wl_surface::WlSurface, +}; use smithay::reexports::wayland_server::{Display, Resource}; use smithay::utils::SERIAL_COUNTER; @@ -54,7 +56,11 @@ pub(crate) fn private_runtime(test: &str) -> bool { std::thread::sleep(Duration::from_millis(10)); }; - assert!(status.success(), "{}", std::fs::read_to_string(log_path).unwrap()); + assert!( + status.success(), + "{}", + std::fs::read_to_string(log_path).unwrap() + ); false } @@ -68,7 +74,9 @@ pub(crate) fn state(events: &mut EventLoop<'static, Ferese>) -> Ferese { #[test] fn nested_instances_share_a_runtime_and_route_launched_clients_to_their_own_ipc() { use smithay::output::{Mode, Output, PhysicalProperties, Subpixel}; - if !private_runtime("startup_tests::nested_instances_share_a_runtime_and_route_launched_clients_to_their_own_ipc") { + if !private_runtime( + "startup_tests::nested_instances_share_a_runtime_and_route_launched_clients_to_their_own_ipc", + ) { return; } let mut host_events = EventLoop::try_new().unwrap(); @@ -168,7 +176,8 @@ with open(sys.argv[1], 'w') as report: assert!(Instant::now() < deadline, "nested IPC client timed out"); events.dispatch(Duration::from_millis(10), state).unwrap(); } - let result: serde_json::Value = serde_json::from_slice(&std::fs::read(report).unwrap()).unwrap(); + let result: serde_json::Value = + serde_json::from_slice(&std::fs::read(report).unwrap()).unwrap(); assert_eq!( result["socket"], state.session_environment.control_socket.to_str().unwrap() @@ -179,17 +188,37 @@ with open(sys.argv[1], 'w') as report: let first_directory = first_socket.parent().unwrap().to_owned(); drop(first); assert!(!first_directory.exists()); - assert!(host_socket.symlink_metadata().unwrap().file_type().is_socket()); - assert!(second_socket.symlink_metadata().unwrap().file_type().is_socket()); + assert!( + host_socket + .symlink_metadata() + .unwrap() + .file_type() + .is_socket() + ); + assert!( + second_socket + .symlink_metadata() + .unwrap() + .file_type() + .is_socket() + ); let second_directory = second_socket.parent().unwrap().to_owned(); drop(second); assert!(!second_directory.exists()); - assert!(host_socket.symlink_metadata().unwrap().file_type().is_socket()); + assert!( + host_socket + .symlink_metadata() + .unwrap() + .file_type() + .is_socket() + ); } #[test] fn launched_clients_unblock_termination_signals_and_keep_private_sockets() { - if !private_runtime("startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets") { + if !private_runtime( + "startup_tests::launched_clients_unblock_termination_signals_and_keep_private_sockets", + ) { return; } @@ -198,13 +227,27 @@ fn launched_clients_unblock_termination_signals_and_keep_private_sockets() { let signals = Signals::new(&[Signal::SIGINT, Signal::SIGTERM, Signal::SIGUSR1]).unwrap(); let mut private = crate::private_client::ClientCapabilities::EFFECTS; private.insert(crate::private_client::ClientCapabilities::SHELL_CONTROL); - for capabilities in [crate::private_client::ClientCapabilities::default(), private] { - let mut child = crate::process::spawn_client(&mut state, ["/bin/sleep", "30"], capabilities).unwrap(); + for capabilities in [ + crate::private_client::ClientCapabilities::default(), + private, + ] { + let mut child = + crate::process::spawn_client(&mut state, ["/bin/sleep", "30"], capabilities).unwrap(); let status = std::fs::read_to_string(format!("/proc/{}/status", child.id())).unwrap(); - let blocked = status.lines().find(|line| line.starts_with("SigBlk:")).unwrap(); + let blocked = status + .lines() + .find(|line| line.starts_with("SigBlk:")) + .unwrap(); let blocked = u64::from_str_radix(blocked.split_whitespace().nth(1).unwrap(), 16).unwrap(); - assert_eq!(blocked & ((1 << (libc::SIGINT - 1)) | (1 << (libc::SIGTERM - 1))), 0); - assert_ne!(blocked & (1 << (libc::SIGUSR1 - 1)), 0, "unrelated mask was lost"); + assert_eq!( + blocked & ((1 << (libc::SIGINT - 1)) | (1 << (libc::SIGTERM - 1))), + 0 + ); + assert_ne!( + blocked & (1 << (libc::SIGUSR1 - 1)), + 0, + "unrelated mask was lost" + ); if !capabilities.is_empty() { let environment = std::fs::read(format!("/proc/{}/environ", child.id())).unwrap(); for name in ["WAYLAND_SOCKET=", "FERESE_SHELL_CONTROL_SOCKET="] { @@ -239,7 +282,9 @@ fn launched_clients_unblock_termination_signals_and_keep_private_sockets() { #[test] fn explicit_client_exit_is_reaped_without_stopping_the_compositor() { - if !private_runtime("startup_tests::explicit_client_exit_is_reaped_without_stopping_the_compositor") { + if !private_runtime( + "startup_tests::explicit_client_exit_is_reaped_without_stopping_the_compositor", + ) { return; } @@ -256,13 +301,21 @@ fn explicit_client_exit_is_reaped_without_stopping_the_compositor() { let deadline = Instant::now() + Duration::from_secs(2); while watched.borrow().is_some() { assert!(Instant::now() < deadline, "client was not reaped"); - events.dispatch(Duration::from_millis(10), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(10), &mut state) + .unwrap(); after_dispatch(&mut state); } let mut status = 0; - assert_eq!(unsafe { libc::waitpid(pid as i32, &mut status, libc::WNOHANG) }, -1); - assert_eq!(std::io::Error::last_os_error().raw_os_error(), Some(libc::ECHILD)); + assert_eq!( + unsafe { libc::waitpid(pid as i32, &mut status, libc::WNOHANG) }, + -1 + ); + assert_eq!( + std::io::Error::last_os_error().raw_os_error(), + Some(libc::ECHILD) + ); // Exit watching removes itself; subsequent event-loop dispatch remains usable. events.dispatch(Duration::ZERO, &mut state).unwrap(); } @@ -306,7 +359,9 @@ fn deferred_cursor_redraw_runs_after_input_lock_is_released() { client_wire.write_all(&word.to_ne_bytes()).unwrap(); } - events.dispatch(Duration::from_millis(10), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(10), &mut state) + .unwrap(); let surface = client .object_from_protocol_id::(&state.display_handle, 2) .unwrap(); @@ -365,16 +420,25 @@ fn wayland_roundtrip_completes_without_a_rendered_frame() { client.write_all(&word.to_ne_bytes()).unwrap(); } - events.dispatch(Duration::from_millis(10), &mut state).unwrap(); - client.set_read_timeout(Some(Duration::from_millis(20))).unwrap(); + events + .dispatch(Duration::from_millis(10), &mut state) + .unwrap(); + client + .set_read_timeout(Some(Duration::from_millis(20))) + .unwrap(); let mut reply = [0u8; 12]; assert!( client.read_exact(&mut reply).is_err(), "dispatch flushed replies before after_dispatch" ); after_dispatch(&mut state); - client.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + client + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); client.read_exact(&mut reply).unwrap(); assert_eq!(u32::from_ne_bytes(reply[0..4].try_into().unwrap()), 2); - assert_eq!(u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, 0); + assert_eq!( + u32::from_ne_bytes(reply[4..8].try_into().unwrap()) & 0xffff, + 0 + ); } diff --git a/compositor/src/state.rs b/compositor/src/state.rs index ea44b50..d2ea774 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -35,18 +35,21 @@ use std::time::{Duration, Instant}; use calloop::LoopHandle; use ferese_animation::{AnimatedValue, ClientSize, PresentationMode, SpringConfig, WindowGeometry}; use ferese_core::{ - LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId, WorkspaceLayout, - WorkspaceSet, + LayoutMode, OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceId, + WorkspaceLayout, WorkspaceSet, }; use ferese_layout::{ - Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, ViewportFocusStrategy, WindowId, + Axis, ColumnWidth, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, + ViewportFocusStrategy, WindowId, }; use ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1; use smithay::backend::drm::{DrmEventTime, DrmNode}; use smithay::backend::input::Keycode; use smithay::desktop::space::SpaceElement; use smithay::desktop::utils::send_frames_surface_tree; -use smithay::desktop::{LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output}; +use smithay::desktop::{ + LayerSurface, PopupKind, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output, +}; use smithay::input::keyboard::XkbConfig; use smithay::input::pointer::{CursorIcon, CursorImageStatus}; use smithay::input::{Seat, SeatState}; @@ -55,7 +58,9 @@ use smithay::reexports::calloop::generic::Generic; use smithay::reexports::calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction}; use smithay::reexports::drm::control::crtc; use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel; -use smithay::reexports::wayland_server::backend::{ClientData, ClientId, DisconnectReason, ObjectId}; +use smithay::reexports::wayland_server::backend::{ + ClientData, ClientId, DisconnectReason, ObjectId, +}; use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; use smithay::reexports::wayland_server::{Display, DisplayHandle, Weak}; use smithay::utils::{Logical, Point, Rectangle, Size}; @@ -67,7 +72,9 @@ use smithay::wayland::fractional_scale::FractionalScaleManagerState; use smithay::wayland::idle_inhibit::IdleInhibitManagerState; use smithay::wayland::idle_notify::IdleNotifierState; use smithay::wayland::input_method::InputMethodManagerState; -use smithay::wayland::keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, +}; use smithay::wayland::output::OutputManagerState; use smithay::wayland::pointer_constraints::PointerConstraintsState; use smithay::wayland::presentation::PresentationState; @@ -90,7 +97,9 @@ use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager; use window_registry::WindowRegistry; use crate::backends::direct::DirectBackendState; -use crate::config::{Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; +use crate::config::{ + Binding, BindingSet, DaemonConfig, InputSettings, OutputProfile, ThemeSettings, +}; use crate::cursor::NamedCursor; use crate::focus_effect::BoundedFade; use crate::gestures::{Swipe, SwipeDirection}; @@ -124,7 +133,9 @@ fn workspace_swipe_is_current( ) -> bool { outputs.focused_output() == Some(output) && outputs.active_workspace(output) == Some(from) - && outputs.output_for_workspace(to).is_none_or(|owner| owner == output) + && outputs + .output_for_workspace(to) + .is_none_or(|owner| owner == output) } #[derive(Clone, Copy, Debug, Default, PartialEq)] @@ -144,7 +155,10 @@ impl SlideOffset { } fn opposite(self) -> Self { - Self { x: -self.x, y: -self.y } + Self { + x: -self.x, + y: -self.y, + } } fn between(self, target: Self, progress: f64) -> Self { @@ -227,7 +241,12 @@ struct WorkspaceSlide { } impl WorkspaceSlide { - fn new(previous: Option, from: WorkspaceId, to: WorkspaceId, direction: SwipeDirection) -> Self { + fn new( + previous: Option, + from: WorkspaceId, + to: WorkspaceId, + direction: SwipeDirection, + ) -> Self { let movement = SlideOffset::for_swipe(direction); let mut items = previous .map(|slide| { @@ -643,12 +662,16 @@ impl Ferese { let fractional_scale_state = FractionalScaleManagerState::new::(&display_handle); let idle_inhibit_state = IdleInhibitManagerState::new::(&display_handle); let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle()); - let session_lock_state = - smithay::wayland::session_lock::SessionLockManagerState::new::(&display_handle, |_| true); - let input_method_enabled = std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1"); + 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::(&display_handle, move |_| input_method_enabled); - let keyboard_shortcuts_inhibit_state = KeyboardShortcutsInhibitState::new::(&display_handle); + let keyboard_shortcuts_inhibit_state = + KeyboardShortcutsInhibitState::new::(&display_handle); let shortcut_inhibit_enabled = std::env::var_os("FERESE_ENABLE_SHORTCUT_INHIBIT").is_some_and(|value| value == "1"); if !shortcut_inhibit_enabled { @@ -659,13 +682,15 @@ impl Ferese { let text_input_manager_state = TextInputManagerState::new::(&display_handle); let output_manager_state = OutputManagerState::new_with_xdg_output::(&display_handle); let pointer_constraints_state = PointerConstraintsState::new::(&display_handle); - let presentation_state = PresentationState::new::(&display_handle, libc::CLOCK_MONOTONIC as u32); + let presentation_state = + PresentationState::new::(&display_handle, libc::CLOCK_MONOTONIC as u32); let data_device_state = DataDeviceState::new::(&display_handle); let primary_selection_state = PrimarySelectionState::new::(&display_handle); - let security_context_state = smithay::wayland::security_context::SecurityContextState::new::( - &display_handle, - crate::handlers::security_context::unrestricted_client, - ); + let security_context_state = + smithay::wayland::security_context::SecurityContextState::new::( + &display_handle, + crate::handlers::security_context::unrestricted_client, + ); // Host clipboard tools retain direct access. Sandboxed clients use // ordinary selection protocols, which follow keyboard focus. let wlr_data_control_state = wlr_data_control::DataControlState::new::( @@ -687,8 +712,8 @@ impl Ferese { let xdg_toplevel_icon_manager = XdgToplevelIconManager::new::(&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(",")); + 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, @@ -900,9 +925,10 @@ impl Ferese { sections: &serde_json::Value, ) -> Result<(), String> { let changed = |keys: &[&str]| { - self.config_sections - .as_ref() - .is_none_or(|previous| keys.iter().any(|key| previous.get(*key) != sections.get(*key))) + self.config_sections.as_ref().is_none_or(|previous| { + keys.iter() + .any(|key| previous.get(*key) != sections.get(*key)) + }) }; let activities_changed = changed(&["activities"]); let layout_changed = changed(&["layout", "scrolling", "workspaces"]); @@ -911,8 +937,9 @@ impl Ferese { let daemons_changed = changed(&["autostart"]); let lock_changed = changed(&["lock_screen"]); let panel_layout_changed = self.panel_style != config.panel_style; - let non_output_scene_changed = - layout_changed || panel_layout_changed || changed(&["animations", "appearance", "window_rules"]); + let non_output_scene_changed = layout_changed + || panel_layout_changed + || changed(&["animations", "appearance", "window_rules"]); let scene_changed = non_output_scene_changed || changed(&["output_profiles"]); if self.output_profiles != config.output_profiles { @@ -978,7 +1005,10 @@ impl Ferese { self.restore_input_capture_focus(); } } - keyboard.change_repeat_info(config.input_settings.repeat_rate, config.input_settings.repeat_delay_ms); + keyboard.change_repeat_info( + config.input_settings.repeat_rate, + config.input_settings.repeat_delay_ms, + ); } if lock_changed { @@ -1022,10 +1052,12 @@ impl Ferese { } self.workspace_auto_back_and_forth = config.workspace_auto_back_and_forth; if bindings_changed || input_changed { - self.portal_shortcuts.reconcile(&self.bindings, &self.input_settings); + self.portal_shortcuts + .reconcile(&self.bindings, &self.input_settings); } let old_rules = std::mem::replace(&mut self.window_rules, config.window_rules); - let idle_policy_changed = self.idle_inhibit != config.idle_inhibit || old_rules != self.window_rules; + let idle_policy_changed = + self.idle_inhibit != config.idle_inhibit || old_rules != self.window_rules; self.idle_inhibit = config.idle_inhibit; self.theme_settings = config.theme_settings; self.panel_corner_radius = config.panel_corner_radius; @@ -1168,11 +1200,19 @@ fn constrained_floating_rect(rect: Rect, constraints: SizeConstraints) -> Rect { Rect::new( rect.x, rect.y, - rect.width - .clamp(min_width, constraints.max_width.unwrap_or(f64::INFINITY).max(min_width)), + rect.width.clamp( + min_width, + constraints + .max_width + .unwrap_or(f64::INFINITY) + .max(min_width), + ), rect.height.clamp( min_height, - constraints.max_height.unwrap_or(f64::INFINITY).max(min_height), + constraints + .max_height + .unwrap_or(f64::INFINITY) + .max(min_height), ), ) } @@ -1188,7 +1228,12 @@ fn natural_floating_rect(bounds: Rect, size: ClientSize) -> Rect { ) } -fn restored_scrolling_world_x(had_geometry: bool, visual_x: f64, viewport_x: f64, target_world_x: f64) -> f64 { +fn restored_scrolling_world_x( + had_geometry: bool, + visual_x: f64, + viewport_x: f64, + target_world_x: f64, +) -> f64 { if had_geometry { visual_x + viewport_x } else { @@ -1224,9 +1269,10 @@ fn client_size(window: &Window) -> Option { pub(crate) fn window_has_buffer(window: &Window) -> bool { window.toplevel().is_some_and(|toplevel| { - smithay::backend::renderer::utils::with_renderer_surface_state(toplevel.wl_surface(), |state| { - state.buffer().is_some() - }) + smithay::backend::renderer::utils::with_renderer_surface_state( + toplevel.wl_surface(), + |state| state.buffer().is_some(), + ) .unwrap_or(false) }) } @@ -1289,7 +1335,8 @@ impl ClientData for ClientState { mod tests { #[test] fn overview_slide_prediction_uses_the_same_clock_as_live_motion() { - let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); + let mut slide = + WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); slide.speed = crate::overview::OVERVIEW_MOTION_SPEED; for delta in [ Duration::from_millis(8), @@ -1309,17 +1356,24 @@ mod tests { #[test] fn slide_item_sampling_matches_advance_without_mutating_the_slide() { - let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Up); + let mut slide = + WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Up); slide.advance(Duration::from_millis(30)); for held in [None, Some(0.35)] { slide.held_progress = held; - for delta in [Duration::ZERO, Duration::from_millis(8), Duration::from_millis(400)] { + for delta in [ + Duration::ZERO, + Duration::from_millis(8), + Duration::from_millis(400), + ] { let before = (slide.items[0].start, slide.items[0].velocity); let mut reference = slide.clone(); reference.advance(delta); for (item, advanced) in slide.items.iter().zip(&reference.items) { let (position, velocity) = slide.sample(item, delta); - let expected = held.map_or(advanced.start, |p| advanced.start.between(advanced.target, p)); + let expected = held.map_or(advanced.start, |p| { + advanced.start.between(advanced.target, p) + }); assert_eq!(position, expected); assert_eq!(velocity, advanced.velocity); } @@ -1341,7 +1395,11 @@ mod tests { .collect(); let reversed = WorkspaceSlide::new(Some(slide), to, from, SwipeDirection::Down); for (id, position, velocity) in before { - let item = reversed.items.iter().find(|item| item.workspace == id).unwrap(); + let item = reversed + .items + .iter() + .find(|item| item.workspace == id) + .unwrap(); assert_eq!(item.start, position); assert_eq!(item.velocity, velocity); assert!(velocity.y < 0.0); @@ -1377,7 +1435,11 @@ mod tests { #[test] fn sampling_slide_progress_matches_advancing_without_mutating_the_slide() { let slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left); - for delta in [Duration::ZERO, Duration::from_millis(16), Duration::from_millis(100)] { + for delta in [ + Duration::ZERO, + Duration::from_millis(16), + Duration::from_millis(100), + ] { let mut advanced = slide.clone(); advanced.advance(delta); for (before, after) in slide.items.iter().zip(&advanced.items) { @@ -1417,7 +1479,11 @@ mod tests { .connect(left, OutputGeometry::new(0, 0, 1000, 1000), from) .unwrap(); outputs - .connect(right, OutputGeometry::new(1000, 0, 1000, 1000), WorkspaceId(2)) + .connect( + right, + OutputGeometry::new(1000, 0, 1000, 1000), + WorkspaceId(2), + ) .unwrap(); assert!(workspace_swipe_is_current(&outputs, left, from, to)); outputs.focus_output(right).unwrap(); @@ -1428,7 +1494,12 @@ mod tests { assert_eq!(outputs.active_workspace(left), Some(from)); assert_eq!(outputs.output_for_workspace(to), Some(right)); outputs.switch_workspace(left, WorkspaceId(4)).unwrap(); - assert!(!workspace_swipe_is_current(&outputs, left, from, WorkspaceId(5))); + assert!(!workspace_swipe_is_current( + &outputs, + left, + from, + WorkspaceId(5) + )); } #[test] @@ -1436,11 +1507,17 @@ mod tests { for assignment in [None, Some(OutputId(1)), Some(OutputId(2))] { let mut workspaces = WorkspaceSet::default(); let first = workspaces.active_id(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let second = workspaces.create_workspace(); workspaces.activate(second).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(3), Axis::Horizontal, 0.5) + .unwrap(); workspaces.activate(first).unwrap(); let mut outputs = OutputWorkspaceMap::default(); @@ -1462,7 +1539,11 @@ mod tests { }; outputs.focus_output(OutputId(1)).unwrap(); - assert!(activate_window_workspace(&mut workspaces, &mut outputs, WindowId(2))); + assert!(activate_window_workspace( + &mut workspaces, + &mut outputs, + WindowId(2) + )); assert_eq!(outputs.active_workspace(owner), Some(second)); assert_eq!(outputs.focused_output(), Some(owner)); assert_eq!(outputs.output_for_workspace(second), Some(owner)); @@ -1477,8 +1558,12 @@ mod tests { #[test] fn selecting_a_tiled_window_reveals_it_past_another_fullscreen_window() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); workspaces.set_fullscreen(WindowId(1), true).unwrap(); let mut outputs = OutputWorkspaceMap::default(); outputs @@ -1489,7 +1574,11 @@ mod tests { ) .unwrap(); - assert!(activate_window_workspace(&mut workspaces, &mut outputs, WindowId(2))); + assert!(activate_window_workspace( + &mut workspaces, + &mut outputs, + WindowId(2) + )); assert_eq!(workspaces.active().fullscreen, None); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); } @@ -1626,15 +1715,26 @@ mod tests { assert!(chained.contains(first)); assert!(chained.contains(second)); assert!(chained.contains(third)); - let first_start = chained.items.iter().find(|item| item.workspace == first).unwrap().start; + let first_start = chained + .items + .iter() + .find(|item| item.workspace == first) + .unwrap() + .start; let second_start = chained .items .iter() .find(|item| item.workspace == second) .unwrap() .start; - assert_eq!((first_start.x * 1600.0, first_start.y * 900.0), first_position); - assert_eq!((second_start.x * 1600.0, second_start.y * 900.0), second_position); + assert_eq!( + (first_start.x * 1600.0, first_start.y * 900.0), + first_position + ); + assert_eq!( + (second_start.x * 1600.0, second_start.y * 900.0), + second_position + ); let reversed = WorkspaceSlide::new(Some(chained), third, first, SwipeDirection::Down); assert_eq!(reversed.items.len(), 3); @@ -1667,7 +1767,10 @@ mod tests { constrained_floating_rect(Rect::new(40., 60., 1000., 800.), constraints), Rect::new(40., 60., 900., 700.) ); - assert_eq!(constrained_floating_rect(rect, SizeConstraints::default()), rect); + assert_eq!( + constrained_floating_rect(rect, SizeConstraints::default()), + rect + ); } #[test] @@ -1675,7 +1778,12 @@ mod tests { let serial = Some(12.into()); assert!(floating_commit_is_current(true, serial, serial, false)); assert!(!floating_commit_is_current(false, serial, serial, false)); - assert!(!floating_commit_is_current(true, Some(11.into()), serial, false)); + assert!(!floating_commit_is_current( + true, + Some(11.into()), + serial, + false + )); assert!(!floating_commit_is_current(true, serial, serial, true)); assert!(!floating_commit_is_current(true, None, None, false)); } @@ -1684,7 +1792,8 @@ mod tests { fn display_reposition_moves_floats_with_their_output_and_clamps_after_shrinking() { let old = ferese_layout::Rect::new(0., 0., 1920., 1080.); let new = ferese_layout::Rect::new(2000., 0., 1000., 700.); - let rect = super::moved_floating_rect(ferese_layout::Rect::new(1500., 800., 400., 300.), old, new); + let rect = + super::moved_floating_rect(ferese_layout::Rect::new(1500., 800., 400., 300.), old, new); assert_eq!(rect, ferese_layout::Rect::new(2600., 400., 400., 300.)); } use super::*; @@ -1752,7 +1861,10 @@ mod tests { fn fullscreen_exit_restores_world_x_from_presented_position() { assert_eq!(restored_scrolling_world_x(true, 0.0, 0.0, 505.0), 0.0); assert_eq!(restored_scrolling_world_x(true, 25.0, 480.0, 505.0), 505.0); - assert_eq!(restored_scrolling_world_x(false, 10.0, 495.0, 1_000.0), 1_000.0); + assert_eq!( + restored_scrolling_world_x(false, 10.0, 495.0, 1_000.0), + 1_000.0 + ); } #[derive(Clone, Copy, PartialEq, Debug)] @@ -1785,7 +1897,11 @@ mod tests { #[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)); + assert!(!stacking_order_settled( + current.iter(), + ¤t, + |window| window.z_index + )); } #[test] diff --git a/compositor/src/state/animation.rs b/compositor/src/state/animation.rs index 90ec06a..e8793fe 100644 --- a/compositor/src/state/animation.rs +++ b/compositor/src/state/animation.rs @@ -19,7 +19,10 @@ impl Ferese { // desktop must not turn suspend/DPMS time into a spring step. if self.space.outputs().next().is_none() || self.session_lock.sleeping - || self.direct_backend.as_ref().is_some_and(|backend| !backend.active) + || self + .direct_backend + .as_ref() + .is_some_and(|backend| !backend.active) { #[cfg(feature = "resize-metrics")] self.resize_metrics.pause_clock(self.start_time.elapsed()); @@ -29,7 +32,13 @@ impl Ferese { self.advance_animations_by(delta) } - pub fn record_drm_presentation(&mut self, node: DrmNode, crtc: crtc::Handle, time: DrmEventTime, sequence: u32) { + pub fn record_drm_presentation( + &mut self, + node: DrmNode, + crtc: crtc::Handle, + time: DrmEventTime, + sequence: u32, + ) { if let Some(backend) = self.direct_backend.as_mut() { backend.record_presentation(node, crtc, time, sequence); } @@ -37,7 +46,12 @@ impl Ferese { #[cfg(feature = "resize-metrics")] pub(super) fn measure_resize_pauses(&mut self, now: Duration) { - if self.session_lock.sleeping || self.direct_backend.as_ref().is_some_and(|backend| !backend.active) { + if self.session_lock.sleeping + || self + .direct_backend + .as_ref() + .is_some_and(|backend| !backend.active) + { self.resize_metrics.pause_clock(now); return; } @@ -46,7 +60,8 @@ impl Ferese { let Some(workspace) = self.workspaces.workspace_for_window(*id) else { continue; }; - let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0; + let moving = + |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0; let viewport = self .presentation_dependencies .viewport_blocked(workspace, &self.windows) @@ -57,28 +72,46 @@ impl Ferese { && self .output_workspaces .output_for_workspace(workspace) - .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace)) - && self.focus_swipe().is_none_or(|swipe| swipe.workspace != workspace); - let others = self.workspaces.workspace(workspace).is_some_and(|workspace| { - workspace - .layout - .window_ids() - .chain(workspace.floating.iter().copied()) - .any(|other| { - other != *id - && self.presentation_dependencies.reflow_blocked(other, &self.windows) - && self - .windows - .window(other) - .is_some_and(|window| self.space.element_location(window).is_some()) - && self.windows.record(other).is_some_and(|record| { - record.geometry.is_some_and(|mut geometry| { - geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) - }) || record.world_x.as_ref().is_some_and(|(_, value)| moving(value)) - || record.coupled_width.as_ref().is_some_and(|(_, value)| moving(value)) - }) + .is_some_and(|output| { + self.output_workspaces.active_workspace(output) == Some(workspace) }) - }); + && self + .focus_swipe() + .is_none_or(|swipe| swipe.workspace != workspace); + let others = self + .workspaces + .workspace(workspace) + .is_some_and(|workspace| { + workspace + .layout + .window_ids() + .chain(workspace.floating.iter().copied()) + .any(|other| { + other != *id + && self + .presentation_dependencies + .reflow_blocked(other, &self.windows) + && self.windows.window(other).is_some_and(|window| { + self.space.element_location(window).is_some() + }) + && self.windows.record(other).is_some_and(|record| { + record.geometry.is_some_and(|mut geometry| { + geometry.advance( + Duration::ZERO, + self.spring_config, + self.animations_enabled, + ) + }) || record + .world_x + .as_ref() + .is_some_and(|(_, value)| moving(value)) + || record + .coupled_width + .as_ref() + .is_some_and(|(_, value)| moving(value)) + }) + }) + }); self.resize_metrics.observe( *id, now, @@ -110,7 +143,9 @@ impl Ferese { if self.viewports.iter().any(|(workspace, viewport)| { self.output_workspaces .output_for_workspace(*workspace) - .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace)) + .is_some_and(|output| { + self.output_workspaces.active_workspace(output) == Some(*workspace) + }) && viewport.needs_tick() }) { return true; @@ -124,7 +159,8 @@ impl Ferese { self.windows.records().any(|(&id, record)| { let focus = if selected == Some(id) { 1.0 } else { 0.0 }; - let activation = crate::focus_effect::target(self.focus_effect, self.focused_window, id); + let activation = + crate::focus_effect::target(self.focus_effect, self.focused_window, id); record_needs_tick(record, focus, activation, || { self.windows @@ -155,15 +191,20 @@ impl Ferese { .filter(|swipe| !self.focus_swipe_is_current(swipe)) .map(|swipe| swipe.workspace) { - let swipe = self.viewports.get_mut(&workspace).unwrap().end_gesture(None).unwrap(); + let swipe = self + .viewports + .get_mut(&workspace) + .unwrap() + .end_gesture(None) + .unwrap(); self.presentation_dependencies .restore_viewport(swipe.workspace, swipe.dependencies); self.release_presentation_dependencies(); } - let mut active_animation = self - .tab_indicator - .advance(delta, self.spring_config, self.animations_enabled); + let mut active_animation = + self.tab_indicator + .advance(delta, self.spring_config, self.animations_enabled); let mut completed_slides = Vec::new(); for (output, slide) in &mut self.workspace_slides { if slide.held_progress.is_some() { @@ -212,7 +253,9 @@ impl Ferese { for (&id, record) in self.windows.records_mut() { let selected = selected_window == Some(id); let target = if selected { 1.0 } else { 0.0 }; - let focus = record.focus.get_or_insert_with(|| AnimatedValue::new(target)); + let focus = record + .focus + .get_or_insert_with(|| AnimatedValue::new(target)); let previous = focus.current; let changed = focus.target != target; focus.set_target(target); @@ -225,7 +268,9 @@ impl Ferese { focus.snap(); } appearance_changed |= previous != focus.current; - let shadow = record.shadow.get_or_insert_with(|| AnimatedValue::new(target)); + let shadow = record + .shadow + .get_or_insert_with(|| AnimatedValue::new(target)); let previous = shadow.current; let changed = shadow.target != target; shadow.set_target(target); @@ -271,8 +316,9 @@ impl Ferese { } let mut finished = Vec::new(); self.render.closing.retain_mut(|window| { - let active = - self.animations_enabled && !self.session_lock.active() && window.advance(delta, self.spring_config); + let active = self.animations_enabled + && !self.session_lock.active() + && window.advance(delta, self.spring_config); if !active { finished.push(window.presentation.id); } @@ -299,8 +345,12 @@ impl Ferese { #[cfg(feature = "resize-metrics")] { for (&id, record) in self.windows.records() { - if record.resize.is_some_and(|transaction| transaction.expired(now)) { - self.resize_metrics.end(id, now, crate::resize_metrics::End::Deadline); + if record + .resize + .is_some_and(|transaction| transaction.expired(now)) + { + self.resize_metrics + .end(id, now, crate::resize_metrics::End::Deadline); } } } @@ -324,7 +374,9 @@ impl Ferese { return false; } - let waiting_for_client = self.presentation_dependencies.reflow_blocked(*id, &self.windows); + let waiting_for_client = self + .presentation_dependencies + .reflow_blocked(*id, &self.windows); // A shrinking client's destination buffer arrives before the // animated bounds reach it. Keep the old native pixels covering // that strip rather than fading them into the neutral resize fill. @@ -355,7 +407,11 @@ impl Ferese { if !active { #[cfg(feature = "resize-metrics")] resize_metrics.handoff_end(*id, now, snapshot.elapsed); - tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot"); + tracing::debug!( + ?id, + bytes = snapshot.bytes(), + "released resize handoff snapshot" + ); } active }); @@ -366,7 +422,9 @@ impl Ferese { if self .output_workspaces .output_for_workspace(*workspace) - .is_none_or(|output| self.output_workspaces.active_workspace(output) != Some(*workspace)) + .is_none_or(|output| { + self.output_workspaces.active_workspace(output) != Some(*workspace) + }) { continue; } @@ -407,7 +465,8 @@ impl Ferese { geometry.visual.velocity.width = 0.0; } if !held { - active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled); + active_animation |= + geometry.advance(delta, self.spring_config, self.animations_enabled); } if zooming != geometry.is_zooming() { @@ -435,7 +494,12 @@ impl Ferese { presented_world.velocity = 0.0; } let presented_viewport = viewport.presented_motion(); - sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false); + sync_scrolling_coordinates( + geometry, + &mut presented_world, + &presented_viewport, + false, + ); } } @@ -520,9 +584,9 @@ impl Ferese { self.retarget_overview(); } let overview_was_presenting = self.overview.is_presenting(); - active_animation |= self - .overview - .advance(delta, self.spring_config, self.animations_enabled); + active_animation |= + self.overview + .advance(delta, self.spring_config, self.animations_enabled); if overview_was_presenting != self.overview.is_presenting() { self.reconcile_layer_keyboard_focus(); } @@ -591,7 +655,10 @@ impl Ferese { let position = slide.position(item, delta); offsets.insert( item.workspace, - (position.x * f64::from(size.w), position.y * f64::from(size.h)), + ( + position.x * f64::from(size.w), + position.y * f64::from(size.h), + ), ); } } @@ -634,8 +701,14 @@ fn record_needs_tick( ) -> bool { if record.resize.is_some() || record.opening.is_some() - || record.focus.as_ref().is_none_or(|motion| motion.needs_update(focus)) - || record.shadow.as_ref().is_none_or(|motion| motion.needs_update(focus)) + || record + .focus + .as_ref() + .is_none_or(|motion| motion.needs_update(focus)) + || record + .shadow + .as_ref() + .is_none_or(|motion| motion.needs_update(focus)) { return true; } @@ -645,7 +718,10 @@ fn record_needs_tick( .focus_transition .as_ref() .is_none_or(|motion| motion.needs_update(activation)) - || record.world_x.as_ref().is_some_and(|(_, world)| moving(world)) + || record + .world_x + .as_ref() + .is_some_and(|(_, world)| moving(world)) || record.coupled_width.is_some() || record.geometry.is_some_and(|geometry| { geometry.is_zooming() @@ -654,7 +730,8 @@ fn record_needs_tick( || geometry.visual.current != geometry.visual.target || geometry.visual.velocity != ferese_animation::RectVelocity::default() || (!record.natural_floating_pending - && geometry.client.last_configured_size != Some(ClientSize::from_rect(geometry.logical))) + && geometry.client.last_configured_size + != Some(ClientSize::from_rect(geometry.logical))) }); // A stale hidden target must not keep waking the whole desktop. @@ -756,15 +833,33 @@ mod tests { viewport.advance(Duration::from_millis(40), config, true, false); let mut world = AnimatedValue::new(500.0); world.set_target(900.0); - world.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing); + world.advance_with_policy( + Duration::from_millis(40), + config, + CrossingPolicy::NoCrossing, + ); let mut width = AnimatedValue::new(400.0); width.set_target(700.0); - width.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing); + width.advance_with_policy( + Duration::from_millis(40), + config, + CrossingPolicy::NoCrossing, + ); let mut geometry = WindowGeometry::new( - Rect::new(world.current - viewport.motion().current, 0.0, width.current, 300.0), + Rect::new( + world.current - viewport.motion().current, + 0.0, + width.current, + 300.0, + ), None, ); - let target = Rect::new(world.target - viewport.motion().target, 0.0, width.target, 300.0); + let target = Rect::new( + world.target - viewport.motion().target, + 0.0, + width.target, + 300.0, + ); geometry.set_logical_target(target, Duration::from_millis(40)); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false); geometry.visual.velocity.width = width.velocity; @@ -773,12 +868,20 @@ mod tests { world.retarget_preserving_motion(1000.0); width.retarget_preserving_motion(600.0); geometry.set_logical_target( - Rect::new(world.target - viewport.motion().target, 0.0, width.target, 300.0), + Rect::new( + world.target - viewport.motion().target, + 0.0, + width.target, + 300.0, + ), Duration::from_millis(40), ); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false); assert_eq!(geometry.visual.current, before); - assert_eq!(geometry.visual.velocity.x, world.velocity - viewport.motion().velocity); + assert_eq!( + geometry.visual.velocity.x, + world.velocity - viewport.motion().velocity + ); assert_eq!(geometry.logical.x, world.target - viewport.motion().target); for mode in [ @@ -787,10 +890,17 @@ mod tests { PresentationMode::Normal, ] { let before = geometry.visual.current; - geometry.set_presentation_mode(Rect::new(0.0, 0.0, 1920.0, 1080.0), mode, Duration::from_millis(40)); + geometry.set_presentation_mode( + Rect::new(0.0, 0.0, 1920.0, 1080.0), + mode, + Duration::from_millis(40), + ); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), true); assert_eq!(geometry.visual.current, before); - assert_eq!(world.current, geometry.visual.current.x + viewport.motion().current); + assert_eq!( + world.current, + geometry.visual.current.x + viewport.motion().current + ); geometry.advance(Duration::ZERO, config, true); assert_eq!(geometry.visual.current, before); geometry.advance(Duration::from_millis(16), config, true); @@ -870,7 +980,10 @@ mod tests { state.register_output(&output, "pause-test".into()); let workspace = state.workspaces.active_id(); let id = WindowId(1); - state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); + state + .workspaces + .insert_window(id, Axis::Horizontal, 0.5) + .unwrap(); state.windows.records.insert(id, settled()); let mut viewport = AnimatedValue::new(0.0); viewport.set_target(500.0); @@ -960,7 +1073,11 @@ mod tests { // A settled transaction still has to flush its resume guard; otherwise // the next unrelated animation would inherit an old paused workspace. let target = state.viewports[&workspace].motion().target; - state.viewports.get_mut(&workspace).unwrap().retarget(target, false); + state + .viewports + .get_mut(&workspace) + .unwrap() + .retarget(target, false); state.windows.record_mut(id).unwrap().focus = Some(AnimatedValue::new(0.0)); state.windows.set_transaction( id, diff --git a/compositor/src/state/capture_privacy.rs b/compositor/src/state/capture_privacy.rs index a3d3fc5..8916d12 100644 --- a/compositor/src/state/capture_privacy.rs +++ b/compositor/src/state/capture_privacy.rs @@ -37,7 +37,10 @@ impl Ferese { data.title.as_deref(), data.parent.is_some(), ); - (rule.block_out_from_screencasts == Some(true), data.parent.clone()) + ( + rule.block_out_from_screencasts == Some(true), + data.parent.clone(), + ) }); if blocked { return true; @@ -84,7 +87,11 @@ impl Ferese { && self.capture_protected(window) }); } - let Some(mut focus) = self.seat.get_pointer().and_then(|pointer| pointer.current_focus()) else { + let Some(mut focus) = self + .seat + .get_pointer() + .and_then(|pointer| pointer.current_focus()) + else { return false; }; while let Some(parent) = get_parent(&focus) { diff --git a/compositor/src/state/capture_privacy_tests.rs b/compositor/src/state/capture_privacy_tests.rs index e98a667..cd22eef 100644 --- a/compositor/src/state/capture_privacy_tests.rs +++ b/compositor/src/state/capture_privacy_tests.rs @@ -22,7 +22,13 @@ use smithay::wayland::shell::xdg::{XdgShellHandler, XdgToplevelSurfaceData, XdgW use crate::Ferese; use crate::state::{ClientState, DesktopOutput}; -pub(crate) fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u32], fd: Option) { +pub(crate) fn request( + wire: &mut UnixStream, + object: u32, + opcode: u32, + args: &[u32], + fd: Option, +) { let bytes = [object, (((args.len() + 2) * 4) as u32) << 16 | opcode] .into_iter() .chain(args.iter().copied()) @@ -47,7 +53,10 @@ pub(crate) fn request(wire: &mut UnixStream, object: u32, opcode: u32, args: &[u (*header).cmsg_type = libc::SCM_RIGHTS; (*header).cmsg_len = libc::CMSG_LEN(std::mem::size_of::() as u32) as usize; libc::CMSG_DATA(header).cast::().write(fd); - assert_eq!(libc::sendmsg(wire.as_raw_fd(), &msg, 0), bytes.len() as isize); + assert_eq!( + libc::sendmsg(wire.as_raw_fd(), &msg, 0), + bytes.len() as isize + ); } } else { wire.write_all(&bytes).unwrap(); @@ -70,7 +79,9 @@ pub(crate) fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 { let object = u32::from_ne_bytes(bytes[offset..offset + 4].try_into().unwrap()); let header = u32::from_ne_bytes(bytes[offset + 4..offset + 8].try_into().unwrap()); if object == xdg && header & 0xffff == 0 { - serial = Some(u32::from_ne_bytes(bytes[offset + 8..offset + 12].try_into().unwrap())); + serial = Some(u32::from_ne_bytes( + bytes[offset + 8..offset + 12].try_into().unwrap(), + )); } offset += (header >> 16) as usize; } @@ -79,7 +90,11 @@ pub(crate) fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 { serial.unwrap() } -pub(crate) fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, color: u32) -> (Window, UnixStream) { +pub(crate) fn window( + state: &mut Ferese, + events: &mut EventLoop<'static, Ferese>, + color: u32, +) -> (Window, UnixStream) { let (server, mut wire) = UnixStream::pair().unwrap(); let client = state .display_handle @@ -93,7 +108,11 @@ pub(crate) fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese> ) .unwrap(); let shell = client - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); let shm = client .create_resource::( @@ -109,7 +128,8 @@ pub(crate) fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese> dispatch(events, state); ack_configure(&mut wire, 3); let mut file = tempfile::tempfile().unwrap(); - file.write_all(&color.to_ne_bytes().repeat(64 * 48)).unwrap(); + file.write_all(&color.to_ne_bytes().repeat(64 * 48)) + .unwrap(); request( &mut wire, shm.id().protocol_id(), @@ -169,7 +189,11 @@ fn attach_children( request(wire, 7, 6, &[], None); request(wire, 2, 6, &[], None); let shell = client - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); request(wire, compositor.id().protocol_id(), 0, &[9], None); request(wire, shell.id().protocol_id(), 2, &[10, 9], None); @@ -185,7 +209,8 @@ fn attach_children( request(wire, 9, 6, &[], None); dispatch(events, state); assert_eq!( - smithay::desktop::PopupManager::popups_for_surface(window.toplevel().unwrap().wl_surface()).count(), + smithay::desktop::PopupManager::popups_for_surface(window.toplevel().unwrap().wl_surface()) + .count(), 1 ); } @@ -224,7 +249,13 @@ fn pixels( Rectangle::from_size((320, 240).into()), )); let mut target = Some(texture.clone()); - crate::backends::direct::capture::capture_output(state, renderer, &mut target, output, &scene); + crate::backends::direct::capture::capture_output( + state, + renderer, + &mut target, + output, + &scene, + ); let mut result = receiver.try_recv().unwrap().result.unwrap().pixels; for pixel in result.chunks_exact_mut(4) { pixel.swap(0, 2); @@ -235,7 +266,11 @@ fn pixels( let mut target = renderer.bind(texture).unwrap(); crate::render::redraw_output(renderer, &mut target, output, &elements).unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((320, 240).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((320, 240).into()), + Fourcc::Abgr8888, + ) .unwrap(); renderer.map_texture(&mapping).unwrap().to_vec() } @@ -300,28 +335,47 @@ fn lock_scene_draws_the_pointer_and_delivers_client_cursor_frames() { smithay::input::pointer::CursorIcon::Default, crate::cursor::fallback_cursor(), ); - state.seat.get_pointer().unwrap().set_location((20.0, 20.0).into()); + state + .seat + .get_pointer() + .unwrap() + .set_location((20.0, 20.0).into()); let device = EGLDevice::enumerate().unwrap().last().expect("EGL device"); let display = unsafe { EGLDisplay::new(device).unwrap() }; let mut renderer = unsafe { GlesRenderer::new(EGLContext::new(&display).unwrap()).unwrap() }; let mut texture = - Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap(); + Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()) + .unwrap(); for scale in [1.0, 1.75] { output.change_current_state(None, None, Some(Scale::Fractional(scale)), None); for include_cursor in [false, true] { let scene = state.sample_frame(&output, Duration::ZERO); - let elements = - crate::render::sampled_output_elements(&mut state, &mut renderer, &output, include_cursor, &scene); + let elements = crate::render::sampled_output_elements( + &mut state, + &mut renderer, + &output, + include_cursor, + &scene, + ); let mut target = renderer.bind(&mut texture).unwrap(); crate::render::redraw_output(&mut renderer, &mut target, &output, &elements).unwrap(); let mapping = renderer - .copy_framebuffer(&target, Rectangle::from_size((320, 240).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &target, + Rectangle::from_size((320, 240).into()), + Fourcc::Abgr8888, + ) .unwrap(); let bytes = renderer.map_texture(&mapping).unwrap(); - let white = bytes.chunks_exact(4).any(|pixel| pixel == [255, 255, 255, 255]); - assert_eq!(white, include_cursor, "cursor above the dim overlay at scale {scale}"); + let white = bytes + .chunks_exact(4) + .any(|pixel| pixel == [255, 255, 255, 255]); + assert_eq!( + white, include_cursor, + "cursor above the dim overlay at scale {scale}" + ); assert!( bytes .chunks_exact(4) @@ -349,15 +403,25 @@ fn lock_scene_draws_the_pointer_and_delivers_client_cursor_frames() { let seat = state.seat.clone(); state.cursor_image(&seat, CursorImageStatus::Surface(cursor.clone())); let scene = state.sample_frame(&output, Duration::ZERO); - let elements = crate::render::sampled_output_elements(&mut state, &mut renderer, &output, true, &scene); + let elements = + crate::render::sampled_output_elements(&mut state, &mut renderer, &output, true, &scene); let mut target = renderer.bind(&mut texture).unwrap(); let mut tracker = OutputDamageTracker::from_output(&output); let rendered = tracker - .render_output(&mut renderer, &mut target, 0, &elements, [0.0, 0.0, 0.0, 1.0]) + .render_output( + &mut renderer, + &mut target, + 0, + &elements, + [0.0, 0.0, 0.0, 1.0], + ) .unwrap(); state.display_presentation.queued(&output, &rendered.states); state.display_presentation.presented(&output); - assert_eq!(state.callback_outputs().get(&(&cursor).into()), Some(&output)); + assert_eq!( + state.callback_outputs().get(&(&cursor).into()), + Some(&output) + ); state.lock_frame_callbacks(&output); state.display_handle.flush_clients().unwrap(); wire.set_nonblocking(true).unwrap(); @@ -371,7 +435,10 @@ fn lock_scene_draws_the_pointer_and_delivers_client_cursor_frames() { callback_done |= object == 9 && header & 0xffff == 0; offset += (header >> 16) as usize; } - assert!(callback_done, "the lock owner's cursor must receive its next frame"); + assert!( + callback_done, + "the lock owner's cursor must receive its next frame" + ); } #[test] @@ -443,9 +510,11 @@ fn capture_privacy_pixels_and_policy_transitions() { let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut display_texture = - Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap(); + Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()) + .unwrap(); let mut capture_texture = - Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap(); + Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()) + .unwrap(); let has_red = |bytes: &[u8]| bytes.chunks_exact(4).any(|pixel| pixel == [255, 0, 0, 255]); assert!(has_red(&pixels( &mut state, @@ -475,11 +544,23 @@ fn capture_privacy_pixels_and_policy_transitions() { assert!(!state.capture_window_allowed(id, epoch)); crate::handlers::window_capture::tests::check_deferred_snapshot(&state, &secret, epoch, false); assert!( - !has_red(&pixels(&mut state, &mut renderer, &output, true, &mut capture_texture)), + !has_red(&pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture + )), "reused capture target leaked previous pixels" ); assert!( - has_red(&pixels(&mut state, &mut renderer, &output, false, &mut display_texture)), + has_red(&pixels( + &mut state, + &mut renderer, + &output, + false, + &mut display_texture + )), "display lost protected content" ); app_id(&mut state, &secret, "ordinary"); @@ -496,11 +577,12 @@ fn capture_privacy_pixels_and_policy_transitions() { true, &mut capture_texture ))); - let rules = - crate::config::Config::parse_source("window-rule app-id=\"ordinary\" block-out-from-screencasts=#true\n") - .unwrap() - .window_rules() - .unwrap(); + let rules = crate::config::Config::parse_source( + "window-rule app-id=\"ordinary\" block-out-from-screencasts=#true\n", + ) + .unwrap() + .window_rules() + .unwrap(); state.window_rules = rules; state.refresh_capture_privacy(); assert!(state.capture_protected(&secret)); @@ -522,18 +604,40 @@ fn capture_privacy_pixels_and_policy_transitions() { Transform::Flipped270, ] { for scale in [1.0, 1.25] { - output.change_current_state(None, Some(transform), Some(Scale::Fractional(scale)), None); + output.change_current_state( + None, + Some(transform), + Some(Scale::Fractional(scale)), + None, + ); assert!( - has_red(&pixels(&mut state, &mut renderer, &output, false, &mut display_texture)), + has_red(&pixels( + &mut state, + &mut renderer, + &output, + false, + &mut display_texture + )), "display {transform:?}/{scale}" ); assert!( - !has_red(&pixels(&mut state, &mut renderer, &output, true, &mut capture_texture)), + !has_red(&pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture + )), "capture {transform:?}/{scale}" ); } } - output.change_current_state(None, Some(Transform::Normal), Some(Scale::Fractional(1.0)), None); + output.change_current_state( + None, + Some(Transform::Normal), + Some(Scale::Fractional(1.0)), + None, + ); // An otherwise unprotected child still inherits its parent's protection. let (child, _child_wire) = window(&mut state, &mut events, 0xff00ff00); with_states(child.toplevel().unwrap().wl_surface(), |states| { @@ -547,14 +651,34 @@ fn capture_privacy_pixels_and_policy_transitions() { }); state.parent_changed(child.toplevel().unwrap().clone()); assert!(state.capture_protected(&child)); - let captured = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); + let captured = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); assert!(!has_red(&captured)); - assert!(!captured.chunks_exact(4).any(|pixel| pixel == [0, 255, 0, 255])); + assert!( + !captured + .chunks_exact(4) + .any(|pixel| pixel == [0, 255, 0, 255]) + ); state.set_overview_active(true); - let overview = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); + let overview = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); assert!(!has_red(&overview)); - assert!(!overview.chunks_exact(4).any(|pixel| pixel == [0, 255, 0, 255])); + assert!( + !overview + .chunks_exact(4) + .any(|pixel| pixel == [0, 255, 0, 255]) + ); state.set_overview_active(false); // Region requests go through the same filtered output readback. @@ -628,19 +752,37 @@ fn capture_privacy_pixels_and_policy_transitions() { state.windows.set_geometry(child_id, geometry); state.space.map_element(child.clone(), (440, 40), false); assert!( - pixels(&mut state, &mut renderer, &second, false, &mut display_texture) - .chunks_exact(4) - .any(|pixel| pixel == [0, 255, 0, 255]) + pixels( + &mut state, + &mut renderer, + &second, + false, + &mut display_texture + ) + .chunks_exact(4) + .any(|pixel| pixel == [0, 255, 0, 255]) ); assert!( - !pixels(&mut state, &mut renderer, &second, true, &mut capture_texture) - .chunks_exact(4) - .any(|pixel| pixel == [0, 255, 0, 255]) + !pixels( + &mut state, + &mut renderer, + &second, + true, + &mut capture_texture + ) + .chunks_exact(4) + .any(|pixel| pixel == [0, 255, 0, 255]) ); // Unprotected content still captures, including translucent pixels. let (public, mut public_wire) = window(&mut state, &mut events, 0x80000080); - let captured = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); + let captured = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); assert!(captured.chunks_exact(4).any(|pixel| pixel[2] > 100)); assert!(!has_red(&captured)); @@ -650,7 +792,13 @@ fn capture_privacy_pixels_and_policy_transitions() { let manager = client .create_resource::(&state.display_handle, 1, ()) .unwrap(); - request(&mut public_wire, manager.id().protocol_id(), 1, &[7, 2], None); + request( + &mut public_wire, + manager.id().protocol_id(), + 1, + &[7, 2], + None, + ); dispatch(&mut events, &mut state); state.theme_settings.material_style = crate::config::MaterialStyle::Translucent; state.theme_settings.backdrop_blur = 12.0; @@ -658,17 +806,49 @@ fn capture_privacy_pixels_and_policy_transitions() { let public_id = state.windows.ids()[&public]; let secret_geometry = *state.windows.geometry(&id).unwrap(); state.windows.set_geometry(public_id, secret_geometry); - state - .space - .map_element(public.clone(), state.space.element_location(&secret).unwrap(), true); - let before = pixels(&mut state, &mut renderer, &output, false, &mut display_texture); - let protected = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); + state.space.map_element( + public.clone(), + state.space.element_location(&secret).unwrap(), + true, + ); + let before = pixels( + &mut state, + &mut renderer, + &output, + false, + &mut display_texture, + ); + let protected = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); state.space.unmap_elem(&secret); state.space.unmap_elem(&child); - let absent = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); - assert_eq!(protected, absent, "protected content leaked through a cached material"); - let without = pixels(&mut state, &mut renderer, &output, false, &mut display_texture); - assert_ne!(before, without, "fixture must place protected content behind glass"); + let absent = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); + assert_eq!( + protected, absent, + "protected content leaked through a cached material" + ); + let without = pixels( + &mut state, + &mut renderer, + &output, + false, + &mut display_texture, + ); + assert_ne!( + before, without, + "fixture must place protected content behind glass" + ); // A stationary client's cursor is a separate surface and can outlive its // protected toplevel. Never reveal that retained image after an opt-out or close. @@ -682,7 +862,13 @@ fn capture_privacy_pixels_and_policy_transitions() { smithay::wayland::GlobalData, ) .unwrap(); - request(&mut secret_wire, compositor.id().protocol_id(), 0, &[13], None); + request( + &mut secret_wire, + compositor.id().protocol_id(), + 0, + &[13], + None, + ); request(&mut secret_wire, 13, 1, &[6, 0, 0], None); request(&mut secret_wire, 13, 6, &[], None); dispatch(&mut events, &mut state); @@ -708,7 +894,9 @@ fn capture_privacy_pixels_and_policy_transitions() { .unwrap(); state.remove_tiled_window(&secret); assert!(state.capture_cursor_protected()); - assert!(matches!(&state.cursor_status, CursorImageStatus::Surface(current) if current == &cursor)); + assert!( + matches!(&state.cursor_status, CursorImageStatus::Surface(current) if current == &cursor) + ); state.cursor_image(&seat, CursorImageStatus::Hidden); assert!(!state.capture_cursor_protected()); assert!(!state.has_capture_exclusions()); @@ -825,14 +1013,19 @@ fn resize_dependencies_protocol_and_pixels() { else { panic!() }; - layout.set_column_width(id, ColumnWidth::Fixed(width)).unwrap(); + layout + .set_column_width(id, ColumnWidth::Fixed(width)) + .unwrap(); state.relayout(); state.display_handle.flush_clients().unwrap(); }; set_width(&mut state, b_id, 240.0); let b_serial = ack_configure(&mut b_wire, 3); dispatch(&mut events, &mut state); - assert!(state.windows.transaction(&b_id).is_some(), "ack alone must not release"); + assert!( + state.windows.transaction(&b_id).is_some(), + "ack alone must not release" + ); // This target uses the first column, independently of the pending later width. let bounds = Rect::new(0.0, 0.0, 320.0, 240.0); state @@ -849,21 +1042,43 @@ fn resize_dependencies_protocol_and_pixels() { before, "independent viewport stalled before delayed buffer commit" ); - assert_eq!(state.windows.geometry(&b_id).unwrap().visual.current.width, held_size); - assert_eq!(state.windows.record(b_id).unwrap().world_x.unwrap().1, held_world); + assert_eq!( + state.windows.geometry(&b_id).unwrap().visual.current.width, + held_size + ); + assert_eq!( + state.windows.record(b_id).unwrap().world_x.unwrap().1, + held_world + ); // BASELINE_REPRO_END: the stage above also runs against the reviewed base. assert!( !state .presentation_dependencies .viewport_blocked(workspace, &state.windows) ); - assert!(state.presentation_dependencies.reflow_blocked(b_id, &state.windows)); - assert!(state.presentation_dependencies.reflow_blocked(c_id, &state.windows)); - assert!(!state.presentation_dependencies.reflow_blocked(a_id, &state.windows)); + assert!( + state + .presentation_dependencies + .reflow_blocked(b_id, &state.windows) + ); + assert!( + state + .presentation_dependencies + .reflow_blocked(c_id, &state.windows) + ); + assert!( + !state + .presentation_dependencies + .reflow_blocked(a_id, &state.windows) + ); let geometry = *state.windows.geometry(&b_id).unwrap(); let position = state.space.element_location(&b).unwrap(); assert_eq!(position.x, geometry.visual.current.x.round() as i32); - let point = (geometry.visual.current.x + 5.0, geometry.visual.current.y + 5.0).into(); + let point = ( + geometry.visual.current.x + 5.0, + geometry.visual.current.y + 5.0, + ) + .into(); let (hit, origin) = state.window_surface_under(point).unwrap(); assert_eq!(hit, *b.toplevel().unwrap().wl_surface()); assert!((origin.x - geometry.visual.current.x).abs() < 0.001); @@ -894,28 +1109,52 @@ fn resize_dependencies_protocol_and_pixels() { let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let mut display_texture = - Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap(); + Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()) + .unwrap(); let mut capture_texture = - Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap(); + Offscreen::::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()) + .unwrap(); // Offscreen fixture explicitly captures the existing native frame; production // capture-before-commit is exercised separately in the privacy regression. - let snapshot = - crate::render::capture_resize_snapshot(&mut renderer, &b, b.geometry(), 1.0, 16 * 1024 * 1024).unwrap(); - state.render.set_snapshot(b_id, snapshot.expect("native snapshot")); + let snapshot = crate::render::capture_resize_snapshot( + &mut renderer, + &b, + b.geometry(), + 1.0, + 16 * 1024 * 1024, + ) + .unwrap(); + state + .render + .set_snapshot(b_id, snapshot.expect("native snapshot")); app_id(&mut state, &b, "resize.secret"); state.refresh_capture_privacy(); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(16)); let actual = state.windows.geometry(&b_id).unwrap().visual.current; - let displayed = pixels(&mut state, &mut renderer, &output, false, &mut display_texture); + let displayed = pixels( + &mut state, + &mut renderer, + &output, + false, + &mut display_texture, + ); let at = ((actual.y.round() as usize + 5) * 320 + actual.x.round() as usize + 5) * 4; assert_eq!( &displayed[at..at + 4], &[0, 255, 0, 255], "live/snapshot pixels did not translate with geometry" ); - let captured = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture); + let captured = pixels( + &mut state, + &mut renderer, + &output, + true, + &mut capture_texture, + ); assert!( - !captured.chunks_exact(4).any(|pixel| pixel == [0, 255, 0, 255]), + !captured + .chunks_exact(4) + .any(|pixel| pixel == [0, 255, 0, 255]), "translated protected snapshot leaked into capture" ); assert!(state.render.snapshot(&b_id).is_some()); @@ -983,7 +1222,9 @@ fn resize_dependencies_protocol_and_pixels() { ) .unwrap(); let mut resized = tempfile::tempfile().unwrap(); - resized.write_all(&0xff00ff00u32.to_ne_bytes().repeat(32 * 48)).unwrap(); + resized + .write_all(&0xff00ff00u32.to_ne_bytes().repeat(32 * 48)) + .unwrap(); request( &mut b_wire, shm.id().protocol_id(), @@ -1008,11 +1249,21 @@ fn resize_dependencies_protocol_and_pixels() { // A hidden workspace must not impose its viewport wait on its replacement. let other = state.workspaces.create_workspace(); let output_id = state.output_id(&output).unwrap(); - state.output_workspaces.assign_workspace(output_id, other).unwrap(); + state + .output_workspaces + .assign_workspace(output_id, other) + .unwrap(); state.workspaces.activate(other).unwrap(); state.relayout(); - assert!(!state.presentation_dependencies.viewport_blocked(other, &state.windows)); - state.output_workspaces.assign_workspace(output_id, workspace).unwrap(); + assert!( + !state + .presentation_dependencies + .viewport_blocked(other, &state.windows) + ); + state + .output_workspaces + .assign_workspace(output_id, workspace) + .unwrap(); state.workspaces.activate(workspace).unwrap(); state.focused_window = Some(a_id); state.relayout(); @@ -1090,7 +1341,9 @@ fn resize_dependencies_protocol_and_pixels() { set_width(&mut state, d_id, 64.0); state.focused_window = Some(d_id); { - let WorkspaceLayout::Scrolling(layout) = &mut state.workspaces.workspace_mut(workspace).unwrap().layout else { + let WorkspaceLayout::Scrolling(layout) = + &mut state.workspaces.workspace_mut(workspace).unwrap().layout + else { panic!() }; layout.set_focus_strategy(ViewportFocusStrategy::Paged); @@ -1130,7 +1383,9 @@ fn resize_dependencies_protocol_and_pixels() { state.animations_enabled = false; state.advance_animations_at(Duration::ZERO, Duration::from_secs(10)); { - let WorkspaceLayout::Scrolling(layout) = &mut state.workspaces.workspace_mut(workspace).unwrap().layout else { + let WorkspaceLayout::Scrolling(layout) = + &mut state.workspaces.workspace_mut(workspace).unwrap().layout + else { panic!() }; layout.move_into_column(d_id, b_id).unwrap(); @@ -1165,7 +1420,10 @@ fn resize_dependencies_protocol_and_pixels() { ); let before = state.windows.geometry(&d_id).unwrap().visual.current; state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(10016)); - assert_eq!(state.windows.geometry(&d_id).unwrap().visual.current, before); + assert_eq!( + state.windows.geometry(&d_id).unwrap().visual.current, + before + ); request(&mut d_wire, 2, 1, &[6, 0, 0], None); request(&mut d_wire, 2, 6, &[], None); @@ -1178,7 +1436,11 @@ fn resize_dependencies_protocol_and_pixels() { state.windows.transaction(&b_id).is_some(), "other tile must still be pending" ); - assert!(!state.presentation_dependencies.reflow_blocked(d_id, &state.windows)); + assert!( + !state + .presentation_dependencies + .reflow_blocked(d_id, &state.windows) + ); assert!(state.windows.geometry(&d_id).unwrap().is_zooming()); // Consume the release tick without charging the preceding client wait. state.advance_animations_at(Duration::ZERO, Duration::from_millis(10016)); @@ -1199,8 +1461,15 @@ fn resize_dependencies_protocol_and_pixels() { dispatch(&mut events, &mut state); assert!(state.windows.transaction(&d_id).is_none()); assert!(state.windows.transaction(&b_id).is_some()); - assert!(state.presentation_dependencies.reflow_blocked(d_id, &state.windows)); + assert!( + state + .presentation_dependencies + .reflow_blocked(d_id, &state.windows) + ); let returning = state.windows.geometry(&d_id).unwrap().visual.current; state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(10048)); - assert_eq!(state.windows.geometry(&d_id).unwrap().visual.current, returning); + assert_eq!( + state.windows.geometry(&d_id).unwrap().visual.current, + returning + ); } diff --git a/compositor/src/state/floating.rs b/compositor/src/state/floating.rs index 711709b..4d7e9dc 100644 --- a/compositor/src/state/floating.rs +++ b/compositor/src/state/floating.rs @@ -1,14 +1,22 @@ use super::*; impl Ferese { - pub(super) fn adjust_floating_direction(&mut self, id: WindowId, direction: Direction, resize: bool) { + pub(super) fn adjust_floating_direction( + &mut self, + id: WindowId, + direction: Direction, + resize: bool, + ) { const STEP: f64 = 32.0; let workspace = self.workspaces.active(); if self.session_lock.active() || self.input_capture.captures(1) || self.workspaces.workspace_for_window(id) != Some(workspace.id) || workspace.fullscreen == Some(id) - || self.windows.record(id).is_some_and(|record| record.maximized) + || self + .windows + .record(id) + .is_some_and(|record| record.maximized) { return; } @@ -32,7 +40,10 @@ impl Ferese { if resize { destination = constrained_floating_rect( destination, - self.window_constraints().get(&id).copied().unwrap_or_default(), + self.window_constraints() + .get(&id) + .copied() + .unwrap_or_default(), ); } destination = crate::floating::clamp_to_work(destination, work); @@ -91,7 +102,10 @@ impl Ferese { .automatic_axis(Some(window), bounds) .unwrap_or(Axis::Horizontal); - if let Err(error) = self.workspaces.toggle_floating(window, floating_rect, axis, 0.5) { + if let Err(error) = self + .workspaces + .toggle_floating(window, floating_rect, axis, 0.5) + { tracing::error!(%error, ?window, "failed to toggle floating window"); return; } @@ -142,7 +156,11 @@ impl Ferese { && let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout { if enabled { - if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) { + if let Some(column) = layout + .columns() + .iter() + .find(|column| column.windows.contains(&window)) + { self.windows.update(window, |w| { w.maximized_column_width.get_or_insert(column.width); }); @@ -177,7 +195,12 @@ impl Ferese { layer != Layer::Top || !self.output_has_fullscreen(output) } - pub(crate) fn layer_visible_for_frame(&self, output: &Output, layer: Layer, overview: bool) -> bool { + pub(crate) fn layer_visible_for_frame( + &self, + output: &Output, + layer: Layer, + overview: bool, + ) -> bool { layer != Layer::Top || !self.output_has_fullscreen_for_frame(output, overview) } @@ -208,7 +231,9 @@ impl Ferese { let Some(id) = self.windows.ids().get(window).copied() else { return; }; - self.windows.update(id, |w| w.resize_anchor.take()).flatten(); + self.windows + .update(id, |w| w.resize_anchor.take()) + .flatten(); let rect = Rect::new( location.x as f64, location.y as f64, @@ -224,8 +249,11 @@ impl Ferese { // Direct manipulation follows the pointer, including while the client is // still drawing its next buffer. Rendering and hit testing share this rect. #[cfg(feature = "resize-metrics")] - self.resize_metrics - .end(id, self.start_time.elapsed(), crate::resize_metrics::End::Cancelled); + self.resize_metrics.end( + id, + self.start_time.elapsed(), + crate::resize_metrics::End::Cancelled, + ); self.windows.clear_transaction(&id); self.render.clear_snapshot(&id); let Some(record) = self.windows.record_mut(id) else { @@ -248,7 +276,8 @@ impl Ferese { toplevel.send_pending_configure(); } self.windows.update(id, |record| record.world_x = None); - self.windows.update(id, |record| record.coupled_width = None); + self.windows + .update(id, |record| record.coupled_width = None); self.map_window_geometry(window.clone(), location); self.sync_window_stacking(); // Move/resize/cancel callbacks run with Smithay's pointer mutex held. @@ -268,14 +297,19 @@ impl Ferese { } pub(super) fn output_bounds(&self) -> Option { - let output = self.focused_output().or_else(|| self.space.outputs().next())?; + let output = self + .focused_output() + .or_else(|| self.space.outputs().next())?; self.output_bounds_for(output) } pub(super) fn floating_outputs(&self) -> Vec<(String, Rect)> { self.space .outputs() - .filter_map(|output| self.output_bounds_for(output).map(|work| (output.name(), work))) + .filter_map(|output| { + self.output_bounds_for(output) + .map(|work| (output.name(), work)) + }) .collect() } @@ -294,7 +328,11 @@ impl Ferese { .unwrap_or_default() } - pub(super) fn floating_obstacles(&self, exclude: Option, work: Rect) -> Vec<(u64, Rect)> { + pub(super) fn floating_obstacles( + &self, + exclude: Option, + work: Rect, + ) -> Vec<(u64, Rect)> { self.space .elements() .filter_map(|window| { @@ -312,7 +350,10 @@ impl Ferese { .workspace(workspace)? .fullscreen .is_some_and(|full| full != id) - && !matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })) + && !matches!( + self.workspaces.placement(id), + Some(WindowPlacement::Floating { .. }) + ) { return None; } @@ -347,7 +388,11 @@ impl Ferese { .and_then(|rect| intersection(rect, work)); let dialog = crate::window_rules::is_native_dialog(app.as_deref()); let remembered = id - .and_then(|id| self.windows.record(id).and_then(|w| w.floating_memory.as_ref())) + .and_then(|id| { + self.windows + .record(id) + .and_then(|w| w.floating_memory.as_ref()) + }) .or_else(|| { if parent.is_none() { app.as_deref().and_then(|app| self.floating_memory.get(app)) @@ -384,7 +429,12 @@ impl Ferese { size }; let sized = constrained_floating_rect( - Rect::new(0., 0., width.unwrap_or(stored_size.0), height.unwrap_or(stored_size.1)), + Rect::new( + 0., + 0., + width.unwrap_or(stored_size.0), + height.unwrap_or(stored_size.1), + ), constraints, ); let dimensions = (sized.width, sized.height); @@ -438,7 +488,9 @@ impl Ferese { .floating_outputs() .into_iter() .max_by(|(_, a), (_, b)| { - let area = |work| crate::floating::intersection(rect, work).map_or(0., |r| r.width * r.height); + let area = |work| { + crate::floating::intersection(rect, work).map_or(0., |r| r.width * r.height) + }; area(*a).total_cmp(&area(*b)) }) .filter(|(_, work)| crate::floating::intersection(rect, *work).is_some()) @@ -446,7 +498,8 @@ impl Ferese { return; }; let entry = crate::floating::Remembered::new(output, rect, work); - self.windows.update(id, |w| w.floating_memory = Some(entry.clone())); + self.windows + .update(id, |w| w.floating_memory = Some(entry.clone())); if let Some(app) = app { self.floating_memory.save(app, entry); } @@ -462,13 +515,18 @@ impl Ferese { &self, window: &Window, raw: Rect, - ) -> (Vec, Vec) { + ) -> ( + Vec, + Vec, + ) { let id = self.windows.ids().get(window).copied(); let work = self .floating_outputs() .into_iter() .max_by(|(_, a), (_, b)| { - let area = |work| crate::floating::intersection(raw, work).map_or(0., |r| r.width * r.height); + let area = |work| { + crate::floating::intersection(raw, work).map_or(0., |r| r.width * r.height) + }; area(*a).total_cmp(&area(*b)) }) .filter(|(_, work)| crate::floating::intersection(raw, *work).is_some()) diff --git a/compositor/src/state/focus.rs b/compositor/src/state/focus.rs index bf351ff..fabf3c6 100644 --- a/compositor/src/state/focus.rs +++ b/compositor/src/state/focus.rs @@ -56,9 +56,17 @@ impl Ferese { .find(|id| { self.workspaces.workspace_for_window(*id) == Some(workspace) && matches!( - (self.workspaces.placement(current), self.workspaces.placement(*id)), - (Some(WindowPlacement::Tiled), Some(WindowPlacement::Floating { .. })) - | (Some(WindowPlacement::Floating { .. }), Some(WindowPlacement::Tiled)) + ( + self.workspaces.placement(current), + self.workspaces.placement(*id) + ), + ( + Some(WindowPlacement::Tiled), + Some(WindowPlacement::Floating { .. }) + ) | ( + Some(WindowPlacement::Floating { .. }), + Some(WindowPlacement::Tiled) + ) ) }); if let Some(id) = target { @@ -74,7 +82,9 @@ impl Ferese { let available = self.focus_candidates(); if !preview { self.cancel_focus_cycle(); - let order = self.focus_history.candidates(self.focused_window, &available); + let order = self + .focus_history + .candidates(self.focused_window, &available); let mut cycle = ferese_core::FocusCycle::new(order, self.focused_window); if let Some(id) = cycle.advance(reverse, &available) { self.activate_managed_window(id); @@ -82,7 +92,9 @@ impl Ferese { return; } if self.focus_cycle.is_none() { - let order = self.focus_history.candidates(self.focused_window, &available); + let order = self + .focus_history + .candidates(self.focused_window, &available); if order.len() < 2 { return; } @@ -194,7 +206,10 @@ mod tests { } fn placements(&self) -> Vec> { - self.ids.iter().map(|id| self.state.workspaces.placement(*id)).collect() + self.ids + .iter() + .map(|id| self.state.workspaces.placement(*id)) + .collect() } fn press(&mut self, expected: usize) { @@ -228,7 +243,10 @@ mod tests { #[test] fn tiled_focuses_most_recently_focused_float() { - let Some(mut f) = fixture("tiled_focuses_most_recently_focused_float", &[false, true, true]) else { + let Some(mut f) = fixture( + "tiled_focuses_most_recently_focused_float", + &[false, true, true], + ) else { return; }; f.focus(1); @@ -238,7 +256,10 @@ mod tests { #[test] fn floating_focuses_most_recently_focused_tile() { - let Some(mut f) = fixture("floating_focuses_most_recently_focused_tile", &[false, false, true]) else { + let Some(mut f) = fixture( + "floating_focuses_most_recently_focused_tile", + &[false, false, true], + ) else { return; }; f.focus(0); @@ -248,7 +269,10 @@ mod tests { #[test] fn multiple_floaters_do_not_cycle_or_wrap() { - let Some(mut f) = fixture("multiple_floaters_do_not_cycle_or_wrap", &[false, true, true, true]) else { + let Some(mut f) = fixture( + "multiple_floaters_do_not_cycle_or_wrap", + &[false, true, true, true], + ) else { return; }; f.focus(2); @@ -336,7 +360,10 @@ mod tests { #[test] fn focus_floating_preserves_all_window_placements() { - let Some(mut f) = fixture("focus_floating_preserves_all_window_placements", &[false, true, true]) else { + let Some(mut f) = fixture( + "focus_floating_preserves_all_window_placements", + &[false, true, true], + ) else { return; }; let placements = f.placements(); @@ -351,7 +378,10 @@ mod tests { use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame}; use std::sync::mpsc; use std::time::{Duration, Instant}; - let Some(mut f) = fixture("ipc_focus_floating_uses_the_managed_activation_path", &[false, true]) else { + let Some(mut f) = fixture( + "ipc_focus_floating_uses_the_managed_activation_path", + &[false, true], + ) else { return; }; f.focus(0); @@ -361,7 +391,9 @@ mod tests { let (send, receive) = mpsc::channel(); let worker = std::thread::spawn(move || { let mut socket = UnixStream::connect(path).unwrap(); - socket.set_read_timeout(Some(Duration::from_secs(3))).unwrap(); + socket + .set_read_timeout(Some(Duration::from_secs(3))) + .unwrap(); write_frame( &mut socket, &Request { @@ -373,7 +405,8 @@ mod tests { }, ) .unwrap(); - send.send(read_frame::(&mut socket).unwrap()).unwrap(); + send.send(read_frame::(&mut socket).unwrap()) + .unwrap(); }); let deadline = Instant::now() + Duration::from_secs(3); let response = loop { @@ -381,7 +414,9 @@ mod tests { break response; } assert!(Instant::now() < deadline, "IPC focus action timed out"); - f.events.dispatch(Duration::from_millis(1), &mut f.state).unwrap(); + f.events + .dispatch(Duration::from_millis(1), &mut f.state) + .unwrap(); }; worker.join().unwrap(); assert!(response.error.is_none(), "{:?}", response.error); diff --git a/compositor/src/state/hit_testing.rs b/compositor/src/state/hit_testing.rs index d5bf4ef..0f800b8 100644 --- a/compositor/src/state/hit_testing.rs +++ b/compositor/src/state/hit_testing.rs @@ -1,7 +1,10 @@ use super::*; impl Ferese { - pub fn surface_under(&self, position: Point) -> Option<(WlSurface, Point)> { + pub fn surface_under( + &self, + position: Point, + ) -> Option<(WlSurface, Point)> { if self.session_lock.active() { return self.lock_surface_under(position); } @@ -29,7 +32,10 @@ impl Ferese { }) } - pub fn layer_under(&self, position: Point) -> Option<(LayerSurface, WlSurface, Point)> { + pub fn layer_under( + &self, + position: Point, + ) -> Option<(LayerSurface, WlSurface, Point)> { if self.session_lock.active() { return None; } @@ -69,7 +75,8 @@ impl Ferese { return None; } - let (source_x, source_y) = self.inverse_presented_window_point(*id, position.x, position.y)?; + let (source_x, source_y) = + self.inverse_presented_window_point(*id, position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); let surface = window.surface_under(source_point, WindowSurfaceType::ALL); @@ -110,7 +117,8 @@ impl Ferese { for layer in map.layers_on(*requested).rev() { let geometry = map.layer_geometry(layer)?; let layer_position = output_position - geometry.loc.to_f64(); - let Some((surface, surface_location)) = layer.surface_under(layer_position, WindowSurfaceType::ALL) + let Some((surface, surface_location)) = + layer.surface_under(layer_position, WindowSurfaceType::ALL) else { continue; }; @@ -135,9 +143,13 @@ impl Ferese { return; } - send_frames_surface_tree(surface, output, self.start_time.elapsed(), None, |surface, _| { - eligible.get(&surface.into()).cloned() - }); + send_frames_surface_tree( + surface, + output, + self.start_time.elapsed(), + None, + |surface, _| eligible.get(&surface.into()).cloned(), + ); } pub fn window_under_visual(&self, position: Point) -> Option { @@ -165,9 +177,13 @@ impl Ferese { } if !overview_active { - let (source_x, source_y) = self.inverse_presented_window_point(*id, position.x, position.y)?; - let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); - if window.surface_under(source_point, WindowSurfaceType::ALL).is_none() + let (source_x, source_y) = + self.inverse_presented_window_point(*id, position.x, position.y)?; + let source_point = + Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); + if window + .surface_under(source_point, WindowSurfaceType::ALL) + .is_none() && !self.window_fills_visual_bounds(*id, workspace) { return None; @@ -184,9 +200,13 @@ impl Ferese { } } - pub(super) fn workspace_under_pointer(&self, position: Point) -> Option { + pub(super) fn workspace_under_pointer( + &self, + position: Point, + ) -> Option { let output = self.space.output_under(position).next()?; - self.output_workspaces.active_workspace(self.output_id(output)?) + self.output_workspaces + .active_workspace(self.output_id(output)?) } pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool { @@ -194,7 +214,8 @@ impl Ferese { return false; }; if self.focus_cycle.is_some() && self.overview.is_active() { - return self.overview.has_window_preview(window) && self.focus_preview_output(window) == Some(output_id); + return self.overview.has_window_preview(window) + && self.focus_preview_output(window) == Some(output_id); } let workspace = self.workspaces.workspace_for_window(window); workspace == self.output_workspaces.active_workspace(output_id) @@ -227,7 +248,10 @@ impl Ferese { )) } - pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option> { + pub(crate) fn visual_rect_for_window( + &self, + window: &Window, + ) -> Option> { let id = self.windows.ids().get(window)?; let rect = self.presented_window_rect(*id)?; let size = ClientSize::from_rect(rect); diff --git a/compositor/src/state/layout.rs b/compositor/src/state/layout.rs index db1f7bd..22ae836 100644 --- a/compositor/src/state/layout.rs +++ b/compositor/src/state/layout.rs @@ -51,7 +51,10 @@ impl Ferese { return bounds; }; let map = layer_map_for_output(output); - for layer in map.layers().filter(|layer| layer.namespace() == "ferese-shell-top-bar") { + for layer in map + .layers() + .filter(|layer| layer.namespace() == "ferese-shell-top-bar") + { let policy = layer.cached_state(); let ExclusiveZone::Exclusive(amount) = policy.exclusive_zone else { continue; @@ -126,7 +129,13 @@ impl Ferese { .tab_bars .values() .flatten() - .flat_map(|bar| bar.group.windows.iter().copied().filter(|id| *id != bar.group.active)) + .flat_map(|bar| { + bar.group + .windows + .iter() + .copied() + .filter(|id| *id != bar.group.active) + }) .collect(); let windows = self .windows @@ -149,7 +158,8 @@ impl Ferese { if inactive_tabs.contains(&id) { // An inactive client must not hold its column's resize or viewport. self.windows.clear_transaction(&id); - self.windows.update(id, |record| record.coupled_width = None); + self.windows + .update(id, |record| record.coupled_width = None); } self.space.unmap_elem(&window); } @@ -201,7 +211,8 @@ impl Ferese { self.space .outputs() .filter(|output| { - self.output_has_animations(output) || self.output_has_pending_visual_changes(output) + self.output_has_animations(output) + || self.output_has_pending_visual_changes(output) }) .cloned(), ); @@ -210,7 +221,8 @@ impl Ferese { self.refresh_input_capture_zones(); let visible_workspaces = self.visible_workspace_ids(); self.reconcile_workspaces(&visible_workspaces); - self.tab_bars.retain(|id, _| visible_workspaces.contains(id)); + self.tab_bars + .retain(|id, _| visible_workspaces.contains(id)); if self.session_lock.active() { self.configure_lock_surfaces(); } @@ -234,7 +246,9 @@ impl Ferese { id, record .resize - .and_then(|transaction| transaction.column_width().map(|(source, _)| source)) + .and_then(|transaction| { + transaction.column_width().map(|(source, _)| source) + }) .or_else(|| { // A sibling may have committed while this column is still // held. Replacement configures must share that cohort's @@ -252,9 +266,13 @@ impl Ferese { || **sibling == column.windows[column.active] }) .find_map(|sibling| { - self.windows.transaction(sibling).and_then(|transaction| { - transaction.column_width().map(|(source, _)| source) - }) + self.windows.transaction(sibling).and_then( + |transaction| { + transaction + .column_width() + .map(|(source, _)| source) + }, + ) }) }) }) @@ -271,7 +289,11 @@ impl Ferese { continue; }; for column in layout.columns() { - if !column.windows.iter().any(|id| pending_column_width_cycles.contains(id)) { + if !column + .windows + .iter() + .any(|id| pending_column_width_cycles.contains(id)) + { continue; } // A cycle belongs to the column. Copy one presented motion @@ -280,10 +302,9 @@ impl Ferese { let mut rows = column.windows.iter().filter(|id| { (column.display != ferese_layout::ColumnDisplay::Tabbed || **id == column.windows[column.active]) - && self - .windows - .geometry(id) - .is_some_and(|geometry| !geometry.is_zooming() && !geometry.is_fullscreen()) + && self.windows.geometry(id).is_some_and(|geometry| { + !geometry.is_zooming() && !geometry.is_fullscreen() + }) }); let source = rows .clone() @@ -353,7 +374,9 @@ impl Ferese { let workspace_focus = workspace.last_focused; let focused = self .focused_window - .filter(|window| self.workspaces.workspace_for_window(*window) == Some(workspace_id)) + .filter(|window| { + self.workspaces.workspace_for_window(*window) == Some(workspace_id) + }) .or(workspace_focus); let Some(workspace) = self.workspaces.workspace_mut(workspace_id) else { continue; @@ -403,12 +426,15 @@ impl Ferese { crate::handlers::set_surface_tree_output(toplevel.wl_surface(), &output); } - let is_maximized = - self.windows.record(*id).is_some_and(|w| w.maximized) && workspace_fullscreen != Some(*id); + let is_maximized = self.windows.record(*id).is_some_and(|w| w.maximized) + && workspace_fullscreen != Some(*id); let placement = self.workspaces.placement(*id); let is_floating = matches!(placement, Some(WindowPlacement::Floating { .. })); // Maximization cannot make an inactive tab visible again. - if !is_floating && workspace_fullscreen != Some(*id) && !layout.geometry.contains_key(id) { + if !is_floating + && workspace_fullscreen != Some(*id) + && !layout.geometry.contains_key(id) + { continue; } let rect = if workspace_fullscreen == Some(*id) { @@ -424,8 +450,10 @@ impl Ferese { rect } Some(WindowPlacement::Floating { rect }) => { - let constrained = - constrained_floating_rect(rect, constraints.get(id).copied().unwrap_or_default()); + let constrained = constrained_floating_rect( + rect, + constraints.get(id).copied().unwrap_or_default(), + ); if constrained != rect { let _ = self.workspaces.set_floating_rect(*id, constrained); } @@ -438,13 +466,14 @@ impl Ferese { let is_fullscreen = workspace_fullscreen == Some(*id); visible.insert(*id); - let scrolling = if !is_fullscreen && !is_floating && (!is_maximized || is_scrolling_layout) { - viewport_target - .zip(viewport_current) - .map(|(target, current)| (workspace_id, rect.x + target, current)) - } else { - None - }; + let scrolling = + if !is_fullscreen && !is_floating && (!is_maximized || is_scrolling_layout) { + viewport_target + .zip(viewport_current) + .map(|(target, current)| (workspace_id, rect.x + target, current)) + } else { + None + }; placements.push(( window.clone(), @@ -465,7 +494,9 @@ impl Ferese { placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id)); - for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in placements { + for (window, id, rect, is_fullscreen, is_maximized, is_floating, scrolling, couple_width) in + placements + { let was_mapped = self.space.element_location(&window).is_some(); let Some(record) = self.windows.record_mut(id) else { continue; @@ -476,7 +507,8 @@ impl Ferese { record.coupled_width = None; } - let natural_pending = record.natural_floating_pending && is_floating && !is_fullscreen && !is_maximized; + let natural_pending = + record.natural_floating_pending && is_floating && !is_fullscreen && !is_maximized; let had_geometry = record.geometry.is_some(); let geometry = record .geometry @@ -504,8 +536,12 @@ impl Ferese { } if let Some((workspace, world_x, viewport_x)) = scrolling { - let restored_world_x = - restored_scrolling_world_x(had_geometry, geometry.visual.current.x, viewport_x, world_x); + let restored_world_x = restored_scrolling_world_x( + had_geometry, + geometry.visual.current.x, + viewport_x, + world_x, + ); let mut animated_world_x = previous_scrolling_world_x .remove(&id) .filter(|(previous_workspace, _)| *previous_workspace == workspace) @@ -534,19 +570,19 @@ impl Ferese { } record.world_x = Some((workspace, animated_world_x)); - let coupled = !geometry.is_zooming() - && (couple_width - || record - .coupled_width - .as_ref() - .is_some_and(|(previous_workspace, _)| *previous_workspace == workspace)); + let coupled = + !geometry.is_zooming() + && (couple_width + || record.coupled_width.as_ref().is_some_and( + |(previous_workspace, _)| *previous_workspace == workspace, + )); if coupled { if let Some(source) = column_width_sources.get(&id) { record.coupled_width = Some(*source); } - let width = record - .coupled_width - .get_or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width))); + let width = record.coupled_width.get_or_insert_with(|| { + (workspace, AnimatedValue::new(geometry.visual.current.width)) + }); if width.0 != workspace { *width = (workspace, AnimatedValue::new(geometry.visual.current.width)); } @@ -628,7 +664,9 @@ impl Ferese { .workspace_for_window(id) .and_then(|workspace| self.workspaces.workspace(workspace)) .and_then(|workspace| match &workspace.layout { - WorkspaceLayout::Scrolling(layout) => layout.allocated_column_width(id), + WorkspaceLayout::Scrolling(layout) => { + layout.allocated_column_width(id) + } _ => None, }), ), @@ -670,7 +708,9 @@ impl Ferese { pub(crate) fn sync_window_stacking(&mut self) { let revision = self.window_stack.revision(); - let rebuild = self.stacking_cache.needs_rebuild(revision, self.space.elements().len()); + let rebuild = self + .stacking_cache + .needs_rebuild(revision, self.space.elements().len()); if !rebuild && !self.stacking_cache.needs_restore() { return; @@ -681,30 +721,43 @@ impl Ferese { windows.sort_by_key(|window| { let id = self.windows.ids().get(window).copied(); let geometry = id.and_then(|id| self.windows.geometry(&id)); - let floating = id - .is_some_and(|id| matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))); + let floating = id.is_some_and(|id| { + matches!( + self.workspaces.placement(id), + Some(WindowPlacement::Floating { .. }) + ) + }); let priority = crate::stacking::layer_priority( floating, geometry.is_some_and(|geometry| geometry.is_zooming()), geometry.is_some_and(|geometry| geometry.is_fullscreen()), floating && id.is_some_and(|id| { - self.floating_above_fullscreen.get(&id).is_some_and(|parent| { - self.workspaces - .workspace_for_window(id) - .and_then(|workspace| self.workspaces.workspace(workspace)) - .is_some_and(|workspace| workspace.fullscreen == Some(*parent)) - }) + self.floating_above_fullscreen + .get(&id) + .is_some_and(|parent| { + self.workspaces + .workspace_for_window(id) + .and_then(|workspace| self.workspaces.workspace(workspace)) + .is_some_and(|workspace| { + workspace.fullscreen == Some(*parent) + }) + }) }), ); - (priority, id.map_or(usize::MAX, |id| self.window_stack.rank(id))) + ( + priority, + id.map_or(usize::MAX, |id| self.window_stack.rank(id)), + ) }); self.stacking_cache.replace(windows, revision); } - if !stacking_order_settled(self.space.elements(), self.stacking_cache.order(), |window| { - window.z_index() - }) { + if !stacking_order_settled( + self.space.elements(), + self.stacking_cache.order(), + |window| window.z_index(), + ) { for window in self.stacking_cache.order() { self.space.raise_element(window, false); } @@ -722,16 +775,17 @@ impl Ferese { self.stacking_cache.remapped(); } - pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result { + pub(super) fn tiled_layout( + &mut self, + bounds: Rect, + ) -> Result { let constraints = self.window_constraints(); let focused = self.focused_window; let bounds = self.workspace_layout_bounds(bounds, &self.workspaces.active().layout); - self.workspaces.active_mut().layout.resolve_geometry_with_constraints( - bounds, - self.gap_config, - &constraints, - focused, - ) + self.workspaces + .active_mut() + .layout + .resolve_geometry_with_constraints(bounds, self.gap_config, &constraints, focused) } #[cfg(test)] @@ -745,7 +799,10 @@ impl Ferese { let mut cache = self.workspace_previews.borrow_mut(); let mut live = HashSet::new(); let mut previews = HashMap::new(); - for view in self.output_workspaces.workspace_views_iter(&self.workspaces) { + for view in self + .output_workspaces + .workspace_views_iter(&self.workspaces) + { let Some(workspace) = self.workspaces.workspace(view.id) else { continue; }; @@ -799,19 +856,31 @@ impl Ferese { // other shell metadata never invalidate geometry. let mut layout = workspace.layout.clone(); let mut rectangles = layout - .resolve_geometry_with_constraints(input.bounds, input.gaps, &constraints, input.focused) + .resolve_geometry_with_constraints( + input.bounds, + input.gaps, + &constraints, + input.focused, + ) .map(|resolved| resolved.geometry) .unwrap_or_default(); for window in &input.windows { if let Some(rect) = window.floating { - rectangles.insert(window.id, constrained_floating_rect(rect, window.constraints)); + rectangles.insert( + window.id, + constrained_floating_rect(rect, window.constraints), + ); } if input.fullscreen == Some(window.id) { rectangles.insert(window.id, input.fullscreen_bounds); } else if window.maximized - && (window.floating.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_))) + && (window.floating.is_some() + || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_))) { - rectangles.insert(window.id, maximized_rect(input.maximized_bounds, input.gaps.outer)); + rectangles.insert( + window.id, + maximized_rect(input.maximized_bounds, input.gaps.outer), + ); } } cache.entries.insert( @@ -827,7 +896,12 @@ impl Ferese { cache.resolutions += 1; } } - previews.extend(cache.entries[&view.id].rectangles.iter().map(|(&id, &rect)| (id, rect))); + previews.extend( + cache.entries[&view.id] + .rectangles + .iter() + .map(|(&id, &rect)| (id, rect)), + ); } cache.entries.retain(|id, _| live.contains(id)); previews @@ -844,8 +918,11 @@ impl Ferese { let state = cached.current(); (state.min_size, state.max_size) }); - let (minimum, maximum) = - self.effective_size_constraints(window, (minimum.w, minimum.h), (maximum.w, maximum.h)); + let (minimum, maximum) = self.effective_size_constraints( + window, + (minimum.w, minimum.h), + (maximum.w, maximum.h), + ); Some(( *id, diff --git a/compositor/src/state/lifecycle.rs b/compositor/src/state/lifecycle.rs index 636e822..40b6b40 100644 --- a/compositor/src/state/lifecycle.rs +++ b/compositor/src/state/lifecycle.rs @@ -7,7 +7,11 @@ impl Ferese { // on_commit_buffer_handler has not reset the previous renderer state. let removed = with_states(surface, |states| { matches!( - states.cached_state.get::().current().buffer, + states + .cached_state + .get::() + .current() + .buffer, Some(BufferAssignment::Removed) ) }); @@ -23,13 +27,20 @@ impl Ferese { } pub(super) fn retain_closed_window(&mut self, window: &Window) { - if !self.animations_enabled || self.session_lock.active() || self.render.closing.len() >= 64 { + if !self.animations_enabled || self.session_lock.active() || self.render.closing.len() >= 64 + { return; } let Some(&id) = self.windows.ids().get(window) else { return; }; - if self.render.closing.iter().any(|window| window.presentation.id == id) || !self.window_content_ready(window) { + if self + .render + .closing + .iter() + .any(|window| window.presentation.id == id) + || !self.window_content_ready(window) + { return; } let Some(output) = self @@ -77,8 +88,17 @@ impl Ferese { .position(|candidate| *candidate == id) .and_then(|index| order.get(index + 1)) .copied(); - let used = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum::() - + self.render.closing.iter().map(|window| window.bytes()).sum::(); + let used = self + .render + .snapshots() + .map(|snapshot| snapshot.bytes()) + .sum::() + + self + .render + .closing + .iter() + .map(|window| window.bytes()) + .sum::(); let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used); let scale = output.current_scale().fractional_scale(); let result = if let Some(backend) = &self.nested_backend { @@ -103,7 +123,9 @@ impl Ferese { let handoff = self .render .snapshot(&id) - .filter(|old| old.context == snapshot.context && (old.scale - scale).abs() < 0.001) + .filter(|old| { + old.context == snapshot.context && (old.scale - scale).abs() < 0.001 + }) .cloned() .map(|old| (old, handoff_size)); let material = window.toplevel().and_then(|toplevel| { diff --git a/compositor/src/state/navigation.rs b/compositor/src/state/navigation.rs index 4261b82..e95e521 100644 --- a/compositor/src/state/navigation.rs +++ b/compositor/src/state/navigation.rs @@ -11,11 +11,14 @@ impl Ferese { && !self.overview.is_presenting() && self.workspaces.active_id() == swipe.workspace && self.focused_window == Some(swipe.from) - && self.workspaces.workspace(swipe.workspace).is_some_and(|workspace| { - workspace.fullscreen.is_none() - && workspace.layout.contains(swipe.from) - && workspace.layout.contains(swipe.to) - }) + && self + .workspaces + .workspace(swipe.workspace) + .is_some_and(|workspace| { + workspace.fullscreen.is_none() + && workspace.layout.contains(swipe.from) + && workspace.layout.contains(swipe.to) + }) } pub(crate) fn preview_focus_swipe( @@ -25,16 +28,28 @@ impl Ferese { progress: f64, ) { if self.focus_swipe().is_none() { - if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() { + if self.swipe.preview_started() + || !self.animations_enabled + || self.overview.is_presenting() + { return; } - let Some(from) = self.focused_window else { return }; - let Some(bounds) = self.output_bounds() else { return }; + let Some(from) = self.focused_window else { + return; + }; + let Some(bounds) = self.output_bounds() else { + return; + }; let workspace = self.workspaces.active(); - if workspace.fullscreen.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) { + if workspace.fullscreen.is_some() + || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) + { return; } - let Ok(neighbor) = workspace.layout.directional_neighbor(from, direction, bounds) else { + let Ok(neighbor) = workspace + .layout + .directional_neighbor(from, direction, bounds) + else { return; }; let to = neighbor.unwrap_or(from); @@ -54,7 +69,12 @@ impl Ferese { } let layout_bounds = self.workspace_layout_bounds(bounds, &candidate); if candidate - .resolve_geometry_with_constraints(layout_bounds, self.gap_config, &self.window_constraints(), Some(to)) + .resolve_geometry_with_constraints( + layout_bounds, + self.gap_config, + &self.window_constraints(), + Some(to), + ) .is_err() { return; @@ -86,7 +106,10 @@ impl Ferese { self.swipe.mark_preview_started(); } if let Some(workspace) = self.focus_swipe().map(|swipe| swipe.workspace) { - self.viewports.get_mut(&workspace).unwrap().update_gesture(progress); + self.viewports + .get_mut(&workspace) + .unwrap() + .update_gesture(progress); } if let Some(output) = self @@ -109,8 +132,10 @@ impl Ferese { .viewport_blocked(swipe.workspace, &self.windows); // Release from the last input position even if no frame rendered that update. let velocity = (current && !blocked).then(|| { - swipe.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity) - / self.animation_speed + swipe.release_velocity( + self.swipe.release_velocity, + self.swipe.unbounded_release_velocity, + ) / self.animation_speed }); let workspace = swipe.workspace; let swipe = self @@ -178,17 +203,23 @@ impl Ferese { let Some(window) = self.windows.window(next).cloned() else { return; }; - let Some(surface) = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()) else { + let Some(surface) = window + .toplevel() + .map(|toplevel| toplevel.wl_surface().clone()) + else { return; }; self.focused_window = Some(next); self.raise_window(&window, true); - self.seat.get_keyboard().expect("seat has a keyboard").set_focus( - self, - Some(surface), - smithay::utils::SERIAL_COUNTER.next_serial(), - ); + self.seat + .get_keyboard() + .expect("seat has a keyboard") + .set_focus( + self, + Some(surface), + smithay::utils::SERIAL_COUNTER.next_serial(), + ); for window in self.space.elements() { if let Some(toplevel) = window.toplevel() { @@ -281,18 +312,22 @@ impl Ferese { return; }; if self.workspaces.active().layout.mode() == LayoutMode::Tree { - if let Some(direction) = [Direction::Left, Direction::Right, Direction::Up, Direction::Down] - .into_iter() - .find(|direction| { - self.workspaces - .active() - .layout - .directional_neighbor(window, *direction, bounds) - .ok() - .flatten() - .is_some() - }) - { + if let Some(direction) = [ + Direction::Left, + Direction::Right, + Direction::Up, + Direction::Down, + ] + .into_iter() + .find(|direction| { + self.workspaces + .active() + .layout + .directional_neighbor(window, *direction, bounds) + .ok() + .flatten() + .is_some() + }) { self.join_tab(direction); } return; @@ -343,9 +378,13 @@ impl Ferese { return; }; - match self.workspaces.cycle_column_width(window, &self.column_width_presets) { + match self + .workspaces + .cycle_column_width(window, &self.column_width_presets) + { Ok(true) => { - self.windows.update(window, |w| w.column_width_pending = true); + self.windows + .update(window, |w| w.column_width_pending = true); self.relayout(); } Ok(false) => {} @@ -463,22 +502,26 @@ impl Ferese { let Some(output) = self.output_workspaces.focused_output() else { return; }; - let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else { + let Some(workspace) = self + .output_workspaces + .workspace_at(&self.workspaces, output, index) + else { return; }; let previous = self.output_workspaces.active_workspace(output); - let owner = match self - .output_workspaces - .select_workspace(output, workspace, auto_back_and_forth) - { - Ok(ferese_core::WorkspaceSwitch::Activated(output)) - | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, - Err(error) => { - tracing::error!(%error, index, "failed to assign workspace to output"); - return; - } - }; + let owner = + match self + .output_workspaces + .select_workspace(output, workspace, auto_back_and_forth) + { + Ok(ferese_core::WorkspaceSwitch::Activated(output)) + | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, + Err(error) => { + tracing::error!(%error, index, "failed to assign workspace to output"); + return; + } + }; let workspace = self .output_workspaces .active_workspace(owner) @@ -490,7 +533,12 @@ impl Ferese { self.restore_keyboard_focus(); } - pub(crate) fn preview_workspace_swipe(&mut self, next: bool, direction: SwipeDirection, progress: f64) { + pub(crate) fn preview_workspace_swipe( + &mut self, + next: bool, + direction: SwipeDirection, + progress: f64, + ) { if !self.animations_enabled || self.overview.is_presenting() { return; } @@ -554,7 +602,10 @@ impl Ferese { && self.activate_managed_workspace_from_swipe(to, expected); if !committed { if let Some(slide) = self.workspace_slides.get_mut(&output) { - slide.release_with_velocity(false, self.swipe.release_velocity / self.animation_speed); + slide.release_with_velocity( + false, + self.swipe.release_velocity / self.animation_speed, + ); } self.last_animation_tick = Instant::now(); self.relayout(); @@ -562,7 +613,10 @@ impl Ferese { true } - pub(crate) fn workspace_slide_workspaces(&self, output: OutputId) -> impl Iterator + '_ { + pub(crate) fn workspace_slide_workspaces( + &self, + output: OutputId, + ) -> impl Iterator + '_ { self.workspace_slides .get(&output) .into_iter() @@ -589,9 +643,9 @@ impl Ferese { } if owner == requested_output && let Some(slide) = previous_slide.as_mut() - && slide - .gesture - .is_some_and(|(_, to, direction)| to == workspace && Some(direction) == slide_direction) + && slide.gesture.is_some_and(|(_, to, direction)| { + to == workspace && Some(direction) == slide_direction + }) { slide.release_with_velocity(true, self.swipe.release_velocity / self.animation_speed); self.last_animation_tick = Instant::now(); @@ -640,8 +694,18 @@ impl Ferese { } } - pub(crate) fn move_managed_window_to_workspace(&mut self, window: WindowId, destination: WorkspaceId) -> bool { - if self.session_lock.active() || self.windows.window(window).and_then(|w| w.toplevel()).is_none() { + pub(crate) fn move_managed_window_to_workspace( + &mut self, + window: WindowId, + destination: WorkspaceId, + ) -> bool { + if self.session_lock.active() + || self + .windows + .window(window) + .and_then(|w| w.toplevel()) + .is_none() + { return false; } let Some(source) = self.workspaces.workspace_for_window(window) else { @@ -661,7 +725,10 @@ impl Ferese { let axis = bounds .and_then(|bounds| { let workspace = self.workspaces.workspace(destination)?; - workspace.layout.automatic_axis(workspace.last_focused, bounds).ok() + workspace + .layout + .automatic_axis(workspace.last_focused, bounds) + .ok() }) .unwrap_or(Axis::Horizontal); if self @@ -686,18 +753,27 @@ impl Ferese { let Some(output) = self.output_workspaces.focused_output() else { return; }; - let Some(destination) = self.output_workspaces.workspace_at(&self.workspaces, output, index) else { + let Some(destination) = + self.output_workspaces + .workspace_at(&self.workspaces, output, index) + else { return; }; let axis = self .output_bounds() .and_then(|bounds| { let target = self.workspaces.workspace(destination)?; - target.layout.automatic_axis(target.last_focused, bounds).ok() + target + .layout + .automatic_axis(target.last_focused, bounds) + .ok() }) .unwrap_or(Axis::Horizontal); - if let Err(error) = self.workspaces.move_window_to_workspace(window, destination, axis, 0.5) { + if let Err(error) = self + .workspaces + .move_window_to_workspace(window, destination, axis, 0.5) + { tracing::error!(%error, ?window, index, "failed to move window to workspace"); return; } @@ -725,10 +801,9 @@ impl Ferese { .map(|toplevel| toplevel.wl_surface().clone()) }); - self.seat.get_keyboard().expect("seat has a keyboard").set_focus( - self, - surface, - smithay::utils::SERIAL_COUNTER.next_serial(), - ); + self.seat + .get_keyboard() + .expect("seat has a keyboard") + .set_focus(self, surface, smithay::utils::SERIAL_COUNTER.next_serial()); } } diff --git a/compositor/src/state/outputs.rs b/compositor/src/state/outputs.rs index 23fee87..9a6ed9f 100644 --- a/compositor/src/state/outputs.rs +++ b/compositor/src/state/outputs.rs @@ -18,11 +18,17 @@ impl Ferese { } pub(crate) fn ensure_output_identity(&mut self, identity: &str) -> OutputId { - *self.output_identity_ids.entry(identity.to_owned()).or_insert_with(|| { - let id = OutputId(self.next_output_id); - self.next_output_id = self.next_output_id.checked_add(1).expect("output ID space exhausted"); - id - }) + *self + .output_identity_ids + .entry(identity.to_owned()) + .or_insert_with(|| { + let id = OutputId(self.next_output_id); + self.next_output_id = self + .next_output_id + .checked_add(1) + .expect("output ID space exhausted"); + id + }) } pub fn register_output(&mut self, output: &Output, identity: String) { @@ -86,7 +92,9 @@ impl Ferese { .into_iter() .filter(|entry| entry.output != *output) .collect(); - let changes = self.publish_desktop(runtime).expect("valid surviving output inventory"); + let changes = self + .publish_desktop(runtime) + .expect("valid surviving output inventory"); self.finish_desktop_transition(changes); } } @@ -102,12 +110,15 @@ impl Ferese { .outputs() .find(|candidate| *candidate != output) .map(|output| output.name()); - self.activities.store.remove_output(&output.name(), fallback.as_deref()); + self.activities + .store + .remove_output(&output.name(), fallback.as_deref()); self.activities .pomodoro_remove_output(&output.name(), fallback.as_deref()); self.activities.changed(); self.display_presentation.remove_output(output); - self.output_redraw_pending.retain(|pending| pending != output); + self.output_redraw_pending + .retain(|pending| pending != output); if let Some(id) = self.output_ids.get(output) { self.render.remove_output(*id); self.capture_render.remove_output(*id); @@ -123,7 +134,11 @@ impl Ferese { self.session_lock.output_removed(output); } - pub(super) fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) { + pub(super) fn activate_output_workspace( + &mut self, + output: OutputId, + workspace: ferese_core::WorkspaceId, + ) { if let Err(error) = self.output_workspaces.focus_output(output) { tracing::error!(%error, "failed to focus output"); return; @@ -163,7 +178,9 @@ impl Ferese { let Some(workspace) = self.output_workspaces.active_workspace(output_id) else { return; }; - if self.output_workspaces.focused_output() == Some(output_id) && self.workspaces.active_id() == workspace { + if self.output_workspaces.focused_output() == Some(output_id) + && self.workspaces.active_id() == workspace + { return; } @@ -176,7 +193,9 @@ impl Ferese { mod tests { use super::*; - use smithay::backend::input::{ButtonState, Event, InputBackend, InputEvent, PointerButtonEvent, UnusedEvent}; + use smithay::backend::input::{ + ButtonState, Event, InputBackend, InputEvent, PointerButtonEvent, UnusedEvent, + }; use smithay::backend::winit::WinitVirtualDevice; use smithay::input::pointer::MotionEvent; use smithay::utils::SERIAL_COUNTER; @@ -316,7 +335,10 @@ mod tests { card.rect.x + card.rect.width / 2.0, card.rect.y + card.rect.height / 2.0, )); - assert_eq!(state.overview_strip_at(position), Some(expected_output.clone())); + assert_eq!( + state.overview_strip_at(position), + Some(expected_output.clone()) + ); click(&mut state, position); assert_eq!(state.workspaces.active_id(), card.workspace); state.windows.records.insert( @@ -332,7 +354,10 @@ mod tests { .find(|card| card.workspace == expected_workspace) .unwrap(); let preview = occupied.windows[0].1; - let image_point = Point::from((preview.x + preview.width / 2.0, preview.y + preview.height / 2.0)); + let image_point = Point::from(( + preview.x + preview.width / 2.0, + preview.y + preview.height / 2.0, + )); let label_point = Point::from(( occupied.rect.x + occupied.rect.width / 2.0, occupied.rect.y + occupied.rect.height - 8.0, diff --git a/compositor/src/state/panel_clearance_tests.rs b/compositor/src/state/panel_clearance_tests.rs index 2102035..a5c6d89 100644 --- a/compositor/src/state/panel_clearance_tests.rs +++ b/compositor/src/state/panel_clearance_tests.rs @@ -100,14 +100,19 @@ fn every_panel_style_owns_clearance_at_its_reserved_edge() { request(&mut wire, 3, 2, &[panel.exclusive_zone(false) as u32]); request(&mut wire, 3, 3, &[geometry.edge_margin as u32, 0, 0, 0]); request(&mut wire, 2, 6, &[]); - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); let bounds = state.output_bounds_for(&output).unwrap(); let edge = f64::from(geometry.edge_margin + 36 + clearance); assert_eq!(bounds.y, edge, "{style:?}, clearance={clearance}"); for outer in [0., 12.] { let adjusted = state.panel_layout_bounds(bounds, outer); let window = maximized_rect(adjusted, outer); - assert_eq!(window.y, edge, "{style:?}, clearance={clearance}, outer={outer}"); + assert_eq!( + window.y, edge, + "{style:?}, clearance={clearance}, outer={outer}" + ); assert_eq!(window.x, bounds.x + outer); assert_eq!(window.y + window.height, bounds.y + bounds.height - outer); } @@ -115,7 +120,9 @@ fn every_panel_style_owns_clearance_at_its_reserved_edge() { } request(&mut wire, 3, 2, &[0]); request(&mut wire, 2, 6, &[]); - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); let bounds = state.output_bounds_for(&output).unwrap(); assert_eq!( state.panel_layout_bounds(bounds, 12.), diff --git a/compositor/src/state/prediction.rs b/compositor/src/state/prediction.rs index 48e275c..2361a0a 100644 --- a/compositor/src/state/prediction.rs +++ b/compositor/src/state/prediction.rs @@ -16,7 +16,10 @@ pub(crate) struct WindowFrame { } impl Ferese { - pub(crate) fn current_window_presentation(&self, id: WindowId) -> Option { + pub(crate) fn current_window_presentation( + &self, + id: WindowId, + ) -> Option { let geometry = *self.windows.geometry(&id)?; Some(self.compose_window_presentation( id, @@ -27,7 +30,12 @@ impl Ferese { )) } - fn window_focus_effect(&self, id: WindowId, delta: Duration, overview_opacity: f32) -> (f32, f64) { + fn window_focus_effect( + &self, + id: WindowId, + delta: Duration, + overview_opacity: f32, + ) -> (f32, f64) { let target = crate::focus_effect::target(self.focus_effect, self.focused_window, id); let mut focus = self .windows @@ -43,8 +51,11 @@ impl Ferese { 0.0 }, ); - self.focus_effect - .resolve(focus.current, self.focused_window.is_some(), overview_opacity) + self.focus_effect.resolve( + focus.current, + self.focused_window.is_some(), + overview_opacity, + ) } fn compose_window_presentation( @@ -55,9 +66,15 @@ impl Ferese { offset: (f64, f64), delta: Duration, ) -> crate::presentation::WindowPresentation { - let record = self.windows.record(id).expect("presentation has a window record"); + let record = self + .windows + .record(id) + .expect("presentation has a window record"); let mut visual = geometry.visual; - if self.presentation_dependencies.reflow_blocked(id, &self.windows) { + if self + .presentation_dependencies + .reflow_blocked(id, &self.windows) + { let predicted_x_velocity = visual.velocity.x; visual.velocity = Default::default(); if !geometry.is_zooming() @@ -115,12 +132,18 @@ impl Ferese { ..self.spring_config }, ); - emphasis.advance(delta, crate::presentation::emphasis_spring(self.spring_config)); - shadow.advance(delta, crate::presentation::shadow_spring(self.spring_config)); + emphasis.advance( + delta, + crate::presentation::emphasis_spring(self.spring_config), + ); + shadow.advance( + delta, + crate::presentation::shadow_spring(self.spring_config), + ); } let presence_scale = 0.97 + 0.03 * opacity.current; - let native_size = - (!overview.is_presenting() && presence_scale != 1.0).then(|| ClientSize::from_rect(bounds.current)); + let native_size = (!overview.is_presenting() && presence_scale != 1.0) + .then(|| ClientSize::from_rect(bounds.current)); bounds.velocity = crate::presentation::scaled_visual_velocity( bounds.current, bounds.velocity, @@ -161,7 +184,10 @@ impl Ferese { Some(( id, self.windows.record(id)?, - block_resizes && self.presentation_dependencies.reflow_blocked(id, &self.windows), + block_resizes + && self + .presentation_dependencies + .reflow_blocked(id, &self.windows), )) }) }) @@ -173,20 +199,21 @@ impl Ferese { } else { self.focused_window }; - self.output_window_records(output, false).any(|(id, record, _)| { - self.window_belongs_to_output(id, output) - && (record - .focus - .as_ref() - .is_some_and(|focus| focus.needs_update(if selected == Some(id) { 1.0 } else { 0.0 })) - || record - .shadow - .as_ref() - .is_some_and(|shadow| shadow.needs_update(if selected == Some(id) { 1.0 } else { 0.0 })) - || record.focus_transition.as_ref().is_some_and(|transition| { - transition.needs_update(crate::focus_effect::target(self.focus_effect, self.focused_window, id)) + self.output_window_records(output, false) + .any(|(id, record, _)| { + self.window_belongs_to_output(id, output) + && (record.focus.as_ref().is_some_and(|focus| { + focus.needs_update(if selected == Some(id) { 1.0 } else { 0.0 }) + }) || record.shadow.as_ref().is_some_and(|shadow| { + shadow.needs_update(if selected == Some(id) { 1.0 } else { 0.0 }) + }) || record.focus_transition.as_ref().is_some_and(|transition| { + transition.needs_update(crate::focus_effect::target( + self.focus_effect, + self.focused_window, + id, + )) })) - }) + }) } pub(crate) fn output_resize_deadline(&self, output: &Output) -> Option { @@ -212,7 +239,8 @@ impl Ferese { } fn snapshot_is_waiting(&self, id: WindowId, geometry: &WindowGeometry) -> bool { - self.presentation_dependencies.reflow_blocked(id, &self.windows) + self.presentation_dependencies + .reflow_blocked(id, &self.windows) || geometry.client.committed_size.is_some_and(|size| { crate::presentation::resize_needs_old_frame( geometry.visual.current, @@ -247,48 +275,58 @@ impl Ferese { return true; } - self.output_window_records(output, true).any(|(id, record, blocked)| { - let Some(mut geometry) = record.geometry else { - return false; - }; + self.output_window_records(output, true) + .any(|(id, record, blocked)| { + let Some(mut geometry) = record.geometry else { + return false; + }; - if !self.window_belongs_to_output(id, output) { - return false; - } + if !self.window_belongs_to_output(id, output) { + return false; + } - if record.opening.is_some() - || (self.render.snapshot(&id).is_some() && !self.snapshot_is_waiting(id, &geometry)) - || record.focus.as_ref().is_some_and(|focus| focus.is_animating()) - || record.shadow.as_ref().is_some_and(|shadow| shadow.is_animating()) - || record - .focus_transition - .as_ref() - .is_some_and(|transition| transition.is_animating()) - { - return true; - } + if record.opening.is_some() + || (self.render.snapshot(&id).is_some() + && !self.snapshot_is_waiting(id, &geometry)) + || record + .focus + .as_ref() + .is_some_and(|focus| focus.is_animating()) + || record + .shadow + .as_ref() + .is_some_and(|shadow| shadow.is_animating()) + || record + .focus_transition + .as_ref() + .is_some_and(|transition| transition.is_animating()) + { + return true; + } - if !blocked && geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) { - return true; - } + if !blocked + && geometry.advance(Duration::ZERO, self.spring_config, self.animations_enabled) + { + return true; + } - let Some(workspace) = self.workspaces.workspace_for_window(id) else { - return false; - }; + let Some(workspace) = self.workspaces.workspace_for_window(id) else { + return false; + }; - (!self - .presentation_dependencies - .viewport_blocked(workspace, &self.windows) - && self - .viewports - .get(&workspace) - .is_some_and(ViewportPresentation::needs_tick)) - || (!blocked - && record.coupled_width.as_ref().is_some_and(|(_, width)| { - let mut width = *width; - width.advance(Duration::ZERO, self.viewport_spring_config) - })) - }) + (!self + .presentation_dependencies + .viewport_blocked(workspace, &self.windows) + && self + .viewports + .get(&workspace) + .is_some_and(ViewportPresentation::needs_tick)) + || (!blocked + && record.coupled_width.as_ref().is_some_and(|(_, width)| { + let mut width = *width; + width.advance(Duration::ZERO, self.viewport_spring_config) + })) + }) } pub(crate) fn sample_frame(&self, output: &Output, horizon: Duration) -> FrameScene { @@ -300,15 +338,17 @@ impl Ferese { }; let overview = self.overview.sample(delta, self.spring_config); - let predicted_slide_offsets = - (!delta.is_zero()).then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta)); + let predicted_slide_offsets = (!delta.is_zero()) + .then(|| self.sample_workspace_slide_offsets(self.output_id(output), delta)); let slide_offsets = predicted_slide_offsets .as_ref() .unwrap_or(&self.workspace_slide_offsets); let mut windows = HashMap::new(); for (id, record, blocked) in self.output_window_records(output, !delta.is_zero()) { - let Some(original) = record.geometry else { continue }; + let Some(original) = record.geometry else { + continue; + }; // The workspace strip also draws inactive workspaces on this output. // Keep their samples through the overview exit transition; the main @@ -350,8 +390,15 @@ impl Ferese { .workspace_for_window(id) .and_then(|workspace| slide_offsets.get(&workspace).copied()) .unwrap_or_default(); - let presentation = self.compose_window_presentation(id, geometry, &overview, offset, delta); - windows.insert(id, WindowFrame { geometry, presentation }); + let presentation = + self.compose_window_presentation(id, geometry, &overview, offset, delta); + windows.insert( + id, + WindowFrame { + geometry, + presentation, + }, + ); } FrameScene { @@ -446,7 +493,10 @@ mod tests { state.space.map_output(&output, (0, 0)); state.register_output(&output, "resize-idle".into()); let id = WindowId(1); - state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); + state + .workspaces + .insert_window(id, Axis::Horizontal, 0.5) + .unwrap(); let settled = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); state.windows.records.insert( id, @@ -460,7 +510,10 @@ mod tests { crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO), ); state.rebuild_presentation_dependencies(); - assert_eq!(state.output_resize_deadline(&output), Some(Duration::from_millis(300))); + assert_eq!( + state.output_resize_deadline(&output), + Some(Duration::from_millis(300)) + ); let mut held = settled; held.visual.set_target(Rect::new(100., 0., 400., 300.)); state.windows.set_geometry(id, held); @@ -500,13 +553,18 @@ mod tests { state.release_presentation_dependencies(); assert_eq!(state.output_resize_deadline(&output), None); assert!(state.output_has_animations(&output)); - state.windows.geometry_mut(&id).unwrap().client.request_size( - ClientSize { - width: 800, - height: 300, - }, - Duration::ZERO, - ); + state + .windows + .geometry_mut(&id) + .unwrap() + .client + .request_size( + ClientSize { + width: 800, + height: 300, + }, + Duration::ZERO, + ); assert_eq!( state.output_resize_deadline(&output), None, @@ -594,14 +652,26 @@ mod tests { let first = WindowId(1); let second = WindowId(2); state.focused_window = Some(first); - assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9); - assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6); + assert_eq!( + state.current_window_presentation(first).unwrap().alpha(), + 0.9 + ); + assert_eq!( + state.current_window_presentation(second).unwrap().alpha(), + 0.6 + ); // Only emphasis follows overview selection; activation and its fade remain unchanged. state.set_overview_active(true); state.overview.select_window(second); assert_eq!(state.focused_window, Some(first)); - assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.9); - assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.6); + assert_eq!( + state.current_window_presentation(first).unwrap().alpha(), + 0.9 + ); + assert_eq!( + state.current_window_presentation(second).unwrap().alpha(), + 0.6 + ); state.set_overview_active(false); state.focused_window = Some(second); let transition = state @@ -640,8 +710,14 @@ mod tests { ); assert!(state.windows.transaction(&first).is_some()); state.animations_enabled = false; - assert_eq!(state.current_window_presentation(first).unwrap().alpha(), 0.6); - assert_eq!(state.current_window_presentation(second).unwrap().alpha(), 0.9); + assert_eq!( + state.current_window_presentation(first).unwrap().alpha(), + 0.6 + ); + assert_eq!( + state.current_window_presentation(second).unwrap().alpha(), + 0.9 + ); } #[test] @@ -680,7 +756,10 @@ mod tests { let output_id = state.output_id(&output).unwrap(); let from = state.output_workspaces.active_workspace(output_id).unwrap(); let to = state.workspaces.create_workspace(); - state.output_workspaces.assign_workspace(output_id, to).unwrap(); + state + .output_workspaces + .assign_workspace(output_id, to) + .unwrap(); for (id, workspace) in [(WindowId(1), from), (WindowId(2), to)] { let normal = Rect::new(100., 160., 640., 480.); state @@ -697,7 +776,11 @@ mod tests { } state.set_overview_active(true); state.advance_animations_by(Duration::from_secs(10)); - let old = state.current_window_presentation(WindowId(1)).unwrap().bounds.current; + let old = state + .current_window_presentation(WindowId(1)) + .unwrap() + .bounds + .current; let cards = state.overview_workspace_cards(&output); let card = cards.iter().find(|card| card.workspace == to).unwrap(); assert!( @@ -711,15 +794,37 @@ mod tests { ); assert!(state.workspace_slides.contains_key(&output_id)); assert!(state.window_belongs_to_output(WindowId(1), &output)); - let incoming = state.current_window_presentation(WindowId(2)).unwrap().bounds.current; + let incoming = state + .current_window_presentation(WindowId(2)) + .unwrap() + .bounds + .current; assert_eq!(incoming.width, old.width); assert_eq!(incoming.height, old.height); assert!(incoming.x > old.x + 1000.); let predicted = state.sample_frame(&output, Duration::from_millis(60)); - assert!(predicted.windows[&WindowId(1)].presentation.bounds.current.x < old.x); - assert!(predicted.windows[&WindowId(2)].presentation.bounds.current.x < incoming.x); + assert!( + predicted.windows[&WindowId(1)] + .presentation + .bounds + .current + .x + < old.x + ); + assert!( + predicted.windows[&WindowId(2)] + .presentation + .bounds + .current + .x + < incoming.x + ); assert_eq!( - predicted.windows[&WindowId(2)].presentation.bounds.current.width, + predicted.windows[&WindowId(2)] + .presentation + .bounds + .current + .width, incoming.width ); // Activation can add a trailing empty workspace. Once that layout is @@ -745,7 +850,10 @@ mod tests { assert_eq!(item.velocity, after.velocity); } // Finish on the selected workspace and retire all outgoing previews. - state.output_workspaces.switch_workspace(output_id, from).unwrap(); + state + .output_workspaces + .switch_workspace(output_id, from) + .unwrap(); state.retarget_overview(); state.advance_animations_by(Duration::from_secs(10)); assert!(state.workspace_slides.is_empty()); @@ -802,7 +910,10 @@ mod tests { state.space.map_output(&output, (0, 0)); state.register_output(&output, "forecast-test".into()); let id = WindowId(1); - state.workspaces.insert_window(id, Axis::Horizontal, 0.5).unwrap(); + state + .workspaces + .insert_window(id, Axis::Horizontal, 0.5) + .unwrap(); let mut geometry = WindowGeometry::new(Rect::new(0., 0., 400., 300.), None); geometry.visual.set_target(Rect::new(600., 0., 400., 300.)); // This test isolates frame sampling from the protocol client fixture. @@ -821,7 +932,9 @@ mod tests { ] { { let forecast = state.sample_frame(&output, horizon); - assert!(forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x); + assert!( + forecast.windows[&id].geometry.visual.current.x > geometry.visual.current.x + ); assert!(forecast.animating); } @@ -898,7 +1011,10 @@ mod tests { let output_id = state.output_id(&output).unwrap(); let from = state.workspaces.workspace_for_window(id).unwrap(); let to = state.workspaces.create_workspace(); - state.output_workspaces.assign_workspace(output_id, to).unwrap(); + state + .output_workspaces + .assign_workspace(output_id, to) + .unwrap(); let mut slide = WorkspaceSlide::new(None, from, to, SwipeDirection::Left); slide.held_progress = Some(0.25); state.workspace_slides.insert(output_id, slide); @@ -907,12 +1023,16 @@ mod tests { assert_eq!(offsets[&from].0, -480.0); assert_eq!(offsets[&to].0, 1440.0); assert_eq!( - state.sample_frame(&output, Duration::from_millis(8)).windows[&id] + state + .sample_frame(&output, Duration::from_millis(8)) + .windows[&id] .presentation .bounds .current .x, - state.sample_frame(&output, Duration::from_millis(8)).windows[&id] + state + .sample_frame(&output, Duration::from_millis(8)) + .windows[&id] .geometry .visual .current @@ -970,7 +1090,10 @@ mod tests { ); let inactive = state.workspaces.create_workspace(); - state.output_workspaces.assign_workspace(output_id, inactive).unwrap(); + state + .output_workspaces + .assign_workspace(output_id, inactive) + .unwrap(); let preview_id = WindowId(3); state .workspaces @@ -1061,7 +1184,8 @@ mod tests { width.set_target(500.); let spring = SpringConfig::default(); let delta = Duration::from_millis(10); - let sampled_viewport = ViewportPresentation::from_motion(viewport).sample(delta, spring, true, false); + let sampled_viewport = + ViewportPresentation::from_motion(viewport).sample(delta, spring, true, false); let predicted = predict_geometry( geometry, delta, diff --git a/compositor/src/state/presentation_dependencies.rs b/compositor/src/state/presentation_dependencies.rs index 114ed0c..2c1d140 100644 --- a/compositor/src/state/presentation_dependencies.rs +++ b/compositor/src/state/presentation_dependencies.rs @@ -42,27 +42,31 @@ fn tree_reflow_groups( allow_overshoot: bool, ) -> HashMap { let margin = inner_gap.max(0.0) / 2.0; - let axis_bounds = - |position: f64, size: f64, target_position: f64, target_size: f64, speed: f64, size_speed: f64| { - let distance = target_position - position; - let size_distance = target_size - size; - let moving = distance != 0.0 || size_distance != 0.0 || speed != 0.0 || size_speed != 0.0; - let reversing = - (speed != 0.0 && distance * speed <= 0.0) || (size_speed != 0.0 && size_distance * size_speed <= 0.0); - // Endpoint bounds cannot contain overshoot or motion away from the - // target. Widen only that axis, preserving separation on a fixed axis. - if reversing || (allow_overshoot && moving) { - return (f64::NEG_INFINITY, f64::INFINITY); - } - // Proportional component velocities preserve a shared edge. Otherwise - // bound position and size separately to include intermediate excursions. - let end = if (speed * size_distance - size_speed * distance).abs() < 1e-6 { - (position + size).max(target_position + target_size) - } else { - position.max(target_position) + size.max(target_size) - }; - (position.min(target_position) - margin, end + margin) + let axis_bounds = |position: f64, + size: f64, + target_position: f64, + target_size: f64, + speed: f64, + size_speed: f64| { + let distance = target_position - position; + let size_distance = target_size - size; + let moving = distance != 0.0 || size_distance != 0.0 || speed != 0.0 || size_speed != 0.0; + let reversing = (speed != 0.0 && distance * speed <= 0.0) + || (size_speed != 0.0 && size_distance * size_speed <= 0.0); + // Endpoint bounds cannot contain overshoot or motion away from the + // target. Widen only that axis, preserving separation on a fixed axis. + if reversing || (allow_overshoot && moving) { + return (f64::NEG_INFINITY, f64::INFINITY); + } + // Proportional component velocities preserve a shared edge. Otherwise + // bound position and size separately to include intermediate excursions. + let end = if (speed * size_distance - size_speed * distance).abs() < 1e-6 { + (position + size).max(target_position + target_size) + } else { + position.max(target_position) + size.max(target_size) }; + (position.min(target_position) - margin, end + margin) + }; let moving = motions .into_iter() .filter(|(_, source, target, velocity)| { @@ -131,7 +135,11 @@ impl PresentationDependencies { self.viewport.get(&workspace).cloned() } - pub(super) fn restore_viewport(&mut self, workspace: WorkspaceId, saved: Option) { + pub(super) fn restore_viewport( + &mut self, + workspace: WorkspaceId, + saved: Option, + ) { if let Some(saved) = saved { self.viewport.insert(workspace, saved); } else { @@ -140,7 +148,12 @@ impl PresentationDependencies { } #[cfg(test)] - pub(super) fn wait_for_viewport(&mut self, workspace: WorkspaceId, id: WindowId, serial: Serial) { + pub(super) fn wait_for_viewport( + &mut self, + workspace: WorkspaceId, + id: WindowId, + serial: Serial, + ) { self.viewport.insert( workspace, ViewportDependencies { @@ -202,7 +215,8 @@ impl PresentationDependencies { self.viewport.retain(|_, waits| { waits.waits.retain(|wait| Self::pending(*wait, windows)); for step in &mut waits.steps { - step.inputs.retain(|input| Self::pending(input.commit, windows)); + step.inputs + .retain(|input| Self::pending(input.commit, windows)); } if waits.steps.iter().all(|step| step.inputs.is_empty()) { waits.steps.clear(); @@ -222,9 +236,9 @@ fn viewport_dependencies( return ViewportDependencies::default(); }; let live = |input: &ViewportCommit| { - pending - .iter() - .any(|(window, transaction)| *window == input.commit.window && transaction.serial() == input.commit.serial) + pending.iter().any(|(window, transaction)| { + *window == input.commit.window && transaction.serial() == input.commit.serial + }) }; for step in &mut previous.steps { for input in &mut step.inputs { @@ -238,7 +252,11 @@ fn viewport_dependencies( } // Fold any prefix with no current configure inputs to the numeric start // already stored in the next request. No obsolete serial owns a recipe. - if let Some(first) = previous.steps.iter().position(|step| !step.inputs.is_empty()) { + if let Some(first) = previous + .steps + .iter() + .position(|step| !step.inputs.is_empty()) + { previous.steps.drain(..first); } else { previous.steps.clear(); @@ -257,7 +275,11 @@ fn viewport_dependencies( }) }) .collect::>(); - if previous.steps.last().is_none_or(|step| step.target != *target) { + if previous + .steps + .last() + .is_none_or(|step| step.target != *target) + { if !target.uses_previous_target() { previous.steps.clear(); previous.overflow = false; @@ -283,7 +305,9 @@ fn viewport_dependencies( owners.extend(inputs.iter().map(|input| input.commit)); owners.sort_by_key(|owner| owner.window.0); owners.dedup(); - let evaluate = |only: Option, exclude: Option, retained: Option<&HashSet>| { + let evaluate = |only: Option, + exclude: Option, + retained: Option<&HashSet>| { let mut start = None; for step in &previous.steps { let widths = step @@ -316,7 +340,10 @@ fn viewport_dependencies( // Retain a collective wait in that case, removing inputs that are not // needed to keep the held target different from the requested target. if previous.waits.is_empty() && (held - layout.viewport_x()).abs() > 0.001 { - let mut collective = owners.iter().map(|owner| owner.window).collect::>(); + let mut collective = owners + .iter() + .map(|owner| owner.window) + .collect::>(); for owner in &owners { collective.remove(&owner.window); if (evaluate(None, None, Some(&collective)) - layout.viewport_x()).abs() <= 0.001 { @@ -342,7 +369,9 @@ impl Ferese { pub(super) fn release_presentation_dependencies(&mut self) { let paused_reflow = self.presentation_dependencies.paused_reflow.clone(); self.rebuild_presentation_dependencies_with_release(true); - self.presentation_dependencies.paused_reflow.extend(paused_reflow); + self.presentation_dependencies + .paused_reflow + .extend(paused_reflow); } pub(super) fn rebuild_presentation_dependencies(&mut self) { @@ -379,7 +408,8 @@ impl Ferese { tree_reflow_groups( motions, self.gap_config.inner, - self.spring_config.crossing == ferese_animation::CrossingPolicy::AllowOvershoot, + self.spring_config.crossing + == ferese_animation::CrossingPolicy::AllowOvershoot, ), )) }) @@ -392,9 +422,10 @@ impl Ferese { continue; }; let own_column = match &workspace.layout { - WorkspaceLayout::Scrolling(layout) => { - layout.columns().iter().position(|column| column.windows.contains(&id)) - } + WorkspaceLayout::Scrolling(layout) => layout + .columns() + .iter() + .position(|column| column.windows.contains(&id)), WorkspaceLayout::Tree(_) => None, }; let mut waits = Vec::new(); @@ -419,12 +450,17 @@ impl Ferese { .iter() .position(|column| column.windows.contains(slow)) .is_some_and(|source| { - source == own || (source < own && transaction.width_changes()) + source == own + || (source < own && transaction.width_changes()) }) }), - WorkspaceLayout::Tree(_) => tree_groups.get(&workspace_id).is_some_and(|groups| { - groups.get(&id).is_some_and(|group| groups.get(slow) == Some(group)) - }), + WorkspaceLayout::Tree(_) => { + tree_groups.get(&workspace_id).is_some_and(|groups| { + groups + .get(&id) + .is_some_and(|group| groups.get(slow) == Some(group)) + }) + } }); if linked { waits.push(commit); @@ -450,7 +486,8 @@ impl Ferese { .windows .resizing() .filter(|id| { - self.workspaces.workspace_for_window(**id) == Some(workspace.id) && layout.contains(**id) + self.workspaces.workspace_for_window(**id) == Some(workspace.id) + && layout.contains(**id) }) .map(|id| (*id, *self.windows.transaction(id).unwrap())) .collect::>(), @@ -554,7 +591,10 @@ mod tests { ); assert_eq!(groups[&WindowId(1)], groups[&WindowId(2)]); let groups = tree_reflow_groups( - [(WindowId(1), source, target), (WindowId(2), neighbor, moving_neighbor)], + [ + (WindowId(1), source, target), + (WindowId(2), neighbor, moving_neighbor), + ], 0.0, ); assert_ne!( @@ -566,7 +606,8 @@ mod tests { fn nested_tree() -> ferese_layout::LayoutTree { let mut tree = ferese_layout::LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Vertical, 0.5) @@ -586,15 +627,18 @@ mod tests { smart: false, }; let mut tree = nested_tree(); - let solve = |tree: &ferese_layout::LayoutTree, constraints: &HashMap| { + let solve = |tree: &ferese_layout::LayoutTree, + constraints: &HashMap| { tree.geometry_with_constraints(bounds, gaps, constraints, Some(WindowId(1))) .unwrap() .geometry }; let constraints = HashMap::new(); let source = solve(&tree, &constraints); - tree.resize_window(WindowId(1), Direction::Down, 0.1).unwrap(); - tree.resize_window(WindowId(3), Direction::Down, 0.1).unwrap(); + tree.resize_window(WindowId(1), Direction::Down, 0.1) + .unwrap(); + tree.resize_window(WindowId(3), Direction::Down, 0.1) + .unwrap(); let target = solve(&tree, &constraints); let group = |source: &HashMap, target: &HashMap| { tree_reflow_groups( @@ -611,7 +655,8 @@ mod tests { // A horizontal request walks through the vertical child to the // root split: both branches' actual x/width now participate. - tree.resize_window(WindowId(1), Direction::Right, 0.1).unwrap(); + tree.resize_window(WindowId(1), Direction::Right, 0.1) + .unwrap(); let ancestor = solve(&tree, &constraints); let groups = group(&source, &ancestor); assert!(groups.values().all(|group| *group == groups[&WindowId(1)])); @@ -649,7 +694,9 @@ mod tests { )]); let clipped_source = solve(&tree, &maximum); let mut wider = tree.clone(); - wider.resize_window(WindowId(1), Direction::Right, 0.1).unwrap(); + wider + .resize_window(WindowId(1), Direction::Right, 0.1) + .unwrap(); let clipped_target = solve(&wider, &maximum); assert_eq!(clipped_source[&WindowId(1)], clipped_target[&WindowId(1)]); assert!(!group(&clipped_source, &clipped_target).contains_key(&WindowId(1))); @@ -677,7 +724,9 @@ mod tests { source[3], ]; let groups = tree_reflow_groups( - ids.into_iter().enumerate().map(|(i, id)| (id, source[i], target[i])), + ids.into_iter() + .enumerate() + .map(|(i, id)| (id, source[i], target[i])), 0.0, ); assert_eq!( @@ -689,7 +738,8 @@ mod tests { let mut tree = nested_tree(); tree.stack_window(WindowId(2), WindowId(1)).unwrap(); let before = tree.geometry(Rect::new(0.0, 0.0, 800.0, 600.0)).unwrap(); - tree.resize_window(WindowId(2), Direction::Right, 0.1).unwrap(); + tree.resize_window(WindowId(2), Direction::Right, 0.1) + .unwrap(); let after = tree.geometry(Rect::new(0.0, 0.0, 800.0, 600.0)).unwrap(); let groups = tree_reflow_groups( before @@ -721,14 +771,19 @@ mod tests { layout.set_focus_strategy(ViewportFocusStrategy::Center); layout.insert(a, None).unwrap(); layout.insert(b, Some(a)).unwrap(); - layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap(); - layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap(); + layout + .set_column_width(a, ColumnWidth::Fixed(150.0)) + .unwrap(); + layout + .set_column_width(b, ColumnWidth::Fixed(300.0)) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b)) .unwrap(); let pending = [( a, - ResizeTransaction::new(10.into(), Duration::ZERO).with_column_width(Some(100.0), Some(150.0)), + ResizeTransaction::new(10.into(), Duration::ZERO) + .with_column_width(Some(100.0), Some(150.0)), )]; let first = viewport_dependencies(&layout, &pending, Default::default()); assert_eq!(first.waits.len(), 1); @@ -753,11 +808,13 @@ mod tests { let pending = [ ( a, - ResizeTransaction::new(10.into(), Duration::ZERO).with_column_width(Some(100.0), Some(40.0)), + ResizeTransaction::new(10.into(), Duration::ZERO) + .with_column_width(Some(100.0), Some(40.0)), ), ( b, - ResizeTransaction::new(20.into(), Duration::ZERO).with_column_width(Some(100.0), Some(40.0)), + ResizeTransaction::new(20.into(), Duration::ZERO) + .with_column_width(Some(100.0), Some(40.0)), ), ]; assert!(!layout.viewport_depends_on_width(a, 100.0, 40.0)); @@ -765,19 +822,31 @@ mod tests { let both = viewport_dependencies(&layout, &pending, Default::default()); assert_eq!(both.waits.len(), 2, "joint held widths select another page"); let partial = viewport_dependencies(&layout, &pending[1..], both); - assert!(partial.waits.is_empty(), "one remaining held width fits this page"); - assert!(viewport_dependencies(&layout, &[], partial).steps.is_empty()); + assert!( + partial.waits.is_empty(), + "one remaining held width fits this page" + ); + assert!( + viewport_dependencies(&layout, &[], partial) + .steps + .is_empty() + ); // Independent absolute requests reset history. Relative request storage // is capped even if a client keeps replacing its configure. layout.set_focus_strategy(ViewportFocusStrategy::Center); - layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap(); - layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap(); + layout + .set_column_width(a, ColumnWidth::Fixed(150.0)) + .unwrap(); + layout + .set_column_width(b, ColumnWidth::Fixed(300.0)) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b)) .unwrap(); let pending = [( a, - ResizeTransaction::new(30.into(), Duration::ZERO).with_column_width(Some(100.0), Some(150.0)), + ResizeTransaction::new(30.into(), Duration::ZERO) + .with_column_width(Some(100.0), Some(150.0)), )]; let mut graph = viewport_dependencies(&layout, &pending, Default::default()); for index in 0..80 { @@ -790,7 +859,10 @@ mod tests { assert!(graph.steps.len() <= MAX_VIEWPORT_STEPS); } assert!(graph.overflow); - assert!(!graph.waits.is_empty(), "bounded fallback must not free linked input"); + assert!( + !graph.waits.is_empty(), + "bounded fallback must not free linked input" + ); layout.center_window(a, bounds, gaps, &constraints).unwrap(); graph = viewport_dependencies(&layout, &pending, graph); assert_eq!(graph.steps.len(), 1); @@ -807,7 +879,10 @@ mod tests { windows.register("b", b); windows.register("neighbor", neighbor); windows.set_transaction(a, ResizeTransaction::new(10.into(), Duration::ZERO)); - windows.set_transaction(b, ResizeTransaction::new(20.into(), Duration::from_millis(100))); + windows.set_transaction( + b, + ResizeTransaction::new(20.into(), Duration::from_millis(100)), + ); let commit = |window, serial| ClientCommit { window, serial: Serial::from(serial), @@ -815,24 +890,34 @@ mod tests { let workspace = WorkspaceId(1); let mut dependencies = PresentationDependencies::default(); dependencies.reflow.insert(a, vec![commit(a, 10)]); - dependencies.reflow.insert(neighbor, vec![commit(a, 10), commit(b, 20)]); + dependencies + .reflow + .insert(neighbor, vec![commit(a, 10), commit(b, 20)]); dependencies.wait_for_viewport(workspace, b, 20.into()); assert!(dependencies.reflow_blocked(neighbor, &windows)); assert!(!dependencies.reflow_blocked(b, &windows)); assert!(dependencies.viewport_blocked(workspace, &windows)); dependencies.holds(&windows); // A replacement cannot be released by the older committed serial. - windows.set_transaction(b, ResizeTransaction::new(21.into(), Duration::from_millis(150))); + windows.set_transaction( + b, + ResizeTransaction::new(21.into(), Duration::from_millis(150)), + ); assert!(!windows.transaction(&b).unwrap().accepts(Some(20.into()))); assert!(!dependencies.viewport_blocked(workspace, &windows)); dependencies.wait_for_viewport(workspace, b, 21.into()); - dependencies.reflow.insert(neighbor, vec![commit(a, 10), commit(b, 21)]); + dependencies + .reflow + .insert(neighbor, vec![commit(a, 10), commit(b, 21)]); windows.expire_transactions(Duration::from_millis(300)); assert!(windows.transaction(&a).is_none()); assert!(windows.transaction(&b).is_some()); assert!(dependencies.reflow_blocked(neighbor, &windows)); let held = dependencies.holds(&windows); - assert!(held.contains(&a), "release tick must not charge waiting time"); + assert!( + held.contains(&a), + "release tick must not charge waiting time" + ); let held = dependencies.holds(&windows); assert!(!held.contains(&a)); // Unmap removes the serial owner; both properties retire on the tick. @@ -864,11 +949,15 @@ mod tests { WindowId(6), ]; for (i, id) in ids.iter().enumerate() { - layout.insert(*id, i.checked_sub(1).map(|j| ids[j])).unwrap(); + layout + .insert(*id, i.checked_sub(1).map(|j| ids[j])) + .unwrap(); } for (id, width) in ids.into_iter().zip([40.0, 40.0, 80.0, 80.0, 40.0, 40.0]) { - layout.set_column_width(id, ColumnWidth::Fixed(width)).unwrap(); + layout + .set_column_width(id, ColumnWidth::Fixed(width)) + .unwrap(); } layout.focus(ids[4]).unwrap(); @@ -893,7 +982,13 @@ mod tests { ) }) .collect::>(); - assert_eq!(layout.viewport_target().unwrap().with_held_widths(&widths, None), 320.0); + assert_eq!( + layout + .viewport_target() + .unwrap() + .with_held_widths(&widths, None), + 320.0 + ); let graph = viewport_dependencies(&layout, &pending, Default::default()); assert!( !graph.waits.is_empty(), @@ -925,7 +1020,10 @@ mod tests { (*id, source, target) }) .collect::>(); - let held = layout.viewport_target().unwrap().with_held_widths(&held_widths, None); + let held = layout + .viewport_target() + .unwrap() + .with_held_widths(&held_widths, None); let partial = viewport_dependencies(&layout, &remaining, graph.clone()); if (held - layout.viewport_x()).abs() > 0.001 { assert!( @@ -958,9 +1056,23 @@ mod tests { viewport.retarget(layout.viewport_x(), true); viewport.reconcile_hold(true, false); viewport.reconcile_hold(blocked, true); - viewport.advance(Duration::from_secs(3), SpringConfig::default(), true, blocked); - assert_eq!(viewport.motion().current, 0.0, "release consumed waiting time: {mask}"); - viewport.advance(Duration::from_millis(16), SpringConfig::default(), true, blocked); + viewport.advance( + Duration::from_secs(3), + SpringConfig::default(), + true, + blocked, + ); + assert_eq!( + viewport.motion().current, + 0.0, + "release consumed waiting time: {mask}" + ); + viewport.advance( + Duration::from_millis(16), + SpringConfig::default(), + true, + blocked, + ); assert_eq!( viewport.motion().current == 0.0, blocked, diff --git a/compositor/src/state/reconciliation.rs b/compositor/src/state/reconciliation.rs index abda2da..0db4777 100644 --- a/compositor/src/state/reconciliation.rs +++ b/compositor/src/state/reconciliation.rs @@ -44,7 +44,10 @@ pub(crate) struct DesktopChanges { impl Ferese { pub(crate) fn begin_desktop_transition(&mut self) { - assert!(self.desktop_transition.is_none(), "nested desktop transition"); + assert!( + self.desktop_transition.is_none(), + "nested desktop transition" + ); self.desktop_transition = Some(DesktopTransition::default()); } @@ -57,12 +60,23 @@ impl Ferese { assert!(self.desktop_transition.is_some()); let inventory = runtime .iter() - .map(|output| (self.ensure_output_identity(&output.identity), output.geometry())) + .map(|output| { + ( + self.ensure_output_identity(&output.identity), + output.geometry(), + ) + }) .collect::>(); - let workspaces = self.workspaces.iter().map(|workspace| workspace.id).collect::>(); - let plan = self - .output_workspaces - .plan_desktop(&inventory, &workspaces, self.workspaces.next_workspace_id())?; + let workspaces = self + .workspaces + .iter() + .map(|workspace| workspace.id) + .collect::>(); + let plan = self.output_workspaces.plan_desktop( + &inventory, + &workspaces, + self.workspaces.next_workspace_id(), + )?; let mut changes = DesktopChanges { layout: !plan.affected_outputs.is_empty(), focus: plan.focus_changed, @@ -83,8 +97,13 @@ impl Ferese { let old_owner = self.output_workspaces.output_for_workspace(workspace); let old = old_owner.and_then(|owner| self.output_workspaces.geometry(owner)); (old_owner != Some(destination) || old != Some(new)).then(|| { - let bounds = |g: OutputGeometry| Rect::new(g.x as f64, g.y as f64, g.width as f64, g.height as f64); - (*id, moved_floating_rect(rect, bounds(old.unwrap_or(new)), bounds(new))) + let bounds = |g: OutputGeometry| { + Rect::new(g.x as f64, g.y as f64, g.width as f64, g.height as f64) + }; + ( + *id, + moved_floating_rect(rect, bounds(old.unwrap_or(new)), bounds(new)), + ) }) }) .collect::>(); @@ -94,7 +113,10 @@ impl Ferese { for id in plan.created_workspaces { assert_eq!(self.workspaces.create_workspace(), id); } - let surviving = runtime.iter().map(|output| output.output.clone()).collect::>(); + let surviving = runtime + .iter() + .map(|output| output.output.clone()) + .collect::>(); let old_outputs = self.output_ids.keys().cloned().collect::>(); for output in old_outputs { if !surviving.contains(&output) { @@ -111,10 +133,16 @@ impl Ferese { for (output, (id, geometry)) in runtime.into_iter().zip(inventory) { let state_changed = output.output.current_mode() != Some(output.mode) || output.output.current_transform() != output.transform - || output.output.current_scale().fractional_scale() != output.scale.fractional_scale() - || self.space.output_geometry(&output.output).is_none_or(|old| { - old.loc != output.position || old.size.w != geometry.width || old.size.h != geometry.height - }); + || output.output.current_scale().fractional_scale() + != output.scale.fractional_scale() + || self + .space + .output_geometry(&output.output) + .is_none_or(|old| { + old.loc != output.position + || old.size.w != geometry.width + || old.size.h != geometry.height + }); if state_changed { output.output.change_current_state( Some(output.mode), @@ -168,7 +196,8 @@ impl Ferese { self.desktop_transition.as_mut().unwrap().publishing = true; let activity_output = self.space.outputs().next().map(|output| output.name()); if let Some(output) = activity_output - && (self.activities.store.attach_output(&output) | self.activities.pomodoro_attach_output(&output)) + && (self.activities.store.attach_output(&output) + | self.activities.pomodoro_attach_output(&output)) { self.activities.changed(); } @@ -190,13 +219,17 @@ impl Ferese { } changes.outputs.retain(|output| { self.output_ids.contains_key(output) - || self - .direct_backend - .as_ref() - .is_some_and(|backend| backend.physical_outputs().any(|physical| physical == output)) + || self.direct_backend.as_ref().is_some_and(|backend| { + backend + .physical_outputs() + .any(|physical| physical == output) + }) }); if !changes.outputs.is_empty() { - crate::backends::direct::render_on(self, &changes.outputs.into_iter().collect::>()); + crate::backends::direct::render_on( + self, + &changes.outputs.into_iter().collect::>(), + ); } } } @@ -241,7 +274,10 @@ mod tests { } } - fn window(state: &mut Ferese, event_loop: &mut EventLoop<'static, Ferese>) -> (Window, UnixStream) { + fn window( + state: &mut Ferese, + event_loop: &mut EventLoop<'static, Ferese>, + ) -> (Window, UnixStream) { use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_wm_base::XdgWmBase; use smithay::reexports::wayland_server::protocol::wl_compositor::WlCompositor; use smithay::wayland::shell::xdg::XdgWmBaseUserData; @@ -258,12 +294,18 @@ mod tests { ) .unwrap(); let shell = client - .create_resource::(&state.display_handle, 6, Default::default()) + .create_resource::( + &state.display_handle, + 6, + Default::default(), + ) .unwrap(); request(&mut wire, compositor.id().protocol_id(), 0, &[2]); request(&mut wire, shell.id().protocol_id(), 2, &[3, 2]); request(&mut wire, 3, 1, &[4]); - event_loop.dispatch(Duration::from_millis(10), state).unwrap(); + event_loop + .dispatch(Duration::from_millis(10), state) + .unwrap(); let window = state .space .elements() @@ -410,7 +452,10 @@ mod tests { events(&mut state, &mut shell_wire); let before_serial = state.shell_snapshot_serial; let spare = state.workspaces.create_workspace(); - state.output_workspaces.assign_workspace(b_id, spare).unwrap(); + state + .output_workspaces + .assign_workspace(b_id, spare) + .unwrap(); state.workspace_slides.insert( b_id, WorkspaceSlide::new(None, spare, b_workspace, SwipeDirection::Right), @@ -423,14 +468,20 @@ mod tests { state.workspaces.placement(slow_id), Some(WindowPlacement::Floating { rect: initial }) ); - assert_eq!(state.output_workspaces.output_for_workspace(b_workspace), Some(b_id)); + assert_eq!( + state.output_workspaces.output_for_workspace(b_workspace), + Some(b_id) + ); state.send_shell_snapshots(); assert!(events(&mut state, &mut shell_wire).is_empty()); let mut runtime = state.current_desktop_outputs(); runtime.retain(|entry| entry.output == a_handle); runtime[0].mode.size = (500, 400).into(); let changes = state.publish_desktop(runtime).unwrap(); - assert_eq!(state.output_workspaces.output_for_workspace(b_workspace), Some(a_id)); + assert_eq!( + state.output_workspaces.output_for_workspace(b_workspace), + Some(a_id) + ); assert_eq!(state.focused_window, Some(slow_id)); assert!(!state.workspace_slides.contains_key(&b_id)); state.send_shell_snapshots(); @@ -442,9 +493,27 @@ mod tests { .iter() .filter(|(id, _, _)| *id == shell.id().protocol_id()) .collect::>(); - assert_eq!(shell_events.iter().filter(|(_, opcode, _)| *opcode == 0).count(), 1); - assert_eq!(shell_events.iter().filter(|(_, opcode, _)| *opcode == 4).count(), 1); - assert_eq!(shell_events.iter().filter(|(_, opcode, _)| *opcode == 2).count(), 1); + assert_eq!( + shell_events + .iter() + .filter(|(_, opcode, _)| *opcode == 0) + .count(), + 1 + ); + assert_eq!( + shell_events + .iter() + .filter(|(_, opcode, _)| *opcode == 4) + .count(), + 1 + ); + assert_eq!( + shell_events + .iter() + .filter(|(_, opcode, _)| *opcode == 2) + .count(), + 1 + ); for (_, opcode, args) in &shell_events { if *opcode == 3 { assert_eq!(args[3], a_id.0 as u32); @@ -460,16 +529,27 @@ mod tests { assert_eq!(args[3], b_workspace.0 as u32); } } - let WindowPlacement::Floating { rect } = state.workspaces.placement(slow_id).unwrap() else { + let WindowPlacement::Floating { rect } = state.workspaces.placement(slow_id).unwrap() + else { panic!() }; - assert!(rect.x >= 0.0 && rect.y >= 0.0 && rect.x + rect.width <= 500.0 && rect.y + rect.height <= 400.0); + assert!( + rect.x >= 0.0 + && rect.y >= 0.0 + && rect.x + rect.width <= 500.0 + && rect.y + rect.height <= 400.0 + ); assert!( state.windows.transaction(&slow_id).is_some(), "topology commit must not await XDG acknowledgement" ); assert_eq!( - state.windows.geometry(&slow_id).unwrap().client.committed_size, + state + .windows + .geometry(&slow_id) + .unwrap() + .client + .committed_size, Some(ClientSize::from_rect(initial)) ); @@ -506,7 +586,10 @@ mod tests { } assert_eq!(state.output_workspaces, before); assert!(events(&mut state, &mut shell_wire).is_empty()); - assert_eq!(state.workspaces.workspace_for_window(slow_id), Some(b_workspace)); + assert_eq!( + state.workspaces.workspace_for_window(slow_id), + Some(b_workspace) + ); let unfocused = state.outputs_by_id[&c_id].clone(); let focused = state.focused_window; @@ -531,7 +614,9 @@ mod tests { ) .unwrap(); request(&mut lock_wire, lock.id().protocol_id(), 1, &[2]); - event_loop.dispatch(Duration::from_millis(10), &mut state).unwrap(); + event_loop + .dispatch(Duration::from_millis(10), &mut state) + .unwrap(); assert!(state.session_lock.active()); state.begin_desktop_transition(); for output in state.output_ids.keys().cloned().collect::>() { @@ -545,11 +630,17 @@ mod tests { assert!(state.focused_window.is_none()); assert!(state.workspace_slides.is_empty()); assert!(state.seat.get_keyboard().unwrap().current_focus().is_none()); - assert_eq!(state.workspaces.workspace_for_window(slow_id), Some(b_workspace)); + assert_eq!( + state.workspaces.workspace_for_window(slow_id), + Some(b_workspace) + ); state.begin_desktop_transition(); let changes = state.publish_desktop(vec![output("a", 0, 1200)]).unwrap(); state.finish_desktop_transition(changes); - assert_eq!(state.output_workspaces.output_for_workspace(b_workspace), Some(a_id)); + assert_eq!( + state.output_workspaces.output_for_workspace(b_workspace), + Some(a_id) + ); assert!(state.session_lock.active()); assert!(state.surface_under((20.0, 20.0).into()).is_none()); } diff --git a/compositor/src/state/tab_indicator.rs b/compositor/src/state/tab_indicator.rs index 1d7891d..b39a467 100644 --- a/compositor/src/state/tab_indicator.rs +++ b/compositor/src/state/tab_indicator.rs @@ -65,8 +65,19 @@ pub(crate) fn contains(rect: Rect, p: Point) -> bool { } impl IndicatorGeometry { - pub fn new(content: Rect, output: Rect, left_boundary: f64, count: usize, c: TabIndicatorSettings) -> Option { - if count < 2 || content.width <= 0.0 || content.height <= 0.0 || output.width <= 0.0 || output.height <= 0.0 { + pub fn new( + content: Rect, + output: Rect, + left_boundary: f64, + count: usize, + c: TabIndicatorSettings, + ) -> Option { + if count < 2 + || content.width <= 0.0 + || content.height <= 0.0 + || output.width <= 0.0 + || output.height <= 0.0 + { return None; } let left = content.x.max(output.x); @@ -83,19 +94,23 @@ impl IndicatorGeometry { left } .clamp(output.x, (right - c.width).max(output.x)); - let natural = count as f64 * c.segment_height + count.saturating_sub(1) as f64 * c.segment_gap; + let natural = + count as f64 * c.segment_height + count.saturating_sub(1) as f64 * c.segment_gap; let fit = ((bottom - top) / natural).min(1.0); let height = (natural * fit).min(bottom - top); let rail = Rect::new(x, top + (bottom - top - height) * 0.5, c.width, height); let hit_width = c.hit_width.min(output.width); let hit = Rect::new( - (x + c.width * 0.5 - hit_width * 0.5).clamp(output.x, output.x + output.width - hit_width), + (x + c.width * 0.5 - hit_width * 0.5) + .clamp(output.x, output.x + output.width - hit_width), rail.y, hit_width, rail.height, ); let padding = c.padding.min(output.height * 0.1).min(output.width * 0.1); - let row_height = c.row_height.min((output.height - 2.0 * padding).max(f64::EPSILON)); + let row_height = c + .row_height + .min((output.height - 2.0 * padding).max(f64::EPSILON)); let rows = (((output.height - 2.0 * padding) / row_height).floor() as usize) .max(1) .min(count); @@ -110,7 +125,8 @@ impl IndicatorGeometry { }; let list = Rect::new( x, - (rail.y + rail.height * 0.5 - list_height * 0.5).clamp(output.y, output.y + output.height - list_height), + (rail.y + rail.height * 0.5 - list_height * 0.5) + .clamp(output.y, output.y + output.height - list_height), list_width, list_height, ); @@ -175,7 +191,12 @@ impl Ferese { } let output = self.outputs_by_id.get(&output_id)?; let g = self.space.output_geometry(output)?; - let bounds = Rect::new(g.loc.x as f64, g.loc.y as f64, g.size.w as f64, g.size.h as f64); + let bounds = Rect::new( + g.loc.x as f64, + g.loc.y as f64, + g.size.w as f64, + g.size.h as f64, + ); let mut boundary = bounds.x; // Measure the current visible gap, including animated column positions. for window in self.space.elements() { @@ -215,7 +236,9 @@ impl Ferese { || self.input_capture.active() || self.overview.is_presenting() || self.seat.get_pointer().is_some_and(|p| p.is_grabbed()) - || self.layer_surface_under(p, &[Layer::Overlay, Layer::Top]).is_some() + || self + .layer_surface_under(p, &[Layer::Overlay, Layer::Top]) + .is_some() { return false; } @@ -228,7 +251,10 @@ impl Ferese { if id == active { break; } - if self.presented_window_rect(id).is_some_and(|r| contains(r, p)) { + if self + .presented_window_rect(id) + .is_some_and(|r| contains(r, p)) + { return false; } } @@ -282,11 +308,19 @@ impl Ferese { .first_row .min(bar.group.windows.len().saturating_sub(g.rows)); let index = ((p.y - g.list.y - g.padding).max(0.0) / g.row_height).floor() as usize; - return bar.group.windows.get(first + index.min(g.rows - 1)).copied(); + return bar + .group + .windows + .get(first + index.min(g.rows - 1)) + .copied(); } if contains(g.hit, p) { let index = ((p.y - g.rail.y) / g.pitch).floor() as usize; - return bar.group.windows.get(index.min(bar.group.windows.len() - 1)).copied(); + return bar + .group + .windows + .get(index.min(bar.group.windows.len() - 1)) + .copied(); } if expanded && g.hover_contains(p) { return Some(bar.group.active); @@ -314,13 +348,24 @@ impl Ferese { } self.advance_animations(Instant::now()); if let Some(anchor) = anchor { - let same = self.tab_indicator.hover.as_ref().is_some_and(|h| h.anchor == anchor); + let same = self + .tab_indicator + .hover + .as_ref() + .is_some_and(|h| h.anchor == anchor); if !same { let selected = self .scrolling_tab_bar(anchor) .and_then(|bar| { - let g = self.tab_indicator_geometry(bar, self.presented_window_rect(bar.group.active)?)?; - let selected = bar.group.windows.iter().position(|id| *id == bar.group.active)?; + let g = self.tab_indicator_geometry( + bar, + self.presented_window_rect(bar.group.active)?, + )?; + let selected = bar + .group + .windows + .iter() + .position(|id| *id == bar.group.active)?; Some( selected .saturating_sub(g.rows / 2) @@ -420,7 +465,10 @@ impl Ferese { let WorkspaceLayout::Scrolling(layout) = &workspace.layout else { return; }; - let Some(group) = layout.tab_group(current).filter(|g| g.windows.contains(&previous)) else { + let Some(group) = layout + .tab_group(current) + .filter(|g| g.windows.contains(&previous)) + else { return; }; if self.tab_indicator_settings.flash_duration.is_zero() { @@ -430,7 +478,12 @@ impl Ferese { if let Some(token) = self.tab_indicator.timer.take() { self.loop_handle.remove(token); } - if let Some(flash) = self.tab_indicator.flash.as_mut().filter(|f| f.anchor == anchor) { + if let Some(flash) = self + .tab_indicator + .flash + .as_mut() + .filter(|f| f.anchor == anchor) + { flash.opacity.retarget_preserving_motion(1.0); } else { let mut opacity = AnimatedValue::new(0.0); @@ -466,7 +519,10 @@ impl Ferese { } pub(crate) fn reconcile_tab_indicator(&mut self) { - if self.session_lock.active() || self.input_capture.active() || self.overview.is_presenting() { + if self.session_lock.active() + || self.input_capture.active() + || self.overview.is_presenting() + { self.clear_tab_indicator(); return; } @@ -571,9 +627,17 @@ mod tests { state.hover.as_mut().unwrap().opacity.set_target(1.); state.advance(Duration::from_millis(30), SpringConfig::default(), true); let before = state.hover.as_ref().unwrap().opacity; - state.hover.as_mut().unwrap().opacity.retarget_preserving_motion(0.); + state + .hover + .as_mut() + .unwrap() + .opacity + .retarget_preserving_motion(0.); let after = state.hover.as_ref().unwrap().opacity; - assert_eq!((before.current, before.velocity), (after.current, after.velocity)); + assert_eq!( + (before.current, before.velocity), + (after.current, after.velocity) + ); for _ in 0..300 { state.advance(Duration::from_millis(16), SpringConfig::default(), true); } diff --git a/compositor/src/state/tabs.rs b/compositor/src/state/tabs.rs index 60e2f0b..c9e23ba 100644 --- a/compositor/src/state/tabs.rs +++ b/compositor/src/state/tabs.rs @@ -12,7 +12,10 @@ impl Ferese { if workspace.fullscreen.is_some() { return; } - let Ok(Some(target)) = workspace.layout.directional_neighbor(window, direction, bounds) else { + let Ok(Some(target)) = workspace + .layout + .directional_neighbor(window, direction, bounds) + else { return; }; if let WorkspaceLayout::Scrolling(layout) = &mut self.workspaces.active_mut().layout { @@ -71,24 +74,38 @@ impl Ferese { if self.workspaces.active().fullscreen.is_some() { return; } - if let Some(target) = self.workspaces.active().layout.adjacent_tab(window, previous) + if let Some(target) = self + .workspaces + .active() + .layout + .adjacent_tab(window, previous) && self.activate_managed_window(target) { self.flash_tab_switch(window, target); } } - pub(crate) fn visible_tab_bars(&self, workspace: WorkspaceId) -> impl Iterator { - self.tab_bars.get(&workspace).into_iter().flatten().filter(move |bar| { - self.workspaces - .workspace(workspace) - .is_some_and(|ws| ws.fullscreen.is_none()) - && self.windows.record(bar.group.active).is_some_and(|w| !w.maximized) - && self - .windows - .window(bar.group.active) - .is_some_and(|w| self.space.element_location(w).is_some()) - }) + pub(crate) fn visible_tab_bars( + &self, + workspace: WorkspaceId, + ) -> impl Iterator { + self.tab_bars + .get(&workspace) + .into_iter() + .flatten() + .filter(move |bar| { + self.workspaces + .workspace(workspace) + .is_some_and(|ws| ws.fullscreen.is_none()) + && self + .windows + .record(bar.group.active) + .is_some_and(|w| !w.maximized) + && self + .windows + .window(bar.group.active) + .is_some_and(|w| self.space.element_location(w).is_some()) + }) } pub(crate) fn tab_at(&self, position: Point) -> Option { @@ -114,7 +131,12 @@ impl Ferese { } self.visible_tab_bars(workspace).find_map(|bar| { let content = self.presented_window_rect(bar.group.active)?; - let rect = Rect::new(content.x, content.y - bar.rect.height, content.width, bar.rect.height); + let rect = Rect::new( + content.x, + content.y - bar.rect.height, + content.width, + bar.rect.height, + ); tab_hit(&bar.group.windows, rect, position.x, position.y) }) } diff --git a/compositor/src/state/viewport.rs b/compositor/src/state/viewport.rs index 495c8c4..de3ddcc 100644 --- a/compositor/src/state/viewport.rs +++ b/compositor/src/state/viewport.rs @@ -107,7 +107,9 @@ impl ViewportPresentation { pub(super) fn reconcile_hold(&mut self, blocked: bool, release_tick: bool) { let mode = std::mem::replace(&mut self.mode, ViewportMode::Running(ViewportOwner::Spring)); self.mode = match mode { - ViewportMode::Running(owner) | ViewportMode::Resuming(owner) if blocked => ViewportMode::Held(owner), + ViewportMode::Running(owner) | ViewportMode::Resuming(owner) if blocked => { + ViewportMode::Held(owner) + } ViewportMode::Held(owner) if !blocked && !release_tick => ViewportMode::Running(owner), mode => mode, }; @@ -130,7 +132,8 @@ impl ViewportPresentation { #[cfg(feature = "resize-metrics")] pub(super) fn spring_is_moving(&self) -> bool { - self.gesture().is_none() && (self.motion.current != self.motion.target || self.motion.velocity != 0.0) + self.gesture().is_none() + && (self.motion.current != self.motion.target || self.motion.velocity != 0.0) } // Prediction and authoritative ticks evaluate the same motion step. The @@ -153,12 +156,14 @@ impl ViewportPresentation { motion.velocity = 0.0; true } - ViewportMode::Running(ViewportOwner::Spring) | ViewportMode::Resuming(ViewportOwner::Spring) + ViewportMode::Running(ViewportOwner::Spring) + | ViewportMode::Resuming(ViewportOwner::Spring) if animations => { motion.advance(delta, spring) } - ViewportMode::Running(ViewportOwner::Spring) | ViewportMode::Resuming(ViewportOwner::Spring) => { + ViewportMode::Running(ViewportOwner::Spring) + | ViewportMode::Resuming(ViewportOwner::Spring) => { motion.snap(); false } @@ -167,16 +172,28 @@ impl ViewportPresentation { (motion, active) } - pub(super) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations: bool, blocked: bool) -> bool { + pub(super) fn advance( + &mut self, + delta: Duration, + spring: SpringConfig, + animations: bool, + blocked: bool, + ) -> bool { let (motion, active) = self.step_motion(delta, spring, animations, blocked); self.motion = motion; let mode = std::mem::replace(&mut self.mode, ViewportMode::Running(ViewportOwner::Spring)); self.mode = match mode { - ViewportMode::Running(owner) | ViewportMode::Held(owner) | ViewportMode::Resuming(owner) if blocked => { + ViewportMode::Running(owner) + | ViewportMode::Held(owner) + | ViewportMode::Resuming(owner) + if blocked => + { ViewportMode::Held(owner) } ViewportMode::Held(owner) => ViewportMode::Resuming(owner), - ViewportMode::Running(owner) | ViewportMode::Resuming(owner) => ViewportMode::Running(owner), + ViewportMode::Running(owner) | ViewportMode::Resuming(owner) => { + ViewportMode::Running(owner) + } }; active } @@ -198,7 +215,9 @@ impl ViewportPresentation { impl Ferese { pub(super) fn focus_swipe(&self) -> Option<&FocusSwipe> { - self.viewports.values().find_map(ViewportPresentation::gesture) + self.viewports + .values() + .find_map(ViewportPresentation::gesture) } } @@ -261,7 +280,10 @@ mod tests { viewport.retarget(700.0, true); assert_eq!(viewport.motion.current, position); assert_eq!(viewport.motion.velocity, velocity); - assert!(matches!(viewport.mode, ViewportMode::Running(ViewportOwner::Spring))); + assert!(matches!( + viewport.mode, + ViewportMode::Running(ViewportOwner::Spring) + )); let mut expected = viewport.motion; expected.advance(TICK, SpringConfig::default()); viewport.advance(TICK, SpringConfig::default(), true, false); @@ -349,7 +371,10 @@ mod tests { viewport.reconcile_hold(true, false); viewport.end_gesture(None).unwrap(); viewport.retarget(700.0, true); - assert!(matches!(viewport.mode, ViewportMode::Held(ViewportOwner::Spring))); + assert!(matches!( + viewport.mode, + ViewportMode::Held(ViewportOwner::Spring) + )); assert_eq!(viewport.motion.current, frozen.current); assert_eq!(viewport.motion.velocity, frozen.velocity); } @@ -374,7 +399,10 @@ mod tests { let mut advanced = state.clone(); advanced.advance(horizon, SpringConfig::default(), true, blocked); assert_eq!(sample, advanced.presented_motion()); - assert!(state == original, "prediction changed authoritative mode or motion"); + assert!( + state == original, + "prediction changed authoritative mode or motion" + ); } } } @@ -402,7 +430,12 @@ mod tests { )); world.velocity = before.velocity.x + motion.velocity; world.set_target(geometry.logical.x + motion.target); - super::super::animation::sync_scrolling_coordinates(&mut geometry, &mut world, &motion, false); + super::super::animation::sync_scrolling_coordinates( + &mut geometry, + &mut world, + &motion, + false, + ); assert!((geometry.visual.current.x - before.current.x).abs() < 1e-6); assert!((geometry.visual.velocity.x - before.velocity.x).abs() < 1e-6); assert_eq!(geometry.logical.x, world.target - motion.target); diff --git a/compositor/src/state/window_lifecycle_tests.rs b/compositor/src/state/window_lifecycle_tests.rs index 4f93b19..58a913e 100644 --- a/compositor/src/state/window_lifecycle_tests.rs +++ b/compositor/src/state/window_lifecycle_tests.rs @@ -4,7 +4,11 @@ use smithay::input::pointer::{Focus, GrabStartData, MotionEvent}; use smithay::reexports::wayland_server::Resource; use smithay::utils::SERIAL_COUNTER; -fn fixture() -> (smithay::reexports::calloop::EventLoop<'static, Ferese>, Ferese, Output) { +fn fixture() -> ( + smithay::reexports::calloop::EventLoop<'static, Ferese>, + Ferese, + Output, +) { let mut events = smithay::reexports::calloop::EventLoop::try_new().unwrap(); let mut state = crate::startup_tests::state(&mut events); state.animations_enabled = false; @@ -53,7 +57,9 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { .iter() .map(|(window, _)| state.windows.ids()[window]) .collect::>(); - let [a, b, c, d] = ids.as_slice() else { unreachable!() }; + let [a, b, c, d] = ids.as_slice() else { + unreachable!() + }; let (a, b, c, d) = (*a, *b, *c, *d); let mut tree = ferese_layout::LayoutTree::default(); tree.insert(a, None, Axis::Horizontal, 0.5).unwrap(); @@ -94,7 +100,10 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(48)); let after = [a, b, c, d].map(|id| state.windows.geometry(&id).unwrap().visual.current); assert_eq!(before[..2], after[..2], "linked left reflow must wait"); - assert_ne!(before[2], after[2], "right branch must continue its existing motion"); + assert_ne!( + before[2], after[2], + "right branch must continue its existing motion" + ); assert_ne!(before[3], after[3]); let frame = state.sample_frame(&output, Duration::from_millis(16)); assert_eq!(frame.windows[&a].geometry.visual.current, after[0]); @@ -105,7 +114,11 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { let location = state.space.element_location(window).unwrap(); assert_eq!(location.x, geometry.visual.current.x.round() as i32); assert_eq!(location.y, geometry.visual.current.y.round() as i32); - let point = (geometry.visual.current.x + 5.0, geometry.visual.current.y + 5.0).into(); + let point = ( + geometry.visual.current.x + 5.0, + geometry.visual.current.y + 5.0, + ) + .into(); let (surface, origin) = state.window_surface_under(point).unwrap(); assert_eq!(surface, *window.toplevel().unwrap().wl_surface()); assert!((origin.y - geometry.visual.current.y).abs() < 0.001); @@ -120,8 +133,16 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { request(&mut clients[0].1, 2, 6, &[], None); dispatch(&mut events, &mut state); assert!(state.windows.transaction(&a).is_none()); - assert!(state.presentation_dependencies.reflow_blocked(a, &state.windows)); - assert!(state.presentation_dependencies.reflow_blocked(b, &state.windows)); + assert!( + state + .presentation_dependencies + .reflow_blocked(a, &state.windows) + ); + assert!( + state + .presentation_dependencies + .reflow_blocked(b, &state.windows) + ); state.advance_animations_at(Duration::from_millis(284), Duration::from_millis(332)); assert!(state.windows.transaction(&b).is_none()); assert_eq!(state.windows.geometry(&a).unwrap().visual.current, after[0]); @@ -134,10 +155,17 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { request(&mut clients[0].1, 2, 1, &[0, 0, 0], None); request(&mut clients[0].1, 2, 6, &[], None); dispatch(&mut events, &mut state); - assert!(!state.presentation_dependencies.reflow_blocked(c, &state.windows)); + assert!( + !state + .presentation_dependencies + .reflow_blocked(c, &state.windows) + ); state .workspaces - .set_active_layout_mode(ferese_core::LayoutMode::Scrolling, Rect::new(0.0, 0.0, 800.0, 600.0)) + .set_active_layout_mode( + ferese_core::LayoutMode::Scrolling, + Rect::new(0.0, 0.0, 800.0, 600.0), + ) .unwrap(); state.relayout(); for id in [c, d] { @@ -145,7 +173,11 @@ fn tree_resize_does_not_hold_motion_in_an_unchanged_neighboring_branch() { } state.release_presentation_dependencies(); for id in [c, d] { - assert!(!state.presentation_dependencies.reflow_blocked(id, &state.windows)); + assert!( + !state + .presentation_dependencies + .reflow_blocked(id, &state.windows) + ); } } @@ -187,7 +219,10 @@ fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal( state.release_presentation_dependencies(); state.advance_animations_at(Duration::ZERO, Duration::ZERO); let before = state.windows.geometry(&a).unwrap().visual.current.width; - assert_eq!(state.windows.geometry(&b).unwrap().visual.current.width, before); + assert_eq!( + state.windows.geometry(&b).unwrap().visual.current.width, + before + ); state.animations_enabled = animations; state.spring_config = SpringConfig { stiffness: 900.0, @@ -222,7 +257,11 @@ fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal( let second = *state.windows.geometry(&b).unwrap(); assert_eq!(first.visual.current.width, second.visual.current.width); assert_eq!(first.visual.velocity.width, second.visual.velocity.width); - for horizon in [Duration::ZERO, Duration::from_millis(4), Duration::from_millis(16)] { + for horizon in [ + Duration::ZERO, + Duration::from_millis(4), + Duration::from_millis(16), + ] { let scene = state.sample_frame(&output, horizon); assert_eq!( scene.windows[&a].geometry.visual.current.width, @@ -236,11 +275,15 @@ fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal( state.windows.clear_transaction(&a); state.release_presentation_dependencies(); assert_eq!( - state.presentation_dependencies.reflow_blocked(a, &state.windows), + state + .presentation_dependencies + .reflow_blocked(a, &state.windows), animations ); assert_eq!( - state.presentation_dependencies.reflow_blocked(b, &state.windows), + state + .presentation_dependencies + .reflow_blocked(b, &state.windows), animations ); for millis in [16, 100, 299, 300, 316, 332] { @@ -262,11 +305,20 @@ fn width_cycle_keeps_stacked_rows_together_through_holds_forecasts_and_reversal( } state.release_presentation_dependencies(); for frame in 1..=150 { - state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(332 + frame * 16)); + state.advance_animations_at( + Duration::from_millis(16), + Duration::from_millis(332 + frame * 16), + ); assert_equal(&state); } - assert_eq!(state.windows.geometry(&a).unwrap().visual.current.width, before); - assert_eq!(state.windows.geometry(&b).unwrap().visual.current.width, before); + assert_eq!( + state.windows.geometry(&a).unwrap().visual.current.width, + before + ); + assert_eq!( + state.windows.geometry(&b).unwrap().visual.current.width, + before + ); } } @@ -292,9 +344,18 @@ fn floating_placement_reserves_the_full_opening_window_footprint() { let obstacles = state.floating_obstacles(None, bounds); assert_eq!( obstacles, - vec![(id.0, crate::floating::intersection(logical, bounds).unwrap())] + vec![( + id.0, + crate::floating::intersection(logical, bounds).unwrap() + )] ); - let placed = crate::floating::min_overlap((80., 80.), bounds, &[obstacles[0].1], (logical.x, logical.y)).unwrap(); + let placed = crate::floating::min_overlap( + (80., 80.), + bounds, + &[obstacles[0].1], + (logical.x, logical.y), + ) + .unwrap(); assert!(crate::floating::intersection(placed, logical).is_none()); } @@ -424,7 +485,12 @@ fn keyboard_floating_actions_preserve_maximized_and_fullscreen_restore_geometry( state.set_window_maximized(id, true); } let geometry = state.windows.geometry(&id).unwrap().logical; - for direction in [Direction::Left, Direction::Right, Direction::Up, Direction::Down] { + for direction in [ + Direction::Left, + Direction::Right, + Direction::Up, + Direction::Down, + ] { state.move_direction(direction); state.resize_direction(direction); assert_eq!(state.workspaces.placement(id), normal); @@ -462,17 +528,29 @@ fn keyboard_floating_resize_reuses_the_client_barrier_and_move_keeps_it_pending( let before = state.windows.geometry(&id).unwrap().visual.current; let client = state.windows.geometry(&id).unwrap().client.committed_size; state.resize_direction(Direction::Right); - let transaction = *state.windows.transaction(&id).expect("existing resize barrier"); + let transaction = *state + .windows + .transaction(&id) + .expect("existing resize barrier"); assert_eq!(transaction.source_geometry(), Some(window.geometry())); - assert_eq!(state.windows.geometry(&id).unwrap().client.committed_size, client); + assert_eq!( + state.windows.geometry(&id).unwrap().client.committed_size, + client + ); assert_eq!(state.windows.geometry(&id).unwrap().visual.current, before); assert_eq!( state.windows.geometry(&id).unwrap().logical, Rect::new(120., 100., 96., 48.) ); state.move_direction(Direction::Right); - assert_eq!(state.windows.transaction(&id).unwrap().serial(), transaction.serial()); - assert_eq!(state.windows.geometry(&id).unwrap().client.committed_size, client); + assert_eq!( + state.windows.transaction(&id).unwrap().serial(), + transaction.serial() + ); + assert_eq!( + state.windows.geometry(&id).unwrap().client.committed_size, + client + ); assert_eq!(state.windows.geometry(&id).unwrap().visual.current, before); assert_eq!( state.windows.geometry(&id).unwrap().logical, @@ -593,12 +671,24 @@ fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() { // being visited first. The mock capability accepts only B's buffers. for (params, buffer) in [(5, None), (6, Some(7))] { request(&mut wire, 4, 1, &[params], None); - request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd())); + request( + &mut wire, + params, + 1, + &[0, 0, 16, 0, 0], + Some(file.as_raw_fd()), + ); let mut args = vec![4, 4, Fourcc::Argb8888 as u32, 0]; if let Some(buffer) = buffer { args.insert(0, buffer); } - request(&mut wire, params, if buffer.is_some() { 3 } else { 2 }, &args, None); + request( + &mut wire, + params, + if buffer.is_some() { 3 } else { 2 }, + &args, + None, + ); dispatch(&mut events, &mut state); assert!(state.space.outputs().next().is_none()); state.process_pending_dmabuf_imports(); @@ -619,10 +709,19 @@ fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() { state.display_handle.flush_clients().unwrap(); let response = read(&mut wire); if buffer.is_none() { - assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params); - assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 0); + assert_eq!( + u32::from_ne_bytes(response[..4].try_into().unwrap()), + params + ); + assert_eq!( + u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, + 0 + ); } else { - assert!(response.is_empty(), "create_immed success must not emit a failed event"); + assert!( + response.is_empty(), + "create_immed success must not emit a failed event" + ); } assert!(client.get_credentials(&state.display_handle).is_ok()); } @@ -630,12 +729,26 @@ fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() { // Deferred requests receive one terminal failure on timeout or removal. for params in [8, 9] { request(&mut wire, 4, 1, &[params], None); - request(&mut wire, params, 1, &[0, 0, 16, 0, 0], Some(file.as_raw_fd())); - request(&mut wire, params, 2, &[4, 4, Fourcc::Argb8888 as u32, 0], None); + request( + &mut wire, + params, + 1, + &[0, 0, 16, 0, 0], + Some(file.as_raw_fd()), + ); + request( + &mut wire, + params, + 2, + &[4, 4, Fourcc::Argb8888 as u32, 0], + None, + ); dispatch(&mut events, &mut state); state.process_pending_dmabuf_imports(); if params == 8 { - state.dmabuf_imports.expire(Instant::now() + Duration::from_secs(5)); + state + .dmabuf_imports + .expire(Instant::now() + Duration::from_secs(5)); } else { state.dmabuf_imports.remove(b); } @@ -643,8 +756,14 @@ fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() { state.display_handle.flush_clients().unwrap(); let response = read(&mut wire); assert_eq!(response.len(), 8); - assert_eq!(u32::from_ne_bytes(response[..4].try_into().unwrap()), params); - assert_eq!(u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, 1); + assert_eq!( + u32::from_ne_bytes(response[..4].try_into().unwrap()), + params + ); + assert_eq!( + u32::from_ne_bytes(response[4..8].try_into().unwrap()) & 0xffff, + 1 + ); state .dmabuf_imports .process_with(b, None, |_| panic!("failed request survived")); @@ -659,9 +778,9 @@ fn dmabuf_validation_uses_the_originating_global_without_a_desktop_frame() { drop(wire); dispatch(&mut events, &mut state); state.process_pending_dmabuf_imports(); - state - .dmabuf_imports - .process_with(b, None, |_| panic!("disconnected client's request survived")); + state.dmabuf_imports.process_with(b, None, |_| { + panic!("disconnected client's request survived") + }); } #[test] @@ -697,7 +816,10 @@ fn hidden_null_commit_removes_membership_and_remaps_without_workspace_switch() { dispatch(&mut events, &mut state); let remapped = state.windows.ids()[&window]; assert_ne!(id, remapped); - assert_eq!(state.workspaces.workspace_for_window(remapped), Some(active)); + assert_eq!( + state.workspaces.workspace_for_window(remapped), + Some(active) + ); assert_eq!(state.workspaces.active_id(), active); } @@ -731,7 +853,10 @@ fn hidden_commits_update_client_geometry_and_release_resize_transactions() { assert!(state.windows.transaction(&id).is_none()); assert_eq!( state.windows.geometry(&id).unwrap().client.committed_size, - Some(ClientSize { width: 32, height: 48 }) + Some(ClientSize { + width: 32, + height: 48 + }) ); assert!(state.space.element_location(&window).is_none()); } @@ -760,13 +885,23 @@ fn floating_maximize_and_restore_work_in_both_layouts() { }; let mut next_object = 7; - for mode in [ferese_core::LayoutMode::Scrolling, ferese_core::LayoutMode::Tree] { + for mode in [ + ferese_core::LayoutMode::Scrolling, + ferese_core::LayoutMode::Tree, + ] { let bounds = state.output_bounds().unwrap(); - state.workspaces.set_active_layout_mode(mode, bounds).unwrap(); + state + .workspaces + .set_active_layout_mode(mode, bounds) + .unwrap(); state.relayout(); state.set_window_maximized(id, true); let maximized = maximized_rect(bounds, state.gap_config.outer); - assert_eq!(state.windows.geometry(&id).unwrap().logical, maximized, "{mode:?}"); + assert_eq!( + state.windows.geometry(&id).unwrap().logical, + maximized, + "{mode:?}" + ); assert_eq!( state.workspaces.placement(id), Some(WindowPlacement::Floating { rect: normal }) @@ -785,10 +920,17 @@ fn floating_maximize_and_restore_work_in_both_layouts() { smithay::wayland::GlobalData, ) .unwrap(); - let (width, height) = (maximized.width.round() as u32, maximized.height.round() as u32); + let (width, height) = ( + maximized.width.round() as u32, + maximized.height.round() as u32, + ); let mut file = tempfile::tempfile().unwrap(); - file.write_all(&0xff112233u32.to_ne_bytes().repeat((width * height) as usize)) - .unwrap(); + file.write_all( + &0xff112233u32 + .to_ne_bytes() + .repeat((width * height) as usize), + ) + .unwrap(); request( &mut wire, shm.id().protocol_id(), @@ -813,7 +955,11 @@ fn floating_maximize_and_restore_work_in_both_layouts() { })); state.set_window_maximized(id, false); - assert_eq!(state.windows.geometry(&id).unwrap().logical, normal, "{mode:?}"); + assert_eq!( + state.windows.geometry(&id).unwrap().logical, + normal, + "{mode:?}" + ); state.display_handle.flush_clients().unwrap(); ack_configure(&mut wire, 3); request(&mut wire, 3, 3, &[0, 0, 64, 48], None); @@ -904,7 +1050,10 @@ fn malformed_resize_limits_disconnect_only_the_client_and_motion_survives() { offset += (header >> 16) as usize; } - assert!(rejected, "invalid limits {minimum:?}/{maximum:?} were not rejected"); + assert!( + rejected, + "invalid limits {minimum:?}/{maximum:?} were not rejected" + ); } let (healthy, _wire) = window(&mut state, &mut events, 0xff112233); @@ -926,37 +1075,47 @@ fn window_rule_minimum_size_replaces_the_client_advertised_minimum() { request(&mut wire, 2, 6, &[], None); dispatch(&mut events, &mut state); assert_eq!( - state.effective_size_constraints(&window, (800, 600), (0, 0)).0, + state + .effective_size_constraints(&window, (800, 600), (0, 0)) + .0, (800, 600) ); assert_eq!(state.window_constraints()[&id].min_width, 800.0); assert_eq!(state.window_constraints()[&id].min_height, 600.0); - state.window_rules = - crate::config::Config::parse_source("window-rule transient=#false min-width=500 min-height=400\n") - .unwrap() - .window_rules() - .unwrap(); + state.window_rules = crate::config::Config::parse_source( + "window-rule transient=#false min-width=500 min-height=400\n", + ) + .unwrap() + .window_rules() + .unwrap(); assert_eq!( - state.effective_size_constraints(&window, (800, 600), (0, 0)).0, + state + .effective_size_constraints(&window, (800, 600), (0, 0)) + .0, (500, 400) ); let constraints = state.window_constraints(); assert_eq!(constraints[&id].min_width, 500.0); assert_eq!(constraints[&id].min_height, 400.0); - state.window_rules = crate::config::Config::parse_source("window-rule transient=#false min-height=400\n") - .unwrap() - .window_rules() - .unwrap(); + state.window_rules = + crate::config::Config::parse_source("window-rule transient=#false min-height=400\n") + .unwrap() + .window_rules() + .unwrap(); assert_eq!( - state.effective_size_constraints(&window, (800, 600), (0, 0)).0, + state + .effective_size_constraints(&window, (800, 600), (0, 0)) + .0, (800, 400) ); state.window_rules.clear(); assert_eq!( - state.effective_size_constraints(&window, (800, 600), (0, 0)).0, + state + .effective_size_constraints(&window, (800, 600), (0, 0)) + .0, (800, 600) ); } @@ -1116,7 +1275,10 @@ fn tree_tabs_join_switch_leave_and_hide_inactive_maximized_windows() { let header = bar.rect; let content = state.windows.geometry(&b).unwrap().visual.target; assert_eq!(content.y, header.y + header.height); - let hit = Point::from((header.x + header.width * 0.25, header.y + header.height * 0.5)); + let hit = Point::from(( + header.x + header.width * 0.25, + header.y + header.height * 0.5, + )); assert_eq!(state.tab_at(hit), Some(a)); assert!(state.surface_under(hit).is_none()); state.cycle_tab(false); @@ -1170,7 +1332,10 @@ fn scrolling_tabs_switch_with_directional_focus_and_drop_hidden_resize_holds() { let position = state.workspaces.active().layout.viewport_x(); state.windows.set_transaction( c, - crate::resize_transaction::ResizeTransaction::new(SERIAL_COUNTER.next_serial(), state.presentation_now()), + crate::resize_transaction::ResizeTransaction::new( + SERIAL_COUNTER.next_serial(), + state.presentation_now(), + ), ); state.focus_direction(Direction::Up); assert_eq!(state.focused_window, Some(b)); @@ -1210,14 +1375,21 @@ fn tab_indicator_hover_switch_and_flash_preserve_window_geometry() { let g = state .tab_indicator_geometry(&bar, state.presented_window_rect(b).unwrap()) .unwrap(); - let point = (g.hit.x + g.hit.width * 0.5, g.rail.y + g.segment_height * 0.5).into(); + let point = ( + g.hit.x + g.hit.width * 0.5, + g.rail.y + g.segment_height * 0.5, + ) + .into(); assert_eq!(state.tab_at(point), Some(a)); assert!( state.surface_under(point).is_none(), "the rail must not deliver input to the client" ); state.update_tab_indicator_hover(point); - assert_eq!(state.tab_indicator.hover.as_ref().unwrap().opacity.current, 1.); + assert_eq!( + state.tab_indicator.hover.as_ref().unwrap().opacity.current, + 1. + ); assert_eq!(state.windows.geometry(&b).unwrap().logical, before); let g = state .tab_indicator_geometry(&bar, state.presented_window_rect(b).unwrap()) @@ -1232,13 +1404,22 @@ fn tab_indicator_hover_switch_and_flash_preserve_window_geometry() { assert_eq!(state.tab_at(point), Some(a)); state.activate_managed_window(a); assert_eq!(state.windows.geometry(&a).unwrap().logical, before); - assert!(state.tab_indicator.flash.is_none(), "pointer activation does not flash"); + assert!( + state.tab_indicator.flash.is_none(), + "pointer activation does not flash" + ); state.update_tab_indicator_hover((799., 599.).into()); - assert_eq!(state.tab_indicator.hover.as_ref().unwrap().opacity.current, 0.); + assert_eq!( + state.tab_indicator.hover.as_ref().unwrap().opacity.current, + 0. + ); state.tab_indicator_settings.flash_duration = Duration::from_millis(1); state.cycle_tab(false); assert_eq!(state.focused_window, Some(b)); - assert_eq!(state.tab_indicator.flash.as_ref().unwrap().opacity.current, 1.); + assert_eq!( + state.tab_indicator.flash.as_ref().unwrap().opacity.current, + 1. + ); assert_eq!(state.windows.geometry(&b).unwrap().logical, before); let deadline = Instant::now() + Duration::from_secs(1); while state @@ -1248,9 +1429,14 @@ fn tab_indicator_hover_switch_and_flash_preserve_window_geometry() { .is_some_and(|f| f.opacity.target != 0.) && Instant::now() < deadline { - events.dispatch(Duration::from_millis(5), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(5), &mut state) + .unwrap(); } - assert_eq!(state.tab_indicator.flash.as_ref().unwrap().opacity.current, 0.); + assert_eq!( + state.tab_indicator.flash.as_ref().unwrap().opacity.current, + 0. + ); state.toggle_column_tabbed(); assert!(state.tab_indicator.hover.is_none()); assert!(state.tab_indicator.flash.is_none()); @@ -1369,7 +1555,10 @@ fn transient_fullscreen_exception_follows_parent_changes() { let child_id = state.windows.ids()[&child]; state.set_window_fullscreen(parent_id, true); change_transient_parent(&mut state, &child, Some(&parent)); - assert_eq!(state.floating_above_fullscreen.get(&child_id), Some(&parent_id)); + assert_eq!( + state.floating_above_fullscreen.get(&child_id), + Some(&parent_id) + ); change_transient_parent(&mut state, &child, None); assert!( !state.floating_above_fullscreen.contains_key(&child_id), @@ -1390,7 +1579,10 @@ fn transient_fullscreen_exception_follows_parent_changes() { ); state.set_window_fullscreen(other_id, true); change_transient_parent(&mut state, &child, Some(&other)); - assert_eq!(state.floating_above_fullscreen.get(&child_id), Some(&other_id)); + assert_eq!( + state.floating_above_fullscreen.get(&child_id), + Some(&other_id) + ); state.remove_tiled_window(&other); assert!( state diff --git a/compositor/src/state/window_registry.rs b/compositor/src/state/window_registry.rs index 2ff1c5d..9847628 100644 --- a/compositor/src/state/window_registry.rs +++ b/compositor/src/state/window_registry.rs @@ -76,9 +76,14 @@ impl WindowRegistry { W: Clone, { assert!(!self.ids.contains_key(&window), "window already registered"); - assert!(!self.records.contains_key(&id), "window ID already registered"); + assert!( + !self.records.contains_key(&id), + "window ID already registered" + ); - let index = self.overview_order.partition_point(|existing| existing.0 < id.0); + let index = self + .overview_order + .partition_point(|existing| existing.0 < id.0); self.overview_order.insert(index, id); self.by_id.insert(id, window.clone()); self.ids.insert(window, id); @@ -92,7 +97,10 @@ impl WindowRegistry { /// Overview render and hit order, with the newest ID in front. Normal /// stacking changes do not affect this independent presentation order. pub fn overview_windows(&self) -> impl DoubleEndedIterator { - self.overview_order.iter().rev().filter_map(|id| self.by_id.get(id)) + self.overview_order + .iter() + .rev() + .filter_map(|id| self.by_id.get(id)) } pub fn window(&self, id: WindowId) -> Option<&W> { @@ -174,14 +182,21 @@ impl WindowRegistry { pub fn expire_transactions(&mut self, now: std::time::Duration) { for (id, record) in &mut self.records { - if record.resize.is_some_and(|transaction| transaction.expired(now)) { + if record + .resize + .is_some_and(|transaction| transaction.expired(now)) + { tracing::warn!(?id, "resize presentation deadline reached"); record.resize = None; } } } - pub fn update(&mut self, id: WindowId, update: impl FnOnce(&mut WindowRecord) -> R) -> Option { + pub fn update( + &mut self, + id: WindowId, + update: impl FnOnce(&mut WindowRecord) -> R, + ) -> Option { self.records.get_mut(&id).map(update) } @@ -205,14 +220,18 @@ impl WindowRegistry { pub fn take_column_width_requests(&mut self) -> HashSet { self.records .iter_mut() - .filter_map(|(id, record)| std::mem::take(&mut record.column_width_pending).then_some(*id)) + .filter_map(|(id, record)| { + std::mem::take(&mut record.column_width_pending).then_some(*id) + }) .collect() } } impl WindowRegistry { pub fn register_window(&mut self, window: Window, id: WindowId) { - let surface = window.toplevel().map(|toplevel| toplevel.wl_surface().clone()); + let surface = window + .toplevel() + .map(|toplevel| toplevel.wl_surface().clone()); self.register(window, id); if let Some(surface) = surface { @@ -228,7 +247,12 @@ impl WindowRegistry { }; let record = self.records.get_mut(&id).unwrap(); with_states(surface, |states| { - let attributes = states.data_map.get::().unwrap().lock().unwrap(); + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); let app_id = attributes.app_id.as_deref().unwrap_or_default(); let title = attributes.title.as_deref().unwrap_or_default(); let changed = record.app_id != app_id || record.title != title; @@ -292,7 +316,10 @@ mod tests { assert_eq!(std::rc::Rc::strong_count(&handle), 3); for _ in 0..100 { - assert!(std::rc::Rc::ptr_eq(windows.overview_windows().next().unwrap(), &handle)); + assert!(std::rc::Rc::ptr_eq( + windows.overview_windows().next().unwrap(), + &handle + )); assert_eq!(std::rc::Rc::strong_count(&handle), 3); } @@ -323,7 +350,11 @@ mod tests { assert_eq!(windows.remove(&"surface"), Some(id)); assert_eq!(windows.remove(&"surface"), None); assert!(windows.record(id).is_none()); - assert!(windows.update(id, |record| record.maximized = true).is_none()); + assert!( + windows + .update(id, |record| record.maximized = true) + .is_none() + ); windows.set_geometry(id, geometry); windows.set_transaction(id, resize); @@ -367,7 +398,10 @@ mod tests { windows.remove(&"first"); - assert_eq!(windows.take_column_width_requests(), HashSet::from([second])); + assert_eq!( + windows.take_column_width_requests(), + HashSet::from([second]) + ); assert!(windows.take_column_width_requests().is_empty()); assert!(windows.record(second).is_some()); } @@ -383,12 +417,25 @@ mod tests { windows.set_transaction(id, ResizeTransaction::new(serial.into(), now)); windows.expire_transactions(now); assert!(windows.transaction(&id).is_some()); - assert!(!windows.transaction(&id).unwrap().accepts(Some((serial - 1).into()))); + assert!( + !windows + .transaction(&id) + .unwrap() + .accepts(Some((serial - 1).into())) + ); } windows.expire_transactions(Duration::from_millis(300)); assert!(windows.transaction(&id).is_none()); - windows.set_transaction(id, ResizeTransaction::new(31.into(), Duration::from_millis(400))); - assert!(!windows.transaction(&id).unwrap().expired(Duration::from_millis(699))); + windows.set_transaction( + id, + ResizeTransaction::new(31.into(), Duration::from_millis(400)), + ); + assert!( + !windows + .transaction(&id) + .unwrap() + .expired(Duration::from_millis(699)) + ); windows.expire_transactions(Duration::from_millis(700)); assert!(windows.transaction(&id).is_none()); } @@ -405,13 +452,24 @@ mod tests { let geometry = WindowGeometry::new(Rect::new(0., 0., 800., 600.), None); windows.set_geometry(first, geometry); windows.set_transaction(first, ResizeTransaction::new(1.into(), Duration::ZERO)); - windows.set_transaction(second, ResizeTransaction::new(2.into(), Duration::from_millis(100))); + windows.set_transaction( + second, + ResizeTransaction::new(2.into(), Duration::from_millis(100)), + ); windows.expire_transactions(Duration::from_millis(300)); assert_eq!(windows.geometry(&first), Some(&geometry)); assert!(windows.transaction(&first).is_none()); - assert!(windows.transaction(&second).unwrap().accepts(Some(2.into()))); - assert_eq!(windows.resizing().copied().collect::>(), vec![second]); + assert!( + windows + .transaction(&second) + .unwrap() + .accepts(Some(2.into())) + ); + assert_eq!( + windows.resizing().copied().collect::>(), + vec![second] + ); } } diff --git a/compositor/src/state/windows.rs b/compositor/src/state/windows.rs index bc0d344..cf308d2 100644 --- a/compositor/src/state/windows.rs +++ b/compositor/src/state/windows.rs @@ -16,9 +16,16 @@ impl Ferese { } pub(crate) fn restore_initial_floating_size(&mut self, window: &Window) { - let Some(toplevel) = window.toplevel() else { return }; + let Some(toplevel) = window.toplevel() else { + return; + }; let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| { - let attributes = states.data_map.get::().unwrap().lock().unwrap(); + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); ( attributes.app_id.clone(), attributes.title.clone(), @@ -29,12 +36,19 @@ impl Ferese { pending.states.contains(xdg_toplevel::State::Fullscreen) || pending.states.contains(xdg_toplevel::State::Maximized) }); - let rule = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient); + let rule = resolve_window_rules( + &self.window_rules, + app_id.as_deref(), + title.as_deref(), + transient, + ); if crate::window_rules::is_native_dialog(app_id.as_deref()) || transient || special_mode || rule.fullscreen == Some(true) - || !rule.floating.unwrap_or(rule.width.is_some() || rule.height.is_some()) + || !rule + .floating + .unwrap_or(rule.width.is_some() || rule.height.is_some()) { return; } @@ -62,7 +76,8 @@ impl Ferese { Some((rule.width, rule.height)), ); let size = ClientSize::from_rect(rect); - toplevel.with_pending_state(|pending| pending.size = Some((size.width, size.height).into())); + toplevel + .with_pending_state(|pending| pending.size = Some((size.width, size.height).into())); } pub(crate) fn add_rule_placed_window( @@ -114,16 +129,22 @@ impl Ferese { let size_rect = client_size(&window) .map(|size| natural_floating_rect(bounds, size)) .unwrap_or_else(|| centered_floating_rect(bounds)); - let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None); + let rect = self.place_floating( + &window, + None, + bounds, + (size_rect.width, size_rect.height), + None, + ); let anchor = self .focused_window .and_then(|focused| self.presented_window_rect(focused)) .unwrap_or(bounds); let fullscreen = self.workspaces.active().fullscreen; let focus = true; - if let Err(error) = self - .workspaces - .insert_floating_window(id, self.workspaces.active_id(), rect, focus) + if let Err(error) = + self.workspaces + .insert_floating_window(id, self.workspaces.active_id(), rect, focus) { tracing::error!(%error, ?id, "failed to insert rule-placed floating window"); return; @@ -131,11 +152,16 @@ impl Ferese { self.windows.register_window(window.clone(), id); self.refresh_capture_privacy(); - self.windows.update(id, |w| w.placement_anchor = Some(anchor)); + self.windows + .update(id, |w| w.placement_anchor = Some(anchor)); - if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none() + if rule.width.is_none() + && rule.height.is_none() + && client_size(&window).is_none() + && remembered_work.is_none() { - self.windows.update(id, |w| w.natural_floating_pending = true); + self.windows + .update(id, |w| w.natural_floating_pending = true); } self.window_stack.insert(id); @@ -202,12 +228,22 @@ impl Ferese { let size_rect = client_size(&window) .map(|s| natural_floating_rect(bounds, s)) .unwrap_or_else(|| centered_transient_rect(visible_parent)); - let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None); + let rect = self.place_floating( + &window, + None, + bounds, + (size_rect.width, size_rect.height), + None, + ); let id = self.windows.allocate_id(); let focus = workspace == self.workspaces.active_id() - && (self.focused_window == Some(parent) || self.workspaces.active().fullscreen == Some(parent)); - if let Err(error) = self.workspaces.insert_floating_window(id, workspace, rect, focus) { + && (self.focused_window == Some(parent) + || self.workspaces.active().fullscreen == Some(parent)); + if let Err(error) = self + .workspaces + .insert_floating_window(id, workspace, rect, focus) + { tracing::error!(%error, ?id, ?parent, "failed to insert transient window"); return; } @@ -216,7 +252,8 @@ impl Ferese { self.refresh_capture_privacy(); self.window_stack.insert(id); if client_size(&window).is_none() { - self.windows.update(id, |w| w.natural_floating_pending = true); + self.windows + .update(id, |w| w.natural_floating_pending = true); } if self @@ -286,8 +323,10 @@ impl Ferese { ); ( ( - rule.min_width.map_or(minimum.0, |width| width.ceil() as i32), - rule.min_height.map_or(minimum.1, |height| height.ceil() as i32), + rule.min_width + .map_or(minimum.0, |width| width.ceil() as i32), + rule.min_height + .map_or(minimum.1, |height| height.ceil() as i32), ), maximum, ) @@ -303,15 +342,26 @@ impl Ferese { }; let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| { - let attributes = states.data_map.get::().unwrap().lock().unwrap(); + let attributes = states + .data_map + .get::() + .unwrap() + .lock() + .unwrap(); ( attributes.app_id.clone(), attributes.title.clone(), attributes.parent.is_some(), ) }); - let old = resolve_window_rules(old_rules, app_id.as_deref(), title.as_deref(), transient); - let new = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient); + let old = + resolve_window_rules(old_rules, app_id.as_deref(), title.as_deref(), transient); + let new = resolve_window_rules( + &self.window_rules, + app_id.as_deref(), + title.as_deref(), + transient, + ); minimums_changed |= old.min_width != new.min_width || old.min_height != new.min_height; if let Some(new) = crate::window_rules::live_result(old, new, transient) { self.apply_window_rule_result(&window, new); @@ -322,7 +372,11 @@ impl Ferese { } } - pub(super) fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) { + pub(super) fn apply_window_rule_result( + &mut self, + window: &Window, + rule: crate::window_rules::WindowRuleResult, + ) { let Some(id) = self.windows.ids().get(window).copied() else { return; }; @@ -347,8 +401,12 @@ impl Ferese { .workspaces .workspace_for_window(id) .and_then(|workspace| self.output_workspaces.output_for_workspace(workspace)) - && let Some(workspace) = self.output_workspaces.workspace_at(&self.workspaces, output, index) - && let Err(error) = self.workspaces.move_window_to_workspace(id, workspace, axis, 0.5) + && let Some(workspace) = + self.output_workspaces + .workspace_at(&self.workspaces, output, index) + && let Err(error) = self + .workspaces + .move_window_to_workspace(id, workspace, axis, 0.5) { tracing::warn!(%error, ?id, ?workspace, "failed to apply window workspace rule"); } @@ -359,7 +417,10 @@ impl Ferese { .floating .or((rule.width.is_some() || rule.height.is_some()).then_some(true)); if let Some(should_float) = should_float { - let is_floating = matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })); + let is_floating = matches!( + self.workspaces.placement(id), + Some(WindowPlacement::Floating { .. }) + ); let mut rect = centered_floating_rect(bounds); if should_float && rule.width.is_none() && rule.height.is_none() { if let Some(size) = client_size(window) { @@ -371,7 +432,8 @@ impl Ferese { .and_then(|entry| entry.restore(&self.floating_outputs())) .is_none() { - self.windows.update(id, |w| w.natural_floating_pending = true); + self.windows + .update(id, |w| w.natural_floating_pending = true); } } rect.width = rule.width.unwrap_or(rect.width).min(bounds.width); @@ -387,7 +449,9 @@ impl Ferese { } let result = if should_float != is_floating { - self.workspaces.toggle_floating(id, rect, axis, 0.5).map(|_| ()) + self.workspaces + .toggle_floating(id, rect, axis, 0.5) + .map(|_| ()) } else if should_float && (rule.width.is_some() || rule.height.is_some()) { self.workspaces.set_floating_rect(id, rect) } else { @@ -426,11 +490,18 @@ impl Ferese { self.invalidate_window_stacking(); } let placement = parent.and_then(|parent| { - if self.workspaces.workspace_for_window(id) != self.workspaces.workspace_for_window(parent) { - tracing::warn!(?id, ?parent, "ignored transient parent on another workspace"); + if self.workspaces.workspace_for_window(id) + != self.workspaces.workspace_for_window(parent) + { + tracing::warn!( + ?id, + ?parent, + "ignored transient parent on another workspace" + ); return None; } - self.floating_bounds_for_window(parent).map(|bounds| (parent, bounds)) + self.floating_bounds_for_window(parent) + .map(|bounds| (parent, bounds)) }); let Some((parent, bounds)) = placement else { if removed { @@ -444,7 +515,13 @@ impl Ferese { let size_rect = client_size(window) .map(|s| natural_floating_rect(bounds, s)) .unwrap_or_else(|| centered_transient_rect(visible_parent)); - let rect = self.place_floating(window, Some(id), bounds, (size_rect.width, size_rect.height), None); + let rect = self.place_floating( + window, + Some(id), + bounds, + (size_rect.width, size_rect.height), + None, + ); let result = match self.workspaces.placement(id) { Some(WindowPlacement::Tiled) => self .workspaces @@ -501,8 +578,11 @@ impl Ferese { self.refresh_capture_privacy(); self.focus_history.remove(id); #[cfg(feature = "resize-metrics")] - self.resize_metrics - .end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled); + self.resize_metrics.end( + id, + self.presentation_now(), + crate::resize_metrics::End::Cancelled, + ); self.render.remove_window(id); self.capture_render.remove_window(id); self.window_stack.remove(id); @@ -532,7 +612,9 @@ impl Ferese { }; let serial = window.toplevel().and_then(|toplevel| { - toplevel.with_cached_state(|state| state.last_acked.as_ref().map(|configure| configure.serial)) + toplevel.with_cached_state(|state| { + state.last_acked.as_ref().map(|configure| configure.serial) + }) }); if !transaction.accepts(serial) { return; @@ -547,7 +629,9 @@ impl Ferese { return; }; - let source_geometry = transaction.source_geometry().unwrap_or_else(|| window.geometry()); + let source_geometry = transaction + .source_geometry() + .unwrap_or_else(|| window.geometry()); let scale = output.current_scale().fractional_scale(); let source_size = source_geometry.size.to_physical_precise_round(scale); if self.render.snapshot(&id).is_some_and(|snapshot| { @@ -561,8 +645,17 @@ impl Ferese { return; } - let used: usize = self.render.snapshots().map(|snapshot| snapshot.bytes()).sum::() - + self.render.closing.iter().map(|window| window.bytes()).sum::(); + let used: usize = self + .render + .snapshots() + .map(|snapshot| snapshot.bytes()) + .sum::() + + self + .render + .closing + .iter() + .map(|window| window.bytes()) + .sum::(); let remaining = crate::presentation::SNAPSHOT_BUDGET.saturating_sub(used); let result = if let Some(backend) = &self.nested_backend { // Commit dispatch does not run inside the Winit event callback; @@ -584,7 +677,11 @@ impl Ferese { }; match result { Ok(Some(snapshot)) => { - tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff"); + tracing::debug!( + ?id, + bytes = snapshot.bytes(), + "captured native resize handoff" + ); self.render.set_snapshot(id, snapshot); } Ok(None) => {} @@ -601,7 +698,9 @@ impl Ferese { // Read the acknowledgement attached to this commit, not a newer ack // that arrived without its corresponding buffer. let committed_serial = window.toplevel().and_then(|toplevel| { - toplevel.with_cached_state(|state| state.last_acked.as_ref().map(|configure| configure.serial)) + toplevel.with_cached_state(|state| { + state.last_acked.as_ref().map(|configure| configure.serial) + }) }); if self .windows @@ -611,8 +710,11 @@ impl Ferese { #[cfg(feature = "resize-metrics")] { self.measure_resize_pauses(self.presentation_now()); - self.resize_metrics - .end(id, self.presentation_now(), crate::resize_metrics::End::Commit); + self.resize_metrics.end( + id, + self.presentation_now(), + crate::resize_metrics::End::Commit, + ); } self.windows.clear_transaction(&id); self.release_presentation_dependencies(); @@ -643,15 +745,24 @@ impl Ferese { .is_some_and(|workspace| workspace.fullscreen == Some(id)); let normal_geometry = !fullscreen && !self.windows.record(id).is_some_and(|w| w.maximized) - && self.windows.geometry(&id).is_none_or(|geometry| !geometry.is_zooming()); + && self + .windows + .geometry(&id) + .is_none_or(|geometry| !geometry.is_zooming()); let initial_floating = self .windows .update(id, |w| std::mem::take(&mut w.natural_floating_pending)) .unwrap_or(false) - || self.windows.record(id).is_some_and(|w| w.placement_anchor.is_some()); + || self + .windows + .record(id) + .is_some_and(|w| w.placement_anchor.is_some()); if initial_floating && normal_geometry - && matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. })) + && matches!( + self.workspaces.placement(id), + Some(WindowPlacement::Floating { .. }) + ) && let Some(bounds) = self.floating_bounds_for_window(id) { let rect = self.place_floating( @@ -665,10 +776,14 @@ impl Ferese { if self.workspaces.set_floating_rect(id, rect).is_ok() { // This is the first mapped client buffer, not a user resize. // Do not animate from the temporary placement box. - self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size))); + self.windows + .set_geometry(id, WindowGeometry::new(rect, Some(size))); #[cfg(feature = "resize-metrics")] - self.resize_metrics - .end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled); + self.resize_metrics.end( + id, + self.presentation_now(), + crate::resize_metrics::End::Cancelled, + ); self.windows.clear_transaction(&id); self.relayout(); } @@ -681,17 +796,23 @@ impl Ferese { let settled_configure = window.toplevel().is_some_and(|toplevel| { let committed = toplevel.with_cached_state(|state| state.last_acked.clone()); with_states(toplevel.wl_surface(), |states| { - states.data_map.get::().is_some_and(|data| { - let Ok(data) = data.lock() else { return false }; - floating_commit_is_current( - data.pending_configures().is_empty(), - committed.as_ref().map(|configure| configure.serial), - data.last_acked.as_ref().map(|configure| configure.serial), - committed - .as_ref() - .is_some_and(|configure| configure.state.states.contains(xdg_toplevel::State::Resizing)), - ) - }) + states + .data_map + .get::() + .is_some_and(|data| { + let Ok(data) = data.lock() else { return false }; + floating_commit_is_current( + data.pending_configures().is_empty(), + committed.as_ref().map(|configure| configure.serial), + data.last_acked.as_ref().map(|configure| configure.serial), + committed.as_ref().is_some_and(|configure| { + configure + .state + .states + .contains(xdg_toplevel::State::Resizing) + }), + ) + }) }) }); if settled_configure @@ -699,25 +820,45 @@ impl Ferese { && let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) && ClientSize::from_rect(rect) != size { - let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height)); - if let Some((left, top, anchor)) = self.windows.record(id).and_then(|w| w.resize_anchor.as_ref()) { - rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top); + let mut rect = Rect::new( + rect.x, + rect.y, + f64::from(size.width), + f64::from(size.height), + ); + if let Some((left, top, anchor)) = self + .windows + .record(id) + .and_then(|w| w.resize_anchor.as_ref()) + { + rect = + crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top); } else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) { rect = crate::floating::clamp_to_work(rect, work); } if self.workspaces.set_floating_rect(id, rect).is_ok() { - self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size))); + self.windows + .set_geometry(id, WindowGeometry::new(rect, Some(size))); #[cfg(feature = "resize-metrics")] - self.resize_metrics - .end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled); + self.resize_metrics.end( + id, + self.presentation_now(), + crate::resize_metrics::End::Cancelled, + ); self.windows.clear_transaction(&id); self.render.clear_snapshot(&id); self.relayout(); } } - if settled_configure && self.windows.update(id, |w| w.resize_anchor.take()).flatten().is_some() { + if settled_configure + && self + .windows + .update(id, |w| w.resize_anchor.take()) + .flatten() + .is_some() + { self.remember_floating(window); } @@ -726,7 +867,12 @@ impl Ferese { }; let matches_target = geometry.client.commit(size); - tracing::debug!(?id, ?size, matches_target, "recorded client geometry commit"); + tracing::debug!( + ?id, + ?size, + matches_target, + "recorded client geometry commit" + ); } pub fn close_focused_window(&mut self) { diff --git a/compositor/src/theme.rs b/compositor/src/theme.rs index a8ccc99..11f67b1 100644 --- a/compositor/src/theme.rs +++ b/compositor/src/theme.rs @@ -35,7 +35,10 @@ fn transition_progress(elapsed: Duration, duration: Duration) -> f64 { } } -pub(crate) fn resolve_candidate(document: &Document, directory: &Path) -> Result { +pub(crate) fn resolve_candidate( + document: &Document, + directory: &Path, +) -> Result { resolve_with_context( document, directory, @@ -53,7 +56,9 @@ pub(crate) fn preview(args: &Value) -> Result { let directory = args["directory"] .as_str() .map(PathBuf::from) - .or_else(|| crate::config::config_path().and_then(|path| path.parent().map(ToOwned::to_owned))) + .or_else(|| { + crate::config::config_path().and_then(|path| path.parent().map(ToOwned::to_owned)) + }) .unwrap_or_else(|| PathBuf::from(".")); let document = Document::parse(source).map_err(|e| e.to_string())?; let candidate = resolve_candidate(&document, &directory)?; @@ -169,7 +174,13 @@ impl Engine { self.active_files == files } - pub(crate) fn watch(&mut self, owner: u64, id: u64, since: u64, response: mpsc::SyncSender) { + pub(crate) fn watch( + &mut self, + owner: u64, + id: u64, + since: u64, + response: mpsc::SyncSender, + ) { if since != self.revision { let _ = response.try_send(Response::success(id, self.value())); } else { @@ -233,7 +244,10 @@ impl Engine { } for path in self.active_files.iter().chain(files) { let path = if let (Some(parent), Some(name)) = (path.parent(), path.file_name()) { - parent.canonicalize().unwrap_or_else(|_| parent.to_owned()).join(name) + parent + .canonicalize() + .unwrap_or_else(|_| parent.to_owned()) + .join(name) } else { path.clone() }; @@ -304,29 +318,35 @@ pub(crate) fn init( state: &mut Ferese, candidate: Candidate, ) -> Result<(), Box> { - let clock = unsafe { libc::timerfd_create(libc::CLOCK_REALTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) }; + let clock = unsafe { + libc::timerfd_create(libc::CLOCK_REALTIME, libc::TFD_CLOEXEC | libc::TFD_NONBLOCK) + }; if clock < 0 { return Err(io::Error::last_os_error().into()); } let clock = unsafe { OwnedFd::from_raw_fd(clock) }; let source = clock.try_clone()?; state.theme_engine.clock = Some(clock); - event_loop - .handle() - .insert_source(Generic::new(source, Interest::READ, PollMode::Level), |_, _, state| { + event_loop.handle().insert_source( + Generic::new(source, Interest::READ, PollMode::Level), + |_, _, state| { if let Some(clock) = &state.theme_engine.clock { let mut expirations = 0u64; unsafe { libc::read(clock.as_raw_fd(), (&mut expirations as *mut u64).cast(), 8) }; } state.refresh_theme(); Ok(PostAction::Continue) - })?; + }, + )?; let (sender, events) = mpsc::sync_channel(1); let paths = state.theme_engine.paths.clone(); if let Some(config) = crate::config::config_path() { paths.lock().unwrap().insert(config); } - paths.lock().unwrap().insert(PathBuf::from("/etc/localtime")); + paths + .lock() + .unwrap() + .insert(PathBuf::from("/etc/localtime")); let wakeup = state.loop_signal.clone(); let system_sender = sender.clone(); let system_wakeup = wakeup.clone(); @@ -355,13 +375,17 @@ pub(crate) fn init( .output() { if let Some(state) = SYSTEM_APPEARANCE.get() { - *state.lock().unwrap() = system_appearance_value(&String::from_utf8_lossy(&output.stdout)); + *state.lock().unwrap() = + system_appearance_value(&String::from_utf8_lossy(&output.stdout)); } let _ = system_sender.try_send(()); system_wakeup.wakeup(); } if let Some(output) = monitor.stdout.take() { - for line in std::io::BufReader::new(output).lines().map_while(Result::ok) { + for line in std::io::BufReader::new(output) + .lines() + .map_while(Result::ok) + { let value = system_appearance_value(&line); if let Some(state) = SYSTEM_APPEARANCE.get() { *state.lock().unwrap() = value; @@ -454,7 +478,8 @@ impl Ferese { self.poll_theme(); return; } - let wallpaper = (self.wallpaper.configuration() != &wallpaper).then(|| self.wallpaper.replacement(wallpaper)); + let wallpaper = (self.wallpaper.configuration() != &wallpaper) + .then(|| self.wallpaper.replacement(wallpaper)); self.theme_engine.pending = Some(Pending { candidate, wallpaper, @@ -469,7 +494,9 @@ impl Ferese { && let Some(wallpaper) = &mut pending.wallpaper { wallpaper.poll(); - if wallpaper.element(renderer, output).is_some() || wallpaper.configuration().path.is_none() { + if wallpaper.element(renderer, output).is_some() + || wallpaper.configuration().path.is_none() + { pending.ready.insert(output.clone()); } } @@ -481,7 +508,10 @@ impl Ferese { output: &Output, ) -> Option { let transition = self.theme_engine.transition.as_mut()?; - transition.previous_wallpaper.as_mut()?.element(renderer, output) + transition + .previous_wallpaper + .as_mut()? + .element(renderer, output) } pub(crate) fn theme_progress(&self) -> f32 { @@ -490,7 +520,10 @@ impl Ferese { .as_ref() .filter(|transition| transition.previous_wallpaper.is_some()) .map_or(1., |transition| { - transition_progress(transition.started.elapsed(), self.theme_transition_duration()) as f32 + transition_progress( + transition.started.elapsed(), + self.theme_transition_duration(), + ) as f32 }) } @@ -512,9 +545,15 @@ impl Ferese { if let Some(wallpaper) = &mut pending.wallpaper { wallpaper.poll(); } - let ready = - pending.wallpaper.is_none() || self.space.outputs().all(|output| pending.ready.contains(output)); - let failed = pending.wallpaper.as_ref().is_some_and(WallpaperState::failed); + let ready = pending.wallpaper.is_none() + || self + .space + .outputs() + .all(|output| pending.ready.contains(output)); + let failed = pending + .wallpaper + .as_ref() + .is_some_and(WallpaperState::failed); start = ready || failed || now >= pending.deadline; if start && !ready { pending.wallpaper = None; @@ -553,7 +592,8 @@ impl Ferese { let duration = self.theme_transition_duration(); if frame_due && let Some(transition) = &self.theme_engine.transition { self.theme_engine.last_frame = Some(now); - let progress = transition_progress(now.saturating_duration_since(transition.started), duration); + let progress = + transition_progress(now.saturating_duration_since(transition.started), duration); let target = &self.theme_engine.live.theme; let complete = progress >= 1.; let presented = if complete { @@ -561,7 +601,8 @@ impl Ferese { } else { transition.from.transition(target, progress) }; - self.theme_settings = Config::resolved_theme_settings(&presented).expect("validated resolved theme"); + self.theme_settings = + Config::resolved_theme_settings(&presented).expect("validated resolved theme"); self.theme_engine.live.presented = presented; self.theme_engine.publish(); @@ -574,20 +615,21 @@ impl Ferese { crate::backends::direct::render_all(self); } if !self.theme_engine.timer_pending { - let deadline = if self.theme_engine.transition.is_some() || self.theme_engine.pending.is_some() { - Some(now + Duration::from_millis(16)) - } else { - self.theme_engine.dirty - }; + let deadline = + if self.theme_engine.transition.is_some() || self.theme_engine.pending.is_some() { + Some(now + Duration::from_millis(16)) + } else { + self.theme_engine.dirty + }; if let Some(deadline) = deadline { self.theme_engine.timer_pending = true; - if let Err(error) = self - .loop_handle - .insert_source(Timer::from_deadline(deadline), |_, _, state| { - state.theme_engine.timer_pending = false; - state.poll_theme(); - TimeoutAction::Drop - }) + if let Err(error) = + self.loop_handle + .insert_source(Timer::from_deadline(deadline), |_, _, state| { + state.theme_engine.timer_pending = false; + state.poll_theme(); + TimeoutAction::Drop + }) { self.theme_engine.timer_pending = false; tracing::warn!(%error, "cannot schedule theme update"); @@ -600,7 +642,9 @@ impl Ferese { let path = crate::config::config_path().ok_or("No configuration directory")?; let original = crate::config::read_source(&path)?; let mut document = Document::parse(&original).map_err(|e| e.to_string())?; - document.set("theme.mode", json!(mode)).map_err(|e| e.to_string())?; + document + .set("theme.mode", json!(mode)) + .map_err(|e| e.to_string())?; let source = document.to_string(); crate::config::prepare(&source, path.parent().unwrap_or(Path::new(".")))?; let temporary = path.with_extension(format!("kdl.{}.tmp", std::process::id())); @@ -615,10 +659,13 @@ impl Ferese { file.set_permissions(metadata.permissions()) .map_err(|e| e.to_string())?; } - file.write_all(source.as_bytes()).map_err(|e| e.to_string())?; + file.write_all(source.as_bytes()) + .map_err(|e| e.to_string())?; file.sync_all().map_err(|e| e.to_string())?; if crate::config::read_source(&path)? != original { - return Err("Configuration changed while updating the mode; please try again".into()); + return Err( + "Configuration changed while updating the mode; please try again".into(), + ); } std::fs::rename(&temporary, &path).map_err(|e| e.to_string())?; Ok(()) @@ -647,7 +694,8 @@ mod tests { events: Some(events), ..Engine::default() }; - engine.watcher = Some(file_watcher(engine.paths.clone(), sender, event_loop.get_signal()).unwrap()); + engine.watcher = + Some(file_watcher(engine.paths.clone(), sender, event_loop.get_signal()).unwrap()); engine.watch_files(std::slice::from_ref(&path)); for source in ["// first save", "// second save"] { let temporary = path.with_extension("new"); @@ -655,7 +703,10 @@ mod tests { std::fs::rename(&temporary, &path).unwrap(); let deadline = Instant::now() + Duration::from_secs(3); while !engine.files_changed(Instant::now()) { - assert!(Instant::now() < deadline, "atomic save notification never settled"); + assert!( + Instant::now() < deadline, + "atomic save notification never settled" + ); std::thread::sleep(Duration::from_millis(10)); } assert_eq!(crate::config::read_source(&path).unwrap(), source); @@ -688,10 +739,18 @@ mod tests { #[test] fn startup_warning_survives_repeated_rejection_without_duplicate_publication() { let mut engine = Engine::default(); - engine.startup_failed(Some(Path::new("/config.kdl")), "unknown field group_surface".into()); + engine.startup_failed( + Some(Path::new("/config.kdl")), + "unknown field group_surface".into(), + ); engine.publish(); let initial = engine.value(); - assert!(initial["error"].as_str().unwrap().contains("default settings")); + assert!( + initial["error"] + .as_str() + .unwrap() + .contains("default settings") + ); assert!(initial["error"].as_str().unwrap().contains("/config.kdl")); for _ in 0..3 { engine.reject("unknown field group_surface".into()); @@ -701,7 +760,10 @@ mod tests { assert_eq!(engine.live.error.as_deref(), Some("different invalid edit")); assert_eq!(engine.revision, 2); engine.reject("unknown field group_surface".into()); - assert_eq!(engine.live.error.as_deref(), Some("unknown field group_surface")); + assert_eq!( + engine.live.error.as_deref(), + Some("unknown field group_surface") + ); assert_eq!(engine.revision, 3); } @@ -741,9 +803,18 @@ mod tests { #[test] fn theme_and_wallpaper_share_scaled_progress_and_instant_completion() { let elapsed = Duration::from_millis(50); - assert_eq!(transition_progress(elapsed, Duration::from_millis(200)), 0.25); - assert_eq!(transition_progress(elapsed, Duration::from_millis(100)), 0.5); - assert_eq!(transition_progress(elapsed, Duration::from_millis(400)), 0.125); + assert_eq!( + transition_progress(elapsed, Duration::from_millis(200)), + 0.25 + ); + assert_eq!( + transition_progress(elapsed, Duration::from_millis(100)), + 0.5 + ); + assert_eq!( + transition_progress(elapsed, Duration::from_millis(400)), + 0.125 + ); assert_eq!(transition_progress(Duration::ZERO, Duration::ZERO), 1.0); assert_eq!( transition_progress(Duration::from_secs(1), Duration::from_millis(200)), diff --git a/compositor/src/wallpaper.rs b/compositor/src/wallpaper.rs index d4578c6..911ebbd 100644 --- a/compositor/src/wallpaper.rs +++ b/compositor/src/wallpaper.rs @@ -93,7 +93,10 @@ impl WallpaperState { Self::with_wakeup(config, None) } - pub fn with_wakeup(config: WallpaperConfig, wakeup: Option) -> Self { + pub fn with_wakeup( + config: WallpaperConfig, + wakeup: Option, + ) -> Self { Self::with_decoder(config, wakeup, Decoder::new(pixels::load)) } @@ -165,7 +168,9 @@ impl WallpaperState { } pub fn reload(&mut self, config: WallpaperConfig) { - if self.config.path != config.path && config.path.as_ref().is_some_and(|path| !path.is_file()) { + if self.config.path != config.path + && config.path.as_ref().is_some_and(|path| !path.is_file()) + { tracing::warn!("new wallpaper is unavailable; retaining previous image"); return; } @@ -214,7 +219,9 @@ impl WallpaperState { } fn upload_ready(&self, context: &ErasedContextId, now: Instant) -> bool { - self.upload_retries.get(context).is_none_or(|retry| now >= *retry) + self.upload_retries + .get(context) + .is_none_or(|retry| now >= *retry) } pub(crate) fn take_retry_wakeup(&self) -> bool { @@ -323,7 +330,11 @@ impl WallpaperState { } } - pub fn element(&mut self, renderer: &mut GlesRenderer, output: &Output) -> Option { + pub fn element( + &mut self, + renderer: &mut GlesRenderer, + output: &Output, + ) -> Option { let context = renderer.context_id().erased(); if !self.textures.contains_key(&context) { @@ -341,7 +352,8 @@ impl WallpaperState { let size = Size::from((pixels.width() as i32, pixels.height() as i32)); let bytes = pixels.as_raw().len(); - let texture = renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false); + let texture = + renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false); (size, bytes, texture) }; @@ -372,7 +384,8 @@ impl WallpaperState { Err(error) => { // Keep pixels alive on failure so the retry does not require // another decode. - self.upload_retries.insert(context, Instant::now() + UPLOAD_RETRY_DELAY); + self.upload_retries + .insert(context, Instant::now() + UPLOAD_RETRY_DELAY); tracing::debug!(%error, "wallpaper texture import failed"); @@ -382,7 +395,9 @@ impl WallpaperState { } let cached = self.textures.get(&context)?; - let output_size = output.current_transform().transform_size(output.current_mode()?.size); + let output_size = output + .current_transform() + .transform_size(output.current_mode()?.size); let (geometry, source) = image_geometry(cached.size, output_size, self.mode); @@ -414,7 +429,11 @@ fn image_geometry( ( Rectangle::from_size(output), Rectangle::new( - ((f64::from(image.w) - width) / 2.0, (f64::from(image.h) - height) / 2.0).into(), + ( + (f64::from(image.w) - width) / 2.0, + (f64::from(image.h) - height) / 2.0, + ) + .into(), (width, height).into(), ), ) @@ -427,7 +446,10 @@ fn image_geometry( ) .into(); ( - Rectangle::new(((output.w - size.w) / 2, (output.h - size.h) / 2).into(), size), + Rectangle::new( + ((output.w - size.w) / 2, (output.h - size.h) / 2).into(), + size, + ), Rectangle::from_size(image.to_f64()), ) } @@ -484,7 +506,9 @@ mod tests { let expected = image::RgbaImage::from_pixel(3, 2, image::Rgba([32, 80, 160, 255])); for name in ["wallpaper-portal", "wallpaper.jpg"] { let path = directory.path().join(name); - expected.save_with_format(&path, image::ImageFormat::Png).unwrap(); + expected + .save_with_format(&path, image::ImageFormat::Png) + .unwrap(); let mut state = WallpaperState::new(WallpaperConfig { path: Some(path), mode: WallpaperMode::Fill, @@ -519,7 +543,9 @@ mod tests { state.arm_retry_timer(&event_loop.handle(), now).unwrap(); assert!(state.retry_timer_pending.get()); assert!(!state.take_retry_wakeup()); - event_loop.dispatch(Some(Duration::from_secs(1)), &mut ()).unwrap(); + event_loop + .dispatch(Some(Duration::from_secs(1)), &mut ()) + .unwrap(); assert!(!state.retry_timer_pending.get()); assert!( state.take_retry_wakeup(), @@ -527,7 +553,10 @@ mod tests { ); assert!(!state.take_retry_wakeup()); let after = Instant::now(); - assert_eq!(state.next_retry_deadline(after), Some(after + UPLOAD_RETRY_DELAY)); + assert_eq!( + state.next_retry_deadline(after), + Some(after + UPLOAD_RETRY_DELAY) + ); state.forget_context(&context); assert_eq!(state.next_retry_deadline(after), None); state.retry_wakeup.set(true); @@ -582,14 +611,18 @@ mod tests { mode: WallpaperMode::Fill, }); let now = Instant::now(); - state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY); + state + .upload_retries + .insert(context.clone(), now + UPLOAD_RETRY_DELAY); assert!(!state.upload_ready(&context, now)); assert!(state.upload_ready(&other, now)); assert!(state.upload_ready(&context, now + UPLOAD_RETRY_DELAY)); state.forget_context(&context); assert!(state.upload_ready(&context, now)); - state.upload_retries.insert(context.clone(), now + UPLOAD_RETRY_DELAY); + state + .upload_retries + .insert(context.clone(), now + UPLOAD_RETRY_DELAY); let (sender, receiver) = mpsc::channel(); state.receiver = Some(receiver); sender @@ -642,11 +675,16 @@ mod tests { #[test] fn fill_crops_and_fit_letterboxes_without_reallocating_image() { - let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fill); + let (geometry, source) = image_geometry( + (3840, 2160).into(), + (1600, 1000).into(), + WallpaperMode::Fill, + ); assert_eq!(geometry.size, (1600, 1000).into()); assert_eq!(source.size.h, 2160.0); assert!(source.loc.x > 0.0); - let (geometry, source) = image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit); + let (geometry, source) = + image_geometry((3840, 2160).into(), (1600, 1000).into(), WallpaperMode::Fit); assert_eq!(geometry.size, (1600, 900).into()); assert_eq!(geometry.loc, (0, 50).into()); assert_eq!(source.size, (3840.0, 2160.0).into()); diff --git a/compositor/src/wallpaper/decoder.rs b/compositor/src/wallpaper/decoder.rs index 2c14201..b6b3395 100644 --- a/compositor/src/wallpaper/decoder.rs +++ b/compositor/src/wallpaper/decoder.rs @@ -30,7 +30,9 @@ impl Decoder { loop { let (queue, ready) = &*worker; let mut queue = ready - .wait_while(queue.lock().unwrap(), |queue| !queue.closed && queue.pending.is_none()) + .wait_while(queue.lock().unwrap(), |queue| { + !queue.closed && queue.pending.is_none() + }) .unwrap(); if queue.closed { break; @@ -55,7 +57,11 @@ impl Decoder { ) -> mpsc::Receiver> { let (result, receiver) = mpsc::channel(); let (queue, ready) = &*self.shared; - queue.lock().unwrap().pending = Some(Request { path, result, wakeup }); + queue.lock().unwrap().pending = Some(Request { + path, + result, + wakeup, + }); ready.notify_one(); receiver } @@ -100,15 +106,26 @@ mod tests { let dropped_too = decoder.submit("third".into(), None); let latest = decoder.submit("latest".into(), None); assert!(events.try_recv().is_err()); - assert!(matches!(dropped.try_recv(), Err(mpsc::TryRecvError::Disconnected))); - assert!(matches!(dropped_too.try_recv(), Err(mpsc::TryRecvError::Disconnected))); + assert!(matches!( + dropped.try_recv(), + Err(mpsc::TryRecvError::Disconnected) + )); + assert!(matches!( + dropped_too.try_recv(), + Err(mpsc::TryRecvError::Disconnected) + )); release.send(()).unwrap(); assert!(first.recv_timeout(Duration::from_secs(2)).unwrap().is_err()); assert_eq!( events.recv_timeout(Duration::from_secs(2)).unwrap(), PathBuf::from("latest") ); - assert!(latest.recv_timeout(Duration::from_secs(2)).unwrap().is_err()); + assert!( + latest + .recv_timeout(Duration::from_secs(2)) + .unwrap() + .is_err() + ); assert!(events.try_recv().is_err()); } } diff --git a/compositor/src/wallpaper/pixels.rs b/compositor/src/wallpaper/pixels.rs index f706269..f75bf84 100644 --- a/compositor/src/wallpaper/pixels.rs +++ b/compositor/src/wallpaper/pixels.rs @@ -39,13 +39,17 @@ fn decode(path: &Path) -> Result { match decoder.color_type() { ColorType::Rgba8 => { let mut data = MmapMut::map_anon(bytes).map_err(|error| error.to_string())?; - decoder.read_image(&mut data).map_err(|error| error.to_string())?; + decoder + .read_image(&mut data) + .map_err(|error| error.to_string())?; mapped_image(width, height, data) } ColorType::Rgb8 => { let mut rgb = MmapMut::map_anon(bytes / 4 * 3).map_err(|error| error.to_string())?; - decoder.read_image(&mut rgb).map_err(|error| error.to_string())?; + decoder + .read_image(&mut rgb) + .map_err(|error| error.to_string())?; let mut rgba = MmapMut::map_anon(bytes).map_err(|error| error.to_string())?; for (source, target) in rgb.chunks_exact(3).zip(rgba.chunks_exact_mut(4)) { target[..3].copy_from_slice(source); @@ -56,7 +60,8 @@ fn decode(path: &Path) -> Result { } _ => { - let decoded = image::DynamicImage::from_decoder(decoder).map_err(|error| error.to_string())?; + let decoded = + image::DynamicImage::from_decoder(decoder).map_err(|error| error.to_string())?; from_rgba(decoded.into_rgba8()) } } @@ -73,7 +78,8 @@ fn rgba_bytes(width: u32, height: u32) -> Result { pub(super) fn from_rgba(image: image::RgbaImage) -> Result { let (width, height) = image.dimensions(); - let mut data = MmapMut::map_anon(rgba_bytes(width, height)?).map_err(|error| error.to_string())?; + let mut data = + MmapMut::map_anon(rgba_bytes(width, height)?).map_err(|error| error.to_string())?; data.copy_from_slice(image.as_raw()); mapped_image(width, height, data) } @@ -96,7 +102,11 @@ mod tests { DynamicImage::ImageLuma8(ImageBuffer::from_pixel(7, 5, Luma([81]))), DynamicImage::ImageLumaA8(ImageBuffer::from_pixel(7, 5, LumaA([81, 127]))), DynamicImage::ImageRgb16(ImageBuffer::from_pixel(7, 5, Rgb([8000, 21000, 41000]))), - DynamicImage::ImageRgba16(ImageBuffer::from_pixel(7, 5, Rgba([8000, 21000, 41000, 32000]))), + DynamicImage::ImageRgba16(ImageBuffer::from_pixel( + 7, + 5, + Rgba([8000, 21000, 41000, 32000]), + )), ]; for (index, image) in images.iter().enumerate() { let path = directory.path().join(format!("image-{index}.png")); @@ -153,7 +163,11 @@ mod tests { .output() .unwrap(); println!("{}", String::from_utf8_lossy(&output.stdout)); - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); return; } @@ -180,13 +194,21 @@ mod tests { "/../assets/wallpapers/ferese-wallpaper-light.png" )), ]; - drop(std::thread::spawn(move || load(paths[0]).unwrap()).join().unwrap()); + drop( + std::thread::spawn(move || load(paths[0]).unwrap()) + .join() + .unwrap(), + ); let baseline = rss_kib(); let mut current = None; for index in 0..20 { // Transfer ownership out of the decoder thread, as WallpaperState does. let path = paths[index % paths.len()]; - current = Some(std::thread::spawn(move || load(path).unwrap()).join().unwrap()); + current = Some( + std::thread::spawn(move || load(path).unwrap()) + .join() + .unwrap(), + ); } let with_wallpaper = rss_kib(); diff --git a/compositor/src/window_rules.rs b/compositor/src/window_rules.rs index 63b132c..b33d788 100644 --- a/compositor/src/window_rules.rs +++ b/compositor/src/window_rules.rs @@ -90,7 +90,12 @@ pub fn validate(configured: &[WindowRuleConfig]) -> Result, Stri .collect() } -pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, transient: bool) -> WindowRuleResult { +pub fn resolve( + rules: &[WindowRule], + app_id: Option<&str>, + title: Option<&str>, + transient: bool, +) -> WindowRuleResult { let app_id = app_id.map(normalize_app_id); let mut result = WindowRuleResult::default(); if is_native_dialog(app_id.as_deref()) { @@ -106,7 +111,10 @@ pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, .app_id .as_deref() .is_none_or(|expected| app_id.as_deref() == Some(expected)); - let title_matches = rule.title.as_deref().is_none_or(|expected| title == Some(expected)); + let title_matches = rule + .title + .as_deref() + .is_none_or(|expected| title == Some(expected)); let transient_matches = rule.transient.is_none_or(|expected| expected == transient); if !app_id_matches || !title_matches || !transient_matches { continue; @@ -119,7 +127,9 @@ pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, result.min_width = rule.min_width.or(result.min_width); result.min_height = rule.min_height.or(result.min_height); result.fullscreen = rule.fullscreen.or(result.fullscreen); - result.block_out_from_screencasts = rule.block_out_from_screencasts.or(result.block_out_from_screencasts); + result.block_out_from_screencasts = rule + .block_out_from_screencasts + .or(result.block_out_from_screencasts); result.idle_inhibit = rule.idle_inhibit.or(result.idle_inhibit); } @@ -128,10 +138,14 @@ pub fn resolve(rules: &[WindowRule], app_id: Option<&str>, title: Option<&str>, fn validate_rule(index: usize, rule: &WindowRuleConfig) -> Result { if rule.app_id.is_none() && rule.title.is_none() && rule.transient.is_none() { - return Err(format!("window_rules[{index}] must match app_id, title, or transient")); + return Err(format!( + "window_rules[{index}] must match app_id, title, or transient" + )); } if rule.workspace == Some(0) { - return Err(format!("window_rules[{index}].workspace must be greater than zero")); + return Err(format!( + "window_rules[{index}].workspace must be greater than zero" + )); } let app_id = rule @@ -178,7 +192,11 @@ fn nonempty_matcher(value: &str) -> Result<&str, &'static str> { } } -fn positive_dimension(value: Option, index: usize, field: &'static str) -> Result, String> { +fn positive_dimension( + value: Option, + index: usize, + field: &'static str, +) -> Result, String> { if value.is_none_or(|value| value.is_finite() && value > 0.0) { Ok(value) } else { @@ -231,7 +249,10 @@ mod tests { #[test] fn capture_privacy_defaults_overrides_and_placement_are_independent() { - for app_id in ["dev.ferese.Authentication", "dev.ferese.Authentication.desktop"] { + for app_id in [ + "dev.ferese.Authentication", + "dev.ferese.Authentication.desktop", + ] { assert_eq!( resolve(&[], Some(app_id), None, false).block_out_from_screencasts, Some(true) @@ -297,9 +318,15 @@ mod tests { ..config(app_id) }]) .unwrap(); - assert_eq!(resolve(&rules, Some(app_id), None, false).floating, Some(false)); + assert_eq!( + resolve(&rules, Some(app_id), None, false).floating, + Some(false) + ); } - assert_eq!(resolve(&[], Some("org.example.Editor"), None, false).floating, None); + assert_eq!( + resolve(&[], Some("org.example.Editor"), None, false).floating, + None + ); } #[test] @@ -417,7 +444,9 @@ mod tests { assert_eq!(removed.floating, Some(false)); assert_eq!(removed.fullscreen, Some(false)); assert_eq!( - super::live_result(old, Default::default(), true).unwrap().floating, + super::live_result(old, Default::default(), true) + .unwrap() + .floating, Some(true) ); let new = super::WindowRuleResult { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 2db693e..1bb497a 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -41,7 +41,11 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() state.dmabuf_imports.register(dmabuf_global); let initial_scale = normalized_scale(backend.scale_factor()); let mode = Mode { - size: if drawable.get() { initial_size } else { (1, 1).into() }, + size: if drawable.get() { + initial_size + } else { + (1, 1).into() + }, refresh: backend .window() .current_monitor() @@ -333,16 +337,24 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output) { .get(*window) .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) { - window.send_frame(output, time, None, |surface, _| eligible.get(&surface.into()).cloned()); + window.send_frame(output, time, None, |surface, _| { + eligible.get(&surface.into()).cloned() + }); } for layer in layer_surfaces(output) { - layer.send_frame(output, time, None, |surface, _| eligible.get(&surface.into()).cloned()); + layer.send_frame(output, time, None, |surface, _| { + eligible.get(&surface.into()).cloned() + }); } state.send_cursor_frame(output); } fn normalized_scale(scale: f64) -> f64 { - if scale.is_finite() && scale > 0.0 { scale } else { 1.0 } + if scale.is_finite() && scale > 0.0 { + scale + } else { + 1.0 + } } #[cfg(test)] @@ -351,7 +363,9 @@ mod tests { #[test] fn zero_host_resize_preserves_geometry_and_recovers() { - if !crate::startup_tests::private_runtime("winit::tests::zero_host_resize_preserves_geometry_and_recovers") { + if !crate::startup_tests::private_runtime( + "winit::tests::zero_host_resize_preserves_geometry_and_recovers", + ) { return; } @@ -437,7 +451,9 @@ mod tests { .collect::>(), ) .unwrap(); - events.dispatch(Duration::from_millis(1), &mut state).unwrap(); + events + .dispatch(Duration::from_millis(1), &mut state) + .unwrap(); state.display_handle.flush_clients().unwrap(); wire.set_nonblocking(true).unwrap(); let mut globals = Vec::new(); @@ -471,7 +487,8 @@ mod tests { let mut events = EventLoop::try_new().unwrap(); let mut state = crate::startup_tests::state(&mut events); - let display = unsafe { EGLDisplay::new(EGLDevice::enumerate().unwrap().last().unwrap()).unwrap() }; + let display = + unsafe { EGLDisplay::new(EGLDevice::enumerate().unwrap().last().unwrap()).unwrap() }; let context = EGLContext::new(&display).unwrap(); let mut renderer = unsafe { GlesRenderer::new(context).unwrap() }; let output = Output::new( @@ -485,8 +502,12 @@ mod tests { }, ); let size: Size = (32, 24).into(); - let mut displayed: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (32, 24).into()).unwrap(); - let mut offscreen: GlesTexture = renderer.create_buffer(Fourcc::Abgr8888, (32, 24).into()).unwrap(); + let mut displayed: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (32, 24).into()) + .unwrap(); + let mut offscreen: GlesTexture = renderer + .create_buffer(Fourcc::Abgr8888, (32, 24).into()) + .unwrap(); let damage = [Rectangle::from_size(size)]; let mut framebuffer = renderer.bind(&mut displayed).unwrap(); for changed_binding in [false, true] { @@ -508,15 +529,20 @@ mod tests { let _target = renderer.bind(&mut offscreen).unwrap(); } assert!( - crate::backends::direct::capture::capture_request(&mut state, &output, false, || { - Err::<(), _>("injected offscreen allocation/render failure") - }) + crate::backends::direct::capture::capture_request( + &mut state, + &output, + false, + || { Err::<(), _>("injected offscreen allocation/render failure") } + ) .is_none() ); true }, |renderer, framebuffer| { - let _ = renderer.render(framebuffer, size, Transform::Normal)?.finish()?; + let _ = renderer + .render(framebuffer, size, Transform::Normal)? + .finish()?; Ok::<_, GlesError>(()) }, ) @@ -524,13 +550,24 @@ mod tests { assert!(receiver.try_recv().unwrap().result.is_err()); assert!(receiver.try_recv().is_err()); assert!(state.pending_screencopies.is_empty()); - let mut frame = renderer.render(&mut framebuffer, size, Transform::Normal).unwrap(); - frame.clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &damage).unwrap(); + let mut frame = renderer + .render(&mut framebuffer, size, Transform::Normal) + .unwrap(); + frame + .clear(Color32F::new(0.0, 0.0, 1.0, 1.0), &damage) + .unwrap(); frame.finish().unwrap().wait().unwrap(); let pixels = renderer - .copy_framebuffer(&framebuffer, Rectangle::from_size((32, 24).into()), Fourcc::Abgr8888) + .copy_framebuffer( + &framebuffer, + Rectangle::from_size((32, 24).into()), + Fourcc::Abgr8888, + ) .unwrap(); - assert_eq!(&renderer.map_texture(&pixels).unwrap()[..4], &[0, 0, 255, 255]); + assert_eq!( + &renderer.map_texture(&pixels).unwrap()[..4], + &[0, 0, 255, 255] + ); } } diff --git a/compositor/src/xwayland/auth.rs b/compositor/src/xwayland/auth.rs index 3b9bcaf..15007fc 100644 --- a/compositor/src/xwayland/auth.rs +++ b/compositor/src/xwayland/auth.rs @@ -17,7 +17,13 @@ fn random_cookie() -> io::Result<[u8; 16]> { let mut cookie = [0_u8; 16]; let mut filled = 0; while filled < cookie.len() { - let count = unsafe { libc::getrandom(cookie[filled..].as_mut_ptr().cast(), cookie.len() - filled, 0) }; + let count = unsafe { + libc::getrandom( + cookie[filled..].as_mut_ptr().cast(), + cookie.len() - filled, + 0, + ) + }; if count < 0 { let error = io::Error::last_os_error(); if error.kind() == io::ErrorKind::Interrupted { @@ -26,7 +32,10 @@ fn random_cookie() -> io::Result<[u8; 16]> { return Err(error); } if count == 0 { - return Err(io::Error::new(io::ErrorKind::UnexpectedEof, "getrandom returned zero")); + return Err(io::Error::new( + io::ErrorKind::UnexpectedEof, + "getrandom returned zero", + )); } filled += count as usize; } @@ -72,7 +81,8 @@ impl AuthorityFile { let mut file = tempfile::Builder::new() .prefix("ferese-xauth-") .tempfile_in(runtime_directory)?; - file.as_file().set_permissions(fs::Permissions::from_mode(0o600))?; + file.as_file() + .set_permissions(fs::Permissions::from_mode(0o600))?; let cookie = random_cookie()?; let host = hostname()?; @@ -159,7 +169,10 @@ mod tests { let file = AuthorityFile::create(&runtime, 3).unwrap(); let path = file.path().to_owned(); let mode = fs::metadata(&path).unwrap().mode() & 0o777; - assert_eq!(mode, 0o600, "authority file must not be group/world readable"); + assert_eq!( + mode, 0o600, + "authority file must not be group/world readable" + ); path }; @@ -168,7 +181,8 @@ mod tests { #[test] fn a_group_readable_runtime_directory_is_rejected() { - let runtime = std::env::temp_dir().join(format!("ferese-xauth-loose-{}", std::process::id())); + let runtime = + std::env::temp_dir().join(format!("ferese-xauth-loose-{}", std::process::id())); let _ = fs::remove_dir_all(&runtime); fs::create_dir_all(&runtime).unwrap(); fs::set_permissions(&runtime, fs::Permissions::from_mode(0o755)).unwrap(); @@ -204,11 +218,17 @@ mod tests { .lines() .find(|line| line.contains("MIT-MAGIC-COOKIE-1")) .unwrap_or_else(|| panic!("xauth did not list the cookie record: {listing:?}")); - assert!(entry.contains(":91"), "entry must reference the display: {entry:?}"); + assert!( + entry.contains(":91"), + "entry must reference the display: {entry:?}" + ); let cookie_hex = entry.split_whitespace().last().expect("cookie field"); assert_eq!(cookie_hex.len(), 32, "16-byte cookie as hex: {cookie_hex}"); - assert!(cookie_hex.chars().any(|c| c != '0'), "cookie must not be zero"); + assert!( + cookie_hex.chars().any(|c| c != '0'), + "cookie must not be zero" + ); } #[test] diff --git a/compositor/src/xwayland/child.rs b/compositor/src/xwayland/child.rs index 36c2143..a93770b 100644 --- a/compositor/src/xwayland/child.rs +++ b/compositor/src/xwayland/child.rs @@ -60,7 +60,14 @@ pub(crate) fn spawn_satellite( )); } - satellite_command(executable, environment, reservation, authority, notify_socket).spawn() + satellite_command( + executable, + environment, + reservation, + authority, + notify_socket, + ) + .spawn() } fn satellite_command( @@ -70,7 +77,11 @@ fn satellite_command( authority: &Path, notify_socket: Option<&Path>, ) -> std::process::Command { - let inherited: Vec<_> = reservation.listeners().iter().map(AsRawFd::as_raw_fd).collect(); + let inherited: Vec<_> = reservation + .listeners() + .iter() + .map(AsRawFd::as_raw_fd) + .collect(); if inherited.is_empty() { panic!("cannot build a Satellite command without reserved listening sockets"); } @@ -260,7 +271,10 @@ impl OwnedProcessGroup { self.reaped } - pub fn terminate(&mut self, grace: std::time::Duration) -> Result, String> { + pub fn terminate( + &mut self, + grace: std::time::Duration, + ) -> Result, String> { self.signal_group(libc::SIGTERM); let deadline = std::time::Instant::now() + grace; @@ -371,7 +385,11 @@ mod tests { .unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::InvalidInput); - assert!(error.to_string().contains("without reserved listening sockets")); + assert!( + error + .to_string() + .contains("without reserved listening sockets") + ); } #[test] @@ -397,10 +415,15 @@ mod tests { assert!(!process.group_is_empty()); process.signal_group(libc::SIGTERM); - let status = process.terminate(Duration::from_secs(5)).expect("terminate"); + let status = process + .terminate(Duration::from_secs(5)) + .expect("terminate"); assert!(status.is_some()); - assert!(process.group_is_empty(), "a reaped group has no members left"); + assert!( + process.group_is_empty(), + "a reaped group has no members left" + ); assert_eq!(process.cleanup_error(), None); } @@ -423,7 +446,9 @@ mod tests { "the surviving descendant keeps the group non-empty" ); - process.terminate(Duration::from_secs(5)).expect("terminate"); + process + .terminate(Duration::from_secs(5)) + .expect("terminate"); assert!( process.group_is_empty(), @@ -463,7 +488,10 @@ mod tests { .terminate(Duration::from_secs(5)) .expect("terminating a reaped owner must not fail"); - assert_eq!(status, None, "there is no leader left to report a status for"); + assert_eq!( + status, None, + "there is no leader left to report a status for" + ); assert_eq!( test_hook::group_signals(), before, @@ -537,9 +565,17 @@ mod tests { let key = key.to_string_lossy(); if matches!( key.as_ref(), - "LISTEN_PID" | "LISTEN_FDS" | "LISTEN_FDNAMES" | "WATCHDOG_PID" | "WATCHDOG_USEC" | "NOTIFY_SOCKET" + "LISTEN_PID" + | "LISTEN_FDS" + | "LISTEN_FDNAMES" + | "WATCHDOG_PID" + | "WATCHDOG_USEC" + | "NOTIFY_SOCKET" ) { - assert_eq!(value, None, "{key} must not be inherited from the launching service"); + assert_eq!( + value, None, + "{key} must not be inherited from the launching service" + ); } if key.as_ref() == "WAYLAND_SOCKET" { assert_eq!( diff --git a/compositor/src/xwayland/integration_tests.rs b/compositor/src/xwayland/integration_tests.rs index bcfce10..6862266 100644 --- a/compositor/src/xwayland/integration_tests.rs +++ b/compositor/src/xwayland/integration_tests.rs @@ -46,18 +46,22 @@ impl Fixture { .expect("make the runtime directory private"); if production_inputs { - crate::xwayland::test_hooks::allocator_root_in(PathBuf::from("/tmp/.X11-unix"), PathBuf::from("/tmp")); + crate::xwayland::test_hooks::allocator_root_in( + PathBuf::from("/tmp/.X11-unix"), + PathBuf::from("/tmp"), + ); } else { // Owned inputs, so this fixture never allocates a real slot in `/tmp/.X11-unix`. let allocator = directory.join("x11"); fs::create_dir_all(&allocator).expect("create the allocator directory"); - fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)).expect("make the allocator private"); + fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)) + .expect("make the allocator private"); crate::xwayland::test_hooks::allocator_root(allocator); } let reservation = Reservation::allocate().expect("reserve a display"); - let authority = - AuthorityFile::create(&runtime_directory, reservation.display_number()).expect("create an authority file"); + let authority = AuthorityFile::create(&runtime_directory, reservation.display_number()) + .expect("create an authority file"); Self { record: directory.join("starts.jsonl"), @@ -70,8 +74,13 @@ impl Fixture { } fn script(&self, assignments: &[(&str, &str)]) -> &Self { - let fake = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fake-xwayland-satellite.py"); - assert!(fake.exists(), "the fake Satellite must exist at {}", fake.display()); + let fake = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../scripts/tests/fake-xwayland-satellite.py"); + assert!( + fake.exists(), + "the fake Satellite must exist at {}", + fake.display() + ); let record = self.record.display(); let fake = fake.display(); @@ -84,7 +93,8 @@ impl Fixture { } body.push_str(&format!("exec '{fake}' \"$@\"\n")); fs::write(&self.wrapper, body).expect("write the wrapper script"); - fs::set_permissions(&self.wrapper, fs::Permissions::from_mode(0o755)).expect("make the wrapper executable"); + fs::set_permissions(&self.wrapper, fs::Permissions::from_mode(0o755)) + .expect("make the wrapper executable"); self } @@ -113,7 +123,8 @@ impl Fixture { } fn spawn(&self, generation: u64) -> (Child, ReadinessSocket) { - let socket = ReadinessSocket::create(&self.runtime_directory, generation).expect("create a readiness socket"); + let socket = ReadinessSocket::create(&self.runtime_directory, generation) + .expect("create a readiness socket"); let notify = socket.path().to_owned(); let spawned = child::spawn_satellite( &self.wrapper, @@ -127,11 +138,13 @@ impl Fixture { } fn spawn_real(&self, generation: u64, executable: &Path) -> (Child, ReadinessSocket) { - let socket = ReadinessSocket::create(&self.runtime_directory, generation).expect("create a readiness socket"); + let socket = ReadinessSocket::create(&self.runtime_directory, generation) + .expect("create a readiness socket"); let notify = socket.path().to_owned(); let mut environment = self.environment(); - environment.wayland_display = std::env::var_os("WAYLAND_DISPLAY").expect("a live Wayland session is required"); + environment.wayland_display = + std::env::var_os("WAYLAND_DISPLAY").expect("a live Wayland session is required"); let spawned = child::spawn_satellite( executable, &environment, @@ -167,7 +180,8 @@ fn run_isolated(test_path: &str) -> bool { )); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the isolated session directory"); - fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)).expect("make the session private"); + fs::set_permissions(&directory, fs::Permissions::from_mode(0o700)) + .expect("make the session private"); let status = Command::new(std::env::current_exe().expect("the test binary")) .args(["--exact", test_path, "--nocapture"]) @@ -177,7 +191,11 @@ fn run_isolated(test_path: &str) -> bool { .env_remove("FERESE_SOCKET") .output() .expect("re-exec the test"); - assert!(status.status.success(), "{}", String::from_utf8_lossy(&status.stderr)); + assert!( + status.status.success(), + "{}", + String::from_utf8_lossy(&status.stderr) + ); false } @@ -239,7 +257,10 @@ fn satellite_receives_exactly_the_owned_listeners_and_no_extra_descriptors() { let record = fixture.starts().remove(0); assert_eq!(record["display"], fixture.reservation.display_name()); - assert_eq!(record["authority"], fixture.authority.path().to_str().unwrap()); + assert_eq!( + record["authority"], + fixture.authority.path().to_str().unwrap() + ); let listenfds = record["listenfds"].as_array().expect("listenfds array"); assert_eq!(listenfds.len(), fixture.reservation.listeners().len()); @@ -304,11 +325,17 @@ fn satellite_gets_the_session_environment_and_never_a_private_socket() { let environment = &record["env"]; assert_eq!(environment["DISPLAY"], fixture.reservation.display_name()); - assert_eq!(environment["XAUTHORITY"], fixture.authority.path().to_str().unwrap()); + assert_eq!( + environment["XAUTHORITY"], + fixture.authority.path().to_str().unwrap() + ); assert_eq!(environment["WAYLAND_DISPLAY"], "wayland-it"); assert_eq!(environment["WAYLAND_SOCKET"], serde_json::Value::Null); - assert_eq!(environment["FERESE_SHELL_CONTROL_SOCKET"], serde_json::Value::Null); + assert_eq!( + environment["FERESE_SHELL_CONTROL_SOCKET"], + serde_json::Value::Null + ); reap(satellite); } @@ -329,10 +356,16 @@ fn a_valid_notification_from_the_service_pid_satisfies_readiness() { #[test] fn a_delayed_notification_is_still_accepted() { let fixture = Fixture::new("ready-delay"); - fixture.script(&[("FAKE_SATELLITE_READY", "1"), ("FAKE_SATELLITE_READY_DELAY", "0.3")]); + fixture.script(&[ + ("FAKE_SATELLITE_READY", "1"), + ("FAKE_SATELLITE_READY_DELAY", "0.3"), + ]); let (satellite, socket) = fixture.spawn(1); let pid = satellite.id(); - assert!(read_ready(&socket, pid), "a delayed READY=1 must be accepted"); + assert!( + read_ready(&socket, pid), + "a delayed READY=1 must be accepted" + ); reap(satellite); } @@ -343,7 +376,10 @@ fn a_notification_from_a_child_pid_is_rejected() { let (satellite, socket) = fixture.spawn(1); let pid = satellite.id(); - assert!(!read_ready(&socket, pid), "a foreign PID's READY=1 must be rejected"); + assert!( + !read_ready(&socket, pid), + "a foreign PID's READY=1 must be rejected" + ); reap(satellite); } @@ -354,7 +390,10 @@ fn malformed_and_truncated_notifications_are_rejected() { fixture.script(&[("FAKE_SATELLITE_READY", payload)]); let (satellite, socket) = fixture.spawn(1); let pid = satellite.id(); - assert!(!read_ready(&socket, pid), "{payload} must not satisfy readiness"); + assert!( + !read_ready(&socket, pid), + "{payload} must not satisfy readiness" + ); reap(satellite); } } @@ -368,7 +407,10 @@ fn a_service_that_exits_before_readiness_leaves_no_running_process() { ]); let (mut satellite, socket) = fixture.spawn(1); let pid = satellite.id(); - assert!(!read_ready(&socket, pid), "an early exit must not satisfy readiness"); + assert!( + !read_ready(&socket, pid), + "an early exit must not satisfy readiness" + ); let status = wait_until_child(&mut satellite); assert_eq!(status.code(), Some(17)); assert!( @@ -404,7 +446,10 @@ fn shutdown_cleans_the_whole_owned_process_group() { ]); let (satellite, socket) = fixture.spawn(1); let pid = satellite.id(); - assert!(read_ready(&socket, pid), "the group fixture must reach readiness first"); + assert!( + read_ready(&socket, pid), + "the group fixture must reach readiness first" + ); let group_children = process::group_members(pid); assert!( @@ -414,10 +459,15 @@ fn shutdown_cleans_the_whole_owned_process_group() { // Ownership, not liveness: the group must hold the endpoints it was handed. let inodes = listener_inodes(&fixture.reservation); - assert!(!inodes.is_empty(), "the reserved listeners must be identifiable"); + assert!( + !inodes.is_empty(), + "the reserved listeners must be identifiable" + ); let holders_before = process::socket_holders(&inodes); assert!( - holders_before.iter().any(|holder| *holder != std::process::id()), + holders_before + .iter() + .any(|holder| *holder != std::process::id()), "a service descendant must hold the X11 listeners before shutdown: {holders_before:?}" ); @@ -447,7 +497,8 @@ fn listener_inodes(reservation: &Reservation) -> Vec { .listeners() .iter() .filter_map(|listener| { - let target = std::fs::read_link(format!("/proc/self/fd/{}", listener.as_raw_fd())).ok()?; + let target = + std::fs::read_link(format!("/proc/self/fd/{}", listener.as_raw_fd())).ok()?; let text = target.to_string_lossy().into_owned(); text.strip_prefix("socket:[") .and_then(|rest| rest.strip_suffix(']')) @@ -478,7 +529,10 @@ mod process { continue; }; let text = target.to_string_lossy(); - let Some(number) = text.strip_prefix("socket:[").and_then(|rest| rest.strip_suffix(']')) else { + let Some(number) = text + .strip_prefix("socket:[") + .and_then(|rest| rest.strip_suffix(']')) + else { continue; }; if inodes.iter().any(|inode| inode == number) { @@ -541,7 +595,10 @@ fn real_satellite_reports_verified_readiness_and_serves_authorized_x_clients() { } } let elapsed = started.elapsed(); - assert!(accepted, "the installed Satellite never sent a usable READY=1"); + assert!( + accepted, + "the installed Satellite never sent a usable READY=1" + ); println!("real Satellite reported readiness in {elapsed:?} (production budget 10s)"); assert!( elapsed <= Duration::from_secs(10), @@ -578,7 +635,10 @@ fn real_satellite_reports_verified_readiness_and_serves_authorized_x_clients() { group.group_is_empty(), "the real service group must be empty after shutdown" ); - assert!(wait_until(|| !process_exists(pid)), "the real Satellite must be gone"); + assert!( + wait_until(|| !process_exists(pid)), + "the real Satellite must be gone" + ); } /// Everything the live smoke test cannot supply for itself, declared up front. @@ -598,17 +658,22 @@ fn live_dependencies() -> PathBuf { } let on_path = |program: &str| { - std::env::var_os("PATH") - .is_some_and(|paths| std::env::split_paths(&paths).any(|directory| directory.join(program).is_file())) + std::env::var_os("PATH").is_some_and(|paths| { + std::env::split_paths(&paths).any(|directory| directory.join(program).is_file()) + }) }; if !on_path("xdpyinfo") { - panic!("dependency missing: xdpyinfo (x11-utils) is required to prove an X client can connect"); + panic!( + "dependency missing: xdpyinfo (x11-utils) is required to prove an X client can connect" + ); } match fs::symlink_metadata("/tmp/.X11-unix") { Ok(metadata) if metadata.is_dir() => {} Ok(_) => panic!("dependency missing: /tmp/.X11-unix exists but is not a directory"), - Err(error) => panic!("dependency missing: the shared X11 socket directory /tmp/.X11-unix: {error}"), + Err(error) => { + panic!("dependency missing: the shared X11 socket directory /tmp/.X11-unix: {error}") + } } executable @@ -626,7 +691,8 @@ mod manager_loop { use smithay::reexports::wayland_server::Display; fn wrapper(directory: &Path, assignments: &[(&str, &str)]) -> PathBuf { - let fake = Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fake-xwayland-satellite.py"); + let fake = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../scripts/tests/fake-xwayland-satellite.py"); let script = directory.join("fake-satellite"); let mut body = String::from("#!/bin/sh\n"); for (key, value) in assignments { @@ -634,7 +700,8 @@ mod manager_loop { } body.push_str(&format!("exec '{}' \"$@\"\n", fake.display())); fs::write(&script, body).expect("write the wrapper"); - fs::set_permissions(&script, fs::Permissions::from_mode(0o755)).expect("make the wrapper executable"); + fs::set_permissions(&script, fs::Permissions::from_mode(0o755)) + .expect("make the wrapper executable"); script } @@ -646,11 +713,13 @@ mod manager_loop { /// A `Ferese` wired to a real event loop, ready to run the manager. fn session(directory: &Path, path: PathBuf) -> (EventLoop<'static, Ferese>, Ferese) { - let (_, runtime, _) = crate::config::prepare("xwayland { }", directory).expect("prepare configuration"); + let (_, runtime, _) = + crate::config::prepare("xwayland { }", directory).expect("prepare configuration"); // Owned inputs, so this test never competes for a real display slot. let allocator = directory.join("x11"); fs::create_dir_all(&allocator).expect("create the allocator directory"); - fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)).expect("make the allocator private"); + fs::set_permissions(&allocator, fs::Permissions::from_mode(0o700)) + .expect("make the allocator private"); test_hooks::allocator_root(allocator); let mut event_loop = EventLoop::try_new().expect("create the event loop"); let display = Display::new().expect("create the Wayland display"); @@ -690,14 +759,19 @@ mod manager_loop { ) { return; } - let directory = std::env::temp_dir().join(format!("ferese-manager-run-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-run-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); - let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")])); + let (mut event_loop, mut state) = session( + &directory, + wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]), + ); xwayland::initialize(&mut state); - let generation = xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); + let generation = xwayland::request_start(&mut state) + .expect("an on-demand service accepts a start request"); assert!(generation > 0); dispatch_until(&mut event_loop, &mut state, |state| { @@ -711,12 +785,20 @@ mod manager_loop { "readiness is earned, not assumed" ); assert_eq!(status.state, State::Running); - assert!(!status.restart_required, "the live service matches the configured one"); - let pid = status.satellite_pid.expect("a running service reports its pid"); + assert!( + !status.restart_required, + "the live service matches the configured one" + ); + let pid = status + .satellite_pid + .expect("a running service reports its pid"); assert!(process_exists(pid), "the service must be alive"); xwayland::shutdown_after_loop(&mut state); - assert!(!process_exists(pid), "shutdown must stop and reap the service"); + assert!( + !process_exists(pid), + "shutdown must stop and reap the service" + ); let _ = fs::remove_dir_all(&directory); } @@ -727,7 +809,8 @@ mod manager_loop { ) { return; } - let directory = std::env::temp_dir().join(format!("ferese-manager-crash-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-crash-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -735,7 +818,10 @@ mod manager_loop { &directory, wrapper( &directory, - &[("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), ("FAKE_SATELLITE_LINGER", "0")], + &[ + ("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), + ("FAKE_SATELLITE_LINGER", "0"), + ], ), ); xwayland::initialize(&mut state); @@ -768,7 +854,10 @@ mod manager_loop { let status = xwayland::status_snapshot(&state); assert!( - matches!(status.state, State::Backoff | State::Stopped | State::Failed), + matches!( + status.state, + State::Backoff | State::Stopped | State::Failed + ), "a crash must land in a stopped phase, not {:?}", status.state ); @@ -796,7 +885,8 @@ mod manager_loop { test_hooks::reset(); test_hooks::STARTUP_DEADLINE_MS.store(600, std::sync::atomic::Ordering::SeqCst); - let directory = std::env::temp_dir().join(format!("ferese-manager-timeout-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-timeout-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); let record = directory.join("starts.jsonl"); @@ -816,15 +906,16 @@ mod manager_loop { ), ); xwayland::initialize(&mut state); - let first_generation = - xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); + let first_generation = xwayland::request_start(&mut state) + .expect("an on-demand service accepts a start request"); let display = xwayland::status_snapshot(&state) .display .expect("a display is reserved"); let socket_path = x11_socket_path(&directory, &display); // The kernel queues the connection, so a re-enabled listener could claim it. - std::os::unix::net::UnixStream::connect(&socket_path).expect("the reserved socket accepts a queued connection"); + std::os::unix::net::UnixStream::connect(&socket_path) + .expect("the reserved socket accepts a queued connection"); let mut first_pid = None; let mut observed_pids = Vec::new(); @@ -856,7 +947,11 @@ mod manager_loop { } let status = xwayland::status_snapshot(&state); - assert_eq!(status.state, State::Failed, "three silent starts must open the circuit"); + assert_eq!( + status.state, + State::Failed, + "three silent starts must open the circuit" + ); assert!( status .last_error @@ -871,7 +966,9 @@ mod manager_loop { "the timed-out service leader must be reaped, pid {first_pid} still exists" ); assert!( - observed_pids.iter().all(|pid| wait_until(|| !process_exists(*pid))), + observed_pids + .iter() + .all(|pid| wait_until(|| !process_exists(*pid))), "every generation started by the queued connection must be reaped" ); @@ -879,7 +976,10 @@ mod manager_loop { .expect("the fake recorded its start") .lines() .next() - .map(|line| serde_json::from_str::(line).expect("valid JSON")["holder_pid"].clone()) + .map(|line| { + serde_json::from_str::(line).expect("valid JSON")["holder_pid"] + .clone() + }) .and_then(|value| value.as_u64()) .map(|pid| pid as u32) .expect("the fake reports the descriptor holder"); @@ -915,7 +1015,8 @@ mod manager_loop { } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-cleanup-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-cleanup-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); let record = directory.join("starts.jsonl"); @@ -960,7 +1061,9 @@ mod manager_loop { let parked = xwayland::status_snapshot(&state); let retired_pid = retired_pid.expect("a service was spawned"); - let generation = parked.generation.expect("the parked generation is reported"); + let generation = parked + .generation + .expect("the parked generation is reported"); let display = parked.display.clone().expect("the display stays reserved"); assert_eq!( parked.satellite_pid, @@ -968,7 +1071,10 @@ mod manager_loop { "the cleanup record must stay owned while its group may still exist" ); { - let manager = state.xwayland.as_ref().expect("the manager stays published"); + let manager = state + .xwayland + .as_ref() + .expect("the manager stays published"); assert!( matches!(manager.lifecycle.phase, Phase::CleanupFailed { .. }), "the expired deadline must park the stop, found {:?}", @@ -984,7 +1090,8 @@ mod manager_loop { xwayland::request_start(&mut state).is_none(), "a parked cleanup must refuse a replacement generation" ); - let error = xwayland::retry(&mut state).expect_err("a retry must be refused while cleanup is unconfirmed"); + let error = xwayland::retry(&mut state) + .expect_err("a retry must be refused while cleanup is unconfirmed"); assert!( error.contains("still holds the X11 display"), "the refusal must name what is still holding the display: {error}" @@ -1019,7 +1126,8 @@ mod manager_loop { xwayland::retry(&mut state).expect("a confirmed cleanup makes a retry legal again"); dispatch_until(&mut event_loop, &mut state, |state| { let status = xwayland::status_snapshot(state); - status.state == State::Running && status.generation.is_some_and(|value| value > generation) + status.state == State::Running + && status.generation.is_some_and(|value| value > generation) }); let recovered = xwayland::status_snapshot(&state); @@ -1052,7 +1160,10 @@ mod manager_loop { } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-cleanup-stop-{}", std::process::id())); + let directory = std::env::temp_dir().join(format!( + "ferese-manager-cleanup-stop-{}", + std::process::id() + )); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -1060,7 +1171,10 @@ mod manager_loop { &directory, wrapper( &directory, - &[("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), ("FAKE_SATELLITE_LINGER", "0")], + &[ + ("FAKE_SATELLITE_EXIT_AFTER_READY", "1"), + ("FAKE_SATELLITE_LINGER", "0"), + ], ), ); xwayland::initialize(&mut state); @@ -1091,7 +1205,10 @@ mod manager_loop { .display .expect("the display stays reserved"); { - let manager = state.xwayland.as_ref().expect("the manager stays published"); + let manager = state + .xwayland + .as_ref() + .expect("the manager stays published"); assert!( matches!(manager.lifecycle.phase, Phase::CleanupFailed { .. }), "the stop must be parked as unconfirmed, found {:?}", @@ -1128,15 +1245,15 @@ mod manager_loop { #[test] fn an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll"; + const PATH: &str = "xwayland::integration_tests::manager_loop::an_unavailable_pidfd_falls_back_to_the_bounded_exit_poll"; if !run_isolated(PATH) { return; } test_hooks::reset(); test_hooks::PIDFD_UNAVAILABLE.store(true, std::sync::atomic::Ordering::SeqCst); - let directory = std::env::temp_dir().join(format!("ferese-manager-poll-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-poll-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -1181,23 +1298,29 @@ mod manager_loop { wait_until(|| !process_exists(pid)), "the observed service must be reaped without a pidfd" ); - assert!(verified_readiness, "the service reported readiness before it exited"); + assert!( + verified_readiness, + "the service reported readiness before it exited" + ); let status = xwayland::status_snapshot(&state); - assert_eq!(status.satellite_pid, None, "cleanup released the process owner"); + assert_eq!( + status.satellite_pid, None, + "cleanup released the process owner" + ); test_hooks::reset(); let _ = fs::remove_dir_all(&directory); } #[test] fn a_stale_exit_callback_cannot_stop_a_replacement_service() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::a_stale_exit_callback_cannot_stop_a_replacement_service"; + const PATH: &str = "xwayland::integration_tests::manager_loop::a_stale_exit_callback_cannot_stop_a_replacement_service"; if !run_isolated(PATH) { return; } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-stale-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-stale-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -1206,8 +1329,8 @@ mod manager_loop { wrapper(&directory, &[("FAKE_SATELLITE_EXIT_BEFORE_READY", "1")]), ); xwayland::initialize(&mut state); - let first_generation = - xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); + let first_generation = xwayland::request_start(&mut state) + .expect("an on-demand service accepts a start request"); let mut retired_pid = None; let deadline = Instant::now() + Duration::from_secs(30); @@ -1222,7 +1345,10 @@ mod manager_loop { if status.state == State::Failed { break; } - assert!(Instant::now() < deadline, "the early failure never settled: {status:?}"); + assert!( + Instant::now() < deadline, + "the early failure never settled: {status:?}" + ); } let retired_pid = retired_pid.expect("the first generation was spawned"); let retired_status = xwayland::status_snapshot(&state); @@ -1238,8 +1364,13 @@ mod manager_loop { }); let live = xwayland::status_snapshot(&state); let live_pid = live.satellite_pid.expect("the replacement reports its pid"); - let live_generation = live.generation.expect("the replacement reports its generation"); - assert_ne!(live_pid, retired_pid, "the replacement must be a different process"); + let live_generation = live + .generation + .expect("the replacement reports its generation"); + assert_ne!( + live_pid, retired_pid, + "the replacement must be a different process" + ); let handle = state.loop_handle.clone(); let stale_generation = first_generation; @@ -1259,7 +1390,11 @@ mod manager_loop { State::Running, "a stale callback must not move the live service" ); - assert_eq!(after.satellite_pid, Some(live_pid), "the live process must stay owned"); + assert_eq!( + after.satellite_pid, + Some(live_pid), + "the live process must stay owned" + ); assert_eq!( after.generation, Some(live_generation), @@ -1274,18 +1409,20 @@ mod manager_loop { #[test] fn shutdown_while_starting_stops_the_service_it_owns() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::shutdown_while_starting_stops_the_service_it_owns"; + const PATH: &str = "xwayland::integration_tests::manager_loop::shutdown_while_starting_stops_the_service_it_owns"; if !run_isolated(PATH) { return; } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-shutdown-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-shutdown-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); - let (mut event_loop, mut state) = - session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_READY_DELAY", "30")])); + let (mut event_loop, mut state) = session( + &directory, + wrapper(&directory, &[("FAKE_SATELLITE_READY_DELAY", "30")]), + ); xwayland::initialize(&mut state); xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); @@ -1332,7 +1469,8 @@ mod manager_loop { } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-rebind-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-rebind-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -1341,8 +1479,8 @@ mod manager_loop { wrapper(&directory, &[("FAKE_SATELLITE_EXIT_BEFORE_READY", "1")]), ); xwayland::initialize(&mut state); - let first_generation = - xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); + let first_generation = xwayland::request_start(&mut state) + .expect("an on-demand service accepts a start request"); let deadline = Instant::now() + Duration::from_secs(30); loop { @@ -1356,7 +1494,8 @@ mod manager_loop { } test_hooks::FAIL_REBIND_AFTER.store(0, std::sync::atomic::Ordering::SeqCst); - let error = xwayland::retry(&mut state).expect_err("an injected rebind failure must be reported"); + let error = + xwayland::retry(&mut state).expect_err("an injected rebind failure must be reported"); assert!( error.contains("rebind X11 display"), "the reported error must name the rebind step: {error}" @@ -1393,14 +1532,14 @@ mod manager_loop { #[test] fn a_listener_registration_failure_leaves_a_retryable_endpoint() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::a_listener_registration_failure_leaves_a_retryable_endpoint"; + const PATH: &str = "xwayland::integration_tests::manager_loop::a_listener_registration_failure_leaves_a_retryable_endpoint"; if !run_isolated(PATH) { return; } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-register-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-register-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); @@ -1411,7 +1550,10 @@ mod manager_loop { // Fail the second listener source: the first is already registered. test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(1, std::sync::atomic::Ordering::SeqCst); xwayland::initialize(&mut state); - let diagnostic = state.x11_diagnostic.clone().expect("the failure must be visible"); + let diagnostic = state + .x11_diagnostic + .clone() + .expect("the failure must be visible"); assert!( diagnostic.contains("injected X11 listener registration failure"), "the diagnostic must name the failure: {diagnostic}" @@ -1421,7 +1563,8 @@ mod manager_loop { "a half-registered endpoint must not be published" ); - test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst); + test_hooks::FAIL_LISTENER_REGISTRATION_AFTER + .store(usize::MAX, std::sync::atomic::Ordering::SeqCst); xwayland::retry(&mut state).expect("the service becomes available once registration works"); let mut first_generation = None; @@ -1437,7 +1580,10 @@ mod manager_loop { if status.state == State::Failed { break; } - assert!(Instant::now() < deadline, "the service never became failed: {status:?}"); + assert!( + Instant::now() < deadline, + "the service never became failed: {status:?}" + ); } let first_generation = first_generation.expect("a generation was started"); @@ -1463,7 +1609,8 @@ mod manager_loop { "the half-registered listeners must be closed" ); - test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst); + test_hooks::FAIL_LISTENER_REGISTRATION_AFTER + .store(usize::MAX, std::sync::atomic::Ordering::SeqCst); wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]); xwayland::retry(&mut state).expect("the endpoint recovers once registration works"); dispatch_until(&mut event_loop, &mut state, |state| { @@ -1477,28 +1624,35 @@ mod manager_loop { #[test] fn an_eager_service_that_could_not_start_retries_exactly_once() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::an_eager_service_that_could_not_start_retries_exactly_once"; + const PATH: &str = "xwayland::integration_tests::manager_loop::an_eager_service_that_could_not_start_retries_exactly_once"; if !run_isolated(PATH) { return; } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-eager-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-eager-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); - let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")])); + let (mut event_loop, mut state) = session( + &directory, + wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]), + ); state.xwayland_config.startup = XwaylandStartup::Eager; test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(0, std::sync::atomic::Ordering::SeqCst); xwayland::initialize(&mut state); - assert!(state.xwayland.is_none(), "an unavailable service publishes no endpoint"); + assert!( + state.xwayland.is_none(), + "an unavailable service publishes no endpoint" + ); assert!( state.x11_diagnostic.is_some(), "the unavailable start must leave a diagnostic" ); - test_hooks::FAIL_LISTENER_REGISTRATION_AFTER.store(usize::MAX, std::sync::atomic::Ordering::SeqCst); + test_hooks::FAIL_LISTENER_REGISTRATION_AFTER + .store(usize::MAX, std::sync::atomic::Ordering::SeqCst); // Eager startup must still issue exactly one start request. xwayland::retry(&mut state).expect("an eager retry of an unavailable service must succeed"); @@ -1516,24 +1670,31 @@ mod manager_loop { Some(1), "exactly one generation was requested: {status:?}" ); - assert_eq!(status.readiness, Readiness::Verified, "readiness is earned: {status:?}"); + assert_eq!( + status.readiness, + Readiness::Verified, + "readiness is earned: {status:?}" + ); let _ = fs::remove_dir_all(&directory); } #[test] fn restart_required_tracks_desired_configuration_only() { - const PATH: &str = - "xwayland::integration_tests::manager_loop::restart_required_tracks_desired_configuration_only"; + const PATH: &str = "xwayland::integration_tests::manager_loop::restart_required_tracks_desired_configuration_only"; if !run_isolated(PATH) { return; } test_hooks::reset(); - let directory = std::env::temp_dir().join(format!("ferese-manager-reload-{}", std::process::id())); + let directory = + std::env::temp_dir().join(format!("ferese-manager-reload-{}", std::process::id())); let _ = fs::remove_dir_all(&directory); fs::create_dir_all(&directory).expect("create the test directory"); - let (mut event_loop, mut state) = session(&directory, wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")])); + let (mut event_loop, mut state) = session( + &directory, + wrapper(&directory, &[("FAKE_SATELLITE_LINGER", "60")]), + ); xwayland::initialize(&mut state); xwayland::request_start(&mut state).expect("an on-demand service accepts a start request"); dispatch_until(&mut event_loop, &mut state, |state| { @@ -1542,11 +1703,19 @@ mod manager_loop { let effective = xwayland::status_snapshot(&state); let display = effective.display.clone().expect("a display is reserved"); - let pid = effective.satellite_pid.expect("a running service reports its pid"); + let pid = effective + .satellite_pid + .expect("a running service reports its pid"); let authority = state .xwayland .as_ref() - .and_then(|manager| manager.x11_environment().authority.to_str().map(str::to_owned)) + .and_then(|manager| { + manager + .x11_environment() + .authority + .to_str() + .map(str::to_owned) + }) .expect("an authority path is published"); assert!( !effective.restart_required, @@ -1578,13 +1747,26 @@ mod manager_loop { Some(display.as_str()), "the display must not move" ); - assert_eq!(status.satellite_pid, Some(pid), "the service must not be restarted"); + assert_eq!( + status.satellite_pid, + Some(pid), + "the service must not be restarted" + ); let authority_now = state .xwayland .as_ref() - .and_then(|manager| manager.x11_environment().authority.to_str().map(str::to_owned)) + .and_then(|manager| { + manager + .x11_environment() + .authority + .to_str() + .map(str::to_owned) + }) .expect("an authority path is published"); - assert_eq!(authority_now, authority, "the authority file must not be replaced"); + assert_eq!( + authority_now, authority, + "the authority file must not be replaced" + ); } // Reverting to the session configuration clears the indication. diff --git a/compositor/src/xwayland/lifecycle.rs b/compositor/src/xwayland/lifecycle.rs index 5456777..96bb992 100644 --- a/compositor/src/xwayland/lifecycle.rs +++ b/compositor/src/xwayland/lifecycle.rs @@ -118,7 +118,12 @@ impl Lifecycle { true } - pub fn begin_stop_after_failure(&mut self, generation: u64, now: Instant, retryable: bool) -> Option { + pub fn begin_stop_after_failure( + &mut self, + generation: u64, + now: Instant, + retryable: bool, + ) -> Option { let current = matches!( self.phase, Phase::Starting(current) @@ -144,7 +149,10 @@ impl Lifecycle { } else { AfterStop::Backoff }; - self.phase = Phase::Stopping { generation, after_stop }; + self.phase = Phase::Stopping { + generation, + after_stop, + }; Some(after_stop) } @@ -183,7 +191,8 @@ impl Lifecycle { pub fn park_cleanup_failure(&mut self, generation: u64) -> bool { let Phase::Stopping { - generation: current, .. + generation: current, + .. } = self.phase else { return false; @@ -255,14 +264,21 @@ mod tests { use super::*; fn start(lifecycle: &mut Lifecycle) -> u64 { - lifecycle.request_start().expect("Idle must yield a generation ticket") + lifecycle + .request_start() + .expect("Idle must yield a generation ticket") } /// Fail a generation the way the manager does, then complete the stop. /// /// Cleanup completing is what publishes the next phase, so a test that /// needs the next generation must say so explicitly. - fn fail_and_stop(lifecycle: &mut Lifecycle, generation: u64, now: Instant, retryable: bool) -> AfterStop { + fn fail_and_stop( + lifecycle: &mut Lifecycle, + generation: u64, + now: Instant, + retryable: bool, + ) -> AfterStop { let after_stop = lifecycle .begin_stop_after_failure(generation, now, retryable) .expect("the current generation must accept a failure"); @@ -328,7 +344,10 @@ mod tests { assert!(!lifecycle.spawned(stale)); assert!(!lifecycle.ready(stale)); - assert_eq!(lifecycle.begin_stop_after_failure(stale, Instant::now(), true), None); + assert_eq!( + lifecycle.begin_stop_after_failure(stale, Instant::now(), true), + None + ); assert_eq!(lifecycle.phase, Phase::Starting(current)); } @@ -361,7 +380,10 @@ mod tests { let now = Instant::now(); let first = start(&mut lifecycle); - assert_eq!(fail_and_stop(&mut lifecycle, first, now, true), AfterStop::Backoff); + assert_eq!( + fail_and_stop(&mut lifecycle, first, now, true), + AfterStop::Backoff + ); let Phase::Backoff { until, .. } = lifecycle.phase else { panic!("first failure must back off"); }; @@ -369,7 +391,10 @@ mod tests { let second = start_after_backoff(&mut lifecycle); let now = Instant::now(); - assert_eq!(fail_and_stop(&mut lifecycle, second, now, true), AfterStop::Backoff); + assert_eq!( + fail_and_stop(&mut lifecycle, second, now, true), + AfterStop::Backoff + ); let Phase::Backoff { until, .. } = lifecycle.phase else { panic!("second failure must back off"); }; @@ -456,7 +481,10 @@ mod tests { .begin_stop_after_failure(generation, now, true) .expect("the current generation accepts a failure"); - assert!(lifecycle.park_cleanup_failure(generation), "an in-flight stop parks"); + assert!( + lifecycle.park_cleanup_failure(generation), + "an in-flight stop parks" + ); assert_eq!(lifecycle.phase, Phase::CleanupFailed { generation }); // The deadline proved nothing, so the service must refuse to act on it. @@ -569,7 +597,10 @@ mod tests { lifecycle .begin_stop_after_failure(generation, Instant::now(), false) .expect("the current generation accepts a failure"); - assert_eq!(lifecycle.finish_stop(generation.wrapping_add(1), Instant::now()), None); + assert_eq!( + lifecycle.finish_stop(generation.wrapping_add(1), Instant::now()), + None + ); // A stale completion leaves the real transition in flight. assert!(lifecycle.phase.is_stopping()); diff --git a/compositor/src/xwayland/mod.rs b/compositor/src/xwayland/mod.rs index 72cd227..7b2a0ea 100644 --- a/compositor/src/xwayland/mod.rs +++ b/compositor/src/xwayland/mod.rs @@ -98,7 +98,8 @@ pub(crate) mod test_hooks { ALLOCATOR_ROOT.with(|slot| *slot.borrow_mut() = Some((sockets, locks))); } - pub(crate) fn allocator_root_for_reservation() -> Option<(std::path::PathBuf, std::path::PathBuf)> { + pub(crate) fn allocator_root_for_reservation() + -> Option<(std::path::PathBuf, std::path::PathBuf)> { ALLOCATOR_ROOT.with(|slot| slot.borrow().clone()) } @@ -111,7 +112,8 @@ pub(crate) mod test_hooks { std::thread::current().id() )); let _ = std::fs::create_dir_all(&root); - let _ = std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700)); + let _ = + std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700)); root } @@ -299,15 +301,23 @@ impl XwaylandManager { self.last_error = None; } - fn install_exit_watch(&mut self, loop_handle: &Loop, generation: u64, pid: u32) -> Result<(), String> { + fn install_exit_watch( + &mut self, + loop_handle: &Loop, + generation: u64, + pid: u32, + ) -> Result<(), String> { if let Ok(fd) = crate::process::pidfd(pid) { - match loop_handle.insert_source(Generic::new(fd, Interest::READ, Mode::Level), move |_, _, state| { - let handle = state.loop_handle.clone(); - if let Some(manager) = state.xwayland.as_mut() { - manager.handle_exit(&handle, generation, pid); - } - Ok(PostAction::Remove) - }) { + match loop_handle.insert_source( + Generic::new(fd, Interest::READ, Mode::Level), + move |_, _, state| { + let handle = state.loop_handle.clone(); + if let Some(manager) = state.xwayland.as_mut() { + manager.handle_exit(&handle, generation, pid); + } + Ok(PostAction::Remove) + }, + ) { Ok(token) => { self.sources.exit_watch = Some(token); return Ok(()); @@ -315,13 +325,21 @@ impl XwaylandManager { Err(error) => warn!(%error, pid, "cannot register the X11 service exit source"), } } else { - warn!(pid, "pidfd unavailable; using a bounded poll to watch the X11 service"); + warn!( + pid, + "pidfd unavailable; using a bounded poll to watch the X11 service" + ); } self.install_exit_poll(loop_handle, generation, pid) } - fn install_exit_poll(&mut self, loop_handle: &Loop, generation: u64, pid: u32) -> Result<(), String> { + fn install_exit_poll( + &mut self, + loop_handle: &Loop, + generation: u64, + pid: u32, + ) -> Result<(), String> { let timer = Timer::from_duration(CHILD_POLL_INTERVAL); let token = loop_handle .insert_source(timer, move |_, _, state| { @@ -333,7 +351,9 @@ impl XwaylandManager { .and_then(|manager| manager.process.as_mut()) .map(|process| process.observe()); match observed { - Some(Observation::Running) => return TimeoutAction::ToDuration(CHILD_POLL_INTERVAL), + Some(Observation::Running) => { + return TimeoutAction::ToDuration(CHILD_POLL_INTERVAL); + } Some(Observation::Exited) | Some(Observation::Failed(_)) | None => {} } if let Some(manager) = state.xwayland.as_mut() { @@ -349,7 +369,10 @@ impl XwaylandManager { fn owns(&self, generation: u64, pid: u32) -> bool { self.lifecycle.phase.generation() == Some(generation) - && self.process.as_ref().is_some_and(|process| process.pid() == pid) + && self + .process + .as_ref() + .is_some_and(|process| process.pid() == pid) } fn handle_exit(&mut self, loop_handle: &Loop, generation: u64, pid: u32) { @@ -366,10 +389,11 @@ impl XwaylandManager { self.last_error = Some(error.to_owned()); // The verdict describes the generation that just ended. self.readiness_verdict = Readiness::NotAttempted; - let Some(after_stop) = - self.lifecycle - .begin_stop_after_failure(generation, std::time::Instant::now(), retryable) - else { + let Some(after_stop) = self.lifecycle.begin_stop_after_failure( + generation, + std::time::Instant::now(), + retryable, + ) else { return; }; @@ -392,7 +416,10 @@ impl XwaylandManager { self.sources.readiness_socket = None; self.disable_listeners(loop_handle); - debug_assert!(self.sources.is_empty(), "a stop must release every source it owns"); + debug_assert!( + self.sources.is_empty(), + "a stop must release every source it owns" + ); let Some(process) = self.process.as_mut() else { // Nothing survived, so cleanup is already complete. @@ -410,7 +437,8 @@ impl XwaylandManager { // The group, not just the leader, has to be gone. Some(Observation::Exited) | None => { self.wait_for_empty_group(loop_handle, generation, after_stop); - if self.process.is_none() || !matches!(self.lifecycle.phase, Phase::Stopping { .. }) { + if self.process.is_none() || !matches!(self.lifecycle.phase, Phase::Stopping { .. }) + { // Cleanup finished, or was parked as unconfirmed, inside // the helper: either way no poll belongs to this stop. return; @@ -434,7 +462,10 @@ impl XwaylandManager { return TimeoutAction::Drop; }; - if !manager.owns(generation, manager.process.as_ref().map_or(0, OwnedProcessGroup::pid)) { + if !manager.owns( + generation, + manager.process.as_ref().map_or(0, OwnedProcessGroup::pid), + ) { return TimeoutAction::Drop; } @@ -493,7 +524,11 @@ impl XwaylandManager { } } - if self.process.as_ref().is_some_and(OwnedProcessGroup::group_is_empty) { + if self + .process + .as_ref() + .is_some_and(OwnedProcessGroup::group_is_empty) + { self.complete_stop(loop_handle, generation, after_stop); return; } @@ -503,7 +538,10 @@ impl XwaylandManager { let now = std::time::Instant::now(); match self.stop_kill_grace { None => { - warn!(generation, "the X11 service left survivors after SIGKILL; waiting"); + warn!( + generation, + "the X11 service left survivors after SIGKILL; waiting" + ); self.stop_kill_grace = Some(now + STOP_TIMEOUT); } Some(grace) if now >= grace => { @@ -605,7 +643,10 @@ impl XwaylandManager { self.process = None; if !self.lifecycle.confirm_cleanup(generation) { - return Err("the X11 service left the unconfirmed cleanup state while it was checked".to_owned()); + return Err( + "the X11 service left the unconfirmed cleanup state while it was checked" + .to_owned(), + ); } info!( generation, @@ -643,7 +684,10 @@ impl XwaylandManager { // Safe now: the leader has been reaped, so Drop has nothing to signal. self.process = None; - let Some(published) = self.lifecycle.finish_stop(generation, std::time::Instant::now()) else { + let Some(published) = self + .lifecycle + .finish_stop(generation, std::time::Instant::now()) + else { return; }; debug_assert!( @@ -659,7 +703,10 @@ impl XwaylandManager { let handle = state.loop_handle.clone(); if let Some(manager) = state.xwayland.as_mut() { // Idle again: a still-queued connection can wake us. - if manager.lifecycle.finish_backoff(generation, std::time::Instant::now()) { + if manager + .lifecycle + .finish_backoff(generation, std::time::Instant::now()) + { manager.enable_listeners(&handle); } } @@ -672,7 +719,9 @@ impl XwaylandManager { AfterStop::Failed => { // The circuit is open. An open listening socket with no consumer // would strand clients, so fail fast instead. - warn!("X11 endpoint disabled after repeated failures; waiting for an explicit retry"); + warn!( + "X11 endpoint disabled after repeated failures; waiting for an explicit retry" + ); self.remove_listener_sources(loop_handle); self.reservation.close_listeners(); } @@ -695,7 +744,9 @@ impl XwaylandManager { return Err("X11 is not in a failed state, so there is nothing to retry".to_owned()); } if self.process.is_some() { - return Err("X11 is still stopping its previous service; retry once it has finished".to_owned()); + return Err( + "X11 is still stopping its previous service; retry once it has finished".to_owned(), + ); } // Rebind the **same** display so launchers that already learned the @@ -756,7 +807,10 @@ fn validate_runtime_directory(path: &std::path::Path) -> Result<(), String> { let metadata = std::fs::symlink_metadata(path) .map_err(|error| format!("XDG_RUNTIME_DIR is unusable ({}): {error}", path.display()))?; let uid = unsafe { libc::geteuid() }; - if !metadata.is_dir() || metadata.file_type().is_symlink() || metadata.uid() != uid || metadata.mode() & 0o077 != 0 + if !metadata.is_dir() + || metadata.file_type().is_symlink() + || metadata.uid() != uid + || metadata.mode() & 0o077 != 0 { return Err(format!( "XDG_RUNTIME_DIR is not a private user-owned directory: {}", @@ -810,8 +864,9 @@ fn start_service(state: &mut Ferese) -> Result<(), String> { } fn request_first_generation(state: &mut Ferese) -> Result { - request_start(state) - .ok_or_else(|| "X11 could not be started yet; a previous attempt is still in progress".to_owned()) + request_start(state).ok_or_else(|| { + "X11 could not be started yet; a previous attempt is still in progress".to_owned() + }) } /// Create the private directory that holds one service instance's readiness @@ -821,7 +876,9 @@ fn request_first_generation(state: &mut Ferese) -> Result { /// against the same code that ships: the directory is `0700` whatever the /// session umask is, because another user must not be able to place a socket /// where Ferese looks for notifications. -fn notification_directory(runtime_directory: &std::path::Path) -> Result { +fn notification_directory( + runtime_directory: &std::path::Path, +) -> Result { tempfile::Builder::new() .prefix("ferese-x11-") .rand_bytes(16) @@ -833,7 +890,8 @@ fn notification_directory(runtime_directory: &std::path::Path) -> Result Result { let runtime_directory = runtime_directory()?; let notify_directory = notification_directory(&runtime_directory)?; - let reservation = Reservation::allocate().map_err(|error| format!("cannot reserve an X11 display: {error}"))?; + let reservation = Reservation::allocate() + .map_err(|error| format!("cannot reserve an X11 display: {error}"))?; let display = reservation.display_number(); let authority = AuthorityFile::create(&runtime_directory, display) .map_err(|error| format!("cannot create an Xauthority file: {error}"))?; @@ -876,31 +934,34 @@ fn register_listeners(loop_handle: &Loop, manager: &mut XwaylandManager) -> Resu error.to_string() })?; let token = loop_handle - .insert_source(Generic::new(listener, Interest::READ, Mode::Level), |_, _, state| { - let handle = state.loop_handle.clone(); + .insert_source( + Generic::new(listener, Interest::READ, Mode::Level), + |_, _, state| { + let handle = state.loop_handle.clone(); - let ticket = state.xwayland.as_mut().and_then(|manager| { - let ticket = manager.lifecycle.request_start(); - if ticket.is_some() { - manager.disable_listeners(&handle); - } - ticket - }); - - if let Some(generation) = ticket { - handle.insert_idle(move |state| { - let current = state - .xwayland - .as_ref() - .is_some_and(|manager| manager.owns_generation(generation)); - if current { - start_generation(state, generation); + let ticket = state.xwayland.as_mut().and_then(|manager| { + let ticket = manager.lifecycle.request_start(); + if ticket.is_some() { + manager.disable_listeners(&handle); } + ticket }); - } - Ok(PostAction::Disable) - }) + if let Some(generation) = ticket { + handle.insert_idle(move |state| { + let current = state + .xwayland + .as_ref() + .is_some_and(|manager| manager.owns_generation(generation)); + if current { + start_generation(state, generation); + } + }); + } + + Ok(PostAction::Disable) + }, + ) .map_err(|error| { for token in inserted.drain(..) { loop_handle.remove(token); @@ -918,7 +979,9 @@ fn register_listeners(loop_handle: &Loop, manager: &mut XwaylandManager) -> Resu pub fn retry(state: &mut Ferese) -> Result<(), String> { if !state.xwayland_config.enabled { - return Err("X11 is disabled in the configuration; enable it and start a new session".to_owned()); + return Err( + "X11 is disabled in the configuration; enable it and start a new session".to_owned(), + ); } if let Some(manager) = state.xwayland.as_mut() { @@ -946,7 +1009,11 @@ pub fn status_snapshot(state: &Ferese) -> ferese_ipc::xwayland::Status { XwaylandStartup::Eager => "eager", } .to_owned(), - state: if disabled { State::Disabled } else { State::Failed }, + state: if disabled { + State::Disabled + } else { + State::Failed + }, display: None, satellite_pid: None, generation: None, @@ -959,11 +1026,9 @@ pub fn status_snapshot(state: &Ferese) -> ferese_ipc::xwayland::Status { None } else { // Never report an empty failure: say what the user can do. - Some( - state.x11_diagnostic.clone().unwrap_or_else(|| { - "the X11 service is not running; retry it or start a new session".to_owned() - }), - ) + Some(state.x11_diagnostic.clone().unwrap_or_else(|| { + "the X11 service is not running; retry it or start a new session".to_owned() + })) }, }; }; @@ -1002,7 +1067,9 @@ fn start_generation(state: &mut Ferese, generation: u64) { let Some(manager) = state.xwayland.as_mut() else { return; }; - if !manager.owns_generation(generation) || manager.lifecycle.phase != Phase::Starting(generation) { + if !manager.owns_generation(generation) + || manager.lifecycle.phase != Phase::Starting(generation) + { return; } @@ -1118,44 +1185,49 @@ fn install_readiness_source( .map_err(|error| format!("cannot duplicate the readiness endpoint: {error}"))?; let token = loop_handle - .insert_source(Generic::new(clone, Interest::READ, Mode::Level), move |_, _, state| { - let handle = state.loop_handle.clone(); - let Some(manager) = state.xwayland.as_mut() else { - return Ok(PostAction::Remove); - }; - if !manager.owns_generation(generation) || manager.lifecycle.phase != Phase::Spawned(generation) { - return Ok(PostAction::Remove); - } - - for _ in 0..READINESS_DRAIN_BUDGET { - let Some(socket) = manager.sources.readiness_socket.as_ref() else { + .insert_source( + Generic::new(clone, Interest::READ, Mode::Level), + move |_, _, state| { + let handle = state.loop_handle.clone(); + let Some(manager) = state.xwayland.as_mut() else { return Ok(PostAction::Remove); }; - match socket.recv_ready(pid) { - Ok(true) => { - info!(generation, "X11 service reported readiness"); - if manager.lifecycle.ready(generation) { - manager.finish_ready(&handle); + if !manager.owns_generation(generation) + || manager.lifecycle.phase != Phase::Spawned(generation) + { + return Ok(PostAction::Remove); + } - manager.enable_listeners(&handle); - } + for _ in 0..READINESS_DRAIN_BUDGET { + let Some(socket) = manager.sources.readiness_socket.as_ref() else { return Ok(PostAction::Remove); - } - // Ignore stale, foreign, or malformed notifications. - Ok(false) => continue, - Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { - // Stay level-triggered for any remaining work. - return Ok(PostAction::Continue); - } - Err(error) => { - warn!(%error, "cannot read the X11 readiness notification"); - return Ok(PostAction::Continue); + }; + match socket.recv_ready(pid) { + Ok(true) => { + info!(generation, "X11 service reported readiness"); + if manager.lifecycle.ready(generation) { + manager.finish_ready(&handle); + + manager.enable_listeners(&handle); + } + return Ok(PostAction::Remove); + } + // Ignore stale, foreign, or malformed notifications. + Ok(false) => continue, + Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { + // Stay level-triggered for any remaining work. + return Ok(PostAction::Continue); + } + Err(error) => { + warn!(%error, "cannot read the X11 readiness notification"); + return Ok(PostAction::Continue); + } } } - } - Ok(PostAction::Continue) - }) + Ok(PostAction::Continue) + }, + ) .map_err(|error| format!("cannot register the readiness source: {error}"))?; manager.sources.readiness = Some(token); @@ -1227,7 +1299,8 @@ mod tests { return; } - let mut manager = prepare(&XwaylandConfig::default()).expect("reserve a managed X11 service"); + let mut manager = + prepare(&XwaylandConfig::default()).expect("reserve a managed X11 service"); assert_eq!( manager.status().readiness, Readiness::NotAttempted, @@ -1257,7 +1330,11 @@ mod tests { let next = notification_directory(root.path()).expect("the second instance directory"); let next_socket = ReadinessSocket::create(next.path(), 1).expect("the second endpoint"); - assert_ne!(stale.path(), next.path(), "each instance owns its own directory"); + assert_ne!( + stale.path(), + next.path(), + "each instance owns its own directory" + ); assert_ne!( stale_socket.path(), next_socket.path(), diff --git a/compositor/src/xwayland/readiness.rs b/compositor/src/xwayland/readiness.rs index 81a687e..8afbc31 100644 --- a/compositor/src/xwayland/readiness.rs +++ b/compositor/src/xwayland/readiness.rs @@ -121,7 +121,8 @@ impl ReadinessSocket { from_satellite = true; } } - } else if cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_RIGHTS { + } else if cmsg.cmsg_level == libc::SOL_SOCKET && cmsg.cmsg_type == libc::SCM_RIGHTS + { // Accepting a datagram must never adopt descriptors from an // untrusted sender. Close them instead of dropping them on // the floor, where they would stay open in this process. @@ -144,7 +145,9 @@ impl ReadinessSocket { // Decided only after the control walk: a completely filled payload // buffer cannot be distinguished from truncation, and the kernel's // truncation flags never satisfy the readiness contract. - if received as usize == buffer.len() || message.msg_flags & (libc::MSG_CTRUNC | libc::MSG_TRUNC) != 0 { + if received as usize == buffer.len() + || message.msg_flags & (libc::MSG_CTRUNC | libc::MSG_TRUNC) != 0 + { return Ok(false); } @@ -187,8 +190,12 @@ fn exact_line(payload: &[u8], wanted: &[u8]) -> Option { } fn is_key_value(line: &str) -> bool { - line.split_once('=') - .is_some_and(|(key, _)| !key.is_empty() && key.bytes().all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')) + line.split_once('=').is_some_and(|(key, _)| { + !key.is_empty() + && key + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'_') + }) } impl Drop for ReadinessSocket { @@ -247,10 +254,12 @@ mod tests { } fn private_runtime_dir(name: &str) -> PathBuf { - let root = std::env::temp_dir().join(format!("ferese-readiness-{name}-{}", std::process::id())); + let root = + std::env::temp_dir().join(format!("ferese-readiness-{name}-{}", std::process::id())); let _ = std::fs::remove_dir_all(&root); std::fs::create_dir_all(&root).unwrap(); - std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700)).unwrap(); + std::fs::set_permissions(&root, std::os::unix::fs::PermissionsExt::from_mode(0o700)) + .unwrap(); root } @@ -290,7 +299,8 @@ mod tests { } // A descriptor for a known file, so the receiver can prove it // closed exactly this one rather than guessing at a count. - let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else { + let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) + else { libc::_exit(73); }; let spare = libc::open(name.as_ptr(), libc::O_RDONLY); @@ -411,7 +421,11 @@ mod tests { /// Send `READY=1` with more descriptors than the receiver's control buffer can /// hold, so the kernel truncates the ancillary data while installing what fits. - fn send_descriptors_beyond_capacity_from_child(path: &Path, sentinel: &Path, count: usize) -> i32 { + fn send_descriptors_beyond_capacity_from_child( + path: &Path, + sentinel: &Path, + count: usize, + ) -> i32 { // SAFETY: the child performs only socket/open/sendmsg/sleep/_exit, all // async-signal-safe except sleep, which is a bounded wait before exit. unsafe { @@ -422,7 +436,8 @@ mod tests { if fd < 0 { libc::_exit(70); } - let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else { + let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) + else { libc::_exit(73); }; let sentinel_fd = libc::open(name.as_ptr(), libc::O_RDONLY); @@ -488,7 +503,8 @@ mod tests { if fd < 0 { libc::_exit(70); } - let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) else { + let Ok(name) = std::ffi::CString::new(sentinel.as_os_str().as_encoded_bytes()) + else { libc::_exit(73); }; let sentinel_fd = libc::open(name.as_ptr(), libc::O_RDONLY); @@ -510,7 +526,8 @@ mod tests { control.payload.as_mut_ptr(), std::mem::size_of::(), ); - control.header.cmsg_len = (cmsg_data_offset() + std::mem::size_of::()) as _; + control.header.cmsg_len = + (cmsg_data_offset() + std::mem::size_of::()) as _; control.header.cmsg_level = libc::SOL_SOCKET; control.header.cmsg_type = libc::SCM_RIGHTS; @@ -518,7 +535,8 @@ mod tests { message.msg_iov = std::ptr::from_mut(&mut iov); message.msg_iovlen = 1; message.msg_control = std::ptr::from_mut(&mut control).cast(); - message.msg_controllen = cmsg_data_offset() + std::mem::size_of::(); + message.msg_controllen = + cmsg_data_offset() + std::mem::size_of::(); if libc::sendmsg(fd, std::ptr::from_ref(&message), libc::MSG_NOSIGNAL) < 0 { libc::_exit(71); } @@ -625,7 +643,9 @@ mod tests { libc::kill(pid, libc::SIGKILL); libc::waitpid(pid, std::ptr::from_mut(&mut status), 0); } - panic!("the storm was not delivered within the shared deadline: {judged} of 128 judged"); + panic!( + "the storm was not delivered within the shared deadline: {judged} of 128 judged" + ); } // Every datagram was accepted, so the sender has reached its exit. @@ -635,7 +655,10 @@ mod tests { libc::WIFEXITED(status) && libc::WEXITSTATUS(status) == 0, "child must deliver the storm: {status:#x}" ); - assert!(judged >= 128, "the storm must be delivered and judged: {judged}"); + assert!( + judged >= 128, + "the storm must be delivered and judged: {judged}" + ); assert!( started.elapsed() < std::time::Duration::from_secs(2), "judging the storm must not stall the consumer: {:?}", @@ -685,7 +708,10 @@ mod tests { /// A unique empty file used to identify a delivered descriptor. fn sentinel_file(name: &str) -> PathBuf { - let path = std::env::temp_dir().join(format!("ferese-readiness-sentinel-{name}-{}", std::process::id())); + let path = std::env::temp_dir().join(format!( + "ferese-readiness-sentinel-{name}-{}", + std::process::id() + )); let _ = std::fs::remove_file(&path); std::fs::write(&path, b"").unwrap(); path @@ -709,7 +735,10 @@ mod tests { let path = socket.path().to_owned(); assert!(path.exists()); assert!(path.starts_with(&runtime)); - assert!(path.to_string_lossy().contains('7'), "path must encode the generation"); + assert!( + path.to_string_lossy().contains('7'), + "path must encode the generation" + ); path }; @@ -815,7 +844,10 @@ mod tests { libc::waitpid(pid, std::ptr::from_mut(&mut status), 0); } - assert!(accepted, "an exact READY=1 from the expected PID must be accepted"); + assert!( + accepted, + "an exact READY=1 from the expected PID must be accepted" + ); } #[test] @@ -830,7 +862,9 @@ mod tests { if pid == 0 { for _ in 0..500 { if let Ok(client) = UnixDatagram::unbound() - && client.send_to(b"RELOADING=1\nSTATUS=starting", &path).is_ok() + && client + .send_to(b"RELOADING=1\nSTATUS=starting", &path) + .is_ok() { libc::_exit(0); } diff --git a/compositor/src/xwayland/sockets.rs b/compositor/src/xwayland/sockets.rs index c0f62a4..44c2e38 100644 --- a/compositor/src/xwayland/sockets.rs +++ b/compositor/src/xwayland/sockets.rs @@ -60,7 +60,10 @@ impl Drop for CreatedPath { fn validate_shared_directory(path: &Path) -> io::Result<()> { let metadata = fs::symlink_metadata(path)?; let uid = unsafe { libc::geteuid() }; - if !metadata.is_dir() || metadata.file_type().is_symlink() || (metadata.uid() != 0 && metadata.uid() != uid) { + if !metadata.is_dir() + || metadata.file_type().is_symlink() + || (metadata.uid() != 0 && metadata.uid() != uid) + { return Err(io::Error::new( io::ErrorKind::PermissionDenied, format!("untrusted X11 directory: {}", path.display()), @@ -93,10 +96,11 @@ impl Reservation { { // Owned inputs, so an ordinary test never competes for a real slot in // `/tmp/.X11-unix` nor depends on that directory existing. - let (sockets, locks) = crate::xwayland::test_hooks::allocator_root_for_reservation().unwrap_or_else(|| { - let root = crate::xwayland::test_hooks::default_allocator_root(); - (root.clone(), root) - }); + let (sockets, locks) = crate::xwayland::test_hooks::allocator_root_for_reservation() + .unwrap_or_else(|| { + let root = crate::xwayland::test_hooks::default_allocator_root(); + (root.clone(), root) + }); Self::allocate_in(&sockets, &locks, 0..64) } @@ -104,7 +108,11 @@ impl Reservation { Self::allocate_in(Path::new("/tmp/.X11-unix"), Path::new("/tmp"), 0..64) } - fn allocate_in(socket_directory: &Path, lock_directory: &Path, candidates: Range) -> io::Result { + fn allocate_in( + socket_directory: &Path, + lock_directory: &Path, + candidates: Range, + ) -> io::Result { validate_shared_directory(socket_directory)?; validate_shared_directory(lock_directory)?; @@ -124,7 +132,9 @@ impl Reservation { }; let lock_path = CreatedPath::record(path, &file.metadata()?); - if let Err(error) = writeln!(file, "{:>10}", std::process::id()).and_then(|()| file.flush()) { + if let Err(error) = + writeln!(file, "{:>10}", std::process::id()).and_then(|()| file.flush()) + { drop(file); drop(lock_path); return Err(error); @@ -227,7 +237,10 @@ mod tests { use std::os::unix::fs::PermissionsExt; fn shared_root(name: &str) -> PathBuf { - let root = std::env::temp_dir().join(format!("ferese-xwayland-alloc-{name}-{}", std::process::id())); + let root = std::env::temp_dir().join(format!( + "ferese-xwayland-alloc-{name}-{}", + std::process::id() + )); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).unwrap(); for directory in ["sockets", "locks"] { @@ -240,10 +253,13 @@ mod tests { #[test] fn allocates_a_display_with_two_listener_transports() { let root = shared_root("two-transports"); - let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); + let reservation = + Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); assert!(reservation.listeners().len() >= 2); - let socket = root.join("sockets").join(format!("X{}", reservation.display_number())); + let socket = root + .join("sockets") + .join(format!("X{}", reservation.display_number())); assert!(socket.exists(), "conventional socket path must exist"); assert!( root.join("locks") @@ -276,14 +292,23 @@ mod tests { fs::write(&stale, b"9999\n").unwrap(); let reservation = Reservation::allocate_in(&root.join("sockets"), &locks, 0..64).unwrap(); - assert_ne!(reservation.display_number(), 3, "occupied number must be skipped"); - assert_eq!(fs::read(&stale).unwrap(), b"9999\n", "stale lock must not be reclaimed"); + assert_ne!( + reservation.display_number(), + 3, + "occupied number must be skipped" + ); + assert_eq!( + fs::read(&stale).unwrap(), + b"9999\n", + "stale lock must not be reclaimed" + ); } #[test] fn cleanup_removes_paths_this_process_created() { let root = shared_root("cleanup"); - let reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); + let reservation = + Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); let socket = root.join("sockets").join(format!("X{display}")); let lock = root.join("locks").join(format!(".X{display}-lock")); @@ -297,20 +322,26 @@ mod tests { #[test] fn close_listeners_keeps_the_display_lock_owned() { let root = shared_root("close-listeners"); - let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); + let mut reservation = + Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); reservation.close_listeners(); assert!(reservation.listeners().is_empty()); - assert!(root.join("locks").join(format!(".X{display}-lock")).exists()); + assert!( + root.join("locks") + .join(format!(".X{display}-lock")) + .exists() + ); assert!(!root.join("sockets").join(format!("X{display}")).exists()); } #[test] fn retry_rebinds_the_same_display_number() { let root = shared_root("retry"); - let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); + let mut reservation = + Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); let display = reservation.display_number(); reservation.close_listeners(); @@ -324,7 +355,8 @@ mod tests { #[test] fn binding_twice_is_rejected() { let root = shared_root("double-bind"); - let mut reservation = Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); + let mut reservation = + Reservation::allocate_in(&root.join("sockets"), &root.join("locks"), 0..64).unwrap(); reservation.close_listeners(); reservation.bind_same_display().unwrap(); @@ -334,7 +366,8 @@ mod tests { #[test] fn an_untrusted_socket_directory_is_rejected() { - let root = std::env::temp_dir().join(format!("ferese-xwayland-untrusted-{}", std::process::id())); + let root = + std::env::temp_dir().join(format!("ferese-xwayland-untrusted-{}", std::process::id())); let _ = fs::remove_dir_all(&root); fs::create_dir_all(&root).unwrap(); @@ -352,7 +385,8 @@ mod tests { #[test] fn a_missing_socket_directory_is_an_actionable_error() { let root = shared_root("missing-dir"); - let error = Reservation::allocate_in(&root.join("absent"), &root.join("locks"), 0..64).unwrap_err(); + let error = + Reservation::allocate_in(&root.join("absent"), &root.join("locks"), 0..64).unwrap_err(); assert_eq!(error.kind(), io::ErrorKind::NotFound); } diff --git a/crates/ferese-activities/src/lib.rs b/crates/ferese-activities/src/lib.rs index afd87ad..500e4e3 100644 --- a/crates/ferese-activities/src/lib.rs +++ b/crates/ferese-activities/src/lib.rs @@ -28,7 +28,12 @@ pub struct Id { } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(tag = "kind", content = "value", rename_all = "kebab-case", deny_unknown_fields)] +#[serde( + tag = "kind", + content = "value", + rename_all = "kebab-case", + deny_unknown_fields +)] pub enum Value { Indeterminate, Progress(f32), @@ -51,7 +56,12 @@ impl Value { } #[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] -#[serde(tag = "kind", content = "data", rename_all = "kebab-case", deny_unknown_fields)] +#[serde( + tag = "kind", + content = "data", + rename_all = "kebab-case", + deny_unknown_fields +)] pub enum Template { Progress, Timer, @@ -96,20 +106,39 @@ pub struct Data { impl Data { pub fn validate(&mut self, now: Time) -> Result<(), &'static str> { text(&self.title)?; - if self.output.as_ref().is_some_and(|output| text(output).is_err()) { + if self + .output + .as_ref() + .is_some_and(|output| text(output).is_err()) + { return Err("invalid output name"); } - if self.icon.len() > 64 || !self.icon.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') { + if self.icon.len() > 64 + || !self + .icon + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + { return Err("icon must be a symbolic name"); } if !matches!( self.icon.as_str(), - "hammer" | "timer" | "download" | "media" | "microphone" | "camera" | "call" | "notification" | "ferese" + "hammer" + | "timer" + | "download" + | "media" + | "microphone" + | "camera" + | "call" + | "notification" + | "ferese" ) { self.icon = "ferese".into(); } match &mut self.value { - Value::Progress(progress) if !progress.is_finite() || !(0.0..=1.0).contains(progress) => { + Value::Progress(progress) + if !progress.is_finite() || !(0.0..=1.0).contains(progress) => + { return Err("progress must be finite and between zero and one"); } Value::Text(value) => text(value)?, @@ -128,15 +157,28 @@ impl Data { _ => {} } match &self.template { - Template::Timer if !matches!(self.value, Value::Countdown { .. } | Value::PausedCountdown { .. }) => { + Template::Timer + if !matches!( + self.value, + Value::Countdown { .. } | Value::PausedCountdown { .. } + ) => + { return Err("timer requires a countdown"); } - Template::Progress if matches!(self.value, Value::Countdown { .. } | Value::PausedCountdown { .. }) => { + Template::Progress + if matches!( + self.value, + Value::Countdown { .. } | Value::PausedCountdown { .. } + ) => + { return Err("countdown requires timer template"); } Template::Media { artist, .. } => { text(artist)?; - if !matches!(self.value, Value::Text(_) | Value::Indeterminate | Value::Progress(_)) { + if !matches!( + self.value, + Value::Text(_) | Value::Indeterminate | Value::Progress(_) + ) { return Err("invalid media value"); } } @@ -211,7 +253,9 @@ impl Activity { now >= self.eligible_at && match self.state { State::Running => self.lease_deadline.is_none_or(|deadline| now < deadline), - State::Done | State::Failed => !dnd && self.result_deadline.is_some_and(|deadline| now < deadline), + State::Done | State::Failed => { + !dnd && self.result_deadline.is_some_and(|deadline| now < deadline) + } State::Cancelled => false, } } @@ -258,7 +302,9 @@ impl Store { } pub fn resolve(&self, owner: Owner, local: &str) -> Option<&Id> { - self.entries.keys().find(|id| id.owner == owner && id.local == local) + self.entries + .keys() + .find(|id| id.owner == owner && id.local == local) } #[allow(clippy::too_many_arguments)] @@ -290,7 +336,10 @@ impl Store { if self.entries.keys().filter(|id| id.owner == owner).count() >= 3 { return Err("owner activity quota exceeded"); } - self.generation = self.generation.checked_add(1).ok_or("generation exhausted")?; + self.generation = self + .generation + .checked_add(1) + .ok_or("generation exhausted")?; let id = Id { owner, local, @@ -351,7 +400,8 @@ impl Store { validate_lease(Some(ms), true)?; entry.lease_deadline = Some(now.saturating_add(ms)); } else if let Some(deadline) = entry.lease_deadline { - entry.lease_deadline = Some(now.saturating_add(deadline.saturating_sub(entry.refreshed_at))); + entry.lease_deadline = + Some(now.saturating_add(deadline.saturating_sub(entry.refreshed_at))); } entry.refreshed_at = now; Ok(()) @@ -380,7 +430,10 @@ impl Store { Err("terminal result already accepted") }; } - if state == State::Cancelled || now < entry.eligible_at || (state == State::Failed && !entry.flash_on_fail) { + if state == State::Cancelled + || now < entry.eligible_at + || (state == State::Failed && !entry.flash_on_fail) + { self.entries.remove(id); return Ok(()); } @@ -409,7 +462,11 @@ impl Store { pub fn attach_output(&mut self, output: &str) -> bool { let mut changed = false; - for entry in self.entries.values_mut().filter(|entry| entry.data.output.is_none()) { + for entry in self + .entries + .values_mut() + .filter(|entry| entry.data.output.is_none()) + { entry.data.output = Some(output.into()); changed = true; } @@ -433,9 +490,11 @@ impl Store { Some(entry.eligible_at.saturating_add(BOOST_MS)), entry.lease_deadline, entry.result_deadline, - entry - .result_deadline - .map(|deadline| deadline.saturating_sub(entry.result_flash_ms).saturating_add(400)), + entry.result_deadline.map(|deadline| { + deadline + .saturating_sub(entry.result_flash_ms) + .saturating_add(400) + }), ] }) .flatten() @@ -497,7 +556,13 @@ impl Selection { pub fn override_pin(&mut self) { self.pinned = None; } - pub fn select(&mut self, ranked: &[&Activity], limits: Limits, dnd: bool, protected_slots: usize) { + pub fn select( + &mut self, + ranked: &[&Activity], + limits: Limits, + dnd: bool, + protected_slots: usize, + ) { let policy = (dnd, protected_slots, limits.max_visible); if self.policy != Some(policy) { self.override_pin(); @@ -557,7 +622,9 @@ pub fn reconcile_focus(focus: &mut Option, previous: &[Id], current: &[Id]) .as_ref() .and_then(|id| previous.iter().position(|old| old == id)) .unwrap_or(0); - *focus = current.get(index.min(current.len().saturating_sub(1))).cloned(); + *focus = current + .get(index.min(current.len().saturating_sub(1))) + .cloned(); } #[cfg(test)] diff --git a/crates/ferese-activities/src/pomodoro.rs b/crates/ferese-activities/src/pomodoro.rs index 10053ce..f04024b 100644 --- a/crates/ferese-activities/src/pomodoro.rs +++ b/crates/ferese-activities/src/pomodoro.rs @@ -131,7 +131,11 @@ impl Session { if let Some(phase) = phase { self.phase = phase; self.remaining_ms = self.settings.duration(phase); - self.status = if running { Status::Running } else { Status::Ready }; + self.status = if running { + Status::Running + } else { + Status::Ready + }; if running { self.deadline = Some(now.saturating_add(self.remaining_ms)); } @@ -210,7 +214,13 @@ mod tests { ] { let deadline = session.deadline.unwrap(); assert!(session.advance(deadline - 1).is_none()); - assert_eq!(session.advance(deadline), Some(Completion { phase: finished, next })); + assert_eq!( + session.advance(deadline), + Some(Completion { + phase: finished, + next + }) + ); assert_eq!(session.completed_work, count); assert!(session.advance(deadline + 99_000_000).is_none()); if next.is_some() { diff --git a/crates/ferese-activities/src/tests.rs b/crates/ferese-activities/src/tests.rs index 55e621c..66b295f 100644 --- a/crates/ferese-activities/src/tests.rs +++ b/crates/ferese-activities/src/tests.rs @@ -1,7 +1,10 @@ use super::*; fn owner() -> Owner { - Owner { pid: 42, birth: 100 } + Owner { + pid: 42, + birth: 100, + } } fn data(priority: u8) -> Data { Data { @@ -84,9 +87,21 @@ fn fast_failures_cancel_and_suppressed_failure_never_flash() { } let mut store = Store::new(1); let id = store - .start(owner(), "a".into(), data(100), 0, 0, None, true, false, None) + .start( + owner(), + "a".into(), + data(100), + 0, + 0, + None, + true, + false, + None, + ) + .unwrap(); + store + .finish(owner(), &id, State::Failed, None, 300) .unwrap(); - store.finish(owner(), &id, State::Failed, None, 300).unwrap(); assert_eq!(store.entries().count(), 0); } @@ -98,7 +113,9 @@ fn result_retry_and_owner_exit_preserve_original_deadline() { .finish(owner(), &id, State::Failed, Some("Failed".into()), 300) .unwrap(); assert_eq!(store.entries().next().unwrap().score(300, 255), 385); - store.finish(owner(), &id, State::Failed, None, 1_000).unwrap(); + store + .finish(owner(), &id, State::Failed, None, 1_000) + .unwrap(); store.owner_exit(owner()); assert_eq!(store.entries().next().unwrap().result_deadline, Some(1_800)); assert!(!store.advance(1_799)); @@ -110,7 +127,17 @@ fn result_retry_and_owner_exit_preserve_original_deadline() { fn leases_refresh_but_abandonment_never_claims_a_result() { let mut store = Store::new(1); let id = store - .start(owner(), "a".into(), data(100), 0, 0, Some(500), false, true, None) + .start( + owner(), + "a".into(), + data(100), + 0, + 0, + Some(500), + false, + true, + None, + ) .unwrap(); store.update(owner(), &id, None, None, 499).unwrap(); assert_eq!(store.entries().next().unwrap().lease_deadline, Some(999)); @@ -132,7 +159,10 @@ fn leases_refresh_but_abandonment_never_claims_a_result() { fn owner_session_generation_and_quotas_are_independent_of_connections() { let mut store = Store::new(12); let id = start(&mut store, "a", 0, 100); - let other = Owner { pid: 42, birth: 101 }; + let other = Owner { + pid: 42, + birth: 101, + }; assert!(store.update(other, &id, None, None, 0).is_err()); let mut stale = id.clone(); stale.session = 11; @@ -149,7 +179,9 @@ fn owner_session_generation_and_quotas_are_independent_of_connections() { .start(owner(), "d".into(), data(100), 0, 0, None, true, true, None) .is_err() ); - store.finish(owner(), &id, State::Cancelled, None, 0).unwrap(); + store + .finish(owner(), &id, State::Cancelled, None, 0) + .unwrap(); let replacement = start(&mut store, "a", 0, 100); assert_ne!(replacement.generation, id.generation); assert!(store.update(owner(), &id, None, None, 0).is_err()); @@ -215,7 +247,10 @@ fn dnd_protected_slots_and_output_routing() { assert_eq!(selection.hidden, 2); store.remove_output("one", Some("two")); assert!(ranked(store.entries(), 300, store.limits, "one", false).is_empty()); - assert_eq!(ranked(store.entries(), 300, store.limits, "two", false).len(), 2); + assert_eq!( + ranked(store.entries(), 300, store.limits, "two", false).len(), + 2 + ); } #[test] @@ -280,9 +315,21 @@ fn notification_boost_cannot_outrank_ordinary_and_grouped_lifetimes_remain_indep let mut notification = data(255); notification.source = Source::Notification; let notice = store - .start(owner(), "notice".into(), notification, 0, 0, None, true, true, None) + .start( + owner(), + "notice".into(), + notification, + 0, + 0, + None, + true, + true, + None, + ) + .unwrap(); + store + .finish(owner(), ¬ice, State::Failed, None, 300) .unwrap(); - store.finish(owner(), ¬ice, State::Failed, None, 300).unwrap(); let entries = ranked(store.entries(), 300, store.limits, "one", false); assert_eq!(entries.last().unwrap().id, notice); let mut selection = Selection::default(); @@ -298,14 +345,27 @@ fn notification_boost_cannot_outrank_ordinary_and_grouped_lifetimes_remain_indep ); store.remove_output("one", None); assert!(store.attach_output("two")); - assert_eq!(ranked(store.entries(), 301, store.limits, "two", false).len(), 3); + assert_eq!( + ranked(store.entries(), 301, store.limits, "two", false).len(), + 3 + ); } #[test] fn invalid_updates_are_atomic_and_suspend_aware_eligibility_hides_expired_records() { let mut store = Store::new(1); let id = store - .start(owner(), "atomic".into(), data(100), 0, 0, Some(1_000), true, true, None) + .start( + owner(), + "atomic".into(), + data(100), + 0, + 0, + Some(1_000), + true, + true, + None, + ) .unwrap(); assert!( store diff --git a/crates/ferese-animation/src/gesture.rs b/crates/ferese-animation/src/gesture.rs index a8e502a..8969edb 100644 --- a/crates/ferese-animation/src/gesture.rs +++ b/crates/ferese-animation/src/gesture.rs @@ -59,7 +59,11 @@ impl VelocityTracker { if divisor.abs() < 1e-12 { let (first, position) = self.samples[0]; let dt = last.saturating_sub(first).as_secs_f64(); - return if dt > 0.0 { (origin - position) / dt } else { 0.0 }; + return if dt > 0.0 { + (origin - position) / dt + } else { + 0.0 + }; } for entry in &mut matrix[col][col..=degree + 1] { *entry /= divisor; @@ -70,7 +74,10 @@ impl VelocityTracker { continue; } let factor = values[col]; - for (entry, pivot) in values[col..=degree + 1].iter_mut().zip(&pivot_row[col..=degree + 1]) { + for (entry, pivot) in values[col..=degree + 1] + .iter_mut() + .zip(&pivot_row[col..=degree + 1]) + { *entry -= factor * pivot; } } @@ -89,7 +96,12 @@ pub fn project(position: f64, velocity: f64, rate: f64) -> f64 { /// Nonlinear resistance, bounded by `extent`; the derivative transfers the /// velocity of the displayed (resisted) position into the release spring. pub fn rubber_band(distance: f64, extent: f64, resistance: f64) -> (f64, f64) { - if !distance.is_finite() || !extent.is_finite() || extent <= 0.0 || !resistance.is_finite() || resistance <= 0.0 { + if !distance.is_finite() + || !extent.is_finite() + || extent <= 0.0 + || !resistance.is_finite() + || resistance <= 0.0 + { return (0.0, 0.0); } let denominator = 1.0 + resistance * distance.abs() / extent; @@ -108,7 +120,9 @@ mod tests { let (position, derivative) = rubber_band(x, 64.0, 0.55); assert!((0.0..64.0).contains(&position)); assert_eq!(rubber_band(-x, 64.0, 0.55).0, -position); - let measured = (rubber_band(x + 0.0001, 64.0, 0.55).0 - rubber_band(x - 0.0001, 64.0, 0.55).0) / 0.0002; + let measured = (rubber_band(x + 0.0001, 64.0, 0.55).0 + - rubber_band(x - 0.0001, 64.0, 0.55).0) + / 0.0002; assert!((measured - derivative).abs() < 1e-6); } } diff --git a/crates/ferese-animation/src/lib.rs b/crates/ferese-animation/src/lib.rs index d394d94..948eea8 100644 --- a/crates/ferese-animation/src/lib.rs +++ b/crates/ferese-animation/src/lib.rs @@ -113,7 +113,12 @@ impl AnimatedValue { self.advance_with_policy(delta, config, config.crossing) } - pub fn advance_with_policy(&mut self, delta: Duration, config: SpringConfig, policy: CrossingPolicy) -> bool { + pub fn advance_with_policy( + &mut self, + delta: Duration, + config: SpringConfig, + policy: CrossingPolicy, + ) -> bool { let config = config.normalized(); let settled = |value: &Self| { (value.current - value.target).abs() <= config.position_tolerance @@ -167,7 +172,12 @@ impl AnimatedRect { self.advance_with_policy(delta, config, config.crossing) } - pub fn advance_with_policy(&mut self, delta: Duration, config: SpringConfig, policy: CrossingPolicy) -> bool { + pub fn advance_with_policy( + &mut self, + delta: Duration, + config: SpringConfig, + policy: CrossingPolicy, + ) -> bool { let config = config.normalized(); if delta.is_zero() { return !self.is_settled(config); @@ -278,7 +288,8 @@ impl ClientGeometry { /// The hard bound for the current uninterrupted client-buffer wait. pub fn commit_deadline(&self) -> Option { - self.waiting_since.map(|started| started + CLIENT_COMMIT_TIMEOUT) + self.waiting_since + .map(|started| started + CLIENT_COMMIT_TIMEOUT) } pub fn timed_out(&self, now: Duration) -> bool { @@ -354,7 +365,12 @@ impl WindowGeometry { self.client.request_size(size, now).then_some(size) } - pub fn set_presentation_target(&mut self, rect: Rect, fullscreen: bool, now: Duration) -> Option { + pub fn set_presentation_target( + &mut self, + rect: Rect, + fullscreen: bool, + now: Duration, + ) -> Option { self.set_presentation_mode( rect, if fullscreen { @@ -366,7 +382,12 @@ impl WindowGeometry { ) } - pub fn set_presentation_mode(&mut self, rect: Rect, mode: PresentationMode, now: Duration) -> Option { + pub fn set_presentation_mode( + &mut self, + rect: Rect, + mode: PresentationMode, + now: Duration, + ) -> Option { let rect = normalized_rect(rect); let before = (self.visual.current, self.decorations); if self.mode != mode || (self.zoom.is_some() && self.logical != rect) { @@ -374,7 +395,11 @@ impl WindowGeometry { .zoom .map(|zoom| zoom.decorations) .unwrap_or_else(|| AnimatedValue::new(self.decorations)); - decorations.set_target(if mode == PresentationMode::Fullscreen { 0.0 } else { 1.0 }); + decorations.set_target(if mode == PresentationMode::Fullscreen { + 0.0 + } else { + 1.0 + }); self.zoom = Some(ZoomTransition { decorations }); } self.mode = mode; @@ -416,14 +441,24 @@ impl WindowGeometry { requested_size } - pub fn advance(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool { + pub fn advance( + &mut self, + delta: Duration, + config: SpringConfig, + animations_enabled: bool, + ) -> bool { let previous = (self.visual.current, self.decorations); let active = self.advance_presentation(delta, config, animations_enabled); self.presentation_changed = previous != (self.visual.current, self.decorations); active } - fn advance_presentation(&mut self, delta: Duration, config: SpringConfig, animations_enabled: bool) -> bool { + fn advance_presentation( + &mut self, + delta: Duration, + config: SpringConfig, + animations_enabled: bool, + ) -> bool { if !animations_enabled { self.visual.snap(); self.decorations = if self.is_fullscreen() { 0.0 } else { 1.0 }; @@ -483,7 +518,9 @@ fn advance_property( config: SpringConfig, policy: CrossingPolicy, ) { - if policy == CrossingPolicy::NoCrossing && spring::crosses(*current, *velocity, target, dt, config) { + if policy == CrossingPolicy::NoCrossing + && spring::crosses(*current, *velocity, target, dt, config) + { *current = target; *velocity = 0.0; } else { @@ -492,7 +529,13 @@ fn advance_property( } #[cfg(test)] -fn advance_value(current: &mut f64, target: f64, velocity: &mut f64, dt: f64, config: SpringConfig) { +fn advance_value( + current: &mut f64, + target: f64, + velocity: &mut f64, + dt: f64, + config: SpringConfig, +) { (*current, *velocity) = spring::solve(*current, *velocity, target, dt, config); } @@ -560,16 +603,31 @@ mod tests { )); } assert_eq!(client.commit_deadline(), Some(Duration::from_millis(500))); - assert!(!client.commit(ClientSize { width: 49, height: 100 })); + assert!(!client.commit(ClientSize { + width: 49, + height: 100 + })); assert!(client.expire_wait(Duration::from_millis(499)).is_none()); assert_eq!( client.expire_wait(Duration::from_millis(500)), - Some(ClientSize { width: 50, height: 100 }) + Some(ClientSize { + width: 50, + height: 100 + }) ); assert_eq!(client.commit_deadline(), None); - assert!(client.request_size(ClientSize { width: 51, height: 100 }, Duration::from_millis(600))); + assert!(client.request_size( + ClientSize { + width: 51, + height: 100 + }, + Duration::from_millis(600) + )); assert_eq!(client.commit_deadline(), Some(Duration::from_millis(1100))); - assert!(client.commit(ClientSize { width: 51, height: 100 })); + assert!(client.commit(ClientSize { + width: 51, + height: 100 + })); assert_eq!(client.commit_deadline(), None); } @@ -634,10 +692,17 @@ mod tests { assert_eq!(geometry.visual.current, rect); assert_eq!( geometry.decorations, - if mode == PresentationMode::Fullscreen { 0.0 } else { 1.0 } + if mode == PresentationMode::Fullscreen { + 0.0 + } else { + 1.0 + } ); assert!(!geometry.is_zooming()); - assert_eq!(geometry.is_fullscreen(), mode == PresentationMode::Fullscreen); + assert_eq!( + geometry.is_fullscreen(), + mode == PresentationMode::Fullscreen + ); } } @@ -664,7 +729,9 @@ mod tests { fn zoom_finishes_without_a_visible_final_edge_jump() { let tiled = Rect::new(12.0, 12.0, 942.0, 1056.0); let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0); - for (start, target, fullscreen_target) in [(tiled, fullscreen, true), (fullscreen, tiled, false)] { + for (start, target, fullscreen_target) in + [(tiled, fullscreen, true), (fullscreen, tiled, false)] + { let mut geometry = WindowGeometry::new(start, None); if !fullscreen_target { geometry.set_presentation_target(start, true, Duration::ZERO); @@ -674,7 +741,11 @@ mod tests { let mut finished = false; for _ in 0..1000 { let before = geometry.visual.current; - if !geometry.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default(), true) { + if !geometry.advance( + Duration::from_secs_f64(1.0 / 144.0), + SpringConfig::default(), + true, + ) { let right_before = before.x + before.width; let right_after = target.x + target.width; assert!( @@ -701,7 +772,9 @@ mod tests { let progress = 1.0 - geometry.decorations; let visual = geometry.visual.current; assert!((visual.x - (start.x + (end.x - start.x) * progress)).abs() < 1e-9); - assert!((visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9); + assert!( + (visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9 + ); } } @@ -712,7 +785,11 @@ mod tests { let mut geometry = WindowGeometry::new(tiled, None); for enabled in [true, false, true, false] { let before = (geometry.visual.current, geometry.decorations); - geometry.set_presentation_target(if enabled { fullscreen } else { tiled }, enabled, Duration::ZERO); + geometry.set_presentation_target( + if enabled { fullscreen } else { tiled }, + enabled, + Duration::ZERO, + ); assert_eq!((geometry.visual.current, geometry.decorations), before); geometry.advance(Duration::from_millis(48), SpringConfig::default(), true); } @@ -756,15 +833,24 @@ mod tests { Some(ClientSize::from_rect(middle)) ); geometry.advance(Duration::from_millis(64), SpringConfig::default(), true); - assert_eq!(geometry.presentation_size_request(Duration::from_millis(64)), None); assert_eq!( - geometry.set_presentation_mode(end, PresentationMode::Normal, Duration::from_millis(64)), + geometry.presentation_size_request(Duration::from_millis(64)), + None + ); + assert_eq!( + geometry.set_presentation_mode( + end, + PresentationMode::Normal, + Duration::from_millis(64) + ), Some(ClientSize::from_rect(end)) ); let mut requests = Vec::new(); for frame in 1..240 { geometry.advance(Duration::from_millis(8), SpringConfig::default(), true); - if let Some(size) = geometry.presentation_size_request(Duration::from_millis(64 + frame * 8)) { + if let Some(size) = + geometry.presentation_size_request(Duration::from_millis(64 + frame * 8)) + { assert_eq!(geometry.visual.current.width, end.width); requests.push(size); } @@ -809,8 +895,13 @@ mod tests { true, ); let current = geometry.visual.current; - let edges = |rect: Rect| [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]; - for ((before, after), end) in edges(previous).into_iter().zip(edges(current)).zip(edges(target)) + let edges = |rect: Rect| { + [rect.x, rect.y, rect.x + rect.width, rect.y + rect.height] + }; + for ((before, after), end) in edges(previous) + .into_iter() + .zip(edges(current)) + .zip(edges(target)) { assert!((after - end).abs() <= (before - end).abs() + 1e-9); } @@ -967,7 +1058,11 @@ mod tests { let mut geometry = WindowGeometry::new(rect, None); assert!(geometry.set_logical_target(rect, Duration::ZERO).is_some()); - assert!(geometry.set_logical_target(rect, Duration::from_millis(16)).is_none()); + assert!( + geometry + .set_logical_target(rect, Duration::from_millis(16)) + .is_none() + ); } #[test] @@ -1026,7 +1121,10 @@ mod tests { assert!(sixty.current.width <= target.width); } for _ in 0..288 { - one_forty_four.advance(Duration::from_secs_f64(1.0 / 144.0), SpringConfig::default()); + one_forty_four.advance( + Duration::from_secs_f64(1.0 / 144.0), + SpringConfig::default(), + ); } assert!(sixty.is_settled(SpringConfig::default())); diff --git a/crates/ferese-animation/src/settings.rs b/crates/ferese-animation/src/settings.rs index 9a5e0da..1a21e84 100644 --- a/crates/ferese-animation/src/settings.rs +++ b/crates/ferese-animation/src/settings.rs @@ -117,7 +117,16 @@ mod tests { #[test] fn speed_validation_rejects_extremes_and_accepts_safe_boundaries() { - for speed in [f64::NAN, f64::INFINITY, 0.0, -1.0, 1e-100, 1e100, 0.009, 101.0] { + for speed in [ + f64::NAN, + f64::INFINITY, + 0.0, + -1.0, + 1e-100, + 1e100, + 0.009, + 101.0, + ] { let settings = MotionSettings { speed, ..Default::default() @@ -164,7 +173,10 @@ mod tests { .unwrap(); assert!((spring.stiffness - 157.913670417).abs() < 1e-8); assert!((spring.damping / (2.0 * spring.stiffness.sqrt()) - ratio).abs() < 1e-10); - assert!((std::f64::consts::TAU * (spring.mass / spring.stiffness).sqrt() - 0.5).abs() < 1e-10); + assert!( + (std::f64::consts::TAU * (spring.mass / spring.stiffness).sqrt() - 0.5).abs() + < 1e-10 + ); } } diff --git a/crates/ferese-animation/src/spring.rs b/crates/ferese-animation/src/spring.rs index 051d97d..6c31742 100644 --- a/crates/ferese-animation/src/spring.rs +++ b/crates/ferese-animation/src/spring.rs @@ -2,7 +2,13 @@ use super::SpringConfig; /// Solve m y'' + c y' + k y = 0, with y = current - target. /// No settling tolerance or crossing clamp is applied here. -pub(super) fn solve(current: f64, velocity: f64, target: f64, dt: f64, config: SpringConfig) -> (f64, f64) { +pub(super) fn solve( + current: f64, + velocity: f64, + target: f64, + dt: f64, + config: SpringConfig, +) -> (f64, f64) { if dt <= 0.0 || !dt.is_finite() { return (current, velocity); } @@ -20,7 +26,8 @@ pub(super) fn solve(current: f64, velocity: f64, target: f64, dt: f64, config: S // rather than replacing a neighborhood with the critical solution. if z2.abs() < 0.01 { let c = 1.0 + z2 * (0.5 + z2 * (1.0 / 24.0 + z2 * (1.0 / 720.0 + z2 / 40320.0))); - let s = dt * (1.0 + z2 * (1.0 / 6.0 + z2 * (1.0 / 120.0 + z2 * (1.0 / 5040.0 + z2 / 362880.0)))); + let s = dt + * (1.0 + z2 * (1.0 / 6.0 + z2 * (1.0 / 120.0 + z2 * (1.0 / 5040.0 + z2 / 362880.0)))); let e = (-a * dt).exp(); return ( target + e * (y * c + (velocity + a * y) * s), @@ -46,13 +53,22 @@ pub(super) fn solve(current: f64, velocity: f64, target: f64, dt: f64, config: S let fast_coefficient = (slow * y - velocity) / (2.0 * q); let slow_term = slow_coefficient * (slow * dt).exp(); let fast_term = fast_coefficient * (fast * dt).exp(); - (target + slow_term + fast_term, slow * slow_term + fast * fast_term) + ( + target + slow_term + fast_term, + slow * slow_term + fast * fast_term, + ) } } /// True if the physical trajectory reaches equilibrium anywhere in (0, dt]. /// Endpoint-only tests miss an even number of crossings after a long stall. -pub(super) fn crosses(current: f64, velocity: f64, target: f64, dt: f64, config: SpringConfig) -> bool { +pub(super) fn crosses( + current: f64, + velocity: f64, + target: f64, + dt: f64, + config: SpringConfig, +) -> bool { if dt <= 0.0 { return false; } @@ -70,7 +86,11 @@ pub(super) fn crosses(current: f64, velocity: f64, target: f64, dt: f64, config: let crossing = if d < 0.0 { let w = (-d).sqrt(); let phase = (-y * w).atan2(b).rem_euclid(std::f64::consts::PI); - let phase = if phase == 0.0 { std::f64::consts::PI } else { phase }; + let phase = if phase == 0.0 { + std::f64::consts::PI + } else { + phase + }; phase / w } else if y == 0.0 || b == 0.0 { return false; diff --git a/crates/ferese-animation/src/traces.rs b/crates/ferese-animation/src/traces.rs index d7654da..c7b119c 100644 --- a/crates/ferese-animation/src/traces.rs +++ b/crates/ferese-animation/src/traces.rs @@ -25,7 +25,12 @@ impl FakeClock { } } - fn sample(&mut self, time: Duration, value: &mut AnimatedValue, config: SpringConfig) -> Sample { + fn sample( + &mut self, + time: Duration, + value: &mut AnimatedValue, + config: SpringConfig, + ) -> Sample { assert!(time >= self.now); self.now = time; let delta = time - self.last; @@ -58,7 +63,8 @@ impl FakeClock { fn legacy_advance(value: &mut AnimatedValue, delta: Duration, config: SpringConfig) { let config = config.normalized(); let settled = |v: &AnimatedValue| { - (v.current - v.target).abs() <= config.position_tolerance && v.velocity.abs() <= config.velocity_tolerance + (v.current - v.target).abs() <= config.position_tolerance + && v.velocity.abs() <= config.velocity_tolerance }; if settled(value) { value.snap(); @@ -70,7 +76,8 @@ fn legacy_advance(value: &mut AnimatedValue, delta: Duration, config: SpringConf let dt = seconds / steps as f64; for _ in 0..steps { let old = value.current - value.target; - value.velocity += (-config.stiffness * old - config.damping * value.velocity) / config.mass * dt; + value.velocity += + (-config.stiffness * old - config.damping * value.velocity) / config.mass * dt; value.current += value.velocity * dt; if old != 0.0 && old.signum() != (value.current - value.target).signum() { value.snap(); @@ -91,7 +98,11 @@ fn capture_old_golden_trace_and_retarget_policy() { let mut value = AnimatedValue::new(0.0); value.set_target(500.0); for i in 1..=18 { - legacy_advance(&mut value, Duration::from_secs_f64(1.0 / 60.0), SpringConfig::default()); + legacy_advance( + &mut value, + Duration::from_secs_f64(1.0 / 60.0), + SpringConfig::default(), + ); if [1, 3, 6, 12, 18].contains(&i) { println!("old normal {i}: {}, {}", value.current, value.velocity); } @@ -99,7 +110,11 @@ fn capture_old_golden_trace_and_retarget_policy() { let mut value = AnimatedValue::new(0.0); value.set_target(1000.0); - legacy_advance(&mut value, Duration::from_millis(40), SpringConfig::default()); + legacy_advance( + &mut value, + Duration::from_millis(40), + SpringConfig::default(), + ); println!("old retarget before: {}, {}", value.current, value.velocity); let position = value.current; let velocity = value.velocity; @@ -127,7 +142,11 @@ fn long_stall_uses_the_whole_elapsed_duration() { let sample = clock.sample(Duration::from_millis(416), &mut value, config); let t: f64 = 0.416; let decay = (-10.0 * t).exp(); - close(sample.current, 500.0 - 500.0 * (1.0 + 10.0 * t) * decay, 1e-8); + close( + sample.current, + 500.0 - 500.0 * (1.0 + 10.0 * t) * decay, + 1e-8, + ); close(sample.velocity, 50000.0 * t * decay, 1e-8); assert_eq!(sample.time, Duration::from_millis(416)); assert_eq!(sample.target, 500.0); @@ -147,13 +166,25 @@ fn fake_clock_pause_excludes_three_hundred_milliseconds() { let mut clock = FakeClock::new(); let mut value = AnimatedValue::new(0.0); value.set_target(500.0); - clock.sample(Duration::from_millis(40), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(40), + &mut value, + SpringConfig::default(), + ); let frozen = value; clock.pause(); - clock.sample(Duration::from_millis(340), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(340), + &mut value, + SpringConfig::default(), + ); assert_eq!(value, frozen); clock.resume(Duration::from_millis(340)); - clock.sample(Duration::from_millis(356), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(356), + &mut value, + SpringConfig::default(), + ); let mut expected = frozen; expected.advance(Duration::from_millis(16), SpringConfig::default()); assert_eq!(value, expected); @@ -182,7 +213,10 @@ fn reference(x: f64, v: f64, target: f64, dt: f64, config: SpringConfig) -> (f64 let b = (v + a * y) / w; let (s, c) = (w * dt).sin_cos(); let e = (-a * dt).exp(); - (target + e * (y * c + b * s), e * (v * c - (a * v + omega2 * y) / w * s)) + ( + target + e * (y * c + b * s), + e * (v * c - (a * v + omega2 * y) / w * s), + ) } else { let q = d.sqrt(); let r1 = -a + q; @@ -197,7 +231,15 @@ fn reference(x: f64, v: f64, target: f64, dt: f64, config: SpringConfig) -> (f64 #[test] fn solver_reference_regimes_initial_states_and_times() { - for damping in [0.0, 5.0, 20.0, 20.0 * (1.0 - 1e-9), 20.0 * (1.0 + 1e-9), 53.0, 2000.0] { + for damping in [ + 0.0, + 5.0, + 20.0, + 20.0 * (1.0 - 1e-9), + 20.0 * (1.0 + 1e-9), + 53.0, + 2000.0, + ] { let config = SpringConfig { stiffness: 100.0, damping, @@ -256,7 +298,11 @@ fn normal_trace_records_the_old_numerical_error() { old.set_target(500.0); let mut max_error: f64 = 0.0; for i in 1..=18 { - legacy_advance(&mut old, Duration::from_secs_f64(1.0 / 60.0), SpringConfig::default()); + legacy_advance( + &mut old, + Duration::from_secs_f64(1.0 / 60.0), + SpringConfig::default(), + ); if let Some((_, x, v)) = gold.iter().find(|(n, _, _)| *n == i) { close(old.current, *x, 1e-9); close(old.velocity, *v, 1e-9); @@ -303,7 +349,11 @@ fn gesture_flick_trace_crosses_once_and_settles_while_clamp_stops_at_crossing() let mut crossings = 0; let mut previous_error: f64 = -100.0; for ms in 1..=1500 { - free.advance_with_policy(Duration::from_millis(1), config, CrossingPolicy::AllowOvershoot); + free.advance_with_policy( + Duration::from_millis(1), + config, + CrossingPolicy::AllowOvershoot, + ); clamped.advance_with_policy(Duration::from_millis(1), config, CrossingPolicy::NoCrossing); let error = free.current - free.target; if error != 0.0 && previous_error.signum() != error.signum() { @@ -332,17 +382,29 @@ fn rapid_retarget_trace_preserves_position_and_the_golden_policy() { let mut clock = FakeClock::new(); let mut value = AnimatedValue::new(0.0); value.set_target(1000.0); - clock.sample(Duration::from_millis(40), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(40), + &mut value, + SpringConfig::default(), + ); let before = value; value.retarget_preserving_motion(2000.0); assert_eq!(value.current, before.current); assert_eq!(value.velocity, before.velocity); - clock.sample(Duration::from_millis(65), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(65), + &mut value, + SpringConfig::default(), + ); let before = value; value.retarget_preserving_motion(-500.0); assert_eq!(value.current, before.current); assert_eq!(value.velocity, before.velocity); - clock.sample(Duration::from_millis(81), &mut value, SpringConfig::default()); + clock.sample( + Duration::from_millis(81), + &mut value, + SpringConfig::default(), + ); assert_ne!(value.velocity, 0.0); // Apply the current policy to the exact old in-flight state too. @@ -366,7 +428,11 @@ fn interrupted_resize_and_zoom_targets_preserve_position_and_velocity() { ] { let before = (geometry.visual.current, geometry.decorations); let velocity = geometry.visual.velocity; - geometry.set_presentation_mode(Rect::new(200., 100., 700., 500.), mode, Duration::from_millis(40)); + geometry.set_presentation_mode( + Rect::new(200., 100., 700., 500.), + mode, + Duration::from_millis(40), + ); assert_eq!((geometry.visual.current, geometry.decorations), before); assert_eq!(geometry.visual.velocity, velocity); geometry.advance(Duration::ZERO, SpringConfig::default(), true); @@ -418,7 +484,10 @@ fn variable_cadence_traces_match_reference_at_every_absolute_timestamp() { reference_settle_us += 100; } let exact_settle = Duration::from_micros(reference_settle_us); - assert!(settled_at.abs_diff(exact_settle) <= Duration::from_micros(*schedule.iter().max().unwrap() + 100)); + assert!( + settled_at.abs_diff(exact_settle) + <= Duration::from_micros(*schedule.iter().max().unwrap() + 100) + ); } } @@ -476,7 +545,10 @@ fn solver_is_finite_for_realistic_ranges_and_invalid_configuration_falls_back() ..SpringConfig::default() }; let (x, v) = physical(-10000., 50000., 10000., dt, config); - assert!(x.is_finite() && v.is_finite(), "{config:?} dt {dt}: {x} {v}"); + assert!( + x.is_finite() && v.is_finite(), + "{config:?} dt {dt}: {x} {v}" + ); } } } @@ -514,7 +586,10 @@ fn solver_is_finite_for_realistic_ranges_and_invalid_configuration_falls_back() #[test] fn physical_zero_and_negative_time_do_not_move_state() { for dt in [0.0, -0.01] { - assert_eq!(physical(5.0, 100.0, 10.0, dt, SpringConfig::default()), (5.0, 100.0)); + assert_eq!( + physical(5.0, 100.0, 10.0, dt, SpringConfig::default()), + (5.0, 100.0) + ); } } @@ -556,7 +631,11 @@ fn creation_clock_excludes_earlier_idle_time_and_records_all_sample_fields() { clock.resume(created); let mut value = AnimatedValue::new(0.0); value.set_target(500.0); - let first = clock.sample(created + Duration::from_millis(16), &mut value, SpringConfig::default()); + let first = clock.sample( + created + Duration::from_millis(16), + &mut value, + SpringConfig::default(), + ); let exact = reference(0.0, 0.0, 500.0, 0.016, SpringConfig::default()); close(first.current, exact.0, 1e-8); close(first.velocity, exact.1, 1e-8); diff --git a/crates/ferese-config/src/activities.rs b/crates/ferese-config/src/activities.rs index 475f330..572461a 100644 --- a/crates/ferese-config/src/activities.rs +++ b/crates/ferese-config/src/activities.rs @@ -114,13 +114,19 @@ impl Matcher { return None; } for (rule, regex) in &self.rules { - if parents.iter().any(|parent| rule.ignore_parents.contains(parent)) { + if parents + .iter() + .any(|parent| rule.ignore_parents.contains(parent)) + { continue; } if let Some(captures) = regex.captures(command) { let mut title = rule.title.clone(); for index in 0..captures.len() { - title = title.replace(&format!("{{{index}}}"), captures.get(index).map_or("", |m| m.as_str())); + title = title.replace( + &format!("{{{index}}}"), + captures.get(index).map_or("", |m| m.as_str()), + ); } if title.len() > 256 || title.chars().any(char::is_control) { return None; @@ -149,14 +155,22 @@ fn validate_rule(rule: &Rule, regex: &Regex) -> Result<(), String> { if rule.pattern.len() > 2_048 || rule.min_duration_ms > 60_000 || !matches!(rule.progress.as_str(), "indeterminate" | "cargo-json") - || rule.dedupe_by.as_deref().is_some_and(|value| value != "cwd") + || rule + .dedupe_by + .as_deref() + .is_some_and(|value| value != "cwd") { return Err("invalid rule duration, progress adapter or dedupe policy".into()); } if rule.ignore_parents.len() > 16 || rule.ignore_parents.iter().any(|name| name.len() > 128) { return Err("too many or too long parent names".into()); } - if rule.icon.len() > 64 || !rule.icon.bytes().all(|b| b.is_ascii_alphanumeric() || b == b'-') { + if rule.icon.len() > 64 + || !rule + .icon + .bytes() + .all(|b| b.is_ascii_alphanumeric() || b == b'-') + { return Err("invalid symbolic icon".into()); } let capture = Regex::new(r"\{([^}]+)\}").expect("static pattern"); @@ -201,7 +215,10 @@ impl Settings { } fn diagnostic(node: &KdlNode, message: impl std::fmt::Display) -> Error { - Error(format!("activities at byte {}: {message}", node.span().offset())) + Error(format!( + "activities at byte {}: {message}", + node.span().offset() + )) } pub(crate) fn parse(node: &KdlNode) -> Result { @@ -241,7 +258,12 @@ pub(crate) fn parse(node: &KdlNode) -> Result { } let key = name.replace('-', "_"); let parsed = if name == "ignore-parents" { - if property.entries().iter().any(|entry| entry.name().is_some()) || property.entries().is_empty() { + if property + .entries() + .iter() + .any(|entry| entry.name().is_some()) + || property.entries().is_empty() + { return Err(diagnostic(property, "ignore-parents requires names")); } Value::Array( @@ -252,7 +274,8 @@ pub(crate) fn parse(node: &KdlNode) -> Result { .collect::>()?, ) } else { - crate::node_value(property, &key, "rule").map_err(|error| diagnostic(property, error))? + crate::node_value(property, &key, "rule") + .map_err(|error| diagnostic(property, error))? }; if value.insert(key, parsed).is_some() { return Err(diagnostic(property, "duplicate rule setting")); @@ -268,12 +291,15 @@ pub(crate) fn parse(node: &KdlNode) -> Result { value .entry("result") .or_insert(json!({"ok":"Finished","fail":"Command failed"})); - let rule: Rule = serde_json::from_value(Value::Object(value)).map_err(|error| diagnostic(child, error))?; + let rule: Rule = serde_json::from_value(Value::Object(value)) + .map_err(|error| diagnostic(child, error))?; let settings = Settings { rules: vec![rule.clone()], ..Default::default() }; - settings.validate().map_err(|error| diagnostic(child, error))?; + settings + .validate() + .map_err(|error| diagnostic(child, error))?; rules.push(rule); } else { if !matches!( @@ -290,16 +316,24 @@ pub(crate) fn parse(node: &KdlNode) -> Result { return Err(diagnostic(child, "unknown activity node")); } let field = key.replace('-', "_"); - let value = crate::node_value(child, &field, "activities").map_err(|error| diagnostic(child, error))?; + let value = crate::node_value(child, &field, "activities") + .map_err(|error| diagnostic(child, error))?; if values.insert(field, value).is_some() { return Err(diagnostic(child, "duplicate activity setting")); } } } - rules.extend(builtins().into_iter().filter(|rule| !names.contains(&rule.name))); + rules.extend( + builtins() + .into_iter() + .filter(|rule| !names.contains(&rule.name)), + ); values.insert("rules".into(), serde_json::to_value(rules).unwrap()); - let settings: Settings = serde_json::from_value(Value::Object(values)).map_err(|error| diagnostic(node, error))?; - settings.validate().map_err(|error| diagnostic(node, error))?; + let settings: Settings = + serde_json::from_value(Value::Object(values)).map_err(|error| diagnostic(node, error))?; + settings + .validate() + .map_err(|error| diagnostic(node, error))?; Ok(serde_json::to_value(settings).unwrap()) } fn builtins() -> Vec { @@ -371,7 +405,8 @@ mod tests { "work-minuts 20", ] { assert!( - crate::Document::parse(&format!("activities {{ pomodoro {{ {invalid}; }}; }}")).is_err(), + crate::Document::parse(&format!("activities {{ pomodoro {{ {invalid}; }}; }}")) + .is_err(), "{invalid}" ); } diff --git a/crates/ferese-config/src/families.rs b/crates/ferese-config/src/families.rs index 020d402..e6fdb42 100644 --- a/crates/ferese-config/src/families.rs +++ b/crates/ferese-config/src/families.rs @@ -2,12 +2,32 @@ use crate::theme::Appearance; pub use ferese_theme_model::families::{Family, Palette}; pub const BUILTINS: [(&str, &str, &str, &str); 6] = [ - ("ferese-blue", "Ferese Blue", "ferese-blue-light", "ferese-blue"), - ("catppuccin", "Catppuccin", "catppuccin-latte", "catppuccin-mocha"), + ( + "ferese-blue", + "Ferese Blue", + "ferese-blue-light", + "ferese-blue", + ), + ( + "catppuccin", + "Catppuccin", + "catppuccin-latte", + "catppuccin-mocha", + ), ("gruvbox", "Gruvbox", "gruvbox-light", "gruvbox"), ("rose-pine", "Rosé Pine", "rose-pine-dawn", "rose-pine-moon"), - ("tokyo-night", "Tokyo Night", "tokyo-night-day", "tokyo-night"), - ("everforest", "Everforest", "everforest-light", "everforest-dark"), + ( + "tokyo-night", + "Tokyo Night", + "tokyo-night-day", + "tokyo-night", + ), + ( + "everforest", + "Everforest", + "everforest-light", + "everforest-dark", + ), ]; pub fn family_id(preset: &str) -> &str { @@ -19,7 +39,11 @@ pub fn family_id(preset: &str) -> &str { pub fn variant(id: &str, appearance: Appearance) -> Option<&'static str> { if let Some(row) = BUILTINS.iter().find(|row| row.0 == id) { - return Some(if appearance == Appearance::Light { row.2 } else { row.3 }); + return Some(if appearance == Appearance::Light { + row.2 + } else { + row.3 + }); } match (id, appearance) { ("dracula", Appearance::Dark) => Some("dracula"), @@ -36,8 +60,12 @@ pub fn builtins() -> Vec { .map(|(id, name, light, dark)| Family { id: (*id).into(), name: (*name).into(), - light: Some(Palette::from(&crate::theme::preset(light, Appearance::Light).unwrap())), - dark: Some(Palette::from(&crate::theme::preset(dark, Appearance::Dark).unwrap())), + light: Some(Palette::from( + &crate::theme::preset(light, Appearance::Light).unwrap(), + )), + dark: Some(Palette::from( + &crate::theme::preset(dark, Appearance::Dark).unwrap(), + )), }) .collect() } diff --git a/crates/ferese-config/src/file.rs b/crates/ferese-config/src/file.rs index 21b8a44..4238d54 100644 --- a/crates/ferese-config/src/file.rs +++ b/crates/ferese-config/src/file.rs @@ -28,7 +28,10 @@ impl ConfigFile { .join(path.file_name().ok_or("Invalid configuration path")?), Err(error) => return Err(error.to_string()), }; - let mut lock_name = path.file_name().ok_or("Invalid configuration path")?.to_os_string(); + let mut lock_name = path + .file_name() + .ok_or("Invalid configuration path")? + .to_os_string(); lock_name.push(".lock"); let lock = OpenOptions::new() @@ -40,7 +43,9 @@ impl ConfigFile { .open(path.with_file_name(lock_name)) .map_err(|e| e.to_string())?; lock.try_lock().map_err(|error| match error { - fs::TryLockError::WouldBlock => "Configuration is being saved by another app. Please try again.".into(), + fs::TryLockError::WouldBlock => { + "Configuration is being saved by another app. Please try again.".into() + } fs::TryLockError::Error(error) => error.to_string(), })?; @@ -77,7 +82,9 @@ impl ConfigFile { .map_err(|e| e.to_string())?; } - staged.write_all(desired.as_bytes()).map_err(|e| e.to_string())?; + staged + .write_all(desired.as_bytes()) + .map_err(|e| e.to_string())?; staged.as_file().sync_all().map_err(|e| e.to_string())?; validate(staged.path())?; @@ -86,8 +93,11 @@ impl ConfigFile { } if let Some(path) = backup { - let mut previous = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?; - previous.write_all(self.source.as_bytes()).map_err(|e| e.to_string())?; + let mut previous = + tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?; + previous + .write_all(self.source.as_bytes()) + .map_err(|e| e.to_string())?; previous.as_file().sync_all().map_err(|e| e.to_string())?; previous.persist(path).map_err(|e| e.to_string())?; } @@ -187,7 +197,10 @@ mod tests { let path = directory.path().join("config.kdl"); fs::write(&path, "// initial").unwrap(); let file = ConfigFile::open(&path).unwrap(); - assert!(file.replace("// rejected", None, |_| Err("invalid".into())).is_err()); + assert!( + file.replace("// rejected", None, |_| Err("invalid".into())) + .is_err() + ); assert_eq!(ConfigFile::open(&path).unwrap().source(), "// initial"); } } diff --git a/crates/ferese-config/src/lib.rs b/crates/ferese-config/src/lib.rs index 68dc23b..e6966f1 100644 --- a/crates/ferese-config/src/lib.rs +++ b/crates/ferese-config/src/lib.rs @@ -66,7 +66,10 @@ pub fn default_wallpaper_for(appearance: theme::Appearance) -> &'static str { .join(name) }); - path.canonicalize().unwrap_or(path).to_string_lossy().into_owned() + path.canonicalize() + .unwrap_or(path) + .to_string_lossy() + .into_owned() }) } @@ -93,7 +96,12 @@ fn is_records(key: &str, parent: &str) -> bool { (parent, key), ( "", - "bindings" | "binding_modes" | "window_rules" | "output_profiles" | "autostart" | "panels" + "bindings" + | "binding_modes" + | "window_rules" + | "output_profiles" + | "autostart" + | "panels" ) | ("output_profiles", "outputs") | ("binding_modes", "bindings") | ("start" | "center" | "end", "groups") @@ -105,7 +113,13 @@ fn is_array(key: &str, parent: &str) -> bool { parent == "commands" || matches!( key, - "command" | "position" | "padding" | "width_presets" | "xkb_options" | "settings_command" | "outputs" + "command" + | "position" + | "padding" + | "width_presets" + | "xkb_options" + | "settings_command" + | "outputs" ) } @@ -114,7 +128,9 @@ fn scalar(value: &KdlValue) -> Result { KdlValue::String(s) => Value::String(s.clone()), KdlValue::Bool(v) => Value::Bool(*v), KdlValue::Null => { - return Err(Error("Omit optional settings instead of using #null".into())); + return Err(Error( + "Omit optional settings instead of using #null".into(), + )); } KdlValue::Integer(n) => { if let Ok(n) = i64::try_from(*n) { @@ -154,7 +170,9 @@ fn object(doc: &KdlDocument, parent: &str) -> Result, Error> node_value(node, &key, parent)? }; if is_records(&key, parent) { - let records = result.entry(key).or_insert_with(|| Value::Array(Vec::new())); + let records = result + .entry(key) + .or_insert_with(|| Value::Array(Vec::new())); records.as_array_mut().unwrap().push(value); } else { insert(&mut result, key, value)?; @@ -166,7 +184,9 @@ fn object(doc: &KdlDocument, parent: &str) -> Result, Error> fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result { if node.ty().is_some() || node.entries().iter().any(|e| e.ty().is_some()) { - return Err(Error("Type annotations are not supported in configuration".into())); + return Err(Error( + "Type annotations are not supported in configuration".into(), + )); } let args = node @@ -182,7 +202,11 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result { .collect::>(); if is_records(key, parent) { - let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default(); + let mut map = node + .children() + .map(|d| object(d, key)) + .transpose()? + .unwrap_or_default(); for (name, entry) in props { insert(&mut map, name, scalar(entry.value())?)?; } @@ -219,7 +243,11 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result { ))); } - let mut map = node.children().map(|d| object(d, key)).transpose()?.unwrap_or_default(); + let mut map = node + .children() + .map(|d| object(d, key)) + .transpose()? + .unwrap_or_default(); for (name, entry) in props { insert(&mut map, name, scalar(entry.value())?)?; } @@ -245,7 +273,9 @@ pub struct Document { impl Document { pub fn parse(source: &str) -> Result { - let doc = source.parse::().map_err(|e| Error(format!("{e:?}")))?; + let doc = source + .parse::() + .map_err(|e| Error(format!("{e:?}")))?; let value = Value::Object(object(&doc, "")?); Ok(Self { doc, value }) @@ -258,7 +288,8 @@ impl Document { /// A runtime view; the original KDL remains available for comment-preserving edits. pub fn with_theme(&self, theme: &theme::ResolvedTheme) -> Self { let mut document = self.clone(); - document.value["theme"] = serde_json::to_value(&theme.tokens).expect("serializable theme tokens"); + document.value["theme"] = + serde_json::to_value(&theme.tokens).expect("serializable theme tokens"); document.value["theme"]["appearance"] = serde_json::to_value(theme.appearance).unwrap(); document } @@ -317,7 +348,10 @@ impl Document { pub fn add(&mut self, path: &str, fields: Vec<(String, Value)>) -> Result<(), Error> { let mut candidate = self.clone(); let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p)); - let parent = parents.rsplit('.').find(|s| s.parse::().is_err()).unwrap_or(""); + let parent = parents + .rsplit('.') + .find(|s| s.parse::().is_err()) + .unwrap_or(""); if !is_records(last, parent) { return Err(Error("Expected a list of records".into())); @@ -325,12 +359,18 @@ impl Document { let doc = section_mut( &mut candidate.doc, - &parents.split('.').filter(|s| !s.is_empty()).collect::>(), + &parents + .split('.') + .filter(|s| !s.is_empty()) + .collect::>(), "", )?; - doc.nodes_mut() - .push(value_node(last, &Value::Object(fields.into_iter().collect()), parent)?); + doc.nodes_mut().push(value_node( + last, + &Value::Object(fields.into_iter().collect()), + parent, + )?); format_document(&mut candidate.doc); candidate.refresh()?; *self = candidate; @@ -344,11 +384,21 @@ impl Document { } if self.get("theme.light").is_some() || self.get("theme.dark").is_some() { - set_in(&mut self.doc, &["theme", "mode"], "", Value::String("dark".into()))?; + set_in( + &mut self.doc, + &["theme", "mode"], + "", + Value::String("dark".into()), + )?; return self.refresh(); } - let Some(index) = self.doc.nodes().iter().position(|node| node.name().value() == "theme") else { + let Some(index) = self + .doc + .nodes() + .iter() + .position(|node| node.name().value() == "theme") + else { return Ok(()); }; @@ -356,7 +406,11 @@ impl Document { let mut moved = Vec::new(); for name in ["colors", "surface", "border", "focus-ring"] { - if let Some(index) = children.nodes().iter().position(|node| node.name().value() == name) { + if let Some(index) = children + .nodes() + .iter() + .position(|node| node.name().value() == name) + { moved.push(children.nodes_mut().remove(index)); } } @@ -367,7 +421,12 @@ impl Document { } } - set_in(&mut self.doc, &["theme", "mode"], "", Value::String("dark".into()))?; + set_in( + &mut self.doc, + &["theme", "mode"], + "", + Value::String("dark".into()), + )?; self.refresh() } @@ -388,14 +447,18 @@ impl Document { let doc = section_mut( &mut candidate.doc, - &parents.split('.').filter(|s| !s.is_empty()).collect::>(), + &parents + .split('.') + .filter(|s| !s.is_empty()) + .collect::>(), "", )?; - if let Some(index) = doc - .nodes() - .iter() - .position(|node| field(node.name().value(), parents.rsplit('.').next().unwrap_or("")) == name) - { + if let Some(index) = doc.nodes().iter().position(|node| { + field( + node.name().value(), + parents.rsplit('.').next().unwrap_or(""), + ) == name + }) { let node = doc.nodes_mut().remove(index); let mut comments = String::new(); @@ -423,14 +486,19 @@ impl Document { let (last, parents) = path.rsplit_once('.').map_or((path, ""), |(p, l)| (l, p)); let doc = section_mut( &mut candidate.doc, - &parents.split('.').filter(|s| !s.is_empty()).collect::>(), + &parents + .split('.') + .filter(|s| !s.is_empty()) + .collect::>(), "", )?; let i = doc .nodes() .iter() .enumerate() - .filter(|(_, n)| field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last) + .filter(|(_, n)| { + field(n.name().value(), parents.rsplit('.').next().unwrap_or("")) == last + }) .nth(index) .map(|(i, _)| i) .ok_or_else(|| Error("This item no longer exists. Reload settings.".into()))?; @@ -498,7 +566,11 @@ type NodeFormat = (Vec>, Option); fn collect_node_formats(doc: &KdlDocument, formats: &mut Vec) { for node in doc.nodes() { - let entries = node.entries().iter().map(|entry| entry.format().cloned()).collect(); + let entries = node + .entries() + .iter() + .map(|entry| entry.format().cloned()) + .collect(); let terminator = node .format() .map(|format| format.terminator.clone()) @@ -543,7 +615,11 @@ fn format_document(doc: &mut KdlDocument) { } fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize { - if let Some(i) = doc.nodes().iter().position(|n| field(n.name().value(), parent) == key) { + if let Some(i) = doc + .nodes() + .iter() + .position(|n| field(n.name().value(), parent) == key) + { return i; } @@ -551,7 +627,11 @@ fn node_index(doc: &mut KdlDocument, key: &str, parent: &str) -> usize { doc.nodes().len() - 1 } -fn section_mut<'a>(doc: &'a mut KdlDocument, parts: &[&str], parent: &str) -> Result<&'a mut KdlDocument, Error> { +fn section_mut<'a>( + doc: &'a mut KdlDocument, + parts: &[&str], + parent: &str, +) -> Result<&'a mut KdlDocument, Error> { if parts.is_empty() { return Ok(doc); } @@ -606,10 +686,11 @@ fn unset_property(doc: &mut KdlDocument, parts: &[&str], parent: &str) -> Result return Ok(false); }; if parts.len() == used + 1 - && let Some(index) = node - .entries() - .iter() - .position(|entry| entry.name().is_some_and(|name| field(name.value(), key) == parts[used])) + && let Some(index) = node.entries().iter().position(|entry| { + entry + .name() + .is_some_and(|name| field(name.value(), key) == parts[used]) + }) { let entry = node.entries_mut().remove(index); if let Some(format) = entry.format() { @@ -660,7 +741,8 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) -> { collect_comments(node, &mut comments); } - doc.nodes_mut().retain(|node| field(node.name().value(), parent) != key); + doc.nodes_mut() + .retain(|node| field(node.name().value(), parent) != key); if let Some(first) = nodes.first_mut() { first.set_format(kdl::KdlNodeFormat { leading: comments, @@ -732,7 +814,9 @@ fn set_in(doc: &mut KdlDocument, parts: &[&str], parent: &str, value: Value) -> } } - if key == "autostart" && rest[0] == "command" && node.children().is_none_or(|d| d.get("command").is_none()) + if key == "autostart" + && rest[0] == "command" + && node.children().is_none_or(|d| d.get("command").is_none()) { node.entries_mut().retain(|e| e.name().is_some()); for value in value @@ -847,9 +931,15 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result { node.entries_mut().push(KdlEntry::new(kdl_value(value)?)); } - } else if is_records(key, parent) && map.len() <= 4 && !value.is_array() && !value.is_object() { - node.entries_mut() - .push(KdlEntry::new_prop(name.replace('_', "-"), kdl_value(value)?)); + } else if is_records(key, parent) + && map.len() <= 4 + && !value.is_array() + && !value.is_object() + { + node.entries_mut().push(KdlEntry::new_prop( + name.replace('_', "-"), + kdl_value(value)?, + )); } else { append(&mut child, name, value, key)?; } @@ -871,7 +961,10 @@ fn value_node(key: &str, value: &Value, parent: &str) -> Result fn append(doc: &mut KdlDocument, key: &str, value: &Value, parent: &str) -> Result<(), Error> { if is_records(key, parent) { - for record in value.as_array().ok_or_else(|| Error("Expected records".into()))? { + for record in value + .as_array() + .ok_or_else(|| Error("Expected records".into()))? + { doc.nodes_mut().push(value_node(key, record, parent)?); } } else { @@ -894,13 +987,22 @@ mod tests { "binding-mode resize { binding h resize left; binding l resize right; }\nbinding-mode launch { binding t spawn terminal; }\n", ).unwrap(); assert_eq!(document.get("binding_modes.0.name").unwrap(), "resize"); - assert_eq!(document.get("binding_modes.1.bindings.0.argument").unwrap(), "terminal"); + assert_eq!( + document.get("binding_modes.1.bindings.0.argument").unwrap(), + "terminal" + ); document .set("binding_modes.0.bindings.0.argument", "up".into()) .unwrap(); let reparsed = super::Document::parse(&document.to_string()).unwrap(); - assert_eq!(reparsed.get("binding_modes.0.bindings.0.argument").unwrap(), "up"); - assert_eq!(reparsed.get("binding_modes.0.bindings.1.argument").unwrap(), "right"); + assert_eq!( + reparsed.get("binding_modes.0.bindings.0.argument").unwrap(), + "up" + ); + assert_eq!( + reparsed.get("binding_modes.0.bindings.1.argument").unwrap(), + "right" + ); } #[test] fn adopting_appearance_modes_preserves_legacy_colors_and_comments() { @@ -908,7 +1010,9 @@ mod tests { "// my desktop\ntheme {\n colors {\n accent \"#123456\" // my accent\n }\n}\n", ) .unwrap(); - document.set("theme.mode", serde_json::json!("light")).unwrap(); + document + .set("theme.mode", serde_json::json!("light")) + .unwrap(); assert_eq!( document.get("theme.dark.colors.accent"), Some(&serde_json::json!("#123456")) @@ -931,7 +1035,8 @@ mod tests { #[test] fn inline_comments_and_failed_edits_leave_document_intact() { - let mut doc = Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap(); + let mut doc = + Document::parse("// header\nanimations {\n speed 0.75 // keep inline\n}\n").unwrap(); doc.set("animations.speed", 0.8.into()).unwrap(); assert!(doc.to_string().contains("// keep inline")); assert_eq!( @@ -974,11 +1079,15 @@ autostart "program" "two words" enabled=#false assert_eq!(doc.get("bindings.0.action").unwrap(), "toggle-overview"); doc.set("bindings.0.action", "none".into()).unwrap(); doc.set("input.repeat_rate", 25.into()).unwrap(); - doc.set("output_profiles.0.outputs.0.scale", 2.0.into()).unwrap(); + doc.set("output_profiles.0.outputs.0.scale", 2.0.into()) + .unwrap(); let reparsed = Document::parse(&doc.to_string()).unwrap(); assert_eq!(reparsed.get("bindings.0.action").unwrap(), "none"); assert_eq!(reparsed.get("input.repeat_rate").unwrap(), 25); - assert_eq!(reparsed.get("output_profiles.0.outputs.0.scale").unwrap(), 2.0); + assert_eq!( + reparsed.get("output_profiles.0.outputs.0.scale").unwrap(), + 2.0 + ); } #[test] @@ -987,13 +1096,21 @@ autostart "program" "two words" enabled=#false "output-profile desk { output match=\"DP-1\" { position 0 0; }; }\ncommands { lock-screen \"locker\"; }\n", ) .unwrap(); - doc.set("output_profiles.0.outputs.0.position", serde_json::json!([100, 200])) - .unwrap(); - doc.set("commands.lock-screen", serde_json::json!(["ferese-lock", "--lock"])) - .unwrap(); + doc.set( + "output_profiles.0.outputs.0.position", + serde_json::json!([100, 200]), + ) + .unwrap(); + doc.set( + "commands.lock-screen", + serde_json::json!(["ferese-lock", "--lock"]), + ) + .unwrap(); let reparsed = Document::parse(&doc.to_string()).unwrap(); assert_eq!( - reparsed.get("output_profiles.0.outputs.0.position").unwrap(), + reparsed + .get("output_profiles.0.outputs.0.position") + .unwrap(), &serde_json::json!([100, 200]) ); assert_eq!( @@ -1023,7 +1140,9 @@ autostart "program" "two words" enabled=#false serde_json::json!({}) ); } - assert!(Document::parse("panel main { metadata { desktop-widgets \"custom\"; }; }").is_ok()); + assert!( + Document::parse("panel main { metadata { desktop-widgets \"custom\"; }; }").is_ok() + ); } #[test] diff --git a/crates/ferese-config/src/theme.rs b/crates/ferese-config/src/theme.rs index cd513c9..4d7f3b2 100644 --- a/crates/ferese-config/src/theme.rs +++ b/crates/ferese-config/src/theme.rs @@ -78,17 +78,18 @@ impl Schedule { AutoSource::System => Ok(( context.system.unwrap_or(Appearance::Dark), None, - context - .system - .is_none() - .then(|| "System appearance is unavailable. Using Dark until it becomes available.".into()), + context.system.is_none().then(|| { + "System appearance is unavailable. Using Dark until it becomes available." + .into() + }), )), AutoSource::Schedule => self .boundaries(now, zone) .map(|(appearance, next)| (appearance, Some(next), None)), - AutoSource::SunriseSunset => { - Err("Sunrise/sunset is not available yet; choose Follow system or Custom schedule.".into()) - } + AutoSource::SunriseSunset => Err( + "Sunrise/sunset is not available yet; choose Follow system or Custom schedule." + .into(), + ), } } @@ -105,7 +106,11 @@ impl Schedule { } } - pub fn boundaries(&self, now: Timestamp, zone: &TimeZone) -> Result<(Appearance, Timestamp), String> { + pub fn boundaries( + &self, + now: Timestamp, + zone: &TimeZone, + ) -> Result<(Appearance, Timestamp), String> { let parse = |s: &str| -> Result { if s.len() != 5 || s.as_bytes()[2] != b':' { return Err("theme schedule times must use HH:MM".into()); @@ -171,7 +176,9 @@ pub struct Policy { pub accent: Option, } -fn deserialize_light_selection<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result { +fn deserialize_light_selection<'de, D: serde::Deserializer<'de>>( + deserializer: D, +) -> Result { let mut value = Value::deserialize(deserializer)?; if let Some(object) = value.as_object_mut() { object.entry("preset").or_insert(json!("ferese-blue-light")); @@ -264,11 +271,12 @@ pub fn resolve_with_context( } else { TimeZone::UTC }; - let (scheduled, next, auto_note) = if policy.mode == Mode::Auto || policy.schedule.source == AutoSource::Schedule { - policy.schedule.resolve(now, &zone, context)? - } else { - (Appearance::Dark, None, None) - }; + let (scheduled, next, auto_note) = + if policy.mode == Mode::Auto || policy.schedule.source == AutoSource::Schedule { + policy.schedule.resolve(now, &zone, context)? + } else { + (Appearance::Dark, None, None) + }; let appearance = match policy.mode { Mode::Light => Appearance::Light, Mode::Dark => Appearance::Dark, @@ -300,14 +308,22 @@ pub fn resolve_with_context( imported.insert(id.clone(), variants); } let split = policy.split.unwrap_or_else(|| { - crate::families::family_id(&policy.light.preset) != crate::families::family_id(&policy.dark.preset) + crate::families::family_id(&policy.light.preset) + != crate::families::family_id(&policy.dark.preset) }); let single = policy .family .as_deref() .unwrap_or_else(|| crate::families::family_id(&policy.dark.preset)); - let mut fallback_note = if policy.mode == Mode::Auto { auto_note } else { None }; - for (kind, selection) in [(Appearance::Light, &policy.light), (Appearance::Dark, &policy.dark)] { + let mut fallback_note = if policy.mode == Mode::Auto { + auto_note + } else { + None + }; + for (kind, selection) in [ + (Appearance::Light, &policy.light), + (Appearance::Dark, &policy.dark), + ] { let family_id = if split { selection .family @@ -316,15 +332,16 @@ pub fn resolve_with_context( } else { single }; - let variant = imported - .get(family_id) - .and_then(|pair: &(Option, Option)| { - if kind == Appearance::Light { - pair.0.clone() - } else { - pair.1.clone() - } - }); + let variant = + imported + .get(family_id) + .and_then(|pair: &(Option, Option)| { + if kind == Appearance::Light { + pair.0.clone() + } else { + pair.1.clone() + } + }); let custom_variant = variant.is_some(); let mut tokens = if let Some(tokens) = variant { tokens @@ -332,7 +349,10 @@ pub fn resolve_with_context( preset(id, kind)? } else { if !imported.contains_key(family_id) - && !matches!(family_id, "dracula" | "monochrome" | "monokai" | "ayu-light") + && !matches!( + family_id, + "dracula" | "monochrome" | "monokai" | "ayu-light" + ) { return Err(format!("Unknown theme family: {family_id}")); } @@ -343,7 +363,11 @@ pub fn resolve_with_context( .map_or(family_id, |family| family.name.as_str()); fallback_note = Some(format!( "{name} has no {} variant. Using Ferese Blue for this appearance.", - if kind == Appearance::Light { "light" } else { "dark" } + if kind == Appearance::Light { + "light" + } else { + "dark" + } )); } preset( @@ -358,8 +382,8 @@ pub fn resolve_with_context( if !families.iter().any(|family| family.id == family_id) { let light = crate::families::variant(family_id, Appearance::Light) .and_then(|id| preset(id, Appearance::Light).ok()); - let dark = - crate::families::variant(family_id, Appearance::Dark).and_then(|id| preset(id, Appearance::Dark).ok()); + let dark = crate::families::variant(family_id, Appearance::Dark) + .and_then(|id| preset(id, Appearance::Dark).ok()); families.push(crate::families::Family { id: family_id.into(), name: family_id.into(), @@ -372,12 +396,16 @@ pub fn resolve_with_context( let authored_accent = tokens.colors.accent.clone(); let preset_end = (!custom_variant) .then(|| { - let id = crate::families::variant(family_id, kind).unwrap_or(if kind == Appearance::Light { - "ferese-blue-light" - } else { - "ferese-blue" - }); - crate::presets::PRESETS.iter().find(|preset| preset.id == id) + let id = crate::families::variant(family_id, kind).unwrap_or( + if kind == Appearance::Light { + "ferese-blue-light" + } else { + "ferese-blue" + }, + ); + crate::presets::PRESETS + .iter() + .find(|preset| preset.id == id) }) .flatten() .map(|preset| preset.gradient_end) @@ -402,7 +430,11 @@ pub fn resolve_with_context( let layer = Document::parse(&source).map_err(|e| format!("{}: {e}", path.display()))?; let layer = layer.get("theme").unwrap_or(layer.value()); for role in ["surface_raised", "application_background"] { - if layer.get("colors").and_then(|colors| colors.get(role)).is_some() { + if layer + .get("colors") + .and_then(|colors| colors.get(role)) + .is_some() + { explicit_surfaces.insert(role); } } @@ -419,7 +451,11 @@ pub fn resolve_with_context( tokens = serde_json::from_value(value).map_err(|e| e.to_string())?; for layer in [&root, &Value::Object(overrides.clone())] { for role in ["surface_raised", "application_background"] { - if layer.get("colors").and_then(|colors| colors.get(role)).is_some() { + if layer + .get("colors") + .and_then(|colors| colors.get(role)) + .is_some() + { explicit_surfaces.insert(role); } } @@ -430,25 +466,35 @@ pub fn resolve_with_context( } else { [1.; 4] }; - if tokens.colors.surface_base != authored_base && !explicit_surfaces.contains("surface_raised") { + if tokens.colors.surface_base != authored_base + && !explicit_surfaces.contains("surface_raised") + { tokens.colors.surface_raised = hex(blend(base, shade, 0.055)); } - if tokens.colors.surface_base != authored_base && !explicit_surfaces.contains("application_background") { + if tokens.colors.surface_base != authored_base + && !explicit_surfaces.contains("application_background") + { tokens.colors.application_background = hex(blend(base, shade, 0.025)); } if let Some(accent) = &policy.accent { tokens.colors.accent = accent.clone(); } - for path in [&mut tokens.background.path, &mut tokens.background.lock_path] - .into_iter() - .flatten() + for path in [ + &mut tokens.background.path, + &mut tokens.background.lock_path, + ] + .into_iter() + .flatten() { *path = theme_path(directory, path); } validate(&tokens)?; let requested_accent = tokens.colors.accent.clone(); transform(&mut tokens, &policy.accessibility, &mut warnings); - resolve_gradients(&mut tokens, preset_end.filter(|_| requested_accent == authored_accent)); + resolve_gradients( + &mut tokens, + preset_end.filter(|_| requested_accent == authored_accent), + ); let theme = ResolvedTheme { appearance: kind, tokens, @@ -471,7 +517,11 @@ pub fn resolve_with_context( theme: selected.expect("selected appearance"), warnings, files: all_files, - next_transition: if policy.mode == Mode::Auto { next } else { None }, + next_transition: if policy.mode == Mode::Auto { + next + } else { + None + }, families, fallback_note, }) @@ -513,7 +563,12 @@ pub fn validate_overrides(document: &Document) -> Result<(), String> { let mut tokens = token_value(&default_tokens())?; merge(&mut tokens, root, "", true)?; for selection in [&policy.light, &policy.dark] { - merge(&mut tokens, &Value::Object(selection.overrides.clone()), "", false)?; + merge( + &mut tokens, + &Value::Object(selection.overrides.clone()), + "", + false, + )?; } Ok(()) } @@ -562,7 +617,10 @@ fn merge(target: &mut Value, layer: &Value, prefix: &str, inline: bool) -> Resul } pub fn preset(name: &str, appearance: Appearance) -> Result { - if let Some(preset) = crate::presets::PRESETS.iter().find(|preset| preset.id == name) { + if let Some(preset) = crate::presets::PRESETS + .iter() + .find(|preset| preset.id == name) + { if preset.appearance != appearance { return Err(format!("Preset {name} does not support {appearance:?}")); } @@ -640,7 +698,10 @@ pub fn authored_warnings(tokens: &Tokens) -> Vec { let surfaces = [ ("surface-base", &tokens.colors.surface_base), ("surface-raised", &tokens.colors.surface_raised), - ("application-background", &tokens.colors.application_background), + ( + "application-background", + &tokens.colors.application_background, + ), ]; let check = |foreground: &str, background: &str, minimum: f64| { let foreground = rgba(foreground).unwrap(); @@ -685,7 +746,9 @@ fn validate(tokens: &Tokens) -> Result<(), String> { } Value::Number(n) => { let n = n.as_f64().ok_or("invalid theme number")?; - if !n.is_finite() || (path != ".shadow.soft.offset_y" && !path.ends_with(".angle") && n < 0.) { + if !n.is_finite() + || (path != ".shadow.soft.offset_y" && !path.ends_with(".angle") && n < 0.) + { return Err(format!("Invalid theme value {path}")); } if (path.ends_with("opacity") || path.ends_with("tint_strength")) && n > 1. { @@ -735,7 +798,9 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec= 3. && (!preserve_foreground || contrast(preferred, color) >= 4.5); + let acceptable = |color| { + score(color) >= 3. && (!preserve_foreground || contrast(preferred, color) >= 4.5) + }; if acceptable(requested) { return Some(requested); } @@ -759,7 +824,11 @@ fn transform(t: &mut Tokens, accessibility: &Accessibility, warnings: &mut Vec) { rgba(&tokens.surface.bar.background).unwrap(), ]; for (paint, color, endpoint, focus) in [ - (&mut tokens.focus_ring, &tokens.colors.accent, preset_end, true), + ( + &mut tokens.focus_ring, + &tokens.colors.accent, + preset_end, + true, + ), (&mut tokens.border, &tokens.colors.border, None, false), ] { if paint.style == PaintStyle::Solid { @@ -865,7 +943,8 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) { .map(|step| initial + (1. - initial) * f64::from(step) / 100.) .find(|opacity| { let bounds = material_bounds(surface, *opacity); - readable(&[bounds[0], bounds[1], raised, app]) && readable(&material_bounds(bar, *opacity)) + readable(&[bounds[0], bounds[1], raised, app]) + && readable(&material_bounds(bar, *opacity)) }) .unwrap_or(1.0); tokens.material.opacity = tokens.material.opacity.max(opacity); @@ -883,12 +962,20 @@ fn resolve_material_contrast(tokens: &mut Tokens, minimum: f64) { } } -pub type ImportedFamily = (crate::families::Family, (Option, Option), Vec); +pub type ImportedFamily = ( + crate::families::Family, + (Option, Option), + Vec, +); pub fn import_family(id: &str, source: &str) -> Result { let document = Document::parse(source).map_err(|e| e.to_string())?; let root = document.get("theme").unwrap_or(document.value()); - let name = root.get("name").and_then(Value::as_str).unwrap_or(id).to_owned(); + let name = root + .get("name") + .and_then(Value::as_str) + .unwrap_or(id) + .to_owned(); let mut variants = [None, None]; let mut warnings = vec![]; for (index, (key, appearance)) in [("light", Appearance::Light), ("dark", Appearance::Dark)] @@ -907,7 +994,11 @@ pub fn import_family(id: &str, source: &str) -> Result { let mut value = token_value(&base)?; merge(&mut value, layer, "", false)?; let mut tokens: Tokens = serde_json::from_value(value).map_err(|e| e.to_string())?; - if layer.get("colors").and_then(|colors| colors.get("border")).is_none() { + if layer + .get("colors") + .and_then(|colors| colors.get("border")) + .is_none() + { // A new family without an authored border follows its own foreground, // rather than inheriting the unrelated Ferese Blue border. let mut border = rgba(&tokens.colors.text_primary)?; @@ -920,9 +1011,16 @@ pub fn import_family(id: &str, source: &str) -> Result { } } if variants.iter().all(Option::is_none) { - return Err("A theme file must declare a light and/or dark section; partial palettes are allowed.".into()); + return Err( + "A theme file must declare a light and/or dark section; partial palettes are allowed." + .into(), + ); } - for key in root.as_object().ok_or("Theme file must be a section")?.keys() { + for key in root + .as_object() + .ok_or("Theme file must be a section")? + .keys() + { if !matches!(key.as_str(), "name" | "light" | "dark") { return Err(format!("Unknown theme token: {key}")); } @@ -946,9 +1044,12 @@ mod tests { "theme {{ geometry {{ shell-radius {requested}; window-radius 7.25; }} }}" )) .unwrap(); - let result = super::resolve(&document, std::path::Path::new("/tmp"), jiff::Timestamp::now(), |_| { - Err("unexpected theme file".into()) - }) + let result = super::resolve( + &document, + std::path::Path::new("/tmp"), + jiff::Timestamp::now(), + |_| Err("unexpected theme file".into()), + ) .unwrap(); assert_eq!(result.theme.tokens.geometry.shell_radius, expected); assert_eq!(result.theme.tokens.geometry.window_radius, 7.25); @@ -1047,7 +1148,11 @@ mod tests { ]; assert_eq!( gradient.to, - hex(readable_across(rgba(preset.gradient_end).unwrap(), &backgrounds, 3.)) + hex(readable_across( + rgba(preset.gradient_end).unwrap(), + &backgrounds, + 3. + )) ); assert!( backgrounds @@ -1078,8 +1183,10 @@ mod tests { assert!(from[0] > from[1] && from[1] > from[2]); assert!(to[0] > to[1] && to[1] > to[2]); - let document = - Document::parse("theme { family custom; custom-themes { custom { file \"custom.kdl\"; }; }; }").unwrap(); + let document = Document::parse( + "theme { family custom; custom-themes { custom { file \"custom.kdl\"; }; }; }", + ) + .unwrap(); let theme = resolve( &document, Path::new("/config"), @@ -1118,12 +1225,18 @@ mod tests { ); assert!(theme.tokens.border.gradient.is_none()); - let source = source.replace("family catppuccin", "family catppuccin\nfocus-ring { style solid; }"); + let source = source.replace( + "family catppuccin", + "family catppuccin\nfocus-ring { style solid; }", + ); let theme = candidate(&source).unwrap().theme; assert!(theme.tokens.focus_ring.gradient.is_none()); assert_eq!(theme.tokens.focus_ring.style, PaintStyle::Solid); assert!(candidate("theme { focus-ring { style rainbow; }; }").is_err()); - assert!(candidate("theme { focus-ring { gradient { from \"bad\"; to \"#FFFFFF\"; }; }; }").is_err()); + assert!( + candidate("theme { focus-ring { gradient { from \"bad\"; to \"#FFFFFF\"; }; }; }") + .is_err() + ); } #[test] @@ -1153,7 +1266,13 @@ mod tests { assert_eq!(middle.tokens.focus_ring.style, PaintStyle::Auto); assert!(middle.tokens.focus_ring.gradient.is_some()); assert_eq!(from.transition(&to, 1.), to); - assert!(to.transition(&from, 0.5).tokens.focus_ring.gradient.is_some()); + assert!( + to.transition(&from, 0.5) + .tokens + .focus_ring + .gradient + .is_some() + ); } #[test] @@ -1168,20 +1287,39 @@ mod tests { authored_warnings(&tokens) ); let source = match entry.appearance { - Appearance::Light => format!("theme {{ mode \"light\"; light {{ preset \"{}\"; }}; }}", entry.id), - Appearance::Dark => format!("theme {{ mode \"dark\"; dark {{ preset \"{}\"; }}; }}", entry.id), + Appearance::Light => format!( + "theme {{ mode \"light\"; light {{ preset \"{}\"; }}; }}", + entry.id + ), + Appearance::Dark => format!( + "theme {{ mode \"dark\"; dark {{ preset \"{}\"; }}; }}", + entry.id + ), }; let resolved = candidate(&source).unwrap().theme; assert_eq!(resolved.tokens.colors.border, tokens.colors.border); - assert_eq!(resolved.tokens.colors.surface_raised, tokens.colors.surface_raised); + assert_eq!( + resolved.tokens.colors.surface_raised, + tokens.colors.surface_raised + ); assert_eq!( resolved.tokens.colors.application_background, tokens.colors.application_background ); - assert!(authored_warnings(&resolved.tokens).is_empty(), "{}", entry.id); + assert!( + authored_warnings(&resolved.tokens).is_empty(), + "{}", + entry.id + ); } - let dark: Vec<_> = PRESETS.iter().filter(|p| p.appearance == Appearance::Dark).collect(); - let light: Vec<_> = PRESETS.iter().filter(|p| p.appearance == Appearance::Light).collect(); + let dark: Vec<_> = PRESETS + .iter() + .filter(|p| p.appearance == Appearance::Dark) + .collect(); + let light: Vec<_> = PRESETS + .iter() + .filter(|p| p.appearance == Appearance::Light) + .collect(); assert_eq!(dark.len(), 6); assert_eq!(light.len(), 6); for (dark, light) in dark.iter().zip(light) { @@ -1209,7 +1347,12 @@ mod tests { entry.id ); let result = candidate(&source).unwrap(); - assert!(result.warnings.is_empty(), "{}: {:?}", entry.id, result.warnings); + assert!( + result.warnings.is_empty(), + "{}: {:?}", + entry.id, + result.warnings + ); let theme = result.theme; assert!(authored_warnings(&theme.tokens).is_empty(), "{}", entry.id); if increase { @@ -1218,9 +1361,15 @@ mod tests { &theme.tokens.colors.surface_raised, &theme.tokens.colors.application_background, ] { - for foreground in [&theme.tokens.colors.text_primary, &theme.tokens.colors.text_muted] { + for foreground in [ + &theme.tokens.colors.text_primary, + &theme.tokens.colors.text_muted, + ] { assert!( - contrast(rgba(foreground).unwrap(), rgba(background).unwrap()) >= 7., + contrast( + rgba(foreground).unwrap(), + rgba(background).unwrap() + ) >= 7., "{}: {foreground} on {background}", entry.id ); @@ -1262,13 +1411,17 @@ mod tests { let resolved = candidate(r#"theme { mode "light"; family "catppuccin"; split #true; light { family "gruvbox"; }; dark { family "everforest"; }; }"#).unwrap(); assert_eq!( resolved.theme.tokens.colors.surface_base, - preset("gruvbox-light", Appearance::Light).unwrap().colors.surface_base + preset("gruvbox-light", Appearance::Light) + .unwrap() + .colors + .surface_base ); } #[test] fn single_variant_families_use_an_explicit_default_fallback() { - let resolved = candidate(r#"theme { mode "light"; family "dracula"; split #false; }"#).unwrap(); + let resolved = + candidate(r#"theme { mode "light"; family "dracula"; split #false; }"#).unwrap(); assert_eq!( resolved.theme.tokens.colors.surface_base, preset("ferese-blue-light", Appearance::Light) @@ -1283,7 +1436,8 @@ mod tests { .iter() .any(|family| family.id == "dracula" && family.light.is_none()) ); - let resolved = candidate(r#"theme { mode "dark"; family "dracula"; split #false; }"#).unwrap(); + let resolved = + candidate(r#"theme { mode "dark"; family "dracula"; split #false; }"#).unwrap(); assert!(resolved.fallback_note.is_none()); assert_eq!(resolved.theme.tokens.colors.surface_base, "#282A36"); } @@ -1376,7 +1530,8 @@ mod tests { #[test] fn system_auto_is_context_driven_and_has_no_polling_deadline() { - let document = Document::parse(r#"theme { mode "auto"; schedule { source "system"; }; }"#).unwrap(); + let document = + Document::parse(r#"theme { mode "auto"; schedule { source "system"; }; }"#).unwrap(); for appearance in [Appearance::Light, Appearance::Dark] { let resolved = resolve_with_context( &document, @@ -1392,7 +1547,13 @@ mod tests { assert!(resolved.next_transition.is_none()); assert!(resolved.fallback_note.is_none()); } - let unavailable = resolve(&document, Path::new("/tmp"), Timestamp::now(), |_| unreachable!()).unwrap(); + let unavailable = resolve( + &document, + Path::new("/tmp"), + Timestamp::now(), + |_| unreachable!(), + ) + .unwrap(); assert!(unavailable.fallback_note.unwrap().contains("unavailable")); } @@ -1446,7 +1607,11 @@ mod tests { light_at: "01:30".into(), ..schedule }; - for now in ["2026-11-01T05:30:00Z", "2026-11-01T06:00:00Z", "2026-11-01T06:30:00Z"] { + for now in [ + "2026-11-01T05:30:00Z", + "2026-11-01T06:00:00Z", + "2026-11-01T06:30:00Z", + ] { let (appearance, next) = schedule.boundaries(now.parse().unwrap(), &zone).unwrap(); assert_eq!(appearance, Appearance::Light); assert_eq!(next, "2026-11-02T00:00:00Z".parse::().unwrap()); @@ -1459,15 +1624,20 @@ mod tests { "theme { file \"themes/shared.kdl\"; light { file \"themes/light.kdl\"; }; mode \"light\"; }", ) .unwrap(); - let result = resolve(&doc, Path::new("/config/ferese"), Timestamp::now(), |path| { - match path.to_str().unwrap() { + let result = resolve( + &doc, + Path::new("/config/ferese"), + Timestamp::now(), + |path| match path.to_str().unwrap() { "/config/ferese/themes/shared.kdl" => { Ok("colors { accent \"#12AABB\"; text-primary \"#182234\"; }".into()) } - "/config/ferese/themes/light.kdl" => Ok("theme { colors { accent \"#315090\"; }; }".into()), + "/config/ferese/themes/light.kdl" => { + Ok("theme { colors { accent \"#315090\"; }; }".into()) + } _ => panic!("wrong relative path"), - } - }) + }, + ) .unwrap(); assert_eq!(result.theme.requested_accent, "#315090"); assert_eq!(result.theme.tokens.colors.text_primary, "#182234"); @@ -1516,10 +1686,13 @@ mod tests { .contains("future_token") ); assert!( - import_family("bad", "theme { light { geometry { top-bar-radius 8; }; }; }") - .err() - .unwrap() - .contains("geometry.top_bar_radius") + import_family( + "bad", + "theme { light { geometry { top-bar-radius 8; }; }; }" + ) + .err() + .unwrap() + .contains("geometry.top_bar_radius") ); assert!( import_family("bad", "theme { name Test; future-token 1; light {}; }") @@ -1549,7 +1722,9 @@ mod tests { ) >= 4.5 ); if increase { - assert!(contrast(rgba(&tokens.colors.text_primary).unwrap(), surface) >= 7.); + assert!( + contrast(rgba(&tokens.colors.text_primary).unwrap(), surface) >= 7. + ); } if reduce { assert_eq!(tokens.material.style, "solid"); @@ -1565,8 +1740,12 @@ mod tests { #[test] fn transition_text_remains_readable_at_fixed_steps_on_composited_surfaces() { use crate::presets::PRESETS; - let darks = PRESETS.iter().filter(|entry| entry.appearance == Appearance::Dark); - let lights = PRESETS.iter().filter(|entry| entry.appearance == Appearance::Light); + let darks = PRESETS + .iter() + .filter(|entry| entry.appearance == Appearance::Dark); + let lights = PRESETS + .iter() + .filter(|entry| entry.appearance == Appearance::Light); for (dark_entry, light_entry) in darks.zip(lights) { let dark = candidate(&format!( r#"theme {{ mode "dark"; dark {{ preset "{}"; }}; material {{ style "translucent"; }}; }}"#, @@ -1585,9 +1764,13 @@ mod tests { for step in 1..20 { let frame = from.transition(to, f64::from(step) / 20.); let mut surface = rgba(&frame.tokens.colors.surface_base).unwrap(); - surface[3] = frame.tokens.material.opacity * frame.tokens.material.tint_strength; + surface[3] = + frame.tokens.material.opacity * frame.tokens.material.tint_strength; let surface = composite(surface, backdrop); - for text in [&frame.tokens.colors.text_primary, &frame.tokens.colors.text_muted] { + for text in [ + &frame.tokens.colors.text_primary, + &frame.tokens.colors.text_muted, + ] { assert!( contrast(rgba(text).unwrap(), surface) >= 4.5, "step {step}: {text}, contrast {}", @@ -1598,7 +1781,10 @@ mod tests { &frame.tokens.colors.surface_raised, &frame.tokens.colors.application_background, ] { - for text in [&frame.tokens.colors.text_primary, &frame.tokens.colors.text_muted] { + for text in [ + &frame.tokens.colors.text_primary, + &frame.tokens.colors.text_muted, + ] { assert!( contrast(rgba(text).unwrap(), rgba(background).unwrap()) >= 4.5, "native app step {step}: {text} on {background}" @@ -1606,13 +1792,17 @@ mod tests { } } let mut bar = rgba(&frame.tokens.surface.bar.background).unwrap(); - bar[3] = frame.tokens.material.opacity * frame.tokens.material.tint_strength; + bar[3] = + frame.tokens.material.opacity * frame.tokens.material.tint_strength; let bar = composite(bar, backdrop); for text in [ &frame.tokens.surface.bar.text_primary, &frame.tokens.surface.bar.text_muted, ] { - assert!(contrast(rgba(text).unwrap(), bar) >= 4.5, "bar step {step}: {text}"); + assert!( + contrast(rgba(text).unwrap(), bar) >= 4.5, + "bar step {step}: {text}" + ); } assert!( contrast( @@ -1670,8 +1860,16 @@ mod tests { assert_eq!(from.transition(&from, p), from); let frame = from.transition(&to, p); assert_eq!(frame.tokens.material.opacity, from.tokens.material.opacity); - let low = from.tokens.material.tint_strength.min(to.tokens.material.tint_strength); - let high = from.tokens.material.tint_strength.max(to.tokens.material.tint_strength); + let low = from + .tokens + .material + .tint_strength + .min(to.tokens.material.tint_strength); + let high = from + .tokens + .material + .tint_strength + .max(to.tokens.material.tint_strength); assert!((low..=high).contains(&frame.tokens.material.tint_strength)); } } @@ -1692,9 +1890,10 @@ mod tests { #[test] fn reduced_motion_cuts_and_policy_does_not_follow_brightness() { - let mut light = candidate("theme { mode \"light\"; colors { surface-base \"#111111\"; }; }") - .unwrap() - .theme; + let mut light = + candidate("theme { mode \"light\"; colors { surface-base \"#111111\"; }; }") + .unwrap() + .theme; assert_eq!(light.appearance, Appearance::Light); light.reduced_motion = true; assert_eq!(default_theme().transition(&light, 0.), light); diff --git a/crates/ferese-config/tests/panel.rs b/crates/ferese-config/tests/panel.rs index 1d684d6..253b10a 100644 --- a/crates/ferese-config/tests/panel.rs +++ b/crates/ferese-config/tests/panel.rs @@ -10,7 +10,8 @@ fn removed_panel_surface_fields_are_rejected() { "style continuous;", "style islands;", ] { - let document = ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap(); + let document = + ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap(); assert!( serde_json::from_value::>(document.get("panels").unwrap().clone()).is_err(), "{source}" @@ -26,9 +27,11 @@ fn panel_surfaces_round_trip_without_group_overrides() { let panel = &panels[0]; assert_eq!(panel.group_surface(), GroupSurface::Island); assert_eq!(panel.preset(), Some(PanelStyle::Floating)); - doc.set("panels", serde_json::to_value(&panels).unwrap()).unwrap(); + doc.set("panels", serde_json::to_value(&panels).unwrap()) + .unwrap(); let restored = ferese_config::Document::parse(&doc.to_string()).unwrap(); - let restored: Vec = serde_json::from_value(restored.get("panels").unwrap().clone()).unwrap(); + let restored: Vec = + serde_json::from_value(restored.get("panels").unwrap().clone()).unwrap(); assert_eq!(restored, panels); assert!(doc.to_string().contains("// keep")); let mut preset = panel.clone(); @@ -77,7 +80,10 @@ animations { speed 0.8; } validate(&panels).unwrap(); assert_eq!(panels[0].end.groups[0].padding, [2, 3]); assert!(panels[0].background_opacity.is_none()); - assert_eq!(panels[0].end.groups[0].items[0].overflow, OverflowPolicy::Always); + assert_eq!( + panels[0].end.groups[0].items[0].overflow, + OverflowPolicy::Always + ); doc.set("panels.0.end.groups.0.items.0.overflow", "never".into()) .unwrap(); let path = "panels.0.end.groups.0.items"; @@ -102,13 +108,18 @@ animations { speed 0.8; } panels[0].item(&ItemId("network".into())).unwrap().overflow, OverflowPolicy::Never ); - assert_eq!(parsed.get("animations.speed").unwrap(), &serde_json::json!(0.8)); + assert_eq!( + parsed.get("animations.speed").unwrap(), + &serde_json::json!(0.8) + ); let value = serde_json::to_value(&panels).unwrap(); doc.set("panels", value.clone()).unwrap(); let round_trip = ferese_config::Document::parse(&doc.to_string()).unwrap(); - let restored: Vec = serde_json::from_value(round_trip.get("panels").unwrap().clone()).unwrap(); + let restored: Vec = + serde_json::from_value(round_trip.get("panels").unwrap().clone()).unwrap(); assert_eq!(restored, panels); - doc.set(path, Vec::::new().into()).unwrap(); + doc.set(path, Vec::::new().into()) + .unwrap(); assert!(doc.to_string().contains("// network comment")); let restored: Vec = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); assert!(restored[0].end.groups[0].items.is_empty()); @@ -117,8 +128,10 @@ animations { speed 0.8; } #[test] fn panel_border_defaults_off_and_round_trips_an_explicit_enabled_value() { - let mut doc = - ferese_config::Document::parse("panel main { end { group status { item clock kind=clock; }; }; }\n").unwrap(); + let mut doc = ferese_config::Document::parse( + "panel main { end { group status { item clock kind=clock; }; }; }\n", + ) + .unwrap(); let panels: Vec = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); assert!(!panels[0].border); doc.set("panels.0.border", true.into()).unwrap(); @@ -131,7 +144,8 @@ fn panel_border_defaults_off_and_round_trips_an_explicit_enabled_value() { #[test] fn panel_background_opacity_inherits_until_overridden_and_validates_bounds() { let doc = ferese_config::Document::parse("panel main { end { group status { }; }; }").unwrap(); - let mut panels: Vec = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); + let mut panels: Vec = + serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); assert_eq!(panels[0].background_opacity, None); assert!( serde_json::to_value(&panels[0]).unwrap()["end"]["groups"][0] @@ -155,14 +169,21 @@ fn panel_edge_defaults_to_top_and_round_trips_both_positions() { ("edge top;", Edge::Top), ("edge bottom;", Edge::Bottom), ] { - let document = ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap(); - let panels: Vec = serde_json::from_value(document.get("panels").unwrap().clone()).unwrap(); + let document = + ferese_config::Document::parse(&format!("panel main {{ {source} }}")).unwrap(); + let panels: Vec = + serde_json::from_value(document.get("panels").unwrap().clone()).unwrap(); validate(&panels).unwrap(); assert_eq!(panels[0].edge, expected); - let restored: Vec = serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap(); + let restored: Vec = + serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap(); assert_eq!(restored[0].edge, expected); let mut moved = panels[0].clone(); - moved.edge = if expected == Edge::Top { Edge::Bottom } else { Edge::Top }; + moved.edge = if expected == Edge::Top { + Edge::Bottom + } else { + Edge::Top + }; assert!(!moved.same_composition(&panels[0])); } } @@ -177,7 +198,9 @@ fn validation_rejects_duplicate_ids_and_unsupported_surface_configuration() { assert!(validate(std::slice::from_ref(&panel)).is_err()); assert!(validate(&[]).is_err()); assert!(validate(&[panel.clone(), panel]).is_err()); - assert!(ferese_config::from_str::("panel \"main\" { edge \"bottom\"; }").is_ok()); + assert!( + ferese_config::from_str::("panel \"main\" { edge \"bottom\"; }").is_ok() + ); let doc = ferese_config::Document::parse("panel \"main\" { edge \"left\"; }").unwrap(); assert!(serde_json::from_value::>(doc.get("panels").unwrap().clone()).is_err()); } @@ -189,13 +212,18 @@ fn workspace_styles_round_trip_and_only_apply_to_workspaces() { style.key() ); let document = ferese_config::Document::parse(&source).unwrap(); - let panels: Vec = serde_json::from_value(document.get("panels").unwrap().clone()).unwrap(); + let panels: Vec = + serde_json::from_value(document.get("panels").unwrap().clone()).unwrap(); validate(&panels).unwrap(); - assert_eq!(panels[0].start.groups[0].items[0].kind, ItemKind::Workspaces { style }); + assert_eq!( + panels[0].start.groups[0].items[0].kind, + ItemKind::Workspaces { style } + ); let mut wrong_kind = serde_json::to_value(&panels[0].start.groups[0].items[0]).unwrap(); wrong_kind["kind"] = "clock".into(); assert!(serde_json::from_value::(wrong_kind).is_err()); - let restored: Vec = serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap(); + let restored: Vec = + serde_json::from_value(serde_json::to_value(&panels).unwrap()).unwrap(); assert_eq!(restored, panels); } let mut item = serde_json::json!({"id": "spaces", "kind": "workspaces"}); @@ -214,8 +242,13 @@ fn item_options_are_strict_and_metadata_is_explicit() { for key in ["ovreflow", "visibile", "custom_note"] { let mut value = serde_json::json!({"id":"network", "kind":"network"}); value[key] = "always".into(); - let error = serde_json::from_value::(value).unwrap_err().to_string(); - assert!(error.contains(key) && error.contains("unknown field"), "{error}"); + let error = serde_json::from_value::(value) + .unwrap_err() + .to_string(); + assert!( + error.contains(key) && error.contains("unknown field"), + "{error}" + ); } let source = "panel main { start { group nav { item network kind=\"network\" { metadata { custom-note \"keep\"; overflow \"authored note\"; }; }; }; }; }"; let panel: Vec = serde_json::from_value( @@ -234,8 +267,10 @@ fn item_options_are_strict_and_metadata_is_explicit() { *item ); assert!( - serde_json::from_value::(serde_json::json!({"id":"network", "kind":"network", "metadata":"wrong"})) - .is_err() + serde_json::from_value::( + serde_json::json!({"id":"network", "kind":"network", "metadata":"wrong"}) + ) + .is_err() ); } @@ -247,16 +282,24 @@ fn activities_are_first_class_items_in_every_zone_and_keep_overflow_settings() { "panel main {{ style {style}; {zone} {{ group tasks {{ item builds kind=activities priority=20 overflow=always; }}; }}; }}" ); let mut doc = ferese_config::Document::parse(&source).unwrap(); - let panels: Vec = serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); + let panels: Vec = + serde_json::from_value(doc.get("panels").unwrap().clone()).unwrap(); ferese_panel::validate(&panels).unwrap(); let parsed = match zone { "start" => &panels[0].start, "center" => &panels[0].center, _ => &panels[0].end, }; - assert_eq!(parsed.groups[0].items[0].kind, ferese_panel::ItemKind::Activities); - assert_eq!(parsed.groups[0].items[0].overflow, ferese_panel::OverflowPolicy::Always); - doc.set("panels", serde_json::to_value(&panels).unwrap()).unwrap(); + assert_eq!( + parsed.groups[0].items[0].kind, + ferese_panel::ItemKind::Activities + ); + assert_eq!( + parsed.groups[0].items[0].overflow, + ferese_panel::OverflowPolicy::Always + ); + doc.set("panels", serde_json::to_value(&panels).unwrap()) + .unwrap(); let restored = ferese_config::Document::parse(&doc.to_string()).unwrap(); assert_eq!(restored.get("panels"), doc.get("panels")); } diff --git a/crates/ferese-core/src/focus.rs b/crates/ferese-core/src/focus.rs index e15a65f..a8817e8 100644 --- a/crates/ferese-core/src/focus.rs +++ b/crates/ferese-core/src/focus.rs @@ -21,7 +21,11 @@ impl FocusHistory { self.windows.retain(|id| *id != window); } - pub fn candidates(&self, current: Option, available: impl AsRef<[WindowId]>) -> Vec { + pub fn candidates( + &self, + current: Option, + available: impl AsRef<[WindowId]>, + ) -> Vec { let mut remaining = available.as_ref().iter().copied().collect::>(); let mut ordered = Vec::with_capacity(remaining.len()); @@ -71,7 +75,12 @@ impl FocusCycle { let old = self .selected .and_then(|id| self.windows.iter().position(|candidate| *candidate == id)); - let before = old.map(|index| self.windows[..index].iter().filter(|id| available.contains(id)).count()); + let before = old.map(|index| { + self.windows[..index] + .iter() + .filter(|id| available.contains(id)) + .count() + }); let survives = self.selected.is_some_and(|id| available.contains(&id)); self.windows.retain(|id| available.contains(id)); if self.windows.is_empty() { @@ -142,7 +151,10 @@ mod tests { [3, 2, 1, 4].map(WindowId) ); history.record(WindowId(2)); - assert_eq!(history.candidates(Some(WindowId(2)), available)[1], WindowId(3)); + assert_eq!( + history.candidates(Some(WindowId(2)), available)[1], + WindowId(3) + ); history.remove(WindowId(3)); assert_eq!( history.candidates(None, vec![WindowId(1), WindowId(4)]), @@ -158,7 +170,10 @@ mod tests { assert_eq!(cycle.advance(false, &available), Some(WindowId(1))); assert_eq!(cycle.advance(false, &available), Some(WindowId(3))); assert_eq!(cycle.advance(true, &available), Some(WindowId(1))); - assert_eq!(cycle.advance(false, &[WindowId(3), WindowId(2)]), Some(WindowId(3))); + assert_eq!( + cycle.advance(false, &[WindowId(3), WindowId(2)]), + Some(WindowId(3)) + ); assert_eq!(cycle.advance(false, &[]), None); } @@ -177,7 +192,13 @@ mod tests { let available = [3, 2, 1].map(WindowId); let mut cycle = FocusCycle::new(available.to_vec(), Some(WindowId(3))); assert_eq!(cycle.advance(false, &available), Some(WindowId(2))); - assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1))); - assert_eq!(cycle.reconcile(&[WindowId(3), WindowId(1)]), Some(WindowId(1))); + assert_eq!( + cycle.reconcile(&[WindowId(3), WindowId(1)]), + Some(WindowId(1)) + ); + assert_eq!( + cycle.reconcile(&[WindowId(3), WindowId(1)]), + Some(WindowId(1)) + ); } } diff --git a/crates/ferese-core/src/lib.rs b/crates/ferese-core/src/lib.rs index a13c6cc..59cdbfa 100644 --- a/crates/ferese-core/src/lib.rs +++ b/crates/ferese-core/src/lib.rs @@ -4,8 +4,11 @@ mod workspace; mod workspace_policy; pub use focus::{FocusCycle, FocusHistory}; -pub use output::{DesktopPlan, OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch}; +pub use output::{ + DesktopPlan, OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch, +}; pub use workspace::{ - LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet, + LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, + WorkspaceSet, }; pub use workspace_policy::WorkspaceView; diff --git a/crates/ferese-core/src/output.rs b/crates/ferese-core/src/output.rs index 4a11d81..7457277 100644 --- a/crates/ferese-core/src/output.rs +++ b/crates/ferese-core/src/output.rs @@ -17,7 +17,12 @@ pub struct OutputGeometry { impl OutputGeometry { pub const fn new(x: i32, y: i32, width: i32, height: i32) -> Self { - Self { x, y, width, height } + Self { + x, + y, + width, + height, + } } fn center(self) -> (i64, i64) { @@ -33,7 +38,10 @@ pub enum OutputError { AlreadyConnected(OutputId), UnknownOutput(OutputId), InvalidGeometry(OutputId), - WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId }, + WorkspaceAlreadyAssigned { + workspace: WorkspaceId, + output: OutputId, + }, } impl fmt::Display for OutputError { @@ -41,9 +49,14 @@ impl fmt::Display for OutputError { match self { Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"), Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"), - Self::InvalidGeometry(output) => write!(formatter, "invalid geometry for output {output:?}"), + Self::InvalidGeometry(output) => { + write!(formatter, "invalid geometry for output {output:?}") + } Self::WorkspaceAlreadyAssigned { workspace, output } => { - write!(formatter, "workspace {workspace:?} is assigned to {output:?}") + write!( + formatter, + "workspace {workspace:?} is assigned to {output:?}" + ) } } } @@ -68,7 +81,10 @@ struct OutputState { impl OutputState { fn activate(&mut self, workspace: WorkspaceId) { if self.active != workspace { - self.previous = self.workspaces.contains(&self.active).then_some(self.active); + self.previous = self + .workspaces + .contains(&self.active) + .then_some(self.active); self.active = workspace; } } @@ -148,12 +164,17 @@ impl OutputWorkspaceMap { .copied() .filter(|workspace| { outputs.output_for_workspace(*workspace).is_none() - && outputs.home_output(*workspace).is_none_or(|home| home == id) + && outputs + .home_output(*workspace) + .is_none_or(|home| home == id) }) .min() .unwrap_or_else(|| { let workspace = next_workspace; - next_workspace.0 = next_workspace.0.checked_add(1).expect("workspace IDs exhausted"); + next_workspace.0 = next_workspace + .0 + .checked_add(1) + .expect("workspace IDs exhausted"); created_workspaces.push(workspace); workspace }); @@ -202,7 +223,10 @@ impl OutputWorkspaceMap { }) } - pub fn assigned_workspaces(&self, output: OutputId) -> impl DoubleEndedIterator + '_ { + pub fn assigned_workspaces( + &self, + output: OutputId, + ) -> impl DoubleEndedIterator + '_ { self.outputs .get(&output) .into_iter() @@ -230,7 +254,11 @@ impl OutputWorkspaceMap { } pub fn forget_workspace(&mut self, workspace: WorkspaceId) -> bool { - if self.outputs.values().any(|output| output.active == workspace) { + if self + .outputs + .values() + .any(|output| output.active == workspace) + { return false; } if let Some(owner) = self.assignments.remove(&workspace) @@ -247,7 +275,9 @@ impl OutputWorkspaceMap { self.orphaned_focus = None; } self.evacuations.retain(|_, record| { - record.workspaces.retain(|entry| entry.workspace != workspace); + record + .workspaces + .retain(|entry| entry.workspace != workspace); if record.active == workspace { if let Some(entry) = record.workspaces.first() { record.active = entry.workspace; @@ -282,7 +312,11 @@ impl OutputWorkspaceMap { .assignments .get(&evacuated.workspace) .copied() - .or_else(|| evacuated.target.filter(|target| self.outputs.contains_key(target))); + .or_else(|| { + evacuated + .target + .filter(|target| self.outputs.contains_key(target)) + }); let assignment_matches = self.homes.get(&evacuated.workspace) == Some(&output); let revision_matches = self.revision(evacuated.workspace) == evacuated.revision; @@ -296,7 +330,10 @@ impl OutputWorkspaceMap { // the reconnecting monitor can use its fresh fallback. if target .and_then(|target| self.outputs.get(&target)) - .is_some_and(|state| state.workspaces.len() == 1 && state.workspaces.contains(&evacuated.workspace)) + .is_some_and(|state| { + state.workspaces.len() == 1 + && state.workspaces.contains(&evacuated.workspace) + }) { continue; } @@ -344,7 +381,12 @@ impl OutputWorkspaceMap { let active = record .as_ref() .and_then(|record| reclaimed.contains(&record.active).then_some(record.active)) - .or_else(|| reclaimed.iter().copied().min_by_key(|workspace| workspace.0)); + .or_else(|| { + reclaimed + .iter() + .copied() + .min_by_key(|workspace| workspace.0) + }); let mut active = match active { Some(active) => active, None => { @@ -398,7 +440,10 @@ impl OutputWorkspaceMap { } pub fn disconnect(&mut self, output: OutputId) -> Result, OutputError> { - let removed = self.outputs.remove(&output).ok_or(OutputError::UnknownOutput(output))?; + let removed = self + .outputs + .remove(&output) + .ok_or(OutputError::UnknownOutput(output))?; let target = self.evacuate(output, removed); debug_assert!(self.validate()); Ok(target) @@ -483,7 +528,10 @@ impl OutputWorkspaceMap { } let reassigned = self.assignments.insert(workspace, output) != Some(output); - let state = self.outputs.get_mut(&output).expect("output was checked above"); + let state = self + .outputs + .get_mut(&output) + .expect("output was checked above"); state.workspaces.insert(workspace); state.activate(workspace); self.focused = Some(output); @@ -510,7 +558,11 @@ impl OutputWorkspaceMap { self.switch_workspace(output, workspace) } - pub fn assign_workspace(&mut self, output: OutputId, workspace: WorkspaceId) -> Result { + pub fn assign_workspace( + &mut self, + output: OutputId, + workspace: WorkspaceId, + ) -> Result { if !self.outputs.contains_key(&output) { return Err(OutputError::UnknownOutput(output)); } @@ -545,7 +597,10 @@ impl OutputWorkspaceMap { if let Some(owner) = self.assignments.get(&workspace).copied() && owner != output { - let donor = self.outputs.get_mut(&owner).expect("assigned owner is connected"); + let donor = self + .outputs + .get_mut(&owner) + .expect("assigned owner is connected"); if donor.workspaces.len() == 1 { if let Some(assigned) = self.assignments.get(&donor_fallback) { return Err(OutputError::WorkspaceAlreadyAssigned { @@ -570,13 +625,21 @@ impl OutputWorkspaceMap { } self.assignments.insert(workspace, output); self.homes.insert(workspace, output); - self.outputs.get_mut(&output).unwrap().workspaces.insert(workspace); + self.outputs + .get_mut(&output) + .unwrap() + .workspaces + .insert(workspace); self.bump_revision(workspace); debug_assert!(self.validate()); Ok(()) } - fn migration_target(&self, removed: OutputId, removed_geometry: OutputGeometry) -> Option { + fn migration_target( + &self, + removed: OutputId, + removed_geometry: OutputGeometry, + ) -> Option { if let Some(focused) = self.focused && focused != removed && self.outputs.contains_key(&focused) @@ -606,7 +669,10 @@ impl OutputWorkspaceMap { } fn validate(&self) -> bool { - if self.focused.is_some_and(|focused| !self.outputs.contains_key(&focused)) { + if self + .focused + .is_some_and(|focused| !self.outputs.contains_key(&focused)) + { return false; } @@ -614,10 +680,9 @@ impl OutputWorkspaceMap { if !state.workspaces.contains(&state.active) { return false; } - if state - .previous - .is_some_and(|previous| previous == state.active || !state.workspaces.contains(&previous)) - { + if state.previous.is_some_and(|previous| { + previous == state.active || !state.workspaces.contains(&previous) + }) { return false; } for workspace in &state.workspaces { @@ -662,8 +727,14 @@ mod tests { assert!(!unchanged.focus_changed); let removed = plan(&second.outputs, &[1]); assert_eq!(removed.outputs.focused_output(), Some(OutputId(1))); - assert_eq!(removed.outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1))); - assert_eq!(removed.outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1))); + assert_eq!( + removed.outputs.active_workspace(OutputId(1)), + Some(WorkspaceId(1)) + ); + assert_eq!( + removed.outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(1)) + ); } #[test] @@ -672,7 +743,10 @@ mod tests { before.focus_output(OutputId(2)).unwrap(); let after = plan(&before, &[4]); assert_eq!(after.outputs.focused_output(), Some(OutputId(4))); - assert_eq!(after.outputs.active_workspace(OutputId(4)), Some(WorkspaceId(2))); + assert_eq!( + after.outputs.active_workspace(OutputId(4)), + Some(WorkspaceId(2)) + ); for workspace in 1..=3 { assert_eq!( after.outputs.output_for_workspace(WorkspaceId(workspace)), @@ -695,7 +769,10 @@ mod tests { let before = plan(&OutputWorkspaceMap::default(), &[1, 2]).outputs; let mut evacuated = plan(&before, &[1]).outputs; let restored = plan(&evacuated, &[1, 2]); - assert_eq!(restored.outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2))); + assert_eq!( + restored.outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(2)) + ); evacuated .reassign_workspace(OutputId(1), WorkspaceId(2), WorkspaceId(9)) .unwrap(); @@ -711,11 +788,16 @@ mod tests { let mut before = plan(&OutputWorkspaceMap::default(), &[1, 2, 3]).outputs; before.focus_output(OutputId(2)).unwrap(); before.disconnect(OutputId(1)).unwrap(); - before.switch_workspace(OutputId(2), WorkspaceId(1)).unwrap(); + before + .switch_workspace(OutputId(2), WorkspaceId(1)) + .unwrap(); assert!(before.forget_workspace(WorkspaceId(2))); before.focus_output(OutputId(3)).unwrap(); let after = plan(&before, &[1, 3]).outputs; - assert_eq!(after.output_for_workspace(WorkspaceId(1)), Some(OutputId(1))); + assert_eq!( + after.output_for_workspace(WorkspaceId(1)), + Some(OutputId(1)) + ); assert_eq!(after.active_workspace(OutputId(1)), Some(WorkspaceId(1))); assert_eq!(after.focused_output(), Some(OutputId(3))); } @@ -735,7 +817,11 @@ mod tests { ); assert!( before - .plan_desktop(&[(OutputId(3), OutputGeometry::new(0, 0, 0, 800))], &[], WorkspaceId(3)) + .plan_desktop( + &[(OutputId(3), OutputGeometry::new(0, 0, 0, 800))], + &[], + WorkspaceId(3) + ) .is_err() ); assert_eq!(before, saved); @@ -749,9 +835,15 @@ mod tests { assert_eq!(headless.focused_output(), None); assert!(headless.assignments.is_empty()); let recovered = plan(&headless, &[3]).outputs; - assert_eq!(recovered.active_workspace(OutputId(3)), Some(WorkspaceId(2))); + assert_eq!( + recovered.active_workspace(OutputId(3)), + Some(WorkspaceId(2)) + ); for id in [1, 2] { - assert_eq!(recovered.output_for_workspace(WorkspaceId(id)), Some(OutputId(3))); + assert_eq!( + recovered.output_for_workspace(WorkspaceId(id)), + Some(OutputId(3)) + ); } } @@ -761,7 +853,9 @@ mod tests { let mut seed = 0x5eed_u64; for step in 0..512 { seed = seed.wrapping_mul(6364136223846793005).wrapping_add(1); - let ids = (1..=4).filter(|id| seed & (1 << id) != 0).collect::>(); + let ids = (1..=4) + .filter(|id| seed & (1 << id) != 0) + .collect::>(); let next = plan(&map, &ids); assert!(next.outputs.validate(), "step {step}"); assert_eq!(plan(&next.outputs, &ids).outputs, next.outputs); @@ -793,7 +887,10 @@ mod tests { .filter(|id| seed & (1 << id) != 0) .map(|id| (OutputId(id), geometry(id as i32 * 800))) .collect::>(); - let ids = workspaces.iter().map(|workspace| workspace.id).collect::>(); + let ids = workspaces + .iter() + .map(|workspace| workspace.id) + .collect::>(); let plan = outputs .plan_desktop(&inventory, &ids, workspaces.next_workspace_id()) .unwrap(); @@ -803,7 +900,9 @@ mod tests { outputs = plan.outputs; outputs.reconcile_workspaces(&mut workspaces, &Default::default()); let window = WindowId(step); - workspaces.insert_window(window, Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(window, Axis::Horizontal, 0.5) + .unwrap(); windows.insert(window); if let Some((output, _)) = inventory.last() { outputs.focus_output(*output).unwrap(); @@ -817,10 +916,22 @@ mod tests { workspaces.validate().unwrap(); let actual = workspaces .iter() - .flat_map(|workspace| workspace.layout.window_ids().chain(workspace.floating.iter().copied())) + .flat_map(|workspace| { + workspace + .layout + .window_ids() + .chain(workspace.floating.iter().copied()) + }) .collect::>(); - assert_eq!(actual.len(), windows.len(), "duplicate or lost window at {step}"); - assert_eq!(actual.into_iter().collect::>(), windows); + assert_eq!( + actual.len(), + windows.len(), + "duplicate or lost window at {step}" + ); + assert_eq!( + actual.into_iter().collect::>(), + windows + ); for workspace in workspaces.iter().filter(|workspace| !workspace.is_empty()) { assert_eq!( outputs.output_for_workspace(workspace.id).is_some(), @@ -833,13 +944,19 @@ mod tests { #[test] fn reconnect_cannot_reclaim_the_donor_monitors_only_workspace() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(OutputId(2)).unwrap(); outputs.disconnect(OutputId(2)).unwrap(); assert!(outputs.forget_workspace(WorkspaceId(1))); - outputs.connect(OutputId(2), geometry(1920), WorkspaceId(3)).unwrap(); + outputs + .connect(OutputId(2), geometry(1920), WorkspaceId(3)) + .unwrap(); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2))); assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(3))); @@ -859,7 +976,9 @@ mod tests { assert!(outputs.forget_workspace(first)); assert_eq!(outputs.output_for_workspace(first), None); outputs.disconnect(display).unwrap(); - outputs.connect(display, geometry, super::WorkspaceId(3)).unwrap(); + outputs + .connect(display, geometry, super::WorkspaceId(3)) + .unwrap(); assert_eq!(outputs.active_workspace(display), Some(second)); assert_eq!(outputs.output_for_workspace(first), None); assert!(outputs.validate()); @@ -888,14 +1007,20 @@ mod tests { fn history_tracks_actual_switches_and_toggles_without_replacing_it_on_a_noop() { let mut outputs = OutputWorkspaceMap::default(); let display = OutputId(1); - outputs.connect(display, geometry(0), WorkspaceId(1)).unwrap(); + outputs + .connect(display, geometry(0), WorkspaceId(1)) + .unwrap(); assert_eq!(outputs.previous_workspace(display), None); - outputs.select_workspace(display, WorkspaceId(1), true).unwrap(); + outputs + .select_workspace(display, WorkspaceId(1), true) + .unwrap(); assert_eq!(outputs.previous_workspace(display), None); outputs.switch_workspace(display, WorkspaceId(2)).unwrap(); outputs.switch_workspace(display, WorkspaceId(2)).unwrap(); assert_eq!(outputs.previous_workspace(display), Some(WorkspaceId(1))); - outputs.select_workspace(display, WorkspaceId(2), false).unwrap(); + outputs + .select_workspace(display, WorkspaceId(2), false) + .unwrap(); assert_eq!(outputs.active_workspace(display), Some(WorkspaceId(2))); for expected in [WorkspaceId(1), WorkspaceId(2), WorkspaceId(1)] { let current = outputs.active_workspace(display).unwrap(); @@ -912,7 +1037,9 @@ mod tests { let left = OutputId(1); let right = OutputId(2); outputs.connect(left, geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(right, geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(right, geometry(1920), WorkspaceId(2)) + .unwrap(); outputs.switch_workspace(left, WorkspaceId(3)).unwrap(); outputs.switch_workspace(right, WorkspaceId(4)).unwrap(); assert_eq!( @@ -935,13 +1062,17 @@ mod tests { let left = OutputId(1); let right = OutputId(2); outputs.connect(left, geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(right, geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(right, geometry(1920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(right).unwrap(); outputs.disconnect(right).unwrap(); assert_eq!(outputs.previous_workspace(left), Some(WorkspaceId(1))); outputs.switch_workspace(left, WorkspaceId(1)).unwrap(); assert_eq!(outputs.previous_workspace(left), Some(WorkspaceId(2))); - outputs.connect(right, geometry(1920), WorkspaceId(3)).unwrap(); + outputs + .connect(right, geometry(1920), WorkspaceId(3)) + .unwrap(); assert_eq!(outputs.previous_workspace(left), None); assert_eq!(outputs.previous_workspace(right), None); assert!(outputs.validate()); @@ -951,11 +1082,15 @@ mod tests { fn forgetting_a_previous_workspace_clears_its_history() { let mut outputs = OutputWorkspaceMap::default(); let display = OutputId(1); - outputs.connect(display, geometry(0), WorkspaceId(1)).unwrap(); + outputs + .connect(display, geometry(0), WorkspaceId(1)) + .unwrap(); outputs.switch_workspace(display, WorkspaceId(2)).unwrap(); assert!(outputs.forget_workspace(WorkspaceId(1))); assert_eq!(outputs.previous_workspace(display), None); - outputs.select_workspace(display, WorkspaceId(2), true).unwrap(); + outputs + .select_workspace(display, WorkspaceId(2), true) + .unwrap(); assert_eq!(outputs.active_workspace(display), Some(WorkspaceId(2))); assert_eq!( outputs.select_workspace(OutputId(99), WorkspaceId(2), true), @@ -975,28 +1110,47 @@ mod tests { outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)), Ok(WorkspaceId(2)) ); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(1)) + ); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(2)) + ); } #[test] fn removal_prefers_a_remaining_focused_output_without_replacing_its_workspace() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(OutputId(1)).unwrap(); assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1)))); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(1)) + ); } #[test] fn removing_the_focused_output_activates_its_workspace_on_the_nearest_output() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); - outputs.connect(OutputId(3), geometry(3_840), WorkspaceId(3)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); + outputs + .connect(OutputId(3), geometry(3_840), WorkspaceId(3)) + .unwrap(); outputs.focus_output(OutputId(2)).unwrap(); assert_eq!(outputs.disconnect(OutputId(2)), Ok(Some(OutputId(1)))); @@ -1007,8 +1161,12 @@ mod tests { #[test] fn reconnect_reclaims_automatically_evacuated_workspaces() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(OutputId(1)).unwrap(); outputs.disconnect(OutputId(2)).unwrap(); @@ -1016,20 +1174,29 @@ mod tests { outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)), Ok(WorkspaceId(2)) ); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(2)) + ); assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2))); } #[test] fn reconnect_restores_the_migration_targets_remaining_workspace() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(OutputId(2)).unwrap(); outputs.disconnect(OutputId(2)).unwrap(); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(2))); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)).unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(3)) + .unwrap(); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1))); assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2))); @@ -1038,24 +1205,37 @@ mod tests { #[test] fn using_an_evacuated_workspace_does_not_prevent_automatic_return() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); outputs.focus_output(OutputId(1)).unwrap(); outputs.disconnect(OutputId(2)).unwrap(); - outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap(); + outputs + .switch_workspace(OutputId(1), WorkspaceId(2)) + .unwrap(); assert_eq!( outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(3)), Ok(WorkspaceId(2)) ); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(2)) + ); } #[test] fn repeated_lid_and_monitor_handoffs_restore_both_workspace_owners() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1920), WorkspaceId(2)) + .unwrap(); for removed in [OutputId(1), OutputId(2), OutputId(1), OutputId(2)] { let remaining = if removed == OutputId(1) { OutputId(2) @@ -1064,8 +1244,14 @@ mod tests { }; outputs.focus_output(removed).unwrap(); outputs.disconnect(removed).unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(remaining)); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(remaining)); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(remaining) + ); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(remaining) + ); // Switching between handed-off workspaces is normal use, not an // explicit change of their home output. outputs.switch_workspace(remaining, WorkspaceId(1)).unwrap(); @@ -1077,8 +1263,14 @@ mod tests { WorkspaceId(3), ) .unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(2))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(1)) + ); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(2)) + ); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1))); assert_eq!(outputs.active_workspace(OutputId(2)), Some(WorkspaceId(2))); } @@ -1087,8 +1279,12 @@ mod tests { #[test] fn selecting_a_workspace_owned_by_another_output_focuses_that_output() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1_920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1_920), WorkspaceId(2)) + .unwrap(); assert_eq!( outputs.switch_workspace(OutputId(1), WorkspaceId(2)), @@ -1100,11 +1296,19 @@ mod tests { #[test] fn assigning_an_unused_workspace_does_not_change_the_active_workspace() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); - assert_eq!(outputs.assign_workspace(OutputId(1), WorkspaceId(2)), Ok(OutputId(1))); + assert_eq!( + outputs.assign_workspace(OutputId(1), WorkspaceId(2)), + Ok(OutputId(1)) + ); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(1))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(1)) + ); } #[test] fn two_hop_evacuation_restores_original_home() { @@ -1117,51 +1321,94 @@ mod tests { outputs.focus_output(OutputId(2)).unwrap(); outputs.disconnect(OutputId(1)).unwrap(); outputs.disconnect(OutputId(2)).unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(3))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(3)) + ); assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(1))); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(4)).unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1))); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(4)) + .unwrap(); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(1)) + ); assert!(outputs.active_workspace(OutputId(3)).is_some()); } #[test] fn explicit_reassignment_after_evacuation_prevents_home_steal_back() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1920), WorkspaceId(2)) + .unwrap(); outputs.disconnect(OutputId(1)).unwrap(); outputs .reassign_workspace(OutputId(2), WorkspaceId(1), WorkspaceId(9)) .unwrap(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(3)).unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2))); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(3)) + .unwrap(); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(2)) + ); assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(2))); } #[test] fn reassigning_last_workspace_supplies_donor_fallback() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.connect(OutputId(2), geometry(1920), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .connect(OutputId(2), geometry(1920), WorkspaceId(2)) + .unwrap(); outputs .reassign_workspace(OutputId(2), WorkspaceId(1), WorkspaceId(3)) .unwrap(); assert_eq!(outputs.active_workspace(OutputId(1)), Some(WorkspaceId(3))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(2))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(2)) + ); } #[test] fn final_output_replacement_keeps_all_workspaces_reachable_without_changing_homes() { let mut outputs = OutputWorkspaceMap::default(); - outputs.connect(OutputId(1), geometry(0), WorkspaceId(1)).unwrap(); - outputs.assign_workspace(OutputId(1), WorkspaceId(2)).unwrap(); - outputs.switch_workspace(OutputId(1), WorkspaceId(2)).unwrap(); + outputs + .connect(OutputId(1), geometry(0), WorkspaceId(1)) + .unwrap(); + outputs + .assign_workspace(OutputId(1), WorkspaceId(2)) + .unwrap(); + outputs + .switch_workspace(OutputId(1), WorkspaceId(2)) + .unwrap(); outputs.disconnect(OutputId(1)).unwrap(); - outputs.connect(OutputId(3), geometry(0), WorkspaceId(3)).unwrap(); + outputs + .connect(OutputId(3), geometry(0), WorkspaceId(3)) + .unwrap(); assert_eq!(outputs.active_workspace(OutputId(3)), Some(WorkspaceId(2))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(3))); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(3)) + ); assert_eq!(outputs.home_output(WorkspaceId(1)), Some(OutputId(1))); - outputs.connect(OutputId(1), geometry(1920), WorkspaceId(4)).unwrap(); - assert_eq!(outputs.output_for_workspace(WorkspaceId(1)), Some(OutputId(1))); - assert_eq!(outputs.output_for_workspace(WorkspaceId(2)), Some(OutputId(1))); + outputs + .connect(OutputId(1), geometry(1920), WorkspaceId(4)) + .unwrap(); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(1)), + Some(OutputId(1)) + ); + assert_eq!( + outputs.output_for_workspace(WorkspaceId(2)), + Some(OutputId(1)) + ); } } diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index 41cff91..2ea15ad 100644 --- a/crates/ferese-core/src/workspace.rs +++ b/crates/ferese-core/src/workspace.rs @@ -3,8 +3,8 @@ use std::error::Error; use std::fmt; use ferese_layout::{ - Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, ScrollingLayout, - SizeConstraints, ViewportFocusStrategy, WindowId, + Axis, ColumnWidth, Direction, GapConfig, LayoutError, LayoutResult, LayoutTree, Rect, + ScrollingLayout, SizeConstraints, ViewportFocusStrategy, WindowId, }; #[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] @@ -39,7 +39,11 @@ impl Default for WorkspaceLayout { } impl WorkspaceLayout { - pub fn new(mode: LayoutMode, default_column_width: ColumnWidth, focus_strategy: ViewportFocusStrategy) -> Self { + pub fn new( + mode: LayoutMode, + default_column_width: ColumnWidth, + focus_strategy: ViewportFocusStrategy, + ) -> Self { match mode { LayoutMode::Scrolling => { let mut layout = ScrollingLayout::with_default_width(default_column_width); @@ -95,7 +99,10 @@ impl WorkspaceLayout { Self::Scrolling(_) => None, }; - scrolling.into_iter().flatten().chain(tree.into_iter().flatten()) + scrolling + .into_iter() + .flatten() + .chain(tree.into_iter().flatten()) } pub fn insert( @@ -158,7 +165,12 @@ impl WorkspaceLayout { Self::Scrolling(layout) => { let mut layout = layout.clone(); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + None, + ) .map(|result| result.geometry) } Self::Tree(layout) => layout.geometry(bounds), @@ -190,17 +202,22 @@ impl WorkspaceLayout { for id in &group.windows { let constraint = constraints.entry(*id).or_default(); constraint.min_height += TAB_HEIGHT; - constraint.max_height = constraint.max_height.map(|height| height + TAB_HEIGHT); + constraint.max_height = + constraint.max_height.map(|height| height + TAB_HEIGHT); } } - let mut result = layout.geometry_with_constraints(bounds, gaps, &constraints, focused)?; + let mut result = + layout.geometry_with_constraints(bounds, gaps, &constraints, focused)?; for group in groups { if let Some(rect) = result.geometry.get_mut(&group.active) { let height = TAB_HEIGHT.min((rect.height - 1.0).max(0.0)); let header = Rect::new(rect.x, rect.y, rect.width, height); rect.y += height; rect.height -= height; - result.tab_bars.push(ferese_layout::TabBar { group, rect: header }); + result.tab_bars.push(ferese_layout::TabBar { + group, + rect: header, + }); } } Ok(result) @@ -208,7 +225,11 @@ impl WorkspaceLayout { } } - pub fn automatic_axis(&self, focused: Option, bounds: Rect) -> Result { + pub fn automatic_axis( + &self, + focused: Option, + bounds: Rect, + ) -> Result { match self { Self::Scrolling(_) => Ok(Axis::Horizontal), Self::Tree(layout) => layout.automatic_axis(focused, bounds), @@ -227,14 +248,24 @@ impl WorkspaceLayout { } } - pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result { + pub fn move_window( + &mut self, + window: WindowId, + direction: Direction, + bounds: Rect, + ) -> Result { match self { Self::Scrolling(layout) => layout.move_window(window, direction), Self::Tree(layout) => layout.move_window(window, direction, bounds), } } - pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result { + pub fn resize_window( + &mut self, + window: WindowId, + direction: Direction, + amount: f64, + ) -> Result { match self { Self::Scrolling(layout) => layout.resize_window(window, direction, amount), Self::Tree(layout) => layout.resize_window(window, direction, amount), @@ -256,7 +287,11 @@ impl WorkspaceLayout { } } - pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result { + pub fn cycle_column_width( + &mut self, + window: WindowId, + presets: &[ColumnWidth], + ) -> Result { match self { Self::Scrolling(layout) => layout.cycle_column_width(window, presets), Self::Tree(_) => Ok(false), @@ -303,7 +338,11 @@ impl WorkspaceLayout { for column in columns { for (row, window) in column.windows.into_iter().enumerate() { - let axis = if row == 0 { Axis::Horizontal } else { Axis::Vertical }; + let axis = if row == 0 { + Axis::Horizontal + } else { + Axis::Vertical + }; tree.insert(window, previous, axis, 0.5)?; previous = Some(window); } @@ -414,7 +453,11 @@ impl WorkspaceSet { let active = WorkspaceId(1); let workspace = Workspace { id: active, - layout: WorkspaceLayout::new(default_layout_mode, default_column_width, scrolling_focus_strategy), + layout: WorkspaceLayout::new( + default_layout_mode, + default_column_width, + scrolling_focus_strategy, + ), floating: Vec::new(), last_focused: None, fullscreen: None, @@ -502,7 +545,12 @@ impl WorkspaceSet { } /// Update policy without moving windows or replacing existing column widths. - pub fn reconfigure_defaults(&mut self, mode: LayoutMode, width: ColumnWidth, strategy: ViewportFocusStrategy) { + pub fn reconfigure_defaults( + &mut self, + mode: LayoutMode, + width: ColumnWidth, + strategy: ViewportFocusStrategy, + ) { self.default_layout_mode = mode; self.default_column_width = width; self.scrolling_focus_strategy = strategy; @@ -539,8 +587,12 @@ impl WorkspaceSet { debug_assert!(self.validate().is_ok()); let focused = self.active().last_focused; - let visible_floating = - focused.filter(|window| matches!(self.placement(*window), Some(WindowPlacement::Floating { .. }))); + let visible_floating = focused.filter(|window| { + matches!( + self.placement(*window), + Some(WindowPlacement::Floating { .. }) + ) + }); Ok(visible_floating.or(self.active().fullscreen).or(focused)) } @@ -564,15 +616,23 @@ impl WorkspaceSet { self.placements.get(&window).copied() } - pub fn set_active_layout_mode(&mut self, mode: LayoutMode, bounds: Rect) -> Result { + pub fn set_active_layout_mode( + &mut self, + mode: LayoutMode, + bounds: Rect, + ) -> Result { let default_column_width = self.default_column_width; let focus_strategy = self.scrolling_focus_strategy; let display = self.default_column_display; let workspace = self.active_mut(); let focused = tiled_focus(workspace); - let changed = workspace - .layout - .set_mode(mode, bounds, focused, default_column_width, focus_strategy)?; + let changed = workspace.layout.set_mode( + mode, + bounds, + focused, + default_column_width, + focus_strategy, + )?; if let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout { layout.set_default_display(display); } @@ -581,7 +641,11 @@ impl WorkspaceSet { Ok(changed) } - pub fn set_floating_rect(&mut self, window: WindowId, rect: Rect) -> Result<(), WorkspaceError> { + pub fn set_floating_rect( + &mut self, + window: WindowId, + rect: Rect, + ) -> Result<(), WorkspaceError> { let placement = self .placements .get_mut(&window) @@ -607,23 +671,35 @@ impl WorkspaceSet { self.focus_window_with_reveal(window, false) } - fn focus_window_with_reveal(&mut self, window: WindowId, reveal: bool) -> Result<(), WorkspaceError> { + fn focus_window_with_reveal( + &mut self, + window: WindowId, + reveal: bool, + ) -> Result<(), WorkspaceError> { if self.workspace_for_window(window) != Some(self.active) { return Err(WorkspaceError::InvalidState( "focused window is not on the active workspace", )); } if self.active().fullscreen.is_some_and(|fullscreen| { - fullscreen != window && !matches!(self.placement(window), Some(WindowPlacement::Floating { .. })) + fullscreen != window + && !matches!( + self.placement(window), + Some(WindowPlacement::Floating { .. }) + ) }) { - return Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen")); + return Err(WorkspaceError::InvalidState( + "focused window is hidden by fullscreen", + )); } if self.placement(window) == Some(WindowPlacement::Tiled) { if reveal { self.active_mut().layout.activate_window(window)?; } else { - self.active_mut().layout.activate_window_without_reveal(window)?; + self.active_mut() + .layout + .activate_window_without_reveal(window)?; } } self.active_mut().last_focused = Some(window); @@ -632,7 +708,11 @@ impl WorkspaceSet { Ok(()) } - pub fn stack_window(&mut self, window: WindowId, target: WindowId) -> Result<(), WorkspaceError> { + pub fn stack_window( + &mut self, + window: WindowId, + target: WindowId, + ) -> Result<(), WorkspaceError> { let workspace = self .workspace_for_window(window) .ok_or(LayoutError::UnknownWindow(window))?; @@ -673,7 +753,11 @@ impl WorkspaceSet { Ok(changed) } - pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result { + pub fn cycle_column_width( + &mut self, + window: WindowId, + presets: &[ColumnWidth], + ) -> Result { if self.workspace_for_window(window) != Some(self.active) || self.placement(window) != Some(WindowPlacement::Tiled) { @@ -682,7 +766,10 @@ impl WorkspaceSet { )); } - let changed = self.active_mut().layout.cycle_column_width(window, presets)?; + let changed = self + .active_mut() + .layout + .cycle_column_width(window, presets)?; debug_assert!(self.validate().is_ok()); Ok(changed) } @@ -743,7 +830,10 @@ impl WorkspaceSet { /// Creates an identity; display positions are assigned by the output owner. pub fn create_workspace(&mut self) -> WorkspaceId { let id = WorkspaceId(self.next_id); - self.next_id = self.next_id.checked_add(1).expect("workspace IDs exhausted"); + self.next_id = self + .next_id + .checked_add(1) + .expect("workspace IDs exhausted"); self.workspaces.insert( id, Workspace { @@ -770,7 +860,12 @@ impl WorkspaceSet { self.activate(workspace) } - pub fn insert_window(&mut self, window: WindowId, axis: Axis, ratio: f64) -> Result<(), WorkspaceError> { + pub fn insert_window( + &mut self, + window: WindowId, + axis: Axis, + ratio: f64, + ) -> Result<(), WorkspaceError> { if self.window_workspaces.contains_key(&window) { return Err(WorkspaceError::DuplicateWindow(window)); } @@ -980,7 +1075,11 @@ impl WorkspaceSet { Ok(enabled) } - pub fn set_fullscreen(&mut self, window: WindowId, enabled: bool) -> Result { + pub fn set_fullscreen( + &mut self, + window: WindowId, + enabled: bool, + ) -> Result { let workspace_id = self .workspace_for_window(window) .ok_or(LayoutError::UnknownWindow(window))?; @@ -1014,7 +1113,9 @@ impl WorkspaceSet { for (id, workspace) in &self.workspaces { if workspace.id != *id { - return Err(WorkspaceError::InvalidState("workspace index is inconsistent")); + return Err(WorkspaceError::InvalidState( + "workspace index is inconsistent", + )); } workspace.layout.validate()?; @@ -1024,7 +1125,9 @@ impl WorkspaceSet { || self.window_workspaces.get(&window) != Some(id) || self.placements.get(&window) != Some(&WindowPlacement::Tiled) { - return Err(WorkspaceError::InvalidState("window ownership is inconsistent")); + return Err(WorkspaceError::InvalidState( + "window ownership is inconsistent", + )); } } @@ -1034,7 +1137,10 @@ impl WorkspaceSet { if !seen_floating.insert(*window) || !seen_windows.insert(*window) || self.window_workspaces.get(window) != Some(id) - || !matches!(self.placements.get(window), Some(WindowPlacement::Floating { .. })) + || !matches!( + self.placements.get(window), + Some(WindowPlacement::Floating { .. }) + ) { return Err(WorkspaceError::InvalidState( "floating window ownership is inconsistent", @@ -1046,7 +1152,9 @@ impl WorkspaceSet { .last_focused .is_some_and(|window| self.window_workspaces.get(&window) != Some(id)) { - return Err(WorkspaceError::InvalidState("workspace focus is inconsistent")); + return Err(WorkspaceError::InvalidState( + "workspace focus is inconsistent", + )); } if workspace @@ -1059,8 +1167,12 @@ impl WorkspaceSet { } } - if seen_windows.len() != self.window_workspaces.len() || self.placements.len() != self.window_workspaces.len() { - return Err(WorkspaceError::InvalidState("window index retains stale entries")); + if seen_windows.len() != self.window_workspaces.len() + || self.placements.len() != self.window_workspaces.len() + { + return Err(WorkspaceError::InvalidState( + "window index retains stale entries", + )); } Ok(()) @@ -1068,10 +1180,13 @@ impl WorkspaceSet { } fn first_window(workspace: &Workspace) -> Option { - workspace - .layout - .preferred_window() - .or_else(|| workspace.floating.iter().copied().min_by_key(|window| window.0)) + workspace.layout.preferred_window().or_else(|| { + workspace + .floating + .iter() + .copied() + .min_by_key(|window| window.0) + }) } fn tiled_focus(workspace: &Workspace) -> Option { @@ -1110,7 +1225,9 @@ mod tests { fn live_policy_updates_existing_layouts_and_default_widths_not_custom_widths() { let mut workspaces = WorkspaceSet::default(); for id in 1..=3 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } let WorkspaceLayout::Scrolling(layout) = &mut workspaces.active_mut().layout else { panic!() @@ -1149,18 +1266,30 @@ mod tests { #[test] fn reconfiguration_preserves_windows_and_current_widths_but_updates_defaults() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let workspace = workspaces.active_id(); - workspaces.reconfigure_defaults(LayoutMode::Tree, ColumnWidth::Full, ViewportFocusStrategy::Minimal); + workspaces.reconfigure_defaults( + LayoutMode::Tree, + ColumnWidth::Full, + ViewportFocusStrategy::Minimal, + ); let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else { panic!("existing layout must survive"); }; assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.5)); assert_eq!(layout.default_width(), ColumnWidth::Full); - assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace)); + assert_eq!( + workspaces.workspace_for_window(WindowId(1)), + Some(workspace) + ); workspaces.switch_to_fixture(2).unwrap(); assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree); - assert_eq!(workspaces.workspace_for_window(WindowId(1)), Some(workspace)); + assert_eq!( + workspaces.workspace_for_window(WindowId(1)), + Some(workspace) + ); } use super::*; @@ -1179,9 +1308,14 @@ mod tests { #[test] fn configured_layout_defaults_apply_to_new_workspaces() { - let mut workspaces = - WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Full, ViewportFocusStrategy::Minimal); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + let mut workspaces = WorkspaceSet::new( + LayoutMode::Scrolling, + ColumnWidth::Full, + ViewportFocusStrategy::Minimal, + ); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let first = workspaces .active_mut() .layout @@ -1193,7 +1327,10 @@ mod tests { ) .unwrap(); - assert_eq!(first.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 992.0, 792.0)); + assert_eq!( + first.geometry[&WindowId(1)], + Rect::new(4.0, 4.0, 992.0, 792.0) + ); workspaces.switch_to_fixture(2).unwrap(); assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling); } @@ -1201,10 +1338,14 @@ mod tests { #[test] fn switching_restores_workspace_focus() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); assert_eq!(workspaces.switch_to_fixture(2).unwrap(), None); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); assert_eq!(workspaces.switch_to_fixture(1).unwrap(), Some(WindowId(1))); assert_eq!(workspaces.switch_to_fixture(2).unwrap(), Some(WindowId(2))); @@ -1214,8 +1355,12 @@ mod tests { #[test] fn moving_a_window_updates_membership_without_switching() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); let first = workspaces.active_id(); let second = workspaces .move_window_to_fixture(WindowId(2), 2, Axis::Horizontal, 0.5) @@ -1223,16 +1368,32 @@ mod tests { assert_eq!(workspaces.active_id(), first); assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(second)); - assert!(!workspaces.workspace(first).unwrap().layout.contains(WindowId(2))); - assert!(workspaces.workspace(second).unwrap().layout.contains(WindowId(2))); + assert!( + !workspaces + .workspace(first) + .unwrap() + .layout + .contains(WindowId(2)) + ); + assert!( + workspaces + .workspace(second) + .unwrap() + .layout + .contains(WindowId(2)) + ); assert!(workspaces.validate().is_ok()); } #[test] fn removing_focus_chooses_a_deterministic_remaining_window() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces.remove_window(WindowId(1)).unwrap(); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); @@ -1243,7 +1404,9 @@ mod tests { fn closing_a_scrolling_column_focuses_its_adjacent_column() { let mut workspaces = WorkspaceSet::default(); for id in 1..=4 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } workspaces.focus_window(WindowId(3)).unwrap(); @@ -1255,7 +1418,9 @@ mod tests { #[test] fn focus_rejects_a_window_on_an_inactive_workspace() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces .move_window_to_fixture(WindowId(1), 2, Axis::Horizontal, 0.5) .unwrap(); @@ -1272,7 +1437,9 @@ mod tests { #[test] fn floating_toggle_removes_and_restores_tiled_membership() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let rect = Rect::new(10.0, 20.0, 640.0, 480.0); assert_eq!( @@ -1298,14 +1465,21 @@ mod tests { #[test] fn floating_geometry_updates_are_authoritative_and_validated() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); assert_eq!( workspaces.set_floating_rect(WindowId(1), Rect::default()), Err(WorkspaceError::InvalidState("window is not floating")) ); workspaces - .toggle_floating(WindowId(1), Rect::new(10.0, 20.0, 640.0, 480.0), Axis::Horizontal, 0.5) + .toggle_floating( + WindowId(1), + Rect::new(10.0, 20.0, 640.0, 480.0), + Axis::Horizontal, + 0.5, + ) .unwrap(); workspaces .set_floating_rect(WindowId(1), Rect::new(30.0, 40.0, f64::NAN, -2.0)) @@ -1327,9 +1501,15 @@ mod tests { ViewportFocusStrategy::Center, ViewportFocusStrategy::Paged, ] { - let mut workspaces = WorkspaceSet::new(LayoutMode::Scrolling, ColumnWidth::Proportion(0.5), strategy); + let mut workspaces = WorkspaceSet::new( + LayoutMode::Scrolling, + ColumnWidth::Proportion(0.5), + strategy, + ); for id in 1..=3 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0); let gaps = GapConfig { @@ -1379,13 +1559,20 @@ mod tests { ViewportFocusStrategy::Paged, ); for id in 1..=4 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0); let before = workspaces .active_mut() .layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4))) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(WindowId(4)), + ) .unwrap(); let scroll = workspaces.active().layout.viewport_x(); workspaces @@ -1399,7 +1586,12 @@ mod tests { let after = workspaces .active_mut() .layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5))) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(WindowId(5)), + ) .unwrap(); assert_eq!(before.geometry, after.geometry); assert_eq!(scroll, workspaces.active().layout.viewport_x()); @@ -1410,7 +1602,9 @@ mod tests { #[test] fn floating_window_can_follow_its_parent_workspace_without_focus() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let parent_workspace = workspaces.active_id(); workspaces.switch_to_fixture(2).unwrap(); workspaces @@ -1422,7 +1616,10 @@ mod tests { ) .unwrap(); - assert_eq!(workspaces.workspace_for_window(WindowId(2)), Some(parent_workspace)); + assert_eq!( + workspaces.workspace_for_window(WindowId(2)), + Some(parent_workspace) + ); assert_eq!( workspaces.workspace(parent_workspace).unwrap().last_focused, Some(WindowId(1)) @@ -1433,7 +1630,9 @@ mod tests { #[test] fn fullscreen_preserves_underlying_placement() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let before = workspaces.placement(WindowId(1)); assert!(workspaces.toggle_fullscreen(WindowId(1)).unwrap()); @@ -1451,9 +1650,13 @@ mod tests { #[test] fn mapping_under_fullscreen_does_not_change_focus() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces.toggle_fullscreen(WindowId(1)).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); assert_eq!(workspaces.active().last_focused, Some(WindowId(1))); assert_eq!(workspaces.active().fullscreen, Some(WindowId(1))); @@ -1465,10 +1668,17 @@ mod tests { fn floating_dialog_can_focus_above_fullscreen() { let mut workspaces = WorkspaceSet::default(); let workspace = workspaces.active_id(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces.toggle_fullscreen(WindowId(1)).unwrap(); workspaces - .insert_floating_window(WindowId(2), workspace, Rect::new(100.0, 100.0, 500.0, 400.0), true) + .insert_floating_window( + WindowId(2), + workspace, + Rect::new(100.0, 100.0, 500.0, 400.0), + true, + ) .unwrap(); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); @@ -1482,10 +1692,14 @@ mod tests { #[test] fn switching_to_fullscreen_restores_the_visible_window() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces.toggle_fullscreen(WindowId(1)).unwrap(); workspaces.switch_to_fixture(2).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); workspaces .move_window_to_fixture(WindowId(2), 1, Axis::Horizontal, 0.5) .unwrap(); @@ -1494,7 +1708,9 @@ mod tests { assert_eq!(workspaces.active().last_focused, Some(WindowId(1))); assert_eq!( workspaces.focus_window(WindowId(2)), - Err(WorkspaceError::InvalidState("focused window is hidden by fullscreen")) + Err(WorkspaceError::InvalidState( + "focused window is hidden by fullscreen" + )) ); assert!(workspaces.validate().is_ok()); } @@ -1502,8 +1718,12 @@ mod tests { #[test] fn grouping_tiled_windows_preserves_workspace_membership() { let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); workspaces.stack_window(WindowId(2), WindowId(1)).unwrap(); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); @@ -1524,16 +1744,26 @@ mod tests { fn layout_mode_conversion_preserves_membership_and_focus() { let mut workspaces = WorkspaceSet::default(); for id in 1..=4 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } workspaces.focus_window(WindowId(2)).unwrap(); let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); - assert!(workspaces.set_active_layout_mode(LayoutMode::Tree, bounds).unwrap()); + assert!( + workspaces + .set_active_layout_mode(LayoutMode::Tree, bounds) + .unwrap() + ); assert_eq!(workspaces.active().layout.mode(), LayoutMode::Tree); assert_eq!(workspaces.active().last_focused, Some(WindowId(2))); assert_eq!( - workspaces.active().layout.window_ids().collect::>(), + workspaces + .active() + .layout + .window_ids() + .collect::>(), HashSet::from([WindowId(1), WindowId(2), WindowId(3), WindowId(4)]) ); @@ -1553,9 +1783,13 @@ mod tests { ColumnWidth::Proportion(0.5), ViewportFocusStrategy::Minimal, ); - workspaces.insert_window(ids[0], Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(ids[0], Axis::Horizontal, 0.5) + .unwrap(); for pair in ids.windows(2) { - workspaces.insert_window(pair[1], Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(pair[1], Axis::Horizontal, 0.5) + .unwrap(); workspaces.stack_window(pair[1], pair[0]).unwrap(); } workspaces @@ -1564,7 +1798,13 @@ mod tests { #[test] fn stacked_tree_mode_conversions_preserve_membership_focus_and_order() { let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); - let all_ids = [WindowId(30), WindowId(10), WindowId(20), WindowId(5), WindowId(40)]; + let all_ids = [ + WindowId(30), + WindowId(10), + WindowId(20), + WindowId(5), + WindowId(40), + ]; for count in [2, 3, 5] { let ids = &all_ids[..count]; let expected = ids.iter().copied().collect::>(); @@ -1583,7 +1823,11 @@ mod tests { assert_eq!(workspaces.active().last_focused, Some(*focused)); assert!(workspaces.validate().is_ok()); - for mode in [LayoutMode::Scrolling, LayoutMode::Tree, LayoutMode::Scrolling] { + for mode in [ + LayoutMode::Scrolling, + LayoutMode::Tree, + LayoutMode::Scrolling, + ] { assert!(workspaces.set_active_layout_mode(mode, bounds).unwrap()); let windows = workspaces.active().layout.window_ids().collect::>(); assert_eq!(windows.len(), ids.len()); @@ -1605,15 +1849,29 @@ mod tests { let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); let mut workspaces = tree_stack_workspaces(&[WindowId(30), WindowId(10), WindowId(20)]); workspaces.focus_window(WindowId(10)).unwrap(); - workspaces.insert_window(WindowId(5), Axis::Vertical, 0.5).unwrap(); - workspaces.insert_window(WindowId(40), Axis::Horizontal, 0.5).unwrap(); - workspaces.insert_window(WindowId(50), Axis::Vertical, 0.5).unwrap(); + workspaces + .insert_window(WindowId(5), Axis::Vertical, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(40), Axis::Horizontal, 0.5) + .unwrap(); + workspaces + .insert_window(WindowId(50), Axis::Vertical, 0.5) + .unwrap(); workspaces.stack_window(WindowId(50), WindowId(40)).unwrap(); workspaces.focus_window(WindowId(40)).unwrap(); - let before = workspaces.active().layout.window_ids().collect::>(); + let before = workspaces + .active() + .layout + .window_ids() + .collect::>(); assert_eq!(before.len(), 6); assert!(workspaces.active().layout.geometry(bounds).unwrap().len() < before.len()); - for mode in [LayoutMode::Scrolling, LayoutMode::Tree, LayoutMode::Scrolling] { + for mode in [ + LayoutMode::Scrolling, + LayoutMode::Tree, + LayoutMode::Scrolling, + ] { assert!(workspaces.set_active_layout_mode(mode, bounds).unwrap()); let windows = workspaces.active().layout.window_ids().collect::>(); assert_eq!(windows.len(), before.len()); @@ -1631,11 +1889,19 @@ mod tests { for _ in 0..32 { let mut workspaces = tree_stack_workspaces(&[WindowId(30), WindowId(20), WindowId(40)]); workspaces.active_mut().last_focused = None; - assert_eq!(workspaces.active().layout.preferred_window(), Some(WindowId(40))); - workspaces.insert_window(WindowId(10), Axis::Horizontal, 0.5).unwrap(); + assert_eq!( + workspaces.active().layout.preferred_window(), + Some(WindowId(40)) + ); + workspaces + .insert_window(WindowId(10), Axis::Horizontal, 0.5) + .unwrap(); workspaces.remove_window(WindowId(10)).unwrap(); assert_eq!(workspaces.active().last_focused, Some(WindowId(40))); - assert_eq!(workspaces.active().layout.preferred_window(), Some(WindowId(40))); + assert_eq!( + workspaces.active().layout.preferred_window(), + Some(WindowId(40)) + ); assert!(workspaces.validate().is_ok()); } } @@ -1644,14 +1910,19 @@ mod tests { fn scrolling_column_commands_preserve_workspace_state() { let mut workspaces = WorkspaceSet::default(); for id in 1..=3 { - workspaces.insert_window(WindowId(id), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(id), Axis::Horizontal, 0.5) + .unwrap(); } workspaces.stack_window(WindowId(3), WindowId(2)).unwrap(); assert!(workspaces.extract_window(WindowId(3)).unwrap()); assert!( workspaces - .cycle_column_width(WindowId(3), &[ColumnWidth::Proportion(0.5), ColumnWidth::Full],) + .cycle_column_width( + WindowId(3), + &[ColumnWidth::Proportion(0.5), ColumnWidth::Full], + ) .unwrap() ); let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); @@ -1679,7 +1950,9 @@ mod tests { 0 if windows.len() < 48 => { let window = WindowId(next_window); next_window += 1; - workspaces.insert_window(window, random_axis(random), 0.5).unwrap(); + workspaces + .insert_window(window, random_axis(random), 0.5) + .unwrap(); windows.push(window); } 1 if !windows.is_empty() => { @@ -1726,7 +1999,8 @@ mod tests { }) .collect::>(); - if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) { + if let Some(window) = candidates.get(random as usize % candidates.len().max(1)) + { workspaces.focus_window(*window).unwrap(); } } @@ -1774,7 +2048,9 @@ mod tab_geometry_tests { ) .unwrap(); } - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else { panic!() }; @@ -1785,9 +2061,13 @@ mod tab_geometry_tests { fn default_column_display_only_changes_future_columns_and_reaches_new_workspaces() { use ferese_layout::ColumnDisplay; let mut workspaces = WorkspaceSet::default(); - workspaces.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); workspaces.set_default_column_display(ColumnDisplay::Tabbed); - workspaces.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else { panic!() }; @@ -1795,13 +2075,17 @@ mod tab_geometry_tests { assert_eq!(layout.columns()[1].display, ColumnDisplay::Tabbed); let next = workspaces.create_workspace(); workspaces.activate(next).unwrap(); - workspaces.insert_window(WindowId(3), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(3), Axis::Horizontal, 0.5) + .unwrap(); let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else { panic!() }; assert_eq!(layout.columns()[0].display, ColumnDisplay::Tabbed); workspaces.set_default_column_display(ColumnDisplay::Stacked); - workspaces.insert_window(WindowId(4), Axis::Horizontal, 0.5).unwrap(); + workspaces + .insert_window(WindowId(4), Axis::Horizontal, 0.5) + .unwrap(); let WorkspaceLayout::Scrolling(layout) = &workspaces.active().layout else { panic!() }; @@ -1812,7 +2096,9 @@ mod tab_geometry_tests { #[test] fn tab_header_reserves_space_and_disappears_after_leaving() { let mut layout = WorkspaceLayout::Tree(LayoutTree::default()); - layout.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + layout + .insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); layout .insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); diff --git a/crates/ferese-core/src/workspace_policy.rs b/crates/ferese-core/src/workspace_policy.rs index 372291e..3dc2f17 100644 --- a/crates/ferese-core/src/workspace_policy.rs +++ b/crates/ferese-core/src/workspace_policy.rs @@ -56,13 +56,22 @@ impl OutputWorkspaceMap { .filter(|id| workspaces.workspace(*id).is_some()) } - pub fn ordered_workspaces(&self, workspaces: &WorkspaceSet, output: OutputId) -> Vec { + pub fn ordered_workspaces( + &self, + workspaces: &WorkspaceSet, + output: OutputId, + ) -> Vec { self.ordered_workspace_ids(workspaces, output).collect() } /// Out-of-range numeric selection resolves to the trailing spare. Looking /// up a gesture target never creates workspaces or changes history. - pub fn workspace_at(&self, workspaces: &WorkspaceSet, output: OutputId, index: u32) -> Option { + pub fn workspace_at( + &self, + workspaces: &WorkspaceSet, + output: OutputId, + index: u32, + ) -> Option { let index = index.checked_sub(1)? as usize; self.ordered_workspace_ids(workspaces, output) .nth(index) @@ -76,8 +85,13 @@ impl OutputWorkspaceMap { workspaces: &mut WorkspaceSet, transitions: &HashSet, ) -> Vec { - if let Some(active) = self.focused_output().and_then(|output| self.active_workspace(output)) { - workspaces.activate(active).expect("output's active workspace exists"); + if let Some(active) = self + .focused_output() + .and_then(|output| self.active_workspace(output)) + { + workspaces + .activate(active) + .expect("output's active workspace exists"); } let mut protected = HashSet::new(); @@ -129,7 +143,11 @@ mod tests { let mut outputs = OutputWorkspaceMap::default(); let output = OutputId(1); outputs - .connect(output, OutputGeometry::new(0, 0, 1000, 800), set.active_id()) + .connect( + output, + OutputGeometry::new(0, 0, 1000, 800), + set.active_id(), + ) .unwrap(); (set, outputs, output) @@ -142,7 +160,10 @@ mod tests { for output in outputs.connected_outputs() { let ids = outputs.ordered_workspaces(set, output); assert!(set.workspace(*ids.last().unwrap()).unwrap().is_empty()); - let empties = ids.iter().filter(|id| set.workspace(**id).unwrap().is_empty()).count(); + let empties = ids + .iter() + .filter(|id| set.workspace(**id).unwrap().is_empty()) + .count(); let active_middle_empty = outputs .active_workspace(output) .is_some_and(|id| Some(&id) != ids.last() && set.workspace(id).unwrap().is_empty()); @@ -156,12 +177,14 @@ mod tests { let first = set.active_id(); reconcile(&mut set, &mut outputs); assert_eq!(set.iter().count(), 1); - set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); let second = outputs.workspace_at(&set, output, 2).unwrap(); outputs.switch_workspace(output, second).unwrap(); reconcile(&mut set, &mut outputs); - set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); let spare = outputs.workspace_at(&set, output, 3).unwrap(); @@ -187,7 +210,8 @@ mod tests { fn selected_empty_survives_until_leaving_but_history_does_not_pin_it() { let (mut set, mut outputs, output) = setup(); let first = set.active_id(); - set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); let spare = outputs.workspace_at(&set, output, 2).unwrap(); set.remove_window(WindowId(1)).unwrap(); @@ -205,7 +229,8 @@ mod tests { fn transitions_temporarily_protect_empty_workspaces_and_cancelled_lookup_has_no_effect() { let (mut set, mut outputs, output) = setup(); let first = set.active_id(); - set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); let spare = outputs.workspace_at(&set, output, 2).unwrap(); let before = outputs.workspace_views(&set); @@ -246,7 +271,8 @@ mod tests { fn monitors_have_independent_positions_and_reconnect_restores_occupied_workspaces() { let (mut set, mut outputs, left) = setup(); let first = set.active_id(); - set.insert_window(WindowId(1), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(1), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); let right = OutputId(2); let second = set.create_workspace(); @@ -255,16 +281,25 @@ mod tests { .unwrap(); outputs.focus_output(right).unwrap(); reconcile(&mut set, &mut outputs); - set.insert_window(WindowId(2), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(2), Axis::Horizontal, 0.5) + .unwrap(); reconcile(&mut set, &mut outputs); assert_eq!(outputs.workspace_at(&set, left, 1), Some(first)); assert_eq!(outputs.workspace_at(&set, right, 1), Some(second)); assert_eq!( - outputs.workspace_views(&set).iter().filter(|view| view.visible).count(), + outputs + .workspace_views(&set) + .iter() + .filter(|view| view.visible) + .count(), 2 ); assert_eq!( - outputs.workspace_views(&set).iter().filter(|view| view.focused).count(), + outputs + .workspace_views(&set) + .iter() + .filter(|view| view.focused) + .count(), 1 ); @@ -295,7 +330,11 @@ mod sequences { let mut set = WorkspaceSet::default(); let mut outputs = OutputWorkspaceMap::default(); outputs - .connect(OutputId(1), OutputGeometry::new(0, 0, 1000, 800), set.active_id()) + .connect( + OutputId(1), + OutputGeometry::new(0, 0, 1000, 800), + set.active_id(), + ) .unwrap(); let mut next_window = 1; let mut seed = 0x1234_5678_u64; @@ -313,11 +352,13 @@ mod sequences { match seed % 5 { 0 => { - set.insert_window(WindowId(next_window), Axis::Horizontal, 0.5).unwrap(); + set.insert_window(WindowId(next_window), Axis::Horizontal, 0.5) + .unwrap(); next_window += 1; } 1 if !windows.is_empty() => { - set.remove_window(windows[seed as usize % windows.len()]).unwrap(); + set.remove_window(windows[seed as usize % windows.len()]) + .unwrap(); } 2 => { let index = (seed % 9 + 1) as u32; @@ -326,8 +367,13 @@ mod sequences { } 3 if !windows.is_empty() => { let target = outputs.workspace_at(&set, output, u32::MAX).unwrap(); - set.move_window_to_workspace(windows[seed as usize % windows.len()], target, Axis::Horizontal, 0.5) - .unwrap(); + set.move_window_to_workspace( + windows[seed as usize % windows.len()], + target, + Axis::Horizontal, + 0.5, + ) + .unwrap(); } _ => { if outputs.active_workspace(OutputId(2)).is_some() { @@ -335,7 +381,11 @@ mod sequences { } else { let fallback = set.create_workspace(); outputs - .connect(OutputId(2), OutputGeometry::new(1000, 0, 1000, 800), fallback) + .connect( + OutputId(2), + OutputGeometry::new(1000, 0, 1000, 800), + fallback, + ) .unwrap(); outputs.focus_output(OutputId(2)).unwrap(); } @@ -348,7 +398,10 @@ mod sequences { assert_eq!(views.iter().filter(|view| view.focused).count(), 1); assert_eq!(views.len(), set.iter().count()); for output in outputs.connected_outputs() { - let rows = views.iter().filter(|view| view.output == output).collect::>(); + let rows = views + .iter() + .filter(|view| view.output == output) + .collect::>(); assert_eq!(rows.last().unwrap().window_count, 0); for (index, row) in rows.iter().enumerate() { assert_eq!(row.index, index as u32 + 1); diff --git a/crates/ferese-ipc/src/activities.rs b/crates/ferese-ipc/src/activities.rs index 45d7fb4..63c6f22 100644 --- a/crates/ferese-ipc/src/activities.rs +++ b/crates/ferese-ipc/src/activities.rs @@ -5,7 +5,10 @@ use serde::{Deserialize, Serialize}; pub const VERSION: u32 = 1; pub fn now_ms() -> u64 { - let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 }; + let mut time = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; // SAFETY: time is a correctly sized writable timespec. CLOCK_BOOTTIME // includes suspend, unlike the renderer's animation clock. if unsafe { libc::clock_gettime(libc::CLOCK_BOOTTIME, &raw mut time) } != 0 { diff --git a/crates/ferese-ipc/src/connection.rs b/crates/ferese-ipc/src/connection.rs index 7e5724d..afcaa3b 100644 --- a/crates/ferese-ipc/src/connection.rs +++ b/crates/ferese-ipc/src/connection.rs @@ -40,7 +40,8 @@ impl Connection { command: command.into(), args, }; - let response = write_frame(&mut self.stream, &request).and_then(|()| read_frame::(&mut self.stream)); + let response = write_frame(&mut self.stream, &request) + .and_then(|()| read_frame::(&mut self.stream)); let response = match response { Ok(response) => response, Err(error) => { @@ -60,7 +61,9 @@ impl Connection { /// Wait for a server-side change; dropping the cancellation handle interrupts it. pub fn wait(&mut self, command: &str, args: Value) -> Result { - self.stream.set_read_timeout(None).map_err(|e| e.to_string())?; + self.stream + .set_read_timeout(None) + .map_err(|e| e.to_string())?; let result = self.call(command, args); self.stream .set_read_timeout(Some(Duration::from_secs(2))) @@ -93,12 +96,19 @@ mod tests { send.send(connection.wait("activity-watch", json!({"since": 7}))) .unwrap(); }); - server.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); let request: Request = read_frame(&mut server).unwrap(); assert_eq!(request.command, "activity-watch"); assert_eq!(request.args, json!({"since": 7})); drop(cancellation); - assert!(receive.recv_timeout(Duration::from_secs(2)).unwrap().is_err()); + assert!( + receive + .recv_timeout(Duration::from_secs(2)) + .unwrap() + .is_err() + ); assert!(read_frame::(&mut server).is_err()); worker.join().unwrap(); } @@ -107,8 +117,12 @@ mod tests { fn response_identity_is_checked_before_returning_values() { for wrong_version in [false, true] { let (stream, mut server) = UnixStream::pair().unwrap(); - server.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); - stream.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + stream + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); let worker = std::thread::spawn(move || { let request: Request = read_frame(&mut server).unwrap(); let mut response = Response::success(request.id, json!({})); diff --git a/crates/ferese-ipc/src/lib.rs b/crates/ferese-ipc/src/lib.rs index 61731b3..ca58ef3 100644 --- a/crates/ferese-ipc/src/lib.rs +++ b/crates/ferese-ipc/src/lib.rs @@ -79,7 +79,10 @@ impl fmt::Display for FrameError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Io(error) => error.fmt(formatter), - Self::Oversized(size) => write!(formatter, "IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}"), + Self::Oversized(size) => write!( + formatter, + "IPC payload is {size} bytes; maximum is {MAX_PAYLOAD_SIZE}" + ), Self::InvalidJson(error) => write!(formatter, "invalid IPC JSON: {error}"), } } diff --git a/crates/ferese-ipc/src/media.rs b/crates/ferese-ipc/src/media.rs index ddaad6b..ffa949b 100644 --- a/crates/ferese-ipc/src/media.rs +++ b/crates/ferese-ipc/src/media.rs @@ -1,7 +1,10 @@ use serde::{Deserialize, Serialize}; pub fn now_us() -> u64 { - let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 }; + let mut time = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; // CLOCK_MONOTONIC shares one epoch between the compositor and shell. if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } != 0 { return 0; @@ -100,7 +103,10 @@ impl Player { } let position = position.max(0.0) as u64; - Some(self.length_us.map_or(position, |length| position.min(length))) + Some( + self.length_us + .map_or(position, |length| position.min(length)), + ) } } diff --git a/crates/ferese-ipc/src/socket.rs b/crates/ferese-ipc/src/socket.rs index 8908092..5b005cf 100644 --- a/crates/ferese-ipc/src/socket.rs +++ b/crates/ferese-ipc/src/socket.rs @@ -16,10 +16,17 @@ pub fn runtime_default() -> io::Result { resolve_with(None, None, std::env::var_os("XDG_RUNTIME_DIR").as_deref()) } -fn resolve_with(explicit: Option<&Path>, socket: Option<&OsStr>, runtime: Option<&OsStr>) -> io::Result { +fn resolve_with( + explicit: Option<&Path>, + socket: Option<&OsStr>, + runtime: Option<&OsStr>, +) -> io::Result { if let Some(path) = explicit { if path.as_os_str().is_empty() { - return Err(io::Error::new(io::ErrorKind::InvalidInput, "IPC socket path is empty")); + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + "IPC socket path is empty", + )); } return Ok(path.to_owned()); } @@ -41,8 +48,14 @@ mod tests { let runtime = Some(OsStr::new("/run/user/1000")); let host = OsStr::new("/run/user/1000/ferese/control.sock"); let nested = Path::new("/tmp/nested/ferese/control.sock"); - assert_eq!(resolve_with(Some(nested), Some(host), runtime).unwrap(), nested); - assert_eq!(resolve_with(None, Some(nested.as_os_str()), runtime).unwrap(), nested); + assert_eq!( + resolve_with(Some(nested), Some(host), runtime).unwrap(), + nested + ); + assert_eq!( + resolve_with(None, Some(nested.as_os_str()), runtime).unwrap(), + nested + ); assert_eq!(resolve_with(None, None, runtime).unwrap(), Path::new(host)); assert_eq!( resolve_with(None, Some(OsStr::new("")), runtime).unwrap(), diff --git a/crates/ferese-ipc/src/theme.rs b/crates/ferese-ipc/src/theme.rs index b501cd1..d683b3e 100644 --- a/crates/ferese-ipc/src/theme.rs +++ b/crates/ferese-ipc/src/theme.rs @@ -22,7 +22,10 @@ pub struct Snapshot { impl Snapshot { /// Decode older messages with the caller's catalog. Explicit empty catalogs stay empty. - pub fn decode(mut value: Value, fallback_families: impl FnOnce() -> Vec) -> Result { + pub fn decode( + mut value: Value, + fallback_families: impl FnOnce() -> Vec, + ) -> Result { if let Some(object) = value.as_object_mut() && !object.contains_key("families") { @@ -40,7 +43,10 @@ impl Snapshot { } impl Connection { - pub fn get(&mut self, fallback_families: impl FnOnce() -> Vec) -> Result { + pub fn get( + &mut self, + fallback_families: impl FnOnce() -> Vec, + ) -> Result { Snapshot::decode(self.call("theme-get", json!({}))?, fallback_families) } @@ -49,6 +55,9 @@ impl Connection { revision: u64, fallback_families: impl FnOnce() -> Vec, ) -> Result { - Snapshot::decode(self.wait("theme-watch", json!({"since": revision}))?, fallback_families) + Snapshot::decode( + self.wait("theme-watch", json!({"since": revision}))?, + fallback_families, + ) } } diff --git a/crates/ferese-ipc/src/xwayland.rs b/crates/ferese-ipc/src/xwayland.rs index d6d9c53..a8d9167 100644 --- a/crates/ferese-ipc/src/xwayland.rs +++ b/crates/ferese-ipc/src/xwayland.rs @@ -89,7 +89,10 @@ mod tests { ] { let encoded = serde_json::to_value(readiness).unwrap(); assert_eq!(encoded, json!(name)); - assert_eq!(serde_json::from_value::(encoded).unwrap(), readiness); + assert_eq!( + serde_json::from_value::(encoded).unwrap(), + readiness + ); } } diff --git a/crates/ferese-layout/src/lib.rs b/crates/ferese-layout/src/lib.rs index d001d31..e65bb67 100644 --- a/crates/ferese-layout/src/lib.rs +++ b/crates/ferese-layout/src/lib.rs @@ -4,7 +4,9 @@ use std::fmt; mod scrolling; -pub use scrolling::{Column, ColumnDisplay, ColumnWidth, ScrollingLayout, ViewportFocusStrategy, ViewportTarget}; +pub use scrolling::{ + Column, ColumnDisplay, ColumnWidth, ScrollingLayout, ViewportFocusStrategy, ViewportTarget, +}; pub const TAB_BAR_HEIGHT: f64 = 24.0; @@ -68,7 +70,12 @@ pub struct Rect { impl Rect { pub const fn new(x: f64, y: f64, width: f64, height: f64) -> Self { - Self { x, y, width, height } + Self { + x, + y, + width, + height, + } } fn center(self) -> (f64, f64) { @@ -184,8 +191,12 @@ impl LayoutTree { fn count(tree: &LayoutTree, node: NodeId) -> usize { match tree.nodes.get(&node) { Some(Node::Window(_)) => 1, - Some(Node::Split { first, second, .. }) => count(tree, *first) + count(tree, *second), - Some(Node::Stack { children, active }) => children.get(*active).map_or(0, |child| count(tree, *child)), + Some(Node::Split { first, second, .. }) => { + count(tree, *first) + count(tree, *second) + } + Some(Node::Stack { children, active }) => { + children.get(*active).map_or(0, |child| count(tree, *child)) + } None => 0, } } @@ -208,7 +219,11 @@ impl LayoutTree { if let Some(root) = self.root { self.collect_window_positions(root, bounds, &mut windows)?; } - windows.sort_by(|(_, left), (_, right)| left.x.total_cmp(&right.x).then_with(|| left.y.total_cmp(&right.y))); + windows.sort_by(|(_, left), (_, right)| { + left.x + .total_cmp(&right.x) + .then_with(|| left.y.total_cmp(&right.y)) + }); Ok(windows.into_iter().map(|(window, _)| window).collect()) } @@ -420,7 +435,11 @@ impl LayoutTree { } pub fn tab_groups(&self) -> Vec { - let mut groups: Vec<_> = self.nodes.keys().filter_map(|id| self.group_at(*id)).collect(); + let mut groups: Vec<_> = self + .nodes + .keys() + .filter_map(|id| self.group_at(*id)) + .collect(); groups.sort_by_key(|group| group.windows[0].0); groups } @@ -482,7 +501,11 @@ impl LayoutTree { Ok(geometry) } - pub fn geometry_with_gaps(&self, bounds: Rect, gaps: GapConfig) -> Result, LayoutError> { + pub fn geometry_with_gaps( + &self, + bounds: Rect, + gaps: GapConfig, + ) -> Result, LayoutError> { let mut geometry = self.geometry(bounds)?; apply_gaps(&mut geometry, bounds, gaps); @@ -547,7 +570,11 @@ impl LayoutTree { Ok(result) } - pub fn automatic_axis(&self, focused: Option, bounds: Rect) -> Result { + pub fn automatic_axis( + &self, + focused: Option, + bounds: Rect, + ) -> Result { let Some(root) = self.root else { return Ok(Axis::Horizontal); }; @@ -625,7 +652,12 @@ impl LayoutTree { .map(|(candidate, _, _, _)| candidate)) } - pub fn move_window(&mut self, window: WindowId, direction: Direction, bounds: Rect) -> Result { + pub fn move_window( + &mut self, + window: WindowId, + direction: Direction, + bounds: Rect, + ) -> Result { let Some(neighbor) = self.directional_neighbor(window, direction, bounds)? else { return Ok(false); }; @@ -649,7 +681,12 @@ impl LayoutTree { Ok(true) } - pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result { + pub fn resize_window( + &mut self, + window: WindowId, + direction: Direction, + amount: f64, + ) -> Result { let mut child = self .windows .get(&window) @@ -778,7 +815,9 @@ impl LayoutTree { match self.nodes.get(&node) { Some(Node::Window(_)) => Ok(node), Some(Node::Split { first, .. }) => self.first_window(*first), - Some(Node::Stack { children, active, .. }) => children + Some(Node::Stack { + children, active, .. + }) => children .get(*active) .copied() .ok_or(LayoutError::InvalidTree("stack active index is invalid")) @@ -787,7 +826,12 @@ impl LayoutTree { } } - fn replace_child(&mut self, parent: NodeId, old: NodeId, new: NodeId) -> Result<(), LayoutError> { + fn replace_child( + &mut self, + parent: NodeId, + old: NodeId, + new: NodeId, + ) -> Result<(), LayoutError> { match self.nodes.get_mut(&parent) { Some(Node::Split { first, .. }) if *first == old => *first = new, Some(Node::Split { second, .. }) if *second == old => *second = new, @@ -804,7 +848,12 @@ impl LayoutTree { Ok(()) } - fn collapse_parent(&mut self, parent: NodeId, leaf: NodeId, sibling: NodeId) -> Result<(), LayoutError> { + fn collapse_parent( + &mut self, + parent: NodeId, + leaf: NodeId, + sibling: NodeId, + ) -> Result<(), LayoutError> { let window = match self.nodes.get(&leaf) { Some(Node::Window(window)) => *window, _ => return Err(LayoutError::InvalidTree("removed leaf is not a window")), @@ -901,11 +950,21 @@ impl LayoutTree { } else { (available - second_required).max(0.0) }; - let adjusted_ratio = if available > 0.0 { first_extent / available } else { 0.5 }; + let adjusted_ratio = if available > 0.0 { + first_extent / available + } else { + 0.5 + }; let (first_bounds, second_bounds) = split_rect(bounds, *axis, adjusted_ratio); self.layout_node_constrained(*first, first_bounds, constraints, focused, geometry)?; - self.layout_node_constrained(*second, second_bounds, constraints, focused, geometry)?; + self.layout_node_constrained( + *second, + second_bounds, + constraints, + focused, + geometry, + )?; } Some(Node::Stack { children, active }) => { let child = children @@ -931,7 +990,10 @@ impl LayoutTree { Ok((constraint.min_width, constraint.min_height)) } Some(Node::Split { - axis, first, second, .. + axis, + first, + second, + .. }) => { let first = self.minimum_size(*first, constraints)?; let second = self.minimum_size(*second, constraints)?; @@ -962,7 +1024,9 @@ impl LayoutTree { Some(Node::Split { first, second, .. }) => { self.contains_window(*first, window) || self.contains_window(*second, window) } - Some(Node::Stack { children, .. }) => children.iter().any(|child| self.contains_window(*child, window)), + Some(Node::Stack { children, .. }) => children + .iter() + .any(|child| self.contains_window(*child, window)), None => false, } } @@ -987,9 +1051,15 @@ impl LayoutTree { } } Some(Node::Split { - ratio, first, second, .. + ratio, + first, + second, + .. }) => { - if first == second || !ratio.is_finite() || !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio) { + if first == second + || !ratio.is_finite() + || !(MIN_SPLIT_RATIO..=MAX_SPLIT_RATIO).contains(ratio) + { return Err(LayoutError::InvalidTree("split is invalid")); } @@ -1025,7 +1095,11 @@ fn valid_gap(gap: f64) -> f64 { } fn valid_resize_amount(amount: f64) -> f64 { - if amount.is_finite() { amount.abs() } else { 0.0 } + if amount.is_finite() { + amount.abs() + } else { + 0.0 + } } fn normalized_constraints(constraints: SizeConstraints) -> SizeConstraints { @@ -1119,7 +1193,12 @@ fn nearly_equal(left: f64, right: f64) -> bool { (left - right).abs() < f64::EPSILON * left.abs().max(right.abs()).max(1.0) } -fn intervals_overlap(first_start: f64, first_size: f64, second_start: f64, second_size: f64) -> bool { +fn intervals_overlap( + first_start: f64, + first_size: f64, + second_start: f64, + second_size: f64, +) -> bool { first_start < second_start + second_size && second_start < first_start + first_size } @@ -1128,14 +1207,24 @@ fn split_rect(bounds: Rect, axis: Axis, ratio: f64) -> (Rect, Rect) { Axis::Horizontal => { let boundary = bounds.x + bounds.width * ratio; let first = Rect::new(bounds.x, bounds.y, boundary - bounds.x, bounds.height); - let second = Rect::new(boundary, bounds.y, bounds.x + bounds.width - boundary, bounds.height); + let second = Rect::new( + boundary, + bounds.y, + bounds.x + bounds.width - boundary, + bounds.height, + ); (first, second) } Axis::Vertical => { let boundary = bounds.y + bounds.height * ratio; let first = Rect::new(bounds.x, bounds.y, bounds.width, boundary - bounds.y); - let second = Rect::new(bounds.x, boundary, bounds.width, bounds.y + bounds.height - boundary); + let second = Rect::new( + bounds.x, + boundary, + bounds.width, + bounds.y + bounds.height - boundary, + ); (first, second) } @@ -1149,32 +1238,43 @@ mod tests { #[test] fn insertion_splits_the_focused_leaf() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5) .unwrap(); assert!(tree.validate().is_ok()); - assert_eq!(tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0)).unwrap().len(), 3); + assert_eq!( + tree.geometry(Rect::new(0.0, 0.0, 100.0, 80.0)) + .unwrap() + .len(), + 3 + ); } #[test] fn removal_collapses_the_parent_split() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.remove(WindowId(1)).unwrap(); assert!(tree.validate().is_ok()); - assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(2)))); + assert_eq!( + tree.node(tree.root().unwrap()), + Some(&Node::Window(WindowId(2))) + ); } #[test] fn shared_split_boundary_is_exact() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 1.0 / 3.0) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 1.0 / 3.0) .unwrap(); @@ -1189,7 +1289,8 @@ mod tests { #[test] fn rejects_duplicate_and_unknown_windows() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); assert_eq!( tree.insert(WindowId(1), None, Axis::Horizontal, 0.5), @@ -1210,20 +1311,28 @@ mod tests { #[test] fn automatic_axis_follows_focused_tile_shape() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); let wide = Rect::new(0.0, 0.0, 1_920.0, 1_080.0); - assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Horizontal); + assert_eq!( + tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), + Axis::Horizontal + ); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); - assert_eq!(tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), Axis::Vertical); + assert_eq!( + tree.automatic_axis(Some(WindowId(1)), wide).unwrap(), + Axis::Vertical + ); } #[test] fn gaps_preserve_outer_and_shared_spacing() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); @@ -1241,7 +1350,8 @@ mod tests { #[test] fn smart_gaps_remove_outer_gap_for_one_window() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); let bounds = Rect::new(0.0, 0.0, 100.0, 80.0); let geometry = tree @@ -1260,7 +1370,8 @@ mod tests { #[test] fn default_gaps_keep_outer_spacing_for_one_window() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); let geometry = tree .geometry_with_gaps(Rect::new(0.0, 0.0, 100.0, 80.0), GapConfig::default()) @@ -1272,7 +1383,8 @@ mod tests { #[test] fn directional_neighbor_prefers_primary_distance() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(3), Some(WindowId(2)), Axis::Vertical, 0.5) @@ -1285,11 +1397,13 @@ mod tests { Some(WindowId(2)) ); assert_eq!( - tree.directional_neighbor(WindowId(2), Direction::Down, bounds).unwrap(), + tree.directional_neighbor(WindowId(2), Direction::Down, bounds) + .unwrap(), Some(WindowId(3)) ); assert_eq!( - tree.directional_neighbor(WindowId(1), Direction::Left, bounds).unwrap(), + tree.directional_neighbor(WindowId(1), Direction::Left, bounds) + .unwrap(), None ); } @@ -1297,12 +1411,16 @@ mod tests { #[test] fn directional_move_swaps_window_leaves() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); let bounds = Rect::new(0.0, 0.0, 100.0, 80.0); - assert!(tree.move_window(WindowId(1), Direction::Right, bounds).unwrap()); + assert!( + tree.move_window(WindowId(1), Direction::Right, bounds) + .unwrap() + ); let geometry = tree.geometry(bounds).unwrap(); assert_eq!(geometry[&WindowId(2)].x, 0.0); @@ -1313,11 +1431,16 @@ mod tests { #[test] fn directional_move_at_boundary_is_a_noop() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); assert!( !tree - .move_window(WindowId(1), Direction::Left, Rect::new(0.0, 0.0, 100.0, 80.0),) + .move_window( + WindowId(1), + Direction::Left, + Rect::new(0.0, 0.0, 100.0, 80.0), + ) .unwrap() ); } @@ -1325,14 +1448,18 @@ mod tests { #[test] fn directional_resize_changes_the_nearest_matching_split() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(3), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); let bounds = Rect::new(0.0, 0.0, 100.0, 100.0); - assert!(tree.resize_window(WindowId(1), Direction::Right, 0.1).unwrap()); + assert!( + tree.resize_window(WindowId(1), Direction::Right, 0.1) + .unwrap() + ); let geometry = tree.geometry(bounds).unwrap(); assert_eq!(geometry[&WindowId(1)].width, 30.0); @@ -1344,15 +1471,23 @@ mod tests { #[test] fn directional_resize_walks_to_an_ancestor_and_clamps() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.5) .unwrap(); let bounds = Rect::new(0.0, 0.0, 100.0, 100.0); - assert!(tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap()); - assert!(!tree.resize_window(WindowId(3), Direction::Right, 1.0).unwrap()); + assert!( + tree.resize_window(WindowId(3), Direction::Right, 1.0) + .unwrap() + ); + assert!( + !tree + .resize_window(WindowId(3), Direction::Right, 1.0) + .unwrap() + ); let geometry = tree.geometry(bounds).unwrap(); assert_eq!(geometry[&WindowId(2)].x, 95.0); @@ -1362,16 +1497,26 @@ mod tests { #[test] fn directional_resize_at_an_outer_edge_is_a_noop() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); - assert!(!tree.resize_window(WindowId(1), Direction::Left, 0.05).unwrap()); - assert!(!tree.resize_window(WindowId(1), Direction::Right, f64::NAN).unwrap()); + assert!( + !tree + .resize_window(WindowId(1), Direction::Left, 0.05) + .unwrap() + ); + assert!( + !tree + .resize_window(WindowId(1), Direction::Right, f64::NAN) + .unwrap() + ); } #[test] fn stacking_windows_shows_only_the_active_child() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.stack_window(WindowId(2), WindowId(1)).unwrap(); @@ -1388,14 +1533,19 @@ mod tests { let ids = [WindowId(30), WindowId(10), WindowId(20), WindowId(5)]; tree.insert(ids[0], None, Axis::Horizontal, 0.5).unwrap(); for pair in ids.windows(2) { - tree.insert(pair[1], Some(pair[0]), Axis::Horizontal, 0.5).unwrap(); + tree.insert(pair[1], Some(pair[0]), Axis::Horizontal, 0.5) + .unwrap(); tree.stack_window(pair[1], pair[0]).unwrap(); } let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); for active in ids { tree.activate_window(active).unwrap(); assert_eq!( - tree.geometry(bounds).unwrap().keys().copied().collect::>(), + tree.geometry(bounds) + .unwrap() + .keys() + .copied() + .collect::>(), vec![active] ); assert_eq!(tree.window_ids_in_reading_order(bounds).unwrap(), ids); @@ -1434,7 +1584,8 @@ mod tests { #[test] fn structural_reading_order_retains_xy_order_through_splits_and_stacks() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(40), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(40), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(10), Some(WindowId(40)), Axis::Vertical, 0.5) .unwrap(); tree.insert(WindowId(30), Some(WindowId(40)), Axis::Horizontal, 0.5) @@ -1461,8 +1612,13 @@ mod tests { fn tree_preference_follows_first_split_branch_and_active_stack_children() { let mut tree = LayoutTree::default(); assert_eq!(tree.preferred_window(), None); - assert!(tree.window_ids_in_reading_order(Rect::default()).unwrap().is_empty()); - tree.insert(WindowId(30), None, Axis::Horizontal, 0.5).unwrap(); + assert!( + tree.window_ids_in_reading_order(Rect::default()) + .unwrap() + .is_empty() + ); + tree.insert(WindowId(30), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(10), Some(WindowId(30)), Axis::Horizontal, 0.5) .unwrap(); tree.insert(WindowId(20), Some(WindowId(30)), Axis::Vertical, 0.5) @@ -1478,7 +1634,8 @@ mod tests { #[test] fn activating_a_hidden_stack_child_changes_visible_geometry() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.stack_window(WindowId(2), WindowId(1)).unwrap(); @@ -1493,7 +1650,8 @@ mod tests { #[test] fn removing_from_a_stack_collapses_or_preserves_cardinality() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); tree.stack_window(WindowId(2), WindowId(1)).unwrap(); @@ -1503,7 +1661,10 @@ mod tests { tree.remove(WindowId(3)).unwrap(); tree.remove(WindowId(2)).unwrap(); - assert_eq!(tree.node(tree.root().unwrap()), Some(&Node::Window(WindowId(1)))); + assert_eq!( + tree.node(tree.root().unwrap()), + Some(&Node::Window(WindowId(1))) + ); assert!(tree.validate().is_ok()); } @@ -1516,7 +1677,9 @@ mod tests { let bounds = Rect::new(0.0, 0.0, 1_920.0, 1_080.0); for _ in 0..2_000 { - random = random.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1); + random = random + .wrapping_mul(6_364_136_223_846_793_005) + .wrapping_add(1); let operation = random % 6; match operation { @@ -1530,7 +1693,8 @@ mod tests { Axis::Vertical }; - tree.insert(window, focused, axis, ratio_from_random(random)).unwrap(); + tree.insert(window, focused, axis, ratio_from_random(random)) + .unwrap(); windows.push(window); } 1 if !windows.is_empty() => { @@ -1557,7 +1721,8 @@ mod tests { } 5 if !windows.is_empty() => { let window = windows[random as usize % windows.len()]; - tree.resize_window(window, random_direction(random), 0.03).unwrap(); + tree.resize_window(window, random_direction(random), 0.03) + .unwrap(); } _ => {} } @@ -1569,7 +1734,8 @@ mod tests { #[test] fn constraints_adjust_split_boundaries_when_space_is_available() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.2).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.2) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.2) .unwrap(); let constraints = HashMap::from([ @@ -1609,7 +1775,8 @@ mod tests { #[test] fn focused_window_wins_an_impossible_minimum_constraint_conflict() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.5) .unwrap(); let constraints = HashMap::from([ @@ -1651,7 +1818,8 @@ mod tests { #[test] fn maximum_constraints_clip_without_overlapping_neighbors() { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); let constraints = HashMap::from([( WindowId(1), SizeConstraints { @@ -1669,7 +1837,10 @@ mod tests { ) .unwrap(); - assert_eq!(result.geometry[&WindowId(1)], Rect::new(4.0, 4.0, 60.0, 40.0)); + assert_eq!( + result.geometry[&WindowId(1)], + Rect::new(4.0, 4.0, 60.0, 40.0) + ); assert_eq!(result.warnings.len(), 2); } @@ -1697,7 +1868,8 @@ mod tab_tests { fn nested() -> LayoutTree { let mut tree = LayoutTree::default(); - tree.insert(WindowId(1), None, Axis::Horizontal, 0.5).unwrap(); + tree.insert(WindowId(1), None, Axis::Horizontal, 0.5) + .unwrap(); tree.insert(WindowId(2), Some(WindowId(1)), Axis::Horizontal, 0.6) .unwrap(); tree.insert(WindowId(3), Some(WindowId(1)), Axis::Vertical, 0.4) @@ -1727,7 +1899,10 @@ mod tab_tests { assert_eq!(tree.adjacent_tab(WindowId(3), true), Some(WindowId(1))); assert!(tree.leave_tab_group(WindowId(1)).unwrap()); assert_eq!(tree.geometry(bounds).unwrap()[&WindowId(2)], neighbor); - assert_eq!(tree.tab_group(WindowId(4)).unwrap().windows, [WindowId(3), WindowId(4)]); + assert_eq!( + tree.tab_group(WindowId(4)).unwrap().windows, + [WindowId(3), WindowId(4)] + ); assert!(tree.tab_group(WindowId(1)).is_none()); assert!(!tree.leave_tab_group(WindowId(1)).unwrap()); tree.remove(WindowId(4)).unwrap(); diff --git a/crates/ferese-layout/src/scrolling.rs b/crates/ferese-layout/src/scrolling.rs index 76d54a6..b5f66cc 100644 --- a/crates/ferese-layout/src/scrolling.rs +++ b/crates/ferese-layout/src/scrolling.rs @@ -1,8 +1,8 @@ use std::collections::{HashMap, HashSet}; use crate::{ - ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect, SizeConstraints, - WindowId, normalized_constraints, record_minimum_warnings, + ConstraintKind, ConstraintWarning, Direction, GapConfig, LayoutError, LayoutResult, Rect, + SizeConstraints, WindowId, normalized_constraints, record_minimum_warnings, }; const DEFAULT_WIDTH: f64 = 0.5; @@ -141,7 +141,9 @@ impl ViewportTarget { ..Default::default() }; match self.request { - ViewportRequest::Reveal(active) => layout.reveal_column(&positions, self.viewport_width, active), + ViewportRequest::Reveal(active) => { + layout.reveal_column(&positions, self.viewport_width, active) + } ViewportRequest::WidthCycle(active) => { layout.active_column = Some(active); layout.retarget_after_width_cycle(&positions, self.viewport_width); @@ -152,7 +154,9 @@ impl ViewportTarget { .clamp(0.0, (last + width - self.viewport_width).max(0.0)); } ViewportRequest::Center(active, maximum) => { - let width = maximum.map_or(positions[active].1, |maximum| positions[active].1.min(maximum)); + let width = maximum.map_or(positions[active].1, |maximum| { + positions[active].1.min(maximum) + }); layout.viewport_x = positions[active].0 + width / 2.0 - self.viewport_width / 2.0 } } @@ -167,7 +171,9 @@ impl ViewportTarget { .filter_map(|(window, source, target)| { self.columns .iter() - .position(|(windows, width)| windows.contains(window) && (*width - target).abs() < 0.001) + .position(|(windows, width)| { + windows.contains(window) && (*width - target).abs() < 0.001 + }) .map(|index| (index, *source)) }) .collect::>(); @@ -176,7 +182,8 @@ impl ViewportTarget { pub fn uses_previous_target(&self) -> bool { matches!(self.request, ViewportRequest::Slide(..)) - || (matches!(self.request, ViewportRequest::Reveal(..)) && self.strategy == ViewportFocusStrategy::Minimal) + || (matches!(self.request, ViewportRequest::Reveal(..)) + && self.strategy == ViewportFocusStrategy::Minimal) } } @@ -235,7 +242,10 @@ impl ScrollingLayout { } fn request_focus(&mut self, request: Option) { - self.pending_viewport = if matches!(self.pending_viewport, Some(PendingViewportRequest::WidthCycle(_))) { + self.pending_viewport = if matches!( + self.pending_viewport, + Some(PendingViewportRequest::WidthCycle(_)) + ) { Some(PendingViewportRequest::WidthCycle(request)) } else { request.map(PendingViewportRequest::Focus) @@ -243,7 +253,10 @@ impl ScrollingLayout { } fn request_reveal(&mut self, window: Option) { - if !matches!(self.pending_focus(), Some(PendingFocusRequest::Slide { .. })) { + if !matches!( + self.pending_focus(), + Some(PendingFocusRequest::Slide { .. }) + ) { self.request_focus(window.map(PendingFocusRequest::Reveal)); } } @@ -252,14 +265,22 @@ impl ScrollingLayout { self.default_display = display; } - pub fn set_column_display(&mut self, window: WindowId, display: ColumnDisplay) -> Result<(), LayoutError> { - let (index, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + pub fn set_column_display( + &mut self, + window: WindowId, + display: ColumnDisplay, + ) -> Result<(), LayoutError> { + let (index, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; self.columns[index].display = display; Ok(()) } pub fn toggle_column_tabbed(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (index, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (index, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; let column = &mut self.columns[index]; column.display = match column.display { ColumnDisplay::Stacked => ColumnDisplay::Tabbed, @@ -271,9 +292,11 @@ impl ScrollingLayout { pub fn tab_group(&self, window: WindowId) -> Option { let (index, _) = self.window_location(window)?; let column = &self.columns[index]; - (column.display == ColumnDisplay::Tabbed && column.windows.len() > 1).then(|| crate::TabGroup { - windows: column.windows.clone(), - active: column.windows[column.active], + (column.display == ColumnDisplay::Tabbed && column.windows.len() > 1).then(|| { + crate::TabGroup { + windows: column.windows.clone(), + active: column.windows[column.active], + } }) } @@ -381,10 +404,16 @@ impl ScrollingLayout { } pub fn window_ids(&self) -> impl Iterator + '_ { - self.columns.iter().flat_map(|column| column.windows.iter().copied()) + self.columns + .iter() + .flat_map(|column| column.windows.iter().copied()) } - pub fn insert(&mut self, window: WindowId, focused: Option) -> Result<(), LayoutError> { + pub fn insert( + &mut self, + window: WindowId, + focused: Option, + ) -> Result<(), LayoutError> { if self.contains(window) { return Err(LayoutError::DuplicateWindow(window)); } @@ -404,9 +433,12 @@ impl ScrollingLayout { } pub fn remove(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (column_index, window_index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (column_index, window_index) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; let column = &mut self.columns[column_index]; - let removed_active_column = self.active_column == Some(column_index) && column.windows.len() == 1; + let removed_active_column = + self.active_column == Some(column_index) && column.windows.len() == 1; column.windows.remove(window_index); column.heights.remove(window_index); @@ -440,7 +472,9 @@ impl ScrollingLayout { } pub fn focus(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (column, index) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; let changed_column = self.active_column != Some(column); self.active_column = Some(column); self.columns[column].active = index; @@ -453,7 +487,9 @@ impl ScrollingLayout { /// Hover changes the active window without requesting viewport movement. /// Keep any earlier explicit reveal attached to its original window. pub fn focus_without_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (column, index) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (column, index) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; self.active_column = Some(column); self.columns[column].active = index; Ok(()) @@ -461,7 +497,9 @@ impl ScrollingLayout { /// An explicit selection must reveal even a window already focused by hover. pub fn focus_and_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (column, row) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; let switching_tab = self.active_column == Some(column) && self.columns[column].display == ColumnDisplay::Tabbed && self.columns[column].active != row; @@ -473,7 +511,11 @@ impl ScrollingLayout { Ok(()) } - pub fn slide_focus_from(&mut self, previous: WindowId, focused: WindowId) -> Result<(), LayoutError> { + pub fn slide_focus_from( + &mut self, + previous: WindowId, + focused: WindowId, + ) -> Result<(), LayoutError> { self.window_location(previous) .ok_or(LayoutError::UnknownWindow(previous))?; if self @@ -493,11 +535,16 @@ impl ScrollingLayout { Ok(()) } - pub fn move_into_column(&mut self, window: WindowId, target: WindowId) -> Result<(), LayoutError> { + pub fn move_into_column( + &mut self, + window: WindowId, + target: WindowId, + ) -> Result<(), LayoutError> { if window == target { return Ok(()); } - self.window_location(target).ok_or(LayoutError::UnknownWindow(target))?; + self.window_location(target) + .ok_or(LayoutError::UnknownWindow(target))?; self.remove(window)?; let (target_column, target_index) = self .window_location(target) @@ -516,7 +563,9 @@ impl ScrollingLayout { } pub fn extract_to_column(&mut self, window: WindowId) -> Result<(), LayoutError> { - let (source, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (source, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; if self.columns[source].windows.len() == 1 { self.active_column = Some(source); return Ok(()); @@ -535,16 +584,32 @@ impl ScrollingLayout { Ok(()) } - pub fn set_column_width(&mut self, window: WindowId, width: ColumnWidth) -> Result<(), LayoutError> { - let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + pub fn set_column_width( + &mut self, + window: WindowId, + width: ColumnWidth, + ) -> Result<(), LayoutError> { + let (column, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; self.columns[column].width = normalized_width(width); self.request_reveal(self.active_window()); Ok(()) } - pub fn cycle_column_width(&mut self, window: WindowId, presets: &[ColumnWidth]) -> Result { - let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; - let presets = presets.iter().copied().map(normalized_width).collect::>(); + pub fn cycle_column_width( + &mut self, + window: WindowId, + presets: &[ColumnWidth], + ) -> Result { + let (column, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; + let presets = presets + .iter() + .copied() + .map(normalized_width) + .collect::>(); if presets.is_empty() { return Ok(false); } @@ -573,7 +638,8 @@ impl ScrollingLayout { constraints: &HashMap, ) -> Result { let before = self.viewport_x; - let result = self.resolve_geometry_with_constraints(bounds, gaps, constraints, Some(window))?; + let result = + self.resolve_geometry_with_constraints(bounds, gaps, constraints, Some(window))?; let rect = result .geometry .get(&window) @@ -629,22 +695,35 @@ impl ScrollingLayout { window: WindowId, direction: Direction, ) -> Result, LayoutError> { - let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + let (column, row) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; let neighbor = match direction { - Direction::Up => row.checked_sub(1).map(|row| self.columns[column].windows[row]), + Direction::Up => row + .checked_sub(1) + .map(|row| self.columns[column].windows[row]), Direction::Down => self.columns[column].windows.get(row + 1).copied(), Direction::Left => column.checked_sub(1).map(|column| { let column = &self.columns[column]; column.windows[column.active] }), - Direction::Right => self.columns.get(column + 1).map(|column| column.windows[column.active]), + Direction::Right => self + .columns + .get(column + 1) + .map(|column| column.windows[column.active]), }; Ok(neighbor) } - pub fn move_window(&mut self, window: WindowId, direction: Direction) -> Result { - let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + pub fn move_window( + &mut self, + window: WindowId, + direction: Direction, + ) -> Result { + let (column, row) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; match direction { Direction::Up if row > 0 => { @@ -658,7 +737,9 @@ impl ScrollingLayout { self.columns[column].active = row + 1; } Direction::Left if column > 0 => self.move_horizontally(window, column - 1)?, - Direction::Right if column + 1 < self.columns.len() => self.move_horizontally(window, column + 1)?, + Direction::Right if column + 1 < self.columns.len() => { + self.move_horizontally(window, column + 1)? + } _ => return Ok(false), } @@ -668,14 +749,26 @@ impl ScrollingLayout { /// Horizontal resize adjusts proportions. Fixed widths need a resolved /// viewport and start from the allocated column width, including minimums. - pub fn resize_window(&mut self, window: WindowId, direction: Direction, amount: f64) -> Result { - let (column, row) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; - let amount = if amount.is_finite() { amount.abs() } else { 0.0 }; + pub fn resize_window( + &mut self, + window: WindowId, + direction: Direction, + amount: f64, + ) -> Result { + let (column, row) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; + let amount = if amount.is_finite() { + amount.abs() + } else { + 0.0 + }; if amount == 0.0 { return Ok(false); } - if self.columns[column].display == ColumnDisplay::Tabbed && matches!(direction, Direction::Up | Direction::Down) + if self.columns[column].display == ColumnDisplay::Tabbed + && matches!(direction, Direction::Up | Direction::Down) { return Ok(false); } @@ -691,12 +784,18 @@ impl ScrollingLayout { sizing.proportion(self.allocated_column_width(window).unwrap_or(width)) } }; - let adjustment = if direction == Direction::Right { amount } else { -amount }; + let adjustment = if direction == Direction::Right { + amount + } else { + -amount + }; let resized = (current + adjustment).clamp(MIN_COLUMN_WIDTH, MAX_COLUMN_WIDTH); self.columns[column].width = ColumnWidth::Proportion(resized); resized != current } - Direction::Up if row > 0 => resize_rows(&mut self.columns[column], row, row - 1, amount), + Direction::Up if row > 0 => { + resize_rows(&mut self.columns[column], row, row - 1, amount) + } Direction::Down if row + 1 < self.columns[column].windows.len() => { resize_rows(&mut self.columns[column], row, row + 1, amount) } @@ -770,7 +869,8 @@ impl ScrollingLayout { .zip(&column_positions) .flat_map(|(column, (_, width))| column.windows.iter().map(move |id| (*id, *width))) .collect(); - let viewport_resized = self.last_column_sizing.map(|sizing| sizing.viewport_width) != Some(viewport_width); + let viewport_resized = + self.last_column_sizing.map(|sizing| sizing.viewport_width) != Some(viewport_width); self.last_column_sizing = Some(sizing); match self.pending_viewport.take() { Some(PendingViewportRequest::WidthCycle(_)) => { @@ -788,9 +888,10 @@ impl ScrollingLayout { from: previous, to: focused, })) => { - if let (Some((from, _)), Some((to, _))) = - (self.window_location(previous), self.window_location(focused)) - { + if let (Some((from, _)), Some((to, _))) = ( + self.window_location(previous), + self.window_location(focused), + ) { self.record_viewport_basis( &column_positions, inner, @@ -802,13 +903,16 @@ impl ScrollingLayout { .map(|(start, width)| start + width) .unwrap_or(0.0); let max_offset = (last_end - viewport_width).max(0.0); - self.viewport_x = - (self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset); + self.viewport_x = (self.viewport_x + column_positions[to].0 + - column_positions[from].0) + .clamp(0.0, max_offset); } } request => { let reveal = match request { - Some(PendingViewportRequest::Focus(PendingFocusRequest::Reveal(window))) => Some(window), + Some(PendingViewportRequest::Focus(PendingFocusRequest::Reveal(window))) => { + Some(window) + } None => None, _ => unreachable!(), } @@ -844,7 +948,8 @@ impl ScrollingLayout { // Keep the column allocation stable and honor compatible maximums. let mut common = SizeConstraints::default(); for id in &column.windows { - let c = normalized_constraints(constraints.get(id).copied().unwrap_or_default()); + let c = + normalized_constraints(constraints.get(id).copied().unwrap_or_default()); common.min_width = common.min_width.max(c.min_width); common.min_height = common.min_height.max(c.min_height); common.max_width = match (common.max_width, c.max_width) { @@ -872,11 +977,15 @@ impl ScrollingLayout { continue; } - let available_height = (viewport_height - inner * (column.windows.len().saturating_sub(1) as f64)).max(0.0); + let available_height = (viewport_height + - inner * (column.windows.len().saturating_sub(1) as f64)) + .max(0.0); let row_constraints = column .windows .iter() - .map(|window| normalized_constraints(constraints.get(window).copied().unwrap_or_default())) + .map(|window| { + normalized_constraints(constraints.get(window).copied().unwrap_or_default()) + }) .collect::>(); let row_heights = constrained_row_heights(column, available_height, &row_constraints); let mut y = bounds.y + outer; @@ -911,7 +1020,9 @@ impl ScrollingLayout { return; } - if start >= self.viewport_x - REVEAL_EPSILON && end <= self.viewport_x + viewport_width + REVEAL_EPSILON { + if start >= self.viewport_x - REVEAL_EPSILON + && end <= self.viewport_x + viewport_width + REVEAL_EPSILON + { return; } @@ -932,7 +1043,12 @@ impl ScrollingLayout { } } - fn paged_viewport(&self, positions: &[(f64, f64)], viewport_width: f64, active: Option) -> Option { + fn paged_viewport( + &self, + positions: &[(f64, f64)], + viewport_width: f64, + active: Option, + ) -> Option { if self.focus_strategy != ViewportFocusStrategy::Paged { return None; } @@ -983,8 +1099,14 @@ impl ScrollingLayout { } } - fn move_horizontally(&mut self, window: WindowId, destination: usize) -> Result<(), LayoutError> { - let (source, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; + fn move_horizontally( + &mut self, + window: WindowId, + destination: usize, + ) -> Result<(), LayoutError> { + let (source, _) = self + .window_location(window) + .ok_or(LayoutError::UnknownWindow(window))?; if self.columns[source].windows.len() == 1 { self.columns.swap(source, destination); self.active_column = Some(destination); @@ -992,7 +1114,11 @@ impl ScrollingLayout { return Ok(()); } - let insertion = if destination < source { source } else { source + 1 }; + let insertion = if destination < source { + source + } else { + source + 1 + }; let width = self.columns[source].width; self.remove(window)?; let insertion = insertion.min(self.columns.len()); @@ -1015,20 +1141,31 @@ impl ScrollingLayout { fn validate_scalars(&self) -> Result<(), LayoutError> { if !self.viewport_x.is_finite() { - return Err(LayoutError::InvalidTree("scrolling viewport offset is invalid")); + return Err(LayoutError::InvalidTree( + "scrolling viewport offset is invalid", + )); } if normalized_width(self.default_width) != self.default_width { - return Err(LayoutError::InvalidTree("scrolling default column width is invalid")); + return Err(LayoutError::InvalidTree( + "scrolling default column width is invalid", + )); } if self.columns.is_empty() { return if self.active_column.is_none() { Ok(()) } else { - Err(LayoutError::InvalidTree("empty scrolling layout has an active column")) + Err(LayoutError::InvalidTree( + "empty scrolling layout has an active column", + )) }; } - if self.active_column.is_none_or(|active| active >= self.columns.len()) { - return Err(LayoutError::InvalidTree("scrolling active column is invalid")); + if self + .active_column + .is_none_or(|active| active >= self.columns.len()) + { + return Err(LayoutError::InvalidTree( + "scrolling active column is invalid", + )); } Ok(()) } @@ -1039,15 +1176,24 @@ impl ScrollingLayout { let mut windows = HashSet::new(); for column in &self.columns { if normalized_width(column.width) != column.width { - return Err(LayoutError::InvalidTree("scrolling column width is invalid")); + return Err(LayoutError::InvalidTree( + "scrolling column width is invalid", + )); } if column.windows.is_empty() { return Err(LayoutError::InvalidTree("scrolling column is empty")); } - if column.active >= column.windows.len() || column.heights.len() != column.windows.len() { - return Err(LayoutError::InvalidTree("scrolling column metadata is invalid")); + if column.active >= column.windows.len() || column.heights.len() != column.windows.len() + { + return Err(LayoutError::InvalidTree( + "scrolling column metadata is invalid", + )); } - if !column.heights.iter().all(|height| height.is_finite() && *height > 0.0) { + if !column + .heights + .iter() + .all(|height| height.is_finite() && *height > 0.0) + { return Err(LayoutError::InvalidTree("scrolling height is invalid")); } for window in &column.windows { @@ -1078,9 +1224,16 @@ fn resize_rows(column: &mut Column, row: usize, neighbor: usize, amount: f64) -> true } -fn constrained_row_heights(column: &Column, available: f64, constraints: &[SizeConstraints]) -> Vec { +fn constrained_row_heights( + column: &Column, + available: f64, + constraints: &[SizeConstraints], +) -> Vec { let mut heights = vec![0.0; column.windows.len()]; - let minimum_total = constraints.iter().map(|constraint| constraint.min_height).sum::(); + let minimum_total = constraints + .iter() + .map(|constraint| constraint.min_height) + .sum::(); if minimum_total > available { // Match the tree layout's focus priority under impossible minima. // Other rows retain their relative weights in the remaining space. @@ -1129,7 +1282,9 @@ fn constrained_row_heights(column: &Column, available: f64, constraints: &[SizeC fn normalized_width(width: ColumnWidth) -> ColumnWidth { match width { - ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => ColumnWidth::Proportion(value), + ColumnWidth::Proportion(value) if value.is_finite() && value > 0.0 => { + ColumnWidth::Proportion(value) + } ColumnWidth::Fixed(value) if value.is_finite() && value > 0.0 => ColumnWidth::Fixed(value), ColumnWidth::Full => ColumnWidth::Full, _ => ColumnWidth::default(), @@ -1137,7 +1292,11 @@ fn normalized_width(width: ColumnWidth) -> ColumnWidth { } fn finite_nonnegative(value: f64) -> f64 { - if value.is_finite() { value.max(0.0) } else { 0.0 } + if value.is_finite() { + value.max(0.0) + } else { + 0.0 + } } fn apply_maximums( @@ -1299,7 +1458,11 @@ mod tests { fn changing_row_minima_restores_weight_intent_and_applies_maximums() { let mut layout = stacked_layout(&[0.25, 0.75]); let weights = layout.columns()[0].heights.clone(); - for (minimum, maximum, expected) in [(500.0, None, 500.0), (100.0, None, 200.0), (100.0, Some(150.0), 150.0)] { + for (minimum, maximum, expected) in [ + (500.0, None, 500.0), + (100.0, None, 200.0), + (100.0, Some(150.0), 150.0), + ] { let constraints = HashMap::from([( window(1), SizeConstraints { @@ -1349,14 +1512,27 @@ mod tests { layout.focus(window(focused)).unwrap(); let result = row_geometry(&mut layout, &constraints, 10.0); assert_eq!(result.geometry[&window(focused)].height, 500.0); - assert!((result.geometry.values().map(|rect| rect.height).sum::() - 780.0).abs() < 1e-6); + assert!( + (result + .geometry + .values() + .map(|rect| rect.height) + .sum::() + - 780.0) + .abs() + < 1e-6 + ); let warnings = result .warnings .iter() .filter(|warning| warning.kind == ConstraintKind::MinimumHeight) .collect::>(); assert_eq!(warnings.len(), 2); - assert!(warnings.iter().all(|warning| warning.window != window(focused))); + assert!( + warnings + .iter() + .all(|warning| warning.window != window(focused)) + ); assert_eq!(row_geometry(&mut layout, &constraints, 10.0), result); } let constraints = HashMap::from([( @@ -1424,15 +1600,30 @@ mod tests { layout.columns[0].normalize_heights(); } let before = layout.columns()[0].heights.clone(); - assert!(layout.resize_window(window(1), Direction::Down, 0.05).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Down, 0.05) + .unwrap() + ); assert!(layout.columns()[0].heights[0] > before[0]); assert!(layout.columns()[0].heights[1] < before[1]); - assert!(layout.resize_window(window(2), Direction::Up, 0.05).unwrap()); + assert!( + layout + .resize_window(window(2), Direction::Up, 0.05) + .unwrap() + ); // Repeated transfers must not eventually underflow the donor to zero. for _ in 0..1_100 { - layout.resize_window(window(1), Direction::Down, 1.0).unwrap(); + layout + .resize_window(window(1), Direction::Down, 1.0) + .unwrap(); } - assert!(layout.columns()[0].heights.iter().all(|weight| *weight > 0.0)); + assert!( + layout.columns()[0] + .heights + .iter() + .all(|weight| *weight > 0.0) + ); assert!((layout.columns()[0].heights.iter().sum::() - 1.0).abs() < 1e-12); assert!(layout.validate().is_ok()); } @@ -1452,7 +1643,12 @@ mod tests { layout.insert(b, Some(a)).unwrap(); layout.set_focus_strategy(ViewportFocusStrategy::Center); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(b)) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(b), + ) .unwrap(); layout.slide_focus_from(b, a).unwrap(); // Ordinary reveal requests must not replace a pending slide. @@ -1471,10 +1667,17 @@ mod tests { layout.focus_and_reveal(a).unwrap(); assert_eq!( layout.pending_viewport, - Some(PendingViewportRequest::WidthCycle(Some(PendingFocusRequest::Reveal(a)))) + Some(PendingViewportRequest::WidthCycle(Some( + PendingFocusRequest::Reveal(a) + ))) ); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a)) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(a), + ) .unwrap(); assert!(matches!( layout.viewport_target().unwrap().request, @@ -1493,11 +1696,24 @@ mod tests { layout.insert(b, Some(a)).unwrap(); layout.set_focus_strategy(ViewportFocusStrategy::Center); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(b)) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(b), + ) .unwrap(); layout.slide_focus_from(b, a).unwrap(); - assert!(layout.cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]).unwrap()); - assert!(!layout.cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]).unwrap()); + assert!( + layout + .cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]) + .unwrap() + ); + assert!( + !layout + .cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]) + .unwrap() + ); assert_eq!( layout.pending_viewport, Some(PendingViewportRequest::Focus(PendingFocusRequest::Slide { @@ -1506,7 +1722,12 @@ mod tests { })) ); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a)) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(a), + ) .unwrap(); assert!(matches!( layout.viewport_target().unwrap().request, @@ -1515,7 +1736,12 @@ mod tests { let viewport = layout.viewport_x(); let provenance = layout.viewport_target().cloned(); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a)) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(a), + ) .unwrap(); assert_eq!(layout.viewport_x(), viewport); assert_eq!(layout.viewport_target(), provenance.as_ref()); @@ -1540,7 +1766,9 @@ mod tests { ..Default::default() }, )]); - layout.center_window(id, bounds, gaps, &constraints).unwrap(); + layout + .center_window(id, bounds, gaps, &constraints) + .unwrap(); assert_eq!(layout.viewport_x(), -50.0); assert_eq!( layout @@ -1556,8 +1784,12 @@ mod tests { ..Default::default() }, )]); - layout.set_column_width(id, ColumnWidth::Fixed(150.0)).unwrap(); - layout.center_window(id, bounds, gaps, &constraints).unwrap(); + layout + .set_column_width(id, ColumnWidth::Fixed(150.0)) + .unwrap(); + layout + .center_window(id, bounds, gaps, &constraints) + .unwrap(); assert!(!layout.viewport_depends_on_width(id, 100.0, 150.0)); } @@ -1585,11 +1817,16 @@ mod tests { layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c)) .unwrap(); - layout.set_column_width(b, ColumnWidth::Fixed(150.0)).unwrap(); + layout + .set_column_width(b, ColumnWidth::Fixed(150.0)) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c)) .unwrap(); - assert!(layout.viewport_depends_on_width(b, 100.0, 150.0), "{strategy:?}"); + assert!( + layout.viewport_depends_on_width(b, 100.0, 150.0), + "{strategy:?}" + ); assert!( !layout.viewport_depends_on_width(b, 150.0, 150.0), "height-only size change" @@ -1607,7 +1844,9 @@ mod tests { !layout.viewport_depends_on_width(b, 100.0, 150.0), "independent reversal {strategy:?}" ); - layout.center_window(b, bounds, gaps, &HashMap::new()).unwrap(); + layout + .center_window(b, bounds, gaps, &HashMap::new()) + .unwrap(); assert!( layout.viewport_depends_on_width(b, 100.0, 150.0), "explicit centering {strategy:?}" @@ -1633,7 +1872,10 @@ mod tests { .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(c)) .unwrap(); assert_eq!(layout.allocated_column_width(b), Some(150.0)); - assert_eq!(result.geometry[&b].width, 150.0, "minimum wins inconsistent maximum"); + assert_eq!( + result.geometry[&b].width, 150.0, + "minimum wins inconsistent maximum" + ); assert!(layout.viewport_depends_on_width(b, 100.0, 150.0)); } @@ -1658,7 +1900,12 @@ mod tests { layout.insert(window(2), Some(window(1))).unwrap(); assert_eq!(layout.default_width(), ColumnWidth::Full); - assert!(layout.columns().iter().all(|column| column.width == ColumnWidth::Full)); + assert!( + layout + .columns() + .iter() + .all(|column| column.width == ColumnWidth::Full) + ); } #[test] @@ -1683,11 +1930,21 @@ mod tests { layout.focus_and_reveal(window(target)).unwrap(); let mut expected = layout.clone(); expected - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(target))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(target)), + ) .unwrap(); layout.focus_without_reveal(window(2)).unwrap(); layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(2)), + ) .unwrap(); assert_eq!(layout.active_window(), Some(window(2))); assert_eq!(layout.viewport_x(), expected.viewport_x(), "{strategy:?}"); @@ -1697,7 +1954,12 @@ mod tests { layout.remove(window(3)).unwrap(); assert!( layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(2)) + ) .is_ok() ); } @@ -1707,7 +1969,9 @@ mod tests { fn focus_reveal_scrolls_only_as_far_as_needed() { let mut layout = ScrollingLayout::default(); for id in 1..=4 { - layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap(); + layout + .insert(window(id), Some(window(id.saturating_sub(1)))) + .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); let gaps = GapConfig { @@ -1748,7 +2012,11 @@ mod tests { "viewport={viewport}, start={start}, width={width}" ); layout.reveal_column(&[(start, width)], 1000.0, 0); - assert_eq!(layout.viewport_x(), expected, "repeated reveal must stay still"); + assert_eq!( + layout.viewport_x(), + expected, + "repeated reveal must stay still" + ); } } @@ -1780,7 +2048,9 @@ mod tests { fn minimal_reveal_enters_oversized_columns_from_either_side() { let mut layout = ScrollingLayout::default(); for id in 1..=3 { - layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap(); + layout + .insert(window(id), Some(window(id.saturating_sub(1)))) + .unwrap(); } layout.columns[1].width = ColumnWidth::Fixed(1500.0); let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0); @@ -1792,7 +2062,12 @@ mod tests { for (focused, expected) in [(1, 0.0), (2, 500.0), (3, 1500.0), (2, 1000.0), (2, 1000.0)] { layout.focus_and_reveal(window(focused)).unwrap(); layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); assert_eq!(layout.viewport_x(), expected); } @@ -1839,24 +2114,43 @@ mod tests { ] { let mut layout = ScrollingLayout::with_default_width(width); for id in 1..=3 { - layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap(); + layout + .insert(window(id), Some(window(id.saturating_sub(1)))) + .unwrap(); } for (focused, offset) in [1, 3, 2, 1].into_iter().zip(offsets) { layout.focus_and_reveal(window(focused)).unwrap(); let result = layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); let rect = result.geometry[&window(focused)]; - assert_eq!(layout.viewport_x(), offset, "width={width:?}, focused={focused}"); + assert_eq!( + layout.viewport_x(), + offset, + "width={width:?}, focused={focused}" + ); if focused == 1 || width == ColumnWidth::Full { assert_eq!(rect.x, 113.5, "left edge includes the outer margin"); } if focused == 3 || width == ColumnWidth::Full { - assert_eq!(rect.x + rect.width, 1087.5, "right edge includes the outer margin"); + assert_eq!( + rect.x + rect.width, + 1087.5, + "right edge includes the outer margin" + ); } let next = result.geometry[&window(2)]; let first = result.geometry[&window(1)]; - assert_eq!(next.x - (first.x + first.width), 9.5, "inner gap stays intact"); + assert_eq!( + next.x - (first.x + first.width), + 9.5, + "inner gap stays intact" + ); } } } @@ -1883,8 +2177,14 @@ mod tests { assert_eq!(layout.viewport_x(), 0.0); assert_eq!(first.geometry, second.geometry); - assert_eq!(second.geometry[&window(1)], Rect::new(10.0, 10.0, 485.0, 780.0)); - assert_eq!(second.geometry[&window(2)], Rect::new(505.0, 10.0, 485.0, 780.0)); + assert_eq!( + second.geometry[&window(1)], + Rect::new(10.0, 10.0, 485.0, 780.0) + ); + assert_eq!( + second.geometry[&window(2)], + Rect::new(505.0, 10.0, 485.0, 780.0) + ); } #[test] @@ -1931,7 +2231,12 @@ mod tests { let (mut layout, bounds, gaps) = paged_layout(6); for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] { let geometry = layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap() .geometry; let first = (focused - 1) / 2 * 2 + 1; @@ -1949,7 +2254,9 @@ mod tests { fn minimal_swipe_focus_reveals_only_hidden_edges() { let mut layout = ScrollingLayout::default(); for id in 1..=3 { - layout.insert(window(id), Some(window(id.saturating_sub(1)))).unwrap(); + layout + .insert(window(id), Some(window(id.saturating_sub(1)))) + .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1000.0, 800.0); let gaps = GapConfig { @@ -1961,10 +2268,19 @@ mod tests { layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .unwrap(); - for (previous, focused, expected) in [(1, 2, 0.0), (2, 3, 500.0), (3, 2, 500.0), (2, 1, 0.0)] { - layout.slide_focus_from(window(previous), window(focused)).unwrap(); + for (previous, focused, expected) in + [(1, 2, 0.0), (2, 3, 500.0), (3, 2, 500.0), (2, 1, 0.0)] + { layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .slide_focus_from(window(previous), window(focused)) + .unwrap(); + layout + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); assert_eq!(layout.viewport_x(), expected); } @@ -1978,11 +2294,20 @@ mod tests { .unwrap(); assert_eq!(layout.viewport_x(), 0.0); - for (previous, focused, expected) in [(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)] { + for (previous, focused, expected) in + [(1, 2, 495.0), (2, 3, 990.0), (3, 2, 495.0), (2, 1, 0.0)] + { layout.focus_and_reveal(window(focused)).unwrap(); - layout.slide_focus_from(window(previous), window(focused)).unwrap(); + layout + .slide_focus_from(window(previous), window(focused)) + .unwrap(); let result = layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); assert_eq!(layout.viewport_x(), expected); @@ -2008,7 +2333,9 @@ mod tests { #[test] fn paged_focus_preserves_explicit_center_until_focus_changes() { let (mut layout, bounds, gaps) = paged_layout(4); - layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap(); + layout + .center_window(window(2), bounds, gaps, &HashMap::new()) + .unwrap(); let result = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .unwrap(); @@ -2062,7 +2389,12 @@ mod tests { let bounds = Rect::new(13.25, 48.5, 1000.5, 800.25); for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] { let result = layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); let first = (focused - 1) / 3 * 3 + 1; assert!((result.geometry[&window(first)].x - 23.25).abs() < 1e-6); @@ -2080,7 +2412,12 @@ mod tests { } for (focused, first) in [(1, 1), (2, 1), (3, 3), (4, 3), (5, 3), (2, 1)] { let result = layout - .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) + .resolve_geometry_with_constraints( + bounds, + gaps, + &HashMap::new(), + Some(window(focused)), + ) .unwrap(); assert_eq!(result.geometry[&window(first)].x, 10.0); let rect = result.geometry[&window(focused)]; @@ -2123,14 +2460,18 @@ mod tests { }; for id in 1..=3 { - layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap(); + layout + .insert(window(id), (id > 1).then(|| window(id - 1))) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id))) .unwrap(); } let before = layout.viewport_x(); - layout.resize_window(window(3), Direction::Right, 0.5).unwrap(); + layout + .resize_window(window(3), Direction::Right, 0.5) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .unwrap(); @@ -2276,10 +2617,18 @@ mod tests { layout.insert(window(2), Some(window(1))).unwrap(); layout.move_into_column(window(2), window(1)).unwrap(); - assert!(layout.resize_window(window(1), Direction::Right, 0.1).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Right, 0.1) + .unwrap() + ); assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.6)); - assert!(layout.resize_window(window(1), Direction::Down, 0.1).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Down, 0.1) + .unwrap() + ); assert_eq!(layout.columns()[0].heights, vec![0.6, 0.4]); } @@ -2287,7 +2636,11 @@ mod tests { fn proportional_resize_keeps_the_existing_proportion() { let mut layout = ScrollingLayout::default(); layout.insert(window(1), None).unwrap(); - assert!(layout.resize_window(window(1), Direction::Right, 0.05).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap() + ); assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.55)); } @@ -2295,7 +2648,11 @@ mod tests { fn full_width_resize_left_starts_at_one() { let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Full); layout.insert(window(1), None).unwrap(); - assert!(layout.resize_window(window(1), Direction::Left, 0.05).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Left, 0.05) + .unwrap() + ); assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(0.95)); } @@ -2307,7 +2664,11 @@ mod tests { let before = layout .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None) .unwrap(); - assert!(layout.resize_window(window(1), Direction::Right, 0.05).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap() + ); let after = layout .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None) .unwrap(); @@ -2325,21 +2686,34 @@ mod tests { (800.0, 48.0, 32.0, false), (800.0, 12.0, 32.0, true), ] { - layout.set_column_width(window(1), ColumnWidth::Fixed(400.0)).unwrap(); + layout + .set_column_width(window(1), ColumnWidth::Fixed(400.0)) + .unwrap(); let bounds = Rect::new(0.0, 0.0, width, 800.0); - let gaps = GapConfig { inner, outer, smart }; + let gaps = GapConfig { + inner, + outer, + smart, + }; let before = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), None) .unwrap(); assert_eq!(before.geometry[&window(1)].width, 400.0); let viewport = width - if smart { 0.0 } else { 2.0 * outer }; let expected = (400.0 + inner) / (viewport + inner) + 0.05; - assert!(layout.resize_window(window(1), Direction::Right, 0.05).unwrap()); + assert!( + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap() + ); assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(expected)); let after = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), None) .unwrap(); - assert!((after.geometry[&window(1)].width - (400.0 + (viewport + inner) * 0.05)).abs() < 1e-6); + assert!( + (after.geometry[&window(1)].width - (400.0 + (viewport + inner) * 0.05)).abs() + < 1e-6 + ); } } @@ -2364,12 +2738,16 @@ mod tests { .resolve_geometry_with_constraints(bounds, gaps, &constraints, None) .unwrap(); assert_eq!(before.geometry[&window(1)].width, 600.0); - layout.resize_window(window(1), Direction::Right, 0.05).unwrap(); + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap(); let after = layout .resolve_geometry_with_constraints(bounds, gaps, &constraints, None) .unwrap(); assert!((after.geometry[&window(1)].width - 649.5).abs() < 1e-6); - layout.resize_window(window(1), Direction::Left, 0.1).unwrap(); + layout + .resize_window(window(1), Direction::Left, 0.1) + .unwrap(); let shrunk = layout .resolve_geometry_with_constraints(bounds, gaps, &constraints, None) .unwrap(); @@ -2390,13 +2768,19 @@ mod tests { .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), None) .unwrap(); for step in 1..=4 { - layout.resize_window(window(1), Direction::Right, 0.05).unwrap(); + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap(); let result = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), None) .unwrap(); - assert!((result.geometry[&window(1)].width - (400.0 + 49.5 * f64::from(step))).abs() < 1e-6); + assert!( + (result.geometry[&window(1)].width - (400.0 + 49.5 * f64::from(step))).abs() < 1e-6 + ); } - layout.resize_window(window(1), Direction::Left, 0.05).unwrap(); + layout + .resize_window(window(1), Direction::Left, 0.05) + .unwrap(); let result = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), None) .unwrap(); @@ -2407,7 +2791,11 @@ mod tests { fn fixed_resize_without_resolved_geometry_leaves_width_unchanged() { let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(400.0)); layout.insert(window(1), None).unwrap(); - assert!(!layout.resize_window(window(1), Direction::Right, 0.05).unwrap()); + assert!( + !layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap() + ); assert_eq!(layout.columns()[0].width, ColumnWidth::Fixed(400.0)); } @@ -2415,7 +2803,9 @@ mod tests { fn width_cycle_after_fixed_resize_preserves_precise_viewport_dependencies() { let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(400.0)); for id in 1..=3 { - layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap(); + layout + .insert(window(id), (id > 1).then(|| window(id - 1))) + .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); let gaps = GapConfig { @@ -2428,7 +2818,9 @@ mod tests { .unwrap(); let target = layout.viewport_target().cloned(); let viewport = layout.viewport_x(); - layout.resize_window(window(1), Direction::Right, 0.05).unwrap(); + layout + .resize_window(window(1), Direction::Right, 0.05) + .unwrap(); layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .unwrap(); @@ -2471,7 +2863,10 @@ mod tests { ]; assert!(layout.cycle_column_width(window(1), &presets).unwrap()); - assert_eq!(layout.columns()[0].width, ColumnWidth::Proportion(2.0 / 3.0)); + assert_eq!( + layout.columns()[0].width, + ColumnWidth::Proportion(2.0 / 3.0) + ); assert!(layout.cycle_column_width(window(1), &presets).unwrap()); assert_eq!(layout.columns()[0].width, ColumnWidth::Full); assert!(layout.cycle_column_width(window(1), &presets).unwrap()); @@ -2556,7 +2951,10 @@ mod tests { let mut layout = ScrollingLayout::default(); for id in 1_u64..=4 { layout - .insert(window(id), id.checked_sub(1).filter(|id| *id > 0).map(window)) + .insert( + window(id), + id.checked_sub(1).filter(|id| *id > 0).map(window), + ) .unwrap(); } let bounds = Rect::new(0.0, 0.0, 1_000.0, 800.0); @@ -2569,7 +2967,11 @@ mod tests { .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .unwrap(); - assert!(layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap()); + assert!( + layout + .center_window(window(2), bounds, gaps, &HashMap::new()) + .unwrap() + ); let result = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .unwrap(); @@ -2608,7 +3010,9 @@ mod tests { if first == second { second = (second + 1) % windows.len(); } - layout.move_into_column(windows[first], windows[second]).unwrap(); + layout + .move_into_column(windows[first], windows[second]) + .unwrap(); } 3 if !windows.is_empty() => { let index = random as usize % windows.len(); @@ -2622,7 +3026,9 @@ mod tests { 5 if !windows.is_empty() => { let index = random as usize % windows.len(); let direction = random_direction(random.rotate_left(23)); - layout.resize_window(windows[index], direction, 0.05).unwrap(); + layout + .resize_window(windows[index], direction, 0.05) + .unwrap(); } 6 if !windows.is_empty() => { let index = random as usize % windows.len(); @@ -2632,7 +3038,10 @@ mod tests { } layout.validate().unwrap(); - assert_eq!(layout.window_ids().collect::>().len(), windows.len()); + assert_eq!( + layout.window_ids().collect::>().len(), + windows.len() + ); } } @@ -2669,7 +3078,9 @@ mod tabbed_tests { ] { let mut layout = fixture(); layout.set_focus_strategy(strategy); - layout.resize_window(WindowId(3), Direction::Up, 0.1).unwrap(); + layout + .resize_window(WindowId(3), Direction::Up, 0.1) + .unwrap(); let bounds = Rect::new(0., 0., 1000., 800.); let gaps = GapConfig::default(); let before = layout @@ -2695,7 +3106,11 @@ mod tabbed_tests { assert_eq!(layout.viewport_x(), viewport); assert_eq!(layout.allocated_widths, widths); } - assert!(!layout.resize_window(WindowId(3), Direction::Up, 0.1).unwrap()); + assert!( + !layout + .resize_window(WindowId(3), Direction::Up, 0.1) + .unwrap() + ); layout.toggle_column_tabbed(WindowId(4)).unwrap(); let after = layout .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(4))) @@ -2712,7 +3127,9 @@ mod tabbed_tests { layout.set_default_display(ColumnDisplay::Tabbed); layout.toggle_column_tabbed(WindowId(4)).unwrap(); assert_eq!( - layout.directional_neighbor(WindowId(3), Direction::Up).unwrap(), + layout + .directional_neighbor(WindowId(3), Direction::Up) + .unwrap(), Some(WindowId(2)) ); assert_eq!(layout.adjacent_tab(WindowId(4), false), Some(WindowId(2))); @@ -2722,7 +3139,12 @@ mod tabbed_tests { assert_eq!(layout.columns[0].display, ColumnDisplay::Tabbed); layout.extract_to_column(WindowId(1)).unwrap(); assert_eq!(layout.columns.len(), 2); - assert!(layout.columns.iter().all(|c| c.display == ColumnDisplay::Tabbed)); + assert!( + layout + .columns + .iter() + .all(|c| c.display == ColumnDisplay::Tabbed) + ); assert!(layout.tab_group(WindowId(1)).is_none()); layout.remove(WindowId(4)).unwrap(); layout.remove(WindowId(3)).unwrap(); @@ -2739,14 +3161,24 @@ mod tabbed_tests { layout.toggle_column_tabbed(WindowId(4)).unwrap(); let bounds = Rect::new(0., 0., 1000., 800.); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4))) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(WindowId(4)), + ) .unwrap(); layout.viewport_x += 33.0; let position = layout.viewport_x; layout.focus_and_reveal(WindowId(3)).unwrap(); layout.slide_focus_from(WindowId(4), WindowId(3)).unwrap(); layout - .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(3))) + .resolve_geometry_with_constraints( + bounds, + GapConfig::default(), + &HashMap::new(), + Some(WindowId(3)), + ) .unwrap(); assert_eq!(layout.viewport_x, position); } diff --git a/crates/ferese-panel/src/adaptive.rs b/crates/ferese-panel/src/adaptive.rs index 3d7b075..15a86e9 100644 --- a/crates/ferese-panel/src/adaptive.rs +++ b/crates/ferese-panel/src/adaptive.rs @@ -88,7 +88,8 @@ impl Adaptive { } pub fn deadline(&self) -> Option { - self.pending.map(|(_, since)| since.saturating_add(HYSTERESIS)) + self.pending + .map(|(_, since)| since.saturating_add(HYSTERESIS)) } } diff --git a/crates/ferese-panel/src/morph.rs b/crates/ferese-panel/src/morph.rs index 98abd7c..9573e81 100644 --- a/crates/ferese-panel/src/morph.rs +++ b/crates/ferese-panel/src/morph.rs @@ -180,7 +180,9 @@ impl Hub { pins.iter() .any(|value| !value.is_finite() || !(0. ..=f64::from(f32::MAX)).contains(value)) }) { - return Err("hub radius pins must be finite, nonnegative pixel values representable as f32"); + return Err( + "hub radius pins must be finite, nonnegative pixel values representable as f32", + ); } self.pins = pins; let mut target = self.motion.target(); @@ -221,7 +223,10 @@ impl Hub { } pub fn retarget(&mut self, mode: Mode, available: [f64; 2]) -> Result<(), &'static str> { - if available.iter().any(|value| !value.is_finite() || *value <= 0.) { + if available + .iter() + .any(|value| !value.is_finite() || *value <= 0.) + { return Err("hub requires a finite positive available area"); } let [width, height] = self.profile.size(mode, self.height); @@ -302,12 +307,14 @@ impl Hub { pub fn sample(&self) -> Sample { let fade = |elapsed: Duration| (elapsed.as_secs_f32() / FADE.as_secs_f32()).clamp(0., 1.); - let opacity = - self.initial_opacity + (1. - self.initial_opacity) * fade(self.elapsed.saturating_sub(CONTENT_DELAY)); + let opacity = self.initial_opacity + + (1. - self.initial_opacity) * fade(self.elapsed.saturating_sub(CONTENT_DELAY)); let mut geometry = self.motion.current(); geometry.bounds.width = geometry.bounds.width.clamp(0., self.available[0]); geometry.bounds.height = geometry.bounds.height.clamp(0., self.available[1]); - geometry.radii = geometry.radii.map(|radius| radius.clamp(0., f64::from(f32::MAX))); + geometry.radii = geometry + .radii + .map(|radius| radius.clamp(0., f64::from(f32::MAX))); Sample { geometry, previous: self.previous, @@ -352,7 +359,8 @@ mod tests { #[test] fn measured_height_preserves_fade_and_motion_and_caps_long_content() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]) + .unwrap(); hub.advance(Duration::from_millis(180), false); let before = hub.sample(); hub.fit_content_height(148.).unwrap(); @@ -363,7 +371,10 @@ mod tests { assert_eq!(hub.sample().geometry.bounds.height, 148.); assert_eq!(hub.sample().opacity, 1.); hub.fit_content_height(900.).unwrap(); - assert_eq!(hub.target_size(), [340., Profile::default().controls_height]); + assert_eq!( + hub.target_size(), + [340., Profile::default().controls_height] + ); hub.retarget(Mode::Idle, [800., 600.]).unwrap(); hub.fit_content_height(100.).unwrap(); assert_eq!(hub.target_size(), [150., 34.]); @@ -372,7 +383,8 @@ mod tests { #[test] fn a_content_swap_fades_without_restarting_the_geometry() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Controls), [800., 600.]) + .unwrap(); hub.advance(Duration::from_secs(2), false); hub.advance(Duration::from_secs(1), false); let geometry = hub.sample().geometry; @@ -394,7 +406,8 @@ mod tests { fn content_swap_uses_a_fixed_delay_instead_of_waiting_for_spring_settlement() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); assert!(!hub.active()); - hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]) + .unwrap(); assert_eq!(hub.target_size(), [340., 230.]); hub.advance(Duration::from_millis(120), false); assert_eq!(hub.sample().opacity, 0.); @@ -403,7 +416,10 @@ mod tests { assert!((hub.sample().opacity - 0.5).abs() < 0.001); hub.advance(Duration::from_millis(60), false); assert_eq!(hub.sample().opacity, 1.); - assert!(hub.active(), "content finishes while the shape is still settling"); + assert!( + hub.active(), + "content finishes while the shape is still settling" + ); for _ in 0..50 { hub.advance(Duration::from_millis(16), false); } @@ -415,9 +431,11 @@ mod tests { #[test] fn an_interrupted_content_delay_never_reveals_the_cancelled_panel() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Media), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Media), [800., 600.]) + .unwrap(); hub.advance(Duration::from_millis(80), false); - hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]) + .unwrap(); for _ in 0..130 { hub.advance(Duration::from_millis(8), false); let sample = hub.sample(); @@ -431,7 +449,8 @@ mod tests { #[test] fn spring_overshoots_then_settles_within_its_reserved_surface() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Calendar), [800., 600.]) + .unwrap(); let allocation = hub.allocation_size(); let mut peak = 340_f64; for _ in 0..200 { @@ -450,7 +469,8 @@ mod tests { #[test] fn overshoot_is_clipped_to_small_outputs_and_reduced_motion_removes_all_fades() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]) + .unwrap(); for _ in 0..60 { hub.advance(Duration::from_millis(8), false); let bounds = hub.sample().geometry.bounds; @@ -467,7 +487,8 @@ mod tests { #[test] fn interrupted_expansion_keeps_the_presented_shape() { let mut hub = Hub::new(Profile::default(), 34.).unwrap(); - hub.retarget(Mode::Expanded(Content::Media), [800., 600.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Media), [800., 600.]) + .unwrap(); hub.advance(Duration::from_millis(40), false); let before = hub.sample().geometry; hub.retarget(Mode::Idle, [800., 600.]).unwrap(); @@ -482,7 +503,8 @@ mod tests { #[test] fn narrow_outputs_and_reduced_motion_keep_the_hub_bounded() { let mut hub = Hub::new(Profile::default(), 30.).unwrap(); - hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]).unwrap(); + hub.retarget(Mode::Expanded(Content::Calendar), [240., 180.]) + .unwrap(); hub.advance(Duration::ZERO, true); assert_eq!(hub.sample().geometry.bounds, Rect::new(0., 0., 240., 180.)); assert_eq!(hub.sample().opacity, 1.); diff --git a/crates/ferese-panel/src/motion.rs b/crates/ferese-panel/src/motion.rs index d011198..b0c4911 100644 --- a/crates/ferese-panel/src/motion.rs +++ b/crates/ferese-panel/src/motion.rs @@ -31,7 +31,12 @@ impl Geometry { fn values(self) -> [f64; COUNT] { let mut values = [0.; COUNT]; - values[..4].copy_from_slice(&[self.bounds.x, self.bounds.y, self.bounds.width, self.bounds.height]); + values[..4].copy_from_slice(&[ + self.bounds.x, + self.bounds.y, + self.bounds.width, + self.bounds.height, + ]); values[4..8].copy_from_slice(&self.radii); values[8..12].copy_from_slice(&self.margin); values[12..16].copy_from_slice(&self.thickness); @@ -53,7 +58,9 @@ impl Geometry { fn validate(self) -> Result { let values = self.values(); - if values.iter().any(|value| !value.is_finite()) || values[2..].iter().any(|value| *value < 0.) { + if values.iter().any(|value| !value.is_finite()) + || values[2..].iter().any(|value| *value < 0.) + { Err("panel animation geometry must be finite with nonnegative extents") } else { Ok(self) @@ -82,7 +89,10 @@ impl Motion { ) } - pub(crate) fn with_spring(geometry: Geometry, spring: SpringConfig) -> Result { + pub(crate) fn with_spring( + geometry: Geometry, + spring: SpringConfig, + ) -> Result { Ok(Self { values: geometry.validate()?.values().map(AnimatedValue::new), spring, @@ -106,14 +116,16 @@ impl Motion { // Extrema alternate; later peaks decay, so the first two suffice. for phase in [first, first + std::f64::consts::PI] { let t = phase / frequency; - let position = - value.target + (-decay * t).exp() * (displacement * phase.cos() + sine * phase.sin()); + let position = value.target + + (-decay * t).exp() * (displacement * phase.cos() + sine * phase.sin()); maximum = maximum.max(position); } } else { // Total spring energy bounds displacement for any nonnegative damping. maximum = maximum.max( - value.target + (displacement * displacement + value.velocity * value.velocity / omega2).sqrt(), + value.target + + (displacement * displacement + value.velocity * value.velocity / omega2) + .sqrt(), ); } maximum @@ -229,7 +241,11 @@ mod tests { let before = motion.current(); let velocity = motion.values.map(|value| value.velocity); motion - .retarget(if turn % 2 == 0 { expanded() } else { collapsed() }) + .retarget(if turn % 2 == 0 { + expanded() + } else { + collapsed() + }) .unwrap(); assert_eq!(motion.current(), before); assert_eq!(motion.values.map(|value| value.velocity), velocity); diff --git a/crates/ferese-panel/src/outline.rs b/crates/ferese-panel/src/outline.rs index ac4d1e6..5f0ef6a 100644 --- a/crates/ferese-panel/src/outline.rs +++ b/crates/ferese-panel/src/outline.rs @@ -4,10 +4,14 @@ use std::f64::consts::{FRAC_PI_2, PI}; pub type Contour = Vec<[f64; 2]>; fn arc(points: &mut Contour, center: [f64; 2], radius: f64, start: f64, tolerance: f64) { - let segments = ((FRAC_PI_2 / (8. * tolerance / radius.max(tolerance)).sqrt()).ceil() as usize).clamp(1, 128); + let segments = ((FRAC_PI_2 / (8. * tolerance / radius.max(tolerance)).sqrt()).ceil() as usize) + .clamp(1, 128); for i in 0..=segments { let angle = start + FRAC_PI_2 * i as f64 / segments as f64; - points.push([center[0] + radius * angle.cos(), center[1] + radius * angle.sin()]); + points.push([ + center[0] + radius * angle.cos(), + center[1] + radius * angle.sin(), + ]); } } @@ -66,20 +70,32 @@ pub fn contours(scene: &Scene, tolerance: f64) -> Result, &'static .collect(); if scene.fusion > 0. { for zone in 1..=3 { - let shapes: Vec<_> = scene.paint.iter().filter(|p| p.blend_zone == zone).copied().collect(); + let shapes: Vec<_> = scene + .paint + .iter() + .filter(|p| p.blend_zone == zone) + .copied() + .collect(); if shapes.is_empty() { continue; } // Fused rows share their vertical baseline. Each component is // vertically convex; solve its upper and lower boundary, rather // than sampling every pixel of a full-output bitmap. - let top = shapes.iter().map(|p| p.bounds.y).fold(f64::INFINITY, f64::min); + let top = shapes + .iter() + .map(|p| p.bounds.y) + .fold(f64::INFINITY, f64::min); let bottom = shapes .iter() .map(|p| p.bounds.y + p.bounds.height) .fold(f64::NEG_INFINITY, f64::max); let mid = (top + bottom) * 0.5; - let left = shapes.iter().map(|p| p.bounds.x).fold(f64::INFINITY, f64::min) - scene.fusion; + let left = shapes + .iter() + .map(|p| p.bounds.x) + .fold(f64::INFINITY, f64::min) + - scene.fusion; let right = shapes .iter() .map(|p| p.bounds.x + p.bounds.width) @@ -135,7 +151,11 @@ pub fn contours(scene: &Scene, tolerance: f64) -> Result, &'static /// Open border paths for screen-attached notches. Split shared straight edges /// before cancelling them, so the body and its fillets have one continuous rim. -pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result, &'static str> { +pub fn notch_border( + scene: &Scene, + tolerance: f64, + edge: crate::Edge, +) -> Result, &'static str> { let contours = contours(scene, tolerance)?; let vertices: Vec<_> = contours.iter().flatten().copied().collect(); let screen_edge = vertices.iter().map(|p| p[1]).fold( @@ -144,13 +164,23 @@ pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result< } else { f64::NEG_INFINITY }, - if edge == crate::Edge::Top { f64::min } else { f64::max }, + if edge == crate::Edge::Top { + f64::min + } else { + f64::max + }, ); let close = |a: [f64; 2], b: [f64; 2]| (a[0] - b[0]).abs() < 1e-6 && (a[1] - b[1]).abs() < 1e-6; let mut segments: Vec<([f64; 2], [f64; 2])> = Vec::new(); for contour in &contours { - for (&a, &b) in contour.iter().zip(contour.iter().cycle().skip(1)).take(contour.len()) { - if close(a, b) || ((a[1] - screen_edge).abs() < 1e-6 && (b[1] - screen_edge).abs() < 1e-6) { + for (&a, &b) in contour + .iter() + .zip(contour.iter().cycle().skip(1)) + .take(contour.len()) + { + if close(a, b) + || ((a[1] - screen_edge).abs() < 1e-6 && (b[1] - screen_edge).abs() < 1e-6) + { continue; } let delta = [b[0] - a[0], b[1] - a[1]]; @@ -181,7 +211,10 @@ pub fn notch_border(scene: &Scene, tolerance: f64, edge: crate::Edge) -> Result< let mut paths = Vec::new(); while let Some((a, b)) = segments.pop() { let mut path = vec![a, b]; - while let Some(index) = segments.iter().position(|&(a, _)| close(a, *path.last().unwrap())) { + while let Some(index) = segments + .iter() + .position(|&(a, _)| close(a, *path.last().unwrap())) + { path.push(segments.swap_remove(index).1); } while let Some(index) = segments.iter().position(|&(_, b)| close(b, path[0])) { @@ -202,7 +235,11 @@ mod tests { fn winding(contours: &[Contour], point: [f64; 2]) -> i32 { let mut winding = 0; for points in contours { - for (a, b) in points.iter().zip(points.iter().cycle().skip(1)).take(points.len()) { + for (a, b) in points + .iter() + .zip(points.iter().cycle().skip(1)) + .take(points.len()) + { let cross = (b[0] - a[0]) * (point[1] - a[1]) - (point[0] - a[0]) * (b[1] - a[1]); if a[1] <= point[1] && b[1] > point[1] && cross > 0. { winding += 1; @@ -241,14 +278,22 @@ mod tests { let screen_y = if edge == crate::Edge::Top { 0. } else { 44. }; assert!((path.first().unwrap()[1] - screen_y).abs() < 1e-6); assert!((path.last().unwrap()[1] - screen_y).abs() < 1e-6); - let body = scene.paint.iter().find(|p| p.operation == 0).unwrap().bounds; + let body = scene + .paint + .iter() + .find(|p| p.operation == 0) + .unwrap() + .bounds; assert!(path.iter().any(|p| p[0] < body.x)); assert!(path.iter().any(|p| p[0] > body.x + body.width)); for segment in path.windows(2) { let [a, b] = [segment[0], segment[1]]; assert!((a[1] - screen_y).abs() > 1e-6 || (b[1] - screen_y).abs() > 1e-6); let mid = [(a[0] + b[0]) / 2., (a[1] + b[1]) / 2.]; - assert!(scene.distance(mid).abs() < 0.12 / scale, "off outline: {mid:?}"); + assert!( + scene.distance(mid).abs() < 0.12 / scale, + "off outline: {mid:?}" + ); // A shared body/fillet seam has fill on both sides, unlike a visible rim. let delta = [b[0] - a[0], b[1] - a[1]]; let length = delta[0].hypot(delta[1]); @@ -266,7 +311,9 @@ mod tests { fn attached_morph_rim_follows_animated_body_at_fractional_scales() { for scale in [1., 1.25, 1.5, 2.] { for edge in [crate::Edge::Top, crate::Edge::Bottom] { - for (width, height, radius) in [(150., 34., 17.), (218.3, 41.7, 20.85), (340., 230., 26.)] { + for (width, height, radius) in + [(150., 34., 17.), (218.3, 41.7, 20.85), (340., 230., 26.)] + { let scene = derive( Parameters { style: Style::Morph, @@ -283,7 +330,11 @@ mod tests { .unwrap(); let body = scene.paint[0]; assert_eq!(scene.paint.len(), 3); - assert_eq!(scene.input.len(), 1, "decorative shoulders must not take pointer input"); + assert_eq!( + scene.input.len(), + 1, + "decorative shoulders must not take pointer input" + ); let screen_y = if edge == crate::Edge::Top { body.bounds.y } else { @@ -299,7 +350,10 @@ mod tests { assert!((path.first().unwrap()[1] - screen_y).abs() < 1e-6); assert!((path.last().unwrap()[1] - screen_y).abs() < 1e-6); assert!(path.iter().any(|p| p[0] < body.bounds.x)); - assert!(path.iter().any(|p| p[0] > body.bounds.x + body.bounds.width)); + assert!( + path.iter() + .any(|p| p[0] > body.bounds.x + body.bounds.width) + ); for segment in path.windows(2) { let [a, b] = [segment[0], segment[1]]; assert!( diff --git a/crates/ferese-panel/src/panel.rs b/crates/ferese-panel/src/panel.rs index 8d6a58c..ab3a676 100644 --- a/crates/ferese-panel/src/panel.rs +++ b/crates/ferese-panel/src/panel.rs @@ -220,9 +220,12 @@ impl<'de> Deserialize<'de> for Item { return Err(serde::de::Error::unknown_field(key, OPTIONS)); } } - if fields.contains_key("style") && fields.get("kind").and_then(serde_json::Value::as_str) != Some("workspaces") + if fields.contains_key("style") + && fields.get("kind").and_then(serde_json::Value::as_str) != Some("workspaces") { - return Err(serde::de::Error::custom("style is only supported by Workspaces items")); + return Err(serde::de::Error::custom( + "style is only supported by Workspaces items", + )); } if fields.contains_key("percentage") && fields.get("kind").and_then(serde_json::Value::as_str) != Some("battery") @@ -447,7 +450,10 @@ pub fn validate(panels: &[Panel]) -> Result<(), String> { let mut panel_ids = HashSet::new(); for panel in panels { if !PanelStyle::AVAILABLE.contains(&panel.style) { - return Err(format!("panel style {} is not available yet", panel.style.key())); + return Err(format!( + "panel style {} is not available yet", + panel.style.key() + )); } panel.geometry.validate()?; panel.morph.validate()?; @@ -497,7 +503,10 @@ pub fn validate(panels: &[Panel]) -> Result<(), String> { return Err("panel item priority must be 0..100 and gaps 0..64".into()); } if item.kind == ItemKind::Overflow { - return Err("overflow is supplied by the panel; it cannot be configured as an item".into()); + return Err( + "overflow is supplied by the panel; it cannot be configured as an item" + .into(), + ); } } } @@ -538,7 +547,11 @@ impl Panel { } pub fn zone_spacing(&self, zone: &Zone) -> f32 { - if self.style.connected() { 0. } else { zone.spacing } + if self.style.connected() { + 0. + } else { + zone.spacing + } } pub fn shape_parameters(&self) -> crate::scene::Parameters { @@ -606,7 +619,8 @@ impl Panel { } pub fn resolved_radius(&self, shell_radius: f32) -> CornerRadii { - self.corner_radius.unwrap_or_else(|| CornerRadii::uniform(shell_radius)) + self.corner_radius + .unwrap_or_else(|| CornerRadii::uniform(shell_radius)) } /// The overflow trigger is generated, never persisted as a configured item. @@ -629,7 +643,9 @@ impl Panel { .end .groups .first() - .map_or(crate::default_island_padding(), |group| group.island_padding), + .map_or(crate::default_island_padding(), |group| { + group.island_padding + }), } } @@ -685,7 +701,12 @@ impl Panel { let mut controls = controls; controls.extend([clock, display]); ( - vec![Group::new("navigation", vec![overview, workspaces], [2, 3], padding)], + vec![Group::new( + "navigation", + vec![overview, workspaces], + [2, 3], + padding, + )], Group::new("title", vec![title], [0, 0], padding), vec![Group::new("status", controls, [2, 3], padding)], ) @@ -703,7 +724,11 @@ impl Panel { corner_radius: Option::None, start: Zone { groups: start, - spacing: if preset == PanelStyle::Floating { 8.0 } else { 3.0 }, + spacing: if preset == PanelStyle::Floating { + 8.0 + } else { + 3.0 + }, }, center: Zone { groups: vec![center], @@ -821,7 +846,9 @@ mod tests { battery: true, external_display: true }), - all.iter().map(|item| item.id.0.as_str()).collect::>() + all.iter() + .map(|item| item.id.0.as_str()) + .collect::>() ); assert_eq!(items(&panel).len(), 13); } @@ -842,7 +869,10 @@ mod tests { { group.island_padding = 13.; } - assert_eq!(panel.center.groups[0].items[0].kind, ItemKind::FocusedWindow); + assert_eq!( + panel.center.groups[0].items[0].kind, + ItemKind::FocusedWindow + ); assert!(!panel.center.groups[0].items[0].visible); let all = items(&panel); assert_eq!( @@ -854,7 +884,13 @@ mod tests { assert_eq!(group.island_padding, 13.0); } } - assert_eq!(all.iter().find(|item| item.id.0 == "clock").unwrap().gap_before, None); + assert_eq!( + all.iter() + .find(|item| item.id.0 == "clock") + .unwrap() + .gap_before, + None + ); } #[test] diff --git a/crates/ferese-panel/src/panel/geometry.rs b/crates/ferese-panel/src/panel/geometry.rs index 85bbb12..f1e6213 100644 --- a/crates/ferese-panel/src/panel/geometry.rs +++ b/crates/ferese-panel/src/panel/geometry.rs @@ -25,8 +25,16 @@ impl GeometryIntent { Morph => 34., _ => 36., }), - edge_margin: if flush { 0 } else { self.edge_margin.unwrap_or(8) }, - side_margins: if flush { 0 } else { self.side_margins.unwrap_or(8) }, + edge_margin: if flush { + 0 + } else { + self.edge_margin.unwrap_or(8) + }, + side_margins: if flush { + 0 + } else { + self.side_margins.unwrap_or(8) + }, window_clearance: self.window_clearance.unwrap_or(0), inner_padding: self.inner_padding.unwrap_or(12.), } @@ -74,7 +82,10 @@ impl PanelGeometry { .into_iter() .any(|value| !(0..=4096).contains(&value)) { - return Err("panel height must be 24..128, inner padding 0..64, and margins/clearance 0..4096".into()); + return Err( + "panel height must be 24..128, inner padding 0..64, and margins/clearance 0..4096" + .into(), + ); } Ok(()) } @@ -101,7 +112,13 @@ mod tests { assert_eq!(pins.resolve(Flat).side_margins, 0); assert_eq!(pins.resolve(Floating).side_margins, 18); assert_eq!(pins.resolve(Notch).edge_margin, 0); - assert!(serde_json::to_value(intent).unwrap().as_object().unwrap().is_empty()); + assert!( + serde_json::to_value(intent) + .unwrap() + .as_object() + .unwrap() + .is_empty() + ); assert!( GeometryIntent { edge_margin: Some(-1), diff --git a/crates/ferese-panel/src/panel/layout.rs b/crates/ferese-panel/src/panel/layout.rs index 05e6e58..298ba6f 100644 --- a/crates/ferese-panel/src/panel/layout.rs +++ b/crates/ferese-panel/src/panel/layout.rs @@ -10,7 +10,10 @@ pub struct Measurement { #[derive(Clone, Debug, PartialEq)] pub enum Placement { - Visible { representation: Representation, width: f32 }, + Visible { + representation: Representation, + width: f32, + }, Overflow, } @@ -103,7 +106,12 @@ impl Resolution { } } -pub fn resolve(panel: &Panel, measurements: &[Measurement], available: f32, overflow_width: f32) -> Resolution { +pub fn resolve( + panel: &Panel, + measurements: &[Measurement], + available: f32, + overflow_width: f32, +) -> Resolution { resolve_with_gap(panel, measurements, available, overflow_width, 8.) } @@ -127,7 +135,10 @@ pub fn resolve_with_gap( .flat_map(|zone| &zone.groups) .flat_map(|group| &group.items) .collect(); - let measurements: BTreeMap<_, _> = measurements.iter().map(|measured| (&measured.id, measured)).collect(); + let measurements: BTreeMap<_, _> = measurements + .iter() + .map(|measured| (&measured.id, measured)) + .collect(); // Allocation policy follows composition, never the renderer's sample order. // Lower priorities yield first; definition order breaks equal-priority ties. let mut candidates: Vec<_> = ordered.iter().enumerate().collect(); @@ -143,9 +154,13 @@ pub fn resolve_with_gap( result.items.insert(item.id.clone(), Placement::Overflow); result.overflow.push(item.id.clone()); } else { - result - .items - .insert(item.id.clone(), Placement::Visible { representation, width }); + result.items.insert( + item.id.clone(), + Placement::Visible { + representation, + width, + }, + ); } } result.update_widths(panel, overflow_width); @@ -164,13 +179,20 @@ pub fn resolve_with_gap( result.minimum_width = minimum.required_width(); // Flexible content yields space without changing the stored item preference. for (_, item) in &candidates { - let Some(measured) = measurements.get(&item.id).filter(|measured| measured.minimum.is_some()) else { + let Some(measured) = measurements + .get(&item.id) + .filter(|measured| measured.minimum.is_some()) + else { continue; }; if fits(&result) { break; } - let Some(Placement::Visible { representation, width }) = result.items.get(&measured.id).cloned() else { + let Some(Placement::Visible { + representation, + width, + }) = result.items.get(&measured.id).cloned() + else { continue; }; let minimum = measured.minimum.unwrap().min(width); @@ -191,12 +213,18 @@ pub fn resolve_with_gap( continue; }; for &(representation, width) in measured.alternatives.iter().skip(1) { - if let Some(Placement::Visible { width: previous, .. }) = result.items.get(&item.id) + if let Some(Placement::Visible { + width: previous, .. + }) = result.items.get(&item.id) && width < *previous { - result - .items - .insert(item.id.clone(), Placement::Visible { representation, width }); + result.items.insert( + item.id.clone(), + Placement::Visible { + representation, + width, + }, + ); result.update_widths(panel, overflow_width); if fits(&result) { break; @@ -214,7 +242,8 @@ pub fn resolve_with_gap( if item.overflow == OverflowPolicy::Never && !force { continue; } - if matches!(result.items.get(&item.id), Some(Placement::Visible { width, .. }) if *width > 0.0) { + if matches!(result.items.get(&item.id), Some(Placement::Visible { width, .. }) if *width > 0.0) + { result.items.insert(item.id.clone(), Placement::Overflow); result.overflow.push(item.id.clone()); result.forced_overflow |= force && item.overflow == OverflowPolicy::Never; @@ -235,7 +264,11 @@ pub fn resolve_with_gap( let Some(measured) = measurements.get(&item.id) else { continue; }; - let Some(&(representation, width)) = measured.alternatives.iter().min_by(|a, b| a.1.total_cmp(&b.1)) else { + let Some(&(representation, width)) = measured + .alternatives + .iter() + .min_by(|a, b| a.1.total_cmp(&b.1)) + else { continue; }; result.items.insert( @@ -255,7 +288,8 @@ pub fn resolve_with_gap( } } result.forced_overflow = ordered.iter().any(|item| { - item.overflow == OverflowPolicy::Never && matches!(result.items.get(&item.id), Some(Placement::Overflow)) + item.overflow == OverflowPolicy::Never + && matches!(result.items.get(&item.id), Some(Placement::Overflow)) }); // A reduction on the shorter side can leave unused room after the other // side shrinks. Restore preferred representations before expanding titles. @@ -263,7 +297,9 @@ pub fn resolve_with_gap( let Some(measured) = measurements.get(&item.id) else { continue; }; - let Some(previous @ Placement::Visible { representation, .. }) = result.items.get(&item.id).cloned() else { + let Some(previous @ Placement::Visible { representation, .. }) = + result.items.get(&item.id).cloned() + else { continue; }; for &(candidate, width) in measured @@ -288,7 +324,10 @@ pub fn resolve_with_gap( result.update_widths(panel, overflow_width); // Restore the most protected flexible items first, reversing the yield order. for (_, item) in candidates.iter().rev() { - let Some(measured) = measurements.get(&item.id).filter(|measured| measured.minimum.is_some()) else { + let Some(measured) = measurements + .get(&item.id) + .filter(|measured| measured.minimum.is_some()) + else { continue; }; let Some(Placement::Visible { @@ -309,9 +348,16 @@ pub fn resolve_with_gap( let mut high = maximum; for _ in 0..20 { let width = (low + high) * 0.5; - *result.items.get_mut(&measured.id).unwrap() = Placement::Visible { representation, width }; + *result.items.get_mut(&measured.id).unwrap() = Placement::Visible { + representation, + width, + }; result.update_widths(panel, overflow_width); - if fits(&result) { low = width } else { high = width } + if fits(&result) { + low = width + } else { + high = width + } } *result.items.get_mut(&measured.id).unwrap() = Placement::Visible { representation, @@ -333,7 +379,11 @@ mod tests { fn asymmetric_overflow_restores_items_that_fit_after_the_larger_side_yields() { use super::super::{Group, Item, ItemKind}; for mirrored in [false, true] { - for policy in [OverflowPolicy::Auto, OverflowPolicy::Never, OverflowPolicy::Always] { + for policy in [ + OverflowPolicy::Auto, + OverflowPolicy::Never, + OverflowPolicy::Always, + ] { let group = |id: &str, priority, overflow| { let mut item = Item::new(id, ItemKind::Clock); item.priority = priority; @@ -360,7 +410,10 @@ mod tests { let resolution = resolve(&panel, &measurements, 300., 32.); assert!(resolution.fits(300.)); assert_eq!( - matches!(resolution.items[&ItemId("small".into())], Placement::Visible { .. }), + matches!( + resolution.items[&ItemId("small".into())], + Placement::Visible { .. } + ), policy != OverflowPolicy::Always ); assert!(resolution.overflow.contains(&ItemId("large".into()))); @@ -381,10 +434,18 @@ mod tests { let mut group = Group::new("content", vec![first, hidden, last], [0, 3], 5.); panel.style = crate::PanelStyle::Floating; group.island_padding = 4.; - let empty = Group::new("empty", vec![Item::new("missing", ItemKind::Media)], [0, 99], 99.); + let empty = Group::new( + "empty", + vec![Item::new("missing", ItemKind::Media)], + [0, 99], + 99., + ); let trailing = Group::new( "trailing", - vec![Item::new("trailing", ItemKind::Battery { percentage: false })], + vec![Item::new( + "trailing", + ItemKind::Battery { percentage: false }, + )], [0, 2], 5., ); @@ -630,9 +691,15 @@ mod tests { let expected = resolve_with_gap(&panel, &measured, width, 32., 16.); for _ in 0..measured.len() { measured.rotate_left(1); - assert_eq!(resolve_with_gap(&panel, &measured, width, 32., 16.), expected); + assert_eq!( + resolve_with_gap(&panel, &measured, width, 32., 16.), + expected + ); measured.reverse(); - assert_eq!(resolve_with_gap(&panel, &measured, width, 32., 16.), expected); + assert_eq!( + resolve_with_gap(&panel, &measured, width, 32., 16.), + expected + ); measured.reverse(); } } @@ -700,7 +767,10 @@ mod tests { let margin = full.side_margin(requested, output_width, 12.); let available = output_width as f32 - 2. * (margin as f32 + 12.); let result = resolve(&panel, &measurements, available, 32.); - assert!(result.overflow.is_empty(), "output {output_width}, margin {margin}"); + assert!( + result.overflow.is_empty(), + "output {output_width}, margin {margin}" + ); assert!(result.fits(available)); assert!(margin <= requested); } @@ -714,7 +784,8 @@ mod tests { } #[test] - fn margin_limit_includes_group_padding_and_authored_overflow_without_dependence_on_current_surface_width() { + fn margin_limit_includes_group_padding_and_authored_overflow_without_dependence_on_current_surface_width() + { let mut panel = Panel::default(); panel.end.groups[0].items[0].overflow = OverflowPolicy::Always; let measurements = measurements(&panel); @@ -722,7 +793,12 @@ mod tests { let narrow = resolve(&panel, &measurements, 100., 32.); assert_eq!(full.minimum_width, narrow.minimum_width); let margin = full.side_margin(4096, 2400, 12.); - let result = resolve(&panel, &measurements, 2400. - 2. * (margin as f32 + 12.), 32.); + let result = resolve( + &panel, + &measurements, + 2400. - 2. * (margin as f32 + 12.), + 32., + ); assert_eq!(result.overflow, full.overflow); let original = full.minimum_width; panel.end.groups[0].padding[1] += 10; @@ -794,7 +870,10 @@ mod tests { assert!(result.overflow.is_empty()); assert!(result.items.values().all(|placement| matches!(placement, Placement::Visible { representation: Representation::Wide, width } if *width == 50.))); } - assert_eq!(panel, initial, "resolution must not rewrite configuration intent"); + assert_eq!( + panel, initial, + "resolution must not rewrite configuration intent" + ); } } #[test] @@ -804,8 +883,15 @@ mod tests { let initial = resolve(&panel, &measured, 2400., 32.); panel.style = crate::PanelStyle::Flat; let plain = resolve(&panel, &measured, 2400., 32.); - for (i, zone) in [&panel.start, &panel.center, &panel.end].into_iter().enumerate() { - let padding: f32 = zone.groups.iter().map(|group| 2. * group.island_padding).sum(); + for (i, zone) in [&panel.start, &panel.center, &panel.end] + .into_iter() + .enumerate() + { + let padding: f32 = zone + .groups + .iter() + .map(|group| 2. * group.island_padding) + .sum(); assert_eq!(plain.zone_widths[i], initial.zone_widths[i] - padding); } panel.style = crate::PanelStyle::Flat; diff --git a/crates/ferese-panel/src/panel/radius.rs b/crates/ferese-panel/src/panel/radius.rs index 65a3cc5..21b71f5 100644 --- a/crates/ferese-panel/src/panel/radius.rs +++ b/crates/ferese-panel/src/panel/radius.rs @@ -79,7 +79,8 @@ impl std::str::FromStr for CornerRadii { let number = if part == "0" { part } else { - part.strip_suffix("px").ok_or("use pixel values such as 4px")? + part.strip_suffix("px") + .ok_or("use pixel values such as 4px")? }; number.parse::().map_err(|_| "invalid corner radius") }) @@ -152,7 +153,10 @@ mod tests { let radii = CornerRadii([4., 8., 12., 16.]); assert_eq!(radii.at_top_edge(true).0, [0., 0., 12., 16.]); assert_eq!(radii.at_top_edge(false), radii); - assert_eq!(radii.at_edge(super::super::Edge::Bottom, true).0, [4., 8., 0., 0.]); + assert_eq!( + radii.at_edge(super::super::Edge::Bottom, true).0, + [4., 8., 0., 0.] + ); assert_eq!(radii.at_edge(super::super::Edge::Bottom, false), radii); assert_eq!(radii.0, [4., 8., 12., 16.]); } @@ -171,6 +175,9 @@ mod tests { ] { assert!(source.parse::().is_err(), "{source}"); } - assert_eq!(CornerRadii([4., 40., 0., 18.]).clamped(100., 28.).0, [4., 14., 0., 14.]); + assert_eq!( + CornerRadii([4., 40., 0., 18.]).clamped(100., 28.).0, + [4., 14., 0., 14.] + ); } } diff --git a/crates/ferese-panel/src/scene.rs b/crates/ferese-panel/src/scene.rs index bb55df7..c8c6ded 100644 --- a/crates/ferese-panel/src/scene.rs +++ b/crates/ferese-panel/src/scene.rs @@ -17,7 +17,12 @@ pub struct Primitive { impl Primitive { pub fn packet(self) -> [f32; 10] { - let Rect { x, y, width, height } = self.bounds; + let Rect { + x, + y, + width, + height, + } = self.bounds; let [tl, tr, br, bl] = self.radii.0; [ x as f32, @@ -34,7 +39,9 @@ impl Primitive { } pub fn from_packet(values: [f32; 10]) -> Result { - if !values.into_iter().all(|v| v.is_finite() && v.abs() <= 32768.) + if !values + .into_iter() + .all(|v| v.is_finite() && v.abs() <= 32768.) || values[2] <= 0. || values[3] <= 0. || values[4..8].iter().any(|v| *v < 0.) @@ -46,8 +53,14 @@ impl Primitive { return Err("invalid panel shape primitive"); } Ok(Self { - bounds: Rect::new(values[0].into(), values[1].into(), values[2].into(), values[3].into()), - radii: CornerRadii([values[4], values[5], values[6], values[7]]).normalized(values[2], values[3]), + bounds: Rect::new( + values[0].into(), + values[1].into(), + values[2].into(), + values[3].into(), + ), + radii: CornerRadii([values[4], values[5], values[6], values[7]]) + .normalized(values[2], values[3]), operation: values[8] as u8, blend_zone: values[9] as u8, }) @@ -63,7 +76,8 @@ impl Primitive { 4 => [b.x + b.width, b.y], _ => [b.x, b.y], }; - return rect.max(b.width.min(b.height) - (point[0] - center[0]).hypot(point[1] - center[1])); + return rect + .max(b.width.min(b.height) - (point[0] - center[0]).hypot(point[1] - center[1])); } let b = self.bounds; let (x, y) = (point[0] - b.x, point[1] - b.y); @@ -150,12 +164,21 @@ impl Scene { let bottom = ((b.y + b.height) * scale).ceil() as i32; for y in top..bottom { let mut first = left; - while first < right && !shape.contains([(f64::from(first) + 0.5) / scale, (f64::from(y) + 0.5) / scale]) + while first < right + && !shape.contains([ + (f64::from(first) + 0.5) / scale, + (f64::from(y) + 0.5) / scale, + ]) { first += 1; } let mut last = right; - while last > first && !shape.contains([(f64::from(last) - 0.5) / scale, (f64::from(y) + 0.5) / scale]) { + while last > first + && !shape.contains([ + (f64::from(last) - 0.5) / scale, + (f64::from(y) + 0.5) / scale, + ]) + { last -= 1; } if first == last { @@ -328,8 +351,18 @@ pub fn derive( Style::CornerWrap => { let h = parameters.height.min(bounds.height); let w = parameters.wrap_width.min(bounds.width); - let horizontal = Rect::new(bounds.x, bounds.y, parameters.wrap_size[0].min(bounds.width), h); - let vertical = Rect::new(bounds.x, bounds.y, w, parameters.wrap_size[1].min(bounds.height)); + let horizontal = Rect::new( + bounds.x, + bounds.y, + parameters.wrap_size[0].min(bounds.width), + h, + ); + let vertical = Rect::new( + bounds.x, + bounds.y, + w, + parameters.wrap_size[1].min(bounds.height), + ); add( &mut scene, grid, @@ -338,7 +371,14 @@ pub fn derive( true, 0, )?; - add(&mut scene, grid, vertical, CornerRadii([0., 0., 14., 0.]), true, 0)?; + add( + &mut scene, + grid, + vertical, + CornerRadii([0., 0., 14., 0.]), + true, + 0, + )?; // Capture the union once: overlapping translucent captures darken the elbow. scene.damage = vec![ horizontal, @@ -388,8 +428,12 @@ pub fn derive( // One material capture owns the body and both shoulders. Separate // captures overlap after logical rounding at fractional scales, // painting translucent joins twice as the spring settles. - scene.damage = - vec![grid.rect(Rect::new(bounds.x - r, bounds.y, bounds.width + 2. * r, bounds.height))?]; + scene.damage = vec![grid.rect(Rect::new( + bounds.x - r, + bounds.y, + bounds.width + 2. * r, + bounds.height, + ))?]; } } } @@ -406,7 +450,10 @@ pub fn derive( continue; } if style.connected() { - let x = occupied.iter().map(|g| g.bounds.x).fold(f64::INFINITY, f64::min); + let x = occupied + .iter() + .map(|g| g.bounds.x) + .fold(f64::INFINITY, f64::min); let end = occupied .iter() .map(|g| g.bounds.x + g.bounds.width) @@ -440,9 +487,12 @@ pub fn derive( add(&mut scene, grid, body, radii, true, 0)?; for group in occupied.iter().take(occupied.len() - 1) { let seam = grid.snap(group.bounds.x + group.bounds.width); - scene - .dividers - .push([seam, bounds.y + 8., seam, bounds.y + bounds.height - 8.]); + scene.dividers.push([ + seam, + bounds.y + 8., + seam, + bounds.y + bounds.height - 8., + ]); } if style == Style::Notch && zone == ZonePosition::Center { let r = parameters @@ -469,7 +519,8 @@ pub fn derive( } } else { for group in occupied { - let body = Rect::new(group.bounds.x, bounds.y, group.bounds.width, bounds.height); + let body = + Rect::new(group.bounds.x, bounds.y, group.bounds.width, bounds.height); let radii = parameters .radius .map_or(CornerRadii::uniform((bounds.height / 2.) as f32), |r| { @@ -500,7 +551,8 @@ pub fn derive( if let Some(first) = shapes.next() { let mut region = first.bounds; for shape in shapes { - let right = (region.x + region.width).max(shape.bounds.x + shape.bounds.width); + let right = + (region.x + region.width).max(shape.bounds.x + shape.bounds.width); region.x = region.x.min(shape.bounds.x); region.width = right - region.x; } @@ -510,7 +562,9 @@ pub fn derive( } } } - if parameters.edge == Edge::Bottom && matches!(parameters.style, Style::Frame | Style::CornerWrap) { + if parameters.edge == Edge::Bottom + && matches!(parameters.style, Style::Frame | Style::CornerWrap) + { let mirror = |mut r: Rect| { r.y = 2. * bounds.y + bounds.height - r.y - r.height; r @@ -529,7 +583,8 @@ pub fn derive( } for input in &mut scene.input { let [tl, tr, br, bl] = input.radii().0; - *input = RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?; + *input = + RoundedSurface::new(mirror(input.bounds()), CornerRadii([bl, br, tr, tl]), grid)?; } for damage in &mut scene.damage { *damage = mirror(*damage); @@ -629,7 +684,12 @@ mod tests { assert!(!scene.contains([38., 38.])); // the decorative elbow is not input for (i, a) in scene.damage.iter().enumerate() { for b in &scene.damage[i + 1..] { - assert!(a.x + a.width <= b.x || b.x + b.width <= a.x || a.y + a.height <= b.y || b.y + b.height <= a.y); + assert!( + a.x + a.width <= b.x + || b.x + b.width <= a.x + || a.y + a.height <= b.y + || b.y + b.height <= a.y + ); } } for x in (0..800).step_by(3) { diff --git a/crates/ferese-panel/src/shape.rs b/crates/ferese-panel/src/shape.rs index 93a2c66..cbe245a 100644 --- a/crates/ferese-panel/src/shape.rs +++ b/crates/ferese-panel/src/shape.rs @@ -77,7 +77,12 @@ impl Style { pub const fn detached(self) -> bool { matches!( self, - Self::Floating | Self::Capsules | Self::Fused | Self::Morph | Self::Adaptive | Self::WindowHug + Self::Floating + | Self::Capsules + | Self::Fused + | Self::Morph + | Self::Adaptive + | Self::WindowHug ) } } @@ -129,7 +134,10 @@ pub fn effective_radii( index: usize, count: usize, ) -> Result { - if !size.into_iter().all(|value| value.is_finite() && value >= 0.) { + if !size + .into_iter() + .all(|value| value.is_finite() && value >= 0.) + { return Err("panel dimensions must be finite and nonnegative"); } if index >= count { @@ -137,9 +145,12 @@ pub fn effective_radii( } let [width, height] = size; let base = AnchoredRadii(match style { - Style::Floating | Style::Capsules | Style::Fused | Style::Morph | Style::Adaptive | Style::WindowHug => { - [height / 2.; 4] - } + Style::Floating + | Style::Capsules + | Style::Fused + | Style::Morph + | Style::Adaptive + | Style::WindowHug => [height / 2.; 4], Style::Notch => match zone { ZonePosition::Start => [0., 0., 14., 0.], ZonePosition::Center => [0., 0., 20., 20.], @@ -189,7 +200,12 @@ impl PixelGrid { if ![left, top, right, bottom].into_iter().all(f64::is_finite) { return Err("surface bounds exceed the physical pixel grid"); } - Ok(Rect::new(left, top, (right - left).max(0.), (bottom - top).max(0.))) + Ok(Rect::new( + left, + top, + (right - left).max(0.), + (bottom - top).max(0.), + )) } } @@ -266,7 +282,17 @@ mod tests { assert_eq!(radius(1, 2).0, [0., 18., 18., 0.]); assert_eq!(radius(0, 1).0, [18.; 4]); assert_eq!(radius(0, 2).0, [18., 0., 0., 18.]); - assert!(effective_radii(Style::Capsules, ZonePosition::Start, Edge::Top, [100., 36.], 0, 0).is_err()); + assert!( + effective_radii( + Style::Capsules, + ZonePosition::Start, + Edge::Top, + [100., 36.], + 0, + 0 + ) + .is_err() + ); } #[test] @@ -288,14 +314,25 @@ mod tests { #[test] fn asymmetric_corners_preserve_notch_shape_and_scale_proportionally() { - let notch = effective_radii(Style::Notch, ZonePosition::Center, Edge::Top, [100., 28.], 0, 1).unwrap(); + let notch = effective_radii( + Style::Notch, + ZonePosition::Center, + Edge::Top, + [100., 28.], + 0, + 1, + ) + .unwrap(); assert_eq!(notch.0, [0., 0., 20., 20.]); assert_eq!(notch.normalized(30., 28.).0, [0., 0., 15., 15.]); assert_eq!( CornerRadii([10., 20., 40., 30.]).normalized(35., 25.).0, [25. / 6., 25. / 3., 50. / 3., 12.5] ); - assert_eq!(CornerRadii::uniform(f32::MAX).normalized(40., 28.).0, [14.; 4]); + assert_eq!( + CornerRadii::uniform(f32::MAX).normalized(40., 28.).0, + [14.; 4] + ); assert_eq!(notch.normalized(0., 28.).0, [0.; 4]); } @@ -317,9 +354,25 @@ mod tests { #[test] fn floating_gaps_and_round_corners_do_not_capture_input() { let grid = PixelGrid::new(1.25).unwrap(); - let left = RoundedSurface::new(Rect::new(8., 8., 100., 36.), CornerRadii::uniform(18.), grid).unwrap(); - let right = RoundedSurface::new(Rect::new(116., 8., 100., 36.), CornerRadii::uniform(18.), grid).unwrap(); - for point in [[0., 0.], [8., 8.], [112., 26.], [216., 26.], [f64::NAN, 26.]] { + let left = RoundedSurface::new( + Rect::new(8., 8., 100., 36.), + CornerRadii::uniform(18.), + grid, + ) + .unwrap(); + let right = RoundedSurface::new( + Rect::new(116., 8., 100., 36.), + CornerRadii::uniform(18.), + grid, + ) + .unwrap(); + for point in [ + [0., 0.], + [8., 8.], + [112., 26.], + [216., 26.], + [f64::NAN, 26.], + ] { assert!(!left.contains(point) && !right.contains(point), "{point:?}"); } assert!(left.contains([30., 26.])); @@ -330,7 +383,12 @@ mod tests { fn asymmetric_hit_testing_matches_a_vertically_mirrored_surface() { let grid = PixelGrid::new(1.).unwrap(); let bounds = Rect::new(0., 0., 100., 28.); - let top = RoundedSurface::new(bounds, AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top), grid).unwrap(); + let top = RoundedSurface::new( + bounds, + AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Top), + grid, + ) + .unwrap(); let bottom = RoundedSurface::new( bounds, AnchoredRadii([0., 0., 20., 20.]).screen_space(Edge::Bottom), @@ -352,8 +410,18 @@ mod tests { #[test] fn a_connected_seam_has_exactly_one_input_owner() { let grid = PixelGrid::new(1.5).unwrap(); - let left = RoundedSurface::new(Rect::new(0., 0., 30., 36.), CornerRadii([18., 0., 0., 18.]), grid).unwrap(); - let right = RoundedSurface::new(Rect::new(30., 0., 40., 36.), CornerRadii([0., 18., 18., 0.]), grid).unwrap(); + let left = RoundedSurface::new( + Rect::new(0., 0., 30., 36.), + CornerRadii([18., 0., 0., 18.]), + grid, + ) + .unwrap(); + let right = RoundedSurface::new( + Rect::new(30., 0., 40., 36.), + CornerRadii([0., 18., 18., 0.]), + grid, + ) + .unwrap(); for y in [0., 18., 35.9] { assert!(!left.contains([30., y])); assert!(right.contains([30., y])); @@ -373,7 +441,20 @@ mod tests { ] { assert!(grid.rect(bounds).is_err()); } - assert!(RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid).is_err()); - assert!(effective_radii(Style::Floating, ZonePosition::Start, Edge::Top, [100., f32::NAN], 0, 1).is_err()); + assert!( + RoundedSurface::new(Rect::new(0., 0., 10., 28.), CornerRadii::uniform(-1.), grid) + .is_err() + ); + assert!( + effective_radii( + Style::Floating, + ZonePosition::Start, + Edge::Top, + [100., f32::NAN], + 0, + 1 + ) + .is_err() + ); } } diff --git a/crates/ferese-protocols/src/lib.rs b/crates/ferese-protocols/src/lib.rs index 56530de..c9a2e77 100644 --- a/crates/ferese-protocols/src/lib.rs +++ b/crates/ferese-protocols/src/lib.rs @@ -196,11 +196,15 @@ pub mod window_capture { use wayland_client::protocol::__interfaces::*; use wayland_protocols_wlr::screencopy::v1::client::__interfaces::*; - wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml"); + wayland_scanner::generate_interfaces!( + "../../protocols/ferese-window-capture-v1.xml" + ); } use self::__interfaces::*; - wayland_scanner::generate_client_code!("../../protocols/ferese-window-capture-v1.xml"); + wayland_scanner::generate_client_code!( + "../../protocols/ferese-window-capture-v1.xml" + ); } #[cfg(feature = "server")] @@ -213,11 +217,15 @@ pub mod window_capture { use wayland_protocols_wlr::screencopy::v1::server::__interfaces::*; use wayland_server::protocol::__interfaces::*; - wayland_scanner::generate_interfaces!("../../protocols/ferese-window-capture-v1.xml"); + wayland_scanner::generate_interfaces!( + "../../protocols/ferese-window-capture-v1.xml" + ); } use self::__interfaces::*; - wayland_scanner::generate_server_code!("../../protocols/ferese-window-capture-v1.xml"); + wayland_scanner::generate_server_code!( + "../../protocols/ferese-window-capture-v1.xml" + ); } } } diff --git a/crates/ferese-theme-client/src/material.rs b/crates/ferese-theme-client/src/material.rs index c54be1b..6976cb9 100644 --- a/crates/ferese-theme-client/src/material.rs +++ b/crates/ferese-theme-client/src/material.rs @@ -33,8 +33,9 @@ impl ModalMaterial { return Err("Dialog materials require Wayland".into()); }; // Iced lends live native handles; this backend borrows its connection. - let backend = - unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) }; + let backend = unsafe { + wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) + }; let connection = Connection::from_backend(backend); let id = unsafe { wayland_client::backend::ObjectId::from_ptr( @@ -43,7 +44,8 @@ impl ModalMaterial { ) } .map_err(|error| error.to_string())?; - let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?; + let surface = + wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?; let (globals, mut queue) = registry_queue_init::(&connection).map_err(|error| error.to_string())?; let qh = queue.handle(); @@ -57,7 +59,9 @@ impl ModalMaterial { material, }; let mut state = MaterialState::default(); - queue.roundtrip(&mut state).map_err(|error| error.to_string())?; + queue + .roundtrip(&mut state) + .map_err(|error| error.to_string())?; if !state.ready { return Err("Dialog material attachment was not acknowledged".into()); } @@ -116,8 +120,8 @@ delegate_noop!(MaterialState: ignore FereseMaterialManagerV1); mod tests { use super::*; use cosmic::iced::window::raw_window_handle::{ - DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WaylandDisplayHandle, WaylandWindowHandle, - WindowHandle, + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, WaylandDisplayHandle, + WaylandWindowHandle, WindowHandle, }; use std::ptr::NonNull; use wayland_client::protocol::wl_compositor; @@ -166,7 +170,8 @@ mod tests { queue.roundtrip(&mut MaterialState::default()).unwrap(); // The compositor rejects duplicate materials. Reattachment proves the final // drop destroyed the first material while preserving the borrowed display. - let replacement = ModalMaterial::attach(&window).expect("last clone released the surface material"); + let replacement = + ModalMaterial::attach(&window).expect("last clone released the surface material"); drop(replacement); queue.roundtrip(&mut MaterialState::default()).unwrap(); window.surface.destroy(); diff --git a/crates/ferese-theme-client/src/service.rs b/crates/ferese-theme-client/src/service.rs index 4c8624e..b48b845 100644 --- a/crates/ferese-theme-client/src/service.rs +++ b/crates/ferese-theme-client/src/service.rs @@ -110,7 +110,9 @@ mod tests { let server = std::thread::spawn(move || { for revision in [1, 2] { let (mut connection, _) = listener.accept().unwrap(); - connection.set_read_timeout(Some(Duration::from_secs(5))).unwrap(); + connection + .set_read_timeout(Some(Duration::from_secs(5))) + .unwrap(); let get: Request = read_frame(&mut connection).unwrap(); assert_eq!(get.command, "theme-get"); let mut snapshot = fallback(); @@ -145,9 +147,11 @@ mod tests { .unwrap(); assert_eq!(snapshot.revision, revision); } - tokio::task::spawn_blocking(move || watch_receive.recv_timeout(Duration::from_secs(5)).unwrap()) - .await - .unwrap(); + tokio::task::spawn_blocking(move || { + watch_receive.recv_timeout(Duration::from_secs(5)).unwrap() + }) + .await + .unwrap(); drop(subscription); }); server.join().unwrap(); diff --git a/crates/ferese-theme-client/tests/theme_wire.rs b/crates/ferese-theme-client/tests/theme_wire.rs index ba717e9..09a68d2 100644 --- a/crates/ferese-theme-client/tests/theme_wire.rs +++ b/crates/ferese-theme-client/tests/theme_wire.rs @@ -37,7 +37,8 @@ fn explicit_solid_style_round_trips_without_resolving_published_values() { value[key]["tokens"]["focus_ring"]["style"] = json!("solid"); } - let snapshot = Snapshot::decode(value.clone(), || panic!("unexpected catalog resolution")).unwrap(); + let snapshot = + Snapshot::decode(value.clone(), || panic!("unexpected catalog resolution")).unwrap(); assert_eq!(serde_json::to_value(snapshot).unwrap(), value); } @@ -46,7 +47,8 @@ fn older_snapshots_supply_catalog_but_explicit_empty_catalog_stays_empty() { let mut value = fixture(); value.as_object_mut().unwrap().remove("families"); value.as_object_mut().unwrap().remove("fallback_note"); - let snapshot: Snapshot = Snapshot::decode(value.clone(), ferese_config::families::builtins).unwrap(); + let snapshot: Snapshot = + Snapshot::decode(value.clone(), ferese_config::families::builtins).unwrap(); assert_eq!(snapshot.families, ferese_config::families::builtins()); assert_eq!(snapshot.fallback_note, None); value["families"] = json!([]); @@ -63,7 +65,8 @@ fn older_snapshots_supply_catalog_but_explicit_empty_catalog_stays_empty() { #[test] fn offline_preview_resolves_without_changing_published_values() { let before = fallback(); - let document = ferese_config::Document::parse("theme { mode \"light\"; family \"gruvbox\"; }\n").unwrap(); + let document = + ferese_config::Document::parse("theme { mode \"light\"; family \"gruvbox\"; }\n").unwrap(); let candidate = ferese_config::theme::resolve( &document, std::path::Path::new("/unused"), @@ -71,7 +74,10 @@ fn offline_preview_resolves_without_changing_published_values() { |_| panic!("built-in preview must not read theme files"), ) .unwrap(); - assert_eq!(candidate.theme.appearance, ferese_config::theme::Appearance::Light); + assert_eq!( + candidate.theme.appearance, + ferese_config::theme::Appearance::Light + ); assert_eq!(candidate.theme.tokens.colors.surface_base, "#FBF1C7"); assert_eq!(before, fallback()); } @@ -79,7 +85,8 @@ fn offline_preview_resolves_without_changing_published_values() { #[test] fn complete_snapshots_do_not_resolve_fallbacks_and_future_versions_are_rejected() { let value = fixture(); - let snapshot = Snapshot::decode(value.clone(), || panic!("unexpected catalog resolution")).unwrap(); + let snapshot = + Snapshot::decode(value.clone(), || panic!("unexpected catalog resolution")).unwrap(); assert_eq!(serde_json::to_value(snapshot).unwrap(), value); let mut future = value; future["version"] = json!(ferese_ipc::theme::SCHEMA_VERSION + 1); diff --git a/crates/ferese-theme-model/src/color.rs b/crates/ferese-theme-model/src/color.rs index 7dce953..c710c31 100644 --- a/crates/ferese-theme-model/src/color.rs +++ b/crates/ferese-theme-model/src/color.rs @@ -1,14 +1,18 @@ use crate::{Gradient, Paint, PaintStyle, ResolvedTheme}; pub fn rgba(s: &str) -> Result<[f64; 4], String> { - let hex = s.strip_prefix('#').ok_or_else(|| format!("Invalid color: {s}"))?; + let hex = s + .strip_prefix('#') + .ok_or_else(|| format!("Invalid color: {s}"))?; if !hex.is_ascii() || !matches!(hex.len(), 6 | 8) { return Err(format!("Invalid color: {s}")); } let mut out = [1.; 4]; for (i, c) in out.iter_mut().enumerate().take(hex.len() / 2) { - *c = f64::from(u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16).map_err(|_| format!("Invalid color: {s}"))?) - / 255.; + *c = f64::from( + u8::from_str_radix(&hex[i * 2..i * 2 + 2], 16) + .map_err(|_| format!("Invalid color: {s}"))?, + ) / 255.; } Ok(out) } @@ -74,7 +78,11 @@ pub fn readable(preferred: [f64; 4], background: [f64; 4], minimum: f64) -> [f64 target } -pub fn readable_across(mut preferred: [f64; 4], backgrounds: &[[f64; 4]], minimum: f64) -> [f64; 4] { +pub fn readable_across( + mut preferred: [f64; 4], + backgrounds: &[[f64; 4]], + minimum: f64, +) -> [f64; 4] { preferred[3] = 1.; let score = |color| { backgrounds @@ -87,7 +95,11 @@ pub fn readable_across(mut preferred: [f64; 4], backgrounds: &[[f64; 4]], minimu } let black = [0., 0., 0., 1.]; let white = [1.; 4]; - let target = if score(black) > score(white) { black } else { white }; + let target = if score(black) > score(white) { + black + } else { + white + }; for step in 1..=100 { let color = rgba(&hex(blend(preferred, target, f64::from(step) / 100.))).unwrap(); if score(color) >= minimum { @@ -99,7 +111,10 @@ pub fn readable_across(mut preferred: [f64; 4], backgrounds: &[[f64; 4]], minimu pub fn material_bounds(mut surface: [f64; 4], opacity: f64) -> [[f64; 4]; 2] { surface[3] = opacity; - [composite(surface, [0., 0., 0., 1.]), composite(surface, [1.; 4])] + [ + composite(surface, [0., 0., 0., 1.]), + composite(surface, [1.; 4]), + ] } fn material_foreground(preferred: [f64; 4], backgrounds: [[f64; 4]; 2]) -> [f64; 4] { @@ -111,7 +126,8 @@ fn material_foreground(preferred: [f64; 4], backgrounds: [[f64; 4]; 2]) -> [f64; (white, backgrounds[1]) }; let result = readable(preferred, worst, 4.5); - let outside = luminance(result) < luminance(backgrounds[0]) || luminance(result) > luminance(backgrounds[1]); + let outside = luminance(result) < luminance(backgrounds[0]) + || luminance(result) > luminance(backgrounds[1]); if outside && backgrounds .iter() @@ -189,8 +205,14 @@ impl ResolvedTheme { let p = p * p * (3. - 2. * p); let mut frame = to.clone(); let mix = |a: &str, b: &str| hex(blend(rgba(a).unwrap(), rgba(b).unwrap(), p)); - frame.tokens.colors.surface_base = mix(&self.tokens.colors.surface_base, &to.tokens.colors.surface_base); - frame.tokens.colors.surface_raised = mix(&self.tokens.colors.surface_raised, &to.tokens.colors.surface_raised); + frame.tokens.colors.surface_base = mix( + &self.tokens.colors.surface_base, + &to.tokens.colors.surface_base, + ); + frame.tokens.colors.surface_raised = mix( + &self.tokens.colors.surface_raised, + &to.tokens.colors.surface_raised, + ); frame.tokens.colors.application_background = mix( &self.tokens.colors.application_background, &to.tokens.colors.application_background, @@ -212,14 +234,16 @@ impl ResolvedTheme { &to.tokens.colors.border, p, ); - frame.tokens.surface.bar.background = - mix(&self.tokens.surface.bar.background, &to.tokens.surface.bar.background); - frame.tokens.material.opacity = - self.tokens.material.opacity + (to.tokens.material.opacity - self.tokens.material.opacity) * p; + frame.tokens.surface.bar.background = mix( + &self.tokens.surface.bar.background, + &to.tokens.surface.bar.background, + ); + frame.tokens.material.opacity = self.tokens.material.opacity + + (to.tokens.material.opacity - self.tokens.material.opacity) * p; frame.tokens.material.tint_strength = self.tokens.material.tint_strength + (to.tokens.material.tint_strength - self.tokens.material.tint_strength) * p; - frame.tokens.shadow.soft.opacity = - self.tokens.shadow.soft.opacity + (to.tokens.shadow.soft.opacity - self.tokens.shadow.soft.opacity) * p; + frame.tokens.shadow.soft.opacity = self.tokens.shadow.soft.opacity + + (to.tokens.shadow.soft.opacity - self.tokens.shadow.soft.opacity) * p; let preferred = if p < 0.5 { self } else { to }; frame.appearance = preferred.appearance; if self.appearance == to.appearance { @@ -228,7 +252,11 @@ impl ResolvedTheme { rgba(&frame.tokens.colors.surface_raised).unwrap(), rgba(&frame.tokens.colors.application_background).unwrap(), ]; - let minimum = if to.accessibility.increase_contrast { 7.0 } else { 4.5 }; + let minimum = if to.accessibility.increase_contrast { + 7.0 + } else { + 4.5 + }; frame.tokens.colors.text_primary = hex(readable_across( rgba(&preferred.tokens.colors.text_primary).unwrap(), &body, @@ -264,7 +292,10 @@ impl ResolvedTheme { .into_iter() .map(luminance) .fold(f64::INFINITY, f64::min); - let maximum = [base, raised, app].into_iter().map(luminance).fold(0., f64::max); + let maximum = [base, raised, app] + .into_iter() + .map(luminance) + .fold(0., f64::max); if ((minimum + 0.05) / 0.05).max(1.05 / (maximum + 0.05)) < 4.5 { frame.tokens.colors.surface_raised = frame.tokens.colors.surface_base.clone(); frame.tokens.colors.application_background = frame.tokens.colors.surface_base.clone(); diff --git a/crates/ferese-theme-model/src/lib.rs b/crates/ferese-theme-model/src/lib.rs index ed68ee3..dd4f0c2 100644 --- a/crates/ferese-theme-model/src/lib.rs +++ b/crates/ferese-theme-model/src/lib.rs @@ -155,7 +155,11 @@ pub struct ResolvedTheme { } /// Accessibility takes precedence over a panel's authored opacity. -pub fn effective_panel_opacity(inherited: f32, custom: Option, reduce_transparency: bool) -> f32 { +pub fn effective_panel_opacity( + inherited: f32, + custom: Option, + reduce_transparency: bool, +) -> f32 { if reduce_transparency { 1.0 } else { diff --git a/crates/ferese-theme/src/button.rs b/crates/ferese-theme/src/button.rs index dd48234..863990e 100644 --- a/crates/ferese-theme/src/button.rs +++ b/crates/ferese-theme/src/button.rs @@ -13,7 +13,11 @@ pub(crate) enum State { Disabled, } -pub(crate) fn accent_button_style(theme: &cosmic::Theme, state: State, focused: bool) -> button::Style { +pub(crate) fn accent_button_style( + theme: &cosmic::Theme, + state: State, + focused: bool, +) -> button::Style { let native = theme.cosmic(); let component = &native.accent_button; let fill: Color = match state { diff --git a/crates/ferese-theme/src/calendar.rs b/crates/ferese-theme/src/calendar.rs index ced2517..c8ec9ca 100644 --- a/crates/ferese-theme/src/calendar.rs +++ b/crates/ferese-theme/src/calendar.rs @@ -61,7 +61,11 @@ fn grid_with_density<'a, M: Clone + 'a>( navigation: [Element<'a, M>; 3], compact: bool, ) -> Element<'a, M> { - let cell_width = if compact { Length::Fill } else { Length::Fixed(30.) }; + let cell_width = if compact { + Length::Fill + } else { + Length::Fixed(30.) + }; let cell_height = if compact { 20. } else { 25. }; let spacing = if compact { 2. } else { 3. }; let months = [ @@ -96,12 +100,17 @@ fn grid_with_density<'a, M: Clone + 'a>( .spacing(2); let mut grid = column::with_capacity(8) .spacing(spacing) - .width(if compact { Length::Fill } else { Length::Shrink }) + .width(if compact { + Length::Fill + } else { + Length::Shrink + }) .push(header); - let mut weekdays = - row::with_capacity(7) - .spacing(spacing) - .width(if compact { Length::Fill } else { Length::Shrink }); + let mut weekdays = row::with_capacity(7).spacing(spacing).width(if compact { + Length::Fill + } else { + Length::Shrink + }); for label in ["M", "T", "W", "T", "F", "S", "S"] { weekdays = weekdays.push( container( @@ -115,27 +124,33 @@ fn grid_with_density<'a, M: Clone + 'a>( } grid = grid.push(weekdays); for week in 0..(start + days).div_ceil(7) { - let mut dates = - row::with_capacity(7) - .spacing(spacing) - .width(if compact { Length::Fill } else { Length::Shrink }); + let mut dates = row::with_capacity(7).spacing(spacing).width(if compact { + Length::Fill + } else { + Length::Shrink + }); for weekday in 0..7 { let cell = week * 7 + weekday; - let day = cell.checked_sub(start).map(|day| day + 1).filter(|day| *day <= days); - let is_today = year == today.year() && month == today.month() && day == Some(today.day() as usize); + let day = cell + .checked_sub(start) + .map(|day| day + 1) + .filter(|day| *day <= days); + let is_today = + year == today.year() && month == today.month() && day == Some(today.day() as usize); let label = day.map_or(String::new(), |day| day.to_string()); dates = dates.push( - container(crate::text(label, font).size(12).class(theme::Text::Color(if is_today { - primary - } else { - muted - }))) + container( + crate::text(label, font) + .size(12) + .class(theme::Text::Color(if is_today { primary } else { muted })), + ) .width(cell_width) .height(cell_height) .center_x(cell_width) .center_y(cell_height) .class(theme::Container::custom(move |_| container::Style { - background: is_today.then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))), + background: is_today + .then_some(Background::Color(palette.accent.scale_alpha(0.2 * opacity))), border: Border { shape: BorderShape::Continuous, radius: palette.radius.min(12.).into(), @@ -155,9 +170,18 @@ mod tests { #[test] fn clock_uses_lowercase_date_and_twelve_hour_time() { for (stamp, expected) in [ - ("2026-09-28T21:32:00+01:00[Africa/Lagos]", "28 sept, 9:32 pm"), - ("2026-09-28T00:05:00+01:00[Africa/Lagos]", "28 sept, 12:05 am"), - ("2026-09-28T12:00:00+01:00[Africa/Lagos]", "28 sept, 12:00 pm"), + ( + "2026-09-28T21:32:00+01:00[Africa/Lagos]", + "28 sept, 9:32 pm", + ), + ( + "2026-09-28T00:05:00+01:00[Africa/Lagos]", + "28 sept, 12:05 am", + ), + ( + "2026-09-28T12:00:00+01:00[Africa/Lagos]", + "28 sept, 12:00 pm", + ), ] { assert_eq!(super::format_bar_time(&stamp.parse().unwrap()), expected); } @@ -169,7 +193,10 @@ mod tests { let (first, start, days) = super::month(february, 0); assert_eq!((first.year(), first.month(), start, days), (2024, 2, 3, 29)); let (first, start, days) = super::month(february, -2); - assert_eq!((first.year(), first.month(), start, days), (2023, 12, 4, 31)); + assert_eq!( + (first.year(), first.month(), start, days), + (2023, 12, 4, 31) + ); let june = jiff::civil::Date::new(2025, 6, 1).unwrap(); let (_, start, days) = super::month(june, 0); assert_eq!((start, days), (6, 30)); diff --git a/crates/ferese-theme/src/contrast.rs b/crates/ferese-theme/src/contrast.rs index f65ac11..60f2622 100644 --- a/crates/ferese-theme/src/contrast.rs +++ b/crates/ferese-theme/src/contrast.rs @@ -76,12 +76,27 @@ pub fn apply(native: &mut cosmic::cosmic_theme::Theme, preferred: Color) { let surface: Color = native.primary(false).base.into(); for component in [&mut native.accent, &mut native.accent_button] { let (base, on) = accent_pair(composite(component.base.into(), surface), preferred); - component.hover = rgba(readable_fill(composite(component.hover.into(), surface), base, on)); - component.pressed = rgba(readable_fill(composite(component.pressed.into(), surface), base, on)); - component.selected = rgba(readable_fill(composite(component.selected.into(), surface), base, on)); + component.hover = rgba(readable_fill( + composite(component.hover.into(), surface), + base, + on, + )); + component.pressed = rgba(readable_fill( + composite(component.pressed.into(), surface), + base, + on, + )); + component.selected = rgba(readable_fill( + composite(component.selected.into(), surface), + base, + on, + )); component.base = rgba(base); component.on = rgba(on); - component.selected_text = rgba(foreground(composite(component.selected.into(), surface), on)); + component.selected_text = rgba(foreground( + composite(component.selected.into(), surface), + on, + )); component.disabled = rgba(composite(Color { a: 0.5, ..base }, surface)); component.on_disabled = rgba(foreground(composite(Color { a: 0.5, ..base }, surface), on)); } @@ -125,7 +140,10 @@ mod tests { let preferred = Color::from_rgb8(240, 242, 246); assert_eq!(foreground(Color::BLACK, preferred), preferred); assert_eq!(foreground(Color::WHITE, preferred), Color::BLACK); - assert_eq!(foreground(Color::from_rgb8(61, 123, 230), preferred), Color::BLACK); + assert_eq!( + foreground(Color::from_rgb8(61, 123, 230), preferred), + Color::BLACK + ); } #[test] @@ -153,7 +171,12 @@ mod tests { .build(); apply(&mut native, preferred); let theme = cosmic::Theme::custom(std::sync::Arc::new(native)); - for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] { + for state in [ + State::Active, + State::Hovered, + State::Pressed, + State::Disabled, + ] { let actual = accent_button_style(&theme, state, false); assert_eq!(actual.text_color, Some(preferred)); assert_eq!(actual.icon_color, Some(preferred)); @@ -201,11 +224,20 @@ mod tests { for fill in [component.base, component.hover, component.pressed] { assert!(contrast(fill.into(), component.on.into()) >= 4.5); } - assert!(contrast(component.selected.into(), component.selected_text.into()) >= 4.5); - assert!(contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5); + assert!( + contrast(component.selected.into(), component.selected_text.into()) >= 4.5 + ); + assert!( + contrast(component.disabled.into(), component.on_disabled.into()) >= 4.5 + ); } let theme = cosmic::Theme::custom(std::sync::Arc::new(native)); - for state in [State::Active, State::Hovered, State::Pressed, State::Disabled] { + for state in [ + State::Active, + State::Hovered, + State::Pressed, + State::Disabled, + ] { let style = accent_button_style(&theme, state, false); let Some(Background::Color(fill)) = style.background else { panic!("No fill") diff --git a/crates/ferese-theme/src/controls.rs b/crates/ferese-theme/src/controls.rs index df97eff..33bfc7c 100644 --- a/crates/ferese-theme/src/controls.rs +++ b/crates/ferese-theme/src/controls.rs @@ -91,7 +91,11 @@ pub(crate) fn switch_colors(palette: Palette, enabled: bool, hovered: bool) -> ( let track = if enabled { palette.accent } else { - mix(palette.card, palette.text, if hovered { 0.18 } else { 0.12 }) + mix( + palette.card, + palette.text, + if hovered { 0.18 } else { 0.12 }, + ) }; let thumb = crate::foreground(track, palette.text); (track, thumb) @@ -148,7 +152,13 @@ pub fn select<'a, M: 'a>( })) } -fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32, focus_visible: bool) -> theme::Button { +fn styled_button( + p: Palette, + selected: bool, + navigation: bool, + opacity: f32, + focus_visible: bool, +) -> theme::Button { let style = move |hover: bool, focused: bool| { let background = if selected { mix(p.sidebar, p.accent, if hover { 0.24 } else { 0.17 }) @@ -177,7 +187,12 @@ fn styled_button(p: Palette, selected: bool, navigation: bool, opacity: f32, foc }; let fill = if selected { - p.selected_background(if hover { 0.24 } else { 0.17 }, background.a, on, background) + p.selected_background( + if hover { 0.24 } else { 0.17 }, + background.a, + on, + background, + ) } else { Background::Color(background) }; @@ -220,7 +235,10 @@ pub fn authentication_button(palette: Palette) -> theme::Button { let candidate = mix(palette.accent, palette.text, interaction); crate::accent_pair(crate::composite(candidate, palette.card), palette.text) } else { - (crate::composite(palette.card, palette.background), palette.muted) + ( + crate::composite(palette.card, palette.background), + palette.muted, + ) }; button::Style { shape: Some(BorderShape::Continuous), @@ -275,7 +293,11 @@ pub fn authentication_input(palette: Palette) -> theme::TextInput { } } -pub fn lock_input(radius: f32, accent: cosmic::iced::Color, surface: cosmic::iced::Color) -> theme::TextInput { +pub fn lock_input( + radius: f32, + accent: cosmic::iced::Color, + surface: cosmic::iced::Color, +) -> theme::TextInput { let appearance = move |focused: bool| widget::text_input::Appearance { shape: Some(BorderShape::Continuous), outline: None, @@ -329,7 +351,9 @@ pub fn shell_button( a: if pressed { 0.20 * opacity } else { - ((if selected { 0.14 } else { 0.0 }) + progress * if selected { 0.02 } else { 0.08 }) * opacity + ((if selected { 0.14 } else { 0.0 }) + + progress * if selected { 0.02 } else { 0.08 }) + * opacity }, ..foreground })), @@ -349,7 +373,12 @@ pub fn surface_appearance(background: Color, radius: f32) -> container::Style { } } -pub fn notification_button(foreground: Color, hover: Color, radius: f32, filled: bool) -> theme::Button { +pub fn notification_button( + foreground: Color, + hover: Color, + radius: f32, + filled: bool, +) -> theme::Button { let style = move |active: bool| button::Style { shape: Some(BorderShape::Continuous), outline: None, diff --git a/crates/ferese-theme/src/gallery.rs b/crates/ferese-theme/src/gallery.rs index a40faeb..7c1a972 100644 --- a/crates/ferese-theme/src/gallery.rs +++ b/crates/ferese-theme/src/gallery.rs @@ -38,11 +38,14 @@ pub fn tile( .or(family.dark.as_ref()) .map(|palette| crate::parse_color(&palette.text).unwrap()) .unwrap_or(palette.text); - let mut caption = row([]).spacing(6).align_y(cosmic::iced::Alignment::Center).push( - crate::text(family.name.clone(), font) - .size(12) - .class(cosmic::theme::Text::Color(label_color)), - ); + let mut caption = row([]) + .spacing(6) + .align_y(cosmic::iced::Alignment::Center) + .push( + crate::text(family.name.clone(), font) + .size(12) + .class(cosmic::theme::Text::Color(label_color)), + ); if family.light.is_none() || family.dark.is_none() { caption = caption.push( crate::text( @@ -129,14 +132,21 @@ fn preview_with_radius( radius: f32, ) -> cosmic::widget::icon::Icon { use cosmic::widget::icon; - let key = format!("{}/{variant:?}/{active:?}/{selected}/{}", family.id, radius.to_bits()); - static CACHE: std::sync::OnceLock>> = - std::sync::OnceLock::new(); + let key = format!( + "{}/{variant:?}/{active:?}/{selected}/{}", + family.id, + radius.to_bits() + ); + static CACHE: std::sync::OnceLock< + std::sync::Mutex>, + > = std::sync::OnceLock::new(); let cache = CACHE.get_or_init(Default::default); let mut cache = cache.lock().unwrap(); if cache.get(&key).is_none_or(|(cached, _)| cached != family) { - let handle = icon::from_svg_bytes(svg_with_radius(family, variant, active, selected, radius).into_bytes()) - .symbolic(false); + let handle = icon::from_svg_bytes( + svg_with_radius(family, variant, active, selected, radius).into_bytes(), + ) + .symbolic(false); cache.insert(key.clone(), (family.clone(), handle)); } let handle = cache[&key].1.clone(); @@ -164,7 +174,12 @@ fn desktop(p: &Palette) -> String { ) } -pub fn svg(family: &Family, variant: Option, active: Option, selected: bool) -> String { +pub fn svg( + family: &Family, + variant: Option, + active: Option, + selected: bool, +) -> String { svg_with_radius(family, variant, active, selected, 10.) } @@ -180,9 +195,12 @@ fn svg_with_radius( "##, ); - let outline = - cosmic::iced::border::Outline::new([0., 0., 200., 126.], [f64::from(radius); 4], BorderShape::Continuous) - .expect("finite preview dimensions"); + let outline = cosmic::iced::border::Outline::new( + [0., 0., 200., 126.], + [f64::from(radius); 4], + BorderShape::Continuous, + ) + .expect("finite preview dimensions"); // Rasterized once per cached preview; no shaped rendering layer per tile. let points = outline.polygon(0.02).expect("positive contour tolerance"); result.push_str("( }) } -pub fn group<'a, M: Clone + 'a>(content: impl Into>, id: Id, label: &str) -> Element<'a, M> { +pub fn group<'a, M: Clone + 'a>( + content: impl Into>, + id: Id, + label: &str, +) -> Element<'a, M> { Element::new(Radio { content: content.into(), id, @@ -366,18 +388,36 @@ impl Widget for Radio<'_, M> { fn size(&self) -> Size { self.content.as_widget().size() } - fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { self.content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } - fn operate(&mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>) { + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation<()>, + ) { if self.group { - self.content - .as_widget_mut() - .operate(&mut tree.children[0], layout, renderer, operation); + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + operation, + ); } else if self.tab_stop { - operation.focusable(Some(&self.id), layout.bounds(), tree.state.downcast_mut::()); + operation.focusable( + Some(&self.id), + layout.bounds(), + tree.state.downcast_mut::(), + ); } else { tree.state.downcast_mut::().0 = false; } @@ -398,19 +438,25 @@ impl Widget for Radio<'_, M> { if !iced_accessibility::IdEq::eq(&id.0, &self.id.0) { return; } - if matches!(request.action, accesskit::Action::Focus | accesskit::Action::Click) - && let Some(message) = self - .navigate - .as_ref() - .and_then(|navigate| navigate(&keyboard::key::Named::Enter)) + if matches!( + request.action, + accesskit::Action::Focus | accesskit::Action::Click + ) && let Some(message) = self + .navigate + .as_ref() + .and_then(|navigate| navigate(&keyboard::key::Named::Enter)) { shell.publish(message); shell.capture_event(); return; } } - if matches!(event, Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))) { - tree.state.downcast_mut::().0 = self.tab_stop && cursor.is_over(layout.bounds()); + if matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) { + tree.state.downcast_mut::().0 = + self.tab_stop && cursor.is_over(layout.bounds()); } } if !self.group && self.tab_stop && tree.state.downcast_ref::().0 { @@ -460,9 +506,15 @@ impl Widget for Radio<'_, M> { cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.content - .as_widget() - .draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); if !self.group && self.tab_stop && tree.state.downcast_ref::().0 { use renderer::Renderer as _; renderer.fill_quad( @@ -489,16 +541,26 @@ impl Widget for Radio<'_, M> { viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.content - .as_widget() - .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer) + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn a11y_nodes(&self, layout: Layout<'_>, tree: &Tree, cursor: mouse::Cursor) -> A11yTree { - let children = self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor); + let children = self + .content + .as_widget() + .a11y_nodes(layout, &tree.children[0], cursor); if self.group { let mut node = accesskit::Node::new(accesskit::Role::RadioGroup); node.set_label(self.label.clone()); - A11yTree::node_with_child_tree(iced_accessibility::A11yNode::new(node, self.id.clone()), children) + A11yTree::node_with_child_tree( + iced_accessibility::A11yNode::new(node, self.id.clone()), + children, + ) } else { let mut children = children; for node in children.root_mut() { diff --git a/crates/ferese-theme/src/icons.rs b/crates/ferese-theme/src/icons.rs index 355df5d..fd43384 100644 --- a/crates/ferese-theme/src/icons.rs +++ b/crates/ferese-theme/src/icons.rs @@ -42,7 +42,8 @@ pub const BATTERY_75: &[u8] = include_bytes!("../assets/icons/status/battery-75. pub const BATTERY_CHARGING: &[u8] = include_bytes!("../assets/icons/status/battery-charging.svg"); pub const BATTERY_EMPTY: &[u8] = include_bytes!("../assets/icons/status/battery-empty.svg"); pub const BATTERY_FULL: &[u8] = include_bytes!("../assets/icons/status/battery-full.svg"); -pub const BLUETOOTH_CONNECTED: &[u8] = include_bytes!("../assets/icons/status/bluetooth-connected.svg"); +pub const BLUETOOTH_CONNECTED: &[u8] = + include_bytes!("../assets/icons/status/bluetooth-connected.svg"); pub const BLUETOOTH_OFF: &[u8] = include_bytes!("../assets/icons/status/bluetooth-off.svg"); pub const BLUETOOTH_ON: &[u8] = include_bytes!("../assets/icons/status/bluetooth-on.svg"); pub const BRIGHTNESS: &[u8] = include_bytes!("../assets/icons/status/brightness.svg"); @@ -95,7 +96,10 @@ pub fn tinted(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon } pub fn symbolic(source: &'static [u8], size: u16) -> icon::Icon { - icon::from_svg_bytes(source).symbolic(true).icon().size(size) + icon::from_svg_bytes(source) + .symbolic(true) + .icon() + .size(size) } const TINT_CACHE_ENTRIES: usize = 64; @@ -141,7 +145,8 @@ impl TintCache { let bytes = svg.capacity(); let handle = icon::from_svg_bytes(svg).symbolic(false); if bytes <= TINT_CACHE_BYTES { - while self.entries.len() >= TINT_CACHE_ENTRIES || self.bytes + bytes > TINT_CACHE_BYTES { + while self.entries.len() >= TINT_CACHE_ENTRIES || self.bytes + bytes > TINT_CACHE_BYTES + { self.bytes -= self.entries.pop_front().unwrap().bytes; } self.bytes += bytes; @@ -176,8 +181,8 @@ pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Col .size(size) .width(Length::Fixed(f32::from(size) * aspect)) .content_fit(ContentFit::Contain) - .class(theme::Svg::custom(move |_| cosmic::iced::widget::svg::Style { - color: None, + .class(theme::Svg::custom(move |_| { + cosmic::iced::widget::svg::Style { color: None } })) } @@ -232,10 +237,22 @@ mod tests { }; let first = cache.get(super::BATTERY_50, foreground, accent); let second = cache.get(super::BATTERY_50, foreground, accent); - assert_eq!(bytes(&first), bytes(&second), "rebuilds must reuse the same allocation"); - let changed_accent = cache.get(super::BATTERY_50, foreground, Color::from_rgb8(42, 90, 180)); + assert_eq!( + bytes(&first), + bytes(&second), + "rebuilds must reuse the same allocation" + ); + let changed_accent = + cache.get(super::BATTERY_50, foreground, Color::from_rgb8(42, 90, 180)); assert_ne!(bytes(&first), bytes(&changed_accent)); - let translucent = cache.get(super::BATTERY_50, Color { a: 0.5, ..foreground }, accent); + let translucent = cache.get( + super::BATTERY_50, + Color { + a: 0.5, + ..foreground + }, + accent, + ); assert_ne!(bytes(&first), bytes(&translucent)); assert_eq!(cache.entries.len(), 3); } @@ -252,7 +269,10 @@ mod tests { assert!(cache.entries.len() <= super::TINT_CACHE_ENTRIES); assert!(cache.bytes <= super::TINT_CACHE_BYTES); } - assert_eq!(cache.entries.back().unwrap().colors[3], (511_f32 / 512.).to_bits()); + assert_eq!( + cache.entries.back().unwrap().colors[3], + (511_f32 / 512.).to_bits() + ); } #[test] diff --git a/crates/ferese-theme/src/menus.rs b/crates/ferese-theme/src/menus.rs index cf7b2e5..ab5f0e5 100644 --- a/crates/ferese-theme/src/menus.rs +++ b/crates/ferese-theme/src/menus.rs @@ -19,9 +19,12 @@ pub fn row<'a, M: 'a>( trailing: Element<'a, M>, font: cosmic::font::Font, ) -> Element<'a, M> { - iced_row![crate::text(label, font).width(Length::Fill).size(14), trailing] - .align_y(Alignment::Center) - .into() + iced_row![ + crate::text(label, font).width(Length::Fill).size(14), + trailing + ] + .align_y(Alignment::Center) + .into() } pub fn heading<'a, M: 'a>( @@ -34,7 +37,12 @@ pub fn heading<'a, M: 'a>( .align_y(Alignment::Center) } -pub fn badge<'a, M: 'a>(content: Element<'a, M>, accent: Color, opacity: f32, radius: f32) -> Element<'a, M> { +pub fn badge<'a, M: 'a>( + content: Element<'a, M>, + accent: Color, + opacity: f32, + radius: f32, +) -> Element<'a, M> { container(content) .width(36) .height(36) @@ -47,7 +55,12 @@ pub fn badge<'a, M: 'a>(content: Element<'a, M>, accent: Color, opacity: f32, ra .into() } -pub fn section<'a, M: 'a>(content: Element<'a, M>, foreground: Color, opacity: f32, radius: f32) -> Element<'a, M> { +pub fn section<'a, M: 'a>( + content: Element<'a, M>, + foreground: Color, + opacity: f32, + radius: f32, +) -> Element<'a, M> { container(content) .padding(10) .width(Length::Fill) @@ -73,7 +86,10 @@ pub fn section_label<'a>( pub fn separator<'a, M: 'a>(foreground: Color, opacity: f32) -> Element<'a, M> { container(cosmic::widget::Space::new().width(Length::Fill).height(1)) - .class(crate::controls::surface(foreground.scale_alpha(0.12 * opacity), 0.)) + .class(crate::controls::surface( + foreground.scale_alpha(0.12 * opacity), + 0., + )) .into() } @@ -85,7 +101,10 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) - .width(16) .height(16) .class(crate::controls::surface( - Color { a: opacity, ..thumb }, + Color { + a: opacity, + ..thumb + }, crate::inner_radius(track_radius, padding), )); container(knob) @@ -97,7 +116,10 @@ pub fn switch<'a, M: 'a>(enabled: bool, palette: crate::Palette, opacity: f32) - } else { cosmic::iced::alignment::Horizontal::Left }) - .class(crate::controls::surface(track.scale_alpha(opacity), track_radius)) + .class(crate::controls::surface( + track.scale_alpha(opacity), + track_radius, + )) .into() } diff --git a/crates/ferese-theme/src/palette.rs b/crates/ferese-theme/src/palette.rs index e8daa7e..a486626 100644 --- a/crates/ferese-theme/src/palette.rs +++ b/crates/ferese-theme/src/palette.rs @@ -27,12 +27,16 @@ impl Palette { sidebar: color(&theme.tokens.colors.surface_base), card: color(&theme.tokens.colors.surface_raised), accent: color(&theme.tokens.colors.accent), - accent_gradient: theme.tokens.focus_ring.effective_gradient().map(|gradient| { - // Iced's zero angle points up; theme angles start left-to-right. - Linear::new((gradient.angle.rem_euclid(360.) as f32 + 90.).to_radians()) - .add_stop(0., color(&gradient.from)) - .add_stop(1., color(&gradient.to)) - }), + accent_gradient: theme + .tokens + .focus_ring + .effective_gradient() + .map(|gradient| { + // Iced's zero angle points up; theme angles start left-to-right. + Linear::new((gradient.angle.rem_euclid(360.) as f32 + 90.).to_radians()) + .add_stop(0., color(&gradient.from)) + .add_stop(1., color(&gradient.to)) + }), on_accent: color(&theme.tokens.colors.on_accent), text: color(&theme.tokens.colors.text_primary), muted: color(&theme.tokens.colors.text_muted), @@ -63,13 +67,19 @@ impl Palette { }; for stop in gradient.stops.iter_mut().flatten() { - stop.color = mix(self.sidebar, crate::composite(stop.color, self.sidebar), strength); + stop.color = mix( + self.sidebar, + crate::composite(stop.color, self.sidebar), + strength, + ); } // Bound every interpolated color, rather than sampling a few points. // Adjust both stops together so one foreground stays readable. let minimum = if self.high_contrast { 7. } else { 4.5 }; - let shade = if crate::contrast(Color::BLACK, foreground) > crate::contrast(Color::WHITE, foreground) { + let shade = if crate::contrast(Color::BLACK, foreground) + > crate::contrast(Color::WHITE, foreground) + { Color::BLACK } else { Color::WHITE @@ -153,7 +163,11 @@ pub fn parse_color(value: &str) -> Option { } pub fn mix(a: Color, b: Color, t: f32) -> Color { - Color::from_rgb(a.r + (b.r - a.r) * t, a.g + (b.g - a.g) * t, a.b + (b.b - a.b) * t) + Color::from_rgb( + a.r + (b.r - a.r) * t, + a.g + (b.g - a.g) * t, + a.b + (b.b - a.b) * t, + ) } pub fn surface_shade(base: Color) -> Color { @@ -223,14 +237,23 @@ mod tests { let native = palette.native_theme(); for opacity in [1., 0.5] { let class = crate::controls::material_button_style(palette, true, opacity); - for style in [native.active(false, true, &class), native.hovered(false, true, &class)] { - let Some(Background::Gradient(cosmic::iced::Gradient::Linear(gradient))) = style.background else { + for style in [ + native.active(false, true, &class), + native.hovered(false, true, &class), + ] { + let Some(Background::Gradient(cosmic::iced::Gradient::Linear(gradient))) = + style.background + else { panic!("{} selected control lost its gradient", preset.id); }; let from = gradient.stops[0].unwrap().color; let to = gradient.stops[1].unwrap().color; assert_eq!(from.a, to.a); - assert!(if opacity == 1. { from.a == 1. } else { from.a < 1. }); + assert!(if opacity == 1. { + from.a == 1. + } else { + from.a < 1. + }); for step in 0..=32 { let fill = crate::composite( Color { @@ -239,7 +262,11 @@ mod tests { }, palette.sidebar, ); - assert!(crate::contrast(fill, style.text_color.unwrap()) >= 4.5, "{}", preset.id); + assert!( + crate::contrast(fill, style.text_color.unwrap()) >= 4.5, + "{}", + preset.id + ); } } } @@ -257,7 +284,9 @@ mod tests { let mut resolved = ferese_config::theme::default_theme(); resolved.tokens.focus_ring.gradient.as_mut().unwrap().angle = 0.; let palette = Palette::from_resolved(&resolved); - assert!((palette.accent_gradient.unwrap().angle.0 - std::f32::consts::FRAC_PI_2).abs() < 0.001); + assert!( + (palette.accent_gradient.unwrap().angle.0 - std::f32::consts::FRAC_PI_2).abs() < 0.001 + ); resolved.tokens.focus_ring.style = ferese_theme_model::PaintStyle::Solid; assert!(Palette::from_resolved(&resolved).accent_gradient.is_none()); resolved.tokens.focus_ring.style = ferese_theme_model::PaintStyle::Auto; diff --git a/crates/ferese-theme/src/panel.rs b/crates/ferese-theme/src/panel.rs index 17f9784..7c440c5 100644 --- a/crates/ferese-theme/src/panel.rs +++ b/crates/ferese-theme/src/panel.rs @@ -89,7 +89,11 @@ impl Widget for Adaptive<'_, M> { } fn children(&self) -> Vec { std::iter::once(widget::Tree::new(&self.content)) - .chain(self.samples.iter().map(|sample| widget::Tree::new(&sample.view))) + .chain( + self.samples + .iter() + .map(|sample| widget::Tree::new(&sample.view)), + ) .collect() } fn diff(&mut self, tree: &mut widget::Tree) { @@ -117,15 +121,22 @@ impl Widget for Adaptive<'_, M> { let mut measurements: Vec = Vec::new(); // Measure preferred content before imposing the panel's width. Otherwise // a clipped wide control can look as small as its compact alternative. - let measurement_limits = layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, limits.max().height)); + let measurement_limits = + layout::Limits::new(Size::ZERO, Size::new(f32::INFINITY, limits.max().height)); for (index, sample) in self.samples.iter_mut().enumerate() { - let node = sample - .view - .as_widget_mut() - .layout(&mut tree.children[index + 1], renderer, &measurement_limits); + let node = sample.view.as_widget_mut().layout( + &mut tree.children[index + 1], + renderer, + &measurement_limits, + ); let width = node.size().width; - if let Some(measurement) = measurements.iter_mut().find(|measurement| measurement.id == sample.id) { - measurement.alternatives.push((sample.representation, width)); + if let Some(measurement) = measurements + .iter_mut() + .find(|measurement| measurement.id == sample.id) + { + measurement + .alternatives + .push((sample.representation, width)); } else { measurements.push(Measurement { id: sample.id.clone(), @@ -162,7 +173,9 @@ impl Widget for Adaptive<'_, M> { tree: &widget::Tree, cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { - self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor) + self.content + .as_widget() + .a11y_nodes(layout, &tree.children[0], cursor) } fn update( &mut self, @@ -186,8 +199,10 @@ impl Widget for Adaptive<'_, M> { viewport, ); let state = tree.state.downcast_mut::(); - if matches!(event, Event::Window(cosmic::iced::window::Event::RedrawRequested(_))) - && state.published.as_ref() != Some(&self.resolution) + if matches!( + event, + Event::Window(cosmic::iced::window::Event::RedrawRequested(_)) + ) && state.published.as_ref() != Some(&self.resolution) { state.published = Some(self.resolution.clone()); shell.publish((self.changed)(self.resolution.clone())); @@ -203,9 +218,15 @@ impl Widget for Adaptive<'_, M> { cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.content - .as_widget() - .draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); } fn mouse_interaction( &self, @@ -215,9 +236,13 @@ impl Widget for Adaptive<'_, M> { viewport: &Rectangle, renderer: &cosmic::Renderer, ) -> mouse::Interaction { - self.content - .as_widget() - .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer) + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn operate( &mut self, @@ -238,9 +263,13 @@ impl Widget for Adaptive<'_, M> { viewport: &Rectangle, translation: Vector, ) -> Option> { - self.content - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, viewport, translation) + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } } @@ -294,9 +323,10 @@ mod tests { for opacity in [0., 0.5, 1.] { for border in [false, true] { renderer.reset(bounds); - let group = cosmic::widget::container(widgets::Space::new().width(40).height(40)).class( - cosmic::theme::Container::custom(move |_| group_border(Color::WHITE, 8., border)), - ); + let group = cosmic::widget::container(widgets::Space::new().width(40).height(40)) + .class(cosmic::theme::Container::custom(move |_| { + group_border(Color::WHITE, 8., border) + })); let mut view: Element<'_, ()> = cosmic::widget::container(group) .class(cosmic::theme::Container::custom(move |_| { crate::controls::surface_appearance( @@ -309,9 +339,11 @@ mod tests { })) .into(); let mut tree = widget::Tree::new(&view); - let node = - view.as_widget_mut() - .layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size())); + let node = view.as_widget_mut().layout( + &mut tree, + &renderer, + &layout::Limits::new(Size::ZERO, bounds.size()), + ); view.as_widget().draw( &tree, &mut renderer, @@ -325,7 +357,8 @@ mod tests { mouse::Cursor::Unavailable, &bounds, ); - let pixels = Headless::screenshot(&mut renderer, Size::new(40, 40), 1., Color::TRANSPARENT); + let pixels = + Headless::screenshot(&mut renderer, Size::new(40, 40), 1., Color::TRANSPARENT); let alpha = pixels[(20 * 40 + 20) * 4 + 3]; assert!( i16::from(alpha).abs_diff((opacity * 255.).round() as i16) <= 1, @@ -370,10 +403,12 @@ mod tests { id: id.clone(), representation, minimum: None, - view: button::custom(text(label).wrapping(cosmic::iced::widget::text::Wrapping::None)) - .height(28) - .on_press(Probe::MeasurementClick) - .into(), + view: button::custom( + text(label).wrapping(cosmic::iced::widget::text::Wrapping::None), + ) + .height(28) + .on_press(Probe::MeasurementClick) + .into(), }) .chain(std::iter::once(Sample { id: ItemId("_overflow".into()), @@ -413,14 +448,23 @@ mod tests { let limits = layout::Limits::new(Size::ZERO, viewport.size()); let node = view.as_widget_mut().layout(tree, &renderer, &limits); let built = builds.get(); - assert_eq!(built, 2, "initial measurement must replace the unallocated content"); + assert_eq!( + built, 2, + "initial measurement must replace the unallocated content" + ); view.as_widget_mut().layout(tree, &renderer, &limits); - assert_eq!(builds.get(), built, "unchanged allocation must reuse its controls"); + assert_eq!( + builds.get(), + built, + "unchanged allocation must reuse its controls" + ); let mut messages = Vec::new(); for _ in 0..4 { view.as_widget_mut().update( tree, - &Event::Window(cosmic::iced::window::Event::RedrawRequested(std::time::Instant::now())), + &Event::Window(cosmic::iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )), Layout::new(&node), mouse::Cursor::Unavailable, &renderer, diff --git a/crates/ferese-theme/src/panel_surface.rs b/crates/ferese-theme/src/panel_surface.rs index 309890e..0a25ccc 100644 --- a/crates/ferese-theme/src/panel_surface.rs +++ b/crates/ferese-theme/src/panel_surface.rs @@ -86,7 +86,12 @@ impl widget::Operation for Collect<'_> { } impl Surface<'_, M> { - fn measure(&mut self, tree: &mut widget::Tree, layout: Layout<'_>, renderer: &cosmic::Renderer) { + fn measure( + &mut self, + tree: &mut widget::Tree, + layout: Layout<'_>, + renderer: &cosmic::Renderer, + ) { let mut collector = Collect { targets: &self.options.groups, origin: layout.bounds().position(), @@ -94,17 +99,20 @@ impl Surface<'_, M> { bounds_id: self.options.bounds.as_ref(), bounds: None, }; - self.content - .as_widget_mut() - .operate(&mut tree.children[0], layout, renderer, &mut collector); + self.content.as_widget_mut().operate( + &mut tree.children[0], + layout, + renderer, + &mut collector, + ); let size = layout.bounds().size(); let scene = PixelGrid::new(self.options.scale) .and_then(|grid| { ferese_panel::scene::derive( self.options.parameters, - collector - .bounds - .unwrap_or_else(|| ferese_layout::Rect::new(0., 0., size.width.into(), size.height.into())), + collector.bounds.unwrap_or_else(|| { + ferese_layout::Rect::new(0., 0., size.width.into(), size.height.into()) + }), &collector.groups, grid, ) @@ -162,7 +170,9 @@ impl Widget for Surface<'_, M> { tree: &widget::Tree, cursor: mouse::Cursor, ) -> iced_accessibility::A11yTree { - self.content.as_widget().a11y_nodes(layout, &tree.children[0], cursor) + self.content + .as_widget() + .a11y_nodes(layout, &tree.children[0], cursor) } fn update( &mut self, @@ -185,7 +195,10 @@ impl Widget for Surface<'_, M> { shell, viewport, ); - if matches!(event, Event::Window(cosmic::iced::window::Event::RedrawRequested(_))) && !shell.is_layout_invalid() + if matches!( + event, + Event::Window(cosmic::iced::window::Event::RedrawRequested(_)) + ) && !shell.is_layout_invalid() { self.measure(tree, layout, renderer); (self.present)(&tree.state.downcast_ref::().scene); @@ -250,7 +263,9 @@ impl Widget for Surface<'_, M> { }; frame.stroke( &Path::line(Point::new(0., y), Point::new(bounds.width, y)), - Stroke::default().with_color(self.options.border).with_width(width), + Stroke::default() + .with_color(self.options.border) + .with_width(width), ); } else if self.options.parameters.style == ferese_panel::PanelStyle::Notch || (self.options.parameters.style == ferese_panel::PanelStyle::Morph @@ -264,30 +279,45 @@ impl Widget for Surface<'_, M> { let border = Path::new(|builder| { for contour in contours { if let Some((first, rest)) = contour.split_first() { - builder.move_to(Point::new(first[0] as f32, first[1] as f32)); + builder.move_to(Point::new( + first[0] as f32, + first[1] as f32, + )); for point in rest { - builder.line_to(Point::new(point[0] as f32, point[1] as f32)); + builder.line_to(Point::new( + point[0] as f32, + point[1] as f32, + )); } } } }); frame.stroke( &border, - Stroke::default().with_color(self.options.border).with_width(width), + Stroke::default() + .with_color(self.options.border) + .with_width(width), ); } } else { frame.stroke( &path, - Stroke::default().with_color(self.options.border).with_width(width), + Stroke::default() + .with_color(self.options.border) + .with_width(width), ); } } } for [x1, y1, x2, y2] in &state.scene.dividers { frame.stroke( - &Path::line(Point::new(*x1 as f32, *y1 as f32), Point::new(*x2 as f32, *y2 as f32)), - Stroke::default().with_color(self.options.divider).with_width(hairline), + &Path::line( + Point::new(*x1 as f32, *y1 as f32), + Point::new(*x2 as f32, *y2 as f32), + ), + Stroke::default() + .with_color(self.options.divider) + .with_width(hairline), ); } }); @@ -300,9 +330,15 @@ impl Widget for Surface<'_, M> { // Keep content after cached vector geometry in the renderer's layer order. // Parent-layer text can otherwise be batched underneath the background. renderer.with_layer(*viewport, |renderer| { - self.content - .as_widget() - .draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport); + self.content.as_widget().draw( + &tree.children[0], + renderer, + theme, + style, + layout, + cursor, + viewport, + ); }); } fn mouse_interaction( @@ -313,9 +349,13 @@ impl Widget for Surface<'_, M> { viewport: &Rectangle, renderer: &cosmic::Renderer, ) -> mouse::Interaction { - self.content - .as_widget() - .mouse_interaction(&tree.children[0], layout, cursor, viewport, renderer) + self.content.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) } fn operate( &mut self, @@ -336,9 +376,13 @@ impl Widget for Surface<'_, M> { viewport: &Rectangle, translation: Vector, ) -> Option> { - self.content - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, viewport, translation) + self.content.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } } @@ -353,11 +397,9 @@ mod tests { fn shared_surface_publishes_layout_changes_and_leaves_native_fill_to_the_compositor() { use cosmic::iced::advanced::clipboard; use std::cell::RefCell; - let mut renderer = cosmic::iced::futures::executor::block_on(::new( - Font::default(), - Pixels(14.), - Some("tiny-skia"), - )) + let mut renderer = cosmic::iced::futures::executor::block_on( + ::new(Font::default(), Pixels(14.), Some("tiny-skia")), + ) .unwrap(); let ids = [widget::Id::unique(), widget::Id::unique()]; let mut tree = None; @@ -379,7 +421,10 @@ mod tests { .spacing(20); let published = RefCell::new(Vec::new()); let mut view = frame( - cosmic::widget::container(content).width(200).height(32).into(), + cosmic::widget::container(content) + .width(200) + .height(32) + .into(), Options { parameters: Parameters { style, @@ -403,14 +448,18 @@ mod tests { ); let tree = tree.get_or_insert_with(|| widget::Tree::new(&view)); tree.diff(&mut view); - let node = view - .as_widget_mut() - .layout(tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size())); + let node = view.as_widget_mut().layout( + tree, + &renderer, + &layout::Limits::new(Size::ZERO, viewport.size()), + ); let mut messages = Vec::new(); for _ in 0..3 { view.as_widget_mut().update( tree, - &Event::Window(cosmic::iced::window::Event::RedrawRequested(std::time::Instant::now())), + &Event::Window(cosmic::iced::window::Event::RedrawRequested( + std::time::Instant::now(), + )), Layout::new(&node), mouse::Cursor::Unavailable, &renderer, @@ -425,9 +474,15 @@ mod tests { usize::from(!(style == Style::Floating && width == 40. && native)) ); let scene = published.borrow()[0].clone(); - assert_eq!(scene.input.len(), if style == Style::Floating { 2 } else { 1 }); + assert_eq!( + scene.input.len(), + if style == Style::Floating { 2 } else { 1 } + ); if style == Style::Floating { - assert!(scene.distance([width as f64 + 10., 16.]) > 0., "the gap stays empty"); + assert!( + scene.distance([width as f64 + 10., 16.]) > 0., + "the gap stays empty" + ); } renderer.reset(viewport); view.as_widget().draw( @@ -450,11 +505,9 @@ mod tests { #[test] fn border_coverage_tracks_physical_scale_and_high_contrast() { - let mut renderer = cosmic::iced::futures::executor::block_on(::new( - Font::default(), - Pixels(14.), - Some("tiny-skia"), - )) + let mut renderer = cosmic::iced::futures::executor::block_on( + ::new(Font::default(), Pixels(14.), Some("tiny-skia")), + ) .unwrap(); for scale in [0.75, 1., 1.25, 1.5, 2.] { for high_contrast in [false, true] { @@ -480,9 +533,11 @@ mod tests { ); let mut tree = widget::Tree::new(&view); let bounds = Rectangle::with_size(Size::new(64., 32.)); - let node = - view.as_widget_mut() - .layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size())); + let node = view.as_widget_mut().layout( + &mut tree, + &renderer, + &layout::Limits::new(Size::ZERO, bounds.size()), + ); renderer.reset(bounds); view.as_widget().draw( &tree, @@ -500,7 +555,10 @@ mod tests { let size = Size::new((64. * scale) as u32, (32. * scale) as u32); let pixels = renderer.screenshot(size, scale as f32, Color::TRANSPARENT); let coverage: f64 = (0..size.height) - .map(|y| f64::from(pixels[((y * size.width + size.width / 2) * 4 + 3) as usize]) / 255.) + .map(|y| { + f64::from(pixels[((y * size.width + size.width / 2) * 4 + 3) as usize]) + / 255. + }) .sum(); let expected = if high_contrast { scale.max(1.) } else { 1. }; assert!( @@ -513,11 +571,9 @@ mod tests { #[test] fn the_surface_background_keeps_parent_layer_text_visible() { - let mut renderer = cosmic::iced::futures::executor::block_on(::new( - Font::default(), - Pixels(14.), - Some("tiny-skia"), - )) + let mut renderer = cosmic::iced::futures::executor::block_on( + ::new(Font::default(), Pixels(14.), Some("tiny-skia")), + ) .unwrap(); for scale in [1., 1.25, 1.5] { let content = cosmic::widget::container( @@ -547,9 +603,11 @@ mod tests { ); let mut tree = widget::Tree::new(&view); let viewport = Rectangle::with_size(Size::new(150., 34.)); - let node = - view.as_widget_mut() - .layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, viewport.size())); + let node = view.as_widget_mut().layout( + &mut tree, + &renderer, + &layout::Limits::new(Size::ZERO, viewport.size()), + ); renderer.reset(viewport); view.as_widget().draw( &tree, @@ -569,7 +627,10 @@ mod tests { scale as f32, Color::BLACK, ); - let ink = pixels.chunks_exact(4).filter(|pixel| pixel[0] > 128).count(); + let ink = pixels + .chunks_exact(4) + .filter(|pixel| pixel[0] > 128) + .count(); assert!( ink > 25, "clock text was covered by the surface background at scale {scale}" @@ -605,9 +666,11 @@ mod tests { let b = widget::Id::unique(); let content = cosmic::widget::container(cosmic::widget::row![ cosmic::widget::Space::new().width(4), - cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)).id(a.clone()), + cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)) + .id(a.clone()), cosmic::widget::Space::new().width(4), - cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)).id(b.clone()), + cosmic::widget::container(cosmic::widget::Space::new().width(20).height(16)) + .id(b.clone()), ]) .width(96) .height(64) @@ -640,9 +703,11 @@ mod tests { ); let mut tree = widget::Tree::new(&view); let bounds = Rectangle::new(Point::ORIGIN, Size::new(96., 64.)); - let node = - view.as_widget_mut() - .layout(&mut tree, &renderer, &layout::Limits::new(Size::ZERO, bounds.size())); + let node = view.as_widget_mut().layout( + &mut tree, + &renderer, + &layout::Limits::new(Size::ZERO, bounds.size()), + ); renderer.reset(bounds); view.as_widget().draw( &tree, diff --git a/crates/ferese-theme/src/workspaces.rs b/crates/ferese-theme/src/workspaces.rs index 4f90095..3b082ec 100644 --- a/crates/ferese-theme/src/workspaces.rs +++ b/crates/ferese-theme/src/workspaces.rs @@ -55,7 +55,11 @@ pub fn indicator<'a, M: 'a>( ) -> Element<'a, M> { let progress = progress.clamp(0., 1.); let occupied = workspace.window_count > 0; - let neutral = if occupied { palette.text } else { palette.muted }; + let neutral = if occupied { + palette.text + } else { + palette.muted + }; let foreground = crate::mix(neutral, palette.accent, progress); let content: Element<'a, M> = match style { WorkspaceStyle::Numbers | WorkspaceStyle::Tabs => { @@ -66,7 +70,11 @@ pub fn indicator<'a, M: 'a>( }; crate::text(label, font) - .size(if style == WorkspaceStyle::Tabs { 11 } else { 12 }) + .size(if style == WorkspaceStyle::Tabs { + 11 + } else { + 12 + }) .width(width(style) - 8.) .align_x(cosmic::iced::alignment::Horizontal::Center) .wrapping(cosmic::iced::widget::text::Wrapping::None) @@ -81,7 +89,11 @@ pub fn indicator<'a, M: 'a>( .height(6) .class(theme::Container::custom(move |_| { let mut paint = crate::controls::surface_appearance( - foreground.scale_alpha(if occupied { 0.65 + 0.35 * progress } else { progress }), + foreground.scale_alpha(if occupied { + 0.65 + 0.35 * progress + } else { + progress + }), 3., ); paint.border.width = 1.; @@ -97,15 +109,25 @@ pub fn indicator<'a, M: 'a>( }; let svg = format!( "{back}{}", - if occupied { "" } else { "" } + if occupied { + "" + } else { + "" + } ); - cosmic::widget::icon::from_svg_bytes(crate::icons::tint_svg(svg.as_bytes(), foreground, foreground)) - .symbolic(false) - .icon() - .size(20) - .class(theme::Svg::custom(|_| cosmic::iced::widget::svg::Style { color: None })) - .into() + cosmic::widget::icon::from_svg_bytes(crate::icons::tint_svg( + svg.as_bytes(), + foreground, + foreground, + )) + .symbolic(false) + .icon() + .size(20) + .class(theme::Svg::custom(|_| cosmic::iced::widget::svg::Style { + color: None, + })) + .into() } WorkspaceStyle::AppIcons => { let mut icons = row::with_capacity(2).spacing(3).align_y(Alignment::Center); @@ -128,7 +150,10 @@ pub fn indicator<'a, M: 'a>( let marker = container(Space::new()) .width(10) .height(2) - .class(crate::controls::surface(foreground.scale_alpha(progress), 1.)); + .class(crate::controls::surface( + foreground.scale_alpha(progress), + 1., + )); cosmic::iced::widget::stack![ face, container(marker) @@ -150,7 +175,8 @@ pub fn indicator<'a, M: 'a>( Color::TRANSPARENT } else if progress > 0. { palette.accent.scale_alpha(0.16 * progress) - } else if style == WorkspaceStyle::Tabs || occupied && style == WorkspaceStyle::Numbers { + } else if style == WorkspaceStyle::Tabs || occupied && style == WorkspaceStyle::Numbers + { palette.text.scale_alpha(0.045) } else { Color::TRANSPARENT @@ -177,7 +203,8 @@ pub fn sample<'a, M: 'a>( for (index, count, active) in [(1, 2, true), (2, 1, false), (3, 0, false)] { let apps = if style == WorkspaceStyle::AppIcons && count > 0 { - let mut apps = vec![crate::icons::tinted(crate::icons::SETTINGS, 16, palette.text).into()]; + let mut apps = + vec![crate::icons::tinted(crate::icons::SETTINGS, 16, palette.text).into()]; if count > 1 { apps.push(crate::icons::tinted(crate::icons::FERESE, 16, palette.accent).into()); @@ -210,7 +237,9 @@ pub fn sample<'a, M: 'a>( #[cfg(test)] mod tests { use super::*; - use cosmic::iced::advanced::{Layout, Renderer, layout, renderer, renderer::Headless, widget::Tree}; + use cosmic::iced::advanced::{ + Layout, Renderer, layout, renderer, renderer::Headless, widget::Tree, + }; use cosmic::iced::{Font, Pixels, Rectangle, Size, mouse}; #[test] @@ -241,7 +270,9 @@ mod tests { (3, 1., "Design and development"), ] { let apps = (0..count.min(2)) - .map(|_| crate::icons::tinted(crate::icons::SETTINGS, 16, palette.text).into()) + .map(|_| { + crate::icons::tinted(crate::icons::SETTINGS, 16, palette.text).into() + }) .collect(); let mut view: Element<'_, ()> = indicator( style, @@ -314,7 +345,10 @@ mod tests { active: true, visible_elsewhere: false, }; - assert_eq!(tooltip_label(&workspace), "Workspace 2 · Design · 1 window · Active"); + assert_eq!( + tooltip_label(&workspace), + "Workspace 2 · Design · 1 window · Active" + ); workspace.name = "2".into(); workspace.window_count = 0; workspace.active = false; diff --git a/docs/configuration/activities.md b/docs/configuration/activities.md index ab3f913..efd3008 100644 --- a/docs/configuration/activities.md +++ b/docs/configuration/activities.md @@ -37,6 +37,7 @@ Omitted fields use these built-in defaults. Integer ranges include both endpoint | `owner-ceiling` | Integer 0–255 | `160` | Highest base priority used to rank ordinary activities | | `start-delay-ms` | Integer 0–60000 | `300` | Minimum delay before a task appears; a rule can require a longer delay | | `result-flash-ms` | Integer 0–10000 | `1500` | How long an eligible completion result remains visible | +| `pomodoro` | Nested block | [Defaults below](#pomodoro) | Focus intervals, breaks and completion notifications | ## Sources diff --git a/feresectl/src/activities.rs b/feresectl/src/activities.rs index 63ca880..3d92c10 100644 --- a/feresectl/src/activities.rs +++ b/feresectl/src/activities.rs @@ -98,7 +98,11 @@ fn value( message: &Option, remaining_ms: Option, ) -> Result, Box> { - if usize::from(progress.is_some()) + usize::from(message.is_some()) + usize::from(remaining_ms.is_some()) > 1 { + if usize::from(progress.is_some()) + + usize::from(message.is_some()) + + usize::from(remaining_ms.is_some()) + > 1 + { return Err("choose one activity value".into()); } Ok(progress @@ -111,7 +115,11 @@ fn value( }) })) } -fn publish(socket: &Path, owner: OwnerScope, publish: Publish) -> Result> { +fn publish( + socket: &Path, + owner: OwnerScope, + publish: Publish, +) -> Result> { publish_with_timeout(socket, owner, publish, std::time::Duration::from_secs(2)) } fn publish_with_timeout( @@ -207,18 +215,28 @@ pub fn command(action: &Action, socket: &Path, json_output: bool) -> Result<(), }, message: message.clone(), }, - Action::Cancel { local, handle: token } => Publish::Cancel { + Action::Cancel { + local, + handle: token, + } => Publish::Cancel { local: local.clone(), handle: handle(token)?, }, - Action::Hook { local, command, cwd } => Publish::Hook { + Action::Hook { + local, + command, + cwd, + } => Publish::Hook { local: local.clone(), command: command.clone(), cwd: cwd.clone(), }, Action::List => { let mut stream = std::os::unix::net::UnixStream::connect(socket)?; - return super::output::print(&super::request(&mut stream, "activity-get", json!({}))?, json_output); + return super::output::print( + &super::request(&mut stream, "activity-get", json!({}))?, + json_output, + ); } }; let result = publish(socket, OwnerScope::Parent, request)?; @@ -275,9 +293,12 @@ pub fn run( } let adapter = (configured_adapter || cargo_json) && program == "cargo" - && args - .get(1) - .is_some_and(|operation| matches!(operation.as_str(), "build" | "check" | "test" | "run" | "clippy")); + && args.get(1).is_some_and(|operation| { + matches!( + operation.as_str(), + "build" | "check" | "test" | "run" | "clippy" + ) + }); let mut command = Command::new(program); command.args(&args[1..]); if adapter && !args.iter().any(|arg| arg.starts_with("--message-format")) { @@ -296,7 +317,11 @@ pub fn run( command .stdin(Stdio::inherit()) .stderr(Stdio::inherit()) - .stdout(if adapter { Stdio::piped() } else { Stdio::inherit() }); + .stdout(if adapter { + Stdio::piped() + } else { + Stdio::inherit() + }); // Terminal-generated signals reach both processes in the shell's job group. // The wrapper waits for the actual child result; the child resets inherited // dispositions before exec. SIGTSTP/SIGCONT retain normal job control. @@ -336,10 +361,15 @@ pub fn run( let drain_deadline = output::Deadline::default(); let reader = child.stdout.take().map(|stdout| { let deadline = drain_deadline.clone(); - let updates = publisher.as_ref().map(|publisher| publisher.updates.clone()); + let updates = publisher + .as_ref() + .map(|publisher| publisher.updates.clone()); let (done, finished) = std::sync::mpsc::sync_channel(1); std::thread::spawn(move || { - let mut input = io::BufReader::new(output::Output { pipe: stdout, deadline }); + let mut input = io::BufReader::new(output::Output { + pipe: stdout, + deadline, + }); let mut stdout = io::stdout().lock(); let mut units = 0_u64; let mut last = std::time::Instant::now(); @@ -362,7 +392,9 @@ pub fn run( } } else if message["reason"] == "compiler-artifact" { units = units.saturating_add(1); - } else if message["reason"] != "build-finished" && message["reason"] != "build-script-executed" { + } else if message["reason"] != "build-finished" + && message["reason"] != "build-script-executed" + { let _ = stdout.write_all(&bytes); } if last.elapsed() >= std::time::Duration::from_millis(200) { @@ -373,7 +405,11 @@ pub fn run( } continue; } - if stdout.write_all(&bytes).and_then(|_| stdout.flush()).is_err() { + if stdout + .write_all(&bytes) + .and_then(|_| stdout.flush()) + .is_err() + { break; } } @@ -384,14 +420,18 @@ pub fn run( }); let status = child.wait()?; CHILD.store(0, std::sync::atomic::Ordering::Relaxed); - *drain_deadline.lock().unwrap() = Some(std::time::Instant::now() + std::time::Duration::from_millis(250)); + *drain_deadline.lock().unwrap() = + Some(std::time::Instant::now() + std::time::Duration::from_millis(250)); if let Some(reader) = reader { // Descendants may inherit the pipe after the direct child exits. Drain // buffered output briefly, but never wait for those descendants' EOF. let _ = reader.recv_timeout(std::time::Duration::from_millis(350)); } if let Some(publisher) = publisher { - let result = if matches!(status.signal(), Some(libc::SIGINT | libc::SIGTERM | libc::SIGQUIT)) { + let result = if matches!( + status.signal(), + Some(libc::SIGINT | libc::SIGTERM | libc::SIGQUIT) + ) { State::Cancelled } else if status.success() { State::Done diff --git a/feresectl/src/activities/progress.rs b/feresectl/src/activities/progress.rs index ae672ea..54f26e2 100644 --- a/feresectl/src/activities/progress.rs +++ b/feresectl/src/activities/progress.rs @@ -102,23 +102,34 @@ mod tests { let (release, released) = mpsc::channel(); let server = std::thread::spawn(move || { let (mut first, _) = listener.accept().unwrap(); - first.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); + first + .set_read_timeout(Some(Duration::from_secs(1))) + .unwrap(); let request: Request = read_frame(&mut first).unwrap(); seen.send(()).unwrap(); released.recv_timeout(Duration::from_secs(1)).unwrap(); // The progress timeout can already have elapsed on a loaded host. - let _ = write_frame(&mut first, &Response::success(request.id, serde_json::json!({}))); + let _ = write_frame( + &mut first, + &Response::success(request.id, serde_json::json!({})), + ); let (mut latest, _) = listener.accept().unwrap(); let request: Request = read_frame(&mut latest).unwrap(); - let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap(); + let wire: ferese_ipc::activities::Request = + serde_json::from_value(request.args).unwrap(); assert!( matches!(wire.publish, Publish::Update { value: Some(Value::Text(ref text)), .. } if text == "1000 units compiled") ); - write_frame(&mut latest, &Response::success(request.id, serde_json::json!({}))).unwrap(); + write_frame( + &mut latest, + &Response::success(request.id, serde_json::json!({})), + ) + .unwrap(); seen.send(()).unwrap(); let (mut end, _) = listener.accept().unwrap(); let request: Request = read_frame(&mut end).unwrap(); - let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap(); + let wire: ferese_ipc::activities::Request = + serde_json::from_value(request.args).unwrap(); assert!(matches!( wire.publish, Publish::End { @@ -126,7 +137,11 @@ mod tests { .. } )); - write_frame(&mut end, &Response::success(request.id, serde_json::json!({}))).unwrap(); + write_frame( + &mut end, + &Response::success(request.id, serde_json::json!({})), + ) + .unwrap(); }); let publisher = Publisher::new( socket, diff --git a/feresectl/src/activities/setup.rs b/feresectl/src/activities/setup.rs index c99780f..4347d3f 100644 --- a/feresectl/src/activities/setup.rs +++ b/feresectl/src/activities/setup.rs @@ -8,7 +8,10 @@ fn prepare(source: &str) -> Result> { return Ok(source.into()); } if doc.get("panels").is_none() { - doc.set("panels", serde_json::to_value(vec![panel::Panel::default()])?)?; + doc.set( + "panels", + serde_json::to_value(vec![panel::Panel::default()])?, + )?; } let panels: Vec = serde_json::from_value(doc.get("panels").unwrap().clone())?; panel::validate(&panels)?; @@ -25,7 +28,11 @@ fn prepare(source: &str) -> Result> { continue; } let mut id = "activities".to_string(); - while groups.iter().flat_map(|group| &group.items).any(|item| item.id.0 == id) { + while groups + .iter() + .flat_map(|group| &group.items) + .any(|item| item.id.0 == id) + { id.push('-'); } let item = serde_json::to_value(panel::Item::new(&id, panel::ItemKind::Activities))?; @@ -43,7 +50,10 @@ fn prepare(source: &str) -> Result> { } else { doc.add( &format!("panels.{index}.end.groups"), - vec![("id".into(), json!("activities")), ("items".into(), json!([item]))], + vec![ + ("id".into(), json!("activities")), + ("items".into(), json!([item])), + ], )?; } } @@ -64,7 +74,10 @@ pub fn configure(path: &Path) -> Result<(), Box> { ferese_ipc::activities::now_ms() )); file.replace(&desired, Some(&backup), |_| Ok(()))?; - println!("Configured Live Activities; previous config: {}", backup.display()); + println!( + "Configured Live Activities; previous config: {}", + backup.display() + ); Ok(()) } @@ -73,20 +86,29 @@ mod tests { use super::*; #[test] fn setup_preserves_layout_comments_rules_and_explicit_disable() { - let source = - "// keep my layout\npanel main { style morph; end { group tools { item activities kind=clock; }; }; }\n"; + let source = "// keep my layout\npanel main { style morph; end { group tools { item activities kind=clock; }; }; }\n"; let next = prepare(source).unwrap(); assert!(next.contains("// keep my layout")); let doc = Document::parse(&next).unwrap(); assert_eq!(doc.get("panels.0.style"), Some(&json!("morph"))); - assert_eq!(doc.get("panels.0.end.groups.0.items.0.kind"), Some(&json!("clock"))); - assert_eq!(doc.get("panels.0.end.groups.0.items.1.id"), Some(&json!("activities-"))); + assert_eq!( + doc.get("panels.0.end.groups.0.items.0.kind"), + Some(&json!("clock")) + ); + assert_eq!( + doc.get("panels.0.end.groups.0.items.1.id"), + Some(&json!("activities-")) + ); assert_eq!(prepare(&next).unwrap(), next); let disabled = "activities { sources { shell-hook #false; }; }\n"; assert_eq!(prepare(disabled).unwrap(), disabled); - let hidden = "panel main { end { group tools { item tasks kind=activities visible=#false; }; }; }\n"; + let hidden = + "panel main { end { group tools { item tasks kind=activities visible=#false; }; }; }\n"; let hidden = Document::parse(&prepare(hidden).unwrap()).unwrap(); - assert_eq!(hidden.get("panels.0.end.groups.0.items.0.visible"), Some(&json!(false))); + assert_eq!( + hidden.get("panels.0.end.groups.0.items.0.visible"), + Some(&json!(false)) + ); assert_eq!( hidden .get("panels.0.end.groups.0.items") @@ -97,9 +119,13 @@ mod tests { 1 ); assert!(prepare("").is_ok()); - let partial = Document::parse(&prepare("activities { max-visible 1; }\n").unwrap()).unwrap(); + let partial = + Document::parse(&prepare("activities { max-visible 1; }\n").unwrap()).unwrap(); assert_eq!(partial.get("activities.max_visible"), Some(&json!(1))); - assert_eq!(partial.get("activities.sources.shell_hook"), Some(&json!(true))); + assert_eq!( + partial.get("activities.sources.shell_hook"), + Some(&json!(true)) + ); assert!(prepare("activities { unknown 1; }").is_err()); } @@ -113,12 +139,22 @@ mod tests { let backups: Vec<_> = std::fs::read_dir(directory.path()) .unwrap() .flatten() - .filter(|file| file.file_name().to_string_lossy().contains(".before-activities-")) + .filter(|file| { + file.file_name() + .to_string_lossy() + .contains(".before-activities-") + }) .collect(); assert_eq!(backups.len(), 1); - assert_eq!(std::fs::read_to_string(backups[0].path()).unwrap(), "// original\n"); + assert_eq!( + std::fs::read_to_string(backups[0].path()).unwrap(), + "// original\n" + ); std::fs::write(&path, "activities { bad #true; }\n").unwrap(); assert!(configure(&path).is_err()); - assert_eq!(std::fs::read_to_string(&path).unwrap(), "activities { bad #true; }\n"); + assert_eq!( + std::fs::read_to_string(&path).unwrap(), + "activities { bad #true; }\n" + ); } } diff --git a/feresectl/src/activities/transport.rs b/feresectl/src/activities/transport.rs index 78532cb..8fc5fa2 100644 --- a/feresectl/src/activities/transport.rs +++ b/feresectl/src/activities/transport.rs @@ -62,7 +62,12 @@ impl Stream { self.deadline .checked_duration_since(Instant::now()) .filter(|remaining| !remaining.is_zero()) - .ok_or_else(|| io::Error::new(io::ErrorKind::TimedOut, "activity publication deadline expired")) + .ok_or_else(|| { + io::Error::new( + io::ErrorKind::TimedOut, + "activity publication deadline expired", + ) + }) } } impl Read for Stream { @@ -107,7 +112,10 @@ mod tests { }); { let mut stream = connect(&path, Duration::from_millis(100)).unwrap(); - assert!(super::super::super::request(&mut stream, "activity-get", serde_json::json!({})).is_err()); + assert!( + super::super::super::request(&mut stream, "activity-get", serde_json::json!({})) + .is_err() + ); } closed .recv_timeout(Duration::from_millis(400)) diff --git a/feresectl/src/autostart.rs b/feresectl/src/autostart.rs index eaf1cd1..6aa89c1 100644 --- a/feresectl/src/autostart.rs +++ b/feresectl/src/autostart.rs @@ -33,14 +33,20 @@ pub(crate) fn run() -> Result<(), Box> { if system_directories.is_empty() { system_directories.push(PathBuf::from("/etc/xdg")); } - let directories = std::iter::once(home).chain(system_directories).collect::>(); + let directories = std::iter::once(home) + .chain(system_directories) + .collect::>(); let mut entries = BTreeMap::new(); for directory in directories { let Ok(files) = fs::read_dir(directory.join("autostart")) else { continue; }; for file in files.flatten() { - if file.path().extension().is_some_and(|extension| extension == "desktop") { + if file + .path() + .extension() + .is_some_and(|extension| extension == "desktop") + { entries.entry(file.file_name()).or_insert(file.path()); } } @@ -100,7 +106,9 @@ fn read_entry(path: &Path) -> Option> { return None; } let mut source = String::new(); - file.take(1024 * 1024 + 1).read_to_string(&mut source).ok()?; + file.take(1024 * 1024 + 1) + .read_to_string(&mut source) + .ok()?; if source.len() > 1024 * 1024 { return None; } @@ -142,12 +150,15 @@ fn enabled(entry: &BTreeMap, desktop: &str) -> bool { fn available(program: &str) -> bool { use std::os::unix::fs::PermissionsExt; let executable = |path: &Path| { - fs::metadata(path).is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) + fs::metadata(path) + .is_ok_and(|metadata| metadata.is_file() && metadata.permissions().mode() & 0o111 != 0) }; if program.contains('/') { executable(Path::new(program)) } else { - env::var_os("PATH").is_some_and(|paths| env::split_paths(&paths).any(|path| executable(&path.join(program)))) + env::var_os("PATH").is_some_and(|paths| { + env::split_paths(&paths).any(|path| executable(&path.join(program))) + }) } } diff --git a/feresectl/src/cli.rs b/feresectl/src/cli.rs index 7ea0bad..09f12ee 100644 --- a/feresectl/src/cli.rs +++ b/feresectl/src/cli.rs @@ -273,7 +273,9 @@ impl Command { Self::CycleColumnWidth => ("cycle-column-width", json!({})), Self::CenterColumn => ("center-column", json!({})), Self::ToggleColumnTabbed => ("toggle-column-tabbed", json!({})), - Self::JoinTab { direction } => ("join-tab", json!({"direction": value_name(direction)})), + Self::JoinTab { direction } => { + ("join-tab", json!({"direction": value_name(direction)})) + } Self::LeaveTab => ("leave-tab", json!({})), Self::NextTab => ("next-tab", json!({})), Self::PreviousTab => ("previous-tab", json!({})), @@ -297,9 +299,14 @@ impl Command { Self::Resize { direction } => ("resize", json!({"direction": value_name(direction)})), Self::Workspace { index } => ("workspace", json!({"index": index})), Self::MoveToWorkspace { index } => ("move-to-workspace", json!({"index": index})), - Self::OutputLayout { layout } => ("output-layout", json!({"layout": value_name(layout)})), + Self::OutputLayout { layout } => { + ("output-layout", json!({"layout": value_name(layout)})) + } Self::OutputProfile { name } => ("output-profile", json!({"name": name})), - Self::OutputInternal { state } => ("output-internal", json!({"enabled": matches!(state, Switch::On)})), + Self::OutputInternal { state } => ( + "output-internal", + json!({"enabled": matches!(state, Switch::On)}), + ), Self::Screenshot { geometry: Some(geometry), } => ("screenshot", json!({"geometry": geometry})), @@ -315,11 +322,17 @@ impl Command { Self::Media { action: None | Some(MediaAction::Get), } => ("media-get", json!({})), - Self::Media { action: Some(action) } => { + Self::Media { + action: Some(action), + } => { let args = match action { MediaAction::Pin { player } => json!({"action": "pin", "player": player}), - MediaAction::Ignore { player } => json!({"action": "ignore", "player": player, "ignored": true}), - MediaAction::Unignore { player } => json!({"action": "ignore", "player": player, "ignored": false}), + MediaAction::Ignore { player } => { + json!({"action": "ignore", "player": player, "ignored": true}) + } + MediaAction::Unignore { player } => { + json!({"action": "ignore", "player": player, "ignored": false}) + } MediaAction::PlayPause => json!({"action": "play-pause"}), MediaAction::Next => json!({"action": "next"}), MediaAction::Previous => json!({"action": "previous"}), diff --git a/feresectl/src/main.rs b/feresectl/src/main.rs index e45908c..a24e92c 100644 --- a/feresectl/src/main.rs +++ b/feresectl/src/main.rs @@ -71,7 +71,11 @@ fn main() -> Result<(), Box> { } } -fn request(stream: &mut (impl io::Read + io::Write), command: &str, args: Value) -> Result> { +fn request( + stream: &mut (impl io::Read + io::Write), + command: &str, + args: Value, +) -> Result> { write_frame( stream, &Request { @@ -92,10 +96,16 @@ fn request(stream: &mut (impl io::Read + io::Write), command: &str, args: Value) Ok(response.result.unwrap_or(Value::Null)) } -fn theme_command(action: &ThemeAction, socket: &Path, json_output: bool) -> Result<(), Box> { +fn theme_command( + action: &ThemeAction, + socket: &Path, + json_output: bool, +) -> Result<(), Box> { let mut connection = ferese_ipc::Connection::connect_to(socket)?; let result = match action { - ThemeAction::Get => serde_json::to_value(connection.get(ferese_config::families::builtins)?)?, + ThemeAction::Get => { + serde_json::to_value(connection.get(ferese_config::families::builtins)?)? + } ThemeAction::Subscribe => { let mut snapshot = connection.get(ferese_config::families::builtins)?; let mut stdout = io::stdout().lock(); @@ -103,7 +113,8 @@ fn theme_command(action: &ThemeAction, socket: &Path, json_output: bool) -> Resu if !output::write_json_line(&mut stdout, &snapshot)? { return Ok(()); } - snapshot = connection.watch(snapshot.revision, ferese_config::families::builtins)?; + snapshot = + connection.watch(snapshot.revision, ferese_config::families::builtins)?; } } ThemeAction::Mode { mode } => connection.call( @@ -139,7 +150,8 @@ fn event_stream(socket: &Path) -> Result<(), Box> { #[cfg(test)] fn parse_args(args: impl IntoIterator) -> Result<(String, Value), String> { - let cli = Cli::try_parse_from(std::iter::once("feresectl".to_owned()).chain(args)).map_err(|e| e.to_string())?; + let cli = Cli::try_parse_from(std::iter::once("feresectl".to_owned()).chain(args)) + .map_err(|e| e.to_string())?; cli.command .ipc() .map(|(command, args)| (command.into(), args)) @@ -224,7 +236,8 @@ mod tests { #[test] fn a_failed_x11_retry_surfaces_the_compositor_error_code() { - let response = Response::error(1, "x11_unavailable", "X11 is disabled in the configuration"); + let response = + Response::error(1, "x11_unavailable", "X11 is disabled in the configuration"); let mut bytes = Vec::new(); write_frame(&mut bytes, &response).expect("encode"); let decoded: Response = read_frame(&mut bytes.as_slice()).expect("decode"); @@ -258,7 +271,9 @@ mod tests { fn display_mode_commands_validate_layouts() { for layout in ["internal-only", "external-only", "extend", "mirror"] { assert_eq!( - parse_args(["output-layout".into(), layout.into()]).unwrap().1, + parse_args(["output-layout".into(), layout.into()]) + .unwrap() + .1, json!({"layout":layout}) ); } @@ -287,11 +302,15 @@ mod tests { #[test] fn parses_direction_and_workspace_commands() { assert_eq!( - parse_args(["focus".to_owned(), "left".to_owned()]).unwrap().1, + parse_args(["focus".to_owned(), "left".to_owned()]) + .unwrap() + .1, json!({ "direction": "left" }) ); assert_eq!( - parse_args(["workspace".to_owned(), "7".to_owned()]).unwrap().1, + parse_args(["workspace".to_owned(), "7".to_owned()]) + .unwrap() + .1, json!({ "index": 7 }) ); assert_eq!( @@ -304,7 +323,10 @@ mod tests { ); assert!(parse_args(["workspace-back-and-forth".into(), "2".into()]).is_err()); for command in ["focus-last-window", "focus-mru-next", "focus-mru-previous"] { - assert_eq!(parse_args([command.into()]).unwrap(), (command.into(), json!({}))); + assert_eq!( + parse_args([command.into()]).unwrap(), + (command.into(), json!({})) + ); assert!(parse_args([command.into(), "2".into()]).is_err()); } } @@ -343,24 +365,40 @@ mod tests { #[test] fn output_commands_have_consistent_arguments() { - for command in ["outputs", "output-profiles", "output-confirm", "output-revert"] { - assert_eq!(parse_args([command.into()]).unwrap(), (command.into(), json!({}))); + for command in [ + "outputs", + "output-profiles", + "output-confirm", + "output-revert", + ] { + assert_eq!( + parse_args([command.into()]).unwrap(), + (command.into(), json!({})) + ); assert!(parse_args([command.into(), "extra".into()]).is_err()); } assert_eq!( - parse_args(["output-profile".into(), "auto".into()]).unwrap().1, + parse_args(["output-profile".into(), "auto".into()]) + .unwrap() + .1, json!({"name": "auto"}) ); assert_eq!( - parse_args(["output-profile".into(), "desk".into()]).unwrap().1, + parse_args(["output-profile".into(), "desk".into()]) + .unwrap() + .1, json!({"name": "desk"}) ); assert_eq!( - parse_args(["output-internal".into(), "off".into()]).unwrap().1, + parse_args(["output-internal".into(), "off".into()]) + .unwrap() + .1, json!({"enabled": false}) ); assert_eq!( - parse_args(["output-internal".into(), "on".into()]).unwrap().1, + parse_args(["output-internal".into(), "on".into()]) + .unwrap() + .1, json!({"enabled": true}) ); assert!(parse_args(["output-internal".into(), "maybe".into()]).is_err()); @@ -377,15 +415,26 @@ mod tests { } assert!(parse_args(["join-tab".into()]).is_err()); assert!(parse_args(["join-tab".into(), "diagonal".into()]).is_err()); - for action in ["toggle-column-tabbed", "leave-tab", "next-tab", "previous-tab"] { - assert_eq!(parse_args([action.into()]).unwrap(), (action.into(), json!({}))); + for action in [ + "toggle-column-tabbed", + "leave-tab", + "next-tab", + "previous-tab", + ] { + assert_eq!( + parse_args([action.into()]).unwrap(), + (action.into(), json!({})) + ); assert!(parse_args([action.into(), "extra".into()]).is_err()); } } #[test] fn rejects_unknown_or_missing_arguments() { - assert_eq!(parse_args(["exit".to_owned()]).unwrap(), ("exit".to_owned(), json!({}))); + assert_eq!( + parse_args(["exit".to_owned()]).unwrap(), + ("exit".to_owned(), json!({})) + ); assert!(parse_args(["exit".to_owned(), "extra".to_owned()]).is_err()); assert!(parse_args(["focus".to_owned()]).is_err()); assert!(parse_args(["reload-config".to_owned()]).is_ok()); diff --git a/feresectl/src/output.rs b/feresectl/src/output.rs index 4e20c5d..fa46546 100644 --- a/feresectl/src/output.rs +++ b/feresectl/src/output.rs @@ -63,7 +63,10 @@ fn terminal_text(text: &str) -> String { } /// Return false when a downstream consumer closes stdout. -pub fn write_json_line(writer: &mut impl Write, value: &impl serde::Serialize) -> Result> { +pub fn write_json_line( + writer: &mut impl Write, + value: &impl serde::Serialize, +) -> Result> { let line = serde_json::to_vec(value)?; match writer .write_all(&line) diff --git a/feresectl/tests/cli.rs b/feresectl/tests/cli.rs index 9f285c4..4cc5e50 100644 --- a/feresectl/tests/cli.rs +++ b/feresectl/tests/cli.rs @@ -29,7 +29,11 @@ fn serve(path: &Path, expected: &'static str, value: serde_json::Value) -> threa } fn success(output: Output) -> String { - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); String::from_utf8(output.stdout).unwrap() } @@ -88,7 +92,14 @@ fn explicit_socket_targets_the_nested_instance_for_queries_and_theme_commands() host_listener.set_nonblocking(true).unwrap(); for args in [ vec!["--socket", nested.to_str().unwrap(), "-j", "windows"], - vec!["theme", "mode", "dark", "--socket", nested.to_str().unwrap(), "--json"], + vec![ + "theme", + "mode", + "dark", + "--socket", + nested.to_str().unwrap(), + "--json", + ], ] { let worker = serve( &nested, @@ -119,7 +130,11 @@ fn socket_environment_and_runtime_fallback_use_the_same_query_path() { std::fs::create_dir(directory.path().join("ferese")).unwrap(); let socket = directory.path().join("ferese/control.sock"); for environment in ["FERESE_SOCKET", "XDG_RUNTIME_DIR"] { - let worker = serve(&socket, "outputs", json!([{"name": "nested", "enabled": true}])); + let worker = serve( + &socket, + "outputs", + json!([{"name": "nested", "enabled": true}]), + ); let target = if environment == "FERESE_SOCKET" { socket.as_path() } else { @@ -172,9 +187,15 @@ fn event_stream_uses_the_selected_socket_and_prints_complete_json_lines() { let request: Request = read_frame(&mut stream).unwrap(); assert_eq!(request.command, "event-stream"); assert_eq!(request.args["version"], VERSION); - write_frame(&mut stream, &Response::success(request.id, json!({"version": VERSION}))).unwrap(); + write_frame( + &mut stream, + &Response::success(request.id, json!({"version": VERSION})), + ) + .unwrap(); write_frame(&mut stream, &event).unwrap(); - stopped.recv_timeout(std::time::Duration::from_secs(5)).unwrap(); + stopped + .recv_timeout(std::time::Duration::from_secs(5)) + .unwrap(); }); let mut child = ctl(&["event-stream", "--socket", socket.to_str().unwrap()]) .stdout(Stdio::piped()) @@ -202,7 +223,10 @@ fn activity_start_uses_versioned_framing_and_parent_scope() { assert_eq!(request.version, ferese_ipc::VERSION); assert_eq!(request.command, "activity-publish"); let wire: ferese_ipc::activities::Request = serde_json::from_value(request.args).unwrap(); - assert!(matches!(wire.owner, ferese_ipc::activities::OwnerScope::Parent)); + assert!(matches!( + wire.owner, + ferese_ipc::activities::OwnerScope::Parent + )); match wire.publish { ferese_ipc::activities::Publish::Start { local, data, .. } => { assert_eq!(local, "build"); @@ -255,7 +279,10 @@ fn run_preserves_arguments_cwd_environment_output_and_exit_when_publishing_fails assert_eq!(output.status.code(), Some(17)); assert_eq!( String::from_utf8(output.stdout).unwrap(), - format!("argument with spaces;$literal|preserved|{}", dir.path().display()) + format!( + "argument with spaces;$literal|preserved|{}", + dir.path().display() + ) ); assert_eq!(output.stderr, b"stderr"); } @@ -273,7 +300,9 @@ fn run_preserves_child_exit_and_signal_status() { .output() .unwrap(); assert_eq!(output.status.code(), Some(19)); - let output = ctl(&["run", "--", "sh", "-c", "kill -TERM $$"]).output().unwrap(); + let output = ctl(&["run", "--", "sh", "-c", "kill -TERM $$"]) + .output() + .unwrap(); assert_eq!(output.status.signal(), Some(libc::SIGTERM)); } @@ -343,7 +372,11 @@ fn fake_cargo(dir: &Path, script: &str) -> String { let path = dir.join("cargo"); std::fs::write(&path, script).unwrap(); std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o755)).unwrap(); - format!("{}:{}", dir.display(), std::env::var("PATH").unwrap_or_default()) + format!( + "{}:{}", + dir.display(), + std::env::var("PATH").unwrap_or_default() + ) } #[test] @@ -442,7 +475,10 @@ sys.exit(19) }; write_frame( &mut stream, - &Response::success(request.id, json!({"handle":handle,"progress":"cargo-json"})), + &Response::success( + request.id, + json!({"handle":handle,"progress":"cargo-json"}), + ), ) .unwrap(); } @@ -457,19 +493,32 @@ sys.exit(19) } panic!("completion was not published"); }); - let output = ctl(&["--socket", socket.to_str().unwrap(), "run", "--", "cargo", "build"]) - .env("PATH", path) - .current_dir(dir.path()) - .output() - .unwrap(); + let output = ctl(&[ + "--socket", + socket.to_str().unwrap(), + "run", + "--", + "cargo", + "build", + ]) + .env("PATH", path) + .current_dir(dir.path()) + .output() + .unwrap(); assert_eq!(output.status.code(), Some(19)); assert_eq!(output.stdout, b"child output\ndiagnostic\n"); - assert!(server.join().unwrap() > 0, "probe must exercise progress publishing"); + assert!( + server.join().unwrap() > 0, + "probe must exercise progress publishing" + ); let elapsed: f64 = std::fs::read_to_string(dir.path().join("elapsed")) .unwrap() .parse() .unwrap(); - assert!(elapsed < 1.5, "stdout reader stalled the child for {elapsed}s"); + assert!( + elapsed < 1.5, + "stdout reader stalled the child for {elapsed}s" + ); } #[test] @@ -488,7 +537,9 @@ fn launcher_toggle_uses_the_shell_command() { #[test] fn pomodoro_commands_use_the_versioned_session_api() { - for action in ["start", "pause", "resume", "next", "stop", "reset", "status"] { + for action in [ + "start", "pause", "resume", "next", "stop", "reset", "status", + ] { let directory = tempfile::tempdir().unwrap(); let socket = directory.path().join("control.sock"); let listener = UnixListener::bind(&socket).unwrap(); @@ -496,8 +547,15 @@ fn pomodoro_commands_use_the_versioned_session_api() { let (mut stream, _) = listener.accept().unwrap(); let request: Request = read_frame(&mut stream).unwrap(); assert_eq!(request.command, "pomodoro"); - assert_eq!(request.args, json!({"version":1,"action":action,"session":null})); - write_frame(&mut stream, &Response::success(request.id, json!({"phase":"work"}))).unwrap(); + assert_eq!( + request.args, + json!({"version":1,"action":action,"session":null}) + ); + write_frame( + &mut stream, + &Response::success(request.id, json!({"phase":"work"})), + ) + .unwrap(); }); success( ctl(&["--socket", socket.to_str().unwrap(), "pomodoro", action]) diff --git a/portal/src/backend.rs b/portal/src/backend.rs index 77bddfe..3e641c8 100644 --- a/portal/src/backend.rs +++ b/portal/src/backend.rs @@ -120,8 +120,12 @@ impl RecorderControl { if !session.bar_controlled || session.phase != Phase::Running { return Err(error("Recording has not started")); } - serde_json::to_string(&session.recording.ok_or_else(|| error("No recording options"))?) - .map_err(|e| error(&e.to_string())) + serde_json::to_string( + &session + .recording + .ok_or_else(|| error("No recording options"))?, + ) + .map_err(|e| error(&e.to_string())) } async fn use_bar_controls( @@ -148,12 +152,18 @@ async fn authorize_recorder( connection: &Connection, header: &Header<'_>, ) -> zbus::fdo::Result<()> { - let sender = header.sender().ok_or_else(|| error("Missing recorder caller"))?; + let sender = header + .sender() + .ok_or_else(|| error("Missing recorder caller"))?; if !owns_session(sender.as_str(), handle.as_str()) { - return Err(zbus::fdo::Error::AccessDenied("Not your recording session".into())); + return Err(zbus::fdo::Error::AccessDenied( + "Not your recording session".into(), + )); } let dbus = zbus::fdo::DBusProxy::new(connection).await?; - let pid = dbus.get_connection_unix_process_id(sender.clone().into()).await?; + let pid = dbus + .get_connection_unix_process_id(sender.clone().into()) + .await?; if !shell_recorder(pid) { return Err(zbus::fdo::Error::AccessDenied( "The shell must own the recording controls".into(), @@ -183,14 +193,18 @@ fn shell_recorder(pid: u32) -> bool { let parent = std::fs::read_to_string(format!("/proc/{pid}/status")) .ok() .and_then(|status| { - status - .lines() - .find_map(|line| line.strip_prefix("PPid:").and_then(|s| s.trim().parse::().ok())) + status.lines().find_map(|line| { + line.strip_prefix("PPid:") + .and_then(|s| s.trim().parse::().ok()) + }) }); parent.is_some_and(|pid| matches_binary(pid, "ferese-shell") == Some(true)) } -pub(crate) async fn authorize(connection: &Connection, header: &Header<'_>) -> zbus::fdo::Result { +pub(crate) async fn authorize( + connection: &Connection, + header: &Header<'_>, +) -> zbus::fdo::Result { let sender = header .sender() .ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing caller".into()))?; @@ -200,7 +214,9 @@ pub(crate) async fn authorize(connection: &Connection, header: &Header<'_>) -> z .await .map_err(|_| zbus::fdo::Error::AccessDenied("Desktop portal is unavailable".into()))?; if sender.as_str() != owner.as_str() { - return Err(zbus::fdo::Error::AccessDenied("Use the desktop portal".into())); + return Err(zbus::fdo::Error::AccessDenied( + "Use the desktop portal".into(), + )); } Ok(sender.to_string()) @@ -249,7 +265,10 @@ impl Backend { let conn = connection.clone(); let path = path.to_owned(); tokio::spawn(async move { - let _ = conn.object_server().remove::(path.as_str()).await; + let _ = conn + .object_server() + .remove::(path.as_str()) + .await; }); } } @@ -295,12 +314,9 @@ impl Backend { "Session limit reached or duplicate session".into(), )); } - let work = Arc::new( - self.work - .clone() - .try_acquire_owned() - .map_err(|_| zbus::fdo::Error::LimitsExceeded("Capture work is still shutting down".into()))?, - ); + let work = Arc::new(self.work.clone().try_acquire_owned().map_err(|_| { + zbus::fdo::Error::LimitsExceeded("Capture work is still shutting down".into()) + })?); let inserted = connection .object_server() .at( @@ -346,14 +362,21 @@ impl Backend { ) -> zbus::fdo::Result<(u32, Options)> { let owner = authorize(connection, &header).await?; let settings = source_options(&options).and_then(|(multiple, cursor, types)| { - Ok((multiple, cursor, types, crate::restore::persist_mode(&options)?)) + Ok(( + multiple, + cursor, + types, + crate::restore::persist_mode(&options)?, + )) }); let mut sessions = self.sessions.lock().await; let session = sessions .get_mut(session_handle.as_str()) .ok_or_else(|| error("Unknown session"))?; - if session.owner != owner || session.app != app_id || !matches!(session.phase, Phase::Created | Phase::Selected) + if session.owner != owner + || session.app != app_id + || !matches!(session.phase, Phase::Created | Phase::Selected) { return Err(error("Invalid session state")); } @@ -389,7 +412,10 @@ impl Backend { ) -> zbus::fdo::Result<(u32, Options)> { let owner = authorize(connection, &header).await?; - if !handle.as_str().starts_with("/org/freedesktop/portal/desktop/request/") { + if !handle + .as_str() + .starts_with("/org/freedesktop/portal/desktop/request/") + { return Err(error("Invalid request path")); } @@ -457,10 +483,15 @@ impl Backend { result.unwrap_or_else(|_| Err(StartError::Failed("Sharing request timed out".into()))) } }; - let _ = connection.object_server().remove::(handle).await; + let _ = connection + .object_server() + .remove::(handle) + .await; match result { - Ok((children, streams, mut reply, recording)) if !cancel.stopped.load(Ordering::SeqCst) => { + Ok((children, streams, mut reply, recording)) + if !cancel.stopped.load(Ordering::SeqCst) => + { if let Some(session) = self.sessions.lock().await.get_mut(session_handle.as_str()) { session.phase = Phase::Running; session.recording = recording; @@ -475,7 +506,8 @@ impl Backend { }); reply.insert( "streams".into(), - OwnedValue::try_from(Value::from(streams)).map_err(|e| error(&e.to_string()))?, + OwnedValue::try_from(Value::from(streams)) + .map_err(|e| error(&e.to_string()))?, ); Ok((0, reply)) @@ -665,7 +697,8 @@ async fn start_streams( let rememberable = sources .iter() .filter(|source| { - crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1).is_some() + crate::restore::Restore::create(app, cursor, std::slice::from_ref(*source), &outputs, 1) + .is_some() }) .map(|source| source.name.clone()) .collect::>(); @@ -673,7 +706,10 @@ async fn start_streams( let selected = restore.resolve(app, cursor, multiple, &sources, &outputs)?; let selected = validate_selection( &sources, - &selected.iter().map(|source| source.name.clone()).collect::>(), + &selected + .iter() + .map(|source| source.name.clone()) + .collect::>(), multiple, ) .ok()?; @@ -708,7 +744,8 @@ async fn start_streams( return Err("Invalid persistence selection".into()); } let selected = validate_selection(&prompt.sources, &selection.names, multiple)?; - if !bar_controlled && (selection.audio != crate::recording::Audio::Off || selection.region) { + if !bar_controlled && (selection.audio != crate::recording::Audio::Off || selection.region) + { return Err("Recording options are only available to the native recorder".into()); } crate::desktop::wait_for_surface_removal_using(|| { @@ -739,7 +776,12 @@ async fn start_streams( return Err(StartError::Cancelled); } crate::desktop::wait_for_surface_removal_using(|| { - capture_ipc(&cancel, &work, "has-client-surfaces", serde_json::json!({"pid": pid})) + capture_ipc( + &cancel, + &work, + "has-client-surfaces", + serde_json::json!({"pid": pid}), + ) }) .await?; Some(crate::recording_region::Region::parse( @@ -814,11 +856,15 @@ async fn start_streams( .map_err(|e| e.to_string())?; let mut line = String::new(); let output = child.stdout.take().unwrap(); - tokio::time::timeout(Duration::from_secs(10), BufReader::new(output).read_line(&mut line)) - .await - .map_err(|_| "PipeWire stream startup timed out")? - .map_err(|e| e.to_string())?; - let ready: Ready = serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?; + tokio::time::timeout( + Duration::from_secs(10), + BufReader::new(output).read_line(&mut line), + ) + .await + .map_err(|_| "PipeWire stream startup timed out")? + .map_err(|e| e.to_string())?; + let ready: Ready = + serde_json::from_str(&line).map_err(|_| "PipeWire stream could not start")?; if ready.node == u32::MAX { return Err("Invalid PipeWire node".into()); @@ -838,7 +884,10 @@ async fn start_streams( return Err("A selected display changed during stream startup".into()); } - if children.iter_mut().any(|child| !matches!(child.try_wait(), Ok(None))) { + if children + .iter_mut() + .any(|child| !matches!(child.try_wait(), Ok(None))) + { return Err("A selected stream ended during startup".into()); } @@ -848,12 +897,23 @@ async fn start_streams( .map(|region| { let (name, ready) = streams.first().ok_or("Missing recording display")?; let metadata = monitor_metadata(&final_outputs, name, ready)?; - let (x, y) = <(i32, i32)>::try_from(metadata["position"].try_clone().map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; - let (width, height) = <(i32, i32)>::try_from(metadata["size"].try_clone().map_err(|e| e.to_string())?) - .map_err(|e| e.to_string())?; + let (x, y) = <(i32, i32)>::try_from( + metadata["position"] + .try_clone() + .map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; + let (width, height) = <(i32, i32)>::try_from( + metadata["size"].try_clone().map_err(|e| e.to_string())?, + ) + .map_err(|e| e.to_string())?; region.crop( - crate::recording_region::Region { x, y, width, height }, + crate::recording_region::Region { + x, + y, + width, + height, + }, (ready.width, ready.height), ) }) @@ -874,10 +934,11 @@ async fn start_streams( .find(|source| source.name == name) .ok_or("Unknown window stream")?; let id = source.window_id().ok_or("Invalid selected window")?; - if !final_windows - .as_array() - .is_some_and(|windows| windows.iter().any(|window| window["id"].as_u64() == Some(id))) - { + if !final_windows.as_array().is_some_and(|windows| { + windows + .iter() + .any(|window| window["id"].as_u64() == Some(id)) + }) { return Err("A selected window closed during startup".to_owned()); } window_metadata(source, &ready) @@ -895,7 +956,9 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result { if source.window_id().is_none() || ready.width == 0 || ready.height == 0 { return Err("Invalid window stream".into()); } - let (width, height) = ready.logical_size.ok_or("Missing captured window geometry")?; + let (width, height) = ready + .logical_size + .ok_or("Missing captured window geometry")?; let size = ( i32::try_from(width).map_err(|_| "Invalid window width")?, i32::try_from(height).map_err(|_| "Invalid window height")?, @@ -907,15 +970,25 @@ fn window_metadata(source: &Source, ready: &Ready) -> Result { ("source_type".into(), 2u32.into()), ( "size".into(), - Value::from(size).try_to_owned().map_err(|e| e.to_string())?, + Value::from(size) + .try_to_owned() + .map_err(|e| e.to_string())?, ), ])) } -fn monitor_metadata(outputs: &serde_json::Value, name: &str, ready: &Ready) -> Result { +fn monitor_metadata( + outputs: &serde_json::Value, + name: &str, + ready: &Ready, +) -> Result { let output = outputs .as_array() - .and_then(|outputs| outputs.iter().find(|output| output["name"].as_str() == Some(name))) + .and_then(|outputs| { + outputs + .iter() + .find(|output| output["name"].as_str() == Some(name)) + }) .ok_or("Selected monitor is unavailable")?; let integer = |key: &str| { output[key] @@ -961,13 +1034,23 @@ fn monitor_metadata(outputs: &serde_json::Value, name: &str, ready: &Ready) -> R ), ( "size".into(), - Value::from(size).try_to_owned().map_err(|error| error.to_string())?, + Value::from(size) + .try_to_owned() + .map_err(|error| error.to_string())?, ), ])) } -fn validate_selection(sources: &[Source], names: &[String], multiple: bool) -> Result, String> { - if names.is_empty() || names.len() > 8 || names.len() > sources.len() || (!multiple && names.len() != 1) { +fn validate_selection( + sources: &[Source], + names: &[String], + multiple: bool, +) -> Result, String> { + if names.is_empty() + || names.len() > 8 + || names.len() > sources.len() + || (!multiple && names.len() != 1) + { return Err("Invalid display selection".into()); } @@ -990,7 +1073,11 @@ fn validate_selection(sources: &[Source], names: &[String], multiple: bool) -> R async fn supervise(mut children: Vec, cancel: Arc) { loop { - if cancel.stopped.load(Ordering::SeqCst) || children.iter_mut().any(|c| !matches!(c.try_wait(), Ok(None))) { + if cancel.stopped.load(Ordering::SeqCst) + || children + .iter_mut() + .any(|c| !matches!(c.try_wait(), Ok(None))) + { break; } tokio::time::sleep(Duration::from_millis(100)).await; @@ -1024,7 +1111,10 @@ pub async fn run() -> Result<(), Box> { .serve_at(PATH, shortcuts.clone())? .serve_at(PATH, inhibit.clone())? .serve_at(PATH, input_capture.clone())? - .serve_at("/org/ferese/ScreenRecorder", RecorderControl(backend.clone()))? + .serve_at( + "/org/ferese/ScreenRecorder", + RecorderControl(backend.clone()), + )? .build() .await?; @@ -1048,7 +1138,9 @@ pub async fn run() -> Result<(), Box> { requests.revoke_stale(owner.as_deref()).await; shortcuts.revoke_stale(&connection, owner.as_deref()).await; inhibit.revoke_stale(&connection, owner.as_deref()).await; - input_capture.revoke_stale(&connection, owner.as_deref()).await; + input_capture + .revoke_stale(&connection, owner.as_deref()) + .await; let stale: Vec<_> = backend .sessions @@ -1097,10 +1189,11 @@ mod tests { permits.remove(0); assert!(backend.work.clone().try_acquire_owned().is_err()); release.send(()).unwrap(); - let recovered = tokio::time::timeout(Duration::from_secs(1), backend.work.clone().acquire_owned()) - .await - .unwrap() - .unwrap(); + let recovered = + tokio::time::timeout(Duration::from_secs(1), backend.work.clone().acquire_owned()) + .await + .unwrap() + .unwrap(); assert_eq!(recovered.num_permits(), 1); assert_eq!(backend.work.available_permits(), 0); drop(recovered); @@ -1203,7 +1296,10 @@ mod tests { scale: 1, }) .collect::>(); - let names = sources.iter().map(|source| source.name.clone()).collect::>(); + let names = sources + .iter() + .map(|source| source.name.clone()) + .collect::>(); assert!(validate_selection(&sources, &names[..8], true).is_ok()); assert!(validate_selection(&sources, &names, true).is_err()); assert!(validate_selection(&sources, &names[..2], false).is_err()); @@ -1260,7 +1356,10 @@ mod tests { scale: 1, }) .collect::>(); - let names = sources.iter().map(|source| source.name.clone()).collect::>(); + let names = sources + .iter() + .map(|source| source.name.clone()) + .collect::>(); assert_eq!(validate_selection(&sources, &names, true).unwrap().len(), 6); assert!(validate_selection(&sources, &names, false).is_err()); } @@ -1277,8 +1376,22 @@ mod tests { scale: 1, }; assert!(validate_selection(std::slice::from_ref(&source), &[], false).is_err()); - assert!(validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err()); - assert!(validate_selection(std::slice::from_ref(&source), &["test".into(), "test".into()], true).is_err()); - assert_eq!(validate_selection(&[source], &["test".into()], false).unwrap().len(), 1); + assert!( + validate_selection(std::slice::from_ref(&source), &["other".into()], false).is_err() + ); + assert!( + validate_selection( + std::slice::from_ref(&source), + &["test".into(), "test".into()], + true + ) + .is_err() + ); + assert_eq!( + validate_selection(&[source], &["test".into()], false) + .unwrap() + .len(), + 1 + ); } } diff --git a/portal/src/bridge.rs b/portal/src/bridge.rs index b35b39a..bdfe5a1 100644 --- a/portal/src/bridge.rs +++ b/portal/src/bridge.rs @@ -49,7 +49,10 @@ impl Bridge { #[cfg(test)] pub(crate) fn test_pair() -> (Self, UnixStream) { let (stream, peer) = UnixStream::pair().unwrap(); - (Self::from_stream(stream, Duration::from_secs(1)).unwrap(), peer) + ( + Self::from_stream(stream, Duration::from_secs(1)).unwrap(), + peer, + ) } pub(crate) async fn call(&self, command: &'static str, args: Json) -> Result { @@ -86,7 +89,10 @@ impl Bridge { return Err("Compositor connection is closed".to_owned()); } - connection.id = connection.id.checked_add(1).ok_or("Compositor request IDs exhausted")?; + connection.id = connection + .id + .checked_add(1) + .ok_or("Compositor request IDs exhausted")?; let request = ferese_ipc::Request { version: ferese_ipc::VERSION, id: connection.id, @@ -98,10 +104,13 @@ impl Bridge { .stream .set_read_timeout(if wait { None } else { Some(self.0.timeout) }) .map_err(|error| error.to_string())?; - ferese_ipc::write_frame(&mut connection.stream, &request).map_err(|error| error.to_string())?; - let value: Json = ferese_ipc::read_frame(&mut connection.stream).map_err(|error| error.to_string())?; + ferese_ipc::write_frame(&mut connection.stream, &request) + .map_err(|error| error.to_string())?; + let value: Json = ferese_ipc::read_frame(&mut connection.stream) + .map_err(|error| error.to_string())?; let has_result = value.get("result").is_some(); - let response: ferese_ipc::Response = serde_json::from_value(value).map_err(|error| error.to_string())?; + let response: ferese_ipc::Response = + serde_json::from_value(value).map_err(|error| error.to_string())?; if response.id != request.id || response.version != ferese_ipc::VERSION { return Err("Unexpected compositor response ID or protocol version".into()); } @@ -128,8 +137,12 @@ impl Bridge { } pub(crate) async fn capture_watch(&self, session: u64) -> Result { - self.request("input-capture-watch", serde_json::json!({"session":session}), true) - .await + self.request( + "input-capture-watch", + serde_json::json!({"session":session}), + true, + ) + .await } pub(crate) fn is_closed(&self) -> bool { diff --git a/portal/src/bridge/tests.rs b/portal/src/bridge/tests.rs index ad75263..9e09f58 100644 --- a/portal/src/bridge/tests.rs +++ b/portal/src/bridge/tests.rs @@ -9,7 +9,9 @@ fn pair() -> (Bridge, UnixStream) { fn pair_with_timeout(timeout: Duration) -> (Bridge, UnixStream) { let (client, server) = UnixStream::pair().unwrap(); - server.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); (Bridge::from_stream(client, timeout).unwrap(), server) } @@ -21,19 +23,29 @@ async fn valid_errors_preserve_connection_and_request_ids_are_unique() { write_frame(&mut server, &Response::error(first.id, "denied", "Denied")).unwrap(); let second: Request = read_frame(&mut server).unwrap(); assert_eq!(second.id, first.id + 1); - write_frame(&mut server, &Response::success(second.id, json!({"enabled":false}))).unwrap(); + write_frame( + &mut server, + &Response::success(second.id, json!({"enabled":false})), + ) + .unwrap(); let third: Request = read_frame(&mut server).unwrap(); assert_eq!(third.id, second.id + 1); write_frame(&mut server, &Response::success(third.id, Json::Null)).unwrap(); }); - assert_eq!(bridge.call("enable", json!({})).await.unwrap_err(), "Denied"); + assert_eq!( + bridge.call("enable", json!({})).await.unwrap_err(), + "Denied" + ); assert!(!bridge.is_closed()); assert_eq!( bridge.call("disable", json!({})).await.unwrap(), json!({"enabled":false}) ); assert!(!bridge.is_closed()); - assert_eq!(bridge.call("empty-result", json!({})).await.unwrap(), Json::Null); + assert_eq!( + bridge.call("empty-result", json!({})).await.unwrap(), + Json::Null + ); peer.join().unwrap(); } @@ -44,7 +56,13 @@ async fn timeout_closes_stream_before_a_late_reply_can_be_reused() { let peer = std::thread::spawn(move || { let request: Request = read_frame(&mut server).unwrap(); blocked.recv_timeout(Duration::from_secs(2)).unwrap(); - assert!(write_frame(&mut server, &Response::success(request.id, json!({"enabled":true}))).is_err()); + assert!( + write_frame( + &mut server, + &Response::success(request.id, json!({"enabled":true})) + ) + .is_err() + ); assert_eq!(server.read(&mut [0u8; 1]).unwrap(), 0); }); assert!(bridge.call("enable", json!({})).await.is_err()); @@ -52,7 +70,13 @@ async fn timeout_closes_stream_before_a_late_reply_can_be_reused() { tokio::time::timeout(Duration::from_secs(1), bridge.terminated()) .await .unwrap(); - assert!(bridge.call("disable", json!({})).await.unwrap_err().contains("closed")); + assert!( + bridge + .call("disable", json!({})) + .await + .unwrap_err() + .contains("closed") + ); release.send(()).unwrap(); peer.join().unwrap(); } @@ -99,7 +123,10 @@ async fn malformed_or_mismatched_replies_poison_the_bridge() { } write_frame(&mut server, &response).unwrap(); }); - assert!(bridge.call("enable", json!({})).await.is_err(), "case {case}"); + assert!( + bridge.call("enable", json!({})).await.is_err(), + "case {case}" + ); assert!(bridge.is_closed(), "case {case}"); assert!(bridge.call("disable", json!({})).await.is_err()); peer.join().unwrap(); diff --git a/portal/src/capture.rs b/portal/src/capture.rs index 9ef8a98..e559bfb 100644 --- a/portal/src/capture.rs +++ b/portal/src/capture.rs @@ -102,7 +102,9 @@ impl Capture { // uninterruptible roundtrip on a stalled compositor. this.sync(stop)?; this.sync(stop)?; - if (this.state.manager.is_none() && this.state.window_manager.is_none()) || this.state.shm.is_none() { + if (this.state.manager.is_none() && this.state.window_manager.is_none()) + || this.state.shm.is_none() + { return Err("Ferese screen capture is unavailable in this session".into()); } Ok(this) @@ -110,7 +112,9 @@ impl Capture { fn sync(&mut self, stop: &AtomicBool) -> Result<(), String> { let done = Arc::new(AtomicBool::new(false)); - self.connection.display().sync(&self.queue.handle(), done.clone()); + self.connection + .display() + .sync(&self.queue.handle(), done.clone()); let deadline = Instant::now() + Duration::from_secs(5); while !done.load(Ordering::Relaxed) { self.dispatch(stop, deadline)?; @@ -138,7 +142,11 @@ impl Capture { Ok(()) } - pub fn validate_sources(&mut self, selected: &[(String, u32)], stop: &AtomicBool) -> Result<(), String> { + pub fn validate_sources( + &mut self, + selected: &[(String, u32)], + stop: &AtomicBool, + ) -> Result<(), String> { self.sync(stop)?; for (name, generation) in selected { if self.generation(name) != Some(*generation) { @@ -162,19 +170,34 @@ impl Capture { .collect() } - pub fn frame(&mut self, name: &str, cursor: bool, stop: &AtomicBool, mut pixels: Vec) -> Result { + pub fn frame( + &mut self, + name: &str, + cursor: bool, + stop: &AtomicBool, + mut pixels: Vec, + ) -> Result { self.state.result = None; self.state.flipped = false; self.state.logical_size = None; self.state.busy = false; - let (frame, transform) = if let Some(id) = name.strip_prefix("window:").and_then(|id| id.parse::().ok()) { + let (frame, transform) = if let Some(id) = name + .strip_prefix("window:") + .and_then(|id| id.parse::().ok()) + { let manager = self .state .window_manager .as_ref() .ok_or("Window capture is unavailable")?; ( - manager.capture_window((id >> 32) as u32, id as u32, cursor as u32, &self.queue.handle(), ()), + manager.capture_window( + (id >> 32) as u32, + id as u32, + cursor as u32, + &self.queue.handle(), + (), + ), wl_output::Transform::Normal, ) } else { @@ -184,11 +207,18 @@ impl Capture { .iter() .find(|o| o.source.name == name) .ok_or("Shared monitor disconnected")?; - if self.selected_output.is_some_and(|selected| selected != output.global) { + if self + .selected_output + .is_some_and(|selected| selected != output.global) + { return Err("Shared monitor was replaced".into()); } self.selected_output = Some(output.global); - let manager = self.state.manager.as_ref().ok_or("Monitor capture is unavailable")?; + let manager = self + .state + .manager + .as_ref() + .ok_or("Monitor capture is unavailable")?; ( manager.capture_output(cursor as i32, &output.proxy, &self.queue.handle(), ()), output.transform, @@ -206,7 +236,11 @@ impl Capture { frame.destroy(); self.state.result.take().unwrap()?; - let buffer = self.state.buffer.as_ref().ok_or("Capture supplied no buffer")?; + let buffer = self + .state + .buffer + .as_ref() + .ok_or("Capture supplied no buffer")?; let (width, height) = copy_oriented_pixels( &buffer.map, buffer.width, @@ -251,7 +285,9 @@ impl Capture { if ready > 0 { read.read().map_err(|e| e.to_string())?; - } else if ready < 0 && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted { + } else if ready < 0 + && std::io::Error::last_os_error().kind() != std::io::ErrorKind::Interrupted + { return Err(std::io::Error::last_os_error().to_string()); } } @@ -293,9 +329,13 @@ impl Dispatch for State { scale: 1, }, }), - "ferese_window_capture_manager_v1" => state.window_manager = Some(registry.bind(name, 1, qh, ())), + "ferese_window_capture_manager_v1" => { + state.window_manager = Some(registry.bind(name, 1, qh, ())) + } "wl_shm" => state.shm = Some(registry.bind(name, 1, qh, ())), - "zwlr_screencopy_manager_v1" => state.manager = Some(registry.bind(name, version.min(3), qh, ())), + "zwlr_screencopy_manager_v1" => { + state.manager = Some(registry.bind(name, version.min(3), qh, ())) + } _ => (), }, wl_registry::Event::GlobalRemove { name } => state.outputs.retain(|o| o.global != name), @@ -320,7 +360,9 @@ impl Dispatch for State { match event { wl_output::Event::Name { name } => output.source.name = name, wl_output::Event::Description { description } => output.source.label = description, - wl_output::Event::Geometry { x, y, transform, .. } => { + wl_output::Event::Geometry { + x, y, transform, .. + } => { if let WEnum::Value(transform) = transform { output.transform = transform; } @@ -372,7 +414,9 @@ impl Dispatch for State { return Ok(()); } // SAFETY: fixed NUL-terminated name and a newly owned memfd. - let fd = unsafe { libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC) }; + let fd = unsafe { + libc::memfd_create(c"ferese-screencast".as_ptr(), libc::MFD_CLOEXEC) + }; if fd < 0 { return Err(std::io::Error::last_os_error().to_string()); @@ -382,13 +426,20 @@ impl Dispatch for State { file.set_len(bytes as u64).map_err(|e| e.to_string())?; // SAFETY: the file has the checked mapping length and lives with the mapping. let map = unsafe { MmapMut::map_mut(&file) }.map_err(|e| e.to_string())?; - let pool = state.shm.as_ref().ok_or("Missing shared memory global")?.create_pool( - file.as_fd(), - bytes as i32, + let pool = state + .shm + .as_ref() + .ok_or("Missing shared memory global")? + .create_pool(file.as_fd(), bytes as i32, qh, ()); + let proxy = pool.create_buffer( + 0, + width as i32, + height as i32, + stride as i32, + format, qh, (), ); - let proxy = pool.create_buffer(0, width as i32, height as i32, stride as i32, format, qh, ()); pool.destroy(); frame.copy(&proxy); @@ -491,7 +542,9 @@ fn copy_oriented_pixels( fn buffer_size(width: u32, height: u32, stride: u32) -> Result { let row = width.checked_mul(4).ok_or("Capture dimensions overflow")?; - let bytes = stride.checked_mul(height).ok_or("Capture dimensions overflow")?; + let bytes = stride + .checked_mul(height) + .ok_or("Capture dimensions overflow")?; if width == 0 || height == 0 || stride < row || bytes > 128 * 1024 * 1024 { return Err("Unsupported capture dimensions".into()); @@ -527,7 +580,9 @@ impl Dispatch for State { _: &QueueHandle, ) { match event { - window_manager::Event::Geometry { width, height, .. } => state.logical_size = Some((width, height)), + window_manager::Event::Geometry { width, height, .. } => { + state.logical_size = Some((width, height)) + } window_manager::Event::Busy { .. } => state.busy = true, _ => (), } @@ -545,13 +600,17 @@ mod tests { for discovery in [true, false] { let (client, mut server) = UnixStream::pair().unwrap(); - server.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); let stop = Arc::new(AtomicBool::new(false)); let flag = stop.clone(); let worker = std::thread::spawn(move || { let connection = Connection::from_socket(client).unwrap(); if discovery { - Capture::connect_with_connection(connection, &flag).err().unwrap() + Capture::connect_with_connection(connection, &flag) + .err() + .unwrap() } else { let queue = connection.new_event_queue(); let mut capture = Capture { @@ -591,7 +650,11 @@ mod tests { for (transform, size, expected) in cases { for y_invert in [false, true] { let mut source = Vec::new(); - let rows = if y_invert { [b"def", b"abc"] } else { [b"abc", b"def"] }; + let rows = if y_invert { + [b"def", b"abc"] + } else { + [b"abc", b"def"] + }; for row in rows { for &value in row { source.extend_from_slice(&pixel(value)); diff --git a/portal/src/consent.rs b/portal/src/consent.rs index be8dcff..cdd83ce 100644 --- a/portal/src/consent.rs +++ b/portal/src/consent.rs @@ -49,7 +49,9 @@ struct Consent { pub(crate) fn run() -> Result<(), Box> { let mut input = String::new(); - std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?; + std::io::stdin() + .take(256 * 1024 + 1) + .read_to_string(&mut input)?; if input.len() > 256 * 1024 { return Err("Consent request is too large".into()); @@ -110,11 +112,15 @@ impl cosmic::Application for Consent { fn subscription(&self) -> cosmic::iced::Subscription { cosmic::iced::Subscription::batch([ - ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), + ferese_theme_client::service::subscription() + .map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), cosmic::iced::event::listen_with(|event, _, id| match event { - cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::Opened(id)), + cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => { + Some(Message::Opened(id)) + } cosmic::iced::Event::Keyboard(cosmic::iced::keyboard::Event::KeyPressed { - key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), + key: + cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), .. }) => Some(Message::Cancel), _ => None, @@ -126,7 +132,8 @@ impl cosmic::Application for Consent { match message { Message::ThemeChanged(snapshot) => { self.palette = Palette::from_resolved(&snapshot.presented).flat(); - self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); + self.font = + ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); self.background = cosmic::iced::Color { a: ferese_theme::material_opacity(&snapshot.presented), ..self.palette.sidebar diff --git a/portal/src/desktop.rs b/portal/src/desktop.rs index 53a1852..5e7a53b 100644 --- a/portal/src/desktop.rs +++ b/portal/src/desktop.rs @@ -59,7 +59,9 @@ impl Requests { let mut pending = self.0.lock().await; if pending.len() >= self.1 || pending.contains_key(handle.as_str()) { - return Err(zbus::fdo::Error::LimitsExceeded("Request limit reached".into())); + return Err(zbus::fdo::Error::LimitsExceeded( + "Request limit reached".into(), + )); } let cancel = Arc::new(Cancel::default()); @@ -74,7 +76,9 @@ impl Requests { ) .await? { - return Err(zbus::fdo::Error::InvalidArgs("Request already exists".into())); + return Err(zbus::fdo::Error::InvalidArgs( + "Request already exists".into(), + )); } pending.insert(handle.to_string(), (owner, cancel.clone())); @@ -83,7 +87,10 @@ impl Requests { pub(crate) async fn end(&self, connection: &Connection, handle: &OwnedObjectPath) { self.0.lock().await.remove(handle.as_str()); - let _ = connection.object_server().remove::(handle).await; + let _ = connection + .object_server() + .remove::(handle) + .await; } } @@ -243,13 +250,19 @@ pub(crate) async fn consent( } } -pub(crate) async fn ipc(command: &'static str, args: serde_json::Value) -> Result { +pub(crate) async fn ipc( + command: &'static str, + args: serde_json::Value, +) -> Result { tokio::task::spawn_blocking(move || ipc_sync(command, args)) .await .map_err(|error| error.to_string())? } -pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result { +pub(crate) fn ipc_sync( + command: &'static str, + args: serde_json::Value, +) -> Result { let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?; let mut stream = UnixStream::connect(path).map_err(|error| error.to_string())?; @@ -271,10 +284,14 @@ pub(crate) fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result exchange(&mut stream, &request) } -fn exchange(stream: &mut UnixStream, request: &ferese_ipc::Request) -> Result { +fn exchange( + stream: &mut UnixStream, + request: &ferese_ipc::Request, +) -> Result { ferese_ipc::write_frame(stream, request).map_err(|error| error.to_string())?; - let response: ferese_ipc::Response = ferese_ipc::read_frame(stream).map_err(|error| error.to_string())?; + let response: ferese_ipc::Response = + ferese_ipc::read_frame(stream).map_err(|error| error.to_string())?; if response.id != request.id || response.version != ferese_ipc::VERSION { return Err("Unexpected compositor response ID or protocol version".into()); } @@ -322,7 +339,8 @@ async fn select_geometry(point: bool) -> Result, String> { } pub(crate) async fn wait_for_surface_removal(pid: u32) -> Result<(), String> { - wait_for_surface_removal_using(|| ipc("has-client-surfaces", serde_json::json!({"pid": pid}))).await + wait_for_surface_removal_using(|| ipc("has-client-surfaces", serde_json::json!({"pid": pid}))) + .await } pub(crate) async fn wait_for_surface_removal_using< @@ -344,7 +362,10 @@ async fn capture(geometry: Option) -> Result, String> { capture_command("screenshot", serde_json::json!({"geometry": geometry})).await } -async fn capture_command(command: &'static str, args: serde_json::Value) -> Result, String> { +async fn capture_command( + command: &'static str, + args: serde_json::Value, +) -> Result, String> { tokio::task::spawn_blocking(move || { let result = ipc_sync(command, args)?; let path = result["path"].as_str().ok_or("Missing screenshot path")?; @@ -501,14 +522,20 @@ fn window_sources(windows: &serde_json::Value) -> Result Result, String> { +pub(crate) fn sharing_window_sources( + windows: &serde_json::Value, +) -> Result, String> { let windows = windows.as_array().ok_or("Invalid window list")?; let eligible = windows .iter() .filter(|window| { window["workspace"].as_u64().is_some() - && window["capture_width"].as_i64().is_some_and(|width| width > 0) - && window["capture_height"].as_i64().is_some_and(|height| height > 0) + && window["capture_width"] + .as_i64() + .is_some_and(|width| width > 0) + && window["capture_height"] + .as_i64() + .is_some_and(|height| height > 0) && window["app_id"].as_str() != Some("dev.ferese.ScreenShare") }) .take(128) @@ -564,7 +591,8 @@ async fn select_window(app: &str, parent: &str) -> Result, String> { if !output.status.success() { return Ok(None); } - let names: Vec = serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?; + let names: Vec = + serde_json::from_slice(&output.stdout).map_err(|_| "Invalid window selection")?; if names.len() != 1 || !prompt.sources.iter().any(|source| source.name == names[0]) { return Err("Unknown selected window".into()); } @@ -595,7 +623,9 @@ async fn pick_color(app: &str, parent: &str) -> Result, String> let color = png_color(&bytes)?; Ok(Some(HashMap::from([( "color".into(), - Value::from(color).try_to_owned().map_err(|error| error.to_string())?, + Value::from(color) + .try_to_owned() + .map_err(|error| error.to_string())?, )]))) } @@ -608,7 +638,9 @@ fn png_color(bytes: &[u8]) -> Result<(f64, f64, f64), String> { .filter(|size| *size <= 16 * 1024) .ok_or("Unexpected pixel dimensions")?; let mut pixels = vec![0; size]; - let info = reader.next_frame(&mut pixels).map_err(|error| error.to_string())?; + let info = reader + .next_frame(&mut pixels) + .map_err(|error| error.to_string())?; if info.width == 0 || info.height == 0 { return Err("Empty image".into()); } @@ -628,7 +660,8 @@ fn private_directory(name: &str) -> Result { .join("ferese/portal") .join(name); std::fs::create_dir_all(&directory).map_err(|error| error.to_string())?; - std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700)).map_err(|error| error.to_string())?; + std::fs::set_permissions(&directory, std::fs::Permissions::from_mode(0o700)) + .map_err(|error| error.to_string())?; Ok(directory) } @@ -696,7 +729,11 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result Result MAX_IMAGE_BYTES { return Err("Wallpaper is too large".into()); } - std::io::Write::write_all(&mut file, &buffer[..bytes]).map_err(|error| error.to_string())?; + std::io::Write::write_all(&mut file, &buffer[..bytes]) + .map_err(|error| error.to_string())?; } if size == 0 { return Err("Wallpaper is empty".into()); @@ -728,7 +766,9 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result 16384 @@ -799,25 +839,35 @@ fn edit_wallpaper( let source = file.source(); let mut document = ferese_config::Document::parse(source).map_err(|error| error.to_string())?; let directory = path.parent().ok_or("Invalid config path")?; - let before = wallpaper_theme(&document, directory, appearance)?.tokens.background; + let before = wallpaper_theme(&document, directory, appearance)? + .tokens + .background; let (variant, other) = match appearance { Appearance::Light => ("light", Appearance::Dark), Appearance::Dark => ("dark", Appearance::Light), }; - let other_before = wallpaper_theme(&document, directory, other)?.tokens.background; + let other_before = wallpaper_theme(&document, directory, other)? + .tokens + .background; let base = format!("theme.{variant}.background"); if target == "background" && before.lock_path.is_none() { let old = before .path .unwrap_or_else(|| ferese_config::default_wallpaper_for(appearance).into()); document - .set(&format!("{base}.lock_path"), old.to_string_lossy().into_owned().into()) + .set( + &format!("{base}.lock_path"), + old.to_string_lossy().into_owned().into(), + ) .map_err(|error| error.to_string())?; } if target != "lockscreen" { document - .set(&format!("{base}.path"), image.to_string_lossy().into_owned().into()) + .set( + &format!("{base}.path"), + image.to_string_lossy().into_owned().into(), + ) .map_err(|error| error.to_string())?; } @@ -830,10 +880,15 @@ fn edit_wallpaper( .map_err(|error| error.to_string())?; } - let after = wallpaper_theme(&document, directory, appearance)?.tokens.background; + let after = wallpaper_theme(&document, directory, appearance)? + .tokens + .background; if (target != "lockscreen" && after.path.as_deref() != Some(image)) || (target != "background" && after.lock_path.as_deref() != Some(image)) - || wallpaper_theme(&document, directory, other)?.tokens.background != other_before + || wallpaper_theme(&document, directory, other)? + .tokens + .background + != other_before { return Err("Wallpaper change did not resolve to the requested image".into()); } @@ -867,11 +922,18 @@ mod tests { (ferese_ipc::VERSION, 8, false), ] { let (mut client, mut server) = UnixStream::pair().unwrap(); - client.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); - server.set_read_timeout(Some(Duration::from_secs(2))).unwrap(); + client + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); + server + .set_read_timeout(Some(Duration::from_secs(2))) + .unwrap(); let peer = std::thread::spawn(move || { let _: ferese_ipc::Request = ferese_ipc::read_frame(&mut server).unwrap(); - let mut response = ferese_ipc::Response::success(id, serde_json::json!({"path": "/tmp/capture.png"})); + let mut response = ferese_ipc::Response::success( + id, + serde_json::json!({"path": "/tmp/capture.png"}), + ); response.version = version; ferese_ipc::write_frame(&mut server, &response).unwrap(); }); @@ -894,7 +956,11 @@ mod tests { fn local_uris_preserve_special_characters_and_reject_network_paths() { let path = Path::new("/tmp/wallpaper #1_日本.png"); assert_eq!(local_path(&file_uri(path).unwrap()).unwrap(), path); - for uri in ["https://example.com/a.png", "file://example.com/a.png", "not a URI"] { + for uri in [ + "https://example.com/a.png", + "file://example.com/a.png", + "not a URI", + ] { assert!(local_path(uri).is_err()); } } @@ -955,29 +1021,60 @@ mod tests { ) .unwrap(); let resolve = |mode| { - let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); - wallpaper_theme(&document, dir.path(), mode).unwrap().tokens.background + let document = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()) + .unwrap(); + wallpaper_theme(&document, dir.path(), mode) + .unwrap() + .tokens + .background }; let other = if appearance == Light { Dark } else { Light }; let untouched = resolve(other); edit_wallpaper(&path, Path::new("/desktop.png"), "background", appearance).unwrap(); - assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/desktop.png"))); - assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/old.png"))); + assert_eq!( + resolve(appearance).path.as_deref(), + Some(Path::new("/desktop.png")) + ); + assert_eq!( + resolve(appearance).lock_path.as_deref(), + Some(Path::new("/old.png")) + ); assert_eq!(resolve(other), untouched); - assert!(std::fs::read_to_string(&path).unwrap().contains("Keep my settings")); + assert!( + std::fs::read_to_string(&path) + .unwrap() + .contains("Keep my settings") + ); edit_wallpaper(&path, Path::new("/lock.png"), "lockscreen", appearance).unwrap(); - assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/desktop.png"))); - assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/lock.png"))); + assert_eq!( + resolve(appearance).path.as_deref(), + Some(Path::new("/desktop.png")) + ); + assert_eq!( + resolve(appearance).lock_path.as_deref(), + Some(Path::new("/lock.png")) + ); assert_eq!(resolve(other), untouched); edit_wallpaper(&path, Path::new("/both.png"), "both", appearance).unwrap(); - assert_eq!(resolve(appearance).path.as_deref(), Some(Path::new("/both.png"))); - assert_eq!(resolve(appearance).lock_path.as_deref(), Some(Path::new("/both.png"))); + assert_eq!( + resolve(appearance).path.as_deref(), + Some(Path::new("/both.png")) + ); + assert_eq!( + resolve(appearance).lock_path.as_deref(), + Some(Path::new("/both.png")) + ); assert_eq!(resolve(other), untouched); - let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let document = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); assert_eq!(document.get("theme.background.path").unwrap(), "/old.png"); - assert_eq!(document.get("commands.terminal").unwrap(), &serde_json::json!(["foot"])); + assert_eq!( + document.get("commands.terminal").unwrap(), + &serde_json::json!(["foot"]) + ); } } @@ -1000,15 +1097,20 @@ mod tests { "#, ) .unwrap(); - let before = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let before = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); let other = if appearance == Light { Dark } else { Light }; let old_active = wallpaper_theme(&before, dir.path(), appearance) .unwrap() .tokens .background; - let old_other = wallpaper_theme(&before, dir.path(), other).unwrap().tokens.background; + let old_other = wallpaper_theme(&before, dir.path(), other) + .unwrap() + .tokens + .background; edit_wallpaper(&path, Path::new("/new.png"), "background", appearance).unwrap(); - let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let after = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); let current = wallpaper_theme(&after, dir.path(), appearance) .unwrap() .tokens @@ -1016,22 +1118,35 @@ mod tests { assert_eq!(current.path.as_deref(), Some(Path::new("/new.png"))); assert_eq!(current.lock_path, old_active.lock_path); assert_eq!( - wallpaper_theme(&after, dir.path(), other).unwrap().tokens.background, + wallpaper_theme(&after, dir.path(), other) + .unwrap() + .tokens + .background, old_other ); assert_eq!(after.get("theme.mode").unwrap(), "auto"); - assert_eq!(after.get("theme.background"), before.get("theme.background")); + assert_eq!( + after.get("theme.background"), + before.get("theme.background") + ); edit_wallpaper(&path, Path::new("/new-lock.png"), "lockscreen", appearance).unwrap(); - let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let after = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); let current = wallpaper_theme(&after, dir.path(), appearance) .unwrap() .tokens .background; assert_eq!(current.path.as_deref(), Some(Path::new("/new.png"))); - assert_eq!(current.lock_path.as_deref(), Some(Path::new("/new-lock.png"))); assert_eq!( - wallpaper_theme(&after, dir.path(), other).unwrap().tokens.background, + current.lock_path.as_deref(), + Some(Path::new("/new-lock.png")) + ); + assert_eq!( + wallpaper_theme(&after, dir.path(), other) + .unwrap() + .tokens + .background, old_other ); } @@ -1053,16 +1168,30 @@ mod tests { "#, ) .unwrap(); - let before = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); - let previous = wallpaper_theme(&before, dir.path(), Dark).unwrap().tokens.background; + let before = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let previous = wallpaper_theme(&before, dir.path(), Dark) + .unwrap() + .tokens + .background; assert_eq!(previous.lock_path, None); edit_wallpaper(&path, Path::new("/new.png"), "background", Dark).unwrap(); - let after = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); - let current = wallpaper_theme(&after, dir.path(), Dark).unwrap().tokens.background; + let after = + ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + let current = wallpaper_theme(&after, dir.path(), Dark) + .unwrap() + .tokens + .background; assert_eq!(current.lock_path, previous.path); assert_eq!( - wallpaper_theme(&after, dir.path(), Light).unwrap().tokens.background, - wallpaper_theme(&before, dir.path(), Light).unwrap().tokens.background + wallpaper_theme(&after, dir.path(), Light) + .unwrap() + .tokens + .background, + wallpaper_theme(&before, dir.path(), Light) + .unwrap() + .tokens + .background ); } @@ -1109,6 +1238,10 @@ mod tests { std::fs::write(&path, [0u8; 32]).unwrap(); let cancel = Cancel::default(); cancel.stop(); - assert!(prepare_wallpaper(&path, &cancel).unwrap_err().contains("cancelled")); + assert!( + prepare_wallpaper(&path, &cancel) + .unwrap_err() + .contains("cancelled") + ); } } diff --git a/portal/src/eis.rs b/portal/src/eis.rs index 986647a..d70f8c5 100644 --- a/portal/src/eis.rs +++ b/portal/src/eis.rs @@ -21,7 +21,11 @@ pub(crate) struct Worker { } impl Worker { - pub(crate) fn start(capabilities: u32, keymap: &str, cancel: Arc) -> Result<(Self, OwnedFd), String> { + pub(crate) fn start( + capabilities: u32, + keymap: &str, + cancel: Arc, + ) -> Result<(Self, OwnedFd), String> { if keymap.is_empty() || keymap.len() > 256 * 1024 { return Err("Invalid captured keyboard map".into()); } @@ -47,7 +51,8 @@ impl Worker { let _finish = Finish(cancel); let run = || { let context = eis::Context::new(server).map_err(|e| e.to_string())?; - let mut event_loop = EventLoop::::try_new().map_err(|e| e.to_string())?; + let mut event_loop = + EventLoop::::try_new().map_err(|e| e.to_string())?; let signal = event_loop.get_signal(); let (sender, receiver) = channel::sync_channel(4); event_loop @@ -59,19 +64,22 @@ impl Worker { .map_err(|e| e.to_string())?; event_loop .handle() - .insert_source(EisRequestSource::new(context, 1), |event, connection, state| { - let action = match event { - Ok(event) => state.request(connection, event), - Err(_) => { + .insert_source( + EisRequestSource::new(context, 1), + |event, connection, state| { + let action = match event { + Ok(event) => state.request(connection, event), + Err(_) => { + state.signal.stop(); + PostAction::Remove + } + }; + if connection.flush().is_err() { state.signal.stop(); - PostAction::Remove } - }; - if connection.flush().is_err() { - state.signal.stop(); - } - Ok(action) - }) + Ok(action) + }, + ) .map_err(|e| e.to_string())?; let mut state = State { signal: signal.clone(), @@ -91,7 +99,8 @@ impl Worker { .map_err(|_| "Input transport startup cancelled".to_owned())?; let _ = event_loop.run(None, &mut state, |_| {}); if let Some(connection) = state.connection.take() { - connection.disconnected(eis::connection::DisconnectReason::Disconnected, None); + connection + .disconnected(eis::connection::DisconnectReason::Disconnected, None); } state.ready.store(false, Ordering::SeqCst); Ok::<(), String>(()) @@ -104,7 +113,14 @@ impl Worker { let (sender, signal) = started .recv_timeout(std::time::Duration::from_secs(2)) .map_err(|_| "Input transport startup timed out".to_owned())??; - Ok((Self { sender, signal, ready }, client.into())) + Ok(( + Self { + sender, + signal, + ready, + }, + client.into(), + )) } pub(crate) fn send(&self, events: Vec) -> Result<(), String> { @@ -156,14 +172,18 @@ impl State { capabilities |= DeviceCapability::Keyboard; } if self.capabilities & 2 != 0 { - capabilities |= DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll; + capabilities |= DeviceCapability::Pointer + | DeviceCapability::Button + | DeviceCapability::Scroll; } let _ = connection.add_seat(Some("Ferese input capture"), capabilities); self.connection = Some(connection.clone()); } EisRequestSourceEvent::Request(EisRequest::Bind(request)) => { - let keyboard = request.capabilities.contains(DeviceCapability::Keyboard) && self.capabilities & 1 != 0; - let pointer = request.capabilities.contains(DeviceCapability::Pointer) && self.capabilities & 2 != 0; + let keyboard = request.capabilities.contains(DeviceCapability::Keyboard) + && self.capabilities & 1 != 0; + let pointer = request.capabilities.contains(DeviceCapability::Pointer) + && self.capabilities & 2 != 0; if !keyboard && let Some(device) = self.keyboard.take() { device.remove(); self.keyboard_ready = false; @@ -179,7 +199,11 @@ impl State { DeviceCapability::Keyboard.into(), |device| { if let Some(keyboard) = device.interface::() { - keyboard.keymap(eis::keyboard::KeymapType::Xkb, self.keymap_size, self.keymap.as_fd()); + keyboard.keymap( + eis::keyboard::KeymapType::Xkb, + self.keymap_size, + self.keymap.as_fd(), + ); } }, ); @@ -189,11 +213,15 @@ impl State { } if pointer && self.pointer.is_none() { let caps = request.capabilities - & (DeviceCapability::Pointer | DeviceCapability::Button | DeviceCapability::Scroll); - let device = - request - .seat - .add_device(Some("Ferese pointer"), eis::device::DeviceType::Virtual, caps, |_| {}); + & (DeviceCapability::Pointer + | DeviceCapability::Button + | DeviceCapability::Scroll); + let device = request.seat.add_device( + Some("Ferese pointer"), + eis::device::DeviceType::Virtual, + caps, + |_| {}, + ); self.pointer_ready = device.device().version() < 3; device.resumed(); self.pointer = Some(device); @@ -227,11 +255,14 @@ impl State { return PostAction::Remove; } self.ready.store( - (self.keyboard.is_none() || self.keyboard_ready) && (self.pointer.is_none() || self.pointer_ready), + (self.keyboard.is_none() || self.keyboard_ready) + && (self.pointer.is_none() || self.pointer_ready), Ordering::SeqCst, ); } - EisRequestSourceEvent::Request(EisRequest::Disconnect | EisRequest::DeviceClosed(_)) => { + EisRequestSourceEvent::Request( + EisRequest::Disconnect | EisRequest::DeviceClosed(_), + ) => { self.signal.stop(); return PostAction::Remove; } @@ -266,7 +297,12 @@ impl State { if let Some(device) = &self.keyboard && let Some(keyboard) = device.interface::() { - for key in event["keys"].as_array().into_iter().flatten().filter_map(Value::as_u64) { + for key in event["keys"] + .as_array() + .into_iter() + .flatten() + .filter_map(Value::as_u64) + { keyboard.key(key as u32, eis::keyboard::KeyState::Press); } device.frame(now()); @@ -346,7 +382,10 @@ impl State { } fn now() -> u64 { - let mut time = libc::timespec { tv_sec: 0, tv_nsec: 0 }; + let mut time = libc::timespec { + tv_sec: 0, + tv_nsec: 0, + }; // SAFETY: the initialized output pointer covers a timespec allocation. if unsafe { libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut time) } == 0 { time.tv_sec as u64 * 1_000_000 + time.tv_nsec as u64 / 1_000 @@ -370,8 +409,8 @@ mod tests { let cancel = Arc::new(Cancel::default()); let map = "xkb_keymap { xkb_keycodes { include \"evdev+aliases(qwerty)\" }; xkb_types { include \"complete\" }; xkb_compatibility { include \"complete\" }; xkb_symbols { include \"pc+us+inet(evdev)\" }; };"; let (worker, fd) = Worker::start(3, map, cancel.clone()).unwrap(); - let fixture = - std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../scripts/tests/fixtures/eis-receiver.py"); + let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../scripts/tests/fixtures/eis-receiver.py"); let mut client = Command::new("python3") .arg(fixture) .stdin(Stdio::from(fd)) @@ -386,7 +425,10 @@ mod tests { if !worker.ready.load(Ordering::SeqCst) { let _ = client.kill(); let output = client.wait_with_output().unwrap(); - panic!("Receiver did not bind: {}", String::from_utf8_lossy(&output.stderr)); + panic!( + "Receiver did not bind: {}", + String::from_utf8_lossy(&output.stderr) + ); } worker .send(vec![ @@ -401,7 +443,11 @@ mod tests { ]) .unwrap(); let output = client.wait_with_output().unwrap(); - assert!(output.status.success(), "{}", String::from_utf8_lossy(&output.stderr)); + assert!( + output.status.success(), + "{}", + String::from_utf8_lossy(&output.stderr) + ); assert!(String::from_utf8_lossy(&output.stdout).contains("\"keymap\": true")); let deadline = Instant::now() + Duration::from_secs(2); while !cancel.stopped.load(Ordering::SeqCst) && Instant::now() < deadline { diff --git a/portal/src/inhibit.rs b/portal/src/inhibit.rs index eed125c..9b57ef3 100644 --- a/portal/src/inhibit.rs +++ b/portal/src/inhibit.rs @@ -64,7 +64,10 @@ impl Inhibit { if let Ok(emitter) = SignalEmitter::new(connection, path) { let _ = MonitorObject::closed(&emitter).await; } - let _ = connection.object_server().remove::(path).await; + let _ = connection + .object_server() + .remove::(path) + .await; self.changed.notify_one(); } } @@ -107,7 +110,9 @@ impl Inhibit { since = state["revision"].as_u64(); for (path, monitor) in monitors { if !state["monitor-owners"].as_array().is_some_and(|owners| { - owners.iter().any(|owner| owner.as_u64() == Some(monitor.native_owner)) + owners + .iter() + .any(|owner| owner.as_u64() == Some(monitor.native_owner)) }) { self.end(&connection, &path).await; continue; @@ -119,7 +124,8 @@ impl Inhibit { let transitions = match *last { None => vec![state.clone()], Some((_, _, revision, _)) => { - let events = state["transitions"].as_array().cloned().unwrap_or_default(); + let events = + state["transitions"].as_array().cloned().unwrap_or_default(); if events .first() .and_then(|event| event["revision"].as_u64()) @@ -131,7 +137,11 @@ impl Inhibit { } events .into_iter() - .filter(|event| event["revision"].as_u64().is_some_and(|next| next > revision)) + .filter(|event| { + event["revision"] + .as_u64() + .is_some_and(|next| next > revision) + }) .collect() } }; @@ -140,13 +150,19 @@ impl Inhibit { let phase = event["session-state"].as_u64().unwrap_or(1) as u32; let revision = event["revision"].as_u64().unwrap_or(0); let token = event["query-token"].as_u64().unwrap_or(0) as u32; - let changed = last - .as_ref() - .is_none_or(|old| old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token)); + let changed = last.as_ref().is_none_or(|old| { + old.0 != locked || old.1 != phase || (phase == 2 && old.3 != token) + }); if changed { let values = HashMap::from([ - ("screensaver-active".into(), Value::from(locked).try_into().unwrap()), - ("session-state".into(), Value::from(phase).try_into().unwrap()), + ( + "screensaver-active".into(), + Value::from(locked).try_into().unwrap(), + ), + ( + "session-state".into(), + Value::from(phase).try_into().unwrap(), + ), ]); if let Ok(emitter) = SignalEmitter::new(&connection, PATH) && Self::state_changed( @@ -182,7 +198,9 @@ impl Inhibit { use futures_util::StreamExt; loop { let result: Result<(), String> = async { - let connection = Connection::system().await.map_err(|error| error.to_string())?; + let connection = Connection::system() + .await + .map_err(|error| error.to_string())?; let proxy = zbus::Proxy::new( &connection, "org.freedesktop.login1", @@ -196,10 +214,16 @@ impl Inhibit { .await .map_err(|error| error.to_string())?; while let Some(message) = signals.next().await { - let (ending,): (bool,) = message.body().deserialize().map_err(|error| error.to_string())?; + let (ending,): (bool,) = message + .body() + .deserialize() + .map_err(|error| error.to_string())?; if let Ok(bridge) = Bridge::connect() { let _ = bridge - .call("logind-session-ending", serde_json::json!({"ending": ending})) + .call( + "logind-session-ending", + serde_json::json!({"ending": ending}), + ) .await; bridge.close(); } @@ -217,7 +241,9 @@ impl Inhibit { fn flags_what(flags: u32) -> zbus::fdo::Result { if flags == 0 || flags & !15 != 0 { - return Err(zbus::fdo::Error::InvalidArgs("Invalid inhibition flags".into())); + return Err(zbus::fdo::Error::InvalidArgs( + "Invalid inhibition flags".into(), + )); } let mut what = Vec::new(); if flags & 1 != 0 { @@ -237,7 +263,9 @@ async fn login_inhibitor(what: &str, app: &str, reason: &str) -> Result 512 || reason.len() > 1024 || app_id.chars().chain(reason.chars()).any(char::is_control) { - return Err(zbus::fdo::Error::InvalidArgs("Invalid application or reason".into())); + if app_id.len() > 512 + || reason.len() > 1024 + || app_id.chars().chain(reason.chars()).any(char::is_control) + { + return Err(zbus::fdo::Error::InvalidArgs( + "Invalid application or reason".into(), + )); } let cancel = self.requests.begin(connection, &header, &handle).await?; let bridge = match Bridge::connect() { @@ -317,7 +350,9 @@ impl Inhibit { bridge.close(); self.requests.end(connection, &handle).await; Err(zbus::fdo::Error::Failed( - result.err().unwrap_or_else(|| "Inhibition was cancelled".into()), + result + .err() + .unwrap_or_else(|| "Inhibition was cancelled".into()), )) } } @@ -333,11 +368,15 @@ impl Inhibit { #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result { let owner = authorize(connection, &header).await?; - if !session_handle.as_str().starts_with(&format!("{PATH}/session/")) + if !session_handle + .as_str() + .starts_with(&format!("{PATH}/session/")) || session_handle.as_str().len() > 512 || app_id.len() > 512 { - return Err(zbus::fdo::Error::InvalidArgs("Invalid session monitor".into())); + return Err(zbus::fdo::Error::InvalidArgs( + "Invalid session monitor".into(), + )); } let cancel = self.requests.begin(connection, &header, &handle).await?; let mut monitors = self.monitors.lock().await; @@ -430,7 +469,9 @@ impl Inhibit { .ok_or_else(|| zbus::fdo::Error::AccessDenied("Not your session monitor".into()))?; let last = monitor.last.lock().await; if monitor.cancel.stopped.load(Ordering::SeqCst) { - return Err(zbus::fdo::Error::AccessDenied("Session monitor closed".into())); + return Err(zbus::fdo::Error::AccessDenied( + "Session monitor closed".into(), + )); } if let Some((_, 2, _, token)) = *last { monitor @@ -470,7 +511,9 @@ impl MonitorObject { #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<()> { if authorize(connection, &header).await? != self.owner { - return Err(zbus::fdo::Error::AccessDenied("Not your session monitor".into())); + return Err(zbus::fdo::Error::AccessDenied( + "Not your session monitor".into(), + )); } self.cancel.stop(); self.backend.end(connection, &self.path).await; diff --git a/portal/src/input_capture.rs b/portal/src/input_capture.rs index 29d9dd0..70dae08 100644 --- a/portal/src/input_capture.rs +++ b/portal/src/input_capture.rs @@ -87,7 +87,10 @@ impl InputCapture { let connection = connection.clone(); let path = path.to_owned(); tokio::spawn(async move { - let _ = connection.object_server().remove::(path).await; + let _ = connection + .object_server() + .remove::(path) + .await; }); } } @@ -98,7 +101,9 @@ impl InputCapture { .lock() .await .iter() - .filter_map(|(path, session)| (Some(session.owner.as_str()) != owner).then_some(path.clone())) + .filter_map(|(path, session)| { + (Some(session.owner.as_str()) != owner).then_some(path.clone()) + }) .collect::>(); for path in stale { self.end(connection, &path).await; @@ -163,7 +168,10 @@ impl InputCapture { if session.cancel.stopped.load(Ordering::SeqCst) { break; } - let transport = state.worker.as_ref().map(|worker| worker.send(events.clone())); + let transport = state + .worker + .as_ref() + .map(|worker| worker.send(events.clone())); if transport.is_some_and(|result| result.is_err()) { break; } @@ -186,9 +194,11 @@ impl InputCapture { if let Some(position) = event["cursor_position"] .as_array() .filter(|position| position.len() == 2) - && let Ok(position) = - Value::from((position[0].as_f64().unwrap_or(0.0), position[1].as_f64().unwrap_or(0.0))) - .try_to_owned() + && let Ok(position) = Value::from(( + position[0].as_f64().unwrap_or(0.0), + position[1].as_f64().unwrap_or(0.0), + )) + .try_to_owned() { options.insert("cursor_position".into(), position); } @@ -205,8 +215,12 @@ impl InputCapture { ) .await } - Some("disabled") => Self::disabled(&emitter, path.clone(), Options::new()).await, - Some("zones") => Self::zones_changed(&emitter, path.clone(), Options::new()).await, + Some("disabled") => { + Self::disabled(&emitter, path.clone(), Options::new()).await + } + Some("zones") => { + Self::zones_changed(&emitter, path.clone(), Options::new()).await + } Some("closed") => { closed = true; Ok(()) @@ -238,7 +252,8 @@ fn capabilities(options: &Options) -> zbus::fdo::Result { let requested = options .get("capabilities") .ok_or_else(|| invalid("Missing requested capabilities"))?; - let requested = u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?; + let requested = + u32::try_from(requested).map_err(|_| invalid("Invalid requested capabilities"))?; if requested == 0 || requested & 3 == 0 { return Err(invalid("No supported capabilities requested")); } @@ -266,7 +281,9 @@ impl InputCapture { #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result { let owner = authorize(connection, &header).await?; - if !session_handle.as_str().starts_with(&format!("{PATH}/session/")) + if !session_handle + .as_str() + .starts_with(&format!("{PATH}/session/")) || session_handle.as_str().len() > 512 || app_id.len() > 512 { @@ -362,7 +379,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let caps = capabilities(&options)?; crate::restore::persist_mode(&options)?; let cancel = self.requests.begin(connection, &header, &handle).await?; @@ -401,11 +420,18 @@ impl InputCapture { let reply = bridge .call("input-capture-register", json!({"capabilities":caps})) .await?; - let id = reply["session"].as_u64().ok_or("Missing capture session ID")?; - let keymap = reply["keymap"].as_str().ok_or("Missing keyboard map")?.to_owned(); + let id = reply["session"] + .as_u64() + .ok_or("Missing capture session ID")?; + let keymap = reply["keymap"] + .as_str() + .ok_or("Missing keyboard map")? + .to_owned(); let watch = Bridge::connect()?; let mut state = session.state.lock().await; - if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) { + if cancel.stopped.load(Ordering::SeqCst) + || session.cancel.stopped.load(Ordering::SeqCst) + { bridge.close(); return Err("Input capture approval was cancelled".to_owned()); } @@ -461,7 +487,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let (bridge, id) = Self::native(&session).await?; let cancel = self.requests.begin(connection, &header, &handle).await?; let result = tokio::select! { @@ -471,20 +499,25 @@ impl InputCapture { result = bridge.call("input-capture-zones", json!({"session":id})) => Some(result), }; self.requests.end(connection, &handle).await; - if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) || result.is_none() { + if cancel.stopped.load(Ordering::SeqCst) + || session.cancel.stopped.load(Ordering::SeqCst) + || result.is_none() + { return Ok((1, Options::new())); } let result = self .finish_native_call(connection, &session_handle, &bridge, result.unwrap()) .await?; - let zones: Vec<(u32, u32, i32, i32)> = - serde_json::from_value(result["zones"].clone()).map_err(|_| invalid("Invalid native capture zones"))?; + let zones: Vec<(u32, u32, i32, i32)> = serde_json::from_value(result["zones"].clone()) + .map_err(|_| invalid("Invalid native capture zones"))?; Ok(( 0, Options::from([ ( "zones".into(), - Value::from(zones).try_to_owned().map_err(|e| failed(e.to_string()))?, + Value::from(zones) + .try_to_owned() + .map_err(|e| failed(e.to_string()))?, ), ( "zone_set".into(), @@ -506,7 +539,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let (bridge, id) = Self::native(&session).await?; if barriers.len() > 64 { return Err(invalid("Too many pointer barriers")); @@ -514,15 +549,20 @@ impl InputCapture { let barriers = barriers .into_iter() .map(|barrier| { - let id = u32::try_from(barrier.get("barrier_id").ok_or_else(|| invalid("Missing barrier ID"))?) - .map_err(|_| invalid("Invalid barrier ID"))?; + let id = u32::try_from( + barrier + .get("barrier_id") + .ok_or_else(|| invalid("Missing barrier ID"))?, + ) + .map_err(|_| invalid("Invalid barrier ID"))?; let position = barrier .get("position") .ok_or_else(|| invalid("Missing barrier position"))? .try_clone() .map_err(|_| invalid("Invalid position"))?; - let position: (i32, i32, i32, i32) = - position.try_into().map_err(|_| invalid("Invalid barrier position"))?; + let position: (i32, i32, i32, i32) = position + .try_into() + .map_err(|_| invalid("Invalid barrier position"))?; Ok(json!({"id":id,"position":[position.0,position.1,position.2,position.3]})) }) .collect::>>()?; @@ -534,7 +574,10 @@ impl InputCapture { result = bridge.call("input-capture-barriers", json!({"session":id,"zone_set":zone_set,"barriers":barriers})) => Some(result), }; self.requests.end(connection, &handle).await; - if cancel.stopped.load(Ordering::SeqCst) || session.cancel.stopped.load(Ordering::SeqCst) || result.is_none() { + if cancel.stopped.load(Ordering::SeqCst) + || session.cancel.stopped.load(Ordering::SeqCst) + || result.is_none() + { self.end(connection, session_handle.as_str()).await; return Ok((1, Options::new())); } @@ -563,7 +606,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; Self::native(&session).await?; let mut state = session.state.lock().await; if state.worker.is_some() { @@ -572,10 +617,11 @@ impl InputCapture { let capabilities = state.capabilities; let keymap = state.keymap.clone(); let cancel = session.cancel.clone(); - let (worker, fd) = tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel)) - .await - .map_err(|e| failed(e.to_string()))? - .map_err(failed)?; + let (worker, fd) = + tokio::task::spawn_blocking(move || Worker::start(capabilities, &keymap, cancel)) + .await + .map_err(|e| failed(e.to_string()))? + .map_err(failed)?; state.worker = Some(worker); Ok(fd.into()) } @@ -588,7 +634,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let (bridge, id) = Self::native(&session).await?; let ready = session .state @@ -600,12 +648,16 @@ impl InputCapture { .ok_or_else(|| invalid("Connect to EIS before enabling input capture"))?; let deadline = tokio::time::Instant::now() + Duration::from_secs(5); while !ready.load(Ordering::SeqCst) { - if session.cancel.stopped.load(Ordering::SeqCst) || tokio::time::Instant::now() >= deadline { + if session.cancel.stopped.load(Ordering::SeqCst) + || tokio::time::Instant::now() >= deadline + { return Err(invalid("EIS receiver has not bound an input device")); } tokio::time::sleep(Duration::from_millis(10)).await; } - let result = bridge.call("input-capture-enable", json!({"session":id})).await; + let result = bridge + .call("input-capture-enable", json!({"session":id})) + .await; self.finish_native_call(connection, &session_handle, &bridge, result) .await?; Ok((0, Options::new())) @@ -619,9 +671,13 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let (bridge, id) = Self::native(&session).await?; - let result = bridge.call("input-capture-disable", json!({"session":id})).await; + let result = bridge + .call("input-capture-disable", json!({"session":id})) + .await; self.finish_native_call(connection, &session_handle, &bridge, result) .await?; Ok((0, Options::new())) @@ -635,7 +691,9 @@ impl InputCapture { #[zbus(connection)] connection: &Connection, #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { - let session = self.session(connection, &header, &session_handle, &app_id).await?; + let session = self + .session(connection, &header, &session_handle, &app_id) + .await?; let (bridge, id) = Self::native(&session).await?; let activation = options .get("activation_id") @@ -769,12 +827,19 @@ mod tests { ..State::default() }), }); - capture.sessions.lock().await.insert(path.to_string(), session.clone()); + capture + .sessions + .lock() + .await + .insert(path.to_string(), session.clone()); let response = std::thread::spawn(move || { let request: ferese_ipc::Request = ferese_ipc::read_frame(&mut peer).unwrap(); if !transport_failure { - ferese_ipc::write_frame(&mut peer, &ferese_ipc::Response::error(request.id, "denied", "Denied")) - .unwrap(); + ferese_ipc::write_frame( + &mut peer, + &ferese_ipc::Response::error(request.id, "denied", "Denied"), + ) + .unwrap(); } }); let result = control.call("input-capture-disable", json!({})).await; @@ -785,7 +850,10 @@ mod tests { .is_err() ); response.join().unwrap(); - assert_eq!(session.cancel.stopped.load(Ordering::SeqCst), transport_failure); + assert_eq!( + session.cancel.stopped.load(Ordering::SeqCst), + transport_failure + ); assert_eq!(watch.is_closed(), transport_failure); assert_eq!( capture.sessions.lock().await.contains_key(path.as_str()), @@ -801,7 +869,9 @@ mod tests { fn requested_capabilities_are_required_and_supported_subset_is_returned() { assert!(capabilities(&Options::new()).is_err()); for requested in [0u32, 4, 8] { - assert!(capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err()); + assert!( + capabilities(&Options::from([("capabilities".into(), requested.into())])).is_err() + ); } for (requested, supported) in [(1u32, 1), (2, 2), (3, 3), (7, 3), (9, 1)] { assert_eq!( diff --git a/portal/src/lockdown.rs b/portal/src/lockdown.rs index 5703766..c00ef04 100644 --- a/portal/src/lockdown.rs +++ b/portal/src/lockdown.rs @@ -64,7 +64,8 @@ impl Lockdown { connection: &Connection, header: Option>, ) -> zbus::fdo::Result<()> { - let header = header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?; + let header = + header.ok_or_else(|| zbus::fdo::Error::AccessDenied("Missing policy caller".into()))?; crate::backend::authorize(connection, &header).await?; let mut current = self.0.write().await; store(index, value).map_err(zbus::fdo::Error::Failed)?; @@ -224,7 +225,8 @@ fn store(index: usize, value: bool) -> Result<(), String> { fn store_path(path: &Path, index: usize, value: bool) -> Result<(), String> { let file = ferese_config::file::ConfigFile::open(path)?; - let mut document = ferese_config::Document::parse(file.source()).map_err(|error| error.to_string())?; + let mut document = + ferese_config::Document::parse(file.source()).map_err(|error| error.to_string())?; if values(&document).is_none() { return Err("Invalid lockdown policy".into()); } @@ -246,14 +248,23 @@ mod tests { let directory = tempfile::tempdir().unwrap(); let path = directory.path().join("config.kdl"); let settings = ferese_config::file::ConfigFile::open(&path).unwrap(); - assert!(store_path(&path, 4, true).unwrap_err().contains("another app")); + assert!( + store_path(&path, 4, true) + .unwrap_err() + .contains("another app") + ); assert!(!path.exists()); - settings.replace("theme \"light\"\n", None, |_| Ok(())).unwrap(); + settings + .replace("theme \"light\"\n", None, |_| Ok(())) + .unwrap(); store_path(&path, 4, true).unwrap(); let source = std::fs::read_to_string(&path).unwrap(); let document = ferese_config::Document::parse(&source).unwrap(); - assert_eq!(document.get("theme").and_then(|value| value.as_str()), Some("light")); + assert_eq!( + document.get("theme").and_then(|value| value.as_str()), + Some("light") + ); assert!(load_path(&path).unwrap()[4]); } @@ -264,7 +275,10 @@ mod tests { for size in [ferese_config::file::LIMIT, ferese_config::file::LIMIT + 1] { let source = format!("//{}\n", "x".repeat(size - 3)); std::fs::write(&path, &source).unwrap(); - assert_eq!(load_path(&path).is_some(), size == ferese_config::file::LIMIT); + assert_eq!( + load_path(&path).is_some(), + size == ferese_config::file::LIMIT + ); assert!(store_path(&path, 4, true).unwrap_err().contains("60 KiB")); assert_eq!(std::fs::read_to_string(&path).unwrap(), source); } @@ -276,7 +290,10 @@ mod tests { let path = directory.path().join("config.kdl"); let source = "portals { lockdown { disable-camera \"yes\"; }; }"; std::fs::write(&path, source).unwrap(); - assert_eq!(store_path(&path, 4, true).unwrap_err(), "Invalid lockdown policy"); + assert_eq!( + store_path(&path, 4, true).unwrap_err(), + "Invalid lockdown policy" + ); assert_eq!(std::fs::read_to_string(&path).unwrap(), source); assert!(ferese_config::file::ConfigFile::open(&path).is_ok()); } @@ -292,7 +309,9 @@ mod tests { let values = values(&enabled).unwrap(); assert!(values[1] && values[4]); assert!(!values[0]); - let invalid = ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }").unwrap(); + let invalid = + ferese_config::Document::parse("portals { lockdown { disable-camera \"yes\"; }; }") + .unwrap(); assert!(super::values(&invalid).is_none()); for source in ["portals #false", "portals { lockdown \"bad\"; }"] { let invalid = ferese_config::Document::parse(source).unwrap(); diff --git a/portal/src/main.rs b/portal/src/main.rs index 99b5bf8..49dce9b 100644 --- a/portal/src/main.rs +++ b/portal/src/main.rs @@ -21,14 +21,18 @@ fn main() -> Result<(), Box> { let args: Vec<_> = std::env::args().skip(1).collect(); match args.as_slice() { - [flag, name, cursor] if flag == "--stream" => stream::run(name.clone(), cursor == "embedded", None), + [flag, name, cursor] if flag == "--stream" => { + stream::run(name.clone(), cursor == "embedded", None) + } [flag, id, cursor] if flag == "--stream-window" => { let id: u64 = id.parse()?; stream::run(format!("window:{id}"), cursor == "embedded", None) } - [flag, name, cursor, generation] if flag == "--stream" => { - stream::run(name.clone(), cursor == "embedded", Some(generation.parse()?)) - } + [flag, name, cursor, generation] if flag == "--stream" => stream::run( + name.clone(), + cursor == "embedded", + Some(generation.parse()?), + ), [flag] if flag == "--sources" => { let capture = capture::Capture::connect(&std::sync::atomic::AtomicBool::new(false))?; println!("{}", serde_json::to_string(&capture.sources())?); diff --git a/portal/src/parent.rs b/portal/src/parent.rs index fc23e5c..3239432 100644 --- a/portal/src/parent.rs +++ b/portal/src/parent.rs @@ -77,7 +77,10 @@ pub(crate) struct Parent { } impl Parent { - pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result { + pub(crate) fn attach( + window: &dyn cosmic::iced::window::Window, + parent: &str, + ) -> Result { use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle}; let handle = match classify_parent(parent) { ParentKind::None => { @@ -87,7 +90,8 @@ impl Parent { return Ok(ParentAttachment { parent: None, diagnostic: Some( - "showing this dialog unparented: X11 parent windows are not supported yet".to_owned(), + "showing this dialog unparented: X11 parent windows are not supported yet" + .to_owned(), ), }); } @@ -104,8 +108,9 @@ impl Parent { return Err("Portal parenting requires Wayland".into()); }; // The window lends live handles; the borrowed backend never owns the display. - let backend = - unsafe { wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) }; + let backend = unsafe { + wayland_client::backend::Backend::from_foreign_display(display.display.as_ptr().cast()) + }; let connection = Connection::from_backend(backend); let id = unsafe { wayland_client::backend::ObjectId::from_ptr( @@ -114,15 +119,19 @@ impl Parent { ) } .map_err(|error| error.to_string())?; - let surface = wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?; - let (globals, mut queue) = registry_queue_init::(&connection).map_err(|error| error.to_string())?; + let surface = + wl_surface::WlSurface::from_id(&connection, id).map_err(|error| error.to_string())?; + let (globals, mut queue) = + registry_queue_init::(&connection).map_err(|error| error.to_string())?; let qh = queue.handle(); let importer = globals .bind::(&qh, 1..=1, ()) .map_err(|error| error.to_string())?; let imported = importer.import_toplevel(handle.into(), &qh, ()); let mut state = State::default(); - queue.roundtrip(&mut state).map_err(|error| error.to_string())?; + queue + .roundtrip(&mut state) + .map_err(|error| error.to_string())?; if state.invalid { imported.destroy(); importer.destroy(); @@ -197,7 +206,9 @@ mod tests { #[test] fn an_x11_parent_is_never_reinterpreted_as_a_wayland_handle() { match classify_parent("x11:0x2400003") { - ParentKind::Wayland(handle) => panic!("an X11 parent must not become a Wayland handle: {handle}"), + ParentKind::Wayland(handle) => { + panic!("an X11 parent must not become a Wayland handle: {handle}") + } ParentKind::X11 => {} other => panic!("unexpected classification: {other:?}"), } diff --git a/portal/src/permissions.rs b/portal/src/permissions.rs index 7724843..4167497 100644 --- a/portal/src/permissions.rs +++ b/portal/src/permissions.rs @@ -58,12 +58,19 @@ fn usb_label(info: &Options, id: &str) -> String { .and_then(|value| HashMap::::try_from(value).ok()) .unwrap_or_default(); let property = |keys: &[&str]| { - keys.iter() - .find_map(|key| properties.get(*key).and_then(|value| <&str>::try_from(value).ok())) + keys.iter().find_map(|key| { + properties + .get(*key) + .and_then(|value| <&str>::try_from(value).ok()) + }) }; let vendor = property(&["ID_VENDOR_FROM_DATABASE", "ID_VENDOR", "ID_VENDOR_ID"]); let model = property(&["ID_MODEL_FROM_DATABASE", "ID_MODEL", "ID_MODEL_ID"]); - let label = [vendor, model].into_iter().flatten().collect::>().join(" "); + let label = [vendor, model] + .into_iter() + .flatten() + .collect::>() + .join(" "); let label = if label.is_empty() { id } else { &label }; label .chars() @@ -97,11 +104,27 @@ async fn usb_consent( description.push_str(&format!( "\n{} — {}", label, - if writable { "read and write" } else { "read only" } + if writable { + "read and write" + } else { + "read only" + } + )); + selected.push(( + id, + HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]), )); - selected.push((id, HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]))); } - if !consent(app, parent, "USB device access", &description, "Allow access", None).await? { + if !consent( + app, + parent, + "USB device access", + &description, + "Allow access", + None, + ) + .await? + { return Ok(None); } Ok(Some(HashMap::from([( @@ -131,7 +154,9 @@ impl Background { loop { let result = async { let path = ferese_ipc::socket::resolve(None).map_err(|error| error.to_string())?; - let stream = UnixStream::connect(path).await.map_err(|error| error.to_string())?; + let stream = UnixStream::connect(path) + .await + .map_err(|error| error.to_string())?; let mut events = BackgroundEvents::subscribe(stream).await?; loop { let next = match events.next().await { @@ -187,7 +212,10 @@ impl BackgroundEvents { let mut bytes = Vec::new(); ferese_ipc::write_frame(&mut bytes, &request).map_err(|error| error.to_string())?; tokio::time::timeout(Duration::from_secs(2), async { - stream.write_all(&bytes).await.map_err(|error| error.to_string())?; + stream + .write_all(&bytes) + .await + .map_err(|error| error.to_string())?; let response: ferese_ipc::Response = read_event_frame(&mut stream).await?; if response.version != ferese_ipc::VERSION || response.id != request.id { return Err("Unexpected event subscription response".into()); @@ -195,7 +223,10 @@ impl BackgroundEvents { if let Some(error) = response.error { return Err(error.message); } - if response.result.as_ref().and_then(|value| value["version"].as_u64()) + if response + .result + .as_ref() + .and_then(|value| value["version"].as_u64()) != Some(u64::from(ferese_ipc::events::VERSION)) { return Err("Unsupported event stream version".into()); @@ -226,8 +257,9 @@ impl BackgroundEvents { } Change::FocusChanged { focus } => { for window in self.windows.as_array_mut().into_iter().flatten() { - window["focused"] = - serde_json::json!(focus["window"].is_number() && window["id"] == focus["window"]); + window["focused"] = serde_json::json!( + focus["window"].is_number() && window["id"] == focus["window"] + ); } self.changed = true; } @@ -241,13 +273,18 @@ impl BackgroundEvents { } } -async fn read_event_frame(stream: &mut UnixStream) -> Result { +async fn read_event_frame( + stream: &mut UnixStream, +) -> Result { let size = stream.read_u32().await.map_err(|error| error.to_string())? as usize; if size > ferese_ipc::MAX_PAYLOAD_SIZE { return Err("Oversized IPC event frame".into()); } let mut bytes = vec![0; size]; - stream.read_exact(&mut bytes).await.map_err(|error| error.to_string())?; + stream + .read_exact(&mut bytes) + .await + .map_err(|error| error.to_string())?; serde_json::from_slice(&bytes).map_err(|error| error.to_string()) } @@ -299,7 +336,8 @@ impl Background { let directory = dirs::config_dir() .ok_or_else(|| zbus::fdo::Error::Failed("Missing config directory".into()))? .join("autostart"); - set_autostart(&directory, &app_id, enable, &commandline, flags).map_err(zbus::fdo::Error::InvalidArgs) + set_autostart(&directory, &app_id, enable, &commandline, flags) + .map_err(zbus::fdo::Error::InvalidArgs) } #[zbus(signal)] @@ -340,7 +378,13 @@ fn app_states(windows: &serde_json::Value) -> HashMap { apps } -fn set_autostart(directory: &Path, app: &str, enable: bool, argv: &[String], flags: u32) -> Result { +fn set_autostart( + directory: &Path, + app: &str, + enable: bool, + argv: &[String], + flags: u32, +) -> Result { if app.is_empty() || app.len() > 255 || !app @@ -367,7 +411,9 @@ fn set_autostart(directory: &Path, app: &str, enable: bool, argv: &[String], fla } return Ok(true); } - if argv.len() > 128 || (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0) { + if argv.len() > 128 + || (argv.first().is_none_or(|argument| argument.is_empty()) && flags & 1 == 0) + { return Err("Invalid autostart command".into()); } let exec = argv @@ -450,7 +496,11 @@ mod tests { request.args, serde_json::json!({"version": ferese_ipc::events::VERSION}) ); - send_frame(&mut peer, &ferese_ipc::Response::success(request.id, request.args)).await; + send_frame( + &mut peer, + &ferese_ipc::Response::success(request.id, request.args), + ) + .await; peer }); let mut events = BackgroundEvents::subscribe(client).await.unwrap(); @@ -547,7 +597,9 @@ mod tests { windows: serde_json::json!([]), changed: false, }; - peer.write_u32((ferese_ipc::MAX_PAYLOAD_SIZE + 1) as u32).await.unwrap(); + peer.write_u32((ferese_ipc::MAX_PAYLOAD_SIZE + 1) as u32) + .await + .unwrap(); assert!(events.next().await.unwrap_err().contains("Oversized")); drop(peer); assert!(events.next().await.is_err()); @@ -575,9 +627,15 @@ mod tests { "ID_VENDOR_FROM_DATABASE".to_owned(), Value::from("Example Vendor").try_to_owned().unwrap(), ), - ("ID_MODEL".to_owned(), Value::from("Gamepad").try_to_owned().unwrap()), + ( + "ID_MODEL".to_owned(), + Value::from("Gamepad").try_to_owned().unwrap(), + ), ]); - let info = HashMap::from([("properties".into(), Value::from(properties).try_to_owned().unwrap())]); + let info = HashMap::from([( + "properties".into(), + Value::from(properties).try_to_owned().unwrap(), + )]); assert_eq!(usb_label(&info, "opaque-id"), "Example Vendor Gamepad"); assert_eq!(usb_label(&Options::new(), "fallback-id"), "fallback-id"); } diff --git a/portal/src/picker.rs b/portal/src/picker.rs index 3204fcc..9a4c832 100644 --- a/portal/src/picker.rs +++ b/portal/src/picker.rs @@ -86,7 +86,9 @@ struct Picker { pub fn run() -> Result<(), Box> { let mut input = String::new(); - std::io::stdin().take(256 * 1024 + 1).read_to_string(&mut input)?; + std::io::stdin() + .take(256 * 1024 + 1) + .read_to_string(&mut input)?; if input.len() > 256 * 1024 { return Err("Picker request is too large".into()); } @@ -118,7 +120,12 @@ pub fn run() -> Result<(), Box> { impl cosmic::Application for Picker { type Executor = cosmic::executor::Default; - type Flags = (Prompt, cosmic::iced::Color, ferese_theme::Palette, cosmic::font::Font); + type Flags = ( + Prompt, + cosmic::iced::Color, + ferese_theme::Palette, + cosmic::font::Font, + ); type Message = Message; const APP_ID: &'static str = "dev.ferese.ScreenShare"; @@ -130,7 +137,10 @@ impl cosmic::Application for Picker { &mut self.core } - fn init(mut core: Core, (prompt, background, palette, font): Self::Flags) -> (Self, Task) { + fn init( + mut core: Core, + (prompt, background, palette, font): Self::Flags, + ) -> (Self, Task) { core.window.show_headerbar = false; core.window.border_padding = Some(0); core.window.content_container = false; @@ -171,7 +181,8 @@ impl cosmic::Application for Picker { fn subscription(&self) -> cosmic::iced::Subscription { cosmic::iced::Subscription::batch([ - ferese_theme_client::service::subscription().map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), + ferese_theme_client::service::subscription() + .map(|snapshot| Message::ThemeChanged(Box::new(snapshot))), cosmic::iced::event::listen_with(|event, _, id| match event { cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => { Some(Message::WindowOpened(id)) @@ -185,7 +196,8 @@ impl cosmic::Application for Picker { match message { Message::ThemeChanged(snapshot) => { self.palette = ferese_theme::Palette::from_resolved(&snapshot.presented).flat(); - self.font = ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); + self.font = + ferese_theme::font(Some(&snapshot.presented.tokens.typography.font_family)); self.background = cosmic::iced::Color { a: ferese_theme::material_opacity(&snapshot.presented), ..self.palette.sidebar @@ -232,7 +244,11 @@ impl cosmic::Application for Picker { Message::Audio(audio) if self.prompt.recording => self.audio = audio, Message::Region if self.prompt.recording => self.region = !self.region, Message::Share if !self.selected.is_empty() => { - let names: Vec<_> = self.selected.iter().map(|i| &self.prompt.sources[*i].name).collect(); + let names: Vec<_> = self + .selected + .iter() + .map(|i| &self.prompt.sources[*i].name) + .collect(); if self.prompt.window_capture { println!("{}", serde_json::to_string(&names).unwrap()); } else { @@ -306,20 +322,26 @@ impl cosmic::Application for Picker { for (index, source) in self.prompt.sources.iter().enumerate() { let is_window = self.prompt.window_capture || source.window_id().is_some(); if !self.prompt.window_capture && previous_kind != Some(is_window) { - sources = sources.push(self.text(if is_window { "Windows" } else { "Displays" }).size(12)); + sources = sources.push( + self.text(if is_window { "Windows" } else { "Displays" }) + .size(12), + ); previous_kind = Some(is_window); } let selected = self.selected.contains(&index); let mut entry = row![ glyph(ferese_theme::icons::DISPLAY, 24), column![ - self.text(if self.prompt.window_capture || source.window_id().is_some() { - &source.label - } else { - &source.name - }) + self.text( + if self.prompt.window_capture || source.window_id().is_some() { + &source.label + } else { + &source.name + } + ) .size(14), - self.text(format!("{} × {}", source.width, source.height)).size(12) + self.text(format!("{} × {}", source.width, source.height)) + .size(12) ] .spacing(3) .width(Length::Fill), @@ -357,12 +379,16 @@ impl cosmic::Application for Picker { content = content.push( row![ self.text("Select a region").width(Length::Fill), - ferese_theme::controls::switch(self.region, self.palette).on_press(Message::Region), + ferese_theme::controls::switch(self.region, self.palette) + .on_press(Message::Region), ] .align_y(Alignment::Center), ); let mut audio = column([]).spacing(4); - for choices in [[Audio::Off, Audio::System], [Audio::Microphone, Audio::Both]] { + for choices in [ + [Audio::Off, Audio::System], + [Audio::Microphone, Audio::Both], + ] { let mut choices_row = row([]).spacing(4); for choice in choices { choices_row = choices_row.push( @@ -404,7 +430,11 @@ impl cosmic::Application for Picker { let share = ferese_theme::controls::text_button( if self.prompt.recording { - if self.region { "Select region" } else { "Record" } + if self.region { + "Select region" + } else { + "Record" + } } else if self.prompt.window_capture { "Capture" } else { @@ -422,9 +452,13 @@ impl cosmic::Application for Picker { self.background.a, )) .on_press(Message::Cancel); - let footer = row![cosmic::iced::widget::Space::new().width(Length::Fill), cancel, share] - .spacing(10) - .align_y(Alignment::Center); + let footer = row![ + cosmic::iced::widget::Space::new().width(Length::Fill), + cancel, + share + ] + .spacing(10) + .align_y(Alignment::Center); // Reserve space for the actions even when the compositor shrinks the // dialog. The source list and options share the remaining scroll area. container( @@ -447,10 +481,11 @@ impl Picker { fn can_remember(&self) -> bool { !self.selected.is_empty() && self.selected.len() <= 16 - && self - .selected - .iter() - .all(|index| self.prompt.rememberable.contains(&self.prompt.sources[*index].name)) + && self.selected.iter().all(|index| { + self.prompt + .rememberable + .contains(&self.prompt.sources[*index].name) + }) } fn text<'a>( diff --git a/portal/src/record_main.rs b/portal/src/record_main.rs index 0381d78..dc0e819 100644 --- a/portal/src/record_main.rs +++ b/portal/src/record_main.rs @@ -2,9 +2,11 @@ mod recorder; mod recording; fn main() { - if let Err(error) = tokio::runtime::Runtime::new() - .and_then(|runtime| runtime.block_on(recorder::run()).map_err(std::io::Error::other)) - { + if let Err(error) = tokio::runtime::Runtime::new().and_then(|runtime| { + runtime + .block_on(recorder::run()) + .map_err(std::io::Error::other) + }) { recorder::event("error", Some(&error.to_string()), None); std::process::exit(1); } diff --git a/portal/src/recorder.rs b/portal/src/recorder.rs index 34cfae9..e7d4bb9 100644 --- a/portal/src/recorder.rs +++ b/portal/src/recorder.rs @@ -38,15 +38,32 @@ async fn request( body: &B, stop: &mut watch::Receiver, ) -> Result, String> { - let sender = connection.unique_name().ok_or("No D-Bus identity")?.as_str()[1..].replace('.', "_"); + let sender = connection + .unique_name() + .ok_or("No D-Bus identity")? + .as_str()[1..] + .replace('.', "_"); let path = format!("{DESKTOP_PATH}/request/{sender}/{token}"); - let request = Proxy::new(connection, DESKTOP, path.as_str(), "org.freedesktop.portal.Request") - .await - .map_err(|e| e.to_string())?; - let mut responses = request.receive_signal("Response").await.map_err(|e| e.to_string())?; - let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast") + let request = Proxy::new( + connection, + DESKTOP, + path.as_str(), + "org.freedesktop.portal.Request", + ) + .await + .map_err(|e| e.to_string())?; + let mut responses = request + .receive_signal("Response") .await .map_err(|e| e.to_string())?; + let portal = Proxy::new( + connection, + DESKTOP, + DESKTOP_PATH, + "org.freedesktop.portal.ScreenCast", + ) + .await + .map_err(|e| e.to_string())?; let returned: OwnedObjectPath = portal .call(method, body) .await @@ -78,7 +95,14 @@ async fn request( pub async fn run() -> Result<(), String> { gst::init().map_err(|e| e.to_string())?; - for factory in ["pipewiresrc", "queue", "videoconvert", "vp8enc", "webmmux", "fdsink"] { + for factory in [ + "pipewiresrc", + "queue", + "videoconvert", + "vp8enc", + "webmmux", + "fdsink", + ] { if gst::ElementFactory::find(factory).is_none() { return Err(format!( "Recording needs the GStreamer {factory} plugin. See the recording installation guide." @@ -112,10 +136,13 @@ pub async fn run() -> Result<(), String> { return Ok(()); }; - let handle = created.get("session_handle").ok_or("Portal returned no session")?; + let handle = created + .get("session_handle") + .ok_or("Portal returned no session")?; // The public portal historically returns this path as a D-Bus string. let handle = OwnedObjectPath::try_from( - String::try_from(handle.try_clone().map_err(|e| e.to_string())?).map_err(|e| e.to_string())?, + String::try_from(handle.try_clone().map_err(|e| e.to_string())?) + .map_err(|e| e.to_string())?, ) .map_err(|e| e.to_string())?; // Older/foreign backends keep their normal sharing indicator. Only Ferese @@ -129,16 +156,32 @@ pub async fn run() -> Result<(), String> { "org.ferese.ScreenRecorder", ) .await?; - control.call::<_, _, ()>("UseBarControls", &(handle.clone(),)).await + control + .call::<_, _, ()>("UseBarControls", &(handle.clone(),)) + .await }) .await, Ok(Ok(())) ); - let session = Proxy::new(&connection, DESKTOP, handle.as_str(), "org.freedesktop.portal.Session") - .await - .map_err(|e| e.to_string())?; - let result = record_session(&connection, &session, &handle, &token, &output, &mut stop, native).await; + let session = Proxy::new( + &connection, + DESKTOP, + handle.as_str(), + "org.freedesktop.portal.Session", + ) + .await + .map_err(|e| e.to_string())?; + let result = record_session( + &connection, + &session, + &handle, + &token, + &output, + &mut stop, + native, + ) + .await; let _: Result<(), _> = session.call("Close", &()).await; match result { @@ -158,7 +201,10 @@ async fn record_session( stop: &mut watch::Receiver, native: bool, ) -> Result, String> { - let mut closed = session.receive_signal("Closed").await.map_err(|e| e.to_string())?; + let mut closed = session + .receive_signal("Closed") + .await + .map_err(|e| e.to_string())?; if request( connection, @@ -187,7 +233,11 @@ async fn record_session( connection, "Start", &token(2), - &(handle, "", HashMap::from([("handle_token", option_string(token(2)))])), + &( + handle, + "", + HashMap::from([("handle_token", option_string(token(2)))]), + ), stop, ) .await? @@ -216,21 +266,30 @@ async fn record_session( ) .await .map_err(|e| e.to_string())?; - let options: String = - tokio::time::timeout(Duration::from_secs(3), control.call("RecordingOptions", &(handle,))) - .await - .map_err(|_| "Recording options timed out")? - .map_err(|e| { - format!("Cannot read recording options; update the Ferese portal and recorder together: {e}") - })?; + let options: String = tokio::time::timeout( + Duration::from_secs(3), + control.call("RecordingOptions", &(handle,)), + ) + .await + .map_err(|_| "Recording options timed out")? + .map_err(|e| { + format!( + "Cannot read recording options; update the Ferese portal and recorder together: {e}" + ) + })?; serde_json::from_str::(&options).map_err(|e| e.to_string())? } else { Recording::default() }; - let portal = Proxy::new(connection, DESKTOP, DESKTOP_PATH, "org.freedesktop.portal.ScreenCast") - .await - .map_err(|e| e.to_string())?; + let portal = Proxy::new( + connection, + DESKTOP, + DESKTOP_PATH, + "org.freedesktop.portal.ScreenCast", + ) + .await + .map_err(|e| e.to_string())?; let remote: OwnedFd = portal .call("OpenPipeWireRemote", &(handle, Options::new())) .await @@ -281,7 +340,8 @@ fn output_directory() -> Result { } fn recording_file(directory: &Path) -> Result<(File, PathBuf, PathBuf), String> { - std::fs::create_dir_all(directory).map_err(|e| format!("Cannot create recording folder: {e}"))?; + std::fs::create_dir_all(directory) + .map_err(|e| format!("Cannot create recording folder: {e}"))?; let name = format!( "Recording-{}-{}", @@ -392,7 +452,8 @@ fn encode( gst::Element::link_many([&source, &queue, &filter, &convert, &encoder, &mux, &sink]) .map_err(|e| e.to_string())?; } else { - gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink]).map_err(|e| e.to_string())?; + gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink]) + .map_err(|e| e.to_string())?; } let mut audio_inputs = Vec::new(); @@ -463,13 +524,17 @@ fn encode( .static_pad("src") .ok_or("Capture has no video output")? .add_probe(gst::PadProbeType::BUFFER, move |_, info| { - if info.buffer().is_some_and(|b| b.size() > 0) && !notice.swap(true, Ordering::Relaxed) { + if info.buffer().is_some_and(|b| b.size() > 0) + && !notice.swap(true, Ordering::Relaxed) + { event("recording", None, None); } gst::PadProbeReturn::Ok }); - pipeline.set_state(gst::State::Playing).map_err(|e| e.to_string())?; + pipeline + .set_state(gst::State::Playing) + .map_err(|e| e.to_string())?; let bus = pipeline.bus().ok_or("Encoder has no message bus")?; let mut finishing = None; @@ -545,7 +610,8 @@ fn encode( result.map_err(|e| format!("{e}. Incomplete file: {}", partial.display()))?; file.sync_all() .map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?; - std::fs::rename(&partial, &destination).map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?; + std::fs::rename(&partial, &destination) + .map_err(|e| format!("Cannot finish saving {}: {e}", partial.display()))?; Ok(destination) } @@ -560,7 +626,10 @@ mod tests { let (_, second, _) = recording_file(directory.path()).unwrap(); assert_ne!(first, second); use std::os::unix::fs::PermissionsExt; - assert_eq!(std::fs::metadata(first).unwrap().permissions().mode() & 0o777, 0o600); + assert_eq!( + std::fs::metadata(first).unwrap().permissions().mode() & 0o777, + 0o600 + ); } #[test] #[ignore = "requires an explicitly provided temporary PipeWire test node"] @@ -629,16 +698,31 @@ mod tests { fn encoder_captures_isolated_system_and_microphone_audio() { // The isolated fixture plays 440 Hz on the default output and 660 Hz // on the default microphone. Both tones must survive combined capture. - let server = std::env::var("FERESE_TEST_RECORD_PULSE_SERVER").expect("isolated audio server required"); + let server = std::env::var("FERESE_TEST_RECORD_PULSE_SERVER") + .expect("isolated audio server required"); assert_eq!(std::env::var("PULSE_SERVER").unwrap(), server); - let socket = Path::new(server.strip_prefix("unix:").expect("explicit Unix socket required")); + let socket = Path::new( + server + .strip_prefix("unix:") + .expect("explicit Unix socket required"), + ); assert!(socket.starts_with(std::env::temp_dir())); - for audio in [Audio::System, Audio::Microphone, Audio::Both, Audio::Both, Audio::Both] { + for audio in [ + Audio::System, + Audio::Microphone, + Audio::Both, + Audio::Both, + Audio::Both, + ] { assert_cropped_audio_recording(false, 0, Some(audio)); } } - fn assert_cropped_audio_recording(video_eos: bool, audio_count: usize, selected_audio: Option