Start work on allowing variables in themes
This commit is contained in:
parent
5ac0a1985e
commit
92353b6967
9 changed files with 349 additions and 110 deletions
|
@ -14,7 +14,7 @@ name = "Zed"
|
||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
test-support = ["tempdir", "serde_json", "zrpc/test-support"]
|
test-support = ["tempdir", "zrpc/test-support"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = "1.0.38"
|
anyhow = "1.0.38"
|
||||||
|
@ -41,9 +41,7 @@ rsa = "0.4"
|
||||||
rust-embed = "5.9.0"
|
rust-embed = "5.9.0"
|
||||||
seahash = "4.1"
|
seahash = "4.1"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = { version = "1.0.64", features = [
|
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||||
"preserve_order",
|
|
||||||
], optional = true }
|
|
||||||
similar = "1.3"
|
similar = "1.3"
|
||||||
simplelog = "0.9"
|
simplelog = "0.9"
|
||||||
smallvec = { version = "1.6", features = ["union"] }
|
smallvec = { version = "1.6", features = ["union"] }
|
||||||
|
|
28
zed/assets/themes/base.toml
Normal file
28
zed/assets/themes/base.toml
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
[ui]
|
||||||
|
background = "$elevation_1"
|
||||||
|
tab_background = "$elevation_2"
|
||||||
|
tab_background_active = "$elevation_3"
|
||||||
|
tab_text = "$text_dull"
|
||||||
|
tab_text_active = "$text_bright"
|
||||||
|
tab_border = 0x000000
|
||||||
|
tab_icon_close = 0x383839
|
||||||
|
tab_icon_dirty = 0x556de8
|
||||||
|
tab_icon_conflict = 0xe45349
|
||||||
|
modal_background = "$elevation_4"
|
||||||
|
modal_match_background = 0x424344
|
||||||
|
modal_match_background_active = 0x094771
|
||||||
|
modal_match_border = 0x000000
|
||||||
|
modal_match_text = 0xcccccc
|
||||||
|
modal_match_text_highlight = 0x18a3ff
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = "$elevation_3"
|
||||||
|
gutter_background = "$elevation_3"
|
||||||
|
active_line_background = "$elevation_4"
|
||||||
|
line_number = "$text_dull"
|
||||||
|
line_number_active = "$text_bright"
|
||||||
|
default_text = "$text_normal"
|
||||||
|
replicas = [
|
||||||
|
{ selection = 0x264f78, cursor = "$text_bright" },
|
||||||
|
{ selection = 0x504f31, cursor = 0xfcf154 },
|
||||||
|
]
|
|
@ -1,30 +1,13 @@
|
||||||
[ui]
|
extends = "base"
|
||||||
tab_background = 0x131415
|
|
||||||
tab_background_active = 0x1c1d1e
|
|
||||||
tab_text = 0x5a5a5b
|
|
||||||
tab_text_active = 0xffffff
|
|
||||||
tab_border = 0x000000
|
|
||||||
tab_icon_close = 0x383839
|
|
||||||
tab_icon_dirty = 0x556de8
|
|
||||||
tab_icon_conflict = 0xe45349
|
|
||||||
modal_background = 0x3a3b3c
|
|
||||||
modal_match_background = 0x424344
|
|
||||||
modal_match_background_active = 0x094771
|
|
||||||
modal_match_border = 0x000000
|
|
||||||
modal_match_text = 0xcccccc
|
|
||||||
modal_match_text_highlight = 0x18a3ff
|
|
||||||
|
|
||||||
[editor]
|
[variables]
|
||||||
background = 0x131415
|
elevation_1 = 0x050101
|
||||||
gutter_background = 0x131415
|
elevation_2 = 0x131415
|
||||||
active_line_background = 0x1c1d1e
|
elevation_3 = 0x1c1d1e
|
||||||
line_number = 0x5a5a5b
|
elevation_4 = 0x3a3b3c
|
||||||
line_number_active = 0xffffff
|
text_dull = 0x5a5a5b
|
||||||
default_text = 0xd4d4d4
|
text_bright = 0xffffff
|
||||||
replicas = [
|
text_normal = 0xd4d4d4
|
||||||
{ selection = 0x264f78, cursor = 0xffffff },
|
|
||||||
{ selection = 0x504f31, cursor = 0xfcf154 },
|
|
||||||
]
|
|
||||||
|
|
||||||
[syntax]
|
[syntax]
|
||||||
keyword = 0xc586c0
|
keyword = 0xc586c0
|
||||||
|
|
|
@ -340,7 +340,7 @@ mod tests {
|
||||||
util::RandomCharIter,
|
util::RandomCharIter,
|
||||||
};
|
};
|
||||||
use buffer::{History, SelectionGoal};
|
use buffer::{History, SelectionGoal};
|
||||||
use gpui::MutableAppContext;
|
use gpui::{color::ColorU, MutableAppContext};
|
||||||
use rand::{prelude::StdRng, Rng};
|
use rand::{prelude::StdRng, Rng};
|
||||||
use std::{env, sync::Arc};
|
use std::{env, sync::Arc};
|
||||||
use Bias::*;
|
use Bias::*;
|
||||||
|
@ -652,13 +652,21 @@ mod tests {
|
||||||
(function_item name: (identifier) @fn.name)"#,
|
(function_item name: (identifier) @fn.name)"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let theme = Theme::parse(
|
let theme = Theme {
|
||||||
r#"
|
syntax: vec![
|
||||||
[syntax]
|
(
|
||||||
"mod.body" = 0xff0000
|
"mod.body".to_string(),
|
||||||
"fn.name" = 0x00ff00"#,
|
ColorU::from_u32(0xff0000ff),
|
||||||
)
|
Default::default(),
|
||||||
.unwrap();
|
),
|
||||||
|
(
|
||||||
|
"fn.name".to_string(),
|
||||||
|
ColorU::from_u32(0x00ff00ff),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let lang = Arc::new(Language {
|
let lang = Arc::new(Language {
|
||||||
config: LanguageConfig {
|
config: LanguageConfig {
|
||||||
name: "Test".to_string(),
|
name: "Test".to_string(),
|
||||||
|
@ -742,13 +750,21 @@ mod tests {
|
||||||
(function_item name: (identifier) @fn.name)"#,
|
(function_item name: (identifier) @fn.name)"#,
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let theme = Theme::parse(
|
let theme = Theme {
|
||||||
r#"
|
syntax: vec![
|
||||||
[syntax]
|
(
|
||||||
"mod.body" = 0xff0000
|
"mod.body".to_string(),
|
||||||
"fn.name" = 0x00ff00"#,
|
ColorU::from_u32(0xff0000ff),
|
||||||
)
|
Default::default(),
|
||||||
.unwrap();
|
),
|
||||||
|
(
|
||||||
|
"fn.name".to_string(),
|
||||||
|
ColorU::from_u32(0x00ff00ff),
|
||||||
|
Default::default(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
let lang = Arc::new(Language {
|
let lang = Arc::new(Language {
|
||||||
config: LanguageConfig {
|
config: LanguageConfig {
|
||||||
name: "Test".to_string(),
|
name: "Test".to_string(),
|
||||||
|
|
|
@ -18,9 +18,11 @@ pub mod workspace;
|
||||||
pub mod worktree;
|
pub mod worktree;
|
||||||
|
|
||||||
pub use settings::Settings;
|
pub use settings::Settings;
|
||||||
|
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub settings: postage::watch::Receiver<Settings>,
|
pub settings: postage::watch::Receiver<Settings>,
|
||||||
pub languages: std::sync::Arc<language::LanguageRegistry>,
|
pub languages: std::sync::Arc<language::LanguageRegistry>,
|
||||||
|
pub themes: std::sync::Arc<settings::ThemeRegistry>,
|
||||||
pub rpc_router: std::sync::Arc<ForegroundRouter>,
|
pub rpc_router: std::sync::Arc<ForegroundRouter>,
|
||||||
pub rpc: rpc::Client,
|
pub rpc: rpc::Client,
|
||||||
pub fs: std::sync::Arc<dyn fs::Fs>,
|
pub fs: std::sync::Arc<dyn fs::Fs>,
|
||||||
|
|
|
@ -20,13 +20,15 @@ fn main() {
|
||||||
|
|
||||||
let app = gpui::App::new(assets::Assets).unwrap();
|
let app = gpui::App::new(assets::Assets).unwrap();
|
||||||
|
|
||||||
let (_, settings) = settings::channel(&app.font_cache()).unwrap();
|
let themes = settings::ThemeRegistry::new(assets::Assets);
|
||||||
|
let (_, settings) = settings::channel_with_themes(&app.font_cache(), &themes).unwrap();
|
||||||
let languages = Arc::new(language::LanguageRegistry::new());
|
let languages = Arc::new(language::LanguageRegistry::new());
|
||||||
languages.set_theme(&settings.borrow().theme);
|
languages.set_theme(&settings.borrow().theme);
|
||||||
|
|
||||||
let mut app_state = AppState {
|
let mut app_state = AppState {
|
||||||
languages: languages.clone(),
|
languages: languages.clone(),
|
||||||
settings,
|
settings,
|
||||||
|
themes,
|
||||||
rpc_router: Arc::new(ForegroundRouter::new()),
|
rpc_router: Arc::new(ForegroundRouter::new()),
|
||||||
rpc: rpc::Client::new(languages),
|
rpc: rpc::Client::new(languages),
|
||||||
fs: Arc::new(RealFs),
|
fs: Arc::new(RealFs),
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
use super::assets::Assets;
|
|
||||||
use anyhow::{anyhow, Context, Result};
|
use anyhow::{anyhow, Context, Result};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
color::ColorU,
|
color::ColorU,
|
||||||
font_cache::{FamilyId, FontCache},
|
font_cache::{FamilyId, FontCache},
|
||||||
fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
|
fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight},
|
||||||
|
AssetSource,
|
||||||
};
|
};
|
||||||
|
use parking_lot::Mutex;
|
||||||
use postage::watch;
|
use postage::watch;
|
||||||
use serde::Deserialize;
|
use serde::{de::value::MapDeserializer, Deserialize};
|
||||||
|
use serde_json::Value;
|
||||||
use std::{
|
use std::{
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
fmt,
|
fmt,
|
||||||
|
@ -26,16 +28,37 @@ pub struct Settings {
|
||||||
pub theme: Arc<Theme>,
|
pub theme: Arc<Theme>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub struct ThemeRegistry {
|
||||||
|
assets: Box<dyn AssetSource>,
|
||||||
|
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
||||||
|
theme_data: Mutex<HashMap<String, Arc<ThemeToml>>>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default)]
|
#[derive(Clone, Default)]
|
||||||
pub struct Theme {
|
pub struct Theme {
|
||||||
pub ui: UiTheme,
|
pub ui: UiTheme,
|
||||||
pub editor: EditorTheme,
|
pub editor: EditorTheme,
|
||||||
syntax: Vec<(String, ColorU, FontProperties)>,
|
pub syntax: Vec<(String, ColorU, FontProperties)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct ThemeToml {
|
||||||
|
#[serde(default)]
|
||||||
|
extends: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
variables: HashMap<String, Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
ui: HashMap<String, Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
editor: HashMap<String, Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
syntax: HashMap<String, Value>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Default, Deserialize)]
|
#[derive(Clone, Default, Deserialize)]
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub struct UiTheme {
|
pub struct UiTheme {
|
||||||
|
pub background: Color,
|
||||||
pub tab_background: Color,
|
pub tab_background: Color,
|
||||||
pub tab_background_active: Color,
|
pub tab_background_active: Color,
|
||||||
pub tab_text: Color,
|
pub tab_text: Color,
|
||||||
|
@ -81,16 +104,17 @@ pub struct StyleId(u32);
|
||||||
|
|
||||||
impl Settings {
|
impl Settings {
|
||||||
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
pub fn new(font_cache: &FontCache) -> Result<Self> {
|
||||||
|
Self::new_with_theme(font_cache, Arc::new(Theme::default()))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_with_theme(font_cache: &FontCache, theme: Arc<Theme>) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?,
|
buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?,
|
||||||
buffer_font_size: 14.0,
|
buffer_font_size: 14.0,
|
||||||
tab_size: 4,
|
tab_size: 4,
|
||||||
ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
|
ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?,
|
||||||
ui_font_size: 12.0,
|
ui_font_size: 12.0,
|
||||||
theme: Arc::new(
|
theme,
|
||||||
Theme::parse(Assets::get("themes/dark.toml").unwrap())
|
|
||||||
.expect("Failed to parse built-in theme"),
|
|
||||||
),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -100,62 +124,104 @@ impl Settings {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Theme {
|
impl ThemeRegistry {
|
||||||
pub fn parse(source: impl AsRef<[u8]>) -> Result<Self> {
|
pub fn new(source: impl AssetSource) -> Arc<Self> {
|
||||||
#[derive(Deserialize)]
|
Arc::new(Self {
|
||||||
struct ThemeToml {
|
assets: Box::new(source),
|
||||||
#[serde(default)]
|
themes: Default::default(),
|
||||||
ui: UiTheme,
|
theme_data: Default::default(),
|
||||||
#[serde(default)]
|
|
||||||
editor: EditorTheme,
|
|
||||||
#[serde(default)]
|
|
||||||
syntax: HashMap<String, StyleToml>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
#[serde(untagged)]
|
|
||||||
enum StyleToml {
|
|
||||||
Color(Color),
|
|
||||||
Full {
|
|
||||||
color: Option<Color>,
|
|
||||||
weight: Option<toml::Value>,
|
|
||||||
#[serde(default)]
|
|
||||||
italic: bool,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
let theme_toml: ThemeToml =
|
|
||||||
toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?;
|
|
||||||
|
|
||||||
let mut syntax = Vec::<(String, ColorU, FontProperties)>::new();
|
|
||||||
for (key, style) in theme_toml.syntax {
|
|
||||||
let (color, weight, italic) = match style {
|
|
||||||
StyleToml::Color(color) => (color, None, false),
|
|
||||||
StyleToml::Full {
|
|
||||||
color,
|
|
||||||
weight,
|
|
||||||
italic,
|
|
||||||
} => (color.unwrap_or(Color::default()), weight, italic),
|
|
||||||
};
|
|
||||||
match syntax.binary_search_by_key(&&key, |e| &e.0) {
|
|
||||||
Ok(i) | Err(i) => {
|
|
||||||
let mut properties = FontProperties::new();
|
|
||||||
properties.weight = deserialize_weight(weight)?;
|
|
||||||
if italic {
|
|
||||||
properties.style = FontStyle::Italic;
|
|
||||||
}
|
|
||||||
syntax.insert(i, (key, color.0, properties));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Theme {
|
|
||||||
ui: theme_toml.ui,
|
|
||||||
editor: theme_toml.editor,
|
|
||||||
syntax,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
||||||
|
if let Some(theme) = self.themes.lock().get(name) {
|
||||||
|
return Ok(theme.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme_toml = self.load(name)?;
|
||||||
|
let mut syntax = Vec::<(String, ColorU, FontProperties)>::new();
|
||||||
|
for (key, style) in theme_toml.syntax.iter() {
|
||||||
|
let mut color = Color::default();
|
||||||
|
let mut properties = FontProperties::new();
|
||||||
|
match style {
|
||||||
|
Value::Object(object) => {
|
||||||
|
if let Some(value) = object.get("color") {
|
||||||
|
color = serde_json::from_value(value.clone())?;
|
||||||
|
}
|
||||||
|
if let Some(Value::Bool(true)) = object.get("italic") {
|
||||||
|
properties.style = FontStyle::Italic;
|
||||||
|
}
|
||||||
|
properties.weight = deserialize_weight(object.get("weight"))?;
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
color = serde_json::from_value(style.clone())?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
match syntax.binary_search_by_key(&key, |e| &e.0) {
|
||||||
|
Ok(i) | Err(i) => {
|
||||||
|
syntax.insert(i, (key.to_string(), color.0, properties));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let theme = Arc::new(Theme {
|
||||||
|
ui: UiTheme::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?,
|
||||||
|
editor: EditorTheme::deserialize(MapDeserializer::new(
|
||||||
|
theme_toml.editor.clone().into_iter(),
|
||||||
|
))?,
|
||||||
|
syntax,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.themes.lock().insert(name.to_string(), theme.clone());
|
||||||
|
Ok(theme)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load(&self, name: &str) -> Result<Arc<ThemeToml>> {
|
||||||
|
if let Some(data) = self.theme_data.lock().get(name) {
|
||||||
|
return Ok(data.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let asset_path = format!("themes/{}.toml", name);
|
||||||
|
let source_code = self
|
||||||
|
.assets
|
||||||
|
.load(&asset_path)
|
||||||
|
.with_context(|| format!("failed to load theme file {}", asset_path))?;
|
||||||
|
|
||||||
|
let mut theme_toml: ThemeToml = toml::from_slice(source_code.as_ref())
|
||||||
|
.with_context(|| format!("failed to parse {}.toml", name))?;
|
||||||
|
|
||||||
|
// If this theme extends another base theme, merge in the raw data from the base theme.
|
||||||
|
if let Some(base_name) = theme_toml.extends.as_ref() {
|
||||||
|
let base_theme_toml = self
|
||||||
|
.load(base_name)
|
||||||
|
.with_context(|| format!("failed to load base theme {}", base_name))?;
|
||||||
|
merge_map(&mut theme_toml.ui, &base_theme_toml.ui);
|
||||||
|
merge_map(&mut theme_toml.editor, &base_theme_toml.editor);
|
||||||
|
merge_map(&mut theme_toml.syntax, &base_theme_toml.syntax);
|
||||||
|
merge_map(&mut theme_toml.variables, &base_theme_toml.variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Substitute any variable references for their definitions.
|
||||||
|
let values = theme_toml
|
||||||
|
.ui
|
||||||
|
.values_mut()
|
||||||
|
.chain(theme_toml.editor.values_mut())
|
||||||
|
.chain(theme_toml.syntax.values_mut());
|
||||||
|
let mut name_stack = Vec::new();
|
||||||
|
for value in values {
|
||||||
|
name_stack.clear();
|
||||||
|
evaluate_variables(value, &theme_toml.variables, &mut name_stack)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = Arc::new(theme_toml);
|
||||||
|
self.theme_data
|
||||||
|
.lock()
|
||||||
|
.insert(name.to_string(), result.clone());
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Theme {
|
||||||
pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) {
|
pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) {
|
||||||
self.syntax.get(id.0 as usize).map_or(
|
self.syntax.get(id.0 as usize).map_or(
|
||||||
(self.editor.default_text.0, FontProperties::new()),
|
(self.editor.default_text.0, FontProperties::new()),
|
||||||
|
@ -221,13 +287,19 @@ impl Default for StyleId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Color {
|
||||||
|
fn from_u32(rgba: u32) -> Self {
|
||||||
|
Self(ColorU::from_u32(rgba))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl<'de> Deserialize<'de> for Color {
|
impl<'de> Deserialize<'de> for Color {
|
||||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
let rgba_value = u32::deserialize(deserializer)?;
|
let rgb = u32::deserialize(deserializer)?;
|
||||||
Ok(Self(ColorU::from_u32((rgba_value << 8) + 0xFF)))
|
Ok(Self::from_u32((rgb << 8) + 0xFF))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -268,11 +340,25 @@ pub fn channel(
|
||||||
Ok(watch::channel_with(Settings::new(font_cache)?))
|
Ok(watch::channel_with(Settings::new(font_cache)?))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
|
pub fn channel_with_themes(
|
||||||
match &weight {
|
font_cache: &FontCache,
|
||||||
|
themes: &ThemeRegistry,
|
||||||
|
) -> Result<(watch::Sender<Settings>, watch::Receiver<Settings>)> {
|
||||||
|
Ok(watch::channel_with(Settings::new_with_theme(
|
||||||
|
font_cache,
|
||||||
|
themes.get("dark").expect("failed to load default theme"),
|
||||||
|
)?))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deserialize_weight(weight: Option<&Value>) -> Result<FontWeight> {
|
||||||
|
match weight {
|
||||||
None => return Ok(FontWeight::NORMAL),
|
None => return Ok(FontWeight::NORMAL),
|
||||||
Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)),
|
Some(Value::Number(number)) => {
|
||||||
Some(toml::Value::String(s)) => match s.as_str() {
|
if let Some(weight) = number.as_f64() {
|
||||||
|
return Ok(FontWeight(weight as f32));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(Value::String(s)) => match s.as_str() {
|
||||||
"normal" => return Ok(FontWeight::NORMAL),
|
"normal" => return Ok(FontWeight::NORMAL),
|
||||||
"bold" => return Ok(FontWeight::BOLD),
|
"bold" => return Ok(FontWeight::BOLD),
|
||||||
"light" => return Ok(FontWeight::LIGHT),
|
"light" => return Ok(FontWeight::LIGHT),
|
||||||
|
@ -284,13 +370,70 @@ fn deserialize_weight(weight: Option<toml::Value>) -> Result<FontWeight> {
|
||||||
Err(anyhow!("Invalid weight {}", weight.unwrap()))
|
Err(anyhow!("Invalid weight {}", weight.unwrap()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn evaluate_variables(
|
||||||
|
expr: &mut Value,
|
||||||
|
variables: &HashMap<String, Value>,
|
||||||
|
stack: &mut Vec<String>,
|
||||||
|
) -> Result<()> {
|
||||||
|
match expr {
|
||||||
|
Value::String(s) => {
|
||||||
|
if let Some(name) = s.strip_prefix("$") {
|
||||||
|
if stack.iter().any(|e| e == name) {
|
||||||
|
Err(anyhow!("variable {} is defined recursively", name))?;
|
||||||
|
}
|
||||||
|
if validate_variable_name(name) {
|
||||||
|
stack.push(name.to_string());
|
||||||
|
if let Some(definition) = variables.get(name).cloned() {
|
||||||
|
*expr = definition;
|
||||||
|
evaluate_variables(expr, variables, stack)?;
|
||||||
|
}
|
||||||
|
stack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Array(a) => {
|
||||||
|
for value in a.iter_mut() {
|
||||||
|
evaluate_variables(value, variables, stack)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Value::Object(object) => {
|
||||||
|
for value in object.values_mut() {
|
||||||
|
evaluate_variables(value, variables, stack)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_variable_name(name: &str) -> bool {
|
||||||
|
let mut chars = name.chars();
|
||||||
|
if let Some(first) = chars.next() {
|
||||||
|
if first.is_alphabetic() || first == '_' {
|
||||||
|
if chars.all(|c| c.is_alphanumeric() || c == '_') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
false
|
||||||
|
}
|
||||||
|
|
||||||
|
fn merge_map(left: &mut HashMap<String, Value>, right: &HashMap<String, Value>) {
|
||||||
|
for (name, value) in right {
|
||||||
|
if !left.contains_key(name) {
|
||||||
|
left.insert(name.clone(), value.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_theme() {
|
fn test_parse_simple_theme() {
|
||||||
let theme = Theme::parse(
|
let assets = TestAssets(&[(
|
||||||
|
"themes/my-theme.toml",
|
||||||
r#"
|
r#"
|
||||||
[ui]
|
[ui]
|
||||||
tab_background_active = 0x100000
|
tab_background_active = 0x100000
|
||||||
|
@ -304,8 +447,10 @@ mod tests {
|
||||||
"alpha.one" = {color = 0x112233, weight = "bold"}
|
"alpha.one" = {color = 0x112233, weight = "bold"}
|
||||||
"gamma.three" = {weight = "light", italic = true}
|
"gamma.three" = {weight = "light", italic = true}
|
||||||
"#,
|
"#,
|
||||||
)
|
)]);
|
||||||
.unwrap();
|
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
let theme = registry.get("my-theme").unwrap();
|
||||||
|
|
||||||
assert_eq!(theme.ui.tab_background_active, ColorU::from_u32(0x100000ff));
|
assert_eq!(theme.ui.tab_background_active, ColorU::from_u32(0x100000ff));
|
||||||
assert_eq!(theme.editor.background, ColorU::from_u32(0x00ed00ff));
|
assert_eq!(theme.editor.background, ColorU::from_u32(0x00ed00ff));
|
||||||
|
@ -334,9 +479,53 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_extended_theme() {
|
||||||
|
let assets = TestAssets(&[
|
||||||
|
(
|
||||||
|
"themes/base.toml",
|
||||||
|
r#"
|
||||||
|
[ui]
|
||||||
|
tab_background = 0x111111
|
||||||
|
tab_text = "$variable_1"
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = 0x222222
|
||||||
|
default_text = "$variable_2"
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"themes/light.toml",
|
||||||
|
r#"
|
||||||
|
extends = "base"
|
||||||
|
|
||||||
|
[variables]
|
||||||
|
variable_1 = 0x333333
|
||||||
|
variable_2 = 0x444444
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
tab_background = 0x555555
|
||||||
|
|
||||||
|
[editor]
|
||||||
|
background = 0x666666
|
||||||
|
"#,
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
let theme = registry.get("light").unwrap();
|
||||||
|
|
||||||
|
assert_eq!(theme.ui.tab_background, ColorU::from_u32(0x555555ff));
|
||||||
|
assert_eq!(theme.ui.tab_text, ColorU::from_u32(0x333333ff));
|
||||||
|
assert_eq!(theme.editor.background, ColorU::from_u32(0x666666ff));
|
||||||
|
assert_eq!(theme.editor.default_text, ColorU::from_u32(0x444444ff));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_empty_theme() {
|
fn test_parse_empty_theme() {
|
||||||
Theme::parse("").unwrap();
|
let assets = TestAssets(&[("themes/my-theme.toml", "")]);
|
||||||
|
let registry = ThemeRegistry::new(assets);
|
||||||
|
registry.get("my-theme").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -371,4 +560,16 @@ mod tests {
|
||||||
Some("variable.builtin")
|
Some("variable.builtin")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct TestAssets(&'static [(&'static str, &'static str)]);
|
||||||
|
|
||||||
|
impl AssetSource for TestAssets {
|
||||||
|
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||||
|
if let Some(row) = self.0.iter().find(|e| e.0 == path) {
|
||||||
|
Ok(row.1.as_bytes().into())
|
||||||
|
} else {
|
||||||
|
Err(anyhow!("no such path {}", path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,11 @@
|
||||||
use crate::{fs::RealFs, language::LanguageRegistry, rpc, settings, time::ReplicaId, AppState};
|
use crate::{
|
||||||
|
fs::RealFs,
|
||||||
|
language::LanguageRegistry,
|
||||||
|
rpc,
|
||||||
|
settings::{self, ThemeRegistry},
|
||||||
|
time::ReplicaId,
|
||||||
|
AppState,
|
||||||
|
};
|
||||||
use gpui::{AppContext, Entity, ModelHandle};
|
use gpui::{AppContext, Entity, ModelHandle};
|
||||||
use smol::channel;
|
use smol::channel;
|
||||||
use std::{
|
use std::{
|
||||||
|
@ -149,8 +156,10 @@ fn write_tree(path: &Path, tree: serde_json::Value) {
|
||||||
pub fn build_app_state(cx: &AppContext) -> Arc<AppState> {
|
pub fn build_app_state(cx: &AppContext) -> Arc<AppState> {
|
||||||
let settings = settings::channel(&cx.font_cache()).unwrap().1;
|
let settings = settings::channel(&cx.font_cache()).unwrap().1;
|
||||||
let languages = Arc::new(LanguageRegistry::new());
|
let languages = Arc::new(LanguageRegistry::new());
|
||||||
|
let themes = ThemeRegistry::new(());
|
||||||
Arc::new(AppState {
|
Arc::new(AppState {
|
||||||
settings,
|
settings,
|
||||||
|
themes,
|
||||||
languages: languages.clone(),
|
languages: languages.clone(),
|
||||||
rpc_router: Arc::new(ForegroundRouter::new()),
|
rpc_router: Arc::new(ForegroundRouter::new()),
|
||||||
rpc: rpc::Client::new(languages),
|
rpc: rpc::Client::new(languages),
|
||||||
|
|
|
@ -887,7 +887,7 @@ impl View for Workspace {
|
||||||
.with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
|
.with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed()))
|
||||||
.boxed(),
|
.boxed(),
|
||||||
)
|
)
|
||||||
.with_background_color(settings.theme.editor.background)
|
.with_background_color(settings.theme.ui.background)
|
||||||
.named("workspace")
|
.named("workspace")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue