Split shell, settings, and compositor into focused modules

This commit is contained in:
Samuel Onoja committed 2026-10-01 12:47:26 +01:00
1 parent 2b9f30fa5d
commit cbf048566a
38 files changed
+9998 -9817

No files matched your search

+352
View File
@@ -0,0 +1,352 @@
use super::*;
impl Ferese {
pub fn advance_animations(&mut self, now: Instant) -> bool {
let delta = crate::presentation::frame_delta(&mut 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) {
if let Some(backend) = self.direct_backend.as_mut() {
backend.record_presentation(node, crtc, time, sequence);
}
}
pub(crate) fn animation_fallback_deadline(&self, interval: std::time::Duration) -> Instant {
self.last_animation_tick + interval.saturating_mul(2)
}
pub(crate) fn reset_animation_clock(&mut self) {
self.last_animation_tick = Instant::now();
}
pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
let delta = delta.mul_f64(self.animation_speed);
if self
.focus_swipe
.as_ref()
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
{
self.focus_swipe = None;
}
let visible_workspaces = if self.viewport_animations.is_empty() {
HashSet::new()
} else {
self.space
.outputs()
.filter_map(|output| self.output_ids.get(output))
.filter_map(|output| self.output_workspaces.active_workspace(*output))
.collect::<HashSet<_>>()
};
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;
let mut completed_slides = Vec::new();
for (output, slide) in &mut self.workspace_slides {
if slide.held_progress.is_some() {
continue;
}
if slide.advance(delta) {
active_animation = true;
} else {
completed_slides.push(*output);
}
}
self.dismissing_popups.retain_mut(|(root, popup, motion)| {
if !smithay::utils::IsAlive::alive(popup.wl_surface()) {
return false;
}
let duration = if self.animations_enabled { 140.0 } else { 0.0 };
let active = motion.advance_visual(0.0, delta, duration);
crate::effects::fade_dismissed_surface(popup.wl_surface(), motion.current);
active_animation |= active;
if !active {
let _ = PopupManager::dismiss_popup(root, popup);
}
active
});
let dim_settings = self.theme_settings.inactive_dim;
let duration = if self.animations_enabled {
dim_settings.duration_ms
} else {
0.0
};
let mut dim_changed = false;
// Include hidden workspace windows: overview can present them too.
// Read selection once so these immutable fields can be borrowed alongside
// window_focus without allocating a temporary window-ID vector.
let selected_window = if self.overview.is_active() {
self.overview.selected()
} else {
self.focused_window
};
for id in self.window_ids.values().copied() {
let selected = selected_window == Some(id);
let target = if selected { 1.0 } else { 0.0 };
let focus = self
.window_focus
.entry(id)
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
let previous = focus.current;
active_animation |= focus.advance_visual(target, delta, duration);
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 dim = self
.window_dimming
.entry(*id)
.or_insert_with(|| crate::dimming::DimAnimation::new(target));
let previous = dim.current;
active_animation |= dim.advance_visual(target, delta, duration);
dim_changed |= previous != dim.current;
}
let mut ready_to_close = Vec::new();
for (id, animation) in &mut self.closing_windows {
if animation.advance(delta, self.animations_enabled) {
ready_to_close.push(*id);
} else if !animation.close_sent {
active_animation = true;
}
}
for id in ready_to_close {
self.send_window_close(id);
}
let now = self.start_time.elapsed();
self.resize_transactions.retain(|id, transaction| {
if transaction.expired(now) {
tracing::warn!(?id, "resize presentation deadline reached");
false
} else {
true
}
});
let blocked_workspaces = self
.resize_transactions
.keys()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let animations_enabled = self.animations_enabled;
let animation_speed = self.animation_speed;
self.resize_snapshots.retain(|id, snapshot| {
if !animations_enabled {
return false;
}
let waiting_for_client = self
.workspaces
.workspace_for_window(*id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
// A shrinking client's destination buffer arrives before the
// animated bounds reach it. Keep the old native pixels covering
// that strip rather than fading them into the neutral resize fill.
let uncovered = self.window_geometry.get(id).is_some_and(|geometry| {
geometry.client.committed_size.is_some_and(|size| {
crate::presentation::resize_needs_old_frame(
geometry.visual.current,
geometry.logical,
size.width,
size.height,
)
})
});
let blocked = waiting_for_client || uncovered;
let active = crate::presentation::advance_handoff(
&mut snapshot.elapsed,
&mut snapshot.last_tick,
now,
blocked,
animation_speed,
);
if !blocked {
snapshot.commit.increment();
}
if !active {
tracing::debug!(?id, bytes = snapshot.bytes(), "released resize handoff snapshot");
}
active
});
active_animation |= !self.resize_snapshots.is_empty();
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty();
for (workspace, viewport) in &mut self.viewport_animations {
if !visible_workspaces.contains(workspace) {
continue;
}
if blocked_workspaces.contains(workspace) {
continue;
}
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
viewport.current = swipe.position();
viewport.velocity = 0.0;
} else if self.animations_enabled {
active_animation |= viewport.advance(delta, self.viewport_spring_config);
} else {
viewport.snap();
}
}
let mut settled_coupled_widths = Vec::new();
for (window, id) in windows {
if self
.workspaces
.workspace_for_window(id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace))
{
continue;
}
let Some(geometry) = self.window_geometry.get_mut(&id) else {
continue;
};
let zooming = geometry.is_zooming();
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);
if let Some(target) = coupled_target {
geometry.visual.target.width = target;
}
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
&& let Some(viewport) = self.viewport_animations.get(workspace)
{
if zooming {
world_x.current = geometry.visual.current.x + viewport.current;
world_x.velocity = geometry.visual.velocity.x + viewport.velocity;
} else if self.animations_enabled {
active_animation |= world_x.advance(delta, self.spring_config);
} else {
world_x.snap();
}
if !zooming {
geometry.visual.current.x = world_x.current - viewport.current;
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
}
}
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
let width_active = if self.animations_enabled {
width.advance(delta, self.viewport_spring_config)
} else {
width.snap();
false
};
active_animation |= width_active;
geometry.visual.current.width = width.current;
geometry.visual.velocity.width = width.velocity;
if !width_active {
settled_coupled_widths.push(id);
}
}
if let Some(size) = geometry.client.expire_wait(now) {
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"
);
}
if let Some(size) = geometry.presentation_size_request(now)
&& !self.natural_floating_pending.contains(&id)
&& let Some(toplevel) = window.toplevel()
{
toplevel.with_pending_state(|state| {
state.size = Some((size.width, size.height).into());
});
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);
}
for id in settled_coupled_widths {
self.viewport_coupled_widths.remove(&id);
}
if !completed_slides.is_empty() {
for output in completed_slides {
self.workspace_slides.remove(&output);
}
let visible = self.visible_workspace_ids();
let visible_windows = self
.window_ids
.values()
.copied()
.filter(|id| {
self.workspaces
.workspace_for_window(*id)
.is_none_or(|workspace| visible.contains(&workspace))
})
.collect::<HashSet<_>>();
self.unmap_invisible_windows(&visible_windows);
self.prune_empty_workspaces(&visible);
self.send_shell_snapshots();
active_animation = true;
}
active_animation |= self
.overview
.advance(delta, self.spring_config, self.animations_enabled);
self.sync_window_stacking();
if active_animation || dim_changed {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
}
active_animation
}
pub(crate) fn animations_enabled(&self) -> bool {
self.animations_enabled
}
pub(crate) fn animation_duration(&self, duration: Duration) -> Duration {
scaled_animation_duration(duration, self.animations_enabled, self.animation_speed)
}
pub(crate) fn closing_visual(&self, id: WindowId) -> (f64, f32) {
let Some(animation) = self.closing_windows.get(&id) else {
return (1.0, 1.0);
};
let eased = smoothstep(animation.progress);
(1.0 - eased * 0.02, (1.0 - eased) as f32)
}
pub(crate) fn workspace_slide_offset(&self, window: WindowId) -> (f64, f64) {
let Some(workspace) = self.workspaces.workspace_for_window(window) else {
return (0.0, 0.0);
};
let Some(output_id) = self
.workspace_slides
.iter()
.find_map(|(output, slide)| slide.contains(workspace).then_some(*output))
.or_else(|| self.output_workspaces.output_for_workspace(workspace))
else {
return (0.0, 0.0);
};
let Some(slide) = self.workspace_slides.get(&output_id) else {
return (0.0, 0.0);
};
let Some(size) = self
.output_ids
.iter()
.find(|(_, id)| **id == output_id)
.and_then(|(output, _)| self.space.output_geometry(output))
.map(|geometry| geometry.size)
else {
return (0.0, 0.0);
};
slide.offset(workspace, f64::from(size.w), f64::from(size.h))
}
pub(crate) fn cancel_workspace_slides(&mut self) -> bool {
let active = !self.workspace_slides.is_empty();
self.workspace_slides.clear();
active
}
}
+434
View File
@@ -0,0 +1,434 @@
use super::*;
impl Ferese {
pub fn toggle_focused_floating(&mut self) {
let Some(window) = self.focused_window else {
return;
};
let Some(bounds) = self.output_bounds() else {
return;
};
let mut 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,
};
if let Some(handle) = self
.window_ids
.iter()
.find_map(|(handle, id)| (*id == window).then_some(handle.clone()))
{
if matches!(
self.workspaces.placement(window),
Some(WindowPlacement::Floating { .. })
) {
self.remember_floating(&handle);
} else {
floating_rect = self.place_floating(
&handle,
Some(window),
bounds,
(floating_rect.width, floating_rect.height),
None,
);
}
}
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 toggle_focused_maximized(&mut self) {
let Some(window) = self.focused_window else {
return;
};
// Super+F from true fullscreen enters decorated maximization.
let fullscreen = self
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(window));
let enabled = fullscreen || !self.maximized_windows.contains(&window);
self.set_window_maximized(window, enabled);
}
pub fn set_window_maximized(&mut self, window: WindowId, enabled: bool) {
if self.workspaces.workspace_for_window(window).is_none() {
return;
}
if enabled {
let _ = self.workspaces.set_fullscreen(window, false);
self.maximized_windows.insert(window);
} else {
self.maximized_windows.remove(&window);
}
if self.workspaces.placement(window) == Some(WindowPlacement::Tiled) {
let workspace_id = self.workspaces.workspace_for_window(window).unwrap();
if let Some(workspace) = self.workspaces.workspace_mut(workspace_id)
&& 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);
}
let _ = layout.set_column_width(window, ColumnWidth::Proportion(1.0));
} else if let Some(width) = self.maximized_column_widths.remove(&window) {
let _ = layout.set_column_width(window, width);
}
}
}
self.relayout();
}
pub(crate) fn output_has_fullscreen(&self, output: &Output) -> bool {
!self.overview.is_presenting()
&& self
.output_id(output)
.and_then(|id| self.output_workspaces.active_workspace(id))
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen.is_some())
}
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;
};
self.floating_resize_anchors.remove(&id);
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");
return;
}
// Direct manipulation follows the pointer, including while the client is
// still drawing its next buffer. Rendering and hit testing share this rect.
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
let geometry = self
.window_geometry
.entry(id)
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
&& let Some(toplevel) = window.toplevel()
{
toplevel.with_pending_state(|state| {
state.size = Some((size.width, size.height).into());
});
toplevel.send_pending_configure();
}
self.scrolling_world_x.remove(&id);
self.viewport_coupled_widths.remove(&id);
self.space.map_element(window.clone(), location, false);
self.sync_window_stacking();
// Move/resize/cancel callbacks run with Smithay's pointer mutex held.
// Cursor rendering reads current_location(), which would lock it again.
// The event-loop epilogue coalesces this redraw after the grab returns.
self.cursor_redraw_pending = true;
}
pub(super) fn output_bounds(&self) -> Option<Rect> {
let output = self.focused_output().or_else(|| self.space.outputs().next())?;
self.output_bounds_for(output)
}
pub(super) fn floating_outputs(&self) -> Vec<(String, Rect)> {
self.space
.outputs()
.filter_map(|output| self.output_bounds_for(output).map(|work| (output.name(), work)))
.collect()
}
pub(super) fn floating_metadata(window: &Window) -> (Option<String>, Option<WlSurface>) {
window
.toplevel()
.map(|top| {
let app = with_states(top.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.app_id.clone()))
});
(app, top.parent())
})
.unwrap_or_default()
}
pub(super) fn floating_obstacles(&self, exclude: Option<WindowId>, work: Rect) -> Vec<(u64, Rect)> {
self.space
.elements()
.filter_map(|window| {
let id = *self.window_ids.get(window)?;
if Some(id) == exclude {
return None;
}
let workspace = self.workspaces.workspace_for_window(id)?;
let owner = self.output_workspaces.output_for_workspace(workspace)?;
if self.output_workspaces.active_workspace(owner) != Some(workspace) {
return None;
}
if self
.workspaces
.workspace(workspace)?
.fullscreen
.is_some_and(|full| full != id)
&& !matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
{
return None;
}
let rect = self.presented_window_rect(id)?;
crate::floating::intersection(rect, work).map(|visible| (id.0, visible))
})
.collect()
}
pub(super) fn place_floating(
&mut self,
window: &Window,
id: Option<WindowId>,
work: Rect,
size: (f64, f64),
overrides: Option<(Option<f64>, Option<f64>)>,
) -> Rect {
use crate::floating::{centered, clamp_to_work, intersection, min_overlap};
let outputs = self.floating_outputs();
let output = outputs
.iter()
.find(|(_, area)| *area == work)
.map(|(name, _)| name.as_str())
.unwrap_or("unknown");
let (app, parent) = Self::floating_metadata(window);
let parent_rect = parent
.as_ref()
.and_then(|surface| {
self.window_ids.iter().find_map(|(window, id)| {
window
.toplevel()
.is_some_and(|top| top.wl_surface() == surface)
.then_some(*id)
})
})
.and_then(|parent| self.presented_window_rect(parent))
.and_then(|rect| intersection(rect, work));
let remembered = id
.and_then(|id| self.floating_window_memory.get(&id))
.or_else(|| {
if parent.is_none() {
app.as_deref().and_then(|app| self.floating_memory.get(app))
} else {
None
}
})
.filter(|entry| entry.output == output)
.and_then(|entry| entry.restore(&outputs));
let (width, height) = overrides.unwrap_or_default();
let constraints = window
.toplevel()
.map(|top| {
with_states(top.wl_surface(), |states| {
let mut cache = states.cached_state.get::<SurfaceCachedState>();
let state = cache.current();
SizeConstraints {
min_width: state.min_size.w.max(1) as f64,
min_height: state.min_size.h.max(1) as f64,
max_width: (state.max_size.w > 0).then_some(state.max_size.w as f64),
max_height: (state.max_size.h > 0).then_some(state.max_size.h as f64),
}
})
})
.unwrap_or_default();
let stored_size = if parent_rect.is_none() {
remembered.map(|r| (r.width, r.height)).unwrap_or(size)
} else {
size
};
let sized = constrained_floating_rect(
Rect::new(0., 0., width.unwrap_or(stored_size.0), height.unwrap_or(stored_size.1)),
constraints,
);
let dimensions = (sized.width, sized.height);
let rect = if let Some(parent) = parent_rect {
centered(dimensions, parent)
} else if let Some(mut rect) = remembered {
rect.width = dimensions.0;
rect.height = dimensions.1;
rect
} else {
let others = self
.floating_obstacles(id, work)
.into_iter()
.map(|(_, r)| r)
.collect::<Vec<_>>();
let anchor = id
.and_then(|id| self.floating_placement_anchors.get(&id).copied())
.or_else(|| {
self.focused_window
.filter(|focused| Some(*focused) != id)
.and_then(|focused| self.presented_window_rect(focused))
})
.and_then(|rect| intersection(rect, work))
.unwrap_or(work);
min_overlap(
dimensions,
work,
&others,
(anchor.x + anchor.width / 2., anchor.y + anchor.height / 2.),
)
.unwrap_or_else(|| self.floating_cascade.place(output, work, dimensions))
};
clamp_to_work(rect, work)
}
pub(crate) fn remember_floating(&mut self, window: &Window) {
let Some(id) = self.window_ids.get(window).copied() else {
return;
};
let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id) else {
return;
};
let (app, parent) = Self::floating_metadata(window);
if parent.is_some() {
return;
}
// A drag can finish on another output without changing workspace ownership.
let Some((output, work)) = self
.floating_outputs()
.into_iter()
.max_by(|(_, a), (_, b)| {
let area = |work| crate::floating::intersection(rect, work).map_or(0., |r| r.width * r.height);
area(*a).total_cmp(&area(*b))
})
.filter(|(_, work)| crate::floating::intersection(rect, *work).is_some())
else {
return;
};
let entry = crate::floating::Remembered::new(output, rect, work);
self.floating_window_memory.insert(id, entry.clone());
if let Some(app) = app {
self.floating_memory.save(app, entry);
}
if self.nested_backend.is_none()
&& let Some(path) = crate::floating::Memory::path()
&& let Err(error) = self.floating_save_worker.queue(path, &self.floating_memory)
{
tracing::warn!(%error,"could not queue floating window placement save");
}
}
pub(crate) fn floating_snap_lines(
&self,
window: &Window,
raw: Rect,
) -> (Vec<crate::floating::SnapLine>, Vec<crate::floating::SnapLine>) {
let id = self.window_ids.get(window).copied();
let work = self
.floating_outputs()
.into_iter()
.max_by(|(_, a), (_, b)| {
let area = |work| crate::floating::intersection(raw, work).map_or(0., |r| r.width * r.height);
area(*a).total_cmp(&area(*b))
})
.filter(|(_, work)| crate::floating::intersection(raw, *work).is_some())
.map(|(_, work)| work)
.or_else(|| id.and_then(|id| self.floating_bounds_for_window(id)));
let Some(work) = work else {
return Default::default();
};
crate::floating::snap_lines(raw, work, &self.floating_obstacles(id, work))
}
pub(crate) fn floating_snap_bypassed(&self) -> bool {
self.seat
.get_keyboard()
.is_some_and(|keyboard| keyboard.modifier_state().shift)
}
pub(super) fn floating_bounds_for_window(&self, window: WindowId) -> Option<Rect> {
let output_id = self
.workspaces
.workspace_for_window(window)
.and_then(|workspace| self.output_workspaces.output_for_workspace(workspace));
self.output_ids
.iter()
.find(|(_, id)| Some(**id) == output_id)
.and_then(|(output, _)| self.output_bounds_for(output))
.or_else(|| self.output_bounds())
}
pub(crate) 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,
))
}
pub(super) 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,
))
}
}
+232
View File
@@ -0,0 +1,232 @@
use super::*;
impl Ferese {
pub fn surface_under(&self, position: Point<f64, Logical>) -> Option<(WlSurface, Point<f64, Logical>)> {
if self.session_lock.active {
return self.lock_surface_under(position);
}
if self.input_capture.active() {
return None;
}
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 self.session_lock.active {
return None;
}
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])
}
pub(super) fn window_surface_under(
&self,
position: Point<f64, Logical>,
) -> Option<(WlSurface, Point<f64, Logical>)> {
if self.overview.is_active() {
return None;
}
let workspace = self.workspace_under_pointer(position)?;
self.space.elements().rev().find_map(|window| {
let id = self.window_ids.get(window)?;
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
return None;
}
let visual = self.presented_window_rect(*id)?;
let 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) = 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);
let (surface, surface_location) = match surface {
Some(hit) => hit,
None if self.window_fills_visual_bounds(*id, workspace) => {
(window.toplevel()?.wl_surface().clone(), Point::from((0, 0)))
}
None => return None,
};
let surface_point = source_point - surface_location.to_f64();
Some((surface, position - surface_point))
})
}
pub(super) fn window_fills_visual_bounds(&self, id: WindowId, workspace: WorkspaceId) -> bool {
self.maximized_windows.contains(&id)
|| self
.workspaces
.workspace(workspace)
.is_some_and(|workspace| workspace.fullscreen == Some(id))
}
pub(super) 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 {
if *requested == Layer::Top && self.output_has_fullscreen(output) {
continue;
}
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;
};
let Some(pointer) = self.seat.get_pointer() else {
return;
};
if self.space.output_under(pointer.current_location()).next() != Some(output) {
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> {
let overview_active = self.overview.is_active();
let workspace = self.workspace_under_pointer(position)?;
let hit = |window: &Window| -> Option<Window> {
if !self.window_content_ready(window) {
return None;
}
let id = self.window_ids.get(window)?;
if self.workspaces.workspace_for_window(*id) != Some(workspace) {
return None;
}
let visual = self.presented_window_rect(*id)?;
let caption_height = if overview_active { 34.0 } else { 0.0 };
let inside_visual = position.x >= visual.x
&& position.y >= visual.y
&& position.x < visual.x + visual.width
&& position.y < visual.y + visual.height + caption_height;
if !inside_visual {
return None;
}
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()
&& !self.window_fills_visual_bounds(*id, workspace)
{
return None;
}
}
Some(window.clone())
};
if overview_active {
let mut candidates = self.window_ids.keys().cloned().collect::<Vec<_>>();
// Match Overview's front-to-back render order during overlapping motion.
candidates.sort_by_key(|window| std::cmp::Reverse(self.window_ids.get(window).map_or(0, |id| id.0)));
candidates.iter().find_map(hit)
} else {
self.space.elements().rev().find_map(hit)
}
}
pub(super) fn workspace_under_pointer(&self, position: Point<f64, Logical>) -> Option<WorkspaceId> {
let output = self.space.output_under(position).next()?;
self.output_workspaces.active_workspace(self.output_id(output)?)
}
pub(crate) fn window_belongs_to_output(&self, window: WindowId, output: &Output) -> bool {
let Some(output_id) = self.output_id(output) else {
return false;
};
let workspace = self.workspaces.workspace_for_window(window);
workspace == self.output_workspaces.active_workspace(output_id)
|| self
.workspace_slides
.get(&output_id)
.is_some_and(|slide| workspace.is_some_and(|workspace| slide.contains(workspace)))
}
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
if !self.overview.is_presenting() {
return Some((1.0, 1.0));
}
let id = self.window_ids.get(window)?;
let geometry = self.window_geometry.get(id)?;
let source = geometry.client.committed_size?;
let presented = self.presented_window_rect(*id)?;
if source.width <= 0 || source.height <= 0 {
return None;
}
Some((
presented.width / f64::from(source.width),
presented.height / f64::from(source.height),
))
}
pub(crate) fn visual_rect_for_window(&self, window: &Window) -> Option<Rectangle<i32, Logical>> {
let id = self.window_ids.get(window)?;
let rect = self.presented_window_rect(*id)?;
let size = ClientSize::from_rect(rect);
Some(Rectangle::new(
(rect.x.round() as i32, rect.y.round() as i32).into(),
(size.width, size.height).into(),
))
}
pub(crate) fn window_content_ready(&self, window: &Window) -> bool {
window_has_buffer(window)
&& self
.window_ids
.get(window)
.and_then(|id| self.window_geometry.get(id))
.is_some_and(|geometry| geometry.client.committed_size.is_some())
}
}
+429
View File
@@ -0,0 +1,429 @@
use super::*;
impl Ferese {
pub(super) fn visible_workspace_ids(&self) -> HashSet<WorkspaceId> {
let mut visible = self
.output_workspaces
.connected_outputs()
.filter_map(|output| self.output_workspaces.active_workspace(output))
.collect::<HashSet<_>>();
visible.extend(
self.workspace_slides
.values()
.flat_map(|slide| slide.items.iter().map(|item| item.workspace)),
);
visible
}
pub(super) fn prune_empty_workspaces(&mut self, visible: &HashSet<WorkspaceId>) {
let mut protected = visible.clone();
protected.extend(self.output_workspaces.history_workspaces());
for workspace in self.workspaces.prune_empty(&protected) {
self.output_workspaces.forget_workspace(workspace);
self.viewport_animations.remove(&workspace);
}
}
pub(super) fn unmap_invisible_windows(&mut self, visible: &HashSet<WindowId>) -> bool {
let mut changed = false;
for (window, id) in &self.window_ids {
if visible.contains(id) {
continue;
}
let was_mapped = self.space.element_location(window).is_some();
changed |= was_mapped;
if was_mapped && let Some(geometry) = self.window_geometry.get_mut(id) {
geometry.settle_presentation();
}
self.space.unmap_elem(window);
}
changed
}
pub fn relayout(&mut self) {
self.refresh_input_capture_zones();
let visible_workspaces = self.visible_workspace_ids();
self.prune_empty_workspaces(&visible_workspaces);
if self.session_lock.active {
self.configure_lock_surfaces();
}
// Consume idle time before setting new targets; it must not become a
// large first animation step after a keypress on an idle desktop.
self.advance_animations(Instant::now());
self.arrange_layers();
let mut previous_scrolling_world_x = std::mem::take(&mut self.scrolling_world_x);
let pending_column_width_cycles = std::mem::take(&mut self.pending_column_width_cycles);
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
let constraints = self.window_constraints();
let mut visible = HashSet::new();
let mut placements = Vec::new();
let mut visible_workspace_pairs = Vec::new();
for output in outputs {
let Some(output_id) = self.output_ids.get(&output).copied() else {
continue;
};
let Some(active_workspace) = self.output_workspaces.active_workspace(output_id) else {
continue;
};
visible_workspace_pairs.push((output.clone(), active_workspace));
if let Some(slide) = self.workspace_slides.get(&output_id) {
for item in &slide.items {
if item.workspace != active_workspace {
visible_workspace_pairs.push((output.clone(), item.workspace));
}
}
}
}
for (output, workspace_id) in visible_workspace_pairs {
let Some(bounds) = self.output_bounds_for(&output) else {
continue;
};
let fullscreen_bounds = self.full_output_bounds_for(&output).unwrap_or(bounds);
let scale = output.current_scale().fractional_scale();
for layer in layer_map_for_output(&output).layers() {
crate::handlers::set_surface_tree_scale(layer.wl_surface(), scale);
}
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
continue;
};
let workspace_fullscreen = workspace.fullscreen;
let is_scrolling_layout = matches!(workspace.layout, WorkspaceLayout::Scrolling(_));
let workspace_focus = workspace.last_focused;
let focused = self
.focused_window
.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 viewport_target = workspace.layout.viewport_x();
let viewport_motion = viewport_target.map(|target| {
let viewport = self
.viewport_animations
.entry(workspace_id)
.or_insert_with(|| AnimatedValue::new(target));
let target_changed = (viewport.target - target).abs() > 0.001;
viewport.retarget_preserving_motion(target);
if !self.animations_enabled {
viewport.snap();
}
(viewport.current, target_changed)
});
let viewport_current = viewport_motion.map(|(current, _)| current);
let viewport_target_changed = viewport_motion.is_some_and(|(_, changed)| changed);
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;
}
if let Some(toplevel) = window.toplevel() {
crate::handlers::set_surface_tree_scale(toplevel.wl_surface(), scale);
}
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 {
maximized_rect(bounds, self.gap_config.outer)
} else {
match self.workspaces.placement(*id) {
Some(WindowPlacement::Tiled) => {
let Some(rect) = layout.geometry.get(id).copied() else {
continue;
};
rect
}
Some(WindowPlacement::Floating { rect }) => {
let constrained =
constrained_floating_rect(rect, constraints.get(id).copied().unwrap_or_default());
if constrained != rect {
let _ = self.workspaces.set_floating_rect(*id, constrained);
}
constrained
}
None => continue,
}
};
let is_fullscreen = workspace_fullscreen == Some(*id);
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
};
placements.push((
window.clone(),
*id,
rect,
is_fullscreen,
is_maximized,
is_floating,
scrolling,
pending_column_width_cycles.contains(id) && viewport_target_changed,
));
}
}
let mut layout_changed = self.unmap_invisible_windows(&visible);
let now = self.start_time.elapsed();
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 {
let was_mapped = self.space.element_location(&window).is_some();
// A viewport-coupled width must never override fullscreen/floating geometry.
if scrolling.is_none() {
self.viewport_coupled_widths.remove(&id);
}
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),
};
let mode = if is_fullscreen {
PresentationMode::Fullscreen
} else if is_maximized {
PresentationMode::Maximized
} else {
PresentationMode::Normal
};
layout_changed |= !had_geometry || geometry.logical != rect;
let mut requested_size = geometry.set_presentation_mode(rect, mode, now);
if !was_mapped && (had_geometry || is_fullscreen) {
geometry.settle_presentation();
}
if !self.animations_enabled {
geometry.advance(Duration::ZERO, self.spring_config, false);
requested_size = geometry.presentation_size_request(now).or(requested_size);
}
if geometry.is_zooming() {
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 mut animated_world_x = previous_scrolling_world_x
.remove(&id)
.filter(|(previous_workspace, _)| *previous_workspace == workspace)
.map(|(_, world_x)| world_x)
.unwrap_or_else(|| AnimatedValue::new(restored_world_x));
animated_world_x.set_target(world_x);
if !self.animations_enabled {
animated_world_x.snap();
}
if !geometry.is_zooming() {
geometry.visual.current.x = animated_world_x.current - viewport_x;
geometry.visual.velocity.x = animated_world_x.velocity;
}
scrolling_world_x.insert(id, (workspace, animated_world_x));
let coupled = !geometry.is_zooming()
&& (couple_width
|| 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)));
if width.0 != workspace {
*width = (workspace, AnimatedValue::new(geometry.visual.current.width));
}
width.1.retarget_preserving_motion(rect.width);
if !self.animations_enabled {
width.1.snap();
}
geometry.visual.current.width = width.1.current;
geometry.visual.velocity.width = width.1.velocity;
} else if pending_column_width_cycles.contains(&id) {
self.viewport_coupled_widths.remove(&id);
}
}
let visual = geometry.visual.current;
let natural_pending =
self.natural_floating_pending.contains(&id) && is_floating && !is_fullscreen && !is_maximized;
if natural_pending {
requested_size = None;
}
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 natural_pending {
state.size = None;
} else 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 maximized_changed = if is_maximized {
state.states.set(xdg_toplevel::State::Maximized)
} else {
state.states.unset(xdg_toplevel::State::Maximized)
};
let tiled = !is_floating && !is_fullscreen;
let tiled_changed = if tiled {
state.states.set(xdg_toplevel::State::TiledLeft)
| state.states.set(xdg_toplevel::State::TiledRight)
| state.states.set(xdg_toplevel::State::TiledTop)
| state.states.set(xdg_toplevel::State::TiledBottom)
} else {
state.states.unset(xdg_toplevel::State::TiledLeft)
| state.states.unset(xdg_toplevel::State::TiledRight)
| state.states.unset(xdg_toplevel::State::TiledTop)
| state.states.unset(xdg_toplevel::State::TiledBottom)
};
let decoration_mode = if is_floating && !is_fullscreen && !is_maximized {
DecorationMode::ClientSide
} else {
DecorationMode::ServerSide
};
let decoration_changed = state.decoration_mode != Some(decoration_mode);
state.decoration_mode = Some(decoration_mode);
fullscreen_changed || maximized_changed || tiled_changed || decoration_changed
});
if (natural_pending || requested_size.is_some() || state_changed)
&& let Some(serial) = toplevel.send_pending_configure()
&& requested_size.is_some()
&& had_geometry
&& self.animations_enabled
{
self.resize_transactions.insert(
id,
crate::resize_transaction::ResizeTransaction::new(serial, now)
.with_source_geometry(window.geometry()),
);
}
}
}
self.scrolling_world_x = scrolling_world_x;
if layout_changed {
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
}
self.sync_window_stacking();
self.retarget_overview();
self.send_shell_snapshots();
crate::backends::direct::render_all(self);
}
pub(crate) fn raise_window(&mut self, window: &Window, activate: bool) {
if let Some(id) = self.window_ids.get(window) {
self.window_stack.raise(*id);
}
self.space.raise_element(window, activate);
self.sync_window_stacking();
}
pub(crate) fn sync_window_stacking(&mut self) {
let mut windows = self.space.elements().cloned().collect::<Vec<_>>();
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 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))
})
}),
);
(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;
}
for window in windows {
self.space.raise_element(&window, false);
}
}
pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
let constraints = self.window_constraints();
let focused = self.focused_window;
self.workspaces
.active_mut()
.layout
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
}
pub(super) 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()
}
pub(super) fn arrange_layers(&self) {
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
for output in outputs {
layer_map_for_output(&output).arrange();
}
}
}
+577
View File
@@ -0,0 +1,577 @@
use super::*;
impl Ferese {
pub fn focus_direction(&mut self, direction: Direction) {
self.focus_direction_with_slide(direction, false);
}
pub(super) fn focus_swipe_is_current(&self, swipe: &FocusSwipe) -> bool {
self.animations_enabled
&& !self.session_lock.active
&& !self.overview.is_presenting()
&& self.workspaces.active_id() == swipe.workspace
&& self.focused_window == Some(swipe.from)
&& self.workspaces.workspace(swipe.workspace).is_some_and(|workspace| {
workspace.fullscreen.is_none()
&& workspace.layout.contains(swipe.from)
&& workspace.layout.contains(swipe.to)
})
}
pub(crate) fn preview_focus_swipe(
&mut self,
direction: Direction,
gesture_direction: SwipeDirection,
progress: f64,
) {
if self.focus_swipe.is_none() {
if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() {
return;
}
let Some(from) = self.focused_window else { return };
let Some(bounds) = self.output_bounds() else { return };
let workspace = self.workspaces.active();
if workspace.fullscreen.is_some() || !matches!(workspace.layout, WorkspaceLayout::Scrolling(_)) {
return;
}
let Ok(Some(to)) = workspace.layout.directional_neighbor(from, direction, bounds) else {
return;
};
let workspace_id = workspace.id;
let start = self
.viewport_animations
.get(&workspace_id)
.map(|viewport| viewport.current)
.or_else(|| workspace.layout.viewport_x())
.unwrap_or(0.0);
let mut candidate = workspace.layout.clone();
if let WorkspaceLayout::Scrolling(layout) = &mut candidate
&& layout.slide_focus_from(from, to).is_err()
{
return;
}
if candidate
.geometry_with_constraints(bounds, self.gap_config, &self.window_constraints(), Some(to))
.is_err()
{
return;
}
self.focus_swipe = Some(FocusSwipe {
workspace: workspace_id,
from,
to,
direction,
gesture_direction,
start,
destination: candidate.viewport_x().unwrap_or(start),
progress,
});
self.swipe.mark_preview_started();
}
if let Some(swipe) = &mut self.focus_swipe {
swipe.progress = progress;
}
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true;
}
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
let Some(swipe) = self.focus_swipe.take() else {
return false;
};
let current = self.focus_swipe_is_current(&swipe);
// Release from the last input position even if no frame rendered that update.
if current && let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) {
viewport.current = swipe.position();
viewport.velocity = 0.0;
}
let neighbor = self.output_bounds().and_then(|bounds| {
self.workspaces
.active()
.layout
.directional_neighbor(swipe.from, swipe.direction, bounds)
.ok()
.flatten()
});
self.last_animation_tick = Instant::now();
if current && direction == Some(swipe.gesture_direction) && neighbor == Some(swipe.to) {
self.focus_direction_from_swipe(swipe.direction);
} else {
self.relayout();
}
true
}
pub(crate) fn focus_direction_from_swipe(&mut self, direction: Direction) {
self.focus_direction_with_slide(direction, true);
}
pub(super) fn focus_direction_with_slide(&mut self, direction: Direction, slide: bool) {
if self.input_capture.captures(1) {
return;
}
if self.focus_overview_direction(direction) {
return;
}
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;
}
if slide
&& let WorkspaceLayout::Scrolling(layout) = &mut self.workspaces.active_mut().layout
&& let Err(error) = layout.slide_focus_from(current, next)
{
tracing::warn!(%error, ?current, ?next, "failed to slide swipe focus");
}
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.raise_window(&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();
}
}
self.relayout();
}
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_layout_mode(&mut self) {
let Some(bounds) = self.output_bounds() else {
return;
};
let mode = match self.workspaces.active().layout.mode() {
LayoutMode::Scrolling => LayoutMode::Tree,
LayoutMode::Tree => LayoutMode::Scrolling,
};
match self.workspaces.set_active_layout_mode(mode, bounds) {
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => tracing::error!(%error, ?mode, "failed to change layout mode"),
}
}
pub fn consume_focused_window(&mut self) {
let Some(window) = self.focused_window else {
return;
};
let Some(bounds) = self.output_bounds() else {
return;
};
if self.workspaces.active().layout.mode() != LayoutMode::Scrolling {
return;
}
let target = self
.workspaces
.active()
.layout
.directional_neighbor(window, Direction::Left, bounds)
.ok()
.flatten()
.or_else(|| {
self.workspaces
.active()
.layout
.directional_neighbor(window, Direction::Right, bounds)
.ok()
.flatten()
});
let Some(target) = target else {
return;
};
if let Err(error) = self.workspaces.stack_window(window, target) {
tracing::error!(%error, ?window, ?target, "failed to consume window into column");
return;
}
self.relayout();
}
pub fn expel_focused_window(&mut self) {
let Some(window) = self.focused_window else {
return;
};
match self.workspaces.extract_window(window) {
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => tracing::error!(%error, ?window, "failed to expel window from column"),
}
}
pub fn cycle_focused_column_width(&mut self) {
let Some(window) = self.focused_window else {
return;
};
match self.workspaces.cycle_column_width(window, &self.column_width_presets) {
Ok(true) => {
self.pending_column_width_cycles.insert(window);
self.relayout();
}
Ok(false) => {}
Err(error) => tracing::error!(%error, ?window, "failed to cycle column width"),
}
}
pub fn center_focused_column(&mut self) {
let Some(window) = self.focused_window else {
return;
};
let Some(bounds) = self.output_bounds() else {
return;
};
let constraints = self.window_constraints();
match self
.workspaces
.center_window(window, bounds, self.gap_config, &constraints)
{
Ok(true) => self.relayout(),
Ok(false) => {}
Err(error) => tracing::error!(%error, ?window, "failed to center column"),
}
}
pub(crate) fn activate_managed_window(&mut self, id: WindowId) -> bool {
let Some(window) = self
.window_ids
.iter()
.find_map(|(window, window_id)| (*window_id == id).then(|| window.clone()))
else {
return false;
};
if !activate_window_workspace(&mut self.workspaces, &mut self.output_workspaces, id) {
return false;
}
let workspace = self.workspaces.active_id();
if let Some(output) = self.output_workspaces.output_for_workspace(workspace) {
self.workspace_slides.remove(&output);
}
self.focused_window = Some(id);
self.raise_window(&window, true);
self.relayout();
self.restore_keyboard_focus();
true
}
pub(crate) fn activate_managed_workspace(&mut self, workspace: WorkspaceId) -> bool {
self.activate_managed_workspace_internal(workspace, None)
}
pub(crate) fn activate_managed_workspace_from_swipe(
&mut self,
workspace: WorkspaceId,
direction: SwipeDirection,
) -> bool {
self.activate_managed_workspace_internal(workspace, Some(direction))
}
pub(super) fn activate_managed_workspace_internal(
&mut self,
workspace: WorkspaceId,
slide_direction: Option<SwipeDirection>,
) -> bool {
if self.workspaces.workspace(workspace).is_none() {
return false;
}
let output = self
.output_workspaces
.focused_output()
.or_else(|| self.output_workspaces.output_for_workspace(workspace));
let Some(output) = output else {
return false;
};
let previous = self.output_workspaces.active_workspace(output);
let owner = match self.output_workspaces.switch_workspace(output, workspace) {
Ok(ferese_core::WorkspaceSwitch::Activated(output))
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
Err(_) => return false,
};
self.update_workspace_slide(output, owner, previous, workspace, slide_direction);
self.activate_output_workspace(owner, workspace);
self.relayout();
self.restore_keyboard_focus();
true
}
pub fn switch_workspace(&mut self, index: u32) {
self.switch_workspace_internal(index, None, false);
}
pub(crate) fn switch_workspace_from_binding(&mut self, index: u32) {
self.switch_workspace_internal(index, None, self.workspace_auto_back_and_forth);
}
pub(crate) fn workspace_back_and_forth(&mut self) {
let Some(output) = self.output_workspaces.focused_output() else {
return;
};
if let Some(workspace) = self.output_workspaces.previous_workspace(output) {
self.activate_managed_workspace(workspace);
}
}
pub(super) fn switch_workspace_internal(
&mut self,
index: u32,
slide_direction: Option<SwipeDirection>,
auto_back_and_forth: bool,
) {
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 previous = self.output_workspaces.active_workspace(output);
let owner = match self
.output_workspaces
.select_workspace(output, workspace, auto_back_and_forth)
{
Ok(ferese_core::WorkspaceSwitch::Activated(output))
| Ok(ferese_core::WorkspaceSwitch::FocusedExisting(output)) => output,
Err(error) => {
tracing::error!(%error, index, "failed to assign workspace to output");
return;
}
};
let workspace = self
.output_workspaces
.active_workspace(owner)
.expect("workspace owner is connected");
self.update_workspace_slide(output, owner, previous, workspace, slide_direction);
self.activate_output_workspace(owner, workspace);
self.relayout();
self.restore_keyboard_focus();
}
pub(crate) fn preview_workspace_swipe(&mut self, next: bool, direction: SwipeDirection, progress: f64) {
if !self.animations_enabled || self.overview.is_presenting() {
return;
}
if let Some((output, (from, to, _))) = self
.workspace_slides
.iter()
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
{
if !workspace_swipe_is_current(&self.output_workspaces, output, from, to) {
self.finish_workspace_swipe(None);
return;
}
self.workspace_slides
.get_mut(&output)
.expect("gesture output exists")
.held_progress = Some(progress);
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.cursor_redraw_pending = true;
return;
}
if self.swipe.preview_started() {
return;
}
let Some(output) = self.output_workspaces.focused_output() else {
return;
};
let Some(from) = self.output_workspaces.active_workspace(output) else {
return;
};
let Some(to) = self.relative_workspace_target(next) else {
return;
};
let previous = self.workspace_slides.remove(&output);
let mut slide = WorkspaceSlide::new(previous, from, to, direction);
slide.held_progress = Some(progress);
slide.gesture = Some((from, to, direction));
self.workspace_slides.insert(output, slide);
self.swipe.mark_preview_started();
self.backdrop_generation = self.backdrop_generation.wrapping_add(1);
self.relayout();
}
pub(crate) fn finish_workspace_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
let Some((output, (from, to, expected))) = self
.workspace_slides
.iter()
.find_map(|(output, slide)| slide.gesture.map(|gesture| (*output, gesture)))
else {
return false;
};
let committed = direction == Some(expected)
&& workspace_swipe_is_current(&self.output_workspaces, output, from, to)
&& self.activate_managed_workspace_from_swipe(to, expected);
if !committed {
if let Some(slide) = self.workspace_slides.get_mut(&output) {
slide.release(false);
}
self.last_animation_tick = Instant::now();
self.relayout();
}
true
}
pub(super) fn update_workspace_slide(
&mut self,
requested_output: OutputId,
owner: OutputId,
previous: Option<WorkspaceId>,
workspace: WorkspaceId,
slide_direction: Option<SwipeDirection>,
) {
let mut previous_slide = self.workspace_slides.remove(&owner);
if owner == requested_output
&& let Some(slide) = previous_slide.as_mut()
&& slide
.gesture
.is_some_and(|(_, to, direction)| to == workspace && Some(direction) == slide_direction)
{
slide.release(true);
self.last_animation_tick = Instant::now();
self.workspace_slides.insert(owner, previous_slide.unwrap());
return;
}
if owner == requested_output
&& let (Some(from), Some(direction)) = (previous, slide_direction)
&& from != workspace
&& self.animations_enabled
{
self.last_animation_tick = Instant::now();
self.workspace_slides
.insert(owner, WorkspaceSlide::new(previous_slide, from, workspace, direction));
}
}
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) {
if self.input_capture.captures(1) {
return;
}
if self.session_lock.active {
self.focus_lock_surface();
return;
}
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(),
);
}
}
+160
View File
@@ -0,0 +1,160 @@
use super::*;
impl Ferese {
pub fn register_output(&mut self, output: &Output, identity: String) {
if self.session_lock.active {
self.session_lock.output_added(output);
self.lock_input_activity();
}
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);
let registration = self.output_workspaces.connect(output_id, geometry, fallback_workspace);
match registration {
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);
}
self.send_shell_snapshots();
}
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.workspace_slides.remove(&output_id);
self.overview_scrims.remove(&output_id);
self.pending_screencopies.retain(|capture| {
if capture.output == *output {
capture.fail();
false
} else {
true
}
});
self.space.unmap_output(output);
self.session_lock.surfaces.remove(output);
self.session_lock.backgrounds.remove(output);
self.session_lock.output_removed(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 reposition_output_floats(&mut self, output: OutputId, old: Rect, new: Rect) {
let floats = self
.window_ids
.values()
.filter_map(|id| {
let workspace = self.workspaces.workspace_for_window(*id)?;
if self.output_workspaces.output_for_workspace(workspace) != Some(output) {
return None;
}
match self.workspaces.placement(*id)? {
WindowPlacement::Floating { rect } => Some((*id, moved_floating_rect(rect, old, new))),
_ => None,
}
})
.collect::<Vec<_>>();
for (id, rect) in floats {
let _ = self.workspaces.set_floating_rect(id, rect);
}
}
pub(super) fn activate_output_workspace(&mut self, output: OutputId, workspace: ferese_core::WorkspaceId) {
if let Err(error) = self.output_workspaces.focus_output(output) {
tracing::error!(%error, "failed to focus output");
return;
}
match self.workspaces.activate(workspace) {
Ok(focus) => self.focused_window = focus,
Err(error) => tracing::error!(%error, "failed to activate output workspace"),
}
}
pub(crate) 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 restore_output_focus(&mut self) {
if let Some(output) = self.output_workspaces.focused_output()
&& let Some(workspace) = self.output_workspaces.active_workspace(output)
{
self.activate_output_workspace(output, workspace);
self.restore_keyboard_focus();
}
}
pub(crate) fn output_id(&self, output: &Output) -> Option<OutputId> {
self.output_ids.get(output).copied()
}
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;
};
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 {
return;
}
self.activate_output_workspace(output_id, workspace);
self.relayout();
}
}
+642
View File
@@ -0,0 +1,642 @@
use super::*;
impl Ferese {
pub(crate) fn restore_initial_floating_size(&mut self, window: &Window) {
let Some(toplevel) = window.toplevel() else { return };
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().unwrap().lock().unwrap();
(
attributes.app_id.clone(),
attributes.title.clone(),
attributes.parent.is_some(),
)
});
let special_mode = toplevel.with_pending_state(|pending| {
pending.states.contains(xdg_toplevel::State::Fullscreen)
|| pending.states.contains(xdg_toplevel::State::Maximized)
});
let rule = resolve_window_rules(&self.window_rules, app_id.as_deref(), title.as_deref(), transient);
if transient
|| special_mode
|| rule.fullscreen == Some(true)
|| !rule.floating.unwrap_or(rule.width.is_some() || rule.height.is_some())
{
return;
}
let outputs = self.floating_outputs();
let Some((remembered, work)) = app_id
.as_deref()
.and_then(|app| self.floating_memory.get(app))
.and_then(|entry| {
let rect = entry.restore(&outputs)?;
let work = outputs.iter().find(|(name, _)| *name == entry.output)?.1;
Some((rect, work))
})
else {
return;
};
// Metadata is available on the initial bufferless commit. Restore the
// size before the client creates its first buffer, without mapping or
// focusing the window. Use the same rule/constraint precedence as map.
let rect = self.place_floating(
window,
None,
work,
(remembered.width, remembered.height),
Some((rule.width, rule.height)),
);
let size = ClientSize::from_rect(rect);
toplevel.with_pending_state(|pending| pending.size = Some((size.width, size.height).into()));
}
pub(crate) fn add_rule_placed_window(
&mut self,
window: Window,
app_id: Option<&str>,
title: Option<&str>,
parent: Option<WindowId>,
) {
let rule = resolve_window_rules(&self.window_rules, app_id, title, parent.is_some());
let floating = rule
.floating
.unwrap_or(parent.is_some() || rule.width.is_some() || rule.height.is_some());
if !floating {
self.add_tiled_window(window);
return;
}
if let Some(parent) = parent {
self.add_transient_window(window, parent);
return;
}
if let Some(pointer) = self.seat.get_pointer() {
self.focus_output_at(pointer.current_location());
}
let remembered_work = app_id.and_then(|app| self.floating_memory.get(app)).and_then(|entry| {
let outputs = self.floating_outputs();
entry.restore(&outputs)?;
outputs
.into_iter()
.find(|(name, _)| *name == entry.output)
.map(|(_, work)| work)
});
if let Some(work) = remembered_work {
self.focus_output_at((work.x + work.width / 2.0, work.y + work.height / 2.0).into());
}
let Some(bounds) = self.output_bounds() else {
self.add_tiled_window(window);
return;
};
let id = WindowId(self.next_window_id);
self.next_window_id += 1;
let size_rect = client_size(&window)
.map(|size| natural_floating_rect(bounds, size))
.unwrap_or_else(|| centered_floating_rect(bounds));
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
let anchor = self
.focused_window
.and_then(|focused| self.presented_window_rect(focused))
.unwrap_or(bounds);
self.floating_placement_anchors.insert(id, anchor);
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)
{
tracing::error!(%error, ?id, "failed to insert rule-placed floating window");
return;
}
if rule.width.is_none() && rule.height.is_none() && client_size(&window).is_none() && remembered_work.is_none()
{
self.natural_floating_pending.insert(id);
}
self.window_ids.insert(window.clone(), id);
self.window_stack.insert(id);
if let Some(fullscreen) = fullscreen {
self.floating_above_fullscreen.insert(id, fullscreen);
}
if focus {
self.focused_window = Some(id);
}
self.space.map_element(window, (0, 0), focus);
// apply_initial_window_rules performs the first relayout/configure.
}
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);
self.window_stack.insert(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.floating_bounds_for_window(parent) else {
self.add_tiled_window(window);
return;
};
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
let size_rect = client_size(&window)
.map(|s| natural_floating_rect(bounds, s))
.unwrap_or_else(|| centered_transient_rect(visible_parent));
let rect = self.place_floating(&window, None, bounds, (size_rect.width, size_rect.height), None);
let id = WindowId(self.next_window_id);
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) {
tracing::error!(%error, ?id, ?parent, "failed to insert transient window");
return;
}
self.window_ids.insert(window.clone(), id);
self.window_stack.insert(id);
if client_size(&window).is_none() {
self.natural_floating_pending.insert(id);
}
if self
.workspaces
.workspace(workspace)
.is_some_and(|workspace| workspace.fullscreen == Some(parent))
{
self.floating_above_fullscreen.insert(id, parent);
}
if focus {
self.focused_window = Some(id);
}
self.space.map_element(window, (0, 0), focus);
self.relayout();
if focus {
self.restore_keyboard_focus();
}
}
pub(crate) fn apply_initial_window_rules(
&mut self,
window: &Window,
app_id: Option<&str>,
title: Option<&str>,
transient: bool,
) {
let Some(id) = self.window_ids.get(window).copied() else {
return;
};
if !self.window_rules_applied.insert(id) {
return;
}
let rule = resolve_window_rules(&self.window_rules, app_id, title, transient);
self.apply_window_rule_result(window, rule);
}
pub(super) fn reapply_window_rules(&mut self, old_rules: &[WindowRule]) {
let windows = self.window_ids.keys().cloned().collect::<Vec<_>>();
for window in windows {
let Some(toplevel) = window.toplevel() else {
continue;
};
let (app_id, title, transient) = with_states(toplevel.wl_surface(), |states| {
let attributes = states.data_map.get::<XdgToplevelSurfaceData>().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);
if let Some(new) = crate::window_rules::live_result(old, new, transient) {
self.apply_window_rule_result(&window, new);
}
}
}
pub(super) 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;
};
let Some(bounds) = self.floating_bounds_for_window(id) else {
return;
};
if rule == Default::default() {
return;
}
let axis = self
.workspaces
.active()
.layout
.automatic_axis(self.focused_window, bounds)
.unwrap_or(Axis::Horizontal);
if let Some(workspace) = rule.workspace
&& 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");
}
let bounds = self.floating_bounds_for_window(id).unwrap_or(bounds);
let should_float = rule
.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 mut rect = centered_floating_rect(bounds);
if should_float && rule.width.is_none() && rule.height.is_none() {
if let Some(size) = client_size(window) {
rect = natural_floating_rect(bounds, size);
} else if Self::floating_metadata(window)
.0
.as_deref()
.and_then(|app| self.floating_memory.get(app))
.and_then(|entry| entry.restore(&self.floating_outputs()))
.is_none()
{
self.natural_floating_pending.insert(id);
}
}
rect.width = rule.width.unwrap_or(rect.width).min(bounds.width);
rect.height = rule.height.unwrap_or(rect.height).min(bounds.height);
if should_float {
rect = self.place_floating(
window,
Some(id),
bounds,
(rect.width, rect.height),
Some((rule.width, rule.height)),
);
}
let result = if should_float != is_floating {
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 {
Ok(())
};
if let Err(error) = result {
tracing::warn!(%error, ?id, "failed to apply floating window rule");
}
}
if let Some(fullscreen) = rule.fullscreen
&& let Err(error) = self.workspaces.set_fullscreen(id, fullscreen)
{
tracing::warn!(%error, ?id, fullscreen, "failed to apply fullscreen window rule");
}
if self.workspaces.workspace_for_window(id) != Some(self.workspaces.active_id())
&& self.focused_window == Some(id)
{
self.focused_window = self.workspaces.active().last_focused;
}
self.relayout();
self.restore_keyboard_focus();
}
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.floating_bounds_for_window(parent) else {
return;
};
let parent_rect = self.presented_window_rect(parent).unwrap_or(bounds);
let visible_parent = crate::floating::intersection(parent_rect, bounds).unwrap_or(bounds);
let size_rect = client_size(window)
.map(|s| natural_floating_rect(bounds, s))
.unwrap_or_else(|| centered_transient_rect(visible_parent));
let rect = self.place_floating(window, Some(id), bounds, (size_rect.width, size_rect.height), None);
let 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;
}
if self
.workspaces
.workspace_for_window(parent)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(parent))
{
self.floating_above_fullscreen.insert(id, parent);
}
if self.focused_window == Some(parent) {
if let Err(error) = self.workspaces.focus_window(id) {
tracing::warn!(%error, ?id, "failed to focus transient window");
} else {
self.focused_window = Some(id);
self.window_stack.raise(id);
}
}
self.relayout();
self.restore_keyboard_focus();
}
pub fn remove_tiled_window(&mut self, window: &Window) {
// Uncommitted toplevels have not entered a workspace yet.
self.space.unmap_elem(window);
let Some(id) = self.window_ids.remove(window) else {
return;
};
self.window_geometry.remove(&id);
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
self.window_resize_fills.remove(&id);
self.maximized_windows.remove(&id);
self.maximized_column_widths.remove(&id);
self.window_stack.remove(id);
self.floating_above_fullscreen.remove(&id);
self.natural_floating_pending.remove(&id);
self.floating_window_memory.remove(&id);
self.floating_placement_anchors.remove(&id);
self.floating_resize_anchors.remove(&id);
self.window_borders.remove(&id);
self.window_dims.remove(&id);
self.window_dimming.remove(&id);
self.window_focus.remove(&id);
self.window_shadows.remove(&id);
self.closing_windows.remove(&id);
self.window_rules_applied.remove(&id);
self.scrolling_world_x.remove(&id);
self.viewport_coupled_widths.remove(&id);
self.pending_column_width_cycles.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(crate) fn capture_resize_before_commit(&mut self, surface: &WlSurface) {
let Some((window, id)) = self
.window_ids
.iter()
.find(|(window, _)| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
})
.map(|(window, id)| (window.clone(), *id))
else {
return;
};
let Some(transaction) = self.resize_transactions.get(&id) else {
return;
};
let serial = with_states(surface, |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
});
if !transaction.accepts(serial) {
return;
}
let Some(output) = self
.space
.outputs()
.find(|output| self.window_belongs_to_output(id, output))
.cloned()
else {
return;
};
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| {
crate::presentation::snapshot_covers_source(
smithay::backend::renderer::Texture::size(&snapshot.texture),
source_size,
snapshot.scale,
scale,
)
}) {
return;
}
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;
// still never risk reentrant renderer access or a compositor panic.
match backend.try_borrow_mut() {
Ok(mut backend) => crate::render::capture_resize_snapshot(
backend.renderer(),
&window,
source_geometry,
output.current_scale().fractional_scale(),
remaining,
),
Err(_) => return,
}
} else if let Some(backend) = &mut self.direct_backend {
backend.capture_resize_snapshot(&window, source_geometry, &output, remaining)
} else {
return;
};
match result {
Ok(Some(snapshot)) => {
tracing::debug!(?id, bytes = snapshot.bytes(), "captured native resize handoff");
self.resize_snapshots.insert(id, snapshot);
}
Ok(None) => {}
Err(error) => {
tracing::debug!(%error, ?id, "resize snapshot unavailable; using native crop")
}
}
}
pub fn record_client_commit(&mut self, window: &Window) {
let Some(&id) = self.window_ids.get(window) else {
return;
};
// current_serial is promoted by Smithay on commit, unlike
// configure_serial, which only records an acknowledgement.
let committed_serial = window.toplevel().and_then(|toplevel| {
with_states(toplevel.wl_surface(), |states| {
states
.data_map
.get::<XdgToplevelSurfaceData>()
.and_then(|data| data.lock().ok().and_then(|data| data.current_serial))
})
});
if self
.resize_transactions
.get(&id)
.is_some_and(|transaction| transaction.accepts(committed_serial))
{
self.resize_transactions.remove(&id);
}
let Some(size) = client_size(window) else {
return;
};
let fullscreen = self
.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.is_some_and(|workspace| workspace.fullscreen == Some(id));
let normal_geometry = !fullscreen
&& !self.maximized_windows.contains(&id)
&& self
.window_geometry
.get(&id)
.is_none_or(|geometry| !geometry.is_zooming());
let initial_floating =
self.natural_floating_pending.remove(&id) || self.floating_placement_anchors.contains_key(&id);
if initial_floating
&& normal_geometry
&& matches!(self.workspaces.placement(id), Some(WindowPlacement::Floating { .. }))
&& let Some(bounds) = self.floating_bounds_for_window(id)
{
let rect = self.place_floating(
window,
Some(id),
bounds,
(f64::from(size.width), f64::from(size.height)),
Some((Some(f64::from(size.width)), Some(f64::from(size.height)))),
);
self.floating_placement_anchors.remove(&id);
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.resize_transactions.remove(&id);
self.relayout();
}
}
// A normal floating client may choose a different size (minimum sizes,
// terminal cell grids, or a dialog changing its contents). Its committed
// window geometry is authoritative once the latest configure is committed.
// Never let an old buffer undo a newer resize or a fullscreen transition.
self.floating_placement_anchors.remove(&id);
let settled_configure = window.toplevel().is_some_and(|toplevel| {
with_states(toplevel.wl_surface(), |states| {
states.data_map.get::<XdgToplevelSurfaceData>().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),
)
})
})
});
if settled_configure
&& normal_geometry
&& let Some(WindowPlacement::Floating { rect }) = self.workspaces.placement(id)
&& ClientSize::from_rect(rect) != size
{
let mut rect = Rect::new(rect.x, rect.y, f64::from(size.width), f64::from(size.height));
if let Some((left, top, anchor)) = self.floating_resize_anchors.get(&id) {
rect = crate::floating::anchored_size(*anchor, (rect.width, rect.height), *left, *top);
} else if initial_floating && let Some(work) = self.floating_bounds_for_window(id) {
rect = crate::floating::clamp_to_work(rect, work);
}
if self.workspaces.set_floating_rect(id, rect).is_ok() {
self.window_geometry.insert(id, WindowGeometry::new(rect, Some(size)));
self.resize_transactions.remove(&id);
self.resize_snapshots.remove(&id);
self.relayout();
}
}
if settled_configure && self.floating_resize_anchors.remove(&id).is_some() {
self.remember_floating(window);
}
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 close_focused_window(&mut self) {
let Some(focused) = self.focused_window else {
return;
};
if !self.animations_enabled {
self.send_window_close(focused);
return;
}
self.closing_windows.entry(focused).or_default();
crate::backends::direct::render_all(self);
}
pub(crate) fn close_managed_window(&mut self, id: WindowId) -> bool {
if !self.window_ids.values().any(|window_id| *window_id == id) {
return false;
}
if self.animations_enabled {
self.closing_windows.entry(id).or_default();
crate::backends::direct::render_all(self);
} else {
self.send_window_close(id);
}
true
}
pub(super) 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 {
return;
};
toplevel.send_close();
}
}