use bevy::ecs::entity::Entity;
use bevy::ecs::observer::On;
use bevy::ecs::query::{Has, With};
use bevy::ecs::system::{Commands, Query, Res, Single};
use log::{debug, error};
use objc2_core_foundation::CGPoint;
use stdext::function_name;
use crate::config::{Config, preset_column_widths};
use crate::ecs::params::ActiveDisplayMut;
use crate::ecs::{
CommandTrigger, FocusedMarker, Unmanaged, WMEventTrigger, reposition_entity, reshuffle_around,
resize_entity,
};
use crate::errors::Result;
use crate::events::Event;
use crate::manager::{Display, Panel, Window, WindowManager, WindowPane};
#[derive(Clone, Debug)]
pub enum Direction {
North,
South,
West,
East,
First,
Last,
}
#[derive(Clone, Debug)]
pub enum Operation {
Focus(Direction),
Swap(Direction),
Center,
Resize,
FullWidth,
ToNextDisplay,
Manage,
Stack(bool),
}
#[derive(Clone, Debug)]
pub enum Command {
Window(Operation),
Quit,
PrintState,
}
fn get_window_in_direction(
direction: &Direction,
entity: Entity,
panel: &WindowPane,
) -> Option<Entity> {
let index = panel.index_of(entity).ok()?;
match direction {
Direction::West => panel.left_neighbour(entity),
Direction::East => panel.right_neighbour(entity),
Direction::First => panel.first().ok().and_then(|panel| panel.top()),
Direction::Last => panel.last().ok().and_then(|panel| panel.top()),
Direction::North => match panel.get(index).ok()? {
Panel::Single(window) => Some(window),
Panel::Stack(stack) => stack
.iter()
.enumerate()
.find(|(_, window_id)| entity == **window_id)
.and_then(|(index, _)| (index > 0).then(|| stack.get(index - 1)).flatten())
.copied(),
},
Direction::South => match panel.get(index).ok()? {
Panel::Single(window) => Some(window),
Panel::Stack(stack) => stack
.iter()
.enumerate()
.find(|(_, window_id)| entity == **window_id)
.and_then(|(index, _)| {
(index < stack.len() - 1)
.then(|| stack.get(index + 1))
.flatten()
})
.copied(),
},
}
}
fn command_move_focus(
direction: &Direction,
current_window: Entity,
strip: &WindowPane,
windows: &Query<&Window>,
) -> Option<Entity> {
get_window_in_direction(direction, current_window, strip).inspect(|entity| {
if let Ok(window) = windows.get(*entity) {
window.focus_with_raise();
}
})
}
fn command_swap_focus(
direction: &Direction,
current: Entity,
active_display: &mut ActiveDisplayMut,
windows: &mut Query<&mut Window>,
commands: &mut Commands,
) -> Option<Entity> {
let display_bounds = active_display.bounds();
let display_id = active_display.id();
let panel = active_display.active_panel().ok()?;
let index = panel.index_of(current).ok()?;
let other_window = get_window_in_direction(direction, current, panel)?;
let new_index = panel.index_of(other_window).ok()?;
let current_frame = windows.get(current).ok()?.frame();
let origin = if new_index == 0 {
CGPoint::new(0.0, 0.0)
} else if new_index == (panel.len() - 1) {
CGPoint::new(display_bounds.size.width - current_frame.size.width, 0.0)
} else {
panel
.get(new_index)
.ok()
.and_then(|panel| panel.top())
.and_then(|entity| windows.get(entity).ok())?
.frame()
.origin
};
reposition_entity(current, origin.x, origin.y, display_id, commands);
if index < new_index {
(index..new_index).for_each(|idx| panel.swap(idx, idx + 1));
} else {
(new_index..index)
.rev()
.for_each(|idx| panel.swap(idx, idx + 1));
}
Some(other_window)
}
fn command_center_window(
focused_entity: Entity,
windows: &mut Query<&mut Window>,
window_manager: &WindowManager,
active_display: &ActiveDisplayMut,
commands: &mut Commands,
) {
let Ok(window) = windows.get_mut(focused_entity) else {
return;
};
let frame = window.frame();
reposition_entity(
focused_entity,
(active_display.bounds().size.width - frame.size.width) / 2.0,
frame.origin.y,
active_display.id(),
commands,
);
window_manager
.0
.center_mouse(&window, &active_display.bounds());
}
fn resize_window(
active_display: &mut Display,
focused_entity: Entity,
windows: &mut Query<&mut Window>,
commands: &mut Commands,
config: &Config,
) {
let Ok(window) = windows.get_mut(focused_entity) else {
return;
};
let display_width = active_display.bounds.size.width;
let width_ratios = preset_column_widths(config);
let width_ratio = window.next_size_ratio(&width_ratios);
let width = width_ratio * display_width;
let height = window.frame().size.height;
let x = (display_width - width).min(window.frame().origin.x);
let y = window.frame().origin.y;
reposition_entity(focused_entity, x, y, active_display.id(), commands);
resize_entity(focused_entity, width, height, active_display.id(), commands);
}
fn full_width_window(
active_display: &mut Display,
focused_entity: Entity,
windows: &mut Query<&mut Window>,
commands: &mut Commands,
config: &Config,
) {
let Ok(window) = windows.get(focused_entity) else {
return;
};
let display_width = active_display.bounds.size.width;
let height = window.frame().size.height;
let y = window.frame().origin.y;
let is_full_width = (window.frame().size.width - display_width).abs() < 1.0;
let (width, x) = if is_full_width {
let width_ratios = preset_column_widths(config);
let ratio = *width_ratios.first().unwrap_or(&0.5);
let w = ratio * display_width;
let x_pos = (display_width - w).min(window.frame().origin.x);
(w, x_pos)
} else {
(display_width, 0.0)
};
reposition_entity(focused_entity, x, y, active_display.id(), commands);
resize_entity(focused_entity, width, height, active_display.id(), commands);
}
fn manage_window(
focused_entity: Entity,
windows: &mut Query<(&mut Window, Entity, Has<Unmanaged>)>,
commands: &mut Commands,
) {
let Ok((window, entity, unmanaged)) = windows.get_mut(focused_entity) else {
return;
};
debug!(
"{}: window: {} {entity} unmanaged: {unmanaged}.",
function_name!(),
window.id()
);
if unmanaged {
commands.entity(entity).try_remove::<Unmanaged>();
} else {
commands.entity(entity).try_insert(Unmanaged::Floating);
}
}
fn to_next_display(
focused_entity: Entity,
windows: &mut Query<(&mut Window, Entity, Has<Unmanaged>)>,
active_display: &mut ActiveDisplayMut,
commands: &mut Commands,
) {
let Ok((window, entity, unmanaged)) = windows.get_mut(focused_entity) else {
return;
};
if unmanaged {
return;
}
let Some(other) = active_display.other().next() else {
debug!("{}: no other display to move window to.", function_name!());
return;
};
debug!(
"{}: moving window (id {}, {entity}) to display {}: {}:{}.",
function_name!(),
window.id(),
other.id(),
other.bounds.size.width / 2.0,
other.menubar_height,
);
let dest = CGPoint::new(other.bounds.size.width / 2.0, other.menubar_height);
reposition_entity(focused_entity, dest.x, dest.y, other.id(), commands);
reshuffle_around(focused_entity, commands);
if let Ok(panel) = active_display.active_panel() {
panel.remove(entity);
}
}
fn command_windows(
operation: &Operation,
window_manager: &WindowManager,
active_display: &mut ActiveDisplayMut,
focused_entity: Entity,
windows: &mut Query<(&mut Window, Entity, Has<Unmanaged>)>,
commands: &mut Commands,
config: &Config,
) -> Result<()> {
let active_panel = active_display.active_panel()?;
let managed = windows
.get(focused_entity)
.is_ok_and(|(_, _, unmanaged)| !unmanaged);
if managed && active_panel.index_of(focused_entity).is_err() {
commands.trigger(WMEventTrigger(Event::SpaceChanged));
return Ok(());
}
let mut lens = windows.transmute_lens::<&mut Window>();
match operation {
Operation::Focus(direction) => {
let mut lens = windows.transmute_lens::<&Window>();
command_move_focus(direction, focused_entity, active_panel, &lens.query());
}
Operation::Swap(direction) => {
command_swap_focus(
direction,
focused_entity,
active_display,
&mut lens.query(),
commands,
);
}
Operation::Center => {
command_center_window(
focused_entity,
&mut lens.query(),
window_manager,
active_display,
commands,
);
}
Operation::Resize => {
resize_window(
active_display.display(),
focused_entity,
&mut lens.query(),
commands,
config,
);
}
Operation::FullWidth => {
full_width_window(
active_display.display(),
focused_entity,
&mut lens.query(),
commands,
config,
);
}
Operation::ToNextDisplay => {
to_next_display(focused_entity, windows, active_display, commands);
}
Operation::Manage => {
manage_window(focused_entity, windows, commands);
}
Operation::Stack(stack) => {
let (_, _, unmanaged) = windows.get(focused_entity)?;
if unmanaged {
return Ok(());
} else if *stack {
active_panel.stack(focused_entity)?;
} else {
active_panel.unstack(focused_entity)?;
}
}
}
reshuffle_around(focused_entity, commands);
Ok(())
}
#[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)]
pub fn process_command_trigger(
trigger: On<CommandTrigger>,
window_manager: Res<WindowManager>,
current_focus: Single<Entity, With<FocusedMarker>>,
mut windows: Query<(&mut Window, Entity, Has<Unmanaged>)>,
mut active_display: ActiveDisplayMut,
mut commands: Commands,
config: Res<Config>,
) {
let focused_entity = *current_focus;
let eligible = windows
.get(focused_entity)
.is_ok_and(|(window, _, _)| window.is_eligible());
let res = match &trigger.event().0 {
Command::Window(operation) => {
if eligible {
let mut lens = windows.transmute_lens::<(&mut Window, Entity, Has<Unmanaged>)>();
command_windows(
operation,
&window_manager,
&mut active_display,
focused_entity,
&mut lens.query(),
&mut commands,
config.as_ref(),
)
} else {
Ok(())
}
}
Command::PrintState => {
commands.trigger(WMEventTrigger(Event::PrintState));
Ok(())
}
Command::Quit => window_manager.quit(),
};
if let Err(err) = res {
error!("{}: {err}", function_name!());
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::*;
fn setup_world_with_layout() -> (World, WindowPane, Vec<Entity>) {
let mut world = World::new();
let entities = world
.spawn_batch(vec![(), (), (), ()])
.collect::<Vec<Entity>>();
let mut pane = WindowPane::default();
pane.append(entities[0]); pane.append(entities[1]);
pane.append(entities[2]);
pane.append(entities[3]);
pane.stack(entities[1]).unwrap();
(world, pane, entities)
}
#[test]
fn test_get_window_in_direction_simple() {
let (_world, pane, 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, &pane), Some(e3));
assert_eq!(get_window_in_direction(&west, e2, &pane), Some(e0));
assert_eq!(get_window_in_direction(&west, e3, &pane), Some(e2));
assert_eq!(get_window_in_direction(&east, e3, &pane), None);
assert_eq!(get_window_in_direction(&east, e0, &pane), Some(e2));
assert_eq!(get_window_in_direction(&west, e0, &pane), None);
}
#[test]
fn test_get_window_in_direction_stacked() {
let (_world, pane, 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, &pane), Some(e1));
assert_eq!(get_window_in_direction(&north, e0, &pane), None);
assert_eq!(get_window_in_direction(&north, e1, &pane), Some(e0));
assert_eq!(get_window_in_direction(&south, e1, &pane), None);
}
}