theme: derive elevations from the platform's native shadows
This commit is contained in:
@@ -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
@@ -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)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user