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::panels::qualify::QualifyPanel;
use crate::panels::family_table_editor::FamilyTableEditor;
use crate::panels::sheets::SheetsPanel;
use crate::store::{ModelStore, DOCK_LAYOUT_KEY};
use brep_render::engine_state::EngineState;
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,
Sheets,
Scene,
Expressions,
EcadLibrary,
EcadInspector,
Qualify,
FamilyTable,
Plm,
}
impl PaneKind {
const SIDE: [PaneKind; 13] = [
PaneKind::History,
PaneKind::Scene,
PaneKind::Expressions,
PaneKind::AssemblyConstraints,
PaneKind::Bom,
PaneKind::WireHarness,
PaneKind::Pmi,
PaneKind::Sheets,
PaneKind::EcadLibrary,
PaneKind::EcadInspector,
PaneKind::Qualify,
PaneKind::FamilyTable,
PaneKind::Plm,
];
pub const ALL: [PaneKind; 13] = 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::Sheets => "Sheets",
PaneKind::Scene => "Scene",
PaneKind::Expressions => "Expressions",
PaneKind::EcadLibrary => "Assembly parts",
PaneKind::EcadInspector => "Inspector",
PaneKind::Qualify => "Qualify",
PaneKind::FamilyTable => "Family table",
PaneKind::Plm => "PLM",
}
}
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::Sheets => Some(workbench::drawing::SHEETS_PANEL_ID),
PaneKind::Scene => Some("scene"),
PaneKind::Expressions => Some("expressions"),
PaneKind::EcadLibrary => Some(workbench::ecad::LIBRARY_PANEL_ID),
PaneKind::EcadInspector => Some(workbench::ecad::INSPECTOR_PANEL_ID),
PaneKind::Qualify => Some(workbench::QUALIFY_PANEL_ID),
PaneKind::FamilyTable => Some(workbench::FAMILY_TABLE_PANEL_ID),
PaneKind::Plm => Some(workbench::PLM_PANEL_ID),
}
}
pub fn visible_under_workbench(self, wb: &str, panels: &workbench::ButtonState) -> bool {
self.visible_in(wb, panels)
}
fn visible_in(self, wb: &str, panels: &workbench::ButtonState) -> bool {
match self.panel_id() {
None => true,
Some(id) => workbench::panel_visible(wb, id, panels),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct DialogTargets {
document: u64,
feature: (Option<String>, u64),
pmi: (Option<String>, u64),
constraint: (Option<String>, u64),
sheet: (Option<String>, u64),
}
impl DialogTargets {
pub fn read(history: &HistoryPanel, engine: &EngineState, document: u64) -> Self {
let (feature, opens) = history.form_open();
Self {
document,
feature: (feature.map(str::to_string), opens),
pmi: (
engine
.pmi_open_annotation()
.or(engine.pmi_open_view())
.map(str::to_string),
engine.pmi_dialog_opens(),
),
constraint: (engine.assembly_open_constraint(), engine.assembly_constraint_opens()),
sheet: (
engine.sheet_open_object().map(str::to_string),
engine.sheet_object_opens(),
),
}
}
}
pub struct DockState {
tree: Tree<PaneKind>,
dirty: bool,
snapshot: Vec<(PaneKind, bool, bool)>,
dialogs: Option<DialogTargets>,
ports: Option<(u64, bool)>,
ecad_entry: Option<(u64, String, Noting)>,
ecad_declined: Vec<PaneKind>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
enum Noting {
Off,
Baseline(bool),
Live,
}
const ECAD_DECLINED_KEY: &str = "brepEcadDeclined";
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 sheets: &'a mut SheetsPanel,
pub scene: &'a mut ScenePanel,
pub expressions: &'a mut ExpressionsPanel,
pub qualify: &'a mut QualifyPanel,
pub family_table: &'a mut FamilyTableEditor,
pub update_components: &'a mut UpdateComponents,
pub model_store: &'a dyn ModelStore,
pub plm: &'a mut crate::panels::plm_host::PlmHost,
}
#[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 saved = store
.read(DOCK_LAYOUT_KEY)
.and_then(|json| serde_json::from_str::<serde_json::Value>(&json).ok());
let ecad_declined = saved
.as_ref()
.and_then(|json| json.get(ECAD_DECLINED_KEY))
.and_then(|notes| serde_json::from_value::<Vec<PaneKind>>(notes.clone()).ok())
.unwrap_or_default();
let tree = saved
.and_then(|json| serde_json::from_value::<Tree<PaneKind>>(json).ok())
.and_then(salvage)
.unwrap_or_else(default_tree);
Self {
tree,
dirty: false,
snapshot: Vec::new(),
dialogs: None,
ports: None,
ecad_entry: None,
ecad_declined,
}
}
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, &workbench::ButtonState::of(ctx.docs.engine()));
self.surface_declared_ports(
ctx.docs.active_id(),
workbench::declares_connection_points(&workbench::ButtonState::of(ctx.docs.engine())),
&wb,
);
self.surface_ecad_pane(ctx.docs.active_id(), &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::new(ctx);
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);
behavior.retract_undrawn(&rendered);
drop(behavior);
self.snapshot = self
.panes_in_layout_order()
.into_iter()
.map(|(id, kind)| (kind, self.tree.tiles.is_visible(id), rendered.contains(&kind)))
.collect();
if self.dirty && !ui.ctx().input(|i| i.pointer.any_down()) {
self.save(store);
self.dirty = false;
}
outcome
}
pub fn retract_all(ctx: DockContext<'_>) {
DockBehavior::new(ctx).retract_undrawn(&[]);
}
#[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,
"activeTab": self.find_pane(*kind).is_some_and(|id| self.tab_active(id)),
"rendered": active && *rendered,
})
})
.collect();
serde_json::json!({ "active": active, "panes": panes }).to_string()
}
fn tab_active(&self, id: TileId) -> bool {
let mut child = id;
while let Some(parent) = self.tree.tiles.parent_of(child) {
if let Some(Tile::Container(Container::Tabs(tabs))) = self.tree.tiles.get(parent) {
if tabs.next_active(&self.tree.tiles) != Some(child) {
return false;
}
}
child = parent;
}
true
}
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));
}
pub fn surface_opened_dialogs(&mut self, current: DialogTargets) {
let Some(previous) = self.dialogs.replace(current.clone()) else {
return;
};
if previous.document != current.document {
return;
}
for (current, previous, host) in [
(¤t.feature, &previous.feature, PaneKind::History),
(¤t.pmi, &previous.pmi, PaneKind::Pmi),
(¤t.constraint, &previous.constraint, PaneKind::AssemblyConstraints),
(¤t.sheet, &previous.sheet, PaneKind::Sheets),
] {
if current.0.is_some() && current != previous {
self.show_pane(host);
}
}
}
fn surface_declared_ports(&mut self, document: u64, declares: bool, wb: &str) {
let previous = self.ports.replace((document, declares));
let authoring = matches!(
workbench::ecad::Target::of_workbench(wb),
Some(workbench::ecad::Target::Symbol | workbench::ecad::Target::Pads)
);
if previous == Some((document, false)) && declares && !authoring {
self.show_pane(PaneKind::Qualify);
}
}
fn surface_ecad_pane(&mut self, document: u64, wb: &str) {
let own = |wb: &str| match workbench::ecad::Target::of_workbench(wb) {
Some(workbench::ecad::Target::Diagram | workbench::ecad::Target::Pcb) => Some(PaneKind::EcadLibrary),
Some(workbench::ecad::Target::Symbol | workbench::ecad::Target::Pads) => Some(PaneKind::EcadInspector),
None => None,
};
let previous = self.ecad_entry.take();
let Some(pane) = own(wb) else {
self.ecad_entry = Some((document, wb.to_string(), Noting::Off));
return;
};
let same_document = matches!(&previous, Some((d, ..)) if *d == document);
let entered = same_document && matches!(&previous, Some((_, before, _)) if own(before) != Some(pane));
if entered && !self.ecad_declined.contains(&pane) && self.front_beside(pane) != Some(PaneKind::Qualify) {
self.show_pane(pane);
}
let in_front = self.find_pane(pane).is_some_and(|id| self.tree.tiles.is_visible(id) && self.tab_active(id));
let noting = match previous {
_ if entered => Noting::Live,
Some((_, _, Noting::Baseline(was))) if same_document => match was == in_front {
true => Noting::Baseline(was),
false => Noting::Live,
},
Some((_, _, noting)) if same_document => noting,
_ => Noting::Baseline(in_front),
};
self.ecad_entry = Some((document, wb.to_string(), noting));
if noting != Noting::Live {
return;
}
if self.ecad_declined.contains(&pane) == in_front {
self.dirty = true;
}
self.ecad_declined.retain(|kind| *kind != pane);
if !in_front {
self.ecad_declined.push(pane);
}
}
fn front_beside(&self, kind: PaneKind) -> Option<PaneKind> {
let id = self.find_pane(kind)?;
let parent = self.tree.tiles.parent_of(id)?;
let Some(Tile::Container(Container::Tabs(tabs))) = self.tree.tiles.get(parent) else {
return None;
};
match self.tree.tiles.get(tabs.next_active(&self.tree.tiles)?) {
Some(Tile::Pane(front)) => Some(*front),
_ => None,
}
}
fn apply_workbench_visibility(&mut self, wb: &str, panels: &workbench::ButtonState) {
let Some(root) = self.tree.root() else {
return;
};
self.refresh_visibility(root, wb, panels);
self.tree.tiles.set_visible(root, true);
}
fn refresh_visibility(&mut self, id: TileId, wb: &str, panels: &workbench::ButtonState) -> bool {
let visible = match self.tree.tiles.get(id) {
Some(Tile::Pane(kind)) => kind.visible_in(wb, panels),
Some(Tile::Container(container)) => {
let mut any = false;
for child in container.children_vec() {
any |= self.refresh_visibility(child, wb, panels);
}
any
}
None => return false,
};
self.tree.tiles.set_visible(id, visible);
visible
}
fn panes_in_layout_order(&self) -> Vec<(TileId, PaneKind)> {
let mut out = Vec::new();
if let Some(root) = self.tree.root() {
self.collect_panes(root, &mut out);
}
out
}
fn collect_panes(&self, id: TileId, out: &mut Vec<(TileId, PaneKind)>) {
match self.tree.tiles.get(id) {
Some(Tile::Pane(kind)) => out.push((id, *kind)),
Some(Tile::Container(container)) => {
for child in container.children() {
self.collect_panes(*child, out);
}
}
None => {}
}
}
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) {
let Ok(mut json) = serde_json::to_value(&self.tree) else {
return;
};
if let (Some(fields), Ok(notes)) = (json.as_object_mut(), serde_json::to_value(&self.ecad_declined)) {
fields.insert(ECAD_DECLINED_KEY.into(), notes);
}
let _ = store.write(DOCK_LAYOUT_KEY, &json.to_string());
}
}
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)
}
const PANES_WHEN_HIDING_BEGAN: [PaneKind; 7] = [
PaneKind::History,
PaneKind::Scene,
PaneKind::Expressions,
PaneKind::AssemblyConstraints,
PaneKind::Bom,
PaneKind::WireHarness,
PaneKind::Pmi,
];
fn salvage(tree: Tree<PaneKind>) -> Option<Tree<PaneKind>> {
tree.root()?;
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(_) => {}
}
}
if !documents || PANES_WHEN_HIDING_BEGAN.iter().any(|kind| !side.contains(kind)) {
return None;
}
let missing: Vec<PaneKind> = PaneKind::SIDE
.into_iter()
.filter(|kind| !side.contains(kind))
.collect();
if missing.is_empty() {
return Some(tree);
}
let mut dock = DockState { tree, dirty: false, snapshot: Vec::new(), dialogs: None, ports: None, ecad_entry: None, ecad_declined: Vec::new() };
let home = dock.side_home()?;
for kind in missing {
let pane = dock.tree.tiles.insert_pane(kind);
match dock.tree.tiles.get_mut(home) {
Some(Tile::Container(container)) => container.add_child(pane),
_ => return None,
}
}
Some(dock.tree)
}
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,
sheets: &'a mut SheetsPanel,
scene: &'a mut ScenePanel,
expressions: &'a mut ExpressionsPanel,
qualify: &'a mut QualifyPanel,
family_table: &'a mut FamilyTableEditor,
update_components: &'a mut UpdateComponents,
model_store: &'a dyn ModelStore,
plm: &'a mut crate::panels::plm_host::PlmHost,
insert_component_requested: bool,
feature_focus: Option<String>,
component_request: Option<ComponentActionRequest>,
document_tabs: TabsOutcome,
tab_title_spacing: f32,
tab_fit: std::collections::HashMap<TileId, (std::sync::Arc<egui::Galley>, String)>,
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> DockBehavior<'a> {
fn new(ctx: DockContext<'a>) -> Self {
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,
sheets: ctx.sheets,
scene: ctx.scene,
expressions: ctx.expressions,
qualify: ctx.qualify,
family_table: ctx.family_table,
update_components: ctx.update_components,
model_store: ctx.model_store,
plm: ctx.plm,
insert_component_requested: false,
feature_focus: None,
component_request: None,
document_tabs: TabsOutcome::default(),
tab_title_spacing: 0.0,
tab_fit: std::collections::HashMap::new(),
layout_changed: false,
rendered: Vec::new(),
}
}
fn retract_undrawn(&mut self, rendered: &[PaneKind]) {
for kind in PaneKind::SIDE {
if rendered.contains(&kind) {
continue;
}
match kind {
PaneKind::History => self.history.clear_hits(),
PaneKind::Bom => self.bom.clear_hits(),
PaneKind::AssemblyConstraints => self.assembly_constraints.clear_hits(),
PaneKind::WireHarness => self.wire_harness.clear_hits(),
PaneKind::Pmi => self.pmi.clear_hits(),
PaneKind::Sheets => self.sheets.clear_hits(),
PaneKind::Scene => self.scene.clear_hits(),
PaneKind::Expressions => self.expressions.clear_hits(),
PaneKind::Qualify => self.qualify.clear_hits(),
PaneKind::FamilyTable => self.family_table.clear_hits(),
PaneKind::Plm => {}
PaneKind::EcadLibrary | PaneKind::EcadInspector | 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(_) => {
let doc = self.docs.active_mut();
match workbench::ecad::Target::of_workbench(&doc.engine.settings.workbench) {
Some(target) => self.viewport.show_ecad(ui, doc, target),
None => self.viewport.show(ui, &mut doc.engine),
}
}
PaneKind::EcadLibrary => {
let doc = self.docs.active_mut();
match workbench::ecad::Target::of_workbench(&doc.engine.settings.workbench) {
Some(workbench::ecad::Target::Diagram) => doc.ecad.diagram.library_panel(ui),
Some(workbench::ecad::Target::Pcb) => doc.ecad.pcb.library_panel(ui),
_ => {}
}
}
PaneKind::EcadInspector => {
let doc = self.docs.active_mut();
match workbench::ecad::Target::of_workbench(&doc.engine.settings.workbench) {
Some(workbench::ecad::Target::Diagram) => doc.ecad.diagram.inspector(ui),
Some(workbench::ecad::Target::Pcb) => doc.ecad.pcb.inspector(ui),
Some(workbench::ecad::Target::Symbol) => doc.ecad.symbol.properties(ui),
Some(workbench::ecad::Target::Pads) => doc.ecad.pads.properties(ui),
None => {}
}
}
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 document = self.docs.active().name().map(str::to_string);
let outcome = self.bom.show(
ui,
self.docs.engine_mut(),
self.model_store,
self.update_components,
document.as_deref(),
);
if outcome.focus.is_some() {
self.feature_focus = outcome.focus;
}
if outcome.component.is_some() {
self.component_request = outcome.component;
}
if outcome.update_components {
if let Err(error) = self.update_components.run(self.docs.engine_mut(), self.model_store) {
self.docs.engine_mut().push_notice(format!("Update components: {error}"));
}
}
});
}
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::Sheets => {
egui::ScrollArea::vertical()
.id_salt("dock-sheets")
.auto_shrink([false, false])
.show(ui, |ui| {
self.sheets.show(ui, self.docs.engine_mut());
});
}
PaneKind::Scene => scroll(ui, "dock-scene", |ui| {
self.scene.show(ui, self.docs.engine_mut());
}),
PaneKind::Expressions => scroll(ui, "dock-expressions", |ui| {
self.expressions.show(ui, self.docs.engine_mut());
}),
PaneKind::Qualify => scroll(ui, "dock-qualify", |ui| {
let doc = self.docs.active_mut();
let target = workbench::ecad::Target::of_workbench(&doc.engine.settings.workbench);
self.qualify.show(ui, &mut doc.engine, &mut doc.ecad, target);
}),
PaneKind::FamilyTable => {
let doc = self.docs.active_mut();
let identity = doc.name().map(str::to_string);
let dirty = doc.dirty_marker();
self.family_table.show(ui, &mut doc.engine, self.model_store, identity.as_deref(), dirty);
if self.family_table.take_save_request() {
let document = doc.engine.history_request_json();
if let Some(name) = identity.as_deref() {
crate::plm::thumbnail::stage_save(self.model_store, name, &document, &doc.engine, doc.id());
}
match identity.as_deref().map(|name| self.model_store.write(name, &document)) {
Some(Ok(())) => doc.mark_clean(),
Some(Err(error)) => self.family_table.save_failed(error),
None => self.family_table.save_failed("the family has never been saved".into()),
}
}
}
PaneKind::Plm => scroll(ui, "dock-plm", |ui| self.plm.ui(ui, self.docs)),
}
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 tab_title_for_tile(&mut self, tiles: &Tiles<PaneKind>, tile_id: TileId) -> egui::WidgetText {
if let Some((galley, _)) = self.tab_fit.get(&tile_id) {
return egui::WidgetText::Galley(galley.clone());
}
match tiles.get(tile_id) {
Some(Tile::Pane(pane)) => self.tab_title_for_pane(pane),
Some(Tile::Container(container)) => format!("{:?}", container.kind()).into(),
None => "MISSING TILE".into(),
}
}
fn top_bar_right_ui(
&mut self,
tiles: &Tiles<PaneKind>,
ui: &mut egui::Ui,
_tile_id: TileId,
tabs: &egui_tiles::Tabs,
_scroll_offset: &mut f32,
) {
let spacing = self.tab_title_spacing;
let titles: Vec<(TileId, String, bool)> = tabs
.children
.iter()
.filter(|id| tiles.is_visible(**id))
.filter_map(|id| match tiles.get(*id) {
Some(Tile::Pane(pane)) => {
let text = self.tab_title_for_pane(pane).text().to_string();
Some((*id, text, is_document_tile(tiles, *id)))
}
_ => None,
})
.collect();
for (id, full, galley) in fit_tab_titles(ui, &titles, ui.available_width(), spacing, self.close_button_outer_size()) {
self.tab_fit.insert(id, (galley, full));
}
}
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));
}
}
if let Some(Tile::Pane(pane)) = tiles.get(tile_id) {
if !matches!(pane, PaneKind::Document(_)) {
let key = format!("pane:{}", pane.title().to_lowercase().replace(' ', "-"));
self.document_tabs.hits.push((key, button_response.rect));
}
}
match self.tab_fit.get(&tile_id) {
Some((_, full)) => button_response.on_hover_text(full.as_str()),
None => 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
}
}
}
const MIN_TAB_TITLE: f32 = 40.0;
fn fit_tab_titles(
ui: &egui::Ui,
titles: &[(TileId, String, bool)],
width: f32,
spacing: f32,
close: f32,
) -> Vec<(TileId, String, std::sync::Arc<egui::Galley>)> {
let natural = |text: &str| {
egui::WidgetText::from(text)
.into_galley(ui, Some(egui::TextWrapMode::Extend), f32::INFINITY, egui::TextStyle::Button)
.size()
.x
};
let mut fixed = 0.0;
let mut elidable: Vec<(TileId, &str, f32)> = Vec::new();
for (id, text, document) in titles {
fixed += 2.0 * spacing;
let w = natural(text);
if *document {
fixed += w + 4.0 + close;
} else {
elidable.push((*id, text, w));
}
}
let budget = width - fixed - 2.0;
let Some(cap) = shared_cap(&elidable.iter().map(|e| e.2).collect::<Vec<_>>(), budget) else {
return Vec::new();
};
let cap = cap.max(MIN_TAB_TITLE);
elidable
.into_iter()
.filter(|(_, _, w)| *w > cap + 0.5)
.map(|(id, text, _)| {
let galley = egui::WidgetText::from(text).into_galley(
ui,
Some(egui::TextWrapMode::Truncate),
cap,
egui::TextStyle::Button,
);
(id, text.to_string(), galley)
})
.collect()
}
fn shared_cap(widths: &[f32], budget: f32) -> Option<f32> {
if widths.iter().sum::<f32>() <= budget {
return None;
}
let mut sorted = widths.to_vec();
sorted.sort_by(f32::total_cmp);
let mut spent = 0.0;
for (i, w) in sorted.iter().enumerate() {
let left = sorted.len() - i;
let cap = (budget - spent) / left as f32;
if cap < *w {
return Some(cap.max(0.0));
}
spent += w;
}
None
}
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);
}