fix: smooth fullscreen and direct geometry transitions

This commit is contained in:
Samuel Onoja committed 2026-09-25 04:50:19 +01:00
1 parent b4fee6f72b
commit 83e36b0d2d
2 files changed
+67 -12

No files matched your search

+55
View File
@@ -118,6 +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>,
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>,
@@ -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::<HashMap<_, _>>();
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) {
+12 -12
View File
@@ -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::<Vec<_>>();
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 {