diff --git a/Cargo.lock b/Cargo.lock index 14be49b0..722e256b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1258,7 +1258,7 @@ dependencies = [ [[package]] name = "collections" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "gpui_util", "indexmap", @@ -1812,7 +1812,7 @@ dependencies = [ [[package]] name = "derive_refineable" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "proc-macro2", "quote", @@ -3003,7 +3003,7 @@ dependencies = [ [[package]] name = "gpui" version = "0.2.2" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "accesskit", "anyhow", @@ -3232,7 +3232,7 @@ dependencies = [ [[package]] name = "gpui_linux" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "accesskit", "accesskit_unix", @@ -3286,7 +3286,7 @@ dependencies = [ [[package]] name = "gpui_macos" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "accesskit", "accesskit_macos", @@ -3333,7 +3333,7 @@ dependencies = [ [[package]] name = "gpui_macros" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "heck 0.5.0", "proc-macro2", @@ -3344,7 +3344,7 @@ dependencies = [ [[package]] name = "gpui_platform" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "console_error_panic_hook", "gpui", @@ -3357,7 +3357,7 @@ dependencies = [ [[package]] name = "gpui_shared_string" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "schemars", "serde", @@ -3367,7 +3367,7 @@ dependencies = [ [[package]] name = "gpui_util" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "log", @@ -3377,7 +3377,7 @@ dependencies = [ [[package]] name = "gpui_web" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "console_error_panic_hook", @@ -3401,7 +3401,7 @@ dependencies = [ [[package]] name = "gpui_wgpu" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "bytemuck", @@ -3430,7 +3430,7 @@ dependencies = [ [[package]] name = "gpui_windows" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "accesskit", "accesskit_windows", @@ -3740,7 +3740,7 @@ dependencies = [ [[package]] name = "http_client" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "async-compression", @@ -4751,7 +4751,7 @@ dependencies = [ [[package]] name = "media" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "bindgen", @@ -5678,7 +5678,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perf" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "collections", "serde", @@ -6668,7 +6668,7 @@ dependencies = [ [[package]] name = "refineable" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "derive_refineable", ] @@ -7122,7 +7122,7 @@ dependencies = [ [[package]] name = "scheduler" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "async-task", "backtrace", @@ -7755,7 +7755,7 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "sum_tree" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "heapless", "log", @@ -9133,7 +9133,7 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "util" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "async-fs", @@ -9172,7 +9172,7 @@ dependencies = [ [[package]] name = "util_macros" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "perf", "quote", @@ -11105,7 +11105,7 @@ dependencies = [ [[package]] name = "zlog" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "anyhow", "chrono", @@ -11122,7 +11122,7 @@ checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" [[package]] name = "ztracing" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" dependencies = [ "tracing", "tracing-subscriber", @@ -11133,7 +11133,7 @@ dependencies = [ [[package]] name = "ztracing_macro" version = "0.1.0" -source = "git+https://github.com/BumpyClock/gpui?rev=24cfa81ca2a9f954b96320e14b393dd519078618#24cfa81ca2a9f954b96320e14b393dd519078618" +source = "git+https://github.com/BumpyClock/gpui?rev=4332ea7deae4838c12bad6ea64292ca22a33cf98#4332ea7deae4838c12bad6ea64292ca22a33cf98" [[package]] name = "zune-core" diff --git a/Cargo.toml b/Cargo.toml index 5fe438bd..9216cf83 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,8 +28,8 @@ gpui-component-macros = { path = "crates/macros", version = "0.5.1" } gpui-component-assets = { path = "crates/assets", version = "0.5.1" } story = { path = "crates/story" } -gpui = { git = "https://github.com/BumpyClock/gpui", rev = "24cfa81ca2a9f954b96320e14b393dd519078618", features = ["font-kit"] } -gpui_platform = { git = "https://github.com/BumpyClock/gpui", rev = "24cfa81ca2a9f954b96320e14b393dd519078618", features = ["font-kit"] } +gpui = { git = "https://github.com/BumpyClock/gpui", rev = "4332ea7deae4838c12bad6ea64292ca22a33cf98", features = ["font-kit"] } +gpui_platform = { git = "https://github.com/BumpyClock/gpui", rev = "4332ea7deae4838c12bad6ea64292ca22a33cf98", features = ["font-kit"] } gpui-macros = "0.2.2" sum-tree = { version = "0.2.0", package = "zed-sum-tree" } # reqwest = { version = "0.12.15-zed", package = "zed-reqwest" } diff --git a/crates/story/src/stories/floating_sidebar_story.rs b/crates/story/src/stories/floating_sidebar_story.rs index 58c5c5c6..efcd90e1 100644 --- a/crates/story/src/stories/floating_sidebar_story.rs +++ b/crates/story/src/stories/floating_sidebar_story.rs @@ -4,7 +4,8 @@ use gpui::{ }; use gpui_component::{ - ActiveTheme, FloatingSidebar, Icon, IconName, Side, h_flex, + ActiveTheme, ElevationToken, FloatingSidebar, Icon, IconName, Side, h_flex, + radio::RadioGroup, sidebar::{SidebarFooter, SidebarGroup, SidebarHeader, SidebarMenu, SidebarMenuItem}, switch::Switch, v_flex, @@ -17,6 +18,7 @@ pub struct FloatingSidebarStory { collapsed: bool, side: Side, wide_inset: bool, + elevation: ElevationToken, } impl FloatingSidebarStory { @@ -26,6 +28,7 @@ impl FloatingSidebarStory { collapsed: false, side: Side::Left, wide_inset: false, + elevation: ElevationToken::Sm, } } @@ -33,6 +36,14 @@ impl FloatingSidebarStory { cx.new(|cx| Self::new(window, cx)) } + fn elevation_index(&self) -> usize { + match self.elevation { + ElevationToken::None => 0, + ElevationToken::Lg => 2, + _ => 1, + } + } + fn render_example( &self, id: &'static str, @@ -46,13 +57,14 @@ impl FloatingSidebarStory { collapsed_width } else { sidebar_width - } + inset; + } + inset * 2.0; let sidebar = FloatingSidebar::new(id) .side(self.side) .collapsed(self.collapsed) .width(sidebar_width) .inset(inset) + .elevation(self.elevation) .header_with(|collapsed, _, _cx| { SidebarHeader::new() .child(Icon::new(IconName::PanelLeft).size_4()) @@ -166,6 +178,20 @@ impl Render for FloatingSidebarStory { this.wide_inset = *checked; cx.notify(); })), + ) + .child( + RadioGroup::horizontal("floating-sidebar-elevation") + .children(["None", "Small", "Large"]) + .selected_index(Some(self.elevation_index())) + .on_click(cx.listener(|this, selected_ix: &usize, _, cx| { + this.elevation = match selected_ix { + 0 => ElevationToken::None, + 1 => ElevationToken::Sm, + 2 => ElevationToken::Lg, + _ => return, + }; + cx.notify(); + })), ), ) .child(self.render_example("floating-sidebar-example", inset, window, cx)), diff --git a/crates/ui/src/floating_sidebar/mod.rs b/crates/ui/src/floating_sidebar/mod.rs index 7777ad39..956b0481 100644 --- a/crates/ui/src/floating_sidebar/mod.rs +++ b/crates/ui/src/floating_sidebar/mod.rs @@ -39,6 +39,9 @@ impl FloatingSidebarState { } /// A floating sidebar that composes SidebarShell and Sidebar with internal resize handling. +/// +/// The default elevation is [`ElevationToken::Sm`]. Large elevations may require +/// a larger inset near window edges to avoid native-window clipping. #[derive(IntoElement)] pub struct FloatingSidebar { id: ElementId, @@ -76,7 +79,7 @@ impl FloatingSidebar { inset: Some(DEFAULT_INSET), top_inset: px(0.0), blur_enabled: None, - elevation: ElevationToken::Lg, + elevation: ElevationToken::Sm, on_resize_end: None, } } @@ -161,7 +164,10 @@ impl FloatingSidebar { self } - /// Set the shadow elevation level for the sidebar panel. + /// Set the shadow elevation level for the complete sidebar panel. + /// + /// Default is [`ElevationToken::Sm`]. Large elevations may require a larger + /// inset near window edges to avoid native-window clipping. pub fn elevation(mut self, elevation: ElevationToken) -> Self { self.elevation = elevation; self @@ -351,6 +357,7 @@ impl RenderOnce for FloatingSidebar { .collapsed(collapsed) .width(expanded_width) .animate_width(false) + .bg(gpui::transparent_black()) .refine_style(&style), ) .child(resize_tracker) @@ -479,6 +486,12 @@ mod tests { #[gpui::test] fn test_floating_sidebar_builder(_cx: &mut gpui::TestAppContext) { + let default_sidebar = FloatingSidebar::::new("default-floating-sidebar"); + assert_eq!(default_sidebar.elevation, ElevationToken::Sm); + assert_eq!(default_sidebar.width, DEFAULT_WIDTH); + assert_eq!(default_sidebar.inset, Some(DEFAULT_INSET)); + assert_eq!(default_sidebar.resizer_width, DEFAULT_RESIZER_WIDTH); + let sidebar = FloatingSidebar::::new("floating-sidebar") .side(Side::Right) .collapsed(true) @@ -503,8 +516,16 @@ mod tests { assert_eq!(sidebar.inset, Some(px(6.0))); assert_eq!(sidebar.top_inset, px(12.0)); assert_eq!(sidebar.blur_enabled, Some(false)); - assert!(matches!(sidebar.elevation, ElevationToken::Md)); + assert_eq!(sidebar.elevation, ElevationToken::Md); assert!(sidebar.on_resize_end.is_some()); + + let no_shadow = + FloatingSidebar::::new("no-shadow").elevation(ElevationToken::None); + assert_eq!(no_shadow.elevation, ElevationToken::None); + + let large_shadow = + FloatingSidebar::::new("large-shadow").elevation(ElevationToken::Lg); + assert_eq!(large_shadow.elevation, ElevationToken::Lg); } #[gpui::test] diff --git a/crates/ui/src/scroll/scrollable_mask.rs b/crates/ui/src/scroll/scrollable_mask.rs index 36a57d23..06046ce9 100644 --- a/crates/ui/src/scroll/scrollable_mask.rs +++ b/crates/ui/src/scroll/scrollable_mask.rs @@ -108,7 +108,11 @@ impl Element for ScrollableMask { let line_height = window.line_height(); let bounds = hitbox.bounds; - window.with_content_mask(Some(ContentMask { bounds }), |window| { + let content_mask = ContentMask { + bounds, + ..Default::default() + }; + window.with_content_mask(Some(content_mask), |window| { if let Some(color) = self.debug { window.paint_quad(PaintQuad { bounds, diff --git a/crates/ui/src/scroll/scrollbar.rs b/crates/ui/src/scroll/scrollbar.rs index 01e31ee8..67de63f3 100644 --- a/crates/ui/src/scroll/scrollbar.rs +++ b/crates/ui/src/scroll/scrollbar.rs @@ -531,7 +531,11 @@ impl Element for Scrollbar { window: &mut Window, cx: &mut App, ) -> Self::PrepaintState { - let hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| { + let content_mask = ContentMask { + bounds, + ..Default::default() + }; + let hitbox = window.with_content_mask(Some(content_mask), |window| { window.insert_hitbox(bounds, HitboxBehavior::Normal) }); @@ -698,7 +702,11 @@ impl Element for Scrollbar { ) }; - let bar_hitbox = window.with_content_mask(Some(ContentMask { bounds }), |window| { + let content_mask = ContentMask { + bounds, + ..Default::default() + }; + let bar_hitbox = window.with_content_mask(Some(content_mask), |window| { window.insert_hitbox(bounds, gpui::HitboxBehavior::Normal) }); @@ -756,6 +764,7 @@ impl Element for Scrollbar { window.with_content_mask( Some(ContentMask { bounds: hitbox_bounds, + ..Default::default() }), |window| { for state in prepaint.states.iter() { diff --git a/crates/ui/src/sidebar_shell/mod.rs b/crates/ui/src/sidebar_shell/mod.rs index c4d00174..34aa8d7a 100644 --- a/crates/ui/src/sidebar_shell/mod.rs +++ b/crates/ui/src/sidebar_shell/mod.rs @@ -2,7 +2,7 @@ //! //! This module provides a reusable sidebar shell that handles: //! - Resizable panel with configurable min/max width constraints -//! - Built-in shadow elevation effects +//! - Theme-controlled panel shadow elevation //! - Glass surface effects via SurfacePreset //! - Draggable resize handle on the inner edge //! - Support for left/right placement @@ -88,14 +88,17 @@ pub fn sidebar_shadow() -> Vec { ] } -/// A resizable sidebar panel with built-in shadow and glass surface effects. +/// A resizable sidebar panel with a theme-controlled shadow and glass surface effects. /// /// SidebarShell provides a container for sidebar content that handles: /// - Absolute positioning with configurable inset from window edges -/// - 3-layer shadow elevation for depth perception +/// - A single complete panel shadow, controlled by its elevation /// - Glass blur and noise effects via SurfacePreset::panel() /// - Draggable resize handle with hover feedback /// +/// The default elevation is [`ElevationToken::Sm`]. Large elevations may require +/// a larger inset near window edges to avoid native-window clipping. +/// /// The component uses a builder pattern for configuration and implements /// `ParentElement` for adding child content, `Styled` for style refinement, /// and `RenderOnce` for rendering. @@ -110,12 +113,10 @@ pub fn sidebar_shadow() -> Vec { /// /// ```text /// +-- Outer Container (absolute, inset from edges) --+ -/// | +-- Shadow Wrapper (3-layer shadow) ----------+ | -/// | | +-- Surface (glass effects) -------------+ | | -/// | | | | | | -/// | | | [Child Content] | | | -/// | | | | | | -/// | | +----------------------------------------+ | | +/// | +-- Surface (shadow + glass effects) --------+ | +/// | | | | +/// | | [Child Content] | | +/// | | | | /// | +--------------------------------------------+ | /// | [Resizer Handle] (on inner edge) | /// +------------------------------------------------+ @@ -137,8 +138,8 @@ pub struct SidebarShell { on_resize_start: Option>, /// Callback invoked when resize ends (mouse up). on_resize_end: Option>, - /// Shadow elevation level for the sidebar panel. - /// Default: `ElevationToken::Lg` for a prominent floating appearance. + /// Shadow elevation level for the complete sidebar panel. + /// Default: `ElevationToken::Sm`. elevation: ElevationToken, /// Placement side (left or right). side: Side, @@ -196,7 +197,7 @@ impl SidebarShell { resizer_hover_bg: None, on_resize_start: None, on_resize_end: None, - elevation: ElevationToken::Lg, + elevation: ElevationToken::Sm, side, inset: None, top_inset: px(0.0), @@ -322,11 +323,11 @@ impl SidebarShell { self } - /// Sets the shadow elevation level for the sidebar panel. + /// Sets the shadow elevation level for the complete sidebar panel. /// - /// Controls the shadow depth and intensity using the theme's elevation - /// system. Higher values create more pronounced floating appearance. - /// Default: `ElevationToken::Lg`. + /// Controls the single panel shadow using the theme's elevation system. + /// Default: `ElevationToken::Sm`. Large elevations may require a larger + /// inset near window edges to avoid native-window clipping. /// /// # Example /// @@ -375,6 +376,7 @@ impl RenderOnce for SidebarShell { .unwrap_or_else(|| GlobalState::global(cx).blur_enabled()); let sidebar_surface = SurfacePreset::panel() + .with_elevation(self.elevation) .wrap_with_bounds( div(), sidebar_width, @@ -411,11 +413,7 @@ impl RenderOnce for SidebarShell { el.right(inset) } }) - .child( - self.elevation - .apply(div().id("sidebar-shell-shadow-wrapper").size_full(), cx) - .child(sidebar_surface), - ) + .child(sidebar_surface) .child( div() .id("sidebar-shell-resizer") @@ -452,3 +450,31 @@ impl RenderOnce for SidebarShell { outer } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_sidebar_shell_defaults_and_elevation_builder() { + let default_shell = SidebarShell::left(px(260.0)); + assert_eq!(default_shell.elevation, ElevationToken::Sm); + assert_eq!(default_shell.min_width, px(DEFAULT_MIN_WIDTH)); + assert_eq!(default_shell.max_width, px(DEFAULT_MAX_WIDTH)); + assert_eq!(default_shell.resizer_width, px(DEFAULT_RESIZER_WIDTH)); + assert_eq!(default_shell.inset, None); + assert_eq!(default_shell.side, Side::Left); + + let no_shadow = SidebarShell::left(px(260.0)).elevation(ElevationToken::None); + assert_eq!(no_shadow.elevation, ElevationToken::None); + + let large_shadow = SidebarShell::right(px(300.0)) + .inset(px(12.0)) + .resizer_width(px(8.0)) + .elevation(ElevationToken::Lg); + assert_eq!(large_shadow.elevation, ElevationToken::Lg); + assert_eq!(large_shadow.inset, Some(px(12.0))); + assert_eq!(large_shadow.resizer_width, px(8.0)); + assert_eq!(large_shadow.side, Side::Right); + } +} diff --git a/crates/ui/src/surface/mod.rs b/crates/ui/src/surface/mod.rs index 023e1f8b..b8d6f911 100644 --- a/crates/ui/src/surface/mod.rs +++ b/crates/ui/src/surface/mod.rs @@ -347,10 +347,9 @@ impl SurfacePreset { /// Wraps content in a surface container with all configured effects. /// /// This method creates a complete surface element with: - /// - Background color with transparency - /// - Backdrop blur (if enabled and configured) - /// - Border/stroke styling - /// - Elevation shadows + /// - One rounded clip shared by the background, backdrop blur, noise, and content + /// - Border/stroke styling using the same rounded geometry + /// - Elevation shadows painted outside the rounded clip /// - Noise overlay (if blur is enabled) pub fn wrap_with_bounds( &self, @@ -392,10 +391,9 @@ impl SurfacePreset { surface = elevation.apply(surface, cx); if should_render_noise { - surface = surface.child(render_noise_overlay( + surface = surface.child(render_noise_overlay_unclipped( width, height, - radius, noise_opacity, scale_factor, )); @@ -483,40 +481,163 @@ pub fn render_noise_overlay( opacity: f32, scale_factor: f32, ) -> impl IntoElement { - let tile_size_value = (GLASS_NOISE_TILE_SIZE_BASE / scale_factor.max(1.0)).round(); - let tile_size = px(tile_size_value); - let cols = ((width / tile_size).max(0.0).ceil() as usize).max(1) + 12; - let rows = ((height / tile_size).max(0.0).ceil() as usize).max(1) + 12; - let tiled_width = px(tile_size_value * cols as f32); - let tiled_height = px(tile_size_value * rows as f32); - let tiles = cols.saturating_mul(rows); - div() .absolute() .inset_0() .size_full() - .overflow_hidden() .rounded(radius) - .child( + .overflow_hidden() + .child(render_noise_tiles(width, height, opacity, scale_factor)) +} + +fn render_noise_overlay_unclipped( + width: Pixels, + height: Pixels, + opacity: f32, + scale_factor: f32, +) -> Div { + div() + .absolute() + .inset_0() + .size_full() + .child(render_noise_tiles(width, height, opacity, scale_factor)) +} + +#[derive(Clone, Copy, Debug, PartialEq)] +struct NoiseTileLayout { + tile_size: Pixels, + cols: usize, + rows: usize, + last_tile_width: Pixels, + last_tile_height: Pixels, +} + +fn noise_tile_layout(width: Pixels, height: Pixels, scale_factor: f32) -> NoiseTileLayout { + let tile_size_value = (GLASS_NOISE_TILE_SIZE_BASE / scale_factor.max(1.0)).round(); + let tile_size = px(tile_size_value); + + if width <= px(0.0) || height <= px(0.0) { + return NoiseTileLayout { + tile_size, + cols: 0, + rows: 0, + last_tile_width: px(0.0), + last_tile_height: px(0.0), + }; + } + + let cols = (width / tile_size).ceil() as usize; + let rows = (height / tile_size).ceil() as usize; + let last_tile_width = width - px(tile_size_value * cols.saturating_sub(1) as f32); + let last_tile_height = height - px(tile_size_value * rows.saturating_sub(1) as f32); + + NoiseTileLayout { + tile_size, + cols, + rows, + last_tile_width, + last_tile_height, + } +} + +fn render_noise_tiles(width: Pixels, height: Pixels, opacity: f32, scale_factor: f32) -> Div { + let layout = noise_tile_layout(width, height, scale_factor); + let tiles = layout.cols.saturating_mul(layout.rows); + + div() + .absolute() + .top_0() + .left_0() + .w(width) + .h(height) + .flex() + .flex_wrap() + .items_start() + .justify_start() + .children((0..tiles).map(move |ix| { + let col = ix % layout.cols; + let row = ix / layout.cols; + let tile_width = if col + 1 == layout.cols { + layout.last_tile_width + } else { + layout.tile_size + }; + let tile_height = if row + 1 == layout.rows { + layout.last_tile_height + } else { + layout.tile_size + }; + div() - .absolute() - .top_0() - .left_0() - .w(tiled_width) - .h(tiled_height) - .flex() - .flex_wrap() - .items_start() - .justify_start() - .children((0..tiles).map(move |_| { + .w(tile_width) + .h(tile_height) + .flex_none() + .overflow_hidden() + .child( img(ImageSource::Resource(Resource::Embedded( GLASS_NOISE_ASSET_PATH.into(), ))) - .w(tile_size) - .h(tile_size) + .w(layout.tile_size) + .h(layout.tile_size) .flex_none() - .object_fit(ObjectFit::Cover) - .opacity(opacity) - })), - ) + .object_fit(ObjectFit::Fill) + .opacity(opacity), + ) + })) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_surface_preset_builder() { + let panel = SurfacePreset::panel(); + assert_eq!(panel.elevation, ElevationToken::Lg); + assert!(panel.use_theme_elevation_defaults); + + let overridden = panel.with_elevation(ElevationToken::Sm); + assert_eq!(overridden.elevation, ElevationToken::Sm); + assert!(!overridden.use_theme_elevation_defaults); + } + + #[test] + fn noise_tile_layout_handles_exact_multiples() { + let layout = noise_tile_layout(px(256.0), px(128.0), 1.0); + + assert_eq!(layout.cols, 2); + assert_eq!(layout.rows, 1); + assert_eq!(layout.last_tile_width, px(128.0)); + assert_eq!(layout.last_tile_height, px(128.0)); + } + + #[test] + fn noise_tile_layout_handles_partial_edges() { + let layout = noise_tile_layout(px(200.0), px(150.0), 1.0); + + assert_eq!(layout.cols, 2); + assert_eq!(layout.rows, 2); + assert_eq!(layout.last_tile_width, px(72.0)); + assert_eq!(layout.last_tile_height, px(22.0)); + } + + #[test] + fn noise_tile_layout_handles_dimensions_smaller_than_a_tile() { + let layout = noise_tile_layout(px(40.0), px(60.0), 1.0); + + assert_eq!(layout.cols, 1); + assert_eq!(layout.rows, 1); + assert_eq!(layout.last_tile_width, px(40.0)); + assert_eq!(layout.last_tile_height, px(60.0)); + } + + #[test] + fn noise_tile_layout_handles_zero_dimensions() { + let layout = noise_tile_layout(px(0.0), px(0.0), 1.0); + + assert_eq!(layout.cols, 0); + assert_eq!(layout.rows, 0); + assert_eq!(layout.last_tile_width, px(0.0)); + assert_eq!(layout.last_tile_height, px(0.0)); + } } diff --git a/crates/ui/src/virtual_list.rs b/crates/ui/src/virtual_list.rs index 4aef0239..c4edd9a1 100644 --- a/crates/ui/src/virtual_list.rs +++ b/crates/ui/src/virtual_list.rs @@ -636,7 +636,10 @@ impl Element for VirtualList { let items = (self.render_items)(visible_range.clone(), window, cx); - let content_mask = ContentMask { bounds }; + let content_mask = ContentMask { + bounds, + ..Default::default() + }; window.with_content_mask(Some(content_mask), |window| { for (mut item, ix) in items.into_iter().zip(visible_range.clone()) { let item_origin = content_bounds.origin diff --git a/crates/webview/src/lib.rs b/crates/webview/src/lib.rs index 382b0c38..90d15109 100644 --- a/crates/webview/src/lib.rs +++ b/crates/webview/src/lib.rs @@ -275,7 +275,11 @@ impl Element for WebViewElement { _: &mut App, ) { let bounds = hitbox.clone().map(|h| h.bounds).unwrap_or(bounds); - window.with_content_mask(Some(ContentMask { bounds }), |window| { + let content_mask = ContentMask { + bounds, + ..Default::default() + }; + window.with_content_mask(Some(content_mask), |window| { let webview = self.view.clone(); window.on_mouse_event(move |event: &MouseDownEvent, _, _, _| { if !bounds.contains(&event.position) { diff --git a/docs/docs/components/sidebar.md b/docs/docs/components/sidebar.md index b6001273..88f00747 100644 --- a/docs/docs/components/sidebar.md +++ b/docs/docs/components/sidebar.md @@ -89,7 +89,9 @@ SidebarToggleButton::new() ### Floating Sidebar (SidebarShell + Sidebar) `FloatingSidebar` composes `SidebarShell` with `Sidebar`, handling resize internally. -It defaults to an 8px inset from the window edges and can be resized when expanded. +It defaults to an 8px inset, `ElevationToken::Sm`, and can be resized when expanded. +The shell elevation controls the complete panel shadow. Larger elevations may need a larger +inset near window edges to avoid native-window clipping; the inset is never adjusted automatically. ```rust use gpui_component::{FloatingSidebar, Side}; diff --git a/vendor/gpui b/vendor/gpui index 24cfa81c..4332ea7d 160000 --- a/vendor/gpui +++ b/vendor/gpui @@ -1 +1 @@ -Subproject commit 24cfa81ca2a9f954b96320e14b393dd519078618 +Subproject commit 4332ea7deae4838c12bad6ea64292ca22a33cf98