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;
|
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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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,17 +242,13 @@ 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()
|
let mut width = *width;
|
||||||
.is_some_and(|swipe| swipe.workspace == workspace);
|
width.advance(Duration::ZERO, self.viewport_spring_config)
|
||||||
!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)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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,
|
||||||
.viewport_blocked(*workspace, &self.windows),
|
self.animations_enabled,
|
||||||
|
self.presentation_dependencies
|
||||||
|
.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}"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
|||||||
@@ -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,50 +654,60 @@ 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() {
|
||||||
if let Some(active) = self.active_column {
|
Some(PendingViewportRequest::WidthCycle(_)) => {
|
||||||
self.record_viewport_basis(
|
if let Some(active) = self.active_column {
|
||||||
&column_positions,
|
self.record_viewport_basis(
|
||||||
inner,
|
&column_positions,
|
||||||
viewport_width,
|
inner,
|
||||||
ViewportRequest::WidthCycle(active),
|
viewport_width,
|
||||||
);
|
ViewportRequest::WidthCycle(active),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
self.retarget_after_width_cycle(&column_positions, viewport_width);
|
||||||
}
|
}
|
||||||
self.retarget_after_width_cycle(&column_positions, viewport_width);
|
Some(PendingViewportRequest::Focus(PendingFocusRequest::Slide {
|
||||||
self.width_cycle_pending = false;
|
from: previous,
|
||||||
self.reveal_pending = None;
|
to: focused,
|
||||||
self.swipe_focus_pending = None;
|
})) => {
|
||||||
} else if let Some((previous, focused)) = self.swipe_focus_pending.take() {
|
if let (Some((from, _)), Some((to, _))) =
|
||||||
if let (Some((from, _)), Some((to, _))) = (self.window_location(previous), self.window_location(focused)) {
|
(self.window_location(previous), self.window_location(focused))
|
||||||
self.record_viewport_basis(
|
{
|
||||||
&column_positions,
|
self.record_viewport_basis(
|
||||||
inner,
|
&column_positions,
|
||||||
viewport_width,
|
inner,
|
||||||
ViewportRequest::Slide(from, to),
|
viewport_width,
|
||||||
);
|
ViewportRequest::Slide(from, to),
|
||||||
let last_end = column_positions
|
);
|
||||||
.last()
|
let last_end = column_positions
|
||||||
.map(|(start, width)| start + width)
|
.last()
|
||||||
.unwrap_or(0.0);
|
.map(|(start, width)| start + width)
|
||||||
let max_offset = (last_end - viewport_width).max(0.0);
|
.unwrap_or(0.0);
|
||||||
self.viewport_x =
|
let max_offset = (last_end - viewport_width).max(0.0);
|
||||||
(self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset);
|
self.viewport_x =
|
||||||
|
(self.viewport_x + column_positions[to].0 - column_positions[from].0).clamp(0.0, max_offset);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
self.reveal_pending = None;
|
request => {
|
||||||
} else {
|
let reveal = match request {
|
||||||
let reveal = self.reveal_pending.take().or_else(|| {
|
Some(PendingViewportRequest::Focus(PendingFocusRequest::Reveal(window))) => Some(window),
|
||||||
(viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged)
|
None => None,
|
||||||
.then(|| self.active_window())
|
_ => unreachable!(),
|
||||||
.flatten()
|
}
|
||||||
});
|
.or_else(|| {
|
||||||
if let Some((column, _)) = reveal.and_then(|window| self.window_location(window)) {
|
(viewport_resized && self.focus_strategy == ViewportFocusStrategy::Paged)
|
||||||
self.record_viewport_basis(
|
.then(|| self.active_window())
|
||||||
&column_positions,
|
.flatten()
|
||||||
inner,
|
});
|
||||||
viewport_width,
|
if let Some((column, _)) = reveal.and_then(|window| self.window_location(window)) {
|
||||||
ViewportRequest::Reveal(column),
|
self.record_viewport_basis(
|
||||||
);
|
&column_positions,
|
||||||
self.reveal_column(&column_positions, viewport_width, column);
|
inner,
|
||||||
|
viewport_width,
|
||||||
|
ViewportRequest::Reveal(column),
|
||||||
|
);
|
||||||
|
self.reveal_column(&column_positions, viewport_width, column);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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)];
|
||||||
|
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in new issue
Block a user