git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/layout.rs (276.2K)

   1 use crate::widget::WidgetHost;
   2 use crate::context::UiContext;
   3 use std::sync::RwLock;
   4 use std::collections::HashMap;
   5 use std::sync::OnceLock;
   6 
   7 /// Per-thread overrides for runtime style writes, under `cfg(test)` only.
   8 ///
   9 /// Every `graph_*`, corner-radius and control-height slot in this file is
  10 /// backed by the one process-wide [`STYLE_REGISTRY`], so a test that pins any
  11 /// of them pins it for every test running beside it. That is the same defect
  12 /// as the font flake fixed in 1dc0ab1, and it was live:
  13 /// `test_graph_style_configuration` sets ~20 style values and restores none,
  14 /// while `dual_geometry_views_stay_consistent` bakes quads with
  15 /// `graph_node_corner_radius` and then re-reads that getter to compare — so a
  16 /// write landing between the two makes them disagree. Widening the write
  17 /// window to 300ms reproduced it on demand.
  18 ///
  19 /// Fixing it at the registry rather than per accessor covers every slot it
  20 /// holds in one place, including ones no test pins yet.
  21 #[cfg(test)]
  22 mod test_overlay {
  23     use std::cell::RefCell;
  24     use std::collections::HashMap;
  25     thread_local! {
  26         static FLOATS: RefCell<HashMap<String, f32>> = RefCell::new(HashMap::new());
  27         static LENS: RefCell<HashMap<String, crate::units::Len>> = RefCell::new(HashMap::new());
  28         static STRINGS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
  29     }
  30     pub fn set_float(k: &str, v: f32) {
  31         LENS.with(|m| m.borrow_mut().remove(k));
  32         FLOATS.with(|m| m.borrow_mut().insert(k.to_string(), v));
  33     }
  34     pub fn set_len(k: &str, v: crate::units::Len) {
  35         FLOATS.with(|m| m.borrow_mut().remove(k));
  36         LENS.with(|m| m.borrow_mut().insert(k.to_string(), v));
  37     }
  38     pub fn set_string(k: &str, v: String) {
  39         STRINGS.with(|m| m.borrow_mut().insert(k.to_string(), v));
  40     }
  41     pub fn get_float(k: &str) -> Option<f32> {
  42         if let Some(l) = LENS.with(|m| m.borrow().get(k).copied()) {
  43             return Some(l.to_px());
  44         }
  45         FLOATS.with(|m| m.borrow().get(k).copied())
  46     }
  47     pub fn get_len(k: &str) -> Option<crate::units::Len> {
  48         if let Some(l) = LENS.with(|m| m.borrow().get(k).copied()) {
  49             return Some(l);
  50         }
  51         FLOATS.with(|m| m.borrow().get(k).copied()).map(crate::units::Len::px)
  52     }
  53     pub fn get_string(k: &str) -> Option<String> {
  54         STRINGS.with(|m| m.borrow().get(k).cloned())
  55     }
  56 }
  57 
  58 #[derive(Debug)]
  59 pub struct StyleRegistry {
  60     pub floats: HashMap<String, f32>,
  61     pub strings: HashMap<String, String>,
  62     /// Slots whose config value carried a unit (`width=(mm)2.0`). Read
  63     /// through `get_float` like any other number, resolved against the
  64     /// process metric (`crate::units::metric`) at EVERY read, so a metric
  65     /// that arrives after config load — outputs come in after the first
  66     /// style read — or changes with the display is honoured live.
  67     pub lens: HashMap<String, crate::units::Len>,
  68 }
  69 
  70 impl StyleRegistry {
  71     pub fn new() -> Self {
  72         Self {
  73             floats: HashMap::new(),
  74             strings: HashMap::new(),
  75             lens: HashMap::new(),
  76         }
  77     }
  78 
  79     pub fn get_float(&self, key: &str) -> Option<f32> {
  80         #[cfg(test)]
  81         if let Some(v) = test_overlay::get_float(key) {
  82             return Some(v);
  83         }
  84         if let Some(len) = self.lens.get(key) {
  85             return Some(len.to_px());
  86         }
  87         self.floats.get(key).copied()
  88     }
  89 
  90     /// The slot as a length with its unit: the configured `Len` when one was
  91     /// given, else the plain number as logical px. For editors that show the
  92     /// unit the user chose rather than the resolved pixel count.
  93     pub fn get_len(&self, key: &str) -> Option<crate::units::Len> {
  94         #[cfg(test)]
  95         if let Some(v) = test_overlay::get_len(key) {
  96             return Some(v);
  97         }
  98         if let Some(len) = self.lens.get(key) {
  99             return Some(*len);
 100         }
 101         self.floats.get(key).map(|v| crate::units::Len::px(*v))
 102     }
 103 
 104     pub fn get_string(&self, key: &str) -> Option<String> {
 105         #[cfg(test)]
 106         if let Some(v) = test_overlay::get_string(key) {
 107             return Some(v);
 108         }
 109         self.strings.get(key).cloned()
 110     }
 111 
 112     /// A plain number wins over any earlier unit value for the slot — a
 113     /// runtime `set_float` is the newest opinion.
 114     pub fn set_float(&mut self, key: &str, val: f32) {
 115         #[cfg(test)]
 116         {
 117             test_overlay::set_float(key, val);
 118             return;
 119         }
 120         #[cfg(not(test))]
 121         self.load_float(key, val);
 122     }
 123 
 124     pub fn set_len(&mut self, key: &str, len: crate::units::Len) {
 125         #[cfg(test)]
 126         {
 127             test_overlay::set_len(key, len);
 128             return;
 129         }
 130         #[cfg(not(test))]
 131         self.load_len(key, len);
 132     }
 133 
 134     pub fn set_string(&mut self, key: &str, val: String) {
 135         #[cfg(test)]
 136         {
 137             test_overlay::set_string(key, val);
 138             return;
 139         }
 140         #[cfg(not(test))]
 141         self.load_string(key, val);
 142     }
 143 
 144     /// The CONFIG-LOAD writes, as opposed to the runtime `set_*` ones above.
 145     /// Kept apart because under `cfg(test)` a runtime set goes to a per-thread
 146     /// overlay while the loaded config must stay the shared base every test
 147     /// reads — routing config through `set_*` would put one thread's config
 148     /// in its overlay and leave every other thread with an empty registry.
 149     pub fn load_float(&mut self, key: &str, val: f32) {
 150         self.lens.remove(key);
 151         self.floats.insert(key.to_string(), val);
 152     }
 153 
 154     pub fn load_len(&mut self, key: &str, len: crate::units::Len) {
 155         self.floats.remove(key);
 156         self.lens.insert(key.to_string(), len);
 157     }
 158 
 159     pub fn load_string(&mut self, key: &str, val: String) {
 160         self.strings.insert(key.to_string(), val);
 161     }
 162 }
 163 
 164 pub static STYLE_REGISTRY: OnceLock<RwLock<StyleRegistry>> = OnceLock::new();
 165 
 166 pub fn get_style_registry() -> &'static RwLock<StyleRegistry> {
 167     STYLE_REGISTRY.get_or_init(|| RwLock::new(StyleRegistry::new()))
 168 }
 169 
 170 pub fn lazy_init_style_registry() {
 171     use std::sync::Once;
 172     static INIT: Once = Once::new();
 173     INIT.call_once(|| {
 174         reload_config();
 175     });
 176 }
 177 
 178 fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props: &mut String) {
 179     match val {
 180         serde_json::Value::Object(map) => {
 181             for (k, v) in map {
 182                 let next_prefix = if prefix.is_empty() {
 183                     k.clone()
 184                 } else {
 185                     format!("{}.{}", prefix, k)
 186                 };
 187                 flatten_json_to_flat_props(v, &next_prefix, flat_props);
 188             }
 189         }
 190         _ => {
 191             let flat_key = match prefix {
 192                 "style.list.font" | "style.data.list.font" => "list_font",
 193                 "style.list.font_color" | "style.data.list.font_color" => "list_font_color",
 194                 "style.control.breadcrumb.font" => "breadcrumb_font",
 195 
 196                 // The control rung's default radius: every control-scale
 197                 // `corner_radius` below falls back to it when its own key is unset.
 198                 "style.control.corner_radius" => "control_corner_radius",
 199                 "style.control.button.padding" => "button_padding",
 200                 "style.control.button.height" => "button_height",
 201                 "style.control.button.corner_radius" => "button_corner_radius",
 202                 "style.control.button.font" => "button_font",
 203                 "style.list.corner_radius" | "style.data.list.corner_radius" => "list_corner_radius",
 204                 "style.control.textbox.corner_radius" | "style.textbox.corner_radius" | "style.data.textbox.corner_radius" => "textbox_corner_radius",
 205                 "style.control.dropdown.color" => "dropdown_color",
 206                 "style.control.font_selector.font" => "font_selector_font",
 207                 "style.container.section.font" | "style.section.font" => "section_label_font",
 208                 "style.container.section.padding" | "style.section.padding" => "section_padding",
 209                 "style.control.button_strip.font" => "button_strip_font",
 210                 "style.control.button_strip.spacing" => "button_strip_spacing",
 211                 "style.control.dropdown.height" => "dropdown_height",
 212                 "style.control.dropdown.corner_radius" => "dropdown_corner_radius",
 213                 "style.control.font_selector.height" => "font_selector_height",
 214                 "style.control.font_selector.corner_radius" => "font_selector_corner_radius",
 215                 "style.control.label.font" => "control_label_font",
 216                 "style.control.label.font_detached" => "control_label_font_detached",
 217                 "style.control.label.margin" => "control_label_margin",
 218                 "style.control.slider.height" => "slider_height",
 219                 "style.control.slider.corner_radius" => "slider_corner_radius",
 220                 "style.control.slider.band_thickness" => "slider_band_thickness",
 221                 "style.control.slider.bulge_width" => "slider_bulge_width",
 222                 "style.control.slider.bulge_height" => "slider_bulge_height",
 223                 "style.control.progressbar.height" => "progressbar_height",
 224                 "style.control.rangeslider.height" => "rangeslider_height",
 225                 "style.control.scrollbar.width" => "scrollbar_width",
 226                 "style.control.scrollbar.inset" => "scrollbar_inset",
 227                 "style.control.spinbox.height" => "spinbox_height",
 228                 "style.control.spinbox.button_padding" => "spinbox_button_padding",
 229                 "style.control.spinbox.corner_radius" => "spinbox_corner_radius",
 230                 "style.control.textbox.height" | "style.textbox.height" | "style.data.textbox.height" => "textbox_height",
 231                 "style.control.textbox.placeholder_text_color" | "style.textbox.placeholder_text_color" | "style.data.textbox.placeholder_text_color" => "textbox_placeholder_text_color",
 232                 "style.control.textbox.background_color" | "style.textbox.background_color" | "style.data.textbox.background_color" => "textbox_background_color",
 233                 "style.control.textbox.background_edit_color" | "style.textbox.background_edit_color" | "style.data.textbox.background_edit_color" => "textbox_background_edit_color",
 234                 "style.control.textbox.multiline.line_wrap" | "style.textbox.multiline.line_wrap" | "style.data.textbox.multiline.line_wrap" => "textbox_line_wrap",
 235                 "style.control.textbox.multiline.border_width" | "style.textbox.multiline.border_width" | "style.data.textbox.multiline.border_width" => "textbox_multiline_border_width",
 236                 "style.control.toggle.height" => "toggle_height",
 237                 "style.control.toggle.border_width" => "toggle_border_width",
 238                 "style.control.toggle.disabled_color" => "toggle_disabled_color",
 239                 "style.control.toggle.border_color" => "toggle_border_color",
 240                 "style.control.toggle.corner_radius" => "toggle_corner_radius",
 241                 "window_manager.light_source_position" => "light_source_position",
 242                 // The DE's relief material: canonical home style.surface.relief
 243                 // (these shade every bevel/boss/recess in the toolkit — the
 244                 // compositor never read them, so the old window_manager
 245                 // spelling survives only as a compat alias).
 246                 // `depth` is the light strength, not a length — `light` is
 247                 // the honest spelling, `depth` the one every config has.
 248                 "style.surface.relief.depth" | "style.surface.relief.light" | "window_manager.bevel_depth" => "bevel_depth",
 249                 "style.surface.relief.width" | "window_manager.bevel_width" => "bevel_width",
 250                 // The geometric heights, both lengths (unit-aware): a carve's
 251                 // drop and the plate roll's rise. Unset = follow the width.
 252                 "style.surface.relief.height" => "bevel_height",
 253                 "style.surface.relief.edge_height" => "roll_height",
 254                 // Ramp-spec strings for the custom wall/roll profiles
 255                 // (written by cce-relief, installed by reload_config).
 256                 "style.surface.relief.profile" => "bevel_profile_spec",
 257                 "style.surface.relief.edge_profile" => "roll_profile_spec",
 258                 // cce-relief's slider positions behind those specs
 259                 // ("shoulder,base,bias" — only the editor reads these).
 260                 "style.surface.relief.profile_knobs" => "bevel_profile_knobs",
 261                 "style.surface.relief.edge_knobs" => "roll_profile_knobs",
 262                 "style.container.section.depth" => "section_depth",
 263                 "style.surface.param.backdrop_compression" => "param_compression",
 264                 "style.surface.param.label_layout" => "param_label_layout",
 265                 "window_manager.bevel_shader" => "bevel_shader",
 266                 "window_manager.control_relief" => "control_relief",
 267                 "window_manager.corner_shape" => "corner_shape",
 268                 "style.control.ramp.height" => "ramp_height",
 269                 "style.layout.column.gap" => "column_gap",
 270                 "style.control.control_panel.padding" => "control_panel_padding",
 271                 "style.control.control_panel.gap" => "control_panel_gap",
 272                 "style.status.normal_color" => "status_normal_color",
 273                 "style.status.background_color" => "status_background_color",
 274                 "style.status.background_blur" => "status_background_blur",
 275                 "style.highlight.primary" => "primary_highlight_color",
 276                 "style.window.page_opacity" => "page_opacity",
 277                 "style.window.page_margin" => "page_margin",
 278                 "style.window.plate_padding" => "plate_padding",
 279                 "style.window.transition_duration" => "transition_duration",
 280                 "style.overlay.behavior" => "overlay_behavior",
 281                 "style.overlay.width" => "overlay_width",
 282                 "style.overlay.position" => "overlay_position",
 283                 "style.overlay.border_gap" => "overlay_border_gap",
 284                 "style.editor.last_page" | "style.data.editor.last_page" => "last_page",
 285                 "style.data.tree.corner_radius" => "tree_corner_radius",
 286                 "style.data.tree.opacity" => "tree_opacity",
 287                 "style.data.tree.blur" => "tree_blur",
 288                 "style.data.tree.font" => "tree_font",
 289                 "style.surface.desktop.gap_color" => "desktop_gap_color",
 290                 "style.surface.desktop.cell_color" => "desktop_cell_color",
 291                 "style.surface.desktop.gap_width" => "desktop_gap_width",
 292                 "style.surface.desktop.cell_fade_inset" => "desktop_cell_fade_inset",
 293                 "style.surface.desktop.mode" => "desktop_mode",
 294                 "style.surface.desktop.solid_color" => "desktop_solid_color",
 295                 "style.surface.desktop.grid_cell_size" => "desktop_grid_scale",
 296                 "style.surface.desktop.grid_cell_width" => "grid_cell_width",
 297                 "style.surface.desktop.grid_cell_height" => "grid_cell_height",
 298                 "style.surface.plate.padding" => "plate_padding",
 299                 "style.surface.plate.gap" => "plate_gap",
 300                 "style.control.gap" => "control_gap",
 301                 // The context menu's own radius (`menu_corner_radius`): a
 302                 // popover's corner is control-scale, not pane-scale.
 303                 "style.surface.menu.corner_radius" => "menu_corner_radius",
 304                 // `style.surface.plate.root.*` is the one spelling of the
 305                 // root-plate style (RFC Phase 7a). The slot names keep the
 306                 // historical `root_plate_` prefix; the legacy `root plate.*`
 307                 // config read-alias was removed 2026-09-06.
 308                 "style.surface.plate.root.padding" => "root_plate_padding",
 309                 "style.surface.plate.root.gap" => "root_plate_gap",
 310                 "style.surface.plate.root.color" => "root_plate_color",
 311                 "style.surface.plate.root.blur" => "root_plate_blur",
 312                 "style.surface.plate.root.corner_radius" => "root_plate_corner_radius",
 313                 "style.surface.plate.root.menubar.color" => "root_plate_menubar_color",
 314                 "style.surface.plate.root.menubar.text_color" => "root_plate_menubar_text_color",
 315                 "style.surface.plate.root.menubar.blur" => "root_plate_menubar_blur",
 316                 "style.surface.plate.root.menubar.font" => "menubar_font",
 317                 "style.surface.statusbar.color" => "root_plate_statusbar_color",
 318                 "style.surface.statusbar.text_color" => "root_plate_statusbar_text_color",
 319                 "style.surface.statusbar.blur" => "root_plate_statusbar_blur",
 320                 "style.surface.statusbar.font" => "statusbar_font",
 321                 "style.surface.page.opacity" => "page_opacity",
 322                 "style.surface.page.margin" => "page_margin",
 323                 "style.surface.graph.cell_color" => "graph_cell_color",
 324                 "style.surface.graph.gap_color" => "graph_gap_color",
 325                 "style.surface.graph.opacity" => "graph_opacity",
 326                 "style.surface.graph.node.opacity" => "graph_node_opacity",
 327                 "style.surface.graph.spacing_x" => "graph_spacing_x",
 328                 "style.surface.graph.spacing_y" => "graph_spacing_y",
 329                 "style.surface.graph.line_width" => "graph_line_width",
 330                 "style.surface.graph.node.width" => "graph_node_width",
 331                 "style.surface.graph.node.height" => "graph_node_height",
 332                 "style.surface.graph.grid_snap" => "graph_grid_snap",
 333                 "style.surface.graph.blur" => "graph_blur",
 334                 "style.surface.graph.font" => "graph_font",
 335                 "style.surface.graph.node.color" => "graph_node_color",
 336                 "style.surface.graph.node.font" => "graph_node_font",
 337                 "style.surface.graph.node.delete" => "graph_node_delete",
 338                 "style.surface.graph.node.selected_color" => "graph_node_selected_color",
 339                 "style.surface.graph.node.drag_color" => "graph_node_drag_color",
 340                 "style.surface.graph.node.corner_radius" => "graph_node_corner_radius",
 341                 "style.surface.graph.node.wire_color" => "graph_wire_color",
 342                 "style.surface.graph.node.wire_highlight_color" => "graph_wire_highlight_color",
 343                 "style.surface.graph.node.wire_size" => "graph_wire_size",
 344                 "style.surface.graph.node.wire_activation_radius" => "graph_wire_activation_radius",
 345                 "style.surface.graph.node.connector_color" => "graph_connector_color",
 346                 "style.surface.graph.node.connector_highlight_color" => "graph_connector_highlight_color",
 347                 "style.surface.graph.node.connector_size" => "graph_connector_size",
 348                 "style.surface.graph.node.connector_activation_radius" => "graph_connector_activation_radius",
 349                 "style.surface.plate.color" => "plate_color",
 350                 "style.surface.plate.border_color" => "plate_border_color",
 351                 "style.surface.plate.border_thickness" => "plate_border_thickness",
 352                 "style.surface.plate.blur" => "plate_blur",
 353                 "input.touchpad.natural_scroll" => "touchpad_natural_scroll",
 354                 
 355                 other => {
 356                     if let Some(rest) = other.strip_prefix("layout.") {
 357                         rest
 358                     } else if let Some(rest) = other.strip_prefix("transparency.") {
 359                         rest
 360                     } else {
 361                         if let Some(idx) = other.find('.') {
 362                             &other[idx + 1..]
 363                         } else {
 364                             other
 365                         }
 366                     }
 367                 }
 368             };
 369             
 370             if let Some(s) = val.as_str() {
 371                 flat_props.push_str(&format!("{} = \"{}\"\n", flat_key, s));
 372             } else if let Some(b) = val.as_bool() {
 373                 flat_props.push_str(&format!("{} = {}\n", flat_key, b));
 374             } else if let Some(n) = val.as_f64() {
 375                 flat_props.push_str(&format!("{} = {}\n", flat_key, n));
 376             } else if let Some(n) = val.as_i64() {
 377                 flat_props.push_str(&format!("{} = {}\n", flat_key, n));
 378             }
 379         }
 380     }
 381 }
 382 
 383 fn read_config() -> Option<String> {
 384     let path = crate::config::get_config_path();
 385     if let Ok(content) = std::fs::read_to_string(&path) {
 386         let val = crate::config::parse_kdl_to_json(&content);
 387         let mut flat_props = String::new();
 388         flatten_json_to_flat_props(&val, "", &mut flat_props);
 389         return Some(flat_props);
 390     }
 391     None
 392 }
 393 
 394 pub fn read_config_value(target_key: &str) -> Option<String> {
 395     let path = crate::config::get_config_path();
 396     if let Ok(content) = std::fs::read_to_string(&path) {
 397         let val = crate::config::parse_kdl_to_json(&content);
 398         let mut flat_props = String::new();
 399         flatten_json_to_flat_props(&val, "", &mut flat_props);
 400         for line in flat_props.lines() {
 401             let trimmed = line.trim();
 402             if let Some(eq_idx) = trimmed.find('=') {
 403                 let key = trimmed[..eq_idx].trim();
 404                 if key == target_key {
 405                     let val_str = trimmed[eq_idx + 1..].trim().trim_matches('"').trim();
 406                     return Some(val_str.to_string());
 407                 }
 408             }
 409         }
 410     }
 411     None
 412 }
 413 
 414 pub fn parse_font_string(s: &str) -> (String, Option<f32>) {
 415     let s = s.trim();
 416     if let Some(last_space_idx) = s.rfind(' ') {
 417         let (family, size_str) = s.split_at(last_space_idx);
 418         let size_str = size_str.trim();
 419         if let Ok(size) = size_str.parse::<f32>() {
 420             return (family.trim().to_string(), Some(size));
 421         }
 422     }
 423     (s.to_string(), None)
 424 }
 425 
 426 static SECTION_PADDING: RwLock<f32> = RwLock::new(8.0);
 427 
 428 /// Per-thread overrides for the style values tests pin, active only under
 429 /// `cfg(test)`.
 430 ///
 431 /// These values are process-global by design: the toolkit reads them from
 432 /// config once and every widget sees the same style. That also makes them
 433 /// shared mutable state BETWEEN tests, and `cargo test` runs tests on
 434 /// parallel threads. `test_vstack_flow` pins a known font and margin so its
 435 /// pixel assertions do not depend on the developer's config — and for as
 436 /// long as it ran, every other test measuring text saw that font. That is
 437 /// the whole "cce-ui parallel test flake": `a_members_wide_label_is_in_the_hull`
 438 /// and two `layout::tests` siblings failing perhaps one run in three on a
 439 /// clean tree, always green at `--test-threads=1`, and blamed on innocent
 440 /// diffs for weeks.
 441 ///
 442 /// A lock around the three known victims would have fixed those three. This
 443 /// fixes the class: a `set_*` is invisible to tests running beside it, and a
 444 /// future test that pins a style needs no lock and no discipline to remember.
 445 /// Production is untouched — `cfg(test)` is set only while compiling this
 446 /// crate's own unit tests, never for downstream crates.
 447 #[cfg(test)]
 448 mod test_style {
 449     use std::cell::RefCell;
 450     thread_local! {
 451         pub static CONTROL_LABEL_MARGIN: RefCell<Option<f32>> = const { RefCell::new(None) };
 452         pub static SECTION_PADDING: RefCell<Option<f32>> = const { RefCell::new(None) };
 453         pub static CONTROL_LABEL_FONT: RefCell<Option<String>> = const { RefCell::new(None) };
 454         pub static CONTROL_LABEL_FONT_DETACHED: RefCell<Option<String>> = const { RefCell::new(None) };
 455     }
 456 }
 457 /// The height every text-bearing control falls back to when its own
 458 /// `style.control.<name>.height` is unset: button, toggle (and the checkbox
 459 /// row), dropdown, textbox (and the keybind recorder), spinbox, font selector,
 460 /// colour selector, button strip, breadcrumb. One number, so a form built from
 461 /// defaults lines up; a per-control key is the deliberate exception.
 462 pub const DEFAULT_CONTROL_HEIGHT: f32 = 24.0;
 463 
 464 /// The one gap between controls — one control height — in BOTH axes: what every
 465 /// layout strategy's `Default` puts between children's blocks (a detached label
 466 /// and the control below it, see `WidgetHost::label_strip`) and around them, and
 467 /// what the legacy row builders advance by. One number, one module, so the space
 468 /// beside a control and the space below it read the same.
 469 pub const CONTROL_GAP: f32 = DEFAULT_CONTROL_HEIGHT;
 470 
 471 /// The one inset from a control's edge to its text: the field text of a TextBox,
 472 /// Dropdown, Spinbox, FontSelector, KeybindRecorder or ColorSelector, a left-
 473 /// justified Button or Toggle label, a Slider's readout. Fields in a column line
 474 /// their text up because they all use this.
 475 pub const CONTROL_TEXT_INSET: f32 = 8.0;
 476 
 477 /// A carve that stays INSIDE its rect. A recess, boss or trough wall straddles the
 478 /// rect edge it is given — half its depth outside — so a well carved at a widget's
 479 /// rect edge painted past the widget's box, and the gap beside a well read up to
 480 /// half a depth smaller than the gap beside a raised plate (whose roll is inside).
 481 /// Every widget carves the rect this returns instead: inset by half the depth, the
 482 /// radii reduced by the same so the OUTER silhouette keeps the configured radius.
 483 /// The widget's footprint is then its rect, and the gap is the gap.
 484 pub fn carve_inside(rect: crate::scene::layout::Rect, radii: crate::scene::paint::Radii, depth: f32) -> (crate::scene::layout::Rect, crate::scene::paint::Radii) {
 485     let g = depth * 0.5;
 486     let r = |r: f32| if r > 0.0 { (r - g).max(0.0) } else { 0.0 };
 487     (
 488         crate::scene::layout::Rect { x: rect.x + g, y: rect.y + g, width: (rect.width - depth).max(0.0), height: (rect.height - depth).max(0.0) },
 489         (r(radii.0), r(radii.1), r(radii.2), r(radii.3)),
 490     )
 491 }
 492 
 493 /// The one inset from a control's left edge to its detached label above it — the
 494 /// x offset the adapter draws the label at, and the tab hugging that label in the
 495 /// carve-out compositions (Slider, Dropdown, RangeSlider). Every labeled control
 496 /// uses it, so a column of labels is one line.
 497 pub const DETACHED_LABEL_INSET: f32 = 4.0;
 498 /// The same for the track-shaped controls: slider, range slider, progress bar,
 499 /// usage bar.
 500 pub const DEFAULT_TRACK_HEIGHT: f32 = 16.0;
 501 
 502 static SPINBOX_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 503 static SPINBOX_BUTTON_PADDING: RwLock<f32> = RwLock::new(0.0);
 504 static COLOR_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 505 static TEXTBOX_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 506 static FONT_SELECTOR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 507 static SLIDER_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
 508 static PROGRESSBAR_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
 509 static RANGESLIDER_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_TRACK_HEIGHT);
 510 static TOGGLE_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 511 static COLOR_SELECTOR_FONT: RwLock<String> = RwLock::new(String::new());
 512 static COLOR_SELECTOR_PREVIEW_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 513 static COLOR_SELECTOR_PREVIEW_MARGIN: RwLock<f32> = RwLock::new(0.0);
 514 static COLOR_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 515 static MENUBAR_FONT: RwLock<String> = RwLock::new(String::new());
 516 static MENUBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 517 static STATUSBAR_FONT: RwLock<String> = RwLock::new(String::new());
 518 static STATUSBAR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 519 static SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
 520 static NESTED_SECTION_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
 521 static BREADCRUMB_FONT: RwLock<String> = RwLock::new(String::new());
 522 static BUTTON_FONT: RwLock<String> = RwLock::new(String::new());
 523 
 524 static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
 525 static BUTTON_PADDING: RwLock<f32> = RwLock::new(14.0);
 526 static BUTTON_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 527 static RAMP_HEIGHT: RwLock<f32> = RwLock::new(32.0);
 528 static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
 529 static SCROLLBAR_WIDTH: RwLock<f32> = RwLock::new(4.0);
 530 static SCROLLBAR_INSET: RwLock<f32> = RwLock::new(16.0);
 531 static COLUMN_GAP: RwLock<Option<f32>> = RwLock::new(None);
 532 static CONTROL_PANEL_PADDING: RwLock<Option<f32>> = RwLock::new(None);
 533 static CONTROL_PANEL_GAP: RwLock<Option<f32>> = RwLock::new(None);
 534 static TREE_OPACITY: RwLock<f32> = RwLock::new(1.0);
 535 static TREE_BLUR: RwLock<f32> = RwLock::new(0.0);
 536 
 537 static PLATE_PADDING: RwLock<f32> = RwLock::new(20.0);
 538 static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(DEFAULT_CONTROL_HEIGHT);
 539 static NESTED_SECTION_LABEL_ALIGNMENT: RwLock<u8> = RwLock::new(0);
 540 static TOUCHPAD_NATURAL_SCROLL: RwLock<bool> = RwLock::new(false);
 541 
 542 
 543 static BUTTON_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 544 static SPINBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 545 static TEXTBOX_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 546 static FONT_SELECTOR_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 547 static DROPDOWN_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 548 static TOGGLE_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 549 static SLIDER_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 550 static RANGESLIDER_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 551 static LIST_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 552 static TREE_CORNER_RADIUS: RwLock<f32> = RwLock::new(4.0);
 553 static TOGGLE_BORDER_WIDTH: RwLock<f32> = RwLock::new(1.0);
 554 static FONT_SELECTOR_FONT: RwLock<String> = RwLock::new(String::new());
 555 static FONT_SELECTOR_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 556 static BUTTON_STRIP_FONT: RwLock<String> = RwLock::new(String::new());
 557 static BUTTON_STRIP_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 558 static CONTROL_LABEL_FONT: RwLock<String> = RwLock::new(String::new());
 559 static CONTROL_LABEL_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 560 static CONTROL_LABEL_FONT_DETACHED: RwLock<String> = RwLock::new(String::new());
 561 static CONTROL_LABEL_FONT_DETACHED_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 562 static CONTROL_LABEL_MARGIN: RwLock<f32> = RwLock::new(6.0);
 563 static PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
 564 static LIST_FONT: RwLock<String> = RwLock::new(String::new());
 565 static LIST_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 566 static TREE_FONT: RwLock<String> = RwLock::new(String::new());
 567 static TREE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 568 static GRAPH_FONT: RwLock<String> = RwLock::new(String::new());
 569 static GRAPH_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 570 static GRAPH_NODE_FONT: RwLock<String> = RwLock::new(String::new());
 571 static GRAPH_NODE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 572 static LIST_JUSTIFICATION: RwLock<u8> = RwLock::new(0);
 573 static PLATE_OPACITY: RwLock<f32> = RwLock::new(1.0);
 574 static PAGE_OPACITY: RwLock<f32> = RwLock::new(1.0);
 575 static LAYER_OPACITY: RwLock<f32> = RwLock::new(1.0);
 576 static TEXTBOX_LINE_WRAP: RwLock<bool> = RwLock::new(true);
 577 static TEXTBOX_MULTILINE_BORDER_WIDTH: RwLock<f32> = RwLock::new(1.0);
 578 
 579 
 580 
 581 
 582 /// Standard line height multiplier for text layout in cce-ui.
 583 pub const TEXT_LINE_HEIGHT_MULTIPLIER: f32 = 1.0;
 584 
 585 /// Standard line height based on font size.
 586 pub fn line_height(font_size: f32) -> f32 {
 587     font_size * TEXT_LINE_HEIGHT_MULTIPLIER
 588 }
 589 
 590 /// Aligns a text label's top coordinate (`y`) so it is centered vertically
 591 /// inside a container of height `container_h` starting at `y`.
 592 pub fn center_text_y(y: f32, container_h: f32, font_size: f32) -> f32 {
 593     y + (container_h - line_height(font_size)) / 2.0
 594 }
 595 
 596 /// Standardized vertical text alignment calculation based on Spinbox widget alignment.
 597 pub fn align_text_y(y: f32, height: f32, font_size: f32, top_offset: f32) -> f32 {
 598     y + top_offset + (height - top_offset - font_size) / 2.0
 599 }
 600 
 601 pub fn reload_config() {
 602     if let Some(content) = read_config() {
 603         let mut menubar_font_changed = false;
 604         let mut statusbar_font_changed = false;
 605         let mut font_selector_font_changed = false;
 606         let mut button_strip_font_changed = false;
 607         let mut label_font_changed = false;
 608         let mut label_font_detached_changed = false;
 609         let mut list_font_changed = false;
 610         let mut tree_font_changed = false;
 611         let mut graph_font_changed = false;
 612         let mut graph_node_font_changed = false;
 613         for line in content.lines() {
 614             let trimmed = line.trim();
 615             let mut key = String::new();
 616             let mut val_str = "";
 617             if let Some(eq_idx) = trimmed.find('=') {
 618                 key = trimmed[..eq_idx].trim().to_string();
 619                 val_str = trimmed[eq_idx + 1..].trim().trim_matches('"').trim();
 620                 if let Ok(mut registry) = get_style_registry().write() {
 621                     if let Ok(f_val) = val_str.parse::<f32>() {
 622                         registry.load_float(&key, f_val);
 623                     } else if let Some(len) = crate::units::Len::parse(val_str) {
 624                         // `(mm)2.0` arrived as the string `2mm`.
 625                         registry.load_len(&key, len);
 626                     } else {
 627                         registry.load_string(&key, val_str.to_string());
 628                     }
 629                 }
 630             }
 631             if let Some(rest) = trimmed.strip_prefix("control_label_margin") {
 632                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 633                 let val_str = rest.trim_end_matches('"').trim();
 634                 if let Ok(val) = val_str.parse::<f32>() {
 635                     if let Ok(mut lock) = CONTROL_LABEL_MARGIN.write() {
 636                         *lock = val;
 637                     }
 638                 }
 639             }
 640             if let Some(rest) = trimmed.strip_prefix("nested_section_label_alignment") {
 641                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 642                 let val_str = rest.trim_end_matches('"').trim();
 643                 if let Ok(val) = val_str.parse::<u8>() {
 644                     if let Ok(mut lock) = NESTED_SECTION_LABEL_ALIGNMENT.write() {
 645                         *lock = val;
 646                     }
 647                 }
 648             }
 649             if let Some(rest) = trimmed.strip_prefix("nested_section_label_offset") {
 650                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 651                 let val_str = rest.trim_end_matches('"').trim();
 652                 if let Ok(val) = val_str.parse::<f32>() {
 653                     if let Ok(mut lock) = NESTED_SECTION_LABEL_OFFSET.write() {
 654                         *lock = val;
 655                     }
 656                 }
 657             }
 658             if let Some(rest) = trimmed.strip_prefix("plate_padding") {
 659                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 660                 let val_str = rest.trim_end_matches('"').trim();
 661                 if let Ok(val) = val_str.parse::<f32>() {
 662                     if let Ok(mut lock) = PLATE_PADDING.write() {
 663                         *lock = val;
 664                     }
 665                 }
 666             }
 667             if let Some(rest) = trimmed.strip_prefix("page_margin") {
 668                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 669                 let val_str = rest.trim_end_matches('"').trim();
 670                 if let Ok(val) = val_str.parse::<f32>() {
 671                     if let Ok(mut lock) = PAGE_MARGIN.write() {
 672                         *lock = Some(val);
 673                     }
 674                 }
 675             }
 676             if let Some(rest) = trimmed.strip_prefix("grid_min_col_width") {
 677                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 678                 let val_str = rest.trim_end_matches('"').trim();
 679                 if let Ok(val) = val_str.parse::<f32>() {
 680                     if let Ok(mut lock) = GRID_MIN_COL_WIDTH.write() {
 681                         *lock = val;
 682                     }
 683                 }
 684             }
 685             if let Some(rest) = trimmed.strip_prefix("grid_gap") {
 686                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 687                 let val_str = rest.trim_end_matches('"').trim();
 688                 if let Ok(val) = val_str.parse::<f32>() {
 689                     if let Ok(mut lock) = GRID_GAP.write() {
 690                         *lock = val;
 691                     }
 692                 }
 693             }
 694             if let Some(rest) = trimmed.strip_prefix("column_gap") {
 695                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 696                 let val_str = rest.trim_end_matches('"').trim();
 697                 if let Ok(val) = val_str.parse::<f32>() {
 698                     if let Ok(mut lock) = COLUMN_GAP.write() {
 699                         *lock = Some(val);
 700                     }
 701                 }
 702             }
 703             if let Some(rest) = trimmed.strip_prefix("control_panel_padding") {
 704                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 705                 let val_str = rest.trim_end_matches('"').trim();
 706                 if let Ok(val) = val_str.parse::<f32>() {
 707                     if let Ok(mut lock) = CONTROL_PANEL_PADDING.write() {
 708                         *lock = Some(val);
 709                     }
 710                 }
 711             }
 712             if let Some(rest) = trimmed.strip_prefix("control_panel_gap") {
 713                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 714                 let val_str = rest.trim_end_matches('"').trim();
 715                 if let Ok(val) = val_str.parse::<f32>() {
 716                     if let Ok(mut lock) = CONTROL_PANEL_GAP.write() {
 717                         *lock = Some(val);
 718                     }
 719                 }
 720             }
 721             if let Some(rest) = trimmed.strip_prefix("section_padding") {
 722                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 723                 let val_str = rest.trim_end_matches('"').trim();
 724                 if let Ok(val) = val_str.parse::<f32>() {
 725                     if let Ok(mut lock) = SECTION_PADDING.write() {
 726                         *lock = val;
 727                     }
 728                 }
 729             }
 730             if let Some(rest) = trimmed.strip_prefix("scrollbar_width") {
 731                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 732                 let val_str = rest.trim_end_matches('"').trim();
 733                 if let Ok(val) = val_str.parse::<f32>() {
 734                     if let Ok(mut lock) = SCROLLBAR_WIDTH.write() {
 735                         *lock = val;
 736                     }
 737                 }
 738             }
 739             if let Some(rest) = trimmed.strip_prefix("scrollbar_inset") {
 740                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 741                 let val_str = rest.trim_end_matches('"').trim();
 742                 if let Ok(val) = val_str.parse::<f32>() {
 743                     if let Ok(mut lock) = SCROLLBAR_INSET.write() {
 744                         *lock = val;
 745                     }
 746                 }
 747             }
 748             if let Some(rest) = trimmed.strip_prefix("tree_opacity") {
 749                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 750                 let val_str = rest.trim_end_matches('"').trim();
 751                 if let Ok(val) = val_str.parse::<f32>() {
 752                     if let Ok(mut lock) = TREE_OPACITY.write() {
 753                         *lock = val;
 754                     }
 755                 }
 756             }
 757             if let Some(rest) = trimmed.strip_prefix("tree_blur") {
 758                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 759                 let val_str = rest.trim_end_matches('"').trim();
 760                 if let Ok(val) = val_str.parse::<f32>() {
 761                     if let Ok(mut lock) = TREE_BLUR.write() {
 762                         *lock = val;
 763                     }
 764                 }
 765             }
 766             if let Some(rest) = trimmed.strip_prefix("spinbox_height") {
 767                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 768                 let val_str = rest.trim_end_matches('"').trim();
 769                 if let Ok(val) = val_str.parse::<f32>() {
 770                     if let Ok(mut lock) = SPINBOX_HEIGHT.write() {
 771                         *lock = val;
 772                     }
 773                 }
 774             }
 775             if let Some(rest) = trimmed.strip_prefix("spinbox_button_padding") {
 776                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 777                 let val_str = rest.trim_end_matches('"').trim();
 778                 if let Ok(val) = val_str.parse::<f32>() {
 779                     if let Ok(mut lock) = SPINBOX_BUTTON_PADDING.write() {
 780                         *lock = val;
 781                     }
 782                 }
 783             }
 784             if let Some(rest) = trimmed.strip_prefix("spinbox_corner_radius") {
 785                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 786                 let val_str = rest.trim_end_matches('"').trim();
 787                 if let Ok(val) = val_str.parse::<f32>() {
 788                     if let Ok(mut lock) = SPINBOX_CORNER_RADIUS.write() {
 789                         *lock = val;
 790                     }
 791                 }
 792             }
 793             if let Some(rest) = trimmed.strip_prefix("textbox_corner_radius") {
 794                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 795                 let val_str = rest.trim_end_matches('"').trim();
 796                 if let Ok(val) = val_str.parse::<f32>() {
 797                     if let Ok(mut lock) = TEXTBOX_CORNER_RADIUS.write() {
 798                         *lock = val;
 799                     }
 800                 }
 801             }
 802             if let Some(rest) = trimmed.strip_prefix("textbox_line_wrap") {
 803                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 804                 let val_str = rest.trim_end_matches('"').trim();
 805                 let wrap = val_str == "true" || val_str == "1" || val_str == "1.0";
 806                 if let Ok(mut lock) = TEXTBOX_LINE_WRAP.write() {
 807                     *lock = wrap;
 808                 }
 809             }
 810             if let Some(rest) = trimmed.strip_prefix("touchpad_natural_scroll") {
 811                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 812                 let val_str = rest.trim_end_matches('"').trim();
 813                 let enabled = val_str == "true" || val_str == "1" || val_str == "1.0";
 814                 if let Ok(mut lock) = TOUCHPAD_NATURAL_SCROLL.write() {
 815                     *lock = enabled;
 816                 }
 817             }
 818             if let Some(rest) = trimmed.strip_prefix("textbox_multiline_border_width") {
 819                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 820                 let val_str = rest.trim_end_matches('"').trim();
 821                 if let Ok(val) = val_str.parse::<f32>() {
 822                     if let Ok(mut lock) = TEXTBOX_MULTILINE_BORDER_WIDTH.write() {
 823                         *lock = val;
 824                     }
 825                 }
 826             }
 827             if let Some(rest) = trimmed.strip_prefix("list_corner_radius") {
 828                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 829                 let val_str = rest.trim_end_matches('"').trim();
 830                 if let Ok(val) = val_str.parse::<f32>() {
 831                     if let Ok(mut lock) = LIST_CORNER_RADIUS.write() {
 832                         *lock = val;
 833                     }
 834                 }
 835             }
 836             if let Some(rest) = trimmed.strip_prefix("tree_corner_radius") {
 837                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 838                 let val_str = rest.trim_end_matches('"').trim();
 839                 if let Ok(val) = val_str.parse::<f32>() {
 840                     if let Ok(mut lock) = TREE_CORNER_RADIUS.write() {
 841                         *lock = val;
 842                     }
 843                 }
 844             }
 845             if let Some(rest) = trimmed.strip_prefix("font_selector_corner_radius") {
 846                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 847                 let val_str = rest.trim_end_matches('"').trim();
 848                 if let Ok(val) = val_str.parse::<f32>() {
 849                     if let Ok(mut lock) = FONT_SELECTOR_CORNER_RADIUS.write() {
 850                         *lock = val;
 851                     }
 852                 }
 853             }
 854             if let Some(rest) = trimmed.strip_prefix("dropdown_corner_radius") {
 855                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 856                 let val_str = rest.trim_end_matches('"').trim();
 857                 if let Ok(val) = val_str.parse::<f32>() {
 858                     if let Ok(mut lock) = DROPDOWN_CORNER_RADIUS.write() {
 859                         *lock = val;
 860                     }
 861                 }
 862             }
 863             if let Some(rest) = trimmed.strip_prefix("toggle_corner_radius") {
 864                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 865                 let val_str = rest.trim_end_matches('"').trim();
 866                 if let Ok(val) = val_str.parse::<f32>() {
 867                     if let Ok(mut lock) = TOGGLE_CORNER_RADIUS.write() {
 868                         *lock = val;
 869                     }
 870                 }
 871             }
 872             if let Some(rest) = trimmed.strip_prefix("plate_corner_radius") {
 873                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 874                 let val_str = rest.trim_end_matches('"').trim();
 875                 if let Ok(val) = val_str.parse::<f32>() {
 876                     if let Ok(mut lock) = PLATE_CORNER_RADIUS.write() {
 877                         *lock = val;
 878                     }
 879                 }
 880             }
 881             if let Some(rest) = trimmed.strip_prefix("plate_opacity") {
 882                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 883                 let val_str = rest.trim_end_matches('"').trim();
 884                 if let Ok(val) = val_str.parse::<f32>() {
 885                     if let Ok(mut lock) = PLATE_OPACITY.write() {
 886                         *lock = val;
 887                     }
 888                 }
 889             }
 890             if let Some(rest) = trimmed.strip_prefix("page_opacity") {
 891                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 892                 let val_str = rest.trim_end_matches('"').trim();
 893                 if let Ok(val) = val_str.parse::<f32>() {
 894                     if let Ok(mut lock) = PAGE_OPACITY.write() {
 895                         *lock = val;
 896                     }
 897                 }
 898             }
 899             if let Some(rest) = trimmed.strip_prefix("layer_opacity") {
 900                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 901                 let val_str = rest.trim_end_matches('"').trim();
 902                 if let Ok(val) = val_str.parse::<f32>() {
 903                     if let Ok(mut lock) = LAYER_OPACITY.write() {
 904                         *lock = val;
 905                     }
 906                 }
 907             }
 908 
 909 
 910             if let Some(rest) = trimmed.strip_prefix("toggle_height") {
 911 
 912                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 913                 let val_str = rest.trim_end_matches('"').trim();
 914                 if let Ok(val) = val_str.parse::<f32>() {
 915                     if let Ok(mut lock) = TOGGLE_HEIGHT.write() {
 916                         *lock = val;
 917                     }
 918                 }
 919             }
 920             if let Some(rest) = trimmed.strip_prefix("color_selector_height") {
 921                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 922                 let val_str = rest.trim_end_matches('"').trim();
 923                 if let Ok(val) = val_str.parse::<f32>() {
 924                     if let Ok(mut lock) = COLOR_SELECTOR_HEIGHT.write() {
 925                         *lock = val;
 926                     }
 927                 }
 928             }
 929             if let Some(rest) = trimmed.strip_prefix("font_selector_height") {
 930                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
 931                 let val_str = rest.trim_end_matches('"').trim();
 932                 if let Ok(val) = val_str.parse::<f32>() {
 933                     if let Ok(mut lock) = FONT_SELECTOR_HEIGHT.write() {
 934                         *lock = val;
 935                     }
 936                 }
 937             }
 938             if let Some(rest) = trimmed.strip_prefix("color_selector_font") {
 939                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
 940                 let rest = rest.trim();
 941                 let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
 942                     &rest[1..rest.len() - 1]
 943                 } else {
 944                     rest
 945                 };
 946                 let font = val_str.trim().to_string();
 947                 if let Ok(mut lock) = COLOR_SELECTOR_FONT.write() {
 948                     *lock = font;
 949                 }
 950             }
 951             if let Some(rest) = trimmed.strip_prefix("menubar_font") {
 952                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
 953                 let rest = rest.trim();
 954                 let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
 955                     &rest[1..rest.len() - 1]
 956                 } else {
 957                     rest
 958                 };
 959                 let font = val_str.trim().to_string();
 960                 let mut changed = false;
 961                 if let Ok(mut lock) = MENUBAR_FONT.write() {
 962                     if *lock != font {
 963                         *lock = font;
 964                         changed = true;
 965                     }
 966                 }
 967                 if changed {
 968                     menubar_font_changed = true;
 969                 }
 970             }
 971             if let Some(rest) = trimmed.strip_prefix("statusbar_font") {
 972                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
 973                 let rest = rest.trim();
 974                 let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
 975                     &rest[1..rest.len() - 1]
 976                 } else {
 977                     rest
 978                 };
 979                 let font = val_str.trim().to_string();
 980                 let mut changed = false;
 981                 if let Ok(mut lock) = STATUSBAR_FONT.write() {
 982                     if *lock != font {
 983                         *lock = font;
 984                         changed = true;
 985                     }
 986                 }
 987                 if changed {
 988                     statusbar_font_changed = true;
 989                 }
 990             }
 991             if let Some(rest) = trimmed.strip_prefix("section_label_font") {
 992                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
 993                 let rest = mod_rest(rest);
 994                 let font = rest.trim().to_string();
 995                 if let Ok(mut lock) = SECTION_LABEL_FONT.write() {
 996                     *lock = font;
 997                 }
 998             }
 999             if let Some(rest) = trimmed.strip_prefix("nested_section_label_font") {
1000                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
1001                 let rest = mod_rest(rest);
1002                 let font = rest.trim().to_string();
1003                 if let Ok(mut lock) = NESTED_SECTION_LABEL_FONT.write() {
1004                     *lock = font;
1005                 }
1006             }
1007             if let Some(rest) = trimmed.strip_prefix("breadcrumb_font") {
1008                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
1009                 let rest = mod_rest(rest);
1010                 let font = rest.trim().to_string();
1011                 if let Ok(mut lock) = BREADCRUMB_FONT.write() {
1012                     *lock = font;
1013                 }
1014             }
1015             if let Some(rest) = trimmed.strip_prefix("button_font") {
1016                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
1017                 let rest = mod_rest(rest);
1018                 let font = rest.trim().to_string();
1019                 if let Ok(mut lock) = BUTTON_FONT.write() {
1020                     *lock = font;
1021                 }
1022             }
1023             if let Some(rest) = trimmed.strip_prefix("color_selector_preview_corner_radius") {
1024                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1025                 let val_str = rest.trim_end_matches('"').trim();
1026                 if let Ok(val) = val_str.parse::<f32>() {
1027                     if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_CORNER_RADIUS.write() {
1028                         *lock = val;
1029                     }
1030                 }
1031             }
1032             if let Some(rest) = trimmed.strip_prefix("color_selector_corner_radius") {
1033                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1034                 let val_str = rest.trim_end_matches('"').trim();
1035                 if let Ok(val) = val_str.parse::<f32>() {
1036                     if let Ok(mut lock) = COLOR_SELECTOR_CORNER_RADIUS.write() {
1037                         *lock = val;
1038                     }
1039                 }
1040             }
1041             if let Some(rest) = trimmed.strip_prefix("color_selector_preview_margin") {
1042                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1043                 let val_str = rest.trim_end_matches('"').trim();
1044                 if let Ok(val) = val_str.parse::<f32>() {
1045                     if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_MARGIN.write() {
1046                         *lock = val;
1047                     }
1048                 }
1049             }
1050             if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_x") {
1051                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1052                 let val_str = rest.trim_end_matches('"').trim();
1053                 if let Ok(val) = val_str.parse::<f32>() {
1054                     if let Ok(mut lock) = PAGINATOR_TAB_PADDING_X.write() {
1055                         *lock = val;
1056                     }
1057                 }
1058             }
1059             if let Some(rest) = trimmed.strip_prefix("button_padding") {
1060                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1061                 let val_str = rest.trim_end_matches('"').trim();
1062                 if let Ok(val) = val_str.parse::<f32>() {
1063                     if let Ok(mut lock) = BUTTON_PADDING.write() {
1064                         *lock = val;
1065                     }
1066                 }
1067             } else if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_y") {
1068                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1069                 let val_str = rest.trim_end_matches('"').trim();
1070                 if let Ok(val) = val_str.parse::<f32>() {
1071                     if let Ok(mut lock) = BUTTON_PADDING.write() {
1072                         *lock = val;
1073                     }
1074                 }
1075             }
1076             if let Some(rest) = trimmed.strip_prefix("button_height") {
1077                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1078                 let val_str = rest.trim_end_matches('"').trim();
1079                 if let Ok(val) = val_str.parse::<f32>() {
1080                     if let Ok(mut lock) = BUTTON_HEIGHT.write() {
1081                         *lock = val;
1082                     }
1083                 }
1084             }
1085             if let Some(rest) = trimmed.strip_prefix("button_strip_spacing") {
1086                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1087                 let val_str = rest.trim_end_matches('"').trim();
1088                 if let Ok(val) = val_str.parse::<f32>() {
1089                     if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
1090                         *lock = val;
1091                     }
1092                 }
1093             }
1094             if let Some(rest) = trimmed.strip_prefix("textbox_height") {
1095                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1096                 let val_str = rest.trim_end_matches('"').trim();
1097                 if let Ok(val) = val_str.parse::<f32>() {
1098                     if let Ok(mut lock) = TEXTBOX_HEIGHT.write() {
1099                         *lock = val;
1100                     }
1101                 }
1102             }
1103             if let Some(rest) = trimmed.strip_prefix("dropdown_height") {
1104                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1105                 let val_str = rest.trim_end_matches('"').trim();
1106                 if let Ok(val) = val_str.parse::<f32>() {
1107                     if let Ok(mut lock) = DROPDOWN_HEIGHT.write() {
1108                         *lock = val;
1109                     }
1110                 }
1111             }
1112             if let Some(rest) = trimmed.strip_prefix("slider_height") {
1113                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1114                 let val_str = rest.trim_end_matches('"').trim();
1115                 if let Ok(val) = val_str.parse::<f32>() {
1116                     if let Ok(mut lock) = SLIDER_HEIGHT.write() {
1117                         *lock = val;
1118                     }
1119                 }
1120             }
1121             if let Some(rest) = trimmed.strip_prefix("rangeslider_height") {
1122                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1123                 let val_str = rest.trim_end_matches('"').trim();
1124                 if let Ok(val) = val_str.parse::<f32>() {
1125                     if let Ok(mut lock) = RANGESLIDER_HEIGHT.write() {
1126                         *lock = val;
1127                     }
1128                 }
1129             }
1130             if let Some(rest) = trimmed.strip_prefix("button_corner_radius") {
1131                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1132                 let val_str = rest.trim_end_matches('"').trim();
1133                 if let Ok(val) = val_str.parse::<f32>() {
1134                     if let Ok(mut lock) = BUTTON_CORNER_RADIUS.write() {
1135                         *lock = val;
1136                     }
1137                 }
1138             }
1139             if let Some(rest) = trimmed.strip_prefix("slider_corner_radius") {
1140                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1141                 let val_str = rest.trim_end_matches('"').trim();
1142                 if let Ok(val) = val_str.parse::<f32>() {
1143                     if let Ok(mut lock) = SLIDER_CORNER_RADIUS.write() {
1144                         *lock = val;
1145                     }
1146                 }
1147             }
1148             if let Some(rest) = trimmed.strip_prefix("rangeslider_corner_radius") {
1149                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1150                 let val_str = rest.trim_end_matches('"').trim();
1151                 if let Ok(val) = val_str.parse::<f32>() {
1152                     if let Ok(mut lock) = RANGESLIDER_CORNER_RADIUS.write() {
1153                         *lock = val;
1154                     }
1155                 }
1156             }
1157             if let Some(rest) = trimmed.strip_prefix("toggle_border_width") {
1158                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1159                 let val_str = rest.trim_end_matches('"').trim();
1160                 if let Ok(val) = val_str.parse::<f32>() {
1161                     if let Ok(mut lock) = TOGGLE_BORDER_WIDTH.write() {
1162                         *lock = val;
1163                     }
1164                 }
1165             }
1166             if key == "control_label_font_detached" {
1167                 let font = val_str.to_string();
1168                 let mut changed = false;
1169                 if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED.write() {
1170                     if *lock != font {
1171                         *lock = font;
1172                         changed = true;
1173                     }
1174                 }
1175                 if changed {
1176                     label_font_detached_changed = true;
1177                 }
1178             }
1179             if key == "control_label_font" {
1180                 let font = val_str.to_string();
1181                 let mut changed = false;
1182                 if let Ok(mut lock) = CONTROL_LABEL_FONT.write() {
1183                     if *lock != font {
1184                         *lock = font;
1185                         changed = true;
1186                     }
1187                 }
1188                 if changed {
1189                     label_font_changed = true;
1190                 }
1191             }
1192             if key == "font_selector_font" {
1193                 let font = val_str.to_string();
1194                 let mut changed = false;
1195                 if let Ok(mut lock) = FONT_SELECTOR_FONT.write() {
1196                     if *lock != font {
1197                         *lock = font;
1198                         changed = true;
1199                     }
1200                 }
1201                 if changed {
1202                     font_selector_font_changed = true;
1203                 }
1204             }
1205             if key == "button_strip_font" {
1206                 let font = val_str.to_string();
1207                 let mut changed = false;
1208                 if let Ok(mut lock) = BUTTON_STRIP_FONT.write() {
1209                     if *lock != font {
1210                         *lock = font;
1211                         changed = true;
1212                     }
1213                 }
1214                 if changed {
1215                     button_strip_font_changed = true;
1216                 }
1217             }
1218             if key == "list_font" {
1219                 let font = val_str.to_string();
1220                 let mut changed = false;
1221                 if let Ok(mut lock) = LIST_FONT.write() {
1222                     if *lock != font {
1223                         *lock = font;
1224                         changed = true;
1225                     }
1226                 }
1227                 if changed {
1228                     list_font_changed = true;
1229                 }
1230             }
1231             if key == "tree_font" {
1232                 let font = val_str.to_string();
1233                 let mut changed = false;
1234                 if let Ok(mut lock) = TREE_FONT.write() {
1235                     if *lock != font {
1236                         *lock = font;
1237                         changed = true;
1238                     }
1239                 }
1240                 if changed {
1241                     tree_font_changed = true;
1242                 }
1243             }
1244             if key == "graph_font" {
1245                 let font = val_str.to_string();
1246                 let mut changed = false;
1247                 if let Ok(mut lock) = GRAPH_FONT.write() {
1248                     if *lock != font {
1249                         *lock = font;
1250                         changed = true;
1251                     }
1252                 }
1253                 if changed {
1254                     graph_font_changed = true;
1255                 }
1256             }
1257             if key == "graph_node_font" {
1258                 let font = val_str.to_string();
1259                 let mut changed = false;
1260                 if let Ok(mut lock) = GRAPH_NODE_FONT.write() {
1261                     if *lock != font {
1262                         *lock = font;
1263                         changed = true;
1264                     }
1265                 }
1266                 if changed {
1267                     graph_node_font_changed = true;
1268                 }
1269             }
1270             if let Some(rest) = trimmed.strip_prefix("list_justification") {
1271                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1272                 let val_str = rest.trim_end_matches('"').trim();
1273                 if let Ok(val) = val_str.parse::<u8>() {
1274                     if let Ok(mut lock) = LIST_JUSTIFICATION.write() {
1275                         *lock = val;
1276                     }
1277                 }
1278             }
1279         }
1280         if menubar_font_changed {
1281             if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
1282                 *lock = None;
1283             }
1284         }
1285         if statusbar_font_changed {
1286             if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
1287                 *lock = None;
1288             }
1289         }
1290         if label_font_detached_changed {
1291             if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED_CACHED.write() {
1292                 *lock = None;
1293             }
1294         }
1295         if font_selector_font_changed {
1296             if let Ok(mut lock) = FONT_SELECTOR_FONT_CACHED.write() {
1297                 *lock = None;
1298             }
1299         }
1300         if button_strip_font_changed {
1301             if let Ok(mut lock) = BUTTON_STRIP_FONT_CACHED.write() {
1302                 *lock = None;
1303             }
1304         }
1305         if label_font_changed {
1306             if let Ok(mut lock) = CONTROL_LABEL_FONT_CACHED.write() {
1307                 *lock = None;
1308             }
1309         }
1310 
1311         if list_font_changed {
1312             if let Ok(mut lock) = LIST_FONT_CACHED.write() {
1313                 *lock = None;
1314             }
1315         }
1316         if tree_font_changed {
1317             if let Ok(mut lock) = TREE_FONT_CACHED.write() {
1318                 *lock = None;
1319             }
1320         }
1321         if graph_font_changed {
1322             if let Ok(mut lock) = GRAPH_FONT_CACHED.write() {
1323                 *lock = None;
1324             }
1325         }
1326         if graph_node_font_changed {
1327             if let Ok(mut lock) = GRAPH_NODE_FONT_CACHED.write() {
1328                 *lock = None;
1329             }
1330         }
1331         if let Ok(raw_kdl) = std::fs::read_to_string(crate::config::get_config_path()) {
1332             crate::color::reload_colors(&raw_kdl);
1333         }
1334         // The configured relief profiles, applied last so every process (not
1335         // just the editor that wrote them) starts with the styled walls.
1336         apply_relief_profile_config();
1337     }
1338 }
1339 
1340 /// The untouched editor curve — the "analytic" sentinel in the config'd
1341 /// profile specs (cce-designer's Edge Profile convention). For the wall curve
1342 /// identity-smooth IS the analytic smoothstep, so skipping it changes
1343 /// nothing; for the roll it would be a straight chamfer, not the analytic
1344 /// superellipse quadrant, so it must read as "no custom profile".
1345 pub const RELIEF_PROFILE_IDENTITY_SPEC: &str = "smooth;0.000:0.000,1.000:1.000";
1346 
1347 /// Parse-and-install the relief profiles config carries as ramp specs
1348 /// (`style.surface.relief.profile` / `edge_profile` → the style registry's
1349 /// `bevel_profile_spec` / `roll_profile_spec`). Absent, identity, or
1350 /// unparseable specs clear back to the analytic profiles.
1351 fn apply_relief_profile_config() {
1352     let (wall, edge) = {
1353         let reg = get_style_registry().read().unwrap();
1354         (reg.get_string("bevel_profile_spec"), reg.get_string("roll_profile_spec"))
1355     };
1356     match parse_relief_profile_spec(wall.as_deref()) {
1357         Some((keys, smooth)) => set_bevel_profile_keys(&keys, smooth),
1358         None => clear_bevel_profile(),
1359     }
1360     match parse_relief_profile_spec(edge.as_deref()) {
1361         Some((keys, smooth)) => set_roll_profile_keys(&keys, smooth),
1362         None => clear_roll_profile(),
1363     }
1364 }
1365 
1366 /// A config'd profile spec → installable keys. `None` (falling back to the
1367 /// analytic profile) for absent, identity-sentinel, or unparseable specs.
1368 fn parse_relief_profile_spec(spec: Option<&str>) -> Option<(Vec<(f32, f32)>, bool)> {
1369     spec.filter(|s| *s != RELIEF_PROFILE_IDENTITY_SPEC).and_then(crate::widget::parse_ramp_spec)
1370 }
1371 
1372 /// Install (or clear back to analytic) the WALL profile from a ramp spec —
1373 /// the entry point for a `(relief)` config value's profile
1374 /// ([`crate::relief_spec::ReliefSpec`]): an app whose feature carries its
1375 /// own material installs it process-wide here. Same identity/unparseable
1376 /// filtering as the config path above.
1377 pub fn install_wall_profile_spec(spec: Option<&str>) {
1378     match parse_relief_profile_spec(spec) {
1379         Some((keys, smooth)) => set_bevel_profile_keys(&keys, smooth),
1380         None => clear_bevel_profile(),
1381     }
1382 }
1383 
1384 fn mod_rest(rest: &str) -> &str {
1385     let rest = rest.trim();
1386     if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
1387         &rest[1..rest.len() - 1]
1388     } else {
1389         rest
1390     }
1391 }
1392 
1393 pub fn control_label_margin() -> f32 {
1394     #[cfg(test)]
1395     if let Some(v) = test_style::CONTROL_LABEL_MARGIN.with(|c| *c.borrow()) {
1396         return v;
1397     }
1398     *CONTROL_LABEL_MARGIN.read().unwrap()
1399 }
1400 
1401 pub(crate) fn control_label_strip() -> f32 {
1402     let (_, font_size) = control_label_font_detached_parsed();
1403     font_size + control_label_margin()
1404 }
1405 
1406 pub fn label_margin() -> f32 {
1407     control_label_margin()
1408 }
1409 
1410 pub fn set_control_label_margin(margin: f32) {
1411     #[cfg(test)]
1412     test_style::CONTROL_LABEL_MARGIN.with(|c| *c.borrow_mut() = Some(margin));
1413     #[cfg(not(test))]
1414     if let Ok(mut lock) = CONTROL_LABEL_MARGIN.write() {
1415         *lock = margin;
1416     }
1417 }
1418 
1419 pub fn set_label_margin(margin: f32) {
1420     set_control_label_margin(margin);
1421 }
1422 
1423 pub fn nested_section_label_alignment() -> u8 {
1424     use std::sync::Once;
1425     static INIT: Once = Once::new();
1426     INIT.call_once(|| {
1427         if let Some(content) = read_config() {
1428             for line in content.lines() {
1429                 let trimmed = line.trim();
1430                 if let Some(rest) = trimmed.strip_prefix("nested_section_label_alignment") {
1431                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1432                     let val_str = rest.trim_end_matches('"').trim();
1433                     if let Ok(val) = val_str.parse::<u8>() {
1434                         if let Ok(mut lock) = NESTED_SECTION_LABEL_ALIGNMENT.write() {
1435                             *lock = val;
1436                         }
1437                     }
1438                 }
1439             }
1440         }
1441     });
1442     *NESTED_SECTION_LABEL_ALIGNMENT.read().unwrap()
1443 }
1444 
1445 pub fn set_nested_section_label_alignment(align: u8) {
1446     if let Ok(mut lock) = NESTED_SECTION_LABEL_ALIGNMENT.write() {
1447         *lock = align;
1448     }
1449 }
1450 
1451 static NESTED_SECTION_LABEL_OFFSET: RwLock<f32> = RwLock::new(0.0);
1452 
1453 pub fn nested_section_label_offset() -> f32 {
1454     use std::sync::Once;
1455     static INIT: Once = Once::new();
1456     INIT.call_once(|| {
1457         if let Some(content) = read_config() {
1458             for line in content.lines() {
1459                 let trimmed = line.trim();
1460                 if let Some(rest) = trimmed.strip_prefix("nested_section_label_offset") {
1461                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1462                     let val_str = rest.trim_end_matches('"').trim();
1463                     if let Ok(val) = val_str.parse::<f32>() {
1464                         if let Ok(mut lock) = NESTED_SECTION_LABEL_OFFSET.write() {
1465                             *lock = val;
1466                         }
1467                     }
1468                 }
1469             }
1470         }
1471     });
1472     *NESTED_SECTION_LABEL_OFFSET.read().unwrap()
1473 }
1474 
1475 pub fn set_nested_section_label_offset(offset: f32) {
1476     if let Ok(mut lock) = NESTED_SECTION_LABEL_OFFSET.write() {
1477         *lock = offset;
1478     }
1479 }
1480 
1481 
1482 /// The pane rung's padding: from a pane plate's rim to its content, in
1483 /// logical px (`style.surface.plate.padding`). The second rung of the
1484 /// spacing ladder — [`root_plate_inset`] / [`root_plate_gap`] on the root
1485 /// plate, this and [`plate_gap`] inside a pane plate, [`control_gap`]
1486 /// between controls. Registry-backed (live-reloadable); the legacy flat
1487 /// `plate_padding = N` line still loads as a fallback.
1488 pub fn plate_padding() -> f32 {
1489     registry_float("plate_padding").unwrap_or_else(|| *PLATE_PADDING.read().unwrap())
1490 }
1491 
1492 pub fn set_plate_padding(padding: f32) {
1493     if let Ok(mut lock) = PLATE_PADDING.write() {
1494         *lock = padding;
1495     }
1496 }
1497 
1498 static PAGE_MARGIN: RwLock<Option<f32>> = RwLock::new(None);
1499 
1500 /// Legacy: the page-level margin (`style.surface.page.margin`). Unset, it
1501 /// IS the pane rung's [`plate_padding`] — a page is a pane — so an app
1502 /// still reading it lands on the ladder. Set, it is honoured as before.
1503 pub fn page_margin() -> f32 {
1504     registry_float("page_margin")
1505         .or_else(|| *PAGE_MARGIN.read().unwrap())
1506         .unwrap_or_else(plate_padding)
1507 }
1508 
1509 pub fn set_page_margin(margin: f32) {
1510     if let Ok(mut lock) = PAGE_MARGIN.write() {
1511         *lock = Some(margin);
1512     }
1513 }
1514 
1515 static GRID_MIN_COL_WIDTH: RwLock<f32> = RwLock::new(260.0);
1516 
1517 pub fn grid_min_col_width() -> f32 {
1518     use std::sync::Once;
1519     static INIT: Once = Once::new();
1520     INIT.call_once(|| {
1521         if let Some(content) = read_config() {
1522             for line in content.lines() {
1523                 let trimmed = line.trim();
1524                 if let Some(rest) = trimmed.strip_prefix("grid_min_col_width") {
1525                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1526                     let val_str = rest.trim_end_matches('"').trim();
1527                     if let Ok(val) = val_str.parse::<f32>() {
1528                         if let Ok(mut lock) = GRID_MIN_COL_WIDTH.write() {
1529                             *lock = val;
1530                         }
1531                     }
1532                 }
1533             }
1534         }
1535     });
1536     *GRID_MIN_COL_WIDTH.read().unwrap()
1537 }
1538 
1539 pub fn set_grid_min_col_width(width: f32) {
1540     if let Ok(mut lock) = GRID_MIN_COL_WIDTH.write() {
1541         *lock = width;
1542     }
1543 }
1544 
1545 static GRID_GAP: RwLock<f32> = RwLock::new(8.0);
1546 
1547 pub fn grid_gap() -> f32 {
1548     use std::sync::Once;
1549     static INIT: Once = Once::new();
1550     INIT.call_once(|| {
1551         if let Some(content) = read_config() {
1552             for line in content.lines() {
1553                 let trimmed = line.trim();
1554                 if let Some(rest) = trimmed.strip_prefix("grid_gap") {
1555                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1556                     let val_str = rest.trim_end_matches('"').trim();
1557                     if let Ok(val) = val_str.parse::<f32>() {
1558                         if let Ok(mut lock) = GRID_GAP.write() {
1559                             *lock = val;
1560                         }
1561                     }
1562                 }
1563             }
1564         }
1565     });
1566     *GRID_GAP.read().unwrap()
1567 }
1568 
1569 pub fn set_grid_gap(gap: f32) {
1570     if let Ok(mut lock) = GRID_GAP.write() {
1571         *lock = gap;
1572     }
1573 }
1574 
1575 /// Legacy: the inter-column gap (`style.layout.column.gap`). Unset, it is
1576 /// the root plate's [`root_plate_gap`] — columns are siblings on the plate.
1577 pub fn column_gap() -> f32 {
1578     registry_float("column_gap")
1579         .or_else(|| *COLUMN_GAP.read().unwrap())
1580         .unwrap_or_else(root_plate_gap)
1581 }
1582 
1583 pub fn set_column_gap(gap: f32) {
1584     if let Ok(mut lock) = COLUMN_GAP.write() {
1585         *lock = Some(gap);
1586     }
1587 }
1588 
1589 /// Legacy: a control panel's padding (`style.control.control_panel.padding`).
1590 /// Unset, it is the pane rung's [`plate_padding`] — a control panel is a pane.
1591 pub fn control_panel_padding() -> f32 {
1592     registry_float("control_panel_padding")
1593         .or_else(|| *CONTROL_PANEL_PADDING.read().unwrap())
1594         .unwrap_or_else(plate_padding)
1595 }
1596 
1597 pub fn set_control_panel_padding(padding: f32) {
1598     if let Ok(mut lock) = CONTROL_PANEL_PADDING.write() {
1599         *lock = Some(padding);
1600     }
1601 }
1602 
1603 /// Legacy: a control panel's gap (`style.control.control_panel.gap`).
1604 /// Unset, it is the pane rung's [`plate_gap`].
1605 pub fn control_panel_gap() -> f32 {
1606     registry_float("control_panel_gap")
1607         .or_else(|| *CONTROL_PANEL_GAP.read().unwrap())
1608         .unwrap_or_else(plate_gap)
1609 }
1610 
1611 pub fn set_control_panel_gap(gap: f32) {
1612     if let Ok(mut lock) = CONTROL_PANEL_GAP.write() {
1613         *lock = Some(gap);
1614     }
1615 }
1616 
1617 /// The section carves' depth multiplier (`style.container.section.depth`,
1618 /// default 1.0): scales the params pane's section-well wall — width and step
1619 /// together — relative to the DE-wide relief material, so sections can read
1620 /// deeper or shallower than the controls around them. Values past 1.0 let the
1621 /// roll widen across the channel groove between the well wall and its packed
1622 /// controls; tune to taste.
1623 pub fn section_depth() -> f32 {
1624     lazy_init_style_registry();
1625     get_style_registry().read().unwrap().get_float("section_depth").unwrap_or(1.0)
1626 }
1627 
1628 /// The parameter rows' backdrop compression
1629 /// (`style.surface.param.backdrop_compression`, 0..1): when set, every
1630 /// parameter row in a params pane is floored with the pane material frosted
1631 /// at THIS compression before its controls paint, so each parameter sits on
1632 /// a tablet that pulls the view toward the tint while the pane around it
1633 /// stays at its own — the row-scale twin of the designer's node
1634 /// compression. `None` (unset) draws no floor — the rows are bare, as they
1635 /// always were.
1636 pub fn param_compression() -> Option<f32> {
1637     lazy_init_style_registry();
1638     get_style_registry().read().unwrap().get_float("param_compression").map(|v| v.clamp(0.0, 1.0))
1639 }
1640 
1641 /// Whether a params pane lays each row's label BESIDE its control
1642 /// (`style.surface.param.label_layout = "inline"`, the default) or lets the
1643 /// control carry it in the strip above itself (`"stacked"`, the layout every
1644 /// row had before 2026-09-21). Inline, the pane owns the labels: it measures
1645 /// a label column off the widest label, hands each control the rest of the
1646 /// row, and the controls are built unlabelled — an unlabelled control takes
1647 /// its whole rect (`WidgetHost::label_strip` is zero), so the row is one
1648 /// control tall. Toggles and buttons carry their label as their own face and
1649 /// are inline either way.
1650 pub fn param_labels_inline() -> bool {
1651     lazy_init_style_registry();
1652     get_style_registry()
1653         .read()
1654         .unwrap()
1655         .get_string("param_label_layout")
1656         .map_or(true, |v| v.trim() != "stacked")
1657 }
1658 
1659 pub fn section_padding() -> f32 {
1660     #[cfg(test)]
1661     if let Some(v) = test_style::SECTION_PADDING.with(|c| *c.borrow()) {
1662         return v;
1663     }
1664     use std::sync::Once;
1665     static INIT: Once = Once::new();
1666     INIT.call_once(|| {
1667         if let Some(content) = read_config() {
1668             for line in content.lines() {
1669                 let trimmed = line.trim();
1670                 if let Some(rest) = trimmed.strip_prefix("section_padding") {
1671                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1672                     let val_str = rest.trim_end_matches('"').trim();
1673                     if let Ok(val) = val_str.parse::<f32>() {
1674                         if let Ok(mut lock) = SECTION_PADDING.write() {
1675                             *lock = val;
1676                         }
1677                     }
1678                 }
1679             }
1680         }
1681     });
1682     *SECTION_PADDING.read().unwrap()
1683 }
1684 
1685 pub fn set_section_padding(padding: f32) {
1686     #[cfg(test)]
1687     test_style::SECTION_PADDING.with(|c| *c.borrow_mut() = Some(padding));
1688     #[cfg(not(test))]
1689     if let Ok(mut lock) = SECTION_PADDING.write() {
1690         *lock = padding;
1691     }
1692 }
1693 
1694 pub fn spinbox_height() -> f32 {
1695     use std::sync::Once;
1696     static INIT: Once = Once::new();
1697     INIT.call_once(|| {
1698         if let Some(content) = read_config() {
1699             for line in content.lines() {
1700                 let trimmed = line.trim();
1701                 if let Some(rest) = trimmed.strip_prefix("spinbox_height") {
1702                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1703                     let val_str = rest.trim_end_matches('"').trim();
1704                     if let Ok(val) = val_str.parse::<f32>() {
1705                         if let Ok(mut lock) = SPINBOX_HEIGHT.write() {
1706                             *lock = val;
1707                         }
1708                     }
1709                 }
1710             }
1711         }
1712     });
1713     *SPINBOX_HEIGHT.read().unwrap()
1714 }
1715 
1716 pub fn set_spinbox_height(height: f32) {
1717     if let Ok(mut lock) = SPINBOX_HEIGHT.write() {
1718         *lock = height;
1719     }
1720 }
1721 
1722 pub fn toggle_height() -> f32 {
1723     use std::sync::Once;
1724     static INIT: Once = Once::new();
1725     INIT.call_once(|| {
1726         if let Some(content) = read_config() {
1727             for line in content.lines() {
1728                 let trimmed = line.trim();
1729                 if let Some(rest) = trimmed.strip_prefix("toggle_height") {
1730                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
1731                     let val_str = rest.trim_end_matches('"').trim();
1732                     if let Ok(val) = val_str.parse::<f32>() {
1733                         if let Ok(mut lock) = TOGGLE_HEIGHT.write() {
1734                             *lock = val;
1735                         }
1736                     }
1737                 }
1738             }
1739         }
1740     });
1741     *TOGGLE_HEIGHT.read().unwrap()
1742 }
1743 
1744 pub fn set_toggle_height(height: f32) {
1745     if let Ok(mut lock) = TOGGLE_HEIGHT.write() {
1746         *lock = height;
1747     }
1748 }
1749 
1750 pub fn light_source_position() -> f32 {
1751     let val = get_style_registry().read().unwrap().get_float("light_source_position").unwrap_or(2.3561945);
1752     if val > 2.0 * std::f32::consts::PI {
1753         val.to_radians()
1754     } else {
1755         val
1756     }
1757 }
1758 
1759 pub fn bevel_depth() -> f32 {
1760     get_style_registry().read().unwrap().get_float("bevel_depth").unwrap_or(0.15)
1761 }
1762 
1763 /// Corner shape exponent for SDF-lit plates: 2.0 (the default) is a circular
1764 /// arc; higher values are superellipse "squircle" corners with continuous
1765 /// curvature — ~4.5 is the Apple-like look. Clamped to [2, 16]: below 2 the
1766 /// Lp construction degenerates toward a chamfer, above 16 it is visually a
1767 /// square corner and the pow() terms start flirting with f32 range.
1768 pub fn corner_shape() -> f32 {
1769     lazy_init_style_registry();
1770     get_style_registry().read().unwrap().get_float("corner_shape").unwrap_or(2.0).clamp(2.0, 16.0)
1771 }
1772 
1773 /// The window silhouette's nominal corner radius: the SHARED config's
1774 /// root plate corner_radius, never the per-app override. The compositor clips
1775 /// every decorated window with this value (widened by
1776 /// [`corner_span_factor`]), so any window-corner arc an app draws itself must
1777 /// use it too — even when the app restyles its own plates through its
1778 /// override file — or its corners detach from the silhouette (and from the
1779 /// desktop grid's cells, which share the same knob).
1780 pub fn window_corner_radius() -> f32 {
1781     crate::config::get_i64_shared("/style/surface/plate/root/corner_radius", 12) as f32
1782 }
1783 
1784 /// The curvature-matched corner-span factor for window-scale squircle corners.
1785 /// A raw superellipse of exponent n at a circle's nominal radius turns tighter
1786 /// at the diagonal than that circle — its radius of curvature there is
1787 /// √2·r / (2^(1/n)·(n − 1)). Scaling the corner span by this factor makes the
1788 /// diagonal curvature equal the configured radius, so the corner reads as the
1789 /// same size as a circular one (the same reason Apple's continuous corners run
1790 /// ~1.5·r along the edge). Exactly 1 at n = 2. Applied to window-scale corners
1791 /// only — `Prim::Plate` and the renderer's window-corner clip — never to
1792 /// widget-scale radii, which must match the nominal-radius squircles around them.
1793 /// The window silhouette's EFFECTIVE corner radius: the shared nominal value
1794 /// widened by the corner-span factor — exactly the arc the compositor clips
1795 /// every decorated window with, and the radius a root plate's corners must
1796 /// wear (RFC Phase 7b; `PlateSpec::radii_for` uses it for window-flagged
1797 /// corners). Apps drawing root-surface geometry through non-Plate prims read
1798 /// this scalar directly.
1799 pub fn window_silhouette_radius() -> f32 {
1800     window_corner_radius() * corner_span_factor()
1801 }
1802 
1803 pub fn corner_span_factor() -> f32 {
1804     corner_span_factor_for(corner_shape())
1805 }
1806 
1807 /// The span factor for an explicit corner exponent — what a plate carrying
1808 /// its own `shape` (see `scene::paint::Prim::Plate`) scales its radii by.
1809 /// Same clamp as [`corner_shape`], so an override cannot reach an exponent
1810 /// the shader would not accept.
1811 pub fn corner_span_factor_for(n: f32) -> f32 {
1812     let n = n.clamp(2.0, 16.0);
1813     if n > 2.001 {
1814         (n - 1.0) * 2f32.powf(1.0 / n) / std::f32::consts::SQRT_2
1815     } else {
1816         1.0
1817     }
1818 }
1819 
1820 /// Whether the relief primitives (see `scene::paint::Prim`) render through
1821 /// shader2d's per-pixel SDF-lit branch (the default) or the legacy banded vertex
1822 /// shading. `bevel_shader 0` in config flips back to the old look for A/B
1823 /// comparison — the key keeps the bevel name because it selects how the shared
1824 /// lit EDGE is computed, not which shapes exist.
1825 pub fn bevel_shader() -> bool {
1826     lazy_init_style_registry();
1827     get_style_registry().read().unwrap().get_float("bevel_shader").map(|v| v != 0.0).unwrap_or(true)
1828 }
1829 
1830 /// How wide a rolled edge is, in logical px — the distance over which a plate's perimeter
1831 /// or a recess wall curves away from the flat surface. `bevel_depth` is the companion
1832 /// knob: it sets how hard the light falls across that distance. Wide and shallow reads as
1833 /// thick glass; narrow and deep reads as a stamped metal lip.
1834 pub fn bevel_width() -> f32 {
1835     lazy_init_style_registry();
1836     get_style_registry().read().unwrap().get_float("bevel_width").unwrap_or(9.3)
1837 }
1838 
1839 /// A carve's geometric drop when the material pins one
1840 /// (`style.surface.relief.height`, a length — `(mm)0.3` resolves through
1841 /// the display metric), in logical px. `None` = follow the wall width at the
1842 /// analytic ratio ([`crate::scene::relief_shade::RECESS_DEPTH`]), the look
1843 /// every config had before heights existed. A configured 0 reads as unset,
1844 /// which is how an editor puts a material back on "follow".
1845 pub fn bevel_height() -> Option<f32> {
1846     lazy_init_style_registry();
1847     get_style_registry()
1848         .read()
1849         .unwrap()
1850         .get_float("bevel_height")
1851         .filter(|h| h.is_finite() && *h > 0.0)
1852 }
1853 
1854 /// The plate roll's rise when pinned (`style.surface.relief.edge_height`, a
1855 /// length), logical px. `None` = a quarter-round of radius `bevel_width`.
1856 pub fn roll_height() -> Option<f32> {
1857     lazy_init_style_registry();
1858     get_style_registry()
1859         .read()
1860         .unwrap()
1861         .get_float("roll_height")
1862         .filter(|h| h.is_finite() && *h > 0.0)
1863 }
1864 
1865 /// The drop of a carve whose wall runs `wall` logical px: the pinned height
1866 /// when there is one, else the analytic ratio of the wall — saturating at the
1867 /// DE's roll width, so a wall wider than the plate's own perimeter roll
1868 /// spreads the same step over a longer run (a softer transition) instead of
1869 /// cutting proportionally deeper. The tessellator's CSG features and the
1870 /// shader's free carves both derive from this rule.
1871 pub fn carve_depth_px(wall: f32) -> f32 {
1872     match bevel_height() {
1873         Some(h) => h,
1874         None => crate::scene::relief_shade::RECESS_DEPTH * wall.min(bevel_width()),
1875     }
1876 }
1877 
1878 /// Drop over run for a wall of the DE roll width — what the shading twin
1879 /// scales its slopes by.
1880 pub fn carve_depth_ratio() -> f32 {
1881     let w = bevel_width().max(0.001);
1882     carve_depth_px(w) / w
1883 }
1884 
1885 /// Rise over run of the plate roll: 1 (the quarter-round) unless pinned.
1886 pub fn roll_height_ratio() -> f32 {
1887     roll_height().map_or(1.0, |h| h / bevel_width().max(0.001))
1888 }
1889 
1890 /// Sample count of the custom bevel profile LUT ([`set_bevel_profile_keys`]).
1891 pub const BEVEL_PROFILE_SAMPLES: usize = 32;
1892 
1893 /// The custom bevel/carve height profile, as the slope LUT the renderer uploads
1894 /// to the 2D shader: slot `i` holds `h'` at `v = (i + 0.5) / N` of the wall's
1895 /// height curve `h(v)` (`v` runs 0 at the surrounding plateau → 1 at the carve
1896 /// floor / boss crest; `h` in units of the feature's depth, so a 0→1 curve is
1897 /// the classic full-depth bevel and a curve ending back at its start height is
1898 /// a pure decorative rim). `None` = the analytic smoothstep profile.
1899 static BEVEL_PROFILE: std::sync::RwLock<Option<[f32; BEVEL_PROFILE_SAMPLES]>> =
1900     std::sync::RwLock::new(None);
1901 /// Bumped on every profile change so renderers know to re-upload their LUT.
1902 static BEVEL_PROFILE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1903 
1904 /// The custom EDGE (plate roll) profile — same slope-LUT encoding as
1905 /// [`BEVEL_PROFILE`], but read by the shader's `roll_slope` for the perimeter
1906 /// roll of widget-scale plates: `v` runs 0 at the face join → 1 at the
1907 /// silhouette, and the curve is the roll's descent progress (0 = face height,
1908 /// 1 = fully dropped), so the identity curve is a straight chamfer and `None`
1909 /// is the analytic superellipse quadrant.
1910 static ROLL_PROFILE: std::sync::RwLock<Option<[f32; BEVEL_PROFILE_SAMPLES]>> =
1911     std::sync::RwLock::new(None);
1912 static ROLL_PROFILE_GEN: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
1913 
1914 /// Evaluate a ramp key list at `t` — THE ramp interpolation of the DE.
1915 /// `cce_ui::widget::Ramp` draws it, `RampPreview` previews it, the relief
1916 /// profile LUTs sample it, and cce-window-manager's camera speed ramp mirrors
1917 /// it verbatim (that crate stays dependency-minimal), so a curve sculpted in
1918 /// the widget is exactly the curve every consumer evaluates. Keys are
1919 /// `(pos, value)` sorted by pos; outside the key range the end values hold.
1920 ///
1921 /// `smooth` is the widget's curved line type: a **monotone cubic** through
1922 /// the keys (Fritsch–Butland tangents, cubic Hermite segments) — C1, passes
1923 /// through every key, never overshoots a key, and flattens only at the ends
1924 /// and at genuine local extrema. It used to be a smoothstep blend PER
1925 /// SEGMENT, which forces zero slope at every key: a curve with more than two
1926 /// keys came out as a chain of little bumps, and a wall profile built from
1927 /// it read as jagged and uneven where a smooth slope was drawn. A two-key
1928 /// ramp is unchanged — zero tangents at both ends make the single Hermite
1929 /// segment exactly the old smoothstep — so the identity sentinel and every
1930 /// simple ease keep their look. `false` is straight segments.
1931 pub fn sample_ramp_keys(keys: &[(f32, f32)], smooth: bool, t: f32) -> f32 {
1932     let Some(first) = keys.first() else { return 0.0 };
1933     let last = keys.last().unwrap();
1934     if t <= first.0 {
1935         return first.1;
1936     }
1937     if t >= last.0 {
1938         return last.1;
1939     }
1940     for i in 0..keys.len() - 1 {
1941         let ((x0, y0), (x1, y1)) = (keys[i], keys[i + 1]);
1942         if t < x0 || t > x1 {
1943             continue;
1944         }
1945         let h = x1 - x0;
1946         if h.abs() < 0.0001 {
1947             return y0;
1948         }
1949         let s = (t - x0) / h;
1950         if !smooth {
1951             return y0 + (y1 - y0) * s;
1952         }
1953         let (m0, m1) = (ramp_key_tangent(keys, i), ramp_key_tangent(keys, i + 1));
1954         let (s2, s3) = (s * s, s * s * s);
1955         let h00 = 2.0 * s3 - 3.0 * s2 + 1.0;
1956         let h10 = s3 - 2.0 * s2 + s;
1957         let h01 = -2.0 * s3 + 3.0 * s2;
1958         let h11 = s3 - s2;
1959         return h00 * y0 + h10 * h * m0 + h01 * y1 + h11 * h * m1;
1960     }
1961     first.1
1962 }
1963 
1964 /// The monotone cubic's tangent (dy/dpos) at key `i`: zero at either end and
1965 /// at any local extremum (so the curve never overshoots a key), otherwise the
1966 /// Fritsch–Butland weighted harmonic mean of the two neighbouring secants —
1967 /// the shape-preserving choice, which keeps every segment monotone whenever
1968 /// its keys are.
1969 fn ramp_key_tangent(keys: &[(f32, f32)], i: usize) -> f32 {
1970     if i == 0 || i + 1 >= keys.len() {
1971         return 0.0;
1972     }
1973     let ((xp, yp), (x, y), (xn, yn)) = (keys[i - 1], keys[i], keys[i + 1]);
1974     let (h0, h1) = (x - xp, xn - x);
1975     if h0 <= 0.0001 || h1 <= 0.0001 {
1976         return 0.0;
1977     }
1978     let (d0, d1) = ((y - yp) / h0, (yn - y) / h1);
1979     if d0 * d1 <= 0.0 {
1980         return 0.0;
1981     }
1982     let (w0, w1) = (2.0 * h1 + h0, h1 + 2.0 * h0);
1983     (w0 + w1) / (w0 / d0 + w1 / d1)
1984 }
1985 
1986 #[cfg(test)]
1987 mod ramp_sampling_tests {
1988     use super::sample_ramp_keys;
1989 
1990     #[test]
1991     fn two_key_smooth_is_exactly_smoothstep() {
1992         let keys = [(0.0, 0.0), (1.0, 1.0)];
1993         for i in 0..=20 {
1994             let t = i as f32 / 20.0;
1995             let ss = t * t * (3.0 - 2.0 * t);
1996             assert!((sample_ramp_keys(&keys, true, t) - ss).abs() < 1e-6, "t={t}");
1997         }
1998     }
1999 
2000     #[test]
2001     fn passes_through_every_key_and_holds_the_ends() {
2002         let keys = [(0.0, 0.0), (0.15, 0.45), (0.35, 0.7), (0.55, 0.78), (0.75, 0.85), (1.0, 1.0)];
2003         for &(p, v) in &keys {
2004             assert!((sample_ramp_keys(&keys, true, p) - v).abs() < 1e-6, "key {p}");
2005         }
2006         assert_eq!(sample_ramp_keys(&keys, true, -1.0), 0.0);
2007         assert_eq!(sample_ramp_keys(&keys, true, 2.0), 1.0);
2008     }
2009 
2010     #[test]
2011     fn monotone_keys_give_a_monotone_curve_without_wobble() {
2012         // The wall profile that came out as a chain of bumps under the old
2013         // per-segment smoothstep.
2014         let keys = [(0.0, 0.0), (0.15, 0.45), (0.35, 0.7), (0.55, 0.78), (0.75, 0.85), (1.0, 1.0)];
2015         let mut last = -1.0f32;
2016         let mut slopes = Vec::new();
2017         for i in 0..=400 {
2018             let t = i as f32 / 400.0;
2019             let v = sample_ramp_keys(&keys, true, t);
2020             assert!(v >= last - 1e-6, "not monotone at t={t}: {v} < {last}");
2021             slopes.push(v - last);
2022             last = v;
2023         }
2024         // No wobble: the slope at an INTERIOR key is a real slope, not the
2025         // zero the old blend pinned there (0.35: secants 1.25 and 0.4 either
2026         // side — the harmonic mean is well above half the smaller one).
2027         let dv = (sample_ramp_keys(&keys, true, 0.355) - sample_ramp_keys(&keys, true, 0.345)) / 0.01;
2028         assert!(dv > 0.3, "slope at key 0.35 is {dv}");
2029         // …and the slope never flips sign back and forth between keys: at
2030         // most one local slope maximum per segment would be a stretch to
2031         // assert, so pin the direct symptom — the curve stays inside the
2032         // key hull (no overshoot beyond the neighbouring key values).
2033         for i in 0..keys.len() - 1 {
2034             let (a, b) = (keys[i], keys[i + 1]);
2035             for j in 1..10 {
2036                 let t = a.0 + (b.0 - a.0) * j as f32 / 10.0;
2037                 let v = sample_ramp_keys(&keys, true, t);
2038                 assert!(v >= a.1.min(b.1) - 1e-6 && v <= a.1.max(b.1) + 1e-6, "overshoot at t={t}: {v}");
2039             }
2040         }
2041     }
2042 
2043     #[test]
2044     fn a_peak_is_flat_at_the_peak_and_never_overshoots() {
2045         // The desktop overview speed ramp: rises then falls.
2046         let keys = [(0.0, 0.15), (0.4, 1.0), (1.0, 0.1)];
2047         assert!((sample_ramp_keys(&keys, true, 0.4) - 1.0).abs() < 1e-6);
2048         for i in 0..=100 {
2049             let v = sample_ramp_keys(&keys, true, i as f32 / 100.0);
2050             assert!(v <= 1.0 + 1e-6 && v >= 0.1 - 1e-6, "overshoot {v}");
2051         }
2052         let near = sample_ramp_keys(&keys, true, 0.39);
2053         assert!(near > 0.99, "flat at the extremum: {near}");
2054     }
2055 
2056     #[test]
2057     fn linear_is_untouched() {
2058         let keys = [(0.0, 0.0), (0.5, 1.0), (1.0, 0.0)];
2059         assert!((sample_ramp_keys(&keys, false, 0.25) - 0.5).abs() < 1e-6);
2060         assert!((sample_ramp_keys(&keys, false, 0.75) - 0.5).abs() < 1e-6);
2061     }
2062 }
2063 
2064 /// Install a custom bevel/carve profile from ramp keys (`(pos, value)`, both
2065 /// 0..1, sorted by pos). Sampled into the slope LUT the shader's `carve_slope`
2066 /// reads in place of its analytic smoothstep — every recess/boss/ridge wall in
2067 /// this process restyles on the next frame. Empty or single-key lists clear
2068 /// back to the analytic profile ([`clear_bevel_profile`]).
2069 pub fn set_bevel_profile_keys(keys: &[(f32, f32)], smooth: bool) {
2070     let Some(lut) = ramp_profile_lut(keys, smooth) else {
2071         clear_bevel_profile();
2072         return;
2073     };
2074     *BEVEL_PROFILE.write().unwrap() = Some(lut);
2075     BEVEL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
2076 }
2077 
2078 /// What a key list installs: its slope LUT, or `None` for a degenerate list
2079 /// (fewer than two keys is no curve at all) — the caller clears back to the
2080 /// analytic profile. The pure half of `set_*_profile_keys`.
2081 fn ramp_profile_lut(keys: &[(f32, f32)], smooth: bool) -> Option<[f32; BEVEL_PROFILE_SAMPLES]> {
2082     if keys.len() < 2 {
2083         return None;
2084     }
2085     Some(ramp_slope_lut(keys, smooth))
2086 }
2087 
2088 /// A ramp key list sampled into the shader's slope LUT — slot `i` holds the
2089 /// curve's slope at `v = (i + 0.5) / N`.
2090 fn ramp_slope_lut(keys: &[(f32, f32)], smooth: bool) -> [f32; BEVEL_PROFILE_SAMPLES] {
2091     let n = BEVEL_PROFILE_SAMPLES;
2092     let mut slopes = [0.0f32; BEVEL_PROFILE_SAMPLES];
2093     for (i, slot) in slopes.iter_mut().enumerate() {
2094         let h0 = sample_ramp_keys(keys, smooth, i as f32 / n as f32);
2095         let h1 = sample_ramp_keys(keys, smooth, (i + 1) as f32 / n as f32);
2096         *slot = (h1 - h0) * n as f32;
2097     }
2098     slopes
2099 }
2100 
2101 /// Install a custom EDGE profile for the plate perimeter roll from ramp keys —
2102 /// the [`set_bevel_profile_keys`] twin for [`ROLL_PROFILE`]. The curve is the
2103 /// roll's descent progress from the face join (0) to the silhouette (1); the
2104 /// shader's `roll_slope` samples it in place of the analytic superellipse
2105 /// quadrant. Empty or single-key lists clear back to the analytic roll.
2106 pub fn set_roll_profile_keys(keys: &[(f32, f32)], smooth: bool) {
2107     let Some(lut) = ramp_profile_lut(keys, smooth) else {
2108         clear_roll_profile();
2109         return;
2110     };
2111     *ROLL_PROFILE.write().unwrap() = Some(lut);
2112     ROLL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
2113 }
2114 
2115 /// Drop the custom edge profile — plate rolls return to the analytic quadrant.
2116 pub fn clear_roll_profile() {
2117     let mut guard = ROLL_PROFILE.write().unwrap();
2118     if guard.is_some() {
2119         *guard = None;
2120         ROLL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
2121     }
2122 }
2123 
2124 /// The installed edge profile's slope LUT, if any — what the renderer uploads.
2125 pub fn roll_profile_slopes() -> Option<[f32; BEVEL_PROFILE_SAMPLES]> {
2126     *ROLL_PROFILE.read().unwrap()
2127 }
2128 
2129 /// Change counter for [`roll_profile_slopes`].
2130 pub fn roll_profile_generation() -> u64 {
2131     ROLL_PROFILE_GEN.load(std::sync::atomic::Ordering::Acquire)
2132 }
2133 
2134 /// Drop the custom bevel profile — walls return to the analytic smoothstep.
2135 pub fn clear_bevel_profile() {
2136     let mut guard = BEVEL_PROFILE.write().unwrap();
2137     if guard.is_some() {
2138         *guard = None;
2139         BEVEL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
2140     }
2141 }
2142 
2143 /// The installed profile's slope LUT, if any — what the renderer uploads.
2144 pub fn bevel_profile_slopes() -> Option<[f32; BEVEL_PROFILE_SAMPLES]> {
2145     *BEVEL_PROFILE.read().unwrap()
2146 }
2147 
2148 /// Change counter for [`bevel_profile_slopes`] — a renderer re-uploads when it
2149 /// differs from the generation it last wrote.
2150 pub fn bevel_profile_generation() -> u64 {
2151     BEVEL_PROFILE_GEN.load(std::sync::atomic::Ordering::Acquire)
2152 }
2153 
2154 /// Padding between the window plate's edge and the objects sitting on it, in
2155 /// logical px (`style.surface.plate.root.padding` in config.kdl). DE-wide so
2156 /// every app's content sits the same distance off the plate rim.
2157 pub fn root_plate_padding() -> f32 {
2158     lazy_init_style_registry();
2159     get_style_registry().read().unwrap().get_float("root_plate_padding").unwrap_or(16.0)
2160 }
2161 
2162 /// Gap between sibling objects on the window plate, in logical px
2163 /// (`style.surface.plate.root.gap`) — pane splits, control rows. The companion to [`root_plate_padding`]:
2164 /// rim distance vs object spacing.
2165 pub fn root_plate_gap() -> f32 {
2166     lazy_init_style_registry();
2167     get_style_registry().read().unwrap().get_float("root_plate_gap").unwrap_or(12.0)
2168 }
2169 
2170 /// Where content starts on the standard root plate, measured from the
2171 /// WINDOW edge: the plate's rolled rim ([`bevel_width`]) plus one
2172 /// [`root_plate_padding`]. The padding is a run of flat plate face, the
2173 /// same run [`root_plate_gap`] leaves between two siblings; but the face
2174 /// only begins where the roll ends, so a bare padding at a window edge
2175 /// leaves most of it on the roll — measured at 4px of visible flat against
2176 /// 12 between panes (cce-mail, 2026-09-19). This is the one number an app
2177 /// on the standard plate insets by at its four edges; between siblings it
2178 /// uses the gap, and everything inside a pane plate uses
2179 /// [`plate_padding`]. An app whose base is NOT the rolled root plate (a
2180 /// transparent surface, a bare fill) has no roll to clear and insets by
2181 /// [`root_plate_padding`] alone.
2182 pub fn root_plate_inset() -> f32 {
2183     bevel_width() + root_plate_padding()
2184 }
2185 
2186 /// One style-registry float, initialising the registry on first use — the
2187 /// one read every rung getter goes through.
2188 fn registry_float(slot: &str) -> Option<f32> {
2189     lazy_init_style_registry();
2190     get_style_registry().read().unwrap().get_float(slot)
2191 }
2192 
2193 /// Gap between siblings INSIDE a pane plate, in logical px
2194 /// (`style.surface.plate.gap`) — the pane rung's twin of
2195 /// [`root_plate_gap`]. Unset, it is the root gap: one number reads as one
2196 /// rhythm across both rungs unless a config says otherwise.
2197 pub fn plate_gap() -> f32 {
2198     registry_float("plate_gap").unwrap_or_else(root_plate_gap)
2199 }
2200 
2201 /// Gap between controls, in logical px (`style.control.gap`) — the control
2202 /// rung of the ladder: what the layout strategies put between a form's
2203 /// controls (and between a detached label's block and the next), in both
2204 /// axes. Unset, it is [`CONTROL_GAP`], one control height, the value every
2205 /// strategy's `Default` carried as a literal.
2206 pub fn control_gap() -> f32 {
2207     registry_float("control_gap").unwrap_or(CONTROL_GAP)
2208 }
2209 
2210 /// Roll-off width for the wall where a bar (menubar / status bar / the demo's
2211 /// header band) steps down into the window plate. Wider than the plate's own
2212 /// perimeter roll on purpose: the carve depth saturates at `bevel_width` in the
2213 /// tessellator, so the extra width flattens the wall's slope — a soft, gradual
2214 /// transition into the bar — instead of cutting a proportionally deeper groove.
2215 pub fn bar_wall_width() -> f32 {
2216     bevel_width() * 1.75
2217 }
2218 
2219 /// DE-wide default for the controls' relief styling (`window_manager.control_relief`
2220 /// in config.kdl, default on): raised Button/Toggle/Dropdown plates, recessed
2221 /// TextBox/Slider wells, recessed MenuBar/StatusBar bands. Widgets read this
2222 /// LIVE, at paint and layout, so [`set_control_relief`] restyles every control
2223 /// in the process at once; the per-widget `with_raised` / `with_recessed` /
2224 /// `with_recess` builders pin one widget either way. `0` reverts the whole DE
2225 /// to the flat look.
2226 pub fn control_relief() -> bool {
2227     lazy_init_style_registry();
2228     get_style_registry().read().unwrap().get_float("control_relief").map(|v| v != 0.0).unwrap_or(true)
2229 }
2230 
2231 /// Switch the controls' relief styling at runtime — the gallery's Style
2232 /// dropdown. Every widget without a per-widget override follows on its next
2233 /// paint; the caller asks for a rebuild. Not persisted: a config reload puts
2234 /// the configured value back.
2235 pub fn set_control_relief(relief: bool) {
2236     lazy_init_style_registry();
2237     get_style_registry().write().unwrap().set_float("control_relief", if relief { 1.0 } else { 0.0 });
2238 }
2239 
2240 pub fn toggle_corner_radius() -> f32 {
2241     lazy_init_style_registry();
2242     get_style_registry().read().unwrap().get_float("toggle_corner_radius").unwrap_or_else(control_corner_radius)
2243 }
2244 
2245 pub fn set_toggle_corner_radius(radius: f32) {
2246     lazy_init_style_registry();
2247     if let Ok(mut registry) = get_style_registry().write() {
2248         registry.set_float("toggle_corner_radius", radius);
2249     }
2250 }
2251 
2252 pub fn toggle_border_width() -> f32 {
2253     use std::sync::Once;
2254     static INIT: Once = Once::new();
2255     INIT.call_once(|| {
2256         if let Some(content) = read_config() {
2257             for line in content.lines() {
2258                 let trimmed = line.trim();
2259                 if let Some(rest) = trimmed.strip_prefix("toggle_border_width") {
2260                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2261                     let val_str = rest.trim_end_matches('"').trim();
2262                     if let Ok(val) = val_str.parse::<f32>() {
2263                         if let Ok(mut lock) = TOGGLE_BORDER_WIDTH.write() {
2264                             *lock = val;
2265                         }
2266                     }
2267                 }
2268             }
2269         }
2270     });
2271     *TOGGLE_BORDER_WIDTH.read().unwrap()
2272 }
2273 
2274 pub fn set_toggle_border_width(width: f32) {
2275     if let Ok(mut lock) = TOGGLE_BORDER_WIDTH.write() {
2276         *lock = width;
2277     }
2278 }
2279 
2280 pub fn slider_corner_radius() -> f32 {
2281     lazy_init_style_registry();
2282     get_style_registry().read().unwrap().get_float("slider_corner_radius").unwrap_or_else(control_corner_radius)
2283 }
2284 
2285 /// The slider band's knobs (`style.control.slider.*`): the flat band's thickness,
2286 /// and the swell's half-span / peak height around the value position. The band —
2287 /// a thin full-range band that swells at the value — is the one slider style.
2288 pub fn slider_band_thickness() -> f32 {
2289     lazy_init_style_registry();
2290     get_style_registry().read().unwrap().get_float("slider_band_thickness").unwrap_or(2.0)
2291 }
2292 
2293 pub fn slider_bulge_width() -> f32 {
2294     lazy_init_style_registry();
2295     get_style_registry().read().unwrap().get_float("slider_bulge_width").unwrap_or(26.0)
2296 }
2297 
2298 pub fn slider_bulge_height() -> f32 {
2299     lazy_init_style_registry();
2300     get_style_registry().read().unwrap().get_float("slider_bulge_height").unwrap_or(14.0)
2301 }
2302 
2303 pub fn set_slider_corner_radius(radius: f32) {
2304     lazy_init_style_registry();
2305     if let Ok(mut registry) = get_style_registry().write() {
2306         registry.set_float("slider_corner_radius", radius);
2307     }
2308 }
2309 
2310 
2311 pub fn plate_corner_radius() -> f32 {
2312     lazy_init_style_registry();
2313     let r = get_style_registry().read().unwrap();
2314     r.get_float("plate_corner_radius")
2315         .or_else(|| r.get_float("root_plate_corner_radius"))
2316         .unwrap_or(12.0)
2317 }
2318 
2319 pub fn set_plate_corner_radius(radius: f32) {
2320     lazy_init_style_registry();
2321     if let Ok(mut registry) = get_style_registry().write() {
2322         registry.set_float("plate_corner_radius", radius);
2323     }
2324 }
2325 
2326 pub fn plate_opacity() -> f32 {
2327     use std::sync::Once;
2328     static INIT: Once = Once::new();
2329     INIT.call_once(|| {
2330         if let Some(content) = read_config() {
2331             for line in content.lines() {
2332                 let trimmed = line.trim();
2333                 if let Some(rest) = trimmed.strip_prefix("plate_opacity") {
2334                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2335                     let val_str = rest.trim_end_matches('"').trim();
2336                     if let Ok(val) = val_str.parse::<f32>() {
2337                         if let Ok(mut lock) = PLATE_OPACITY.write() {
2338                             *lock = val;
2339                         }
2340                     }
2341                 }
2342             }
2343         }
2344     });
2345     *PLATE_OPACITY.read().unwrap()
2346 }
2347 
2348 pub fn set_plate_opacity(opacity: f32) {
2349     if let Ok(mut lock) = PLATE_OPACITY.write() {
2350         *lock = opacity;
2351     }
2352 }
2353 
2354 pub fn page_opacity() -> f32 {
2355     use std::sync::Once;
2356     static INIT: Once = Once::new();
2357     INIT.call_once(|| {
2358         if let Some(content) = read_config() {
2359             for line in content.lines() {
2360                 let trimmed = line.trim();
2361                 if let Some(rest) = trimmed.strip_prefix("page_opacity") {
2362                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2363                     let val_str = rest.trim_end_matches('"').trim();
2364                     if let Ok(val) = val_str.parse::<f32>() {
2365                         if let Ok(mut lock) = PAGE_OPACITY.write() {
2366                             *lock = val;
2367                         }
2368                     }
2369                 }
2370             }
2371         }
2372     });
2373     *PAGE_OPACITY.read().unwrap()
2374 }
2375 
2376 pub fn set_page_opacity(opacity: f32) {
2377     if let Ok(mut lock) = PAGE_OPACITY.write() {
2378         *lock = opacity;
2379     }
2380 }
2381 
2382 pub fn layer_opacity() -> f32 {
2383     use std::sync::Once;
2384     static INIT: Once = Once::new();
2385     INIT.call_once(|| {
2386         if let Some(content) = read_config() {
2387             for line in content.lines() {
2388                 let trimmed = line.trim();
2389                 if let Some(rest) = trimmed.strip_prefix("layer_opacity") {
2390                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2391                     let val_str = rest.trim_end_matches('"').trim();
2392                     if let Ok(val) = val_str.parse::<f32>() {
2393                         if let Ok(mut lock) = LAYER_OPACITY.write() {
2394                             *lock = val;
2395                         }
2396                     }
2397                 }
2398             }
2399         }
2400     });
2401     *LAYER_OPACITY.read().unwrap()
2402 }
2403 
2404 pub fn set_layer_opacity(opacity: f32) {
2405     if let Ok(mut lock) = LAYER_OPACITY.write() {
2406         *lock = opacity;
2407     }
2408 }
2409 
2410 
2411 
2412 
2413 pub fn color_selector_height() -> f32 {
2414     use std::sync::Once;
2415     static INIT: Once = Once::new();
2416     INIT.call_once(|| {
2417         if let Some(content) = read_config() {
2418             for line in content.lines() {
2419                 let trimmed = line.trim();
2420                 if let Some(rest) = trimmed.strip_prefix("color_selector_height") {
2421                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2422                     let val_str = rest.trim_end_matches('"').trim();
2423                     if let Ok(val) = val_str.parse::<f32>() {
2424                         if let Ok(mut lock) = COLOR_SELECTOR_HEIGHT.write() {
2425                             *lock = val;
2426                         }
2427                     }
2428                 }
2429             }
2430         }
2431     });
2432     *COLOR_SELECTOR_HEIGHT.read().unwrap()
2433 }
2434 
2435 pub fn set_color_selector_height(height: f32) {
2436     if let Ok(mut lock) = COLOR_SELECTOR_HEIGHT.write() {
2437         *lock = height;
2438     }
2439 }
2440 
2441 pub fn font_selector_height() -> f32 {
2442     use std::sync::Once;
2443     static INIT: Once = Once::new();
2444     INIT.call_once(|| {
2445         if let Some(content) = read_config() {
2446             for line in content.lines() {
2447                 let trimmed = line.trim();
2448                 if let Some(rest) = trimmed.strip_prefix("font_selector_height") {
2449                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2450                     let val_str = rest.trim_end_matches('"').trim();
2451                     if let Ok(val) = val_str.parse::<f32>() {
2452                         if let Ok(mut lock) = FONT_SELECTOR_HEIGHT.write() {
2453                             *lock = val;
2454                         }
2455                     }
2456                 }
2457             }
2458         }
2459     });
2460     *FONT_SELECTOR_HEIGHT.read().unwrap()
2461 }
2462 
2463 pub fn set_font_selector_height(height: f32) {
2464     if let Ok(mut lock) = FONT_SELECTOR_HEIGHT.write() {
2465         *lock = height;
2466     }
2467 }
2468 
2469 pub fn color_selector_font() -> String {
2470     use std::sync::Once;
2471     static INIT: Once = Once::new();
2472     INIT.call_once(|| {
2473         let mut font = "monospace".to_string();
2474         if let Some(content) = read_config() {
2475             for line in content.lines() {
2476                 let trimmed = line.trim();
2477                 if let Some(rest) = trimmed.strip_prefix("color_selector_font") {
2478                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
2479                     let rest = rest.trim();
2480                     let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
2481                         &rest[1..rest.len() - 1]
2482                     } else {
2483                         rest
2484                     };
2485                     font = val_str.trim().to_string();
2486                 }
2487             }
2488         }
2489         if let Ok(mut lock) = COLOR_SELECTOR_FONT.write() {
2490             *lock = font;
2491         }
2492     });
2493     let lock = COLOR_SELECTOR_FONT.read().unwrap();
2494     if lock.is_empty() {
2495         "monospace".to_string()
2496     } else {
2497         lock.clone()
2498     }
2499 }
2500 
2501 pub fn set_color_selector_font(font: &str) {
2502     if let Ok(mut lock) = COLOR_SELECTOR_FONT.write() {
2503         *lock = font.to_string();
2504     }
2505 }
2506 
2507 pub fn menubar_font() -> String {
2508     use std::sync::Once;
2509     static INIT: Once = Once::new();
2510     INIT.call_once(|| {
2511         let font = read_config_value("menubar_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2512         if let Ok(mut lock) = MENUBAR_FONT.write() {
2513             *lock = font;
2514         }
2515     });
2516     let lock = MENUBAR_FONT.read().unwrap();
2517     if lock.is_empty() {
2518         "Berkeley Mono".to_string()
2519     } else {
2520         lock.clone()
2521     }
2522 }
2523 
2524 pub fn menubar_font_parsed() -> (String, f32) {
2525     if let Ok(lock) = MENUBAR_FONT_CACHED.read() {
2526         if let Some(ref val) = *lock {
2527             return val.clone();
2528         }
2529     }
2530     let font_str = menubar_font();
2531     let parsed = parse_font_string(&font_str);
2532     let size = parsed.1.unwrap_or(12.0);
2533     let val = (parsed.0, size);
2534     if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
2535         *lock = Some(val.clone());
2536     }
2537     val
2538 }
2539 
2540 pub fn set_menubar_font(font: &str) {
2541     if let Ok(mut lock) = MENUBAR_FONT.write() {
2542         *lock = font.to_string();
2543     }
2544     if let Ok(mut lock) = MENUBAR_FONT_CACHED.write() {
2545         *lock = None;
2546     }
2547 }
2548 
2549 pub fn statusbar_font() -> String {
2550     use std::sync::Once;
2551     static INIT: Once = Once::new();
2552     INIT.call_once(|| {
2553         let font = read_config_value("statusbar_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2554         if let Ok(mut lock) = STATUSBAR_FONT.write() {
2555             *lock = font;
2556         }
2557     });
2558     let lock = STATUSBAR_FONT.read().unwrap();
2559     if lock.is_empty() {
2560         "Berkeley Mono".to_string()
2561     } else {
2562         lock.clone()
2563     }
2564 }
2565 
2566 pub fn statusbar_font_parsed() -> (String, f32) {
2567     if let Ok(lock) = STATUSBAR_FONT_CACHED.read() {
2568         if let Some(ref val) = *lock {
2569             return val.clone();
2570         }
2571     }
2572     let font_str = statusbar_font();
2573     let parsed = parse_font_string(&font_str);
2574     let size = parsed.1.unwrap_or(12.0);
2575     let val = (parsed.0, size);
2576     if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
2577         *lock = Some(val.clone());
2578     }
2579     val
2580 }
2581 
2582 pub fn set_statusbar_font(font: &str) {
2583     if let Ok(mut lock) = STATUSBAR_FONT.write() {
2584         *lock = font.to_string();
2585     }
2586     if let Ok(mut lock) = STATUSBAR_FONT_CACHED.write() {
2587         *lock = None;
2588     }
2589 }
2590 
2591 
2592 
2593 pub fn font_selector_font() -> String {
2594     use std::sync::Once;
2595     static INIT: Once = Once::new();
2596     INIT.call_once(|| {
2597         let font = read_config_value("font_selector_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2598         if let Ok(mut lock) = FONT_SELECTOR_FONT.write() {
2599             *lock = font;
2600         }
2601     });
2602     let lock = FONT_SELECTOR_FONT.read().unwrap();
2603     if lock.is_empty() {
2604         "Berkeley Mono".to_string()
2605     } else {
2606         lock.clone()
2607     }
2608 }
2609 
2610 pub fn font_selector_font_parsed() -> (String, f32) {
2611     if let Ok(lock) = FONT_SELECTOR_FONT_CACHED.read() {
2612         if let Some(ref val) = *lock {
2613             return val.clone();
2614         }
2615     }
2616     let font_str = font_selector_font();
2617     let parsed = parse_font_string(&font_str);
2618     let size = parsed.1.unwrap_or(12.0);
2619     let val = (parsed.0, size);
2620     if let Ok(mut lock) = FONT_SELECTOR_FONT_CACHED.write() {
2621         *lock = Some(val.clone());
2622     }
2623     val
2624 }
2625 
2626 pub fn set_font_selector_font(font: &str) {
2627     if let Ok(mut lock) = FONT_SELECTOR_FONT.write() {
2628         *lock = font.to_string();
2629     }
2630     if let Ok(mut lock) = FONT_SELECTOR_FONT_CACHED.write() {
2631         *lock = None;
2632     }
2633 }
2634 
2635 // Button Strip Font
2636 pub fn button_strip_font() -> String {
2637     use std::sync::Once;
2638     static INIT: Once = Once::new();
2639     INIT.call_once(|| {
2640         let font = read_config_value("button_strip_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2641         if let Ok(mut lock) = BUTTON_STRIP_FONT.write() {
2642             *lock = font;
2643         }
2644     });
2645     let lock = BUTTON_STRIP_FONT.read().unwrap();
2646     if lock.is_empty() {
2647         "Berkeley Mono".to_string()
2648     } else {
2649         lock.clone()
2650     }
2651 }
2652 
2653 pub fn button_strip_font_parsed() -> (String, f32) {
2654     if let Ok(lock) = BUTTON_STRIP_FONT_CACHED.read() {
2655         if let Some(ref val) = *lock {
2656             return val.clone();
2657         }
2658     }
2659     let font_str = button_strip_font();
2660     let parsed = parse_font_string(&font_str);
2661     let size = parsed.1.unwrap_or(12.0);
2662     let val = (parsed.0, size);
2663     if let Ok(mut lock) = BUTTON_STRIP_FONT_CACHED.write() {
2664         *lock = Some(val.clone());
2665     }
2666     val
2667 }
2668 
2669 pub fn set_button_strip_font(font: &str) {
2670     if let Ok(mut lock) = BUTTON_STRIP_FONT.write() {
2671         *lock = font.to_string();
2672     }
2673     if let Ok(mut lock) = BUTTON_STRIP_FONT_CACHED.write() {
2674         *lock = None;
2675     }
2676 }
2677 
2678 
2679 
2680 // Control Label Font
2681 pub fn control_label_font() -> String {
2682     #[cfg(test)]
2683     if let Some(v) = test_style::CONTROL_LABEL_FONT.with(|c| c.borrow().clone()) {
2684         return v;
2685     }
2686     use std::sync::Once;
2687     static INIT: Once = Once::new();
2688     INIT.call_once(|| {
2689         let font = read_config_value("control_label_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2690         if let Ok(mut lock) = CONTROL_LABEL_FONT.write() {
2691             *lock = font;
2692         }
2693     });
2694     let lock = CONTROL_LABEL_FONT.read().unwrap();
2695     if lock.is_empty() {
2696         "Berkeley Mono".to_string()
2697     } else {
2698         lock.clone()
2699     }
2700 }
2701 
2702 pub fn control_label_font_parsed() -> (String, f32) {
2703     // The parse cache is process-wide, so under test it would hand back one
2704     // thread's pinned font to every other. Parsing is cheap; skip it there.
2705     #[cfg(not(test))]
2706     if let Ok(lock) = CONTROL_LABEL_FONT_CACHED.read() {
2707         if let Some(ref val) = *lock {
2708             return val.clone();
2709         }
2710     }
2711     let font_str = control_label_font();
2712     let parsed = parse_font_string(&font_str);
2713     let size = parsed.1.unwrap_or(12.0);
2714     let val = (parsed.0, size);
2715     #[cfg(not(test))]
2716     if let Ok(mut lock) = CONTROL_LABEL_FONT_CACHED.write() {
2717         *lock = Some(val.clone());
2718     }
2719     val
2720 }
2721 
2722 pub fn set_control_label_font(font: &str) {
2723     #[cfg(test)]
2724     test_style::CONTROL_LABEL_FONT.with(|c| *c.borrow_mut() = Some(font.to_string()));
2725     #[cfg(not(test))]
2726     if let Ok(mut lock) = CONTROL_LABEL_FONT.write() {
2727         *lock = font.to_string();
2728     }
2729     #[cfg(not(test))]
2730     if let Ok(mut lock) = CONTROL_LABEL_FONT_CACHED.write() {
2731         *lock = None;
2732     }
2733 }
2734 
2735 // Control Label Font Detached
2736 pub fn control_label_font_detached() -> String {
2737     #[cfg(test)]
2738     if let Some(v) = test_style::CONTROL_LABEL_FONT_DETACHED.with(|c| c.borrow().clone()) {
2739         return v;
2740     }
2741     use std::sync::Once;
2742     static INIT: Once = Once::new();
2743     INIT.call_once(|| {
2744         let font = read_config_value("control_label_font_detached").unwrap_or_else(|| "Berkeley Mono".to_string());
2745         if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED.write() {
2746             *lock = font;
2747         }
2748     });
2749     let lock = CONTROL_LABEL_FONT_DETACHED.read().unwrap();
2750     if lock.is_empty() {
2751         "Berkeley Mono".to_string()
2752     } else {
2753         lock.clone()
2754     }
2755 }
2756 
2757 pub fn control_label_font_detached_parsed() -> (String, f32) {
2758     // The parse cache is process-wide, so under test it would hand back one
2759     // thread's pinned font to every other. Parsing is cheap; skip it there.
2760     #[cfg(not(test))]
2761     if let Ok(lock) = CONTROL_LABEL_FONT_DETACHED_CACHED.read() {
2762         if let Some(ref val) = *lock {
2763             return val.clone();
2764         }
2765     }
2766     let font_str = control_label_font_detached();
2767     let parsed = parse_font_string(&font_str);
2768     let size = parsed.1.unwrap_or(12.0);
2769     let val = (parsed.0, size);
2770     #[cfg(not(test))]
2771     if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED_CACHED.write() {
2772         *lock = Some(val.clone());
2773     }
2774     val
2775 }
2776 
2777 pub fn set_control_label_font_detached(font: &str) {
2778     #[cfg(test)]
2779     test_style::CONTROL_LABEL_FONT_DETACHED.with(|c| *c.borrow_mut() = Some(font.to_string()));
2780     #[cfg(not(test))]
2781     if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED.write() {
2782         *lock = font.to_string();
2783     }
2784     #[cfg(not(test))]
2785     if let Ok(mut lock) = CONTROL_LABEL_FONT_DETACHED_CACHED.write() {
2786         *lock = None;
2787     }
2788 }
2789 
2790 
2791 
2792 // List Font
2793 pub fn list_font() -> String {
2794     use std::sync::Once;
2795     static INIT: Once = Once::new();
2796     INIT.call_once(|| {
2797         let font = read_config_value("list_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2798         if let Ok(mut lock) = LIST_FONT.write() {
2799             *lock = font;
2800         }
2801     });
2802     let lock = LIST_FONT.read().unwrap();
2803     if lock.is_empty() {
2804         "Berkeley Mono".to_string()
2805     } else {
2806         lock.clone()
2807     }
2808 }
2809 
2810 pub fn list_font_parsed() -> (String, f32) {
2811     if let Ok(lock) = LIST_FONT_CACHED.read() {
2812         if let Some(ref val) = *lock {
2813             return val.clone();
2814         }
2815     }
2816     let font_str = list_font();
2817     let parsed = parse_font_string(&font_str);
2818     let size = parsed.1.unwrap_or(12.0);
2819     let val = (parsed.0, size);
2820     if let Ok(mut lock) = LIST_FONT_CACHED.write() {
2821         *lock = Some(val.clone());
2822     }
2823     val
2824 }
2825 
2826 pub fn set_list_font(font: &str) {
2827     if let Ok(mut lock) = LIST_FONT.write() {
2828         *lock = font.to_string();
2829     }
2830     if let Ok(mut lock) = LIST_FONT_CACHED.write() {
2831         *lock = None;
2832     }
2833 }
2834 
2835 // Tree Font
2836 pub fn tree_font() -> String {
2837     use std::sync::Once;
2838     static INIT: Once = Once::new();
2839     INIT.call_once(|| {
2840         let font = read_config_value("tree_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2841         if let Ok(mut lock) = TREE_FONT.write() {
2842             *lock = font;
2843         }
2844     });
2845     let lock = TREE_FONT.read().unwrap();
2846     if lock.is_empty() {
2847         "Berkeley Mono".to_string()
2848     } else {
2849         lock.clone()
2850     }
2851 }
2852 
2853 pub fn tree_font_parsed() -> (String, f32) {
2854     if let Ok(lock) = TREE_FONT_CACHED.read() {
2855         if let Some(ref val) = *lock {
2856             return val.clone();
2857         }
2858     }
2859     let font_str = tree_font();
2860     let parsed = parse_font_string(&font_str);
2861     let size = parsed.1.unwrap_or(12.0);
2862     let val = (parsed.0, size);
2863     if let Ok(mut lock) = TREE_FONT_CACHED.write() {
2864         *lock = Some(val.clone());
2865     }
2866     val
2867 }
2868 
2869 pub fn set_tree_font(font: &str) {
2870     if let Ok(mut lock) = TREE_FONT.write() {
2871         *lock = font.to_string();
2872     }
2873     if let Ok(mut lock) = TREE_FONT_CACHED.write() {
2874         *lock = None;
2875     }
2876 }
2877 
2878 // Graph Font
2879 pub fn graph_font() -> String {
2880     use std::sync::Once;
2881     static INIT: Once = Once::new();
2882     INIT.call_once(|| {
2883         let font = read_config_value("graph_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2884         if let Ok(mut lock) = GRAPH_FONT.write() {
2885             *lock = font;
2886         }
2887     });
2888     let lock = GRAPH_FONT.read().unwrap();
2889     if lock.is_empty() {
2890         "Berkeley Mono".to_string()
2891     } else {
2892         lock.clone()
2893     }
2894 }
2895 
2896 pub fn graph_font_parsed() -> (String, f32) {
2897     if let Ok(lock) = GRAPH_FONT_CACHED.read() {
2898         if let Some(ref val) = *lock {
2899             return val.clone();
2900         }
2901     }
2902     let font_str = graph_font();
2903     let parsed = parse_font_string(&font_str);
2904     let size = parsed.1.unwrap_or(12.0);
2905     let val = (parsed.0, size);
2906     if let Ok(mut lock) = GRAPH_FONT_CACHED.write() {
2907         *lock = Some(val.clone());
2908     }
2909     val
2910 }
2911 
2912 pub fn set_graph_font(font: &str) {
2913     if let Ok(mut lock) = GRAPH_FONT.write() {
2914         *lock = font.to_string();
2915     }
2916     if let Ok(mut lock) = GRAPH_FONT_CACHED.write() {
2917         *lock = None;
2918     }
2919 }
2920 
2921 // Graph Node Font
2922 pub fn graph_node_font() -> String {
2923     use std::sync::Once;
2924     static INIT: Once = Once::new();
2925     INIT.call_once(|| {
2926         let font = read_config_value("graph_node_font").unwrap_or_else(|| "Berkeley Mono".to_string());
2927         if let Ok(mut lock) = GRAPH_NODE_FONT.write() {
2928             *lock = font;
2929         }
2930     });
2931     let lock = GRAPH_NODE_FONT.read().unwrap();
2932     if lock.is_empty() {
2933         "Berkeley Mono".to_string()
2934     } else {
2935         lock.clone()
2936     }
2937 }
2938 
2939 pub fn graph_node_font_parsed() -> (String, f32) {
2940     if let Ok(lock) = GRAPH_NODE_FONT_CACHED.read() {
2941         if let Some(ref val) = *lock {
2942             return val.clone();
2943         }
2944     }
2945     let font_str = graph_node_font();
2946     let parsed = parse_font_string(&font_str);
2947     let size = parsed.1.unwrap_or(12.0);
2948     let val = (parsed.0, size);
2949     if let Ok(mut lock) = GRAPH_NODE_FONT_CACHED.write() {
2950         *lock = Some(val.clone());
2951     }
2952     val
2953 }
2954 
2955 pub fn set_graph_node_font(font: &str) {
2956     if let Ok(mut lock) = GRAPH_NODE_FONT.write() {
2957         *lock = font.to_string();
2958     }
2959     if let Ok(mut lock) = GRAPH_NODE_FONT_CACHED.write() {
2960         *lock = None;
2961     }
2962 }
2963 
2964 // List Justification
2965 pub fn list_justification() -> u8 {
2966     use std::sync::Once;
2967     static INIT: Once = Once::new();
2968     INIT.call_once(|| {
2969         if let Some(content) = read_config() {
2970             for line in content.lines() {
2971                 let trimmed = line.trim();
2972                 if let Some(rest) = trimmed.strip_prefix("list_justification") {
2973                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
2974                     let val_str = rest.trim_end_matches('"').trim();
2975                     if let Ok(val) = val_str.parse::<u8>() {
2976                         if let Ok(mut lock) = LIST_JUSTIFICATION.write() {
2977                             *lock = val;
2978                         }
2979                     }
2980                 }
2981             }
2982         }
2983     });
2984     *LIST_JUSTIFICATION.read().unwrap()
2985 }
2986 
2987 pub fn set_list_justification(just: u8) {
2988     if let Ok(mut lock) = LIST_JUSTIFICATION.write() {
2989         *lock = just;
2990     }
2991 }
2992 
2993 
2994 
2995 
2996 pub fn section_label_font() -> String {
2997     use std::sync::Once;
2998     static INIT: Once = Once::new();
2999     INIT.call_once(|| {
3000         let mut font = "Berkeley Mono".to_string();
3001         if let Some(content) = read_config() {
3002             for line in content.lines() {
3003                 let trimmed = line.trim();
3004                 if let Some(rest) = trimmed.strip_prefix("section_label_font") {
3005                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
3006                     let rest = rest.trim();
3007                     let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
3008                         &rest[1..rest.len() - 1]
3009                     } else {
3010                         rest
3011                     };
3012                     font = val_str.trim().to_string();
3013                 }
3014             }
3015         }
3016         if let Ok(mut lock) = SECTION_LABEL_FONT.write() {
3017             *lock = font;
3018         }
3019     });
3020     let lock = SECTION_LABEL_FONT.read().unwrap();
3021     if lock.is_empty() {
3022         "Berkeley Mono".to_string()
3023     } else {
3024         lock.clone()
3025     }
3026 }
3027 
3028 pub fn set_section_label_font(font: &str) {
3029     if let Ok(mut lock) = SECTION_LABEL_FONT.write() {
3030         *lock = font.to_string();
3031     }
3032 }
3033 
3034 pub fn nested_section_label_font() -> String {
3035     use std::sync::Once;
3036     static INIT: Once = Once::new();
3037     INIT.call_once(|| {
3038         let mut font = "Berkeley Mono".to_string();
3039         if let Some(content) = read_config() {
3040             for line in content.lines() {
3041                 let trimmed = line.trim();
3042                 if let Some(rest) = trimmed.strip_prefix("nested_section_label_font") {
3043                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
3044                     let rest = rest.trim();
3045                     let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
3046                         &rest[1..rest.len() - 1]
3047                     } else {
3048                         rest
3049                     };
3050                     font = val_str.trim().to_string();
3051                 }
3052             }
3053         }
3054         if let Ok(mut lock) = NESTED_SECTION_LABEL_FONT.write() {
3055             *lock = font;
3056         }
3057     });
3058     let lock = NESTED_SECTION_LABEL_FONT.read().unwrap();
3059     if lock.is_empty() {
3060         "Berkeley Mono".to_string()
3061     } else {
3062         lock.clone()
3063     }
3064 }
3065 
3066 pub fn set_nested_section_label_font(font: &str) {
3067     if let Ok(mut lock) = NESTED_SECTION_LABEL_FONT.write() {
3068         *lock = font.to_string();
3069     }
3070 }
3071 
3072 pub fn breadcrumb_font() -> String {
3073     use std::sync::Once;
3074     static INIT: Once = Once::new();
3075     INIT.call_once(|| {
3076         let mut font = "Berkeley Mono".to_string();
3077         if let Some(content) = read_config() {
3078             for line in content.lines() {
3079                 let trimmed = line.trim();
3080                 if let Some(rest) = trimmed.strip_prefix("breadcrumb_font") {
3081                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
3082                     let rest = rest.trim();
3083                     let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
3084                         &rest[1..rest.len() - 1]
3085                     } else {
3086                         rest
3087                     };
3088                     font = val_str.trim().to_string();
3089                 }
3090             }
3091         }
3092         if let Ok(mut lock) = BREADCRUMB_FONT.write() {
3093             *lock = font;
3094         }
3095     });
3096     let lock = BREADCRUMB_FONT.read().unwrap();
3097     if lock.is_empty() {
3098         "Berkeley Mono".to_string()
3099     } else {
3100         lock.clone()
3101     }
3102 }
3103 
3104 pub fn set_breadcrumb_font(font: &str) {
3105     if let Ok(mut lock) = BREADCRUMB_FONT.write() {
3106         *lock = font.to_string();
3107     }
3108 }
3109 
3110 pub fn button_font() -> String {
3111     use std::sync::Once;
3112     static INIT: Once = Once::new();
3113     INIT.call_once(|| {
3114         let mut font = "Berkeley Mono".to_string();
3115         if let Some(content) = read_config() {
3116             for line in content.lines() {
3117                 let trimmed = line.trim();
3118                 if let Some(rest) = trimmed.strip_prefix("button_font") {
3119                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
3120                     let rest = rest.trim();
3121                     let val_str = if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
3122                         &rest[1..rest.len() - 1]
3123                     } else {
3124                         rest
3125                     };
3126                     font = val_str.trim().to_string();
3127                 }
3128             }
3129         }
3130         if let Ok(mut lock) = BUTTON_FONT.write() {
3131             *lock = font;
3132         }
3133     });
3134     let lock = BUTTON_FONT.read().unwrap();
3135     if lock.is_empty() {
3136         "Berkeley Mono".to_string()
3137     } else {
3138         lock.clone()
3139     }
3140 }
3141 
3142 pub fn set_button_font(font: &str) {
3143     if let Ok(mut lock) = BUTTON_FONT.write() {
3144         *lock = font.to_string();
3145     }
3146 }
3147 
3148 pub fn color_selector_preview_corner_radius() -> f32 {
3149     lazy_init_style_registry();
3150     // Unset: the swatch rounds like the text field beside it (the TextBox radius).
3151     get_style_registry().read().unwrap().get_float("color_selector_preview_corner_radius").unwrap_or_else(textbox_corner_radius)
3152 }
3153 
3154 pub fn set_color_selector_preview_corner_radius(radius: f32) {
3155     lazy_init_style_registry();
3156     if let Ok(mut registry) = get_style_registry().write() {
3157         registry.set_float("color_selector_preview_corner_radius", radius);
3158     }
3159 }
3160 
3161 pub fn color_selector_corner_radius() -> f32 {
3162     lazy_init_style_registry();
3163     // Unset: the selector's frame rounds like the text field it stands in for.
3164     get_style_registry().read().unwrap().get_float("color_selector_corner_radius").unwrap_or_else(textbox_corner_radius)
3165 }
3166 
3167 pub fn set_color_selector_corner_radius(radius: f32) {
3168     lazy_init_style_registry();
3169     if let Ok(mut registry) = get_style_registry().write() {
3170         registry.set_float("color_selector_corner_radius", radius);
3171     }
3172 }
3173 
3174 /// The control rung's corner radius (`style.control.corner_radius`): the
3175 /// default every control-scale radius getter falls back to when the widget's
3176 /// own `corner_radius` key is unset — buttons, dropdowns, font selectors,
3177 /// sliders, spinboxes, text boxes, toggles, and the list and tree wells. The
3178 /// per-widget keys are overrides on top of it. See "Plates, wells and seams"
3179 /// in `CLAUDE.md`; the pane and root rungs are `plate_corner_radius` and
3180 /// `crate::color::root_plate_corner_radius`.
3181 pub fn control_corner_radius() -> f32 {
3182     lazy_init_style_registry();
3183     get_style_registry().read().unwrap().get_float("control_corner_radius").unwrap_or(8.0)
3184 }
3185 
3186 pub fn set_control_corner_radius(radius: f32) {
3187     lazy_init_style_registry();
3188     if let Ok(mut registry) = get_style_registry().write() {
3189         registry.set_float("control_corner_radius", radius);
3190     }
3191 }
3192 
3193 pub fn button_corner_radius() -> f32 {
3194     lazy_init_style_registry();
3195     get_style_registry().read().unwrap().get_float("button_corner_radius").unwrap_or_else(control_corner_radius)
3196 }
3197 
3198 pub fn set_button_corner_radius(radius: f32) {
3199     lazy_init_style_registry();
3200     if let Ok(mut registry) = get_style_registry().write() {
3201         registry.set_float("button_corner_radius", radius);
3202     }
3203 }
3204 
3205 pub fn spinbox_corner_radius() -> f32 {
3206     lazy_init_style_registry();
3207     get_style_registry().read().unwrap().get_float("spinbox_corner_radius").unwrap_or_else(control_corner_radius)
3208 }
3209 
3210 pub fn set_spinbox_corner_radius(radius: f32) {
3211     lazy_init_style_registry();
3212     if let Ok(mut registry) = get_style_registry().write() {
3213         registry.set_float("spinbox_corner_radius", radius);
3214     }
3215 }
3216 
3217 pub fn spinbox_button_padding() -> f32 {
3218     use std::sync::Once;
3219     static INIT: Once = Once::new();
3220     INIT.call_once(|| {
3221         if let Some(content) = read_config() {
3222             for line in content.lines() {
3223                 let trimmed = line.trim();
3224                 if let Some(rest) = trimmed.strip_prefix("spinbox_button_padding") {
3225                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3226                     let val_str = rest.trim_end_matches('"').trim();
3227                     if let Ok(val) = val_str.parse::<f32>() {
3228                         if let Ok(mut lock) = SPINBOX_BUTTON_PADDING.write() {
3229                             *lock = val;
3230                         }
3231                     }
3232                 }
3233             }
3234         }
3235     });
3236     *SPINBOX_BUTTON_PADDING.read().unwrap()
3237 }
3238 
3239 pub fn scrollbar_width() -> f32 {
3240     use std::sync::Once;
3241     static INIT: Once = Once::new();
3242     INIT.call_once(|| {
3243         if let Some(content) = read_config() {
3244             for line in content.lines() {
3245                 let trimmed = line.trim();
3246                 if let Some(rest) = trimmed.strip_prefix("scrollbar_width") {
3247                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3248                     let val_str = rest.trim_end_matches('"').trim();
3249                     if let Ok(val) = val_str.parse::<f32>() {
3250                         if let Ok(mut lock) = SCROLLBAR_WIDTH.write() {
3251                             *lock = val;
3252                         }
3253                     }
3254                 }
3255             }
3256         }
3257     });
3258     *SCROLLBAR_WIDTH.read().unwrap()
3259 }
3260 
3261 pub fn set_scrollbar_width(width: f32) {
3262     if let Ok(mut lock) = SCROLLBAR_WIDTH.write() {
3263         *lock = width;
3264     }
3265 }
3266 
3267 /// How far a page-level scrollbar stands off its window/plate right edge — the
3268 /// designer parameter-pane look (config `style.control.scrollbar.inset`).
3269 /// Framed inner lists keep their own tight 4px hug; this is for bars floating
3270 /// over a plate.
3271 pub fn scrollbar_inset() -> f32 {
3272     use std::sync::Once;
3273     static INIT: Once = Once::new();
3274     INIT.call_once(|| {
3275         if let Some(content) = read_config() {
3276             for line in content.lines() {
3277                 let trimmed = line.trim();
3278                 if let Some(rest) = trimmed.strip_prefix("scrollbar_inset") {
3279                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3280                     let val_str = rest.trim_end_matches('"').trim();
3281                     if let Ok(val) = val_str.parse::<f32>() {
3282                         if let Ok(mut lock) = SCROLLBAR_INSET.write() {
3283                             *lock = val;
3284                         }
3285                     }
3286                 }
3287             }
3288         }
3289     });
3290     *SCROLLBAR_INSET.read().unwrap()
3291 }
3292 
3293 pub fn tree_opacity() -> f32 {
3294     use std::sync::Once;
3295     static INIT: Once = Once::new();
3296     INIT.call_once(|| {
3297         if let Some(content) = read_config() {
3298             for line in content.lines() {
3299                 let trimmed = line.trim();
3300                 if let Some(rest) = trimmed.strip_prefix("tree_opacity") {
3301                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3302                     let val_str = rest.trim_end_matches('"').trim();
3303                     if let Ok(val) = val_str.parse::<f32>() {
3304                         if let Ok(mut lock) = TREE_OPACITY.write() {
3305                             *lock = val;
3306                         }
3307                     }
3308                 }
3309             }
3310         }
3311     });
3312     *TREE_OPACITY.read().unwrap()
3313 }
3314 
3315 pub fn set_tree_opacity(opacity: f32) {
3316     if let Ok(mut lock) = TREE_OPACITY.write() {
3317         *lock = opacity;
3318     }
3319 }
3320 
3321 pub fn tree_blur() -> f32 {
3322     use std::sync::Once;
3323     static INIT: Once = Once::new();
3324     INIT.call_once(|| {
3325         if let Some(content) = read_config() {
3326             for line in content.lines() {
3327                 let trimmed = line.trim();
3328                 if let Some(rest) = trimmed.strip_prefix("tree_blur") {
3329                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3330                     let val_str = rest.trim_end_matches('"').trim();
3331                     if let Ok(val) = val_str.parse::<f32>() {
3332                         if let Ok(mut lock) = TREE_BLUR.write() {
3333                             *lock = val;
3334                         }
3335                     }
3336                 }
3337             }
3338         }
3339     });
3340     *TREE_BLUR.read().unwrap()
3341 }
3342 
3343 pub fn set_tree_blur(blur: f32) {
3344     if let Ok(mut lock) = TREE_BLUR.write() {
3345         *lock = blur;
3346     }
3347 }
3348 
3349 pub fn set_spinbox_button_padding(padding: f32) {
3350     if let Ok(mut lock) = SPINBOX_BUTTON_PADDING.write() {
3351         *lock = padding;
3352     }
3353 }
3354 
3355 pub fn textbox_corner_radius() -> f32 {
3356     lazy_init_style_registry();
3357     get_style_registry().read().unwrap().get_float("textbox_corner_radius").unwrap_or_else(control_corner_radius)
3358 }
3359 
3360 pub fn set_textbox_corner_radius(radius: f32) {
3361     lazy_init_style_registry();
3362     if let Ok(mut registry) = get_style_registry().write() {
3363         registry.set_float("textbox_corner_radius", radius);
3364     }
3365 }
3366 
3367 pub fn textbox_line_wrap() -> bool {
3368     lazy_init_style_registry();
3369     *TEXTBOX_LINE_WRAP.read().unwrap()
3370 }
3371 
3372 pub fn set_textbox_line_wrap(wrap: bool) {
3373     lazy_init_style_registry();
3374     if let Ok(mut lock) = TEXTBOX_LINE_WRAP.write() {
3375         *lock = wrap;
3376     }
3377 }
3378 
3379 pub fn touchpad_natural_scroll() -> bool {
3380     lazy_init_style_registry();
3381     *TOUCHPAD_NATURAL_SCROLL.read().unwrap()
3382 }
3383 
3384 pub fn set_touchpad_natural_scroll(enabled: bool) {
3385     lazy_init_style_registry();
3386     if let Ok(mut lock) = TOUCHPAD_NATURAL_SCROLL.write() {
3387         *lock = enabled;
3388     }
3389 }
3390 
3391 pub fn textbox_multiline_border_width() -> f32 {
3392     lazy_init_style_registry();
3393     *TEXTBOX_MULTILINE_BORDER_WIDTH.read().unwrap()
3394 }
3395 
3396 pub fn set_textbox_multiline_border_width(width: f32) {
3397     lazy_init_style_registry();
3398     if let Ok(mut lock) = TEXTBOX_MULTILINE_BORDER_WIDTH.write() {
3399         *lock = width;
3400     }
3401 }
3402 
3403 
3404 pub fn list_corner_radius() -> f32 {
3405     lazy_init_style_registry();
3406     get_style_registry().read().unwrap().get_float("list_corner_radius").unwrap_or_else(control_corner_radius)
3407 }
3408 
3409 pub fn set_list_corner_radius(radius: f32) {
3410     lazy_init_style_registry();
3411     if let Ok(mut registry) = get_style_registry().write() {
3412         registry.set_float("list_corner_radius", radius);
3413     }
3414 }
3415 
3416 pub fn tree_corner_radius() -> f32 {
3417     lazy_init_style_registry();
3418     get_style_registry().read().unwrap().get_float("tree_corner_radius").unwrap_or_else(control_corner_radius)
3419 }
3420 
3421 pub fn set_tree_corner_radius(radius: f32) {
3422     lazy_init_style_registry();
3423     if let Ok(mut registry) = get_style_registry().write() {
3424         registry.set_float("tree_corner_radius", radius);
3425     }
3426 }
3427 
3428 /// The graph grid's pitch along x: the distance from the centre of one
3429 /// vertical grid line to the centre of the next. It is the grid's ONE size
3430 /// per axis — nodes are centred on the lattice intersections. The node body
3431 /// has a size of its own (`graph_node_width` / `graph_node_height`), so a
3432 /// denser grid does not shrink the nodes. The pitch replaced a cell size
3433 /// plus a gap (`spacing_*` was the cell, `gap_col_w` / `gap_row_h` the gap,
3434 /// and a step was the two added up); the defaults are what those two used
3435 /// to add up to, so a config that set neither draws the same lattice it did.
3436 pub fn graph_spacing_x() -> f32 {
3437     lazy_init_style_registry();
3438     get_style_registry().read().unwrap().get_float("graph_spacing_x").unwrap_or(187.5)
3439 }
3440 
3441 pub fn set_graph_spacing_x(spacing: f32) {
3442     lazy_init_style_registry();
3443     if let Ok(mut registry) = get_style_registry().write() {
3444         registry.set_float("graph_spacing_x", spacing);
3445     }
3446 }
3447 
3448 /// The graph grid's pitch along y — see [`graph_spacing_x`].
3449 pub fn graph_spacing_y() -> f32 {
3450     lazy_init_style_registry();
3451     get_style_registry().read().unwrap().get_float("graph_spacing_y").unwrap_or(112.5)
3452 }
3453 
3454 pub fn set_graph_spacing_y(spacing: f32) {
3455     lazy_init_style_registry();
3456     if let Ok(mut registry) = get_style_registry().write() {
3457         registry.set_float("graph_spacing_y", spacing);
3458     }
3459 }
3460 
3461 /// The drawn width of a graph grid line, in logical px. The pitch is
3462 /// measured centre to centre, so this changes how heavy the lattice looks
3463 /// and nothing about where anything sits. Not scaled by zoom — a lattice is
3464 /// a reference, not a thing in the scene.
3465 pub fn graph_line_width() -> f32 {
3466     lazy_init_style_registry();
3467     get_style_registry().read().unwrap().get_float("graph_line_width").unwrap_or(1.0)
3468 }
3469 
3470 pub fn set_graph_line_width(width: f32) {
3471     lazy_init_style_registry();
3472     if let Ok(mut registry) = get_style_registry().write() {
3473         registry.set_float("graph_line_width", width);
3474     }
3475 }
3476 
3477 /// The node body's width at 100% zoom (`style.surface.graph.node.width`),
3478 /// independent of the grid pitch: a node is a thing of its own size sitting
3479 /// on a crossing, and the grid is a reference under it. The default is the
3480 /// cell the old grid gave a node.
3481 pub fn graph_node_width() -> f32 {
3482     lazy_init_style_registry();
3483     get_style_registry().read().unwrap().get_float("graph_node_width").unwrap_or(150.0)
3484 }
3485 
3486 pub fn set_graph_node_width(width: f32) {
3487     lazy_init_style_registry();
3488     if let Ok(mut registry) = get_style_registry().write() {
3489         registry.set_float("graph_node_width", width);
3490     }
3491 }
3492 
3493 /// The node body's height at 100% zoom — see [`graph_node_width`].
3494 pub fn graph_node_height() -> f32 {
3495     lazy_init_style_registry();
3496     get_style_registry().read().unwrap().get_float("graph_node_height").unwrap_or(75.0)
3497 }
3498 
3499 pub fn set_graph_node_height(height: f32) {
3500     lazy_init_style_registry();
3501     if let Ok(mut registry) = get_style_registry().write() {
3502         registry.set_float("graph_node_height", height);
3503     }
3504 }
3505 
3506 pub fn graph_grid_snap() -> bool {
3507     lazy_init_style_registry();
3508     get_style_registry().read().unwrap().get_float("graph_grid_snap").unwrap_or(0.0) != 0.0
3509 }
3510 
3511 pub fn set_graph_grid_snap(snap: bool) {
3512     lazy_init_style_registry();
3513     if let Ok(mut registry) = get_style_registry().write() {
3514         registry.set_float("graph_grid_snap", if snap { 1.0 } else { 0.0 });
3515     }
3516 }
3517 
3518 pub fn graph_blur() -> f32 {
3519     lazy_init_style_registry();
3520     get_style_registry().read().unwrap().get_float("graph_blur").unwrap_or(0.0)
3521 }
3522 
3523 pub fn set_graph_blur(blur: f32) {
3524     lazy_init_style_registry();
3525     if let Ok(mut registry) = get_style_registry().write() {
3526         registry.set_float("graph_blur", blur);
3527     }
3528 }
3529 
3530 pub fn graph_node_corner_radius() -> f32 {
3531     lazy_init_style_registry();
3532     get_style_registry().read().unwrap().get_float("graph_node_corner_radius").unwrap_or(4.0)
3533 }
3534 
3535 pub fn set_graph_node_corner_radius(radius: f32) {
3536     lazy_init_style_registry();
3537     if let Ok(mut registry) = get_style_registry().write() {
3538         registry.set_float("graph_node_corner_radius", radius);
3539     }
3540 }
3541 
3542 pub fn graph_node_delete() -> String {
3543     lazy_init_style_registry();
3544     get_style_registry().read().unwrap().get_string("graph_node_delete").unwrap_or_else(|| "delete".to_string())
3545 }
3546 
3547 pub fn set_graph_node_delete(key: String) {
3548     lazy_init_style_registry();
3549     if let Ok(mut registry) = get_style_registry().write() {
3550         registry.set_string("graph_node_delete", key);
3551     }
3552 }
3553 
3554 pub fn graph_wire_size() -> f32 {
3555     lazy_init_style_registry();
3556     get_style_registry().read().unwrap().get_float("graph_wire_size").unwrap_or(6.0)
3557 }
3558 
3559 pub fn set_graph_wire_size(size: f32) {
3560     lazy_init_style_registry();
3561     if let Ok(mut registry) = get_style_registry().write() {
3562         registry.set_float("graph_wire_size", size);
3563     }
3564 }
3565 
3566 pub fn graph_wire_activation_radius() -> f32 {
3567     lazy_init_style_registry();
3568     get_style_registry().read().unwrap().get_float("graph_wire_activation_radius").unwrap_or(9.0)
3569 }
3570 
3571 pub fn set_graph_wire_activation_radius(radius: f32) {
3572     lazy_init_style_registry();
3573     if let Ok(mut registry) = get_style_registry().write() {
3574         registry.set_float("graph_wire_activation_radius", radius);
3575     }
3576 }
3577 
3578 pub fn graph_connector_size() -> f32 {
3579     lazy_init_style_registry();
3580     get_style_registry().read().unwrap().get_float("graph_connector_size").unwrap_or(8.0)
3581 }
3582 
3583 pub fn set_graph_connector_size(size: f32) {
3584     lazy_init_style_registry();
3585     if let Ok(mut registry) = get_style_registry().write() {
3586         registry.set_float("graph_connector_size", size);
3587     }
3588 }
3589 
3590 pub fn graph_connector_activation_radius() -> f32 {
3591     lazy_init_style_registry();
3592     get_style_registry().read().unwrap().get_float("graph_connector_activation_radius").unwrap_or(12.0)
3593 }
3594 
3595 pub fn set_graph_connector_activation_radius(radius: f32) {
3596     lazy_init_style_registry();
3597     if let Ok(mut registry) = get_style_registry().write() {
3598         registry.set_float("graph_connector_activation_radius", radius);
3599     }
3600 }
3601 
3602 pub fn font_selector_corner_radius() -> f32 {
3603     lazy_init_style_registry();
3604     get_style_registry().read().unwrap().get_float("font_selector_corner_radius").unwrap_or_else(control_corner_radius)
3605 }
3606 
3607 pub fn set_font_selector_corner_radius(radius: f32) {
3608     lazy_init_style_registry();
3609     if let Ok(mut registry) = get_style_registry().write() {
3610         registry.set_float("font_selector_corner_radius", radius);
3611     }
3612 }
3613 
3614 /// The context menu's corner radius (`style.surface.menu.corner_radius`),
3615 /// falling back to the control rung's. It used to take the pane radius
3616 /// (`plate_corner_radius`, 12 in the shipped config), which is a corner
3617 /// too wide for a surface whose labels sit 8px in from its edge: the first
3618 /// row's text ran off the plate through the arc. A menu is a popover, and
3619 /// its corner belongs to the control scale, like the dropdown's.
3620 pub fn menu_corner_radius() -> f32 {
3621     lazy_init_style_registry();
3622     get_style_registry().read().unwrap().get_float("menu_corner_radius").unwrap_or_else(control_corner_radius)
3623 }
3624 
3625 pub fn dropdown_corner_radius() -> f32 {
3626     lazy_init_style_registry();
3627     get_style_registry().read().unwrap().get_float("dropdown_corner_radius").unwrap_or_else(control_corner_radius)
3628 }
3629 
3630 pub fn set_dropdown_corner_radius(radius: f32) {
3631     lazy_init_style_registry();
3632     if let Ok(mut registry) = get_style_registry().write() {
3633         registry.set_float("dropdown_corner_radius", radius);
3634     }
3635 }
3636 
3637 pub fn color_selector_preview_margin() -> f32 {
3638     use std::sync::Once;
3639     static INIT: Once = Once::new();
3640     INIT.call_once(|| {
3641         if let Some(content) = read_config() {
3642             for line in content.lines() {
3643                 let trimmed = line.trim();
3644                 if let Some(rest) = trimmed.strip_prefix("color_selector_preview_margin") {
3645                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3646                     let val_str = rest.trim_end_matches('"').trim();
3647                     if let Ok(val) = val_str.parse::<f32>() {
3648                         if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_MARGIN.write() {
3649                             *lock = val;
3650                         }
3651                     }
3652                 }
3653             }
3654         }
3655     });
3656     *COLOR_SELECTOR_PREVIEW_MARGIN.read().unwrap()
3657 }
3658 
3659 pub fn set_color_selector_preview_margin(margin: f32) {
3660     if let Ok(mut lock) = COLOR_SELECTOR_PREVIEW_MARGIN.write() {
3661         *lock = margin;
3662     }
3663 }
3664 
3665 
3666 
3667 pub fn paginator_tab_padding_x() -> f32 {
3668     use std::sync::Once;
3669     static INIT: Once = Once::new();
3670     INIT.call_once(|| {
3671         if let Some(content) = read_config() {
3672             for line in content.lines() {
3673                 let trimmed = line.trim();
3674                 if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_x") {
3675                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3676                     let val_str = rest.trim_end_matches('"').trim();
3677                     if let Ok(val) = val_str.parse::<f32>() {
3678                         if let Ok(mut lock) = PAGINATOR_TAB_PADDING_X.write() {
3679                             *lock = val;
3680                         }
3681                     }
3682                 }
3683             }
3684         }
3685     });
3686     *PAGINATOR_TAB_PADDING_X.read().unwrap()
3687 }
3688 
3689 pub fn set_paginator_tab_padding_x(padding: f32) {
3690     if let Ok(mut lock) = PAGINATOR_TAB_PADDING_X.write() {
3691         *lock = padding;
3692     }
3693 }
3694 
3695 pub fn button_padding() -> f32 {
3696     use std::sync::Once;
3697     static INIT: Once = Once::new();
3698     INIT.call_once(|| {
3699         if let Some(content) = read_config() {
3700             let mut found = false;
3701             for line in content.lines() {
3702                 let trimmed = line.trim();
3703                 if let Some(rest) = trimmed.strip_prefix("button_padding") {
3704                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3705                     let val_str = rest.trim_end_matches('"').trim();
3706                     if let Ok(val) = val_str.parse::<f32>() {
3707                         if let Ok(mut lock) = BUTTON_PADDING.write() {
3708                             *lock = val;
3709                             found = true;
3710                         }
3711                     }
3712                 }
3713             }
3714             if !found {
3715                 for line in content.lines() {
3716                     let trimmed = line.trim();
3717                     if let Some(rest) = trimmed.strip_prefix("paginator_tab_padding_y") {
3718                         let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3719                         let val_str = rest.trim_end_matches('"').trim();
3720                         if let Ok(val) = val_str.parse::<f32>() {
3721                             if let Ok(mut lock) = BUTTON_PADDING.write() {
3722                                 *lock = val;
3723                             }
3724                         }
3725                     }
3726                 }
3727             }
3728         }
3729     });
3730     *BUTTON_PADDING.read().unwrap()
3731 }
3732 
3733 pub fn set_button_padding(padding: f32) {
3734     if let Ok(mut lock) = BUTTON_PADDING.write() {
3735         *lock = padding;
3736     }
3737 }
3738 
3739 pub fn button_height() -> f32 {
3740     use std::sync::Once;
3741     static INIT: Once = Once::new();
3742     INIT.call_once(|| {
3743         if let Some(content) = read_config() {
3744             for line in content.lines() {
3745                 let trimmed = line.trim();
3746                 if let Some(rest) = trimmed.strip_prefix("button_height") {
3747                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3748                     let val_str = rest.trim_end_matches('"').trim();
3749                     if let Ok(val) = val_str.parse::<f32>() {
3750                         if let Ok(mut lock) = BUTTON_HEIGHT.write() {
3751                             *lock = val;
3752                         }
3753                     }
3754                 }
3755             }
3756         }
3757     });
3758     *BUTTON_HEIGHT.read().unwrap()
3759 }
3760 
3761 pub fn ramp_height() -> f32 {
3762     use std::sync::Once;
3763     static INIT: Once = Once::new();
3764     INIT.call_once(|| {
3765         if let Some(content) = read_config() {
3766             for line in content.lines() {
3767                 let trimmed = line.trim();
3768                 if let Some(rest) = trimmed.strip_prefix("ramp_height") {
3769                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3770                     let val_str = rest.trim_end_matches('"').trim();
3771                     if let Ok(val) = val_str.parse::<f32>() {
3772                         if let Ok(mut lock) = RAMP_HEIGHT.write() {
3773                             *lock = val;
3774                         }
3775                     }
3776                 }
3777             }
3778         }
3779     });
3780     *RAMP_HEIGHT.read().unwrap()
3781 }
3782 
3783 pub fn set_ramp_height(height: f32) {
3784     if let Ok(mut lock) = RAMP_HEIGHT.write() {
3785         *lock = height;
3786     }
3787 }
3788 
3789 pub fn set_button_height(height: f32) {
3790     if let Ok(mut lock) = BUTTON_HEIGHT.write() {
3791         *lock = height;
3792     }
3793 }
3794 
3795 pub fn button_strip_spacing() -> f32 {
3796     use std::sync::Once;
3797     static INIT: Once = Once::new();
3798     INIT.call_once(|| {
3799         if let Some(content) = read_config() {
3800             for line in content.lines() {
3801                 let trimmed = line.trim();
3802                 if let Some(rest) = trimmed.strip_prefix("button_strip_spacing") {
3803                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3804                     let val_str = rest.trim_end_matches('"').trim();
3805                     if let Ok(val) = val_str.parse::<f32>() {
3806                         if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
3807                             *lock = val;
3808                         }
3809                     }
3810                 }
3811             }
3812         }
3813     });
3814     *BUTTON_STRIP_SPACING.read().unwrap()
3815 }
3816 
3817 pub fn set_button_strip_spacing(spacing: f32) {
3818     if let Ok(mut lock) = BUTTON_STRIP_SPACING.write() {
3819         *lock = spacing;
3820     }
3821 }
3822 
3823 pub fn paginator_tab_padding_y() -> f32 {
3824     button_padding()
3825 }
3826 
3827 pub fn set_paginator_tab_padding_y(padding: f32) {
3828     set_button_padding(padding);
3829 }
3830 
3831 pub fn textbox_height() -> f32 {
3832     use std::sync::Once;
3833     static INIT: Once = Once::new();
3834     INIT.call_once(|| {
3835         if let Some(content) = read_config() {
3836             for line in content.lines() {
3837                 let trimmed = line.trim();
3838                 if let Some(rest) = trimmed.strip_prefix("textbox_height") {
3839                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3840                     let val_str = rest.trim_end_matches('"').trim();
3841                     if let Ok(val) = val_str.parse::<f32>() {
3842                         if let Ok(mut lock) = TEXTBOX_HEIGHT.write() {
3843                             *lock = val;
3844                         }
3845                     }
3846                 }
3847             }
3848         }
3849     });
3850     *TEXTBOX_HEIGHT.read().unwrap()
3851 }
3852 
3853 pub fn set_textbox_height(height: f32) {
3854     if let Ok(mut lock) = TEXTBOX_HEIGHT.write() {
3855         *lock = height;
3856     }
3857 }
3858 
3859 pub fn dropdown_height() -> f32 {
3860     use std::sync::Once;
3861     static INIT: Once = Once::new();
3862     INIT.call_once(|| {
3863         if let Some(content) = read_config() {
3864             for line in content.lines() {
3865                 let trimmed = line.trim();
3866                 if let Some(rest) = trimmed.strip_prefix("dropdown_height") {
3867                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3868                     let val_str = rest.trim_end_matches('"').trim();
3869                     if let Ok(val) = val_str.parse::<f32>() {
3870                         if let Ok(mut lock) = DROPDOWN_HEIGHT.write() {
3871                             *lock = val;
3872                         }
3873                     }
3874                 }
3875             }
3876         }
3877     });
3878     *DROPDOWN_HEIGHT.read().unwrap()
3879 }
3880 
3881 pub fn set_dropdown_height(height: f32) {
3882     if let Ok(mut lock) = DROPDOWN_HEIGHT.write() {
3883         *lock = height;
3884     }
3885 }
3886 
3887 pub fn slider_height() -> f32 {
3888     use std::sync::Once;
3889     static INIT: Once = Once::new();
3890     INIT.call_once(|| {
3891         if let Some(content) = read_config() {
3892             for line in content.lines() {
3893                 let trimmed = line.trim();
3894                 if let Some(rest) = trimmed.strip_prefix("slider_height") {
3895                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3896                     let val_str = rest.trim_end_matches('"').trim();
3897                     if let Ok(val) = val_str.parse::<f32>() {
3898                         if let Ok(mut lock) = SLIDER_HEIGHT.write() {
3899                             *lock = val;
3900                         }
3901                     }
3902                 }
3903             }
3904         }
3905     });
3906     *SLIDER_HEIGHT.read().unwrap()
3907 }
3908 
3909 pub fn set_slider_height(height: f32) {
3910     if let Ok(mut lock) = SLIDER_HEIGHT.write() {
3911         *lock = height;
3912     }
3913 }
3914 
3915 pub fn progressbar_height() -> f32 {
3916     use std::sync::Once;
3917     static INIT: Once = Once::new();
3918     INIT.call_once(|| {
3919         if let Some(content) = read_config() {
3920             for line in content.lines() {
3921                 let trimmed = line.trim();
3922                 if let Some(rest) = trimmed.strip_prefix("progressbar_height") {
3923                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3924                     let val_str = rest.trim_end_matches('"').trim();
3925                     if let Ok(val) = val_str.parse::<f32>() {
3926                         if let Ok(mut lock) = PROGRESSBAR_HEIGHT.write() {
3927                             *lock = val;
3928                         }
3929                     }
3930                 }
3931             }
3932         }
3933     });
3934     *PROGRESSBAR_HEIGHT.read().unwrap()
3935 }
3936 
3937 pub fn set_progressbar_height(height: f32) {
3938     if let Ok(mut lock) = PROGRESSBAR_HEIGHT.write() {
3939         *lock = height;
3940     }
3941 }
3942 
3943 pub fn rangeslider_height() -> f32 {
3944     use std::sync::Once;
3945     static INIT: Once = Once::new();
3946     INIT.call_once(|| {
3947         if let Some(content) = read_config() {
3948             for line in content.lines() {
3949                 let trimmed = line.trim();
3950                 if let Some(rest) = trimmed.strip_prefix("rangeslider_height") {
3951                     let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
3952                     let val_str = rest.trim_end_matches('"').trim();
3953                     if let Ok(val) = val_str.parse::<f32>() {
3954                         if let Ok(mut lock) = RANGESLIDER_HEIGHT.write() {
3955                             *lock = val;
3956                         }
3957                     }
3958                 }
3959             }
3960         }
3961     });
3962     *RANGESLIDER_HEIGHT.read().unwrap()
3963 }
3964 
3965 pub fn set_rangeslider_height(height: f32) {
3966     if let Ok(mut lock) = RANGESLIDER_HEIGHT.write() {
3967         *lock = height;
3968     }
3969 }
3970 
3971 
3972 
3973 /// One step carve a widget's `paint` draws, handed to a flat-path host through
3974 /// [`RenderTarget::relief_carve`] so it can re-emit it as a real prim.
3975 ///
3976 /// Geometry always comes from the WIDGET (`TextBox::well`, `Toggle::well` /
3977 /// `Toggle::slide_plate`), never re-derived here — a second copy of that math
3978 /// in the bridge is exactly how the flat host's carve and the drawn one drift
3979 /// apart.
3980 #[derive(Clone, Copy, Debug, PartialEq)]
3981 pub struct ReliefCarve {
3982     pub kind: CarveKind,
3983     pub x: f32,
3984     pub y: f32,
3985     pub w: f32,
3986     pub h: f32,
3987     /// Per-corner radii, clockwise from top-left.
3988     pub radii: (f32, f32, f32, f32),
3989     /// Full width of the step's transition band.
3990     pub depth: f32,
3991     /// Which walls the carve has (top, right, bottom, left). A suppressed wall
3992     /// means the step runs flush to its neighbour there — a Spinbox's field
3993     /// running into its button column, where the two meet in ONE step rather
3994     /// than two facing walls.
3995     pub edges: (bool, bool, bool, bool),
3996 }
3997 
3998 /// Which way a [`ReliefCarve`] steps.
3999 #[derive(Clone, Copy, Debug, PartialEq)]
4000 pub enum CarveKind {
4001     /// Interior one step DOWN ([`crate::scene::paint::PaintCtx::recess_edges`]).
4002     /// `tint` lights the rim in the focus accent (`recess_tinted`).
4003     Recess { tint: Option<[f32; 3]> },
4004     /// Interior one step UP ([`crate::scene::paint::PaintCtx::boss_edges`]).
4005     Boss { tint: Option<[f32; 3]> },
4006     /// A FLUSH inset ([`crate::scene::paint::PaintCtx::trough_edges`]): the
4007     /// interior stays level with the surface and a valley seam runs the
4008     /// boundary — the closed dropdown's chrome, for a control that is part
4009     /// of the plate rather than a step up or down from it.
4010     Trough,
4011 }
4012 
4013 impl ReliefCarve {
4014     /// This carve shifted vertically — the page-scroll adjustment a host
4015     /// applies when it re-emits collected carves.
4016     pub fn shifted_y(self, dy: f32) -> Self {
4017         Self { y: self.y + dy, ..self }
4018     }
4019 }
4020 
4021 pub trait RenderTarget {
4022     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32);
4023     fn rect_with_radius(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, _radius: f32) {
4024         self.rect(color, x, y, w, h);
4025     }
4026     fn rect_with_radius_corners(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, _corners: (bool, bool, bool, bool)) {
4027         self.rect_with_radius(color, x, y, w, h, radius);
4028     }
4029     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]);
4030     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _font: &str) {
4031         self.text(content, x, y, size, color);
4032     }
4033     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], _bounds: Option<[f32; 4]>) {
4034         self.text(content, x, y, size, color);
4035     }
4036     fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, _bounds: Option<[f32; 4]>) {
4037         self.text_with_font(content, x, y, size, color, font);
4038     }
4039     fn push_clip_rect(&mut self, _x: f32, _y: f32, _w: f32, _h: f32) {}
4040     fn pop_clip_rect(&mut self) {}
4041     /// A flush inset control plate ([`PaintCtx::inset_plate`]) — the raised
4042     /// control surface (groove ring down, beveled lip back up). Lets a popover
4043     /// draw the ACTUAL widget surface expanded (the Dropdown's grown trigger).
4044     /// Hosts without relief prims degrade to a flat rounded fill.
4045     fn inset_plate(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, _depth: f32) {
4046         self.rect_with_radius(color, x, y, w, h, radius);
4047     }
4048     /// [`inset_plate`](Self::inset_plate) with the rim lit — the focused
4049     /// control plate's ring (`ControlPlate::with_tint`). Hosts without relief
4050     /// prims draw the plain plate.
4051     fn inset_plate_tinted(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32, radius: f32, depth: f32, _tint: [f32; 3]) {
4052         self.inset_plate(color, x, y, w, h, radius, depth);
4053     }
4054     /// One step carve from a widget's `paint` ([`ReliefCarve`]) — offered here
4055     /// for the same reason as `inset_plate`: the legacy `all_quads` stream
4056     /// carries no relief prims, so a flat-path host never sees them.
4057     ///
4058     /// The default is deliberately a NO-OP, not a fill. These controls have
4059     /// transparent faces by design (the host surface IS the well floor / the
4060     /// plate's face), so the carve is their entire decoration — a host that
4061     /// can't carve has nothing truthful to draw, and a solid box here would
4062     /// paint every text field a flat slab it never had.
4063     fn relief_carve(&mut self, _carve: &ReliefCarve) {}
4064     /// Whether this host renders sections as sunken wells (the designer idiom).
4065     /// `SectionContext` then lays the title out left-aligned over its tab box
4066     /// instead of centered on the top border.
4067     fn section_relief_style(&self) -> bool {
4068         false
4069     }
4070     /// The section frame hatch: `SectionContext::finish` offers the frame here
4071     /// before falling back to the legacy 1px outline. A relief-capable host
4072     /// returns true and carves the section into its plate instead (the
4073     /// designer sunken-well idiom); the tuple hosts keep the default.
4074     fn section_relief(&mut self, _frame: &SectionFrame) -> bool {
4075         false
4076     }
4077 }
4078 
4079 /// A section frame offered to [`RenderTarget::section_relief`]: the content
4080 /// body box plus, under [`RenderTarget::section_relief_style`], the title tab
4081 /// box the label was laid out in — the tab sits flush on the body's top edge
4082 /// (the designer union-carve shape).
4083 pub struct SectionFrame {
4084     pub x: f32,
4085     pub y: f32,
4086     pub w: f32,
4087     pub h: f32,
4088     pub tab: Option<(f32, f32, f32, f32)>,
4089     pub focused: bool,
4090     pub is_child: bool,
4091 }
4092 
4093 pub struct PopoverCollector {
4094     pub rects: Vec<([f32; 4], f32, f32, f32, f32)>,
4095     pub texts: Vec<(String, f32, f32, f32, [f32; 4], Option<String>, Option<[f32; 4]>)>,
4096 }
4097 
4098 impl PopoverCollector {
4099     pub fn new() -> Self {
4100         Self { rects: Vec::new(), texts: Vec::new() }
4101     }
4102 }
4103 
4104 impl RenderTarget for PopoverCollector {
4105     fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
4106         self.rects.push((color, x, y, w, h));
4107     }
4108 
4109     fn text(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4]) {
4110         self.texts.push((content.to_string(), size, x, y, color, None, None));
4111     }
4112 
4113     fn text_with_font(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str) {
4114         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), None));
4115     }
4116 
4117     fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], bounds: Option<[f32; 4]>) {
4118         self.texts.push((content.to_string(), size, x, y, color, None, bounds));
4119     }
4120 
4121     fn text_with_font_and_bounds(&mut self, content: &str, x: f32, y: f32, size: f32, color: [f32; 4], font: &str, bounds: Option<[f32; 4]>) {
4122         self.texts.push((content.to_string(), size, x, y, color, Some(font.to_string()), bounds));
4123     }
4124 }
4125 
4126 
4127 pub fn render_widget<T: WidgetHost + 'static>(pc: &mut dyn RenderTarget, w: &mut T, x: f32, y: f32, ww: f32, wh: f32, ctx: &mut UiContext) {
4128     let id = Some(w.base().id());
4129     if let Some(w_id) = id {
4130         ctx.register_widget(w_id, w as *mut T as *mut (dyn WidgetHost + 'static));
4131     }
4132     // The flat-host contract, the same block `set_rect` takes: `(x, y)` is the top of
4133     // the detached label and `wh` the block height, label strip included. `layout`
4134     // takes the CONTENT origin and height, so step down by the strip.
4135     let strip = w.label_strip();
4136     let content_h = (wh - strip).max(0.0);
4137     w.layout(crate::widget::Point { x, y: y + strip }, crate::widget::LayoutConstraints::new(ww, ww, content_h, content_h), ctx);
4138 
4139     // Shape, which on this path nobody else does. A flat host consumes
4140     // `all_quads`, so `prepare_text` — where a TextBox records the per-glyph x
4141     // offsets its selection highlight, caret and click->index mapping all read
4142     // — was never called for the widgets it draws. Those three then fell back
4143     // to `measure_text_width("M")`, an SVG-rasterized INKED extent rather than
4144     // an advance, so the highlight under-ran the glyphs by a few px per
4145     // character (a full glyph by the end of "example.com"). Hosts that shape
4146     // for themselves (cce-files, the TreeList) just re-read the shared buffer
4147     // cache here.
4148     if let Ok(mut fs) = crate::geometry_font_system().lock() {
4149         w.prepare_text(&mut fs);
4150     }
4151     let (style_r, corners) = w.corner_style();
4152     let r = if corners != (false, false, false, false) { style_r } else { 0.0 };
4153     let (wx, mut wy, www, mut whh) = w.rect();
4154     let top_room = w.label_strip();
4155     wy += top_room;
4156     whh -= top_room;
4157 
4158     // ONE ordered replay of the paint walk — the same walk the live display-
4159     // list render runs (`scene::painter`), every prim in the order the widget
4160     // painted it, each mapped onto the flat host's RenderTarget surface.
4161     //
4162     // This used to be three passes over three typed views of the same paint:
4163     // the relief prims (downcast per widget type and re-derived from the
4164     // widget's accessors — the Dropdown's inset plate, the TextBox's well, the
4165     // Button's face, the Toggle's faces and steps), then every plain quad
4166     // (`all_quads`), then every rounded quad (`all_rounded_quads`). Splitting
4167     // one paint into typed streams loses the order between them, and the
4168     // order is the picture: a TextBox draws its rounded background, carves
4169     // its well, THEN lays the selection highlight and caret on top — the
4170     // three-pass replay put the well under the highlight and the background
4171     // over both. A Dropdown's hovered row is drawn after its menu plate; a
4172     // host that replays plates after rects buries the highlight. The prim
4173     // walk keeps the widget's order, covers every widget instead of the four
4174     // that had a special case, and carries the relief prims' own per-corner
4175     // radii and depth (the re-derivations rounded those off).
4176     //
4177     // Mapping onto the tuple surface: Quad and RoundedRect are the two
4178     // native fills (the root's plain background keeps its solid-border
4179     // expansion and window-corner resolution); a zero-stroke Border is an
4180     // inset plate's FACE and is held until the Trough that follows it, so the
4181     // pair reaches the host as ONE `inset_plate` call (a relief host carves
4182     // it for real; the default degrades to the flat fill); Recess/Boss go to
4183     // `relief_carve`; a Bevel degrades to its fill — what a flat host can
4184     // draw of a raised plate. Ridges, circles, arcs, vectors and images have
4185     // no flat-surface counterpart and are skipped, as they always were.
4186     let solid_border = w.solid_border();
4187     let mut text_scratch = crate::scene::paint::PaintCtx::new();
4188     crate::scene::painter::paint_root_into(&*ctx, &*w, &mut text_scratch);
4189 
4190     // A zero-stroke Border waiting for its Trough: (rect, radii, fill).
4191     let mut pending_face: Option<(crate::scene::layout::Rect, crate::scene::paint::Radii, [f32; 4])> = None;
4192     fn same_rect(a: crate::scene::layout::Rect, b: crate::scene::layout::Rect) -> bool {
4193         (a.x - b.x).abs() < 0.1 && (a.y - b.y).abs() < 0.1 && (a.width - b.width).abs() < 0.1 && (a.height - b.height).abs() < 0.1
4194     }
4195     fn emit_rounded(pc: &mut dyn RenderTarget, rect: crate::scene::layout::Rect, radii: crate::scene::paint::Radii, color: [f32; 4]) {
4196         let (r1, r2, r3, r4) = radii;
4197         let radius = r1.max(r2).max(r3).max(r4);
4198         let mask = (r1 > 0.0, r2 > 0.0, r3 > 0.0, r4 > 0.0);
4199         pc.rect_with_radius_corners(color, rect.x, rect.y, rect.width, rect.height, radius, mask);
4200     }
4201 
4202     for item in text_scratch.finish().items {
4203         use crate::scene::paint::Prim;
4204         let trough_for_face = match (&item.prim, &pending_face) {
4205             (Prim::Trough { rect, .. }, Some((face_rect, _, _))) => same_rect(*rect, *face_rect),
4206             _ => false,
4207         };
4208         if !trough_for_face {
4209             if let Some((frect, fradii, fill)) = pending_face.take() {
4210                 emit_rounded(pc, frect, fradii, fill);
4211             }
4212         }
4213         match item.prim {
4214             Prim::Quad { rect, color: qc } => {
4215                 let (qx, qy, qw, qh) = (rect.x, rect.y, rect.width, rect.height);
4216                 let extra_corners = (
4217                     corners.0 && qx <= wx + 1.5 && qy <= wy + 1.5,
4218                     corners.1 && qx + qw >= wx + www - 1.5 && qy <= wy + 1.5,
4219                     corners.2 && qx + qw >= wx + www - 1.5 && qy + qh >= wy + whh - 1.5,
4220                     corners.3 && qx <= wx + 1.5 && qy + qh >= wy + whh - 1.5,
4221                 );
4222 
4223                 let (resolved_r, resolved_corners) = if r <= 0.1 || corners == (false, false, false, false) || extra_corners == (false, false, false, false) {
4224                     (0.0, (false, false, false, false))
4225                 } else {
4226                     (r, extra_corners)
4227                 };
4228 
4229                 // The widget's own background quad with a solid border: full-size
4230                 // border quad, then the inset background over it.
4231                 let mut border_drawn = false;
4232                 let is_bg_quad = (qx - wx).abs() < 0.1 && (qy - wy).abs() < 0.1 && (qw - www).abs() < 0.1 && (qh - whh).abs() < 0.1;
4233                 if is_bg_quad {
4234                     if let Some((border_color, thickness)) = solid_border {
4235                         if thickness > 0.0 {
4236                             pc.rect_with_radius_corners(border_color, qx, qy, qw, qh, resolved_r, resolved_corners);
4237                             pc.rect_with_radius_corners(
4238                                 qc,
4239                                 qx + thickness,
4240                                 qy + thickness,
4241                                 (qw - 2.0 * thickness).max(0.0),
4242                                 (qh - 2.0 * thickness).max(0.0),
4243                                 (resolved_r - thickness).max(0.0),
4244                                 resolved_corners,
4245                             );
4246                             border_drawn = true;
4247                         }
4248                     }
4249                 }
4250 
4251                 if !border_drawn {
4252                     pc.rect_with_radius_corners(qc, qx, qy, qw, qh, resolved_r, resolved_corners);
4253                 }
4254             }
4255             Prim::RoundedRect { rect, radius, corners: qcorners, color: qc } => {
4256                 pc.rect_with_radius_corners(qc, rect.x, rect.y, rect.width, rect.height, radius, qcorners);
4257             }
4258             Prim::Border { rect, radii, fill, border, thickness } => {
4259                 if thickness > 0.0 && border[3].abs() > 0.001 {
4260                     emit_rounded(pc, rect, radii, border);
4261                     if fill[3].abs() > 0.001 {
4262                         let inner = crate::scene::layout::Rect {
4263                             x: rect.x + thickness,
4264                             y: rect.y + thickness,
4265                             width: (rect.width - 2.0 * thickness).max(0.0),
4266                             height: (rect.height - 2.0 * thickness).max(0.0),
4267                         };
4268                         let (r1, r2, r3, r4) = radii;
4269                         let shrink = |v: f32| if v > 0.0 { (v - thickness).max(0.0) } else { 0.0 };
4270                         emit_rounded(pc, inner, (shrink(r1), shrink(r2), shrink(r3), shrink(r4)), fill);
4271                     }
4272                 } else if fill[3].abs() > 0.001 {
4273                     // abs(): a negative alpha is the frost sentinel, a real face.
4274                     pending_face = Some((rect, radii, fill));
4275                 }
4276             }
4277             Prim::Trough { rect, radii, depth, tint, .. } => {
4278                 let face = pending_face.take().map(|(_, _, fill)| fill).unwrap_or([0.0; 4]);
4279                 let (r1, r2, r3, r4) = radii;
4280                 let r = r1.max(r2).max(r3).max(r4);
4281                 match tint {
4282                     Some(t) => pc.inset_plate_tinted(face, rect.x, rect.y, rect.width, rect.height, r, depth, t),
4283                     None => pc.inset_plate(face, rect.x, rect.y, rect.width, rect.height, r, depth),
4284                 }
4285             }
4286             Prim::Bevel { rect, radii, material, .. } => {
4287                 let color = material.fill(crate::scene::material::PlateRole::Nested);
4288                 if color[3].abs() > 0.001 {
4289                     emit_rounded(pc, rect, radii, color);
4290                 }
4291             }
4292             Prim::Recess { rect, radii, depth, edges, tint } => {
4293                 pc.relief_carve(&ReliefCarve {
4294                     kind: CarveKind::Recess { tint },
4295                     x: rect.x,
4296                     y: rect.y,
4297                     w: rect.width,
4298                     h: rect.height,
4299                     radii,
4300                     depth,
4301                     edges,
4302                 });
4303             }
4304             Prim::Boss { rect, radii, depth, edges, tint } => {
4305                 pc.relief_carve(&ReliefCarve {
4306                     kind: CarveKind::Boss { tint },
4307                     x: rect.x,
4308                     y: rect.y,
4309                     w: rect.width,
4310                     h: rect.height,
4311                     radii,
4312                     depth,
4313                     edges,
4314                 });
4315             }
4316             // Text via the paint walk: each widget's Text prims (content font + scroll-ancestor
4317             // clip) exactly as the live display-list render does; the prim already carries the
4318             // per-widget font + bounds.
4319             Prim::Text { text, x, y, font_size, color, font, bounds, .. } => {
4320                 let color_f32 = [
4321                     color[0] as f32 / 255.0,
4322                     color[1] as f32 / 255.0,
4323                     color[2] as f32 / 255.0,
4324                     1.0,
4325                 ];
4326                 // Compose the walk's container clip with the prim's own bounds (the engine's dl-text
4327                 // merge), so a clipping ancestor still bounds the text.
4328                 let clip = item.clip.map(|c| [c.x, c.y, c.x + c.width, c.y + c.height]);
4329                 let merged = match (clip, bounds) {
4330                     (Some(a), Some(b)) => Some([a[0].max(b[0]), a[1].max(b[1]), a[2].min(b[2]), a[3].min(b[3])]),
4331                     (Some(a), None) => Some(a),
4332                     (None, b) => b,
4333                 };
4334                 match font {
4335                     Some(ref f) => pc.text_with_font_and_bounds(&text, x, y, font_size, color_f32, f, merged),
4336                     None => pc.text_with_bounds(&text, x, y, font_size, color_f32, merged),
4337                 }
4338             }
4339             _ => {}
4340         }
4341     }
4342     if let Some((frect, fradii, fill)) = pending_face.take() {
4343         emit_rounded(pc, frect, fradii, fill);
4344     }
4345     if w.popover_rect().is_some() {
4346         ctx.register_popover(w);
4347     }
4348 }
4349 
4350 pub fn render_popovers(pc: &mut dyn RenderTarget, ctx: &UiContext) {
4351     for &pop_id in &ctx.active_popovers {
4352         if let Some(ptr) = ctx.tree.get_ptr(pop_id) {
4353             unsafe {
4354                 (*ptr).render_popover(pc);
4355             }
4356         }
4357     }
4358     // Registry sweep for open popovers the app never registered (popover
4359     // registration is optional and spotty) — the same fallback the coverage
4360     // check and the engine's outside-press close use.
4361     for (id, ptr) in ctx.tree.iter_registered() {
4362         if ctx.active_popovers.contains(&id) {
4363             continue;
4364         }
4365         unsafe {
4366             if let Some(w) = ptr.as_ref() {
4367                 if w.visible() && w.popover_rect().is_some() {
4368                     w.render_popover(pc);
4369                 }
4370             }
4371         }
4372     }
4373 }
4374 
4375 pub fn partition_concentric_corners(
4376     x: f32, y: f32, w: f32, h: f32,
4377     _r_std: f32,
4378     r_adjust: [f32; 4],
4379     color: [f32; 4],
4380 ) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
4381     let mut quads = Vec::new();
4382 
4383     let r0 = r_adjust[0];
4384     let r1 = r_adjust[1];
4385     let r2 = r_adjust[2];
4386     let r3 = r_adjust[3];
4387 
4388     let max_left = r0.max(r3);
4389     let max_right = r1.max(r2);
4390 
4391     let mut push_valid_quad = |qx: f32, qy: f32, qw: f32, qh: f32, qr: f32, qcorners: (bool, bool, bool, bool)| {
4392         if qw > 0.001 && qh > 0.001 {
4393             quads.push((qx, qy, qw, qh, qr, color, qcorners));
4394         }
4395     };
4396 
4397     // 1. Center vertical block
4398     push_valid_quad(x + max_left, y, w - max_left - max_right, h, 0.0, (false, false, false, false));
4399 
4400     // 2. Left block
4401     push_valid_quad(x, y + r0, max_left, h - r0 - r3, 0.0, (false, false, false, false));
4402 
4403     // 3. Top-left transition
4404     push_valid_quad(x + r0, y, max_left - r0, r0, 0.0, (false, false, false, false));
4405 
4406     // 4. Bottom-left transition
4407     push_valid_quad(x + r3, y + h - r3, max_left - r3, r3, 0.0, (false, false, false, false));
4408 
4409     // 5. Right block
4410     push_valid_quad(x + w - max_right, y + r1, max_right, h - r1 - r2, 0.0, (false, false, false, false));
4411 
4412     // 6. Top-right transition
4413     push_valid_quad(x + w - max_right, y, max_right - r1, r1, 0.0, (false, false, false, false));
4414 
4415     // 7. Bottom-right transition
4416     push_valid_quad(x + w - max_right, y + h - r2, max_right - r2, r2, 0.0, (false, false, false, false));
4417 
4418     // 8. Corner 0 (top-left)
4419     push_valid_quad(x, y, r0, r0, r0, (true, false, false, false));
4420 
4421     // 9. Corner 1 (top-right)
4422     push_valid_quad(x + w - r1, y, r1, r1, r1, (false, true, false, false));
4423 
4424     // 10. Corner 2 (bottom-right)
4425     push_valid_quad(x + w - r2, y + h - r2, r2, r2, r2, (false, false, true, false));
4426 
4427     // 11. Corner 3 (bottom-left)
4428     push_valid_quad(x, y + h - r3, r3, r3, r3, (false, false, false, true));
4429 
4430     quads
4431 }
4432 
4433 pub struct UiFrame;
4434 
4435 impl UiFrame {
4436     pub fn start(scroll_offset: f32) -> Self {
4437         crate::widget::hover_animation::reset_frame_registration();
4438         crate::widget::hover_animation::set_scroll_offset(scroll_offset);
4439         Self
4440     }
4441 
4442     pub fn finish(self, pc: &mut dyn RenderTarget) {
4443         crate::widget::hover_animation::post_render_check();
4444         if let Some((qx, qy, qw, qh, qc)) = crate::widget::hover_animation::get_quad() {
4445             pc.rect(qc, qx, qy, qw, qh);
4446         }
4447         // render_popovers(pc);
4448     }
4449 }
4450 
4451 pub struct Column {
4452     ox: f32,
4453     oy: f32,
4454     cx: f32,
4455     pub y: f32,
4456     pub cw: f32,
4457 }
4458 
4459 impl Column {
4460     pub fn new(ox: f32, oy: f32, cx: f32, cy: f32, cw: f32) -> Self {
4461         Self { ox, oy, cx, y: cy, cw }
4462     }
4463 
4464     pub fn ax(&self, x_off: f32) -> f32 {
4465         self.ox + self.cx + x_off
4466     }
4467 
4468     pub fn ay(&self) -> f32 {
4469         self.oy + self.y
4470     }
4471 
4472     pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
4473         pc.rect(color, self.ax(x_off), self.ay(), w, h);
4474         self.y += h;
4475     }
4476 
4477     pub fn advance(&mut self, dy: f32) {
4478         self.y += dy;
4479     }
4480 
4481     pub fn spacing(&mut self, dy: f32) {
4482         self.y += dy;
4483     }
4484 
4485     pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
4486         let x = self.ax(8.0);
4487         let y = self.ay();
4488         pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 16.0, 1.0);
4489         self.y += 8.0;
4490     }
4491 
4492     pub fn header(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32) {
4493         let x = self.ax(x_off);
4494         let y = self.ay();
4495         pc.text(text, x, y, 14.0, [0.83, 0.83, 0.83, 1.0]);
4496         self.y += 22.0;
4497     }
4498 
4499     pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
4500         let x = self.ax(x_off);
4501         let y = self.ay() + y_off;
4502         pc.text(text, x, y, font_size, color);
4503     }
4504 
4505     pub fn widget<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, x_off: f32, ww: f32, mut wh: f32, ctx: &mut UiContext) {
4506         if let Some(pref) = w.preferred_height() {
4507             wh = pref;
4508         }
4509         let top_room = w.label_strip();
4510         let total_h = wh + top_room;
4511         let x = self.ax(x_off);
4512         let y = self.ay();
4513         w.set_row_rect(self.ox + self.cx + 8.0, self.cw - 16.0);
4514         let clamped_w = ww.min((self.cw - x_off).max(0.0));
4515         render_widget(pc, w, x, y, clamped_w, total_h, ctx);
4516         self.y += total_h;
4517     }
4518 
4519     pub fn row<F: FnOnce(&mut Row)>(&mut self, pc: &mut dyn RenderTarget, h: f32, f: F) {
4520         let row_y = self.ay();
4521         let mut row = Row {
4522             pc: &mut *pc,
4523             base_x: self.ox + self.cx,
4524             y: row_y,
4525             cursor_x: 0.0,
4526             spacing: CONTROL_GAP,
4527         };
4528         f(&mut row);
4529         self.y = self.y + h;
4530     }
4531 }
4532 
4533 pub struct Row<'a> {
4534     pc: &'a mut dyn RenderTarget,
4535     base_x: f32,
4536     y: f32,
4537     pub cursor_x: f32,
4538     pub spacing: f32,
4539 }
4540 
4541 impl<'a> Row<'a> {
4542     pub fn set_spacing(&mut self, spacing: f32) {
4543         self.spacing = spacing;
4544     }
4545 
4546     pub fn gap(&mut self, width: f32) {
4547         self.cursor_x += width;
4548     }
4549 
4550     pub fn text(&mut self, text: &str, y_off: f32, font_size: f32, color: [f32; 4], width: f32) {
4551         self.pc
4552             .text(text, self.base_x + self.cursor_x, self.y + y_off, font_size, color);
4553         self.cursor_x += width + self.spacing;
4554     }
4555 
4556     pub fn widget<T: WidgetHost + 'static>(&mut self, w: &mut T, ww: f32, mut wh: f32, ctx: &mut UiContext) {
4557         if let Some(pref) = w.preferred_height() {
4558             wh = pref;
4559         }
4560         render_widget(self.pc, w, self.base_x + self.cursor_x, self.y, ww, wh, ctx);
4561         self.cursor_x += ww + self.spacing;
4562     }
4563 }
4564 
4565 fn estimate_label_width_helper(label: &str, font_size: f32, font_fam: &str) -> f32 {
4566     let fam_lower = font_fam.to_lowercase();
4567     let is_mono = fam_lower.contains("mono") || fam_lower.contains("courier") || fam_lower == "monospace";
4568     if is_mono {
4569         label.chars().count() as f32 * font_size * 0.60
4570     } else {
4571         let mut width = 0.0;
4572         for c in label.chars() {
4573             let factor = match c {
4574                 'i' | 'l' | 'I' | ' ' | '.' | ',' | '!' | ';' | ':' | '\'' | '"' | '(' | ')' | '[' | ']' | '-' => 0.30,
4575                 'f' | 'j' | 't' => 0.35,
4576                 'r' | 's' | 'c' | 'z' => 0.50,
4577                 'a' | 'b' | 'd' | 'e' | 'g' | 'h' | 'k' | 'n' | 'o' | 'p' | 'q' | 'u' | 'v' | 'x' | 'y' => 0.60,
4578                 'm' | 'w' | 'M' | 'W' | '&' | '@' | 'O' | 'Q' | 'G' => 0.85,
4579                 'A' | 'B' | 'C' | 'D' | 'H' | 'N' | 'U' | 'V' | 'X' | 'Y' => 0.75,
4580                 'E' | 'F' | 'K' | 'L' | 'P' | 'R' | 'S' | 'T' | 'Z' | 'J' => 0.68,
4581                 '0'..='9' => 0.60,
4582                 _ => 0.60,
4583             };
4584             width += factor * font_size;
4585         }
4586         width
4587     }
4588 }
4589 
4590 pub struct Section {
4591     pub left: f32,
4592     pub top: f32,
4593     pub content_y: f32,
4594     pub cw: f32,
4595     pub label_width: f32,
4596     pub is_child: bool,
4597     pub grid: Grid,
4598     pub last_col: usize,
4599 }
4600 
4601 impl Section {
4602     pub const DEFAULT_MARGIN_X: f32 = 12.0;
4603     pub const DEFAULT_ROW_GAP: f32 = 8.0;
4604 
4605     fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
4606         estimate_label_width_helper(label, font_size, font_fam)
4607     }
4608 
4609     pub fn padding(&self) -> f32 {
4610         section_padding()
4611     }
4612 
4613     pub fn new(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str) -> Self {
4614         Self::new_opt(pc, left, top, cw, label, false)
4615     }
4616 
4617     pub fn new_opt(pc: &mut dyn RenderTarget, left: f32, top: f32, cw: f32, label: &str, is_child: bool) -> Self {
4618         let font_setting = if is_child {
4619             nested_section_label_font()
4620         } else {
4621             section_label_font()
4622         };
4623         let (font_fam, font_size_opt) = parse_font_string(&font_setting);
4624         let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
4625         let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
4626         let label_width = Self::estimate_label_width(label, font_size, &font_fam);
4627         let label_x = if is_child {
4628             let base_x = match nested_section_label_alignment() {
4629                 0 => left + 12.0,
4630                 1 => left + (cw - label_width) / 2.0,
4631                 2 => left + cw - 12.0 - label_width,
4632                 _ => left + 12.0,
4633             };
4634             base_x + nested_section_label_offset()
4635         } else {
4636             left + (cw - label_width) / 2.0
4637         };
4638         pc.text_with_font(label, label_x, top, font_size, font_color, &font_fam);
4639 
4640         let pad = section_padding();
4641         let margin_x = 2.0 * pad + 12.0;
4642         let usable_w = (cw - 2.0 * margin_x).max(1.0);
4643         let min_col_width = 130.0;
4644         let gap = 8.0;
4645         let max_cols = if is_child {
4646             1
4647         } else {
4648             ((usable_w + gap) / (min_col_width + gap)).floor().max(1.0).min(2.0) as usize
4649         };
4650         let content_start_y = top + pad + 19.0;
4651         let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
4652 
4653         Self { left, top, content_y: content_start_y, cw, label_width, is_child, grid, last_col: usize::MAX }
4654     }
4655 
4656     /// Horizontal inset of content from this section's left edge — the same
4657     /// one `row_layout`, the column `Grid` and `widget` use, so everything in
4658     /// a section lines up. See `SectionContext::content_margin`.
4659     pub fn content_margin(&self) -> f32 {
4660         2.0 * self.padding() + 12.0
4661     }
4662 
4663     pub fn content_left(&self) -> f32 {
4664         self.left + self.content_margin()
4665     }
4666 
4667     pub fn content_width(&self) -> f32 {
4668         (self.cw - 2.0 * self.content_margin()).max(0.0)
4669     }
4670 
4671     /// See `SectionContext::ax` — same mapping, same reason it is no longer
4672     /// stepped at `x_off == 12.0`.
4673     pub fn ax(&self, x_off: f32) -> f32 {
4674         self.left + 2.0 * self.padding() + x_off
4675     }
4676 
4677     pub fn ay(&self) -> f32 { self.content_y }
4678 
4679     pub fn spacing(&mut self, dy: f32) {
4680         if self.grid.col_heights.len() >= 2 {
4681             if self.last_col == usize::MAX {
4682                 for h in &mut self.grid.col_heights {
4683                     *h += dy;
4684                 }
4685             } else if self.last_col < self.grid.col_heights.len() {
4686                 self.grid.col_heights[self.last_col] += dy;
4687             }
4688             self.content_y = self.grid.max_height();
4689         } else {
4690             self.content_y += dy;
4691             for h in &mut self.grid.col_heights {
4692                 *h += dy;
4693             }
4694         }
4695     }
4696 
4697     pub fn text(&mut self, pc: &mut dyn RenderTarget, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
4698         let x = self.ax(x_off);
4699         let y = self.ay() + y_off;
4700         // Bounded to the content box, like `SectionContext::text` — text is
4701         // the only thing a section draws that is not already sized to fit it.
4702         let right = self.content_left() + self.content_width();
4703         let bounds = if right > x {
4704             Some([x, y - font_size, right, y + 2.0 * font_size])
4705         } else {
4706             None
4707         };
4708         pc.text_with_bounds(text, x, y, font_size, color, bounds);
4709     }
4710 
4711     pub fn widget<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
4712         if let Some(pref) = w.preferred_height() {
4713             wh = pref;
4714         }
4715         let pad = self.padding();
4716         let top_room = w.label_strip();
4717         let total_h = wh + top_room;
4718 
4719         let name = w.type_name();
4720         let span_full = name == "Trackpad"
4721             || name == "Canvas"
4722             || name == "UsageBar"
4723             || name == "ProgressBar"
4724             || name == "ButtonStrip"
4725             || name == "Spreadsheet"
4726             || name == "Graph";
4727 
4728         if span_full {
4729             let margin_x = 2.0 * pad + 12.0;
4730             let x = self.left + margin_x;
4731             let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
4732             let max_h = self.grid.max_height().max(self.content_y);
4733             let y = max_h;
4734 
4735             w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
4736             render_widget(pc, w, x, y, clamped_w, total_h, ctx);
4737 
4738             let new_bottom = y + total_h;
4739             self.content_y = new_bottom;
4740             for h in &mut self.grid.col_heights {
4741                 *h = new_bottom;
4742             }
4743         } else {
4744             let max_h = self.grid.max_height();
4745             if self.content_y > max_h {
4746                 for h in &mut self.grid.col_heights {
4747                     *h = self.content_y;
4748                 }
4749             }
4750 
4751             let col = self.grid.next_column();
4752             self.last_col = col;
4753             let x = self.grid.col_lefts[col];
4754             let y = self.grid.col_heights[col];
4755 
4756             w.set_row_rect(x, self.grid.col_width);
4757             render_widget(pc, w, x, y, self.grid.col_width, total_h, ctx);
4758             self.grid.col_heights[col] += total_h;
4759             self.content_y = self.grid.max_height();
4760         }
4761     }
4762 
4763     pub fn widget_full<T: WidgetHost + 'static>(&mut self, pc: &mut dyn RenderTarget, w: &mut T, wh: f32, ctx: &mut UiContext) {
4764         let x_off = 12.0;
4765         let ww = self.cw - 2.0 * (self.padding() + x_off);
4766         self.widget(pc, w, x_off, ww, wh, ctx);
4767     }
4768 
4769     pub fn separator(&mut self, pc: &mut dyn RenderTarget) {
4770         let pad = self.padding();
4771         let x = self.ax(pad);
4772         let max_h = self.grid.max_height().max(self.content_y);
4773         let y = max_h;
4774         pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
4775         self.content_y = max_h + 8.0;
4776         for h in &mut self.grid.col_heights {
4777             *h = self.content_y;
4778         }
4779     }
4780 
4781     pub fn rect(&mut self, pc: &mut dyn RenderTarget, color: [f32; 4], x_off: f32, w: f32, h: f32) {
4782         let max_h = self.grid.max_height().max(self.content_y);
4783         pc.rect(color, self.ax(x_off), max_h, w, h);
4784         self.content_y = max_h + h;
4785         for col_h in &mut self.grid.col_heights {
4786             *col_h = self.content_y;
4787         }
4788     }
4789 
4790     pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
4791         let margin_x = self.content_margin();
4792         let usable_w = self.content_width();
4793         if count == 0 {
4794             return Vec::new();
4795         }
4796         let total_gap = gap * (count - 1) as f32;
4797         let col_w = (usable_w - total_gap).max(0.0) / count as f32;
4798 
4799         let mut cols = Vec::with_capacity(count);
4800         for i in 0..count {
4801             let x = self.left + margin_x + i as f32 * (col_w + gap);
4802             cols.push((x, col_w));
4803         }
4804         cols
4805     }
4806 
4807     pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
4808     where
4809         F: FnMut(usize, f32, f32),
4810     {
4811         let max_h = self.grid.max_height().max(self.content_y);
4812         for col_h in &mut self.grid.col_heights {
4813             *col_h = max_h;
4814         }
4815         self.content_y = max_h;
4816 
4817         let cols = self.row_layout(count, gap);
4818         for (i, &(x, w)) in cols.iter().enumerate() {
4819             f(i, x, w);
4820         }
4821         self.content_y += h;
4822 
4823         for col_h in &mut self.grid.col_heights {
4824             *col_h = self.content_y;
4825         }
4826     }
4827 
4828     pub fn finish(&mut self, pc: &mut dyn RenderTarget) -> f32 {
4829         self.finish_focused(pc, false)
4830     }
4831 
4832     pub fn finish_focused(&mut self, pc: &mut dyn RenderTarget, focused: bool) -> f32 {
4833         let border: [f32; 4] = if self.is_child {
4834             if focused {
4835                 [0.22, 0.38, 0.24, 1.0]
4836             } else {
4837                 [0.18, 0.18, 0.25, 1.0]
4838             }
4839         } else {
4840             if focused {
4841                 [0.30, 0.50, 0.32, 1.0]
4842             } else {
4843                 [0.25, 0.25, 0.35, 1.0]
4844             }
4845         };
4846         let pad = self.padding();
4847         let x = self.left + pad;
4848         let y = self.top + 7.0;
4849         let w = self.cw - 2.0 * pad;
4850         let h = self.content_y - y;
4851         
4852         let left_edge = x;
4853         let right_edge = x + w;
4854         if self.label_width > 0.0 {
4855             let label_x = if self.is_child {
4856                 let base_x = match nested_section_label_alignment() {
4857                     0 => self.left + 12.0,
4858                     1 => self.left + (self.cw - self.label_width) / 2.0,
4859                     2 => self.left + self.cw - 12.0 - self.label_width,
4860                     _ => self.left + 12.0,
4861                 };
4862                 base_x + nested_section_label_offset()
4863             } else {
4864                 self.left + (self.cw - self.label_width) / 2.0
4865             };
4866             let gap_margin = 6.0;
4867             let gap_start = label_x - gap_margin;
4868             let gap_end = label_x + self.label_width + gap_margin;
4869             if gap_start > left_edge {
4870                 pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
4871             }
4872             if right_edge > gap_end {
4873                 pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
4874             }
4875         } else {
4876             pc.rect(border, left_edge, y, w, 1.0);
4877         }
4878 
4879         let extra_bottom = pad + 12.0;
4880         pc.rect(border, x, y + h + extra_bottom, w, 1.0);
4881         pc.rect(border, x, y, 1.0, h + extra_bottom);
4882         pc.rect(border, x + w - 1.0, y, 1.0, h + extra_bottom);
4883         self.content_y + extra_bottom + 8.0
4884     }
4885 
4886     pub fn vstack<'a>(&'a mut self, pc: &'a mut dyn RenderTarget, spacing: f32) -> SectionVStack<'a> {
4887         SectionVStack {
4888             section: self,
4889             pc,
4890             spacing,
4891         }
4892     }
4893 }
4894 
4895 pub struct SectionVStack<'a> {
4896     section: &'a mut Section,
4897     pc: &'a mut dyn RenderTarget,
4898     spacing: f32,
4899 }
4900 
4901 impl<'a> SectionVStack<'a> {
4902     pub fn add_widget<T: WidgetHost + 'static>(&mut self, w: &mut T, ww: f32, wh: f32, ctx: &mut UiContext) {
4903         self.section.widget(self.pc, w, Section::DEFAULT_MARGIN_X, ww, wh, ctx);
4904         self.section.spacing(self.spacing);
4905     }
4906 
4907     pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, f: F)
4908     where
4909         F: FnMut(usize, f32, f32),
4910     {
4911         self.section.row(count, gap, h, f);
4912         self.section.spacing(self.spacing);
4913     }
4914 }
4915 
4916 
4917 pub struct SplitterLayout {
4918     pub splitter1_x: f32,
4919     pub splitter2_x: f32,
4920     pub splitter_width: f32,
4921     pub min_column_width: f32,
4922 }
4923 
4924 impl SplitterLayout {
4925     pub fn new(width: f32, splitter_width: f32, min_column_width: f32) -> Self {
4926         let s1 = (width - 2.0 * splitter_width) / 3.0;
4927         let s2 = s1 + splitter_width + (width - 2.0 * splitter_width) / 3.0;
4928         Self {
4929             splitter1_x: s1,
4930             splitter2_x: s2,
4931             splitter_width,
4932             min_column_width,
4933         }
4934     }
4935 
4936     pub fn clamp(&mut self, total_width: f32, detached_circular_network: bool) {
4937         if detached_circular_network {
4938             let min_s2 = self.min_column_width;
4939             let max_s2 = (total_width - self.min_column_width).max(min_s2);
4940             self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
4941         } else {
4942             let min_s1 = self.min_column_width;
4943             let max_s1 = (self.splitter2_x - self.splitter_width - self.min_column_width).max(min_s1);
4944             self.splitter1_x = self.splitter1_x.clamp(min_s1, max_s1);
4945             let min_s2 = self.splitter1_x + self.splitter_width + self.min_column_width;
4946             let max_s2 = (total_width - self.min_column_width).max(min_s2);
4947             self.splitter2_x = self.splitter2_x.clamp(min_s2, max_s2);
4948         }
4949     }
4950 
4951     pub fn scale(&mut self, factor: f32) {
4952         self.splitter1_x *= factor;
4953         self.splitter2_x *= factor;
4954     }
4955 
4956     pub fn left_col(&self) -> (f32, f32) { // (x, width)
4957         (0.0, self.splitter1_x)
4958     }
4959 
4960     pub fn center_col(&self) -> (f32, f32) { // (x, width)
4961         let x = self.splitter1_x + self.splitter_width;
4962         (x, self.splitter2_x - x)
4963     }
4964 
4965     pub fn right_col(&self, total_width: f32) -> (f32, f32) { // (x, width)
4966         let x = self.splitter2_x + self.splitter_width;
4967         (x, (total_width - x).max(0.0))
4968     }
4969 }
4970 
4971 #[derive(Debug, Clone)]
4972 pub struct Grid {
4973     pub left: f32,
4974     pub top: f32,
4975     pub width: f32,
4976     pub col_width: f32,
4977     pub gap: f32,
4978     pub col_heights: Vec<f32>,
4979     pub col_lefts: Vec<f32>,
4980 }
4981 
4982 impl Grid {
4983     pub fn new(left: f32, top: f32, width: f32, min_col_width: f32, gap: f32, count: usize) -> Self {
4984         let total_gap = gap * (count - 1) as f32;
4985         let col_width = if count > 0 {
4986             (width - total_gap).max(0.0) / count as f32
4987         } else {
4988             min_col_width
4989         };
4990         let left_offset = 0.0;
4991 
4992         let mut col_lefts = Vec::with_capacity(count);
4993         let col_heights = vec![top; count];
4994         for i in 0..count {
4995             col_lefts.push(left + left_offset + i as f32 * (col_width + gap));
4996         }
4997 
4998         Self {
4999             left,
5000             top,
5001             width,
5002             col_width,
5003             gap,
5004             col_heights,
5005             col_lefts,
5006         }
5007     }
5008 
5009     pub fn next_column(&self) -> usize {
5010         let mut min_idx = 0;
5011         let mut min_h = self.col_heights[0];
5012         for i in 1..self.col_heights.len() {
5013             if self.col_heights[i] < min_h {
5014                 min_h = self.col_heights[i];
5015                 min_idx = i;
5016             }
5017         }
5018         min_idx
5019     }
5020 
5021     pub fn max_height(&self) -> f32 {
5022         let mut max_h = self.col_heights[0];
5023         for i in 1..self.col_heights.len() {
5024             if self.col_heights[i] > max_h {
5025                 max_h = self.col_heights[i];
5026             }
5027         }
5028         max_h
5029     }
5030 }
5031 
5032 pub struct CircularPaneLayout {
5033     pub x: f32,
5034     pub y: f32,
5035     pub r: f32,
5036 }
5037 
5038 impl CircularPaneLayout {
5039     pub fn new(x: f32, y: f32, r: f32) -> Self {
5040         Self { x, y, r }
5041     }
5042 
5043     pub fn hit_test_content(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
5044         let dx = cx - self.x;
5045         let dy = cy - self.y;
5046         let dist_sq = dx * dx + dy * dy;
5047         dist_sq <= self.r * self.r && cy >= self.y - self.r + 45.0 + menubar_h + breadcrumb_h
5048     }
5049 
5050     pub fn hit_test_menubar(&self, cx: f32, cy: f32, menubar_h: f32) -> bool {
5051         let dx = cx - self.x;
5052         let dy = cy - self.y;
5053         let dist = (dx * dx + dy * dy).sqrt();
5054         dist >= self.r - menubar_h && dist <= self.r && cy < self.y
5055     }
5056 
5057     pub fn hit_test_breadcrumb(&self, cx: f32, cy: f32, menubar_h: f32, breadcrumb_h: f32) -> bool {
5058         let dx = cx - self.x;
5059         let dy = cy - self.y;
5060         let dist_sq = dx * dx + dy * dy;
5061         dist_sq <= self.r * self.r && cy >= self.y - self.r + menubar_h && cy < self.y - self.r + 45.0 + menubar_h + breadcrumb_h
5062     }
5063 
5064     pub fn hit_test_border(&self, cx: f32, cy: f32, border_thickness: f32) -> bool {
5065         let dx = cx - self.x;
5066         let dy = cy - self.y;
5067         let dist = (dx * dx + dy * dy).sqrt();
5068         dist >= self.r - border_thickness && dist <= self.r
5069     }
5070 }
5071 
5072 #[derive(Debug, Clone)]
5073 pub struct Radial {
5074     pub center_x: f32,
5075     pub center_y: f32,
5076     pub aspect_ratio: f32,
5077     pub base_spacing: f32,
5078 }
5079 
5080 impl Radial {
5081     pub fn new(center_x: f32, center_y: f32, aspect_ratio: f32, base_spacing: f32) -> Self {
5082         Self { center_x, center_y, aspect_ratio, base_spacing }
5083     }
5084 
5085     pub fn widget_rect(&self, idx: usize, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
5086         if idx == 0 {
5087             (self.center_x - ww / 2.0, self.center_y - wh / 2.0, ww, wh)
5088         } else {
5089             let mut ring = 1;
5090             let mut ring_start = 1;
5091             loop {
5092                 let ring_capacity = ring * 6;
5093                 if idx < ring_start + ring_capacity {
5094                     let pos_in_ring = idx - ring_start;
5095                     let angle = (pos_in_ring as f32) * (2.0 * std::f32::consts::PI / ring_capacity as f32);
5096                     let radius = (ring as f32) * self.base_spacing;
5097 
5098                     let x_offset = radius * angle.cos() * self.aspect_ratio;
5099                     let y_offset = radius * angle.sin();
5100 
5101                     return (
5102                         self.center_x + x_offset - ww / 2.0,
5103                         self.center_y + y_offset - wh / 2.0,
5104                         ww,
5105                         wh,
5106                     );
5107                 }
5108                 ring_start += ring_capacity;
5109                 ring += 1;
5110             }
5111         }
5112     }
5113 
5114 }
5115 
5116 
5117 
5118 pub trait LayoutStrategy: std::fmt::Debug {
5119     fn init(&mut self, left: f32, top: f32, width: f32, height: f32);
5120     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32);
5121     fn set_section_count(&mut self, _count: usize) {}
5122     fn get_column_width(&self) -> Option<f32> { None }
5123     fn get_gap(&self) -> f32 { 20.0 }
5124 
5125     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &mut crate::context::UiContext) -> f32;
5126     fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size;
5127     fn box_clone(&self) -> Box<dyn LayoutStrategy>;
5128 }
5129 
5130 impl Clone for Box<dyn LayoutStrategy> {
5131     fn clone(&self) -> Self {
5132         self.box_clone()
5133     }
5134 }
5135 
5136 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
5137 pub enum FlexDirection {
5138     Row,
5139     Column,
5140 }
5141 
5142 #[derive(Debug, Clone)]
5143 pub struct FlexLayout {
5144     left: f32,
5145     top: f32,
5146     width: f32,
5147     height: f32,
5148     direction: FlexDirection,
5149     spacing: f32,
5150     current_x: f32,
5151     current_y: f32,
5152 }
5153 
5154 impl FlexLayout {
5155     pub fn new(direction: FlexDirection, spacing: f32) -> Self {
5156         Self {
5157             left: 0.0,
5158             top: 0.0,
5159             width: 0.0,
5160             height: 0.0,
5161             direction,
5162             spacing,
5163             current_x: 0.0,
5164             current_y: 0.0,
5165         }
5166     }
5167 }
5168 
5169 impl LayoutStrategy for FlexLayout {
5170     fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
5171         self.left = left;
5172         self.top = top;
5173         self.width = width;
5174         self.height = height;
5175         self.current_x = left;
5176         self.current_y = top;
5177     }
5178 
5179     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
5180         match self.direction {
5181             FlexDirection::Row => {
5182                 let rx = self.current_x;
5183                 let ry = self.current_y;
5184                 self.current_x += ww + self.spacing;
5185                 (rx, ry, ww, wh)
5186             }
5187             FlexDirection::Column => {
5188                 let rx = self.current_x;
5189                 let ry = self.current_y;
5190                 self.current_y += wh + self.spacing;
5191                 (rx, ry, ww, wh)
5192             }
5193         }
5194     }
5195 
5196     fn get_gap(&self) -> f32 {
5197         self.spacing
5198     }
5199 
5200     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &mut crate::context::UiContext) -> f32 {
5201         let (cur_x, cur_y) = (x, y);
5202         // Blocks: the label row (`label_lead`) above every child's content, the
5203         // gap between blocks, so a row's controls are level and a carve-out tab
5204         // sits a full gap from its neighbour.
5205         let lead = crate::widget::container::container_layout::label_lead(children);
5206         match self.direction {
5207             FlexDirection::Row => {
5208                 let mut cur_x = cur_x;
5209                 for &child_ptr in children {
5210                     unsafe {
5211                         let child = &mut *child_ptr;
5212                         let child_w = child.rect().2;
5213                         let child_h = crate::widget::container::container_layout::content_height(child);
5214                         let use_h = if child_h > 0.0 { child_h } else { h };
5215                         child.layout(
5216                             crate::widget::Point { x: cur_x, y: cur_y + lead },
5217                             crate::widget::LayoutConstraints::new(child_w, child_w, use_h, use_h),
5218                             ctx,
5219                         );
5220                         cur_x += child_w + self.spacing;
5221                     }
5222                 }
5223                 (cur_x - x).max(0.0)
5224             }
5225             FlexDirection::Column => {
5226                 let mut cur_y = cur_y;
5227                 for &child_ptr in children {
5228                     unsafe {
5229                         let child = &mut *child_ptr;
5230                         let child_h = crate::widget::container::container_layout::content_height(child);
5231                         let use_h = if child_h > 0.0 { child_h } else { 44.0 };
5232                         child.layout(
5233                             crate::widget::Point { x, y: cur_y + lead },
5234                             crate::widget::LayoutConstraints::new(w, w, use_h, use_h),
5235                             ctx,
5236                         );
5237                         cur_y += lead + use_h + self.spacing;
5238                     }
5239                 }
5240                 (cur_y - y).max(0.0)
5241             }
5242         }
5243     }
5244 
5245     fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
5246         match self.direction {
5247             FlexDirection::Row => {
5248                 let mut total_w = 0.0f32;
5249                 let mut max_h = 0.0f32;
5250                 for (i, &child_ptr) in children.iter().enumerate() {
5251                     unsafe {
5252                         let size = (*child_ptr).measure(constraints, ctx);
5253                         total_w += size.width;
5254                         max_h = max_h.max(size.height);
5255                         if i > 0 {
5256                             total_w += self.spacing;
5257                         }
5258                     }
5259                 }
5260                 crate::widget::Size {
5261                     width: total_w.clamp(constraints.min_width, constraints.max_width),
5262                     height: max_h.clamp(constraints.min_height, constraints.max_height),
5263                 }
5264             }
5265             FlexDirection::Column => {
5266                 let mut total_h = 0.0f32;
5267                 let mut max_w = 0.0f32;
5268                 for (i, &child_ptr) in children.iter().enumerate() {
5269                     unsafe {
5270                         let size = (*child_ptr).measure(constraints, ctx);
5271                         total_h += size.height;
5272                         max_w = max_w.max(size.width);
5273                         if i > 0 {
5274                             total_h += self.spacing;
5275                         }
5276                     }
5277                 }
5278                 crate::widget::Size {
5279                     width: max_w.clamp(constraints.min_width, constraints.max_width),
5280                     height: total_h.clamp(constraints.min_height, constraints.max_height),
5281                 }
5282             }
5283         }
5284     }
5285 
5286     fn box_clone(&self) -> Box<dyn LayoutStrategy> {
5287         Box::new(self.clone())
5288     }
5289 }
5290 
5291 #[derive(Debug, Clone)]
5292 pub struct ColumnLayout {
5293     left: f32,
5294     top: f32,
5295     width: f32,
5296     current_y: f32,
5297     gap: f32,
5298 }
5299 
5300 impl ColumnLayout {
5301     pub fn new(gap: f32) -> Self {
5302         Self {
5303             left: 0.0,
5304             top: 0.0,
5305             width: 0.0,
5306             current_y: 0.0,
5307             gap,
5308         }
5309     }
5310 }
5311 
5312 impl LayoutStrategy for ColumnLayout {
5313     fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
5314         self.left = left;
5315         self.top = top;
5316         self.width = width;
5317         self.current_y = top;
5318     }
5319 
5320     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
5321         let x = self.left;
5322         let y = self.current_y;
5323         self.current_y += wh + self.gap;
5324         (x, y, ww, wh)
5325     }
5326 
5327     fn get_column_width(&self) -> Option<f32> {
5328         Some(self.width)
5329     }
5330 
5331     fn get_gap(&self) -> f32 {
5332         self.gap
5333     }
5334 
5335     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
5336         let mut cur_y = y;
5337         for &child_ptr in children {
5338             unsafe {
5339                 let child = &mut *child_ptr;
5340                 let child_h = child.preferred_height().unwrap_or(child.rect().3);
5341                 let use_h = if child_h > 0.0 { child_h } else { 44.0 };
5342                 child.set_rect(x, cur_y, w, use_h);
5343                 cur_y += use_h + self.gap;
5344             }
5345         }
5346         (cur_y - y).max(0.0)
5347     }
5348 
5349     fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
5350         let mut total_h = 0.0f32;
5351         let mut max_w = 0.0f32;
5352         for (i, &child_ptr) in children.iter().enumerate() {
5353             unsafe {
5354                 let size = (*child_ptr).measure(constraints, ctx);
5355                 total_h += size.height;
5356                 max_w = max_w.max(size.width);
5357                 if i > 0 {
5358                     total_h += self.gap;
5359                 }
5360             }
5361         }
5362         crate::widget::Size {
5363             width: max_w.clamp(constraints.min_width, constraints.max_width),
5364             height: total_h.clamp(constraints.min_height, constraints.max_height),
5365         }
5366     }
5367 
5368     fn box_clone(&self) -> Box<dyn LayoutStrategy> {
5369         Box::new(self.clone())
5370     }
5371 }
5372 
5373 #[derive(Debug, Clone)]
5374 pub struct AdaptiveGrid {
5375     grid: Option<Grid>,
5376     #[allow(dead_code)]
5377     min_col_width: f32,
5378     #[allow(dead_code)]
5379     gap: f32,
5380     num_sections: Option<usize>,
5381 }
5382 
5383 impl AdaptiveGrid {
5384     pub fn new(min_col_width: f32, gap: f32) -> Self {
5385         Self {
5386             grid: None,
5387             min_col_width,
5388             gap,
5389             num_sections: None,
5390         }
5391     }
5392 }
5393 
5394 impl LayoutStrategy for AdaptiveGrid {
5395     fn init(&mut self, left: f32, top: f32, width: f32, _height: f32) {
5396         let min_col_width = crate::layout::grid_min_col_width();
5397         let gap = crate::layout::grid_gap();
5398         let max_cols = ((width + gap) / (min_col_width + gap)).floor().max(1.0) as usize;
5399         let count = if let Some(n) = self.num_sections {
5400             n.min(max_cols).max(1)
5401         } else {
5402             max_cols
5403         };
5404         self.grid = Some(Grid::new(left, top, width, min_col_width, gap, count));
5405     }
5406 
5407     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
5408         if let Some(ref mut grid) = self.grid {
5409             let num_cols = grid.col_heights.len();
5410             let num_cols_spanned = (((ww + grid.gap) / (grid.col_width + grid.gap)).round() as usize)
5411                 .min(num_cols)
5412                 .max(1);
5413 
5414             if num_cols_spanned >= num_cols {
5415                 let y = grid.max_height();
5416                 let x = grid.left;
5417                 let allocated_w = grid.width;
5418                 for col_h in &mut grid.col_heights {
5419                     *col_h = y + wh + grid.gap;
5420                 }
5421                 (x, y, allocated_w, wh)
5422             } else if num_cols_spanned == 1 {
5423                 let col = grid.next_column();
5424                 let x = grid.col_lefts[col];
5425                 let y = grid.col_heights[col];
5426                 grid.col_heights[col] += wh + grid.gap;
5427                 (x, y, grid.col_width, wh)
5428             } else {
5429                 let n = num_cols_spanned;
5430                 let mut best_start_col = 0;
5431                 let mut min_max_h = f32::MAX;
5432                 for c in 0..=(num_cols - n) {
5433                     let mut max_h = 0.0f32;
5434                     for i in 0..n {
5435                         if grid.col_heights[c + i] > max_h {
5436                             max_h = grid.col_heights[c + i];
5437                         }
5438                     }
5439                     if max_h < min_max_h {
5440                         min_max_h = max_h;
5441                         best_start_col = c;
5442                     }
5443                 }
5444                 let x = grid.col_lefts[best_start_col];
5445                 let y = min_max_h;
5446                 let allocated_w = n as f32 * grid.col_width + (n - 1) as f32 * grid.gap;
5447                 for i in 0..n {
5448                     grid.col_heights[best_start_col + i] = y + wh + grid.gap;
5449                 }
5450                 (x, y, allocated_w, wh)
5451             }
5452         } else {
5453             (0.0, 0.0, 0.0, wh)
5454         }
5455     }
5456 
5457     fn set_section_count(&mut self, count: usize) {
5458         self.num_sections = Some(count);
5459     }
5460 
5461     fn get_column_width(&self) -> Option<f32> {
5462         self.grid.as_ref().map(|g| g.col_width)
5463     }
5464 
5465     fn get_gap(&self) -> f32 {
5466         crate::layout::grid_gap()
5467     }
5468 
5469     fn layout(&self, x: f32, y: f32, w: f32, _h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
5470         let usable_w = w.max(1.0);
5471         let min_col_width = crate::layout::grid_min_col_width();
5472         let gap = crate::layout::grid_gap();
5473         let cols = (((usable_w + gap) / (min_col_width + gap)).floor().max(1.0)) as usize;
5474         let count = if let Some(n) = self.num_sections {
5475             n.min(cols).max(1)
5476         } else {
5477             cols
5478         };
5479 
5480         let total_gap = gap * (count - 1) as f32;
5481         let available_w = (w - total_gap).max(1.0);
5482         let col_w = available_w / count as f32;
5483         
5484         let mut col_heights = vec![y; count];
5485 
5486         for &child_ptr in children {
5487             unsafe {
5488                 let child = &mut *child_ptr;
5489                 let ch = child.preferred_height().unwrap_or(child.rect().3);
5490                 let use_h = if ch > 0.0 { ch } else { 44.0 };
5491                 
5492                 let mut min_col = 0;
5493                 let mut min_h = col_heights[0];
5494                 for i in 1..count {
5495                     if col_heights[i] < min_h {
5496                         min_h = col_heights[i];
5497                         min_col = i;
5498                     }
5499                 }
5500                 
5501                 let cx = x + min_col as f32 * (col_w + gap);
5502                 let cy = col_heights[min_col];
5503                 child.set_rect(cx, cy, col_w, use_h);
5504                 col_heights[min_col] += use_h + gap;
5505             }
5506         }
5507         
5508         let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
5509         (max_h - y).max(0.0)
5510     }
5511 
5512     fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
5513         let usable_w = constraints.max_width.max(1.0);
5514         let min_col_width = crate::layout::grid_min_col_width();
5515         let gap = crate::layout::grid_gap();
5516         let cols = (((usable_w + gap) / (min_col_width + gap)).floor().max(1.0)) as usize;
5517         let count = if let Some(n) = self.num_sections {
5518             n.min(cols).max(1)
5519         } else {
5520             cols
5521         };
5522 
5523         let mut col_heights = vec![0.0f32; count];
5524         let total_gap = gap * (count - 1) as f32;
5525         let available_w = (constraints.max_width - total_gap).max(1.0);
5526         let col_w = available_w / count as f32;
5527         
5528         let child_constraints = crate::widget::LayoutConstraints::new(col_w, col_w, constraints.min_height, constraints.max_height);
5529 
5530         for &child_ptr in children {
5531             unsafe {
5532                 let size = (*child_ptr).measure(child_constraints, ctx);
5533                 let mut min_col = 0;
5534                 let mut min_h = col_heights[0];
5535                 for i in 1..count {
5536                     if col_heights[i] < min_h {
5537                         min_h = col_heights[i];
5538                         min_col = i;
5539                     }
5540                 }
5541                 col_heights[min_col] += size.height + gap;
5542             }
5543         }
5544         
5545         let max_h = col_heights.iter().cloned().fold(0.0f32, |a, b| a.max(b));
5546         crate::widget::Size {
5547             width: constraints.max_width,
5548             height: max_h.clamp(constraints.min_height, constraints.max_height),
5549         }
5550     }
5551 
5552     fn box_clone(&self) -> Box<dyn LayoutStrategy> {
5553         Box::new(self.clone())
5554     }
5555 }
5556 
5557 #[derive(Debug, Clone)]
5558 pub struct RadialLayout {
5559     radial: Option<Radial>,
5560     aspect_ratio: f32,
5561     base_spacing: f32,
5562     idx: usize,
5563 }
5564 
5565 impl RadialLayout {
5566     pub fn new(aspect_ratio: f32, base_spacing: f32) -> Self {
5567         Self {
5568             radial: None,
5569             aspect_ratio,
5570             base_spacing,
5571             idx: 0,
5572         }
5573     }
5574 }
5575 
5576 impl LayoutStrategy for RadialLayout {
5577     fn init(&mut self, left: f32, top: f32, width: f32, height: f32) {
5578         let cx = left + width / 2.0;
5579         let cy = top + height / 2.0;
5580         let aspect = if self.aspect_ratio > 0.0 {
5581             self.aspect_ratio
5582         } else {
5583             let screen_aspect = (width / height.max(1.0)).max(0.1);
5584             1.0 + (screen_aspect - 1.0) * 0.4
5585         };
5586         self.radial = Some(Radial::new(cx, cy, aspect, self.base_spacing));
5587         self.idx = 0;
5588     }
5589 
5590     fn allocate(&mut self, ww: f32, wh: f32) -> (f32, f32, f32, f32) {
5591         if let Some(ref radial) = self.radial {
5592             let rect = radial.widget_rect(self.idx, ww, wh);
5593             self.idx += 1;
5594             rect
5595         } else {
5596             (0.0, 0.0, ww, wh)
5597         }
5598     }
5599 
5600     fn layout(&self, x: f32, y: f32, w: f32, h: f32, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], _ctx: &mut crate::context::UiContext) -> f32 {
5601         let cx = x + w / 2.0;
5602         let cy = y + h / 2.0;
5603         let aspect = if self.aspect_ratio > 0.0 {
5604             self.aspect_ratio
5605         } else {
5606             let screen_aspect = (w / h.max(1.0)).max(0.1);
5607             1.0 + (screen_aspect - 1.0) * 0.4
5608         };
5609         let radial = Radial::new(cx, cy, aspect, self.base_spacing);
5610         for (idx, &child_ptr) in children.iter().enumerate() {
5611             unsafe {
5612                 let child = &mut *child_ptr;
5613                 let cw = child.rect().2;
5614                 let ch = child.preferred_height().unwrap_or(child.rect().3);
5615                 let use_h = if ch > 0.0 { ch } else { 44.0 };
5616                 let (rx, ry, rw, rh) = radial.widget_rect(idx, cw, use_h);
5617                 child.set_rect(rx, ry, rw, rh);
5618             }
5619         }
5620         h
5621     }
5622 
5623     fn measure(&self, constraints: crate::widget::LayoutConstraints, children: &[*mut (dyn crate::widget::WidgetHost + 'static)], ctx: &crate::context::UiContext) -> crate::widget::Size {
5624         let cx = constraints.max_width / 2.0;
5625         let cy = constraints.max_height / 2.0;
5626         let aspect = if self.aspect_ratio > 0.0 {
5627             self.aspect_ratio
5628         } else {
5629             let screen_aspect = (constraints.max_width / constraints.max_height.max(1.0)).max(0.1);
5630             1.0 + (screen_aspect - 1.0) * 0.4
5631         };
5632         let radial = Radial::new(cx, cy, aspect, self.base_spacing);
5633         let mut max_w = 0.0f32;
5634         let mut max_h = 0.0f32;
5635         for (idx, &child_ptr) in children.iter().enumerate() {
5636             unsafe {
5637                 let size = (*child_ptr).measure(constraints, ctx);
5638                 let (rx, ry, rw, rh) = radial.widget_rect(idx, size.width, size.height);
5639                 max_w = max_w.max(rx + rw);
5640                 max_h = max_h.max(ry + rh);
5641             }
5642         }
5643         crate::widget::Size {
5644             width: max_w.clamp(constraints.min_width, constraints.max_width),
5645             height: max_h.clamp(constraints.min_height, constraints.max_height),
5646         }
5647     }
5648 
5649     fn box_clone(&self) -> Box<dyn LayoutStrategy> {
5650         Box::new(self.clone())
5651     }
5652 }
5653 
5654 /// Vertical slack for a section's content clip. The clip exists to stop
5655 /// content escaping its section SIDEWAYS, which is the axis a section's width
5656 /// actually fixes; a section's height is only known once its content has been
5657 /// placed, so bounding that axis too would risk cutting content off rather
5658 /// than keeping it in. Deliberately far larger than any section.
5659 const SECTION_CLIP_SLACK: f32 = 100_000.0;
5660 
5661 pub struct PageLayoutBuilder<'a, P> {
5662     pub strategy: &'a mut dyn LayoutStrategy,
5663     pub cx: f32,
5664     pub cy: f32,
5665     pub cw: f32,
5666     pub ch: f32,
5667     pub section_width: f32,
5668     pub idx: usize,
5669     _phantom: std::marker::PhantomData<P>,
5670 }
5671 
5672 impl<'a, P: RenderTarget + Default> PageLayoutBuilder<'a, P> {
5673     pub fn new(
5674         strategy: &'a mut dyn LayoutStrategy,
5675         cx: f32,
5676         cy: f32,
5677         cw: f32,
5678         ch: f32,
5679         section_width: f32,
5680     ) -> Self {
5681         strategy.init(cx, cy, cw, ch);
5682         Self {
5683             strategy,
5684             cx,
5685             cy,
5686             cw,
5687             ch,
5688             section_width,
5689             idx: 0,
5690             _phantom: std::marker::PhantomData,
5691         }
5692     }
5693 
5694     pub fn with_section_count(self, count: usize) -> Self {
5695         self.strategy.set_section_count(count);
5696         self.strategy.init(self.cx, self.cy, self.cw, self.ch);
5697         self
5698     }
5699 
5700     pub fn add_section<F>(&mut self, final_pc: &mut P, label: &str, focused: bool, mut render_fn: F)
5701     where
5702         F: FnMut(&mut SectionContext<'_, P>),
5703     {
5704         let width = self.strategy.get_column_width().unwrap_or(self.section_width);
5705         let mut dummy = P::default();
5706         let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
5707         render_fn(&mut dummy_ctx);
5708         let wh = dummy_ctx.finish();
5709         let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
5710         let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
5711         let (clip_x, clip_w) = (real_ctx.content_left(), real_ctx.content_width());
5712         real_ctx.pc.push_clip_rect(clip_x, ry - SECTION_CLIP_SLACK, clip_w, 2.0 * SECTION_CLIP_SLACK);
5713         render_fn(&mut real_ctx);
5714         real_ctx.pc.pop_clip_rect();
5715         real_ctx.finish();
5716         self.idx += 1;
5717     }
5718 
5719     pub fn add_section_with_width<F>(&mut self, final_pc: &mut P, width: f32, label: &str, focused: bool, mut render_fn: F)
5720     where
5721         F: FnMut(&mut SectionContext<'_, P>),
5722     {
5723         let mut dummy = P::default();
5724         let mut dummy_ctx = SectionContext::new(&mut dummy, 0.0, 0.0, width, label, focused, false);
5725         render_fn(&mut dummy_ctx);
5726         let wh = dummy_ctx.finish();
5727         let (rx, ry, rw, _) = self.strategy.allocate(width, wh);
5728         let mut real_ctx = SectionContext::new(final_pc, rx, ry, rw, label, focused, false);
5729         let (clip_x, clip_w) = (real_ctx.content_left(), real_ctx.content_width());
5730         real_ctx.pc.push_clip_rect(clip_x, ry - SECTION_CLIP_SLACK, clip_w, 2.0 * SECTION_CLIP_SLACK);
5731         render_fn(&mut real_ctx);
5732         real_ctx.pc.pop_clip_rect();
5733         real_ctx.finish();
5734         self.idx += 1;
5735     }
5736 
5737     pub fn add_section_spanned<F>(&mut self, final_pc: &mut P, label: &str, span: usize, focused: bool, render_fn: F)
5738     where
5739         F: FnMut(&mut SectionContext<'_, P>),
5740     {
5741         let col_width = self.strategy.get_column_width().unwrap_or(self.section_width);
5742         let gap = self.strategy.get_gap();
5743         let width = span as f32 * col_width + (span - 1) as f32 * gap;
5744         self.add_section_with_width(final_pc, width, label, focused, render_fn);
5745     }
5746 }
5747 
5748 pub struct SectionContext<'a, P> {
5749     pub pc: &'a mut P,
5750     pub left: f32,
5751     pub top: f32,
5752     pub content_y: f32,
5753     pub cw: f32,
5754     pub label_width: f32,
5755     pub label_x: f32,
5756     /// Whether the host renders sections as sunken wells (`section_relief_style`).
5757     pub relief_style: bool,
5758     /// The title tab box (x, y, w, h) when the host's relief styling laid the
5759     /// label out left-aligned — `finish` offers it with the section carve.
5760     /// None under relief styling means a label-less section: the well carves
5761     /// tabless, flush with the allocation top.
5762     pub relief_tab: Option<(f32, f32, f32, f32)>,
5763     pub focused: bool,
5764     pub is_child: bool,
5765     pub grid: Grid,
5766     pub last_col: usize,
5767     pub content_start_y: f32,
5768     pub row_gap: f32,
5769 }
5770 
5771 impl<'a, P: RenderTarget> SectionContext<'a, P> {
5772     pub const DEFAULT_MARGIN_X: f32 = 12.0;
5773     pub const DEFAULT_ROW_GAP: f32 = 8.0;
5774 
5775     fn estimate_label_width(label: &str, font_size: f32, font_fam: &str) -> f32 {
5776         estimate_label_width_helper(label, font_size, font_fam)
5777     }
5778 
5779     pub fn padding(&self) -> f32 {
5780         section_padding()
5781     }
5782 
5783     pub fn new(pc: &'a mut P, left: f32, top: f32, cw: f32, label: &str, focused: bool, is_child: bool) -> Self {
5784         let font_setting = if is_child {
5785             nested_section_label_font()
5786         } else {
5787             section_label_font()
5788         };
5789         let (font_fam, font_size_opt) = parse_font_string(&font_setting);
5790         let font_size = font_size_opt.unwrap_or(if is_child { 12.0 } else { 14.0 });
5791         let font_color = if is_child { [0.53, 0.53, 0.60, 1.0] } else { [0.83, 0.83, 0.83, 1.0] };
5792         let label_width = Self::estimate_label_width(label, font_size, &font_fam);
5793         let relief_style = pc.section_relief_style();
5794         let label_x = if is_child {
5795             let base_x = match nested_section_label_alignment() {
5796                 0 => left + 12.0,
5797                 1 => left + (cw - label_width) / 2.0,
5798                 2 => left + cw - 12.0 - label_width,
5799                 _ => left + 12.0,
5800             };
5801             base_x + nested_section_label_offset()
5802         } else if relief_style {
5803             // Sunken style: the title sits in a tab flush with the well's left
5804             // edge (the designer look), not centered on the border.
5805             left + 12.0
5806         } else {
5807             left + (cw - label_width) / 2.0
5808         };
5809         // Under relief styling the well fills the whole allocated rect (so the
5810         // page's gaps and margins are the visual gaps) — the tab tops the
5811         // allocation and the label centers inside it.
5812         let label_y = if relief_style { top + 4.0 } else { top };
5813         pc.text_with_font(label, label_x, label_y, font_size, font_color, &font_fam);
5814 
5815         // The tab wraps the label, flush on the well's top-left corner; clamped
5816         // so off-default child alignments can't push it outside the well.
5817         let relief_tab = if relief_style && label_width > 0.0 {
5818             let tab_x = (label_x - 12.0).max(left);
5819             Some((tab_x, top, label_width + 24.0, font_size + 10.0))
5820         } else {
5821             None
5822         };
5823 
5824         let pad = section_padding();
5825         let margin_x = 2.0 * pad + 12.0;
5826         let usable_w = (cw - 2.0 * margin_x).max(1.0);
5827         let min_col_width = 130.0;
5828         let gap = 8.0;
5829         let max_cols = if is_child {
5830             1
5831         } else {
5832             ((usable_w + gap) / (min_col_width + gap)).floor().max(1.0).min(2.0) as usize
5833         };
5834         let content_start_y = top + pad + 19.0;
5835         let grid = Grid::new(left + margin_x, content_start_y, usable_w, min_col_width, gap, max_cols);
5836 
5837         Self {
5838             pc,
5839             left,
5840             top,
5841             content_y: content_start_y,
5842             cw,
5843             label_width,
5844             label_x,
5845             relief_style,
5846             relief_tab,
5847             focused,
5848             is_child,
5849             grid,
5850             last_col: usize::MAX,
5851             content_start_y,
5852             row_gap: Self::DEFAULT_ROW_GAP,
5853         }
5854     }
5855 
5856     pub fn with_row_gap(mut self, gap: f32) -> Self {
5857         self.row_gap = gap;
5858         self
5859     }
5860 
5861     /// The section's content-box top edge: the body well's top (the tab's
5862     /// bottom) under relief styling, the outline's border line otherwise. Lets
5863     /// a page place content at an exact inset from the well's walls.
5864     pub fn well_top(&self) -> f32 {
5865         if self.relief_style {
5866             self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top)
5867         } else {
5868             self.top + 7.0
5869         }
5870     }
5871 
5872     pub fn set_row_gap(&mut self, gap: f32) {
5873         self.row_gap = gap;
5874     }
5875 
5876     /// Horizontal inset of section CONTENT from the section's left edge.
5877     ///
5878     /// One number, used by every content placer in here — `row_layout`, the
5879     /// column `Grid`, `widget`, `VStack` and `ax` (so `text`) — because they
5880     /// share a section and have to line up inside it. The section's border is
5881     /// drawn at `left + padding()` (see `finish`), so content clears the
5882     /// border by `padding() + 12`.
5883     pub fn content_margin(&self) -> f32 {
5884         2.0 * self.padding() + 12.0
5885     }
5886 
5887     /// Left edge of the content box: where a row, a widget or a `text(_, 12.0,
5888     /// ..)` starts.
5889     pub fn content_left(&self) -> f32 {
5890         self.left + self.content_margin()
5891     }
5892 
5893     /// Width of the content box — the section's width less the inset on both
5894     /// sides. Nothing a section draws should extend past `content_left() +
5895     /// content_width()`.
5896     pub fn content_width(&self) -> f32 {
5897         (self.cw - 2.0 * self.content_margin()).max(0.0)
5898     }
5899 
5900     /// `x_off` px into the content box's coordinate space, where 12.0 is the
5901     /// content's own left edge — the offset 46 of the ~55 call sites in the
5902     /// tree already pass, and the one that lines text up with the buttons and
5903     /// widgets beside it.
5904     ///
5905     /// This used to add `padding()` only when `x_off >= 12.0`, which made the
5906     /// mapping DISCONTINUOUS: asking for 11 instead of 12 moved the text 5px
5907     /// LEFT rather than 1px, and silently dropped it out of alignment with
5908     /// every row in the same section. `cce-mail` and two others sit on the
5909     /// wrong side of that cliff today.
5910     pub fn ax(&self, x_off: f32) -> f32 {
5911         self.left + 2.0 * self.padding() + x_off
5912     }
5913 
5914     pub fn ay(&self) -> f32 {
5915         self.content_y
5916     }
5917 
5918     pub fn spacing(&mut self, dy: f32) {
5919         if self.grid.col_heights.len() >= 2 {
5920             if self.last_col == usize::MAX {
5921                 for h in &mut self.grid.col_heights {
5922                     *h += dy;
5923                 }
5924             } else if self.last_col < self.grid.col_heights.len() {
5925                 self.grid.col_heights[self.last_col] += dy;
5926             }
5927             self.content_y = self.grid.max_height();
5928         } else {
5929             self.content_y += dy;
5930             for h in &mut self.grid.col_heights {
5931                 *h += dy;
5932             }
5933         }
5934     }
5935 
5936     pub fn text(&mut self, text: &str, x_off: f32, y_off: f32, font_size: f32, color: [f32; 4]) {
5937         let mut y = self.content_y + y_off;
5938         if y > self.content_start_y {
5939             y += self.row_gap;
5940         }
5941         let x = self.ax(x_off);
5942         // Bound it to the content box. A section's text was drawn unbounded,
5943         // so a string wider than its section simply kept going — over the
5944         // border, over whatever sat to the right, and off the window (the
5945         // settings app's GPU names did all three). Rows and widgets have
5946         // always been sized to the section; text was the one thing that could
5947         // leave it. The vertical band is generous on purpose: it is the
5948         // horizontal overrun that has to be cut, and a tight band would
5949         // shave descenders.
5950         let right = self.content_left() + self.content_width();
5951         let bounds = if right > x {
5952             Some([x, y - font_size, right, y + 2.0 * font_size])
5953         } else {
5954             None
5955         };
5956         self.pc.text_with_bounds(text, x, y, font_size, color, bounds);
5957         let new_bottom = y + font_size + 4.0;
5958         self.content_y = new_bottom;
5959         for h in &mut self.grid.col_heights {
5960             *h = new_bottom;
5961         }
5962     }
5963 
5964     pub fn widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _x_off: f32, _ww: f32, mut wh: f32, ctx: &mut UiContext) {
5965         if let Some(pref) = w.preferred_height() {
5966             wh = pref;
5967         }
5968         let pad = self.padding();
5969         let top_room = w.label_strip();
5970         let total_h = wh + top_room;
5971 
5972         let name = w.type_name();
5973         let span_full = name == "Trackpad"
5974             || name == "Canvas"
5975             || name == "UsageBar"
5976             || name == "ProgressBar"
5977             || name == "ButtonStrip"
5978             || name == "Spreadsheet"
5979             || name == "Graph";
5980 
5981         if span_full {
5982             let margin_x = 2.0 * pad + 12.0;
5983             let x = self.left + margin_x;
5984             let clamped_w = (self.cw - 2.0 * margin_x).max(0.0);
5985             let mut max_h = self.grid.max_height().max(self.content_y);
5986             if max_h > self.content_start_y {
5987                 max_h += self.row_gap;
5988             }
5989             let y = max_h;
5990 
5991             w.set_row_rect(self.left + pad, self.cw - 2.0 * pad);
5992             render_widget(self.pc, w, x, y, clamped_w, total_h, ctx);
5993 
5994             let new_bottom = y + total_h;
5995             self.content_y = new_bottom;
5996             for h in &mut self.grid.col_heights {
5997                 *h = new_bottom;
5998             }
5999         } else {
6000             let max_h = self.grid.max_height();
6001             if self.content_y > max_h {
6002                 for h in &mut self.grid.col_heights {
6003                     *h = self.content_y;
6004                 }
6005             }
6006 
6007             let col = self.grid.next_column();
6008             self.last_col = col;
6009             let x = self.grid.col_lefts[col];
6010             let mut y = self.grid.col_heights[col];
6011             if y > self.content_start_y {
6012                 y += self.row_gap;
6013             }
6014 
6015             let aligned_x = x;
6016             let aligned_w = self.grid.col_width;
6017 
6018             w.set_row_rect(aligned_x, aligned_w);
6019             render_widget(self.pc, w, aligned_x, y, aligned_w, total_h, ctx);
6020             self.grid.col_heights[col] = y + total_h;
6021             self.content_y = self.grid.max_height();
6022         }
6023     }
6024 
6025     pub fn widget_full<T: WidgetHost + 'static>(&mut self, w: &mut T, wh: f32, ctx: &mut UiContext) {
6026         let x_off = 12.0;
6027         let ww = self.cw - 2.0 * (self.padding() + x_off); // cw - 40.0
6028         self.widget(w, x_off, ww, wh, ctx);
6029     }
6030 
6031     pub fn separator(&mut self) {
6032         let pad = self.padding();
6033         let x = self.ax(pad);
6034         let max_h = self.grid.max_height().max(self.content_y);
6035         let y = max_h;
6036         self.pc.rect([0.18, 0.18, 0.27, 1.0], x, y, self.cw - 2.0 * pad, 1.0);
6037         self.content_y = max_h + 8.0;
6038         for h in &mut self.grid.col_heights {
6039             *h = self.content_y;
6040         }
6041     }
6042 
6043     pub fn rect(&mut self, color: [f32; 4], x_off: f32, w: f32, h: f32) {
6044         let max_h = self.grid.max_height().max(self.content_y);
6045         self.pc.rect(color, self.ax(x_off), max_h, w, h);
6046         self.content_y = max_h + h;
6047         for col_h in &mut self.grid.col_heights {
6048             *col_h = self.content_y;
6049         }
6050     }
6051 
6052     pub fn row_layout(&self, count: usize, gap: f32) -> Vec<(f32, f32)> {
6053         let margin_x = self.content_margin();
6054         let usable_w = self.content_width();
6055         if count == 0 {
6056             return Vec::new();
6057         }
6058         let total_gap = gap * (count - 1) as f32;
6059         let col_w = (usable_w - total_gap).max(0.0) / count as f32;
6060 
6061         let mut cols = Vec::with_capacity(count);
6062         for i in 0..count {
6063             let x = self.left + margin_x + i as f32 * (col_w + gap);
6064             cols.push((x, col_w));
6065         }
6066         cols
6067     }
6068 
6069     /// A row whose columns are sized to what goes IN them: each gets the width
6070     /// it asked for in `needs`, and whatever is left over is shared equally.
6071     ///
6072     /// `row_layout` splits a row evenly and knows nothing about content, so it
6073     /// hands "Reboot" and "Hibernate" the same width — one floats in slack
6074     /// while the other is cut off, which is what a row of mismatched labels
6075     /// looks like. Sharing the SLACK equally rather than sizing proportionally
6076     /// is deliberate: proportional widths would make a two-character label a
6077     /// sliver, where what is wanted is "everyone fits, then everyone gets the
6078     /// same bonus".
6079     ///
6080     /// When the needs do not fit, every column is scaled by the same factor, so
6081     /// the row still cannot overflow its section and the shortfall is shared
6082     /// rather than landing entirely on the last column.
6083     pub fn row_layout_for(&self, needs: &[f32], gap: f32) -> Vec<(f32, f32)> {
6084         let count = needs.len();
6085         if count == 0 {
6086             return Vec::new();
6087         }
6088         let total_gap = gap * (count - 1) as f32;
6089         let room = (self.content_width() - total_gap).max(0.0);
6090         let total_need: f32 = needs.iter().map(|n| n.max(0.0)).sum();
6091 
6092         let widths: Vec<f32> = if total_need <= room {
6093             let extra = (room - total_need) / count as f32;
6094             needs.iter().map(|n| n.max(0.0) + extra).collect()
6095         } else if total_need > 0.0 {
6096             let scale = room / total_need;
6097             needs.iter().map(|n| n.max(0.0) * scale).collect()
6098         } else {
6099             vec![room / count as f32; count]
6100         };
6101 
6102         let mut cols = Vec::with_capacity(count);
6103         let mut x = self.content_left();
6104         for w in widths {
6105             cols.push((x, w));
6106             x += w + gap;
6107         }
6108         cols
6109     }
6110 
6111 
6112     pub fn row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
6113     where
6114         F: FnMut(usize, f32, f32),
6115     {
6116         let max_h = self.grid.max_height().max(self.content_y);
6117         for col_h in &mut self.grid.col_heights {
6118             *col_h = max_h;
6119         }
6120         self.content_y = max_h;
6121 
6122         let cols = self.row_layout(count, gap);
6123         for (i, &(x, w)) in cols.iter().enumerate() {
6124             f(i, x, w);
6125         }
6126         self.content_y += h;
6127 
6128         for col_h in &mut self.grid.col_heights {
6129             *col_h = self.content_y;
6130         }
6131     }
6132 
6133     pub fn vstack(&mut self, spacing: f32) -> VStack<'_, 'a, P> {
6134         VStack {
6135             context: self,
6136             spacing,
6137         }
6138     }
6139 
6140     pub fn add_section<F>(&mut self, label: &str, focused: bool, mut render_fn: F)
6141     where
6142         F: FnMut(&mut SectionContext<'_, P>),
6143     {
6144         let pad = self.padding();
6145         let (left, top, cw, is_side_by_side) = if self.grid.col_heights.len() >= 2 {
6146             let col = self.grid.next_column();
6147             self.last_col = col;
6148             let x = self.grid.col_lefts[col];
6149             let mut y = self.grid.col_heights[col];
6150             if y > self.content_start_y {
6151                 y += self.row_gap;
6152             }
6153             (x, y, self.grid.col_width, true)
6154         } else {
6155             let left = self.ax(0.0) + pad;
6156             let mut max_h = self.grid.max_height().max(self.content_y);
6157             if max_h > self.content_start_y {
6158                 max_h += self.row_gap;
6159             }
6160             let top = max_h;
6161             let cw = self.cw - 2.0 * pad;
6162             (left, top, cw, false)
6163         };
6164 
6165         let mut sub_ctx = SectionContext::new(self.pc, left, top, cw, label, focused, true);
6166         render_fn(&mut sub_ctx);
6167         let new_bottom = sub_ctx.finish();
6168 
6169         if is_side_by_side {
6170             let col = self.last_col;
6171             self.grid.col_heights[col] = new_bottom;
6172             self.content_y = self.grid.max_height();
6173         } else {
6174             self.content_y = new_bottom;
6175             for h in &mut self.grid.col_heights {
6176                 *h = new_bottom;
6177             }
6178         }
6179     }
6180 
6181     pub fn finish(self) -> f32 {
6182         let border: [f32; 4] = if self.is_child {
6183             if self.focused {
6184                 [0.22, 0.38, 0.24, 1.0]
6185             } else {
6186                 [0.18, 0.18, 0.25, 1.0]
6187             }
6188         } else {
6189             if self.focused {
6190                 [0.30, 0.50, 0.32, 1.0] // Focused green
6191             } else {
6192                 [0.25, 0.25, 0.35, 1.0] // Default gray
6193             }
6194         };
6195         let pad = self.padding();
6196         let x = self.left + pad;
6197         let y = self.top + 7.0;
6198         let w = self.cw - 2.0 * pad;
6199         let h = self.content_y - y;
6200         let extra_bottom = pad + 12.0;
6201         let bottom = y + h + extra_bottom;
6202 
6203         if self.relief_style {
6204             // Sunken style: the well spans the full allocated rect — body top
6205             // edge at the tab's bottom (the tab is flush ON the body, the
6206             // designer union shape; label-less sections carve tabless from the
6207             // allocation top), walls on the allocation's edges, and no
6208             // trailing slack so the layout gap IS the visual gap.
6209             let body_y = self.relief_tab.map(|t| t.1 + t.3).unwrap_or(self.top);
6210             let frame = SectionFrame {
6211                 x: self.left,
6212                 y: body_y,
6213                 w: self.cw,
6214                 h: bottom - body_y,
6215                 tab: self.relief_tab,
6216                 focused: self.focused,
6217                 is_child: self.is_child,
6218             };
6219             if self.pc.section_relief(&frame) {
6220                 return self.content_y + extra_bottom;
6221             }
6222         }
6223 
6224         let left_edge = x;
6225         let right_edge = x + w;
6226         if self.label_width > 0.0 {
6227             let label_x = self.label_x;
6228             let gap_margin = 6.0;
6229             let gap_start = label_x - gap_margin;
6230             let gap_end = label_x + self.label_width + gap_margin;
6231             if gap_start > left_edge {
6232                 self.pc.rect(border, left_edge, y, gap_start - left_edge, 1.0);
6233             }
6234             if right_edge > gap_end {
6235                 self.pc.rect(border, gap_end, y, right_edge - gap_end, 1.0);
6236             }
6237         } else {
6238             self.pc.rect(border, left_edge, y, w, 1.0);
6239         }
6240 
6241         self.pc.rect(border, x, y + h + extra_bottom, w, 1.0);
6242         self.pc.rect(border, x, y, 1.0, h + extra_bottom);
6243         self.pc.rect(border, x + w - 1.0, y, 1.0, h + extra_bottom);
6244         self.content_y + extra_bottom + 8.0
6245     }
6246 }
6247 
6248 
6249 pub struct VStack<'b, 'a, P> {
6250     pub context: &'b mut SectionContext<'a, P>,
6251     pub spacing: f32,
6252 }
6253 
6254 impl<'b, 'a, P: RenderTarget> VStack<'b, 'a, P> {
6255     pub fn add_widget<T: WidgetHost + 'static>(&mut self, w: &mut T, _ww: f32, wh: f32, ctx: &mut UiContext) {
6256         let pad = self.context.padding();
6257         let margin_x = 2.0 * pad + 12.0;
6258         let x = self.context.left + margin_x;
6259         let clamped_w = (self.context.cw - 2.0 * margin_x).max(0.0);
6260 
6261         let mut max_h = self.context.grid.max_height().max(self.context.content_y);
6262         if max_h > self.context.content_start_y {
6263             max_h += self.context.row_gap;
6264         }
6265         let y = max_h;
6266 
6267         let pref_h = w.preferred_height().unwrap_or(wh);
6268         let top_room = w.label_strip();
6269         let total_h = pref_h + top_room;
6270 
6271         w.set_row_rect(self.context.left + pad, self.context.cw - 2.0 * pad);
6272         render_widget(self.context.pc, w, x, y, clamped_w, total_h, ctx);
6273 
6274         let new_bottom = y + total_h;
6275         self.context.content_y = new_bottom;
6276         for h in &mut self.context.grid.col_heights {
6277             *h = new_bottom;
6278         }
6279         self.context.spacing(self.spacing);
6280     }
6281 
6282     /// [`add_row`](Self::add_row) with per-column widths from `needs` — see
6283     /// [`SectionContext::row_layout_for`]. For a row of buttons, `needs` is
6284     /// each label's measured width plus the plate's own inset.
6285     pub fn add_row_for<F>(&mut self, needs: &[f32], gap: f32, h: f32, mut f: F)
6286     where
6287         F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
6288     {
6289         let max_h = self.context.grid.max_height().max(self.context.content_y);
6290         for col_h in &mut self.context.grid.col_heights {
6291             *col_h = max_h;
6292         }
6293         self.context.content_y = max_h;
6294 
6295         let cols = self.context.row_layout_for(needs, gap);
6296         for (i, &(x, w)) in cols.iter().enumerate() {
6297             self.context.content_y = max_h;
6298             f(self.context, i, x, w);
6299         }
6300 
6301         let new_bottom = max_h + h;
6302         self.context.content_y = new_bottom;
6303         for col_h in &mut self.context.grid.col_heights {
6304             *col_h = new_bottom;
6305         }
6306         self.context.spacing(self.spacing);
6307     }
6308 
6309     pub fn add_row<F>(&mut self, count: usize, gap: f32, h: f32, mut f: F)
6310     where
6311         F: FnMut(&mut SectionContext<'a, P>, usize, f32, f32),
6312     {
6313         let max_h = self.context.grid.max_height().max(self.context.content_y);
6314         for col_h in &mut self.context.grid.col_heights {
6315             *col_h = max_h;
6316         }
6317         self.context.content_y = max_h;
6318 
6319         let cols = self.context.row_layout(count, gap);
6320         for (i, &(x, w)) in cols.iter().enumerate() {
6321             self.context.content_y = max_h;
6322             f(self.context, i, x, w);
6323         }
6324 
6325         let new_bottom = max_h + h;
6326         self.context.content_y = new_bottom;
6327         for col_h in &mut self.context.grid.col_heights {
6328             *col_h = new_bottom;
6329         }
6330         self.context.spacing(self.spacing);
6331     }
6332 }
6333 
6334 pub fn get_system_monospace_font() -> &'static str {
6335     static MONOSPACE_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
6336     MONOSPACE_FONT.get_or_init(|| {
6337         if let Ok(output) = std::process::Command::new("fc-match")
6338             .args(["-f", "%{family}", "monospace"])
6339             .output()
6340         {
6341             let name = String::from_utf8_lossy(&output.stdout);
6342             let parsed = name.split(',').next().unwrap_or("monospace").trim();
6343             if !parsed.is_empty() {
6344                 return parsed.to_string();
6345             }
6346         }
6347         "monospace".to_string()
6348     })
6349 }
6350 
6351 pub fn get_system_sans_serif_font() -> &'static str {
6352     static SANS_SERIF_FONT: std::sync::OnceLock<String> = std::sync::OnceLock::new();
6353     SANS_SERIF_FONT.get_or_init(|| {
6354         if let Ok(output) = std::process::Command::new("fc-match")
6355             .args(["-f", "%{family}", "sans-serif"])
6356             .output()
6357         {
6358             let name = String::from_utf8_lossy(&output.stdout);
6359             let parsed = name.split(',').next().unwrap_or("sans-serif").trim();
6360             if !parsed.is_empty() {
6361                 return parsed.to_string();
6362             }
6363         }
6364         "sans-serif".to_string()
6365     })
6366 }
6367 
6368 /// The DE's font families, from the shared config's `fonts { }` block:
6369 /// `(sans_serif, serif, monospace, terminal)`.
6370 ///
6371 /// These used to live in `~/.config/fontconfig/fonts.conf`, read back out of
6372 /// fontconfig's XML by alias. Three of the seven aliases it carried
6373 /// (`window-borders`, `status-interface`, `fuzzel`) were cce inventions
6374 /// squatting in fontconfig's family namespace, and by the end none of them was
6375 /// read by anything: window borders lost their text when titlebars went away,
6376 /// the status bar moved to `module { font }` / `/style/status/font` in KDL and
6377 /// only ever consulted the alias as a last-resort fallback, and fuzzel was
6378 /// replaced by cce-cloud. The settings app's Fonts page — which edited that
6379 /// file, rewriting it wholesale and preserving only its `<dir>` lines — went
6380 /// with them.
6381 ///
6382 /// fonts.conf is still fontconfig's file and still governs GTK/Electron apps;
6383 /// cce simply no longer reads or writes it. Per-app overrides work here because
6384 /// this reads the merged config, the same way the status bar's font does.
6385 pub fn read_preferred_fonts() -> (String, String, String, String) {
6386     let get = |key: &str, fallback: &str| {
6387         crate::config::get_string(&format!("/fonts/{key}"))
6388             .filter(|s| !s.trim().is_empty())
6389             .unwrap_or_else(|| fallback.to_string())
6390     };
6391     (
6392         get("sans_serif", "Noto Sans"),
6393         get("serif", "Noto Serif"),
6394         get("monospace", "Noto Sans Mono"),
6395         get("terminal", "Noto Sans Mono"),
6396     )
6397 }
6398 
6399 impl crate::widget::ContainerLayout for FlexLayout {
6400     fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
6401         Box::new(self.clone())
6402     }
6403 }
6404 
6405 impl crate::widget::ContainerLayout for ColumnLayout {
6406     fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
6407         Box::new(self.clone())
6408     }
6409 }
6410 
6411 impl crate::widget::ContainerLayout for AdaptiveGrid {
6412     fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
6413         Box::new(self.clone())
6414     }
6415 }
6416 
6417 impl crate::widget::ContainerLayout for RadialLayout {
6418     fn box_clone_container(&self) -> Box<dyn crate::widget::ContainerLayout> {
6419         Box::new(self.clone())
6420     }
6421 }
6422 
6423 #[cfg(test)]
6424 mod tests {
6425     #[test]
6426     fn unit_slots_resolve_through_the_metric_at_read_time() {
6427         use crate::units::{Len, Metric, MetricSource};
6428         let mut reg = super::StyleRegistry::new();
6429         reg.set_len("probe_width", Len::mm(2.0));
6430         // `get_float` resolves against the PROCESS metric at read time — the
6431         // same number `Len::to_px` gives — so it tracks whatever the metric
6432         // is now, not what it was at load. (The metric itself is left alone:
6433         // it is process-global and the suite runs in parallel.)
6434         let live = reg.get_float("probe_width").unwrap();
6435         assert!((live - Len::mm(2.0).to_px()).abs() < 1e-4, "{live}");
6436         // Two metrics give two answers for the one stored length.
6437         let assumed = Metric::assumed(1.0);
6438         let panel = Metric::from_sizes(2.0, (1920.0, 1200.0), (344.0, 215.0), MetricSource::Measured).unwrap();
6439         let (a, b) = (Len::mm(2.0).resolve(&assumed), Len::mm(2.0).resolve(&panel));
6440         assert!((a - 2.0 * 96.0 / 25.4).abs() < 1e-3, "{a}");
6441         assert!((b - 2.0 * panel.px_per_mm).abs() < 1e-3, "{b}");
6442         assert_ne!(a, b);
6443         assert_eq!(reg.get_len("probe_width"), Some(Len::mm(2.0)));
6444         // A plain number written later wins, and reads back as px.
6445         reg.set_float("probe_width", 7.0);
6446         assert_eq!(reg.get_float("probe_width"), Some(7.0));
6447         assert_eq!(reg.get_len("probe_width"), Some(Len::px(7.0)));
6448     }
6449 
6450     use super::*;
6451 
6452     struct MockRenderTarget {
6453         rects: Vec<([f32; 4], f32, f32, f32, f32)>,
6454     }
6455 
6456     impl RenderTarget for MockRenderTarget {
6457         fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
6458             self.rects.push((color, x, y, w, h));
6459         }
6460         fn text(&mut self, _content: &str, _x: f32, _y: f32, _size: f32, _color: [f32; 4]) {}
6461     }
6462 
6463     #[derive(Default)]
6464     struct ProbeTarget {
6465         rects: Vec<([f32; 4], f32, f32, f32, f32)>,
6466         bounded: Vec<(String, f32, f32, Option<[f32; 4]>)>,
6467     }
6468 
6469     impl RenderTarget for ProbeTarget {
6470         fn rect(&mut self, color: [f32; 4], x: f32, y: f32, w: f32, h: f32) {
6471             self.rects.push((color, x, y, w, h));
6472         }
6473         fn text(&mut self, content: &str, x: f32, y: f32, _size: f32, _color: [f32; 4]) {
6474             self.bounded.push((content.to_string(), x, y, None));
6475         }
6476         fn text_with_bounds(&mut self, content: &str, x: f32, y: f32, _size: f32, _color: [f32; 4], bounds: Option<[f32; 4]>) {
6477             self.bounded.push((content.to_string(), x, y, bounds));
6478         }
6479     }
6480 
6481     /// Everything a section places horizontally — text, button rows, widgets,
6482     /// the column grid — must share ONE inset, or content does not line up
6483     /// with the content beside it. Text used to sit `padding()` to the left of
6484     /// every row in the same section.
6485     #[test]
6486     fn section_text_and_rows_share_one_inset() {
6487         let mut pc = ProbeTarget::default();
6488         let (left, cw) = (100.0f32, 320.0f32);
6489         let mut sec: SectionContext<'_, ProbeTarget> =
6490             SectionContext::new(&mut pc, left, 50.0, cw, "Probe", false, false);
6491 
6492         let cols = sec.row_layout(2, 8.0);
6493         assert_eq!(sec.ax(12.0), cols[0].0, "text at the conventional 12.0 must start where a row starts");
6494         assert_eq!(sec.ax(12.0), sec.content_left());
6495 
6496         // Symmetric: the right-hand inset from the section border matches the
6497         // left-hand one.
6498         let pad = sec.padding();
6499         let (border_l, border_r) = (left + pad, left + cw - pad);
6500         let row_right = cols[1].0 + cols[1].1;
6501         assert_eq!(cols[0].0 - border_l, border_r - row_right);
6502     }
6503 
6504     /// Even division gives a long label and a short one the same box, so one
6505     /// is clipped while the other floats in slack — the row of Suspend /
6506     /// Hibernate / Reboot / Power Off that prompted this. `row_layout_for`
6507     /// gives each column what it needs and shares the leftover equally.
6508     #[test]
6509     fn a_row_sized_for_content_fits_every_column() {
6510         let mut pc = ProbeTarget::default();
6511         let sec: SectionContext<'_, ProbeTarget> =
6512             SectionContext::new(&mut pc, 100.0, 50.0, 400.0, "Probe", false, false);
6513         let needs = [80.0f32, 30.0, 50.0, 40.0];
6514         let cols = sec.row_layout_for(&needs, 8.0);
6515 
6516         for (i, &(_, w)) in cols.iter().enumerate() {
6517             assert!(w >= needs[i], "column {i} got {w}, less than the {} it needs", needs[i]);
6518         }
6519         // The slack is shared equally, so every column overshoots by the same
6520         // amount — not proportionally, which would starve the short ones.
6521         let slack: Vec<f32> = cols.iter().zip(needs.iter()).map(|(&(_, w), n)| w - n).collect();
6522         for s in &slack {
6523             assert!((s - slack[0]).abs() < 1.0e-3, "slack shared unevenly: {slack:?}");
6524         }
6525         // And the row still ends inside the section.
6526         let (lx, lw) = *cols.last().unwrap();
6527         assert!(lx + lw <= sec.content_left() + sec.content_width() + 1.0e-3);
6528     }
6529 
6530     /// When the labels genuinely do not fit, everyone shrinks by the same
6531     /// factor rather than the last column absorbing the whole shortfall.
6532     #[test]
6533     fn an_overfull_row_shrinks_every_column_together() {
6534         let mut pc = ProbeTarget::default();
6535         let sec: SectionContext<'_, ProbeTarget> =
6536             SectionContext::new(&mut pc, 100.0, 50.0, 200.0, "Probe", false, false);
6537         let needs = [300.0f32, 150.0];
6538         let cols = sec.row_layout_for(&needs, 8.0);
6539         let ratio0 = cols[0].1 / needs[0];
6540         let ratio1 = cols[1].1 / needs[1];
6541         assert!((ratio0 - ratio1).abs() < 1.0e-3, "shrink was not shared: {ratio0} vs {ratio1}");
6542         let (lx, lw) = cols[1];
6543         assert!(lx + lw <= sec.content_left() + sec.content_width() + 1.0e-3, "overfull row escaped the section");
6544     }
6545 
6546     /// `ax` used to add `padding()` only for `x_off >= 12.0`, so asking for one
6547     /// pixel less moved content a whole `padding()` the other way. Callers do
6548     /// pass 11.0 and 13.0 in this tree, and the step put them in different
6549     /// coordinate spaces from each other.
6550     #[test]
6551     fn section_ax_is_continuous() {
6552         let mut pc = ProbeTarget::default();
6553         let mut sec: SectionContext<'_, ProbeTarget> =
6554             SectionContext::new(&mut pc, 100.0, 50.0, 320.0, "Probe", false, false);
6555         for off in [0.0f32, 1.0, 11.0, 11.999, 12.0, 13.0, 24.0] {
6556             assert!(
6557                 (sec.ax(off) - sec.ax(0.0) - off).abs() < 1.0e-3,
6558                 "ax must be a plain translation; it stepped at {off}"
6559             );
6560         }
6561     }
6562 
6563     /// A string wider than its section used to be drawn unbounded, running over
6564     /// the border and out of the window. Every section text now carries bounds
6565     /// no wider than the content box.
6566     #[test]
6567     fn section_text_is_bounded_to_the_content_box() {
6568         let mut pc = ProbeTarget::default();
6569         let (left, cw) = (100.0f32, 320.0f32);
6570         {
6571             let mut sec: SectionContext<'_, ProbeTarget> =
6572                 SectionContext::new(&mut pc, left, 50.0, cw, "Probe", false, false);
6573             let right = sec.content_left() + sec.content_width();
6574             sec.text(
6575                 "NVIDIA Corporation AD104M [GeForce RTX 4080 Max-Q / Mobile] and then some",
6576                 12.0, 0.0, 12.0, [1.0; 4],
6577             );
6578             assert!(right < left + cw, "content box must sit inside the section");
6579         }
6580         // Pick our string out by content: the section's own label is drawn
6581         // through this target too, and it is not content.
6582         let (_, _, _, bounds) = pc
6583             .bounded
6584             .iter()
6585             .find(|(t, ..)| t.starts_with("NVIDIA"))
6586             .cloned()
6587             .expect("the section text must reach the render target");
6588         let b = bounds.expect("section text must be bounded");
6589         // Recompute the expectation from the same inputs the section used.
6590         let mut pc2 = ProbeTarget::default();
6591         let probe: SectionContext<'_, ProbeTarget> =
6592             SectionContext::new(&mut pc2, left, 50.0, cw, "Probe", false, false);
6593         assert_eq!(b[2], probe.content_left() + probe.content_width());
6594         assert!(b[2] <= left + cw - probe.padding(), "bound must not exceed the section border");
6595     }
6596 
6597     struct MockWidget {
6598         base: crate::widget::Widget,
6599         x: f32,
6600         y: f32,
6601         w: f32,
6602         h: f32,
6603     }
6604 
6605     impl WidgetHost for MockWidget {
6606         crate::impl_widget_base!(MockWidget);
6607         fn rect(&self) -> (f32, f32, f32, f32) {
6608             (self.x, self.y, self.w, self.h)
6609         }
6610         fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
6611             self.x = x;
6612             self.y = y;
6613             self.w = w;
6614             self.h = h;
6615         }
6616         fn color(&self) -> [f32; 4] {
6617             [0.0, 0.0, 0.0, 0.0]
6618         }
6619     }
6620 
6621     struct MockWidgetWithLabel {
6622         base: crate::widget::Widget,
6623     }
6624 
6625     impl WidgetHost for MockWidgetWithLabel {
6626         crate::impl_widget_base!(MockWidgetWithLabel);
6627         fn rect(&self) -> (f32, f32, f32, f32) {
6628             let offset = self.label_strip();
6629             (self.base.x, self.base.y - offset, self.base.w, self.base.h + offset)
6630         }
6631         fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
6632             let offset = self.label_strip();
6633             self.base.x = x;
6634             self.base.y = y + offset;
6635             self.base.w = w;
6636             self.base.h = (h - offset).max(0.0);
6637         }
6638         fn color(&self) -> [f32; 4] {
6639             [0.0, 0.0, 0.0, 0.0]
6640         }
6641     }
6642 
6643     /// The vstack flow is checked against the LIVE style — the label margin, the
6644     /// detached-label font and the section padding are process-global and the suite
6645     /// runs in parallel, so pinning them here would be a window every other test
6646     /// could see (a label measured in one font by its own call and in another by the
6647     /// group hull's is exactly the flake this cost us). Every expectation below is
6648     /// derived from the getters instead, so the flow holds under any config.
6649     #[test]
6650     fn test_vstack_flow() {
6651         // `Section` seats its first column one `margin_x` in from its left edge.
6652         let first_col_x = 10.0 + 2.0 * section_padding() + Section::DEFAULT_MARGIN_X;
6653         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
6654         let mut sec = Section::new(&mut mock_pc, 10.0, 20.0, 200.0, "Test Section");
6655         
6656         let start_y = sec.ay();
6657         let mut stack = sec.vstack(&mut mock_pc, 10.0);
6658 
6659         let mut dummy = crate::context::UiContext::new();
6660         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6661         stack.add_widget(&mut w1, 50.0, 30.0, &mut dummy);
6662 
6663         // Standard margin should be applied
6664         assert_eq!(w1.x, first_col_x);
6665         assert_eq!(w1.y, start_y);
6666 
6667         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6668         stack.add_widget(&mut w2, 60.0, 40.0, &mut dummy);
6669 
6670         // Second widget should start after first widget height + vstack spacing
6671         assert_eq!(w2.y, start_y + 30.0 + 10.0);
6672 
6673         let mut base = crate::widget::Widget::new();
6674         base.label = Some("Test Label".to_string());
6675         let mut w3 = MockWidgetWithLabel { base };
6676         stack.add_widget(&mut w3, 70.0, 50.0, &mut dummy);
6677 
6678         let offset = w3.base.label_offset();
6679         
6680         // Third widget has label, so its y should be shifted by offset
6681         assert!(offset > 0.0, "a labelled widget has a strip");
6682         assert_eq!(w3.base.y, start_y + 30.0 + 10.0 + 40.0 + 10.0 + offset);
6683     }
6684 
6685     #[test]
6686     fn test_grid_layout() {
6687         // Test single column layout (width = 200, min_col_width = 300)
6688         let grid1 = Grid::new(10.0, 20.0, 200.0, 300.0, 10.0, 1);
6689         assert_eq!(grid1.col_heights.len(), 1);
6690         assert_eq!(grid1.col_lefts[0], 10.0);
6691         assert_eq!(grid1.col_width, 200.0);
6692 
6693         // Test multi column layout (width = 700, min_col_width = 300, gap = 20)
6694         // count = floor((700 + 20) / (300 + 20)) = floor(720 / 320) = 2.
6695         // total_gap = 20 * 1 = 20.
6696         // col_width = (700 - 20) / 2 = 340.
6697         let mut grid2 = Grid::new(5.0, 15.0, 700.0, 300.0, 20.0, 2);
6698         assert_eq!(grid2.col_heights.len(), 2);
6699         assert_eq!(grid2.col_lefts[0], 5.0);
6700         assert_eq!(grid2.col_lefts[1], 365.0);
6701         assert_eq!(grid2.col_width, 340.0);
6702 
6703         assert_eq!(grid2.next_column(), 0);
6704         grid2.col_heights[0] += 50.0; // Column 0 height becomes 65.0
6705         assert_eq!(grid2.next_column(), 1);
6706         grid2.col_heights[1] += 30.0; // Column 1 height becomes 45.0
6707         assert_eq!(grid2.next_column(), 1);
6708         grid2.col_heights[1] += 30.0; // Column 1 height becomes 75.0
6709         assert_eq!(grid2.next_column(), 0);
6710         
6711         assert_eq!(grid2.max_height(), 75.0);
6712     }
6713 
6714     #[test]
6715     fn test_subsection() {
6716         let mut pc = PopoverCollector::new();
6717         let mut subsec = Section::new_opt(&mut pc, 10.0, 20.0, 300.0, "Test Subsec", true);
6718         assert_eq!(subsec.left, 10.0);
6719         assert_eq!(subsec.top, 20.0);
6720         assert_eq!(subsec.cw, 300.0);
6721         assert!(subsec.is_child);
6722         
6723         let mut dummy = crate::context::UiContext::new();
6724         let mut w = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6725         subsec.widget(&mut pc, &mut w, 12.0, 100.0, 40.0, &mut dummy);
6726         
6727         let bottom = subsec.finish(&mut pc);
6728         assert!(bottom > 20.0);
6729     }
6730 
6731     #[test]
6732     fn test_radial_layout() {
6733         let radial = Radial::new(100.0, 100.0, 1.5, 50.0);
6734         
6735         // Check first widget is centered at (100.0, 100.0)
6736         let rect0 = radial.widget_rect(0, 40.0, 30.0);
6737         assert_eq!(rect0, (100.0 - 20.0, 100.0 - 15.0, 40.0, 30.0));
6738         
6739         // Check Ring 1 (idx = 1) vs Ring 2 (idx = 7)
6740         let rect1 = radial.widget_rect(1, 40.0, 30.0);
6741         let rect7 = radial.widget_rect(7, 40.0, 30.0);
6742         
6743         let c1_x = rect1.0 + rect1.2 / 2.0;
6744         let c1_y = rect1.1 + rect1.3 / 2.0;
6745         let c7_x = rect7.0 + rect7.2 / 2.0;
6746         let c7_y = rect7.1 + rect7.3 / 2.0;
6747         
6748         let d1 = ((c1_x - 100.0).powi(2) + (c1_y - 100.0).powi(2)).sqrt();
6749         let d7 = ((c7_x - 100.0).powi(2) + (c7_y - 100.0).powi(2)).sqrt();
6750         
6751         // Ring 2 should be further out than Ring 1
6752         assert!(d7 > d1);
6753         assert!(d1 > 0.0);
6754     }
6755 
6756     // The `Once`-initialised style getters below are checked against the
6757     // statics their config pass fills, under whatever the live config says.
6758     // None of them WRITES: these are process globals and the suite runs in
6759     // parallel, so a set/restore window is visible to every other test — and
6760     // a "restore" that writes a hardcoded literal (as these did) clobbers a
6761     // non-default config permanently. What a getter here can actually get
6762     // wrong is which static it reads, and that is what these pin.
6763 
6764     #[test]
6765     fn test_nested_section_label_alignment() {
6766         let align = nested_section_label_alignment();
6767         assert_eq!(align, *super::NESTED_SECTION_LABEL_ALIGNMENT.read().unwrap());
6768 
6769         let offset = nested_section_label_offset();
6770         assert_eq!(offset, *super::NESTED_SECTION_LABEL_OFFSET.read().unwrap());
6771         assert!(offset.is_finite(), "label offset {offset}");
6772     }
6773 
6774     #[test]
6775     fn test_dropdown_height() {
6776         let h = dropdown_height();
6777         assert_eq!(h, *super::DROPDOWN_HEIGHT.read().unwrap());
6778         assert!(h.is_finite() && h > 0.0, "dropdown height {h}");
6779     }
6780 
6781     #[test]
6782     fn test_column_gap() {
6783         // A legacy key: set (by config or setter) it is honoured; unset it
6784         // lands on the ladder — the root plate's gap.
6785         let gap = column_gap();
6786         match (registry_float("column_gap"), *super::COLUMN_GAP.read().unwrap()) {
6787             (Some(v), _) | (None, Some(v)) => assert_eq!(gap, v),
6788             (None, None) => assert_eq!(gap, root_plate_gap()),
6789         }
6790         assert!(gap.is_finite() && gap >= 0.0, "column gap {gap}");
6791     }
6792 
6793     #[test]
6794     fn spacing_ladder_falls_back_rung_by_rung() {
6795         // Every rung getter is finite and non-negative, and the inset is the
6796         // roll plus the padding, whatever the config says.
6797         for v in [root_plate_padding(), root_plate_gap(), root_plate_inset(), plate_padding(), plate_gap(), control_gap()] {
6798             assert!(v.is_finite() && v >= 0.0, "{v}");
6799         }
6800         assert_eq!(root_plate_inset(), bevel_width() + root_plate_padding());
6801         if registry_float("plate_gap").is_none() {
6802             assert_eq!(plate_gap(), root_plate_gap());
6803         }
6804         if registry_float("control_gap").is_none() {
6805             assert_eq!(control_gap(), CONTROL_GAP);
6806         }
6807     }
6808 
6809     #[test]
6810     fn test_print_fonts() {
6811         let db = crate::widget::get_font_db();
6812         for face in db.faces() {
6813             println!("FAMILY: {:?}", face.families);
6814         }
6815     }
6816 
6817     #[test]
6818     fn test_section_context_grid() {
6819         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
6820         let mut ctx = SectionContext::new(&mut mock_pc, 10.0, 20.0, 500.0, "Test Section", false, false);
6821         assert_eq!(ctx.left, 10.0);
6822         assert_eq!(ctx.top, 20.0);
6823         assert_eq!(ctx.cw, 500.0);
6824 
6825         let mut ui_ctx = crate::context::UiContext::new();
6826         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6827         ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx);
6828 
6829         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6830         ctx.widget(&mut w2, 12.0, 100.0, 30.0, &mut ui_ctx);
6831 
6832         // Since cw=500, we should have multiple columns!
6833         // The first widget goes into column 0, second into column 1.
6834         assert_ne!(w1.x, w2.x);
6835         
6836         let bottom = ctx.finish();
6837         assert!(bottom > 20.0);
6838     }
6839 
6840     #[test]
6841     fn test_child_section_single_column_controls() {
6842         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
6843         let mut ctx = SectionContext::new(&mut mock_pc, 10.0, 20.0, 500.0, "Test Child Section", false, true);
6844         assert_eq!(ctx.grid.col_heights.len(), 1);
6845         
6846         let mut ui_ctx = crate::context::UiContext::new();
6847         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6848         ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx);
6849 
6850         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6851         ctx.widget(&mut w2, 12.0, 100.0, 30.0, &mut ui_ctx);
6852 
6853         // Since it's a child section, we should have a single column only, so w1.x == w2.x.
6854         assert_eq!(w1.x, w2.x);
6855     }
6856 
6857     #[test]
6858     fn test_parent_section_side_by_side_child_sections() {
6859         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
6860         let mut parent_ctx = SectionContext::new(&mut mock_pc, 10.0, 20.0, 500.0, "Parent Section", false, false);
6861         assert_eq!(parent_ctx.grid.col_heights.len(), 2);
6862 
6863         let mut sub_left_1 = 0.0;
6864         let mut sub_left_2 = 0.0;
6865 
6866         parent_ctx.add_section("Child Section 1", false, |subsec1| {
6867             sub_left_1 = subsec1.left;
6868         });
6869 
6870         parent_ctx.add_section("Child Section 2", false, |subsec2| {
6871             sub_left_2 = subsec2.left;
6872         });
6873 
6874         // The two child sections should be rendered side-by-side in different columns, so sub_left_1 != sub_left_2.
6875         assert_ne!(sub_left_1, sub_left_2);
6876     }
6877 
6878     #[test]
6879     fn test_section_context_spacing_preserves_columns() {
6880         let mut mock_pc = MockRenderTarget { rects: Vec::new() };
6881         let mut ctx = SectionContext::new(&mut mock_pc, 10.0, 20.0, 500.0, "Test Section", false, false);
6882         assert_eq!(ctx.grid.col_heights.len(), 2);
6883 
6884         let mut ui_ctx = crate::context::UiContext::new();
6885         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 0.0, h: 0.0 };
6886         ctx.widget(&mut w1, 12.0, 100.0, 40.0, &mut ui_ctx); // placed in col 0
6887 
6888         let height_col_0_before = ctx.grid.col_heights[0];
6889         let height_col_1_before = ctx.grid.col_heights[1];
6890         assert_ne!(height_col_0_before, height_col_1_before);
6891 
6892         ctx.spacing(12.0);
6893 
6894         let height_col_0_after = ctx.grid.col_heights[0];
6895         let height_col_1_after = ctx.grid.col_heights[1];
6896         assert_eq!(height_col_0_after, height_col_0_before + 12.0);
6897         assert_eq!(height_col_1_after, height_col_1_before);
6898         assert_ne!(height_col_0_after, height_col_1_after);
6899     }
6900 
6901     #[test]
6902     fn test_spinbox_button_padding_config() {
6903         let padding = spinbox_button_padding();
6904         println!("Parsed spinbox button padding: {}", padding);
6905         assert!(padding >= 0.0);
6906     }
6907 
6908     /// The control rung's key flattens to `control_corner_radius`, beside a
6909     /// widget's own override — the config shape `control corner_radius=8 { button corner_radius=10 }`.
6910     #[test]
6911     fn control_rung_radius_flattens_beside_widget_overrides() {
6912         let val: serde_json::Value = serde_json::json!({
6913             "style": { "control": { "corner_radius": 8, "button": { "corner_radius": 10 } } }
6914         });
6915         let mut flat = String::new();
6916         flatten_json_to_flat_props(&val, "", &mut flat);
6917         assert!(flat.lines().any(|l| l.starts_with("control_corner_radius")), "{flat}");
6918         assert!(flat.lines().any(|l| l.starts_with("button_corner_radius")), "{flat}");
6919     }
6920 
6921     /// A registry-backed style pinned by one test is invisible to a test
6922     /// beside it.
6923     ///
6924     /// The graph-style test below used to pin ~20 of these and restore none
6925     /// (it now reads the live registry instead: see
6926     /// `graph_style_getters_resolve_their_own_registry_keys`). Before the
6927     /// per-thread overlay that reached every test running alongside —
6928     /// provably: `dual_geometry_views_stay_consistent` bakes
6929     /// quads with `graph_node_corner_radius`, then re-reads the getter to
6930     /// compare, and a write landing between the two made them disagree.
6931     #[test]
6932     fn a_pinned_style_is_private_to_its_thread() {
6933         let base = graph_node_corner_radius();
6934         set_graph_node_corner_radius(base + 17.0);
6935         assert_eq!(graph_node_corner_radius(), base + 17.0, "the pinning thread sees its own value");
6936 
6937         let elsewhere = std::thread::spawn(graph_node_corner_radius).join().unwrap();
6938         assert_eq!(elsewhere, base, "a thread beside it must still see the shared base");
6939     }
6940 
6941     /// Every graph style getter resolves the registry key it is named for,
6942     /// falling back to its own documented default — checked against the live
6943     /// registry as it stands. Nothing is written: the style registry and the
6944     /// colour statics are process-global and the suite runs in parallel, so a
6945     /// set/assert here would be visible to every other test (this one used to
6946     /// set all twenty-one and restore none).
6947     #[test]
6948     fn graph_style_getters_resolve_their_own_registry_keys() {
6949         fn stored(key: &str) -> Option<f32> {
6950             crate::layout::lazy_init_style_registry();
6951             let reg = crate::layout::get_style_registry().read().unwrap();
6952             reg.get_float(key)
6953         }
6954 
6955         assert_eq!(graph_spacing_x(), stored("graph_spacing_x").unwrap_or(187.5));
6956         assert_eq!(graph_spacing_y(), stored("graph_spacing_y").unwrap_or(112.5));
6957         assert_eq!(graph_line_width(), stored("graph_line_width").unwrap_or(1.0));
6958         assert_eq!(graph_node_width(), stored("graph_node_width").unwrap_or(150.0));
6959         assert_eq!(graph_node_height(), stored("graph_node_height").unwrap_or(75.0));
6960         assert_eq!(graph_grid_snap(), stored("graph_grid_snap").unwrap_or(0.0) != 0.0);
6961         assert_eq!(graph_blur(), stored("graph_blur").unwrap_or(0.0));
6962         assert_eq!(graph_node_corner_radius(), stored("graph_node_corner_radius").unwrap_or(4.0));
6963         assert_eq!(graph_wire_size(), stored("graph_wire_size").unwrap_or(6.0));
6964         assert_eq!(
6965             graph_wire_activation_radius(),
6966             stored("graph_wire_activation_radius").unwrap_or(9.0)
6967         );
6968         assert_eq!(graph_connector_size(), stored("graph_connector_size").unwrap_or(8.0));
6969         assert_eq!(
6970             graph_connector_activation_radius(),
6971             stored("graph_connector_activation_radius").unwrap_or(12.0)
6972         );
6973 
6974         // The graph colours live behind statics in `color`, out of this
6975         // module's reach; what is checkable without writing them is that each
6976         // resolves to a real, in-gamut colour rather than an unparsed or
6977         // uninitialised one.
6978         let rgb: [(&str, [f32; 3]); 2] = [
6979             ("graph_cell", crate::color::graph_cell_color()),
6980             ("graph_gap", crate::color::graph_gap_color()),
6981         ];
6982         for (name, c) in rgb {
6983             assert!(c.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{name}: {c:?}");
6984         }
6985         let rgba: [(&str, [f32; 4]); 10] = [
6986             ("graph_node", crate::color::graph_node_color()),
6987             ("graph_node_selected", crate::color::graph_node_selected_color()),
6988             ("graph_node_drag", crate::color::graph_node_drag_color()),
6989             ("node", crate::color::node_color()),
6990             ("node_selected", crate::color::node_selected_color()),
6991             ("node_drag", crate::color::node_drag_color()),
6992             ("graph_wire", crate::color::graph_wire_color()),
6993             ("graph_wire_highlight", crate::color::graph_wire_highlight_color()),
6994             ("graph_connector", crate::color::graph_connector_color()),
6995             ("graph_connector_highlight", crate::color::graph_connector_highlight_color()),
6996         ];
6997         for (name, c) in rgba {
6998             assert!(c.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{name}: {c:?}");
6999         }
7000     }
7001 
7002     #[test]
7003     fn test_mosaic_layout() {
7004         use crate::widget::MosaicLayout;
7005         use crate::layout::LayoutStrategy;
7006         
7007         let layout = MosaicLayout {
7008             gap: 10.0,
7009             padding_x: 5.0,
7010             padding_y: 5.0,
7011         };
7012 
7013         let mut dummy = crate::context::UiContext::new();
7014         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
7015         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
7016         let mut w3 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
7017         
7018         let children = vec![
7019             &mut w1 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7020             &mut w2 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7021             &mut w3 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7022         ];
7023 
7024         let _ = layout.layout(10.0, 20.0, 250.0, 500.0, &children, &mut dummy);
7025 
7026         assert_eq!(w1.x, 15.0);
7027         assert_eq!(w1.y, 25.0);
7028         assert_eq!(w2.x, 15.0);
7029         assert_eq!(w2.y, 85.0);
7030         assert_eq!(w3.x, 125.0);
7031         assert_eq!(w3.y, 25.0);
7032     }
7033 
7034     #[test]
7035     fn test_reverse_mosaic_layout() {
7036         use crate::widget::ReverseMosaicLayout;
7037         use crate::layout::LayoutStrategy;
7038         
7039         let layout = ReverseMosaicLayout {
7040             gap: 10.0,
7041             padding_x: 5.0,
7042             padding_y: 5.0,
7043         };
7044 
7045         let mut dummy = crate::context::UiContext::new();
7046         let mut w1 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 50.0 };
7047         let mut w2 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 100.0, h: 80.0 };
7048         let mut w3 = MockWidget { base: crate::widget::Widget::new(), x: 0.0, y: 0.0, w: 80.0, h: 40.0 };
7049         
7050         let children = vec![
7051             &mut w1 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7052             &mut w2 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7053             &mut w3 as *mut MockWidget as *mut (dyn WidgetHost + 'static),
7054         ];
7055 
7056         let _ = layout.layout(10.0, 20.0, 250.0, 300.0, &children, &mut dummy);
7057 
7058         assert_eq!(w1.x, 15.0);
7059         assert_eq!(w1.y, 25.0);
7060         assert!((w1.w - 126.315).abs() < 0.01);
7061         assert!((w1.h - 103.57).abs() < 0.01);
7062 
7063         assert!((w3.x - 153.947).abs() < 0.01);
7064         assert_eq!(w3.y, 25.0);
7065         assert!((w3.w - 101.05).abs() < 0.01);
7066         assert!((w3.h - 82.857).abs() < 0.01);
7067     }
7068 
7069     /// The LUT is checked on `ramp_profile_lut`, the pure half of
7070     /// `set_bevel_profile_keys` / `set_roll_profile_keys` — installing it
7071     /// would restyle every wall in the process, and the suite runs in
7072     /// parallel.
7073     #[test]
7074     fn bevel_profile_lut_integrates_to_the_curves_net_rise() {
7075         let n = crate::layout::BEVEL_PROFILE_SAMPLES as f32;
7076         let net_rise = |slopes: [f32; crate::layout::BEVEL_PROFILE_SAMPLES]| -> f32 {
7077             slopes.iter().map(|s| s / n).sum()
7078         };
7079 
7080         // The identity 0→1 curve: slopes sum/N to its net rise of 1 (the same
7081         // total step the analytic smoothstep carries).
7082         let slopes = super::ramp_profile_lut(&[(0.0, 0.0), (1.0, 1.0)], true).expect("a curve");
7083         let rise = net_rise(slopes);
7084         assert!((rise - 1.0).abs() < 0.001, "net rise {rise}");
7085 
7086         // A rim curve that returns to its start height nets zero.
7087         let slopes =
7088             super::ramp_profile_lut(&[(0.0, 0.5), (0.2, 1.0), (0.8, 1.0), (1.0, 0.5)], false)
7089                 .expect("a curve");
7090         let rise = net_rise(slopes);
7091         assert!(rise.abs() < 0.001, "net rise {rise}");
7092 
7093         // Degenerate key lists are no curve at all — the installers take that
7094         // `None` as "clear back to the analytic profile".
7095         assert!(super::ramp_profile_lut(&[(0.0, 1.0)], false).is_none());
7096         assert!(super::ramp_profile_lut(&[], true).is_none());
7097     }
7098 
7099     #[test]
7100     fn relief_profile_specs_parse_with_identity_sentinel() {
7101         // Absent and identity-smooth mean "analytic" — nothing to install.
7102         assert!(crate::layout::parse_relief_profile_spec(None).is_none());
7103         assert!(crate::layout::parse_relief_profile_spec(Some(
7104             crate::layout::RELIEF_PROFILE_IDENTITY_SPEC
7105         ))
7106         .is_none());
7107         // Garbage falls back to analytic instead of poisoning the walls.
7108         assert!(crate::layout::parse_relief_profile_spec(Some("not a spec")).is_none());
7109         // A real curve installs: keys and line type round-trip.
7110         let (keys, smooth) = crate::layout::parse_relief_profile_spec(Some(
7111             "linear;0.000:0.200,0.500:1.000,1.000:0.800",
7112         ))
7113         .expect("custom spec parses");
7114         assert!(!smooth);
7115         assert_eq!(keys.len(), 3);
7116         assert!((keys[1].0 - 0.5).abs() < 0.001 && (keys[1].1 - 1.0).abs() < 0.001);
7117         // Identity under a LINEAR line type is a real profile (a straight
7118         // chamfer), not the sentinel — only the smooth spelling is analytic.
7119         assert!(crate::layout::parse_relief_profile_spec(Some(
7120             "linear;0.000:0.000,1.000:1.000"
7121         ))
7122         .is_some());
7123     }
7124 }
7125