GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor(style): the style is one snapshot (global-state RFC phase 3)
crate::style::Style holds the colour slots, the layout slots, the named
materials and the registry, published as an Arc and replaced whole on change.
Each former style `static RwLock` (178) is a style::StyleCell handle on one
field with the same .read() / .write() API, so none of their ~550 uses
changed (get_style_registry returns the registry's handle; cce-grid compiles
unchanged). Reads take no lock (a per-thread snapshot checked against a
generation); a write publishes when its guard drops; reload_config and the
colour load run as one style::batch, published once -- a reload is atomic.
14 slots written and never read are gone: 13 per-widget corner radii whose
getters read the registry, and the button hover colour (a key that did
nothing).
Checked: the demo and the settings app against the real config draw
identically to the pixel before and after.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 9 +-
docs/rfc-global-state.md | 22 ++-
src/color/load.rs | 9 +-
src/color/materials.rs | 5 +-
src/color/mod.rs | 330 +++++++++++++++++++++++++++++---------------
src/layout/mod.rs | 350 ++++++++++++++++++++---------------------------
src/layout/registry.rs | 13 +-
src/lib.rs | 1 +
src/style.rs | 252 ++++++++++++++++++++++++++++++++++
9 files changed, 660 insertions(+), 331 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 0129c79..9f3a9e8 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1665,7 +1665,12 @@ cursor, the side swipe, the input-method composition — is a `window_state::Win
window OWNS, which its shell makes current (`window_state::enter`) while it runs that
window's code; the modules' free functions (`context_menu::show`, `ime::caret`, …) act on
the current one, so no caller changed, and with none entered (a test) each thread has a
-default. `context_menu::with_state` replaces reaching for the old `CONTEXT_MENU`. An app
+default. `context_menu::with_state` replaces reaching for the old `CONTEXT_MENU`. Phase 3
+is done: the style is ONE snapshot (`crate::style::Style`: colour slots, layout slots, named
+materials, the registry), published as an `Arc`; each former style `RwLock` is a
+`style::StyleCell` handle with the same `.read()` / `.write()` API, reads take no lock, and
+a reload runs as one `style::batch`, published once. A new style value is a field in its
+module's `style_slots!` block and a `StyleCell` handle, not a new lock. An app
that drives a widget's focus itself calls `UiContext::focus_widget` / `unfocus_widget`
rather than `w.focus()` / `w.unfocus()`, so the window's record of focus follows. Do not
add a static for state that belongs to a window: give it a field in `WindowState`.
@@ -1766,6 +1771,8 @@ cce-system-interface) to confirm behavior, not just the test suite.
`docs/rfc-accessibility-locale.md`, phase 2).
- `l10n.rs` — the toolkit's catalogue (`tr`, `tr_args`, `catalog`) over `cce_core::l10n`;
its English is `locale/en-US/cce-ui.ftl`.
+- `style.rs` — the style snapshot: `Style`, `StyleCell`, `batch`, `style_slots!`.
+- `window_state.rs` — a window's interaction state (`WindowState`, `enter`).
- `ime.rs` — input-method composition shared between the editing widget and the shell:
`Preedit`, the composition and its generation, the reported caret, the reset request
(see "Input-method composition is one model for every shell").
diff --git a/docs/rfc-global-state.md b/docs/rfc-global-state.md
index be23bfb..8c1839c 100644
--- a/docs/rfc-global-state.md
+++ b/docs/rfc-global-state.md
@@ -130,7 +130,27 @@ widgets and through `UiContext` by apps; the free functions stay as deprecated f
for one release while the apps move. The 139 direct `focus()` / `unfocus()` calls move to
the context's focus at the same time.
-### Phase 3 — style as one snapshot
+### Phase 3 — style as one snapshot — DONE 2026-10-08
+
+`crate::style::Style` is the whole style — the colour slots, the layout slots, the named
+materials and the registry — published as an `Arc` and replaced whole on change. Each
+former `static RwLock` (178 of them) is a typed handle on one field, `style::StyleCell`,
+keeping the `RwLock` API (`.read()` / `.write()` returning a `Result` of a guard), so none of
+the ~550 places that read or write them changed; `get_style_registry` returns the
+registry's handle, and cce-grid, which writes it, compiles unchanged. A read takes no lock
+(each thread keeps the last snapshot and checks a generation); a write publishes a new
+snapshot when its guard drops, and guards never wait on each other. `reload_config` and
+the colour load run as one `style::batch`: their writes go to one pending snapshot, which
+their own reads see, published once — a reload is atomic, and its hundreds of per-key
+writes cost one copy. 14 slots that were written and never read went with it (13
+per-widget corner radii whose getters read the registry long ago, and the button hover
+colour, a config key that did nothing). The per-thread test overlays are as they were.
+Checked: the suite passes repeatedly; the demo and the settings app, run against the real
+config, draw identically to the pixel before and after.
+
+Not done: some keys are still parsed twice in one reload, into the registry and into a
+slot; they are now one snapshot, so they cannot disagree, but one of the two copies could
+go per key.
`Style` (every colour, radius, font and size, and the registry's flattened config) built
whole by a reload and published as an `Arc`; getters read the current snapshot; tests
diff --git a/src/color/load.rs b/src/color/load.rs
index 45d6567..3960583 100644
--- a/src/color/load.rs
+++ b/src/color/load.rs
@@ -11,7 +11,13 @@ fn read_config() -> Option<String> {
None
}
+/// Load the colour and surface config as one change of style (`crate::style::batch`): a
+/// frame drawn meanwhile sees the style before or after, never half of each.
fn parse_and_set_colors(content: &str) {
+ crate::style::batch(|| parse_and_set_colors_in_batch(content));
+}
+
+fn parse_and_set_colors_in_batch(content: &str) {
let val = crate::config::parse_kdl_to_json(content);
let parse_hex = |hex_str: &str| -> Option<[f32; 4]> {
@@ -205,9 +211,6 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/style/control/button/border_color").or_else(|| get_color("/style/button/border_color")) {
if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() { *lock = Some(c); }
}
- if let Some(c) = get_color("/style/control/button/hover_color").or_else(|| get_color("/style/button/hover_color")) {
- if let Ok(mut lock) = BUTTON_HOVER_COLOR.write() { *lock = Some(c); }
- }
if let Some(c) = get_color("/style/control/dropdown/border_color").or_else(|| get_color("/style/dropdown/border_color")) {
if let Ok(mut lock) = DROPDOWN_BORDER_COLOR.write() { *lock = c; }
}
diff --git a/src/color/materials.rs b/src/color/materials.rs
index e2c929c..78e9cda 100644
--- a/src/color/materials.rs
+++ b/src/color/materials.rs
@@ -1,15 +1,14 @@
//! The named materials config defines and the rung bindings (`docs/rfc-material.md` § 5).
use super::*;
-use std::sync::RwLock;
/// The named materials config defines (`style.surface.material { <name> {…} }`)
/// and the rung bindings (`plate material=`, `plate { root material= }`,
/// `control material=`) — see `docs/rfc-material.md` § 5. Replaced wholesale
/// on every config load, so a node or binding removed from config is gone
/// after a reload.
-pub(super) static NAMED_MATERIALS: RwLock<Vec<(String, crate::scene::material::MaterialDef)>> = RwLock::new(Vec::new());
-pub(super) static MATERIAL_BINDINGS: RwLock<[Option<String>; 3]> = RwLock::new([None, None, None]);
+pub(super) static NAMED_MATERIALS: crate::style::StyleCell<Vec<(String, crate::scene::material::MaterialDef)>> = crate::style::StyleCell::new(|s| &s.color.NAMED_MATERIALS, |s| &mut s.color.NAMED_MATERIALS);
+pub(super) static MATERIAL_BINDINGS: crate::style::StyleCell<[Option<String>; 3]> = crate::style::StyleCell::new(|s| &s.color.MATERIAL_BINDINGS, |s| &mut s.color.MATERIAL_BINDINGS);
fn rung_index(rung: crate::scene::material::PlateRung) -> usize {
match rung {
diff --git a/src/color/mod.rs b/src/color/mod.rs
index 32b0ab2..95c08ed 100644
--- a/src/color/mod.rs
+++ b/src/color/mod.rs
@@ -29,7 +29,110 @@ pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
-use std::sync::RwLock;
+
+// This module's part of the one style snapshot (`crate::style`): every slot below,
+// with its default. Each slot's `static` handle stands where its lock did.
+crate::style::style_slots! {
+ NAMED_MATERIALS: Vec<(String, crate::scene::material::MaterialDef)> = Vec::new();
+ MATERIAL_BINDINGS: [Option<String>; 3] = [None, None, None];
+ PAGE_LOW_COLOR: [f32; 4] = [0.0600316, 0.0600316, 0.080219, 1.0];
+ COLOR_BORDERS_COLOR: [f32; 4] = [0.2039, 0.2039, 0.2530, 1.0];
+ NODE_COLOR: [f32; 4] = NODE_IDLE;
+ NODE_SELECTED_COLOR: [f32; 4] = NODE_SELECTED;
+ NODE_DRAG_COLOR: [f32; 4] = NODE_DRAG;
+ SIDEBAR_BG_COLOR: [f32; 4] = SIDEBAR_BG;
+ HIGHLIGHT_PRIMARY_COLOR: [f32; 4] = HIGHLIGHT_PRIMARY;
+ MENUBAR_TAB_LABEL_COLOR: [f32; 4] = [0.90196, 0.90196, 0.94902, 1.0];
+ CONTROL_LABEL_COLOR: [f32; 4] = [0.61206, 0.61206, 0.68666, 1.0];
+ CONTROL_LABEL_COLOR_DETACHED: [f32; 4] = [0.22416, 0.22416, 0.2526, 1.0];
+ CONTROL_LABEL_HOVER_COLOR: Option<[f32; 4]> = None;
+ CONTROL_LABEL_FOCUS_COLOR: Option<[f32; 4]> = None;
+ OPACITY: Option<f32> = None;
+ ROOT_PLATE_OPACITY: Option<f32> = None;
+ LIST_BG_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 0.3];
+ LIST_ENTRY_BG_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 0.04];
+ LIST_ENTRY_HIGHLIGHT_COLOR: [f32; 4] = [1.0, 1.0, 1.0, 0.8];
+ LIST_FONT_COLOR: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
+ BREADCRUMB_BG_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+ POPOVER_BG_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+ PAGE_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
+ LAYER_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
+ ROOT_PLATE_CORNER_RADIUS: f32 = 12.0;
+ DROPDOWN_BACKGROUND_COLOR: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
+ TEXTBOX_PLACEHOLDER_TEXT_COLOR: [u8; 3] = [0x60, 0x60, 0x6a];
+ TEXTBOX_BACKGROUND_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+ TEXTBOX_BACKGROUND_EDIT_COLOR: [f32; 4] = [0.06, 0.10, 0.18, 1.0];
+ ROOT_PLATE_MENUBAR_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+ ROOT_PLATE_MENUBAR_TEXT_COLOR: [f32; 4] = [0.90196, 0.90196, 0.94902, 1.0];
+ ROOT_PLATE_MENUBAR_BLUR: bool = false;
+ MENU_OPACITY: f32 = 0.8;
+ MENU_COMPRESSION: f32 = 0.6;
+ MENU_COLOR: Option<[f32; 4]> = None;
+ ROOT_PLATE_STATUSBAR_COLOR: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
+ ROOT_PLATE_STATUSBAR_TEXT_COLOR: [f32; 4] = [0.6666, 0.6666, 0.7333, 1.0];
+ ROOT_PLATE_STATUSBAR_BLUR: bool = false;
+ BUTTON_BACKGROUND_COLOR: [f32; 4] = BUTTON_IDLE;
+ RAMP_BACKGROUND_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
+ RAMP_BORDER_COLOR: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
+ CONTROL_PANEL_COLOR: [f32; 4] = [0.075, 0.082, 0.11, 1.0];
+ CONTROL_PANEL_BORDER_COLOR: [f32; 4] = [0.161, 0.173, 0.216, 1.0];
+ PROGRESS_BG_COLOR: [f32; 4] = PROGRESS_BG;
+ PROGRESS_FILL_COLOR: [f32; 4] = PROGRESS_FILL;
+ SPINBOX_DISPLAY_COLOR: [f32; 4] = SPINBOX_DISPLAY;
+ SPINBOX_BUTTON_COLOR: [f32; 4] = SPINBOX_BUTTON;
+ SPINBOX_BUTTON_HOVER_COLOR: [f32; 4] = SPINBOX_BUTTON_HOVER;
+ SPINBOX_TEXT_COLOR: [f32; 4] = [0.8, 0.8, 0.83, 1.0];
+ BUTTON_BORDER_COLOR: Option<[f32; 4]> = None;
+ DROPDOWN_BORDER_COLOR: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
+ DROPDOWN_TEXT_COLOR: [f32; 4] = [0.72305, 0.72305, 0.76008, 1.0];
+ SLIDER_THUMB_COLOR: [f32; 4] = SLIDER_THUMB;
+ SLIDER_THUMB_DRAG_COLOR: [f32; 4] = SLIDER_THUMB_DRAG;
+ RANGE_SLIDER_THUMB_COLOR: [f32; 4] = SLIDER_THUMB;
+ RANGE_SLIDER_THUMB_DRAG_COLOR: [f32; 4] = SLIDER_THUMB_DRAG;
+ TREE_BACKGROUND_COLOR: [f32; 4] = [0.08, 0.08, 0.12, 0.3];
+ TREE_BORDER_COLOR: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
+ TREE_BORDER_HOVER_COLOR: [f32; 4] = [0.25, 0.25, 0.35, 1.0];
+ TREE_BORDER_FOCUS_COLOR: [f32; 4] = [0.30, 0.50, 0.32, 1.0];
+ TREE_OPEN_SEARCH_KEY: String = String::new();
+ LIST_OPEN_SEARCH_KEY: String = String::new();
+ LIST_CLOSE_SEARCH_KEY: String = String::new();
+ TREE_SECTION_BG_COLOR: [f32; 4] = [0.07, 0.07, 0.09, 1.0];
+ TREE_SECTION_BG_HOVER_COLOR: [f32; 4] = [0.10, 0.12, 0.18, 1.0];
+ TREE_LEAF_BG_EVEN_COLOR: [f32; 4] = [0.09, 0.09, 0.11, 1.0];
+ TREE_LEAF_BG_ODD_COLOR: [f32; 4] = [0.08, 0.08, 0.10, 1.0];
+ TREE_LEAF_BG_HOVER_COLOR: [f32; 4] = [0.12, 0.12, 0.16, 1.0];
+ TREE_LEAF_BG_SELECTED_COLOR: [f32; 4] = [0.15, 0.20, 0.30, 1.0];
+ TREE_SECTION_TEXT_COLOR: [f32; 4] = [0.38, 0.69, 0.94, 1.0];
+ TREE_LEAF_TEXT_COLOR: [f32; 4] = [0.80, 0.80, 0.83, 1.0];
+ TREE_LEAF_TEXT_SELECTED_COLOR: [f32; 4] = [0.49, 1.0, 1.0, 1.0];
+ TREE_TYPE_TEXT_COLOR: [f32; 4] = [0.78, 0.47, 0.87, 1.0];
+ TREE_VALUE_TEXT_COLOR: [f32; 4] = [0.51, 0.51, 0.54, 1.0];
+ TREE_SEPARATOR_COLOR: [f32; 4] = [0.15, 0.15, 0.19, 1.0];
+ SCROLLBAR_TRACK_COLOR: [f32; 4] = [0.15, 0.15, 0.20, 0.3];
+ SCROLLBAR_THUMB_COLOR: [f32; 4] = [0.60, 0.60, 0.65, 0.4];
+ GRAPH_GRID_COLOR: [f32; 3] = [0.07, 0.07, 0.09];
+ GRAPH_OPACITY: f32 = 0.95;
+ GRAPH_NODE_OPACITY: f32 = 1.0;
+ GRAPH_NODE_COLOR: [f32; 4] = NODE_IDLE;
+ GRAPH_NODE_SELECTED_COLOR: [f32; 4] = NODE_SELECTED;
+ GRAPH_NODE_DRAG_COLOR: [f32; 4] = NODE_DRAG;
+ GRAPH_WIRE_COLOR: [f32; 4] = [0.1, 0.8, 0.4, 1.0];
+ GRAPH_WIRE_HIGHLIGHT_COLOR: [f32; 4] = [0.0, 1.0, 0.9, 1.0];
+ GRAPH_CONNECTOR_COLOR: [f32; 4] = [0.1, 0.8, 0.4, 1.0];
+ GRAPH_CONNECTOR_HIGHLIGHT_COLOR: [f32; 4] = [0.0, 1.0, 0.9, 1.0];
+ PARAM_BG_COLOR: [f32; 4] = PARAM_BG;
+ PANE_COLOR_WHOLE: bool = false;
+ PLATE_COLOR: Option<[f32; 4]> = Some([0.15, 0.15, 0.2, 0.95]);
+ PLATE_BORDER_COLOR: Option<[f32; 4]> = Some([0.3, 0.3, 0.4, 1.0]);
+ PLATE_BORDER_THICKNESS: f32 = 1.0;
+ PLATE_BACKDROP_COMPRESSION: f32 = 0.0;
+ PLATE_REFRACTION: f32 = 0.0;
+ PLATE_BLUR: bool = false;
+ FINISH_SPEC: f32 = FINISH_SPEC_DEFAULT;
+ FINISH_SHININESS: f32 = FINISH_SHININESS_DEFAULT;
+ FINISH_CURVATURE: f32 = FINISH_CURVATURE_DEFAULT;
+ PLATE_FROST_RADIUS: f32 = crate::scene::material::Frost::DEFAULT_RADIUS;
+}
/// Per-thread overrides for runtime colour writes, under `cfg(test)` only.
///
@@ -61,7 +164,7 @@ mod test_overlay {
/// Read a style static, preferring this thread's test override.
#[inline]
-fn style_read<T: Clone + 'static>(cell: &'static RwLock<T>) -> T {
+fn style_read<T: Clone + 'static>(cell: &'static crate::style::StyleCell<T>) -> T {
#[cfg(test)]
if let Some(v) = test_overlay::get::<T>(cell as *const _ as usize) {
return v;
@@ -71,7 +174,7 @@ fn style_read<T: Clone + 'static>(cell: &'static RwLock<T>) -> T {
/// Write a style static: per-thread under `cfg(test)`, process-wide otherwise.
#[inline]
-fn style_write<T: Clone + 'static>(cell: &'static RwLock<T>, val: T) {
+fn style_write<T: Clone + 'static>(cell: &'static crate::style::StyleCell<T>, val: T) {
#[cfg(test)]
test_overlay::set(cell as *const _ as usize, val);
#[cfg(not(test))]
@@ -81,132 +184,131 @@ fn style_write<T: Clone + 'static>(cell: &'static RwLock<T>, val: T) {
}
-static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
-static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
-static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
-static NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
-static NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
-static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
-static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
-static MENUBAR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
-static CONTROL_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.61206, 0.61206, 0.68666, 1.0]); // sRGB [204, 204, 212]
-static CONTROL_LABEL_COLOR_DETACHED: RwLock<[f32; 4]> = RwLock::new([0.22416, 0.22416, 0.2526, 1.0]); // sRGB [131, 131, 138]
-static CONTROL_LABEL_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
-static CONTROL_LABEL_FOCUS_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
-static OPACITY: RwLock<Option<f32>> = RwLock::new(None);
-static ROOT_PLATE_OPACITY: RwLock<Option<f32>> = RwLock::new(None);
-static LIST_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
-static LIST_ENTRY_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.04]);
-static LIST_ENTRY_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.8]);
-static LIST_FONT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.85, 1.0]);
-static BREADCRUMB_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
-static POPOVER_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
-static PAGE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
-static LAYER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
-static ROOT_PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
+static PAGE_LOW_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.PAGE_LOW_COLOR, |s| &mut s.color.PAGE_LOW_COLOR);
+static COLOR_BORDERS_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.COLOR_BORDERS_COLOR, |s| &mut s.color.COLOR_BORDERS_COLOR);
+static NODE_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.NODE_COLOR, |s| &mut s.color.NODE_COLOR);
+static NODE_SELECTED_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.NODE_SELECTED_COLOR, |s| &mut s.color.NODE_SELECTED_COLOR);
+static NODE_DRAG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.NODE_DRAG_COLOR, |s| &mut s.color.NODE_DRAG_COLOR);
+static SIDEBAR_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SIDEBAR_BG_COLOR, |s| &mut s.color.SIDEBAR_BG_COLOR);
+static HIGHLIGHT_PRIMARY_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.HIGHLIGHT_PRIMARY_COLOR, |s| &mut s.color.HIGHLIGHT_PRIMARY_COLOR);
+static MENUBAR_TAB_LABEL_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.MENUBAR_TAB_LABEL_COLOR, |s| &mut s.color.MENUBAR_TAB_LABEL_COLOR); // sRGB [230, 230, 242] linear
+static CONTROL_LABEL_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.CONTROL_LABEL_COLOR, |s| &mut s.color.CONTROL_LABEL_COLOR); // sRGB [204, 204, 212]
+static CONTROL_LABEL_COLOR_DETACHED: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.CONTROL_LABEL_COLOR_DETACHED, |s| &mut s.color.CONTROL_LABEL_COLOR_DETACHED); // sRGB [131, 131, 138]
+static CONTROL_LABEL_HOVER_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.CONTROL_LABEL_HOVER_COLOR, |s| &mut s.color.CONTROL_LABEL_HOVER_COLOR);
+static CONTROL_LABEL_FOCUS_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.CONTROL_LABEL_FOCUS_COLOR, |s| &mut s.color.CONTROL_LABEL_FOCUS_COLOR);
+static OPACITY: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.color.OPACITY, |s| &mut s.color.OPACITY);
+static ROOT_PLATE_OPACITY: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_OPACITY, |s| &mut s.color.ROOT_PLATE_OPACITY);
+static LIST_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.LIST_BG_COLOR, |s| &mut s.color.LIST_BG_COLOR);
+static LIST_ENTRY_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.LIST_ENTRY_BG_COLOR, |s| &mut s.color.LIST_ENTRY_BG_COLOR);
+static LIST_ENTRY_HIGHLIGHT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.LIST_ENTRY_HIGHLIGHT_COLOR, |s| &mut s.color.LIST_ENTRY_HIGHLIGHT_COLOR);
+static LIST_FONT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.LIST_FONT_COLOR, |s| &mut s.color.LIST_FONT_COLOR);
+static BREADCRUMB_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.BREADCRUMB_BG_COLOR, |s| &mut s.color.BREADCRUMB_BG_COLOR);
+static POPOVER_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.POPOVER_BG_COLOR, |s| &mut s.color.POPOVER_BG_COLOR);
+static PAGE_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.PAGE_COLOR, |s| &mut s.color.PAGE_COLOR);
+static LAYER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.LAYER_COLOR, |s| &mut s.color.LAYER_COLOR);
+static ROOT_PLATE_CORNER_RADIUS: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_CORNER_RADIUS, |s| &mut s.color.ROOT_PLATE_CORNER_RADIUS);
// Transparent by default (alpha 0): a dropdown picks up the surface it sits
// on, and its closed-state chrome is the flush inset trough alone — the
// cce-files treatment, DE-wide. A configured `dropdown color=` opts a theme
// back into a filled face (the paint path judges the RAW alpha).
-static DROPDOWN_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
+static DROPDOWN_BACKGROUND_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.DROPDOWN_BACKGROUND_COLOR, |s| &mut s.color.DROPDOWN_BACKGROUND_COLOR);
-static TEXTBOX_PLACEHOLDER_TEXT_COLOR: RwLock<[u8; 3]> = RwLock::new([0x60, 0x60, 0x6a]);
-static TEXTBOX_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
-static TEXTBOX_BACKGROUND_EDIT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.10, 0.18, 1.0]);
+static TEXTBOX_PLACEHOLDER_TEXT_COLOR: crate::style::StyleCell<[u8; 3]> = crate::style::StyleCell::new(|s| &s.color.TEXTBOX_PLACEHOLDER_TEXT_COLOR, |s| &mut s.color.TEXTBOX_PLACEHOLDER_TEXT_COLOR);
+static TEXTBOX_BACKGROUND_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TEXTBOX_BACKGROUND_COLOR, |s| &mut s.color.TEXTBOX_BACKGROUND_COLOR);
+static TEXTBOX_BACKGROUND_EDIT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TEXTBOX_BACKGROUND_EDIT_COLOR, |s| &mut s.color.TEXTBOX_BACKGROUND_EDIT_COLOR);
-static ROOT_PLATE_MENUBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
-static ROOT_PLATE_MENUBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]);
-static ROOT_PLATE_MENUBAR_BLUR: RwLock<bool> = RwLock::new(false);
+static ROOT_PLATE_MENUBAR_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_MENUBAR_COLOR, |s| &mut s.color.ROOT_PLATE_MENUBAR_COLOR);
+static ROOT_PLATE_MENUBAR_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_MENUBAR_TEXT_COLOR, |s| &mut s.color.ROOT_PLATE_MENUBAR_TEXT_COLOR);
+static ROOT_PLATE_MENUBAR_BLUR: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_MENUBAR_BLUR, |s| &mut s.color.ROOT_PLATE_MENUBAR_BLUR);
/// Tint strength of frosted menus/popovers over the blurred backdrop:
/// 1.0 is fully opaque (frost invisible), lower shows more content through.
-static MENU_OPACITY: RwLock<f32> = RwLock::new(0.8);
+static MENU_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.MENU_OPACITY, |s| &mut s.color.MENU_OPACITY);
/// Backdrop compression of frosted menus/popovers (`style.surface.menu.
/// compression`, 0..1): how hard the blurred content beneath a menu is
/// pulled toward the menu's own key, so the menu holds its legibility over
/// whatever it opens above. Menu-scoped, overriding the DE recipe's
/// `plate.backdrop_compression` for popovers only; a menu is read while
/// something else is going on beneath it, which a pane is not.
-static MENU_COMPRESSION: RwLock<f32> = RwLock::new(0.6);
+static MENU_COMPRESSION: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.MENU_COMPRESSION, |s| &mut s.color.MENU_COMPRESSION);
/// The colour a menu's face is tinted with (`style.surface.menu.color`),
/// when the config names one; `None` follows the root plate colour
/// (`page_low_color`), which is what every menu wore before the key
/// existed. The alpha is ignored — `menu.opacity` is the tint strength —
/// so a menu can be dark on a light window without the window's own
/// plate going dark with it.
-static MENU_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
-
-static ROOT_PLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.10, 1.0]);
-static ROOT_PLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
-static ROOT_PLATE_STATUSBAR_BLUR: RwLock<bool> = RwLock::new(false);
-static BUTTON_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new(BUTTON_IDLE);
-static RAMP_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
-static RAMP_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
-static CONTROL_PANEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.075, 0.082, 0.11, 1.0]); // default #13151cff
-static CONTROL_PANEL_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.161, 0.173, 0.216, 1.0]); // default #292c37ff
-
-static PROGRESS_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_BG);
-static PROGRESS_FILL_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_FILL);
-static SPINBOX_DISPLAY_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_DISPLAY);
-static SPINBOX_BUTTON_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON);
-static SPINBOX_BUTTON_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON_HOVER);
-static SPINBOX_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.8, 0.8, 0.83, 1.0]);
-
-static BUTTON_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
-static BUTTON_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
-
-static DROPDOWN_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
-static DROPDOWN_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.72305, 0.72305, 0.76008, 1.0]);
-
-static SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
-static SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
-
-static RANGE_SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
-static RANGE_SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
-
-static TREE_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
-static TREE_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
-static TREE_BORDER_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.25, 0.25, 0.35, 1.0]);
-static TREE_BORDER_FOCUS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.30, 0.50, 0.32, 1.0]);
-static TREE_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
-static LIST_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
-static LIST_CLOSE_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
-
-static TREE_SECTION_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.07, 0.07, 0.09, 1.0]);
-static TREE_SECTION_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.10, 0.12, 0.18, 1.0]);
-
-static TREE_LEAF_BG_EVEN_COLOR: RwLock<[f32; 4]> = RwLock::new([0.09, 0.09, 0.11, 1.0]);
-static TREE_LEAF_BG_ODD_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.10, 1.0]);
-static TREE_LEAF_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.12, 0.12, 0.16, 1.0]);
-static TREE_LEAF_BG_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.20, 0.30, 1.0]);
-
-static TREE_SECTION_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.38, 0.69, 0.94, 1.0]);
-static TREE_LEAF_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.83, 1.0]);
-static TREE_LEAF_TEXT_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.49, 1.0, 1.0, 1.0]);
-
-static TREE_TYPE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.78, 0.47, 0.87, 1.0]);
-static TREE_VALUE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.51, 0.51, 0.54, 1.0]);
-static TREE_SEPARATOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.19, 1.0]);
-
-static SCROLLBAR_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.20, 0.3]);
-static SCROLLBAR_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new([0.60, 0.60, 0.65, 0.4]);
-
-static GRAPH_GRID_COLOR: RwLock<[f32; 3]> = RwLock::new([0.07, 0.07, 0.09]);
-static GRAPH_OPACITY: RwLock<f32> = RwLock::new(0.95);
+static MENU_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.MENU_COLOR, |s| &mut s.color.MENU_COLOR);
+
+static ROOT_PLATE_STATUSBAR_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_STATUSBAR_COLOR, |s| &mut s.color.ROOT_PLATE_STATUSBAR_COLOR);
+static ROOT_PLATE_STATUSBAR_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_STATUSBAR_TEXT_COLOR, |s| &mut s.color.ROOT_PLATE_STATUSBAR_TEXT_COLOR);
+static ROOT_PLATE_STATUSBAR_BLUR: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.color.ROOT_PLATE_STATUSBAR_BLUR, |s| &mut s.color.ROOT_PLATE_STATUSBAR_BLUR);
+static BUTTON_BACKGROUND_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.BUTTON_BACKGROUND_COLOR, |s| &mut s.color.BUTTON_BACKGROUND_COLOR);
+static RAMP_BACKGROUND_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.RAMP_BACKGROUND_COLOR, |s| &mut s.color.RAMP_BACKGROUND_COLOR);
+static RAMP_BORDER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.RAMP_BORDER_COLOR, |s| &mut s.color.RAMP_BORDER_COLOR);
+static CONTROL_PANEL_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.CONTROL_PANEL_COLOR, |s| &mut s.color.CONTROL_PANEL_COLOR); // default #13151cff
+static CONTROL_PANEL_BORDER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.CONTROL_PANEL_BORDER_COLOR, |s| &mut s.color.CONTROL_PANEL_BORDER_COLOR); // default #292c37ff
+
+static PROGRESS_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.PROGRESS_BG_COLOR, |s| &mut s.color.PROGRESS_BG_COLOR);
+static PROGRESS_FILL_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.PROGRESS_FILL_COLOR, |s| &mut s.color.PROGRESS_FILL_COLOR);
+static SPINBOX_DISPLAY_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SPINBOX_DISPLAY_COLOR, |s| &mut s.color.SPINBOX_DISPLAY_COLOR);
+static SPINBOX_BUTTON_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SPINBOX_BUTTON_COLOR, |s| &mut s.color.SPINBOX_BUTTON_COLOR);
+static SPINBOX_BUTTON_HOVER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SPINBOX_BUTTON_HOVER_COLOR, |s| &mut s.color.SPINBOX_BUTTON_HOVER_COLOR);
+static SPINBOX_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SPINBOX_TEXT_COLOR, |s| &mut s.color.SPINBOX_TEXT_COLOR);
+
+static BUTTON_BORDER_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.BUTTON_BORDER_COLOR, |s| &mut s.color.BUTTON_BORDER_COLOR);
+
+static DROPDOWN_BORDER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.DROPDOWN_BORDER_COLOR, |s| &mut s.color.DROPDOWN_BORDER_COLOR);
+static DROPDOWN_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.DROPDOWN_TEXT_COLOR, |s| &mut s.color.DROPDOWN_TEXT_COLOR);
+
+static SLIDER_THUMB_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SLIDER_THUMB_COLOR, |s| &mut s.color.SLIDER_THUMB_COLOR);
+static SLIDER_THUMB_DRAG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SLIDER_THUMB_DRAG_COLOR, |s| &mut s.color.SLIDER_THUMB_DRAG_COLOR);
+
+static RANGE_SLIDER_THUMB_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.RANGE_SLIDER_THUMB_COLOR, |s| &mut s.color.RANGE_SLIDER_THUMB_COLOR);
+static RANGE_SLIDER_THUMB_DRAG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.RANGE_SLIDER_THUMB_DRAG_COLOR, |s| &mut s.color.RANGE_SLIDER_THUMB_DRAG_COLOR);
+
+static TREE_BACKGROUND_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_BACKGROUND_COLOR, |s| &mut s.color.TREE_BACKGROUND_COLOR);
+static TREE_BORDER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_BORDER_COLOR, |s| &mut s.color.TREE_BORDER_COLOR);
+static TREE_BORDER_HOVER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_BORDER_HOVER_COLOR, |s| &mut s.color.TREE_BORDER_HOVER_COLOR);
+static TREE_BORDER_FOCUS_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_BORDER_FOCUS_COLOR, |s| &mut s.color.TREE_BORDER_FOCUS_COLOR);
+static TREE_OPEN_SEARCH_KEY: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.color.TREE_OPEN_SEARCH_KEY, |s| &mut s.color.TREE_OPEN_SEARCH_KEY);
+static LIST_OPEN_SEARCH_KEY: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.color.LIST_OPEN_SEARCH_KEY, |s| &mut s.color.LIST_OPEN_SEARCH_KEY);
+static LIST_CLOSE_SEARCH_KEY: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.color.LIST_CLOSE_SEARCH_KEY, |s| &mut s.color.LIST_CLOSE_SEARCH_KEY);
+
+static TREE_SECTION_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_SECTION_BG_COLOR, |s| &mut s.color.TREE_SECTION_BG_COLOR);
+static TREE_SECTION_BG_HOVER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_SECTION_BG_HOVER_COLOR, |s| &mut s.color.TREE_SECTION_BG_HOVER_COLOR);
+
+static TREE_LEAF_BG_EVEN_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_BG_EVEN_COLOR, |s| &mut s.color.TREE_LEAF_BG_EVEN_COLOR);
+static TREE_LEAF_BG_ODD_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_BG_ODD_COLOR, |s| &mut s.color.TREE_LEAF_BG_ODD_COLOR);
+static TREE_LEAF_BG_HOVER_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_BG_HOVER_COLOR, |s| &mut s.color.TREE_LEAF_BG_HOVER_COLOR);
+static TREE_LEAF_BG_SELECTED_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_BG_SELECTED_COLOR, |s| &mut s.color.TREE_LEAF_BG_SELECTED_COLOR);
+
+static TREE_SECTION_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_SECTION_TEXT_COLOR, |s| &mut s.color.TREE_SECTION_TEXT_COLOR);
+static TREE_LEAF_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_TEXT_COLOR, |s| &mut s.color.TREE_LEAF_TEXT_COLOR);
+static TREE_LEAF_TEXT_SELECTED_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_LEAF_TEXT_SELECTED_COLOR, |s| &mut s.color.TREE_LEAF_TEXT_SELECTED_COLOR);
+
+static TREE_TYPE_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_TYPE_TEXT_COLOR, |s| &mut s.color.TREE_TYPE_TEXT_COLOR);
+static TREE_VALUE_TEXT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_VALUE_TEXT_COLOR, |s| &mut s.color.TREE_VALUE_TEXT_COLOR);
+static TREE_SEPARATOR_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.TREE_SEPARATOR_COLOR, |s| &mut s.color.TREE_SEPARATOR_COLOR);
+
+static SCROLLBAR_TRACK_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SCROLLBAR_TRACK_COLOR, |s| &mut s.color.SCROLLBAR_TRACK_COLOR);
+static SCROLLBAR_THUMB_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.SCROLLBAR_THUMB_COLOR, |s| &mut s.color.SCROLLBAR_THUMB_COLOR);
+
+static GRAPH_GRID_COLOR: crate::style::StyleCell<[f32; 3]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_GRID_COLOR, |s| &mut s.color.GRAPH_GRID_COLOR);
+static GRAPH_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.GRAPH_OPACITY, |s| &mut s.color.GRAPH_OPACITY);
/// Opacity of the graph's NODE-domain content (node bodies, wires, connectors,
/// node text) — `style.surface.graph.node.opacity`, deliberately independent of
/// `GRAPH_OPACITY`, which fades only the pane surface (grid cells/gaps).
-static GRAPH_NODE_OPACITY: RwLock<f32> = RwLock::new(1.0);
+static GRAPH_NODE_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.GRAPH_NODE_OPACITY, |s| &mut s.color.GRAPH_NODE_OPACITY);
-static GRAPH_NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
-static GRAPH_NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
-static GRAPH_NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
+static GRAPH_NODE_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_NODE_COLOR, |s| &mut s.color.GRAPH_NODE_COLOR);
+static GRAPH_NODE_SELECTED_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_NODE_SELECTED_COLOR, |s| &mut s.color.GRAPH_NODE_SELECTED_COLOR);
+static GRAPH_NODE_DRAG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_NODE_DRAG_COLOR, |s| &mut s.color.GRAPH_NODE_DRAG_COLOR);
-static GRAPH_WIRE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
-static GRAPH_WIRE_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
+static GRAPH_WIRE_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_WIRE_COLOR, |s| &mut s.color.GRAPH_WIRE_COLOR);
+static GRAPH_WIRE_HIGHLIGHT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_WIRE_HIGHLIGHT_COLOR, |s| &mut s.color.GRAPH_WIRE_HIGHLIGHT_COLOR);
-static GRAPH_CONNECTOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
-static GRAPH_CONNECTOR_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
+static GRAPH_CONNECTOR_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_CONNECTOR_COLOR, |s| &mut s.color.GRAPH_CONNECTOR_COLOR);
+static GRAPH_CONNECTOR_HIGHLIGHT_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.GRAPH_CONNECTOR_HIGHLIGHT_COLOR, |s| &mut s.color.GRAPH_CONNECTOR_HIGHLIGHT_COLOR);
pub fn button_background_color() -> [f32; 4] {
style_read(&BUTTON_BACKGROUND_COLOR)
@@ -449,12 +551,12 @@ pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
/// `style.surface.param.color` in config overrides it, and apps can retint at
/// runtime (the designer's Style section "Plate Color"). Alpha doubles as the
/// frost strength under plate blur.
-static PARAM_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PARAM_BG);
+static PARAM_BG_COLOR: crate::style::StyleCell<[f32; 4]> = crate::style::StyleCell::new(|s| &s.color.PARAM_BG_COLOR, |s| &mut s.color.PARAM_BG_COLOR);
/// Whether the pane tint came in as `style.surface.plate.pane.color` — the
/// spelling whose alpha IS the tint strength — rather than the legacy
/// `style.surface.param.color`, which the top-level `plate_opacity` line
/// still multiplies (`Material::pane_legacy`). Two spellings, one tint.
-static PANE_COLOR_WHOLE: RwLock<bool> = RwLock::new(false);
+static PANE_COLOR_WHOLE: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.color.PANE_COLOR_WHOLE, |s| &mut s.color.PANE_COLOR_WHOLE);
pub fn param_bg_color() -> [f32; 4] {
load_colors_once();
@@ -1034,9 +1136,9 @@ pub fn set_root_plate_statusbar_blur(b: bool) {
style_write(&ROOT_PLATE_STATUSBAR_BLUR, b);
}
-static PLATE_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.15, 0.15, 0.2, 0.95]));
-static PLATE_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.3, 0.3, 0.4, 1.0]));
-static PLATE_BORDER_THICKNESS: RwLock<f32> = RwLock::new(1.0);
+static PLATE_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.PLATE_COLOR, |s| &mut s.color.PLATE_COLOR);
+static PLATE_BORDER_COLOR: crate::style::StyleCell<Option<[f32; 4]>> = crate::style::StyleCell::new(|s| &s.color.PLATE_BORDER_COLOR, |s| &mut s.color.PLATE_BORDER_COLOR);
+static PLATE_BORDER_THICKNESS: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.PLATE_BORDER_THICKNESS, |s| &mut s.color.PLATE_BORDER_THICKNESS);
pub fn plate_color() -> Option<[f32; 4]> {
load_colors_once();
style_read(&PLATE_COLOR)
@@ -1097,7 +1199,7 @@ pub fn plate_bevel_width() -> f32 {
/// luminance while its hue, chroma and movement still read.
///
/// Default 0.0 — the behavior every existing config already has.
-static PLATE_BACKDROP_COMPRESSION: RwLock<f32> = RwLock::new(0.0);
+static PLATE_BACKDROP_COMPRESSION: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.PLATE_BACKDROP_COMPRESSION, |s| &mut s.color.PLATE_BACKDROP_COMPRESSION);
pub fn plate_backdrop_compression() -> f32 {
load_colors_once();
@@ -1120,7 +1222,7 @@ pub fn set_plate_backdrop_compression(c: f32) {
/// rim as the only place the material can still say what it is.
///
/// Default 0.0 — no existing config changes appearance.
-static PLATE_REFRACTION: RwLock<f32> = RwLock::new(0.0);
+static PLATE_REFRACTION: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.PLATE_REFRACTION, |s| &mut s.color.PLATE_REFRACTION);
pub fn plate_refraction() -> f32 {
load_colors_once();
@@ -1135,7 +1237,7 @@ pub fn set_plate_refraction(r: f32) {
/// `Frost::from_style`. Spelled `style.surface.plate.frost` (a block, the
/// knobs inside it; see the loader). The older `style.surface.plate.blur`
/// bool is retired and reported, not read.
-static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
+static PLATE_BLUR: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.color.PLATE_BLUR, |s| &mut s.color.PLATE_BLUR);
/// The finish's three fixed terms — specular strength, shininess exponent,
/// curvature/AO strength — as `scene::material::Finish::from_style` reads
@@ -1143,9 +1245,9 @@ static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
/// (0.4 / 24 / 0.2); the getters exist so the DE's plastic can be edited and
/// so a named material (RFC material, step 4) has somewhere to land. No config
/// path yet: the defaults ARE the shipped look.
-static FINISH_SPEC: RwLock<f32> = RwLock::new(FINISH_SPEC_DEFAULT);
-static FINISH_SHININESS: RwLock<f32> = RwLock::new(FINISH_SHININESS_DEFAULT);
-static FINISH_CURVATURE: RwLock<f32> = RwLock::new(FINISH_CURVATURE_DEFAULT);
+static FINISH_SPEC: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.FINISH_SPEC, |s| &mut s.color.FINISH_SPEC);
+static FINISH_SHININESS: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.FINISH_SHININESS, |s| &mut s.color.FINISH_SHININESS);
+static FINISH_CURVATURE: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.FINISH_CURVATURE, |s| &mut s.color.FINISH_CURVATURE);
/// The finish a config without `relief spec` / `shininess` / `curvature`
/// gets: the literals the shader shipped with.
pub(crate) const FINISH_SPEC_DEFAULT: f32 = 0.4;
@@ -1155,7 +1257,7 @@ pub(crate) const FINISH_CURVATURE_DEFAULT: f32 = 0.2;
/// The default material's blur radius (`style.surface.plate.frost.radius`,
/// the kernel sigma in logical px) — [`crate::scene::Frost::DEFAULT_RADIUS`]
/// unless config says otherwise.
-static PLATE_FROST_RADIUS: RwLock<f32> = RwLock::new(crate::scene::material::Frost::DEFAULT_RADIUS);
+static PLATE_FROST_RADIUS: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.color.PLATE_FROST_RADIUS, |s| &mut s.color.PLATE_FROST_RADIUS);
pub fn plate_frost_radius() -> f32 {
load_colors_once();
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 90b8e10..19efb37 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -1,4 +1,74 @@
-use std::sync::RwLock;
+
+// This module's part of the one style snapshot (`crate::style`): every slot below,
+// with its default. Each slot's `static` handle stands where its lock did.
+crate::style::style_slots! {
+ SECTION_PADDING: f32 = 8.0;
+ SPINBOX_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ SPINBOX_BUTTON_PADDING: f32 = 0.0;
+ COLOR_SELECTOR_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ TEXTBOX_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ FONT_SELECTOR_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ SLIDER_HEIGHT: f32 = DEFAULT_TRACK_HEIGHT;
+ PROGRESSBAR_HEIGHT: f32 = DEFAULT_TRACK_HEIGHT;
+ RANGESLIDER_HEIGHT: f32 = DEFAULT_TRACK_HEIGHT;
+ TOGGLE_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ COLOR_SELECTOR_FONT: String = String::new();
+ COLOR_SELECTOR_PREVIEW_MARGIN: f32 = 0.0;
+ MENUBAR_FONT: String = String::new();
+ MENUBAR_FONT_CACHED: Option<(String, f32)> = None;
+ STATUSBAR_FONT: String = String::new();
+ STATUSBAR_FONT_CACHED: Option<(String, f32)> = None;
+ SECTION_LABEL_FONT: String = String::new();
+ NESTED_SECTION_LABEL_FONT: String = String::new();
+ BREADCRUMB_FONT: String = String::new();
+ BUTTON_FONT: String = String::new();
+ PAGINATOR_TAB_PADDING_X: f32 = 10.0;
+ BUTTON_PADDING: f32 = 14.0;
+ BUTTON_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ RAMP_HEIGHT: f32 = 32.0;
+ BUTTON_STRIP_SPACING: f32 = 8.0;
+ SCROLLBAR_WIDTH: f32 = 4.0;
+ SCROLLBAR_INSET: f32 = 16.0;
+ COLUMN_GAP: Option<f32> = None;
+ CONTROL_PANEL_PADDING: Option<f32> = None;
+ CONTROL_PANEL_GAP: Option<f32> = None;
+ TREE_OPACITY: f32 = 1.0;
+ TREE_BLUR: f32 = 0.0;
+ PLATE_PADDING: f32 = 20.0;
+ DROPDOWN_HEIGHT: f32 = DEFAULT_CONTROL_HEIGHT;
+ NESTED_SECTION_LABEL_ALIGNMENT: u8 = 0;
+ TOUCHPAD_NATURAL_SCROLL: bool = false;
+ TOGGLE_BORDER_WIDTH: f32 = 1.0;
+ FONT_SELECTOR_FONT: String = String::new();
+ FONT_SELECTOR_FONT_CACHED: Option<(String, f32)> = None;
+ BUTTON_STRIP_FONT: String = String::new();
+ BUTTON_STRIP_FONT_CACHED: Option<(String, f32)> = None;
+ CONTROL_LABEL_FONT: String = String::new();
+ CONTROL_LABEL_FONT_CACHED: Option<(String, f32)> = None;
+ CONTROL_LABEL_FONT_DETACHED: String = String::new();
+ CONTROL_LABEL_FONT_DETACHED_CACHED: Option<(String, f32)> = None;
+ CONTROL_LABEL_MARGIN: f32 = 6.0;
+ LIST_FONT: String = String::new();
+ LIST_FONT_CACHED: Option<(String, f32)> = None;
+ TREE_FONT: String = String::new();
+ TREE_FONT_CACHED: Option<(String, f32)> = None;
+ GRAPH_FONT: String = String::new();
+ GRAPH_FONT_CACHED: Option<(String, f32)> = None;
+ GRAPH_NODE_FONT: String = String::new();
+ GRAPH_NODE_FONT_CACHED: Option<(String, f32)> = None;
+ LIST_JUSTIFICATION: u8 = 0;
+ PLATE_OPACITY: f32 = 1.0;
+ PAGE_OPACITY: f32 = 1.0;
+ LAYER_OPACITY: f32 = 1.0;
+ TEXTBOX_LINE_WRAP: bool = true;
+ TEXTBOX_MULTILINE_BORDER_WIDTH: f32 = 1.0;
+ NESTED_SECTION_LABEL_OFFSET: f32 = 0.0;
+ PAGE_MARGIN: Option<f32> = None;
+ GRID_MIN_COL_WIDTH: f32 = 260.0;
+ GRID_GAP: f32 = 8.0;
+ BEVEL_PROFILE: Option<[f32; BEVEL_PROFILE_SAMPLES]> = None;
+ ROLL_PROFILE: Option<[f32; BEVEL_PROFILE_SAMPLES]> = None;
+}
mod registry;
pub use registry::*;
@@ -7,7 +77,7 @@ pub use bridge::*;
mod legacy;
pub use legacy::*;
-static SECTION_PADDING: RwLock<f32> = RwLock::new(8.0);
+static SECTION_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SECTION_PADDING, |s| &mut s.layout.SECTION_PADDING);
/// Per-thread overrides for the style values tests pin, active only under
/// `cfg(test)`.
@@ -83,82 +153,69 @@ pub const DETACHED_LABEL_INSET: f32 = 4.0;
/// usage bar.
pub const DEFAULT_TRACK_HEIGHT: f32 = 16.0;
-static SPINBOX_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static SPINBOX_BUTTON_PADDING: RwLock<f32> = RwLock::new(0.0);
-static COLOR_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static TEXTBOX_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static FONT_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static SLIDER_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
-static PROGRESSBAR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
-static RANGESLIDER_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
-static TOGGLE_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static COLOR_SELECTOR_FONT: RwLock<String> = RwLock::new(String::new());
-static COLOR_SELECTOR_PREVIEW_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static COLOR_SELECTOR_PREVIEW_MARGIN: RwLock<f32> = RwLock::new(0.0);
-static COLOR_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static MENUBAR_FONT: RwLock<String> = RwLock::new(String::new());
-static MENUBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static STATUSBAR_FONT: RwLock<String> = RwLock::new(String::new());
-static STATUSBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
-static NESTED_SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
-static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
-static BUTTON_FONT: RwLock<String> = RwLock::new(String::new());
-
-static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
-static BUTTON_PADDING: RwLock<f32> = RwLock::new(14.0);
-static BUTTON_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static RAMP_HEIGHT: RwLock<f32> = RwLock::new(32.0);
-static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
-static SCROLLBAR_WIDTH: RwLock<f32> = RwLock::new(4.0);
-static SCROLLBAR_INSET: RwLock<f32> = RwLock::new(16.0);
-static COLUMN_GAP: RwLock<Option<f32>> = RwLock::new(None);
-static CONTROL_PANEL_PADDING: RwLock<Option<f32>> = RwLock::new(None);
-static CONTROL_PANEL_GAP: RwLock<Option<f32>> = RwLock::new(None);
-static TREE_OPACITY: RwLock<f32> = RwLock::new(1.0);
-static TREE_BLUR: RwLock<f32> = RwLock::new(0.0);
-
-static PLATE_PADDING: RwLock<f32> = RwLock::new(20.0);
-static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
-static NESTED_SECTION_LABEL_ALIGNMENT: RwLock<u8> = RwLock::new(0);
-static TOUCHPAD_NATURAL_SCROLL: RwLock<bool> = RwLock::new(false);
-
-
-static BUTTON_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static SPINBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static TEXTBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static FONT_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static DROPDOWN_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static TOGGLE_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static SLIDER_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static RANGESLIDER_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static LIST_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static TREE_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
-static TOGGLE_BORDER_WIDTH: RwLock<f32> = RwLock::new(1.0);
-static FONT_SELECTOR_FONT: RwLock<String> = RwLock::new(String::new());
-static FONT_SELECTOR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static BUTTON_STRIP_FONT: RwLock<String> = RwLock::new(String::new());
-static BUTTON_STRIP_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static CONTROL_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
-static CONTROL_LABEL_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static CONTROL_LABEL_FONT_DETACHED: RwLock<String> = RwLock::new(String::new());
-static CONTROL_LABEL_FONT_DETACHED_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static CONTROL_LABEL_MARGIN: RwLock<f32> = RwLock::new(6.0);
-static PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
-static LIST_FONT: RwLock<String> = RwLock::new(String::new());
-static LIST_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static TREE_FONT: RwLock<String> = RwLock::new(String::new());
-static TREE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static GRAPH_FONT: RwLock<String> = RwLock::new(String::new());
-static GRAPH_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static GRAPH_NODE_FONT: RwLock<String> = RwLock::new(String::new());
-static GRAPH_NODE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
-static LIST_JUSTIFICATION: RwLock<u8> = RwLock::new(0);
-static PLATE_OPACITY: RwLock<f32> = RwLock::new(1.0);
-static PAGE_OPACITY: RwLock<f32> = RwLock::new(1.0);
-static LAYER_OPACITY: RwLock<f32> = RwLock::new(1.0);
-static TEXTBOX_LINE_WRAP: RwLock<bool> = RwLock::new(true);
-static TEXTBOX_MULTILINE_BORDER_WIDTH: RwLock<f32> = RwLock::new(1.0);
+static SPINBOX_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SPINBOX_HEIGHT, |s| &mut s.layout.SPINBOX_HEIGHT);
+static SPINBOX_BUTTON_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SPINBOX_BUTTON_PADDING, |s| &mut s.layout.SPINBOX_BUTTON_PADDING);
+static COLOR_SELECTOR_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.COLOR_SELECTOR_HEIGHT, |s| &mut s.layout.COLOR_SELECTOR_HEIGHT);
+static TEXTBOX_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TEXTBOX_HEIGHT, |s| &mut s.layout.TEXTBOX_HEIGHT);
+static FONT_SELECTOR_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.FONT_SELECTOR_HEIGHT, |s| &mut s.layout.FONT_SELECTOR_HEIGHT);
+static SLIDER_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SLIDER_HEIGHT, |s| &mut s.layout.SLIDER_HEIGHT);
+static PROGRESSBAR_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.PROGRESSBAR_HEIGHT, |s| &mut s.layout.PROGRESSBAR_HEIGHT);
+static RANGESLIDER_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.RANGESLIDER_HEIGHT, |s| &mut s.layout.RANGESLIDER_HEIGHT);
+static TOGGLE_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TOGGLE_HEIGHT, |s| &mut s.layout.TOGGLE_HEIGHT);
+static COLOR_SELECTOR_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.COLOR_SELECTOR_FONT, |s| &mut s.layout.COLOR_SELECTOR_FONT);
+static COLOR_SELECTOR_PREVIEW_MARGIN: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.COLOR_SELECTOR_PREVIEW_MARGIN, |s| &mut s.layout.COLOR_SELECTOR_PREVIEW_MARGIN);
+static MENUBAR_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.MENUBAR_FONT, |s| &mut s.layout.MENUBAR_FONT);
+static MENUBAR_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.MENUBAR_FONT_CACHED, |s| &mut s.layout.MENUBAR_FONT_CACHED);
+static STATUSBAR_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.STATUSBAR_FONT, |s| &mut s.layout.STATUSBAR_FONT);
+static STATUSBAR_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.STATUSBAR_FONT_CACHED, |s| &mut s.layout.STATUSBAR_FONT_CACHED);
+static SECTION_LABEL_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.SECTION_LABEL_FONT, |s| &mut s.layout.SECTION_LABEL_FONT);
+static NESTED_SECTION_LABEL_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.NESTED_SECTION_LABEL_FONT, |s| &mut s.layout.NESTED_SECTION_LABEL_FONT);
+static BREADCRUMB_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.BREADCRUMB_FONT, |s| &mut s.layout.BREADCRUMB_FONT);
+static BUTTON_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_FONT, |s| &mut s.layout.BUTTON_FONT);
+
+static PAGINATOR_TAB_PADDING_X: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.PAGINATOR_TAB_PADDING_X, |s| &mut s.layout.PAGINATOR_TAB_PADDING_X);
+static BUTTON_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_PADDING, |s| &mut s.layout.BUTTON_PADDING);
+static BUTTON_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_HEIGHT, |s| &mut s.layout.BUTTON_HEIGHT);
+static RAMP_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.RAMP_HEIGHT, |s| &mut s.layout.RAMP_HEIGHT);
+static BUTTON_STRIP_SPACING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_STRIP_SPACING, |s| &mut s.layout.BUTTON_STRIP_SPACING);
+static SCROLLBAR_WIDTH: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SCROLLBAR_WIDTH, |s| &mut s.layout.SCROLLBAR_WIDTH);
+static SCROLLBAR_INSET: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.SCROLLBAR_INSET, |s| &mut s.layout.SCROLLBAR_INSET);
+static COLUMN_GAP: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.layout.COLUMN_GAP, |s| &mut s.layout.COLUMN_GAP);
+static CONTROL_PANEL_PADDING: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_PANEL_PADDING, |s| &mut s.layout.CONTROL_PANEL_PADDING);
+static CONTROL_PANEL_GAP: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_PANEL_GAP, |s| &mut s.layout.CONTROL_PANEL_GAP);
+static TREE_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TREE_OPACITY, |s| &mut s.layout.TREE_OPACITY);
+static TREE_BLUR: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TREE_BLUR, |s| &mut s.layout.TREE_BLUR);
+
+static PLATE_PADDING: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.PLATE_PADDING, |s| &mut s.layout.PLATE_PADDING);
+static DROPDOWN_HEIGHT: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.DROPDOWN_HEIGHT, |s| &mut s.layout.DROPDOWN_HEIGHT);
+static NESTED_SECTION_LABEL_ALIGNMENT: crate::style::StyleCell<u8> = crate::style::StyleCell::new(|s| &s.layout.NESTED_SECTION_LABEL_ALIGNMENT, |s| &mut s.layout.NESTED_SECTION_LABEL_ALIGNMENT);
+static TOUCHPAD_NATURAL_SCROLL: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.layout.TOUCHPAD_NATURAL_SCROLL, |s| &mut s.layout.TOUCHPAD_NATURAL_SCROLL);
+
+
+static TOGGLE_BORDER_WIDTH: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TOGGLE_BORDER_WIDTH, |s| &mut s.layout.TOGGLE_BORDER_WIDTH);
+static FONT_SELECTOR_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.FONT_SELECTOR_FONT, |s| &mut s.layout.FONT_SELECTOR_FONT);
+static FONT_SELECTOR_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.FONT_SELECTOR_FONT_CACHED, |s| &mut s.layout.FONT_SELECTOR_FONT_CACHED);
+static BUTTON_STRIP_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_STRIP_FONT, |s| &mut s.layout.BUTTON_STRIP_FONT);
+static BUTTON_STRIP_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.BUTTON_STRIP_FONT_CACHED, |s| &mut s.layout.BUTTON_STRIP_FONT_CACHED);
+static CONTROL_LABEL_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_LABEL_FONT, |s| &mut s.layout.CONTROL_LABEL_FONT);
+static CONTROL_LABEL_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_LABEL_FONT_CACHED, |s| &mut s.layout.CONTROL_LABEL_FONT_CACHED);
+static CONTROL_LABEL_FONT_DETACHED: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_LABEL_FONT_DETACHED, |s| &mut s.layout.CONTROL_LABEL_FONT_DETACHED);
+static CONTROL_LABEL_FONT_DETACHED_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_LABEL_FONT_DETACHED_CACHED, |s| &mut s.layout.CONTROL_LABEL_FONT_DETACHED_CACHED);
+static CONTROL_LABEL_MARGIN: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.CONTROL_LABEL_MARGIN, |s| &mut s.layout.CONTROL_LABEL_MARGIN);
+static LIST_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.LIST_FONT, |s| &mut s.layout.LIST_FONT);
+static LIST_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.LIST_FONT_CACHED, |s| &mut s.layout.LIST_FONT_CACHED);
+static TREE_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.TREE_FONT, |s| &mut s.layout.TREE_FONT);
+static TREE_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.TREE_FONT_CACHED, |s| &mut s.layout.TREE_FONT_CACHED);
+static GRAPH_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.GRAPH_FONT, |s| &mut s.layout.GRAPH_FONT);
+static GRAPH_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.GRAPH_FONT_CACHED, |s| &mut s.layout.GRAPH_FONT_CACHED);
+static GRAPH_NODE_FONT: crate::style::StyleCell<String> = crate::style::StyleCell::new(|s| &s.layout.GRAPH_NODE_FONT, |s| &mut s.layout.GRAPH_NODE_FONT);
+static GRAPH_NODE_FONT_CACHED: crate::style::StyleCell<Option<(String, f32)>> = crate::style::StyleCell::new(|s| &s.layout.GRAPH_NODE_FONT_CACHED, |s| &mut s.layout.GRAPH_NODE_FONT_CACHED);
+static LIST_JUSTIFICATION: crate::style::StyleCell<u8> = crate::style::StyleCell::new(|s| &s.layout.LIST_JUSTIFICATION, |s| &mut s.layout.LIST_JUSTIFICATION);
+static PLATE_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.PLATE_OPACITY, |s| &mut s.layout.PLATE_OPACITY);
+static PAGE_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.PAGE_OPACITY, |s| &mut s.layout.PAGE_OPACITY);
+static LAYER_OPACITY: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.LAYER_OPACITY, |s| &mut s.layout.LAYER_OPACITY);
+static TEXTBOX_LINE_WRAP: crate::style::StyleCell<bool> = crate::style::StyleCell::new(|s| &s.layout.TEXTBOX_LINE_WRAP, |s| &mut s.layout.TEXTBOX_LINE_WRAP);
+static TEXTBOX_MULTILINE_BORDER_WIDTH: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.TEXTBOX_MULTILINE_BORDER_WIDTH, |s| &mut s.layout.TEXTBOX_MULTILINE_BORDER_WIDTH);
@@ -182,7 +239,13 @@ pub fn align_text_y(y: f32, height: f32, font_size: f32, top_offset: f32) -> f32
y + top_offset + (height - top_offset - font_size) / 2.0
}
+/// Load the style config as one change of style (`crate::style::batch`): the registry and
+/// every slot are published together, and the hundreds of per-key writes cost one copy.
pub fn reload_config() {
+ crate::style::batch(reload_config_in_batch);
+}
+
+fn reload_config_in_batch() {
if let Some(content) = read_config() {
let mut menubar_font_changed = false;
let mut statusbar_font_changed = false;
@@ -365,24 +428,6 @@ pub fn reload_config() {
}
}
}
- if let Some(rest) = trimmed.strip_prefix("spinbox_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = SPINBOX_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("textbox_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = TEXTBOX_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
if let Some(rest) = trimmed.strip_prefix("textbox_line_wrap") {
let rest = rest.trim_start_matches([' ', '=', '"']);
let val_str = rest.trim_end_matches('"').trim();
@@ -408,60 +453,6 @@ pub fn reload_config() {
}
}
}
- if let Some(rest) = trimmed.strip_prefix("list_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = LIST_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("tree_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = TREE_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("font_selector_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = FONT_SELECTOR_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("toggle_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = TOGGLE_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("plate_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = PLATE_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
if let Some(rest) = trimmed.strip_prefix("plate_opacity") {
let rest = rest.trim_start_matches([' ', '=', '"']);
let val_str = rest.trim_end_matches('"').trim();
@@ -604,24 +595,6 @@ pub fn reload_config() {
*lock = font;
}
}
- if let Some(rest) = trimmed.strip_prefix("color_selector_preview_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("color_selector_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = COLOR_SELECTOR_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
if let Some(rest) = trimmed.strip_prefix("color_selector_preview_margin") {
let rest = rest.trim_start_matches([' ', '=', '"']);
let val_str = rest.trim_end_matches('"').trim();
@@ -711,33 +684,6 @@ pub fn reload_config() {
}
}
}
- if let Some(rest) = trimmed.strip_prefix("button_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = BUTTON_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("slider_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = SLIDER_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
- if let Some(rest) = trimmed.strip_prefix("rangeslider_corner_radius") {
- let rest = rest.trim_start_matches([' ', '=', '"']);
- let val_str = rest.trim_end_matches('"').trim();
- if let Ok(val) = val_str.parse::<f32>() {
- if let Ok(mut lock) = RANGESLIDER_CORNER_RADIUS.write() {
- *lock = val;
- }
- }
- }
if let Some(rest) = trimmed.strip_prefix("toggle_border_width") {
let rest = rest.trim_start_matches([' ', '=', '"']);
let val_str = rest.trim_end_matches('"').trim();
@@ -1032,7 +978,7 @@ pub fn set_nested_section_label_alignment(align: u8) {
}
}
-static NESTED_SECTION_LABEL_OFFSET: RwLock<f32> = RwLock::new(0.0);
+static NESTED_SECTION_LABEL_OFFSET: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.NESTED_SECTION_LABEL_OFFSET, |s| &mut s.layout.NESTED_SECTION_LABEL_OFFSET);
pub fn nested_section_label_offset() -> f32 {
use std::sync::Once;
@@ -1079,7 +1025,7 @@ pub fn set_plate_padding(padding: f32) {
}
}
-static PAGE_MARGIN: RwLock<Option<f32>> = RwLock::new(None);
+static PAGE_MARGIN: crate::style::StyleCell<Option<f32>> = crate::style::StyleCell::new(|s| &s.layout.PAGE_MARGIN, |s| &mut s.layout.PAGE_MARGIN);
/// Legacy: the page-level margin (`style.surface.page.margin`). Unset, it
/// IS the pane rung's [`plate_padding`] — a page is a pane — so an app
@@ -1096,7 +1042,7 @@ pub fn set_page_margin(margin: f32) {
}
}
-static GRID_MIN_COL_WIDTH: RwLock<f32> = RwLock::new(260.0);
+static GRID_MIN_COL_WIDTH: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.GRID_MIN_COL_WIDTH, |s| &mut s.layout.GRID_MIN_COL_WIDTH);
pub fn grid_min_col_width() -> f32 {
use std::sync::Once;
@@ -1126,7 +1072,7 @@ pub fn set_grid_min_col_width(width: f32) {
}
}
-static GRID_GAP: RwLock<f32> = RwLock::new(8.0);
+static GRID_GAP: crate::style::StyleCell<f32> = crate::style::StyleCell::new(|s| &s.layout.GRID_GAP, |s| &mut s.layout.GRID_GAP);
pub fn grid_gap() -> f32 {
use std::sync::Once;
@@ -1495,8 +1441,7 @@ pub const BEVEL_PROFILE_SAMPLES: usize = 32;
/// floor / boss crest; `h` in units of the feature's depth, so a 0→1 curve is
/// the classic full-depth bevel and a curve ending back at its start height is
/// a pure decorative rim). `None` = the analytic smoothstep profile.
-static BEVEL_PROFILE: std::sync::RwLock<Option<[f32; BEVEL_PROFILE_SAMPLES]>> =
- std::sync::RwLock::new(None);
+static BEVEL_PROFILE: crate::style::StyleCell<Option<[f32; BEVEL_PROFILE_SAMPLES]>> = crate::style::StyleCell::new(|s| &s.layout.BEVEL_PROFILE, |s| &mut s.layout.BEVEL_PROFILE);
/// Bumped on every profile change so renderers know to re-upload their LUT.
static BEVEL_PROFILE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
@@ -1506,8 +1451,7 @@ static BEVEL_PROFILE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::Atom
/// silhouette, and the curve is the roll's descent progress (0 = face height,
/// 1 = fully dropped), so the identity curve is a straight chamfer and `None`
/// is the analytic superellipse quadrant.
-static ROLL_PROFILE: std::sync::RwLock<Option<[f32; BEVEL_PROFILE_SAMPLES]>> =
- std::sync::RwLock::new(None);
+static ROLL_PROFILE: crate::style::StyleCell<Option<[f32; BEVEL_PROFILE_SAMPLES]>> = crate::style::StyleCell::new(|s| &s.layout.ROLL_PROFILE, |s| &mut s.layout.ROLL_PROFILE);
static ROLL_PROFILE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub use cce_core::ramp::sample_ramp_keys;
diff --git a/src/layout/registry.rs b/src/layout/registry.rs
index f067856..8eb781a 100644
--- a/src/layout/registry.rs
+++ b/src/layout/registry.rs
@@ -3,9 +3,7 @@
//! getter in `layout` and `color` reads through it.
use super::*;
-use std::sync::RwLock;
use std::collections::HashMap;
-use std::sync::OnceLock;
/// Per-thread overrides for runtime style writes, under `cfg(test)` only.
///
@@ -58,7 +56,7 @@ mod test_overlay {
}
}
-#[derive(Debug)]
+#[derive(Debug, Clone, Default)]
pub struct StyleRegistry {
pub floats: HashMap<String, f32>,
pub strings: HashMap<String, String>,
@@ -161,10 +159,13 @@ impl StyleRegistry {
}
}
-pub static STYLE_REGISTRY: OnceLock<RwLock<StyleRegistry>> = OnceLock::new();
+/// The registry is a field of the one style snapshot (`crate::style`); this is its handle,
+/// with the `RwLock` API it had.
+pub static STYLE_REGISTRY: crate::style::StyleCell<StyleRegistry> =
+ crate::style::StyleCell::new(|s| &s.registry, |s| &mut s.registry);
-pub fn get_style_registry() -> &'static RwLock<StyleRegistry> {
- STYLE_REGISTRY.get_or_init(|| RwLock::new(StyleRegistry::new()))
+pub fn get_style_registry() -> &'static crate::style::StyleCell<StyleRegistry> {
+ &STYLE_REGISTRY
}
pub fn lazy_init_style_registry() {
diff --git a/src/lib.rs b/src/lib.rs
index 9cf0826..21ba405 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -37,6 +37,7 @@ pub use cce_core::motion;
pub use cce_core::locale;
pub mod l10n;
pub mod window_state;
+pub mod style;
pub mod text_input;
#[cfg(not(target_arch = "wasm32"))]
pub mod vk;
diff --git a/src/style.rs b/src/style.rs
new file mode 100644
index 0000000..5d21d83
--- /dev/null
+++ b/src/style.rs
@@ -0,0 +1,252 @@
+//! The style, as ONE snapshot (`docs/rfc-global-state.md`, phase 3).
+//!
+//! Every configured colour, radius, font and size, and the registry of flattened config keys,
+//! is a field of one [`Style`], published whole as an `Arc` and swapped on change. Until
+//! 2026-10-08 each was a `static RwLock` of its own — about 170 of them beside the registry's
+//! lock — so a reload wrote them one by one and a frame drawn meanwhile could see half of it,
+//! and every read took a lock.
+//!
+//! What a slot is now: a typed handle, [`StyleCell`], on one field of the snapshot. It keeps
+//! the `RwLock` API its callers were written against — `.read()` and `.write()`, each
+//! returning a `Result` whose `Ok` is a guard — so they did not change:
+//!
+//! - **`.read()`** derefs into the current snapshot. Taking it costs no lock: each thread
+//! keeps the last snapshot it saw and checks a generation counter.
+//! - **`.write()`** gives a copy of the field to change; when the guard drops, a new snapshot
+//! with that field changed is published. Guards never wait on each other, so writes may
+//! nest as freely as before.
+//! - **[`batch`]** runs a reload as one change: inside it, on its thread, writes go to one
+//! pending snapshot and reads see it; the snapshot is published once when the batch ends.
+//! A reload is atomic now.
+//!
+//! The per-thread test overlays (`color::style_write`, the registry's) are unchanged: they
+//! key on a cell's address, which a `StyleCell` has as a `static` as the lock did.
+
+use std::cell::{Cell, RefCell};
+use std::sync::atomic::{AtomicU64, Ordering};
+use std::sync::{Arc, RwLock};
+
+/// The whole style: the colour slots, the layout slots and the registry.
+#[derive(Clone, Default)]
+pub struct Style {
+ pub(crate) color: crate::color::Slots,
+ pub(crate) layout: crate::layout::Slots,
+ pub(crate) registry: crate::layout::StyleRegistry,
+}
+
+static GLOBAL: RwLock<Option<Arc<Style>>> = RwLock::new(None);
+static GENERATION: AtomicU64 = AtomicU64::new(1);
+
+thread_local! {
+ /// The snapshot this thread read last, and the generation it was.
+ static CACHE: RefCell<(u64, Option<Arc<Style>>)> = const { RefCell::new((0, None)) };
+ /// A batch in progress on this thread: its pending snapshot and its depth.
+ static PENDING: Cell<*mut Style> = const { Cell::new(std::ptr::null_mut()) };
+ static DEPTH: Cell<u32> = const { Cell::new(0) };
+}
+
+/// The current snapshot.
+pub fn current() -> Arc<Style> {
+ let generation = GENERATION.load(Ordering::Acquire);
+ CACHE.with(|c| {
+ let mut c = c.borrow_mut();
+ if c.0 == generation {
+ if let Some(s) = &c.1 {
+ return Arc::clone(s);
+ }
+ }
+ let s = {
+ let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
+ Arc::clone(g.get_or_insert_with(|| Arc::new(Style::default())))
+ };
+ *c = (generation, Some(Arc::clone(&s)));
+ s
+ })
+}
+
+fn publish(style: Style) {
+ let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
+ *g = Some(Arc::new(style));
+ GENERATION.fetch_add(1, Ordering::AcqRel);
+}
+
+/// Apply `change` to a copy of the current snapshot and publish it.
+fn change(change: impl FnOnce(&mut Style)) {
+ let mut g = GLOBAL.write().unwrap_or_else(|e| e.into_inner());
+ let mut style = g.as_deref().cloned().unwrap_or_default();
+ change(&mut style);
+ *g = Some(Arc::new(style));
+ GENERATION.fetch_add(1, Ordering::AcqRel);
+}
+
+fn pending() -> Option<*mut Style> {
+ let p = PENDING.with(Cell::get);
+ (!p.is_null()).then_some(p)
+}
+
+/// Run `f` as one change of style (see the module docs): its writes are published together
+/// when it returns, and its reads on this thread see them as they go. Batches nest; the
+/// outermost publishes.
+pub fn batch<R>(f: impl FnOnce() -> R) -> R {
+ if DEPTH.with(Cell::get) > 0 {
+ DEPTH.with(|d| d.set(d.get() + 1));
+ let r = f();
+ DEPTH.with(|d| d.set(d.get() - 1));
+ return r;
+ }
+ let boxed = Box::into_raw(Box::new((*current()).clone()));
+ PENDING.with(|p| p.set(boxed));
+ DEPTH.with(|d| d.set(1));
+ // Publish and clear even if `f` unwinds: a half-applied reload is still the newest.
+ struct End(*mut Style);
+ impl Drop for End {
+ fn drop(&mut self) {
+ PENDING.with(|p| p.set(std::ptr::null_mut()));
+ DEPTH.with(|d| d.set(0));
+ // SAFETY: the box made above, released exactly once, here.
+ let style = unsafe { Box::from_raw(self.0) };
+ publish(*style);
+ }
+ }
+ let _end = End(boxed);
+ f()
+}
+
+/// A handle on one field of the [`Style`]: what a style slot's `static` is.
+pub struct StyleCell<T: 'static> {
+ get: fn(&Style) -> &T,
+ get_mut: fn(&mut Style) -> &mut T,
+}
+
+/// A style slot cannot fail to read or write; the `Result` is the `RwLock` API's shape.
+#[derive(Debug)]
+pub struct StyleError;
+
+impl<T: Clone + 'static> StyleCell<T> {
+ pub const fn new(get: fn(&Style) -> &T, get_mut: fn(&mut Style) -> &mut T) -> StyleCell<T> {
+ StyleCell { get, get_mut }
+ }
+
+ /// The field in the current snapshot (or this thread's pending one, in a batch).
+ pub fn read(&self) -> Result<StyleRef<T>, StyleError> {
+ match pending() {
+ // SAFETY: the pending snapshot lives until its batch ends, and a guard is a
+ // temporary of code running inside the batch; read and write guards on one
+ // slot never overlap there, as they could not under the lock this replaced.
+ Some(p) => Ok(StyleRef::Pending((self.get)(unsafe { &*p }) as *const T)),
+ None => Ok(StyleRef::Snapshot(current(), self.get)),
+ }
+ }
+
+ /// The field to change: published when the guard drops (in a batch, changed in place).
+ pub fn write(&self) -> Result<StyleMut<T>, StyleError> {
+ match pending() {
+ // SAFETY: as in `read`.
+ Some(p) => Ok(StyleMut::Pending((self.get_mut)(unsafe { &mut *p }) as *mut T)),
+ None => Ok(StyleMut::Copy(Some((self.get)(¤t()).clone()), self.get_mut)),
+ }
+ }
+
+ /// The field's value now.
+ pub fn get(&self) -> T {
+ self.read().map(|v| v.clone()).unwrap_or_else(|_| unreachable!())
+ }
+}
+
+/// A read of one style field.
+pub enum StyleRef<T: 'static> {
+ Snapshot(Arc<Style>, fn(&Style) -> &T),
+ Pending(*const T),
+}
+
+impl<T> std::ops::Deref for StyleRef<T> {
+ type Target = T;
+ fn deref(&self) -> &T {
+ match self {
+ StyleRef::Snapshot(s, get) => get(s),
+ // SAFETY: see `StyleCell::read`.
+ StyleRef::Pending(p) => unsafe { &**p },
+ }
+ }
+}
+
+/// A write of one style field.
+pub enum StyleMut<T: 'static> {
+ /// Outside a batch: a copy, published into a new snapshot on drop.
+ Copy(Option<T>, fn(&mut Style) -> &mut T),
+ /// In a batch: the pending snapshot's field.
+ Pending(*mut T),
+}
+
+impl<T> std::ops::Deref for StyleMut<T> {
+ type Target = T;
+ fn deref(&self) -> &T {
+ match self {
+ StyleMut::Copy(v, _) => v.as_ref().expect("taken only on drop"),
+ // SAFETY: see `StyleCell::read`.
+ StyleMut::Pending(p) => unsafe { &**p },
+ }
+ }
+}
+
+impl<T> std::ops::DerefMut for StyleMut<T> {
+ fn deref_mut(&mut self) -> &mut T {
+ match self {
+ StyleMut::Copy(v, _) => v.as_mut().expect("taken only on drop"),
+ // SAFETY: see `StyleCell::read`.
+ StyleMut::Pending(p) => unsafe { &mut **p },
+ }
+ }
+}
+
+impl<T: 'static> Drop for StyleMut<T> {
+ fn drop(&mut self) {
+ if let StyleMut::Copy(v, get_mut) = self {
+ if let Some(value) = v.take() {
+ let get_mut = *get_mut;
+ change(|s| *get_mut(s) = value);
+ }
+ }
+ }
+}
+
+/// Declare a module's style slots: the fields of its part of the [`Style`] and their defaults.
+/// Each has a `static` [`StyleCell`] handle of the same name beside the module's getters.
+macro_rules! style_slots {
+ ($( $name:ident : $ty:ty = $default:expr ; )*) => {
+ /// This module's style slots (see `crate::style`).
+ #[derive(Clone)]
+ #[allow(non_snake_case)]
+ pub struct Slots { $( pub(crate) $name: $ty, )* }
+ impl Default for Slots {
+ fn default() -> Self { Slots { $( $name: $default, )* } }
+ }
+ };
+}
+pub(crate) use style_slots;
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ /// A write publishes; a batch publishes once, and its own reads see its writes before.
+ #[test]
+ fn writes_publish_and_a_batch_publishes_once() {
+ static PROBE: StyleCell<crate::layout::StyleRegistry> = StyleCell::new(|s| &s.registry, |s| &mut s.registry);
+ let before = GENERATION.load(Ordering::Acquire);
+ PROBE.write().unwrap().load_float("style-test-a", 1.0);
+ assert_eq!(PROBE.read().unwrap().floats.get("style-test-a"), Some(&1.0));
+ assert!(GENERATION.load(Ordering::Acquire) > before);
+
+ batch(|| {
+ PROBE.write().unwrap().load_float("style-test-b", 2.0);
+ PROBE.write().unwrap().load_float("style-test-c", 3.0);
+ assert_eq!(PROBE.read().unwrap().floats.get("style-test-b"), Some(&2.0), "a batch reads its own writes");
+ // Another thread still sees the snapshot from before the batch.
+ let seen = std::thread::spawn(|| PROBE.read().unwrap().floats.get("style-test-b").copied()).join().unwrap();
+ assert_eq!(seen, None, "nothing published yet");
+ });
+ let r = PROBE.read().unwrap();
+ assert_eq!((r.floats.get("style-test-b"), r.floats.get("style-test-c")), (Some(&2.0), Some(&3.0)));
+ }
+}