Domain: - AppearanceSettings gains translucency_pct (u8, 0..=100, default 40) with a clamping setter and serde round-trip coverage. - DEFAULT_TRANSLUCENCY_PCT and MAX_TRANSLUCENCY_PCT exported for the shell. Core: - BrowserCore::set_translucency_pct delegates to the appearance struct; the existing reset_appearance covers the reset path. - Integration test covers persistence into snapshot.appearance. Render: - chrome::sidebar::panel_bg(snapshot) replaces the static PANEL_BG constant, mapping the user's translucency_pct linearly into the alpha byte 0xff..0xb3. Sidebar (expanded + compact) and main pane consume the helper so changing the setting at runtime updates every glass surface in lock-step. Form: - Translucency row in chrome::appearance_form mirrors the design's static track + thumb visual driven by the persisted percentage, plus three preset chips (Solid 0 / Default 40 / Glassy 75) that mutate the setting through shell.set_translucency_pct. Strict UX rule preserved: alpha never drops below 0xb3 so panels stay readable without backdrop blur (which GPUI 0.2.2 doesn't expose).
460 lines
15 KiB
Rust
460 lines
15 KiB
Rust
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, linear_color_stop, linear_gradient, point,
|
|
prelude::FluentBuilder, px, rgb, rgba,
|
|
};
|
|
use gpui_component::{IconName, StyledExt, scroll::ScrollableElement};
|
|
|
|
use crate::shell::ElyShell;
|
|
use crate::shell::chrome::{render_glyph_for, render_sidebar_header};
|
|
|
|
impl ElyShell {
|
|
pub(crate) fn render_expanded_sidebar(
|
|
&mut self,
|
|
snapshot: &BrowserSnapshot,
|
|
sidebar_width: f32,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let panel_color = panel_bg(snapshot);
|
|
div()
|
|
.w(px(sidebar_width))
|
|
.h_full()
|
|
.flex()
|
|
.flex_col()
|
|
.rounded(px(spacing::RADIUS_CARD))
|
|
.bg(rgba(panel_color))
|
|
.shadow(panel_shadow())
|
|
.overflow_hidden()
|
|
.child(render_sidebar_header(snapshot, cx))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.overflow_y_scrollbar()
|
|
.p(px(10.0))
|
|
.flex()
|
|
.flex_col()
|
|
.gap(px(2.0))
|
|
.child(self.render_home_anchor_row(snapshot, cx))
|
|
.children(snapshot.favorites.iter().map(|tab| {
|
|
self.render_launcher_row(
|
|
tab,
|
|
tab.id() == &snapshot.active_tab_id,
|
|
cx,
|
|
)
|
|
}))
|
|
.child(section_label("PINNED"))
|
|
.children(snapshot.pinned_tabs.iter().map(|tab| {
|
|
self.render_launcher_row(
|
|
tab,
|
|
tab.id() == &snapshot.active_tab_id,
|
|
cx,
|
|
)
|
|
}))
|
|
.child(section_tabs_label(snapshot.tabs.len()))
|
|
.children(self.render_sidebar_tab_rows(snapshot, cx))
|
|
.child(self.render_new_tab_row(cx)),
|
|
)
|
|
.child(self.render_sidebar_footer(snapshot, cx))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_sidebar_footer(
|
|
&mut self,
|
|
snapshot: &BrowserSnapshot,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
div()
|
|
.px(px(10.0))
|
|
.py(px(8.0))
|
|
.flex()
|
|
.flex_col()
|
|
.gap(px(2.0))
|
|
.flex_shrink_0()
|
|
.border_t_1()
|
|
.border_color(rgba(colors::DIVIDER))
|
|
.child(self.render_settings_row(cx))
|
|
.child(self.render_profile_row(snapshot, cx))
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_settings_row(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
|
div()
|
|
.id(SharedString::from("nav-settings"))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.text_size(px(13.0))
|
|
.text_color(rgb(colors::INK_2))
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)).text_color(rgb(colors::INK)))
|
|
.active(|style| style.opacity(0.82))
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.open_internal_tab("ely://settings", window, cx);
|
|
}))
|
|
.child(
|
|
div()
|
|
.text_color(rgb(colors::INK_3))
|
|
.child(IconName::Settings),
|
|
)
|
|
.child("Settings")
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_profile_row(
|
|
&mut self,
|
|
snapshot: &BrowserSnapshot,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let profile_name = snapshot.active_profile_name.clone();
|
|
|
|
div()
|
|
.id(SharedString::from("nav-profile"))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(6.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
|
|
.active(|style| style.opacity(0.82))
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.open_internal_tab("ely://settings/profiles", window, cx);
|
|
}))
|
|
.child(
|
|
div()
|
|
.size(px(26.0))
|
|
.rounded_full()
|
|
.bg(linear_gradient(
|
|
135.0,
|
|
linear_color_stop(hsla(20.0 / 360.0, 0.6, 0.84, 1.0), 0.0),
|
|
linear_color_stop(hsla(15.0 / 360.0, 0.55, 0.53, 1.0), 1.0),
|
|
))
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.child(
|
|
div()
|
|
.text_size(px(10.0))
|
|
.font_semibold()
|
|
.text_color(rgb(0xffffff))
|
|
.child(profile_initial(&profile_name)),
|
|
),
|
|
)
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.truncate()
|
|
.text_size(px(13.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(colors::INK_2))
|
|
.child(profile_name),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(IconName::ChevronDown),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_home_anchor_row(
|
|
&mut self,
|
|
snapshot: &BrowserSnapshot,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let active_tab = snapshot
|
|
.tabs
|
|
.iter()
|
|
.find(|tab| tab.id() == &snapshot.active_tab_id);
|
|
let active = active_tab
|
|
.map(|tab| tab.url().as_str() == "ely://new-tab")
|
|
.unwrap_or(false);
|
|
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
|
|
let text_color = if active { colors::INK } else { colors::INK_2 };
|
|
|
|
div()
|
|
.id(SharedString::from("nav-home"))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
|
|
.active(|style| style.opacity(0.82))
|
|
.bg(rgba(bg_color))
|
|
.when(active, |el| el.shadow(soft_shadow()))
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.open_internal_tab("ely://new-tab", window, cx);
|
|
}))
|
|
.child(div().text_color(rgb(colors::INK_3)).child(IconName::Frame))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.text_size(px(13.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(text_color))
|
|
.child("Home"),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_launcher_row(
|
|
&mut self,
|
|
tab: &BrowserTab,
|
|
active: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let tab_id = tab.id().clone();
|
|
let close_tab_id = tab.id().clone();
|
|
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
|
|
let text_color = if active { colors::INK } else { colors::INK_2 };
|
|
let host = tab.url().host().map(|host| host.to_string());
|
|
let title = tab.title().to_string();
|
|
let initial = title.chars().next().unwrap_or('?').to_string();
|
|
let unread = tab.unread_count();
|
|
let group_name = SharedString::from(format!("launcher-{}", tab.id().as_str()));
|
|
let close_id = SharedString::from(format!("launcher-close-{}", tab.id().as_str()));
|
|
|
|
div()
|
|
.id(SharedString::from(format!("nav-{}", tab.id().as_str())))
|
|
.group(group_name.clone())
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
|
|
.active(|style| style.opacity(0.82))
|
|
.bg(rgba(bg_color))
|
|
.when(active, |el| el.shadow(soft_shadow()))
|
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
shell.select_tab(&tab_id, window, cx);
|
|
}))
|
|
.child(render_glyph_for(host.as_deref(), &initial, 18.0))
|
|
.child(
|
|
div()
|
|
.flex_1()
|
|
.min_w_0()
|
|
.overflow_hidden()
|
|
.truncate()
|
|
.text_size(px(13.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(text_color))
|
|
.child(title),
|
|
)
|
|
.when(unread > 0, |el| el.child(render_unread_badge(unread)))
|
|
.child(
|
|
div()
|
|
.id(close_id)
|
|
.size(px(16.0))
|
|
.rounded(px(4.0))
|
|
.flex()
|
|
.items_center()
|
|
.justify_center()
|
|
.text_color(rgb(colors::INK_4))
|
|
.opacity(0.0)
|
|
.group_hover(group_name, |style| style.opacity(1.0))
|
|
.hover(|style| style.bg(rgba(0x281e1414)).text_color(rgb(colors::INK)))
|
|
.cursor_pointer()
|
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
shell.select_tab(&close_tab_id, window, cx);
|
|
shell.close_active_tab(window, cx);
|
|
}))
|
|
.child(IconName::Close),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
fn render_new_tab_row(&mut self, cx: &mut Context<Self>) -> AnyElement {
|
|
div()
|
|
.id(SharedString::from("nav-new-tab"))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.text_color(rgb(colors::INK_3))
|
|
.text_size(px(13.0))
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)).text_color(rgb(colors::INK)))
|
|
.active(|style| style.opacity(0.82))
|
|
.on_click(cx.listener(|shell, _, window, cx| {
|
|
shell.open_new_tab(window, cx);
|
|
}))
|
|
.child(
|
|
div()
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(IconName::Plus),
|
|
)
|
|
.child("New Tab")
|
|
.into_any_element()
|
|
}
|
|
|
|
pub(crate) fn render_tab_row(
|
|
&mut self,
|
|
tab: &BrowserTab,
|
|
active: bool,
|
|
cx: &mut Context<Self>,
|
|
) -> AnyElement {
|
|
let tab_id = tab.id().clone();
|
|
let bg_color = if active { ACTIVE_NAV_BG } else { 0x00000000 };
|
|
let text_color = if active { colors::INK } else { colors::INK_2 };
|
|
|
|
div()
|
|
.id(SharedString::from(tab.id().as_str().to_string()))
|
|
.rounded(px(spacing::RADIUS_NAV))
|
|
.px(px(10.0))
|
|
.py(px(7.0))
|
|
.gap(px(6.0))
|
|
.flex()
|
|
.flex_col()
|
|
.cursor_pointer()
|
|
.hover(|style| style.bg(rgba(ACTIVE_NAV_BG)))
|
|
.active(|style| style.opacity(0.82))
|
|
.bg(rgba(bg_color))
|
|
.when(active, |el| el.shadow(soft_shadow()))
|
|
.on_click(cx.listener(move |shell, _, window, cx| {
|
|
shell.select_tab(&tab_id, window, cx);
|
|
}))
|
|
.child(
|
|
div()
|
|
.text_size(px(13.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(text_color))
|
|
.overflow_hidden()
|
|
.child(tab.title().to_string()),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(px(11.0))
|
|
.text_color(rgb(colors::INK_4))
|
|
.overflow_hidden()
|
|
.child(tab.display_url()),
|
|
)
|
|
.into_any_element()
|
|
}
|
|
|
|
}
|
|
|
|
fn profile_initial(name: &str) -> String {
|
|
name.chars()
|
|
.next()
|
|
.unwrap_or('P')
|
|
.to_uppercase()
|
|
.to_string()
|
|
}
|
|
|
|
fn render_unread_badge(count: u32) -> impl IntoElement {
|
|
let label = if count > 99 {
|
|
"99+".to_string()
|
|
} else {
|
|
count.to_string()
|
|
};
|
|
|
|
div()
|
|
.px(px(6.0))
|
|
.py(px(1.0))
|
|
.rounded(px(999.0))
|
|
.bg(rgba(UNREAD_BADGE_BG))
|
|
.text_size(px(10.0))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(colors::INK_3))
|
|
.child(label)
|
|
}
|
|
|
|
fn section_tabs_label(count: usize) -> impl IntoElement {
|
|
div()
|
|
.pt(px(12.0))
|
|
.pb(px(4.0))
|
|
.px(px(10.0))
|
|
.flex()
|
|
.items_center()
|
|
.gap(px(6.0))
|
|
.child(
|
|
div()
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(IconName::Frame),
|
|
)
|
|
.child(
|
|
div()
|
|
.text_size(px(10.5))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(format!("TABS · {count}")),
|
|
)
|
|
}
|
|
|
|
fn section_label(label: &'static str) -> impl IntoElement {
|
|
div()
|
|
.pt(px(8.0))
|
|
.pb(px(4.0))
|
|
.px(px(10.0))
|
|
.text_size(px(10.5))
|
|
.font_weight(gpui::FontWeight(500.0))
|
|
.text_color(rgb(colors::INK_4))
|
|
.child(label)
|
|
}
|
|
|
|
pub(crate) const ACTIVE_NAV_BG: u32 = 0xffffffd9;
|
|
const UNREAD_BADGE_BG: u32 = 0x281e140f;
|
|
|
|
/// Maps appearance translucency_pct to a panel rgba u32.
|
|
///
|
|
/// translucency_pct 0 → alpha 0xff (fully opaque)
|
|
/// translucency_pct 100 → alpha 0xb3 (mostly opaque, ~70%) so panels stay
|
|
/// readable without backdrop blur.
|
|
pub(crate) fn panel_bg(snapshot: &BrowserSnapshot) -> u32 {
|
|
let pct = snapshot.appearance.translucency_pct().min(100) as u32;
|
|
let max_alpha: u32 = 0xff;
|
|
let min_alpha: u32 = 0xb3;
|
|
let alpha = max_alpha - (pct * (max_alpha - min_alpha)) / 100;
|
|
0xffffff00 | alpha
|
|
}
|
|
|
|
pub(crate) fn panel_shadow() -> Vec<BoxShadow> {
|
|
vec![
|
|
BoxShadow {
|
|
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.30),
|
|
offset: point(px(0.), px(20.)),
|
|
blur_radius: px(50.),
|
|
spread_radius: px(-15.),
|
|
},
|
|
BoxShadow {
|
|
color: hsla(0., 0., 1., 0.5),
|
|
offset: point(px(0.), px(0.)),
|
|
blur_radius: px(0.),
|
|
spread_radius: px(1.),
|
|
},
|
|
]
|
|
}
|
|
|
|
pub(crate) fn soft_shadow() -> Vec<BoxShadow> {
|
|
vec![
|
|
BoxShadow {
|
|
color: hsla(0., 0., 1., 0.7),
|
|
offset: point(px(0.), px(1.)),
|
|
blur_radius: px(0.),
|
|
spread_radius: px(0.),
|
|
},
|
|
BoxShadow {
|
|
color: hsla(25.0 / 360.0, 0.33, 0.12, 0.08),
|
|
offset: point(px(0.), px(0.)),
|
|
blur_radius: px(0.),
|
|
spread_radius: px(1.),
|
|
},
|
|
]
|
|
}
|