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.
|
||||
|
||||
+3
-2
@@ -150,7 +150,7 @@ Omarchy `colors.toml` maps almost 1:1: `lighter_background` → `surface`, `dark
|
||||
|
||||
The set stays role-minimal and grows one concept at a time, driven by the unit that needs it. Known planned additions:
|
||||
|
||||
- **Overlay scrim + shadow/elevation tokens** — with the overlay units (Popover/Dialog/Menu, unit 11+); §5.5's popup elevation promise. No current role covers the modality backdrop.
|
||||
- ~~**Overlay scrim + shadow/elevation tokens**~~ — ✅ landed with unit 11 (2026-10-29): `colors.overlay` (translucent-black scrim, denser in dark; TOML-overridable; no platform states one), `Radius.overlay` (10 pt), `Elevations.popover`/`dialog` (transcribed NSPopover-class shadows, builder → `Vec<BoxShadow>`), and `Motion.overlay` (200 ms enter transition). Popover consumes elevation + motion; the scrim waits for Dialog.
|
||||
- **`success` / `warning` roles** — with the first severity-bearing component (Toast, badges, validation); the Omarchy mapping above already anticipates them.
|
||||
- **`knob` / control-fill role** — only if Slider (unit 7) confirms the Switch's borrowed roles (`primary_foreground` thumb, `border` off-track) as a second consumer; until then no expansion.
|
||||
|
||||
@@ -208,7 +208,8 @@ Each row is one review unit: build it, show it in the gallery, review, then move
|
||||
| 8 | core | Progress | Determinate + indeterminate. |
|
||||
| 9 | core | Avatar | Initials; image slot later. |
|
||||
| 10 | core | Input (single-line) then multiline, NumberInput | String buffer + `unicode-segmentation` word movement; the field owns its own selection. May seed `crates/editor`. Single-line finishing pass: §6.2. |
|
||||
| 11+ | core | Popover, Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. |
|
||||
| 11 | core | Popover | ✅ Landed (2026-10-29). First overlay. Controlled `open` + `on_change` (Rc-shared handler); `trigger`/`content` element closures; panel = surface + `Radius.overlay` + `Elevations.popover` + border, role Dialog with label; `anchored()` + `deferred()` (window-overflow flip for free); enter fade via gpui `with_animation` at `Motion.overlay` (respects reduce-motion); claims keyboard focus while open; escape and click-outside dismiss (outside clicks swallowed). No scrim — native macOS popover model; the overlay token waits for Dialog. Known v1 gaps: no exit animation, trigger aria-expanded left to the app |
|
||||
| 11+ | core | Tooltip, Menu, Dialog, AlertDialog, Sheet, Toast | One unit each, same gate. Menu reuses the popover shell + `Radius.overlay`/`Elevations.popover`; Dialog consumes `colors.overlay` (scrim) + `Elevations.dialog` |
|
||||
| then | data (new) | List, Table, Tree, Tabs, Sidebar | Created on demand, same gate. |
|
||||
| then | pickers or data | Calendar/DatePicker, TimeField, ColorPicker | Decide at unit time. |
|
||||
| editor | editor (new) | Text/code editor, syntax highlighting (D12) | Same gate. |
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui_native_core::checkbox::Checkbox;
|
||||
use gpui_native_core::common::{Sizable as _, h_flex, v_flex};
|
||||
use gpui_native_core::input::{Input, InputState};
|
||||
use gpui_native_core::link::Link;
|
||||
use gpui_native_core::popover::Popover;
|
||||
use gpui_native_core::progress::Progress;
|
||||
use gpui_native_core::radio::{Radio, RadioGroup};
|
||||
use gpui_native_core::slider::Slider;
|
||||
@@ -27,6 +28,7 @@ struct Gallery {
|
||||
toggle_pressed: bool,
|
||||
volume: f32,
|
||||
input_state: Entity<InputState>,
|
||||
popover_open: bool,
|
||||
}
|
||||
|
||||
impl Render for Gallery {
|
||||
@@ -275,6 +277,47 @@ impl Render for Gallery {
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
.child(section_label(
|
||||
"popover · native ref — NSPopover, WinUI Flyout, Adwaita Popover",
|
||||
colors,
|
||||
))
|
||||
.child({
|
||||
let gallery = gallery.clone();
|
||||
Popover::new("gallery-popover")
|
||||
.open(self.popover_open)
|
||||
.on_change(move |open, _, cx| {
|
||||
gallery.update(cx, |gallery, cx| {
|
||||
gallery.popover_open = open;
|
||||
cx.notify();
|
||||
});
|
||||
})
|
||||
.trigger(|_, _| {
|
||||
Button::new("gallery-popover-trigger")
|
||||
.label("Options")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.into_any_element()
|
||||
})
|
||||
.content(move |_, _| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.w(px(240.))
|
||||
.p_2()
|
||||
.child(section_label("appearance", colors))
|
||||
.child(
|
||||
Checkbox::new("gallery-popover-telemetry")
|
||||
.label("Enable telemetry"),
|
||||
)
|
||||
.child(
|
||||
Checkbox::new("gallery-popover-crash")
|
||||
.label("Send crash reports"),
|
||||
)
|
||||
.into_any_element()
|
||||
})
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_2()
|
||||
@@ -627,6 +670,7 @@ fn main() {
|
||||
toggle_pressed: false,
|
||||
volume: 40.0,
|
||||
input_state,
|
||||
popover_open: false,
|
||||
})
|
||||
},
|
||||
) {
|
||||
|
||||
Reference in New Issue
Block a user