use bevy_ecs::{entity::MapEntities, prelude::*, reflect::ReflectMapEntities};
use bevy_reflect::{Reflect, ReflectDeserialize, ReflectSerialize, std_traits::ReflectDefault};
use bevy_ui::{FlexDirection, Node, Val};
use serde::{Deserialize, Serialize};
#[derive(Component, Reflect, Default, Clone)]
#[reflect(Component, Default)]
#[require(Node = root_node(), LayoutCache)]
pub struct Workspace;
#[derive(Component, Default)]
pub struct LayoutCache {
pub scene: Option<std::sync::Arc<bevy_world_serialization::DynamicWorld>>,
pub built_at: bevy_ecs::change_detection::Tick,
pub members: bevy_ecs::entity::EntityHashMap<Shape>,
}
pub type Shape = Box<[bevy_ecs::component::ComponentId]>;
pub(crate) fn shape(world: &World, entity: Entity) -> Shape {
let ignored = [
world.component_id::<Viewers>(),
world.component_id::<TabViewers>(),
world.component_id::<FocusedBy>(),
];
let mut ids: Vec<_> = world
.entity(entity)
.archetype()
.components()
.iter()
.copied()
.filter(|id| !ignored.contains(&Some(*id)))
.collect();
ids.sort();
ids.into_boxed_slice()
}
#[derive(Component, Reflect, Default, Clone)]
#[reflect(Component, Default)]
#[require(Node = split_node(FlexDirection::Row))]
pub struct Split;
#[derive(Component, Reflect, Default, Clone)]
#[reflect(Component, Default)]
#[require(Node = tab_node())]
pub struct Tab;
fn root_node() -> Node {
Node {
flex_basis: Val::ZERO,
..tab_node()
}
}
pub(crate) fn tab_node() -> Node {
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_basis: Val::Auto,
..leaf_node()
}
}
fn leaf_node() -> Node {
Node {
flex_grow: 1.0,
flex_basis: Val::ZERO,
min_width: Val::ZERO,
min_height: Val::ZERO,
..Default::default()
}
}
fn pane_node() -> Node {
Node {
min_width: Val::Px(2.0),
min_height: Val::Px(2.0),
..leaf_node()
}
}
pub(crate) fn split_node(direction: FlexDirection) -> Node {
Node {
flex_basis: Val::ZERO,
flex_direction: direction,
column_gap: Val::Px(1.0),
row_gap: Val::Px(1.0),
min_width: Val::Auto,
min_height: Val::Auto,
..leaf_node()
}
}
#[derive(Component, Reflect, Default, Clone, Copy)]
#[reflect(Component, Default)]
pub struct WorkspaceOrder(pub i64);
#[derive(Component, Reflect, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
#[serde(transparent)]
#[component(on_insert = crate::navigation::repair_later, on_remove = crate::navigation::repair_later)]
#[relationship(relationship_target = Viewers)]
pub struct Viewing(pub Entity);
#[derive(Component, Default)]
#[relationship_target(relationship = Viewing)]
pub struct Viewers(Vec<Entity>);
#[derive(Component, Reflect, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
#[serde(transparent)]
#[component(on_insert = crate::navigation::remember_tab, on_remove = crate::navigation::repair_later)]
#[relationship(relationship_target = TabViewers)]
pub struct OnTab(pub Entity);
#[derive(Component, Default)]
#[relationship_target(relationship = OnTab)]
pub struct TabViewers(Vec<Entity>);
#[derive(Component, Reflect, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
#[serde(transparent)]
#[component(on_insert = crate::navigation::remember_focus, on_remove = crate::navigation::repair_later)]
#[relationship(relationship_target = FocusedBy)]
pub struct Focused(pub Entity);
#[derive(Component, Default)]
#[relationship_target(relationship = Focused)]
pub struct FocusedBy(Vec<Entity>);
#[derive(Component, Default)]
pub struct Memory {
pub tabs: bevy_ecs::entity::EntityHashMap<Entity>,
pub focus: bevy_ecs::entity::EntityHashMap<Entity>,
pub previous: bevy_ecs::entity::EntityHashMap<Entity>,
}
#[derive(Component, Reflect, Clone, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
#[require(ProcessState)]
pub struct Launch {
pub argv: Vec<String>,
pub cwd: String,
pub history_lines: usize,
}
#[derive(Reflect, Clone, PartialEq, Default, Debug, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize)]
#[serde(rename_all = "snake_case", tag = "kind")]
pub enum Status {
#[default]
Starting,
Running {
pid: u32,
error: Option<String>,
},
Exited {
code: i32,
},
Failed {
error: String,
},
}
#[derive(Component, Reflect, Clone, PartialEq)]
#[reflect(Component, Default)]
pub struct ProcessState {
pub status: Status,
pub rows: u16,
pub cols: u16,
pub revision: u64,
}
impl Default for ProcessState {
fn default() -> Self {
Self {
status: Status::Starting,
rows: 24,
cols: 80,
revision: 0,
}
}
}
#[derive(Component, Reflect, Clone, MapEntities, Serialize, Deserialize)]
#[reflect(Component, MapEntities, Serialize, Deserialize)]
#[require(Node = pane_node())]
#[relationship(relationship_target = PaneViews)]
pub struct PaneView {
#[entities]
pub pane: Entity,
}
#[derive(Component, Reflect, Default)]
#[reflect(Component, Default)]
#[relationship_target(relationship = PaneView)]
pub struct PaneViews(Vec<Entity>);
pub type LayoutNode = (Workspace, Tab, Split, PaneView);
pub type LayoutRole = Or<(With<Workspace>, With<Tab>, With<Split>, With<PaneView>)>;
pub type NotLayout = (
Without<Workspace>,
Without<Tab>,
Without<Split>,
Without<PaneView>,
);
pub type IsViewer = (With<Viewer>, NotLayout);
#[derive(Component, Reflect, Clone, Serialize, Deserialize)]
#[reflect(Component, Serialize, Deserialize)]
#[require(Memory, crate::paste::Ownership)]
pub struct Viewer {
pub rows: u16,
pub cols: u16,
pub zoom: bool,
pub scrollback: usize,
pub notice: Option<Notice>,
}
impl Viewer {
pub(crate) fn reset_view(&mut self) {
self.zoom = false;
self.scrollback = 0;
}
}
#[derive(Reflect, Clone, PartialEq, Debug, Serialize, Deserialize)]
#[reflect(Serialize, Deserialize)]
pub struct Notice {
pub text: String,
pub error: bool,
}
impl Notice {
pub fn info(text: impl Into<String>) -> Option<Self> {
Some(Self {
text: text.into(),
error: false,
})
}
pub fn error(text: impl Into<String>) -> Option<Self> {
Some(Self {
text: text.into(),
error: true,
})
}
}
pub const DETACHED: &str = "viewer no longer attached";
pub(crate) fn viewing(world: &World, id: Entity) -> Option<Entity> {
world.get::<Viewing>(id).map(|v| v.0)
}
pub(crate) fn on_tab(world: &World, id: Entity) -> Option<Entity> {
world.get::<OnTab>(id).map(|t| t.0)
}
pub(crate) fn focused(world: &World, id: Entity) -> Option<Entity> {
world.get::<Focused>(id).map(|f| f.0)
}
pub(crate) fn notify(world: &mut World, id: Entity, notice: Option<Notice>) {
if let Some(mut v) = world.get_mut::<Viewer>(id) {
v.notice = notice;
}
}
#[derive(Resource, Clone)]
pub struct Wake(pub std::thread::Thread);
impl Wake {
pub fn notify(&self) {
self.0.unpark();
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testing::*;
#[test]
fn required_layout_nodes_preserve_defaults_and_explicit_overrides() -> Outcome {
let mut world = World::new();
let pane = world.spawn_empty().id();
let root = world.spawn(Workspace).id();
let tab = world.spawn(Tab).id();
let leaf = world.spawn(PaneView { pane }).id();
let row = world.spawn(Split).id();
assert_eq!(*world.get::<Node>(root).need()?, root_node());
assert_eq!(*world.get::<Node>(tab).need()?, tab_node());
assert_eq!(*world.get::<Node>(leaf).need()?, pane_node());
assert_eq!(pane_node().min_height, Val::Px(2.0));
assert_eq!(
*world.get::<Node>(row).need()?,
split_node(FlexDirection::Row)
);
let column = split_node(FlexDirection::Column);
let custom = Node {
width: Val::Px(37.0),
padding: bevy_ui::UiRect::all(Val::Px(3.0)),
..column.clone()
};
let split = world.spawn((Split, column.clone())).id();
let explicit = world.spawn((custom.clone(), Tab)).id();
assert_eq!(*world.get::<Node>(split).need()?, column);
world.entity_mut(explicit).insert(Split);
assert_eq!(*world.get::<Node>(explicit).need()?, custom);
world.entity_mut(explicit).remove::<Node>();
assert!(world.get::<Node>(explicit).is_none());
Ok(())
}
#[test]
fn layout_relationships_do_not_own_shared_processes() -> crate::testing::Outcome {
let mut world = World::new();
let process = world
.spawn(Launch {
argv: vec!["/bin/sh".into()],
cwd: String::new(),
history_lines: 0,
})
.id();
let left = world.spawn(Workspace).id();
let right = world.spawn(Workspace).id();
let removed = world
.spawn((PaneView { pane: process }, ChildOf(left)))
.id();
let retained = world
.spawn((PaneView { pane: process }, ChildOf(right)))
.id();
world.despawn(left);
assert!(world.get_entity(removed).is_err());
assert_eq!(world.get::<PaneView>(retained).need()?.pane, process);
assert!(world.get::<Launch>(process).is_some());
world.despawn(right);
assert!(world.get::<Launch>(process).is_some());
Ok(())
}
}