#[cfg(test)]
mod tests;
use crate::{control::Scope, model::*};
use bevy_ecs::{lifecycle::HookContext, prelude::*, world::DeferredWorld};
use bevy_ui::Node;
#[derive(Resource, Default)]
pub struct ApplyingLayout(pub u32);
pub struct ApplyGuard;
impl ApplyGuard {
pub fn begin(world: &mut World) -> Self {
let mut applying = world.get_resource_or_insert_with(ApplyingLayout::default);
applying.0 = applying.0.saturating_add(1);
Self
}
pub fn end(self, world: &mut World) {
if let Some(mut applying) = world.get_resource_mut::<ApplyingLayout>() {
applying.0 = applying.0.saturating_sub(1);
}
}
}
fn applying(applying: &Option<Res<ApplyingLayout>>) -> bool {
applying.as_ref().is_some_and(|a| a.0 > 0)
}
pub fn workspaces(world: &mut World) -> Vec<Entity> {
let mut roots: Vec<_> = world
.query_filtered::<Entity, With<Workspace>>()
.iter(world)
.collect();
roots.sort_by_key(|e| {
(
world.get::<WorkspaceOrder>(*e).map_or(0, |o| o.0),
e.to_bits(),
)
});
roots
}
pub fn tabs(world: &World, workspace: Entity) -> Vec<Entity> {
world
.get::<Children>(workspace)
.map(|children| {
children
.iter()
.filter(|e| world.get::<Tab>(*e).is_some())
.collect()
})
.unwrap_or_default()
}
pub fn leaves(world: &World, root: Entity) -> Vec<Entity> {
let mut result = Vec::new();
fn visit(world: &World, entity: Entity, result: &mut Vec<Entity>) {
if world.get::<PaneView>(entity).is_some() {
result.push(entity);
}
if let Some(children) = world.get::<Children>(entity) {
for child in children.iter() {
visit(world, child, result);
}
}
}
visit(world, root, &mut result);
result
}
pub fn normalize_workspace(world: &mut World, root: Entity) {
let loose: Vec<_> = world
.get::<Children>(root)
.map(|children| {
children
.iter()
.filter(|e| world.get::<Tab>(*e).is_none())
.collect()
})
.unwrap_or_default();
let legacy = tabs(world, root).is_empty();
if !loose.is_empty() || legacy {
let node = if legacy {
world.get::<Node>(root).cloned().unwrap_or_else(tab_node)
} else {
tab_node()
};
let tab = world
.spawn((Tab, Name::new("main"), node, ChildOf(root)))
.id();
if legacy {
world.entity_mut(root).insert(tab_node());
if world.get::<Split>(root).is_some() {
world.entity_mut(tab).insert(Split);
}
}
world.entity_mut(tab).add_children(&loose);
}
}
pub enum Pick {
Entity(Entity),
Next,
Previous,
}
pub fn tab_new(world: &mut World, id: Entity, name: Option<String>) -> Result<(), String> {
let root = viewing(world, id).ok_or(DETACHED)?;
let title = name.unwrap_or_else(|| format!("tab-{}", tabs(world, root).len() + 1));
let tab = world.spawn((Tab, Name::new(title), ChildOf(root))).id();
let settings = world.resource::<crate::assets::Settings>().clone();
let leaf = crate::server::spawn_pane(world, &settings, tab, None, None)?;
world
.get_entity_mut(id)
.map_err(|_| DETACHED)?
.insert((OnTab(tab), Focused(leaf)));
world.get_mut::<Viewer>(id).ok_or(DETACHED)?.zoom = false;
Ok(())
}
pub fn focus_last(world: &mut World, id: Entity) -> Result<(), String> {
let tab = on_tab(world, id).ok_or("no active tab")?;
let previous = world
.get::<Memory>(id)
.and_then(|memory| memory.previous.get(&tab).copied());
if let Some(previous) = previous
.filter(|e| leaves(world, tab).contains(e) && crate::frame::visible_leaf(world, id, *e))
{
world
.get_entity_mut(id)
.map_err(|_| DETACHED)?
.insert(Focused(previous));
}
Ok(())
}
pub fn select(world: &mut World, id: Entity, scope: Scope, pick: Pick) -> Result<(), String> {
let root = viewing(world, id).ok_or(DETACHED)?;
let tab = on_tab(world, id).ok_or("no active tab")?;
let (all, current) = match scope {
Scope::Workspace => (workspaces(world), root),
Scope::Tab => (tabs(world, root), tab),
};
let index = all
.iter()
.position(|e| *e == current)
.ok_or("target disappeared")?;
let selected = match pick {
Pick::Entity(entity) => all
.iter()
.copied()
.find(|e| *e == entity)
.ok_or("selection target no longer exists")?,
Pick::Previous => all
.get((index + all.len() - 1) % all.len())
.copied()
.ok_or("target disappeared")?,
Pick::Next => all
.get((index + 1) % all.len())
.copied()
.ok_or("target disappeared")?,
};
let mut entity = world.get_entity_mut(id).map_err(|_| DETACHED)?;
match scope {
Scope::Workspace => {
entity
.remove::<(OnTab, Focused)>()
.insert(Viewing(selected));
}
Scope::Tab => {
entity.remove::<Focused>().insert(OnTab(selected));
}
}
world.get_mut::<Viewer>(id).ok_or(DETACHED)?.reset_view();
Ok(())
}
pub(crate) fn normalize_on_tab_removed(
removed: On<Remove<Tab>>,
parents: Query<&ChildOf>,
in_progress: Option<Res<ApplyingLayout>>,
mut commands: Commands,
) {
if applying(&in_progress) {
return;
}
if let Ok(parent) = parents.get(removed.entity) {
let workspace = parent.parent();
commands.queue(move |world: &mut World| {
if world.get::<Workspace>(workspace).is_some() {
normalize_workspace(world, workspace);
}
});
}
}
pub(crate) fn normalize_on_child_added(
added: On<Insert<ChildOf>>,
parents: Query<&ChildOf>,
workspaces: Query<(), With<Workspace>>,
tabs: Query<(), With<Tab>>,
in_progress: Option<Res<ApplyingLayout>>,
mut commands: Commands,
) {
let entity = added.entity;
if let Ok(parent) = parents.get(entity) {
let mut cursor = parent.parent();
for _ in 0..u16::MAX {
if cursor == entity {
bevy_log::warn!(
"The ChildOf relationship on entity {entity} points into its own descendants. The cyclic ChildOf relationship has been removed."
);
commands.entity(entity).try_remove::<ChildOf>();
return;
}
match parents.get(cursor) {
Ok(next) => cursor = next.parent(),
Err(_) => break,
}
}
}
if applying(&in_progress) {
return;
}
if tabs.contains(entity)
&& parents
.get(entity)
.is_ok_and(|parent| !workspaces.contains(parent.parent()))
{
commands.queue(repair);
}
if let Ok(parent) = parents.get(entity)
&& workspaces.contains(parent.parent())
&& !tabs.contains(entity)
{
let workspace = parent.parent();
commands.queue(move |world: &mut World| {
if world.get::<Workspace>(workspace).is_some() {
normalize_workspace(world, workspace);
}
});
}
}
pub(crate) fn repair_on_tab_unlinked(
removed: On<Remove<ChildOf>>,
tabs: Query<(), With<Tab>>,
in_progress: Option<Res<ApplyingLayout>>,
mut commands: Commands,
) {
if applying(&in_progress) || !tabs.contains(removed.entity) {
return;
}
commands.queue(repair);
}
#[derive(Resource, Default)]
pub(crate) struct Repairing {
running: bool,
again: bool,
}
const MAX_PASSES: usize = 16;
fn absorbed(world: &mut DeferredWorld) -> bool {
match world.get_resource_mut::<Repairing>() {
Some(mut state) if state.running => {
state.again = true;
true
}
_ => false,
}
}
pub(crate) fn repair_later(mut world: DeferredWorld, _: HookContext) {
if !absorbed(&mut world) {
world.commands().queue(repair);
}
}
pub(crate) fn reject_viewer_on_layout(
inserted: On<Insert<(Viewer, LayoutNode)>>,
mixed: Query<(), (With<Viewer>, LayoutRole)>,
mut commands: Commands,
) {
let entity = inserted.entity;
if !mixed.contains(entity) {
return;
}
bevy_log::warn!(
"Entity {entity} is a layout node and cannot also be a viewer. The Viewer component has been removed."
);
commands.entity(entity).try_remove::<(
Viewer,
Viewing,
OnTab,
Focused,
Memory,
crate::paste::Ownership,
crate::presentation::Presentation,
crate::interaction::Prefix,
crate::interaction::Overlay,
crate::selection::Selection,
)>();
}
pub(crate) fn remember_tab(mut world: DeferredWorld, context: HookContext) {
if let (Some(viewing), Some(tab)) = (
viewing(&world, context.entity),
on_tab(&world, context.entity),
) && let Some(mut memory) = world.get_mut::<Memory>(context.entity)
{
memory.tabs.insert(viewing, tab);
}
repair_later(world, context);
}
pub(crate) fn remember_focus(mut world: DeferredWorld, context: HookContext) {
if let (Some(tab), Some(focus)) = (
on_tab(&world, context.entity),
focused(&world, context.entity),
) && let Some(mut memory) = world.get_mut::<Memory>(context.entity)
&& let Some(old) = memory.focus.insert(tab, focus)
&& old != focus
&& std::iter::successors(world.get::<ChildOf>(old), |parent| {
world.get::<ChildOf>(parent.parent())
})
.any(|parent| parent.parent() == tab)
&& let Some(mut memory) = world.get_mut::<Memory>(context.entity)
{
memory.previous.insert(tab, old);
}
repair_later(world, context);
}
pub(crate) fn forget<C: Component>(
removed: On<Remove<C>>,
mut viewers: Query<&mut Memory>,
mut commands: Commands,
) {
let gone = removed.entity;
commands.queue(repair);
for mut memory in &mut viewers {
memory
.tabs
.retain(|key, value| *key != gone && *value != gone);
memory
.focus
.retain(|key, value| *key != gone && *value != gone);
memory
.previous
.retain(|key, value| *key != gone && *value != gone);
}
}
pub(crate) fn observe(world: &mut World) {
world.add_observer(reject_viewer_on_layout);
world.add_observer(normalize_on_tab_removed);
world.add_observer(normalize_on_child_added);
world.add_observer(repair_on_tab_unlinked);
world.add_observer(forget::<Workspace>);
world.add_observer(forget::<Tab>);
world.add_observer(forget::<PaneView>);
}
pub fn repair(world: &mut World) {
let mut state = world.get_resource_or_insert_with(Repairing::default);
if state.running {
state.again = true;
return;
}
state.running = true;
let mut passes = 0;
loop {
world.get_resource_or_insert_with(Repairing::default).again = false;
repair_viewers(world);
passes += 1;
if !world.get_resource_or_insert_with(Repairing::default).again {
break;
}
if passes == MAX_PASSES {
bevy_log::warn!(
"Viewer repair did not settle after {MAX_PASSES} passes; the remaining inconsistency is left for the next change."
);
break;
}
}
let mut state = world.get_resource_or_insert_with(Repairing::default);
state.running = false;
state.again = false;
}
fn repair_viewers(world: &mut World) {
let roots = workspaces(world);
let viewers: Vec<_> = world
.query_filtered::<Entity, IsViewer>()
.iter(world)
.collect();
for id in viewers {
let workspace = match viewing(world, id).filter(|w| roots.contains(w)) {
Some(workspace) => workspace,
None => {
let Some(&root) = roots.first() else {
world.despawn(id);
continue;
};
world.entity_mut(id).insert(Viewing(root));
root
}
};
let mut available = tabs(world, workspace);
if available.is_empty() {
normalize_workspace(world, workspace);
available = tabs(world, workspace);
}
let remembered =
|world: &World,
key: Entity,
which: fn(&Memory) -> &bevy_ecs::entity::EntityHashMap<Entity>| {
world
.get::<Memory>(id)
.and_then(|m| which(m).get(&key).copied())
};
let tab = match on_tab(world, id).filter(|t| available.contains(t)) {
Some(tab) => tab,
None => {
let Some(tab) = remembered(world, workspace, |m| &m.tabs)
.filter(|t| available.contains(t))
.or_else(|| available.first().copied())
else {
continue;
};
world.entity_mut(id).insert(OnTab(tab));
tab
}
};
let visible = leaves(world, tab);
if focused(world, id).is_none_or(|f| !visible.contains(&f)) {
let focus = remembered(world, tab, |m| &m.focus)
.filter(|e| visible.contains(e))
.or_else(|| visible.first().copied());
let mut entity = world.entity_mut(id);
match focus {
Some(focus) => {
entity.insert(Focused(focus));
}
None => {
entity.remove::<Focused>();
}
}
if let Some(mut v) = world.get_mut::<Viewer>(id) {
v.reset_view();
}
}
}
}