git.lucas.co / cce-data-editor
structured data editor
git clone https://git.lucas.co/cce-data-editor.git

commitf20825aef3f459b2ef76b733ab53fce412e6631f
authorLucas Galante <[email protected]>
date2026-06-29 11:21
Initial commit of cce-data-editor

 .gitignore  |    1 +
 Cargo.toml  |   20 ++
 Makefile    |   14 +
 src/main.rs | 1156 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 4 files changed, 1191 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..eb42264
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "cce-data-editor"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+cce-ui = { path = "../cce-ui" }
+smithay-client-toolkit = "0.19.2"
+calloop = "0.13.0"
+calloop-wayland-source = "0.3.0"
+wayland-client = { version = "0.31", features = ["system"] }
+xkeysym = "0.2"
+raw-window-handle = "0.6"
+wgpu = "24"
+bytemuck = { version = "1", features = ["derive"] }
+pollster = "0.4"
+glyphon = "0.8"
+tokio = { version = "1", features = ["full"] }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..7dd7e69
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,14 @@
+.PHONY: build install run clean
+
+build:
+	cargo build --release
+
+install: build
+	mkdir -p ~/.local/bin
+	install -m 755 ../target/release/cce-data-editor ~/.local/bin/cce-data-editor
+
+run:
+	cargo run
+
+clean:
+	cargo clean
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..387f4cf
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,1156 @@
+use wayland_client::QueueHandle;
+use glyphon::{FontSystem, Buffer, Metrics, Attrs};
+use cce_ui::engine::{Application, EngineState, LogicalPosition, LogicalSize, WindowSettings};
+use cce_ui::widget::{
+    MouseButton, ElementState, MouseScrollDelta, KeyEvent, TextItem, Element,
+    TextBox, Button, TextLabel, Key
+};
+
+#[derive(Debug, Clone)]
+enum AppMessage {
+    Exit,
+    OpenDocument,
+    SaveDocument,
+    SaveDocumentAs,
+    FormatJson,
+    AddKey,
+    ApplyValue,
+    DeleteKey,
+}
+
+#[derive(Debug, Clone)]
+enum PathToken {
+    Key(String),
+    Index(usize),
+}
+
+fn parse_path(path: &str) -> Vec<PathToken> {
+    let mut tokens = Vec::new();
+    for part in path.split('.') {
+        if part.is_empty() { continue; }
+        if let Some(bracket_idx) = part.find('[') {
+            let name = &part[..bracket_idx];
+            if !name.is_empty() {
+                tokens.push(PathToken::Key(name.to_string()));
+            }
+            let mut rest = &part[bracket_idx..];
+            while let Some(start) = rest.find('[') {
+                if let Some(end) = rest.find(']') {
+                    let idx_str = &rest[start + 1..end];
+                    if let Ok(idx) = idx_str.parse::<usize>() {
+                        tokens.push(PathToken::Index(idx));
+                    }
+                    rest = &rest[end + 1..];
+                } else {
+                    break;
+                }
+            }
+        } else {
+            tokens.push(PathToken::Key(part.to_string()));
+        }
+    }
+    tokens
+}
+
+fn insert_value(target: &mut serde_json::Value, tokens: &[PathToken], value: serde_json::Value) {
+    if tokens.is_empty() {
+        *target = value;
+        return;
+    }
+    
+    match &tokens[0] {
+        PathToken::Key(key) => {
+            if !target.is_object() {
+                *target = serde_json::Value::Object(serde_json::Map::new());
+            }
+            let map = target.as_object_mut().unwrap();
+            
+            let next_default = if tokens.len() > 1 {
+                match &tokens[1] {
+                    PathToken::Key(_) => serde_json::Value::Object(serde_json::Map::new()),
+                    PathToken::Index(_) => serde_json::Value::Array(Vec::new()),
+                }
+            } else {
+                serde_json::Value::Null
+            };
+            
+            let entry = map.entry(key.clone()).or_insert(next_default);
+            insert_value(entry, &tokens[1..], value);
+        }
+        PathToken::Index(idx) => {
+            if !target.is_array() {
+                *target = serde_json::Value::Array(Vec::new());
+            }
+            let arr = target.as_array_mut().unwrap();
+            
+            let next_default = if tokens.len() > 1 {
+                match &tokens[1] {
+                    PathToken::Key(_) => serde_json::Value::Object(serde_json::Map::new()),
+                    PathToken::Index(_) => serde_json::Value::Array(Vec::new()),
+                }
+            } else {
+                serde_json::Value::Null
+            };
+            
+            while arr.len() <= *idx {
+                arr.push(next_default.clone());
+            }
+            insert_value(&mut arr[*idx], &tokens[1..], value);
+        }
+    }
+}
+
+fn flatten_json(value: &serde_json::Value, prefix: &str, out: &mut Vec<(String, serde_json::Value)>) {
+    match value {
+        serde_json::Value::Object(map) => {
+            if map.is_empty() {
+                out.push((prefix.to_string(), value.clone()));
+            } else {
+                for (k, v) in map {
+                    let new_prefix = if prefix.is_empty() {
+                        k.clone()
+                    } else {
+                        format!("{}.{}", prefix, k)
+                    };
+                    flatten_json(v, &new_prefix, out);
+                }
+            }
+        }
+        serde_json::Value::Array(arr) => {
+            if arr.is_empty() {
+                out.push((prefix.to_string(), value.clone()));
+            } else {
+                for (i, v) in arr.iter().enumerate() {
+                    let new_prefix = if prefix.is_empty() {
+                        format!("[{}]", i)
+                    } else {
+                        format!("{}[{}]", prefix, i)
+                    };
+                    flatten_json(v, &new_prefix, out);
+                }
+            }
+        }
+        _ => {
+            out.push((prefix.to_string(), value.clone()));
+        }
+    }
+}
+
+fn unflatten_json(flat: &[(String, serde_json::Value)]) -> serde_json::Value {
+    let mut root = serde_json::Value::Null;
+    for (path, val) in flat {
+        let tokens = parse_path(path);
+        insert_value(&mut root, &tokens, val.clone());
+    }
+    root
+}
+
+fn bottom_y_calc(height: u32) -> f32 {
+    height as f32 - 180.0
+}
+
+struct DataEditorApp {
+    // Toolbar Buttons
+    btn_open: Button,
+    btn_save: Button,
+    btn_save_as: Button,
+    btn_format: Button,
+    btn_exit: Button,
+
+    // Left Panel Form Edit
+    flat_keys: Vec<(String, serde_json::Value)>,
+    selected_key_idx: Option<usize>,
+    scroll_y: f32,
+    hovered_row_idx: Option<usize>,
+
+    // New Key input
+    new_key_editor: TextBox,
+    btn_add_key: Button,
+
+    // Edit Value input
+    selected_value_editor: TextBox,
+    btn_apply_val: Button,
+    btn_delete_key: Button,
+
+    // Right Panel Raw Json
+    raw_json_editor: TextBox,
+
+    // App state
+    current_file_path: Option<std::path::PathBuf>,
+    status_message: Option<(String, bool)>,
+
+    // UI state
+    width: u32,
+    height: u32,
+    scale_factor: f64,
+    text_items: Vec<TextItem>,
+    font_system: FontSystem,
+    needs_rebuild: bool,
+    ui_context: cce_ui::context::UiContext,
+    ctrl_pressed: bool,
+    initial_focus: bool,
+}
+
+impl DataEditorApp {
+    fn pick_file_to_open(&self) -> Option<std::path::PathBuf> {
+        let output = std::process::Command::new("/home/lsgalante/.local/bin/cce-files")
+            .arg("--select")
+            .output()
+            .or_else(|_| {
+                std::process::Command::new("cce-files")
+                    .arg("--select")
+                    .output()
+            })
+            .ok()?;
+        
+        if output.status.success() {
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            let trimmed = stdout.trim();
+            if !trimmed.is_empty() {
+                return Some(std::path::PathBuf::from(trimmed));
+            }
+        }
+        None
+    }
+
+    fn pick_file_to_save(&self) -> Option<std::path::PathBuf> {
+        let output = std::process::Command::new("/home/lsgalante/.local/bin/cce-files")
+            .arg("--save")
+            .output()
+            .or_else(|_| {
+                std::process::Command::new("cce-files")
+                    .arg("--save")
+                    .output()
+            })
+            .ok()?;
+        
+        if output.status.success() {
+            let stdout = String::from_utf8_lossy(&output.stdout);
+            let trimmed = stdout.trim();
+            if !trimmed.is_empty() {
+                return Some(std::path::PathBuf::from(trimmed));
+            }
+        }
+        None
+    }
+
+    fn update_raw_from_flat(&mut self) {
+        let root = unflatten_json(&self.flat_keys);
+        if let Ok(pretty) = serde_json::to_string_pretty(&root) {
+            self.raw_json_editor.text = pretty;
+            self.raw_json_editor.edit_buffer = self.raw_json_editor.text.clone();
+            self.raw_json_editor.sync_editor_state();
+        }
+    }
+
+    fn add_textbox_labels(
+        editor: &TextBox,
+        ui_context: &cce_ui::context::UiContext,
+        font_system: &mut FontSystem,
+        text_items: &mut Vec<TextItem>,
+        scale: f32,
+        bounds: Option<[f32; 4]>,
+    ) {
+        let font_family = editor.font_family.clone();
+        for (label, tb_bounds) in editor.text_labels_with_bounds(ui_context) {
+            let physical_size = label.font_size * scale;
+            let metrics = Metrics::new(physical_size, physical_size * 1.4);
+            let mut buf = Buffer::new(font_system, metrics);
+            let family_val = match font_family.as_str() {
+                "monospace" => glyphon::Family::Name(cce_ui::layout::get_system_monospace_font()),
+                "sans-serif" => glyphon::Family::SansSerif,
+                "serif" => glyphon::Family::Serif,
+                _ => glyphon::Family::Name(&font_family),
+            };
+            let attrs = Attrs::new().family(family_val);
+            buf.set_text(font_system, &label.text, attrs, glyphon::Shaping::Advanced);
+            buf.shape_until_scroll(font_system, true);
+            text_items.push(TextItem {
+                buffer: buf,
+                x: label.x,
+                y: label.y,
+                color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+                bounds: bounds.or(tb_bounds),
+            });
+        }
+    }
+
+    fn rebuild_text_items(&mut self) {
+        self.raw_json_editor.prepare_text(&mut self.font_system);
+        self.selected_value_editor.prepare_text(&mut self.font_system);
+        self.new_key_editor.prepare_text(&mut self.font_system);
+        self.text_items.clear();
+        let scale = cce_ui::scale::scale_factor();
+        let mut labels = Vec::new();
+
+        // 1. Button labels
+        labels.extend(self.btn_open.text_labels());
+        labels.extend(self.btn_save.text_labels());
+        labels.extend(self.btn_save_as.text_labels());
+        labels.extend(self.btn_format.text_labels());
+        labels.extend(self.btn_exit.text_labels());
+        labels.extend(self.btn_add_key.text_labels());
+        labels.extend(self.btn_apply_val.text_labels());
+        labels.extend(self.btn_delete_key.text_labels());
+
+        // 2. Section labels
+        let bottom_y = bottom_y_calc(self.height);
+        labels.push(TextLabel {
+            text: "Add New Key:".to_string(),
+            x: 10.0,
+            y: bottom_y - 6.0,
+            font_size: 11.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+
+        let selected_key_name = match self.selected_key_idx {
+            Some(idx) => {
+                let k = &self.flat_keys[idx].0;
+                if k.len() > 30 {
+                    format!("Edit Key: ...{}", &k[k.len() - 27..])
+                } else {
+                    format!("Edit Key: {}", k)
+                }
+            }
+            None => "No Key Selected".to_string(),
+        };
+        labels.push(TextLabel {
+            text: selected_key_name,
+            x: 10.0,
+            y: bottom_y + 54.0,
+            font_size: 11.0,
+            color: [0x83, 0x83, 0x8a],
+        });
+
+        // 3. File path info in toolbar
+        let file_name_str = match &self.current_file_path {
+            Some(path) => path.file_name().unwrap_or_default().to_string_lossy().into_owned(),
+            None => "Untitled".to_string(),
+        };
+        labels.push(TextLabel {
+            text: format!("File: {}", file_name_str),
+            x: 420.0,
+            y: 15.0,
+            font_size: 12.0,
+            color: [0xdd, 0xdd, 0xe2],
+        });
+
+        // 4. Status Bar indicators
+        if let Some((msg, is_error)) = &self.status_message {
+            let color = if *is_error { [0xfa, 0x52, 0x52] } else { [0x40, 0xc0, 0x57] };
+            labels.push(TextLabel {
+                text: msg.clone(),
+                x: (self.width as f32 - 400.0).max(300.0),
+                y: self.height as f32 - 20.0,
+                font_size: 11.0,
+                color,
+            });
+        } else {
+            labels.push(TextLabel {
+                text: format!("Keys: {} | Selected Index: {:?}", self.flat_keys.len(), self.selected_key_idx),
+                x: 15.0,
+                y: self.height as f32 - 20.0,
+                font_size: 11.0,
+                color: [0x83, 0x83, 0x8a],
+            });
+        }
+
+        // 5. Add Textbox contents to text_items
+        Self::add_textbox_labels(
+            &self.raw_json_editor,
+            &self.ui_context,
+            &mut self.font_system,
+            &mut self.text_items,
+            scale,
+            None,
+        );
+        Self::add_textbox_labels(
+            &self.selected_value_editor,
+            &self.ui_context,
+            &mut self.font_system,
+            &mut self.text_items,
+            scale,
+            None,
+        );
+        Self::add_textbox_labels(
+            &self.new_key_editor,
+            &self.ui_context,
+            &mut self.font_system,
+            &mut self.text_items,
+            scale,
+            None,
+        );
+
+        // 6. Build static text items
+        for label in labels {
+            let physical_size = label.font_size * scale;
+            let metrics = Metrics::new(physical_size, physical_size * 1.4);
+            let mut buf = Buffer::new(&mut self.font_system, metrics);
+            buf.set_text(&mut self.font_system, &label.text, Attrs::new(), glyphon::Shaping::Advanced);
+            buf.shape_until_scroll(&mut self.font_system, true);
+            self.text_items.push(TextItem {
+                buffer: buf,
+                x: label.x,
+                y: label.y,
+                color: glyphon::Color::rgb(label.color[0], label.color[1], label.color[2]),
+                bounds: None,
+            });
+        }
+
+        // 7. Left List rows text with clip bounds
+        let list_left = 10.0;
+        let list_right = 376.0;
+        let list_top = 52.0;
+        let list_bottom = bottom_y_calc(self.height);
+        let list_bounds = Some([list_left, list_top, list_right, list_bottom]);
+
+        for (i, (key_path, val)) in self.flat_keys.iter().enumerate() {
+            let row_y = list_top + i as f32 * 28.0 - self.scroll_y;
+            if row_y + 28.0 < list_top || row_y > list_bottom {
+                continue;
+            }
+            
+            let display_key = if key_path.len() > 24 {
+                format!("...{}", &key_path[key_path.len() - 21..])
+            } else {
+                key_path.clone()
+            };
+            
+            let val_str = serde_json::to_string(val).unwrap_or_default();
+            let display_val = if val_str.len() > 18 {
+                format!("{}...", &val_str[..15])
+            } else {
+                val_str
+            };
+            
+            let color = if Some(i) == self.selected_key_idx {
+                [0x7d, 0xff, 0xff]
+            } else {
+                [0xcc, 0xcc, 0xd4]
+            };
+            
+            let physical_size = 12.0 * scale;
+            let metrics = Metrics::new(physical_size, physical_size * 1.4);
+            
+            // Key Path
+            let mut buf_key = Buffer::new(&mut self.font_system, metrics.clone());
+            buf_key.set_text(&mut self.font_system, &display_key, Attrs::new(), glyphon::Shaping::Advanced);
+            buf_key.shape_until_scroll(&mut self.font_system, true);
+            self.text_items.push(TextItem {
+                buffer: buf_key,
+                x: list_left + 8.0,
+                y: row_y + 6.0,
+                color: glyphon::Color::rgb(color[0], color[1], color[2]),
+                bounds: list_bounds,
+            });
+            
+            // Value
+            let mut buf_val = Buffer::new(&mut self.font_system, metrics);
+            buf_val.set_text(&mut self.font_system, &display_val, Attrs::new(), glyphon::Shaping::Advanced);
+            buf_val.shape_until_scroll(&mut self.font_system, true);
+            self.text_items.push(TextItem {
+                buffer: buf_val,
+                x: list_left + 200.0,
+                y: row_y + 6.0,
+                color: glyphon::Color::rgb(0x83, 0x83, 0x8a),
+                bounds: list_bounds,
+            });
+        }
+    }
+}
+
+impl Application for DataEditorApp {
+    type Message = AppMessage;
+
+    fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
+        Some(&self.ui_context)
+    }
+
+    fn new(_qh: &QueueHandle<EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
+        let btn_open = Button::new(10.0, 8.0, 70.0, 26.0).with_label("Open");
+        let btn_save = Button::new(90.0, 8.0, 70.0, 26.0).with_label("Save");
+        let btn_save_as = Button::new(170.0, 8.0, 80.0, 26.0).with_label("Save As");
+        let btn_format = Button::new(260.0, 8.0, 80.0, 26.0).with_label("Format");
+        let btn_exit = Button::new(720.0, 8.0, 70.0, 26.0).with_label("Exit");
+
+        let mut new_key_editor = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true);
+        new_key_editor.set_placeholder("new.key.path");
+        let btn_add_key = Button::new(280.0, 430.0, 100.0, 26.0).with_label("Add Key");
+
+        let mut selected_value_editor = TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true);
+        selected_value_editor.set_placeholder("value (e.g. 42, true, \"hello\")");
+        let btn_apply_val = Button::new(280.0, 490.0, 48.0, 26.0).with_label("Set");
+        let btn_delete_key = Button::new(332.0, 490.0, 48.0, 26.0).with_label("Del");
+
+        let mut raw_json_editor = TextBox::new(String::new())
+            .with_multiline(true)
+            .with_draw_bg_border(true)
+            .with_max_width(None);
+        raw_json_editor.font_family = "monospace".to_string();
+        raw_json_editor.font_size = 13.0;
+
+        // Auto-load argument path if passed
+        let args: Vec<String> = std::env::args().collect();
+        let mut current_file_path = None;
+        let mut flat_keys = Vec::new();
+        if args.len() > 1 {
+            let path = std::path::PathBuf::from(&args[1]);
+            if path.exists() {
+                if let Ok(content) = std::fs::read_to_string(&path) {
+                    raw_json_editor.text = content;
+                    raw_json_editor.edit_buffer = raw_json_editor.text.clone();
+                    current_file_path = Some(path);
+                    if let Ok(val) = serde_json::from_str::<serde_json::Value>(&raw_json_editor.text) {
+                        flatten_json(&val, "", &mut flat_keys);
+                    }
+                }
+            }
+        }
+
+        Self {
+            btn_open,
+            btn_save,
+            btn_save_as,
+            btn_format,
+            btn_exit,
+            flat_keys,
+            selected_key_idx: None,
+            scroll_y: 0.0,
+            hovered_row_idx: None,
+            new_key_editor,
+            btn_add_key,
+            selected_value_editor,
+            btn_apply_val,
+            btn_delete_key,
+            raw_json_editor,
+            current_file_path,
+            status_message: None,
+            width: 800,
+            height: 600,
+            scale_factor: 1.0,
+            text_items: Vec::new(),
+            font_system: {
+                let mut fs = FontSystem::new();
+                fs.db_mut().load_fonts_dir("/home/lsgalante/Dropbox/Fonts");
+                fs
+            },
+            needs_rebuild: true,
+            ui_context: cce_ui::context::UiContext::new(),
+            ctrl_pressed: false,
+            initial_focus: true,
+        }
+    }
+
+    fn settings(&self) -> WindowSettings {
+        WindowSettings {
+            title: "Clear JSON Data Editor".to_string(),
+            app_id: "cce-data-editor".to_string(),
+            width: 800,
+            height: 600,
+            fullscreen: false,
+            min_size: Some((600, 450)),
+        }
+    }
+
+    fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
+        match msg {
+            AppMessage::Exit => {
+                *exit = true;
+            }
+            AppMessage::OpenDocument => {
+                if let Some(path) = self.pick_file_to_open() {
+                    match std::fs::read_to_string(&path) {
+                        Ok(content) => {
+                            self.raw_json_editor.text = content;
+                            self.raw_json_editor.edit_buffer = self.raw_json_editor.text.clone();
+                            self.raw_json_editor.cursor_idx = 0;
+                            self.raw_json_editor.select_anchor = None;
+                            self.raw_json_editor.editing = false;
+                            
+                            self.flat_keys.clear();
+                            if let Ok(val) = serde_json::from_str::<serde_json::Value>(&self.raw_json_editor.text) {
+                                flatten_json(&val, "", &mut self.flat_keys);
+                                self.status_message = Some((format!("Opened {}", path.file_name().unwrap_or_default().to_string_lossy()), false));
+                            } else {
+                                self.status_message = Some(("Loaded, but JSON is syntactically invalid".to_string(), true));
+                            }
+                            
+                            self.selected_key_idx = None;
+                            self.scroll_y = 0.0;
+                            self.current_file_path = Some(path);
+                        }
+                        Err(e) => {
+                            self.status_message = Some((format!("Error opening: {}", e), true));
+                        }
+                    }
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+            }
+            AppMessage::SaveDocument => {
+                let content = if self.raw_json_editor.editing { &self.raw_json_editor.edit_buffer } else { &self.raw_json_editor.text };
+                if let Err(e) = serde_json::from_str::<serde_json::Value>(content) {
+                    self.status_message = Some((format!("Cannot save: invalid JSON ({})", e), true));
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                    return;
+                }
+                
+                if let Some(ref path) = self.current_file_path {
+                    match std::fs::write(path, content) {
+                        Ok(_) => {
+                            if self.raw_json_editor.editing {
+                                self.raw_json_editor.text = self.raw_json_editor.edit_buffer.clone();
+                            }
+                            self.status_message = Some((format!("Saved to {}", path.file_name().unwrap_or_default().to_string_lossy()), false));
+                        }
+                        Err(e) => {
+                            self.status_message = Some((format!("Save failed: {}", e), true));
+                        }
+                    }
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                } else {
+                    self.update(AppMessage::SaveDocumentAs, needs_rebuild, exit);
+                }
+            }
+            AppMessage::SaveDocumentAs => {
+                let content = if self.raw_json_editor.editing { &self.raw_json_editor.edit_buffer } else { &self.raw_json_editor.text };
+                if let Err(e) = serde_json::from_str::<serde_json::Value>(content) {
+                    self.status_message = Some((format!("Cannot save: invalid JSON ({})", e), true));
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                    return;
+                }
+                
+                if let Some(path) = self.pick_file_to_save() {
+                    match std::fs::write(&path, content) {
+                        Ok(_) => {
+                            if self.raw_json_editor.editing {
+                                self.raw_json_editor.text = self.raw_json_editor.edit_buffer.clone();
+                            }
+                            self.current_file_path = Some(path.clone());
+                            self.status_message = Some((format!("Saved to {}", path.file_name().unwrap_or_default().to_string_lossy()), false));
+                        }
+                        Err(e) => {
+                            self.status_message = Some((format!("Save failed: {}", e), true));
+                        }
+                    }
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+            }
+            AppMessage::FormatJson => {
+                let content = if self.raw_json_editor.editing { &self.raw_json_editor.edit_buffer } else { &self.raw_json_editor.text };
+                match serde_json::from_str::<serde_json::Value>(content) {
+                    Ok(val) => {
+                        if let Ok(pretty) = serde_json::to_string_pretty(&val) {
+                            self.raw_json_editor.text = pretty;
+                            self.raw_json_editor.edit_buffer = self.raw_json_editor.text.clone();
+                            self.raw_json_editor.editing = false;
+                            self.raw_json_editor.sync_editor_state();
+                            
+                            self.flat_keys.clear();
+                            flatten_json(&val, "", &mut self.flat_keys);
+                            self.status_message = Some(("Formatted successfully".to_string(), false));
+                        }
+                    }
+                    Err(e) => {
+                        self.status_message = Some((format!("JSON error: {}", e), true));
+                    }
+                }
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::AddKey => {
+                let new_key = if self.new_key_editor.editing { &self.new_key_editor.edit_buffer } else { &self.new_key_editor.text };
+                let trimmed = new_key.trim().to_string();
+                if trimmed.is_empty() {
+                    self.status_message = Some(("Key path cannot be empty".to_string(), true));
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                    return;
+                }
+                
+                if self.flat_keys.iter().any(|(k, _)| k == &trimmed) {
+                    self.status_message = Some(("Key path already exists".to_string(), true));
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                    return;
+                }
+                
+                self.flat_keys.push((trimmed.clone(), serde_json::Value::String(String::new())));
+                self.update_raw_from_flat();
+                
+                self.new_key_editor.text.clear();
+                self.new_key_editor.edit_buffer.clear();
+                self.new_key_editor.editing = false;
+                
+                if let Some(idx) = self.flat_keys.iter().position(|(k, _)| k == &trimmed) {
+                    self.selected_key_idx = Some(idx);
+                    self.selected_value_editor.text = serde_json::to_string(&self.flat_keys[idx].1).unwrap_or_default();
+                    self.selected_value_editor.edit_buffer = self.selected_value_editor.text.clone();
+                    self.selected_value_editor.editing = false;
+                }
+                
+                self.status_message = Some((format!("Added key: {}", trimmed), false));
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::ApplyValue => {
+                if let Some(idx) = self.selected_key_idx {
+                    let val_str = if self.selected_value_editor.editing { &self.selected_value_editor.edit_buffer } else { &self.selected_value_editor.text };
+                    let val_trimmed = val_str.trim();
+                    match serde_json::from_str::<serde_json::Value>(val_trimmed) {
+                        Ok(parsed) => {
+                            self.flat_keys[idx].1 = parsed;
+                            self.update_raw_from_flat();
+                            self.status_message = Some(("Value applied".to_string(), false));
+                        }
+                        Err(e) => {
+                            if !val_trimmed.starts_with('"') && !val_trimmed.starts_with('[') && !val_trimmed.starts_with('{') {
+                                let parsed = serde_json::Value::String(val_trimmed.to_string());
+                                self.flat_keys[idx].1 = parsed;
+                                self.update_raw_from_flat();
+                                
+                                self.selected_value_editor.text = serde_json::to_string(&self.flat_keys[idx].1).unwrap_or_default();
+                                self.selected_value_editor.edit_buffer = self.selected_value_editor.text.clone();
+                                self.selected_value_editor.editing = false;
+                                
+                                self.status_message = Some(("Value applied as string".to_string(), false));
+                            } else {
+                                self.status_message = Some((format!("Parse Error: must be valid JSON value ({})", e), true));
+                            }
+                        }
+                    }
+                }
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+            AppMessage::DeleteKey => {
+                if let Some(idx) = self.selected_key_idx {
+                    let name = self.flat_keys[idx].0.clone();
+                    self.flat_keys.remove(idx);
+                    self.selected_key_idx = None;
+                    self.selected_value_editor.text.clear();
+                    self.selected_value_editor.edit_buffer.clear();
+                    self.selected_value_editor.editing = false;
+                    self.update_raw_from_flat();
+                    self.status_message = Some((format!("Deleted key: {}", name), false));
+                }
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+    }
+
+    fn tick(&mut self, _dt: f32, needs_rebuild: &mut bool) {
+        if self.raw_json_editor.take_change() {
+            let content = if self.raw_json_editor.editing { &self.raw_json_editor.edit_buffer } else { &self.raw_json_editor.text };
+            if let Ok(val) = serde_json::from_str::<serde_json::Value>(content) {
+                let mut new_flat = Vec::new();
+                flatten_json(&val, "", &mut new_flat);
+                
+                let selected_key = self.selected_key_idx.and_then(|idx| self.flat_keys.get(idx).map(|(k, _)| k.clone()));
+                self.flat_keys = new_flat;
+                
+                if let Some(k) = selected_key {
+                    self.selected_key_idx = self.flat_keys.iter().position(|(key, _)| key == &k);
+                    if let Some(idx) = self.selected_key_idx {
+                        self.selected_value_editor.text = serde_json::to_string(&self.flat_keys[idx].1).unwrap_or_default();
+                        if !self.selected_value_editor.editing {
+                            self.selected_value_editor.edit_buffer = self.selected_value_editor.text.clone();
+                        }
+                    } else {
+                        self.selected_value_editor.text.clear();
+                        self.selected_value_editor.edit_buffer.clear();
+                        self.selected_value_editor.editing = false;
+                    }
+                }
+                self.status_message = None;
+            } else {
+                self.status_message = Some(("JSON syntax error in text editor".to_string(), true));
+            }
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+    }
+
+    fn view(&mut self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>, size: LogicalSize, scale: f64) {
+        if self.initial_focus {
+            self.initial_focus = false;
+            self.ui_context.set_focused(&mut self.raw_json_editor);
+            TextBox::focus(&mut self.raw_json_editor);
+            self.needs_rebuild = true;
+        }
+        
+        let size_changed = self.width != size.width as u32 || self.height != size.height as u32 || self.scale_factor != scale;
+        if self.needs_rebuild || size_changed {
+            self.width = size.width as u32;
+            self.height = size.height as u32;
+            self.scale_factor = scale;
+            
+            // Top bar
+            self.btn_open.set_rect(10.0, 8.0, 70.0, 26.0);
+            self.btn_save.set_rect(90.0, 8.0, 70.0, 26.0);
+            self.btn_save_as.set_rect(170.0, 8.0, 80.0, 26.0);
+            self.btn_format.set_rect(260.0, 8.0, 80.0, 26.0);
+            self.btn_exit.set_rect((self.width as f32 - 80.0).max(350.0), 8.0, 70.0, 26.0);
+            
+            // Bottom edit area in left panel
+            let bottom_y = bottom_y_calc(self.height);
+            self.new_key_editor.set_rect(10.0, bottom_y + 10.0, 260.0, 26.0);
+            self.btn_add_key.set_rect(280.0, bottom_y + 10.0, 100.0, 26.0);
+            
+            self.selected_value_editor.set_rect(10.0, bottom_y + 70.0, 260.0, 26.0);
+            self.btn_apply_val.set_rect(280.0, bottom_y + 70.0, 48.0, 26.0);
+            self.btn_delete_key.set_rect(332.0, bottom_y + 70.0, 48.0, 26.0);
+            
+            // Right pane raw editor
+            let right_w = (self.width as f32 - 420.0).max(100.0);
+            let right_h = (self.height as f32 - 92.0).max(100.0);
+            self.raw_json_editor.set_rect(410.0, 52.0, right_w, right_h);
+            
+            self.rebuild_text_items();
+            self.needs_rebuild = false;
+        }
+
+        // 1. Editor Window Background
+        quads.push((0.0, 0.0, self.width as f32, self.height as f32, [0.05, 0.05, 0.07, 1.0]));
+
+        // 2. Toolbar Header
+        quads.push((0.0, 0.0, self.width as f32, 42.0, [0.08, 0.08, 0.12, 1.0]));
+        quads.push((0.0, 42.0, self.width as f32, 1.0, [0.18, 0.18, 0.22, 1.0]));
+
+        // 3. Status Bar
+        let status_y = self.height as f32 - 30.0;
+        quads.push((0.0, status_y, self.width as f32, 30.0, [0.08, 0.08, 0.10, 1.0]));
+        quads.push((0.0, status_y, self.width as f32, 1.0, [0.18, 0.18, 0.22, 1.0]));
+        
+        // 4. Left Panel List Box background and borders
+        let list_top = 52.0;
+        let list_bottom = bottom_y_calc(self.height);
+        let list_height = list_bottom - list_top;
+        quads.push((10.0, list_top, 380.0, list_height, [0.08, 0.08, 0.10, 1.0]));
+        quads.push((10.0, list_top, 380.0, 1.0, [0.18, 0.18, 0.22, 1.0]));
+        quads.push((10.0, list_bottom, 380.0, 1.0, [0.18, 0.18, 0.22, 1.0]));
+        quads.push((10.0, list_top, 1.0, list_height, [0.18, 0.18, 0.22, 1.0]));
+        quads.push((390.0, list_top, 1.0, list_height, [0.18, 0.18, 0.22, 1.0]));
+
+        // Draw left list rows
+        for i in 0..self.flat_keys.len() {
+            let row_y = list_top + i as f32 * 28.0 - self.scroll_y;
+            if row_y + 28.0 < list_top || row_y > list_bottom {
+                continue;
+            }
+            
+            let draw_y = row_y.max(list_top);
+            let draw_bottom = (row_y + 28.0).min(list_bottom);
+            let draw_h = draw_bottom - draw_y;
+            if draw_h <= 0.0 { continue; }
+            
+            let bg_color = if Some(i) == self.selected_key_idx {
+                [0.15, 0.20, 0.30, 1.0]
+            } else if Some(i) == self.hovered_row_idx {
+                [0.12, 0.12, 0.16, 1.0]
+            } else if i % 2 == 0 {
+                [0.09, 0.09, 0.11, 1.0]
+            } else {
+                [0.08, 0.08, 0.10, 1.0]
+            };
+            
+            quads.push((11.0, draw_y, 378.0, draw_h, bg_color));
+            
+            if row_y + 28.0 <= list_bottom {
+                quads.push((11.0, row_y + 27.0, 378.0, 1.0, [0.13, 0.13, 0.17, 1.0]));
+            }
+        }
+
+        // Draw left list scrollbar
+        let total_content_height = self.flat_keys.len() as f32 * 28.0;
+        let max_scroll_y = (total_content_height - list_height).max(0.0);
+        if max_scroll_y > 0.0 {
+            let track_h = list_height - 8.0;
+            let thumb_h = (track_h * (list_height / total_content_height)).max(20.0);
+            let thumb_y = list_top + 4.0 + (track_h - thumb_h) * (self.scroll_y / max_scroll_y);
+            quads.push((384.0, list_top + 4.0, 4.0, track_h, [0.12, 0.12, 0.15, 1.0]));
+            quads.push((384.0, thumb_y, 4.0, thumb_h, [0.25, 0.25, 0.30, 1.0]));
+        }
+
+        // 5. Button and editor extra quads
+        quads.extend(self.btn_open.extra_quads());
+        quads.extend(self.btn_save.extra_quads());
+        quads.extend(self.btn_save_as.extra_quads());
+        quads.extend(self.btn_format.extra_quads());
+        quads.extend(self.btn_exit.extra_quads());
+        quads.extend(self.btn_add_key.extra_quads());
+        quads.extend(self.btn_apply_val.extra_quads());
+        quads.extend(self.btn_delete_key.extra_quads());
+        
+        quads.extend(self.new_key_editor.extra_quads());
+        quads.extend(self.selected_value_editor.extra_quads());
+        quads.extend(self.raw_json_editor.extra_quads());
+    }
+
+    fn text_items(&self) -> &[TextItem] {
+        &self.text_items
+    }
+
+    fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        let mut changed = false;
+        let px = pos.x as f32;
+        let py = pos.y as f32;
+
+        if self.btn_open.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_save.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_save_as.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_format.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_exit.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_add_key.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_apply_val.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.btn_delete_key.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        
+        if self.new_key_editor.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.selected_value_editor.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+        if self.raw_json_editor.on_cursor_moved(px, py, &mut self.ui_context) { changed = true; }
+
+        // Hover detection inside list box
+        let list_top = 52.0;
+        let list_bottom = bottom_y_calc(self.height);
+        if px >= 10.0 && px <= 380.0 && py >= list_top && py <= list_bottom {
+            let relative_y = py - list_top + self.scroll_y;
+            let row_idx = (relative_y / 28.0) as usize;
+            let new_hover = if row_idx < self.flat_keys.len() { Some(row_idx) } else { None };
+            if self.hovered_row_idx != new_hover {
+                self.hovered_row_idx = new_hover;
+                changed = true;
+            }
+        } else if self.hovered_row_idx.is_some() {
+            self.hovered_row_idx = None;
+            changed = true;
+        }
+
+        if changed {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+    }
+
+    fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        let mut changed = false;
+        let mut msg_out = None;
+        let px = pos.x as f32;
+        let py = pos.y as f32;
+
+        if self.btn_open.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_open.take_click() {
+                msg_out = Some(AppMessage::OpenDocument);
+            }
+        }
+        if self.btn_save.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_save.take_click() {
+                msg_out = Some(AppMessage::SaveDocument);
+            }
+        }
+        if self.btn_save_as.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_save_as.take_click() {
+                msg_out = Some(AppMessage::SaveDocumentAs);
+            }
+        }
+        if self.btn_format.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_format.take_click() {
+                msg_out = Some(AppMessage::FormatJson);
+            }
+        }
+        if self.btn_exit.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_exit.take_click() {
+                msg_out = Some(AppMessage::Exit);
+            }
+        }
+        if self.btn_add_key.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_add_key.take_click() {
+                msg_out = Some(AppMessage::AddKey);
+            }
+        }
+        if self.btn_apply_val.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_apply_val.take_click() {
+                msg_out = Some(AppMessage::ApplyValue);
+            }
+        }
+        if self.btn_delete_key.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+            if state == ElementState::Released && self.btn_delete_key.take_click() {
+                msg_out = Some(AppMessage::DeleteKey);
+            }
+        }
+
+        if self.new_key_editor.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+        }
+        if self.selected_value_editor.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+        }
+        if self.raw_json_editor.mouse_input(button, state, px, py, &mut self.ui_context) {
+            changed = true;
+        }
+
+        // Selection clicking inside list box
+        let list_top = 52.0;
+        let list_bottom = list_bottom_calc(self.height);
+        if button == MouseButton::Left && state == ElementState::Pressed {
+            if px >= 10.0 && px <= 380.0 && py >= list_top && py <= list_bottom {
+                let relative_y = py - list_top + self.scroll_y;
+                let row_idx = (relative_y / 28.0) as usize;
+                if row_idx < self.flat_keys.len() {
+                    self.selected_key_idx = Some(row_idx);
+                    
+                    let value_str = serde_json::to_string(&self.flat_keys[row_idx].1).unwrap_or_default();
+                    self.selected_value_editor.text = value_str;
+                    self.selected_value_editor.edit_buffer = self.selected_value_editor.text.clone();
+                    self.selected_value_editor.editing = false;
+                    
+                    self.ui_context.set_focused(&mut self.selected_value_editor);
+                    TextBox::focus(&mut self.selected_value_editor);
+                    changed = true;
+                }
+            } else {
+                let bottom_y = bottom_y_calc(self.height);
+                let in_new_key = px >= 10.0 && px <= 270.0 && py >= bottom_y + 10.0 && py <= bottom_y + 36.0;
+                let in_sel_val = px >= 10.0 && px <= 270.0 && py >= bottom_y + 70.0 && py <= bottom_y + 96.0;
+                let in_raw = px >= 410.0 && px <= self.width as f32 - 10.0 && py >= 52.0 && py <= self.height as f32 - 40.0;
+                
+                if !in_new_key && !in_sel_val && !in_raw {
+                    self.raw_json_editor.unfocus();
+                    self.new_key_editor.unfocus();
+                    self.selected_value_editor.unfocus();
+                    changed = true;
+                }
+            }
+        }
+
+        if changed {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+
+        msg_out
+    }
+
+    fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
+        let px = pos.x as f32;
+        let py = pos.y as f32;
+        let list_top = 52.0;
+        let list_bottom = bottom_y_calc(self.height);
+        let list_height = list_bottom - list_top;
+        
+        if px >= 10.0 && px <= 380.0 && py >= list_top && py <= list_bottom {
+            let scroll_amount = match delta {
+                MouseScrollDelta::LineDelta(_, y) => -y * 24.0,
+                MouseScrollDelta::PixelDelta(p) => -p.y as f32,
+            };
+            let total_content_height = self.flat_keys.len() as f32 * 28.0;
+            let max_scroll_y = (total_content_height - list_height).max(0.0);
+            let old_scroll = self.scroll_y;
+            self.scroll_y = (self.scroll_y + scroll_amount).clamp(0.0, max_scroll_y);
+            if (self.scroll_y - old_scroll).abs() > 0.01 {
+                *needs_rebuild = true;
+                self.needs_rebuild = true;
+            }
+        }
+    }
+
+    fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
+        self.ctrl_pressed = event.ctrl;
+
+        if event.state == ElementState::Pressed && self.status_message.is_some() {
+            self.status_message = None;
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+
+        let mut handled = false;
+        let mut msg_out = None;
+
+        // Keyboard Shortcuts
+        if event.ctrl && event.state == ElementState::Pressed {
+            if let Key::Character(ref ch) = event.logical_key {
+                match ch.to_lowercase().as_str() {
+                    "o" => {
+                        msg_out = Some(AppMessage::OpenDocument);
+                        handled = true;
+                    }
+                    "s" => {
+                        msg_out = Some(AppMessage::SaveDocument);
+                        handled = true;
+                    }
+                    "q" => {
+                        msg_out = Some(AppMessage::Exit);
+                        handled = true;
+                    }
+                    "f" => {
+                        msg_out = Some(AppMessage::FormatJson);
+                        handled = true;
+                    }
+                    "=" | "+" => {
+                        self.raw_json_editor.font_size = (self.raw_json_editor.font_size + 1.0).min(72.0);
+                        self.selected_value_editor.font_size = (self.selected_value_editor.font_size + 1.0).min(72.0);
+                        self.new_key_editor.font_size = (self.new_key_editor.font_size + 1.0).min(72.0);
+                        *needs_rebuild = true;
+                        self.needs_rebuild = true;
+                        handled = true;
+                    }
+                    "-" | "_" => {
+                        self.raw_json_editor.font_size = (self.raw_json_editor.font_size - 1.0).max(6.0);
+                        self.selected_value_editor.font_size = (self.selected_value_editor.font_size - 1.0).max(6.0);
+                        self.new_key_editor.font_size = (self.new_key_editor.font_size - 1.0).max(6.0);
+                        *needs_rebuild = true;
+                        self.needs_rebuild = true;
+                        handled = true;
+                    }
+                    _ => {}
+                }
+            }
+        }
+
+        if !handled {
+            if self.raw_json_editor.keyboard_input(event, &mut self.ui_context) { handled = true; }
+            if self.selected_value_editor.keyboard_input(event, &mut self.ui_context) {
+                handled = true;
+                if event.state == ElementState::Pressed && event.logical_key == Key::Named(cce_ui::widget::NamedKey::Enter) {
+                    msg_out = Some(AppMessage::ApplyValue);
+                }
+            }
+            if self.new_key_editor.keyboard_input(event, &mut self.ui_context) {
+                handled = true;
+                if event.state == ElementState::Pressed && event.logical_key == Key::Named(cce_ui::widget::NamedKey::Enter) {
+                    msg_out = Some(AppMessage::AddKey);
+                }
+            }
+        }
+
+        if handled {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
+
+        msg_out
+    }
+}
+
+fn list_bottom_calc(height: u32) -> f32 {
+    bottom_y_calc(height)
+}
+
+fn main() {
+    let rt = tokio::runtime::Runtime::new().expect("tokio runtime");
+    let _guard = rt.enter();
+    
+    cce_ui::engine::run::<DataEditorApp>();
+}