git.lucas.co / cce-ui
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)(&current()).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 }