Files
feresaria/compositor/src/state.rs
T

1259 lines
44 KiB
Rust

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<Window>,
pub workspaces: WorkspaceSet,
pub output_workspaces: OutputWorkspaceMap,
output_ids: HashMap<Output, OutputId>,
output_identity_ids: HashMap<String, OutputId>,
pub window_ids: HashMap<Window, WindowId>,
pub window_geometry: HashMap<WindowId, WindowGeometry>,
pub focused_window: Option<WindowId>,
pub cursor_status: CursorImageStatus,
pub(crate) cursor_theme: xcursor::CursorTheme,
pub(crate) named_cursors: HashMap<CursorIcon, crate::cursor::NamedCursor>,
pub intercepted_keys: HashSet<smithay::input::keyboard::Keycode>,
pub idle_inhibitors: HashMap<WlSurface, usize>,
pub active_shortcuts_inhibitor: Option<KeyboardShortcutsInhibitor>,
pub direct_backend: Option<crate::backends::direct::DirectBackendState>,
pending_dmabuf_imports: Vec<(Dmabuf, ImportNotifier)>,
pub(crate) pending_screencopies: Vec<crate::handlers::screencopy::PendingScreencopy>,
next_window_id: u64,
next_output_id: u64,
last_animation_tick: Instant,
pub popups: PopupManager,
pub seat: Seat<Self>,
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<Self>,
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<Self>,
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<f64, Logical>) {
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<R>(&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::<Self>() {
tracing::debug!(?error, "dma-buf client disappeared before import completed");
}
} else {
notifier.failed();
}
}
}
pub fn new(
event_loop: &mut EventLoop<'static, Self>,
display: Display<Self>,
) -> Result<Self, Box<dyn Error>> {
let display_handle = display.handle();
crate::handlers::screencopy::init_global(&display_handle);
let compositor_state = CompositorState::new::<Self>(&display_handle);
let cursor_shape_state = CursorShapeManagerState::new::<Self>(&display_handle);
let xdg_shell_state = XdgShellState::new::<Self>(&display_handle);
let decoration_state = XdgDecorationState::new::<Self>(&display_handle);
let dmabuf_state = DmabufState::new();
let fractional_scale_state = FractionalScaleManagerState::new::<Self>(&display_handle);
let idle_inhibit_state = IdleInhibitManagerState::new::<Self>(&display_handle);
let idle_notifier_state = IdleNotifierState::new(&display_handle, event_loop.handle());
let input_method_enabled =
std::env::var_os("FERESE_ENABLE_INPUT_METHOD").is_some_and(|value| value == "1");
let input_method_manager_state =
InputMethodManagerState::new::<Self, _>(&display_handle, move |_| input_method_enabled);
let keyboard_shortcuts_inhibit_state =
KeyboardShortcutsInhibitState::new::<Self>(&display_handle);
let shm_state = ShmState::new::<Self>(&display_handle, Vec::new());
let single_pixel_buffer_state = SinglePixelBufferState::new::<Self>(&display_handle);
let text_input_manager_state = TextInputManagerState::new::<Self>(&display_handle);
let output_manager_state = OutputManagerState::new_with_xdg_output::<Self>(&display_handle);
let pointer_constraints_state = PointerConstraintsState::new::<Self>(&display_handle);
let presentation_state =
PresentationState::new::<Self>(&display_handle, libc::CLOCK_MONOTONIC as u32);
let data_device_state = DataDeviceState::new::<Self>(&display_handle);
let primary_selection_state = PrimarySelectionState::new::<Self>(&display_handle);
let relative_pointer_state = RelativePointerManagerState::new::<Self>(&display_handle);
let viewporter_state = ViewporterState::new::<Self>(&display_handle);
let layer_shell_state = WlrLayerShellState::new::<Self>(&display_handle);
let xdg_activation_state = XdgActivationState::new::<Self>(&display_handle);
let xdg_foreign_state = XdgForeignState::new::<Self>(&display_handle);
let 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<Self>,
event_loop: &mut EventLoop<'static, Self>,
) -> Result<OsString, Box<dyn Error>> {
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<f64, Logical>,
) -> Option<(WlSurface, Point<f64, Logical>)> {
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<f64, Logical>,
) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
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<f64, Logical>,
) -> Option<(WlSurface, Point<f64, Logical>)> {
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<f64, Logical>,
layers: &[Layer],
) -> Option<(LayerSurface, WlSurface, Point<f64, Logical>)> {
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<f64, Logical>) -> Option<Window> {
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::<Vec<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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<i32, Logical>,
size: Size<i32, Logical>,
) {
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<Rect> {
let output = self
.focused_output()
.or_else(|| self.space.outputs().next())?;
self.output_bounds_for(output)
}
fn output_bounds_for(&self, output: &Output) -> Option<Rect> {
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<Rect> {
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::<Vec<_>>();
for output in outputs {
layer_map_for_output(&output).arrange();
}
}
fn logical_window_rect(&self, window: WindowId, bounds: Rect) -> Option<Rect> {
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<LayoutResult, ferese_layout::LayoutError> {
self.workspaces.active().layout.geometry_with_constraints(
bounds,
GapConfig::default(),
&self.window_constraints(),
self.focused_window,
)
}
fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
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::<SurfaceCachedState>();
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<ClientSize> {
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));
}
}