Unify scrolling viewport presentation ownership
This commit is contained in:
1 parent
4a9880eaad
commit
a00f1c6f3a
12 files changed
+894
-316
No files matched your search
@@ -14,9 +14,12 @@ mod prediction;
|
||||
mod presentation_dependencies;
|
||||
pub(crate) use prediction::FrameScene;
|
||||
mod lifecycle;
|
||||
mod viewport;
|
||||
mod window_registry;
|
||||
mod windows;
|
||||
|
||||
use viewport::ViewportPresentation;
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::error::Error;
|
||||
use std::ffi::OsString;
|
||||
@@ -146,6 +149,7 @@ impl SlideOffset {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct FocusSwipe {
|
||||
workspace: WorkspaceId,
|
||||
from: WindowId,
|
||||
@@ -444,11 +448,10 @@ pub struct Ferese {
|
||||
pub(crate) pending_logout: Option<u32>,
|
||||
pub(crate) logout_query: Option<u32>,
|
||||
pub(crate) logout_owner: Option<ObjectId>,
|
||||
viewport_animations: HashMap<WorkspaceId, AnimatedValue>,
|
||||
viewports: HashMap<WorkspaceId, ViewportPresentation>,
|
||||
presentation_dependencies: presentation_dependencies::PresentationDependencies,
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
pub(crate) resize_metrics: crate::resize_metrics::ResizeMetrics,
|
||||
focus_swipe: Option<FocusSwipe>,
|
||||
workspace_slides: HashMap<OutputId, WorkspaceSlide>,
|
||||
workspace_slide_offsets: HashMap<WorkspaceId, (f64, f64)>,
|
||||
pub focused_window: Option<WindowId>,
|
||||
@@ -721,11 +724,10 @@ impl Ferese {
|
||||
pending_logout: None,
|
||||
logout_query: None,
|
||||
logout_owner: None,
|
||||
viewport_animations: HashMap::new(),
|
||||
viewports: HashMap::new(),
|
||||
presentation_dependencies: Default::default(),
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
resize_metrics: crate::resize_metrics::ResizeMetrics::default(),
|
||||
focus_swipe: None,
|
||||
workspace_slides: HashMap::new(),
|
||||
workspace_slide_offsets: HashMap::new(),
|
||||
focused_window: None,
|
||||
|
||||
@@ -50,15 +50,15 @@ impl Ferese {
|
||||
let viewport = self
|
||||
.presentation_dependencies
|
||||
.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
|
||||
.output_workspaces
|
||||
.output_for_workspace(workspace)
|
||||
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(workspace))
|
||||
&& self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.is_none_or(|swipe| swipe.workspace != workspace);
|
||||
&& self.focus_swipe().is_none_or(|swipe| swipe.workspace != workspace);
|
||||
let others = self.workspaces.workspace(workspace).is_some_and(|workspace| {
|
||||
workspace
|
||||
.layout
|
||||
@@ -95,7 +95,7 @@ impl Ferese {
|
||||
}
|
||||
|
||||
fn animations_need_tick(&self) -> bool {
|
||||
if self.focus_swipe.is_some()
|
||||
if self.focus_swipe().is_some()
|
||||
|| self.presentation_dependencies.needs_tick()
|
||||
|| !self.workspace_slides.is_empty()
|
||||
|| !self.render.closing.is_empty()
|
||||
@@ -106,13 +106,11 @@ impl Ferese {
|
||||
return true;
|
||||
}
|
||||
|
||||
let moving = |value: &AnimatedValue| value.current != value.target || value.velocity != 0.0;
|
||||
|
||||
if self.viewport_animations.iter().any(|(workspace, viewport)| {
|
||||
if self.viewports.iter().any(|(workspace, viewport)| {
|
||||
self.output_workspaces
|
||||
.output_for_workspace(*workspace)
|
||||
.is_some_and(|output| self.output_workspaces.active_workspace(output) == Some(*workspace))
|
||||
&& moving(viewport)
|
||||
&& viewport.needs_tick()
|
||||
}) {
|
||||
return true;
|
||||
}
|
||||
@@ -151,12 +149,12 @@ impl Ferese {
|
||||
}
|
||||
|
||||
let delta = delta.mul_f64(self.animation_speed);
|
||||
if self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.is_some_and(|swipe| !self.focus_swipe_is_current(swipe))
|
||||
&& let Some(swipe) = self.focus_swipe.take()
|
||||
if let Some(workspace) = self
|
||||
.focus_swipe()
|
||||
.filter(|swipe| !self.focus_swipe_is_current(swipe))
|
||||
.map(|swipe| swipe.workspace)
|
||||
{
|
||||
let swipe = self.viewports.get_mut(&workspace).unwrap().end_gesture(None).unwrap();
|
||||
self.presentation_dependencies
|
||||
.restore_viewport(swipe.workspace, swipe.dependencies);
|
||||
self.release_presentation_dependencies();
|
||||
@@ -284,7 +282,15 @@ impl Ferese {
|
||||
}
|
||||
|
||||
// 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")]
|
||||
self.measure_resize_pauses(now);
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
@@ -304,9 +310,8 @@ impl Ferese {
|
||||
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_viewport.extend(released_viewport);
|
||||
let animations_enabled = self.animations_enabled;
|
||||
let animation_speed = self.animation_speed;
|
||||
#[cfg(feature = "resize-metrics")]
|
||||
@@ -353,9 +358,8 @@ impl Ferese {
|
||||
});
|
||||
active_animation |= self.render.snapshots().next().is_some();
|
||||
// Keep scheduling frames while waiting, so the deadline cannot stall.
|
||||
active_animation |=
|
||||
self.windows.resizing().next().is_some() || !held_reflow.is_empty() || !held_viewport.is_empty();
|
||||
for (workspace, viewport) in &mut self.viewport_animations {
|
||||
active_animation |= self.windows.resizing().next().is_some() || !held_reflow.is_empty();
|
||||
for (workspace, viewport) in &mut self.viewports {
|
||||
if self
|
||||
.output_workspaces
|
||||
.output_for_workspace(*workspace)
|
||||
@@ -364,18 +368,13 @@ impl Ferese {
|
||||
continue;
|
||||
}
|
||||
|
||||
if held_viewport.contains(workspace) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Some(swipe) = self.focus_swipe.as_ref().filter(|swipe| swipe.workspace == *workspace) {
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = 0.0;
|
||||
} else if self.animations_enabled {
|
||||
active_animation |= viewport.advance(delta, self.viewport_spring_config);
|
||||
} else {
|
||||
viewport.snap();
|
||||
}
|
||||
active_animation |= viewport.advance(
|
||||
delta,
|
||||
self.viewport_spring_config,
|
||||
self.animations_enabled,
|
||||
self.presentation_dependencies
|
||||
.viewport_blocked(*workspace, &self.windows),
|
||||
);
|
||||
}
|
||||
|
||||
let mut remaps = Vec::new();
|
||||
@@ -417,10 +416,10 @@ impl Ferese {
|
||||
}
|
||||
|
||||
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 {
|
||||
sync_scrolling_coordinates(geometry, world_x, viewport, true);
|
||||
sync_scrolling_coordinates(geometry, world_x, viewport.motion(), true);
|
||||
} else if !held && self.animations_enabled {
|
||||
active_animation |= world_x.advance(delta, self.spring_config);
|
||||
} else if !held {
|
||||
@@ -432,10 +431,7 @@ impl Ferese {
|
||||
if held {
|
||||
presented_world.velocity = 0.0;
|
||||
}
|
||||
let mut presented_viewport = *viewport;
|
||||
if held_viewport.contains(workspace) {
|
||||
presented_viewport.velocity = 0.0;
|
||||
}
|
||||
let presented_viewport = viewport.presented_motion();
|
||||
sync_scrolling_coordinates(geometry, &mut presented_world, &presented_viewport, false);
|
||||
}
|
||||
}
|
||||
@@ -748,9 +744,9 @@ mod tests {
|
||||
#[test]
|
||||
fn scrolling_target_changes_and_zoom_ownership_switches_are_continuous() {
|
||||
let config = SpringConfig::default();
|
||||
let mut viewport = AnimatedValue::new(100.0);
|
||||
viewport.set_target(400.0);
|
||||
viewport.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing);
|
||||
let mut viewport = ViewportPresentation::new(100.0);
|
||||
viewport.retarget(400.0, true);
|
||||
viewport.advance(Duration::from_millis(40), config, true, false);
|
||||
let mut world = AnimatedValue::new(500.0);
|
||||
world.set_target(900.0);
|
||||
world.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing);
|
||||
@@ -758,25 +754,25 @@ mod tests {
|
||||
width.set_target(700.0);
|
||||
width.advance_with_policy(Duration::from_millis(40), config, CrossingPolicy::NoCrossing);
|
||||
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,
|
||||
);
|
||||
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));
|
||||
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;
|
||||
let before = geometry.visual.current;
|
||||
viewport.retarget_preserving_motion(300.0);
|
||||
viewport.retarget(300.0, true);
|
||||
world.retarget_preserving_motion(1000.0);
|
||||
width.retarget_preserving_motion(600.0);
|
||||
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),
|
||||
);
|
||||
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.velocity.x, world.velocity - viewport.velocity);
|
||||
assert_eq!(geometry.logical.x, world.target - viewport.target);
|
||||
assert_eq!(geometry.visual.velocity.x, world.velocity - viewport.motion().velocity);
|
||||
assert_eq!(geometry.logical.x, world.target - viewport.motion().target);
|
||||
|
||||
for mode in [
|
||||
PresentationMode::Maximized,
|
||||
@@ -785,9 +781,9 @@ mod tests {
|
||||
] {
|
||||
let before = geometry.visual.current;
|
||||
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!(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);
|
||||
assert_eq!(geometry.visual.current, before);
|
||||
geometry.advance(Duration::from_millis(16), config, true);
|
||||
@@ -799,7 +795,7 @@ mod tests {
|
||||
let zoom_owned = geometry.is_zooming();
|
||||
geometry.advance(Duration::from_millis(16), config, true);
|
||||
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() {
|
||||
break;
|
||||
@@ -807,8 +803,8 @@ mod tests {
|
||||
}
|
||||
assert!(!geometry.is_zooming());
|
||||
let before = geometry.visual.current;
|
||||
world.set_target(geometry.logical.x + viewport.target);
|
||||
sync_scrolling_coordinates(&mut geometry, &mut world, &viewport, false);
|
||||
world.set_target(geometry.logical.x + viewport.motion().target);
|
||||
sync_scrolling_coordinates(&mut geometry, &mut world, viewport.motion(), false);
|
||||
assert_eq!(geometry.visual.current, before);
|
||||
}
|
||||
|
||||
@@ -870,9 +866,11 @@ mod tests {
|
||||
state.windows.records.insert(id, settled());
|
||||
let mut viewport = AnimatedValue::new(0.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));
|
||||
let frozen = state.viewport_animations[&workspace];
|
||||
let frozen = *state.viewports[&workspace].motion();
|
||||
assert!(frozen.current > 0.0 && frozen.current < 500.0);
|
||||
|
||||
// No intervening dispatch at all: expiration happens on the 300ms tick.
|
||||
@@ -886,7 +884,7 @@ mod tests {
|
||||
.wait_for_viewport(workspace, id, 1.into());
|
||||
state.advance_animations_at(Duration::from_millis(300), Duration::from_millis(340));
|
||||
assert!(state.windows.transaction(&id).is_none());
|
||||
assert_eq!(state.viewport_animations[&workspace], frozen);
|
||||
assert_eq!(*state.viewports[&workspace].motion(), frozen);
|
||||
state.advance_animations_at(Duration::from_millis(16), Duration::from_millis(356));
|
||||
let mut expected = frozen;
|
||||
expected.advance_with_policy(
|
||||
@@ -894,7 +892,7 @@ mod tests {
|
||||
state.viewport_spring_config,
|
||||
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
|
||||
// elapsed pause too, then resume at the next normal 16ms interval.
|
||||
@@ -907,10 +905,10 @@ mod tests {
|
||||
.presentation_dependencies
|
||||
.wait_for_viewport(workspace, id, 2.into());
|
||||
state.advance_animations_at(Duration::from_millis(100), Duration::from_millis(456));
|
||||
let frozen = state.viewport_animations[&workspace];
|
||||
let frozen = *state.viewports[&workspace].motion();
|
||||
state.windows.clear_transaction(&id);
|
||||
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));
|
||||
let mut expected = frozen;
|
||||
expected.advance_with_policy(
|
||||
@@ -918,14 +916,14 @@ mod tests {
|
||||
state.viewport_spring_config,
|
||||
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();
|
||||
state.last_animation_tick = instant;
|
||||
state.session_lock.sleeping = true;
|
||||
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.advance_animations(instant + Duration::from_millis(316));
|
||||
let mut expected = frozen;
|
||||
@@ -934,11 +932,11 @@ mod tests {
|
||||
state.viewport_spring_config,
|
||||
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
|
||||
// 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.reset_animation_clock();
|
||||
let resumed = state.last_animation_tick;
|
||||
@@ -949,11 +947,12 @@ mod tests {
|
||||
state.viewport_spring_config,
|
||||
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
|
||||
// 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.set_transaction(
|
||||
id,
|
||||
@@ -964,9 +963,11 @@ mod tests {
|
||||
.presentation_dependencies
|
||||
.wait_for_viewport(workspace, id, 3.into());
|
||||
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.advance_animations_at(Duration::from_millis(16), Duration::from_millis(916));
|
||||
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())
|
||||
.unwrap();
|
||||
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_world = state.windows.record(b_id).unwrap().world_x.unwrap().1;
|
||||
state.advance_animations_at(Duration::from_millis(16), Duration::ZERO);
|
||||
assert_ne!(
|
||||
state.viewport_animations[&workspace].current, before,
|
||||
state.viewports[&workspace].motion().current,
|
||||
before,
|
||||
"independent viewport stalled before delayed buffer commit"
|
||||
);
|
||||
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);
|
||||
let forecast = state.sample_frame(&output, Duration::from_millis(8));
|
||||
let predicted = &forecast.windows[&b_id];
|
||||
let mut viewport = state.viewport_animations[&workspace];
|
||||
let mut viewport = *state.viewports[&workspace].motion();
|
||||
viewport.advance(
|
||||
Duration::from_millis(8).mul_f64(state.animation_speed),
|
||||
state.viewport_spring_config,
|
||||
@@ -779,9 +780,9 @@ fn resize_dependencies_protocol_and_pixels() {
|
||||
.presentation_dependencies
|
||||
.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));
|
||||
assert_eq!(state.viewport_animations[&workspace], frozen);
|
||||
assert_eq!(*state.viewports[&workspace].motion(), frozen);
|
||||
// Supersede the acknowledged size, then commit the older acknowledgement.
|
||||
set_width(&mut state, b_id, 96.0);
|
||||
let latest = *state.windows.transaction(&b_id).unwrap();
|
||||
@@ -813,9 +814,9 @@ fn resize_dependencies_protocol_and_pixels() {
|
||||
.presentation_dependencies
|
||||
.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));
|
||||
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 shm = client
|
||||
.create_resource::<WlShm, (), Ferese>(&state.display_handle, 1, ())
|
||||
@@ -900,7 +901,7 @@ fn resize_dependencies_protocol_and_pixels() {
|
||||
.viewport_blocked(workspace, &state.windows)
|
||||
);
|
||||
state.preview_focus_swipe(Direction::Right, SwipeDirection::Left, 0.5);
|
||||
assert!(state.focus_swipe.is_some());
|
||||
assert!(state.focus_swipe().is_some());
|
||||
assert!(
|
||||
state
|
||||
.presentation_dependencies
|
||||
|
||||
@@ -20,7 +20,7 @@ impl Ferese {
|
||||
.output_workspaces
|
||||
.reconcile_workspaces(&mut self.workspaces, visible)
|
||||
{
|
||||
self.viewport_animations.remove(&workspace);
|
||||
self.viewports.remove(&workspace);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -201,7 +201,7 @@ impl Ferese {
|
||||
let layout =
|
||||
match workspace
|
||||
.layout
|
||||
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||
.resolve_geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||
{
|
||||
Ok(layout) => layout,
|
||||
Err(error) => {
|
||||
@@ -212,15 +212,11 @@ impl Ferese {
|
||||
let viewport_target = workspace.layout.viewport_x();
|
||||
let viewport_motion = viewport_target.map(|target| {
|
||||
let viewport = self
|
||||
.viewport_animations
|
||||
.viewports
|
||||
.entry(workspace_id)
|
||||
.or_insert_with(|| AnimatedValue::new(target));
|
||||
let target_changed = (viewport.target - target).abs() > 0.001;
|
||||
viewport.retarget_preserving_motion(target);
|
||||
if !self.animations_enabled {
|
||||
viewport.snap();
|
||||
}
|
||||
(viewport.current, target_changed)
|
||||
.or_insert_with(|| ViewportPresentation::new(target));
|
||||
let target_changed = viewport.retarget(target, self.animations_enabled);
|
||||
(viewport.motion().current, target_changed)
|
||||
});
|
||||
let viewport_current = viewport_motion.map(|(current, _)| current);
|
||||
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);
|
||||
world.velocity = geometry.visual.velocity.x
|
||||
+ self
|
||||
.viewport_animations
|
||||
.viewports
|
||||
.get(&workspace)
|
||||
.map_or(0.0, |viewport| viewport.velocity);
|
||||
.map_or(0.0, |viewport| viewport.motion().velocity);
|
||||
world
|
||||
});
|
||||
animated_world_x.set_target(world_x);
|
||||
@@ -361,11 +357,11 @@ impl Ferese {
|
||||
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(
|
||||
geometry,
|
||||
&mut animated_world_x,
|
||||
viewport,
|
||||
viewport.motion(),
|
||||
geometry.is_zooming(),
|
||||
);
|
||||
}
|
||||
@@ -557,10 +553,12 @@ impl Ferese {
|
||||
pub(super) fn tiled_layout(&mut self, bounds: Rect) -> Result<LayoutResult, ferese_layout::LayoutError> {
|
||||
let constraints = self.window_constraints();
|
||||
let focused = self.focused_window;
|
||||
self.workspaces
|
||||
.active_mut()
|
||||
.layout
|
||||
.geometry_with_constraints(bounds, self.gap_config, &constraints, focused)
|
||||
self.workspaces.active_mut().layout.resolve_geometry_with_constraints(
|
||||
bounds,
|
||||
self.gap_config,
|
||||
&constraints,
|
||||
focused,
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn window_constraints(&self) -> HashMap<WindowId, SizeConstraints> {
|
||||
|
||||
@@ -24,7 +24,7 @@ impl Ferese {
|
||||
gesture_direction: SwipeDirection,
|
||||
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() {
|
||||
return;
|
||||
}
|
||||
@@ -40,9 +40,9 @@ impl Ferese {
|
||||
let to = neighbor.unwrap_or(from);
|
||||
let workspace_id = workspace.id;
|
||||
let start = self
|
||||
.viewport_animations
|
||||
.viewports
|
||||
.get(&workspace_id)
|
||||
.map(|viewport| viewport.current)
|
||||
.map(|viewport| viewport.motion().current)
|
||||
.or_else(|| workspace.layout.viewport_x())
|
||||
.unwrap_or(0.0);
|
||||
let mut candidate = workspace.layout.clone();
|
||||
@@ -53,12 +53,12 @@ impl Ferese {
|
||||
return;
|
||||
}
|
||||
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()
|
||||
{
|
||||
return;
|
||||
}
|
||||
self.focus_swipe = Some(FocusSwipe {
|
||||
let swipe = FocusSwipe {
|
||||
workspace: workspace_id,
|
||||
from,
|
||||
to,
|
||||
@@ -76,17 +76,20 @@ impl Ferese {
|
||||
WorkspaceLayout::Scrolling(layout) => Some(layout),
|
||||
_ => None,
|
||||
},
|
||||
});
|
||||
};
|
||||
self.viewports
|
||||
.entry(workspace_id)
|
||||
.or_insert_with(|| ViewportPresentation::new(start))
|
||||
.begin_gesture(swipe);
|
||||
self.rebuild_presentation_dependencies();
|
||||
self.swipe.mark_preview_started();
|
||||
}
|
||||
if let Some(swipe) = &mut self.focus_swipe {
|
||||
swipe.progress = progress;
|
||||
if let Some(workspace) = self.focus_swipe().map(|swipe| swipe.workspace) {
|
||||
self.viewports.get_mut(&workspace).unwrap().update_gesture(progress);
|
||||
}
|
||||
|
||||
if let Some(output) = self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.focus_swipe()
|
||||
.and_then(|swipe| self.output_workspaces.output_for_workspace(swipe.workspace))
|
||||
.and_then(|id| self.outputs_by_id.get(&id))
|
||||
.cloned()
|
||||
@@ -96,25 +99,27 @@ impl Ferese {
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
let current = self.focus_swipe_is_current(&swipe);
|
||||
let current = self.focus_swipe_is_current(swipe);
|
||||
let blocked = self
|
||||
.presentation_dependencies
|
||||
.viewport_blocked(swipe.workspace, &self.windows);
|
||||
self.presentation_dependencies
|
||||
.restore_viewport(swipe.workspace, swipe.dependencies.clone());
|
||||
// Release from the last input position even if no frame rendered that update.
|
||||
if current
|
||||
&& !blocked
|
||||
&& let Some(viewport) = self.viewport_animations.get_mut(&swipe.workspace)
|
||||
{
|
||||
viewport.current = swipe.position();
|
||||
viewport.velocity = swipe
|
||||
.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity)
|
||||
/ self.animation_speed;
|
||||
}
|
||||
let velocity = (current && !blocked).then(|| {
|
||||
swipe.release_velocity(self.swipe.release_velocity, self.swipe.unbounded_release_velocity)
|
||||
/ self.animation_speed
|
||||
});
|
||||
let workspace = swipe.workspace;
|
||||
let swipe = self
|
||||
.viewports
|
||||
.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| {
|
||||
self.workspaces
|
||||
.active()
|
||||
|
||||
@@ -65,10 +65,10 @@ impl Ferese {
|
||||
&& !self
|
||||
.presentation_dependencies
|
||||
.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() {
|
||||
-viewport.velocity
|
||||
-viewport.presented_motion().velocity
|
||||
} else {
|
||||
predicted_x_velocity
|
||||
};
|
||||
@@ -242,17 +242,13 @@ impl Ferese {
|
||||
return false;
|
||||
};
|
||||
|
||||
self.viewport_animations.get(&workspace).is_some_and(|viewport| {
|
||||
let mut viewport = *viewport;
|
||||
let held = self
|
||||
.focus_swipe
|
||||
.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;
|
||||
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||
})
|
||||
self.viewports
|
||||
.get(&workspace)
|
||||
.is_some_and(ViewportPresentation::needs_tick)
|
||||
|| record.coupled_width.as_ref().is_some_and(|(_, width)| {
|
||||
let mut width = *width;
|
||||
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 viewport = self.viewport_animations.get(workspace)?;
|
||||
let held = self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.is_some_and(|swipe| swipe.workspace == *workspace);
|
||||
|
||||
let viewport = self.viewports.get(workspace)?;
|
||||
Some((
|
||||
*world,
|
||||
*viewport,
|
||||
held || self
|
||||
.presentation_dependencies
|
||||
.viewport_blocked(*workspace, &self.windows),
|
||||
viewport.sample(
|
||||
delta,
|
||||
self.viewport_spring_config,
|
||||
self.animations_enabled,
|
||||
self.presentation_dependencies
|
||||
.viewport_blocked(*workspace, &self.windows),
|
||||
),
|
||||
))
|
||||
});
|
||||
let width = record.coupled_width.as_ref().map(|(_, width)| *width);
|
||||
@@ -335,7 +329,7 @@ fn predict_geometry(
|
||||
delta: Duration,
|
||||
spring: SpringConfig,
|
||||
viewport_spring: SpringConfig,
|
||||
world: Option<(AnimatedValue, AnimatedValue, bool)>,
|
||||
world: Option<(AnimatedValue, AnimatedValue)>,
|
||||
width: Option<AnimatedValue>,
|
||||
reflow_held: bool,
|
||||
) -> WindowGeometry {
|
||||
@@ -352,7 +346,7 @@ fn predict_geometry(
|
||||
predicted.visual.velocity = Default::default();
|
||||
}
|
||||
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
|
||||
{
|
||||
if !reflow_held {
|
||||
@@ -360,11 +354,6 @@ fn predict_geometry(
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
@@ -916,12 +905,13 @@ mod tests {
|
||||
width.set_target(500.);
|
||||
let spring = SpringConfig::default();
|
||||
let delta = Duration::from_millis(10);
|
||||
let sampled_viewport = ViewportPresentation::from_motion(viewport).sample(delta, spring, true, false);
|
||||
let predicted = predict_geometry(
|
||||
geometry,
|
||||
delta,
|
||||
spring,
|
||||
spring,
|
||||
Some((world, viewport, false)),
|
||||
Some((world, sampled_viewport)),
|
||||
Some(width),
|
||||
false,
|
||||
);
|
||||
|
||||
@@ -7,19 +7,19 @@ struct ClientCommit {
|
||||
serial: Serial,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
struct ViewportCommit {
|
||||
commit: ClientCommit,
|
||||
width: (f64, f64),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, PartialEq)]
|
||||
struct ViewportStep {
|
||||
target: ferese_layout::ViewportTarget,
|
||||
inputs: Vec<ViewportCommit>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
#[derive(Clone, Default, PartialEq)]
|
||||
pub(super) struct ViewportDependencies {
|
||||
steps: Vec<ViewportStep>,
|
||||
waits: Vec<ClientCommit>,
|
||||
@@ -35,10 +35,10 @@ const MAX_VIEWPORT_STEPS: usize = 32;
|
||||
#[derive(Default)]
|
||||
pub(super) struct PresentationDependencies {
|
||||
reflow: HashMap<WindowId, Vec<ClientCommit>>,
|
||||
// Provenance and configure serials only; ViewportPresentation owns holds.
|
||||
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_viewport: HashSet<WorkspaceId>,
|
||||
}
|
||||
|
||||
impl PresentationDependencies {
|
||||
@@ -66,7 +66,7 @@ impl PresentationDependencies {
|
||||
}
|
||||
|
||||
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>(
|
||||
@@ -101,23 +101,15 @@ impl PresentationDependencies {
|
||||
pub(super) fn holds<W: std::hash::Hash + Eq + Clone>(
|
||||
&mut self,
|
||||
windows: &window_registry::WindowRegistry<W>,
|
||||
) -> (HashSet<WindowId>, HashSet<WorkspaceId>) {
|
||||
) -> HashSet<WindowId> {
|
||||
let reflow = self
|
||||
.reflow
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|id| self.reflow_blocked(*id, windows))
|
||||
.collect::<HashSet<_>>();
|
||||
let viewport = self
|
||||
.viewport
|
||||
.keys()
|
||||
.copied()
|
||||
.filter(|workspace| self.viewport_blocked(*workspace, windows))
|
||||
.collect::<HashSet<_>>();
|
||||
let mut held_reflow = std::mem::replace(&mut self.paused_reflow, reflow.clone());
|
||||
let mut held_viewport = std::mem::replace(&mut self.paused_viewport, viewport.clone());
|
||||
held_reflow.extend(reflow);
|
||||
held_viewport.extend(viewport);
|
||||
self.reflow.retain(|_, waits| {
|
||||
waits.retain(|wait| Self::pending(*wait, windows));
|
||||
!waits.is_empty()
|
||||
@@ -132,7 +124,7 @@ impl PresentationDependencies {
|
||||
}
|
||||
!waits.steps.is_empty() || !waits.waits.is_empty()
|
||||
});
|
||||
(held_reflow, held_viewport)
|
||||
held_reflow
|
||||
}
|
||||
}
|
||||
|
||||
@@ -264,13 +256,15 @@ fn viewport_dependencies(
|
||||
impl Ferese {
|
||||
pub(super) fn release_presentation_dependencies(&mut self) {
|
||||
let paused_reflow = self.presentation_dependencies.paused_reflow.clone();
|
||||
let paused_viewport = self.presentation_dependencies.paused_viewport.clone();
|
||||
self.rebuild_presentation_dependencies();
|
||||
self.rebuild_presentation_dependencies_with_release(true);
|
||||
self.presentation_dependencies.paused_reflow.extend(paused_reflow);
|
||||
self.presentation_dependencies.paused_viewport.extend(paused_viewport);
|
||||
}
|
||||
|
||||
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 viewport = HashMap::new();
|
||||
let mut previous_viewport = std::mem::take(&mut self.presentation_dependencies.viewport);
|
||||
@@ -326,8 +320,7 @@ impl Ferese {
|
||||
}
|
||||
for workspace in self.workspaces.iter() {
|
||||
let layout = self
|
||||
.focus_swipe
|
||||
.as_ref()
|
||||
.focus_swipe()
|
||||
.filter(|swipe| swipe.workspace == workspace.id)
|
||||
.and_then(|swipe| swipe.layout.as_ref())
|
||||
.or(match &workspace.layout {
|
||||
@@ -356,11 +349,15 @@ impl Ferese {
|
||||
self.presentation_dependencies
|
||||
.paused_reflow
|
||||
.retain(|id| reflow.contains_key(id));
|
||||
self.presentation_dependencies
|
||||
.paused_viewport
|
||||
.retain(|id| viewport.get(id).is_some_and(|waits| !waits.waits.is_empty()));
|
||||
self.presentation_dependencies.reflow = reflow;
|
||||
self.presentation_dependencies.viewport = viewport;
|
||||
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(b, ColumnWidth::Fixed(300.0)).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &constraints, Some(b))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b))
|
||||
.unwrap();
|
||||
let pending = [(
|
||||
a,
|
||||
@@ -399,7 +396,7 @@ mod tests {
|
||||
assert_eq!(first.waits.len(), 1);
|
||||
layout.slide_focus_from(b, a).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &constraints, Some(a))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(a))
|
||||
.unwrap();
|
||||
assert_eq!(layout.viewport_x(), 50.0);
|
||||
let canceled = viewport_dependencies(&layout, &pending, first);
|
||||
@@ -413,7 +410,7 @@ mod tests {
|
||||
layout.insert(b, Some(a)).unwrap();
|
||||
layout.insert(c, Some(b)).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &constraints, Some(c))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(c))
|
||||
.unwrap();
|
||||
let pending = [
|
||||
(
|
||||
@@ -438,7 +435,7 @@ mod tests {
|
||||
layout.set_column_width(a, ColumnWidth::Fixed(150.0)).unwrap();
|
||||
layout.set_column_width(b, ColumnWidth::Fixed(300.0)).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &constraints, Some(b))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(b))
|
||||
.unwrap();
|
||||
let pending = [(
|
||||
a,
|
||||
@@ -449,7 +446,7 @@ mod tests {
|
||||
let (from, to) = if index % 2 == 0 { (b, a) } else { (a, b) };
|
||||
layout.slide_focus_from(from, to).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &constraints, Some(to))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &constraints, Some(to))
|
||||
.unwrap();
|
||||
graph = viewport_dependencies(&layout, &pending, graph);
|
||||
assert!(graph.steps.len() <= MAX_VIEWPORT_STEPS);
|
||||
@@ -496,16 +493,15 @@ mod tests {
|
||||
assert!(windows.transaction(&a).is_none());
|
||||
assert!(windows.transaction(&b).is_some());
|
||||
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");
|
||||
let (held, _) = dependencies.holds(&windows);
|
||||
let held = dependencies.holds(&windows);
|
||||
assert!(!held.contains(&a));
|
||||
// Unmap removes the serial owner; both properties retire on the tick.
|
||||
windows.remove(&"b");
|
||||
assert!(!dependencies.viewport_blocked(workspace, &windows));
|
||||
let (_, held) = dependencies.holds(&windows);
|
||||
assert!(held.contains(&workspace));
|
||||
assert!(dependencies.holds(&windows).1.is_empty());
|
||||
dependencies.holds(&windows);
|
||||
dependencies.holds(&windows);
|
||||
assert!(!dependencies.needs_tick());
|
||||
assert!(dependencies.reflow.is_empty() && dependencies.viewport.is_empty());
|
||||
}
|
||||
@@ -539,7 +535,7 @@ mod tests {
|
||||
|
||||
layout.focus(ids[4]).unwrap();
|
||||
layout
|
||||
.geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(ids[4]))
|
||||
.resolve_geometry_with_constraints(bounds, gaps, &HashMap::new(), Some(ids[4]))
|
||||
.unwrap();
|
||||
assert_eq!(layout.viewport_x(), 160.0);
|
||||
let widths = [
|
||||
@@ -607,6 +603,31 @@ mod tests {
|
||||
if remaining.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}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -471,10 +471,12 @@ mod tests {
|
||||
let c_workspace = state.output_workspaces.active_workspace(c_id).unwrap();
|
||||
let mut viewport = AnimatedValue::new(0.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();
|
||||
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());
|
||||
state.advance_animations_at(Duration::from_millis(334), now + Duration::from_millis(350));
|
||||
assert!(state.windows.transaction(&slow_id).is_none());
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user