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
//! The DOCUMENT TABS seam — the outcome type the dock reports model-tab clicks
//! through, plus the verifier global describing the open models.
//!
//! # There is no strip here any more
//!
//! The tabs are drawn by `egui_tiles` itself: every open model is a
//! [`crate::panels::dock::PaneKind::Document`] pane, and they all live in one
//! `Tabs` container whose tab bar IS the model switcher. An earlier version drew
//! a hand-rolled strip INSIDE the single viewport pane, which left the pane
//! wearing two stacked bars — the tiles tab bar saying "3D View" above our own
//! row of file names. Making the pane's own bar the document bar is both what a
//! docked editor should look like and less code.
//!
//! Exclusivity — the bar holds 3D views and nothing else — is enforced in
//! `dock.rs` instead of by construction, because `egui_tiles` has no hook to
//! refuse a drop into a container: see `evict_foreign_panes_from_document_group`
//! (nothing else may land in the group) and `is_tile_draggable` (no document may
//! leave it).
use crate::automation::hit_keys::HitKeyDoc;
use crate::document::Documents;
use eframe::egui;
/// What the user did in the document tab bar this frame. At most one of each —
/// the shell acts on them after the dock's borrows are released.
#[derive(Default)]
pub struct TabsOutcome {
/// Activate this document (a tab click, seen as egui_tiles' active tab
/// disagreeing with [`Documents`]).
pub activate: Option<usize>,
/// Close this document (its `✕`). Routed through the file dialog so a dirty
/// model is confirmed first.
pub close: Option<usize>,
/// Per-tab screen rects for the headed verifier, keyed `doctab:<index>`,
/// collected in `DockBehavior::on_tab_button`.
pub hits: Vec<(String, egui::Rect)>,
}
/// The `__brepDocuments` verifier global: the open tabs (title, raw name, dirty)
/// and which one is active.
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
pub fn state_json(docs: &Documents) -> String {
let tabs: Vec<serde_json::Value> = docs
.iter()
.map(|doc| {
serde_json::json!({
"title": doc.title(),
"name": doc.name(),
"dirty": doc.dirty_marker(),
})
})
.collect();
serde_json::json!({ "active": docs.active_index(), "tabs": tabs }).to_string()
}
/// The hit keys this panel publishes (see `automation::hit_keys`).
pub static HIT_KEYS: &[HitKeyDoc] = &[
HitKeyDoc { panel: "documents", prefix: "doctab:", meaning: "activate a document tab (doctab:i) or close it (doctab:i:close)", command: None },
HitKeyDoc { panel: "documents", prefix: "pane:", meaning: "a side pane's tab in the dock, by its title in kebab case (pane:inspector); a click brings the pane to the front", command: Some("show_pane") },
];
/// One title formatter for dock tabs and the mobile document menu.
pub(crate) fn display_title(doc: &crate::document::Document) -> String {
if doc.dirty_marker() { format!("{} \u{2022}", doc.title()) } else { doc.title() }
}
#[derive(Default)]
pub(crate) struct MenuState {
titles: Vec<String>,
active: usize,
enabled: bool,
}
impl MenuState {
pub(crate) fn from_documents(docs: &Documents) -> Self {
Self { titles: docs.iter().map(display_title).collect(), active: docs.active_index(),
enabled: !docs.engine().any_sketch_mode() && !docs.engine().ref_select_active() }
}
}
/// The same open documents and TabsOutcome, presented as a finger-sized menu.
pub(crate) fn menu(ui: &mut egui::Ui, state: &MenuState) -> (TabsOutcome, egui::Response) {
let mut outcome = TabsOutcome::default();
let response = ui.add_enabled_ui(state.enabled, |ui| {
ui.menu_button("Docs", |ui| {
ui.set_width((ui.ctx().content_rect().width() - 40.0).min(360.0).max(160.0));
ui.strong("Open documents");
egui::ScrollArea::vertical().max_height((ui.ctx().content_rect().height() - 120.0).max(88.0)).show(ui, |ui| {
for (index, title) in state.titles.iter().enumerate() {
ui.horizontal(|ui| {
let width = (ui.available_width() - 44.0 - ui.spacing().item_spacing.x).max(44.0);
let row = ui.add_sized([width, 44.0], egui::Button::selectable(index == state.active, title).truncate());
outcome.hits.push((format!("doctab:{index}"), row.rect));
if row.clicked() { outcome.activate = Some(index); ui.close(); }
let close = ui.add_sized([44.0, 44.0], egui::Button::new("×")).on_hover_text("Close document");
outcome.hits.push((format!("doctab:{index}:close"), close.rect));
if close.clicked() { outcome.close = Some(index); ui.close(); }
});
}
});
}).response
}).inner.on_disabled_hover_text("Finish the sketch or reference selection before switching documents");
(outcome, response)
}