fix: unify animated presentation and stacking

This commit is contained in:
Samuel Onoja committed 2026-09-25 04:57:39 +01:00
1 parent 83e36b0d2d
commit 6d19932a64
11 files changed
+484 -107

No files matched your search

+4 -9
View File
@@ -7,7 +7,7 @@ use smithay::{
PointerInnerHandle, RelativeMotionEvent,
},
reexports::wayland_server::protocol::wl_surface::WlSurface,
utils::{Logical, Point},
utils::{Logical, Point, Size},
};
use crate::Ferese;
@@ -16,6 +16,7 @@ pub struct MoveSurfaceGrab {
pub start_data: GrabStartData<Ferese>,
pub window: Window,
pub initial_location: Point<i32, Logical>,
pub initial_size: Size<i32, Logical>,
pub finished: bool,
}
@@ -30,10 +31,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
handle.motion(data, None, event);
let location = self.initial_location.to_f64() + (event.location - self.start_data.location);
let location = location.to_i32_round();
let size = self.window.geometry().size;
data.set_floating_window_geometry(&self.window, location, size);
data.space.map_element(self.window.clone(), location, true);
data.set_floating_window_geometry(&self.window, location, self.initial_size);
}
fn relative_motion(
@@ -153,9 +151,6 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
return;
}
let size = self.window.geometry().size;
data.set_floating_window_geometry(&self.window, self.initial_location, size);
data.space
.map_element(self.window.clone(), self.initial_location, true);
data.set_floating_window_geometry(&self.window, self.initial_location, self.initial_size);
}
}
+5 -32
View File
@@ -1,7 +1,7 @@
use std::cell::RefCell;
use smithay::{
desktop::{Space, Window},
desktop::Window,
input::pointer::{
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
@@ -275,8 +275,6 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
self.initial_rect.loc,
self.initial_rect.size,
);
data.space
.map_element(self.window.clone(), self.initial_rect.loc, true);
}
}
@@ -326,35 +324,10 @@ impl ResizeState {
}
}
pub fn handle_resize_commit(space: &mut Space<Window>, surface: &WlSurface) {
let Some(window) = space
.elements()
.find(|window| {
window
.toplevel()
.is_some_and(|toplevel| toplevel.wl_surface() == surface)
})
.cloned()
else {
return;
};
let Some(mut location) = space.element_location(&window) else {
return;
};
let geometry = window.geometry();
let Some((edges, initial_rect)) = ResizeState::with(surface, ResizeState::commit) else {
return;
};
if edges.left() {
location.x = initial_rect.loc.x + initial_rect.size.w - geometry.size.w;
}
if edges.top() {
location.y = initial_rect.loc.y + initial_rect.size.h - geometry.size.h;
}
if edges.left() || edges.top() {
space.map_element(window, location, false);
}
pub fn handle_resize_commit(surface: &WlSurface) {
// The compositor's visual rect owns the anchored edge. A delayed client
// commit must not move the window or change its stacking order.
ResizeState::with(surface, ResizeState::commit);
}
fn constrained_size(
+1 -1
View File
@@ -54,7 +54,7 @@ impl XdgActivationHandler for Ferese {
tracing::error!(%error, ?focused, "failed to update workspace focus");
return;
}
self.space.raise_element(&window, true);
self.raise_window(&window, true);
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial());
self.space.elements().for_each(|window| {
+6 -6
View File
@@ -12,7 +12,7 @@ use smithay::{
Resource,
protocol::{wl_seat, wl_surface::WlSurface},
},
utils::{Rectangle, Serial},
utils::Serial,
wayland::{
compositor::with_states,
shell::xdg::decoration::XdgDecorationHandler,
@@ -136,7 +136,7 @@ impl XdgShellHandler for Ferese {
else {
return;
};
let Some(initial_location) = self.space.element_location(&window) else {
let Some(initial_rect) = self.visual_rect_for_window(&window) else {
return;
};
if !self.is_floating_window(&window) {
@@ -147,7 +147,8 @@ impl XdgShellHandler for Ferese {
MoveSurfaceGrab {
start_data,
window,
initial_location,
initial_location: initial_rect.loc,
initial_size: initial_rect.size,
finished: false,
},
serial,
@@ -183,13 +184,12 @@ impl XdgShellHandler for Ferese {
else {
return;
};
let Some(location) = self.space.element_location(&window) else {
let Some(rect) = self.visual_rect_for_window(&window) else {
return;
};
if !self.is_floating_window(&window) {
return;
}
let rect = Rectangle::new(location, window.geometry().size);
pointer.set_grab(
self,
ResizeSurfaceGrab::new(start_data, window, ResizeEdge::from(edges), rect),
@@ -314,7 +314,7 @@ fn set_decoration_mode(toplevel: &ToplevelSurface, mode: Mode) {
}
pub fn handle_commit(popups: &mut PopupManager, space: &mut Space<Window>, surface: &WlSurface) {
handle_resize_commit(space, surface);
handle_resize_commit(surface);
if let Some(window) = space
.elements()
.find(|window| {
+1 -1
View File
@@ -308,7 +308,7 @@ impl Ferese {
{
tracing::error!(%error, ?focused, "failed to update workspace focus");
}
self.space.raise_element(&window, true);
self.raise_window(&window, true);
let surface = window
.toplevel()
.expect("mapped window has a toplevel")
+1
View File
@@ -8,6 +8,7 @@ mod handlers;
mod input;
mod ipc;
mod metrics;
mod stacking;
mod state;
mod window_rules;
mod winit;
+48
View File
@@ -0,0 +1,48 @@
use ferese_layout::WindowId;
/// Persistent back-to-front order. Geometry updates never change this order.
#[derive(Default)]
pub(crate) struct WindowStack {
order: Vec<WindowId>,
}
impl WindowStack {
pub fn insert(&mut self, id: WindowId) {
if !self.order.contains(&id) {
self.order.push(id);
}
}
pub fn remove(&mut self, id: WindowId) {
self.order.retain(|candidate| *candidate != id);
}
pub fn raise(&mut self, id: WindowId) {
self.remove(id);
self.insert(id);
}
pub fn rank(&self, id: WindowId) -> usize {
self.order
.iter()
.position(|candidate| *candidate == id)
.unwrap_or(usize::MAX)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn remapping_neighbours_does_not_change_order() {
let mut stack = WindowStack::default();
stack.insert(WindowId(1));
stack.insert(WindowId(2));
stack.raise(WindowId(1));
stack.insert(WindowId(2));
assert!(stack.rank(WindowId(1)) > stack.rank(WindowId(2)));
stack.remove(WindowId(1));
assert_eq!(stack.rank(WindowId(2)), 0);
}
}
+88 -54
View File
@@ -45,7 +45,7 @@ use smithay::{
protocol::wl_surface::WlSurface,
},
},
utils::{Logical, Point, Size},
utils::{Logical, Point, Rectangle, Size},
wayland::{
alpha_modifier::AlphaModifierState,
compositor::{CompositorClientState, CompositorState, with_states},
@@ -118,7 +118,7 @@ pub struct Ferese {
output_identity_ids: HashMap<String, OutputId>,
pub window_ids: HashMap<Window, WindowId>,
pub window_geometry: HashMap<WindowId, WindowGeometry>,
pub(crate) window_decoration_progress: HashMap<WindowId, AnimatedValue>,
window_stack: crate::stacking::WindowStack,
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
@@ -429,7 +429,7 @@ impl Ferese {
output_identity_ids: HashMap::new(),
window_ids: HashMap::new(),
window_geometry: HashMap::new(),
window_decoration_progress: HashMap::new(),
window_stack: crate::stacking::WindowStack::default(),
window_borders: HashMap::new(),
window_shadows: HashMap::new(),
rounded_clip_programs: HashMap::new(),
@@ -634,11 +634,49 @@ impl Ferese {
})
}
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 priority = id
.and_then(|id| self.window_geometry.get(&id))
.is_some_and(|geometry| geometry.is_fullscreen() || geometry.is_zooming());
(
priority,
id.map_or(usize::MAX, |id| self.window_stack.rank(id)),
)
});
for window in windows {
self.space.raise_element(&window, false);
}
}
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(crate) fn visual_rect_for_window(
&self,
window: &Window,
) -> Option<Rectangle<i32, Logical>> {
let id = self.window_ids.get(window)?;
let rect = self.window_geometry.get(id)?.visual.current;
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 fn add_tiled_window(&mut self, window: Window) {
let id = WindowId(self.next_window_id);
self.next_window_id += 1;
@@ -661,6 +699,7 @@ impl Ferese {
}
self.window_ids.insert(window.clone(), id);
self.window_stack.insert(id);
if focus_new_window {
self.focused_window = Some(id);
}
@@ -695,6 +734,7 @@ impl Ferese {
}
self.window_ids.insert(window.clone(), id);
self.window_stack.insert(id);
self.space.map_element(window, (0, 0), false);
self.relayout();
}
@@ -821,7 +861,7 @@ impl Ferese {
self.space.unmap_elem(window);
self.window_geometry.remove(&id);
self.window_decoration_progress.remove(&id);
self.window_stack.remove(id);
self.window_borders.remove(&id);
self.window_shadows.remove(&id);
self.closing_windows.remove(&id);
@@ -918,8 +958,6 @@ impl Ferese {
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) => {
@@ -973,30 +1011,9 @@ impl Ferese {
let now = self.start_time.elapsed();
let mut scrolling_world_x = HashMap::new();
// map_element also raises windows. Keep relayout's HashMap iteration
// order from randomly changing the stack during overlapping animations.
let stacking_order = self
.space
.elements()
.enumerate()
.filter_map(|(index, window)| self.window_ids.get(window).map(|id| (*id, index)))
.collect::<HashMap<_, _>>();
placements.sort_by_key(|(_, id, ..)| stacking_order.get(id).copied().unwrap_or(usize::MAX));
let mut fullscreen_transitions = Vec::new();
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
for (window, id, rect, is_fullscreen, is_floating, scrolling, couple_width) in placements {
let decoration_target = if is_fullscreen { 0.0 } else { 1.0 };
let decorations = self
.window_decoration_progress
.entry(id)
.or_insert_with(|| AnimatedValue::new(decoration_target));
if decorations.target != decoration_target {
fullscreen_transitions.push(window.clone());
}
decorations.set_target(decoration_target);
if !self.animations_enabled {
decorations.snap();
}
// A viewport-coupled width must never override fullscreen/floating geometry.
if scrolling.is_none() {
self.viewport_coupled_widths.remove(&id);
@@ -1007,9 +1024,13 @@ impl Ferese {
.window_geometry
.entry(id)
.or_insert_with(|| WindowGeometry::new(rect, committed_size));
let requested_size = geometry.set_logical_target(rect, now);
let mut requested_size = geometry.set_presentation_target(rect, is_fullscreen, now);
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(
@@ -1027,15 +1048,17 @@ impl Ferese {
if !self.animations_enabled {
animated_world_x.snap();
}
geometry.visual.current.x = animated_world_x.current - viewport_x;
geometry.visual.velocity.x = 0.0;
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 = couple_width
|| self
.viewport_coupled_widths
.get(&id)
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace);
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))
@@ -1099,11 +1122,7 @@ impl Ferese {
}
}
self.scrolling_world_x = scrolling_world_x;
// Neighbours are remapped on fullscreen exit; the shrinking window must
// remain above them. Subsequent animation ticks preserve this ordering.
for window in fullscreen_transitions {
self.space.raise_element(&window, false);
}
self.sync_window_stacking();
crate::backends::direct::render_all(self);
}
@@ -1175,16 +1194,10 @@ impl Ferese {
let mut settled_coupled_widths = Vec::new();
for (window, id) in windows {
if let Some(decorations) = self.window_decoration_progress.get_mut(&id) {
if self.animations_enabled {
active_animation |= decorations.advance(delta, self.spring_config);
} else {
decorations.snap();
}
}
let Some(geometry) = self.window_geometry.get_mut(&id) else {
continue;
};
let zooming = geometry.is_zooming();
let coupled_target = self
.viewport_coupled_widths
@@ -1202,13 +1215,18 @@ impl Ferese {
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
&& let Some(viewport) = self.viewport_animations.get(workspace)
{
if self.animations_enabled {
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();
}
geometry.visual.current.x = world_x.current - viewport.current;
geometry.visual.velocity.x = 0.0;
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 {
@@ -1233,6 +1251,14 @@ impl Ferese {
"client did not commit the final configured size within 500 ms"
);
}
if let Some(size) = geometry.presentation_size_request(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();
}
let visual = geometry.visual.current;
self.space.map_element(
window,
@@ -1243,6 +1269,7 @@ impl Ferese {
for id in settled_coupled_widths {
self.viewport_coupled_widths.remove(&id);
}
self.sync_window_stacking();
active_animation
}
@@ -1302,7 +1329,7 @@ impl Ferese {
};
self.focused_window = Some(next);
self.space.raise_element(&window, true);
self.raise_window(&window, true);
self.seat
.get_keyboard()
.expect("seat has a keyboard")
@@ -1585,11 +1612,18 @@ impl Ferese {
.window_geometry
.entry(id)
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
geometry.set_logical_target(rect, self.start_time.elapsed());
geometry.visual.snap();
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();
crate::backends::direct::render_all(self);
}
+18 -4
View File
@@ -264,6 +264,7 @@ struct RoundedSurfaceRenderElement {
program: GlesTexProgram,
clip_rect: [f32; 4],
radius: f32,
clip_changed: bool,
}
impl Element for RoundedSurfaceRenderElement {
@@ -292,11 +293,18 @@ impl Element for RoundedSurfaceRenderElement {
scale: RenderScale<f64>,
commit: Option<CommitCounter>,
) -> DamageSet<i32, Physical> {
if self.clip_changed {
return DamageSet::from_slice(&[Rectangle::from_size(self.inner.geometry(scale).size)]);
}
self.inner.damage_since(scale, commit)
}
fn opaque_regions(&self, _scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
OpaqueRegions::default()
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
if self.radius == 0.0 {
self.inner.opaque_regions(scale)
} else {
OpaqueRegions::default()
}
}
fn alpha(&self) -> f32 {
@@ -575,9 +583,9 @@ fn output_elements(
let (close_scale, close_alpha) = state.closing_visual(id);
let visual = scaled_visual_rect(visual, close_scale);
let decoration_progress = state
.window_decoration_progress
.window_geometry
.get(&id)
.map_or(1.0, |progress| progress.current.clamp(0.0, 1.0));
.map_or(1.0, |geometry| geometry.decorations.clamp(0.0, 1.0));
Some((window.clone(), id, visual, decoration_progress, close_alpha))
})
@@ -641,6 +649,10 @@ fn output_elements(
scale,
window_radius,
close_alpha,
state
.window_geometry
.get(&id)
.is_some_and(|geometry| geometry.presentation_changed),
output,
programs.texture.clone(),
));
@@ -905,6 +917,7 @@ fn rounded_window_elements(
scale: f64,
requested_radius: f64,
alpha: f32,
clip_changed: bool,
output: &Output,
program: GlesTexProgram,
) -> Vec<AnimatedWindowRenderElement> {
@@ -966,6 +979,7 @@ fn rounded_window_elements(
program: program.clone(),
clip_rect: clip,
radius,
clip_changed,
}
.into()
}),