Decouple scrolling translation from resize readiness

Track configure-owned presentation dependencies so independent viewport translation can continue while clients resize. Preserve linked reflow waits, native snapshots, handoff deadlines and shared rendering/input coordinates.

Closes #10.
This commit is contained in:
Samuel Onoja authored and GitHub committed 2026-10-04 08:05:44 +01:00
1 parent c1801e1f07
commit d5a829eb16
14 files changed
+1742 -72

No files matched your search

+81 -40
View File
@@ -1,7 +1,19 @@
use super::*;
impl Ferese {
pub(super) fn presentation_now(&self) -> Duration {
#[cfg(test)]
if let Some(now) = self.animation_test_time {
return now;
}
self.start_time.elapsed()
}
pub fn advance_animations(&mut self, now: Instant) -> bool {
#[cfg(test)]
if let Some(time) = self.animation_test_time {
return self.advance_animations_at(Duration::ZERO, time);
}
let delta = crate::presentation::frame_delta(&mut self.last_animation_tick, now);
// Consume wall time even while presentation is paused. A paused
// desktop must not turn suspend/DPMS time into a spring step.
@@ -35,7 +47,10 @@ impl Ferese {
continue;
};
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
let viewport = self.viewport_animations.get(&workspace).is_some_and(moving)
let viewport = self
.presentation_dependencies
.viewport_blocked(workspace, &self.windows)
&& self.viewport_animations.get(&workspace).is_some_and(moving)
&& self
.output_workspaces
.output_for_workspace(workspace)
@@ -51,6 +66,7 @@ impl Ferese {
.chain(workspace.floating.iter().copied())
.any(|other| {
other != *id
&& self.presentation_dependencies.reflow_blocked(other, &self.windows)
&& self
.windows
.window(other)
@@ -80,7 +96,7 @@ impl Ferese {
fn animations_need_tick(&self) -> bool {
if self.focus_swipe.is_some()
|| !self.paused_workspaces.is_empty()
|| self.presentation_dependencies.needs_tick()
|| !self.workspace_slides.is_empty()
|| !self.render.closing.is_empty()
|| !self.dismissing_popups.is_empty()
@@ -125,11 +141,15 @@ impl Ferese {
}
pub(super) fn advance_animations_by(&mut self, delta: std::time::Duration) -> bool {
self.advance_animations_at(delta, self.start_time.elapsed())
self.advance_animations_at(delta, self.presentation_now())
}
// The production caller supplies monotonic wall time; tests supply a fake clock.
pub(super) fn advance_animations_at(&mut self, delta: Duration, now: Duration) -> bool {
#[cfg(test)]
if self.animation_test_time.is_some() {
self.animation_test_time = Some(now);
}
if !self.animations_need_tick() {
self.sync_window_stacking();
return false;
@@ -141,7 +161,11 @@ impl Ferese {
.as_ref()
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
{
self.focus_swipe = None;
if let Some(swipe) = self.focus_swipe.take() {
self.presentation_dependencies
.restore_viewport(swipe.workspace, swipe.dependencies);
self.release_presentation_dependencies();
}
}
let mut active_animation = false;
@@ -274,13 +298,8 @@ impl Ferese {
self.render.remove_window(id);
}
// Include transactions expiring on this dispatch: their waiting time
// must not be charged to the first resumed animation step.
let paused_before_expiry = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
// Capture holds before expiry so waiting wall time never steps reflow.
let (mut held_reflow, mut held_viewport) = self.presentation_dependencies.holds(&self.windows);
#[cfg(feature = "resize-metrics")]
self.measure_resize_pauses(now);
#[cfg(feature = "resize-metrics")]
@@ -291,15 +310,18 @@ impl Ferese {
}
}
}
self.windows.expire_transactions(now);
let blocked_workspaces = self
let expired = self
.windows
.resizing()
.filter_map(|id| self.workspaces.workspace_for_window(*id))
.collect::<HashSet<_>>();
let mut resume_pauses = std::mem::replace(&mut self.paused_workspaces, blocked_workspaces.clone());
resume_pauses.extend(paused_before_expiry);
.any(|id| self.windows.transaction(id).unwrap().expired(now));
self.windows.expire_transactions(now);
if expired {
self.release_presentation_dependencies();
}
let (released_reflow, released_viewport) = self.presentation_dependencies.holds(&self.windows);
held_reflow.extend(released_reflow);
held_viewport.extend(released_viewport);
let animations_enabled = self.animations_enabled;
let animation_speed = self.animation_speed;
#[cfg(feature = "resize-metrics")]
@@ -309,10 +331,7 @@ impl Ferese {
return false;
}
let waiting_for_client = self
.workspaces
.workspace_for_window(*id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace));
let waiting_for_client = self.presentation_dependencies.reflow_blocked(*id, &self.windows);
// A shrinking client's destination buffer arrives before the
// animated bounds reach it. Keep the old native pixels covering
// that strip rather than fading them into the neutral resize fill.
@@ -349,7 +368,8 @@ impl Ferese {
});
active_animation |= self.render.snapshots().next().is_some();
// Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= !blocked_workspaces.is_empty() || !resume_pauses.is_empty();
active_animation |=
self.windows.resizing().next().is_some() || !held_reflow.is_empty() || !held_viewport.is_empty();
for (workspace, viewport) in &mut self.viewport_animations {
if self
.output_workspaces
@@ -359,7 +379,7 @@ impl Ferese {
continue;
}
if blocked_workspaces.contains(workspace) || resume_pauses.contains(workspace) {
if held_viewport.contains(workspace) {
continue;
}
@@ -380,14 +400,7 @@ impl Ferese {
continue;
};
if self
.workspaces
.workspace_for_window(id)
.is_some_and(|workspace| blocked_workspaces.contains(&workspace) || resume_pauses.contains(&workspace))
{
continue;
}
let held = held_reflow.contains(&id);
let Some(record) = self.windows.record_mut(id) else {
continue;
};
@@ -397,6 +410,8 @@ impl Ferese {
continue;
};
let previous = geometry.visual.current;
geometry.presentation_changed = false;
let zooming = geometry.is_zooming();
let coupled_target = record.coupled_width.as_ref().map(|(_, width)| width.target);
@@ -404,7 +419,9 @@ impl Ferese {
geometry.visual.target.width = geometry.visual.current.width;
geometry.visual.velocity.width = 0.0;
}
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
if !held {
active_animation |= geometry.advance(delta, self.spring_config, self.animations_enabled);
}
if zooming != geometry.is_zooming() {
self.stacking_cache.invalidate();
@@ -419,18 +436,28 @@ impl Ferese {
{
if zooming {
sync_scrolling_coordinates(geometry, world_x, viewport, true);
} else if self.animations_enabled {
} else if !held && self.animations_enabled {
active_animation |= world_x.advance(delta, self.spring_config);
} else {
} else if !held {
world_x.snap();
}
if !zooming {
sync_scrolling_coordinates(geometry, world_x, viewport, false);
let mut presented_world = *world_x;
if held {
presented_world.velocity = 0.0;
}
let mut presented_viewport = *viewport;
if held_viewport.contains(workspace) {
presented_viewport.velocity = 0.0;
}
sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false);
}
}
if let Some((_, width)) = record.coupled_width.as_mut() {
if let Some((_, width)) = record.coupled_width.as_mut()
&& !held
{
let width_active = if self.animations_enabled {
width.advance(delta, self.viewport_spring_config)
} else {
@@ -455,7 +482,9 @@ impl Ferese {
);
}
if let Some(size) = geometry.presentation_size_request(now)
geometry.presentation_changed |= previous != geometry.visual.current;
if !held
&& let Some(size) = geometry.presentation_size_request(now)
&& !natural_pending
&& let Some(toplevel) = window.toplevel()
{
@@ -864,6 +893,10 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(1.into(), Duration::from_millis(40)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 1.into());
state.advance_animations_at(Duration::from_millis(300), Duration::from_millis(340));
assert!(state.windows.transaction(&id).is_none());
assert_eq!(state.viewport_animations[&workspace], frozen);
@@ -882,6 +915,10 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(2.into(), Duration::from_millis(356)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 2.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(456));
let frozen = state.viewport_animations[&workspace];
state.windows.clear_transaction(&id);
@@ -935,10 +972,14 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(3.into(), Duration::from_millis(800)),
);
state.rebuild_presentation_dependencies();
state
.presentation_dependencies
.wait_for_viewport(workspace, id, 3.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(900));
assert!(state.paused_workspaces.contains(&workspace));
assert!(state.presentation_dependencies.needs_tick());
state.windows.clear_transaction(&id);
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(916));
assert!(state.paused_workspaces.is_empty());
assert!(!state.presentation_dependencies.needs_tick());
}
}
+402 -1
View File
@@ -59,7 +59,7 @@ fn dispatch(events: &mut EventLoop<'static, Ferese>, state: &mut Ferese) {
state.display_handle.flush_clients().unwrap();
}
fn ack_configure(wire: &mut UnixStream, xdg: u32) {
fn ack_configure(wire: &mut UnixStream, xdg: u32) -> u32 {
wire.set_nonblocking(true).unwrap();
let mut bytes = Vec::new();
let _ = wire.read_to_end(&mut bytes);
@@ -76,6 +76,7 @@ fn ack_configure(wire: &mut UnixStream, xdg: u32) {
}
assert!(serial.is_some(), "missing configure for {xdg}: {bytes:?}");
request(wire, xdg, 4, &[serial.unwrap()], None);
serial.unwrap()
}
fn window(state: &mut Ferese, events: &mut EventLoop<'static, Ferese>, color: u32) -> (Window, UnixStream) {
@@ -593,3 +594,403 @@ fn capture_privacy_pixels_and_policy_transitions() {
&mut capture_texture
)));
}
#[test]
#[ignore = "requires private Wayland sockets and an offscreen EGL device"]
fn resize_dependencies_protocol_and_pixels() {
if std::env::var_os("FERESE_RESIZE_TEST_CHILD").is_none() {
let runtime = tempfile::tempdir().unwrap();
let result = std::process::Command::new(std::env::current_exe().unwrap())
.args([
"--exact",
"state::capture_privacy::tests::resize_dependencies_protocol_and_pixels",
"--ignored",
"--nocapture",
])
.env("FERESE_RESIZE_TEST_CHILD", "1")
.env("FERESE_ENABLE_SCREENCOPY", "1")
.env("XDG_RUNTIME_DIR", runtime.path())
.env("XDG_CONFIG_HOME", runtime.path())
.env("XDG_STATE_HOME", runtime.path())
.env_remove("FERESE_SOCKET")
.output()
.unwrap();
assert!(
result.status.success(),
"{}\n{}",
String::from_utf8_lossy(&result.stdout),
String::from_utf8_lossy(&result.stderr)
);
return;
}
use crate::gestures::SwipeDirection;
use ferese_core::WorkspaceLayout;
use ferese_layout::{ColumnWidth, Direction, Rect, ViewportFocusStrategy};
let mut events = EventLoop::try_new().unwrap();
let config = crate::config::Config::parse_source("animations { enabled #false; }\nlayout { inner-gap 0; outer-gap 0; }\ntheme { geometry { window-radius 0; border-width 0; focus-ring-width 0; }; }\nwindow-rule app-id=\"resize.secret\" block-out-from-screencasts=#true\n").unwrap().runtime_config().unwrap();
let mut state = Ferese::new(&mut events, Display::new().unwrap(), config).unwrap();
state.animation_test_time = Some(Duration::ZERO);
let output = Output::new(
"resize".into(),
PhysicalProperties {
size: (0, 0).into(),
subpixel: Subpixel::Unknown,
make: "test".into(),
model: "test".into(),
},
);
output.change_current_state(
Some(Mode {
size: (320, 240).into(),
refresh: 60_000,
}),
Some(Transform::Normal),
None,
None,
);
state.space.map_output(&output, (0, 0));
state.register_output(&output, "resize".into());
let (a, mut a_wire) = window(&mut state, &mut events, 0xffff0000);
let (b, mut b_wire) = window(&mut state, &mut events, 0xff00ff00);
let (c, mut c_wire) = window(&mut state, &mut events, 0xff0000ff);
let a_id = state.windows.ids()[&a];
let b_id = state.windows.ids()[&b];
let c_id = state.windows.ids()[&c];
let workspace = state.workspaces.workspace_for_window(a_id).unwrap();
state.focused_window = Some(a_id);
state.workspaces.focus_window(a_id).unwrap();
state.relayout();
state.animations_enabled = true;
for (_, record) in state.windows.records_mut() {
record.opening = None;
}
let set_width = |state: &mut Ferese, id, width| {
let ferese_core::WorkspaceLayout::Scrolling(layout) =
&mut state.workspaces.workspace_mut(workspace).unwrap().layout
else {
panic!()
};
layout.set_column_width(id, ColumnWidth::Fixed(width)).unwrap();
state.relayout();
state.display_handle.flush_clients().unwrap();
};
set_width(&mut state, b_id, 240.0);
let b_serial = ack_configure(&mut b_wire, 3);
dispatch(&mut events, &mut state);
assert!(state.windows.transaction(&b_id).is_some(), "ack alone must not release");
// This target uses the first column, independently of the pending later width.
let bounds = Rect::new(0.0, 0.0, 320.0, 240.0);
state
.workspaces
.center_window(a_id, bounds, state.gap_config, &state.window_constraints())
.unwrap();
state.relayout();
let before = state.viewport_animations[&workspace].current;
let held_size = state.windows.geometry(&b_id).unwrap().visual.current.width;
let held_world = state.windows.record(b_id).unwrap().world_x.unwrap().1;
state.advance_animations_at(Duration::from_millis(16), Duration::ZERO);
assert_ne!(
state.viewport_animations[&workspace].current, before,
"independent viewport stalled before delayed buffer commit"
);
assert_eq!(state.windows.geometry(&b_id).unwrap().visual.current.width, held_size);
assert_eq!(state.windows.record(b_id).unwrap().world_x.unwrap().1, held_world);
// BASELINE_REPRO_END: the stage above also runs against the reviewed base.
assert!(
!state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows)
);
assert!(state.presentation_dependencies.reflow_blocked(b_id, &state.windows));
assert!(state.presentation_dependencies.reflow_blocked(c_id, &state.windows));
assert!(!state.presentation_dependencies.reflow_blocked(a_id, &state.windows));
let geometry = *state.windows.geometry(&b_id).unwrap();
let position = state.space.element_location(&b).unwrap();
assert_eq!(position.x, geometry.visual.current.x.round() as i32);
let point = (geometry.visual.current.x + 5.0, geometry.visual.current.y + 5.0).into();
let (hit, origin) = state.window_surface_under(point).unwrap();
assert_eq!(hit, *b.toplevel().unwrap().wl_surface());
assert!((origin.x - geometry.visual.current.x).abs() < 0.001);
let forecast = state.sample_frame(&output, Duration::from_millis(8));
let predicted = &forecast.windows[&b_id];
let mut viewport = state.viewport_animations[&workspace];
viewport.advance(
Duration::from_millis(8).mul_f64(state.animation_speed),
state.viewport_spring_config,
);
assert_eq!(
predicted.geometry.visual.current.x,
held_world.current - viewport.current
);
assert_eq!(predicted.geometry.visual.current.width, held_size);
assert_eq!(
predicted.presentation.bounds.current.x,
predicted.geometry.visual.current.x
);
assert_eq!(predicted.presentation.bounds.velocity.x, -viewport.velocity);
assert_eq!(
*state.windows.geometry(&b_id).unwrap(),
geometry,
"sampling mutated authoritative state"
);
let device = EGLDevice::enumerate().unwrap().last().expect("EGL device");
let display = unsafe { EGLDisplay::new(device).unwrap() };
let context = EGLContext::new(&display).unwrap();
let mut renderer = unsafe { GlesRenderer::new(context).unwrap() };
let mut display_texture =
Offscreen::<GlesTexture>::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap();
let mut capture_texture =
Offscreen::<GlesTexture>::create_buffer(&mut renderer, Fourcc::Abgr8888, (320, 240).into()).unwrap();
// Offscreen fixture explicitly captures the existing native frame; production
// capture-before-commit is exercised separately in the privacy regression.
let snapshot =
crate::render::capture_resize_snapshot(&mut renderer, &b, b.geometry(), 1.0, 16 * 1024 * 1024).unwrap();
state.render.set_snapshot(b_id, snapshot.expect("native snapshot"));
app_id(&mut state, &b, "resize.secret");
state.refresh_capture_privacy();
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(16));
let actual = state.windows.geometry(&b_id).unwrap().visual.current;
let displayed = pixels(&mut state, &mut renderer, &output, false, &mut display_texture);
let at = ((actual.y.round() as usize + 5) * 320 + actual.x.round() as usize + 5) * 4;
assert_eq!(
&displayed[at..at + 4],
&[0, 255, 0, 255],
"live/snapshot pixels did not translate with geometry"
);
let captured = pixels(&mut state, &mut renderer, &output, true, &mut capture_texture);
assert!(
!captured.chunks_exact(4).any(|pixel| pixel == [0, 255, 0, 255]),
"translated protected snapshot leaked into capture"
);
assert!(state.render.snapshot(&b_id).is_some());
// A second slow client has an independent serial owner.
set_width(&mut state, c_id, 128.0);
ack_configure(&mut c_wire, 3);
dispatch(&mut events, &mut state);
assert!(state.windows.transaction(&c_id).is_some());
state.focused_window = Some(c_id);
state.workspaces.focus_window(c_id).unwrap();
// Centering a later column now genuinely uses both pending allocated widths.
state
.workspaces
.center_window(c_id, bounds, state.gap_config, &state.window_constraints())
.unwrap();
state.relayout();
assert!(
state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows)
);
let frozen = state.viewport_animations[&workspace];
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(32));
assert_eq!(state.viewport_animations[&workspace], frozen);
// Supersede the acknowledged size, then commit the older acknowledgement.
set_width(&mut state, b_id, 96.0);
let latest = *state.windows.transaction(&b_id).unwrap();
assert!(!latest.accepts(Some(b_serial.into())));
request(&mut b_wire, 2, 1, &[6, 0, 0], None);
request(&mut b_wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(
state.windows.transaction(&b_id).unwrap().serial(),
latest.serial(),
"obsolete commit released replacement"
);
ack_configure(&mut b_wire, 3);
dispatch(&mut events, &mut state);
assert!(
state.windows.transaction(&b_id).is_some(),
"latest ack without commit released resize"
);
state.focused_window = Some(a_id);
state.workspaces.focus_window(a_id).unwrap();
// Rapid reversal to the independent first-column target retires viewport waits.
state
.workspaces
.center_window(a_id, bounds, state.gap_config, &state.window_constraints())
.unwrap();
state.relayout();
assert!(
!state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows)
);
let frozen = state.viewport_animations[&workspace].current;
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(48));
assert_ne!(state.viewport_animations[&workspace].current, frozen);
let client = b.toplevel().unwrap().wl_surface().client().unwrap();
let shm = client
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
.unwrap();
let mut resized = tempfile::tempfile().unwrap();
resized.write_all(&0xff00ff00u32.to_ne_bytes().repeat(32 * 48)).unwrap();
request(
&mut b_wire,
shm.id().protocol_id(),
0,
&[7, 32 * 48 * 4],
Some(resized.as_raw_fd()),
);
request(&mut b_wire, 7, 0, &[8, 0, 32, 48, 32 * 4, 0], None);
request(&mut b_wire, 3, 3, &[0, 0, 32, 48], None);
request(&mut b_wire, 2, 1, &[8, 0, 0], None);
request(&mut b_wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert_eq!(b.geometry().size.w, 32, "resized raster was not committed");
assert!(
state.windows.transaction(&b_id).is_none(),
"latest surface commit did not release"
);
assert!(
state.windows.transaction(&c_id).is_some(),
"one client released another owner"
);
// A hidden workspace must not impose its viewport wait on its replacement.
let other = state.workspaces.create_workspace();
let output_id = state.output_id(&output).unwrap();
state.output_workspaces.assign_workspace(output_id, other).unwrap();
state.workspaces.activate(other).unwrap();
state.relayout();
assert!(!state.presentation_dependencies.viewport_blocked(other, &state.windows));
state.output_workspaces.assign_workspace(output_id, workspace).unwrap();
state.workspaces.activate(workspace).unwrap();
state.focused_window = Some(a_id);
state.relayout();
request(&mut c_wire, 2, 1, &[0, 0, 0], None);
request(&mut c_wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(
state.windows.transaction(&c_id).is_none(),
"unmap kept a configure owner"
);
// Deliberately unresponsive final configure expires without charging the wait.
set_width(&mut state, b_id, 112.0);
ack_configure(&mut b_wire, 3);
dispatch(&mut events, &mut state);
assert!(state.windows.transaction(&b_id).is_some());
state.advance_animations_at(Duration::from_millis(300), Duration::from_secs(1));
assert!(state.windows.transaction(&b_id).is_none());
for tick in 1..=160 {
state.advance_animations_at(
Duration::from_millis(16),
Duration::from_secs(1) + Duration::from_millis(tick * 16),
);
}
assert!(
state.render.snapshot(&b_id).is_none(),
"resize snapshot outlived settled handoff"
);
// Aborting a preview must restore the original recipe, not append a
// reversal to the candidate recipe (oversized columns expose the difference).
state.animations_enabled = false;
set_width(&mut state, a_id, 400.0);
set_width(&mut state, b_id, 100.0);
state.focused_window = Some(a_id);
state.workspaces.focus_window(a_id).unwrap();
state
.workspaces
.center_window(a_id, bounds, state.gap_config, &state.window_constraints())
.unwrap();
state.relayout();
state.animations_enabled = true;
set_width(&mut state, b_id, 150.0);
ack_configure(&mut b_wire, 3);
dispatch(&mut events, &mut state);
assert!(
!state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows)
);
state.preview_focus_swipe(Direction::Right, SwipeDirection::Left, 0.5);
assert!(state.focus_swipe.is_some());
assert!(
state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows)
);
assert!(state.finish_focus_swipe(None));
assert!(
!state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows),
"cancelled preview contaminated restored target"
);
// Two shrinking prefixes can jointly cross a page boundary although
// neither source width does so alone. A normal partial commit must rebuild
// the remaining graph immediately, without an explicit relayout.
state.animations_enabled = false;
state.advance_animations_at(Duration::ZERO, Duration::from_secs(5));
state.focused_window = Some(b_id);
state.workspaces.focus_window(b_id).unwrap();
let (d, mut d_wire) = window(&mut state, &mut events, 0xffffffff);
let d_id = state.windows.ids()[&d];
set_width(&mut state, a_id, 160.0);
set_width(&mut state, b_id, 160.0);
set_width(&mut state, d_id, 64.0);
state.focused_window = Some(d_id);
{
let WorkspaceLayout::Scrolling(layout) = &mut state.workspaces.workspace_mut(workspace).unwrap().layout else {
panic!()
};
layout.set_focus_strategy(ViewportFocusStrategy::Paged);
layout.focus(a_id).unwrap();
layout.focus(d_id).unwrap();
}
state.relayout();
state.animations_enabled = true;
set_width(&mut state, a_id, 64.0);
set_width(&mut state, b_id, 64.0);
ack_configure(&mut a_wire, 3);
ack_configure(&mut b_wire, 3);
dispatch(&mut events, &mut state);
assert!(
state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows),
"joint page dependency was lost: layout={:?} a={:?} b={:?}",
state.workspaces.workspace(workspace).unwrap().layout,
state.windows.transaction(&a_id),
state.windows.transaction(&b_id)
);
request(&mut a_wire, 2, 1, &[6, 0, 0], None);
request(&mut a_wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(state.windows.transaction(&a_id).is_none());
assert!(state.windows.transaction(&b_id).is_some());
assert!(
!state
.presentation_dependencies
.viewport_blocked(workspace, &state.windows),
"partial commit failed to retire collective page wait"
);
// A sibling that commits early still shares the held source width when
// a stacked column is retargeted. Input ordering must not choose its source.
state.animations_enabled = false;
state.advance_animations_at(Duration::ZERO, Duration::from_secs(10));
{
let WorkspaceLayout::Scrolling(layout) = &mut state.workspaces.workspace_mut(workspace).unwrap().layout else {
panic!()
};
layout.move_into_column(d_id, b_id).unwrap();
}
set_width(&mut state, b_id, 100.0);
state.animations_enabled = true;
set_width(&mut state, b_id, 200.0);
ack_configure(&mut b_wire, 3);
ack_configure(&mut d_wire, 3);
dispatch(&mut events, &mut state);
request(&mut d_wire, 2, 1, &[6, 0, 0], None);
request(&mut d_wire, 2, 6, &[], None);
dispatch(&mut events, &mut state);
assert!(state.windows.transaction(&d_id).is_none());
set_width(&mut state, b_id, 150.0);
for id in [b_id, d_id] {
assert_eq!(
state.windows.transaction(&id).unwrap().column_width(),
Some((100.0, 150.0)),
"stacked replacement lost canonical presented source"
);
}
}
+49 -3
View File
@@ -111,6 +111,42 @@ impl Ferese {
self.advance_animations(Instant::now());
self.stacking_cache.invalidate();
self.arrange_layers();
let source_column_widths = self
.windows
.records()
.filter_map(|(&id, record)| {
let layout = &self
.workspaces
.workspace(self.workspaces.workspace_for_window(id)?)?
.layout;
let WorkspaceLayout::Scrolling(layout) = layout else {
return None;
};
Some((
id,
record
.resize
.and_then(|transaction| transaction.column_width().map(|(source, _)| source))
.or_else(|| {
// A sibling may have committed while this column is still
// held. Replacement configures must share that cohort's
// presented source, rather than its latest allocated width.
layout
.columns()
.iter()
.find(|column| column.windows.contains(&id))
.and_then(|column| {
column.windows.iter().find_map(|sibling| {
self.windows.transaction(sibling).and_then(|transaction| {
transaction.column_width().map(|(source, _)| source)
})
})
})
})
.or_else(|| layout.allocated_column_width(id))?,
))
})
.collect::<HashMap<_, _>>();
let mut previous_scrolling_world_x = self.windows.take_world_positions();
let pending_column_width_cycles = self.windows.take_column_width_requests();
let outputs = self.space.outputs().cloned().collect::<Vec<_>>();
@@ -261,7 +297,7 @@ impl Ferese {
self.unmap_invisible_windows(&visible);
let now = self.start_time.elapsed();
let now = self.presentation_now();
placements.sort_by_key(|(_, id, ..)| self.window_stack.rank(*id));
@@ -408,7 +444,6 @@ impl Ferese {
&& had_geometry
&& self.animations_enabled
{
self.paused_workspaces.extend(self.workspaces.workspace_for_window(id));
#[cfg(feature = "resize-metrics")]
self.resize_metrics.begin(
id,
@@ -419,12 +454,23 @@ impl Ferese {
self.windows.set_transaction(
id,
crate::resize_transaction::ResizeTransaction::new(serial, now)
.with_source_geometry(window.geometry()),
.with_source_geometry(window.geometry())
.with_column_width(
source_column_widths.get(&id).copied(),
self.workspaces
.workspace_for_window(id)
.and_then(|workspace| self.workspaces.workspace(workspace))
.and_then(|workspace| match &workspace.layout {
WorkspaceLayout::Scrolling(layout) => layout.allocated_column_width(id),
_ => None,
}),
),
);
}
}
}
self.rebuild_presentation_dependencies();
self.sync_window_stacking();
self.retarget_overview();
if !finalize {
+15 -1
View File
@@ -71,7 +71,13 @@ impl Ferese {
candidate.viewport_x().unwrap_or(start)
},
progress,
dependencies: self.presentation_dependencies.save_viewport(workspace_id),
layout: match candidate {
WorkspaceLayout::Scrolling(layout) => Some(layout),
_ => None,
},
});
self.rebuild_presentation_dependencies();
self.swipe.mark_preview_started();
}
if let Some(swipe) = &mut self.focus_swipe {
@@ -94,8 +100,16 @@ impl Ferese {
return false;
};
let current = self.focus_swipe_is_current(&swipe);
let blocked = self
.presentation_dependencies
.viewport_blocked(swipe.workspace, &self.windows);
self.presentation_dependencies
.restore_viewport(swipe.workspace, swipe.dependencies.clone());
// Release from the last input position even if no frame rendered that update.
if current && let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) {
if current
&& !blocked
&& let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace)
{
viewport.current = swipe.position();
viewport.velocity = swipe
.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity)
+57 -17
View File
@@ -37,7 +37,25 @@ impl Ferese {
delta: Duration,
) -> crate::presentation::WindowPresentation {
let record = self.windows.record(id).expect("presentation has a window record");
let mut bounds = overview.presented_bounds(id, geometry.visual);
let mut visual = geometry.visual;
if self.presentation_dependencies.reflow_blocked(id, &self.windows) {
let predicted_x_velocity = visual.velocity.x;
visual.velocity = Default::default();
if !geometry.is_zooming()
&& let Some((workspace, _)) = record.world_x.as_ref()
&& !self
.presentation_dependencies
.viewport_blocked(*workspace, &self.windows)
&& let Some(viewport) = self.viewport_animations.get(workspace)
{
visual.velocity.x = if delta.is_zero() {
-viewport.velocity
} else {
predicted_x_velocity
};
}
}
let mut bounds = overview.presented_bounds(id, visual);
bounds.current.x += offset.0;
bounds.current.y += offset.1;
if let Some(workspace) = self.workspaces.workspace_for_window(id)
@@ -113,18 +131,17 @@ impl Ferese {
.flat_map(move |output| self.output_workspaces.assigned_workspaces(output))
.filter_map(move |id| self.workspaces.workspace(id))
.flat_map(move |workspace| {
let blocked = block_resizes
&& workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
.any(|id| self.windows.record(id).is_some_and(|record| record.resize.is_some()));
workspace
.layout
.window_ids()
.chain(workspace.floating.iter().copied())
.filter_map(move |id| Some((id, self.windows.record(id)?, blocked)))
.filter_map(move |id| {
Some((
id,
self.windows.record(id)?,
block_resizes && self.presentation_dependencies.reflow_blocked(id, &self.windows),
))
})
})
}
@@ -252,10 +269,16 @@ impl Ferese {
.as_ref()
.is_some_and(|swipe| swipe.workspace == *workspace);
Some((*world, *viewport, held))
Some((
*world,
*viewport,
held || self
.presentation_dependencies
.viewport_blocked(*workspace, &self.windows),
))
});
let width = record.coupled_width.as_ref().map(|(_, width)| *width);
let geometry = if delta.is_zero() || blocked {
let geometry = if delta.is_zero() {
original
} else {
predict_geometry(
@@ -265,6 +288,7 @@ impl Ferese {
self.viewport_spring_config,
world,
width,
blocked,
)
};
@@ -304,6 +328,7 @@ fn predict_geometry(
viewport_spring: SpringConfig,
world: Option<(AnimatedValue, AnimatedValue, bool)>,
width: Option<AnimatedValue>,
reflow_held: bool,
) -> WindowGeometry {
let mut predicted = original;
let zooming = predicted.is_zooming();
@@ -312,21 +337,32 @@ fn predict_geometry(
predicted.visual.velocity.width = 0.0;
}
predicted.advance(delta, spring, true);
if !reflow_held {
predicted.advance(delta, spring, true);
} else {
predicted.visual.velocity = Default::default();
}
predicted.visual.target.width = original.visual.target.width;
if let Some((mut world, mut viewport, held)) = world
&& !zooming
{
world.advance(delta, spring);
if !reflow_held {
world.advance(delta, spring);
} else {
world.velocity = 0.0;
}
if !held {
viewport.advance(delta, viewport_spring);
} else {
viewport.velocity = 0.0;
}
predicted.visual.current.x = world.current - viewport.current;
predicted.visual.velocity.x = world.velocity - viewport.velocity;
super::animation::sync_scrolling_coordinates(&mut predicted, &mut world, &viewport, false);
}
if let Some(mut width) = width {
if let Some(mut width) = width
&& !reflow_held
{
width.advance(delta, viewport_spring);
predicted.visual.current.width = width.current;
predicted.visual.velocity.width = width.velocity;
@@ -625,6 +661,7 @@ mod tests {
id,
crate::resize_transaction::ResizeTransaction::new(9.into(), Duration::ZERO),
);
state.rebuild_presentation_dependencies();
let blocked = state.sample_frame(&output, Duration::from_millis(16));
assert_eq!(blocked.windows[&id].geometry, geometry);
assert!(state.windows.transaction(&id).is_some());
@@ -726,6 +763,7 @@ mod tests {
SpringConfig::default(),
None,
None,
false,
)
};
@@ -741,7 +779,8 @@ mod tests {
#[test]
fn scrolling_coordinates_and_coupled_width_use_the_same_forecast_time() {
let geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None);
let mut geometry = WindowGeometry::new(Rect::new(500., 0., 400., 300.), None);
geometry.set_logical_target(Rect::new(400., 0., 500., 300.), Duration::ZERO);
let mut world = AnimatedValue::new(500.);
world.set_target(600.);
let mut viewport = AnimatedValue::new(0.);
@@ -757,6 +796,7 @@ mod tests {
spring,
Some((world, viewport, false)),
Some(width),
false,
);
world.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing);
viewport.advance_with_policy(delta, spring, CrossingPolicy::NoCrossing);
@@ -0,0 +1,479 @@
use super::*;
use smithay::utils::Serial;
#[derive(Clone, Copy, Debug, PartialEq)]
struct ClientCommit {
window: WindowId,
serial: Serial,
}
#[derive(Clone, Copy)]
struct ViewportCommit {
commit: ClientCommit,
width: (f64, f64),
}
#[derive(Clone)]
struct ViewportStep {
target: ferese_layout::ViewportTarget,
inputs: Vec<ViewportCommit>,
}
#[derive(Clone, Default)]
pub(super) struct ViewportDependencies {
steps: Vec<ViewportStep>,
waits: Vec<ClientCommit>,
// A pathological burst must not retain an unbounded history. The fallback
// preserves the linked wait until commit/deadline or an absolute retarget.
overflow: bool,
}
const MAX_VIEWPORT_STEPS: usize = 32;
/// The buffer-dependent properties in this path. Translation has its own
/// inputs; it does not inherit a tile's resize/reflow wait by membership.
#[derive(Default)]
pub(super) struct PresentationDependencies {
reflow: HashMap<WindowId, Vec<ClientCommit>>,
viewport: HashMap<WorkspaceId, ViewportDependencies>,
// A release tick consumes waiting wall time without stepping held springs.
paused_reflow: HashSet<WindowId>,
paused_viewport: HashSet<WorkspaceId>,
}
impl PresentationDependencies {
pub(super) fn save_viewport(&self, workspace: WorkspaceId) -> Option<ViewportDependencies> {
self.viewport.get(&workspace).cloned()
}
pub(super) fn restore_viewport(&mut self, workspace: WorkspaceId, saved: Option<ViewportDependencies>) {
if let Some(saved) = saved {
self.viewport.insert(workspace, saved);
} else {
self.viewport.remove(&workspace);
}
}
#[cfg(test)]
pub(super) fn wait_for_viewport(&mut self, workspace: WorkspaceId, id: WindowId, serial: Serial) {
self.viewport.insert(
workspace,
ViewportDependencies {
waits: vec![ClientCommit { window: id, serial }],
..Default::default()
},
);
}
pub(super) fn needs_tick(&self) -> bool {
!self.paused_reflow.is_empty() || !self.paused_viewport.is_empty()
}
fn pending<W: std::hash::Hash + Eq + Clone>(
wait: ClientCommit,
windows: &window_registry::WindowRegistry<W>,
) -> bool {
windows
.transaction(&wait.window)
.is_some_and(|transaction| transaction.serial() == wait.serial)
}
pub(super) fn reflow_blocked<W: std::hash::Hash + Eq + Clone>(
&self,
id: WindowId,
windows: &window_registry::WindowRegistry<W>,
) -> bool {
self.reflow
.get(&id)
.is_some_and(|waits| waits.iter().any(|wait| Self::pending(*wait, windows)))
}
pub(super) fn viewport_blocked<W: std::hash::Hash + Eq + Clone>(
&self,
workspace: WorkspaceId,
windows: &window_registry::WindowRegistry<W>,
) -> bool {
self.viewport
.get(&workspace)
.is_some_and(|waits| waits.waits.iter().any(|wait| Self::pending(*wait, windows)))
}
pub(super) fn holds<W: std::hash::Hash + Eq + Clone>(
&mut self,
windows: &window_registry::WindowRegistry<W>,
) -> (HashSet<WindowId>, HashSet<WorkspaceId>) {
let reflow = self
.reflow
.keys()
.copied()
.filter(|id| self.reflow_blocked(*id, windows))
.collect::<HashSet<_>>();
let viewport = self
.viewport
.keys()
.copied()
.filter(|workspace| self.viewport_blocked(*workspace, windows))
.collect::<HashSet<_>>();
let mut held_reflow = std::mem::replace(&mut self.paused_reflow, reflow.clone());
let mut held_viewport = std::mem::replace(&mut self.paused_viewport, viewport.clone());
held_reflow.extend(reflow);
held_viewport.extend(viewport);
self.reflow.retain(|_, waits| {
waits.retain(|wait| Self::pending(*wait, windows));
!waits.is_empty()
});
self.viewport.retain(|_, waits| {
waits.waits.retain(|wait| Self::pending(*wait, windows));
for step in &mut waits.steps {
step.inputs.retain(|input| Self::pending(input.commit, windows));
}
if waits.steps.iter().all(|step| step.inputs.is_empty()) {
waits.steps.clear();
}
!waits.steps.is_empty() || !waits.waits.is_empty()
});
(held_reflow, held_viewport)
}
}
fn viewport_dependencies(
layout: &ferese_layout::ScrollingLayout,
pending: &[(WindowId, crate::resize_transaction::ResizeTransaction)],
mut previous: ViewportDependencies,
) -> ViewportDependencies {
let Some(target) = layout.viewport_target() else {
return ViewportDependencies::default();
};
let live = |input: &ViewportCommit| {
pending
.iter()
.any(|(window, transaction)| *window == input.commit.window && transaction.serial() == input.commit.serial)
};
for step in &mut previous.steps {
for input in &mut step.inputs {
if let Some((_, transaction)) = pending.iter().find(|(window, transaction)| {
*window == input.commit.window && transaction.column_width() == Some(input.width)
}) {
input.commit.serial = transaction.serial();
}
}
step.inputs.retain(live);
}
// Fold any prefix with no current configure inputs to the numeric start
// already stored in the next request. No obsolete serial owns a recipe.
if let Some(first) = previous.steps.iter().position(|step| !step.inputs.is_empty()) {
previous.steps.drain(..first);
} else {
previous.steps.clear();
previous.overflow = false;
}
let inputs = pending
.iter()
.filter_map(|(window, transaction)| {
let width = transaction.column_width()?;
transaction.width_changes().then_some(ViewportCommit {
commit: ClientCommit {
window: *window,
serial: transaction.serial(),
},
width,
})
})
.collect::<Vec<_>>();
if previous.steps.last().is_none_or(|step| step.target != *target) {
if !target.uses_previous_target() {
previous.steps.clear();
previous.overflow = false;
}
if previous.steps.len() < MAX_VIEWPORT_STEPS {
previous.steps.push(ViewportStep {
target: target.clone(),
inputs: inputs.clone(),
});
} else {
previous.overflow = true;
}
} else if let Some(step) = previous.steps.last_mut() {
// A replacement binds this request to its latest configure, including
// same-width height changes. Older recipe inputs retire above.
step.inputs = inputs.clone();
}
let mut owners = previous
.steps
.iter()
.flat_map(|step| step.inputs.iter().map(|input| input.commit))
.collect::<Vec<_>>();
owners.extend(inputs.iter().map(|input| input.commit));
owners.sort_by_key(|owner| owner.window.0);
owners.dedup();
let evaluate = |only: Option<ClientCommit>, exclude: Option<ClientCommit>| {
let mut start = None;
for step in &previous.steps {
let widths = step
.inputs
.iter()
.filter(|input| only.is_none_or(|only| input.commit == only) && exclude != Some(input.commit))
.map(|input| (input.commit.window, input.width.0, input.width.1))
.collect::<Vec<_>>();
start = Some(step.target.with_held_widths(&widths, start));
}
start.unwrap_or(layout.viewport_x())
};
let held = evaluate(None, None);
previous.waits = owners
.into_iter()
.filter(|owner| {
previous.overflow
|| (evaluate(Some(*owner), None) - layout.viewport_x()).abs() > 0.001
|| (held - evaluate(None, Some(*owner))).abs() > 0.001
})
.collect();
// Keep a bounded recipe while any of its inputs is pending: a partial
// commit can expose a page-packing dependency previously masked by others.
if previous.steps.iter().all(|step| step.inputs.is_empty()) {
return ViewportDependencies::default();
}
previous
}
impl Ferese {
pub(super) fn release_presentation_dependencies(&mut self) {
let paused_reflow = self.presentation_dependencies.paused_reflow.clone();
let paused_viewport = self.presentation_dependencies.paused_viewport.clone();
self.rebuild_presentation_dependencies();
self.presentation_dependencies.paused_reflow.extend(paused_reflow);
self.presentation_dependencies.paused_viewport.extend(paused_viewport);
}
pub(super) fn rebuild_presentation_dependencies(&mut self) {
let mut reflow = HashMap::new();
let mut viewport = HashMap::new();
let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport);
for (&id, _) in self.windows.records() {
let Some(workspace_id) = self.workspaces.workspace_for_window(id) else {
continue;
};
let Some(workspace) = self.workspaces.workspace(workspace_id) else {
continue;
};
let own_column = match &workspace.layout {
WorkspaceLayout::Scrolling(layout) => {
layout.columns().iter().position(|column| column.windows.contains(&id))
}
WorkspaceLayout::Tree(_) => None,
};
let mut waits = Vec::new();
for slow in self.windows.resizing() {
if self.workspaces.workspace_for_window(*slow) != Some(workspace_id) {
continue;
}
let transaction = *self.windows.transaction(slow).unwrap();
let commit = ClientCommit {
window: *slow,
serial: transaction.serial(),
};
let linked = id == *slow
|| match &workspace.layout {
WorkspaceLayout::Scrolling(layout) => own_column.is_some_and(|own| {
layout
.columns()
.iter()
.position(|column| column.windows.contains(slow))
.is_some_and(|source| source == own || (source < own && transaction.width_changes()))
}),
WorkspaceLayout::Tree(_) => workspace.layout.contains(id) && workspace.layout.contains(*slow),
};
if linked {
waits.push(commit);
}
}
if !waits.is_empty() {
reflow.insert(id, waits);
}
}
for workspace in self.workspaces.iter() {
let layout = self
.focus_swipe
.as_ref()
.filter(|swipe| swipe.workspace == workspace.id)
.and_then(|swipe| swipe.layout.as_ref())
.or_else(|| match &workspace.layout {
WorkspaceLayout::Scrolling(layout) => Some(layout),
_ => None,
});
let Some(layout) = layout else { continue };
let waits = viewport_dependencies(
layout,
&self
.windows
.resizing()
.filter(|id| {
self.workspaces.workspace_for_window(**id) == Some(workspace.id) && layout.contains(**id)
})
.map(|id| (*id, *self.windows.transaction(id).unwrap()))
.collect::<Vec<_>>(),
previous_viewport.remove(&workspace.id).unwrap_or_default(),
);
if !waits.steps.is_empty() || !waits.waits.is_empty() {
viewport.insert(workspace.id, waits);
}
}
// A retarget uses the new dependency set immediately. A dependency that
// disappeared must not freeze a newly independent target for one frame.
self.presentation_dependencies
.paused_reflow
.retain(|id| reflow.contains_key(id));
self.presentation_dependencies
.paused_viewport
.retain(|id| viewport.get(id).is_some_and(|waits| !waits.waits.is_empty()));
self.presentation_dependencies.reflow = reflow;
self.presentation_dependencies.viewport = viewport;
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::resize_transaction::ResizeTransaction;
#[test]
fn viewport_recipe_handles_cancellation_joint_pages_and_bounded_bursts() {
use ferese_layout::{ColumnWidth, ScrollingLayout};
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
let gaps = GapConfig {
inner: 0.0,
outer: 0.0,
smart: false,
};
let constraints = HashMap::new();
let a = WindowId(1);
let b = WindowId(2);
let c = WindowId(3);
let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(100.0));
layout.set_focus_strategy(ViewportFocusStrategy::Center);
layout.insert(a, None).unwrap();
layout.insert(b, Some(a)).unwrap();
layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap();
layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap();
layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(b))
.unwrap();
let pending = [(
a,
ResizeTransaction::new(10.into(), Duration::ZERO).with_column_width(Some(100.0), Some(150.0)),
)];
let first = viewport_dependencies(&layout, &pending, Default::default());
assert_eq!(first.waits.len(), 1);
layout.slide_focus_from(b, a).unwrap();
layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(a))
.unwrap();
assert_eq!(layout.viewport_x(), 50.0);
let canceled = viewport_dependencies(&layout, &pending, first);
assert!(
canceled.waits.is_empty(),
"relative target cancels pending prefix width"
);
let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(40.0));
layout.set_focus_strategy(ViewportFocusStrategy::Paged);
layout.insert(a, None).unwrap();
layout.insert(b, Some(a)).unwrap();
layout.insert(c, Some(b)).unwrap();
layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(c))
.unwrap();
let pending = [
(
a,
ResizeTransaction::new(10.into(), Duration::ZERO).with_column_width(Some(100.0), Some(40.0)),
),
(
b,
ResizeTransaction::new(20.into(), Duration::ZERO).with_column_width(Some(100.0), Some(40.0)),
),
];
assert!(!layout.viewport_depends_on_width(a, 100.0, 40.0));
assert!(!layout.viewport_depends_on_width(b, 100.0, 40.0));
let both = viewport_dependencies(&layout, &pending, Default::default());
assert_eq!(both.waits.len(), 2, "joint held widths select another page");
let partial = viewport_dependencies(&layout, &pending[1..], both);
assert!(partial.waits.is_empty(), "one remaining held width fits this page");
assert!(viewport_dependencies(&layout, &[], partial).steps.is_empty());
// Independent absolute requests reset history. Relative request storage
// is capped even if a client keeps replacing its configure.
layout.set_focus_strategy(ViewportFocusStrategy::Center);
layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap();
layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap();
layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(b))
.unwrap();
let pending = [(
a,
ResizeTransaction::new(30.into(), Duration::ZERO).with_column_width(Some(100.0), Some(150.0)),
)];
let mut graph = viewport_dependencies(&layout, &pending, Default::default());
for index in 0..80 {
let (from, to) = if index % 2 == 0 { (b, a) } else { (a, b) };
layout.slide_focus_from(from, to).unwrap();
layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(to))
.unwrap();
graph = viewport_dependencies(&layout, &pending, graph);
assert!(graph.steps.len() <= MAX_VIEWPORT_STEPS);
}
assert!(graph.overflow);
assert!(!graph.waits.is_empty(), "bounded fallback must not free linked input");
layout.center_window(a, bounds, gaps, &constraints).unwrap();
graph = viewport_dependencies(&layout, &pending, graph);
assert_eq!(graph.steps.len(), 1);
assert!(!graph.overflow);
}
#[test]
fn properties_wait_for_only_current_configures_and_consume_release_time() {
let mut windows = window_registry::WindowRegistry::default();
let a = WindowId(1);
let b = WindowId(2);
let neighbor = WindowId(3);
windows.register("a", a);
windows.register("b", b);
windows.register("neighbor", neighbor);
windows.set_transaction(a, ResizeTransaction::new(10.into(), Duration::ZERO));
windows.set_transaction(b, ResizeTransaction::new(20.into(), Duration::from_millis(100)));
let commit = |window, serial| ClientCommit {
window,
serial: Serial::from(serial),
};
let workspace = WorkspaceId(1);
let mut dependencies = PresentationDependencies::default();
dependencies.reflow.insert(a, vec![commit(a, 10)]);
dependencies.reflow.insert(neighbor, vec![commit(a, 10), commit(b, 20)]);
dependencies.wait_for_viewport(workspace, b, 20.into());
assert!(dependencies.reflow_blocked(neighbor, &windows));
assert!(!dependencies.reflow_blocked(b, &windows));
assert!(dependencies.viewport_blocked(workspace, &windows));
dependencies.holds(&windows);
// A replacement cannot be released by the older committed serial.
windows.set_transaction(b, ResizeTransaction::new(21.into(), Duration::from_millis(150)));
assert!(!windows.transaction(&b).unwrap().accepts(Some(20.into())));
assert!(!dependencies.viewport_blocked(workspace, &windows));
dependencies.wait_for_viewport(workspace, b, 21.into());
dependencies.reflow.insert(neighbor, vec![commit(a, 10), commit(b, 21)]);
windows.expire_transactions(Duration::from_millis(300));
assert!(windows.transaction(&a).is_none());
assert!(windows.transaction(&b).is_some());
assert!(dependencies.reflow_blocked(neighbor, &windows));
let (held, _) = dependencies.holds(&windows);
assert!(held.contains(&a), "release tick must not charge waiting time");
let (held, _) = dependencies.holds(&windows);
assert!(!held.contains(&a));
// Unmap removes the serial owner; both properties retire on the tick.
windows.remove(&"b");
assert!(!dependencies.viewport_blocked(workspace, &windows));
let (_, held) = dependencies.holds(&windows);
assert!(held.contains(&workspace));
assert!(dependencies.holds(&windows).1.is_empty());
assert!(!dependencies.needs_tick());
assert!(dependencies.reflow.is_empty() && dependencies.viewport.is_empty());
}
}
+6 -5
View File
@@ -432,7 +432,7 @@ impl Ferese {
self.focus_history.remove(id);
#[cfg(feature = "resize-metrics")]
self.resize_metrics
.end(id, self.start_time.elapsed(), crate::resize_metrics::End::Cancelled);
.end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled);
self.render.remove_window(id);
self.capture_render.remove_window(id);
self.window_stack.remove(id);
@@ -554,11 +554,12 @@ impl Ferese {
{
#[cfg(feature = "resize-metrics")]
{
self.measure_resize_pauses(self.start_time.elapsed());
self.measure_resize_pauses(self.presentation_now());
self.resize_metrics
.end(id, self.start_time.elapsed(), crate::resize_metrics::End::Commit);
.end(id, self.presentation_now(), crate::resize_metrics::End::Commit);
}
self.windows.clear_transaction(&id);
self.release_presentation_dependencies();
}
let Some(size) = client_size(window) else {
@@ -611,7 +612,7 @@ impl Ferese {
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
#[cfg(feature = "resize-metrics")]
self.resize_metrics
.end(id, self.start_time.elapsed(), crate::resize_metrics::End::Cancelled);
.end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled);
self.windows.clear_transaction(&id);
self.relayout();
}
@@ -650,7 +651,7 @@ impl Ferese {
self.windows.set_geometry(id, WindowGeometry::new(rect, Some(size)));
#[cfg(feature = "resize-metrics")]
self.resize_metrics
.end(id, self.start_time.elapsed(), crate::resize_metrics::End::Cancelled);
.end(id, self.presentation_now(), crate::resize_metrics::End::Cancelled);
self.windows.clear_transaction(&id);
self.render.clear_snapshot(&id);
self.relayout();