use gpui::{point, px, BoxShadow, Pixels};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SkeletonAnimation {
#[default]
Shimmer,
Pulse,
None,
}
#[derive(Clone, Debug)]
#[non_exhaustive]
pub struct LayoutTheme {
pub spacing: Pixels,
pub radius: Pixels,
pub field_radius: Pixels,
pub border_width: Pixels,
pub field_border_width: Pixels,
pub disabled_opacity: f32,
pub ring_offset_width: Pixels,
pub surface_shadow: Vec<BoxShadow>,
pub overlay_shadow: Vec<BoxShadow>,
pub field_shadow: Vec<BoxShadow>,
pub skeleton_animation: SkeletonAnimation,
pub tooltip_delay_ms: u64,
pub tooltip_close_delay_ms: u64,
pub overlay_hairline: Option<gpui::Hsla>,
pub cursor_interactive: gpui::CursorStyle,
pub tabs_hover_opacity: f32,
pub tooltip_cooldown_ms: u64,
pub long_press_ms: u64,
pub hover_fade_ms: u64,
}
impl Default for LayoutTheme {
fn default() -> Self {
Self::light()
}
}
impl LayoutTheme {
pub fn light() -> Self {
Self {
surface_shadow: vec![
shadow(0., 2., 4., 0.04),
shadow(0., 1., 2., 0.06),
shadow(0., 0., 1., 0.06),
],
overlay_shadow: vec![
shadow(0., 2., 8., 0.06),
shadow(0., -6., 12., 0.03),
shadow(0., 14., 28., 0.08),
],
field_shadow: vec![
shadow(0., 2., 4., 0.04),
shadow(0., 1., 2., 0.06),
shadow(0., 0., 1., 0.06),
],
..Self::common()
}
}
pub fn dark() -> Self {
Self {
surface_shadow: Vec::new(),
overlay_shadow: Vec::new(),
field_shadow: Vec::new(),
overlay_hairline: Some(gpui::hsla(0., 0., 1., 0.3)),
..Self::common()
}
}
fn common() -> Self {
let radius = px(8.0);
Self {
spacing: px(4.0),
radius,
field_radius: radius * 1.5,
border_width: px(1.0),
field_border_width: px(0.0),
disabled_opacity: 0.5,
ring_offset_width: px(2.0),
surface_shadow: Vec::new(),
overlay_shadow: Vec::new(),
field_shadow: Vec::new(),
skeleton_animation: SkeletonAnimation::Shimmer,
tooltip_delay_ms: 1500,
tooltip_close_delay_ms: 500,
overlay_hairline: None,
cursor_interactive: gpui::CursorStyle::PointingHand,
tabs_hover_opacity: 0.7,
tooltip_cooldown_ms: 500,
long_press_ms: 500,
hover_fade_ms: 100,
}
}
pub fn radius_xs(&self) -> Pixels {
self.radius * 0.25
}
pub fn radius_sm(&self) -> Pixels {
self.radius * 0.5
}
pub fn radius_md(&self) -> Pixels {
self.radius * 0.75
}
pub fn radius_lg(&self) -> Pixels {
self.radius
}
pub fn radius_xl(&self) -> Pixels {
self.radius * 1.5
}
pub fn radius_2xl(&self) -> Pixels {
self.radius * 2.0
}
pub fn radius_3xl(&self) -> Pixels {
self.radius * 3.0
}
pub fn radius_4xl(&self) -> Pixels {
self.radius * 4.0
}
pub fn capped(&self, radius: Pixels) -> Pixels {
radius.min(px(32.0))
}
}
fn shadow(x: f32, y: f32, blur: f32, alpha: f32) -> BoxShadow {
BoxShadow {
inset: false,
color: gpui::hsla(0.0, 0.0, 0.0, alpha),
offset: point(px(x), px(y)),
blur_radius: px(blur),
spread_radius: px(0.),
}
}
#[cfg(test)]
mod tests {
use super::*;
use gpui::{div, Styled};
#[test]
fn the_default_interactive_cursor_is_gpuis_own_pointer() {
let mut probe = div().cursor_pointer();
assert_eq!(
Some(LayoutTheme::light().cursor_interactive),
probe.style().mouse_cursor,
);
assert_eq!(
LayoutTheme::dark().cursor_interactive,
LayoutTheme::light().cursor_interactive,
"light and dark share the token; only a custom theme changes it"
);
}
#[test]
fn customisation_tokens_default_to_the_literals_they_replace() {
for layout in [LayoutTheme::light(), LayoutTheme::dark()] {
assert!((layout.tabs_hover_opacity - 0.7).abs() < f32::EPSILON);
assert_eq!(layout.tooltip_cooldown_ms, 500);
assert_eq!(layout.long_press_ms, 500);
assert_eq!(layout.hover_fade_ms, 100);
}
}
}