theme: derive elevations from the platform's native shadows

This commit is contained in:
2026-11-01 09:54:35 +07:00
parent 5a32401e25
commit d107f35084
4 changed files with 127 additions and 37 deletions
+2 -8
View File
@@ -7,7 +7,7 @@ use gpui::{
Styled as _, Window, deferred, div, ease_out_quint, px,
};
use gpui_native_icons::{IconRole, icon};
use gpui_native_theme::{ActiveTheme as _, ThemeVariant};
use gpui_native_theme::ActiveTheme as _;
use crate::common::DismissHandler;
use crate::common::v_flex;
@@ -127,13 +127,7 @@ impl RenderOnce for Alert {
let muted = theme.colors.muted;
let surface = theme.colors.surface;
let border = theme.colors.border;
let mut elevation = theme.elevations.popover;
if theme.variant == ThemeVariant::Light {
elevation.color = elevation.color.alpha(0.12);
}
let shadows = elevation.shadows();
let shadows = theme.elevations.popover.shadows();
let text_size = theme.sizes.small.text_size;
let icon_size = theme.sizes.small.icon_size;
+90 -24
View File
@@ -84,10 +84,10 @@ impl Default for Radius {
}
}
/// One drop shadow of a floating layer,
/// transcribed from the platform's native overlay shadows.
/// One layer of a floating panel's drop shadow.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Elevation {
pub struct Shadow {
pub offset_x: Pixels,
pub offset_y: Pixels,
pub blur_radius: Pixels,
pub spread_radius: Pixels,
@@ -95,43 +95,109 @@ pub struct Elevation {
pub color: Hsla,
}
impl Shadow {
fn none() -> Self {
Self {
offset_x: px(0.),
offset_y: px(0.),
blur_radius: px(0.),
spread_radius: px(0.),
color: colors::scrim(0.),
}
}
fn black(offset_x: f32, offset_y: f32, blur_radius: f32, alpha: f32) -> Self {
Self {
offset_x: px(offset_x),
offset_y: px(offset_y),
blur_radius: px(blur_radius),
spread_radius: px(0.),
color: colors::scrim(alpha),
}
}
}
/// The drop shadow stack of a floating layer.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Elevation {
/// Key shadow first, ambient grounding second.
pub layers: [Shadow; 2],
}
impl Elevation {
fn single(key: Shadow) -> Self {
Self {
layers: [key, Shadow::none()],
}
}
fn layered(key: Shadow, ambient: Shadow) -> Self {
Self {
layers: [key, ambient],
}
}
/// The gpui shadow list an overlay panel paints with.
pub fn shadows(self) -> Vec<gpui::BoxShadow> {
vec![gpui::BoxShadow {
color: self.color,
offset: gpui::point(px(0.), self.offset_y),
blur_radius: self.blur_radius,
spread_radius: self.spread_radius,
inset: false,
}]
self.layers
.iter()
.filter(|layer| layer.color.a > 0.)
.map(|layer| gpui::BoxShadow {
color: layer.color,
offset: gpui::point(layer.offset_x, layer.offset_y),
blur_radius: layer.blur_radius,
spread_radius: layer.spread_radius,
inset: false,
})
.collect()
}
}
/// The drop shadows of each floating layer class.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Elevations {
/// Popovers and menus ride above content on a tight shadow.
/// Popovers, menus, and pushed alerts ride above content.
pub popover: Elevation,
/// Modal dialogs sit higher with a wider one.
pub dialog: Elevation,
}
impl Default for Elevations {
fn default() -> Self {
Self {
popover: Elevation {
offset_y: px(4.),
blur_radius: px(24.),
spread_radius: px(0.),
color: colors::scrim(0.3),
impl Elevations {
pub(crate) fn for_platform(source: SourceKind, variant: ThemeVariant) -> Self {
if variant == ThemeVariant::Dark {
return Self {
popover: Elevation::single(Shadow::black(0., 4., 24., 0.3)),
dialog: Elevation::single(Shadow::black(0., 8., 40., 0.35)),
};
}
match source {
SourceKind::Macos => Self {
popover: Elevation::layered(
Shadow::black(0., 4., 24., 0.12),
Shadow::black(0., 1., 1., 0.05),
),
dialog: Elevation::layered(
Shadow::black(0., 8., 40., 0.14),
Shadow::black(0., 1., 2., 0.06),
),
},
dialog: Elevation {
offset_y: px(8.),
blur_radius: px(40.),
spread_radius: px(0.),
color: colors::scrim(0.35),
SourceKind::Windows => Self {
popover: Elevation::layered(
Shadow::black(0., 8., 16., 0.14),
Shadow::black(0., 2., 4., 0.1),
),
dialog: Elevation::layered(
Shadow::black(0., 16., 32., 0.16),
Shadow::black(0., 4., 8., 0.1),
),
},
SourceKind::Omarchy | SourceKind::Kde | SourceKind::Gnome | SourceKind::Fallback => {
Self {
popover: Elevation::single(Shadow::black(0., 3., 6., 0.2)),
dialog: Elevation::single(Shadow::black(0., 9., 18., 0.24)),
}
}
}
}
}
+31 -2
View File
@@ -220,7 +220,7 @@ pub(crate) fn resolve(
},
colors,
radius,
elevations: Elevations::default(),
elevations: Elevations::for_platform(system.source, variant),
motion: Motion::default(),
scrollbar: Scrollbar::default(),
root_font_size,
@@ -361,13 +361,42 @@ mod tests {
);
// The popover shadow is softer and tighter than the dialog's.
assert!(resolved.elevations.popover.blur_radius < resolved.elevations.dialog.blur_radius);
assert!(
resolved.elevations.popover.layers[0].blur_radius
< resolved.elevations.dialog.layers[0].blur_radius
);
assert!(resolved.motion.overlay > std::time::Duration::ZERO);
let shadows = resolved.elevations.popover.shadows();
assert_eq!(shadows.len(), 1);
assert!(!shadows[0].inset);
}
#[test]
fn elevations_follow_the_platform_in_light_mode_and_share_one_dark_stack() {
let popover = |source| {
let mut stated = system(ThemeVariant::Light, PartialPalette::default(), None);
stated.source = source;
resolve(&stated, None, None).elevations.popover
};
// macOS paints a soft layered stack; Linux keeps a compact single one.
assert_eq!(popover(SourceKind::Macos).shadows().len(), 2);
assert_eq!(popover(SourceKind::Gnome).shadows().len(), 1);
assert!(
popover(SourceKind::Macos).layers[0].color.a
< popover(SourceKind::Gnome).layers[0].color.a
);
// Dark mode is one shared stack, however the adapter detected.
let dark = |source| {
let mut stated = system(ThemeVariant::Dark, PartialPalette::default(), None);
stated.source = source;
resolve(&stated, None, None).elevations
};
assert_eq!(dark(SourceKind::Macos), dark(SourceKind::Windows));
assert_eq!(dark(SourceKind::Macos), dark(SourceKind::Fallback));
}
#[test]
fn system_colors_override_the_base_but_leave_the_rest_intact() {
let palette = PartialPalette {