irongit

perf: smarter cloning on groups view

huncholanehuncholaneauthored
parent 8bef318commit 5ab7ffc925d780acee0f7242d54d28ea78fc5bfdBrowse files

3 files changed, +269 -168

+33-14src/app.rs
@@ -42,6 +42,9 @@ pub struct JvApp {
4242
4343 // Disabled (toggled-off) source files by index
4444 disabled_files: std::collections::BTreeSet<usize>,
45+ // Cached active files — avoids cloning every frame
46+ cached_active_files: Vec<(String, serde_json::Value)>,
47+ cached_active_key: u64,
4548
4649 // UI state
4750 new_session_name: String,
@@ -227,6 +230,8 @@ impl JvApp {
227230 last_file_index: usize::MAX,
228231 last_file_count: 0,
229232 disabled_files: std::collections::BTreeSet::new(),
233+ cached_active_files: Vec::new(),
234+ cached_active_key: u64::MAX,
230235 new_session_name: String::new(),
231236 show_new_session_dialog: false,
232237 sidebar_width: 240.0,
@@ -245,17 +250,28 @@ impl JvApp {
245250 }
246251
247252 /// Get only the enabled (non-disabled) parsed files
248- fn active_parsed_files(&self) -> Vec<(String, serde_json::Value)> {
249- let Some(loaded) = &self.current_session else {
250- return Vec::new();
251- };
252- loaded
253- .parsed_files
254- .iter()
255- .enumerate()
256- .filter(|(i, _)| !self.disabled_files.contains(i))
257- .map(|(_, f)| f.clone())
258- .collect()
253+ fn ensure_active_files_cache(&mut self) {
254+ use std::hash::{Hash, Hasher};
255+ let mut h = std::collections::hash_map::DefaultHasher::new();
256+ let file_count = self.current_session.as_ref()
257+ .map(|l| l.parsed_files.len()).unwrap_or(0);
258+ file_count.hash(&mut h);
259+ self.disabled_files.len().hash(&mut h);
260+ for &i in &self.disabled_files {
261+ i.hash(&mut h);
262+ }
263+ let key = h.finish();
264+ if self.cached_active_key != key {
265+ self.cached_active_files = if let Some(loaded) = &self.current_session {
266+ loaded.parsed_files.iter().enumerate()
267+ .filter(|(i, _)| !self.disabled_files.contains(i))
268+ .map(|(_, f)| f.clone())
269+ .collect()
270+ } else {
271+ Vec::new()
272+ };
273+ self.cached_active_key = key;
274+ }
259275 }
260276
261277 fn rebuild_schema(&mut self) {
@@ -799,7 +815,8 @@ impl JvApp {
799815 });
800816 return;
801817 }
802- let active_files = self.active_parsed_files();
818+ self.ensure_active_files_cache();
819+ let active_files = &self.cached_active_files;
803820 let loaded = self.current_session.as_ref().unwrap();
804821 let schema = loaded.schema.as_ref().unwrap();
805822 let structs = schema.structs.clone();
@@ -874,7 +891,8 @@ impl JvApp {
874891 return;
875892 }
876893
877- let active_files = self.active_parsed_files();
894+ self.ensure_active_files_cache();
895+ let active_files = &self.cached_active_files;
878896 let loaded = self.current_session.as_ref().unwrap();
879897 let schema = loaded.schema.as_ref().unwrap();
880898 self.shared_browser_view.show(ui, schema, &active_files);
@@ -896,7 +914,8 @@ impl JvApp {
896914 });
897915 return;
898916 }
899- let active_files = self.active_parsed_files();
917+ self.ensure_active_files_cache();
918+ let active_files = &self.cached_active_files;
900919 let loaded = self.current_session.as_ref().unwrap();
901920 let schema = loaded.schema.clone();
902921 let prev_enums = loaded.session.enum_conversions.clone();
+6-0src/schema.rs
@@ -18,6 +18,12 @@ pub struct SchemaOverview {
1818
1919 impl SchemaOverview {
2020 /// All known structs (shared + unique) for type resolution.
21+ /// Returns references — no cloning.
22+ pub fn all_structs_ref(&self) -> Vec<&SharedStruct> {
23+ self.structs.iter().chain(self.unique_structs.iter()).collect()
24+ }
25+
26+ /// All known structs (shared + unique) as owned vec. Use sparingly (clones).
2127 pub fn all_structs(&self) -> Vec<SharedStruct> {
2228 self.structs.iter().chain(self.unique_structs.iter()).cloned().collect()
2329 }
+230-154src/views/shared_browser.rs
@@ -11,6 +11,22 @@ pub struct SharedBrowserView {
1111 scroll_to_selection: bool,
1212 restore_key: Option<String>,
1313 sort_alpha: bool,
14+ /// Cached all_structs — recomputed only when schema changes
15+ cached_all: Vec<SharedStruct>,
16+ cached_all_key: usize,
17+ /// Cached entries — recomputed only when path changes
18+ entries_cache_key: Vec<String>,
19+ cached_current_entries: Vec<Entry>,
20+ cached_parent_entries: Option<Vec<Entry>>,
21+ /// Cached values column data: (path, selection_label) -> flattened rows
22+ values_cache_key: (Vec<String>, String),
23+ values_rows: Vec<ValueRow>,
24+}
25+
26+/// Pre-flattened row for the values column (file headers + values).
27+enum ValueRow {
28+ FileHeader(String), // display filename
29+ Value { text: String, color: egui::Color32, count: Option<usize> },
1430 }
1531
1632 /// An entry at a given level of the schema tree.
@@ -30,12 +46,23 @@ impl SharedBrowserView {
3046 scroll_to_selection: false,
3147 restore_key: None,
3248 sort_alpha: false,
49+ cached_all: Vec::new(),
50+ cached_all_key: usize::MAX, // force initial rebuild
51+ entries_cache_key: vec!["__invalid__".to_string()], // force initial rebuild
52+ cached_current_entries: Vec::new(),
53+ cached_parent_entries: None,
54+ values_cache_key: (Vec::new(), String::new()),
55+ values_rows: Vec::new(),
3356 }
3457 }
3558
3659 pub fn invalidate(&mut self) {
3760 self.path.clear();
3861 self.selection = 0;
62+ self.cached_all_key = usize::MAX;
63+ self.entries_cache_key = vec!["__invalid__".to_string()];
64+ self.values_cache_key = (Vec::new(), String::new());
65+ self.values_rows.clear();
3966 }
4067
4168 pub fn show(
@@ -55,43 +82,56 @@ impl SharedBrowserView {
5582 return;
5683 }
5784
58- // Build entries for current level and parent level
59- let mut current_entries = build_entries_at_path(&self.path, schema);
60- if self.sort_alpha && self.path.len() == 1 {
61- current_entries.sort_by(|a, b| a.label.cmp(&b.label));
85+ // Cache all_structs — only recompute when schema changes
86+ let schema_key = schema.structs.len() + schema.unique_structs.len();
87+ if self.cached_all_key != schema_key {
88+ self.cached_all = schema.all_structs();
89+ self.cached_all_key = schema_key;
90+ }
91+ let all = &self.cached_all;
92+
93+ // Build entries — cached, only recompute when path changes
94+ if self.entries_cache_key != self.path {
95+ self.cached_current_entries = build_entries_at_path(&self.path, schema, all);
96+ if self.sort_alpha && self.path.len() == 1 {
97+ self.cached_current_entries.sort_by(|a, b| a.label.cmp(&b.label));
98+ }
99+ self.cached_parent_entries = if self.path.is_empty() {
100+ None
101+ } else {
102+ Some(build_entries_at_path(
103+ &self.path[..self.path.len() - 1],
104+ schema,
105+ all,
106+ ))
107+ };
108+ self.entries_cache_key = self.path.clone();
62109 }
63- let parent_entries = if self.path.is_empty() {
64- None
65- } else {
66- Some(build_entries_at_path(
67- &self.path[..self.path.len() - 1],
68- schema,
69- ))
70- };
71110
72111 // Restore selection by key after going back
73112 if let Some(key) = self.restore_key.take() {
74- if let Some(idx) = current_entries.iter().position(|e| e.label == key) {
113+ if let Some(idx) = self.cached_current_entries.iter().position(|e| e.label == key) {
75114 self.selection = idx;
76115 }
77116 }
78117
79118 // Clamp selection
80- if !current_entries.is_empty() && self.selection >= current_entries.len() {
81- self.selection = current_entries.len().saturating_sub(1);
119+ if !self.cached_current_entries.is_empty() && self.selection >= self.cached_current_entries.len() {
120+ self.selection = self.cached_current_entries.len().saturating_sub(1);
82121 }
83122
84123 // Keyboard navigation
85124 let action = crate::widgets::read_miller_keys(ui, false);
86- if crate::widgets::apply_selection(&mut self.selection, action, current_entries.len()) {
125+ if crate::widgets::apply_selection(&mut self.selection, action, self.cached_current_entries.len()) {
87126 self.scroll_to_selection = true;
88127 }
89128 if action == crate::widgets::MillerAction::Enter {
90- if let Some(entry) = current_entries.get(self.selection) {
129+ if let Some(entry) = self.cached_current_entries.get(self.selection) {
91130 if entry.is_container {
92131 self.path.push(entry.label.clone());
93132 self.selection = 0;
94133 self.scroll_to_selection = true;
134+ self.entries_cache_key.clear(); // force rebuild next frame
95135 }
96136 }
97137 }
@@ -99,26 +139,30 @@ impl SharedBrowserView {
99139 let popped = self.path.pop().unwrap();
100140 self.restore_key = Some(popped);
101141 self.scroll_to_selection = true;
142+ self.entries_cache_key.clear(); // force rebuild next frame
102143 }
103144
104145 // 'A' toggles alphabetical sort on the structs pane
105146 let toggle_sort = ui.input(|i| i.key_pressed(egui::Key::A));
106147 if toggle_sort && self.path.len() == 1 {
107- let selected_label = current_entries.get(self.selection).map(|e| e.label.clone());
148+ let selected_label = self.cached_current_entries.get(self.selection).map(|e| e.label.clone());
108149 self.sort_alpha = !self.sort_alpha;
109- // Re-sort in place
150+ // Invalidate + rebuild
151+ self.entries_cache_key.clear();
152+ self.cached_current_entries = build_entries_at_path(&self.path, schema, all);
110153 if self.sort_alpha {
111- current_entries.sort_by(|a, b| a.label.cmp(&b.label));
112- } else {
113- current_entries = build_entries_at_path(&self.path, schema);
154+ self.cached_current_entries.sort_by(|a, b| a.label.cmp(&b.label));
114155 }
115- // Restore selection to the same struct after reordering
156+ self.entries_cache_key = self.path.clone();
116157 if let Some(label) = selected_label {
117- if let Some(idx) = current_entries.iter().position(|e| e.label == label) {
158+ if let Some(idx) = self.cached_current_entries.iter().position(|e| e.label == label) {
118159 self.selection = idx;
119160 }
120161 }
121162 }
163+ // Re-borrow after potential mutation
164+ let current_entries = &self.cached_current_entries;
165+ let parent_entries = &self.cached_parent_entries;
122166
123167 // --- Three-column miller layout ---
124168 let avail = ui.available_rect_before_wrap();
@@ -176,12 +220,22 @@ impl SharedBrowserView {
176220 ui.set_height(col_height);
177221 render_pane_title(ui, &right_title);
178222 if let Some(entry) = current_entries.get(self.selection) {
223+ // Cache the expensive value collection + flattening
224+ let cache_key = (self.path.clone(), entry.label.clone());
225+ if self.values_cache_key != cache_key {
226+ self.values_rows = build_value_rows(
227+ &self.path, &entry.label, schema, &all, files,
228+ );
229+ self.values_cache_key = cache_key;
230+ }
231+
179232 if let Some(nav) = render_values_column(
180233 ui,
181234 &self.path,
182235 &entry.label,
183236 schema,
184- files,
237+ &all,
238+ &self.values_rows,
185239 col_height,
186240 ) {
187241 type_nav = Some(nav);
@@ -215,7 +269,7 @@ impl SharedBrowserView {
215269 }
216270
217271 /// Build the list of entries at a given path in the schema tree.
218-fn build_entries_at_path(path: &[String], schema: &SchemaOverview) -> Vec<Entry> {
272+fn build_entries_at_path(path: &[String], schema: &SchemaOverview, all: &[SharedStruct]) -> Vec<Entry> {
219273 match path.len() {
220274 0 => {
221275 // Root: show "Shared" and "Unique" categories
@@ -269,11 +323,8 @@ fn build_entries_at_path(path: &[String], schema: &SchemaOverview) -> Vec<Entry>
269323 return Vec::new();
270324 };
271325
272- // Use all structs (shared + unique) for type resolution and display
273- let all = schema.all_structs();
274-
275326 // Walk remaining path segments to find the current type
276- let current_type = resolve_type_at_path(&root_struct.fields, &path[2..], &all);
327+ let current_type = resolve_type_at_path(&root_struct.fields, &path[2..], all);
277328
278329 match current_type {
279330 Some(fields) => fields
@@ -556,12 +607,132 @@ fn render_current_column(
556607
557608 /// Render the right column showing all values found for the selected entry.
558609 /// Returns an optional navigation path if the user clicks a type link.
610+/// Build pre-flattened rows for the values column (expensive — cached).
611+fn build_value_rows(
612+ path: &[String],
613+ selected_label: &str,
614+ schema: &SchemaOverview,
615+ all: &[SharedStruct],
616+ files: &[(String, serde_json::Value)],
617+) -> Vec<ValueRow> {
618+ if path.len() < 2 {
619+ return Vec::new();
620+ }
621+ let structs = match path[0].as_str() {
622+ "Shared" => &schema.structs,
623+ "Unique" => &schema.unique_structs,
624+ _ => return Vec::new(),
625+ };
626+ let Some(root_struct) = structs.iter().find(|s| s.name == path[1]) else {
627+ return Vec::new();
628+ };
629+
630+ let selected_field_type = resolve_type_at_path(&root_struct.fields, &path[2..], all)
631+ .and_then(|fields| fields.get(selected_label).cloned());
632+ let is_primitive = selected_field_type
633+ .as_ref()
634+ .map(|t| !is_navigable_type(t, all))
635+ .unwrap_or(true);
636+
637+ let raw_values = collect_values_for_field(path, selected_label, schema, all, files);
638+
639+ // Group by file
640+ let mut per_file: Vec<(&str, Vec<&str>)> = Vec::new();
641+ for (val, filename) in &raw_values {
642+ if let Some(entry) = per_file.last_mut().filter(|(f, _)| *f == filename.as_str()) {
643+ entry.1.push(val.as_str());
644+ } else {
645+ per_file.push((filename.as_str(), vec![val.as_str()]));
646+ }
647+ }
648+
649+ let mut rows = Vec::new();
650+ for (filename, values) in &per_file {
651+ let display_name = filename.strip_suffix(".json").unwrap_or(filename);
652+ rows.push(ValueRow::FileHeader(format!("── {} ──", display_name)));
653+
654+ if is_primitive {
655+ let mut counts: Vec<(&str, usize)> = Vec::new();
656+ for val in values {
657+ if let Some(entry) = counts.iter_mut().find(|(v, _)| *v == *val) {
658+ entry.1 += 1;
659+ } else {
660+ counts.push((val, 1));
661+ }
662+ }
663+ counts.sort_by(|a, b| b.1.cmp(&a.1));
664+ for (val_str, count) in counts {
665+ let display = if val_str.len() > 60 {
666+ format!("{}…", &val_str[..57])
667+ } else {
668+ val_str.to_string()
669+ };
670+ let color = value_color(val_str);
671+ rows.push(ValueRow::Value {
672+ text: display,
673+ color,
674+ count: if count > 1 { Some(count) } else { None },
675+ });
676+ }
677+ } else {
678+ for val_str in values {
679+ let display = if val_str.len() > 80 {
680+ format!("{}…", &val_str[..77])
681+ } else {
682+ val_str.to_string()
683+ };
684+ let color = value_color(val_str);
685+ rows.push(ValueRow::Value { text: display, color, count: None });
686+ }
687+ }
688+ }
689+ rows
690+}
691+
692+/// Collect all field values across files (expensive — should be cached).
693+fn collect_values_for_field(
694+ path: &[String],
695+ selected_label: &str,
696+ schema: &SchemaOverview,
697+ all: &[SharedStruct],
698+ files: &[(String, serde_json::Value)],
699+) -> Vec<(String, String)> {
700+ if path.len() < 2 {
701+ return Vec::new();
702+ }
703+ let structs = match path[0].as_str() {
704+ "Shared" => &schema.structs,
705+ "Unique" => &schema.unique_structs,
706+ _ => return Vec::new(),
707+ };
708+ let Some(root_struct) = structs.iter().find(|s| s.name == path[1]) else {
709+ return Vec::new();
710+ };
711+ let mut field_path: Vec<&str> = path[2..].iter().map(|s| s.as_str()).collect();
712+ field_path.push(selected_label);
713+ let field_keys: Vec<&str> = root_struct.fields.keys().map(|k| k.as_str()).collect();
714+
715+ let mut all_values: Vec<(String, String)> = Vec::new();
716+ for (filename, value) in files {
717+ if !root_struct.source_files.contains(filename) {
718+ continue;
719+ }
720+ let mut values: Vec<String> = Vec::new();
721+ collect_nested_field_values(value, &field_keys, &field_path, &mut values);
722+ for v in values {
723+ all_values.push((v, filename.clone()));
724+ }
725+ }
726+ all_values
727+}
728+
559729 fn render_values_column(
560730 ui: &mut Ui,
561731 path: &[String],
562732 selected_label: &str,
563733 schema: &SchemaOverview,
564- files: &[(String, serde_json::Value)],
734+ all: &[SharedStruct],
735+ rows: &[ValueRow],
565736 height: f32,
566737 ) -> Option<Vec<String>> {
567738 let mut nav_target: Option<Vec<String>> = None;
@@ -581,12 +752,14 @@ fn render_values_column(
581752 );
582753 ui.add_space(4.0);
583754
755+ let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 6.0;
584756 egui::ScrollArea::vertical()
585757 .id_salt("shared_values")
586758 .auto_shrink(false)
587759 .max_height(height - 24.0)
588- .show(ui, |ui| {
589- for s in structs {
760+ .show_rows(ui, row_height, structs.len(), |ui, range| {
761+ for i in range {
762+ let s = &structs[i];
590763 ui.horizontal(|ui| {
591764 ui.label(
592765 RichText::new(egui_phosphor::regular::BRACKETS_CURLY)
@@ -639,13 +812,15 @@ fn render_values_column(
639812 );
640813 ui.add_space(4.0);
641814
815+ let fields_vec: Vec<(&String, &InferredType)> = s.fields.iter().collect();
816+ let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 6.0;
642817 egui::ScrollArea::vertical()
643818 .id_salt("shared_values")
644819 .auto_shrink(false)
645820 .max_height(height - 40.0)
646- .show(ui, |ui| {
647- let all = schema.all_structs();
648- for (idx, (name, typ)) in s.fields.iter().enumerate() {
821+ .show_rows(ui, row_height, fields_vec.len(), |ui, range| {
822+ for idx in range {
823+ let (name, typ) = fields_vec[idx];
649824 let short = typ.short_name(&all);
650825 let color = crate::theme::type_color(&short);
651826 let is_nav = is_navigable_type(typ, &all);
@@ -712,7 +887,7 @@ fn render_values_column(
712887 });
713888 }
714889 _ => {
715- // Inside a struct — selected is a field name, show all values across files
890+ // Inside a struct — show pre-flattened value rows with virtual scrolling
716891 let structs = match path[0].as_str() {
717892 "Shared" => &schema.structs,
718893 "Unique" => &schema.unique_structs,
@@ -722,26 +897,14 @@ fn render_values_column(
722897 return None;
723898 };
724899
725- // Build the full field path from path[2..] + selected_label
726- let mut field_path: Vec<&str> = path[2..].iter().map(|s| s.as_str()).collect();
727- field_path.push(selected_label);
728-
729- let all = schema.all_structs();
730-
731- // Get the type of the selected field
732- let selected_field_type = resolve_type_at_path(&root_struct.fields, &path[2..], &all)
900+ let selected_field_type = resolve_type_at_path(&root_struct.fields, &path[2..], all)
733901 .and_then(|fields| fields.get(selected_label).cloned());
734902
735903 let field_type_str = selected_field_type
736904 .as_ref()
737- .map(|t| t.short_name(&all))
905+ .map(|t| t.short_name(all))
738906 .unwrap_or_default();
739907
740- let is_primitive = selected_field_type
741- .as_ref()
742- .map(|t| !is_navigable_type(t, &all))
743- .unwrap_or(true);
744-
745908 ui.label(
746909 RichText::new(format!("{}: {}", selected_label, field_type_str))
747910 .color(CatppuccinMocha::BLUE)
@@ -749,100 +912,34 @@ fn render_values_column(
749912 );
750913 ui.add_space(4.0);
751914
752- // Collect field keys for shape matching
753- let field_keys: Vec<&str> = root_struct.fields.keys().map(|k| k.as_str()).collect();
754-
755- // Collect all values across all files
756- let mut all_values: Vec<(String, String)> = Vec::new(); // (value, filename)
757- for (filename, value) in files {
758- if !root_struct.source_files.contains(filename) {
759- continue;
760- }
761- let mut values: Vec<String> = Vec::new();
762- collect_nested_field_values(value, &field_keys, &field_path, &mut values);
763- for v in values {
764- all_values.push((v, filename.clone()));
765- }
766- }
767-
768- // Group values per file for display
769- let mut per_file: Vec<(&str, Vec<&str>)> = Vec::new();
770- for (val, filename) in &all_values {
771- if let Some(entry) = per_file.last_mut().filter(|(f, _)| *f == filename.as_str()) {
772- entry.1.push(val.as_str());
773- } else {
774- per_file.push((filename.as_str(), vec![val.as_str()]));
775- }
776- }
777-
915+ let row_height = ui.text_style_height(&egui::TextStyle::Monospace) + 4.0;
778916 egui::ScrollArea::vertical()
779917 .id_salt("shared_values")
780918 .auto_shrink(false)
781919 .max_height(height - 24.0)
782- .show(ui, |ui| {
783- for (filename, values) in &per_file {
784- let display_name = filename.strip_suffix(".json").unwrap_or(filename);
785-
786- // File separator header
787- ui.add_space(2.0);
788- ui.horizontal(|ui| {
789- let sep_rect = ui.available_rect_before_wrap();
790- let y = sep_rect.center().y;
791- let left = sep_rect.left();
792-
793- ui.painter().line_segment(
794- [egui::pos2(left, y), egui::pos2(left + 8.0, y)],
795- egui::Stroke::new(1.0, CatppuccinMocha::SURFACE2),
796- );
797-
798- ui.add_space(10.0);
799- ui.label(
800- RichText::new(display_name)
801- .color(CatppuccinMocha::YELLOW)
802- .family(egui::FontFamily::Monospace)
803- .size(11.0),
804- );
805-
806- let after = ui.available_rect_before_wrap();
807- ui.painter().line_segment(
808- [egui::pos2(after.left(), y), egui::pos2(after.right(), y)],
809- egui::Stroke::new(1.0, CatppuccinMocha::SURFACE2),
810- );
811- });
812- ui.add_space(2.0);
813-
814- if is_primitive {
815- // Group identical values within this file
816- let mut counts: Vec<(&str, usize)> = Vec::new();
817- for val in values {
818- if let Some(entry) = counts.iter_mut().find(|(v, _)| *v == *val) {
819- entry.1 += 1;
820- } else {
821- counts.push((val, 1));
822- }
920+ .show_rows(ui, row_height, rows.len(), |ui, range| {
921+ for i in range {
922+ match &rows[i] {
923+ ValueRow::FileHeader(name) => {
924+ ui.label(
925+ RichText::new(name)
926+ .color(CatppuccinMocha::YELLOW)
927+ .family(egui::FontFamily::Monospace)
928+ .size(11.0),
929+ );
823930 }
824- counts.sort_by(|a, b| b.1.cmp(&a.1));
825-
826- for (val_str, count) in &counts {
827- let display = if val_str.len() > 60 {
828- format!("{}…", &val_str[..57])
829- } else {
830- val_str.to_string()
831- };
832-
833- let color = value_color(val_str);
834-
931+ ValueRow::Value { text, color, count } => {
835932 ui.horizontal(|ui| {
836933 ui.add_space(4.0);
837934 ui.label(
838- RichText::new(&display)
839- .color(color)
935+ RichText::new(text)
936+ .color(*color)
840937 .family(egui::FontFamily::Monospace)
841938 .size(11.0),
842939 );
843- if *count > 1 {
940+ if let Some(c) = count {
844941 ui.label(
845- RichText::new(format!("×{}", count))
942+ RichText::new(format!("×{}", c))
846943 .color(CatppuccinMocha::OVERLAY0)
847944 .family(egui::FontFamily::Monospace)
848945 .size(10.0),
@@ -850,27 +947,6 @@ fn render_values_column(
850947 }
851948 });
852949 }
853- } else {
854- // Non-primitive: list each value
855- for val_str in values {
856- let display = if val_str.len() > 80 {
857- format!("{}…", &val_str[..77])
858- } else {
859- val_str.to_string()
860- };
861-
862- let color = value_color(val_str);
863-
864- ui.horizontal(|ui| {
865- ui.add_space(4.0);
866- ui.label(
867- RichText::new(&display)
868- .color(color)
869- .family(egui::FontFamily::Monospace)
870- .size(11.0),
871- );
872- });
873- }
874950 }
875951 }
876952 });