GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: ScrollMotion::apply_phase names the phase; force_scroll_settings pins a test's
The scroll phase is a process global the runner publishes, and apply_px
reads it: a test that set it changed every other test's pixel deltas while
the suite ran in parallel. apply_phase takes it as an argument (apply_px is
it with the published one), for a host that reads the phase itself.
force_scroll_settings pins scroll_settings() on the calling thread, as
force_natural_scroll does natural scrolling, so a dependent's test that
hangs on a coast does not read the machine's kinetic_scroll.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 17 +++++++++++++++++
src/widget/scroll_motion.rs | 30 +++++++++++++++++++++++++++++-
2 files changed, 46 insertions(+), 1 deletion(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index b1a76be..772ba20 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1042,6 +1042,23 @@ deliberately left alone: a slider's and a ramp key's drift after a scroll
over them, which changes a VALUE after the hand has stopped.
`the_animations_switch_stops_the_glide_and_not_the_coast` is the test.
+### A host may name the phase; a test may pin the settings (2026-09-30)
+
+The phase a wheel event belongs to (`Finger`, `FingerEnd`, `Wheel`) is a
+process GLOBAL the runner publishes before each dispatch, and
+`ScrollMotion::apply_px` reads it. So does anything a test would set it
+through — which, in a suite running tests in parallel, changes what every
+other test's pixel delta means. `ScrollMotion::apply_phase` takes the phase
+as an argument (`apply_px` is it with the published one), for a host that
+reads the phase itself and hands it on; cce-designer's viewport does, and
+its test drives a flick without touching the global.
+
+`scroll_motion::force_scroll_settings` pins what `scroll_settings()`
+answers on the calling THREAD, over input.kdl and the animations switch
+alike, as `force_natural_scroll` pins natural scrolling: a dependent's test
+binary links cce-ui without `cfg(test)`, so a test whose result hangs on a
+coast would otherwise pass or fail by the machine's `kinetic_scroll`.
+
### The suite's animations switch is its own
`motion::enabled()` reads `/run/cce/animations` in a shipped binary (see
diff --git a/src/widget/scroll_motion.rs b/src/widget/scroll_motion.rs
index 3747b26..52f2292 100644
--- a/src/widget/scroll_motion.rs
+++ b/src/widget/scroll_motion.rs
@@ -123,10 +123,29 @@ static SETTINGS: std::sync::OnceLock<ScrollSettings> = std::sync::OnceLock::new(
/// notch is an animation the toolkit adds, and the switch is for those;
/// the coast is the rest of a gesture the hand made, and `kinetic_scroll`
/// in input.kdl is the setting for it.
+///
+/// A thread's override ([`force_scroll_settings`], for a test) wins over
+/// all of it, animations switch included.
pub fn scroll_settings() -> ScrollSettings {
+ if let Some(forced) = SETTINGS_OVERRIDE.with(|f| f.get()) {
+ return forced;
+ }
with_animations(configured_scroll_settings(), crate::motion::enabled())
}
+thread_local! {
+ static SETTINGS_OVERRIDE: std::cell::Cell<Option<ScrollSettings>> = const { std::cell::Cell::new(None) };
+}
+
+/// Force what [`scroll_settings`] answers on this thread, for a test whose
+/// result hangs on a coast or a glide — which input.kdl and the power
+/// mode's animations switch would otherwise decide on the machine that runs
+/// it. `None` lifts it. Thread-local, as `input::force_natural_scroll` is,
+/// because a suite runs its tests in parallel.
+pub fn force_scroll_settings(settings: Option<ScrollSettings>) {
+ SETTINGS_OVERRIDE.with(|f| f.set(settings));
+}
+
/// `configured` as the animations switch leaves it: the glide follows the
/// switch, the coast does not.
fn with_animations(configured: ScrollSettings, animations: bool) -> ScrollSettings {
@@ -454,8 +473,17 @@ impl ScrollMotion {
/// wheel-notch delta; a pixel delta takes the finger path only while the
/// runner reports a finger gesture, else it is applied instantly.
pub fn apply_px(&mut self, dx: f32, dy: f32, discrete: bool, bx: Bounds, by: Bounds) -> bool {
- let s = scroll_settings();
let phase = if discrete { ScrollPhase::Wheel } else { current_scroll_phase() };
+ self.apply_phase(phase, dx, dy, discrete, bx, by)
+ }
+
+ /// [`Self::apply_px`] in a phase the caller names instead of the
+ /// runner-published one — which is a process global, so a test that set
+ /// it would change what every other test's wheel means while the suite
+ /// runs in parallel. A host that reads the phase itself hands it on
+ /// through this.
+ pub fn apply_phase(&mut self, phase: ScrollPhase, dx: f32, dy: f32, discrete: bool, bx: Bounds, by: Bounds) -> bool {
+ let s = scroll_settings();
match phase {
ScrollPhase::Wheel => {
let mut moved = false;