blockworx/tools/chrome.rs
1//! The chrome still laid directly into the canvas `Ui`: the selection
2//! overlay and the document notices. The docked bands are `egui::Panel`s and
3//! carry their own ids (see [`crate::shell`]).
4
5/// One piece of canvas chrome. The variant *is* the panel's egui
6/// [`egui::Id`], so no two panels can collide and none of them depends on
7/// where it falls among its siblings.
8#[derive(Clone, Copy, PartialEq, Eq, Debug)]
9pub enum Panel {
10 SelectionButtons,
11 /// The right-click menu, which carries exactly the overlay's commands
12 /// (ยง3.6). Its own id so egui's popup memory can hold it open.
13 SelectionMenu,
14 DocumentNotices,
15}
16
17impl From<Panel> for egui::Id {
18 fn from(panel: Panel) -> Self {
19 egui::Id::new(match panel {
20 Panel::SelectionButtons => "selection_buttons",
21 Panel::SelectionMenu => "selection_menu",
22 Panel::DocumentNotices => "document_notices",
23 })
24 }
25}
26
27impl Panel {
28 /// A [`egui::UiBuilder`] carrying this panel's id, for the caller to give
29 /// a rect and a layout.
30 ///
31 /// The id is *explicit*, not a salt: `UiBuilder::id_salt` mixes in the
32 /// parent's auto-id counter, so a transient sibling drawn earlier in the
33 /// same `Ui` โ an inline editor opening over the canvas โ renumbers every
34 /// panel after it. The panels keep their rects, so a debug build outlines
35 /// the lot in red for that frame (egui's `warn_if_rect_changes_id`).
36 pub fn ui_builder(self) -> egui::UiBuilder {
37 egui::UiBuilder::new().id(egui::Id::from(self))
38 }
39
40 /// Where this panel stashes a measurement between frames โ a size it can
41 /// only know after egui has laid it out once.
42 pub fn measurement(self, what: &'static str) -> egui::Id {
43 egui::Id::from(self).with(what)
44 }
45}
46
47#[cfg(test)]
48mod tests {
49 use super::Panel;
50
51 const PANELS: [Panel; 3] = [
52 Panel::SelectionButtons,
53 Panel::SelectionMenu,
54 Panel::DocumentNotices,
55 ];
56
57 /// Two panels sharing an id would share egui state (focus, open popups).
58 #[test]
59 fn every_panel_has_its_own_id() {
60 let ids: std::collections::HashSet<egui::Id> =
61 PANELS.into_iter().map(egui::Id::from).collect();
62 assert_eq!(ids.len(), PANELS.len());
63 }
64
65 /// The bug this type exists for: a transient widget drawn earlier in the
66 /// same `Ui` must not renumber the panel or anything inside it.
67 #[test]
68 fn a_panel_ignores_how_many_siblings_came_first() {
69 let ctx = egui::Context::default();
70 let ids = |siblings: usize| {
71 let mut ids = None;
72 ctx.clone()
73 .run_ui(egui::RawInput::default(), |ui| {
74 for _ in 0..siblings {
75 ui.label("an editor that opened over the canvas");
76 }
77 let mut panel =
78 ui.new_child(Panel::SelectionButtons.ui_builder().max_rect(ui.max_rect()));
79 let button = panel.button("Undo").id;
80 ids = Some((panel.unique_id(), button));
81 })
82 .drop_without_applying_deltas();
83 ids.expect("the pass runs the closure")
84 };
85 let (none_before, one_before) = (ids(0), ids(1));
86 assert_ne!(
87 none_before.0, none_before.1,
88 "precondition: the panel and its button are distinct ids",
89 );
90 assert_eq!(none_before, one_before);
91 }
92}