1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
//! mode_bar — the special-mode EXIT controls, always pinned to the TOP-RIGHT
//! corner of the screen. Every special mode (a card-based reference pick,
//! sketch mode, and any future mode) surfaces its Finish / Cancel here so the
//! exit is in a single, predictable place — the pattern the user asked for.
//! A FORM FIELD's reference pick is not a mode: the field stays focused in its
//! own form and takes the picks (`EngineState::ref_select_in_form`), so it has
//! no card here.
//!
//! It draws INTO a caller-owned `ui` (the shell owns the top-right `Area` and
//! stacks the context-action rail below it), and owns no model state.
use crate::automation::hit_keys::HitKeyDoc;
use brep_render::engine_state::EngineState;
use eframe::egui;
use std::collections::HashMap;
/// Who this card is when it drives the viewport's dialog-row hover
/// (`EngineState::hover_entity_by_name`) — the owner tag that keeps its
/// highlight independent of the form panes' and the Scene tree's.
const DIALOG_HOVER_OWNER: &str = "refsel";
/// The mode-exit card's transient UI state (the model lives in the engine).
#[derive(Default)]
pub struct ModeBar {
/// Per-frame widget rects, published for the headed verifier.
hits: HashMap<String, egui::Rect>,
}
impl ModeBar {
pub(crate) fn clear_hits(&mut self) { self.hits.clear(); }
pub fn new() -> Self {
Self::default()
}
/// Draw the active special mode's exit controls as a card. No-op (draws
/// nothing) in the normal modeling environment. Called from the shell inside
/// the shared top-right overlay `Area`.
pub fn card(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
self.hits.clear();
// A FORM FIELD's pick draws no card: the field itself, focused in its
// pane, lists the picks and ends them (`form::ActivePick`). The card is
// for the picks opened from a list row with no field of their own — a
// connection point's seat, a spline anchor's port.
if state.ref_select_modal() {
self.reference_card(ui, state);
} else if state.sketch_mode() {
self.sketch_card(ui, state);
} else if state.sketch3d_mode() {
self.sketch3d_card(ui, state);
}
}
/// 3D sketch mode's exit: Finish commits, Cancel discards (deleting a sketch
/// this session created).
fn sketch3d_card(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
egui::Frame::popup(ui.style()).show(ui, |ui| {
let id = state.sketch3d_edit_feature_id().unwrap_or("").to_string();
ui.label(egui::RichText::new(format!("3D sketch: {id}")).strong());
ui.horizontal(|ui| {
let finish = ui.button("Finish").on_hover_text("Commit the 3D sketch");
self.hits.insert("sketch3d:finish".into(), finish.rect);
if finish.clicked() {
let _ = state.exit_sketch3d_mode(true);
}
let cancel = ui
.button("Cancel")
.on_hover_text("Discard changes (deletes a new 3D sketch)");
self.hits.insert("sketch3d:cancel".into(), cancel.rect);
if cancel.clicked() {
let _ = state.exit_sketch3d_mode(false);
}
});
});
}
/// Reference-selection: the running picked-name list (each with an ✕ to drop
/// it) + Finish / Cancel. Picking itself happens by clicking in the viewport;
/// this card is the whole picker UI now (the side panel is hidden).
fn reference_card(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
egui::Frame::popup(ui.style()).show(ui, |ui| {
ui.set_max_width(260.0);
ui.label(egui::RichText::new("Select reference").strong());
ui.label(egui::RichText::new(state.ref_select_prompt()).weak().small());
// The same card for a pick on a drawing sheet, where the picks are
// the anchors marked on the paper and a drag pans rather than orbits.
let hint = if state.ref_select_is_sheet() {
"Click an anchor marked on the sheet to pick; drag to pan."
} else {
"Click in the viewport to pick; drag to orbit."
};
ui.label(egui::RichText::new(hint).weak().small());
ui.separator();
// Finish / Cancel sit ABOVE the picks: a long pick list below them
// must never push the way out of the picker off the screen.
ui.horizontal(|ui| {
let finish = ui.button("Finish");
self.hits.insert("refsel:finish".into(), finish.rect);
if finish.clicked() {
state.finish_ref_select();
}
let cancel = ui.button("Cancel");
self.hits.insert("refsel:cancel".into(), cancel.rect);
if cancel.clicked() {
state.cancel_ref_select();
}
});
ui.separator();
let names = state.ref_select_names();
// The entity a hovered picked-name line names. The picker rolls the
// model to BEFORE the edited feature, so these rows name geometry that
// IS in the scene — hovering one lights the very edge/face that pick
// grabbed.
let mut hover: Option<String> = None;
if names.is_empty() {
ui.label(egui::RichText::new("(nothing picked yet)").weak());
}
let mut remove = None;
// The picks scroll inside what is left of the screen below this
// card's top-right anchor, so the card never runs off the bottom.
let list_height =
(ui.ctx().content_rect().bottom() - ui.cursor().top() - 24.0).max(80.0);
egui::ScrollArea::vertical()
.id_salt("refsel-picks")
.max_height(list_height)
.show(ui, |ui| {
for (i, name) in names.iter().enumerate() {
// Same order as the feature form's reference lines: the ✕ is
// allocated FIRST, from the RIGHT edge, and the name truncates
// into the remainder. A picked entity's name is unbounded, and
// name-then-button would push the ✕ past the card's 260 pt cap
// and off the screen corner this card is pinned to.
ui.horizontal(|ui| {
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let x = {
let b = crate::icon_text::icon_button(ui, "\u{2716}").small();
ui.add(b)
};
self.hits.insert(format!("refsel:x{i}"), x.rect);
if x.clicked() {
remove = Some(i);
}
// An elided `Label` tooltips its own full text, so a
// truncated name still reads in full on hover.
ui.with_layout(egui::Layout::left_to_right(egui::Align::Center), |ui| {
let line = ui.add(
egui::Label::new(format!("\u{2022} {name}")).truncate(),
);
self.hits.insert(format!("refsel:line{i}"), line.rect);
if line.hovered() {
hover = Some(name.clone());
}
});
});
});
}
});
if let Some(i) = remove {
state.ref_select_remove(i);
}
// Light the hovered pick in the 3D view (the card is drawn over the
// viewport, so the pointer is off it and the engine's one-frame yield
// flag is what keeps the highlight alive). `dialog_hover_end` only ends
// a hover THIS card set.
let hover_changed = match &hover {
Some(name) => state.hover_entity_by_name(DIALOG_HOVER_OWNER, name),
None => state.dialog_hover_end(DIALOG_HOVER_OWNER),
};
if hover_changed {
// This card is an overlay drawn in the same frame as the viewport
// tile, which may have drawn (and consumed `state.dirty`) FIRST —
// without this the new highlight waits for the next input event.
ui.ctx().request_repaint();
}
});
}
/// Sketch mode: the sketch title + Finish (commit) / Cancel (discard). The
/// drawing tools live in the main toolbar's workbench row, as shared
/// workbench buttons (`workbench::sketch`); the selection-driven constraint
/// actions live in the shared context rail below this card. This card is the
/// EXIT, not a tool — which is why it stayed here when the tools moved.
fn sketch_card(&mut self, ui: &mut egui::Ui, state: &mut EngineState) {
egui::Frame::popup(ui.style()).show(ui, |ui| {
let id = state.sketch_edit_feature_id().unwrap_or("").to_string();
ui.label(egui::RichText::new(format!("Sketch: {id}")).strong());
ui.horizontal(|ui| {
let finish = ui.button("Finish").on_hover_text("Commit the sketch");
self.hits.insert("sketch:finish".into(), finish.rect);
if finish.clicked() {
let _ = state.exit_sketch_mode(true);
}
let cancel = ui
.button("Cancel")
.on_hover_text("Discard changes (deletes a new sketch)");
self.hits.insert("sketch:cancel".into(), cancel.rect);
if cancel.clicked() {
let _ = state.exit_sketch_mode(false);
}
});
});
}
/// Published widget hit-rects for the headed verifier.
pub fn hits_json(&self) -> String {
crate::automation::hit_rects::hits_json(&self.hits)
}
}
/// The hit keys this panel publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "modebar", prefix: "refsel:finish", meaning: "finish a CARD-based reference selection (a connection point's seat, a spline anchor's port). A form field's pick has no card: its own `<panel>/field:<path>#activate` is its Done, and a single-valued field finishes on its pick", command: None },
HitKeyDoc { panel: "modebar", prefix: "refsel:cancel", meaning: "cancel a card-based reference selection (Escape cancels a form field's)", command: None },
HitKeyDoc { panel: "modebar", prefix: "refsel:x", meaning: "remove the i-th picked reference (refsel:xi)", command: None },
HitKeyDoc { panel: "modebar", prefix: "refsel:line", meaning: "the i-th picked reference's name line — hovering it lights that entity (refsel:linei)", command: None },
HitKeyDoc { panel: "modebar", prefix: "sketch:finish", meaning: "commit the sketch", command: None },
HitKeyDoc { panel: "modebar", prefix: "sketch:cancel", meaning: "abandon the sketch", command: None },
HitKeyDoc { panel: "modebar", prefix: "sketch3d:finish", meaning: "commit the 3D sketch", command: None },
HitKeyDoc { panel: "modebar", prefix: "sketch3d:cancel", meaning: "abandon the 3D sketch", command: None },
];