use bevy::ecs::component::Component;
use bevy::ecs::entity::Entity;
use bevy::math::IRect;
use std::collections::VecDeque;
use stdext::function_name;
use tracing::debug;
use crate::errors::{Error, Result};
use crate::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()
}
pub fn at_or_last(&self, index: usize) -> Option<Entity> {
match self {
Column::Single(id) => Some(*id),
Column::Stack(stack) => stack.get(index).or_else(|| stack.last()).copied(),
}
}
pub fn position_of(&self, entity: Entity) -> Option<usize> {
match self {
Column::Single(id) => (*id == entity).then_some(0),
Column::Stack(stack) => stack.iter().position(|&e| e == entity),
}
}
}
#[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()?;
let stack_pos = self.columns.get(index)?.position_of(entity)?;
(index < self.columns.len())
.then_some(index + 1)
.and_then(|i| self.columns.get(i))
.and_then(|col| col.at_or_last(stack_pos))
}
pub fn left_neighbour(&self, entity: Entity) -> Option<Entity> {
let index = self.index_of(entity).ok()?;
let stack_pos = self.columns.get(index)?.position_of(entity)?;
(index > 0)
.then(|| index - 1)
.and_then(|i| self.columns.get(i))
.and_then(|col| col.at_or_last(stack_pos))
}
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
}
pub fn absolute_positions<W>(
&self,
get_window_frame: &W,
) -> impl Iterator<Item = (&Column, i32)>
where
W: Fn(Entity) -> Option<IRect>,
{
let mut left_edge = 0;
self.all_columns()
.into_iter()
.filter_map(|entity| {
let frame = get_window_frame(entity);
let column = self
.index_of(entity)
.ok()
.and_then(|index| self.columns.get(index));
column.zip(frame)
})
.map(move |(column, frame)| {
let temp = left_edge;
left_edge += frame.width();
(column, temp)
})
}
pub fn calculate_layout<W>(
&self,
offset: i32,
viewport: &IRect,
get_window_frame: &W,
) -> impl Iterator<Item = (Entity, IRect)>
where
W: Fn(Entity) -> Option<IRect>,
{
const MIN_WINDOW_HEIGHT: i32 = 200;
const WINDOW_HIDDEN_THRESHOLD: i32 = 10;
self.absolute_positions(get_window_frame)
.filter_map(move |(column, position)| {
let windows = match column {
Column::Single(entity) => vec![*entity],
Column::Stack(stack) => stack.clone(),
};
let current_heights = windows
.iter()
.filter_map(|&entity| get_window_frame(entity))
.map(|frame| frame.height())
.collect::<Vec<_>>();
let heights =
binpack_heights(¤t_heights, MIN_WINDOW_HEIGHT, viewport.height())?;
let column_width = windows
.first()
.and_then(|&entity| get_window_frame(entity))
.map(|frame| frame.width())?;
let mut prev_y = 0;
let frames = windows
.into_iter()
.zip(heights)
.filter_map(|(entity, height)| {
let mut frame = get_window_frame(entity)?;
let top_left = position - offset;
frame.min.x = top_left.clamp(
viewport.min.x + WINDOW_HIDDEN_THRESHOLD - column_width,
viewport.width() - WINDOW_HIDDEN_THRESHOLD,
);
frame.min.y = prev_y;
prev_y += height;
frame.max.x = frame.min.x + column_width;
frame.max.y = frame.min.y + height;
Some((entity, frame))
})
.collect::<Vec<_>>();
Some(frames)
})
.flatten()
}
pub fn above(&self, entity: Entity) -> Option<Entity> {
let stack = self.index_of(entity).and_then(|idx| self.get(idx)).ok()?;
match stack {
Column::Single(_) => None,
Column::Stack(items) => {
let pos = items.iter().position(|&e| e == entity)?;
(pos > 0).then(|| items[pos - 1])
}
}
}
}
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(", "))
}
}
pub fn binpack_heights(heights: &[i32], min_height: i32, total_height: i32) -> Option<Vec<i32>> {
let mut count = heights.len();
let mut output = vec![];
loop {
let mut idx = 0;
let mut remaining = total_height;
while idx < count {
let remaining_windows = heights.len() - idx;
if heights[idx] < remaining {
if idx + 1 == count {
output.push(remaining);
} else {
output.push(heights[idx]);
}
remaining -= heights[idx];
} else if remaining >= min_height * i32::try_from(remaining_windows).ok()? {
output.push(remaining);
remaining = 0;
} else {
break;
}
idx += 1;
}
if idx == count {
break;
}
count -= 1;
output.clear();
}
let remaining = i32::try_from(heights.len() - count).ok()?;
if remaining > 0 && count > 0 {
count -= 1;
output.truncate(count);
let sum = output.iter().sum::<i32>();
let avg_height = (f64::from(total_height - sum) / f64::from(remaining + 1)) as i32;
if avg_height < min_height {
return None;
}
while count < heights.len() {
output.push(avg_height);
count += 1;
}
}
Some(output)
}
#[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);
}
#[test]
fn test_binpack() {
const MIN_HEIGHT: i32 = 100;
let heights = [300, 300, 300, 300];
let out = binpack_heights(&heights, MIN_HEIGHT, 1500).unwrap();
assert_eq!(out, vec![300, 300, 300, 600]);
let out = binpack_heights(&heights, MIN_HEIGHT, 1024).unwrap();
assert_eq!(out, vec![300, 300, 300, 124]);
let out = binpack_heights(&heights, MIN_HEIGHT, 800).unwrap();
assert_eq!(out, vec![300, 300, 100, 100]);
let out = binpack_heights(&heights, MIN_HEIGHT, 440).unwrap();
assert_eq!(out, vec![110, 110, 110, 110]);
let out = binpack_heights(&heights, MIN_HEIGHT, 390);
assert_eq!(out, None);
}
#[test]
fn test_layout_positioning() {
let mut world = World::new();
let entities = world
.spawn_batch(vec![(), (), (), ()])
.collect::<Vec<Entity>>();
let sizes = [
IRect::new(0, 0, 300, 300),
IRect::new(0, 0, 300, 300),
IRect::new(0, 0, 300, 300),
IRect::new(0, 0, 300, 300),
];
let mut strip = LayoutStrip::default();
strip.append(entities[0]);
strip.append(entities[1]);
strip.append(entities[2]);
strip.append(entities[3]);
_ = strip.stack(entities[2]);
let viewport = IRect::new(0, 0, 600, 500);
let get_window_frame = |_| Some(sizes[0]);
let out = strip
.calculate_layout(50, &viewport, &get_window_frame)
.collect::<Vec<_>>();
let xpos = out.iter().map(|(_, frame)| frame.min.x).collect::<Vec<_>>();
assert_eq!(xpos, vec![-50, 250, 250, 550]);
let height = out
.iter()
.map(|(_, frame)| frame.height())
.collect::<Vec<_>>();
assert_eq!(height, vec![500, 300, 200, 500]);
}
#[test]
fn test_layout_singles_get_full_viewport_height() {
let mut world = World::new();
let entities = world.spawn_batch(vec![(), (), ()]).collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
for &e in &entities {
strip.append(e);
}
let viewport = IRect::new(0, 0, 900, 800);
let frame = |_| Some(IRect::new(0, 0, 300, 400));
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
assert_eq!(out.len(), 3);
for (_, f) in &out {
assert_eq!(f.height(), 800, "single window should fill viewport height");
assert_eq!(f.min.y, 0);
}
let xs: Vec<_> = out.iter().map(|(_, f)| f.min.x).collect();
assert_eq!(xs, vec![0, 300, 600]);
}
#[test]
fn test_layout_stack_shares_height_and_width() {
let mut world = World::new();
let entities = world
.spawn_batch(vec![(), (), (), ()])
.collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
for &e in &entities {
strip.append(e);
}
strip.stack(entities[1]).unwrap();
strip.stack(entities[2]).unwrap();
let viewport = IRect::new(0, 0, 800, 600);
let frame = |e: Entity| {
if e == entities[0] {
Some(IRect::new(0, 0, 400, 200))
} else if e == entities[1] || e == entities[2] {
Some(IRect::new(0, 0, 300, 200))
} else {
Some(IRect::new(0, 0, 400, 500))
}
};
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
assert_eq!(out.len(), 4);
for &(e, ref f) in &out {
if e == entities[0] || e == entities[1] || e == entities[2] {
assert_eq!(
f.width(),
400,
"stacked window should use top window's width"
);
}
}
let stack_heights: i32 = out
.iter()
.filter(|(e, _)| *e != entities[3])
.map(|(_, f)| f.height())
.sum();
assert_eq!(stack_heights, 600, "stack heights must sum to viewport");
let stack_frames: Vec<_> = out
.iter()
.filter(|(e, _)| *e != entities[3])
.map(|(_, f)| *f)
.collect();
assert_eq!(stack_frames[0].min.y, 0);
assert_eq!(stack_frames[0].max.y, stack_frames[1].min.y);
assert_eq!(stack_frames[1].max.y, stack_frames[2].min.y);
assert_eq!(stack_frames[2].max.y, 600);
let e3_frame = out.iter().find(|(e, _)| *e == entities[3]).unwrap().1;
assert_eq!(e3_frame.height(), 600);
}
#[test]
fn test_layout_offscreen_sliver_clamping() {
let mut world = World::new();
let entities = world.spawn_batch(vec![(), (), ()]).collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
for &e in &entities {
strip.append(e);
}
let viewport = IRect::new(0, 0, 600, 400);
let frame = |_| Some(IRect::new(0, 0, 300, 300));
let out: Vec<_> = strip.calculate_layout(5000, &viewport, &frame).collect();
for (_, f) in &out {
assert!(
f.min.x >= -290,
"window should not be pushed further left than sliver allows: {}",
f.min.x
);
assert!(
f.min.x <= 590,
"window should not start past right sliver threshold: {}",
f.min.x
);
}
let out: Vec<_> = strip.calculate_layout(-5000, &viewport, &frame).collect();
for (_, f) in &out {
assert!(f.min.x >= -290);
assert!(f.min.x <= 590);
}
}
#[test]
fn test_layout_unstack_gives_full_height() {
let mut world = World::new();
let entities = world.spawn_batch(vec![(), (), ()]).collect::<Vec<Entity>>();
let mut strip = LayoutStrip::default();
for &e in &entities {
strip.append(e);
}
strip.stack(entities[1]).unwrap();
let viewport = IRect::new(0, 0, 600, 500);
let frame = |_| Some(IRect::new(0, 0, 300, 250));
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
let e1_height = out
.iter()
.find(|(e, _)| *e == entities[1])
.unwrap()
.1
.height();
assert!(e1_height < 500, "stacked e1 should not have full height");
strip.unstack(entities[1]).unwrap();
assert_eq!(strip.len(), 3);
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
for (_, f) in &out {
assert_eq!(
f.height(),
500,
"after unstack every single column gets full viewport height"
);
}
}
#[test]
fn test_layout_restack_restores_shared_heights() {
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]);
let viewport = IRect::new(0, 0, 600, 500);
let frame = |_| Some(IRect::new(0, 0, 300, 250));
strip.stack(entities[1]).unwrap();
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
let heights: Vec<_> = out.iter().map(|(_, f)| f.height()).collect();
assert_eq!(heights.iter().sum::<i32>(), 500);
assert_eq!(heights.len(), 2);
strip.unstack(entities[1]).unwrap();
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
for (_, f) in &out {
assert_eq!(f.height(), 500);
}
strip.stack(entities[1]).unwrap();
let out: Vec<_> = strip.calculate_layout(0, &viewport, &frame).collect();
let heights: Vec<_> = out.iter().map(|(_, f)| f.height()).collect();
assert_eq!(heights.iter().sum::<i32>(), 500);
assert_eq!(heights.len(), 2);
}
}