use std::sync::Arc;
use bevy::prelude::*;
use objc2_core_foundation::CGPoint;
use crate::commands::{Command, Direction, MoveFocus, Operation};
use crate::config::{Config, MainOptions, WindowParams};
use crate::ecs::display::FloatingLayer;
use crate::ecs::{
ActiveWorkspaceMarker, FocusedMarker, NativeFullscreenMarker, Position, Unmanaged,
layout::LayoutStrip,
};
use crate::ecs::{RepositionMarker, Scrolling, SpawnWindowTrigger};
use crate::events::Event;
use crate::manager::{Origin, Size, Window};
use crate::platform::Modifiers;
use crate::{assert_focused, assert_window_at, assert_window_size};
use super::*;
#[test]
fn modifier_scroll_uses_native_momentum_without_synthetic_velocity() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Scroll { delta: 1.0 },
];
TestHarness::new()
.with_windows(3)
.on_iteration(1, |world, _state| {
let mut query = world.query_filtered::<&Scrolling, With<ActiveWorkspaceMarker>>();
let scrolling = query.single(world).expect("active workspace is scrolling");
assert!(scrolling.velocity.abs() < 0.0001);
assert!(scrolling.is_user_swiping);
})
.run(commands);
}
#[test]
fn native_fullscreen_transition_removes_window_from_original_strip_without_focus_marker() {
const FULLSCREEN_WORKSPACE_ID: WorkspaceId = TEST_WORKSPACE_ID + 100;
TestHarness::new()
.with_windows(2)
.on_iteration(0, |world, state| {
let focused = world
.query_filtered::<Entity, With<FocusedMarker>>()
.iter(world)
.collect::<Vec<_>>();
for entity in focused {
world.entity_mut(entity).remove::<FocusedMarker>();
}
state.update_window(0, |window| {
window.workspace_id = FULLSCREEN_WORKSPACE_ID;
window.is_full_screen = true;
});
state.activate_workspace(TEST_DISPLAY_ID, FULLSCREEN_WORKSPACE_ID, true);
})
.on_iteration(1, |world, _state| {
let fullscreen_window = find_window_entity(0, world);
let sibling_window = find_window_entity(1, world);
let mut strips = world.query::<(&LayoutStrip, Option<&NativeFullscreenMarker>)>();
let original_strip = strips
.iter(world)
.find_map(|(strip, marker)| {
(strip.id() == TEST_WORKSPACE_ID && marker.is_none()).then_some(strip)
})
.expect("original strip");
assert!(
!original_strip.contains(fullscreen_window),
"fullscreen window must not leave a reserved column in the original strip"
);
assert!(original_strip.contains(sibling_window));
let (fullscreen_strip, fullscreen_marker) = strips
.iter(world)
.find(|(strip, _)| strip.id() == FULLSCREEN_WORKSPACE_ID)
.expect("fullscreen strip");
assert!(fullscreen_strip.contains(fullscreen_window));
assert!(fullscreen_marker.is_some());
})
.on_iteration(2, |world, _state| {
let fullscreen_window = find_window_entity(0, world);
let sibling_window = find_window_entity(1, world);
let mut strips = world.query::<&LayoutStrip>();
let original_strip = strips
.iter(world)
.find(|strip| strip.id() == TEST_WORKSPACE_ID)
.expect("original strip");
assert!(original_strip.contains(fullscreen_window));
assert!(original_strip.contains(sibling_window));
assert_eq!(
original_strip
.index_of(fullscreen_window)
.expect("restored fullscreen window index"),
0
);
assert!(
strips
.iter(world)
.all(|strip| strip.id() != FULLSCREEN_WORKSPACE_ID)
);
})
.run(vec![
Event::Command {
command: Command::PrintState,
},
Event::SpaceChanged,
Event::SpaceDestroyed {
space_id: FULLSCREEN_WORKSPACE_ID,
},
]);
}
#[test]
fn frontmost_floating_window_is_focused_after_setup() {
let mut params = WindowParams::new(".*", None);
params.floating = Some(true);
let config: Config = (MainOptions::default(), vec![params]).into();
TestHarness::new()
.with_config(config)
.with_windows(1)
.with_focused_window(0)
.on_iteration(0, |world, _state| {
assert_focused!(world, 0);
let entity = find_window_entity(0, world);
assert!(world.entity(entity).contains::<Unmanaged>());
})
.run(vec![Event::MenuOpened { window_id: 0 }]);
}
#[test]
fn floating_grid_window_uses_active_display_usable_origin() {
let options = MainOptions {
padding_left: Some(40),
padding_top: Some(15),
..MainOptions::default()
};
let mut params = WindowParams::new(".*", None);
params.floating = Some(true);
params.grid = Some("1:1:0:0:1:1".to_string());
let config: Config = (options, vec![params]).into();
TestHarness::new()
.with_config(config)
.on_iteration(1, |world, state| {
let origin = Origin::new(0, 0);
let size = Size::new(TEST_WINDOW_WIDTH, TEST_WINDOW_HEIGHT);
let frame = IRect::from_corners(origin, origin + size);
let window = state.spawn_window(TEST_PROCESS_ID, TEST_WORKSPACE_ID, 0, frame);
world.trigger(SpawnWindowTrigger(vec![window]));
})
.on_iteration(3, |world, _state| {
assert_window_at!(world, 0, 40, TEST_MENUBAR_HEIGHT + 15);
})
.run(vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::PrintState,
},
]);
}
#[test]
fn test_dont_focus() {
let commands = vec![
Event::MenuOpened { window_id: 0 }, Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
}, Event::Command {
command: Command::Window(Operation::Focus(Direction::First)),
}, Event::Command {
command: Command::PrintState,
}, ];
let offscreen_right = TEST_DISPLAY_WIDTH - 5;
let mut params = WindowParams::new(".*", None);
params.dont_focus = Some(true);
params.index = Some(100);
let config: Config = (MainOptions::default(), vec![params]).into();
let harness = TestHarness::new().with_config(config).with_windows(3);
harness
.on_iteration(1, move |world, state| {
let origin = Origin::new(0, 0);
let size = Size::new(TEST_WINDOW_WIDTH, TEST_WINDOW_HEIGHT);
let frame = IRect::from_corners(origin, origin + size);
let window = state.spawn_window(TEST_PROCESS_ID, TEST_WORKSPACE_ID, 3, frame);
world.trigger(SpawnWindowTrigger(vec![window]));
})
.on_iteration(3, move |world, _| {
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 1, 400, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 2, 800, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 3, offscreen_right, TEST_MENUBAR_HEIGHT);
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn test_offscreen_windows_preserve_height() {
let expected_height = TEST_DISPLAY_HEIGHT - TEST_MENUBAR_HEIGHT;
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::First)),
},
];
TestHarness::new()
.with_windows(5)
.on_iteration(1, move |world, _state| {
assert_window_size!(world, 4, TEST_WINDOW_WIDTH, expected_height);
assert_window_size!(world, 3, TEST_WINDOW_WIDTH, expected_height);
assert_window_size!(world, 2, TEST_WINDOW_WIDTH, expected_height);
assert_window_size!(world, 1, TEST_WINDOW_WIDTH, expected_height);
assert_window_size!(world, 0, TEST_WINDOW_WIDTH, expected_height);
})
.run(commands);
}
#[test]
fn test_sliver_smaller_than_edge_padding() {
const PADDING: u16 = 8;
const SLIVER: u16 = 1;
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
},
Event::Command {
command: Command::Window(Operation::Focus(Direction::First)),
},
Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
},
];
let top_edge = TEST_MENUBAR_HEIGHT + i32::from(PADDING);
let right_edge = TEST_DISPLAY_WIDTH - i32::from(PADDING);
let offscreen_right = TEST_DISPLAY_WIDTH - i32::from(SLIVER);
let offscreen_left = i32::from(SLIVER) - TEST_WINDOW_WIDTH;
let left_edge = i32::from(PADDING);
let config: Config = (
MainOptions {
sliver_width: Some(SLIVER),
padding_top: Some(PADDING),
padding_bottom: Some(PADDING),
padding_left: Some(PADDING),
padding_right: Some(PADDING),
..Default::default()
},
vec![],
)
.into();
TestHarness::new()
.with_config(config)
.with_windows(5)
.on_iteration(2, move |world, _state| {
assert_window_at!(world, 0, left_edge, top_edge);
assert_window_at!(world, 1, left_edge + TEST_WINDOW_WIDTH, top_edge);
assert_window_at!(world, 2, left_edge + 2 * TEST_WINDOW_WIDTH, top_edge);
assert_window_at!(world, 3, offscreen_right, top_edge);
assert_window_at!(world, 4, offscreen_right, top_edge);
})
.on_iteration(3, move |world, _state| {
assert_window_at!(world, 0, offscreen_left, top_edge);
assert_window_at!(world, 1, offscreen_left, top_edge);
assert_window_at!(world, 2, right_edge - 3 * TEST_WINDOW_WIDTH, top_edge);
assert_window_at!(world, 3, right_edge - 2 * TEST_WINDOW_WIDTH, top_edge);
assert_window_at!(world, 4, right_edge - TEST_WINDOW_WIDTH, top_edge);
})
.run(commands);
}
#[test]
fn test_scrolling() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
},
Event::Command {
command: Command::Window(Operation::Focus(Direction::First)),
},
Event::Command {
command: Command::PrintState,
},
Event::Swipe {
delta: 0.2,
fingers: 3,
},
Event::Command {
command: Command::PrintState,
},
];
let config: Config = (
MainOptions {
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
TestHarness::new()
.with_config(config)
.with_windows(3)
.on_iteration(3, move |world, _state| {
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 1, 400, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 2, 800, TEST_MENUBAR_HEIGHT);
})
.on_iteration(5, move |world, _state| {
assert_window_at!(world, 0, -395, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 1, -382, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 2, 18, TEST_MENUBAR_HEIGHT);
})
.run(commands);
}
#[test]
#[allow(clippy::float_cmp)]
fn test_scrolling_stop() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Swipe {
delta: 0.3,
fingers: 3,
},
Event::TouchpadDown,
];
let config: Config = (
MainOptions {
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
TestHarness::new()
.with_config(config)
.with_windows(3)
.on_iteration(3, |world, _state| {
use crate::ecs::Scrolling;
let mut query = world.query::<&Scrolling>();
let scroll = query.single(world).unwrap();
assert_eq!(scroll.velocity, 0.0);
assert!(scroll.is_user_swiping);
})
.run(commands);
}
#[test]
fn test_window_hidden_ratio() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Swipe {
delta: 0.3,
fingers: 3,
},
Event::Command {
command: Command::Window(Operation::Focus(Direction::First)),
},
];
let config: Config = (
MainOptions {
window_hidden_ratio: Some(0.5),
animation_speed: Some(10000.0),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
TestHarness::new()
.with_config(config)
.with_windows(2)
.on_iteration(2, |world, _state| {
let entity = find_window_entity(0, world);
let window = world.get::<Window>(entity).expect("finding window");
assert!(window.frame().min.x < 0);
})
.run(commands);
}
#[test]
fn test_window_swap_brings_focused_into_view() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::Window(Operation::Center),
},
Event::Command {
command: Command::Window(Operation::Swap(Direction::Last)),
},
Event::Command {
command: Command::PrintState,
},
];
let config: Config = (
MainOptions {
animation_speed: Some(10000.0),
..Default::default()
},
vec![],
)
.into();
let centered = (TEST_DISPLAY_WIDTH - TEST_WINDOW_WIDTH) / 2;
let right_edge = TEST_DISPLAY_WIDTH - TEST_WINDOW_WIDTH;
TestHarness::new()
.with_config(config)
.with_windows(5)
.on_iteration(2, move |world, _state| {
assert_window_at!(world, 0, centered, TEST_MENUBAR_HEIGHT);
})
.on_iteration(4, move |world, _state| {
assert_window_at!(world, 0, right_edge, TEST_MENUBAR_HEIGHT);
assert_window_at!(
world,
4,
right_edge - TEST_WINDOW_WIDTH,
TEST_MENUBAR_HEIGHT
);
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn test_window_swap_keeps_strip_when_in_view() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
},
Event::Command {
command: Command::Window(Operation::Swap(Direction::West)),
},
];
let config: Config = (
MainOptions {
animation_speed: Some(10000.0),
..Default::default()
},
vec![],
)
.into();
TestHarness::new()
.with_config(config)
.with_windows(2)
.on_iteration(2, |world, _state| {
assert_window_at!(world, 1, 0, TEST_MENUBAR_HEIGHT);
assert_window_at!(world, 0, TEST_WINDOW_WIDTH, TEST_MENUBAR_HEIGHT);
assert_focused!(world, 1);
})
.run(commands);
}
#[test]
fn test_rapid_focus_not_swallowed() {
let mut harness = TestHarness::new().with_windows(5);
harness.run(vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::Last)),
},
Event::Command {
command: Command::PrintState,
},
]);
assert_focused!(harness.world(), 4);
let focus_west = Event::Command {
command: Command::Window(Operation::Focus(Direction::West)),
};
for _ in 0..3 {
harness
.app
.world_mut()
.write_message::<Event>(focus_west.clone());
harness.app.update();
}
assert_focused!(harness.world(), 1);
}
#[test]
fn test_stale_focus_event_ignored() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::East)),
},
Event::WindowFocused { window_id: 4 },
Event::Command {
command: Command::PrintState,
},
];
TestHarness::new()
.with_windows(5)
.on_iteration(1, |world, _state| {
assert_focused!(world, 1);
})
.on_iteration(2, |world, _state| {
assert_focused!(world, 1);
})
.run(commands);
}
#[test]
fn test_repeated_external_focus_reshuffles_already_focused_window() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::PrintState,
},
];
TestHarness::new()
.with_windows(5)
.on_iteration(1, |world, _state| {
assert_focused!(world, 0);
let mut query = world.query::<(Entity, &LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let (entity, _, _) = query
.iter(world)
.find(|(_, _, active)| *active)
.expect("active strip");
world.commands().entity(entity).insert((
Position(Origin::new(0, 0)),
RepositionMarker(Origin::new(-TEST_DISPLAY_WIDTH, 0)),
));
})
.on_iteration(2, |_world, state| {
state.focus_window(0);
})
.on_iteration(4, |world, _state| {
assert_focused!(world, 0);
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
})
.run(commands);
}
#[test]
fn test_external_focus_reactivates_hidden_virtual_strip_when_marker_is_stale() {
let commands = vec![
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::Window(Operation::VirtualNumber(1)),
},
Event::WindowFocused { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
];
TestHarness::new()
.with_windows(1)
.on_iteration(1, |world, _state| {
let mut query = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let active = query
.iter(world)
.find_map(|(strip, active)| active.then_some(strip.virtual_index))
.expect("an active virtual strip");
assert_eq!(active, 1);
assert_focused!(world, 0);
})
.on_iteration(3, |world, _state| {
let mut query = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let active = query
.iter(world)
.find_map(|(strip, active)| active.then_some(strip.virtual_index))
.expect("an active virtual strip");
assert_eq!(active, 0);
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
assert_focused!(world, 0);
})
.run(commands);
}
fn focused_window_id(world: &mut World) -> i32 {
let mut q = world.query::<(&Window, Has<crate::ecs::FocusedMarker>)>();
q.iter(world)
.find_map(|(w, f)| f.then_some(w.id()))
.expect("a focused window")
}
fn entity_to_window_id(world: &mut World, entity: Entity) -> i32 {
let mut q = world.query::<(&Window, Entity)>();
q.iter(world)
.find_map(|(w, e)| (e == entity).then_some(w.id()))
.expect("entity must be a Window")
}
fn active_strip_first_id(world: &mut World) -> i32 {
let entity = {
let mut q = world.query_filtered::<&LayoutStrip, With<ActiveWorkspaceMarker>>();
let strip = q.single(world).expect("a single active strip");
strip
.first()
.expect("strip should have a column")
.top()
.expect("column should have a top entity")
};
entity_to_window_id(world, entity)
}
fn active_strip_last_id(world: &mut World) -> i32 {
let entity = {
let mut q = world.query_filtered::<&LayoutStrip, With<ActiveWorkspaceMarker>>();
let strip = q.single(world).expect("a single active strip");
strip
.last()
.expect("strip should have a column")
.top()
.expect("column should have a top entity")
};
entity_to_window_id(world, entity)
}
fn remove_focused_from_all_strips(world: &mut World) {
let entity = {
let mut q = world.query_filtered::<Entity, With<crate::ecs::FocusedMarker>>();
q.single(world).expect("a single focused entity")
};
let mut q = world.query::<&mut LayoutStrip>();
for mut strip in q.iter_mut(world) {
while strip.contains(entity) {
strip.remove(entity);
}
}
}
#[test]
fn test_focus_recovers_when_focused_window_is_outside_strip() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::East)),
},
];
TestHarness::new()
.with_windows(3)
.on_iteration(0, |world, _state| {
remove_focused_from_all_strips(world);
})
.on_iteration(1, |world, _state| {
let focused = focused_window_id(world);
assert_ne!(
focused, 0,
"focus must leave the off-strip window 0 when pressing East",
);
let expected = active_strip_first_id(world);
assert_eq!(
focused, expected,
"East from outside the strip enters at the first (leftmost) column",
);
})
.run(commands);
}
#[test]
fn test_focus_west_from_outside_strip_enters_at_last_column() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::West)),
},
];
TestHarness::new()
.with_windows(3)
.on_iteration(0, |world, _state| {
remove_focused_from_all_strips(world);
})
.on_iteration(1, |world, _state| {
let focused = focused_window_id(world);
let expected = active_strip_last_id(world);
assert_ne!(focused, 0);
assert_eq!(
focused, expected,
"West from outside the strip enters at the last (rightmost) column",
);
})
.run(commands);
}
#[test]
fn test_external_focus_restores_app_hidden_window_to_original_virtual_strip() {
let commands = vec![
Event::Command {
command: Command::PrintState,
},
Event::ApplicationHidden {
pid: TEST_PROCESS_ID,
},
Event::Command {
command: Command::Window(Operation::VirtualNumber(1)),
},
Event::ApplicationVisible {
pid: TEST_PROCESS_ID,
},
Event::WindowFocused { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
];
TestHarness::new()
.with_windows(1)
.on_iteration(2, |world, _state| {
let mut query = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let active = query
.iter(world)
.find_map(|(strip, active)| active.then_some(strip.virtual_index))
.expect("an active virtual strip");
assert_eq!(active, 1);
})
.on_iteration(5, |world, _state| {
let mut query = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let active = query
.iter(world)
.find_map(|(strip, active)| active.then_some(strip.virtual_index))
.expect("an active virtual strip");
assert_eq!(active, 0);
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn test_external_focus_restores_hidden_window_without_visible_event() {
let ignored_repositions = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let commands = vec![
Event::Command {
command: Command::PrintState,
},
Event::ApplicationHidden {
pid: TEST_PROCESS_ID,
},
Event::Command {
command: Command::Window(Operation::VirtualNumber(1)),
},
Event::WindowFocused { window_id: 0 },
Event::Command {
command: Command::PrintState,
},
];
TestHarness::new()
.with_windows(1)
.on_iteration(1, move |world, _state| {
let mut query = world.query::<&mut Window>();
let mut window = query
.iter_mut(world)
.find(|window| window.id() == 0)
.expect("window 0");
window.reposition(Origin::new(0, TEST_DISPLAY_HEIGHT));
ignored_repositions.store(1, std::sync::atomic::Ordering::SeqCst);
})
.on_iteration(4, |world, _state| {
let mut query = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let active = query
.iter(world)
.find_map(|(strip, active)| active.then_some(strip.virtual_index))
.expect("an active virtual strip");
assert_eq!(active, 0);
assert_window_at!(world, 0, 0, TEST_MENUBAR_HEIGHT);
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn mouse_in_bottom_right_corner_does_not_change_focus() {
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::West)),
},
Event::MouseMoved {
point: CGPoint {
x: 1010.0,
y: 750.0,
},
modifiers: Modifiers::empty(),
},
];
TestHarness::new()
.with_windows(3)
.on_iteration(2, |world, _state| {
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn mouse_outside_corner_still_changes_focus() {
use crate::events::Event;
use crate::platform::Modifiers;
use objc2_core_foundation::CGPoint;
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::Focus(Direction::West)),
},
Event::MouseMoved {
point: CGPoint { x: 500.0, y: 400.0 },
modifiers: Modifiers::empty(),
},
];
TestHarness::new()
.with_windows(3)
.on_iteration(2, |world, _state| {
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn toggle_floating_layer_flips_state() {
fn current_layer(world: &mut World) -> FloatingLayer {
let mut query = world.query::<&FloatingLayer>();
*query
.query(world)
.iter()
.find(|layer| layer.workspace_id == TEST_WORKSPACE_ID)
.expect("active workspace has FloatingLayer")
}
let commands = vec![
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::Window(Operation::ToggleFloatingLayer),
},
Event::Command {
command: Command::Window(Operation::ToggleFloatingLayer),
},
];
TestHarness::new()
.with_config(Config::default())
.with_windows(3)
.on_iteration(0, |world, _state| {
assert!(!current_layer(world).front);
})
.on_iteration(1, |world, _state| {
assert!(current_layer(world).front);
})
.on_iteration(2, |world, _state| {
assert!(!current_layer(world).front);
})
.run(commands);
}
#[test]
fn test_unfloat_after_virtual_switch_uses_active_workspace() {
let commands = vec![
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::Window(Operation::Manage),
},
Event::Command {
command: Command::Window(Operation::VirtualNumber(1)),
},
Event::Command {
command: Command::Window(Operation::Manage),
},
];
TestHarness::new()
.with_windows(2)
.on_iteration(3, |world, _state| {
let entity = find_window_entity(0, world);
let mut query = world.query_filtered::<&LayoutStrip, With<ActiveWorkspaceMarker>>();
let strip = query.single(world).expect("an active virtual workspace");
assert_eq!(strip.virtual_index, 1);
assert!(strip.contains(entity));
})
.run(commands);
}
#[test]
fn focus_unmanaged_ignores_floats_from_other_workspaces() {
let workspaces = vec![TEST_WORKSPACE_ID, TEST_WORKSPACE_ID + 1];
let harness = TestHarness::new()
.with_display(
TEST_DISPLAY_ID,
IRect::new(0, 0, TEST_DISPLAY_WIDTH, TEST_DISPLAY_HEIGHT),
workspaces,
)
.with_workspace_window(0, TEST_WORKSPACE_ID, |_| {})
.with_workspace_window(99, TEST_WORKSPACE_ID + 1, |w| {
w.frame = IRect::new(600, 0, 600 + TEST_WINDOW_WIDTH, TEST_WINDOW_HEIGHT);
});
let commands = vec![
Event::MenuOpened { window_id: 0 },
Event::Command {
command: Command::Window(Operation::FocusUnmanaged),
},
Event::Command {
command: Command::PrintState,
},
Event::Command {
command: Command::Window(Operation::Focus(Direction::East)),
},
];
harness
.on_iteration(2, |world, _state| {
let off_workspace_float = find_window_entity(99, world);
world
.entity_mut(off_workspace_float)
.insert(Unmanaged::Floating);
assert_focused!(world, 0);
})
.on_iteration(3, |world, _state| {
let active_float = find_window_entity(0, world);
world.entity_mut(active_float).insert(Unmanaged::Floating);
assert_focused!(world, 0);
})
.on_iteration(4, |world, _state| {
assert_focused!(world, 0);
})
.run(commands);
}
#[test]
fn test_mid_strip_insertion_preserves_window_x() {
let config: Config = (
MainOptions {
insert_windows_mid_strip: Some(true),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(8);
let pump_event = |h: &mut TestHarness, ev: Event| {
h.app.world_mut().write_message::<Event>(ev);
for _ in 0..6 {
h.app.update();
for event in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(event);
}
}
};
let cmd = |h: &mut TestHarness, c: Command| pump_event(h, Event::Command { command: c });
let win_x = |h: &mut TestHarness, id: i32| -> i32 {
let world = h.app.world_mut();
let mut q = world.query::<(&Window, &Position)>();
q.iter(world)
.find_map(|(w, p)| (w.id() == id).then_some(p.0.x))
.expect("window position")
};
for _ in 0..4 {
cmd(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Stay)),
);
}
cmd(&mut h, Command::Window(Operation::VirtualNumber(1)));
pump_event(
&mut h,
Event::Swipe {
delta: 0.3,
fingers: 3,
},
);
cmd(&mut h, Command::Window(Operation::VirtualNumber(0)));
pump_event(
&mut h,
Event::Swipe {
delta: 0.3,
fingers: 3,
},
);
let mover = {
let world = h.app.world_mut();
let mut q = world.query_filtered::<&Window, With<crate::ecs::FocusedMarker>>();
q.single(world).expect("a focused window").id()
};
let before = win_x(&mut h, mover);
assert_ne!(
before % TEST_WINDOW_WIDTH,
0,
"test setup should leave the window off the column grid, got x={before}",
);
cmd(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Follow)),
);
assert_eq!(
win_x(&mut h, mover),
before,
"moved window must keep its exact on-screen x",
);
}
#[test]
fn test_move_appends_to_end_by_default() {
let mut h = TestHarness::new().with_windows(3);
let pump = |h: &mut TestHarness, cmd: Command| {
h.app
.world_mut()
.write_message::<Event>(Event::Command { command: cmd });
for _ in 0..6 {
h.app.update();
for event in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(event);
}
}
};
pump(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Stay)),
);
let mover = focused_window_id(h.app.world_mut());
pump(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Follow)),
);
let last = {
let world = h.app.world_mut();
let mut q = world.query::<(&LayoutStrip, Has<ActiveWorkspaceMarker>)>();
let entity = q
.iter(world)
.find_map(|(s, a)| a.then(|| s.all_windows()))
.and_then(|windows| windows.last().copied())
.expect("active strip with windows");
let mut wq = world.query::<(Entity, &Window)>();
wq.iter(world)
.find_map(|(ent, w)| (ent == entity).then_some(w.id()))
.expect("window id")
};
assert_eq!(
last, mover,
"default move should append to the end of the strip"
);
}
#[test]
fn test_mid_strip_move_does_not_animate() {
let config: Config = (
MainOptions {
insert_windows_mid_strip: Some(true),
animation_speed: Some(12.0),
virtual_workspace_animations: Some(false),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(8);
let pump = |h: &mut TestHarness, c: Command| {
h.app
.world_mut()
.write_message::<Event>(Event::Command { command: c });
for _ in 0..8 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
for _ in 0..4 {
pump(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Stay)),
);
}
pump(&mut h, Command::Window(Operation::VirtualNumber(1)));
h.app.world_mut().write_message::<Event>(Event::Swipe {
delta: 0.3,
fingers: 3,
});
for _ in 0..6 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
pump(&mut h, Command::Window(Operation::VirtualNumber(0)));
h.app.world_mut().write_message::<Event>(Event::Swipe {
delta: 0.3,
fingers: 3,
});
for _ in 0..6 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
h.app.world_mut().write_message::<Event>(Event::Command {
command: Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Follow)),
});
for step in 0..10 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
let world = h.app.world_mut();
let mut q = world.query_filtered::<&Window, With<RepositionMarker>>();
let animating: Vec<i32> = q.iter(world).map(|w| w.id()).collect();
assert!(
animating.is_empty(),
"step {step}: no window should animate during a mid-strip move, got {animating:?}",
);
}
}
#[test]
fn test_virtual_workspace_switch_no_horizontal_slide_no_animations() {
let config: Config = (
MainOptions {
virtual_workspace_animations: Some(false),
animation_speed: Some(12.0),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump_event = |h: &mut TestHarness, ev: Event| {
h.app.world_mut().write_message::<Event>(ev);
for _ in 0..8 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
let pump = |h: &mut TestHarness, c: Command| pump_event(h, Event::Command { command: c });
pump(&mut h, Command::PrintState);
pump_event(
&mut h,
Event::Swipe {
delta: 0.3,
fingers: 3,
},
);
let strip_x_after_scroll = {
let world = h.app.world_mut();
let mut q = world.query_filtered::<&crate::ecs::Position, With<ActiveWorkspaceMarker>>();
q.single(world)
.expect("exactly one active strip after scroll")
.0
.x
};
assert_ne!(
strip_x_after_scroll, 0,
"test setup: strip should be scrolled to a non-zero position, got 0"
);
pump(&mut h, Command::Window(Operation::VirtualNumber(1)));
h.app.world_mut().write_message::<Event>(Event::Command {
command: Command::Window(Operation::VirtualNumber(0)),
});
for _ in 0..10 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
let strip_x_final = {
let world = h.app.world_mut();
let mut q = world.query_filtered::<&crate::ecs::Position, With<ActiveWorkspaceMarker>>();
q.single(world)
.expect("exactly one active strip after switch-back")
.0
.x
};
assert_eq!(
strip_x_final, strip_x_after_scroll,
"strip x must equal the pre-switch scroll position after VW switch-back (no sideways slide). \
Expected {strip_x_after_scroll}, got {strip_x_final}"
);
}
#[test]
fn test_virtual_workspace_switch_stops_in_flight_strip_animation() {
let config: Config = (
MainOptions {
virtual_workspace_animations: Some(false),
animation_speed: Some(12.0),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump_n = |h: &mut TestHarness, n: usize, ev: Event| {
h.app.world_mut().write_message::<Event>(ev);
for _ in 0..n {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
let pump = |h: &mut TestHarness, c: Command| {
pump_n(h, 8, Event::Command { command: c });
};
pump(&mut h, Command::PrintState);
h.app.world_mut().write_message::<Event>(Event::Swipe {
delta: 0.3,
fingers: 3,
});
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
pump(&mut h, Command::Window(Operation::VirtualNumber(1)));
{
let world = h.app.world_mut();
let mut q = world.query_filtered::<Entity, (
With<crate::ecs::layout::LayoutStrip>,
Without<ActiveWorkspaceMarker>,
)>();
for entity in q.iter(world) {
assert!(
world.get::<RepositionMarker>(entity).is_none(),
"hidden strip {entity:?} must not have RepositionMarker after VW switch"
);
}
}
let saved_x = {
h.app.world_mut().write_message::<Event>(Event::Command {
command: Command::Window(Operation::VirtualNumber(0)),
});
for _ in 0..10 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
let world = h.app.world_mut();
let mut q = world.query_filtered::<&crate::ecs::Position, With<ActiveWorkspaceMarker>>();
q.single(world)
.expect("exactly one active strip after restore")
.0
.x
};
{
let world = h.app.world_mut();
let mut q = world.query_filtered::<Entity, (
With<crate::ecs::layout::LayoutStrip>,
With<ActiveWorkspaceMarker>,
)>();
for entity in q.iter(world) {
assert!(
world.get::<RepositionMarker>(entity).is_none(),
"restored strip {entity:?} must not have RepositionMarker (no animation after no-anim VW switch)"
);
}
}
for _ in 0..5 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
let world = h.app.world_mut();
let mut q = world.query_filtered::<&crate::ecs::Position, With<ActiveWorkspaceMarker>>();
let final_x = q.single(world).expect("exactly one active strip").0.x;
assert_eq!(
final_x, saved_x,
"strip x drifted after restore: was {saved_x}, now {final_x}"
);
}
#[test]
fn test_reshuffle_leftmost_pins_strip_to_left_edge_with_stale_frame() {
use crate::ecs::{Position, ReshuffleAroundMarker};
let config: Config = (
MainOptions {
auto_center: Some(false),
animation_speed: Some(30.0),
continuous_swipe: Some(false),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump = |h: &mut TestHarness, c: Command| {
h.app
.world_mut()
.write_message::<Event>(Event::Command { command: c });
for _ in 0..10 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
pump(&mut h, Command::PrintState);
let leftmost = find_window_entity(0, h.app.world_mut());
{
let world = h.app.world_mut();
if let Ok(mut e) = world.get_entity_mut(leftmost) {
e.insert(Position(Origin::new(
TEST_DISPLAY_WIDTH - 5,
TEST_MENUBAR_HEIGHT,
)));
e.remove::<RepositionMarker>();
e.insert(ReshuffleAroundMarker);
}
}
for _ in 0..15 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
let world = h.app.world_mut();
let mut q = world.query_filtered::<&Position, With<ActiveWorkspaceMarker>>();
let strip_x = q.single(world).expect("exactly one active strip").0.x;
assert_eq!(
strip_x, 0,
"reshuffle around leftmost window must pin strip to left edge (offset 0), got {strip_x}"
);
}
#[test]
fn test_virtual_workspace_switch_preserves_scroll_with_animations() {
use crate::ecs::Position;
let config: Config = (
MainOptions {
auto_center: Some(false),
animation_speed: Some(30.0),
swipe_gesture_fingers: Some(3),
virtual_workspace_animations: Some(true),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump_event = |h: &mut TestHarness, ev: Event| {
h.app.world_mut().write_message::<Event>(ev);
for _ in 0..14 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
let pump = |h: &mut TestHarness, c: Command| pump_event(h, Event::Command { command: c });
pump(&mut h, Command::PrintState);
pump_event(
&mut h,
Event::Swipe {
delta: 0.4,
fingers: 3,
},
);
let strip_x_after_scroll = {
let world = h.app.world_mut();
let mut q = world.query_filtered::<&Position, With<ActiveWorkspaceMarker>>();
q.single(world).expect("active strip after scroll").0.x
};
assert_ne!(
strip_x_after_scroll, 0,
"test setup: strip should be scrolled off the left edge, got 0"
);
pump(&mut h, Command::Window(Operation::VirtualNumber(1)));
pump(&mut h, Command::Window(Operation::VirtualNumber(0)));
let strip_x_restored = {
let world = h.app.world_mut();
let mut q = world.query_filtered::<&Position, With<ActiveWorkspaceMarker>>();
q.single(world).expect("active strip after restore").0.x
};
assert_eq!(
strip_x_restored, strip_x_after_scroll,
"animated VW restore must preserve the saved scroll position. \
Expected {strip_x_after_scroll}, got {strip_x_restored}"
);
}
#[test]
fn test_virtual_workspace_switch_hides_old_strip_with_animations() {
use crate::ecs::{Position, layout::LayoutStrip};
let config: Config = (
MainOptions {
auto_center: Some(false),
animation_speed: Some(30.0),
swipe_gesture_fingers: Some(3),
virtual_workspace_animations: Some(true),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump = |h: &mut TestHarness, c: Command| {
h.app
.world_mut()
.write_message::<Event>(Event::Command { command: c });
for _ in 0..14 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
pump(&mut h, Command::PrintState);
pump(
&mut h,
Command::Window(Operation::VirtualMoveNumber(1, MoveFocus::Stay)),
);
pump(&mut h, Command::Window(Operation::VirtualNumber(1)));
let world = h.app.world_mut();
let mut q = world.query::<(&LayoutStrip, &Position, Has<ActiveWorkspaceMarker>)>();
let inactive_x = q
.iter(world)
.find_map(|(_, pos, active)| (!active).then_some(pos.0.x))
.expect("an inactive strip exists after switching workspaces");
assert!(
inactive_x >= TEST_DISPLAY_WIDTH - 10,
"inactive strip must be parked off-screen (>= {}), got {inactive_x}",
TEST_DISPLAY_WIDTH - 10
);
}
#[test]
fn test_stack_unstack_brings_focused_window_into_view() {
use crate::ecs::{Bounds, Position, Scrolling};
let config: Config = (
MainOptions {
auto_center: Some(false),
animation_speed: Some(10000.0),
swipe_gesture_fingers: Some(3),
..Default::default()
},
vec![],
)
.into();
let mut h = TestHarness::new().with_config(config).with_windows(5);
let pump = |h: &mut TestHarness, c: Command| {
h.app
.world_mut()
.write_message::<Event>(Event::Command { command: c });
for _ in 0..16 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
let settle_scroll_offset = |h: &mut TestHarness| {
h.app.world_mut().write_message::<Event>(Event::Swipe {
delta: 0.9,
fingers: 3,
});
for _ in 0..16 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
let world = h.app.world_mut();
let mut q = world.query_filtered::<Entity, With<Scrolling>>();
for e in q.iter(world).collect::<Vec<_>>() {
if let Ok(mut em) = world.get_entity_mut(e) {
em.remove::<Scrolling>();
}
}
for _ in 0..4 {
h.app.update();
for e in h.mock_state.drain_events() {
h.app.world_mut().write_message::<Event>(e);
}
}
};
let is_fully_onscreen = |h: &mut TestHarness, id: i32| {
let e = find_window_entity(id, h.app.world_mut());
let world = h.app.world();
let p = world.get::<Position>(e).unwrap().0;
let b = world.get::<Bounds>(e).unwrap().0;
p.x >= 0 && p.x + b.x <= TEST_DISPLAY_WIDTH
};
pump(&mut h, Command::PrintState);
pump(&mut h, Command::Window(Operation::Focus(Direction::First)));
pump(&mut h, Command::Window(Operation::Focus(Direction::East)));
settle_scroll_offset(&mut h);
assert!(
!is_fully_onscreen(&mut h, 1),
"test setup: focused window should be off-screen before stacking"
);
pump(&mut h, Command::Window(Operation::Stack(true)));
assert!(
is_fully_onscreen(&mut h, 1),
"stacking must bring the focused window fully back into view"
);
settle_scroll_offset(&mut h);
assert!(
!is_fully_onscreen(&mut h, 1),
"test setup: focused window should be off-screen before unstacking"
);
pump(&mut h, Command::Window(Operation::Stack(false)));
assert!(
is_fully_onscreen(&mut h, 1),
"unstacking must bring the focused window fully back into view"
);
}