Skip to main content

ui/
ids.rs

1// ids -- widget ids from names: hashing and a stack of scopes
2//
3//   wid(name) -> u32             const: the id of a name outside every scope, for constants
4//                                (const SAVE: u32 = ui::wid("save") == ui.id("save") there)
5//   wid_path(&[names]) -> u32    const: the last name inside the scopes of the ones before
6//                                (wid_path(&["layers", "add"]) == ui.id("add") after
7//                                ui.id_push_str("layers"))
8//   ui.id(name) -> u32           a name in the current scope (takes &self: ui.button(ui.id("save"), ..))
9//   ui.id_n(n) -> u32            a number in the current scope (the rows of a loop)
10//   ui.id_push(n) / ui.id_push_str(name) / ui.id_pop()   open / close a scope: a number (an
11//                                index, a document's id) or a name
12//   ui.id_scope(n, |ui| ...)     id_push, the closure, id_pop
13// The names of hashed ids are kept for the frame in scripts, with jfgui_DEBUG=1 and while the
14// inspector is open (it shows them: inspect.rs).
15//
16// A hashed id is FNV-1a over the bytes (a number: its four bytes, little endian) and a tag
17// byte (1 name, 2 number), seeded with the scope, then mixed (murmur3's finaliser) into
18// ID_HASH_BASE .. 0x0FFE_FFFF.  Constants written by hand stay below ID_HASH_BASE and never
19// meet hashed ids; bits 28..31 stay the library's (part_id); the 64 Ki ids above the range
20// leave room for widgets that also take id + 1 + i.  Two names of one pass collide with a
21// chance of about 1 : 134 million per pair; scripts and jfgui_DEBUG=1 warn about two names
22// with one id and about an id two widgets share (naming the hashed names involved).  The
23// stack starts empty in every pass; a scope still open at the end of App::frame is closed
24// (a UIWARN the first time).
25
26use alloc::collections::BTreeMap;
27use alloc::string::String;
28use alloc::vec::Vec;
29use core::cell::RefCell;
30
31use super::Ui;
32
33/// the first hashed id; ids written by hand stay below it
34pub const ID_HASH_BASE: u32 = 0x0800_0000;
35const SPAN: u32 = 0x07FF_0000; // hashed ids: ID_HASH_BASE .. ID_HASH_BASE + SPAN
36const SEED: u32 = 0x811C_9DC5; // FNV-1a offset basis: the scope outside every scope
37const PRIME: u32 = 0x0100_0193;
38const TAG_NAME: u32 = 1;
39const TAG_NUM: u32 = 2;
40const DEPTH_MAX: usize = 64; // id scopes inside each other (fixed: nesting; deeper ones warn)
41
42const fn fnv(mut h: u32, b: &[u8], tag: u32) -> u32 {
43    let mut i = 0;
44    while i < b.len() {
45        h = (h ^ b[i] as u32).wrapping_mul(PRIME);
46        i += 1;
47    }
48    (h ^ tag).wrapping_mul(PRIME)
49}
50
51// a scope hash -> its id: murmur3's finaliser, into the hashed range
52const fn finish(mut h: u32) -> u32 {
53    h ^= h >> 16;
54    h = h.wrapping_mul(0x85EB_CA6B);
55    h ^= h >> 13;
56    h = h.wrapping_mul(0xC2B2_AE35);
57    h ^= h >> 16;
58    ID_HASH_BASE + h % SPAN
59}
60
61/// the id of a name outside every scope (the same as ui.id(name) there)
62pub const fn wid(name: &str) -> u32 {
63    finish(fnv(SEED, name.as_bytes(), TAG_NAME))
64}
65
66/// the id of the last name inside the scopes of the names before it (id_push_str each);
67/// no names: wid("")
68pub const fn wid_path(names: &[&str]) -> u32 {
69    if names.is_empty() {
70        return wid("");
71    }
72    let mut h = SEED;
73    let mut i = 0;
74    while i < names.len() {
75        h = fnv(h, names[i].as_bytes(), TAG_NAME);
76        i += 1;
77    }
78    finish(h)
79}
80
81/// the scope stack (Ui::ids)
82#[derive(Default)]
83pub(crate) struct Ids {
84    stack: Vec<u32>,
85    over: u32, // id_push beyond DEPTH_MAX (their id_pop are ignored)
86    /// scripts and jfgui_DEBUG: the hashed ids of this frame -> their names; collisions found
87    /// (reported at the end of App::frame)
88    names: RefCell<BTreeMap<u32, String>>,
89    clash: RefCell<Vec<String>>,
90    reported: alloc::collections::BTreeSet<String>, // each collision is reported once
91    leak_warned: bool, // scopes left open: reported once
92}
93
94impl Ids {
95    fn seed(&self) -> u32 {
96        self.stack.last().copied().unwrap_or(SEED)
97    }
98}
99
100impl Ui {
101    /// the id of a name in the current scope
102    pub fn id(&self, name: &str) -> u32 {
103        let id = finish(fnv(self.ids.seed(), name.as_bytes(), TAG_NAME));
104        if self.dup_check || self.m.ins.on {
105            self.id_note(id, name);
106        }
107        id
108    }
109
110    /// the id of a number in the current scope (the rows of a loop)
111    pub fn id_n(&self, n: u32) -> u32 {
112        let id = finish(fnv(self.ids.seed(), &n.to_le_bytes(), TAG_NUM));
113        if self.dup_check || self.m.ins.on {
114            self.id_note(id, &alloc::format!("#{}", n));
115        }
116        id
117    }
118
119    /// open a scope named by a number (an index, a document's id); always pair with id_pop
120    pub fn id_push(&mut self, n: u32) {
121        let h = fnv(self.ids.seed(), &n.to_le_bytes(), TAG_NUM);
122        self.id_enter(h);
123    }
124
125    /// open a scope named by a name; always pair with id_pop
126    pub fn id_push_str(&mut self, name: &str) {
127        let h = fnv(self.ids.seed(), name.as_bytes(), TAG_NAME);
128        self.id_enter(h);
129    }
130
131    /// close the scope of the last id_push / id_push_str
132    pub fn id_pop(&mut self) {
133        if self.ids.over > 0 {
134            self.ids.over -= 1;
135        } else if self.ids.stack.pop().is_none() {
136            self.warn("id_pop without id_push");
137        }
138    }
139
140    /// id_push(n), f(ui), id_pop()
141    pub fn id_scope<R>(&mut self, n: u32, f: impl FnOnce(&mut Ui) -> R) -> R {
142        self.id_push(n);
143        let r = f(self);
144        self.id_pop();
145        r
146    }
147
148    fn id_enter(&mut self, h: u32) {
149        if self.ids.stack.len() >= DEPTH_MAX {
150            if self.ids.over == 0 {
151                self.warn("id_push: scopes nested too deeply");
152            }
153            self.ids.over += 1;
154            return;
155        }
156        self.ids.stack.push(h);
157    }
158
159    // remember the name of a hashed id; another name with the same id is a collision
160    fn id_note(&self, id: u32, name: &str) {
161        let mut names = self.ids.names.borrow_mut();
162        match names.get(&id) {
163            Some(old) if old != name => {
164                let msg = alloc::format!("ids of \"{}\" and \"{}\" collide ({:#X}): rename one", old, name, id);
165                let mut clash = self.ids.clash.borrow_mut();
166                if !clash.contains(&msg) {
167                    clash.push(msg);
168                }
169            }
170            Some(_) => {}
171            None => {
172                names.insert(id, String::from(name));
173            }
174        }
175    }
176
177    /// (run_passes, after App::frame) collisions found; scopes the app left open are closed
178    pub(crate) fn ids_unwind(&mut self) {
179        let clash = core::mem::take(&mut *self.ids.clash.borrow_mut());
180        for m in clash {
181            if self.ids.reported.insert(m.clone()) {
182                self.warn(&m);
183            }
184        }
185        if !self.ids.stack.is_empty() || self.ids.over != 0 {
186            let n = self.ids.stack.len() as u32 + self.ids.over;
187            self.ids.stack.clear();
188            self.ids.over = 0;
189            if !self.ids.leak_warned {
190                self.ids.leak_warned = true;
191                self.warn(&alloc::format!("id_push without id_pop ({} scope(s) open at the end of the frame)", n));
192            }
193        }
194    }
195
196    /// (frame_begin) a new frame: the names of the last one are forgotten
197    pub(crate) fn ids_frame_begin(&mut self) {
198        if self.dup_check || self.m.ins.on {
199            self.ids.names.borrow_mut().clear();
200        }
201    }
202
203    /// the name of a hashed id seen in this frame (dup_ids)
204    pub(crate) fn id_name(&self, id: u32) -> Option<String> {
205        self.ids.names.borrow().get(&id).cloned()
206    }
207}