From 3108f3265b3fde924dd02293d5b7ab8db6ea05b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sat, 9 May 2026 19:39:11 -0400 Subject: [PATCH] Add translucency setting + apply to runtime panel alpha 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). --- .../src/shell/chrome/appearance_form.rs | 72 +++++++++++++++++++ crates/ely_app/src/shell/chrome/mod.rs | 2 +- crates/ely_app/src/shell/chrome/sidebar.rs | 17 ++++- crates/ely_app/src/shell/render.rs | 5 +- crates/ely_app/src/shell/settings_actions.rs | 7 ++ crates/ely_app/src/shell/sidebar.rs | 6 +- crates/ely_browser_core/src/state.rs | 4 ++ crates/ely_browser_core/tests/appearance.rs | 2 + crates/ely_domain/src/appearance.rs | 46 ++++++++++-- crates/ely_domain/src/lib.rs | 4 +- 10 files changed, 150 insertions(+), 15 deletions(-) diff --git a/crates/ely_app/src/shell/chrome/appearance_form.rs b/crates/ely_app/src/shell/chrome/appearance_form.rs index c690ff2..460cf58 100644 --- a/crates/ely_app/src/shell/chrome/appearance_form.rs +++ b/crates/ely_app/src/shell/chrome/appearance_form.rs @@ -177,11 +177,82 @@ fn render_appearance_rows( .flex_col() .child(render_theme_mode_row(appearance.theme_mode(), cx)) .child(render_accent_row()) + .child(render_translucency_row(appearance.translucency_pct(), cx)) .child(render_reduce_motion_row(appearance.reduce_motion(), cx)) .child(render_reset_row(cx)) .into_any_element() } +fn render_translucency_row( + pct: u8, + cx: &mut Context, +) -> AnyElement { + settings_row( + "Translucency", + "Glass density of sidebar & panels.", + div() + .flex() + .items_center() + .gap(px(10.0)) + .child(render_translucency_track(pct)) + .child(translucency_preset("Solid", 0, pct, cx)) + .child(translucency_preset("Default", 40, pct, cx)) + .child(translucency_preset("Glassy", 75, pct, cx)) + .into_any_element(), + ) +} + +fn render_translucency_track(pct: u8) -> AnyElement { + let clamped = pct.min(100) as f32; + + div() + .w(px(120.0)) + .h(px(4.0)) + .rounded(px(2.0)) + .bg(rgba(TRACK_BG)) + .relative() + .child( + div() + .absolute() + .left_0() + .top_0() + .bottom_0() + .w(px(120.0 * clamped / 100.0)) + .rounded(px(2.0)) + .bg(rgb(colors::ACCENT)), + ) + .into_any_element() +} + +fn translucency_preset( + label: &'static str, + target_pct: u8, + active_pct: u8, + cx: &mut Context, +) -> AnyElement { + let selected = active_pct == target_pct; + let bg = if selected { 0xffffffff } else { 0x281e140d }; + let id = SharedString::from(format!("translucency-{}", label.to_ascii_lowercase())); + + div() + .id(id) + .px(px(10.0)) + .py(px(4.0)) + .rounded(px(6.0)) + .bg(rgba(bg)) + .text_size(px(11.5)) + .font_weight(FontWeight(500.0)) + .text_color(rgb(colors::INK_2)) + .cursor_pointer() + .hover(|style| style.opacity(0.92)) + .active(|style| style.opacity(0.82)) + .on_click(cx.listener(move |shell, _, _, cx| { + shell.set_translucency_pct(target_pct, cx); + })) + .child(label) + .into_any_element() +} + fn render_theme_mode_row( active: ThemeMode, cx: &mut Context, @@ -384,3 +455,4 @@ fn transparent_like(color: Hsla) -> Hsla { } const SEGMENT_BG: u32 = 0x281e140d; +const TRACK_BG: u32 = 0x281e1414; diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index 3153ef7..578ad06 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -21,7 +21,7 @@ pub(crate) use command_overlay::render_command_overlay; pub(crate) use home::render_home_page; pub(crate) use plugin_detail_view::render_plugin_detail_view; pub(crate) use settings_layout::render_settings_landing; -pub(crate) use sidebar::{PANEL_BG, panel_shadow}; +pub(crate) use sidebar::{panel_bg, panel_shadow}; pub(crate) use sidebar_header::render_sidebar_header; pub(crate) use split_pane::{ pane_host_label, pane_path_label, pane_url_is_secure, render_compact_split_canvas, diff --git a/crates/ely_app/src/shell/chrome/sidebar.rs b/crates/ely_app/src/shell/chrome/sidebar.rs index 901df86..cbde061 100644 --- a/crates/ely_app/src/shell/chrome/sidebar.rs +++ b/crates/ely_app/src/shell/chrome/sidebar.rs @@ -18,13 +18,14 @@ impl ElyShell { sidebar_width: f32, cx: &mut Context, ) -> 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_BG)) + .bg(rgba(panel_color)) .shadow(panel_shadow()) .overflow_hidden() .child(render_sidebar_header(snapshot, cx)) @@ -408,9 +409,21 @@ fn section_label(label: &'static str) -> impl IntoElement { } pub(crate) const ACTIVE_NAV_BG: u32 = 0xffffffd9; -pub(crate) const PANEL_BG: u32 = 0xffffffe0; 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 { vec![ BoxShadow { diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index 427723e..38cc56a 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -7,7 +7,7 @@ use gpui::{ }; use super::chrome::{ - PANEL_BG, panel_shadow, render_command_overlay, + panel_bg, panel_shadow, render_command_overlay, render_topbar as render_topbar_chrome, render_wallpaper, }; use super::sidebar::collapsed_sidebar_active; @@ -86,6 +86,7 @@ impl ElyShell { sidebar_collapsed: bool, cx: &mut Context, ) -> AnyElement { + let panel_color = panel_bg(snapshot); div() .flex_1() .h_full() @@ -93,7 +94,7 @@ impl ElyShell { .flex() .flex_col() .rounded(px(spacing::RADIUS_CARD)) - .bg(rgba(PANEL_BG)) + .bg(rgba(panel_color)) .shadow(panel_shadow()) .overflow_hidden() .child(render_topbar_chrome(self, snapshot, active_tab, sidebar_collapsed, cx)) diff --git a/crates/ely_app/src/shell/settings_actions.rs b/crates/ely_app/src/shell/settings_actions.rs index 07c6b0a..66c9293 100644 --- a/crates/ely_app/src/shell/settings_actions.rs +++ b/crates/ely_app/src/shell/settings_actions.rs @@ -79,6 +79,13 @@ impl ElyShell { } } + pub(crate) fn set_translucency_pct(&mut self, value: u8, cx: &mut Context) { + if let ShellState::Ready(core) = &mut self.state { + core.set_translucency_pct(value); + cx.notify(); + } + } + pub(super) fn reset_appearance(&mut self, cx: &mut Context) { if let ShellState::Ready(core) = &mut self.state { core.reset_appearance(); diff --git a/crates/ely_app/src/shell/sidebar.rs b/crates/ely_app/src/shell/sidebar.rs index b4cdf8d..ee38d1d 100644 --- a/crates/ely_app/src/shell/sidebar.rs +++ b/crates/ely_app/src/shell/sidebar.rs @@ -12,6 +12,7 @@ use gpui_component::{ button::{Button, ButtonVariants}, }; +use super::chrome::panel_bg; use super::{ElyShell, ShellState, render::tab_profile_label}; use crate::ToggleSidebar; @@ -31,6 +32,7 @@ impl ElyShell { sidebar_width: f32, cx: &mut Context, ) -> AnyElement { + let panel_color = panel_bg(snapshot); div() .w(px(sidebar_width)) .h_full() @@ -40,7 +42,7 @@ impl ElyShell { .gap_2() .p_2() .rounded(px(spacing::RADIUS_CARD)) - .bg(rgba(PANEL_BG)) + .bg(rgba(panel_color)) .shadow(panel_shadow()) .children(snapshot.favorites.iter().enumerate().map(|(index, tab)| { self.render_compact_tab_button( @@ -238,8 +240,6 @@ pub(super) fn collapsed_sidebar_active(sidebar_width: f32) -> bool { sidebar_width <= f32::from(COLLAPSED_SIDEBAR_WIDTH_PX) } -const PANEL_BG: u32 = 0xffffffe0; - fn panel_shadow() -> Vec { vec![ BoxShadow { diff --git a/crates/ely_browser_core/src/state.rs b/crates/ely_browser_core/src/state.rs index a376c7a..dff16bb 100644 --- a/crates/ely_browser_core/src/state.rs +++ b/crates/ely_browser_core/src/state.rs @@ -385,6 +385,10 @@ impl BrowserCore { self.appearance.set_reduce_motion(reduce_motion); } + pub fn set_translucency_pct(&mut self, value: u8) { + self.appearance.set_translucency_pct(value); + } + pub fn reset_appearance(&mut self) { self.appearance = AppearanceSettings::default(); } diff --git a/crates/ely_browser_core/tests/appearance.rs b/crates/ely_browser_core/tests/appearance.rs index 5ae46bb..33e9354 100644 --- a/crates/ely_browser_core/tests/appearance.rs +++ b/crates/ely_browser_core/tests/appearance.rs @@ -21,11 +21,13 @@ fn setters_persist_into_subsequent_snapshots() -> Result<(), Box> { core.set_wallpaper_theme(WallpaperTheme::Mint); core.set_theme_mode(ThemeMode::Dark); core.set_reduce_motion(true); + core.set_translucency_pct(75); let snapshot = core.snapshot()?; assert_eq!(snapshot.appearance.wallpaper(), WallpaperTheme::Mint); assert_eq!(snapshot.appearance.theme_mode(), ThemeMode::Dark); assert!(snapshot.appearance.reduce_motion()); + assert_eq!(snapshot.appearance.translucency_pct(), 75); Ok(()) } diff --git a/crates/ely_domain/src/appearance.rs b/crates/ely_domain/src/appearance.rs index e2e3b66..b2fc659 100644 --- a/crates/ely_domain/src/appearance.rs +++ b/crates/ely_domain/src/appearance.rs @@ -19,11 +19,26 @@ pub enum ThemeMode { Dark, } -#[derive(Clone, Copy, Debug, Default, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] +pub const DEFAULT_TRANSLUCENCY_PCT: u8 = 40; +pub const MAX_TRANSLUCENCY_PCT: u8 = 100; + +#[derive(Clone, Copy, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] pub struct AppearanceSettings { wallpaper: WallpaperTheme, theme_mode: ThemeMode, reduce_motion: bool, + translucency_pct: u8, +} + +impl Default for AppearanceSettings { + fn default() -> Self { + Self { + wallpaper: WallpaperTheme::default(), + theme_mode: ThemeMode::default(), + reduce_motion: false, + translucency_pct: DEFAULT_TRANSLUCENCY_PCT, + } + } } impl AppearanceSettings { @@ -39,6 +54,10 @@ impl AppearanceSettings { self.reduce_motion } + pub fn translucency_pct(&self) -> u8 { + self.translucency_pct + } + pub fn set_wallpaper(&mut self, wallpaper: WallpaperTheme) { self.wallpaper = wallpaper; } @@ -50,6 +69,10 @@ impl AppearanceSettings { pub fn set_reduce_motion(&mut self, reduce_motion: bool) { self.reduce_motion = reduce_motion; } + + pub fn set_translucency_pct(&mut self, value: u8) { + self.translucency_pct = value.min(MAX_TRANSLUCENCY_PCT); + } } #[cfg(test)] @@ -62,6 +85,7 @@ mod tests { assert_eq!(settings.wallpaper(), WallpaperTheme::Dawn); assert_eq!(settings.theme_mode(), ThemeMode::System); assert!(!settings.reduce_motion()); + assert_eq!(settings.translucency_pct(), super::DEFAULT_TRANSLUCENCY_PCT); } #[test] @@ -71,23 +95,33 @@ mod tests { settings.set_wallpaper(WallpaperTheme::Violet); settings.set_theme_mode(ThemeMode::Dark); settings.set_reduce_motion(true); + settings.set_translucency_pct(75); assert_eq!(settings.wallpaper(), WallpaperTheme::Violet); assert_eq!(settings.theme_mode(), ThemeMode::Dark); assert!(settings.reduce_motion()); + assert_eq!(settings.translucency_pct(), 75); + } + + #[test] + fn translucency_setter_clamps_above_max() { + let mut settings = AppearanceSettings::default(); + settings.set_translucency_pct(200); + assert_eq!(settings.translucency_pct(), super::MAX_TRANSLUCENCY_PCT); } #[test] fn json_round_trip_preserves_kebab_case_variants() { - let settings = AppearanceSettings { - wallpaper: WallpaperTheme::Mint, - theme_mode: ThemeMode::Light, - reduce_motion: true, - }; + let mut settings = AppearanceSettings::default(); + settings.set_wallpaper(WallpaperTheme::Mint); + settings.set_theme_mode(ThemeMode::Light); + settings.set_reduce_motion(true); + settings.set_translucency_pct(60); let json = serde_json::to_string(&settings).unwrap(); assert!(json.contains("\"wallpaper\":\"mint\"")); assert!(json.contains("\"theme_mode\":\"light\"")); + assert!(json.contains("\"translucency_pct\":60")); let restored: AppearanceSettings = serde_json::from_str(&json).unwrap(); assert_eq!(restored, settings); diff --git a/crates/ely_domain/src/lib.rs b/crates/ely_domain/src/lib.rs index c403fc1..ad67f4e 100644 --- a/crates/ely_domain/src/lib.rs +++ b/crates/ely_domain/src/lib.rs @@ -24,7 +24,9 @@ mod tab_group; mod update; mod url_text; -pub use appearance::{AppearanceSettings, ThemeMode, WallpaperTheme}; +pub use appearance::{ + AppearanceSettings, DEFAULT_TRANSLUCENCY_PCT, MAX_TRANSLUCENCY_PCT, ThemeMode, WallpaperTheme, +}; pub use archive::{ArchiveSource, ArchivedTab}; pub use bookmark::BookmarkEntry; pub use command::{CommandIntent, CommandScope};