GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
src/color/load.rs (28.9K)
1 //! Loading the colour and surface config: reading `config.kdl` into the colour statics
2 //! (`parse_and_set_colors`), reporting retired surface keys, and the reload entry points.
3
4 use super::*;
5
6 fn read_config() -> Option<String> {
7 let path = crate::config::get_config_path();
8 if let Ok(content) = std::fs::read_to_string(&path) {
9 return Some(content);
10 }
11 None
12 }
13
14 /// Load the colour and surface config as one change of style (`crate::style::batch`): a
15 /// frame drawn meanwhile sees the style before or after, never half of each.
16 fn parse_and_set_colors(content: &str) {
17 crate::style::batch(|| parse_and_set_colors_in_batch(content));
18 }
19
20 fn parse_and_set_colors_in_batch(content: &str) {
21 let val = crate::config::parse_kdl_to_json(content);
22
23 let parse_hex = |hex_str: &str| -> Option<[f32; 4]> {
24 let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
25 let hex = hex.trim_start_matches('#');
26 if hex.len() >= 8 {
27 if let (Ok(r), Ok(g), Ok(b), Ok(a)) = (
28 u8::from_str_radix(&hex[0..2], 16),
29 u8::from_str_radix(&hex[2..4], 16),
30 u8::from_str_radix(&hex[4..6], 16),
31 u8::from_str_radix(&hex[6..8], 16),
32 ) {
33 let r_f = srgb_to_linear(r as f32 / 255.0);
34 let g_f = srgb_to_linear(g as f32 / 255.0);
35 let b_f = srgb_to_linear(b as f32 / 255.0);
36 let a_f = a as f32 / 255.0;
37 return Some([r_f, g_f, b_f, a_f]);
38 }
39 } else if hex.len() >= 6 {
40 if let (Ok(r), Ok(g), Ok(b)) = (
41 u8::from_str_radix(&hex[0..2], 16),
42 u8::from_str_radix(&hex[2..4], 16),
43 u8::from_str_radix(&hex[4..6], 16),
44 ) {
45 let r_f = srgb_to_linear(r as f32 / 255.0);
46 let g_f = srgb_to_linear(g as f32 / 255.0);
47 let b_f = srgb_to_linear(b as f32 / 255.0);
48 return Some([r_f, g_f, b_f, 1.0]);
49 }
50 }
51 None
52 };
53
54 let get_color = |pointer: &str| -> Option<[f32; 4]> {
55 val.pointer(pointer).and_then(|v| v.as_str()).and_then(parse_hex)
56 };
57
58 let get_color_u8 = |pointer: &str| -> Option<[u8; 3]> {
59 val.pointer(pointer).and_then(|v| v.as_str()).and_then(|hex_str| {
60 let hex = hex_str.trim_matches(|c| c == '"' || c == '\'' || c == ' ');
61 let hex = hex.trim_start_matches('#');
62 if hex.len() >= 6 {
63 if let (Ok(r), Ok(g), Ok(b)) = (
64 u8::from_str_radix(&hex[0..2], 16),
65 u8::from_str_radix(&hex[2..4], 16),
66 u8::from_str_radix(&hex[4..6], 16),
67 ) {
68 return Some([r, g, b]);
69 }
70 }
71 None
72 })
73 };
74
75 if let Some(opacity) = val.pointer("/layout/menubar_opacity").and_then(|v| v.as_f64()) {
76 if let Ok(mut lock) = OPACITY.write() {
77 *lock = Some(opacity as f32);
78 }
79 }
80
81 // `/style/surface/plate/root/...` is the one spelling of the root-plate
82 // style (RFC Phase 7a; the legacy `root plate` read-alias was removed
83 // 2026-09-06 once every live config had migrated).
84 if let Some(radius) = val.pointer("/style/surface/plate/root/corner_radius")
85 .and_then(|v| v.as_f64())
86 {
87 if let Ok(mut lock) = ROOT_PLATE_CORNER_RADIUS.write() {
88 *lock = radius as f32;
89 }
90 }
91
92 if let Some(c) = get_color("/style/surface/plate/root/color") {
93 if let Ok(mut lock) = PAGE_LOW_COLOR.write() { *lock = c; }
94 if let Ok(mut lock) = ROOT_PLATE_OPACITY.write() { *lock = Some(c[3]); }
95 }
96
97 if let Some(c) = get_color("/style/surface/plate/root/menubar/color") {
98 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_COLOR.write() { *lock = c; }
99 }
100 if let Some(c) = get_color("/style/surface/plate/root/menubar/text_color") {
101 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_TEXT_COLOR.write() { *lock = c; }
102 }
103 if let Some(o) = val.pointer("/style/surface/menu/opacity").and_then(|v| v.as_f64()) {
104 if let Ok(mut lock) = MENU_OPACITY.write() { *lock = (o as f32).clamp(0.0, 1.0); }
105 }
106 if let Some(c) = val.pointer("/style/surface/menu/compression").and_then(|v| v.as_f64()) {
107 if let Ok(mut lock) = MENU_COMPRESSION.write() { *lock = (c as f32).clamp(0.0, 1.0); }
108 }
109 if let Some(c) = get_color("/style/surface/menu/color") {
110 if let Ok(mut lock) = MENU_COLOR.write() { *lock = Some(c); }
111 }
112
113 let menubar_blur_ptr = val.pointer("/style/surface/plate/root/menubar/blur");
114 if let Some(blur) = menubar_blur_ptr.and_then(|v| v.as_bool()) {
115 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur; }
116 } else if let Some(blur_val) = menubar_blur_ptr.and_then(|v| v.as_f64()) {
117 if let Ok(mut lock) = ROOT_PLATE_MENUBAR_BLUR.write() { *lock = blur_val > 0.001; }
118 }
119
120 if let Some(c) = get_color("/style/surface/statusbar/color") {
121 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_COLOR.write() { *lock = c; }
122 }
123 if let Some(c) = get_color("/style/surface/statusbar/text_color") {
124 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_TEXT_COLOR.write() { *lock = c; }
125 }
126 if let Some(blur) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_bool()) {
127 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur; }
128 } else if let Some(blur_val) = val.pointer("/style/surface/statusbar/blur").and_then(|v| v.as_f64()) {
129 if let Ok(mut lock) = ROOT_PLATE_STATUSBAR_BLUR.write() { *lock = blur_val > 0.001; }
130 }
131 if let Some(c) = get_color("/layout/color_borders_color") {
132 if let Ok(mut lock) = COLOR_BORDERS_COLOR.write() { *lock = c; }
133 }
134 if let Some(c) = get_color("/layout/paginator_sidebar_color") {
135 if let Ok(mut lock) = SIDEBAR_BG_COLOR.write() { *lock = c; }
136 }
137 if let Some(c) = get_color("/style/highlight/primary") {
138 if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() { *lock = [c[0], c[1], c[2], 0.12]; }
139 }
140 if let Some(c) = get_color("/style/control/button/background").or_else(|| get_color("/layout/button_background_color")) {
141 if let Ok(mut lock) = BUTTON_BACKGROUND_COLOR.write() { *lock = c; }
142 }
143 if let Some(c) = get_color("/style/control/dropdown/color") {
144 if let Ok(mut lock) = DROPDOWN_BACKGROUND_COLOR.write() { *lock = c; }
145 }
146 if let Some(c) = get_color_u8("/style/control/textbox/placeholder_text_color")
147 .or_else(|| get_color_u8("/style/textbox/placeholder_text_color"))
148 .or_else(|| get_color_u8("/style/data/textbox/placeholder_text_color")) {
149 if let Ok(mut lock) = TEXTBOX_PLACEHOLDER_TEXT_COLOR.write() { *lock = c; }
150 }
151 if let Some(c) = get_color("/style/control/textbox/background_color")
152 .or_else(|| get_color("/style/textbox/background_color"))
153 .or_else(|| get_color("/style/data/textbox/background_color")) {
154 if let Ok(mut lock) = TEXTBOX_BACKGROUND_COLOR.write() { *lock = c; }
155 }
156 if let Some(c) = get_color("/style/control/textbox/background_edit_color")
157 .or_else(|| get_color("/style/textbox/background_edit_color"))
158 .or_else(|| get_color("/style/data/textbox/background_edit_color")) {
159 if let Ok(mut lock) = TEXTBOX_BACKGROUND_EDIT_COLOR.write() { *lock = c; }
160 }
161 if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
162 if let Ok(mut lock) = MENUBAR_TAB_LABEL_COLOR.write() { *lock = c; }
163 }
164 if let Some(c) = get_color("/style/control/label/color") {
165 if let Ok(mut lock) = CONTROL_LABEL_COLOR.write() { *lock = c; }
166 }
167 if let Some(c) = get_color("/style/control/label/color_detached") {
168 if let Ok(mut lock) = CONTROL_LABEL_COLOR_DETACHED.write() { *lock = c; }
169 }
170 if let Some(c) = get_color("/style/control/label/hover_color") {
171 if let Ok(mut lock) = CONTROL_LABEL_HOVER_COLOR.write() { *lock = Some(c); }
172 }
173 if let Some(c) = get_color("/style/control/label/focus_color") {
174 if let Ok(mut lock) = CONTROL_LABEL_FOCUS_COLOR.write() { *lock = Some(c); }
175 }
176 // The toggle state colors (enabled/disabled/gradient/background) are not
177 // read: the control inherits its plate — see the note by the retired
178 // getters below.
179 if let Some(c) = get_color("/style/control/ramp/background") {
180 if let Ok(mut lock) = RAMP_BACKGROUND_COLOR.write() { *lock = c; }
181 }
182 if let Some(c) = get_color("/style/control/ramp/border_color") {
183 if let Ok(mut lock) = RAMP_BORDER_COLOR.write() { *lock = c; }
184 }
185 if let Some(c) = get_color("/style/control/control_panel/color") {
186 if let Ok(mut lock) = CONTROL_PANEL_COLOR.write() { *lock = c; }
187 }
188 if let Some(c) = get_color("/style/control/control_panel/border_color") {
189 if let Ok(mut lock) = CONTROL_PANEL_BORDER_COLOR.write() { *lock = c; }
190 }
191 if let Some(c) = get_color("/style/control/spinbox/background_color") {
192 if let Ok(mut lock) = SPINBOX_DISPLAY_COLOR.write() { *lock = c; }
193 }
194 if let Some(c) = get_color("/style/control/spinbox/button_color") {
195 if let Ok(mut lock) = SPINBOX_BUTTON_COLOR.write() { *lock = c; }
196 if let Ok(mut lock_hover) = SPINBOX_BUTTON_HOVER_COLOR.write() {
197 *lock_hover = [
198 (c[0] + 0.1).min(1.0),
199 (c[1] + 0.1).min(1.0),
200 (c[2] + 0.1).min(1.0),
201 c[3]
202 ];
203 }
204 }
205 if let Some(c) = get_color("/style/control/spinbox/button_hover_color") {
206 if let Ok(mut lock) = SPINBOX_BUTTON_HOVER_COLOR.write() { *lock = c; }
207 }
208 if let Some(c) = get_color("/style/control/spinbox/text_color") {
209 if let Ok(mut lock) = SPINBOX_TEXT_COLOR.write() { *lock = c; }
210 }
211 if let Some(c) = get_color("/style/control/button/border_color").or_else(|| get_color("/style/button/border_color")) {
212 if let Ok(mut lock) = BUTTON_BORDER_COLOR.write() { *lock = Some(c); }
213 }
214 if let Some(c) = get_color("/style/control/dropdown/border_color").or_else(|| get_color("/style/dropdown/border_color")) {
215 if let Ok(mut lock) = DROPDOWN_BORDER_COLOR.write() { *lock = c; }
216 }
217 if let Some(c) = get_color("/style/control/dropdown/text_color").or_else(|| get_color("/style/dropdown/text_color")) {
218 if let Ok(mut lock) = DROPDOWN_TEXT_COLOR.write() { *lock = c; }
219 }
220 if let Some(c) = get_color("/style/control/slider/thumb_color").or_else(|| get_color("/style/slider/thumb_color")) {
221 if let Ok(mut lock) = SLIDER_THUMB_COLOR.write() { *lock = c; }
222 if let Ok(mut lock_drag) = SLIDER_THUMB_DRAG_COLOR.write() {
223 *lock_drag = [
224 (c[0] + 0.15).min(1.0),
225 (c[1] + 0.15).min(1.0),
226 (c[2] + 0.15).min(1.0),
227 c[3]
228 ];
229 }
230 }
231 if let Some(c) = get_color("/style/control/rangeslider/thumb_color").or_else(|| get_color("/style/rangeslider/thumb_color")) {
232 if let Ok(mut lock) = RANGE_SLIDER_THUMB_COLOR.write() { *lock = c; }
233 if let Ok(mut lock_drag) = RANGE_SLIDER_THUMB_DRAG_COLOR.write() {
234 *lock_drag = [
235 (c[0] + 0.15).min(1.0),
236 (c[1] + 0.15).min(1.0),
237 (c[2] + 0.15).min(1.0),
238 c[3]
239 ];
240 }
241 }
242 if let Some(c) = get_color("/style/control/progressbar/background") {
243 if let Ok(mut lock) = PROGRESS_BG_COLOR.write() { *lock = c; }
244 }
245 if let Some(c) = get_color("/style/control/progressbar/fill") {
246 if let Ok(mut lock) = PROGRESS_FILL_COLOR.write() { *lock = c; }
247 }
248 let parsed_list_bg = get_color("/layout/list_bg_color");
249 let parsed_breadcrumb_bg = get_color("/layout/breadcrumb_bg_color");
250 let parsed_popover_bg = get_color("/layout/popover_bg_color");
251
252 if let Some(c) = get_color("/layout/page_color") {
253 if let Ok(mut lock) = PAGE_COLOR.write() { *lock = c; }
254 }
255 if let Some(c) = get_color("/layout/layer_color") {
256 if let Ok(mut lock) = LAYER_COLOR.write() { *lock = c; }
257 }
258
259 if let Some(c) = parsed_list_bg {
260 if let Ok(mut lock) = LIST_BG_COLOR.write() {
261 *lock = [c[0], c[1], c[2], 0.3];
262 }
263 }
264 if let Some(c) = get_color("/layout/list_entry_bg_color") {
265 if let Ok(mut lock) = LIST_ENTRY_BG_COLOR.write() {
266 *lock = c;
267 }
268 }
269 if let Some(c) = get_color("/layout/list_entry_highlight_color") {
270 if let Ok(mut lock) = LIST_ENTRY_HIGHLIGHT_COLOR.write() {
271 *lock = c;
272 }
273 }
274 if let Some(c) = get_color("/style/data/list/font_color").or_else(|| get_color("/layout/list_font_color")) {
275 if let Ok(mut lock) = LIST_FONT_COLOR.write() {
276 *lock = c;
277 }
278 }
279 if let Some(c) = parsed_breadcrumb_bg {
280 if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
281 *lock = [c[0], c[1], c[2], 1.0];
282 }
283 } else if let Some(c) = parsed_list_bg {
284 if let Ok(mut lock) = BREADCRUMB_BG_COLOR.write() {
285 *lock = [c[0], c[1], c[2], 1.0];
286 }
287 }
288 if let Some(c) = parsed_popover_bg {
289 if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
290 *lock = [c[0], c[1], c[2], 1.0];
291 }
292 } else if let Some(c) = parsed_list_bg {
293 if let Ok(mut lock) = POPOVER_BG_COLOR.write() {
294 *lock = [c[0], c[1], c[2], 1.0];
295 }
296 }
297
298 // The lattice's lines. The cells between them have no colour of their
299 // own: a graph shows whatever it is painted on. `cell_color` and
300 // `gap_color`, the cell model's pair, are retired (`retired_surface_keys`).
301 if let Some(c) = get_color("/style/surface/graph/grid_color") {
302 if let Ok(mut lock) = GRAPH_GRID_COLOR.write() { *lock = [c[0], c[1], c[2]]; }
303 }
304 if let Some(c) = get_color("/style/surface/graph/node/color") {
305 if let Ok(mut lock) = GRAPH_NODE_COLOR.write() { *lock = c; }
306 if let Ok(mut lock) = NODE_COLOR.write() { *lock = c; }
307 }
308 if let Some(c) = get_color("/style/surface/graph/node/selected_color") {
309 if let Ok(mut lock) = GRAPH_NODE_SELECTED_COLOR.write() { *lock = c; }
310 if let Ok(mut lock) = NODE_SELECTED_COLOR.write() { *lock = c; }
311 }
312 if let Some(c) = get_color("/style/surface/graph/node/drag_color") {
313 if let Ok(mut lock) = GRAPH_NODE_DRAG_COLOR.write() { *lock = c; }
314 if let Ok(mut lock) = NODE_DRAG_COLOR.write() { *lock = c; }
315 }
316 if let Some(c) = get_color("/style/surface/graph/node/wire_color") {
317 if let Ok(mut lock) = GRAPH_WIRE_COLOR.write() { *lock = c; }
318 }
319 if let Some(c) = get_color("/style/surface/graph/node/wire_highlight_color") {
320 if let Ok(mut lock) = GRAPH_WIRE_HIGHLIGHT_COLOR.write() { *lock = c; }
321 }
322 if let Some(c) = get_color("/style/surface/graph/node/connector_color") {
323 if let Ok(mut lock) = GRAPH_CONNECTOR_COLOR.write() { *lock = c; }
324 }
325 if let Some(c) = get_color("/style/surface/graph/node/connector_highlight_color") {
326 if let Ok(mut lock) = GRAPH_CONNECTOR_HIGHLIGHT_COLOR.write() { *lock = c; }
327 }
328 if let Some(opacity) = val.pointer("/style/surface/graph/opacity").and_then(|v| v.as_f64()) {
329 if let Ok(mut lock) = GRAPH_OPACITY.write() { *lock = opacity as f32; }
330 }
331 if let Some(opacity) = val.pointer("/style/surface/graph/node/opacity").and_then(|v| v.as_f64()) {
332 if let Ok(mut lock) = GRAPH_NODE_OPACITY.write() { *lock = opacity as f32; }
333 }
334
335 if let Some(c) = get_color("/style/data/tree/background_color") {
336 if let Ok(mut lock) = TREE_BACKGROUND_COLOR.write() { *lock = c; }
337 }
338 if let Some(c) = get_color("/style/data/tree/border_color") {
339 if let Ok(mut lock) = TREE_BORDER_COLOR.write() { *lock = c; }
340 }
341 if let Some(c) = get_color("/style/data/tree/border_hover_color") {
342 if let Ok(mut lock) = TREE_BORDER_HOVER_COLOR.write() { *lock = c; }
343 }
344 if let Some(c) = get_color("/style/data/tree/border_focus_color") {
345 if let Ok(mut lock) = TREE_BORDER_FOCUS_COLOR.write() { *lock = c; }
346 }
347 if let Some(c) = get_color("/style/data/tree/section_bg_color") {
348 if let Ok(mut lock) = TREE_SECTION_BG_COLOR.write() { *lock = c; }
349 }
350 if let Some(c) = get_color("/style/data/tree/section_bg_hover_color") {
351 if let Ok(mut lock) = TREE_SECTION_BG_HOVER_COLOR.write() { *lock = c; }
352 }
353 if let Some(c) = get_color("/style/data/tree/leaf_bg_even_color") {
354 if let Ok(mut lock) = TREE_LEAF_BG_EVEN_COLOR.write() { *lock = c; }
355 }
356 if let Some(c) = get_color("/style/data/tree/leaf_bg_odd_color") {
357 if let Ok(mut lock) = TREE_LEAF_BG_ODD_COLOR.write() { *lock = c; }
358 }
359 if let Some(c) = get_color("/style/data/tree/leaf_bg_hover_color") {
360 if let Ok(mut lock) = TREE_LEAF_BG_HOVER_COLOR.write() { *lock = c; }
361 }
362 if let Some(c) = get_color("/style/data/tree/leaf_bg_selected_color") {
363 if let Ok(mut lock) = TREE_LEAF_BG_SELECTED_COLOR.write() { *lock = c; }
364 }
365 if let Some(c) = get_color("/style/data/tree/section_text_color") {
366 if let Ok(mut lock) = TREE_SECTION_TEXT_COLOR.write() { *lock = c; }
367 }
368 if let Some(c) = get_color("/style/data/tree/leaf_text_color") {
369 if let Ok(mut lock) = TREE_LEAF_TEXT_COLOR.write() { *lock = c; }
370 }
371 if let Some(c) = get_color("/style/data/tree/leaf_text_selected_color") {
372 if let Ok(mut lock) = TREE_LEAF_TEXT_SELECTED_COLOR.write() { *lock = c; }
373 }
374 if let Some(c) = get_color("/style/data/tree/type_text_color") {
375 if let Ok(mut lock) = TREE_TYPE_TEXT_COLOR.write() { *lock = c; }
376 }
377 if let Some(c) = get_color("/style/data/tree/value_text_color") {
378 if let Ok(mut lock) = TREE_VALUE_TEXT_COLOR.write() { *lock = c; }
379 }
380 if let Some(c) = get_color("/style/data/tree/separator_color") {
381 if let Ok(mut lock) = TREE_SEPARATOR_COLOR.write() { *lock = c; }
382 }
383 if let Some(k) = val.pointer("/style/data/tree/open_search").and_then(|v| v.as_str()) {
384 if let Ok(mut lock) = TREE_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
385 }
386 if let Some(k) = val.pointer("/style/data/list/open_search").and_then(|v| v.as_str()) {
387 if let Ok(mut lock) = LIST_OPEN_SEARCH_KEY.write() { *lock = k.to_string(); }
388 }
389 if let Some(k) = val.pointer("/style/data/list/close_search").and_then(|v| v.as_str()) {
390 if let Ok(mut lock) = LIST_CLOSE_SEARCH_KEY.write() { *lock = k.to_string(); }
391 }
392 if let Some(c) = get_color("/style/control/scrollbar/track_color") {
393 if let Ok(mut lock) = SCROLLBAR_TRACK_COLOR.write() { *lock = c; }
394 }
395 if let Some(c) = get_color("/style/control/scrollbar/thumb_color") {
396 if let Ok(mut lock) = SCROLLBAR_THUMB_COLOR.write() { *lock = c; }
397 }
398 if let Some(c) = get_color("/style/surface/plate/color") {
399 if let Ok(mut lock) = PLATE_COLOR.write() { *lock = Some(c); }
400 }
401 if let Some(c) = get_color("/style/surface/plate/border_color") {
402 if let Ok(mut lock) = PLATE_BORDER_COLOR.write() { *lock = Some(c); }
403 }
404 if let Some(t) = val.pointer("/style/surface/plate/border_thickness").and_then(|v| v.as_f64()) {
405 if let Ok(mut lock) = PLATE_BORDER_THICKNESS.write() { *lock = t as f32; }
406 }
407 if let Some(c) = get_color("/style/surface/param/color") {
408 if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
409 if let Ok(mut lock) = PANE_COLOR_WHOLE.write() { *lock = false; }
410 }
411 // The clear spelling of the same tint: the PANE plate's colour, whole —
412 // its alpha is the tint strength and nothing multiplies it.
413 if let Some(c) = get_color("/style/surface/plate/pane/color") {
414 if let Ok(mut lock) = PARAM_BG_COLOR.write() { *lock = c; }
415 if let Ok(mut lock) = PANE_COLOR_WHOLE.write() { *lock = true; }
416 }
417 // The default plate's frost is ONE block, the same shape a named
418 // material's `frost` child has: `plate { frost radius=5.5 compression=0
419 // refraction=0 }` is frosted with those knobs, a bare `frost` is frosted
420 // at the defaults, `frost (bool)false` is not frosted. The four scattered
421 // keys it replaced on 2026-09-28 (`blur` as the switch, `radius`,
422 // `backdrop_compression`, `refraction`) were read as aliases for the rest
423 // of that day and are retired: a config still carrying one is reported
424 // (`retired_surface_keys`) and the key does nothing, because an alias that
425 // keeps working is a second spelling forever. The block is absent → not
426 // frosted, which is what `blur=true` alone now amounts to; the report
427 // is what tells the user why their plates went sharp.
428 let retired = retired_surface_keys(&val);
429 if !retired.is_empty() {
430 log::warn!(
431 "retired style.surface keys in config: {} — the frost is `plate {{ frost radius= compression= refraction= }}` (a material's `frost` child spells `compression`), every roll's width is `relief width=`, the light strength is `relief light=` (a material's `finish light=`), and the relief's geometry is `relief {{ wall height= profile= ; edge height= profile= }}` (the window_manager bevel_* spellings are the same relief keys, and its bevel_shader is `relief shader=`); a graph's lines are `graph {{ grid_color }}` and its cells take no colour",
432 retired.join(", ")
433 );
434 }
435 match val.pointer("/style/surface/plate/frost") {
436 Some(serde_json::Value::Object(fo)) => {
437 let num = |k: &str| fo.get(k).and_then(|v| v.as_f64()).map(|v| v as f32);
438 if let Ok(mut lock) = PLATE_BLUR.write() { *lock = true; }
439 if let Some(c) = num("compression") {
440 if let Ok(mut lock) = PLATE_BACKDROP_COMPRESSION.write() { *lock = c.clamp(0.0, 1.0); }
441 }
442 if let Some(r) = num("refraction") {
443 if let Ok(mut lock) = PLATE_REFRACTION.write() { *lock = r.clamp(0.0, 1.0); }
444 }
445 if let Some(r) = num("radius") {
446 if let Ok(mut lock) = PLATE_FROST_RADIUS.write() { *lock = r.max(0.0); }
447 }
448 }
449 Some(serde_json::Value::Null) => {
450 if let Ok(mut lock) = PLATE_BLUR.write() { *lock = true; }
451 }
452 Some(serde_json::Value::Bool(on)) => {
453 if let Ok(mut lock) = PLATE_BLUR.write() { *lock = *on; }
454 }
455 _ => {}
456 }
457
458 // The DE finish beyond its strength (`relief.depth` / `light`, which
459 // lives in the style registry): the three terms that were literals.
460 let f = |k: &str| val.pointer(&format!("/style/surface/relief/{k}")).and_then(|v| v.as_f64()).map(|v| v as f32);
461 if let Some(v) = f("spec") {
462 if let Ok(mut lock) = FINISH_SPEC.write() { *lock = v.max(0.0); }
463 }
464 if let Some(v) = f("shininess") {
465 if let Ok(mut lock) = FINISH_SHININESS.write() { *lock = v.max(1.0); }
466 }
467 if let Some(v) = f("curvature") {
468 if let Ok(mut lock) = FINISH_CURVATURE.write() { *lock = v.max(0.0); }
469 }
470
471 // Named materials and the rung bindings (RFC material § 5), replaced
472 // wholesale so a reload forgets what config no longer says.
473 {
474 use crate::scene::material::{FrostDef, MaterialDef};
475 let num = |v: Option<&serde_json::Value>| v.and_then(|v| v.as_f64()).map(|v| v as f32);
476 let mut map: Vec<(String, MaterialDef)> = Vec::new();
477 if let Some(obj) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
478 for (name, node) in obj {
479 let Some(node) = node.as_object() else { continue };
480 let frost = node.get("frost").map(|fr| match fr.as_object() {
481 Some(fo) => FrostDef {
482 compression: num(fo.get("compression")),
483 refraction: num(fo.get("refraction")),
484 radius: num(fo.get("radius")),
485 },
486 // A bare `frost` node (no knobs) is frosted at the defaults.
487 None => FrostDef::default(),
488 });
489 let fin = node.get("finish").and_then(|v| v.as_object());
490 let fk = |k: &str| fin.and_then(|fo| num(fo.get(k)));
491 map.push((
492 name.clone(),
493 MaterialDef {
494 tint: node.get("color").and_then(|v| v.as_str()).and_then(parse_hex),
495 frost,
496 light: fk("light"),
497 spec: fk("spec"),
498 shininess: fk("shininess"),
499 curvature: fk("curvature"),
500 },
501 ));
502 }
503 }
504 if let Ok(mut lock) = NAMED_MATERIALS.write() { *lock = map; }
505 let bind = |p: &str| val.pointer(p).and_then(|v| v.as_str()).map(|s| s.to_string()).filter(|s| !s.is_empty());
506 let bindings = [
507 bind("/style/surface/plate/root/material"),
508 bind("/style/surface/plate/material"),
509 bind("/style/control/material"),
510 ];
511 if let Ok(mut lock) = MATERIAL_BINDINGS.write() { *lock = bindings; }
512 }
513 }
514
515 /// The retired `style.surface` spellings a parsed config still carries, as
516 /// the dotted paths a user would grep for: the four flat frost keys under
517 /// `plate` (`blur`, `radius`, `backdrop_compression`, `refraction`),
518 /// `backdrop_compression` inside any `frost` child (the plate's, or a named
519 /// material's), whose one name is `compression`; `plate.bevel_width`, the
520 /// pane roll's former width of its own (every roll is `relief.width`); and
521 /// `relief.depth` with `depth` inside any material's `finish` child, the
522 /// light strength's former name (`light`, since it is not a length); and
523 /// the relief's flat geometry keys `height` / `profile` (the wall's, now
524 /// `wall.height` / `wall.profile`) and `edge_height` / `edge_profile` (the
525 /// edge's, now `edge.height` / `edge.profile`); and `window_manager.bevel_depth`
526 /// / `bevel_width` / `bevel_shader`, the block the relief keys were born in
527 /// before `style.surface.relief` existed (the compositor never read them;
528 /// the shader toggle is `relief.shader` now). Each was an alias for part
529 /// of 2026-09-28 and is not read now. And the graph's `cell_color`,
530 /// `gap_color` and `uniform_background` (2026-09-29): a graph's cells have
531 /// no colour, so there is no fill to switch, and its lines are
532 /// `graph.grid_color`. Empty for a clean config.
533 pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
534 let mut found = Vec::new();
535 for k in ["blur", "radius", "backdrop_compression", "refraction", "bevel_width"] {
536 if val.pointer(&format!("/style/surface/plate/{k}")).is_some() {
537 found.push(format!("style.surface.plate.{k}"));
538 }
539 }
540 if val.pointer("/style/surface/plate/frost/backdrop_compression").is_some() {
541 found.push("style.surface.plate.frost.backdrop_compression".to_string());
542 }
543 for k in ["depth", "height", "profile", "edge_height", "edge_profile"] {
544 if val.pointer(&format!("/style/surface/relief/{k}")).is_some() {
545 found.push(format!("style.surface.relief.{k}"));
546 }
547 }
548 for k in ["bevel_depth", "bevel_width", "bevel_shader"] {
549 if val.pointer(&format!("/window_manager/{k}")).is_some() {
550 found.push(format!("window_manager.{k}"));
551 }
552 }
553 if let Some(mats) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
554 for (name, node) in mats {
555 if node.pointer("/frost/backdrop_compression").is_some() {
556 found.push(format!("style.surface.material.{name}.frost.backdrop_compression"));
557 }
558 if node.pointer("/finish/depth").is_some() {
559 found.push(format!("style.surface.material.{name}.finish.depth"));
560 }
561 }
562 }
563 for k in ["cell_color", "gap_color", "uniform_background"] {
564 if val.pointer(&format!("/style/surface/graph/{k}")).is_some() {
565 found.push(format!("style.surface.graph.{k}"));
566 }
567 }
568 found
569 }
570
571 pub(super) fn load_colors_once() {
572 use std::sync::Once;
573 static INIT: Once = Once::new();
574 INIT.call_once(|| {
575 if let Some(content) = read_config() {
576 parse_and_set_colors(&content);
577 }
578 });
579 }
580
581 pub fn reload_colors(content: &str) {
582 parse_and_set_colors(content);
583 }
584
585 /// Test-only: the color state is process-global, so any test that calls
586 /// [`reload_colors`] and then asserts getter values races every other such
587 /// test on the parallel harness. Each of those tests must hold this lock
588 /// across its reload + asserts, and should fire the once-per-process config
589 /// loads ([`load_colors_once`] via any getter, and
590 /// `layout::lazy_init_style_registry`) inside the lock BEFORE its reload, so
591 /// neither can rewrite the state from the live config file mid-assert.
592 #[cfg(test)]
593 pub(crate) fn test_color_state_lock() -> std::sync::MutexGuard<'static, ()> {
594 static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
595 LOCK.lock().unwrap_or_else(std::sync::PoisonError::into_inner)
596 }
597
598 pub fn dropdown_background_color() -> [f32; 4] {
599 load_colors_once();
600 style_read(&DROPDOWN_BACKGROUND_COLOR)
601 }
602
603 pub fn set_dropdown_background_color(color: [f32; 4]) {
604 style_write(&DROPDOWN_BACKGROUND_COLOR, color);
605 }