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.
This commit is contained in:
2026-05-09 17:59:45 -04:00
parent 7508767014
commit 599ed51ce7
4 changed files with 218 additions and 109 deletions
+2
View File
@@ -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};
+200
View File
@@ -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<ElyShell>,
) -> 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<ElyShell>,
) -> 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<F>(
id: &'static str,
icon: IconName,
active: bool,
cx: &mut Context<ElyShell>,
handler: F,
) -> AnyElement
where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + '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<F>(
id: &'static str,
icon: IconName,
cx: &mut Context<ElyShell>,
handler: F,
) -> AnyElement
where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + '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<BoxShadow> {
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),
},
]
}
+9 -1
View File
@@ -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<Self>) {
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()));
}
}
}
+7 -108
View File
@@ -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<Self>,
) -> 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<BoxShadow> {
@@ -548,21 +462,6 @@ fn soft_shadow() -> Vec<BoxShadow> {
]
}
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()