Skip to main content

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}