From 599ed51ce74e0c528289624a726ac81f7c55d270 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 17:59:45 -0400 Subject: [PATCH] Restructure topbar to match design's omnibar + right action cluster Move topbar rendering into chrome::topbar so it does not bloat render.rs. The omnibar now ends in two glass chips (filters + favorite star) inline with the address input, matching the design's pill layout. The toolbar right cluster is rebuilt as copy-url, downloads, theme, and menu icons: - Copy chip writes the active tab's URL to the system clipboard. - Downloads opens ely://downloads (existing internal route). - Theme chip routes to ely://settings/appearance for now (a Light/Dark toggle still has to live in appearance settings before it can be a one-shot). - Menu opens ely://settings. The pin/new-tab buttons are removed; they were not part of the design's topbar. Pinning still works through the favorite chip + sidebar selection state, and Cmd+T continues to create tabs. --- crates/ely_app/src/shell/chrome/mod.rs | 2 + crates/ely_app/src/shell/chrome/topbar.rs | 200 ++++++++++++++++++++++ crates/ely_app/src/shell/navigation.rs | 10 +- crates/ely_app/src/shell/render.rs | 115 +------------ 4 files changed, 218 insertions(+), 109 deletions(-) create mode 100644 crates/ely_app/src/shell/chrome/topbar.rs diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index cb05aca..1e2d938 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -1,5 +1,7 @@ pub(crate) mod sidebar_header; +pub(crate) mod topbar; pub(crate) mod wallpaper; pub(crate) use sidebar_header::render_sidebar_header; +pub(crate) use topbar::render_topbar; pub(crate) use wallpaper::{WallpaperTheme, render_wallpaper}; diff --git a/crates/ely_app/src/shell/chrome/topbar.rs b/crates/ely_app/src/shell/chrome/topbar.rs new file mode 100644 index 0000000..c703d3a --- /dev/null +++ b/crates/ely_app/src/shell/chrome/topbar.rs @@ -0,0 +1,200 @@ +use ely_browser_core::BrowserSnapshot; +use ely_design_system::{colors, spacing}; +use ely_domain::BrowserTab; +use gpui::{ + AnyElement, BoxShadow, Context, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, div, hsla, point, prelude::FluentBuilder, px, rgb, rgba, +}; +use gpui_component::{IconName, input::Input}; + +use crate::shell::ElyShell; +use crate::shell::sidebar::render_command_bar_identity; + +pub(crate) fn render_topbar( + shell: &mut ElyShell, + snapshot: &BrowserSnapshot, + active_tab: &BrowserTab, + sidebar_collapsed: bool, + cx: &mut Context, +) -> AnyElement { + div() + .h(px(spacing::TOPBAR_HEIGHT)) + .px(px(14.0)) + .gap(px(8.0)) + .flex() + .items_center() + .flex_shrink_0() + .border_b_1() + .border_color(rgba(colors::DIVIDER)) + .when(sidebar_collapsed, |el| { + el.child(render_command_bar_identity(snapshot, 56.0, true)) + }) + .child(render_nav_arrow("nav-back", IconName::ChevronLeft)) + .child(render_nav_arrow("nav-forward", IconName::ChevronRight)) + .child(render_omnibar(shell, active_tab, cx)) + .child(render_topbar_action( + "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( + "toggle-theme", + IconName::Moon, + cx, + |shell, window, cx| shell.open_internal_tab("ely://settings/appearance", window, cx), + )) + .child(render_topbar_action( + "open-menu", + IconName::Menu, + cx, + |shell, window, cx| shell.open_internal_tab("ely://settings", window, cx), + )) + .into_any_element() +} + +fn render_omnibar( + shell: &mut ElyShell, + active_tab: &BrowserTab, + cx: &mut Context, +) -> AnyElement { + let favorite_active = active_tab.flags().favorite; + let favorite_icon = if favorite_active { + IconName::Star + } else { + IconName::StarOff + }; + + div() + .flex_1() + .h(px(spacing::OMNIBAR_HEIGHT)) + .rounded(px(spacing::RADIUS_PILL)) + .bg(rgba(OMNIBAR_BG)) + .shadow(soft_shadow()) + .pl(px(14.0)) + .pr(px(8.0)) + .flex() + .items_center() + .gap(px(10.0)) + .child( + div() + .text_color(rgb(colors::INK_3)) + .child(IconName::Search), + ) + .child( + div().flex_1().child( + Input::new(&shell.command_input) + .appearance(false) + .cleanable(true), + ), + ) + .child(render_omnibar_chip( + "omnibar-filters", + IconName::Settings2, + false, + cx, + |shell, window, cx| shell.focus_address_bar(window, cx), + )) + .child(render_omnibar_chip( + "omnibar-favorite", + favorite_icon, + favorite_active, + cx, + |shell, _window, cx| shell.toggle_active_tab_favorite(cx), + )) + .into_any_element() +} + +fn render_omnibar_chip( + id: &'static str, + icon: IconName, + active: bool, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, +{ + let color = if active { colors::ACCENT } else { colors::INK_4 }; + div() + .id(SharedString::from(id)) + .size(px(22.0)) + .rounded(px(6.0)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(color)) + .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() +} + +fn render_nav_arrow(id: &'static str, icon: IconName) -> AnyElement { + div() + .id(SharedString::from(id)) + .size(px(30.0)) + .rounded(px(8.0)) + .flex() + .items_center() + .justify_center() + .cursor_pointer() + .text_color(rgb(colors::INK_3)) + .hover(|style| style.bg(rgba(OMNIBAR_BG)).text_color(rgb(colors::INK))) + .active(|style| style.opacity(0.82)) + .child(icon) + .into_any_element() +} + +fn render_topbar_action( + id: &'static str, + icon: IconName, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, +{ + div() + .id(SharedString::from(id)) + .size(px(30.0)) + .rounded(px(8.0)) + .flex() + .items_center() + .justify_center() + .cursor_pointer() + .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() +} + +const OMNIBAR_BG: u32 = 0xffffff8c; +const CHIP_HOVER_BG: u32 = 0xffffffd9; + +fn soft_shadow() -> Vec { + vec![ + BoxShadow { + color: hsla(0.0, 0.0, 1.0, 0.7), + offset: point(px(0.0), px(1.0)), + blur_radius: px(0.0), + spread_radius: px(0.0), + }, + BoxShadow { + color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08), + offset: point(px(0.0), px(0.0)), + blur_radius: px(0.0), + spread_radius: px(1.0), + }, + ] +} diff --git a/crates/ely_app/src/shell/navigation.rs b/crates/ely_app/src/shell/navigation.rs index 2aba629..651094a 100644 --- a/crates/ely_app/src/shell/navigation.rs +++ b/crates/ely_app/src/shell/navigation.rs @@ -1,5 +1,5 @@ use ely_domain::UrlText; -use gpui::{Context, Window}; +use gpui::{ClipboardItem, Context, Window}; use gpui_component::input::SelectAll; use super::{ElyShell, ShellState}; @@ -64,4 +64,12 @@ impl ElyShell { }); window.dispatch_action(Box::new(SelectAll), cx); } + + pub(super) fn copy_active_tab_url(&mut self, _window: &mut Window, cx: &mut Context) { + if let ShellState::Ready(core) = &self.state + && let Ok(tab) = core.active_tab() + { + cx.write_to_clipboard(ClipboardItem::new_string(tab.url().as_str().to_string())); + } + } } diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index f86941e..eb534e4 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -6,15 +6,13 @@ use gpui::{ SharedString, StatefulInteractiveElement, Styled, Window, div, hsla, linear_gradient, point, linear_color_stop, prelude::FluentBuilder, px, rgb, rgba, }; -use gpui_component::{ - IconName, Selectable, Sizable, StyledExt, - button::{Button, ButtonVariants}, - input::Input, - scroll::ScrollableElement, -}; +use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; -use super::chrome::{WallpaperTheme, render_sidebar_header, render_wallpaper}; -use super::sidebar::{collapsed_sidebar_active, render_command_bar_identity}; +use super::chrome::{ + WallpaperTheme, render_sidebar_header, render_topbar as render_topbar_chrome, + render_wallpaper, +}; +use super::sidebar::collapsed_sidebar_active; use super::{ElyShell, ShellState, archive_labels::archive_detail_label}; impl Render for ElyShell { @@ -103,7 +101,7 @@ impl ElyShell { .bg(rgba(PANEL_BG)) .shadow(panel_shadow()) .overflow_hidden() - .child(self.render_topbar(snapshot, active_tab, sidebar_collapsed, cx)) + .child(render_topbar_chrome(self, snapshot, active_tab, sidebar_collapsed, cx)) .child( div() .flex_1() @@ -113,86 +111,6 @@ impl ElyShell { .into_any_element() } - fn render_topbar( - &mut self, - snapshot: &BrowserSnapshot, - active_tab: &BrowserTab, - sidebar_collapsed: bool, - cx: &mut Context, - ) -> AnyElement { - let favorite_icon = - if active_tab.flags().favorite { IconName::Star } else { IconName::StarOff }; - let favorite_tooltip = - if active_tab.flags().favorite { "Remove Favorite" } else { "Add Favorite" }; - let pinned_tooltip = if active_tab.flags().pinned { "Unpin Tab" } else { "Pin Tab" }; - - div() - .h(px(spacing::TOPBAR_HEIGHT)) - .px(px(14.0)) - .gap(px(8.0)) - .flex() - .items_center() - .flex_shrink_0() - .border_b_1() - .border_color(rgba(colors::DIVIDER)) - .children(if sidebar_collapsed { - Some(render_command_bar_identity(snapshot, 56.0, true)) - } else { - None - }) - .child(render_icon_button("nav-back", IconName::ChevronLeft)) - .child(render_icon_button("nav-forward", IconName::ChevronRight)) - .child( - div() - .flex_1() - .h(px(spacing::OMNIBAR_HEIGHT)) - .rounded(px(spacing::RADIUS_PILL)) - .bg(rgba(OMNIBAR_BG)) - .shadow(soft_shadow()) - .px(px(14.0)) - .flex() - .items_center() - .gap(px(10.0)) - .child(div().text_color(rgb(colors::INK_3)).child(IconName::Search)) - .child( - div() - .flex_1() - .child( - Input::new(&self.command_input) - .appearance(false) - .cleanable(true), - ), - ), - ) - .child( - Button::new("toggle-pinned-tab") - .ghost() - .small() - .selected(active_tab.flags().pinned) - .icon(IconName::Asterisk) - .tooltip(pinned_tooltip) - .on_click(cx.listener(|shell, _, _, cx| shell.toggle_active_tab_pinned(cx))), - ) - .child( - Button::new("toggle-favorite-tab") - .ghost() - .small() - .selected(active_tab.flags().favorite) - .icon(favorite_icon) - .tooltip(favorite_tooltip) - .on_click(cx.listener(|shell, _, _, cx| shell.toggle_active_tab_favorite(cx))), - ) - .child( - Button::new("new-tab") - .ghost() - .small() - .icon(IconName::Plus) - .tooltip("New Tab") - .on_click(cx.listener(|shell, _, window, cx| shell.open_new_tab(window, cx))), - ) - .into_any_element() - } - fn render_sidebar( &mut self, snapshot: &BrowserSnapshot, @@ -507,11 +425,7 @@ impl ElyShell { } } -// rgba(255,255,255,0.55) — omnibar background -const OMNIBAR_BG: u32 = 0xffffff8c; -// rgba(255,255,255,0.85) — active nav item background const ACTIVE_NAV_BG: u32 = 0xffffffd9; -// rgba(255,255,255,0.88) — glass panel background const PANEL_BG: u32 = 0xffffffe0; fn panel_shadow() -> Vec { @@ -548,21 +462,6 @@ fn soft_shadow() -> Vec { ] } -fn render_icon_button(id: &'static str, icon: IconName) -> impl IntoElement { - div() - .id(id) - .size(px(30.0)) - .rounded(px(8.0)) - .flex() - .items_center() - .justify_center() - .cursor_pointer() - .text_color(rgb(colors::INK_3)) - .hover(|style| style.bg(rgba(OMNIBAR_BG)).text_color(rgb(colors::INK))) - .active(|style| style.opacity(0.82)) - .child(icon) -} - fn render_error(message: String) -> AnyElement { div() .size_full()