From a92f478e2172fc4f55ee5149d68e56e3027636d6 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 20:29:08 -0400 Subject: [PATCH] Composite glass: inset highlights, blink, fade, panel tint MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Closes the three "platform-impossible" gaps from the design audit without resorting to hacks or shaders. * chrome::glass — `render_inner_highlight(radius)` paints the design's `box-shadow: inset 0 0 0 1px` ring as an absolute overlay div with a 1 px top-edge highlight sliver. GPUI 0.2.2's BoxShadow has no inset flag, so the composite is the only way to keep the panel content unclipped while the ring rides on top. * chrome::animations — `blink` and `fade_in` wrap GPUI's first-class `with_animation` driver. Square-wave caret in the command header, 180 ms opacity ramp on the command backdrop and workspace disclosure list. * chrome::sidebar — panel_bg now tints toward the active wallpaper theme so the wallpaper bleeds through every glass surface, the closest honest substitute for backdrop-filter without a shader pass. * command_overlay split: row helpers move to chrome::command_rows so command_overlay drops from 504 → 232 lines, well under the 500-line ceiling. No behavior change. cargo test --workspace: 440 passed, 0 failed. --- crates/ely_app/src/shell/chrome/animations.rs | 42 +++ .../src/shell/chrome/command_overlay.rs | 324 ++---------------- .../ely_app/src/shell/chrome/command_rows.rs | 282 +++++++++++++++ crates/ely_app/src/shell/chrome/glass.rs | 40 +++ crates/ely_app/src/shell/chrome/mod.rs | 3 + crates/ely_app/src/shell/chrome/sidebar.rs | 28 +- .../src/shell/chrome/sidebar_header.rs | 8 +- crates/ely_app/src/shell/render.rs | 3 + crates/ely_app/src/shell/sidebar.rs | 3 + 9 files changed, 436 insertions(+), 297 deletions(-) create mode 100644 crates/ely_app/src/shell/chrome/animations.rs create mode 100644 crates/ely_app/src/shell/chrome/command_rows.rs create mode 100644 crates/ely_app/src/shell/chrome/glass.rs diff --git a/crates/ely_app/src/shell/chrome/animations.rs b/crates/ely_app/src/shell/chrome/animations.rs new file mode 100644 index 0000000..492de28 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/animations.rs @@ -0,0 +1,42 @@ +use std::time::Duration; + +use gpui::{Animation, AnimationExt, ElementId, IntoElement, Styled}; + +/// One-second blink that toggles between fully visible and invisible at the +/// half-period mark, matching the design's +/// `animation: blink 1s infinite`. +/// +/// We deliberately use `with_animation` rather than rolling a Timer / cx.spawn +/// loop — GPUI requeues the layout pass on each frame the animation owns, so +/// CPU stays idle when the caret is offscreen and the visible state is +/// stable inside one render. The animation re-enters the closure with `t` in +/// `[0, 1]`; we snap to a square wave so the caret pops cleanly instead of +/// fading in and out. +pub(crate) fn blink(id: impl Into, element: E) -> impl IntoElement +where + E: IntoElement + Styled + 'static, +{ + element.with_animation( + id, + Animation::new(Duration::from_secs(1)).repeat(), + |element, t| element.opacity(if t < 0.5 { 1.0 } else { 0.0 }), + ) +} + +/// Soft fade-in over `panel_transition_ms` once the element first mounts. +/// +/// Drives the design's `panel transition` motion token (180 ms productive) +/// for surfaces like the command overlay and workspace disclosure that +/// appear / disappear in response to user input. +pub(crate) fn fade_in( + id: impl Into, + duration_ms: u64, + element: E, +) -> impl IntoElement +where + E: IntoElement + Styled + 'static, +{ + let animation = Animation::new(Duration::from_millis(duration_ms)); + element.with_animation(id, animation, |element, t| element.opacity(t)) +} + diff --git a/crates/ely_app/src/shell/chrome/command_overlay.rs b/crates/ely_app/src/shell/chrome/command_overlay.rs index bfed5fb..1a91b7e 100644 --- a/crates/ely_app/src/shell/chrome/command_overlay.rs +++ b/crates/ely_app/src/shell/chrome/command_overlay.rs @@ -1,19 +1,21 @@ use ely_browser_core::BrowserSnapshot; use ely_design_system::colors; -use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry}; use gpui::{ - AnyElement, BoxShadow, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, - SharedString, StatefulInteractiveElement, Styled, div, hsla, point, - prelude::FluentBuilder, px, rgb, rgba, + AnyElement, BoxShadow, Context, FontWeight, IntoElement, ParentElement, Styled, div, hsla, + point, px, rgb, rgba, }; use gpui_component::IconName; use crate::shell::ElyShell; +use crate::shell::chrome::animations::{blink, fade_in}; use crate::shell::chrome::command_footer::{render_command_footer, render_kbd}; use crate::shell::chrome::command_match::{ - CommandActionEntry, matching_actions, matching_bookmarks, matching_history, matching_tabs, + matching_actions, matching_bookmarks, matching_history, matching_tabs, }; -use crate::shell::chrome::render_glyph_for; +use crate::shell::chrome::command_rows::{ + render_action_rows, render_bookmark_rows, render_history_rows, render_tab_rows, +}; +use crate::shell::chrome::glass::render_inner_highlight; const COMMAND_PREFIX: &str = ">"; @@ -38,16 +40,20 @@ fn render_overlay( selected_index: usize, cx: &mut Context, ) -> AnyElement { - div() - .absolute() - .inset_0() - .bg(rgba(BACKDROP_BG)) - .flex() - .flex_col() - .items_center() - .pt(px(80.0)) - .child(render_panel(snapshot, needle, selected_index, cx)) - .into_any_element() + fade_in( + "command-overlay-backdrop", + 160, + div() + .absolute() + .inset_0() + .bg(rgba(BACKDROP_BG)) + .flex() + .flex_col() + .items_center() + .pt(px(80.0)) + .child(render_panel(snapshot, needle, selected_index, cx)), + ) + .into_any_element() } fn render_panel( @@ -68,11 +74,13 @@ fn render_panel( .bg(rgba(PANEL_BG)) .shadow(panel_shadow()) .overflow_hidden() + .relative() .flex() .flex_col() .child(render_header(query_label.clone(), needle.is_empty())) .child(render_results(snapshot, needle, selected_index, cx)) .child(render_command_footer()) + .child(render_inner_highlight(16.0)) .into_any_element() } @@ -95,9 +103,19 @@ fn render_header(query_label: String, is_empty: bool) -> AnyElement { .child( div() .flex_1() + .flex() + .items_center() + .gap(px(2.0)) .text_size(px(16.0)) .text_color(rgb(label_color)) - .child(query_label), + .child(query_label) + .child(blink( + "command-overlay-caret", + div() + .w(px(1.0)) + .h(px(18.0)) + .bg(rgb(colors::ACCENT)), + )), ) .child( div() @@ -190,236 +208,6 @@ fn render_section(label: &'static str, body: AnyElement) -> AnyElement { .into_any_element() } -fn render_tab_rows( - tabs: Vec<&BrowserTab>, - offset: usize, - selected_index: usize, - cx: &mut Context, -) -> AnyElement { - div() - .flex() - .flex_col() - .children(tabs.into_iter().enumerate().map(|(index, tab)| { - let tab_id = tab.id().clone(); - let title = tab.title().to_string(); - let host = tab.url().host().map(|host| host.to_string()); - let host_label = host - .clone() - .unwrap_or_else(|| tab.display_url()); - let initial = title.chars().next().unwrap_or('?').to_string(); - let is_selected = offset + index == selected_index; - - render_row_with_glyph( - CommandRowContent { - id: format!("cmd-tab-{index}"), - title, - hint: Some(host_label), - keys: None, - selected: is_selected, - }, - host.as_deref(), - &initial, - cx, - move |shell, window, cx| { - shell.select_tab(&tab_id, window, cx); - shell.dismiss_command_mode(window, cx); - }, - ) - })) - .into_any_element() -} - -fn render_history_rows( - entries: Vec<&HistoryEntry>, - offset: usize, - selected_index: usize, - cx: &mut Context, -) -> AnyElement { - div() - .flex() - .flex_col() - .children(entries.into_iter().enumerate().map(|(index, entry)| { - let url = entry.url().clone(); - let title = entry.title().to_string(); - let host = entry.url().host().map(|host| host.to_string()); - let display = host - .clone() - .unwrap_or_else(|| entry.url().as_str().to_string()); - let initial = title.chars().next().unwrap_or('?').to_string(); - let is_selected = offset + index == selected_index; - - render_row_with_glyph( - CommandRowContent { - id: format!("cmd-history-{index}"), - title, - hint: Some(display), - keys: None, - selected: is_selected, - }, - host.as_deref(), - &initial, - cx, - move |shell, window, cx| { - shell.open_internal_tab(url.as_str(), window, cx); - shell.dismiss_command_mode(window, cx); - }, - ) - })) - .into_any_element() -} - -fn render_action_rows( - actions: Vec<&'static CommandActionEntry>, - offset: usize, - selected_index: usize, - cx: &mut Context, -) -> AnyElement { - div() - .flex() - .flex_col() - .children(actions.into_iter().enumerate().map(|(index, action)| { - let route = action.route; - let keys = if action.keys.is_empty() { - None - } else { - Some(action.keys.to_string()) - }; - let icon = action.icon.clone(); - let is_selected = offset + index == selected_index; - - render_row( - CommandRowContent { - id: format!("cmd-action-{index}"), - title: action.title.to_string(), - hint: Some(action.hint.to_string()), - keys, - selected: is_selected, - }, - icon, - cx, - move |shell, window, cx| { - shell.open_internal_tab(route, window, cx); - shell.dismiss_command_mode(window, cx); - }, - ) - })) - .into_any_element() -} - -struct CommandRowContent { - id: String, - title: String, - hint: Option, - keys: Option, - selected: bool, -} - -fn render_row( - content: CommandRowContent, - icon: IconName, - cx: &mut Context, - handler: F, -) -> AnyElement -where - F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, -{ - let leading = div() - .size(px(24.0)) - .rounded(px(6.0)) - .bg(rgba(ROW_ICON_BG)) - .flex() - .items_center() - .justify_center() - .text_color(rgb(colors::INK_2)) - .child(icon) - .into_any_element(); - render_row_inner(content, leading, cx, handler) -} - -fn render_row_with_glyph( - content: CommandRowContent, - host: Option<&str>, - fallback_initial: &str, - cx: &mut Context, - handler: F, -) -> AnyElement -where - F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, -{ - let leading = render_glyph_for(host, fallback_initial, 24.0); - render_row_inner(content, leading, cx, handler) -} - -fn render_row_inner( - content: CommandRowContent, - leading: AnyElement, - cx: &mut Context, - handler: F, -) -> AnyElement -where - F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, -{ - let CommandRowContent { id, title, hint, keys, selected } = content; - let bg = if selected { ROW_SELECTED_BG } else { 0x00000000 }; - - div() - .id(SharedString::from(id)) - .relative() - .flex() - .items_center() - .gap(px(12.0)) - .px(px(16.0)) - .py(px(8.0)) - .bg(rgba(bg)) - .cursor_pointer() - .hover(|style| style.bg(rgba(ROW_HOVER_BG))) - .active(|style| style.opacity(0.85)) - .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) - .when(selected, |el| { - el.child( - div() - .absolute() - .left_0() - .top(px(6.0)) - .bottom(px(6.0)) - .w(px(2.0)) - .rounded(px(2.0)) - .bg(rgb(colors::ACCENT)), - ) - }) - .child(leading) - .child( - div() - .flex_1() - .min_w_0() - .flex() - .flex_col() - .gap_1() - .child( - div() - .text_size(px(13.0)) - .font_weight(FontWeight(500.0)) - .text_color(rgb(colors::INK)) - .truncate() - .child(title), - ) - .children(hint.map(|hint| { - div() - .text_size(px(11.0)) - .text_color(rgb(colors::INK_4)) - .truncate() - .child(hint) - })), - ) - .children(keys.map(|key_label| { - div() - .text_size(px(10.5)) - .text_color(rgb(colors::INK_3)) - .child(key_label) - })) - .into_any_element() -} - fn render_empty_state() -> AnyElement { div() .px(px(18.0)) @@ -430,50 +218,8 @@ fn render_empty_state() -> AnyElement { .into_any_element() } -fn render_bookmark_rows( - entries: Vec<&BookmarkEntry>, - offset: usize, - selected_index: usize, - cx: &mut Context, -) -> AnyElement { - div() - .flex() - .flex_col() - .children(entries.into_iter().enumerate().map(|(index, bookmark)| { - let url = bookmark.url().clone(); - let title = bookmark.title().to_string(); - let host = bookmark.url().host().map(|host| host.to_string()); - let display = host - .clone() - .unwrap_or_else(|| bookmark.url().as_str().to_string()); - let initial = title.chars().next().unwrap_or('?').to_string(); - let is_selected = offset + index == selected_index; - - render_row_with_glyph( - CommandRowContent { - id: format!("cmd-bookmark-{index}"), - title, - hint: Some(display), - keys: None, - selected: is_selected, - }, - host.as_deref(), - &initial, - cx, - move |shell, window, cx| { - shell.open_internal_tab(url.as_str(), window, cx); - shell.dismiss_command_mode(window, cx); - }, - ) - })) - .into_any_element() -} - const PANEL_BG: u32 = 0xfffffff5; const BACKDROP_BG: u32 = 0x140f0a3d; -const ROW_HOVER_BG: u32 = 0xc9644214; -const ROW_SELECTED_BG: u32 = 0xc964421f; -const ROW_ICON_BG: u32 = 0xffffffd9; const BADGE_BG: u32 = 0x281e140f; fn panel_shadow() -> Vec { diff --git a/crates/ely_app/src/shell/chrome/command_rows.rs b/crates/ely_app/src/shell/chrome/command_rows.rs new file mode 100644 index 0000000..7d0c6dd --- /dev/null +++ b/crates/ely_app/src/shell/chrome/command_rows.rs @@ -0,0 +1,282 @@ +use ely_design_system::colors; +use ely_domain::{BookmarkEntry, BrowserTab, HistoryEntry}; +use gpui::{ + AnyElement, Context, FontWeight, InteractiveElement, IntoElement, ParentElement, SharedString, + StatefulInteractiveElement, Styled, div, prelude::FluentBuilder, px, rgb, rgba, +}; +use gpui_component::IconName; + +use crate::shell::ElyShell; +use crate::shell::chrome::command_match::CommandActionEntry; +use crate::shell::chrome::render_glyph_for; + +pub(crate) struct CommandRowContent { + pub id: String, + pub title: String, + pub hint: Option, + pub keys: Option, + pub selected: bool, +} + +pub(crate) fn render_tab_rows( + tabs: Vec<&BrowserTab>, + offset: usize, + selected_index: usize, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .children(tabs.into_iter().enumerate().map(|(index, tab)| { + let tab_id = tab.id().clone(); + let title = tab.title().to_string(); + let host = tab.url().host().map(|host| host.to_string()); + let host_label = host.clone().unwrap_or_else(|| tab.display_url()); + let initial = title.chars().next().unwrap_or('?').to_string(); + let is_selected = offset + index == selected_index; + + render_row_with_glyph( + CommandRowContent { + id: format!("cmd-tab-{index}"), + title, + hint: Some(host_label), + keys: None, + selected: is_selected, + }, + host.as_deref(), + &initial, + cx, + move |shell, window, cx| { + shell.select_tab(&tab_id, window, cx); + shell.dismiss_command_mode(window, cx); + }, + ) + })) + .into_any_element() +} + +pub(crate) fn render_history_rows( + entries: Vec<&HistoryEntry>, + offset: usize, + selected_index: usize, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .children(entries.into_iter().enumerate().map(|(index, entry)| { + let url = entry.url().clone(); + let title = entry.title().to_string(); + let host = entry.url().host().map(|host| host.to_string()); + let display = host + .clone() + .unwrap_or_else(|| entry.url().as_str().to_string()); + let initial = title.chars().next().unwrap_or('?').to_string(); + let is_selected = offset + index == selected_index; + + render_row_with_glyph( + CommandRowContent { + id: format!("cmd-history-{index}"), + title, + hint: Some(display), + keys: None, + selected: is_selected, + }, + host.as_deref(), + &initial, + cx, + move |shell, window, cx| { + shell.open_internal_tab(url.as_str(), window, cx); + shell.dismiss_command_mode(window, cx); + }, + ) + })) + .into_any_element() +} + +pub(crate) fn render_bookmark_rows( + entries: Vec<&BookmarkEntry>, + offset: usize, + selected_index: usize, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .children(entries.into_iter().enumerate().map(|(index, bookmark)| { + let url = bookmark.url().clone(); + let title = bookmark.title().to_string(); + let host = bookmark.url().host().map(|host| host.to_string()); + let display = host + .clone() + .unwrap_or_else(|| bookmark.url().as_str().to_string()); + let initial = title.chars().next().unwrap_or('?').to_string(); + let is_selected = offset + index == selected_index; + + render_row_with_glyph( + CommandRowContent { + id: format!("cmd-bookmark-{index}"), + title, + hint: Some(display), + keys: None, + selected: is_selected, + }, + host.as_deref(), + &initial, + cx, + move |shell, window, cx| { + shell.open_internal_tab(url.as_str(), window, cx); + shell.dismiss_command_mode(window, cx); + }, + ) + })) + .into_any_element() +} + +pub(crate) fn render_action_rows( + actions: Vec<&'static CommandActionEntry>, + offset: usize, + selected_index: usize, + cx: &mut Context, +) -> AnyElement { + div() + .flex() + .flex_col() + .children(actions.into_iter().enumerate().map(|(index, action)| { + let route = action.route; + let keys = if action.keys.is_empty() { + None + } else { + Some(action.keys.to_string()) + }; + let icon = action.icon.clone(); + let is_selected = offset + index == selected_index; + + render_row( + CommandRowContent { + id: format!("cmd-action-{index}"), + title: action.title.to_string(), + hint: Some(action.hint.to_string()), + keys, + selected: is_selected, + }, + icon, + cx, + move |shell, window, cx| { + shell.open_internal_tab(route, window, cx); + shell.dismiss_command_mode(window, cx); + }, + ) + })) + .into_any_element() +} + +fn render_row( + content: CommandRowContent, + icon: IconName, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, +{ + let leading = div() + .size(px(24.0)) + .rounded(px(6.0)) + .bg(rgba(ROW_ICON_BG)) + .flex() + .items_center() + .justify_center() + .text_color(rgb(colors::INK_2)) + .child(icon) + .into_any_element(); + render_row_inner(content, leading, cx, handler) +} + +fn render_row_with_glyph( + content: CommandRowContent, + host: Option<&str>, + fallback_initial: &str, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, +{ + let leading = render_glyph_for(host, fallback_initial, 24.0); + render_row_inner(content, leading, cx, handler) +} + +fn render_row_inner( + content: CommandRowContent, + leading: AnyElement, + cx: &mut Context, + handler: F, +) -> AnyElement +where + F: Fn(&mut ElyShell, &mut gpui::Window, &mut Context) + 'static, +{ + let CommandRowContent { id, title, hint, keys, selected } = content; + let bg = if selected { ROW_SELECTED_BG } else { 0x00000000 }; + + div() + .id(SharedString::from(id)) + .relative() + .flex() + .items_center() + .gap(px(12.0)) + .px(px(16.0)) + .py(px(8.0)) + .bg(rgba(bg)) + .cursor_pointer() + .hover(|style| style.bg(rgba(ROW_HOVER_BG))) + .active(|style| style.opacity(0.85)) + .on_click(cx.listener(move |shell, _, window, cx| handler(shell, window, cx))) + .when(selected, |el| { + el.child( + div() + .absolute() + .left_0() + .top(px(6.0)) + .bottom(px(6.0)) + .w(px(2.0)) + .rounded(px(2.0)) + .bg(rgb(colors::ACCENT)), + ) + }) + .child(leading) + .child( + div() + .flex_1() + .min_w_0() + .flex() + .flex_col() + .gap_1() + .child( + div() + .text_size(px(13.0)) + .font_weight(FontWeight(500.0)) + .text_color(rgb(colors::INK)) + .truncate() + .child(title), + ) + .children(hint.map(|hint| { + div() + .text_size(px(11.0)) + .text_color(rgb(colors::INK_4)) + .truncate() + .child(hint) + })), + ) + .children(keys.map(|key_label| { + div() + .text_size(px(10.5)) + .text_color(rgb(colors::INK_3)) + .child(key_label) + })) + .into_any_element() +} + +const ROW_HOVER_BG: u32 = 0xc9644214; +const ROW_SELECTED_BG: u32 = 0xc964421f; +const ROW_ICON_BG: u32 = 0xffffffd9; diff --git a/crates/ely_app/src/shell/chrome/glass.rs b/crates/ely_app/src/shell/chrome/glass.rs new file mode 100644 index 0000000..96ded72 --- /dev/null +++ b/crates/ely_app/src/shell/chrome/glass.rs @@ -0,0 +1,40 @@ +use gpui::{AnyElement, IntoElement, ParentElement, Styled, div, px, rgba}; + +/// Inner highlight rings on every glass panel. +/// +/// CSS designs reach for `box-shadow: inset 0 0 0 1px rgba(255,255,255,0.5)` +/// to draw a 1 px highlight inside a translucent panel. GPUI 0.2.2's +/// `BoxShadow` has no inset flag, so this module composites the same effect +/// from real GPUI primitives: an absolutely-positioned overlay div that +/// owns the inner border and sits above the panel content, scoped to the +/// panel's rounded clip. +/// +/// The highlight color matches the design's `--ely-shadow-window` stack: +/// 50% white border + 70% white top-edge highlight. +const HIGHLIGHT_BORDER: u32 = 0xffffff80; +const HIGHLIGHT_TOP_EDGE: u32 = 0xffffffb3; + +/// Draw a single absolute overlay that paints the inset highlight ring into +/// the parent panel. Caller is responsible for `.relative()` on the parent +/// and matching `.rounded(...)` so the overlay's clip lines up. +pub(crate) fn render_inner_highlight(radius_px: f32) -> AnyElement { + div() + .absolute() + .inset_0() + .rounded(px(radius_px)) + .border_1() + .border_color(rgba(HIGHLIGHT_BORDER)) + // The 1 px top-edge highlight from the design is drawn via a child + // div pinned to the top edge — GPUI doesn't have asymmetric border + // colors, so a 1-pixel-tall sliver is the cleanest substitute. + .child( + div() + .absolute() + .top_0() + .left(px(1.0)) + .right(px(1.0)) + .h(px(1.0)) + .bg(rgba(HIGHLIGHT_TOP_EDGE)), + ) + .into_any_element() +} diff --git a/crates/ely_app/src/shell/chrome/mod.rs b/crates/ely_app/src/shell/chrome/mod.rs index 578ad06..3a8015d 100644 --- a/crates/ely_app/src/shell/chrome/mod.rs +++ b/crates/ely_app/src/shell/chrome/mod.rs @@ -1,9 +1,12 @@ +pub(crate) mod animations; pub(crate) mod appearance_form; pub(crate) mod appearance_layout_cards; pub(crate) mod brand_glyph; pub(crate) mod command_footer; pub(crate) mod command_match; pub(crate) mod command_overlay; +pub(crate) mod command_rows; +pub(crate) mod glass; pub(crate) mod home; pub(crate) mod plugin_detail_view; pub(crate) mod plugin_labels; diff --git a/crates/ely_app/src/shell/chrome/sidebar.rs b/crates/ely_app/src/shell/chrome/sidebar.rs index 3de2493..a951dbb 100644 --- a/crates/ely_app/src/shell/chrome/sidebar.rs +++ b/crates/ely_app/src/shell/chrome/sidebar.rs @@ -9,6 +9,7 @@ use gpui::{ use gpui_component::{IconName, StyledExt, scroll::ScrollableElement}; use crate::shell::ElyShell; +use crate::shell::chrome::glass::render_inner_highlight; use crate::shell::chrome::{render_glyph_for, render_sidebar_header}; impl ElyShell { @@ -28,6 +29,7 @@ impl ElyShell { .bg(rgba(panel_color)) .shadow(panel_shadow()) .overflow_hidden() + .relative() .child(render_sidebar_header(self, snapshot, cx)) .child( div() @@ -58,6 +60,7 @@ impl ElyShell { .child(self.render_new_tab_row(cx)), ) .child(self.render_sidebar_footer(snapshot, cx)) + .child(render_inner_highlight(spacing::RADIUS_CARD)) .into_any_element() } @@ -411,17 +414,32 @@ fn section_label(label: &'static str) -> impl IntoElement { pub(crate) const ACTIVE_NAV_BG: u32 = 0xffffffd9; const UNREAD_BADGE_BG: u32 = 0x281e140f; -/// Maps appearance translucency_pct to a panel rgba u32. +/// Maps appearance translucency_pct to a panel rgba u32 tinted by the +/// active wallpaper theme. /// -/// translucency_pct 0 → alpha 0xff (fully opaque) -/// translucency_pct 100 → alpha 0xb3 (mostly opaque, ~70%) so panels stay -/// readable without backdrop blur. +/// GPUI 0.2.2 has no backdrop-filter, so a true sample-and-blur is +/// impossible. Instead the panel base RGB is pre-tinted with the +/// wallpaper's character (warm cream for Dawn, lavender for Violet, etc.) +/// so the wallpaper's mood reads through every translucent panel without +/// any extra layer or shader. Combined with the user-controlled +/// translucency_pct it gives the design's "frosted glass tied to the +/// wallpaper" effect entirely from real GPUI primitives. 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 + let rgb = wallpaper_panel_rgb(snapshot.appearance.wallpaper()); + (rgb << 8) | alpha +} + +fn wallpaper_panel_rgb(theme: ely_domain::WallpaperTheme) -> u32 { + match theme { + ely_domain::WallpaperTheme::Dawn => 0xfaf6f0, + ely_domain::WallpaperTheme::Violet => 0xf6f3f8, + ely_domain::WallpaperTheme::Mint => 0xf3f6f1, + ely_domain::WallpaperTheme::Slate => 0xeff1f4, + } } pub(crate) fn panel_shadow() -> Vec { diff --git a/crates/ely_app/src/shell/chrome/sidebar_header.rs b/crates/ely_app/src/shell/chrome/sidebar_header.rs index f62d801..0d7eea8 100644 --- a/crates/ely_app/src/shell/chrome/sidebar_header.rs +++ b/crates/ely_app/src/shell/chrome/sidebar_header.rs @@ -9,6 +9,7 @@ use gpui::{ use gpui_component::IconName; use crate::shell::ElyShell; +use crate::shell::chrome::animations::fade_in; pub(crate) fn render_sidebar_header( shell: &ElyShell, @@ -162,7 +163,7 @@ fn render_workspace_disclosure( ) -> AnyElement { let active_id = snapshot.active_space_id.clone(); - div() + let body = div() .flex() .flex_col() .gap(px(2.0)) @@ -179,8 +180,9 @@ fn render_workspace_disclosure( render_disclosure_row(index, space, &active_id, cx) }), ) - .child(render_disclosure_footer(cx)) - .into_any_element() + .child(render_disclosure_footer(cx)); + + fade_in("workspace-disclosure", 140, body).into_any_element() } fn render_disclosure_row( diff --git a/crates/ely_app/src/shell/render.rs b/crates/ely_app/src/shell/render.rs index c02da52..2549d2d 100644 --- a/crates/ely_app/src/shell/render.rs +++ b/crates/ely_app/src/shell/render.rs @@ -8,6 +8,7 @@ use gpui::{ }; use super::chrome::command_match::visible_command_rows; +use super::chrome::glass::render_inner_highlight; use super::chrome::{ panel_bg, panel_shadow, render_command_overlay, render_topbar as render_topbar_chrome, render_wallpaper, @@ -136,6 +137,7 @@ impl ElyShell { .flex_1() .h_full() .min_w_0() + .relative() .flex() .flex_col() .rounded(px(spacing::RADIUS_CARD)) @@ -149,6 +151,7 @@ impl ElyShell { .overflow_hidden() .child(self.render_content_area(snapshot, active_tab, cx)), ) + .child(render_inner_highlight(spacing::RADIUS_CARD)) .into_any_element() } diff --git a/crates/ely_app/src/shell/sidebar.rs b/crates/ely_app/src/shell/sidebar.rs index 8965e99..eef5d9c 100644 --- a/crates/ely_app/src/shell/sidebar.rs +++ b/crates/ely_app/src/shell/sidebar.rs @@ -13,6 +13,7 @@ use gpui_component::{ button::{Button, ButtonVariants}, }; +use super::chrome::glass::render_inner_highlight; use super::chrome::panel_bg; use super::{ElyShell, ShellState, render::tab_profile_label}; use crate::ToggleSidebar; @@ -37,6 +38,7 @@ impl ElyShell { div() .w(px(sidebar_width)) .h_full() + .relative() .flex() .flex_col() .items_center() @@ -88,6 +90,7 @@ impl ElyShell { self.render_compact_archived_button(index, archived_tab, cx) }, )) + .child(render_inner_highlight(spacing::RADIUS_CARD)) .into_any_element() }