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
//! 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 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 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();
// 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;
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();
}
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.
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 reference selection", command: None },
HitKeyDoc { panel: "modebar", prefix: "refsel:cancel", meaning: "cancel reference selection", 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 },
];