From 92353b696784e29eae1c06a2a535af2b1a1738fc Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Fri, 30 Jul 2021 17:22:59 -0700 Subject: [PATCH 01/23] Start work on allowing variables in themes --- zed/Cargo.toml | 6 +- zed/assets/themes/base.toml | 28 +++ zed/assets/themes/dark.toml | 35 +--- zed/src/editor/display_map.rs | 46 +++-- zed/src/lib.rs | 2 + zed/src/main.rs | 4 +- zed/src/settings.rs | 323 +++++++++++++++++++++++++++------- zed/src/test.rs | 11 +- zed/src/workspace.rs | 2 +- 9 files changed, 348 insertions(+), 109 deletions(-) create mode 100644 zed/assets/themes/base.toml diff --git a/zed/Cargo.toml b/zed/Cargo.toml index 7e4d4949d6732de853f942fae8b6e234dc9bc677..25aafaac5f9f71adecd8b4f99bc1425194cbc755 100644 --- a/zed/Cargo.toml +++ b/zed/Cargo.toml @@ -14,7 +14,7 @@ name = "Zed" path = "src/main.rs" [features] -test-support = ["tempdir", "serde_json", "zrpc/test-support"] +test-support = ["tempdir", "zrpc/test-support"] [dependencies] anyhow = "1.0.38" @@ -41,9 +41,7 @@ rsa = "0.4" rust-embed = "5.9.0" seahash = "4.1" serde = { version = "1", features = ["derive"] } -serde_json = { version = "1.0.64", features = [ - "preserve_order", -], optional = true } +serde_json = { version = "1.0.64", features = ["preserve_order"] } similar = "1.3" simplelog = "0.9" smallvec = { version = "1.6", features = ["union"] } diff --git a/zed/assets/themes/base.toml b/zed/assets/themes/base.toml new file mode 100644 index 0000000000000000000000000000000000000000..fd86c98e6744bce343c38cb46a9ea8e451b25018 --- /dev/null +++ b/zed/assets/themes/base.toml @@ -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 }, +] diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index e4b064ca97a74f91ed0cca7ab2d1460dd7a93cd7..2376293c8afb6126671d70d16263a9e9c96d5d0a 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,30 +1,13 @@ -[ui] -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 +extends = "base" -[editor] -background = 0x131415 -gutter_background = 0x131415 -active_line_background = 0x1c1d1e -line_number = 0x5a5a5b -line_number_active = 0xffffff -default_text = 0xd4d4d4 -replicas = [ - { selection = 0x264f78, cursor = 0xffffff }, - { selection = 0x504f31, cursor = 0xfcf154 }, -] +[variables] +elevation_1 = 0x050101 +elevation_2 = 0x131415 +elevation_3 = 0x1c1d1e +elevation_4 = 0x3a3b3c +text_dull = 0x5a5a5b +text_bright = 0xffffff +text_normal = 0xd4d4d4 [syntax] keyword = 0xc586c0 diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 8efb42f71eef45021f39535436fed60809bd4153..2bebfeaf9e4798e023abfb4cce368a1d890fbb95 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -340,7 +340,7 @@ mod tests { util::RandomCharIter, }; use buffer::{History, SelectionGoal}; - use gpui::MutableAppContext; + use gpui::{color::ColorU, MutableAppContext}; use rand::{prelude::StdRng, Rng}; use std::{env, sync::Arc}; use Bias::*; @@ -652,13 +652,21 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme::parse( - r#" - [syntax] - "mod.body" = 0xff0000 - "fn.name" = 0x00ff00"#, - ) - .unwrap(); + let theme = Theme { + syntax: vec![ + ( + "mod.body".to_string(), + ColorU::from_u32(0xff0000ff), + Default::default(), + ), + ( + "fn.name".to_string(), + ColorU::from_u32(0x00ff00ff), + Default::default(), + ), + ], + ..Default::default() + }; let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), @@ -742,13 +750,21 @@ mod tests { (function_item name: (identifier) @fn.name)"#, ) .unwrap(); - let theme = Theme::parse( - r#" - [syntax] - "mod.body" = 0xff0000 - "fn.name" = 0x00ff00"#, - ) - .unwrap(); + let theme = Theme { + syntax: vec![ + ( + "mod.body".to_string(), + ColorU::from_u32(0xff0000ff), + Default::default(), + ), + ( + "fn.name".to_string(), + ColorU::from_u32(0x00ff00ff), + Default::default(), + ), + ], + ..Default::default() + }; let lang = Arc::new(Language { config: LanguageConfig { name: "Test".to_string(), diff --git a/zed/src/lib.rs b/zed/src/lib.rs index b8c84feb1a1079bb786aab564c6f8d34a8ca1f47..b8cfc02c087161ff6965a07604f2d97a2600b396 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -18,9 +18,11 @@ pub mod workspace; pub mod worktree; pub use settings::Settings; + pub struct AppState { pub settings: postage::watch::Receiver, pub languages: std::sync::Arc, + pub themes: std::sync::Arc, pub rpc_router: std::sync::Arc, pub rpc: rpc::Client, pub fs: std::sync::Arc, diff --git a/zed/src/main.rs b/zed/src/main.rs index d69818a54100faa3b1aa89f13179b1aa950fbc53..9cbf082b8c53102bd9f0d558806b6b6ee1f776ac 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -20,13 +20,15 @@ fn main() { 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()); languages.set_theme(&settings.borrow().theme); let mut app_state = AppState { languages: languages.clone(), settings, + themes, rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), fs: Arc::new(RealFs), diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 54621b905b59812306a1475ce36ce5935d52568a..1eabc19ef0e5c6b6f8c9e3f42a1deb739ba79b8a 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -1,12 +1,14 @@ -use super::assets::Assets; use anyhow::{anyhow, Context, Result}; use gpui::{ color::ColorU, font_cache::{FamilyId, FontCache}, fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}, + AssetSource, }; +use parking_lot::Mutex; use postage::watch; -use serde::Deserialize; +use serde::{de::value::MapDeserializer, Deserialize}; +use serde_json::Value; use std::{ collections::HashMap, fmt, @@ -26,16 +28,37 @@ pub struct Settings { pub theme: Arc, } +pub struct ThemeRegistry { + assets: Box, + themes: Mutex>>, + theme_data: Mutex>>, +} + #[derive(Clone, Default)] pub struct Theme { pub ui: UiTheme, pub editor: EditorTheme, - syntax: Vec<(String, ColorU, FontProperties)>, + pub syntax: Vec<(String, ColorU, FontProperties)>, +} + +#[derive(Deserialize)] +struct ThemeToml { + #[serde(default)] + extends: Option, + #[serde(default)] + variables: HashMap, + #[serde(default)] + ui: HashMap, + #[serde(default)] + editor: HashMap, + #[serde(default)] + syntax: HashMap, } #[derive(Clone, Default, Deserialize)] #[serde(default)] pub struct UiTheme { + pub background: Color, pub tab_background: Color, pub tab_background_active: Color, pub tab_text: Color, @@ -81,16 +104,17 @@ pub struct StyleId(u32); impl Settings { pub fn new(font_cache: &FontCache) -> Result { + Self::new_with_theme(font_cache, Arc::new(Theme::default())) + } + + pub fn new_with_theme(font_cache: &FontCache, theme: Arc) -> Result { Ok(Self { buffer_font_family: font_cache.load_family(&["Fira Code", "Monaco"])?, buffer_font_size: 14.0, tab_size: 4, ui_font_family: font_cache.load_family(&["SF Pro", "Helvetica"])?, ui_font_size: 12.0, - theme: Arc::new( - Theme::parse(Assets::get("themes/dark.toml").unwrap()) - .expect("Failed to parse built-in theme"), - ), + theme, }) } @@ -100,62 +124,104 @@ impl Settings { } } -impl Theme { - pub fn parse(source: impl AsRef<[u8]>) -> Result { - #[derive(Deserialize)] - struct ThemeToml { - #[serde(default)] - ui: UiTheme, - #[serde(default)] - editor: EditorTheme, - #[serde(default)] - syntax: HashMap, - } +impl ThemeRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + assets: Box::new(source), + themes: Default::default(), + theme_data: Default::default(), + }) + } - #[derive(Deserialize)] - #[serde(untagged)] - enum StyleToml { - Color(Color), - Full { - color: Option, - weight: Option, - #[serde(default)] - italic: bool, - }, + pub fn get(&self, name: &str) -> Result> { + if let Some(theme) = self.themes.lock().get(name) { + return Ok(theme.clone()); } - let theme_toml: ThemeToml = - toml::from_slice(source.as_ref()).context("failed to parse theme TOML")?; - + let theme_toml = self.load(name)?; 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 { + 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; } - syntax.insert(i, (key, color.0, properties)); + 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)); } } } - Ok(Theme { - ui: theme_toml.ui, - editor: theme_toml.editor, + 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> { + 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) { self.syntax.get(id.0 as usize).map_or( (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 { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { - let rgba_value = u32::deserialize(deserializer)?; - Ok(Self(ColorU::from_u32((rgba_value << 8) + 0xFF))) + let rgb = u32::deserialize(deserializer)?; + Ok(Self::from_u32((rgb << 8) + 0xFF)) } } @@ -268,11 +340,25 @@ pub fn channel( Ok(watch::channel_with(Settings::new(font_cache)?)) } -fn deserialize_weight(weight: Option) -> Result { - match &weight { +pub fn channel_with_themes( + font_cache: &FontCache, + themes: &ThemeRegistry, +) -> Result<(watch::Sender, watch::Receiver)> { + 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 { + match weight { None => return Ok(FontWeight::NORMAL), - Some(toml::Value::Integer(i)) => return Ok(FontWeight(*i as f32)), - Some(toml::Value::String(s)) => match s.as_str() { + Some(Value::Number(number)) => { + 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), "bold" => return Ok(FontWeight::BOLD), "light" => return Ok(FontWeight::LIGHT), @@ -284,13 +370,70 @@ fn deserialize_weight(weight: Option) -> Result { Err(anyhow!("Invalid weight {}", weight.unwrap())) } +fn evaluate_variables( + expr: &mut Value, + variables: &HashMap, + stack: &mut Vec, +) -> 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, right: &HashMap) { + for (name, value) in right { + if !left.contains_key(name) { + left.insert(name.clone(), value.clone()); + } + } +} + #[cfg(test)] mod tests { use super::*; #[test] - fn test_parse_theme() { - let theme = Theme::parse( + fn test_parse_simple_theme() { + let assets = TestAssets(&[( + "themes/my-theme.toml", r#" [ui] tab_background_active = 0x100000 @@ -304,8 +447,10 @@ mod tests { "alpha.one" = {color = 0x112233, weight = "bold"} "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.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] 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] @@ -371,4 +560,16 @@ mod tests { Some("variable.builtin") ); } + + struct TestAssets(&'static [(&'static str, &'static str)]); + + impl AssetSource for TestAssets { + fn load(&self, path: &str) -> Result> { + 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)) + } + } + } } diff --git a/zed/src/test.rs b/zed/src/test.rs index a3e5914b4ea94254a166b54a723b8da07795179b..f1367775f2cc428d28bb34adf93a3b4fdd867d91 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -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 smol::channel; use std::{ @@ -149,8 +156,10 @@ fn write_tree(path: &Path, tree: serde_json::Value) { pub fn build_app_state(cx: &AppContext) -> Arc { let settings = settings::channel(&cx.font_cache()).unwrap().1; let languages = Arc::new(LanguageRegistry::new()); + let themes = ThemeRegistry::new(()); Arc::new(AppState { settings, + themes, languages: languages.clone(), rpc_router: Arc::new(ForegroundRouter::new()), rpc: rpc::Client::new(languages), diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index 54e5f11f2c3d6be95b43051906db1f5f33fba006..d0d774a817ebc1a8927771efb7038215d73aa031 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -887,7 +887,7 @@ impl View for Workspace { .with_children(self.modal.as_ref().map(|m| ChildView::new(m.id()).boxed())) .boxed(), ) - .with_background_color(settings.theme.editor.background) + .with_background_color(settings.theme.ui.background) .named("workspace") } From 76c07fb2322572661ddf75ea24bdd69f48158ab9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Aug 2021 13:09:43 -0700 Subject: [PATCH 02/23] Make Theme::default have a non-empty replica theme vector Co-Authored-By: Nathan Sobo --- zed/src/settings.rs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 1eabc19ef0e5c6b6f8c9e3f42a1deb739ba79b8a..cc90a5a44366bc85b521e83024385cc178db7f28 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -75,7 +75,7 @@ pub struct UiTheme { pub modal_match_text_highlight: Color, } -#[derive(Clone, Default, Deserialize)] +#[derive(Clone, Deserialize)] #[serde(default)] pub struct EditorTheme { pub background: Color, @@ -87,7 +87,7 @@ pub struct EditorTheme { pub replicas: Vec, } -#[derive(Clone, Copy, Deserialize)] +#[derive(Clone, Copy, Deserialize, Default)] pub struct ReplicaTheme { pub cursor: Color, pub selection: Color, @@ -235,6 +235,20 @@ impl Theme { } } +impl Default for EditorTheme { + fn default() -> Self { + Self { + background: Default::default(), + gutter_background: Default::default(), + active_line_background: Default::default(), + line_number: Default::default(), + line_number_active: Default::default(), + default_text: Default::default(), + replicas: vec![ReplicaTheme::default()], + } + } +} + impl ThemeMap { pub fn new(capture_names: &[String], theme: &Theme) -> Self { // For each capture name in the highlight query, find the longest From e080739d57db2a96359e5436a4384847170c9f12 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Aug 2021 13:10:59 -0700 Subject: [PATCH 03/23] Remove use of replace_with crate for managing element lifecycles Co-Authored-By: Nathan Sobo --- Cargo.lock | 7 ------ gpui/Cargo.toml | 1 - gpui/src/elements.rs | 60 ++++++++++++++++++++++++-------------------- 3 files changed, 33 insertions(+), 35 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 695d4e9b4fb4c92c7af52465f7586524cd6138d4..a24b7b81edf21c93cbd8cd71fa934dc8daf79f50 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2172,7 +2172,6 @@ dependencies = [ "png 0.16.8", "postage", "rand 0.8.3", - "replace_with", "resvg", "seahash", "serde 1.0.125", @@ -3927,12 +3926,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "replace_with" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3a8614ee435691de62bcffcf4a66d91b3594bf1428a5722e79103249a095690" - [[package]] name = "resvg" version = "0.14.0" diff --git a/gpui/Cargo.toml b/gpui/Cargo.toml index d6e8640327d328421615cad7426fe7f11f365d7e..535fe16155ba9f2a1b16397cf2dcf27f050578cd 100644 --- a/gpui/Cargo.toml +++ b/gpui/Cargo.toml @@ -19,7 +19,6 @@ pathfinder_color = "0.5" pathfinder_geometry = "0.5" postage = { version = "0.4.1", features = ["futures-traits"] } rand = "0.8.3" -replace_with = "0.1.7" resvg = "0.14" seahash = "4.1" serde = { version = "1.0.125", features = ["derive"] } diff --git a/gpui/src/elements.rs b/gpui/src/elements.rs index 869a26b542d99d551537b579e5bad6222045fa6d..3d0357409484aafb8916bcc70b3c3bf561f2fa17 100644 --- a/gpui/src/elements.rs +++ b/gpui/src/elements.rs @@ -34,8 +34,7 @@ use crate::{ }; use core::panic; use json::ToJson; -use replace_with::replace_with_or_abort; -use std::{any::Any, borrow::Cow}; +use std::{any::Any, borrow::Cow, mem}; trait AnyElement { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F; @@ -115,6 +114,7 @@ pub trait Element { } pub enum Lifecycle { + Empty, Init { element: T, }, @@ -139,8 +139,9 @@ pub struct ElementBox { impl AnyElement for Lifecycle { fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { - let mut result = None; - replace_with_or_abort(self, |me| match me { + let result; + *self = match mem::take(self) { + Lifecycle::Empty => unreachable!(), Lifecycle::Init { mut element } | Lifecycle::PostLayout { mut element, .. } | Lifecycle::PostPaint { mut element, .. } => { @@ -148,7 +149,7 @@ impl AnyElement for Lifecycle { debug_assert!(size.x().is_finite()); debug_assert!(size.y().is_finite()); - result = Some(size); + result = size; Lifecycle::PostLayout { element, constraint, @@ -156,8 +157,8 @@ impl AnyElement for Lifecycle { layout, } } - }); - result.unwrap() + }; + result } fn after_layout(&mut self, cx: &mut AfterLayoutContext) { @@ -175,27 +176,25 @@ impl AnyElement for Lifecycle { } fn paint(&mut self, origin: Vector2F, cx: &mut PaintContext) { - replace_with_or_abort(self, |me| { - if let Lifecycle::PostLayout { - mut element, + *self = if let Lifecycle::PostLayout { + mut element, + constraint, + size, + mut layout, + } = mem::take(self) + { + let bounds = RectF::new(origin, size); + let paint = element.paint(bounds, &mut layout, cx); + Lifecycle::PostPaint { + element, constraint, - size, - mut layout, - } = me - { - let bounds = RectF::new(origin, size); - let paint = element.paint(bounds, &mut layout, cx); - Lifecycle::PostPaint { - element, - constraint, - bounds, - layout, - paint, - } - } else { - panic!("invalid element lifecycle state"); + bounds, + layout, + paint, } - }); + } else { + panic!("invalid element lifecycle state"); + }; } fn dispatch_event(&mut self, event: &Event, cx: &mut EventContext) -> bool { @@ -215,7 +214,7 @@ impl AnyElement for Lifecycle { fn size(&self) -> Vector2F { match self { - Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), + Lifecycle::Empty | Lifecycle::Init { .. } => panic!("invalid element lifecycle state"), Lifecycle::PostLayout { size, .. } => *size, Lifecycle::PostPaint { bounds, .. } => bounds.size(), } @@ -223,6 +222,7 @@ impl AnyElement for Lifecycle { fn metadata(&self) -> Option<&dyn Any> { match self { + Lifecycle::Empty => unreachable!(), Lifecycle::Init { element } | Lifecycle::PostLayout { element, .. } | Lifecycle::PostPaint { element, .. } => element.metadata(), @@ -257,6 +257,12 @@ impl AnyElement for Lifecycle { } } +impl Default for Lifecycle { + fn default() -> Self { + Self::Empty + } +} + impl ElementBox { pub fn layout(&mut self, constraint: SizeConstraint, cx: &mut LayoutContext) -> Vector2F { self.element.layout(constraint, cx) From b30d0daabf7340f3186687e98c2a09825b174c0b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 2 Aug 2021 14:55:27 -0700 Subject: [PATCH 04/23] Add a theme picker Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 2 +- gpui/src/app.rs | 71 +- gpui/src/assets.rs | 7 +- gpui/src/elements/uniform_list.rs | 16 +- server/src/tests.rs | 2 +- zed/assets/themes/{base.toml => _base.toml} | 0 zed/assets/themes/dark.toml | 2 +- zed/assets/themes/light.toml | 21 + zed/src/assets.rs | 4 + zed/src/editor.rs | 6 +- zed/src/file_finder.rs | 32 +- zed/src/lib.rs | 19 +- zed/src/main.rs | 11 +- zed/src/settings.rs | 47 +- zed/src/test.rs | 4 +- zed/src/theme_picker.rs | 308 ++++++++ zed/src/workspace.rs | 16 +- zed/src/workspace/pane.rs | 5 +- zed/src/worktree.rs | 8 +- zed/src/worktree/fuzzy.rs | 800 ++++++++++++-------- 20 files changed, 963 insertions(+), 418 deletions(-) rename zed/assets/themes/{base.toml => _base.toml} (100%) create mode 100644 zed/assets/themes/light.toml create mode 100644 zed/src/theme_picker.rs diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 11c327e2bb75488e1a9c33d8065af4aa920ffb37..58314b5e948ab7175394f78e32a8721e4fcd54c5 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -28,7 +28,7 @@ impl gpui::View for TextView { "View" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render(&self, _: &gpui::RenderContext) -> gpui::ElementBox { TextElement.boxed() } } diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 05e5ee15c47fa1757f720d7d15f12e713697b343..6d857952e2724a2997f16d499deadaf53bb5510a 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -36,9 +36,9 @@ pub trait Entity: 'static + Send + Sync { fn release(&mut self, _: &mut MutableAppContext) {} } -pub trait View: Entity { +pub trait View: Entity + Sized { fn ui_name() -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render(&self, cx: &RenderContext<'_, Self>) -> ElementBox; fn on_focus(&mut self, _: &mut ViewContext) {} fn on_blur(&mut self, _: &mut ViewContext) {} fn keymap_context(&self, _: &AppContext) -> keymap::Context { @@ -1503,7 +1503,7 @@ impl AppContext { pub fn render_view(&self, window_id: usize, view_id: usize) -> Result { self.views .get(&(window_id, view_id)) - .map(|v| v.render(self)) + .map(|v| v.render(window_id, view_id, self)) .ok_or(anyhow!("view not found")) } @@ -1512,7 +1512,7 @@ impl AppContext { .iter() .filter_map(|((win_id, view_id), view)| { if *win_id == window_id { - Some((*view_id, view.render(self))) + Some((*view_id, view.render(*win_id, *view_id, self))) } else { None } @@ -1650,7 +1650,7 @@ pub trait AnyView: Send + Sync { fn as_any_mut(&mut self) -> &mut dyn Any; fn release(&mut self, cx: &mut MutableAppContext); fn ui_name(&self) -> &'static str; - fn render<'a>(&self, cx: &AppContext) -> ElementBox; + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox; fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn on_blur(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize); fn keymap_context(&self, cx: &AppContext) -> keymap::Context; @@ -1676,8 +1676,16 @@ where T::ui_name() } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { - View::render(self, cx) + fn render<'a>(&self, window_id: usize, view_id: usize, cx: &AppContext) -> ElementBox { + View::render( + self, + &RenderContext { + window_id, + view_id, + app: cx, + view_type: PhantomData::, + }, + ) } fn on_focus(&mut self, cx: &mut MutableAppContext, window_id: usize, view_id: usize) { @@ -2094,12 +2102,33 @@ impl<'a, T: View> ViewContext<'a, T> { } } +pub struct RenderContext<'a, T: View> { + pub app: &'a AppContext, + window_id: usize, + view_id: usize, + view_type: PhantomData, +} + +impl<'a, T: View> RenderContext<'a, T> { + pub fn handle(&self) -> WeakViewHandle { + WeakViewHandle::new(self.window_id, self.view_id) + } +} + impl AsRef for &AppContext { fn as_ref(&self) -> &AppContext { self } } +impl Deref for RenderContext<'_, V> { + type Target = AppContext; + + fn deref(&self) -> &Self::Target { + &self.app + } +} + impl AsRef for ViewContext<'_, M> { fn as_ref(&self) -> &AppContext { &self.app.cx @@ -3004,7 +3033,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3067,7 +3096,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { let mouse_down_count = self.mouse_down_count.clone(); EventHandler::new(Empty::new().boxed()) .on_mouse_down(move |_| { @@ -3129,7 +3158,7 @@ mod tests { "View" } - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3169,7 +3198,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3222,7 +3251,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3272,7 +3301,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3315,7 +3344,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3362,7 +3391,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3420,7 +3449,7 @@ mod tests { } impl View for ViewA { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3438,7 +3467,7 @@ mod tests { } impl View for ViewB { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3541,7 +3570,7 @@ mod tests { } impl super::View for View { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } @@ -3674,7 +3703,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3719,7 +3748,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } @@ -3742,7 +3771,7 @@ mod tests { "test view" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { Empty::new().boxed() } } diff --git a/gpui/src/assets.rs b/gpui/src/assets.rs index 63c6c07570a35de73415afe97c61e6229e2c6514..ac0d72dee97a8d5553963c4677bc3f6051fbaf34 100644 --- a/gpui/src/assets.rs +++ b/gpui/src/assets.rs @@ -1,8 +1,9 @@ use anyhow::{anyhow, Result}; use std::{borrow::Cow, cell::RefCell, collections::HashMap}; -pub trait AssetSource: 'static { +pub trait AssetSource: 'static + Send + Sync { fn load(&self, path: &str) -> Result>; + fn list(&self, path: &str) -> Vec>; } impl AssetSource for () { @@ -12,6 +13,10 @@ impl AssetSource for () { path )) } + + fn list(&self, _: &str) -> Vec> { + vec![] + } } pub struct AssetCache { diff --git a/gpui/src/elements/uniform_list.rs b/gpui/src/elements/uniform_list.rs index b414a20430159ff373422240add657792687f2e9..74ebccdf379080beb603f1464d19a08e132154a0 100644 --- a/gpui/src/elements/uniform_list.rs +++ b/gpui/src/elements/uniform_list.rs @@ -13,17 +13,10 @@ use json::ToJson; use parking_lot::Mutex; use std::{cmp, ops::Range, sync::Arc}; -#[derive(Clone)] +#[derive(Clone, Default)] pub struct UniformListState(Arc>); impl UniformListState { - pub fn new() -> Self { - Self(Arc::new(Mutex::new(StateInner { - scroll_top: 0.0, - scroll_to: None, - }))) - } - pub fn scroll_to(&self, item_ix: usize) { self.0.lock().scroll_to = Some(item_ix); } @@ -33,6 +26,7 @@ impl UniformListState { } } +#[derive(Default)] struct StateInner { scroll_top: f32, scroll_to: Option, @@ -57,11 +51,11 @@ impl UniformList where F: Fn(Range, &mut Vec, &AppContext), { - pub fn new(state: UniformListState, item_count: usize, build_items: F) -> Self { + pub fn new(state: UniformListState, item_count: usize, append_items: F) -> Self { Self { state, item_count, - append_items: build_items, + append_items, } } @@ -79,7 +73,7 @@ where let mut state = self.state.0.lock(); state.scroll_top = (state.scroll_top - delta.y()).max(0.0).min(scroll_max); - cx.dispatch_action("uniform_list:scroll", state.scroll_top); + cx.notify(); true } diff --git a/server/src/tests.rs b/server/src/tests.rs index 8767155ca0b220f9139c41896fd036f525c26ae9..66d904746772c5c4d0813ac85ab33b108c074207 100644 --- a/server/src/tests.rs +++ b/server/src/tests.rs @@ -607,7 +607,7 @@ impl gpui::View for EmptyView { "empty view" } - fn render<'a>(&self, _: &gpui::AppContext) -> gpui::ElementBox { + fn render<'a>(&self, _: &gpui::RenderContext) -> gpui::ElementBox { gpui::Element::boxed(gpui::elements::Empty) } } diff --git a/zed/assets/themes/base.toml b/zed/assets/themes/_base.toml similarity index 100% rename from zed/assets/themes/base.toml rename to zed/assets/themes/_base.toml diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 2376293c8afb6126671d70d16263a9e9c96d5d0a..fdf5a5adeecfd7a0988cb2036a67c0c74239cd2e 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,4 +1,4 @@ -extends = "base" +extends = "_base" [variables] elevation_1 = 0x050101 diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml new file mode 100644 index 0000000000000000000000000000000000000000..77757ac26089b0b87603e62b3e05d2042d0ebc10 --- /dev/null +++ b/zed/assets/themes/light.toml @@ -0,0 +1,21 @@ +extends = "_base" + +[variables] +elevation_1 = 0xffffff +elevation_2 = 0xf3f3f3 +elevation_3 = 0xececec +elevation_4 = 0x3a3b3c +text_dull = 0xacacac +text_bright = 0x111111 +text_normal = 0x333333 + +[syntax] +keyword = 0x0000fa +function = 0x795e26 +string = 0xa82121 +type = 0x267f29 +number = 0xb5cea8 +comment = 0x6a9955 +property = 0x4e94ce +variant = 0x4fc1ff +constant = 0x9cdcfe diff --git a/zed/src/assets.rs b/zed/src/assets.rs index 072a1a049620ab42152fd4b1b413a9d761f65815..e7c0103421620b6888ee2dd28b723ed22d4daa11 100644 --- a/zed/src/assets.rs +++ b/zed/src/assets.rs @@ -10,4 +10,8 @@ impl AssetSource for Assets { fn load(&self, path: &str) -> Result> { Self::get(path).ok_or_else(|| anyhow!("could not find asset at path \"{}\"", path)) } + + fn list(&self, path: &str) -> Vec> { + Self::iter().filter(|p| p.starts_with(path)).collect() + } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 3911970249c95bf3afaa646399b242aec9a4801b..705afaeb20ab272987accad3a9e041feef821d4e 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -18,8 +18,8 @@ pub use element::*; use gpui::{ color::ColorU, font_cache::FamilyId, fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, - ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, Task, TextLayoutCache, View, - ViewContext, WeakViewHandle, + ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, + TextLayoutCache, View, ViewContext, WeakViewHandle, }; use postage::watch; use serde::{Deserialize, Serialize}; @@ -2533,7 +2533,7 @@ impl Entity for Editor { } impl View for Editor { - fn render<'a>(&self, _: &AppContext) -> ElementBox { + fn render<'a>(&self, _: &RenderContext) -> ElementBox { EditorElement::new(self.handle.clone()).boxed() } diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index d7f0f8fff10c5718ac028eaa6a08e5ff5058d502..794b40facee69043e49dfcae8393909809a1b041 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -11,8 +11,8 @@ use gpui::{ fonts::{Properties, Weight}, geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Entity, MutableAppContext, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + AppContext, Axis, Border, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, + ViewHandle, WeakViewHandle, }; use postage::watch; use std::{ @@ -45,7 +45,6 @@ pub fn init(cx: &mut MutableAppContext) { cx.add_action("file_finder:select", FileFinder::select); cx.add_action("menu:select_prev", FileFinder::select_prev); cx.add_action("menu:select_next", FileFinder::select_next); - cx.add_action("uniform_list:scroll", FileFinder::scroll); cx.add_bindings(vec![ Binding::new("cmd-p", "file_finder:toggle", None), @@ -68,7 +67,7 @@ impl View for FileFinder { "FileFinder" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Align::new( @@ -267,31 +266,30 @@ impl FileFinder { }) } - fn toggle(workspace_view: &mut Workspace, _: &(), cx: &mut ViewContext) { - workspace_view.toggle_modal(cx, |cx, workspace_view| { - let workspace = cx.handle(); - let finder = - cx.add_view(|cx| Self::new(workspace_view.settings.clone(), workspace, cx)); + fn toggle(workspace: &mut Workspace, _: &(), cx: &mut ViewContext) { + workspace.toggle_modal(cx, |cx, workspace| { + let handle = cx.handle(); + let finder = cx.add_view(|cx| Self::new(workspace.settings.clone(), handle, cx)); cx.subscribe_to_view(&finder, Self::on_event); finder }); } fn on_event( - workspace_view: &mut Workspace, + workspace: &mut Workspace, _: ViewHandle, event: &Event, cx: &mut ViewContext, ) { match event { Event::Selected(tree_id, path) => { - workspace_view + workspace .open_entry((*tree_id, path.clone()), cx) .map(|d| d.detach()); - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } Event::Dismissed => { - workspace_view.dismiss_modal(cx); + workspace.dismiss_modal(cx); } } } @@ -318,7 +316,7 @@ impl FileFinder { matches: Vec::new(), selected: None, cancel_flag: Arc::new(AtomicBool::new(false)), - list_state: UniformListState::new(), + list_state: Default::default(), } } @@ -388,10 +386,6 @@ impl FileFinder { cx.notify(); } - fn scroll(&mut self, _: &f32, cx: &mut ViewContext) { - cx.notify(); - } - fn confirm(&mut self, _: &(), cx: &mut ViewContext) { if let Some(m) = self.matches.get(self.selected_index()) { cx.emit(Event::Selected(m.tree_id, m.path.clone())); @@ -426,7 +420,7 @@ impl FileFinder { false, false, 100, - cancel_flag.clone(), + cancel_flag.as_ref(), background, ) .await; diff --git a/zed/src/lib.rs b/zed/src/lib.rs index b8cfc02c087161ff6965a07604f2d97a2600b396..2ae8ad0aeda896e195c84717ae066564540328f9 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -1,5 +1,3 @@ -use zrpc::ForegroundRouter; - pub mod assets; pub mod editor; pub mod file_finder; @@ -12,6 +10,7 @@ pub mod settings; mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod theme_picker; mod time; mod util; pub mod workspace; @@ -19,13 +18,19 @@ pub mod worktree; pub use settings::Settings; +use futures::lock::Mutex; +use postage::watch; +use std::sync::Arc; +use zrpc::ForegroundRouter; + pub struct AppState { - pub settings: postage::watch::Receiver, - pub languages: std::sync::Arc, - pub themes: std::sync::Arc, - pub rpc_router: std::sync::Arc, + pub settings_tx: Arc>>, + pub settings: watch::Receiver, + pub languages: Arc, + pub themes: Arc, + pub rpc_router: Arc, pub rpc: rpc::Client, - pub fs: std::sync::Arc, + pub fs: Arc, } pub fn init(cx: &mut gpui::MutableAppContext) { diff --git a/zed/src/main.rs b/zed/src/main.rs index 9cbf082b8c53102bd9f0d558806b6b6ee1f776ac..94e3cb066b49c7c668977f42a2b7deac6fbaefec 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -2,13 +2,14 @@ #![allow(non_snake_case)] use fs::OpenOptions; +use futures::lock::Mutex; use log::LevelFilter; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, + language, menus, rpc, settings, theme_picker, workspace::{self, OpenParams}, worktree::{self}, AppState, @@ -21,12 +22,14 @@ fn main() { let app = gpui::App::new(assets::Assets).unwrap(); let themes = settings::ThemeRegistry::new(assets::Assets); - let (_, settings) = settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); + let (settings_tx, settings) = + settings::channel_with_themes(&app.font_cache(), &themes).unwrap(); let languages = Arc::new(language::LanguageRegistry::new()); languages.set_theme(&settings.borrow().theme); let mut app_state = AppState { languages: languages.clone(), + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, rpc_router: Arc::new(ForegroundRouter::new()), @@ -40,12 +43,14 @@ fn main() { &app_state.rpc, Arc::get_mut(&mut app_state.rpc_router).unwrap(), ); + let app_state = Arc::new(app_state); + zed::init(cx); workspace::init(cx); editor::init(cx); file_finder::init(cx); + theme_picker::init(cx, &app_state); - let app_state = Arc::new(app_state); cx.set_menus(menus::menus(&app_state.clone())); if stdout_is_a_pty() { diff --git a/zed/src/settings.rs b/zed/src/settings.rs index cc90a5a44366bc85b521e83024385cc178db7f28..79ee243ae08c2f8cb86e5719ebebc24bbcac5d13 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -133,6 +133,18 @@ impl ThemeRegistry { }) } + pub fn list(&self) -> impl Iterator { + self.assets.list("themes/").into_iter().filter_map(|path| { + let filename = path.strip_prefix("themes/")?; + let theme_name = filename.strip_suffix(".toml")?; + if theme_name.starts_with('_') { + None + } else { + Some(theme_name.to_string()) + } + }) + } + pub fn get(&self, name: &str) -> Result> { if let Some(theme) = self.themes.lock().get(name) { return Ok(theme.clone()); @@ -497,8 +509,10 @@ mod tests { fn test_parse_extended_theme() { let assets = TestAssets(&[ ( - "themes/base.toml", + "themes/_base.toml", r#" + abstract = true + [ui] tab_background = 0x111111 tab_text = "$variable_1" @@ -511,7 +525,7 @@ mod tests { ( "themes/light.toml", r#" - extends = "base" + extends = "_base" [variables] variable_1 = 0x333333 @@ -524,6 +538,16 @@ mod tests { background = 0x666666 "#, ), + ( + "themes/dark.toml", + r#" + extends = "_base" + + [variables] + variable_1 = 0x555555 + variable_2 = 0x666666 + "#, + ), ]); let registry = ThemeRegistry::new(assets); @@ -533,6 +557,11 @@ mod tests { 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)); + + assert_eq!( + registry.list().collect::>(), + &["light".to_string(), "dark".to_string()] + ); } #[test] @@ -585,5 +614,19 @@ mod tests { Err(anyhow!("no such path {}", path)) } } + + fn list(&self, prefix: &str) -> Vec> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } } } diff --git a/zed/src/test.rs b/zed/src/test.rs index f1367775f2cc428d28bb34adf93a3b4fdd867d91..7d7d6e3ed6ab1b2898a8d6bc1afb79d2dcbcde47 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -6,6 +6,7 @@ use crate::{ time::ReplicaId, AppState, }; +use futures::lock::Mutex; use gpui::{AppContext, Entity, ModelHandle}; use smol::channel; use std::{ @@ -154,10 +155,11 @@ fn write_tree(path: &Path, tree: serde_json::Value) { } pub fn build_app_state(cx: &AppContext) -> Arc { - let settings = settings::channel(&cx.font_cache()).unwrap().1; + let (settings_tx, settings) = settings::channel(&cx.font_cache()).unwrap(); let languages = Arc::new(LanguageRegistry::new()); let themes = ThemeRegistry::new(()); Arc::new(AppState { + settings_tx: Arc::new(Mutex::new(settings_tx)), settings, themes, languages: languages.clone(), diff --git a/zed/src/theme_picker.rs b/zed/src/theme_picker.rs new file mode 100644 index 0000000000000000000000000000000000000000..3aacf69ebf1854210f61f3f0514271167c84f09c --- /dev/null +++ b/zed/src/theme_picker.rs @@ -0,0 +1,308 @@ +use std::{cmp, sync::Arc}; + +use crate::{ + editor::{self, Editor}, + settings::ThemeRegistry, + workspace::Workspace, + worktree::fuzzy::{match_strings, StringMatch, StringMatchCandidate}, + AppState, Settings, +}; +use futures::lock::Mutex; +use gpui::{ + color::ColorF, + elements::{ + Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, + UniformList, UniformListState, + }, + fonts::{Properties, Weight}, + geometry::vector::vec2f, + keymap::{self, Binding}, + AppContext, Axis, Border, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, + ViewContext, ViewHandle, +}; +use postage::watch; + +pub struct ThemePicker { + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + matches: Vec, + query_buffer: ViewHandle, + list_state: UniformListState, + selected_index: usize, +} + +pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { + cx.add_action("theme_picker:confirm", ThemePicker::confirm); + // cx.add_action("file_finder:select", ThemePicker::select); + cx.add_action("menu:select_prev", ThemePicker::select_prev); + cx.add_action("menu:select_next", ThemePicker::select_next); + cx.add_action("theme_picker:toggle", ThemePicker::toggle); + + cx.add_bindings(vec![ + Binding::new("cmd-k cmd-t", "theme_picker:toggle", None).with_arg(app_state.clone()), + Binding::new("escape", "theme_picker:toggle", Some("ThemePicker")) + .with_arg(app_state.clone()), + Binding::new("enter", "theme_picker:confirm", Some("ThemePicker")), + ]); +} + +pub enum Event { + Dismissed, +} + +impl ThemePicker { + fn new( + settings_tx: Arc>>, + settings: watch::Receiver, + registry: Arc, + cx: &mut ViewContext, + ) -> Self { + let query_buffer = cx.add_view(|cx| Editor::single_line(settings.clone(), cx)); + cx.subscribe_to_view(&query_buffer, Self::on_query_editor_event); + + let mut this = Self { + settings, + settings_tx, + registry, + query_buffer, + matches: Vec::new(), + list_state: Default::default(), + selected_index: 0, + }; + this.update_matches(cx); + this + } + + fn toggle( + workspace: &mut Workspace, + app_state: &Arc, + cx: &mut ViewContext, + ) { + workspace.toggle_modal(cx, |cx, _| { + let picker = cx.add_view(|cx| { + Self::new( + app_state.settings_tx.clone(), + app_state.settings.clone(), + app_state.themes.clone(), + cx, + ) + }); + cx.subscribe_to_view(&picker, Self::on_event); + picker + }); + } + + fn confirm(&mut self, _: &(), cx: &mut ViewContext) { + if let Some(mat) = self.matches.get(self.selected_index) { + let settings_tx = self.settings_tx.clone(); + if let Ok(theme) = self.registry.get(&mat.string) { + cx.foreground() + .spawn(async move { + settings_tx.lock().await.borrow_mut().theme = theme; + }) + .detach(); + } + } + cx.emit(Event::Dismissed); + } + + fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index > 0 { + self.selected_index -= 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + fn select_next(&mut self, _: &(), cx: &mut ViewContext) { + if self.selected_index + 1 < self.matches.len() { + self.selected_index += 1; + } + self.list_state.scroll_to(self.selected_index); + cx.notify(); + } + + // fn select(&mut self, selected_index: &usize, cx: &mut ViewContext) { + // self.selected_index = *selected_index; + // self.confirm(&(), cx); + // } + + fn update_matches(&mut self, cx: &mut ViewContext) { + let background = cx.background().clone(); + let candidates = self + .registry + .list() + .map(|name| StringMatchCandidate { + char_bag: name.as_str().into(), + string: name, + }) + .collect::>(); + let query = self.query_buffer.update(cx, |buffer, cx| buffer.text(cx)); + + self.matches = if query.is_empty() { + candidates + .into_iter() + .map(|candidate| StringMatch { + string: candidate.string, + positions: Vec::new(), + score: 0.0, + }) + .collect() + } else { + smol::block_on(match_strings( + &candidates, + &query, + false, + 100, + &Default::default(), + background, + )) + }; + } + + fn on_event( + workspace: &mut Workspace, + _: ViewHandle, + event: &Event, + cx: &mut ViewContext, + ) { + match event { + Event::Dismissed => { + workspace.dismiss_modal(cx); + } + } + } + + fn on_query_editor_event( + &mut self, + _: ViewHandle, + event: &editor::Event, + cx: &mut ViewContext, + ) { + match event { + editor::Event::Edited => self.update_matches(cx), + editor::Event::Blurred => cx.emit(Event::Dismissed), + _ => {} + } + } + + fn render_matches(&self, cx: &RenderContext) -> ElementBox { + if self.matches.is_empty() { + let settings = self.settings.borrow(); + return Container::new( + Label::new( + "No matches".into(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_default_color(settings.theme.editor.default_text.0) + .boxed(), + ) + .with_margin_top(6.0) + .named("empty matches"); + } + + let handle = cx.handle(); + let list = UniformList::new( + self.list_state.clone(), + self.matches.len(), + move |mut range, items, cx| { + let cx = cx.as_ref(); + let picker = handle.upgrade(cx).unwrap(); + let picker = picker.read(cx); + let start = range.start; + range.end = cmp::min(range.end, picker.matches.len()); + items.extend( + picker.matches[range] + .iter() + .enumerate() + .map(move |(i, path_match)| picker.render_match(path_match, start + i)), + ); + }, + ); + + Container::new(list.boxed()) + .with_margin_top(6.0) + .named("matches") + } + + fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox { + let settings = self.settings.borrow(); + let theme = &settings.theme.ui; + let bold = *Properties::new().weight(Weight::BOLD); + + let mut container = Container::new( + Label::new( + theme_match.string.clone(), + settings.ui_font_family, + settings.ui_font_size, + ) + .with_default_color(theme.modal_match_text.0) + .with_highlights( + theme.modal_match_text_highlight.0, + bold, + theme_match.positions.clone(), + ) + .boxed(), + ) + .with_uniform_padding(6.0) + .with_background_color(if index == self.selected_index { + theme.modal_match_background_active.0 + } else { + theme.modal_match_background.0 + }); + + if index == self.selected_index || index < self.matches.len() - 1 { + container = container.with_border(Border::bottom(1.0, theme.modal_match_border)); + } + + container.boxed() + } +} + +impl Entity for ThemePicker { + type Event = Event; +} + +impl View for ThemePicker { + fn ui_name() -> &'static str { + "ThemePicker" + } + + fn render(&self, cx: &RenderContext) -> ElementBox { + let settings = self.settings.borrow(); + + Align::new( + ConstrainedBox::new( + Container::new( + Flex::new(Axis::Vertical) + .with_child(ChildView::new(self.query_buffer.id()).boxed()) + .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) + .boxed(), + ) + .with_margin_top(12.0) + .with_uniform_padding(6.0) + .with_corner_radius(6.0) + .with_background_color(settings.theme.ui.modal_background) + .with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8()) + .boxed(), + ) + .with_max_width(600.0) + .with_max_height(400.0) + .boxed(), + ) + .top() + .named("theme picker") + } + + fn on_focus(&mut self, cx: &mut ViewContext) { + cx.focus(&self.query_buffer); + } + + fn keymap_context(&self, _: &AppContext) -> keymap::Context { + let mut cx = Self::default_keymap_context(); + cx.set.insert("menu".into()); + cx + } +} diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index d0d774a817ebc1a8927771efb7038215d73aa031..1006d5bc6169db8934816af5dfaee3e560dabb14 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -13,8 +13,8 @@ use crate::{ use anyhow::{anyhow, Result}; use gpui::{ elements::*, json::to_string_pretty, keymap::Binding, AnyViewHandle, AppContext, ClipboardItem, - Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, Task, View, - ViewContext, ViewHandle, WeakModelHandle, + Entity, ModelHandle, MutableAppContext, PathPromptOptions, PromptLevel, RenderContext, Task, + View, ViewContext, ViewHandle, WeakModelHandle, }; use log::error; pub use pane::*; @@ -879,7 +879,7 @@ impl View for Workspace { "Workspace" } - fn render(&self, _: &AppContext) -> ElementBox { + fn render(&self, _: &RenderContext) -> ElementBox { let settings = self.settings.borrow(); Container::new( Stack::new() @@ -974,8 +974,8 @@ mod tests { }) .await; assert_eq!(cx.window_ids().len(), 1); - let workspace_view_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); - workspace_view_1.read_with(&cx, |workspace, _| { + let workspace_1 = cx.root_view::(cx.window_ids()[0]).unwrap(); + workspace_1.read_with(&cx, |workspace, _| { assert_eq!(workspace.worktrees().len(), 2) }); @@ -1380,9 +1380,9 @@ mod tests { assert_eq!(pane2_item.entry_id(cx.as_ref()), Some(file1.clone())); cx.dispatch_action(window_id, vec![pane_2.id()], "pane:close_active_item", ()); - let workspace_view = workspace.read(cx); - assert_eq!(workspace_view.panes.len(), 1); - assert_eq!(workspace_view.active_pane(), &pane_1); + let workspace = workspace.read(cx); + assert_eq!(workspace.panes.len(), 1); + assert_eq!(workspace.active_pane(), &pane_1); }); } } diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 2b152a6996ac5b4e8ccfa323a518f638083fa6ba..9102e9f8136812432d10a29d1587b1640c462e68 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -5,7 +5,8 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, - AppContext, Border, Entity, MutableAppContext, Quad, View, ViewContext, ViewHandle, + AppContext, Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, + ViewHandle, }; use postage::watch; use std::{cmp, path::Path, sync::Arc}; @@ -371,7 +372,7 @@ impl View for Pane { "Pane" } - fn render<'a>(&self, cx: &AppContext) -> ElementBox { + fn render<'a>(&self, cx: &RenderContext) -> ElementBox { if let Some(active_item) = self.active_item() { Flex::column() .with_child(self.render_tabs(cx)) diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 041a94770db565389d123fe64eb7bde03c9f83f8..83ae844dbaca4f866dd4c6a88f06f302e44f19b6 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,5 +1,5 @@ mod char_bag; -mod fuzzy; +pub(crate) mod fuzzy; mod ignore; use self::{char_bag::CharBag, ignore::IgnoreStack}; @@ -2615,6 +2615,7 @@ mod tests { tree.snapshot() }); + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2624,7 +2625,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) @@ -2667,6 +2668,7 @@ mod tests { assert_eq!(tree.file_count(), 0); tree.snapshot() }); + let cancel_flag = Default::default(); let results = cx .read(|cx| { match_paths( @@ -2676,7 +2678,7 @@ mod tests { false, false, 10, - Default::default(), + &cancel_flag, cx.background().clone(), ) }) diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/worktree/fuzzy.rs index 0fb406fc98326a01ab353a4812ad3023fe4ebe57..a0f2c57edcaeb45b9520d67a75a08c414be4cd1c 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/worktree/fuzzy.rs @@ -2,6 +2,7 @@ use super::{char_bag::CharBag, EntryKind, Snapshot}; use crate::util; use gpui::executor; use std::{ + borrow::Cow, cmp::{max, min, Ordering}, path::Path, sync::atomic::{self, AtomicBool}, @@ -12,8 +13,31 @@ const BASE_DISTANCE_PENALTY: f64 = 0.6; const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; +struct Matcher<'a> { + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + min_score: f64, + match_positions: Vec, + last_positions: Vec, + score_matrix: Vec>, + best_position_matrix: Vec, +} + +trait Match: Ord { + fn score(&self) -> f64; + fn set_positions(&mut self, positions: Vec); +} + +trait MatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool; + fn to_string<'a>(&'a self) -> Cow<'a, str>; +} + #[derive(Clone, Debug)] -pub struct MatchCandidate<'a> { +pub struct PathMatchCandidate<'a> { pub path: &'a Arc, pub char_bag: CharBag, } @@ -27,6 +51,82 @@ pub struct PathMatch { pub include_root_name: bool, } +#[derive(Clone, Debug)] +pub struct StringMatchCandidate { + pub string: String, + pub char_bag: CharBag, +} + +impl Match for PathMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl Match for StringMatch { + fn score(&self) -> f64 { + self.score + } + + fn set_positions(&mut self, positions: Vec) { + self.positions = positions; + } +} + +impl<'a> MatchCandidate for PathMatchCandidate<'a> { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.path.to_string_lossy() + } +} + +impl<'a> MatchCandidate for &'a StringMatchCandidate { + fn has_chars(&self, bag: CharBag) -> bool { + self.char_bag.is_superset(bag) + } + + fn to_string(&self) -> Cow<'a, str> { + self.string.as_str().into() + } +} + +#[derive(Clone, Debug)] +pub struct StringMatch { + pub score: f64, + pub positions: Vec, + pub string: String, +} + +impl PartialEq for StringMatch { + fn eq(&self, other: &Self) -> bool { + self.score.eq(&other.score) + } +} + +impl Eq for StringMatch {} + +impl PartialOrd for StringMatch { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for StringMatch { + fn cmp(&self, other: &Self) -> Ordering { + self.score + .partial_cmp(&other.score) + .unwrap_or(Ordering::Equal) + .then_with(|| self.string.cmp(&other.string)) + } +} + impl PartialEq for PathMatch { fn eq(&self, other: &Self) -> bool { self.score.eq(&other.score) @@ -51,6 +151,62 @@ impl Ord for PathMatch { } } +pub async fn match_strings( + candidates: &[StringMatchCandidate], + query: &str, + smart_case: bool, + max_results: usize, + cancel_flag: &AtomicBool, + background: Arc, +) -> Vec { + let lowercase_query = query.to_lowercase().chars().collect::>(); + let query = query.chars().collect::>(); + + let lowercase_query = &lowercase_query; + let query = &query; + let query_char_bag = CharBag::from(&lowercase_query[..]); + + let num_cpus = background.num_cpus().min(candidates.len()); + let segment_size = (candidates.len() + num_cpus - 1) / num_cpus; + let mut segment_results = (0..num_cpus) + .map(|_| Vec::with_capacity(max_results)) + .collect::>(); + + background + .scoped(|scope| { + for (segment_idx, results) in segment_results.iter_mut().enumerate() { + let cancel_flag = &cancel_flag; + scope.spawn(async move { + let segment_start = segment_idx * segment_size; + let segment_end = segment_start + segment_size; + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); + matcher.match_strings( + &candidates[segment_start..segment_end], + results, + cancel_flag, + ); + }); + } + }) + .await; + + let mut results = Vec::new(); + for segment_result in segment_results { + if results.is_empty() { + results = segment_result; + } else { + util::extend_sorted(&mut results, segment_result, max_results, |a, b| b.cmp(&a)); + } + } + results +} + pub async fn match_paths<'a, T>( snapshots: T, query: &str, @@ -58,7 +214,7 @@ pub async fn match_paths<'a, T>( include_ignored: bool, smart_case: bool, max_results: usize, - cancel_flag: Arc, + cancel_flag: &AtomicBool, background: Arc, ) -> Vec where @@ -78,7 +234,7 @@ where let lowercase_query = &lowercase_query; let query = &query; - let query_chars = CharBag::from(&lowercase_query[..]); + let query_char_bag = CharBag::from(&lowercase_query[..]); let num_cpus = background.num_cpus().min(path_count); let segment_size = (path_count + num_cpus - 1) / num_cpus; @@ -90,18 +246,16 @@ where .scoped(|scope| { for (segment_idx, results) in segment_results.iter_mut().enumerate() { let snapshots = snapshots.clone(); - let cancel_flag = &cancel_flag; scope.spawn(async move { let segment_start = segment_idx * segment_size; let segment_end = segment_start + segment_size; - - let mut min_score = 0.0; - let mut last_positions = Vec::new(); - last_positions.resize(query.len(), 0); - let mut match_positions = Vec::new(); - match_positions.resize(query.len(), 0); - let mut score_matrix = Vec::new(); - let mut best_position_matrix = Vec::new(); + let mut matcher = Matcher::new( + query, + lowercase_query, + query_char_bag, + smart_case, + max_results, + ); let mut tree_start = 0; for snapshot in snapshots { @@ -123,7 +277,7 @@ where }; let paths = entries.map(|entry| { if let EntryKind::File(char_bag) = entry.kind { - MatchCandidate { + PathMatchCandidate { path: &entry.path, char_bag, } @@ -132,21 +286,11 @@ where } }); - match_single_tree_paths( + matcher.match_paths( snapshot, include_root_name, paths, - query, - lowercase_query, - query_chars, - smart_case, results, - max_results, - &mut min_score, - &mut match_positions, - &mut last_positions, - &mut score_matrix, - &mut best_position_matrix, &cancel_flag, ); } @@ -171,322 +315,335 @@ where results } -fn match_single_tree_paths<'a>( - snapshot: &Snapshot, - include_root_name: bool, - path_entries: impl Iterator>, - query: &[char], - lowercase_query: &[char], - query_chars: CharBag, - smart_case: bool, - results: &mut Vec, - max_results: usize, - min_score: &mut f64, - match_positions: &mut Vec, - last_positions: &mut Vec, - score_matrix: &mut Vec>, - best_position_matrix: &mut Vec, - cancel_flag: &AtomicBool, -) { - let mut path_chars = Vec::new(); - let mut lowercase_path_chars = Vec::new(); +impl<'a> Matcher<'a> { + fn new( + query: &'a [char], + lowercase_query: &'a [char], + query_char_bag: CharBag, + smart_case: bool, + max_results: usize, + ) -> Self { + Self { + query, + lowercase_query, + query_char_bag, + min_score: 0.0, + last_positions: vec![0; query.len()], + match_positions: vec![0; query.len()], + score_matrix: Vec::new(), + best_position_matrix: Vec::new(), + smart_case, + max_results, + } + } - let prefix = if include_root_name { - snapshot.root_name() - } else { - "" + fn match_strings( + &mut self, + candidates: &[StringMatchCandidate], + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + self.match_internal( + &[], + &[], + candidates.iter(), + results, + cancel_flag, + |candidate, score| StringMatch { + score, + positions: Vec::new(), + string: candidate.string.to_string(), + }, + ) } - .chars() - .collect::>(); - let lowercase_prefix = prefix - .iter() - .map(|c| c.to_ascii_lowercase()) - .collect::>(); - for candidate in path_entries { - if !candidate.char_bag.is_superset(query_chars) { - continue; + fn match_paths( + &mut self, + snapshot: &Snapshot, + include_root_name: bool, + path_entries: impl Iterator>, + results: &mut Vec, + cancel_flag: &AtomicBool, + ) { + let tree_id = snapshot.id; + let prefix = if include_root_name { + snapshot.root_name() + } else { + "" } + .chars() + .collect::>(); + let lowercase_prefix = prefix + .iter() + .map(|c| c.to_ascii_lowercase()) + .collect::>(); + self.match_internal( + &prefix, + &lowercase_prefix, + path_entries, + results, + cancel_flag, + |candidate, score| PathMatch { + score, + tree_id, + positions: Vec::new(), + path: candidate.path.clone(), + include_root_name, + }, + ) + } - if cancel_flag.load(atomic::Ordering::Relaxed) { - break; - } + fn match_internal( + &mut self, + prefix: &[char], + lowercase_prefix: &[char], + candidates: impl Iterator, + results: &mut Vec, + cancel_flag: &AtomicBool, + build_match: F, + ) where + R: Match, + F: Fn(&C, f64) -> R, + { + let mut candidate_chars = Vec::new(); + let mut lowercase_candidate_chars = Vec::new(); + + for candidate in candidates { + if !candidate.has_chars(self.query_char_bag) { + continue; + } - path_chars.clear(); - lowercase_path_chars.clear(); - for c in candidate.path.to_string_lossy().chars() { - path_chars.push(c); - lowercase_path_chars.push(c.to_ascii_lowercase()); - } + if cancel_flag.load(atomic::Ordering::Relaxed) { + break; + } - if !find_last_positions( - last_positions, - &lowercase_prefix, - &lowercase_path_chars, - &lowercase_query[..], - ) { - continue; - } + candidate_chars.clear(); + lowercase_candidate_chars.clear(); + for c in candidate.to_string().chars() { + candidate_chars.push(c); + lowercase_candidate_chars.push(c.to_ascii_lowercase()); + } - let matrix_len = query.len() * (path_chars.len() + prefix.len()); - score_matrix.clear(); - score_matrix.resize(matrix_len, None); - best_position_matrix.clear(); - best_position_matrix.resize(matrix_len, 0); - - let score = score_match( - &query[..], - &lowercase_query[..], - &path_chars, - &lowercase_path_chars, - &prefix, - &lowercase_prefix, - smart_case, - &last_positions, - score_matrix, - best_position_matrix, - match_positions, - *min_score, - ); + if !self.find_last_positions(&lowercase_prefix, &lowercase_candidate_chars) { + continue; + } - if score > 0.0 { - let mat = PathMatch { - tree_id: snapshot.id, - path: candidate.path.clone(), - score, - positions: match_positions.clone(), - include_root_name, - }; - if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { - if results.len() < max_results { - results.insert(i, mat); - } else if i < results.len() { - results.pop(); - results.insert(i, mat); - } - if results.len() == max_results { - *min_score = results.last().unwrap().score; + let matrix_len = self.query.len() * (prefix.len() + candidate_chars.len()); + self.score_matrix.clear(); + self.score_matrix.resize(matrix_len, None); + self.best_position_matrix.clear(); + self.best_position_matrix.resize(matrix_len, 0); + + let score = self.score_match( + &candidate_chars, + &lowercase_candidate_chars, + &prefix, + &lowercase_prefix, + ); + + if score > 0.0 { + let mut mat = build_match(&candidate, score); + if let Err(i) = results.binary_search_by(|m| mat.cmp(&m)) { + if results.len() < self.max_results { + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } else if i < results.len() { + results.pop(); + mat.set_positions(self.match_positions.clone()); + results.insert(i, mat); + } + if results.len() == self.max_results { + self.min_score = results.last().unwrap().score(); + } } } } } -} -fn find_last_positions( - last_positions: &mut Vec, - prefix: &[char], - path: &[char], - query: &[char], -) -> bool { - let mut path = path.iter(); - let mut prefix_iter = prefix.iter(); - for (i, char) in query.iter().enumerate().rev() { - if let Some(j) = path.rposition(|c| c == char) { - last_positions[i] = j + prefix.len(); - } else if let Some(j) = prefix_iter.rposition(|c| c == char) { - last_positions[i] = j; - } else { - return false; + fn find_last_positions(&mut self, prefix: &[char], path: &[char]) -> bool { + let mut path = path.iter(); + let mut prefix_iter = prefix.iter(); + for (i, char) in self.query.iter().enumerate().rev() { + if let Some(j) = path.rposition(|c| c == char) { + self.last_positions[i] = j + prefix.len(); + } else if let Some(j) = prefix_iter.rposition(|c| c == char) { + self.last_positions[i] = j; + } else { + return false; + } } - } - true -} - -fn score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - match_positions: &mut [usize], - min_score: f64, -) -> f64 { - let score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - 0, - 0, - query.len() as f64, - ) * query.len() as f64; - - if score <= 0.0 { - return 0.0; + true } - let path_len = prefix.len() + path.len(); - let mut cur_start = 0; - let mut byte_ix = 0; - let mut char_ix = 0; - for i in 0..query.len() { - let match_char_ix = best_position_matrix[i * path_len + cur_start]; - while char_ix < match_char_ix { - let ch = prefix - .get(char_ix) - .or_else(|| path.get(char_ix - prefix.len())) - .unwrap(); - byte_ix += ch.len_utf8(); - char_ix += 1; + fn score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + ) -> f64 { + let score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + 0, + 0, + self.query.len() as f64, + ) * self.query.len() as f64; + + if score <= 0.0 { + return 0.0; } - cur_start = match_char_ix + 1; - match_positions[i] = byte_ix; - } - score -} + let path_len = prefix.len() + path.len(); + let mut cur_start = 0; + let mut byte_ix = 0; + let mut char_ix = 0; + for i in 0..self.query.len() { + let match_char_ix = self.best_position_matrix[i * path_len + cur_start]; + while char_ix < match_char_ix { + let ch = prefix + .get(char_ix) + .or_else(|| path.get(char_ix - prefix.len())) + .unwrap(); + byte_ix += ch.len_utf8(); + char_ix += 1; + } + cur_start = match_char_ix + 1; + self.match_positions[i] = byte_ix; + } -fn recursive_score_match( - query: &[char], - query_cased: &[char], - path: &[char], - path_cased: &[char], - prefix: &[char], - lowercase_prefix: &[char], - smart_case: bool, - last_positions: &[usize], - score_matrix: &mut [Option], - best_position_matrix: &mut [usize], - min_score: f64, - query_idx: usize, - path_idx: usize, - cur_score: f64, -) -> f64 { - if query_idx == query.len() { - return 1.0; + score } - let path_len = prefix.len() + path.len(); - - if let Some(memoized) = score_matrix[query_idx * path_len + path_idx] { - return memoized; - } + fn recursive_score_match( + &mut self, + path: &[char], + path_cased: &[char], + prefix: &[char], + lowercase_prefix: &[char], + query_idx: usize, + path_idx: usize, + cur_score: f64, + ) -> f64 { + if query_idx == self.query.len() { + return 1.0; + } - let mut score = 0.0; - let mut best_position = 0; + let path_len = prefix.len() + path.len(); - let query_char = query_cased[query_idx]; - let limit = last_positions[query_idx]; + if let Some(memoized) = self.score_matrix[query_idx * path_len + path_idx] { + return memoized; + } - let mut last_slash = 0; - for j in path_idx..=limit { - let path_char = if j < prefix.len() { - lowercase_prefix[j] - } else { - path_cased[j - prefix.len()] - }; - let is_path_sep = path_char == '/' || path_char == '\\'; + let mut score = 0.0; + let mut best_position = 0; - if query_idx == 0 && is_path_sep { - last_slash = j; - } + let query_char = self.lowercase_query[query_idx]; + let limit = self.last_positions[query_idx]; - if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { - let curr = if j < prefix.len() { - prefix[j] + let mut last_slash = 0; + for j in path_idx..=limit { + let path_char = if j < prefix.len() { + lowercase_prefix[j] } else { - path[j - prefix.len()] + path_cased[j - prefix.len()] }; + let is_path_sep = path_char == '/' || path_char == '\\'; + + if query_idx == 0 && is_path_sep { + last_slash = j; + } - let mut char_score = 1.0; - if j > path_idx { - let last = if j - 1 < prefix.len() { - prefix[j - 1] + if query_char == path_char || (is_path_sep && query_char == '_' || query_char == '\\') { + let curr = if j < prefix.len() { + prefix[j] } else { - path[j - 1 - prefix.len()] + path[j - prefix.len()] }; - if last == '/' { - char_score = 0.9; - } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { - char_score = 0.8; - } else if last.is_lowercase() && curr.is_uppercase() { - char_score = 0.8; - } else if last == '.' { - char_score = 0.7; - } else if query_idx == 0 { - char_score = BASE_DISTANCE_PENALTY; - } else { - char_score = MIN_DISTANCE_PENALTY.max( - BASE_DISTANCE_PENALTY - - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, - ); + let mut char_score = 1.0; + if j > path_idx { + let last = if j - 1 < prefix.len() { + prefix[j - 1] + } else { + path[j - 1 - prefix.len()] + }; + + if last == '/' { + char_score = 0.9; + } else if last == '-' || last == '_' || last == ' ' || last.is_numeric() { + char_score = 0.8; + } else if last.is_lowercase() && curr.is_uppercase() { + char_score = 0.8; + } else if last == '.' { + char_score = 0.7; + } else if query_idx == 0 { + char_score = BASE_DISTANCE_PENALTY; + } else { + char_score = MIN_DISTANCE_PENALTY.max( + BASE_DISTANCE_PENALTY + - (j - path_idx - 1) as f64 * ADDITIONAL_DISTANCE_PENALTY, + ); + } } - } - // Apply a severe penalty if the case doesn't match. - // This will make the exact matches have higher score than the case-insensitive and the - // path insensitive matches. - if (smart_case || curr == '/') && query[query_idx] != curr { - char_score *= 0.001; - } + // Apply a severe penalty if the case doesn't match. + // This will make the exact matches have higher score than the case-insensitive and the + // path insensitive matches. + if (self.smart_case || curr == '/') && self.query[query_idx] != curr { + char_score *= 0.001; + } - let mut multiplier = char_score; + let mut multiplier = char_score; - // Scale the score based on how deep within the path we found the match. - if query_idx == 0 { - multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; - } + // Scale the score based on how deep within the path we found the match. + if query_idx == 0 { + multiplier /= ((prefix.len() + path.len()) - last_slash) as f64; + } - let mut next_score = 1.0; - if min_score > 0.0 { - next_score = cur_score * multiplier; - // Scores only decrease. If we can't pass the previous best, bail - if next_score < min_score { - // Ensure that score is non-zero so we use it in the memo table. - if score == 0.0 { - score = 1e-18; + let mut next_score = 1.0; + if self.min_score > 0.0 { + next_score = cur_score * multiplier; + // Scores only decrease. If we can't pass the previous best, bail + if next_score < self.min_score { + // Ensure that score is non-zero so we use it in the memo table. + if score == 0.0 { + score = 1e-18; + } + continue; } - continue; } - } - let new_score = recursive_score_match( - query, - query_cased, - path, - path_cased, - prefix, - lowercase_prefix, - smart_case, - last_positions, - score_matrix, - best_position_matrix, - min_score, - query_idx + 1, - j + 1, - next_score, - ) * multiplier; - - if new_score > score { - score = new_score; - best_position = j; - // Optimization: can't score better than 1. - if new_score == 1.0 { - break; + let new_score = self.recursive_score_match( + path, + path_cased, + prefix, + lowercase_prefix, + query_idx + 1, + j + 1, + next_score, + ) * multiplier; + + if new_score > score { + score = new_score; + best_position = j; + // Optimization: can't score better than 1. + if new_score == 1.0 { + break; + } } } } - } - if best_position != 0 { - best_position_matrix[query_idx * path_len + path_idx] = best_position; - } + if best_position != 0 { + self.best_position_matrix[query_idx * path_len + path_idx] = best_position; + } - score_matrix[query_idx * path_len + path_idx] = Some(score); - score + self.score_matrix[query_idx * path_len + path_idx] = Some(score); + score + } } #[cfg(test)] @@ -496,34 +653,22 @@ mod tests { #[test] fn test_get_last_positions() { - let mut last_positions = vec![0; 2]; - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['d', 'c'], - ); + let mut query: &[char] = &['d', 'c']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); assert_eq!(result, false); - last_positions.resize(2, 0); - let result = find_last_positions( - &mut last_positions, - &['a', 'b', 'c'], - &['b', 'd', 'e', 'f'], - &['c', 'd'], - ); + query = &['c', 'd']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['a', 'b', 'c'], &['b', 'd', 'e', 'f']); assert_eq!(result, true); - assert_eq!(last_positions, vec![2, 4]); - - last_positions.resize(4, 0); - let result = find_last_positions( - &mut last_positions, - &['z', 'e', 'd', '/'], - &['z', 'e', 'd', '/', 'f'], - &['z', '/', 'z', 'f'], - ); + assert_eq!(matcher.last_positions, vec![2, 4]); + + query = &['z', '/', 'z', 'f']; + let mut matcher = Matcher::new(query, query, query.into(), false, 10); + let result = matcher.find_last_positions(&['z', 'e', 'd', '/'], &['z', 'e', 'd', '/', 'f']); assert_eq!(result, true); - assert_eq!(last_positions, vec![0, 3, 4, 8]); + assert_eq!(matcher.last_positions, vec![0, 3, 4, 8]); } #[test] @@ -604,20 +749,17 @@ mod tests { for (i, path) in paths.iter().enumerate() { let lowercase_path = path.to_lowercase().chars().collect::>(); let char_bag = CharBag::from(lowercase_path.as_slice()); - path_entries.push(MatchCandidate { + path_entries.push(PathMatchCandidate { char_bag, path: path_arcs.get(i).unwrap(), }); } - let mut match_positions = Vec::new(); - let mut last_positions = Vec::new(); - match_positions.resize(query.len(), 0); - last_positions.resize(query.len(), 0); + let mut matcher = Matcher::new(&query, &lowercase_query, query_chars, smart_case, 100); let cancel_flag = AtomicBool::new(false); let mut results = Vec::new(); - match_single_tree_paths( + matcher.match_paths( &Snapshot { id: 0, scan_id: 0, @@ -632,17 +774,7 @@ mod tests { }, false, path_entries.into_iter(), - &query[..], - &lowercase_query[..], - query_chars, - smart_case, &mut results, - 100, - &mut 0.0, - &mut match_positions, - &mut last_positions, - &mut Vec::new(), - &mut Vec::new(), &cancel_flag, ); From 4c534708007ff5c0174f12cac019fdd8eb8d9b90 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 2 Aug 2021 16:57:10 -0600 Subject: [PATCH 05/23] Notify all views when a theme is selected --- gpui/src/app.rs | 14 ++++++++++++++ zed/src/theme_picker.rs | 15 +++++++++------ 2 files changed, 23 insertions(+), 6 deletions(-) diff --git a/gpui/src/app.rs b/gpui/src/app.rs index 6d857952e2724a2997f16d499deadaf53bb5510a..579bf5c4682dba2fcfbe15d48166396f3a9ea81d 100644 --- a/gpui/src/app.rs +++ b/gpui/src/app.rs @@ -813,6 +813,16 @@ impl MutableAppContext { .push_back(Effect::ViewNotification { window_id, view_id }); } + pub(crate) fn notify_all_views(&mut self) { + let notifications = self + .views + .keys() + .copied() + .map(|(window_id, view_id)| Effect::ViewNotification { window_id, view_id }) + .collect::>(); + self.pending_effects.extend(notifications); + } + pub fn dispatch_action( &mut self, window_id: usize, @@ -2087,6 +2097,10 @@ impl<'a, T: View> ViewContext<'a, T> { self.app.notify_view(self.window_id, self.view_id); } + pub fn notify_all(&mut self) { + self.app.notify_all_views(); + } + pub fn propagate_action(&mut self) { self.halt_action_dispatch = false; } diff --git a/zed/src/theme_picker.rs b/zed/src/theme_picker.rs index 3aacf69ebf1854210f61f3f0514271167c84f09c..419ad1ad26a7776b315f552e8f631a09ba801327 100644 --- a/zed/src/theme_picker.rs +++ b/zed/src/theme_picker.rs @@ -95,16 +95,19 @@ impl ThemePicker { fn confirm(&mut self, _: &(), cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { - let settings_tx = self.settings_tx.clone(); if let Ok(theme) = self.registry.get(&mat.string) { - cx.foreground() - .spawn(async move { - settings_tx.lock().await.borrow_mut().theme = theme; + let settings_tx = self.settings_tx.clone(); + cx.spawn(|this, mut cx| async move { + let mut settings_tx = settings_tx.lock().await; + this.update(&mut cx, |_, cx| { + settings_tx.borrow_mut().theme = theme; + cx.notify_all(); + cx.emit(Event::Dismissed); }) - .detach(); + }) + .detach(); } } - cx.emit(Event::Dismissed); } fn select_prev(&mut self, _: &(), cx: &mut ViewContext) { From 200e278bc967ae4a09975f35b3812a2d22ef49d3 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Mon, 2 Aug 2021 20:07:48 -0600 Subject: [PATCH 06/23] =?UTF-8?q?=F0=9F=92=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- zed/src/lib.rs | 2 +- zed/src/main.rs | 4 +- .../{theme_picker.rs => theme_selector.rs} | 46 +++++++++---------- 3 files changed, 26 insertions(+), 26 deletions(-) rename zed/src/{theme_picker.rs => theme_selector.rs} (87%) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 2ae8ad0aeda896e195c84717ae066564540328f9..a02b00dc6aafd1b4b4275a14470f8fcd8b634451 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -10,7 +10,7 @@ pub mod settings; mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; -pub mod theme_picker; +pub mod theme_selector; mod time; mod util; pub mod workspace; diff --git a/zed/src/main.rs b/zed/src/main.rs index 94e3cb066b49c7c668977f42a2b7deac6fbaefec..84a249b2f70323ea9b417af60ae1b9a9be4b2d3f 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -9,7 +9,7 @@ use std::{fs, path::PathBuf, sync::Arc}; use zed::{ self, assets, editor, file_finder, fs::RealFs, - language, menus, rpc, settings, theme_picker, + language, menus, rpc, settings, theme_selector, workspace::{self, OpenParams}, worktree::{self}, AppState, @@ -49,7 +49,7 @@ fn main() { workspace::init(cx); editor::init(cx); file_finder::init(cx); - theme_picker::init(cx, &app_state); + theme_selector::init(cx, &app_state); cx.set_menus(menus::menus(&app_state.clone())); diff --git a/zed/src/theme_picker.rs b/zed/src/theme_selector.rs similarity index 87% rename from zed/src/theme_picker.rs rename to zed/src/theme_selector.rs index 419ad1ad26a7776b315f552e8f631a09ba801327..7b8269d6fdad968bcea4083824d28b24960c5524 100644 --- a/zed/src/theme_picker.rs +++ b/zed/src/theme_selector.rs @@ -22,7 +22,7 @@ use gpui::{ }; use postage::watch; -pub struct ThemePicker { +pub struct ThemeSelector { settings_tx: Arc>>, settings: watch::Receiver, registry: Arc, @@ -33,17 +33,17 @@ pub struct ThemePicker { } pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { - cx.add_action("theme_picker:confirm", ThemePicker::confirm); - // cx.add_action("file_finder:select", ThemePicker::select); - cx.add_action("menu:select_prev", ThemePicker::select_prev); - cx.add_action("menu:select_next", ThemePicker::select_next); - cx.add_action("theme_picker:toggle", ThemePicker::toggle); + cx.add_action("theme_selector:confirm", ThemeSelector::confirm); + // cx.add_action("file_finder:select", ThemeSelector::select); + cx.add_action("menu:select_prev", ThemeSelector::select_prev); + cx.add_action("menu:select_next", ThemeSelector::select_next); + cx.add_action("theme_selector:toggle", ThemeSelector::toggle); cx.add_bindings(vec![ - Binding::new("cmd-k cmd-t", "theme_picker:toggle", None).with_arg(app_state.clone()), - Binding::new("escape", "theme_picker:toggle", Some("ThemePicker")) + Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()), + Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector")) .with_arg(app_state.clone()), - Binding::new("enter", "theme_picker:confirm", Some("ThemePicker")), + Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")), ]); } @@ -51,7 +51,7 @@ pub enum Event { Dismissed, } -impl ThemePicker { +impl ThemeSelector { fn new( settings_tx: Arc>>, settings: watch::Receiver, @@ -80,7 +80,7 @@ impl ThemePicker { cx: &mut ViewContext, ) { workspace.toggle_modal(cx, |cx, _| { - let picker = cx.add_view(|cx| { + let selector = cx.add_view(|cx| { Self::new( app_state.settings_tx.clone(), app_state.settings.clone(), @@ -88,8 +88,8 @@ impl ThemePicker { cx, ) }); - cx.subscribe_to_view(&picker, Self::on_event); - picker + cx.subscribe_to_view(&selector, Self::on_event); + selector }); } @@ -166,7 +166,7 @@ impl ThemePicker { fn on_event( workspace: &mut Workspace, - _: ViewHandle, + _: ViewHandle, event: &Event, cx: &mut ViewContext, ) { @@ -212,15 +212,15 @@ impl ThemePicker { self.matches.len(), move |mut range, items, cx| { let cx = cx.as_ref(); - let picker = handle.upgrade(cx).unwrap(); - let picker = picker.read(cx); + let selector = handle.upgrade(cx).unwrap(); + let selector = selector.read(cx); let start = range.start; - range.end = cmp::min(range.end, picker.matches.len()); + range.end = cmp::min(range.end, selector.matches.len()); items.extend( - picker.matches[range] + selector.matches[range] .iter() .enumerate() - .map(move |(i, path_match)| picker.render_match(path_match, start + i)), + .map(move |(i, path_match)| selector.render_match(path_match, start + i)), ); }, ); @@ -264,13 +264,13 @@ impl ThemePicker { } } -impl Entity for ThemePicker { +impl Entity for ThemeSelector { type Event = Event; } -impl View for ThemePicker { +impl View for ThemeSelector { fn ui_name() -> &'static str { - "ThemePicker" + "ThemeSelector" } fn render(&self, cx: &RenderContext) -> ElementBox { @@ -296,7 +296,7 @@ impl View for ThemePicker { .boxed(), ) .top() - .named("theme picker") + .named("theme selector") } fn on_focus(&mut self, cx: &mut ViewContext) { From fa01273466248e70f353edbdccfb2ff3e1bf98e9 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Aug 2021 11:07:03 -0700 Subject: [PATCH 07/23] Extract {Container,Label}Style structs from those elements Co-Authored-By: Nathan Sobo --- gpui/src/elements/container.rs | 118 +++++++++++++++------------ gpui/src/elements/label.rs | 141 ++++++++++++++++----------------- zed/src/file_finder.rs | 22 +++-- zed/src/theme_selector.rs | 18 ++--- 4 files changed, 155 insertions(+), 144 deletions(-) diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index c0b829fbe6f881b0cf24e8c80d2bf92a16cabcb2..9ae084193143a48db09a2d21af9305bd869dae90 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -10,53 +10,58 @@ use crate::{ SizeConstraint, }; -pub struct Container { +#[derive(Clone, Debug, Default)] +pub struct ContainerStyle { margin: Margin, padding: Padding, background_color: Option, border: Border, corner_radius: f32, shadow: Option, +} + +pub struct Container { child: ElementBox, + style: ContainerStyle, } impl Container { pub fn new(child: ElementBox) -> Self { Self { - margin: Margin::default(), - padding: Padding::default(), - background_color: None, - border: Border::default(), - corner_radius: 0.0, - shadow: None, child, + style: Default::default(), } } + pub fn with_style(mut self, style: &ContainerStyle) -> Self { + self.style = style.clone(); + self + } + pub fn with_margin_top(mut self, margin: f32) -> Self { - self.margin.top = margin; + self.style.margin.top = margin; self } pub fn with_margin_left(mut self, margin: f32) -> Self { - self.margin.left = margin; + self.style.margin.left = margin; self } pub fn with_horizontal_padding(mut self, padding: f32) -> Self { - self.padding.left = padding; - self.padding.right = padding; + self.style.padding.left = padding; + self.style.padding.right = padding; self } pub fn with_vertical_padding(mut self, padding: f32) -> Self { - self.padding.top = padding; - self.padding.bottom = padding; + self.style.padding.top = padding; + self.style.padding.bottom = padding; self } pub fn with_uniform_padding(mut self, padding: f32) -> Self { - self.padding = Padding { + self.style.padding = Padding { top: padding, left: padding, bottom: padding, @@ -66,32 +71,32 @@ impl Container { } pub fn with_padding_right(mut self, padding: f32) -> Self { - self.padding.right = padding; + self.style.padding.right = padding; self } pub fn with_padding_bottom(mut self, padding: f32) -> Self { - self.padding.bottom = padding; + self.style.padding.bottom = padding; self } pub fn with_background_color(mut self, color: impl Into) -> Self { - self.background_color = Some(color.into()); + self.style.background_color = Some(color.into()); self } pub fn with_border(mut self, border: Border) -> Self { - self.border = border; + self.style.border = border; self } pub fn with_corner_radius(mut self, radius: f32) -> Self { - self.corner_radius = radius; + self.style.corner_radius = radius; self } pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: impl Into) -> Self { - self.shadow = Some(Shadow { + self.style.shadow = Some(Shadow { offset, blur, color: color.into(), @@ -101,33 +106,33 @@ impl Container { fn margin_size(&self) -> Vector2F { vec2f( - self.margin.left + self.margin.right, - self.margin.top + self.margin.bottom, + self.style.margin.left + self.style.margin.right, + self.style.margin.top + self.style.margin.bottom, ) } fn padding_size(&self) -> Vector2F { vec2f( - self.padding.left + self.padding.right, - self.padding.top + self.padding.bottom, + self.style.padding.left + self.style.padding.right, + self.style.padding.top + self.style.padding.bottom, ) } fn border_size(&self) -> Vector2F { let mut x = 0.0; - if self.border.left { - x += self.border.width; + if self.style.border.left { + x += self.style.border.width; } - if self.border.right { - x += self.border.width; + if self.style.border.right { + x += self.style.border.width; } let mut y = 0.0; - if self.border.top { - y += self.border.width; + if self.style.border.top { + y += self.style.border.width; } - if self.border.bottom { - y += self.border.width; + if self.style.border.bottom { + y += self.style.border.width; } vec2f(x, y) @@ -168,28 +173,31 @@ impl Element for Container { cx: &mut PaintContext, ) -> Self::PaintState { let quad_bounds = RectF::from_points( - bounds.origin() + vec2f(self.margin.left, self.margin.top), - bounds.lower_right() - vec2f(self.margin.right, self.margin.bottom), + bounds.origin() + vec2f(self.style.margin.left, self.style.margin.top), + bounds.lower_right() - vec2f(self.style.margin.right, self.style.margin.bottom), ); - if let Some(shadow) = self.shadow.as_ref() { + if let Some(shadow) = self.style.shadow.as_ref() { cx.scene.push_shadow(scene::Shadow { bounds: quad_bounds + shadow.offset, - corner_radius: self.corner_radius, + corner_radius: self.style.corner_radius, sigma: shadow.blur, color: shadow.color, }); } cx.scene.push_quad(Quad { bounds: quad_bounds, - background: self.background_color, - border: self.border, - corner_radius: self.corner_radius, + background: self.style.background_color, + border: self.style.border, + corner_radius: self.style.corner_radius, }); let child_origin = quad_bounds.origin() - + vec2f(self.padding.left, self.padding.top) - + vec2f(self.border.left_width(), self.border.top_width()); + + vec2f(self.style.padding.left, self.style.padding.top) + + vec2f( + self.style.border.left_width(), + self.style.border.top_width(), + ); self.child.paint(child_origin, cx); } @@ -214,20 +222,26 @@ impl Element for Container { json!({ "type": "Container", "bounds": bounds.to_json(), - "details": { - "margin": self.margin.to_json(), - "padding": self.padding.to_json(), - "background_color": self.background_color.to_json(), - "border": self.border.to_json(), - "corner_radius": self.corner_radius, - "shadow": self.shadow.to_json(), - }, + "details": self.style.to_json(), "child": self.child.debug(cx), }) } } -#[derive(Default)] +impl ToJson for ContainerStyle { + fn to_json(&self) -> serde_json::Value { + json!({ + "margin": self.margin.to_json(), + "padding": self.padding.to_json(), + "background_color": self.background_color.to_json(), + "border": self.border.to_json(), + "corner_radius": self.corner_radius, + "shadow": self.shadow.to_json(), + }) + } +} + +#[derive(Clone, Debug, Default)] pub struct Margin { top: f32, left: f32, @@ -254,7 +268,7 @@ impl ToJson for Margin { } } -#[derive(Default)] +#[derive(Clone, Debug, Default)] pub struct Padding { top: f32, left: f32, @@ -281,7 +295,7 @@ impl ToJson for Padding { } } -#[derive(Default)] +#[derive(Clone, Debug, Default)] pub struct Shadow { offset: Vector2F, blur: f32, diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index 6b6c3358f2742bd818841e34a81fd73b2eda6521..c64b1b8f00d22928d82512d51336aa040e5a84c6 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -18,16 +18,17 @@ use crate::{ pub struct Label { text: String, family_id: FamilyId, - font_properties: Properties, font_size: f32, - default_color: ColorU, - highlights: Option, + style: LabelStyle, + highlight_indices: Vec, } -pub struct Highlights { - color: ColorU, - indices: Vec, - font_properties: Properties, +#[derive(Clone, Debug, Default)] +pub struct LabelStyle { + pub default_color: ColorU, + pub highlight_color: ColorU, + pub font_properties: Properties, + pub highlight_font_properties: Properties, } impl Label { @@ -35,29 +36,24 @@ impl Label { Self { text, family_id, - font_properties: Properties::new(), font_size, - default_color: ColorU::black(), - highlights: None, + highlight_indices: Default::default(), + style: Default::default(), } } + pub fn with_style(mut self, style: &LabelStyle) -> Self { + self.style = style.clone(); + self + } + pub fn with_default_color(mut self, color: ColorU) -> Self { - self.default_color = color; + self.style.default_color = color; self } - pub fn with_highlights( - mut self, - color: ColorU, - font_properties: Properties, - indices: Vec, - ) -> Self { - self.highlights = Some(Highlights { - color, - font_properties, - indices, - }); + pub fn with_highlights(mut self, indices: Vec) -> Self { + self.highlight_indices = indices; self } @@ -66,46 +62,45 @@ impl Label { font_cache: &FontCache, font_id: FontId, ) -> SmallVec<[(usize, FontId, ColorU); 8]> { - if let Some(highlights) = self.highlights.as_ref() { - let highlight_font_id = font_cache - .select_font(self.family_id, &highlights.font_properties) - .unwrap_or(font_id); - - let mut highlight_indices = highlights.indices.iter().copied().peekable(); - let mut runs = SmallVec::new(); - - for (char_ix, c) in self.text.char_indices() { - let mut font_id = font_id; - let mut color = self.default_color; - if let Some(highlight_ix) = highlight_indices.peek() { - if char_ix == *highlight_ix { - font_id = highlight_font_id; - color = highlights.color; - highlight_indices.next(); - } - } + if self.highlight_indices.is_empty() { + return smallvec![(self.text.len(), font_id, self.style.default_color)]; + } - let push_new_run = - if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { - if font_id == *last_font_id && color == *last_color { - *last_len += c.len_utf8(); - false - } else { - true - } - } else { - true - }; - - if push_new_run { - runs.push((c.len_utf8(), font_id, color)); + let highlight_font_id = font_cache + .select_font(self.family_id, &self.style.highlight_font_properties) + .unwrap_or(font_id); + + let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); + let mut runs = SmallVec::new(); + + for (char_ix, c) in self.text.char_indices() { + let mut font_id = font_id; + let mut color = self.style.default_color; + if let Some(highlight_ix) = highlight_indices.peek() { + if char_ix == *highlight_ix { + font_id = highlight_font_id; + color = self.style.highlight_color; + highlight_indices.next(); } } - runs - } else { - smallvec![(self.text.len(), font_id, self.default_color)] + let push_new_run = if let Some((last_len, last_font_id, last_color)) = runs.last_mut() { + if font_id == *last_font_id && color == *last_color { + *last_len += c.len_utf8(); + false + } else { + true + } + } else { + true + }; + + if push_new_run { + runs.push((c.len_utf8(), font_id, color)); + } } + + runs } } @@ -120,7 +115,7 @@ impl Element for Label { ) -> (Vector2F, Self::LayoutState) { let font_id = cx .font_cache - .select_font(self.family_id, &self.font_properties) + .select_font(self.family_id, &self.style.font_properties) .unwrap(); let runs = self.compute_runs(&cx.font_cache, font_id); let line = @@ -172,21 +167,22 @@ impl Element for Label { json!({ "type": "Label", "bounds": bounds.to_json(), + "text": &self.text, + "highlight_indices": self.highlight_indices, "font_family": cx.font_cache.family_name(self.family_id).unwrap(), "font_size": self.font_size, - "font_properties": self.font_properties.to_json(), - "text": &self.text, - "highlights": self.highlights.to_json(), + "style": self.style.to_json(), }) } } -impl ToJson for Highlights { +impl ToJson for LabelStyle { fn to_json(&self) -> Value { json!({ - "color": self.color.to_json(), - "indices": self.indices, - "font_properties": self.font_properties.to_json(), + "default_color": self.default_color.to_json(), + "default_font_properties": self.font_properties.to_json(), + "highlight_color": self.highlight_color.to_json(), + "highlight_font_properties": self.highlight_font_properties.to_json(), }) } } @@ -211,17 +207,20 @@ mod tests { let black = ColorU::black(); let red = ColorU::new(255, 0, 0, 255); - let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0).with_highlights( - red, - *Properties::new().weight(Weight::BOLD), - vec![ + let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) + .with_style(&LabelStyle { + default_color: black, + highlight_color: red, + highlight_font_properties: *Properties::new().weight(Weight::BOLD), + ..Default::default() + }) + .with_highlights(vec![ ".α".len(), ".αβ".len(), ".αβγδ".len(), ".αβγδε.ⓐ".len(), ".αβγδε.ⓐⓑ".len(), - ], - ); + ]); let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular); assert_eq!( diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 794b40facee69043e49dfcae8393909809a1b041..faecae42f47462e71b6565be39567b92d9ea291f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -154,6 +154,12 @@ impl FileFinder { |(file_name, file_name_positions, full_path, full_path_positions)| { let bold = *Properties::new().weight(Weight::BOLD); let selected_index = self.selected_index(); + let label_style = LabelStyle { + default_color: theme.modal_match_text.0, + highlight_color: theme.modal_match_text_highlight.0, + highlight_font_properties: bold, + ..Default::default() + }; let mut container = Container::new( Flex::row() .with_child( @@ -178,12 +184,8 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(theme.modal_match_text.0) - .with_highlights( - theme.modal_match_text_highlight.0, - bold, - file_name_positions, - ) + .with_style(&label_style) + .with_highlights(file_name_positions) .boxed(), ) .with_child( @@ -192,12 +194,8 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(theme.modal_match_text.0) - .with_highlights( - theme.modal_match_text_highlight.0, - bold, - full_path_positions, - ) + .with_style(&label_style) + .with_highlights(full_path_positions) .boxed(), ) .boxed(), diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 7b8269d6fdad968bcea4083824d28b24960c5524..55a32d94c5426c9f574e9dacab0506a6b1d63471 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -11,8 +11,8 @@ use futures::lock::Mutex; use gpui::{ color::ColorF, elements::{ - Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, - UniformList, UniformListState, + Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, LabelStyle, + ParentElement, UniformList, UniformListState, }, fonts::{Properties, Weight}, geometry::vector::vec2f, @@ -233,7 +233,6 @@ impl ThemeSelector { fn render_match(&self, theme_match: &StringMatch, index: usize) -> ElementBox { let settings = self.settings.borrow(); let theme = &settings.theme.ui; - let bold = *Properties::new().weight(Weight::BOLD); let mut container = Container::new( Label::new( @@ -241,12 +240,13 @@ impl ThemeSelector { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(theme.modal_match_text.0) - .with_highlights( - theme.modal_match_text_highlight.0, - bold, - theme_match.positions.clone(), - ) + .with_style(&LabelStyle { + default_color: theme.modal_match_text.0, + highlight_color: theme.modal_match_text_highlight.0, + highlight_font_properties: *Properties::new().weight(Weight::BOLD), + ..Default::default() + }) + .with_highlights(theme_match.positions.clone()) .boxed(), ) .with_uniform_padding(6.0) From ef0ffbe8190ae7c3069bd1370e450c87870eb899 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Aug 2021 12:48:58 -0700 Subject: [PATCH 08/23] Use custom color wrapper type everywhere in gpui & zed Co-Authored-By: Nathan Sobo --- gpui/examples/text.rs | 14 ++-- gpui/src/color.rs | 75 ++++++++++++++++++++- gpui/src/elements/container.rs | 29 ++++---- gpui/src/elements/label.rs | 50 +++++++------- gpui/src/elements/svg.rs | 8 +-- gpui/src/fonts.rs | 71 ++++++++++++++++++-- gpui/src/geometry.rs | 13 +++- gpui/src/platform.rs | 4 +- gpui/src/platform/mac/fonts.rs | 12 ++-- gpui/src/platform/mac/renderer.rs | 18 ++--- gpui/src/scene.rs | 36 +++++----- gpui/src/text_layout.rs | 16 ++--- zed/src/editor.rs | 10 +-- zed/src/editor/display_map.rs | 10 +-- zed/src/editor/element.rs | 22 +++--- zed/src/file_finder.rs | 16 ++--- zed/src/settings.rs | 108 ++++++++---------------------- zed/src/theme_selector.rs | 16 ++--- zed/src/workspace/pane.rs | 14 ++-- zed/src/workspace/pane_group.rs | 10 +-- 20 files changed, 319 insertions(+), 233 deletions(-) diff --git a/gpui/examples/text.rs b/gpui/examples/text.rs index 58314b5e948ab7175394f78e32a8721e4fcd54c5..45aa5931129ed43f9e6bf76a1063392827063ed1 100644 --- a/gpui/examples/text.rs +++ b/gpui/examples/text.rs @@ -1,5 +1,5 @@ use gpui::{ - color::ColorU, + color::Color, fonts::{Properties, Weight}, DebugContext, Element as _, Quad, }; @@ -82,17 +82,17 @@ impl gpui::Element for TextElement { text, font_size, &[ - (1, normal, ColorU::default()), - (1, bold, ColorU::default()), - (1, normal, ColorU::default()), - (1, bold, ColorU::default()), - (text.len() - 4, normal, ColorU::default()), + (1, normal, Color::default()), + (1, bold, Color::default()), + (1, normal, Color::default()), + (1, bold, Color::default()), + (text.len() - 4, normal, Color::default()), ], ); cx.scene.push_quad(Quad { bounds: bounds, - background: Some(ColorU::white()), + background: Some(Color::white()), ..Default::default() }); line.paint(bounds.origin(), bounds, cx); diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 95b966493e08c0005132d8942436f45ffd6dbe73..818b07111d590e126c341f4d63a55f0b392860cd 100644 --- a/gpui/src/color.rs +++ b/gpui/src/color.rs @@ -1,9 +1,78 @@ +use std::{ + fmt, + ops::{Deref, DerefMut}, +}; + use crate::json::ToJson; -pub use pathfinder_color::*; +use pathfinder_color::ColorU; +use serde::{Deserialize, Deserializer}; use serde_json::json; -impl ToJson for ColorU { +#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] +#[repr(transparent)] +pub struct Color(ColorU); + +impl Color { + pub fn transparent_black() -> Self { + Self(ColorU::transparent_black()) + } + + pub fn black() -> Self { + Self(ColorU::black()) + } + + pub fn white() -> Self { + Self(ColorU::white()) + } + + pub fn new(r: u8, g: u8, b: u8, a: u8) -> Self { + Self(ColorU::new(r, g, b, a)) + } + + pub fn from_u32(rgba: u32) -> Self { + Self(ColorU::from_u32(rgba)) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let mut rgba = u32::deserialize(deserializer)?; + + if rgba <= 0xFFFFFF { + rgba = (rgba << 8) + 0xFF; + } + + Ok(Self::from_u32(rgba)) + } +} + +impl ToJson for Color { fn to_json(&self) -> serde_json::Value { - json!(format!("0x{:x}{:x}{:x}", self.r, self.g, self.b)) + json!(format!( + "0x{:x}{:x}{:x}{:x}", + self.0.r, self.0.g, self.0.b, self.0.a + )) + } +} + +impl Deref for Color { + type Target = ColorU; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for Color { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl fmt::Debug for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.0.fmt(f) } } diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 9ae084193143a48db09a2d21af9305bd869dae90..212e349314e48cb4acc827770d51b14dc6cd30c4 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -1,20 +1,24 @@ use pathfinder_geometry::rect::RectF; +use serde::Deserialize; use serde_json::json; use crate::{ - color::ColorU, - geometry::vector::{vec2f, Vector2F}, + color::Color, + geometry::{ + deserialize_vec2f, + vector::{vec2f, Vector2F}, + }, json::ToJson, scene::{self, Border, Quad}, AfterLayoutContext, Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint, }; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct ContainerStyle { margin: Margin, padding: Padding, - background_color: Option, + background_color: Option, border: Border, corner_radius: f32, shadow: Option, @@ -80,8 +84,8 @@ impl Container { self } - pub fn with_background_color(mut self, color: impl Into) -> Self { - self.style.background_color = Some(color.into()); + pub fn with_background_color(mut self, color: Color) -> Self { + self.style.background_color = Some(color); self } @@ -95,11 +99,11 @@ impl Container { self } - pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: impl Into) -> Self { + pub fn with_shadow(mut self, offset: Vector2F, blur: f32, color: Color) -> Self { self.style.shadow = Some(Shadow { offset, blur, - color: color.into(), + color, }); self } @@ -241,7 +245,7 @@ impl ToJson for ContainerStyle { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Margin { top: f32, left: f32, @@ -268,7 +272,7 @@ impl ToJson for Margin { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Padding { top: f32, left: f32, @@ -295,11 +299,12 @@ impl ToJson for Padding { } } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct Shadow { + #[serde(deserialize_with = "deserialize_vec2f")] offset: Vector2F, blur: f32, - color: ColorU, + color: Color, } impl ToJson for Shadow { diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index c64b1b8f00d22928d82512d51336aa040e5a84c6..ae1c4d17fd1628b258d2a76e0a4949698fdbafde 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,10 +1,7 @@ -use serde_json::json; -use smallvec::{smallvec, SmallVec}; - use crate::{ - color::ColorU, + color::Color, font_cache::FamilyId, - fonts::{FontId, Properties}, + fonts::{deserialize_font_properties, deserialize_option_font_properties, FontId, Properties}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -14,6 +11,9 @@ use crate::{ AfterLayoutContext, DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext, SizeConstraint, }; +use serde::Deserialize; +use serde_json::json; +use smallvec::{smallvec, SmallVec}; pub struct Label { text: String, @@ -23,12 +23,14 @@ pub struct Label { highlight_indices: Vec, } -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize)] pub struct LabelStyle { - pub default_color: ColorU, - pub highlight_color: ColorU, + pub color: Color, + pub highlight_color: Option, + #[serde(deserialize_with = "deserialize_font_properties")] pub font_properties: Properties, - pub highlight_font_properties: Properties, + #[serde(default, deserialize_with = "deserialize_option_font_properties")] + pub highlight_font_properties: Option, } impl Label { @@ -47,8 +49,8 @@ impl Label { self } - pub fn with_default_color(mut self, color: ColorU) -> Self { - self.style.default_color = color; + pub fn with_default_color(mut self, color: Color) -> Self { + self.style.color = color; self } @@ -61,13 +63,15 @@ impl Label { &self, font_cache: &FontCache, font_id: FontId, - ) -> SmallVec<[(usize, FontId, ColorU); 8]> { + ) -> SmallVec<[(usize, FontId, Color); 8]> { if self.highlight_indices.is_empty() { - return smallvec![(self.text.len(), font_id, self.style.default_color)]; + return smallvec![(self.text.len(), font_id, self.style.color)]; } - let highlight_font_id = font_cache - .select_font(self.family_id, &self.style.highlight_font_properties) + let highlight_font_id = self + .style + .highlight_font_properties + .and_then(|properties| font_cache.select_font(self.family_id, &properties).ok()) .unwrap_or(font_id); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); @@ -75,11 +79,11 @@ impl Label { for (char_ix, c) in self.text.char_indices() { let mut font_id = font_id; - let mut color = self.style.default_color; + let mut color = self.style.color; if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { font_id = highlight_font_id; - color = self.style.highlight_color; + color = self.style.highlight_color.unwrap_or(self.style.color); highlight_indices.next(); } } @@ -179,7 +183,7 @@ impl Element for Label { impl ToJson for LabelStyle { fn to_json(&self) -> Value { json!({ - "default_color": self.default_color.to_json(), + "default_color": self.color.to_json(), "default_font_properties": self.font_properties.to_json(), "highlight_color": self.highlight_color.to_json(), "highlight_font_properties": self.highlight_font_properties.to_json(), @@ -204,14 +208,14 @@ mod tests { .font_cache() .select_font(menlo, Properties::new().weight(Weight::BOLD)) .unwrap(); - let black = ColorU::black(); - let red = ColorU::new(255, 0, 0, 255); + let black = Color::black(); + let red = Color::new(255, 0, 0, 255); let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) .with_style(&LabelStyle { - default_color: black, - highlight_color: red, - highlight_font_properties: *Properties::new().weight(Weight::BOLD), + color: black, + highlight_color: Some(red), + highlight_font_properties: Some(*Properties::new().weight(Weight::BOLD)), ..Default::default() }) .with_highlights(vec![ diff --git a/gpui/src/elements/svg.rs b/gpui/src/elements/svg.rs index 855d30b1a3b619f902b06a354100dcebaac5276e..93d26f9656a45ba2328972c99226cc6a3c58588b 100644 --- a/gpui/src/elements/svg.rs +++ b/gpui/src/elements/svg.rs @@ -3,7 +3,7 @@ use std::borrow::Cow; use serde_json::json; use crate::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -14,18 +14,18 @@ use crate::{ pub struct Svg { path: Cow<'static, str>, - color: ColorU, + color: Color, } impl Svg { pub fn new(path: impl Into>) -> Self { Self { path: path.into(), - color: ColorU::black(), + color: Color::black(), } } - pub fn with_color(mut self, color: ColorU) -> Self { + pub fn with_color(mut self, color: Color) -> Self { self.color = color; self } diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index e7e6b5dedb308de91635846a44d5fce340236235..b9fcc88b8240946c2556d6747664accf633f4e2a 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,14 +1,75 @@ -use crate::json::json; -pub use font_kit::metrics::Metrics; -pub use font_kit::properties::{Properties, Stretch, Style, Weight}; - -use crate::json::ToJson; +use crate::json::{json, ToJson}; +pub use font_kit::{ + metrics::Metrics, + properties::{Properties, Stretch, Style, Weight}, +}; +use serde::{Deserialize, Deserializer}; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; +#[allow(non_camel_case_types)] +#[derive(Deserialize)] +enum WeightJson { + thin, + extra_light, + light, + normal, + medium, + semibold, + bold, + extra_bold, + black, +} + +#[derive(Deserialize)] +struct PropertiesJson { + weight: Option, + #[serde(default)] + italic: bool, +} + +impl Into for PropertiesJson { + fn into(self) -> Properties { + let mut result = Properties::new(); + result.weight = match self.weight.unwrap_or(WeightJson::normal) { + WeightJson::thin => Weight::THIN, + WeightJson::extra_light => Weight::EXTRA_LIGHT, + WeightJson::light => Weight::LIGHT, + WeightJson::normal => Weight::NORMAL, + WeightJson::medium => Weight::MEDIUM, + WeightJson::semibold => Weight::SEMIBOLD, + WeightJson::bold => Weight::BOLD, + WeightJson::extra_bold => Weight::EXTRA_BOLD, + WeightJson::black => Weight::BLACK, + }; + if self.italic { + result.style = Style::Italic; + } + result + } +} + +pub fn deserialize_option_font_properties<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let json: Option = Deserialize::deserialize(deserializer)?; + Ok(json.map(Into::into)) +} + +pub fn deserialize_font_properties<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let json: PropertiesJson = Deserialize::deserialize(deserializer)?; + Ok(json.into()) +} + impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ diff --git a/gpui/src/geometry.rs b/gpui/src/geometry.rs index cf8ec9cb40e681cadcfd38783f25742f0eccc401..705184fa6bae2f2b1ffd5f9ede4e713cbb9a828e 100644 --- a/gpui/src/geometry.rs +++ b/gpui/src/geometry.rs @@ -1,7 +1,8 @@ use super::scene::{Path, PathVertex}; -use crate::{color::ColorU, json::ToJson}; +use crate::{color::Color, json::ToJson}; pub use pathfinder_geometry::*; use rect::RectF; +use serde::{Deserialize, Deserializer}; use serde_json::json; use vector::{vec2f, Vector2F}; @@ -55,7 +56,7 @@ impl PathBuilder { self.current = point; } - pub fn build(mut self, color: ColorU, clip_bounds: Option) -> Path { + pub fn build(mut self, color: Color, clip_bounds: Option) -> Path { if let Some(clip_bounds) = clip_bounds { self.bounds = self .bounds @@ -108,6 +109,14 @@ impl PathBuilder { } } +pub fn deserialize_vec2f<'de, D>(deserializer: D) -> Result +where + D: Deserializer<'de>, +{ + let [x, y]: [f32; 2] = Deserialize::deserialize(deserializer)?; + Ok(vec2f(x, y)) +} + impl ToJson for Vector2F { fn to_json(&self) -> serde_json::Value { json!([self.x(), self.y()]) diff --git a/gpui/src/platform.rs b/gpui/src/platform.rs index 7159e0c14969f72cf58f8c6877a7c348158aeccd..7107d7763dd373383d874cb90421c4eedd2d0ca2 100644 --- a/gpui/src/platform.rs +++ b/gpui/src/platform.rs @@ -8,7 +8,7 @@ pub mod current { } use crate::{ - color::ColorU, + color::Color, executor, fonts::{FontId, GlyphId, Metrics as FontMetrics, Properties as FontProperties}, geometry::{ @@ -134,7 +134,7 @@ pub trait FontSystem: Send + Sync { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout; fn wrap_line(&self, text: &str, font_id: FontId, font_size: f32, width: f32) -> Vec; } diff --git a/gpui/src/platform/mac/fonts.rs b/gpui/src/platform/mac/fonts.rs index 2f29625232e8f3d6ecd2c6e4c305c09d125fea39..d5bcc6695edb9e5444b52ba5662045cc3eafcb89 100644 --- a/gpui/src/platform/mac/fonts.rs +++ b/gpui/src/platform/mac/fonts.rs @@ -1,5 +1,5 @@ use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId, Metrics, Properties}, geometry::{ rect::{RectF, RectI}, @@ -82,7 +82,7 @@ impl platform::FontSystem for FontSystem { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout { self.0.read().layout_line(text, font_size, runs) } @@ -191,7 +191,7 @@ impl FontSystemState { &self, text: &str, font_size: f32, - runs: &[(usize, FontId, ColorU)], + runs: &[(usize, FontId, Color)], ) -> LineLayout { let font_id_attr_name = CFString::from_static_string("zed_font_id"); @@ -436,9 +436,9 @@ mod tests { text, 16.0, &[ - (9, zapfino_regular, ColorU::default()), - (13, menlo_regular, ColorU::default()), - (text.len() - 22, zapfino_regular, ColorU::default()), + (9, zapfino_regular, Color::default()), + (13, menlo_regular, Color::default()), + (text.len() - 22, zapfino_regular, Color::default()), ], ); assert_eq!( diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 82521cdf86464ddb548c0da1a9a64f611d6801f9..40ae31627d81151ae96ba73edd2ed8cb62043271 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -1,6 +1,6 @@ use super::{atlas::AtlasAllocator, sprite_cache::SpriteCache}; use crate::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, vec2i, Vector2F}, @@ -11,7 +11,7 @@ use crate::{ }; use cocoa::foundation::NSUInteger; use metal::{MTLPixelFormat, MTLResourceOptions, NSRange}; -use shaders::{ToFloat2 as _, ToUchar4 as _}; +use shaders::ToFloat2 as _; use std::{collections::HashMap, ffi::c_void, iter::Peekable, mem, sync::Arc, vec}; const SHADERS_METALLIB: &'static [u8] = @@ -438,7 +438,7 @@ impl Renderer { size: bounds.size().round().to_float2(), background_color: quad .background - .unwrap_or(ColorU::transparent_black()) + .unwrap_or(Color::transparent_black()) .to_uchar4(), border_top: border_width * (quad.border.top as usize as f32), border_right: border_width * (quad.border.right as usize as f32), @@ -447,7 +447,7 @@ impl Renderer { border_color: quad .border .color - .unwrap_or(ColorU::transparent_black()) + .unwrap_or(Color::transparent_black()) .to_uchar4(), corner_radius: quad.corner_radius * scene.scale_factor(), }; @@ -782,7 +782,7 @@ mod shaders { use pathfinder_geometry::vector::Vector2I; - use crate::{color::ColorU, geometry::vector::Vector2F}; + use crate::{color::Color, geometry::vector::Vector2F}; use std::mem; include!(concat!(env!("OUT_DIR"), "/shaders.rs")); @@ -791,10 +791,6 @@ mod shaders { fn to_float2(&self) -> vector_float2; } - pub trait ToUchar4 { - fn to_uchar4(&self) -> vector_uchar4; - } - impl ToFloat2 for (f32, f32) { fn to_float2(&self) -> vector_float2 { unsafe { @@ -823,8 +819,8 @@ mod shaders { } } - impl ToUchar4 for ColorU { - fn to_uchar4(&self) -> vector_uchar4 { + impl Color { + pub fn to_uchar4(&self) -> vector_uchar4 { let mut vec = self.a as vector_uchar4; vec <<= 8; vec |= self.b as vector_uchar4; diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 10460ed2e0a526af5c4de1a7b6edac3bf823531f..b6d89cf3ae94de267e508295f29805c5edb7a6c3 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -1,9 +1,9 @@ -use std::borrow::Cow; - +use serde::Deserialize; use serde_json::json; +use std::borrow::Cow; use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId}, geometry::{rect::RectF, vector::Vector2F}, json::ToJson, @@ -28,7 +28,7 @@ pub struct Layer { #[derive(Default, Debug)] pub struct Quad { pub bounds: RectF, - pub background: Option, + pub background: Option, pub border: Border, pub corner_radius: f32, } @@ -38,7 +38,7 @@ pub struct Shadow { pub bounds: RectF, pub corner_radius: f32, pub sigma: f32, - pub color: ColorU, + pub color: Color, } #[derive(Debug)] @@ -47,20 +47,20 @@ pub struct Glyph { pub font_size: f32, pub id: GlyphId, pub origin: Vector2F, - pub color: ColorU, + pub color: Color, } pub struct Icon { pub bounds: RectF, pub svg: usvg::Tree, pub path: Cow<'static, str>, - pub color: ColorU, + pub color: Color, } -#[derive(Clone, Copy, Default, Debug)] +#[derive(Clone, Copy, Default, Debug, Deserialize)] pub struct Border { pub width: f32, - pub color: Option, + pub color: Option, pub top: bool, pub right: bool, pub bottom: bool, @@ -70,7 +70,7 @@ pub struct Border { #[derive(Debug)] pub struct Path { pub bounds: RectF, - pub color: ColorU, + pub color: Color, pub vertices: Vec, } @@ -193,10 +193,10 @@ impl Layer { } impl Border { - pub fn new(width: f32, color: impl Into) -> Self { + pub fn new(width: f32, color: Color) -> Self { Self { width, - color: Some(color.into()), + color: Some(color), top: false, left: false, bottom: false, @@ -204,10 +204,10 @@ impl Border { } } - pub fn all(width: f32, color: impl Into) -> Self { + pub fn all(width: f32, color: Color) -> Self { Self { width, - color: Some(color.into()), + color: Some(color), top: true, left: true, bottom: true, @@ -215,25 +215,25 @@ impl Border { } } - pub fn top(width: f32, color: impl Into) -> Self { + pub fn top(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.top = true; border } - pub fn left(width: f32, color: impl Into) -> Self { + pub fn left(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.left = true; border } - pub fn bottom(width: f32, color: impl Into) -> Self { + pub fn bottom(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.bottom = true; border } - pub fn right(width: f32, color: impl Into) -> Self { + pub fn right(width: f32, color: Color) -> Self { let mut border = Self::new(width, color); border.right = true; border diff --git a/gpui/src/text_layout.rs b/gpui/src/text_layout.rs index ee4401788fcc8691f0ba7d416e4cb24c6a8d6f6b..57556d61b6b25b25ee28bbda20893dd2c0c196b0 100644 --- a/gpui/src/text_layout.rs +++ b/gpui/src/text_layout.rs @@ -1,5 +1,5 @@ use crate::{ - color::ColorU, + color::Color, fonts::{FontId, GlyphId}, geometry::{ rect::RectF, @@ -43,7 +43,7 @@ impl TextLayoutCache { &'a self, text: &'a str, font_size: f32, - runs: &'a [(usize, FontId, ColorU)], + runs: &'a [(usize, FontId, Color)], ) -> Line { let key = &CacheKeyRef { text, @@ -94,7 +94,7 @@ impl<'a> Hash for (dyn CacheKey + 'a) { struct CacheKeyValue { text: String, font_size: OrderedFloat, - runs: SmallVec<[(usize, FontId, ColorU); 1]>, + runs: SmallVec<[(usize, FontId, Color); 1]>, } impl CacheKey for CacheKeyValue { @@ -123,7 +123,7 @@ impl<'a> Borrow for CacheKeyValue { struct CacheKeyRef<'a> { text: &'a str, font_size: OrderedFloat, - runs: &'a [(usize, FontId, ColorU)], + runs: &'a [(usize, FontId, Color)], } impl<'a> CacheKey for CacheKeyRef<'a> { @@ -135,7 +135,7 @@ impl<'a> CacheKey for CacheKeyRef<'a> { #[derive(Default, Debug)] pub struct Line { layout: Arc, - color_runs: SmallVec<[(u32, ColorU); 32]>, + color_runs: SmallVec<[(u32, Color); 32]>, } #[derive(Default, Debug)] @@ -162,7 +162,7 @@ pub struct Glyph { } impl Line { - fn new(layout: Arc, runs: &[(usize, FontId, ColorU)]) -> Self { + fn new(layout: Arc, runs: &[(usize, FontId, Color)]) -> Self { let mut color_runs = SmallVec::new(); for (len, _, color) in runs { color_runs.push((*len as u32, *color)); @@ -206,7 +206,7 @@ impl Line { let mut color_runs = self.color_runs.iter(); let mut color_end = 0; - let mut color = ColorU::black(); + let mut color = Color::black(); for run in &self.layout.runs { let max_glyph_width = cx @@ -230,7 +230,7 @@ impl Line { color = next_run.1; } else { color_end = self.layout.len; - color = ColorU::black(); + color = Color::black(); } } diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 705afaeb20ab272987accad3a9e041feef821d4e..7283e8f6b458d3ff792b1d28a6e32f733ad61ca8 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -16,7 +16,7 @@ pub use display_map::DisplayPoint; use display_map::*; pub use element::*; use gpui::{ - color::ColorU, font_cache::FamilyId, fonts::Properties as FontProperties, + color::Color, font_cache::FamilyId, fonts::Properties as FontProperties, geometry::vector::Vector2F, keymap::Binding, text_layout, AppContext, ClipboardItem, Element, ElementBox, Entity, FontCache, ModelHandle, MutableAppContext, RenderContext, Task, TextLayoutCache, View, ViewContext, WeakViewHandle, @@ -2349,7 +2349,7 @@ impl Snapshot { .layout_str( "1".repeat(digit_count).as_str(), font_size, - &[(digit_count, font_id, ColorU::black())], + &[(digit_count, font_id, Color::black())], ) .width()) } @@ -2374,9 +2374,9 @@ impl Snapshot { { let display_row = rows.start + ix as u32; let color = if active_rows.contains_key(&display_row) { - theme.editor.line_number_active.0 + theme.editor.line_number_active } else { - theme.editor.line_number.0 + theme.editor.line_number }; if soft_wrapped { layouts.push(None); @@ -2485,7 +2485,7 @@ impl Snapshot { &[( self.display_snapshot.line_len(row) as usize, font_id, - ColorU::black(), + Color::black(), )], )) } diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 2bebfeaf9e4798e023abfb4cce368a1d890fbb95..67246bd983575ebc8057201652bebd66099bddc1 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -340,7 +340,7 @@ mod tests { util::RandomCharIter, }; use buffer::{History, SelectionGoal}; - use gpui::{color::ColorU, MutableAppContext}; + use gpui::{color::Color, MutableAppContext}; use rand::{prelude::StdRng, Rng}; use std::{env, sync::Arc}; use Bias::*; @@ -656,12 +656,12 @@ mod tests { syntax: vec![ ( "mod.body".to_string(), - ColorU::from_u32(0xff0000ff), + Color::from_u32(0xff0000ff), Default::default(), ), ( "fn.name".to_string(), - ColorU::from_u32(0x00ff00ff), + Color::from_u32(0x00ff00ff), Default::default(), ), ], @@ -754,12 +754,12 @@ mod tests { syntax: vec![ ( "mod.body".to_string(), - ColorU::from_u32(0xff0000ff), + Color::from_u32(0xff0000ff), Default::default(), ), ( "fn.name".to_string(), - ColorU::from_u32(0x00ff00ff), + Color::from_u32(0x00ff00ff), Default::default(), ), ], diff --git a/zed/src/editor/element.rs b/zed/src/editor/element.rs index c2a486afdd884cc73019f5b40bb02f33dc9b8c30..44b283dee1d7bf7108be123c535578b33a402560 100644 --- a/zed/src/editor/element.rs +++ b/zed/src/editor/element.rs @@ -1,7 +1,7 @@ use super::{DisplayPoint, Editor, SelectAction, Snapshot}; use crate::time::ReplicaId; use gpui::{ - color::ColorU, + color::Color, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -196,14 +196,14 @@ impl EditorElement { let theme = &settings.theme; cx.scene.push_quad(Quad { bounds: gutter_bounds, - background: Some(theme.editor.gutter_background.0), - border: Border::new(0., ColorU::transparent_black()), + background: Some(theme.editor.gutter_background), + border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); cx.scene.push_quad(Quad { bounds: text_bounds, - background: Some(theme.editor.background.0), - border: Border::new(0., ColorU::transparent_black()), + background: Some(theme.editor.background), + border: Border::new(0., Color::transparent_black()), corner_radius: 0., }); @@ -229,7 +229,7 @@ impl EditorElement { ); cx.scene.push_quad(Quad { bounds: RectF::new(origin, size), - background: Some(theme.editor.active_line_background.0), + background: Some(theme.editor.active_line_background), border: Border::default(), corner_radius: 0., }); @@ -290,7 +290,7 @@ impl EditorElement { }; let selection = Selection { - color: replica_theme.selection.0, + color: replica_theme.selection, line_height: layout.line_height, start_y: content_origin.y() + row_range.start as f32 * layout.line_height - scroll_top, @@ -333,7 +333,7 @@ impl EditorElement { - scroll_left; let y = selection.end.row() as f32 * layout.line_height - scroll_top; cursors.push(Cursor { - color: replica_theme.cursor.0, + color: replica_theme.cursor, origin: content_origin + vec2f(x, y), line_height: layout.line_height, }); @@ -707,7 +707,7 @@ impl PaintState { struct Cursor { origin: Vector2F, line_height: f32, - color: ColorU, + color: Color, } impl Cursor { @@ -715,7 +715,7 @@ impl Cursor { cx.scene.push_quad(Quad { bounds: RectF::new(self.origin, vec2f(2.0, self.line_height)), background: Some(self.color), - border: Border::new(0., ColorU::black()), + border: Border::new(0., Color::black()), corner_radius: 0., }); } @@ -726,7 +726,7 @@ struct Selection { start_y: f32, line_height: f32, lines: Vec, - color: ColorU, + color: Color, } #[derive(Debug)] diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index faecae42f47462e71b6565be39567b92d9ea291f..36ff7c8312014cc3a12d9b195ee1c77edf9e376d 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -6,7 +6,7 @@ use crate::{ worktree::{match_paths, PathMatch, Worktree}, }; use gpui::{ - color::ColorF, + color::Color, elements::*, fonts::{Properties, Weight}, geometry::vector::vec2f, @@ -82,7 +82,7 @@ impl View for FileFinder { .with_uniform_padding(6.0) .with_corner_radius(6.0) .with_background_color(settings.theme.ui.modal_background) - .with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8()) + .with_shadow(vec2f(0., 4.), 12., Color::new(0, 0, 0, 128)) .boxed(), ) .with_max_width(600.0) @@ -114,7 +114,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.default_text.0) + .with_default_color(settings.theme.editor.default_text) .boxed(), ) .with_margin_top(6.0) @@ -155,9 +155,9 @@ impl FileFinder { let bold = *Properties::new().weight(Weight::BOLD); let selected_index = self.selected_index(); let label_style = LabelStyle { - default_color: theme.modal_match_text.0, - highlight_color: theme.modal_match_text_highlight.0, - highlight_font_properties: bold, + color: theme.modal_match_text, + highlight_color: Some(theme.modal_match_text_highlight), + highlight_font_properties: Some(bold), ..Default::default() }; let mut container = Container::new( @@ -206,9 +206,9 @@ impl FileFinder { ) .with_uniform_padding(6.0) .with_background_color(if index == selected_index { - theme.modal_match_background_active.0 + theme.modal_match_background_active } else { - theme.modal_match_background.0 + theme.modal_match_background }); if index == selected_index || index < self.matches.len() - 1 { diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 79ee243ae08c2f8cb86e5719ebebc24bbcac5d13..fcdf230130ec3296ef8e2a9c94b11a99d977f3cf 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -1,6 +1,6 @@ use anyhow::{anyhow, Context, Result}; use gpui::{ - color::ColorU, + color::Color, font_cache::{FamilyId, FontCache}, fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}, AssetSource, @@ -9,12 +9,7 @@ use parking_lot::Mutex; use postage::watch; use serde::{de::value::MapDeserializer, Deserialize}; use serde_json::Value; -use std::{ - collections::HashMap, - fmt, - ops::{Deref, DerefMut}, - sync::Arc, -}; +use std::{collections::HashMap, sync::Arc}; const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); @@ -38,7 +33,7 @@ pub struct ThemeRegistry { pub struct Theme { pub ui: UiTheme, pub editor: EditorTheme, - pub syntax: Vec<(String, ColorU, FontProperties)>, + pub syntax: Vec<(String, Color, FontProperties)>, } #[derive(Deserialize)] @@ -93,9 +88,6 @@ pub struct ReplicaTheme { pub selection: Color, } -#[derive(Clone, Copy, Default)] -pub struct Color(pub ColorU); - #[derive(Clone, Debug)] pub struct ThemeMap(Arc<[StyleId]>); @@ -151,7 +143,7 @@ impl ThemeRegistry { } let theme_toml = self.load(name)?; - let mut syntax = Vec::<(String, ColorU, FontProperties)>::new(); + let mut syntax = Vec::<(String, Color, FontProperties)>::new(); for (key, style) in theme_toml.syntax.iter() { let mut color = Color::default(); let mut properties = FontProperties::new(); @@ -171,7 +163,7 @@ impl ThemeRegistry { } match syntax.binary_search_by_key(&key, |e| &e.0) { Ok(i) | Err(i) => { - syntax.insert(i, (key.to_string(), color.0, properties)); + syntax.insert(i, (key.to_string(), color, properties)); } } } @@ -234,11 +226,12 @@ impl ThemeRegistry { } impl Theme { - pub fn syntax_style(&self, id: StyleId) -> (ColorU, FontProperties) { - self.syntax.get(id.0 as usize).map_or( - (self.editor.default_text.0, FontProperties::new()), - |entry| (entry.1, entry.2), - ) + pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) { + self.syntax + .get(id.0 as usize) + .map_or((self.editor.default_text, FontProperties::new()), |entry| { + (entry.1, entry.2) + }) } #[cfg(test)] @@ -313,53 +306,6 @@ impl Default for StyleId { } } -impl Color { - fn from_u32(rgba: u32) -> Self { - Self(ColorU::from_u32(rgba)) - } -} - -impl<'de> Deserialize<'de> for Color { - fn deserialize(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let rgb = u32::deserialize(deserializer)?; - Ok(Self::from_u32((rgb << 8) + 0xFF)) - } -} - -impl Into for Color { - fn into(self) -> ColorU { - self.0 - } -} - -impl Deref for Color { - type Target = ColorU; - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for Color { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl fmt::Debug for Color { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - -impl PartialEq for Color { - fn eq(&self, other: &ColorU) -> bool { - self.0.eq(other) - } -} - pub fn channel( font_cache: &FontCache, ) -> Result<(watch::Sender, watch::Receiver)> { @@ -478,25 +424,25 @@ mod tests { 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.editor.background, ColorU::from_u32(0x00ed00ff)); - assert_eq!(theme.editor.line_number, ColorU::from_u32(0xddddddff)); + assert_eq!(theme.ui.tab_background_active, Color::from_u32(0x100000ff)); + assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff)); + assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff)); assert_eq!( theme.syntax, &[ ( "alpha.one".to_string(), - ColorU::from_u32(0x112233ff), + Color::from_u32(0x112233ff), *FontProperties::new().weight(FontWeight::BOLD) ), ( "beta.two".to_string(), - ColorU::from_u32(0xaabbccff), + Color::from_u32(0xaabbccff), *FontProperties::new().weight(FontWeight::NORMAL) ), ( "gamma.three".to_string(), - ColorU::from_u32(0x00000000), + Color::from_u32(0x00000000), *FontProperties::new() .weight(FontWeight::LIGHT) .style(FontStyle::Italic), @@ -553,10 +499,10 @@ mod tests { 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)); + assert_eq!(theme.ui.tab_background, Color::from_u32(0x555555ff)); + assert_eq!(theme.ui.tab_text, Color::from_u32(0x333333ff)); + assert_eq!(theme.editor.background, Color::from_u32(0x666666ff)); + assert_eq!(theme.editor.default_text, Color::from_u32(0x444444ff)); assert_eq!( registry.list().collect::>(), @@ -577,12 +523,12 @@ mod tests { ui: Default::default(), editor: Default::default(), syntax: [ - ("function", ColorU::from_u32(0x100000ff)), - ("function.method", ColorU::from_u32(0x200000ff)), - ("function.async", ColorU::from_u32(0x300000ff)), - ("variable.builtin.self.rust", ColorU::from_u32(0x400000ff)), - ("variable.builtin", ColorU::from_u32(0x500000ff)), - ("variable", ColorU::from_u32(0x600000ff)), + ("function", Color::from_u32(0x100000ff)), + ("function.method", Color::from_u32(0x200000ff)), + ("function.async", Color::from_u32(0x300000ff)), + ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), + ("variable.builtin", Color::from_u32(0x500000ff)), + ("variable", Color::from_u32(0x600000ff)), ] .iter() .map(|e| (e.0.to_string(), e.1, FontProperties::new())) diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 55a32d94c5426c9f574e9dacab0506a6b1d63471..c88f1e70055dfa698bff7935787627516f5c40ec 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -9,7 +9,7 @@ use crate::{ }; use futures::lock::Mutex; use gpui::{ - color::ColorF, + color::Color, elements::{ Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, LabelStyle, ParentElement, UniformList, UniformListState, @@ -199,7 +199,7 @@ impl ThemeSelector { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.default_text.0) + .with_default_color(settings.theme.editor.default_text) .boxed(), ) .with_margin_top(6.0) @@ -241,9 +241,9 @@ impl ThemeSelector { settings.ui_font_size, ) .with_style(&LabelStyle { - default_color: theme.modal_match_text.0, - highlight_color: theme.modal_match_text_highlight.0, - highlight_font_properties: *Properties::new().weight(Weight::BOLD), + color: theme.modal_match_text, + highlight_color: Some(theme.modal_match_text_highlight), + highlight_font_properties: Some(*Properties::new().weight(Weight::BOLD)), ..Default::default() }) .with_highlights(theme_match.positions.clone()) @@ -251,9 +251,9 @@ impl ThemeSelector { ) .with_uniform_padding(6.0) .with_background_color(if index == self.selected_index { - theme.modal_match_background_active.0 + theme.modal_match_background_active } else { - theme.modal_match_background.0 + theme.modal_match_background }); if index == self.selected_index || index < self.matches.len() - 1 { @@ -288,7 +288,7 @@ impl View for ThemeSelector { .with_uniform_padding(6.0) .with_corner_radius(6.0) .with_background_color(settings.theme.ui.modal_background) - .with_shadow(vec2f(0., 4.), 12., ColorF::new(0.0, 0.0, 0.0, 0.5).to_u8()) + .with_shadow(vec2f(0., 4.), 12., Color::new(0, 0, 0, 128)) .boxed(), ) .with_max_width(600.0) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 9102e9f8136812432d10a29d1587b1640c462e68..a8eda55d9df799088f7878c492ad75e24338b2bf 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,7 +1,7 @@ use super::{ItemViewHandle, SplitDirection}; use crate::settings::{Settings, UiTheme}; use gpui::{ - color::ColorU, + color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, @@ -200,7 +200,7 @@ impl Pane { MouseEventHandler::new::(item.id(), cx, |mouse_state| { let title = item.title(cx); - let mut border = Border::new(1.0, theme.tab_border.0); + let mut border = Border::new(1.0, theme.tab_border); border.left = ix > 0; border.right = ix == last_item_ix; border.bottom = ix != self.active_item; @@ -215,9 +215,9 @@ impl Pane { settings.ui_font_size, ) .with_default_color(if is_active { - theme.tab_text_active.0 + theme.tab_text_active } else { - theme.tab_text.0 + theme.tab_text }) .boxed(), ) @@ -317,12 +317,12 @@ impl Pane { }; let icon = if tab_hovered { - let close_color = current_color.unwrap_or(theme.tab_icon_close).0; + let close_color = current_color.unwrap_or(theme.tab_icon_close); let icon = Svg::new("icons/x.svg").with_color(close_color); MouseEventHandler::new::(item_id, cx, |mouse_state| { if mouse_state.hovered { - Container::new(icon.with_color(ColorU::white()).boxed()) + Container::new(icon.with_color(Color::white()).boxed()) .with_background_color(if mouse_state.clicked { clicked_color } else { @@ -344,7 +344,7 @@ impl Pane { let square = RectF::new(bounds.origin(), vec2f(diameter, diameter)); cx.scene.push_quad(Quad { bounds: square, - background: Some(current_color.0), + background: Some(current_color), border: Default::default(), corner_radius: diameter / 2., }); diff --git a/zed/src/workspace/pane_group.rs b/zed/src/workspace/pane_group.rs index 46a00b3f2a0e63b7adde8fb44a1ed5b514a65da3..d5d0040009fba87bfd9bc0bb29acb6a533457336 100644 --- a/zed/src/workspace/pane_group.rs +++ b/zed/src/workspace/pane_group.rs @@ -1,9 +1,5 @@ use anyhow::{anyhow, Result}; -use gpui::{ - color::{rgbu, ColorU}, - elements::*, - Axis, Border, -}; +use gpui::{color::Color, elements::*, Axis, Border}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct PaneGroup { @@ -388,6 +384,6 @@ fn border_width() -> f32 { } #[inline(always)] -fn border_color() -> ColorU { - rgbu(0xdb, 0xdb, 0xdc) +fn border_color() -> Color { + Color::new(0xdb, 0xdb, 0xdc, 0xff) } From 81041d7841735feacca98799f271d084e62b385b Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Tue, 3 Aug 2021 13:36:58 -0700 Subject: [PATCH 09/23] Restructure Theme with new style objects --- gpui/src/elements/container.rs | 30 ++++++++--- gpui/src/elements/label.rs | 4 +- gpui/src/scene.rs | 10 ++++ zed/assets/themes/_base.toml | 47 +++++++++++------ zed/src/file_finder.rs | 37 ++++--------- zed/src/lib.rs | 1 + zed/src/settings.rs | 96 ++++++++-------------------------- zed/src/theme.rs | 80 ++++++++++++++++++++++++++++ zed/src/theme_selector.rs | 39 +++++--------- zed/src/workspace/pane.rs | 43 ++++++++------- 10 files changed, 218 insertions(+), 169 deletions(-) create mode 100644 zed/src/theme.rs diff --git a/gpui/src/elements/container.rs b/gpui/src/elements/container.rs index 212e349314e48cb4acc827770d51b14dc6cd30c4..ae13b5d82116539ac1cefc2ef5c8b66bbf1ff644 100644 --- a/gpui/src/elements/container.rs +++ b/gpui/src/elements/container.rs @@ -16,12 +16,18 @@ use crate::{ #[derive(Clone, Debug, Default, Deserialize)] pub struct ContainerStyle { - margin: Margin, - padding: Padding, - background_color: Option, - border: Border, - corner_radius: f32, - shadow: Option, + #[serde(default)] + pub margin: Margin, + #[serde(default)] + pub padding: Padding, + #[serde(rename = "background")] + pub background_color: Option, + #[serde(default)] + pub border: Border, + #[serde(default)] + pub corner_radius: f32, + #[serde(default)] + pub shadow: Option, } pub struct Container { @@ -247,9 +253,13 @@ impl ToJson for ContainerStyle { #[derive(Clone, Debug, Default, Deserialize)] pub struct Margin { + #[serde(default)] top: f32, + #[serde(default)] left: f32, + #[serde(default)] bottom: f32, + #[serde(default)] right: f32, } @@ -274,9 +284,13 @@ impl ToJson for Margin { #[derive(Clone, Debug, Default, Deserialize)] pub struct Padding { + #[serde(default)] top: f32, + #[serde(default)] left: f32, + #[serde(default)] bottom: f32, + #[serde(default)] right: f32, } @@ -301,9 +315,11 @@ impl ToJson for Padding { #[derive(Clone, Debug, Default, Deserialize)] pub struct Shadow { - #[serde(deserialize_with = "deserialize_vec2f")] + #[serde(default, deserialize_with = "deserialize_vec2f")] offset: Vector2F, + #[serde(default)] blur: f32, + #[serde(default)] color: Color, } diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index ae1c4d17fd1628b258d2a76e0a4949698fdbafde..ba9b9e8c5e9ba6b480830df6e0d544349ae03ea6 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -25,9 +25,11 @@ pub struct Label { #[derive(Clone, Debug, Default, Deserialize)] pub struct LabelStyle { + #[serde(default = "Color::black")] pub color: Color, + #[serde(default)] pub highlight_color: Option, - #[serde(deserialize_with = "deserialize_font_properties")] + #[serde(default, deserialize_with = "deserialize_font_properties")] pub font_properties: Properties, #[serde(default, deserialize_with = "deserialize_option_font_properties")] pub highlight_font_properties: Option, diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index b6d89cf3ae94de267e508295f29805c5edb7a6c3..8c83c5268b35d6d8a9f3b6a8646fab47396aef8d 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -59,14 +59,24 @@ pub struct Icon { #[derive(Clone, Copy, Default, Debug, Deserialize)] pub struct Border { + #[serde(default = "default_border_width")] pub width: f32, + #[serde(default)] pub color: Option, + #[serde(default)] pub top: bool, + #[serde(default)] pub right: bool, + #[serde(default)] pub bottom: bool, + #[serde(default)] pub left: bool, } +fn default_border_width() -> f32 { + 1.0 +} + #[derive(Debug)] pub struct Path { pub bounds: RectF, diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index fd86c98e6744bce343c38cb46a9ea8e451b25018..fb73a3830df27e879e2c85537882b64d3bfe0e6a 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -1,19 +1,36 @@ [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 + +[ui.tab] +background = "$elevation_2" +color = "$text_dull" +border.color = 0x000000 +icon_close = 0x383839 +icon_dirty = 0x556de8 +icon_conflict = 0xe45349 + +[ui.active_tab] +extends = ".." +background = "$elevation_3" +color = "$text_bright" + +[ui.selector] +background = "$elevation_4" +padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } +margin.top = 12.0 +corner_radius = 6.0 +shadow = { offset = [0.0, 0.0], blur = 12.0, color = 0x00000088 } + +[ui.selector.item] +background = 0x424344 +text = 0xcccccc +highlight_text = 0x18a3ff +highlight_font_properties = { weight = "bold" } +border = { color = 0x000000, width = 1.0 } + +[ui.selector.active_item] +extends = ".." +background = 0x094771 [editor] background = "$elevation_3" @@ -21,7 +38,7 @@ gutter_background = "$elevation_3" active_line_background = "$elevation_4" line_number = "$text_dull" line_number_active = "$text_bright" -default_text = "$text_normal" +text = "$text_normal" replicas = [ { selection = 0x264f78, cursor = "$text_bright" }, { selection = 0x504f31, cursor = 0xfcf154 }, diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 36ff7c8312014cc3a12d9b195ee1c77edf9e376d..3bd1e54c8242aaf9305661c192cdd9405d9a29fa 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -6,12 +6,9 @@ use crate::{ worktree::{match_paths, PathMatch, Worktree}, }; use gpui::{ - color::Color, elements::*, - fonts::{Properties, Weight}, - geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, + AppContext, Axis, Entity, MutableAppContext, RenderContext, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; use postage::watch; @@ -78,11 +75,7 @@ impl View for FileFinder { .with_child(Expanded::new(1.0, self.render_matches()).boxed()) .boxed(), ) - .with_margin_top(12.0) - .with_uniform_padding(6.0) - .with_corner_radius(6.0) - .with_background_color(settings.theme.ui.modal_background) - .with_shadow(vec2f(0., 4.), 12., Color::new(0, 0, 0, 128)) + .with_style(&settings.theme.ui.selector.container) .boxed(), ) .with_max_width(600.0) @@ -114,7 +107,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.default_text) + .with_default_color(settings.theme.editor.text) .boxed(), ) .with_margin_top(6.0) @@ -152,15 +145,8 @@ impl FileFinder { let theme = &settings.theme.ui; self.labels_for_match(path_match, cx).map( |(file_name, file_name_positions, full_path, full_path_positions)| { - let bold = *Properties::new().weight(Weight::BOLD); let selected_index = self.selected_index(); - let label_style = LabelStyle { - color: theme.modal_match_text, - highlight_color: Some(theme.modal_match_text_highlight), - highlight_font_properties: Some(bold), - ..Default::default() - }; - let mut container = Container::new( + let container = Container::new( Flex::row() .with_child( Container::new( @@ -184,7 +170,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&label_style) + .with_style(&theme.selector.label) .with_highlights(file_name_positions) .boxed(), ) @@ -194,7 +180,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&label_style) + .with_style(&theme.selector.label) .with_highlights(full_path_positions) .boxed(), ) @@ -205,17 +191,12 @@ impl FileFinder { .boxed(), ) .with_uniform_padding(6.0) - .with_background_color(if index == selected_index { - theme.modal_match_background_active + .with_style(if index == selected_index { + &theme.selector.active_item.container } else { - theme.modal_match_background + &theme.selector.item.container }); - if index == selected_index || index < self.matches.len() - 1 { - container = - container.with_border(Border::bottom(1.0, theme.modal_match_border)); - } - let entry = (path_match.tree_id, path_match.path.clone()); EventHandler::new(container.boxed()) .on_mouse_down(move |cx| { diff --git a/zed/src/lib.rs b/zed/src/lib.rs index a02b00dc6aafd1b4b4275a14470f8fcd8b634451..0236fdd0330a00f521f15e2b6dcabdd4f5a47c0e 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -10,6 +10,7 @@ pub mod settings; mod sum_tree; #[cfg(any(test, feature = "test-support"))] pub mod test; +pub mod theme; pub mod theme_selector; mod time; mod util; diff --git a/zed/src/settings.rs b/zed/src/settings.rs index fcdf230130ec3296ef8e2a9c94b11a99d977f3cf..42b4afa3d3e0890a6e7a427f59ee123e2933d96a 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -11,6 +11,9 @@ use serde::{de::value::MapDeserializer, Deserialize}; use serde_json::Value; use std::{collections::HashMap, sync::Arc}; +use crate::theme; +pub use theme::Theme; + const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); #[derive(Clone)] @@ -29,13 +32,6 @@ pub struct ThemeRegistry { theme_data: Mutex>>, } -#[derive(Clone, Default)] -pub struct Theme { - pub ui: UiTheme, - pub editor: EditorTheme, - pub syntax: Vec<(String, Color, FontProperties)>, -} - #[derive(Deserialize)] struct ThemeToml { #[serde(default)] @@ -50,44 +46,6 @@ struct ThemeToml { syntax: HashMap, } -#[derive(Clone, Default, Deserialize)] -#[serde(default)] -pub struct UiTheme { - pub background: Color, - pub tab_background: Color, - pub tab_background_active: Color, - pub tab_text: Color, - pub tab_text_active: Color, - pub tab_border: Color, - pub tab_icon_close: Color, - pub tab_icon_dirty: Color, - pub tab_icon_conflict: Color, - pub modal_background: Color, - pub modal_match_background: Color, - pub modal_match_background_active: Color, - pub modal_match_border: Color, - pub modal_match_text: Color, - pub modal_match_text_highlight: Color, -} - -#[derive(Clone, Deserialize)] -#[serde(default)] -pub struct EditorTheme { - pub background: Color, - pub gutter_background: Color, - pub active_line_background: Color, - pub line_number: Color, - pub line_number_active: Color, - pub default_text: Color, - pub replicas: Vec, -} - -#[derive(Clone, Copy, Deserialize, Default)] -pub struct ReplicaTheme { - pub cursor: Color, - pub selection: Color, -} - #[derive(Clone, Debug)] pub struct ThemeMap(Arc<[StyleId]>); @@ -169,8 +127,8 @@ impl ThemeRegistry { } let theme = Arc::new(Theme { - ui: UiTheme::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?, - editor: EditorTheme::deserialize(MapDeserializer::new( + ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?, + editor: theme::Editor::deserialize(MapDeserializer::new( theme_toml.editor.clone().into_iter(), ))?, syntax, @@ -229,7 +187,7 @@ impl Theme { pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) { self.syntax .get(id.0 as usize) - .map_or((self.editor.default_text, FontProperties::new()), |entry| { + .map_or((self.editor.text, FontProperties::new()), |entry| { (entry.1, entry.2) }) } @@ -240,20 +198,6 @@ impl Theme { } } -impl Default for EditorTheme { - fn default() -> Self { - Self { - background: Default::default(), - gutter_background: Default::default(), - active_line_background: Default::default(), - line_number: Default::default(), - line_number_active: Default::default(), - default_text: Default::default(), - replicas: vec![ReplicaTheme::default()], - } - } -} - impl ThemeMap { pub fn new(capture_names: &[String], theme: &Theme) -> Self { // For each capture name in the highlight query, find the longest @@ -407,8 +351,8 @@ mod tests { let assets = TestAssets(&[( "themes/my-theme.toml", r#" - [ui] - tab_background_active = 0x100000 + [ui.tab.active] + background = 0x100000 [editor] background = 0x00ed00 @@ -424,7 +368,10 @@ mod tests { let registry = ThemeRegistry::new(assets); let theme = registry.get("my-theme").unwrap(); - assert_eq!(theme.ui.tab_background_active, Color::from_u32(0x100000ff)); + assert_eq!( + theme.ui.active_tab.container.background_color, + Some(Color::from_u32(0x100000ff)) + ); assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff)); assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff)); assert_eq!( @@ -459,9 +406,9 @@ mod tests { r#" abstract = true - [ui] - tab_background = 0x111111 - tab_text = "$variable_1" + [ui.tab] + background = 0x111111 + text = "$variable_1" [editor] background = 0x222222 @@ -477,8 +424,8 @@ mod tests { variable_1 = 0x333333 variable_2 = 0x444444 - [ui] - tab_background = 0x555555 + [ui.tab] + background = 0x555555 [editor] background = 0x666666 @@ -499,10 +446,13 @@ mod tests { let registry = ThemeRegistry::new(assets); let theme = registry.get("light").unwrap(); - assert_eq!(theme.ui.tab_background, Color::from_u32(0x555555ff)); - assert_eq!(theme.ui.tab_text, Color::from_u32(0x333333ff)); + assert_eq!( + theme.ui.tab.container.background_color, + Some(Color::from_u32(0x555555ff)) + ); + assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff)); assert_eq!(theme.editor.background, Color::from_u32(0x666666ff)); - assert_eq!(theme.editor.default_text, Color::from_u32(0x444444ff)); + assert_eq!(theme.editor.text, Color::from_u32(0x444444ff)); assert_eq!( registry.list().collect::>(), diff --git a/zed/src/theme.rs b/zed/src/theme.rs new file mode 100644 index 0000000000000000000000000000000000000000..1a733ff3a6de8c1aa3296c383c712f8af9f5f215 --- /dev/null +++ b/zed/src/theme.rs @@ -0,0 +1,80 @@ +use gpui::color::Color; +use gpui::elements::{ContainerStyle, LabelStyle}; +use gpui::fonts::Properties as FontProperties; +use serde::Deserialize; + +#[derive(Debug, Default)] +pub struct Theme { + pub ui: Ui, + pub editor: Editor, + pub syntax: Vec<(String, Color, FontProperties)>, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Ui { + pub background: Color, + pub tab: Tab, + pub active_tab: Tab, + pub selector: Selector, +} + +#[derive(Debug, Deserialize)] +pub struct Editor { + pub background: Color, + pub gutter_background: Color, + pub active_line_background: Color, + pub line_number: Color, + pub line_number_active: Color, + pub text: Color, + pub replicas: Vec, +} + +#[derive(Clone, Copy, Debug, Default, Deserialize)] +pub struct Replica { + pub cursor: Color, + pub selection: Color, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Tab { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, + pub icon_close: Color, + pub icon_dirty: Color, + pub icon_conflict: Color, +} + +#[derive(Debug, Default, Deserialize)] +pub struct Selector { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, + + pub item: SelectorItem, + pub active_item: SelectorItem, +} + +#[derive(Debug, Default, Deserialize)] +pub struct SelectorItem { + #[serde(flatten)] + pub container: ContainerStyle, + #[serde(flatten)] + pub label: LabelStyle, +} + +impl Default for Editor { + fn default() -> Self { + Self { + background: Default::default(), + gutter_background: Default::default(), + active_line_background: Default::default(), + line_number: Default::default(), + line_number_active: Default::default(), + text: Default::default(), + replicas: vec![Replica::default()], + } + } +} diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index c88f1e70055dfa698bff7935787627516f5c40ec..ec50e92a6e81decb50e0649e6693d8caeca6ca4d 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -9,15 +9,12 @@ use crate::{ }; use futures::lock::Mutex; use gpui::{ - color::Color, elements::{ - Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, LabelStyle, - ParentElement, UniformList, UniformListState, + Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, + UniformList, UniformListState, }, - fonts::{Properties, Weight}, - geometry::vector::vec2f, keymap::{self, Binding}, - AppContext, Axis, Border, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, + AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; use postage::watch; @@ -199,7 +196,7 @@ impl ThemeSelector { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.default_text) + .with_default_color(settings.theme.editor.text) .boxed(), ) .with_margin_top(6.0) @@ -234,32 +231,26 @@ impl ThemeSelector { let settings = self.settings.borrow(); let theme = &settings.theme.ui; - let mut container = Container::new( + let container = Container::new( Label::new( theme_match.string.clone(), settings.ui_font_family, settings.ui_font_size, ) - .with_style(&LabelStyle { - color: theme.modal_match_text, - highlight_color: Some(theme.modal_match_text_highlight), - highlight_font_properties: Some(*Properties::new().weight(Weight::BOLD)), - ..Default::default() + .with_style(if index == self.selected_index { + &theme.selector.active_item.label + } else { + &theme.selector.item.label }) .with_highlights(theme_match.positions.clone()) .boxed(), ) - .with_uniform_padding(6.0) - .with_background_color(if index == self.selected_index { - theme.modal_match_background_active + .with_style(if index == self.selected_index { + &theme.selector.active_item.container } else { - theme.modal_match_background + &theme.selector.item.container }); - if index == self.selected_index || index < self.matches.len() - 1 { - container = container.with_border(Border::bottom(1.0, theme.modal_match_border)); - } - container.boxed() } } @@ -284,11 +275,7 @@ impl View for ThemeSelector { .with_child(Expanded::new(1.0, self.render_matches(cx)).boxed()) .boxed(), ) - .with_margin_top(12.0) - .with_uniform_padding(6.0) - .with_corner_radius(6.0) - .with_background_color(settings.theme.ui.modal_background) - .with_shadow(vec2f(0., 4.), 12., Color::new(0, 0, 0, 128)) + .with_style(&settings.theme.ui.selector.container) .boxed(), ) .with_max_width(600.0) diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index a8eda55d9df799088f7878c492ad75e24338b2bf..69c1a3af0b1336d5984e861a550c0a5bed8dd891 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -1,5 +1,5 @@ use super::{ItemViewHandle, SplitDirection}; -use crate::settings::{Settings, UiTheme}; +use crate::{settings::Settings, theme}; use gpui::{ color::Color, elements::*, @@ -193,6 +193,7 @@ impl Pane { let is_active = ix == self.active_item; enum Tab {} + let border = &theme.tab.container.border; row.add_child( Expanded::new( @@ -200,10 +201,10 @@ impl Pane { MouseEventHandler::new::(item.id(), cx, |mouse_state| { let title = item.title(cx); - let mut border = Border::new(1.0, theme.tab_border); + let mut border = border.clone(); border.left = ix > 0; border.right = ix == last_item_ix; - border.bottom = ix != self.active_item; + border.bottom = !is_active; let mut container = Container::new( Stack::new() @@ -214,10 +215,10 @@ impl Pane { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(if is_active { - theme.tab_text_active + .with_style(if is_active { + &theme.active_tab.label } else { - theme.tab_text + &theme.tab.label }) .boxed(), ) @@ -238,15 +239,16 @@ impl Pane { ) .boxed(), ) + .with_style(if is_active { + &theme.active_tab.container + } else { + &theme.tab.container + }) .with_horizontal_padding(10.) .with_border(border); if is_active { - container = container - .with_background_color(theme.tab_background_active) - .with_padding_bottom(border.width); - } else { - container = container.with_background_color(theme.tab_background); + container = container.with_padding_bottom(border.width); } ConstrainedBox::new( @@ -269,10 +271,13 @@ impl Pane { // Ensure there's always a minimum amount of space after the last tab, // so that the tab's border doesn't abut the window's border. + let mut border = Border::bottom(1.0, Color::default()); + border.color = theme.tab.container.border.color; + row.add_child( ConstrainedBox::new( Container::new(Empty::new().boxed()) - .with_border(Border::bottom(1.0, theme.tab_border)) + .with_border(border) .boxed(), ) .with_min_width(20.) @@ -283,7 +288,7 @@ impl Pane { Expanded::new( 0.0, Container::new(Empty::new().boxed()) - .with_border(Border::bottom(1.0, theme.tab_border)) + .with_border(border) .boxed(), ) .named("filler"), @@ -300,24 +305,24 @@ impl Pane { tab_hovered: bool, is_dirty: bool, has_conflict: bool, - theme: &UiTheme, + theme: &theme::Ui, cx: &AppContext, ) -> ElementBox { enum TabCloseButton {} - let mut clicked_color = theme.tab_icon_dirty; + let mut clicked_color = theme.tab.icon_dirty; clicked_color.a = 180; let current_color = if has_conflict { - Some(theme.tab_icon_conflict) + Some(theme.tab.icon_conflict) } else if is_dirty { - Some(theme.tab_icon_dirty) + Some(theme.tab.icon_dirty) } else { None }; let icon = if tab_hovered { - let close_color = current_color.unwrap_or(theme.tab_icon_close); + let close_color = current_color.unwrap_or(theme.tab.icon_close); let icon = Svg::new("icons/x.svg").with_color(close_color); MouseEventHandler::new::(item_id, cx, |mouse_state| { @@ -326,7 +331,7 @@ impl Pane { .with_background_color(if mouse_state.clicked { clicked_color } else { - theme.tab_icon_dirty + theme.tab.icon_dirty }) .with_corner_radius(close_icon_size / 2.) .boxed() From ca9862fff181bb45e64f84902e1073ddb3ebd7aa Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Aug 2021 18:51:06 -0600 Subject: [PATCH 10/23] Start on new theme::ThemeRegistry Co-Authored-By: Max Brunsfeld --- zed/src/theme.rs | 304 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 301 insertions(+), 3 deletions(-) diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 1a733ff3a6de8c1aa3296c383c712f8af9f5f215..ca6aac18a914696a4bda31c86095dc307407b77a 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -1,7 +1,21 @@ -use gpui::color::Color; -use gpui::elements::{ContainerStyle, LabelStyle}; -use gpui::fonts::Properties as FontProperties; +use anyhow::{anyhow, Context, Result}; +use gpui::{ + color::Color, + elements::{ContainerStyle, LabelStyle}, + fonts::Properties as FontProperties, + AssetSource, +}; +use json::{Map, Value}; +use parking_lot::Mutex; use serde::Deserialize; +use serde_json as json; +use std::{cmp::Ordering, collections::HashMap, sync::Arc}; + +pub struct ThemeRegistry { + assets: Box, + themes: Mutex>>, + theme_data: Mutex>>>, +} #[derive(Debug, Default)] pub struct Theme { @@ -78,3 +92,287 @@ impl Default for Editor { } } } + +impl ThemeRegistry { + pub fn new(source: impl AssetSource) -> Arc { + Arc::new(Self { + assets: Box::new(source), + themes: Default::default(), + theme_data: Default::default(), + }) + } + + pub fn list(&self) -> impl Iterator { + self.assets.list("themes/").into_iter().filter_map(|path| { + let filename = path.strip_prefix("themes/")?; + let theme_name = filename.strip_suffix(".toml")?; + if theme_name.starts_with('_') { + None + } else { + Some(theme_name.to_string()) + } + }) + } + + pub fn get(&self, name: &str) -> Result> { + todo!() + // if let Some(theme) = self.themes.lock().get(name) { + // return Ok(theme.clone()); + // } + + // let theme_toml = self.load(name)?; + // let mut syntax = Vec::<(String, Color, 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, properties)); + // } + // } + // } + + // let theme = Arc::new(Theme { + // ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?, + // editor: theme::Editor::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>> { + 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_data: Map = toml::from_slice(source_code.as_ref()) + .with_context(|| format!("failed to parse {}.toml", name))?; + + // If this theme extends another base theme, deeply merge it into the base theme's data + if let Some(base_name) = theme_data + .get("extends") + .and_then(|name| name.as_str()) + .map(str::to_string) + { + let mut base_theme_data = self + .load(&base_name) + .with_context(|| format!("failed to load base theme {}", base_name))? + .as_ref() + .clone(); + deep_merge_json(&mut base_theme_data, theme_data); + theme_data = base_theme_data; + } + + // Evaluate `extends` fields in styles + let mut directives = Vec::new(); + let mut key_path = Vec::new(); + for (key, value) in theme_data.iter() { + if value.is_array() || value.is_object() { + key_path.push(Key::Object(key.clone())); + find_extensions(value, &mut key_path, &mut directives); + key_path.pop(); + } + } + directives.sort_unstable(); + for ExtendDirective { + source_path, + target_path, + } in directives + { + let source = value_at(&mut theme_data, &source_path)?.clone(); + let target = value_at(&mut theme_data, &target_path)?; + if let Value::Object(source_object) = source { + deep_merge_json(target.as_object_mut().unwrap(), source_object); + } + } + + // Evaluate any variables + if let Some((key, variables)) = theme_data.remove_entry("variables") { + if let Some(variables) = variables.as_object() { + for value in theme_data.values_mut() { + evaluate_variables(value, &variables, &mut Vec::new())?; + } + } + theme_data.insert(key, variables); + } + + let result = Arc::new(theme_data); + self.theme_data + .lock() + .insert(name.to_string(), result.clone()); + + Ok(result) + } +} + +fn deep_merge_json(base: &mut Map, extension: Map) { + for (key, extension_value) in extension { + if let Value::Object(extension_object) = extension_value { + if let Some(base_object) = base.get_mut(&key).and_then(|value| value.as_object_mut()) { + deep_merge_json(base_object, extension_object); + } else { + base.insert(key, Value::Object(extension_object)); + } + } else { + base.insert(key, extension_value); + } + } +} + +#[derive(Clone, PartialEq, Eq)] +enum Key { + Array(usize), + Object(String), +} + +#[derive(PartialEq, Eq)] +struct ExtendDirective { + source_path: Vec, + target_path: Vec, +} + +impl Ord for ExtendDirective { + fn cmp(&self, other: &Self) -> Ordering { + if self.target_path.starts_with(&other.source_path) + || other.source_path.starts_with(&self.target_path) + { + Ordering::Less + } else if other.target_path.starts_with(&self.source_path) + || self.source_path.starts_with(&other.target_path) + { + Ordering::Greater + } else { + Ordering::Equal + } + } +} + +impl PartialOrd for ExtendDirective { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +fn find_extensions(value: &Value, key_path: &mut Vec, directives: &mut Vec) { + match value { + Value::Array(vec) => { + for (ix, value) in vec.iter().enumerate() { + key_path.push(Key::Array(ix)); + find_extensions(value, key_path, directives); + key_path.pop(); + } + } + Value::Object(map) => { + for (key, value) in map.iter() { + if key == "extends" { + if let Some(source_path) = value.as_str() { + directives.push(ExtendDirective { + source_path: source_path + .split(".") + .map(|key| Key::Object(key.to_string())) + .collect(), + target_path: key_path.clone(), + }); + } + } else if value.is_array() || value.is_object() { + key_path.push(Key::Object(key.to_string())); + find_extensions(value, key_path, directives); + key_path.pop(); + } + } + } + _ => {} + } +} + +fn value_at<'a>(object: &'a mut Map, key_path: &Vec) -> Result<&'a mut Value> { + let mut key_path = key_path.iter(); + if let Some(Key::Object(first_key)) = key_path.next() { + let mut cur_value = object.get_mut(first_key); + for key in key_path { + if let Some(value) = cur_value { + match key { + Key::Array(ix) => cur_value = value.get_mut(ix), + Key::Object(key) => cur_value = value.get_mut(key), + } + } else { + return Err(anyhow!("invalid key path")); + } + } + cur_value.ok_or_else(|| anyhow!("invalid key path")) + } else { + Err(anyhow!("invalid key path")) + } +} + +fn evaluate_variables( + value: &mut Value, + variables: &Map, + stack: &mut Vec, +) -> Result<()> { + match value { + 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() { + *value = definition; + evaluate_variables(value, 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 +} From 90b51c3356a685cd25ce03fa34caec555ffd7ed7 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Aug 2021 19:35:15 -0600 Subject: [PATCH 11/23] Implement themes::ThemeRegistry::get Co-Authored-By: Max Brunsfeld --- gpui/src/fonts.rs | 6 +++ zed/src/theme.rs | 108 +++++++++++++++++++++++++--------------------- 2 files changed, 64 insertions(+), 50 deletions(-) diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index b9fcc88b8240946c2556d6747664accf633f4e2a..4a9bf0a9dfc3275e11a5566936498a2f9813b6c3 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -70,6 +70,12 @@ where Ok(json.into()) } +pub fn font_properties_from_json( + value: serde_json::Value, +) -> Result { + Ok(serde_json::from_value::(value)?.into()) +} + impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ diff --git a/zed/src/theme.rs b/zed/src/theme.rs index ca6aac18a914696a4bda31c86095dc307407b77a..60dbe10d70fd4de83e9e8783cf8dad93a7658eb0 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -2,25 +2,26 @@ use anyhow::{anyhow, Context, Result}; use gpui::{ color::Color, elements::{ContainerStyle, LabelStyle}, - fonts::Properties as FontProperties, + fonts::{font_properties_from_json, Properties as FontProperties}, AssetSource, }; use json::{Map, Value}; use parking_lot::Mutex; -use serde::Deserialize; +use serde::{de, Deserialize, Deserializer}; use serde_json as json; use std::{cmp::Ordering, collections::HashMap, sync::Arc}; pub struct ThemeRegistry { assets: Box, themes: Mutex>>, - theme_data: Mutex>>>, + theme_data: Mutex>>, } -#[derive(Debug, Default)] +#[derive(Debug, Default, Deserialize)] pub struct Theme { pub ui: Ui, pub editor: Editor, + #[serde(deserialize_with = "deserialize_syntax_theme")] pub syntax: Vec<(String, Color, FontProperties)>, } @@ -115,50 +116,19 @@ impl ThemeRegistry { } pub fn get(&self, name: &str) -> Result> { - todo!() - // if let Some(theme) = self.themes.lock().get(name) { - // return Ok(theme.clone()); - // } - - // let theme_toml = self.load(name)?; - // let mut syntax = Vec::<(String, Color, 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, properties)); - // } - // } - // } - - // let theme = Arc::new(Theme { - // ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?, - // editor: theme::Editor::deserialize(MapDeserializer::new( - // theme_toml.editor.clone().into_iter(), - // ))?, - // syntax, - // }); - - // self.themes.lock().insert(name.to_string(), theme.clone()); - // Ok(theme) + if let Some(theme) = self.themes.lock().get(name) { + return Ok(theme.clone()); + } + + let theme_data = self.load(name)?; + let theme = Arc::new(serde_json::from_value::( + theme_data.as_ref().clone(), + )?); + self.themes.lock().insert(name.to_string(), theme.clone()); + Ok(theme) } - fn load(&self, name: &str) -> Result>> { + fn load(&self, name: &str) -> Result> { if let Some(data) = self.theme_data.lock().get(name) { return Ok(data.clone()); } @@ -178,16 +148,19 @@ impl ThemeRegistry { .and_then(|name| name.as_str()) .map(str::to_string) { - let mut base_theme_data = self + let base_theme_data = self .load(&base_name) .with_context(|| format!("failed to load base theme {}", base_name))? .as_ref() .clone(); - deep_merge_json(&mut base_theme_data, theme_data); - theme_data = base_theme_data; + if let Value::Object(mut base_theme_object) = base_theme_data { + deep_merge_json(&mut base_theme_object, theme_data); + theme_data = base_theme_object; + } } // Evaluate `extends` fields in styles + // First, find the key paths of all objects with `extends` directives let mut directives = Vec::new(); let mut key_path = Vec::new(); for (key, value) in theme_data.iter() { @@ -197,7 +170,9 @@ impl ThemeRegistry { key_path.pop(); } } + // If you extend something with an extend directive, process the source's extend directive first directives.sort_unstable(); + // Now update objects to include the fields of objects they extend for ExtendDirective { source_path, target_path, @@ -220,7 +195,7 @@ impl ThemeRegistry { theme_data.insert(key, variables); } - let result = Arc::new(theme_data); + let result = Arc::new(Value::Object(theme_data)); self.theme_data .lock() .insert(name.to_string(), result.clone()); @@ -376,3 +351,36 @@ fn validate_variable_name(name: &str) -> bool { } false } + +pub fn deserialize_syntax_theme<'de, D>( + deserializer: D, +) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + let mut result = Vec::<(String, Color, FontProperties)>::new(); + + let syntax_data: Map = Deserialize::deserialize(deserializer)?; + for (key, style) in syntax_data { + 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()).map_err(de::Error::custom)?; + } + properties = font_properties_from_json(style).map_err(de::Error::custom)?; + } + _ => { + color = serde_json::from_value(style.clone()).map_err(de::Error::custom)?; + } + } + match result.binary_search_by(|(needle, _, _)| needle.cmp(&key)) { + Ok(i) | Err(i) => { + result.insert(i, (key, color, properties)); + } + } + } + + Ok(result) +} From 5761756fb429bc299e7a9409d69e4c0cc7999673 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Tue, 3 Aug 2021 19:42:39 -0600 Subject: [PATCH 12/23] Move remaining theme-related code and tests from settings mod to theme mod --- zed/src/settings.rs | 483 +------------------------------------------- zed/src/theme.rs | 261 ++++++++++++++++++++++++ 2 files changed, 266 insertions(+), 478 deletions(-) diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 42b4afa3d3e0890a6e7a427f59ee123e2933d96a..ad4bb80e006c3c1a410fe745f6189d4fd1553e06 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -1,20 +1,10 @@ -use anyhow::{anyhow, Context, Result}; -use gpui::{ - color::Color, - font_cache::{FamilyId, FontCache}, - fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}, - AssetSource, -}; -use parking_lot::Mutex; -use postage::watch; -use serde::{de::value::MapDeserializer, Deserialize}; -use serde_json::Value; -use std::{collections::HashMap, sync::Arc}; - use crate::theme; -pub use theme::Theme; +use anyhow::Result; +use gpui::font_cache::{FamilyId, FontCache}; +use postage::watch; +use std::sync::Arc; -const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); +pub use theme::{StyleId, Theme, ThemeMap, ThemeRegistry}; #[derive(Clone)] pub struct Settings { @@ -26,32 +16,6 @@ pub struct Settings { pub theme: Arc, } -pub struct ThemeRegistry { - assets: Box, - themes: Mutex>>, - theme_data: Mutex>>, -} - -#[derive(Deserialize)] -struct ThemeToml { - #[serde(default)] - extends: Option, - #[serde(default)] - variables: HashMap, - #[serde(default)] - ui: HashMap, - #[serde(default)] - editor: HashMap, - #[serde(default)] - syntax: HashMap, -} - -#[derive(Clone, Debug)] -pub struct ThemeMap(Arc<[StyleId]>); - -#[derive(Clone, Copy, Debug)] -pub struct StyleId(u32); - impl Settings { pub fn new(font_cache: &FontCache) -> Result { Self::new_with_theme(font_cache, Arc::new(Theme::default())) @@ -74,182 +38,6 @@ impl Settings { } } -impl ThemeRegistry { - pub fn new(source: impl AssetSource) -> Arc { - Arc::new(Self { - assets: Box::new(source), - themes: Default::default(), - theme_data: Default::default(), - }) - } - - pub fn list(&self) -> impl Iterator { - self.assets.list("themes/").into_iter().filter_map(|path| { - let filename = path.strip_prefix("themes/")?; - let theme_name = filename.strip_suffix(".toml")?; - if theme_name.starts_with('_') { - None - } else { - Some(theme_name.to_string()) - } - }) - } - - pub fn get(&self, name: &str) -> Result> { - if let Some(theme) = self.themes.lock().get(name) { - return Ok(theme.clone()); - } - - let theme_toml = self.load(name)?; - let mut syntax = Vec::<(String, Color, 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, properties)); - } - } - } - - let theme = Arc::new(Theme { - ui: theme::Ui::deserialize(MapDeserializer::new(theme_toml.ui.clone().into_iter()))?, - editor: theme::Editor::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> { - 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) -> (Color, FontProperties) { - self.syntax - .get(id.0 as usize) - .map_or((self.editor.text, FontProperties::new()), |entry| { - (entry.1, entry.2) - }) - } - - #[cfg(test)] - pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> { - self.syntax.get(id.0 as usize).map(|e| e.0.as_str()) - } -} - -impl ThemeMap { - pub fn new(capture_names: &[String], theme: &Theme) -> Self { - // For each capture name in the highlight query, find the longest - // key in the theme's syntax styles that matches all of the - // dot-separated components of the capture name. - ThemeMap( - capture_names - .iter() - .map(|capture_name| { - theme - .syntax - .iter() - .enumerate() - .filter_map(|(i, (key, _, _))| { - let mut len = 0; - let capture_parts = capture_name.split('.'); - for key_part in key.split('.') { - if capture_parts.clone().any(|part| part == key_part) { - len += 1; - } else { - return None; - } - } - Some((i, len)) - }) - .max_by_key(|(_, len)| *len) - .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32)) - }) - .collect(), - ) - } - - pub fn get(&self, capture_id: u32) -> StyleId { - self.0 - .get(capture_id as usize) - .copied() - .unwrap_or(DEFAULT_STYLE_ID) - } -} - -impl Default for ThemeMap { - fn default() -> Self { - Self(Arc::new([])) - } -} - -impl Default for StyleId { - fn default() -> Self { - DEFAULT_STYLE_ID - } -} - pub fn channel( font_cache: &FontCache, ) -> Result<(watch::Sender, watch::Receiver)> { @@ -265,264 +53,3 @@ pub fn channel_with_themes( themes.get("dark").expect("failed to load default theme"), )?)) } - -fn deserialize_weight(weight: Option<&Value>) -> Result { - match weight { - None => return Ok(FontWeight::NORMAL), - Some(Value::Number(number)) => { - 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), - "bold" => return Ok(FontWeight::BOLD), - "light" => return Ok(FontWeight::LIGHT), - "semibold" => return Ok(FontWeight::SEMIBOLD), - _ => {} - }, - _ => {} - } - Err(anyhow!("Invalid weight {}", weight.unwrap())) -} - -fn evaluate_variables( - expr: &mut Value, - variables: &HashMap, - stack: &mut Vec, -) -> 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, right: &HashMap) { - for (name, value) in right { - if !left.contains_key(name) { - left.insert(name.clone(), value.clone()); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_parse_simple_theme() { - let assets = TestAssets(&[( - "themes/my-theme.toml", - r#" - [ui.tab.active] - background = 0x100000 - - [editor] - background = 0x00ed00 - line_number = 0xdddddd - - [syntax] - "beta.two" = 0xAABBCC - "alpha.one" = {color = 0x112233, weight = "bold"} - "gamma.three" = {weight = "light", italic = true} - "#, - )]); - - let registry = ThemeRegistry::new(assets); - let theme = registry.get("my-theme").unwrap(); - - assert_eq!( - theme.ui.active_tab.container.background_color, - Some(Color::from_u32(0x100000ff)) - ); - assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff)); - assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff)); - assert_eq!( - theme.syntax, - &[ - ( - "alpha.one".to_string(), - Color::from_u32(0x112233ff), - *FontProperties::new().weight(FontWeight::BOLD) - ), - ( - "beta.two".to_string(), - Color::from_u32(0xaabbccff), - *FontProperties::new().weight(FontWeight::NORMAL) - ), - ( - "gamma.three".to_string(), - Color::from_u32(0x00000000), - *FontProperties::new() - .weight(FontWeight::LIGHT) - .style(FontStyle::Italic), - ), - ] - ); - } - - #[test] - fn test_parse_extended_theme() { - let assets = TestAssets(&[ - ( - "themes/_base.toml", - r#" - abstract = true - - [ui.tab] - background = 0x111111 - 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 - "#, - ), - ( - "themes/dark.toml", - r#" - extends = "_base" - - [variables] - variable_1 = 0x555555 - variable_2 = 0x666666 - "#, - ), - ]); - - let registry = ThemeRegistry::new(assets); - let theme = registry.get("light").unwrap(); - - assert_eq!( - theme.ui.tab.container.background_color, - Some(Color::from_u32(0x555555ff)) - ); - assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff)); - assert_eq!(theme.editor.background, Color::from_u32(0x666666ff)); - assert_eq!(theme.editor.text, Color::from_u32(0x444444ff)); - - assert_eq!( - registry.list().collect::>(), - &["light".to_string(), "dark".to_string()] - ); - } - - #[test] - fn test_parse_empty_theme() { - let assets = TestAssets(&[("themes/my-theme.toml", "")]); - let registry = ThemeRegistry::new(assets); - registry.get("my-theme").unwrap(); - } - - #[test] - fn test_theme_map() { - let theme = Theme { - ui: Default::default(), - editor: Default::default(), - syntax: [ - ("function", Color::from_u32(0x100000ff)), - ("function.method", Color::from_u32(0x200000ff)), - ("function.async", Color::from_u32(0x300000ff)), - ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), - ("variable.builtin", Color::from_u32(0x500000ff)), - ("variable", Color::from_u32(0x600000ff)), - ] - .iter() - .map(|e| (e.0.to_string(), e.1, FontProperties::new())) - .collect(), - }; - - let capture_names = &[ - "function.special".to_string(), - "function.async.rust".to_string(), - "variable.builtin.self".to_string(), - ]; - - let map = ThemeMap::new(capture_names, &theme); - assert_eq!(theme.syntax_style_name(map.get(0)), Some("function")); - assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async")); - assert_eq!( - theme.syntax_style_name(map.get(2)), - Some("variable.builtin") - ); - } - - struct TestAssets(&'static [(&'static str, &'static str)]); - - impl AssetSource for TestAssets { - fn load(&self, path: &str) -> Result> { - 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)) - } - } - - fn list(&self, prefix: &str) -> Vec> { - self.0 - .iter() - .copied() - .filter_map(|(path, _)| { - if path.starts_with(prefix) { - Some(path.into()) - } else { - None - } - }) - .collect() - } - } -} diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 60dbe10d70fd4de83e9e8783cf8dad93a7658eb0..0dcccc52f06558452731e1e765835558b0a740df 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -11,12 +11,20 @@ use serde::{de, Deserialize, Deserializer}; use serde_json as json; use std::{cmp::Ordering, collections::HashMap, sync::Arc}; +const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); + pub struct ThemeRegistry { assets: Box, themes: Mutex>>, theme_data: Mutex>>, } +#[derive(Clone, Debug)] +pub struct ThemeMap(Arc<[StyleId]>); + +#[derive(Clone, Copy, Debug)] +pub struct StyleId(u32); + #[derive(Debug, Default, Deserialize)] pub struct Theme { pub ui: Ui, @@ -204,6 +212,73 @@ impl ThemeRegistry { } } +impl Theme { + pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) { + self.syntax + .get(id.0 as usize) + .map_or((self.editor.text, FontProperties::new()), |entry| { + (entry.1, entry.2) + }) + } + + #[cfg(test)] + pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> { + self.syntax.get(id.0 as usize).map(|e| e.0.as_str()) + } +} + +impl ThemeMap { + pub fn new(capture_names: &[String], theme: &Theme) -> Self { + // For each capture name in the highlight query, find the longest + // key in the theme's syntax styles that matches all of the + // dot-separated components of the capture name. + ThemeMap( + capture_names + .iter() + .map(|capture_name| { + theme + .syntax + .iter() + .enumerate() + .filter_map(|(i, (key, _, _))| { + let mut len = 0; + let capture_parts = capture_name.split('.'); + for key_part in key.split('.') { + if capture_parts.clone().any(|part| part == key_part) { + len += 1; + } else { + return None; + } + } + Some((i, len)) + }) + .max_by_key(|(_, len)| *len) + .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32)) + }) + .collect(), + ) + } + + pub fn get(&self, capture_id: u32) -> StyleId { + self.0 + .get(capture_id as usize) + .copied() + .unwrap_or(DEFAULT_STYLE_ID) + } +} + +impl Default for ThemeMap { + fn default() -> Self { + Self(Arc::new([])) + } +} + +impl Default for StyleId { + fn default() -> Self { + DEFAULT_STYLE_ID + } +} + fn deep_merge_json(base: &mut Map, extension: Map) { for (key, extension_value) in extension { if let Value::Object(extension_object) = extension_value { @@ -384,3 +459,189 @@ where Ok(result) } + +#[cfg(test)] +mod tests { + use super::*; + use gpui::fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}; + + #[test] + fn test_parse_simple_theme() { + let assets = TestAssets(&[( + "themes/my-theme.toml", + r#" + [ui.tab.active] + background = 0x100000 + + [editor] + background = 0x00ed00 + line_number = 0xdddddd + + [syntax] + "beta.two" = 0xAABBCC + "alpha.one" = {color = 0x112233, weight = "bold"} + "gamma.three" = {weight = "light", italic = true} + "#, + )]); + + let registry = ThemeRegistry::new(assets); + let theme = registry.get("my-theme").unwrap(); + + assert_eq!( + theme.ui.active_tab.container.background_color, + Some(Color::from_u32(0x100000ff)) + ); + assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff)); + assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff)); + assert_eq!( + theme.syntax, + &[ + ( + "alpha.one".to_string(), + Color::from_u32(0x112233ff), + *FontProperties::new().weight(FontWeight::BOLD) + ), + ( + "beta.two".to_string(), + Color::from_u32(0xaabbccff), + *FontProperties::new().weight(FontWeight::NORMAL) + ), + ( + "gamma.three".to_string(), + Color::from_u32(0x00000000), + *FontProperties::new() + .weight(FontWeight::LIGHT) + .style(FontStyle::Italic), + ), + ] + ); + } + + #[test] + fn test_parse_extended_theme() { + let assets = TestAssets(&[ + ( + "themes/_base.toml", + r#" + abstract = true + + [ui.tab] + background = 0x111111 + 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 + "#, + ), + ( + "themes/dark.toml", + r#" + extends = "_base" + + [variables] + variable_1 = 0x555555 + variable_2 = 0x666666 + "#, + ), + ]); + + let registry = ThemeRegistry::new(assets); + let theme = registry.get("light").unwrap(); + + assert_eq!( + theme.ui.tab.container.background_color, + Some(Color::from_u32(0x555555ff)) + ); + assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff)); + assert_eq!(theme.editor.background, Color::from_u32(0x666666ff)); + assert_eq!(theme.editor.text, Color::from_u32(0x444444ff)); + + assert_eq!( + registry.list().collect::>(), + &["light".to_string(), "dark".to_string()] + ); + } + + #[test] + fn test_parse_empty_theme() { + let assets = TestAssets(&[("themes/my-theme.toml", "")]); + let registry = ThemeRegistry::new(assets); + registry.get("my-theme").unwrap(); + } + + #[test] + fn test_theme_map() { + let theme = Theme { + ui: Default::default(), + editor: Default::default(), + syntax: [ + ("function", Color::from_u32(0x100000ff)), + ("function.method", Color::from_u32(0x200000ff)), + ("function.async", Color::from_u32(0x300000ff)), + ("variable.builtin.self.rust", Color::from_u32(0x400000ff)), + ("variable.builtin", Color::from_u32(0x500000ff)), + ("variable", Color::from_u32(0x600000ff)), + ] + .iter() + .map(|e| (e.0.to_string(), e.1, FontProperties::new())) + .collect(), + }; + + let capture_names = &[ + "function.special".to_string(), + "function.async.rust".to_string(), + "variable.builtin.self".to_string(), + ]; + + let map = ThemeMap::new(capture_names, &theme); + assert_eq!(theme.syntax_style_name(map.get(0)), Some("function")); + assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async")); + assert_eq!( + theme.syntax_style_name(map.get(2)), + Some("variable.builtin") + ); + } + + struct TestAssets(&'static [(&'static str, &'static str)]); + + impl AssetSource for TestAssets { + fn load(&self, path: &str) -> Result> { + 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)) + } + } + + fn list(&self, prefix: &str) -> Vec> { + self.0 + .iter() + .copied() + .filter_map(|(path, _)| { + if path.starts_with(prefix) { + Some(path.into()) + } else { + None + } + }) + .collect() + } + } +} From 802f1f4e78c177c407dd5ddce73686faedf0896e Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Aug 2021 14:07:19 -0700 Subject: [PATCH 13/23] Get new theme structure working * Fix precedence of extends directives * Always group color with font properties for text theming Co-Authored-By: Nathan Sobo --- gpui/src/color.rs | 25 ++- gpui/src/elements/label.rs | 63 ++++--- gpui/src/fonts.rs | 96 ++++++---- zed/assets/themes/_base.toml | 35 ++-- zed/assets/themes/dark.toml | 32 ++-- zed/src/editor.rs | 14 +- zed/src/editor/buffer.rs | 16 +- zed/src/editor/display_map.rs | 30 +-- zed/src/editor/display_map/fold_map.rs | 8 +- zed/src/editor/display_map/tab_map.rs | 6 +- zed/src/editor/display_map/wrap_map.rs | 8 +- zed/src/file_finder.rs | 19 +- zed/src/language.rs | 16 +- zed/src/settings.rs | 11 +- zed/src/theme.rs | 250 ++++++++++--------------- 15 files changed, 309 insertions(+), 320 deletions(-) diff --git a/gpui/src/color.rs b/gpui/src/color.rs index 818b07111d590e126c341f4d63a55f0b392860cd..9c6de6247a62c5fb96b793c1f6332e495eb442a2 100644 --- a/gpui/src/color.rs +++ b/gpui/src/color.rs @@ -1,11 +1,15 @@ use std::{ + borrow::Cow, fmt, ops::{Deref, DerefMut}, }; use crate::json::ToJson; use pathfinder_color::ColorU; -use serde::{Deserialize, Deserializer}; +use serde::{ + de::{self, Unexpected}, + Deserialize, Deserializer, +}; use serde_json::json; #[derive(Clone, Copy, Default, PartialEq, Eq, Hash)] @@ -39,13 +43,20 @@ impl<'de> Deserialize<'de> for Color { where D: Deserializer<'de>, { - let mut rgba = u32::deserialize(deserializer)?; - - if rgba <= 0xFFFFFF { - rgba = (rgba << 8) + 0xFF; + let literal: Cow = Deserialize::deserialize(deserializer)?; + if let Some(digits) = literal.strip_prefix('#') { + if let Ok(value) = u32::from_str_radix(digits, 16) { + if digits.len() == 6 { + return Ok(Color::from_u32((value << 8) | 0xFF)); + } else if digits.len() == 8 { + return Ok(Color::from_u32(value)); + } + } } - - Ok(Self::from_u32(rgba)) + Err(de::Error::invalid_value( + Unexpected::Str(literal.as_ref()), + &"#RRGGBB[AA]", + )) } } diff --git a/gpui/src/elements/label.rs b/gpui/src/elements/label.rs index ba9b9e8c5e9ba6b480830df6e0d544349ae03ea6..72f755905cad91b179b9b9421c5feeec0a3b7c5f 100644 --- a/gpui/src/elements/label.rs +++ b/gpui/src/elements/label.rs @@ -1,7 +1,7 @@ use crate::{ color::Color, font_cache::FamilyId, - fonts::{deserialize_font_properties, deserialize_option_font_properties, FontId, Properties}, + fonts::{FontId, TextStyle}, geometry::{ rect::RectF, vector::{vec2f, Vector2F}, @@ -25,14 +25,8 @@ pub struct Label { #[derive(Clone, Debug, Default, Deserialize)] pub struct LabelStyle { - #[serde(default = "Color::black")] - pub color: Color, - #[serde(default)] - pub highlight_color: Option, - #[serde(default, deserialize_with = "deserialize_font_properties")] - pub font_properties: Properties, - #[serde(default, deserialize_with = "deserialize_option_font_properties")] - pub highlight_font_properties: Option, + pub text: TextStyle, + pub highlight_text: Option, } impl Label { @@ -52,7 +46,7 @@ impl Label { } pub fn with_default_color(mut self, color: Color) -> Self { - self.style.color = color; + self.style.text.color = color; self } @@ -67,13 +61,18 @@ impl Label { font_id: FontId, ) -> SmallVec<[(usize, FontId, Color); 8]> { if self.highlight_indices.is_empty() { - return smallvec![(self.text.len(), font_id, self.style.color)]; + return smallvec![(self.text.len(), font_id, self.style.text.color)]; } let highlight_font_id = self .style - .highlight_font_properties - .and_then(|properties| font_cache.select_font(self.family_id, &properties).ok()) + .highlight_text + .as_ref() + .and_then(|style| { + font_cache + .select_font(self.family_id, &style.font_properties) + .ok() + }) .unwrap_or(font_id); let mut highlight_indices = self.highlight_indices.iter().copied().peekable(); @@ -81,11 +80,16 @@ impl Label { for (char_ix, c) in self.text.char_indices() { let mut font_id = font_id; - let mut color = self.style.color; + let mut color = self.style.text.color; if let Some(highlight_ix) = highlight_indices.peek() { if char_ix == *highlight_ix { font_id = highlight_font_id; - color = self.style.highlight_color.unwrap_or(self.style.color); + color = self + .style + .highlight_text + .as_ref() + .unwrap_or(&self.style.text) + .color; highlight_indices.next(); } } @@ -121,7 +125,7 @@ impl Element for Label { ) -> (Vector2F, Self::LayoutState) { let font_id = cx .font_cache - .select_font(self.family_id, &self.style.font_properties) + .select_font(self.family_id, &self.style.text.font_properties) .unwrap(); let runs = self.compute_runs(&cx.font_cache, font_id); let line = @@ -185,40 +189,43 @@ impl Element for Label { impl ToJson for LabelStyle { fn to_json(&self) -> Value { json!({ - "default_color": self.color.to_json(), - "default_font_properties": self.font_properties.to_json(), - "highlight_color": self.highlight_color.to_json(), - "highlight_font_properties": self.highlight_font_properties.to_json(), + "text": self.text.to_json(), + "highlight_text": self.highlight_text + .as_ref() + .map_or(serde_json::Value::Null, |style| style.to_json()) }) } } #[cfg(test)] mod tests { - use font_kit::properties::Weight; - use super::*; + use crate::fonts::{Properties as FontProperties, Weight}; #[crate::test(self)] fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) { let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap(); let menlo_regular = cx .font_cache() - .select_font(menlo, &Properties::new()) + .select_font(menlo, &FontProperties::new()) .unwrap(); let menlo_bold = cx .font_cache() - .select_font(menlo, Properties::new().weight(Weight::BOLD)) + .select_font(menlo, FontProperties::new().weight(Weight::BOLD)) .unwrap(); let black = Color::black(); let red = Color::new(255, 0, 0, 255); let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0) .with_style(&LabelStyle { - color: black, - highlight_color: Some(red), - highlight_font_properties: Some(*Properties::new().weight(Weight::BOLD)), - ..Default::default() + text: TextStyle { + color: black, + font_properties: Default::default(), + }, + highlight_text: Some(TextStyle { + color: red, + font_properties: *FontProperties::new().weight(Weight::BOLD), + }), }) .with_highlights(vec![ ".α".len(), diff --git a/gpui/src/fonts.rs b/gpui/src/fonts.rs index 4a9bf0a9dfc3275e11a5566936498a2f9813b6c3..e9f84676e791f19fdd7badf3a7e52de277f22a99 100644 --- a/gpui/src/fonts.rs +++ b/gpui/src/fonts.rs @@ -1,15 +1,25 @@ -use crate::json::{json, ToJson}; +use crate::{ + color::Color, + json::{json, ToJson}, +}; pub use font_kit::{ metrics::Metrics, properties::{Properties, Stretch, Style, Weight}, }; -use serde::{Deserialize, Deserializer}; +use serde::{de, Deserialize}; +use serde_json::Value; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct FontId(pub usize); pub type GlyphId = u32; +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TextStyle { + pub color: Color, + pub font_properties: Properties, +} + #[allow(non_camel_case_types)] #[derive(Deserialize)] enum WeightJson { @@ -25,16 +35,53 @@ enum WeightJson { } #[derive(Deserialize)] -struct PropertiesJson { +struct TextStyleJson { + color: Color, weight: Option, #[serde(default)] italic: bool, } -impl Into for PropertiesJson { - fn into(self) -> Properties { - let mut result = Properties::new(); - result.weight = match self.weight.unwrap_or(WeightJson::normal) { +impl<'de> Deserialize<'de> for TextStyle { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let json = Value::deserialize(deserializer)?; + if json.is_object() { + let style_json: TextStyleJson = + serde_json::from_value(json).map_err(de::Error::custom)?; + Ok(style_json.into()) + } else { + Ok(Self { + color: serde_json::from_value(json).map_err(de::Error::custom)?, + font_properties: Properties::new(), + }) + } + } +} + +impl From for TextStyle { + fn from(color: Color) -> Self { + Self { + color, + font_properties: Default::default(), + } + } +} + +impl ToJson for TextStyle { + fn to_json(&self) -> Value { + json!({ + "color": self.color.to_json(), + "font_properties": self.font_properties.to_json(), + }) + } +} + +impl Into for TextStyleJson { + fn into(self) -> TextStyle { + let weight = match self.weight.unwrap_or(WeightJson::normal) { WeightJson::thin => Weight::THIN, WeightJson::extra_light => Weight::EXTRA_LIGHT, WeightJson::light => Weight::LIGHT, @@ -45,37 +92,18 @@ impl Into for PropertiesJson { WeightJson::extra_bold => Weight::EXTRA_BOLD, WeightJson::black => Weight::BLACK, }; - if self.italic { - result.style = Style::Italic; + let style = if self.italic { + Style::Italic + } else { + Style::Normal + }; + TextStyle { + color: self.color, + font_properties: *Properties::new().weight(weight).style(style), } - result } } -pub fn deserialize_option_font_properties<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let json: Option = Deserialize::deserialize(deserializer)?; - Ok(json.map(Into::into)) -} - -pub fn deserialize_font_properties<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let json: PropertiesJson = Deserialize::deserialize(deserializer)?; - Ok(json.into()) -} - -pub fn font_properties_from_json( - value: serde_json::Value, -) -> Result { - Ok(serde_json::from_value::(value)?.into()) -} - impl ToJson for Properties { fn to_json(&self) -> crate::json::Value { json!({ diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index fb73a3830df27e879e2c85537882b64d3bfe0e6a..1b646b40a204d6f94d7eb1d615ddbf644fa0d87c 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -3,34 +3,35 @@ background = "$elevation_1" [ui.tab] background = "$elevation_2" -color = "$text_dull" -border.color = 0x000000 -icon_close = 0x383839 -icon_dirty = 0x556de8 -icon_conflict = 0xe45349 +text = "$text_dull" +border.color = "#000000" +icon_close = "#383839" +icon_dirty = "#556de8" +icon_conflict = "#e45349" [ui.active_tab] -extends = ".." +extends = "ui.tab" background = "$elevation_3" -color = "$text_bright" +text = "$text_bright" [ui.selector] background = "$elevation_4" +text = "$text_bright" padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } margin.top = 12.0 corner_radius = 6.0 -shadow = { offset = [0.0, 0.0], blur = 12.0, color = 0x00000088 } +shadow = { offset = [0.0, 0.0], blur = 12.0, color = "#00000088" } [ui.selector.item] -background = 0x424344 -text = 0xcccccc -highlight_text = 0x18a3ff -highlight_font_properties = { weight = "bold" } -border = { color = 0x000000, width = 1.0 } +background = "#424344" +text = "#cccccc" +highlight_text = { color = "#18a3ff", weight = "bold" } +border = { color = "#000000", width = 1.0 } +padding = { top = 6.0, bottom = 6.0, left = 6.0, right = 6.0 } [ui.selector.active_item] -extends = ".." -background = 0x094771 +extends = "ui.selector.item" +background = "#094771" [editor] background = "$elevation_3" @@ -40,6 +41,6 @@ line_number = "$text_dull" line_number_active = "$text_bright" text = "$text_normal" replicas = [ - { selection = 0x264f78, cursor = "$text_bright" }, - { selection = 0x504f31, cursor = 0xfcf154 }, + { selection = "#264f78", cursor = "$text_bright" }, + { selection = "#504f31", cursor = "#fcf154" }, ] diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index fdf5a5adeecfd7a0988cb2036a67c0c74239cd2e..5db98bf091bcfd6167c76e75e0917cc90afca73b 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -1,21 +1,21 @@ extends = "_base" [variables] -elevation_1 = 0x050101 -elevation_2 = 0x131415 -elevation_3 = 0x1c1d1e -elevation_4 = 0x3a3b3c -text_dull = 0x5a5a5b -text_bright = 0xffffff -text_normal = 0xd4d4d4 +elevation_1 = "#050101" +elevation_2 = "#131415" +elevation_3 = "#1c1d1e" +elevation_4 = "#3a3b3c" +text_dull = "#5a5a5b" +text_bright = "#ffffff" +text_normal = "#d4d4d4" [syntax] -keyword = 0xc586c0 -function = 0xdcdcaa -string = 0xcb8f77 -type = 0x4ec9b0 -number = 0xb5cea8 -comment = 0x6a9955 -property = 0x4e94ce -variant = 0x4fc1ff -constant = 0x9cdcfe +keyword = { color = "#c586c0", weight = "bold" } +function = "#dcdcaa" +string = "#cb8f77" +type = "#4ec9b0" +number = "#b5cea8" +comment = "#6a9955" +property = "#4e94ce" +variant = "#4fc1ff" +constant = "#9cdcfe" diff --git a/zed/src/editor.rs b/zed/src/editor.rs index 7283e8f6b458d3ff792b1d28a6e32f733ad61ca8..d5507f38c9a17b2be23a38e24c0c3182dd3809f1 100644 --- a/zed/src/editor.rs +++ b/zed/src/editor.rs @@ -4,7 +4,7 @@ mod element; pub mod movement; use crate::{ - settings::{Settings, StyleId, Theme}, + settings::{HighlightId, Settings, Theme}, time::ReplicaId, util::{post_inc, Bias}, workspace, @@ -2419,7 +2419,7 @@ impl Snapshot { .display_snapshot .highlighted_chunks_for_rows(rows.clone()); - 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", StyleId::default()))) { + 'outer: for (chunk, style_ix) in chunks.chain(Some(("\n", HighlightId::default()))) { for (ix, mut line_chunk) in chunk.split('\n').enumerate() { if ix > 0 { layouts.push(layout_cache.layout_str(&line, self.font_size, &styles)); @@ -2433,12 +2433,12 @@ impl Snapshot { } if !line_chunk.is_empty() && !line_exceeded_max_len { - let (color, font_properties) = self.theme.syntax_style(style_ix); + let style = self.theme.highlight_style(style_ix); // Avoid a lookup if the font properties match the previous ones. - let font_id = if font_properties == prev_font_properties { + let font_id = if style.font_properties == prev_font_properties { prev_font_id } else { - font_cache.select_font(self.font_family, &font_properties)? + font_cache.select_font(self.font_family, &style.font_properties)? }; if line.len() + line_chunk.len() > MAX_LINE_LEN { @@ -2451,9 +2451,9 @@ impl Snapshot { } line.push_str(line_chunk); - styles.push((line_chunk.len(), font_id, color)); + styles.push((line_chunk.len(), font_id, style.color)); prev_font_id = font_id; - prev_font_properties = font_properties; + prev_font_properties = style.font_properties; } } } diff --git a/zed/src/editor/buffer.rs b/zed/src/editor/buffer.rs index 3a3876d808696f94a62d5ba7eb19e5d6f3dc3fc0..9b1f169e48c71a10e37cb60ae12ef7e6a0e2d49b 100644 --- a/zed/src/editor/buffer.rs +++ b/zed/src/editor/buffer.rs @@ -16,7 +16,7 @@ use zrpc::proto; use crate::{ language::{Language, Tree}, operation_queue::{self, OperationQueue}, - settings::{StyleId, ThemeMap}, + settings::{HighlightId, HighlightMap}, sum_tree::{self, FilterCursor, SumTree}, time::{self, ReplicaId}, util::Bias, @@ -1985,7 +1985,7 @@ impl Snapshot { captures, next_capture: None, stack: Default::default(), - theme_mapping: language.theme_mapping(), + highlight_map: language.highlight_map(), }), } } else { @@ -2316,8 +2316,8 @@ impl<'a> tree_sitter::TextProvider<'a> for TextProvider<'a> { struct Highlights<'a> { captures: tree_sitter::QueryCaptures<'a, 'a, TextProvider<'a>>, next_capture: Option<(tree_sitter::QueryMatch<'a, 'a>, usize)>, - stack: Vec<(usize, StyleId)>, - theme_mapping: ThemeMap, + stack: Vec<(usize, HighlightId)>, + highlight_map: HighlightMap, } pub struct HighlightedChunks<'a> { @@ -2341,7 +2341,7 @@ impl<'a> HighlightedChunks<'a> { if offset < next_capture_end { highlights.stack.push(( next_capture_end, - highlights.theme_mapping.get(capture.index), + highlights.highlight_map.get(capture.index), )); } highlights.next_capture.take(); @@ -2357,7 +2357,7 @@ impl<'a> HighlightedChunks<'a> { } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { let mut next_capture_start = usize::MAX; @@ -2381,7 +2381,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { next_capture_start = capture.node.start_byte(); break; } else { - let style_id = highlights.theme_mapping.get(capture.index); + let style_id = highlights.highlight_map.get(capture.index); highlights.stack.push((capture.node.end_byte(), style_id)); highlights.next_capture = highlights.captures.next(); } @@ -2391,7 +2391,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { if let Some(chunk) = self.chunks.peek() { let chunk_start = self.range.start; let mut chunk_end = (self.chunks.offset() + chunk.len()).min(next_capture_start); - let mut style_id = StyleId::default(); + let mut style_id = HighlightId::default(); if let Some((parent_capture_end, parent_style_id)) = self.highlights.as_ref().and_then(|h| h.stack.last()) { diff --git a/zed/src/editor/display_map.rs b/zed/src/editor/display_map.rs index 67246bd983575ebc8057201652bebd66099bddc1..ab97179a6b2ffaf28143a83d1b20cbe2ccf6a796 100644 --- a/zed/src/editor/display_map.rs +++ b/zed/src/editor/display_map.rs @@ -654,16 +654,8 @@ mod tests { .unwrap(); let theme = Theme { syntax: vec![ - ( - "mod.body".to_string(), - Color::from_u32(0xff0000ff), - Default::default(), - ), - ( - "fn.name".to_string(), - Color::from_u32(0x00ff00ff), - Default::default(), - ), + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), ], ..Default::default() }; @@ -676,7 +668,7 @@ mod tests { grammar: grammar.clone(), highlight_query, brackets_query: tree_sitter::Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }); lang.set_theme(&theme); @@ -752,16 +744,8 @@ mod tests { .unwrap(); let theme = Theme { syntax: vec![ - ( - "mod.body".to_string(), - Color::from_u32(0xff0000ff), - Default::default(), - ), - ( - "fn.name".to_string(), - Color::from_u32(0x00ff00ff), - Default::default(), - ), + ("mod.body".to_string(), Color::from_u32(0xff0000ff).into()), + ("fn.name".to_string(), Color::from_u32(0x00ff00ff).into()), ], ..Default::default() }; @@ -774,7 +758,7 @@ mod tests { grammar: grammar.clone(), highlight_query, brackets_query: tree_sitter::Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }); lang.set_theme(&theme); @@ -953,7 +937,7 @@ mod tests { let mut snapshot = map.update(cx, |map, cx| map.snapshot(cx)); let mut chunks: Vec<(String, Option<&str>)> = Vec::new(); for (chunk, style_id) in snapshot.highlighted_chunks_for_rows(rows) { - let style_name = theme.syntax_style_name(style_id); + let style_name = theme.highlight_name(style_id); if let Some((last_chunk, last_style_name)) = chunks.last_mut() { if style_name == *last_style_name { last_chunk.push_str(chunk); diff --git a/zed/src/editor/display_map/fold_map.rs b/zed/src/editor/display_map/fold_map.rs index b8ab39cce169e86c5ea4e83588d42aceaf005b8f..3bfa1f3d240429e74f7ed9532dca9bf8eda04be2 100644 --- a/zed/src/editor/display_map/fold_map.rs +++ b/zed/src/editor/display_map/fold_map.rs @@ -4,7 +4,7 @@ use super::{ }; use crate::{ editor::buffer, - settings::StyleId, + settings::HighlightId, sum_tree::{self, Cursor, FilterCursor, SumTree}, time, util::Bias, @@ -1004,12 +1004,12 @@ impl<'a> Iterator for Chunks<'a> { pub struct HighlightedChunks<'a> { transform_cursor: Cursor<'a, Transform, FoldOffset, usize>, buffer_chunks: buffer::HighlightedChunks<'a>, - buffer_chunk: Option<(usize, &'a str, StyleId)>, + buffer_chunk: Option<(usize, &'a str, HighlightId)>, buffer_offset: usize, } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { let transform = if let Some(item) = self.transform_cursor.item() { @@ -1031,7 +1031,7 @@ impl<'a> Iterator for HighlightedChunks<'a> { self.transform_cursor.next(&()); } - return Some((output_text, StyleId::default())); + return Some((output_text, HighlightId::default())); } // Retrieve a chunk from the current location in the buffer. diff --git a/zed/src/editor/display_map/tab_map.rs b/zed/src/editor/display_map/tab_map.rs index 99257c847fa686e3e3e47f5ae5d754e04a815bc7..299320ca321adb9e197ddfc5f4295b69aaacdd4a 100644 --- a/zed/src/editor/display_map/tab_map.rs +++ b/zed/src/editor/display_map/tab_map.rs @@ -1,7 +1,7 @@ use parking_lot::Mutex; use super::fold_map::{self, FoldEdit, FoldPoint, Snapshot as FoldSnapshot}; -use crate::{editor::rope, settings::StyleId, util::Bias}; +use crate::{editor::rope, settings::HighlightId, util::Bias}; use std::{mem, ops::Range}; pub struct TabMap(Mutex); @@ -416,14 +416,14 @@ impl<'a> Iterator for Chunks<'a> { pub struct HighlightedChunks<'a> { fold_chunks: fold_map::HighlightedChunks<'a>, chunk: &'a str, - style_id: StyleId, + style_id: HighlightId, column: usize, tab_size: usize, skip_leading_tab: bool, } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { if self.chunk.is_empty() { diff --git a/zed/src/editor/display_map/wrap_map.rs b/zed/src/editor/display_map/wrap_map.rs index 18d6dbba6afe2cecdd0eb021015947e02324b424..358730748b89173c361decf4bbe9db23864da7d3 100644 --- a/zed/src/editor/display_map/wrap_map.rs +++ b/zed/src/editor/display_map/wrap_map.rs @@ -5,7 +5,7 @@ use super::{ }; use crate::{ editor::Point, - settings::StyleId, + settings::HighlightId, sum_tree::{self, Cursor, SumTree}, util::Bias, Settings, @@ -59,7 +59,7 @@ pub struct Chunks<'a> { pub struct HighlightedChunks<'a> { input_chunks: tab_map::HighlightedChunks<'a>, input_chunk: &'a str, - style_id: StyleId, + style_id: HighlightId, output_position: WrapPoint, max_output_row: u32, transforms: Cursor<'a, Transform, WrapPoint, TabPoint>, @@ -487,7 +487,7 @@ impl Snapshot { HighlightedChunks { input_chunks: self.tab_snapshot.highlighted_chunks(input_start..input_end), input_chunk: "", - style_id: StyleId::default(), + style_id: HighlightId::default(), output_position: output_start, max_output_row: rows.end, transforms, @@ -670,7 +670,7 @@ impl<'a> Iterator for Chunks<'a> { } impl<'a> Iterator for HighlightedChunks<'a> { - type Item = (&'a str, StyleId); + type Item = (&'a str, HighlightId); fn next(&mut self) -> Option { if self.output_position.row() >= self.max_output_row { diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 3bd1e54c8242aaf9305661c192cdd9405d9a29fa..8e2e0bfa578e19329b40c92a2420186da445db86 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -141,11 +141,15 @@ impl FileFinder { index: usize, cx: &AppContext, ) -> Option { + let selected_index = self.selected_index(); let settings = self.settings.borrow(); - let theme = &settings.theme.ui; + let style = if index == selected_index { + &settings.theme.ui.selector.active_item + } else { + &settings.theme.ui.selector.item + }; self.labels_for_match(path_match, cx).map( |(file_name, file_name_positions, full_path, full_path_positions)| { - let selected_index = self.selected_index(); let container = Container::new( Flex::row() .with_child( @@ -170,7 +174,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&theme.selector.label) + .with_style(&style.label) .with_highlights(file_name_positions) .boxed(), ) @@ -180,7 +184,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_style(&theme.selector.label) + .with_style(&style.label) .with_highlights(full_path_positions) .boxed(), ) @@ -190,12 +194,7 @@ impl FileFinder { ) .boxed(), ) - .with_uniform_padding(6.0) - .with_style(if index == selected_index { - &theme.selector.active_item.container - } else { - &theme.selector.item.container - }); + .with_style(&style.container); let entry = (path_match.tree_id, path_match.path.clone()); EventHandler::new(container.boxed()) diff --git a/zed/src/language.rs b/zed/src/language.rs index 7d286dfbe3e9ae3fbf2df2fd671550885e517455..886befe595e551befa25c78348ef75f6c5db1cf2 100644 --- a/zed/src/language.rs +++ b/zed/src/language.rs @@ -1,4 +1,4 @@ -use crate::settings::{Theme, ThemeMap}; +use crate::settings::{HighlightMap, Theme}; use parking_lot::Mutex; use rust_embed::RustEmbed; use serde::Deserialize; @@ -27,7 +27,7 @@ pub struct Language { pub grammar: Grammar, pub highlight_query: Query, pub brackets_query: Query, - pub theme_mapping: Mutex, + pub highlight_map: Mutex, } pub struct LanguageRegistry { @@ -35,12 +35,12 @@ pub struct LanguageRegistry { } impl Language { - pub fn theme_mapping(&self) -> ThemeMap { - self.theme_mapping.lock().clone() + pub fn highlight_map(&self) -> HighlightMap { + self.highlight_map.lock().clone() } pub fn set_theme(&self, theme: &Theme) { - *self.theme_mapping.lock() = ThemeMap::new(self.highlight_query.capture_names(), theme); + *self.highlight_map.lock() = HighlightMap::new(self.highlight_query.capture_names(), theme); } } @@ -53,7 +53,7 @@ impl LanguageRegistry { grammar, highlight_query: Self::load_query(grammar, "rust/highlights.scm"), brackets_query: Self::load_query(grammar, "rust/brackets.scm"), - theme_mapping: Mutex::new(ThemeMap::default()), + highlight_map: Mutex::new(HighlightMap::default()), }; Self { @@ -114,7 +114,7 @@ mod tests { grammar, highlight_query: Query::new(grammar, "").unwrap(), brackets_query: Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }), Arc::new(Language { config: LanguageConfig { @@ -125,7 +125,7 @@ mod tests { grammar, highlight_query: Query::new(grammar, "").unwrap(), brackets_query: Query::new(grammar, "").unwrap(), - theme_mapping: Default::default(), + highlight_map: Default::default(), }), ], }; diff --git a/zed/src/settings.rs b/zed/src/settings.rs index ad4bb80e006c3c1a410fe745f6189d4fd1553e06..81ae3c9e2ac2a7095a64e4da01c0ba02d8dff6df 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -4,7 +4,7 @@ use gpui::font_cache::{FamilyId, FontCache}; use postage::watch; use std::sync::Arc; -pub use theme::{StyleId, Theme, ThemeMap, ThemeRegistry}; +pub use theme::{HighlightId, HighlightMap, Theme, ThemeRegistry}; #[derive(Clone)] pub struct Settings { @@ -48,8 +48,13 @@ pub fn channel_with_themes( font_cache: &FontCache, themes: &ThemeRegistry, ) -> Result<(watch::Sender, watch::Receiver)> { + let theme = match themes.get("dark") { + Ok(theme) => dbg!(theme), + Err(err) => { + panic!("failed to deserialize default theme: {:?}", err) + } + }; Ok(watch::channel_with(Settings::new_with_theme( - font_cache, - themes.get("dark").expect("failed to load default theme"), + font_cache, theme, )?)) } diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 0dcccc52f06558452731e1e765835558b0a740df..7334ef7862a96fd5e501028ab644e85bea54e888 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -2,16 +2,16 @@ use anyhow::{anyhow, Context, Result}; use gpui::{ color::Color, elements::{ContainerStyle, LabelStyle}, - fonts::{font_properties_from_json, Properties as FontProperties}, + fonts::TextStyle, AssetSource, }; use json::{Map, Value}; use parking_lot::Mutex; -use serde::{de, Deserialize, Deserializer}; +use serde::{Deserialize, Deserializer}; use serde_json as json; use std::{cmp::Ordering, collections::HashMap, sync::Arc}; -const DEFAULT_STYLE_ID: StyleId = StyleId(u32::MAX); +const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); pub struct ThemeRegistry { assets: Box, @@ -20,17 +20,17 @@ pub struct ThemeRegistry { } #[derive(Clone, Debug)] -pub struct ThemeMap(Arc<[StyleId]>); +pub struct HighlightMap(Arc<[HighlightId]>); #[derive(Clone, Copy, Debug)] -pub struct StyleId(u32); +pub struct HighlightId(u32); #[derive(Debug, Default, Deserialize)] pub struct Theme { pub ui: Ui, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] - pub syntax: Vec<(String, Color, FontProperties)>, + pub syntax: Vec<(String, TextStyle)>, } #[derive(Debug, Default, Deserialize)] @@ -180,6 +180,7 @@ impl ThemeRegistry { } // If you extend something with an extend directive, process the source's extend directive first directives.sort_unstable(); + // Now update objects to include the fields of objects they extend for ExtendDirective { source_path, @@ -188,8 +189,11 @@ impl ThemeRegistry { { let source = value_at(&mut theme_data, &source_path)?.clone(); let target = value_at(&mut theme_data, &target_path)?; - if let Value::Object(source_object) = source { - deep_merge_json(target.as_object_mut().unwrap(), source_object); + if let (Value::Object(mut source_object), Value::Object(target_object)) = + (source, target.take()) + { + deep_merge_json(&mut source_object, target_object); + *target = Value::Object(source_object); } } @@ -213,26 +217,28 @@ impl ThemeRegistry { } impl Theme { - pub fn syntax_style(&self, id: StyleId) -> (Color, FontProperties) { + pub fn highlight_style(&self, id: HighlightId) -> TextStyle { self.syntax .get(id.0 as usize) - .map_or((self.editor.text, FontProperties::new()), |entry| { - (entry.1, entry.2) + .map(|entry| entry.1.clone()) + .unwrap_or_else(|| TextStyle { + color: self.editor.text, + font_properties: Default::default(), }) } #[cfg(test)] - pub fn syntax_style_name(&self, id: StyleId) -> Option<&str> { + pub fn highlight_name(&self, id: HighlightId) -> Option<&str> { self.syntax.get(id.0 as usize).map(|e| e.0.as_str()) } } -impl ThemeMap { +impl HighlightMap { pub fn new(capture_names: &[String], theme: &Theme) -> Self { // For each capture name in the highlight query, find the longest // key in the theme's syntax styles that matches all of the // dot-separated components of the capture name. - ThemeMap( + HighlightMap( capture_names .iter() .map(|capture_name| { @@ -240,7 +246,7 @@ impl ThemeMap { .syntax .iter() .enumerate() - .filter_map(|(i, (key, _, _))| { + .filter_map(|(i, (key, _))| { let mut len = 0; let capture_parts = capture_name.split('.'); for key_part in key.split('.') { @@ -253,29 +259,29 @@ impl ThemeMap { Some((i, len)) }) .max_by_key(|(_, len)| *len) - .map_or(DEFAULT_STYLE_ID, |(i, _)| StyleId(i as u32)) + .map_or(DEFAULT_HIGHLIGHT_ID, |(i, _)| HighlightId(i as u32)) }) .collect(), ) } - pub fn get(&self, capture_id: u32) -> StyleId { + pub fn get(&self, capture_id: u32) -> HighlightId { self.0 .get(capture_id as usize) .copied() - .unwrap_or(DEFAULT_STYLE_ID) + .unwrap_or(DEFAULT_HIGHLIGHT_ID) } } -impl Default for ThemeMap { +impl Default for HighlightMap { fn default() -> Self { Self(Arc::new([])) } } -impl Default for StyleId { +impl Default for HighlightId { fn default() -> Self { - DEFAULT_STYLE_ID + DEFAULT_HIGHLIGHT_ID } } @@ -293,13 +299,13 @@ fn deep_merge_json(base: &mut Map, extension: Map) } } -#[derive(Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] enum Key { Array(usize), Object(String), } -#[derive(PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] struct ExtendDirective { source_path: Vec, target_path: Vec, @@ -429,30 +435,17 @@ fn validate_variable_name(name: &str) -> bool { pub fn deserialize_syntax_theme<'de, D>( deserializer: D, -) -> Result, D::Error> +) -> Result, D::Error> where D: Deserializer<'de>, { - let mut result = Vec::<(String, Color, FontProperties)>::new(); + let mut result = Vec::<(String, TextStyle)>::new(); - let syntax_data: Map = Deserialize::deserialize(deserializer)?; + let syntax_data: HashMap = Deserialize::deserialize(deserializer)?; for (key, style) in syntax_data { - 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()).map_err(de::Error::custom)?; - } - properties = font_properties_from_json(style).map_err(de::Error::custom)?; - } - _ => { - color = serde_json::from_value(style.clone()).map_err(de::Error::custom)?; - } - } - match result.binary_search_by(|(needle, _, _)| needle.cmp(&key)) { + match result.binary_search_by(|(needle, _)| needle.cmp(&key)) { Ok(i) | Err(i) => { - result.insert(i, (key, color, properties)); + result.insert(i, (key, style)); } } } @@ -463,131 +456,95 @@ where #[cfg(test)] mod tests { use super::*; - use gpui::fonts::{Properties as FontProperties, Style as FontStyle, Weight as FontWeight}; - - #[test] - fn test_parse_simple_theme() { - let assets = TestAssets(&[( - "themes/my-theme.toml", - r#" - [ui.tab.active] - background = 0x100000 - - [editor] - background = 0x00ed00 - line_number = 0xdddddd - - [syntax] - "beta.two" = 0xAABBCC - "alpha.one" = {color = 0x112233, weight = "bold"} - "gamma.three" = {weight = "light", italic = true} - "#, - )]); - - let registry = ThemeRegistry::new(assets); - let theme = registry.get("my-theme").unwrap(); - - assert_eq!( - theme.ui.active_tab.container.background_color, - Some(Color::from_u32(0x100000ff)) - ); - assert_eq!(theme.editor.background, Color::from_u32(0x00ed00ff)); - assert_eq!(theme.editor.line_number, Color::from_u32(0xddddddff)); - assert_eq!( - theme.syntax, - &[ - ( - "alpha.one".to_string(), - Color::from_u32(0x112233ff), - *FontProperties::new().weight(FontWeight::BOLD) - ), - ( - "beta.two".to_string(), - Color::from_u32(0xaabbccff), - *FontProperties::new().weight(FontWeight::NORMAL) - ), - ( - "gamma.three".to_string(), - Color::from_u32(0x00000000), - *FontProperties::new() - .weight(FontWeight::LIGHT) - .style(FontStyle::Italic), - ), - ] - ); - } #[test] - fn test_parse_extended_theme() { + fn test_theme_extension() { let assets = TestAssets(&[ ( "themes/_base.toml", - r#" - abstract = true + r##" + [ui.active_tab] + extends = "ui.tab" + border.color = "#666666" + text = "$bright_text" [ui.tab] - background = 0x111111 - text = "$variable_1" + extends = "ui.element" + text = "$dull_text" + + [ui.element] + background = "#111111" + border = {width = 2.0, color = "#00000000"} [editor] - background = 0x222222 - default_text = "$variable_2" - "#, + background = "#222222" + default_text = "$regular_text" + "##, ), ( "themes/light.toml", - r#" + r##" extends = "_base" [variables] - variable_1 = 0x333333 - variable_2 = 0x444444 - - [ui.tab] - background = 0x555555 + bright_text = "#ffffff" + regular_text = "#eeeeee" + dull_text = "#dddddd" [editor] - background = 0x666666 - "#, - ), - ( - "themes/dark.toml", - r#" - extends = "_base" - - [variables] - variable_1 = 0x555555 - variable_2 = 0x666666 - "#, + background = "#232323" + "##, ), ]); let registry = ThemeRegistry::new(assets); - let theme = registry.get("light").unwrap(); - - assert_eq!( - theme.ui.tab.container.background_color, - Some(Color::from_u32(0x555555ff)) - ); - assert_eq!(theme.ui.tab.label.color, Color::from_u32(0x333333ff)); - assert_eq!(theme.editor.background, Color::from_u32(0x666666ff)); - assert_eq!(theme.editor.text, Color::from_u32(0x444444ff)); - + let theme_data = registry.load("light").unwrap(); assert_eq!( - registry.list().collect::>(), - &["light".to_string(), "dark".to_string()] + theme_data.as_ref(), + &serde_json::json!({ + "ui": { + "active_tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#666666" + }, + "extends": "ui.tab", + "text": "#ffffff" + }, + "tab": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + }, + "extends": "ui.element", + "text": "#dddddd" + }, + "element": { + "background": "#111111", + "border": { + "width": 2.0, + "color": "#00000000" + } + } + }, + "editor": { + "background": "#232323", + "default_text": "#eeeeee" + }, + "extends": "_base", + "variables": { + "bright_text": "#ffffff", + "regular_text": "#eeeeee", + "dull_text": "#dddddd" + } + }) ); } #[test] - fn test_parse_empty_theme() { - let assets = TestAssets(&[("themes/my-theme.toml", "")]); - let registry = ThemeRegistry::new(assets); - registry.get("my-theme").unwrap(); - } - - #[test] - fn test_theme_map() { + fn test_highlight_map() { let theme = Theme { ui: Default::default(), editor: Default::default(), @@ -600,7 +557,7 @@ mod tests { ("variable", Color::from_u32(0x600000ff)), ] .iter() - .map(|e| (e.0.to_string(), e.1, FontProperties::new())) + .map(|(name, color)| (name.to_string(), (*color).into())) .collect(), }; @@ -610,13 +567,10 @@ mod tests { "variable.builtin.self".to_string(), ]; - let map = ThemeMap::new(capture_names, &theme); - assert_eq!(theme.syntax_style_name(map.get(0)), Some("function")); - assert_eq!(theme.syntax_style_name(map.get(1)), Some("function.async")); - assert_eq!( - theme.syntax_style_name(map.get(2)), - Some("variable.builtin") - ); + let map = HighlightMap::new(capture_names, &theme); + assert_eq!(theme.highlight_name(map.get(0)), Some("function")); + assert_eq!(theme.highlight_name(map.get(1)), Some("function.async")); + assert_eq!(theme.highlight_name(map.get(2)), Some("variable.builtin")); } struct TestAssets(&'static [(&'static str, &'static str)]); From 8238c87481b6364be6abc3612d9f54038b124423 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Aug 2021 14:16:28 -0700 Subject: [PATCH 14/23] Test bundled themes and store names on themes Co-Authored-By: Nathan Sobo --- zed/assets/themes/light.toml | 32 ++++++++++++++++---------------- zed/src/settings.rs | 4 ++-- zed/src/theme.rs | 26 +++++++++++++++++++++++--- 3 files changed, 41 insertions(+), 21 deletions(-) diff --git a/zed/assets/themes/light.toml b/zed/assets/themes/light.toml index 77757ac26089b0b87603e62b3e05d2042d0ebc10..ba7ec9915a6fbf054939dc7431292acd13d8d408 100644 --- a/zed/assets/themes/light.toml +++ b/zed/assets/themes/light.toml @@ -1,21 +1,21 @@ extends = "_base" [variables] -elevation_1 = 0xffffff -elevation_2 = 0xf3f3f3 -elevation_3 = 0xececec -elevation_4 = 0x3a3b3c -text_dull = 0xacacac -text_bright = 0x111111 -text_normal = 0x333333 +elevation_1 = "#ffffff" +elevation_2 = "#f3f3f3" +elevation_3 = "#ececec" +elevation_4 = "#3a3b3c" +text_dull = "#acacac" +text_bright = "#111111" +text_normal = "#333333" [syntax] -keyword = 0x0000fa -function = 0x795e26 -string = 0xa82121 -type = 0x267f29 -number = 0xb5cea8 -comment = 0x6a9955 -property = 0x4e94ce -variant = 0x4fc1ff -constant = 0x9cdcfe +keyword = "#0000fa" +function = "#795e26" +string = "#a82121" +type = "#267f29" +number = "#b5cea8" +comment = "#6a9955" +property = "#4e94ce" +variant = "#4fc1ff" +constant = "#9cdcfe" diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 81ae3c9e2ac2a7095a64e4da01c0ba02d8dff6df..65dc04190d30e907c1fd03cc71a2d8d5f4adecd9 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -1,4 +1,4 @@ -use crate::theme; +use crate::theme::{self, DEFAULT_THEME_NAME}; use anyhow::Result; use gpui::font_cache::{FamilyId, FontCache}; use postage::watch; @@ -48,7 +48,7 @@ pub fn channel_with_themes( font_cache: &FontCache, themes: &ThemeRegistry, ) -> Result<(watch::Sender, watch::Receiver)> { - let theme = match themes.get("dark") { + let theme = match themes.get(DEFAULT_THEME_NAME) { Ok(theme) => dbg!(theme), Err(err) => { panic!("failed to deserialize default theme: {:?}", err) diff --git a/zed/src/theme.rs b/zed/src/theme.rs index 7334ef7862a96fd5e501028ab644e85bea54e888..f5c01b2759ebb2ff405590a136496ac4689fcdd7 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -12,6 +12,7 @@ use serde_json as json; use std::{cmp::Ordering, collections::HashMap, sync::Arc}; const DEFAULT_HIGHLIGHT_ID: HighlightId = HighlightId(u32::MAX); +pub const DEFAULT_THEME_NAME: &'static str = "dark"; pub struct ThemeRegistry { assets: Box, @@ -27,6 +28,8 @@ pub struct HighlightId(u32); #[derive(Debug, Default, Deserialize)] pub struct Theme { + #[serde(default)] + pub name: String, pub ui: Ui, pub editor: Editor, #[serde(deserialize_with = "deserialize_syntax_theme")] @@ -129,9 +132,9 @@ impl ThemeRegistry { } let theme_data = self.load(name)?; - let theme = Arc::new(serde_json::from_value::( - theme_data.as_ref().clone(), - )?); + let mut theme = serde_json::from_value::(theme_data.as_ref().clone())?; + theme.name = name.into(); + let theme = Arc::new(theme); self.themes.lock().insert(name.to_string(), theme.clone()); Ok(theme) } @@ -455,8 +458,24 @@ where #[cfg(test)] mod tests { + use crate::assets::Assets; + use super::*; + #[test] + fn test_bundled_themes() { + let registry = ThemeRegistry::new(Assets); + let mut has_default_theme = false; + for theme_name in registry.list() { + let theme = registry.get(&theme_name).unwrap(); + if theme.name == DEFAULT_THEME_NAME { + has_default_theme = true; + } + assert_eq!(theme.name, theme_name); + } + assert!(has_default_theme); + } + #[test] fn test_theme_extension() { let assets = TestAssets(&[ @@ -546,6 +565,7 @@ mod tests { #[test] fn test_highlight_map() { let theme = Theme { + name: "test".into(), ui: Default::default(), editor: Default::default(), syntax: [ From 4d947580b1fd786301e7ba18b0f3b5cefa366ed8 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 15:52:23 -0600 Subject: [PATCH 15/23] Reload current theme on cmd-k shift-T Co-Authored-By: Max Brunsfeld --- zed/src/lib.rs | 2 +- zed/src/main.rs | 2 +- zed/src/test.rs | 2 +- zed/src/theme.rs | 5 +++++ zed/src/theme_selector.rs | 32 ++++++++++++++++++++------------ 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 0236fdd0330a00f521f15e2b6dcabdd4f5a47c0e..24196a42226caf68640f246b88a612ebc77a4327 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -19,7 +19,7 @@ pub mod worktree; pub use settings::Settings; -use futures::lock::Mutex; +use parking_lot::Mutex; use postage::watch; use std::sync::Arc; use zrpc::ForegroundRouter; diff --git a/zed/src/main.rs b/zed/src/main.rs index 84a249b2f70323ea9b417af60ae1b9a9be4b2d3f..b488dbe95b5c9947fec80bc76e430203657dcd52 100644 --- a/zed/src/main.rs +++ b/zed/src/main.rs @@ -2,8 +2,8 @@ #![allow(non_snake_case)] use fs::OpenOptions; -use futures::lock::Mutex; use log::LevelFilter; +use parking_lot::Mutex; use simplelog::SimpleLogger; use std::{fs, path::PathBuf, sync::Arc}; use zed::{ diff --git a/zed/src/test.rs b/zed/src/test.rs index 7d7d6e3ed6ab1b2898a8d6bc1afb79d2dcbcde47..e2ddff054ea1b288404bbcc4bfc75d462e540b43 100644 --- a/zed/src/test.rs +++ b/zed/src/test.rs @@ -6,8 +6,8 @@ use crate::{ time::ReplicaId, AppState, }; -use futures::lock::Mutex; use gpui::{AppContext, Entity, ModelHandle}; +use parking_lot::Mutex; use smol::channel; use std::{ marker::PhantomData, diff --git a/zed/src/theme.rs b/zed/src/theme.rs index f5c01b2759ebb2ff405590a136496ac4689fcdd7..b1c09bf7c862fe1cc6e5384defafb223a46507a1 100644 --- a/zed/src/theme.rs +++ b/zed/src/theme.rs @@ -126,6 +126,11 @@ impl ThemeRegistry { }) } + pub fn clear(&self) { + self.theme_data.lock().clear(); + self.themes.lock().clear(); + } + pub fn get(&self, name: &str) -> Result> { if let Some(theme) = self.themes.lock().get(name) { return Ok(theme.clone()); diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index ec50e92a6e81decb50e0649e6693d8caeca6ca4d..9aa35cb3959f3695abcc28b2b3bdc9908b84732f 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -7,7 +7,6 @@ use crate::{ worktree::fuzzy::{match_strings, StringMatch, StringMatchCandidate}, AppState, Settings, }; -use futures::lock::Mutex; use gpui::{ elements::{ Align, ChildView, ConstrainedBox, Container, Expanded, Flex, Label, ParentElement, @@ -17,6 +16,7 @@ use gpui::{ AppContext, Axis, Element, ElementBox, Entity, MutableAppContext, RenderContext, View, ViewContext, ViewHandle, }; +use parking_lot::Mutex; use postage::watch; pub struct ThemeSelector { @@ -31,13 +31,14 @@ pub struct ThemeSelector { pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { cx.add_action("theme_selector:confirm", ThemeSelector::confirm); - // cx.add_action("file_finder:select", ThemeSelector::select); cx.add_action("menu:select_prev", ThemeSelector::select_prev); cx.add_action("menu:select_next", ThemeSelector::select_next); cx.add_action("theme_selector:toggle", ThemeSelector::toggle); + cx.add_action("theme_selector:reload", ThemeSelector::reload); cx.add_bindings(vec![ Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()), + Binding::new("cmd-k shift-T", "theme_selector:reload", None).with_arg(app_state.clone()), Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector")) .with_arg(app_state.clone()), Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")), @@ -90,19 +91,26 @@ impl ThemeSelector { }); } + fn reload(_: &mut Workspace, app_state: &Arc, cx: &mut ViewContext) { + let current_theme_name = app_state.settings.borrow().theme.name.clone(); + app_state.themes.clear(); + match app_state.themes.get(¤t_theme_name) { + Ok(theme) => { + cx.notify_all(); + app_state.settings_tx.lock().borrow_mut().theme = theme; + } + Err(error) => { + log::error!("failed to load theme {}: {:?}", current_theme_name, error) + } + } + } + fn confirm(&mut self, _: &(), cx: &mut ViewContext) { if let Some(mat) = self.matches.get(self.selected_index) { if let Ok(theme) = self.registry.get(&mat.string) { - let settings_tx = self.settings_tx.clone(); - cx.spawn(|this, mut cx| async move { - let mut settings_tx = settings_tx.lock().await; - this.update(&mut cx, |_, cx| { - settings_tx.borrow_mut().theme = theme; - cx.notify_all(); - cx.emit(Event::Dismissed); - }) - }) - .detach(); + self.settings_tx.lock().borrow_mut().theme = theme; + cx.notify_all(); + cx.emit(Event::Dismissed); } } } From 7494a395ed0ec80e0314bd11c6024ae6f0887058 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 16:08:15 -0600 Subject: [PATCH 16/23] Specify tab padding in theme --- zed/assets/themes/_base.toml | 1 + zed/src/workspace/pane.rs | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index 1b646b40a204d6f94d7eb1d615ddbf644fa0d87c..e2283d25808abfd3d99708bccd2651fa5703d11f 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -5,6 +5,7 @@ background = "$elevation_1" background = "$elevation_2" text = "$text_dull" border.color = "#000000" +padding = { left = 10, right = 10 } icon_close = "#383839" icon_dirty = "#556de8" icon_conflict = "#e45349" diff --git a/zed/src/workspace/pane.rs b/zed/src/workspace/pane.rs index 69c1a3af0b1336d5984e861a550c0a5bed8dd891..511e48dfd41dc4e7a5a5b5382e82208477606525 100644 --- a/zed/src/workspace/pane.rs +++ b/zed/src/workspace/pane.rs @@ -244,7 +244,6 @@ impl Pane { } else { &theme.tab.container }) - .with_horizontal_padding(10.) .with_border(border); if is_active { From 33a8942c8b7f0e6fbb15497c2905c382e5e5e90f Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 16:09:19 -0600 Subject: [PATCH 17/23] Pull empty selector label styling from correct place in theme --- zed/src/file_finder.rs | 2 +- zed/src/theme_selector.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index 8e2e0bfa578e19329b40c92a2420186da445db86..bacba1ee2a5c6b1905150893f7ccd81116bd9c8f 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -107,7 +107,7 @@ impl FileFinder { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.text) + .with_style(&settings.theme.ui.selector.label) .boxed(), ) .with_margin_top(6.0) diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index 9aa35cb3959f3695abcc28b2b3bdc9908b84732f..a6a54ec0845146599c1d3de897b5f8f75b23c7cb 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -204,7 +204,7 @@ impl ThemeSelector { settings.ui_font_family, settings.ui_font_size, ) - .with_default_color(settings.theme.editor.text) + .with_style(&settings.theme.ui.selector.label) .boxed(), ) .with_margin_top(6.0) From d484d80238ccf96f70ede99ba8eb6d6149a6c0cf Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 16:17:33 -0600 Subject: [PATCH 18/23] Match file finder icon color to label color --- zed/src/file_finder.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/zed/src/file_finder.rs b/zed/src/file_finder.rs index bacba1ee2a5c6b1905150893f7ccd81116bd9c8f..f5bacbd452a09be9fd2131fc1699b7a8537445f9 100644 --- a/zed/src/file_finder.rs +++ b/zed/src/file_finder.rs @@ -157,7 +157,9 @@ impl FileFinder { LineBox::new( settings.ui_font_family, settings.ui_font_size, - Svg::new("icons/file-16.svg").boxed(), + Svg::new("icons/file-16.svg") + .with_color(style.label.text.color) + .boxed(), ) .boxed(), ) From 039dae064c150dc547faf10b1d195e699f71fd82 Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 17:09:15 -0600 Subject: [PATCH 19/23] Remove logging --- zed/src/settings.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/settings.rs b/zed/src/settings.rs index 65dc04190d30e907c1fd03cc71a2d8d5f4adecd9..a6b9e667c1d848e431a9805b3f7d36f385800520 100644 --- a/zed/src/settings.rs +++ b/zed/src/settings.rs @@ -49,7 +49,7 @@ pub fn channel_with_themes( themes: &ThemeRegistry, ) -> Result<(watch::Sender, watch::Receiver)> { let theme = match themes.get(DEFAULT_THEME_NAME) { - Ok(theme) => dbg!(theme), + Ok(theme) => theme, Err(err) => { panic!("failed to deserialize default theme: {:?}", err) } From 593afb2d9ea8dacd45e8751e2e10be8df197c84d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 17:34:15 -0600 Subject: [PATCH 20/23] Change reload theme binding --- zed/src/theme_selector.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index a6a54ec0845146599c1d3de897b5f8f75b23c7cb..f257b41ab8862a2ae739c8e585ee539c032ec098 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -38,7 +38,7 @@ pub fn init(cx: &mut MutableAppContext, app_state: &Arc) { cx.add_bindings(vec![ Binding::new("cmd-k cmd-t", "theme_selector:toggle", None).with_arg(app_state.clone()), - Binding::new("cmd-k shift-T", "theme_selector:reload", None).with_arg(app_state.clone()), + Binding::new("cmd-k t", "theme_selector:reload", None).with_arg(app_state.clone()), Binding::new("escape", "theme_selector:toggle", Some("ThemeSelector")) .with_arg(app_state.clone()), Binding::new("enter", "theme_selector:confirm", Some("ThemeSelector")), From 1a21902460e10f86c46b4c7991fa7d4de5cc99fe Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 17:46:53 -0600 Subject: [PATCH 21/23] Move fuzzy mod out of worktree We now match against arbitrary strings in addition to paths. Co-Authored-By: Max Brunsfeld --- zed/src/{worktree => }/fuzzy.rs | 12 +++- zed/src/{worktree => fuzzy}/char_bag.rs | 0 zed/src/lib.rs | 1 + zed/src/theme_selector.rs | 2 +- zed/src/workspace.rs | 2 +- zed/src/worktree.rs | 90 +++++++++++-------------- 6 files changed, 53 insertions(+), 54 deletions(-) rename zed/src/{worktree => }/fuzzy.rs (99%) rename zed/src/{worktree => fuzzy}/char_bag.rs (100%) diff --git a/zed/src/worktree/fuzzy.rs b/zed/src/fuzzy.rs similarity index 99% rename from zed/src/worktree/fuzzy.rs rename to zed/src/fuzzy.rs index c565e56ed12613b68fa021a0611924be778cf851..a04bbe99b9c7999074de4ce1c63e5fc1c42d38df 100644 --- a/zed/src/worktree/fuzzy.rs +++ b/zed/src/fuzzy.rs @@ -1,5 +1,9 @@ -use super::{char_bag::CharBag, EntryKind, Snapshot}; -use crate::util; +mod char_bag; + +use crate::{ + util, + worktree::{EntryKind, Snapshot}, +}; use gpui::executor; use std::{ borrow::Cow, @@ -9,6 +13,8 @@ use std::{ sync::Arc, }; +pub use char_bag::CharBag; + const BASE_DISTANCE_PENALTY: f64 = 0.6; const ADDITIONAL_DISTANCE_PENALTY: f64 = 0.05; const MIN_DISTANCE_PENALTY: f64 = 0.2; @@ -367,7 +373,7 @@ impl<'a> Matcher<'a> { results: &mut Vec, cancel_flag: &AtomicBool, ) { - let tree_id = snapshot.id; + let tree_id = snapshot.id(); let prefix = path_prefix.chars().collect::>(); let lowercase_prefix = prefix .iter() diff --git a/zed/src/worktree/char_bag.rs b/zed/src/fuzzy/char_bag.rs similarity index 100% rename from zed/src/worktree/char_bag.rs rename to zed/src/fuzzy/char_bag.rs diff --git a/zed/src/lib.rs b/zed/src/lib.rs index 24196a42226caf68640f246b88a612ebc77a4327..e59fb3fd67a246e4f3f32aba96ad1b38f6b3a80e 100644 --- a/zed/src/lib.rs +++ b/zed/src/lib.rs @@ -2,6 +2,7 @@ pub mod assets; pub mod editor; pub mod file_finder; pub mod fs; +mod fuzzy; pub mod language; pub mod menus; mod operation_queue; diff --git a/zed/src/theme_selector.rs b/zed/src/theme_selector.rs index f257b41ab8862a2ae739c8e585ee539c032ec098..7b4456fd39f75ab91cbf9efa59ffaf50cb764d73 100644 --- a/zed/src/theme_selector.rs +++ b/zed/src/theme_selector.rs @@ -2,9 +2,9 @@ use std::{cmp, sync::Arc}; use crate::{ editor::{self, Editor}, + fuzzy::{match_strings, StringMatch, StringMatchCandidate}, settings::ThemeRegistry, workspace::Workspace, - worktree::fuzzy::{match_strings, StringMatch, StringMatchCandidate}, AppState, Settings, }; use gpui::{ diff --git a/zed/src/workspace.rs b/zed/src/workspace.rs index b01ff1faf9b95813fd25cc2e5c3e227970af8d79..cb543522d8d348e9dd1b39eb2034ddb3974ceb33 100644 --- a/zed/src/workspace.rs +++ b/zed/src/workspace.rs @@ -911,7 +911,7 @@ impl WorkspaceHandle for ViewHandle { let tree_id = tree.id(); tree.read(cx) .files(0) - .map(move |f| (tree_id, f.path().clone())) + .map(move |f| (tree_id, f.path.clone())) }) .collect::>() } diff --git a/zed/src/worktree.rs b/zed/src/worktree.rs index 55a0738f41f6e07d6564f32abad099e78f1c5a74..f8566cd8c40b67fcca79d6401f1476058fbea620 100644 --- a/zed/src/worktree.rs +++ b/zed/src/worktree.rs @@ -1,11 +1,11 @@ -mod char_bag; -pub(crate) mod fuzzy; mod ignore; -use self::{char_bag::CharBag, ignore::IgnoreStack}; +use self::ignore::IgnoreStack; use crate::{ editor::{self, Buffer, History, Operation, Rope}, fs::{self, Fs}, + fuzzy, + fuzzy::CharBag, language::LanguageRegistry, rpc::{self, proto}, sum_tree::{self, Cursor, Edit, SumTree}, @@ -1116,6 +1116,10 @@ pub struct Snapshot { } impl Snapshot { + pub fn id(&self) -> usize { + self.id + } + pub fn build_update(&self, other: &Self, worktree_id: u64) -> proto::UpdateWorktree { let mut updated_entries = Vec::new(); let mut removed_entries = Vec::new(); @@ -1214,7 +1218,7 @@ impl Snapshot { self.entries_by_path .cursor::<(), ()>() .filter(move |entry| entry.path.as_ref() != empty_path) - .map(|entry| entry.path()) + .map(|entry| &entry.path) } pub fn visible_files(&self, start: usize) -> FileIter { @@ -1248,17 +1252,17 @@ impl Snapshot { } pub fn inode_for_path(&self, path: impl AsRef) -> Option { - self.entry_for_path(path.as_ref()).map(|e| e.inode()) + self.entry_for_path(path.as_ref()).map(|e| e.inode) } fn insert_entry(&mut self, mut entry: Entry) -> Entry { - if !entry.is_dir() && entry.path().file_name() == Some(&GITIGNORE) { - let (ignore, err) = Gitignore::new(self.abs_path.join(entry.path())); + if !entry.is_dir() && entry.path.file_name() == Some(&GITIGNORE) { + let (ignore, err) = Gitignore::new(self.abs_path.join(&entry.path)); if let Some(err) = err { - log::error!("error in ignore file {:?} - {:?}", entry.path(), err); + log::error!("error in ignore file {:?} - {:?}", &entry.path, err); } - let ignore_dir_path = entry.path().parent().unwrap(); + let ignore_dir_path = entry.path.parent().unwrap(); self.ignores .insert(ignore_dir_path.into(), (Arc::new(ignore), self.scan_id)); } @@ -1381,10 +1385,10 @@ impl Snapshot { impl fmt::Debug for Snapshot { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { for entry in self.entries_by_path.cursor::<(), ()>() { - for _ in entry.path().ancestors().skip(1) { + for _ in entry.path.ancestors().skip(1) { write!(f, " ")?; } - writeln!(f, "{:?} (inode: {})", entry.path(), entry.inode())?; + writeln!(f, "{:?} (inode: {})", entry.path, entry.inode)?; } Ok(()) } @@ -1535,19 +1539,19 @@ impl File { } pub fn entry_id(&self) -> (usize, Arc) { - (self.worktree.id(), self.path()) + (self.worktree.id(), self.path.clone()) } } #[derive(Clone, Debug)] pub struct Entry { - id: usize, - kind: EntryKind, - path: Arc, - inode: u64, - mtime: SystemTime, - is_symlink: bool, - is_ignored: bool, + pub id: usize, + pub kind: EntryKind, + pub path: Arc, + pub inode: u64, + pub mtime: SystemTime, + pub is_symlink: bool, + pub is_ignored: bool, } #[derive(Clone, Debug)] @@ -1579,23 +1583,11 @@ impl Entry { } } - pub fn path(&self) -> &Arc { - &self.path - } - - pub fn inode(&self) -> u64 { - self.inode - } - - pub fn is_ignored(&self) -> bool { - self.is_ignored - } - - fn is_dir(&self) -> bool { + pub fn is_dir(&self) -> bool { matches!(self.kind, EntryKind::Dir | EntryKind::PendingDir) } - fn is_file(&self) -> bool { + pub fn is_file(&self) -> bool { matches!(self.kind, EntryKind::File(_)) } } @@ -1619,7 +1611,7 @@ impl sum_tree::Item for Entry { } EntrySummary { - max_path: self.path().clone(), + max_path: self.path.clone(), file_count, visible_file_count, } @@ -1630,7 +1622,7 @@ impl sum_tree::KeyedItem for Entry { type Key = PathKey; fn key(&self) -> Self::Key { - PathKey(self.path().clone()) + PathKey(self.path.clone()) } } @@ -2147,7 +2139,7 @@ impl BackgroundScanner { let mut edits = Vec::new(); for mut entry in snapshot.child_entries(&job.path).cloned() { let was_ignored = entry.is_ignored; - entry.is_ignored = ignore_stack.is_path_ignored(entry.path(), entry.is_dir()); + entry.is_ignored = ignore_stack.is_path_ignored(&entry.path, entry.is_dir()); if entry.is_dir() { let child_ignore_stack = if entry.is_ignored { IgnoreStack::all() @@ -2156,7 +2148,7 @@ impl BackgroundScanner { }; job.ignore_queue .send(UpdateIgnoreStatusJob { - path: entry.path().clone(), + path: entry.path.clone(), ignore_stack: child_ignore_stack, ignore_queue: job.ignore_queue.clone(), }) @@ -2333,9 +2325,9 @@ impl<'a> Iterator for ChildEntriesIter<'a> { fn next(&mut self) -> Option { if let Some(item) = self.cursor.item() { - if item.path().starts_with(self.parent_path) { + if item.path.starts_with(self.parent_path) { self.cursor - .seek_forward(&PathSearch::Successor(item.path()), Bias::Left, &()); + .seek_forward(&PathSearch::Successor(&item.path), Bias::Left, &()); Some(item) } else { None @@ -2930,8 +2922,8 @@ mod tests { let tree = tree.read(cx); let tracked = tree.entry_for_path("tracked-dir/tracked-file1").unwrap(); let ignored = tree.entry_for_path("ignored-dir/ignored-file1").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); + assert_eq!(tracked.is_ignored, false); + assert_eq!(ignored.is_ignored, true); }); std::fs::write(dir.path().join("tracked-dir/tracked-file2"), "").unwrap(); @@ -2942,9 +2934,9 @@ mod tests { let dot_git = tree.entry_for_path(".git").unwrap(); let tracked = tree.entry_for_path("tracked-dir/tracked-file2").unwrap(); let ignored = tree.entry_for_path("ignored-dir/ignored-file2").unwrap(); - assert_eq!(tracked.is_ignored(), false); - assert_eq!(ignored.is_ignored(), true); - assert_eq!(dot_git.is_ignored(), true); + assert_eq!(tracked.is_ignored, false); + assert_eq!(ignored.is_ignored, true); + assert_eq!(dot_git.is_ignored, true); }); } @@ -3177,9 +3169,9 @@ mod tests { let mut visible_files = self.visible_files(0); for entry in self.entries_by_path.cursor::<(), ()>() { if entry.is_file() { - assert_eq!(files.next().unwrap().inode(), entry.inode); + assert_eq!(files.next().unwrap().inode, entry.inode); if !entry.is_ignored { - assert_eq!(visible_files.next().unwrap().inode(), entry.inode); + assert_eq!(visible_files.next().unwrap().inode, entry.inode); } } } @@ -3192,14 +3184,14 @@ mod tests { bfs_paths.push(path); let ix = stack.len(); for child_entry in self.child_entries(path) { - stack.insert(ix, child_entry.path()); + stack.insert(ix, &child_entry.path); } } let dfs_paths = self .entries_by_path .cursor::<(), ()>() - .map(|e| e.path().as_ref()) + .map(|e| e.path.as_ref()) .collect::>(); assert_eq!(bfs_paths, dfs_paths); @@ -3214,7 +3206,7 @@ mod tests { fn to_vec(&self) -> Vec<(&Path, u64, bool)> { let mut paths = Vec::new(); for entry in self.entries_by_path.cursor::<(), ()>() { - paths.push((entry.path().as_ref(), entry.inode(), entry.is_ignored())); + paths.push((entry.path.as_ref(), entry.inode, entry.is_ignored)); } paths.sort_by(|a, b| a.0.cmp(&b.0)); paths From 522fac9690652856ee8d0fd26d0241d3e6015d1d Mon Sep 17 00:00:00 2001 From: Nathan Sobo Date: Wed, 4 Aug 2021 17:56:38 -0600 Subject: [PATCH 22/23] Fix compile error in tests Co-Authored-By: Max Brunsfeld --- zed/src/fuzzy.rs | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/zed/src/fuzzy.rs b/zed/src/fuzzy.rs index a04bbe99b9c7999074de4ce1c63e5fc1c42d38df..dde7f8fd7aff2fd198816f6115119e6128172978 100644 --- a/zed/src/fuzzy.rs +++ b/zed/src/fuzzy.rs @@ -295,7 +295,7 @@ pub async fn match_paths( }); matcher.match_paths( - snapshot, + snapshot.id(), path_prefix, paths, results, @@ -367,13 +367,12 @@ impl<'a> Matcher<'a> { fn match_paths( &mut self, - snapshot: &Snapshot, + tree_id: usize, path_prefix: Arc, path_entries: impl Iterator>, results: &mut Vec, cancel_flag: &AtomicBool, ) { - let tree_id = snapshot.id(); let prefix = path_prefix.chars().collect::>(); let lowercase_prefix = prefix .iter() @@ -762,18 +761,7 @@ mod tests { let cancel_flag = AtomicBool::new(false); let mut results = Vec::new(); matcher.match_paths( - &Snapshot { - id: 0, - scan_id: 0, - abs_path: PathBuf::new().into(), - ignores: Default::default(), - entries_by_path: Default::default(), - entries_by_id: Default::default(), - removed_entry_ids: Default::default(), - root_name: Default::default(), - root_char_bag: Default::default(), - next_entry_id: Default::default(), - }, + 0, "".into(), path_entries.into_iter(), &mut results, From d37a98782eb86b8b59d160bcab1bc3fc19b33502 Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Wed, 4 Aug 2021 17:19:30 -0700 Subject: [PATCH 23/23] Improve border parsing in themes The `top`, `left`, `bottom` and `right` fields are optional. If none are specified, then they are all set to true. Co-Authored-By: Nathan Sobo --- gpui/src/platform/mac/renderer.rs | 6 +--- gpui/src/scene.rs | 52 ++++++++++++++++++++++++------- zed/assets/themes/_base.toml | 2 +- zed/assets/themes/dark.toml | 2 +- 4 files changed, 43 insertions(+), 19 deletions(-) diff --git a/gpui/src/platform/mac/renderer.rs b/gpui/src/platform/mac/renderer.rs index 40ae31627d81151ae96ba73edd2ed8cb62043271..82e80790cb83d0dfe542cb2aaf4a50ef4d41aff6 100644 --- a/gpui/src/platform/mac/renderer.rs +++ b/gpui/src/platform/mac/renderer.rs @@ -444,11 +444,7 @@ impl Renderer { border_right: border_width * (quad.border.right as usize as f32), border_bottom: border_width * (quad.border.bottom as usize as f32), border_left: border_width * (quad.border.left as usize as f32), - border_color: quad - .border - .color - .unwrap_or(Color::transparent_black()) - .to_uchar4(), + border_color: quad.border.color.to_uchar4(), corner_radius: quad.corner_radius * scene.scale_factor(), }; unsafe { diff --git a/gpui/src/scene.rs b/gpui/src/scene.rs index 8c83c5268b35d6d8a9f3b6a8646fab47396aef8d..3818a0870120ba7a0183db69ad3d9dfc1ecb8b60 100644 --- a/gpui/src/scene.rs +++ b/gpui/src/scene.rs @@ -57,24 +57,52 @@ pub struct Icon { pub color: Color, } -#[derive(Clone, Copy, Default, Debug, Deserialize)] +#[derive(Clone, Copy, Default, Debug)] pub struct Border { - #[serde(default = "default_border_width")] pub width: f32, - #[serde(default)] - pub color: Option, - #[serde(default)] + pub color: Color, pub top: bool, - #[serde(default)] pub right: bool, - #[serde(default)] pub bottom: bool, - #[serde(default)] pub left: bool, } -fn default_border_width() -> f32 { - 1.0 +impl<'de> Deserialize<'de> for Border { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + #[derive(Deserialize)] + struct BorderData { + pub width: f32, + pub color: Color, + #[serde(default)] + pub top: bool, + #[serde(default)] + pub right: bool, + #[serde(default)] + pub bottom: bool, + #[serde(default)] + pub left: bool, + } + + let data = BorderData::deserialize(deserializer)?; + let mut border = Border { + width: data.width, + color: data.color, + top: data.top, + bottom: data.bottom, + left: data.left, + right: data.right, + }; + if !border.top && !border.bottom && !border.left && !border.right { + border.top = true; + border.bottom = true; + border.left = true; + border.right = true; + } + Ok(border) + } } #[derive(Debug)] @@ -206,7 +234,7 @@ impl Border { pub fn new(width: f32, color: Color) -> Self { Self { width, - color: Some(color), + color, top: false, left: false, bottom: false, @@ -217,7 +245,7 @@ impl Border { pub fn all(width: f32, color: Color) -> Self { Self { width, - color: Some(color), + color, top: true, left: true, bottom: true, diff --git a/zed/assets/themes/_base.toml b/zed/assets/themes/_base.toml index e2283d25808abfd3d99708bccd2651fa5703d11f..20cc34ee0e8d418b84caedebb1c29df26ba04b83 100644 --- a/zed/assets/themes/_base.toml +++ b/zed/assets/themes/_base.toml @@ -4,7 +4,7 @@ background = "$elevation_1" [ui.tab] background = "$elevation_2" text = "$text_dull" -border.color = "#000000" +border = { color = "#000000", width = 1.0 } padding = { left = 10, right = 10 } icon_close = "#383839" icon_dirty = "#556de8" diff --git a/zed/assets/themes/dark.toml b/zed/assets/themes/dark.toml index 5db98bf091bcfd6167c76e75e0917cc90afca73b..dae0bdf1eceefea9c2d5994ca0932271238e52cd 100644 --- a/zed/assets/themes/dark.toml +++ b/zed/assets/themes/dark.toml @@ -10,7 +10,7 @@ text_bright = "#ffffff" text_normal = "#d4d4d4" [syntax] -keyword = { color = "#c586c0", weight = "bold" } +keyword = { color = "#0086c0", weight = "bold" } function = "#dcdcaa" string = "#cb8f77" type = "#4ec9b0"