use eframe::egui;
use egui_tiles::{
Behavior, Container, EditAction, TabState, Tile, TileId, Tiles, Tree, UiResponse,
};
use serde::{Deserialize, Serialize};
use brep_render::engine_state::EngineState;
use crate::panels::assembly_constraints::AssemblyConstraintsPanel;
use crate::panels::component_actions::ComponentActionRequest;
use crate::panels::bom::BomPanel;
use crate::panels::expressions::ExpressionsPanel;
use crate::panels::history::HistoryPanel;
use crate::panels::scene::ScenePanel;
use crate::panels::update_components::UpdateComponents;
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 {
Viewport,
History,
AssemblyConstraints,
Bom,
Scene,
Expressions,
}
impl PaneKind {
const SIDE: [PaneKind; 5] = [
PaneKind::History,
PaneKind::Bom,
PaneKind::AssemblyConstraints,
PaneKind::Scene,
PaneKind::Expressions,
];
const ALWAYS: [PaneKind; 3] = [
PaneKind::History,
PaneKind::Scene,
PaneKind::Expressions,
];
fn title(self) -> &'static str {
match self {
PaneKind::Viewport => "3D View",
PaneKind::History => "History",
PaneKind::Bom => "BOM",
PaneKind::AssemblyConstraints => "Constraints",
PaneKind::Scene => "Scene",
PaneKind::Expressions => "Expressions",
}
}
fn panel_id(self) -> Option<&'static str> {
match self {
PaneKind::Viewport => None,
PaneKind::History => Some("history"),
PaneKind::Bom => Some(workbench::assembly::BOM_PANEL_ID),
PaneKind::AssemblyConstraints => Some(workbench::assembly::CONSTRAINTS_PANEL_ID),
PaneKind::Scene => Some("scene"),
PaneKind::Expressions => Some("expressions"),
}
}
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,
last_wb: Option<String>,
snapshot: Vec<(PaneKind, bool, bool)>,
}
pub struct DockContext<'a> {
pub state: &'a mut EngineState,
pub viewport: &'a mut Viewport,
pub history: &'a mut HistoryPanel,
pub bom: &'a mut BomPanel,
pub assembly_constraints: &'a mut AssemblyConstraintsPanel,
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>,
}
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())
.and_then(|mut t| reconcile(&mut t).then_some(t))
.unwrap_or_else(default_tree);
Self {
tree,
dirty: false,
last_wb: None,
snapshot: Vec::new(),
}
}
pub fn ui(&mut self, ui: &mut egui::Ui, ctx: DockContext<'_>) -> DockOutcome {
let wb = ctx.state.settings.workbench.clone();
self.apply_workbench_membership(&wb);
let store = ctx.model_store;
let mut behavior = DockBehavior {
state: ctx.state,
viewport: ctx.viewport,
history: ctx.history,
bom: ctx.bom,
assembly_constraints: ctx.assembly_constraints,
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,
layout_changed: false,
rendered: Vec::new(),
};
self.tree.ui(&mut behavior, ui);
let outcome = DockOutcome {
insert_component_requested: behavior.insert_component_requested,
feature_focus: behavior.feature_focus.take(),
component_request: behavior.component_request.take(),
};
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) {
self.tree
.make_active(|_id, tile| matches!(tile, Tile::Pane(k) if *k == kind));
}
fn apply_workbench_membership(&mut self, wb: &str) {
if self.last_wb.as_deref() == Some(wb) {
return;
}
self.last_wb = Some(wb.to_string());
for kind in [PaneKind::AssemblyConstraints, PaneKind::Bom] {
let should_show = kind.visible_in(wb);
match (should_show, self.find_pane(kind)) {
(true, None) => self.insert_side_pane(kind),
(false, Some(id)) => {
self.tree.remove_recursively(id);
}
_ => {}
}
}
}
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 insert_side_pane(&mut self, kind: PaneKind) {
let id = self.tree.tiles.insert_pane(kind);
let target = self.side_column().or_else(|| self.tree.root());
if let Some(target) = target {
if let Some(Tile::Container(container)) = self.tree.tiles.get_mut(target) {
container.add_child(id);
}
}
}
fn side_column(&self) -> Option<TileId> {
self.tree.tiles.iter().find_map(|(id, tile)| match tile {
Tile::Container(Container::Linear(lin))
if lin.dir == egui_tiles::LinearDir::Vertical =>
{
Some(*id)
}
_ => None,
})
}
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_vertical_tile(side);
let viewport = tiles.insert_pane(PaneKind::Viewport);
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 reconcile(tree: &mut Tree<PaneKind>) -> bool {
use std::collections::HashSet;
let mut viewport_count = 0usize;
let mut present: HashSet<PaneKind> = HashSet::new();
for tile in tree.tiles.tiles() {
if let Tile::Pane(kind) = tile {
if *kind == PaneKind::Viewport {
viewport_count += 1;
}
present.insert(*kind);
}
}
if viewport_count != 1 {
return false;
}
let Some(root) = tree.root() else {
return false;
};
for kind in PaneKind::ALWAYS {
if !present.contains(&kind) {
let id = tree.tiles.insert_pane(kind);
if let Some(Tile::Container(container)) = tree.tiles.get_mut(root) {
container.add_child(id);
}
}
}
true
}
struct DockBehavior<'a> {
state: &'a mut EngineState,
viewport: &'a mut Viewport,
history: &'a mut HistoryPanel,
bom: &'a mut BomPanel,
assembly_constraints: &'a mut AssemblyConstraintsPanel,
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>,
layout_changed: bool,
rendered: Vec<PaneKind>,
}
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 *pane != PaneKind::Viewport {
let visuals = ui.visuals();
ui.painter()
.rect_filled(ui.max_rect(), 0.0, visuals.panel_fill);
}
match *pane {
PaneKind::Viewport => {
self.viewport.show(ui, self.state);
}
PaneKind::History => scroll(ui, "dock-history", |ui| {
self.history.show(ui, self.state);
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.state, 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.state,
self.model_store,
self.update_components,
);
}),
PaneKind::Scene => scroll(ui, "dock-scene", |ui| {
self.scene.show(ui, self.state);
}),
PaneKind::Expressions => {
self.expressions.show(ui, self.state);
}
}
UiResponse::None
}
fn tab_title_for_pane(&mut self, pane: &PaneKind) -> egui::WidgetText {
pane.title().into()
}
fn is_tab_closable(&self, _tiles: &Tiles<PaneKind>, _tile_id: TileId) -> bool {
false
}
fn is_tile_draggable(&self, tiles: &Tiles<PaneKind>, tile_id: TileId) -> bool {
!matches!(tiles.get(tile_id), Some(Tile::Pane(PaneKind::Viewport)))
}
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);
}
#[cfg(test)]
mod tests {
use super::*;
fn active_tab(tree: &Tree<PaneKind>) -> Option<TileId> {
tree.tiles.iter().find_map(|(_, tile)| match tile {
Tile::Container(Container::Tabs(t)) => t.active,
_ => None,
})
}
#[test]
fn show_pane_activates_a_backgrounded_tab() {
let mut tiles = Tiles::default();
let history = tiles.insert_pane(PaneKind::History);
let scene = tiles.insert_pane(PaneKind::Scene);
let tabs = tiles.insert_tab_tile(vec![history, scene]);
if let Some(Tile::Container(Container::Tabs(t))) = tiles.get_mut(tabs) {
t.set_active(scene);
}
let tree = Tree::new("test-dock", tabs, tiles);
let mut dock = DockState { tree, dirty: false, last_wb: None, snapshot: Vec::new() };
assert_eq!(active_tab(&dock.tree), Some(scene), "precondition: History is behind");
dock.show_pane(PaneKind::History);
assert_eq!(
active_tab(&dock.tree),
Some(history),
"show_pane brings the History tab to the front"
);
dock.show_pane(PaneKind::History);
assert_eq!(active_tab(&dock.tree), Some(history));
}
}