fix: unify animated presentation and stacking
This commit is contained in:
1 parent
83e36b0d2d
commit
6d19932a64
11 files changed
+481
-104
No files matched your search
@@ -7,7 +7,7 @@ use smithay::{
|
|||||||
PointerInnerHandle, RelativeMotionEvent,
|
PointerInnerHandle, RelativeMotionEvent,
|
||||||
},
|
},
|
||||||
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
reexports::wayland_server::protocol::wl_surface::WlSurface,
|
||||||
utils::{Logical, Point},
|
utils::{Logical, Point, Size},
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::Ferese;
|
use crate::Ferese;
|
||||||
@@ -16,6 +16,7 @@ pub struct MoveSurfaceGrab {
|
|||||||
pub start_data: GrabStartData<Ferese>,
|
pub start_data: GrabStartData<Ferese>,
|
||||||
pub window: Window,
|
pub window: Window,
|
||||||
pub initial_location: Point<i32, Logical>,
|
pub initial_location: Point<i32, Logical>,
|
||||||
|
pub initial_size: Size<i32, Logical>,
|
||||||
pub finished: bool,
|
pub finished: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,10 +31,7 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
|||||||
handle.motion(data, None, event);
|
handle.motion(data, None, event);
|
||||||
let location = self.initial_location.to_f64() + (event.location - self.start_data.location);
|
let location = self.initial_location.to_f64() + (event.location - self.start_data.location);
|
||||||
let location = location.to_i32_round();
|
let location = location.to_i32_round();
|
||||||
let size = self.window.geometry().size;
|
data.set_floating_window_geometry(&self.window, location, self.initial_size);
|
||||||
|
|
||||||
data.set_floating_window_geometry(&self.window, location, size);
|
|
||||||
data.space.map_element(self.window.clone(), location, true);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn relative_motion(
|
fn relative_motion(
|
||||||
@@ -153,9 +151,6 @@ impl PointerGrab<Ferese> for MoveSurfaceGrab {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let size = self.window.geometry().size;
|
data.set_floating_window_geometry(&self.window, self.initial_location, self.initial_size);
|
||||||
data.set_floating_window_geometry(&self.window, self.initial_location, size);
|
|
||||||
data.space
|
|
||||||
.map_element(self.window.clone(), self.initial_location, true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
|
|
||||||
use smithay::{
|
use smithay::{
|
||||||
desktop::{Space, Window},
|
desktop::Window,
|
||||||
input::pointer::{
|
input::pointer::{
|
||||||
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
AxisFrame, ButtonEvent, GestureHoldBeginEvent, GestureHoldEndEvent, GesturePinchBeginEvent,
|
||||||
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
GesturePinchEndEvent, GesturePinchUpdateEvent, GestureSwipeBeginEvent,
|
||||||
@@ -275,8 +275,6 @@ impl PointerGrab<Ferese> for ResizeSurfaceGrab {
|
|||||||
self.initial_rect.loc,
|
self.initial_rect.loc,
|
||||||
self.initial_rect.size,
|
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) {
|
pub fn handle_resize_commit(surface: &WlSurface) {
|
||||||
let Some(window) = space
|
// The compositor's visual rect owns the anchored edge. A delayed client
|
||||||
.elements()
|
// commit must not move the window or change its stacking order.
|
||||||
.find(|window| {
|
ResizeState::with(surface, ResizeState::commit);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn constrained_size(
|
fn constrained_size(
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ impl XdgActivationHandler for Ferese {
|
|||||||
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
self.space.raise_element(&window, true);
|
self.raise_window(&window, true);
|
||||||
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
let keyboard = self.seat.get_keyboard().expect("seat has a keyboard");
|
||||||
keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial());
|
keyboard.set_focus(self, Some(surface), SERIAL_COUNTER.next_serial());
|
||||||
self.space.elements().for_each(|window| {
|
self.space.elements().for_each(|window| {
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ use smithay::{
|
|||||||
Resource,
|
Resource,
|
||||||
protocol::{wl_seat, wl_surface::WlSurface},
|
protocol::{wl_seat, wl_surface::WlSurface},
|
||||||
},
|
},
|
||||||
utils::{Rectangle, Serial},
|
utils::Serial,
|
||||||
wayland::{
|
wayland::{
|
||||||
compositor::with_states,
|
compositor::with_states,
|
||||||
shell::xdg::decoration::XdgDecorationHandler,
|
shell::xdg::decoration::XdgDecorationHandler,
|
||||||
@@ -136,7 +136,7 @@ impl XdgShellHandler for Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(initial_location) = self.space.element_location(&window) else {
|
let Some(initial_rect) = self.visual_rect_for_window(&window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !self.is_floating_window(&window) {
|
if !self.is_floating_window(&window) {
|
||||||
@@ -147,7 +147,8 @@ impl XdgShellHandler for Ferese {
|
|||||||
MoveSurfaceGrab {
|
MoveSurfaceGrab {
|
||||||
start_data,
|
start_data,
|
||||||
window,
|
window,
|
||||||
initial_location,
|
initial_location: initial_rect.loc,
|
||||||
|
initial_size: initial_rect.size,
|
||||||
finished: false,
|
finished: false,
|
||||||
},
|
},
|
||||||
serial,
|
serial,
|
||||||
@@ -183,13 +184,12 @@ impl XdgShellHandler for Ferese {
|
|||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(location) = self.space.element_location(&window) else {
|
let Some(rect) = self.visual_rect_for_window(&window) else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
if !self.is_floating_window(&window) {
|
if !self.is_floating_window(&window) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rect = Rectangle::new(location, window.geometry().size);
|
|
||||||
pointer.set_grab(
|
pointer.set_grab(
|
||||||
self,
|
self,
|
||||||
ResizeSurfaceGrab::new(start_data, window, ResizeEdge::from(edges), rect),
|
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) {
|
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
|
if let Some(window) = space
|
||||||
.elements()
|
.elements()
|
||||||
.find(|window| {
|
.find(|window| {
|
||||||
|
|||||||
@@ -308,7 +308,7 @@ impl Ferese {
|
|||||||
{
|
{
|
||||||
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
tracing::error!(%error, ?focused, "failed to update workspace focus");
|
||||||
}
|
}
|
||||||
self.space.raise_element(&window, true);
|
self.raise_window(&window, true);
|
||||||
let surface = window
|
let surface = window
|
||||||
.toplevel()
|
.toplevel()
|
||||||
.expect("mapped window has a toplevel")
|
.expect("mapped window has a toplevel")
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ mod handlers;
|
|||||||
mod input;
|
mod input;
|
||||||
mod ipc;
|
mod ipc;
|
||||||
mod metrics;
|
mod metrics;
|
||||||
|
mod stacking;
|
||||||
mod state;
|
mod state;
|
||||||
mod window_rules;
|
mod window_rules;
|
||||||
mod winit;
|
mod winit;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+86
-52
@@ -45,7 +45,7 @@ use smithay::{
|
|||||||
protocol::wl_surface::WlSurface,
|
protocol::wl_surface::WlSurface,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
utils::{Logical, Point, Size},
|
utils::{Logical, Point, Rectangle, Size},
|
||||||
wayland::{
|
wayland::{
|
||||||
alpha_modifier::AlphaModifierState,
|
alpha_modifier::AlphaModifierState,
|
||||||
compositor::{CompositorClientState, CompositorState, with_states},
|
compositor::{CompositorClientState, CompositorState, with_states},
|
||||||
@@ -118,7 +118,7 @@ pub struct Ferese {
|
|||||||
output_identity_ids: HashMap<String, OutputId>,
|
output_identity_ids: HashMap<String, OutputId>,
|
||||||
pub window_ids: HashMap<Window, WindowId>,
|
pub window_ids: HashMap<Window, WindowId>,
|
||||||
pub window_geometry: HashMap<WindowId, WindowGeometry>,
|
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_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>,
|
||||||
@@ -429,7 +429,7 @@ impl Ferese {
|
|||||||
output_identity_ids: HashMap::new(),
|
output_identity_ids: HashMap::new(),
|
||||||
window_ids: HashMap::new(),
|
window_ids: HashMap::new(),
|
||||||
window_geometry: HashMap::new(),
|
window_geometry: HashMap::new(),
|
||||||
window_decoration_progress: HashMap::new(),
|
window_stack: crate::stacking::WindowStack::default(),
|
||||||
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(),
|
||||||
@@ -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)> {
|
pub fn visual_scale_for_window(&self, window: &Window) -> Option<(f64, f64)> {
|
||||||
let id = self.window_ids.get(window)?;
|
let id = self.window_ids.get(window)?;
|
||||||
self.window_geometry.get(id)?.visual_scale()
|
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) {
|
pub fn add_tiled_window(&mut self, window: Window) {
|
||||||
let id = WindowId(self.next_window_id);
|
let id = WindowId(self.next_window_id);
|
||||||
self.next_window_id += 1;
|
self.next_window_id += 1;
|
||||||
@@ -661,6 +699,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.window_stack.insert(id);
|
||||||
if focus_new_window {
|
if focus_new_window {
|
||||||
self.focused_window = Some(id);
|
self.focused_window = Some(id);
|
||||||
}
|
}
|
||||||
@@ -695,6 +734,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
|
|
||||||
self.window_ids.insert(window.clone(), id);
|
self.window_ids.insert(window.clone(), id);
|
||||||
|
self.window_stack.insert(id);
|
||||||
self.space.map_element(window, (0, 0), false);
|
self.space.map_element(window, (0, 0), false);
|
||||||
self.relayout();
|
self.relayout();
|
||||||
}
|
}
|
||||||
@@ -821,7 +861,7 @@ impl Ferese {
|
|||||||
|
|
||||||
self.space.unmap_elem(window);
|
self.space.unmap_elem(window);
|
||||||
self.window_geometry.remove(&id);
|
self.window_geometry.remove(&id);
|
||||||
self.window_decoration_progress.remove(&id);
|
self.window_stack.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.closing_windows.remove(&id);
|
||||||
@@ -918,8 +958,6 @@ impl Ferese {
|
|||||||
|
|
||||||
let rect = if workspace_fullscreen == Some(*id) {
|
let rect = if workspace_fullscreen == Some(*id) {
|
||||||
fullscreen_bounds
|
fullscreen_bounds
|
||||||
} else if workspace_fullscreen.is_some() {
|
|
||||||
continue;
|
|
||||||
} else {
|
} else {
|
||||||
match self.workspaces.placement(*id) {
|
match self.workspaces.placement(*id) {
|
||||||
Some(WindowPlacement::Tiled) => {
|
Some(WindowPlacement::Tiled) => {
|
||||||
@@ -973,30 +1011,9 @@ impl Ferese {
|
|||||||
let now = self.start_time.elapsed();
|
let now = self.start_time.elapsed();
|
||||||
|
|
||||||
let mut scrolling_world_x = HashMap::new();
|
let mut scrolling_world_x = HashMap::new();
|
||||||
// map_element also raises windows. Keep relayout's HashMap iteration
|
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
|
||||||
// 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 {
|
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.
|
// A viewport-coupled width must never override fullscreen/floating geometry.
|
||||||
if scrolling.is_none() {
|
if scrolling.is_none() {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.viewport_coupled_widths.remove(&id);
|
||||||
@@ -1007,9 +1024,13 @@ impl Ferese {
|
|||||||
.window_geometry
|
.window_geometry
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| WindowGeometry::new(rect, committed_size));
|
.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 {
|
if !self.animations_enabled {
|
||||||
geometry.advance(Duration::ZERO, self.spring_config, false);
|
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 {
|
if let Some((workspace, world_x, viewport_x)) = scrolling {
|
||||||
let restored_world_x = restored_scrolling_world_x(
|
let restored_world_x = restored_scrolling_world_x(
|
||||||
@@ -1027,15 +1048,17 @@ impl Ferese {
|
|||||||
if !self.animations_enabled {
|
if !self.animations_enabled {
|
||||||
animated_world_x.snap();
|
animated_world_x.snap();
|
||||||
}
|
}
|
||||||
|
if !geometry.is_zooming() {
|
||||||
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
geometry.visual.current.x = animated_world_x.current - viewport_x;
|
||||||
geometry.visual.velocity.x = 0.0;
|
geometry.visual.velocity.x = animated_world_x.velocity;
|
||||||
|
}
|
||||||
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
scrolling_world_x.insert(id, (workspace, animated_world_x));
|
||||||
|
|
||||||
let coupled = couple_width
|
let coupled = !geometry.is_zooming()
|
||||||
|| self
|
&& (couple_width
|
||||||
.viewport_coupled_widths
|
|| self.viewport_coupled_widths.get(&id).is_some_and(
|
||||||
.get(&id)
|
|(previous_workspace, _)| *previous_workspace == workspace,
|
||||||
.is_some_and(|(previous_workspace, _)| *previous_workspace == workspace);
|
));
|
||||||
if coupled {
|
if coupled {
|
||||||
let width = self.viewport_coupled_widths.entry(id).or_insert_with(|| {
|
let width = self.viewport_coupled_widths.entry(id).or_insert_with(|| {
|
||||||
(workspace, AnimatedValue::new(geometry.visual.current.width))
|
(workspace, AnimatedValue::new(geometry.visual.current.width))
|
||||||
@@ -1099,11 +1122,7 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
self.scrolling_world_x = scrolling_world_x;
|
self.scrolling_world_x = scrolling_world_x;
|
||||||
// Neighbours are remapped on fullscreen exit; the shrinking window must
|
self.sync_window_stacking();
|
||||||
// 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);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
@@ -1175,16 +1194,10 @@ impl Ferese {
|
|||||||
|
|
||||||
let mut settled_coupled_widths = Vec::new();
|
let mut settled_coupled_widths = Vec::new();
|
||||||
for (window, id) in windows {
|
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 {
|
let Some(geometry) = self.window_geometry.get_mut(&id) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
let zooming = geometry.is_zooming();
|
||||||
|
|
||||||
let coupled_target = self
|
let coupled_target = self
|
||||||
.viewport_coupled_widths
|
.viewport_coupled_widths
|
||||||
@@ -1202,13 +1215,18 @@ impl Ferese {
|
|||||||
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
if let Some((workspace, world_x)) = self.scrolling_world_x.get_mut(&id)
|
||||||
&& let Some(viewport) = self.viewport_animations.get(workspace)
|
&& 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);
|
active_animation |= world_x.advance(delta, self.spring_config);
|
||||||
} else {
|
} else {
|
||||||
world_x.snap();
|
world_x.snap();
|
||||||
}
|
}
|
||||||
|
if !zooming {
|
||||||
geometry.visual.current.x = world_x.current - viewport.current;
|
geometry.visual.current.x = world_x.current - viewport.current;
|
||||||
geometry.visual.velocity.x = 0.0;
|
geometry.visual.velocity.x = world_x.velocity - viewport.velocity;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
|
if let Some((_, width)) = self.viewport_coupled_widths.get_mut(&id) {
|
||||||
let width_active = if self.animations_enabled {
|
let width_active = if self.animations_enabled {
|
||||||
@@ -1233,6 +1251,14 @@ impl Ferese {
|
|||||||
"client did not commit the final configured size within 500 ms"
|
"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;
|
let visual = geometry.visual.current;
|
||||||
self.space.map_element(
|
self.space.map_element(
|
||||||
window,
|
window,
|
||||||
@@ -1243,6 +1269,7 @@ impl Ferese {
|
|||||||
for id in settled_coupled_widths {
|
for id in settled_coupled_widths {
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.viewport_coupled_widths.remove(&id);
|
||||||
}
|
}
|
||||||
|
self.sync_window_stacking();
|
||||||
|
|
||||||
active_animation
|
active_animation
|
||||||
}
|
}
|
||||||
@@ -1302,7 +1329,7 @@ impl Ferese {
|
|||||||
};
|
};
|
||||||
|
|
||||||
self.focused_window = Some(next);
|
self.focused_window = Some(next);
|
||||||
self.space.raise_element(&window, true);
|
self.raise_window(&window, true);
|
||||||
self.seat
|
self.seat
|
||||||
.get_keyboard()
|
.get_keyboard()
|
||||||
.expect("seat has a keyboard")
|
.expect("seat has a keyboard")
|
||||||
@@ -1585,11 +1612,18 @@ impl Ferese {
|
|||||||
.window_geometry
|
.window_geometry
|
||||||
.entry(id)
|
.entry(id)
|
||||||
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
.or_insert_with(|| WindowGeometry::new(rect, client_size(window)));
|
||||||
geometry.set_logical_target(rect, self.start_time.elapsed());
|
if let Some(size) = geometry.follow_pointer(rect, self.start_time.elapsed())
|
||||||
geometry.visual.snap();
|
&& 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.scrolling_world_x.remove(&id);
|
||||||
self.viewport_coupled_widths.remove(&id);
|
self.viewport_coupled_widths.remove(&id);
|
||||||
self.space.map_element(window.clone(), location, false);
|
self.space.map_element(window.clone(), location, false);
|
||||||
|
self.sync_window_stacking();
|
||||||
crate::backends::direct::render_all(self);
|
crate::backends::direct::render_all(self);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-3
@@ -264,6 +264,7 @@ struct RoundedSurfaceRenderElement {
|
|||||||
program: GlesTexProgram,
|
program: GlesTexProgram,
|
||||||
clip_rect: [f32; 4],
|
clip_rect: [f32; 4],
|
||||||
radius: f32,
|
radius: f32,
|
||||||
|
clip_changed: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Element for RoundedSurfaceRenderElement {
|
impl Element for RoundedSurfaceRenderElement {
|
||||||
@@ -292,12 +293,19 @@ impl Element for RoundedSurfaceRenderElement {
|
|||||||
scale: RenderScale<f64>,
|
scale: RenderScale<f64>,
|
||||||
commit: Option<CommitCounter>,
|
commit: Option<CommitCounter>,
|
||||||
) -> DamageSet<i32, Physical> {
|
) -> 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)
|
self.inner.damage_since(scale, commit)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn opaque_regions(&self, _scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
fn opaque_regions(&self, scale: RenderScale<f64>) -> OpaqueRegions<i32, Physical> {
|
||||||
|
if self.radius == 0.0 {
|
||||||
|
self.inner.opaque_regions(scale)
|
||||||
|
} else {
|
||||||
OpaqueRegions::default()
|
OpaqueRegions::default()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn alpha(&self) -> f32 {
|
fn alpha(&self) -> f32 {
|
||||||
self.inner.alpha()
|
self.inner.alpha()
|
||||||
@@ -575,9 +583,9 @@ fn output_elements(
|
|||||||
let (close_scale, close_alpha) = state.closing_visual(id);
|
let (close_scale, close_alpha) = state.closing_visual(id);
|
||||||
let visual = scaled_visual_rect(visual, close_scale);
|
let visual = scaled_visual_rect(visual, close_scale);
|
||||||
let decoration_progress = state
|
let decoration_progress = state
|
||||||
.window_decoration_progress
|
.window_geometry
|
||||||
.get(&id)
|
.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))
|
Some((window.clone(), id, visual, decoration_progress, close_alpha))
|
||||||
})
|
})
|
||||||
@@ -641,6 +649,10 @@ fn output_elements(
|
|||||||
scale,
|
scale,
|
||||||
window_radius,
|
window_radius,
|
||||||
close_alpha,
|
close_alpha,
|
||||||
|
state
|
||||||
|
.window_geometry
|
||||||
|
.get(&id)
|
||||||
|
.is_some_and(|geometry| geometry.presentation_changed),
|
||||||
output,
|
output,
|
||||||
programs.texture.clone(),
|
programs.texture.clone(),
|
||||||
));
|
));
|
||||||
@@ -905,6 +917,7 @@ fn rounded_window_elements(
|
|||||||
scale: f64,
|
scale: f64,
|
||||||
requested_radius: f64,
|
requested_radius: f64,
|
||||||
alpha: f32,
|
alpha: f32,
|
||||||
|
clip_changed: bool,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
program: GlesTexProgram,
|
program: GlesTexProgram,
|
||||||
) -> Vec<AnimatedWindowRenderElement> {
|
) -> Vec<AnimatedWindowRenderElement> {
|
||||||
@@ -966,6 +979,7 @@ fn rounded_window_elements(
|
|||||||
program: program.clone(),
|
program: program.clone(),
|
||||||
clip_rect: clip,
|
clip_rect: clip,
|
||||||
radius,
|
radius,
|
||||||
|
clip_changed,
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -272,6 +272,18 @@ pub struct WindowGeometry {
|
|||||||
pub logical: Rect,
|
pub logical: Rect,
|
||||||
pub visual: AnimatedRect,
|
pub visual: AnimatedRect,
|
||||||
pub client: ClientGeometry,
|
pub client: ClientGeometry,
|
||||||
|
pub decorations: f64,
|
||||||
|
pub presentation_changed: bool,
|
||||||
|
fullscreen: bool,
|
||||||
|
zoom: Option<ZoomTransition>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fullscreen geometry and decorations use one clock, including on reversal.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||||
|
struct ZoomTransition {
|
||||||
|
from: Rect,
|
||||||
|
decorations_from: f64,
|
||||||
|
progress: AnimatedValue,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WindowGeometry {
|
impl WindowGeometry {
|
||||||
@@ -286,6 +298,10 @@ impl WindowGeometry {
|
|||||||
committed_size,
|
committed_size,
|
||||||
waiting_since: None,
|
waiting_since: None,
|
||||||
},
|
},
|
||||||
|
decorations: 1.0,
|
||||||
|
presentation_changed: true,
|
||||||
|
fullscreen: false,
|
||||||
|
zoom: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,17 +314,135 @@ impl WindowGeometry {
|
|||||||
self.client.request_size(size, now).then_some(size)
|
self.client.request_size(size, now).then_some(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_presentation_target(
|
||||||
|
&mut self,
|
||||||
|
rect: Rect,
|
||||||
|
fullscreen: bool,
|
||||||
|
now: Duration,
|
||||||
|
) -> Option<ClientSize> {
|
||||||
|
let rect = normalized_rect(rect);
|
||||||
|
if self.fullscreen != fullscreen || (self.zoom.is_some() && self.logical != rect) {
|
||||||
|
let mut progress = AnimatedValue::new(0.0);
|
||||||
|
progress.set_target(1.0);
|
||||||
|
let from = self.visual.current;
|
||||||
|
let distance = [
|
||||||
|
rect.x - from.x,
|
||||||
|
rect.y - from.y,
|
||||||
|
rect.width - from.width,
|
||||||
|
rect.height - from.height,
|
||||||
|
];
|
||||||
|
let velocity = self.visual.velocity;
|
||||||
|
let speed = [velocity.x, velocity.y, velocity.width, velocity.height];
|
||||||
|
let squared_length = distance.iter().map(|value| value * value).sum::<f64>();
|
||||||
|
if squared_length > 0.001 {
|
||||||
|
let projected =
|
||||||
|
distance.iter().zip(speed).map(|(d, v)| d * v).sum::<f64>() / squared_length;
|
||||||
|
// Keep forward momentum; soften a reversal without jumping position.
|
||||||
|
progress.velocity = if projected < 0.0 {
|
||||||
|
projected * 0.35
|
||||||
|
} else {
|
||||||
|
projected
|
||||||
|
};
|
||||||
|
}
|
||||||
|
self.zoom = Some(ZoomTransition {
|
||||||
|
from: self.visual.current,
|
||||||
|
decorations_from: self.decorations,
|
||||||
|
progress,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.fullscreen = fullscreen;
|
||||||
|
self.logical = rect;
|
||||||
|
self.visual.set_target(rect);
|
||||||
|
self.presentation_size_request(now)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pace intermediate resizes to client commits, but always deliver the final
|
||||||
|
/// size. Fast clients redraw near the presented size instead of one giant
|
||||||
|
/// buffer being stretched throughout the zoom.
|
||||||
|
pub fn presentation_size_request(&mut self, now: Duration) -> Option<ClientSize> {
|
||||||
|
if self.zoom.is_some() && self.client.waiting_since.is_some() && !self.client.timed_out(now)
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let rect = if self.zoom.is_some() {
|
||||||
|
self.visual.current
|
||||||
|
} else {
|
||||||
|
self.logical
|
||||||
|
};
|
||||||
|
let size = ClientSize::from_rect(rect);
|
||||||
|
self.client.request_size(size, now).then_some(size)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_zooming(&self) -> bool {
|
||||||
|
self.zoom.is_some()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn is_fullscreen(&self) -> bool {
|
||||||
|
self.fullscreen
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn follow_pointer(&mut self, rect: Rect, now: Duration) -> Option<ClientSize> {
|
||||||
|
self.zoom = None;
|
||||||
|
let requested_size = self.set_logical_target(rect, now);
|
||||||
|
self.visual.snap();
|
||||||
|
self.decorations = if self.fullscreen { 0.0 } else { 1.0 };
|
||||||
|
self.presentation_changed = true;
|
||||||
|
requested_size
|
||||||
|
}
|
||||||
|
|
||||||
pub fn advance(
|
pub fn advance(
|
||||||
&mut self,
|
&mut self,
|
||||||
delta: Duration,
|
delta: Duration,
|
||||||
config: SpringConfig,
|
config: SpringConfig,
|
||||||
animations_enabled: bool,
|
animations_enabled: bool,
|
||||||
|
) -> bool {
|
||||||
|
let previous = (self.visual.current, self.decorations);
|
||||||
|
let active = self.advance_presentation(delta, config, animations_enabled);
|
||||||
|
self.presentation_changed = previous != (self.visual.current, self.decorations);
|
||||||
|
active
|
||||||
|
}
|
||||||
|
|
||||||
|
fn advance_presentation(
|
||||||
|
&mut self,
|
||||||
|
delta: Duration,
|
||||||
|
config: SpringConfig,
|
||||||
|
animations_enabled: bool,
|
||||||
) -> bool {
|
) -> bool {
|
||||||
if !animations_enabled {
|
if !animations_enabled {
|
||||||
self.visual.snap();
|
self.visual.snap();
|
||||||
|
self.decorations = if self.fullscreen { 0.0 } else { 1.0 };
|
||||||
|
self.zoom = None;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(zoom) = &mut self.zoom {
|
||||||
|
let active = zoom.progress.advance(delta, config);
|
||||||
|
let p = zoom.progress.current;
|
||||||
|
let target = self.visual.target;
|
||||||
|
let lerp = |a: f64, b: f64| a + (b - a) * p;
|
||||||
|
self.visual.current = Rect::new(
|
||||||
|
lerp(zoom.from.x, target.x),
|
||||||
|
lerp(zoom.from.y, target.y),
|
||||||
|
lerp(zoom.from.width, target.width),
|
||||||
|
lerp(zoom.from.height, target.height),
|
||||||
|
);
|
||||||
|
self.visual.velocity = RectVelocity {
|
||||||
|
x: (target.x - zoom.from.x) * zoom.progress.velocity,
|
||||||
|
y: (target.y - zoom.from.y) * zoom.progress.velocity,
|
||||||
|
width: (target.width - zoom.from.width) * zoom.progress.velocity,
|
||||||
|
height: (target.height - zoom.from.height) * zoom.progress.velocity,
|
||||||
|
};
|
||||||
|
self.decorations = lerp(
|
||||||
|
zoom.decorations_from,
|
||||||
|
if self.fullscreen { 0.0 } else { 1.0 },
|
||||||
|
);
|
||||||
|
if !active {
|
||||||
|
self.zoom = None;
|
||||||
|
self.visual.snap();
|
||||||
|
}
|
||||||
|
return active;
|
||||||
|
}
|
||||||
|
|
||||||
self.visual.advance(delta, config)
|
self.visual.advance(delta, config)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -407,6 +541,159 @@ fn finite_or(value: f64, fallback: f64) -> f64 {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zoom_geometry_and_decorations_share_progress() {
|
||||||
|
let start = Rect::new(200.0, 40.0, 600.0, 800.0);
|
||||||
|
let end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||||
|
geometry.set_presentation_target(end, true, Duration::ZERO);
|
||||||
|
for _ in 0..20 {
|
||||||
|
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
let progress = 1.0 - geometry.decorations;
|
||||||
|
let visual = geometry.visual.current;
|
||||||
|
assert!((visual.x - (start.x + (end.x - start.x) * progress)).abs() < 1e-9);
|
||||||
|
assert!(
|
||||||
|
(visual.width - (start.width + (end.width - start.width) * progress)).abs() < 1e-9
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rapid_zoom_reversal_preserves_presented_frame_and_reaches_latest_target() {
|
||||||
|
let tiled = Rect::new(600.0, 20.0, 600.0, 900.0);
|
||||||
|
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
let mut geometry = WindowGeometry::new(tiled, None);
|
||||||
|
for enabled in [true, false, true, false] {
|
||||||
|
let before = (geometry.visual.current, geometry.decorations);
|
||||||
|
geometry.set_presentation_target(
|
||||||
|
if enabled { fullscreen } else { tiled },
|
||||||
|
enabled,
|
||||||
|
Duration::ZERO,
|
||||||
|
);
|
||||||
|
assert_eq!((geometry.visual.current, geometry.decorations), before);
|
||||||
|
geometry.advance(Duration::from_millis(48), SpringConfig::default(), true);
|
||||||
|
}
|
||||||
|
for _ in 0..240 {
|
||||||
|
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
}
|
||||||
|
assert_eq!(geometry.visual.current, tiled);
|
||||||
|
assert_eq!(geometry.decorations, 1.0);
|
||||||
|
assert!(!geometry.is_zooming());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn slow_client_does_not_queue_intermediate_sizes_but_gets_final_size() {
|
||||||
|
let start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||||
|
let end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
|
||||||
|
assert_eq!(
|
||||||
|
geometry.set_presentation_target(end, true, Duration::ZERO),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
geometry.advance(Duration::from_millis(16), SpringConfig::default(), true);
|
||||||
|
let first = geometry
|
||||||
|
.presentation_size_request(Duration::from_millis(16))
|
||||||
|
.unwrap();
|
||||||
|
assert_ne!(first, ClientSize::from_rect(end));
|
||||||
|
geometry.advance(Duration::from_millis(16), SpringConfig::default(), true);
|
||||||
|
assert_eq!(
|
||||||
|
geometry.presentation_size_request(Duration::from_millis(32)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
geometry.client.commit(first);
|
||||||
|
assert!(
|
||||||
|
geometry
|
||||||
|
.presentation_size_request(Duration::from_millis(32))
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
for _ in 0..240 {
|
||||||
|
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
}
|
||||||
|
assert_eq!(
|
||||||
|
geometry.presentation_size_request(Duration::from_secs(2)),
|
||||||
|
Some(ClientSize::from_rect(end))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn drag_takes_ownership_from_an_unfinished_zoom() {
|
||||||
|
let start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||||
|
let mut geometry = WindowGeometry::new(start, None);
|
||||||
|
geometry.set_presentation_target(Rect::new(0.0, 0.0, 1920.0, 1080.0), true, Duration::ZERO);
|
||||||
|
geometry.advance(Duration::from_millis(32), SpringConfig::default(), true);
|
||||||
|
geometry.set_presentation_target(start, false, Duration::ZERO);
|
||||||
|
let dragged = Rect::new(40.0, 50.0, 500.0, 700.0);
|
||||||
|
geometry.follow_pointer(dragged, Duration::ZERO);
|
||||||
|
assert!(!geometry.advance(Duration::from_millis(16), SpringConfig::default(), true));
|
||||||
|
assert_eq!(geometry.visual.current, dragged);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reduced_motion_finishes_zoom_and_decorations_together() {
|
||||||
|
let mut geometry = WindowGeometry::new(Rect::new(100.0, 20.0, 600.0, 800.0), None);
|
||||||
|
let target = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
geometry.set_presentation_target(target, true, Duration::ZERO);
|
||||||
|
geometry.advance(Duration::ZERO, SpringConfig::default(), false);
|
||||||
|
assert_eq!(geometry.visual.current, target);
|
||||||
|
assert_eq!(geometry.decorations, 0.0);
|
||||||
|
assert!(!geometry.is_zooming());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn relayout_during_zoom_does_not_restart_its_clock() {
|
||||||
|
let start = Rect::new(600.0, 20.0, 600.0, 900.0);
|
||||||
|
let target = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
let mut uninterrupted = WindowGeometry::new(start, None);
|
||||||
|
uninterrupted.set_presentation_target(target, true, Duration::ZERO);
|
||||||
|
let mut relayout = uninterrupted;
|
||||||
|
for frame in 0..120 {
|
||||||
|
let now = Duration::from_millis(frame * 8);
|
||||||
|
relayout.set_presentation_target(target, true, now);
|
||||||
|
relayout.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
uninterrupted.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
assert_eq!(relayout.visual, uninterrupted.visual);
|
||||||
|
assert_eq!(relayout.decorations, uninterrupted.decorations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zoom_at_different_refresh_rates_has_comparable_progress() {
|
||||||
|
let start = Rect::new(600.0, 20.0, 600.0, 900.0);
|
||||||
|
let target = Rect::new(0.0, 0.0, 1920.0, 1080.0);
|
||||||
|
let mut progress = Vec::new();
|
||||||
|
for rate in [60, 120, 144] {
|
||||||
|
let mut geometry = WindowGeometry::new(start, None);
|
||||||
|
geometry.set_presentation_target(target, true, Duration::ZERO);
|
||||||
|
for _ in 0..rate / 6 {
|
||||||
|
geometry.advance(
|
||||||
|
Duration::from_secs_f64(1.0 / f64::from(rate)),
|
||||||
|
SpringConfig::default(),
|
||||||
|
true,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
progress.push(geometry.decorations);
|
||||||
|
}
|
||||||
|
assert!((progress[0] - progress[1]).abs() < 0.01);
|
||||||
|
assert!((progress[0] - progress[2]).abs() < 0.01);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn final_frame_invalidates_clip_even_when_zoom_has_settled() {
|
||||||
|
let start = Rect::new(0.0, 0.0, 600.0, 800.0);
|
||||||
|
let mut geometry = WindowGeometry::new(start, None);
|
||||||
|
geometry.set_presentation_target(start, true, Duration::ZERO);
|
||||||
|
for _ in 0..1000 {
|
||||||
|
if !geometry.advance(Duration::from_millis(8), SpringConfig::default(), true) {
|
||||||
|
assert!(geometry.presentation_changed);
|
||||||
|
assert_eq!(geometry.decorations, 0.0);
|
||||||
|
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
|
||||||
|
assert!(!geometry.presentation_changed);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("zoom failed to settle");
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn scalar_spring_retargets_from_current_motion() {
|
fn scalar_spring_retargets_from_current_motion() {
|
||||||
let mut value = AnimatedValue::new(0.0);
|
let mut value = AnimatedValue::new(0.0);
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Animation regression checks
|
||||||
|
|
||||||
|
Run `cargo test -p ferese-animation -p ferese --offline` for transition,
|
||||||
|
resize pacing, reduced motion, stacking, and refresh-rate regression tests.
|
||||||
|
|
||||||
|
For visual validation, use a disposable Ferese session with three tiled windows
|
||||||
|
containing text and at least one floating window. Test both the nested and DRM
|
||||||
|
backends. Record the session at its native refresh rate when comparing builds;
|
||||||
|
single screenshots cannot establish smoothness.
|
||||||
|
|
||||||
|
| Scenario | Expected result |
|
||||||
|
| --- | --- |
|
||||||
|
| Super+F on the middle column, then exit | Neighbours remain behind the expanding window. The shrinking window stays above them until the transition finishes. |
|
||||||
|
| Toggle Super+F repeatedly at 40–80 ms intervals | Every reversal starts at the presented frame. Corners, border and shadow follow the same zoom progress. The final state matches the last input. |
|
||||||
|
| Cycle width, then immediately Super+F | The old column-width spring cannot override fullscreen width. |
|
||||||
|
| Exit fullscreen and immediately resize or scroll | The current frame is retained when the target changes; scrolling resumes without a position jump. |
|
||||||
|
| Drag a floating window; resize from its top-left corner | The window follows the pointer directly. The opposite edge stays anchored. Delayed client commits do not move or raise it. |
|
||||||
|
| Zoom a slow-redrawing client | Intermediate resize requests wait for commits or timeout. The final size is always requested and the animation keeps moving. |
|
||||||
|
| Disable borders and shadows, then zoom | Rounded corner pixels repaint through the final frame; no corner remnants remain. |
|
||||||
|
| Repeat on 60 Hz and 144 Hz outputs; move focus between them | Similar elapsed-time motion with no double advancement. Check frame timing on actual DRM hardware. |
|
||||||
|
| Enable reduced motion | Geometry and decorations reach their final values together. |
|
||||||
|
|
||||||
|
The zoom resize policy asks responsive clients to redraw near the current visual
|
||||||
|
size. A late client buffer is still stretched as a fallback; this is not a buffer
|
||||||
|
snapshot or a guarantee of perfectly sharp text on every intermediate frame.
|
||||||
Reference in new issue
Block a user