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 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_PRESS_MS: u64 = 120;
pub(crate) const MOTION_SELECTION_MS: u64 = 160; pub(crate) const MOTION_SELECTION_MS: u64 = 160;
pub(crate) const MOTION_TOGGLE_MS: u64 = 150;
#[cfg(test)] #[cfg(test)]
pub(crate) const MOTION_FRAME_BUDGET_120HZ: Duration = Duration::from_micros(8_333); pub(crate) const MOTION_FRAME_BUDGET_120HZ: Duration = Duration::from_micros(8_333);
@@ -83,9 +84,37 @@ where
element.into_any_element() 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)] #[cfg(test)]
mod tests { 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] #[test]
fn ease_out_cubic_anchors_at_endpoints() { fn ease_out_cubic_anchors_at_endpoints() {
@@ -108,5 +137,6 @@ mod tests {
assert_eq!(MOTION_FRAME_BUDGET_120HZ.as_micros(), 8_333); 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_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_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 gpui_component::{IconName, scroll::ScrollableElement, slider::Slider};
use crate::shell::ElyShell; 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( pub(crate) fn render_appearance_form(
shell: &mut ElyShell, shell: &mut ElyShell,
@@ -146,7 +146,7 @@ fn render_appearance_rows(
.child(render_theme_mode_row(appearance.theme_mode(), cx)) .child(render_theme_mode_row(appearance.theme_mode(), cx))
.child(render_accent_row()) .child(render_accent_row())
.child(render_translucency_row(shell, appearance.translucency_pct())) .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)) .child(render_reset_row(cx))
.into_any_element() .into_any_element()
} }
@@ -288,12 +288,18 @@ fn swatch(color: u32, selected: bool) -> AnyElement {
element.into_any_element() 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( settings_row(
"Reduce motion", "Reduce motion",
"Mute transitions and ambient effects.", "Mute transitions and ambient effects.",
div() div()
.id(SharedString::from("toggle-reduce-motion")) .id(SharedString::from(id))
.w(px(34.0)) .w(px(34.0))
.h(px(20.0)) .h(px(20.0))
.rounded_full() .rounded_full()
@@ -302,14 +308,16 @@ fn render_reduce_motion_row(reduce: bool, cx: &mut Context<ElyShell>) -> AnyElem
.cursor_pointer() .cursor_pointer()
.hover(|style| style.opacity(0.9)) .hover(|style| style.opacity(0.9))
.active(|style| style.opacity(0.78)) .active(|style| style.opacity(0.78))
.on_click(cx.listener(|shell, _, _, cx| shell.toggle_reduce_motion(cx))) .on_click(cx.listener(move |shell, _, _, cx| {
.child( shell.toggle_reduce_motion(cx);
div() shell.trigger_chrome_motion(id);
.size(px(16.0)) }))
.rounded_full() .child(toggle_thumb_motion(
.bg(rgb(0xffffff)) press_id,
.when(reduce, |el| el.ml(px(14.0))), reduce,
) 14.0,
div().size(px(16.0)).rounded_full().bg(rgb(0xffffff)),
))
.into_any_element(), .into_any_element(),
) )
} }
+17 -1
View File
@@ -1,6 +1,6 @@
use gpui::SharedString; use gpui::SharedString;
use super::ElyShell; use super::{ElyShell, ShellState};
#[derive(Default)] #[derive(Default)]
pub(crate) struct ChromeMotionState { pub(crate) struct ChromeMotionState {
@@ -9,12 +9,28 @@ pub(crate) struct ChromeMotionState {
} }
impl ElyShell { 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>) { 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.target = Some(target.into());
self.chrome_motion.epoch = self.chrome_motion.epoch.wrapping_add(1); self.chrome_motion.epoch = self.chrome_motion.epoch.wrapping_add(1);
} }
pub(crate) fn chrome_motion_animation_id(&self, target: &str) -> Option<SharedString> { pub(crate) fn chrome_motion_animation_id(&self, target: &str) -> Option<SharedString> {
if !self.chrome_motion_enabled() {
return None;
}
self.chrome_motion self.chrome_motion
.target .target
.as_ref() .as_ref()
@@ -6,7 +6,7 @@ use gpui::{
}; };
use crate::shell::ElyShell; 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>( pub(super) fn render_primary_button<F>(
shell: &ElyShell, shell: &ElyShell,
@@ -106,6 +106,7 @@ pub(super) fn render_policy_toggle(
let track_color = if enabled { colors::accent() } else { 0x281e1426 }; let track_color = if enabled { colors::accent() } else { 0x281e1426 };
let id = SharedString::from(format!("sync-policy-{index}")); let id = SharedString::from(format!("sync-policy-{index}"));
let press_id = shell.chrome_motion_animation_id(id.as_str()); 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 selection_id = SharedString::from(format!("{}-selection", id.as_str()));
let element = div() let element = div()
@@ -122,13 +123,12 @@ pub(super) fn render_policy_toggle(
shell.trigger_chrome_motion(id.clone()); shell.trigger_chrome_motion(id.clone());
shell.set_sync_object_policy(kind, next_policy, cx); shell.set_sync_object_policy(kind, next_policy, cx);
})) }))
.child( .child(toggle_thumb_motion(
div() thumb_press_id,
.size(px(16.0)) enabled,
.rounded_full() 14.0,
.bg(rgb(0xffffff)) div().size(px(16.0)).rounded_full().bg(rgb(0xffffff)),
.when(enabled, |this| this.ml(px(14.0))), ));
);
chrome_motion_feedback(press_id, selection_id, enabled, element) chrome_motion_feedback(press_id, selection_id, enabled, element)
} }