diff --git a/crates/ely_app/src/shell/chrome/animations.rs b/crates/ely_app/src/shell/chrome/animations.rs index 4a13617..50c12e2 100644 --- a/crates/ely_app/src/shell/chrome/animations.rs +++ b/crates/ely_app/src/shell/chrome/animations.rs @@ -1,6 +1,11 @@ use std::time::Duration; -use gpui::{Animation, AnimationExt, ElementId, IntoElement, Styled}; +use gpui::{Animation, AnimationExt, AnyElement, ElementId, IntoElement, SharedString, Styled}; + +pub(crate) const MOTION_PRESS_MS: u64 = 120; +pub(crate) const MOTION_SELECTION_MS: u64 = 160; +#[cfg(test)] +pub(crate) const MOTION_FRAME_BUDGET_120HZ: Duration = Duration::from_micros(8_333); /// One-second blink that toggles between fully visible and invisible at the /// half-period mark, matching the design's @@ -45,9 +50,42 @@ where element.with_animation(id, animation, |element, t| element.opacity(ease_out_cubic(t))) } +pub(crate) fn chrome_motion_feedback( + press_id: Option, + selection_id: impl Into, + selected: bool, + element: E, +) -> AnyElement +where + E: IntoElement + Styled + 'static, +{ + if let Some(press_id) = press_id { + return element + .with_animation( + press_id, + Animation::new(Duration::from_millis(MOTION_PRESS_MS)).with_easing(ease_out_cubic), + |element, t| element.opacity(0.84 + 0.16 * t), + ) + .into_any_element(); + } + + if selected { + return element + .with_animation( + selection_id, + Animation::new(Duration::from_millis(MOTION_SELECTION_MS)) + .with_easing(ease_out_cubic), + |element, t| element.opacity(0.88 + 0.12 * t), + ) + .into_any_element(); + } + + element.into_any_element() +} + #[cfg(test)] mod tests { - use super::ease_out_cubic; + use super::{MOTION_FRAME_BUDGET_120HZ, MOTION_PRESS_MS, MOTION_SELECTION_MS, ease_out_cubic}; #[test] fn ease_out_cubic_anchors_at_endpoints() { @@ -64,4 +102,11 @@ mod tests { assert!(first_half > 0.5, "ease-out spends more time near 1.0 ({first_half})"); assert!(second_half < 0.5); } + + #[test] + fn shell_motion_tokens_fit_120hz_frame_budget() { + 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); + } } diff --git a/crates/ely_app/src/shell/chrome/sidebar.rs b/crates/ely_app/src/shell/chrome/sidebar.rs index 1791220..df0744f 100644 --- a/crates/ely_app/src/shell/chrome/sidebar.rs +++ b/crates/ely_app/src/shell/chrome/sidebar.rs @@ -9,6 +9,7 @@ use gpui::{ use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; use crate::shell::ElyShell; +use crate::shell::chrome::animations::chrome_motion_feedback; use crate::shell::chrome::sidebar_chrome::{ ROW_CLOSE_SIZE, active_nav_bg, active_nav_bg_hover, close_hover_bg, highlight_border, hover_nav_bg, panel_bg, panel_shadow, profile_initial, render_sidebar_resize_handle, @@ -105,8 +106,10 @@ impl ElyShell { // so it uses hover_nav_bg() directly. Keeping it lighter than the // active nav card means the eye still finds the active selection // first when both are visible. - div() - .id(SharedString::from("nav-settings")) + let target = "nav-settings"; + let press_id = self.chrome_motion_animation_id(target); + let element = div() + .id(SharedString::from(target)) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(7.0)) @@ -118,12 +121,14 @@ impl ElyShell { .cursor_pointer() .hover(|style| style.bg(rgba(hover_nav_bg())).text_color(rgb(colors::ink()))) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(|shell, _, window, cx| { + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target); shell.open_internal_tab("ely://settings", window, cx); })) .child(div().text_color(rgb(colors::ink_3())).child(IconName::Settings)) - .child("Settings") - .into_any_element() + .child("Settings"); + + chrome_motion_feedback(press_id, "nav-settings-selection", false, element) } fn render_profile_row( @@ -133,8 +138,10 @@ impl ElyShell { ) -> AnyElement { let profile_name = snapshot.active_profile_name.clone(); - div() - .id(SharedString::from("nav-profile")) + let target = "nav-profile"; + let press_id = self.chrome_motion_animation_id(target); + let element = div() + .id(SharedString::from(target)) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(6.0)) @@ -144,7 +151,8 @@ impl ElyShell { .cursor_pointer() .hover(|style| style.bg(rgba(hover_nav_bg()))) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(|shell, _, window, cx| { + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target); shell.open_internal_tab("ely://settings/profiles", window, cx); })) .child( @@ -183,8 +191,9 @@ impl ElyShell { // promises "this opens a page" instead of the down // chevron that promises "this opens a menu inline". div().text_color(rgb(colors::ink_4())).child(IconName::ChevronRight), - ) - .into_any_element() + ); + + chrome_motion_feedback(press_id, "nav-profile-selection", false, element) } fn render_home_anchor_row( @@ -195,9 +204,11 @@ impl ElyShell { let active_tab = snapshot.tabs.iter().find(|tab| tab.id() == &snapshot.active_tab_id); let active = active_tab.map(|tab| tab.url().as_str() == "ely://new-tab").unwrap_or(false); let palette = nav_row_palette(active); + let target = "nav-home"; + let press_id = self.chrome_motion_animation_id(target); - div() - .id(SharedString::from("nav-home")) + let element = div() + .id(SharedString::from(target)) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(7.0)) @@ -209,7 +220,8 @@ impl ElyShell { .active(|style| style.opacity(0.82)) .bg(rgba(palette.bg)) .when(active, |el| el.shadow(soft_shadow())) - .on_click(cx.listener(|shell, _, window, cx| { + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target); shell.open_internal_tab("ely://new-tab", window, cx); })) .child(div().text_color(rgb(colors::ink_3())).child(IconName::Frame)) @@ -220,8 +232,9 @@ impl ElyShell { .font_weight(FontWeight(500.0)) .text_color(rgb(palette.text)) .child("Home"), - ) - .into_any_element() + ); + + chrome_motion_feedback(press_id, "nav-home-selection", active, element) } fn render_launcher_row( @@ -237,11 +250,17 @@ impl ElyShell { let title = tab.title().to_string(); let initial = title.chars().next().unwrap_or('?').to_string(); let unread = tab.unread_count(); + let row_id = format!("nav-{}", tab.id().as_str()); + let row_motion_target = SharedString::from(row_id.clone()); + let row_press_id = self.chrome_motion_animation_id(&row_id); + let row_selection_id = SharedString::from(format!("{row_id}-selection")); let group_name = SharedString::from(format!("launcher-{}", tab.id().as_str())); let close_id = SharedString::from(format!("launcher-close-{}", tab.id().as_str())); + let close_motion_target = close_id.clone(); + let close_press_id = self.chrome_motion_animation_id(close_id.as_str()); - div() - .id(SharedString::from(format!("nav-{}", tab.id().as_str()))) + let element = div() + .id(SharedString::from(row_id)) .group(group_name.clone()) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) @@ -255,6 +274,7 @@ impl ElyShell { .bg(rgba(palette.bg)) .when(active, |el| el.shadow(soft_shadow())) .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(row_motion_target.clone()); shell.select_tab(&tab_id, window, cx); })) .child(render_glyph_for(host.as_deref(), &initial, 18.0)) @@ -273,17 +293,22 @@ impl ElyShell { .child(render_row_close_button( close_id, group_name, + close_press_id, cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(close_motion_target.clone()); shell.close_tab_by_id(&close_tab_id, window, cx); cx.stop_propagation(); }), - )) - .into_any_element() + )); + + chrome_motion_feedback(row_press_id, row_selection_id, active, element) } fn render_new_tab_row(&mut self, cx: &mut Context) -> AnyElement { - div() - .id(SharedString::from("nav-new-tab")) + let target = "nav-new-tab"; + let press_id = self.chrome_motion_animation_id(target); + let element = div() + .id(SharedString::from(target)) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) .py(px(7.0)) @@ -295,12 +320,14 @@ impl ElyShell { .cursor_pointer() .hover(|style| style.bg(rgba(hover_nav_bg())).text_color(rgb(colors::ink()))) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(|shell, _, window, cx| { + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target); shell.open_new_tab(window, cx); })) .child(div().text_color(rgb(colors::ink_4())).child(IconName::Plus)) - .child("New Tab") - .into_any_element() + .child("New Tab"); + + chrome_motion_feedback(press_id, "nav-new-tab-selection", false, element) } pub(crate) fn render_tab_row( @@ -316,9 +343,15 @@ impl ElyShell { let close_id = SharedString::from(format!("tab-close-{}", tab.id().as_str())); let title = tab.title().to_string(); let initial = title.chars().next().unwrap_or('?').to_string(); + let row_id = tab.id().as_str().to_string(); + let row_motion_target = SharedString::from(row_id.clone()); + let row_press_id = self.chrome_motion_animation_id(&row_id); + let row_selection_id = SharedString::from(format!("tab-selection-{row_id}")); + let close_motion_target = close_id.clone(); + let close_press_id = self.chrome_motion_animation_id(close_id.as_str()); - div() - .id(SharedString::from(tab.id().as_str().to_string())) + let element = div() + .id(SharedString::from(row_id)) .group(group_name.clone()) .rounded(px(spacing::RADIUS_NAV)) .px(px(10.0)) @@ -332,6 +365,7 @@ impl ElyShell { .bg(rgba(palette.bg)) .when(active, |el| el.shadow(soft_shadow())) .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(row_motion_target.clone()); shell.select_tab(&tab_id, window, cx); })) .child(render_tab_favicon(tab, &initial)) @@ -361,12 +395,15 @@ impl ElyShell { .child(render_row_close_button( close_id, group_name, + close_press_id, cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(close_motion_target.clone()); shell.close_tab_by_id(&close_tab_id, window, cx); cx.stop_propagation(); }), - )) - .into_any_element() + )); + + chrome_motion_feedback(row_press_id, row_selection_id, active, element) } } @@ -401,12 +438,14 @@ struct NavRowPalette { fn render_row_close_button( close_id: SharedString, group_name: SharedString, + press_id: Option, on_click: F, -) -> impl IntoElement +) -> AnyElement where F: Fn(&gpui::ClickEvent, &mut gpui::Window, &mut gpui::App) + 'static, { - div() + let selection_id = SharedString::from(format!("{}-selection", close_id.as_str())); + let element = div() .id(close_id) .size(px(ROW_CLOSE_SIZE)) .rounded_full() @@ -420,7 +459,9 @@ where .hover(|style| style.bg(rgba(close_hover_bg())).text_color(rgb(colors::ink()))) .cursor_pointer() .on_click(on_click) - .child(IconName::Close) + .child(IconName::Close); + + chrome_motion_feedback(press_id, selection_id, false, element) } /// Resolve the favicon glyph for a tab row. Prefers the favicon URL diff --git a/crates/ely_app/src/shell/chrome/topbar.rs b/crates/ely_app/src/shell/chrome/topbar.rs index 2ce55d5..4ba16ed 100644 --- a/crates/ely_app/src/shell/chrome/topbar.rs +++ b/crates/ely_app/src/shell/chrome/topbar.rs @@ -9,6 +9,7 @@ use gpui::{ use gpui_component::{IconName, input::Input}; use crate::shell::ElyShell; +use crate::shell::chrome::animations::chrome_motion_feedback; use crate::shell::sidebar::render_command_bar_identity; pub(crate) fn render_topbar( @@ -30,6 +31,7 @@ pub(crate) fn render_topbar( .border_color(rgba(colors::divider())) .when(sidebar_collapsed, |el| el.child(render_command_bar_identity(snapshot, 56.0, true))) .child(render_nav_arrow( + shell, "nav-back", IconName::ArrowLeft, active_tab.can_navigate_back(), @@ -37,6 +39,7 @@ pub(crate) fn render_topbar( |shell, window, cx| shell.navigate_active_tab_back(window, cx), )) .child(render_nav_arrow( + shell, "nav-forward", IconName::ArrowRight, active_tab.can_navigate_forward(), @@ -44,19 +47,24 @@ pub(crate) fn render_topbar( |shell, window, cx| shell.navigate_active_tab_forward(window, cx), )) .child(render_omnibar(shell, active_tab, window, cx)) - .child(render_topbar_action("share-url", IconName::Copy, cx, |shell, window, cx| { + .child(render_topbar_action(shell, "share-url", IconName::Copy, cx, |shell, window, cx| { shell.copy_active_tab_url(window, cx) })) - .child(render_topbar_action("open-downloads", IconName::Folder, cx, |shell, window, cx| { - shell.open_downloads(window, cx) - })) .child(render_topbar_action( + shell, + "open-downloads", + IconName::Folder, + cx, + |shell, window, cx| shell.open_downloads(window, cx), + )) + .child(render_topbar_action( + shell, "toggle-theme", theme_mode_icon(snapshot.appearance.theme_mode()), cx, |shell, _window, cx| shell.cycle_theme_mode(cx), )) - .child(render_topbar_action("open-menu", IconName::Menu, cx, |shell, window, cx| { + .child(render_topbar_action(shell, "open-menu", IconName::Menu, cx, |shell, window, cx| { shell.open_internal_tab("ely://settings", window, cx) })) .into_any_element() @@ -74,6 +82,8 @@ fn render_omnibar( let command_focused = shell.command_input.read(cx).focus_handle(cx).is_focused(window); let show_styled = !command_focused && active_url != "ely://new-tab"; let secure = active_url.starts_with("https://") || active_url.starts_with("ely://"); + let omnibar_motion_target = "omnibar-content"; + let omnibar_press_id = shell.chrome_motion_animation_id(omnibar_motion_target); div() .flex_1() @@ -86,14 +96,18 @@ fn render_omnibar( .items_center() .gap(px(10.0)) .child(render_lock_or_search(secure, show_styled)) - .child( + .child(chrome_motion_feedback( + omnibar_press_id, + "omnibar-content-selection", + false, div() .id(SharedString::from("omnibar-content")) .flex_1() .min_w_0() .cursor_pointer() .hover(|style| style.opacity(0.92)) - .on_click(cx.listener(|shell, _, window, cx| { + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(omnibar_motion_target); shell.focus_address_bar(window, cx); })) .child(if show_styled { @@ -101,8 +115,9 @@ fn render_omnibar( } else { render_omnibar_input(shell) }), - ) + )) .child(render_omnibar_chip( + shell, "omnibar-filters", IconName::Settings2, false, @@ -110,6 +125,7 @@ fn render_omnibar( |shell, window, cx| shell.focus_address_bar(window, cx), )) .child(render_omnibar_chip( + shell, "omnibar-favorite", favorite_icon, favorite_active, @@ -149,6 +165,7 @@ fn render_lock_or_search(secure: bool, show_styled: bool) -> AnyElement { } fn render_omnibar_chip( + shell: &ElyShell, id: &'static str, icon: IconName, active: bool, @@ -159,7 +176,8 @@ where F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, { let color = if active { colors::accent() } else { colors::ink_4() }; - div() + let press_id = shell.chrome_motion_animation_id(id); + let element = div() .id(SharedString::from(id)) .size(px(22.0)) .rounded(px(6.0)) @@ -170,12 +188,17 @@ where .cursor_pointer() .hover(|style| style.bg(rgba(chip_hover_bg())).text_color(rgb(colors::ink()))) .active(|style| style.opacity(0.7)) - .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) - .child(icon) - .into_any_element() + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(id); + handler(shell, window, cx); + })) + .child(icon); + + chrome_motion_feedback(press_id, SharedString::from(format!("{id}-selection")), active, element) } fn render_nav_arrow( + shell: &ElyShell, id: &'static str, icon: IconName, enabled: bool, @@ -186,7 +209,8 @@ where F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, { let color = if enabled { colors::ink_3() } else { colors::ink_5() }; - div() + let press_id = if enabled { shell.chrome_motion_animation_id(id) } else { None }; + let element = div() .id(SharedString::from(id)) .size(px(30.0)) .rounded(px(8.0)) @@ -198,13 +222,18 @@ where el.cursor_pointer() .hover(|style| style.bg(rgba(omnibar_bg())).text_color(rgb(colors::ink()))) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(id); + handler(shell, window, cx); + })) }) - .child(icon) - .into_any_element() + .child(icon); + + chrome_motion_feedback(press_id, SharedString::from(format!("{id}-selection")), false, element) } fn render_topbar_action( + shell: &ElyShell, id: &'static str, icon: IconName, cx: &mut Context, @@ -213,7 +242,8 @@ fn render_topbar_action( where F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, { - div() + let press_id = shell.chrome_motion_animation_id(id); + let element = div() .id(SharedString::from(id)) .size(px(30.0)) .rounded(px(8.0)) @@ -224,9 +254,13 @@ where .text_color(rgb(colors::ink_3())) .hover(|style| style.bg(rgba(omnibar_bg())).text_color(rgb(colors::ink()))) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) - .child(icon) - .into_any_element() + .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(id); + handler(shell, window, cx); + })) + .child(icon); + + chrome_motion_feedback(press_id, SharedString::from(format!("{id}-selection")), false, element) } fn omnibar_bg() -> u32 { diff --git a/crates/ely_app/src/shell/chrome_motion.rs b/crates/ely_app/src/shell/chrome_motion.rs new file mode 100644 index 0000000..da0d411 --- /dev/null +++ b/crates/ely_app/src/shell/chrome_motion.rs @@ -0,0 +1,24 @@ +use gpui::SharedString; + +use super::ElyShell; + +#[derive(Default)] +pub(crate) struct ChromeMotionState { + target: Option, + epoch: u64, +} + +impl ElyShell { + pub(crate) fn trigger_chrome_motion(&mut self, target: impl Into) { + 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 { + self.chrome_motion + .target + .as_ref() + .filter(|current| current.as_str() == target) + .map(|_| SharedString::from(format!("{target}-motion-{}", self.chrome_motion.epoch))) + } +} diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 368461c..0a08442 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -3,6 +3,7 @@ mod auth; mod bookmark_files; mod bookmarks; pub(crate) mod chrome; +mod chrome_motion; mod command_actions; mod download_targets; mod downloads; @@ -70,6 +71,7 @@ pub struct ElyShell { pub(crate) workspace_picker_open: bool, pub(crate) sidebar_hover_expanded: bool, pub(crate) command_selected_index: usize, + chrome_motion: chrome_motion::ChromeMotionState, /// Live state for sidebar-resize drag. While the user holds the /// resize handle: `(mouse_x_at_drag_start, sidebar_width_px_at_drag_start)`. /// `None` whenever no drag is in flight. @@ -202,6 +204,7 @@ impl ElyShell { workspace_picker_open: false, sidebar_hover_expanded: false, command_selected_index: 0, + chrome_motion: chrome_motion::ChromeMotionState::default(), sidebar_resize_origin: None, download_action_error: None, download_clear_confirmation: false, @@ -239,36 +242,6 @@ impl ElyShell { shell } - /// Inspect the on-disk bearer token (if any) and seed the - /// `SyncConnectionState` so the Sync settings page reads the right - /// label on first render — without any sync setting page open it - /// would otherwise stay `SignedOut` until the user clicks Sync now. - fn probe_initial_sync_state(&mut self) { - let ShellState::Ready(core) = &mut self.state else { - return; - }; - let Some(snapshot) = core.snapshot().ok() else { - return; - }; - let active_profile_id = snapshot.active_profile_id.clone(); - let Some(profile_root) = crate::services::servo_profile_data::default_profile_data_root() - else { - return; - }; - let profile_dir = crate::services::servo_profile_data::profile_data_dir( - &profile_root, - &active_profile_id, - ); - let bearer_path = profile_dir.join("sync").join("bearer.token"); - let bearer_present = std::fs::metadata(&bearer_path).map(|m| m.len() > 0).unwrap_or(false); - let state = if bearer_present { - ely_domain::SyncConnectionState::SignedIn - } else { - ely_domain::SyncConnectionState::SignedOut - }; - core.set_sync_connection_state(state); - } - /// Drain any sync upload outcomes the off-thread worker pushed /// since the previous tick and stamp the resulting connection /// state on `BrowserCore`. Returns `true` when at least one diff --git a/crates/ely_app/src/shell/sidebar.rs b/crates/ely_app/src/shell/sidebar.rs index e8ef734..21992ad 100644 --- a/crates/ely_app/src/shell/sidebar.rs +++ b/crates/ely_app/src/shell/sidebar.rs @@ -5,15 +5,15 @@ use ely_domain::{ HIDDEN_SIDEBAR_WIDTH_PX, Space, }; use gpui::{ - AnyElement, BoxShadow, Context, IntoElement, ParentElement, Styled, Window, div, hsla, point, - px, rgb, rgba, + AnyElement, BoxShadow, Context, IntoElement, ParentElement, SharedString, Styled, Window, div, + hsla, point, px, rgb, rgba, }; use gpui_component::{ IconName, Selectable, Sizable, StyledExt, button::{Button, ButtonVariants}, }; -use super::chrome::panel_bg; +use super::chrome::{animations::chrome_motion_feedback, panel_bg}; use super::{ElyShell, ShellState, render::tab_profile_label}; use crate::ToggleSidebar; @@ -167,16 +167,21 @@ impl ElyShell { cx: &mut Context, ) -> AnyElement { let space_id = space.id().clone(); - Button::new(("compact-space", index)) + let target = SharedString::from(format!("compact-space-{}", space.id().as_str())); + let press_id = self.chrome_motion_animation_id(target.as_str()); + let selection_id = SharedString::from(format!("{}-selection", target.as_str())); + let button = Button::new(("compact-space", index)) .ghost() .small() .selected(active) .label(space.icon().to_string()) .tooltip(space.name().to_string()) .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target.clone()); shell.select_space(&space_id, window, cx); - })) - .into_any_element() + })); + + chrome_motion_feedback(press_id, selection_id, active, div().child(button)) } fn render_compact_tab_button( @@ -190,16 +195,18 @@ impl ElyShell { ) -> AnyElement { let tab_id = tab.id().clone(); let tooltip = format!("{} - {}", tab.title(), tab_profile_label(tab, &snapshot.profiles)); - Button::new(id) - .ghost() - .small() - .selected(active) - .icon(icon) - .tooltip(tooltip) - .on_click(cx.listener(move |shell, _, window, cx| { - shell.select_tab(&tab_id, window, cx); - })) - .into_any_element() + let target = SharedString::from(format!("compact-tab-{}", tab.id().as_str())); + let press_id = self.chrome_motion_animation_id(target.as_str()); + let selection_id = SharedString::from(format!("{}-selection", target.as_str())); + let button = + Button::new(id).ghost().small().selected(active).icon(icon).tooltip(tooltip).on_click( + cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target.clone()); + shell.select_tab(&tab_id, window, cx); + }), + ); + + chrome_motion_feedback(press_id, selection_id, active, div().child(button)) } fn render_compact_archived_button( @@ -210,15 +217,20 @@ impl ElyShell { ) -> AnyElement { let tab = archived_tab.tab(); let tab_id = tab.id().clone(); - Button::new(("compact-archive", index)) + let target = SharedString::from(format!("compact-archive-{}", tab.id().as_str())); + let press_id = self.chrome_motion_animation_id(target.as_str()); + let selection_id = SharedString::from(format!("{}-selection", target.as_str())); + let button = Button::new(("compact-archive", index)) .ghost() .small() .icon(IconName::Undo2) .tooltip(tab.title().to_string()) .on_click(cx.listener(move |shell, _, window, cx| { + shell.trigger_chrome_motion(target.clone()); shell.restore_archived_tab(&tab_id, window, cx); - })) - .into_any_element() + })); + + chrome_motion_feedback(press_id, selection_id, false, div().child(button)) } } diff --git a/crates/ely_app/src/shell/sync_state.rs b/crates/ely_app/src/shell/sync_state.rs index e6452c5..715384b 100644 --- a/crates/ely_app/src/shell/sync_state.rs +++ b/crates/ely_app/src/shell/sync_state.rs @@ -1,3 +1,5 @@ +use super::{ElyShell, ShellState}; + /// Messages the off-thread sync workers push back to the shell so /// `SyncConnectionState` on `BrowserCore` and the in-flight auth /// form reflect live state without the UI thread ever touching the @@ -28,3 +30,33 @@ pub(crate) const fn sync_platform_label() -> &'static str { "other" } } + +impl ElyShell { + /// Inspect the on-disk bearer token and seed `SyncConnectionState` + /// so the Sync settings page reads the startup state on first render. + pub(super) fn probe_initial_sync_state(&mut self) { + let ShellState::Ready(core) = &mut self.state else { + return; + }; + let Some(snapshot) = core.snapshot().ok() else { + return; + }; + let active_profile_id = snapshot.active_profile_id.clone(); + let Some(profile_root) = crate::services::servo_profile_data::default_profile_data_root() + else { + return; + }; + let profile_dir = crate::services::servo_profile_data::profile_data_dir( + &profile_root, + &active_profile_id, + ); + let bearer_path = profile_dir.join("sync").join("bearer.token"); + let bearer_present = std::fs::metadata(&bearer_path).map(|m| m.len() > 0).unwrap_or(false); + let state = if bearer_present { + ely_domain::SyncConnectionState::SignedIn + } else { + ely_domain::SyncConnectionState::SignedOut + }; + core.set_sync_connection_state(state); + } +}