Files
ELY-Browser/crates/ely_app/src/shell/chrome/topbar.rs
T
ZacharyZhang-NY 6b2578c3a8 Make every shell color token theme-aware
Dark mode was persistent in `AppearanceSettings` but never reached any
paint code: every call site in the shell read `colors::INK` etc. as a
`pub const u32`, so toggling `ThemeMode::Dark` mutated state nothing
sampled. Root-cause fix is to invert the contract — the design-system
exports functions that resolve through a thread-local `Mode`, and the
GPUI render impl sets that mode each frame.

What lands:
- `ely_design_system::colors::Mode` + thread-local + `set_mode` /
  `mode` accessors. Every ink shade, glass surface, stroke, divider,
  hairline, canvas, success / error chip now picks the warm-dark
  counterpart when the active mode is `Mode::Dark`.
- `ElyShell::render` resolves `ThemeMode::System` against
  `Window::appearance()` and pushes the mode before traversing the
  tree, so widgets lower down read the right shade without owning a
  `Mode` parameter.
- `render_wallpaper` + `panel_bg` now branch on `colors::mode()` so
  the gradient base, panel tint, and overlay highlights flip to
  warm-graphite when dark mode is active.
- Mechanical conversion across 687 call sites in 55 files from
  `colors::FOO` constants to `colors::foo()` accessors. The
  `Theme` / `ELY_THEME` const surface (unused outside the design
  system) is removed; the function surface is the new contract.
2026-05-15 19:50:38 -04:00

263 lines
8.6 KiB
Rust

use ely_browser_core::BrowserSnapshot;
use ely_design_system::{colors, spacing};
use ely_domain::{BrowserTab, ThemeMode};
use gpui::{
AnyElement, BoxShadow, Context, Focusable, FontWeight, InteractiveElement, IntoElement,
ParentElement, SharedString, StatefulInteractiveElement, Styled, Window, 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,
window: &mut Window,
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::ArrowLeft,
active_tab.can_navigate_back(),
cx,
|shell, window, cx| shell.navigate_active_tab_back(window, cx),
))
.child(render_nav_arrow(
"nav-forward",
IconName::ArrowRight,
active_tab.can_navigate_forward(),
cx,
|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| {
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",
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| {
shell.open_internal_tab("ely://settings", window, cx)
}))
.into_any_element()
}
fn render_omnibar(
shell: &mut ElyShell,
active_tab: &BrowserTab,
window: &mut Window,
cx: &mut Context<ElyShell>,
) -> AnyElement {
let favorite_active = active_tab.flags().favorite;
let favorite_icon = if favorite_active { IconName::Star } else { IconName::StarOff };
let active_url = active_tab.url().as_str().to_string();
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://");
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(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 && !secure { IconName::Globe } else { IconName::Search };
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<F>(
id: &'static str,
icon: IconName,
enabled: bool,
cx: &mut Context<ElyShell>,
handler: F,
) -> AnyElement
where
F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context<ElyShell>) + 'static,
{
let color = if enabled { colors::ink_3() } else { colors::ink_5() };
div()
.id(SharedString::from(id))
.size(px(30.0))
.rounded(px(8.0))
.flex()
.items_center()
.justify_center()
.text_color(rgb(color))
.when(enabled, |el| {
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)))
})
.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;
/// Topbar quick-toggle icon for the current theme mode. The button
/// cycles System → Light → Dark → System, and the icon previews the
/// state the user is in: sun for light, moon for dark. System falls
/// back to moon since the bundled icon set has no combined sun-moon
/// glyph and the OS-driven mode visually leans neutral.
fn theme_mode_icon(mode: ThemeMode) -> IconName {
match mode {
ThemeMode::Light => IconName::Sun,
ThemeMode::System | ThemeMode::Dark => IconName::Moon,
}
}
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),
},
]
}