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