port bezel theme system into the theme crate
This commit is contained in:
+141
-33
@@ -1,26 +1,53 @@
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::rc::Rc;
|
||||
|
||||
use appearance::AppearanceMode;
|
||||
use gpui::{App, Global, Pixels, SharedString, Window, px};
|
||||
|
||||
mod brand;
|
||||
mod color;
|
||||
mod colors;
|
||||
mod geometry;
|
||||
mod layout;
|
||||
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 brand::{BASE_COLORS, Brand, Ink, TextInk, Tint};
|
||||
pub use color::{
|
||||
contrast_ratio, flatten, grey, hsl_to_rgb, lightness, mix, neutral, oklch, oklch_to_srgb,
|
||||
relative_luminance, rgb_to_hsl, tint,
|
||||
};
|
||||
pub use colors::*;
|
||||
pub use geometry::*;
|
||||
pub use layout::{
|
||||
BASE_RADIUS, BUTTON_HEIGHT, CONTENT_MARGIN, CONTROL_HEIGHT_SMALL, EDGE, SPACE, bubble_radius,
|
||||
button_radius, control_radius, inset_radius, panel_radius, surface_radius,
|
||||
};
|
||||
pub use notification::*;
|
||||
pub use paint::{
|
||||
INK_FILL_SCALE, INK_HAIRLINE_SCALE, SCRIM_ALPHA_DARK, band, current_appearance, hairline, ink,
|
||||
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::*;
|
||||
|
||||
/// Which appearance the app is painting. Bezel names this enum `Appearance`;
|
||||
/// coop's `ThemeMode` predates the port and carries the same two variants.
|
||||
pub type Appearance = ThemeMode;
|
||||
|
||||
/// Defines window border radius for platforms that use client side decorations.
|
||||
pub const CLIENT_SIDE_DECORATION_ROUNDING: Pixels = px(10.0);
|
||||
|
||||
@@ -41,24 +68,64 @@ pub const SIDEBAR_WIDTH: Pixels = px(240.);
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
registry::init(cx);
|
||||
|
||||
Theme::sync_system_appearance(None, 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.
|
||||
pub type PaletteBuilder = fn(ThemeMode) -> ThemeColors;
|
||||
|
||||
struct GlobalPaletteBuilder(Option<PaletteBuilder>);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
impl AppExt for App {
|
||||
fn set_brand(&mut self, brand: Brand) {
|
||||
self.set_global(brand);
|
||||
reinstall(self);
|
||||
}
|
||||
|
||||
fn set_palette(&mut self, build: PaletteBuilder) {
|
||||
self.set_global(GlobalPaletteBuilder(Some(build)));
|
||||
reinstall(self);
|
||||
}
|
||||
|
||||
fn set_appearance_mode(&mut self, mode: AppearanceMode) {
|
||||
appearance::set_mode(mode, self);
|
||||
}
|
||||
}
|
||||
|
||||
/// Re-install the palette for the current appearance.
|
||||
fn reinstall(cx: &mut App) {
|
||||
let mode = cx
|
||||
.try_global::<Theme>()
|
||||
.map(|theme| theme.mode)
|
||||
.unwrap_or_default();
|
||||
Theme::change(mode, None, cx);
|
||||
cx.refresh_windows();
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Only roles base can act on are projected. Radius, spacing, typography sizes,
|
||||
/// shadows, and scrollbar geometry keep their base defaults: coop has a single
|
||||
/// `radius`/`radius_lg`/`font_size` where base has six-point scales, so any
|
||||
/// mapping would be invented rather than derived.
|
||||
///
|
||||
/// This is a no-op before the coop theme global exists; [`Theme::change`] is the
|
||||
/// authoritative hook that keeps the projection current.
|
||||
/// 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;
|
||||
@@ -83,20 +150,20 @@ pub fn sync_base(cx: &mut App) {
|
||||
base.tokens.typography.sans = font_family;
|
||||
|
||||
let tokens = &mut base.tokens.colors;
|
||||
tokens.background = colors.background;
|
||||
tokens.background = colors.bg;
|
||||
tokens.foreground = colors.text;
|
||||
tokens.surface = colors.surface_background;
|
||||
tokens.surface = colors.surface;
|
||||
tokens.surface_foreground = colors.text;
|
||||
tokens.primary = colors.element_background;
|
||||
tokens.primary_foreground = colors.element_foreground;
|
||||
tokens.secondary = colors.secondary_background;
|
||||
tokens.secondary_foreground = colors.secondary_foreground;
|
||||
tokens.muted = colors.ghost_element_background_alt;
|
||||
tokens.primary = colors.solid;
|
||||
tokens.primary_foreground = colors.on_solid;
|
||||
tokens.secondary = colors.surface_raised;
|
||||
tokens.secondary_foreground = colors.text;
|
||||
tokens.muted = colors.surface_card;
|
||||
tokens.muted_foreground = colors.text_muted;
|
||||
tokens.accent = colors.ghost_element_hover;
|
||||
tokens.accent = colors.element_hover;
|
||||
tokens.accent_foreground = colors.text;
|
||||
tokens.destructive = colors.danger_background;
|
||||
tokens.destructive_foreground = colors.danger_foreground;
|
||||
tokens.destructive = colors.danger_strong;
|
||||
tokens.destructive_foreground = colors.on_accent;
|
||||
tokens.border = colors.border;
|
||||
tokens.input = colors.border;
|
||||
tokens.ring = colors.ring;
|
||||
@@ -128,6 +195,13 @@ pub struct Theme {
|
||||
/// 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,
|
||||
|
||||
@@ -181,12 +255,7 @@ impl Theme {
|
||||
|
||||
/// Sync the theme with the system appearance
|
||||
pub fn sync_system_appearance(window: Option<&mut Window>, cx: &mut App) {
|
||||
let appearance = window
|
||||
.as_ref()
|
||||
.map(|window| window.appearance())
|
||||
.unwrap_or_else(|| cx.window_appearance());
|
||||
|
||||
Self::change(appearance, window, cx);
|
||||
appearance::record_system_appearance(window.as_deref(), cx);
|
||||
}
|
||||
|
||||
/// Sync the Scrollbar showing behavior with the system
|
||||
@@ -221,23 +290,47 @@ impl Theme {
|
||||
}
|
||||
|
||||
let mode = mode.into();
|
||||
let theme = cx.global_mut::<Theme>();
|
||||
let builder = cx
|
||||
.try_global::<GlobalPaletteBuilder>()
|
||||
.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()
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// 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.
|
||||
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);
|
||||
|
||||
// Set the theme mode
|
||||
theme.mode = mode;
|
||||
|
||||
// Set the theme colors
|
||||
if mode.is_dark() {
|
||||
theme.colors = *theme.theme.dark();
|
||||
} else {
|
||||
theme.colors = *theme.theme.light();
|
||||
}
|
||||
theme.colors = colors;
|
||||
|
||||
// Refresh the window if available
|
||||
if let Some(window) = window {
|
||||
window.refresh();
|
||||
}
|
||||
|
||||
// Keep the context-free paint helpers and the radius accessors in step
|
||||
paint::set_current_appearance(mode);
|
||||
layout::set_base_radius(installed_brand.radius);
|
||||
|
||||
// Keep the base-layer projection in step with the coop palette
|
||||
sync_base(cx);
|
||||
}
|
||||
@@ -263,8 +356,10 @@ impl From<ThemeFamily> for Theme {
|
||||
Theme {
|
||||
font_size: px(15.),
|
||||
font_family: font_family.into(),
|
||||
radius: px(6.),
|
||||
radius_lg: px(10.),
|
||||
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(),
|
||||
@@ -274,3 +369,16 @@ impl From<ThemeFamily> for Theme {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The mono face has no alias of its own, so each backend is named here.
|
||||
fn system_mono() -> &'static str {
|
||||
if cfg!(target_family = "wasm") {
|
||||
".ZedMono"
|
||||
} else if cfg!(target_os = "macos") {
|
||||
".AppleSystemUIFontMonospaced"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Cascadia Mono"
|
||||
} else {
|
||||
"DejaVu Sans Mono"
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user