use eframe::egui;
use egui_tiles::{
Behavior, Container, EditAction, TabState, Tile, TileId, Tiles, Tree, UiResponse,
};
use serde::{Deserialize, Serialize};
use crate::document::Documents;
use crate::panels::assembly_constraints::AssemblyConstraintsPanel;
use crate::panels::component_actions::ComponentActionRequest;
use crate::panels::bom::BomPanel;
use crate::panels::document_tabs::TabsOutcome;
use crate::panels::expressions::ExpressionsPanel;
use crate::panels::history::HistoryPanel;
use crate::panels::scene::ScenePanel;
use crate::panels::update_components::UpdateComponents;
use crate::panels::wire_harness::WireHarnessPanel;
use crate::panels::pmi::PmiPanel;
use crate::store::{ModelStore, DOCK_LAYOUT_KEY};
use crate::viewport::Viewport;
use crate::workbench;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug, Serialize, Deserialize)]
pub enum PaneKind {
Document(u64),
History,
AssemblyConstraints,
Bom,
WireHarness,
Pmi,
Scene,
Expressions,
}
impl PaneKind {
const SIDE: [PaneKind; 7] = [
PaneKind::History,
PaneKind::Scene,
PaneKind::Expressions,
PaneKind::AssemblyConstraints,
PaneKind::Bom,
PaneKind::WireHarness,
PaneKind::Pmi,
];
pub const ALL: [PaneKind; 7] = Self::SIDE;
pub fn title(self) -> &'static str {
match self {
PaneKind::Document(_) => "3D View",
PaneKind::History => "History",
PaneKind::Bom => "BOM",
PaneKind::AssemblyConstraints => "Constraints",
PaneKind::WireHarness => "Wire Harness",
PaneKind::Pmi => "PMI",
PaneKind::Scene => "Scene",
PaneKind::Expressions => "Expressions",
}
}
fn panel_id(self) -> Option<&'static str> {
match self {
PaneKind::Document(_) => None,
PaneKind::History => Some("history"),
PaneKind::Bom => Some(workbench::assembly::BOM_PANEL_ID),
PaneKind::AssemblyConstraints => Some(workbench::assembly::CONSTRAINTS_PANEL_ID),
PaneKind::WireHarness => Some(workbench::wire_harness::PANEL_ID),
PaneKind::Pmi => Some(workbench::pmi::PANEL_ID),
PaneKind::Scene => Some("scene"),
PaneKind::Expressions => Some("expressions"),
}
}
pub fn visible_under_workbench(self, wb: &str) -> bool {
self.visible_in(wb)
}
fn visible_in(self, wb: &str) -> bool {
match self.panel_id() {
None => true,
Some(id) => workbench::panel_visible(wb, id),
}
}
}
pub struct DockState {
tree: Tree<PaneKind>,
dirty: bool,
snapshot: Vec<(PaneKind, bool, bool)>,
}
pub struct DockContext<'a> {
pub docs: &'a mut Documents,
pub viewport: &'a mut Viewport,
pub history: &'a mut HistoryPanel,
pub bom: &'a mut BomPanel,
pub assembly_constraints: &'a mut AssemblyConstraintsPanel,
pub wire_harness: &'a mut WireHarnessPanel,
pub pmi: &'a mut PmiPanel,
pub scene: &'a mut ScenePanel,
pub expressions: &'a mut ExpressionsPanel,
pub update_components: &'a mut UpdateComponents,
pub model_store: &'a dyn ModelStore,
}
#[derive(Default)]
pub struct DockOutcome {
pub insert_component_requested: bool,
pub feature_focus: Option<String>,
pub component_request: Option<ComponentActionRequest>,
pub document_tabs: TabsOutcome,
}
impl DockState {
pub fn new(store: &dyn ModelStore) -> Self {
let tree = store
.read(DOCK_LAYOUT_KEY)
.and_then(|json| serde_json::from_str::<Tree<PaneKind>>(&json).ok())
.filter(salvageable)
.unwrap_or_else(default_tree);
Self {
tree,
dirty: false,
snapshot: Vec::new(),
}
}
pub fn ui(&mut self, ui: &mut egui::Ui, ctx: DockContext<'_>) -> DockOutcome {
let wb = ctx.docs.engine().settings.workbench.clone();
self.apply_workbench_visibility(&wb);
if self.sync_document_panes(ctx.docs) {
self.dirty = true;
}
if self.evict_foreign_panes_from_document_group() {
self.dirty = true;
}
let active_id = ctx.docs.active_id();
let document_ids: Vec<u64> = ctx.docs.iter().map(|d| d.id()).collect();
let store = ctx.model_store;
let mut behavior = DockBehavior {
docs: ctx.docs,
viewport: ctx.viewport,
history: ctx.history,
bom: ctx.bom,
assembly_constraints: ctx.assembly_constraints,
wire_harness: ctx.wire_harness,
pmi: ctx.pmi,
scene: ctx.scene,
expressions: ctx.expressions,
update_components: ctx.update_components,
model_store: ctx.model_store,
insert_component_requested: false,
feature_focus: None,
component_request: None,
document_tabs: TabsOutcome::default(),
tab_title_spacing: 0.0,
layout_changed: false,
rendered: Vec::new(),
};
behavior.tab_title_spacing = behavior.tab_title_spacing(ui.visuals());
self.tree.ui(&mut behavior, ui);
behavior.document_tabs.activate = self.tab_bar_selection(active_id, &document_ids);
let outcome = DockOutcome {
insert_component_requested: behavior.insert_component_requested,
feature_focus: behavior.feature_focus.take(),
component_request: behavior.component_request.take(),
document_tabs: std::mem::take(&mut behavior.document_tabs),
};
if behavior.layout_changed {
self.dirty = true;
}
let rendered = std::mem::take(&mut behavior.rendered);
drop(behavior);
self.snapshot = self
.tree
.tiles
.iter()
.filter_map(|(id, tile)| match tile {
Tile::Pane(kind) => Some((
*kind,
self.tree.tiles.is_visible(*id),
rendered.contains(kind),
)),
Tile::Container(_) => None,
})
.collect();
if self.dirty && !ui.ctx().input(|i| i.pointer.any_down()) {
self.save(store);
self.dirty = false;
}
outcome
}
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub fn state_json(&self, active: bool) -> String {
let panes: Vec<serde_json::Value> = self
.snapshot
.iter()
.map(|(kind, visible, rendered)| {
serde_json::json!({
"kind": format!("{kind:?}"),
"visible": visible,
"rendered": active && *rendered,
})
})
.collect();
serde_json::json!({ "active": active, "panes": panes }).to_string()
}
pub fn show_pane(&mut self, kind: PaneKind) {
match self.find_pane(kind) {
Some(id) if self.tree.tiles.is_visible(id) => {}
_ => return,
}
self.tree
.make_active(|_id, tile| matches!(tile, Tile::Pane(k) if *k == kind));
}
fn apply_workbench_visibility(&mut self, wb: &str) {
let Some(root) = self.tree.root() else {
return;
};
self.refresh_visibility(root, wb);
self.tree.tiles.set_visible(root, true);
}
fn refresh_visibility(&mut self, id: TileId, wb: &str) -> bool {
let visible = match self.tree.tiles.get(id) {
Some(Tile::Pane(kind)) => kind.visible_in(wb),
Some(Tile::Container(container)) => {
let mut any = false;
for child in container.children_vec() {
any |= self.refresh_visibility(child, wb);
}
any
}
None => return false,
};
self.tree.tiles.set_visible(id, visible);
visible
}
fn find_pane(&self, kind: PaneKind) -> Option<TileId> {
self.tree.tiles.iter().find_map(|(id, tile)| match tile {
Tile::Pane(k) if *k == kind => Some(*id),
_ => None,
})
}
fn document_group(&self) -> Option<TileId> {
let pane = self.tree.tiles.iter().find_map(|(id, tile)| {
matches!(tile, Tile::Pane(PaneKind::Document(_))).then_some(*id)
})?;
let parent = self.tree.tiles.parent_of(pane)?;
matches!(self.tree.tiles.get(parent), Some(Tile::Container(Container::Tabs(_))))
.then_some(parent)
}
fn sync_document_panes(&mut self, docs: &Documents) -> bool {
let Some(group) = self.document_group() else {
return false;
};
let wanted: Vec<u64> = docs.iter().map(|d| d.id()).collect();
let present: Vec<(TileId, u64)> = match self.tree.tiles.get(group) {
Some(Tile::Container(Container::Tabs(tabs))) => tabs
.children
.iter()
.filter_map(|id| match self.tree.tiles.get(*id) {
Some(Tile::Pane(PaneKind::Document(doc))) => Some((*id, *doc)),
_ => None,
})
.collect(),
_ => return false,
};
let mut changed = false;
for ((tile, current), want) in present.iter().zip(wanted.iter()) {
if current != want {
if let Some(Tile::Pane(kind)) = self.tree.tiles.get_mut(*tile) {
*kind = PaneKind::Document(*want);
changed = true;
}
}
}
for want in wanted.iter().skip(present.len()) {
let tile = self.tree.tiles.insert_pane(PaneKind::Document(*want));
if let Some(Tile::Container(container)) = self.tree.tiles.get_mut(group) {
container.add_child(tile);
changed = true;
}
}
for (tile, _) in present.iter().skip(wanted.len()) {
self.tree.remove_recursively(*tile);
changed = true;
}
let active_id = docs.active_id();
let active_tile = self.tree.tiles.iter().find_map(|(id, tile)| {
matches!(tile, Tile::Pane(PaneKind::Document(d)) if *d == active_id).then_some(*id)
});
if let (Some(active_tile), Some(Tile::Container(Container::Tabs(tabs)))) =
(active_tile, self.tree.tiles.get_mut(group))
{
if tabs.active != Some(active_tile) {
tabs.set_active(active_tile);
}
}
changed
}
fn tab_bar_selection(&self, active_id: u64, ids: &[u64]) -> Option<usize> {
let group = self.document_group()?;
let Some(Tile::Container(Container::Tabs(tabs))) = self.tree.tiles.get(group) else {
return None;
};
let Some(Tile::Pane(PaneKind::Document(id))) = self.tree.tiles.get(tabs.active?) else {
return None;
};
(*id != active_id).then(|| ids.iter().position(|d| d == id))?
}
fn evict_foreign_panes_from_document_group(&mut self) -> bool {
let Some(group) = self.document_group() else {
return false;
};
let intruders: Vec<TileId> = match self.tree.tiles.get(group) {
Some(Tile::Container(Container::Tabs(tabs))) => tabs
.children
.iter()
.copied()
.filter(|id| {
!matches!(self.tree.tiles.get(*id), Some(Tile::Pane(PaneKind::Document(_))))
})
.collect(),
_ => return false,
};
if intruders.is_empty() {
return false;
}
if let Some(Tile::Container(container)) = self.tree.tiles.get_mut(group) {
for id in &intruders {
container.remove_child(*id);
}
}
let target = self.side_home().or_else(|| self.tree.root());
match target {
Some(target) if target != group => {
if let Some(Tile::Container(container)) = self.tree.tiles.get_mut(target) {
for id in &intruders {
container.add_child(*id);
}
return true;
}
self.put_back(group, &intruders);
false
}
_ => {
self.put_back(group, &intruders);
false
}
}
}
fn put_back(&mut self, group: TileId, panes: &[TileId]) {
if let Some(Tile::Container(container)) = self.tree.tiles.get_mut(group) {
for id in panes {
container.add_child(*id);
}
}
}
fn side_home(&self) -> Option<TileId> {
let group = self.document_group();
let mut best: Option<((usize, bool, std::cmp::Reverse<u64>), TileId)> = None;
for (id, tile) in self.tree.tiles.iter() {
let Tile::Container(container) = tile else {
continue;
};
if Some(*id) == group {
continue;
}
let side_panes = container
.children()
.filter(|child| {
matches!(
self.tree.tiles.get(**child),
Some(Tile::Pane(kind)) if !matches!(kind, PaneKind::Document(_))
)
})
.count();
if side_panes == 0 {
continue;
}
let rank = (
side_panes,
matches!(container, Container::Tabs(_)),
std::cmp::Reverse(id.0),
);
if best.is_none_or(|(current, _)| rank > current) {
best = Some((rank, *id));
}
}
best.map(|(_, id)| id)
}
fn save(&self, store: &dyn ModelStore) {
if let Ok(json) = serde_json::to_string(&self.tree) {
let _ = store.write(DOCK_LAYOUT_KEY, &json);
}
}
}
fn default_tree() -> Tree<PaneKind> {
let mut tiles = Tiles::default();
let side: Vec<TileId> = PaneKind::SIDE
.into_iter()
.map(|k| tiles.insert_pane(k))
.collect();
let side_container = tiles.insert_tab_tile(side);
let placeholder = tiles.insert_pane(PaneKind::Document(0));
let viewport = tiles.insert_tab_tile(vec![placeholder]);
let root = tiles.insert_horizontal_tile(vec![side_container, viewport]);
if let Some(Tile::Container(Container::Linear(linear))) = tiles.get_mut(root) {
linear.shares.set_share(side_container, 0.30);
linear.shares.set_share(viewport, 0.70);
}
Tree::new("brep-dock", root, tiles)
}
fn salvageable(tree: &Tree<PaneKind>) -> bool {
let Some(_) = tree.root() else {
return false;
};
let mut documents = false;
let mut side = std::collections::HashSet::new();
for tile in tree.tiles.tiles() {
match tile {
Tile::Pane(PaneKind::Document(_)) => documents = true,
Tile::Pane(kind) => {
side.insert(*kind);
}
Tile::Container(_) => {}
}
}
documents && PaneKind::SIDE.iter().all(|kind| side.contains(kind))
}
struct DockBehavior<'a> {
docs: &'a mut Documents,
viewport: &'a mut Viewport,
history: &'a mut HistoryPanel,
bom: &'a mut BomPanel,
assembly_constraints: &'a mut AssemblyConstraintsPanel,
wire_harness: &'a mut WireHarnessPanel,
pmi: &'a mut PmiPanel,
scene: &'a mut ScenePanel,
expressions: &'a mut ExpressionsPanel,
update_components: &'a mut UpdateComponents,
model_store: &'a dyn ModelStore,
insert_component_requested: bool,
feature_focus: Option<String>,
component_request: Option<ComponentActionRequest>,
document_tabs: TabsOutcome,
tab_title_spacing: f32,
layout_changed: bool,
rendered: Vec<PaneKind>,
}
fn is_document_tile(tiles: &Tiles<PaneKind>, tile_id: TileId) -> bool {
matches!(tiles.get(tile_id), Some(Tile::Pane(PaneKind::Document(_))))
}
impl<'a> Behavior<PaneKind> for DockBehavior<'a> {
fn pane_ui(
&mut self,
ui: &mut egui::Ui,
_tile_id: TileId,
pane: &mut PaneKind,
) -> UiResponse {
self.rendered.push(*pane);
if !matches!(*pane, PaneKind::Document(_)) {
let visuals = ui.visuals();
ui.painter()
.rect_filled(ui.max_rect(), 0.0, visuals.panel_fill);
}
match *pane {
PaneKind::Document(_) => {
self.viewport.show(ui, self.docs.engine_mut());
}
PaneKind::History => scroll(ui, "dock-history", |ui| {
self.history.show(ui, self.docs.engine_mut());
self.insert_component_requested |= self.history.take_insert_component_request();
}),
PaneKind::Bom => {
egui::ScrollArea::vertical()
.id_salt("dock-bom")
.auto_shrink([false, false])
.show(ui, |ui| {
let outcome = self.bom.show(
ui,
self.docs.engine_mut(),
self.model_store,
self.update_components,
);
if outcome.focus.is_some() {
self.feature_focus = outcome.focus;
}
if outcome.component.is_some() {
self.component_request = outcome.component;
}
});
}
PaneKind::AssemblyConstraints => scroll(ui, "dock-constraints", |ui| {
self.assembly_constraints.show(
ui,
self.docs.engine_mut(),
self.model_store,
self.update_components,
);
}),
PaneKind::WireHarness => {
egui::ScrollArea::vertical()
.id_salt("dock-wire-harness")
.auto_shrink([false, false])
.show(ui, |ui| {
self.wire_harness.show(ui, self.docs.engine_mut());
});
}
PaneKind::Pmi => scroll(ui, "dock-pmi", |ui| {
self.pmi.show(ui, self.docs.engine_mut());
}),
PaneKind::Scene => scroll(ui, "dock-scene", |ui| {
self.scene.show(ui, self.docs.engine_mut());
}),
PaneKind::Expressions => {
self.expressions.show(ui, self.docs.engine_mut());
}
}
UiResponse::None
}
fn tab_title_for_pane(&mut self, pane: &PaneKind) -> egui::WidgetText {
match pane {
PaneKind::Document(id) => match self.docs.iter().find(|d| d.id() == *id) {
Some(doc) => {
let title = doc.title();
if doc.dirty_marker() {
format!("{title} \u{2022}").into()
} else {
title.into()
}
}
None => pane.title().into(),
},
_ => pane.title().into(),
}
}
fn is_tab_closable(&self, tiles: &Tiles<PaneKind>, tile_id: TileId) -> bool {
is_document_tile(tiles, tile_id)
}
fn on_tab_close(&mut self, tiles: &mut Tiles<PaneKind>, tile_id: TileId) -> bool {
if let Some(Tile::Pane(PaneKind::Document(id))) = tiles.get(tile_id) {
if let Some(index) = self.docs.iter().position(|d| d.id() == *id) {
self.document_tabs.close = Some(index);
}
}
false
}
fn on_tab_button(
&mut self,
tiles: &mut Tiles<PaneKind>,
tile_id: TileId,
button_response: egui::Response,
) -> egui::Response {
if let Some(Tile::Pane(PaneKind::Document(id))) = tiles.get(tile_id) {
if let Some(index) = self.docs.iter().position(|d| d.id() == *id) {
let tab = button_response.rect;
let close = egui::Align2::RIGHT_CENTER.align_size_within_rect(
egui::Vec2::splat(self.close_button_outer_size()),
tab.shrink(self.tab_title_spacing),
);
self.document_tabs.hits.push((format!("doctab:{index}"), tab));
self.document_tabs
.hits
.push((format!("doctab:{index}:close"), close));
}
}
button_response
}
fn is_tile_draggable(&self, tiles: &Tiles<PaneKind>, tile_id: TileId) -> bool {
!is_document_tile(tiles, tile_id)
}
fn on_edit(&mut self, _edit_action: EditAction) {
self.layout_changed = true;
}
fn simplification_options(&self) -> egui_tiles::SimplificationOptions {
egui_tiles::SimplificationOptions {
all_panes_must_have_tabs: true,
..Default::default()
}
}
fn tab_bar_color(&self, visuals: &egui::Visuals) -> egui::Color32 {
visuals.panel_fill
}
fn tab_bg_color(
&self,
visuals: &egui::Visuals,
_tiles: &Tiles<PaneKind>,
_tile_id: TileId,
state: &TabState,
) -> egui::Color32 {
if state.active {
visuals.widgets.active.bg_fill
} else {
visuals.widgets.inactive.bg_fill
}
}
}
fn scroll(ui: &mut egui::Ui, salt: &str, add: impl FnOnce(&mut egui::Ui)) {
egui::ScrollArea::vertical()
.id_salt(salt)
.auto_shrink([false, false])
.show(ui, add);
}