fix: isolate close animation to departing window
This commit is contained in:
1 parent
b8d6c9b02a
commit
578b536b26
2 files changed
+139
-12
No files matched your search
+88
-6
@@ -81,6 +81,31 @@ use crate::{
|
|||||||
window_rules::{WindowRule, resolve as resolve_window_rules},
|
window_rules::{WindowRule, resolve as resolve_window_rules},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const CLOSE_ANIMATION_DURATION: Duration = Duration::from_millis(140);
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
struct ClosingAnimation {
|
||||||
|
progress: f64,
|
||||||
|
close_sent: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ClosingAnimation {
|
||||||
|
fn advance(&mut self, delta: Duration) -> bool {
|
||||||
|
if self.close_sent {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.progress =
|
||||||
|
(self.progress + delta.as_secs_f64() / CLOSE_ANIMATION_DURATION.as_secs_f64()).min(1.0);
|
||||||
|
if self.progress < 1.0 {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.close_sent = true;
|
||||||
|
true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub struct Ferese {
|
pub struct Ferese {
|
||||||
pub start_time: Instant,
|
pub start_time: Instant,
|
||||||
pub socket_name: OsString,
|
pub socket_name: OsString,
|
||||||
@@ -96,6 +121,7 @@ pub struct Ferese {
|
|||||||
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
pub(crate) window_borders: HashMap<WindowId, crate::winit::WindowBorderBuffers>,
|
||||||
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
pub(crate) window_shadows: HashMap<WindowId, crate::winit::WindowShadowBuffers>,
|
||||||
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
|
pub(crate) rounded_clip_programs: HashMap<ErasedContextId, crate::winit::RoundedClipPrograms>,
|
||||||
|
closing_windows: HashMap<WindowId, ClosingAnimation>,
|
||||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||||
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
scrolling_world_x: HashMap<WindowId, (WorkspaceId, AnimatedValue)>,
|
||||||
pub focused_window: Option<WindowId>,
|
pub focused_window: Option<WindowId>,
|
||||||
@@ -401,6 +427,7 @@ impl Ferese {
|
|||||||
window_borders: HashMap::new(),
|
window_borders: HashMap::new(),
|
||||||
window_shadows: HashMap::new(),
|
window_shadows: HashMap::new(),
|
||||||
rounded_clip_programs: HashMap::new(),
|
rounded_clip_programs: HashMap::new(),
|
||||||
|
closing_windows: HashMap::new(),
|
||||||
viewport_animations: HashMap::new(),
|
viewport_animations: HashMap::new(),
|
||||||
scrolling_world_x: HashMap::new(),
|
scrolling_world_x: HashMap::new(),
|
||||||
focused_window: None,
|
focused_window: None,
|
||||||
@@ -787,6 +814,7 @@ impl Ferese {
|
|||||||
self.window_geometry.remove(&id);
|
self.window_geometry.remove(&id);
|
||||||
self.window_borders.remove(&id);
|
self.window_borders.remove(&id);
|
||||||
self.window_shadows.remove(&id);
|
self.window_shadows.remove(&id);
|
||||||
|
self.closing_windows.remove(&id);
|
||||||
self.window_rules_applied.remove(&id);
|
self.window_rules_applied.remove(&id);
|
||||||
self.scrolling_world_x.remove(&id);
|
self.scrolling_world_x.remove(&id);
|
||||||
if let Err(error) = self.workspaces.remove_window(id) {
|
if let Err(error) = self.workspaces.remove_window(id) {
|
||||||
@@ -1045,6 +1073,18 @@ impl Ferese {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let mut active_animation = false;
|
let mut active_animation = false;
|
||||||
|
|
||||||
|
let mut ready_to_close = Vec::new();
|
||||||
|
for (id, animation) in &mut self.closing_windows {
|
||||||
|
if animation.advance(delta) {
|
||||||
|
ready_to_close.push(*id);
|
||||||
|
} else if !animation.close_sent {
|
||||||
|
active_animation = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for id in ready_to_close {
|
||||||
|
self.send_window_close(id);
|
||||||
|
}
|
||||||
|
|
||||||
for (workspace, viewport) in &mut self.viewport_animations {
|
for (workspace, viewport) in &mut self.viewport_animations {
|
||||||
if !visible_workspaces.contains(workspace) {
|
if !visible_workspaces.contains(workspace) {
|
||||||
continue;
|
continue;
|
||||||
@@ -1352,15 +1392,33 @@ impl Ferese {
|
|||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn close_focused_window(&self) {
|
pub fn close_focused_window(&mut self) {
|
||||||
let Some(focused) = self.focused_window else {
|
let Some(focused) = self.focused_window else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(toplevel) = self
|
|
||||||
.window_ids
|
if !self.animations_enabled {
|
||||||
.iter()
|
self.send_window_close(focused);
|
||||||
.find_map(|(window, id)| (*id == focused).then(|| window.toplevel()).flatten())
|
return;
|
||||||
else {
|
}
|
||||||
|
|
||||||
|
self.closing_windows.entry(focused).or_default();
|
||||||
|
crate::backends::direct::render_all(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1591,6 +1649,12 @@ fn centered_floating_rect(bounds: Rect) -> Rect {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn smoothstep(progress: f64) -> f64 {
|
||||||
|
let progress = progress.clamp(0.0, 1.0);
|
||||||
|
|
||||||
|
progress * progress * (3.0 - 2.0 * progress)
|
||||||
|
}
|
||||||
|
|
||||||
fn centered_transient_rect(parent: Rect) -> Rect {
|
fn centered_transient_rect(parent: Rect) -> Rect {
|
||||||
let width = (parent.width * 0.75).clamp(1.0, 640.0);
|
let width = (parent.width * 0.75).clamp(1.0, 640.0);
|
||||||
let height = (parent.height * 0.75).clamp(1.0, 480.0);
|
let height = (parent.height * 0.75).clamp(1.0, 480.0);
|
||||||
@@ -1633,4 +1697,22 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0));
|
assert_eq!(transient, Rect::new(280.0, 210.0, 640.0, 480.0));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_easing_is_bounded_and_symmetric() {
|
||||||
|
assert_eq!(smoothstep(-1.0), 0.0);
|
||||||
|
assert_eq!(smoothstep(0.5), 0.5);
|
||||||
|
assert_eq!(smoothstep(2.0), 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_animation_delays_the_protocol_close_until_it_finishes() {
|
||||||
|
let mut animation = ClosingAnimation::default();
|
||||||
|
|
||||||
|
assert!(!animation.advance(Duration::from_millis(70)));
|
||||||
|
assert_eq!(animation.progress, 0.5);
|
||||||
|
assert!(animation.advance(Duration::from_millis(70)));
|
||||||
|
assert!(animation.close_sent);
|
||||||
|
assert!(!animation.advance(Duration::from_secs(1)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+51
-6
@@ -547,17 +547,19 @@ fn output_elements(
|
|||||||
.filter_map(|window| {
|
.filter_map(|window| {
|
||||||
let id = *state.window_ids.get(window)?;
|
let id = *state.window_ids.get(window)?;
|
||||||
let visual = state.window_geometry.get(&id)?.visual.current;
|
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
|
let fullscreen = state
|
||||||
.workspaces
|
.workspaces
|
||||||
.workspace_for_window(id)
|
.workspace_for_window(id)
|
||||||
.and_then(|workspace| state.workspaces.workspace(workspace))
|
.and_then(|workspace| state.workspaces.workspace(workspace))
|
||||||
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
.is_some_and(|workspace| workspace.fullscreen == Some(id));
|
||||||
|
|
||||||
Some((window.clone(), id, visual, fullscreen))
|
Some((window.clone(), id, visual, fullscreen, close_alpha))
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (window, id, visual, fullscreen) in windows {
|
for (window, id, visual, fullscreen, close_alpha) in windows {
|
||||||
let constrain = Rectangle::<i32, Logical>::new(
|
let constrain = Rectangle::<i32, Logical>::new(
|
||||||
(
|
(
|
||||||
(visual.x - f64::from(output_geometry.loc.x)).round() as i32,
|
(visual.x - f64::from(output_geometry.loc.x)).round() as i32,
|
||||||
@@ -588,6 +590,7 @@ fn output_elements(
|
|||||||
} else {
|
} else {
|
||||||
state.theme_settings.border_color.0
|
state.theme_settings.border_color.0
|
||||||
};
|
};
|
||||||
|
let border_color = color_with_alpha(border_color, close_alpha);
|
||||||
|
|
||||||
if let Some(border) = window_border_element(
|
if let Some(border) = window_border_element(
|
||||||
state,
|
state,
|
||||||
@@ -612,7 +615,7 @@ fn output_elements(
|
|||||||
window_radius,
|
window_radius,
|
||||||
shadow_offset_y,
|
shadow_offset_y,
|
||||||
shadow_blur,
|
shadow_blur,
|
||||||
shadow_opacity,
|
shadow_opacity * f64::from(close_alpha),
|
||||||
shadow_color,
|
shadow_color,
|
||||||
output,
|
output,
|
||||||
&programs,
|
&programs,
|
||||||
@@ -623,6 +626,7 @@ fn output_elements(
|
|||||||
constrain,
|
constrain,
|
||||||
scale,
|
scale,
|
||||||
window_radius,
|
window_radius,
|
||||||
|
close_alpha,
|
||||||
output,
|
output,
|
||||||
programs.texture.clone(),
|
programs.texture.clone(),
|
||||||
));
|
));
|
||||||
@@ -886,6 +890,7 @@ fn rounded_window_elements(
|
|||||||
constrain: Rectangle<i32, Logical>,
|
constrain: Rectangle<i32, Logical>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
requested_radius: f64,
|
||||||
|
alpha: f32,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
program: GlesTexProgram,
|
program: GlesTexProgram,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
@@ -921,7 +926,7 @@ fn rounded_window_elements(
|
|||||||
popup.wl_surface(),
|
popup.wl_surface(),
|
||||||
location + offset,
|
location + offset,
|
||||||
scale,
|
scale,
|
||||||
1.0,
|
alpha,
|
||||||
RenderElementKind::Unspecified,
|
RenderElementKind::Unspecified,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -937,7 +942,7 @@ fn rounded_window_elements(
|
|||||||
surface,
|
surface,
|
||||||
location,
|
location,
|
||||||
scale,
|
scale,
|
||||||
1.0,
|
alpha,
|
||||||
RenderElementKind::Unspecified,
|
RenderElementKind::Unspecified,
|
||||||
)
|
)
|
||||||
.into_iter()
|
.into_iter()
|
||||||
@@ -965,6 +970,24 @@ fn rounded_window_elements(
|
|||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scaled_visual_rect(rect: ferese_layout::Rect, scale: f64) -> ferese_layout::Rect {
|
||||||
|
let scale = scale.clamp(0.0, 1.0);
|
||||||
|
let width = rect.width * scale;
|
||||||
|
let height = rect.height * scale;
|
||||||
|
|
||||||
|
ferese_layout::Rect::new(
|
||||||
|
rect.x + (rect.width - width) / 2.0,
|
||||||
|
rect.y + (rect.height - height) / 2.0,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn color_with_alpha(mut color: [f32; 4], alpha: f32) -> [f32; 4] {
|
||||||
|
color[3] *= alpha;
|
||||||
|
color
|
||||||
|
}
|
||||||
|
|
||||||
fn framebuffer_clip_rect(
|
fn framebuffer_clip_rect(
|
||||||
geometry: Rectangle<i32, Physical>,
|
geometry: Rectangle<i32, Physical>,
|
||||||
output_size: smithay::utils::Size<i32, Physical>,
|
output_size: smithay::utils::Size<i32, Physical>,
|
||||||
@@ -1148,7 +1171,10 @@ fn normalized_scale(scale: f64) -> f64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use smithay::utils::{Logical, Physical, Rectangle, Transform};
|
use smithay::utils::{Logical, Physical, Rectangle, Transform};
|
||||||
|
|
||||||
use super::{framebuffer_clip_rect, normalized_scale, shadow_bounds};
|
use super::{
|
||||||
|
color_with_alpha, framebuffer_clip_rect, normalized_scale, scaled_visual_rect,
|
||||||
|
shadow_bounds,
|
||||||
|
};
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn accepts_positive_finite_scale() {
|
fn accepts_positive_finite_scale() {
|
||||||
@@ -1199,4 +1225,23 @@ mod tests {
|
|||||||
Rectangle::new((80, 54).into(), (440, 340).into())
|
Rectangle::new((80, 54).into(), (440, 340).into())
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_transform_scales_about_the_window_center() {
|
||||||
|
let rect = ferese_layout::Rect::new(100.0, 50.0, 400.0, 300.0);
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
scaled_visual_rect(rect, 0.98),
|
||||||
|
ferese_layout::Rect::new(104.0, 53.0, 392.0, 294.0)
|
||||||
|
);
|
||||||
|
assert_eq!(scaled_visual_rect(rect, 1.0), rect);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn close_opacity_only_changes_the_alpha_channel() {
|
||||||
|
assert_eq!(
|
||||||
|
color_with_alpha([0.2, 0.4, 0.6, 0.8], 0.5),
|
||||||
|
[0.2, 0.4, 0.6, 0.4]
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user