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:
1 parent
5f2e6a5cda
commit
3d2bc0f4d6
13 files changed
+465
-11
No files matched your search
@@ -0,0 +1,61 @@
|
||||
use ely_gpui_component::{
|
||||
buttons::{Button, ButtonVariant},
|
||||
settings::{ThemeDraft, ThemeEditor},
|
||||
theme::{ActiveTheme, Density, Mode, Theme},
|
||||
};
|
||||
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
|
||||
|
||||
use crate::ui::section;
|
||||
|
||||
/// Applies a draft to the app's theme: its measures and switches, and its colors as the shown mode's own.
|
||||
fn apply(draft: &ThemeDraft, cx: &mut App) {
|
||||
let now = ThemeDraft::of(cx.theme());
|
||||
Theme::update(cx, |theme| {
|
||||
theme.density = draft.density;
|
||||
theme.radius_scale = draft.radius_scale;
|
||||
theme.font_scale = draft.font_scale;
|
||||
theme.reduced_motion = draft.reduced_motion;
|
||||
});
|
||||
if draft.high_contrast != now.high_contrast {
|
||||
Theme::set_high_contrast(draft.high_contrast, cx);
|
||||
}
|
||||
if draft.colors != now.colors {
|
||||
Theme::set_palette(cx.theme().mode(), Some(draft.colors.clone()), cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// Ely's own theme again, in both modes.
|
||||
fn restore(cx: &mut App) {
|
||||
Theme::update(cx, |theme| {
|
||||
theme.density = Density::Standard;
|
||||
theme.radius_scale = 1.0;
|
||||
theme.font_scale = 1.0;
|
||||
});
|
||||
Theme::set_palette(Mode::Light, None, cx);
|
||||
Theme::set_palette(Mode::Dark, None, cx);
|
||||
Theme::set_high_contrast(false, cx);
|
||||
}
|
||||
|
||||
pub fn editor(_: &mut Window, cx: &mut App) -> impl IntoElement + use<> {
|
||||
let draft = ThemeDraft::of(cx.theme());
|
||||
section(
|
||||
"ThemeEditor · DensityProvider · RadiusProvider · FontProvider · HighContrastMode · ReducedMotionProvider",
|
||||
"The theme laid open: every change here applies to the whole gallery at once, the colors as the shown mode's own. The theme switch above is settings::ThemeSelector.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.w(px(520.))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_4()
|
||||
.child(ThemeEditor::new("theme-editor", draft).on_change(|draft, _, cx| apply(draft, cx)))
|
||||
.child(
|
||||
div().flex().child(
|
||||
Button::new("theme-restore", "Back to Ely's theme")
|
||||
.variant(ButtonVariant::Secondary)
|
||||
.on_click(|_, _, cx| restore(cx)),
|
||||
),
|
||||
),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use ely_gpui_component::{
|
||||
motion,
|
||||
theme::{ActiveTheme, Elevation, Radius, TextSize},
|
||||
};
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyElement, App, FontStyle, FontWeight, HighlightStyle, Hsla,
|
||||
IntoElement, ParentElement, SharedString, Styled, StyledText, Window, div, px,
|
||||
};
|
||||
|
||||
mod editing;
|
||||
|
||||
use super::Page;
|
||||
use crate::ui::{section, specimen, specimens};
|
||||
|
||||
pub const PAGE: Page = Page {
|
||||
number: 39,
|
||||
slug: "theme",
|
||||
title: "Theme",
|
||||
summary: "Tokens every component reads. Light and dark share one grammar.",
|
||||
render,
|
||||
script: &[],
|
||||
};
|
||||
|
||||
fn render(window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
let colors = cx.theme().colors.clone();
|
||||
let surfaces = [
|
||||
("bg", colors.bg),
|
||||
("surface", colors.surface),
|
||||
("sunken", colors.sunken),
|
||||
("hover", colors.hover),
|
||||
("active", colors.active),
|
||||
("border", colors.border),
|
||||
("border strong", colors.border_strong),
|
||||
];
|
||||
let inks = [
|
||||
("fg", colors.fg),
|
||||
("muted", colors.fg_muted),
|
||||
("subtle", colors.fg_subtle),
|
||||
("disabled", colors.fg_disabled),
|
||||
("focus", colors.focus),
|
||||
("link", colors.link),
|
||||
];
|
||||
let signals = [
|
||||
("success", colors.success, colors.success_subtle),
|
||||
("warning", colors.warning, colors.warning_subtle),
|
||||
("danger", colors.danger, colors.danger_subtle),
|
||||
("info", colors.info, colors.info_subtle),
|
||||
];
|
||||
|
||||
div()
|
||||
.child(
|
||||
section(
|
||||
"Surfaces",
|
||||
"Warm neutrals. Depth comes from tone, not shadow.",
|
||||
cx,
|
||||
)
|
||||
.child(
|
||||
specimens()
|
||||
.gap_3()
|
||||
.children(surfaces.map(|(name, color)| swatch(name, color, cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Ink", "Four steps of text. Blue marks focus and links.", cx).child(
|
||||
specimens()
|
||||
.gap_3()
|
||||
.children(inks.map(|(name, color)| swatch(name, color, cx))),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Signals", "Status colors, each with a quiet fill.", cx).child(
|
||||
specimens()
|
||||
.gap_3()
|
||||
.children(signals.map(|(name, strong, quiet)| {
|
||||
specimen(name, signal_pair(strong, quiet, cx), cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Series", "Eight chart colors, even in weight.", cx).child(
|
||||
specimens()
|
||||
.gap_2()
|
||||
.children(colors.chart.iter().enumerate().map(|(ix, &color)| {
|
||||
specimen(format!("{}", ix + 1), chip(color, 40.0, cx), cx)
|
||||
})),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
section("Syntax", "Code tokens sit below text in weight.", cx).child(code_sample(cx)),
|
||||
)
|
||||
.child(
|
||||
section("Type", "Inter for words. JetBrains Mono for code.", cx).child(type_scale(cx)),
|
||||
)
|
||||
.child(
|
||||
section("Shape", "Four radii. Three elevations.", cx)
|
||||
.child(
|
||||
specimens().children([Radius::Sm, Radius::Md, Radius::Lg, Radius::Xl].map(
|
||||
|radius| {
|
||||
specimen(
|
||||
format!("{radius:?}"),
|
||||
div()
|
||||
.size(px(64.0))
|
||||
.rounded(cx.theme().radius(radius))
|
||||
.bg(colors.sunken)
|
||||
.border_1()
|
||||
.border_color(colors.border_strong),
|
||||
cx,
|
||||
)
|
||||
},
|
||||
)),
|
||||
)
|
||||
.child(specimens().gap_8().pt_4().children(
|
||||
[Elevation::Raised, Elevation::Floating, Elevation::Modal].map(|level| {
|
||||
specimen(
|
||||
format!("{level:?}"),
|
||||
div()
|
||||
.w(px(120.0))
|
||||
.h(px(72.0))
|
||||
.rounded(cx.theme().radius(Radius::Lg))
|
||||
.bg(colors.overlay)
|
||||
.shadow(cx.theme().elevation(level)),
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
section(
|
||||
"Motion",
|
||||
"Ease out to arrive. Springs only where a hand leads. Slowed to one second here.",
|
||||
cx,
|
||||
)
|
||||
.child(track(
|
||||
"ease-out",
|
||||
"ease out cubic",
|
||||
motion::ease_out_cubic,
|
||||
cx,
|
||||
))
|
||||
.child(track(
|
||||
"ease-in-out",
|
||||
"ease in out cubic",
|
||||
motion::ease_in_out_cubic,
|
||||
cx,
|
||||
))
|
||||
.child(track("spring", "spring · damping 0.75", motion::spring, cx)),
|
||||
)
|
||||
.child(editing::editor(window, cx))
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn chip(color: Hsla, side: f32, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.w(px(side))
|
||||
.h(px(side))
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(color)
|
||||
.border_1()
|
||||
.border_color(theme.colors.border)
|
||||
}
|
||||
|
||||
fn swatch(name: &'static str, color: Hsla, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
specimen(
|
||||
name,
|
||||
div()
|
||||
.w(px(88.0))
|
||||
.h(px(56.0))
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(color)
|
||||
.border_1()
|
||||
.border_color(theme.colors.border),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
fn signal_pair(strong: Hsla, quiet: Hsla, cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_2()
|
||||
.h(px(40.0))
|
||||
.px_3()
|
||||
.rounded(theme.radius(Radius::Md))
|
||||
.bg(quiet)
|
||||
.child(div().size(px(8.0)).rounded_full().bg(strong))
|
||||
.child(
|
||||
div()
|
||||
.text_color(strong)
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.child("Aa 12"),
|
||||
)
|
||||
}
|
||||
|
||||
fn code_sample(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
let syntax = &theme.colors.syntax;
|
||||
let text = "fn answer() -> u32 { let x = 42; x } // truth";
|
||||
let color = |c: Hsla| HighlightStyle {
|
||||
color: Some(c),
|
||||
..Default::default()
|
||||
};
|
||||
let spans = [
|
||||
(0..2, color(syntax.keyword)),
|
||||
(3..9, color(syntax.function)),
|
||||
(9..11, color(syntax.punctuation)),
|
||||
(12..14, color(syntax.operator)),
|
||||
(15..18, color(syntax.type_name)),
|
||||
(19..20, color(syntax.punctuation)),
|
||||
(21..24, color(syntax.keyword)),
|
||||
(25..26, color(syntax.variable)),
|
||||
(27..28, color(syntax.operator)),
|
||||
(29..31, color(syntax.number)),
|
||||
(31..32, color(syntax.punctuation)),
|
||||
(35..36, color(syntax.punctuation)),
|
||||
(
|
||||
37..45,
|
||||
HighlightStyle {
|
||||
color: Some(syntax.comment),
|
||||
font_style: Some(FontStyle::Italic),
|
||||
..Default::default()
|
||||
},
|
||||
),
|
||||
];
|
||||
div()
|
||||
.px_4()
|
||||
.py_3()
|
||||
.rounded(theme.radius(Radius::Lg))
|
||||
.bg(theme.colors.sunken)
|
||||
.font_family(theme.mono_family.clone())
|
||||
.text_size(theme.text_size(TextSize::Base))
|
||||
.text_color(theme.colors.fg)
|
||||
.child(StyledText::new(text).with_highlights(spans))
|
||||
}
|
||||
|
||||
fn type_scale(cx: &App) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
let steps = [
|
||||
(TextSize::Display, "Display 32", FontWeight::SEMIBOLD),
|
||||
(TextSize::Xxl, "Heading 24", FontWeight::SEMIBOLD),
|
||||
(TextSize::Xl, "Title 20", FontWeight::SEMIBOLD),
|
||||
(TextSize::Lg, "Subtitle 16", FontWeight::MEDIUM),
|
||||
(TextSize::Md, "Body large 14", FontWeight::NORMAL),
|
||||
(TextSize::Base, "Body 13", FontWeight::NORMAL),
|
||||
(TextSize::Sm, "Small 12", FontWeight::NORMAL),
|
||||
(TextSize::Xs, "Caption 11", FontWeight::NORMAL),
|
||||
];
|
||||
div()
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap_3()
|
||||
.children(steps.map(|(size, name, weight)| {
|
||||
div()
|
||||
.flex()
|
||||
.items_center()
|
||||
.gap_6()
|
||||
.child(
|
||||
div()
|
||||
.w(px(120.0))
|
||||
.text_size(theme.text_size(TextSize::Xs))
|
||||
.text_color(theme.colors.fg_subtle)
|
||||
.child(SharedString::from(name)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_size(theme.text_size(size))
|
||||
.font_weight(weight)
|
||||
.text_color(theme.colors.fg)
|
||||
.child("Quiet interfaces age well."),
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn track(
|
||||
id: &'static str,
|
||||
name: &'static str,
|
||||
ease: fn(f32) -> f32,
|
||||
cx: &App,
|
||||
) -> impl IntoElement + use<> {
|
||||
let theme = cx.theme();
|
||||
let width = 360.0;
|
||||
let travel = width - 12.0;
|
||||
let knob = div()
|
||||
.absolute()
|
||||
.top(px(6.0))
|
||||
.size(px(12.0))
|
||||
.rounded_full()
|
||||
.bg(theme.colors.fg);
|
||||
let knob = if theme.reduced_motion {
|
||||
knob.left(px(travel)).into_any_element()
|
||||
} else {
|
||||
knob.with_animation(
|
||||
id,
|
||||
Animation::new(Duration::from_millis(2400)).repeat(),
|
||||
move |knob, t| {
|
||||
let offset = match t {
|
||||
t if t < 0.42 => ease(t / 0.42),
|
||||
t if t < 0.5 => 1.0,
|
||||
t if t < 0.92 => 1.0 - ease((t - 0.5) / 0.42),
|
||||
_ => 0.0,
|
||||
};
|
||||
knob.left(px(offset * travel))
|
||||
},
|
||||
)
|
||||
.into_any_element()
|
||||
};
|
||||
specimen(
|
||||
name,
|
||||
div()
|
||||
.relative()
|
||||
.w(px(width))
|
||||
.h(px(24.0))
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.top(px(11.5))
|
||||
.left_0()
|
||||
.w(px(width))
|
||||
.h(px(1.0))
|
||||
.bg(theme.colors.border),
|
||||
)
|
||||
.child(knob),
|
||||
cx,
|
||||
)
|
||||
}
|
||||
Reference in new issue
Block a user