Extract theme
into its own crate
This commit is contained in:
parent
0022c6b828
commit
2087c4731f
16 changed files with 62 additions and 38 deletions
15
crates/theme/Cargo.toml
Normal file
15
crates/theme/Cargo.toml
Normal file
|
@ -0,0 +1,15 @@
|
|||
[package]
|
||||
name = "theme"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
editor = { path = "../editor" }
|
||||
gpui = { path = "../gpui" }
|
||||
anyhow = "1.0.38"
|
||||
indexmap = "1.6.2"
|
||||
parking_lot = "0.11.1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1.0.64", features = ["preserve_order"] }
|
||||
serde_path_to_error = "0.1.4"
|
||||
toml = "0.5"
|
232
crates/theme/src/lib.rs
Normal file
232
crates/theme/src/lib.rs
Normal file
|
@ -0,0 +1,232 @@
|
|||
mod resolution;
|
||||
mod theme_registry;
|
||||
|
||||
use editor::{EditorStyle, SelectionStyle};
|
||||
use gpui::{
|
||||
color::Color,
|
||||
elements::{ContainerStyle, ImageStyle, LabelStyle},
|
||||
fonts::TextStyle,
|
||||
Border,
|
||||
};
|
||||
use serde::Deserialize;
|
||||
|
||||
pub use theme_registry::*;
|
||||
|
||||
pub const DEFAULT_THEME_NAME: &'static str = "black";
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Theme {
|
||||
#[serde(default)]
|
||||
pub name: String,
|
||||
pub workspace: Workspace,
|
||||
pub chat_panel: ChatPanel,
|
||||
pub people_panel: PeoplePanel,
|
||||
pub project_panel: ProjectPanel,
|
||||
pub selector: Selector,
|
||||
pub editor: EditorStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Workspace {
|
||||
pub background: Color,
|
||||
pub titlebar: Titlebar,
|
||||
pub tab: Tab,
|
||||
pub active_tab: Tab,
|
||||
pub pane_divider: Border,
|
||||
pub left_sidebar: Sidebar,
|
||||
pub right_sidebar: Sidebar,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Titlebar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub title: TextStyle,
|
||||
pub avatar_width: f32,
|
||||
pub offline_icon: OfflineIcon,
|
||||
pub icon_color: Color,
|
||||
pub avatar: ImageStyle,
|
||||
pub outdated_warning: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct OfflineIcon {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct Tab {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
pub spacing: f32,
|
||||
pub icon_width: f32,
|
||||
pub icon_close: Color,
|
||||
pub icon_close_active: Color,
|
||||
pub icon_dirty: Color,
|
||||
pub icon_conflict: Color,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Sidebar {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub width: f32,
|
||||
pub item: SidebarItem,
|
||||
pub active_item: SidebarItem,
|
||||
pub resize_handle: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct SidebarItem {
|
||||
pub icon_color: Color,
|
||||
pub icon_size: f32,
|
||||
pub height: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub message: ChatMessage,
|
||||
pub pending_message: ChatMessage,
|
||||
pub channel_select: ChannelSelect,
|
||||
pub input_editor: InputEditorStyle,
|
||||
pub sign_in_prompt: TextStyle,
|
||||
pub hovered_sign_in_prompt: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectPanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub entry: ProjectPanelEntry,
|
||||
pub hovered_entry: ProjectPanelEntry,
|
||||
pub selected_entry: ProjectPanelEntry,
|
||||
pub hovered_selected_entry: ProjectPanelEntry,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ProjectPanelEntry {
|
||||
pub height: f32,
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub text: TextStyle,
|
||||
pub icon_color: Color,
|
||||
pub icon_size: f32,
|
||||
pub icon_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct PeoplePanel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub host_row_height: f32,
|
||||
pub host_avatar: ImageStyle,
|
||||
pub host_username: ContainedText,
|
||||
pub tree_branch_width: f32,
|
||||
pub tree_branch_color: Color,
|
||||
pub shared_worktree: WorktreeRow,
|
||||
pub hovered_shared_worktree: WorktreeRow,
|
||||
pub unshared_worktree: WorktreeRow,
|
||||
pub hovered_unshared_worktree: WorktreeRow,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct WorktreeRow {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub height: f32,
|
||||
pub name: ContainedText,
|
||||
pub guest_avatar: ImageStyle,
|
||||
pub guest_avatar_spacing: f32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChatMessage {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub body: TextStyle,
|
||||
pub sender: ContainedText,
|
||||
pub timestamp: ContainedText,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChannelSelect {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub header: ChannelName,
|
||||
pub item: ChannelName,
|
||||
pub active_item: ChannelName,
|
||||
pub hovered_item: ChannelName,
|
||||
pub hovered_active_item: ChannelName,
|
||||
pub menu: ContainerStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ChannelName {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub hash: ContainedText,
|
||||
pub name: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Selector {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub empty: ContainedLabel,
|
||||
pub input_editor: InputEditorStyle,
|
||||
pub item: ContainedLabel,
|
||||
pub active_item: ContainedLabel,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
pub struct ContainedText {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub text: TextStyle,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct ContainedLabel {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
#[serde(flatten)]
|
||||
pub label: LabelStyle,
|
||||
}
|
||||
|
||||
#[derive(Clone, Deserialize)]
|
||||
pub struct InputEditorStyle {
|
||||
#[serde(flatten)]
|
||||
pub container: ContainerStyle,
|
||||
pub text: TextStyle,
|
||||
#[serde(default)]
|
||||
pub placeholder_text: Option<TextStyle>,
|
||||
pub selection: SelectionStyle,
|
||||
}
|
||||
|
||||
impl InputEditorStyle {
|
||||
pub fn as_editor(&self) -> EditorStyle {
|
||||
EditorStyle {
|
||||
text: self.text.clone(),
|
||||
placeholder_text: self.placeholder_text.clone(),
|
||||
background: self
|
||||
.container
|
||||
.background_color
|
||||
.unwrap_or(Color::transparent_black()),
|
||||
selection: self.selection,
|
||||
gutter_background: Default::default(),
|
||||
active_line_background: Default::default(),
|
||||
line_number: Default::default(),
|
||||
line_number_active: Default::default(),
|
||||
guest_selections: Default::default(),
|
||||
syntax: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
497
crates/theme/src/resolution.rs
Normal file
497
crates/theme/src/resolution.rs
Normal file
|
@ -0,0 +1,497 @@
|
|||
use anyhow::{anyhow, Result};
|
||||
use indexmap::IndexMap;
|
||||
use serde_json::Value;
|
||||
use std::{
|
||||
cell::RefCell,
|
||||
mem,
|
||||
rc::{Rc, Weak},
|
||||
};
|
||||
|
||||
pub fn resolve_references(value: Value) -> Result<Value> {
|
||||
let tree = Tree::from_json(value)?;
|
||||
tree.resolve()?;
|
||||
tree.to_json()
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Node {
|
||||
Reference {
|
||||
path: String,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Object {
|
||||
base: Option<String>,
|
||||
children: IndexMap<String, Tree>,
|
||||
resolved: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Array {
|
||||
children: Vec<Tree>,
|
||||
resolved: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
String {
|
||||
value: String,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Number {
|
||||
value: serde_json::Number,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Bool {
|
||||
value: bool,
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
Null {
|
||||
parent: Option<Weak<RefCell<Node>>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Tree(Rc<RefCell<Node>>);
|
||||
|
||||
impl Tree {
|
||||
pub fn new(node: Node) -> Self {
|
||||
Self(Rc::new(RefCell::new(node)))
|
||||
}
|
||||
|
||||
fn from_json(value: Value) -> Result<Self> {
|
||||
match value {
|
||||
Value::String(value) => {
|
||||
if let Some(path) = value.strip_prefix("$") {
|
||||
Ok(Self::new(Node::Reference {
|
||||
path: path.to_string(),
|
||||
parent: None,
|
||||
}))
|
||||
} else {
|
||||
Ok(Self::new(Node::String {
|
||||
value,
|
||||
parent: None,
|
||||
}))
|
||||
}
|
||||
}
|
||||
Value::Number(value) => Ok(Self::new(Node::Number {
|
||||
value,
|
||||
parent: None,
|
||||
})),
|
||||
Value::Bool(value) => Ok(Self::new(Node::Bool {
|
||||
value,
|
||||
parent: None,
|
||||
})),
|
||||
Value::Null => Ok(Self::new(Node::Null { parent: None })),
|
||||
Value::Object(object) => {
|
||||
let tree = Self::new(Node::Object {
|
||||
base: Default::default(),
|
||||
children: Default::default(),
|
||||
resolved: false,
|
||||
parent: None,
|
||||
});
|
||||
let mut children = IndexMap::new();
|
||||
let mut resolved = true;
|
||||
let mut base = None;
|
||||
for (key, value) in object.into_iter() {
|
||||
let value = if key == "extends" {
|
||||
if value.is_string() {
|
||||
if let Value::String(value) = value {
|
||||
base = value.strip_prefix("$").map(str::to_string);
|
||||
resolved = false;
|
||||
Self::new(Node::String {
|
||||
value,
|
||||
parent: None,
|
||||
})
|
||||
} else {
|
||||
unreachable!()
|
||||
}
|
||||
} else {
|
||||
Tree::from_json(value)?
|
||||
}
|
||||
} else {
|
||||
Tree::from_json(value)?
|
||||
};
|
||||
value
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&tree.0)));
|
||||
resolved &= value.is_resolved();
|
||||
children.insert(key.clone(), value);
|
||||
}
|
||||
|
||||
*tree.0.borrow_mut() = Node::Object {
|
||||
base,
|
||||
children,
|
||||
resolved,
|
||||
parent: None,
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
Value::Array(elements) => {
|
||||
let tree = Self::new(Node::Array {
|
||||
children: Default::default(),
|
||||
resolved: false,
|
||||
parent: None,
|
||||
});
|
||||
|
||||
let mut children = Vec::new();
|
||||
let mut resolved = true;
|
||||
for element in elements {
|
||||
let child = Tree::from_json(element)?;
|
||||
child
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&tree.0)));
|
||||
resolved &= child.is_resolved();
|
||||
children.push(child);
|
||||
}
|
||||
|
||||
*tree.0.borrow_mut() = Node::Array {
|
||||
children,
|
||||
resolved,
|
||||
parent: None,
|
||||
};
|
||||
Ok(tree)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Result<Value> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { .. } => Err(anyhow!("unresolved tree")),
|
||||
Node::String { value, .. } => Ok(Value::String(value.clone())),
|
||||
Node::Number { value, .. } => Ok(Value::Number(value.clone())),
|
||||
Node::Bool { value, .. } => Ok(Value::Bool(*value)),
|
||||
Node::Null { .. } => Ok(Value::Null),
|
||||
Node::Object { children, .. } => {
|
||||
let mut json_children = serde_json::Map::new();
|
||||
for (key, value) in children {
|
||||
json_children.insert(key.clone(), value.to_json()?);
|
||||
}
|
||||
Ok(Value::Object(json_children))
|
||||
}
|
||||
Node::Array { children, .. } => {
|
||||
let mut json_children = Vec::new();
|
||||
for child in children {
|
||||
json_children.push(child.to_json()?);
|
||||
}
|
||||
Ok(Value::Array(json_children))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parent(&self) -> Option<Tree> {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { parent, .. }
|
||||
| Node::Object { parent, .. }
|
||||
| Node::Array { parent, .. }
|
||||
| Node::String { parent, .. }
|
||||
| Node::Number { parent, .. }
|
||||
| Node::Bool { parent, .. }
|
||||
| Node::Null { parent } => parent.as_ref().and_then(|p| p.upgrade()).map(Tree),
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, path: &str) -> Result<Option<Tree>> {
|
||||
let mut tree = self.clone();
|
||||
for component in path.split('.') {
|
||||
let node = tree.0.borrow();
|
||||
match &*node {
|
||||
Node::Object { children, .. } => {
|
||||
if let Some(subtree) = children.get(component).cloned() {
|
||||
drop(node);
|
||||
tree = subtree;
|
||||
} else {
|
||||
return Err(anyhow!(
|
||||
"key \"{}\" does not exist in path \"{}\"",
|
||||
component,
|
||||
path
|
||||
));
|
||||
}
|
||||
}
|
||||
Node::Reference { .. } => return Ok(None),
|
||||
Node::Array { .. }
|
||||
| Node::String { .. }
|
||||
| Node::Number { .. }
|
||||
| Node::Bool { .. }
|
||||
| Node::Null { .. } => {
|
||||
return Err(anyhow!(
|
||||
"key \"{}\" in path \"{}\" is not an object",
|
||||
component,
|
||||
path
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(tree))
|
||||
}
|
||||
|
||||
fn is_resolved(&self) -> bool {
|
||||
match &*self.0.borrow() {
|
||||
Node::Reference { .. } => false,
|
||||
Node::Object { resolved, .. } | Node::Array { resolved, .. } => *resolved,
|
||||
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_resolved(&self) {
|
||||
match &mut *self.0.borrow_mut() {
|
||||
Node::Object {
|
||||
resolved,
|
||||
base,
|
||||
children,
|
||||
..
|
||||
} => {
|
||||
*resolved = base.is_none() && children.values().all(|c| c.is_resolved());
|
||||
}
|
||||
Node::Array {
|
||||
resolved, children, ..
|
||||
} => {
|
||||
*resolved = children.iter().all(|c| c.is_resolved());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resolve(&self) -> Result<()> {
|
||||
let mut unresolved = vec![self.clone()];
|
||||
let mut made_progress = true;
|
||||
|
||||
while made_progress && !unresolved.is_empty() {
|
||||
made_progress = false;
|
||||
for mut tree in mem::take(&mut unresolved) {
|
||||
made_progress |= tree.resolve_subtree(self, &mut unresolved)?;
|
||||
if tree.is_resolved() {
|
||||
while let Some(parent) = tree.parent() {
|
||||
parent.update_resolved();
|
||||
if !parent.is_resolved() {
|
||||
break;
|
||||
}
|
||||
tree = parent;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if unresolved.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(anyhow!("tree contains cycles"))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_subtree(&self, root: &Tree, unresolved: &mut Vec<Tree>) -> Result<bool> {
|
||||
let node = self.0.borrow();
|
||||
match &*node {
|
||||
Node::Reference { path, parent } => {
|
||||
if let Some(subtree) = root.get(&path)? {
|
||||
if subtree.is_resolved() {
|
||||
let parent = parent.clone();
|
||||
drop(node);
|
||||
let mut new_node = subtree.0.borrow().clone();
|
||||
new_node.set_parent(parent);
|
||||
*self.0.borrow_mut() = new_node;
|
||||
Ok(true)
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
Ok(false)
|
||||
}
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
Node::Object {
|
||||
base,
|
||||
children,
|
||||
resolved,
|
||||
..
|
||||
} => {
|
||||
if *resolved {
|
||||
Ok(false)
|
||||
} else {
|
||||
let mut made_progress = false;
|
||||
let mut children_resolved = true;
|
||||
for child in children.values() {
|
||||
made_progress |= child.resolve_subtree(root, unresolved)?;
|
||||
children_resolved &= child.is_resolved();
|
||||
}
|
||||
|
||||
if children_resolved {
|
||||
let mut has_base = false;
|
||||
let mut resolved_base = None;
|
||||
if let Some(base) = base {
|
||||
has_base = true;
|
||||
if let Some(base) = root.get(base)? {
|
||||
if base.is_resolved() {
|
||||
resolved_base = Some(base);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(node);
|
||||
|
||||
if let Some(base) = resolved_base.as_ref() {
|
||||
self.extend_from(&base);
|
||||
made_progress = true;
|
||||
}
|
||||
|
||||
if let Node::Object { resolved, base, .. } = &mut *self.0.borrow_mut() {
|
||||
if has_base {
|
||||
if resolved_base.is_some() {
|
||||
base.take();
|
||||
*resolved = true;
|
||||
} else {
|
||||
unresolved.push(self.clone());
|
||||
}
|
||||
} else {
|
||||
*resolved = true;
|
||||
}
|
||||
}
|
||||
} else if base.is_some() {
|
||||
unresolved.push(self.clone());
|
||||
}
|
||||
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
Node::Array {
|
||||
children, resolved, ..
|
||||
} => {
|
||||
if *resolved {
|
||||
Ok(false)
|
||||
} else {
|
||||
let mut made_progress = false;
|
||||
let mut children_resolved = true;
|
||||
for child in children.iter() {
|
||||
made_progress |= child.resolve_subtree(root, unresolved)?;
|
||||
children_resolved &= child.is_resolved();
|
||||
}
|
||||
|
||||
if children_resolved {
|
||||
drop(node);
|
||||
|
||||
if let Node::Array { resolved, .. } = &mut *self.0.borrow_mut() {
|
||||
*resolved = true;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(made_progress)
|
||||
}
|
||||
}
|
||||
Node::String { .. } | Node::Number { .. } | Node::Bool { .. } | Node::Null { .. } => {
|
||||
Ok(false)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn extend_from(&self, base: &Tree) {
|
||||
if Rc::ptr_eq(&self.0, &base.0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if let (
|
||||
Node::Object { children, .. },
|
||||
Node::Object {
|
||||
children: base_children,
|
||||
..
|
||||
},
|
||||
) = (&mut *self.0.borrow_mut(), &*base.0.borrow())
|
||||
{
|
||||
for (key, base_value) in base_children {
|
||||
if let Some(value) = children.get(key) {
|
||||
value.extend_from(base_value);
|
||||
} else {
|
||||
let base_value = base_value.clone();
|
||||
base_value
|
||||
.0
|
||||
.borrow_mut()
|
||||
.set_parent(Some(Rc::downgrade(&self.0)));
|
||||
children.insert(key.clone(), base_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Node {
|
||||
fn set_parent(&mut self, new_parent: Option<Weak<RefCell<Node>>>) {
|
||||
match self {
|
||||
Node::Reference { parent, .. }
|
||||
| Node::Object { parent, .. }
|
||||
| Node::Array { parent, .. }
|
||||
| Node::String { parent, .. }
|
||||
| Node::Number { parent, .. }
|
||||
| Node::Bool { parent, .. }
|
||||
| Node::Null { parent } => *parent = new_parent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_references() {
|
||||
let json = serde_json::json!({
|
||||
"a": {
|
||||
"extends": "$g",
|
||||
"x": "$b.d"
|
||||
},
|
||||
"b": {
|
||||
"c": "$a",
|
||||
"d": "$e.f"
|
||||
},
|
||||
"e": {
|
||||
"extends": "$a",
|
||||
"f": "1"
|
||||
},
|
||||
"g": {
|
||||
"h": 2
|
||||
}
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
resolve_references(json).unwrap(),
|
||||
serde_json::json!({
|
||||
"a": {
|
||||
"extends": "$g",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"b": {
|
||||
"c": {
|
||||
"extends": "$g",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"d": "1"
|
||||
},
|
||||
"e": {
|
||||
"extends": "$a",
|
||||
"f": "1",
|
||||
"x": "1",
|
||||
"h": 2
|
||||
},
|
||||
"g": {
|
||||
"h": 2
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycles() {
|
||||
let json = serde_json::json!({
|
||||
"a": {
|
||||
"b": "$c.d"
|
||||
},
|
||||
"c": {
|
||||
"d": "$a.b",
|
||||
},
|
||||
});
|
||||
|
||||
assert!(resolve_references(json).is_err());
|
||||
}
|
||||
}
|
270
crates/theme/src/theme_registry.rs
Normal file
270
crates/theme/src/theme_registry.rs
Normal file
|
@ -0,0 +1,270 @@
|
|||
use crate::{resolution::resolve_references, Theme};
|
||||
use anyhow::{Context, Result};
|
||||
use gpui::{fonts, AssetSource, FontCache};
|
||||
use parking_lot::Mutex;
|
||||
use serde_json::{Map, Value};
|
||||
use std::{collections::HashMap, sync::Arc};
|
||||
|
||||
pub struct ThemeRegistry {
|
||||
assets: Box<dyn AssetSource>,
|
||||
themes: Mutex<HashMap<String, Arc<Theme>>>,
|
||||
theme_data: Mutex<HashMap<String, Arc<Value>>>,
|
||||
font_cache: Arc<FontCache>,
|
||||
}
|
||||
|
||||
impl ThemeRegistry {
|
||||
pub fn new(source: impl AssetSource, font_cache: Arc<FontCache>) -> Arc<Self> {
|
||||
Arc::new(Self {
|
||||
assets: Box::new(source),
|
||||
themes: Default::default(),
|
||||
theme_data: Default::default(),
|
||||
font_cache,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn list(&self) -> impl Iterator<Item = String> {
|
||||
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 clear(&self) {
|
||||
self.theme_data.lock().clear();
|
||||
self.themes.lock().clear();
|
||||
}
|
||||
|
||||
pub fn get(&self, name: &str) -> Result<Arc<Theme>> {
|
||||
if let Some(theme) = self.themes.lock().get(name) {
|
||||
return Ok(theme.clone());
|
||||
}
|
||||
|
||||
let theme_data = self.load(name, true)?;
|
||||
let mut theme: Theme = fonts::with_font_cache(self.font_cache.clone(), || {
|
||||
serde_path_to_error::deserialize(theme_data.as_ref())
|
||||
})?;
|
||||
|
||||
theme.name = name.into();
|
||||
let theme = Arc::new(theme);
|
||||
self.themes.lock().insert(name.to_string(), theme.clone());
|
||||
Ok(theme)
|
||||
}
|
||||
|
||||
fn load(&self, name: &str, evaluate_references: bool) -> Result<Arc<Value>> {
|
||||
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<String, Value> = 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 base_theme_data = self
|
||||
.load(&base_name, false)
|
||||
.with_context(|| format!("failed to load base theme {}", base_name))?
|
||||
.as_ref()
|
||||
.clone();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let mut theme_data = Value::Object(theme_data);
|
||||
|
||||
// Find all of the key path references in the object, and then sort them according
|
||||
// to their dependencies.
|
||||
if evaluate_references {
|
||||
theme_data = resolve_references(theme_data)?;
|
||||
}
|
||||
|
||||
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<String, Value>, extension: Map<String, Value>) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use anyhow::anyhow;
|
||||
use gpui::MutableAppContext;
|
||||
|
||||
#[gpui::test]
|
||||
fn test_theme_extension(cx: &mut MutableAppContext) {
|
||||
let assets = TestAssets(&[
|
||||
(
|
||||
"themes/_base.toml",
|
||||
r##"
|
||||
[ui.active_tab]
|
||||
extends = "$ui.tab"
|
||||
border.color = "#666666"
|
||||
text = "$text_colors.bright"
|
||||
|
||||
[ui.tab]
|
||||
extends = "$ui.element"
|
||||
text = "$text_colors.dull"
|
||||
|
||||
[ui.element]
|
||||
background = "#111111"
|
||||
border = {width = 2.0, color = "#00000000"}
|
||||
|
||||
[editor]
|
||||
background = "#222222"
|
||||
default_text = "$text_colors.regular"
|
||||
"##,
|
||||
),
|
||||
(
|
||||
"themes/light.toml",
|
||||
r##"
|
||||
extends = "_base"
|
||||
|
||||
[text_colors]
|
||||
bright = "#ffffff"
|
||||
regular = "#eeeeee"
|
||||
dull = "#dddddd"
|
||||
|
||||
[editor]
|
||||
background = "#232323"
|
||||
"##,
|
||||
),
|
||||
]);
|
||||
|
||||
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
|
||||
let theme_data = registry.load("light", true).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
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",
|
||||
"text_colors": {
|
||||
"bright": "#ffffff",
|
||||
"regular": "#eeeeee",
|
||||
"dull": "#dddddd"
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_nested_extension(cx: &mut MutableAppContext) {
|
||||
let assets = TestAssets(&[(
|
||||
"themes/theme.toml",
|
||||
r##"
|
||||
[a]
|
||||
text = { extends = "$text.0" }
|
||||
|
||||
[b]
|
||||
extends = "$a"
|
||||
text = { extends = "$text.1" }
|
||||
|
||||
[text]
|
||||
0 = { color = "red" }
|
||||
1 = { color = "blue" }
|
||||
"##,
|
||||
)]);
|
||||
|
||||
let registry = ThemeRegistry::new(assets, cx.font_cache().clone());
|
||||
let theme_data = registry.load("theme", true).unwrap();
|
||||
assert_eq!(
|
||||
theme_data
|
||||
.get("b")
|
||||
.unwrap()
|
||||
.get("text")
|
||||
.unwrap()
|
||||
.get("color")
|
||||
.unwrap(),
|
||||
"blue"
|
||||
);
|
||||
}
|
||||
|
||||
struct TestAssets(&'static [(&'static str, &'static str)]);
|
||||
|
||||
impl AssetSource for TestAssets {
|
||||
fn load(&self, path: &str) -> Result<std::borrow::Cow<[u8]>> {
|
||||
if let Some(row) = self.0.iter().find(|e| e.0 == path) {
|
||||
Ok(row.1.as_bytes().into())
|
||||
} else {
|
||||
Err(anyhow!("no such path {}", path))
|
||||
}
|
||||
}
|
||||
|
||||
fn list(&self, prefix: &str) -> Vec<std::borrow::Cow<'static, str>> {
|
||||
self.0
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|(path, _)| {
|
||||
if path.starts_with(prefix) {
|
||||
Some(path.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue