use std::{ collections::{HashMap, HashSet}, error::Error, ffi::OsString, sync::Arc, time::{Duration, Instant}, }; use ferese_animation::{ClientSize, SpringConfig, WindowGeometry}; use ferese_core::{OutputGeometry, OutputId, OutputWorkspaceMap, WindowPlacement, WorkspaceSet}; use ferese_layout::{Axis, Direction, GapConfig, LayoutResult, Rect, SizeConstraints, WindowId}; use smithay::{ backend::{ allocator::dmabuf::Dmabuf, drm::{DrmEventTime, DrmNode}, renderer::ImportDma, }, desktop::{ LayerSurface, PopupManager, Space, Window, WindowSurfaceType, layer_map_for_output, utils::send_frames_surface_tree, }, input::{ Seat, SeatState, 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, Size}, wayland::{ 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}, shm::ShmState, single_pixel_buffer::SinglePixelBufferState, socket::ListeningSocketSource, text_input::TextInputManagerState, viewporter::ViewporterState, xdg_activation::XdgActivationState, xdg_foreign::XdgForeignState, }, }; pub struct Ferese { pub start_time: Instant, pub socket_name: OsString, pub display_handle: DisplayHandle, pub loop_signal: LoopSignal, pub space: Space, pub workspaces: WorkspaceSet, pub output_workspaces: OutputWorkspaceMap, output_ids: HashMap, output_identity_ids: HashMap, pub window_ids: HashMap, pub window_geometry: HashMap, pub focused_window: Option, pub cursor_status: CursorImageStatus, pub(crate) cursor_theme: xcursor::CursorTheme, pub(crate) named_cursors: HashMap, pub intercepted_keys: HashSet, pub idle_inhibitors: HashMap, pub active_shortcuts_inhibitor: Option, pub direct_backend: Option, pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>, pub(crate) pending_screencopies: Vec, next_window_id: u64, next_output_id: u64, last_animation_tick: Instant, pub popups: PopupManager, pub seat: Seat, pub compositor_state: CompositorState, pub cursor_shape_state: CursorShapeManagerState, pub data_device_state: DataDeviceState, pub decoration_state: XdgDecorationState, pub dmabuf_state: DmabufState, pub fractional_scale_state: FractionalScaleManagerState, pub idle_inhibit_state: IdleInhibitManagerState, pub idle_notifier_state: IdleNotifierState, pub input_method_manager_state: InputMethodManagerState, pub keyboard_shortcuts_inhibit_state: KeyboardShortcutsInhibitState, pub output_manager_state: OutputManagerState, pub pointer_constraints_state: PointerConstraintsState, pub presentation_state: PresentationState, pub primary_selection_state: PrimarySelectionState, pub relative_pointer_state: RelativePointerManagerState, pub seat_state: SeatState, pub shm_state: ShmState, pub single_pixel_buffer_state: SinglePixelBufferState, pub text_input_manager_state: TextInputManagerState, pub viewporter_state: ViewporterState, pub layer_shell_state: WlrLayerShellState, pub xdg_activation_state: XdgActivationState, pub xdg_foreign_state: XdgForeignState, pub xdg_shell_state: XdgShellState, } impl Ferese { pub fn register_output(&mut self, output: &Output, identity: String) { let Some(geometry) = self.space.output_geometry(output) else { tracing::error!(output = %output.name(), "cannot register an unmapped output"); return; }; let output_id = *self.output_identity_ids.entry(identity).or_insert_with(|| { let id = OutputId(self.next_output_id); self.next_output_id = self.next_output_id.saturating_add(1); id }); let fallback_workspace = (1..) .find_map(|index| { let workspace = self.workspaces.ensure_numeric(index).ok()?; self.output_workspaces .output_for_workspace(workspace) .is_none() .then_some(workspace) }) .expect("numeric workspaces are inexhaustible"); let geometry = OutputGeometry::new( geometry.loc.x, geometry.loc.y, geometry.size.w, geometry.size.h, ); match self .output_workspaces .connect(output_id, geometry, fallback_workspace) { Ok(workspace) => { self.output_ids.insert(output.clone(), output_id); if self.output_workspaces.focused_output() == Some(output_id) { self.activate_output_workspace(output_id, workspace); } } Err(error) => { tracing::error!(%error, output = %output.name(), "failed to register output") } } } pub fn unregister_output(&mut self, output: &Output) { let Some(output_id) = self.output_ids.remove(output) else { return; }; self.pending_screencopies.retain(|capture| { if capture.output == *output { capture.frame.failed(); false } else { true } }); self.space.unmap_output(output); match self.output_workspaces.disconnect(output_id) { Ok(Some(target)) => { if let Some(workspace) = self.output_workspaces.active_workspace(target) { self.activate_output_workspace(target, workspace); } } Ok(None) => { self.focused_window = None; self.restore_keyboard_focus(); } Err(error) => { tracing::error!(%error, output = %output.name(), "failed to unregister output") } } self.relayout(); } pub(crate) fn is_focused_output(&self, output: &Output) -> bool { self.output_ids.get(output).copied() == self.output_workspaces.focused_output() } fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) { if let Err(error) = self.output_workspaces.focus_output(output) { tracing::error!(%error, "failed to focus output"); return; } match self.workspaces.activate(workspace) { Ok(focus) => self.focused_window = focus, Err(error) => tracing::error!(%error, "failed to activate output workspace"), } } fn focused_output(&self) -> Option<&Output> { let focused = self.output_workspaces.focused_output()?; self.output_ids .iter() .find_map(|(output, id)| (*id == focused).then_some(output)) } pub(crate) fn focus_output_at(&mut self, position: Point) { let output = self.space.outputs().find_map(|output| { let geometry = self.space.output_geometry(output)?; (position.x >= f64::from(geometry.loc.x) && position.y >= f64::from(geometry.loc.y) && position.x < f64::from(geometry.loc.x + geometry.size.w) && position.y < f64::from(geometry.loc.y + geometry.size.h)) .then(|| output.clone()) }); let Some(output) = output else { return; }; let Some(output_id) = self.output_ids.get(&output).copied() else { return; }; if self.output_workspaces.focused_output() == Some(output_id) { return; } let Some(workspace) = self.output_workspaces.active_workspace(output_id) else { return; }; self.activate_output_workspace(output_id, workspace); self.relayout(); } pub(crate) fn queue_dmabuf_import(&mut self, dmabuf: Dmabuf, notifier: ImportNotifier) { self.pending_dmabuf_imports.push((dmabuf, notifier)); } pub(crate) fn process_dmabuf_imports(&mut self, renderer: &mut R) where R: ImportDma, { for (dmabuf, notifier) in self.pending_dmabuf_imports.drain(..) { if renderer.import_dmabuf(&dmabuf, None).is_ok() { if let Err(error) = notifier.successful::() { tracing::debug!(?error, "dma-buf client disappeared before import completed"); } } else { notifier.failed(); } } } pub fn new( event_loop: &mut EventLoop<'static, Self>, display: Display, ) -> Result> { let display_handle = display.handle(); crate::handlers::screencopy::init_global(&display_handle); let compositor_state = CompositorState::new::(&display_handle); let cursor_shape_state = CursorShapeManagerState::new::(&display_handle); let xdg_shell_state = XdgShellState::new::(&display_handle); let decoration_state = XdgDecorationState::new::(&display_handle); let dmabuf_state = DmabufState::new(); 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 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 shm_state = ShmState::new::(&display_handle, Vec::new()); let single_pixel_buffer_state = SinglePixelBufferState::new::(&display_handle); 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 data_device_state = DataDeviceState::new::(&display_handle); let primary_selection_state = PrimarySelectionState::new::(&display_handle); let relative_pointer_state = RelativePointerManagerState::new::(&display_handle); let viewporter_state = ViewporterState::new::(&display_handle); let layer_shell_state = WlrLayerShellState::new::(&display_handle); let xdg_activation_state = XdgActivationState::new::(&display_handle); let xdg_foreign_state = XdgForeignState::new::(&display_handle); let mut seat_state = SeatState::new(); let mut seat = seat_state.new_wl_seat(&display_handle, "ferese-winit"); seat.add_keyboard(Default::default(), 200, 25)?; seat.add_pointer(); let socket_name = Self::init_wayland_listener(display, event_loop)?; let start_time = Instant::now(); let cursor_theme = crate::cursor::cursor_theme(); let default_cursor = crate::cursor::load_named_cursor(&cursor_theme, CursorIcon::Default); let named_cursors = HashMap::from([(CursorIcon::Default, default_cursor)]); Ok(Self { start_time, socket_name, display_handle, loop_signal: event_loop.get_signal(), space: Space::default(), workspaces: WorkspaceSet::default(), output_workspaces: OutputWorkspaceMap::default(), output_ids: HashMap::new(), output_identity_ids: HashMap::new(), window_ids: HashMap::new(), window_geometry: HashMap::new(), focused_window: None, cursor_status: CursorImageStatus::default_named(), cursor_theme, named_cursors, intercepted_keys: HashSet::new(), idle_inhibitors: HashMap::new(), active_shortcuts_inhibitor: None, direct_backend: None, pending_dmabuf_imports: Vec::new(), pending_screencopies: Vec::new(), next_window_id: 1, next_output_id: 1, last_animation_tick: start_time, popups: PopupManager::default(), seat, compositor_state, cursor_shape_state, data_device_state, decoration_state, dmabuf_state, fractional_scale_state, idle_inhibit_state, idle_notifier_state, input_method_manager_state, keyboard_shortcuts_inhibit_state, output_manager_state, pointer_constraints_state, presentation_state, primary_selection_state, relative_pointer_state, seat_state, shm_state, single_pixel_buffer_state, text_input_manager_state, viewporter_state, layer_shell_state, xdg_activation_state, xdg_foreign_state, xdg_shell_state, }) } fn init_wayland_listener( display: Display, event_loop: &mut EventLoop<'static, Self>, ) -> Result> { let listening_socket = ListeningSocketSource::new_auto()?; let socket_name = listening_socket.socket_name().to_os_string(); let loop_handle = event_loop.handle(); loop_handle.insert_source(listening_socket, |client_stream, _, state| { if let Err(error) = state .display_handle .insert_client(client_stream, Arc::new(ClientState::default())) { tracing::warn!(%error, "failed to register Wayland client"); } })?; loop_handle.insert_source( Generic::new(display, Interest::READ, Mode::Level), |_, display, state| { // SAFETY: this event source owns the display for the loop lifetime. unsafe { display.get_mut().dispatch_clients(state)? }; Ok(PostAction::Continue) }, )?; Ok(socket_name) } pub fn surface_under( &self, position: Point, ) -> Option<(WlSurface, Point)> { self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) .map(|(_, surface, origin)| (surface, origin)) .or_else(|| self.window_surface_under(position)) .or_else(|| { self.layer_surface_under(position, &[Layer::Bottom, Layer::Background]) .map(|(_, surface, origin)| (surface, origin)) }) } pub fn layer_under( &self, position: Point, ) -> Option<(LayerSurface, WlSurface, Point)> { if let Some(layer) = self.layer_surface_under(position, &[Layer::Overlay, Layer::Top]) { return Some(layer); } if self.window_surface_under(position).is_some() { return None; } self.layer_surface_under(position, &[Layer::Bottom, Layer::Background]) } fn window_surface_under( &self, position: Point, ) -> Option<(WlSurface, Point)> { self.space.elements().rev().find_map(|window| { let id = self.window_ids.get(window)?; let geometry = self.window_geometry.get(id)?; let visual = geometry.visual.current; let inside_visual = position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width && position.y < visual.y + visual.height; if !inside_visual { return None; } let (source_x, source_y) = geometry.inverse_visual_point(position.x, position.y)?; let source_point = Point::from((source_x, source_y)) + window.geometry().loc.to_f64(); window .surface_under(source_point, WindowSurfaceType::ALL) .map(|(surface, surface_location)| { let surface_point = source_point - surface_location.to_f64(); (surface, position - surface_point) }) }) } fn layer_surface_under( &self, position: Point, layers: &[Layer], ) -> Option<(LayerSurface, WlSurface, Point)> { let output = self.space.output_under(position).next()?; let output_geometry = self.space.output_geometry(output)?; let output_position = position - output_geometry.loc.to_f64(); let map = layer_map_for_output(output); for requested in layers { 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) else { continue; }; let origin = output_geometry.loc + geometry.loc + surface_location; return Some((layer.clone(), surface, origin.to_f64())); } } None } pub fn send_cursor_frame(&self, output: &Output) { let CursorImageStatus::Surface(surface) = &self.cursor_status else { return; }; send_frames_surface_tree( surface, output, self.start_time.elapsed(), Some(Duration::ZERO), |_, _| Some(output.clone()), ); } pub fn window_under_visual(&self, position: Point) -> Option { self.space.elements().rev().find_map(|window| { let id = self.window_ids.get(window)?; let visual = self.window_geometry.get(id)?.visual.current; (position.x >= visual.x && position.y >= visual.y && position.x < visual.x + visual.width && position.y < visual.y + visual.height) .then(|| window.clone()) }) } pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> { let id = self.window_ids.get(window)?; self.window_geometry.get(id)?.visual_scale() } pub fn add_tiled_window(&mut self, window: Window) { let id = WindowId(self.next_window_id); self.next_window_id += 1; let focus_new_window = self.workspaces.active().fullscreen.is_none(); let axis = self .output_bounds() .and_then(|bounds| { self.workspaces .active() .layout .automatic_axis(self.focused_window, bounds) .ok() }) .unwrap_or(Axis::Horizontal); if let Err(error) = self.workspaces.insert_window(id, axis, 0.5) { tracing::error!(%error, ?id, "failed to insert window into layout"); return; } self.window_ids.insert(window.clone(), id); if focus_new_window { self.focused_window = Some(id); } self.space.map_element(window, (0, 0), focus_new_window); self.relayout(); if focus_new_window { self.restore_keyboard_focus(); } } pub fn add_transient_window(&mut self, window: Window, parent: WindowId) { let Some(workspace) = self.workspaces.workspace_for_window(parent) else { self.add_tiled_window(window); return; }; let Some(bounds) = self.output_bounds() else { self.add_tiled_window(window); return; }; let parent_rect = self.logical_window_rect(parent, bounds).unwrap_or(bounds); let rect = centered_transient_rect(parent_rect); let id = WindowId(self.next_window_id); self.next_window_id += 1; if let Err(error) = self .workspaces .insert_floating_window(id, workspace, rect, false) { tracing::error!(%error, ?id, ?parent, "failed to insert transient window"); return; } self.window_ids.insert(window.clone(), id); self.space.map_element(window, (0, 0), false); self.relayout(); } pub fn make_window_transient(&mut self, window: &Window, parent: WindowId) { 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" ); return; } let Some(bounds) = self.output_bounds() else { return; }; let parent_rect = self.logical_window_rect(parent, bounds).unwrap_or(bounds); let rect = centered_transient_rect(parent_rect); let result = match self.workspaces.placement(id) { Some(WindowPlacement::Tiled) => self .workspaces .toggle_floating(id, rect, Axis::Horizontal, 0.5) .map(|_| ()), Some(WindowPlacement::Floating { .. }) => self.workspaces.set_floating_rect(id, rect), None => return, }; if let Err(error) = result { tracing::warn!(%error, ?id, ?parent, "failed to apply transient placement"); return; } self.relayout(); } pub fn remove_tiled_window(&mut self, window: &Window) { let Some(id) = self.window_ids.remove(window) else { return; }; self.space.unmap_elem(window); self.window_geometry.remove(&id); if let Err(error) = self.workspaces.remove_window(id) { tracing::error!(%error, ?id, "failed to remove window from layout"); } if self.focused_window == Some(id) { self.focused_window = self.workspaces.active().last_focused; } self.relayout(); } pub fn relayout(&mut self) { self.arrange_layers(); let outputs = self.space.outputs().cloned().collect::>(); let mut visible = HashSet::new(); let mut placements = Vec::new(); for output in outputs { let Some(output_id) = self.output_ids.get(&output).copied() else { continue; }; let Some(workspace_id) = self.output_workspaces.active_workspace(output_id) else { continue; }; let Some(workspace) = self.workspaces.workspace(workspace_id) else { continue; }; let Some(bounds) = self.output_bounds_for(&output) else { continue; }; let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds); let focused = self .focused_window .filter(|window| { self.workspaces.workspace_for_window(*window) == Some(workspace_id) }) .or(workspace.last_focused); let layout = match workspace.layout.geometry_with_constraints( bounds, GapConfig::default(), &self.window_constraints(), focused, ) { Ok(layout) => layout, Err(error) => { tracing::error!(%error, ?workspace_id, "failed to compute tiled geometry"); continue; } }; for warning in layout.warnings { tracing::warn!( window = ?warning.window, kind = ?warning.kind, requested = warning.requested, assigned = warning.assigned, "window size constraint could not be satisfied exactly" ); } for (window, id) in &self.window_ids { if self.workspaces.workspace_for_window(*id) != Some(workspace_id) { continue; } let rect = if workspace.fullscreen == Some(*id) { fullscreen_bounds } else if workspace.fullscreen.is_some() { continue; } else { match self.workspaces.placement(*id) { Some(WindowPlacement::Tiled) => { let Some(rect) = layout.geometry.get(id).copied() else { continue; }; rect } Some(WindowPlacement::Floating { rect }) => rect, None => continue, } }; let is_fullscreen = workspace.fullscreen == Some(*id); let is_floating = matches!( self.workspaces.placement(*id), Some(WindowPlacement::Floating { .. }) ); visible.insert(*id); placements.push((window.clone(), *id, rect, is_fullscreen, is_floating)); } } let hidden = self .window_ids .iter() .filter(|(_, id)| !visible.contains(id)) .map(|(window, _)| window.clone()) .collect::>(); for window in hidden { self.space.unmap_elem(&window); } let now = self.start_time.elapsed(); for (window, id, rect, is_fullscreen, is_floating) in placements { let committed_size = client_size(&window); let geometry = self .window_geometry .entry(id) .or_insert_with(|| WindowGeometry::new(rect, committed_size)); let requested_size = geometry.set_logical_target(rect, now); let visual = geometry.visual.current; let location = (visual.x.round() as i32, visual.y.round() as i32); self.space.map_element(window.clone(), location, false); if let Some(toplevel) = window.toplevel() { let state_changed = toplevel.with_pending_state(|state| { if let Some(size) = requested_size { state.size = Some((size.width, size.height).into()); } let fullscreen_changed = if is_fullscreen { state.states.set(xdg_toplevel::State::Fullscreen) } else { state.states.unset(xdg_toplevel::State::Fullscreen) }; let decoration_mode = if is_floating && !is_fullscreen { DecorationMode::ClientSide } else { DecorationMode::ServerSide }; let decoration_changed = state.decoration_mode != Some(decoration_mode); state.decoration_mode = Some(decoration_mode); fullscreen_changed || decoration_changed }); if requested_size.is_some() || state_changed { toplevel.send_pending_configure(); } } } crate::backends::direct::render_all(self); } pub fn advance_animations(&mut self, now: Instant) -> bool { let delta = now.saturating_duration_since(self.last_animation_tick); self.last_animation_tick = now; self.advance_animations_by(delta) } pub fn record_drm_presentation( &mut self, node: DrmNode, crtc: crtc::Handle, time: DrmEventTime, sequence: u32, ) { let delta = self .direct_backend .as_mut() .and_then(|backend| backend.record_presentation(node, crtc, time, sequence)); if let Some(delta) = delta { self.advance_animations_by(delta); } } fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool { let windows = self .space .elements() .filter_map(|window| { self.window_ids .get(window) .copied() .map(|id| (window.clone(), id)) }) .collect::>(); let mut active_animation = false; for (window, id) in windows { let Some(geometry) = self.window_geometry.get_mut(&id) else { continue; }; active_animation |= geometry.advance(delta, SpringConfig::default(), true); if let Some(size) = geometry.client.expire_wait(self.start_time.elapsed()) { tracing::warn!( ?id, configured_width = size.width, configured_height = size.height, committed = ?geometry.client.committed_size, "client did not commit the final configured size within 500 ms" ); } let visual = geometry.visual.current; self.space.map_element( window, (visual.x.round() as i32, visual.y.round() as i32), false, ); } active_animation } pub fn record_client_commit(&mut self, window: &Window) { let Some(id) = self.window_ids.get(window) else { return; }; let Some(size) = client_size(window) else { return; }; let Some(geometry) = self.window_geometry.get_mut(id) else { return; }; let matches_target = geometry.client.commit(size); tracing::debug!( ?id, ?size, matches_target, "recorded client geometry commit" ); } pub fn focus_direction(&mut self, direction: Direction) { let Some(current) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; let Ok(Some(next)) = self .workspaces .active() .layout .directional_neighbor(current, direction, bounds) else { return; }; if let Err(error) = self.workspaces.focus_window(next) { tracing::error!(%error, ?next, "failed to update workspace focus"); return; } let Some(window) = self .window_ids .iter() .find_map(|(window, id)| (*id == next).then(|| window.clone())) else { return; }; let Some(surface) = window .toplevel() .map(|toplevel| toplevel.wl_surface().clone()) else { return; }; self.focused_window = Some(next); self.space.raise_element(&window, true); 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() { toplevel.send_pending_configure(); } } } pub fn move_direction(&mut self, direction: Direction) { let Some(current) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; match self .workspaces .active_mut() .layout .move_window(current, direction, bounds) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?current, "failed to move tiled window"), } } pub fn resize_direction(&mut self, direction: Direction) { const RESIZE_STEP: f64 = 0.05; let Some(current) = self.focused_window else { return; }; match self .workspaces .active_mut() .layout .resize_window(current, direction, RESIZE_STEP) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => tracing::error!(%error, ?current, "failed to resize tiled window"), } } pub fn toggle_focused_floating(&mut self) { let Some(window) = self.focused_window else { return; }; let Some(bounds) = self.output_bounds() else { return; }; let floating_rect = match self.workspaces.placement(window) { Some(WindowPlacement::Tiled) => self .tiled_layout(bounds) .ok() .and_then(|layout| layout.geometry.get(&window).copied()) .unwrap_or_else(|| centered_floating_rect(bounds)), Some(WindowPlacement::Floating { rect }) => rect, None => return, }; let axis = self .workspaces .active() .layout .automatic_axis(Some(window), bounds) .unwrap_or(Axis::Horizontal); if let Err(error) = self .workspaces .toggle_floating(window, floating_rect, axis, 0.5) { tracing::error!(%error, ?window, "failed to toggle floating window"); return; } self.relayout(); } pub fn toggle_focused_fullscreen(&mut self) { let Some(window) = self.focused_window else { return; }; if let Err(error) = self.workspaces.toggle_fullscreen(window) { tracing::error!(%error, ?window, "failed to toggle fullscreen window"); return; } self.relayout(); } pub fn close_focused_window(&self) { let Some(focused) = self.focused_window else { return; }; let Some(toplevel) = self .window_ids .iter() .find_map(|(window, id)| (*id == focused).then(|| window.toplevel()).flatten()) else { return; }; toplevel.send_close(); } pub fn set_window_fullscreen(&mut self, window: WindowId, enabled: bool) { match self.workspaces.set_fullscreen(window, enabled) { Ok(true) => self.relayout(), Ok(false) => {} Err(error) => { tracing::error!(%error, ?window, enabled, "failed to set fullscreen window") } } } pub fn is_floating_window(&self, window: &Window) -> bool { self.window_ids .get(window) .and_then(|id| self.workspaces.placement(*id)) .is_some_and(|placement| matches!(placement, WindowPlacement::Floating { .. })) } pub fn set_floating_window_geometry( &mut self, window: &Window, location: Point, size: Size, ) { let Some(id) = self.window_ids.get(window).copied() else { return; }; let rect = Rect::new( location.x as f64, location.y as f64, size.w.max(1) as f64, size.h.max(1) as f64, ); if let Err(error) = self.workspaces.set_floating_rect(id, rect) { tracing::error!(%error, ?id, "failed to update floating window geometry"); } } pub fn switch_workspace(&mut self, index: u32) { let workspace = match self.workspaces.ensure_numeric(index) { Ok(workspace) => workspace, Err(error) => { tracing::error!(%error, index, "failed to switch workspace"); return; } }; let Some(output) = self.output_workspaces.focused_output() else { return; }; let owner = match self.output_workspaces.switch_workspace(output, workspace) { Ok(ferese_core::WorkspaceSwitch::Activated(output)) | Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output, Err(error) => { tracing::error!(%error, index, "failed to assign workspace to output"); return; } }; self.activate_output_workspace(owner, workspace); self.relayout(); self.restore_keyboard_focus(); } pub fn move_focused_to_workspace(&mut self, index: u32) { let Some(window) = self.focused_window else { return; }; let axis = self .output_bounds() .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() }) .unwrap_or(Axis::Horizontal); 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"); return; } }; if let Some(output) = self.output_workspaces.focused_output() && let Err(error) = self.output_workspaces.assign_workspace(output, destination) { tracing::error!(%error, ?destination, "failed to assign destination workspace"); } self.focused_window = self.workspaces.active().last_focused; self.relayout(); self.restore_keyboard_focus(); } pub(crate) fn restore_keyboard_focus(&mut self) { let surface = self.focused_window.and_then(|focused| { self.window_ids.iter().find_map(|(window, id)| { (*id == focused) .then(|| window.toplevel()) .flatten() .map(|toplevel| toplevel.wl_surface().clone()) }) }); self.seat .get_keyboard() .expect("seat has a keyboard") .set_focus(self, surface, smithay::utils::SERIAL_COUNTER.next_serial()); } fn output_bounds(&self) -> Option { let output = self .focused_output() .or_else(|| self.space.outputs().next())?; self.output_bounds_for(output) } fn output_bounds_for(&self, output: &Output) -> Option { let geometry = self.space.output_geometry(output)?; let zone = layer_map_for_output(output).non_exclusive_zone(); Some(Rect::new( (geometry.loc.x + zone.loc.x) as f64, (geometry.loc.y + zone.loc.y) as f64, zone.size.w.max(0) as f64, zone.size.h.max(0) as f64, )) } fn full_output_bounds_for(&self, output: &Output) -> Option { let geometry = self.space.output_geometry(output)?; Some(Rect::new( geometry.loc.x as f64, geometry.loc.y as f64, geometry.size.w as f64, geometry.size.h as f64, )) } fn arrange_layers(&self) { let outputs = self.space.outputs().cloned().collect::>(); for output in outputs { layer_map_for_output(&output).arrange(); } } fn logical_window_rect(&self, window: WindowId, bounds: Rect) -> Option { let workspace = self.workspaces.workspace_for_window(window)?; let workspace = self.workspaces.workspace(workspace)?; if workspace.fullscreen == Some(window) { return Some(bounds); } match self.workspaces.placement(window)? { WindowPlacement::Tiled => workspace .layout .geometry_with_constraints( bounds, GapConfig::default(), &self.window_constraints(), self.focused_window, ) .ok()? .geometry .get(&window) .copied(), WindowPlacement::Floating { rect } => Some(rect), } } fn tiled_layout(&self, bounds: Rect) -> Result { self.workspaces.active().layout.geometry_with_constraints( bounds, GapConfig::default(), &self.window_constraints(), self.focused_window, ) } fn window_constraints(&self) -> HashMap { self.window_ids .iter() .filter_map(|(window, id)| { let toplevel = window.toplevel()?; let (minimum, maximum) = with_states(toplevel.wl_surface(), |states| { let mut cached = states.cached_state.get::(); let state = cached.current(); (state.min_size, state.max_size) }); Some(( *id, SizeConstraints { min_width: minimum.w.max(1) as f64, min_height: minimum.h.max(1) as f64, max_width: (maximum.w > 0).then_some(maximum.w as f64), max_height: (maximum.h > 0).then_some(maximum.h as f64), }, )) }) .collect() } } fn centered_floating_rect(bounds: Rect) -> Rect { let width = (bounds.width * 0.6).max(1.0); let height = (bounds.height * 0.6).max(1.0); Rect::new( bounds.x + (bounds.width - width) / 2.0, bounds.y + (bounds.height - height) / 2.0, width, height, ) } fn centered_transient_rect(parent: Rect) -> Rect { let width = (parent.width * 0.75).clamp(1.0, 640.0); let height = (parent.height * 0.75).clamp(1.0, 480.0); Rect::new( parent.x + (parent.width - width) / 2.0, parent.y + (parent.height - height) / 2.0, width, height, ) } fn client_size(window: &Window) -> Option { let size = window.geometry().size; (size.w > 0 && size.h > 0).then_some(ClientSize { width: size.w, height: size.h, }) } #[derive(Default)] pub struct ClientState { pub compositor_state: CompositorClientState, } impl ClientData for ClientState { fn initialized(&self, _client_id: ClientId) {} fn disconnected(&self, _client_id: ClientId, _reason: DisconnectReason) {} } #[cfg(test)] mod tests { use super::*; #[test] fn transient_geometry_is_centered_and_bounded_by_its_parent() { let parent = Rect::new(100.0, 50.0, 1_000.0, 800.0); let transient = centered_transient_rect(parent); assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0)); } }