From 83e36b0d2dd3e8db867c715b3bf498c78a2a615c Mon Sep 17 00:00:00 2001 From: Samuel Onoja Date: Fri, 25 Sep 2026 04:50:19 +0100 Subject: [PATCH] fix: smooth fullscreen and direct geometry transitions --- compositor/src/state.rs | 55 +++++++++++++++++++++++++++++++++++++++++ compositor/src/winit.rs | 24 +++++++++--------- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/compositor/src/state.rs b/compositor/src/state.rs index a6858de..5efccce 100644 --- a/compositor/src/state.rs +++ b/compositor/src/state.rs @@ -118,6 +118,7 @@ pub struct Ferese { output_identity_ids: HashMap, pub window_ids: HashMap, pub window_geometry: HashMap, + pub(crate) window_decoration_progress: HashMap, pub(crate) window_borders: HashMap, pub(crate) window_shadows: HashMap, pub(crate) rounded_clip_programs: HashMap, @@ -428,6 +429,7 @@ impl Ferese { output_identity_ids: HashMap::new(), window_ids: HashMap::new(), window_geometry: HashMap::new(), + window_decoration_progress: HashMap::new(), window_borders: HashMap::new(), window_shadows: HashMap::new(), rounded_clip_programs: HashMap::new(), @@ -819,6 +821,7 @@ impl Ferese { self.space.unmap_elem(window); self.window_geometry.remove(&id); + self.window_decoration_progress.remove(&id); self.window_borders.remove(&id); self.window_shadows.remove(&id); self.closing_windows.remove(&id); @@ -970,8 +973,34 @@ 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::>(); + placements.sort_by_key(|(_, id, ..)| stacking_order.get(id).copied().unwrap_or(usize::MAX)); + let mut fullscreen_transitions = Vec::new(); 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); + } let committed_size = client_size(&window); let had_geometry = self.window_geometry.contains_key(&id); let geometry = self @@ -1070,6 +1099,11 @@ 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); + } crate::backends::direct::render_all(self); } @@ -1141,6 +1175,13 @@ 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; }; @@ -1535,7 +1576,21 @@ impl Ferese { 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. + let geometry = self + .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(); + self.scrolling_world_x.remove(&id); + self.viewport_coupled_widths.remove(&id); + self.space.map_element(window.clone(), location, false); + crate::backends::direct::render_all(self); } pub fn switch_workspace(&mut self, index: u32) { diff --git a/compositor/src/winit.rs b/compositor/src/winit.rs index 3504f1d..187a20d 100644 --- a/compositor/src/winit.rs +++ b/compositor/src/winit.rs @@ -574,21 +574,20 @@ fn output_elements( let visual = state.window_geometry.get(&id)?.visual.current; let (close_scale, close_alpha) = state.closing_visual(id); let visual = scaled_visual_rect(visual, close_scale); - let fullscreen = state - .workspaces - .workspace_for_window(id) - .and_then(|workspace| state.workspaces.workspace(workspace)) - .is_some_and(|workspace| workspace.fullscreen == Some(id)); + let decoration_progress = state + .window_decoration_progress + .get(&id) + .map_or(1.0, |progress| progress.current.clamp(0.0, 1.0)); - Some((window.clone(), id, visual, fullscreen, close_alpha)) + Some((window.clone(), id, visual, decoration_progress, close_alpha)) }) .collect::>(); - for (window, id, visual, fullscreen, close_alpha) in windows { + for (window, id, visual, decoration_progress, close_alpha) in windows { let constrain = rounded_visual_rect(visual, output_geometry.loc); - if !fullscreen && let Some(programs) = rounded_clip_program.clone() { - let window_radius = state.theme_settings.window_radius; + if let Some(programs) = rounded_clip_program.clone() { + let window_radius = state.theme_settings.window_radius * decoration_progress; let shadow_offset_y = state.theme_settings.shadow_offset_y; let shadow_blur = state.theme_settings.shadow_blur; let shadow_opacity = state.theme_settings.shadow_opacity; @@ -604,7 +603,8 @@ fn output_elements( } else { state.theme_settings.border_color.0 }; - let border_color = color_with_alpha(border_color, close_alpha); + let border_color = + color_with_alpha(border_color, close_alpha * decoration_progress as f32); if let Some(border) = window_border_element( state, @@ -629,7 +629,7 @@ fn output_elements( window_radius, shadow_offset_y, shadow_blur, - shadow_opacity * f64::from(close_alpha), + shadow_opacity * f64::from(close_alpha) * decoration_progress, shadow_color, output, &programs, @@ -656,7 +656,7 @@ fn output_elements( renderer, &window, constrain.loc, - 1.0, + close_alpha, scale, constrain, ConstrainBehavior {