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

commitc951d775cf1af1487481af5c130a28b83f1ef369
parent96e1010480
authorLucas Galante <[email protected]>
date2026-09-10 10:27
test: read the live style instead of pinning it, everywhere left

Five tests set process-global style state and asserted on it. The suite runs
in parallel, so each was a window every other test could see — the same shape
as the vstack/group flake — and three of them "restored" by writing a
hardcoded literal rather than the value they read, so they also flattened a
non-default config for the rest of the run (dropdown height was left at 44.0,
whose real default is DEFAULT_CONTROL_HEIGHT).

Where the assertion was only ever a lock roundtrip, it now derives from the
live getter. The four Once-backed knobs pin the getter to the static its
config pass fills — which is the thing a getter here can actually get wrong,
and the roundtrip never checked. The graph getters resolve through the style
registry, so each is checked against registry.get_float of the key it names,
or its documented default; that pins the getter/key wiring the old
set-then-read could not. Its colour half (twenty-one setters, no restore at
all) asserts each colour resolves finite and in gamut; those statics live in
`color`, out of the module's reach.

Two tests genuinely needed a specific value. The bevel profile LUT is checked
on ramp_profile_lut now — the keys.len() < 2 guard lifted out of
set_bevel_profile_keys and set_roll_profile_keys, which duplicated it, into
the pure half both call. The generation-bump assertion does not survive: it is
only observable by installing. The textbox wrap test states wrap on the widget
via the existing with_line_wrap override, which it should have done from the
start — the box is single-line, so line_wrap_enabled() was false whatever the
global said and the pinning bought it nothing.

15 consecutive full parallel runs are clean. (Verified with 0c4e11f applied on
top and then reverted: test_vstack_flow still pins four globals on this branch,
and its own fix is on claude/gifted-matsumoto-e09ec7.)

Co-Authored-By: Claude Opus 5 <[email protected]>

 src/layout.rs                | 213 +++++++++++++++++++++++--------------------
 src/widget/input/text_box.rs |  28 +++---
 2 files changed, 127 insertions(+), 114 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index b0745d8..637bc98 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2095,14 +2095,24 @@ mod ramp_sampling_tests {
 /// this process restyles on the next frame. Empty or single-key lists clear
 /// back to the analytic profile ([`clear_bevel_profile`]).
 pub fn set_bevel_profile_keys(keys: &[(f32, f32)], smooth: bool) {
-    if keys.len() < 2 {
+    let Some(lut) = ramp_profile_lut(keys, smooth) else {
         clear_bevel_profile();
         return;
-    }
-    *BEVEL_PROFILE.write().unwrap() = Some(ramp_slope_lut(keys, smooth));
+    };
+    *BEVEL_PROFILE.write().unwrap() = Some(lut);
     BEVEL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
 }
 
+/// What a key list installs: its slope LUT, or `None` for a degenerate list
+/// (fewer than two keys is no curve at all) — the caller clears back to the
+/// analytic profile. The pure half of `set_*_profile_keys`.
+fn ramp_profile_lut(keys: &[(f32, f32)], smooth: bool) -> Option<[f32; BEVEL_PROFILE_SAMPLES]> {
+    if keys.len() < 2 {
+        return None;
+    }
+    Some(ramp_slope_lut(keys, smooth))
+}
+
 /// A ramp key list sampled into the shader's slope LUT — slot `i` holds the
 /// curve's slope at `v = (i + 0.5) / N`.
 fn ramp_slope_lut(keys: &[(f32, f32)], smooth: bool) -> [f32; BEVEL_PROFILE_SAMPLES] {
@@ -2122,11 +2132,11 @@ fn ramp_slope_lut(keys: &[(f32, f32)], smooth: bool) -> [f32; BEVEL_PROFILE_SAMP
 /// shader's `roll_slope` samples it in place of the analytic superellipse
 /// quadrant. Empty or single-key lists clear back to the analytic roll.
 pub fn set_roll_profile_keys(keys: &[(f32, f32)], smooth: bool) {
-    if keys.len() < 2 {
+    let Some(lut) = ramp_profile_lut(keys, smooth) else {
         clear_roll_profile();
         return;
-    }
-    *ROLL_PROFILE.write().unwrap() = Some(ramp_slope_lut(keys, smooth));
+    };
+    *ROLL_PROFILE.write().unwrap() = Some(lut);
     ROLL_PROFILE_GEN.fetch_add(1, std::sync::atomic::Ordering::Release);
 }
 
@@ -6693,40 +6703,36 @@ mod tests {
         assert!(d1 > 0.0);
     }
 
+    // The `Once`-initialised style getters below are checked against the
+    // statics their config pass fills, under whatever the live config says.
+    // None of them WRITES: these are process globals and the suite runs in
+    // parallel, so a set/restore window is visible to every other test — and
+    // a "restore" that writes a hardcoded literal (as these did) clobbers a
+    // non-default config permanently. What a getter here can actually get
+    // wrong is which static it reads, and that is what these pin.
+
     #[test]
     fn test_nested_section_label_alignment() {
-        let _ = nested_section_label_alignment();
-        set_nested_section_label_alignment(1);
-        assert_eq!(nested_section_label_alignment(), 1);
-        set_nested_section_label_alignment(2);
-        assert_eq!(nested_section_label_alignment(), 2);
-        set_nested_section_label_alignment(0);
-        assert_eq!(nested_section_label_alignment(), 0);
+        let align = nested_section_label_alignment();
+        assert_eq!(align, *super::NESTED_SECTION_LABEL_ALIGNMENT.read().unwrap());
 
-        let _ = nested_section_label_offset();
-        set_nested_section_label_offset(15.0);
-        assert_eq!(nested_section_label_offset(), 15.0);
-        set_nested_section_label_offset(0.0);
-        assert_eq!(nested_section_label_offset(), 0.0);
+        let offset = nested_section_label_offset();
+        assert_eq!(offset, *super::NESTED_SECTION_LABEL_OFFSET.read().unwrap());
+        assert!(offset.is_finite(), "label offset {offset}");
     }
 
     #[test]
     fn test_dropdown_height() {
-        let _ = dropdown_height();
-        set_dropdown_height(48.0);
-        assert_eq!(dropdown_height(), 48.0);
-        set_dropdown_height(44.0);
-        assert_eq!(dropdown_height(), 44.0);
+        let h = dropdown_height();
+        assert_eq!(h, *super::DROPDOWN_HEIGHT.read().unwrap());
+        assert!(h.is_finite() && h > 0.0, "dropdown height {h}");
     }
 
     #[test]
     fn test_column_gap() {
-        println!("LIST FONT: {:?}", list_font());
-        let _ = column_gap();
-        set_column_gap(24.0);
-        assert_eq!(column_gap(), 24.0);
-        set_column_gap(16.0);
-        assert_eq!(column_gap(), 16.0);
+        let gap = column_gap();
+        assert_eq!(gap, *super::COLUMN_GAP.read().unwrap());
+        assert!(gap.is_finite() && gap >= 0.0, "column gap {gap}");
     }
 
     #[test]
@@ -6844,9 +6850,11 @@ mod tests {
     /// A registry-backed style pinned by one test is invisible to a test
     /// beside it.
     ///
-    /// `test_graph_style_configuration` below pins ~20 of these and restores
-    /// none. Before the per-thread overlay that reached every test running
-    /// alongside — provably: `dual_geometry_views_stay_consistent` bakes
+    /// The graph-style test below used to pin ~20 of these and restore none
+    /// (it now reads the live registry instead: see
+    /// `graph_style_getters_resolve_their_own_registry_keys`). Before the
+    /// per-thread overlay that reached every test running alongside —
+    /// provably: `dual_geometry_views_stay_consistent` bakes
     /// quads with `graph_node_corner_radius`, then re-reads the getter to
     /// compare, and a write landing between the two made them disagree.
     #[test]
@@ -6859,58 +6867,62 @@ mod tests {
         assert_eq!(elsewhere, base, "a thread beside it must still see the shared base");
     }
 
+    /// Every graph style getter resolves the registry key it is named for,
+    /// falling back to its own documented default — checked against the live
+    /// registry as it stands. Nothing is written: the style registry and the
+    /// colour statics are process-global and the suite runs in parallel, so a
+    /// set/assert here would be visible to every other test (this one used to
+    /// set all twenty-one and restore none).
     #[test]
-    fn test_graph_style_configuration() {
-        // Trigger load_colors_once first so it doesn't overwrite values later
-        let _ = crate::color::page_low_color();
-
-        set_graph_spacing_x(200.0);
-        set_graph_spacing_y(100.0);
-        set_graph_grid_snap(true);
-        set_graph_blur(0.8);
-        set_graph_node_corner_radius(8.0);
-        set_graph_wire_size(10.0);
-        set_graph_wire_activation_radius(15.0);
-        set_graph_connector_size(12.0);
-        set_graph_connector_activation_radius(18.0);
-
-        assert_eq!(graph_spacing_x(), 200.0);
-        assert_eq!(graph_spacing_y(), 100.0);
-        assert_eq!(graph_grid_snap(), true);
-        assert_eq!(graph_blur(), 0.8);
-        assert_eq!(graph_node_corner_radius(), 8.0);
-        assert_eq!(graph_wire_size(), 10.0);
-        assert_eq!(graph_wire_activation_radius(), 15.0);
-        assert_eq!(graph_connector_size(), 12.0);
-        assert_eq!(graph_connector_activation_radius(), 18.0);
-
-        crate::color::set_graph_cell_color([0.1, 0.2, 0.3]);
-        crate::color::set_graph_gap_color([0.4, 0.5, 0.6]);
-        crate::color::set_graph_node_color([0.7, 0.8, 0.9, 1.0]);
-        crate::color::set_graph_node_selected_color([0.9, 0.8, 0.7, 1.0]);
-        crate::color::set_graph_node_drag_color([0.5, 0.5, 0.5, 1.0]);
-        crate::color::set_node_color([0.7, 0.8, 0.9, 1.0]);
-        crate::color::set_node_selected_color([0.9, 0.8, 0.7, 1.0]);
-        crate::color::set_node_drag_color([0.5, 0.5, 0.5, 1.0]);
-        crate::color::set_graph_wire_color([0.1, 0.1, 0.1, 1.0]);
-        crate::color::set_graph_wire_highlight_color([0.2, 0.2, 0.2, 1.0]);
-        crate::color::set_graph_connector_color([0.3, 0.3, 0.3, 1.0]);
-        crate::color::set_graph_connector_highlight_color([0.4, 0.4, 0.4, 1.0]);
-
-        let cell_color = crate::color::graph_cell_color();
-        assert_eq!(cell_color, [0.1, 0.2, 0.3]);
-        let gap_color = crate::color::graph_gap_color();
-        assert_eq!(gap_color, [0.4, 0.5, 0.6]);
-        assert_eq!(crate::color::graph_node_color(), [0.7, 0.8, 0.9, 1.0]);
-        assert_eq!(crate::color::graph_node_selected_color(), [0.9, 0.8, 0.7, 1.0]);
-        assert_eq!(crate::color::graph_node_drag_color(), [0.5, 0.5, 0.5, 1.0]);
-        assert_eq!(crate::color::node_color(), [0.7, 0.8, 0.9, 1.0]);
-        assert_eq!(crate::color::node_selected_color(), [0.9, 0.8, 0.7, 1.0]);
-        assert_eq!(crate::color::node_drag_color(), [0.5, 0.5, 0.5, 1.0]);
-        assert_eq!(crate::color::graph_wire_color(), [0.1, 0.1, 0.1, 1.0]);
-        assert_eq!(crate::color::graph_wire_highlight_color(), [0.2, 0.2, 0.2, 1.0]);
-        assert_eq!(crate::color::graph_connector_color(), [0.3, 0.3, 0.3, 1.0]);
-        assert_eq!(crate::color::graph_connector_highlight_color(), [0.4, 0.4, 0.4, 1.0]);
+    fn graph_style_getters_resolve_their_own_registry_keys() {
+        fn stored(key: &str) -> Option<f32> {
+            crate::layout::lazy_init_style_registry();
+            let reg = crate::layout::get_style_registry().read().unwrap();
+            reg.get_float(key)
+        }
+
+        assert_eq!(graph_spacing_x(), stored("graph_spacing_x").unwrap_or(150.0));
+        assert_eq!(graph_spacing_y(), stored("graph_spacing_y").unwrap_or(75.0));
+        assert_eq!(graph_grid_snap(), stored("graph_grid_snap").unwrap_or(0.0) != 0.0);
+        assert_eq!(graph_blur(), stored("graph_blur").unwrap_or(0.0));
+        assert_eq!(graph_node_corner_radius(), stored("graph_node_corner_radius").unwrap_or(4.0));
+        assert_eq!(graph_wire_size(), stored("graph_wire_size").unwrap_or(6.0));
+        assert_eq!(
+            graph_wire_activation_radius(),
+            stored("graph_wire_activation_radius").unwrap_or(9.0)
+        );
+        assert_eq!(graph_connector_size(), stored("graph_connector_size").unwrap_or(8.0));
+        assert_eq!(
+            graph_connector_activation_radius(),
+            stored("graph_connector_activation_radius").unwrap_or(12.0)
+        );
+
+        // The graph colours live behind statics in `color`, out of this
+        // module's reach; what is checkable without writing them is that each
+        // resolves to a real, in-gamut colour rather than an unparsed or
+        // uninitialised one.
+        let rgb: [(&str, [f32; 3]); 2] = [
+            ("graph_cell", crate::color::graph_cell_color()),
+            ("graph_gap", crate::color::graph_gap_color()),
+        ];
+        for (name, c) in rgb {
+            assert!(c.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{name}: {c:?}");
+        }
+        let rgba: [(&str, [f32; 4]); 10] = [
+            ("graph_node", crate::color::graph_node_color()),
+            ("graph_node_selected", crate::color::graph_node_selected_color()),
+            ("graph_node_drag", crate::color::graph_node_drag_color()),
+            ("node", crate::color::node_color()),
+            ("node_selected", crate::color::node_selected_color()),
+            ("node_drag", crate::color::node_drag_color()),
+            ("graph_wire", crate::color::graph_wire_color()),
+            ("graph_wire_highlight", crate::color::graph_wire_highlight_color()),
+            ("graph_connector", crate::color::graph_connector_color()),
+            ("graph_connector_highlight", crate::color::graph_connector_highlight_color()),
+        ];
+        for (name, c) in rgba {
+            assert!(c.iter().all(|v| v.is_finite() && (0.0..=1.0).contains(v)), "{name}: {c:?}");
+        }
     }
 
     #[test]
@@ -6980,31 +6992,34 @@ mod tests {
         assert!((w3.h - 82.857).abs() < 0.01);
     }
 
+    /// The LUT is checked on `ramp_profile_lut`, the pure half of
+    /// `set_bevel_profile_keys` / `set_roll_profile_keys` — installing it
+    /// would restyle every wall in the process, and the suite runs in
+    /// parallel.
     #[test]
     fn bevel_profile_lut_integrates_to_the_curves_net_rise() {
+        let n = crate::layout::BEVEL_PROFILE_SAMPLES as f32;
+        let net_rise = |slopes: [f32; crate::layout::BEVEL_PROFILE_SAMPLES]| -> f32 {
+            slopes.iter().map(|s| s / n).sum()
+        };
+
         // The identity 0→1 curve: slopes sum/N to its net rise of 1 (the same
         // total step the analytic smoothstep carries).
-        crate::layout::set_bevel_profile_keys(&[(0.0, 0.0), (1.0, 1.0)], true);
-        let slopes = crate::layout::bevel_profile_slopes().expect("profile installed");
-        let n = crate::layout::BEVEL_PROFILE_SAMPLES as f32;
-        let rise: f32 = slopes.iter().map(|s| s / n).sum();
+        let slopes = super::ramp_profile_lut(&[(0.0, 0.0), (1.0, 1.0)], true).expect("a curve");
+        let rise = net_rise(slopes);
         assert!((rise - 1.0).abs() < 0.001, "net rise {rise}");
 
         // A rim curve that returns to its start height nets zero.
-        crate::layout::set_bevel_profile_keys(
-            &[(0.0, 0.5), (0.2, 1.0), (0.8, 1.0), (1.0, 0.5)],
-            false,
-        );
-        let slopes = crate::layout::bevel_profile_slopes().unwrap();
-        let rise: f32 = slopes.iter().map(|s| s / n).sum();
+        let slopes =
+            super::ramp_profile_lut(&[(0.0, 0.5), (0.2, 1.0), (0.8, 1.0), (1.0, 0.5)], false)
+                .expect("a curve");
+        let rise = net_rise(slopes);
         assert!(rise.abs() < 0.001, "net rise {rise}");
 
-        // Degenerate key lists clear back to the analytic profile; the
-        // generation moves on every change so renderers re-upload.
-        let gen = crate::layout::bevel_profile_generation();
-        crate::layout::set_bevel_profile_keys(&[(0.0, 1.0)], false);
-        assert!(crate::layout::bevel_profile_slopes().is_none());
-        assert!(crate::layout::bevel_profile_generation() > gen);
+        // Degenerate key lists are no curve at all — the installers take that
+        // `None` as "clear back to the analytic profile".
+        assert!(super::ramp_profile_lut(&[(0.0, 1.0)], false).is_none());
+        assert!(super::ramp_profile_lut(&[], true).is_none());
     }
 
     #[test]
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 4acd8d7..78c6115 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -2233,10 +2233,15 @@ mod tests {
     #[test]
     fn test_textbox_line_wrap_disabled_horizontal_scrolling() {
         let _dummy = crate::context::UiContext::new();
-        crate::layout::set_textbox_line_wrap(false);
 
-        let mut tb = TextBox::new("Very long text that should not wrap and instead scroll horizontally".to_string());
+        // Wrap is stated on the widget (`line_wrap_override`), not on the
+        // process-global `textbox_line_wrap` — this box is single-line, which
+        // is already unwrapped, and the override says so whatever the config
+        // does. The global would be visible to every other test in parallel.
+        let mut tb = TextBox::new("Very long text that should not wrap and instead scroll horizontally".to_string())
+            .with_line_wrap(false);
         tb.set_rect(10.0, 10.0, 100.0, 30.0);
+        assert!(!tb.line_wrap_enabled());
 
         assert_eq!(tb.scroll_x, 0.0);
 
@@ -2245,8 +2250,6 @@ mod tests {
         tb.scroll_to_cursor();
 
         assert!(tb.scroll_x > 0.0, "scroll_x should be scrolled horizontally to keep the cursor visible");
-
-        crate::layout::set_textbox_line_wrap(true);
     }
 
     #[test]
@@ -2270,23 +2273,18 @@ mod tests {
         assert_eq!(tb.edit_buffer, "");
     }
 
+    /// A single-line box's border is always the hairline; a multiline box's
+    /// is whatever `textbox_multiline_border_width` resolves to. Read, never
+    /// written: that width is a process global and the suite runs in
+    /// parallel, so setting it here would widen every other test's borders.
     #[test]
     fn test_multiline_textbox_border_width() {
         let _dummy = crate::context::UiContext::new();
-        crate::layout::set_textbox_multiline_border_width(1.0);
         let tb_single = TextBox::new("Singleline".to_string()).with_multiline(false);
         let tb_multi = TextBox::new("Multiline".to_string()).with_multiline(true);
 
-        // Default border width
+        let configured = crate::layout::textbox_multiline_border_width();
         assert_eq!(tb_single.border_width(), 1.0);
-        assert_eq!(tb_multi.border_width(), 1.0);
-
-        // Configure custom border width
-        crate::layout::set_textbox_multiline_border_width(4.5);
-        assert_eq!(tb_single.border_width(), 1.0);
-        assert_eq!(tb_multi.border_width(), 4.5);
-
-        // Reset to default
-        crate::layout::set_textbox_multiline_border_width(1.0);
+        assert_eq!(tb_multi.border_width(), configured);
     }
 }