add popover
This commit is contained in:
@@ -36,6 +36,9 @@ pub mod avatar;
|
||||
#[cfg(feature = "input")]
|
||||
pub mod input;
|
||||
|
||||
#[cfg(feature = "popover")]
|
||||
pub mod popover;
|
||||
|
||||
/// Initializes everything the components need at startup.
|
||||
pub fn init(cx: &mut App) {
|
||||
gpui_native_theme::Theme::init(cx);
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
//! Semantic color roles. This module is the only place in the workspace
|
||||
//! allowed to hold color literals.
|
||||
|
||||
use gpui::{Hsla, rgb};
|
||||
use gpui::{Hsla, hsla, rgb};
|
||||
|
||||
fn hex(hex: u32) -> Hsla {
|
||||
rgb(hex).into()
|
||||
}
|
||||
|
||||
/// The dim scrim behind modal layers; always a translucent black.
|
||||
pub(crate) fn scrim(alpha: f32) -> Hsla {
|
||||
hsla(0., 0., 0., alpha)
|
||||
}
|
||||
|
||||
/// The semantic color roles components paint from.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct ColorTokens {
|
||||
@@ -28,6 +33,8 @@ pub struct ColorTokens {
|
||||
pub input: Hsla,
|
||||
pub ring: Hsla,
|
||||
pub selection: Hsla,
|
||||
/// The scrim behind modal layers; translucent in every theme.
|
||||
pub overlay: Hsla,
|
||||
}
|
||||
|
||||
impl ColorTokens {
|
||||
@@ -52,6 +59,7 @@ impl ColorTokens {
|
||||
input: hex(0xe4e4e7),
|
||||
ring: hex(0x93c5fd),
|
||||
selection: hex(0xdbeafe),
|
||||
overlay: scrim(0.25),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,6 +84,7 @@ impl ColorTokens {
|
||||
input: hex(0x27272a),
|
||||
ring: hex(0x1d4ed8),
|
||||
selection: hex(0x1e3a8a),
|
||||
overlay: scrim(0.45),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ pub struct ThemeDefinition {
|
||||
pub input: Option<String>,
|
||||
pub ring: Option<String>,
|
||||
pub selection: Option<String>,
|
||||
pub overlay: Option<String>,
|
||||
}
|
||||
|
||||
impl ThemeDefinition {
|
||||
@@ -107,6 +108,10 @@ impl ThemeDefinition {
|
||||
pub fn selection(&self) -> Option<Hsla> {
|
||||
parse_hex(self.selection.as_deref()?)
|
||||
}
|
||||
|
||||
pub fn overlay(&self) -> Option<Hsla> {
|
||||
parse_hex(self.overlay.as_deref()?)
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses `#rgb` or `#rrggbb` into an opaque color.
|
||||
|
||||
+77
-1
@@ -20,7 +20,8 @@ pub use ramps::{AccentShades, Oklch};
|
||||
pub use resolve::{Ramps, ResolvedTheme};
|
||||
pub use system::{AccessibilityPreferences, PartialPalette, SourceKind, SystemTheme};
|
||||
|
||||
use gpui::{App, Global, Pixels, WindowAppearance, px};
|
||||
use gpui::{App, Global, Hsla, Pixels, WindowAppearance, px};
|
||||
use std::time::Duration;
|
||||
|
||||
/// Light or dark, resolved from the system appearance.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Deserialize)]
|
||||
@@ -47,6 +48,9 @@ pub struct Theme {
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
pub elevations: Elevations,
|
||||
/// Overlay transition timing; gpui's animation primitives do the rest.
|
||||
pub motion: Motion,
|
||||
/// The platform's native root font size; rem-based sizing scales with it.
|
||||
pub root_font_size: Pixels,
|
||||
pub sizes: SizeTable,
|
||||
@@ -61,6 +65,8 @@ pub struct Radius {
|
||||
/// Small controls: checkbox and radio indicators.
|
||||
pub sm: Pixels,
|
||||
pub control: Pixels,
|
||||
/// Floating layers: popovers, menus, dialogs.
|
||||
pub overlay: Pixels,
|
||||
/// Pill and circle shapes.
|
||||
pub full: Pixels,
|
||||
}
|
||||
@@ -70,11 +76,79 @@ impl Default for Radius {
|
||||
Self {
|
||||
sm: px(4.),
|
||||
control: px(6.),
|
||||
overlay: px(10.),
|
||||
full: px(9999.),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One drop shadow of a floating layer, transcribed from the platform's
|
||||
/// native overlay shadows.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Elevation {
|
||||
pub offset_y: Pixels,
|
||||
pub blur_radius: Pixels,
|
||||
pub spread_radius: Pixels,
|
||||
/// Translucent black, like every native overlay shadow.
|
||||
pub color: Hsla,
|
||||
}
|
||||
|
||||
impl Elevation {
|
||||
/// 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,
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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),
|
||||
},
|
||||
dialog: Elevation {
|
||||
offset_y: px(8.),
|
||||
blur_radius: px(40.),
|
||||
spread_radius: px(0.),
|
||||
color: colors::scrim(0.35),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Overlay transition timings; the platform refines these per phase.
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Motion {
|
||||
/// The enter transition of popovers, menus, and dialogs.
|
||||
pub overlay: Duration,
|
||||
}
|
||||
|
||||
impl Default for Motion {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
overlay: Duration::from_millis(200),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A named control size class.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Size {
|
||||
@@ -170,6 +244,8 @@ impl Theme {
|
||||
colors: resolved.colors,
|
||||
ramps: resolved.ramps,
|
||||
radius: resolved.radius,
|
||||
elevations: resolved.elevations,
|
||||
motion: resolved.motion,
|
||||
root_font_size: resolved.root_font_size,
|
||||
sizes: resolved.sizes,
|
||||
accessibility: resolved.accessibility,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use crate::definition::ThemeDefinition;
|
||||
use crate::ramps::{AccentShades, Oklch, contrast_foreground, derive_shades, mix};
|
||||
use crate::system::SystemTheme;
|
||||
use crate::{ColorTokens, Radius, SizeTable, ThemeVariant};
|
||||
use crate::{ColorTokens, Elevations, Motion, Radius, SizeTable, ThemeVariant};
|
||||
use gpui::px;
|
||||
|
||||
/// Derived state shades for the colors that get pressed and hovered.
|
||||
@@ -19,6 +19,8 @@ pub struct ResolvedTheme {
|
||||
pub colors: ColorTokens,
|
||||
pub ramps: Ramps,
|
||||
pub radius: Radius,
|
||||
pub elevations: Elevations,
|
||||
pub motion: Motion,
|
||||
/// The platform's native root font size; the fallback theme uses 16.
|
||||
pub root_font_size: gpui::Pixels,
|
||||
pub sizes: SizeTable,
|
||||
@@ -123,6 +125,11 @@ pub(crate) fn resolve(
|
||||
system_palette.selection,
|
||||
base.selection,
|
||||
),
|
||||
overlay: pick(
|
||||
def_color(|d| d.overlay()),
|
||||
system_palette.overlay,
|
||||
base.overlay,
|
||||
),
|
||||
};
|
||||
|
||||
let accessibility = system.accessibility;
|
||||
@@ -172,6 +179,8 @@ pub(crate) fn resolve(
|
||||
},
|
||||
colors,
|
||||
radius,
|
||||
elevations: Elevations::default(),
|
||||
motion: Motion::default(),
|
||||
root_font_size,
|
||||
sizes,
|
||||
accessibility,
|
||||
@@ -274,6 +283,49 @@ mod tests {
|
||||
assert!((resolved.sizes.small.input_height - expected_small).abs() < px(0.01));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overlay_is_translucent_and_overridable() {
|
||||
let light = resolve(
|
||||
&system(ThemeVariant::Light, PartialPalette::default(), None),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let dark = resolve(
|
||||
&system(ThemeVariant::Dark, PartialPalette::default(), None),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// Both scrims are translucent black; dark is denser for contrast.
|
||||
assert!(light.colors.overlay.a > 0.0 && light.colors.overlay.a < 0.5);
|
||||
assert!(dark.colors.overlay.a > light.colors.overlay.a);
|
||||
|
||||
let def = ThemeDefinition::parse_toml("overlay = \"#000000\"").unwrap();
|
||||
let overridden = resolve(
|
||||
&system(ThemeVariant::Light, PartialPalette::default(), None),
|
||||
Some(&def),
|
||||
None,
|
||||
);
|
||||
assert_eq!(overridden.colors.overlay, hsla(0x000000));
|
||||
assert_eq!(overridden.colors.overlay.a, 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn elevations_and_motion_resolve_with_defaults() {
|
||||
let resolved = resolve(
|
||||
&system(ThemeVariant::Dark, PartialPalette::default(), None),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
|
||||
// The popover shadow is softer and tighter than the dialog's.
|
||||
assert!(resolved.elevations.popover.blur_radius < resolved.elevations.dialog.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 system_colors_override_the_base_but_leave_the_rest_intact() {
|
||||
let palette = PartialPalette {
|
||||
|
||||
@@ -85,6 +85,7 @@ impl MacosSource {
|
||||
input: Self::color_to_hsla(&NSColor::controlBackgroundColor()),
|
||||
ring: Self::color_to_hsla(&NSColor::keyboardFocusIndicatorColor()),
|
||||
selection: Self::color_to_hsla(&NSColor::selectedTextBackgroundColor()),
|
||||
overlay: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,6 +47,8 @@ pub struct PartialPalette {
|
||||
pub input: Option<Hsla>,
|
||||
pub ring: Option<Hsla>,
|
||||
pub selection: Option<Hsla>,
|
||||
/// No platform states a scrim color; preset and user files override it.
|
||||
pub overlay: Option<Hsla>,
|
||||
}
|
||||
|
||||
/// A normalized snapshot of the running system's theme.
|
||||
|
||||
Reference in New Issue
Block a user