GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: support app-specific configuration files override
src/config.rs | 44 +++++++++++++++++++++--
src/layout.rs | 77 ++++++++++++++++++++++++++++++++++++++++
src/widget/container/treelist.rs | 71 ++++++++++++++++++++++++++++--------
3 files changed, 176 insertions(+), 16 deletions(-)
diff --git a/src/config.rs b/src/config.rs
index 3e299f9..b7846b1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -197,12 +197,52 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
serde_json::Value::Object(map)
}
+pub fn get_app_name() -> Option<String> {
+ std::env::current_exe()
+ .ok()
+ .and_then(|p| {
+ p.file_name()
+ .and_then(|s| s.to_str().map(|ss| ss.to_string()))
+ })
+}
+
+pub fn get_app_config_path(app_name: &str) -> std::path::PathBuf {
+ get_config_path().parent().unwrap().join(app_name).join("config.kdl")
+}
+
+fn merge_json(a: &mut serde_json::Value, b: &serde_json::Value) {
+ match (a, b) {
+ (serde_json::Value::Object(a_map), serde_json::Value::Object(b_map)) => {
+ for (k, v) in b_map {
+ if !v.is_null() {
+ merge_json(a_map.entry(k.clone()).or_insert(serde_json::Value::Null), v);
+ }
+ }
+ }
+ (a_val, b_val) => {
+ *a_val = b_val.clone();
+ }
+ }
+}
+
pub fn parse_kdl_to_json(content: &str) -> serde_json::Value {
- if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
+ let mut main_val = if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
kdl_to_json(&doc)
} else {
serde_json::json!({})
+ };
+
+ if let Some(app_name) = get_app_name() {
+ let app_path = get_app_config_path(&app_name);
+ if let Ok(override_content) = std::fs::read_to_string(&app_path) {
+ if let Ok(override_doc) = override_content.parse::<kdl::KdlDocument>() {
+ let override_val = kdl_to_json(&override_doc);
+ merge_json(&mut main_val, &override_val);
+ }
+ }
}
+
+ main_val
}
pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, default_section: &str) -> bool {
@@ -339,7 +379,7 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
};
if let Some(ref ext_ty) = existing_ty {
- if ext_ty.starts_with("menu:") {
+ if ext_ty.starts_with("menu:") || ext_ty == "button" || ext_ty.starts_with("button:") {
kdl_ty = Some(ext_ty.clone());
}
}
diff --git a/src/layout.rs b/src/layout.rs
index 86f3060..b4b2804 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -116,6 +116,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
"style.data.tree.corner_radius" => "tree_corner_radius",
"style.data.tree.opacity" => "tree_opacity",
"style.data.tree.blur" => "tree_blur",
+ "style.data.tree.font" => "tree_font",
"style.surface.desktop.gap_color" => "desktop_gap_color",
"style.surface.desktop.cell_color" => "desktop_cell_color",
"style.surface.desktop.gap_width" => "desktop_gap_width",
@@ -269,6 +270,8 @@ static SLIDER_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
static PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
static LIST_FONT: RwLock<String> = RwLock::new(String::new());
static LIST_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
+static TREE_FONT: RwLock<String> = RwLock::new(String::new());
+static TREE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
static LIST_JUSTIFICATION: RwLock<u8> = RwLock::new(0);
static PLATE_OPACITY: RwLock<f32> = RwLock::new(1.0);
static PAGE_OPACITY: RwLock<f32> = RwLock::new(1.0);
@@ -311,6 +314,7 @@ pub fn reload_config() {
let mut spinbox_font_changed = false;
let mut slider_font_changed = false;
let mut list_font_changed = false;
+ let mut tree_font_changed = false;
for line in content.lines() {
let trimmed = line.trim();
if let Some(eq_idx) = trimmed.find('=') {
@@ -919,6 +923,21 @@ pub fn reload_config() {
list_font_changed = true;
}
}
+ if let Some(rest) = trimmed.strip_prefix("tree_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = mod_rest(rest);
+ let font = rest.trim().to_string();
+ let mut changed = false;
+ if let Ok(mut lock) = TREE_FONT.write() {
+ if *lock != font {
+ *lock = font;
+ changed = true;
+ }
+ }
+ if changed {
+ tree_font_changed = true;
+ }
+ }
if let Some(rest) = trimmed.strip_prefix("list_justification") {
let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
let val_str = rest.trim_end_matches('"').trim();
@@ -984,6 +1003,11 @@ pub fn reload_config() {
*lock = None;
}
}
+ if tree_font_changed {
+ if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+ *lock = None;
+ }
+ }
crate::color::reload_colors(&content);
}
}
@@ -1960,6 +1984,59 @@ pub fn set_list_font(font: &str) {
}
}
+// Tree Font
+pub fn tree_font() -> String {
+ use std::sync::Once;
+ static INIT: Once = Once::new();
+ INIT.call_once(|| {
+ let mut font = "Outfit".to_string();
+ if let Some(content) = read_config() {
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if let Some(rest) = trimmed.strip_prefix("tree_font") {
+ let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+ let rest = mod_rest(rest);
+ font = rest.trim().to_string();
+ }
+ }
+ }
+ if let Ok(mut lock) = TREE_FONT.write() {
+ *lock = font;
+ }
+ });
+ let lock = TREE_FONT.read().unwrap();
+ if lock.is_empty() {
+ "Outfit".to_string()
+ } else {
+ lock.clone()
+ }
+}
+
+pub fn tree_font_parsed() -> (String, f32) {
+ if let Ok(lock) = TREE_FONT_CACHED.read() {
+ if let Some(ref val) = *lock {
+ return val.clone();
+ }
+ }
+ let font_str = tree_font();
+ let parsed = parse_font_string(&font_str);
+ let size = parsed.1.unwrap_or(12.0);
+ let val = (parsed.0, size);
+ if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+ *lock = Some(val.clone());
+ }
+ val
+}
+
+pub fn set_tree_font(font: &str) {
+ if let Ok(mut lock) = TREE_FONT.write() {
+ *lock = font.to_string();
+ }
+ if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+ *lock = None;
+ }
+}
+
// List Justification
pub fn list_justification() -> u8 {
use std::sync::Once;
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 9d33e68..5a4d789 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -339,6 +339,10 @@ impl Element for TreeList {
true
}
+ fn widget_font(&self) -> Option<String> {
+ Some(crate::layout::tree_font())
+ }
+
fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
self.base.x = x;
self.base.y = y;
@@ -814,6 +818,9 @@ impl Element for TreeList {
]
};
+ let (_, tree_font_size) = crate::layout::tree_font_parsed();
+ let header_font_size = (tree_font_size - 1.0).max(8.0);
+
let mut labels = Vec::new();
let list_left = self.scroll_box.base.x;
let list_top = self.scroll_box.viewport_y;
@@ -827,21 +834,21 @@ impl Element for TreeList {
text: "Key".to_string(),
x: list_left + 8.0,
y: self.base.y + offset_y + 6.0,
- font_size: 11.0,
+ font_size: header_font_size,
color: [200, 200, 210],
});
labels.push(TextLabel {
text: "Type".to_string(),
x: list_left + 180.0 + 8.0,
y: self.base.y + offset_y + 6.0,
- font_size: 11.0,
+ font_size: header_font_size,
color: [200, 200, 210],
});
labels.push(TextLabel {
text: "Value".to_string(),
x: list_left + 235.0 + 8.0,
y: self.base.y + offset_y + 6.0,
- font_size: 11.0,
+ font_size: header_font_size,
color: [200, 200, 210],
});
@@ -859,7 +866,7 @@ impl Element for TreeList {
text: display_text,
x: list_left + 8.0 + *indent as f32 * 12.0,
y: row_y + 6.0,
- font_size: 12.0,
+ font_size: tree_font_size,
color: f32_to_rgb(crate::color::tree_section_text_color()),
});
}
@@ -883,7 +890,7 @@ impl Element for TreeList {
text: name.clone(),
x: list_left + 8.0 + *indent as f32 * 12.0,
y: row_y + 6.0,
- font_size: 12.0,
+ font_size: tree_font_size,
color,
});
}
@@ -919,6 +926,8 @@ impl Element for TreeList {
if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
if anno.starts_with("menu:") {
display_ty = Some("menu".to_string());
+ } else if anno == "button" || anno.starts_with("button:") {
+ display_ty = Some("button".to_string());
} else {
display_ty = Some(anno.clone());
}
@@ -934,12 +943,19 @@ impl Element for TreeList {
text: ty_text,
x: list_left + 190.0,
y: row_y + 6.0,
- font_size: 12.0,
+ font_size: tree_font_size,
color: f32_to_rgb(crate::color::tree_type_text_color()),
});
}
if Some(*original_idx) != self.selected_key_idx {
+ let mut is_button = false;
+ if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
+ if anno == "button" || anno.starts_with("button:") {
+ is_button = true;
+ }
+ }
+
let is_color = if let serde_json::Value::String(s) = val {
s.starts_with('#')
} else {
@@ -952,13 +968,23 @@ impl Element for TreeList {
list_left + 245.0
};
- labels.push(TextLabel {
- text: display_val,
- x: label_x,
- y: row_y + 6.0,
- font_size: 12.0,
- color: f32_to_rgb(crate::color::tree_value_text_color()),
- });
+ if is_button {
+ labels.push(TextLabel {
+ text: display_val,
+ x: list_left + 245.0 + 8.0,
+ y: row_y + 6.0,
+ font_size: tree_font_size,
+ color: [240, 240, 245],
+ });
+ } else {
+ labels.push(TextLabel {
+ text: display_val,
+ x: label_x,
+ y: row_y + 6.0,
+ font_size: tree_font_size,
+ color: f32_to_rgb(crate::color::tree_value_text_color()),
+ });
+ }
}
}
}
@@ -1161,8 +1187,25 @@ impl Element for TreeList {
quads.push((list_left + 180.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
quads.push((list_left + 235.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
+ let mut is_button = false;
+ if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
+ if anno == "button" || anno.starts_with("button:") {
+ is_button = true;
+ }
+ }
+
if Some(*original_idx) != self.selected_key_idx {
- if let serde_json::Value::String(s) = val {
+ if is_button {
+ let btn_x = list_left + 245.0;
+ let btn_y = row_y + 1.0;
+ let btn_bottom = (row_y + 27.0).min(list_bottom);
+ let btn_draw_y = btn_y.max(list_top);
+ let btn_draw_h = btn_bottom - btn_draw_y;
+ if btn_draw_h > 0.0 {
+ let btn_bg = [0.10, 0.29, 0.33, 0.65]; // theme button color
+ quads.push((btn_x, btn_draw_y, 125.0, btn_draw_h, 4.0, apply_opacity(btn_bg), (true, true, true, true)));
+ }
+ } else if let serde_json::Value::String(s) = val {
if s.starts_with('#') {
if let Some(rgba) = parse_hex_f32(s) {
let preview_x = list_left + 245.0;