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.
263 lines
8.6 KiB
Rust
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),
|
|
},
|
|
]
|
|
}
|