T00 foundation: theme, motion, assets, icon, buttons, gallery
Theme tokens in light and dark with high contrast, a palette cross-fade, motion curves and a damped spring, embedded Lucide icons with Inter and JetBrains Mono, Icon, Button, IconButton, and a gallery that captures its own window. Reviewed by codex gpt-6-astra (max): PASS in round 3.
This commit is contained in:
258 files changed
+16908
No files matched your search
@@ -0,0 +1,36 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use anyhow::Context as _;
|
||||
use gpui::{App, AssetSource, Result, SharedString};
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
/// Icons and fonts. Pass to `Application::with_assets`.
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "assets"]
|
||||
pub struct Assets;
|
||||
|
||||
impl AssetSource for Assets {
|
||||
fn load(&self, path: &str) -> Result<Option<Cow<'static, [u8]>>> {
|
||||
Ok(Self::get(path).map(|file| file.data))
|
||||
}
|
||||
|
||||
fn list(&self, path: &str) -> Result<Vec<SharedString>> {
|
||||
Ok(Self::iter()
|
||||
.filter(|name| name.starts_with(path))
|
||||
.map(SharedString::from)
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn load_fonts(cx: &App) -> Result<()> {
|
||||
let fonts = Assets::iter()
|
||||
.filter(|name| name.ends_with(".ttf"))
|
||||
.map(|name| {
|
||||
Assets::get(&name)
|
||||
.map(|file| file.data)
|
||||
.with_context(|| format!("font {name} vanished from the bundle"))
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
log::info!("assets: registering {} fonts", fonts.len());
|
||||
cx.text_system().add_fonts(fonts)
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, FontWeight, Hsla, IntoElement, MouseButton, RenderOnce,
|
||||
SharedString, Window, div, prelude::*, transparent_black,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, IconSize, Mix, Palette, Radius, TextSize},
|
||||
};
|
||||
|
||||
pub(crate) type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ButtonVariant {
|
||||
Primary,
|
||||
#[default]
|
||||
Secondary,
|
||||
Outline,
|
||||
Ghost,
|
||||
Subtle,
|
||||
Danger,
|
||||
Link,
|
||||
}
|
||||
|
||||
pub(crate) struct Tone {
|
||||
pub bg: Hsla,
|
||||
pub hover: Hsla,
|
||||
pub pressed: Hsla,
|
||||
pub fg: Hsla,
|
||||
pub border: Option<Hsla>,
|
||||
}
|
||||
|
||||
pub(crate) fn tone(variant: ButtonVariant, colors: &Palette) -> Tone {
|
||||
let clear = transparent_black();
|
||||
match variant {
|
||||
ButtonVariant::Primary => Tone {
|
||||
bg: colors.accent,
|
||||
hover: colors.accent_hover,
|
||||
pressed: colors.accent_hover.mix(&colors.on_accent, 0.12),
|
||||
fg: colors.on_accent,
|
||||
border: None,
|
||||
},
|
||||
ButtonVariant::Secondary => Tone {
|
||||
bg: colors.surface,
|
||||
hover: colors.hover,
|
||||
pressed: colors.active,
|
||||
fg: colors.fg,
|
||||
border: Some(colors.border),
|
||||
},
|
||||
ButtonVariant::Outline => Tone {
|
||||
bg: clear,
|
||||
hover: colors.hover,
|
||||
pressed: colors.active,
|
||||
fg: colors.fg,
|
||||
border: Some(colors.border_strong),
|
||||
},
|
||||
ButtonVariant::Ghost => Tone {
|
||||
bg: clear,
|
||||
hover: colors.hover,
|
||||
pressed: colors.active,
|
||||
fg: colors.fg,
|
||||
border: None,
|
||||
},
|
||||
ButtonVariant::Subtle => Tone {
|
||||
bg: colors.hover,
|
||||
hover: colors.active,
|
||||
pressed: colors.active.mix(&colors.border_strong, 0.5),
|
||||
fg: colors.fg,
|
||||
border: None,
|
||||
},
|
||||
ButtonVariant::Danger => Tone {
|
||||
bg: colors.danger,
|
||||
hover: colors.danger.mix(&colors.fg, 0.14),
|
||||
pressed: colors.danger.mix(&colors.fg, 0.24),
|
||||
fg: colors.on_accent,
|
||||
border: None,
|
||||
},
|
||||
ButtonVariant::Link => Tone {
|
||||
bg: clear,
|
||||
hover: clear,
|
||||
pressed: clear,
|
||||
fg: colors.link,
|
||||
border: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn label_size(size: ControlSize) -> (TextSize, IconSize) {
|
||||
match size {
|
||||
ControlSize::Sm => (TextSize::Sm, IconSize::Xs),
|
||||
ControlSize::Md => (TextSize::Base, IconSize::Sm),
|
||||
ControlSize::Lg => (TextSize::Md, IconSize::Md),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Button {
|
||||
id: ElementId,
|
||||
label: SharedString,
|
||||
icon: Option<IconName>,
|
||||
trailing_icon: Option<IconName>,
|
||||
variant: ButtonVariant,
|
||||
size: ControlSize,
|
||||
disabled: bool,
|
||||
full_width: bool,
|
||||
on_click: Option<ClickHandler>,
|
||||
}
|
||||
|
||||
impl Button {
|
||||
pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
label: label.into(),
|
||||
icon: None,
|
||||
trailing_icon: None,
|
||||
variant: ButtonVariant::default(),
|
||||
size: ControlSize::default(),
|
||||
disabled: false,
|
||||
full_width: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn primary(self) -> Self {
|
||||
self.variant(ButtonVariant::Primary)
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn icon(mut self, icon: IconName) -> Self {
|
||||
self.icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn trailing_icon(mut self, icon: IconName) -> Self {
|
||||
self.trailing_icon = Some(icon);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn full_width(mut self) -> Self {
|
||||
self.full_width = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Button {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let tone = tone(self.variant, &theme.colors);
|
||||
let (text, icon_size) = label_size(self.size);
|
||||
let link = self.variant == ButtonVariant::Link;
|
||||
let icon = |name| Icon::new(name).size(icon_size).color(tone.fg);
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.gap_1p5()
|
||||
.h(theme.control_height(self.size))
|
||||
.when(!link, |el| el.px(theme.control_padding(self.size)))
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.text_size(theme.text_size(text))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(tone.fg)
|
||||
.bg(tone.bg)
|
||||
.when_some(tone.border, |el, border| el.border_1().border_color(border))
|
||||
.when(self.full_width, |el| el.w_full())
|
||||
.when_some(self.icon, |el, name| el.child(icon(name)))
|
||||
.child(self.label)
|
||||
.when_some(self.trailing_icon, |el, name| el.child(icon(name)))
|
||||
.map(|el| {
|
||||
if self.disabled {
|
||||
return el.opacity(0.45).cursor_not_allowed();
|
||||
}
|
||||
el.cursor_pointer()
|
||||
.tab_index(0)
|
||||
.hover(|style| {
|
||||
let style = style.bg(tone.hover);
|
||||
if link { style.underline() } else { style }
|
||||
})
|
||||
.active(|style| style.bg(tone.pressed))
|
||||
.focus(|style| style.shadow(theme.focus_ring()))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when_some(self.on_click, |el, handler| el.on_click(handler))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
use gpui::{
|
||||
App, ClickEvent, ElementId, IntoElement, MouseButton, RenderOnce, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use super::button::{ButtonVariant, ClickHandler, label_size, tone};
|
||||
use crate::{
|
||||
primitives::{Icon, IconName},
|
||||
theme::{ActiveTheme, ControlSize, Radius},
|
||||
};
|
||||
|
||||
/// Square button holding one icon.
|
||||
#[derive(IntoElement)]
|
||||
pub struct IconButton {
|
||||
id: ElementId,
|
||||
icon: IconName,
|
||||
variant: ButtonVariant,
|
||||
size: ControlSize,
|
||||
disabled: bool,
|
||||
on_click: Option<ClickHandler>,
|
||||
}
|
||||
|
||||
impl IconButton {
|
||||
pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
|
||||
Self {
|
||||
id: id.into(),
|
||||
icon,
|
||||
variant: ButtonVariant::Ghost,
|
||||
size: ControlSize::default(),
|
||||
disabled: false,
|
||||
on_click: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn variant(mut self, variant: ButtonVariant) -> Self {
|
||||
self.variant = variant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: ControlSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn disabled(mut self, disabled: bool) -> Self {
|
||||
self.disabled = disabled;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_click(
|
||||
mut self,
|
||||
handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.on_click = Some(Box::new(handler));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for IconButton {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let tone = tone(self.variant, &theme.colors);
|
||||
let (_, icon_size) = label_size(self.size);
|
||||
let side = theme.control_height(self.size);
|
||||
|
||||
div()
|
||||
.id(self.id)
|
||||
.flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.justify_center()
|
||||
.size(side)
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(tone.bg)
|
||||
.when_some(tone.border, |el, border| el.border_1().border_color(border))
|
||||
.child(Icon::new(self.icon).size(icon_size).color(tone.fg))
|
||||
.map(|el| {
|
||||
if self.disabled {
|
||||
return el.opacity(0.45).cursor_not_allowed();
|
||||
}
|
||||
el.cursor_pointer()
|
||||
.tab_index(0)
|
||||
.hover(|style| style.bg(tone.hover))
|
||||
.active(|style| style.bg(tone.pressed))
|
||||
.focus(|style| style.shadow(theme.focus_ring()))
|
||||
.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
|
||||
.when_some(self.on_click, |el, handler| el.on_click(handler))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod button;
|
||||
mod icon_button;
|
||||
|
||||
pub use button::{Button, ButtonVariant};
|
||||
pub use icon_button::IconButton;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
mod assets;
|
||||
|
||||
pub mod buttons;
|
||||
pub mod motion;
|
||||
pub mod primitives;
|
||||
pub mod theme;
|
||||
|
||||
pub use assets::Assets;
|
||||
|
||||
use gpui::App;
|
||||
|
||||
use crate::primitives::IconName;
|
||||
|
||||
/// Loads fonts and the theme. Call once, first.
|
||||
pub fn init(cx: &mut App) {
|
||||
let probe = IconName::Check.path();
|
||||
let wired = cx.asset_source().load(probe).ok().flatten().is_some();
|
||||
assert!(
|
||||
wired,
|
||||
"ely: pass `ely_gpui_component::Assets` to `Application::with_assets`"
|
||||
);
|
||||
assets::load_fonts(cx).expect("ely: embedded fonts failed to register");
|
||||
theme::Theme::init(cx);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use gpui::App;
|
||||
|
||||
use crate::theme::ActiveTheme;
|
||||
|
||||
pub const FAST: Duration = Duration::from_millis(120);
|
||||
pub const BASE: Duration = Duration::from_millis(200);
|
||||
pub const SLOW: Duration = Duration::from_millis(320);
|
||||
pub const THEME: Duration = Duration::from_millis(280);
|
||||
|
||||
const DAMPING: f32 = 0.75;
|
||||
const OMEGA: f32 = 6.91 / DAMPING;
|
||||
|
||||
/// `base`, or one millisecond under reduced motion.
|
||||
pub fn duration(base: Duration, cx: &App) -> Duration {
|
||||
if cx.theme().reduced_motion {
|
||||
Duration::from_millis(1)
|
||||
} else {
|
||||
base
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lerp(from: f32, to: f32, t: f32) -> f32 {
|
||||
from + (to - from) * t
|
||||
}
|
||||
|
||||
pub fn ease_out_cubic(t: f32) -> f32 {
|
||||
1.0 - (1.0 - t).powi(3)
|
||||
}
|
||||
|
||||
pub fn ease_in_out_cubic(t: f32) -> f32 {
|
||||
if t < 0.5 {
|
||||
4.0 * t * t * t
|
||||
} else {
|
||||
1.0 - (-2.0 * t + 2.0).powi(3) / 2.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Damped spring. Overshoots; not a gpui easing.
|
||||
pub fn spring(t: f32) -> f32 {
|
||||
if t >= 1.0 {
|
||||
return 1.0;
|
||||
}
|
||||
let damped = OMEGA * (1.0 - DAMPING * DAMPING).sqrt();
|
||||
let decay = (-DAMPING * OMEGA * t).exp();
|
||||
1.0 - decay * ((damped * t).cos() + DAMPING * OMEGA / damped * (damped * t).sin())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn easings_pin_their_ends() {
|
||||
for ease in [ease_out_cubic, ease_in_out_cubic, spring] {
|
||||
assert!(ease(0.0).abs() < 1e-6);
|
||||
assert_eq!(ease(1.0), 1.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spring_overshoots_a_little_then_settles() {
|
||||
let peak = (0..=100)
|
||||
.map(|i| spring(i as f32 / 100.0))
|
||||
.fold(0.0, f32::max);
|
||||
assert!(peak > 1.01 && peak < 1.05, "peak {peak}");
|
||||
assert!((spring(0.95) - 1.0).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ease_in_out_is_symmetric() {
|
||||
for i in 0..=10 {
|
||||
let t = i as f32 / 10.0;
|
||||
assert!((ease_in_out_cubic(t) + ease_in_out_cubic(1.0 - t) - 1.0).abs() < 1e-5);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod curve;
|
||||
|
||||
pub use curve::*;
|
||||
@@ -0,0 +1,314 @@
|
||||
use gpui::{App, Hsla, IntoElement, Radians, RenderOnce, Transformation, Window, prelude::*, svg};
|
||||
|
||||
use crate::theme::{ActiveTheme, IconSize};
|
||||
|
||||
macro_rules! icons {
|
||||
($($variant:ident => $file:literal,)*) => {
|
||||
/// Lucide icons bundled with Ely.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum IconName {
|
||||
$($variant,)*
|
||||
}
|
||||
|
||||
impl IconName {
|
||||
pub const ALL: &[IconName] = &[$(IconName::$variant,)*];
|
||||
|
||||
pub fn path(self) -> &'static str {
|
||||
match self {
|
||||
$(IconName::$variant => concat!("icons/", $file, ".svg"),)*
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
icons! {
|
||||
Accessibility => "accessibility",
|
||||
Activity => "activity",
|
||||
AlarmClock => "alarm-clock",
|
||||
AlignCenter => "align-center",
|
||||
AlignLeft => "align-left",
|
||||
AlignRight => "align-right",
|
||||
Archive => "archive",
|
||||
ArrowDownLeft => "arrow-down-left",
|
||||
ArrowDownWideNarrow => "arrow-down-wide-narrow",
|
||||
ArrowDown => "arrow-down",
|
||||
ArrowLeft => "arrow-left",
|
||||
ArrowRight => "arrow-right",
|
||||
ArrowUpNarrowWide => "arrow-up-narrow-wide",
|
||||
ArrowUpRight => "arrow-up-right",
|
||||
ArrowUp => "arrow-up",
|
||||
AtSign => "at-sign",
|
||||
Battery => "battery",
|
||||
Bell => "bell",
|
||||
Bold => "bold",
|
||||
BookOpen => "book-open",
|
||||
Bookmark => "bookmark",
|
||||
Bot => "bot",
|
||||
Box => "box",
|
||||
Brush => "brush",
|
||||
Bug => "bug",
|
||||
CalendarDays => "calendar-days",
|
||||
CalendarRange => "calendar-range",
|
||||
Calendar => "calendar",
|
||||
ChartBar => "chart-bar",
|
||||
ChartCandlestick => "chart-candlestick",
|
||||
ChartLine => "chart-line",
|
||||
ChartPie => "chart-pie",
|
||||
CheckCheck => "check-check",
|
||||
Check => "check",
|
||||
ChevronDown => "chevron-down",
|
||||
ChevronLeft => "chevron-left",
|
||||
ChevronRight => "chevron-right",
|
||||
ChevronUp => "chevron-up",
|
||||
ChevronsLeft => "chevrons-left",
|
||||
ChevronsRight => "chevrons-right",
|
||||
ChevronsUpDown => "chevrons-up-down",
|
||||
CircleAlert => "circle-alert",
|
||||
CircleCheck => "circle-check",
|
||||
CircleDot => "circle-dot",
|
||||
CircleHelp => "circle-help",
|
||||
CirclePause => "circle-pause",
|
||||
CircleStop => "circle-stop",
|
||||
CircleX => "circle-x",
|
||||
Circle => "circle",
|
||||
ClipboardCheck => "clipboard-check",
|
||||
Clipboard => "clipboard",
|
||||
Clock => "clock",
|
||||
CloudOff => "cloud-off",
|
||||
Cloud => "cloud",
|
||||
Code => "code",
|
||||
Command => "command",
|
||||
Compass => "compass",
|
||||
Contrast => "contrast",
|
||||
Copy => "copy",
|
||||
Cpu => "cpu",
|
||||
CreditCard => "credit-card",
|
||||
Crop => "crop",
|
||||
Crosshair => "crosshair",
|
||||
Database => "database",
|
||||
DollarSign => "dollar-sign",
|
||||
Dot => "dot",
|
||||
Download => "download",
|
||||
EllipsisVertical => "ellipsis-vertical",
|
||||
Ellipsis => "ellipsis",
|
||||
Eraser => "eraser",
|
||||
ExternalLink => "external-link",
|
||||
EyeOff => "eye-off",
|
||||
Eye => "eye",
|
||||
FilePlus => "file-plus",
|
||||
FileText => "file-text",
|
||||
File => "file",
|
||||
Filter => "filter",
|
||||
Fingerprint => "fingerprint",
|
||||
Flag => "flag",
|
||||
FolderOpen => "folder-open",
|
||||
FolderPlus => "folder-plus",
|
||||
Folder => "folder",
|
||||
Forward => "forward",
|
||||
Frame => "frame",
|
||||
Gauge => "gauge",
|
||||
GitBranch => "git-branch",
|
||||
GitCommitHorizontal => "git-commit-horizontal",
|
||||
GitMerge => "git-merge",
|
||||
GitPullRequest => "git-pull-request",
|
||||
Globe => "globe",
|
||||
GripHorizontal => "grip-horizontal",
|
||||
GripVertical => "grip-vertical",
|
||||
Hand => "hand",
|
||||
HardDrive => "hard-drive",
|
||||
Hash => "hash",
|
||||
Heading => "heading",
|
||||
Heart => "heart",
|
||||
History => "history",
|
||||
Hourglass => "hourglass",
|
||||
House => "house",
|
||||
Image => "image",
|
||||
Inbox => "inbox",
|
||||
Info => "info",
|
||||
Italic => "italic",
|
||||
Kanban => "kanban",
|
||||
Key => "key",
|
||||
Keyboard => "keyboard",
|
||||
Languages => "languages",
|
||||
Layers => "layers",
|
||||
LayoutDashboard => "layout-dashboard",
|
||||
LayoutGrid => "layout-grid",
|
||||
Library => "library",
|
||||
Lightbulb => "lightbulb",
|
||||
Link => "link",
|
||||
ListChecks => "list-checks",
|
||||
ListOrdered => "list-ordered",
|
||||
ListTodo => "list-todo",
|
||||
List => "list",
|
||||
LoaderCircle => "loader-circle",
|
||||
LockOpen => "lock-open",
|
||||
Lock => "lock",
|
||||
LogIn => "log-in",
|
||||
LogOut => "log-out",
|
||||
Mail => "mail",
|
||||
MapPin => "map-pin",
|
||||
Map => "map",
|
||||
Maximize2 => "maximize-2",
|
||||
MemoryStick => "memory-stick",
|
||||
Menu => "menu",
|
||||
MessageSquare => "message-square",
|
||||
MicOff => "mic-off",
|
||||
Mic => "mic",
|
||||
Minimize2 => "minimize-2",
|
||||
Minus => "minus",
|
||||
Monitor => "monitor",
|
||||
Moon => "moon",
|
||||
MousePointer2 => "mouse-pointer-2",
|
||||
Move => "move",
|
||||
Network => "network",
|
||||
NotebookPen => "notebook-pen",
|
||||
Package => "package",
|
||||
Palette => "palette",
|
||||
PanelBottom => "panel-bottom",
|
||||
PanelLeft => "panel-left",
|
||||
PanelRight => "panel-right",
|
||||
Paperclip => "paperclip",
|
||||
Pause => "pause",
|
||||
PenTool => "pen-tool",
|
||||
Pencil => "pencil",
|
||||
PhoneOff => "phone-off",
|
||||
Phone => "phone",
|
||||
PinOff => "pin-off",
|
||||
Pin => "pin",
|
||||
Play => "play",
|
||||
Plus => "plus",
|
||||
Power => "power",
|
||||
Printer => "printer",
|
||||
Puzzle => "puzzle",
|
||||
Quote => "quote",
|
||||
Redo2 => "redo-2",
|
||||
RefreshCw => "refresh-cw",
|
||||
Reply => "reply",
|
||||
Rocket => "rocket",
|
||||
RotateCcw => "rotate-ccw",
|
||||
RotateCw => "rotate-cw",
|
||||
Save => "save",
|
||||
Scissors => "scissors",
|
||||
Search => "search",
|
||||
Send => "send",
|
||||
Server => "server",
|
||||
Settings => "settings",
|
||||
Shapes => "shapes",
|
||||
Share2 => "share-2",
|
||||
Shield => "shield",
|
||||
SkipBack => "skip-back",
|
||||
SkipForward => "skip-forward",
|
||||
SlidersHorizontal => "sliders-horizontal",
|
||||
Smartphone => "smartphone",
|
||||
Smile => "smile",
|
||||
Sparkles => "sparkles",
|
||||
Spline => "spline",
|
||||
SquareCheck => "square-check",
|
||||
Square => "square",
|
||||
Star => "star",
|
||||
StepForward => "step-forward",
|
||||
Strikethrough => "strikethrough",
|
||||
Sun => "sun",
|
||||
Table => "table",
|
||||
Tag => "tag",
|
||||
Target => "target",
|
||||
Terminal => "terminal",
|
||||
ThumbsDown => "thumbs-down",
|
||||
ThumbsUp => "thumbs-up",
|
||||
Timer => "timer",
|
||||
Trash2 => "trash-2",
|
||||
TrendingDown => "trending-down",
|
||||
TrendingUp => "trending-up",
|
||||
TriangleAlert => "triangle-alert",
|
||||
Type => "type",
|
||||
Underline => "underline",
|
||||
Undo2 => "undo-2",
|
||||
Upload => "upload",
|
||||
User => "user",
|
||||
Users => "users",
|
||||
VideoOff => "video-off",
|
||||
Video => "video",
|
||||
Volume2 => "volume-2",
|
||||
VolumeX => "volume-x",
|
||||
Wallet => "wallet",
|
||||
WandSparkles => "wand-sparkles",
|
||||
WifiOff => "wifi-off",
|
||||
Wifi => "wifi",
|
||||
X => "x",
|
||||
Zap => "zap",
|
||||
ZoomIn => "zoom-in",
|
||||
ZoomOut => "zoom-out",
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct Icon {
|
||||
name: IconName,
|
||||
size: IconSize,
|
||||
color: Option<Hsla>,
|
||||
rotation: Option<Radians>,
|
||||
}
|
||||
|
||||
impl Icon {
|
||||
pub fn new(name: IconName) -> Self {
|
||||
Self {
|
||||
name,
|
||||
size: IconSize::Md,
|
||||
color: None,
|
||||
rotation: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = size;
|
||||
self
|
||||
}
|
||||
|
||||
/// Defaults to `colors.fg`.
|
||||
pub fn color(mut self, color: impl Into<Hsla>) -> Self {
|
||||
self.color = Some(color.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn rotate(mut self, angle: impl Into<Radians>) -> Self {
|
||||
self.rotation = Some(angle.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<IconName> for Icon {
|
||||
fn from(name: IconName) -> Self {
|
||||
Icon::new(name)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for Icon {
|
||||
fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let theme = cx.theme();
|
||||
let color = self.color.unwrap_or(theme.colors.fg);
|
||||
svg()
|
||||
.path(self.name.path())
|
||||
.size(theme.icon_size(self.size))
|
||||
.flex_none()
|
||||
.text_color(color)
|
||||
.when_some(self.rotation, |svg, angle| {
|
||||
svg.with_transformation(Transformation::rotate(angle))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::IconName;
|
||||
|
||||
#[test]
|
||||
fn every_icon_ships_in_the_bundle() {
|
||||
for icon in IconName::ALL {
|
||||
assert!(
|
||||
crate::Assets::get(icon.path()).is_some(),
|
||||
"{icon:?} missing at {}",
|
||||
icon.path()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
mod icon;
|
||||
|
||||
pub use icon::{Icon, IconName};
|
||||
@@ -0,0 +1,136 @@
|
||||
mod palette;
|
||||
mod tokens;
|
||||
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use gpui::{App, Global, SharedString, WindowAppearance};
|
||||
|
||||
use crate::motion;
|
||||
pub use palette::{Mix, Palette, Syntax};
|
||||
pub use tokens::{ControlSize, Density, Elevation, IconSize, Radius, TextSize};
|
||||
|
||||
const FRAME: Duration = Duration::from_millis(8);
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Mode {
|
||||
#[default]
|
||||
Light,
|
||||
Dark,
|
||||
}
|
||||
|
||||
impl From<WindowAppearance> for Mode {
|
||||
fn from(appearance: WindowAppearance) -> Self {
|
||||
match appearance {
|
||||
WindowAppearance::Dark | WindowAppearance::VibrantDark => Mode::Dark,
|
||||
WindowAppearance::Light | WindowAppearance::VibrantLight => Mode::Light,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Design tokens. One global per app.
|
||||
pub struct Theme {
|
||||
mode: Mode,
|
||||
high_contrast: bool,
|
||||
transition: u64,
|
||||
pub colors: Palette,
|
||||
pub density: Density,
|
||||
pub radius_scale: f32,
|
||||
pub font_scale: f32,
|
||||
pub font_family: SharedString,
|
||||
pub mono_family: SharedString,
|
||||
pub reduced_motion: bool,
|
||||
}
|
||||
|
||||
impl Global for Theme {}
|
||||
|
||||
pub trait ActiveTheme {
|
||||
fn theme(&self) -> &Theme;
|
||||
}
|
||||
|
||||
impl ActiveTheme for App {
|
||||
fn theme(&self) -> &Theme {
|
||||
self.global::<Theme>()
|
||||
}
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub(crate) fn init(cx: &mut App) {
|
||||
cx.set_global(Theme {
|
||||
mode: Mode::Light,
|
||||
high_contrast: false,
|
||||
transition: 0,
|
||||
colors: Palette::light(false),
|
||||
density: Density::Standard,
|
||||
radius_scale: 1.0,
|
||||
font_scale: 1.0,
|
||||
font_family: "Inter".into(),
|
||||
mono_family: "JetBrains Mono".into(),
|
||||
reduced_motion: false,
|
||||
});
|
||||
}
|
||||
|
||||
pub fn mode(&self) -> Mode {
|
||||
self.mode
|
||||
}
|
||||
|
||||
pub fn high_contrast(&self) -> bool {
|
||||
self.high_contrast
|
||||
}
|
||||
|
||||
pub fn is_dark(&self) -> bool {
|
||||
self.mode == Mode::Dark
|
||||
}
|
||||
|
||||
/// Cross-fades the palette to `mode`.
|
||||
pub fn set_mode(mode: Mode, cx: &mut App) {
|
||||
log::info!("theme: mode -> {mode:?}");
|
||||
cx.global_mut::<Theme>().mode = mode;
|
||||
Self::fade_to_target(cx);
|
||||
}
|
||||
|
||||
pub fn set_high_contrast(on: bool, cx: &mut App) {
|
||||
log::info!("theme: high contrast -> {on}");
|
||||
cx.global_mut::<Theme>().high_contrast = on;
|
||||
Self::fade_to_target(cx);
|
||||
}
|
||||
|
||||
/// Edits non-color tokens, then repaints.
|
||||
pub fn update(cx: &mut App, edit: impl FnOnce(&mut Theme)) {
|
||||
edit(cx.global_mut::<Theme>());
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
fn target(&self) -> Palette {
|
||||
match self.mode {
|
||||
Mode::Light => Palette::light(self.high_contrast),
|
||||
Mode::Dark => Palette::dark(self.high_contrast),
|
||||
}
|
||||
}
|
||||
|
||||
fn fade_to_target(cx: &mut App) {
|
||||
let duration = motion::duration(motion::THEME, cx);
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
theme.transition += 1;
|
||||
let (id, from, to) = (theme.transition, theme.colors.clone(), theme.target());
|
||||
let start = Instant::now();
|
||||
cx.spawn(async move |cx| {
|
||||
loop {
|
||||
cx.background_executor().timer(FRAME).await;
|
||||
let t = (start.elapsed().as_secs_f32() / duration.as_secs_f32()).min(1.0);
|
||||
let running = cx.update(|cx| {
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
if theme.transition != id {
|
||||
return false;
|
||||
}
|
||||
theme.colors = from.mix(&to, motion::ease_in_out_cubic(t));
|
||||
cx.refresh_windows();
|
||||
t < 1.0
|
||||
})?;
|
||||
if !running {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
use gpui::{Hsla, Rgba, rgb, rgba};
|
||||
|
||||
pub trait Mix {
|
||||
fn mix(&self, to: &Self, t: f32) -> Self;
|
||||
}
|
||||
|
||||
impl Mix for Hsla {
|
||||
fn mix(&self, to: &Self, t: f32) -> Self {
|
||||
if t <= 0.0 {
|
||||
return *self;
|
||||
}
|
||||
if t >= 1.0 {
|
||||
return *to;
|
||||
}
|
||||
let (a, b) = (self.to_rgb(), to.to_rgb());
|
||||
let lerp = |x: f32, y: f32| x + (y - x) * t;
|
||||
Rgba {
|
||||
r: lerp(a.r, b.r),
|
||||
g: lerp(a.g, b.g),
|
||||
b: lerp(a.b, b.b),
|
||||
a: lerp(a.a, b.a),
|
||||
}
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl<const N: usize> Mix for [Hsla; N] {
|
||||
fn mix(&self, to: &Self, t: f32) -> Self {
|
||||
std::array::from_fn(|i| self[i].mix(&to[i], t))
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! mixable {
|
||||
($(#[$meta:meta])* $name:ident { $($field:ident: $ty:ty),* $(,)? }) => {
|
||||
$(#[$meta])*
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct $name {
|
||||
$(pub $field: $ty,)*
|
||||
}
|
||||
|
||||
impl Mix for $name {
|
||||
fn mix(&self, to: &Self, t: f32) -> Self {
|
||||
Self { $($field: self.$field.mix(&to.$field, t),)* }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
mixable!(
|
||||
/// Code token colors.
|
||||
Syntax {
|
||||
keyword: Hsla,
|
||||
string: Hsla,
|
||||
number: Hsla,
|
||||
comment: Hsla,
|
||||
function: Hsla,
|
||||
type_name: Hsla,
|
||||
constant: Hsla,
|
||||
property: Hsla,
|
||||
tag: Hsla,
|
||||
attribute: Hsla,
|
||||
operator: Hsla,
|
||||
punctuation: Hsla,
|
||||
variable: Hsla,
|
||||
}
|
||||
);
|
||||
|
||||
mixable!(
|
||||
/// Semantic colors. Components read these, never literals.
|
||||
Palette {
|
||||
bg: Hsla,
|
||||
surface: Hsla,
|
||||
sunken: Hsla,
|
||||
overlay: Hsla,
|
||||
hover: Hsla,
|
||||
active: Hsla,
|
||||
border: Hsla,
|
||||
border_strong: Hsla,
|
||||
fg: Hsla,
|
||||
fg_muted: Hsla,
|
||||
fg_subtle: Hsla,
|
||||
fg_disabled: Hsla,
|
||||
accent: Hsla,
|
||||
accent_hover: Hsla,
|
||||
on_accent: Hsla,
|
||||
focus: Hsla,
|
||||
link: Hsla,
|
||||
selection: Hsla,
|
||||
success: Hsla,
|
||||
warning: Hsla,
|
||||
danger: Hsla,
|
||||
info: Hsla,
|
||||
success_subtle: Hsla,
|
||||
warning_subtle: Hsla,
|
||||
danger_subtle: Hsla,
|
||||
info_subtle: Hsla,
|
||||
backdrop: Hsla,
|
||||
shadow: Hsla,
|
||||
chart: [Hsla; 8],
|
||||
ansi: [Hsla; 16],
|
||||
syntax: Syntax,
|
||||
}
|
||||
);
|
||||
|
||||
fn c(hex: u32) -> Hsla {
|
||||
rgb(hex).into()
|
||||
}
|
||||
|
||||
fn c8<const N: usize>(hex: [u32; N]) -> [Hsla; N] {
|
||||
hex.map(c)
|
||||
}
|
||||
|
||||
impl Palette {
|
||||
pub fn light(high_contrast: bool) -> Self {
|
||||
let mut palette = Self {
|
||||
bg: c(0xfcfaf7),
|
||||
surface: c(0xffffff),
|
||||
sunken: c(0xf6f4f1),
|
||||
overlay: c(0xffffff),
|
||||
hover: c(0xf2f0ed),
|
||||
active: c(0xe9e7e5),
|
||||
border: c(0xe1dfdd),
|
||||
border_strong: c(0xcccac8),
|
||||
fg: c(0x181613),
|
||||
fg_muted: c(0x605d5a),
|
||||
fg_subtle: c(0x82807d),
|
||||
fg_disabled: c(0xacaaa8),
|
||||
accent: c(0x181613),
|
||||
accent_hover: c(0x302d2b),
|
||||
on_accent: c(0xfcfaf7),
|
||||
focus: c(0x3772bb),
|
||||
link: c(0x2863ab),
|
||||
selection: rgba(0x3772bb33).into(),
|
||||
success: c(0x267b4c),
|
||||
warning: c(0xa25f12),
|
||||
danger: c(0xba3e38),
|
||||
info: c(0x2e69b2),
|
||||
success_subtle: c(0xe7f9ec),
|
||||
warning_subtle: c(0xfef2dd),
|
||||
danger_subtle: c(0xffefec),
|
||||
info_subtle: c(0xebf4ff),
|
||||
backdrop: rgba(0x18161352).into(),
|
||||
shadow: c(0x181613),
|
||||
chart: c8([
|
||||
0x5182c1, 0x009589, 0xaa732b, 0xb8636b, 0x8572bb, 0x539156, 0xb66946, 0x138db1,
|
||||
]),
|
||||
ansi: c8([
|
||||
0x181613, 0xa34942, 0x287c42, 0x8c5f00, 0x346aac, 0x875093, 0x007c84, 0xacaaa8,
|
||||
0x605d5a, 0xbd6158, 0x439458, 0xa57710, 0x4c82c6, 0xa067ac, 0x00959c, 0xcccac8,
|
||||
]),
|
||||
syntax: Syntax {
|
||||
keyword: c(0x714ca6),
|
||||
string: c(0x317a45),
|
||||
number: c(0xb3621e),
|
||||
comment: c(0x82807d),
|
||||
function: c(0x2863ab),
|
||||
type_name: c(0x1e7777),
|
||||
constant: c(0xb1582b),
|
||||
property: c(0xa14759),
|
||||
tag: c(0xa14759),
|
||||
attribute: c(0xa25f12),
|
||||
operator: c(0x605d5a),
|
||||
punctuation: c(0x605d5a),
|
||||
variable: c(0x181613),
|
||||
},
|
||||
};
|
||||
if high_contrast {
|
||||
palette.fg_muted = c(0x44423f);
|
||||
palette.fg_subtle = c(0x575552);
|
||||
palette.border = c(0xa6a4a2);
|
||||
palette.border_strong = c(0x82807d);
|
||||
}
|
||||
palette
|
||||
}
|
||||
|
||||
pub fn dark(high_contrast: bool) -> Self {
|
||||
let mut palette = Self {
|
||||
bg: c(0x131110),
|
||||
surface: c(0x1b1917),
|
||||
sunken: c(0x0e0d0b),
|
||||
overlay: c(0x201f1d),
|
||||
hover: c(0x252422),
|
||||
active: c(0x2c2b29),
|
||||
border: c(0x2c2b29),
|
||||
border_strong: c(0x413f3d),
|
||||
fg: c(0xf3f1f0),
|
||||
fg_muted: c(0xafadab),
|
||||
fg_subtle: c(0x858380),
|
||||
fg_disabled: c(0x5c5a58),
|
||||
accent: c(0xf3f1f0),
|
||||
accent_hover: c(0xd9d7d5),
|
||||
on_accent: c(0x131110),
|
||||
focus: c(0x69a1e8),
|
||||
link: c(0x7eb1f3),
|
||||
selection: rgba(0x69a1e84d).into(),
|
||||
success: c(0x6bbc89),
|
||||
warning: c(0xe8b45e),
|
||||
danger: c(0xe5756e),
|
||||
info: c(0x77abec),
|
||||
success_subtle: c(0x182c1f),
|
||||
warning_subtle: c(0x342611),
|
||||
danger_subtle: c(0x3a1d1b),
|
||||
info_subtle: c(0x19273a),
|
||||
backdrop: rgba(0x00000080).into(),
|
||||
shadow: c(0x000000),
|
||||
chart: c8([
|
||||
0x79a7e2, 0x4eb9ad, 0xcd995c, 0xdc8a90, 0xa998dd, 0x7cb57d, 0xd9906f, 0x56b2d4,
|
||||
]),
|
||||
ansi: c8([
|
||||
0x2c2b29, 0xe1897f, 0x6fb880, 0xc99d4e, 0x74a7e8, 0xc38ecf, 0x35b9c0, 0xafadab,
|
||||
0x5c5a58, 0xfda297, 0x89d298, 0xe3b667, 0x8dc1ff, 0xdda7ea, 0x56d3da, 0xf3f1f0,
|
||||
]),
|
||||
syntax: Syntax {
|
||||
keyword: c(0xbba3e8),
|
||||
string: c(0x8ac596),
|
||||
number: c(0xeaab78),
|
||||
comment: c(0x858380),
|
||||
function: c(0x85b6e9),
|
||||
type_name: c(0x84c9c8),
|
||||
constant: c(0xeda382),
|
||||
property: c(0xe29fa9),
|
||||
tag: c(0xe29fa9),
|
||||
attribute: c(0xe1b671),
|
||||
operator: c(0xafadab),
|
||||
punctuation: c(0xafadab),
|
||||
variable: c(0xf3f1f0),
|
||||
},
|
||||
};
|
||||
if high_contrast {
|
||||
palette.fg_muted = c(0xcfcdcb);
|
||||
palette.fg_subtle = c(0xafadab);
|
||||
palette.border = c(0x6a6966);
|
||||
palette.border_strong = c(0x858380);
|
||||
}
|
||||
palette
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn mix_hits_both_ends_and_the_middle() {
|
||||
let (black, white) = (c(0x000000), c(0xffffff));
|
||||
assert_eq!(black.mix(&white, 0.0).to_rgb().r, 0.0);
|
||||
assert_eq!(black.mix(&white, 1.0).to_rgb().r, 1.0);
|
||||
assert!((black.mix(&white, 0.5).to_rgb().g - 0.5).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_mix_moves_every_field() {
|
||||
let (light, dark) = (Palette::light(false), Palette::dark(false));
|
||||
assert_eq!(light.mix(&dark, 1.0), dark);
|
||||
assert_eq!(light.mix(&dark, 0.0), light);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn high_contrast_strengthens_borders() {
|
||||
let (base, hc) = (Palette::light(false), Palette::light(true));
|
||||
assert!(hc.border.l < base.border.l);
|
||||
let (base, hc) = (Palette::dark(false), Palette::dark(true));
|
||||
assert!(hc.border.l > base.border.l);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
use gpui::{BoxShadow, Hsla, Rems, point, px, rems};
|
||||
|
||||
use super::{Mode, Theme};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum Density {
|
||||
Compact,
|
||||
#[default]
|
||||
Standard,
|
||||
Comfortable,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum ControlSize {
|
||||
Sm,
|
||||
#[default]
|
||||
Md,
|
||||
Lg,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
pub enum IconSize {
|
||||
Xs,
|
||||
Sm,
|
||||
#[default]
|
||||
Md,
|
||||
Lg,
|
||||
Xl,
|
||||
Xxl,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum TextSize {
|
||||
Xs,
|
||||
Sm,
|
||||
Base,
|
||||
Md,
|
||||
Lg,
|
||||
Xl,
|
||||
Xxl,
|
||||
Display,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Radius {
|
||||
Sm,
|
||||
Md,
|
||||
Lg,
|
||||
Xl,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Elevation {
|
||||
Raised,
|
||||
Floating,
|
||||
Modal,
|
||||
}
|
||||
|
||||
fn px_to_rems(value: f32) -> Rems {
|
||||
rems(value / 16.0)
|
||||
}
|
||||
|
||||
impl Theme {
|
||||
pub fn control_height(&self, size: ControlSize) -> Rems {
|
||||
let base = match size {
|
||||
ControlSize::Sm => 24.0,
|
||||
ControlSize::Md => 28.0,
|
||||
ControlSize::Lg => 32.0,
|
||||
};
|
||||
let delta = match self.density {
|
||||
Density::Compact => -4.0,
|
||||
Density::Standard => 0.0,
|
||||
Density::Comfortable => 4.0,
|
||||
};
|
||||
px_to_rems(base + delta)
|
||||
}
|
||||
|
||||
pub fn control_padding(&self, size: ControlSize) -> Rems {
|
||||
let base = match size {
|
||||
ControlSize::Sm => 8.0,
|
||||
ControlSize::Md => 12.0,
|
||||
ControlSize::Lg => 16.0,
|
||||
};
|
||||
let delta = match self.density {
|
||||
Density::Compact => -2.0,
|
||||
Density::Standard => 0.0,
|
||||
Density::Comfortable => 2.0,
|
||||
};
|
||||
px_to_rems(base + delta)
|
||||
}
|
||||
|
||||
pub fn icon_size(&self, size: IconSize) -> Rems {
|
||||
let base = match size {
|
||||
IconSize::Xs => 12.0,
|
||||
IconSize::Sm => 14.0,
|
||||
IconSize::Md => 16.0,
|
||||
IconSize::Lg => 20.0,
|
||||
IconSize::Xl => 24.0,
|
||||
IconSize::Xxl => 32.0,
|
||||
};
|
||||
px_to_rems(base * self.font_scale)
|
||||
}
|
||||
|
||||
pub fn text_size(&self, size: TextSize) -> Rems {
|
||||
let base = match size {
|
||||
TextSize::Xs => 11.0,
|
||||
TextSize::Sm => 12.0,
|
||||
TextSize::Base => 13.0,
|
||||
TextSize::Md => 14.0,
|
||||
TextSize::Lg => 16.0,
|
||||
TextSize::Xl => 20.0,
|
||||
TextSize::Xxl => 24.0,
|
||||
TextSize::Display => 32.0,
|
||||
};
|
||||
px_to_rems(base * self.font_scale)
|
||||
}
|
||||
|
||||
pub fn radius(&self, size: Radius) -> Rems {
|
||||
let base = match size {
|
||||
Radius::Sm => 4.0,
|
||||
Radius::Md => 6.0,
|
||||
Radius::Lg => 8.0,
|
||||
Radius::Xl => 12.0,
|
||||
};
|
||||
px_to_rems(base * self.radius_scale)
|
||||
}
|
||||
|
||||
pub fn elevation(&self, level: Elevation) -> Vec<BoxShadow> {
|
||||
let dark = self.mode == Mode::Dark;
|
||||
let layers: &[(f32, f32, f32)] = match (level, dark) {
|
||||
(Elevation::Raised, false) => &[(1.0, 2.0, 0.06)],
|
||||
(Elevation::Floating, false) => &[(1.0, 2.0, 0.04), (6.0, 16.0, 0.08)],
|
||||
(Elevation::Modal, false) => &[(2.0, 4.0, 0.04), (16.0, 40.0, 0.14)],
|
||||
(Elevation::Raised, true) => &[(1.0, 2.0, 0.4)],
|
||||
(Elevation::Floating, true) => &[(2.0, 4.0, 0.3), (8.0, 24.0, 0.45)],
|
||||
(Elevation::Modal, true) => &[(4.0, 8.0, 0.3), (20.0, 48.0, 0.55)],
|
||||
};
|
||||
layers
|
||||
.iter()
|
||||
.map(|&(y, blur, alpha)| shadow(self.colors.shadow.alpha(alpha), y, blur))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn focus_ring(&self) -> Vec<BoxShadow> {
|
||||
vec![BoxShadow {
|
||||
color: self.colors.focus.alpha(0.45),
|
||||
offset: point(px(0.0), px(0.0)),
|
||||
blur_radius: px(0.0),
|
||||
spread_radius: px(2.0),
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
fn shadow(color: Hsla, y: f32, blur: f32) -> BoxShadow {
|
||||
BoxShadow {
|
||||
color,
|
||||
offset: point(px(0.0), px(y)),
|
||||
blur_radius: px(blur),
|
||||
spread_radius: px(0.0),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user