GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(runner): tick the widget roster once per frame, not twice
The frame loop ticked UiContext's tick_receivers itself after the app's
tick, on the assumption that receivers were wall-clock based and a second
tick only re-reported 'changed'. ScrollMotion (and the slider/ramp glides
before it) integrate dt, so every app that ticks its context in its own
Application::tick — files, fonts, graph, list, mail, data-editor,
system-interface, the demo — ran glides at twice the configured rate. The
runner now reads UiContext::tick_count around the app's tick and only
ticks the roster when the app did not.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
src/backend/window_runner.rs | 16 +++++++++++-----
src/context.rs | 12 ++++++++++++
2 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 217b9fd..08691c5 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -5673,17 +5673,23 @@ fn run_session<'l, A: Application>(
}
let mut rebuild = false;
+ let roster_ticks_before =
+ engine_state.inner.as_mut().unwrap().ui_context_mut().map(|ctx| ctx.tick_count());
engine_state.inner.as_mut().unwrap().tick(dt, &mut rebuild);
if rebuild {
engine_state.redraw = true;
}
// Tick the app's retained UiContext (widget tick_receivers — e.g. an
- // animating Dropdown popover) for apps that expose it. Apps that also
- // tick it themselves are safe to double-tick: receivers' animations are
- // wall-clock-based, so an extra tick only re-reports "changed".
+ // animating Dropdown popover) for apps that expose it — but only when
+ // the app's own tick did not already do so this frame. Receivers
+ // integrate `dt` (scroll glides, slider inertia), so the old
+ // "double-ticking is harmless" assumption ran every glide at twice
+ // its configured rate in apps that tick the context themselves.
if let Some(ctx) = engine_state.inner.as_mut().unwrap().ui_context_mut() {
- if ctx.tick(dt) {
- engine_state.redraw = true;
+ if Some(ctx.tick_count()) == roster_ticks_before {
+ if ctx.tick(dt) {
+ engine_state.redraw = true;
+ }
}
}
diff --git a/src/context.rs b/src/context.rs
index 5731499..0d45670 100644
--- a/src/context.rs
+++ b/src/context.rs
@@ -80,6 +80,11 @@ pub struct UiContext {
pub is_dragging: bool,
pub any_dirty: bool,
pub tick_receivers: Vec<WidgetId>,
+ /// How many times `tick` has run. The runner reads it around the app's
+ /// own `Application::tick` to see whether the app already advanced the
+ /// roster this frame — receivers integrate `dt` (scroll glides, slider
+ /// inertia), so a second tick per frame would run them at double speed.
+ tick_count: u64,
pub spatial_grid: SpatialGrid,
pub last_scroll_time: Option<std::time::Instant>,
pub scroll_initiate_widget_id: Option<WidgetId>,
@@ -106,6 +111,7 @@ impl UiContext {
is_dragging: false,
any_dirty: false,
tick_receivers: Vec::new(),
+ tick_count: 0,
spatial_grid: SpatialGrid::new(100.0),
last_scroll_time: None,
scroll_initiate_widget_id: None,
@@ -441,7 +447,13 @@ impl UiContext {
true
}
+ /// Number of `tick` calls so far (see the field doc).
+ pub fn tick_count(&self) -> u64 {
+ self.tick_count
+ }
+
pub fn tick(&mut self, dt: f32) -> bool {
+ self.tick_count = self.tick_count.wrapping_add(1);
let mut changed = false;
let ids = self.tick_receivers.clone();
for id in ids {