GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Group configuration styling properties into nested nodes (Option C)
src/color.rs | 62 ++++++++++++++-------------
src/config.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
src/layout.rs | 129 +++++++++++++++++++++++++++++++++++++-------------------
3 files changed, 243 insertions(+), 81 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index 4539bc0..83fa3b7 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -86,28 +86,44 @@ fn read_config() -> Option<String> {
None
}
-fn parse_and_set_colors(content: &str) {
- let val: serde_json::Value = match serde_json::from_str(content) {
- Ok(v) => v,
- Err(e) => {
- log::error!("JSON parse error: {}", e);
- return;
+fn get_config_val<'a>(val: &'a serde_json::Value, pointer: &str) -> Option<&'a serde_json::Value> {
+ if let Some(v) = val.pointer(pointer) {
+ return Some(v);
+ }
+ let parts: Vec<&str> = pointer.trim_start_matches('/').split('/').collect();
+ if parts.len() == 2 {
+ let section = parts[0];
+ let key = parts[1];
+ let (target_sec, target_node, target_prop) = crate::config::map_legacy_key(key, section);
+ if let Some(sec_val) = val.get(&target_sec) {
+ if let Some(node_val) = sec_val.get(&target_node) {
+ if let Some(prop_name) = target_prop {
+ return node_val.get(&prop_name);
+ } else {
+ return Some(node_val);
+ }
+ }
}
- };
+ }
+ None
+}
- if let Some(opacity) = val.pointer("/layout/menubar_opacity").and_then(|v| v.as_f64()) {
+fn parse_and_set_colors(content: &str) {
+ let val = crate::config::parse_kdl_to_json(content);
+
+ if let Some(opacity) = get_config_val(&val, "/layout/menubar_opacity").and_then(|v| v.as_f64()) {
if let Ok(mut lock) = OPACITY.write() {
*lock = Some(opacity as f32);
}
}
- if let Some(w_opacity) = val.pointer("/surfaces/backplate_opacity").or_else(|| val.pointer("/surfaces/window_opacity")).and_then(|v| v.as_f64()) {
+ if let Some(w_opacity) = get_config_val(&val, "/surfaces/backplate_opacity").or_else(|| get_config_val(&val, "/surfaces/window_opacity")).and_then(|v| v.as_f64()) {
if let Ok(mut lock) = BACKPLATE_OPACITY.write() {
*lock = Some(w_opacity as f32);
}
}
- if let Some(radius) = val.pointer("/surfaces/backplate_corner_radius").or_else(|| val.pointer("/surfaces/window_corner_radius")).and_then(|v| v.as_f64()) {
+ if let Some(radius) = get_config_val(&val, "/surfaces/backplate_corner_radius").or_else(|| get_config_val(&val, "/surfaces/window_corner_radius")).and_then(|v| v.as_f64()) {
if let Ok(mut lock) = BACKPLATE_CORNER_RADIUS.write() {
*lock = radius as f32;
}
@@ -145,7 +161,7 @@ fn parse_and_set_colors(content: &str) {
};
let get_color = |pointer: &str| -> Option<[f32; 4]> {
- val.pointer(pointer).and_then(|v| v.as_str()).and_then(parse_hex)
+ get_config_val(&val, pointer).and_then(|v| v.as_str()).and_then(parse_hex)
};
if let Some(c) = get_color("/surfaces/backplate_color").or_else(|| get_color("/surfaces/window_color")).or_else(|| get_color("/layout/page_low_color")) {
@@ -232,11 +248,7 @@ fn load_colors_once() {
}
pub fn reload_colors(content: &str) {
- if serde_json::from_str::<serde_json::Value>(content).is_ok() {
- parse_and_set_colors(content);
- } else if let Some(raw) = read_config() {
- parse_and_set_colors(&raw);
- }
+ parse_and_set_colors(content);
}
pub fn page_low_color() -> [f32; 4] {
@@ -531,10 +543,7 @@ pub fn active_window_mode() -> String {
Ok(c) => c,
Err(_) => return "floating".to_string(),
};
- let val: serde_json::Value = match serde_json::from_str(&content) {
- Ok(v) => v,
- Err(_) => return "floating".to_string(),
- };
+ let val = crate::config::parse_kdl_to_json(&content);
if let Some(mode_rules) = val.get("mode_rule").and_then(|r| r.as_array()) {
for rule in mode_rules {
@@ -574,10 +583,7 @@ pub fn active_backplate_opacity() -> f32 {
Ok(c) => c,
Err(_) => return 0.9,
};
- let val: serde_json::Value = match serde_json::from_str(&content) {
- Ok(v) => v,
- Err(_) => return 0.9,
- };
+ let val = crate::config::parse_kdl_to_json(&content);
let key = match mode.as_str() {
"fullscreen" => "fullscreen_backplate_opacity",
@@ -589,13 +595,13 @@ pub fn active_backplate_opacity() -> f32 {
_ => "window_backplate_opacity",
};
- if let Some(opacity) = val.pointer(&format!("/layout/{}", key)).and_then(|v| v.as_f64()) {
+ if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", key)).and_then(|v| v.as_f64()) {
return opacity as f32;
}
// Modern surfaces fallback:
- if let Some(opacity) = val.pointer("/surfaces/backplate_opacity")
- .or_else(|| val.pointer("/surfaces/window_opacity"))
+ if let Some(opacity) = get_config_val(&val, "/surfaces/backplate_opacity")
+ .or_else(|| get_config_val(&val, "/surfaces/window_opacity"))
.and_then(|v| v.as_f64()) {
return opacity as f32;
}
@@ -611,7 +617,7 @@ pub fn active_backplate_opacity() -> f32 {
_ => "window_opacity",
};
- if let Some(opacity) = val.pointer(&format!("/layout/{}", legacy_key)).and_then(|v| v.as_f64()) {
+ if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", legacy_key)).and_then(|v| v.as_f64()) {
return opacity as f32;
}
diff --git a/src/config.rs b/src/config.rs
index 3744676..6fa9306 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -133,11 +133,82 @@ pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, defaul
updated
}
+pub fn map_legacy_key(key: &str, section: &str) -> (String, String, Option<String>) {
+ let parts: Vec<&str> = key.split('.').collect();
+ if parts.len() == 3 {
+ return (parts[0].to_string(), parts[1].to_string(), Some(parts[2].to_string()));
+ } else if parts.len() == 2 {
+ return (parts[0].to_string(), parts[1].to_string(), None);
+ }
+
+ if section == "layout" {
+ match key {
+ "border_color" => ("style".to_string(), "border".to_string(), Some("color".to_string())),
+ "border_width" => ("style".to_string(), "border".to_string(), Some("width".to_string())),
+ "border_blur" => ("style".to_string(), "border".to_string(), Some("blur".to_string())),
+ "border_font_size" => ("style".to_string(), "border".to_string(), Some("font_size".to_string())),
+ "fullscreen_border_width" => ("style".to_string(), "border".to_string(), Some("fullscreen_border_width".to_string())),
+ "cascade_border_width" => ("style".to_string(), "border".to_string(), Some("cascade_border_width".to_string())),
+ "grid_border_width" => ("style".to_string(), "border".to_string(), Some("grid_border_width".to_string())),
+ "floating_border_width" => ("style".to_string(), "border".to_string(), Some("floating_border_width".to_string())),
+
+ "background_color" => ("style".to_string(), "background".to_string(), Some("color".to_string())),
+
+ "button_font" => ("style".to_string(), "button".to_string(), Some("font".to_string())),
+ "button_padding" => ("style".to_string(), "button".to_string(), Some("padding".to_string())),
+
+ "button_strip_font" => ("style".to_string(), "button_strip".to_string(), Some("font".to_string())),
+ "button_strip_spacing" => ("style".to_string(), "button_strip".to_string(), Some("spacing".to_string())),
+
+ "dropdown_height" => ("style".to_string(), "dropdown".to_string(), Some("height".to_string())),
+ "font_selector_height" => ("style".to_string(), "font_selector".to_string(), Some("height".to_string())),
+ "label_font" => ("style".to_string(), "label".to_string(), Some("font".to_string())),
+
+ "slider_height" => ("style".to_string(), "slider".to_string(), Some("height".to_string())),
+ "slider_corner_radius" => ("style".to_string(), "slider".to_string(), Some("corner_radius".to_string())),
+
+ "spinbox_height" => ("style".to_string(), "spinbox".to_string(), Some("height".to_string())),
+ "textbox_height" => ("style".to_string(), "textbox".to_string(), Some("height".to_string())),
+
+ "toggle_font" => ("style".to_string(), "toggle".to_string(), Some("font".to_string())),
+ "toggle_height" => ("style".to_string(), "toggle".to_string(), Some("height".to_string())),
+ "toggle_border_width" => ("style".to_string(), "toggle".to_string(), Some("border_width".to_string())),
+ "toggle_disabled_color" => ("style".to_string(), "toggle".to_string(), Some("disabled_color".to_string())),
+
+ "status_normal_color" => ("style".to_string(), "status".to_string(), Some("normal_color".to_string())),
+ "status_box_opacity" => ("style".to_string(), "status".to_string(), Some("box_opacity".to_string())),
+
+ "primary_highlight_color" => ("style".to_string(), "highlight".to_string(), Some("primary".to_string())),
+
+ "window_opacity" => ("style".to_string(), "window".to_string(), Some("opacity".to_string())),
+ "floating_backplate_opacity" => ("style".to_string(), "window".to_string(), Some("floating_backplate_opacity".to_string())),
+ "window_blur" => ("style".to_string(), "window".to_string(), Some("blur".to_string())),
+ "page_opacity" => ("style".to_string(), "window".to_string(), Some("page_opacity".to_string())),
+ "page_margin" => ("style".to_string(), "window".to_string(), Some("page_margin".to_string())),
+ "plate_padding" => ("style".to_string(), "window".to_string(), Some("plate_padding".to_string())),
+ "transition_duration" => ("style".to_string(), "window".to_string(), Some("transition_duration".to_string())),
+
+ "overlay_behavior" => ("style".to_string(), "overlay".to_string(), Some("behavior".to_string())),
+ "overlay_width" => ("style".to_string(), "overlay".to_string(), Some("width".to_string())),
+ "overlay_position" => ("style".to_string(), "overlay".to_string(), Some("position".to_string())),
+ "overlay_border_gap" => ("style".to_string(), "overlay".to_string(), Some("border_gap".to_string())),
+
+ "last_page" => ("style".to_string(), "editor".to_string(), Some("last_page".to_string())),
+
+ _ => (section.to_string(), key.to_string(), None),
+ }
+ } else {
+ (section.to_string(), key.to_string(), None)
+ }
+}
+
pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str, default_section: &str) -> bool {
- let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == default_section) {
+ let (target_section, target_node, target_prop) = map_legacy_key(key, default_section);
+
+ let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == target_section) {
node
} else {
- if let Ok(new_node) = format!("{}\n", default_section).parse::<kdl::KdlNode>() {
+ if let Ok(new_node) = format!("{}\n", target_section).parse::<kdl::KdlNode>() {
doc.nodes_mut().push(new_node);
doc.nodes_mut().last_mut().unwrap()
} else {
@@ -147,10 +218,10 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
let children = section_node.ensure_children();
- let child_node = if let Some(child) = children.nodes_mut().iter_mut().find(|n| n.name().value() == key) {
+ let child_node = if let Some(child) = children.nodes_mut().iter_mut().find(|n| n.name().value() == target_node) {
child
} else {
- if let Ok(new_child) = format!("{}\n", key).parse::<kdl::KdlNode>() {
+ if let Ok(new_child) = format!("{}\n", target_node).parse::<kdl::KdlNode>() {
children.nodes_mut().push(new_child);
children.nodes_mut().last_mut().unwrap()
} else {
@@ -175,12 +246,35 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
(kdl::KdlValue::String(s), None)
};
- child_node.entries_mut().clear();
- let mut entry = kdl::KdlEntry::new(kdl_val);
- if let Some(ty) = kdl_ty {
- entry.set_ty(ty);
+ if let Some(prop_name) = target_prop {
+ let mut found = false;
+ for entry in child_node.entries_mut() {
+ if let Some(id) = entry.name() {
+ if id.value() == prop_name {
+ *entry = kdl::KdlEntry::new_prop(prop_name.clone(), kdl_val.clone());
+ if let Some(ty) = kdl_ty {
+ entry.set_ty(ty);
+ }
+ found = true;
+ break;
+ }
+ }
+ }
+ if !found {
+ let mut entry = kdl::KdlEntry::new_prop(prop_name, kdl_val);
+ if let Some(ty) = kdl_ty {
+ entry.set_ty(ty);
+ }
+ child_node.entries_mut().push(entry);
+ }
+ } else {
+ child_node.entries_mut().clear();
+ let mut entry = kdl::KdlEntry::new(kdl_val);
+ if let Some(ty) = kdl_ty {
+ entry.set_ty(ty);
+ }
+ child_node.entries_mut().push(entry);
}
- child_node.entries_mut().push(entry);
true
}
@@ -251,3 +345,24 @@ pub fn write_config_value(path: &str, key: &str, value: &str, default_section: &
}
false
}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn test_nested_parsing() {
+ let content = "style {\n status box_opacity=(f64)0.75\n}\n";
+ let val = parse_kdl_to_json(content);
+ println!("val = {:?}", val);
+ let (sec, node, prop) = map_legacy_key("status_box_opacity", "layout");
+ assert_eq!(sec, "style");
+ assert_eq!(node, "status");
+ assert_eq!(prop, Some("box_opacity".to_string()));
+
+ let sec_val = val.get(&sec).unwrap();
+ let node_val = sec_val.get(&node).unwrap();
+ let prop_val = node_val.get(prop.as_ref().unwrap()).unwrap();
+ assert_eq!(prop_val.as_f64().unwrap(), 0.75);
+ }
+}
diff --git a/src/layout.rs b/src/layout.rs
index c989b30..6666a2f 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2,55 +2,96 @@ use crate::widget::Element;
use crate::context::UiContext;
use std::sync::RwLock;
-fn read_config() -> Option<String> {
- let path = crate::config::get_config_path();
- if let Ok(content) = std::fs::read_to_string(&path) {
- if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
- let mut toml_like = String::new();
- if let Some(layout) = val.get("layout").and_then(|l| l.as_object()) {
- for (k, v) in layout {
- if let Some(s) = v.as_str() {
- toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
- } else if let Some(b) = v.as_bool() {
- toml_like.push_str(&format!("{} = {}\n", k, b));
- } else if let Some(n) = v.as_f64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
- } else if let Some(n) = v.as_i64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
- }
- }
- }
- if let Some(notifications) = val.get("notifications").and_then(|n| n.as_object()) {
- toml_like.push_str("[notifications]\n");
- for (k, v) in notifications {
- if let Some(s) = v.as_str() {
- toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
- } else if let Some(b) = v.as_bool() {
- toml_like.push_str(&format!("{} = {}\n", k, b));
- } else if let Some(n) = v.as_f64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
- } else if let Some(n) = v.as_i64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
- }
- }
- }
- if let Some(transparency) = val.get("transparency").and_then(|t| t.as_object()) {
- toml_like.push_str("[transparency]\n");
- for (k, v) in transparency {
- if let Some(s) = v.as_str() {
- toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
- } else if let Some(b) = v.as_bool() {
- toml_like.push_str(&format!("{} = {}\n", k, b));
- } else if let Some(n) = v.as_f64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
- } else if let Some(n) = v.as_i64() {
- toml_like.push_str(&format!("{} = {}\n", k, n));
+fn flatten_map_json(val: &serde_json::Value, prefix: &str, toml_like: &mut String) {
+ match val {
+ serde_json::Value::Object(map) => {
+ for (k, v) in map {
+ let next_prefix = if prefix.is_empty() {
+ k.clone()
+ } else {
+ format!("{}.{}", prefix, k)
+ };
+ flatten_map_json(v, &next_prefix, toml_like);
+ }
+ }
+ _ => {
+ let flat_key = match prefix {
+ "style.border.color" => "border_color",
+ "style.border.width" => "border_width",
+ "style.border.blur" => "border_blur",
+ "style.border.font_size" => "border_font_size",
+ "style.border.fullscreen_border_width" => "fullscreen_border_width",
+ "style.border.cascade_border_width" => "cascade_border_width",
+ "style.border.grid_border_width" => "grid_border_width",
+ "style.border.floating_border_width" => "floating_border_width",
+ "style.background.color" => "background_color",
+ "style.button.font" => "button_font",
+ "style.button.padding" => "button_padding",
+ "style.button_strip.font" => "button_strip_font",
+ "style.button_strip.spacing" => "button_strip_spacing",
+ "style.dropdown.height" => "dropdown_height",
+ "style.font_selector.height" => "font_selector_height",
+ "style.label.font" => "label_font",
+ "style.slider.height" => "slider_height",
+ "style.slider.corner_radius" => "slider_corner_radius",
+ "style.spinbox.height" => "spinbox_height",
+ "style.textbox.height" => "textbox_height",
+ "style.toggle.font" => "toggle_font",
+ "style.toggle.height" => "toggle_height",
+ "style.toggle.border_width" => "toggle_border_width",
+ "style.toggle.disabled_color" => "toggle_disabled_color",
+ "style.status.normal_color" => "status_normal_color",
+ "style.status.box_opacity" => "status_box_opacity",
+ "style.highlight.primary" => "primary_highlight_color",
+ "style.window.opacity" => "window_opacity",
+ "style.window.floating_backplate_opacity" => "floating_backplate_opacity",
+ "style.window.blur" => "window_blur",
+ "style.window.page_opacity" => "page_opacity",
+ "style.window.page_margin" => "page_margin",
+ "style.window.plate_padding" => "plate_padding",
+ "style.window.transition_duration" => "transition_duration",
+ "style.overlay.behavior" => "overlay_behavior",
+ "style.overlay.width" => "overlay_width",
+ "style.overlay.position" => "overlay_position",
+ "style.overlay.border_gap" => "overlay_border_gap",
+ "style.editor.last_page" => "last_page",
+
+ other => {
+ if let Some(rest) = other.strip_prefix("layout.") {
+ rest
+ } else if let Some(rest) = other.strip_prefix("transparency.") {
+ rest
+ } else {
+ if let Some(idx) = other.find('.') {
+ &other[idx + 1..]
+ } else {
+ other
}
}
}
- return Some(toml_like);
+ };
+
+ if let Some(s) = val.as_str() {
+ toml_like.push_str(&format!("{} = \"{}\"\n", flat_key, s));
+ } else if let Some(b) = val.as_bool() {
+ toml_like.push_str(&format!("{} = {}\n", flat_key, b));
+ } else if let Some(n) = val.as_f64() {
+ toml_like.push_str(&format!("{} = {}\n", flat_key, n));
+ } else if let Some(n) = val.as_i64() {
+ toml_like.push_str(&format!("{} = {}\n", flat_key, n));
+ }
}
}
+}
+
+fn read_config() -> Option<String> {
+ let path = crate::config::get_config_path();
+ if let Ok(content) = std::fs::read_to_string(&path) {
+ let val = crate::config::parse_kdl_to_json(&content);
+ let mut toml_like = String::new();
+ flatten_map_json(&val, "", &mut toml_like);
+ return Some(toml_like);
+ }
None
}