Unify scrolling viewport presentation ownership

This commit is contained in:
Samuel Onoja committed 2026-10-07 08:44:40 +01:00
1 parent 4a9880eaad
commit a00f1c6f3a
12 files changed
+857 -279

No files matched your search

+6 -4
View File
@@ -14,9 +14,12 @@ mod prediction;
mod presentation_dependencies; mod presentation_dependencies;
pub(crate) use prediction::FrameScene; pub(crate) use prediction::FrameScene;
mod lifecycle; mod lifecycle;
mod viewport;
mod window_registry; mod window_registry;
mod windows; mod windows;
use viewport::ViewportPresentation;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::error::Error; use std::error::Error;
use std::ffi::OsString; use std::ffi::OsString;
@@ -146,6 +149,7 @@ impl SlideOffset {
} }
} }
#[derive(Clone, PartialEq)]
struct FocusSwipe { struct FocusSwipe {
workspace: WorkspaceId, workspace: WorkspaceId,
from: WindowId, from: WindowId,
@@ -444,11 +448,10 @@ pub struct Ferese {
pub(crate) pending_logout: Option<u32>, pub(crate) pending_logout: Option<u32>,
pub(crate) logout_query: Option<u32>, pub(crate) logout_query: Option<u32>,
pub(crate) logout_owner: Option<ObjectId>, pub(crate) logout_owner: Option<ObjectId>,
viewport_animations: HashMap<WorkspaceId, AnimatedValue>, viewports: HashMap<WorkspaceId, ViewportPresentation>,
presentation_dependencies: presentation_dependencies::PresentationDependencies, presentation_dependencies: presentation_dependencies::PresentationDependencies,
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
pub(crate) resize_metrics: crate::resize_metrics::ResizeMetrics, pub(crate) resize_metrics: crate::resize_metrics::ResizeMetrics,
focus_swipe: Option<FocusSwipe>,
workspace_slides: HashMap<OutputId, WorkspaceSlide>, workspace_slides: HashMap<OutputId, WorkspaceSlide>,
workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>, workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>,
pub focused_window: Option<WindowId>, pub focused_window: Option<WindowId>,
@@ -721,11 +724,10 @@ impl Ferese {
pending_logout: None, pending_logout: None,
logout_query: None, logout_query: None,
logout_owner: None, logout_owner: None,
viewport_animations: HashMap::new(), viewports: HashMap::new(),
presentation_dependencies: Default::default(), presentation_dependencies: Default::default(),
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
resize_metrics: crate::resize_metrics::ResizeMetrics::default(), resize_metrics: crate::resize_metrics::ResizeMetrics::default(),
focus_swipe: None,
workspace_slides: HashMap::new(), workspace_slides: HashMap::new(),
workspace_slide_offsets: HashMap::new(), workspace_slide_offsets: HashMap::new(),
focused_window: None, focused_window: None,
+70 -69
View File
@@ -50,15 +50,15 @@ impl Ferese {
let viewport = self let viewport = self
.presentation_dependencies .presentation_dependencies
.viewport_blocked(workspace, &self.windows) .viewport_blocked(workspace, &self.windows)
&& self.viewport_animations.get(&workspace).is_some_and(moving) && self
.viewports
.get(&workspace)
.is_some_and(ViewportPresentation::spring_is_moving)
&& self && self
.output_workspaces .output_workspaces
.output_for_workspace(workspace) .output_for_workspace(workspace)
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace)) .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace))
&& self && self.focus_swipe().is_none_or(|swipe| swipe.workspace != workspace);
.focus_swipe
.as_ref()
.is_none_or(|swipe| swipe.workspace != workspace);
let others = self.workspaces.workspace(workspace).is_some_and(|workspace| { let others = self.workspaces.workspace(workspace).is_some_and(|workspace| {
workspace workspace
.layout .layout
@@ -95,7 +95,7 @@ impl Ferese {
} }
fn animations_need_tick(&self) -> bool { fn animations_need_tick(&self) -> bool {
if self.focus_swipe.is_some() if self.focus_swipe().is_some()
|| self.presentation_dependencies.needs_tick() || self.presentation_dependencies.needs_tick()
|| !self.workspace_slides.is_empty() || !self.workspace_slides.is_empty()
|| !self.render.closing.is_empty() || !self.render.closing.is_empty()
@@ -106,13 +106,11 @@ impl Ferese {
return true; return true;
} }
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0; if self.viewports.iter().any(|(workspace, viewport)| {
if self.viewport_animations.iter().any(|(workspace, viewport)| {
self.output_workspaces self.output_workspaces
.output_for_workspace(*workspace) .output_for_workspace(*workspace)
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace)) .is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace))
&& moving(viewport) && viewport.needs_tick()
}) { }) {
return true; return true;
} }
@@ -151,12 +149,12 @@ impl Ferese {
} }
let delta = delta.mul_f64(self.animation_speed); let delta = delta.mul_f64(self.animation_speed);
if self if let Some(workspace) = self
.focus_swipe .focus_swipe()
.as_ref() .filter(|swipe| !self.focus_swipe_is_current(swipe))
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe)) .map(|swipe| swipe.workspace)
&& let Some(swipe) = self.focus_swipe.take()
{ {
let swipe = self.viewports.get_mut(&workspace).unwrap().end_gesture(None).unwrap();
self.presentation_dependencies self.presentation_dependencies
.restore_viewport(swipe.workspace, swipe.dependencies); .restore_viewport(swipe.workspace, swipe.dependencies);
self.release_presentation_dependencies(); self.release_presentation_dependencies();
@@ -284,7 +282,15 @@ impl Ferese {
} }
// Capture holds before expiry so waiting wall time never steps reflow. // Capture holds before expiry so waiting wall time never steps reflow.
let (mut held_reflow, mut held_viewport) = self.presentation_dependencies.holds(&self.windows); for (workspace, viewport) in &mut self.viewports {
if self
.presentation_dependencies
.viewport_blocked(*workspace, &self.windows)
{
viewport.reconcile_hold(true, true);
}
}
let mut held_reflow = self.presentation_dependencies.holds(&self.windows);
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
self.measure_resize_pauses(now); self.measure_resize_pauses(now);
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
@@ -304,9 +310,8 @@ impl Ferese {
self.release_presentation_dependencies(); self.release_presentation_dependencies();
} }
let (released_reflow, released_viewport) = self.presentation_dependencies.holds(&self.windows); let released_reflow = self.presentation_dependencies.holds(&self.windows);
held_reflow.extend(released_reflow); held_reflow.extend(released_reflow);
held_viewport.extend(released_viewport);
let animations_enabled = self.animations_enabled; let animations_enabled = self.animations_enabled;
let animation_speed = self.animation_speed; let animation_speed = self.animation_speed;
#[cfg(feature = "resize-metrics")] #[cfg(feature = "resize-metrics")]
@@ -353,9 +358,8 @@ impl Ferese {
}); });
active_animation |= self.render.snapshots().next().is_some(); active_animation |= self.render.snapshots().next().is_some();
// Keep scheduling frames while waiting, so the deadline cannot stall. // Keep scheduling frames while waiting, so the deadline cannot stall.
active_animation |= active_animation |= self.windows.resizing().next().is_some() || !held_reflow.is_empty();
self.windows.resizing().next().is_some() || !held_reflow.is_empty() || !held_viewport.is_empty(); for (workspace, viewport) in &mut self.viewports {
for (workspace, viewport) in &mut self.viewport_animations {
if self if self
.output_workspaces .output_workspaces
.output_for_workspace(*workspace) .output_for_workspace(*workspace)
@@ -364,18 +368,13 @@ impl Ferese {
continue; continue;
} }
if held_viewport.contains(workspace) { active_animation |= viewport.advance(
continue; delta,
} self.viewport_spring_config,
self.animations_enabled,
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) { self.presentation_dependencies
viewport.current = swipe.position(); .viewport_blocked(*workspace, &self.windows),
viewport.velocity = 0.0; );
} else if self.animations_enabled {
active_animation |= viewport.advance(delta, self.viewport_spring_config);
} else {
viewport.snap();
}
} }
let mut remaps = Vec::new(); let mut remaps = Vec::new();
@@ -417,10 +416,10 @@ impl Ferese {
} }
if let Some((workspace, world_x)) = record.world_x.as_mut() if let Some((workspace, world_x)) = record.world_x.as_mut()
&& let Some(viewport) = self.viewport_animations.get(workspace) && let Some(viewport) = self.viewports.get(workspace)
{ {
if zooming { if zooming {
sync_scrolling_coordinates(geometry, world_x, viewport, true); sync_scrolling_coordinates(geometry, world_x, viewport.motion(), true);
} else if !held && self.animations_enabled { } else if !held && self.animations_enabled {
active_animation |= world_x.advance(delta, self.spring_config); active_animation |= world_x.advance(delta, self.spring_config);
} else if !held { } else if !held {
@@ -432,10 +431,7 @@ impl Ferese {
if held { if held {
presented_world.velocity = 0.0; presented_world.velocity = 0.0;
} }
let mut presented_viewport = *viewport; let presented_viewport = viewport.presented_motion();
if held_viewport.contains(workspace) {
presented_viewport.velocity = 0.0;
}
sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false); sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false);
} }
} }
@@ -748,9 +744,9 @@ mod tests {
#[test] #[test]
fn scrolling_target_changes_and_zoom_ownership_switches_are_continuous() { fn scrolling_target_changes_and_zoom_ownership_switches_are_continuous() {
let config = SpringConfig::default(); let config = SpringConfig::default();
let mut viewport = AnimatedValue::new(100.0); let mut viewport = ViewportPresentation::new(100.0);
viewport.set_target(400.0); viewport.retarget(400.0, true);
viewport.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing); viewport.advance(Duration::from_millis(40), config, true, false);
let mut world = AnimatedValue::new(500.0); let mut world = AnimatedValue::new(500.0);
world.set_target(900.0); world.set_target(900.0);
world.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing); world.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing);
@@ -758,25 +754,25 @@ mod tests {
width.set_target(700.0); width.set_target(700.0);
width.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing); width.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing);
let mut geometry = WindowGeometry::new( let mut geometry = WindowGeometry::new(
Rect::new(world.current - viewport.current, 0.0, width.current, 300.0), Rect::new(world.current - viewport.motion().current, 0.0, width.current, 300.0),
None, None,
); );
let target = Rect::new(world.target - viewport.target, 0.0, width.target, 300.0); let target = Rect::new(world.target - viewport.motion().target, 0.0, width.target, 300.0);
geometry.set_logical_target(target, Duration::from_millis(40)); geometry.set_logical_target(target, Duration::from_millis(40));
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, false); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false);
geometry.visual.velocity.width = width.velocity; geometry.visual.velocity.width = width.velocity;
let before = geometry.visual.current; let before = geometry.visual.current;
viewport.retarget_preserving_motion(300.0); viewport.retarget(300.0, true);
world.retarget_preserving_motion(1000.0); world.retarget_preserving_motion(1000.0);
width.retarget_preserving_motion(600.0); width.retarget_preserving_motion(600.0);
geometry.set_logical_target( geometry.set_logical_target(
Rect::new(world.target - viewport.target, 0.0, width.target, 300.0), Rect::new(world.target - viewport.motion().target, 0.0, width.target, 300.0),
Duration::from_millis(40), Duration::from_millis(40),
); );
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, false); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false);
assert_eq!(geometry.visual.current, before); assert_eq!(geometry.visual.current, before);
assert_eq!(geometry.visual.velocity.x, world.velocity - viewport.velocity); assert_eq!(geometry.visual.velocity.x, world.velocity - viewport.motion().velocity);
assert_eq!(geometry.logical.x, world.target - viewport.target); assert_eq!(geometry.logical.x, world.target - viewport.motion().target);
for mode in [ for mode in [
PresentationMode::Maximized, PresentationMode::Maximized,
@@ -785,9 +781,9 @@ mod tests {
] { ] {
let before = geometry.visual.current; let before = geometry.visual.current;
geometry.set_presentation_mode(Rect::new(0.0, 0.0, 1920.0, 1080.0), mode, Duration::from_millis(40)); geometry.set_presentation_mode(Rect::new(0.0, 0.0, 1920.0, 1080.0), mode, Duration::from_millis(40));
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, true); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), true);
assert_eq!(geometry.visual.current, before); assert_eq!(geometry.visual.current, before);
assert_eq!(world.current, geometry.visual.current.x + viewport.current); assert_eq!(world.current, geometry.visual.current.x + viewport.motion().current);
geometry.advance(Duration::ZERO, config, true); geometry.advance(Duration::ZERO, config, true);
assert_eq!(geometry.visual.current, before); assert_eq!(geometry.visual.current, before);
geometry.advance(Duration::from_millis(16), config, true); geometry.advance(Duration::from_millis(16), config, true);
@@ -799,7 +795,7 @@ mod tests {
let zoom_owned = geometry.is_zooming(); let zoom_owned = geometry.is_zooming();
geometry.advance(Duration::from_millis(16), config, true); geometry.advance(Duration::from_millis(16), config, true);
if zoom_owned { if zoom_owned {
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, true); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), true);
} }
if !geometry.is_zooming() { if !geometry.is_zooming() {
break; break;
@@ -807,8 +803,8 @@ mod tests {
} }
assert!(!geometry.is_zooming()); assert!(!geometry.is_zooming());
let before = geometry.visual.current; let before = geometry.visual.current;
world.set_target(geometry.logical.x + viewport.target); world.set_target(geometry.logical.x + viewport.motion().target);
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, false); sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false);
assert_eq!(geometry.visual.current, before); assert_eq!(geometry.visual.current, before);
} }
@@ -870,9 +866,11 @@ mod tests {
state.windows.records.insert(id, settled()); state.windows.records.insert(id, settled());
let mut viewport = AnimatedValue::new(0.0); let mut viewport = AnimatedValue::new(0.0);
viewport.set_target(500.0); viewport.set_target(500.0);
state.viewport_animations.insert(workspace, viewport); state
.viewports
.insert(workspace, ViewportPresentation::from_motion(viewport));
state.advance_animations_at(Duration::from_millis(40), Duration::from_millis(40)); state.advance_animations_at(Duration::from_millis(40), Duration::from_millis(40));
let frozen = state.viewport_animations[&workspace]; let frozen = *state.viewports[&workspace].motion();
assert!(frozen.current > 0.0 && frozen.current < 500.0); assert!(frozen.current > 0.0 && frozen.current < 500.0);
// No intervening dispatch at all: expiration happens on the 300ms tick. // No intervening dispatch at all: expiration happens on the 300ms tick.
@@ -886,7 +884,7 @@ mod tests {
.wait_for_viewport(workspace, id, 1.into()); .wait_for_viewport(workspace, id, 1.into());
state.advance_animations_at(Duration::from_millis(300), Duration::from_millis(340)); state.advance_animations_at(Duration::from_millis(300), Duration::from_millis(340));
assert!(state.windows.transaction(&id).is_none()); assert!(state.windows.transaction(&id).is_none());
assert_eq!(state.viewport_animations[&workspace], frozen); assert_eq!(*state.viewports[&workspace].motion(), frozen);
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(356)); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(356));
let mut expected = frozen; let mut expected = frozen;
expected.advance_with_policy( expected.advance_with_policy(
@@ -894,7 +892,7 @@ mod tests {
state.viewport_spring_config, state.viewport_spring_config,
CrossingPolicy::NoCrossing, CrossingPolicy::NoCrossing,
); );
assert_eq!(state.viewport_animations[&workspace], expected); assert_eq!(*state.viewports[&workspace].motion(), expected);
// A commit releases the barrier between ticks. Drop that release tick's // A commit releases the barrier between ticks. Drop that release tick's
// elapsed pause too, then resume at the next normal 16ms interval. // elapsed pause too, then resume at the next normal 16ms interval.
@@ -907,10 +905,10 @@ mod tests {
.presentation_dependencies .presentation_dependencies
.wait_for_viewport(workspace, id, 2.into()); .wait_for_viewport(workspace, id, 2.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(456)); state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(456));
let frozen = state.viewport_animations[&workspace]; let frozen = *state.viewports[&workspace].motion();
state.windows.clear_transaction(&id); state.windows.clear_transaction(&id);
state.advance_animations_at(Duration::from_millis(200), Duration::from_millis(656)); state.advance_animations_at(Duration::from_millis(200), Duration::from_millis(656));
assert_eq!(state.viewport_animations[&workspace], frozen); assert_eq!(*state.viewports[&workspace].motion(), frozen);
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(672)); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(672));
let mut expected = frozen; let mut expected = frozen;
expected.advance_with_policy( expected.advance_with_policy(
@@ -918,14 +916,14 @@ mod tests {
state.viewport_spring_config, state.viewport_spring_config,
CrossingPolicy::NoCrossing, CrossingPolicy::NoCrossing,
); );
assert_eq!(state.viewport_animations[&workspace], expected); assert_eq!(*state.viewports[&workspace].motion(), expected);
let frozen = state.viewport_animations[&workspace]; let frozen = *state.viewports[&workspace].motion();
let instant = Instant::now(); let instant = Instant::now();
state.last_animation_tick = instant; state.last_animation_tick = instant;
state.session_lock.sleeping = true; state.session_lock.sleeping = true;
state.advance_animations(instant + Duration::from_millis(300)); state.advance_animations(instant + Duration::from_millis(300));
assert_eq!(state.viewport_animations[&workspace], frozen); assert_eq!(*state.viewports[&workspace].motion(), frozen);
state.session_lock.sleeping = false; state.session_lock.sleeping = false;
state.advance_animations(instant + Duration::from_millis(316)); state.advance_animations(instant + Duration::from_millis(316));
let mut expected = frozen; let mut expected = frozen;
@@ -934,11 +932,11 @@ mod tests {
state.viewport_spring_config, state.viewport_spring_config,
CrossingPolicy::NoCrossing, CrossingPolicy::NoCrossing,
); );
assert_eq!(state.viewport_animations[&workspace], expected); assert_eq!(*state.viewports[&workspace].motion(), expected);
// Wake paths reset the global clock even when nothing dispatched while // Wake paths reset the global clock even when nothing dispatched while
// asleep. This excludes a whole undelivered pause interval as well. // asleep. This excludes a whole undelivered pause interval as well.
let frozen = state.viewport_animations[&workspace]; let frozen = *state.viewports[&workspace].motion();
state.last_animation_tick = Instant::now() - Duration::from_secs(5); state.last_animation_tick = Instant::now() - Duration::from_secs(5);
state.reset_animation_clock(); state.reset_animation_clock();
let resumed = state.last_animation_tick; let resumed = state.last_animation_tick;
@@ -949,11 +947,12 @@ mod tests {
state.viewport_spring_config, state.viewport_spring_config,
CrossingPolicy::NoCrossing, CrossingPolicy::NoCrossing,
); );
assert_eq!(state.viewport_animations[&workspace], expected); assert_eq!(*state.viewports[&workspace].motion(), expected);
// A settled transaction still has to flush its resume guard; otherwise // A settled transaction still has to flush its resume guard; otherwise
// the next unrelated animation would inherit an old paused workspace. // the next unrelated animation would inherit an old paused workspace.
state.viewport_animations.get_mut(&workspace).unwrap().snap(); let target = state.viewports[&workspace].motion().target;
state.viewports.get_mut(&workspace).unwrap().retarget(target, false);
state.windows.record_mut(id).unwrap().focus = Some(AnimatedValue::new(0.0)); state.windows.record_mut(id).unwrap().focus = Some(AnimatedValue::new(0.0));
state.windows.set_transaction( state.windows.set_transaction(
id, id,
@@ -964,9 +963,11 @@ mod tests {
.presentation_dependencies .presentation_dependencies
.wait_for_viewport(workspace, id, 3.into()); .wait_for_viewport(workspace, id, 3.into());
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(900)); state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(900));
assert!(state.presentation_dependencies.needs_tick()); assert!(state.viewports[&workspace].is_held());
state.windows.clear_transaction(&id); state.windows.clear_transaction(&id);
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(916)); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(916));
assert!(!state.presentation_dependencies.needs_tick()); assert!(!state.presentation_dependencies.needs_tick());
assert!(!state.viewports[&workspace].is_held());
assert!(!state.viewports[&workspace].needs_tick());
} }
} }
@@ -683,12 +683,13 @@ fn resize_dependencies_protocol_and_pixels() {
.center_window(a_id, bounds, state.gap_config, &state.window_constraints()) .center_window(a_id, bounds, state.gap_config, &state.window_constraints())
.unwrap(); .unwrap();
state.relayout(); state.relayout();
let before = state.viewport_animations[&workspace].current; let before = state.viewports[&workspace].motion().current;
let held_size = state.windows.geometry(&b_id).unwrap().visual.current.width; 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; let held_world = state.windows.record(b_id).unwrap().world_x.unwrap().1;
state.advance_animations_at(Duration::from_millis(16), Duration::ZERO); state.advance_animations_at(Duration::from_millis(16), Duration::ZERO);
assert_ne!( assert_ne!(
state.viewport_animations[&workspace].current, before, state.viewports[&workspace].motion().current,
before,
"independent viewport stalled before delayed buffer commit" "independent viewport stalled before delayed buffer commit"
); );
assert_eq!(state.windows.geometry(&b_id).unwrap().visual.current.width, held_size); assert_eq!(state.windows.geometry(&b_id).unwrap().visual.current.width, held_size);
@@ -711,7 +712,7 @@ fn resize_dependencies_protocol_and_pixels() {
assert!((origin.x - geometry.visual.current.x).abs() < 0.001); assert!((origin.x - geometry.visual.current.x).abs() < 0.001);
let forecast = state.sample_frame(&output, Duration::from_millis(8)); let forecast = state.sample_frame(&output, Duration::from_millis(8));
let predicted = &forecast.windows[&b_id]; let predicted = &forecast.windows[&b_id];
let mut viewport = state.viewport_animations[&workspace]; let mut viewport = *state.viewports[&workspace].motion();
viewport.advance( viewport.advance(
Duration::from_millis(8).mul_f64(state.animation_speed), Duration::from_millis(8).mul_f64(state.animation_speed),
state.viewport_spring_config, state.viewport_spring_config,
@@ -779,9 +780,9 @@ fn resize_dependencies_protocol_and_pixels() {
.presentation_dependencies .presentation_dependencies
.viewport_blocked(workspace, &state.windows) .viewport_blocked(workspace, &state.windows)
); );
let frozen = state.viewport_animations[&workspace]; let frozen = *state.viewports[&workspace].motion();
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(32)); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(32));
assert_eq!(state.viewport_animations[&workspace], frozen); assert_eq!(*state.viewports[&workspace].motion(), frozen);
// Supersede the acknowledged size, then commit the older acknowledgement. // Supersede the acknowledged size, then commit the older acknowledgement.
set_width(&mut state, b_id, 96.0); set_width(&mut state, b_id, 96.0);
let latest = *state.windows.transaction(&b_id).unwrap(); let latest = *state.windows.transaction(&b_id).unwrap();
@@ -813,9 +814,9 @@ fn resize_dependencies_protocol_and_pixels() {
.presentation_dependencies .presentation_dependencies
.viewport_blocked(workspace, &state.windows) .viewport_blocked(workspace, &state.windows)
); );
let frozen = state.viewport_animations[&workspace].current; let frozen = state.viewports[&workspace].motion().current;
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(48)); state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(48));
assert_ne!(state.viewport_animations[&workspace].current, frozen); assert_ne!(state.viewports[&workspace].motion().current, frozen);
let client = b.toplevel().unwrap().wl_surface().client().unwrap(); let client = b.toplevel().unwrap().wl_surface().client().unwrap();
let shm = client let shm = client
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ()) .create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
@@ -900,7 +901,7 @@ fn resize_dependencies_protocol_and_pixels() {
.viewport_blocked(workspace, &state.windows) .viewport_blocked(workspace, &state.windows)
); );
state.preview_focus_swipe(Direction::Right, SwipeDirection::Left, 0.5); state.preview_focus_swipe(Direction::Right, SwipeDirection::Left, 0.5);
assert!(state.focus_swipe.is_some()); assert!(state.focus_swipe().is_some());
assert!( assert!(
state state
.presentation_dependencies .presentation_dependencies
+16 -18
View File
@@ -20,7 +20,7 @@ impl Ferese {
.output_workspaces .output_workspaces
.reconcile_workspaces(&mut self.workspaces, visible) .reconcile_workspaces(&mut self.workspaces, visible)
{ {
self.viewport_animations.remove(&workspace); self.viewports.remove(&workspace);
} }
} }
@@ -201,7 +201,7 @@ impl Ferese {
let layout = let layout =
match workspace match workspace
.layout .layout
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused) .resolve_geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
{ {
Ok(layout) => layout, Ok(layout) => layout,
Err(error) => { Err(error) => {
@@ -212,15 +212,11 @@ impl Ferese {
let viewport_target = workspace.layout.viewport_x(); let viewport_target = workspace.layout.viewport_x();
let viewport_motion = viewport_target.map(|target| { let viewport_motion = viewport_target.map(|target| {
let viewport = self let viewport = self
.viewport_animations .viewports
.entry(workspace_id) .entry(workspace_id)
.or_insert_with(|| AnimatedValue::new(target)); .or_insert_with(|| ViewportPresentation::new(target));
let target_changed = (viewport.target - target).abs() > 0.001; let target_changed = viewport.retarget(target, self.animations_enabled);
viewport.retarget_preserving_motion(target); (viewport.motion().current, target_changed)
if !self.animations_enabled {
viewport.snap();
}
(viewport.current, target_changed)
}); });
let viewport_current = viewport_motion.map(|(current, _)| current); let viewport_current = viewport_motion.map(|(current, _)| current);
let viewport_target_changed = viewport_motion.is_some_and(|(_, changed)| changed); let viewport_target_changed = viewport_motion.is_some_and(|(_, changed)| changed);
@@ -351,9 +347,9 @@ impl Ferese {
let mut world = AnimatedValue::new(restored_world_x); let mut world = AnimatedValue::new(restored_world_x);
world.velocity = geometry.visual.velocity.x world.velocity = geometry.visual.velocity.x
+ self + self
.viewport_animations .viewports
.get(&workspace) .get(&workspace)
.map_or(0.0, |viewport| viewport.velocity); .map_or(0.0, |viewport| viewport.motion().velocity);
world world
}); });
animated_world_x.set_target(world_x); animated_world_x.set_target(world_x);
@@ -361,11 +357,11 @@ impl Ferese {
animated_world_x.snap(); animated_world_x.snap();
} }
if let Some(viewport) = self.viewport_animations.get(&workspace) { if let Some(viewport) = self.viewports.get(&workspace) {
super::animation::sync_scrolling_coordinates( super::animation::sync_scrolling_coordinates(
geometry, geometry,
&mut animated_world_x, &mut animated_world_x,
viewport, viewport.motion(),
geometry.is_zooming(), geometry.is_zooming(),
); );
} }
@@ -557,10 +553,12 @@ impl Ferese {
pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> { pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
let constraints = self.window_constraints(); let constraints = self.window_constraints();
let focused = self.focused_window; let focused = self.focused_window;
self.workspaces self.workspaces.active_mut().layout.resolve_geometry_with_constraints(
.active_mut() bounds,
.layout self.gap_config,
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused) &constraints,
focused,
)
} }
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> { pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
+28 -23
View File
@@ -24,7 +24,7 @@ impl Ferese {
gesture_direction: SwipeDirection, gesture_direction: SwipeDirection,
progress: f64, progress: f64,
) { ) {
if self.focus_swipe.is_none() { if self.focus_swipe().is_none() {
if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() { if self.swipe.preview_started() || !self.animations_enabled || self.overview.is_presenting() {
return; return;
} }
@@ -40,9 +40,9 @@ impl Ferese {
let to = neighbor.unwrap_or(from); let to = neighbor.unwrap_or(from);
let workspace_id = workspace.id; let workspace_id = workspace.id;
let start = self let start = self
.viewport_animations .viewports
.get(&workspace_id) .get(&workspace_id)
.map(|viewport| viewport.current) .map(|viewport| viewport.motion().current)
.or_else(|| workspace.layout.viewport_x()) .or_else(|| workspace.layout.viewport_x())
.unwrap_or(0.0); .unwrap_or(0.0);
let mut candidate = workspace.layout.clone(); let mut candidate = workspace.layout.clone();
@@ -53,12 +53,12 @@ impl Ferese {
return; return;
} }
if candidate if candidate
.geometry_with_constraints(bounds, self.gap_config, &self.window_constraints(), Some(to)) .resolve_geometry_with_constraints(bounds, self.gap_config, &self.window_constraints(), Some(to))
.is_err() .is_err()
{ {
return; return;
} }
self.focus_swipe = Some(FocusSwipe { let swipe = FocusSwipe {
workspace: workspace_id, workspace: workspace_id,
from, from,
to, to,
@@ -76,17 +76,20 @@ impl Ferese {
WorkspaceLayout::Scrolling(layout) => Some(layout), WorkspaceLayout::Scrolling(layout) => Some(layout),
_ => None, _ => None,
}, },
}); };
self.viewports
.entry(workspace_id)
.or_insert_with(|| ViewportPresentation::new(start))
.begin_gesture(swipe);
self.rebuild_presentation_dependencies(); self.rebuild_presentation_dependencies();
self.swipe.mark_preview_started(); self.swipe.mark_preview_started();
} }
if let Some(swipe) = &mut self.focus_swipe { if let Some(workspace) = self.focus_swipe().map(|swipe| swipe.workspace) {
swipe.progress = progress; self.viewports.get_mut(&workspace).unwrap().update_gesture(progress);
} }
if let Some(output) = self if let Some(output) = self
.focus_swipe .focus_swipe()
.as_ref()
.and_then(|swipe| self.output_workspaces.output_for_workspace(swipe.workspace)) .and_then(|swipe| self.output_workspaces.output_for_workspace(swipe.workspace))
.and_then(|id| self.outputs_by_id.get(&id)) .and_then(|id| self.outputs_by_id.get(&id))
.cloned() .cloned()
@@ -96,25 +99,27 @@ impl Ferese {
} }
pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool { pub(crate) fn finish_focus_swipe(&mut self, direction: Option<SwipeDirection>) -> bool {
let Some(swipe) = self.focus_swipe.take() else { let Some(swipe) = self.focus_swipe() else {
return false; return false;
}; };
let current = self.focus_swipe_is_current(&swipe); let current = self.focus_swipe_is_current(swipe);
let blocked = self let blocked = self
.presentation_dependencies .presentation_dependencies
.viewport_blocked(swipe.workspace, &self.windows); .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. // Release from the last input position even if no frame rendered that update.
if current let velocity = (current && !blocked).then(|| {
&& !blocked swipe.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity)
&& let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace) / self.animation_speed
{ });
viewport.current = swipe.position(); let workspace = swipe.workspace;
viewport.velocity = swipe let swipe = self
.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity) .viewports
/ self.animation_speed; .get_mut(&workspace)
} .unwrap()
.end_gesture(velocity)
.unwrap();
self.presentation_dependencies
.restore_viewport(workspace, swipe.dependencies.clone());
let neighbor = self.output_bounds().and_then(|bounds| { let neighbor = self.output_bounds().and_then(|bounds| {
self.workspaces self.workspaces
.active() .active()
+17 -27
View File
@@ -65,10 +65,10 @@ impl Ferese {
&& !self && !self
.presentation_dependencies .presentation_dependencies
.viewport_blocked(*workspace, &self.windows) .viewport_blocked(*workspace, &self.windows)
&& let Some(viewport) = self.viewport_animations.get(workspace) && let Some(viewport) = self.viewports.get(workspace)
{ {
visual.velocity.x = if delta.is_zero() { visual.velocity.x = if delta.is_zero() {
-viewport.velocity -viewport.presented_motion().velocity
} else { } else {
predicted_x_velocity predicted_x_velocity
}; };
@@ -242,14 +242,10 @@ impl Ferese {
return false; return false;
}; };
self.viewport_animations.get(&workspace).is_some_and(|viewport| { self.viewports
let mut viewport = *viewport; .get(&workspace)
let held = self .is_some_and(ViewportPresentation::needs_tick)
.focus_swipe || record.coupled_width.as_ref().is_some_and(|(_, width)| {
.as_ref()
.is_some_and(|swipe| swipe.workspace == workspace);
!held && viewport.advance(Duration::ZERO, self.viewport_spring_config)
}) || record.coupled_width.as_ref().is_some_and(|(_, width)| {
let mut width = *width; let mut width = *width;
width.advance(Duration::ZERO, self.viewport_spring_config) width.advance(Duration::ZERO, self.viewport_spring_config)
}) })
@@ -283,18 +279,16 @@ impl Ferese {
} }
let world = record.world_x.as_ref().and_then(|(workspace, world)| { let world = record.world_x.as_ref().and_then(|(workspace, world)| {
let viewport = self.viewport_animations.get(workspace)?; let viewport = self.viewports.get(workspace)?;
let held = self
.focus_swipe
.as_ref()
.is_some_and(|swipe| swipe.workspace == *workspace);
Some(( Some((
*world, *world,
*viewport, viewport.sample(
held || self delta,
.presentation_dependencies self.viewport_spring_config,
self.animations_enabled,
self.presentation_dependencies
.viewport_blocked(*workspace, &self.windows), .viewport_blocked(*workspace, &self.windows),
),
)) ))
}); });
let width = record.coupled_width.as_ref().map(|(_, width)| *width); let width = record.coupled_width.as_ref().map(|(_, width)| *width);
@@ -335,7 +329,7 @@ fn predict_geometry(
delta: Duration, delta: Duration,
spring: SpringConfig, spring: SpringConfig,
viewport_spring: SpringConfig, viewport_spring: SpringConfig,
world: Option<(AnimatedValue, AnimatedValue, bool)>, world: Option<(AnimatedValue, AnimatedValue)>,
width: Option<AnimatedValue>, width: Option<AnimatedValue>,
reflow_held: bool, reflow_held: bool,
) -> WindowGeometry { ) -> WindowGeometry {
@@ -352,7 +346,7 @@ fn predict_geometry(
predicted.visual.velocity = Default::default(); predicted.visual.velocity = Default::default();
} }
predicted.visual.target.width = original.visual.target.width; predicted.visual.target.width = original.visual.target.width;
if let Some((mut world, mut viewport, held)) = world if let Some((mut world, viewport)) = world
&& !zooming && !zooming
{ {
if !reflow_held { if !reflow_held {
@@ -360,11 +354,6 @@ fn predict_geometry(
} else { } else {
world.velocity = 0.0; world.velocity = 0.0;
} }
if !held {
viewport.advance(delta, viewport_spring);
} else {
viewport.velocity = 0.0;
}
super::animation::sync_scrolling_coordinates(&mut predicted, &mut world, &viewport, false); super::animation::sync_scrolling_coordinates(&mut predicted, &mut world, &viewport, false);
} }
@@ -916,12 +905,13 @@ mod tests {
width.set_target(500.); width.set_target(500.);
let spring = SpringConfig::default(); let spring = SpringConfig::default();
let delta = Duration::from_millis(10); let delta = Duration::from_millis(10);
let sampled_viewport = ViewportPresentation::from_motion(viewport).sample(delta, spring, true, false);
let predicted = predict_geometry( let predicted = predict_geometry(
geometry, geometry,
delta, delta,
spring, spring,
spring, spring,
Some((world, viewport, false)), Some((world, sampled_viewport)),
Some(width), Some(width),
false, false,
); );
@@ -7,19 +7,19 @@ struct ClientCommit {
serial: Serial, serial: Serial,
} }
#[derive(Clone, Copy)] #[derive(Clone, Copy, PartialEq)]
struct ViewportCommit { struct ViewportCommit {
commit: ClientCommit, commit: ClientCommit,
width: (f64, f64), width: (f64, f64),
} }
#[derive(Clone)] #[derive(Clone, PartialEq)]
struct ViewportStep { struct ViewportStep {
target: ferese_layout::ViewportTarget, target: ferese_layout::ViewportTarget,
inputs: Vec<ViewportCommit>, inputs: Vec<ViewportCommit>,
} }
#[derive(Clone, Default)] #[derive(Clone, Default, PartialEq)]
pub(super) struct ViewportDependencies { pub(super) struct ViewportDependencies {
steps: Vec<ViewportStep>, steps: Vec<ViewportStep>,
waits: Vec<ClientCommit>, waits: Vec<ClientCommit>,
@@ -35,10 +35,10 @@ const MAX_VIEWPORT_STEPS: usize = 32;
#[derive(Default)] #[derive(Default)]
pub(super) struct PresentationDependencies { pub(super) struct PresentationDependencies {
reflow: HashMap<WindowId, Vec<ClientCommit>>, reflow: HashMap<WindowId, Vec<ClientCommit>>,
// Provenance and configure serials only; ViewportPresentation owns holds.
viewport: HashMap<WorkspaceId, ViewportDependencies>, viewport: HashMap<WorkspaceId, ViewportDependencies>,
// A release tick consumes waiting wall time without stepping held springs. // A reflow release tick consumes waiting wall time without stepping springs.
paused_reflow: HashSet<WindowId>, paused_reflow: HashSet<WindowId>,
paused_viewport: HashSet<WorkspaceId>,
} }
impl PresentationDependencies { impl PresentationDependencies {
@@ -66,7 +66,7 @@ impl PresentationDependencies {
} }
pub(super) fn needs_tick(&self) -> bool { pub(super) fn needs_tick(&self) -> bool {
!self.paused_reflow.is_empty() || !self.paused_viewport.is_empty() !self.paused_reflow.is_empty()
} }
fn pending<W: std::hash::Hash + Eq + Clone>( fn pending<W: std::hash::Hash + Eq + Clone>(
@@ -101,23 +101,15 @@ impl PresentationDependencies {
pub(super) fn holds<W: std::hash::Hash + Eq + Clone>( pub(super) fn holds<W: std::hash::Hash + Eq + Clone>(
&mut self, &mut self,
windows: &window_registry::WindowRegistry<W>, windows: &window_registry::WindowRegistry<W>,
) -> (HashSet<WindowId>, HashSet<WorkspaceId>) { ) -> HashSet<WindowId> {
let reflow = self let reflow = self
.reflow .reflow
.keys() .keys()
.copied() .copied()
.filter(|id| self.reflow_blocked(*id, windows)) .filter(|id| self.reflow_blocked(*id, windows))
.collect::<HashSet<_>>(); .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_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_reflow.extend(reflow);
held_viewport.extend(viewport);
self.reflow.retain(|_, waits| { self.reflow.retain(|_, waits| {
waits.retain(|wait| Self::pending(*wait, windows)); waits.retain(|wait| Self::pending(*wait, windows));
!waits.is_empty() !waits.is_empty()
@@ -132,7 +124,7 @@ impl PresentationDependencies {
} }
!waits.steps.is_empty() || !waits.waits.is_empty() !waits.steps.is_empty() || !waits.waits.is_empty()
}); });
(held_reflow, held_viewport) held_reflow
} }
} }
@@ -264,13 +256,15 @@ fn viewport_dependencies(
impl Ferese { impl Ferese {
pub(super) fn release_presentation_dependencies(&mut self) { pub(super) fn release_presentation_dependencies(&mut self) {
let paused_reflow = self.presentation_dependencies.paused_reflow.clone(); let paused_reflow = self.presentation_dependencies.paused_reflow.clone();
let paused_viewport = self.presentation_dependencies.paused_viewport.clone(); self.rebuild_presentation_dependencies_with_release(true);
self.rebuild_presentation_dependencies();
self.presentation_dependencies.paused_reflow.extend(paused_reflow); self.presentation_dependencies.paused_reflow.extend(paused_reflow);
self.presentation_dependencies.paused_viewport.extend(paused_viewport);
} }
pub(super) fn rebuild_presentation_dependencies(&mut self) { pub(super) fn rebuild_presentation_dependencies(&mut self) {
self.rebuild_presentation_dependencies_with_release(false);
}
fn rebuild_presentation_dependencies_with_release(&mut self, release_tick: bool) {
let mut reflow = HashMap::new(); let mut reflow = HashMap::new();
let mut viewport = HashMap::new(); let mut viewport = HashMap::new();
let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport); let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport);
@@ -326,8 +320,7 @@ impl Ferese {
} }
for workspace in self.workspaces.iter() { for workspace in self.workspaces.iter() {
let layout = self let layout = self
.focus_swipe .focus_swipe()
.as_ref()
.filter(|swipe| swipe.workspace == workspace.id) .filter(|swipe| swipe.workspace == workspace.id)
.and_then(|swipe| swipe.layout.as_ref()) .and_then(|swipe| swipe.layout.as_ref())
.or(match &workspace.layout { .or(match &workspace.layout {
@@ -356,11 +349,15 @@ impl Ferese {
self.presentation_dependencies self.presentation_dependencies
.paused_reflow .paused_reflow
.retain(|id| reflow.contains_key(id)); .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.reflow = reflow;
self.presentation_dependencies.viewport = viewport; self.presentation_dependencies.viewport = viewport;
for (workspace, presentation) in &mut self.viewports {
presentation.reconcile_hold(
self.presentation_dependencies
.viewport_blocked(*workspace, &self.windows),
release_tick,
);
}
} }
} }
@@ -389,7 +386,7 @@ mod tests {
layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap(); layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap();
layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap(); layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(b)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b))
.unwrap(); .unwrap();
let pending = [( let pending = [(
a, a,
@@ -399,7 +396,7 @@ mod tests {
assert_eq!(first.waits.len(), 1); assert_eq!(first.waits.len(), 1);
layout.slide_focus_from(b, a).unwrap(); layout.slide_focus_from(b, a).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(a)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(a))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 50.0); assert_eq!(layout.viewport_x(), 50.0);
let canceled = viewport_dependencies(&layout, &pending, first); let canceled = viewport_dependencies(&layout, &pending, first);
@@ -413,7 +410,7 @@ mod tests {
layout.insert(b, Some(a)).unwrap(); layout.insert(b, Some(a)).unwrap();
layout.insert(c, Some(b)).unwrap(); layout.insert(c, Some(b)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(c)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(c))
.unwrap(); .unwrap();
let pending = [ let pending = [
( (
@@ -438,7 +435,7 @@ mod tests {
layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap(); layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap();
layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap(); layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(b)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b))
.unwrap(); .unwrap();
let pending = [( let pending = [(
a, a,
@@ -449,7 +446,7 @@ mod tests {
let (from, to) = if index % 2 == 0 { (b, a) } else { (a, b) }; let (from, to) = if index % 2 == 0 { (b, a) } else { (a, b) };
layout.slide_focus_from(from, to).unwrap(); layout.slide_focus_from(from, to).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(to)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(to))
.unwrap(); .unwrap();
graph = viewport_dependencies(&layout, &pending, graph); graph = viewport_dependencies(&layout, &pending, graph);
assert!(graph.steps.len() <= MAX_VIEWPORT_STEPS); assert!(graph.steps.len() <= MAX_VIEWPORT_STEPS);
@@ -496,16 +493,15 @@ mod tests {
assert!(windows.transaction(&a).is_none()); assert!(windows.transaction(&a).is_none());
assert!(windows.transaction(&b).is_some()); assert!(windows.transaction(&b).is_some());
assert!(dependencies.reflow_blocked(neighbor, &windows)); assert!(dependencies.reflow_blocked(neighbor, &windows));
let (held, _) = dependencies.holds(&windows); let held = dependencies.holds(&windows);
assert!(held.contains(&a), "release tick must not charge waiting time"); assert!(held.contains(&a), "release tick must not charge waiting time");
let (held, _) = dependencies.holds(&windows); let held = dependencies.holds(&windows);
assert!(!held.contains(&a)); assert!(!held.contains(&a));
// Unmap removes the serial owner; both properties retire on the tick. // Unmap removes the serial owner; both properties retire on the tick.
windows.remove(&"b"); windows.remove(&"b");
assert!(!dependencies.viewport_blocked(workspace, &windows)); assert!(!dependencies.viewport_blocked(workspace, &windows));
let (_, held) = dependencies.holds(&windows); dependencies.holds(&windows);
assert!(held.contains(&workspace)); dependencies.holds(&windows);
assert!(dependencies.holds(&windows).1.is_empty());
assert!(!dependencies.needs_tick()); assert!(!dependencies.needs_tick());
assert!(dependencies.reflow.is_empty() && dependencies.viewport.is_empty()); assert!(dependencies.reflow.is_empty() && dependencies.viewport.is_empty());
} }
@@ -539,7 +535,7 @@ mod tests {
layout.focus(ids[4]).unwrap(); layout.focus(ids[4]).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(ids[4])) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(ids[4]))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 160.0); assert_eq!(layout.viewport_x(), 160.0);
let widths = [ let widths = [
@@ -607,6 +603,31 @@ mod tests {
if remaining.is_empty() { if remaining.is_empty() {
assert!(partial.steps.is_empty() && partial.waits.is_empty()); assert!(partial.steps.is_empty() && partial.waits.is_empty());
} }
// Feed each precise partial-commit result through the presentation
// owner. Remaining unrelated configures must not keep it held.
let mut windows = window_registry::WindowRegistry::default();
for id in ids {
windows.register(id, id);
}
for (id, transaction) in &remaining {
windows.set_transaction(*id, *transaction);
}
let workspace = WorkspaceId(1);
let mut dependencies = PresentationDependencies::default();
dependencies.viewport.insert(workspace, partial);
let blocked = dependencies.viewport_blocked(workspace, &windows);
let mut viewport = ViewportPresentation::new(0.0);
viewport.retarget(layout.viewport_x(), true);
viewport.reconcile_hold(true, false);
viewport.reconcile_hold(blocked, true);
viewport.advance(Duration::from_secs(3), SpringConfig::default(), true, blocked);
assert_eq!(viewport.motion().current, 0.0, "release consumed waiting time: {mask}");
viewport.advance(Duration::from_millis(16), SpringConfig::default(), true, blocked);
assert_eq!(
viewport.motion().current == 0.0,
blocked,
"incorrect held owner: {mask}"
);
} }
} }
} }
+4 -2
View File
@@ -471,10 +471,12 @@ mod tests {
let c_workspace = state.output_workspaces.active_workspace(c_id).unwrap(); let c_workspace = state.output_workspaces.active_workspace(c_id).unwrap();
let mut viewport = AnimatedValue::new(0.0); let mut viewport = AnimatedValue::new(0.0);
viewport.set_target(200.0); viewport.set_target(200.0);
state.viewport_animations.insert(c_workspace, viewport); state
.viewports
.insert(c_workspace, ViewportPresentation::from_motion(viewport));
let now = state.start_time.elapsed(); let now = state.start_time.elapsed();
state.advance_animations_at(Duration::from_millis(16), now + Duration::from_millis(16)); state.advance_animations_at(Duration::from_millis(16), now + Duration::from_millis(16));
assert!(state.viewport_animations[&c_workspace].current > 0.0); assert!(state.viewports[&c_workspace].motion().current > 0.0);
assert!(state.windows.transaction(&slow_id).is_some()); assert!(state.windows.transaction(&slow_id).is_some());
state.advance_animations_at(Duration::from_millis(334), now + Duration::from_millis(350)); state.advance_animations_at(Duration::from_millis(334), now + Duration::from_millis(350));
assert!(state.windows.transaction(&slow_id).is_none()); assert!(state.windows.transaction(&slow_id).is_none());
+428
View File
@@ -0,0 +1,428 @@
use super::*;
#[derive(Clone, PartialEq)]
enum ViewportOwner {
Spring,
Gesture(Box<FocusSwipe>),
}
#[derive(Clone, PartialEq)]
enum ViewportMode {
Running(ViewportOwner),
// The paused owner keeps its position and velocity. Releasing consumes one
// tick before that same owner resumes, excluding time spent waiting.
Held(ViewportOwner),
// The release frame excludes held wall time and presents zero velocity.
Resuming(ViewportOwner),
}
#[derive(Clone, PartialEq)]
pub(super) struct ViewportPresentation {
motion: AnimatedValue,
mode: ViewportMode,
}
impl ViewportPresentation {
pub(super) fn new(target: f64) -> Self {
Self {
motion: AnimatedValue::new(target),
mode: ViewportMode::Running(ViewportOwner::Spring),
}
}
#[cfg(test)]
pub(super) fn from_motion(motion: AnimatedValue) -> Self {
Self {
motion,
..Self::new(motion.target)
}
}
pub(super) fn motion(&self) -> &AnimatedValue {
&self.motion
}
pub(super) fn retarget(&mut self, target: f64, animations: bool) -> bool {
let changed = (self.motion.target - target).abs() > 0.001;
self.motion.retarget_preserving_motion(target);
if !animations {
self.motion.snap();
}
changed
}
pub(super) fn gesture(&self) -> Option<&FocusSwipe> {
match &self.mode {
ViewportMode::Running(ViewportOwner::Gesture(swipe))
| ViewportMode::Held(ViewportOwner::Gesture(swipe))
| ViewportMode::Resuming(ViewportOwner::Gesture(swipe)) => Some(swipe),
_ => None,
}
}
pub(super) fn update_gesture(&mut self, progress: f64) {
match &mut self.mode {
ViewportMode::Running(ViewportOwner::Gesture(swipe))
| ViewportMode::Held(ViewportOwner::Gesture(swipe))
| ViewportMode::Resuming(ViewportOwner::Gesture(swipe)) => swipe.progress = progress,
_ => {}
}
}
pub(super) fn begin_gesture(&mut self, swipe: FocusSwipe) {
debug_assert!(self.gesture().is_none());
let owner = ViewportOwner::Gesture(Box::new(swipe));
self.mode = if self.is_held() {
ViewportMode::Held(owner)
} else {
ViewportMode::Running(owner)
};
}
pub(super) fn end_gesture(&mut self, release_velocity: Option<f64>) -> Option<FocusSwipe> {
self.gesture()?;
let mode = std::mem::replace(&mut self.mode, ViewportMode::Running(ViewportOwner::Spring));
let (swipe, held) = match mode {
ViewportMode::Running(ViewportOwner::Gesture(swipe)) => (swipe, false),
ViewportMode::Held(ViewportOwner::Gesture(swipe)) => (swipe, true),
ViewportMode::Resuming(ViewportOwner::Gesture(swipe)) => (swipe, false),
_ => unreachable!(),
};
if let Some(velocity) = release_velocity {
self.motion.current = swipe.position();
self.motion.velocity = velocity;
}
if held {
self.mode = ViewportMode::Held(ViewportOwner::Spring);
}
Some(*swipe)
}
pub(super) fn is_held(&self) -> bool {
matches!(self.mode, ViewportMode::Held(_))
}
// Commits/deadlines retain a release tick. An independent retarget can
// discard the old dependency hold immediately instead.
pub(super) fn reconcile_hold(&mut self, blocked: bool, release_tick: bool) {
let mode = std::mem::replace(&mut self.mode, ViewportMode::Running(ViewportOwner::Spring));
self.mode = match mode {
ViewportMode::Running(owner) | ViewportMode::Resuming(owner) if blocked => ViewportMode::Held(owner),
ViewportMode::Held(owner) if !blocked && !release_tick => ViewportMode::Running(owner),
mode => mode,
};
}
pub(super) fn presented_motion(&self) -> AnimatedValue {
let mut motion = self.motion;
if matches!(self.mode, ViewportMode::Held(_) | ViewportMode::Resuming(_)) {
motion.velocity = 0.0;
}
motion
}
pub(super) fn needs_tick(&self) -> bool {
self.is_held()
|| self.gesture().is_some()
|| self.motion.current != self.motion.target
|| self.motion.velocity != 0.0
}
#[cfg(feature = "resize-metrics")]
pub(super) fn spring_is_moving(&self) -> bool {
self.gesture().is_none() && (self.motion.current != self.motion.target || self.motion.velocity != 0.0)
}
// Prediction and authoritative ticks evaluate the same motion step. The
// sampler borrows gesture/dependency metadata instead of cloning layouts.
fn step_motion(
&self,
delta: Duration,
spring: SpringConfig,
animations: bool,
blocked: bool,
) -> (AnimatedValue, bool) {
let mut motion = self.motion;
if blocked || self.is_held() {
return (motion, blocked || self.needs_tick());
}
let active = match &self.mode {
ViewportMode::Running(ViewportOwner::Gesture(swipe))
| ViewportMode::Resuming(ViewportOwner::Gesture(swipe)) => {
motion.current = swipe.position();
motion.velocity = 0.0;
true
}
ViewportMode::Running(ViewportOwner::Spring) | ViewportMode::Resuming(ViewportOwner::Spring)
if animations =>
{
motion.advance(delta, spring)
}
ViewportMode::Running(ViewportOwner::Spring) | ViewportMode::Resuming(ViewportOwner::Spring) => {
motion.snap();
false
}
ViewportMode::Held(_) => unreachable!(),
};
(motion, active)
}
pub(super) fn advance(&mut self, delta: Duration, spring: SpringConfig, animations: bool, blocked: bool) -> bool {
let (motion, active) = self.step_motion(delta, spring, animations, blocked);
self.motion = motion;
let mode = std::mem::replace(&mut self.mode, ViewportMode::Running(ViewportOwner::Spring));
self.mode = match mode {
ViewportMode::Running(owner) | ViewportMode::Held(owner) | ViewportMode::Resuming(owner) if blocked => {
ViewportMode::Held(owner)
}
ViewportMode::Held(owner) => ViewportMode::Resuming(owner),
ViewportMode::Running(owner) | ViewportMode::Resuming(owner) => ViewportMode::Running(owner),
};
active
}
pub(super) fn sample(
&self,
delta: Duration,
spring: SpringConfig,
animations: bool,
blocked: bool,
) -> AnimatedValue {
let (mut motion, _) = self.step_motion(delta, spring, animations, blocked);
if blocked || self.is_held() {
motion.velocity = 0.0;
}
motion
}
}
impl Ferese {
pub(super) fn focus_swipe(&self) -> Option<&FocusSwipe> {
self.viewports.values().find_map(ViewportPresentation::gesture)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TICK: Duration = Duration::from_millis(16);
fn moving() -> ViewportPresentation {
let mut viewport = ViewportPresentation::new(0.0);
viewport.retarget(500.0, true);
viewport.advance(TICK, SpringConfig::default(), true, false);
viewport
}
fn swipe(viewport: &ViewportPresentation) -> FocusSwipe {
FocusSwipe {
workspace: WorkspaceId(1),
from: WindowId(1),
to: WindowId(2),
direction: Direction::Right,
gesture_direction: SwipeDirection::Left,
start: viewport.motion.current,
destination: 700.0,
progress: 0.0,
layout: None,
dependencies: None,
}
}
#[test]
fn focus_retarget_preserves_moving_spring_position_and_velocity() {
let mut viewport = moving();
let before = viewport.motion;
viewport.retarget(-200.0, true);
assert_eq!(viewport.motion.current, before.current);
assert_eq!(viewport.motion.velocity, before.velocity);
assert_eq!(viewport.motion.target, -200.0);
let mut expected = before;
expected.retarget_preserving_motion(-200.0);
expected.advance(TICK, SpringConfig::default());
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_eq!(viewport.motion, expected);
}
#[test]
fn gesture_interrupts_spring_and_releases_from_latest_input() {
let mut viewport = moving();
let before = viewport.motion.current;
viewport.begin_gesture(swipe(&viewport));
viewport.advance(Duration::from_secs(5), SpringConfig::default(), true, false);
assert_eq!(viewport.motion.current, before);
assert_eq!(viewport.motion.velocity, 0.0);
viewport.update_gesture(0.5);
// No frame is necessary between the last input and release.
let position = viewport.gesture().unwrap().position();
let velocity = viewport.gesture().unwrap().release_velocity(0.4, 99.0);
viewport.end_gesture(Some(velocity)).unwrap();
viewport.retarget(700.0, true);
assert_eq!(viewport.motion.current, position);
assert_eq!(viewport.motion.velocity, velocity);
assert!(matches!(viewport.mode, ViewportMode::Running(ViewportOwner::Spring)));
let mut expected = viewport.motion;
expected.advance(TICK, SpringConfig::default());
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_eq!(viewport.motion, expected);
}
#[test]
fn edge_gesture_keeps_rubber_band_and_release_derivative() {
let mut viewport = moving();
let mut gesture = swipe(&viewport);
gesture.to = gesture.from;
gesture.destination = gesture.start;
gesture.progress = 2.0;
let (position, derivative) = ferese_animation::gesture::rubber_band(2.0, 1.0, 0.55);
let expected = gesture.start + 64.0 * position;
assert_eq!(gesture.release_velocity(99.0, 0.4), 64.0 * 0.4 * derivative);
viewport.begin_gesture(gesture);
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_eq!(viewport.motion.current, expected);
}
#[test]
fn hold_release_retains_motion_and_excludes_waiting_wall_time() {
let mut viewport = moving();
let frozen = viewport.motion;
viewport.reconcile_hold(true, false);
viewport.advance(Duration::from_secs(3), SpringConfig::default(), true, true);
assert_eq!(viewport.motion, frozen);
assert_eq!(viewport.presented_motion().velocity, 0.0);
viewport.reconcile_hold(false, true);
viewport.advance(Duration::from_secs(2), SpringConfig::default(), true, false);
assert_eq!(viewport.motion, frozen);
assert_eq!(viewport.presented_motion().velocity, 0.0);
let mut expected = frozen;
expected.advance(TICK, SpringConfig::default());
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_eq!(viewport.motion, expected);
assert_eq!(viewport.presented_motion(), expected);
}
#[test]
fn held_retarget_preserves_motion_until_its_dependencies_clear() {
let mut viewport = moving();
viewport.reconcile_hold(true, false);
let frozen = viewport.motion;
viewport.retarget(900.0, true);
viewport.reconcile_hold(true, false);
viewport.advance(TICK, SpringConfig::default(), true, true);
assert_eq!(viewport.motion.current, frozen.current);
assert_eq!(viewport.motion.velocity, frozen.velocity);
assert_eq!(viewport.motion.target, 900.0);
// An independent policy request retires the hold immediately, even if
// other widths on this workspace are still awaiting client commits.
viewport.retarget(-100.0, true);
viewport.reconcile_hold(false, false);
assert!(!viewport.is_held());
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_ne!(viewport.motion.current, frozen.current);
}
#[test]
fn hold_pauses_gesture_owner_and_release_resumes_that_gesture() {
let mut viewport = moving();
let frozen = viewport.motion;
viewport.reconcile_hold(true, false);
viewport.begin_gesture(swipe(&viewport));
viewport.update_gesture(0.75);
let position = viewport.gesture().unwrap().position();
viewport.advance(TICK, SpringConfig::default(), true, true);
assert_eq!(viewport.motion, frozen);
viewport.advance(Duration::from_secs(3), SpringConfig::default(), true, false);
assert_eq!(viewport.motion, frozen);
viewport.advance(TICK, SpringConfig::default(), true, false);
assert_eq!(viewport.motion.current, position);
assert_eq!(viewport.motion.velocity, 0.0);
assert!(viewport.gesture().is_some());
}
#[test]
fn blocked_gesture_release_does_not_publish_uncommitted_position() {
let mut viewport = moving();
let frozen = viewport.motion;
viewport.begin_gesture(swipe(&viewport));
viewport.update_gesture(0.8);
viewport.reconcile_hold(true, false);
viewport.end_gesture(None).unwrap();
viewport.retarget(700.0, true);
assert!(matches!(viewport.mode, ViewportMode::Held(ViewportOwner::Spring)));
assert_eq!(viewport.motion.current, frozen.current);
assert_eq!(viewport.motion.velocity, frozen.velocity);
}
#[test]
fn prediction_steps_a_copy_of_every_owner_and_hold_phase() {
let running = moving();
let mut gesture = running.clone();
gesture.begin_gesture(swipe(&gesture));
gesture.update_gesture(0.3);
let mut held_spring = running.clone();
held_spring.reconcile_hold(true, false);
let mut held_gesture = gesture.clone();
held_gesture.reconcile_hold(true, false);
let mut resuming = held_spring.clone();
resuming.advance(TICK, SpringConfig::default(), true, false);
for state in [running, gesture, held_spring, held_gesture, resuming] {
for blocked in [false, true] {
for horizon in [Duration::ZERO, TICK, Duration::from_millis(40)] {
let original = state.clone();
let sample = state.sample(horizon, SpringConfig::default(), true, blocked);
let mut advanced = state.clone();
advanced.advance(horizon, SpringConfig::default(), true, blocked);
assert_eq!(sample, advanced.presented_motion());
assert!(state == original, "prediction changed authoritative mode or motion");
}
}
}
}
#[test]
fn layout_handoff_preserves_screen_position_for_each_viewport_owner() {
let spring = moving();
let mut gesture = spring.clone();
gesture.begin_gesture(swipe(&gesture));
gesture.update_gesture(0.6);
gesture.advance(TICK, SpringConfig::default(), true, false);
let mut held = spring.clone();
held.reconcile_hold(true, false);
for viewport in [spring, gesture, held] {
let mut geometry = WindowGeometry::new(Rect::new(123.0, 0.0, 400.0, 300.0), None);
geometry.visual.velocity.x = 42.0;
let before = geometry.visual;
let motion = viewport.presented_motion();
let mut world = AnimatedValue::new(restored_scrolling_world_x(
true,
before.current.x,
motion.current,
900.0,
));
world.velocity = before.velocity.x + motion.velocity;
world.set_target(geometry.logical.x + motion.target);
super::super::animation::sync_scrolling_coordinates(&mut geometry, &mut world, &motion, false);
assert!((geometry.visual.current.x - before.current.x).abs() < 1e-6);
assert!((geometry.visual.velocity.x - before.velocity.x).abs() < 1e-6);
assert_eq!(geometry.logical.x, world.target - motion.target);
}
}
#[test]
fn reduced_motion_snaps_policy_target_after_hold_release() {
let mut viewport = moving();
viewport.reconcile_hold(true, false);
let frozen = viewport.motion;
viewport.advance(TICK, SpringConfig::default(), false, true);
assert_eq!(viewport.motion, frozen);
viewport.advance(TICK, SpringConfig::default(), false, false);
assert_eq!(viewport.motion, frozen);
viewport.advance(TICK, SpringConfig::default(), false, false);
assert_eq!(viewport.motion.current, viewport.motion.target);
assert_eq!(viewport.motion.velocity, 0.0);
assert!(!viewport.needs_tick());
viewport.retarget(300.0, false);
assert_eq!(viewport.motion.current, 300.0);
}
}
+9 -9
View File
@@ -137,14 +137,14 @@ impl WorkspaceLayout {
Self::Scrolling(layout) => { Self::Scrolling(layout) => {
let mut layout = layout.clone(); let mut layout = layout.clone();
layout layout
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None) .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), None)
.map(|result| result.geometry) .map(|result| result.geometry)
} }
Self::Tree(layout) => layout.geometry(bounds), Self::Tree(layout) => layout.geometry(bounds),
} }
} }
pub fn geometry_with_constraints( pub fn resolve_geometry_with_constraints(
&mut self, &mut self,
bounds: Rect, bounds: Rect,
gaps: GapConfig, gaps: GapConfig,
@@ -152,7 +152,7 @@ impl WorkspaceLayout {
focused: Option<WindowId>, focused: Option<WindowId>,
) -> Result<LayoutResult, LayoutError> { ) -> Result<LayoutResult, LayoutError> {
match self { match self {
Self::Scrolling(layout) => layout.geometry_with_constraints( Self::Scrolling(layout) => layout.resolve_geometry_with_constraints(
bounds, bounds,
gaps, gaps,
constraints, constraints,
@@ -1121,7 +1121,7 @@ mod tests {
let first = workspaces let first = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints( .resolve_geometry_with_constraints(
Rect::new(0.0, 0.0, 1_000.0, 800.0), Rect::new(0.0, 0.0, 1_000.0, 800.0),
GapConfig::default(), GapConfig::default(),
&HashMap::new(), &HashMap::new(),
@@ -1277,7 +1277,7 @@ mod tests {
let before = workspaces let before = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(2)))
.unwrap(); .unwrap();
let scroll = workspaces.active().layout.viewport_x(); let scroll = workspaces.active().layout.viewport_x();
// Rapid right/middle/left hover must never scroll or skip a focus target, // Rapid right/middle/left hover must never scroll or skip a focus target,
@@ -1289,7 +1289,7 @@ mod tests {
let after = workspaces let after = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(id)) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(id))
.unwrap(); .unwrap();
assert_eq!(before.geometry, after.geometry, "{strategy:?}"); assert_eq!(before.geometry, after.geometry, "{strategy:?}");
assert_eq!(scroll, workspaces.active().layout.viewport_x()); assert_eq!(scroll, workspaces.active().layout.viewport_x());
@@ -1299,7 +1299,7 @@ mod tests {
let clicked = workspaces let clicked = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(3))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(WindowId(3)))
.unwrap(); .unwrap();
let rect = clicked.geometry[&WindowId(3)]; let rect = clicked.geometry[&WindowId(3)];
assert!(rect.x >= 0.0 && rect.x + rect.width <= bounds.width); assert!(rect.x >= 0.0 && rect.x + rect.width <= bounds.width);
@@ -1321,7 +1321,7 @@ mod tests {
let before = workspaces let before = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4))) .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(4)))
.unwrap(); .unwrap();
let scroll = workspaces.active().layout.viewport_x(); let scroll = workspaces.active().layout.viewport_x();
workspaces workspaces
@@ -1335,7 +1335,7 @@ mod tests {
let after = workspaces let after = workspaces
.active_mut() .active_mut()
.layout .layout
.geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5))) .resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(WindowId(5)))
.unwrap(); .unwrap();
assert_eq!(before.geometry, after.geometry); assert_eq!(before.geometry, after.geometry);
assert_eq!(scroll, workspaces.active().layout.viewport_x()); assert_eq!(scroll, workspaces.active().layout.viewport_x());
+212 -84
View File
@@ -149,6 +149,20 @@ impl ViewportTarget {
} }
} }
#[derive(Clone, Copy, Debug, PartialEq)]
enum PendingFocusRequest {
Reveal(WindowId),
Slide { from: WindowId, to: WindowId },
}
#[derive(Clone, Copy, Debug, PartialEq)]
enum PendingViewportRequest {
Focus(PendingFocusRequest),
// Width cycling takes precedence. Keep the focus request only in case a
// subsequent no-op cycle cancels the width request before layout resolves.
WidthCycle(Option<PendingFocusRequest>),
}
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ScrollingLayout { pub struct ScrollingLayout {
columns: Vec<Column>, columns: Vec<Column>,
@@ -156,9 +170,7 @@ pub struct ScrollingLayout {
viewport_x: f64, viewport_x: f64,
default_width: ColumnWidth, default_width: ColumnWidth,
focus_strategy: ViewportFocusStrategy, focus_strategy: ViewportFocusStrategy,
reveal_pending: Option<WindowId>, pending_viewport: Option<PendingViewportRequest>,
swipe_focus_pending: Option<(WindowId, WindowId)>,
width_cycle_pending: bool,
last_viewport_width: Option<f64>, last_viewport_width: Option<f64>,
allocated_widths: HashMap<WindowId, f64>, allocated_widths: HashMap<WindowId, f64>,
viewport_basis: Option<ViewportTarget>, viewport_basis: Option<ViewportTarget>,
@@ -172,9 +184,7 @@ impl Default for ScrollingLayout {
viewport_x: 0.0, viewport_x: 0.0,
default_width: ColumnWidth::default(), default_width: ColumnWidth::default(),
focus_strategy: ViewportFocusStrategy::Minimal, focus_strategy: ViewportFocusStrategy::Minimal,
reveal_pending: None, pending_viewport: None,
swipe_focus_pending: None,
width_cycle_pending: false,
last_viewport_width: None, last_viewport_width: None,
allocated_widths: HashMap::new(), allocated_widths: HashMap::new(),
viewport_basis: None, viewport_basis: None,
@@ -183,6 +193,28 @@ impl Default for ScrollingLayout {
} }
impl ScrollingLayout { impl ScrollingLayout {
fn pending_focus(&self) -> Option<PendingFocusRequest> {
match self.pending_viewport {
Some(PendingViewportRequest::Focus(request)) => Some(request),
Some(PendingViewportRequest::WidthCycle(request)) => request,
None => None,
}
}
fn request_focus(&mut self, request: Option<PendingFocusRequest>) {
self.pending_viewport = if matches!(self.pending_viewport, Some(PendingViewportRequest::WidthCycle(_))) {
Some(PendingViewportRequest::WidthCycle(request))
} else {
request.map(PendingViewportRequest::Focus)
};
}
fn request_reveal(&mut self, window: Option<WindowId>) {
if !matches!(self.pending_focus(), Some(PendingFocusRequest::Slide { .. })) {
self.request_focus(window.map(PendingFocusRequest::Reveal));
}
}
pub fn set_default_width(&mut self, width: ColumnWidth) { pub fn set_default_width(&mut self, width: ColumnWidth) {
self.default_width = normalized_width(width); self.default_width = normalized_width(width);
} }
@@ -196,8 +228,7 @@ impl ScrollingLayout {
pub fn set_focus_strategy(&mut self, strategy: ViewportFocusStrategy) { pub fn set_focus_strategy(&mut self, strategy: ViewportFocusStrategy) {
self.focus_strategy = strategy; self.focus_strategy = strategy;
self.reveal_pending = self.active_window(); self.request_focus(self.active_window().map(PendingFocusRequest::Reveal));
self.swipe_focus_pending = None;
} }
pub fn default_width(&self) -> ColumnWidth { pub fn default_width(&self) -> ColumnWidth {
@@ -282,7 +313,7 @@ impl ScrollingLayout {
.unwrap_or(self.columns.len()); .unwrap_or(self.columns.len());
self.columns.insert(index, Column::new(window, self.default_width)); self.columns.insert(index, Column::new(window, self.default_width));
self.active_column = Some(index); self.active_column = Some(index);
self.reveal_pending = (self.columns.len() > 1).then_some(window); self.request_reveal((self.columns.len() > 1).then_some(window));
debug_assert!(self.validate().is_ok()); debug_assert!(self.validate().is_ok());
Ok(()) Ok(())
@@ -307,11 +338,11 @@ impl ScrollingLayout {
column.active = column.active.min(column.windows.len() - 1); column.active = column.active.min(column.windows.len() - 1);
column.normalize_heights(); column.normalize_heights();
} }
if self.reveal_pending == Some(window) if self.pending_focus() == Some(PendingFocusRequest::Reveal(window))
|| removed_active_column || removed_active_column
|| self.focus_strategy == ViewportFocusStrategy::Paged || self.focus_strategy == ViewportFocusStrategy::Paged
{ {
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
} }
debug_assert!(self.validate().is_ok()); debug_assert!(self.validate().is_ok());
@@ -324,7 +355,7 @@ impl ScrollingLayout {
self.active_column = Some(column); self.active_column = Some(column);
self.columns[column].active = index; self.columns[column].active = index;
if changed_column { if changed_column {
self.reveal_pending = Some(window); self.request_reveal(Some(window));
} }
Ok(()) Ok(())
} }
@@ -341,8 +372,7 @@ impl ScrollingLayout {
/// An explicit selection must reveal even a window already focused by hover. /// An explicit selection must reveal even a window already focused by hover.
pub fn focus_and_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> { pub fn focus_and_reveal(&mut self, window: WindowId) -> Result<(), LayoutError> {
self.focus_without_reveal(window)?; self.focus_without_reveal(window)?;
self.reveal_pending = Some(window); self.request_focus(Some(PendingFocusRequest::Reveal(window)));
self.swipe_focus_pending = None;
Ok(()) Ok(())
} }
@@ -353,8 +383,10 @@ impl ScrollingLayout {
return self.focus_and_reveal(focused); return self.focus_and_reveal(focused);
} }
self.focus_without_reveal(focused)?; self.focus_without_reveal(focused)?;
self.reveal_pending = None; self.request_focus(Some(PendingFocusRequest::Slide {
self.swipe_focus_pending = Some((previous, focused)); from: previous,
to: focused,
}));
Ok(()) Ok(())
} }
@@ -374,7 +406,7 @@ impl ScrollingLayout {
column.active = insertion; column.active = insertion;
column.normalize_heights(); column.normalize_heights();
self.active_column = Some(target_column); self.active_column = Some(target_column);
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
debug_assert!(self.validate().is_ok()); debug_assert!(self.validate().is_ok());
Ok(()) Ok(())
@@ -391,7 +423,7 @@ impl ScrollingLayout {
let insertion = (source + 1).min(self.columns.len()); let insertion = (source + 1).min(self.columns.len());
self.columns.insert(insertion, Column::new(window, self.default_width)); self.columns.insert(insertion, Column::new(window, self.default_width));
self.active_column = Some(insertion); self.active_column = Some(insertion);
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
debug_assert!(self.validate().is_ok()); debug_assert!(self.validate().is_ok());
Ok(()) Ok(())
@@ -400,7 +432,7 @@ impl ScrollingLayout {
pub fn set_column_width(&mut self, window: WindowId, width: ColumnWidth) -> Result<(), LayoutError> { pub fn set_column_width(&mut self, window: WindowId, width: ColumnWidth) -> Result<(), LayoutError> {
let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?; let (column, _) = self.window_location(window).ok_or(LayoutError::UnknownWindow(window))?;
self.columns[column].width = normalized_width(width); self.columns[column].width = normalized_width(width);
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
Ok(()) Ok(())
} }
@@ -418,7 +450,12 @@ impl ScrollingLayout {
.map(|index| presets[(index + 1) % presets.len()]) .map(|index| presets[(index + 1) % presets.len()])
.unwrap_or(presets[0]); .unwrap_or(presets[0]);
self.columns[column].width = next; self.columns[column].width = next;
self.width_cycle_pending = next != current; let focus = self.pending_focus();
self.pending_viewport = if next != current {
Some(PendingViewportRequest::WidthCycle(focus))
} else {
focus.map(PendingViewportRequest::Focus)
};
Ok(next != current) Ok(next != current)
} }
@@ -430,7 +467,7 @@ impl ScrollingLayout {
constraints: &HashMap<WindowId, SizeConstraints>, constraints: &HashMap<WindowId, SizeConstraints>,
) -> Result<bool, LayoutError> { ) -> Result<bool, LayoutError> {
let before = self.viewport_x; let before = self.viewport_x;
let result = self.geometry_with_constraints(bounds, gaps, constraints, Some(window))?; let result = self.resolve_geometry_with_constraints(bounds, gaps, constraints, Some(window))?;
let rect = result let rect = result
.geometry .geometry
.get(&window) .get(&window)
@@ -476,7 +513,7 @@ impl ScrollingLayout {
}), }),
), ),
); );
self.geometry_with_constraints(bounds, gaps, constraints, Some(window))?; self.resolve_geometry_with_constraints(bounds, gaps, constraints, Some(window))?;
Ok(self.viewport_x != before) Ok(self.viewport_x != before)
} }
@@ -552,7 +589,8 @@ impl ScrollingLayout {
Ok(changed) Ok(changed)
} }
pub fn geometry_with_constraints( /// Consume pending viewport policy requests and solve the logical layout.
pub fn resolve_geometry_with_constraints(
&mut self, &mut self,
bounds: Rect, bounds: Rect,
gaps: GapConfig, gaps: GapConfig,
@@ -616,7 +654,8 @@ impl ScrollingLayout {
.collect(); .collect();
let viewport_resized = self.last_viewport_width != Some(viewport_width); let viewport_resized = self.last_viewport_width != Some(viewport_width);
self.last_viewport_width = Some(viewport_width); self.last_viewport_width = Some(viewport_width);
if self.width_cycle_pending { match self.pending_viewport.take() {
Some(PendingViewportRequest::WidthCycle(_)) => {
if let Some(active) = self.active_column { if let Some(active) = self.active_column {
self.record_viewport_basis( self.record_viewport_basis(
&column_positions, &column_positions,
@@ -626,11 +665,14 @@ impl ScrollingLayout {
); );
} }
self.retarget_after_width_cycle(&column_positions, viewport_width); self.retarget_after_width_cycle(&column_positions, viewport_width);
self.width_cycle_pending = false; }
self.reveal_pending = None; Some(PendingViewportRequest::Focus(PendingFocusRequest::Slide {
self.swipe_focus_pending = None; from: previous,
} else if let Some((previous, focused)) = self.swipe_focus_pending.take() { to: focused,
if let (Some((from, _)), Some((to, _))) = (self.window_location(previous), self.window_location(focused)) { })) => {
if let (Some((from, _)), Some((to, _))) =
(self.window_location(previous), self.window_location(focused))
{
self.record_viewport_basis( self.record_viewport_basis(
&column_positions, &column_positions,
inner, inner,
@@ -645,9 +687,14 @@ impl ScrollingLayout {
self.viewport_x = self.viewport_x =
(self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset); (self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset);
} }
self.reveal_pending = None; }
} else { request => {
let reveal = self.reveal_pending.take().or_else(|| { let reveal = match request {
Some(PendingViewportRequest::Focus(PendingFocusRequest::Reveal(window))) => Some(window),
None => None,
_ => unreachable!(),
}
.or_else(|| {
(viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged) (viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged)
.then(|| self.active_window()) .then(|| self.active_window())
.flatten() .flatten()
@@ -662,6 +709,7 @@ impl ScrollingLayout {
self.reveal_column(&column_positions, viewport_width, column); self.reveal_column(&column_positions, viewport_width, column);
} }
} }
}
let mut result = LayoutResult::default(); let mut result = LayoutResult::default();
for (column_index, column) in self.columns.iter().enumerate() { for (column_index, column) in self.columns.iter().enumerate() {
@@ -780,7 +828,7 @@ impl ScrollingLayout {
if self.columns[source].windows.len() == 1 { if self.columns[source].windows.len() == 1 {
self.columns.swap(source, destination); self.columns.swap(source, destination);
self.active_column = Some(destination); self.active_column = Some(destination);
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
return Ok(()); return Ok(());
} }
@@ -789,7 +837,7 @@ impl ScrollingLayout {
let insertion = insertion.min(self.columns.len()); let insertion = insertion.min(self.columns.len());
self.columns.insert(insertion, Column::new(window, self.default_width)); self.columns.insert(insertion, Column::new(window, self.default_width));
self.active_column = Some(insertion); self.active_column = Some(insertion);
self.reveal_pending = self.active_window(); self.request_reveal(self.active_window());
Ok(()) Ok(())
} }
@@ -912,6 +960,86 @@ mod tests {
WindowId(id) WindowId(id)
} }
#[test]
fn pending_request_preserves_slide_and_width_cycle_precedence() {
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(150.0));
let a = window(1);
let b = window(2);
layout.insert(a, None).unwrap();
layout.insert(b, Some(a)).unwrap();
layout.set_focus_strategy(ViewportFocusStrategy::Center);
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(b))
.unwrap();
layout.slide_focus_from(b, a).unwrap();
// Ordinary reveal requests must not replace a pending slide.
layout.focus(b).unwrap();
assert_eq!(
layout.pending_focus(),
Some(PendingFocusRequest::Slide { from: b, to: a })
);
layout
.cycle_column_width(b, &[ColumnWidth::Fixed(150.0), ColumnWidth::Fixed(100.0)])
.unwrap();
assert!(matches!(
layout.pending_viewport,
Some(PendingViewportRequest::WidthCycle(_))
));
layout.focus_and_reveal(a).unwrap();
assert_eq!(
layout.pending_viewport,
Some(PendingViewportRequest::WidthCycle(Some(PendingFocusRequest::Reveal(a))))
);
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a))
.unwrap();
assert!(matches!(
layout.viewport_target().unwrap().request,
ViewportRequest::WidthCycle(_)
));
assert!(layout.pending_viewport.is_none());
}
#[test]
fn canceled_width_cycle_restores_focus_request_and_solver_consumes_it_once() {
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
let mut layout = ScrollingLayout::with_default_width(ColumnWidth::Fixed(150.0));
let a = window(1);
let b = window(2);
layout.insert(a, None).unwrap();
layout.insert(b, Some(a)).unwrap();
layout.set_focus_strategy(ViewportFocusStrategy::Center);
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(b))
.unwrap();
layout.slide_focus_from(b, a).unwrap();
assert!(layout.cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]).unwrap());
assert!(!layout.cycle_column_width(a, &[ColumnWidth::Fixed(100.0)]).unwrap());
assert_eq!(
layout.pending_viewport,
Some(PendingViewportRequest::Focus(PendingFocusRequest::Slide {
from: b,
to: a
}))
);
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a))
.unwrap();
assert!(matches!(
layout.viewport_target().unwrap().request,
ViewportRequest::Slide(_, _)
));
let viewport = layout.viewport_x();
let provenance = layout.viewport_target().cloned();
layout
.resolve_geometry_with_constraints(bounds, GapConfig::default(), &HashMap::new(), Some(a))
.unwrap();
assert_eq!(layout.viewport_x(), viewport);
assert_eq!(layout.viewport_target(), provenance.as_ref());
assert!(layout.pending_viewport.is_none());
}
#[test] #[test]
fn explicit_center_replays_maximum_even_when_only_held_width_crosses_it() { fn explicit_center_replays_maximum_even_when_only_held_width_crosses_it() {
let bounds = Rect::new(0.0, 0.0, 200.0, 100.0); let bounds = Rect::new(0.0, 0.0, 200.0, 100.0);
@@ -973,11 +1101,11 @@ mod tests {
layout.insert(b, Some(a)).unwrap(); layout.insert(b, Some(a)).unwrap();
layout.insert(c, Some(b)).unwrap(); layout.insert(c, Some(b)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c)) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c))
.unwrap(); .unwrap();
layout.set_column_width(b, ColumnWidth::Fixed(150.0)).unwrap(); layout.set_column_width(b, ColumnWidth::Fixed(150.0)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c)) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c))
.unwrap(); .unwrap();
assert!(layout.viewport_depends_on_width(b, 100.0, 150.0), "{strategy:?}"); assert!(layout.viewport_depends_on_width(b, 100.0, 150.0), "{strategy:?}");
assert!( assert!(
@@ -986,12 +1114,12 @@ mod tests {
); );
// An unrelated relayout does not erase the last calculation. // An unrelated relayout does not erase the last calculation.
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c)) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(c))
.unwrap(); .unwrap();
assert!(layout.viewport_depends_on_width(b, 100.0, 150.0)); assert!(layout.viewport_depends_on_width(b, 100.0, 150.0));
layout.focus_and_reveal(a).unwrap(); layout.focus_and_reveal(a).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(a)) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(a))
.unwrap(); .unwrap();
assert!( assert!(
!layout.viewport_depends_on_width(b, 100.0, 150.0), !layout.viewport_depends_on_width(b, 100.0, 150.0),
@@ -1020,7 +1148,7 @@ mod tests {
}, },
)]); )]);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(c)) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(c))
.unwrap(); .unwrap();
assert_eq!(layout.allocated_column_width(b), Some(150.0)); assert_eq!(layout.allocated_column_width(b), Some(150.0));
assert_eq!(result.geometry[&b].width, 150.0, "minimum wins inconsistent maximum"); assert_eq!(result.geometry[&b].width, 150.0, "minimum wins inconsistent maximum");
@@ -1073,11 +1201,11 @@ mod tests {
layout.focus_and_reveal(window(target)).unwrap(); layout.focus_and_reveal(window(target)).unwrap();
let mut expected = layout.clone(); let mut expected = layout.clone();
expected expected
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(target))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(target)))
.unwrap(); .unwrap();
layout.focus_without_reveal(window(2)).unwrap(); layout.focus_without_reveal(window(2)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.active_window(), Some(window(2))); assert_eq!(layout.active_window(), Some(window(2)));
assert_eq!(layout.viewport_x(), expected.viewport_x(), "{strategy:?}"); assert_eq!(layout.viewport_x(), expected.viewport_x(), "{strategy:?}");
@@ -1087,7 +1215,7 @@ mod tests {
layout.remove(window(3)).unwrap(); layout.remove(window(3)).unwrap();
assert!( assert!(
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.is_ok() .is_ok()
); );
} }
@@ -1107,7 +1235,7 @@ mod tests {
}; };
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 1_000.0); assert_eq!(layout.viewport_x(), 1_000.0);
@@ -1182,7 +1310,7 @@ mod tests {
for (focused, expected) in [(1, 0.0), (2, 500.0), (3, 1500.0), (2, 1000.0), (2, 1000.0)] { for (focused, expected) in [(1, 0.0), (2, 500.0), (3, 1500.0), (2, 1000.0), (2, 1000.0)] {
layout.focus_and_reveal(window(focused)).unwrap(); layout.focus_and_reveal(window(focused)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), expected); assert_eq!(layout.viewport_x(), expected);
} }
@@ -1200,7 +1328,7 @@ mod tests {
smart: false, smart: false,
}; };
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 1010.0); assert_eq!(layout.viewport_x(), 1010.0);
assert_eq!(result.geometry[&window(2)].x, 0.0); assert_eq!(result.geometry[&window(2)].x, 0.0);
@@ -1208,7 +1336,7 @@ mod tests {
assert!(previous.x + previous.width <= 0.0); assert!(previous.x + previous.width <= 0.0);
layout.focus_and_reveal(window(1)).unwrap(); layout.focus_and_reveal(window(1)).unwrap();
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
assert_eq!(result.geometry[&window(1)].x, 0.0); assert_eq!(result.geometry[&window(1)].x, 0.0);
@@ -1234,7 +1362,7 @@ mod tests {
for (focused, offset) in [1, 3, 2, 1].into_iter().zip(offsets) { for (focused, offset) in [1, 3, 2, 1].into_iter().zip(offsets) {
layout.focus_and_reveal(window(focused)).unwrap(); layout.focus_and_reveal(window(focused)).unwrap();
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
let rect = result.geometry[&window(focused)]; let rect = result.geometry[&window(focused)];
assert_eq!(layout.viewport_x(), offset, "width={width:?}, focused={focused}"); assert_eq!(layout.viewport_x(), offset, "width={width:?}, focused={focused}");
@@ -1264,11 +1392,11 @@ mod tests {
}; };
let first = layout let first = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
let second = layout let second = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1285,12 +1413,12 @@ mod tests {
.set_column_width(window(2), ColumnWidth::Proportion(1.0)) .set_column_width(window(2), ColumnWidth::Proportion(1.0))
.unwrap(); .unwrap();
let zoomed = layout let zoomed = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(zoomed.geometry[&window(2)].width, 980.0); assert_eq!(zoomed.geometry[&window(2)].width, 980.0);
layout.insert(window(3), Some(window(2))).unwrap(); layout.insert(window(3), Some(window(2))).unwrap();
let opened = layout let opened = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
.unwrap(); .unwrap();
let previous = opened.geometry[&window(2)]; let previous = opened.geometry[&window(2)];
let new = opened.geometry[&window(3)]; let new = opened.geometry[&window(3)];
@@ -1302,7 +1430,7 @@ mod tests {
.set_column_width(window(2), ColumnWidth::Proportion(0.5)) .set_column_width(window(2), ColumnWidth::Proportion(0.5))
.unwrap(); .unwrap();
let restored = layout let restored = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(restored.geometry[&window(2)].width, 485.0); assert_eq!(restored.geometry[&window(2)].width, 485.0);
} }
@@ -1321,7 +1449,7 @@ mod tests {
let (mut layout, bounds, gaps) = paged_layout(6); let (mut layout, bounds, gaps) = paged_layout(6);
for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] { for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] {
let geometry = layout let geometry = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap() .unwrap()
.geometry; .geometry;
let first = (focused - 1) / 2 * 2 + 1; let first = (focused - 1) / 2 * 2 + 1;
@@ -1349,12 +1477,12 @@ mod tests {
}; };
layout.focus_and_reveal(window(1)).unwrap(); layout.focus_and_reveal(window(1)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
for (previous, focused, expected) in [(1, 2, 0.0), (2, 3, 500.0), (3, 2, 500.0), (2, 1, 0.0)] { for (previous, focused, expected) in [(1, 2, 0.0), (2, 3, 500.0), (3, 2, 500.0), (2, 1, 0.0)] {
layout.slide_focus_from(window(previous), window(focused)).unwrap(); layout.slide_focus_from(window(previous), window(focused)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), expected); assert_eq!(layout.viewport_x(), expected);
} }
@@ -1364,7 +1492,7 @@ mod tests {
fn swipe_focus_slides_one_column_at_a_time_in_paged_layout() { fn swipe_focus_slides_one_column_at_a_time_in_paged_layout() {
let (mut layout, bounds, gaps) = paged_layout(5); let (mut layout, bounds, gaps) = paged_layout(5);
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1372,7 +1500,7 @@ mod tests {
layout.focus_and_reveal(window(focused)).unwrap(); layout.focus_and_reveal(window(focused)).unwrap();
layout.slide_focus_from(window(previous), window(focused)).unwrap(); layout.slide_focus_from(window(previous), window(focused)).unwrap();
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), expected); assert_eq!(layout.viewport_x(), expected);
@@ -1384,11 +1512,11 @@ mod tests {
fn paged_focus_realigns_after_output_resize() { fn paged_focus_realigns_after_output_resize() {
let (mut layout, bounds, gaps) = paged_layout(6); let (mut layout, bounds, gaps) = paged_layout(6);
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(5))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(5)))
.unwrap(); .unwrap();
let bounds = Rect::new(20.0, 48.0, 1400.0, 800.0); let bounds = Rect::new(20.0, 48.0, 1400.0, 800.0);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(5))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(5)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 2780.0); assert_eq!(layout.viewport_x(), 2780.0);
assert_eq!(result.geometry[&window(5)].x, 30.0); assert_eq!(result.geometry[&window(5)].x, 30.0);
@@ -1400,12 +1528,12 @@ mod tests {
let (mut layout, bounds, gaps) = paged_layout(4); let (mut layout, bounds, gaps) = paged_layout(4);
layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap(); layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap();
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
let rect = result.geometry[&window(2)]; let rect = result.geometry[&window(2)];
assert_eq!(rect.x + rect.width / 2.0, 500.0); assert_eq!(rect.x + rect.width / 2.0, 500.0);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(result.geometry[&window(1)].x, 10.0); assert_eq!(result.geometry[&window(1)].x, 10.0);
} }
@@ -1415,12 +1543,12 @@ mod tests {
let (mut layout, bounds, gaps) = paged_layout(4); let (mut layout, bounds, gaps) = paged_layout(4);
layout.columns[0].width = ColumnWidth::Full; layout.columns[0].width = ColumnWidth::Full;
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
.unwrap(); .unwrap();
assert_eq!(result.geometry[&window(2)].x, 10.0); assert_eq!(result.geometry[&window(2)].x, 10.0);
assert_eq!(result.geometry[&window(3)].x, 505.0); assert_eq!(result.geometry[&window(3)].x, 505.0);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
.unwrap(); .unwrap();
assert_eq!(result.geometry[&window(4)].x, 10.0); assert_eq!(result.geometry[&window(4)].x, 10.0);
} }
@@ -1436,7 +1564,7 @@ mod tests {
}, },
)]); )]);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(window(2)))
.unwrap(); .unwrap();
assert!(layout.viewport_x() > 0.0); assert!(layout.viewport_x() > 0.0);
let rect = result.geometry[&window(2)]; let rect = result.geometry[&window(2)];
@@ -1452,7 +1580,7 @@ mod tests {
let bounds = Rect::new(13.25, 48.5, 1000.5, 800.25); let bounds = Rect::new(13.25, 48.5, 1000.5, 800.25);
for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] { for focused in [1, 2, 3, 4, 5, 6, 5, 4, 3, 2, 1] {
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
let first = (focused - 1) / 3 * 3 + 1; let first = (focused - 1) / 3 * 3 + 1;
assert!((result.geometry[&window(first)].x - 23.25).abs() < 1e-6); assert!((result.geometry[&window(first)].x - 23.25).abs() < 1e-6);
@@ -1470,7 +1598,7 @@ mod tests {
} }
for (focused, first) in [(1, 1), (2, 1), (3, 3), (4, 3), (5, 3), (2, 1)] { for (focused, first) in [(1, 1), (2, 1), (3, 3), (4, 3), (5, 3), (2, 1)] {
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(focused)))
.unwrap(); .unwrap();
assert_eq!(result.geometry[&window(first)].x, 10.0); assert_eq!(result.geometry[&window(first)].x, 10.0);
let rect = result.geometry[&window(focused)]; let rect = result.geometry[&window(focused)];
@@ -1489,13 +1617,13 @@ mod tests {
}, },
)]); )]);
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &constraints, Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(result.geometry[&window(2)].x, 10.0); assert_eq!(result.geometry[&window(2)].x, 10.0);
assert_eq!(result.geometry[&window(3)].x, 505.0); assert_eq!(result.geometry[&window(3)].x, 505.0);
layout.remove(window(1)).unwrap(); layout.remove(window(1)).unwrap();
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
assert_eq!(result.geometry[&window(2)].x, 10.0); assert_eq!(result.geometry[&window(2)].x, 10.0);
@@ -1515,14 +1643,14 @@ mod tests {
for id in 1..=3 { for id in 1..=3 {
layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap(); layout.insert(window(id), (id > 1).then(|| window(id - 1))).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(id)))
.unwrap(); .unwrap();
} }
let before = layout.viewport_x(); let before = layout.viewport_x();
layout.resize_window(window(3), Direction::Right, 0.5).unwrap(); layout.resize_window(window(3), Direction::Right, 0.5).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), before); assert_eq!(layout.viewport_x(), before);
@@ -1542,10 +1670,10 @@ mod tests {
layout.insert(window(id), None).unwrap(); layout.insert(window(id), None).unwrap();
} }
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(3)))
.unwrap(); .unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1565,13 +1693,13 @@ mod tests {
layout.insert(window(id), None).unwrap(); layout.insert(window(id), None).unwrap();
} }
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
.unwrap(); .unwrap();
let before = layout.viewport_x(); let before = layout.viewport_x();
layout.remove(window(1)).unwrap(); layout.remove(window(1)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), before); assert_eq!(layout.viewport_x(), before);
@@ -1605,13 +1733,13 @@ mod tests {
..GapConfig::default() ..GapConfig::default()
}; };
let first = layout let first = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap() .unwrap()
.geometry[&window(1)]; .geometry[&window(1)];
layout.insert(window(2), Some(window(1))).unwrap(); layout.insert(window(2), Some(window(1))).unwrap();
let second = layout let second = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap() .unwrap()
.geometry[&window(1)]; .geometry[&window(1)];
@@ -1698,11 +1826,11 @@ mod tests {
layout.focus(window(1)).unwrap(); layout.focus(window(1)).unwrap();
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
layout.cycle_column_width(window(1), &presets).unwrap(); layout.cycle_column_width(window(1), &presets).unwrap();
let zoomed = layout let zoomed = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1712,7 +1840,7 @@ mod tests {
layout.cycle_column_width(window(1), &presets).unwrap(); layout.cycle_column_width(window(1), &presets).unwrap();
let restored = layout let restored = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(1)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1726,11 +1854,11 @@ mod tests {
let presets = [ColumnWidth::Proportion(0.5), ColumnWidth::Full]; let presets = [ColumnWidth::Proportion(0.5), ColumnWidth::Full];
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
layout.cycle_column_width(window(2), &presets).unwrap(); layout.cycle_column_width(window(2), &presets).unwrap();
let zoomed = layout let zoomed = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 495.0); assert_eq!(layout.viewport_x(), 495.0);
@@ -1740,7 +1868,7 @@ mod tests {
layout.cycle_column_width(window(2), &presets).unwrap(); layout.cycle_column_width(window(2), &presets).unwrap();
let restored = layout let restored = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
assert_eq!(layout.viewport_x(), 0.0); assert_eq!(layout.viewport_x(), 0.0);
@@ -1779,12 +1907,12 @@ mod tests {
smart: false, smart: false,
}; };
layout layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(4)))
.unwrap(); .unwrap();
assert!(layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap()); assert!(layout.center_window(window(2), bounds, gaps, &HashMap::new()).unwrap());
let result = layout let result = layout
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2))) .resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(window(2)))
.unwrap(); .unwrap();
let rect = result.geometry[&window(2)]; let rect = result.geometry[&window(2)];
+2
View File
@@ -17,6 +17,8 @@ screen_x = world_x - viewport_x
The window and viewport have separate springs because either can move without the other. Fullscreen and maximize temporarily take control of presentation geometry, which makes returning to the scrolling layout a handoff between two ways of positioning the same window. That handoff must preserve the visible rectangle; otherwise the window would jump when the layout resumes control. The window and viewport have separate springs because either can move without the other. Fullscreen and maximize temporarily take control of presentation geometry, which makes returning to the scrolling layout a handoff between two ways of positioning the same window. That handoff must preserve the visible rectangle; otherwise the window would jump when the layout resumes control.
Each workspace’s [ViewportPresentation](../compositor/src/state/viewport.rs) stores one motion value and its current owner: a spring or a focus-swipe gesture. A client-commit hold pauses that owner without discarding its position or velocity. The release frame excludes waiting time; the following tick resumes motion. The existing width dependency graph determines whether a target needs a hold by replaying its provenance with uncommitted widths. It does not advance the viewport. Prediction evaluates the same motion step without changing the owner or consuming a pending layout request.
The compositor carries the result in [WindowPresentation](../compositor/src/presentation.rs), alongside window identity, opacity, focus emphasis, shadow strength, and content-scaling information. Keeping the identity with the presentation allows an overview thumbnail or a retained closing image to remain associated with its window even though its bounds have changed. The compositor carries the result in [WindowPresentation](../compositor/src/presentation.rs), alongside window identity, opacity, focus emphasis, shadow strength, and content-scaling information. Keeping the identity with the presentation allows an overview thumbnail or a retained closing image to remain associated with its window even though its bounds have changed.
Those bounds also keep the rendering layers together. Content, clip, and border use the same presented geometry, with their physical edges rounded together at the output scale. Independent springs for those edges could let the border pull away from the content, especially at fractional scales. Shadow strength and focus emphasis can have different responses because neither determines where the content ends. Those bounds also keep the rendering layers together. Content, clip, and border use the same presented geometry, with their physical edges rounded together at the output scale. Independent springs for those edges could let the border pull away from the content, especially at fractional scales. Shadow strength and focus emphasis can have different responses because neither determines where the content ends.