From 797560d71c8ad4e4c6a1487c4d53d1f445df0150 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:46:43 -0400 Subject: [PATCH] Replace translucency presets with a real continuous slider ElyShell now hosts a translucency_slider: Entity bound to 0..=100 step 1, defaulting to DEFAULT_TRANSLUCENCY_PCT. A subscription on SliderEvent::Change writes the rounded value into the core via set_translucency_pct, so dragging the thumb mutates the persisted appearance setting in real time. The appearance form swaps the static track-and-thumb visual for the gpui_component Slider (160 wide) plus a live percentage readout. The three preset chips move below the row as fast-set buttons that go through a new set_translucency_pct_from_preset helper which writes both core and the SliderState so the thumb tracks the chip choice. reset_appearance now resets the slider to DEFAULT_TRANSLUCENCY_PCT alongside resetting the core, keeping every UI source of truth in lock-step. --- .../src/shell/chrome/appearance_form.rs | 64 ++++++++++--------- .../src/shell/chrome/settings_layout.rs | 3 +- .../src/shell/internal_pages/appearance.rs | 2 +- .../src/shell/internal_pages/settings.rs | 2 +- crates/ely_app/src/shell/mod.rs | 23 ++++++- crates/ely_app/src/shell/settings_actions.rs | 34 ++++++++-- 6 files changed, 89 insertions(+), 39 deletions(-) diff --git a/crates/ely_app/src/shell/chrome/appearance_form.rs b/crates/ely_app/src/shell/chrome/appearance_form.rs index 460cf58..fa5650a 100644 --- a/crates/ely_app/src/shell/chrome/appearance_form.rs +++ b/crates/ely_app/src/shell/chrome/appearance_form.rs @@ -6,7 +6,7 @@ use gpui::{ SharedString, StatefulInteractiveElement, Styled, div, hsla, linear_color_stop, linear_gradient, prelude::FluentBuilder, px, rgb, rgba, }; -use gpui_component::{IconName, scroll::ScrollableElement}; +use gpui_component::{IconName, scroll::ScrollableElement, slider::Slider}; use super::appearance_layout_cards::render_sidebar_layout_section; @@ -14,6 +14,7 @@ use crate::shell::ElyShell; use crate::shell::chrome::SERIF_FAMILY; pub(crate) fn render_appearance_form( + shell: &mut ElyShell, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { @@ -33,7 +34,8 @@ pub(crate) fn render_appearance_form( .gap(px(28.0)) .child(render_header()) .child(render_wallpaper_grid(snapshot, cx)) - .child(render_appearance_rows(snapshot, cx)) + .child(render_appearance_rows(shell, snapshot, cx)) + .child(render_translucency_presets(cx)) .child(render_sidebar_layout_section(snapshot, cx)), ) .into_any_element() @@ -167,6 +169,7 @@ fn render_wallpaper_swatch( } fn render_appearance_rows( + shell: &mut ElyShell, snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { @@ -177,16 +180,17 @@ 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_translucency_row(shell, appearance.translucency_pct())) .child(render_reduce_motion_row(appearance.reduce_motion(), cx)) .child(render_reset_row(cx)) .into_any_element() } fn render_translucency_row( + shell: &mut ElyShell, pct: u8, - cx: &mut Context, ) -> AnyElement { + let slider_state = shell.translucency_slider.clone(); settings_row( "Translucency", "Glass density of sidebar & panels.", @@ -194,44 +198,41 @@ fn render_translucency_row( .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)) + .child(div().w(px(160.0)).child(Slider::new(&slider_state))) + .child( + div() + .min_w(px(36.0)) + .text_size(px(11.0)) + .text_color(rgb(colors::INK_3)) + .child(format!("{pct}%")), + ) .into_any_element(), ) } -fn render_translucency_track(pct: u8) -> AnyElement { - let clamped = pct.min(100) as f32; - +fn render_translucency_presets(cx: &mut Context) -> AnyElement { div() - .w(px(120.0)) - .h(px(4.0)) - .rounded(px(2.0)) - .bg(rgba(TRACK_BG)) - .relative() + .flex() + .items_center() + .gap(px(8.0)) + .pt(px(2.0)) .child( div() - .absolute() - .left_0() - .top_0() - .bottom_0() - .w(px(120.0 * clamped / 100.0)) - .rounded(px(2.0)) - .bg(rgb(colors::ACCENT)), + .text_size(px(10.5)) + .text_color(rgb(colors::INK_4)) + .child("Presets"), ) + .child(translucency_preset("Solid", 0, cx)) + .child(translucency_preset("Default", 40, cx)) + .child(translucency_preset("Glassy", 75, cx)) .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() @@ -239,15 +240,15 @@ fn translucency_preset( .px(px(10.0)) .py(px(4.0)) .rounded(px(6.0)) - .bg(rgba(bg)) + .bg(rgba(SEGMENT_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); + .on_click(cx.listener(move |shell, _, window, cx| { + shell.set_translucency_pct_from_preset(target_pct, window, cx); })) .child(label) .into_any_element() @@ -377,7 +378,9 @@ fn render_reset_row(cx: &mut Context) -> AnyElement { .cursor_pointer() .hover(|style| style.opacity(0.92)) .active(|style| style.opacity(0.82)) - .on_click(cx.listener(|shell, _, _, cx| shell.reset_appearance(cx))) + .on_click(cx.listener(|shell, _, window, cx| { + shell.reset_appearance(window, cx); + })) .child("Reset") .into_any_element(), ) @@ -455,4 +458,3 @@ 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/settings_layout.rs b/crates/ely_app/src/shell/chrome/settings_layout.rs index 59e4181..48b73e8 100644 --- a/crates/ely_app/src/shell/chrome/settings_layout.rs +++ b/crates/ely_app/src/shell/chrome/settings_layout.rs @@ -112,6 +112,7 @@ const NAV_GROUPS: &[NavGroup] = &[ ]; pub(crate) fn render_settings_landing( + shell: &mut ElyShell, snapshot: &BrowserSnapshot, active_route: &str, cx: &mut Context, @@ -121,7 +122,7 @@ pub(crate) fn render_settings_landing( .h_full() .flex() .child(render_nav_column(snapshot, active_route, cx)) - .child(render_appearance_form(snapshot, cx)) + .child(render_appearance_form(shell, snapshot, cx)) .into_any_element() } diff --git a/crates/ely_app/src/shell/internal_pages/appearance.rs b/crates/ely_app/src/shell/internal_pages/appearance.rs index ae7ca13..3bbcce7 100644 --- a/crates/ely_app/src/shell/internal_pages/appearance.rs +++ b/crates/ely_app/src/shell/internal_pages/appearance.rs @@ -10,6 +10,6 @@ impl ElyShell { snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { - render_appearance_form(snapshot, cx) + render_appearance_form(self, snapshot, cx) } } diff --git a/crates/ely_app/src/shell/internal_pages/settings.rs b/crates/ely_app/src/shell/internal_pages/settings.rs index a8f9822..f0f27ad 100644 --- a/crates/ely_app/src/shell/internal_pages/settings.rs +++ b/crates/ely_app/src/shell/internal_pages/settings.rs @@ -10,6 +10,6 @@ impl ElyShell { snapshot: &BrowserSnapshot, cx: &mut Context, ) -> AnyElement { - render_settings_landing(snapshot, "ely://settings/appearance", cx) + render_settings_landing(self, snapshot, "ely://settings/appearance", cx) } } diff --git a/crates/ely_app/src/shell/mod.rs b/crates/ely_app/src/shell/mod.rs index 95f0775..413e25f 100644 --- a/crates/ely_app/src/shell/mod.rs +++ b/crates/ely_app/src/shell/mod.rs @@ -36,9 +36,10 @@ mod web_surface_view; use std::time::Duration; use ely_browser_core::{BrowserCore, InitialBrowserConfig}; -use ely_domain::{ProfileId, SpaceId, TabId}; +use ely_domain::{DEFAULT_TRANSLUCENCY_PCT, ProfileId, SpaceId, TabId}; use gpui::{AppContext, Context, Entity, FocusHandle, Subscription, Timer, Window}; use gpui_component::input::{InputEvent, InputState}; +use gpui_component::slider::{SliderEvent, SliderState, SliderValue}; use crate::shortcuts::ShortcutProfile; use bookmarks::PendingBookmarkEdit; @@ -63,6 +64,7 @@ pub struct ElyShell { focus_handle: FocusHandle, command_input: Entity, pub(crate) plugin_search_input: Entity, + pub(crate) translucency_slider: Entity, download_action_error: Option, download_clear_confirmation: bool, download_security_confirmation: Option, @@ -87,6 +89,7 @@ pub struct ElyShell { pending_plugin_uninstall: Option, web_surfaces: WebSurfaceStore, _command_subscription: Subscription, + _translucency_subscription: Subscription, } impl ElyShell { @@ -107,6 +110,22 @@ impl ElyShell { cx.new(|cx| InputState::new(window, cx).placeholder("Search or enter address")); let plugin_search_input = cx.new(|cx| InputState::new(window, cx).placeholder("Search plugins…")); + let translucency_slider = cx.new(|_cx| { + SliderState::new() + .min(0.0) + .max(100.0) + .step(1.0) + .default_value(f32::from(DEFAULT_TRANSLUCENCY_PCT)) + }); + + let translucency_subscription = + cx.subscribe(&translucency_slider, |shell: &mut Self, _state, event: &SliderEvent, cx| { + let SliderEvent::Change(SliderValue::Single(value)) = event else { + return; + }; + let pct = value.clamp(0.0, 100.0).round() as u8; + shell.set_translucency_pct(pct, cx); + }); let command_subscription = cx.subscribe_in( &command_input, @@ -155,6 +174,7 @@ impl ElyShell { focus_handle: cx.focus_handle(), command_input, plugin_search_input, + translucency_slider, download_action_error: None, download_clear_confirmation: false, download_security_confirmation: None, @@ -179,6 +199,7 @@ impl ElyShell { pending_plugin_uninstall: None, web_surfaces: WebSurfaceStore::new(), _command_subscription: command_subscription, + _translucency_subscription: translucency_subscription, }; start_external_web_surface_timer(cx); shell diff --git a/crates/ely_app/src/shell/settings_actions.rs b/crates/ely_app/src/shell/settings_actions.rs index 66c9293..842cc70 100644 --- a/crates/ely_app/src/shell/settings_actions.rs +++ b/crates/ely_app/src/shell/settings_actions.rs @@ -1,9 +1,10 @@ use ely_domain::{ - ArchivePolicy, DiagnosticsReportingPolicy, DownloadPolicy, FavoriteLimit, - HistoryRecordingPolicy, NewTabDestination, ProfileId, ProfileSyncPolicy, SearchEngine, - SyncObjectKind, SyncObjectPolicy, ThemeMode, UpdatePolicy, WallpaperTheme, + ArchivePolicy, DEFAULT_TRANSLUCENCY_PCT, DiagnosticsReportingPolicy, DownloadPolicy, + FavoriteLimit, HistoryRecordingPolicy, NewTabDestination, ProfileId, ProfileSyncPolicy, + SearchEngine, SyncObjectKind, SyncObjectPolicy, ThemeMode, UpdatePolicy, WallpaperTheme, }; use gpui::Context; +use gpui_component::slider::SliderValue; use super::{ElyShell, ShellState}; @@ -86,11 +87,36 @@ impl ElyShell { } } - pub(super) fn reset_appearance(&mut self, cx: &mut Context) { + pub(crate) fn set_translucency_pct_from_preset( + &mut self, + value: u8, + window: &mut gpui::Window, + cx: &mut Context, + ) { + self.set_translucency_pct(value, cx); + let slider = self.translucency_slider.clone(); + slider.update(cx, |state, cx| { + state.set_value(SliderValue::Single(f32::from(value)), window, cx); + }); + } + + pub(super) fn reset_appearance( + &mut self, + window: &mut gpui::Window, + cx: &mut Context, + ) { if let ShellState::Ready(core) = &mut self.state { core.reset_appearance(); cx.notify(); } + let slider = self.translucency_slider.clone(); + slider.update(cx, |state, cx| { + state.set_value( + SliderValue::Single(f32::from(DEFAULT_TRANSLUCENCY_PCT)), + window, + cx, + ); + }); } pub(super) fn reset_general_settings(&mut self, cx: &mut Context) {