Get new theme structure working

* Fix precedence of extends directives
* Always group color with font properties for text theming

Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld 2021-08-04 14:07:19 -07:00
parent 5761756fb4
commit 802f1f4e78
15 changed files with 309 additions and 320 deletions

View File

@ -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<str> = 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]",
))
}
}

View File

@ -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<Color>,
#[serde(default, deserialize_with = "deserialize_font_properties")]
pub font_properties: Properties,
#[serde(default, deserialize_with = "deserialize_option_font_properties")]
pub highlight_font_properties: Option<Properties>,
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
}
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(),

View File

@ -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<WeightJson>,
#[serde(default)]
italic: bool,
}
impl Into<Properties> 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<D>(deserializer: D) -> Result<Self, D::Error>
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<Color> 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<TextStyle> 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<Properties> 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<Option<Properties>, D::Error>
where
D: Deserializer<'de>,
{
let json: Option<PropertiesJson> = Deserialize::deserialize(deserializer)?;
Ok(json.map(Into::into))
}
pub fn deserialize_font_properties<'de, D>(deserializer: D) -> Result<Properties, D::Error>
where
D: Deserializer<'de>,
{
let json: PropertiesJson = Deserialize::deserialize(deserializer)?;
Ok(json.into())
}
pub fn font_properties_from_json(
value: serde_json::Value,
) -> Result<Properties, serde_json::Error> {
Ok(serde_json::from_value::<PropertiesJson>(value)?.into())
}
impl ToJson for Properties {
fn to_json(&self) -> crate::json::Value {
json!({

View File

@ -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" },
]

View File

@ -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"

View File

@ -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;
}
}
}

View File

@ -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<Self::Item> {
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())
{

View File

@ -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);

View File

@ -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<Self::Item> {
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.

View File

@ -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<Snapshot>);
@ -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<Self::Item> {
if self.chunk.is_empty() {

View File

@ -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<Self::Item> {
if self.output_position.row() >= self.max_output_row {

View File

@ -141,11 +141,15 @@ impl FileFinder {
index: usize,
cx: &AppContext,
) -> Option<ElementBox> {
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())

View File

@ -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<ThemeMap>,
pub highlight_map: Mutex<HighlightMap>,
}
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(),
}),
],
};

View File

@ -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<Settings>, watch::Receiver<Settings>)> {
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,
)?))
}

View File

@ -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<dyn AssetSource>,
@ -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<String, Value>, extension: Map<String, Value>)
}
}
#[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<Key>,
target_path: Vec<Key>,
@ -429,30 +435,17 @@ fn validate_variable_name(name: &str) -> bool {
pub fn deserialize_syntax_theme<'de, D>(
deserializer: D,
) -> Result<Vec<(String, Color, FontProperties)>, D::Error>
) -> Result<Vec<(String, TextStyle)>, 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<String, Value> = Deserialize::deserialize(deserializer)?;
let syntax_data: HashMap<String, TextStyle> = 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();
let theme_data = registry.load("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::<Vec<_>>(),
&["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)]);