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
//! mode_bar — the special-mode EXIT controls, always pinned to the TOP-RIGHT
//! corner of the screen. Every special mode (reference-selection, 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.
//!
//! 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 brep_render::engine_state::EngineState;
use eframe::egui;
use std::collections::HashMap;
/// 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 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();
if state.ref_select_active() {
self.reference_card(ui, state);
} else if state.sketch_mode() {
self.sketch_card(ui, state);
}
}
/// 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());
ui.label(
egui::RichText::new("Click in the viewport to pick; drag to orbit.")
.weak()
.small(),
);
ui.separator();
let names = state.ref_select_names();
if names.is_empty() {
ui.label(egui::RichText::new("(nothing picked yet)").weak());
}
let mut remove = None;
for (i, name) in names.iter().enumerate() {
ui.horizontal(|ui| {
ui.label(format!("\u{2022} {name}"));
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);
}
});
}
if let Some(i) = remove {
state.ref_select_remove(i);
}
ui.separator();
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();
}
});
});
}
/// Sketch mode: the sketch title + Finish (commit) / Cancel (discard). The
/// drawing tools live in the sketch tool strip; the selection-driven
/// constraint actions live in the shared context rail below this card.
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.
#[cfg(target_arch = "wasm32")]
pub fn hits_json(&self) -> String {
let map: serde_json::Map<String, serde_json::Value> = self
.hits
.iter()
.map(|(k, r)| {
(
k.clone(),
serde_json::json!([r.min.x, r.min.y, r.width(), r.height()]),
)
})
.collect();
serde_json::Value::Object(map).to_string()
}
}