Land 125 Hz Servo polling and eased shell transitions

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.
This commit is contained in:
2026-05-15 17:19:38 -04:00
parent 6bacb3faa8
commit 5548427df3
2 changed files with 41 additions and 3 deletions
+34 -2
View File
@@ -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) /// Drives the design's `panel transition` motion token (180 ms productive)
/// for surfaces like the command overlay and workspace disclosure that /// for surfaces like the command overlay and workspace disclosure that
@@ -31,5 +42,26 @@ where
E: IntoElement + Styled + 'static, E: IntoElement + Styled + 'static,
{ {
let animation = Animation::new(Duration::from_millis(duration_ms)); 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);
}
} }
+7 -1
View File
@@ -482,8 +482,14 @@ impl ElyShell {
fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) { fn start_external_web_surface_timer(cx: &mut Context<ElyShell>) {
cx.spawn(async move |shell, cx| { 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 { loop {
Timer::after(Duration::from_millis(16)).await; Timer::after(Duration::from_millis(8)).await;
let result = shell.update(cx, |shell, cx| { let result = shell.update(cx, |shell, cx| {
if shell.tick_external_web_surfaces() { if shell.tick_external_web_surfaces() {
cx.notify(); cx.notify();