Files
ELY-Browser/crates/ely_app/src/shell/chrome/topbar.rs
T
ZacharyZhang-NY 60400a78d1 Normalize hover/active feedback across chrome surfaces
Brings hover/active opacity feedback to a consistent 0.92/0.82 baseline
across the omnibar content, the home search pill, and the workspace
picker tile + add button. The home search pill and the omnibar styled
URL display previously had no hover state at all, so the click target
felt invisible. The motion design framework wants every interactive
element to acknowledge hover and click; this change closes the gap
without introducing new keyframe animations (GPUI 0.2.2 has no
transition layer to drive them).
2026-05-09 18:43:03 -04:00

269 lines
7.8 KiB
Rust

use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::BrowserTab;
use gpui::{
AnyElement, BoxShadow, Context, FontWeight, 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
};
let input_value = shell.command_input.read(cx).value().to_string();
let active_url = active_tab.url().as_str().to_string();
let show_styled = !input_value.is_empty()
&& input_value == active_url
&& active_url != "ely://new-tab";
let secure = active_url.starts_with("https://") || active_url.starts_with("ely://");
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(render_lock_or_search(secure, show_styled))
.child(
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| {
shell.focus_address_bar(window, cx);
}))
.child(if show_styled {
render_styled_url(active_tab)
} else {
render_omnibar_input(shell)
}),
)
.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_input(shell: &ElyShell) -> AnyElement {
Input::new(&shell.command_input)
.appearance(false)
.cleanable(true)
.into_any_element()
}
fn render_styled_url(active_tab: &BrowserTab) -> AnyElement {
let host = active_tab
.url()
.host()
.map(|host| host.to_string())
.unwrap_or_default();
let url = active_tab.url().as_str();
let path_start = url.find("://").map(|prefix| prefix + 3).unwrap_or(0);
let from_path = &url[path_start..];
let path = match from_path.find('/') {
Some(slash) => from_path[slash..].to_string(),
None => String::new(),
};
div()
.flex()
.items_center()
.gap(px(2.0))
.text_size(px(13.0))
.child(
div()
.font_weight(FontWeight(500.0))
.text_color(rgb(colors::INK))
.child(host),
)
.child(
div()
.min_w_0()
.truncate()
.text_color(rgb(colors::INK_3))
.child(path),
)
.into_any_element()
}
fn render_lock_or_search(secure: bool, show_styled: bool) -> AnyElement {
let icon = if !show_styled {
IconName::Search
} else if secure {
IconName::Search
} else {
IconName::Globe
};
div()
.text_color(rgb(colors::INK_3))
.child(icon)
.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),
},
]
}