fix: configure zoom clients at final size

This commit is contained in:
Samuel Onoja committed 2026-09-25 05:33:46 +01:00
1 parent 03033bf5c3
commit b3124119d2
2 files changed
+59 -42

No files matched your search

+53 -37
View File
@@ -356,20 +356,10 @@ impl WindowGeometry {
self.presentation_size_request(now) self.presentation_size_request(now)
} }
/// Pace intermediate resizes to client commits, but always deliver the final /// Configure the destination once. Intermediate sizes make clients reflow
/// size. Fast clients redraw near the presented size instead of one giant /// and replace buffers on every frame, disrupting an otherwise smooth zoom.
/// buffer being stretched throughout the zoom.
pub fn presentation_size_request(&mut self, now: Duration) -> Option<ClientSize> { 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) let size = ClientSize::from_rect(self.logical);
{
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) self.client.request_size(size, now).then_some(size)
} }
@@ -631,37 +621,63 @@ mod tests {
} }
#[test] #[test]
fn slow_client_does_not_queue_intermediate_sizes_but_gets_final_size() { fn zoom_requests_destination_once_even_with_a_responsive_client() {
let start = Rect::new(0.0, 0.0, 600.0, 800.0); 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 end = Rect::new(0.0, 0.0, 1920.0, 1080.0);
let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start))); let mut geometry = WindowGeometry::new(start, Some(ClientSize::from_rect(start)));
assert_eq!( assert_eq!(
geometry.set_presentation_target(end, true, Duration::ZERO), 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)) Some(ClientSize::from_rect(end))
); );
geometry.client.commit(ClientSize::from_rect(end));
for frame in 0..240 {
geometry.advance(Duration::from_millis(8), SpringConfig::default(), true);
assert_eq!(
geometry.presentation_size_request(Duration::from_millis(frame * 8)),
None
);
}
assert_eq!(
geometry.set_presentation_target(start, false, Duration::from_secs(2)),
Some(ClientSize::from_rect(start))
);
}
#[test]
fn left_and_right_zoom_edges_do_not_reverse_direction() {
let fullscreen = Rect::new(0.0, 0.0, 1920.0, 1080.0);
for tiled in [
Rect::new(12.0, 12.0, 942.0, 1056.0),
Rect::new(966.0, 12.0, 942.0, 1056.0),
] {
for rate in [60, 144] {
let mut geometry = WindowGeometry::new(tiled, Some(ClientSize::from_rect(tiled)));
for (target, enabled) in [(fullscreen, true), (tiled, false)] {
geometry.set_presentation_target(target, enabled, Duration::ZERO);
let mut previous = geometry.visual.current;
for _ in 0..rate * 2 {
geometry.advance(
Duration::from_secs_f64(1.0 / f64::from(rate)),
SpringConfig::default(),
true,
);
let current = geometry.visual.current;
let edges = |rect: Rect| {
[rect.x, rect.y, rect.x + rect.width, rect.y + rect.height]
};
for ((before, after), end) in edges(previous)
.into_iter()
.zip(edges(current))
.zip(edges(target))
{
assert!((after - end).abs() <= (before - end).abs() + 1e-9);
}
previous = current;
}
assert_eq!(geometry.visual.current, target);
}
}
}
} }
#[test] #[test]
+6 -5
View File
@@ -1,7 +1,7 @@
# Animation regression checks # Animation regression checks
Run `cargo test -p ferese-animation -p ferese --offline` for transition, Run `cargo test -p ferese-animation -p ferese --offline` for transition,
resize pacing, reduced motion, stacking, and refresh-rate regression tests. resize requests, reduced motion, stacking, and refresh-rate regression tests.
For visual validation, use a disposable Ferese session with three tiled windows 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 containing text and at least one floating window. Test both the nested and DRM
@@ -15,11 +15,12 @@ single screenshots cannot establish smoothness.
| Cycle width, then immediately Super+F | The old column-width spring cannot override fullscreen width. | | 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. | | 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. | | 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. | | Zoom responsive and slow-redrawing clients | Each zoom requests the destination size once. Client redraws do not generate intermediate resize requests or restart the motion. |
| Zoom the leftmost and rightmost columns in and out | Each outer edge moves monotonically toward its destination; neither side trembles or reverses near the screen edge. |
| Disable borders and shadows, then zoom | Rounded corner pixels repaint through the final frame; no corner remnants remain. | | 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. | | 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. | | Enable reduced motion | Geometry and decorations reach their final values together. |
The zoom resize policy asks responsive clients to redraw near the current visual Zoom requests the destination client size once and scales the available buffer
size. A late client buffer is still stretched as a fallback; this is not a buffer along the transition. This avoids repeated content reflow during motion. It is
snapshot or a guarantee of perfectly sharp text on every intermediate frame. not a buffer snapshot or a guarantee of perfectly sharp intermediate text.