Skip to main content

brep_app/panels/
workbench_toolbar.rs

1//! Workbench actions toolbar — a second top strip drawn UNDER the primary
2//! toolbar, listing the ACTIVE WORKBENCH's creatable features as one square
3//! icon button each, and (in a workbench that shows the Constraints panel —
4//! Assembly, and All as its union) a second group with one button per
5//! assembly-constraint type.
6//!
7//! It is a shortcut surface over creation, nothing more: a feature button does
8//! exactly what picking that entry in the **Add new feature** palette does
9//! (`HistoryPanel::add_feature_of_type`, so ACOMP still routes to the
10//! component selector), and a constraint button does what the context bar's
11//! constraint offer does (`context_bar::add_constraint_from_selection`, seeding
12//! `elements` from the current selection). Which features appear is the SAME
13//! workbench filter the palette applies ([`workbench::includes_feature`]); the
14//! constraints group is gated on the SAME panel claim the dock and the context
15//! bar use ([`workbench::panel_visible`]). Nothing here decides anything on
16//! its own.
17//!
18//! Shown only while `settings.show_workbench_toolbar` is on (the Settings
19//! "Show workbench actions toolbar" checkbox), and NEVER in a special mode: a
20//! sketch takes over the shell with its own tool strip, and reference
21//! selection bypasses the dock the new feature's form would open in. Follows
22//! the toolbar pattern: a small state struct owning only the per-frame `hits`
23//! map (widget rects) the headed verifier reads, a `show(...)` the shell calls
24//! right after the primary toolbar so the strip lands below it, and clicks
25//! flow OUT through a returned [`WorkbenchToolbarOutcome`] — the shell
26//! dispatches, because adding a feature touches panels this strip cannot
27//! borrow.
28
29use crate::automation::hit_keys::HitKeyDoc;
30use crate::panels::toolbar_button;
31use crate::workbench;
32use brep_render::engine_state::EngineState;
33use brep_render::features;
34use eframe::egui;
35use serde_json::Value;
36use std::collections::HashMap;
37
38/// What one frame of the strip produced for the shell to act on.
39#[derive(Default)]
40pub struct WorkbenchToolbarOutcome {
41    /// A feature button click — the feature TYPE CODE to add (`"E"`, `"ACOMP"`).
42    pub feature: Option<String>,
43    /// A constraint button click — the constraint type id to add (`"fixed"`).
44    pub constraint: Option<String>,
45}
46
47/// One drawn button: its stable id, the glyph on the square, and the tooltip.
48struct ActionButton {
49    id: String,
50    glyph: String,
51    tooltip: String,
52}
53
54/// The strip's own state: the per-frame hit-rects, plus the button lists,
55/// which are derived from the kernel catalogues and so are rebuilt only when
56/// the workbench changes rather than re-walked every frame.
57#[derive(Default)]
58pub struct WorkbenchToolbarPanel {
59    hits: HashMap<String, egui::Rect>,
60    /// The feature buttons for `cached_workbench` (the catalogue filtered by
61    /// that workbench, in catalogue order).
62    features: Vec<ActionButton>,
63    cached_workbench: Option<String>,
64    /// The constraint buttons — the constraint catalogue is static, so this is
65    /// built once.
66    constraints: Vec<ActionButton>,
67}
68
69impl WorkbenchToolbarPanel {
70    pub fn new() -> Self {
71        Self::default()
72    }
73
74    /// Whether the strip is drawn this frame: the setting is on AND no special
75    /// mode owns the shell (a sketch has its own tool strip; reference
76    /// selection hides the dock the new feature's form would open in).
77    pub fn visible(state: &EngineState) -> bool {
78        state.settings.show_workbench_toolbar && !state.sketch_mode() && !state.ref_select_active()
79    }
80
81    /// Draw the strip as a top panel — called right AFTER the primary toolbar
82    /// so egui stacks it directly underneath. Draws nothing (and publishes no
83    /// hit-rects) when [`Self::visible`] is false, or when the active
84    /// workbench offers nothing to put on it (the placeholder workbenches).
85    pub fn show(&mut self, ui: &mut egui::Ui, state: &EngineState) -> WorkbenchToolbarOutcome {
86        self.hits.clear();
87        let mut outcome = WorkbenchToolbarOutcome::default();
88        if !Self::visible(state) {
89            return outcome;
90        }
91        let active = state.settings.workbench.clone();
92        self.refresh_buttons(&active);
93        let with_constraints =
94            workbench::panel_visible(&active, workbench::assembly::CONSTRAINTS_PANEL_ID);
95        if self.features.is_empty() && !with_constraints {
96            return outcome;
97        }
98        egui::containers::panel::Panel::top("brep-workbench-toolbar")
99            .resizable(false)
100            .show(ui, |ui| {
101                ui.add_space(2.0);
102                ui.horizontal_wrapped(|ui| {
103                    if !self.features.is_empty() {
104                        caption(ui, "Features");
105                        outcome.feature = Self::draw_group(ui, &self.features, &mut self.hits);
106                    }
107                    if with_constraints {
108                        if !self.features.is_empty() {
109                            ui.separator();
110                        }
111                        caption(ui, "Constraints");
112                        outcome.constraint =
113                            Self::draw_group(ui, &self.constraints, &mut self.hits);
114                    }
115                });
116                ui.add_space(2.0);
117            });
118        outcome
119    }
120
121    /// Rebuild the button lists when the workbench changed (or on first use).
122    fn refresh_buttons(&mut self, active: &str) {
123        if self.cached_workbench.as_deref() != Some(active) {
124            self.features = feature_buttons(active);
125            self.cached_workbench = Some(active.to_string());
126        }
127        if self.constraints.is_empty() {
128            self.constraints = constraint_buttons();
129        }
130    }
131
132    /// Draw one group of square buttons through the shared toolbar-button
133    /// helper, publishing each hit-rect under `wbtb:<id>`; returns the clicked
134    /// button's payload (the part of the id after the group prefix).
135    fn draw_group(
136        ui: &mut egui::Ui,
137        buttons: &[ActionButton],
138        hits: &mut HashMap<String, egui::Rect>,
139    ) -> Option<String> {
140        let mut clicked = None;
141        for button in buttons {
142            let resp = toolbar_button::button(ui, &button.glyph, &button.tooltip);
143            hits.insert(format!("wbtb:{}", button.id), resp.rect);
144            if resp.clicked() {
145                clicked = button.id.split_once(':').map(|(_, payload)| payload.to_string());
146            }
147        }
148        clicked
149    }
150
151    /// The published widget hit-rects (egui points) for the headed verifier —
152    /// `wbtb:feature:<type>` / `wbtb:constraint:<type>`; empty while hidden.
153    pub fn hits_json(&self) -> String {
154        crate::automation::hit_rects::hits_json(&self.hits)
155    }
156}
157
158/// A small, dim group caption ahead of its buttons.
159fn caption(ui: &mut egui::Ui, text: &str) {
160    ui.label(egui::RichText::new(text).weak().small());
161}
162
163/// One button per catalogue feature the workbench INCLUDES, in catalogue
164/// order — the same list, same filter, as the Add-feature palette. The glyph is
165/// the feature's icon (the catalogued artwork paints it); a feature with no icon
166/// falls back to its short name as text.
167fn feature_buttons(active: &str) -> Vec<ActionButton> {
168    let catalogue = features::feature_catalogue();
169    let mut buttons = Vec::new();
170    if let Some(list) = catalogue.get("features").and_then(Value::as_array) {
171        for feature in list {
172            let ty = feature.get("type").and_then(Value::as_str).unwrap_or("");
173            if ty.is_empty() || !workbench::includes_feature(active, ty) {
174                continue;
175            }
176            let name = feature
177                .get("longName")
178                .and_then(Value::as_str)
179                .unwrap_or(ty)
180                .to_string();
181            let glyph = match features::feature_icon(ty) {
182                Some(icon) => icon.to_string(),
183                None => feature
184                    .get("shortName")
185                    .and_then(Value::as_str)
186                    .unwrap_or(ty)
187                    .to_string(),
188            };
189            buttons.push(ActionButton {
190                id: format!("feature:{ty}"),
191                glyph,
192                tooltip: format!("Add {name}"),
193            });
194        }
195    }
196    buttons
197}
198
199/// One button per assembly-constraint type, spec §4 order (the same order as
200/// the constraint catalogue and the panel's `+` dropdown). The glyph is the
201/// type's icon (`ConstraintTypeDef::icon` — the picture the context bar's
202/// offer and the viewport chip show), drawn as catalogued artwork; the plain
203/// name rides in the tooltip.
204fn constraint_buttons() -> Vec<ActionButton> {
205    brep_render::brep_kernel::CONSTRAINT_TYPES
206        .iter()
207        .map(|def| ActionButton {
208            id: format!("constraint:{}", def.type_id),
209            glyph: def.icon.to_string(),
210            tooltip: format!("Add {} constraint from the selection", def.label),
211        })
212        .collect()
213}
214
215// BREP private tests: b9d6f42ef8a9605e
216
217/// The hit keys this panel publishes (see `automation::hit_keys`).
218pub static HIT_KEYS: &[HitKeyDoc] = &[
219    HitKeyDoc { panel: "wbtoolbar", prefix: "wbtb:feature:", meaning: "add a feature of that type (one button per feature the active workbench offers)", command: Some("feature_add") },
220    HitKeyDoc { panel: "wbtoolbar", prefix: "wbtb:constraint:", meaning: "add an assembly constraint of that type, seeded from the current selection", command: Some("assembly_add_constraint") },
221];