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

commit7f559cc6837a391a126e2db4c835d26c51539074
parentc57e5cd01e
authorLucas Galante <[email protected]>
date2026-09-09 15:03
feat: unit-aware editor for length values

Step 2 of the unit system. A key whose annotation is a unit (`(mm)`,
`(px)`, `(cm)`, `(in)`, `(pt)`) — or whose string value parses as
`cce_ui::units::Len`, which is the same thing seen from the JSON side —
edits as a two-decimal Spinbox for the number beside a Dropdown for the
unit, side by side over the row. The number commits as `"<n><unit>"`,
which the KDL writer emits as `(<unit>)<n>`. Switching the unit keeps
the LENGTH, converted through the display metric, and rewrites the
number: 2 mm shown in inches reads 0.08.

Both decision sites (display_list and the click handler) key off
is_len_type; commit_len / sync_len_editors are the one commit and sync
path, and every raw-reparse sync point checks for a Len before the
colour and font guesses.

Verified in a scale-2 shadow: rows render as `2 mm (mm)`, the editors
appear over the selected row, picking `in` rewrites to `width=(in)0.08`,
and the plus button steps to `(mm)2.1`.

Co-Authored-By: Claude Fable 5.1 <[email protected]>

 CLAUDE.md   |  12 +++-
 src/main.rs | 183 ++++++++++++++++++++++++++++++++++++++++++++++++++++++------
 2 files changed, 177 insertions(+), 18 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index f96dcfc..132c42d 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -66,7 +66,7 @@ loads (best-effort parse) with an error in the status bar.
 
 ### Type-driven inline value editors
 
-Nine editor widgets exist permanently as fields (`selected_*_editor`); exactly one is
+Eleven editor widgets exist permanently as fields (`selected_*_editor`); exactly one is
 positioned inline over the selected tree row, the rest are parked off-screen at
 `(-1000, -1000)` (the "hide" convention — there is no visibility flag). Which editor
 appears is decided in two places that must stay in agreement: the layout block in
@@ -91,6 +91,16 @@ appears is decided in two places that must stay in agreement: the layout block i
   the `(ramp)` annotation. Ramp-named keys (`ramp` / `*_ramp`) whose string
   value parses as a ramp spec get the treatment before the annotation exists
   (`overview_ramp` shipped unannotated).
+  A **unit annotation** (`px` / `mm` / `cm` / `in` / `pt`, e.g. `width=(mm)2.0`), or
+  any string value `cce_ui::units::Len` parses (`"2mm"` — which is exactly what
+  `kdl_to_json` makes of the annotated number) → a two-decimal `Spinbox`
+  (`selected_len_editor`) beside a unit `Dropdown` (`selected_unit_editor`).
+  Changing the number writes `"<n><unit>"`, which the KDL writer emits as
+  `(<unit>)<n>`; changing the unit keeps the LENGTH — converted through
+  `cce_ui::units::metric()`, the display's px-per-mm — and rewrites the number,
+  so 2 mm switched to inches reads 0.08 and a px value switched to mm reads what
+  it measures on this panel. Both sites (`display_list`, click) key off
+  `is_len_type`; the commit paths are `commit_len` / `sync_len_editors`.
 - **Key-name heuristics**: `font` / `*_font` / `*.font` → `FontSelector`; keybind-ish
   names (`key`, `shortcut`, `brightness_up`, …) → `KeybindRecorder`.
 - **Value shape**: `#`-prefixed string → `ColorSelector`, bool → `Checkbox`,
diff --git a/src/main.rs b/src/main.rs
index 0e52601..81887d8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -385,6 +385,13 @@ struct DataEditorApp {
     selected_spinbox_editor: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
     selected_font_editor: cce_ui::widget::Adapted<FontSelector>,
     selected_choice_editor: cce_ui::widget::Adapted<Dropdown>,
+    /// A unit-bearing length — a `(mm)`/`(px)`/`(cm)`/`(in)`/`(pt)` annotation,
+    /// or any string `cce_ui::units::Len` parses — edits as a two-decimal
+    /// spinbox for the number beside a dropdown for the unit. Switching the
+    /// unit keeps the LENGTH (converted through the process metric) and
+    /// changes the number, so 2 mm shown in inches reads 0.08.
+    selected_len_editor: cce_ui::widget::Adapted<cce_ui::widget::Spinbox>,
+    selected_unit_editor: cce_ui::widget::Adapted<Dropdown>,
     selected_keybind_editor: cce_ui::widget::Adapted<KeybindRecorder>,
     selected_bool_editor: cce_ui::widget::Adapted<Checkbox>,
     selected_button_editor: cce_ui::widget::Adapted<cce_ui::widget::Button>,
@@ -445,8 +452,44 @@ struct DataEditorApp {
 }
 
 
+/// The `Len` a flat value holds, if it is a unit-suffixed string (`"2mm"` —
+/// what `kdl_to_json` makes of `(mm)2.0`).
+fn len_of(val: &serde_json::Value) -> Option<cce_ui::units::Len> {
+    val.as_str().and_then(cce_ui::units::Len::parse)
+}
+
 impl DataEditorApp {
 
+    /// The unit the unit dropdown currently shows.
+    fn selected_unit(&self) -> Option<cce_ui::units::Unit> {
+        self.selected_unit_editor
+            .options
+            .get(self.selected_unit_editor.selected)
+            .and_then(|s| cce_ui::units::Unit::parse(s))
+    }
+
+    /// Point the length editors at `len` (the raw-reparse sync pattern).
+    fn sync_len_editors(&mut self, len: cce_ui::units::Len) {
+        self.selected_len_editor.value = (len.value * 100.0).round() as i32;
+        if let Some(pos) = cce_ui::units::Unit::ALL.iter().position(|u| *u == len.unit) {
+            self.selected_unit_editor.selected = pos;
+        }
+    }
+
+    /// Write `len` into the selected key as its suffixed string; the KDL
+    /// writer turns `"2mm"` back into `(mm)2`. Rounded to the spinbox's two
+    /// decimals so the file and the control agree.
+    fn commit_len(&mut self, idx: usize, len: cce_ui::units::Len) {
+        let len = cce_ui::units::Len::new((len.value * 100.0).round() / 100.0, len.unit);
+        self.flat_keys[idx].1 = serde_json::Value::String(len.serialize());
+        self.sync_len_editors(len);
+        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.update_raw_from_flat();
+        self.sync_preview_selection();
+    }
+
     fn load_recent_files(&self) -> Vec<String> {
         cce_ui::config::load_recent_files()
     }
@@ -617,7 +660,9 @@ impl DataEditorApp {
 
             let val = &self.flat_keys[idx].1;
             if let serde_json::Value::String(s) = val {
-                if let Some(c) = parse_hex_color(s) {
+                if let Some(len) = cce_ui::units::Len::parse(s) {
+                    self.sync_len_editors(len);
+                } else if let Some(c) = parse_hex_color(s) {
                     self.selected_color_editor.color = c;
                 } else {
                     self.selected_font_editor.font_family = s.clone();
@@ -821,6 +866,13 @@ impl Application for DataEditorApp {
         let selected_spinbox_editor = Spinbox::new(0, -1000000, 1000000, 1);
         let selected_font_editor = FontSelector::new(String::new());
         let selected_choice_editor = Dropdown::new(Vec::new(), 0);
+        // Hundredths: the spinbox is integer-valued, `decimals` scales its
+        // display; a wheel notch steps 0.10.
+        let selected_len_editor = Spinbox::new(0, -100_000_000, 100_000_000, 10).with_decimals(2);
+        let selected_unit_editor = Dropdown::new(
+            cce_ui::units::Unit::ALL.iter().map(|u| u.suffix().to_string()).collect(),
+            1,
+        );
         let selected_keybind_editor = KeybindRecorder::new(String::new());
         let selected_bool_editor = Checkbox::new();
         let selected_button_editor = Button::new(0.0, 0.0, 125.0, 26.0).with_label("Send Test");
@@ -937,6 +989,8 @@ impl Application for DataEditorApp {
                 selected_spinbox_editor,
                 selected_font_editor,
                 selected_choice_editor,
+                selected_len_editor,
+                selected_unit_editor,
                 selected_keybind_editor,
                 selected_bool_editor,
                 selected_button_editor,
@@ -1165,7 +1219,9 @@ impl Application for DataEditorApp {
                                 // Sync controls (the raw-reparse pattern).
                                 let val = &self.flat_keys[idx].1;
                                 if let serde_json::Value::String(s) = val {
-                                    if let Some(c) = parse_hex_color(s) {
+                                    if let Some(len) = cce_ui::units::Len::parse(s) {
+                                        self.sync_len_editors(len);
+                                    } else if let Some(c) = parse_hex_color(s) {
                                         self.selected_color_editor.color = c;
                                     } else {
                                         self.selected_font_editor.font_family = s.clone();
@@ -1225,7 +1281,9 @@ impl Application for DataEditorApp {
                             // Sync controls:
                             let val = &self.flat_keys[idx].1;
                             if let serde_json::Value::String(s) = val {
-                                if let Some(c) = parse_hex_color(s) {
+                                if let Some(len) = cce_ui::units::Len::parse(s) {
+                                    self.sync_len_editors(len);
+                                } else if let Some(c) = parse_hex_color(s) {
                                     self.selected_color_editor.color = c;
                                 } else {
                                     self.selected_font_editor.font_family = s.clone();
@@ -1247,7 +1305,9 @@ impl Application for DataEditorApp {
                                 // Sync controls:
                                 let val = &self.flat_keys[idx].1;
                                 if let serde_json::Value::String(s) = val {
-                                    if let Some(c) = parse_hex_color(s) {
+                                    if let Some(len) = cce_ui::units::Len::parse(s) {
+                                        self.sync_len_editors(len);
+                                    } else if let Some(c) = parse_hex_color(s) {
                                         self.selected_color_editor.color = c;
                                     } else {
                                         self.selected_font_editor.font_family = s.clone();
@@ -1300,6 +1360,7 @@ impl Application for DataEditorApp {
             for (open, geom) in [
                 (self.btn_open.popover_rect().is_some(), self.btn_open.get_popover_geom()),
                 (self.selected_choice_editor.popover_rect().is_some(), self.selected_choice_editor.get_popover_geom()),
+                (self.selected_unit_editor.popover_rect().is_some(), self.selected_unit_editor.get_popover_geom()),
             ] {
                 if !open {
                     continue;
@@ -1419,6 +1480,10 @@ impl Application for DataEditorApp {
             *needs_rebuild = true;
             self.needs_rebuild = true;
         }
+        if self.selected_unit_editor.tick(_dt, &mut self.ui_context) {
+            *needs_rebuild = true;
+            self.needs_rebuild = true;
+        }
 
         if self.selected_value_editor.take_change() {
             let mut exit = false;
@@ -1464,6 +1529,29 @@ impl Application for DataEditorApp {
                 self.needs_rebuild = true;
             }
         }
+        if self.selected_len_editor.take_change() {
+            if let Some(idx) = self.selected_key_idx {
+                if let Some(unit) = self.selected_unit() {
+                    let len = cce_ui::units::Len::new(self.selected_len_editor.value as f32 / 100.0, unit);
+                    self.commit_len(idx, len);
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+            }
+        }
+        if self.selected_unit_editor.take_change() {
+            if let Some(idx) = self.selected_key_idx {
+                if let (Some(cur), Some(unit)) = (len_of(&self.flat_keys[idx].1), self.selected_unit()) {
+                    // The same length in the new unit, through the display
+                    // metric — on an assumed metric that is the CSS 96 ppi
+                    // guess, which is all a conversion can honestly be there.
+                    let len = cur.convert(unit, &cce_ui::units::metric());
+                    self.commit_len(idx, len);
+                    *needs_rebuild = true;
+                    self.needs_rebuild = true;
+                }
+            }
+        }
         if self.selected_font_editor.take_change() {
             if let Some(idx) = self.selected_key_idx {
                 let new_val = serde_json::Value::String(self.selected_font_editor.font_family.clone());
@@ -1543,7 +1631,9 @@ impl Application for DataEditorApp {
                         // Sync controls:
                         let val = &self.flat_keys[idx].1;
                         if let serde_json::Value::String(s) = val {
-                            if let Some(c) = parse_hex_color(s) {
+                            if let Some(len) = cce_ui::units::Len::parse(s) {
+                                self.sync_len_editors(len);
+                            } else if let Some(c) = parse_hex_color(s) {
                                 self.selected_color_editor.color = c;
                             } else {
                                 self.selected_font_editor.font_family = s.clone();
@@ -1590,6 +1680,8 @@ impl Application for DataEditorApp {
                 self.ui_context.register_widget(self.selected_spinbox_editor.base().id(), (*self_ptr).selected_spinbox_editor.as_ptr_mut());
                 self.ui_context.register_widget(self.selected_font_editor.base().id(), (*self_ptr).selected_font_editor.as_ptr_mut());
                 self.ui_context.register_widget(self.selected_choice_editor.base().id(), (*self_ptr).selected_choice_editor.as_ptr_mut());
+                self.ui_context.register_widget(self.selected_len_editor.base().id(), (*self_ptr).selected_len_editor.as_ptr_mut());
+                self.ui_context.register_widget(self.selected_unit_editor.base().id(), (*self_ptr).selected_unit_editor.as_ptr_mut());
                 self.ui_context.register_widget(self.selected_keybind_editor.base().id(), (*self_ptr).selected_keybind_editor.as_ptr_mut());
                 self.ui_context.register_widget(self.selected_bool_editor.base().id(), (*self_ptr).selected_bool_editor.as_ptr_mut());
                 self.ui_context.register_widget(self.selected_button_editor.base().id(), (*self_ptr).selected_button_editor.as_ptr_mut());
@@ -1753,13 +1845,37 @@ impl Application for DataEditorApp {
                     {
                         is_relief_type = true;
                     }
-                    // Parked unless the bevel/ramp branch below places it (the
-                    // other branches never show them, so one shared park
-                    // suffices).
+                    // A length: the annotation is a unit, or the value is a
+                    // unit-suffixed string (the two are the same thing seen
+                    // from the KDL and the JSON side).
+                    let is_len_type = annotation_str.as_deref().and_then(cce_ui::units::Unit::parse).is_some()
+                        || len_of(val).is_some();
+                    // Parked unless the bevel/ramp/length branch below places
+                    // it (the other branches never show them, so one shared
+                    // park suffices).
                     self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                     self.selected_ramp_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
-
-                    if is_bevel_type || is_relief_type {
+                    self.selected_len_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                    self.selected_unit_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+
+                    if is_len_type {
+                        // Number and unit side by side in the value cell. An
+                        // annotated bare number (`width=(mm)2` reads as the
+                        // string "2mm" already, so `len_of` covers both).
+                        if let Some(len) = len_of(val) {
+                            self.sync_len_editors(len);
+                        }
+                        self.selected_len_editor.set_rect(row_x + 245.0, row_y + 1.0, 70.0, 26.0);
+                        self.selected_unit_editor.set_rect(row_x + 319.0, row_y + 1.0, 54.0, 26.0);
+                        self.selected_choice_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_color_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_spinbox_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_font_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_keybind_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_bool_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_value_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                        self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                    } else if is_bevel_type || is_relief_type {
                         // The (bevel)/(relief) preview: a mini lit
                         // cross-section of the knob triple; clicking it opens
                         // cce-relief (targeted at the KEY for a relief value).
@@ -1901,6 +2017,8 @@ impl Application for DataEditorApp {
                     self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                     self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                     self.selected_ramp_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                    self.selected_len_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                    self.selected_unit_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 }
             } else {
                 self.selected_value_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
@@ -1910,6 +2028,8 @@ impl Application for DataEditorApp {
                 self.selected_choice_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 self.selected_keybind_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 self.selected_bool_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                self.selected_len_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
+                self.selected_unit_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 self.selected_button_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 self.selected_bevel_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
                 self.selected_ramp_editor.set_rect(-1000.0, -1000.0, 1.0, 1.0);
@@ -1929,6 +2049,9 @@ impl Application for DataEditorApp {
         if self.selected_choice_editor.popover_rect().is_some() {
             self.ui_context.register_popover(&mut self.selected_choice_editor);
         }
+        if self.selected_unit_editor.popover_rect().is_some() {
+            self.ui_context.register_popover(&mut self.selected_unit_editor);
+        }
         if self.btn_open.popover_rect().is_some() {
             self.ui_context.register_popover(&mut self.btn_open);
         }
@@ -1991,7 +2114,9 @@ impl Application for DataEditorApp {
             // tree, an editor's box, border and caret all landed under that quad — only its
             // text survived, because the engine draws every label after all geometry. Hence
             // "the value control has no caret".
-            let editors: [&dyn cce_ui::widget::WidgetHost; 10] = [
+            let editors: [&dyn cce_ui::widget::WidgetHost; 12] = [
+                &self.selected_len_editor,
+                &self.selected_unit_editor,
                 &self.selected_value_editor,
                 &self.selected_color_editor,
                 &self.selected_spinbox_editor,
@@ -2104,6 +2229,8 @@ impl Application for DataEditorApp {
             if self.ui_context.propagate_event(&ev, self.selected_spinbox_editor.id()) { changed = true; }
             if self.ui_context.propagate_event(&ev, self.selected_font_editor.id()) { changed = true; }
             if self.ui_context.propagate_event(&ev, self.selected_choice_editor.id()) { changed = true; }
+            if self.ui_context.propagate_event(&ev, self.selected_len_editor.id()) { changed = true; }
+            if self.ui_context.propagate_event(&ev, self.selected_unit_editor.id()) { changed = true; }
             if self.ui_context.propagate_event(&ev, self.selected_keybind_editor.id()) { changed = true; }
             if self.ui_context.propagate_event(&ev, self.selected_bool_editor.id()) { changed = true; }
             if self.ui_context.propagate_event(&ev, self.selected_button_editor.id()) { changed = true; }
@@ -2232,6 +2359,14 @@ impl Application for DataEditorApp {
             changed = true;
             editor_handled = true;
         }
+        if self.ui_context.propagate_event(&mouse_ev, self.selected_len_editor.id()) {
+            changed = true;
+            editor_handled = true;
+        }
+        if self.ui_context.propagate_event(&mouse_ev, self.selected_unit_editor.id()) {
+            changed = true;
+            editor_handled = true;
+        }
         if self.ui_context.propagate_event(&mouse_ev, self.selected_keybind_editor.id()) {
             changed = true;
             editor_handled = true;
@@ -2349,9 +2484,12 @@ impl Application for DataEditorApp {
                         self.selected_value_editor.edit_buffer = self.selected_value_editor.text.clone();
                         self.selected_value_editor.editing = false;
                         
-                        // Sync controls:
-                        let val = &self.flat_keys[original_idx].1;
-                        let key_name = &self.flat_keys[original_idx].0;
+                        // Sync controls (owned copies: the length sync below
+                        // borrows self mutably while these are still in use).
+                        let val = self.flat_keys[original_idx].1.clone();
+                        let val = &val;
+                        let key_name = self.flat_keys[original_idx].0.clone();
+                        let key_name = &key_name;
                         let is_font_type = key_name == "font" || key_name.ends_with("_font") || key_name.ends_with(".font");
                         
                         let mut is_menu_type = false;
@@ -2380,7 +2518,9 @@ impl Application for DataEditorApp {
                                 self.selected_choice_editor.selected = 0;
                             }
                         } else if let serde_json::Value::String(s) = val {
-                            if let Some(rgba) = parse_hex_color_rgba(s) {
+                            if let Some(len) = cce_ui::units::Len::parse(s) {
+                                self.sync_len_editors(len);
+                            } else if let Some(rgba) = parse_hex_color_rgba(s) {
                                 self.selected_color_editor.color = [rgba[0], rgba[1], rgba[2]];
                                 self.selected_color_editor.alpha = rgba[3];
                                 let clean_s = s.trim_matches(|c| c == '"' || c == '\'' || c == ' ').trim_start_matches('#');
@@ -2396,7 +2536,11 @@ impl Application for DataEditorApp {
                         
                         let is_keybind_type = key_name == "key" || key_name == "keybind" || key_name == "shortcut" || key_name == "delete" || key_name.ends_with("_key") || key_name.ends_with(".key") || key_name.ends_with(".keybind") || key_name.ends_with("_delete") || key_name.ends_with(".delete") || key_name == "brightness_up" || key_name == "brightness_down" || key_name.ends_with(".brightness_up") || key_name.ends_with(".brightness_down") || annotation_str.as_deref() == Some("keybind");
 
-                        if is_menu_type {
+                        let is_len_type = annotation_str.as_deref().and_then(cce_ui::units::Unit::parse).is_some()
+                            || len_of(val).is_some();
+                        if is_len_type {
+                            self.ui_context.set_focused(&mut self.selected_len_editor);
+                        } else if is_menu_type {
                             self.ui_context.set_focused(&mut self.selected_choice_editor);
                         } else if is_keybind_type {
                             if let serde_json::Value::String(s) = val {
@@ -2436,6 +2580,8 @@ impl Application for DataEditorApp {
                 self.selected_spinbox_editor.unfocus();
                 self.selected_font_editor.unfocus();
                 self.selected_choice_editor.unfocus();
+                self.selected_len_editor.unfocus();
+                self.selected_unit_editor.unfocus();
                 self.selected_keybind_editor.unfocus();
                 self.tree_list.unfocus();
                 // The manual unfocus sweep above leaves ctx.focused_widget stale;
@@ -2578,7 +2724,7 @@ impl Application for DataEditorApp {
         let value_was_editing = self.selected_value_editor.editing;
         if !handled {
             let key_ev = cce_ui::widget::Event::KeyInput(event.clone());
-            let roots: [cce_ui::widget::WidgetId; 10] = [
+            let roots: [cce_ui::widget::WidgetId; 12] = [
                 self.tree_list.id(),
                 self.btn_open.id(),
                 self.raw_json_editor.id(),
@@ -2587,6 +2733,8 @@ impl Application for DataEditorApp {
                 self.selected_spinbox_editor.id(),
                 self.selected_font_editor.id(),
                 self.selected_choice_editor.id(),
+                self.selected_len_editor.id(),
+                self.selected_unit_editor.id(),
                 self.selected_keybind_editor.id(),
                 self.selected_bool_editor.id(),
             ];
@@ -2643,6 +2791,7 @@ impl Application for DataEditorApp {
         for (open, id) in [
             (self.btn_open.popover_rect().is_some(), self.btn_open.id()),
             (self.selected_choice_editor.popover_rect().is_some(), self.selected_choice_editor.id()),
+            (self.selected_unit_editor.popover_rect().is_some(), self.selected_unit_editor.id()),
         ] {
             if open {
                 self.ui_context.propagate_event(&cce_ui::widget::Event::FocusOut, id);