remove the multi-theme system

This commit is contained in:
2026-10-09 15:58:26 +07:00
parent 8e34ddabcb
commit 7dcc8033c9
25 changed files with 214 additions and 2718 deletions
+56 -425
View File
@@ -2,486 +2,117 @@ use gpui::Hsla;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use crate::paint::{ink_for, plate_active_for, plate_hover_for, wash_for};
use crate::syntax::SyntaxPalette;
use crate::{ThemeMode, palette};
/// The full token set.
///
/// The first groups are the legacy coop tokens; they stay until every call
/// site has moved to the bezel tokens (see docs/ui-theme-bezel-migration.md),
/// and are derived from the tokens by [`ThemeColors::sync_legacy`]. The bezel
/// token groups follow. Deserialization refines over the built-in dark
/// palette, so a JSON that names only some fields still yields a complete
/// palette.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize, JsonSchema)]
#[serde(default)]
pub struct ThemeColors {
// Surface colors
pub background: Hsla,
pub surface_background: Hsla,
pub elevated_surface_background: Hsla,
pub panel_background: Hsla,
pub overlay: Hsla,
pub title_bar: Hsla,
pub title_bar_inactive: Hsla,
pub window_border: Hsla,
/// The main content plane.
pub bg: Hsla,
/// The shell / sidebar plane.
pub surface: Hsla,
/// Opaque pills and chips raised above the panel.
pub surface_raised: Hsla,
/// An inline card resting on the main plane.
pub surface_card: Hsla,
/// A modal dialog, over the scrim.
pub surface_dialog: Hsla,
/// Popovers and menus — the highest plane.
pub surface_overlay: Hsla,
/// Row hover — translucent ink over whatever is beneath.
pub element_hover: Hsla,
/// Pressed or selected, one rung past [`ThemeColors::element_hover`].
pub element_active: Hsla,
// Border colors
/// The standard hairline border.
pub border: Hsla,
pub border_variant: Hsla,
pub border_focused: Hsla,
pub border_selected: Hsla,
pub border_transparent: Hsla,
pub border_disabled: Hsla,
/// Dividers inside cards, nested frames.
pub border_faint: Hsla,
/// Focused or raised edges.
pub border_strong: Hsla,
/// The focus ring.
pub ring: Hsla,
// Text colors
/// Primary text.
pub text: Hsla,
/// Secondary text.
pub text_muted: Hsla,
pub text_placeholder: Hsla,
pub text_accent: Hsla,
pub text_danger: Hsla,
pub text_warning: Hsla,
// Icon colors
pub icon: Hsla,
pub icon_muted: Hsla,
pub icon_accent: Hsla,
// Element colors
pub element_foreground: Hsla,
pub element_background: Hsla,
pub element_hover: Hsla,
pub element_active: Hsla,
pub element_selected: Hsla,
pub element_disabled: Hsla,
// Secondary element colors
pub secondary_foreground: Hsla,
pub secondary_background: Hsla,
pub secondary_hover: Hsla,
pub secondary_active: Hsla,
pub secondary_selected: Hsla,
pub secondary_disabled: Hsla,
// Danger element colors
pub danger_foreground: Hsla,
pub danger_background: Hsla,
pub danger_hover: Hsla,
pub danger_active: Hsla,
pub danger_selected: Hsla,
pub danger_disabled: Hsla,
// Warning element colors
pub warning_foreground: Hsla,
pub warning_background: Hsla,
pub warning_hover: Hsla,
pub warning_active: Hsla,
pub warning_selected: Hsla,
pub warning_disabled: Hsla,
// Ghost element colors
pub ghost_element_background: Hsla,
pub ghost_element_background_alt: Hsla,
pub ghost_element_hover: Hsla,
pub ghost_element_active: Hsla,
pub ghost_element_selected: Hsla,
pub ghost_element_disabled: Hsla,
// Tab colors
pub tab_background: Hsla,
pub tab_foreground: Hsla,
pub tab_hover_background: Hsla,
pub tab_active_background: Hsla,
pub tab_active_foreground: Hsla,
// Scrollbar colors
pub scrollbar_thumb_background: Hsla,
pub scrollbar_thumb_hover_background: Hsla,
pub scrollbar_thumb_border: Hsla,
pub scrollbar_track_background: Hsla,
pub scrollbar_track_border: Hsla,
// Interactive colors
pub drop_target_background: Hsla,
pub cursor: Hsla,
pub selection: Hsla,
// Bezel tokens: neutral surfaces
/// Main content panel.
pub bg: Hsla,
/// Shell / sidebar surface.
pub surface: Hsla,
/// Raised surface: opaque pills and chips that sit proud of the panel.
pub surface_raised: Hsla,
/// Inline card resting on the main panel.
pub surface_card: Hsla,
/// Modal dialog, floating over the scrim.
pub surface_dialog: Hsla,
/// Popover, menu and command-palette surface — the highest plane.
pub surface_overlay: Hsla,
// Bezel tokens: hairlines
/// Faintest hairline: dividers inside a card, nested frames.
pub border_faint: Hsla,
/// Stronger border for focused/raised edges.
pub border_strong: Hsla,
// Bezel tokens: text
/// Faint text: placeholders, disabled.
/// Placeholders and disabled text.
pub text_faint: Hsla,
/// One notch below `text_muted` — the diff file-path tone.
/// Diff file paths — one notch below [`ThemeColors::text_muted`].
pub text_dim: Hsla,
// Bezel tokens: high-contrast solid
/// The maximum-contrast solid fill: the primary button plate.
/// The text cursor.
pub cursor: Hsla,
/// The selected-text wash.
pub selection: Hsla,
/// The primary button plate — the maximum-contrast fill.
pub solid: Hsla,
/// Label/icon color on top of [`ThemeColors::solid`] — its inverse.
/// Label ink on [`ThemeColors::solid`].
pub on_solid: Hsla,
// Bezel tokens: accents
/// Accent — the emphasis weight for text and icons. Neutral by default.
/// Emphasis for text and icons; neutral by default.
pub accent: Hsla,
/// Stronger accent for fills that carry [`ThemeColors::on_accent`].
/// Accent fills carrying [`ThemeColors::on_accent`].
pub accent_strong: Hsla,
/// Label color on top of [`ThemeColors::accent_strong`].
/// Label ink on [`ThemeColors::accent_strong`].
pub on_accent: Hsla,
// Bezel tokens: semantic
/// Danger — red (errors, stop button).
/// Errors and destructive actions.
pub danger: Hsla,
/// Softer danger for secondary/inline error copy.
/// Inline error copy.
pub danger_muted: Hsla,
/// Destructive-action button fill (danger plate, carries `on_accent`).
/// The destructive plate — white ink in both appearances.
pub danger_strong: Hsla,
/// Warning — amber.
/// Warnings.
pub warning: Hsla,
/// Softer warning for secondary copy.
/// The translucent warning wash behind chips and banners.
pub warning_muted: Hsla,
/// Success / online — emerald.
/// Success and online presence.
pub success: Hsla,
/// Softer success for text on a success-tinted chip.
/// Text on success-tinted chips.
pub success_muted: Hsla,
/// Working / streaming indicator — pink.
/// Working or streaming states.
pub busy: Hsla,
// Bezel tokens: components
/// Hover tone for an *opaque* raised pill.
/// Hover over [`ThemeColors::surface_raised`].
pub surface_raised_hover: Hsla,
/// Recessed band behind a header or footer strip.
/// A recessed header / footer strip.
pub band: Hsla,
/// Input plates.
/// Input backgrounds.
pub input_bg: Hsla,
/// Text caret.
/// The text caret.
pub caret: Hsla,
/// Where a dragged block would land.
/// The line marking where a drag will land.
pub drop_line: Hsla,
/// Fill of the region a dragged pane would dock into.
/// The fill of the region a dragged pane would dock into.
pub drop_target: Hsla,
// Bezel tokens: code & diff
/// Inline-code text.
pub code_text: Hsla,
/// Inline-code wash behind [`ThemeColors::code_text`].
/// The wash behind [`ThemeColors::code_text`].
pub code_wash: Hsla,
/// Shared paint-only syntax palette.
/// The shared syntax palette, read at paint time.
pub syntax: SyntaxPalette,
/// Diff: added lines.
/// Diff additions.
pub diff_add: Hsla,
/// Diff: deleted lines.
/// Diff deletions.
pub diff_del: Hsla,
/// Diff: hunk-header wash.
/// The diff hunk-header wash.
pub diff_hunk_bg: Hsla,
}
impl Default for ThemeColors {
fn default() -> Self {
Self::dark()
palette::dark()
}
}
impl ThemeColors {
/// Returns the default colors for light themes.
pub(crate) fn light() -> Self {
palette::light()
}
/// Returns the default colors for dark themes.
pub(crate) fn dark() -> Self {
palette::dark()
}
/// Returns the default colors for an appearance.
pub fn for_appearance(appearance: ThemeMode) -> Self {
palette::for_appearance(appearance)
}
/// A placeholder instance whose legacy fields the palette overwrites.
pub(crate) fn legacy_default() -> Self {
let transparent = gpui::transparent_black();
Self {
background: transparent,
surface_background: transparent,
elevated_surface_background: transparent,
panel_background: transparent,
overlay: transparent,
title_bar: transparent,
title_bar_inactive: transparent,
window_border: transparent,
border: transparent,
border_variant: transparent,
border_focused: transparent,
border_selected: transparent,
border_transparent: transparent,
border_disabled: transparent,
ring: transparent,
text: transparent,
text_muted: transparent,
text_placeholder: transparent,
text_accent: transparent,
text_danger: transparent,
text_warning: transparent,
icon: transparent,
icon_muted: transparent,
icon_accent: transparent,
element_foreground: transparent,
element_background: transparent,
element_hover: transparent,
element_active: transparent,
element_selected: transparent,
element_disabled: transparent,
secondary_foreground: transparent,
secondary_background: transparent,
secondary_hover: transparent,
secondary_active: transparent,
secondary_selected: transparent,
secondary_disabled: transparent,
danger_foreground: transparent,
danger_background: transparent,
danger_hover: transparent,
danger_active: transparent,
danger_selected: transparent,
danger_disabled: transparent,
warning_foreground: transparent,
warning_background: transparent,
warning_hover: transparent,
warning_active: transparent,
warning_selected: transparent,
warning_disabled: transparent,
ghost_element_background: transparent,
ghost_element_background_alt: transparent,
ghost_element_hover: transparent,
ghost_element_active: transparent,
ghost_element_selected: transparent,
ghost_element_disabled: transparent,
tab_background: transparent,
tab_foreground: transparent,
tab_hover_background: transparent,
tab_active_background: transparent,
tab_active_foreground: transparent,
scrollbar_thumb_background: transparent,
scrollbar_thumb_hover_background: transparent,
scrollbar_thumb_border: transparent,
scrollbar_track_background: transparent,
scrollbar_track_border: transparent,
drop_target_background: transparent,
cursor: transparent,
selection: transparent,
bg: transparent,
surface: transparent,
surface_raised: transparent,
surface_card: transparent,
surface_dialog: transparent,
surface_overlay: transparent,
border_faint: transparent,
border_strong: transparent,
text_faint: transparent,
text_dim: transparent,
solid: transparent,
on_solid: transparent,
accent: transparent,
accent_strong: transparent,
on_accent: transparent,
danger: transparent,
danger_muted: transparent,
danger_strong: transparent,
warning: transparent,
warning_muted: transparent,
success: transparent,
success_muted: transparent,
busy: transparent,
surface_raised_hover: transparent,
band: transparent,
input_bg: transparent,
caret: transparent,
drop_line: transparent,
drop_target: transparent,
code_text: transparent,
code_wash: transparent,
syntax: crate::syntax::SyntaxPalette::dark(transparent, transparent, transparent),
diff_add: transparent,
diff_del: transparent,
diff_hunk_bg: transparent,
}
}
/// Derive the legacy fields from the tokens.
pub(crate) fn sync_legacy(&mut self, appearance: ThemeMode) {
// Surfaces
self.background = self.bg;
self.surface_background = self.surface;
self.elevated_surface_background = self.surface_card;
self.panel_background = self.surface;
self.overlay = ink_for(appearance, 0.10);
self.title_bar = gpui::transparent_black();
self.title_bar_inactive = self.bg;
self.window_border = self.border;
// Borders
self.border_variant = self.border_faint;
self.border_focused = self.border_strong;
self.border_selected = self.border_strong;
self.border_transparent = gpui::transparent_black();
self.border_disabled = self.border_faint;
// Text
self.text_placeholder = self.text_faint;
self.text_accent = self.accent;
self.text_danger = self.danger;
self.text_warning = self.warning;
// Icons
self.icon = self.text_muted;
self.icon_muted = self.text_faint;
self.icon_accent = self.accent;
// Elements
self.element_foreground = self.on_solid;
self.element_background = self.solid;
self.element_selected = self.element_active;
self.element_disabled = wash_for(appearance, 0.06);
// Secondary
self.secondary_foreground = self.text;
self.secondary_background = self.surface_raised;
self.secondary_hover = self.element_hover;
self.secondary_active = self.element_active;
self.secondary_selected = self.element_active;
self.secondary_disabled = wash_for(appearance, 0.06);
// Danger
self.danger_foreground = self.on_accent;
self.danger_background = self.danger_strong;
self.danger_hover = plate_hover_for(appearance, self.danger_strong);
self.danger_active = plate_active_for(appearance, self.danger_strong);
self.danger_selected = self.danger_muted;
self.danger_disabled = wash_for(appearance, 0.06);
// Warning
// warning_muted is a translucent amber wash, so its ink is the amber
// token itself, and its state washes deepen the plate.
self.warning_foreground = self.warning;
self.warning_background = self.warning_muted;
self.warning_hover = plate_hover_for(appearance, self.warning_muted);
self.warning_active = plate_active_for(appearance, self.warning_muted);
self.warning_selected = self.element_active;
self.warning_disabled = wash_for(appearance, 0.06);
// Ghost
self.ghost_element_background = gpui::transparent_black();
self.ghost_element_background_alt = self.surface_raised;
self.ghost_element_hover = self.element_hover;
self.ghost_element_active = self.element_active;
self.ghost_element_selected = self.element_active;
self.ghost_element_disabled = wash_for(appearance, 0.06);
// Tabs
self.tab_background = self.surface;
self.tab_foreground = self.text_muted;
self.tab_hover_background = self.element_hover;
self.tab_active_background = self.element_active;
self.tab_active_foreground = self.text;
// Scrollbar
self.scrollbar_thumb_background = ink_for(appearance, 0.20);
self.scrollbar_thumb_hover_background = ink_for(appearance, 0.30);
self.scrollbar_thumb_border = gpui::transparent_black();
self.scrollbar_track_background = gpui::transparent_black();
self.scrollbar_track_border = self.border_faint;
// Interactive
self.drop_target_background = self.drop_target;
}
/// Best-effort re-derivation of the tokens from a legacy palette, for
/// old-schema JSON themes loaded through the registry. Fields with no
/// legacy source (`success`, `busy`, the syntax palette, the diff colors)
/// keep the built-in palette's value for `appearance`.
pub fn rederive_tokens(&mut self, appearance: ThemeMode) {
let base = Self::for_appearance(appearance);
self.success = base.success;
self.success_muted = base.success_muted;
self.busy = base.busy;
self.syntax = base.syntax;
self.diff_add = base.diff_add;
self.diff_del = base.diff_del;
self.diff_hunk_bg = base.diff_hunk_bg;
self.bg = self.background;
self.surface = self.surface_background;
self.surface_raised = self.elevated_surface_background;
self.surface_card = self.elevated_surface_background;
self.surface_dialog = self.elevated_surface_background;
self.surface_overlay = self.elevated_surface_background;
self.border_faint = self.border_variant;
self.border_strong = self.border_focused;
self.text_faint = self.text_placeholder;
self.text_dim = self.text_muted;
self.solid = self.element_background;
self.on_solid = self.element_foreground;
self.accent = self.text_accent;
self.accent_strong = self.element_background;
self.on_accent = self.element_foreground;
self.danger = self.text_danger;
self.danger_muted = self.text_danger;
self.danger_strong = self.danger_background;
self.warning = self.text_warning;
self.warning_muted = self.warning_background;
self.surface_raised_hover = self.element_hover;
self.band = self.ghost_element_background_alt;
self.input_bg = self.surface_background;
self.caret = self.cursor;
self.drop_line = self.border_focused;
self.drop_target = self.drop_target_background;
self.code_text = self.text;
self.code_wash = self.ghost_element_hover;
}
}
+9 -89
View File
@@ -1,7 +1,5 @@
use std::ops::{Deref, DerefMut};
use std::rc::Rc;
use appearance::AppearanceMode;
use gpui::{App, Global, Pixels, SharedString, Window, px};
mod brand;
@@ -13,14 +11,13 @@ mod notification;
mod paint;
mod palette;
mod platform_kind;
mod registry;
mod scale;
mod scrollbar_mode;
mod syntax;
mod theme;
pub mod appearance;
pub use appearance::AppearanceMode;
pub use brand::{BASE_COLORS, Brand, Ink, TextInk, Tint};
pub use color::{
contrast_ratio, flatten, grey, hsl_to_rgb, lightness, mix, neutral, oklch, oklch_to_srgb,
@@ -38,8 +35,6 @@ pub use paint::{
plate_active, plate_hover, scrim, set_current_appearance, surface_shadows, wash,
};
pub use platform_kind::PlatformKind;
pub use registry::*;
pub use scale::*;
pub use scrollbar_mode::*;
pub use syntax::{HighlightKind, SyntaxPalette};
pub use theme::*;
@@ -67,13 +62,11 @@ pub const TABBAR_HEIGHT: Pixels = px(36.0);
pub const SIDEBAR_WIDTH: Pixels = px(240.);
pub fn init(cx: &mut App) {
registry::init(cx);
appearance::init(AppearanceMode::System, cx);
Theme::sync_scrollbar_appearance(cx);
}
/// An alternative palette source. Runs first on every appearance install, so
/// an override survives a light/dark switch.
/// An alternative palette source.
pub type PaletteBuilder = fn(ThemeMode) -> ThemeColors;
struct GlobalPaletteBuilder(Option<PaletteBuilder>);
@@ -82,13 +75,8 @@ impl Global for GlobalPaletteBuilder {}
/// Install a brand or an alternative palette on the app.
pub trait AppExt {
/// Install a brand and repaint. Call before [`appearance::init`] to have
/// the first frame already branded.
fn set_brand(&mut self, brand: Brand);
/// Install an alternative palette builder and repaint. The builder runs
/// first on every appearance install.
fn set_palette(&mut self, build: PaletteBuilder);
/// Change the appearance preference (`System`, `Light` or `Dark`).
fn set_appearance_mode(&mut self, mode: AppearanceMode);
}
@@ -119,13 +107,6 @@ fn reinstall(cx: &mut App) {
}
/// Mirror the active coop theme into the `gpui-base` global theme.
///
/// Base paints a few things from its own tokens -- the focus ring, the wash
/// behind selected text, scrollbars, and overlay backdrops -- so the two
/// globals have to agree or those details drift away from the palette.
///
/// This is a no-op before the coop theme global exists; [`Theme::change`] is
/// the authoritative hook that keeps the projection current.
pub fn sync_base(cx: &mut App) {
let Some(theme) = cx.try_global::<Theme>() else {
return;
@@ -163,8 +144,6 @@ pub fn sync_base(cx: &mut App) {
tokens.accent = colors.element_hover;
tokens.accent_foreground = colors.text;
tokens.destructive = colors.danger_strong;
// Bezel's destructive pairing: white ink on the red plate in both
// appearances.
tokens.destructive_foreground = gpui::white();
tokens.border = colors.border;
tokens.input = colors.border;
@@ -185,41 +164,14 @@ impl ActiveTheme for App {
#[derive(Debug, Clone)]
pub struct Theme {
/// Theme colors
pub colors: ThemeColors,
/// Theme family
pub theme: Rc<ThemeFamily>,
/// The appearance of the theme (light or dark).
pub mode: ThemeMode,
/// The font family for the application.
pub font_family: SharedString,
/// The family prose is set in — what markdown shapes a document's body
/// with. Defaults to `font_family`.
pub font_body: SharedString,
/// Monospace family for code and terminal.
pub font_mono: SharedString,
/// The root font size for the application, default is 15px.
pub font_size: Pixels,
/// Radius for the general elements.
pub radius: Pixels,
/// Radius for the large elements, e.g.: dialog, notification.
pub radius_lg: Pixels,
/// Enable shadow for the general elements. default is true
pub shadow: bool,
/// Show the scrollbar mode, default: scrolling
pub scrollbar_mode: ScrollbarMode,
/// Notification settings
pub notification: NotificationSettings,
}
@@ -269,26 +221,13 @@ impl Theme {
};
}
/// Apply a new theme to the application.
pub fn apply_theme(new_theme: Rc<ThemeFamily>, window: Option<&mut Window>, cx: &mut App) {
let theme = cx.global_mut::<Theme>();
let mode = theme.mode;
// Update the theme
theme.theme = new_theme;
// Emit a theme change event
Self::change(mode, window, cx);
}
/// Change the app's appearance
pub fn change<M>(mode: M, window: Option<&mut Window>, cx: &mut App)
where
M: Into<ThemeMode>,
{
if !cx.has_global::<Theme>() {
let default_theme = ThemeFamily::default();
let theme = Theme::from(default_theme);
cx.set_global(theme);
cx.set_global(Theme::default());
}
let mode = mode.into();
@@ -297,23 +236,13 @@ impl Theme {
.and_then(|builder| builder.0);
let mut colors = match builder {
Some(build) => build(mode),
None => {
let family = Theme::global_mut(cx).theme.clone();
if mode.is_dark() {
*family.dark()
} else {
*family.light()
}
}
None => ThemeColors::for_appearance(mode),
};
// A default brand changes nothing, so a JSON theme's own colors are
// left alone; anything else rotates the tokens and re-derives the
// legacy fields from them.
// A default brand changes nothing; anything else rotates the tokens.
let installed_brand = brand::installed(cx);
if !installed_brand.is_default() {
installed_brand.apply(mode, &mut colors);
colors.sync_legacy(mode);
}
let theme = Theme::global_mut(cx);
@@ -338,8 +267,8 @@ impl Theme {
}
}
impl From<ThemeFamily> for Theme {
fn from(family: ThemeFamily) -> Self {
impl Default for Theme {
fn default() -> Self {
let platform = PlatformKind::platform();
let mode = ThemeMode::default();
@@ -349,25 +278,16 @@ impl From<ThemeFamily> for Theme {
_ => ".SystemUIFont",
};
// Define the theme colors based on the appearance
let colors = match mode {
ThemeMode::Light => family.light(),
ThemeMode::Dark => family.dark(),
};
Theme {
Self {
font_size: px(15.),
font_family: font_family.into(),
font_body: font_family.into(),
font_mono: system_mono().into(),
radius: px(layout::button_radius()),
radius_lg: px(layout::surface_radius()),
shadow: true,
scrollbar_mode: ScrollbarMode::default(),
notification: NotificationSettings::default(),
mode,
colors: *colors,
theme: Rc::new(family),
colors: ThemeColors::for_appearance(mode),
}
}
}
+27 -84
View File
@@ -1,63 +1,44 @@
//! The two concrete palettes: dark and light. Ported from bezel
//! (`crates/theme/src/theme/palettes.rs`, MIT License, Copyright (c) 2026
//! bezel contributors).
//!
//! Light is designed, not inverted — mirroring lightness reverses surface
//! order, elevation and accent contrast. Each light text token lands within
//! ~0.5 of its dark counterpart's ratio; `tests/contrast.rs` asserts it.
use gpui::{hsla, rgb};
use crate::paint::{band_for, hairline_for};
use crate::syntax::SyntaxPalette;
use crate::{Ink, TextInk, ThemeColors, ThemeMode, color};
/// Build the dark palette. The surface tones are sampled from bezel's
/// reference screenshots: main panel `#060606`, shell/sidebar `#0d0d0d`.
pub(crate) fn dark() -> ThemeColors {
let mut colors = ThemeColors {
// ---- neutral surfaces ----
bg: color::grey(6), // main panel — #060606
surface: color::grey(13), // shell / sidebar — #0d0d0d
let warning = color::oklch(0.828, 0.189, 84.429);
ThemeColors {
bg: color::grey(6),
surface: color::grey(13),
surface_raised: color::neutral(0.235),
surface_card: color::grey(0x0e),
surface_dialog: color::grey(0x10),
surface_overlay: color::grey(0x16),
// The reference app's `--color-hover`, pure ink: white at 8%.
element_hover: hsla(0.0, 0.0, 1.0, 0.08),
// `--color-active`, a rung above the hover: white at 12%.
element_active: hsla(0.0, 0.0, 1.0, 0.12),
// ---- hairlines ----
border_faint: hairline_for(ThemeMode::Dark, 0.06),
border: hairline_for(ThemeMode::Dark, 0.08),
border_strong: hairline_for(ThemeMode::Dark, 0.14),
// ---- text ----
text: TextInk::color(ThemeMode::Dark, Ink::APPKIT.dark.text),
text_muted: TextInk::color(ThemeMode::Dark, Ink::APPKIT.dark.muted),
text_faint: TextInk::color(ThemeMode::Dark, Ink::APPKIT.dark.faint),
text_dim: color::grey(0x98),
// ---- high-contrast solid ----
solid: color::neutral(0.922),
on_solid: color::grey(0x0e),
// ---- accents, neutral by default ----
accent: color::neutral(0.673),
accent_strong: color::neutral(0.922),
on_accent: color::grey(0x0e),
// ---- semantic ----
danger: color::oklch(0.704, 0.191, 22.216), // red-400
danger_muted: color::oklch(0.808, 0.114, 19.571), // red-300
danger: color::oklch(0.704, 0.191, 22.216),
danger_muted: color::oklch(0.808, 0.114, 19.571),
danger_strong: color::oklch(0.58, 0.16, 25.0),
warning: color::oklch(0.828, 0.189, 84.429), // amber-400
success: color::oklch(0.765, 0.177, 163.223), // emerald-400
success_muted: color::oklch(0.845, 0.143, 164.978), // emerald-300
busy: color::oklch(0.718, 0.202, 349.761), // pink-400
success: color::oklch(0.765, 0.177, 163.223),
success_muted: color::oklch(0.845, 0.143, 164.978),
busy: color::oklch(0.718, 0.202, 349.761),
// ---- components ----
surface_raised_hover: color::neutral(0.29),
band: band_for(ThemeMode::Dark),
input_bg: hsla(0.0, 0.0, 1.0, 0.03),
@@ -68,7 +49,6 @@ pub(crate) fn dark() -> ThemeColors {
drop_line: hairline_for(ThemeMode::Dark, 0.35),
drop_target: hsla(0.0, 0.0, 1.0, 0.10),
// ---- code & diff ----
code_text: color::neutral(0.94),
code_wash: hsla(0.0, 0.0, 1.0, 0.08),
syntax: SyntaxPalette::dark(
@@ -76,82 +56,54 @@ pub(crate) fn dark() -> ThemeColors {
color::neutral(0.60),
color::oklch(0.704, 0.191, 22.216),
),
diff_add: color::oklch(0.765, 0.177, 163.223), // emerald-400
diff_del: color::oklch(0.704, 0.191, 22.216), // red-400
diff_add: color::oklch(0.765, 0.177, 163.223),
diff_del: color::oklch(0.704, 0.191, 22.216),
diff_hunk_bg: hsla(0.6, 0.35, 0.6, 0.05),
..ThemeColors::legacy_default()
};
// Same warning-chip rule as light: a translucent amber wash under the
// `warning` token, not an opaque plate.
colors.warning_muted = colors.warning.alpha(0.16);
colors.sync_legacy(ThemeMode::Dark);
colors
warning,
warning_muted: warning.alpha(0.16),
}
}
/// Build the light palette. Neutrals are the same oklch scale read from the
/// other end, but the *roles* are reassigned rather than mirrored: content
/// plane white, chrome grey, raised surfaces white-plus-shadow. Text tones
/// reproduce the dark theme's contrast ratios, and accents drop from the 400
/// to the 600 step at identical hue so they clear WCAG AA on white.
pub(crate) fn light() -> ThemeColors {
let mut colors = ThemeColors {
// ---- neutral surfaces ----
bg: color::grey(0xff), // main panel — clean white
// Deeper than ~neutral-100 looks on paper: the content card is pure
// white and sits *inside* this surface, so too small a step leaves the
// whole window one flat sheet with a hairline drawn on it.
let warning = color::oklch(0.555, 0.163, 48.998);
ThemeColors {
bg: color::grey(0xff),
surface: color::neutral(0.968),
// A real grey, NOT white: this is the opaque-plate tone, and those sit
// directly on the white content plane with no border or shadow to save
// them.
surface_raised: color::neutral(0.940),
surface_card: color::grey(0xff),
surface_dialog: color::grey(0xff),
surface_overlay: color::grey(0xff),
// The same token in light: black at 4%.
element_hover: hsla(0.0, 0.0, 0.0, 0.04),
// The same rung in light: black at 6%.
element_active: hsla(0.0, 0.0, 0.0, 0.06),
// ---- hairlines ----
border_faint: hairline_for(ThemeMode::Light, 0.06),
border: hairline_for(ThemeMode::Light, 0.08),
border_strong: hairline_for(ThemeMode::Light, 0.14),
// ---- text ----
text: TextInk::color(ThemeMode::Light, Ink::APPKIT.light.text),
// One rung darker than AppKit's secondary label: sidebar and list
// labels sit on the chrome grey and need ~5:1, not the system ~4:1.
text_muted: color::neutral(0.45),
text_faint: TextInk::color(ThemeMode::Light, Ink::APPKIT.light.faint),
text_dim: color::neutral(0.50),
// ---- high-contrast solid ----
solid: color::neutral(0.205),
on_solid: color::neutral(0.985),
// ---- accents, neutral by default ----
accent: color::neutral(0.511),
accent_strong: color::neutral(0.205),
on_accent: color::neutral(0.985),
// ---- semantic ----
danger: color::oklch(0.577, 0.245, 27.325), // red-600
danger_muted: color::oklch(0.505, 0.213, 27.518), // red-700
danger: color::oklch(0.577, 0.245, 27.325),
danger_muted: color::oklch(0.505, 0.213, 27.518),
danger_strong: color::oklch(0.51, 0.20, 25.0),
warning: color::oklch(0.555, 0.163, 48.998), // amber-700 — carries 12px text
success: color::oklch(0.596, 0.145, 163.225), // emerald-600
success_muted: color::oklch(0.508, 0.118, 165.612), // emerald-700
busy: color::oklch(0.592, 0.249, 0.584), // pink-600
warning,
warning_muted: warning.alpha(0.10),
success: color::oklch(0.596, 0.145, 163.225),
success_muted: color::oklch(0.508, 0.118, 165.612),
busy: color::oklch(0.592, 0.249, 0.584),
// ---- components ----
// Opaque pills darken on hover here rather than brighten — same
// "brighten the plate, don't wash it out" rule, read the other way.
surface_raised_hover: color::neutral(0.900),
band: band_for(ThemeMode::Light),
// Inputs read as one rung below the chrome grey — the old scale's
// step-3 input plate. Pure white vanished against the content plane.
input_bg: color::neutral(0.950),
selection: rgb(0xb3d7ff).into(),
cursor: color::neutral(0.205),
@@ -160,7 +112,6 @@ pub(crate) fn light() -> ThemeColors {
drop_line: hairline_for(ThemeMode::Light, 0.35),
drop_target: hsla(0.0, 0.0, 0.0, 0.06),
// ---- code & diff ----
code_text: color::neutral(0.18),
code_wash: hsla(0.0, 0.0, 0.0, 0.06),
syntax: SyntaxPalette::light(
@@ -168,18 +119,10 @@ pub(crate) fn light() -> ThemeColors {
color::neutral(0.48),
color::oklch(0.505, 0.213, 27.518),
),
diff_add: color::oklch(0.596, 0.145, 163.225), // emerald-600
diff_del: color::oklch(0.577, 0.245, 27.325), // red-600
diff_add: color::oklch(0.596, 0.145, 163.225),
diff_del: color::oklch(0.577, 0.245, 27.325),
diff_hunk_bg: hsla(0.6, 0.35, 0.35, 0.07),
..ThemeColors::legacy_default()
};
// The warning chip is a translucent amber wash, not an opaque plate: the
// 700-800 amber reads as muddy brown, and a wash lets the label ride on
// the `warning` token in both appearances.
colors.warning_muted = colors.warning.alpha(0.10);
colors.sync_legacy(ThemeMode::Light);
colors
}
}
pub(crate) fn for_appearance(appearance: ThemeMode) -> ThemeColors {
-73
View File
@@ -1,73 +0,0 @@
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::Arc;
use anyhow::{Context as AnyhowContext, Error};
use gpui::{App, AppContext, AssetSource, Context, Entity, Global, SharedString};
use crate::{ThemeFamily, ThemeMode};
pub fn init(cx: &mut App) {
ThemeRegistry::set_global(cx.new(ThemeRegistry::new), cx);
}
struct GlobalThemeRegistry(Entity<ThemeRegistry>);
impl Global for GlobalThemeRegistry {}
pub struct ThemeRegistry {
/// Map of theme names to theme families
themes: HashMap<SharedString, Rc<ThemeFamily>>,
}
impl ThemeRegistry {
/// Retrieve the global theme registry state
pub fn global(cx: &App) -> Entity<Self> {
cx.global::<GlobalThemeRegistry>().0.clone()
}
/// Set the global theme registry instance
pub(crate) fn set_global(state: Entity<Self>, cx: &mut App) {
cx.set_global(GlobalThemeRegistry(state));
}
/// Create a new theme registry instance
pub(crate) fn new(cx: &mut Context<Self>) -> Self {
let mut themes = HashMap::new();
let asset = cx.asset_source();
if let Ok(paths) = asset.list("themes") {
for path in paths.into_iter() {
match Self::load(&path, asset) {
Ok(theme) => {
themes.insert(path, Rc::new(theme));
}
Err(e) => {
log::error!("Failed to load theme: {path}. Error: {e}");
}
}
}
}
Self { themes }
}
/// Load a theme from the asset source.
fn load(path: &str, asset: &Arc<dyn AssetSource>) -> Result<ThemeFamily, Error> {
// Load the theme file from the assets
let content = asset.load(path)?.context("Theme not found")?;
// Parse the JSON content into a Theme Family struct, and re-derive the
// tokens from the legacy fields the file carries.
let mut theme: ThemeFamily = serde_json::from_slice(&content)?;
theme.light.rederive_tokens(ThemeMode::Light);
theme.dark.rederive_tokens(ThemeMode::Dark);
Ok(theme)
}
/// Returns a reference to the map of themes.
pub fn themes(&self) -> &HashMap<SharedString, Rc<ThemeFamily>> {
&self.themes
}
}
-182
View File
@@ -1,182 +0,0 @@
use gpui::Hsla;
/// A collection of colors that are used to style the UI.
///
/// Each step has a semantic meaning, and is used to style different parts of the UI.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub struct ColorScaleStep(usize);
impl ColorScaleStep {
pub const EIGHT: Self = Self(8);
pub const ELEVEN: Self = Self(11);
pub const FIVE: Self = Self(5);
pub const FOUR: Self = Self(4);
pub const NINE: Self = Self(9);
pub const ONE: Self = Self(1);
pub const SEVEN: Self = Self(7);
pub const SIX: Self = Self(6);
pub const TEN: Self = Self(10);
pub const THREE: Self = Self(3);
pub const TWELVE: Self = Self(12);
pub const TWO: Self = Self(2);
}
/// A scale of colors for a given [`ColorScaleSet`].
///
/// Each [`ColorScale`] contains exactly 12 colors. Refer to
/// [`ColorScaleStep`] for a reference of what each step is used for.
pub struct ColorScale(Vec<Hsla>);
impl FromIterator<Hsla> for ColorScale {
fn from_iter<T: IntoIterator<Item = Hsla>>(iter: T) -> Self {
Self(Vec::from_iter(iter))
}
}
impl ColorScale {
/// Returns the specified step in the [`ColorScale`].
#[inline]
pub fn step(&self, step: ColorScaleStep) -> Hsla {
// Steps are one-based, so we need convert to the zero-based vec index.
self.0[step.0 - 1]
}
/// `Step 1` - Used for main application backgrounds.
///
/// This step provides a neutral base for any overlaying components, ideal for applications' main backdrop or empty spaces such as canvas areas.
///
#[inline]
pub fn step_1(&self) -> Hsla {
self.step(ColorScaleStep::ONE)
}
/// `Step 2` - Used for both main application backgrounds and subtle component backgrounds.
///
/// Like `Step 1`, this step allows variations in background styles, from striped tables, sidebar backgrounds, to card backgrounds.
#[inline]
pub fn step_2(&self) -> Hsla {
self.step(ColorScaleStep::TWO)
}
/// `Step 3` - Used for UI component backgrounds in their normal states.
///
/// This step maintains accessibility by guaranteeing a contrast ratio of 4.5:1 with steps 11 and 12 for text. It could also suit hover states for transparent components.
#[inline]
pub fn step_3(&self) -> Hsla {
self.step(ColorScaleStep::THREE)
}
/// `Step 4` - Used for UI component backgrounds in their hover states.
///
/// Also suited for pressed or selected states of components with a transparent background.
#[inline]
pub fn step_4(&self) -> Hsla {
self.step(ColorScaleStep::FOUR)
}
/// `Step 5` - Used for UI component backgrounds in their pressed or selected states.
#[inline]
pub fn step_5(&self) -> Hsla {
self.step(ColorScaleStep::FIVE)
}
/// `Step 6` - Used for subtle borders on non-interactive components.
///
/// Its usage spans from sidebars' borders, headers' dividers, cards' outlines, to alerts' edges and separators.
#[inline]
pub fn step_6(&self) -> Hsla {
self.step(ColorScaleStep::SIX)
}
/// `Step 7` - Used for subtle borders on interactive components.
///
/// This step subtly delineates the boundary of elements users interact with.
#[inline]
pub fn step_7(&self) -> Hsla {
self.step(ColorScaleStep::SEVEN)
}
/// `Step 8` - Used for stronger borders on interactive components and focus rings.
///
/// It strengthens the visibility and accessibility of active elements and their focus states.
#[inline]
pub fn step_8(&self) -> Hsla {
self.step(ColorScaleStep::EIGHT)
}
/// `Step 9` - Used for solid backgrounds.
///
/// `Step 9` is the most saturated step, having the least mix of white or black.
///
/// Due to its high chroma, `Step 9` is versatile and particularly useful for semantic colors such as
/// error, warning, and success indicators.
#[inline]
pub fn step_9(&self) -> Hsla {
self.step(ColorScaleStep::NINE)
}
/// `Step 10` - Used for hovered or active solid backgrounds, particularly when `Step 9` is their normal state.
///
/// May also be used for extremely low contrast text. This should be used sparingly, as it may be difficult to read.
#[inline]
pub fn step_10(&self) -> Hsla {
self.step(ColorScaleStep::TEN)
}
/// `Step 11` - Used for text and icons requiring low contrast or less emphasis.
#[inline]
pub fn step_11(&self) -> Hsla {
self.step(ColorScaleStep::ELEVEN)
}
/// `Step 12` - Used for text and icons requiring high contrast or prominence.
#[inline]
pub fn step_12(&self) -> Hsla {
self.step(ColorScaleStep::TWELVE)
}
}
/// Provides groups of [`ColorScale`]s for light and dark themes, as well as transparent versions of each scale.
///
/// No longer used to build the built-in palettes; kept public until the
/// deprecation window closes (docs/ui-theme-bezel-migration.md, Phase 5).
#[allow(dead_code)]
pub struct ColorScaleSet {
light: ColorScale,
dark: ColorScale,
light_alpha: ColorScale,
dark_alpha: ColorScale,
}
#[allow(dead_code)]
impl ColorScaleSet {
pub(crate) fn new(
light: ColorScale,
light_alpha: ColorScale,
dark: ColorScale,
dark_alpha: ColorScale,
) -> Self {
Self {
light,
light_alpha,
dark,
dark_alpha,
}
}
pub(crate) fn light(&self) -> &ColorScale {
&self.light
}
pub(crate) fn light_alpha(&self) -> &ColorScale {
&self.light_alpha
}
pub(crate) fn dark(&self) -> &ColorScale {
&self.dark
}
pub(crate) fn dark_alpha(&self) -> &ColorScale {
&self.dark_alpha
}
}
+2 -330
View File
@@ -1,11 +1,7 @@
use std::path::Path;
use gpui::{SharedString, WindowAppearance};
use schemars::JsonSchema;
use gpui::WindowAppearance;
use serde::{Deserialize, Serialize};
use crate::ThemeColors;
/// Which appearance the palette is painted for.
#[derive(Debug, Clone, Copy, Default, PartialEq, PartialOrd, Eq, Hash, Deserialize, Serialize)]
pub enum ThemeMode {
#[default]
@@ -44,327 +40,3 @@ impl From<WindowAppearance> for ThemeMode {
}
}
}
/// Theme family
#[derive(Clone, Debug, PartialEq, Deserialize, Serialize, JsonSchema)]
pub struct ThemeFamily {
/// The unique identifier for the theme.
pub id: String,
/// The name of the theme.
pub name: SharedString,
/// The author of the theme.
pub author: SharedString,
/// The URL of the theme.
pub url: String,
/// The light colors for the theme.
pub light: ThemeColors,
/// The dark colors for the theme.
pub dark: ThemeColors,
}
impl Default for ThemeFamily {
fn default() -> Self {
ThemeFamily {
id: "coop".into(),
name: "Coop Default Theme".into(),
author: "Coop".into(),
url: "https://github.com/reyakov/coop".into(),
light: ThemeColors::light(),
dark: ThemeColors::dark(),
}
}
}
impl ThemeFamily {
/// Returns the light colors for the theme.
#[inline(always)]
pub fn light(&self) -> &ThemeColors {
&self.light
}
/// Returns the dark colors for the theme.
#[inline(always)]
pub fn dark(&self) -> &ThemeColors {
&self.dark
}
/// Load a theme family from a JSON file.
///
/// # Arguments
///
/// * `path` - Path to the JSON file containing the theme family. This can be
/// an absolute path or a path relative to the current working directory.
///
/// # Returns
///
/// Returns `Ok(ThemeFamily)` if the file was successfully loaded and parsed,
/// or `Err(anyhow::Error)` if there was an error reading or parsing the file.
///
/// # Errors
///
/// This function will return an error if:
/// - The file cannot be read (permission issues, file doesn't exist, etc.)
/// - The file contains invalid JSON
/// - The JSON structure doesn't match the `ThemeFamily` schema
///
/// # Example
///
/// ```no_run
/// use theme::ThemeFamily;
///
/// # fn main() -> anyhow::Result<()> {
/// // Load from a relative path
/// let theme = ThemeFamily::from_file("assets/themes/my-theme.json")?;
///
/// // Load from an absolute path
/// let theme = ThemeFamily::from_file("/path/to/themes/my-theme.json")?;
/// # Ok(())
/// # }
/// ```
pub fn from_file<P: AsRef<Path>>(path: P) -> anyhow::Result<Self> {
let json_data = std::fs::read(path)?;
let mut theme_family: ThemeFamily = serde_json::from_slice(&json_data)?;
theme_family.light.rederive_tokens(ThemeMode::Light);
theme_family.dark.rederive_tokens(ThemeMode::Dark);
Ok(theme_family)
}
/// Load a theme family from a JSON file in the assets/themes directory.
///
/// This function looks for the file at `assets/themes/{name}.json` relative
/// to the current working directory. This is useful for loading themes
/// from the standard theme directory in the project structure.
///
/// # Arguments
///
/// * `name` - Name of the theme file (without the .json extension)
///
/// # Returns
///
/// Returns `Ok(ThemeFamily)` if the file was successfully loaded and parsed,
/// or `Err(anyhow::Error)` if there was an error reading or parsing the file.
///
/// # Errors
///
/// This function will return an error if:
/// - The file cannot be read (permission issues, file doesn't exist, etc.)
/// - The file contains invalid JSON
/// - The JSON structure doesn't match the `ThemeFamily` schema
///
/// # Example
///
/// ```no_run
/// use theme::ThemeFamily;
///
/// # fn main() -> anyhow::Result<()> {
/// // Assuming the file exists at `assets/themes/my-theme.json`
/// let theme = ThemeFamily::from_assets("themes/my-theme.json")?;
///
/// println!("Loaded theme: {}", theme.name);
/// # Ok(())
/// # }
/// ```
pub fn from_assets(target: &str) -> anyhow::Result<Self> {
let path = format!("assets/{target}");
Self::from_file(path)
}
}
#[cfg(test)]
mod tests {
use std::fs;
use tempfile::tempdir;
use super::*;
#[test]
fn test_from_file() {
// Create a temporary directory for our test
let dir = tempdir().unwrap();
let file_path = dir.path().join("test-theme.json");
// Create a minimal valid theme JSON with hex colors
// Using simple hex colors that Hsla can parse
// Note: We need to escape the # characters in the raw string
let json_data = r##"{
"id": "test-theme",
"name": "Test Theme",
"author": "Coop",
"url": "https://github.com/reyakov/coop",
"light": {
"background": "#ffffff",
"surface_background": "#fafafa",
"elevated_surface_background": "#f5f5f5",
"panel_background": "#ffffff",
"overlay": "#0000001a",
"title_bar": "#00000000",
"title_bar_inactive": "#ffffff",
"window_border": "#c7c7cf",
"border": "#dbdbdb",
"border_variant": "#d1d1d1",
"border_focused": "#3366cc",
"border_selected": "#3366cc",
"border_transparent": "#00000000",
"border_disabled": "#e6e6e6",
"ring": "#4d79d6",
"text": "#1a1a1a",
"text_muted": "#4d4d4d",
"text_placeholder": "#808080",
"text_accent": "#3366cc",
"icon": "#4d4d4d",
"icon_muted": "#808080",
"icon_accent": "#3366cc",
"element_foreground": "#ffffff",
"element_background": "#3366cc",
"element_hover": "#3366cce6",
"element_active": "#2e5cb8",
"element_selected": "#2952a3",
"element_disabled": "#3366cc4d",
"secondary_foreground": "#2952a3",
"secondary_background": "#e6ecf5",
"secondary_hover": "#3366cc1a",
"secondary_active": "#d9e2f0",
"secondary_selected": "#d9e2f0",
"secondary_disabled": "#3366cc4d",
"danger_foreground": "#ffffff",
"danger_background": "#f5e6e6",
"danger_hover": "#cc33331a",
"danger_active": "#f0d9d9",
"danger_selected": "#f0d9d9",
"danger_disabled": "#cc33334d",
"warning_foreground": "#1a1a1a",
"warning_background": "#f5f0e6",
"warning_hover": "#cc99331a",
"warning_active": "#f0ead9",
"warning_selected": "#f0ead9",
"warning_disabled": "#cc99334d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#e6e6e6",
"ghost_element_hover": "#0000001a",
"ghost_element_active": "#d9d9d9",
"ghost_element_selected": "#d9d9d9",
"ghost_element_disabled": "#0000000d",
"tab_inactive_background": "#e6e6e6",
"tab_hover_background": "#e0e0e0",
"tab_active_background": "#d9d9d9",
"scrollbar_thumb_background": "#00000033",
"scrollbar_thumb_hover_background": "#0000004d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#d9d9d9",
"drop_target_background": "#3366cc1a",
"cursor": "#3399ff",
"selection": "#3399ff40"
},
"dark": {
"background": "#1a1a1a",
"surface_background": "#1f1f1f",
"elevated_surface_background": "#242424",
"panel_background": "#262626",
"overlay": "#ffffff1a",
"title_bar": "#00000000",
"title_bar_inactive": "#1a1a1a",
"window_border": "#404046",
"border": "#404040",
"border_variant": "#383838",
"border_focused": "#4d79d6",
"border_selected": "#4d79d6",
"border_transparent": "#00000000",
"border_disabled": "#2e2e2e",
"ring": "#668cdf",
"text": "#f2f2f2",
"text_muted": "#b3b3b3",
"text_placeholder": "#808080",
"text_accent": "#668cdf",
"icon": "#b3b3b3",
"icon_muted": "#808080",
"icon_accent": "#668cdf",
"element_foreground": "#ffffff",
"element_background": "#4d79d6",
"element_hover": "#4d79d6e6",
"element_active": "#456dc1",
"element_selected": "#3e62ac",
"element_disabled": "#4d79d64d",
"secondary_foreground": "#3e62ac",
"secondary_background": "#2a3652",
"secondary_hover": "#4d79d61a",
"secondary_active": "#303d5c",
"secondary_selected": "#303d5c",
"secondary_disabled": "#4d79d64d",
"danger_foreground": "#ffffff",
"danger_background": "#522a2a",
"danger_hover": "#d64d4d1a",
"danger_active": "#5c3030",
"danger_selected": "#5c3030",
"danger_disabled": "#d64d4d4d",
"warning_foreground": "#f2f2f2",
"warning_background": "#52482a",
"warning_hover": "#d6b34d1a",
"warning_active": "#5c5430",
"warning_selected": "#5c5430",
"warning_disabled": "#d6b34d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#2e2e2e",
"ghost_element_hover": "#ffffff1a",
"ghost_element_active": "#383838",
"ghost_element_selected": "#383838",
"ghost_element_disabled": "#ffffff0d",
"tab_inactive_background": "#2e2e2e",
"tab_hover_background": "#333333",
"tab_active_background": "#383838",
"scrollbar_thumb_background": "#ffffff33",
"scrollbar_thumb_hover_background": "#ffffff4d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#383838",
"drop_target_background": "#4d79d61a",
"cursor": "#4db3ff",
"selection": "#4db3ff40"
}
}"##;
// Write the JSON to the file
fs::write(&file_path, json_data).unwrap();
// Test loading the theme from file
let theme = ThemeFamily::from_file(&file_path).unwrap();
// Verify the loaded theme
assert_eq!(theme.id, "test-theme");
assert_eq!(theme.name, "Test Theme");
// Clean up
dir.close().unwrap();
}
#[test]
fn test_from_file_nonexistent() {
// Test that loading a non-existent file returns an error
let result = ThemeFamily::from_file("non-existent-file.json");
assert!(result.is_err());
}
#[test]
fn test_from_file_invalid_json() {
// Create a temporary directory for our test
let dir = tempdir().unwrap();
let file_path = dir.path().join("invalid-theme.json");
// Write invalid JSON
fs::write(&file_path, "invalid json").unwrap();
// Test that loading invalid JSON returns an error
let result = ThemeFamily::from_file(&file_path);
assert!(result.is_err());
// Clean up
dir.close().unwrap();
}
}
+1 -109
View File
@@ -4,7 +4,7 @@
//! palette was tuned for.
use gpui::Hsla;
use theme::{Brand, ThemeColors, ThemeFamily, ThemeMode, Tint, contrast_ratio, flatten};
use theme::{Brand, ThemeColors, ThemeMode, Tint, contrast_ratio, flatten};
// The shipped palette's measured floors (AppKit-style ink, per bezel):
// text/bg is 14.4:1 dark and 15.1:1 light, muted lands near 6.3 dark and
@@ -123,111 +123,3 @@ fn warning_wash_carries_its_label() {
);
}
}
#[test]
fn legacy_aliases_track_tokens() {
for appearance in [ThemeMode::Dark, ThemeMode::Light] {
let colors = ThemeColors::for_appearance(appearance);
assert_eq!(colors.background, colors.bg);
assert_eq!(colors.surface_background, colors.surface);
assert_eq!(colors.text_placeholder, colors.text_faint);
assert_eq!(colors.element_background, colors.solid);
assert_eq!(colors.secondary_background, colors.surface_raised);
assert_eq!(colors.tab_active_background, colors.element_active);
}
}
#[test]
fn rederive_round_trips_a_legacy_theme() {
let json = br##"{
"id": "test", "name": "Test", "author": "coop", "url": "",
"light": {
"background": "#ffffff", "surface_background": "#fafafa",
"elevated_surface_background": "#f5f5f5", "panel_background": "#ffffff",
"overlay": "#0000001a", "title_bar": "#00000000",
"title_bar_inactive": "#ffffff", "window_border": "#c7c7cf",
"border": "#dbdbdb", "border_variant": "#d1d1d1",
"border_focused": "#3366cc", "border_selected": "#3366cc",
"border_transparent": "#00000000", "border_disabled": "#e6e6e6",
"ring": "#4d79d6", "text": "#1a1a1a", "text_muted": "#4d4d4d",
"text_placeholder": "#808080", "text_accent": "#3366cc",
"text_danger": "#cc3333", "text_warning": "#cc9933",
"icon": "#4d4d4d", "icon_muted": "#808080", "icon_accent": "#3366cc",
"element_foreground": "#ffffff", "element_background": "#3366cc",
"element_hover": "#3366cce6", "element_active": "#2e5cb8",
"element_selected": "#2952a3", "element_disabled": "#3366cc4d",
"secondary_foreground": "#2952a3", "secondary_background": "#e6ecf5",
"secondary_hover": "#3366cc1a", "secondary_active": "#d9e2f0",
"secondary_selected": "#d9e2f0", "secondary_disabled": "#3366cc4d",
"danger_foreground": "#ffffff", "danger_background": "#f5e6e6",
"danger_hover": "#cc33331a", "danger_active": "#f0d9d9",
"danger_selected": "#f0d9d9", "danger_disabled": "#cc33334d",
"warning_foreground": "#1a1a1a", "warning_background": "#f5f0e6",
"warning_hover": "#cc99331a", "warning_active": "#f0ead9",
"warning_selected": "#f0ead9", "warning_disabled": "#cc99334d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#e6e6e6",
"ghost_element_hover": "#0000001a", "ghost_element_active": "#d9d9d9",
"ghost_element_selected": "#d9d9d9", "ghost_element_disabled": "#0000000d",
"tab_inactive_background": "#e6e6e6", "tab_hover_background": "#e0e0e0",
"tab_active_background": "#d9d9d9",
"scrollbar_thumb_background": "#00000033",
"scrollbar_thumb_hover_background": "#0000004d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#d9d9d9",
"drop_target_background": "#3366cc1a",
"cursor": "#3399ff", "selection": "#3399ff40"
},
"dark": {
"background": "#1a1a1a", "surface_background": "#1f1f1f",
"elevated_surface_background": "#242424", "panel_background": "#262626",
"overlay": "#ffffff1a", "title_bar": "#00000000",
"title_bar_inactive": "#1a1a1a", "window_border": "#404046",
"border": "#404040", "border_variant": "#383838",
"border_focused": "#4d79d6", "border_selected": "#4d79d6",
"border_transparent": "#00000000", "border_disabled": "#2e2e2e",
"ring": "#668cdf", "text": "#f2f2f2", "text_muted": "#b3b3b3",
"text_placeholder": "#808080", "text_accent": "#668cdf",
"text_danger": "#d64d4d", "text_warning": "#d6b34d",
"icon": "#b3b3b3", "icon_muted": "#808080", "icon_accent": "#668cdf",
"element_foreground": "#ffffff", "element_background": "#4d79d6",
"element_hover": "#4d79d6e6", "element_active": "#456dc1",
"element_selected": "#3e62ac", "element_disabled": "#4d79d64d",
"secondary_foreground": "#3e62ac", "secondary_background": "#2a3652",
"secondary_hover": "#4d79d61a", "secondary_active": "#303d5c",
"secondary_selected": "#303d5c", "secondary_disabled": "#4d79d64d",
"danger_foreground": "#ffffff", "danger_background": "#522a2a",
"danger_hover": "#d64d4d1a", "danger_active": "#5c3030",
"danger_selected": "#5c3030", "danger_disabled": "#d64d4d4d",
"warning_foreground": "#f2f2f2", "warning_background": "#52482a",
"warning_hover": "#d6b34d1a", "warning_active": "#5c5430",
"warning_selected": "#5c5430", "warning_disabled": "#d6b34d4d",
"ghost_element_background": "#00000000",
"ghost_element_background_alt": "#2e2e2e",
"ghost_element_hover": "#ffffff1a", "ghost_element_active": "#383838",
"ghost_element_selected": "#383838", "ghost_element_disabled": "#ffffff0d",
"tab_inactive_background": "#2e2e2e", "tab_hover_background": "#333333",
"tab_active_background": "#383838",
"scrollbar_thumb_background": "#ffffff33",
"scrollbar_thumb_hover_background": "#ffffff4d",
"scrollbar_thumb_border": "#00000000",
"scrollbar_track_background": "#00000000",
"scrollbar_track_border": "#383838",
"drop_target_background": "#4d79d61a",
"cursor": "#4db3ff", "selection": "#4db3ff40"
}
}"##;
let family: ThemeFamily = serde_json::from_slice(json).expect("parse");
let mut light = family.light;
light.rederive_tokens(ThemeMode::Light);
assert_eq!(light.bg, light.background, "bg tracks the JSON background");
assert_eq!(light.surface, light.surface_background);
assert_eq!(light.solid, light.element_background);
// Fields with no legacy source keep the built-in palette's value.
assert_eq!(
light.success,
ThemeColors::for_appearance(ThemeMode::Light).success
);
}