From 5548427df3374613a746a409ddaf41345a6a05a9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 15 May 2026 17:19:38 -0400 Subject: [PATCH] Land 125 Hz Servo polling and eased shell transitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Now that `LiveRuntimeWorker` does all the blocking IPC off the UI thread, the 16 ms shell tick is no longer the bottleneck — drop it to 8 ms (≈ 125 Hz) so a 120 Hz display can present a fresh Servo frame between every refresh. The worker queue still coalesces, so doubling the rate does not double the wire traffic. Replace `fade_in`'s linear ramp with an ease-out cubic so panel / overlay reveals decelerate the way the design tokens promise instead of cutting in abruptly at the end. Unit-tests pin the curve shape so a future refactor that wires in a different easing function won't silently revert to linear. --- crates/ely_app/src/shell/chrome/animations.rs | 36 +++++++++++++++++-- crates/ely_app/src/shell/mod.rs | 8 ++++- 2 files changed, 41 insertions(+), 3 deletions(-) diff --git a/crates/ely_app/src/shell/chrome/animations.rs b/crates/ely_app/src/shell/chrome/animations.rs index 087a4cf..4a13617 100644 --- a/crates/ely_app/src/shell/chrome/animations.rs +++ b/crates/ely_app/src/shell/chrome/animations.rs @@ -21,7 +21,18 @@ where }) } -/// Soft fade-in over `panel_transition_ms` once the element first mounts. +/// Cubic ease-out: `1 - (1 - t)^3`. Decelerates as it approaches `1.0`, +/// matching the productive register's "settles fast" feel used by every +/// reveal / pop / scale animation in the shell. Inlined so callers can +/// shape their `t` value before applying it to opacity or scale without +/// adding a per-frame function-call cost. +pub(crate) fn ease_out_cubic(t: f32) -> f32 { + let inv = (1.0 - t).clamp(0.0, 1.0); + 1.0 - inv * inv * inv +} + +/// Soft fade-in over `duration_ms` once the element first mounts, eased +/// out so the trail end settles to fully opaque without an abrupt cut. /// /// Drives the design's `panel transition` motion token (180 ms productive) /// for surfaces like the command overlay and workspace disclosure that @@ -31,5 +42,26 @@ where E: IntoElement + Styled + 'static, { let animation = Animation::new(Duration::from_millis(duration_ms)); - element.with_animation(id, animation, |element, t| element.opacity(t)) + element.with_animation(id, animation, |element, t| element.opacity(ease_out_cubic(t))) +} + +#[cfg(test)] +mod tests { + use super::ease_out_cubic; + + #[test] + fn ease_out_cubic_anchors_at_endpoints() { + assert!((ease_out_cubic(0.0)).abs() < f32::EPSILON); + assert!((ease_out_cubic(1.0) - 1.0).abs() < f32::EPSILON); + } + + #[test] + fn ease_out_cubic_decelerates() { + // First half should travel less than the second half — that's + // the visual signature of ease-out. + let first_half = ease_out_cubic(0.5); + let second_half = 1.0 - first_half; + assert!(first_half > 0.5, "ease-out spends more time near 1.0 ({first_half})"); + assert!(second_half < 0.5); + } } diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 0c05d1c..1c3876a 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -482,8 +482,14 @@ impl ElyShell { fn start_external_web_surface_timer(cx: &mut Context) { cx.spawn(async move |shell, cx| { + // 8 ms ≈ 125 Hz, matched to a 120 Hz display's frame budget. + // The Servo worker thread does the blocking IPC, so this tick + // only enqueues Poll requests and drains the response channel + // — cheap enough to run twice as often as the previous 60 Hz + // schedule and lets active pages feed the renderer a fresh + // frame between every display refresh. loop { - Timer::after(Duration::from_millis(16)).await; + Timer::after(Duration::from_millis(8)).await; let result = shell.update(cx, |shell, cx| { if shell.tick_external_web_surfaces() { cx.notify();