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).
This commit is contained in:
2026-05-09 19:39:11 -04:00
parent 22186bb8a4
commit 3108f3265b
10 changed files with 150 additions and 15 deletions
@@ -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<ElyShell>,
) -> 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<ElyShell>,
) -> 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<ElyShell>,
@@ -384,3 +455,4 @@ fn transparent_like(color: Hsla) -> Hsla {
}
const SEGMENT_BG: u32 = 0x281e140d;
const TRACK_BG: u32 = 0x281e1414;
+1 -1
View File
@@ -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,
+15 -2
View File
@@ -18,13 +18,14 @@ impl ElyShell {
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_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<BoxShadow> {
vec![
BoxShadow {
+3 -2
View File
@@ -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<Self>,
) -> 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))
@@ -79,6 +79,13 @@ impl ElyShell {
}
}
pub(crate) fn set_translucency_pct(&mut self, value: u8, cx: &mut Context<Self>) {
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<Self>) {
if let ShellState::Ready(core) = &mut self.state {
core.reset_appearance();
+3 -3
View File
@@ -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<Self>,
) -> 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<BoxShadow> {
vec![
BoxShadow {