From 46509f880a3f7c38c87fe8eb15700165a787b87f Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Wed, 30 Sep 2026 06:39:58 +0100 Subject: [PATCH] Fix compositor output routing and pointer hints; format imports --- compositor/src/backends/direct.rs | 394 +++------ compositor/src/backends/mod.rs | 43 +- compositor/src/config.rs | 425 +++------- compositor/src/cursor.rs | 53 +- compositor/src/daemon.rs | 34 +- compositor/src/dimming.rs | 12 +- compositor/src/effects.rs | 113 +-- compositor/src/grabs/move_grab.rs | 31 +- compositor/src/grabs/resize_grab.rs | 92 +-- compositor/src/handlers/activation.rs | 17 +- compositor/src/handlers/compositor.rs | 47 +- compositor/src/handlers/input_method.rs | 20 +- compositor/src/handlers/layer_shell.rs | 57 +- compositor/src/handlers/mod.rs | 95 +-- compositor/src/handlers/screencopy.rs | 78 +- compositor/src/handlers/screenshot.rs | 268 ++---- compositor/src/handlers/screenshot_worker.rs | 35 +- compositor/src/handlers/window_capture.rs | 93 +-- compositor/src/handlers/xdg_shell.rs | 155 ++-- compositor/src/input.rs | 305 ++++--- compositor/src/input_capture.rs | 101 +-- compositor/src/ipc.rs | 388 ++++----- compositor/src/main.rs | 48 +- compositor/src/metrics.rs | 29 +- compositor/src/overview.rs | 211 ++--- compositor/src/portal_session.rs | 132 +-- compositor/src/portal_shortcuts.rs | 64 +- compositor/src/presentation.rs | 164 ++-- compositor/src/private_client.rs | 32 +- compositor/src/reload.rs | 52 +- compositor/src/resize_transaction.rs | 3 +- compositor/src/session_lock.rs | 118 +-- compositor/src/shell_control.rs | 122 +-- compositor/src/stacking.rs | 26 +- compositor/src/state.rs | 818 +++++++------------ compositor/src/wallpaper.rs | 120 +-- compositor/src/window_rules.rs | 40 +- compositor/src/winit.rs | 709 +++++----------- crates/ferese-animation/src/lib.rs | 138 +--- crates/ferese-config/src/lib.rs | 155 +--- crates/ferese-core/src/desktop.rs | 43 +- crates/ferese-core/src/lib.rs | 4 +- crates/ferese-core/src/output.rs | 203 +---- crates/ferese-core/src/workspace.rs | 426 +++------- crates/ferese-ipc/src/lib.rs | 9 +- crates/ferese-layout/src/lib.rs | 258 ++---- crates/ferese-layout/src/scrolling.rs | 308 ++----- crates/ferese-protocols/src/lib.rs | 16 +- crates/ferese-theme/src/button.rs | 15 +- crates/ferese-theme/src/calendar.rs | 75 +- crates/ferese-theme/src/contrast.rs | 53 +- crates/ferese-theme/src/controls.rs | 32 +- crates/ferese-theme/src/icons.rs | 20 +- crates/ferese-theme/src/material.rs | 26 +- crates/ferese-theme/src/menus.rs | 41 +- crates/ferese-theme/src/palette.rs | 12 +- crates/ferese-theme/src/typography.rs | 6 +- feresectl/src/autostart.rs | 35 +- feresectl/src/main.rs | 25 +- portal/src/backend.rs | 212 ++--- portal/src/bridge.rs | 25 +- portal/src/capture.rs | 116 +-- portal/src/consent.rs | 34 +- portal/src/desktop.rs | 180 ++-- portal/src/eis.rs | 148 ++-- portal/src/inhibit.rs | 166 ++-- portal/src/input_capture.rs | 155 ++-- portal/src/lockdown.rs | 28 +- portal/src/main.rs | 12 +- portal/src/parent.rs | 35 +- portal/src/permissions.rs | 88 +- portal/src/picker.rs | 96 +-- portal/src/record_main.rs | 8 +- portal/src/recorder.rs | 140 +--- portal/src/restore.rs | 81 +- portal/src/settings.rs | 30 +- portal/src/shortcuts.rs | 107 +-- portal/src/stream.rs | 73 +- rustfmt.toml | 4 + shell/ferese-lock/src/appearance.rs | 21 +- shell/ferese-lock/src/auth.rs | 28 +- shell/ferese-lock/src/main.rs | 117 +-- shell/ferese-lock/src/runtime.rs | 23 +- shell/ferese-polkit-agent/src/main.rs | 38 +- shell/ferese-polkit-agent/src/prompt.rs | 81 +- shell/ferese-polkit-agent/src/session.rs | 82 +- shell/ferese-settings/src/fonts.rs | 3 +- shell/ferese-settings/src/main.rs | 315 +++---- shell/ferese-settings/src/schema.rs | 31 +- shell/ferese-settings/src/store.rs | 87 +- shell/ferese-settings/src/visuals.rs | 71 +- shell/ferese-settings/src/watch.rs | 30 +- shell/ferese-shell/src/compositor_ipc.rs | 63 +- shell/ferese-shell/src/config.rs | 45 +- shell/ferese-shell/src/control.rs | 62 +- shell/ferese-shell/src/keybinding_guide.rs | 3 +- shell/ferese-shell/src/main.rs | 697 ++++------------ shell/ferese-shell/src/motion.rs | 144 +--- shell/ferese-shell/src/note_store.rs | 16 +- shell/ferese-shell/src/notification_ui.rs | 203 ++--- shell/ferese-shell/src/notifications.rs | 101 +-- shell/ferese-shell/src/recording.rs | 34 +- shell/ferese-shell/src/status.rs | 131 +-- shell/ferese-shell/src/status/bluetooth.rs | 33 +- shell/ferese-shell/src/status/dbus_cache.rs | 57 +- shell/ferese-shell/src/status/network.rs | 14 +- shell/ferese-shell/src/status_ui.rs | 211 ++--- shell/ferese-shell/src/system_modal.rs | 130 +-- 108 files changed, 3682 insertions(+), 8371 deletions(-) create mode 100644 rustfmt.toml diff --git a/compositor/src/backends/direct.rs b/compositor/src/backends/direct.rs index 2ce1390..64774ad 100644 --- a/compositor/src/backends/direct.rs +++ b/compositor/src/backends/direct.rs @@ -1,57 +1,41 @@ -use std::{ - collections::{HashMap, HashSet}, - error::Error, - io, - path::Path, - time::{Duration, Instant}, -}; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::io; +use std::path::Path; +use std::time::{Duration, Instant}; -use smithay::{ - backend::{ - allocator::{ - Fourcc, - gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}, - }, - drm::{ - DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType, - }, - egl::{EGLContext, EGLDisplay}, - input::InputEvent, - libinput::{LibinputInputBackend, LibinputSessionInterface}, - renderer::{ - Bind, Frame, ImportDma, ImportMemWl, Renderer, damage::OutputDamageTracker, - gles::GlesRenderer, - }, - session::{Event as SessionEvent, Session, libseat::LibSeatSession}, - udev::{UdevBackend, UdevEvent, primary_gpu}, - }, - desktop::{layer_map_for_output, utils::OutputPresentationFeedback}, - output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}, - reexports::{ - calloop::{ - EventLoop, RegistrationToken, - timer::{TimeoutAction, Timer}, - }, - drm::control::{ - Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, - }, - input::{Device as LibinputDevice, Libinput}, - rustix::fs::OFlags, - wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind, - wayland_server::backend::GlobalId, - }, - utils::{DeviceFd, Monotonic, Time, Transform}, - wayland::{dmabuf::DmabufFeedbackBuilder, presentation::Refresh}, +use smithay::backend::allocator::Fourcc; +use smithay::backend::allocator::gbm::{GbmAllocator, GbmBufferFlags, GbmDevice}; +use smithay::backend::drm::{DrmDevice, DrmDeviceFd, DrmEvent, DrmEventTime, DrmNode, GbmBufferedSurface, NodeType}; +use smithay::backend::egl::{EGLContext, EGLDisplay}; +use smithay::backend::input::InputEvent; +use smithay::backend::libinput::{LibinputInputBackend, LibinputSessionInterface}; +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::gles::GlesRenderer; +use smithay::backend::renderer::{Bind, Frame, ImportDma, ImportMemWl, Renderer}; +use smithay::backend::session::libseat::LibSeatSession; +use smithay::backend::session::{Event as SessionEvent, Session}; +use smithay::backend::udev::{UdevBackend, UdevEvent, primary_gpu}; +use smithay::desktop::layer_map_for_output; +use smithay::desktop::utils::OutputPresentationFeedback; +use smithay::output::{Mode as OutputMode, Output, PhysicalProperties, Scale, Subpixel}; +use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; +use smithay::reexports::calloop::{EventLoop, RegistrationToken}; +use smithay::reexports::drm::control::{ + Device as ControlDevice, Mode as DrmMode, ModeTypeFlags, connector, crtc, property, }; +use smithay::reexports::input::{Device as LibinputDevice, Libinput}; +use smithay::reexports::rustix::fs::OFlags; +use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind; +use smithay::reexports::wayland_server::backend::GlobalId; +use smithay::utils::{DeviceFd, Monotonic, Time, Transform}; +use smithay::wayland::dmabuf::DmabufFeedbackBuilder; +use smithay::wayland::presentation::Refresh; -use crate::{ - Ferese, - config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}, - metrics::RenderMetrics, - winit::{ - animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output, - }, -}; +use crate::Ferese; +use crate::config::{OutputModeRequest, OutputProfile, OutputSettings, OutputTransform}; +use crate::metrics::RenderMetrics; +use crate::winit::{animated_window_elements, cursorless_window_elements, frame_effect_metrics, redraw_output}; pub struct DirectBackendState { pub session: LibSeatSession, @@ -72,14 +56,12 @@ 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::winit::capture_resize_snapshot( @@ -100,12 +82,7 @@ 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::winit::capture_window_buffer( &mut device.renderer, @@ -124,12 +101,7 @@ 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::winit::capture_window_frame( &mut device.renderer, @@ -212,12 +184,7 @@ 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()); @@ -254,9 +221,7 @@ 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); } } event => state.process_input_event(event), @@ -275,8 +240,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() device.drm.pause(); device.outputs.values_mut().for_each(|output| { output.surface.reset_buffers(); - output.damage_tracker = - OutputDamageTracker::from_output(&output.output); + output.damage_tracker = OutputDamageTracker::from_output(&output.output); output.frame_pending = false; output.power_off = false; }); @@ -335,20 +299,12 @@ fn configure_libinput_device(state: &Ferese, device: &mut LibinputDevice) { if device.config_tap_finger_count() > 0 && let Err(error) = device.config_tap_set_enabled(settings.tap) { - tracing::warn!( - device = device.name(), - ?error, - "failed to configure tap-to-click" - ); + tracing::warn!(device = device.name(), ?error, "failed to configure tap-to-click"); } if device.config_scroll_has_natural_scroll() && let Err(error) = device.config_scroll_set_natural_scroll_enabled(settings.natural_scroll) { - tracing::warn!( - device = device.name(), - ?error, - "failed to configure natural scrolling" - ); + tracing::warn!(device = device.name(), ?error, "failed to configure natural scrolling"); } if device.config_dwt_is_available() && let Err(error) = device.config_dwt_set_enabled(settings.disable_while_typing) @@ -383,10 +339,7 @@ pub(crate) fn reload_outputs(state: &mut Ferese) { } } -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(()); }; @@ -414,10 +367,7 @@ fn open_primary_device( .session .clone(); let node = DrmNode::from_path(path)?; - let fd = session.open( - path, - OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK, - )?; + let fd = session.open(path, OFlags::RDWR | OFlags::CLOEXEC | OFlags::NOCTTY | OFlags::NONBLOCK)?; let fd = DrmDeviceFd::new(DeviceFd::from(fd)); let (mut drm, notifier) = DrmDevice::new(fd.clone(), true)?; let gbm = GbmDevice::new(fd)?; @@ -443,10 +393,7 @@ fn open_primary_device( let mut scan = select_outputs(&drm, &state.output_profiles)?; apply_lid_policy( &mut scan, - state - .direct_backend - .as_ref() - .is_some_and(|backend| backend.lid_closed), + state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed), ); if scan.selections.is_empty() { return Err(io::Error::new( @@ -468,9 +415,7 @@ fn open_primary_device( mode, settings, } = selection; - let output = create_direct_output( - state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings, - )?; + let output = create_direct_output(state, &mut drm, &gbm, &renderer, connector, crtc, mode, &settings)?; state .direct_backend .as_mut() @@ -503,9 +448,7 @@ fn open_primary_device( None } }); - if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = - (feedback, metadata.time) - { + if let (Some(mut feedback), DrmEventTime::Monotonic(time)) = (feedback, metadata.time) { feedback.presented( Time::::from(time), Refresh::fixed(refresh_duration(state, node, crtc)), @@ -538,10 +481,7 @@ fn open_primary_device( } state.record_drm_presentation(node, crtc, metadata.time, metadata.sequence); tracing::trace!(?node, ?crtc, sequence = metadata.sequence, "page flip"); - let active = state - .direct_backend - .as_ref() - .is_some_and(|backend| backend.active); + let active = state.direct_backend.as_ref().is_some_and(|backend| backend.active); if active { let was_animating = state .direct_backend @@ -588,11 +528,7 @@ pub(crate) fn sleep_locked_outputs(state: &mut Ferese) { let Some(backend) = state.direct_backend.as_mut() 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_off || output.frame_pending { continue; @@ -621,11 +557,7 @@ pub(crate) fn wake_locked_outputs(state: &mut Ferese) { } pub fn render_all(state: &mut Ferese) { - if state - .direct_backend - .as_ref() - .is_none_or(|backend| !backend.active) - { + if state.direct_backend.as_ref().is_none_or(|backend| !backend.active) { return; } let animating = state.advance_animations(Instant::now()); @@ -692,9 +624,7 @@ fn initial_lid_closed() -> bool { 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 ) } @@ -767,8 +697,7 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { state.process_dmabuf_imports(&mut device.renderer); let (mut buffer, age) = output.surface.next_buffer()?; let elements = animated_window_elements(state, &mut device.renderer, &output.output); - let effects = - frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); + let effects = frame_effect_metrics(&elements, output.output.current_scale().fractional_scale()); let mut framebuffer = device.renderer.bind(&mut buffer)?; let result = output.damage_tracker.render_output( &mut device.renderer, @@ -778,12 +707,10 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { [0.035, 0.04, 0.055, 1.0], )?; let cursorless_capture = state.has_pending_screencopy(&output.output, false); - let captured_with_cursor = - state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true); + let captured_with_cursor = state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, true); if cursorless_capture { - let cursorless_elements = - cursorless_window_elements(state, &mut device.renderer, &output.output); + let cursorless_elements = cursorless_window_elements(state, &mut device.renderer, &output.output); redraw_output( &mut device.renderer, &mut framebuffer, @@ -791,22 +718,13 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { &cursorless_elements, )?; state.process_screencopies(&mut device.renderer, &framebuffer, &output.output, false); - redraw_output( - &mut device.renderer, - &mut framebuffer, - &output.output, - &elements, - )?; + redraw_output(&mut device.renderer, &mut framebuffer, &output.output, &elements)?; } else if captured_with_cursor { let _ = device .renderer .render( &mut framebuffer, - output - .output - .current_mode() - .expect("output has a mode") - .size, + output.output.current_mode().expect("output has a mode").size, output.output.current_transform(), )? .finish()?; @@ -818,34 +736,18 @@ fn render_output(state: &mut Ferese, node: DrmNode, crtc: crtc::Handle) { return Ok(false); }; - let mut presentation = OutputPresentationFeedback::new(&output.output); - state.space.elements().for_each(|window| { - window.take_presentation_feedback( - &mut presentation, - |_, _| Some(output.output.clone()), - |_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion, - ); - }); - let layers = layer_map_for_output(&output.output) - .layers() - .cloned() - .collect::>(); - layers.iter().for_each(|layer| { - layer.take_presentation_feedback( - &mut presentation, - |_, _| Some(output.output.clone()), - |_, _| Kind::Vsync | Kind::HwClock | Kind::HwCompletion, - ); - }); + let presentation = crate::presentation::take_output_feedback( + state, + &output.output, + &result.states, + Kind::Vsync | Kind::HwClock | Kind::HwCompletion, + ); output .surface .queue_buffer(Some(result.sync), Some(damage.clone()), presentation)?; - 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; output.lock_frame_pending = state.session_lock.active; Ok(true) @@ -893,10 +795,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { return; } }; - let lid_closed = state - .direct_backend - .as_ref() - .is_some_and(|backend| backend.lid_closed); + let lid_closed = state.direct_backend.as_ref().is_some_and(|backend| backend.lid_closed); apply_lid_policy(&mut scan, lid_closed); if let Some(backend) = state.direct_backend.as_mut() { backend.connected_outputs = scan.connected_outputs; @@ -940,9 +839,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { Some(Scale::Fractional(output.settings.scale)), Some((position[0], position[1]).into()), ); - state - .space - .map_output(&output.output, (position[0], position[1])); + state.space.map_output(&output.output, (position[0], position[1])); if let Some(id) = state.output_id(&output.output) && let Some(g) = state.space.output_geometry(&output.output) { @@ -1045,10 +942,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { } for crtc in deferred_removals { - let keep_internal = device - .outputs - .get(&crtc) - .is_some_and(|output| output.internal) + let keep_internal = device.outputs.get(&crtc).is_some_and(|output| output.internal) && lid_closed && state.direct_backend.as_ref().is_some_and(|backend| { device.outputs.get(&crtc).is_some_and(|output| { @@ -1060,10 +954,7 @@ fn rescan_device(state: &mut Ferese, node: DrmNode) { }) && !device.outputs.values().any(|output| !output.internal); if keep_internal { - tracing::warn!( - ?crtc, - "keeping laptop panel enabled: external output activation failed" - ); + tracing::warn!(?crtc, "keeping laptop panel enabled: external output activation failed"); if let Some(output) = device.outputs.get(&crtc) && let Some(backend) = state.direct_backend.as_mut() && let Some(info) = backend @@ -1116,9 +1007,7 @@ fn remove_device(state: &mut Ferese, node: DrmNode) { let context = device.renderer.context_id().erased(); state.wallpaper.forget_context(&context); - state - .resize_snapshots - .retain(|_, snapshot| snapshot.context != context); + state.resize_snapshots.retain(|_, snapshot| snapshot.context != context); for (crtc, output) in device.outputs { state.display_handle.disable_global::(output.global); state.unregister_output(&output.output); @@ -1170,9 +1059,10 @@ fn arm_animation_timer(state: &mut Ferese) { match state .loop_handle .insert_source(Timer::from_deadline(deadline), |_, _, state| { - let active = state.direct_backend.as_ref().is_some_and(|backend| { - backend.active && backend.animation_active && !state.session_lock.sleeping - }); + let active = state + .direct_backend + .as_ref() + .is_some_and(|backend| backend.active && backend.animation_active && !state.session_lock.sleeping); if active { let deadline = state.animation_fallback_deadline(animation_interval(state)); if Instant::now() < deadline { @@ -1184,9 +1074,7 @@ fn arm_animation_timer(state: &mut Ferese) { .as_ref() .is_some_and(|backend| backend.animation_active) { - return TimeoutAction::ToInstant( - state.animation_fallback_deadline(animation_interval(state)), - ); + return TimeoutAction::ToInstant(state.animation_fallback_deadline(animation_interval(state))); } } if let Some(backend) = state.direct_backend.as_mut() { @@ -1244,12 +1132,7 @@ 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; }; @@ -1286,24 +1169,15 @@ fn send_frame_callbacks(state: &mut Ferese, output: &Output) { .is_some_and(|id| state.window_belongs_to_output(*id, output)) }) .for_each(|window| { - window.send_frame( - output, - state.start_time.elapsed(), - Some(Duration::ZERO), - |_, _| Some(output.clone()), - ); + window.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }); }); - 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(), - Some(Duration::ZERO), - |_, _| Some(output.clone()), - ); + layer.send_frame(output, state.start_time.elapsed(), Some(Duration::ZERO), |_, _| { + Some(output.clone()) + }); }); state.send_cursor_frame(output); state.space.refresh(); @@ -1365,12 +1239,7 @@ fn select_outputs(drm: &DrmDevice, profiles: &[OutputProfile]) -> io::Result OutputSettings { } } -fn select_mode( - connector: &connector::Info, - requested: Option, -) -> Option { +fn select_mode(connector: &connector::Info, requested: Option) -> Option { let fallback = || { connector .modes() @@ -1460,24 +1326,11 @@ fn select_mode( .filter(|mode| mode.size() == (requested.width, requested.height)); let selected = 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() }; if selected.is_none() { tracing::warn!( @@ -1504,10 +1357,7 @@ fn create_direct_output( ) -> Result> { let identity = connector_identity(drm, &connector); 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 surface = GbmBufferedSurface::new( drm_surface, allocator, @@ -1602,21 +1452,18 @@ fn output_transform(transform: OutputTransform) -> Transform { } fn connector_identity(drm: &DrmDevice, connector: &connector::Info) -> String { - let edid = 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, - } - }) - }); + let edid = 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, + } + }) + }); match edid { Some(edid) if !edid.is_empty() => format!("drm-edid:{:016x}", stable_hash(&edid)), @@ -1654,23 +1501,16 @@ 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); @@ -1709,9 +1549,7 @@ 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)); @@ -1744,10 +1582,7 @@ 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); } @@ -1770,10 +1605,7 @@ 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/mod.rs b/compositor/src/backends/mod.rs index d733e52..4316926 100644 --- a/compositor/src/backends/mod.rs +++ b/compositor/src/backends/mod.rs @@ -4,7 +4,8 @@ use std::ffi::OsString; use smithay::reexports::calloop::EventLoop; -use crate::{Ferese, private_client::ClientCapabilities}; +use crate::Ferese; +use crate::private_client::ClientCapabilities; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum BackendKind { @@ -21,16 +22,12 @@ 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(); @@ -99,10 +96,7 @@ 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)), } } @@ -161,42 +155,25 @@ 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/config.rs b/compositor/src/config.rs index 73800e0..0b9538d 100644 --- a/compositor/src/config.rs +++ b/compositor/src/config.rs @@ -1,9 +1,7 @@ use std::collections::{HashMap, HashSet}; use std::error::Error; -use std::fmt; -use std::fs; -use std::io; use std::path::PathBuf; +use std::{fmt, fs, io}; use ferese_animation::SpringConfig; use ferese_core::LayoutMode; @@ -11,7 +9,8 @@ use ferese_layout::{ColumnWidth, Direction, GapConfig, ViewportFocusStrategy}; use serde::Deserialize; use smithay::input::keyboard::{Keycode, keysyms, xkb}; -use crate::window_rules::{self, WindowRule, WindowRuleConfig}; +use crate::window_rules; +use crate::window_rules::{WindowRule, WindowRuleConfig}; #[derive(Debug, Default, Deserialize)] pub struct Config { @@ -163,11 +162,7 @@ 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") }; self.gradient .as_ref() @@ -274,18 +269,10 @@ impl Config { } pub(crate) fn runtime_config(&self) -> Result { - self.notifications - .validate() - .map_err(ConfigError::InvalidBinding)?; - self.desktop_widgets - .validate() - .map_err(ConfigError::InvalidBinding)?; + self.notifications.validate().map_err(ConfigError::InvalidBinding)?; + self.desktop_widgets.validate().map_err(ConfigError::InvalidBinding)?; for daemon in &self.autostart { - if daemon - .command - .first() - .is_none_or(|program| program.trim().is_empty()) - { + if daemon.command.first().is_none_or(|program| program.trim().is_empty()) { return Err(ConfigError::InvalidBinding( "autostart command must contain a program".into(), )); @@ -510,15 +497,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::InvalidBinding( - "Reserved system shortcut".into(), - )); + return Err(ConfigError::InvalidBinding("Reserved system shortcut".into())); } Ok(Self { modifiers, @@ -534,34 +516,19 @@ 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, @@ -572,11 +539,7 @@ impl Binding { pub(crate) fn guide_entry(&self, keymap: Option<&xkb::Keymap>) -> Option { let description = match &self.action { 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 { @@ -598,12 +561,9 @@ impl Binding { BindingAction::Resize(direction) => format!("Resize window {direction:?}"), BindingAction::SwitchWorkspace(index) => format!("Go to workspace {index}"), BindingAction::MoveToWorkspace(index) => format!("Move window to workspace {index}"), - BindingAction::SwitchRelativeWorkspace(next) => if *next { - "Next workspace" - } else { - "Previous workspace" + BindingAction::SwitchRelativeWorkspace(next) => { + if *next { "Next workspace" } else { "Previous workspace" }.into() } - .into(), BindingAction::ToggleFullscreen => "Toggle fullscreen".into(), BindingAction::ToggleMaximized => "Toggle full-width window".into(), BindingAction::ToggleLayout => "Switch workspace layout".into(), @@ -651,11 +611,7 @@ 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) } } @@ -1022,11 +978,7 @@ 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, @@ -1078,10 +1030,7 @@ impl Config { pub fn bindings(&self, input: &InputSettings) -> Result, ConfigError> { 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()], @@ -1197,19 +1146,13 @@ impl Config { } 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 @@ -1224,25 +1167,16 @@ 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_base_color: parse_color(&self.theme.colors.surface_base, "colors.surface_base")?, shell_opacity: unit_theme_value(self.theme.material.opacity, "material.opacity")?, inactive_dim: InactiveDimSettings { enabled: self.appearance.inactive_dim.enabled, - amount: unit_theme_value( - self.appearance.inactive_dim.amount, - "appearance.inactive_dim.amount", - )?, + amount: unit_theme_value(self.appearance.inactive_dim.amount, "appearance.inactive_dim.amount")?, duration_ms: nonnegative_theme_value( self.appearance.inactive_dim.duration_ms, "appearance.inactive_dim.duration_ms", @@ -1253,11 +1187,7 @@ 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), + backdrop_blur: nonnegative_theme_value(self.theme.material.blur_radius, "material.blur_radius")?.min(32.0), material_radius, panel_radius: material_radius, }) @@ -1273,8 +1203,7 @@ impl Config { pub fn spring_config(&self) -> Result { let mass = positive_animation_value(self.animations.spring.mass, "spring.mass")?; - let stiffness = - positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?; + let stiffness = positive_animation_value(self.animations.spring.stiffness, "spring.stiffness")?; let damping = self.animations.spring.damping; if !damping.is_finite() || damping < 0.0 { return Err(ConfigError::InvalidAnimationValue { @@ -1292,12 +1221,9 @@ impl Config { } pub fn viewport_spring_config(&self) -> Result { - let mass = - positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?; - let stiffness = positive_animation_value( - self.animations.viewport_spring.stiffness, - "viewport_spring.stiffness", - )?; + let mass = positive_animation_value(self.animations.viewport_spring.mass, "viewport_spring.mass")?; + let stiffness = + positive_animation_value(self.animations.viewport_spring.stiffness, "viewport_spring.stiffness")?; let damping_ratio = positive_animation_value( self.animations.viewport_spring.damping_ratio, "viewport_spring.damping_ratio", @@ -1322,9 +1248,7 @@ struct BindingIdentity { fn validate_commands(commands: &HashMap>) -> Result<(), ConfigError> { for (name, argv) in commands { if name.trim().is_empty() { - return Err(ConfigError::InvalidBinding( - "command names cannot be empty".to_owned(), - )); + return Err(ConfigError::InvalidBinding("command names cannot be empty".to_owned())); } if argv.is_empty() || argv[0].is_empty() { return Err(ConfigError::InvalidBinding(format!( @@ -1365,9 +1289,10 @@ fn parse_binding( } let identity = binding_identity(configured, keymap)?; - let action = configured.action.as_deref().ok_or_else(|| { - ConfigError::InvalidBinding(format!("binding {:?} has no action", configured.keys)) - })?; + let action = configured + .action + .as_deref() + .ok_or_else(|| ConfigError::InvalidBinding(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), @@ -1390,15 +1315,10 @@ fn parse_binding( }) } -fn binding_identity( - configured: &BindingConfig, - keymap: &xkb::Keymap, -) -> Result { +fn binding_identity(configured: &BindingConfig, keymap: &xkb::Keymap) -> 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::InvalidBinding( "swipes cannot use keyboard modifiers or physical key matching".into(), )); @@ -1438,9 +1358,7 @@ 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 { @@ -1456,9 +1374,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> { for component in chord.split('+').map(str::trim) { if component.is_empty() { - return Err(ConfigError::InvalidBinding(format!( - "invalid key chord {chord:?}" - ))); + return Err(ConfigError::InvalidBinding(format!("invalid key chord {chord:?}"))); } let slot = match component.to_ascii_lowercase().as_str() { @@ -1470,9 +1386,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> { }; if let Some(slot) = slot { if *slot { - return Err(ConfigError::InvalidBinding(format!( - "duplicate modifier in {chord:?}" - ))); + return Err(ConfigError::InvalidBinding(format!("duplicate modifier in {chord:?}"))); } *slot = true; } else if key.replace(component.to_owned()).is_some() { @@ -1482,9 +1396,7 @@ fn parse_chord(chord: &str) -> Result<(BindingModifiers, String), ConfigError> { } } - let key = key.ok_or_else(|| { - ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")) - })?; + let key = key.ok_or_else(|| ConfigError::InvalidBinding(format!("key chord {chord:?} does not contain a key")))?; Ok((modifiers, key)) } @@ -1520,9 +1432,7 @@ fn parse_keysym(name: &str) -> Result { symbol = xkb::keysym_from_name(&normalized, xkb::KEYSYM_CASE_INSENSITIVE); } if symbol.raw() == keysyms::KEY_NoSymbol { - Err(ConfigError::InvalidBinding(format!( - "unknown keysym {name:?}" - ))) + Err(ConfigError::InvalidBinding(format!("unknown keysym {name:?}"))) } else { Ok(symbol.raw()) } @@ -1542,11 +1452,8 @@ fn parse_action( Ok(()) } }; - let required_argument = || { - argument.ok_or_else(|| { - ConfigError::InvalidBinding(format!("action {action:?} requires an argument")) - }) - }; + let required_argument = + || argument.ok_or_else(|| ConfigError::InvalidBinding(format!("action {action:?} requires an argument"))); match action { "none" => { @@ -1555,15 +1462,13 @@ fn parse_action( } "workspace-next" | "workspace-previous" => { no_argument()?; - Ok(BindingAction::SwitchRelativeWorkspace( - action == "workspace-next", - )) + Ok(BindingAction::SwitchRelativeWorkspace(action == "workspace-next")) } "spawn" => { let command = required_argument()?; - let argv = commands.get(command).ok_or_else(|| { - ConfigError::InvalidBinding(format!("unknown command {command:?}")) - })?; + let argv = commands + .get(command) + .ok_or_else(|| ConfigError::InvalidBinding(format!("unknown command {command:?}")))?; Ok(BindingAction::Spawn(argv.clone())) } "close" => { @@ -1576,15 +1481,9 @@ 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) @@ -1621,9 +1520,7 @@ fn parse_action( no_argument()?; Ok(BindingAction::ToggleOverview) } - _ => Err(ConfigError::InvalidBinding(format!( - "unknown action {action:?}" - ))), + _ => Err(ConfigError::InvalidBinding(format!("unknown action {action:?}"))), } } @@ -1633,18 +1530,14 @@ fn parse_direction(argument: &str) -> Result { "right" => Ok(Direction::Right), "up" => Ok(Direction::Up), "down" => Ok(Direction::Down), - _ => Err(ConfigError::InvalidBinding(format!( - "invalid direction {argument:?}" - ))), + _ => Err(ConfigError::InvalidBinding(format!("invalid direction {argument:?}"))), } } fn parse_workspace(argument: &str) -> Result { match argument.parse() { Ok(workspace) if workspace > 0 => Ok(workspace), - _ => Err(ConfigError::InvalidBinding(format!( - "invalid workspace {argument:?}" - ))), + _ => Err(ConfigError::InvalidBinding(format!("invalid workspace {argument:?}"))), } } @@ -1672,25 +1565,13 @@ 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", @@ -1701,11 +1582,7 @@ fn default_bindings() -> Vec { bindings } -fn binding( - keys: impl Into, - action: impl Into, - argument: Option<&str>, -) -> BindingConfig { +fn binding(keys: impl Into, action: impl Into, argument: Option<&str>) -> BindingConfig { BindingConfig { keys: keys.into(), match_mode: BindingMatch::Keysym, @@ -1909,9 +1786,7 @@ fn parse_output_mode(value: &str) -> Result { let (size, refresh) = value .trim() .split_once('@') - .map_or((value.trim(), None), |(size, refresh)| { - (size, Some(refresh)) - }); + .map_or((value.trim(), None), |(size, refresh)| (size, Some(refresh))); let (width, height) = size .split_once('x') .ok_or_else(|| format!("mode {value:?} must use WIDTHxHEIGHT or WIDTHxHEIGHT@REFRESH"))?; @@ -1944,17 +1819,10 @@ fn parse_output_mode(value: &str) -> Result { }) } -fn parse_column_width( - value: &ColumnWidthValue, - field: &'static str, -) -> Result { +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::InvalidColumnWidth { field, value: match value { @@ -1982,15 +1850,9 @@ 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}\ndesktop-widgets {\n clock {\n enabled #true\n outputs \"DP-1\"\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.input_settings().unwrap().touchpad.swipe_threshold, 96); assert_eq!(config.desktop_widgets.clock.outputs, ["DP-1"]); - assert_eq!( - config.output_profiles().unwrap()[0].outputs[0].position, - Some([0, 0]) - ); + assert_eq!(config.output_profiles().unwrap()[0].outputs[0].position, Some([0, 0])); } #[test] @@ -2026,10 +1888,7 @@ 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()); } @@ -2039,10 +1898,7 @@ 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] @@ -2066,19 +1922,10 @@ 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] @@ -2120,8 +1967,7 @@ 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!( @@ -2159,11 +2005,9 @@ 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(), @@ -2197,24 +2041,15 @@ 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() ); } @@ -2249,14 +2084,17 @@ 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!(ferese_config::from_str::("theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n").is_err()); + assert!( + ferese_config::from_str::( + "theme {\n border {\n gradient {\n from \"#112233\"\n }\n }\n}\n" + ) + .is_err() + ); } #[test] @@ -2336,11 +2174,9 @@ mod tests { ("duration_ms", "#inf"), ] { assert!( - Config::parse_source(&format!( - "appearance {{ inactive-dim {{ {key} {value}; }} }}" - )) - .and_then(|config| config.theme_settings()) - .is_err() + Config::parse_source(&format!("appearance {{ inactive-dim {{ {key} {value}; }} }}")) + .and_then(|config| config.theme_settings()) + .is_err() ); } } @@ -2409,11 +2245,9 @@ 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() ); } } @@ -2435,9 +2269,8 @@ 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 @@ -2446,13 +2279,12 @@ 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["description"] == "Close window") + .any(|entry| entry["keys"] == "Super + Tab" && entry["description"] == "Toggle floating window") ); - assert!(entries.iter().any(|entry| entry["keys"] == "Super + Tab" - && entry["description"] == "Toggle floating window")); assert!(entries.iter().any(|entry| entry["keys"] == "Super + Enter")); } @@ -2472,11 +2304,7 @@ 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.trigger == BindingTrigger::Keysym(keysyms::KEY_Return) @@ -2491,11 +2319,7 @@ mod tests { 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(), - ]) + && binding.action == BindingAction::Spawn(vec!["ferese-screenshot".to_owned(), "--full".to_owned()]) })); assert!(bindings.iter().any(|binding| { binding.modifiers.logo @@ -2515,29 +2339,18 @@ mod tests { let bindings = replaced.bindings(&input).unwrap(); assert_eq!(bindings.len(), 48); assert!(bindings.iter().any(|binding| { - binding.action - == BindingAction::Spawn(vec![ - "foot".to_owned(), - "--app-id".to_owned(), - "work".to_owned(), - ]) + binding.action == BindingAction::Spawn(vec!["foot".to_owned(), "--app-id".to_owned(), "work".to_owned()]) })); let input = unbound.input_settings().unwrap(); let bindings = unbound.bindings(&input).unwrap(); assert_eq!(bindings.len(), 47); - 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(); @@ -2549,13 +2362,9 @@ 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(); @@ -2569,12 +2378,7 @@ mod tests { "window-rule app-id=\"org.example.Editor\" workspace=3 floating=#true width=900.0 height=600.0 fullscreen=#false\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)); @@ -2640,13 +2444,10 @@ mod tests { #[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/cursor.rs b/compositor/src/cursor.rs index 5007f0d..d85278d 100644 --- a/compositor/src/cursor.rs +++ b/compositor/src/cursor.rs @@ -1,11 +1,12 @@ -use std::{fs, sync::OnceLock}; +use std::fs; +use std::sync::OnceLock; -use smithay::{ - backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer}, - input::pointer::CursorIcon, - utils::{Buffer, Point, Size, Transform}, -}; -use xcursor::{CursorTheme, parser::Image}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::memory::MemoryRenderBuffer; +use smithay::input::pointer::CursorIcon; +use smithay::utils::{Buffer, Point, Size, Transform}; +use xcursor::CursorTheme; +use xcursor::parser::Image; const DEFAULT_CURSOR_SIZE: u32 = 24; @@ -58,26 +59,17 @@ fn load_image(theme: &CursorTheme, name: &str, requested_size: u32) -> Option NamedCursor { let size = Size::::from((image.width as i32, image.height as i32)); - let buffer = MemoryRenderBuffer::from_slice( - &image.pixels_rgba, - Fourcc::Abgr8888, - size, - 1, - Transform::Normal, - None, - ); + let buffer = MemoryRenderBuffer::from_slice(&image.pixels_rgba, Fourcc::Abgr8888, size, 1, Transform::Normal, None); NamedCursor { buffer, @@ -122,18 +114,9 @@ mod tests { #[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"]); } #[test] diff --git a/compositor/src/daemon.rs b/compositor/src/daemon.rs index 6fd2f27..d65971d 100644 --- a/compositor/src/daemon.rs +++ b/compositor/src/daemon.rs @@ -1,9 +1,10 @@ //! Session-owned services. Never grant shell/effects privileges to daemons. -use crate::{Ferese, config::DaemonConfig, private_client::ClientCapabilities}; -use std::{ - process::Child, - time::{Duration, Instant}, -}; +use std::process::Child; +use std::time::{Duration, Instant}; + +use crate::Ferese; +use crate::config::DaemonConfig; +use crate::private_client::ClientCapabilities; struct Service { config: DaemonConfig, @@ -17,15 +18,8 @@ pub(crate) struct Runner(Vec); impl Runner { pub fn reconcile(&mut self, configs: &[DaemonConfig], nested: bool) { let mut previous = std::mem::take(&mut self.0); - for config in configs - .iter() - .filter(|c| c.enabled && (!nested || c.nested)) - { - if self - .0 - .iter() - .any(|service| service.config.command == config.command) - { + for config in configs.iter().filter(|c| c.enabled && (!nested || c.nested)) { + if self.0.iter().any(|service| service.config.command == config.command) { continue; } if let Some(index) = previous @@ -132,12 +126,7 @@ mod tests { nested: false, }; let mut runner = Runner::new(&[config.clone()], false); - 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()], false); @@ -150,10 +139,7 @@ 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/dimming.rs b/compositor/src/dimming.rs index 54bd3b8..2e8f906 100644 --- a/compositor/src/dimming.rs +++ b/compositor/src/dimming.rs @@ -1,7 +1,9 @@ -use crate::config::InactiveDimSettings; -use ferese_layout::WindowId; use std::time::Duration; +use ferese_layout::WindowId; + +use crate::config::InactiveDimSettings; + pub(crate) fn target( settings: InactiveDimSettings, focused: Option, @@ -60,11 +62,7 @@ impl DimAnimation { 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/effects.rs b/compositor/src/effects.rs index a8b3310..1e88990 100644 --- a/compositor/src/effects.rs +++ b/compositor/src/effects.rs @@ -1,27 +1,23 @@ -use std::sync::{ - Mutex, - atomic::{AtomicBool, AtomicU64, Ordering}, -}; +use std::sync::Mutex; +use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use ferese_protocols::effects::v1::server::{ - ferese_effects_manager_v1::{self, FereseEffectsManagerV1}, - ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1}, -}; -use ferese_protocols::material::v1::server::{ - ferese_material_manager_v1::{self, FereseMaterialManagerV1}, - ferese_surface_material_v1::{self, FereseSurfaceMaterialV1}, -}; -use smithay::{ - reexports::wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak, - backend::ClientId, protocol::wl_surface::WlSurface, - }, - wayland::compositor::with_states, +use ferese_protocols::effects::v1::server::ferese_effects_manager_v1::FereseEffectsManagerV1; +use ferese_protocols::effects::v1::server::ferese_surface_effects_v1::FereseSurfaceEffectsV1; +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 smithay::reexports::wayland_server::backend::ClientId; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::{ + Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, WEnum, Weak, }; +use smithay::wayland::compositor::with_states; -use crate::{ - Ferese, config::MaterialStyle, private_client::ClientCapabilities, state::ClientState, -}; +use crate::Ferese; +use crate::config::MaterialStyle; +use crate::private_client::ClientCapabilities; +use crate::state::ClientState; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub(crate) enum SemanticRole { @@ -41,16 +37,10 @@ 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], }; @@ -58,11 +48,7 @@ pub(crate) fn resolve_material( MaterialStyle::Solid => 1.0, MaterialStyle::Translucent => opacity.clamp(0.0, 1.0), }; - ResolvedMaterial { - style, - opacity, - shadow, - } + ResolvedMaterial { style, opacity, shadow } } #[derive(Debug)] @@ -145,9 +131,7 @@ 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) }); @@ -220,13 +204,9 @@ fn decode_regions(bytes: &[u8]) -> Option> { .0 .iter() .map(|tuple| { - let values: [i32; 5] = - std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index])); - (values[2] > 0 - && values[3] > 0 - && values[4] >= 0 - && values.iter().all(|v| v.abs_diff(0) <= 32768)) - .then_some(values) + let values: [i32; 5] = std::array::from_fn(|index| i32::from_ne_bytes(tuple.as_chunks::<4>().0[index])); + (values[2] > 0 && values[3] > 0 && values[4] >= 0 && values.iter().all(|v| v.abs_diff(0) <= 32768)) + .then_some(values) }) .collect() } @@ -273,9 +253,7 @@ 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::() @@ -346,14 +324,10 @@ impl Dispatch for Ferese { ferese_surface_effects_v1::Request::SetRole { role } => { let role = match role { WEnum::Value(ferese_surface_effects_v1::Role::Panel) => SemanticRole::Panel, - WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => { - SemanticRole::PanelElevated - } + WEnum::Value(ferese_surface_effects_v1::Role::PanelElevated) => SemanticRole::PanelElevated, WEnum::Value(ferese_surface_effects_v1::Role::Popover) => SemanticRole::Popover, WEnum::Value(ferese_surface_effects_v1::Role::Menu) => SemanticRole::Menu, - WEnum::Value(ferese_surface_effects_v1::Role::Notification) => { - SemanticRole::Notification - } + WEnum::Value(ferese_surface_effects_v1::Role::Notification) => SemanticRole::Notification, WEnum::Value(ferese_surface_effects_v1::Role::Hud) => SemanticRole::Hud, WEnum::Value(ferese_surface_effects_v1::Role::Modal) => SemanticRole::Modal, WEnum::Unknown(_) | WEnum::Value(_) => return, @@ -437,22 +411,17 @@ 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) + }) }) } @@ -477,20 +446,8 @@ mod tests { assert_eq!(decode_regions(&[]), Some(vec![])); assert!(decode_regions(&bytes[..19]).is_none()); assert!(decode_regions(&bytes.repeat(33)).is_none()); - for invalid in [ - [0_i32, 0, 0, 48, 11], - [0, 0, 120, 48, -1], - [i32::MIN, 0, 120, 48, 11], - ] { - assert!( - decode_regions( - &invalid - .into_iter() - .flat_map(i32::to_ne_bytes) - .collect::>() - ) - .is_none() - ); + for invalid in [[0_i32, 0, 0, 48, 11], [0, 0, 120, 48, -1], [i32::MIN, 0, 120, 48, 11]] { + assert!(decode_regions(&invalid.into_iter().flat_map(i32::to_ne_bytes).collect::>()).is_none()); } } diff --git a/compositor/src/grabs/move_grab.rs b/compositor/src/grabs/move_grab.rs index d0301f8..eb30dbc 100644 --- a/compositor/src/grabs/move_grab.rs +++ b/compositor/src/grabs/move_grab.rs @@ -1,14 +1,11 @@ -use smithay::{ - desktop::Window, - input::pointer::{ - AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, - GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, - GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, - PointerInnerHandle, RelativeMotionEvent, - }, - reexports::wayland_server::protocol::wl_surface::WlSurface, - utils::{Logical, Point, Size}, +use smithay::desktop::Window; +use smithay::input::pointer::{ + 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}; use crate::Ferese; @@ -44,12 +41,7 @@ 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; @@ -57,12 +49,7 @@ 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 7891eb2..80c7dd3 100644 --- a/compositor/src/grabs/resize_grab.rs +++ b/compositor/src/grabs/resize_grab.rs @@ -1,20 +1,16 @@ use std::cell::RefCell; -use smithay::{ - desktop::Window, - input::pointer::{ - AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent, - GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent, - GestureSwipeEndEvent, GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerGrab, - PointerInnerHandle, RelativeMotionEvent, - }, - reexports::{ - wayland_protocols::xdg::shell::server::xdg_toplevel, - wayland_server::protocol::wl_surface::WlSurface, - }, - utils::{Logical, Point, Rectangle, Size}, - wayland::{compositor::with_states, shell::xdg::SurfaceCachedState}, +use smithay::desktop::Window; +use smithay::input::pointer::{ + 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; +use smithay::utils::{Logical, Point, Rectangle, Size}; +use smithay::wayland::compositor::with_states; +use smithay::wayland::shell::xdg::SurfaceCachedState; use crate::Ferese; @@ -31,9 +27,7 @@ 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 ) } @@ -49,9 +43,7 @@ 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 ) } @@ -82,16 +74,8 @@ impl ResizeSurfaceGrab { initial_rect: Rectangle, ) -> Self { ResizeState::with( - window - .toplevel() - .expect("managed window has a toplevel") - .wl_surface(), - |state| { - *state = ResizeState::Resizing { - edges, - initial_rect, - } - }, + window.toplevel().expect("managed window has a toplevel").wl_surface(), + |state| *state = ResizeState::Resizing { edges, initial_rect }, ); Self { start_data, @@ -114,17 +98,13 @@ impl PointerGrab for ResizeSurfaceGrab { ) { handle.motion(data, None, event); let delta = event.location - self.start_data.location; - 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) }); - self.last_size = - constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum); + self.last_size = constrained_size(self.initial_rect.size, delta, self.edges, minimum, maximum); let rect = resized_rect(self.initial_rect, self.last_size, self.edges); data.set_floating_window_geometry(&self.window, rect.loc, rect.size); @@ -145,20 +125,12 @@ 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; 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); @@ -173,12 +145,7 @@ 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); } @@ -267,21 +234,14 @@ 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(); ResizeState::with(surface.wl_surface(), |state| *state = ResizeState::Idle); - 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); } } @@ -316,14 +276,8 @@ impl ResizeState { fn commit(&mut self) -> Option<(ResizeEdge, Rectangle)> { match *self { Self::Idle => None, - Self::Resizing { - edges, - initial_rect, - } => Some((edges, initial_rect)), - Self::WaitingForFinalCommit { - edges, - initial_rect, - } => { + Self::Resizing { edges, initial_rect } => Some((edges, initial_rect)), + Self::WaitingForFinalCommit { edges, initial_rect } => { *self = Self::Idle; Some((edges, initial_rect)) } diff --git a/compositor/src/handlers/activation.rs b/compositor/src/handlers/activation.rs index f136316..e52617b 100644 --- a/compositor/src/handlers/activation.rs +++ b/compositor/src/handlers/activation.rs @@ -1,11 +1,9 @@ use std::time::Duration; -use smithay::{ - reexports::wayland_server::protocol::wl_surface::WlSurface, - utils::SERIAL_COUNTER, - wayland::xdg_activation::{ - XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, - }, +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::SERIAL_COUNTER; +use smithay::wayland::xdg_activation::{ + XdgActivationHandler, XdgActivationState, XdgActivationToken, XdgActivationTokenData, }; use crate::Ferese; @@ -21,12 +19,7 @@ impl XdgActivationHandler for Ferese { data.serial.is_some() && data.timestamp.elapsed() <= TOKEN_MAX_AGE } - fn request_activation( - &mut self, - token: XdgActivationToken, - data: XdgActivationTokenData, - surface: WlSurface, - ) { + fn request_activation(&mut self, token: XdgActivationToken, data: XdgActivationTokenData, surface: WlSurface) { if self.session_lock.active || self.input_capture.captures(1) { return; } diff --git a/compositor/src/handlers/compositor.rs b/compositor/src/handlers/compositor.rs index 87f5fa4..60ef9b5 100644 --- a/compositor/src/handlers/compositor.rs +++ b/compositor/src/handlers/compositor.rs @@ -1,28 +1,23 @@ -use smithay::{ - backend::{allocator::dmabuf::Dmabuf, renderer::utils::on_commit_buffer_handler}, - reexports::wayland_server::{ - Client, - protocol::{wl_buffer, wl_surface::WlSurface}, - }, - wayland::{ - buffer::BufferHandler, - compositor::{ - CompositorClientState, CompositorHandler, CompositorState, get_parent, - is_sync_subsurface, - }, - dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}, - shm::{ShmHandler, ShmState}, - }, +use smithay::backend::allocator::dmabuf::Dmabuf; +use smithay::backend::renderer::utils::on_commit_buffer_handler; +use smithay::reexports::wayland_server::Client; +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, is_sync_subsurface, }; +use smithay::wayland::dmabuf::{DmabufGlobal, DmabufHandler, DmabufState, ImportNotifier}; +use smithay::wayland::shm::{ShmHandler, ShmState}; use super::{layer_shell, xdg_shell}; -use crate::{Ferese, state::ClientState}; +use crate::Ferese; +use crate::state::ClientState; fn layer_affects_backdrop(layer: smithay::wayland::shell::wlr_layer::Layer) -> bool { matches!( layer, - smithay::wayland::shell::wlr_layer::Layer::Background - | smithay::wayland::shell::wlr_layer::Layer::Bottom + smithay::wayland::shell::wlr_layer::Layer::Background | smithay::wayland::shell::wlr_layer::Layer::Bottom ) } @@ -68,11 +63,7 @@ impl CompositorHandler for Ferese { let window = { self.space .elements() - .find(|window| { - window - .toplevel() - .is_some_and(|toplevel| toplevel.wl_surface() == &root) - }) + .find(|window| window.toplevel().is_some_and(|toplevel| toplevel.wl_surface() == &root)) .cloned() }; @@ -124,12 +115,7 @@ impl DmabufHandler for Ferese { &mut self.dmabuf_state } - fn dmabuf_imported( - &mut self, - _global: &DmabufGlobal, - dmabuf: Dmabuf, - notifier: ImportNotifier, - ) { + fn dmabuf_imported(&mut self, _global: &DmabufGlobal, dmabuf: Dmabuf, notifier: ImportNotifier) { self.queue_dmabuf_import(dmabuf, notifier); crate::backends::direct::render_all(self); } @@ -143,8 +129,9 @@ impl ShmHandler for Ferese { #[cfg(test)] mod tests { - use super::*; use smithay::wayland::shell::wlr_layer::Layer; + + use super::*; #[test] fn panel_commits_do_not_invalidate_their_own_backdrop() { assert!(!layer_affects_backdrop(Layer::Top)); diff --git a/compositor/src/handlers/input_method.rs b/compositor/src/handlers/input_method.rs index 904d371..90f22ab 100644 --- a/compositor/src/handlers/input_method.rs +++ b/compositor/src/handlers/input_method.rs @@ -1,18 +1,13 @@ -use smithay::{ - desktop::{PopupKind, WindowSurfaceType, layer_map_for_output}, - reexports::wayland_server::protocol::wl_surface::WlSurface, - utils::{Logical, Rectangle}, - wayland::input_method::{InputMethodHandler, PopupSurface}, -}; +use smithay::desktop::{PopupKind, WindowSurfaceType, layer_map_for_output}; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Rectangle}; +use smithay::wayland::input_method::{InputMethodHandler, PopupSurface}; 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_all(self); @@ -38,10 +33,7 @@ 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 f603e79..5a78909 100644 --- a/compositor/src/handlers/layer_shell.rs +++ b/compositor/src/handlers/layer_shell.rs @@ -1,16 +1,11 @@ -use smithay::{ - desktop::{ - LayerSurface as DesktopLayerSurface, PopupKind, WindowSurfaceType, layer_map_for_output, - }, - output::Output, - reexports::wayland_server::protocol::{wl_output::WlOutput, wl_surface::WlSurface}, - wayland::shell::{ - wlr_layer::{ - KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState, - }, - xdg::PopupSurface, - }, +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; +use smithay::wayland::shell::wlr_layer::{ + KeyboardInteractivity, Layer, LayerSurface, WlrLayerShellHandler, WlrLayerShellState, }; +use smithay::wayland::shell::xdg::PopupSurface; use crate::Ferese; @@ -19,13 +14,7 @@ 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) { let output = output .as_ref() .and_then(Output::from_resource) @@ -42,10 +31,7 @@ impl WlrLayerShellHandler for Ferese { } tracing::debug!(%namespace, output = %output.name(), "mapped layer surface"); - super::set_surface_tree_scale( - layer.wl_surface(), - output.current_scale().fractional_scale(), - ); + super::set_surface_tree_scale(layer.wl_surface(), output.current_scale().fractional_scale()); self.relayout(); crate::backends::direct::render_all(self); } @@ -89,10 +75,7 @@ impl WlrLayerShellHandler for Ferese { } 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().cloned().collect::>(); @@ -112,15 +95,11 @@ pub fn handle_commit(state: &mut Ferese, surface: &WlSurface) { && policy.keyboard_interactivity == KeyboardInteractivity::Exclusive && matches!(policy.layer, Layer::Top | Layer::Overlay) { - 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(), + ); } else if !layer.can_receive_keyboard_focus() && layer_has_keyboard_focus(state, &layer) { state.restore_keyboard_focus(); } @@ -136,10 +115,6 @@ fn layer_has_keyboard_focus(state: &Ferese, layer: &DesktopLayerSurface) -> bool .seat .get_keyboard() .and_then(|keyboard| keyboard.current_focus()) - .and_then(|surface| { - state - .space - .layer_for_surface(&surface, WindowSurfaceType::ALL) - }) + .and_then(|surface| state.space.layer_for_surface(&surface, WindowSurfaceType::ALL)) .is_some_and(|focused| focused == *layer) } diff --git a/compositor/src/handlers/mod.rs b/compositor/src/handlers/mod.rs index b0d8fe2..2e295b4 100644 --- a/compositor/src/handlers/mod.rs +++ b/compositor/src/handlers/mod.rs @@ -8,42 +8,31 @@ pub(crate) mod screenshot_worker; pub(crate) mod window_capture; mod xdg_shell; -use smithay::{ - desktop::{PopupManager, layer_map_for_output}, - input::{ - Seat, SeatHandler, SeatState, - pointer::{CursorImageStatus, PointerHandle}, - }, - reexports::wayland_server::{Resource, protocol::wl_surface::WlSurface}, - utils::Point, - wayland::{ - compositor::{TraversalAction, with_states, with_surface_tree_downward}, - fractional_scale::{FractionalScaleHandler, with_fractional_scale}, - idle_inhibit::IdleInhibitHandler, - idle_notify::{IdleNotifierHandler, IdleNotifierState}, - keyboard_shortcuts_inhibit::{ - KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, - KeyboardShortcutsInhibitor, KeyboardShortcutsInhibitorSeat, - }, - output::OutputHandler, - pointer_constraints::{ - PointerConstraint, PointerConstraintsHandler, with_pointer_constraint, - }, - selection::{ - SelectionHandler, - data_device::{ - ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, - set_data_device_focus, - }, - primary_selection::{ - PrimarySelectionHandler, PrimarySelectionState, set_primary_focus, - }, - }, - tablet_manager::TabletSeatHandler, - xdg_foreign::{XdgForeignHandler, XdgForeignState}, - xdg_toplevel_icon::XdgToplevelIconHandler, - }, +use smithay::desktop::{PopupManager, layer_map_for_output}; +use smithay::input::pointer::{CursorImageStatus, PointerHandle}; +use smithay::input::{Seat, SeatHandler, SeatState}; +use smithay::reexports::wayland_server::Resource; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::wayland::compositor::{TraversalAction, with_states, with_surface_tree_downward}; +use smithay::wayland::fractional_scale::{FractionalScaleHandler, with_fractional_scale}; +use smithay::wayland::idle_inhibit::IdleInhibitHandler; +use smithay::wayland::idle_notify::{IdleNotifierHandler, IdleNotifierState}; +use smithay::wayland::keyboard_shortcuts_inhibit::{ + KeyboardShortcutsInhibitHandler, KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor, + KeyboardShortcutsInhibitorSeat, }; +use smithay::wayland::output::OutputHandler; +use smithay::wayland::pointer_constraints::{PointerConstraint, PointerConstraintsHandler, with_pointer_constraint}; +use smithay::wayland::selection::SelectionHandler; +use smithay::wayland::selection::data_device::{ + ClientDndGrabHandler, DataDeviceHandler, DataDeviceState, ServerDndGrabHandler, set_data_device_focus, +}; +use smithay::wayland::selection::primary_selection::{ + PrimarySelectionHandler, PrimarySelectionState, set_primary_focus, +}; +use smithay::wayland::tablet_manager::TabletSeatHandler; +use smithay::wayland::xdg_foreign::{XdgForeignHandler, XdgForeignState}; +use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconHandler; use crate::Ferese; @@ -75,9 +64,7 @@ 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); } @@ -137,10 +124,7 @@ 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(); @@ -199,7 +183,14 @@ pub(crate) fn set_surface_tree_scale(surface: &WlSurface, scale: f64) { } impl PointerConstraintsHandler for Ferese { - fn new_constraint(&mut self, _surface: &WlSurface, pointer: &PointerHandle) { + fn new_constraint(&mut self, surface: &WlSurface, pointer: &PointerHandle) { + if self + .locked_pointer_hint + .as_ref() + .is_some_and(|hint| hint.surface == *surface) + { + self.locked_pointer_hint = None; + } self.activate_focused_pointer_constraint(pointer); } @@ -218,20 +209,10 @@ impl PointerConstraintsHandler for Ferese { return; } - let Some((focused_surface, origin)) = self.surface_under(pointer.current_location()) else { - return; - }; - if focused_surface == *surface { - let current = pointer.current_location(); - let (scale_x, scale_y) = self - .window_under_visual(current) - .and_then(|window| self.visual_scale_for_window(&window)) - .unwrap_or((1.0, 1.0)); - let current_surface_location = current - origin; - let offset = location - current_surface_location; - - pointer.set_location(current + Point::from((offset.x * scale_x, offset.y * scale_y))); - } + self.locked_pointer_hint = Some(crate::input::LockedPointerHint { + surface: surface.clone(), + local: location, + }); } } diff --git a/compositor/src/handlers/screencopy.rs b/compositor/src/handlers/screencopy.rs index 4bd1f96..1c45bbd 100644 --- a/compositor/src/handlers/screencopy.rs +++ b/compositor/src/handlers/screencopy.rs @@ -1,27 +1,21 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; -use super::screenshot::{CaptureBuffer, PartOutcome, PartSender}; -use smithay::{ - backend::{ - allocator::Fourcc, - renderer::{ExportMem, TextureMapping}, - }, - output::Output, - reexports::{ - wayland_protocols_wlr::screencopy::v1::server::{ - zwlr_screencopy_frame_v1::{self, ZwlrScreencopyFrameV1}, - zwlr_screencopy_manager_v1::{self, ZwlrScreencopyManagerV1}, - }, - wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, - protocol::{wl_buffer::WlBuffer, wl_shm}, - }, - }, - utils::{Buffer, Logical, Rectangle, Size}, - wayland::shm::with_buffer_contents_mut, +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::{ExportMem, TextureMapping}; +use smithay::output::Output; +use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::ZwlrScreencopyFrameV1; +use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_manager_v1::ZwlrScreencopyManagerV1; +use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::{ + zwlr_screencopy_frame_v1, zwlr_screencopy_manager_v1, }; +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::utils::{Buffer, Logical, Rectangle, Size}; +use smithay::wayland::shm::with_buffer_contents_mut; +use super::screenshot::{CaptureBuffer, PartOutcome, PartSender}; use crate::Ferese; const BYTES_PER_PIXEL: usize = 4; @@ -150,8 +144,7 @@ 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 { @@ -312,9 +305,7 @@ 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; } @@ -325,8 +316,7 @@ 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(); } } @@ -336,10 +326,9 @@ 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)) } } @@ -404,10 +393,7 @@ 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(); @@ -434,9 +420,7 @@ pub(crate) fn capture_region_for_geometry( ) -> Option> { 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; @@ -488,9 +472,7 @@ 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(()); @@ -548,20 +530,17 @@ 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(()) } #[cfg(test)] mod tests { - use super::*; use smithay::utils::{Physical, Transform}; + use super::*; + #[test] fn capture_is_off_unless_the_opt_in_is_exactly_one() { use std::ffi::OsStr; @@ -580,8 +559,7 @@ 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())); } @@ -591,9 +569,7 @@ 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 9874677..94ab920 100644 --- a/compositor/src/handlers/screenshot.rs +++ b/compositor/src/handlers/screenshot.rs @@ -36,9 +36,7 @@ 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; } @@ -81,11 +79,7 @@ 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; @@ -97,13 +91,8 @@ fn scale_and_convert( 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)?; @@ -154,13 +143,7 @@ fn scale_and_convert( 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), @@ -186,10 +169,7 @@ 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()); } @@ -216,22 +196,15 @@ 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) @@ -292,12 +265,7 @@ 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 { @@ -307,16 +275,9 @@ 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 }) } } @@ -394,10 +355,7 @@ struct Request { pub(crate) enum Action { None, - Encode { - request: u64, - frames: Vec, - }, + Encode { request: u64, frames: Vec }, // The path was already sent to the caller; it must not be cleaned up. Delivered(PathBuf), // The request is gone, so this file is ours to remove. @@ -461,10 +419,7 @@ 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: Instant::now() + REQUEST_TIMEOUT, }, @@ -485,12 +440,7 @@ 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; }; @@ -595,10 +545,7 @@ 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 } @@ -682,10 +629,7 @@ 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()); } @@ -696,8 +640,7 @@ pub(crate) fn plan( { return Err("Screenshot region must have a positive size".into()); } - let logical_sizes: Vec> = - outputs.iter().map(OutputLayout::logical_size).collect(); + let logical_sizes: Vec> = outputs.iter().map(OutputLayout::logical_size).collect(); let requested = match geometry { Geometry::All => { @@ -709,10 +652,8 @@ pub(crate) fn plan( }; union = Some(match union { Some(current) => { - 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), @@ -724,12 +665,7 @@ pub(crate) fn plan( } union.expect("outputs is not empty") } - Geometry::Region { - x, - y, - width, - height, - } => Rectangle { + Geometry::Region { x, y, width, height } => Rectangle { loc: (*x, *y).into(), size: (*width, *height).into(), }, @@ -757,11 +693,7 @@ pub(crate) fn plan( } // capture_region_for_geometry works in output-local logical space. let local = Rectangle { - loc: ( - clipped.loc.x - output.location.x, - clipped.loc.y - output.location.y, - ) - .into(), + loc: (clipped.loc.x - output.location.x, clipped.loc.y - output.location.y).into(), size: clipped.size, }; // The clip above already intersected against this same logical rect, so @@ -808,34 +740,19 @@ mod planning { } fn region(x: i32, y: i32, width: i32, height: i32) -> 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[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[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. @@ -868,14 +785,8 @@ 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); @@ -885,10 +796,7 @@ 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. @@ -899,10 +807,7 @@ 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"); } @@ -912,17 +817,11 @@ 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] @@ -932,16 +831,10 @@ 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] @@ -981,10 +874,12 @@ mod planning { #[cfg(test)] mod completion { + use std::sync::mpsc::sync_channel; + + use smithay::reexports::calloop::channel::channel as loop_channel; + use super::*; use crate::handlers::screencopy::CaptureSink; - use smithay::reexports::calloop::channel::channel as loop_channel; - use std::sync::mpsc::sync_channel; fn spec(width: i32, height: i32) -> PartSpec { PartSpec { @@ -1021,10 +916,7 @@ mod completion { coordinator .admit(42, reply.clone(), (0..parts).map(|_| spec(2, 2)).collect()) .expect("admitted"); - Self { - coordinator, - received, - } + Self { coordinator, received } } fn response(&self) -> Option { @@ -1041,21 +933,13 @@ 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,9 +954,7 @@ 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()); @@ -1142,9 +1024,7 @@ 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"); } @@ -1155,9 +1035,7 @@ 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()); @@ -1165,9 +1043,7 @@ 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 @@ -1190,9 +1066,7 @@ 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)); @@ -1205,23 +1079,13 @@ 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(), 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 ); @@ -1302,11 +1166,7 @@ mod completion { // buffer that was handed off is never replaced. assert!(!sink.publish(buffer(64, 64))); - let PartOutcome { - request, - part, - result, - } = receiver.try_recv().expect("published once"); + let PartOutcome { request, part, result } = receiver.try_recv().expect("published once"); assert_eq!((request, part), (7, 1)); assert_eq!(result.unwrap().width, 2); assert!(receiver.try_recv().is_err(), "no second buffer was sent"); @@ -1321,11 +1181,7 @@ mod completion { // failure has already been reported. assert!(!sink.publish(buffer(2, 2))); - let PartOutcome { - request, - part, - result, - } = receiver.try_recv().expect("answered"); + let PartOutcome { request, part, result } = receiver.try_recv().expect("answered"); assert_eq!((request, part), (3, 0)); assert!(result.is_err()); assert!(receiver.try_recv().is_err()); @@ -1715,11 +1571,7 @@ 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"); } } @@ -1751,11 +1603,7 @@ 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); } @@ -1784,11 +1632,7 @@ 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 dcdecc8..7a76072 100644 --- a/compositor/src/handlers/screenshot_worker.rs +++ b/compositor/src/handlers/screenshot_worker.rs @@ -1,10 +1,8 @@ -use std::fs; -use std::io; use std::os::unix::fs::{OpenOptionsExt, PermissionsExt}; use std::path::PathBuf; use std::sync::mpsc::{Receiver, SyncSender, TrySendError, sync_channel}; -use std::thread; use std::time::{Duration, SystemTime}; +use std::{fs, io, thread}; use smithay::reexports::calloop::channel; @@ -125,14 +123,8 @@ 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) } @@ -161,8 +153,8 @@ fn write_private_file(png: &[u8]) -> Result { } 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) { @@ -177,9 +169,7 @@ 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}")), @@ -189,9 +179,10 @@ fn private_directory() -> Result { #[cfg(test)] mod tests { - use super::*; use smithay::utils::Transform; + use super::*; + fn frame(width: i32, height: i32) -> OutputFrame { let stride = width as usize * 4; let mut pixels = vec![0u8; stride * height as usize]; @@ -237,10 +228,7 @@ 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(); } @@ -254,10 +242,7 @@ 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/window_capture.rs b/compositor/src/handlers/window_capture.rs index 2fd9b1f..ff04b05 100644 --- a/compositor/src/handlers/window_capture.rs +++ b/compositor/src/handlers/window_capture.rs @@ -1,27 +1,19 @@ -use std::{ - os::unix::fs::MetadataExt, - sync::{Arc, Mutex}, -}; +use std::os::unix::fs::MetadataExt; +use std::sync::{Arc, Mutex}; use ferese_layout::WindowId; -use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::{ - self as manager, FereseWindowCaptureManagerV1, -}; -use smithay::{ - reexports::{ - wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1::{ - self as frame, ZwlrScreencopyFrameV1, - }, - wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, - protocol::{wl_buffer::WlBuffer, wl_shm}, - }, - }, - utils::Rectangle, - wayland::shm::with_buffer_contents_mut, -}; +use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1 as manager; +use ferese_protocols::window_capture::v1::server::ferese_window_capture_manager_v1::FereseWindowCaptureManagerV1; +use smithay::reexports::wayland_protocols_wlr::screencopy::v1::server::zwlr_screencopy_frame_v1 as frame; +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::utils::Rectangle; +use smithay::wayland::shm::with_buffer_contents_mut; -use super::{screencopy::FrameData, screenshot::CaptureBuffer}; +use super::screencopy::FrameData; +use super::screenshot::CaptureBuffer; use crate::Ferese; const MAX_BYTES: usize = 128 * 1024 * 1024; @@ -42,10 +34,7 @@ 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; @@ -79,11 +68,7 @@ pub(crate) struct Global { 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. @@ -101,33 +86,23 @@ 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()) } pub(super) fn init_global(display: &DisplayHandle) { - display.create_global::( - 1, - Global { - budget: Arc::default(), - }, - ); + display.create_global::(1, Global { budget: Arc::default() }); } 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( @@ -226,8 +201,7 @@ 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()?)? @@ -247,8 +221,7 @@ impl Ferese { } 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)?; + let (surface, _) = window.surface_under(surface_point, smithay::desktop::WindowSurfaceType::ALL)?; if pointer.current_focus().as_ref() != Some(&surface) { return None; } @@ -298,18 +271,10 @@ 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; @@ -342,11 +307,7 @@ pub(super) fn copy_snapshot( // 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 }) diff --git a/compositor/src/handlers/xdg_shell.rs b/compositor/src/handlers/xdg_shell.rs index d52f371..ef83e14 100644 --- a/compositor/src/handlers/xdg_shell.rs +++ b/compositor/src/handlers/xdg_shell.rs @@ -1,35 +1,23 @@ -use smithay::{ - desktop::{ - PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window, - WindowSurfaceType, find_popup_root_surface, get_popup_toplevel_coords, - }, - input::{ - Seat, - pointer::{Focus, GrabStartData}, - }, - reexports::wayland_protocols::xdg::{ - decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode, - shell::server::xdg_toplevel::State as ToplevelState, - }, - reexports::wayland_server::{ - Resource, - protocol::{wl_seat, wl_surface::WlSurface}, - }, - utils::Serial, - wayland::{ - compositor::with_states, - shell::xdg::decoration::XdgDecorationHandler, - shell::xdg::{ - PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, - XdgToplevelSurfaceData, - }, - }, +use smithay::desktop::{ + PopupKeyboardGrab, PopupKind, PopupManager, PopupPointerGrab, Space, Window, WindowSurfaceType, + find_popup_root_surface, get_popup_toplevel_coords, +}; +use smithay::input::Seat; +use smithay::input::pointer::{Focus, GrabStartData}; +use smithay::reexports::wayland_protocols::xdg::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::State as ToplevelState; +use smithay::reexports::wayland_server::Resource; +use smithay::reexports::wayland_server::protocol::wl_seat; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Point, Rectangle, Serial}; +use smithay::wayland::compositor::with_states; +use smithay::wayland::shell::xdg::decoration::XdgDecorationHandler; +use smithay::wayland::shell::xdg::{ + PopupSurface, PositionerState, ToplevelSurface, XdgShellHandler, XdgShellState, XdgToplevelSurfaceData, }; -use crate::{ - Ferese, - grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit}, -}; +use crate::Ferese; +use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab, handle_resize_commit}; impl XdgShellHandler for Ferese { fn xdg_shell_state(&mut self) -> &mut XdgShellState { @@ -93,12 +81,7 @@ 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; @@ -273,12 +256,7 @@ 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); @@ -290,21 +268,14 @@ 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 { @@ -381,9 +352,7 @@ pub fn handle_commit(popups: &mut PopupManager, space: &mut Space, surfa if let Some(PopupKind::Xdg(popup)) = 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"); } } @@ -444,40 +413,82 @@ 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 output = self.space.outputs().next()?; - let output_geometry = self.space.output_geometry(output)?; - let window_geometry = self.space.element_geometry(window)?; - - Some((output_geometry, window_geometry.loc)) - } else { - let layer = self + let id = *self.window_ids.get(window)?; + let output = self .space - .layer_for_surface(&root, WindowSurfaceType::TOPLEVEL)?; + .outputs() + .find(|output| self.window_belongs_to_output(id, output))?; + let output_geometry = self.space.output_geometry(output)?; + let presented = self.presented_window_rect(id)?; + let location = (presented.x.round() as i32, presented.y.round() as i32).into(); + + Some((output_geometry, location)) + } else { + 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)) }) } })(); - let Some((mut target, root_location)) = geometry else { + let Some((target, root_location)) = geometry else { return; }; - target.loc -= get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())); - target.loc -= root_location; + let target = popup_constraint_target( + target, + root_location, + get_popup_toplevel_coords(&PopupKind::Xdg(popup.clone())), + ); popup.with_pending_state(|state| { state.geometry = state.positioner.get_unconstrained_geometry(target); }); } } + +fn popup_constraint_target( + mut output: Rectangle, + root_location: Point, + parent_offset: Point, +) -> Rectangle { + output.loc -= root_location + parent_offset; + output +} + +#[cfg(test)] +mod tests { + use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_positioner; + + use super::*; + + #[test] + fn popup_uses_presented_parent_coordinates_on_an_offset_monitor() { + let output = Rectangle::new((1920, -200).into(), (1920, 1080).into()); + let root = Point::from((2200, -100)); + let parent = Point::from((40, 30)); + let target = popup_constraint_target(output, root, parent); + assert_eq!(target.loc, Point::from((-320, -130))); + let positioner = PositionerState { + rect_size: (320, 180).into(), + anchor_rect: Rectangle::new((1850, 900).into(), (1, 1).into()), + anchor_edges: xdg_positioner::Anchor::BottomRight, + gravity: xdg_positioner::Gravity::BottomRight, + constraint_adjustment: xdg_positioner::ConstraintAdjustment::SlideX + | xdg_positioner::ConstraintAdjustment::SlideY, + ..Default::default() + }; + let popup = positioner.get_unconstrained_geometry(target); + assert_eq!(popup.loc, Point::from((1280, 770))); + let global = Rectangle::new(root + parent + popup.loc, popup.size); + assert_eq!(global.intersection(output), Some(global)); + } +} diff --git a/compositor/src/input.rs b/compositor/src/input.rs index ac5eeac..5c1a008 100644 --- a/compositor/src/input.rs +++ b/compositor/src/input.rs @@ -1,53 +1,122 @@ -use smithay::reexports::wayland_server::Resource; use std::process::Command; 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, - }, - input::{ - keyboard::{FilterResult, keysyms}, - pointer::{ - AxisFrame, ButtonEvent, Focus, GestureSwipeBeginEvent, GestureSwipeEndEvent, - GestureSwipeUpdateEvent, GrabStartData, MotionEvent, PointerHandle, - RelativeMotionEvent, - }, - touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}, - }, - reexports::{ - wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge, - wayland_server::protocol::wl_surface::WlSurface, - }, - utils::{Logical, Point, SERIAL_COUNTER, Serial}, - wayland::{ - keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat, - pointer_constraints::{PointerConstraint, with_pointer_constraint}, - }, +use smithay::backend::input::{ + 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, +}; +use smithay::input::touch::{DownEvent, MotionEvent as TouchMotionEvent, UpEvent}; +use smithay::reexports::wayland_protocols::xdg::shell::server::xdg_toplevel::ResizeEdge as XdgResizeEdge; +use smithay::reexports::wayland_server::Resource; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Logical, Point, SERIAL_COUNTER, Serial}; +use smithay::wayland::keyboard_shortcuts_inhibit::KeyboardShortcutsInhibitorSeat; +use smithay::wayland::pointer_constraints::{PointerConstraint, with_pointer_constraint}; -use crate::{ - Ferese, - config::BindingAction, - gestures::SwipeDirection, - grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab}, -}; +use crate::Ferese; +use crate::config::BindingAction; +use crate::gestures::SwipeDirection; +use crate::grabs::{MoveSurfaceGrab, ResizeEdge, ResizeSurfaceGrab}; const BTN_LEFT: u32 = 0x110; const BTN_RIGHT: u32 = 0x111; +pub(crate) struct LockedPointerHint { + pub(crate) surface: WlSurface, + pub(crate) local: Point, +} + impl Ferese { + pub(crate) fn pointer_hint_position( + &self, + surface: &WlSurface, + hint: Point, + ) -> Option> { + for window in self.space.elements() { + let root = window.toplevel()?.wl_surface(); + let geometry = window.geometry(); + let Some(local) = surface_tree_position(root, surface, geometry.loc) else { + continue; + }; + let id = self.window_ids.get(window)?; + let presented = self.presented_window_rect(*id)?; + let (scale_x, scale_y) = self.visual_scale_for_window(window)?; + let point = local.to_f64() + hint - geometry.loc.to_f64(); + return Some(Point::from(( + presented.x + point.x * scale_x, + presented.y + point.y * scale_y, + ))); + } + for output in self.space.outputs() { + let output_geometry = self.space.output_geometry(output)?; + 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 { + continue; + }; + let geometry = layers.layer_geometry(layer)?; + return Some((output_geometry.loc + geometry.loc + local).to_f64() + hint); + } + } + None + } + + pub(crate) fn apply_unlocked_pointer_hint(&mut self) { + let Some(hint) = self.locked_pointer_hint.take() else { + return; + }; + if !hint.surface.is_alive() || self.session_lock.active || self.input_capture.active() { + return; + } + let Some(pointer) = self.seat.get_pointer() else { + return; + }; + let locked = with_pointer_constraint(&hint.surface, &pointer, |constraint| { + constraint.is_some_and(|constraint| { + constraint.is_active() && matches!(&*constraint, PointerConstraint::Locked(_)) + }) + }); + if locked { + self.locked_pointer_hint = Some(hint); + return; + } + if pointer.current_focus().is_some_and(|surface| { + with_pointer_constraint(&surface, &pointer, |constraint| { + constraint.is_some_and(|constraint| constraint.is_active()) + }) + }) { + return; + } + let Some(global) = self.pointer_hint_position(&hint.surface, hint.local) else { + return; + }; + let location = self.clamp_pointer_position(global); + let focus = self.surface_under(location); + pointer.motion( + self, + focus, + &MotionEvent { + location, + serial: SERIAL_COUNTER.next_serial(), + time: self.last_pointer_time, + }, + ); + pointer.frame(self); + self.cursor_redraw_pending = true; + } + pub fn process_input_event(&mut self, event: InputEvent) { let seat = self.seat.clone(); self.idle_notifier_state.notify_activity(&seat); if !matches!( &event, - InputEvent::DeviceAdded { .. } - | InputEvent::DeviceRemoved { .. } - | InputEvent::SwitchToggle { .. } + InputEvent::DeviceAdded { .. } | InputEvent::DeviceRemoved { .. } | InputEvent::SwitchToggle { .. } ) { self.lock_input_activity(); } @@ -117,15 +186,13 @@ impl Ferese { if self.swipe.active() { self.swipe.update(event.delta_x(), event.delta_y()); } else { - seat.get_pointer() - .expect("seat has a pointer") - .gesture_swipe_update( - self, - &GestureSwipeUpdateEvent { - time: event.time_msec(), - delta: event.delta(), - }, - ); + seat.get_pointer().expect("seat has a pointer").gesture_swipe_update( + self, + &GestureSwipeUpdateEvent { + time: event.time_msec(), + delta: event.delta(), + }, + ); } } InputEvent::GestureSwipeEnd { event } => { @@ -140,9 +207,7 @@ 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) => { @@ -152,16 +217,14 @@ impl Ferese { } } } else { - seat.get_pointer() - .expect("seat has a pointer") - .gesture_swipe_end( - self, - &GestureSwipeEndEvent { - serial: SERIAL_COUNTER.next_serial(), - time: event.time_msec(), - cancelled: event.cancelled(), - }, - ); + seat.get_pointer().expect("seat has a pointer").gesture_swipe_end( + self, + &GestureSwipeEndEvent { + serial: SERIAL_COUNTER.next_serial(), + time: event.time_msec(), + cancelled: event.cancelled(), + }, + ); } } InputEvent::SwitchToggle { event } if event.switch() == Some(Switch::Lid) => { @@ -301,6 +364,7 @@ impl Ferese { } } InputEvent::PointerMotionAbsolute { event, .. } => { + self.last_pointer_time = event.time_msec(); let Some(position) = self.absolute_event_position(&event) else { return; }; @@ -325,6 +389,7 @@ impl Ferese { crate::backends::direct::render_all(self); } InputEvent::PointerMotion { event, .. } => { + self.last_pointer_time = event.time_msec(); let pointer = self.seat.get_pointer().expect("seat has a pointer"); let position = pointer.current_location(); let focus = self.surface_under(position); @@ -346,8 +411,7 @@ 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(), utime: event.time(), }, ); @@ -369,6 +433,7 @@ impl Ferese { crate::backends::direct::render_all(self); } InputEvent::PointerButton { event, .. } => { + self.last_pointer_time = event.time_msec(); if self.input_capture.active() { if self.input_capture.captures(2) { self.input_capture.push(serde_json::json!({"type":"button", "button":event.button_code(), "pressed":event.state() == ButtonState::Pressed})); @@ -392,11 +457,8 @@ impl Ferese { && crate::effects::begin_surface_dismiss(&surface) { let opacity = crate::effects::surface_opacity(&surface); - self.dismissing_popups.push(( - root, - popup, - crate::dimming::DimAnimation::new(f64::from(opacity)), - )); + self.dismissing_popups + .push((root, popup, crate::dimming::DimAnimation::new(f64::from(opacity)))); pointer.unset_grab(self, serial, event.time_msec()); self.focus_window_at(pointer.current_location(), serial, true); crate::backends::direct::render_all(self); @@ -466,13 +528,14 @@ impl Ferese { pointer.frame(self); } InputEvent::PointerAxis { event, .. } => { + self.last_pointer_time = event.time_msec(); 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)})); @@ -482,11 +545,7 @@ impl Ferese { let pointer = self.seat.get_pointer().expect("seat has a pointer"); if self.scroll_overview_strip( pointer.current_location(), - if horizontal != 0.0 { - horizontal - } else { - vertical - }, + if horizontal != 0.0 { horizontal } else { vertical }, ) { return; } @@ -577,12 +636,7 @@ impl Ferese { } } - 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) @@ -629,10 +683,8 @@ impl Ferese { Focus::Clear, ); } else { - let horizontal_right = - location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0; - let vertical_bottom = - location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0; + let horizontal_right = location.x >= f64::from(rect.loc.x) + f64::from(rect.size.w) / 2.0; + let vertical_bottom = location.y >= f64::from(rect.loc.y) + f64::from(rect.size.h) / 2.0; let edge = match (horizontal_right, vertical_bottom) { (false, false) => XdgResizeEdge::TopLeft, (true, false) => XdgResizeEdge::TopRight, @@ -655,9 +707,7 @@ 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()) @@ -666,14 +716,8 @@ 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) { @@ -689,10 +733,7 @@ impl Ferese { .output_for_workspace(workspace.id) .is_none_or(|owner| owner == output) }); - let Some(index) = candidates - .iter() - .position(|workspace| workspace.id == current) - else { + let Some(index) = candidates.iter().position(|workspace| workspace.id == current) else { return; }; let index = if next { @@ -799,11 +840,7 @@ 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; } @@ -813,10 +850,7 @@ 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() { @@ -952,9 +986,7 @@ impl Ferese { BindingAction::Focus(direction) => self.focus_direction(direction), 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::SwitchWorkspace(workspace) => { self.switch_workspace(u32::from(workspace)); } @@ -974,6 +1006,55 @@ impl Ferese { } } +fn surface_tree_position( + root: &WlSurface, + target: &WlSurface, + geometry_origin: Point, +) -> Option> { + use smithay::desktop::PopupManager; + + surface_tree_offset(root, target).or_else(|| { + PopupManager::popups_for_surface(root).find_map(|(popup, offset)| { + surface_tree_offset(popup.wl_surface(), target) + .map(|local| geometry_origin + offset - popup.geometry().loc + local) + }) + }) +} + +fn surface_tree_offset(root: &WlSurface, target: &WlSurface) -> Option> { + use std::cell::Cell; + + use smithay::backend::renderer::utils::RendererSurfaceStateUserData; + use smithay::wayland::compositor::{TraversalAction, with_surface_tree_downward}; + + let found = Cell::new(None); + with_surface_tree_downward( + root, + Point::::default(), + |_, states, location| { + states + .data_map + .get::() + .and_then(|data| data.lock().unwrap().view()) + .map_or(TraversalAction::SkipChildren, |view| { + TraversalAction::DoChildren(*location + view.offset) + }) + }, + |surface, states, location| { + if surface == target + && let Some(view) = states + .data_map + .get::() + .and_then(|data| data.lock().unwrap().view()) + { + found.set(Some(*location + view.offset)); + } + }, + |_, _, _| found.get().is_none(), + ); + found.get() +} + fn virtual_terminal(symbol: u32, ctrl: bool, alt: bool, direct: bool) -> Option { if !direct { return None; diff --git a/compositor/src/input_capture.rs b/compositor/src/input_capture.rs index 8dfe4ed..f8a91cb 100644 --- a/compositor/src/input_capture.rs +++ b/compositor/src/input_capture.rs @@ -1,7 +1,5 @@ -use std::{ - collections::{HashMap, VecDeque}, - sync::mpsc::SyncSender, -}; +use std::collections::{HashMap, VecDeque}; +use std::sync::mpsc::SyncSender; use ferese_ipc::Response; use serde::Deserialize; @@ -91,24 +89,14 @@ 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() @@ -186,10 +174,7 @@ 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, @@ -208,24 +193,15 @@ 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()); @@ -250,10 +226,7 @@ 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()); } @@ -279,11 +252,7 @@ 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 @@ -368,10 +337,7 @@ 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(()) @@ -396,18 +362,10 @@ 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) @@ -491,10 +449,7 @@ 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], @@ -503,10 +458,7 @@ 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, @@ -595,11 +547,7 @@ 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]]); @@ -667,19 +615,10 @@ 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 { diff --git a/compositor/src/ipc.rs b/compositor/src/ipc.rs index 2b97f62..83e967b 100644 --- a/compositor/src/ipc.rs +++ b/compositor/src/ipc.rs @@ -1,32 +1,26 @@ -use std::env; -use std::fs; -use std::io; use std::os::fd::AsRawFd; use std::os::unix::fs::{FileTypeExt, MetadataExt, PermissionsExt}; use std::os::unix::net::{UnixListener, UnixStream}; use std::path::{Path, PathBuf}; +use std::sync::Arc; use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering}; -use std::sync::{ - Arc, - mpsc::{SyncSender, sync_channel}, -}; -use std::thread; +use std::sync::mpsc::{SyncSender, sync_channel}; use std::time::Duration; +use std::{env, fs, io, thread}; use ferese_core::LayoutMode; use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame}; use ferese_layout::Direction; use serde_json::{Value, json}; +use smithay::output::Output; use smithay::reexports::calloop::{EventLoop, LoopSignal, channel, timer}; use smithay::utils::Transform; -use crate::handlers::screencopy; -use crate::handlers::screenshot::{ - Action, Geometry, OutputLayout, PartOutcome, PartSender, parse_geometry, plan, -}; -use crate::handlers::screenshot_worker::{self, Encoded, Job, Worker}; -use crate::{Ferese, config::OutputTransform}; -use smithay::output::Output; +use crate::Ferese; +use crate::config::OutputTransform; +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}; const REQUEST_QUEUE_CAPACITY: usize = 128; const MAX_CONNECTIONS: usize = 64; @@ -90,18 +84,14 @@ pub(crate) struct IpcSocketGuard { 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); } } } -pub(crate) fn init( - event_loop: &mut EventLoop<'static, Ferese>, -) -> Result> { +pub(crate) fn init(event_loop: &mut EventLoop<'static, Ferese>) -> Result> { let path = socket_path()?; prepare_parent(&path)?; let listener = bind_listener(&path)?; @@ -115,101 +105,88 @@ 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::Closed(owner) => { - 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.refresh_idle_inhibition(); - return; + event_loop.handle().insert_source(receiver, |event, _, state| { + if let channel::Event::Msg(event) = event { + let call = match event { + IpcEvent::Call(call) => call, + IpcEvent::Closed(owner) => { + let captured = state.input_capture.active(); + state.input_capture.remove_owner(owner); + if captured && !state.input_capture.active() { + state.restore_input_capture_focus(); } - }; - if call.request.command.starts_with("input-capture-") && !call.native_portal { + state.portal_shortcuts.remove(owner); + state.portal_session.remove(owner); + state.refresh_idle_inhibition(); + return; + } + }; + 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, - "access_denied", - "Input capture is restricted to the native portal", + "invalid_argument", + "Missing capture session", )); - } 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 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 == "screenshot-window" { - state.start_window_screenshot(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.request, call.response); - } else { - let response = state.handle_ipc_request(call.owner, call.request); - let _ = call.response.send(response); } + } 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 == "screenshot-window" { + state.start_window_screenshot(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.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 signal = event_loop.get_signal(); @@ -231,9 +208,9 @@ pub(crate) fn init( // Readbacks are published by the render path, so a request whose outputs // are not redrawn would otherwise wait forever. This re-arms to the exact // remaining time whenever a request is outstanding, and idles otherwise. - event_loop.handle().insert_source( - timer::Timer::from_duration(DEADLINE_IDLE), - |_, _, state| { + event_loop + .handle() + .insert_source(timer::Timer::from_duration(DEADLINE_IDLE), |_, _, state| { // Abandoned requests have already been answered, so their // readbacks are simply dropped: they are not failed, because // that would publish into a request that no longer exists. @@ -244,8 +221,7 @@ pub(crate) fn init( } let next = state.screenshot.next_deadline().unwrap_or(DEADLINE_IDLE); timer::TimeoutAction::ToDuration(next) - }, - )?; + })?; tracing::info!(path = %path.display(), "Ferese IPC is accepting connections"); @@ -265,11 +241,7 @@ pub(crate) struct ScreenshotInit { pub(crate) _guard: IpcSocketGuard, } -fn accept_connections( - listener: UnixListener, - sender: channel::SyncSender, - signal: LoopSignal, -) { +fn accept_connections(listener: UnixListener, sender: channel::SyncSender, signal: LoopSignal) { let active_connections = Arc::new(AtomicUsize::new(0)); let connection_ids = AtomicU64::new(1); @@ -296,10 +268,7 @@ 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(); @@ -329,17 +298,10 @@ fn try_acquire_connection(active: &Arc) -> Option } } - Some(ConnectionPermit { - active: active.clone(), - }) + Some(ConnectionPermit { active: active.clone() }) } -fn serve_connection( - mut stream: UnixStream, - sender: channel::SyncSender, - signal: LoopSignal, - owner: u64, -) { +fn serve_connection(mut stream: UnixStream, sender: channel::SyncSender, signal: LoopSignal, owner: u64) { let native_portal = crate::handlers::window_capture::is_portal(&stream); let _connection = IpcConnection { owner, @@ -351,9 +313,7 @@ fn serve_connection( 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; @@ -365,8 +325,7 @@ fn serve_connection( }; let (response, receiver) = sync_channel(1); let exit_requested = request.command == "exit"; - 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(IpcCall { native_portal, owner, @@ -432,10 +391,7 @@ 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 { @@ -459,11 +415,7 @@ 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); @@ -475,12 +427,7 @@ 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 { "portal-inhibit" => { let inhibition = serde_json::from_value(args.clone()) @@ -522,9 +469,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!({})); @@ -534,17 +481,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!({})); } @@ -573,8 +520,7 @@ 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})); } @@ -583,9 +529,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)); } @@ -642,12 +588,8 @@ 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() { @@ -696,9 +638,7 @@ 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()), @@ -783,10 +723,7 @@ 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. @@ -794,8 +731,7 @@ impl Ferese { let id = match self.screenshot.admit(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; } }; @@ -819,11 +755,7 @@ impl Ferese { fn start_window_screenshot(&mut self, 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()); @@ -877,19 +809,13 @@ impl Ferese { let capture = match self.screenshot.admit(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::winit::capture_window_buffer( - backend.renderer(), - &window, - geometry, - scale, - ), + Ok(mut backend) => crate::winit::capture_window_buffer(backend.renderer(), &window, geometry, scale), Err(_) => Err("Window renderer is busy".into()), } } else if let Some(backend) = &mut self.direct_backend { @@ -905,9 +831,7 @@ impl Ferese { } pub(crate) fn on_screenshot_part(&mut self, outcome: PartOutcome) { - let action = self - .screenshot - .on_part(outcome.request, outcome.part, outcome.result); + let action = self.screenshot.on_part(outcome.request, outcome.part, outcome.result); self.apply_screenshot_action(action); } @@ -925,8 +849,7 @@ impl Ferese { .as_ref() .is_some_and(|worker| worker.submit(Job { request, frames }).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) => { @@ -955,13 +878,12 @@ 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.window_ids .keys() .filter_map(|window| window.toplevel()) @@ -974,19 +896,15 @@ impl Ferese { } fn windows_json(&self) -> Value { - use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}; + use smithay::wayland::compositor::with_states; + use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; let mut windows = self .window_ids .iter() .filter_map(|(window, id)| { let toplevel = window.toplevel()?; let (app_id, title) = 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().unwrap_or_default(), attributes.title.clone().unwrap_or_default(), @@ -1044,10 +962,7 @@ impl Ferese { .connected_outputs .iter() .map(|info| { - let mapped = self - .space - .outputs() - .find(|output| output.name() == info.connector); + let mapped = self.space.outputs().find(|output| output.name() == info.connector); let id = mapped.and_then(|output| self.output_id(output)); let geometry = mapped.and_then(|output| self.space.output_geometry(output)); let workspace = id.and_then(|id| self.output_workspaces.active_workspace(id)); @@ -1105,9 +1020,7 @@ 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); } @@ -1163,9 +1076,7 @@ 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) } } @@ -1179,10 +1090,7 @@ 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(()) @@ -1247,12 +1155,7 @@ 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 socket_path() -> Result { @@ -1264,9 +1167,7 @@ fn socket_path() -> Result { } 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"); match fs::symlink_metadata(parent) { Ok(metadata) => { if !metadata.is_dir() || metadata.file_type().is_symlink() { @@ -1311,11 +1212,7 @@ 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 @@ -1361,10 +1258,7 @@ 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()); } @@ -1402,9 +1296,7 @@ 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 @@ -1451,10 +1343,7 @@ 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(); @@ -1470,10 +1359,7 @@ 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(); } diff --git a/compositor/src/main.rs b/compositor/src/main.rs index 4574bb2..7d95efe 100644 --- a/compositor/src/main.rs +++ b/compositor/src/main.rs @@ -28,16 +28,14 @@ mod wallpaper; mod window_rules; mod winit; -use std::{ - error::Error, - io, - process::{Child, Command}, - thread, - time::Duration, -}; +use std::error::Error; +use std::process::{Child, Command}; +use std::time::Duration; +use std::{io, thread}; use calloop::signals::{Signal, Signals}; -use smithay::reexports::{calloop::EventLoop, wayland_server::Display}; +use smithay::reexports::calloop::EventLoop; +use smithay::reexports::wayland_server::Display; pub use state::{Ferese, RuntimeConfig}; use tracing::{info, warn}; @@ -60,8 +58,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 initial_source = config::config_path().and_then(|path| std::fs::read_to_string(path).ok()); let config = match initial_source.as_deref() { Some(source) => Config::parse_source(source)?, @@ -81,15 +79,15 @@ fn main() -> Result<(), Box> { state.config_source = initial_source.filter(|source| source.len() <= 60 * 1024); overview::init_font_loader(&mut event_loop, &mut state)?; backends::init(launch.backend, &mut event_loop, &mut state)?; - let mut monitor = config::config_path().and_then( - |path| match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) { + let mut monitor = config::config_path().and_then(|path| { + match reload::ConfigMonitor::with_wakeup(path, Some(event_loop.get_signal())) { Ok(monitor) => Some(monitor), Err(error) => { warn!(%error, "automatic config watching unavailable; use feresectl reload-config"); None } - }, - ); + } + }); use calloop::timer::{TimeoutAction, Timer}; info!(socket = ?state.socket_name, backend = ?launch.backend, "Ferese is accepting Wayland clients"); let mut child = spawn_client(&mut state, launch.client, launch.client_capabilities); @@ -141,9 +139,9 @@ fn main() -> Result<(), Box> { if state.input_capture.restore_focus { state.restore_input_capture_focus(); } + state.apply_unlocked_pointer_hint(); let wallpaper_retry = state.wallpaper.take_retry_wakeup(); - if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry - { + if std::mem::take(&mut state.cursor_redraw_pending) || wallpaper_changed || wallpaper_retry { backends::direct::render_all(state); } if let Err(error) = state @@ -242,12 +240,11 @@ fn terminate_child(child: &mut Child) { #[cfg(test)] mod startup_tests { + use std::io::{Read, Write}; + use std::os::unix::net::UnixStream; + use std::sync::Arc; + use super::*; - use std::{ - io::{Read, Write}, - os::unix::net::UnixStream, - sync::Arc, - }; #[test] fn deferred_cursor_redraw_runs_after_input_lock_is_released() { @@ -295,15 +292,10 @@ mod startup_tests { } display.dispatch_clients(&mut ()).unwrap(); display.handle().flush_clients().unwrap(); - client - .set_read_timeout(Some(Duration::from_secs(1))) - .unwrap(); + client.set_read_timeout(Some(Duration::from_secs(1))).unwrap(); let mut reply = [0u8; 12]; 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/metrics.rs b/compositor/src/metrics.rs index 14def73..2169090 100644 --- a/compositor/src/metrics.rs +++ b/compositor/src/metrics.rs @@ -1,7 +1,5 @@ -use std::{ - collections::VecDeque, - time::{Duration, Instant}, -}; +use std::collections::VecDeque; +use std::time::{Duration, Instant}; use smithay::utils::{Physical, Rectangle}; @@ -94,14 +92,8 @@ 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", @@ -131,8 +123,7 @@ 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)] @@ -211,14 +202,8 @@ 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/overview.rs b/compositor/src/overview.rs index 1748c03..d0dead0 100644 --- a/compositor/src/overview.rs +++ b/compositor/src/overview.rs @@ -1,23 +1,17 @@ -use std::{ - collections::HashMap, - sync::{Arc, Mutex, mpsc}, - time::Duration, -}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, mpsc}; +use std::time::Duration; +use ab_glyph::{Font, FontArc, FontVec, ScaleFont}; use ferese_animation::{AnimatedRect, AnimatedValue, SpringConfig}; use ferese_core::{OutputId, WorkspaceId}; use ferese_layout::{Direction, Rect, WindowId}; -use smithay::{ - output::Output, - utils::{Logical, Point}, -}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::memory::MemoryRenderBuffer; +use smithay::output::Output; +use smithay::utils::{Logical, Point, Transform}; use crate::Ferese; -use ab_glyph::{Font, FontArc, FontVec, ScaleFont}; -use smithay::{ - backend::{allocator::Fourcc, renderer::element::memory::MemoryRenderBuffer}, - utils::Transform, -}; const OVERVIEW_MARGIN: f64 = 48.0; const OVERVIEW_GAP: f64 = 40.0; @@ -46,16 +40,14 @@ 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() - { - 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() + { + 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); @@ -92,13 +84,8 @@ pub(crate) fn init_font_loader( Ok(()) } -type OverviewStateLabel = HashMap< - (String, u64, [u32; 4]), - ( - MemoryRenderBuffer, - smithay::utils::Size, - ), ->; +type OverviewStateLabel = + HashMap<(String, u64, [u32; 4]), (MemoryRenderBuffer, smithay::utils::Size)>; #[derive(Debug)] pub(crate) struct OverviewState { @@ -154,19 +141,11 @@ 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 { @@ -212,10 +191,7 @@ impl OverviewState { text: &str, scale: f64, color: [f32; 4], - ) -> Option<( - MemoryRenderBuffer, - smithay::utils::Size, - )> { + ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { let key = (text.to_owned(), scale.to_bits(), color.map(f32::to_bits)); if let Some(buffer) = self.labels.get(&key) { return Some(buffer.clone()); @@ -234,10 +210,7 @@ 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(); @@ -257,14 +230,8 @@ 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.labels.len() >= 128 { self.labels.clear(); } @@ -291,13 +258,7 @@ impl OverviewState { .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 selected(&self) -> Option { @@ -319,13 +280,9 @@ 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)) - }) - .min_by(|(left_id, left), (right_id, right)| { - left.total_cmp(right) - .then_with(|| left_id.0.cmp(&right_id.0)) + 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))) .map(|(id, _)| id); let Some(next) = next else { return false; @@ -396,10 +353,7 @@ impl OverviewState { presentation.snap(); } } - if self - .selected - .is_none_or(|id| !self.presentations.contains_key(&id)) - { + if self.selected.is_none_or(|id| !self.presentations.contains_key(&id)) { self.selected = self.presentations.keys().copied().min_by_key(|id| id.0); } } @@ -429,12 +383,7 @@ 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 { if let Some((elapsed, initial_opacity)) = self.exit_transition { let elapsed = if animations_enabled { elapsed + delta.as_secs_f64() @@ -506,18 +455,12 @@ impl Ferese { window: &smithay::desktop::Window, scale: f64, width: f64, - ) -> Option<( - MemoryRenderBuffer, - smithay::utils::Size, - )> { - use smithay::wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}; + ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { + use smithay::wayland::compositor::with_states; + use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; let surface = window.toplevel()?.wl_surface(); let title = with_states(surface, |states| { - let data = states - .data_map - .get::()? - .lock() - .ok()?; + let data = states.data_map.get::()?.lock().ok()?; data.title .clone() .filter(|title| !title.trim().is_empty()) @@ -539,10 +482,7 @@ impl Ferese { &mut self, workspace: WorkspaceId, scale: f64, - ) -> Option<( - MemoryRenderBuffer, - smithay::utils::Size, - )> { + ) -> Option<(MemoryRenderBuffer, smithay::utils::Size)> { let name = self.workspaces.workspace(workspace)?.name.clone(); self.overview .label(&name, scale, self.theme_settings.text_primary_color.0) @@ -621,8 +561,7 @@ impl Ferese { .collect::>() }) .unwrap_or_default(); - self.overview - .select_workspace_windows(&windows, self.focused_window); + self.overview.select_workspace_windows(&windows, self.focused_window); } } @@ -637,12 +576,7 @@ impl Ferese { Some(presented) } - 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.window_geometry.get(&id)?; if !self.overview.is_presenting() { let presented = self.presented_window_rect(id)?; @@ -651,11 +585,7 @@ impl Ferese { let source = geometry.client.committed_size?; let presented = self.overview.presented_rect(id, geometry.visual.current); - 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; } @@ -692,10 +622,9 @@ 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 @@ -726,9 +655,7 @@ impl Ferese { (rect.width - 12.0).max(1.0), (rect.height - 26.0).max(1.0), ); - let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0) - .into_iter() - .collect(); + let windows = preview_layout(preview_bounds, &windows, 4.0, 4.0).into_iter().collect(); WorkspaceCard { workspace: workspace.id, rect, @@ -796,11 +723,7 @@ impl Ferese { let ordered = self.workspaces.ordered(); let current = cards .first() - .and_then(|card| { - ordered - .iter() - .position(|workspace| workspace.id == card.workspace) - }) + .and_then(|card| ordered.iter().position(|workspace| workspace.id == card.workspace)) .unwrap_or(0); let next = if delta > 0.0 { current.saturating_add(1) @@ -863,12 +786,7 @@ fn overview_layout(bounds: Rect, windows: &[(WindowId, Rect)]) -> HashMap HashMap { +fn preview_layout(bounds: Rect, windows: &[(WindowId, Rect)], margin: f64, gap: f64) -> HashMap { if windows.is_empty() { return HashMap::new(); } @@ -881,10 +799,8 @@ fn preview_layout( 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| { @@ -926,10 +842,8 @@ fn preview_layout( 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, @@ -947,11 +861,7 @@ 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 { @@ -1001,10 +911,7 @@ 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))); @@ -1024,9 +931,7 @@ 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); } } @@ -1043,11 +948,7 @@ 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)); } } @@ -1146,10 +1047,7 @@ 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); @@ -1235,10 +1133,7 @@ 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 2ac65f7..020d0f6 100644 --- a/compositor/src/portal_session.rs +++ b/compositor/src/portal_session.rs @@ -1,12 +1,11 @@ +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::mpsc::SyncSender; +use std::time::{Duration, Instant}; + use ferese_ipc::Response; use serde::Deserialize; use serde_json::{Value, json}; use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; -use std::{ - collections::{HashMap, HashSet, VecDeque}, - sync::mpsc::SyncSender, - time::{Duration, Instant}, -}; #[derive(Debug, Deserialize)] #[serde(deny_unknown_fields)] @@ -104,31 +103,21 @@ 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(); } } 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 { @@ -152,11 +141,7 @@ 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); } } @@ -169,42 +154,19 @@ 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> { - if !self.query_ready() - || !self - .query - .as_ref() - .is_some_and(|query| query.token == token) - { - return Err( - "Session-ending confirmation expired or applications are still responding".into(), - ); + pub(crate) fn validate_end(&self, token: u32, inhibitor_revision: u32, force: bool) -> Result<(), String> { + if !self.query_ready() || !self.query.as_ref().is_some_and(|query| query.token == token) { + 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(()) @@ -247,13 +209,7 @@ 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 { @@ -262,12 +218,13 @@ impl PortalSession { } pub(crate) fn snapshot(&self) -> Value { - let inhibitors = self.inhibitors.values().map(|inhibitor| json!({"flags": inhibitor.flags, "app": inhibitor.app, "reason": inhibitor.reason})).collect::>(); + let inhibitors = self + .inhibitors + .values() + .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, @@ -278,24 +235,22 @@ impl PortalSession { impl crate::Ferese { pub(crate) fn refresh_idle_inhibition(&mut self) { - self.idle_notifier_state.set_is_inhibited( - !self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited(), - ); + self.idle_notifier_state + .set_is_inhibited(!self.idle_inhibitors.is_empty() || self.portal_session.idle_inhibited()); } 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(); @@ -354,20 +309,10 @@ 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] @@ -410,10 +355,7 @@ 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 e5ef346..5d3f83a 100644 --- a/compositor/src/portal_shortcuts.rs +++ b/compositor/src/portal_shortcuts.rs @@ -1,7 +1,5 @@ -use std::{ - collections::{HashMap, VecDeque}, - time::Instant, -}; +use std::collections::{HashMap, VecDeque}; +use std::time::Instant; use serde::Deserialize; use serde_json::{Value, json}; @@ -28,9 +26,7 @@ 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) { @@ -57,11 +53,7 @@ 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(); @@ -70,17 +62,11 @@ 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)) @@ -120,9 +106,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() @@ -158,13 +144,7 @@ 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) @@ -212,9 +192,10 @@ impl PortalShortcuts { #[cfg(test)] mod tests { - use super::*; use smithay::input::keyboard::keysyms; + use super::*; + fn shortcut(id: &str, trigger: &str) -> Shortcut { Shortcut { id: id.into(), @@ -244,10 +225,7 @@ 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 @@ -296,12 +274,7 @@ 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()); } @@ -332,10 +305,9 @@ 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 ce8169f..7ad5dc1 100644 --- a/compositor/src/presentation.rs +++ b/compositor/src/presentation.rs @@ -2,21 +2,63 @@ //! logical pixels. Content, clips and decorations share those exact edges. use std::time::{Duration, Instant}; -use smithay::{ - backend::renderer::{ - element::{Element, Id, Kind, RenderElement}, - gles::{ - GlesError, GlesFrame, GlesRenderer, GlesTexProgram, GlesTexture, Uniform, - element::PixelShaderElement, - }, - utils::{CommitCounter, DamageSet}, - }, - utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform}, +use smithay::backend::renderer::element::{ + Element, Id, Kind, RenderElement, RenderElementPresentationState, RenderElementStates, }; +use smithay::backend::renderer::gles::element::PixelShaderElement; +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; +use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback; +use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface; +use smithay::utils::{Buffer, Logical, Physical, Point, Rectangle, Scale, Transform}; +use smithay::wayland::compositor::SurfaceData; pub(crate) const HANDOFF: Duration = Duration::from_millis(80); pub(crate) const SNAPSHOT_BUDGET: usize = 64 * 1024 * 1024; +pub(crate) fn take_output_feedback( + state: &crate::Ferese, + output: &Output, + rendered: &RenderElementStates, + flags: wp_presentation_feedback::Kind, +) -> OutputPresentationFeedback { + use smithay::desktop::layer_map_for_output; + 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 surface_flags = |surface: &WlSurface, _: &SurfaceData| { + flags | surface_presentation_feedback_flags_from_states(surface, rendered) + }; + + for window in state.space.elements().filter(|window| { + state + .window_ids + .get(*window) + .is_some_and(|id| state.window_belongs_to_output(*id, output)) + }) { + window.take_presentation_feedback(&mut feedback, visible_output, surface_flags); + } + + for layer in layer_map_for_output(output).layers() { + layer.take_presentation_feedback(&mut feedback, visible_output, surface_flags); + } + + feedback +} + +fn rendered_feedback_output(output: &Output, id: Id, rendered: &RenderElementStates) -> Option { + rendered + .element_render_state(id) + .is_some_and(|state| { + state.visible_area > 0 && state.presentation_state != RenderElementPresentationState::Skipped + }) + .then(|| output.clone()) +} + #[derive(Default)] pub(crate) struct CallbackClock { last_sent: Option, @@ -24,8 +66,7 @@ pub(crate) struct CallbackClock { impl CallbackClock { pub(crate) fn deadline(&self, now: Instant, interval: Duration) -> Instant { - self.last_sent - .map_or(now, |last| (last + interval).max(now)) + self.last_sent.map_or(now, |last| (last + interval).max(now)) } pub(crate) fn sent(&mut self, now: Instant) { @@ -48,6 +89,7 @@ pub(crate) fn physical_rect( let top = ((rect.y - f64::from(origin.y)) * scale).round() as i32; let right = ((rect.x + rect.width - f64::from(origin.x)) * scale).round() as i32; let bottom = ((rect.y + rect.height - f64::from(origin.y)) * scale).round() as i32; + Rectangle::new( (left, top).into(), ((right - left).max(1), (bottom - top).max(1)).into(), @@ -122,11 +164,7 @@ 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 { @@ -187,11 +225,7 @@ 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 { @@ -227,26 +261,59 @@ impl RenderElement for NativeTextureElement { mod tests { use super::*; + #[test] + fn presentation_feedback_excludes_missing_and_occluded_surfaces() { + use smithay::backend::renderer::element::RenderElementState; + use smithay::output::{PhysicalProperties, Subpixel}; + + let output = Output::new( + "owner".into(), + PhysicalProperties { + size: (0, 0).into(), + subpixel: Subpixel::Unknown, + make: "test".into(), + model: "test".into(), + }, + ); + let id = Id::new(); + let mut states = RenderElementStates::default(); + assert!(rendered_feedback_output(&output, id.clone(), &states).is_none()); + for state in [ + RenderElementState { + visible_area: 0, + presentation_state: RenderElementPresentationState::Skipped, + }, + RenderElementState { + visible_area: 0, + presentation_state: RenderElementPresentationState::Rendering { reason: None }, + }, + ] { + states.states.insert(id.clone(), state); + assert!(rendered_feedback_output(&output, id.clone(), &states).is_none()); + } + for presentation_state in [ + RenderElementPresentationState::Rendering { reason: None }, + RenderElementPresentationState::ZeroCopy, + ] { + states.states.insert( + id.clone(), + RenderElementState { + visible_area: 32, + presentation_state, + }, + ); + assert_eq!( + rendered_feedback_output(&output, id.clone(), &states), + Some(output.clone()) + ); + } + } + #[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] @@ -347,25 +414,14 @@ 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/private_client.rs b/compositor/src/private_client.rs index d23ed20..77f11e2 100644 --- a/compositor/src/private_client.rs +++ b/compositor/src/private_client.rs @@ -1,14 +1,12 @@ -use std::{ - io, - os::{ - fd::AsRawFd, - unix::{net::UnixStream, process::CommandExt}, - }, - process::Command, - sync::Arc, -}; +use std::io; +use std::os::fd::AsRawFd; +use std::os::unix::net::UnixStream; +use std::os::unix::process::CommandExt; +use std::process::Command; +use std::sync::Arc; -use crate::{Ferese, state::ClientState}; +use crate::Ferese; +use crate::state::ClientState; #[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] pub(crate) struct ClientCapabilities(u8); @@ -36,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 { @@ -56,10 +54,7 @@ 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); } @@ -81,10 +76,7 @@ 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/reload.rs b/compositor/src/reload.rs index da6e49a..d8c82f7 100644 --- a/compositor/src/reload.rs +++ b/compositor/src/reload.rs @@ -1,11 +1,10 @@ //! Filesystem events wake the loop; only pending edits need a debounce timer. +use std::io::Read; +use std::path::PathBuf; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + use notify::{RecommendedWatcher, RecursiveMode, Watcher}; -use std::{ - io::Read, - path::PathBuf, - sync::mpsc, - time::{Duration, Instant}, -}; fn read_source(path: &std::path::Path) -> Result { let file = std::fs::File::open(path).map_err(|e| e.to_string())?; @@ -40,21 +39,17 @@ impl ConfigMonitor { let (sender, events) = mpsc::sync_channel(1); let target = path.clone(); let event_wakeup = wakeup.clone(); - let mut watcher = - notify::recommended_watcher(move |event: notify::Result| { - if let Ok(event) = event - && !event.kind.is_access() - && event - .paths - .iter() - .any(|p| p == &target || target.starts_with(p)) - { - let _ = sender.try_send(()); - if let Some(wakeup) = &event_wakeup { - wakeup.wakeup(); - } + let mut watcher = notify::recommended_watcher(move |event: notify::Result| { + if let Ok(event) = event + && !event.kind.is_access() + && event.paths.iter().any(|p| p == &target || target.starts_with(p)) + { + let _ = sender.try_send(()); + if let Some(wakeup) = &event_wakeup { + wakeup.wakeup(); } - })?; + } + })?; // Watch the directory, not the inode: atomic editor saves replace it. let mut directory = path.parent().unwrap_or(std::path::Path::new(".")); while !directory.is_dir() { @@ -224,10 +219,7 @@ mod tests { assert_eq!(result.unwrap(), "animations {\n speed 0.75\n}\n"); break; } - 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)); } } @@ -242,17 +234,9 @@ mod tests { let mut monitor = ConfigMonitor::new(path).unwrap(); let now = Instant::now(); monitor.dirty = Some(now); - assert_eq!( - monitor.next_deadline(), - Some(now + Duration::from_millis(120)) - ); + assert_eq!(monitor.next_deadline(), Some(now + Duration::from_millis(120))); assert!(monitor.poll(now + Duration::from_millis(119)).is_none()); - assert!( - monitor - .poll(now + Duration::from_millis(121)) - .unwrap() - .is_ok() - ); + assert!(monitor.poll(now + Duration::from_millis(121)).unwrap().is_ok()); assert_eq!(monitor.next_deadline(), None); } } diff --git a/compositor/src/resize_transaction.rs b/compositor/src/resize_transaction.rs index 31b57f1..b448bae 100644 --- a/compositor/src/resize_transaction.rs +++ b/compositor/src/resize_transaction.rs @@ -72,8 +72,7 @@ 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/session_lock.rs b/compositor/src/session_lock.rs index 42e1ab1..ba91b82 100644 --- a/compositor/src/session_lock.rs +++ b/compositor/src/session_lock.rs @@ -1,29 +1,21 @@ +use std::collections::{HashMap, HashSet}; +use std::time::{Duration, Instant}; + +use smithay::output::Output; +use smithay::reexports::calloop::RegistrationToken; +use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; +use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_manager_v1::ExtSessionLockManagerV1; +use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_surface_v1::ExtSessionLockSurfaceV1; +use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1; +use smithay::reexports::wayland_protocols::ext::session_lock::v1::server::ext_session_lock_v1::ExtSessionLockV1; +use smithay::reexports::wayland_server::protocol::wl_output::WlOutput; +use smithay::reexports::wayland_server::{Client, DataInit, Dispatch, DisplayHandle, Resource}; +use smithay::utils::{Logical, Point, SERIAL_COUNTER}; +use smithay::wayland::session_lock::{ + LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker, +}; + use crate::Ferese; -use smithay::reexports::calloop::{ - RegistrationToken, - timer::{TimeoutAction, Timer}, -}; -use smithay::{ - output::Output, - reexports::{ - wayland_protocols::ext::session_lock::v1::server::{ - ext_session_lock_manager_v1::ExtSessionLockManagerV1, - ext_session_lock_surface_v1::ExtSessionLockSurfaceV1, - ext_session_lock_v1::{self, ExtSessionLockV1}, - }, - wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, Resource, protocol::wl_output::WlOutput, - }, - }, - utils::{Logical, Point, SERIAL_COUNTER}, - wayland::session_lock::{ - LockSurface, SessionLockHandler, SessionLockManagerState, SessionLockState, SessionLocker, - }, -}; -use std::{ - collections::{HashMap, HashSet}, - time::{Duration, Instant}, -}; #[derive(Clone, Copy, Debug, serde::Deserialize)] #[serde(default)] @@ -54,10 +46,7 @@ impl IdleSettings { fn next_poll(self, elapsed: Duration, animate: bool) -> Duration { let dim_at = Duration::from_secs(self.dim_after_seconds); - if animate - && self.dim_after_seconds != 0 - && elapsed >= dim_at - && elapsed < dim_at + Duration::from_millis(500) + if animate && self.dim_after_seconds != 0 && elapsed >= dim_at && elapsed < dim_at + Duration::from_millis(500) { return Duration::from_millis(16); } @@ -72,8 +61,7 @@ impl IdleSettings { } fn appearance(self, elapsed: 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 = if self.dim_after_seconds == 0 { 0.0 } else { @@ -99,17 +87,13 @@ 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, } 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) { @@ -146,14 +130,11 @@ impl Ferese { return; } let since = self.session_lock.idle_since.get_or_insert(now); - let (mut opacity, sleeping) = self - .lock_idle - .appearance(now.saturating_duration_since(*since)); + let (mut opacity, sleeping) = self.lock_idle.appearance(now.saturating_duration_since(*since)); if !self.animations_enabled() && opacity > 0.0 && !sleeping { opacity = 0.65; } - 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; @@ -190,12 +171,7 @@ impl Ferese { let surface = self .focused_output() .and_then(|output| self.session_lock.surfaces.get(output)) - .or_else(|| { - self.session_lock - .surfaces - .values() - .find(|surface| surface.alive()) - }) + .or_else(|| self.session_lock.surfaces.values().find(|surface| surface.alive())) .filter(|surface| surface.alive()) .map(|surface| surface.wl_surface().clone()); self.seat @@ -234,12 +210,7 @@ impl Ferese { } pub(crate) fn lock_frame_callbacks(&self, output: &Output) { - 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, @@ -263,9 +234,9 @@ impl SessionLockHandler for Ferese { self.cancel_logout_confirmation(); self.session_lock.active = true; self.session_lock.idle_since = Some(Instant::now()); - match self.loop_handle.insert_source( - Timer::from_duration(Duration::from_secs(1)), - |_, _, state| { + match self + .loop_handle + .insert_source(Timer::from_duration(Duration::from_secs(1)), |_, _, state| { if !state.session_lock.active { return TimeoutAction::Drop; } @@ -274,13 +245,8 @@ impl SessionLockHandler for Ferese { .session_lock .idle_since .map_or(Duration::ZERO, |since| since.elapsed()); - TimeoutAction::ToDuration( - state - .lock_idle - .next_poll(elapsed, state.animations_enabled()), - ) - }, - ) { + TimeoutAction::ToDuration(state.lock_idle.next_poll(elapsed, state.animations_enabled())) + }) { Ok(token) => self.session_lock.idle_timer = Some(token), Err(error) => tracing::warn!(%error, "could not schedule lock screen inactivity"), } @@ -295,10 +261,7 @@ 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(); @@ -345,9 +308,8 @@ 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())); self.session_lock.surfaces.insert(output, surface); self.focus_lock_surface(); } @@ -377,10 +339,7 @@ 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; } >::request( @@ -413,10 +372,7 @@ mod tests { policy.next_poll(Duration::from_secs(30), true), Duration::from_millis(16) ); - assert_eq!( - policy.next_poll(Duration::from_secs(30), false), - Duration::from_secs(1) - ); + assert_eq!(policy.next_poll(Duration::from_secs(30), false), Duration::from_secs(1)); } #[test] @@ -498,9 +454,7 @@ mod tests { .validate() .is_err() ); - assert!( - crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err() - ); + assert!(crate::config::Config::parse_source("lock-screen { dim-after-seconds -1; }").is_err()); assert!( crate::config::Config::parse_source("lock-screen { sleep-after-seconds 90000; }") .unwrap() diff --git a/compositor/src/shell_control.rs b/compositor/src/shell_control.rs index c79242e..0ac4757 100644 --- a/compositor/src/shell_control.rs +++ b/compositor/src/shell_control.rs @@ -1,17 +1,15 @@ use ferese_core::{OutputId, WorkspaceId}; use ferese_layout::WindowId; -use ferese_protocols::shell::v1::server::{ - ferese_shell_manager_v1::{self, FereseShellManagerV1}, - ferese_shell_v1::{self, FereseShellV1}, -}; -use smithay::{ - reexports::wayland_server::{ - Client, DataInit, Dispatch, DisplayHandle, GlobalDispatch, New, Resource, - }, - wayland::{compositor::with_states, shell::xdg::XdgToplevelSurfaceData}, -}; +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::wayland::compositor::with_states; +use smithay::wayland::shell::xdg::XdgToplevelSurfaceData; -use crate::{Ferese, private_client::ClientCapabilities, state::ClientState}; +use crate::Ferese; +use crate::private_client::ClientCapabilities; +use crate::state::ClientState; pub(crate) fn init_global(display: &DisplayHandle) { display.create_global::(4, ()); @@ -52,11 +50,9 @@ 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)) } } @@ -104,19 +100,13 @@ impl Dispatch for Ferese { return; } match request { - 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,11 +119,7 @@ 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::ConfirmLogout { serial } => { @@ -149,36 +135,21 @@ 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 => { @@ -202,12 +173,7 @@ 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) } @@ -221,14 +187,7 @@ 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) { @@ -269,9 +228,7 @@ 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); } @@ -280,11 +237,7 @@ 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); } @@ -294,8 +247,7 @@ impl Ferese { } pub(crate) fn send_shell_snapshots(&mut self) { - 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; @@ -325,8 +277,7 @@ 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 { @@ -354,9 +305,8 @@ impl Ferese { .into_iter() .map(|workspace| { let output = self.output_workspaces.output_for_workspace(workspace.id); - let active = output.is_some_and(|output| { - self.output_workspaces.active_workspace(output) == Some(workspace.id) - }); + let active = + output.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace.id)); WorkspaceSnapshot { id: workspace.id, @@ -479,10 +429,7 @@ 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, @@ -564,12 +511,7 @@ 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)); } @@ -633,11 +575,7 @@ 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 d42bbcd..3931ef0 100644 --- a/compositor/src/stacking.rs +++ b/compositor/src/stacking.rs @@ -1,12 +1,8 @@ -use ferese_layout::WindowId; use std::collections::HashMap; -pub(crate) fn layer_priority( - floating: bool, - zooming: bool, - fullscreen: bool, - above_fullscreen: bool, -) -> u8 { +use ferese_layout::WindowId; + +pub(crate) fn layer_priority(floating: bool, zooming: bool, fullscreen: bool, above_fullscreen: bool) -> u8 { if above_fullscreen { 4 } else if fullscreen { @@ -97,18 +93,10 @@ 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/state.rs b/compositor/src/state.rs index ee52397..770a608 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -1,85 +1,90 @@ -use std::{ - collections::{HashMap, HashSet, hash_map::Entry}, - error::Error, - ffi::OsString, - sync::Arc, - time::{Duration, Instant}, -}; +use std::collections::hash_map::Entry; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::ffi::OsString; +use std::sync::Arc; +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::allocator::dmabuf::Dmabuf; +use smithay::backend::drm::{DrmEventTime, DrmNode}; +use smithay::backend::input::Keycode; +use smithay::backend::renderer::{ErasedContextId, ImportDma}; +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::input::keyboard::XkbConfig; +use smithay::input::pointer::{CursorIcon, CursorImageStatus}; +use smithay::input::{Seat, SeatState}; +use smithay::output::Output; +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::decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode; +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::protocol::wl_surface::WlSurface; +use smithay::reexports::wayland_server::{Display, DisplayHandle, Weak}; +use smithay::utils::{Logical, Point, Rectangle, Size}; +use smithay::wayland::alpha_modifier::AlphaModifierState; +use smithay::wayland::compositor::{CompositorClientState, CompositorState, with_states}; +use smithay::wayland::cursor_shape::CursorShapeManagerState; +use smithay::wayland::dmabuf::{DmabufState, ImportNotifier}; +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::output::OutputManagerState; +use smithay::wayland::pointer_constraints::PointerConstraintsState; +use smithay::wayland::presentation::PresentationState; +use smithay::wayland::relative_pointer::RelativePointerManagerState; +use smithay::wayland::selection::data_device::DataDeviceState; +use smithay::wayland::selection::primary_selection::PrimarySelectionState; +use smithay::wayland::session_lock::SessionLockManagerState; +use smithay::wayland::shell::wlr_layer::{Layer, WlrLayerShellState}; +use smithay::wayland::shell::xdg::decoration::XdgDecorationState; +use smithay::wayland::shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData}; +use smithay::wayland::shm::ShmState; +use smithay::wayland::single_pixel_buffer::SinglePixelBufferState; +use smithay::wayland::socket::ListeningSocketSource; +use smithay::wayland::text_input::TextInputManagerState; +use smithay::wayland::viewporter::ViewporterState; +use smithay::wayland::xdg_activation::XdgActivationState; +use smithay::wayland::xdg_foreign::XdgForeignState; +use smithay::wayland::xdg_toplevel_icon::XdgToplevelIconManager; -use smithay::{ - backend::{ - allocator::dmabuf::Dmabuf, - drm::{DrmEventTime, DrmNode}, - renderer::{ErasedContextId, ImportDma}, - }, - desktop::{ - LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output, - space::SpaceElement, utils::send_frames_surface_tree, - }, - input::{ - Seat, SeatState, - keyboard::XkbConfig, - pointer::{CursorIcon, CursorImageStatus}, - }, - output::Output, - reexports::{ - calloop::{EventLoop, Interest, LoopSignal, Mode, PostAction, generic::Generic}, - drm::control::crtc, - wayland_protocols::xdg::{ - decoration::zv1::server::zxdg_toplevel_decoration_v1::Mode as DecorationMode, - shell::server::xdg_toplevel, - }, - wayland_server::{ - Display, DisplayHandle, - backend::{ClientData, ClientId, DisconnectReason}, - protocol::wl_surface::WlSurface, - }, - }, - utils::{Logical, Point, Rectangle, Size}, - wayland::{ - alpha_modifier::AlphaModifierState, - compositor::{CompositorClientState, CompositorState, with_states}, - cursor_shape::CursorShapeManagerState, - dmabuf::{DmabufState, ImportNotifier}, - fractional_scale::FractionalScaleManagerState, - idle_inhibit::IdleInhibitManagerState, - idle_notify::IdleNotifierState, - input_method::InputMethodManagerState, - keyboard_shortcuts_inhibit::{KeyboardShortcutsInhibitState, KeyboardShortcutsInhibitor}, - output::OutputManagerState, - pointer_constraints::PointerConstraintsState, - presentation::PresentationState, - relative_pointer::RelativePointerManagerState, - selection::{data_device::DataDeviceState, primary_selection::PrimarySelectionState}, - shell::wlr_layer::Layer, - shell::wlr_layer::WlrLayerShellState, - shell::xdg::decoration::XdgDecorationState, - shell::xdg::{SurfaceCachedState, XdgShellState, XdgToplevelSurfaceData}, - shm::ShmState, - single_pixel_buffer::SinglePixelBufferState, - socket::ListeningSocketSource, - text_input::TextInputManagerState, - viewporter::ViewporterState, - xdg_activation::XdgActivationState, - xdg_foreign::XdgForeignState, - xdg_toplevel_icon::XdgToplevelIconManager, - }, -}; - -use crate::{ - config::{Binding, InputSettings, OutputProfile, ThemeSettings}, - gestures::SwipeDirection, - window_rules::{WindowRule, resolve as resolve_window_rules}, +use crate::backends::direct::DirectBackendState; +use crate::config::{Binding, DaemonConfig, InputSettings, OutputProfile, ThemeSettings}; +use crate::cursor::NamedCursor; +use crate::dimming::DimAnimation; +use crate::gestures::{Swipe, SwipeDirection}; +use crate::handlers::screencopy::PendingScreencopy; +use crate::handlers::screenshot::{Coordinator, PartSender}; +use crate::handlers::screenshot_worker::Worker; +use crate::input::LockedPointerHint; +use crate::input_capture::InputCapture; +use crate::ipc::IpcSocketGuard; +use crate::portal_session::PortalSession; +use crate::portal_shortcuts::PortalShortcuts; +use crate::resize_transaction::ResizeTransaction; +use crate::session_lock::{IdleSettings, Lock}; +use crate::shell_control::ShellSnapshot; +use crate::stacking::WindowStack; +use crate::wallpaper::WallpaperState; +use crate::window_rules::{WindowRule, resolve as resolve_window_rules}; +use crate::winit::{ + BlurProgram, MaterialBuffers, MaterialProgram, NestedBackend, OverviewScrim, ResizeSnapshot, RoundedClipPrograms, + WindowBorderBuffers, WindowShadowBuffers, }; const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140); @@ -103,10 +108,7 @@ 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 { @@ -129,12 +131,7 @@ 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| { @@ -212,8 +209,7 @@ impl ClosingAnimation { return false; } - self.progress = - (self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0); + self.progress = (self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0); if self.progress < 1.0 { return false; } @@ -224,17 +220,17 @@ impl ClosingAnimation { } pub struct Ferese { - pub(crate) session_lock_state: smithay::wayland::session_lock::SessionLockManagerState, - pub(crate) session_lock: crate::session_lock::Lock, - pub(crate) lock_idle: crate::session_lock::IdleSettings, + pub(crate) session_lock_state: SessionLockManagerState, + pub(crate) session_lock: Lock, + pub(crate) lock_idle: IdleSettings, pub(crate) config_source: Option, pub start_time: Instant, pub socket_name: OsString, pub display_handle: DisplayHandle, pub loop_signal: LoopSignal, - pub(crate) loop_handle: smithay::reexports::calloop::LoopHandle<'static, Self>, - pub(crate) input_capture: crate::input_capture::InputCapture, - pub(crate) portal_session: crate::portal_session::PortalSession, + pub(crate) loop_handle: LoopHandle<'static, Self>, + pub(crate) input_capture: InputCapture, + pub(crate) portal_session: PortalSession, pub space: Space, pub workspaces: WorkspaceSet, pub output_workspaces: OutputWorkspaceMap, @@ -242,30 +238,30 @@ pub struct Ferese { output_identity_ids: HashMap, pub window_ids: HashMap, pub window_geometry: HashMap, - resize_transactions: HashMap, - pub(crate) resize_snapshots: HashMap, - pub(crate) nested_backend: Option, - pub(crate) wallpaper: crate::wallpaper::WallpaperState, + resize_transactions: HashMap, + pub(crate) resize_snapshots: HashMap, + pub(crate) nested_backend: Option, + pub(crate) wallpaper: WallpaperState, maximized_windows: HashSet, maximized_column_widths: HashMap, - window_stack: crate::stacking::WindowStack, + window_stack: WindowStack, floating_above_fullscreen: HashMap, natural_floating_pending: HashSet, - pub(crate) window_borders: HashMap, - pub(crate) window_dims: HashMap, - pub(crate) window_resize_fills: HashMap, - pub(crate) window_dimming: HashMap, - pub(crate) window_focus: HashMap, - pub(crate) window_shadows: HashMap, - pub(crate) rounded_clip_programs: HashMap, - pub(crate) overview_scrims: HashMap, - pub(crate) material_programs: HashMap, - pub(crate) material_buffers: HashMap, - pub(crate) portal_shortcuts: crate::portal_shortcuts::PortalShortcuts, + pub(crate) window_borders: HashMap, + pub(crate) window_dims: HashMap, + pub(crate) window_resize_fills: HashMap, + pub(crate) window_dimming: HashMap, + pub(crate) window_focus: HashMap, + pub(crate) window_shadows: HashMap, + pub(crate) rounded_clip_programs: HashMap, + pub(crate) overview_scrims: HashMap, + pub(crate) material_programs: HashMap, + pub(crate) material_buffers: HashMap, + pub(crate) portal_shortcuts: PortalShortcuts, pub(crate) pending_logout: Option, pub(crate) logout_query: Option, - pub(crate) logout_owner: Option, - pub(crate) blur_programs: HashMap, + pub(crate) logout_owner: Option, + pub(crate) blur_programs: HashMap, pub(crate) backdrop_generation: u64, closing_windows: HashMap, viewport_animations: HashMap, @@ -286,44 +282,38 @@ pub struct Ferese { spring_config: SpringConfig, viewport_spring_config: SpringConfig, pub(crate) output_profiles: Vec, - pub(crate) autostart: Vec, + pub(crate) autostart: Vec, pub cursor_status: CursorImageStatus, // Cursor callbacks may run with Smithay's pointer mutex held. Rendering // reads the pointer position, so defer it until event dispatch returns. pub(crate) cursor_redraw_pending: bool, + pub(crate) locked_pointer_hint: Option, + pub(crate) last_pointer_time: u32, pub(crate) cursor_theme: xcursor::CursorTheme, - pub(crate) named_cursors: HashMap, - pub intercepted_keys: HashSet, - pub(crate) swipe: crate::gestures::Swipe, + pub(crate) named_cursors: HashMap, + pub intercepted_keys: HashSet, + pub(crate) swipe: Swipe, pub idle_inhibitors: HashMap, pub active_shortcuts_inhibitor: Option, - pub direct_backend: Option, - _ipc_socket: Option, + pub direct_backend: Option, + _ipc_socket: Option, pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>, - pub(crate) pending_screencopies: Vec, + pub(crate) pending_screencopies: Vec, // Screenshot requests outlive the readback that filled them: the // coordinator owns them until encoding finishes and the caller is // answered, or the request is terminated. - pub(crate) screenshot: crate::handlers::screenshot::Coordinator, - pub(crate) screenshot_parts: Option, - pub(crate) screenshot_worker: Option, - pub(crate) shell_resources: Vec< - smithay::reexports::wayland_server::Weak< - ferese_protocols::shell::v1::server::ferese_shell_v1::FereseShellV1, - >, - >, + pub(crate) screenshot: Coordinator, + pub(crate) screenshot_parts: Option, + pub(crate) screenshot_worker: Option, + pub(crate) shell_resources: Vec>, pub(crate) shell_snapshot_serial: u32, - pub(crate) last_shell_snapshot: Option, + pub(crate) last_shell_snapshot: Option, pub(crate) overview: crate::overview::OverviewState, next_window_id: u64, next_output_id: u64, last_animation_tick: Instant, pub popups: PopupManager, - pub(crate) dismissing_popups: Vec<( - WlSurface, - smithay::desktop::PopupKind, - crate::dimming::DimAnimation, - )>, + pub(crate) dismissing_popups: Vec<(WlSurface, PopupKind, DimAnimation)>, pub seat: Seat, pub alpha_modifier_state: AlphaModifierState, pub compositor_state: CompositorState, @@ -393,16 +383,12 @@ 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::< - Self, - _, - >(&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::(&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 { @@ -413,8 +399,7 @@ 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 relative_pointer_state = RelativePointerManagerState::new::(&display_handle); @@ -425,8 +410,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, @@ -472,10 +457,7 @@ impl Ferese { resize_transactions: HashMap::new(), resize_snapshots: HashMap::new(), nested_backend: None, - wallpaper: crate::wallpaper::WallpaperState::with_wakeup( - config.wallpaper, - Some(event_loop.get_signal()), - ), + wallpaper: crate::wallpaper::WallpaperState::with_wakeup(config.wallpaper, Some(event_loop.get_signal())), maximized_windows: HashSet::new(), maximized_column_widths: HashMap::new(), window_stack: crate::stacking::WindowStack::default(), @@ -519,6 +501,8 @@ impl Ferese { autostart: config.autostart, cursor_status: CursorImageStatus::default_named(), cursor_redraw_pending: false, + locked_pointer_hint: None, + last_pointer_time: 0, cursor_theme, named_cursors, intercepted_keys: HashSet::new(), @@ -606,10 +590,7 @@ impl Ferese { ) .map_err(|e| format!("keymap reload failed: {e}"))?; } - 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); } self.lock_idle = config.lock_idle; @@ -644,8 +625,7 @@ impl Ferese { self.gap_config = config.gap_config; self.input_settings = config.input_settings; self.bindings = config.bindings; - 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); self.theme_settings = config.theme_settings; self.column_width_presets = config.column_width_presets; @@ -703,16 +683,9 @@ impl Ferese { .then_some(workspace) }) .expect("numeric workspaces are inexhaustible"); - let geometry = OutputGeometry::new( - geometry.loc.x, - geometry.loc.y, - geometry.size.w, - geometry.size.h, - ); + let geometry = OutputGeometry::new(geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h); - let registration = self - .output_workspaces - .connect(output_id, geometry, fallback_workspace); + let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace); match registration { Ok(workspace) => { @@ -776,9 +749,7 @@ impl Ferese { return None; } match self.workspaces.placement(*id)? { - WindowPlacement::Floating { rect } => { - Some((*id, moved_floating_rect(rect, old, new))) - } + WindowPlacement::Floating { rect } => Some((*id, moved_floating_rect(rect, old, new))), _ => None, } }) @@ -838,9 +809,7 @@ 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; } @@ -900,10 +869,7 @@ impl Ferese { Ok(socket_name) } - 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); } @@ -919,10 +885,7 @@ 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; } @@ -936,10 +899,7 @@ impl Ferese { self.layer_surface_under(position, &[Layer::Bottom, Layer::Background]) } - fn window_surface_under( - &self, - position: Point, - ) -> Option<(WlSurface, Point)> { + fn window_surface_under(&self, position: Point) -> Option<(WlSurface, Point)> { if self.overview.is_active() { return None; } @@ -959,8 +919,7 @@ 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); @@ -1001,8 +960,7 @@ 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; }; @@ -1060,13 +1018,9 @@ 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; @@ -1079,9 +1033,7 @@ impl Ferese { if overview_active { let mut candidates = self.window_ids.keys().cloned().collect::>(); // Match Overview's front-to-back render order during overlapping motion. - candidates.sort_by_key(|window| { - std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0)) - }); + candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0))); candidates.iter().find_map(hit) } else { self.space.elements().rev().find_map(hit) @@ -1090,8 +1042,7 @@ impl Ferese { 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 { @@ -1147,32 +1098,23 @@ impl Ferese { windows.sort_by_key(|window| { let id = self.window_ids.get(window).copied(); let geometry = id.and_then(|id| self.window_geometry.get(&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))) }); if stacking_order_settled(self.space.elements(), &windows, |window| window.z_index()) { return; @@ -1201,10 +1143,7 @@ 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.window_ids.get(window)?; let rect = self.presented_window_rect(*id)?; let size = ClientSize::from_rect(rect); @@ -1256,9 +1195,9 @@ impl Ferese { .unwrap_or_else(|| centered_floating_rect(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; @@ -1327,12 +1266,8 @@ impl Ferese { self.next_window_id += 1; 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; } @@ -1381,37 +1316,22 @@ impl Ferese { continue; }; 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); if let Some(new) = crate::window_rules::live_result(old, new, transient) { self.apply_window_rule_result(&window, new); } } } - fn apply_window_rule_result( - &mut self, - window: &Window, - rule: crate::window_rules::WindowRuleResult, - ) { + fn apply_window_rule_result(&mut self, window: &Window, rule: crate::window_rules::WindowRuleResult) { let Some(id) = self.window_ids.get(window).copied() else { return; }; @@ -1429,9 +1349,7 @@ impl Ferese { .unwrap_or(Axis::Horizontal); if let Some(workspace) = rule.workspace - && let Err(error) = self - .workspaces - .move_window_to_numeric(id, workspace, axis, 0.5) + && let Err(error) = self.workspaces.move_window_to_numeric(id, workspace, axis, 0.5) { tracing::warn!(%error, ?id, workspace, "failed to apply window workspace rule"); } @@ -1441,10 +1359,7 @@ 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) { @@ -1459,9 +1374,7 @@ impl Ferese { rect.y = bounds.y + (bounds.height - rect.height) / 2.0; 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 { @@ -1492,13 +1405,8 @@ impl Ferese { let Some(id) = self.window_ids.get(window).copied() else { return; }; - 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; } let Some(bounds) = self.output_bounds() else { @@ -1666,25 +1574,22 @@ 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; }; - let layout = match workspace.layout.geometry_with_constraints( - bounds, - self.gap_config, - &constraints, - focused, - ) { - Ok(layout) => layout, - Err(error) => { - tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry"); - continue; - } - }; + let layout = + match workspace + .layout + .geometry_with_constraints(bounds, self.gap_config, &constraints, focused) + { + Ok(layout) => layout, + Err(error) => { + tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry"); + continue; + } + }; let viewport_target = workspace.layout.viewport_x(); let viewport_motion = viewport_target.map(|target| { let viewport = self @@ -1719,8 +1624,7 @@ impl Ferese { crate::handlers::set_surface_tree_scale(toplevel.wl_surface(), scale); } - let is_maximized = - self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id); + let is_maximized = self.maximized_windows.contains(id) && workspace_fullscreen != Some(*id); let rect = if workspace_fullscreen == Some(*id) { fullscreen_bounds } else if is_maximized && !is_scrolling_layout { @@ -1734,10 +1638,8 @@ 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); } @@ -1747,20 +1649,16 @@ impl Ferese { } }; let is_fullscreen = workspace_fullscreen == Some(*id); - let is_floating = matches!( - self.workspaces.placement(*id), - Some(WindowPlacement::Floating { .. }) - ); + let is_floating = matches!(self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. })); 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(), @@ -1782,9 +1680,7 @@ impl Ferese { let mut scrolling_world_x = HashMap::new(); 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(); // A viewport-coupled width must never override fullscreen/floating geometry. if scrolling.is_none() { @@ -1792,10 +1688,7 @@ impl Ferese { } let (geometry, had_geometry) = match self.window_geometry.entry(id) { Entry::Occupied(entry) => (entry.into_mut(), true), - Entry::Vacant(entry) => ( - entry.insert(WindowGeometry::new(rect, client_size(&window))), - false, - ), + Entry::Vacant(entry) => (entry.insert(WindowGeometry::new(rect, client_size(&window))), false), }; let mode = if is_fullscreen { PresentationMode::Fullscreen @@ -1817,12 +1710,8 @@ impl Ferese { self.viewport_coupled_widths.remove(&id); } 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) @@ -1840,13 +1729,15 @@ impl Ferese { let coupled = !geometry.is_zooming() && (couple_width - || self.viewport_coupled_widths.get(&id).is_some_and( - |(previous_workspace, _)| *previous_workspace == workspace, - )); + || self + .viewport_coupled_widths + .get(&id) + .is_some_and(|(previous_workspace, _)| *previous_workspace == workspace)); if coupled { - let width = self.viewport_coupled_widths.entry(id).or_insert_with(|| { - (workspace, AnimatedValue::new(geometry.visual.current.width)) - }); + let width = self + .viewport_coupled_widths + .entry(id) + .or_insert_with(|| (workspace, AnimatedValue::new(geometry.visual.current.width))); if width.0 != workspace { *width = (workspace, AnimatedValue::new(geometry.visual.current.width)); } @@ -1861,10 +1752,8 @@ impl Ferese { } } let visual = geometry.visual.current; - let natural_pending = self.natural_floating_pending.contains(&id) - && is_floating - && !is_fullscreen - && !is_maximized; + let natural_pending = + self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized; if natural_pending { requested_size = None; } @@ -1944,13 +1833,7 @@ 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); } @@ -1975,12 +1858,7 @@ impl Ferese { let windows = self .space .elements() - .filter_map(|window| { - self.window_ids - .get(window) - .copied() - .map(|id| (window.clone(), id)) - }) + .filter_map(|window| self.window_ids.get(window).copied().map(|id| (window.clone(), id))) .collect::>(); let mut active_animation = false; let mut completed_slides = Vec::new(); @@ -2029,12 +1907,7 @@ impl Ferese { dim_changed |= previous != focus.current; } for (_, id) in &windows { - let target = crate::dimming::target( - dim_settings, - self.focused_window, - *id, - self.overview.is_presenting(), - ); + let target = crate::dimming::target(dim_settings, self.focused_window, *id, self.overview.is_presenting()); let dim = self .window_dimming .entry(*id) @@ -2105,11 +1978,7 @@ impl Ferese { snapshot.commit.increment(); } if !active { - tracing::debug!( - ?id, - bytes = snapshot.bytes(), - "released resize handoff snapshot" - ); + tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot"); } active }); @@ -2144,16 +2013,12 @@ impl Ferese { }; let zooming = geometry.is_zooming(); - let coupled_target = self - .viewport_coupled_widths - .get(&id) - .map(|(_, width)| width.target); + let coupled_target = self.viewport_coupled_widths.get(&id).map(|(_, width)| width.target); if coupled_target.is_some() { geometry.visual.target.width = geometry.visual.current.width; geometry.visual.velocity.width = 0.0; } - active_animation |= - geometry.advance(delta, self.spring_config, self.animations_enabled); + active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled); if let Some(target) = coupled_target { geometry.visual.target.width = target; } @@ -2206,11 +2071,8 @@ impl Ferese { toplevel.send_pending_configure(); } let visual = geometry.visual.current; - self.space.map_element( - window, - (visual.x.round() as i32, visual.y.round() as i32), - false, - ); + self.space + .map_element(window, (visual.x.round() as i32, visual.y.round() as i32), false); } for id in settled_coupled_widths { self.viewport_coupled_widths.remove(&id); @@ -2235,9 +2097,9 @@ impl Ferese { self.send_shell_snapshots(); active_animation = true; } - active_animation |= - self.overview - .advance(delta, self.spring_config, self.animations_enabled); + active_animation |= self + .overview + .advance(delta, self.spring_config, self.animations_enabled); self.sync_window_stacking(); if active_animation || dim_changed { @@ -2283,9 +2145,7 @@ impl Ferese { else { 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.resize_snapshots.get(&id).is_some_and(|snapshot| { @@ -2298,11 +2158,7 @@ impl Ferese { }) { return; } - let used: usize = self - .resize_snapshots - .values() - .map(|snapshot| snapshot.bytes()) - .sum(); + let used: usize = self.resize_snapshots.values().map(|snapshot| snapshot.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; @@ -2324,11 +2180,7 @@ 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.resize_snapshots.insert(id, snapshot); } Ok(None) => {} @@ -2363,18 +2215,14 @@ impl Ferese { return; }; if self.natural_floating_pending.remove(&id) - && 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 = natural_floating_rect(bounds, size); 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.window_geometry - .insert(id, WindowGeometry::new(rect, Some(size))); + self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size))); self.resize_transactions.remove(&id); self.relayout(); } @@ -2385,18 +2233,15 @@ impl Ferese { // Never let an old buffer undo a newer resize or a fullscreen transition. let settled_configure = window.toplevel().is_some_and(|toplevel| { 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(), - data.current_serial, - data.configure_serial, - data.current.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(), + data.current_serial, + data.configure_serial, + data.current.states.contains(xdg_toplevel::State::Resizing), + ) + }) }) }); let fullscreen = self @@ -2414,15 +2259,9 @@ impl Ferese { && let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) && ClientSize::from_rect(rect) != size { - let rect = Rect::new( - rect.x, - rect.y, - f64::from(size.width), - f64::from(size.height), - ); + let rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height)); if self.workspaces.set_floating_rect(id, rect).is_ok() { - self.window_geometry - .insert(id, WindowGeometry::new(rect, Some(size))); + self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size))); self.resize_transactions.remove(&id); self.resize_snapshots.remove(&id); self.relayout(); @@ -2433,12 +2272,7 @@ 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 focus_direction(&mut self, direction: Direction) { @@ -2489,23 +2323,17 @@ impl Ferese { 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() { @@ -2624,10 +2452,7 @@ 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.pending_column_width_cycles.insert(window); self.relayout(); @@ -2679,10 +2504,7 @@ 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; } @@ -2733,14 +2555,8 @@ impl Ferese { && let WorkspaceLayout::Scrolling(layout) = &mut workspace.layout { if enabled { - if let Some(column) = layout - .columns() - .iter() - .find(|column| column.windows.contains(&window)) - { - self.maximized_column_widths - .entry(window) - .or_insert(column.width); + if let Some(column) = layout.columns().iter().find(|column| column.windows.contains(&window)) { + self.maximized_column_widths.entry(window).or_insert(column.width); } let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0)); } else if let Some(width) = self.maximized_column_widths.remove(&window) { @@ -2864,9 +2680,11 @@ impl Ferese { } fn send_window_close(&self, id: WindowId) { - let Some(toplevel) = self.window_ids.iter().find_map(|(window, window_id)| { - (*window_id == id).then(|| window.toplevel()).flatten() - }) else { + let Some(toplevel) = self + .window_ids + .iter() + .find_map(|(window, window_id)| (*window_id == id).then(|| window.toplevel()).flatten()) + else { return; }; @@ -2987,10 +2805,8 @@ impl Ferese { && self.animations_enabled { self.last_animation_tick = Instant::now(); - self.workspace_slides.insert( - owner, - WorkspaceSlide::new(previous_slide, from, workspace, direction), - ); + self.workspace_slides + .insert(owner, WorkspaceSlide::new(previous_slide, from, workspace, direction)); } } @@ -3003,17 +2819,11 @@ impl Ferese { .and_then(|bounds| { let workspace = self.workspaces.ensure_numeric(index).ok()?; let target = self.workspaces.workspace(workspace)?; - target - .layout - .automatic_axis(target.last_focused, bounds) - .ok() + target.layout.automatic_axis(target.last_focused, bounds).ok() }) .unwrap_or(Axis::Horizontal); - let destination = match self - .workspaces - .move_window_to_numeric(window, index, axis, 0.5) - { + let destination = match self.workspaces.move_window_to_numeric(window, index, axis, 0.5) { Ok(destination) => destination, Err(error) => { tracing::error!(%error, ?window, index, "failed to move window to workspace"); @@ -3048,16 +2858,15 @@ impl Ferese { }) }); - 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(), + ); } 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) } @@ -3212,19 +3021,11 @@ 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), ), ) } @@ -3246,12 +3047,7 @@ fn smoothstep(progress: f64) -> f64 { progress * progress * (3.0 - 2.0 * progress) } -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 { @@ -3287,10 +3083,9 @@ 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) }) } @@ -3355,17 +3150,11 @@ 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.ensure_numeric(2).unwrap(); 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(); @@ -3387,11 +3176,7 @@ 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)); @@ -3406,12 +3191,8 @@ 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 @@ -3422,11 +3203,7 @@ 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))); } @@ -3472,26 +3249,15 @@ 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); @@ -3524,10 +3290,7 @@ 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] @@ -3535,12 +3298,7 @@ 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)); } @@ -3549,8 +3307,7 @@ 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::*; @@ -3635,10 +3392,7 @@ 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)] @@ -3671,11 +3425,7 @@ 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/wallpaper.rs b/compositor/src/wallpaper.rs index 9a5f95c..5e5984e 100644 --- a/compositor/src/wallpaper.rs +++ b/compositor/src/wallpaper.rs @@ -1,28 +1,22 @@ //! One decoded image and one texture per GPU context, not double-buffered //! full-screen UI surfaces per monitor. Resize changes sampling only. -use crate::presentation::NativeTextureElement; +use std::cell::Cell; +use std::collections::HashMap; +use std::path::PathBuf; +use std::rc::Rc; +use std::sync::mpsc; +use std::time::{Duration, Instant}; + use serde::Deserialize; -use smithay::{ - backend::{ - allocator::Fourcc, - renderer::{ - ErasedContextId, ImportMem, Renderer, - element::Id, - gles::{GlesRenderer, GlesTexture}, - utils::CommitCounter, - }, - }, - output::Output, - utils::{Buffer, Physical, Rectangle, Size}, -}; -use std::{ - cell::Cell, - collections::HashMap, - path::PathBuf, - rc::Rc, - sync::mpsc, - time::{Duration, Instant}, -}; +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::element::Id; +use smithay::backend::renderer::gles::{GlesRenderer, GlesTexture}; +use smithay::backend::renderer::utils::CommitCounter; +use smithay::backend::renderer::{ErasedContextId, ImportMem, Renderer}; +use smithay::output::Output; +use smithay::utils::{Buffer, Physical, Rectangle, Size}; + +use crate::presentation::NativeTextureElement; const UPLOAD_RETRY_DELAY: Duration = Duration::from_secs(5); @@ -80,10 +74,7 @@ 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 { let retained = config.clone(); let (sender, receiver) = mpsc::channel(); let owned = config.path.as_ref().is_some_and(|path| path.is_file()); @@ -92,8 +83,7 @@ impl WallpaperState { let wakeup = wakeup.clone(); std::thread::spawn(move || { let load = || -> Result { - let (width, height) = - image::image_dimensions(&path).map_err(|e| e.to_string())?; + let (width, height) = image::image_dimensions(&path).map_err(|e| e.to_string())?; if u64::from(width) * u64::from(height) * 4 > 256 * 1024 * 1024 { return Err("wallpaper exceeds the 256 MiB decode limit".into()); } @@ -164,9 +154,7 @@ 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 { @@ -250,11 +238,7 @@ 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 pixels = self.pixels.as_ref()?; let context = renderer.context_id().erased(); if !self.textures.contains_key(&context) { @@ -265,32 +249,21 @@ impl WallpaperState { match renderer.import_memory(pixels.as_raw(), Fourcc::Abgr8888, size, false) { Ok(texture) => { self.upload_retries.remove(&context); - tracing::debug!( - bytes = pixels.as_raw().len(), - "uploaded shared wallpaper texture" - ); - self.textures.insert( - context.clone(), - WallpaperTexture { - texture, - id: Id::new(), - }, - ); + tracing::debug!(bytes = pixels.as_raw().len(), "uploaded shared wallpaper texture"); + self.textures + .insert(context.clone(), WallpaperTexture { texture, id: Id::new() }); } Err(error) => { // Avoid retrying a large failed allocation every frame, but // allow recovery from temporary GPU memory pressure. - 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"); return None; } } } 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( (pixels.width() as i32, pixels.height() as i32).into(), output_size, @@ -324,11 +297,7 @@ 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(), ), ) @@ -341,10 +310,7 @@ 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()), ) } @@ -373,9 +339,7 @@ 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(), @@ -383,10 +347,7 @@ 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); @@ -398,7 +359,8 @@ mod tests { #[test] fn decoder_wakes_an_idle_event_loop() { - use smithay::reexports::calloop::{EventLoop, timer::Timer}; + use smithay::reexports::calloop::EventLoop; + use smithay::reexports::calloop::timer::Timer; let mut event_loop = EventLoop::::try_new().unwrap(); event_loop .handle() @@ -407,8 +369,7 @@ mod tests { }) .unwrap(); let signal = event_loop.get_signal(); - let mut state = - WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone())); + let mut state = WallpaperState::with_wakeup(WallpaperConfig::default(), Some(signal.clone())); event_loop .run(None, &mut state, |state| { if state.poll() { @@ -429,18 +390,14 @@ 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.send(Ok(image::RgbaImage::new(2, 2))).unwrap(); @@ -489,16 +446,11 @@ 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/window_rules.rs b/compositor/src/window_rules.rs index cc20188..fd45c58 100644 --- a/compositor/src/window_rules.rs +++ b/compositor/src/window_rules.rs @@ -63,12 +63,7 @@ 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 matches!( @@ -88,10 +83,7 @@ pub fn resolve( .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; @@ -109,14 +101,10 @@ pub fn resolve( 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 @@ -157,11 +145,7 @@ 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 { @@ -193,9 +177,7 @@ 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 { @@ -234,15 +216,9 @@ 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] diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index a84d4bf..300a0e5 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -1,84 +1,65 @@ -use std::{ - cell::{Cell, RefCell}, - collections::HashMap, - error::Error, - rc::Rc, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - time::{Duration, Instant}, +use std::cell::{Cell, RefCell}; +use std::collections::HashMap; +use std::error::Error; +use std::rc::Rc; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + +use smithay::backend::allocator::Fourcc; +use smithay::backend::renderer::damage::OutputDamageTracker; +use smithay::backend::renderer::element::memory::MemoryRenderBufferRenderElement; +use smithay::backend::renderer::element::solid::SolidColorRenderElement; +use smithay::backend::renderer::element::surface::{ + WaylandSurfaceRenderElement, WaylandSurfaceTexture, render_elements_from_surface_tree, }; - -use smithay::{ - backend::{ - allocator::Fourcc, - renderer::{ - Bind, Color32F, ErasedContextId, ExportMem, Frame, ImportDma, Offscreen, Renderer, - Texture, - damage::OutputDamageTracker, - element::{ - Element, Id, Kind as RenderElementKind, RenderElement, UnderlyingStorage, - memory::MemoryRenderBufferRenderElement, - render_elements, - solid::SolidColorRenderElement, - surface::{ - WaylandSurfaceRenderElement, WaylandSurfaceTexture, - render_elements_from_surface_tree, - }, - utils::{ - ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, - RelocateRenderElement, RescaleRenderElement, constrain_render_elements, - }, - }, - gles::{ - GlesError, GlesFrame, GlesPixelProgram, GlesRenderer, GlesTexProgram, GlesTexture, - Uniform, UniformName, UniformType, element::PixelShaderElement, - }, - utils::{CommitCounter, DamageSet, OpaqueRegions, draw_render_elements}, - }, - winit::{self, WinitEvent}, - }, - desktop::{ - LayerSurface, PopupManager, layer_map_for_output, - space::{ConstrainBehavior, ConstrainReference, constrain_space_element}, - utils::OutputPresentationFeedback, - }, - input::pointer::{CursorImageStatus, CursorImageSurfaceData}, - output::{Mode, Output, PhysicalProperties, Scale, Subpixel}, - reexports::calloop::{ - EventLoop, - timer::{TimeoutAction, Timer}, - }, - reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind, - reexports::wayland_server::Resource, - utils::{ - Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size, - Transform, - }, - wayland::shell::wlr_layer::Layer, - wayland::{compositor::with_states, presentation::Refresh}, +use smithay::backend::renderer::element::utils::{ + ConstrainAlign, ConstrainScaleBehavior, CropRenderElement, Relocate, RelocateRenderElement, RescaleRenderElement, + constrain_render_elements, }; - -use crate::{ - Ferese, - metrics::{FrameEffectMetrics, RenderMetrics}, - presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect}, +use smithay::backend::renderer::element::{ + Element, Id, Kind as RenderElementKind, RenderElement, RenderElementStates, UnderlyingStorage, render_elements, }; +use smithay::backend::renderer::gles::element::PixelShaderElement; +use smithay::backend::renderer::gles::{ + 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, ImportDma, Offscreen, Renderer, Texture, +}; +use smithay::backend::winit; +use smithay::backend::winit::WinitEvent; +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::{Mode, Output, PhysicalProperties, Scale, Subpixel}; +use smithay::reexports::calloop::EventLoop; +use smithay::reexports::calloop::timer::{TimeoutAction, Timer}; +use smithay::reexports::wayland_protocols::wp::presentation_time::server::wp_presentation_feedback::Kind as PresentationKind; +use smithay::reexports::wayland_server::Resource; +use smithay::utils::{ + Buffer, Clock, Logical, Monotonic, Physical, Point, Rectangle, Scale as RenderScale, Size, Transform, +}; +use smithay::wayland::compositor::with_states; +use smithay::wayland::presentation::Refresh; +use smithay::wayland::shell::wlr_layer::Layer; -type SurfaceRenderElement = CropRenderElement< - RelocateRenderElement>>, ->; +use crate::Ferese; +use crate::metrics::{FrameEffectMetrics, RenderMetrics}; +use crate::presentation::{NativeTextureElement, PhysicalShaderElement, physical_rect}; -type WindowRenderElement = - CropRenderElement>>; +type SurfaceRenderElement = + CropRenderElement>>>; -type MemoryRenderElement = CropRenderElement< - RelocateRenderElement>>, ->; +type WindowRenderElement = CropRenderElement>>; + +type MemoryRenderElement = + CropRenderElement>>>; type PhysicalDamage = Vec>; -type DamageRenderResult = Result<(Option, FrameEffectMetrics), Box>; +type DamageRenderResult = Result<(Option, FrameEffectMetrics, RenderElementStates), Box>; render_elements! { pub(crate) AnimatedWindowRenderElement<=GlesRenderer>; @@ -131,14 +112,10 @@ pub(crate) fn capture_resize_snapshot( } // Called before on_commit_buffer_handler: renderer surface state still // owns the previous buffer. Copy only during an actual resize handoff. - let content = render_elements_from_surface_tree::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + let content = render_elements_from_surface_tree::>( 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, @@ -146,11 +123,7 @@ pub(crate) fn capture_resize_snapshot( if content.is_empty() { return Ok(None); } - let mut texture = Offscreen::::create_buffer( - renderer, - Fourcc::Abgr8888, - (size.w, size.h).into(), - )?; + let mut texture = Offscreen::::create_buffer(renderer, Fourcc::Abgr8888, (size.w, size.h).into())?; { let mut target = renderer.bind(&mut texture)?; let mut frame = renderer.render(&mut target, size, Transform::Normal)?; @@ -186,16 +159,13 @@ pub(crate) fn capture_window_frame( scale: f64, cursor: Option<(&Ferese, Rectangle)>, ) -> Result { - let mut snapshot = - capture_resize_snapshot(renderer, window, geometry, scale, 128 * 1024 * 1024) - .map_err(|error| error.to_string())? - .ok_or("Window has no capturable content")?; + let mut snapshot = capture_resize_snapshot(renderer, window, geometry, scale, 128 * 1024 * 1024) + .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())?; @@ -209,11 +179,7 @@ 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()); @@ -222,9 +188,7 @@ pub(crate) fn capture_window_frame( .checked_mul(size.h as usize) .and_then(|pixels| pixels.checked_mul(4)) .ok_or("Window is too large")?; - 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()); } @@ -547,11 +511,7 @@ 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.commit, commit) } @@ -804,11 +764,7 @@ 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 { if self.clip_changed { return DamageSet::from_slice(&[Rectangle::from_size(self.inner.geometry(scale).size)]); } @@ -856,9 +812,7 @@ impl RenderElement for RoundedSurfaceRenderElement { Uniform::new("radius", self.radius), ], ), - WaylandSurfaceTexture::SolidColor(_) => { - self.inner.draw(frame, src, dst, damage, opaque_regions) - } + WaylandSurfaceTexture::SolidColor(_) => self.inner.draw(frame, src, dst, damage, opaque_regions), } } @@ -943,10 +897,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() refresh.set(Duration::from_nanos(1_000_000_000_000 / rate.max(1) as u64)); output.change_current_state( - Some(Mode { - size, - refresh: rate, - }), + Some(Mode { size, refresh: rate }), None, Some(Scale::Fractional(scale)), None, @@ -973,10 +924,7 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() let (renderer, mut framebuffer) = backend.bind()?; state.process_dmabuf_imports(renderer); let elements = animated_window_elements(state, renderer, &output); - let effects = frame_effect_metrics( - &elements, - output.current_scale().fractional_scale(), - ); + let effects = frame_effect_metrics(&elements, output.current_scale().fractional_scale()); let result = damage_tracker.render_output( renderer, &mut framebuffer, @@ -989,14 +937,8 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() state.process_screencopies(renderer, &framebuffer, &output, true); if cursorless_capture { - let cursorless_elements = - output_elements(state, renderer, &output, false); - redraw_output( - renderer, - &mut framebuffer, - &output, - &cursorless_elements, - )?; + let cursorless_elements = output_elements(state, renderer, &output, false); + redraw_output(renderer, &mut framebuffer, &output, &cursorless_elements)?; state.process_screencopies(renderer, &framebuffer, &output, false); redraw_output(renderer, &mut framebuffer, &output, &elements)?; } else if captured_with_cursor { @@ -1009,14 +951,13 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() .finish()?; } - Ok((result.damage.cloned(), effects)) + Ok((result.damage.cloned(), effects, result.states)) } })(); - let (damage, effects) = match rendered { - Ok((Some(damage), effects)) => (damage, effects), - Ok((None, _)) => { - render_metrics - .record_no_damage(render_started.elapsed(), missed_deadlines); + let (damage, effects, rendered_states) = match rendered { + Ok((Some(damage), effects, states)) => (damage, effects, states), + Ok((None, _, _)) => { + render_metrics.record_no_damage(render_started.elapsed(), missed_deadlines); // A callback means permission to draw the next client // frame, not proof of a new compositor presentation. // No-damage frames must still unblock layer clients. @@ -1049,21 +990,12 @@ pub fn init(event_loop: &mut EventLoop, state: &mut Ferese) -> Result<() } render_metrics.record_frame(elapsed, &damage, missed_deadlines, effects); - let mut presentation = OutputPresentationFeedback::new(&output); - state.space.elements().for_each(|window| { - window.take_presentation_feedback( - &mut presentation, - |_, _| Some(output.clone()), - |_, _| PresentationKind::Vsync, - ); - }); - layer_surfaces(&output).iter().for_each(|layer| { - layer.take_presentation_feedback( - &mut presentation, - |_, _| Some(output.clone()), - |_, _| PresentationKind::Vsync, - ); - }); + let mut presentation = crate::presentation::take_output_feedback( + state, + &output, + &rendered_states, + PresentationKind::Vsync, + ); sequence = sequence.wrapping_add(1); presentation.presented( clock.now(), @@ -1092,7 +1024,12 @@ fn send_nested_frame_callbacks(state: &mut Ferese, output: &Output, refresh: Dur return; } let time = state.start_time.elapsed(); - for window in state.space.elements() { + for window in state.space.elements().filter(|window| { + state + .window_ids + .get(*window) + .is_some_and(|id| state.window_belongs_to_output(*id, output)) + }) { window.send_frame(output, time, Some(refresh), |_, _| Some(output.clone())); } for layer in layer_surfaces(output) { @@ -1117,10 +1054,7 @@ pub(crate) fn cursorless_window_elements( output_elements(state, renderer, output, false) } -pub(crate) fn frame_effect_metrics( - _elements: &[AnimatedWindowRenderElement], - _scale: f64, -) -> FrameEffectMetrics { +pub(crate) fn frame_effect_metrics(_elements: &[AnimatedWindowRenderElement], _scale: f64) -> FrameEffectMetrics { FrameEffectMetrics } @@ -1136,16 +1070,9 @@ fn output_elements( return Vec::new(); }; let scale = output.current_scale().fractional_scale(); - 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 mut elements = Vec::new(); let opacity = state.session_lock.idle_opacity; @@ -1162,14 +1089,8 @@ fn output_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 @@ -1179,10 +1100,7 @@ fn output_elements( .filter(|surface| surface.alive()) { elements.extend( - render_elements_from_surface_tree::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + render_elements_from_surface_tree::>( renderer, surface.wl_surface(), (0, 0), @@ -1195,14 +1113,7 @@ fn output_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; } @@ -1224,13 +1135,7 @@ fn output_elements( }; elements.extend(layer_elements(state, renderer, output, upper_layers)); if state.overview.is_presenting() { - elements.extend(overview_strip_elements( - state, - renderer, - output, - output_geometry, - scale, - )); + elements.extend(overview_strip_elements(state, renderer, output, output_geometry, scale)); } let mut candidates = if state.overview.is_active() { state.window_ids.keys().cloned().collect::>() @@ -1238,9 +1143,7 @@ fn output_elements( state.space.elements().rev().cloned().collect::>() }; if state.overview.is_active() { - candidates.sort_by_key(|window| { - std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0)) - }); + candidates.sort_by_key(|window| std::cmp::Reverse(state.window_ids.get(window).map_or(0, |id| id.0))); } let windows = candidates .iter() @@ -1321,8 +1224,7 @@ fn output_elements( .get(&id) .map_or(if focused { 1.0 } else { 0.0 }, |v| v.current); 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; @@ -1365,8 +1267,7 @@ fn output_elements( && (snapshot.scale - scale).abs() < 0.001 { let size = snapshot.texture.size(); - let visible = - Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels); + let visible = Rectangle::new(pixels.loc, (size.w, size.h).into()).intersection(pixels); if let Some(visible) = visible { let clip = framebuffer_clip_rect( pixels, @@ -1383,16 +1284,12 @@ fn output_elements( f64::from(visible.size.w), f64::from(visible.size.h), ))), - alpha: crate::presentation::handoff_alpha(snapshot.elapsed) - * close_alpha, + alpha: crate::presentation::handoff_alpha(snapshot.elapsed) * close_alpha, program: Some(programs.texture.clone()), uniforms: vec![ Uniform::new("clip_rect", clip).into_owned(), - Uniform::new( - "radius", - scaled_effect_value(window_radius, constrain, scale), - ) - .into_owned(), + Uniform::new("radius", scaled_effect_value(window_radius, constrain, scale)) + .into_owned(), ], } .into(), @@ -1460,11 +1357,7 @@ fn output_elements( elements.push(shadow.into()); } } else { - elements.extend(constrain_space_element::< - GlesRenderer, - _, - AnimatedWindowRenderElement, - >( + elements.extend(constrain_space_element::( renderer, &window, constrain.loc, @@ -1544,9 +1437,7 @@ fn overview_chrome_element( let chrome = &mut state .overview_scrims .entry(output_id) - .or_insert_with(|| OverviewScrim { - chrome: HashMap::new(), - }) + .or_insert_with(|| OverviewScrim { chrome: HashMap::new() }) .chrome; let cached = chrome .entry(key) @@ -1622,9 +1513,7 @@ fn overview_strip_elements( { cache.chrome.retain(|(id, part), _| match part { OverviewChromePart::Caption => windows_by_id.keys().any(|window| window.0 == *id), - OverviewChromePart::Card | OverviewChromePart::Outline => { - cards.iter().any(|card| card.workspace.0 == *id) - } + OverviewChromePart::Card | OverviewChromePart::Outline => cards.iter().any(|card| card.workspace.0 == *id), OverviewChromePart::Strip => true, }); } @@ -1648,9 +1537,7 @@ 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( @@ -1658,9 +1545,7 @@ 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, @@ -1672,11 +1557,9 @@ 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()); } } @@ -1739,20 +1622,16 @@ fn overview_strip_elements( } if let Some((buffer, size)) = state.overview_workspace_label(card.workspace, 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, @@ -1763,8 +1642,7 @@ 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, @@ -1819,10 +1697,7 @@ fn overview_strip_elements( elements } -fn rounded_clip_program( - state: &mut Ferese, - renderer: &mut GlesRenderer, -) -> Option { +fn rounded_clip_program(state: &mut Ferese, renderer: &mut GlesRenderer) -> Option { let context = renderer.context_id().erased(); if let Some(program) = state.rounded_clip_programs.get(&context) { return Some(program.clone()); @@ -1854,9 +1729,7 @@ fn rounded_clip_program( let texture = renderer.compile_custom_texture_shader(ROUNDED_TEXTURE_SHADER, &texture_uniforms); let border = renderer.compile_custom_pixel_shader(ROUNDED_BORDER_SHADER, &border_uniforms); let shadow = renderer.compile_custom_pixel_shader(WINDOW_SHADOW_SHADER, &shadow_uniforms); - let compiled = texture.and_then(|texture| { - border.and_then(|border| shadow.map(|shadow| (texture, border, shadow))) - }); + let compiled = texture.and_then(|texture| border.and_then(|border| shadow.map(|shadow| (texture, border, shadow)))); match compiled { Ok((texture, border, shadow)) => { let programs = RoundedClipPrograms { @@ -1864,9 +1737,7 @@ fn rounded_clip_program( border, shadow, }; - state - .rounded_clip_programs - .insert(context, programs.clone()); + state.rounded_clip_programs.insert(context, programs.clone()); Some(programs) } Err(error) => { @@ -1903,26 +1774,15 @@ fn window_border_element( Some(gradient) => ( gradient.from.0, gradient.to.0, - border_gradient_line( - physical, - mode.size, - output.current_transform().invert(), - gradient.angle, - ), + border_gradient_line(physical, mode.size, output.current_transform().invert(), gradient.angle), ), None => (color, color, [0.0; 4]), }; - let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient - { + let (focus_from, focus_to, focus_gradient_line) = match state.theme_settings.focus_ring_gradient { Some(g) => ( g.from.0, g.to.0, - border_gradient_line( - physical, - mode.size, - output.current_transform().invert(), - g.angle, - ), + border_gradient_line(physical, mode.size, output.current_transform().invert(), g.angle), ), None => ( state.theme_settings.accent_color.0, @@ -2045,20 +1905,17 @@ fn window_tint_element( } else { state.window_dims.entry(id).or_default() }; - let cached = buffers - .contexts - .entry(context) - .or_insert_with(|| CachedBorder { - element: PixelShaderElement::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: PixelShaderElement::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); @@ -2096,22 +1953,14 @@ fn window_shadow_element( } let shadow_geometry = Rectangle::new( - ( - physical.loc.x, - physical.loc.y + (offset_y * scale).round() as i32, - ) - .into(), + (physical.loc.x, physical.loc.y + (offset_y * scale).round() as i32).into(), physical.size, ); let bounds = shadow_bounds(geometry, offset_y, blur); 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: scaled_effect_value(requested_radius, geometry, scale), opacity: opacity as f32, color, @@ -2149,16 +1998,8 @@ 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(), ), }) } @@ -2173,11 +2014,7 @@ fn shadow_uniforms(parameters: &ShadowParameters) -> Vec> { ] } -fn shadow_bounds( - geometry: Rectangle, - offset_y: f64, - blur: f64, -) -> Rectangle { +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; @@ -2218,23 +2055,15 @@ fn rounded_window_elements( let geometry = window.geometry(); let reference = geometry.to_physical_precise_round(scale); 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 = scaled_effect_value(requested_radius, constrain, scale); 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::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + render_elements_from_surface_tree::>( renderer, popup.wl_surface(), location + offset, @@ -2247,10 +2076,7 @@ fn rounded_window_elements( }) .collect::>(); content.extend( - render_elements_from_surface_tree::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + render_elements_from_surface_tree::>( renderer, surface, location, @@ -2310,10 +2136,7 @@ fn color_with_alpha(mut color: [f32; 4], alpha: f32) -> [f32; 4] { color } -fn rounded_visual_rect( - rect: ferese_layout::Rect, - output_location: Point, -) -> Rectangle { +fn rounded_visual_rect(rect: ferese_layout::Rect, output_location: Point) -> Rectangle { let left = (rect.x - f64::from(output_location.x)).round() as i32; let top = (rect.y - f64::from(output_location.y)).round() as i32; let right = (rect.x + rect.width - f64::from(output_location.x)).round() as i32; @@ -2421,17 +2244,14 @@ 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 - .material_buffers - .retain(|surface, _| surface.is_alive()); + state.material_buffers.retain(|surface, _| surface.is_alive()); let mut elements = Vec::new(); for (geometry, layer) in layers { @@ -2479,13 +2299,12 @@ 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 targets: Vec<_> = match ®ions { None => vec![(geometry, radius as f32)], @@ -2505,15 +2324,11 @@ fn append_material_surface( .iter() .enumerate() .filter_map(|(index, (rect, radius))| { - material_element( - state, renderer, output, surface, *rect, *radius, index, geometry, 1.0, - ) + material_element(state, renderer, output, surface, *rect, *radius, index, geometry, 1.0) }) .collect(); if let Some(buffers) = state.material_buffers.get_mut(surface) { - buffers - .contexts - .retain(|(_, index), _| *index < targets.len()); + buffers.contexts.retain(|(_, index), _| *index < targets.len()); } let material = (!materials.is_empty() && regions.is_none()).then_some(()); let clip = material.as_ref().and_then(|_| { @@ -2530,10 +2345,7 @@ fn append_material_surface( )) }); // Smithay element lists are front to back: content, material, shadow. - let content = render_elements_from_surface_tree::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + let content = render_elements_from_surface_tree::>( renderer, surface, content_origin.to_physical_precise_round(scale), @@ -2588,8 +2400,7 @@ fn material_element( let [red, green, blue, _] = state.theme_settings.surface_base_color.0; let radius = radius.min(geometry.size.w.min(geometry.size.h) as f32 * 0.5); let [offset_y, shadow_blur, shadow_opacity] = material.shadow; - let edge_bar = - role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0; + let edge_bar = role == crate::effects::SemanticRole::Panel && radius == 0.0 && geometry.loc.y == 0; let shadow_opacity = if edge_bar { 0.0 } else { @@ -2600,11 +2411,7 @@ fn material_element( geometry.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 output_size = output .current_transform() .transform_size(mode.size) @@ -2617,37 +2424,23 @@ fn material_element( presentation_alpha, background_opacity, blur: (blur * scale) as f32, - scene_generation: if blur > 0.0 { - state.backdrop_generation - } else { - 0 - }, + scene_generation: if blur > 0.0 { state.backdrop_generation } else { 0 }, sample_geometry, sample_physical, sample_framebuffer: framebuffer_clip_rect(sample_physical, mode.size, transform), radius: radius * scale as f32, - shadow_rect: framebuffer_clip_rect( - shadow_geometry.to_physical_precise_round(scale), - mode.size, - transform, - ), + shadow_rect: framebuffer_clip_rect(shadow_geometry.to_physical_precise_round(scale), mode.size, transform), shadow_values: [(shadow_blur * scale) as f32, shadow_opacity as f32], shadow_bounds: shadow_bounds(geometry, offset_y, shadow_blur), geometry, tint: [red, green, blue, material.opacity * presentation_alpha], generation, opaque: material.opacity == 1.0 && presentation_alpha == 1.0 && radius == 0.0, - visible_framebuffer: framebuffer_clip_rect( - geometry.to_physical_precise_round(scale), - mode.size, - transform, - ), + visible_framebuffer: framebuffer_clip_rect(geometry.to_physical_precise_round(scale), mode.size, transform), }; let context = renderer.context_id().erased(); let program = material_program(state, renderer)?; - let blur_program = (blur > 0.0) - .then(|| blur_program(state, renderer)) - .flatten(); + let blur_program = (blur > 0.0).then(|| blur_program(state, renderer)).flatten(); let capture_rect = framebuffer_capture_rect(parameters.sample_framebuffer); let capture_size = Size::from((capture_rect[2], capture_rect[3])); let buffers = state.material_buffers.entry(surface.clone()).or_default(); @@ -2657,16 +2450,12 @@ fn material_element( .get(&context) .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( context.clone(), @@ -2704,9 +2493,7 @@ fn material_element( MaterialElement::Fill(PixelShaderElement::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, @@ -2739,16 +2526,12 @@ fn material_element( } if cached.parameters != parameters { 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)); } @@ -2806,16 +2589,8 @@ fn blur_uniforms(p: &MaterialParameters) -> Vec> { vec![ Uniform::new("visible_rect", p.visible_framebuffer).into_owned(), Uniform::new("material_radius", p.radius).into_owned(), - Uniform::new( - "texture_size", - [p.sample_framebuffer[2], p.sample_framebuffer[3]], - ) - .into_owned(), - Uniform::new( - "capture_origin", - [p.sample_framebuffer[0], p.sample_framebuffer[1]], - ) - .into_owned(), + Uniform::new("texture_size", [p.sample_framebuffer[2], p.sample_framebuffer[3]]).into_owned(), + Uniform::new("capture_origin", [p.sample_framebuffer[0], p.sample_framebuffer[1]]).into_owned(), Uniform::new("blur_radius", p.blur).into_owned(), Uniform::new("presentation_alpha", p.presentation_alpha).into_owned(), Uniform::new("background_opacity", p.background_opacity).into_owned(), @@ -2896,8 +2671,7 @@ 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 { @@ -2909,13 +2683,9 @@ 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::< - GlesRenderer, - WaylandSurfaceRenderElement, - >( + render_elements_from_surface_tree::>( renderer, surface, physical_location, @@ -2926,8 +2696,7 @@ 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) }) @@ -2963,45 +2732,30 @@ fn cursor_elements( } 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)] mod tests { - use smithay::{ - backend::renderer::{ - damage::OutputDamageTracker, - element::{Element, Id}, - utils::CommitCounter, - }, - utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}, - }; + use smithay::backend::renderer::damage::OutputDamageTracker; + use smithay::backend::renderer::element::{Element, Id}; + use smithay::backend::renderer::utils::CommitCounter; + use smithay::utils::{Buffer, Logical, Physical, Rectangle, Scale, Transform}; use super::{ - border_gradient_line, color_with_alpha, framebuffer_clip_rect, normalized_scale, - resize_content_behavior, rounded_visual_rect, scaled_visual_rect, shadow_bounds, + border_gradient_line, color_with_alpha, framebuffer_clip_rect, normalized_scale, resize_content_behavior, + rounded_visual_rect, scaled_visual_rect, shadow_bounds, }; #[test] #[ignore = "requires an EGL rendering device"] fn translucent_blur_does_not_leak_the_sharp_backdrop() { - use smithay::backend::{ - allocator::Fourcc, - egl::{EGLContext, EGLDevice, EGLDisplay}, - renderer::{ - Bind, Color32F, ExportMem, Frame, ImportMem, Offscreen, Renderer, - gles::{GlesRenderer, GlesTexture, Uniform}, - }, - }; + 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}; - 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() }; @@ -3013,11 +2767,8 @@ mod tests { for pixel in pixels.chunks_exact_mut(4) { 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 in [0.2f32, 0.71, 1.0] { @@ -3036,9 +2787,7 @@ 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()), @@ -3095,8 +2844,7 @@ 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)), @@ -3109,14 +2857,8 @@ 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}"); } } } @@ -3165,12 +2907,7 @@ 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 { @@ -3194,9 +2931,7 @@ 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()); @@ -3279,18 +3014,14 @@ 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. @@ -3311,10 +3042,7 @@ 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}" @@ -3343,17 +3071,11 @@ 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)))); @@ -3370,22 +3092,13 @@ 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] @@ -3401,10 +3114,7 @@ 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] @@ -3418,10 +3128,7 @@ 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/crates/ferese-animation/src/lib.rs b/crates/ferese-animation/src/lib.rs index 477dd6c..387dd38 100644 --- a/crates/ferese-animation/src/lib.rs +++ b/crates/ferese-animation/src/lib.rs @@ -108,13 +108,7 @@ impl AnimatedValue { for _ in 0..steps { let previous_error = self.current - self.target; - advance_value( - &mut self.current, - self.target, - &mut self.velocity, - step, - config, - ); + advance_value(&mut self.current, self.target, &mut self.velocity, step, config); let current_error = self.current - self.target; if previous_error != 0.0 && previous_error.signum() != current_error.signum() { self.snap(); @@ -163,20 +157,8 @@ impl AnimatedRect { let step = seconds / steps as f64; for _ in 0..steps { - advance_value( - &mut self.current.x, - self.target.x, - &mut self.velocity.x, - step, - config, - ); - advance_value( - &mut self.current.y, - self.target.y, - &mut self.velocity.y, - step, - config, - ); + advance_value(&mut self.current.x, self.target.x, &mut self.velocity.x, step, config); + advance_value(&mut self.current.y, self.target.y, &mut self.velocity.y, step, config); advance_value( &mut self.current.width, self.target.width, @@ -321,12 +303,7 @@ 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 { @@ -338,12 +315,7 @@ 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); if self.mode != mode || (self.zoom.is_some() && self.logical != rect) { let mut progress = AnimatedValue::new(0.0); @@ -359,14 +331,9 @@ impl WindowGeometry { let speed = [velocity.x, velocity.y, velocity.width, velocity.height]; let squared_length = distance.iter().map(|value| value * value).sum::(); if squared_length > 0.001 { - let projected = - distance.iter().zip(speed).map(|(d, v)| d * v).sum::() / squared_length; + let projected = distance.iter().zip(speed).map(|(d, v)| d * v).sum::() / squared_length; // Keep forward momentum; soften a reversal without jumping position. - progress.velocity = if projected < 0.0 { - projected * 0.35 - } else { - projected - }; + progress.velocity = if projected < 0.0 { projected * 0.35 } else { projected }; } self.zoom = Some(ZoomTransition { from: self.visual.current, @@ -412,24 +379,14 @@ 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 }; @@ -506,15 +463,8 @@ impl WindowGeometry { } } -fn advance_value( - current: &mut f64, - target: f64, - velocity: &mut f64, - delta: f64, - config: SpringConfig, -) { - let acceleration = - (-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass; +fn advance_value(current: &mut f64, target: f64, velocity: &mut f64, delta: f64, config: SpringConfig) { + let acceleration = (-config.stiffness * (*current - target) - config.damping * *velocity) / config.mass; *velocity += acceleration * delta; let previous_error = *current - target; *current += *velocity * delta; @@ -641,17 +591,10 @@ 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); } } @@ -678,9 +621,7 @@ 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); @@ -690,11 +631,7 @@ 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!( @@ -721,9 +658,7 @@ 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); } } @@ -734,11 +669,7 @@ 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); } @@ -782,24 +713,15 @@ 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.presentation_size_request(Duration::from_millis(64)), - None - ); - assert_eq!( - geometry.set_presentation_mode( - end, - PresentationMode::Normal, - Duration::from_millis(64) - ), + 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); } @@ -844,13 +766,8 @@ 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); } @@ -1004,11 +921,7 @@ 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] @@ -1067,10 +980,7 @@ 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-config/src/lib.rs b/crates/ferese-config/src/lib.rs index 74bfb9c..dfcf406 100644 --- a/crates/ferese-config/src/lib.rs +++ b/crates/ferese-config/src/lib.rs @@ -1,7 +1,9 @@ +use std::fmt; +use std::path::PathBuf; + use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use serde::de::DeserializeOwned; use serde_json::{Map, Value}; -use std::{fmt, path::PathBuf}; pub const DEFAULT_MATERIAL_OPACITY: f64 = 0.78; @@ -36,14 +38,9 @@ pub fn default_wallpaper() -> &'static str { .ok() .and_then(|path| path.parent().map(|dir| dir.join("wallpapers/ferese.png"))) .filter(|path| path.is_file()) - .unwrap_or_else(|| { - PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png") - }); + .unwrap_or_else(|| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../assets/wallpapers/ferese.png")); - path.canonicalize() - .unwrap_or(path) - .to_string_lossy() - .into_owned() + path.canonicalize().unwrap_or(path).to_string_lossy().into_owned() }) } @@ -65,10 +62,8 @@ fn field(name: &str, parent: &str) -> String { fn is_records(key: &str, parent: &str) -> bool { matches!( (parent, key), - ( - "", - "bindings" | "window_rules" | "output_profiles" | "autostart" - ) | ("output_profiles", "outputs") + ("", "bindings" | "window_rules" | "output_profiles" | "autostart") + | ("output_profiles", "outputs") | ("desktop_widgets", "notes") ) } @@ -77,12 +72,7 @@ fn is_array(key: &str, parent: &str) -> bool { parent == "commands" || matches!( key, - "command" - | "position" - | "width_presets" - | "xkb_options" - | "settings_command" - | "outputs" + "command" | "position" | "width_presets" | "xkb_options" | "settings_command" | "outputs" ) } @@ -91,9 +81,7 @@ 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) { @@ -124,9 +112,7 @@ fn object(doc: &KdlDocument, parent: &str) -> Result, Error> let key = field(node.name().value(), parent); let value = 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)?; @@ -138,9 +124,7 @@ 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 @@ -156,11 +140,7 @@ 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())?)?; } @@ -195,11 +175,7 @@ fn node_value(node: &KdlNode, key: &str, parent: &str) -> Result { node.name() ))); } - 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())?)?; } @@ -224,9 +200,7 @@ 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 }) @@ -267,10 +241,7 @@ 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())); @@ -278,18 +249,12 @@ 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; @@ -302,19 +267,14 @@ 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()))?; @@ -346,11 +306,7 @@ 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()) @@ -391,11 +347,7 @@ 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; } @@ -403,11 +355,7 @@ 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); } @@ -500,9 +448,7 @@ 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 @@ -613,15 +559,9 @@ 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)?; } @@ -643,10 +583,7 @@ 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 { @@ -677,10 +614,7 @@ desktop-widgets { } "#; let mut doc = Document::parse(source).unwrap(); - assert_eq!( - doc.get("commands.my_command").unwrap(), - &serde_json::json!(["program"]) - ); + assert_eq!(doc.get("commands.my_command").unwrap(), &serde_json::json!(["program"])); assert_eq!( doc.get("desktop_widgets.clock.outputs").unwrap(), &serde_json::json!(["DP-1"]) @@ -691,8 +625,7 @@ desktop-widgets { ); doc.set("commands.new_command", serde_json::json!(["two", "words"])) .unwrap(); - doc.set("desktop_widgets.clock.outputs", serde_json::json!([])) - .unwrap(); + doc.set("desktop_widgets.clock.outputs", serde_json::json!([])).unwrap(); let reparsed = Document::parse(&doc.to_string()).unwrap(); assert_eq!( reparsed.get("commands.new_command").unwrap(), @@ -706,8 +639,7 @@ desktop-widgets { #[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!( @@ -750,15 +682,11 @@ 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] @@ -786,28 +714,17 @@ desktop-widgets { } "#; let mut doc = Document::parse(source).unwrap(); - doc.set("desktop_widgets.notes.0.text", "one\ntwo".into()) - .unwrap(); + doc.set("desktop_widgets.notes.0.text", "one\ntwo".into()).unwrap(); assert!(doc.to_string().contains("// personal comment")); let reparsed = Document::parse(&doc.to_string()).unwrap(); assert_eq!( reparsed.get("commands.screenshot-full").unwrap(), &serde_json::json!(["ferese-screenshot", "--full"]) ); - assert_eq!( - reparsed.get("desktop_widgets.notes.0.text").unwrap(), - "one\ntwo" - ); + assert_eq!(reparsed.get("desktop_widgets.notes.0.text").unwrap(), "one\ntwo"); doc.add("desktop_widgets.notes", vec![("id".into(), "b".into())]) .unwrap(); - assert_eq!( - doc.get("desktop_widgets.notes") - .unwrap() - .as_array() - .unwrap() - .len(), - 2 - ); + assert_eq!(doc.get("desktop_widgets.notes").unwrap().as_array().unwrap().len(), 2); doc.remove("desktop_widgets.notes", 0).unwrap(); assert_eq!(doc.get("desktop_widgets.notes.0.id").unwrap(), "b"); } diff --git a/crates/ferese-core/src/desktop.rs b/crates/ferese-core/src/desktop.rs index de3f2c4..08bab6e 100644 --- a/crates/ferese-core/src/desktop.rs +++ b/crates/ferese-core/src/desktop.rs @@ -81,9 +81,7 @@ impl Default for StickyNote { impl StickyNote { pub fn on_output(&self, name: Option<&str>) -> bool { - self.enabled - && (self.outputs.is_empty() - || name.is_some_and(|name| self.outputs.iter().any(|s| s == name))) + self.enabled && (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|s| s == name))) } /// Text/style changes repaint in place, never remap the desktop card. pub fn same_surface(&self, other: &Self) -> bool { @@ -99,12 +97,7 @@ impl StickyNote { } pub fn validate(&self) -> Result<(), String> { - let invalid = |field: &str| { - Err(format!( - "desktop_widgets.notes[{}].{field} is invalid", - self.id - )) - }; + let invalid = |field: &str| Err(format!("desktop_widgets.notes[{}].{field} is invalid", self.id)); if self.id.is_empty() || self.id.len() > 64 || !self @@ -138,9 +131,9 @@ impl StickyNote { for color in [&self.color, &self.background].into_iter().flatten() { if !color.is_empty() && color != "theme" - && !color.strip_prefix('#').is_some_and(|hex| { - matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()) - }) + && !color + .strip_prefix('#') + .is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())) { return invalid("color/background"); } @@ -253,8 +246,7 @@ impl Default for Clock { impl Clock { pub fn on_output(&self, name: Option<&str>) -> bool { self.enabled - && (self.outputs.is_empty() - || name.is_some_and(|name| self.outputs.iter().any(|output| output == name))) + && (self.outputs.is_empty() || name.is_some_and(|name| self.outputs.iter().any(|output| output == name))) } pub fn labels(&self, now: &jiff::Zoned) -> Result<(String, String), String> { @@ -264,10 +256,8 @@ impl Clock { } _ => now.clone(), }; - let time = - jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?; - let date = - jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?; + let time = jiff::fmt::strtime::format(&self.time_format, &zoned).map_err(|e| e.to_string())?; + let date = jiff::fmt::strtime::format(&self.date_format, &zoned).map_err(|e| e.to_string())?; Ok(if self.lowercase { (time.to_lowercase(), date.to_lowercase()) } else { @@ -295,16 +285,14 @@ impl Clock { return invalid(name); } } - for color in [&self.color, &self.date_color, &self.background] - .into_iter() - .flatten() - { + for color in [&self.color, &self.date_color, &self.background].into_iter().flatten() { if color.is_empty() { continue; } - if !color.strip_prefix('#').is_some_and(|hex| { - matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit()) - }) { + if !color + .strip_prefix('#') + .is_some_and(|hex| matches!(hex.len(), 6 | 8) && hex.bytes().all(|b| b.is_ascii_hexdigit())) + { return invalid("color"); } } @@ -414,9 +402,6 @@ mod tests { date_format: "%-d %b".into(), ..Clock::default() }; - assert_eq!( - clock.labels(&now).unwrap(), - ("10:05 pm".into(), "26 sep".into()) - ); + assert_eq!(clock.labels(&now).unwrap(), ("10:05 pm".into(), "26 sep".into())); } } diff --git a/crates/ferese-core/src/lib.rs b/crates/ferese-core/src/lib.rs index bab96a0..26cd524 100644 --- a/crates/ferese-core/src/lib.rs +++ b/crates/ferese-core/src/lib.rs @@ -5,6 +5,6 @@ mod workspace; pub use output::{OutputError, OutputGeometry, OutputId, OutputWorkspaceMap, WorkspaceSwitch}; pub use workspace::{ - LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, - WorkspaceSet, workspace_order_key, + LayoutMode, WindowPlacement, Workspace, WorkspaceError, WorkspaceId, WorkspaceLayout, WorkspaceSet, + workspace_order_key, }; diff --git a/crates/ferese-core/src/output.rs b/crates/ferese-core/src/output.rs index b7860ea..9aec84c 100644 --- a/crates/ferese-core/src/output.rs +++ b/crates/ferese-core/src/output.rs @@ -1,8 +1,6 @@ -use std::{ - collections::{HashMap, HashSet}, - error::Error, - fmt, -}; +use std::collections::{HashMap, HashSet}; +use std::error::Error; +use std::fmt; use crate::WorkspaceId; @@ -19,12 +17,7 @@ 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) { @@ -39,10 +32,7 @@ impl OutputGeometry { pub enum OutputError { AlreadyConnected(OutputId), UnknownOutput(OutputId), - WorkspaceAlreadyAssigned { - workspace: WorkspaceId, - output: OutputId, - }, + WorkspaceAlreadyAssigned { workspace: WorkspaceId, output: OutputId }, } impl fmt::Display for OutputError { @@ -51,10 +41,7 @@ impl fmt::Display for OutputError { Self::AlreadyConnected(output) => write!(formatter, "output {output:?} is connected"), Self::UnknownOutput(output) => write!(formatter, "unknown output {output:?}"), Self::WorkspaceAlreadyAssigned { workspace, output } => { - write!( - formatter, - "workspace {workspace:?} is assigned to {output:?}" - ) + write!(formatter, "workspace {workspace:?} is assigned to {output:?}") } } } @@ -111,11 +98,7 @@ 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) @@ -125,9 +108,7 @@ impl OutputWorkspaceMap { } self.revisions.remove(&workspace); 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; @@ -165,8 +146,7 @@ impl OutputWorkspaceMap { let mut reclaimed = Vec::new(); if let Some(record) = &record { for evacuated in &record.workspaces { - let assignment_matches = - self.assignments.get(&evacuated.workspace).copied() == evacuated.target; + let assignment_matches = self.assignments.get(&evacuated.workspace).copied() == evacuated.target; let revision_matches = self.revision(evacuated.workspace) == evacuated.revision; if assignment_matches && revision_matches { @@ -193,12 +173,7 @@ 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 active = match active { Some(active) => active, None => { @@ -229,10 +204,7 @@ 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.migration_target(output, removed.geometry); let removed_had_focus = self.focused == Some(output); let mut evacuated = removed.workspaces.into_iter().collect::>(); @@ -309,10 +281,7 @@ 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.active = workspace; self.focused = Some(output); @@ -324,11 +293,7 @@ impl OutputWorkspaceMap { Ok(WorkspaceSwitch::Activated(output)) } - 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)); } @@ -348,11 +313,7 @@ impl OutputWorkspaceMap { Ok(output) } - 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) @@ -382,10 +343,7 @@ 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; } @@ -423,9 +381,7 @@ 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()); @@ -462,47 +418,28 @@ 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)))); @@ -513,12 +450,8 @@ 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(); @@ -526,29 +459,20 @@ 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))); @@ -557,37 +481,24 @@ 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) @@ -596,14 +507,8 @@ 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(); @@ -615,14 +520,8 @@ 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))); } @@ -631,12 +530,8 @@ 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)), @@ -648,18 +543,10 @@ 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))); } } diff --git a/crates/ferese-core/src/workspace.rs b/crates/ferese-core/src/workspace.rs index 75056a4..bf4f8d3 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, PartialEq)] @@ -44,11 +44,7 @@ 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); @@ -161,17 +157,11 @@ impl WorkspaceLayout { constraints, focused.filter(|window| layout.contains(*window)), ), - Self::Tree(layout) => { - layout.geometry_with_constraints(bounds, gaps, constraints, focused) - } + Self::Tree(layout) => layout.geometry_with_constraints(bounds, gaps, constraints, focused), } } - 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), @@ -190,24 +180,14 @@ 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), @@ -229,11 +209,7 @@ 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), @@ -280,11 +256,7 @@ 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); } @@ -444,12 +416,7 @@ 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; @@ -470,11 +437,7 @@ impl WorkspaceSet { let workspace = Workspace { id: active, name: "1".to_owned(), - 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, @@ -520,12 +483,8 @@ 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)) } @@ -543,8 +502,7 @@ impl WorkspaceSet { pub fn ordered(&self) -> Vec<&Workspace> { let mut workspaces = self.iter().collect::>(); - workspaces - .sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id)); + workspaces.sort_unstable_by_key(|workspace| workspace_order_key(&workspace.name, workspace.id)); workspaces } @@ -552,20 +510,12 @@ impl WorkspaceSet { let mut empty = self .workspaces .values() - .filter(|workspace| { - workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty() - }) + .filter(|workspace| workspace.layout.window_ids().next().is_none() && workspace.floating.is_empty()) .map(|workspace| workspace.id) .collect::>(); empty.sort_by_key(|id| id.0); - let visible_empty = empty - .iter() - .any(|id| *id == self.active || protected.contains(id)); - let spare = if visible_empty { - None - } else { - empty.first().copied() - }; + let visible_empty = empty.iter().any(|id| *id == self.active || protected.contains(id)); + let spare = if visible_empty { None } else { empty.first().copied() }; let mut removed = Vec::new(); for id in empty { if id == self.active || protected.contains(&id) || Some(id) == spare { @@ -588,32 +538,20 @@ 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 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)?; debug_assert!(self.validate().is_ok()); 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) @@ -639,35 +577,23 @@ 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); @@ -676,11 +602,7 @@ 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))?; @@ -721,11 +643,7 @@ 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) { @@ -734,10 +652,7 @@ 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) } @@ -804,12 +719,7 @@ 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)); } @@ -1004,11 +914,7 @@ 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))?; @@ -1038,18 +944,14 @@ impl WorkspaceSet { return Err(WorkspaceError::InvalidState("active workspace is missing")); } if self.names.len() != self.workspaces.len() { - return Err(WorkspaceError::InvalidState( - "workspace name index is inconsistent", - )); + return Err(WorkspaceError::InvalidState("workspace name index is inconsistent")); } let mut seen_windows = HashSet::new(); for (id, workspace) in &self.workspaces { if workspace.id != *id || self.names.get(&workspace.name) != Some(id) { - return Err(WorkspaceError::InvalidState( - "workspace index is inconsistent", - )); + return Err(WorkspaceError::InvalidState("workspace index is inconsistent")); } workspace.layout.validate()?; @@ -1059,9 +961,7 @@ 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")); } } @@ -1071,10 +971,7 @@ 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", @@ -1086,9 +983,7 @@ 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 @@ -1101,12 +996,8 @@ 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(()) @@ -1114,13 +1005,10 @@ 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 { @@ -1151,10 +1039,7 @@ mod tests { let original_first = set.active_id(); let second = set.ensure_numeric(2).unwrap(); set.activate(second).unwrap(); - assert_eq!( - set.prune_empty(&std::collections::HashSet::new()), - vec![original_first] - ); + assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![original_first]); let tenth = set.ensure_numeric(10).unwrap(); let first = set.ensure_numeric(1).unwrap(); @@ -1188,19 +1073,13 @@ mod tests { .unwrap(); let spare = set.ensure_numeric(3).unwrap(); let excess = set.ensure_numeric(4).unwrap(); - assert_eq!( - set.prune_empty(&std::collections::HashSet::new()), - vec![excess] - ); + assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![excess]); assert!(set.workspace(first).is_some()); assert!(set.workspace(floating).is_some()); assert!(set.workspace(spare).is_some()); set.activate(spare).unwrap(); set.remove_window(super::WindowId(1)).unwrap(); - assert_eq!( - set.prune_empty(&std::collections::HashSet::new()), - vec![first] - ); + assert_eq!(set.prune_empty(&std::collections::HashSet::new()), vec![first]); assert!(set.validate().is_ok()); assert_ne!(set.ensure_numeric(1).unwrap(), first); } @@ -1222,9 +1101,7 @@ 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!() @@ -1263,30 +1140,18 @@ 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_numeric(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::*; @@ -1304,14 +1169,9 @@ 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 @@ -1323,10 +1183,7 @@ 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_numeric(2).unwrap(); assert_eq!(workspaces.active().layout.mode(), LayoutMode::Scrolling); } @@ -1334,14 +1191,10 @@ 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_numeric(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_numeric(1).unwrap(), Some(WindowId(1))); assert_eq!(workspaces.switch_to_numeric(2).unwrap(), Some(WindowId(2))); @@ -1351,12 +1204,8 @@ 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_numeric(WindowId(2), 2, Axis::Horizontal, 0.5) @@ -1364,32 +1213,16 @@ 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))); @@ -1400,9 +1233,7 @@ 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(); @@ -1414,9 +1245,7 @@ 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_numeric(WindowId(1), 2, Axis::Horizontal, 0.5) .unwrap(); @@ -1433,9 +1262,7 @@ 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!( @@ -1461,21 +1288,14 @@ 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)) @@ -1497,15 +1317,9 @@ 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 { @@ -1555,20 +1369,13 @@ 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 - .geometry_with_constraints( - bounds, - GapConfig::default(), - &HashMap::new(), - Some(WindowId(4)), - ) + .geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4))) .unwrap(); let scroll = workspaces.active().layout.viewport_x(); workspaces @@ -1582,12 +1389,7 @@ mod tests { let after = workspaces .active_mut() .layout - .geometry_with_constraints( - bounds, - GapConfig::default(), - &HashMap::new(), - Some(WindowId(5)), - ) + .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()); @@ -1598,9 +1400,7 @@ 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_numeric(2).unwrap(); workspaces @@ -1612,10 +1412,7 @@ 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)) @@ -1626,9 +1423,7 @@ 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()); @@ -1646,13 +1441,9 @@ 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))); @@ -1664,17 +1455,10 @@ 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))); @@ -1688,14 +1472,10 @@ 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_numeric(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_numeric(WindowId(2), 1, Axis::Horizontal, 0.5) .unwrap(); @@ -1704,9 +1484,7 @@ 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()); } @@ -1714,12 +1492,8 @@ 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))); @@ -1740,26 +1514,16 @@ 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)]) ); @@ -1777,19 +1541,14 @@ 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); @@ -1817,9 +1576,7 @@ 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() => { @@ -1866,8 +1623,7 @@ 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(); } } diff --git a/crates/ferese-ipc/src/lib.rs b/crates/ferese-ipc/src/lib.rs index ad829df..03b1dd8 100644 --- a/crates/ferese-ipc/src/lib.rs +++ b/crates/ferese-ipc/src/lib.rs @@ -1,6 +1,6 @@ use std::error::Error; -use std::fmt; -use std::io::{self, Read, Write}; +use std::io::{Read, Write}; +use std::{fmt, io}; use serde::{Deserialize, Serialize}; use serde_json::Value; @@ -69,10 +69,7 @@ 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-layout/src/lib.rs b/crates/ferese-layout/src/lib.rs index 9f125ea..888ff99 100644 --- a/crates/ferese-layout/src/lib.rs +++ b/crates/ferese-layout/src/lib.rs @@ -54,12 +54,7 @@ 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) { @@ -365,11 +360,7 @@ 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); @@ -434,11 +425,7 @@ 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); }; @@ -516,12 +503,7 @@ 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); }; @@ -545,12 +527,7 @@ 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) @@ -651,9 +628,7 @@ 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")) @@ -662,12 +637,7 @@ 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, @@ -684,12 +654,7 @@ 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")), @@ -786,21 +751,11 @@ 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 @@ -826,10 +781,7 @@ 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)?; @@ -860,9 +812,7 @@ 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, } } @@ -887,15 +837,9 @@ 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")); } @@ -931,11 +875,7 @@ 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 { @@ -1029,12 +969,7 @@ 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 } @@ -1043,24 +978,14 @@ 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) } @@ -1074,43 +999,32 @@ 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(); @@ -1125,8 +1039,7 @@ 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), @@ -1147,28 +1060,20 @@ 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(); @@ -1186,8 +1091,7 @@ 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 @@ -1206,8 +1110,7 @@ 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()) @@ -1219,8 +1122,7 @@ 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) @@ -1233,13 +1135,11 @@ 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 ); } @@ -1247,16 +1147,12 @@ 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); @@ -1267,16 +1163,11 @@ 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() ); } @@ -1284,18 +1175,14 @@ 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); @@ -1307,23 +1194,15 @@ 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); @@ -1333,26 +1212,16 @@ 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(); @@ -1366,8 +1235,7 @@ 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(); @@ -1382,8 +1250,7 @@ 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(); @@ -1393,10 +1260,7 @@ 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()); } @@ -1409,9 +1273,7 @@ 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 { @@ -1425,8 +1287,7 @@ 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() => { @@ -1453,8 +1314,7 @@ 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(); } _ => {} } @@ -1466,8 +1326,7 @@ 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([ @@ -1507,8 +1366,7 @@ 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([ @@ -1550,8 +1408,7 @@ 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 { @@ -1569,10 +1426,7 @@ 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); } diff --git a/crates/ferese-layout/src/scrolling.rs b/crates/ferese-layout/src/scrolling.rs index 4e3d681..afe856d 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; @@ -145,16 +145,10 @@ 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)); } @@ -163,8 +157,7 @@ impl ScrollingLayout { .and_then(|focused| self.window_location(focused)) .map(|(column, _)| column + 1) .unwrap_or(self.columns.len()); - self.columns - .insert(index, Column::new(window, self.default_width)); + self.columns.insert(index, Column::new(window, self.default_width)); self.active_column = Some(index); self.reveal_pending = (self.columns.len() > 1).then_some(window); @@ -173,12 +166,9 @@ 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); @@ -206,9 +196,7 @@ 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; @@ -221,9 +209,7 @@ 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(()) @@ -237,11 +223,7 @@ 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))?; self.focus_without_reveal(focused)?; @@ -250,16 +232,11 @@ 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) @@ -278,9 +255,7 @@ 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(()); @@ -288,8 +263,7 @@ impl ScrollingLayout { self.remove(window)?; let insertion = (source + 1).min(self.columns.len()); - self.columns - .insert(insertion, Column::new(window, self.default_width)); + self.columns.insert(insertion, Column::new(window, self.default_width)); self.active_column = Some(insertion); self.reveal_pending = self.active_window(); @@ -297,32 +271,16 @@ 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.reveal_pending = 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); } @@ -365,35 +323,22 @@ 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 => { @@ -407,9 +352,7 @@ 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), } @@ -417,20 +360,9 @@ impl ScrollingLayout { Ok(true) } - 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); } @@ -441,18 +373,12 @@ impl ScrollingLayout { ColumnWidth::Proportion(value) => value, ColumnWidth::Fixed(_) | ColumnWidth::Full => DEFAULT_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) } @@ -496,9 +422,7 @@ impl ScrollingLayout { for column in &self.columns { let requested = match normalized_width(column.width) { - ColumnWidth::Proportion(proportion) => { - ((viewport_width + inner) * proportion - inner).max(1.0) - } + ColumnWidth::Proportion(proportion) => ((viewport_width + inner) * proportion - inner).max(1.0), ColumnWidth::Fixed(width) => width, ColumnWidth::Full => viewport_width, }; @@ -526,18 +450,14 @@ impl ScrollingLayout { self.reveal_pending = None; self.swipe_focus_pending = None; } else if let Some((previous, focused)) = self.swipe_focus_pending.take() { - 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)) { let last_end = column_positions .last() .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); } self.reveal_pending = None; } else { @@ -554,9 +474,7 @@ impl ScrollingLayout { let mut result = LayoutResult::default(); for (column_index, column) in self.columns.iter().enumerate() { let (column_x, column_width) = column_positions[column_index]; - let available_height = (viewport_height - - inner * (column.windows.len().saturating_sub(1) as f64)) - .max(1.0); + let available_height = (viewport_height - inner * (column.windows.len().saturating_sub(1) as f64)).max(1.0); let mut y = bounds.y + outer; for (window_index, window) in column.windows.iter().enumerate() { @@ -615,12 +533,7 @@ 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; } @@ -671,14 +584,8 @@ 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); @@ -686,15 +593,10 @@ impl ScrollingLayout { return Ok(()); } - let insertion = if destination < source { - source - } else { - source + 1 - }; + let insertion = if destination < source { source } else { source + 1 }; self.remove(window)?; let insertion = insertion.min(self.columns.len()); - self.columns - .insert(insertion, Column::new(window, self.default_width)); + self.columns.insert(insertion, Column::new(window, self.default_width)); self.active_column = Some(insertion); self.reveal_pending = self.active_window(); Ok(()) @@ -711,31 +613,20 @@ 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(()) } @@ -746,24 +637,15 @@ 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 { @@ -790,9 +672,7 @@ fn resize_rows(column: &mut Column, row: usize, neighbor: usize, amount: f64) -> 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(), @@ -800,11 +680,7 @@ 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( @@ -866,12 +742,7 @@ 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] @@ -920,9 +791,7 @@ 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 { @@ -961,14 +830,8 @@ 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] @@ -1037,13 +900,9 @@ 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 .geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .unwrap(); @@ -1071,9 +930,7 @@ 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 .geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .unwrap(); @@ -1188,18 +1045,14 @@ 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 .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 .geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .unwrap(); @@ -1345,18 +1198,10 @@ 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]); } @@ -1371,10 +1216,7 @@ 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()); @@ -1459,10 +1301,7 @@ 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); @@ -1475,11 +1314,7 @@ mod tests { .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 .geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .unwrap(); @@ -1518,9 +1353,7 @@ 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(); @@ -1534,9 +1367,7 @@ 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(); @@ -1546,10 +1377,7 @@ mod tests { } layout.validate().unwrap(); - assert_eq!( - layout.window_ids().collect::>().len(), - windows.len() - ); + assert_eq!(layout.window_ids().collect::>().len(), windows.len()); } } diff --git a/crates/ferese-protocols/src/lib.rs b/crates/ferese-protocols/src/lib.rs index 8c6f044..6b14461 100644 --- a/crates/ferese-protocols/src/lib.rs +++ b/crates/ferese-protocols/src/lib.rs @@ -195,15 +195,11 @@ 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")] @@ -216,15 +212,11 @@ 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/src/button.rs b/crates/ferese-theme/src/button.rs index 96a6d1a..2d3a6ab 100644 --- a/crates/ferese-theme/src/button.rs +++ b/crates/ferese-theme/src/button.rs @@ -1,9 +1,8 @@ +use cosmic::iced::{Background, Color}; +use cosmic::theme; +use cosmic::widget::button; + use crate::{composite, foreground}; -use cosmic::{ - iced::{Background, Color}, - theme, - widget::button, -}; #[derive(Clone, Copy)] pub(crate) enum State { @@ -13,11 +12,7 @@ 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 2dd4be2..8f839e4 100644 --- a/crates/ferese-theme/src/calendar.rs +++ b/crates/ferese-theme/src/calendar.rs @@ -1,10 +1,7 @@ //! Date/time labels and a calendar grid; callers own navigation and popup behavior. -use cosmic::{ - Element, - iced::{Alignment, Background, Border, Length}, - theme, - widget::{column, container, row}, -}; +use cosmic::iced::{Alignment, Background, Border, Length}; +use cosmic::widget::{column, container, row}; +use cosmic::{Element, theme}; use jiff::Zoned; pub fn format_bar_time(now: &Zoned) -> String { @@ -41,10 +38,7 @@ pub fn stacked_digits(label: &str) -> Option<[String; 2]> { let (hour, minute) = time.split_once(':')?; if !(1..=2).contains(&hour.len()) || minute.len() != 2 - || !hour - .bytes() - .chain(minute.bytes()) - .all(|digit| digit.is_ascii_digit()) + || !hour.bytes().chain(minute.bytes()).all(|digit| digit.is_ascii_digit()) { return None; } @@ -104,13 +98,9 @@ pub fn grid<'a, M: Clone + 'a>( let mut weekdays = row::with_capacity(7).spacing(3); for label in ["M", "T", "W", "T", "F", "S", "S"] { weekdays = weekdays.push( - container( - crate::text(label, font) - .size(11) - .class(theme::Text::Color(muted)), - ) - .width(30) - .center_x(30), + container(crate::text(label, font).size(11).class(theme::Text::Color(muted))) + .width(30) + .center_x(30), ); } grid = grid.push(weekdays); @@ -118,26 +108,21 @@ pub fn grid<'a, M: Clone + 'a>( let mut dates = row::with_capacity(7).spacing(3); 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(30) .height(25) .center_x(30) .center_y(25) .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 { radius: palette.radius.min(12.).into(), ..Default::default() @@ -155,14 +140,8 @@ pub fn grid<'a, M: Clone + 'a>( mod tests { #[test] fn pixel_clock_stacks_only_hour_and_minute_formats() { - assert_eq!( - super::stacked_digits("2:45 pm"), - Some(["02".into(), "45".into()]) - ); - assert_eq!( - super::stacked_digits("14:45"), - Some(["14".into(), "45".into()]) - ); + assert_eq!(super::stacked_digits("2:45 pm"), Some(["02".into(), "45".into()])); + assert_eq!(super::stacked_digits("14:45"), Some(["14".into(), "45".into()])); assert!(super::stacked_digits("14:45:30").is_none()); assert!(super::stacked_digits("Today 14:45").is_none()); assert!(super::stacked_digits("14:45 pm extra").is_none()); @@ -171,18 +150,9 @@ 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); } @@ -194,10 +164,7 @@ 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 21100db..f65ac11 100644 --- a/crates/ferese-theme/src/contrast.rs +++ b/crates/ferese-theme/src/contrast.rs @@ -76,27 +76,12 @@ 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,9 +110,10 @@ fn rgba(c: Color) -> cosmic::cosmic_theme::palette::Srgba { #[cfg(test)] mod tests { + use cosmic::iced::Background; + use super::*; use crate::button::{State, accent_button_style}; - use cosmic::iced::Background; #[test] fn reference_contrast_values_use_linear_srgb() { assert!((contrast(Color::BLACK, Color::WHITE) - 21.).abs() < 0.00001); @@ -139,10 +125,7 @@ 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] @@ -170,12 +153,7 @@ 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)); @@ -223,20 +201,11 @@ 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 3acc294..d3918d3 100644 --- a/crates/ferese-theme/src/controls.rs +++ b/crates/ferese-theme/src/controls.rs @@ -1,9 +1,8 @@ +use cosmic::iced::{Background, Border, Color, Vector}; +use cosmic::widget::{button, container}; +use cosmic::{theme, widget}; + use crate::{Palette, mix}; -use cosmic::{ - iced::{Background, Border, Color, Vector}, - theme, - widget::{self, button, container}, -}; pub fn surface(background: Color, radius: f32) -> theme::Container<'static> { theme::Container::custom(move |_| surface_appearance(background, radius)) @@ -85,11 +84,7 @@ fn styled_button(p: Palette, selected: bool, navigation: bool) -> theme::Button icon_color: Some(on), border_radius: p.radius.min(9.).into(), border_width: if selected { 1. } else { 0. }, - border_color: if selected { - p.accent - } else { - Color::TRANSPARENT - }, + border_color: if selected { p.accent } else { Color::TRANSPARENT }, outline_width: 0., outline_color: Color::TRANSPARENT, overlay: None, @@ -136,11 +131,7 @@ 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 { background: cosmic::iced::Color::from_rgba( (surface.r + 1.) * 0.5, @@ -190,9 +181,7 @@ 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 })), @@ -211,12 +200,7 @@ 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 { text_color: Some(foreground), icon_color: Some(foreground), diff --git a/crates/ferese-theme/src/icons.rs b/crates/ferese-theme/src/icons.rs index 55f06e5..b39c0d8 100644 --- a/crates/ferese-theme/src/icons.rs +++ b/crates/ferese-theme/src/icons.rs @@ -1,9 +1,7 @@ //! Canonical SVG assets, sizing and semantic tint rules for native Ferese UIs. -use cosmic::{ - iced::{Color, ContentFit, Length}, - theme, - widget::icon, -}; +use cosmic::iced::{Color, ContentFit, Length}; +use cosmic::theme; +use cosmic::widget::icon; pub const ARROW: &[u8] = include_bytes!("../assets/icons/arrow.svg"); pub const BATTERY: &[u8] = include_bytes!("../assets/icons/battery.svg"); @@ -18,8 +16,7 @@ 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"); @@ -67,10 +64,7 @@ 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) } pub fn accented(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon { @@ -88,8 +82,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, })) } diff --git a/crates/ferese-theme/src/material.rs b/crates/ferese-theme/src/material.rs index 32a3759..549b38b 100644 --- a/crates/ferese-theme/src/material.rs +++ b/crates/ferese-theme/src/material.rs @@ -1,14 +1,10 @@ use std::sync::Arc; -use ferese_protocols::material::v1::client::{ - ferese_material_manager_v1::FereseMaterialManagerV1, - ferese_surface_material_v1::FereseSurfaceMaterialV1, -}; -use wayland_client::{ - Connection, Dispatch, Proxy, QueueHandle, delegate_noop, - globals::{GlobalListContents, registry_queue_init}, - protocol::{wl_registry, wl_surface}, -}; +use ferese_protocols::material::v1::client::ferese_material_manager_v1::FereseMaterialManagerV1; +use ferese_protocols::material::v1::client::ferese_surface_material_v1::FereseSurfaceMaterialV1; +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::{wl_registry, wl_surface}; +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop}; /// Keeps a compositor-owned dialog material attached for the window lifetime. #[derive(Clone, Debug)] @@ -37,9 +33,8 @@ 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( @@ -48,8 +43,7 @@ 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(); @@ -63,9 +57,7 @@ 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()); } diff --git a/crates/ferese-theme/src/menus.rs b/crates/ferese-theme/src/menus.rs index 8cda52a..92101c9 100644 --- a/crates/ferese-theme/src/menus.rs +++ b/crates/ferese-theme/src/menus.rs @@ -1,9 +1,7 @@ //! Shared menu layouts and paint; callers own actions and animation timing. -use cosmic::{ - Element, - iced::{Alignment, Color, Length}, - widget::{button, container, row as iced_row}, -}; +use cosmic::Element; +use cosmic::iced::{Alignment, Color, Length}; +use cosmic::widget::{button, container, row as iced_row}; pub fn button<'a, M: Clone + 'a>( label: impl Into> + 'a, @@ -20,12 +18,9 @@ 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>( @@ -38,12 +33,7 @@ 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) @@ -56,12 +46,7 @@ pub fn badge<'a, M: 'a>( .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) @@ -87,10 +72,7 @@ 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() } @@ -112,10 +94,7 @@ 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 }, palette.radius.min(8.), )); container(knob) diff --git a/crates/ferese-theme/src/palette.rs b/crates/ferese-theme/src/palette.rs index 4fa6766..9b707e2 100644 --- a/crates/ferese-theme/src/palette.rs +++ b/crates/ferese-theme/src/palette.rs @@ -102,11 +102,7 @@ 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 { @@ -179,10 +175,8 @@ mod tests { ("translucent", 2., 1.), ("translucent", -1., 0.), ] { - let document = Document::parse(&format!( - "theme {{ material {{ style {style}; opacity {opacity}; }} }}" - )) - .unwrap(); + let document = + Document::parse(&format!("theme {{ material {{ style {style}; opacity {opacity}; }} }}")).unwrap(); assert_eq!(material_opacity(Some(&document)), expected); } } diff --git a/crates/ferese-theme/src/typography.rs b/crates/ferese-theme/src/typography.rs index 51ad94c..4fbc2ed 100644 --- a/crates/ferese-theme/src/typography.rs +++ b/crates/ferese-theme/src/typography.rs @@ -1,7 +1,5 @@ -use std::{ - collections::HashMap, - sync::{Mutex, OnceLock}, -}; +use std::collections::HashMap; +use std::sync::{Mutex, OnceLock}; pub fn font(family: Option<&str>) -> cosmic::font::Font { let Some(family) = family else { diff --git a/feresectl/src/autostart.rs b/feresectl/src/autostart.rs index c323f3b..8ada136 100644 --- a/feresectl/src/autostart.rs +++ b/feresectl/src/autostart.rs @@ -1,11 +1,9 @@ -use std::{ - collections::BTreeMap, - env, fs, - io::Read, - os::unix::process::CommandExt, - path::{Path, PathBuf}, - process::{Command, Stdio}, -}; +use std::collections::BTreeMap; +use std::io::Read; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::{env, fs}; pub(crate) fn run() -> Result<(), Box> { for key in ["WAYLAND_SOCKET", "FERESE_SHELL_CONTROL_SOCKET"] { @@ -35,20 +33,14 @@ 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()); } } @@ -108,9 +100,7 @@ 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; } @@ -152,15 +142,12 @@ 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/main.rs b/feresectl/src/main.rs index 47c3695..8afee14 100644 --- a/feresectl/src/main.rs +++ b/feresectl/src/main.rs @@ -1,11 +1,10 @@ mod autostart; -use std::env; use std::error::Error; -use std::fs; -use std::io::{self, Write}; +use std::io::Write; use std::os::unix::net::UnixStream; use std::path::PathBuf; +use std::{env, fs, io}; use ferese_ipc::{Request, Response, VERSION, read_frame, write_frame}; use serde_json::{Value, json}; @@ -101,10 +100,9 @@ fn parse_args(args: impl IntoIterator) -> Result<(String, Value), )); } }, - "toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout" - | "toggle-overview" | "cycle-column-width" | "center-column" | "consume" | "expel" - | "close" | "get-focused-window" | "get-windows" | "get-workspaces" | "get-outputs" - | "reload-config" | "exit" | "request-logout" => { + "toggle-floating" | "toggle-fullscreen" | "toggle-maximized" | "toggle-layout" | "toggle-overview" + | "cycle-column-width" | "center-column" | "consume" | "expel" | "close" | "get-focused-window" + | "get-windows" | "get-workspaces" | "get-outputs" | "reload-config" | "exit" | "request-logout" => { if !positional.is_empty() { return Err(format!("{command} does not accept arguments")); } @@ -152,15 +150,11 @@ 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!( @@ -203,10 +197,7 @@ mod tests { #[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/portal/src/backend.rs b/portal/src/backend.rs index 2b6343f..b3f2233 100644 --- a/portal/src/backend.rs +++ b/portal/src/backend.rs @@ -1,26 +1,22 @@ //! ScreenCast portal backend. Only the portal frontend may create sessions; //! saved monitor consent is restored only through the frontend permission store. -use crate::{ - capture::{Capture, Source}, - picker::Prompt, - stream::Ready, -}; -use std::sync::{ - Arc, - atomic::{AtomicBool, Ordering}, -}; -use std::{collections::HashMap, process::Stdio, time::Duration}; -use tokio::{ - io::{AsyncBufReadExt, AsyncWriteExt, BufReader}, - process::{Child, Command}, - sync::{Mutex, Notify}, -}; -use zbus::{ - Connection, - message::Header, - object_server::SignalEmitter, - zvariant::{OwnedObjectPath, OwnedValue, Value}, -}; +use std::collections::HashMap; +use std::process::Stdio; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::Duration; + +use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; +use tokio::process::{Child, Command}; +use tokio::sync::{Mutex, Notify}; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; + +use crate::capture::{Capture, Source}; +use crate::picker::Prompt; +use crate::stream::Ready; pub(crate) type Options = HashMap; pub(crate) static CONFIG_TRANSACTION: std::sync::Mutex<()> = std::sync::Mutex::new(()); @@ -105,18 +101,12 @@ impl RecorderControl { #[zbus(connection)] connection: &Connection, #[zbus(header)] 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(), session_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(), @@ -155,18 +145,14 @@ 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()))?; @@ -176,9 +162,7 @@ pub(crate) async fn authorize( .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()) @@ -227,10 +211,7 @@ 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; }); } } @@ -319,21 +300,14 @@ 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")); } @@ -369,10 +343,7 @@ 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")); } @@ -435,10 +406,7 @@ 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)) if !cancel.stopped.load(Ordering::SeqCst) => { @@ -455,8 +423,7 @@ 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)) @@ -593,8 +560,7 @@ 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::>(); @@ -602,10 +568,7 @@ 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()?; @@ -707,15 +670,11 @@ 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()); @@ -734,10 +693,7 @@ 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()); } @@ -751,11 +707,10 @@ 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) @@ -773,9 +728,7 @@ 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")?, @@ -787,25 +740,15 @@ 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] @@ -851,23 +794,13 @@ fn monitor_metadata( ), ( "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()); } @@ -890,11 +823,7 @@ fn validate_selection( 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; @@ -928,10 +857,7 @@ 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?; @@ -955,9 +881,7 @@ 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 @@ -1057,10 +981,7 @@ 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()); @@ -1117,10 +1038,7 @@ 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()); } @@ -1137,22 +1055,8 @@ 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 62e76f2..718b308 100644 --- a/portal/src/bridge.rs +++ b/portal/src/bridge.rs @@ -1,5 +1,8 @@ +use std::os::unix::net::UnixStream; +use std::sync::Arc; +use std::time::Duration; + use serde_json::Value as Json; -use std::{os::unix::net::UnixStream, sync::Arc, time::Duration}; #[derive(Clone)] pub(crate) struct Bridge(Arc); @@ -41,17 +44,9 @@ impl Bridge { async fn request(&self, command: &'static str, args: Json, wait: bool) -> Result { let bridge = self.clone(); tokio::task::spawn_blocking(move || { - let mut stream = bridge - .0 - .stream - .lock() - .map_err(|_| "Compositor connection failed")?; + let mut stream = bridge.0.stream.lock().map_err(|_| "Compositor connection failed")?; stream - .set_read_timeout(if wait { - None - } else { - Some(Duration::from_secs(2)) - }) + .set_read_timeout(if wait { None } else { Some(Duration::from_secs(2)) }) .map_err(|error| error.to_string())?; let request = ferese_ipc::Request { version: ferese_ipc::VERSION, @@ -73,12 +68,8 @@ 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) async fn closed(&self) { diff --git a/portal/src/capture.rs b/portal/src/capture.rs index d52fa7b..b990e84 100644 --- a/portal/src/capture.rs +++ b/portal/src/capture.rs @@ -1,20 +1,15 @@ //! Bounded, cancellable Wayland capture. No screenshots or external capture tools. +use std::fs::File; +use std::os::fd::{AsFd, AsRawFd, FromRawFd}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + use ferese_protocols::window_capture::v1::client::ferese_window_capture_manager_v1 as window_manager; use memmap2::MmapMut; use serde::{Deserialize, Serialize}; -use std::{ - fs::File, - os::fd::{AsFd, AsRawFd, FromRawFd}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - time::{Duration, Instant}, -}; -use wayland_client::{ - Connection, Dispatch, EventQueue, QueueHandle, WEnum, - protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool}, -}; +use wayland_client::protocol::{wl_buffer, wl_output, wl_registry, wl_shm, wl_shm_pool}; +use wayland_client::{Connection, Dispatch, EventQueue, QueueHandle, WEnum}; use wayland_protocols_wlr::screencopy::v1::client::{ zwlr_screencopy_frame_v1 as frame, zwlr_screencopy_manager_v1 as manager, }; @@ -104,9 +99,7 @@ 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) @@ -114,9 +107,7 @@ 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)?; @@ -168,34 +159,19 @@ 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 { @@ -205,18 +181,11 @@ 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, @@ -234,11 +203,7 @@ 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, @@ -283,9 +248,7 @@ 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()); } } @@ -327,13 +290,9 @@ 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), @@ -358,9 +317,7 @@ 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; } @@ -412,9 +369,7 @@ 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()); @@ -424,20 +379,13 @@ 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, qh, ()); - let proxy = pool.create_buffer( - 0, - width as i32, - height as i32, - stride as i32, - format, + 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, ()); pool.destroy(); frame.copy(&proxy); @@ -540,9 +488,7 @@ 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()); @@ -578,9 +524,7 @@ 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, _ => (), } @@ -609,11 +553,7 @@ 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 99c0ddb..e899310 100644 --- a/portal/src/consent.rs +++ b/portal/src/consent.rs @@ -1,14 +1,14 @@ -use cosmic::{ - Element, - app::{Core, Settings, Task}, - iced::{Alignment, Length}, - widget::{button, column, container, image, row, scrollable, text_input}, -}; -use ferese_theme::{Palette, accent_button, controls, material::ModalMaterial, text}; -use serde::{Deserialize, Serialize}; use std::io::{Read, Write}; use std::sync::Arc; +use cosmic::Element; +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::{Alignment, Length}; +use cosmic::widget::{button, column, container, image, row, scrollable, text_input}; +use ferese_theme::material::ModalMaterial; +use ferese_theme::{Palette, accent_button, controls, text}; +use serde::{Deserialize, Serialize}; + #[derive(Clone, Serialize, Deserialize)] pub(crate) struct Prompt { pub title: String, @@ -30,15 +30,7 @@ pub(crate) struct ShortcutField { #[derive(Clone, Debug)] enum Message { Opened(cosmic::iced::window::Id), - Attached( - Result< - ( - Option>, - Result, - ), - String, - >, - ), + Attached(Result<(Option>, Result), String>), Accept, Cancel, Shortcut(usize, String), @@ -56,9 +48,7 @@ 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()); @@ -124,9 +114,7 @@ impl cosmic::Application for Consent { fn subscription(&self) -> cosmic::iced::Subscription { 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), .. diff --git a/portal/src/desktop.rs b/portal/src/desktop.rs index 6d63deb..f729382 100644 --- a/portal/src/desktop.rs +++ b/portal/src/desktop.rs @@ -1,22 +1,21 @@ -use crate::{ - backend::{Cancel, Options, Request, authorize}, - consent::Prompt, -}; -use std::{ - collections::HashMap, - io::Read, - os::unix::{fs::PermissionsExt, net::UnixStream}, - path::{Path, PathBuf}, - process::Stdio, - sync::{Arc, atomic::Ordering}, - time::Duration, -}; -use tokio::{io::AsyncWriteExt, sync::Mutex}; -use zbus::{ - Connection, - message::Header, - zvariant::{OwnedObjectPath, Value}, -}; +use std::collections::HashMap; +use std::io::Read; +use std::os::unix::fs::PermissionsExt; +use std::os::unix::net::UnixStream; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Duration; + +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; +use zbus::Connection; +use zbus::message::Header; +use zbus::zvariant::{OwnedObjectPath, Value}; + +use crate::backend::{Cancel, Options, Request, authorize}; +use crate::consent::Prompt; const PATH: &str = "/org/freedesktop/portal/desktop"; const MAX_IMAGE_BYTES: u64 = 128 * 1024 * 1024; @@ -56,9 +55,7 @@ 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()); if !connection @@ -72,9 +69,7 @@ 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())); Ok(cancel) @@ -82,10 +77,7 @@ 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; } } @@ -231,10 +223,7 @@ 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())? @@ -260,8 +249,7 @@ fn ipc_sync(command: &'static str, args: serde_json::Value) -> Result) -> 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")?; @@ -462,20 +447,14 @@ 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) @@ -530,8 +509,7 @@ 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()); } @@ -562,9 +540,7 @@ 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())?, )]))) } @@ -577,9 +553,7 @@ 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()); } @@ -599,8 +573,7 @@ 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) } @@ -668,11 +641,7 @@ 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()); @@ -705,9 +673,7 @@ fn prepare_wallpaper(input: &Path, cancel: &Cancel) -> Result 16384 @@ -741,8 +707,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String> Err(error) if error.kind() == std::io::ErrorKind::NotFound => String::new(), Err(error) => return Err(error.to_string()), }; - let mut document = - ferese_config::Document::parse(&source).map_err(|error| error.to_string())?; + let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?; if target == "background" && document.get("theme.background.lock_path").is_none() { let old = document .get("theme.background.path") @@ -755,10 +720,7 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String> } if target != "lockscreen" { document - .set( - "theme.background.path", - image.to_string_lossy().into_owned().into(), - ) + .set("theme.background.path", image.to_string_lossy().into_owned().into()) .map_err(|error| error.to_string())?; } if target != "background" { @@ -772,11 +734,8 @@ fn edit_wallpaper(path: &Path, image: &Path, target: &str) -> Result<(), String> let parent = path.parent().ok_or("Invalid config path")?; std::fs::create_dir_all(parent).map_err(|error| error.to_string())?; let mut temp = tempfile::NamedTempFile::new_in(parent).map_err(|error| error.to_string())?; - std::io::Write::write_all(&mut temp, document.to_string().as_bytes()) - .map_err(|error| error.to_string())?; - temp.as_file() - .sync_all() - .map_err(|error| error.to_string())?; + std::io::Write::write_all(&mut temp, document.to_string().as_bytes()).map_err(|error| error.to_string())?; + temp.as_file().sync_all().map_err(|error| error.to_string())?; if std::fs::read_to_string(&path).unwrap_or_default() != source { return Err("Configuration changed; please try again".into()); } @@ -806,11 +765,7 @@ 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()); } } @@ -862,46 +817,25 @@ mod tests { fn wallpaper_targets_preserve_independent_paths_and_other_settings() { let dir = tempfile::tempdir().unwrap(); let path = dir.path().join("config.kdl"); - std::fs::write(&path, "// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n").unwrap(); + std::fs::write( + &path, + "// Keep my settings\ntheme { background { path \"/old.png\"; }; }; commands { terminal \"foot\"; }\n", + ) + .unwrap(); edit_wallpaper(&path, Path::new("/desktop.png"), "background").unwrap(); - let document = - ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); - assert_eq!( - document.get("theme.background.path").unwrap(), - "/desktop.png" - ); - assert_eq!( - document.get("theme.background.lock_path").unwrap(), - "/old.png" - ); - assert!( - std::fs::read_to_string(&path) - .unwrap() - .contains("Keep my settings") - ); + let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png"); + assert_eq!(document.get("theme.background.lock_path").unwrap(), "/old.png"); + assert!(std::fs::read_to_string(&path).unwrap().contains("Keep my settings")); edit_wallpaper(&path, Path::new("/lock.png"), "lockscreen").unwrap(); - let document = - ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); - assert_eq!( - document.get("theme.background.path").unwrap(), - "/desktop.png" - ); - assert_eq!( - document.get("theme.background.lock_path").unwrap(), - "/lock.png" - ); + let document = ferese_config::Document::parse(&std::fs::read_to_string(&path).unwrap()).unwrap(); + assert_eq!(document.get("theme.background.path").unwrap(), "/desktop.png"); + assert_eq!(document.get("theme.background.lock_path").unwrap(), "/lock.png"); edit_wallpaper(&path, Path::new("/both.png"), "both").unwrap(); - 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(), "/both.png"); - assert_eq!( - document.get("theme.background.lock_path").unwrap(), - "/both.png" - ); - assert_eq!( - document.get("commands.terminal").unwrap(), - &serde_json::json!(["foot"]) - ); + assert_eq!(document.get("theme.background.lock_path").unwrap(), "/both.png"); + assert_eq!(document.get("commands.terminal").unwrap(), &serde_json::json!(["foot"])); } #[test] @@ -912,10 +846,6 @@ 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 b9d55fe..eb04052 100644 --- a/portal/src/eis.rs +++ b/portal/src/eis.rs @@ -1,23 +1,15 @@ -use std::{ - fs::File, - io::Write, - os::{ - fd::{AsFd, OwnedFd}, - unix::net::UnixStream, - }, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, -}; +use std::fs::File; +use std::io::Write; +use std::os::fd::{AsFd, OwnedFd}; +use std::os::unix::net::UnixStream; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; use calloop::{EventLoop, LoopSignal, PostAction, channel}; -use reis::{ - calloop::{EisRequestSource, EisRequestSourceEvent}, - eis, - enumflags2::BitFlags, - request::{Connection, Device, DeviceCapability, EisRequest}, -}; +use reis::calloop::{EisRequestSource, EisRequestSourceEvent}; +use reis::eis; +use reis::enumflags2::BitFlags; +use reis::request::{Connection, Device, DeviceCapability, EisRequest}; use serde_json::Value; use crate::backend::Cancel; @@ -29,11 +21,7 @@ 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()); } @@ -59,8 +47,7 @@ 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 @@ -72,22 +59,19 @@ 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(_) => { - state.signal.stop(); - PostAction::Remove - } - }; - if connection.flush().is_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 } - Ok(action) - }, - ) + }; + if connection.flush().is_err() { + state.signal.stop(); + } + Ok(action) + }) .map_err(|e| e.to_string())?; let mut state = State { signal: signal.clone(), @@ -107,8 +91,7 @@ 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>(()) @@ -121,14 +104,7 @@ 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> { @@ -180,18 +156,14 @@ 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; @@ -207,11 +179,7 @@ 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()); } }, ); @@ -221,15 +189,11 @@ 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); @@ -263,14 +227,11 @@ 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; } @@ -307,12 +268,7 @@ 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()); @@ -392,10 +348,7 @@ 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 @@ -406,12 +359,12 @@ fn now() -> u64 { #[cfg(test)] mod tests { - use super::*; + use std::process::{Command, Stdio}; + use std::time::{Duration, Instant}; + use serde_json::json; - use std::{ - process::{Command, Stdio}, - time::{Duration, Instant}, - }; + + use super::*; #[test] #[ignore = "requires Python and the system libei receiver library"] @@ -419,8 +372,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)) @@ -435,10 +388,7 @@ 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![ @@ -453,11 +403,7 @@ 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 0970c6f..8174ee1 100644 --- a/portal/src/inhibit.rs +++ b/portal/src/inhibit.rs @@ -1,21 +1,17 @@ -use std::{ - collections::HashMap, - sync::{Arc, atomic::Ordering}, - time::Duration, -}; -use tokio::sync::{Mutex, Notify}; -use zbus::{ - Connection, - message::Header, - object_server::SignalEmitter, - zvariant::{OwnedFd, OwnedObjectPath, Value}, -}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Duration; -use crate::{ - backend::{Cancel, Options, authorize}, - bridge::Bridge, - desktop::Requests, -}; +use tokio::sync::{Mutex, Notify}; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value}; + +use crate::backend::{Cancel, Options, authorize}; +use crate::bridge::Bridge; +use crate::desktop::Requests; const PATH: &str = "/org/freedesktop/portal/desktop"; @@ -68,10 +64,7 @@ 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(); } } @@ -114,9 +107,7 @@ 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; @@ -128,8 +119,7 @@ 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()) @@ -141,11 +131,7 @@ 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() } }; @@ -154,19 +140,13 @@ 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) { if Self::state_changed( @@ -203,9 +183,7 @@ 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", @@ -219,16 +197,10 @@ 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(); } @@ -246,9 +218,7 @@ 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 { @@ -268,9 +238,7 @@ 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() { @@ -351,9 +314,7 @@ 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()), )) } } @@ -369,15 +330,11 @@ 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; @@ -394,30 +351,59 @@ impl Inhibit { _ = cancel.wait() => Err("Monitor creation was cancelled".into()), result = bridge.call("portal-monitor-register", serde_json::json!({})) => result, }; - let native = match result { Ok(native) => native, Err(error) => { bridge.close(); return Err(zbus::fdo::Error::Failed(error)); } }; + let native = match result { + Ok(native) => native, + Err(error) => { + bridge.close(); + return Err(zbus::fdo::Error::Failed(error)); + } + }; let Some(native_owner) = native["monitor-owner"].as_u64() else { bridge.close(); return Err(zbus::fdo::Error::Failed("Missing monitor owner".into())); }; - let exported = connection.object_server().at(&session_handle, MonitorObject { - backend: self.clone(), path: session_handle.to_string(), owner: owner.clone(), cancel: cancel.clone(), - }).await; + let exported = connection + .object_server() + .at( + &session_handle, + MonitorObject { + backend: self.clone(), + path: session_handle.to_string(), + owner: owner.clone(), + cancel: cancel.clone(), + }, + ) + .await; match exported { Ok(true) if !cancel.stopped.load(Ordering::SeqCst) => { - monitors.insert(session_handle.to_string(), Arc::new(Monitor { - owner, cancel: cancel.clone(), last: Mutex::new(None), bridge, - native_owner, - })); + monitors.insert( + session_handle.to_string(), + Arc::new(Monitor { + owner, + cancel: cancel.clone(), + last: Mutex::new(None), + bridge, + native_owner, + }), + ); self.changed.notify_one(); Ok(0) } result => { bridge.close(); - if matches!(result, Ok(true)) { let _ = connection.object_server().remove::(&session_handle).await; } - result.map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 }).map_err(Into::into) + if matches!(result, Ok(true)) { + let _ = connection + .object_server() + .remove::(&session_handle) + .await; + } + result + .map(|_| if cancel.stopped.load(Ordering::SeqCst) { 1 } else { 2 }) + .map_err(Into::into) } } - }.await; + } + .await; attempt }; drop(monitors); @@ -442,9 +428,7 @@ 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 @@ -484,9 +468,7 @@ 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 a46fa0c..c4bb4c1 100644 --- a/portal/src/input_capture.rs +++ b/portal/src/input_capture.rs @@ -1,24 +1,19 @@ -use std::{ - collections::HashMap, - sync::{Arc, atomic::Ordering}, - time::Duration, -}; +use std::collections::HashMap; +use std::sync::Arc; +use std::sync::atomic::Ordering; +use std::time::Duration; use serde_json::json; use tokio::sync::Mutex; -use zbus::{ - Connection, - message::Header, - object_server::SignalEmitter, - zvariant::{OwnedFd, OwnedObjectPath, Value}, -}; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedFd, OwnedObjectPath, Value}; -use crate::{ - backend::{Cancel, Options, authorize}, - bridge::Bridge, - desktop::Requests, - eis::Worker, -}; +use crate::backend::{Cancel, Options, authorize}; +use crate::bridge::Bridge; +use crate::desktop::Requests; +use crate::eis::Worker; const PATH: &str = "/org/freedesktop/portal/desktop"; @@ -92,10 +87,7 @@ 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; }); } } @@ -106,9 +98,7 @@ 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; @@ -152,10 +142,7 @@ 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; } @@ -179,11 +166,9 @@ impl InputCapture { .as_array() .filter(|position| position.len() == 2) { - if let Ok(position) = Value::from(( - position[0].as_f64().unwrap_or(0.0), - position[1].as_f64().unwrap_or(0.0), - )) - .try_to_owned() + if 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); } @@ -201,12 +186,8 @@ 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,8 +219,7 @@ 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")); } @@ -267,9 +247,7 @@ 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 { @@ -365,9 +343,7 @@ 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?; @@ -406,18 +382,11 @@ 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()); } @@ -473,9 +442,7 @@ 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! { @@ -485,23 +452,18 @@ 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 = result.unwrap().map_err(failed)?; - 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(), @@ -523,9 +485,7 @@ 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")); @@ -533,20 +493,15 @@ 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::>>()?; @@ -558,10 +513,7 @@ 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())); } @@ -588,9 +540,7 @@ 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() { @@ -599,11 +549,10 @@ 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()) } @@ -616,9 +565,7 @@ 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 @@ -630,9 +577,7 @@ 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; @@ -652,9 +597,7 @@ 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?; bridge .call("input-capture-disable", json!({"session":id})) @@ -671,9 +614,7 @@ 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") @@ -770,9 +711,7 @@ 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 684be17..eda191b 100644 --- a/portal/src/lockdown.rs +++ b/portal/src/lockdown.rs @@ -1,6 +1,13 @@ -use std::{borrow::Cow, collections::HashMap, sync::Arc, time::Duration}; +use std::borrow::Cow; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + use tokio::sync::RwLock; -use zbus::{Connection, message::Header, object_server::SignalEmitter, zvariant::Value}; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::Value; const PATH: &str = "/org/freedesktop/portal/desktop"; const KEYS: [&str; 7] = [ @@ -55,8 +62,7 @@ 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)?; @@ -209,8 +215,7 @@ fn store(index: usize, value: bool) -> Result<(), String> { Err(error) => return Err(error.to_string()), _ => return Err("Configuration is too large".into()), }; - let mut document = - ferese_config::Document::parse(&source).map_err(|error| error.to_string())?; + let mut document = ferese_config::Document::parse(&source).map_err(|error| error.to_string())?; if values(&document).is_none() { return Err("Invalid lockdown policy".into()); } @@ -223,11 +228,8 @@ fn store(index: usize, value: bool) -> Result<(), String> { let directory = path.parent().ok_or("Invalid config path")?; std::fs::create_dir_all(directory).map_err(|error| error.to_string())?; let mut file = tempfile::NamedTempFile::new_in(directory).map_err(|error| error.to_string())?; - std::io::Write::write_all(&mut file, document.to_string().as_bytes()) - .map_err(|error| error.to_string())?; - file.as_file() - .sync_all() - .map_err(|error| error.to_string())?; + std::io::Write::write_all(&mut file, document.to_string().as_bytes()).map_err(|error| error.to_string())?; + file.as_file().sync_all().map_err(|error| error.to_string())?; if std::fs::read_to_string(&path).unwrap_or_default() != source { return Err("Configuration changed; please try again".into()); } @@ -250,9 +252,7 @@ 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 bdeaccc..f27468f 100644 --- a/portal/src/main.rs +++ b/portal/src/main.rs @@ -19,18 +19,14 @@ 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 183026d..6426e2b 100644 --- a/portal/src/parent.rs +++ b/portal/src/parent.rs @@ -1,12 +1,9 @@ -use wayland_client::{ - Connection, Dispatch, Proxy, QueueHandle, delegate_noop, - globals::{GlobalListContents, registry_queue_init}, - protocol::{wl_registry, wl_surface}, -}; -use wayland_protocols::xdg::foreign::zv2::client::{ - zxdg_imported_v2::{self, ZxdgImportedV2}, - zxdg_importer_v2::ZxdgImporterV2, -}; +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::{wl_registry, wl_surface}; +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, delegate_noop}; +use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2; +use wayland_protocols::xdg::foreign::zv2::client::zxdg_imported_v2::ZxdgImportedV2; +use wayland_protocols::xdg::foreign::zv2::client::zxdg_importer_v2::ZxdgImporterV2; #[derive(Debug)] pub(crate) struct Parent { @@ -16,10 +13,7 @@ pub(crate) struct Parent { } impl Parent { - pub(crate) fn attach( - window: &dyn cosmic::iced::window::Window, - parent: &str, - ) -> Result, String> { + pub(crate) fn attach(window: &dyn cosmic::iced::window::Window, parent: &str) -> Result, String> { use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle}; if parent.is_empty() { return Ok(None); @@ -36,9 +30,8 @@ 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( @@ -47,19 +40,15 @@ 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(); diff --git a/portal/src/permissions.rs b/portal/src/permissions.rs index 07e523b..1d7b843 100644 --- a/portal/src/permissions.rs +++ b/portal/src/permissions.rs @@ -1,20 +1,16 @@ -use crate::{ - backend::{Options, authorize}, - desktop::{Requests, consent, ipc, reply}, -}; -use std::{ - collections::{HashMap, HashSet}, - path::Path, - sync::Arc, - time::Duration, -}; +use std::collections::{HashMap, HashSet}; +use std::path::Path; +use std::sync::Arc; +use std::time::Duration; + use tokio::sync::RwLock; -use zbus::{ - Connection, - message::Header, - object_server::SignalEmitter, - zvariant::{OwnedObjectPath, OwnedValue, Value}, -}; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; + +use crate::backend::{Options, authorize}; +use crate::desktop::{Requests, consent, ipc, reply}; const PATH: &str = "/org/freedesktop/portal/desktop"; @@ -56,19 +52,12 @@ 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() @@ -102,27 +91,11 @@ async fn usb_consent( description.push_str(&format!( "\n{} — {}", label, - if writable { - "read and write" - } else { - "read only" - } - )); - selected.push(( - id, - HashMap::from([("writable".to_owned(), OwnedValue::from(writable))]), + if writable { "read and write" } else { "read only" } )); + 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([( @@ -212,8 +185,7 @@ 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)] @@ -254,13 +226,7 @@ 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 @@ -287,9 +253,7 @@ fn set_autostart( } 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 @@ -377,15 +341,9 @@ 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 b8e1b68..77e4d8c 100644 --- a/portal/src/picker.rs +++ b/portal/src/picker.rs @@ -1,13 +1,13 @@ -use crate::capture::Source; -use cosmic::{ - ApplicationExt, Element, - app::{Core, Settings, Task}, - iced::{Alignment, Length}, - widget::{button, column, container, row, scrollable}, -}; +use std::io::{Read, Write}; + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::{Alignment, Length}; +use cosmic::widget::{button, column, container, row, scrollable}; +use cosmic::{ApplicationExt, Element}; use ferese_theme::icons::symbolic as glyph; use serde::{Deserialize, Serialize}; -use std::io::{Read, Write}; + +use crate::capture::Source; #[derive(Clone, Serialize, Deserialize)] pub struct Prompt { @@ -82,9 +82,7 @@ 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()); } @@ -95,8 +93,7 @@ pub fn run() -> Result<(), Box> { } let (theme, font, background, palette) = appearance(); - let height = - 180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. }; + let height = 180. + prompt.source_list_height() + if prompt.persist_mode > 0 { 64. } else { 0. }; cosmic::app::run::( Settings::default() .size(cosmic::iced::Size::new(400., height)) @@ -114,12 +111,7 @@ 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"; @@ -131,10 +123,7 @@ 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,9 +160,7 @@ impl cosmic::Application for Picker { fn subscription(&self) -> cosmic::iced::Subscription { cosmic::iced::event::listen_with(|event, _, id| match event { - cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => { - Some(Message::WindowOpened(id)) - } + cosmic::iced::Event::Window(cosmic::iced::window::Event::Opened { .. }) => Some(Message::WindowOpened(id)), _ => None, }) } @@ -183,12 +170,8 @@ impl cosmic::Application for Picker { Message::WindowOpened(id) => { let parent = self.prompt.parent.clone(); return cosmic::iced::window::run(id, move |window| { - let parent = - crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new); - Ok(( - parent, - ferese_theme::material::ModalMaterial::attach(window), - )) + let parent = crate::parent::Parent::attach(window, &parent)?.map(std::sync::Arc::new); + Ok((parent, ferese_theme::material::ModalMaterial::attach(window))) }) .map(|result| cosmic::Action::App(Message::Attached(result))); } @@ -216,11 +199,7 @@ impl cosmic::Application for Picker { } Message::Remember if self.can_remember() => self.remember = !self.remember, 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 { @@ -288,26 +267,20 @@ 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), @@ -329,8 +302,7 @@ impl cosmic::Application for Picker { .on_press(Message::Select(index)), ); } - content = content - .push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height()))); + content = content.push(scrollable(sources).height(Length::Fixed(self.prompt.source_list_height()))); content = content.push( self.text(if self.prompt.window_capture { "Only the selected window will be captured." @@ -354,21 +326,14 @@ impl cosmic::Application for Picker { ferese_theme::menus::switch(self.remember, self.palette, 1.), self.font, )) - .class(ferese_theme::controls::navigation_style( - self.palette, - false, - )) + .class(ferese_theme::controls::navigation_style(self.palette, false)) .width(Length::Fill) .on_press_maybe(self.can_remember().then_some(Message::Remember)), ); } let mut share = button::custom( row![ - self.text(if self.prompt.window_capture { - "Capture" - } else { - "Share" - }), + self.text(if self.prompt.window_capture { "Capture" } else { "Share" }), glyph(ferese_theme::icons::ARROW, 16) ] .spacing(8) @@ -396,11 +361,10 @@ 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 0fc9cf8..045b878 100644 --- a/portal/src/record_main.rs +++ b/portal/src/record_main.rs @@ -3,11 +3,9 @@ mod recorder; 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 68141f6..7139c30 100644 --- a/portal/src/recorder.rs +++ b/portal/src/recorder.rs @@ -1,22 +1,19 @@ +use std::collections::HashMap; +use std::fs::{File, OpenOptions}; +use std::io::{Read, Write}; +use std::os::fd::AsRawFd; +use std::os::unix::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::time::{Duration, Instant}; + use futures_util::StreamExt; -use gstreamer::{self as gst, prelude::*}; -use std::{ - collections::HashMap, - fs::{File, OpenOptions}, - io::{Read, Write}, - os::{fd::AsRawFd, unix::fs::OpenOptionsExt}, - path::{Path, PathBuf}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - }, - time::{Duration, Instant}, -}; +use gstreamer as gst; +use gstreamer::prelude::*; use tokio::sync::watch; -use zbus::{ - Connection, Proxy, - zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value}, -}; +use zbus::zvariant::{OwnedFd, OwnedObjectPath, OwnedValue, Value}; +use zbus::{Connection, Proxy}; type Options = HashMap; const DESKTOP: &str = "org.freedesktop.portal.Desktop"; @@ -38,32 +35,15 @@ 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") + 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 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 @@ -90,14 +70,7 @@ 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." @@ -127,13 +100,10 @@ pub async fn run() -> Result<(), String> { event("cancelled", None, None); 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 @@ -146,19 +116,12 @@ pub async fn run() -> Result<(), String> { "org.ferese.ScreenRecorder", ) .await?; - control - .call::<_, _, ()>("UseBarControls", &(handle.clone(),)) - .await + control.call::<_, _, ()>("UseBarControls", &(handle.clone(),)).await }) .await; - let session = Proxy::new( - &connection, - DESKTOP, - handle.as_str(), - "org.freedesktop.portal.Session", - ) - .await - .map_err(|e| e.to_string())?; + 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).await; let _: Result<(), _> = session.call("Close", &()).await; match result { @@ -176,10 +139,7 @@ async fn record_session( output: &Path, stop: &mut watch::Receiver, ) -> 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, "SelectSources", @@ -205,11 +165,7 @@ 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? @@ -226,14 +182,9 @@ async fn record_session( if streams.len() != 1 { return Err("Recording requires exactly one display".into()); } - 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 @@ -278,8 +229,7 @@ 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-{}-{}", jiff::Zoned::now().strftime("%Y-%m-%d_%H-%M-%S"), @@ -338,24 +288,19 @@ fn encode(source: gst::Element, output: &Path, stop: Arc) -> Result< pipeline .add_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())?; + gst::Element::link_many([&source, &queue, &convert, &encoder, &mux, &sink]).map_err(|e| e.to_string())?; let announced = Arc::new(AtomicBool::new(false)); let notice = announced.clone(); source .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; let started = Instant::now(); @@ -404,8 +349,7 @@ fn encode(source: gst::Element, output: &Path, stop: Arc) -> Result< 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) } @@ -419,10 +363,7 @@ 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"] @@ -493,10 +434,7 @@ mod tests { "Invalid saved duration: {duration:?}" ); assert!( - matches!( - message.as_ref().map(|m| m.view()), - Some(gst::MessageView::Eos(_)) - ), + matches!(message.as_ref().map(|m| m.view()), Some(gst::MessageView::Eos(_))), "Saved video did not decode: {message:?}" ); } diff --git a/portal/src/restore.rs b/portal/src/restore.rs index 59b5be7..35d507a 100644 --- a/portal/src/restore.rs +++ b/portal/src/restore.rs @@ -1,8 +1,10 @@ -use crate::{backend::Options, capture::Source}; use serde::{Deserialize, Serialize}; use serde_json::Value as Json; use zbus::zvariant::{OwnedValue, Value}; +use crate::backend::Options; +use crate::capture::Source; + const VENDOR: &str = "Ferese"; const VERSION: u32 = 1; const MAX_DATA: usize = 16 * 1024; @@ -31,9 +33,7 @@ pub(crate) fn persist_mode(options: &Options) -> zbus::fdo::Result { .map_err(|_| zbus::fdo::Error::InvalidArgs("Invalid persistence option".into()))? .unwrap_or(0); if mode > 2 { - return Err(zbus::fdo::Error::InvalidArgs( - "Unsupported persistence mode".into(), - )); + return Err(zbus::fdo::Error::InvalidArgs("Unsupported persistence mode".into())); } Ok(mode) } @@ -43,9 +43,7 @@ fn identity<'a>(outputs: &'a Json, name: &str) -> Option<&'a str> { (output["name"].as_str() == Some(name) && output["enabled"] == true) .then(|| output["identity"].as_str()) .flatten() - .filter(|identity| { - !identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:") - }) + .filter(|identity| !identity.is_empty() && identity.len() <= 512 && identity.starts_with("drm-edid:")) }) } @@ -54,14 +52,15 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso outputs .as_array() .and_then(|outputs| { - outputs.iter().find(|output| { - output["name"].as_str() == Some(name) && output["enabled"] == true - }) + outputs + .iter() + .find(|output| output["name"].as_str() == Some(name) && output["enabled"] == true) }) .map(|output| (output["identity"].clone(), output["id"].clone())) }; - sources.iter().all( - |source| match (find(before, &source.name), find(after, &source.name)) { + sources + .iter() + .all(|source| match (find(before, &source.name), find(after, &source.name)) { (Some((identity, id)), Some((current_identity, current_id))) => { if identity.is_null() { id.as_u64().is_some() && id == current_id && current_identity.is_null() @@ -70,8 +69,7 @@ pub(crate) fn selection_unchanged(sources: &[Source], before: &Json, after: &Jso } } _ => false, - }, - ) + }) } impl Restore { @@ -125,29 +123,13 @@ impl Restore { { return None; } - selected.push( - sources - .iter() - .find(|source| source.name == monitor.name)? - .clone(), - ); + selected.push(sources.iter().find(|source| source.name == monitor.name)?.clone()); } Some(selected) } - pub(crate) fn create( - app: &str, - cursor: bool, - sources: &[Source], - outputs: &Json, - mode: u32, - ) -> Option { - if app.is_empty() - || app.len() > 512 - || sources.is_empty() - || sources.len() > 16 - || !(1..=2).contains(&mode) - { + pub(crate) fn create(app: &str, cursor: bool, sources: &[Source], outputs: &Json, mode: u32) -> Option { + if app.is_empty() || app.len() > 512 || sources.is_empty() || sources.len() > 16 || !(1..=2).contains(&mode) { return None; } let monitors = sources @@ -196,8 +178,7 @@ mod tests { #[test] fn restore_is_bound_to_app_capture_options_and_display_identity() { - let outputs = - serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]); + let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]); let sources = [source()]; let saved = Restore::create("org.test.App", true, &sources, &outputs, 2).unwrap(); let options = Options::from([("restore_data".into(), saved.value().unwrap())]); @@ -217,20 +198,14 @@ mod tests { .resolve("org.test.App", false, false, &sources, &outputs) .is_none() ); - assert!( - restored - .resolve("org.test.App", true, false, &[], &outputs) - .is_none() - ); - let changed = - serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]); + assert!(restored.resolve("org.test.App", true, false, &[], &outputs).is_none()); + let changed = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:456", "enabled":true}]); assert!( restored .resolve("org.test.App", true, false, &sources, &changed) .is_none() ); - let disabled = - serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]); + let disabled = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":false}]); assert!( restored .resolve("org.test.App", true, false, &sources, &disabled) @@ -240,14 +215,9 @@ mod tests { #[test] fn transient_permission_cannot_be_upgraded_without_new_consent() { - let outputs = - serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]); + let outputs = serde_json::json!([{"name":"DP-1", "identity":"drm-edid:123", "enabled":true}]); let saved = Restore::create("org.test.App", false, &[source()], &outputs, 1).unwrap(); - let restored = Restore::read(&Options::from([( - "restore_data".into(), - saved.value().unwrap(), - )])) - .unwrap(); + let restored = Restore::read(&Options::from([("restore_data".into(), saved.value().unwrap())])).unwrap(); assert_eq!(restored.effective_mode(2), 1); assert_eq!(restored.effective_mode(0), 0); let weak = serde_json::json!([{"name":"DP-1", "identity":"drm:DP-1", "enabled":true}]); @@ -259,8 +229,7 @@ mod tests { fn anonymous_nested_selection_has_lifetime_checks_without_saved_grants() { let outputs = serde_json::json!([{"name":"DP-1", "id":1, "identity":null, "enabled":true}]); assert!(selection_unchanged(&[source()], &outputs, &outputs)); - let replacement = - serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]); + let replacement = serde_json::json!([{"name":"DP-1", "id":2, "identity":null, "enabled":true}]); assert!(!selection_unchanged(&[source()], &outputs, &replacement)); assert!(Restore::create("org.test.App", false, &[source()], &outputs, 2).is_none()); } @@ -288,11 +257,7 @@ mod tests { .is_none() ); saved.monitors[1] = saved.monitors[0].clone(); - assert!( - saved - .resolve("org.test.App", false, true, &sources, &outputs) - .is_none() - ); + assert!(saved.resolve("org.test.App", false, true, &sources, &outputs).is_none()); } #[test] diff --git a/portal/src/settings.rs b/portal/src/settings.rs index 3d42214..54eb7d0 100644 --- a/portal/src/settings.rs +++ b/portal/src/settings.rs @@ -1,12 +1,13 @@ +use std::collections::HashMap; +use std::sync::Arc; +use std::time::Duration; + use ferese_config::Document; use ferese_theme::Palette; -use std::{collections::HashMap, sync::Arc, time::Duration}; use tokio::sync::RwLock; -use zbus::{ - Connection, - object_server::SignalEmitter, - zvariant::{OwnedValue, Value}, -}; +use zbus::Connection; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedValue, Value}; const APPEARANCE: &str = "org.freedesktop.appearance"; const PATH: &str = "/org/freedesktop/portal/desktop"; @@ -50,10 +51,7 @@ impl Appearance { fn values(self) -> HashMap { HashMap::from([ ("color-scheme".into(), self.scheme.into()), - ( - "accent-color".into(), - Value::from(self.accent).try_to_owned().unwrap(), - ), + ("accent-color".into(), Value::from(self.accent).try_to_owned().unwrap()), ("contrast".into(), 0u32.into()), ("reduced-motion".into(), self.reduced_motion.into()), ]) @@ -156,12 +154,7 @@ mod tests { #[test] fn namespaces_support_only_trailing_section_wildcards() { - for filter in [ - "", - "org.freedesktop.appearance", - "org.freedesktop.*", - "org.*", - ] { + for filter in ["", "org.freedesktop.appearance", "org.freedesktop.*", "org.*"] { assert!(matches_namespace(&[filter.into()], APPEARANCE)); } for filter in [ @@ -177,7 +170,10 @@ mod tests { #[test] fn reports_ferese_theme_and_reduced_motion() { - let doc = Document::parse("theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }").unwrap(); + let doc = Document::parse( + "theme { colors { surface-base \"#ffffff\"; accent \"#ff8000\"; }; }; animations { enabled #false; }", + ) + .unwrap(); let appearance = Appearance::from_document(Some(&doc)); assert_eq!(appearance.scheme, 2); assert_eq!(appearance.reduced_motion, 1); diff --git a/portal/src/shortcuts.rs b/portal/src/shortcuts.rs index 94eebe5..ae89d4b 100644 --- a/portal/src/shortcuts.rs +++ b/portal/src/shortcuts.rs @@ -1,20 +1,21 @@ -use std::{collections::HashMap, io::Read, process::Stdio, sync::Arc, time::Duration}; +use std::collections::HashMap; +use std::io::Read; +use std::process::Stdio; +use std::sync::Arc; +use std::time::Duration; use serde_json::json; -use tokio::{io::AsyncWriteExt, sync::Mutex}; -use zbus::{ - Connection, - message::Header, - object_server::SignalEmitter, - zvariant::{OwnedObjectPath, OwnedValue, Value}, -}; +use tokio::io::AsyncWriteExt; +use tokio::sync::Mutex; +use zbus::Connection; +use zbus::message::Header; +use zbus::object_server::SignalEmitter; +use zbus::zvariant::{OwnedObjectPath, OwnedValue, Value}; -use crate::{ - backend::{Cancel, Options, authorize}, - bridge::Bridge, - consent::{Prompt, ShortcutField}, - desktop::Requests, -}; +use crate::backend::{Cancel, Options, authorize}; +use crate::bridge::Bridge; +use crate::consent::{Prompt, ShortcutField}; +use crate::desktop::Requests; const PATH: &str = "/org/freedesktop/portal/desktop"; type Shortcuts = Vec<(String, Options)>; @@ -88,10 +89,7 @@ impl GlobalShortcuts { if let Ok(emitter) = SignalEmitter::new(connection, path) { let _ = SessionObject::closed(&emitter).await; } - let _ = connection - .object_server() - .remove::(path) - .await; + let _ = connection.object_server().remove::(path).await; } } @@ -134,25 +132,14 @@ impl GlobalShortcuts { .filter(|field| !field.trigger.trim().is_empty()) .map(|field| json!({"id":field.id,"trigger":field.trigger.trim()})) .collect::>(); - match bridge - .call("portal-shortcuts-register", json!(registration)) - .await - { + match bridge.call("portal-shortcuts-register", json!(registration)).await { Ok(_) => { let mut state = session.state.lock().await; - if session - .cancel - .stopped - .load(std::sync::atomic::Ordering::SeqCst) + if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) || cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) { - if first - || session - .cancel - .stopped - .load(std::sync::atomic::Ordering::SeqCst) - { + if first || session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) { bridge.close(); } else { let previous = previous @@ -161,11 +148,7 @@ impl GlobalShortcuts { .map(|field| json!({"id":field.id,"trigger":field.trigger})) .collect::>(); - if bridge - .call("portal-shortcuts-register", json!(previous)) - .await - .is_err() - { + if bridge.call("portal-shortcuts-register", json!(previous)).await.is_err() { bridge.close(); } } @@ -202,13 +185,7 @@ impl GlobalShortcuts { } } - async fn watch( - &self, - connection: Connection, - path: OwnedObjectPath, - session: Arc, - bridge: Bridge, - ) { + async fn watch(&self, connection: Connection, path: OwnedObjectPath, session: Arc, bridge: Bridge) { let emitter = SignalEmitter::new(&connection, PATH).unwrap(); loop { tokio::select! { @@ -221,11 +198,7 @@ impl GlobalShortcuts { _ => break, }; let mut state = session.state.lock().await; - if session - .cancel - .stopped - .load(std::sync::atomic::Ordering::SeqCst) - { + if session.cancel.stopped.load(std::sync::atomic::Ordering::SeqCst) { break; } if let Some(active) = value["shortcuts"] @@ -234,16 +207,13 @@ impl GlobalShortcuts { { let mut changed = false; for field in &mut state.fields { - if !field.trigger.is_empty() - && !active.iter().any(|item| item["id"] == field.id) - { + if !field.trigger.is_empty() && !active.iter().any(|item| item["id"] == field.id) { field.trigger.clear(); changed = true; } } if changed { - let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields)) - .await; + let _ = Self::shortcuts_changed(&emitter, path.clone(), results(&state.fields)).await; } } for event in value["events"].as_array().into_iter().flatten() { @@ -252,12 +222,9 @@ impl GlobalShortcuts { }; let timestamp = event["timestamp"].as_u64().unwrap_or(0); if event["active"] == true { - let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new()) - .await; + let _ = Self::activated(&emitter, path.clone(), id, timestamp, Options::new()).await; } else { - let _ = - Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new()) - .await; + let _ = Self::deactivated(&emitter, path.clone(), id, timestamp, Options::new()).await; } } } @@ -282,15 +249,11 @@ impl GlobalShortcuts { #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<(u32, Options)> { 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 shortcut session".into(), - )); + return Err(zbus::fdo::Error::InvalidArgs("Invalid shortcut session".into())); } let cancel = self.requests.begin(connection, &header, &handle).await?; let mut sessions = self.sessions.lock().await; @@ -500,9 +463,7 @@ impl SessionObject { #[zbus(header)] header: Header<'_>, ) -> zbus::fdo::Result<()> { if header.sender().map(|sender| sender.as_str()) != Some(self.owner.as_str()) { - return Err(zbus::fdo::Error::AccessDenied( - "Not your shortcut session".into(), - )); + return Err(zbus::fdo::Error::AccessDenied("Not your shortcut session".into())); } self.backend.end(connection, &self.path).await; Ok(()) @@ -547,10 +508,7 @@ fn fields(shortcuts: Shortcuts) -> Result, String> { Ok(fields) } -fn validate_selection( - requested: &[ShortcutField], - selected: &[ShortcutField], -) -> Result<(), String> { +fn validate_selection(requested: &[ShortcutField], selected: &[ShortcutField]) -> Result<(), String> { if selected.len() != requested.len() || selected.iter().zip(requested).any(|(chosen, original)| { chosen.id != original.id @@ -590,10 +548,7 @@ fn reply(response: Result, String>) -> (u32, Options) { match response { Ok(Some(shortcuts)) => ( 0, - HashMap::from([( - "shortcuts".into(), - Value::from(shortcuts).try_into().unwrap(), - )]), + HashMap::from([("shortcuts".into(), Value::from(shortcuts).try_into().unwrap())]), ), Ok(None) => (1, Options::new()), Err(error) => { diff --git a/portal/src/stream.rs b/portal/src/stream.rs index 43df624..1711ba9 100644 --- a/portal/src/stream.rs +++ b/portal/src/stream.rs @@ -1,17 +1,17 @@ //! PipeWire runs outside the compositor. Keep one frame, drop old frames under //! backpressure, and destroy the node when the portal's control pipe closes. -use crate::capture::{Capture, Frame}; -use pipewire::{self as pw, properties::properties, spa}; +use std::io::{Read, Write}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::{Duration, Instant}; + +use pipewire as pw; +use pipewire::properties::properties; +use pipewire::spa; use serde::{Deserialize, Serialize}; use spa::pod::Pod; -use std::{ - io::{Read, Write}, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, - time::{Duration, Instant}, -}; + +use crate::capture::{Capture, Frame}; #[derive(Debug, Serialize, Deserialize)] pub struct Ready { @@ -70,10 +70,7 @@ impl<'a> OutputBuffer<'a> { ) .cast::(); if let Some(header) = header.as_mut() { - let mut now = libc::timespec { - tv_sec: 0, - tv_nsec: 0, - }; + let mut now = libc::timespec { tv_sec: 0, tv_nsec: 0 }; header.pts = if libc::clock_gettime(libc::CLOCK_MONOTONIC, &mut now) == 0 { now.tv_sec * 1_000_000_000 + now.tv_nsec } else { @@ -104,13 +101,10 @@ fn int_property(key: u32, value: i32) -> spa::pod::Property { } } fn pod(object: spa::pod::Object) -> Vec { - spa::pod::serialize::PodSerializer::serialize( - std::io::Cursor::new(Vec::new()), - &spa::pod::Value::Object(object), - ) - .unwrap() - .0 - .into_inner() + spa::pod::serialize::PodSerializer::serialize(std::io::Cursor::new(Vec::new()), &spa::pod::Value::Object(object)) + .unwrap() + .0 + .into_inner() } fn video_format(width: u32, height: u32) -> Vec { @@ -145,11 +139,7 @@ fn video_format(width: u32, height: u32) -> Vec { )) } -pub fn run( - name: String, - cursor: bool, - generation: Option, -) -> Result<(), Box> { +pub fn run(name: String, cursor: bool, generation: Option) -> Result<(), Box> { let stop = Arc::new(AtomicBool::new(false)); let pipe_stop = stop.clone(); std::thread::spawn(move || { @@ -213,14 +203,10 @@ pub fn run( .add_local_listener_with_user_data(data) .state_changed(move |stream, data, _, state| { data.fresh.store(false, Ordering::SeqCst); - data.streaming.store( - matches!(state, pw::stream::StreamState::Streaming), - Ordering::Relaxed, - ); + data.streaming + .store(matches!(state, pw::stream::StreamState::Streaming), Ordering::Relaxed); match state { - pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming - if !data.announced => - { + pw::stream::StreamState::Paused | pw::stream::StreamState::Streaming if !data.announced => { let node = stream.node_id(); if node != u32::MAX { data.announced = true; @@ -257,8 +243,7 @@ pub fn run( || format.size().width == 0 || format.size().height == 0 || (!resizable && (format.size().width, format.size().height) != (width, height)) - || u64::from(format.size().width) * u64::from(format.size().height) * 4 - > 128 * 1024 * 1024 + || u64::from(format.size().width) * u64::from(format.size().height) * 4 > 128 * 1024 * 1024 || format.format() != spa::param::video::VideoFormat::BGRx { data.stop.store(true, Ordering::Relaxed); @@ -271,10 +256,7 @@ pub fn run( spa::param::ParamType::Buffers, int_property(spa::sys::SPA_PARAM_BUFFERS_buffers, 4), int_property(spa::sys::SPA_PARAM_BUFFERS_blocks, 1), - int_property( - spa::sys::SPA_PARAM_BUFFERS_size, - (width * height * 4) as i32 - ), + int_property(spa::sys::SPA_PARAM_BUFFERS_size, (width * height * 4) as i32), int_property(spa::sys::SPA_PARAM_BUFFERS_stride, (width * 4) as i32), int_property(spa::sys::SPA_PARAM_BUFFERS_align, 16) )); @@ -292,10 +274,7 @@ pub fn run( ) )); if stream - .update_params(&mut [ - Pod::from_bytes(&buffers).unwrap(), - Pod::from_bytes(&header).unwrap(), - ]) + .update_params(&mut [Pod::from_bytes(&buffers).unwrap(), Pod::from_bytes(&header).unwrap()]) .is_err() { data.stop.store(true, Ordering::Relaxed); @@ -358,9 +337,7 @@ pub fn run( while !capture_stop.load(Ordering::Relaxed) { let start = Instant::now(); - if capture_streaming.load(Ordering::Relaxed) - || checked.elapsed() >= Duration::from_secs(1) - { + if capture_streaming.load(Ordering::Relaxed) || checked.elapsed() >= Duration::from_secs(1) { checked = Instant::now(); match capture.frame(&name, cursor, &capture_stop, std::mem::take(&mut spare)) { Ok(frame) if resizable || (frame.width == width && frame.height == height) => { @@ -407,9 +384,7 @@ pub fn run( } else { Duration::from_millis(10) }; - mainloop - .loop_() - .iterate(pw::loop_::Timeout::Finite(timeout)); + mainloop.loop_().iterate(pw::loop_::Timeout::Finite(timeout)); if streaming.load(Ordering::Relaxed) && Instant::now() >= next_frame { let _ = stream.trigger_process(); next_frame = Instant::now() + interval; diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..31bf2c9 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1,4 @@ +max_width = 120 +imports_granularity = "Module" +group_imports = "StdExternalCrate" +reorder_imports = true diff --git a/shell/ferese-lock/src/appearance.rs b/shell/ferese-lock/src/appearance.rs index 7a0ed12..4303050 100644 --- a/shell/ferese-lock/src/appearance.rs +++ b/shell/ferese-lock/src/appearance.rs @@ -1,4 +1,5 @@ -use cosmic::{iced::Color, widget::image}; +use cosmic::iced::Color; +use cosmic::widget::image; use ferese_config::Document; #[derive(Clone)] @@ -89,11 +90,7 @@ impl Appearance { } pub fn clock_format(&self) -> &'static str { - if self.twelve_hour { - "%I:%M %p" - } else { - "%H:%M" - } + if self.twelve_hour { "%I:%M %p" } else { "%H:%M" } } pub fn theme(&self) -> cosmic::Theme { @@ -112,14 +109,12 @@ impl Appearance { } fn account_picture(user: &str) -> Option { - let mut candidates = - vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)]; + let mut candidates = vec![std::path::PathBuf::from("/var/lib/AccountsService/icons").join(user)]; if let Some(home) = std::env::var_os("HOME") { candidates.push(std::path::PathBuf::from(home).join(".face")); } candidates.into_iter().find_map(|path| { - let pixels = - cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?; + let pixels = cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)).ok()?; let side = pixels.width().min(pixels.height()); if side == 0 { return None; @@ -132,13 +127,11 @@ fn account_picture(user: &str) -> Option { side, ) .to_image(); - let mut pixels = - ::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3); + let mut pixels = ::image::imageops::resize(&square, 256, 256, ::image::imageops::FilterType::Lanczos3); // Clip the decoded thumbnail, so the avatar stays circular even on // renderers that do not support rounded image clipping. for (x, y, pixel) in pixels.enumerate_pixels_mut() { - let distance = - ((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt(); + let distance = ((x as f32 + 0.5 - 128.).powi(2) + (y as f32 + 0.5 - 128.).powi(2)).sqrt(); let coverage = (128. - distance).clamp(0., 1.); pixel[3] = (pixel[3] as f32 * coverage).round() as u8; } diff --git a/shell/ferese-lock/src/auth.rs b/shell/ferese-lock/src/auth.rs index 4c96f49..f6fbf76 100644 --- a/shell/ferese-lock/src/auth.rs +++ b/shell/ferese-lock/src/auth.rs @@ -1,5 +1,6 @@ //! PAM stays on a worker thread. No password is placed in argv, logs or IPC. use std::ffi::{CStr, CString, c_char, c_int, c_void}; + use zeroize::Zeroizing; #[repr(C)] @@ -14,8 +15,7 @@ struct Response { } #[repr(C)] struct Conversation { - callback: - unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int, + callback: unsafe extern "C" fn(c_int, *mut *const Message, *mut *mut Response, *mut c_void) -> c_int, data: *mut c_void, } struct Credentials { @@ -39,8 +39,7 @@ unsafe extern "C" fn converse( unsafe { *out = std::ptr::null_mut(); let credentials = &mut *(data as *mut Credentials); - let responses = - libc::calloc(count as usize, std::mem::size_of::()) as *mut Response; + let responses = libc::calloc(count as usize, std::mem::size_of::()) as *mut Response; if responses.is_null() { return CONV_ERR; } @@ -51,13 +50,11 @@ unsafe extern "C" fn converse( } else { match (*msg).style { 1 => { - credentials.password_prompts = - credentials.password_prompts.saturating_add(1); - (credentials.password_prompts == 1) - .then_some(credentials.password.as_slice()) + credentials.password_prompts = credentials.password_prompts.saturating_add(1); + (credentials.password_prompts == 1).then_some(credentials.password.as_slice()) } // Never send the password as a second-factor response. 2 => Some(credentials.user.as_bytes_with_nul()), // PAM_PROMPT_ECHO_ON - 3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO + 3 | 4 => Some(&[][..]), // PAM_ERROR_MSG / PAM_TEXT_INFO _ => None, } }; @@ -65,11 +62,7 @@ unsafe extern "C" fn converse( if !bytes.is_empty() { let allocation = libc::malloc(bytes.len()) as *mut c_char; if !allocation.is_null() { - std::ptr::copy_nonoverlapping( - bytes.as_ptr(), - allocation.cast(), - bytes.len(), - ); + std::ptr::copy_nonoverlapping(bytes.as_ptr(), allocation.cast(), bytes.len()); (*responses.add(i)).text = allocation; continue; } @@ -139,12 +132,7 @@ fn authenticate_with(user: &str, password: &str, config: Option<&CStr>) -> bool let Ok(library) = libloading::Library::new("libpam.so.0") else { return false; }; - type Start = unsafe extern "C" fn( - *const c_char, - *const c_char, - *const Conversation, - *mut *mut c_void, - ) -> c_int; + type Start = unsafe extern "C" fn(*const c_char, *const c_char, *const Conversation, *mut *mut c_void) -> c_int; type StartConf = unsafe extern "C" fn( *const c_char, *const c_char, diff --git a/shell/ferese-lock/src/main.rs b/shell/ferese-lock/src/main.rs index 3fba417..5fe1972 100644 --- a/shell/ferese-lock/src/main.rs +++ b/shell/ferese-lock/src/main.rs @@ -4,22 +4,15 @@ mod appearance; mod auth; mod runtime; +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::event::{PlatformSpecific, wayland}; use cosmic::iced::platform_specific::shell::commands::session_lock; -use cosmic::{ - Element, - app::{Core, Settings, Task}, - iced::{ - self, Event, Length, Subscription, - event::{self, PlatformSpecific, wayland}, - window, - }, - theme, - widget::{self, column, container, image, row}, -}; -use std::{ - collections::HashMap, - time::{Duration, Instant}, -}; +use cosmic::iced::{Event, Length, Subscription, event, window}; +use cosmic::widget::{column, container, image, row}; +use cosmic::{Element, iced, theme, widget}; use wayland_client::protocol::wl_output::WlOutput; use zeroize::{Zeroize, Zeroizing}; @@ -31,8 +24,7 @@ fn main() -> iced::Result { ); return Ok(()); } - let preview = args == ["--preview"] - || (args.len() == 3 && args[0] == "--preview" && args[1] == "--config"); + let preview = args == ["--preview"] || (args.len() == 3 && args[0] == "--preview" && args[1] == "--config"); let config = if preview && args.len() == 3 { Some(std::path::PathBuf::from(&args[2])) } else { @@ -201,9 +193,7 @@ impl cosmic::Application for Locker { wayland::Event::Output(..) | wayland::Event::SessionLock(..), )) | Event::Window(window::Event::Opened { .. }) - | Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => { - Some(Message::Event(Box::new(event))) - } + | Event::Keyboard(iced::keyboard::Event::ModifiersChanged(_)) => Some(Message::Event(Box::new(event))), _ => None, }), iced::time::every(Duration::from_secs(1)).map(|_| Message::Tick), @@ -212,9 +202,7 @@ impl cosmic::Application for Locker { fn update(&mut self, message: Message) -> Task { match message { - Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged( - modifiers, - ))) => { + Message::Event(box Event::Keyboard(iced::keyboard::Event::ModifiersChanged(modifiers))) => { self.caps_lock = modifiers.contains(iced::keyboard::Modifiers::CAPS_LOCK); } Message::Tick => { @@ -222,8 +210,7 @@ impl cosmic::Application for Locker { self.clock = now.strftime(self.appearance.clock_format()).to_string(); self.date = now.strftime("%A, %B %-d").to_string(); - if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) - { + if matches!(self.state, AuthState::Rejected(at) if at.elapsed() >= Duration::from_secs(2)) { self.state = AuthState::Ready; } } @@ -242,10 +229,7 @@ impl cosmic::Application for Locker { return focus(); } - if !self.confirmation.confirmed() - || !self.state.may_submit() - || self.password.is_empty() - { + if !self.confirmation.confirmed() || !self.state.may_submit() || self.password.is_empty() { return Task::none(); } @@ -269,9 +253,10 @@ impl cosmic::Application for Locker { self.status = "Password not accepted. Try again."; return focus(); } - Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland( - wayland::Event::Output(event, output), - ))) if !self.preview => { + Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output( + event, + output, + )))) if !self.preview => { if matches!(event, wayland::OutputEvent::Removed) { if let Some(id) = self.outputs.remove(&output) { return session_lock::destroy_lock_surface(id); @@ -284,9 +269,9 @@ impl cosmic::Application for Locker { } } } - Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland( - wayland::Event::SessionLock(event), - ))) if !self.preview => match event { + Message::Event(box Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::SessionLock( + event, + )))) if !self.preview => match event { wayland::SessionLockEvent::Locked => { if self.confirmation.observe() { use std::io::Write; @@ -296,9 +281,11 @@ impl cosmic::Application for Locker { } if !self.started { self.started = true; - return Task::batch(self.outputs.iter().map(|(output, id)| { - session_lock::get_lock_surface(*id, output.clone()) - })); + return Task::batch( + self.outputs + .iter() + .map(|(output, id)| session_lock::get_lock_surface(*id, output.clone())), + ); } } wayland::SessionLockEvent::Focused(..) => return focus(), @@ -353,14 +340,7 @@ impl Locker { fn screen_content(&self, size: iced::Size) -> Element<'_, Message> { let avatar_size = (size.height * 0.08).clamp(56., 88.); let clock_size = (size.height * 0.165) - .min( - size.width - * if self.appearance.twelve_hour { - 0.075 - } else { - 0.12 - }, - ) + .min(size.width * if self.appearance.twelve_hour { 0.075 } else { 0.12 }) .clamp(40., 180.) as u16; let margin = if size.width < 640. { 24. } else { 36. }; let top = (size.height * 0.09).max(24.); @@ -368,9 +348,7 @@ impl Locker { let mut input = widget::text_input("Password", self.password.as_str()) .password() .padding([6, 12]) - .style(ferese_theme::controls::lock_input( - a.radius, a.accent, a.panel, - )) + .style(ferese_theme::controls::lock_input(a.radius, a.accent, a.panel)) .font(a.font) .id(widget::Id::new("password")); @@ -422,15 +400,8 @@ impl Locker { self.label(&self.user, 16).class(iced::Color::WHITE), widget::Space::new().height(6), input, - self.label( - if self.caps_lock { - "Caps Lock is on" - } else { - self.status - }, - 12 - ) - .class(iced::Color::WHITE.scale_alpha(0.68)), + self.label(if self.caps_lock { "Caps Lock is on" } else { self.status }, 12) + .class(iced::Color::WHITE.scale_alpha(0.68)), ] .spacing(12) .align_x(iced::Alignment::Center) @@ -453,17 +424,10 @@ impl Locker { } if a.show_date { - clock = clock.push( - self.label(&self.date, 18) - .class(iced::Color::WHITE.scale_alpha(0.72)), - ); + clock = clock.push(self.label(&self.date, 18).class(iced::Color::WHITE.scale_alpha(0.72))); } - let clock_height = if a.show_clock { - clock_size as f32 * 1.3 - } else { - 0. - } + if a.show_date { 24. } else { 0. }; + let clock_height = if a.show_clock { clock_size as f32 * 1.3 } else { 0. } + if a.show_date { 24. } else { 0. }; let gap = (size.height * 0.44 - top - clock_height).max(24.); let foreground = container( column![ @@ -497,14 +461,12 @@ impl Locker { }) .width(Length::Fill) .height(Length::Fill) - .class(theme::Container::custom(move |_| { - widget::container::Style { - text_color: Some(iced::Color::WHITE.scale_alpha(0.65)), - background: Some(iced::Background::Color(iced::Color::from_rgba( - 0.025, 0.035, 0.06, a.dim, - ))), - ..Default::default() - } + .class(theme::Container::custom(move |_| widget::container::Style { + text_color: Some(iced::Color::WHITE.scale_alpha(0.65)), + background: Some(iced::Background::Color(iced::Color::from_rgba( + 0.025, 0.035, 0.06, a.dim, + ))), + ..Default::default() })); let background: Element<'_, Message> = image(a.wallpaper.clone()) .width(Length::Fill) @@ -524,10 +486,7 @@ fn line_icon(path: &str, size: u16) -> widget::icon::Icon { fn ferese_symbol() -> widget::icon::Icon { widget::icon( - widget::icon::from_svg_bytes( - include_bytes!("../../../packaging/icons/ferese.svg").as_slice(), - ) - .symbolic(true), + widget::icon::from_svg_bytes(include_bytes!("../../../packaging/icons/ferese.svg").as_slice()).symbolic(true), ) .size(30) } diff --git a/shell/ferese-lock/src/runtime.rs b/shell/ferese-lock/src/runtime.rs index a25740d..b274e58 100644 --- a/shell/ferese-lock/src/runtime.rs +++ b/shell/ferese-lock/src/runtime.rs @@ -1,15 +1,12 @@ //! Startup handshake: the invoking command exits successfully only after the //! compositor's confirmation. The UI child remains alive for authentication. -use std::{ - io::{BufRead, BufReader}, - process::{Command, Stdio}, - time::Duration, -}; -use wayland_client::{ - Connection, Dispatch, QueueHandle, - globals::{GlobalListContents, registry_queue_init}, - protocol::wl_registry, -}; +use std::io::{BufRead, BufReader}; +use std::process::{Command, Stdio}; +use std::time::Duration; + +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::wl_registry; +use wayland_client::{Connection, Dispatch, QueueHandle}; struct Registry; impl Dispatch for Registry { fn event( @@ -23,10 +20,8 @@ impl Dispatch for Registry { } } pub fn check_protocol() -> Result<(), String> { - let connection = - Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?; - let (globals, _) = registry_queue_init::(&connection) - .map_err(|e| format!("Wayland registry: {e}"))?; + let connection = Connection::connect_to_env().map_err(|e| format!("Wayland connection: {e}"))?; + let (globals, _) = registry_queue_init::(&connection).map_err(|e| format!("Wayland registry: {e}"))?; if globals .contents() .clone_list() diff --git a/shell/ferese-polkit-agent/src/main.rs b/shell/ferese-polkit-agent/src/main.rs index 5b3d113..2302bf2 100644 --- a/shell/ferese-polkit-agent/src/main.rs +++ b/shell/ferese-polkit-agent/src/main.rs @@ -1,14 +1,11 @@ mod prompt; mod session; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; + use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - sync::{ - Arc, Mutex, - atomic::{AtomicBool, Ordering}, - }, -}; use zbus::zvariant::{OwnedValue, Value}; const AGENT_PATH: &str = "/dev/ferese/PolkitAgent"; @@ -45,9 +42,8 @@ impl Agent { cookie: String, identities: Vec, ) -> zbus::fdo::Result<()> { - let uid = choose_identity(&identities).ok_or_else(|| { - zbus::fdo::Error::Failed("No supported authentication identity".into()) - })?; + let uid = choose_identity(&identities) + .ok_or_else(|| zbus::fdo::Error::Failed("No supported authentication identity".into()))?; let cancelled = Arc::new(AtomicBool::new(false)); { @@ -60,9 +56,7 @@ impl Agent { let result = tokio::task::spawn_blocking({ let cookie = cookie.clone(); - move || { - session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled) - } + move || session::authenticate(uid, message.chars().take(240).collect(), cookie, cancelled) }) .await .map_err(|error| zbus::fdo::Error::Failed(error.to_string()))?; @@ -116,10 +110,7 @@ fn session_subject() -> Result<(String, HashMap>), String .map_err(|error| error.to_string())?; let mut details = HashMap::new(); details.insert("pid".to_owned(), Value::from(std::process::id())); - details.insert( - "uid".to_owned(), - Value::from(unsafe { libc::geteuid() } as i32), - ); + details.insert("uid".to_owned(), Value::from(unsafe { libc::geteuid() } as i32)); details.insert("start-time".to_owned(), Value::from(started)); Ok(("unix-process".into(), details)) } @@ -130,15 +121,11 @@ async fn run_agent() -> Result<(), Box> { .serve_at(AGENT_PATH, Agent::default())? .build() .await?; - let authority = - zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?; + let authority = zbus::Proxy::new(&connection, AUTHORITY, AUTHORITY_PATH, AUTHORITY_INTERFACE).await?; let locale = std::env::var("LANG").unwrap_or_default(); authority - .call::<_, _, ()>( - "RegisterAuthenticationAgent", - &(subject, locale, AGENT_PATH), - ) + .call::<_, _, ()>("RegisterAuthenticationAgent", &(subject, locale, AGENT_PATH)) .await?; std::future::pending::<()>().await; @@ -174,9 +161,6 @@ mod tests { HashMap::from([("uid".to_owned(), OwnedValue::from(uid))]), ) }; - assert_eq!( - choose_identity(&[identity(0), identity(current)]), - Some(current) - ); + assert_eq!(choose_identity(&[identity(0), identity(current)]), Some(current)); } } diff --git a/shell/ferese-polkit-agent/src/prompt.rs b/shell/ferese-polkit-agent/src/prompt.rs index f7c89a2..ccb946e 100644 --- a/shell/ferese-polkit-agent/src/prompt.rs +++ b/shell/ferese-polkit-agent/src/prompt.rs @@ -1,19 +1,16 @@ -use crate::PromptEvent; -use cosmic::{ - Element, - app::{Core, Settings, Task}, - iced::{Alignment, Color, Length, Size, Subscription, window}, - theme, - widget::{self, button, column, container, row}, -}; -use std::{ - collections::VecDeque, - io::{BufRead, BufReader, Write}, - sync::{Arc, Mutex}, - time::Duration, -}; +use std::collections::VecDeque; +use std::io::{BufRead, BufReader, Write}; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::{Alignment, Color, Length, Size, Subscription, window}; +use cosmic::widget::{button, column, container, row}; +use cosmic::{Element, theme, widget}; use zeroize::{Zeroize, Zeroizing}; +use crate::PromptEvent; + #[derive(Clone)] struct Appearance { surface: Color, @@ -103,9 +100,7 @@ fn content_height(description: &str, status: Option<&str>) -> f32 { .lines() .map(|line| line.chars().count().div_ceil(48).max(1)) .sum(); - let status_height = status.map_or(0, |message| { - 14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3) - }); + let status_height = status.map_or(0, |message| 14 + 18 * message.chars().count().div_ceil(48).clamp(1, 3)); (214 + 18 * lines.saturating_sub(1) + status_height).min(360) as f32 } @@ -163,10 +158,7 @@ fn focus() -> Task { impl Prompt { fn resize_to_content(&mut self) -> Task { - let height = content_height( - &self.description, - self.error.as_deref().or(self.info.as_deref()), - ); + let height = content_height(&self.description, self.error.as_deref().or(self.info.as_deref())); if (height - self.height).abs() < 1.0 { return Task::none(); } @@ -180,12 +172,7 @@ impl Prompt { impl cosmic::Application for Prompt { type Executor = cosmic::executor::Default; - type Flags = ( - Arc>>, - String, - String, - Appearance, - ); + type Flags = (Arc>>, String, String, Appearance); type Message = Message; const APP_ID: &'static str = "dev.ferese.Authentication"; @@ -198,21 +185,14 @@ impl cosmic::Application for Prompt { } fn style(&self) -> Option { - Some( - self.appearance - .palette() - .application_style(if self.material.is_some() { - cosmic::iced::Color::TRANSPARENT - } else { - self.appearance.surface - }), - ) + Some(self.appearance.palette().application_style(if self.material.is_some() { + cosmic::iced::Color::TRANSPARENT + } else { + self.appearance.surface + })) } - fn init( - mut core: Core, - (incoming, description, user, appearance): Self::Flags, - ) -> (Self, Task) { + fn init(mut core: Core, (incoming, description, user, appearance): Self::Flags) -> (Self, Task) { core.window.show_headerbar = false; core.window.border_padding = Some(0); core.window.content_container = false; @@ -257,11 +237,8 @@ impl cosmic::Application for Prompt { fn update(&mut self, message: Message) -> Task { match message { Message::WindowOpened(id) => { - return cosmic::iced::window::run( - id, - ferese_theme::material::ModalMaterial::attach, - ) - .map(|result| cosmic::Action::App(Message::MaterialAttached(result))); + return cosmic::iced::window::run(id, ferese_theme::material::ModalMaterial::attach) + .map(|result| cosmic::Action::App(Message::MaterialAttached(result))); } Message::MaterialAttached(result) => { self.material = result.ok(); @@ -323,9 +300,7 @@ impl cosmic::Application for Prompt { .id(widget::Id::new("auth-response")) .font(palette.font) .padding([7, 12]) - .style(ferese_theme::controls::authentication_input( - palette.palette(), - )); + .style(ferese_theme::controls::authentication_input(palette.palette())); if !self.echo { input = input.password(); } @@ -347,10 +322,7 @@ impl cosmic::Application for Prompt { .height(40) .center_x(40) .center_y(40) - .class(ferese_theme::controls::surface( - palette.accent.scale_alpha(0.13), - 12., - )), + .class(ferese_theme::controls::surface(palette.accent.scale_alpha(0.13), 12.,)), column![ ferese_theme::text("Authentication required", palette.font).size(18), ferese_theme::text(format!("Confirm as {}", self.user), palette.font) @@ -384,10 +356,7 @@ impl cosmic::Application for Prompt { body = body.push( row![ button::custom(ferese_theme::text("Cancel", palette.font).size(13)) - .class(ferese_theme::controls::button_style( - palette.palette(), - false - )) + .class(ferese_theme::controls::button_style(palette.palette(), false)) .padding([8, 14]) .on_press(Message::Cancel), action diff --git a/shell/ferese-polkit-agent/src/session.rs b/shell/ferese-polkit-agent/src/session.rs index 82e4947..39ad782 100644 --- a/shell/ferese-polkit-agent/src/session.rs +++ b/shell/ferese-polkit-agent/src/session.rs @@ -1,19 +1,17 @@ -use crate::PromptEvent; +use std::ffi::{CStr, CString, c_char, c_int, c_void}; +use std::io::{BufRead, BufReader, BufWriter, Write}; +use std::process::{Child, ChildStdin, Command, Stdio}; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::Sender; +use std::sync::{Arc, mpsc}; +use std::time::Duration; + use glib::gobject_ffi; use libloading::Library; -use std::{ - ffi::{CStr, CString, c_char, c_int, c_void}, - io::{BufRead, BufReader, BufWriter, Write}, - process::{Child, ChildStdin, Command, Stdio}, - sync::{ - Arc, - atomic::{AtomicBool, Ordering}, - mpsc::{self, Sender}, - }, - time::Duration, -}; use zeroize::Zeroize; +use crate::PromptEvent; + enum SessionEvent { Request(String, bool), Info(String), @@ -38,21 +36,15 @@ impl Api { unsafe { let agent = Library::new("libpolkit-agent-1.so.0").map_err(|e| e.to_string())?; let polkit = Library::new("libpolkit-gobject-1.so.0").map_err(|e| e.to_string())?; - let user_new = *polkit - .get(b"polkit_unix_user_new\0") - .map_err(|e| e.to_string())?; - let session_new = *agent - .get(b"polkit_agent_session_new\0") - .map_err(|e| e.to_string())?; + let user_new = *polkit.get(b"polkit_unix_user_new\0").map_err(|e| e.to_string())?; + let session_new = *agent.get(b"polkit_agent_session_new\0").map_err(|e| e.to_string())?; let initiate = *agent .get(b"polkit_agent_session_initiate\0") .map_err(|e| e.to_string())?; let response = *agent .get(b"polkit_agent_session_response\0") .map_err(|e| e.to_string())?; - let cancel = *agent - .get(b"polkit_agent_session_cancel\0") - .map_err(|e| e.to_string())?; + let cancel = *agent.get(b"polkit_agent_session_cancel\0").map_err(|e| e.to_string())?; Ok(Self { _agent: agent, @@ -72,9 +64,7 @@ pub fn check() -> Result<(), String> { } fn username(uid: u32) -> String { - let output = Command::new("getent") - .args(["passwd", &uid.to_string()]) - .output(); + let output = Command::new("getent").args(["passwd", &uid.to_string()]).output(); output .ok() .filter(|output| output.status.success()) @@ -122,32 +112,21 @@ fn prompt_process( Ok((child, writer, rx)) } -unsafe extern "C" fn request( - _session: Object, - prompt: *const c_char, - echo: c_int, - data: *mut c_void, -) { +unsafe extern "C" fn request(_session: Object, prompt: *const c_char, echo: c_int, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; - let prompt = unsafe { CStr::from_ptr(prompt) } - .to_string_lossy() - .into_owned(); + let prompt = unsafe { CStr::from_ptr(prompt) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Request(prompt, echo != 0)); } unsafe extern "C" fn info(_session: Object, text: *const c_char, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; - let text = unsafe { CStr::from_ptr(text) } - .to_string_lossy() - .into_owned(); + let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Info(text)); } unsafe extern "C" fn error(_session: Object, text: *const c_char, data: *mut c_void) { let tx = unsafe { &*(data as *const Sender) }; - let text = unsafe { CStr::from_ptr(text) } - .to_string_lossy() - .into_owned(); + let text = unsafe { CStr::from_ptr(text) }.to_string_lossy().into_owned(); let _ = tx.send(SessionEvent::Error(text)); } @@ -156,12 +135,7 @@ unsafe extern "C" fn completed(_session: Object, success: c_int, data: *mut c_vo let _ = tx.send(SessionEvent::Completed(success != 0)); } -unsafe fn connect( - session: Object, - name: &'static CStr, - callback: gobject_ffi::GCallback, - data: *mut c_void, -) { +unsafe fn connect(session: Object, name: &'static CStr, callback: gobject_ffi::GCallback, data: *mut c_void) { unsafe { gobject_ffi::g_signal_connect_data( session.cast(), @@ -215,7 +189,7 @@ fn run_session( unsafe extern "C" fn(Object, *const c_char, *mut c_void), unsafe extern "C" fn(), >( - info as unsafe extern "C" fn(Object, *const c_char, *mut c_void), + info as unsafe extern "C" fn(Object, *const c_char, *mut c_void) )), signal_data, ); @@ -226,7 +200,7 @@ fn run_session( unsafe extern "C" fn(Object, *const c_char, *mut c_void), unsafe extern "C" fn(), >( - error as unsafe extern "C" fn(Object, *const c_char, *mut c_void), + error as unsafe extern "C" fn(Object, *const c_char, *mut c_void) )), signal_data, ); @@ -237,7 +211,7 @@ fn run_session( unsafe extern "C" fn(Object, c_int, *mut c_void), unsafe extern "C" fn(), >( - completed as unsafe extern "C" fn(Object, c_int, *mut c_void), + completed as unsafe extern "C" fn(Object, c_int, *mut c_void) )), signal_data, ); @@ -276,10 +250,7 @@ fn run_session( _ => {} } } - if !stopped - && (cancelled.load(Ordering::Acquire) - || child.try_wait().map_err(|e| e.to_string())?.is_some()) - { + if !stopped && (cancelled.load(Ordering::Acquire) || child.try_wait().map_err(|e| e.to_string())?.is_some()) { unsafe { (api.cancel)(session) }; stopped = true; } @@ -293,12 +264,7 @@ fn run_session( Ok(outcome.filter(|_| !stopped)) } -pub fn authenticate( - uid: u32, - message: String, - cookie: String, - cancelled: Arc, -) -> Result { +pub fn authenticate(uid: u32, message: String, cookie: String, cancelled: Arc) -> Result { let api = Api::load()?; let cookie = CString::new(cookie).map_err(|_| "Invalid authentication cookie")?; let context = glib::MainContext::new(); diff --git a/shell/ferese-settings/src/fonts.rs b/shell/ferese-settings/src/fonts.rs index 53ee052..4d1f543 100644 --- a/shell/ferese-settings/src/fonts.rs +++ b/shell/ferese-settings/src/fonts.rs @@ -1,4 +1,5 @@ -use std::{process::Command, sync::OnceLock}; +use std::process::Command; +use std::sync::OnceLock; pub fn families() -> &'static [String] { static FAMILIES: OnceLock> = OnceLock::new(); diff --git a/shell/ferese-settings/src/main.rs b/shell/ferese-settings/src/main.rs index 47fbca9..d6d2e46 100644 --- a/shell/ferese-settings/src/main.rs +++ b/shell/ferese-settings/src/main.rs @@ -4,22 +4,20 @@ mod store; mod visuals; mod watch; -use cosmic::{ - ApplicationExt, Element, - app::{Core, Settings, Task}, - iced::{Alignment, Length}, - widget::{self, button, column, container, row, scrollable, slider, text_input, toggler}, -}; +use std::collections::HashMap; +use std::path::PathBuf; + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::{Alignment, Length}; +use cosmic::widget::{button, column, container, row, scrollable, slider, text_input, toggler}; +use cosmic::{ApplicationExt, Element, widget}; use schema::{Field, Kind, Page}; -use std::{collections::HashMap, path::PathBuf}; use store::{Edit, Snapshot, set}; fn main() -> cosmic::iced::Result { let mut args = std::env::args_os().skip(1); let path = match args.next() { - Some(arg) if arg == "--config" => { - PathBuf::from(args.next().expect("--config requires a path")) - } + Some(arg) if arg == "--config" => PathBuf::from(args.next().expect("--config requires a path")), Some(_) => { eprintln!("Usage: ferese-settings [--config PATH]"); return Ok(()); @@ -195,9 +193,7 @@ impl cosmic::Application for App { return self.change(set(&format!("desktop_widgets.notes.{index}.text"), value)); } } - Message::AddNote - if !self.saving && self.draft.records("desktop_widgets.notes") < 32 => - { + Message::AddNote if !self.saving && self.draft.records("desktop_widgets.notes") < 32 => { let id = format!( "note-{}", std::time::SystemTime::now() @@ -223,9 +219,7 @@ impl cosmic::Application for App { } Message::DragWindow => return self.core.drag(None), Message::ExternalConfig(result) => match result { - Ok(snapshot) - if snapshot.source == self.current.source - || snapshot.source == self.draft.source => {} + Ok(snapshot) if snapshot.source == self.current.source || snapshot.source == self.draft.source => {} Ok(snapshot) => { if self.saving || self.note_editors.values().any(|editor| editor.dirty) @@ -233,9 +227,7 @@ impl cosmic::Application for App { || !self.ranges.is_empty() || !self.pending.is_empty() { - self.status = - "Config changed externally · Reload when your edits are finished" - .into(); + self.status = "Config changed externally · Reload when your edits are finished".into(); } else { self.current = snapshot.clone(); self.draft = snapshot; @@ -307,9 +299,7 @@ impl cosmic::Application for App { match shlex::split(&value).filter(|v| !v.is_empty()) { Some(args) => set( &field.path, - args.into_iter() - .map(serde_json::Value::from) - .collect::>(), + args.into_iter().map(serde_json::Value::from).collect::>(), ), None => { self.error = Some("Enter a program and arguments with balanced quotes. Commands are not run through a shell.".into()); @@ -348,11 +338,7 @@ impl cosmic::Application for App { } .into(); - return Task::batch([ - self.update_theme(), - self.flush(), - self.load_thumbnail(), - ]); + return Task::batch([self.update_theme(), self.flush(), self.load_thumbnail()]); } Err(error) => { self.error = Some(error); @@ -410,13 +396,20 @@ impl cosmic::Application for App { "--file-filter=All files | *", ]) .output() - .map_err(|error| format!("Could not open the file chooser: {error}. Install zenity or enter an image path.")) + .map_err(|error| { + format!( + "Could not open the file chooser: {error}. Install zenity or enter an image path." + ) + }) .and_then(|output| { if output.status.code() == Some(1) { return Ok(None); // User cancelled. } if !output.status.success() { - return Err(format!("File chooser failed: {}", String::from_utf8_lossy(&output.stderr).trim())); + return Err(format!( + "File chooser failed: {}", + String::from_utf8_lossy(&output.stderr).trim() + )); } let path = String::from_utf8(output.stdout) .map_err(|_| "The image path is not valid UTF-8.".to_owned())?; @@ -458,31 +451,23 @@ impl cosmic::Application for App { return self.load_thumbnail(); } Message::NewCommand(value) => self.new_command = value, - Message::AddCommand if !self.saving => { - match shlex::split(&self.new_command).filter(|a| !a.is_empty()) { - Some(args) => { - self.new_command.clear(); - return self.change(Edit::Add( - "autostart".into(), - vec![ - ( - "command".into(), - args.into_iter() - .map(serde_json::Value::from) - .collect::>() - .into(), - ), - ("enabled".into(), true.into()), - ("restart".into(), true.into()), - ], - )); - } - None => { - self.error = - Some("Enter a program and arguments with balanced quotes.".into()) - } + Message::AddCommand if !self.saving => match shlex::split(&self.new_command).filter(|a| !a.is_empty()) { + Some(args) => { + self.new_command.clear(); + return self.change(Edit::Add( + "autostart".into(), + vec![ + ( + "command".into(), + args.into_iter().map(serde_json::Value::from).collect::>().into(), + ), + ("enabled".into(), true.into()), + ("restart".into(), true.into()), + ], + )); } - } + None => self.error = Some("Enter a program and arguments with balanced quotes.".into()), + }, Message::AddSwipe(keys) if !self.saving => { let (action, argument) = match keys { "Swipe3Up" => ("workspace-next", None), @@ -490,10 +475,7 @@ impl cosmic::Application for App { "Swipe3Left" => ("focus", Some("right")), _ => ("focus", Some("left")), }; - let mut fields = vec![ - ("keys".into(), keys.into()), - ("action".into(), action.into()), - ]; + let mut fields = vec![("keys".into(), keys.into()), ("action".into(), action.into())]; if let Some(argument) = argument { fields.push(("argument".into(), argument.into())); @@ -596,8 +578,7 @@ impl cosmic::Application for App { if !self.search.is_empty() { if !Page::ALL.into_iter().any(|p| p.matches(&self.search)) { - body = body - .push(self.note("No matching settings. Try wallpaper, keyboard, or motion.")); + body = body.push(self.note("No matching settings. Try wallpaper, keyboard, or motion.")); } for page in Page::ALL.into_iter().filter(|p| p.matches(&self.search)) { body = body.push( @@ -682,8 +663,7 @@ impl cosmic::Application for App { .unwrap_or("Choose an image to preview your wallpaper.") })); } - body = - body.push(button::standard("Choose image…").on_press(Message::PickWallpaper)); + body = body.push(button::standard("Choose image…").on_press(Message::PickWallpaper)); } let fields = schema::fields(self.page); @@ -692,30 +672,21 @@ impl cosmic::Application for App { for field in fields { group = group.push(self.field(field)); } - body = body.push( - container(group) - .padding(8) - .class(visuals::surface(palette.card, 14.)), - ); + body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.))); } match self.page { Page::LockScreen => { body = body.push(self.note("Uses your wallpaper, shell colors, font and corner radius from Appearance. Changes apply when the locker or preview opens.")); - body = body.push( - button::standard("Preview lock screen").on_press(Message::PreviewLock), - ); + body = body.push(button::standard("Preview lock screen").on_press(Message::PreviewLock)); body = body.push(self.note("The preview is an ordinary window and does not lock your session. Automatic locking is configured separately in Login items.")); } Page::Desktop => { - let can_change_list = - !self.saving && !self.note_editors.values().any(|e| e.dirty); + let can_change_list = !self.saving && !self.note_editors.values().any(|e| e.dirty); body = body.push(self.label("Sticky notes", 16.)); body = body.push(self.note("Edit here; notes save after you pause typing. Desktop cards stay behind windows and are click-through.")); for index in 0..self.draft.records("desktop_widgets.notes") { - let id = self - .draft - .string(&format!("desktop_widgets.notes.{index}.id"), "note"); + let id = self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note"); let mut group = column([]).spacing(8); for field in schema::note_fields(index) { @@ -729,23 +700,13 @@ impl cosmic::Application for App { .height(160) .font(self.font) .size(14.) - .on_action(move |action| { - Message::NoteAction(id.clone(), action) - }), + .on_action(move |action| Message::NoteAction(id.clone(), action)), ); } - group = - group.push(button::destructive("Remove note").on_press_maybe( - can_change_list.then_some(Message::Remove( - "desktop_widgets.notes".into(), - index, - )), - )); - body = body.push( - container(group) - .padding(12) - .class(visuals::surface(palette.card, 14.)), - ); + group = group.push(button::destructive("Remove note").on_press_maybe( + can_change_list.then_some(Message::Remove("desktop_widgets.notes".into(), index)), + )); + body = body.push(container(group).padding(12).class(visuals::surface(palette.card, 14.))); } body = body.push( button::standard("Add note").on_press_maybe( @@ -783,19 +744,12 @@ impl cosmic::Application for App { Kind::Toggle(true), ))) .push( - container( - button::destructive("Remove item").on_press_maybe( - (!self.saving) - .then_some(Message::Remove("autostart".into(), index)), - ), - ) + container(button::destructive("Remove item").on_press_maybe( + (!self.saving).then_some(Message::Remove("autostart".into(), index)), + )) .padding(12), ); - body = body.push( - container(group) - .padding(8) - .class(visuals::surface(palette.card, 14.)), - ); + body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.))); } body = body.push( row([]) @@ -804,12 +758,9 @@ impl cosmic::Application for App { text_input("Program and arguments", self.new_command.clone()) .on_input(Message::NewCommand), ) - .push( - button::standard("Add login item").on_press_maybe( - (!self.saving && !self.new_command.trim().is_empty()) - .then_some(Message::AddCommand), - ), - ), + .push(button::standard("Add login item").on_press_maybe( + (!self.saving && !self.new_command.trim().is_empty()).then_some(Message::AddCommand), + )), ); } Page::Shortcuts => { @@ -828,10 +779,10 @@ impl cosmic::Application for App { }); if !exists { - gestures = - gestures.push(button::standard(label).on_press_maybe( - (!self.saving).then_some(Message::AddSwipe(keys)), - )); + gestures = gestures.push( + button::standard(label) + .on_press_maybe((!self.saving).then_some(Message::AddSwipe(keys))), + ); } } @@ -859,11 +810,7 @@ impl cosmic::Application for App { "Command name, direction, or action argument.", "", ))); - body = body.push( - container(group) - .padding(8) - .class(visuals::surface(palette.card, 14.)), - ); + body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.))); } if self.draft.records("bindings") == 0 { body = body.push(self.note("You are using the built-in shortcuts. Add custom bindings in config.kdl; they will appear here after Reload.")); @@ -873,29 +820,44 @@ impl cosmic::Application for App { body = body.push(self.note("Display profiles update connected outputs live. Keep your config open for adding profiles or changing display positions.")); for profile in 0..self.draft.records("output_profiles") { let prefix = format!("output_profiles.{profile}"); - body = body.push( - self.label( - self.draft - .string(&format!("{prefix}.name"), "Display profile"), - 17., - ), - ); + body = + body.push(self.label(self.draft.string(&format!("{prefix}.name"), "Display profile"), 17.)); for output in 0..self.draft.records(&format!("{prefix}.outputs")) { let prefix = format!("{prefix}.outputs.{output}"); - let group = column([]).spacing(1) - .push(self.field(schema::text(format!("{prefix}.match"), "Display", "Output name or matching pattern.", ""))) - .push(self.field(schema::text(format!("{prefix}.mode"), "Resolution", "For example: 2560x1440@60. Leave unchanged to retain automatic selection.", ""))) - .push(self.field(schema::range(format!("{prefix}.scale"), "Scale", "Logical size of text and controls.", 1., 0.75, 3., 0.25, "×", false))); - body = body.push( - container(group) - .padding(8) - .class(visuals::surface(palette.card, 14.)), - ); + let group = column([]) + .spacing(1) + .push(self.field(schema::text( + format!("{prefix}.match"), + "Display", + "Output name or matching pattern.", + "", + ))) + .push(self.field(schema::text( + format!("{prefix}.mode"), + "Resolution", + "For example: 2560x1440@60. Leave unchanged to retain automatic selection.", + "", + ))) + .push(self.field(schema::range( + format!("{prefix}.scale"), + "Scale", + "Logical size of text and controls.", + 1., + 0.75, + 3., + 0.25, + "×", + false, + ))); + body = body.push(container(group).padding(8).class(visuals::surface(palette.card, 14.))); } } if self.draft.records("output_profiles") == 0 { - body = body.push(self.note("No saved profiles. Ferese currently configures connected displays automatically.")); + body = + body.push(self.note( + "No saved profiles. Ferese currently configures connected displays automatically.", + )); } } _ => {} @@ -947,14 +909,7 @@ impl cosmic::Application for App { }, palette.muted, ), - self.label( - if self.saving { - "Saving…" - } else { - &self.status - }, - 11., - ), + self.label(if self.saving { "Saving…" } else { &self.status }, 11.), widget::tooltip::Position::Top, )) .push(widget::Space::new().width(Length::Fill)) @@ -986,21 +941,16 @@ impl cosmic::Application for App { .push(sidebar) .push( container( - container( - column([]) - .push(content.height(Length::Fill)) - .push(footer) - .spacing(6), - ) - .padding(cosmic::iced::Padding { - top: 20., - right: 20., - bottom: 8., - left: 20., - }) - .max_width(840) - .width(Length::Fill) - .height(Length::Fill), + container(column([]).push(content.height(Length::Fill)).push(footer).spacing(6)) + .padding(cosmic::iced::Padding { + top: 20., + right: 20., + bottom: 8., + left: 20., + }) + .max_width(840) + .width(Length::Fill) + .height(Length::Fill), ) .width(Length::Fill) .center_x(Length::Fill) @@ -1022,11 +972,8 @@ impl App { } fn note_index(&self, id: &str) -> Option { - (0..self.draft.records("desktop_widgets.notes")).find(|index| { - self.draft - .string(&format!("desktop_widgets.notes.{index}.id"), "note") - == id - }) + (0..self.draft.records("desktop_widgets.notes")) + .find(|index| self.draft.string(&format!("desktop_widgets.notes.{index}.id"), "note") == id) } fn sync_notes(&mut self) { @@ -1036,14 +983,11 @@ impl App { let prefix = format!("desktop_widgets.notes.{index}"); let id = self.draft.string(&format!("{prefix}.id"), "note"); let text = self.draft.string(&format!("{prefix}.text"), ""); - let editor = self - .note_editors - .entry(id.clone()) - .or_insert_with(|| NoteEditor { - content: widget::text_editor::Content::with_text(&text), - revision: 0, - dirty: false, - }); + let editor = self.note_editors.entry(id.clone()).or_insert_with(|| NoteEditor { + content: widget::text_editor::Content::with_text(&text), + revision: 0, + dirty: false, + }); if !editor.dirty && editor.content.text() != text { editor.content = widget::text_editor::Content::with_text(&text); } @@ -1121,9 +1065,7 @@ impl App { fn field(&self, field: Field) -> Element<'static, Message> { let palette = visuals::Palette::from_document(Some(&self.draft.doc)); - let mut labels = column([]) - .spacing(3) - .push(self.label(field.label.clone(), 13.)); + let mut labels = column([]).spacing(3).push(self.label(field.label.clone(), 13.)); if !field.description.is_empty() { labels = labels.push( self.label(field.description.clone(), 11.) @@ -1205,12 +1147,10 @@ impl App { .align_y(Alignment::Center) .spacing(14) .push( - slider(min..=max, value, move |value| { - Message::Range(field.clone(), value) - }) - .step(step) - .on_release(Message::Release(release)) - .width(145), + slider(min..=max, value, move |value| Message::Range(field.clone(), value)) + .step(step) + .on_release(Message::Release(release)) + .width(145), ) .push(self.label(display, 12.).width(65)) .into() @@ -1254,10 +1194,7 @@ impl App { row([]) .spacing(8) .align_y(Alignment::Center) - .push( - container(widget::Space::new().width(24).height(24)) - .class(visuals::surface(swatch, 6.)), - ) + .push(container(widget::Space::new().width(24).height(24)).class(visuals::surface(swatch, 6.))) .push(input) .into() } else { @@ -1342,8 +1279,9 @@ impl App { #[cfg(test)] mod tests { - use super::*; use cosmic::Application; + + use super::*; fn app() -> App { App::init( Core::default(), @@ -1361,10 +1299,7 @@ mod tests { app.draft .edit(&Edit::Add( "desktop_widgets.notes".into(), - vec![ - ("id".into(), "test".into()), - ("text".into(), "first\nsecond".into()), - ], + vec![("id".into(), "test".into()), ("text".into(), "first\nsecond".into())], )) .unwrap(); app.sync_notes(); @@ -1377,10 +1312,7 @@ mod tests { let _ = app.update(Message::SaveNote("test".into(), 2)); assert!(app.saving); assert!(!app.note_editors["test"].dirty); - assert_eq!( - app.draft.string("desktop_widgets.notes.0.text", ""), - "first\nsecond" - ); + assert_eq!(app.draft.string("desktop_widgets.notes.0.text", ""), "first\nsecond"); } #[test] @@ -1424,8 +1356,7 @@ mod tests { let _ = app.update(Message::ExternalConfig(Ok(external.clone()))); assert_eq!(app.current.source, external.source); assert_eq!(app.draft.number("animations.speed", 0.), 0.5); - app.inputs - .insert("theme.colors.accent".into(), "#ffffff".into()); + app.inputs.insert("theme.colors.accent".into(), "#ffffff".into()); let newer = Snapshot::parse("animations {\n speed 0.8\n}\n".into()).unwrap(); let _ = app.update(Message::ExternalConfig(Ok(newer))); assert_eq!(app.current.source, external.source); diff --git a/shell/ferese-settings/src/schema.rs b/shell/ferese-settings/src/schema.rs index 01c2774..265f866 100644 --- a/shell/ferese-settings/src/schema.rs +++ b/shell/ferese-settings/src/schema.rs @@ -129,12 +129,7 @@ pub struct Field { } impl Field { - pub fn new( - path: impl Into, - label: impl Into, - description: impl Into, - kind: Kind, - ) -> Self { + pub fn new(path: impl Into, label: impl Into, description: impl Into, kind: Kind) -> Self { Self { path: path.into(), label: label.into(), @@ -180,15 +175,7 @@ pub fn text( description: impl Into, default: &'static str, ) -> Field { - Field::new( - path, - label, - description, - Kind::Text { - default, - argv: false, - }, - ) + Field::new(path, label, description, Kind::Text { default, argv: false }) } fn choice( @@ -400,18 +387,8 @@ pub fn note_fields(index: usize) -> Vec { "Drag the title and edit directly. Off makes the card click-through.", Kind::Toggle(true), ), - Field::new( - format!("{prefix}.enabled"), - "Show note", - "", - Kind::Toggle(true), - ), - text( - format!("{prefix}.title"), - "Title", - "Empty hides the title.", - "Note", - ), + Field::new(format!("{prefix}.enabled"), "Show note", "", Kind::Toggle(true)), + text(format!("{prefix}.title"), "Title", "Empty hides the title.", "Note"), text( format!("{prefix}.anchor"), "Position", diff --git a/shell/ferese-settings/src/store.rs b/shell/ferese-settings/src/store.rs index f5d8923..5becca0 100644 --- a/shell/ferese-settings/src/store.rs +++ b/shell/ferese-settings/src/store.rs @@ -1,12 +1,11 @@ +use std::fs; +use std::io::{Read, Write}; +use std::path::{Path, PathBuf}; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + use ferese_config::Document; use serde_json::Value; -use std::{ - fs, - io::{Read, Write}, - path::{Path, PathBuf}, - process::{Command, Stdio}, - time::{Duration, Instant}, -}; pub const LIMIT: usize = 60 * 1024; @@ -45,10 +44,7 @@ impl Snapshot { } pub fn string(&self, path: &str, fallback: &str) -> String { - self.item(path) - .and_then(Value::as_str) - .unwrap_or(fallback) - .to_owned() + self.item(path).and_then(Value::as_str).unwrap_or(fallback).to_owned() } pub fn number(&self, path: &str, fallback: f64) -> f64 { @@ -60,9 +56,7 @@ impl Snapshot { } pub fn records(&self, path: &str) -> usize { - self.item(path) - .and_then(Value::as_array) - .map_or(0, |a| a.len()) + self.item(path).and_then(Value::as_array).map_or(0, |a| a.len()) } pub fn argv(&self, path: &str) -> String { @@ -172,9 +166,7 @@ fn save_with( let snapshot = Snapshot::parse(desired.to_owned())?; let unchanged = || -> Result<(), String> { if Snapshot::read(path)?.source != expected { - return Err( - "Your config changed in another app. Reload settings before saving.".into(), - ); + return Err("Your config changed in another app. Reload settings before saving.".into()); } Ok(()) }; @@ -188,17 +180,13 @@ fn save_with( .set_permissions(metadata.permissions()) .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())?; unchanged()?; // Keep the last working source, including comments and unknown keys. let mut backup = tempfile::NamedTempFile::new_in(parent).map_err(|e| e.to_string())?; - backup - .write_all(expected.as_bytes()) - .map_err(|e| e.to_string())?; + backup.write_all(expected.as_bytes()).map_err(|e| e.to_string())?; backup.as_file().sync_all().map_err(|e| e.to_string())?; backup .persist(path.with_extension("kdl.settings-backup")) @@ -212,8 +200,7 @@ pub fn reload_running() -> bool { let Some(runtime) = std::env::var_os("XDG_RUNTIME_DIR") else { return false; }; - let Ok(mut socket) = - std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock")) + let Ok(mut socket) = std::os::unix::net::UnixStream::connect(PathBuf::from(runtime).join("ferese/control.sock")) else { return false; }; @@ -293,17 +280,11 @@ window-rule app-id="mine" floating=#true #[test] fn nested_array_edits_and_login_items_roundtrip() { - let mut snapshot = Snapshot::parse( - "output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into(), - ) - .unwrap(); - snapshot - .edit(&set("output_profiles.0.outputs.0.scale", 1.75)) - .unwrap(); - assert_eq!( - snapshot.number("output_profiles.0.outputs.0.scale", 1.), - 1.75 - ); + let mut snapshot = + Snapshot::parse("output-profile name=\"desk\" {\n output match=\"HDMI-A-1\" scale=1.5\n}\n".into()) + .unwrap(); + snapshot.edit(&set("output_profiles.0.outputs.0.scale", 1.75)).unwrap(); + assert_eq!(snapshot.number("output_profiles.0.outputs.0.scale", 1.), 1.75); snapshot .edit(&Edit::Add( "autostart".into(), @@ -331,28 +312,18 @@ window-rule app-id="mine" floating=#true #[test] fn nested_note_lists_keep_existing_clock_settings() { - let mut snapshot = Snapshot::parse( - "desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into(), - ) - .unwrap(); + let mut snapshot = + Snapshot::parse("desktop-widgets {\n clock {\n enabled #true\n }\n}\n".into()).unwrap(); snapshot .edit(&Edit::Add( "desktop_widgets.notes".into(), - vec![ - ("id".into(), "first".into()), - ("text".into(), "one\ntwo".into()), - ], + vec![("id".into(), "first".into()), ("text".into(), "one\ntwo".into())], )) .unwrap(); assert_eq!(snapshot.records("desktop_widgets.notes"), 1); assert!(snapshot.boolean("desktop_widgets.clock.enabled", false)); - assert_eq!( - snapshot.string("desktop_widgets.notes.0.text", ""), - "one\ntwo" - ); - snapshot - .edit(&Edit::Remove("desktop_widgets.notes".into(), 0)) - .unwrap(); + assert_eq!(snapshot.string("desktop_widgets.notes.0.text", ""), "one\ntwo"); + snapshot.edit(&Edit::Remove("desktop_widgets.notes".into(), 0)).unwrap(); assert_eq!(snapshot.records("desktop_widgets.notes"), 0); } @@ -364,11 +335,7 @@ window-rule app-id="mine" floating=#true let path = directory.path().join("config.kdl"); let validate = |snapshot: &Snapshot| { fs::write(&path, &snapshot.source).unwrap(); - let result = Command::new(&binary) - .arg("--check-config") - .arg(&path) - .output() - .unwrap(); + let result = Command::new(&binary).arg("--check-config").arg(&path).output().unwrap(); assert!( result.status.success(), "{}\n{}", @@ -386,9 +353,7 @@ window-rule app-id="mine" floating=#true let values: Vec = match field.kind { Kind::Font => vec!["".into(), "sans-serif".into()], Kind::Toggle(_) => vec![true.into(), false.into()], - Kind::Range { - min, max, integer, .. - } => { + Kind::Range { min, max, integer, .. } => { if integer { vec![(min as i64).into(), (max as i64).into()] } else { @@ -396,9 +361,7 @@ window-rule app-id="mine" floating=#true } } Kind::Text { default, .. } => vec![default.into()], - Kind::Choice { choices, .. } => { - choices.iter().map(|(v, _)| (*v).into()).collect() - } + Kind::Choice { choices, .. } => choices.iter().map(|(v, _)| (*v).into()).collect(), }; for value in values { let mut snapshot = Snapshot::parse(String::new()).unwrap(); diff --git a/shell/ferese-settings/src/visuals.rs b/shell/ferese-settings/src/visuals.rs index 3b96fa2..714cdcf 100644 --- a/shell/ferese-settings/src/visuals.rs +++ b/shell/ferese-settings/src/visuals.rs @@ -1,19 +1,13 @@ -use crate::{ - Message, - schema::Page, - store::{Edit, Snapshot, set}, -}; -use cosmic::{ - Element, - iced::{Color, Length}, - widget::icon as svg_icon, -}; - -pub use ferese_theme::controls::{ - button_style, navigation_style, settings_input as input_style, surface, -}; +use cosmic::Element; +use cosmic::iced::{Color, Length}; +use cosmic::widget::icon as svg_icon; +pub use ferese_theme::controls::{button_style, navigation_style, settings_input as input_style, surface}; pub use ferese_theme::{Palette, mix, surface_shade}; +use crate::Message; +use crate::schema::Page; +use crate::store::{Edit, Snapshot, set}; + pub fn color(value: &str, fallback: Color) -> Color { ferese_theme::parse_color(value).unwrap_or(fallback) } @@ -32,9 +26,7 @@ pub fn native_theme(snapshot: Option<&Snapshot>) -> cosmic::Theme { } pub fn configured_font(snapshot: &Snapshot) -> cosmic::font::Font { - ferese_theme::font(Some( - &snapshot.string("theme.typography.font_family", "Inter"), - )) + ferese_theme::font(Some(&snapshot.string("theme.typography.font_family", "Inter"))) } pub fn icon(page: Page, tint: Color) -> svg_icon::Icon { @@ -91,20 +83,14 @@ pub fn preset_selected(snapshot: &Snapshot, index: usize) -> bool { ("theme.surface.bar.text_primary", preset.text, "#F0F3FA"), ("theme.surface.bar.text_muted", preset.muted, "#AAB4C7"), ("theme.focus_ring.gradient.from", preset.accent, "#3D7BE6"), - ( - "theme.focus_ring.gradient.to", - preset.gradient_end, - "#3D7BE6", - ), + ("theme.focus_ring.gradient.to", preset.gradient_end, "#3D7BE6"), ] .into_iter() .all(|(path, expected, fallback)| { if index == 0 && snapshot.item(path).is_none() { return true; } - snapshot - .string(path, fallback) - .eq_ignore_ascii_case(expected) + snapshot.string(path, fallback).eq_ignore_ascii_case(expected) }) && snapshot.number("theme.focus_ring.gradient.angle", 0.) == 0. } @@ -164,19 +150,14 @@ pub fn preview(snapshot: &Snapshot) -> Element<'static, Message> { let accent = hex(p.accent); let muted = hex(p.muted); let gap = snapshot.number("layout.inner_gap", 8.).clamp(0., 32.); - let radius = snapshot - .number("theme.geometry.window_radius", 14.) - .clamp(0., 28.); + let radius = snapshot.number("theme.geometry.window_radius", 14.).clamp(0., 28.); let bar_y = snapshot.number("theme.geometry.top_bar_margin_top", 0.) * 0.5 + 12.; let bar_margin = snapshot.number("theme.geometry.top_bar_margin_horizontal", 0.) * 0.5 + 14.; let bar_height = snapshot.number("theme.geometry.top_bar_height", 30.) * 0.65; let bar_radius = snapshot.number("theme.geometry.shell_radius", 14.) * 0.65; let opacity = if snapshot.string("theme.material.style", "solid") == "translucent" { snapshot - .number( - "theme.material.opacity", - ferese_config::DEFAULT_MATERIAL_OPACITY, - ) + .number("theme.material.opacity", ferese_config::DEFAULT_MATERIAL_OPACITY) .clamp(0., 1.) } else { 1. @@ -235,16 +216,8 @@ mod tests { let snapshot = configured_preset(index); let palette = Palette::from_document(Some(&snapshot.doc)); for background in [palette.background, palette.sidebar, palette.card] { - assert!( - contrast(palette.text, background) >= 4.5, - "{} text", - preset.name - ); - assert!( - contrast(palette.muted, background) >= 4.5, - "{} muted text", - preset.name - ); + assert!(contrast(palette.text, background) >= 4.5, "{} text", preset.name); + assert!(contrast(palette.muted, background) >= 4.5, "{} muted text", preset.name); } for other in PRESETS.iter().skip(index + 1) { assert_ne!(preset.accent, other.accent); @@ -263,17 +236,12 @@ mod tests { .unwrap(); assert!(!preset_selected(&snapshot, index)); let mut snapshot = configured_preset(index); - snapshot - .edit(&set("theme.focus_ring.gradient.to", "#123456")) - .unwrap(); + snapshot.edit(&set("theme.focus_ring.gradient.to", "#123456")).unwrap(); assert!(!preset_selected(&snapshot, index)); } assert!(preset_selected(&Snapshot::parse(String::new()).unwrap(), 0)); assert!(preset(usize::MAX).is_empty()); - assert!(!preset_selected( - &Snapshot::parse(String::new()).unwrap(), - usize::MAX - )); + assert!(!preset_selected(&Snapshot::parse(String::new()).unwrap(), usize::MAX)); } #[test] @@ -283,10 +251,7 @@ mod tests { for (index, item) in PRESETS.iter().enumerate() { let snapshot = configured_preset(index); - assert_eq!( - native_theme(Some(&snapshot)).cosmic().is_dark, - item.name != "Ayu Light" - ); + assert_eq!(native_theme(Some(&snapshot)).cosmic().is_dark, item.name != "Ayu Light"); } } } diff --git a/shell/ferese-settings/src/watch.rs b/shell/ferese-settings/src/watch.rs index 0882a62..c85575a 100644 --- a/shell/ferese-settings/src/watch.rs +++ b/shell/ferese-settings/src/watch.rs @@ -1,8 +1,12 @@ //! Watch the parent directory so atomic editor saves keep working. -use crate::{Message, store::Snapshot}; +use std::path::PathBuf; +use std::time::Duration; + use cosmic::iced::futures::{SinkExt, Stream}; use notify::{RecursiveMode, Watcher}; -use std::{path::PathBuf, time::Duration}; + +use crate::Message; +use crate::store::Snapshot; pub fn changes(path: &PathBuf) -> impl Stream + use<> { let path = path.clone(); @@ -21,9 +25,7 @@ pub fn changes(path: &PathBuf) -> impl Stream + use<> { let mut watcher = match watcher { Ok(watcher) => watcher, Err(error) => { - let _ = output - .send(Message::ExternalConfig(Err(error.to_string()))) - .await; + let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await; return; } }; @@ -32,9 +34,7 @@ pub fn changes(path: &PathBuf) -> impl Stream + use<> { path.parent().unwrap_or(std::path::Path::new(".")), RecursiveMode::NonRecursive, ) { - let _ = output - .send(Message::ExternalConfig(Err(error.to_string()))) - .await; + let _ = output.send(Message::ExternalConfig(Err(error.to_string()))).await; return; } @@ -44,11 +44,7 @@ pub fn changes(path: &PathBuf) -> impl Stream + use<> { let snapshot = tokio::task::spawn_blocking(move || Snapshot::read(&file)) .await .unwrap_or_else(|e| Err(e.to_string())); - if output - .send(Message::ExternalConfig(snapshot)) - .await - .is_err() - { + if output.send(Message::ExternalConfig(snapshot)).await.is_err() { return; } if events.recv().await.is_none() { @@ -64,9 +60,10 @@ pub fn changes(path: &PathBuf) -> impl Stream + use<> { #[cfg(test)] mod tests { - use super::*; use cosmic::iced::futures::{StreamExt, pin_mut}; + use super::*; + #[test] fn watches_atomic_replacement_and_retains_subscription_after_bad_kdl() { tokio::runtime::Builder::new_current_thread() @@ -79,10 +76,7 @@ mod tests { std::fs::write(&path, "animations {\n speed 1\n}\n").unwrap(); let stream = changes(&path); pin_mut!(stream); - assert!(matches!( - stream.next().await, - Some(Message::ExternalConfig(Ok(_))) - )); + assert!(matches!(stream.next().await, Some(Message::ExternalConfig(Ok(_))))); for source in ["bad [", "animations {\n speed 0.75\n}\n"] { let temporary = directory.path().join("replacement"); std::fs::write(&temporary, source).unwrap(); diff --git a/shell/ferese-shell/src/compositor_ipc.rs b/shell/ferese-shell/src/compositor_ipc.rs index 0bc08e4..6360ceb 100644 --- a/shell/ferese-shell/src/compositor_ipc.rs +++ b/shell/ferese-shell/src/compositor_ipc.rs @@ -1,9 +1,8 @@ +use std::os::unix::net::UnixStream; +use std::path::PathBuf; +use std::time::{Duration, Instant}; + use serde_json::{Value, json}; -use std::{ - os::unix::net::UnixStream, - path::PathBuf, - time::{Duration, Instant}, -}; pub(super) fn call(command: &str) -> Result { request(command, json!({})) @@ -33,10 +32,7 @@ impl SessionConnection { } fn request(&self, command: &str, args: Value) -> Result { - let mut stream = self - .stream - .lock() - .map_err(|_| "Compositor connection failed")?; + let mut stream = self.stream.lock().map_err(|_| "Compositor connection failed")?; let request = ferese_ipc::Request { version: ferese_ipc::VERSION, id: 1, @@ -45,8 +41,7 @@ impl SessionConnection { args, }; ferese_ipc::write_frame(&mut *stream, &request).map_err(|error| error.to_string())?; - let response: ferese_ipc::Response = - ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?; + let response: ferese_ipc::Response = ferese_ipc::read_frame(&mut *stream).map_err(|error| error.to_string())?; if let Some(error) = response.error { return Err(error.message); } @@ -89,24 +84,21 @@ impl Approval { } pub fn prepare(&self) -> Result<(), String> { - if matches!(self.mode, Mode::Shutdown | Mode::Suspend) - && login_inhibitors(self.mode)? != self.external - { - return Err( - "Applications changed their inhibitors; review the confirmation again".into(), - ); + if matches!(self.mode, Mode::Shutdown | Mode::Suspend) && login_inhibitors(self.mode)? != self.external { + return Err("Applications changed their inhibitors; review the confirmation again".into()); } if self.mode == Mode::Shutdown { - self.connection.as_ref().ok_or("Missing session-ending lease")?.request( - "validate-session-end", - json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}), - )?; + self.connection + .as_ref() + .ok_or("Missing session-ending lease")? + .request( + "validate-session-end", + json!({"token": self.token, "inhibitor-revision": self.revision, "force": self.force()}), + )?; } else if self.mode == Mode::Suspend { let state = call("get-session-state")?; if state["inhibitor-revision"].as_u64() != Some(u64::from(self.revision)) { - return Err( - "Applications changed their inhibitors; review the confirmation again".into(), - ); + return Err("Applications changed their inhibitors; review the confirmation again".into()); } } Ok(()) @@ -129,19 +121,12 @@ fn login_inhibitors(mode: Mode) -> Result, String> { "org.freedesktop.login1.Manager", ) .map_err(|error| error.to_string())?; - let inhibitors: Vec<(String, String, String, String, u32, u32)> = proxy - .call("ListInhibitors", &()) - .map_err(|error| error.to_string())?; - let what = if mode == Mode::Suspend { - "sleep" - } else { - "shutdown" - }; + let inhibitors: Vec<(String, String, String, String, u32, u32)> = + proxy.call("ListInhibitors", &()).map_err(|error| error.to_string())?; + let what = if mode == Mode::Suspend { "sleep" } else { "shutdown" }; let mut reasons = inhibitors .into_iter() - .filter(|(flags, _, _, inhibition, _, _)| { - inhibition == "block" && flags.split(':').any(|flag| flag == what) - }) + .filter(|(flags, _, _, inhibition, _, _)| inhibition == "block" && flags.split(':').any(|flag| flag == what)) .map(|(_, app, reason, _, _, _)| format!("{app}: {reason}")) .collect::>(); reasons.sort(); @@ -181,9 +166,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result { } } let flags = if mode == Mode::Suspend { 4 } else { 1 }; - let entries = state["inhibitors"] - .as_array() - .ok_or("Invalid inhibitor list")?; + let entries = state["inhibitors"].as_array().ok_or("Invalid inhibitor list")?; let mut reasons = entries .iter() .filter(|entry| entry["flags"].as_u64().unwrap_or(0) & flags != 0) @@ -192,9 +175,7 @@ pub(super) fn inhibitors(mode: Mode) -> Result { .as_str() .filter(|app| !app.is_empty()) .unwrap_or("Application"); - let reason = entry["reason"] - .as_str() - .unwrap_or("Requested session inhibition"); + let reason = entry["reason"].as_str().unwrap_or("Requested session inhibition"); format!("{app}: {reason}") }) .collect::>(); diff --git a/shell/ferese-shell/src/config.rs b/shell/ferese-shell/src/config.rs index b6f7b19..d07332e 100644 --- a/shell/ferese-shell/src/config.rs +++ b/shell/ferese-shell/src/config.rs @@ -93,8 +93,7 @@ impl Default for ShellTheme { impl ShellTheme { pub(crate) fn palette(self) -> ferese_theme::Palette { - let color = - |[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.); + let color = |[r, g, b, a]: [u8; 4]| cosmic::iced::Color::from_rgba8(r, g, b, f32::from(a) / 255.); let surface = color(self.surface_base); ferese_theme::Palette { background: surface, @@ -330,14 +329,8 @@ pub(crate) fn load() -> ShellConfig { pub(crate) fn parse_source(source: &str) -> Result { match ferese_config::from_str::(source) { Ok(config) => { - config - .notifications - .validate() - .map_err(ferese_config::Error::from)?; - config - .desktop_widgets - .validate() - .map_err(ferese_config::Error::from)?; + config.notifications.validate().map_err(ferese_config::Error::from)?; + config.desktop_widgets.validate().map_err(ferese_config::Error::from)?; let mut theme = shell_theme(&config.theme); theme.material_radius = nonnegative_or( config @@ -387,19 +380,15 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme { ShellTheme { material_radius: defaults.material_radius, bar_background: { - let mut background = - parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background); + let mut background = parse_color(&theme.surface.bar.background).unwrap_or(defaults.bar_background); background[3] = alpha; background }, - bar_text_primary: parse_color(&theme.surface.bar.text_primary) - .unwrap_or(defaults.bar_text_primary), - bar_text_muted: parse_color(&theme.surface.bar.text_muted) - .unwrap_or(defaults.bar_text_muted), + bar_text_primary: parse_color(&theme.surface.bar.text_primary).unwrap_or(defaults.bar_text_primary), + bar_text_muted: parse_color(&theme.surface.bar.text_muted).unwrap_or(defaults.bar_text_muted), surface_base: parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base), surface_popover: { - let mut color = - parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base); + let mut color = parse_color(&theme.colors.surface_base).unwrap_or(defaults.surface_base); color[3] = alpha; color }, @@ -424,8 +413,7 @@ fn shell_theme(theme: &ThemeConfig) -> ShellTheme { control_gap: nonnegative_or(theme.geometry.control_gap, defaults.control_gap), shadow_offset_y: finite_or(theme.shadow.soft.offset_y, defaults.shadow_offset_y), shadow_blur: nonnegative_or(theme.shadow.soft.blur, defaults.shadow_blur), - shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity) - .clamp(0.0, 1.0), + shadow_opacity: finite_or(theme.shadow.soft.opacity, defaults.shadow_opacity).clamp(0.0, 1.0), } } @@ -556,16 +544,8 @@ mod tests { .clock; assert!(clock.enabled); assert_eq!(clock.anchor, ferese_core::desktop::Anchor::BottomRight); - assert!( - super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n") - .is_err() - ); - assert!( - super::parse_source( - "desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n" - ) - .is_err() - ); + assert!(super::parse_source("desktop-widgets {\n clock {\n opacity 1.1\n }\n}\n").is_err()); + assert!(super::parse_source("desktop-widgets {\n clock {\n time-format \"%\"\n }\n}\n").is_err()); } use super::*; @@ -603,10 +583,7 @@ theme { config.theme.typography.font_family.as_deref(), Some("JetBrainsMono Nerd Font") ); - assert_eq!( - config.theme.background.path, - Some(PathBuf::from("/tmp/wallpaper.png")) - ); + assert_eq!(config.theme.background.path, Some(PathBuf::from("/tmp/wallpaper.png"))); assert_eq!(config.theme.background.mode, WallpaperMode::Fit); } diff --git a/shell/ferese-shell/src/control.rs b/shell/ferese-shell/src/control.rs index d6c0f36..21ea44c 100644 --- a/shell/ferese-shell/src/control.rs +++ b/shell/ferese-shell/src/control.rs @@ -1,22 +1,16 @@ -use std::{ - error::Error, - os::{ - fd::{FromRawFd, RawFd}, - unix::net::UnixStream, - }, - sync::mpsc::{self, Receiver, Sender, TryRecvError}, - thread, -}; +use std::error::Error; +use std::os::fd::{FromRawFd, RawFd}; +use std::os::unix::net::UnixStream; +use std::sync::mpsc; +use std::sync::mpsc::{Receiver, Sender, TryRecvError}; +use std::thread; -use ferese_protocols::shell::v1::client::{ - ferese_shell_manager_v1::FereseShellManagerV1, - ferese_shell_v1::{self, FereseShellV1}, -}; -use wayland_client::{ - Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop, - globals::{GlobalListContents, registry_queue_init}, - protocol::wl_registry, -}; +use ferese_protocols::shell::v1::client::ferese_shell_manager_v1::FereseShellManagerV1; +use ferese_protocols::shell::v1::client::ferese_shell_v1; +use ferese_protocols::shell::v1::client::ferese_shell_v1::FereseShellV1; +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::wl_registry; +use wayland_client::{Connection, Dispatch, Proxy, QueueHandle, WEnum, delegate_noop}; #[derive(Clone, Debug, Default)] pub(crate) struct ShellSnapshot { @@ -116,11 +110,7 @@ impl ShellControl { match self.updates.try_recv() { Ok(ControlUpdate::Logout(serial, output)) => poll.logout = Some((serial, output)), Ok(ControlUpdate::LogoutCancelled(serial)) => { - if poll - .logout - .as_ref() - .is_some_and(|(pending, _)| *pending == serial) - { + if poll.logout.as_ref().is_some_and(|(pending, _)| *pending == serial) { poll.logout = None; } poll.logout_cancelled.push(serial); @@ -174,8 +164,8 @@ impl ShellControl { } fn control_connection() -> Result> { - let raw_fd = std::env::var_os("FERESE_SHELL_CONTROL_SOCKET") - .ok_or("Ferese did not provide a shell-control connection")?; + let raw_fd = + std::env::var_os("FERESE_SHELL_CONTROL_SOCKET").ok_or("Ferese did not provide a shell-control connection")?; let fd = raw_fd .to_str() .ok_or("FERESE_SHELL_CONTROL_SOCKET is not valid UTF-8")? @@ -299,26 +289,14 @@ impl Dispatch for ControlState { fullscreen, }); } - ferese_shell_v1::Event::SnapshotEnd { serial } - if state.serial.take() == Some(serial) => - { + ferese_shell_v1::Event::SnapshotEnd { serial } if state.serial.take() == Some(serial) => { state.pending.workspaces.sort_unstable_by_key(|workspace| { - ferese_core::workspace_order_key( - &workspace.name, - ferese_core::WorkspaceId(workspace.id), - ) + ferese_core::workspace_order_key(&workspace.name, ferese_core::WorkspaceId(workspace.id)) }); - let _ = state - .sender - .send(ControlUpdate::Snapshot(state.pending.clone())); + let _ = state.sender.send(ControlUpdate::Snapshot(state.pending.clone())); } - ferese_shell_v1::Event::LogoutRequested { - serial, - output_name, - } => { - let _ = state - .sender - .send(ControlUpdate::Logout(serial, output_name)); + ferese_shell_v1::Event::LogoutRequested { serial, output_name } => { + let _ = state.sender.send(ControlUpdate::Logout(serial, output_name)); } ferese_shell_v1::Event::LogoutCancelled { serial } => { let _ = state.sender.send(ControlUpdate::LogoutCancelled(serial)); diff --git a/shell/ferese-shell/src/keybinding_guide.rs b/shell/ferese-shell/src/keybinding_guide.rs index 9878175..03de85b 100644 --- a/shell/ferese-shell/src/keybinding_guide.rs +++ b/shell/ferese-shell/src/keybinding_guide.rs @@ -6,8 +6,7 @@ pub(super) struct Entry { pub(super) fn load() -> Result, String> { let entries: Vec = - serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?) - .map_err(|error| error.to_string())?; + serde_json::from_value(crate::compositor_ipc::call("get-keybindings")?).map_err(|error| error.to_string())?; if entries.len() > 256 || entries .iter() diff --git a/shell/ferese-shell/src/main.rs b/shell/ferese-shell/src/main.rs index 60ae127..9fb762d 100644 --- a/shell/ferese-shell/src/main.rs +++ b/shell/ferese-shell/src/main.rs @@ -11,40 +11,35 @@ mod status; mod status_ui; mod system_modal; -use cosmic::Element; -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::alignment; -use cosmic::iced::event::{self, PlatformSpecific, wayland}; -use cosmic::iced::platform_specific::{ - runtime::wayland::layer_surface::{IcedMargin, IcedOutput, SctkLayerSurfaceSettings}, - shell::commands::layer_surface::{Anchor, KeyboardInteractivity, Layer}, - shell::commands::layer_surface::{ - destroy_layer_surface, set_anchor, set_exclusive_zone, set_input_zone, set_margin, set_size, - }, -}; -use cosmic::iced::{ - Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, window, -}; -use cosmic::theme; -use cosmic::widget::{button, container, icon, image, row}; -use ferese_protocols::effects::v1::client::{ - ferese_effects_manager_v1::FereseEffectsManagerV1, - ferese_surface_effects_v1::{self, FereseSurfaceEffectsV1}, -}; -use jiff::Zoned; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; -use wayland_client::{ - Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop, - globals::{GlobalListContents, registry_queue_init}, - protocol::{wl_output, wl_registry, wl_surface}, + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::event::{PlatformSpecific, wayland}; +use cosmic::iced::platform_specific::runtime::wayland::layer_surface::{ + IcedMargin, IcedOutput, SctkLayerSurfaceSettings, }; +use cosmic::iced::platform_specific::shell::commands::layer_surface::{ + Anchor, KeyboardInteractivity, Layer, destroy_layer_surface, set_anchor, set_exclusive_zone, set_input_zone, + set_margin, set_size, +}; +use cosmic::iced::{ + Background, Border, Color, ContentFit, Event, Length, Limits, Subscription, alignment, event, window, +}; +use cosmic::widget::{button, container, icon, image, row}; +use cosmic::{Element, theme}; +use ferese_protocols::effects::v1::client::ferese_effects_manager_v1::FereseEffectsManagerV1; +use ferese_protocols::effects::v1::client::ferese_surface_effects_v1; +use ferese_protocols::effects::v1::client::ferese_surface_effects_v1::FereseSurfaceEffectsV1; +use ferese_theme::calendar::{format_bar_time, stacked_digits as stacked_clock_digits}; +use ferese_theme::icons::{accented as accented_icon, tinted as bar_icon}; +use jiff::Zoned; +use wayland_client::globals::{GlobalListContents, registry_queue_init}; +use wayland_client::protocol::{wl_output, wl_registry, wl_surface}; +use wayland_client::{Connection, Dispatch, EventQueue, Proxy, QueueHandle, delegate_noop}; use crate::config::{ShellConfig, ShellTheme, WallpaperMode}; use crate::control::{ShellControl, ShellSnapshot}; -use ferese_theme::calendar::format_bar_time; -use ferese_theme::calendar::stacked_digits as stacked_clock_digits; -use ferese_theme::icons::{accented as accented_icon, tinted as bar_icon}; const APP_ID: &str = "dev.ferese.Shell"; @@ -52,8 +47,7 @@ const APP_ID: &str = "dev.ferese.Shell"; // that compatibility path gated; current backends use Window::Opened and // window::run to retrieve handles without updates at the display refresh rate. static EFFECT_FRAME_PENDING: AtomicBool = AtomicBool::new(true); -static SHELL_FONT: std::sync::OnceLock> = - std::sync::OnceLock::new(); +static SHELL_FONT: std::sync::OnceLock> = std::sync::OnceLock::new(); fn configured_font(family: Option<&str>) -> cosmic::font::Font { ferese_theme::font(family) @@ -133,12 +127,9 @@ fn main() -> cosmic::iced::Result { let wallpaper = config.wallpaper.path.clone().map(|path| { let (sender, receiver) = cosmic::iced::futures::channel::oneshot::channel(); std::thread::spawn(move || { - let result = - cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)) - .map(|pixels| { - image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw()) - }) - .map_err(|error| format!("{error:?}")); + let result = cosmic::iced::advanced::graphics::image::load(&image::Handle::from_path(path)) + .map(|pixels| image::Handle::from_rgba(pixels.width(), pixels.height(), pixels.into_raw())) + .map_err(|error| format!("{error:?}")); let _ = sender.send(result); }); receiver @@ -155,8 +146,7 @@ fn main() -> cosmic::iced::Result { cosmic::app::run::(settings, (config, wallpaper)) } -type WallpaperLoad = - cosmic::iced::futures::channel::oneshot::Receiver>; +type WallpaperLoad = cosmic::iced::futures::channel::oneshot::Receiver>; struct FereseShell { core: Core, @@ -231,10 +221,7 @@ enum Message { NotesStored(Result<(), String>), WallpaperLoaded(Result), Event(Event, window::Id), - NativeSurface( - window::Id, - Result<(Connection, wl_surface::WlSurface), String>, - ), + NativeSurface(window::Id, Result<(Connection, wl_surface::WlSurface), String>), Tick, ActivateWorkspace(u64), ToggleOverview, @@ -284,11 +271,7 @@ impl cosmic::Application for FereseShell { fn init(core: Core, (config, wallpaper): Self::Flags) -> (Self, Task) { let bar_surface_id = window::Id::unique(); - let desktop_clock = config - .desktop_widgets - .clock - .labels(&Zoned::now()) - .unwrap_or_default(); + let desktop_clock = config.desktop_widgets.clock.labels(&Zoned::now()).unwrap_or_default(); let app = Self { core, bar_surface_id, @@ -328,9 +311,7 @@ impl cosmic::Application for FereseShell { let wallpaper_task = match wallpaper { Some(receiver) => cosmic::task::future(async move { cosmic::Action::App(Message::WallpaperLoaded( - receiver - .await - .unwrap_or_else(|error| Err(error.to_string())), + receiver.await.unwrap_or_else(|error| Err(error.to_string())), )) }), None => Task::none(), @@ -350,40 +331,30 @@ impl cosmic::Application for FereseShell { } else { Subscription::none() }, - self.notifications - .subscription() - .map(Message::NotificationEvent), + self.notifications.subscription().map(Message::NotificationEvent), if self.notifications.has_toasts() { - cosmic::iced::time::every(Duration::from_millis( - if self.notifications.animating() { - 16 - } else { - 250 - }, - )) + cosmic::iced::time::every(Duration::from_millis(if self.notifications.animating() { + 16 + } else { + 250 + })) .map(|_| Message::NotificationTick) } else { Subscription::none() }, event::listen_with(|event, _status, id| match &event { - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(..))) => { - EFFECT_FRAME_PENDING - .load(Ordering::Relaxed) - .then_some(Message::Event(event, id)) - } + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(..))) => EFFECT_FRAME_PENDING + .load(Ordering::Relaxed) + .then_some(Message::Event(event, id)), Event::Keyboard(_) | Event::Window(window::Event::Opened { .. } | window::Event::Closed) | Event::PlatformSpecific(PlatformSpecific::Wayland( - wayland::Event::Popup(..) - | wayland::Event::Layer(..) - | wayland::Event::Output(..), + wayland::Event::Popup(..) | wayland::Event::Layer(..) | wayland::Event::Output(..), )) => Some(Message::Event(event, id)), _ => None, }), cosmic::iced::time::every(Duration::from_millis(500)).map(|_| Message::Tick), - self.status_service - .subscription() - .map(Message::StatusUpdated), + self.status_service.subscription().map(Message::StatusUpdated), if self.config.desktop_widgets.clock.enabled || self .config @@ -441,13 +412,7 @@ impl cosmic::Application for FereseShell { Message::BeginNoteEdit(id) => { let mut tasks = vec![self.finish_note_edit()]; - if let Some(note) = self - .config - .desktop_widgets - .notes - .iter() - .find(|note| note.id == id) - { + if let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) { self.note_editor = Some(DesktopNoteEditor { id, content: cosmic::widget::text_editor::Content::with_text(¬e.text), @@ -514,9 +479,7 @@ impl cosmic::Application for FereseShell { } Message::NativeSurface(id, result) => { match result { - Ok((_connection, surface)) - if self.outputs.iter().any(|entry| entry.bar == id) => - { + Ok((_connection, surface)) if self.outputs.iter().any(|entry| entry.bar == id) => { self.attach_effects(id, &surface) } Ok((_connection, surface)) => { @@ -527,9 +490,7 @@ impl cosmic::Application for FereseShell { { match EffectsBinding::attach_role(&surface, None, 1.0) { Ok(binding) => entry.effects = Some(binding), - Err(error) => eprintln!( - "ferese-shell: notification effects unavailable: {error}" - ), + Err(error) => eprintln!("ferese-shell: notification effects unavailable: {error}"), } } @@ -537,11 +498,7 @@ impl cosmic::Application for FereseShell { && menu.id == id && menu.effects.is_none() { - match EffectsBinding::attach_role( - &surface, - menu.kind.material_role(), - menu.progress(), - ) { + match EffectsBinding::attach_role(&surface, menu.kind.material_role(), menu.progress()) { Ok(binding) => menu.effects = Some(binding), Err(error) => { eprintln!("ferese-shell: popover material unavailable: {error}") @@ -676,10 +633,7 @@ impl cosmic::Application for FereseShell { if let Err(error) = result { eprintln!("ferese-shell: keybinding guide unavailable: {error}"); } - if self.guide_attempts < 5 - && self.config.status.keybinding_guide - && !self.guide_shown - { + if self.guide_attempts < 5 && self.config.status.keybinding_guide && !self.guide_shown { Task::perform( async { tokio::time::sleep(Duration::from_secs(2)).await; @@ -717,10 +671,7 @@ impl cosmic::Application for FereseShell { } if let status::Action::PowerProfile(profile) = action { - self.status_error = self - .status_service - .send(status::Action::PowerProfile(profile)) - .err(); + self.status_error = self.status_service.send(status::Action::PowerProfile(profile)).err(); if self.status_error.is_none() && let Some(profiles) = &mut self.status.power_profiles @@ -740,10 +691,7 @@ impl cosmic::Application for FereseShell { return Task::none(); } status::Action::Notifications => { - return Task::batch([ - self.close_menu(), - self.toggle_notification_history(), - ]); + return Task::batch([self.close_menu(), self.toggle_notification_history()]); } _ => {} } @@ -755,10 +703,7 @@ impl cosmic::Application for FereseShell { self.optimistic_status(&action); } - if matches!( - action, - status::Action::Notifications | status::Action::Settings - ) { + if matches!(action, status::Action::Notifications | status::Action::Settings) { return self.close_menu(); } @@ -817,10 +762,7 @@ impl cosmic::Application for FereseShell { { eprintln!("ferese-shell: could not update panel material: {error}"); } - tasks.push(set_input_zone( - entry.bar, - if hidden { Some(Vec::new()) } else { None }, - )); + tasks.push(set_input_zone(entry.bar, if hidden { Some(Vec::new()) } else { None })); } if self.bar_hidden(self.bar_surface_id) { tasks.push(self.destroy_menu()); @@ -833,10 +775,7 @@ impl cosmic::Application for FereseShell { if let Some((serial, output)) = poll.logout { reload_task = Task::batch([ reload_task, - self.open_system_modal( - system_modal::PowerAction::Logout(serial), - Some(&output), - ), + self.open_system_modal(system_modal::PowerAction::Logout(serial), Some(&output)), ]); } } @@ -908,10 +847,7 @@ impl FereseShell { let old_notes = &self.config.desktop_widgets.notes; let new_notes = &config.desktop_widgets.notes; let notes_changed = old_notes.len() != new_notes.len() - || old_notes - .iter() - .zip(new_notes) - .any(|(old, new)| !old.same_surface(new)); + || old_notes.iter().zip(new_notes).any(|(old, new)| !old.same_surface(new)); let new_clock = &config.desktop_widgets.clock; let clock_changed = old_clock.enabled != new_clock.enabled || old_clock.outputs != new_clock.outputs @@ -939,12 +875,7 @@ impl FereseShell { Task::none() }]; - if !self.config.status.keybinding_guide - && self - .system_modal - .as_ref() - .is_some_and(|modal| modal.is_guide()) - { + if !self.config.status.keybinding_guide && self.system_modal.as_ref().is_some_and(|modal| modal.is_guide()) { tasks.push(self.destroy_system_modal(false)); } @@ -952,12 +883,7 @@ impl FereseShell { tasks.push(cosmic::command::set_theme(theme.palette().native_theme())); } - if clock_changed - && self - .note_drag - .as_ref() - .is_some_and(|drag| drag.id.is_none()) - { + if clock_changed && self.note_drag.as_ref().is_some_and(|drag| drag.id.is_none()) { tasks.push(self.finish_note_drag(false)); } @@ -977,11 +903,7 @@ impl FereseShell { } for entry in self.outputs.iter().filter(|_| geometry_changed) { - tasks.push(set_size( - entry.bar, - None, - Some(theme.bar_height.round() as u32), - )); + tasks.push(set_size(entry.bar, None, Some(theme.bar_height.round() as u32))); tasks.push(set_margin( entry.bar, theme.bar_margin_top, @@ -998,11 +920,7 @@ impl FereseShell { Task::batch(tasks) } - fn output_event( - &mut self, - event: wayland::OutputEvent, - output: wl_output::WlOutput, - ) -> Task { + fn output_event(&mut self, event: wayland::OutputEvent, output: wl_output::WlOutput) -> Task { if matches!(event, wayland::OutputEvent::Removed) { if let Some(index) = self.outputs.iter().position(|entry| entry.output == output) { let entry = self.outputs.remove(index); @@ -1012,14 +930,9 @@ impl FereseShell { Task::none() }; - let mut tasks = vec![ - destroy_layer_surface(entry.bar), - menu, - self.rebuild_system_modal(), - ]; + let mut tasks = vec![destroy_layer_surface(entry.bar), menu, self.rebuild_system_modal()]; if self.note_drag.as_ref().is_some_and(|drag| { - entry.clock == Some(drag.source) - || entry.notes.iter().any(|(_, id)| *id == drag.source) + entry.clock == Some(drag.source) || entry.notes.iter().any(|(_, id)| *id == drag.source) }) { tasks.push(self.finish_note_drag(false)); } @@ -1137,8 +1050,7 @@ impl FereseShell { size_limits: Limits::NONE, // Reserve breathing room below the visible bar; layer-shell // accounts for the top margin separately. - exclusive_zone: (bar.height.round() as i32) - .saturating_add(shell_theme.bar_window_gap), + exclusive_zone: (bar.height.round() as i32).saturating_add(shell_theme.bar_window_gap), }, Some(Box::new(move |app| app.view_layer(bar_surface_id))), ); @@ -1189,17 +1101,11 @@ impl FereseShell { } fn set_note_text(&mut self, id: &str, value: String) -> Task { - if let Some(note) = self - .config - .desktop_widgets - .notes - .iter_mut() - .find(|note| note.id == id) + if let Some(note) = self.config.desktop_widgets.notes.iter_mut().find(|note| note.id == id) && (note.text != value || self.note_error.is_some()) { note.text = value.clone(); - self.note_pending - .push(note_store::Edit::Text(id.to_owned(), value)); + self.note_pending.push(note_store::Edit::Text(id.to_owned(), value)); } self.flush_note_changes() } @@ -1220,22 +1126,18 @@ impl FereseShell { if self.note_drag.is_some() { return Task::none(); } - let Some(entry) = self.outputs.iter().find(|entry| { - entry.clock == Some(surface) || entry.notes.iter().any(|(_, window)| *window == surface) - }) else { + let Some(entry) = self + .outputs + .iter() + .find(|entry| entry.clock == Some(surface) || entry.notes.iter().any(|(_, window)| *window == surface)) + else { return Task::none(); }; let Some(size) = entry.size else { return Task::none(); }; let (origin, dimensions) = if let Some(id) = &id { - let Some(note) = self - .config - .desktop_widgets - .notes - .iter() - .find(|note| ¬e.id == id) - else { + let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| ¬e.id == id) else { return Task::none(); }; (note_origin(note, size), (note.width, note.height)) @@ -1348,10 +1250,8 @@ impl FereseShell { tasks.push(set_margin(id, clock.margin_y, 0, 0, clock.margin_x)); } } - self.note_pending.push(note_store::Edit::ClockPosition( - clock.margin_x, - clock.margin_y, - )); + self.note_pending + .push(note_store::Edit::ClockPosition(clock.margin_x, clock.margin_y)); } if let Some(note) = self @@ -1366,11 +1266,7 @@ impl FereseShell { note.margin_y = drag.position.y.round() as i32; for entry in &self.outputs { - if let Some((_, id)) = entry - .notes - .iter() - .find(|(id, _)| Some(id) == drag.id.as_ref()) - { + if let Some((_, id)) = entry.notes.iter().find(|(id, _)| Some(id) == drag.id.as_ref()) { tasks.push(set_anchor(*id, Anchor::TOP | Anchor::LEFT)); tasks.push(set_margin(*id, note.margin_y, 0, 0, note.margin_x)); } @@ -1400,9 +1296,7 @@ impl FereseShell { } for note in &self.config.desktop_widgets.notes { - if !note.on_output(entry.name.as_deref()) - || entry.notes.iter().any(|(id, _)| id == ¬e.id) - { + if !note.on_output(entry.name.as_deref()) || entry.notes.iter().any(|(id, _)| id == ¬e.id) { continue; } @@ -1414,8 +1308,7 @@ impl FereseShell { let action = cosmic::surface::action::app_layer_shell::( |_| Default::default(), move |_| { - let (anchor, margin) = - widget_placement(note.anchor, note.margin_x, note.margin_y); + let (anchor, margin) = widget_placement(note.anchor, note.margin_x, note.margin_y); SctkLayerSurfaceSettings { id, layer: Layer::Bottom, @@ -1424,11 +1317,7 @@ impl FereseShell { } else { KeyboardInteractivity::None }, - input_zone: if note.interactive { - None - } else { - Some(Vec::new()) - }, + input_zone: if note.interactive { None } else { Some(Vec::new()) }, anchor, margin, output: IcedOutput::Output(output.clone()), @@ -1454,27 +1343,14 @@ impl FereseShell { .as_ref() .is_some_and(|drag| drag.id.as_deref() == Some(id)) { - return container(text("")) - .width(Length::Fill) - .height(Length::Fill) - .into(); + return container(text("")).width(Length::Fill).height(Length::Fill).into(); } self.view_note_content(id, surface) } - fn view_note_content( - &self, - id: &str, - surface: window::Id, - ) -> Element<'_, cosmic::Action> { - let Some(note) = self - .config - .desktop_widgets - .notes - .iter() - .find(|note| note.id == id) - else { + fn view_note_content(&self, id: &str, surface: window::Id) -> Element<'_, cosmic::Action> { + let Some(note) = self.config.desktop_widgets.notes.iter().find(|note| note.id == id) else { return text("").into(); }; let alignment = match note.alignment { @@ -1509,31 +1385,16 @@ impl FereseShell { tint.a *= note.opacity; Some(Background::Color(tint)) }; - let mut body = cosmic::widget::column([]) - .spacing(note.gap) - .align_x(alignment); + let mut body = cosmic::widget::column([]).spacing(note.gap).align_x(alignment); if note.interactive { - let title = ferese_theme::text( - if note.title.is_empty() { - "⋮⋮" - } else { - ¬e.title - }, - font, - ) - .size(note.title_size) - .class(theme::Text::Color(foreground)); + let title = ferese_theme::text(if note.title.is_empty() { "⋮⋮" } else { ¬e.title }, font) + .size(note.title_size) + .class(theme::Text::Color(foreground)); let header = cosmic::widget::mouse_area(container(title).width(Length::Fill)) .interaction(cosmic::iced::mouse::Interaction::Grab) - .on_press(cosmic::Action::App(Message::BeginNoteDrag( - id.to_owned(), - surface, - ))); - let editing = self - .note_editor - .as_ref() - .is_some_and(|editor| editor.id == id); + .on_press(cosmic::Action::App(Message::BeginNoteDrag(id.to_owned(), surface))); + let editing = self.note_editor.as_ref().is_some_and(|editor| editor.id == id); body = body.push( row([]).push(header).push(motion::button( @@ -1580,9 +1441,7 @@ impl FereseShell { .height(Length::Fill) .font(font) .size(note.text_size) - .on_action(move |action| { - cosmic::Action::App(Message::NoteAction(id.clone(), action)) - }), + .on_action(move |action| cosmic::Action::App(Message::NoteAction(id.clone(), action))), ); } else { body = body.push( @@ -1595,11 +1454,7 @@ impl FereseShell { } if let Some(error) = &self.note_error { - body = body.push( - text(error.clone()) - .size(10) - .class(theme::Text::Color(foreground)), - ); + body = body.push(text(error.clone()).size(10).class(theme::Text::Color(foreground))); } let radius = self.config.theme.material_radius; @@ -1680,10 +1535,7 @@ impl FereseShell { .as_ref() .is_some_and(|drag| drag.id.is_none() && drag.source == id) { - return container(text("")) - .width(Length::Fill) - .height(Length::Fill) - .into(); + return container(text("")).width(Length::Fill).height(Length::Fill).into(); } cosmic::widget::mouse_area(self.view_clock_content()) @@ -1701,10 +1553,7 @@ impl FereseShell { ClockAlignment::Right => alignment::Horizontal::Right, }; let pixel = clock.style == ferese_core::desktop::ClockStyle::Pixel; - let custom_font = clock - .font_family - .as_deref() - .filter(|family| !family.trim().is_empty()); + let custom_font = clock.font_family.as_deref().filter(|family| !family.trim().is_empty()); let mut font = configured_font(if pixel { custom_font.or(Some("Cantarell")) } else { @@ -1717,22 +1566,13 @@ impl FereseShell { } let date_font = configured_font(custom_font.or(self.config.font_family.as_deref())); let tint = |custom: &Option, fallback| { - let mut tint = color( - custom - .as_deref() - .and_then(config::parse_color) - .unwrap_or(fallback), - ); + let mut tint = color(custom.as_deref().and_then(config::parse_color).unwrap_or(fallback)); tint.a *= clock.opacity; tint }; let foreground = tint(&clock.color, self.config.theme.text_primary); let accent = color(self.config.theme.accent); - let custom_color = clock - .color - .as_deref() - .and_then(config::parse_color) - .is_some(); + let custom_color = clock.color.as_deref().and_then(config::parse_color).is_some(); let digit_colors = [0.8, 0.45].map(|text_mix| { if custom_color { return foreground; @@ -1744,9 +1584,7 @@ impl FereseShell { a: foreground.a, } }); - let mut labels = cosmic::widget::column([]) - .spacing(clock.gap) - .align_x(alignment); + let mut labels = cosmic::widget::column([]).spacing(clock.gap).align_x(alignment); if pixel && clock.show_date { labels = labels.push( @@ -1759,10 +1597,7 @@ impl FereseShell { ); } - if let Some(digits) = pixel - .then(|| stacked_clock_digits(&self.desktop_clock.0)) - .flatten() - { + if let Some(digits) = pixel.then(|| stacked_clock_digits(&self.desktop_clock.0)).flatten() { let available_height = (clock.height as f32 - clock.padding * 2.0 - if clock.show_date { @@ -1830,15 +1665,11 @@ impl FereseShell { ))), ); } - let background = clock - .background - .as_deref() - .and_then(config::parse_color) - .map(|rgba| { - let mut tint = color(rgba); - tint.a *= clock.opacity; - Background::Color(tint) - }); + let background = clock.background.as_deref().and_then(config::parse_color).map(|rgba| { + let mut tint = color(rgba); + tint.a *= clock.opacity; + Background::Color(tint) + }); let radius = self.config.theme.material_radius; container(labels) @@ -1859,30 +1690,21 @@ impl FereseShell { } fn output_for_bar(&self, id: window::Id) -> Option<&control::OutputSnapshot> { - let name = self - .outputs - .iter() - .find(|entry| entry.bar == id)? - .name - .as_deref()?; - self.snapshot - .outputs - .iter() - .find(|output| output.name == name) + let name = self.outputs.iter().find(|entry| entry.bar == id)?.name.as_deref()?; + self.snapshot.outputs.iter().find(|output| output.name == name) } fn bar_hidden(&self, id: window::Id) -> bool { - output_bar_hidden( - &self.snapshot, - self.output_for_bar(id).map(|output| output.id), - ) + output_bar_hidden(&self.snapshot, self.output_for_bar(id).map(|output| output.id)) } fn handle_event(&mut self, event: Event, id: window::Id) -> Task { if let Event::Mouse(mouse) = &event { - if !self.outputs.iter().any(|entry| { - entry.clock == Some(id) || entry.notes.iter().any(|(_, surface)| *surface == id) - }) { + if !self + .outputs + .iter() + .any(|entry| entry.clock == Some(id) || entry.notes.iter().any(|(_, surface)| *surface == id)) + { return Task::none(); } @@ -1907,17 +1729,8 @@ impl FereseShell { }; if let Some(dimensions) = dimensions { - let requested = clamp_note_position( - drag.origin + delta, - drag.output_size, - dimensions, - ); - drag.position = avoid_widget_overlap( - drag.position, - requested, - dimensions, - &drag.obstacles, - ); + let requested = clamp_note_position(drag.origin + delta, drag.output_size, dimensions); + drag.position = avoid_widget_overlap(drag.position, requested, dimensions, &drag.obstacles); // Move a compact, cached buffer in the compositor instead of // repainting an output-sized buffer for every pointer event. return set_margin( @@ -1932,10 +1745,7 @@ impl FereseShell { } cosmic::iced::mouse::Event::ButtonReleased(cosmic::iced::mouse::Button::Left) - if self - .note_drag - .as_ref() - .is_some_and(|drag| drag.source == id) => + if self.note_drag.as_ref().is_some_and(|drag| drag.source == id) => { return self.finish_note_drag(true); } @@ -1966,17 +1776,13 @@ impl FereseShell { } match event { - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output( - event, - output, - ))) => self.output_event(event, output), + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Output(event, output))) => { + self.output_event(event, output) + } Event::Window(window::Event::Opened { .. }) if self.outputs.iter().any(|entry| entry.bar == id) || self.menu.as_ref().is_some_and(|menu| menu.id == id) - || self - .system_modal - .as_ref() - .is_some_and(|modal| modal.contains(id)) + || self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) || self .notification_surface .as_ref() @@ -2000,11 +1806,7 @@ impl FereseShell { key: cosmic::iced::keyboard::Key::Named(cosmic::iced::keyboard::key::Named::Escape), .. }) if self.menu.is_some() => self.close_menu(), - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Popup( - event, - surface, - popup_id, - ))) => { + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Popup(event, surface, popup_id))) => { if let Some(menu) = &mut self.menu && menu.id == popup_id { @@ -2020,15 +1822,10 @@ impl FereseShell { } wayland::PopupEvent::Focused => { if menu.effects.is_none() { - match EffectsBinding::attach_role( - &surface, - menu.kind.material_role(), - menu.progress(), - ) { + match EffectsBinding::attach_role(&surface, menu.kind.material_role(), menu.progress()) + { Ok(binding) => menu.effects = Some(binding), - Err(error) => eprintln!( - "ferese-shell: popover material unavailable: {error}" - ), + Err(error) => eprintln!("ferese-shell: popover material unavailable: {error}"), } EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); } @@ -2036,15 +1833,10 @@ impl FereseShell { wayland::PopupEvent::Configured { .. } => { menu.motion.begin(Instant::now()); if menu.effects.is_none() { - match EffectsBinding::attach_role( - &surface, - menu.kind.material_role(), - menu.progress(), - ) { + match EffectsBinding::attach_role(&surface, menu.kind.material_role(), menu.progress()) + { Ok(binding) => menu.effects = Some(binding), - Err(error) => eprintln!( - "ferese-shell: popover material unavailable: {error}" - ), + Err(error) => eprintln!("ferese-shell: popover material unavailable: {error}"), } EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); } @@ -2057,35 +1849,24 @@ impl FereseShell { } Task::none() } - Event::Window(window::Event::Closed) - if self.menu.as_ref().is_some_and(|menu| menu.id == id) => - { + Event::Window(window::Event::Closed) if self.menu.as_ref().is_some_and(|menu| menu.id == id) => { self.menu = None; EFFECT_FRAME_PENDING.store(false, Ordering::Relaxed); Task::none() } Event::Window(window::Event::Closed) - if self - .system_modal - .as_ref() - .is_some_and(|modal| modal.contains(id)) => + if self.system_modal.as_ref().is_some_and(|modal| modal.contains(id)) => { self.close_system_modal() } Event::Window(window::Event::Closed) => Task::none(), - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame( - _, - surface, - frame_id, - ))) if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => { + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id))) + if self.menu.as_ref().is_some_and(|menu| menu.id == frame_id) => + { if let Some(menu) = &mut self.menu && menu.effects.is_none() { - match EffectsBinding::attach_role( - &surface, - menu.kind.material_role(), - menu.progress(), - ) { + match EffectsBinding::attach_role(&surface, menu.kind.material_role(), menu.progress()) { Ok(binding) => menu.effects = Some(binding), Err(error) => { eprintln!("ferese-shell: popover material unavailable: {error}") @@ -2095,22 +1876,17 @@ impl FereseShell { } Task::none() } - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame( - _, - surface, - frame_id, - ))) if self.outputs.iter().any(|entry| entry.bar == frame_id) => { + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Frame(_, surface, frame_id))) + if self.outputs.iter().any(|entry| entry.bar == frame_id) => + { self.attach_effects(frame_id, &surface); Task::none() } - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer( - _, - surface, - layer_id, - ))) if self - .notification_surface - .as_ref() - .is_some_and(|entry| entry.id == layer_id) => + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id))) + if self + .notification_surface + .as_ref() + .is_some_and(|entry| entry.id == layer_id) => { let entry = self.notification_surface.as_mut().unwrap(); @@ -2124,11 +1900,9 @@ impl FereseShell { } Task::none() } - Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer( - _, - surface, - layer_id, - ))) if self.outputs.iter().any(|entry| entry.bar == layer_id) => { + Event::PlatformSpecific(PlatformSpecific::Wayland(wayland::Event::Layer(_, surface, layer_id))) + if self.outputs.iter().any(|entry| entry.bar == layer_id) => + { self.attach_effects(layer_id, &surface); self.attach_power_material(layer_id, &surface); Task::none() @@ -2157,10 +1931,7 @@ impl FereseShell { fn view_layer(&self, id: window::Id) -> Element<'_, cosmic::Action> { if self.bar_hidden(id) { - return container(text("")) - .width(Length::Fill) - .height(Length::Fill) - .into(); + return container(text("")).width(Length::Fill).height(Length::Fill).into(); } let focused_output = self.output_for_bar(id); @@ -2210,9 +1981,7 @@ impl FereseShell { )) .height(bar.control_height) .padding(0) - .on_press(cosmic::Action::App(Message::ActivateWorkspace( - workspace.id, - ))); + .on_press(cosmic::Action::App(Message::ActivateWorkspace(workspace.id))); let selector = motion::button( workspace_button, @@ -2229,9 +1998,11 @@ impl FereseShell { // accessible button name, without an overlay stealing its target. workspace_buttons = workspace_buttons.push(selector); } - workspace_row = workspace_row.push(container(workspace_buttons).padding([0, 3]).class( - theme::Container::custom(move |_| bar_group_style(shell_theme)), - )); + workspace_row = workspace_row.push( + container(workspace_buttons) + .padding([0, 3]) + .class(theme::Container::custom(move |_| bar_group_style(shell_theme))), + ); let foreground = color(shell_theme.text_primary); let left = cosmic::iced::widget::scrollable(workspace_row) @@ -2249,9 +2020,7 @@ impl FereseShell { text(date) .size(12) .class(theme::Text::Color(color(shell_theme.text_muted))), - text(time) - .size(bar.text_size) - .class(theme::Text::Color(foreground)), + text(time).size(bar.text_size).class(theme::Text::Color(foreground)), ] .spacing(8) .align_y(cosmic::iced::Alignment::Center), @@ -2279,9 +2048,7 @@ impl FereseShell { )) .height(bar.control_height) .align_y(alignment::Vertical::Center) - .class(theme::Container::custom(move |_| { - bar_group_style(shell_theme) - })); + .class(theme::Container::custom(move |_| bar_group_style(shell_theme))); let right = row![ self.view_status_bar().map(move |action| match action { cosmic::Action::App(Message::OpenMenu(kind, anchor)) => @@ -2376,10 +2143,7 @@ impl FereseShell { } } -fn overview_control( - bar: BarMetrics, - foreground: Color, -) -> Element<'static, cosmic::Action> { +fn overview_control(bar: BarMetrics, foreground: Color) -> Element<'static, cosmic::Action> { container(bar_icon( ferese_theme::icons::FERESE, bar.overview_icon_size, @@ -2448,10 +2212,7 @@ fn workspace_indicator( .into() } -fn focused_bar_title<'a>( - snapshot: &'a ShellSnapshot, - output: Option<&control::OutputSnapshot>, -) -> &'a str { +fn focused_bar_title<'a>(snapshot: &'a ShellSnapshot, output: Option<&control::OutputSnapshot>) -> &'a str { let Some(output) = output else { return ""; }; @@ -2480,15 +2241,9 @@ fn workspace_selector_style( ) -> container::Style { container::Style { background: if active { - Some(Background::Color(color_with_opacity( - shell_theme.accent, - 0.16, - ))) + Some(Background::Color(color_with_opacity(shell_theme.accent, 0.16))) } else if occupied { - Some(Background::Color(color_with_opacity( - shell_theme.text_primary, - 0.04, - ))) + Some(Background::Color(color_with_opacity(shell_theme.text_primary, 0.04))) } else { None }, @@ -2536,18 +2291,10 @@ fn avoid_widget_overlap( position } -fn clamp_note_position( - position: cosmic::iced::Point, - output: (i32, i32), - size: (u32, u32), -) -> cosmic::iced::Point { +fn clamp_note_position(position: cosmic::iced::Point, output: (i32, i32), size: (u32, u32)) -> cosmic::iced::Point { cosmic::iced::Point::new( - position - .x - .clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)), - position - .y - .clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)), + position.x.clamp(0., (output.0 as f32 - size.0 as f32).clamp(0., 8192.)), + position.y.clamp(0., (output.1 as f32 - size.1 as f32).clamp(0., 8192.)), ) } @@ -2571,16 +2318,12 @@ fn widget_origin( use ferese_core::desktop::Anchor as A; let x = match anchor { A::TopLeft | A::CenterLeft | A::BottomLeft => margin_x as f32, - A::TopRight | A::CenterRight | A::BottomRight => { - output.0 as f32 - size.0 as f32 - margin_x as f32 - } + A::TopRight | A::CenterRight | A::BottomRight => output.0 as f32 - size.0 as f32 - margin_x as f32, _ => (output.0 as f32 - size.0 as f32) / 2., }; let y = match anchor { A::TopLeft | A::TopCenter | A::TopRight => margin_y as f32, - A::BottomLeft | A::BottomCenter | A::BottomRight => { - output.1 as f32 - size.1 as f32 - margin_y as f32 - } + A::BottomLeft | A::BottomCenter | A::BottomRight => output.1 as f32 - size.1 as f32 - margin_y as f32, _ => (output.1 as f32 - size.1 as f32) / 2., }; clamp_note_position(cosmic::iced::Point::new(x, y), output, size) @@ -2590,11 +2333,7 @@ fn clock_placement(clock: &ferese_core::desktop::Clock) -> (Anchor, IcedMargin) widget_placement(clock.anchor, clock.margin_x, clock.margin_y) } -fn widget_placement( - position: ferese_core::desktop::Anchor, - margin_x: i32, - margin_y: i32, -) -> (Anchor, IcedMargin) { +fn widget_placement(position: ferese_core::desktop::Anchor, margin_x: i32, margin_y: i32) -> (Anchor, IcedMargin) { use ferese_core::desktop::Anchor as Position; let horizontal = match position { Position::TopLeft | Position::CenterLeft | Position::BottomLeft => Anchor::LEFT, @@ -2610,21 +2349,9 @@ fn widget_placement( horizontal | vertical, IcedMargin { top: if vertical == Anchor::TOP { margin_y } else { 0 }, - bottom: if vertical == Anchor::BOTTOM { - margin_y - } else { - 0 - }, - left: if horizontal == Anchor::LEFT { - margin_x - } else { - 0 - }, - right: if horizontal == Anchor::RIGHT { - margin_x - } else { - 0 - }, + bottom: if vertical == Anchor::BOTTOM { margin_y } else { 0 }, + left: if horizontal == Anchor::LEFT { margin_x } else { 0 }, + right: if horizontal == Anchor::RIGHT { margin_x } else { 0 }, }, ) } @@ -2638,8 +2365,7 @@ fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style { } fn bar_style(theme: ShellTheme, compositor_material: bool) -> container::Style { - let mut style = - ferese_theme::controls::surface_appearance(color(theme.bar_background), theme.bar_radius); + let mut style = ferese_theme::controls::surface_appearance(color(theme.bar_background), theme.bar_radius); if compositor_material { style.background = None; } @@ -2651,10 +2377,7 @@ fn bar_style(theme: ShellTheme, compositor_material: bool) -> container::Style { fn bar_group_style(theme: ShellTheme) -> container::Style { container::Style { - background: Some(Background::Color(color_with_opacity( - theme.text_primary, - 0.035, - ))), + background: Some(Background::Color(color_with_opacity(theme.text_primary, 0.035))), border: Border { color: color_with_opacity(theme.text_muted, 0.12), width: 1.0, @@ -2690,28 +2413,21 @@ fn native_wayland_surface( use cosmic::iced::window::raw_window_handle::{RawDisplayHandle, RawWindowHandle}; let display = window.display_handle().map_err(|error| error.to_string())?; let handle = window.window_handle().map_err(|error| error.to_string())?; - let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) = - (display.as_raw(), handle.as_raw()) + let (RawDisplayHandle::Wayland(display), RawWindowHandle::Wayland(handle)) = (display.as_raw(), handle.as_raw()) else { return Err("shell material requires a Wayland surface".to_owned()); }; // SAFETY: iced lends matching, live Wayland handles for this callback. // This backend borrows the display; it never disconnects iced's connection. // Both handles belong to the runtime, which outlives the shell bindings. - 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); // SAFETY: raw-window-handle identifies this pointer as a live wl_surface. let id = unsafe { - wayland_client::backend::ObjectId::from_ptr( - wl_surface::WlSurface::interface(), - handle.surface.as_ptr().cast(), - ) + wayland_client::backend::ObjectId::from_ptr(wl_surface::WlSurface::interface(), handle.surface.as_ptr().cast()) } .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())?; Ok((connection, surface)) } @@ -2725,10 +2441,7 @@ struct EffectsBinding { } impl EffectsBinding { - fn attach( - surface: &wl_surface::WlSurface, - visible: bool, - ) -> Result> { + fn attach(surface: &wl_surface::WlSurface, visible: bool) -> Result> { Self::attach_role( surface, visible.then_some(ferese_surface_effects_v1::Role::Panel), @@ -2803,8 +2516,7 @@ impl EffectsBinding { fn set_visible(&self, visible: bool) -> Result<(), Box> { self.set_opacity(1.0)?; if visible { - self.surface - .set_role(ferese_surface_effects_v1::Role::Panel); + self.surface.set_role(ferese_surface_effects_v1::Role::Panel); } else { self.surface.clear_role(); } @@ -2855,48 +2567,23 @@ mod tests { height: 100., }]; assert_eq!( - super::avoid_widget_overlap( - Point::new(0., 0.), - Point::new(300., 0.), - (20, 20), - &obstacles - ), + super::avoid_widget_overlap(Point::new(0., 0.), Point::new(300., 0.), (20, 20), &obstacles), Point::new(80., 0.) ); assert_eq!( - super::avoid_widget_overlap( - Point::new(80., 0.), - Point::new(120., 50.), - (20, 20), - &obstacles - ), + super::avoid_widget_overlap(Point::new(80., 0.), Point::new(120., 50.), (20, 20), &obstacles), Point::new(80., 50.) ); assert_eq!( - super::avoid_widget_overlap( - Point::new(160., 0.), - Point::new(0., 0.), - (20, 20), - &obstacles - ), + super::avoid_widget_overlap(Point::new(160., 0.), Point::new(0., 0.), (20, 20), &obstacles), Point::new(140., 0.) ); assert_eq!( - super::avoid_widget_overlap( - Point::new(100., 120.), - Point::new(100., 0.), - (20, 20), - &obstacles - ), + super::avoid_widget_overlap(Point::new(100., 120.), Point::new(100., 0.), (20, 20), &obstacles), Point::new(100., 100.) ); assert_eq!( - super::avoid_widget_overlap( - Point::new(0., 100.), - Point::new(200., 100.), - (20, 20), - &obstacles - ), + super::avoid_widget_overlap(Point::new(0., 100.), Point::new(200., 100.), (20, 20), &obstacles), Point::new(200., 100.) ); } @@ -2905,15 +2592,9 @@ mod tests { fn note_drag_origin_and_bounds_use_logical_output_coordinates() { use cosmic::iced::Point; let mut note = ferese_core::desktop::StickyNote::default(); - assert_eq!( - super::note_origin(¬e, (1920, 1080)), - Point::new(1552., 80.) - ); + assert_eq!(super::note_origin(¬e, (1920, 1080)), Point::new(1552., 80.)); note.anchor = ferese_core::desktop::Anchor::Center; - assert_eq!( - super::note_origin(¬e, (1920, 1080)), - Point::new(800., 420.) - ); + assert_eq!(super::note_origin(¬e, (1920, 1080)), Point::new(800., 420.)); assert_eq!( super::clamp_note_position(Point::new(-20., 1200.), (1920, 1080), (320, 240)), Point::new(0., 840.) @@ -2935,19 +2616,13 @@ mod tests { }; let (anchor, margin) = super::clock_placement(&clock); assert_eq!(anchor, super::Anchor::BOTTOM | super::Anchor::RIGHT); - assert_eq!( - (margin.top, margin.right, margin.bottom, margin.left), - (0, 30, 40, 0) - ); + assert_eq!((margin.top, margin.right, margin.bottom, margin.left), (0, 30, 40, 0)); let (anchor, margin) = super::clock_placement(&Clock { anchor: Position::Center, ..clock }); assert!(anchor.is_empty()); - assert_eq!( - (margin.top, margin.right, margin.bottom, margin.left), - (0, 0, 0, 0) - ); + assert_eq!((margin.top, margin.right, margin.bottom, margin.left), (0, 0, 0, 0)); } #[test] @@ -2994,31 +2669,20 @@ mod tests { #[test] fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() { let theme = ShellTheme::default(); - assert_eq!( - shell_surface_style(theme).background_color, - Color::TRANSPARENT - ); - let Some(Background::Color(background)) = bar_style(theme.for_bar(), false).background - else { + assert_eq!(shell_surface_style(theme).background_color, Color::TRANSPARENT); + let Some(Background::Color(background)) = bar_style(theme.for_bar(), false).background else { panic!("first frame needs a fallback fill before material attachment"); }; assert!(background.r < 0.15 && background.g < 0.15 && background.b < 0.20); assert!(background.a > 0.9); - assert!( - theme.bar_text_primary[..3] - .iter() - .all(|channel| *channel > 220) - ); + assert!(theme.bar_text_primary[..3].iter().all(|channel| *channel > 220)); } #[test] fn bar_fallback_uses_the_matching_dark_palette_and_geometry() { let theme = ShellTheme::default().for_bar(); let style = bar_style(theme, false); - assert_eq!( - style.background, - Some(Background::Color(color(theme.bar_background))) - ); + assert_eq!(style.background, Some(Background::Color(color(theme.bar_background)))); assert_eq!(style.text_color, Some(color(theme.bar_text_primary))); assert_eq!(style.border.radius, theme.bar_radius.into()); assert_eq!(style.shadow, cosmic::iced::Shadow::default()); @@ -3084,10 +2748,7 @@ mod tests { #[test] fn bar_title_tracks_focus_and_the_outputs_active_workspace() { let mut snapshot = snapshot_with_fullscreen_window(7); - assert_eq!( - focused_bar_title(&snapshot, snapshot.outputs.first()), - "Test" - ); + assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), "Test"); snapshot.windows[0].focused = false; assert_eq!(focused_bar_title(&snapshot, snapshot.outputs.first()), ""); snapshot.windows[0].focused = true; diff --git a/shell/ferese-shell/src/motion.rs b/shell/ferese-shell/src/motion.rs index 1393862..c3a8979 100644 --- a/shell/ferese-shell/src/motion.rs +++ b/shell/ferese-shell/src/motion.rs @@ -1,6 +1,5 @@ //! Draw-only popup transform; layout stays stable while its presentation animates. -use cosmic::iced::advanced::Renderer as _; -use cosmic::iced::advanced::{Clipboard, Layout, Shell, Widget, layout, mouse, renderer, widget}; +use cosmic::iced::advanced::{Clipboard, Layout, Renderer as _, Shell, Widget, layout, mouse, renderer, widget}; use cosmic::iced::{Event, Length, Rectangle, Size, Transformation, Vector}; use cosmic::{Element, Theme}; @@ -42,8 +41,7 @@ impl Settings { } } static SETTINGS: std::sync::OnceLock> = std::sync::OnceLock::new(); -static SHELL_RADIUS: std::sync::atomic::AtomicU32 = - std::sync::atomic::AtomicU32::new(14.0_f32.to_bits()); +static SHELL_RADIUS: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(14.0_f32.to_bits()); /// All shell shapes share one radius, capped to fit smaller controls. pub(crate) fn radius(limit: f32) -> f32 { @@ -97,17 +95,14 @@ impl PopupMotion { } pub(crate) fn progress_at(&self, now: std::time::Instant) -> f32 { - let duration = self - .settings - .duration(if self.target == 1.0 { 200.0 } else { 140.0 }); + let duration = self.settings.duration(if self.target == 1.0 { 200.0 } else { 140.0 }); if duration.is_zero() { return self.target; } let Some(started) = self.started else { return self.start; }; - let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32()) - .min(1.0); + let t = (now.saturating_duration_since(started).as_secs_f32() / duration.as_secs_f32()).min(1.0); if t == 1.0 { return self.target; } @@ -145,14 +140,7 @@ pub(crate) fn button<'a, M: Clone + 'a>( let active = progress.clone(); let hovered = progress.clone(); let paint = move |progress: f32, pressed: bool| { - ferese_theme::controls::shell_button( - foreground, - selected, - opacity, - radius(14.), - progress, - pressed, - ) + ferese_theme::controls::shell_button(foreground, selected, opacity, radius(14.), progress, pressed) }; let content = button .class(cosmic::theme::Button::Custom { @@ -181,12 +169,7 @@ struct HoverState { } impl HoverState { - fn advance( - &mut self, - target: f32, - now: std::time::Instant, - duration: std::time::Duration, - ) -> bool { + fn advance(&mut self, target: f32, now: std::time::Instant, duration: std::time::Duration) -> bool { if self.target != target { self.start = self.current; self.target = target; @@ -196,8 +179,7 @@ impl HoverState { 1.0 } else { self.started.map_or(1.0, |start| { - (now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32()) - .min(1.0) + (now.saturating_duration_since(start).as_secs_f32() / duration.as_secs_f32()).min(1.0) }) }; self.current = self.start + (self.target - self.start) * t * t * (3.0 - 2.0 * t); @@ -222,9 +204,7 @@ impl Widget for Hover<'_, 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 tag(&self) -> widget::tree::Tag { @@ -298,17 +278,10 @@ impl Widget for Hover<'_, M> { cursor: mouse::Cursor, viewport: &Rectangle, ) { - self.progress - .set(tree.state.downcast_ref::().current); - self.content.as_widget().draw( - &tree.children[0], - renderer, - theme, - style, - layout, - cursor, - viewport, - ); + self.progress.set(tree.state.downcast_ref::().current); + self.content + .as_widget() + .draw(&tree.children[0], renderer, theme, style, layout, cursor, viewport); } fn mouse_interaction( @@ -319,13 +292,9 @@ impl Widget for Hover<'_, 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( @@ -348,22 +317,13 @@ impl Widget for Hover<'_, 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) } } -pub fn animated<'a, M: 'a>( - content: Element<'a, M>, - progress: f32, - regions: Regions, - radius: f32, -) -> Element<'a, M> { +pub fn animated<'a, M: 'a>(content: Element<'a, M>, progress: f32, regions: Regions, radius: f32) -> Element<'a, M> { Element::new(Motion { content, progress, @@ -445,9 +405,9 @@ impl Widget for Motion<'_, M> { } *self.regions.lock().unwrap() = collector.into_regions(self.translation(), self.radius); let cursor = self.cursor(cursor); - self.content.as_widget_mut().update( - tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ); + self.content + .as_widget_mut() + .update(tree, event, layout, cursor, renderer, clipboard, shell, viewport); } fn draw( @@ -463,15 +423,9 @@ impl Widget for Motion<'_, M> { let v = self.translation(); let transform = Transformation::translate(v.x, v.y); renderer.with_transformation(transform, |renderer| { - self.content.as_widget().draw( - tree, - renderer, - theme, - style, - layout, - self.cursor(cursor), - viewport, - ) + self.content + .as_widget() + .draw(tree, renderer, theme, style, layout, self.cursor(cursor), viewport) }); } @@ -483,13 +437,9 @@ impl Widget for Motion<'_, M> { viewport: &Rectangle, renderer: &cosmic::Renderer, ) -> mouse::Interaction { - self.content.as_widget().mouse_interaction( - tree, - layout, - self.cursor(cursor), - viewport, - renderer, - ) + self.content + .as_widget() + .mouse_interaction(tree, layout, self.cursor(cursor), viewport, renderer) } fn operate( @@ -499,9 +449,7 @@ impl Widget for Motion<'_, M> { renderer: &cosmic::Renderer, operation: &mut dyn widget::Operation, ) { - self.content - .as_widget_mut() - .operate(tree, layout, renderer, operation); + self.content.as_widget_mut().operate(tree, layout, renderer, operation); } } @@ -557,11 +505,7 @@ mod tests { fn centered_modal_material_tracks_surface_position_and_animation() { let mut collector = CollectRegions(Vec::new()); let card = Rectangle::new((380.0, 420.0).into(), (440.0, 220.0).into()); - widget::Operation::container( - &mut collector, - Some(&widget::Id::new("ferese-blur-card")), - card, - ); + widget::Operation::container(&mut collector, Some(&widget::Id::new("ferese-blur-card")), card); assert_eq!( collector.into_regions(Vector::new(0.0, -2.0), 14.0), vec![[380, 418, 440, 220, 14]] @@ -572,29 +516,16 @@ mod tests { fn popup_waits_for_configuration_then_opens_and_closes_faster() { let now = std::time::Instant::now(); let mut motion = PopupMotion::new(Settings::default()); - assert_eq!( - motion.progress_at(now + std::time::Duration::from_secs(30)), - 0.0 - ); + assert_eq!(motion.progress_at(now + std::time::Duration::from_secs(30)), 0.0); motion.begin(now); // Later configure/focus notifications must not restart the clock. motion.begin(now + std::time::Duration::from_millis(90)); - assert!( - (motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001 - ); - assert_eq!( - motion.progress_at(now + std::time::Duration::from_millis(200)), - 1.0 - ); + assert!((motion.progress_at(now + std::time::Duration::from_millis(100)) - 0.5).abs() < 0.00001); + assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(200)), 1.0); motion.retarget(0.0, now + std::time::Duration::from_millis(200)); assert!(motion.closing()); - assert!( - (motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001 - ); - assert_eq!( - motion.progress_at(now + std::time::Duration::from_millis(340)), - 0.0 - ); + assert!((motion.progress_at(now + std::time::Duration::from_millis(270)) - 0.5).abs() < 0.00001); + assert_eq!(motion.progress_at(now + std::time::Duration::from_millis(340)), 0.0); } #[test] @@ -613,10 +544,7 @@ mod tests { let before = motion.progress_at(reopen); motion.retarget(1.0, reopen); assert_eq!(motion.progress_at(reopen), before); - assert_eq!( - motion.progress_at(reopen + std::time::Duration::from_secs(1)), - 1.0 - ); + assert_eq!(motion.progress_at(reopen + std::time::Duration::from_secs(1)), 1.0); for settings in [ Settings { reduced_motion: true, diff --git a/shell/ferese-shell/src/note_store.rs b/shell/ferese-shell/src/note_store.rs index 9b41fa5..041a062 100644 --- a/shell/ferese-shell/src/note_store.rs +++ b/shell/ferese-shell/src/note_store.rs @@ -1,5 +1,7 @@ //! Atomic, conflict-checked note edits; always called from a worker thread. -use std::{fs, io::Write, path::Path}; +use std::fs; +use std::io::Write; +use std::path::Path; #[derive(Clone, Debug)] pub enum Edit { @@ -63,13 +65,10 @@ pub fn save(path: &Path, edits: &[Edit]) -> Result<(), String> { file.as_file() .set_permissions(fs::metadata(path).map_err(|e| e.to_string())?.permissions()) .map_err(|e| e.to_string())?; - file.write_all(updated.as_bytes()) - .map_err(|e| e.to_string())?; + file.write_all(updated.as_bytes()).map_err(|e| e.to_string())?; file.as_file().sync_all().map_err(|e| e.to_string())?; if fs::read_to_string(path).map_err(|e| e.to_string())? != source { - return Err( - "Config changed while saving; your note remains open. Edit again to retry.".into(), - ); + return Err("Config changed while saving; your note remains open. Edit again to retry.".into()); } file.persist(path).map_err(|e| e.to_string())?; fs::File::open(parent) @@ -131,10 +130,7 @@ desktop-widgets { let source = fs::read_to_string(&path).unwrap(); assert!(source.contains("// keep this")); assert!(source.contains("#123456")); - let notes = crate::config::parse_source(&source) - .unwrap() - .desktop_widgets - .notes; + let notes = crate::config::parse_source(&source).unwrap().desktop_widgets.notes; assert_eq!(notes[0].text, "one\ntwo"); assert_eq!((notes[0].margin_x, notes[0].margin_y), (50, 60)); assert_eq!(notes[0].anchor, ferese_core::desktop::Anchor::TopLeft); diff --git a/shell/ferese-shell/src/notification_ui.rs b/shell/ferese-shell/src/notification_ui.rs index 8823fa8..10135b8 100644 --- a/shell/ferese-shell/src/notification_ui.rs +++ b/shell/ferese-shell/src/notification_ui.rs @@ -1,15 +1,12 @@ -use super::*; use cosmic::iced::widget::scrollable::{Direction, Scrollbar}; use cosmic::widget::{column, scrollable}; +use super::*; + const POPUP_WIDTH: u32 = 368; fn popup_height(notice: ¬ifications::Notice, count: usize) -> u32 { - let body = if notice.body.is_empty() && count == 1 { - 0 - } else { - 22 - }; + let body = if notice.body.is_empty() && count == 1 { 0 } else { 22 }; let actions = if notice.live && notice.actions.iter().any(|(key, _)| key != "default") { 39 } else { @@ -33,18 +30,13 @@ fn history_height(center: ¬ifications::Center) -> u32 { } else { 1 }; - popup_height(notice, count) - + wrapped_lines.saturating_sub(1) as u32 * 17 - + if count > 1 { 12 } else { 0 } + popup_height(notice, count) + wrapped_lines.saturating_sub(1) as u32 * 17 + if count > 1 { 12 } else { 0 } }; let cards: u32 = groups .iter() .map(|group| { if group.len() > 1 && center.expanded_apps.contains(&group[0].app) { - 32 + group - .iter() - .map(|notice| card_height(notice, 1)) - .sum::() + 32 + group.iter().map(|notice| card_height(notice, 1)).sum::() + group.len().saturating_sub(1) as u32 * 8 } else { card_height(group[0], group.len()) @@ -188,9 +180,10 @@ impl FereseShell { .outputs .iter() .find(|entry| { - self.snapshot.outputs.iter().any(|output| { - output.focused && Some(output.name.as_str()) == entry.name.as_deref() - }) + self.snapshot + .outputs + .iter() + .any(|output| output.focused && Some(output.name.as_str()) == entry.name.as_deref()) }) .or_else(|| self.outputs.first()) .map(|entry| entry.output.clone()); @@ -213,9 +206,7 @@ impl FereseShell { .notifications .popup_groups() .iter() - .map(|(notice, _, count)| { - popup_height(notice, *count) + if *count > 1 { 12 } else { 0 } - }) + .map(|(notice, _, count)| popup_height(notice, *count) + if *count > 1 { 12 } else { 0 }) .sum::() + count.saturating_sub(1) as u32 * 8 + 8; @@ -299,8 +290,7 @@ impl FereseShell { let id = notice.id; let mut bold = *SHELL_FONT.get().unwrap().read().unwrap(); bold.weight = cosmic::iced::font::Weight::Bold; - let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty() - { + let app_icon = if notice.app.to_lowercase().starts_with("ferese") || notice.icon.is_empty() { accented_icon(ferese_theme::icons::FERESE, 18, accent, accent) } else if notice.icon.starts_with('/') { icon::icon(icon::from_path(notice.icon.clone().into())).size(18) @@ -308,18 +298,17 @@ impl FereseShell { icon::from_name(notice.icon.as_str()).size(18).icon() } .opacity(opacity); - let icon_well = - container(app_icon) - .center_x(30) - .center_y(30) - .class(theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(well)), - border: Border { - radius: motion::radius(15.0).into(), - ..Default::default() - }, + let icon_well = container(app_icon) + .center_x(30) + .center_y(30) + .class(theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(well)), + border: Border { + radius: motion::radius(15.0).into(), ..Default::default() - })); + }, + ..Default::default() + })); let icon: Element<'_, cosmic::Action> = if count > 1 { let badge_fill = ferese_theme::composite( color(palette.accent), @@ -329,8 +318,7 @@ impl FereseShell { palette.surface_popover }), ); - let (badge_fill, badge_text) = - ferese_theme::accent_pair(badge_fill, color(palette.text_primary)); + let (badge_fill, badge_text) = ferese_theme::accent_pair(badge_fill, color(palette.text_primary)); let badge = container( text(count.to_string()) .size(10) @@ -375,15 +363,10 @@ impl FereseShell { .align_y(alignment::Vertical::Center) .push(icon) .push( - container( - text(app) - .size(11) - .font(bold) - .class(theme::Text::Color(muted)), - ) - .width(Length::Fill) - .height(16) - .clip(true), + container(text(app).size(11).font(bold).class(theme::Text::Color(muted))) + .width(Length::Fill) + .height(16) + .clip(true), ) .push( text(age_label(notice.received_at.elapsed())) @@ -446,13 +429,13 @@ impl FereseShell { let mut actions = row([]); for (index, (key, label)) in available.into_iter().enumerate() { if index > 0 { - actions = actions.push( - container(cosmic::iced::widget::Space::new().width(1).height(38)) - .class(theme::Container::custom(move |_| container::Style { + actions = + actions.push(container(cosmic::iced::widget::Space::new().width(1).height(38)).class( + theme::Container::custom(move |_| container::Style { background: Some(Background::Color(divider)), ..Default::default() - })), - ); + }), + )); } actions = actions.push(card_button( container( @@ -472,17 +455,12 @@ impl FereseShell { } face = face .push( - container( - cosmic::iced::widget::Space::new() - .width(Length::Fill) - .height(1), - ) - .class(theme::Container::custom(move |_| { - container::Style { + container(cosmic::iced::widget::Space::new().width(Length::Fill).height(1)).class( + theme::Container::custom(move |_| container::Style { background: Some(Background::Color(divider)), ..Default::default() - } - })), + }), + ), ) .push(actions); } @@ -518,23 +496,19 @@ impl FereseShell { // bottom edges, after the card's measured height. let back = move |inset: f32| { container( - container( - cosmic::iced::widget::Space::new() - .width(Length::Fill) - .height(6), - ) - .width(Length::Fill) - .class(theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(Color { - a: 0.045 * opacity, - ..foreground - })), - border: Border { - radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(), + container(cosmic::iced::widget::Space::new().width(Length::Fill).height(6)) + .width(Length::Fill) + .class(theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(Color { + a: 0.045 * opacity, + ..foreground + })), + border: Border { + radius: [0.0, 0.0, motion::radius(6.0), motion::radius(6.0)].into(), + ..Default::default() + }, ..Default::default() - }, - ..Default::default() - })), + })), ) .padding([0.0, inset]) }; @@ -552,10 +526,7 @@ impl FereseShell { Message::ToggleNotificationHistory })); } else if notice.live && notice.actions.iter().any(|(key, _)| key == "default") { - area = area.on_press(cosmic::Action::App(Message::InvokeNotification( - id, - "default".into(), - ))); + area = area.on_press(cosmic::Action::App(Message::InvokeNotification(id, "default".into()))); } area.into() } @@ -589,11 +560,7 @@ impl FereseShell { } else { format!( "{count} {} · {} {}", - if count == 1 { - "notification" - } else { - "notifications" - }, + if count == 1 { "notification" } else { "notifications" }, groups.len(), if groups.len() == 1 { "app" } else { "apps" } ) @@ -619,11 +586,7 @@ impl FereseShell { well, 28, )); - let dnd_color = if self.notifications.dnd { - accent - } else { - muted - }; + let dnd_color = if self.notifications.dnd { accent } else { muted }; let dnd_icon = if self.notifications.dnd { ferese_theme::icons::NOTIFICATIONS_OFF } else { @@ -674,40 +637,34 @@ impl FereseShell { self.notifications.dnd, 1.0, )); - let controls = - container(dnd) - .padding(10) - .width(Length::Fill) - .class(theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(base)), - border: Border { - radius: palette.material_radius.into(), - ..Default::default() - }, + let controls = container(dnd) + .padding(10) + .width(Length::Fill) + .class(theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(base)), + border: Border { + radius: palette.material_radius.into(), ..Default::default() - })); + }, + ..Default::default() + })); let mut content = column([]).spacing(10).push(header).push(controls); if count == 0 { let empty = column([]) .spacing(10) .align_x(alignment::Horizontal::Center) .push( - container(accented_icon( - ferese_theme::icons::NOTIFICATIONS, - 26, - muted, - accent, - )) - .center_x(56) - .center_y(56) - .class(theme::Container::custom(move |_| container::Style { - background: Some(Background::Color(well)), - border: Border { - radius: motion::radius(28.0).into(), + container(accented_icon(ferese_theme::icons::NOTIFICATIONS, 26, muted, accent)) + .center_x(56) + .center_y(56) + .class(theme::Container::custom(move |_| container::Style { + background: Some(Background::Color(well)), + border: Border { + radius: motion::radius(28.0).into(), + ..Default::default() + }, ..Default::default() - }, - ..Default::default() - })), + })), ) .push( text("You're all caught up") @@ -720,11 +677,7 @@ impl FereseShell { .size(11) .class(theme::Text::Color(muted)), ); - content = content.push( - container(empty) - .center_x(Length::Fill) - .center_y(Length::Fill), - ); + content = content.push(container(empty).center_x(Length::Fill).center_y(Length::Fill)); } else { content = content.push( row([]) @@ -786,12 +739,7 @@ impl FereseShell { } entries = entries.push(cards); } else { - entries = entries.push(self.notification_card( - latest, - opacity, - true, - group.len(), - )); + entries = entries.push(self.notification_card(latest, opacity, true, group.len())); } } content = content.push( @@ -806,8 +754,7 @@ impl FereseShell { .height(history_height(&self.notifications) as f32) .width(Length::Fill) .class(theme::Container::custom(move |_| container::Style { - background: (!compositor_material) - .then_some(Background::Color(color(palette.surface_popover))), + background: (!compositor_material).then_some(Background::Color(color(palette.surface_popover))), border: Border { color: color_with_opacity(palette.text_muted, 0.18 * opacity), width: 1.0, @@ -819,9 +766,7 @@ impl FereseShell { motion::animated( panel, progress, - surface - .map(|surface| surface.regions.clone()) - .unwrap_or_default(), + surface.map(|surface| surface.regions.clone()).unwrap_or_default(), palette.material_radius, ) } else { diff --git a/shell/ferese-shell/src/notifications.rs b/shell/ferese-shell/src/notifications.rs index 37a21b5..5749a3b 100644 --- a/shell/ferese-shell/src/notifications.rs +++ b/shell/ferese-shell/src/notifications.rs @@ -1,11 +1,7 @@ -use std::{ - collections::{HashMap, HashSet, VecDeque}, - sync::{ - Arc, Mutex, - mpsc::{self, SyncSender}, - }, - time::{Duration, Instant}, -}; +use std::collections::{HashMap, HashSet, VecDeque}; +use std::sync::mpsc::SyncSender; +use std::sync::{Arc, Mutex, mpsc}; +use std::time::{Duration, Instant}; const PATH: &str = "/org/freedesktop/Notifications"; const INTERFACE: &str = "org.freedesktop.Notifications"; @@ -97,9 +93,7 @@ impl Server { let mut registry = self.registry.lock().unwrap(); let replacing = registry.live.contains_key(&replaces_id); if !replacing && registry.live.len() >= HISTORY_LIMIT { - return Err(zbus::fdo::Error::LimitsExceeded( - "Too many active notifications".into(), - )); + return Err(zbus::fdo::Error::LimitsExceeded("Too many active notifications".into())); } let id = if replacing { replaces_id @@ -201,10 +195,7 @@ impl Service { Self::connect(config, None) } - fn connect( - config: &ferese_core::notifications::NotificationConfig, - address: Option, - ) -> Self { + fn connect(config: &ferese_core::notifications::NotificationConfig, address: Option) -> Self { let timeout = Arc::new(std::sync::atomic::AtomicU32::new(config.timeout_ms)); let worker_timeout = timeout.clone(); let (sender, events) = tokio::sync::mpsc::channel(256); @@ -237,25 +228,13 @@ impl Service { if !registry.lock().unwrap().close(id, revision, reason) { continue; } - connection.emit_signal( - None::<&str>, - PATH, - INTERFACE, - "NotificationClosed", - &(id, reason), - ) + connection.emit_signal(None::<&str>, PATH, INTERFACE, "NotificationClosed", &(id, reason)) } Command::Action(id, revision, action) => { if registry.lock().unwrap().live.get(&id) != Some(&revision) { continue; } - connection.emit_signal( - None::<&str>, - PATH, - INTERFACE, - "ActionInvoked", - &(id, action), - ) + connection.emit_signal(None::<&str>, PATH, INTERFACE, "ActionInvoked", &(id, action)) } }; if let Err(error) = result { @@ -337,9 +316,7 @@ impl Center { } else { toast.closing.map_or_else( || (toast.born.elapsed().as_secs_f32() / duration.as_secs_f32()).min(1.0), - |closing| { - (1.0 - closing.elapsed().as_secs_f32() / duration.as_secs_f32()).max(0.0) - }, + |closing| (1.0 - closing.elapsed().as_secs_f32() / duration.as_secs_f32()).max(0.0), ) }; Some((notice, p * p * (3.0 - 2.0 * p))) @@ -347,18 +324,15 @@ impl Center { } pub fn animating(&self) -> bool { - self.toasts.iter().any(|toast| { - toast.closing.is_some() || toast.born.elapsed() < super::motion::notification_duration() - }) + self.toasts + .iter() + .any(|toast| toast.closing.is_some() || toast.born.elapsed() < super::motion::notification_duration()) } pub fn popup_groups(&self) -> Vec<(&Notice, f32, usize)> { let mut groups: Vec<(&Notice, f32, usize)> = Vec::new(); for (notice, opacity) in self.visible() { - if let Some(group) = groups - .iter_mut() - .find(|(head, _, _)| head.app == notice.app) - { + if let Some(group) = groups.iter_mut().find(|(head, _, _)| head.app == notice.app) { group.2 += 1; } else { groups.push((notice, opacity, 1)); @@ -451,10 +425,7 @@ impl Center { } pub fn invoke(&mut self, id: u32, action: String) { - if let Some(notice) = self - .entries - .iter() - .find(|notice| notice.id == id && notice.live) + if let Some(notice) = self.entries.iter().find(|notice| notice.id == id && notice.live) && notice.actions.iter().any(|(key, _)| key == &action) { let resident = notice.resident; @@ -593,9 +564,9 @@ impl Center { self.close(id, 1); } self.toasts.retain(|toast| { - toast.closing.is_none_or(|closing| { - now.duration_since(closing) < super::motion::notification_duration() - }) + toast + .closing + .is_none_or(|closing| now.duration_since(closing) < super::motion::notification_duration()) }); let visible: HashSet<_> = self.toasts.iter().map(|toast| toast.id).collect(); self.entries @@ -605,9 +576,10 @@ impl Center { #[cfg(test)] mod tests { - use super::*; use ferese_core::notifications::NotificationConfig; + use super::*; + fn fixture() -> (Center, std::sync::mpsc::Receiver) { let (_, events) = tokio::sync::mpsc::channel(256); let (commands, receiver) = mpsc::sync_channel(256); @@ -736,10 +708,7 @@ mod tests { center.last_tick = Instant::now() - Duration::from_secs(10); center.tick(); assert!(!center.entries[0].live); - assert!(matches!( - commands.try_recv().unwrap(), - Command::Close(1, 1, 1) - )); + assert!(matches!(commands.try_recv().unwrap(), Command::Close(1, 1, 1))); } #[test] @@ -786,10 +755,7 @@ mod tests { center.invoke(1, "invalid".into()); assert!(commands.try_recv().is_err()); center.invoke(1, "open".into()); - assert!(matches!( - commands.try_recv().unwrap(), - Command::Action(1, 1, _) - )); + assert!(matches!(commands.try_recv().unwrap(), Command::Action(1, 1, _))); assert!(center.entries[0].live); } @@ -811,13 +777,7 @@ mod tests { ..Default::default() }); assert!(center.dnd); - assert_eq!( - center - .service - .timeout - .load(std::sync::atomic::Ordering::Relaxed), - 10000 - ); + assert_eq!(center.service.timeout.load(std::sync::atomic::Ordering::Relaxed), 10000); center.receive(notice(1)); center.configure(NotificationConfig { show_popups: false, @@ -870,8 +830,7 @@ mod tests { .build() .unwrap(); let proxy = zbus::blocking::Proxy::new(&connection, INTERFACE, PATH, INTERFACE).unwrap(); - let info: (String, String, String, String) = - proxy.call("GetServerInformation", &()).unwrap(); + let info: (String, String, String, String) = proxy.call("GetServerInformation", &()).unwrap(); assert_eq!(info.0, "Ferese"); let notify = |replace| -> u32 { proxy @@ -905,22 +864,14 @@ mod tests { let (sender, receiver) = mpsc::channel(); let worker = std::thread::spawn(move || { let signal = signals.next().unwrap(); - sender - .send(signal.body().deserialize::<(u32, u32)>().unwrap()) - .unwrap(); + sender.send(signal.body().deserialize::<(u32, u32)>().unwrap()).unwrap(); }); proxy.call::<_, _, ()>("CloseNotification", &(id,)).unwrap(); let Event::Close(closed, revision) = received(&service) else { panic!("Expected close"); }; - service - .commands - .send(Command::Close(closed, revision, 3)) - .unwrap(); - assert_eq!( - receiver.recv_timeout(Duration::from_secs(3)).unwrap(), - (id, 3) - ); + service.commands.send(Command::Close(closed, revision, 3)).unwrap(); + assert_eq!(receiver.recv_timeout(Duration::from_secs(3)).unwrap(), (id, 3)); worker.join().unwrap(); assert!(proxy.call::<_, _, ()>("CloseNotification", &(id,)).is_err()); } diff --git a/shell/ferese-shell/src/recording.rs b/shell/ferese-shell/src/recording.rs index 9c37777..d64b827 100644 --- a/shell/ferese-shell/src/recording.rs +++ b/shell/ferese-shell/src/recording.rs @@ -1,13 +1,13 @@ //! The bar only controls the recorder and consumes bounded status messages. //! Encoding and portal negotiation run in the separate ferese-record process. +use std::io::{BufRead, BufReader, Read}; +use std::path::PathBuf; +use std::process::{ChildStdin, Command, Stdio}; +use std::sync::mpsc; +use std::sync::mpsc::Receiver; +use std::time::Instant; + use serde::Deserialize; -use std::{ - io::{BufRead, BufReader, Read}, - path::PathBuf, - process::{ChildStdin, Command, Stdio}, - sync::mpsc::{self, Receiver}, - time::Instant, -}; #[derive(Debug, Default)] pub enum State { @@ -126,28 +126,22 @@ impl Recorder { match event { Event::Update(update) => match update.state.as_str() { "selecting" if self.control.is_some() => self.state = State::Selecting, - "recording" if self.control.is_some() => { - self.state = State::Recording(Instant::now()) - } + "recording" if self.control.is_some() => self.state = State::Recording(Instant::now()), "saving" => self.state = State::Saving, "saved" => { - self.state = update.path.map(State::Saved).unwrap_or_else(|| { - State::Error("Recorder returned no saved file".into()) - }) + self.state = update + .path + .map(State::Saved) + .unwrap_or_else(|| State::Error("Recorder returned no saved file".into())) } "cancelled" => self.state = State::Idle, "error" => { - self.state = State::Error( - update.message.unwrap_or_else(|| "Recording failed".into()), - ) + self.state = State::Error(update.message.unwrap_or_else(|| "Recording failed".into())) } _ => (), }, Event::Exited => { - if matches!( - self.state, - State::Selecting | State::Recording(_) | State::Saving - ) { + if matches!(self.state, State::Selecting | State::Recording(_) | State::Saving) { self.state = State::Error("The recorder stopped unexpectedly".into()); } done = true; diff --git a/shell/ferese-shell/src/status.rs b/shell/ferese-shell/src/status.rs index c2ae785..6b7982f 100644 --- a/shell/ferese-shell/src/status.rs +++ b/shell/ferese-shell/src/status.rs @@ -3,17 +3,12 @@ mod bluetooth; mod dbus_cache; mod network; -use std::{ - env, fs, - path::Path, - process::{Command, Stdio}, - sync::{ - Arc, Condvar, Mutex, - mpsc::{self, SyncSender}, - }, - thread, - time::Duration, -}; +use std::path::Path; +use std::process::{Command, Stdio}; +use std::sync::mpsc::SyncSender; +use std::sync::{Arc, Condvar, Mutex, mpsc}; +use std::time::Duration; +use std::{env, fs, thread}; #[derive(Clone, Debug, Default)] pub struct Snapshot { @@ -146,10 +141,7 @@ impl Service { let (updates, rx) = tokio::sync::watch::channel(None); // Polling and writes have separate workers: a missing D-Bus service must // never hold up volume/brightness changes. Publish only coherent polls. - let shared = Arc::new(( - Mutex::new((0u64, false, None::, false)), - Condvar::new(), - )); + let shared = Arc::new((Mutex::new((0u64, false, None::, false)), Condvar::new())); let polling = shared.clone(); thread::spawn(move || { @@ -249,8 +241,7 @@ pub fn available(program: &str) -> bool { return executable(Path::new(program)); } - 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)))) } fn run(program: &str, args: &[&str]) -> Result { @@ -302,10 +293,7 @@ impl StatusBus { } } - fn query( - &mut self, - query: impl FnOnce(&zbus::blocking::Connection) -> zbus::Result, - ) -> Option { + fn query(&mut self, query: impl FnOnce(&zbus::blocking::Connection) -> zbus::Result) -> Option { if self.connection.is_none() { let builder = if self.system { zbus::blocking::connection::Builder::system() @@ -313,11 +301,7 @@ impl StatusBus { zbus::blocking::connection::Builder::session() }; - self.connection = builder - .ok()? - .method_timeout(Duration::from_secs(2)) - .build() - .ok(); + self.connection = builder.ok()?.method_timeout(Duration::from_secs(2)).build().ok(); } match query(self.connection.as_ref()?) { @@ -397,8 +381,7 @@ pub fn parse_audio(value: &str) -> Option<(u8, bool)> { } fn audio() -> Option