GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Implement hierarchical keybindings and dynamic TextBox text measurement
src/config.rs | 90 ++++++++++++++++++++++++++++++++++++--------
src/widget/input/text_box.rs | 61 +++++++++++++++++-------------
2 files changed, 110 insertions(+), 41 deletions(-)
diff --git a/src/config.rs b/src/config.rs
index cbfc5d8..8bac96a 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -32,7 +32,42 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
}
}
- let val = if let Some(children) = node.children() {
+ let val = if name == "key_bindings" {
+ if let Some(children) = node.children() {
+ let mut binds = Vec::new();
+ for child in children.nodes() {
+ let mut child_map = serde_json::Map::new();
+ for entry in child.entries() {
+ if let Some(prop_name) = entry.name() {
+ let j_val = match entry.value() {
+ kdl::KdlValue::Bool(b) => serde_json::Value::Bool(*b),
+ kdl::KdlValue::Base2(i) |
+ kdl::KdlValue::Base8(i) |
+ kdl::KdlValue::Base10(i) |
+ kdl::KdlValue::Base16(i) => serde_json::Value::Number(serde_json::Number::from(*i)),
+ kdl::KdlValue::Base10Float(f) => {
+ if let Some(num) = serde_json::Number::from_f64(*f) {
+ serde_json::Value::Number(num)
+ } else {
+ serde_json::Value::Null
+ }
+ }
+ kdl::KdlValue::String(s) |
+ kdl::KdlValue::RawString(s) => serde_json::Value::String(s.clone()),
+ kdl::KdlValue::Null => serde_json::Value::Null,
+ };
+ child_map.insert(prop_name.value().to_string(), j_val);
+ }
+ }
+ binds.push(serde_json::Value::Object(child_map));
+ }
+ serde_json::Value::Array(binds)
+ } else if has_props {
+ serde_json::Value::Object(node_map)
+ } else {
+ serde_json::Value::Null
+ }
+ } else if let Some(children) = node.children() {
kdl_to_json(children)
} else if has_props {
serde_json::Value::Object(node_map)
@@ -61,17 +96,40 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
if let Some(existing) = map.remove(&name) {
match existing {
serde_json::Value::Array(mut arr) => {
- arr.push(val);
+ match val {
+ serde_json::Value::Array(new_arr) => {
+ arr.extend(new_arr);
+ }
+ _ => {
+ arr.push(val);
+ }
+ }
map.insert(name, serde_json::Value::Array(arr));
}
other => {
- map.insert(name, serde_json::Value::Array(vec![other, val]));
+ match val {
+ serde_json::Value::Array(new_arr) => {
+ let mut combined = vec![other];
+ combined.extend(new_arr);
+ map.insert(name, serde_json::Value::Array(combined));
+ }
+ _ => {
+ map.insert(name, serde_json::Value::Array(vec![other, val]));
+ }
+ }
}
}
} else {
let list_names = ["key_bindings", "pointer_bind", "gesture_bind", "mode_rule", "tag_layout", "startup", "device"];
if list_names.contains(&name.as_str()) {
- map.insert(name, serde_json::Value::Array(vec![val]));
+ match val {
+ serde_json::Value::Array(_) => {
+ map.insert(name, val);
+ }
+ _ => {
+ map.insert(name, serde_json::Value::Array(vec![val]));
+ }
+ }
} else {
map.insert(name, val);
}
@@ -336,7 +394,8 @@ pub fn write_keybindings_to_kdl(path: &str, keybinds: &[serde_json::Value]) -> b
// Remove all existing key_bindings nodes
doc.nodes_mut().retain(|n| n.name().value() != "key_bindings");
- // Add new key_bindings nodes
+ // Construct nested key_bindings block
+ let mut block_str = "key_bindings {\n".to_string();
for v in keybinds {
if let Some(obj) = v.as_object() {
let mods = obj.get("mods").and_then(|m| m.as_str()).unwrap_or("");
@@ -344,26 +403,27 @@ pub fn write_keybindings_to_kdl(path: &str, keybinds: &[serde_json::Value]) -> b
let action = obj.get("action").and_then(|a| a.as_str()).unwrap_or("");
let command = obj.get("command").and_then(|c| c.as_str()).unwrap_or("");
- let mut node_str = "key_bindings".to_string();
+ block_str.push_str(" bind");
if !action.is_empty() {
- node_str.push_str(&format!(" action={:?}", action));
+ block_str.push_str(&format!(" action={:?}", action));
}
if !command.is_empty() {
- node_str.push_str(&format!(" command={:?}", command));
+ block_str.push_str(&format!(" command={:?}", command));
}
if !key.is_empty() {
- node_str.push_str(&format!(" key={:?}", key));
+ block_str.push_str(&format!(" key={:?}", key));
}
if !mods.is_empty() {
- node_str.push_str(&format!(" mods={:?}", mods));
- }
- node_str.push('\n');
-
- if let Ok(node) = node_str.parse::<kdl::KdlNode>() {
- doc.nodes_mut().push(node);
+ block_str.push_str(&format!(" mods={:?}", mods));
}
+ block_str.push('\n');
}
}
+ block_str.push_str("}\n");
+
+ if let Ok(node) = block_str.parse::<kdl::KdlNode>() {
+ doc.nodes_mut().push(node);
+ }
let updated_str = doc.to_string();
safe_write(path, &updated_str)
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index d0e1970..d61a77e 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -92,7 +92,6 @@ impl TextBox {
self.text_color = color;
self
}
-
pub fn with_font_size(mut self, size: f32) -> Self {
self.font_size = size;
self
@@ -103,6 +102,14 @@ impl TextBox {
self
}
+ pub fn char_width(&self) -> f32 {
+ crate::widget::display::measure_text_width("M", &self.font_family, self.font_size)
+ }
+
+ pub fn line_height(&self) -> f32 {
+ self.font_size * 1.333
+ }
+
pub fn wrap_text(&self, max_chars_per_line: usize) -> (Vec<String>, Vec<(usize, usize)>) {
let text_src = if self.editing { &self.edit_buffer } else { &self.text };
let chars: Vec<char> = text_src.chars().collect();
@@ -331,8 +338,8 @@ impl TextBox {
self.scroll_y = 0.0;
return;
}
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, _) = self.wrap_text(max_chars);
let content_h = lines.len() as f32 * line_height;
@@ -342,8 +349,8 @@ impl TextBox {
pub fn scroll_to_cursor(&mut self) {
if !self.multiline { return; }
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (_, index_map) = self.wrap_text(max_chars);
if index_map.is_empty() { return; }
@@ -493,9 +500,9 @@ impl Element for TextBox {
}
let mut changed = false;
if self.dragging && self.editing {
- let char_width = self.font_size * 0.6;
+ let char_width = self.char_width();
let drag_idx = if self.multiline {
- let line_height = self.font_size * 1.333;
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let top = self.base.label_offset();
@@ -536,10 +543,10 @@ impl Element for TextBox {
fn drag_update(&mut self, px: f32, py: f32) -> bool {
if self.disabled || !self.editing { return false; }
- let char_width = self.font_size * 0.6;
+ let char_width = self.char_width();
let top = self.base.label_offset();
let drag_idx = if self.multiline {
- let line_height = self.font_size * 1.333;
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let click_line = (((py - (self.base.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
@@ -586,10 +593,10 @@ impl Element for TextBox {
if !self.editing {
self.focus();
} else {
- let char_width = self.font_size * 0.6;
+ let char_width = self.char_width();
let top = self.base.label_offset();
let idx = if self.multiline {
- let line_height = self.font_size * 1.333;
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let click_line = (((py - (self.base.y + top + 8.0) + self.scroll_y) / line_height).floor() as isize).max(0) as usize;
@@ -681,7 +688,7 @@ impl Element for TextBox {
state.clear_selection();
}
if self.multiline {
- let char_width = self.font_size * 0.6;
+ let char_width = self.char_width();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
@@ -706,7 +713,7 @@ impl Element for TextBox {
state.clear_selection();
}
if self.multiline {
- let char_width = self.font_size * 0.6;
+ let char_width = self.char_width();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, index_map) = self.wrap_text(max_chars);
let (cursor_l, cursor_c) = index_map[state.cursor_idx.min(index_map.len() - 1)];
@@ -834,8 +841,8 @@ impl Element for TextBox {
}
if self.editing || self.select_anchor.is_some() {
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let highlight_color = [0.20, 0.50, 0.85, 0.3];
let cursor_color = if self.draw_bg_border {
@@ -967,8 +974,8 @@ impl Element for TextBox {
}
if self.editing || self.select_anchor.is_some() {
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let highlight_color = [0.20, 0.50, 0.85, 0.3];
let cursor_color = if self.draw_bg_border {
@@ -1103,8 +1110,8 @@ impl Element for TextBox {
};
if self.multiline {
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, _) = self.wrap_text(max_chars);
let lines_to_draw = if is_placeholder {
@@ -1164,8 +1171,8 @@ impl Element for TextBox {
fn mouse_wheel(&mut self, delta: &MouseScrollDelta, _px: f32, _py: f32, ctx: &mut UiContext) -> bool {
if self.disabled || !self.multiline { return false; }
- let char_width = self.font_size * 0.6;
- let line_height = self.font_size * 1.333;
+ let char_width = self.char_width();
+ let line_height = self.line_height();
let max_chars = (((self.base.w - 16.0) / char_width).floor() as usize).max(1);
let (lines, _) = self.wrap_text(max_chars);
let content_h = lines.len() as f32 * line_height;
@@ -1264,16 +1271,18 @@ mod tests {
assert_eq!(tb.cursor_idx, 11);
assert_eq!(tb.select_anchor, Some(0));
- // 2. Click inside placed caret at index 5 (x = 10 + 8 + 5 * 7.2 = 54)
- let pressed_inside = tb.mouse_input(MouseButton::Left, ElementState::Pressed, 54.0, 20.0, &mut dummy);
+ // 2. Click inside placed caret at index 5
+ let click_x5 = 10.0 + 8.0 + 5.0 * tb.char_width();
+ let pressed_inside = tb.mouse_input(MouseButton::Left, ElementState::Pressed, click_x5, 20.0, &mut dummy);
assert!(pressed_inside);
assert_eq!(tb.cursor_idx, 5);
assert_eq!(tb.select_anchor, Some(5));
assert!(!tb.all_selected);
- // 3. Drag to index 11 (x = 10 + 8 + 11 * 7.2 = 97.2)
- tb.drag_begin(54.0, 20.0);
- let updated = tb.drag_update(97.2, 20.0);
+ // 3. Drag to index 11
+ let drag_x11 = 10.0 + 8.0 + 11.0 * tb.char_width();
+ tb.drag_begin(click_x5, 20.0);
+ let updated = tb.drag_update(drag_x11, 20.0);
assert!(updated);
assert_eq!(tb.cursor_idx, 11);
assert_eq!(tb.select_anchor, Some(5));