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 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 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 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 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}