fix overview entrance animation
This commit is contained in:
1 parent
d75c93db85
commit
c1801e1f07
6 files changed
+319
-43
No files matched your search
+32
-20
@@ -16,7 +16,7 @@ use crate::Ferese;
|
|||||||
const OVERVIEW_MARGIN: f64 = 48.0;
|
const OVERVIEW_MARGIN: f64 = 48.0;
|
||||||
const OVERVIEW_GAP: f64 = 40.0;
|
const OVERVIEW_GAP: f64 = 40.0;
|
||||||
// Applied after the configured animation speed, in both live and predicted motion.
|
// Applied after the configured animation speed, in both live and predicted motion.
|
||||||
const OVERVIEW_MOTION_SPEED: f64 = 0.6;
|
pub(crate) const OVERVIEW_MOTION_SPEED: f64 = 0.6;
|
||||||
const WORKSPACE_CARD_GAP: f64 = 8.0;
|
const WORKSPACE_CARD_GAP: f64 = 8.0;
|
||||||
const MAX_PREVIEW_SCALE: f64 = 0.82;
|
const MAX_PREVIEW_SCALE: f64 = 0.82;
|
||||||
|
|
||||||
@@ -804,11 +804,15 @@ impl Ferese {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if self
|
// Consume idle time before creating the new slide. Subsequent
|
||||||
.output_workspaces
|
// clicks then retarget the motion currently visible on this output.
|
||||||
.switch_workspace(output_id, card.workspace)
|
self.advance_animations(std::time::Instant::now());
|
||||||
.is_ok()
|
let previous = self.output_workspaces.active_workspace(output_id);
|
||||||
|
if let Ok(
|
||||||
|
ferese_core::WorkspaceSwitch::Activated(owner) | ferese_core::WorkspaceSwitch::FocusedExisting(owner),
|
||||||
|
) = self.output_workspaces.switch_workspace(output_id, card.workspace)
|
||||||
{
|
{
|
||||||
|
self.update_workspace_slide(output_id, owner, previous, card.workspace, None);
|
||||||
// Existing monitor ownership wins; never steal another output's workspace.
|
// Existing monitor ownership wins; never steal another output's workspace.
|
||||||
self.restore_output_focus();
|
self.restore_output_focus();
|
||||||
self.overview.strip_offsets.clear();
|
self.overview.strip_offsets.clear();
|
||||||
@@ -881,22 +885,30 @@ impl Ferese {
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
let Some(workspace) = self.workspaces.workspace(workspace) else {
|
let mut visible = vec![workspace];
|
||||||
continue;
|
let sliding = self.workspace_slide_workspaces(output_id).collect::<Vec<_>>();
|
||||||
};
|
visible.extend(sliding.iter().copied().filter(|id| *id != workspace));
|
||||||
let windows = workspace
|
for workspace in visible {
|
||||||
.layout
|
let Some(workspace) = self.workspaces.workspace(workspace) else {
|
||||||
.window_ids()
|
continue;
|
||||||
.chain(workspace.floating.iter().copied())
|
};
|
||||||
.filter_map(|id| {
|
let windows = workspace
|
||||||
let normal = self.windows.geometry(&id)?.visual.current;
|
.layout
|
||||||
Some((id, normal))
|
.window_ids()
|
||||||
})
|
.chain(workspace.floating.iter().copied())
|
||||||
.collect::<Vec<_>>();
|
.filter_map(|id| {
|
||||||
|
let normal = self.windows.geometry(&id)?.visual.current;
|
||||||
|
Some((id, normal))
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
for (id, target) in overview_layout(window_area(bounds), &windows) {
|
||||||
let normal = self.windows.geometry(&id).unwrap().visual.current;
|
let normal = self.windows.geometry(&id).unwrap().visual.current;
|
||||||
targets.insert(id, (normal, target));
|
// Incoming grids start at their overview size. Translation
|
||||||
|
// belongs to the workspace slide, not a second desktop zoom.
|
||||||
|
let source = if !sliding.is_empty() { target } else { normal };
|
||||||
|
targets.insert(id, (source, target));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+26
-2
@@ -206,6 +206,7 @@ struct WorkspaceSlideItem {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct WorkspaceSlide {
|
struct WorkspaceSlide {
|
||||||
|
speed: f64,
|
||||||
items: Vec<WorkspaceSlideItem>,
|
items: Vec<WorkspaceSlideItem>,
|
||||||
spring: SpringConfig,
|
spring: SpringConfig,
|
||||||
held_progress: Option<f64>,
|
held_progress: Option<f64>,
|
||||||
@@ -253,6 +254,7 @@ impl WorkspaceSlide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
|
speed: 1.0,
|
||||||
items,
|
items,
|
||||||
spring: SpringConfig {
|
spring: SpringConfig {
|
||||||
stiffness: 320.0,
|
stiffness: 320.0,
|
||||||
@@ -271,6 +273,7 @@ impl WorkspaceSlide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn sample(&self, item: &WorkspaceSlideItem, delta: Duration) -> (SlideOffset, SlideOffset) {
|
fn sample(&self, item: &WorkspaceSlideItem, delta: Duration) -> (SlideOffset, SlideOffset) {
|
||||||
|
let delta = delta.mul_f64(self.speed);
|
||||||
if let Some(progress) = self.held_progress {
|
if let Some(progress) = self.held_progress {
|
||||||
return (item.start.between(item.target, progress), item.velocity);
|
return (item.start.between(item.target, progress), item.velocity);
|
||||||
}
|
}
|
||||||
@@ -292,8 +295,8 @@ impl WorkspaceSlide {
|
|||||||
y: y.current,
|
y: y.current,
|
||||||
},
|
},
|
||||||
SlideOffset {
|
SlideOffset {
|
||||||
x: x.velocity,
|
x: x.velocity * self.speed,
|
||||||
y: y.velocity,
|
y: y.velocity * self.speed,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -332,6 +335,7 @@ impl WorkspaceSlide {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn advance(&mut self, delta: Duration) -> bool {
|
fn advance(&mut self, delta: Duration) -> bool {
|
||||||
|
let delta = delta.mul_f64(self.speed);
|
||||||
if self.held_progress.is_some() {
|
if self.held_progress.is_some() {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1138,6 +1142,26 @@ impl ClientData for ClientState {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn overview_slide_prediction_uses_the_same_clock_as_live_motion() {
|
||||||
|
let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Left);
|
||||||
|
slide.speed = crate::overview::OVERVIEW_MOTION_SPEED;
|
||||||
|
for delta in [
|
||||||
|
Duration::from_millis(8),
|
||||||
|
Duration::from_millis(100),
|
||||||
|
Duration::from_secs(2),
|
||||||
|
] {
|
||||||
|
let mut advanced = slide.clone();
|
||||||
|
advanced.advance(delta);
|
||||||
|
for (item, after) in slide.items.iter().zip(&advanced.items) {
|
||||||
|
let (position, velocity) = slide.sample(item, delta);
|
||||||
|
assert_eq!(position, after.start);
|
||||||
|
assert_eq!(velocity.x, after.velocity.x * slide.speed);
|
||||||
|
assert_eq!(velocity.y, after.velocity.y * slide.speed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn slide_item_sampling_matches_advance_without_mutating_the_slide() {
|
fn slide_item_sampling_matches_advance_without_mutating_the_slide() {
|
||||||
let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Up);
|
let mut slide = WorkspaceSlide::new(None, WorkspaceId(1), WorkspaceId(2), SwipeDirection::Up);
|
||||||
|
|||||||
@@ -500,6 +500,7 @@ impl Ferese {
|
|||||||
self.unmap_invisible_windows(&visible_windows);
|
self.unmap_invisible_windows(&visible_windows);
|
||||||
self.reconcile_workspaces(&visible);
|
self.reconcile_workspaces(&visible);
|
||||||
self.send_shell_snapshots();
|
self.send_shell_snapshots();
|
||||||
|
self.retarget_overview();
|
||||||
active_animation = true;
|
active_animation = true;
|
||||||
}
|
}
|
||||||
if !self.overview.is_active() {
|
if !self.overview.is_active() {
|
||||||
|
|||||||
@@ -502,7 +502,14 @@ impl Ferese {
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(super) fn update_workspace_slide(
|
pub(crate) fn workspace_slide_workspaces(&self, output: OutputId) -> impl Iterator<Item = WorkspaceId> + '_ {
|
||||||
|
self.workspace_slides
|
||||||
|
.get(&output)
|
||||||
|
.into_iter()
|
||||||
|
.flat_map(|slide| slide.items.iter().map(|item| item.workspace))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn update_workspace_slide(
|
||||||
&mut self,
|
&mut self,
|
||||||
requested_output: OutputId,
|
requested_output: OutputId,
|
||||||
owner: OutputId,
|
owner: OutputId,
|
||||||
@@ -539,7 +546,13 @@ impl Ferese {
|
|||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
let from = ids.iter().position(|id| *id == from)?;
|
let from = ids.iter().position(|id| *id == from)?;
|
||||||
let to = ids.iter().position(|id| *id == workspace)?;
|
let to = ids.iter().position(|id| *id == workspace)?;
|
||||||
Some(if to > from {
|
Some(if self.overview.is_active() {
|
||||||
|
if to > from {
|
||||||
|
SwipeDirection::Left
|
||||||
|
} else {
|
||||||
|
SwipeDirection::Right
|
||||||
|
}
|
||||||
|
} else if to > from {
|
||||||
SwipeDirection::Up
|
SwipeDirection::Up
|
||||||
} else {
|
} else {
|
||||||
SwipeDirection::Down
|
SwipeDirection::Down
|
||||||
@@ -549,10 +562,15 @@ impl Ferese {
|
|||||||
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
&& let (Some(from), Some(direction)) = (previous, slide_direction)
|
||||||
&& from != workspace
|
&& from != workspace
|
||||||
&& self.animations_enabled
|
&& self.animations_enabled
|
||||||
&& !self.overview.is_presenting()
|
&& (!self.overview.is_presenting() || self.overview.is_active())
|
||||||
{
|
{
|
||||||
self.last_animation_tick = Instant::now();
|
self.last_animation_tick = Instant::now();
|
||||||
let mut slide = WorkspaceSlide::new(previous_slide, from, workspace, direction);
|
let mut slide = WorkspaceSlide::new(previous_slide, from, workspace, direction);
|
||||||
|
slide.speed = if self.overview.is_active() {
|
||||||
|
crate::overview::OVERVIEW_MOTION_SPEED
|
||||||
|
} else {
|
||||||
|
1.0
|
||||||
|
};
|
||||||
slide.spring = SpringConfig {
|
slide.spring = SpringConfig {
|
||||||
position_tolerance: 0.00001,
|
position_tolerance: 0.00001,
|
||||||
velocity_tolerance: 0.00001,
|
velocity_tolerance: 0.00001,
|
||||||
|
|||||||
@@ -38,22 +38,22 @@ impl Ferese {
|
|||||||
) -> crate::presentation::WindowPresentation {
|
) -> crate::presentation::WindowPresentation {
|
||||||
let record = self.windows.record(id).expect("presentation has a window record");
|
let record = self.windows.record(id).expect("presentation has a window record");
|
||||||
let mut bounds = overview.presented_bounds(id, geometry.visual);
|
let mut bounds = overview.presented_bounds(id, geometry.visual);
|
||||||
if !overview.is_presenting() {
|
bounds.current.x += offset.0;
|
||||||
bounds.current.x += offset.0;
|
bounds.current.y += offset.1;
|
||||||
bounds.current.y += offset.1;
|
if let Some(workspace) = self.workspaces.workspace_for_window(id)
|
||||||
if let Some(workspace) = self.workspaces.workspace_for_window(id)
|
&& let Some((output_id, slide)) = self
|
||||||
&& let Some(output_id) = self.output_workspaces.output_for_workspace(workspace)
|
.workspace_slides
|
||||||
&& let Some(slide) = self.workspace_slides.get(&output_id)
|
.iter()
|
||||||
&& let Some(item) = slide.items.iter().find(|item| item.workspace == workspace)
|
.find(|(_, slide)| slide.contains(workspace))
|
||||||
&& let Some(area) = self
|
&& let Some(item) = slide.items.iter().find(|item| item.workspace == workspace)
|
||||||
.outputs_by_id
|
&& let Some(area) = self
|
||||||
.get(&output_id)
|
.outputs_by_id
|
||||||
.and_then(|output| self.space.output_geometry(output))
|
.get(output_id)
|
||||||
{
|
.and_then(|output| self.space.output_geometry(output))
|
||||||
let (_, velocity) = slide.sample(item, delta);
|
{
|
||||||
bounds.velocity.x += velocity.x * f64::from(area.size.w);
|
let (_, velocity) = slide.sample(item, delta);
|
||||||
bounds.velocity.y += velocity.y * f64::from(area.size.h);
|
bounds.velocity.x += velocity.x * f64::from(area.size.w);
|
||||||
}
|
bounds.velocity.y += velocity.y * f64::from(area.size.h);
|
||||||
}
|
}
|
||||||
let focused = if self.overview.is_active() {
|
let focused = if self.overview.is_active() {
|
||||||
self.overview_selected(id)
|
self.overview_selected(id)
|
||||||
@@ -339,6 +339,129 @@ fn predict_geometry(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||||
|
fn overview_strip_switch_slides_stable_grids_and_keeps_strip_fixed() {
|
||||||
|
let runtime = std::path::PathBuf::from(std::env::var_os("XDG_RUNTIME_DIR").unwrap());
|
||||||
|
assert!(runtime.starts_with(std::env::temp_dir()));
|
||||||
|
let mut event_loop = EventLoop::try_new().unwrap();
|
||||||
|
let mut state = Ferese::new(
|
||||||
|
&mut event_loop,
|
||||||
|
Display::new().unwrap(),
|
||||||
|
crate::config::Config::default().runtime_config().unwrap(),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let output = Output::new(
|
||||||
|
"overview-slide-test".into(),
|
||||||
|
smithay::output::PhysicalProperties {
|
||||||
|
size: (0, 0).into(),
|
||||||
|
subpixel: smithay::output::Subpixel::Unknown,
|
||||||
|
make: "test".into(),
|
||||||
|
model: "test".into(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
output.change_current_state(
|
||||||
|
Some(smithay::output::Mode {
|
||||||
|
size: (1200, 800).into(),
|
||||||
|
refresh: 120_000,
|
||||||
|
}),
|
||||||
|
Some(smithay::utils::Transform::Normal),
|
||||||
|
None,
|
||||||
|
Some((0, 0).into()),
|
||||||
|
);
|
||||||
|
state.space.map_output(&output, (0, 0));
|
||||||
|
state.register_output(&output, "overview-slide-test".into());
|
||||||
|
let output_id = state.output_id(&output).unwrap();
|
||||||
|
let from = state.output_workspaces.active_workspace(output_id).unwrap();
|
||||||
|
let to = state.workspaces.create_workspace();
|
||||||
|
state.output_workspaces.assign_workspace(output_id, to).unwrap();
|
||||||
|
for (id, workspace) in [(WindowId(1), from), (WindowId(2), to)] {
|
||||||
|
let normal = Rect::new(100., 160., 640., 480.);
|
||||||
|
state
|
||||||
|
.workspaces
|
||||||
|
.insert_floating_window(id, workspace, normal, false)
|
||||||
|
.unwrap();
|
||||||
|
state.windows.records.insert(
|
||||||
|
id,
|
||||||
|
super::super::window_registry::WindowRecord {
|
||||||
|
geometry: Some(WindowGeometry::new(normal, None)),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
state.set_overview_active(true);
|
||||||
|
state.advance_animations_by(Duration::from_secs(10));
|
||||||
|
let old = state.current_window_presentation(WindowId(1)).unwrap().bounds.current;
|
||||||
|
let cards = state.overview_workspace_cards(&output);
|
||||||
|
let card = cards.iter().find(|card| card.workspace == to).unwrap();
|
||||||
|
assert!(
|
||||||
|
state.click_overview_workspace(
|
||||||
|
(
|
||||||
|
card.rect.x + card.rect.width * 0.5,
|
||||||
|
card.rect.y + card.rect.height * 0.5
|
||||||
|
)
|
||||||
|
.into()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
assert!(state.workspace_slides.contains_key(&output_id));
|
||||||
|
assert!(state.window_belongs_to_output(WindowId(1), &output));
|
||||||
|
let incoming = state.current_window_presentation(WindowId(2)).unwrap().bounds.current;
|
||||||
|
assert_eq!(incoming.width, old.width);
|
||||||
|
assert_eq!(incoming.height, old.height);
|
||||||
|
assert!(incoming.x > old.x + 1000.);
|
||||||
|
let predicted = state.sample_frame(&output, Duration::from_millis(60));
|
||||||
|
assert!(predicted.windows[&WindowId(1)].presentation.bounds.current.x < old.x);
|
||||||
|
assert!(predicted.windows[&WindowId(2)].presentation.bounds.current.x < incoming.x);
|
||||||
|
assert_eq!(
|
||||||
|
predicted.windows[&WindowId(2)].presentation.bounds.current.width,
|
||||||
|
incoming.width
|
||||||
|
);
|
||||||
|
// Activation can add a trailing empty workspace. Once that layout is
|
||||||
|
// established, slide prediction must not translate the strip or previews.
|
||||||
|
let current_cards = state.overview_workspace_cards(&output);
|
||||||
|
let predicted_cards = state.overview_workspace_cards_for_frame(&output, Some(&predicted));
|
||||||
|
for (before, after) in current_cards.iter().zip(predicted_cards) {
|
||||||
|
assert_eq!(before.rect, after.rect);
|
||||||
|
assert_eq!(before.windows, after.windows);
|
||||||
|
}
|
||||||
|
state.advance_animations_by(Duration::from_millis(80));
|
||||||
|
let before = state.workspace_slides[&output_id].clone();
|
||||||
|
state.update_workspace_slide(output_id, output_id, Some(to), from, None);
|
||||||
|
let reversed = &state.workspace_slides[&output_id];
|
||||||
|
assert_eq!(reversed.speed, crate::overview::OVERVIEW_MOTION_SPEED);
|
||||||
|
for item in &before.items {
|
||||||
|
let after = reversed
|
||||||
|
.items
|
||||||
|
.iter()
|
||||||
|
.find(|other| other.workspace == item.workspace)
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(item.start, after.start);
|
||||||
|
assert_eq!(item.velocity, after.velocity);
|
||||||
|
}
|
||||||
|
// Finish on the selected workspace and retire all outgoing previews.
|
||||||
|
state.output_workspaces.switch_workspace(output_id, from).unwrap();
|
||||||
|
state.retarget_overview();
|
||||||
|
state.advance_animations_by(Duration::from_secs(10));
|
||||||
|
assert!(state.workspace_slides.is_empty());
|
||||||
|
assert!(!state.overview.has_window_preview(WindowId(2)));
|
||||||
|
assert!(!state.overview.needs_tick());
|
||||||
|
state.animations_enabled = false;
|
||||||
|
let card = state
|
||||||
|
.overview_workspace_cards(&output)
|
||||||
|
.into_iter()
|
||||||
|
.find(|card| card.workspace == to)
|
||||||
|
.unwrap();
|
||||||
|
state.click_overview_workspace(
|
||||||
|
(
|
||||||
|
card.rect.x + card.rect.width * 0.5,
|
||||||
|
card.rect.y + card.rect.height * 0.5,
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
assert!(state.workspace_slides.is_empty());
|
||||||
|
assert!(!state.overview.needs_tick());
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
#[ignore = "requires a private XDG_RUNTIME_DIR and permission to bind test sockets"]
|
||||||
fn frame_sampling_preserves_compositor_state_between_output_deadlines() {
|
fn frame_sampling_preserves_compositor_state_between_output_deadlines() {
|
||||||
|
|||||||
@@ -9,6 +9,16 @@ use super::{Menu, OpenMenu};
|
|||||||
use crate::status::Action;
|
use crate::status::Action;
|
||||||
use crate::{FereseShell, Message};
|
use crate::{FereseShell, Message};
|
||||||
|
|
||||||
|
impl OpenMenu {
|
||||||
|
fn switch_panel(&mut self, kind: Menu, settings: crate::motion::Settings, now: Instant) {
|
||||||
|
self.kind = kind;
|
||||||
|
// Keep the Wayland popup and its material binding, but give the new
|
||||||
|
// panel its own entrance instead of reusing the settled old panel.
|
||||||
|
self.motion = crate::motion::PopupMotion::new(settings);
|
||||||
|
self.motion.begin(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl FereseShell {
|
impl FereseShell {
|
||||||
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
|
pub fn open_menu(&mut self, kind: Menu, anchor: Rectangle<i32>) -> Task<Message> {
|
||||||
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
|
if self.menu.as_ref().is_some_and(|menu| menu.kind == kind) {
|
||||||
@@ -77,8 +87,7 @@ impl FereseShell {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
if let Some(menu) = &mut self.menu {
|
if let Some(menu) = &mut self.menu {
|
||||||
menu.kind = kind;
|
menu.switch_panel(kind, self.config.animations, Instant::now());
|
||||||
menu.motion.retarget(1.0, Instant::now());
|
|
||||||
return Task::batch([
|
return Task::batch([
|
||||||
notifications,
|
notifications,
|
||||||
cosmic::iced::platform_specific::shell::commands::popup::reposition(menu.id, positioner),
|
cosmic::iced::platform_specific::shell::commands::popup::reposition(menu.id, positioner),
|
||||||
@@ -184,3 +193,92 @@ impl FereseShell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::motion::{PopupMotion, Settings};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn switching_panels_replays_entrance_on_the_same_surface() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let settings = Settings::default();
|
||||||
|
let mut menu = OpenMenu {
|
||||||
|
id: window::Id::unique(),
|
||||||
|
kind: Menu::Battery,
|
||||||
|
motion: PopupMotion::new(settings),
|
||||||
|
effects: None,
|
||||||
|
regions: Default::default(),
|
||||||
|
};
|
||||||
|
let id = menu.id;
|
||||||
|
let regions = menu.regions.clone();
|
||||||
|
menu.motion.begin(now);
|
||||||
|
for (index, kind) in [
|
||||||
|
Menu::Network,
|
||||||
|
Menu::Bluetooth,
|
||||||
|
Menu::Audio,
|
||||||
|
Menu::Calendar,
|
||||||
|
Menu::Recording,
|
||||||
|
Menu::Notifications,
|
||||||
|
Menu::System,
|
||||||
|
Menu::Battery,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.enumerate()
|
||||||
|
{
|
||||||
|
let at = now + Duration::from_secs(10 * (index as u64 + 1));
|
||||||
|
assert_eq!(menu.motion.progress_at(at), 1.0);
|
||||||
|
menu.switch_panel(kind, settings, at);
|
||||||
|
assert_eq!(menu.kind, kind);
|
||||||
|
assert_eq!(menu.id, id);
|
||||||
|
assert!(std::sync::Arc::ptr_eq(®ions, &menu.regions));
|
||||||
|
assert_eq!(menu.motion.progress_at(at), 0.0);
|
||||||
|
assert!(menu.motion.frame_active(at));
|
||||||
|
let progress = menu.motion.progress_at(at + Duration::from_millis(30));
|
||||||
|
assert!(progress > 0.0 && progress < 1.0);
|
||||||
|
}
|
||||||
|
let at = now + Duration::from_secs(90);
|
||||||
|
menu.motion.retarget(0.0, at);
|
||||||
|
menu.switch_panel(Menu::Network, settings, at + Duration::from_millis(30));
|
||||||
|
assert!(!menu.motion.closing());
|
||||||
|
assert!(menu.motion.frame_active(at + Duration::from_millis(30)));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn panel_switch_respects_reduced_motion_and_speed() {
|
||||||
|
let now = Instant::now();
|
||||||
|
let mut menu = OpenMenu {
|
||||||
|
id: window::Id::unique(),
|
||||||
|
kind: Menu::Battery,
|
||||||
|
motion: PopupMotion::new(Settings::default()),
|
||||||
|
effects: None,
|
||||||
|
regions: Default::default(),
|
||||||
|
};
|
||||||
|
for settings in [
|
||||||
|
Settings {
|
||||||
|
reduced_motion: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
Settings {
|
||||||
|
enabled: false,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
] {
|
||||||
|
menu.switch_panel(Menu::Network, settings, now);
|
||||||
|
assert_eq!(menu.motion.progress_at(now), 1.0);
|
||||||
|
assert!(!menu.motion.frame_active(now));
|
||||||
|
}
|
||||||
|
menu.switch_panel(Menu::Battery, Settings::default(), now);
|
||||||
|
let normal = menu.motion.progress_at(now + Duration::from_millis(30));
|
||||||
|
menu.switch_panel(
|
||||||
|
Menu::Network,
|
||||||
|
Settings {
|
||||||
|
speed: 0.5,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
now,
|
||||||
|
);
|
||||||
|
assert_eq!(menu.motion.progress_at(now + Duration::from_millis(60)), normal);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user