GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Refactor path parsing to Option C config structure and add focus-on-right-click support for TextBox
src/color.rs | 42 +++++++-------------------
src/config.rs | 70 ++++----------------------------------------
src/widget/input/text_box.rs | 20 +++++++++++++
src/widget/json_layout.rs | 4 +--
4 files changed, 38 insertions(+), 98 deletions(-)
diff --git a/src/color.rs b/src/color.rs
index 83fa3b7..301b722 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -86,44 +86,22 @@ fn read_config() -> Option<String> {
None
}
-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
-}
-
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 Some(opacity) = val.pointer("/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) = get_config_val(&val, "/surfaces/backplate_opacity").or_else(|| get_config_val(&val, "/surfaces/window_opacity")).and_then(|v| v.as_f64()) {
+ if let Some(w_opacity) = val.pointer("/surfaces/backplate_opacity").or_else(|| val.pointer("/style/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) = 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 Some(radius) = val.pointer("/surfaces/backplate_corner_radius").or_else(|| val.pointer("/style/window/corner_radius")).and_then(|v| v.as_f64()) {
if let Ok(mut lock) = BACKPLATE_CORNER_RADIUS.write() {
*lock = radius as f32;
}
@@ -161,7 +139,7 @@ fn parse_and_set_colors(content: &str) {
};
let get_color = |pointer: &str| -> Option<[f32; 4]> {
- get_config_val(&val, pointer).and_then(|v| v.as_str()).and_then(parse_hex)
+ val.pointer(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")) {
@@ -176,7 +154,7 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/paginator_sidebar_color") {
if let Ok(mut lock) = SIDEBAR_BG_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/layout/primary_highlight_color") {
+ if let Some(c) = get_color("/style/highlight/primary") {
if let Ok(mut lock) = HIGHLIGHT_PRIMARY_COLOR.write() { *lock = [c[0], c[1], c[2], 0.12]; }
}
if let Some(c) = get_color("/layout/menubar_tab_label_color").or_else(|| get_color("/layout/paginator_tab_label_color")) {
@@ -185,7 +163,7 @@ fn parse_and_set_colors(content: &str) {
if let Some(c) = get_color("/layout/toggle_enabled_color") {
if let Ok(mut lock) = TOGGLE_ON_COLOR.write() { *lock = c; }
}
- if let Some(c) = get_color("/layout/toggle_disabled_color") {
+ if let Some(c) = get_color("/style/toggle/disabled_color") {
if let Ok(mut lock) = TOGGLE_OFF_COLOR.write() { *lock = c; }
}
if let Some(c) = get_color("/layout/toggle_bg_color") {
@@ -595,13 +573,13 @@ pub fn active_backplate_opacity() -> f32 {
_ => "window_backplate_opacity",
};
- if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", key)).and_then(|v| v.as_f64()) {
+ if let Some(opacity) = val.pointer(&format!("/style/window/{}", key)).and_then(|v| v.as_f64()) {
return opacity as f32;
}
// Modern surfaces fallback:
- if let Some(opacity) = get_config_val(&val, "/surfaces/backplate_opacity")
- .or_else(|| get_config_val(&val, "/surfaces/window_opacity"))
+ if let Some(opacity) = val.pointer("/surfaces/backplate_opacity")
+ .or_else(|| val.pointer("/style/window/opacity"))
.and_then(|v| v.as_f64()) {
return opacity as f32;
}
@@ -617,7 +595,7 @@ pub fn active_backplate_opacity() -> f32 {
_ => "window_opacity",
};
- if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", legacy_key)).and_then(|v| v.as_f64()) {
+ if let Some(opacity) = val.pointer(&format!("/style/window/{}", legacy_key)).and_then(|v| v.as_f64()) {
return opacity as f32;
}
diff --git a/src/config.rs b/src/config.rs
index 6fa9306..8e9960e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -133,77 +133,19 @@ 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>) {
+pub fn parse_config_path(key: &str, default_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()));
+ (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),
- }
+ (parts[0].to_string(), parts[1].to_string(), None)
} else {
- (section.to_string(), key.to_string(), None)
+ (default_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 (target_section, target_node, target_prop) = map_legacy_key(key, default_section);
+ let (target_section, target_node, target_prop) = parse_config_path(key, default_section);
let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == target_section) {
node
@@ -355,7 +297,7 @@ mod tests {
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");
+ let (sec, node, prop) = parse_config_path("style.status.box_opacity", "layout");
assert_eq!(sec, "style");
assert_eq!(node, "status");
assert_eq!(prop, Some("box_opacity".to_string()));
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index ac7058f..a77d5a3 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -512,6 +512,9 @@ impl Element for TextBox {
if self.disabled { return false; }
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
+ if !self.editing {
+ self.focus();
+ }
ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
@@ -1099,5 +1102,22 @@ mod tests {
assert_eq!(tb.cursor_idx, 4);
assert_eq!(tb.select_anchor, None);
}
+
+ #[test]
+ fn test_textbox_right_click_context_menu() {
+ let mut dummy = crate::context::UiContext::new();
+ let mut tb = TextBox::new("Context Menu Text".to_string());
+ tb.set_rect(10.0, 10.0, 200.0, 30.0);
+
+ // Hide context menu initially
+ dummy.hide_context_menu();
+ assert!(!dummy.is_context_menu_visible());
+
+ // Right click on textbox
+ let clicked = tb.mouse_input(MouseButton::Right, ElementState::Pressed, 50.0, 20.0, &mut dummy);
+ assert!(clicked);
+ assert!(dummy.is_context_menu_visible());
+ assert!(tb.editing);
+ }
}
diff --git a/src/widget/json_layout.rs b/src/widget/json_layout.rs
index d6eabfe..2ae7b8c 100644
--- a/src/widget/json_layout.rs
+++ b/src/widget/json_layout.rs
@@ -531,8 +531,8 @@ impl Element for JsonLayoutWidget {
let max_scroll_y = (total_height - visible_h).max(0.0);
if max_scroll_y > 0.0 {
let scroll_amount = match delta {
- crate::widget::MouseScrollDelta::LineDelta(_x, y) => *y * 24.0,
- crate::widget::MouseScrollDelta::PixelDelta(pos) => pos.y as f32,
+ crate::widget::MouseScrollDelta::LineDelta(_x, y) => -*y * 24.0,
+ crate::widget::MouseScrollDelta::PixelDelta(pos) => -pos.y as f32,
};
let old_scroll = self.page_scroll_y[active_page];
self.page_scroll_y[active_page] = (old_scroll + scroll_amount).clamp(0.0, max_scroll_y);