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}