Theme · Editing: a theme laid open, and an owner's palette per mode

settings::ThemeEditor lays a theme open: density, corners, text size,
high contrast and reduced motion, then twelve colors of the mode shown,
each a swatch that opens a picker beside its hex; each change hands the
owner a whole draft. Theme::set_palette takes an owner's palette per
mode, the colors fading to it when the mode shows; Theme::palette names
the palette the shown mode fades to; Palette::token_mut and
Syntax::token_mut reach a color by its field's name. The gallery's
theme page applies the editor to the whole gallery live, with a way
back to Ely's theme.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 5.5 committed 2026-09-27 16:31:33 -04:00
1 parent 5f2e6a5cda
commit 3d2bc0f4d6
13 files changed
+465 -11

No files matched your search

+27
View File
@@ -12,6 +12,9 @@ mod palette;
mod project;
mod tokens;
#[cfg(all(test, feature = "test-support"))]
mod tests;
use std::time::{Duration, Instant};
use gpui::{App, Global, SharedString, WindowAppearance};
@@ -56,6 +59,7 @@ pub struct Theme {
mode: Mode,
high_contrast: bool,
transition: u64,
custom: [Option<Palette>; 2],
pub colors: Palette,
pub density: Density,
pub radius_scale: f32,
@@ -83,6 +87,7 @@ impl Theme {
mode: Mode::Light,
high_contrast: false,
transition: 0,
custom: [None, None],
colors: Palette::light(false),
density: Density::Standard,
radius_scale: 1.0,
@@ -118,6 +123,25 @@ impl Theme {
Self::fade_to_target(cx);
}
/// The owner's palette for `mode`, as an edited or imported theme gives, or none to bring Ely's back. The colors fade to it when `mode` shows. High contrast strengthens Ely's own palettes alone.
pub fn set_palette(mode: Mode, palette: Option<Palette>, cx: &mut App) {
log::info!(
"theme: {mode:?} palette -> {}",
if palette.is_some() {
"the owner's"
} else {
"Ely's"
}
);
cx.global_mut::<Theme>().custom[mode as usize] = palette;
Self::fade_to_target(cx);
}
/// The palette the shown mode fades to: the owner's, or Ely's.
pub fn palette(&self) -> Palette {
self.target()
}
/// Edits non-color tokens, then repaints.
pub fn update(cx: &mut App, edit: impl FnOnce(&mut Theme)) {
edit(cx.global_mut::<Theme>());
@@ -125,6 +149,9 @@ impl Theme {
}
fn target(&self) -> Palette {
if let Some(palette) = &self.custom[self.mode as usize] {
return palette.clone();
}
match self.mode {
Mode::Light => Palette::light(self.high_contrast),
Mode::Dark => Palette::dark(self.high_contrast),
+54
View File
@@ -122,7 +122,61 @@ pub const HUE_NAMES: [&str; 8] = [
"Blue", "Teal", "Ochre", "Rose", "Violet", "Green", "Rust", "Cyan",
];
impl Syntax {
/// The color of the token kind named `token`, a field's name; any other name fails.
pub fn token_mut(&mut self, token: &str) -> &mut Hsla {
match token {
"keyword" => &mut self.keyword,
"string" => &mut self.string,
"number" => &mut self.number,
"comment" => &mut self.comment,
"function" => &mut self.function,
"type_name" => &mut self.type_name,
"constant" => &mut self.constant,
"property" => &mut self.property,
"tag" => &mut self.tag,
"attribute" => &mut self.attribute,
"operator" => &mut self.operator,
"punctuation" => &mut self.punctuation,
"variable" => &mut self.variable,
other => panic!("no syntax color {other}"),
}
}
}
impl Palette {
/// The color named `token`, a field's name; any other name fails.
pub fn token_mut(&mut self, token: &str) -> &mut Hsla {
match token {
"bg" => &mut self.bg,
"surface" => &mut self.surface,
"sunken" => &mut self.sunken,
"overlay" => &mut self.overlay,
"hover" => &mut self.hover,
"active" => &mut self.active,
"border" => &mut self.border,
"border_strong" => &mut self.border_strong,
"fg" => &mut self.fg,
"fg_muted" => &mut self.fg_muted,
"fg_subtle" => &mut self.fg_subtle,
"fg_disabled" => &mut self.fg_disabled,
"accent" => &mut self.accent,
"accent_hover" => &mut self.accent_hover,
"on_accent" => &mut self.on_accent,
"focus" => &mut self.focus,
"link" => &mut self.link,
"selection" => &mut self.selection,
"success" => &mut self.success,
"warning" => &mut self.warning,
"danger" => &mut self.danger,
"info" => &mut self.info,
"tooltip_bg" => &mut self.tooltip_bg,
"tooltip_fg" => &mut self.tooltip_fg,
"shadow" => &mut self.shadow,
other => panic!("no palette color {other}"),
}
}
/// The chart color at `hue`; a hue past the chart fails, naming `owner`.
pub fn hue(&self, hue: usize, owner: impl std::fmt::Display) -> Hsla {
*self
+35
View File
@@ -0,0 +1,35 @@
use gpui::{TestAppContext, hsla};
use super::{ActiveTheme, Mode, Palette, Theme};
/// An owner's palette serves its own mode; the other keeps Ely's, and none brings Ely's back.
#[gpui::test]
fn an_owners_palette_serves_its_mode_alone(cx: &mut TestAppContext) {
cx.update(Theme::init);
let mut own = Palette::light(false);
own.accent = hsla(0.6, 0.5, 0.5, 1.0);
cx.update(|cx| Theme::set_palette(Mode::Light, Some(own.clone()), cx));
assert_eq!(cx.read(|cx| cx.theme().palette()), own);
cx.update(|cx| Theme::set_mode(Mode::Dark, cx));
assert_eq!(cx.read(|cx| cx.theme().palette()), Palette::dark(false));
cx.update(|cx| {
Theme::set_mode(Mode::Light, cx);
Theme::set_palette(Mode::Light, None, cx);
});
assert_eq!(cx.read(|cx| cx.theme().palette()), Palette::light(false));
}
#[test]
fn every_named_color_answers() {
let mut palette = Palette::light(false);
*palette.token_mut("accent") = hsla(0.1, 0.2, 0.3, 1.0);
assert_eq!(palette.accent, hsla(0.1, 0.2, 0.3, 1.0));
*palette.syntax.token_mut("comment") = hsla(0.4, 0.2, 0.3, 1.0);
assert_eq!(palette.syntax.comment, hsla(0.4, 0.2, 0.3, 1.0));
}
#[test]
#[should_panic(expected = "no palette color glow")]
fn an_unknown_color_fails() {
Palette::light(false).token_mut("glow");
}