GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/color.rs (80.6K)
1 pub const HEADER_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
2 pub const HEADER_ACCENT: [f32; 4] = [0.60, 0.40, 0.20, 1.0];
3 pub const SIDEBAR_BG: [f32; 4] = [0.10, 0.10, 0.13, 1.0];
4 pub const CONTENT_BG: [f32; 4] = [0.13, 0.13, 0.16, 0.2];
5 pub const PANEL_IDLE: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
6 pub const PANEL_DRAG: [f32; 4] = [0.24, 0.85, 0.50, 1.0];
7 pub const NODE_IDLE: [f32; 4] = [0.10, 0.45, 0.70, 1.0];
8 pub const NODE_SELECTED: [f32; 4] = [0.20, 0.65, 0.90, 1.0];
9 pub const NODE_DRAG: [f32; 4] = [0.30, 0.80, 1.00, 1.0];
10 pub const BUTTON_IDLE: [f32; 4] = [0.20, 0.40, 0.65, 0.4];
11 pub const BUTTON_HOVER: [f32; 4] = [0.30, 0.52, 0.78, 0.6];
12 pub const BUTTON_PRESS: [f32; 4] = [0.12, 0.28, 0.50, 0.8];
13 pub const STATUS_BG: [f32; 4] = [0.06, 0.06, 0.10, 1.0];
14 pub const STATUS_ACCENT: [f32; 4] = [0.20, 0.20, 0.25, 1.0];
15 pub const RESET_BTN_IDLE: [f32; 4] = [0.55, 0.20, 0.20, 0.4];
16 pub const RESET_BTN_HOVER: [f32; 4] = [0.70, 0.30, 0.30, 0.6];
17 pub const RESET_BTN_PRESS: [f32; 4] = [0.40, 0.12, 0.12, 0.8];
18 pub const TOGGLE_OFF: [f32; 4] = [0.25, 0.25, 0.30, 1.0];
19 pub const TOGGLE_ON: [f32; 4] = [0.14, 0.70, 0.38, 1.0];
20 pub const TOGGLE_HOVER: [f32; 4] = [0.30, 0.30, 0.35, 1.0];
21 pub const SLIDER_TRACK: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
22
23 use std::sync::RwLock;
24
25 /// Per-thread overrides for runtime colour writes, under `cfg(test)` only.
26 ///
27 /// Same defect as the font flake (cce-ui 1dc0ab1) and the style registry
28 /// beside it: these are process-wide `RwLock` statics, so a test that pins a
29 /// colour pins it for every test running alongside. It was live —
30 /// `test_graph_style_configuration` sets a dozen of them and restores none.
31 ///
32 /// Keyed by the address of the static itself, so a slot needs no name
33 /// repeated in two places and cannot be typo'd into a silent miss. Config
34 /// loading is unaffected: `parse_and_set_colors` writes the statics directly
35 /// and never goes through these setters, so the loaded palette stays the
36 /// shared base every test thread reads.
37 #[cfg(test)]
38 mod test_overlay {
39 use std::any::Any;
40 use std::cell::RefCell;
41 use std::collections::HashMap;
42 thread_local! {
43 static MAP: RefCell<HashMap<usize, Box<dyn Any>>> = RefCell::new(HashMap::new());
44 }
45 pub fn get<T: Clone + 'static>(key: usize) -> Option<T> {
46 MAP.with(|m| m.borrow().get(&key).and_then(|b| b.downcast_ref::<T>()).cloned())
47 }
48 pub fn set<T: 'static>(key: usize, val: T) {
49 MAP.with(|m| m.borrow_mut().insert(key, Box::new(val)));
50 }
51 }
52
53 /// Read a style static, preferring this thread's test override.
54 #[inline]
55 fn style_read<T: Clone + 'static>(cell: &'static RwLock<T>) -> T {
56 #[cfg(test)]
57 if let Some(v) = test_overlay::get::<T>(cell as *const _ as usize) {
58 return v;
59 }
60 cell.read().unwrap().clone()
61 }
62
63 /// Write a style static: per-thread under `cfg(test)`, process-wide otherwise.
64 #[inline]
65 fn style_write<T: Clone + 'static>(cell: &'static RwLock<T>, val: T) {
66 #[cfg(test)]
67 test_overlay::set(cell as *const _ as usize, val);
68 #[cfg(not(test))]
69 if let Ok(mut lock) = cell.write() {
70 *lock = val;
71 }
72 }
73
74
75 static PAGE_LOW_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0600316, 0.0600316, 0.080219, 1.0]);
76 static COLOR_BORDERS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.2039, 0.2039, 0.2530, 1.0]);
77 static NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
78 static NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
79 static NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
80 static SIDEBAR_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(SIDEBAR_BG);
81 static HIGHLIGHT_PRIMARY_COLOR: RwLock<[f32; 4]> = RwLock::new(HIGHLIGHT_PRIMARY);
82 static MENUBAR_TAB_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]); // sRGB [230, 230, 242] linear
83 static CONTROL_LABEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.61206, 0.61206, 0.68666, 1.0]); // sRGB [204, 204, 212]
84 static CONTROL_LABEL_COLOR_DETACHED: RwLock<[f32; 4]> = RwLock::new([0.22416, 0.22416, 0.2526, 1.0]); // sRGB [131, 131, 138]
85 static CONTROL_LABEL_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
86 static CONTROL_LABEL_FOCUS_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
87 static OPACITY: RwLock<Option<f32>> = RwLock::new(None);
88 static ROOT_PLATE_OPACITY: RwLock<Option<f32>> = RwLock::new(None);
89 static LIST_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
90 static LIST_ENTRY_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.04]);
91 static LIST_ENTRY_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([1.0, 1.0, 1.0, 0.8]);
92 static LIST_FONT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.85, 1.0]);
93 static BREADCRUMB_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
94 static POPOVER_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
95 static PAGE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
96 static LAYER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
97 static ROOT_PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
98
99 // Transparent by default (alpha 0): a dropdown picks up the surface it sits
100 // on, and its closed-state chrome is the flush inset trough alone — the
101 // cce-files treatment, DE-wide. A configured `dropdown color=` opts a theme
102 // back into a filled face (the paint path judges the RAW alpha).
103 static DROPDOWN_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 0.0, 0.0, 0.0]);
104
105 static TEXTBOX_PLACEHOLDER_TEXT_COLOR: RwLock<[u8; 3]> = RwLock::new([0x60, 0x60, 0x6a]);
106 static TEXTBOX_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
107 static TEXTBOX_BACKGROUND_EDIT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.10, 0.18, 1.0]);
108
109 static ROOT_PLATE_MENUBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
110 static ROOT_PLATE_MENUBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.90196, 0.90196, 0.94902, 1.0]);
111 static ROOT_PLATE_MENUBAR_BLUR: RwLock<bool> = RwLock::new(false);
112 /// Tint strength of frosted menus/popovers over the blurred backdrop:
113 /// 1.0 is fully opaque (frost invisible), lower shows more content through.
114 static MENU_OPACITY: RwLock<f32> = RwLock::new(0.8);
115 /// Backdrop compression of frosted menus/popovers (`style.surface.menu.
116 /// compression`, 0..1): how hard the blurred content beneath a menu is
117 /// pulled toward the menu's own key, so the menu holds its legibility over
118 /// whatever it opens above. Menu-scoped, overriding the DE recipe's
119 /// `plate.backdrop_compression` for popovers only; a menu is read while
120 /// something else is going on beneath it, which a pane is not.
121 static MENU_COMPRESSION: RwLock<f32> = RwLock::new(0.6);
122
123 static ROOT_PLATE_STATUSBAR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.06, 0.06, 0.10, 1.0]);
124 static ROOT_PLATE_STATUSBAR_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.6666, 0.6666, 0.7333, 1.0]);
125 static ROOT_PLATE_STATUSBAR_BLUR: RwLock<bool> = RwLock::new(false);
126 static BUTTON_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new(BUTTON_IDLE);
127 static RAMP_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 1.0]);
128 static RAMP_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
129 static CONTROL_PANEL_COLOR: RwLock<[f32; 4]> = RwLock::new([0.075, 0.082, 0.11, 1.0]); // default #13151cff
130 static CONTROL_PANEL_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.161, 0.173, 0.216, 1.0]); // default #292c37ff
131
132 static PROGRESS_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_BG);
133 static PROGRESS_FILL_COLOR: RwLock<[f32; 4]> = RwLock::new(PROGRESS_FILL);
134 static SPINBOX_DISPLAY_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_DISPLAY);
135 static SPINBOX_BUTTON_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON);
136 static SPINBOX_BUTTON_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new(SPINBOX_BUTTON_HOVER);
137 static SPINBOX_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.8, 0.8, 0.83, 1.0]);
138
139 static BUTTON_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
140 static BUTTON_HOVER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(None);
141
142 static DROPDOWN_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
143 static DROPDOWN_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.72305, 0.72305, 0.76008, 1.0]);
144
145 static SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
146 static SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
147
148 static RANGE_SLIDER_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB);
149 static RANGE_SLIDER_THUMB_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(SLIDER_THUMB_DRAG);
150
151 static TREE_BACKGROUND_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.12, 0.3]);
152 static TREE_BORDER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.18, 0.18, 0.24, 1.0]);
153 static TREE_BORDER_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.25, 0.25, 0.35, 1.0]);
154 static TREE_BORDER_FOCUS_COLOR: RwLock<[f32; 4]> = RwLock::new([0.30, 0.50, 0.32, 1.0]);
155 static TREE_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
156 static LIST_OPEN_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
157 static LIST_CLOSE_SEARCH_KEY: RwLock<String> = RwLock::new(String::new());
158
159 static TREE_SECTION_BG_COLOR: RwLock<[f32; 4]> = RwLock::new([0.07, 0.07, 0.09, 1.0]);
160 static TREE_SECTION_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.10, 0.12, 0.18, 1.0]);
161
162 static TREE_LEAF_BG_EVEN_COLOR: RwLock<[f32; 4]> = RwLock::new([0.09, 0.09, 0.11, 1.0]);
163 static TREE_LEAF_BG_ODD_COLOR: RwLock<[f32; 4]> = RwLock::new([0.08, 0.08, 0.10, 1.0]);
164 static TREE_LEAF_BG_HOVER_COLOR: RwLock<[f32; 4]> = RwLock::new([0.12, 0.12, 0.16, 1.0]);
165 static TREE_LEAF_BG_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.20, 0.30, 1.0]);
166
167 static TREE_SECTION_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.38, 0.69, 0.94, 1.0]);
168 static TREE_LEAF_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.80, 0.80, 0.83, 1.0]);
169 static TREE_LEAF_TEXT_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new([0.49, 1.0, 1.0, 1.0]);
170
171 static TREE_TYPE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.78, 0.47, 0.87, 1.0]);
172 static TREE_VALUE_TEXT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.51, 0.51, 0.54, 1.0]);
173 static TREE_SEPARATOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.19, 1.0]);
174
175 static SCROLLBAR_TRACK_COLOR: RwLock<[f32; 4]> = RwLock::new([0.15, 0.15, 0.20, 0.3]);
176 static SCROLLBAR_THUMB_COLOR: RwLock<[f32; 4]> = RwLock::new([0.60, 0.60, 0.65, 0.4]);
177
178 static GRAPH_CELL_COLOR: RwLock<[f32; 3]> = RwLock::new([0.13, 0.13, 0.16]);
179 static GRAPH_GAP_COLOR: RwLock<[f32; 3]> = RwLock::new([0.07, 0.07, 0.09]);
180 static GRAPH_OPACITY: RwLock<f32> = RwLock::new(0.95);
181 /// Opacity of the graph's NODE-domain content (node bodies, wires, connectors,
182 /// node text) — `style.surface.graph.node.opacity`, deliberately independent of
183 /// `GRAPH_OPACITY`, which fades only the pane surface (grid cells/gaps).
184 static GRAPH_NODE_OPACITY: RwLock<f32> = RwLock::new(1.0);
185
186 static GRAPH_NODE_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_IDLE);
187 static GRAPH_NODE_SELECTED_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_SELECTED);
188 static GRAPH_NODE_DRAG_COLOR: RwLock<[f32; 4]> = RwLock::new(NODE_DRAG);
189
190 static GRAPH_WIRE_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
191 static GRAPH_WIRE_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
192
193 static GRAPH_CONNECTOR_COLOR: RwLock<[f32; 4]> = RwLock::new([0.1, 0.8, 0.4, 1.0]);
194 static GRAPH_CONNECTOR_HIGHLIGHT_COLOR: RwLock<[f32; 4]> = RwLock::new([0.0, 1.0, 0.9, 1.0]);
195
196 pub fn button_background_color() -> [f32; 4] {
197 style_read(&BUTTON_BACKGROUND_COLOR)
198 }
199
200 pub fn set_button_background_color(color: [f32; 4]) {
201 style_write(&BUTTON_BACKGROUND_COLOR, color);
202 }
203
204 pub fn button_hover_color() -> [f32; 4] {
205 let base = button_background_color();
206 let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
207 oklab[0] = (oklab[0] + 0.05).min(1.0); // increase lightness slightly
208 let rgb = oklab_to_linear_srgb(oklab);
209 [
210 rgb[0].clamp(0.0, 1.0),
211 rgb[1].clamp(0.0, 1.0),
212 rgb[2].clamp(0.0, 1.0),
213 (base[3] + 0.20).min(1.0),
214 ]
215 }
216
217 pub fn button_press_color() -> [f32; 4] {
218 let base = button_background_color();
219 let mut oklab = linear_srgb_to_oklab([base[0], base[1], base[2]]);
220 oklab[0] = (oklab[0] - 0.07).max(0.0); // decrease lightness
221 let rgb = oklab_to_linear_srgb(oklab);
222 [
223 rgb[0].clamp(0.0, 1.0),
224 rgb[1].clamp(0.0, 1.0),
225 rgb[2].clamp(0.0, 1.0),
226 (base[3] + 0.40).min(1.0),
227 ]
228 }
229
230 pub fn node_color() -> [f32; 4] {
231 load_colors_once();
232 style_read(&NODE_COLOR)
233 }
234
235 pub fn set_node_color(color: [f32; 4]) {
236 style_write(&NODE_COLOR, color);
237 }
238
239 pub fn node_selected_color() -> [f32; 4] {
240 load_colors_once();
241 style_read(&NODE_SELECTED_COLOR)
242 }
243
244 pub fn set_node_selected_color(color: [f32; 4]) {
245 style_write(&NODE_SELECTED_COLOR, color);
246 }
247
248 pub fn node_drag_color() -> [f32; 4] {
249 load_colors_once();
250 style_read(&NODE_DRAG_COLOR)
251 }
252
253 pub fn set_node_drag_color(color: [f32; 4]) {
254 style_write(&NODE_DRAG_COLOR, color);
255 }
256
257 fn read_config() -> Option<String> {
258 let path = crate::config::get_config_path();
259 if let Ok(content) = std::fs::read_to_string(&path) {
260 return Some(content);
261 }
262 None
263 }
264
265 fn parse_and_set_colors(content: &str) {
266 let val = crate::config::parse_kdl_to_json(content);
267
268 let parse_hex = |hex_str: &str| -> Option<[f32; 4]> {
269 let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
270 let hex = hex.trim_start_matches('#');
271 if hex.len() >= 8 {
272 if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
273 u8::from_str_radix(&hex[0..2], 16),
274 u8::from_str_radix(&hex[2..4], 16),
275 u8::from_str_radix(&hex[4..6], 16),
276 u8::from_str_radix(&hex[6..8], 16),
277 ) {
278 let r_f = srgb_to_linear(r as f32 / 255.0);
279 let g_f = srgb_to_linear(g as f32 / 255.0);
280 let b_f = srgb_to_linear(b as f32 / 255.0);
281 let a_f = a as f32 / 255.0;
282 return Some([r_f, g_f, b_f, a_f]);
283 }
284 } else if hex.len() >= 6 {
285 if let (Ok(r), Ok(g), Ok(b)) = (
286 u8::from_str_radix(&hex[0..2], 16),
287 u8::from_str_radix(&hex[2..4], 16),
288 u8::from_str_radix(&hex[4..6], 16),
289 ) {
290 let r_f = srgb_to_linear(r as f32 / 255.0);
291 let g_f = srgb_to_linear(g as f32 / 255.0);
292 let b_f = srgb_to_linear(b as f32 / 255.0);
293 return Some([r_f, g_f, b_f, 1.0]);
294 }
295 }
296 None
297 };
298
299 let get_color = |pointer: &str| -> Option<[f32; 4]> {
300 val.pointer(pointer).and_then(|v| v.as_str()).and_then(parse_hex)
301 };
302
303 let get_color_u8 = |pointer: &str| -> Option<[u8; 3]> {
304 val.pointer(pointer).and_then(|v| v.as_str()).and_then(|hex_str| {
305 let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
306 let hex = hex.trim_start_matches('#');
307 if hex.len() >= 6 {
308 if let (Ok(r), Ok(g), Ok(b)) = (
309 u8::from_str_radix(&hex[0..2], 16),
310 u8::from_str_radix(&hex[2..4], 16),
311 u8::from_str_radix(&hex[4..6], 16),
312 ) {
313 return Some([r, g, b]);
314 }
315 }
316 None
317 })
318 };
319
320 if let Some(opacity) = val.pointer("/layout/menubar_opacity").and_then(|v| v.as_f64()) {
321 if let Ok(mut lock) = OPACITY.write() {
322 *lock = Some(opacity as f32);
323 }
324 }
325
326 // `/style/surface/plate/root/...` is the one spelling of the root-plate
327 // style (RFC Phase 7a; the legacy `root plate` read-alias was removed
328 // 2026-09-06 once every live config had migrated).
329 if let Some(radius) = val.pointer("/style/surface/plate/root/corner_radius")
330 .and_then(|v| v.as_f64())
331 {
332 if let Ok(mut lock) = ROOT_PLATE_CORNER_RADIUS.write() {
333 *lock = radius as f32;
334 }
335 }
336
337 if let Some(c) = get_color("/style/surface/plate/root/color") {
338 if let Ok(mut lock) = PAGE_LOW_COLOR.write() { *lock = c; }
339 if let Ok(mut lock) = ROOT_PLATE_OPACITY.write() { *lock = Some(c[3]); }
340 }
341
342 if let Some(c) = get_color("/style/surface/plate/root/menubar/color") {
343 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_COLOR.write() { *lock = c; }
344 }
345 if let Some(c) = get_color("/style/surface/plate/root/menubar/text_color") {
346 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_TEXT_COLOR.write() { *lock = c; }
347 }
348 if let Some(o) = val.pointer("/style/surface/menu/opacity").and_then(|v| v.as_f64()) {
349 if let Ok(mut lock) = MENU_OPACITY.write() { *lock = (o as f32).clamp(0.0, 1.0); }
350 }
351 if let Some(c) = val.pointer("/style/surface/menu/compression").and_then(|v| v.as_f64()) {
352 if let Ok(mut lock) = MENU_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
353 }
354
355 let menubar_blur_ptr = val.pointer("/style/surface/plate/root/menubar/blur");
356 if let Some(blur) = menubar_blur_ptr.and_then(|v| v.as_bool()) {
357 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur; }
358 } else if let Some(blur_val) = menubar_blur_ptr.and_then(|v| v.as_f64()) {
359 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur_val > 0.001; }
360 }
361
362 if let Some(c) = get_color("/style/surface/statusbar/color") {
363 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_COLOR.write() { *lock = c; }
364 }
365 if let Some(c) = get_color("/style/surface/statusbar/text_color") {
366 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_TEXT_COLOR.write() { *lock = c; }
367 }
368 if let Some(blur) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_bool()) {
369 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur; }
370 } else if let Some(blur_val) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_f64()) {
371 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur_val > 0.001; }
372 }
373 if let Some(c) = get_color("/layout/color_borders_color") {
374 if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
375 }
376 if let Some(c) = get_color("/layout/paginator_sidebar_color") {
377 if let Ok(mut lock) = SIDEBAR_BG_COLOR.write() { *lock = c; }
378 }
379 if let Some(c) = get_color("/style/highlight/primary") {
380 if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() { *lock = [c[0], c[1], c[2], 0.12]; }
381 }
382 if let Some(c) = get_color("/style/control/button/background").or_else(|| get_color("/layout/button_background_color")) {
383 if let Ok(mut lock) = BUTTON_BACKGROUND_COLOR.write() { *lock = c; }
384 }
385 if let Some(c) = get_color("/style/control/dropdown/color") {
386 if let Ok(mut lock) = DROPDOWN_BACKGROUND_COLOR.write() { *lock = c; }
387 }
388 if let Some(c) = get_color_u8("/style/control/textbox/placeholder_text_color")
389 .or_else(|| get_color_u8("/style/textbox/placeholder_text_color"))
390 .or_else(|| get_color_u8("/style/data/textbox/placeholder_text_color")) {
391 if let Ok(mut lock) = TEXTBOX_PLACEHOLDER_TEXT_COLOR.write() { *lock = c; }
392 }
393 if let Some(c) = get_color("/style/control/textbox/background_color")
394 .or_else(|| get_color("/style/textbox/background_color"))
395 .or_else(|| get_color("/style/data/textbox/background_color")) {
396 if let Ok(mut lock) = TEXTBOX_BACKGROUND_COLOR.write() { *lock = c; }
397 }
398 if let Some(c) = get_color("/style/control/textbox/background_edit_color")
399 .or_else(|| get_color("/style/textbox/background_edit_color"))
400 .or_else(|| get_color("/style/data/textbox/background_edit_color")) {
401 if let Ok(mut lock) = TEXTBOX_BACKGROUND_EDIT_COLOR.write() { *lock = c; }
402 }
403 if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
404 if let Ok(mut lock) = MENUBAR_TAB_LABEL_COLOR.write() { *lock = c; }
405 }
406 if let Some(c) = get_color("/style/control/label/color") {
407 if let Ok(mut lock) = CONTROL_LABEL_COLOR.write() { *lock = c; }
408 }
409 if let Some(c) = get_color("/style/control/label/color_detached") {
410 if let Ok(mut lock) = CONTROL_LABEL_COLOR_DETACHED.write() { *lock = c; }
411 }
412 if let Some(c) = get_color("/style/control/label/hover_color") {
413 if let Ok(mut lock) = CONTROL_LABEL_HOVER_COLOR.write() { *lock = Some(c); }
414 }
415 if let Some(c) = get_color("/style/control/label/focus_color") {
416 if let Ok(mut lock) = CONTROL_LABEL_FOCUS_COLOR.write() { *lock = Some(c); }
417 }
418 // The toggle state colors (enabled/disabled/gradient/background) are not
419 // read: the control inherits its plate — see the note by the retired
420 // getters below.
421 if let Some(c) = get_color("/style/control/ramp/background") {
422 if let Ok(mut lock) = RAMP_BACKGROUND_COLOR.write() { *lock = c; }
423 }
424 if let Some(c) = get_color("/style/control/ramp/border_color") {
425 if let Ok(mut lock) = RAMP_BORDER_COLOR.write() { *lock = c; }
426 }
427 if let Some(c) = get_color("/style/control/control_panel/color") {
428 if let Ok(mut lock) = CONTROL_PANEL_COLOR.write() { *lock = c; }
429 }
430 if let Some(c) = get_color("/style/control/control_panel/border_color") {
431 if let Ok(mut lock) = CONTROL_PANEL_BORDER_COLOR.write() { *lock = c; }
432 }
433 if let Some(c) = get_color("/style/control/spinbox/background_color") {
434 if let Ok(mut lock) = SPINBOX_DISPLAY_COLOR.write() { *lock = c; }
435 }
436 if let Some(c) = get_color("/style/control/spinbox/button_color") {
437 if let Ok(mut lock) = SPINBOX_BUTTON_COLOR.write() { *lock = c; }
438 if let Ok(mut lock_hover) = SPINBOX_BUTTON_HOVER_COLOR.write() {
439 *lock_hover = [
440 (c[0] + 0.1).min(1.0),
441 (c[1] + 0.1).min(1.0),
442 (c[2] + 0.1).min(1.0),
443 c[3]
444 ];
445 }
446 }
447 if let Some(c) = get_color("/style/control/spinbox/button_hover_color") {
448 if let Ok(mut lock) = SPINBOX_BUTTON_HOVER_COLOR.write() { *lock = c; }
449 }
450 if let Some(c) = get_color("/style/control/spinbox/text_color") {
451 if let Ok(mut lock) = SPINBOX_TEXT_COLOR.write() { *lock = c; }
452 }
453 if let Some(c) = get_color("/style/control/button/border_color").or_else(|| get_color("/style/button/border_color")) {
454 if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() { *lock = Some(c); }
455 }
456 if let Some(c) = get_color("/style/control/button/hover_color").or_else(|| get_color("/style/button/hover_color")) {
457 if let Ok(mut lock) = BUTTON_HOVER_COLOR.write() { *lock = Some(c); }
458 }
459 if let Some(c) = get_color("/style/control/dropdown/border_color").or_else(|| get_color("/style/dropdown/border_color")) {
460 if let Ok(mut lock) = DROPDOWN_BORDER_COLOR.write() { *lock = c; }
461 }
462 if let Some(c) = get_color("/style/control/dropdown/text_color").or_else(|| get_color("/style/dropdown/text_color")) {
463 if let Ok(mut lock) = DROPDOWN_TEXT_COLOR.write() { *lock = c; }
464 }
465 if let Some(c) = get_color("/style/control/slider/thumb_color").or_else(|| get_color("/style/slider/thumb_color")) {
466 if let Ok(mut lock) = SLIDER_THUMB_COLOR.write() { *lock = c; }
467 if let Ok(mut lock_drag) = SLIDER_THUMB_DRAG_COLOR.write() {
468 *lock_drag = [
469 (c[0] + 0.15).min(1.0),
470 (c[1] + 0.15).min(1.0),
471 (c[2] + 0.15).min(1.0),
472 c[3]
473 ];
474 }
475 }
476 if let Some(c) = get_color("/style/control/rangeslider/thumb_color").or_else(|| get_color("/style/rangeslider/thumb_color")) {
477 if let Ok(mut lock) = RANGE_SLIDER_THUMB_COLOR.write() { *lock = c; }
478 if let Ok(mut lock_drag) = RANGE_SLIDER_THUMB_DRAG_COLOR.write() {
479 *lock_drag = [
480 (c[0] + 0.15).min(1.0),
481 (c[1] + 0.15).min(1.0),
482 (c[2] + 0.15).min(1.0),
483 c[3]
484 ];
485 }
486 }
487 if let Some(c) = get_color("/style/control/progressbar/background") {
488 if let Ok(mut lock) = PROGRESS_BG_COLOR.write() { *lock = c; }
489 }
490 if let Some(c) = get_color("/style/control/progressbar/fill") {
491 if let Ok(mut lock) = PROGRESS_FILL_COLOR.write() { *lock = c; }
492 }
493 let parsed_list_bg = get_color("/layout/list_bg_color");
494 let parsed_breadcrumb_bg = get_color("/layout/breadcrumb_bg_color");
495 let parsed_popover_bg = get_color("/layout/popover_bg_color");
496
497 if let Some(c) = get_color("/layout/page_color") {
498 if let Ok(mut lock) = PAGE_COLOR.write() { *lock = c; }
499 }
500 if let Some(c) = get_color("/layout/layer_color") {
501 if let Ok(mut lock) = LAYER_COLOR.write() { *lock = c; }
502 }
503
504 if let Some(c) = parsed_list_bg {
505 if let Ok(mut lock) = LIST_BG_COLOR.write() {
506 *lock = [c[0], c[1], c[2], 0.3];
507 }
508 }
509 if let Some(c) = get_color("/layout/list_entry_bg_color") {
510 if let Ok(mut lock) = LIST_ENTRY_BG_COLOR.write() {
511 *lock = c;
512 }
513 }
514 if let Some(c) = get_color("/layout/list_entry_highlight_color") {
515 if let Ok(mut lock) = LIST_ENTRY_HIGHLIGHT_COLOR.write() {
516 *lock = c;
517 }
518 }
519 if let Some(c) = get_color("/style/data/list/font_color").or_else(|| get_color("/layout/list_font_color")) {
520 if let Ok(mut lock) = LIST_FONT_COLOR.write() {
521 *lock = c;
522 }
523 }
524 if let Some(c) = parsed_breadcrumb_bg {
525 if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
526 *lock = [c[0], c[1], c[2], 1.0];
527 }
528 } else if let Some(c) = parsed_list_bg {
529 if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
530 *lock = [c[0], c[1], c[2], 1.0];
531 }
532 }
533 if let Some(c) = parsed_popover_bg {
534 if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
535 *lock = [c[0], c[1], c[2], 1.0];
536 }
537 } else if let Some(c) = parsed_list_bg {
538 if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
539 *lock = [c[0], c[1], c[2], 1.0];
540 }
541 }
542
543 if let Some(c) = get_color("/style/surface/graph/cell_color") {
544 if let Ok(mut lock) = GRAPH_CELL_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
545 }
546 if let Some(c) = get_color("/style/surface/graph/gap_color") {
547 if let Ok(mut lock) = GRAPH_GAP_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
548 }
549 if let Some(c) = get_color("/style/surface/graph/node/color") {
550 if let Ok(mut lock) = GRAPH_NODE_COLOR.write() { *lock = c; }
551 if let Ok(mut lock) = NODE_COLOR.write() { *lock = c; }
552 }
553 if let Some(c) = get_color("/style/surface/graph/node/selected_color") {
554 if let Ok(mut lock) = GRAPH_NODE_SELECTED_COLOR.write() { *lock = c; }
555 if let Ok(mut lock) = NODE_SELECTED_COLOR.write() { *lock = c; }
556 }
557 if let Some(c) = get_color("/style/surface/graph/node/drag_color") {
558 if let Ok(mut lock) = GRAPH_NODE_DRAG_COLOR.write() { *lock = c; }
559 if let Ok(mut lock) = NODE_DRAG_COLOR.write() { *lock = c; }
560 }
561 if let Some(c) = get_color("/style/surface/graph/node/wire_color") {
562 if let Ok(mut lock) = GRAPH_WIRE_COLOR.write() { *lock = c; }
563 }
564 if let Some(c) = get_color("/style/surface/graph/node/wire_highlight_color") {
565 if let Ok(mut lock) = GRAPH_WIRE_HIGHLIGHT_COLOR.write() { *lock = c; }
566 }
567 if let Some(c) = get_color("/style/surface/graph/node/connector_color") {
568 if let Ok(mut lock) = GRAPH_CONNECTOR_COLOR.write() { *lock = c; }
569 }
570 if let Some(c) = get_color("/style/surface/graph/node/connector_highlight_color") {
571 if let Ok(mut lock) = GRAPH_CONNECTOR_HIGHLIGHT_COLOR.write() { *lock = c; }
572 }
573 if let Some(opacity) = val.pointer("/style/surface/graph/opacity").and_then(|v| v.as_f64()) {
574 if let Ok(mut lock) = GRAPH_OPACITY.write() { *lock = opacity as f32; }
575 }
576 if let Some(opacity) = val.pointer("/style/surface/graph/node/opacity").and_then(|v| v.as_f64()) {
577 if let Ok(mut lock) = GRAPH_NODE_OPACITY.write() { *lock = opacity as f32; }
578 }
579
580 if let Some(c) = get_color("/style/data/tree/background_color") {
581 if let Ok(mut lock) = TREE_BACKGROUND_COLOR.write() { *lock = c; }
582 }
583 if let Some(c) = get_color("/style/data/tree/border_color") {
584 if let Ok(mut lock) = TREE_BORDER_COLOR.write() { *lock = c; }
585 }
586 if let Some(c) = get_color("/style/data/tree/border_hover_color") {
587 if let Ok(mut lock) = TREE_BORDER_HOVER_COLOR.write() { *lock = c; }
588 }
589 if let Some(c) = get_color("/style/data/tree/border_focus_color") {
590 if let Ok(mut lock) = TREE_BORDER_FOCUS_COLOR.write() { *lock = c; }
591 }
592 if let Some(c) = get_color("/style/data/tree/section_bg_color") {
593 if let Ok(mut lock) = TREE_SECTION_BG_COLOR.write() { *lock = c; }
594 }
595 if let Some(c) = get_color("/style/data/tree/section_bg_hover_color") {
596 if let Ok(mut lock) = TREE_SECTION_BG_HOVER_COLOR.write() { *lock = c; }
597 }
598 if let Some(c) = get_color("/style/data/tree/leaf_bg_even_color") {
599 if let Ok(mut lock) = TREE_LEAF_BG_EVEN_COLOR.write() { *lock = c; }
600 }
601 if let Some(c) = get_color("/style/data/tree/leaf_bg_odd_color") {
602 if let Ok(mut lock) = TREE_LEAF_BG_ODD_COLOR.write() { *lock = c; }
603 }
604 if let Some(c) = get_color("/style/data/tree/leaf_bg_hover_color") {
605 if let Ok(mut lock) = TREE_LEAF_BG_HOVER_COLOR.write() { *lock = c; }
606 }
607 if let Some(c) = get_color("/style/data/tree/leaf_bg_selected_color") {
608 if let Ok(mut lock) = TREE_LEAF_BG_SELECTED_COLOR.write() { *lock = c; }
609 }
610 if let Some(c) = get_color("/style/data/tree/section_text_color") {
611 if let Ok(mut lock) = TREE_SECTION_TEXT_COLOR.write() { *lock = c; }
612 }
613 if let Some(c) = get_color("/style/data/tree/leaf_text_color") {
614 if let Ok(mut lock) = TREE_LEAF_TEXT_COLOR.write() { *lock = c; }
615 }
616 if let Some(c) = get_color("/style/data/tree/leaf_text_selected_color") {
617 if let Ok(mut lock) = TREE_LEAF_TEXT_SELECTED_COLOR.write() { *lock = c; }
618 }
619 if let Some(c) = get_color("/style/data/tree/type_text_color") {
620 if let Ok(mut lock) = TREE_TYPE_TEXT_COLOR.write() { *lock = c; }
621 }
622 if let Some(c) = get_color("/style/data/tree/value_text_color") {
623 if let Ok(mut lock) = TREE_VALUE_TEXT_COLOR.write() { *lock = c; }
624 }
625 if let Some(c) = get_color("/style/data/tree/separator_color") {
626 if let Ok(mut lock) = TREE_SEPARATOR_COLOR.write() { *lock = c; }
627 }
628 if let Some(k) = val.pointer("/style/data/tree/open_search").and_then(|v| v.as_str()) {
629 if let Ok(mut lock) = TREE_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
630 }
631 if let Some(k) = val.pointer("/style/data/list/open_search").and_then(|v| v.as_str()) {
632 if let Ok(mut lock) = LIST_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
633 }
634 if let Some(k) = val.pointer("/style/data/list/close_search").and_then(|v| v.as_str()) {
635 if let Ok(mut lock) = LIST_CLOSE_SEARCH_KEY.write() { *lock = k.to_string(); }
636 }
637 if let Some(c) = get_color("/style/control/scrollbar/track_color") {
638 if let Ok(mut lock) = SCROLLBAR_TRACK_COLOR.write() { *lock = c; }
639 }
640 if let Some(c) = get_color("/style/control/scrollbar/thumb_color") {
641 if let Ok(mut lock) = SCROLLBAR_THUMB_COLOR.write() { *lock = c; }
642 }
643 if let Some(c) = get_color("/style/surface/plate/color") {
644 if let Ok(mut lock) = PLATE_COLOR.write() { *lock = Some(c); }
645 }
646 if let Some(c) = get_color("/style/surface/plate/border_color") {
647 if let Ok(mut lock) = PLATE_BORDER_COLOR.write() { *lock = Some(c); }
648 }
649 if let Some(t) = val.pointer("/style/surface/plate/border_thickness").and_then(|v| v.as_f64()) {
650 if let Ok(mut lock) = PLATE_BORDER_THICKNESS.write() { *lock = t as f32; }
651 }
652 if let Some(t) = val.pointer("/style/surface/plate/bevel_width").and_then(|v| v.as_f64()) {
653 if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() { *lock = t as f32; }
654 }
655 if let Some(c) = get_color("/style/surface/param/color") {
656 if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
657 }
658 if let Some(c) = val
659 .pointer("/style/surface/plate/backdrop_compression")
660 .and_then(|v| v.as_f64())
661 {
662 if let Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
663 }
664 if let Some(r) = val.pointer("/style/surface/plate/refraction").and_then(|v| v.as_f64()) {
665 if let Ok(mut lock) = PLATE_REFRACTION.write() { *lock = (r as f32).clamp(0.0, 1.0); }
666 }
667 if let Some(blur) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_bool()) {
668 if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur; }
669 } else if let Some(blur_val) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_f64()) {
670 if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur_val > 0.001; }
671 }
672 if let Some(r) = val.pointer("/style/surface/plate/radius").and_then(|v| v.as_f64()) {
673 if let Ok(mut lock) = PLATE_FROST_RADIUS.write() { *lock = (r as f32).max(0.0); }
674 }
675
676 // The DE finish beyond its strength (`relief.depth` / `light`, which
677 // lives in the style registry): the three terms that were literals.
678 let f = |k: &str| val.pointer(&format!("/style/surface/relief/{k}")).and_then(|v| v.as_f64()).map(|v| v as f32);
679 if let Some(v) = f("spec") {
680 if let Ok(mut lock) = FINISH_SPEC.write() { *lock = v.max(0.0); }
681 }
682 if let Some(v) = f("shininess") {
683 if let Ok(mut lock) = FINISH_SHININESS.write() { *lock = v.max(1.0); }
684 }
685 if let Some(v) = f("curvature") {
686 if let Ok(mut lock) = FINISH_CURVATURE.write() { *lock = v.max(0.0); }
687 }
688
689 // Named materials and the rung bindings (RFC material § 5), replaced
690 // wholesale so a reload forgets what config no longer says.
691 {
692 use crate::scene::material::{FrostDef, MaterialDef};
693 let num = |v: Option<&serde_json::Value>| v.and_then(|v| v.as_f64()).map(|v| v as f32);
694 let mut map: Vec<(String, MaterialDef)> = Vec::new();
695 if let Some(obj) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
696 for (name, node) in obj {
697 let Some(node) = node.as_object() else { continue };
698 let frost = node.get("frost").map(|fr| match fr.as_object() {
699 Some(fo) => FrostDef {
700 compression: num(fo.get("backdrop_compression").or(fo.get("compression"))),
701 refraction: num(fo.get("refraction")),
702 radius: num(fo.get("radius")),
703 },
704 // A bare `frost` node (no knobs) is frosted at the defaults.
705 None => FrostDef::default(),
706 });
707 let fin = node.get("finish").and_then(|v| v.as_object());
708 let fk = |k: &str| fin.and_then(|fo| num(fo.get(k)));
709 map.push((
710 name.clone(),
711 MaterialDef {
712 tint: node.get("color").and_then(|v| v.as_str()).and_then(parse_hex),
713 frost,
714 light: fk("light").or(fk("depth")),
715 spec: fk("spec"),
716 shininess: fk("shininess"),
717 curvature: fk("curvature"),
718 },
719 ));
720 }
721 }
722 if let Ok(mut lock) = NAMED_MATERIALS.write() { *lock = map; }
723 let bind = |p: &str| val.pointer(p).and_then(|v| v.as_str()).map(|s| s.to_string()).filter(|s| !s.is_empty());
724 let bindings = [
725 bind("/style/surface/plate/root/material"),
726 bind("/style/surface/plate/material"),
727 bind("/style/control/material"),
728 ];
729 if let Ok(mut lock) = MATERIAL_BINDINGS.write() { *lock = bindings; }
730 }
731 }
732
733 fn load_colors_once() {
734 use std::sync::Once;
735 static INIT: Once = Once::new();
736 INIT.call_once(|| {
737 if let Some(content) = read_config() {
738 parse_and_set_colors(&content);
739 }
740 });
741 }
742
743 pub fn reload_colors(content: &str) {
744 parse_and_set_colors(content);
745 }
746
747 /// Test-only: the color state is process-global, so any test that calls
748 /// [`reload_colors`] and then asserts getter values races every other such
749 /// test on the parallel harness. Each of those tests must hold this lock
750 /// across its reload + asserts, and should fire the once-per-process config
751 /// loads ([`load_colors_once`] via any getter, and
752 /// `layout::lazy_init_style_registry`) inside the lock BEFORE its reload, so
753 /// neither can rewrite the state from the live config file mid-assert.
754 #[cfg(test)]
755 pub(crate) fn test_color_state_lock() -> std::sync::MutexGuard<'static, ()> {
756 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
757 LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
758 }
759
760 pub fn dropdown_background_color() -> [f32; 4] {
761 load_colors_once();
762 style_read(&DROPDOWN_BACKGROUND_COLOR)
763 }
764
765 pub fn set_dropdown_background_color(color: [f32; 4]) {
766 style_write(&DROPDOWN_BACKGROUND_COLOR, color);
767 }
768
769 pub fn textbox_placeholder_text_color() -> [u8; 3] {
770 load_colors_once();
771 style_read(&TEXTBOX_PLACEHOLDER_TEXT_COLOR)
772 }
773
774 pub fn set_textbox_placeholder_text_color(color: [u8; 3]) {
775 style_write(&TEXTBOX_PLACEHOLDER_TEXT_COLOR, color);
776 }
777
778 pub fn textbox_background_color() -> [f32; 4] {
779 load_colors_once();
780 style_read(&TEXTBOX_BACKGROUND_COLOR)
781 }
782
783 pub fn set_textbox_background_color(color: [f32; 4]) {
784 style_write(&TEXTBOX_BACKGROUND_COLOR, color);
785 }
786
787 pub fn textbox_background_edit_color() -> [f32; 4] {
788 load_colors_once();
789 style_read(&TEXTBOX_BACKGROUND_EDIT_COLOR)
790 }
791
792 pub fn set_textbox_background_edit_color(color: [f32; 4]) {
793 style_write(&TEXTBOX_BACKGROUND_EDIT_COLOR, color);
794 }
795
796 pub fn graph_wire_color() -> [f32; 4] {
797 load_colors_once();
798 style_read(&GRAPH_WIRE_COLOR)
799 }
800
801 pub fn set_graph_wire_color(color: [f32; 4]) {
802 style_write(&GRAPH_WIRE_COLOR, color);
803 }
804
805 pub fn graph_wire_highlight_color() -> [f32; 4] {
806 load_colors_once();
807 style_read(&GRAPH_WIRE_HIGHLIGHT_COLOR)
808 }
809
810 pub fn set_graph_wire_highlight_color(color: [f32; 4]) {
811 style_write(&GRAPH_WIRE_HIGHLIGHT_COLOR, color);
812 }
813
814 pub fn graph_connector_color() -> [f32; 4] {
815 load_colors_once();
816 style_read(&GRAPH_CONNECTOR_COLOR)
817 }
818
819 pub fn set_graph_connector_color(color: [f32; 4]) {
820 style_write(&GRAPH_CONNECTOR_COLOR, color);
821 }
822
823 pub fn graph_connector_highlight_color() -> [f32; 4] {
824 load_colors_once();
825 style_read(&GRAPH_CONNECTOR_HIGHLIGHT_COLOR)
826 }
827
828 pub fn set_graph_connector_highlight_color(color: [f32; 4]) {
829 style_write(&GRAPH_CONNECTOR_HIGHLIGHT_COLOR, color);
830 }
831
832 pub fn graph_node_color() -> [f32; 4] {
833 load_colors_once();
834 style_read(&GRAPH_NODE_COLOR)
835 }
836
837 pub fn set_graph_node_color(color: [f32; 4]) {
838 style_write(&GRAPH_NODE_COLOR, color);
839 }
840
841 pub fn graph_node_selected_color() -> [f32; 4] {
842 load_colors_once();
843 style_read(&GRAPH_NODE_SELECTED_COLOR)
844 }
845
846 pub fn set_graph_node_selected_color(color: [f32; 4]) {
847 style_write(&GRAPH_NODE_SELECTED_COLOR, color);
848 }
849
850 pub fn graph_node_drag_color() -> [f32; 4] {
851 load_colors_once();
852 style_read(&GRAPH_NODE_DRAG_COLOR)
853 }
854
855 pub fn set_graph_node_drag_color(color: [f32; 4]) {
856 style_write(&GRAPH_NODE_DRAG_COLOR, color);
857 }
858
859 pub fn graph_cell_color() -> [f32; 3] {
860 load_colors_once();
861 style_read(&GRAPH_CELL_COLOR)
862 }
863
864 pub fn set_graph_cell_color(color: [f32; 3]) {
865 style_write(&GRAPH_CELL_COLOR, color);
866 }
867
868 pub fn graph_gap_color() -> [f32; 3] {
869 load_colors_once();
870 style_read(&GRAPH_GAP_COLOR)
871 }
872
873 pub fn set_graph_gap_color(color: [f32; 3]) {
874 style_write(&GRAPH_GAP_COLOR, color);
875 }
876
877 pub fn graph_opacity() -> f32 {
878 load_colors_once();
879 style_read(&GRAPH_OPACITY)
880 }
881
882 pub fn graph_node_opacity() -> f32 {
883 load_colors_once();
884 style_read(&GRAPH_NODE_OPACITY)
885 }
886
887 pub fn set_graph_node_opacity(opacity: f32) {
888 style_write(&GRAPH_NODE_OPACITY, opacity);
889 }
890
891 pub fn set_graph_opacity(opacity: f32) {
892 style_write(&GRAPH_OPACITY, opacity);
893 }
894
895 pub fn page_low_color() -> [f32; 4] {
896 load_colors_once();
897 let mut color = *PAGE_LOW_COLOR.read().unwrap();
898 if let Some(opacity) = read_root_plate_opacity_if_configured() {
899 color[3] = opacity;
900 }
901 color
902 }
903
904 pub fn set_page_low_color(color: [f32; 4]) {
905 style_write(&PAGE_LOW_COLOR, color);
906 }
907
908 pub fn page_color() -> [f32; 4] {
909 load_colors_once();
910 style_read(&PAGE_COLOR)
911 }
912
913 pub fn set_page_color(color: [f32; 4]) {
914 style_write(&PAGE_COLOR, color);
915 }
916
917 pub fn layer_color() -> [f32; 4] {
918 load_colors_once();
919 style_read(&LAYER_COLOR)
920 }
921
922 pub fn set_layer_color(color: [f32; 4]) {
923 style_write(&LAYER_COLOR, color);
924 }
925
926
927 pub fn color_borders_color() -> [f32; 4] {
928 load_colors_once();
929 style_read(&COLOR_BORDERS_COLOR)
930 }
931
932 pub fn set_color_borders_color(color: [f32; 4]) {
933 style_write(&COLOR_BORDERS_COLOR, color);
934 }
935
936 pub const SLIDER_THUMB: [f32; 4] = [0.60, 0.60, 0.65, 1.0];
937 pub const SLIDER_THUMB_DRAG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
938 pub const PROGRESS_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
939 pub const PROGRESS_FILL: [f32; 4] = [0.20, 0.50, 0.75, 1.0];
940
941 pub const HIGHLIGHT_PRIMARY: [f32; 4] = [1.0, 1.0, 1.0, 0.12];
942 pub const HIGHLIGHT_SECONDARY: [f32; 4] = [1.0, 1.0, 1.0, 0.06];
943
944 pub const SPINBOX_BG: [f32; 4] = [0.18, 0.18, 0.22, 1.0];
945 pub const SPINBOX_BUTTON: [f32; 4] = [0.25, 0.25, 0.32, 1.0];
946 pub const SPINBOX_BUTTON_HOVER: [f32; 4] = [0.35, 0.35, 0.42, 1.0];
947 pub const SPINBOX_DISPLAY: [f32; 4] = [0.12, 0.12, 0.16, 1.0];
948
949 pub const CANVAS_BG: [f32; 4] = [0.05, 0.05, 0.10, 1.0];
950 pub const VIEWPORT_BG: [f32; 4] = [0.0, 0.0, 0.0, 0.0];
951 pub const PARAM_BG: [f32; 4] = [0.10, 0.10, 0.14, 0.25];
952
953 /// The params pane's plate tint (linear rgba) — [`PARAM_BG`] made live:
954 /// `style.surface.param.color` in config overrides it, and apps can retint at
955 /// runtime (the designer's Style section "Plate Color"). Alpha doubles as the
956 /// frost strength under plate blur.
957 static PARAM_BG_COLOR: RwLock<[f32; 4]> = RwLock::new(PARAM_BG);
958
959 pub fn param_bg_color() -> [f32; 4] {
960 load_colors_once();
961 style_read(&PARAM_BG_COLOR)
962 }
963
964 pub fn set_param_bg_color(color: [f32; 4]) {
965 style_write(&PARAM_BG_COLOR, color);
966 }
967
968 /// The params plate's final fill as the renderer consumes it: the pane rung's
969 /// material ([`crate::scene::Material::pane`] — the tint at the global plate
970 /// opacity, frosted under plate blur) encoded for a nested plate. The single
971 /// source both `ParametersBg`'s own plate and any surface that wants to match
972 /// it (the designer's node bodies) draw from, so they track a live retint /
973 /// opacity / blur toggle together.
974 pub fn param_plate_fill() -> [f32; 4] {
975 use crate::scene::material::{Material, PlateRole};
976 Material::pane().fill(PlateRole::Nested)
977 }
978
979 pub const PANEL_MENU_BG: [f32; 4] = [0.08, 0.08, 0.12, 1.0];
980 pub const PANEL_MENU_HOVER: [f32; 4] = [0.18, 0.18, 0.25, 1.0];
981 pub const PANEL_MENU_FOCUSED: [f32; 4] = [0.08, 0.16, 0.28, 1.0];
982
983 pub const SPLITTER_IDLE: [f32; 4] = [0.20, 0.20, 0.27, 1.0];
984 pub const SPLITTER_HOVER: [f32; 4] = [0.40, 0.40, 0.50, 1.0];
985 pub const SPLITTER_DRAG: [f32; 4] = [0.50, 0.50, 0.60, 1.0];
986
987 pub const TEXT_FG: [f32; 4] = [0.80, 0.80, 0.85, 1.0];
988 pub const TEXT_DIM: [f32; 4] = [0.53, 0.53, 0.60, 1.0];
989
990 /// A canvas well's floor: the plate darkened, not a fill of its own, so every
991 /// opening you look into — Trackpad, Slider2D, the bevel and ramp previews —
992 /// is cut from the one material ([`crate::scene::paint::PaintCtx::well_floor`]).
993 pub const WELL_FLOOR: [f32; 4] = [0.0, 0.0, 0.0, 0.18];
994 /// [`WELL_FLOOR`] risen toward the plate: a clickable canvas's hover cue.
995 pub const WELL_FLOOR_LIFTED: [f32; 4] = [0.0, 0.0, 0.0, 0.10];
996 /// The hairline a well is framed with when relief is off, idle; see
997 /// [`well_frame_color`].
998 pub const WELL_FRAME: [f32; 4] = [0.18, 0.18, 0.24, 1.0];
999 /// [`WELL_FRAME`] under the pointer.
1000 pub const WELL_FRAME_HOVER: [f32; 4] = [0.25, 0.25, 0.35, 1.0];
1001
1002 /// The frame a flat well is drawn in — the one hairline every well (text,
1003 /// keybind, colour and font fields, the canvases) wears when relief is off:
1004 /// neutral greys idle and hovered, the highlight accent while the well is
1005 /// active (editing, recording, pressed) — the colour the relief wells light
1006 /// their rims with, so the two styles share one focus cue. A flat well has no
1007 /// floor of its own any more than a relief one: the plate is the floor.
1008 pub fn well_frame_color(hovered: bool, active: bool) -> [f32; 4] {
1009 if active {
1010 let c = highlight_primary_color();
1011 [c[0], c[1], c[2], 1.0]
1012 } else if hovered {
1013 WELL_FRAME_HOVER
1014 } else {
1015 WELL_FRAME
1016 }
1017 }
1018 pub const TEXT_HEADER: [f32; 4] = [0.90, 0.90, 0.95, 1.0];
1019 pub const TEXT_ACCENT: [f32; 4] = [0.56, 0.83, 0.56, 1.0];
1020
1021 pub fn srgb_to_linear(c: f32) -> f32 {
1022 if c <= 0.04045 {
1023 c / 12.92
1024 } else {
1025 ((c + 0.055) / 1.055).powf(2.4)
1026 }
1027 }
1028
1029 /// Parse a hex color string into raw RGBA bytes.
1030 ///
1031 /// Accepts `#RRGGBB` or `#RRGGBBAA`, tolerating surrounding quotes/whitespace and
1032 /// an optional leading `#`. 6-digit input yields alpha `255`. Returns `None` for
1033 /// any shorter/invalid input. This is the primitive the `[f32;_]` parsers build on.
1034 pub fn parse_hex_bytes(s: &str) -> Option<[u8; 4]> {
1035 let hex = s
1036 .trim_matches(|c| c == '"' || c == '\'' || c == ' ')
1037 .trim_start_matches('#');
1038 let b = |i: usize| u8::from_str_radix(hex.get(i * 2..i * 2 + 2)?, 16).ok();
1039 if hex.len() >= 8 {
1040 Some([b(0)?, b(1)?, b(2)?, b(3)?])
1041 } else if hex.len() >= 6 {
1042 Some([b(0)?, b(1)?, b(2)?, 255])
1043 } else {
1044 None
1045 }
1046 }
1047
1048 /// Parse a hex color string into raw sRGB RGBA in `[0,1]` (no gamma conversion).
1049 /// See [`parse_hex_bytes`] for the accepted formats. Apply [`srgb_to_linear`] to
1050 /// the RGB channels yourself if your render target expects linear color.
1051 pub fn parse_hex_rgba(s: &str) -> Option<[f32; 4]> {
1052 parse_hex_bytes(s).map(|[r, g, b, a]| {
1053 [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
1054 })
1055 }
1056
1057 /// Like [`parse_hex_rgba`] but drops alpha, returning raw sRGB RGB in `[0,1]`.
1058 pub fn parse_hex_rgb(s: &str) -> Option<[f32; 3]> {
1059 parse_hex_rgba(s).map(|[r, g, b, _]| [r, g, b])
1060 }
1061
1062 /// Like [`parse_hex_rgba`] but converts RGB from sRGB to linear (alpha kept as-is).
1063 /// Use when your render target samples colors in linear space.
1064 pub fn parse_hex_rgba_linear(s: &str) -> Option<[f32; 4]> {
1065 parse_hex_rgba(s).map(|[r, g, b, a]| {
1066 [srgb_to_linear(r), srgb_to_linear(g), srgb_to_linear(b), a]
1067 })
1068 }
1069
1070 pub fn linear_to_srgb(c: f32) -> f32 {
1071 if c <= 0.0031308 {
1072 c * 12.92
1073 } else {
1074 1.055 * c.powf(1.0 / 2.4) - 0.055
1075 }
1076 }
1077
1078 pub fn to_linear(color: [f32; 4]) -> [f32; 4] {
1079 [
1080 srgb_to_linear(color[0]),
1081 srgb_to_linear(color[1]),
1082 srgb_to_linear(color[2]),
1083 color[3],
1084 ]
1085 }
1086
1087 pub fn to_srgb(color: [f32; 4]) -> [f32; 4] {
1088 [
1089 linear_to_srgb(color[0]),
1090 linear_to_srgb(color[1]),
1091 linear_to_srgb(color[2]),
1092 color[3],
1093 ]
1094 }
1095
1096 pub fn to_linear_rgb(color: [f32; 3]) -> [f32; 3] {
1097 [
1098 srgb_to_linear(color[0]),
1099 srgb_to_linear(color[1]),
1100 srgb_to_linear(color[2]),
1101 ]
1102 }
1103
1104 pub fn to_srgb_rgb(color: [f32; 3]) -> [f32; 3] {
1105 [
1106 linear_to_srgb(color[0]),
1107 linear_to_srgb(color[1]),
1108 linear_to_srgb(color[2]),
1109 ]
1110 }
1111
1112 pub fn linear_srgb_to_oklab(rgb: [f32; 3]) -> [f32; 3] {
1113 let l = 0.4122214708 * rgb[0] + 0.5363325363 * rgb[1] + 0.0514459929 * rgb[2];
1114 let m = 0.2119034982 * rgb[0] + 0.6806995451 * rgb[1] + 0.1073969566 * rgb[2];
1115 let s = 0.0883024619 * rgb[0] + 0.2817188376 * rgb[1] + 0.6299787005 * rgb[2];
1116
1117 let l_ = l.max(0.0).powf(1.0 / 3.0);
1118 let m_ = m.max(0.0).powf(1.0 / 3.0);
1119 let s_ = s.max(0.0).powf(1.0 / 3.0);
1120
1121 [
1122 0.2104542553 * l_ + 0.7936177850 * m_ - 0.0040720468 * s_,
1123 1.9779984951 * l_ - 2.4285922050 * m_ + 0.4505937099 * s_,
1124 0.0259040371 * l_ + 0.7827717662 * m_ - 0.8086757660 * s_,
1125 ]
1126 }
1127
1128 pub fn oklab_to_linear_srgb(lab: [f32; 3]) -> [f32; 3] {
1129 let l_ = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
1130 let m_ = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
1131 let s_ = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
1132
1133 let l = l_ * l_ * l_;
1134 let m = m_ * m_ * m_;
1135 let s = s_ * s_ * s_;
1136
1137 [
1138 4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
1139 -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
1140 -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
1141 ]
1142 }
1143
1144 /// The alpha that makes an overlay's fade-out read as an EVEN fade to the eye.
1145 ///
1146 /// `t` runs 0 (overlay at `max_alpha`) to 1 (fully faded); `overlay` and `backdrop` are
1147 /// linear-light RGB, the backdrop being whatever the fade composites onto.
1148 ///
1149 /// A linear alpha ramp is not a linear fade: the surface is an sRGB attachment, so the GPU
1150 /// blends in LINEAR light, and perceived lightness goes as roughly the cube root of that. A
1151 /// straight alpha ramp therefore hangs bright through the middle and then dives near the
1152 /// end. This walks perceived lightness linearly instead and solves back for the alpha that
1153 /// lands on it — exactly, since the composite is linear in alpha:
1154 ///
1155 /// ```text
1156 /// Y(a) = a·Y_overlay + (1 - a)·Y_backdrop (blending, in linear light)
1157 /// L ≈ cbrt(Y) (perception — OKLab's L, and CIE L* to within a hair)
1158 /// ```
1159 ///
1160 /// Solved on luminance rather than per channel because a single alpha can only satisfy one
1161 /// axis, and lightness is the one the eye reads a fade by. With no lightness contrast to
1162 /// shape (a fade between equally-bright colors) it falls back to the linear ramp.
1163 pub fn perceptual_fade_alpha(t: f32, overlay: [f32; 3], backdrop: [f32; 3], max_alpha: f32) -> f32 {
1164 let t = t.clamp(0.0, 1.0);
1165 let luminance = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
1166 let y_over = luminance(overlay);
1167 let y_back = luminance(backdrop);
1168 let contrast = y_over - y_back;
1169 if contrast.abs() < 1e-4 {
1170 return max_alpha * (1.0 - t);
1171 }
1172 // Walk L from the fully-applied composite down to the bare backdrop.
1173 let l = |y: f32| y.max(0.0).cbrt();
1174 let y_full = max_alpha * y_over + (1.0 - max_alpha) * y_back;
1175 let l_t = l(y_full) + (l(y_back) - l(y_full)) * t;
1176 let y_t = l_t * l_t * l_t;
1177 ((y_t - y_back) / contrast).clamp(0.0, max_alpha)
1178 }
1179
1180 pub fn sidebar_bg_color() -> [f32; 4] {
1181 load_colors_once();
1182 let mut color = *SIDEBAR_BG_COLOR.read().unwrap();
1183 if let Some(opacity) = read_opacity_if_configured() {
1184 color[3] = opacity;
1185 }
1186 color
1187 }
1188
1189 pub fn set_sidebar_bg_color(color: [f32; 4]) {
1190 style_write(&SIDEBAR_BG_COLOR, color);
1191 }
1192
1193 pub fn highlight_primary_color() -> [f32; 4] {
1194 load_colors_once();
1195 style_read(&HIGHLIGHT_PRIMARY_COLOR)
1196 }
1197
1198 pub fn set_highlight_primary_color(color: [f32; 4]) {
1199 if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() {
1200 *lock = [color[0], color[1], color[2], 0.12];
1201 }
1202 }
1203
1204 pub fn menubar_tab_label_color() -> [f32; 4] {
1205 load_colors_once();
1206 style_read(&MENUBAR_TAB_LABEL_COLOR)
1207 }
1208
1209 pub fn set_menubar_tab_label_color(color: [f32; 4]) {
1210 style_write(&MENUBAR_TAB_LABEL_COLOR, color);
1211 }
1212
1213 pub fn read_opacity_if_configured() -> Option<f32> {
1214 load_colors_once();
1215 style_read(&OPACITY)
1216 }
1217
1218 /// Tint strength for frosted menus/popovers (`/style/surface/menu/opacity`,
1219 /// default 0.8): the |alpha| of the blur-behind sentinel their plates carry.
1220 pub fn menu_opacity() -> f32 {
1221 load_colors_once();
1222 style_read(&MENU_OPACITY)
1223 }
1224
1225 /// Backdrop compression of frosted menus/popovers
1226 /// (`/style/surface/menu/compression`, default 0.6) — see `MENU_COMPRESSION`.
1227 pub fn menu_compression() -> f32 {
1228 load_colors_once();
1229 style_read(&MENU_COMPRESSION)
1230 }
1231
1232 pub fn set_menu_compression(c: f32) {
1233 style_write(&MENU_COMPRESSION, c.clamp(0.0, 1.0));
1234 }
1235
1236 pub fn read_root_plate_opacity_if_configured() -> Option<f32> {
1237 load_colors_once();
1238 style_read(&ROOT_PLATE_OPACITY)
1239 }
1240
1241 // The toggle's own palette (enabled/disabled/background) is RETIRED: a toggle
1242 // inherits the plate it sits on and marks state with light and relief, so
1243 // there is nothing left to tint. The `TOGGLE_*` consts above survive for the
1244 // graph's node geometry switch, which is a different control.
1245
1246 pub fn list_bg_color() -> [f32; 4] {
1247 load_colors_once();
1248 style_read(&LIST_BG_COLOR)
1249 }
1250
1251 pub fn set_list_bg_color(color: [f32; 4]) {
1252 if let Ok(mut lock) = LIST_BG_COLOR.write() {
1253 *lock = [color[0], color[1], color[2], 0.3];
1254 }
1255 }
1256
1257 pub fn list_entry_bg_color() -> [f32; 4] {
1258 load_colors_once();
1259 style_read(&LIST_ENTRY_BG_COLOR)
1260 }
1261
1262 pub fn set_list_entry_bg_color(color: [f32; 4]) {
1263 style_write(&LIST_ENTRY_BG_COLOR, color);
1264 }
1265
1266 pub fn list_entry_highlight_color() -> [f32; 4] {
1267 load_colors_once();
1268 style_read(&LIST_ENTRY_HIGHLIGHT_COLOR)
1269 }
1270
1271 pub fn set_list_entry_highlight_color(color: [f32; 4]) {
1272 style_write(&LIST_ENTRY_HIGHLIGHT_COLOR, color);
1273 }
1274
1275 pub fn list_font_color() -> [f32; 4] {
1276 load_colors_once();
1277 style_read(&LIST_FONT_COLOR)
1278 }
1279
1280 pub fn set_list_font_color(color: [f32; 4]) {
1281 style_write(&LIST_FONT_COLOR, color);
1282 }
1283
1284 pub fn breadcrumb_bg_color() -> [f32; 4] {
1285 load_colors_once();
1286 style_read(&BREADCRUMB_BG_COLOR)
1287 }
1288
1289 pub fn set_breadcrumb_bg_color(color: [f32; 4]) {
1290 if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
1291 *lock = [color[0], color[1], color[2], 1.0];
1292 }
1293 }
1294
1295 pub fn popover_bg_color() -> [f32; 4] {
1296 load_colors_once();
1297 style_read(&POPOVER_BG_COLOR)
1298 }
1299
1300 pub fn set_popover_bg_color(color: [f32; 4]) {
1301 if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
1302 *lock = [color[0], color[1], color[2], 1.0];
1303 }
1304 }
1305
1306 /// Corner radius of the root plate (`style.surface.plate.root.corner_radius`).
1307 /// The window silhouette value — the compositor clips windows from the SHARED
1308 /// copy of this.
1309 pub fn root_plate_corner_radius() -> f32 {
1310 load_colors_once();
1311 style_read(&ROOT_PLATE_CORNER_RADIUS)
1312 }
1313
1314 pub fn ramp_background_color() -> [f32; 4] {
1315 load_colors_once();
1316 style_read(&RAMP_BACKGROUND_COLOR)
1317 }
1318
1319 pub fn ramp_border_color() -> [f32; 4] {
1320 load_colors_once();
1321 style_read(&RAMP_BORDER_COLOR)
1322 }
1323
1324 pub fn control_panel_color() -> [f32; 4] {
1325 load_colors_once();
1326 style_read(&CONTROL_PANEL_COLOR)
1327 }
1328
1329 pub fn set_control_panel_color(color: [f32; 4]) {
1330 style_write(&CONTROL_PANEL_COLOR, color);
1331 }
1332
1333 pub fn control_panel_border_color() -> [f32; 4] {
1334 load_colors_once();
1335 style_read(&CONTROL_PANEL_BORDER_COLOR)
1336 }
1337
1338 pub fn set_control_panel_border_color(color: [f32; 4]) {
1339 style_write(&CONTROL_PANEL_BORDER_COLOR, color);
1340 }
1341
1342 pub fn progress_bg() -> [f32; 4] {
1343 load_colors_once();
1344 style_read(&PROGRESS_BG_COLOR)
1345 }
1346
1347 pub fn set_progress_bg(color: [f32; 4]) {
1348 style_write(&PROGRESS_BG_COLOR, color);
1349 }
1350
1351 pub fn progress_fill() -> [f32; 4] {
1352 load_colors_once();
1353 style_read(&PROGRESS_FILL_COLOR)
1354 }
1355
1356 pub fn set_progress_fill(color: [f32; 4]) {
1357 style_write(&PROGRESS_FILL_COLOR, color);
1358 }
1359
1360 pub fn button_border_color() -> Option<[f32; 4]> {
1361 load_colors_once();
1362 style_read(&BUTTON_BORDER_COLOR)
1363 }
1364
1365 pub fn set_button_border_color(color: [f32; 4]) {
1366 if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() {
1367 *lock = Some(color);
1368 }
1369 }
1370
1371 pub fn dropdown_border_color() -> [f32; 4] {
1372 load_colors_once();
1373 style_read(&DROPDOWN_BORDER_COLOR)
1374 }
1375
1376 pub fn set_dropdown_border_color(color: [f32; 4]) {
1377 style_write(&DROPDOWN_BORDER_COLOR, color);
1378 }
1379
1380 pub fn dropdown_text_color() -> [f32; 4] {
1381 load_colors_once();
1382 style_read(&DROPDOWN_TEXT_COLOR)
1383 }
1384
1385 pub fn set_dropdown_text_color(color: [f32; 4]) {
1386 style_write(&DROPDOWN_TEXT_COLOR, color);
1387 }
1388
1389
1390 pub fn slider_thumb() -> [f32; 4] {
1391 load_colors_once();
1392 style_read(&SLIDER_THUMB_COLOR)
1393 }
1394
1395 pub fn set_slider_thumb(color: [f32; 4]) {
1396 style_write(&SLIDER_THUMB_COLOR, color);
1397 }
1398
1399 pub fn slider_thumb_drag() -> [f32; 4] {
1400 load_colors_once();
1401 style_read(&SLIDER_THUMB_DRAG_COLOR)
1402 }
1403
1404 pub fn set_slider_thumb_drag(color: [f32; 4]) {
1405 style_write(&SLIDER_THUMB_DRAG_COLOR, color);
1406 }
1407
1408 pub fn rangeslider_thumb() -> [f32; 4] {
1409 load_colors_once();
1410 style_read(&RANGE_SLIDER_THUMB_COLOR)
1411 }
1412
1413 pub fn set_rangeslider_thumb(color: [f32; 4]) {
1414 style_write(&RANGE_SLIDER_THUMB_COLOR, color);
1415 }
1416
1417 pub fn rangeslider_thumb_drag() -> [f32; 4] {
1418 load_colors_once();
1419 style_read(&RANGE_SLIDER_THUMB_DRAG_COLOR)
1420 }
1421
1422 pub fn set_rangeslider_thumb_drag(color: [f32; 4]) {
1423 style_write(&RANGE_SLIDER_THUMB_DRAG_COLOR, color);
1424 }
1425
1426 pub fn spinbox_display() -> [f32; 4] {
1427 load_colors_once();
1428 style_read(&SPINBOX_DISPLAY_COLOR)
1429 }
1430
1431 pub fn set_spinbox_display(color: [f32; 4]) {
1432 style_write(&SPINBOX_DISPLAY_COLOR, color);
1433 }
1434
1435 pub fn spinbox_button() -> [f32; 4] {
1436 load_colors_once();
1437 style_read(&SPINBOX_BUTTON_COLOR)
1438 }
1439
1440 pub fn set_spinbox_button(color: [f32; 4]) {
1441 style_write(&SPINBOX_BUTTON_COLOR, color);
1442 }
1443
1444 pub fn spinbox_button_hover() -> [f32; 4] {
1445 load_colors_once();
1446 style_read(&SPINBOX_BUTTON_HOVER_COLOR)
1447 }
1448
1449 pub fn set_spinbox_button_hover(color: [f32; 4]) {
1450 style_write(&SPINBOX_BUTTON_HOVER_COLOR, color);
1451 }
1452
1453 pub fn spinbox_text_color() -> [f32; 4] {
1454 load_colors_once();
1455 style_read(&SPINBOX_TEXT_COLOR)
1456 }
1457
1458 pub fn set_spinbox_text_color(color: [f32; 4]) {
1459 style_write(&SPINBOX_TEXT_COLOR, color);
1460 }
1461
1462 pub fn set_root_plate_corner_radius(radius: f32) {
1463 style_write(&ROOT_PLATE_CORNER_RADIUS, radius);
1464 }
1465
1466 #[derive(Debug, Clone, Copy)]
1467 pub struct Theme {
1468 pub surface_bg: [f32; 4],
1469 pub surface_border: [f32; 4],
1470 pub primary_accent: [f32; 4],
1471 pub press_overlay: [f32; 4],
1472 pub hover_overlay: [f32; 4],
1473 }
1474
1475 pub fn active_theme() -> Theme {
1476 Theme {
1477 surface_bg: popover_bg_color(),
1478 surface_border: [0.25, 0.25, 0.35, 0.8],
1479 primary_accent: [0.20, 0.50, 0.75, 1.0],
1480 press_overlay: [1.0, 1.0, 1.0, 0.15],
1481 hover_overlay: [1.0, 1.0, 1.0, 0.08],
1482 }
1483 }
1484
1485 pub fn active_window_mode() -> String {
1486 let app_id = crate::scale::app_id();
1487 if app_id.is_empty() {
1488 return "floating".to_string();
1489 }
1490
1491 let config_path = crate::config::get_config_path();
1492 let content = match std::fs::read_to_string(&config_path) {
1493 Ok(c) => c,
1494 Err(_) => return "floating".to_string(),
1495 };
1496 let val = crate::config::parse_kdl_to_json(&content);
1497
1498 if let Some(mode_rules) = val.get("mode_rule").and_then(|r| r.as_array()) {
1499 for rule in mode_rules {
1500 if rule.get("app_id").and_then(|id| id.as_str()) == Some(&app_id) {
1501 if let Some(mode) = rule.get("mode").and_then(|m| m.as_str()) {
1502 return mode.to_string();
1503 }
1504 }
1505 }
1506 }
1507
1508 let mut default_tile_mode = "cascade".to_string();
1509 if let Some(tag_layouts) = val.get("tag_layout").and_then(|l| l.as_array()) {
1510 for tl in tag_layouts {
1511 if tl.get("tag").and_then(|t| t.as_u64()) == Some(1) {
1512 if let Some(m) = tl.get("mode").and_then(|m| m.as_str()) {
1513 default_tile_mode = m.to_string();
1514 }
1515 }
1516 }
1517 }
1518
1519 if crate::scale::is_fullscreen() {
1520 return "fullscreen".to_string();
1521 }
1522 if crate::scale::is_maximized() {
1523 return default_tile_mode;
1524 }
1525
1526 "floating".to_string()
1527 }
1528
1529 /// Opacity of the root plate's fill (the alpha of the root-plate color).
1530 pub fn root_plate_opacity() -> f32 {
1531 page_low_color()[3]
1532 }
1533
1534 pub fn tree_background_color() -> [f32; 4] {
1535 style_read(&TREE_BACKGROUND_COLOR)
1536 }
1537 pub fn tree_border_color() -> [f32; 4] {
1538 style_read(&TREE_BORDER_COLOR)
1539 }
1540 pub fn tree_border_hover_color() -> [f32; 4] {
1541 style_read(&TREE_BORDER_HOVER_COLOR)
1542 }
1543 pub fn tree_border_focus_color() -> [f32; 4] {
1544 style_read(&TREE_BORDER_FOCUS_COLOR)
1545 }
1546
1547 pub fn tree_section_bg_color() -> [f32; 4] {
1548 style_read(&TREE_SECTION_BG_COLOR)
1549 }
1550 pub fn tree_section_bg_hover_color() -> [f32; 4] {
1551 style_read(&TREE_SECTION_BG_HOVER_COLOR)
1552 }
1553
1554 pub fn tree_leaf_bg_even_color() -> [f32; 4] {
1555 style_read(&TREE_LEAF_BG_EVEN_COLOR)
1556 }
1557 pub fn tree_leaf_bg_odd_color() -> [f32; 4] {
1558 style_read(&TREE_LEAF_BG_ODD_COLOR)
1559 }
1560 pub fn tree_leaf_bg_hover_color() -> [f32; 4] {
1561 style_read(&TREE_LEAF_BG_HOVER_COLOR)
1562 }
1563 pub fn tree_leaf_bg_selected_color() -> [f32; 4] {
1564 style_read(&TREE_LEAF_BG_SELECTED_COLOR)
1565 }
1566
1567 pub fn tree_section_text_color() -> [f32; 4] {
1568 style_read(&TREE_SECTION_TEXT_COLOR)
1569 }
1570 pub fn tree_leaf_text_color() -> [f32; 4] {
1571 style_read(&TREE_LEAF_TEXT_COLOR)
1572 }
1573 pub fn tree_leaf_text_selected_color() -> [f32; 4] {
1574 style_read(&TREE_LEAF_TEXT_SELECTED_COLOR)
1575 }
1576
1577 pub fn tree_type_text_color() -> [f32; 4] {
1578 style_read(&TREE_TYPE_TEXT_COLOR)
1579 }
1580 pub fn tree_value_text_color() -> [f32; 4] {
1581 style_read(&TREE_VALUE_TEXT_COLOR)
1582 }
1583 pub fn tree_separator_color() -> [f32; 4] {
1584 style_read(&TREE_SEPARATOR_COLOR)
1585 }
1586
1587 pub fn set_tree_background_color(c: [f32; 4]) {
1588 style_write(&TREE_BACKGROUND_COLOR, c);
1589 }
1590 pub fn set_tree_border_color(c: [f32; 4]) {
1591 style_write(&TREE_BORDER_COLOR, c);
1592 }
1593 pub fn set_tree_border_hover_color(c: [f32; 4]) {
1594 style_write(&TREE_BORDER_HOVER_COLOR, c);
1595 }
1596 pub fn set_tree_border_focus_color(c: [f32; 4]) {
1597 style_write(&TREE_BORDER_FOCUS_COLOR, c);
1598 }
1599
1600 pub fn set_tree_section_bg_color(c: [f32; 4]) {
1601 style_write(&TREE_SECTION_BG_COLOR, c);
1602 }
1603 pub fn set_tree_section_bg_hover_color(c: [f32; 4]) {
1604 style_write(&TREE_SECTION_BG_HOVER_COLOR, c);
1605 }
1606
1607 pub fn set_tree_leaf_bg_even_color(c: [f32; 4]) {
1608 style_write(&TREE_LEAF_BG_EVEN_COLOR, c);
1609 }
1610 pub fn set_tree_leaf_bg_odd_color(c: [f32; 4]) {
1611 style_write(&TREE_LEAF_BG_ODD_COLOR, c);
1612 }
1613 pub fn set_tree_leaf_bg_hover_color(c: [f32; 4]) {
1614 style_write(&TREE_LEAF_BG_HOVER_COLOR, c);
1615 }
1616 pub fn set_tree_leaf_bg_selected_color(c: [f32; 4]) {
1617 style_write(&TREE_LEAF_BG_SELECTED_COLOR, c);
1618 }
1619
1620 pub fn set_tree_section_text_color(c: [f32; 4]) {
1621 style_write(&TREE_SECTION_TEXT_COLOR, c);
1622 }
1623 pub fn set_tree_leaf_text_color(c: [f32; 4]) {
1624 style_write(&TREE_LEAF_TEXT_COLOR, c);
1625 }
1626 pub fn set_tree_leaf_text_selected_color(c: [f32; 4]) {
1627 style_write(&TREE_LEAF_TEXT_SELECTED_COLOR, c);
1628 }
1629
1630 pub fn set_tree_type_text_color(c: [f32; 4]) {
1631 style_write(&TREE_TYPE_TEXT_COLOR, c);
1632 }
1633 pub fn set_tree_value_text_color(c: [f32; 4]) {
1634 style_write(&TREE_VALUE_TEXT_COLOR, c);
1635 }
1636 pub fn set_tree_separator_color(c: [f32; 4]) {
1637 style_write(&TREE_SEPARATOR_COLOR, c);
1638 }
1639
1640 pub fn scrollbar_track_color() -> [f32; 4] {
1641 style_read(&SCROLLBAR_TRACK_COLOR)
1642 }
1643 pub fn set_scrollbar_track_color(c: [f32; 4]) {
1644 style_write(&SCROLLBAR_TRACK_COLOR, c);
1645 }
1646 pub fn scrollbar_thumb_color() -> [f32; 4] {
1647 style_read(&SCROLLBAR_THUMB_COLOR)
1648 }
1649 pub fn set_scrollbar_thumb_color(c: [f32; 4]) {
1650 style_write(&SCROLLBAR_THUMB_COLOR, c);
1651 }
1652
1653 pub fn tree_open_search_key() -> String {
1654 load_colors_once();
1655 let legacy = TREE_OPEN_SEARCH_KEY.read().unwrap().clone();
1656 crate::input::widget_chord("open_search", &legacy, "ctrl+f")
1657 }
1658 pub fn set_tree_open_search_key(k: String) {
1659 style_write(&TREE_OPEN_SEARCH_KEY, k);
1660 }
1661
1662 pub fn list_open_search_key() -> String {
1663 load_colors_once();
1664 let legacy = LIST_OPEN_SEARCH_KEY.read().unwrap().clone();
1665 crate::input::widget_chord("open_search", &legacy, "ctrl+f")
1666 }
1667 pub fn set_list_open_search_key(k: String) {
1668 style_write(&LIST_OPEN_SEARCH_KEY, k);
1669 }
1670
1671 pub fn list_close_search_key() -> String {
1672 load_colors_once();
1673 let legacy = LIST_CLOSE_SEARCH_KEY.read().unwrap().clone();
1674 crate::input::widget_chord("close_search", &legacy, "escape")
1675 }
1676 pub fn set_list_close_search_key(k: String) {
1677 style_write(&LIST_CLOSE_SEARCH_KEY, k);
1678 }
1679
1680 pub fn root_plate_menubar_color() -> [f32; 4] {
1681 load_colors_once();
1682 style_read(&ROOT_PLATE_MENUBAR_COLOR)
1683 }
1684
1685 pub fn set_root_plate_menubar_color(c: [f32; 4]) {
1686 style_write(&ROOT_PLATE_MENUBAR_COLOR, c);
1687 }
1688
1689 pub fn root_plate_menubar_text_color() -> [f32; 4] {
1690 load_colors_once();
1691 style_read(&ROOT_PLATE_MENUBAR_TEXT_COLOR)
1692 }
1693
1694 pub fn set_root_plate_menubar_text_color(c: [f32; 4]) {
1695 style_write(&ROOT_PLATE_MENUBAR_TEXT_COLOR, c);
1696 }
1697
1698 pub fn root_plate_menubar_blur() -> bool {
1699 load_colors_once();
1700 style_read(&ROOT_PLATE_MENUBAR_BLUR)
1701 }
1702
1703 pub fn set_root_plate_menubar_blur(b: bool) {
1704 style_write(&ROOT_PLATE_MENUBAR_BLUR, b);
1705 }
1706
1707 pub fn root_plate_statusbar_color() -> [f32; 4] {
1708 load_colors_once();
1709 style_read(&ROOT_PLATE_STATUSBAR_COLOR)
1710 }
1711
1712 pub fn set_root_plate_statusbar_color(c: [f32; 4]) {
1713 style_write(&ROOT_PLATE_STATUSBAR_COLOR, c);
1714 }
1715
1716 pub fn root_plate_statusbar_text_color() -> [f32; 4] {
1717 load_colors_once();
1718 style_read(&ROOT_PLATE_STATUSBAR_TEXT_COLOR)
1719 }
1720
1721 pub fn set_root_plate_statusbar_text_color(c: [f32; 4]) {
1722 style_write(&ROOT_PLATE_STATUSBAR_TEXT_COLOR, c);
1723 }
1724
1725 pub fn root_plate_statusbar_blur() -> bool {
1726 load_colors_once();
1727 style_read(&ROOT_PLATE_STATUSBAR_BLUR)
1728 }
1729
1730 pub fn set_root_plate_statusbar_blur(b: bool) {
1731 style_write(&ROOT_PLATE_STATUSBAR_BLUR, b);
1732 }
1733
1734 static PLATE_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.15, 0.15, 0.2, 0.95]));
1735 static PLATE_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.3, 0.3, 0.4, 1.0]));
1736 static PLATE_BORDER_THICKNESS: RwLock<f32> = RwLock::new(1.0);
1737 /// Roll width of the beveled plate border (the control_relief replacement for
1738 /// the flat border line) — `style.surface.plate.bevel_width`.
1739 static PLATE_BEVEL_WIDTH: RwLock<f32> = RwLock::new(6.0);
1740
1741 pub fn plate_color() -> Option<[f32; 4]> {
1742 load_colors_once();
1743 style_read(&PLATE_COLOR)
1744 }
1745
1746 pub fn set_plate_color(c: Option<[f32; 4]>) {
1747 style_write(&PLATE_COLOR, c);
1748 }
1749
1750 pub fn plate_border_color() -> Option<[f32; 4]> {
1751 load_colors_once();
1752 style_read(&PLATE_BORDER_COLOR)
1753 }
1754
1755 pub fn set_plate_border_color(c: Option<[f32; 4]>) {
1756 style_write(&PLATE_BORDER_COLOR, c);
1757 }
1758
1759 pub fn plate_border_thickness() -> f32 {
1760 load_colors_once();
1761 style_read(&PLATE_BORDER_THICKNESS)
1762 }
1763
1764 pub fn set_plate_border_thickness(t: f32) {
1765 style_write(&PLATE_BORDER_THICKNESS, t);
1766 }
1767
1768 pub fn plate_bevel_width() -> f32 {
1769 load_colors_once();
1770 style_read(&PLATE_BEVEL_WIDTH)
1771 }
1772
1773 pub fn set_plate_bevel_width(t: f32) {
1774 style_write(&PLATE_BEVEL_WIDTH, t);
1775 }
1776
1777 /// How hard a frosted plate pulls its backdrop's LUMINANCE toward its own key
1778 /// before tinting: 0 = the backdrop passes through untouched, 1 = flat.
1779 ///
1780 /// This is the plate's legibility control, and it is NOT opacity. Blur
1781 /// destroys a backdrop's spatial detail but preserves its mean luminance, and
1782 /// text contrast is a mean-luminance property — so a frosted plate over
1783 /// something bright washes out however hard it is blurred, which is the whole
1784 /// of the "liquid glass" legibility problem. Compression remaps the backdrop's
1785 /// luminance toward the plate's own, symmetrically: a bright backdrop comes
1786 /// down and a DARK one comes up, so the plate stops swinging through the ink's
1787 /// luminance while its hue, chroma and movement still read.
1788 ///
1789 /// Default 0.0 — the behavior every existing config already has.
1790 static PLATE_BACKDROP_COMPRESSION: RwLock<f32> = RwLock::new(0.0);
1791
1792 pub fn plate_backdrop_compression() -> f32 {
1793 load_colors_once();
1794 style_read(&PLATE_BACKDROP_COMPRESSION)
1795 }
1796
1797 pub fn set_plate_backdrop_compression(c: f32) {
1798 style_write(&PLATE_BACKDROP_COMPRESSION, c.clamp(0.0, 1.0));
1799 }
1800
1801 /// How far a frosted plate's roll bends what it samples, and how much clearer
1802 /// its rim is than its frosted body: 0 = a flat window, 1 = full.
1803 ///
1804 /// This buys no legibility and is not meant to — see
1805 /// [`plate_backdrop_compression`] for that. What it buys is the plate reading
1806 /// as an OBJECT: a curved edge displaces the view through it, so the
1807 /// silhouette stops being where the haze ends and becomes where a slab with a
1808 /// thickness begins. The two are complementary, and on a dark desktop
1809 /// especially: compression flattens the body toward the tint, which leaves the
1810 /// rim as the only place the material can still say what it is.
1811 ///
1812 /// Default 0.0 — no existing config changes appearance.
1813 static PLATE_REFRACTION: RwLock<f32> = RwLock::new(0.0);
1814
1815 pub fn plate_refraction() -> f32 {
1816 load_colors_once();
1817 style_read(&PLATE_REFRACTION)
1818 }
1819
1820 pub fn set_plate_refraction(r: f32) {
1821 style_write(&PLATE_REFRACTION, r.clamp(0.0, 1.0));
1822 }
1823
1824 static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
1825
1826 /// The finish's three fixed terms — specular strength, shininess exponent,
1827 /// curvature/AO strength — as `scene::material::Finish::from_style` reads
1828 /// them. Until 2026-09-20 these were literals in the finish constructor
1829 /// (0.4 / 24 / 0.2); the getters exist so the DE's plastic can be edited and
1830 /// so a named material (RFC material, step 4) has somewhere to land. No config
1831 /// path yet: the defaults ARE the shipped look.
1832 static FINISH_SPEC: RwLock<f32> = RwLock::new(0.4);
1833 static FINISH_SHININESS: RwLock<f32> = RwLock::new(24.0);
1834 static FINISH_CURVATURE: RwLock<f32> = RwLock::new(0.2);
1835
1836 /// The default material's blur radius (`style.surface.plate.radius`, the
1837 /// kernel sigma in logical px) — [`crate::scene::Frost::DEFAULT_RADIUS`]
1838 /// unless config says otherwise.
1839 static PLATE_FROST_RADIUS: RwLock<f32> = RwLock::new(crate::scene::material::Frost::DEFAULT_RADIUS);
1840
1841 pub fn plate_frost_radius() -> f32 {
1842 load_colors_once();
1843 style_read(&PLATE_FROST_RADIUS)
1844 }
1845 pub fn set_plate_frost_radius(r: f32) {
1846 style_write(&PLATE_FROST_RADIUS, r.max(0.0));
1847 }
1848
1849 /// The named materials config defines (`style.surface.material { <name> {…} }`)
1850 /// and the rung bindings (`plate material=`, `plate { root material= }`,
1851 /// `control material=`) — see `docs/rfc-material.md` § 5. Replaced wholesale
1852 /// on every config load, so a node or binding removed from config is gone
1853 /// after a reload.
1854 static NAMED_MATERIALS: RwLock<Vec<(String, crate::scene::material::MaterialDef)>> = RwLock::new(Vec::new());
1855 static MATERIAL_BINDINGS: RwLock<[Option<String>; 3]> = RwLock::new([None, None, None]);
1856
1857 fn rung_index(rung: crate::scene::material::PlateRung) -> usize {
1858 match rung {
1859 crate::scene::material::PlateRung::Root => 0,
1860 crate::scene::material::PlateRung::Pane => 1,
1861 crate::scene::material::PlateRung::Control => 2,
1862 }
1863 }
1864
1865 pub fn named_material(name: &str) -> Option<crate::scene::material::MaterialDef> {
1866 load_colors_once();
1867 style_read(&NAMED_MATERIALS).iter().find(|(n, _)| n == name).map(|(_, d)| d.clone())
1868 }
1869
1870 /// Every defined material's name, sorted.
1871 pub fn material_names() -> Vec<String> {
1872 load_colors_once();
1873 let mut v: Vec<String> = style_read(&NAMED_MATERIALS).into_iter().map(|(n, _)| n).collect();
1874 v.sort();
1875 v
1876 }
1877
1878 pub fn set_named_material(name: &str, def: Option<crate::scene::material::MaterialDef>) {
1879 let mut list = style_read(&NAMED_MATERIALS);
1880 list.retain(|(n, _)| n != name);
1881 if let Some(d) = def {
1882 list.push((name.to_string(), d));
1883 }
1884 style_write(&NAMED_MATERIALS, list);
1885 }
1886
1887 pub fn material_binding(rung: crate::scene::material::PlateRung) -> Option<String> {
1888 load_colors_once();
1889 style_read(&MATERIAL_BINDINGS)[rung_index(rung)].clone()
1890 }
1891
1892 pub fn set_material_binding(rung: crate::scene::material::PlateRung, name: Option<String>) {
1893 let mut b = style_read(&MATERIAL_BINDINGS);
1894 b[rung_index(rung)] = name;
1895 style_write(&MATERIAL_BINDINGS, b);
1896 }
1897
1898 pub fn finish_spec() -> f32 {
1899 style_read(&FINISH_SPEC)
1900 }
1901 pub fn set_finish_spec(v: f32) {
1902 style_write(&FINISH_SPEC, v.max(0.0));
1903 }
1904 pub fn finish_shininess() -> f32 {
1905 style_read(&FINISH_SHININESS)
1906 }
1907 pub fn set_finish_shininess(v: f32) {
1908 style_write(&FINISH_SHININESS, v.max(1.0));
1909 }
1910 pub fn finish_curvature() -> f32 {
1911 style_read(&FINISH_CURVATURE)
1912 }
1913 pub fn set_finish_curvature(v: f32) {
1914 style_write(&FINISH_CURVATURE, v.max(0.0));
1915 }
1916
1917 pub fn plate_blur() -> bool {
1918 load_colors_once();
1919 style_read(&PLATE_BLUR)
1920 }
1921
1922 pub fn set_plate_blur(b: bool) {
1923 style_write(&PLATE_BLUR, b);
1924 }
1925
1926 pub fn control_label_color() -> [f32; 4] {
1927 style_read(&CONTROL_LABEL_COLOR)
1928 }
1929
1930 pub fn control_label_color_u8() -> [u8; 3] {
1931 let c = control_label_color();
1932 [
1933 (linear_to_srgb(c[0]) * 255.0).round() as u8,
1934 (linear_to_srgb(c[1]) * 255.0).round() as u8,
1935 (linear_to_srgb(c[2]) * 255.0).round() as u8,
1936 ]
1937 }
1938
1939 pub fn set_control_label_color(color: [f32; 4]) {
1940 style_write(&CONTROL_LABEL_COLOR, color);
1941 }
1942
1943 pub fn control_label_hover_color() -> Option<[f32; 4]> {
1944 style_read(&CONTROL_LABEL_HOVER_COLOR)
1945 }
1946
1947 pub fn control_label_focus_color() -> Option<[f32; 4]> {
1948 style_read(&CONTROL_LABEL_FOCUS_COLOR)
1949 }
1950
1951 pub fn control_label_color_for_state(hovered: bool, focused: bool) -> [u8; 3] {
1952 let c = if focused {
1953 control_label_focus_color().unwrap_or_else(|| control_label_color())
1954 } else if hovered {
1955 control_label_hover_color().unwrap_or_else(|| control_label_color())
1956 } else {
1957 control_label_color()
1958 };
1959 [
1960 (linear_to_srgb(c[0]) * 255.0).round() as u8,
1961 (linear_to_srgb(c[1]) * 255.0).round() as u8,
1962 (linear_to_srgb(c[2]) * 255.0).round() as u8,
1963 ]
1964 }
1965
1966 pub fn control_label_color_detached() -> [f32; 4] {
1967 style_read(&CONTROL_LABEL_COLOR_DETACHED)
1968 }
1969
1970 pub fn control_label_color_detached_u8() -> [u8; 3] {
1971 let c = control_label_color_detached();
1972 [
1973 (linear_to_srgb(c[0]) * 255.0).round() as u8,
1974 (linear_to_srgb(c[1]) * 255.0).round() as u8,
1975 (linear_to_srgb(c[2]) * 255.0).round() as u8,
1976 ]
1977 }
1978
1979 pub fn set_control_label_color_detached(color: [f32; 4]) {
1980 style_write(&CONTROL_LABEL_COLOR_DETACHED, color);
1981 }
1982
1983 pub fn control_label_color_detached_for_state(hovered: bool, focused: bool) -> [u8; 3] {
1984 let c = if focused {
1985 control_label_focus_color().unwrap_or_else(|| control_label_color_detached())
1986 } else if hovered {
1987 control_label_hover_color().unwrap_or_else(|| control_label_color_detached())
1988 } else {
1989 control_label_color_detached()
1990 };
1991 [
1992 (linear_to_srgb(c[0]) * 255.0).round() as u8,
1993 (linear_to_srgb(c[1]) * 255.0).round() as u8,
1994 (linear_to_srgb(c[2]) * 255.0).round() as u8,
1995 ]
1996 }
1997
1998 #[cfg(test)]
1999 mod color_tests {
2000 use super::*;
2001
2002 #[test]
2003 fn test_parse_hex_rgba() {
2004 // 6-digit → alpha 1.0, raw sRGB
2005 assert_eq!(parse_hex_rgba("#ff0000"), Some([1.0, 0.0, 0.0, 1.0]));
2006 // 8-digit → explicit alpha
2007 assert_eq!(parse_hex_rgba("#00ff0080"), Some([0.0, 1.0, 0.0, 128.0 / 255.0]));
2008 // leading '#' optional; quotes/whitespace tolerated
2009 assert_eq!(parse_hex_rgba("\" ffffff \""), Some([1.0, 1.0, 1.0, 1.0]));
2010 assert_eq!(parse_hex_rgba("000000"), Some([0.0, 0.0, 0.0, 1.0]));
2011 // invalid
2012 assert_eq!(parse_hex_rgba("#fff"), None);
2013 assert_eq!(parse_hex_rgba("nothex"), None);
2014 assert_eq!(parse_hex_rgba(""), None);
2015 // rgb drops alpha; linear applies gamma to rgb only
2016 assert_eq!(parse_hex_rgb("#ff0000"), Some([1.0, 0.0, 0.0]));
2017 assert_eq!(parse_hex_rgba_linear("#000000ff"), Some([0.0, 0.0, 0.0, 1.0]));
2018 // byte primitive
2019 assert_eq!(parse_hex_bytes("#010203"), Some([1, 2, 3, 255]));
2020 assert_eq!(parse_hex_bytes("#01020304"), Some([1, 2, 3, 4]));
2021 assert_eq!(parse_hex_bytes("#fff"), None);
2022 }
2023
2024 #[test]
2025 fn perceptual_fade_alpha_walks_lightness_not_luminance() {
2026 let overlay = [0.62, 0.70, 0.95];
2027 let backdrop = [0.028, 0.028, 0.041];
2028 let a = |t: f32| perceptual_fade_alpha(t, overlay, backdrop, 1.0);
2029
2030 // Endpoints are the plain ones, and the ramp only ever falls.
2031 assert!((a(0.0) - 1.0).abs() < 1e-4);
2032 assert!(a(1.0).abs() < 1e-4);
2033 for i in 1..=20 {
2034 assert!(a(i as f32 / 20.0) <= a((i - 1) as f32 / 20.0));
2035 }
2036
2037 // The correction runs BELOW the straight ramp — that ramp's excess brightness
2038 // through the middle is the bow the eye reads as non-linear.
2039 assert!(a(0.5) < 0.5 - 0.1, "midpoint {} should sit well under 0.5", a(0.5));
2040
2041 // What it buys: composited lightness lands on a straight line. cbrt(luminance) is
2042 // the same proxy the implementation uses, checked here end to end through blending.
2043 let lum = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
2044 let l_at = |t: f32| {
2045 let a = a(t);
2046 let y: f32 = a * lum(overlay) + (1.0 - a) * lum(backdrop);
2047 y.cbrt()
2048 };
2049 let (top, bottom) = (l_at(0.0), l_at(1.0));
2050 for i in 0..=10 {
2051 let t = i as f32 / 10.0;
2052 let ideal = top + (bottom - top) * t;
2053 assert!((l_at(t) - ideal).abs() < 1e-3, "t={t}: {} vs {ideal}", l_at(t));
2054 }
2055
2056 // No lightness contrast to shape: falls back to the straight ramp.
2057 let flat = perceptual_fade_alpha(0.5, overlay, overlay, 1.0);
2058 assert!((flat - 0.5).abs() < 1e-4);
2059 }
2060
2061 #[test]
2062 fn test_print_active_config() {
2063 let path = crate::config::get_config_path();
2064 println!("ACTIVE CONFIG PATH: {:?}", path);
2065 if let Ok(content) = std::fs::read_to_string(&path) {
2066 println!("FILE READ OK! Length: {}", content.len());
2067 let val = crate::config::parse_kdl_to_json(&content);
2068 println!("PARSED JSON POINTER: {:?}", val.pointer("/style/control/dropdown/color"));
2069 } else {
2070 println!("FILE READ FAILED!");
2071 }
2072 println!("DROPDOWN COLOR GETTER: {:?}", dropdown_background_color());
2073 println!("LIST FONT COLOR GETTER: {:?}", list_font_color());
2074 println!("PLATE COLOR: {:?}", plate_color());
2075 println!("PLATE BORDER COLOR: {:?}", plate_border_color());
2076 println!("PLATE BORDER THICKNESS: {:?}", plate_border_thickness());
2077 println!("PLATE BLUR: {:?}", plate_blur());
2078 }
2079 }
2080
2081
2082 #[cfg(test)]
2083 mod tests {
2084 use super::*;
2085
2086 /// The hex that pins a built-in colour is NOT its floats times 255.
2087 ///
2088 /// The style loader gamma-decodes every config hex; the constants in this
2089 /// file are already linear. So `PARAM_BG = [0.10, 0.10, 0.14]` is spelled
2090 /// `#595969` in config, and the naive `#1a1a24` decodes to a plate ten
2091 /// times darker — silently, both being valid config. Pinned here because
2092 /// it cost a measurement round: a sweep meant to hold the tint constant
2093 /// was quietly sweeping it, and the two halves disagreed by 3x.
2094 #[test]
2095 fn the_hex_that_pins_a_default_round_trips_through_the_gamma_decode() {
2096 let decoded = parse_hex_rgba_linear("#595969").expect("valid hex");
2097 // RGB only — a 6-digit hex carries no alpha, which the asserts at the
2098 // bottom cover as its own hazard.
2099 for (got, want) in decoded.iter().take(3).zip(PARAM_BG.iter().take(3)) {
2100 assert!(
2101 (got - want).abs() < 0.005,
2102 "#595969 decodes to {decoded:?}, PARAM_BG is {PARAM_BG:?}"
2103 );
2104 }
2105
2106 // The naive spelling, and how far off it lands.
2107 let naive = parse_hex_rgba_linear("#1a1a24").expect("valid hex");
2108 assert!(
2109 naive[0] < PARAM_BG[0] / 5.0,
2110 "#1a1a24 was supposed to be nowhere near PARAM_BG, got {naive:?}"
2111 );
2112
2113 // Alpha is NOT decoded, and a 6-digit hex means OPAQUE — dropping the
2114 // last byte off a translucent plate colour does not leave it alone.
2115 assert!((parse_hex_rgba_linear("#05050840").unwrap()[3] - 0.251).abs() < 0.002);
2116 assert_eq!(parse_hex_rgba_linear("#050508").unwrap()[3], 1.0);
2117 }
2118
2119 /// Rim refraction defaults OFF and clamps, like its neighbour.
2120 #[test]
2121 fn plate_refraction_defaults_off_and_clamps() {
2122 assert_eq!(plate_refraction(), 0.0, "off unless a config asks");
2123 set_plate_refraction(0.6);
2124 assert_eq!(plate_refraction(), 0.6);
2125 set_plate_refraction(9.0);
2126 assert_eq!(plate_refraction(), 1.0);
2127 set_plate_refraction(-0.5);
2128 assert_eq!(plate_refraction(), 0.0);
2129 }
2130
2131 /// The plate's backdrop compression defaults OFF and clamps.
2132 ///
2133 /// Off is load-bearing: it is the behaviour every config already in the
2134 /// wild has, and a toolkit-wide default that changed how every frosted
2135 /// surface in the DE looks would arrive unannounced in eighteen apps.
2136 /// Opting in is a per-app `style.surface.plate.backdrop_compression`.
2137 #[test]
2138 fn backdrop_compression_defaults_off_and_clamps() {
2139 assert_eq!(plate_backdrop_compression(), 0.0, "off unless a config asks");
2140
2141 set_plate_backdrop_compression(0.85);
2142 assert_eq!(plate_backdrop_compression(), 0.85);
2143
2144 // The shader clamps too, but a nonsense config value should not be
2145 // able to reach it and make the plate flat or inverted.
2146 set_plate_backdrop_compression(4.0);
2147 assert_eq!(plate_backdrop_compression(), 1.0);
2148 set_plate_backdrop_compression(-1.0);
2149 assert_eq!(plate_backdrop_compression(), 0.0);
2150 }
2151
2152 /// A colour pinned by one test is invisible to a test beside it.
2153 ///
2154 /// The invariant that ends the parallel-flake class here. These statics
2155 /// are process-wide, and `test_graph_style_configuration` pins a dozen of
2156 /// them without restoring any; before `style_write` became per-thread
2157 /// under `cfg(test)`, every test running alongside it saw those values.
2158 #[test]
2159 fn a_pinned_colour_is_private_to_its_thread() {
2160 let base = node_color();
2161 let pinned = [0.123, 0.456, 0.789, 1.0];
2162 assert_ne!(base, pinned, "pick a value the config cannot already hold");
2163
2164 set_node_color(pinned);
2165 assert_eq!(node_color(), pinned, "the pinning thread sees its own value");
2166
2167 let elsewhere = std::thread::spawn(node_color).join().unwrap();
2168 assert_eq!(elsewhere, base, "a thread beside it must still see the shared base");
2169 }
2170 }