use std::collections::HashMap;
use std::fmt;
use std::sync::Arc;
use bevy::ecs::relationship::RelatedSpawner;
use bevy::ecs::spawn::SpawnWith;
use bevy::ecs::system::SystemParam;
use bevy::input::ButtonInput;
use bevy::input::ButtonState;
use bevy::input::keyboard::{KeyCode, KeyboardInput};
use bevy::input_focus::{FocusCause, FocusedInput, InputFocus, InputFocusVisible};
use bevy::picking::events::{PointerCancel, PointerDrag, PointerDragEnd, PointerDragStart};
use bevy::picking::pointer::PointerButton;
use bevy::prelude::*;
use bevy::ui::{ComputedNode, UiGlobalTransform};
use bevy::ui_widgets::{ValueChange, observe};
use crate::cancel::Cancel;
use crate::dock_layout::{DockDropTarget, DockLayout, DockSkip};
use crate::model::{
DOCK_PANE_MIN_PX, DockZone, MIN_TARGET_PX, dock_drop_rect, dock_edge_zone, dock_neighbour,
dock_tab_slot, dock_zone,
};
use crate::nav::show_focus;
use crate::skin::Role;
use crate::split::{SplitFraction, SplitMin};
use crate::widgets::{panel, split_in, tab_page, tabs_in};
const GHOST_OFFSET_PX: f32 = 14.0;
type SpawnPanel = Arc<dyn Fn(&mut EntityCommands) + Send + Sync>;
#[derive(Clone)]
struct PanelDef {
title: String,
spawn: SpawnPanel,
}
#[derive(Resource, Default, Clone)]
pub struct PanelRegistry {
panels: HashMap<String, PanelDef>,
}
impl fmt::Debug for PanelRegistry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut ids: Vec<&str> = self.panels.keys().map(String::as_str).collect();
ids.sort_unstable();
f.debug_struct("PanelRegistry")
.field("panels", &ids)
.finish()
}
}
impl PanelRegistry {
pub fn register(
&mut self,
id: impl Into<String>,
title: impl Into<String>,
spawn: impl Fn(&mut EntityCommands) + Send + Sync + 'static,
) -> &mut Self {
self.panels.insert(
id.into(),
PanelDef {
title: title.into(),
spawn: Arc::new(spawn),
},
);
self
}
pub fn contains(&self, id: &str) -> bool {
self.panels.contains_key(id)
}
pub fn title<'a>(&'a self, id: &'a str) -> &'a str {
self.panels.get(id).map_or(id, |panel| panel.title.as_str())
}
}
#[derive(Component, Clone, Debug, PartialEq)]
#[require(DockShown)]
pub struct Dock(pub DockLayout);
#[derive(Component, Clone, Debug, PartialEq, Eq, Hash)]
pub struct DockPanel(pub String);
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct DockLayoutChanged {
#[event_target]
pub entity: Entity,
pub layout: DockLayout,
}
#[derive(EntityEvent, Clone, Debug, PartialEq)]
pub struct DockPanelSkipped {
#[event_target]
pub entity: Entity,
pub id: String,
pub reason: DockSkip,
}
#[derive(Component, Default)]
pub(crate) struct DockShown {
built: bool,
layout: Option<DockLayout>,
root: Option<Entity>,
pages: HashMap<String, Entity>,
refocus: Option<(String, bool)>,
}
#[derive(Component, Clone, Debug)]
pub(crate) struct DockNode {
dock: Entity,
path: Vec<bool>,
}
#[derive(Component)]
pub(crate) struct DockGroup;
#[derive(Component, Clone, Debug)]
pub(crate) struct DockTab {
dock: Entity,
panel: String,
}
#[derive(Component, Clone, Debug)]
pub(crate) struct DockDrag {
panel: String,
pointer: Vec2,
target: Option<DockDropTarget>,
}
pub(crate) fn adopt(entities: Vec<Entity>) -> impl Bundle {
Children::spawn(SpawnWith(move |parent: &mut RelatedSpawner<ChildOf>| {
let target = parent.target_entity();
for entity in entities {
parent
.world_mut()
.entity_mut(entity)
.insert(ChildOf(target));
}
}))
}
pub(crate) fn update_docks(
registry: Res<PanelRegistry>,
mut docks: Query<(Entity, Ref<Dock>, &mut DockShown, &Children)>,
roles: Query<&Role>,
mut commands: Commands,
) {
for (entity, dock, mut shown, kids) in &mut docks {
if shown.built && !dock.is_changed() && !registry.is_changed() {
continue;
}
let (layout, skipped) = dock.0.prune(|id| registry.contains(id));
if shown.built && layout == shown.layout {
continue;
}
let Some(body) = kids
.iter()
.find(|&kid| roles.get(kid) == Ok(&Role::DockBody))
else {
continue;
};
let wanted = layout.as_ref().map(DockLayout::panels).unwrap_or_default();
let mut pages = HashMap::new();
for (id, page) in std::mem::take(&mut shown.pages) {
if wanted.contains(&id.as_str()) {
commands.entity(page).remove::<ChildOf>();
pages.insert(id, page);
} else {
commands.entity(page).despawn();
}
}
for id in wanted {
if !pages.contains_key(id) {
let mut page = commands.spawn((tab_page(()), DockPanel(id.to_owned())));
if let Some(def) = registry.panels.get(id) {
(def.spawn)(&mut page);
}
pages.insert(id.to_owned(), page.id());
}
}
if let Some(old) = shown.root.take() {
commands.entity(old).despawn();
}
let root = layout
.as_ref()
.map(|layout| spawn_node(&mut commands, entity, ®istry, &pages, layout, Vec::new()));
if let Some(root) = root {
commands.entity(body).add_child(root);
}
for (id, reason) in skipped {
warn!("dock: panel {id:?} is not shown ({reason:?})");
commands.trigger(DockPanelSkipped { entity, id, reason });
}
if let Some((_, rebuilt)) = shown.refocus.as_mut() {
*rebuilt = true;
}
shown.built = true;
shown.layout = layout;
shown.root = root;
shown.pages = pages;
}
}
fn spawn_node(
commands: &mut Commands,
dock: Entity,
registry: &PanelRegistry,
pages: &HashMap<String, Entity>,
layout: &DockLayout,
path: Vec<bool>,
) -> Entity {
match layout {
DockLayout::Split {
axis,
fraction,
first,
second,
} => {
let down = |second: bool| {
let mut path = path.clone();
path.push(second);
path
};
let a = spawn_node(commands, dock, registry, pages, first, down(false));
let b = spawn_node(commands, dock, registry, pages, second, down(true));
commands
.spawn((
split_in(*axis, adopt(vec![a]), adopt(vec![b])),
SplitFraction(*fraction),
SplitMin::new(DOCK_PANE_MIN_PX, DOCK_PANE_MIN_PX),
DockNode { dock, path },
))
.id()
}
DockLayout::Group { panels, selected } => {
let labels: Vec<(String, DockTab)> = panels
.iter()
.map(|id| {
(
registry.title(id).to_owned(),
DockTab {
dock,
panel: id.clone(),
},
)
})
.collect();
let shown: Vec<Entity> = panels
.iter()
.filter_map(|id| pages.get(id))
.copied()
.collect();
commands
.spawn((
panel(),
DockGroup,
DockNode { dock, path },
children![tabs_in(
labels.into_iter().map(|(title, tab)| {
(
title,
(
tab,
observe(tab_on_drag_start),
observe(tab_on_drag),
observe(tab_on_drag_end),
observe(tab_on_pointer_cancel),
observe(tab_on_cancel),
observe(tab_on_key),
),
)
}),
*selected,
adopt(shown),
true,
)],
))
.id()
}
}
}
pub(crate) fn dock_on_select(
change: On<ValueChange<usize>>,
parents: Query<&ChildOf>,
nodes: Query<&DockNode>,
mut docks: Query<(&mut Dock, &mut DockShown)>,
mut commands: Commands,
) {
let Some(node) = parents
.get(change.source)
.ok()
.and_then(|parent| nodes.get(parent.parent()).ok())
else {
return;
};
let value = change.value;
commit(
&mut docks,
node.dock,
&mut commands,
|layout| match layout.node_mut(&node.path) {
Some(DockLayout::Group { selected, .. }) if *selected != value => {
*selected = value;
true
}
_ => false,
},
);
}
pub(crate) fn dock_on_resize(
change: On<ValueChange<f32>>,
nodes: Query<&DockNode>,
mut docks: Query<(&mut Dock, &mut DockShown)>,
mut commands: Commands,
) {
if !change.is_final {
return;
}
let Ok(node) = nodes.get(change.source) else {
return;
};
let value = change.value;
commit(
&mut docks,
node.dock,
&mut commands,
|layout| match layout.node_mut(&node.path) {
Some(DockLayout::Split { fraction, .. }) if *fraction != value => {
*fraction = value;
true
}
_ => false,
},
);
}
fn commit(
docks: &mut Query<(&mut Dock, &mut DockShown)>,
dock: Entity,
commands: &mut Commands,
edit: impl FnOnce(&mut DockLayout) -> bool,
) {
let Ok((mut state, mut shown)) = docks.get_mut(dock) else {
return;
};
if shown.refocus.is_some() {
return;
}
let Some(layout) = shown.layout.as_mut() else {
return;
};
if edit(layout) {
let layout = layout.clone();
state.0 = layout.clone();
commands.trigger(DockLayoutChanged {
entity: dock,
layout,
});
}
}
fn move_panel(
docks: &mut Query<(&mut Dock, &mut DockShown)>,
dock: Entity,
panel: &str,
target: &DockDropTarget,
commands: &mut Commands,
) {
let Ok((mut state, mut shown)) = docks.get_mut(dock) else {
return;
};
let Some(layout) = shown.layout.as_ref().and_then(|l| l.moved(panel, target)) else {
return;
};
state.0 = layout.clone();
shown.refocus = Some((panel.to_owned(), false));
commands.trigger(DockLayoutChanged {
entity: dock,
layout,
});
}
#[derive(SystemParam)]
pub(crate) struct DockGeometry<'w, 's> {
ui_scale: Option<Res<'w, UiScale>>,
groups: Query<
'w,
's,
(
Entity,
&'static DockNode,
&'static ComputedNode,
&'static UiGlobalTransform,
&'static Children,
),
With<DockGroup>,
>,
children: Query<'w, 's, &'static Children>,
roles: Query<'w, 's, &'static Role>,
tabs: Query<
'w,
's,
(
&'static DockTab,
&'static ComputedNode,
&'static UiGlobalTransform,
),
>,
frames: Query<'w, 's, (&'static ComputedNode, &'static UiGlobalTransform)>,
parents: Query<'w, 's, &'static ChildOf>,
}
struct GroupSeen {
entity: Entity,
card: Rect,
strip: Rect,
tabs: Vec<(String, f32, f32, f32)>,
}
struct Landing {
target: DockDropTarget,
rect: Rect,
}
impl DockGeometry<'_, '_> {
fn scale(&self) -> f32 {
self.ui_scale.as_ref().map_or(1.0, |scale| scale.0)
}
fn rect(&self, entity: Entity) -> Option<Rect> {
let (node, at) = self.frames.get(entity).ok()?;
let scale = node.inverse_scale_factor();
Some(Rect::from_center_size(
at.translation * scale,
node.size() * scale,
))
}
fn groups_of(&self, dock: Entity) -> Vec<GroupSeen> {
self.groups
.iter()
.filter(|(_, node, ..)| node.dock == dock)
.filter_map(|(entity, _, _, _, kids)| {
let root = kids.first().copied()?;
let parts = self.children.get(root).ok()?;
let strip = parts
.iter()
.find(|&part| self.roles.get(part) == Ok(&Role::TabList))?;
let tabs = self
.children
.get(strip)
.ok()?
.iter()
.filter_map(|tab| {
let (tab, node, at) = self.tabs.get(tab).ok()?;
let scale = node.inverse_scale_factor();
let half = node.size().y * scale / 2.0;
let centre = at.translation * scale;
Some((
tab.panel.clone(),
centre.x,
centre.y - half,
centre.y + half,
))
})
.collect();
Some(GroupSeen {
entity,
card: self.rect(entity)?,
strip: self.rect(strip)?,
tabs,
})
})
.collect()
}
fn landing(
&self,
dock: Entity,
pointer: Vec2,
panel: &str,
layout: &DockLayout,
) -> Option<Landing> {
let at = pointer / self.scale();
let whole = self.rect(dock)?;
if !whole.contains(at) {
return None;
}
let rel = at - whole.min;
let groups = self.groups_of(dock);
let group = groups.iter().find(|g| g.card.contains(at));
let landing = if let Some(group) = group.filter(|g| at.y <= g.strip.max.y) {
let slots: Vec<(f32, f32, f32)> = group
.tabs
.iter()
.map(|&(_, x, top, bottom)| (x, top, bottom))
.collect();
Landing {
target: DockDropTarget {
onto: Some(group.tabs.first().map(|(id, ..)| id.clone())?),
zone: DockZone::Into,
index: Some(dock_tab_slot(&slots, at.x, at.y)),
},
rect: group.card,
}
} else if let Some(zone) =
dock_edge_zone(whole.width(), whole.height(), rel.x, rel.y, MIN_TARGET_PX)
{
Landing {
target: DockDropTarget {
onto: None,
zone,
index: None,
},
rect: part(whole, zone, true),
}
} else {
let group = group?;
let zone = dock_zone(
group.card.width(),
group.card.height(),
at.x - group.card.min.x,
at.y - group.card.min.y,
);
Landing {
target: DockDropTarget {
onto: Some(group.tabs.first().map(|(id, ..)| id.clone())?),
zone,
index: None,
},
rect: part(group.card, zone, false),
}
};
layout.moved(panel, &landing.target).map(|_| landing)
}
fn group_of_tab(&self, tab: Entity) -> Option<(Entity, Vec<GroupSeen>)> {
let dock = self.tabs.get(tab).ok()?.0.dock;
let strip = self.parents.get(tab).ok()?.parent();
let root = self.parents.get(strip).ok()?.parent();
let card = self.parents.get(root).ok()?.parent();
Some((card, self.groups_of(dock)))
}
}
fn part(rect: Rect, zone: DockZone, whole: bool) -> Rect {
let (x, y, w, h) = dock_drop_rect(zone, rect.width(), rect.height(), whole);
Rect::from_corners(
rect.min + Vec2::new(x, y),
rect.min + Vec2::new(x + w, y + h),
)
}
fn tab_on_drag_start(
mut start: On<PointerDragStart>,
tabs: Query<&DockTab>,
mut commands: Commands,
) {
start.propagate(false);
if start.button != PointerButton::Primary {
return;
}
if let Ok(tab) = tabs.get(start.entity) {
commands.entity(tab.dock).insert(DockDrag {
panel: tab.panel.clone(),
pointer: start.pointer.position,
target: None,
});
}
}
fn tab_on_drag(
mut drag: On<PointerDrag>,
tabs: Query<&DockTab>,
mut carried: Query<&mut DockDrag>,
) {
drag.propagate(false);
if let Ok(tab) = tabs.get(drag.entity)
&& let Ok(mut carried) = carried.get_mut(tab.dock)
{
carried.pointer = drag.pointer.position;
}
}
fn tab_on_drag_end(
mut end: On<PointerDragEnd>,
tabs: Query<&DockTab>,
carried: Query<&DockDrag>,
geometry: DockGeometry,
mut docks: Query<(&mut Dock, &mut DockShown)>,
mut commands: Commands,
) {
end.propagate(false);
let Ok(tab) = tabs.get(end.entity) else {
return;
};
commands.entity(tab.dock).remove::<DockDrag>();
let Ok(carried) = carried.get(tab.dock) else {
return;
};
let Some(layout) = docks
.get(tab.dock)
.ok()
.and_then(|(_, shown)| shown.layout.clone())
else {
return;
};
if let Some(landing) = geometry.landing(tab.dock, end.pointer.position, &carried.panel, &layout)
{
move_panel(
&mut docks,
tab.dock,
&carried.panel,
&landing.target,
&mut commands,
);
}
}
fn tab_on_pointer_cancel(
mut cancel: On<PointerCancel>,
tabs: Query<&DockTab>,
mut commands: Commands,
) {
cancel.propagate(false);
if let Ok(tab) = tabs.get(cancel.entity) {
commands.entity(tab.dock).remove::<DockDrag>();
}
}
fn tab_on_cancel(
mut cancel: On<Cancel>,
tabs: Query<&DockTab>,
carried: Query<(), With<DockDrag>>,
mut commands: Commands,
) {
if let Ok(tab) = tabs.get(cancel.entity)
&& carried.contains(tab.dock)
{
cancel.propagate(false);
commands.entity(tab.dock).remove::<DockDrag>();
}
}
fn tab_on_key(
mut input: On<FocusedInput<KeyboardInput>>,
keys: Res<ButtonInput<KeyCode>>,
mut shown: ResMut<InputFocusVisible>,
tabs: Query<&DockTab>,
geometry: DockGeometry,
mut docks: Query<(&mut Dock, &mut DockShown)>,
mut commands: Commands,
) {
if input.input.state != ButtonState::Pressed {
return;
}
let zone = match input.input.key_code {
KeyCode::ArrowLeft => DockZone::Left,
KeyCode::ArrowRight => DockZone::Right,
KeyCode::ArrowUp => DockZone::Top,
KeyCode::ArrowDown => DockZone::Bottom,
_ => return,
};
if !moves_tabs(&keys) {
return;
}
let Ok(tab) = tabs.get(input.focused_entity) else {
return;
};
input.propagate(false);
show_focus(&mut shown);
let Some((card, groups)) = geometry.group_of_tab(input.focused_entity) else {
return;
};
let Some(here) = groups.iter().find(|g| g.entity == card) else {
return;
};
let others: Vec<&GroupSeen> = groups.iter().filter(|g| g.entity != card).collect();
let centres: Vec<(f32, f32)> = others
.iter()
.map(|g| (g.card.center().x, g.card.center().y))
.collect();
let from = (here.card.center().x, here.card.center().y);
let target = match dock_neighbour(from, ¢res, zone) {
Some(index) => DockDropTarget {
onto: others[index].tabs.first().map(|(id, ..)| id.clone()),
zone: DockZone::Into,
index: None,
},
None => DockDropTarget {
onto: None,
zone,
index: None,
},
};
move_panel(&mut docks, tab.dock, &tab.panel, &target, &mut commands);
}
pub(crate) fn moves_tabs(keys: &ButtonInput<KeyCode>) -> bool {
keys.any_pressed([KeyCode::ControlLeft, KeyCode::ControlRight])
&& keys.any_pressed([KeyCode::ShiftLeft, KeyCode::ShiftRight])
}
pub(crate) fn update_dock_drag(
geometry: DockGeometry,
registry: Res<PanelRegistry>,
mut docks: Query<(Entity, &DockShown, &Children, Option<&mut DockDrag>)>,
mut gone: RemovedComponents<DockDrag>,
mut parts: Query<(&Role, &mut Node)>,
mut texts: Query<&mut Text>,
) {
let gone: Vec<Entity> = gone.read().collect();
for (entity, shown, kids, mut carried) in &mut docks {
if carried.is_none() && !gone.contains(&entity) {
continue;
}
let landing = carried.as_ref().and_then(|drag| {
let layout = shown.layout.as_ref()?;
geometry.landing(entity, drag.pointer, &drag.panel, layout)
});
if let Some(drag) = carried.as_mut() {
let target = landing.as_ref().map(|landing| landing.target.clone());
if drag.target != target {
drag.target = target;
}
}
let origin = geometry.rect(entity).map_or(Vec2::ZERO, |rect| rect.min);
for kid in kids.iter() {
let Ok((role, mut node)) = parts.get_mut(kid) else {
continue;
};
let mut next = node.clone();
match role {
Role::DockDrop => match &landing {
Some(landing) => {
let rect = Rect::from_corners(
landing.rect.min - origin,
landing.rect.max - origin,
);
next.display = Display::Flex;
next.left = Val::Px(rect.min.x);
next.top = Val::Px(rect.min.y);
next.width = Val::Px(rect.width());
next.height = Val::Px(rect.height());
}
None => next.display = Display::None,
},
Role::DockGhost => match &carried {
Some(drag) => {
let at = drag.pointer / geometry.scale() - origin;
next.display = Display::Flex;
next.left = Val::Px(at.x + GHOST_OFFSET_PX);
next.top = Val::Px(at.y + GHOST_OFFSET_PX);
if let Ok(mut text) = texts.get_mut(kid) {
text.set_if_neq(Text::new(registry.title(&drag.panel)));
}
}
None => next.display = Display::None,
},
_ => continue,
}
node.set_if_neq(next);
}
}
}
pub(crate) fn land_dock_focus(
mut docks: Query<(Entity, &mut DockShown)>,
tabs: Query<(Entity, &DockTab)>,
mut focus: ResMut<InputFocus>,
) {
for (dock, mut shown) in &mut docks {
let Some((panel, true)) = shown.refocus.as_ref() else {
continue;
};
if let Some((tab, _)) = tabs
.iter()
.find(|(_, tab)| tab.dock == dock && &tab.panel == panel)
{
focus.set(tab, FocusCause::Navigated);
shown.refocus = None;
}
}
}