Skip to main content

libsurfer/
memory_viewer.rs

1use crate::{
2    Message,
3    system_state::SystemState,
4    translation::{TranslationResultExt, ValueKindExt},
5    wave_container::{ScopeRef, ScopeRefExt},
6};
7use egui::collapsing_header::CollapsingState;
8use egui::{Button, DragValue};
9use egui_extras::{Column, TableBuilder};
10use egui_remixicon::icons;
11use fuzzy_matcher::{FuzzyMatcher, skim::SkimMatcherV2};
12use regex::RegexBuilder;
13use std::rc::Rc;
14use surfer_translation_types::{TranslationPreference, Translator, ValueKind};
15#[derive(Clone, Copy, PartialEq, Eq)]
16pub enum MemoryViewerFormat {
17    Decimal,
18    Hexadecimal,
19    Binary,
20}
21impl MemoryViewerFormat {
22    #[must_use]
23    pub fn label(self) -> &'static str {
24        match self {
25            Self::Decimal => "Decimal",
26            Self::Hexadecimal => "Hexadecimal",
27            Self::Binary => "Binary",
28        }
29    }
30}
31pub(crate) struct MemoryViewerState {
32    pub(crate) open: bool,
33    pub(crate) scope: Option<ScopeRef>,
34    pub(crate) name: Option<String>,
35    jump_to_index: String,
36    search_value: String,
37    highlight_value: String,
38    search_match_mode: ValueMatchMode,
39    highlight_match_mode: ValueMatchMode,
40    highlight_case_insensitive: bool,
41    search_case_insensitive: bool,
42    filter_value: String,
43    filter_match_mode: ValueMatchMode,
44    filter_case_insensitive: bool,
45    index_format: MemoryViewerFormat,
46    value_format: String,
47    scroll_to_row: Option<usize>,
48    color_values: bool,
49    filter_mode: ChangeModes,
50    highlight_mode: ChangeModes,
51    value_column_count: usize,
52    selected_value_position: Option<usize>,
53}
54impl Default for MemoryViewerState {
55    fn default() -> Self {
56        Self {
57            open: false,
58            scope: None,
59            name: None,
60            jump_to_index: String::new(),
61            search_value: String::new(),
62            search_match_mode: ValueMatchMode::Contains,
63            search_case_insensitive: true,
64            highlight_value: String::new(),
65            highlight_match_mode: ValueMatchMode::Contains,
66            highlight_case_insensitive: true,
67            filter_value: String::new(),
68            filter_match_mode: ValueMatchMode::Contains,
69            filter_case_insensitive: true,
70            index_format: MemoryViewerFormat::Decimal,
71            value_format: "Hexadecimal".to_string(),
72            scroll_to_row: None,
73            color_values: false,
74            selected_value_position: None,
75            value_column_count: 1,
76            filter_mode: ChangeModes::AllValues,
77            highlight_mode: ChangeModes::AllValues,
78        }
79    }
80}
81#[derive(Clone, Copy, PartialEq, Eq)]
82pub enum ChangeModes {
83    AllValues,
84    ChangedAtCursor,
85    ChangedBtwCursorAndMarker(u8),
86}
87#[derive(Clone)]
88pub(crate) struct MemoryRow {
89    index: i64,
90    value: String,
91    kind: ValueKind,
92    changed_at_cursor: bool,
93    changed_for_filter: bool,
94    changed_for_highlight: bool,
95}
96#[derive(Clone, PartialEq, Eq)]
97pub(crate) struct MemoryViewerCacheKey {
98    pub scope: crate::wave_container::ScopeRef,
99    pub cursor: num::BigUint,
100    pub value_format: String,
101    pub filter_mode: ChangeModes,
102    pub highlight_mode: ChangeModes,
103}
104#[derive(Clone, Copy, PartialEq, Eq)]
105pub(crate) enum ValueMatchMode {
106    Contains,
107    StartsWith,
108    Regex,
109    Fuzzy,
110}
111
112impl ValueMatchMode {
113    fn label(self) -> &'static str {
114        match self {
115            Self::Contains => "Value contains",
116            Self::StartsWith => "Value starts with",
117            Self::Regex => "Regular expression",
118            Self::Fuzzy => "Fuzzy",
119        }
120    }
121}
122#[derive(Clone)]
123pub(crate) struct MemoryViewerCache {
124    pub key: MemoryViewerCacheKey,
125    pub rows: std::rc::Rc<Vec<MemoryRow>>,
126}
127fn format_index(index: i64, format: MemoryViewerFormat, max_index: i64) -> String {
128    match format {
129        MemoryViewerFormat::Decimal => {
130            let width = max_index.to_string().len();
131            format!("{index:0width$}")
132        }
133        MemoryViewerFormat::Hexadecimal => {
134            let width = format!("{max_index:x}").len();
135            format!("{index:0width$x}")
136        }
137        MemoryViewerFormat::Binary => {
138            let width = format!("{max_index:b}").len();
139            format!("{index:0width$b}")
140        }
141    }
142}
143
144fn parse_index_from_name(name: &str) -> Option<i64> {
145    name.trim()
146        .trim_start_matches('[')
147        .split(']')
148        .next()
149        .and_then(|index| index.parse::<i64>().ok())
150}
151
152fn parse_jump_to_index(input: &str) -> Option<i64> {
153    let input = input.trim();
154
155    if let Some(hex) = input.strip_prefix("0x") {
156        i64::from_str_radix(hex, 16).ok()
157    } else if let Some(binary) = input.strip_prefix("0b") {
158        i64::from_str_radix(binary, 2).ok()
159    } else {
160        input.parse::<i64>().ok()
161    }
162}
163
164fn closest_row_index(rows: &[&MemoryRow], target_index: i64) -> Option<usize> {
165    rows.iter()
166        .enumerate()
167        .min_by_key(|(_position, row)| (row.index - target_index).abs())
168        .map(|(row_index, _)| row_index)
169}
170
171type ValueMatcher = Box<dyn Fn(&str) -> bool>;
172
173fn build_value_matcher(
174    pattern: &str,
175    mode: ValueMatchMode,
176    case_insensitive: bool,
177) -> Option<ValueMatcher> {
178    let pattern = pattern.trim();
179
180    if pattern.is_empty() {
181        return None;
182    }
183
184    match mode {
185        ValueMatchMode::Contains => {
186            if case_insensitive {
187                let pattern = pattern.to_lowercase();
188
189                Some(Box::new(move |value| {
190                    value.to_lowercase().contains(&pattern)
191                }))
192            } else {
193                let pattern = pattern.to_string();
194
195                Some(Box::new(move |value| value.contains(&pattern)))
196            }
197        }
198
199        ValueMatchMode::StartsWith => {
200            if case_insensitive {
201                let pattern = pattern.to_lowercase();
202
203                Some(Box::new(move |value| {
204                    value.to_lowercase().starts_with(&pattern)
205                }))
206            } else {
207                let pattern = pattern.to_string();
208
209                Some(Box::new(move |value| value.starts_with(&pattern)))
210            }
211        }
212
213        ValueMatchMode::Regex => {
214            let regex = RegexBuilder::new(pattern)
215                .case_insensitive(case_insensitive)
216                .build()
217                .ok()?;
218
219            Some(Box::new(move |value| regex.is_match(value)))
220        }
221
222        ValueMatchMode::Fuzzy => {
223            let pattern = pattern.to_string();
224
225            let matcher = if case_insensitive {
226                SkimMatcherV2::default().ignore_case()
227            } else {
228                SkimMatcherV2::default().respect_case()
229            };
230
231            Some(Box::new(move |value| {
232                matcher.fuzzy_match(value, &pattern).is_some()
233            }))
234        }
235    }
236}
237fn value_match_menu(ui: &mut egui::Ui, mode: &mut ValueMatchMode, case_insensitive: &mut bool) {
238    ui.checkbox(case_insensitive, "Case insensitive");
239
240    ui.separator();
241
242    for candidate in [
243        ValueMatchMode::Fuzzy,
244        ValueMatchMode::Regex,
245        ValueMatchMode::StartsWith,
246        ValueMatchMode::Contains,
247    ] {
248        ui.radio_value(mode, candidate, candidate.label());
249    }
250}
251fn matching_text_edit(
252    ui: &mut egui::Ui,
253    value: &mut String,
254    hint: &str,
255    width: f32,
256    mode: &mut ValueMatchMode,
257    case_insensitive: &mut bool,
258) {
259    let text_response = ui
260        .add_sized(
261            [width, 20.0],
262            egui::TextEdit::singleline(value).hint_text(hint),
263        )
264        .on_hover_text("Matching options");
265
266    text_response.context_menu(|ui| {
267        value_match_menu(ui, mode, case_insensitive);
268    });
269    ui.menu_button(egui::RichText::new(icons::FILTER_FILL).size(12.0), |ui| {
270        value_match_menu(ui, mode, case_insensitive);
271    });
272
273    if ui
274        .add_enabled(
275            !value.is_empty(),
276            Button::new(icons::CLOSE_FILL).frame(false),
277        )
278        .on_hover_text("Clear")
279        .clicked()
280    {
281        value.clear();
282    }
283}
284fn change_range(
285    mode: ChangeModes,
286    cursor: &num::BigUint,
287    markers: &std::collections::HashMap<u8, num::BigInt>,
288) -> Option<(num::BigUint, num::BigUint)> {
289    let ChangeModes::ChangedBtwCursorAndMarker(marker_index) = mode else {
290        return None;
291    };
292
293    let marker_time = markers.get(&marker_index)?.to_biguint()?;
294
295    if cursor <= &marker_time {
296        Some((cursor.clone(), marker_time))
297    } else {
298        Some((marker_time, cursor.clone()))
299    }
300}
301fn marker_combo(ui: &mut egui::Ui, id: &'static str, selected: &mut ChangeModes) {
302    let selected_marker = match selected {
303        ChangeModes::ChangedBtwCursorAndMarker(index) => *index,
304        _ => 1,
305    };
306
307    egui::ComboBox::from_id_salt(id)
308        .selected_text(format!("Marker {selected_marker}"))
309        .show_ui(ui, |ui| {
310            for marker_index in 0..=254 {
311                ui.selectable_value(
312                    selected,
313                    ChangeModes::ChangedBtwCursorAndMarker(marker_index),
314                    format!("Marker {marker_index}"),
315                );
316            }
317        });
318}
319fn change_mode_menu(ui: &mut egui::Ui, marker_id: &'static str, selected: &mut ChangeModes) {
320    ui.radio_value(selected, ChangeModes::AllValues, "None");
321
322    ui.radio_value(selected, ChangeModes::ChangedAtCursor, "Cursor");
323
324    let marker_index = match *selected {
325        ChangeModes::ChangedBtwCursorAndMarker(index) => index,
326        _ => 1,
327    };
328
329    ui.radio_value(
330        selected,
331        ChangeModes::ChangedBtwCursorAndMarker(marker_index),
332        "Cursor and marker",
333    );
334
335    if matches!(selected, ChangeModes::ChangedBtwCursorAndMarker(_)) {
336        marker_combo(ui, marker_id, selected);
337    }
338}
339fn left_aligned_header(ui: &mut egui::Ui, text: &str) {
340    ui.with_layout(egui::Layout::left_to_right(egui::Align::Min), |ui| {
341        ui.label(text);
342    });
343}
344fn highlight_dropdown(
345    ui: &mut egui::Ui,
346    selected: &mut ChangeModes,
347    highlight_value: &mut String,
348    match_mode: &mut ValueMatchMode,
349    case_insensitive: &mut bool,
350) {
351    ui.vertical(|ui| {
352        CollapsingState::load_with_default_open(
353            ui.ctx(),
354            ui.make_persistent_id("memory_viewer_highlight"),
355            false,
356        )
357        .show_header(ui, |ui| {
358            left_aligned_header(ui, "Highlight");
359        })
360        .body(|ui| {
361            ui.allocate_ui_with_layout(
362                egui::vec2(260.0, 0.0),
363                egui::Layout::top_down(egui::Align::LEFT),
364                |ui| {
365                    change_mode_menu(ui, "memory_viewer_highlight_marker", selected);
366
367                    ui.horizontal(|ui| {
368                        ui.label("Value:");
369
370                        matching_text_edit(
371                            ui,
372                            highlight_value,
373                            "Highlight",
374                            120.0,
375                            match_mode,
376                            case_insensitive,
377                        );
378                    });
379                },
380            );
381        });
382    });
383}
384fn filter_dropdown(
385    ui: &mut egui::Ui,
386    selected: &mut ChangeModes,
387    filter_value: &mut String,
388    match_mode: &mut ValueMatchMode,
389    case_insensitive: &mut bool,
390) {
391    ui.vertical(|ui| {
392        CollapsingState::load_with_default_open(
393            ui.ctx(),
394            ui.make_persistent_id("memory_viewer_filter"),
395            false,
396        )
397        .show_header(ui, |ui| {
398            left_aligned_header(ui, "Filter");
399        })
400        .body(|ui| {
401            ui.allocate_ui_with_layout(
402                egui::vec2(260.0, 0.0),
403                egui::Layout::top_down(egui::Align::LEFT),
404                |ui| {
405                    change_mode_menu(ui, "memory_viewer_filter_marker", selected);
406
407                    ui.horizontal(|ui| {
408                        ui.label("Value:");
409
410                        matching_text_edit(
411                            ui,
412                            filter_value,
413                            "Filter",
414                            120.0,
415                            match_mode,
416                            case_insensitive,
417                        );
418                    });
419                },
420            );
421        });
422    });
423}
424impl SystemState {
425    pub fn draw_memory_viewer_window(&mut self, ctx: &egui::Context, _msgs: &mut Vec<Message>) {
426        if !self.memory_viewer.open {
427            return;
428        }
429
430        let mut open = self.memory_viewer.open;
431        let translator_name = self.memory_viewer.value_format.clone();
432        let translator = self.translators.get_translator(&translator_name);
433        egui::Window::new("Memory Viewer")
434            .open(&mut open)
435            .resizable(true)
436            .default_size([520.0, 500.0])
437            .show(ctx, |ui| {
438                ui.heading("Memory Viewer");
439
440                let Some(scope) = self.memory_viewer.scope.clone() else {
441                    ui.label("No variable selected");
442                    return;
443                };
444
445                let display_name = self
446                    .memory_viewer
447                    .name
448                    .clone()
449                    .unwrap_or_else(|| scope.name());
450
451                ui.label(format!("Variable: {display_name}"));
452
453                let Some(waves) = &self.user.waves else {
454                    ui.label("No waveform loaded");
455                    return;
456                };
457
458                let Some(cursor) = waves.cursor.as_ref().and_then(num::BigInt::to_biguint) else {
459                    ui.label("Place the cursor to inspect values.");
460                    return;
461                };
462
463                ui.label(format!("Time: {cursor}"));
464
465                let Some(wave_container) = waves.inner.as_waves() else {
466                    ui.label("No wave container available");
467                    return;
468                };
469
470                let mut rows = Vec::new();
471                let cache_key = MemoryViewerCacheKey {
472                    scope: scope.clone(),
473                    cursor: cursor.clone(),
474                    value_format: translator_name.clone(),
475                    filter_mode: self.memory_viewer.filter_mode,
476                    highlight_mode: self.memory_viewer.highlight_mode,
477                };
478                let cache_hit = self
479                    .memory_viewer_cache
480                    .as_ref()
481                    .filter(|cache| cache.key == cache_key)
482                    .map(|cache| Rc::clone(&cache.rows));
483
484                if let Some(cached_rows) = cache_hit {
485                    rows = cached_rows.as_ref().clone();
486                }
487
488                let mut variables = wave_container.variables_in_scope(&scope);
489                variables.sort_by_key(|v| v.index.unwrap_or(i64::MAX));
490                let filter_change_range =
491                    change_range(self.memory_viewer.filter_mode, &cursor, &waves.markers);
492
493                let highlight_change_range =
494                    change_range(self.memory_viewer.highlight_mode, &cursor, &waves.markers);
495                if rows.is_empty() {
496                    for var_ref in &variables {
497                        let Some(index) = var_ref
498                            .index
499                            .or_else(|| parse_index_from_name(&var_ref.name))
500                        else {
501                            continue;
502                        };
503
504                        let Some((change_time, raw_value)) = wave_container
505                            .query_variable(var_ref, &cursor)
506                            .ok()
507                            .flatten()
508                            .and_then(|query_result| query_result.current)
509                        else {
510                            continue;
511                        };
512
513                        let changed_at_cursor = change_time == cursor;
514                        let changed_in_range =
515                            |range: Option<&(num::BigUint, num::BigUint)>| -> bool {
516                                range
517                                    .and_then(|(start_time, end_time)| {
518                                        wave_container
519                                            .query_variable(var_ref, start_time)
520                                            .ok()
521                                            .flatten()
522                                            .and_then(|query_result| query_result.next)
523                                            .map(|next_change| {
524                                                next_change > *start_time && next_change < *end_time
525                                            })
526                                    })
527                                    .unwrap_or(false)
528                            };
529
530                        let changed_for_filter = changed_in_range(filter_change_range.as_ref());
531
532                        let changed_for_highlight = if filter_change_range == highlight_change_range
533                        {
534                            changed_for_filter
535                        } else {
536                            changed_in_range(highlight_change_range.as_ref())
537                        };
538                        let display_value = wave_container
539                            .variable_meta(var_ref)
540                            .ok()
541                            .and_then(|meta| {
542                                translator
543                                    .translate(&meta, &raw_value)
544                                    .ok()
545                                    .and_then(|result| {
546                                        result
547                                            .format_flat(
548                                                &Some(translator_name.clone()),
549                                                &[],
550                                                &self.translators,
551                                            )
552                                            .into_iter()
553                                            .next()
554                                            .and_then(|formatted| {
555                                                formatted
556                                                    .value
557                                                    .map(|value| (value.value, value.kind))
558                                            })
559                                    })
560                            })
561                            .unwrap_or_else(|| (raw_value.to_string(), ValueKind::Normal));
562
563                        let (value, kind) = display_value;
564
565                        rows.push(MemoryRow {
566                            index,
567                            value,
568                            kind,
569                            changed_at_cursor,
570                            changed_for_filter,
571                            changed_for_highlight,
572                        });
573                    }
574
575                    self.memory_viewer_cache = Some(MemoryViewerCache {
576                        key: cache_key,
577                        rows: Rc::new(rows.clone()),
578                    });
579                }
580                ui.label(format!("Structured entries: {}", rows.len()));
581
582                ui.separator();
583
584                ui.horizontal_top(|ui| {
585                    if self.memory_viewer.value_column_count == 1 {
586                        filter_dropdown(
587                            ui,
588                            &mut self.memory_viewer.filter_mode,
589                            &mut self.memory_viewer.filter_value,
590                            &mut self.memory_viewer.filter_match_mode,
591                            &mut self.memory_viewer.filter_case_insensitive,
592                        );
593                    }
594                    highlight_dropdown(
595                        ui,
596                        &mut self.memory_viewer.highlight_mode,
597                        &mut self.memory_viewer.highlight_value,
598                        &mut self.memory_viewer.highlight_match_mode,
599                        &mut self.memory_viewer.highlight_case_insensitive,
600                    );
601
602                    ui.checkbox(&mut self.memory_viewer.color_values, "Color values");
603                });
604                let search_matcher = build_value_matcher(
605                    &self.memory_viewer.search_value,
606                    self.memory_viewer.search_match_mode,
607                    self.memory_viewer.search_case_insensitive,
608                );
609                let highlight_matcher = build_value_matcher(
610                    &self.memory_viewer.highlight_value,
611                    self.memory_viewer.highlight_match_mode,
612                    self.memory_viewer.highlight_case_insensitive,
613                );
614                let filter_matcher = build_value_matcher(
615                    &self.memory_viewer.filter_value,
616                    self.memory_viewer.filter_match_mode,
617                    self.memory_viewer.filter_case_insensitive,
618                );
619                let visible_rows: Vec<_> = rows
620                    .iter()
621                    .filter(|row| {
622                        let matches_change_filter = match self.memory_viewer.filter_mode {
623                            ChangeModes::AllValues => true,
624                            ChangeModes::ChangedAtCursor => row.changed_at_cursor,
625                            ChangeModes::ChangedBtwCursorAndMarker(_) => row.changed_for_filter,
626                        };
627
628                        let matches_text_filter = filter_matcher
629                            .as_ref()
630                            .is_none_or(|matcher| matcher(&row.value));
631
632                        matches_change_filter && matches_text_filter
633                    })
634                    .collect();
635                let matching_positions: Vec<usize> = search_matcher
636                    .as_ref()
637                    .map(|matcher| {
638                        visible_rows
639                            .iter()
640                            .enumerate()
641                            .filter_map(|(position, row)| matcher(&row.value).then_some(position))
642                            .collect()
643                    })
644                    .unwrap_or_default();
645                let current_match = self
646                    .memory_viewer
647                    .selected_value_position
648                    .and_then(|selected| {
649                        matching_positions
650                            .iter()
651                            .position(|&position| position == selected)
652                    })
653                    .map(|index| index + 1)
654                    .unwrap_or(0);
655                let total_matches = matching_positions.len();
656                let mut find_previous_requested = false;
657                let mut find_next_requested = false;
658                ui.horizontal(|ui| {
659                    ui.label("Find:");
660                    matching_text_edit(
661                        ui,
662                        &mut self.memory_viewer.search_value,
663                        "Find",
664                        160.0,
665                        &mut self.memory_viewer.search_match_mode,
666                        &mut self.memory_viewer.search_case_insensitive,
667                    );
668
669                    if ui
670                        .add(egui::Button::new(icons::ARROW_UP_LINE).frame(false))
671                        .on_hover_text("Previous match")
672                        .clicked()
673                    {
674                        find_previous_requested = true;
675                    }
676
677                    if ui
678                        .add(egui::Button::new(icons::ARROW_DOWN_LINE).frame(false))
679                        .on_hover_text("Next match")
680                        .clicked()
681                    {
682                        find_next_requested = true;
683                    }
684                    if !self.memory_viewer.search_value.is_empty() {
685                        if total_matches == 0 {
686                            ui.label("No results");
687                        } else {
688                            ui.label(format!("{current_match} of {total_matches}"));
689                        }
690                    }
691                });
692                ui.separator();
693                ui.horizontal(|ui| {
694                    ui.label("Index format:");
695                    egui::ComboBox::from_id_salt("memory_viewer_index_format")
696                        .selected_text(self.memory_viewer.index_format.label())
697                        .show_ui(ui, |ui| {
698                            ui.selectable_value(
699                                &mut self.memory_viewer.index_format,
700                                MemoryViewerFormat::Decimal,
701                                "Decimal",
702                            );
703                            ui.selectable_value(
704                                &mut self.memory_viewer.index_format,
705                                MemoryViewerFormat::Hexadecimal,
706                                "Hexadecimal",
707                            );
708                            ui.selectable_value(
709                                &mut self.memory_viewer.index_format,
710                                MemoryViewerFormat::Binary,
711                                "Binary",
712                            );
713                        });
714
715                    ui.label("Value format:");
716
717                    let (mut preferred_translators, mut bad_translators): (Vec<_>, Vec<_>) =
718                        variables
719                            .first()
720                            .and_then(|first_var_ref| {
721                                wave_container.variable_meta(first_var_ref).ok()
722                            })
723                            .map_or_else(
724                                || (vec![], self.translators.all_translator_names()),
725                                |meta| {
726                                    self.translators
727                                        .all_translator_names()
728                                        .into_iter()
729                                        .partition(|translator_name| {
730                                            let translator =
731                                                self.translators.get_translator(translator_name);
732
733                                            match translator.translates(&meta) {
734                                                Ok(TranslationPreference::Yes) => true,
735                                                Ok(TranslationPreference::Prefer) => true,
736                                                Ok(TranslationPreference::No) => false,
737                                                Err(_) => false,
738                                            }
739                                        })
740                                },
741                            );
742
743                    preferred_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
744                    bad_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
745
746                    egui::ComboBox::from_id_salt("memory_viewer_value_format")
747                        .selected_text(self.memory_viewer.value_format.clone())
748                        .show_ui(ui, |ui| {
749                            for name in preferred_translators {
750                                ui.selectable_value(
751                                    &mut self.memory_viewer.value_format,
752                                    name.to_string(),
753                                    name,
754                                );
755                            }
756
757                            if !bad_translators.is_empty() {
758                                ui.separator();
759                                ui.label("Not recommended");
760
761                                for name in bad_translators {
762                                    ui.selectable_value(
763                                        &mut self.memory_viewer.value_format,
764                                        name.to_string(),
765                                        name,
766                                    );
767                                }
768                            }
769                        });
770                });
771
772                let mut jump_requested = false;
773                let mut shrink_columns_requested = false;
774
775                ui.horizontal(|ui| {
776                    ui.label("Jump to index:");
777                    let response = ui.add_sized(
778                        [60.0, 20.0],
779                        egui::TextEdit::singleline(&mut self.memory_viewer.jump_to_index),
780                    );
781
782                    if ui.button("Jump").clicked()
783                        || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
784                    {
785                        jump_requested = true;
786                    }
787                    ui.label("Columns:");
788
789                    ui.add(
790                        DragValue::new(&mut self.memory_viewer.value_column_count).range(1..=32),
791                    );
792                    if ui
793                        .add(Button::new(icons::ASPECT_RATIO_FILL).frame(false))
794                        .on_hover_text("Auto-size")
795                        .clicked()
796                    {
797                        shrink_columns_requested = true;
798                    }
799                    if self.memory_viewer.value_column_count > 1 {
800                        self.memory_viewer.filter_mode = ChangeModes::AllValues;
801                        self.memory_viewer.filter_value.clear();
802                    }
803
804                    ui.separator();
805                    ui.separator();
806                });
807
808                ui.separator();
809
810                let max_index = rows.iter().map(|row| row.index).max().unwrap_or(0);
811                let text_height = egui::TextStyle::Body
812                    .resolve(ui.style())
813                    .size
814                    .max(ui.spacing().interact_size.y);
815
816                // let available_height = ui.available_height().max(250.0);
817                let value_column_count = self
818                    .memory_viewer
819                    .value_column_count
820                    .clamp(1, visible_rows.len().max(1));
821                self.memory_viewer.value_column_count = value_column_count;
822                let table_rows = visible_rows.len().div_ceil(value_column_count);
823
824                let mut navigation_target = None;
825                // jump to the closest memory index
826                if jump_requested
827                    && let Some(target_index) =
828                        parse_jump_to_index(&self.memory_viewer.jump_to_index)
829                    && let Some(value_position) = closest_row_index(&visible_rows, target_index)
830                {
831                    navigation_target = Some(value_position);
832                }
833                // Move to the next matching value, wrapping to the first match.
834                if find_next_requested {
835                    navigation_target = self
836                        .memory_viewer
837                        .selected_value_position
838                        .and_then(|selected_position| {
839                            matching_positions
840                                .iter()
841                                .copied()
842                                .find(|position| *position > selected_position)
843                        })
844                        .or_else(|| matching_positions.first().copied());
845                }
846                // Move to the previous matching value, wrapping to the last match.
847                if find_previous_requested {
848                    navigation_target = self
849                        .memory_viewer
850                        .selected_value_position
851                        .and_then(|selected_position| {
852                            matching_positions
853                                .iter()
854                                .rev()
855                                .copied()
856                                .find(|position| *position < selected_position)
857                        })
858                        .or_else(|| matching_positions.last().copied());
859                }
860                if let Some(value_position) = navigation_target {
861                    self.memory_viewer.selected_value_position = Some(value_position);
862
863                    self.memory_viewer.scroll_to_row = Some(value_position / value_column_count);
864                }
865
866                let table_width = ui.available_width();
867                let table_height = ui.available_height();
868
869                let horizontal_navigation_target = navigation_target;
870                ui.allocate_ui_with_layout(
871                    egui::vec2(table_width, table_height),
872                    egui::Layout::top_down(egui::Align::LEFT),
873                    |ui| {
874                        egui::ScrollArea::horizontal()
875                            .auto_shrink([false, true])
876                            .show(ui, |ui| {
877                                let mut table = TableBuilder::new(ui)
878                                    .striped(true)
879                                    .resizable(true)
880                                    .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
881                                    .column(Column::auto());
882
883                                for _ in 0..value_column_count {
884                                    table = table.column(Column::auto());
885                                }
886                                if shrink_columns_requested {
887                                    table.reset();
888                                }
889                                if let Some(row_index) = self.memory_viewer.scroll_to_row.take() {
890                                    table =
891                                        table.scroll_to_row(row_index, Some(egui::Align::Center));
892                                }
893                                table
894                                    .header(24.0, |mut header| {
895                                        header.col(|ui| {
896                                            ui.monospace("Index");
897                                        });
898
899                                        for column_index in 0..value_column_count {
900                                            header.col(|ui| {
901                                                if column_index == 0 {
902                                                    ui.monospace("Value");
903                                                } else {
904                                                    ui.monospace(format!("+{column_index}"));
905                                                }
906                                            });
907                                        }
908                                    })
909                                    .body(|body| {
910                                        body.rows(text_height, table_rows, |mut row| {
911                                            let table_row_index = row.index();
912                                            let row_start = table_row_index * value_column_count;
913
914                                            let Some(first_row_data) = visible_rows.get(row_start)
915                                            else {
916                                                return;
917                                            };
918
919                                            row.col(|ui| {
920                                                ui.monospace(format_index(
921                                                    first_row_data.index,
922                                                    self.memory_viewer.index_format,
923                                                    max_index,
924                                                ));
925                                            });
926
927                                            for column_index in 0..value_column_count {
928                                                row.col(|ui| {
929                                                    let value_index = row_start + column_index;
930
931                                                    let Some(row_data) =
932                                                        visible_rows.get(value_index)
933                                                    else {
934                                                        return;
935                                                    };
936
937                                                    let is_selected =
938                                                        self.memory_viewer.selected_value_position
939                                                            == Some(value_index);
940                                                    let highlighted_by_change = match self
941                                                        .memory_viewer
942                                                        .highlight_mode
943                                                    {
944                                                        ChangeModes::AllValues => false,
945
946                                                        ChangeModes::ChangedAtCursor => {
947                                                            row_data.changed_at_cursor
948                                                        }
949
950                                                        ChangeModes::ChangedBtwCursorAndMarker(
951                                                            _,
952                                                        ) => row_data.changed_for_highlight,
953                                                    };
954
955                                                    let highlighted_by_value =
956                                                        highlight_matcher.as_ref().is_some_and(
957                                                            |matcher| matcher(&row_data.value),
958                                                        );
959
960                                                    let is_highlighted = highlighted_by_change
961                                                        || highlighted_by_value;
962                                                    let frame = if is_selected {
963                                                        egui::Frame::NONE
964                                                            .fill(
965                                                                self.user
966                                                                    .config
967                                                                    .theme
968                                                                    .accent_info
969                                                                    .background,
970                                                            )
971                                                            .inner_margin(egui::Margin::symmetric(
972                                                                4, 1,
973                                                            ))
974                                                    } else if is_highlighted {
975                                                        egui::Frame::NONE
976                                                            .fill(
977                                                                self.user
978                                                                    .config
979                                                                    .theme
980                                                                    .selected_elements_colors
981                                                                    .background,
982                                                            )
983                                                            .inner_margin(egui::Margin::symmetric(
984                                                                4, 1,
985                                                            ))
986                                                    } else {
987                                                        egui::Frame::NONE.inner_margin(
988                                                            egui::Margin::symmetric(4, 1),
989                                                        )
990                                                    };
991
992                                                    let frame_response = frame.show(ui, |ui| {
993                                                        if is_selected {
994                                                            ui.visuals_mut().override_text_color =
995                                                                Some(
996                                                                    self.user
997                                                                        .config
998                                                                        .theme
999                                                                        .accent_info
1000                                                                        .foreground,
1001                                                                );
1002                                                        } else if is_highlighted {
1003                                                            ui.visuals_mut().override_text_color =
1004                                                                Some(
1005                                                                    self.user
1006                                                                        .config
1007                                                                        .theme
1008                                                                        .selected_elements_colors
1009                                                                        .foreground,
1010                                                                );
1011                                                        }
1012
1013                                                        if self.memory_viewer.color_values {
1014                                                            let color = row_data.kind.color(
1015                                                                self.user
1016                                                                    .config
1017                                                                    .theme
1018                                                                    .variable_default,
1019                                                                &self.user.config.theme,
1020                                                            );
1021
1022                                                            ui.horizontal(|ui| {
1023                                                                ui.colored_label(color, "■");
1024                                                                ui.monospace(&row_data.value);
1025                                                            });
1026                                                        } else {
1027                                                            ui.monospace(&row_data.value);
1028                                                        }
1029                                                    });
1030                                                    if horizontal_navigation_target
1031                                                        == Some(value_index)
1032                                                    {
1033                                                        ui.scroll_to_rect(
1034                                                            frame_response.response.rect,
1035                                                            Some(egui::Align::Center),
1036                                                        );
1037                                                    }
1038                                                });
1039                                            }
1040                                        });
1041                                    });
1042                            });
1043                    },
1044                );
1045            });
1046
1047        self.memory_viewer.open = open;
1048    }
1049}