GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/style.rs (9.5K)
1 //! The style, as ONE snapshot (`docs/rfc-global-state.md`, phase 3).
2 //!
3 //! Every configured colour, radius, font and size, and the registry of flattened config keys,
4 //! is a field of one [`Style`], published whole as an `Arc` and swapped on change. Until
5 //! 2026-10-08 each was a `static RwLock` of its own — about 170 of them beside the registry's
6 //! lock — so a reload wrote them one by one and a frame drawn meanwhile could see half of it,
7 //! and every read took a lock.
8 //!
9 //! What a slot is now: a typed handle, [`StyleCell`], on one field of the snapshot. It keeps
10 //! the `RwLock` API its callers were written against — `.read()` and `.write()`, each
11 //! returning a `Result` whose `Ok` is a guard — so they did not change:
12 //!
13 //! - **`.read()`** derefs into the current snapshot. Taking it costs no lock: each thread
14 //! keeps the last snapshot it saw and checks a generation counter.
15 //! - **`.write()`** gives a copy of the field to change; when the guard drops, a new snapshot
16 //! with that field changed is published. Guards never wait on each other, so writes may
17 //! nest as freely as before.
18 //! - **[`batch`]** runs a reload as one change: inside it, on its thread, writes go to one
19 //! pending snapshot and reads see it; the snapshot is published once when the batch ends.
20 //! A reload is atomic now.
21 //!
22 //! The per-thread test overlays (`color::style_write`, the registry's) are unchanged: they
23 //! key on a cell's address, which a `StyleCell` has as a `static` as the lock did.
24
25 use std::cell::{Cell, RefCell};
26 use std::sync::atomic::{AtomicU64, Ordering};
27 use std::sync::{Arc, RwLock};
28
29 /// The whole style: the colour slots, the layout slots and the registry.
30 #[derive(Clone, Default)]
31 pub struct Style {
32 pub(crate) color: crate::color::Slots,
33 pub(crate) layout: crate::layout::Slots,
34 pub(crate) registry: crate::layout::StyleRegistry,
35 }
36
37 static GLOBAL: RwLock<Option<Arc<Style>>> = RwLock::new(None);
38 static GENERATION: AtomicU64 = AtomicU64::new(1);
39
40 thread_local! {
41 /// The snapshot this thread read last, and the generation it was.
42 static CACHE: RefCell<(u64, Option<Arc<Style>>)> = const { RefCell::new((0, None)) };
43 /// A batch in progress on this thread: its pending snapshot and its depth.
44 static PENDING: Cell<*mut Style> = const { Cell::new(std::ptr::null_mut()) };
45 static DEPTH: Cell<u32> = const { Cell::new(0) };
46 }
47
48 /// The current snapshot.
49 pub fn current() -> Arc<Style> {
50 let generation = GENERATION.load(Ordering::Acquire);
51 CACHE.with(|c| {
52 let mut c = c.borrow_mut();
53 if c.0 == generation {
54 if let Some(s) = &c.1 {
55 return Arc::clone(s);
56 }
57 }
58 let s = {
59 let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
60 Arc::clone(g.get_or_insert_with(|| Arc::new(Style::default())))
61 };
62 *c = (generation, Some(Arc::clone(&s)));
63 s
64 })
65 }
66
67 fn publish(style: Style) {
68 let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
69 *g = Some(Arc::new(style));
70 GENERATION.fetch_add(1, Ordering::AcqRel);
71 }
72
73 /// Apply `change` to a copy of the current snapshot and publish it.
74 fn change(change: impl FnOnce(&mut Style)) {
75 let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
76 let mut style = g.as_deref().cloned().unwrap_or_default();
77 change(&mut style);
78 *g = Some(Arc::new(style));
79 GENERATION.fetch_add(1, Ordering::AcqRel);
80 }
81
82 fn pending() -> Option<*mut Style> {
83 let p = PENDING.with(Cell::get);
84 (!p.is_null()).then_some(p)
85 }
86
87 /// Run `f` as one change of style (see the module docs): its writes are published together
88 /// when it returns, and its reads on this thread see them as they go. Batches nest; the
89 /// outermost publishes.
90 pub fn batch<R>(f: impl FnOnce() -> R) -> R {
91 if DEPTH.with(Cell::get) > 0 {
92 DEPTH.with(|d| d.set(d.get() + 1));
93 let r = f();
94 DEPTH.with(|d| d.set(d.get() - 1));
95 return r;
96 }
97 let boxed = Box::into_raw(Box::new((*current()).clone()));
98 PENDING.with(|p| p.set(boxed));
99 DEPTH.with(|d| d.set(1));
100 // Publish and clear even if `f` unwinds: a half-applied reload is still the newest.
101 struct End(*mut Style);
102 impl Drop for End {
103 fn drop(&mut self) {
104 PENDING.with(|p| p.set(std::ptr::null_mut()));
105 DEPTH.with(|d| d.set(0));
106 // SAFETY: the box made above, released exactly once, here.
107 let style = unsafe { Box::from_raw(self.0) };
108 publish(*style);
109 }
110 }
111 let _end = End(boxed);
112 f()
113 }
114
115 /// A handle on one field of the [`Style`]: what a style slot's `static` is.
116 pub struct StyleCell<T: 'static> {
117 get: fn(&Style) -> &T,
118 get_mut: fn(&mut Style) -> &mut T,
119 }
120
121 /// A style slot cannot fail to read or write; the `Result` is the `RwLock` API's shape.
122 #[derive(Debug)]
123 pub struct StyleError;
124
125 impl<T: Clone + 'static> StyleCell<T> {
126 pub const fn new(get: fn(&Style) -> &T, get_mut: fn(&mut Style) -> &mut T) -> StyleCell<T> {
127 StyleCell { get, get_mut }
128 }
129
130 /// The field in the current snapshot (or this thread's pending one, in a batch).
131 pub fn read(&self) -> Result<StyleRef<T>, StyleError> {
132 match pending() {
133 // SAFETY: the pending snapshot lives until its batch ends, and a guard is a
134 // temporary of code running inside the batch; read and write guards on one
135 // slot never overlap there, as they could not under the lock this replaced.
136 Some(p) => Ok(StyleRef::Pending((self.get)(unsafe { &*p }) as *const T)),
137 None => Ok(StyleRef::Snapshot(current(), self.get)),
138 }
139 }
140
141 /// The field to change: published when the guard drops (in a batch, changed in place).
142 pub fn write(&self) -> Result<StyleMut<T>, StyleError> {
143 match pending() {
144 // SAFETY: as in `read`.
145 Some(p) => Ok(StyleMut::Pending((self.get_mut)(unsafe { &mut *p }) as *mut T)),
146 None => Ok(StyleMut::Copy(Some((self.get)(¤t()).clone()), self.get_mut)),
147 }
148 }
149
150 /// The field's value now.
151 pub fn get(&self) -> T {
152 self.read().map(|v| v.clone()).unwrap_or_else(|_| unreachable!())
153 }
154 }
155
156 /// A read of one style field.
157 pub enum StyleRef<T: 'static> {
158 Snapshot(Arc<Style>, fn(&Style) -> &T),
159 Pending(*const T),
160 }
161
162 impl<T> std::ops::Deref for StyleRef<T> {
163 type Target = T;
164 fn deref(&self) -> &T {
165 match self {
166 StyleRef::Snapshot(s, get) => get(s),
167 // SAFETY: see `StyleCell::read`.
168 StyleRef::Pending(p) => unsafe { &**p },
169 }
170 }
171 }
172
173 /// A write of one style field.
174 pub enum StyleMut<T: 'static> {
175 /// Outside a batch: a copy, published into a new snapshot on drop.
176 Copy(Option<T>, fn(&mut Style) -> &mut T),
177 /// In a batch: the pending snapshot's field.
178 Pending(*mut T),
179 }
180
181 impl<T> std::ops::Deref for StyleMut<T> {
182 type Target = T;
183 fn deref(&self) -> &T {
184 match self {
185 StyleMut::Copy(v, _) => v.as_ref().expect("taken only on drop"),
186 // SAFETY: see `StyleCell::read`.
187 StyleMut::Pending(p) => unsafe { &**p },
188 }
189 }
190 }
191
192 impl<T> std::ops::DerefMut for StyleMut<T> {
193 fn deref_mut(&mut self) -> &mut T {
194 match self {
195 StyleMut::Copy(v, _) => v.as_mut().expect("taken only on drop"),
196 // SAFETY: see `StyleCell::read`.
197 StyleMut::Pending(p) => unsafe { &mut **p },
198 }
199 }
200 }
201
202 impl<T: 'static> Drop for StyleMut<T> {
203 fn drop(&mut self) {
204 if let StyleMut::Copy(v, get_mut) = self {
205 if let Some(value) = v.take() {
206 let get_mut = *get_mut;
207 change(|s| *get_mut(s) = value);
208 }
209 }
210 }
211 }
212
213 /// Declare a module's style slots: the fields of its part of the [`Style`] and their defaults.
214 /// Each has a `static` [`StyleCell`] handle of the same name beside the module's getters.
215 macro_rules! style_slots {
216 ($( $name:ident : $ty:ty = $default:expr ; )*) => {
217 /// This module's style slots (see `crate::style`).
218 #[derive(Clone)]
219 #[allow(non_snake_case)]
220 pub struct Slots { $( pub(crate) $name: $ty, )* }
221 impl Default for Slots {
222 fn default() -> Self { Slots { $( $name: $default, )* } }
223 }
224 };
225 }
226 pub(crate) use style_slots;
227
228 #[cfg(test)]
229 mod tests {
230 use super::*;
231
232 /// A write publishes; a batch publishes once, and its own reads see its writes before.
233 #[test]
234 fn writes_publish_and_a_batch_publishes_once() {
235 static PROBE: StyleCell<crate::layout::StyleRegistry> = StyleCell::new(|s| &s.registry, |s| &mut s.registry);
236 let before = GENERATION.load(Ordering::Acquire);
237 PROBE.write().unwrap().load_float("style-test-a", 1.0);
238 assert_eq!(PROBE.read().unwrap().floats.get("style-test-a"), Some(&1.0));
239 assert!(GENERATION.load(Ordering::Acquire) > before);
240
241 batch(|| {
242 PROBE.write().unwrap().load_float("style-test-b", 2.0);
243 PROBE.write().unwrap().load_float("style-test-c", 3.0);
244 assert_eq!(PROBE.read().unwrap().floats.get("style-test-b"), Some(&2.0), "a batch reads its own writes");
245 // Another thread still sees the snapshot from before the batch.
246 let seen = std::thread::spawn(|| PROBE.read().unwrap().floats.get("style-test-b").copied()).join().unwrap();
247 assert_eq!(seen, None, "nothing published yet");
248 });
249 let r = PROBE.read().unwrap();
250 assert_eq!((r.floats.get("style-test-b"), r.floats.get("style-test-c")), (Some(&2.0), Some(&3.0)));
251 }
252 }