feat(app): animate sync toggle motion

This commit is contained in:
2026-05-16 05:27:24 -04:00
parent 0c9bfc2673
commit 973fd0a582
4 changed files with 77 additions and 23 deletions
+32 -2
View File
@@ -1,9 +1,10 @@
use std::time::Duration;
use gpui::{Animation, AnimationExt, AnyElement, ElementId, IntoElement, SharedString, Styled};
use gpui::{Animation, AnimationExt, AnyElement, ElementId, IntoElement, SharedString, Styled, px};
pub(crate) const MOTION_PRESS_MS: u64 = 120;
pub(crate) const MOTION_SELECTION_MS: u64 = 160;
pub(crate) const MOTION_TOGGLE_MS: u64 = 150;
#[cfg(test)]
pub(crate) const MOTION_FRAME_BUDGET_120HZ: Duration = Duration::from_micros(8_333);
@@ -83,9 +84,37 @@ where
element.into_any_element()
}
pub(crate) fn toggle_thumb_motion<E>(
press_id: Option<SharedString>,
checked: bool,
active_offset_px: f32,
element: E,
) -> AnyElement
where
E: IntoElement + Styled + 'static,
{
let end = if checked { active_offset_px } else { 0.0 };
let element = element.ml(px(end));
let Some(press_id) = press_id else {
return element.into_any_element();
};
let start = if checked { 0.0 } else { active_offset_px };
element
.with_animation(
press_id,
Animation::new(Duration::from_millis(MOTION_TOGGLE_MS)).with_easing(ease_out_cubic),
move |element, t| element.ml(px(start + (end - start) * t)),
)
.into_any_element()
}
#[cfg(test)]
mod tests {
use super::{MOTION_FRAME_BUDGET_120HZ, MOTION_PRESS_MS, MOTION_SELECTION_MS, ease_out_cubic};
use super::{
MOTION_FRAME_BUDGET_120HZ, MOTION_PRESS_MS, MOTION_SELECTION_MS, MOTION_TOGGLE_MS,
ease_out_cubic,
};
#[test]
fn ease_out_cubic_anchors_at_endpoints() {
@@ -108,5 +137,6 @@ mod tests {
assert_eq!(MOTION_FRAME_BUDGET_120HZ.as_micros(), 8_333);
assert!(MOTION_PRESS_MS <= 15 * MOTION_FRAME_BUDGET_120HZ.as_millis() as u64);
assert!(MOTION_SELECTION_MS <= 20 * MOTION_FRAME_BUDGET_120HZ.as_millis() as u64);
assert!(MOTION_TOGGLE_MS <= 19 * MOTION_FRAME_BUDGET_120HZ.as_millis() as u64);
}
}
@@ -9,7 +9,7 @@ use gpui::{
use gpui_component::{IconName, scroll::ScrollableElement, slider::Slider};
use crate::shell::ElyShell;
use crate::shell::chrome::SERIF_FAMILY;
use crate::shell::chrome::{SERIF_FAMILY, animations::toggle_thumb_motion};
pub(crate) fn render_appearance_form(
shell: &mut ElyShell,
@@ -146,7 +146,7 @@ fn render_appearance_rows(
.child(render_theme_mode_row(appearance.theme_mode(), cx))
.child(render_accent_row())
.child(render_translucency_row(shell, appearance.translucency_pct()))
.child(render_reduce_motion_row(appearance.reduce_motion(), cx))
.child(render_reduce_motion_row(shell, appearance.reduce_motion(), cx))
.child(render_reset_row(cx))
.into_any_element()
}
@@ -288,12 +288,18 @@ fn swatch(color: u32, selected: bool) -> AnyElement {
element.into_any_element()
}
fn render_reduce_motion_row(reduce: bool, cx: &mut Context<ElyShell>) -> AnyElement {
fn render_reduce_motion_row(
shell: &ElyShell,
reduce: bool,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let id = "toggle-reduce-motion";
let press_id = shell.chrome_motion_animation_id(id);
settings_row(
"Reduce motion",
"Mute transitions and ambient effects.",
div()
.id(SharedString::from("toggle-reduce-motion"))
.id(SharedString::from(id))
.w(px(34.0))
.h(px(20.0))
.rounded_full()
@@ -302,14 +308,16 @@ fn render_reduce_motion_row(reduce: bool, cx: &mut Context<ElyShell>) -> AnyElem
.cursor_pointer()
.hover(|style| style.opacity(0.9))
.active(|style| style.opacity(0.78))
.on_click(cx.listener(|shell, _, _, cx| shell.toggle_reduce_motion(cx)))
.child(
div()
.size(px(16.0))
.rounded_full()
.bg(rgb(0xffffff))
.when(reduce, |el| el.ml(px(14.0))),
)
.on_click(cx.listener(move |shell, _, _, cx| {
shell.toggle_reduce_motion(cx);
shell.trigger_chrome_motion(id);
}))
.child(toggle_thumb_motion(
press_id,
reduce,
14.0,
div().size(px(16.0)).rounded_full().bg(rgb(0xffffff)),
))
.into_any_element(),
)
}
+17 -1
View File
@@ -1,6 +1,6 @@
use gpui::SharedString;
use super::ElyShell;
use super::{ElyShell, ShellState};
#[derive(Default)]
pub(crate) struct ChromeMotionState {
@@ -9,12 +9,28 @@ pub(crate) struct ChromeMotionState {
}
impl ElyShell {
pub(crate) fn chrome_motion_enabled(&self) -> bool {
match &self.state {
ShellState::Ready(core) => !core.appearance().reduce_motion(),
ShellState::StartupError(_) => true,
}
}
pub(crate) fn trigger_chrome_motion(&mut self, target: impl Into<SharedString>) {
if !self.chrome_motion_enabled() {
self.chrome_motion.target = None;
return;
}
self.chrome_motion.target = Some(target.into());
self.chrome_motion.epoch = self.chrome_motion.epoch.wrapping_add(1);
}
pub(crate) fn chrome_motion_animation_id(&self, target: &str) -> Option<SharedString> {
if !self.chrome_motion_enabled() {
return None;
}
self.chrome_motion
.target
.as_ref()
@@ -6,7 +6,7 @@ use gpui::{
};
use crate::shell::ElyShell;
use crate::shell::chrome::animations::chrome_motion_feedback;
use crate::shell::chrome::animations::{chrome_motion_feedback, toggle_thumb_motion};
pub(super) fn render_primary_button<F>(
shell: &ElyShell,
@@ -106,6 +106,7 @@ pub(super) fn render_policy_toggle(
let track_color = if enabled { colors::accent() } else { 0x281e1426 };
let id = SharedString::from(format!("sync-policy-{index}"));
let press_id = shell.chrome_motion_animation_id(id.as_str());
let thumb_press_id = press_id.clone();
let selection_id = SharedString::from(format!("{}-selection", id.as_str()));
let element = div()
@@ -122,13 +123,12 @@ pub(super) fn render_policy_toggle(
shell.trigger_chrome_motion(id.clone());
shell.set_sync_object_policy(kind, next_policy, cx);
}))
.child(
div()
.size(px(16.0))
.rounded_full()
.bg(rgb(0xffffff))
.when(enabled, |this| this.ml(px(14.0))),
);
.child(toggle_thumb_motion(
thumb_press_id,
enabled,
14.0,
div().size(px(16.0)).rounded_full().bg(rgb(0xffffff)),
));
chrome_motion_feedback(press_id, selection_id, enabled, element)
}