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_or(0, |index| index + 1);
654 let total_matches = matching_positions.len();
655 let mut find_previous_requested = false;
656 let mut find_next_requested = false;
657 ui.horizontal(|ui| {
658 ui.label("Find:");
659 matching_text_edit(
660 ui,
661 &mut self.memory_viewer.search_value,
662 "Find",
663 160.0,
664 &mut self.memory_viewer.search_match_mode,
665 &mut self.memory_viewer.search_case_insensitive,
666 );
667
668 if ui
669 .add(egui::Button::new(icons::ARROW_UP_LINE).frame(false))
670 .on_hover_text("Previous match")
671 .clicked()
672 {
673 find_previous_requested = true;
674 }
675
676 if ui
677 .add(egui::Button::new(icons::ARROW_DOWN_LINE).frame(false))
678 .on_hover_text("Next match")
679 .clicked()
680 {
681 find_next_requested = true;
682 }
683 if !self.memory_viewer.search_value.is_empty() {
684 if total_matches == 0 {
685 ui.label("No results");
686 } else {
687 ui.label(format!("{current_match} of {total_matches}"));
688 }
689 }
690 });
691 ui.separator();
692 ui.horizontal(|ui| {
693 ui.label("Index format:");
694 egui::ComboBox::from_id_salt("memory_viewer_index_format")
695 .selected_text(self.memory_viewer.index_format.label())
696 .show_ui(ui, |ui| {
697 ui.selectable_value(
698 &mut self.memory_viewer.index_format,
699 MemoryViewerFormat::Decimal,
700 "Decimal",
701 );
702 ui.selectable_value(
703 &mut self.memory_viewer.index_format,
704 MemoryViewerFormat::Hexadecimal,
705 "Hexadecimal",
706 );
707 ui.selectable_value(
708 &mut self.memory_viewer.index_format,
709 MemoryViewerFormat::Binary,
710 "Binary",
711 );
712 });
713
714 ui.label("Value format:");
715
716 let (mut preferred_translators, mut bad_translators): (Vec<_>, Vec<_>) =
717 variables
718 .first()
719 .and_then(|first_var_ref| {
720 wave_container.variable_meta(first_var_ref).ok()
721 })
722 .map_or_else(
723 || (vec![], self.translators.all_translator_names()),
724 |meta| {
725 self.translators
726 .all_translator_names()
727 .into_iter()
728 .partition(|translator_name| {
729 let translator =
730 self.translators.get_translator(translator_name);
731
732 match translator.translates(&meta) {
733 Ok(TranslationPreference::Yes) => true,
734 Ok(TranslationPreference::Prefer) => true,
735 Ok(TranslationPreference::No) => false,
736 Err(_) => false,
737 }
738 })
739 },
740 );
741
742 preferred_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
743 bad_translators.sort_by(|a, b| numeric_sort::cmp(a, b));
744
745 egui::ComboBox::from_id_salt("memory_viewer_value_format")
746 .selected_text(self.memory_viewer.value_format.clone())
747 .show_ui(ui, |ui| {
748 for name in preferred_translators {
749 ui.selectable_value(
750 &mut self.memory_viewer.value_format,
751 name.to_string(),
752 name,
753 );
754 }
755
756 if !bad_translators.is_empty() {
757 ui.separator();
758 ui.label("Not recommended");
759
760 for name in bad_translators {
761 ui.selectable_value(
762 &mut self.memory_viewer.value_format,
763 name.to_string(),
764 name,
765 );
766 }
767 }
768 });
769 });
770
771 let mut jump_requested = false;
772 let mut shrink_columns_requested = false;
773
774 ui.horizontal(|ui| {
775 ui.label("Jump to index:");
776 let response = ui.add_sized(
777 [60.0, 20.0],
778 egui::TextEdit::singleline(&mut self.memory_viewer.jump_to_index),
779 );
780
781 if ui.button("Jump").clicked()
782 || (response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)))
783 {
784 jump_requested = true;
785 }
786 ui.label("Columns:");
787
788 ui.add(
789 DragValue::new(&mut self.memory_viewer.value_column_count).range(1..=32),
790 );
791 if ui
792 .add(Button::new(icons::ASPECT_RATIO_FILL).frame(false))
793 .on_hover_text("Auto-size")
794 .clicked()
795 {
796 shrink_columns_requested = true;
797 }
798 if self.memory_viewer.value_column_count > 1 {
799 self.memory_viewer.filter_mode = ChangeModes::AllValues;
800 self.memory_viewer.filter_value.clear();
801 }
802
803 ui.separator();
804 ui.separator();
805 });
806
807 ui.separator();
808
809 let max_index = rows.iter().map(|row| row.index).max().unwrap_or(0);
810 let text_height = egui::TextStyle::Body
811 .resolve(ui.style())
812 .size
813 .max(ui.spacing().interact_size.y);
814
815 let value_column_count = self
817 .memory_viewer
818 .value_column_count
819 .clamp(1, visible_rows.len().max(1));
820 self.memory_viewer.value_column_count = value_column_count;
821 let table_rows = visible_rows.len().div_ceil(value_column_count);
822
823 let mut navigation_target = None;
824 if jump_requested
826 && let Some(target_index) =
827 parse_jump_to_index(&self.memory_viewer.jump_to_index)
828 && let Some(value_position) = closest_row_index(&visible_rows, target_index)
829 {
830 navigation_target = Some(value_position);
831 }
832 if find_next_requested {
834 navigation_target = self
835 .memory_viewer
836 .selected_value_position
837 .and_then(|selected_position| {
838 matching_positions
839 .iter()
840 .copied()
841 .find(|position| *position > selected_position)
842 })
843 .or_else(|| matching_positions.first().copied());
844 }
845 if find_previous_requested {
847 navigation_target = self
848 .memory_viewer
849 .selected_value_position
850 .and_then(|selected_position| {
851 matching_positions
852 .iter()
853 .rev()
854 .copied()
855 .find(|position| *position < selected_position)
856 })
857 .or_else(|| matching_positions.last().copied());
858 }
859 if let Some(value_position) = navigation_target {
860 self.memory_viewer.selected_value_position = Some(value_position);
861
862 self.memory_viewer.scroll_to_row = Some(value_position / value_column_count);
863 }
864
865 let table_width = ui.available_width();
866 let table_height = ui.available_height();
867
868 let horizontal_navigation_target = navigation_target;
869 ui.allocate_ui_with_layout(
870 egui::vec2(table_width, table_height),
871 egui::Layout::top_down(egui::Align::LEFT),
872 |ui| {
873 egui::ScrollArea::horizontal()
874 .auto_shrink([false, true])
875 .show(ui, |ui| {
876 let mut table = TableBuilder::new(ui)
877 .striped(true)
878 .resizable(true)
879 .cell_layout(egui::Layout::left_to_right(egui::Align::Center))
880 .column(Column::auto());
881
882 for _ in 0..value_column_count {
883 table = table.column(Column::auto());
884 }
885 if shrink_columns_requested {
886 table.reset();
887 }
888 if let Some(row_index) = self.memory_viewer.scroll_to_row.take() {
889 table =
890 table.scroll_to_row(row_index, Some(egui::Align::Center));
891 }
892 table
893 .header(24.0, |mut header| {
894 header.col(|ui| {
895 ui.monospace("Index");
896 });
897
898 for column_index in 0..value_column_count {
899 header.col(|ui| {
900 if column_index == 0 {
901 ui.monospace("Value");
902 } else {
903 ui.monospace(format!("+{column_index}"));
904 }
905 });
906 }
907 })
908 .body(|body| {
909 body.rows(text_height, table_rows, |mut row| {
910 let table_row_index = row.index();
911 let row_start = table_row_index * value_column_count;
912
913 let Some(first_row_data) = visible_rows.get(row_start)
914 else {
915 return;
916 };
917
918 row.col(|ui| {
919 ui.monospace(format_index(
920 first_row_data.index,
921 self.memory_viewer.index_format,
922 max_index,
923 ));
924 });
925
926 for column_index in 0..value_column_count {
927 row.col(|ui| {
928 let value_index = row_start + column_index;
929
930 let Some(row_data) =
931 visible_rows.get(value_index)
932 else {
933 return;
934 };
935
936 let is_selected =
937 self.memory_viewer.selected_value_position
938 == Some(value_index);
939 let highlighted_by_change = match self
940 .memory_viewer
941 .highlight_mode
942 {
943 ChangeModes::AllValues => false,
944
945 ChangeModes::ChangedAtCursor => {
946 row_data.changed_at_cursor
947 }
948
949 ChangeModes::ChangedBtwCursorAndMarker(
950 _,
951 ) => row_data.changed_for_highlight,
952 };
953
954 let highlighted_by_value =
955 highlight_matcher.as_ref().is_some_and(
956 |matcher| matcher(&row_data.value),
957 );
958
959 let is_highlighted = highlighted_by_change
960 || highlighted_by_value;
961 let frame = if is_selected {
962 egui::Frame::NONE
963 .fill(
964 self.user
965 .config
966 .theme
967 .accent_info
968 .background,
969 )
970 .inner_margin(egui::Margin::symmetric(
971 4, 1,
972 ))
973 } else if is_highlighted {
974 egui::Frame::NONE
975 .fill(
976 self.user
977 .config
978 .theme
979 .selected_elements_colors
980 .background,
981 )
982 .inner_margin(egui::Margin::symmetric(
983 4, 1,
984 ))
985 } else {
986 egui::Frame::NONE.inner_margin(
987 egui::Margin::symmetric(4, 1),
988 )
989 };
990
991 let frame_response = frame.show(ui, |ui| {
992 if is_selected {
993 ui.visuals_mut().override_text_color =
994 Some(
995 self.user
996 .config
997 .theme
998 .accent_info
999 .foreground,
1000 );
1001 } else if is_highlighted {
1002 ui.visuals_mut().override_text_color =
1003 Some(
1004 self.user
1005 .config
1006 .theme
1007 .selected_elements_colors
1008 .foreground,
1009 );
1010 }
1011
1012 if self.memory_viewer.color_values {
1013 let color = row_data.kind.color(
1014 self.user
1015 .config
1016 .theme
1017 .variable_default,
1018 &self.user.config.theme,
1019 );
1020
1021 ui.horizontal(|ui| {
1022 ui.colored_label(color, "■");
1023 ui.monospace(&row_data.value);
1024 });
1025 } else {
1026 ui.monospace(&row_data.value);
1027 }
1028 });
1029 if horizontal_navigation_target
1030 == Some(value_index)
1031 {
1032 ui.scroll_to_rect(
1033 frame_response.response.rect,
1034 Some(egui::Align::Center),
1035 );
1036 }
1037 });
1038 }
1039 });
1040 });
1041 });
1042 },
1043 );
1044 });
1045
1046 self.memory_viewer.open = open;
1047 }
1048}