GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
Fix context menu dangling pointer crash, resolve mutability UB, and migrate application config from TOML to JSON
src/main.rs | 23 ++----
src/widget/container/container.rs | 2 +-
src/widget/container/menu.rs | 4 +-
src/widget/container/scroll_box.rs | 2 +-
src/widget/core.rs | 22 +++++
src/widget/display/float3.rs | 2 +-
src/widget/display/graph.rs | 2 +-
src/widget/display/node.rs | 2 +-
src/widget/input/checkbox.rs | 8 +-
src/widget/input/color_selector.rs | 4 +-
src/widget/input/dropdown.rs | 4 +-
src/widget/input/font_selector.rs | 2 +-
src/widget/input/multi_control.rs | 159 +++++++++++++++++++++----------------
src/widget/input/slider.rs | 4 +-
src/widget/input/spinbox.rs | 4 +-
src/widget/input/text_box.rs | 4 +-
src/widget/mod.rs | 9 ++-
17 files changed, 149 insertions(+), 108 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index e557b41..b86443a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -686,21 +686,8 @@ impl State {
} else {
for (i, w) in self.widgets.iter().enumerate() {
for (label, font, bounds) in w.text_labels_with_font_and_bounds(ui_context) {
- let mut covered = false;
- for (pi, pw) in self.widgets.iter().enumerate() {
- if pi != i {
- if let Some((px, py, pw_val, ph)) = pw.popover_rect() {
- if label.is_covered_by(px, py, pw_val, ph) {
- covered = true;
- break;
- }
- }
- }
- }
- if !covered {
- widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
- widget_labels.push((label, bounds));
- }
+ widget_buffers.push(make_text_buffer_with_font(font_system, &label.text, label.font_size, font.as_deref()));
+ widget_labels.push((label, bounds));
}
}
@@ -1426,11 +1413,11 @@ impl AppState {
shift: self.shift_pressed,
};
- if crate::widget::context_menu::is_visible() {
- if custom_event.state == crate::widget::ElementState::Pressed
+ if cce_ui::widget::context_menu::is_visible() {
+ if custom_event.state == cce_ui::widget::ElementState::Pressed
&& custom_event.logical_key == Key::Named(NamedKey::Escape)
{
- crate::widget::context_menu::hide();
+ cce_ui::widget::context_menu::hide();
if let Some(st) = &mut self.state {
st.upload_vertices();
}
diff --git a/src/widget/container/container.rs b/src/widget/container/container.rs
index 0e96a0d..c3efa69 100644
--- a/src/widget/container/container.rs
+++ b/src/widget/container/container.rs
@@ -31,6 +31,6 @@ impl Element for Container {
impl Drop for Container {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index eeb606e..20b7c6d 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -923,7 +923,7 @@ impl Element for MenuBar {
}
impl Drop for MenuBar {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
@@ -1334,7 +1334,7 @@ unsafe impl Sync for Menu {}
impl Drop for Menu {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 59e5bd7..aa83ba3 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -197,7 +197,7 @@ impl Element for ScrollBox {
impl Drop for ScrollBox {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/core.rs b/src/widget/core.rs
index aade366..2f71627 100644
--- a/src/widget/core.rs
+++ b/src/widget/core.rs
@@ -612,6 +612,20 @@ pub mod context_menu {
CONTEXT_MENU.with(|m| m.borrow_mut().hide());
}
+ pub fn clear_if_matches(w: &dyn Element) {
+ CONTEXT_MENU.with(|m| {
+ let mut menu = m.borrow_mut();
+ if let Some(ptr) = menu.target {
+ let current_data = ptr as *const () as usize;
+ let query_data = w as *const dyn Element as *const () as usize;
+ if current_data == query_data {
+ menu.target = None;
+ menu.visible = false;
+ }
+ }
+ });
+ }
+
pub fn x() -> f32 { CONTEXT_MENU.with(|m| m.borrow().x) }
pub fn y() -> f32 { CONTEXT_MENU.with(|m| m.borrow().y) }
pub fn w() -> f32 { CONTEXT_MENU.with(|m| m.borrow().w) }
@@ -707,6 +721,11 @@ impl Widget {
}
+pub fn clear_widget_references(w: &dyn Element) {
+ focus::clear_if_matches(w);
+ context_menu::clear_if_matches(w);
+}
+
#[macro_export]
macro_rules! impl_widget_base {
($name:ident) => {
@@ -717,5 +736,8 @@ macro_rules! impl_widget_base {
fn as_ptr(&self) -> *mut (dyn $crate::widget::Element + 'static) {
self as *const Self as *mut Self as *mut (dyn $crate::widget::Element + 'static)
}
+ fn as_ptr_mut(&mut self) -> *mut (dyn $crate::widget::Element + 'static) {
+ self as *mut Self as *mut (dyn $crate::widget::Element + 'static)
+ }
};
}
diff --git a/src/widget/display/float3.rs b/src/widget/display/float3.rs
index 6c2a3a0..4119e91 100644
--- a/src/widget/display/float3.rs
+++ b/src/widget/display/float3.rs
@@ -310,7 +310,7 @@ impl Element for Float3 {
impl Drop for Float3 {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/display/graph.rs b/src/widget/display/graph.rs
index e2dd97d..ad58b85 100644
--- a/src/widget/display/graph.rs
+++ b/src/widget/display/graph.rs
@@ -827,7 +827,7 @@ impl Element for Graph {
impl Drop for Graph {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/display/node.rs b/src/widget/display/node.rs
index 07492de..6ba2e09 100644
--- a/src/widget/display/node.rs
+++ b/src/widget/display/node.rs
@@ -194,6 +194,6 @@ impl GeomController for Node {
impl Drop for Node {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/checkbox.rs b/src/widget/input/checkbox.rs
index a0aba44..2811093 100644
--- a/src/widget/input/checkbox.rs
+++ b/src/widget/input/checkbox.rs
@@ -82,7 +82,7 @@ impl Element for Checkbox {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -169,6 +169,12 @@ impl Element for Checkbox {
fn value(&self) -> i32 { if self.checked { 1 } else { 0 } }
}
+impl Drop for Checkbox {
+ fn drop(&mut self) {
+ clear_widget_references(self);
+ }
+}
+
#[derive(Debug, Clone)]
pub struct Toggle {
base: Widget,
diff --git a/src/widget/input/color_selector.rs b/src/widget/input/color_selector.rs
index 42c545d..85e4c4c 100644
--- a/src/widget/input/color_selector.rs
+++ b/src/widget/input/color_selector.rs
@@ -176,7 +176,7 @@ impl Element for ColorSelector {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -499,7 +499,7 @@ impl Element for ColorSelector {
impl Drop for ColorSelector {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 789aa3c..48f2e37 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -226,7 +226,7 @@ impl Element for Dropdown {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -390,7 +390,7 @@ impl Element for Dropdown {
impl Drop for Dropdown {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/font_selector.rs b/src/widget/input/font_selector.rs
index 6f12848..4003bc3 100644
--- a/src/widget/input/font_selector.rs
+++ b/src/widget/input/font_selector.rs
@@ -146,7 +146,7 @@ impl Element for FontSelector {
impl Drop for FontSelector {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/multi_control.rs b/src/widget/input/multi_control.rs
index bb2978a..6af7b1a 100644
--- a/src/widget/input/multi_control.rs
+++ b/src/widget/input/multi_control.rs
@@ -684,7 +684,7 @@ fn get_application_config_path() -> std::path::PathBuf {
.and_then(|p| p.file_name().map(|n| n.to_string_lossy().into_owned()))
.unwrap_or_else(|| "this-application".to_string());
- base_dir.join(app_name).join("this-application.toml")
+ base_dir.join(app_name).join("this-application.json")
}
fn save_config(name: &str, controls: &[InstancedControl]) {
@@ -693,89 +693,75 @@ fn save_config(name: &str, controls: &[InstancedControl]) {
let _ = std::fs::create_dir_all(parent);
}
- let json_val = serde_json::to_string(controls).unwrap_or_else(|_| "[]".to_string());
- let escaped_json = json_val.replace("'", "''");
- let new_line = format!("{} = '{}'", name, escaped_json);
-
- let content = std::fs::read_to_string(&path).unwrap_or_default();
- let mut lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
-
- let mut multicontrol_sec_idx = None;
- let mut next_section_idx = None;
- let mut key_idx = None;
+ let mut map = if path.exists() {
+ let content = std::fs::read_to_string(&path).unwrap_or_default();
+ serde_json::from_str::<serde_json::Map<String, serde_json::Value>>(&content)
+ .unwrap_or_default()
+ } else {
+ serde_json::Map::new()
+ };
- for (i, line) in lines.iter().enumerate() {
- let trimmed = line.trim();
- if trimmed == "[multicontrol]" {
- multicontrol_sec_idx = Some(i);
- } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
- if multicontrol_sec_idx.is_some() && next_section_idx.is_none() {
- next_section_idx = Some(i);
- }
- } else if let Some(_) = multicontrol_sec_idx {
- if next_section_idx.is_none() {
- if trimmed.starts_with(name) {
- if let Some(eq_idx) = trimmed.find('=') {
- if trimmed[..eq_idx].trim() == name {
- key_idx = Some(i);
- }
- }
- }
- }
- }
+ if let Ok(json_val) = serde_json::to_value(controls) {
+ map.insert(name.to_string(), json_val);
}
- if let Some(k_idx) = key_idx {
- lines[k_idx] = new_line;
- } else if let Some(m_idx) = multicontrol_sec_idx {
- let insert_idx = next_section_idx.unwrap_or(lines.len());
- lines.insert(insert_idx, new_line);
- } else {
- if !lines.is_empty() && !lines.last().unwrap().is_empty() {
- lines.push(String::new());
- }
- lines.push("[multicontrol]".to_string());
- lines.push(new_line);
+ if let Ok(updated_content) = serde_json::to_string_pretty(&map) {
+ let _ = std::fs::write(&path, updated_content);
}
-
- let _ = std::fs::write(&path, lines.join("\n"));
}
fn load_config(name: &str) -> Vec<InstancedControl> {
let path = get_application_config_path();
- let content = match std::fs::read_to_string(&path) {
- Ok(c) => c,
- Err(_) => return Vec::new(),
- };
-
- let mut multicontrol_sec = false;
- for line in content.lines() {
- let trimmed = line.trim();
- if trimmed == "[multicontrol]" {
- multicontrol_sec = true;
- } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
- multicontrol_sec = false;
- } else if multicontrol_sec {
- if trimmed.starts_with(name) {
- if let Some(eq_idx) = trimmed.find('=') {
- if trimmed[..eq_idx].trim() == name {
- let value_part = trimmed[eq_idx + 1..].trim();
- let json_str = if value_part.starts_with('\'') && value_part.ends_with('\'') {
- &value_part[1..value_part.len() - 1]
- } else if value_part.starts_with('"') && value_part.ends_with('"') {
- &value_part[1..value_part.len() - 1]
- } else {
- value_part
- };
- let json_str = json_str.replace("''", "'");
- if let Ok(controls) = serde_json::from_str::<Vec<InstancedControl>>(&json_str) {
- return controls;
+
+ if path.exists() {
+ if let Ok(content) = std::fs::read_to_string(&path) {
+ if let Ok(serde_json::Value::Object(map)) = serde_json::from_str(&content) {
+ if let Some(val) = map.get(name) {
+ if let Ok(controls) = serde_json::from_value::<Vec<InstancedControl>>(val.clone()) {
+ return controls;
+ }
+ }
+ }
+ }
+ return Vec::new();
+ }
+
+ let toml_path = path.with_extension("toml");
+ if toml_path.exists() {
+ if let Ok(content) = std::fs::read_to_string(&toml_path) {
+ let mut multicontrol_sec = false;
+ for line in content.lines() {
+ let trimmed = line.trim();
+ if trimmed == "[multicontrol]" {
+ multicontrol_sec = true;
+ } else if trimmed.starts_with('[') && trimmed.ends_with(']') {
+ multicontrol_sec = false;
+ } else if multicontrol_sec {
+ if trimmed.starts_with(name) {
+ if let Some(eq_idx) = trimmed.find('=') {
+ if trimmed[..eq_idx].trim() == name {
+ let value_part = trimmed[eq_idx + 1..].trim();
+ let json_str = if value_part.starts_with('\'') && value_part.ends_with('\'') {
+ &value_part[1..value_part.len() - 1]
+ } else if value_part.starts_with('"') && value_part.ends_with('"') {
+ &value_part[1..value_part.len() - 1]
+ } else {
+ value_part
+ };
+ let json_str = json_str.replace("''", "'");
+ if let Ok(controls) = serde_json::from_str::<Vec<InstancedControl>>(&json_str) {
+ save_config(name, &controls);
+ let _ = std::fs::remove_file(&toml_path);
+ return controls;
+ }
+ }
}
}
}
}
}
}
+
Vec::new()
}
@@ -836,6 +822,39 @@ mod tests {
}
}
+ #[test]
+ fn test_config_toml_to_json_migration() {
+ let _guard = ENV_MUTEX.lock().unwrap();
+ let temp_dir = std::env::temp_dir().join("cce_test_home_migration");
+ let _ = std::fs::create_dir_all(&temp_dir);
+ let old_home = std::env::var("HOME").ok();
+ std::env::set_var("HOME", temp_dir.to_str().unwrap());
+
+ let path = get_application_config_path();
+ let toml_path = path.with_extension("toml");
+ if let Some(parent) = toml_path.parent() {
+ let _ = std::fs::create_dir_all(parent);
+ }
+ let toml_content = "[multicontrol]\ntest_param = '[{\"key\":\"x\",\"control_type\":\"Spinbox\",\"value\":\"5\"}]'\n";
+ std::fs::write(&toml_path, toml_content).unwrap();
+
+ let loaded = load_config("test_param");
+ assert_eq!(loaded.len(), 1);
+ assert_eq!(loaded[0].key, "x");
+ assert_eq!(loaded[0].value, "5");
+
+ assert!(!toml_path.exists());
+ assert!(path.exists());
+
+ let json_content = std::fs::read_to_string(&path).unwrap();
+ assert!(json_content.contains("\"test_param\""));
+
+ let _ = std::fs::remove_dir_all(&temp_dir);
+ if let Some(h) = old_home {
+ std::env::set_var("HOME", h);
+ }
+ }
+
#[test]
fn test_popover_interaction() {
let _guard = ENV_MUTEX.lock().unwrap();
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index 7e93d06..721e479 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -234,7 +234,7 @@ impl Element for Slider {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -432,7 +432,7 @@ impl Element for Slider {
impl Drop for Slider {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/spinbox.rs b/src/widget/input/spinbox.rs
index f3bfba6..f7ba592 100644
--- a/src/widget/input/spinbox.rs
+++ b/src/widget/input/spinbox.rs
@@ -149,7 +149,7 @@ impl Element for Spinbox {
fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -388,7 +388,7 @@ impl Element for Spinbox {
impl Drop for Spinbox {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/input/text_box.rs b/src/widget/input/text_box.rs
index 201622b..36d6b25 100644
--- a/src/widget/input/text_box.rs
+++ b/src/widget/input/text_box.rs
@@ -506,7 +506,7 @@ impl Element for TextBox {
if self.disabled { return false; }
if button == MouseButton::Right && state == ElementState::Pressed {
if self.hit_test(px, py, ctx) {
- ctx.handle_right_click(self.as_ptr(), px, py);
+ ctx.handle_right_click(self.as_ptr_mut(), px, py);
return true;
}
}
@@ -941,7 +941,7 @@ impl Element for TextBox {
impl Drop for TextBox {
fn drop(&mut self) {
- focus::clear_if_matches(self);
+ clear_widget_references(self);
}
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index b3797f6..1c0367c 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -145,6 +145,13 @@ pub trait Element {
}
std::ptr::null_mut::<DummyElement>() as *mut (dyn Element + 'static)
}
+ fn as_ptr_mut(&mut self) -> *mut (dyn Element + 'static) {
+ struct DummyElement;
+ impl Element for DummyElement {
+ fn color(&self) -> [f32; 4] { [0.0, 0.0, 0.0, 0.0] }
+ }
+ std::ptr::null_mut::<DummyElement>() as *mut (dyn Element + 'static)
+ }
fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
match event {
@@ -526,7 +533,7 @@ pub mod editor;
// Re-exports
pub use self::editor::TextEditorState;
-pub use self::core::{Widget, focus, hover_animation, popovers, clipboard, context_menu};
+pub use self::core::{Widget, focus, hover_animation, popovers, clipboard, context_menu, clear_widget_references};
pub use self::input::{
Button, TextBox, Spinbox, Dropdown, Checkbox, Toggle, Slider, RangeSlider,
ColorSelector, Finger, Trackpad, Canvas, get_font_db, ActiveThumb, FontSelector,