feat(chrome): animate high-frequency actions
This commit is contained in:
@@ -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<E>(
|
||||
press_id: Option<SharedString>,
|
||||
selection_id: impl Into<ElementId>,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Self>) -> 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<F>(
|
||||
close_id: SharedString,
|
||||
group_name: SharedString,
|
||||
press_id: Option<SharedString>,
|
||||
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
|
||||
|
||||
@@ -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<F>(
|
||||
shell: &ElyShell,
|
||||
id: &'static str,
|
||||
icon: IconName,
|
||||
active: bool,
|
||||
@@ -159,7 +176,8 @@ where
|
||||
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + '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<F>(
|
||||
shell: &ElyShell,
|
||||
id: &'static str,
|
||||
icon: IconName,
|
||||
enabled: bool,
|
||||
@@ -186,7 +209,8 @@ where
|
||||
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + '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<F>(
|
||||
shell: &ElyShell,
|
||||
id: &'static str,
|
||||
icon: IconName,
|
||||
cx: &mut Context<ElyShell>,
|
||||
@@ -213,7 +242,8 @@ fn render_topbar_action<F>(
|
||||
where
|
||||
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + '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 {
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
use gpui::SharedString;
|
||||
|
||||
use super::ElyShell;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct ChromeMotionState {
|
||||
target: Option<SharedString>,
|
||||
epoch: u64,
|
||||
}
|
||||
|
||||
impl ElyShell {
|
||||
pub(crate) fn trigger_chrome_motion(&mut self, target: impl Into<SharedString>) {
|
||||
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> {
|
||||
self.chrome_motion
|
||||
.target
|
||||
.as_ref()
|
||||
.filter(|current| current.as_str() == target)
|
||||
.map(|_| SharedString::from(format!("{target}-motion-{}", self.chrome_motion.epoch)))
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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<Self>,
|
||||
) -> 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| {
|
||||
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);
|
||||
}))
|
||||
.into_any_element()
|
||||
}),
|
||||
);
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user