use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use core::ptr::NonNull;
use log::debug;
use objc2_core_foundation::{CFRetained, CFString, CFUUID, CGRect};
use objc2_core_graphics::CGDirectDisplayID;
use std::collections::VecDeque;
use stdext::function_name;
use super::skylight::{CGDisplayCreateUUIDFromDisplayID, CGDisplayGetDisplayIDFromUUID};
use crate::{
errors::{Error, Result},
platform::WorkspaceId,
};
#[derive(Clone, Debug)]
pub enum Column {
Single(Entity),
Stack(Vec<Entity>),
}
impl Column {
pub fn top(&self) -> Option<Entity> {
match self {
Column::Single(id) => Some(id),
Column::Stack(stack) => stack.first(),
}
.copied()
}
}
#[derive(Component, Debug, Default)]
pub struct LayoutStrip {
id: WorkspaceId,
columns: VecDeque<Column>,
}
impl LayoutStrip {
pub fn new(id: WorkspaceId) -> Self {
Self {
id,
columns: VecDeque::new(),
}
}
pub fn index_of(&self, entity: Entity) -> Result<usize> {
self.columns
.iter()
.position(|column| match column {
Column::Single(id) => *id == entity,
Column::Stack(stack) => stack.contains(&entity),
})
.ok_or(Error::NotFound(format!(
"{}: can not find window {entity} in the current pane.",
function_name!()
)))
}
pub fn insert_at(&mut self, after: usize, entity: Entity) {
let index = after;
if index >= self.len() {
self.columns.push_back(Column::Single(entity));
} else {
self.columns.insert(index, Column::Single(entity));
}
}
pub fn append(&mut self, entity: Entity) {
self.columns.push_back(Column::Single(entity));
}
pub fn remove(&mut self, entity: Entity) {
let removed = self
.index_of(entity)
.ok()
.and_then(|index| self.columns.remove(index).zip(Some(index)));
if let Some((Column::Stack(mut stack), index)) = removed {
stack.retain(|id| *id != entity);
if stack.len() > 1 {
self.columns.insert(index, Column::Stack(stack));
} else if let Some(remaining_id) = stack.first() {
self.columns.insert(index, Column::Single(*remaining_id));
}
}
}
pub fn get(&self, at: usize) -> Result<Column> {
self.columns
.get(at)
.cloned()
.ok_or(Error::InvalidInput(format!(
"{}: {at} out of bounds",
function_name!()
)))
}
pub fn swap(&mut self, left: usize, right: usize) {
self.columns.swap(left, right);
}
pub fn len(&self) -> usize {
self.columns.len()
}
pub fn first(&self) -> Result<Column> {
self.columns.front().cloned().ok_or(Error::NotFound(format!(
"{}: can not find first element.",
function_name!()
)))
}
pub fn last(&self) -> Result<Column> {
self.columns.back().cloned().ok_or(Error::NotFound(format!(
"{}: can not find last element.",
function_name!()
)))
}
pub fn right_neighbour(&self, entity: Entity) -> Option<Entity> {
let index = self.index_of(entity).ok()?;
(index < self.columns.len())
.then_some(index + 1)
.and_then(|index| self.columns.get(index))
.and_then(Column::top)
}
pub fn left_neighbour(&self, entity: Entity) -> Option<Entity> {
let index = self.index_of(entity).ok()?;
(index > 0)
.then(|| index - 1)
.and_then(|index| self.columns.get(index))
.and_then(Column::top)
}
pub fn stack(&mut self, entity: Entity) -> Result<()> {
let index = self.index_of(entity)?;
if index == 0 {
return Ok(());
}
if let Column::Stack(_) = self.columns[index] {
return Ok(());
}
self.columns.remove(index);
let column = self.columns.remove(index - 1);
if let Some(column) = column {
let newstack = match column {
Column::Stack(mut stack) => {
stack.push(entity);
stack
}
Column::Single(id) => vec![id, entity],
};
debug!("Stacked windows: {newstack:#?}");
self.columns.insert(index - 1, Column::Stack(newstack));
}
Ok(())
}
pub fn unstack(&mut self, entity: Entity) -> Result<()> {
let index = self.index_of(entity)?;
if let Column::Single(_) = self.columns[index] {
return Ok(());
}
let column = self.columns.remove(index);
if let Some(column) = column {
let newstack = match column {
Column::Stack(mut stack) => {
stack.retain(|id| *id != entity);
if stack.len() == 1 {
Column::Single(stack[0])
} else {
Column::Stack(stack)
}
}
Column::Single(_) => unreachable!("Is checked at the start of the function"),
};
self.columns.insert(index, Column::Single(entity));
self.columns.insert(index, newstack);
}
Ok(())
}
pub fn all_windows(&self) -> Vec<Entity> {
self.columns
.iter()
.flat_map(|column| match column {
Column::Single(entity) => vec![*entity],
Column::Stack(ids) => ids.clone(),
})
.collect()
}
pub fn all_columns(&self) -> Vec<Entity> {
self.columns.iter().filter_map(Column::top).collect()
}
pub fn id(&self) -> WorkspaceId {
self.id
}
}
impl std::fmt::Display for LayoutStrip {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let out = self
.columns
.iter()
.map(|column| format!("{column:?}"))
.collect::<Vec<_>>();
write!(f, "[{}]", out.join(", "))
}
}
#[derive(Component)]
pub struct Display {
id: CGDirectDisplayID,
pub bounds: CGRect,
pub menubar_height: f64,
}
impl Display {
pub fn new(id: CGDirectDisplayID, bounds: CGRect, menubar_height: u32) -> Self {
Self {
id,
bounds,
menubar_height: menubar_height.into(),
}
}
pub fn uuid_from_id(id: CGDirectDisplayID) -> Result<CFRetained<CFString>> {
unsafe {
let uuid = NonNull::new(CGDisplayCreateUUIDFromDisplayID(id))
.map(|ptr| CFRetained::from_raw(ptr))
.ok_or(Error::InvalidInput(format!(
"{}: can not create uuid from {id}.",
function_name!()
)))?;
CFUUID::new_string(None, Some(&uuid)).ok_or(Error::InvalidInput(format!(
"{}: can not create string from {uuid:?}.",
function_name!()
)))
}
}
pub fn id_from_uuid(uuid: &CFRetained<CFString>) -> Result<u32> {
unsafe {
let id = CFUUID::from_string(None, Some(uuid)).ok_or(Error::NotFound(format!(
"{}: can not convert from {uuid}.",
function_name!()
)))?;
Ok(CGDisplayGetDisplayIDFromUUID(&id))
}
}
pub fn id(&self) -> CGDirectDisplayID {
self.id
}
}
#[cfg(test)]
mod tests {
use super::*;
use bevy::prelude::*;
fn setup_world_and_strip() -> (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]);
(world, strip, entities)
}
#[test]
fn test_window_pane_index_of() {
let (_world, strip, entities) = setup_world_and_strip();
assert_eq!(strip.index_of(entities[0]).unwrap(), 0);
assert_eq!(strip.index_of(entities[1]).unwrap(), 1);
assert_eq!(strip.index_of(entities[2]).unwrap(), 2);
}
#[test]
fn test_window_pane_swap() {
let (_world, mut strip, entities) = setup_world_and_strip();
strip.swap(0, 2);
assert_eq!(strip.index_of(entities[2]).unwrap(), 0);
assert_eq!(strip.index_of(entities[0]).unwrap(), 2);
}
#[test]
fn test_window_pane_stack_and_unstack() {
let (_world, mut strip, entities) = setup_world_and_strip();
strip.stack(entities[1]).unwrap();
assert_eq!(strip.len(), 2);
assert_eq!(strip.index_of(entities[0]).unwrap(), 0);
assert_eq!(strip.index_of(entities[1]).unwrap(), 0);
match strip.get(0).unwrap() {
Column::Stack(stack) => {
assert_eq!(stack.len(), 2);
assert_eq!(stack[0], entities[0]);
assert_eq!(stack[1], entities[1]);
}
Column::Single(_) => panic!("Expected a stack"),
}
strip.unstack(entities[0]).unwrap();
assert_eq!(strip.len(), 3);
assert_eq!(strip.index_of(entities[1]).unwrap(), 0);
assert_eq!(strip.index_of(entities[0]).unwrap(), 1);
assert_eq!(strip.index_of(entities[2]).unwrap(), 2);
}
}