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:
@@ -177,11 +177,82 @@ fn render_appearance_rows(
|
|||||||
.flex_col()
|
.flex_col()
|
||||||
.child(render_theme_mode_row(appearance.theme_mode(), cx))
|
.child(render_theme_mode_row(appearance.theme_mode(), cx))
|
||||||
.child(render_accent_row())
|
.child(render_accent_row())
|
||||||
|
.child(render_translucency_row(appearance.translucency_pct(), cx))
|
||||||
.child(render_reduce_motion_row(appearance.reduce_motion(), cx))
|
.child(render_reduce_motion_row(appearance.reduce_motion(), cx))
|
||||||
.child(render_reset_row(cx))
|
.child(render_reset_row(cx))
|
||||||
.into_any_element()
|
.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(
|
fn render_theme_mode_row(
|
||||||
active: ThemeMode,
|
active: ThemeMode,
|
||||||
cx: &mut Context<ElyShell>,
|
cx: &mut Context<ElyShell>,
|
||||||
@@ -384,3 +455,4 @@ fn transparent_like(color: Hsla) -> Hsla {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEGMENT_BG: u32 = 0x281e140d;
|
const SEGMENT_BG: u32 = 0x281e140d;
|
||||||
|
const TRACK_BG: u32 = 0x281e1414;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ pub(crate) use command_overlay::render_command_overlay;
|
|||||||
pub(crate) use home::render_home_page;
|
pub(crate) use home::render_home_page;
|
||||||
pub(crate) use plugin_detail_view::render_plugin_detail_view;
|
pub(crate) use plugin_detail_view::render_plugin_detail_view;
|
||||||
pub(crate) use settings_layout::render_settings_landing;
|
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 sidebar_header::render_sidebar_header;
|
||||||
pub(crate) use split_pane::{
|
pub(crate) use split_pane::{
|
||||||
pane_host_label, pane_path_label, pane_url_is_secure, render_compact_split_canvas,
|
pane_host_label, pane_path_label, pane_url_is_secure, render_compact_split_canvas,
|
||||||
|
|||||||
@@ -18,13 +18,14 @@ impl ElyShell {
|
|||||||
sidebar_width: f32,
|
sidebar_width: f32,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let panel_color = panel_bg(snapshot);
|
||||||
div()
|
div()
|
||||||
.w(px(sidebar_width))
|
.w(px(sidebar_width))
|
||||||
.h_full()
|
.h_full()
|
||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.rounded(px(spacing::RADIUS_CARD))
|
.rounded(px(spacing::RADIUS_CARD))
|
||||||
.bg(rgba(PANEL_BG))
|
.bg(rgba(panel_color))
|
||||||
.shadow(panel_shadow())
|
.shadow(panel_shadow())
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(render_sidebar_header(snapshot, cx))
|
.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 ACTIVE_NAV_BG: u32 = 0xffffffd9;
|
||||||
pub(crate) const PANEL_BG: u32 = 0xffffffe0;
|
|
||||||
const UNREAD_BADGE_BG: u32 = 0x281e140f;
|
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> {
|
pub(crate) fn panel_shadow() -> Vec<BoxShadow> {
|
||||||
vec![
|
vec![
|
||||||
BoxShadow {
|
BoxShadow {
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ use gpui::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use super::chrome::{
|
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,
|
render_topbar as render_topbar_chrome, render_wallpaper,
|
||||||
};
|
};
|
||||||
use super::sidebar::collapsed_sidebar_active;
|
use super::sidebar::collapsed_sidebar_active;
|
||||||
@@ -86,6 +86,7 @@ impl ElyShell {
|
|||||||
sidebar_collapsed: bool,
|
sidebar_collapsed: bool,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let panel_color = panel_bg(snapshot);
|
||||||
div()
|
div()
|
||||||
.flex_1()
|
.flex_1()
|
||||||
.h_full()
|
.h_full()
|
||||||
@@ -93,7 +94,7 @@ impl ElyShell {
|
|||||||
.flex()
|
.flex()
|
||||||
.flex_col()
|
.flex_col()
|
||||||
.rounded(px(spacing::RADIUS_CARD))
|
.rounded(px(spacing::RADIUS_CARD))
|
||||||
.bg(rgba(PANEL_BG))
|
.bg(rgba(panel_color))
|
||||||
.shadow(panel_shadow())
|
.shadow(panel_shadow())
|
||||||
.overflow_hidden()
|
.overflow_hidden()
|
||||||
.child(render_topbar_chrome(self, snapshot, active_tab, sidebar_collapsed, cx))
|
.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>) {
|
pub(super) fn reset_appearance(&mut self, cx: &mut Context<Self>) {
|
||||||
if let ShellState::Ready(core) = &mut self.state {
|
if let ShellState::Ready(core) = &mut self.state {
|
||||||
core.reset_appearance();
|
core.reset_appearance();
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ use gpui_component::{
|
|||||||
button::{Button, ButtonVariants},
|
button::{Button, ButtonVariants},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::chrome::panel_bg;
|
||||||
use super::{ElyShell, ShellState, render::tab_profile_label};
|
use super::{ElyShell, ShellState, render::tab_profile_label};
|
||||||
use crate::ToggleSidebar;
|
use crate::ToggleSidebar;
|
||||||
|
|
||||||
@@ -31,6 +32,7 @@ impl ElyShell {
|
|||||||
sidebar_width: f32,
|
sidebar_width: f32,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) -> AnyElement {
|
) -> AnyElement {
|
||||||
|
let panel_color = panel_bg(snapshot);
|
||||||
div()
|
div()
|
||||||
.w(px(sidebar_width))
|
.w(px(sidebar_width))
|
||||||
.h_full()
|
.h_full()
|
||||||
@@ -40,7 +42,7 @@ impl ElyShell {
|
|||||||
.gap_2()
|
.gap_2()
|
||||||
.p_2()
|
.p_2()
|
||||||
.rounded(px(spacing::RADIUS_CARD))
|
.rounded(px(spacing::RADIUS_CARD))
|
||||||
.bg(rgba(PANEL_BG))
|
.bg(rgba(panel_color))
|
||||||
.shadow(panel_shadow())
|
.shadow(panel_shadow())
|
||||||
.children(snapshot.favorites.iter().enumerate().map(|(index, tab)| {
|
.children(snapshot.favorites.iter().enumerate().map(|(index, tab)| {
|
||||||
self.render_compact_tab_button(
|
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)
|
sidebar_width <= f32::from(COLLAPSED_SIDEBAR_WIDTH_PX)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PANEL_BG: u32 = 0xffffffe0;
|
|
||||||
|
|
||||||
fn panel_shadow() -> Vec<BoxShadow> {
|
fn panel_shadow() -> Vec<BoxShadow> {
|
||||||
vec![
|
vec![
|
||||||
BoxShadow {
|
BoxShadow {
|
||||||
|
|||||||
@@ -385,6 +385,10 @@ impl BrowserCore {
|
|||||||
self.appearance.set_reduce_motion(reduce_motion);
|
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) {
|
pub fn reset_appearance(&mut self) {
|
||||||
self.appearance = AppearanceSettings::default();
|
self.appearance = AppearanceSettings::default();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,11 +21,13 @@ fn setters_persist_into_subsequent_snapshots() -> Result<(), Box<dyn Error>> {
|
|||||||
core.set_wallpaper_theme(WallpaperTheme::Mint);
|
core.set_wallpaper_theme(WallpaperTheme::Mint);
|
||||||
core.set_theme_mode(ThemeMode::Dark);
|
core.set_theme_mode(ThemeMode::Dark);
|
||||||
core.set_reduce_motion(true);
|
core.set_reduce_motion(true);
|
||||||
|
core.set_translucency_pct(75);
|
||||||
|
|
||||||
let snapshot = core.snapshot()?;
|
let snapshot = core.snapshot()?;
|
||||||
assert_eq!(snapshot.appearance.wallpaper(), WallpaperTheme::Mint);
|
assert_eq!(snapshot.appearance.wallpaper(), WallpaperTheme::Mint);
|
||||||
assert_eq!(snapshot.appearance.theme_mode(), ThemeMode::Dark);
|
assert_eq!(snapshot.appearance.theme_mode(), ThemeMode::Dark);
|
||||||
assert!(snapshot.appearance.reduce_motion());
|
assert!(snapshot.appearance.reduce_motion());
|
||||||
|
assert_eq!(snapshot.appearance.translucency_pct(), 75);
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,11 +19,26 @@ pub enum ThemeMode {
|
|||||||
Dark,
|
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 {
|
pub struct AppearanceSettings {
|
||||||
wallpaper: WallpaperTheme,
|
wallpaper: WallpaperTheme,
|
||||||
theme_mode: ThemeMode,
|
theme_mode: ThemeMode,
|
||||||
reduce_motion: bool,
|
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 {
|
impl AppearanceSettings {
|
||||||
@@ -39,6 +54,10 @@ impl AppearanceSettings {
|
|||||||
self.reduce_motion
|
self.reduce_motion
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn translucency_pct(&self) -> u8 {
|
||||||
|
self.translucency_pct
|
||||||
|
}
|
||||||
|
|
||||||
pub fn set_wallpaper(&mut self, wallpaper: WallpaperTheme) {
|
pub fn set_wallpaper(&mut self, wallpaper: WallpaperTheme) {
|
||||||
self.wallpaper = wallpaper;
|
self.wallpaper = wallpaper;
|
||||||
}
|
}
|
||||||
@@ -50,6 +69,10 @@ impl AppearanceSettings {
|
|||||||
pub fn set_reduce_motion(&mut self, reduce_motion: bool) {
|
pub fn set_reduce_motion(&mut self, reduce_motion: bool) {
|
||||||
self.reduce_motion = reduce_motion;
|
self.reduce_motion = reduce_motion;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_translucency_pct(&mut self, value: u8) {
|
||||||
|
self.translucency_pct = value.min(MAX_TRANSLUCENCY_PCT);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -62,6 +85,7 @@ mod tests {
|
|||||||
assert_eq!(settings.wallpaper(), WallpaperTheme::Dawn);
|
assert_eq!(settings.wallpaper(), WallpaperTheme::Dawn);
|
||||||
assert_eq!(settings.theme_mode(), ThemeMode::System);
|
assert_eq!(settings.theme_mode(), ThemeMode::System);
|
||||||
assert!(!settings.reduce_motion());
|
assert!(!settings.reduce_motion());
|
||||||
|
assert_eq!(settings.translucency_pct(), super::DEFAULT_TRANSLUCENCY_PCT);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -71,23 +95,33 @@ mod tests {
|
|||||||
settings.set_wallpaper(WallpaperTheme::Violet);
|
settings.set_wallpaper(WallpaperTheme::Violet);
|
||||||
settings.set_theme_mode(ThemeMode::Dark);
|
settings.set_theme_mode(ThemeMode::Dark);
|
||||||
settings.set_reduce_motion(true);
|
settings.set_reduce_motion(true);
|
||||||
|
settings.set_translucency_pct(75);
|
||||||
|
|
||||||
assert_eq!(settings.wallpaper(), WallpaperTheme::Violet);
|
assert_eq!(settings.wallpaper(), WallpaperTheme::Violet);
|
||||||
assert_eq!(settings.theme_mode(), ThemeMode::Dark);
|
assert_eq!(settings.theme_mode(), ThemeMode::Dark);
|
||||||
assert!(settings.reduce_motion());
|
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]
|
#[test]
|
||||||
fn json_round_trip_preserves_kebab_case_variants() {
|
fn json_round_trip_preserves_kebab_case_variants() {
|
||||||
let settings = AppearanceSettings {
|
let mut settings = AppearanceSettings::default();
|
||||||
wallpaper: WallpaperTheme::Mint,
|
settings.set_wallpaper(WallpaperTheme::Mint);
|
||||||
theme_mode: ThemeMode::Light,
|
settings.set_theme_mode(ThemeMode::Light);
|
||||||
reduce_motion: true,
|
settings.set_reduce_motion(true);
|
||||||
};
|
settings.set_translucency_pct(60);
|
||||||
|
|
||||||
let json = serde_json::to_string(&settings).unwrap();
|
let json = serde_json::to_string(&settings).unwrap();
|
||||||
assert!(json.contains("\"wallpaper\":\"mint\""));
|
assert!(json.contains("\"wallpaper\":\"mint\""));
|
||||||
assert!(json.contains("\"theme_mode\":\"light\""));
|
assert!(json.contains("\"theme_mode\":\"light\""));
|
||||||
|
assert!(json.contains("\"translucency_pct\":60"));
|
||||||
|
|
||||||
let restored: AppearanceSettings = serde_json::from_str(&json).unwrap();
|
let restored: AppearanceSettings = serde_json::from_str(&json).unwrap();
|
||||||
assert_eq!(restored, settings);
|
assert_eq!(restored, settings);
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ mod tab_group;
|
|||||||
mod update;
|
mod update;
|
||||||
mod url_text;
|
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 archive::{ArchiveSource, ArchivedTab};
|
||||||
pub use bookmark::BookmarkEntry;
|
pub use bookmark::BookmarkEntry;
|
||||||
pub use command::{CommandIntent, CommandScope};
|
pub use command::{CommandIntent, CommandScope};
|
||||||
|
|||||||
Reference in New Issue
Block a user