use std::sync::atomic::{AtomicBool, Ordering};
use bevy::app::PreUpdate;
use bevy::ecs::entity::{Entity, EntityHashSet};
use bevy::ecs::hierarchy::ChildOf;
use bevy::ecs::message::MessageReader;
use bevy::ecs::query::{Has, With};
use bevy::ecs::system::{Commands, Query, Res, ResMut, Single};
use bevy::math::IRect;
use tracing::{Level, instrument};
use tracing::{debug, info};
use crate::config::Config;
use crate::ecs::layout::{Column, LayoutStrip, StackItem};
use crate::ecs::params::{ActiveDisplay, ActiveDisplayMut, Windows};
use crate::ecs::{
ActiveDisplayMarker, FocusFollowsMouse, FocusedMarker, FullWidthMarker, SendMessageTrigger,
Unmanaged, WMEventTrigger, reposition_entity, reshuffle_around, resize_entity,
};
use crate::events::Event;
use crate::manager::{Application, Display, Origin, Size, Window, WindowManager, origin_to};
pub static ON_FULLSCREEN_SPACE: AtomicBool = AtomicBool::new(false);
#[derive(Clone, Debug)]
pub enum Direction {
North,
South,
West,
East,
First,
Last,
}
#[derive(Clone, Copy, Debug)]
pub enum ResizeDirection {
Grow,
Shrink,
}
#[derive(Clone, Debug)]
pub enum Operation {
Focus(Direction),
Swap(Direction),
Center,
Resize(ResizeDirection),
FullWidth,
ToNextDisplay,
Equalize,
Manage,
Stack(bool),
Snap,
}
#[derive(Clone, Debug)]
pub enum MouseMove {
ToNextDisplay,
}
#[derive(Clone, Debug)]
pub enum Command {
Window(Operation),
Mouse(MouseMove),
Quit,
PrintState,
}
pub fn register_commands(app: &mut bevy::app::App) {
app.add_systems(
PreUpdate,
(
command_quit_handler,
print_internal_state_handler,
mouse_to_next_display,
resize_window,
command_center_window,
full_width_window,
to_next_display,
equalize_column,
manage_window,
stack_windows_handler,
command_move_focus,
command_swap_focus,
snap_window,
),
);
}
fn filter_window_operations<'a, F: Fn(&Operation) -> bool>(
messages: &'a mut MessageReader<Event>,
filter: F,
) -> impl Iterator<Item = &'a Operation> {
messages.read().filter_map(move |event| {
if let Event::Command {
command: Command::Window(op),
} = event
&& filter(op)
{
Some(op)
} else {
None
}
})
}
#[instrument(level = Level::DEBUG, ret)]
fn get_window_in_direction(
direction: &Direction,
entity: Entity,
strip: &LayoutStrip,
) -> Option<Entity> {
let index = strip.index_of(entity).ok()?;
match direction {
Direction::West => strip.left_neighbour(entity),
Direction::East => strip.right_neighbour(entity),
Direction::First => strip.first().ok().and_then(|column| column.top()),
Direction::Last => strip.last().ok().and_then(|column| column.top()),
Direction::North => match strip.get(index).ok()? {
Column::Single(_) | Column::Tabs(_) => None,
Column::Stack(stack) => stack
.iter()
.enumerate()
.find(|(_, item)| item.contains(entity))
.and_then(|(index, _)| (index > 0).then(|| stack.get(index - 1)).flatten())
.and_then(StackItem::top),
},
Direction::South => match strip.get(index).ok()? {
Column::Single(_) | Column::Tabs(_) => None,
Column::Stack(stack) => stack
.iter()
.enumerate()
.find(|(_, item)| item.contains(entity))
.and_then(|(index, _)| {
(index < stack.len() - 1)
.then(|| stack.get(index + 1))
.flatten()
})
.and_then(StackItem::top),
},
}
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
fn command_move_focus(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
apps: Query<&Application>,
fs_windows: Query<(Entity, &crate::ecs::NativeFullscreenMarker)>,
mut commands: Commands,
) {
let Some(Operation::Focus(direction)) =
filter_window_operations(&mut messages, |op| matches!(op, Operation::Focus(_))).next()
else {
return;
};
let strip = active_display.active_strip();
if ON_FULLSCREEN_SPACE.load(Ordering::Relaxed) {
let Some(cur) = windows
.focused()
.and_then(|(_, e)| fs_windows.get(e).ok())
.map(|(_, m)| m.order)
else {
return;
};
let mut sorted: Vec<_> = fs_windows.iter().collect();
sorted.sort_by_key(|(_, m)| m.order);
let pos = sorted.iter().position(|(_, m)| m.order == cur);
let target = match direction {
Direction::West => pos
.and_then(|p| p.checked_sub(1))
.map(|p| sorted[p].0)
.or_else(|| strip.get(strip.len().wrapping_sub(1)).ok()?.top()),
Direction::East => pos.and_then(|p| sorted.get(p + 1)).map(|(e, _)| *e),
_ => None,
};
if let Some(entity) = target
&& let Some(window) = windows.get(entity)
&& let Some(psn) = windows.psn(window.id(), &apps)
{
debug!("fullscreen: raising {entity}");
window.focus_with_raise(psn);
}
return;
}
let Some((_, focused_entity)) = windows.focused() else {
return;
};
let candidate = get_window_in_direction(direction, focused_entity, strip).or_else(|| {
(matches!(direction, Direction::East) && strip.right_neighbour(focused_entity).is_none())
.then(|| {
fs_windows
.iter()
.min_by_key(|(_, m)| m.order)
.map(|(e, _)| e)
})
.flatten()
});
if let Some(entity) = candidate
&& let Some(window) = windows.get(entity)
&& let Some(psn) = windows.psn(window.id(), &apps)
{
window.focus_with_raise(psn);
commands.entity(focused_entity).try_remove::<FocusedMarker>();
commands.entity(entity).try_insert(FocusedMarker);
reshuffle_around(entity, &mut commands);
return;
}
let Some(other_display) = active_display.other().next() else {
return;
};
let change_display = match direction {
Direction::North => active_display.bounds().min.y > other_display.bounds().min.y,
Direction::South => active_display.bounds().min.y < other_display.bounds().min.y,
_ => false,
};
debug!("moving focus to another display: {change_display}");
if change_display {
commands.trigger(SendMessageTrigger(Event::Command {
command: Command::Mouse(MouseMove::ToNextDisplay),
}));
}
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
fn command_swap_focus(
mut messages: MessageReader<Event>,
windows: Windows,
apps: Query<&Application>,
mut active_display: ActiveDisplayMut,
mut commands: Commands,
) {
let Some(Operation::Swap(direction)) =
filter_window_operations(&mut messages, |op| matches!(op, Operation::Swap(_))).next()
else {
return;
};
if ON_FULLSCREEN_SPACE.load(Ordering::Relaxed) {
let strip = active_display.active_strip();
if let Some(entity) = strip
.get(strip.len().wrapping_sub(1))
.ok()
.and_then(|c| c.top())
&& let Some(window) = windows.get(entity)
&& let Some(psn) = windows.psn(window.id(), &apps)
{
debug!("fullscreen: swap raising {entity}");
window.focus_with_raise(psn);
}
return;
}
let active_strip = active_display.active_strip();
let mut handler = || {
let (_, current) = windows.focused()?;
let index = active_strip.index_of(current).ok()?;
let other_window = get_window_in_direction(direction, current, active_strip)?;
let new_index = active_strip.index_of(other_window).ok()?;
debug!(
"swap {direction:?}: current={current} idx={index}, other={other_window} idx={new_index}, strip_len={}",
active_strip.len()
);
if index < new_index {
(index..new_index).for_each(|idx| active_strip.swap(idx, idx + 1));
} else {
(new_index..index)
.rev()
.for_each(|idx| active_strip.swap(idx, idx + 1));
}
Some(current)
};
if let Some(window) = handler() {
reshuffle_around(window, &mut commands);
} else {
debug!(
"swap {direction:?}: handler returned None (focused={:?}, strip_len={})",
windows.focused().map(|(_, e)| e),
active_strip.len()
);
}
if windows
.focused()
.and_then(|(_, current)| get_window_in_direction(direction, current, active_strip))
.is_none()
{
let bounds = active_display.bounds();
let Some(other_display) = active_display.other().next() else {
return;
};
let change_display = match direction {
Direction::North => bounds.min.y > other_display.bounds().min.y,
Direction::South => bounds.min.y < other_display.bounds().min.y,
_ => false,
};
debug!("swapping window to another display: {change_display}");
if change_display {
commands.trigger(SendMessageTrigger(Event::Command {
command: Command::Window(Operation::ToNextDisplay),
}));
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn command_center_window(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::Center))
.next()
.is_none()
{
return;
}
if let Some((_, entity)) = windows.focused()
&& let Some(layout_position) = windows.layout_position(entity)
&& let Some(size) = windows.size(entity)
&& let Some(mut origin) = windows.origin(entity)
{
let center = active_display.bounds().center().x;
origin.x = center - size.x / 2;
let strip_position = origin - layout_position.0;
reposition_entity(
active_display.active_strip_entity(),
strip_position,
&mut commands,
);
window_manager.warp_mouse(active_display.bounds().center());
}
}
#[allow(clippy::needless_pass_by_value)]
fn resize_window(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
config: Res<Config>,
mut commands: Commands,
) {
let Some(Operation::Resize(direction)) =
filter_window_operations(&mut messages, |op| matches!(op, Operation::Resize(_))).next()
else {
return;
};
let Some((frame, entity)) = windows
.focused()
.and_then(|(_, entity)| windows.frame(entity).zip(Some(entity)))
else {
return;
};
if let Ok(mut cmds) = commands.get_entity(entity) {
cmds.try_remove::<FullWidthMarker>();
}
let display_width = active_display.bounds().width();
let (_, pad_right, _, pad_left) = config.edge_padding();
let padded_width = display_width - pad_left - pad_right;
let current_ratio = f64::from(frame.width()) / f64::from(padded_width);
let widths = config.preset_column_widths();
let fallback = *widths.first().unwrap_or(&0.5);
let next_ratio = match direction {
ResizeDirection::Grow => widths
.iter()
.copied()
.find(|&r| r > current_ratio + 0.05)
.unwrap_or(fallback),
ResizeDirection::Shrink => widths
.iter()
.rev()
.copied()
.find(|&r| r < current_ratio - 0.05)
.unwrap_or_else(|| *widths.last().unwrap_or(&fallback)),
};
let new_width = (next_ratio * f64::from(padded_width)).round() as i32;
let size = Size::new(new_width, frame.height());
let mut frame = IRect::from_center_size(frame.center(), size);
if frame.max.x > active_display.bounds().max.x - pad_right {
frame.min.x = active_display.bounds().max.x - pad_right - size.x;
reposition_entity(entity, frame.min, &mut commands);
}
let strip = active_display.active_strip();
if let Some(Column::Stack(stack)) = strip
.index_of(entity)
.ok()
.and_then(|idx| strip.get(idx).ok())
{
for sibling in stack.iter().flat_map(StackItem::all_windows) {
if sibling != entity
&& let Some(size) = windows.size(sibling)
{
resize_entity(sibling, size.with_x(new_width), &mut commands);
}
}
}
resize_entity(entity, size, &mut commands);
}
#[allow(clippy::needless_pass_by_value)]
fn full_width_window(
mut messages: MessageReader<Event>,
windows: Windows,
mut active_display: ActiveDisplayMut,
config: Res<Config>,
mut commands: Commands,
) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::FullWidth))
.next()
.is_none()
{
return;
}
let Some((frame, (_, entity))) = windows
.focused()
.and_then(|(window, entity)| windows.frame(entity).zip(Some((window, entity))))
else {
return;
};
let viewport = active_display
.display()
.actual_display_bounds(active_display.dock(), &config);
let height = frame.height();
let y = frame.min.y;
let (width, x) = if let Some(marker) = windows.full_width(entity) {
let previous_ratio = marker.width_ratio;
let was_stacked = marker.was_stacked;
commands.entity(entity).try_remove::<FullWidthMarker>();
if was_stacked {
_ = active_display.active_strip().stack(entity);
}
let w = (previous_ratio * f64::from(viewport.width())).round() as i32;
(w, viewport.center().x - frame.width() / 2)
} else {
let strip = active_display.active_strip();
let was_stacked = strip
.index_of(entity)
.ok()
.and_then(|idx| strip.get(idx).ok())
.is_some_and(|col| matches!(col, Column::Stack(_)));
if was_stacked {
_ = strip.unstack(entity);
}
let width_ratio = windows.width_ratio(entity).unwrap_or(0.5);
commands.entity(entity).try_insert(FullWidthMarker {
width_ratio,
was_stacked,
});
(viewport.width(), viewport.min.x)
};
reposition_entity(entity, Origin::new(x, y), &mut commands);
resize_entity(entity, Size::new(width, height), &mut commands);
}
#[allow(clippy::needless_pass_by_value)]
fn manage_window(mut messages: MessageReader<Event>, windows: Windows, mut commands: Commands) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::Manage))
.next()
.is_none()
{
return;
}
let Some((window, entity, unmanaged)) = windows
.focused()
.and_then(|(_, entity)| windows.get_managed(entity))
else {
return;
};
debug!(
"window: {} {entity} unmanaged: {}.",
window.id(),
unmanaged.is_some()
);
if unmanaged.is_some() {
commands.entity(entity).try_remove::<Unmanaged>();
} else {
commands.entity(entity).try_insert(Unmanaged::Floating);
}
}
#[allow(clippy::needless_pass_by_value)]
fn to_next_display(
mut messages: MessageReader<Event>,
windows: Windows,
mut active_display: ActiveDisplayMut,
window_manager: Res<WindowManager>,
mut commands: Commands,
) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::ToNextDisplay))
.next()
.is_none()
{
return;
}
let Some((window, entity, unmanaged)) = windows
.focused()
.and_then(|(_, entity)| windows.get_managed(entity))
else {
return;
};
if unmanaged.is_some() {
return;
}
let Some(other) = active_display.other().next() else {
debug!("no other display to move window to.");
return;
};
debug!(
"moving window (id {}, {entity}) to display {}: {}.",
window.id(),
other.id(),
other.width() / 2,
);
let center = other.bounds().center().x;
let Some(size) = windows.size(entity) else {
return;
};
let dest = other.bounds().min.with_x(center - size.x / 2);
reposition_entity(entity, dest, &mut commands);
reshuffle_around(entity, &mut commands);
window_manager.warp_mouse(other.bounds().center());
if let Some(neighbour) = active_display.active_strip().right_neighbour(entity) {
reshuffle_around(neighbour, &mut commands);
}
active_display.active_strip().remove(entity);
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
fn mouse_to_next_display(
mut messages: MessageReader<Event>,
windows: Windows,
layout_strips: Query<(&LayoutStrip, Entity)>,
displays: Query<(&Display, Entity, Has<ActiveDisplayMarker>)>,
window_manager: Res<WindowManager>,
mut ffm_flag: ResMut<FocusFollowsMouse>,
mut commands: Commands,
) {
if !messages.read().any(|event| {
matches!(
event,
Event::Command {
command: Command::Mouse(MouseMove::ToNextDisplay),
}
)
}) {
return;
}
let Some((other, _, _)) = displays.iter().find(|(_, _, active)| !*active) else {
debug!("no other display to move mouse to.");
return;
};
let Some((other_strip, _)) = window_manager
.active_display_space(other.id())
.ok()
.and_then(|id| layout_strips.iter().find(|(strip, _)| strip.id() == id))
else {
return;
};
let visible_width = |frame: IRect| other.bounds().intersect(frame).width();
let Some(frame) = other_strip
.all_windows()
.iter()
.filter_map(|entity| windows.frame(*entity))
.max_by(|left, right| {
if visible_width(*left) < visible_width(*right) {
std::cmp::Ordering::Less
} else {
std::cmp::Ordering::Greater
}
})
else {
debug!("no suitable windows on the other display to move the mouse.");
return;
};
let visible_frame = other.bounds().intersect(frame);
debug!("warping mouse to {visible_frame:?}",);
window_manager.warp_mouse(visible_frame.center());
let point = origin_to(visible_frame.center());
ffm_flag.as_mut().0 = None;
commands.trigger(WMEventTrigger(Event::MouseMoved { point }));
}
#[allow(clippy::needless_pass_by_value)]
fn equalize_column(
mut messages: MessageReader<Event>,
current_focus: Single<(&Window, Entity), With<FocusedMarker>>,
windows: Windows,
active_display: ActiveDisplay,
mut commands: Commands,
) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::Equalize))
.next()
.is_none()
{
return;
}
let (_, entity) = *current_focus;
let active_strip = active_display.active_strip();
let Ok(column) = active_strip
.index_of(entity)
.and_then(|index| active_strip.get(index))
else {
return;
};
if let Column::Stack(stack) = column {
#[allow(clippy::cast_precision_loss)]
let equal_height = active_display.bounds().height() / i32::try_from(stack.len()).unwrap();
for item in &stack {
for entity in item.all_windows() {
if let Some(size) = windows.size(entity) {
resize_entity(entity, size.with_y(equal_height), &mut commands);
}
}
}
}
}
#[allow(clippy::needless_pass_by_value)]
fn snap_window(
mut messages: MessageReader<Event>,
windows: Windows,
active_display: ActiveDisplay,
config: Res<Config>,
mut commands: Commands,
) {
if filter_window_operations(&mut messages, |op| matches!(op, Operation::Snap))
.next()
.is_none()
{
return;
}
let Some((_, entity)) = windows.focused() else {
return;
};
let Some(layout_position) = windows.layout_position(entity) else {
return;
};
let Some(mut frame) = windows.moving_frame(entity) else {
return;
};
let display_bounds = active_display
.display()
.actual_display_bounds(active_display.dock(), &config);
let size = frame.size();
frame.min = frame
.min
.clamp(display_bounds.min, display_bounds.max - size);
frame.max = frame.min + size;
let strip_position = frame.min - layout_position.0;
reposition_entity(
active_display.active_strip_entity(),
strip_position,
&mut commands,
);
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
pub fn stack_windows_handler(
mut messages: MessageReader<Event>,
windows: Windows,
mut active_display: ActiveDisplayMut,
) {
let Some(Operation::Stack(stack)) =
filter_window_operations(&mut messages, |op| matches!(op, Operation::Stack(_))).next()
else {
return;
};
if let Some((_, entity, unmanaged)) = windows
.focused()
.and_then(|(_, entity)| windows.get_managed(entity))
&& unmanaged.is_none()
{
let strip = active_display.active_strip();
if *stack {
_ = strip.stack(entity);
} else {
_ = strip.unstack(entity);
}
}
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
pub fn command_quit_handler(
mut messages: MessageReader<Event>,
window_manager: Res<WindowManager>,
) {
if messages.read().any(|event| {
matches!(
event,
Event::Command {
command: Command::Quit
}
)
}) {
_ = window_manager.quit();
}
}
#[instrument(level = Level::DEBUG, skip_all)]
#[allow(clippy::needless_pass_by_value)]
fn print_internal_state_handler(
mut messages: MessageReader<Event>,
focused: Query<(&Window, Entity), With<FocusedMarker>>,
windows: Query<(&Window, Entity, &ChildOf, Option<&Unmanaged>)>,
apps: Query<&Application>,
workspaces: Query<(&LayoutStrip, Entity, &ChildOf)>,
displays: Query<(&Display, Entity, Has<ActiveDisplayMarker>)>,
) {
if !messages.read().any(|event| {
matches!(
event,
Event::Command {
command: Command::PrintState,
}
)
}) {
return;
}
let focused = focused.single().ok();
let print_window = |(window, entity, child, unmanaged): (
&Window,
Entity,
&ChildOf,
Option<_>,
)| {
let bundle_id = apps
.get(child.parent())
.ok()
.and_then(|app| app.bundle_id().map(str::to_owned))
.unwrap_or_default();
format!(
"\tid: {}, {entity}, {}:{}, {}x{}{}{}, bundle: {}, role: {}, subrole: {}, title: '{:.70}'",
window.id(),
window.frame().min.x,
window.frame().min.y,
window.frame().width(),
window.frame().height(),
if focused.is_some_and(|(_, focus)| focus == entity) {
", focused"
} else {
""
},
unmanaged.map(|m| format!(", {m:?}")).unwrap_or_default(),
bundle_id,
window.role().unwrap_or_default(),
window.subrole().unwrap_or_default(),
window.title().unwrap_or_default()
)
};
let mut seen = EntityHashSet::new();
for (display, display_entity, active) in displays {
for (strip, _, _) in workspaces
.iter()
.filter(|(_, _, child)| child.parent() == display_entity)
{
let windows = strip
.all_windows()
.iter()
.filter_map(|entity| windows.get(*entity).ok())
.inspect(|(_, entity, _, _)| {
seen.insert(*entity);
})
.map(print_window)
.collect::<Vec<_>>();
let display_id = display.id();
info!(
"Display {display_id}{}, workspace id {}: {strip}:\n{}",
if active { ", active" } else { "" },
strip.id(),
windows.join("\n")
);
}
}
let remaining = windows
.iter()
.filter(|entity| !seen.contains(&entity.1))
.map(print_window)
.collect::<Vec<_>>();
info!("Remaining:\n{}", remaining.join("\n"));
if let Some(pool) = bevy::tasks::ComputeTaskPool::try_get() {
info!("Running with {} threads", pool.thread_num());
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::*;
fn setup_world_with_layout() -> (World, LayoutStrip, Vec<Entity>) {
let mut world = World::new();
let entities = world
.spawn_batch(vec![(), (), (), ()])
.collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
strip.append(entities[0]); strip.append(entities[1]);
strip.append(entities[2]);
strip.append(entities[3]);
strip.stack(entities[1]).unwrap();
(world, strip, entities)
}
#[test]
fn test_get_window_in_direction_simple() {
let (_world, strip, entities) = setup_world_with_layout();
let e0 = entities[0];
let e2 = entities[2];
let e3 = entities[3];
let east = Direction::East;
let west = Direction::West;
assert_eq!(get_window_in_direction(&east, e2, &strip), Some(e3));
assert_eq!(get_window_in_direction(&west, e2, &strip), Some(e0));
assert_eq!(get_window_in_direction(&west, e3, &strip), Some(e2));
assert_eq!(get_window_in_direction(&east, e3, &strip), None);
assert_eq!(get_window_in_direction(&east, e0, &strip), Some(e2));
assert_eq!(get_window_in_direction(&west, e0, &strip), None);
}
#[test]
fn test_get_window_in_direction_stacked() {
let (_world, strip, entities) = setup_world_with_layout();
let e0 = entities[0];
let e1 = entities[1];
let north = Direction::North;
let south = Direction::South;
assert_eq!(get_window_in_direction(&south, e0, &strip), Some(e1));
assert_eq!(get_window_in_direction(&north, e0, &strip), None);
assert_eq!(get_window_in_direction(&north, e1, &strip), Some(e0));
assert_eq!(get_window_in_direction(&south, e1, &strip), None);
}
#[test]
fn test_get_window_in_direction_adjacent_stacks() {
let mut world = World::new();
let entities = world
.spawn_batch(vec![(), (), (), ()])
.collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
strip.append(entities[0]);
strip.append(entities[1]);
strip.append(entities[2]);
strip.append(entities[3]);
strip.stack(entities[1]).unwrap(); strip.stack(entities[3]).unwrap();
let east = Direction::East;
let west = Direction::West;
assert_eq!(
get_window_in_direction(&east, entities[0], &strip),
Some(entities[2])
);
assert_eq!(
get_window_in_direction(&east, entities[1], &strip),
Some(entities[3])
);
assert_eq!(
get_window_in_direction(&west, entities[2], &strip),
Some(entities[0])
);
assert_eq!(
get_window_in_direction(&west, entities[3], &strip),
Some(entities[1])
);
}
}