From d938988d43054e038717cfc14ca05566c54fa38f Mon Sep 17 00:00:00 2001 From: Luke Street Date: Sun, 2 Feb 2025 17:32:08 -0700 Subject: [PATCH] Diff view refactor --- Cargo.toml | 2 +- objdiff-gui/src/app.rs | 17 +- objdiff-gui/src/views/data_diff.rs | 191 +------ objdiff-gui/src/views/diff.rs | 741 +++++++++++++++++++++++++ objdiff-gui/src/views/extab_diff.rs | 193 +------ objdiff-gui/src/views/function_diff.rs | 488 +--------------- objdiff-gui/src/views/mod.rs | 1 + objdiff-gui/src/views/symbol_diff.rs | 299 ++-------- 8 files changed, 814 insertions(+), 1118 deletions(-) create mode 100644 objdiff-gui/src/views/diff.rs diff --git a/Cargo.toml b/Cargo.toml index 1b83a4f..c2f6e65 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,4 +18,4 @@ authors = ["Luke Street "] edition = "2021" license = "MIT OR Apache-2.0" repository = "https://github.com/encounter/objdiff" -rust-version = "1.81" +rust-version = "1.82" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index 9bfdb22..ddc395e 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -33,16 +33,14 @@ use crate::{ arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState, CONFIG_DISABLED_TEXT, }, - data_diff::data_diff_ui, debug::debug_window, demangle::{demangle_window, DemangleViewState}, - extab_diff::extab_diff_ui, + diff::diff_view_ui, frame_history::FrameHistory, - function_diff::function_diff_ui, graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, jobs::{jobs_menu_ui, jobs_window}, rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, - symbol_diff::{symbol_diff_ui, DiffViewAction, DiffViewNavigation, DiffViewState, View}, + symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState, View}, }, }; @@ -753,16 +751,7 @@ impl eframe::App for App { let mut action = None; egui::CentralPanel::default().show(ctx, |ui| { - let build_success = matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success); - action = if diff_state.current_view == View::FunctionDiff && build_success { - function_diff_ui(ui, diff_state, appearance) - } else if diff_state.current_view == View::DataDiff && build_success { - data_diff_ui(ui, diff_state, appearance) - } else if diff_state.current_view == View::ExtabDiff && build_success { - extab_diff_ui(ui, diff_state, appearance) - } else { - symbol_diff_ui(ui, diff_state, appearance) - }; + action = diff_view_ui(ui, diff_state, appearance); }); project_window(ctx, state, show_project_config, config_state, appearance); diff --git a/objdiff-gui/src/views/data_diff.rs b/objdiff-gui/src/views/data_diff.rs index a918fb9..6ee91ee 100644 --- a/objdiff-gui/src/views/data_diff.rs +++ b/objdiff-gui/src/views/data_diff.rs @@ -4,28 +4,15 @@ use std::{ mem::take, }; -use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget}; +use egui::{text::LayoutJob, Label, Sense, Widget}; use objdiff_core::{ - diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjDiff}, + diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff}, obj::ObjInfo, }; -use time::format_description; -use crate::{ - hotkeys, - views::{ - appearance::Appearance, - column_layout::{render_header, render_table}, - symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState}, - write_text, - }, -}; +use crate::views::{appearance::Appearance, write_text}; -const BYTES_PER_ROW: usize = 16; - -fn find_section(obj: &ObjInfo, section_name: &str) -> Option { - obj.sections.iter().position(|section| section.name == section_name) -} +pub(crate) const BYTES_PER_ROW: usize = 16; fn data_row_hover_ui( ui: &mut egui::Ui, @@ -122,7 +109,7 @@ fn get_color_for_diff_kind(diff_kind: ObjDataDiffKind, appearance: &Appearance) } } -fn data_row_ui( +pub(crate) fn data_row_ui( ui: &mut egui::Ui, obj: Option<&ObjInfo>, address: usize, @@ -212,7 +199,7 @@ fn data_row_ui( } } -fn split_diffs( +pub(crate) fn split_diffs( diffs: &[ObjDataDiff], reloc_diffs: &[ObjDataRelocDiff], ) -> Vec)>> { @@ -273,169 +260,3 @@ fn split_diffs( } split_diffs } - -#[derive(Clone, Copy)] -struct SectionDiffContext<'a> { - obj: &'a ObjInfo, - diff: &'a ObjDiff, - section_index: Option, -} - -impl<'a> SectionDiffContext<'a> { - pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option { - obj.map(|(obj, diff)| Self { - obj, - diff, - section_index: section_name.and_then(|section_name| find_section(obj, section_name)), - }) - } - - #[inline] - pub fn has_section(&self) -> bool { self.section_index.is_some() } -} - -fn data_table_ui( - ui: &mut egui::Ui, - available_width: f32, - left_ctx: Option>, - right_ctx: Option>, - config: &Appearance, -) -> Option<()> { - let left_obj = left_ctx.map(|ctx| ctx.obj); - let right_obj = right_ctx.map(|ctx| ctx.obj); - let left_section = left_ctx - .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); - let right_section = right_ctx - .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); - let total_bytes = left_section - .or(right_section)? - .1 - .data_diff - .iter() - .fold(0usize, |accum, item| accum + item.len); - if total_bytes == 0 { - return None; - } - let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; - - let left_diffs = - left_section.map(|(_, section)| split_diffs(§ion.data_diff, §ion.reloc_diff)); - let right_diffs = - right_section.map(|(_, section)| split_diffs(§ion.data_diff, §ion.reloc_diff)); - - hotkeys::check_scroll_hotkeys(ui, true); - - render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| { - let i = row.index(); - let address = i * BYTES_PER_ROW; - row.col(|ui| { - if column == 0 { - if let Some(left_diffs) = &left_diffs { - data_row_ui(ui, left_obj, address, &left_diffs[i], config); - } - } else if column == 1 { - if let Some(right_diffs) = &right_diffs { - data_row_ui(ui, right_obj, address, &right_diffs[i], config); - } - } - }); - }); - Some(()) -} - -#[must_use] -pub fn data_diff_ui( - ui: &mut egui::Ui, - state: &DiffViewState, - appearance: &Appearance, -) -> Option { - let mut ret = None; - let Some(result) = &state.build else { - return ret; - }; - - let section_name = - state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref()).or_else( - || state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref()), - ); - let left_ctx = SectionDiffContext::new(result.first_obj.as_ref(), section_name); - let right_ctx = SectionDiffContext::new(result.second_obj.as_ref(), section_name); - - // If both sides are missing a symbol, switch to symbol diff view - if !right_ctx.is_some_and(|ctx| ctx.has_section()) - && !left_ctx.is_some_and(|ctx| ctx.has_section()) - { - return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - - // Header - let available_width = ui.available_width(); - render_header(ui, available_width, 2, |ui, column| { - if column == 0 { - // Left column - if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) { - ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - - if let Some(section) = - left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) - { - ui.label( - RichText::new(section.name.clone()) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - } - } else if column == 1 { - // Right column - ui.horizontal(|ui| { - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - ret = Some(DiffViewAction::Build); - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), - ); - } - }); - }); - - if let Some(section) = - right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) - { - ui.label( - RichText::new(section.name.clone()) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - } - } - }); - - // Table - let id = - Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref())) - .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref())); - ui.push_id(id, |ui| { - data_table_ui(ui, available_width, left_ctx, right_ctx, appearance); - }); - ret -} diff --git a/objdiff-gui/src/views/diff.rs b/objdiff-gui/src/views/diff.rs new file mode 100644 index 0000000..12c9617 --- /dev/null +++ b/objdiff-gui/src/views/diff.rs @@ -0,0 +1,741 @@ +use egui::{Id, Layout, RichText, ScrollArea, TextEdit, Ui, Widget}; +use objdiff_core::{ + build::BuildStatus, + diff::{ObjDiff, ObjSectionDiff, ObjSymbolDiff}, + obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, SymbolRef}, +}; +use time::format_description; + +use crate::{ + hotkeys, + views::{ + appearance::Appearance, + column_layout::{render_header, render_strips, render_table}, + data_diff::{data_row_ui, split_diffs, BYTES_PER_ROW}, + extab_diff::extab_ui, + function_diff::{asm_col_ui, FunctionDiffContext}, + symbol_diff::{ + match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, + DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, View, + }, + }, +}; + +#[derive(Clone, Copy)] +enum SelectedSymbol { + Symbol(SymbolRef), + Section(usize), +} + +#[derive(Clone, Copy)] +struct DiffColumnContext<'a> { + status: &'a BuildStatus, + obj: Option<&'a (ObjInfo, ObjDiff)>, + section: Option<(&'a ObjSection, &'a ObjSectionDiff)>, + symbol: Option<(&'a ObjSymbol, &'a ObjSymbolDiff)>, +} + +impl<'a> DiffColumnContext<'a> { + pub fn new( + view: View, + status: &'a BuildStatus, + obj: Option<&'a (ObjInfo, ObjDiff)>, + selected_symbol: Option<&SymbolRefByName>, + ) -> Self { + let selected_symbol = match view { + View::SymbolDiff => None, + View::FunctionDiff | View::ExtabDiff => match (obj, selected_symbol) { + (Some(obj), Some(s)) => find_symbol(&obj.0, s).map(SelectedSymbol::Symbol), + _ => None, + }, + View::DataDiff => match (obj, selected_symbol) { + (Some(obj), Some(SymbolRefByName { section_name: Some(section_name), .. })) => { + find_section(&obj.0, section_name).map(SelectedSymbol::Section) + } + _ => None, + }, + }; + let (section, symbol) = match (obj, selected_symbol) { + (Some((obj, obj_diff)), Some(SelectedSymbol::Symbol(symbol_ref))) => { + let (section, symbol) = obj.section_symbol(symbol_ref); + ( + section.map(|s| (s, obj_diff.section_diff(symbol_ref.section_idx))), + Some((symbol, obj_diff.symbol_diff(symbol_ref))), + ) + } + (Some((obj, obj_diff)), Some(SelectedSymbol::Section(section_idx))) => { + (Some((&obj.sections[section_idx], obj_diff.section_diff(section_idx))), None) + } + _ => (None, None), + }; + Self { status, obj, section, symbol } + } + + #[inline] + pub fn has_symbol(&self) -> bool { self.section.is_some() || self.symbol.is_some() } + + #[inline] + pub fn id(&self) -> Option<&str> { + self.symbol + .map(|(symbol, _)| symbol.name.as_str()) + .or_else(|| self.section.map(|(section, _)| section.name.as_str())) + } +} + +#[must_use] +pub fn diff_view_ui( + ui: &mut Ui, + state: &DiffViewState, + appearance: &Appearance, +) -> Option { + let mut ret = None; + let Some(result) = &state.build else { + return ret; + }; + + let left_ctx = DiffColumnContext::new( + state.current_view, + &result.first_status, + result.first_obj.as_ref(), + state.symbol_state.left_symbol.as_ref(), + ); + let right_ctx = DiffColumnContext::new( + state.current_view, + &result.second_status, + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // Check if we need to perform any navigation + let current_navigation = DiffViewNavigation { + view: state.current_view, + left_symbol: state.symbol_state.left_symbol.clone(), + right_symbol: state.symbol_state.right_symbol.clone(), + }; + let mut navigation = current_navigation.clone(); + if let Some((_symbol, symbol_diff)) = left_ctx.symbol { + // If a matching symbol appears, select it + if !right_ctx.has_symbol() { + if let Some(target_symbol_ref) = symbol_diff.target_symbol { + let (target_section, target_symbol) = + right_ctx.obj.unwrap().0.section_symbol(target_symbol_ref); + navigation.right_symbol = Some(SymbolRefByName::new(target_symbol, target_section)); + } + } + } else if navigation.left_symbol.is_some() + && left_ctx.obj.is_some() + && left_ctx.section.is_none() + { + // Clear selection if symbol goes missing + navigation.left_symbol = None; + } + if let Some((_symbol, symbol_diff)) = right_ctx.symbol { + // If a matching symbol appears, select it + if !left_ctx.has_symbol() { + if let Some(target_symbol_ref) = symbol_diff.target_symbol { + let (target_section, target_symbol) = + left_ctx.obj.unwrap().0.section_symbol(target_symbol_ref); + navigation.left_symbol = Some(SymbolRefByName::new(target_symbol, target_section)); + } + } + } else if navigation.right_symbol.is_some() + && right_ctx.obj.is_some() + && right_ctx.section.is_none() + { + // Clear selection if symbol goes missing + navigation.right_symbol = None; + } + // If both sides are missing a symbol, switch to symbol diff view + if navigation.left_symbol.is_none() && navigation.right_symbol.is_none() { + navigation.view = View::SymbolDiff; + } + // Execute navigation if it changed + if navigation != current_navigation && !state.post_build_nav.is_some() { + ret = Some(DiffViewAction::Navigate(navigation)); + } + + let available_width = ui.available_width(); + let mut open_sections = (None, None); + + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { + // Left column + + // First row + if state.current_view == View::SymbolDiff { + ui.label(RichText::new("Target object").text_style(egui::TextStyle::Monospace)); + } else { + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) { + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); + } + + if let Some((symbol, _)) = left_ctx.symbol { + ui.separator(); + if ui + .add_enabled( + !state.scratch_running + && state.scratch_available + && left_ctx.has_symbol(), + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); + } + } + }); + } + + // Second row + if !left_ctx.status.success { + ui.label( + RichText::new("Fail") + .font(appearance.code_font.clone()) + .color(appearance.delete_color), + ); + } else if state.current_view == View::SymbolDiff { + if left_ctx.obj.is_some() { + ui.label( + RichText::new(state.object_name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if let Some((symbol, _)) = left_ctx.symbol { + ui.label( + RichText::new(symbol.demangled_name.as_deref().unwrap_or(&symbol.name)) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else if let Some((section, _)) = left_ctx.section { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else if right_ctx.has_symbol() { + ui.label( + RichText::new("Choose target symbol") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + + // Third row + if left_ctx.has_symbol() && right_ctx.has_symbol() { + if state.current_view == View::FunctionDiff + && ui + .button("Change target") + .on_hover_text_at_pointer("Choose a different symbol to use as the target") + .clicked() + || hotkeys::consume_change_target_shortcut(ui.ctx()) + { + if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() { + ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); + } + } + } else if left_ctx.status.success && !left_ctx.has_symbol() { + ui.horizontal(|ui| { + let mut search = state.search.clone(); + let response = + TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui); + if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) { + response.request_focus(); + } + if response.changed() { + ret = Some(DiffViewAction::SetSearch(search)); + } + + ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { + open_sections.0 = Some(true); + } + if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() + { + open_sections.0 = Some(false); + } + }) + }); + } + } else if column == 1 { + // Right column + + // First row + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + ret = Some(DiffViewAction::Build); + } + if state.build_running { + ui.colored_label( + appearance.replace_color, + RichText::new("Building…").text_style(egui::TextStyle::Monospace), + ); + } else { + ui.label(RichText::new("Last built:").text_style(egui::TextStyle::Monospace)); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + RichText::new( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ) + .text_style(egui::TextStyle::Monospace), + ); + } + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + ret = Some(DiffViewAction::OpenSourcePath); + } + }); + + // Second row + if !right_ctx.status.success { + ui.label( + RichText::new("Fail") + .font(appearance.code_font.clone()) + .color(appearance.delete_color), + ); + } else if state.current_view == View::SymbolDiff { + if right_ctx.obj.is_some() { + if left_ctx.obj.is_some() { + ui.label(RichText::new("Base object").font(appearance.code_font.clone())); + } else { + ui.label( + RichText::new(state.object_name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if let Some((symbol, _)) = right_ctx.symbol { + ui.label( + RichText::new(symbol.demangled_name.as_deref().unwrap_or(&symbol.name)) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else if let Some((section, _)) = right_ctx.section { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else if left_ctx.has_symbol() { + ui.label( + RichText::new("Choose base symbol") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + + // Third row + ui.horizontal(|ui| { + if let Some((_, symbol_diff)) = right_ctx.symbol { + if let Some(match_percent) = symbol_diff.match_percent { + ui.label( + RichText::new(format!("{:.0}%", match_percent.floor())) + .font(appearance.code_font.clone()) + .color(match_color_for_symbol(match_percent, appearance)), + ); + } + if state.current_view == View::FunctionDiff && left_ctx.has_symbol() { + ui.separator(); + if ui + .button("Change base") + .on_hover_text_at_pointer( + "Choose a different symbol to use as the base", + ) + .clicked() + || hotkeys::consume_change_base_shortcut(ui.ctx()) + { + if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() { + ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone())); + } + } + } + } else if right_ctx.status.success && !right_ctx.has_symbol() { + let mut search = state.search.clone(); + let response = + TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui); + if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) { + response.request_focus(); + } + if response.changed() { + ret = Some(DiffViewAction::SetSearch(search)); + } + + ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { + if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { + open_sections.1 = Some(true); + } + if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() + { + open_sections.1 = Some(false); + } + }); + } + }); + } + }); + + // Table + ui.push_id(Id::new(left_ctx.id()).with(right_ctx.id()), |ui| { + if let ( + View::FunctionDiff, + Some((left_obj, left_diff)), + Some((right_obj, right_diff)), + Some((_, left_symbol_diff)), + Some((_, right_symbol_diff)), + ) = (state.current_view, left_ctx.obj, right_ctx.obj, left_ctx.symbol, right_ctx.symbol) + { + // Joint diff view + hotkeys::check_scroll_hotkeys(ui, true); + if left_symbol_diff.instructions.len() != right_symbol_diff.instructions.len() { + ui.label("Instruction count mismatch"); + return; + } + let instructions_len = left_symbol_diff.instructions.len(); + render_table( + ui, + available_width, + 2, + appearance.code_font.size, + instructions_len, + |row, column| { + if column == 0 { + if let Some(action) = asm_col_ui( + row, + FunctionDiffContext { + obj: left_obj, + diff: left_diff, + symbol_ref: Some(left_symbol_diff.symbol_ref), + }, + appearance, + &state.function_state, + column, + ) { + ret = Some(action); + } + } else if column == 1 { + if let Some(action) = asm_col_ui( + row, + FunctionDiffContext { + obj: right_obj, + diff: right_diff, + symbol_ref: Some(right_symbol_diff.symbol_ref), + }, + appearance, + &state.function_state, + column, + ) { + ret = Some(action); + } + if row.response().clicked() { + ret = Some(DiffViewAction::ClearDiffHighlight); + } + } + }, + ); + } else if let ( + View::DataDiff, + Some((left_obj, _left_diff)), + Some((right_obj, _right_diff)), + Some((_left_section, left_section_diff)), + Some((_right_section, right_section_diff)), + ) = + (state.current_view, left_ctx.obj, right_ctx.obj, left_ctx.section, right_ctx.section) + { + // Joint diff view + let left_total_bytes = + left_section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len); + let right_total_bytes = + right_section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len); + if left_total_bytes != right_total_bytes { + ui.label("Data size mismatch"); + return; + } + if left_total_bytes == 0 { + return; + } + let total_rows = (left_total_bytes - 1) / BYTES_PER_ROW + 1; + let left_diffs = + split_diffs(&left_section_diff.data_diff, &left_section_diff.reloc_diff); + let right_diffs = + split_diffs(&right_section_diff.data_diff, &right_section_diff.reloc_diff); + render_table( + ui, + available_width, + 2, + appearance.code_font.size, + total_rows, + |row, column| { + let i = row.index(); + let address = i * BYTES_PER_ROW; + row.col(|ui| { + if column == 0 { + data_row_ui(ui, Some(left_obj), address, &left_diffs[i], appearance); + } else if column == 1 { + data_row_ui(ui, Some(right_obj), address, &right_diffs[i], appearance); + } + }); + }, + ); + } else { + // Split view + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + if let Some(action) = diff_col_ui( + ui, + state, + appearance, + column, + left_ctx, + right_ctx, + available_width, + open_sections.0, + ) { + ret = Some(action); + } + } else if column == 1 { + if let Some(action) = diff_col_ui( + ui, + state, + appearance, + column, + right_ctx, + left_ctx, + available_width, + open_sections.1, + ) { + ret = Some(action); + } + } + }); + } + }); + + ret +} + +#[must_use] +#[allow(clippy::too_many_arguments)] +fn diff_col_ui( + ui: &mut Ui, + state: &DiffViewState, + appearance: &Appearance, + column: usize, + ctx: DiffColumnContext, + other_ctx: DiffColumnContext, + available_width: f32, + open_sections: Option, +) -> Option { + let mut ret = None; + if !ctx.status.success { + build_log_ui(ui, ctx.status, appearance); + } else if let Some((obj, diff)) = ctx.obj { + if let Some((_symbol, symbol_diff)) = ctx.symbol { + hotkeys::check_scroll_hotkeys(ui, false); + let ctx = FunctionDiffContext { obj, diff, symbol_ref: Some(symbol_diff.symbol_ref) }; + if state.current_view == View::ExtabDiff { + extab_ui(ui, ctx, appearance, column); + } else { + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + symbol_diff.instructions.len(), + |row, column| { + if let Some(action) = + asm_col_ui(row, ctx, appearance, &state.function_state, column) + { + ret = Some(action); + } + if row.response().clicked() { + ret = Some(DiffViewAction::ClearDiffHighlight); + } + }, + ); + } + } else if let Some((_section, section_diff)) = ctx.section { + hotkeys::check_scroll_hotkeys(ui, false); + let total_bytes = + section_diff.data_diff.iter().fold(0usize, |accum, item| accum + item.len); + if total_bytes == 0 { + return ret; + } + let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; + let diffs = split_diffs(§ion_diff.data_diff, §ion_diff.reloc_diff); + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + total_rows, + |row, _column| { + let i = row.index(); + let address = i * BYTES_PER_ROW; + row.col(|ui| { + data_row_ui(ui, Some(obj), address, &diffs[i], appearance); + }); + }, + ); + } else if let ( + Some((other_section, _other_section_diff)), + Some((other_symbol, other_symbol_diff)), + ) = (other_ctx.section, other_ctx.symbol) + { + if let Some(action) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + None, + &state.symbol_state, + SymbolFilter::Mapping(other_symbol_diff.symbol_ref, None), + appearance, + column, + open_sections, + ) { + match (column, action) { + ( + 0, + DiffViewAction::Navigate(DiffViewNavigation { + left_symbol: Some(left_symbol_ref), + .. + }), + ) => { + ret = Some(DiffViewAction::SetMapping( + match other_section.kind { + ObjSectionKind::Code => View::FunctionDiff, + _ => View::SymbolDiff, + }, + left_symbol_ref, + SymbolRefByName::new(other_symbol, Some(other_section)), + )); + } + ( + 1, + DiffViewAction::Navigate(DiffViewNavigation { + right_symbol: Some(right_symbol_ref), + .. + }), + ) => { + ret = Some(DiffViewAction::SetMapping( + match other_section.kind { + ObjSectionKind::Code => View::FunctionDiff, + _ => View::SymbolDiff, + }, + SymbolRefByName::new(other_symbol, Some(other_section)), + right_symbol_ref, + )); + } + (_, action) => { + ret = Some(action); + } + } + } + } else { + let filter = match &state.search_regex { + Some(regex) => SymbolFilter::Search(regex), + _ => SymbolFilter::None, + }; + if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + other_ctx.obj.map(|(obj, diff)| SymbolDiffContext { obj, diff }), + &state.symbol_state, + filter, + appearance, + column, + open_sections, + ) { + ret = Some(result); + } + } + } else { + missing_obj_ui(ui, appearance); + } + + ret +} + +fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) { + ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { + ui.horizontal(|ui| { + if !status.cmdline.is_empty() && ui.button("Copy command").clicked() { + ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline)); + } + if ui.button("Copy log").clicked() { + ui.output_mut(|output| { + output.copied_text = format!("{}\n{}", status.stdout, status.stderr) + }); + } + }); + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + if !status.cmdline.is_empty() { + ui.label(&status.cmdline); + } + if !status.stdout.is_empty() { + ui.colored_label(appearance.replace_color, &status.stdout); + } + if !status.stderr.is_empty() { + ui.colored_label(appearance.delete_color, &status.stderr); + } + }); + }); +} + +fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) { + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + + ui.colored_label(appearance.replace_color, "No object configured"); + }); +} + +fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option { + for (section_idx, section) in obj.sections.iter().enumerate() { + for (symbol_idx, symbol) in section.symbols.iter().enumerate() { + if symbol.name == selected_symbol.symbol_name { + return Some(SymbolRef { section_idx, symbol_idx }); + } + } + } + None +} + +fn find_section(obj: &ObjInfo, section_name: &str) -> Option { + obj.sections.iter().position(|section| section.name == section_name) +} diff --git a/objdiff-gui/src/views/extab_diff.rs b/objdiff-gui/src/views/extab_diff.rs index 5d99117..9065cac 100644 --- a/objdiff-gui/src/views/extab_diff.rs +++ b/objdiff-gui/src/views/extab_diff.rs @@ -1,22 +1,10 @@ -use egui::{RichText, ScrollArea}; +use egui::ScrollArea; use objdiff_core::{ arch::ppc::ExceptionInfo, obj::{ObjInfo, ObjSymbol}, }; -use time::format_description; -use crate::{ - hotkeys, - views::{ - appearance::Appearance, - column_layout::{render_header, render_strips}, - function_diff::FunctionDiffContext, - symbol_diff::{ - match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, - SymbolRefByName, View, - }, - }, -}; +use crate::views::{appearance::Appearance, function_diff::FunctionDiffContext}; fn decode_extab(extab: &ExceptionInfo) -> String { let mut text = String::from(""); @@ -57,7 +45,7 @@ fn extab_text_ui( None } -fn extab_ui( +pub(crate) fn extab_ui( ui: &mut egui::Ui, ctx: FunctionDiffContext<'_>, appearance: &Appearance, @@ -76,178 +64,3 @@ fn extab_ui( }); }); } - -#[must_use] -pub fn extab_diff_ui( - ui: &mut egui::Ui, - state: &DiffViewState, - appearance: &Appearance, -) -> Option { - let mut ret = None; - let Some(result) = &state.build else { - return ret; - }; - - let mut left_ctx = FunctionDiffContext::new( - result.first_obj.as_ref(), - state.symbol_state.left_symbol.as_ref(), - ); - let mut right_ctx = FunctionDiffContext::new( - result.second_obj.as_ref(), - state.symbol_state.right_symbol.as_ref(), - ); - - // If one side is missing a symbol, but the diff process found a match, use that symbol - let left_diff_symbol = left_ctx.and_then(|ctx| { - ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) - }); - let right_diff_symbol = right_ctx.and_then(|ctx| { - ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) - }); - if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) { - let (right_section, right_symbol) = - right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); - let symbol_ref = SymbolRefByName::new(right_symbol, right_section); - right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); - ret = Some(DiffViewAction::Navigate(DiffViewNavigation { - view: Some(View::FunctionDiff), - left_symbol: state.symbol_state.left_symbol.clone(), - right_symbol: Some(symbol_ref), - })); - } else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) { - let (left_section, left_symbol) = - left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); - let symbol_ref = SymbolRefByName::new(left_symbol, left_section); - left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); - ret = Some(DiffViewAction::Navigate(DiffViewNavigation { - view: Some(View::FunctionDiff), - left_symbol: Some(symbol_ref), - right_symbol: state.symbol_state.right_symbol.clone(), - })); - } - - // If both sides are missing a symbol, switch to symbol diff view - if right_ctx.is_some_and(|ctx| !ctx.has_symbol()) - && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) - { - return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - - // Header - let available_width = ui.available_width(); - render_header(ui, available_width, 2, |ui, column| { - if column == 0 { - // Left column - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) { - ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - ui.separator(); - if ui - .add_enabled( - !state.scratch_running - && state.scratch_available - && left_ctx.is_some_and(|ctx| ctx.has_symbol()), - egui::Button::new("📲 decomp.me"), - ) - .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") - .on_disabled_hover_text("Scratch configuration missing") - .clicked() - { - if let Some((_section, symbol)) = left_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) - }) { - ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); - } - } - }); - - if let Some((_section, symbol)) = left_ctx - .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) - { - let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); - ui.label( - RichText::new(name) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - } - } else if column == 1 { - // Right column - ui.horizontal(|ui| { - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - ret = Some(DiffViewAction::Build); - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), - ); - } - }); - ui.separator(); - if ui - .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") - .clicked() - { - ret = Some(DiffViewAction::OpenSourcePath); - } - }); - - if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| { - (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) - }) - }) { - let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); - ui.label( - RichText::new(name) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - if let Some(match_percent) = symbol_diff.match_percent { - ui.label( - RichText::new(format!("{:.0}%", match_percent.floor())) - .font(appearance.code_font.clone()) - .color(match_color_for_symbol(match_percent, appearance)), - ); - } - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - } - } - }); - - hotkeys::check_scroll_hotkeys(ui, true); - - // Table - render_strips(ui, available_width, 2, |ui, column| { - if column == 0 { - if let Some(ctx) = left_ctx { - extab_ui(ui, ctx, appearance, column); - } - } else if column == 1 { - if let Some(ctx) = right_ctx { - extab_ui(ui, ctx, appearance, column); - } - } - }); - ret -} diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index 3928a75..8981af2 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -1,30 +1,16 @@ use std::{cmp::Ordering, default::Default}; -use egui::{text::LayoutJob, Id, Label, Layout, Response, RichText, Sense, Widget}; +use egui::{text::LayoutJob, Label, Response, Sense, Widget}; use egui_extras::TableRow; use objdiff_core::{ diff::{ display::{display_diff, DiffText, HighlightKind}, ObjDiff, ObjInsDiff, ObjInsDiffKind, }, - obj::{ - ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol, - SymbolRef, - }, + obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, }; -use time::format_description; -use crate::{ - hotkeys, - views::{ - appearance::Appearance, - column_layout::{render_header, render_strips, render_table}, - symbol_diff::{ - match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, - DiffViewState, SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View, - }, - }, -}; +use crate::views::{appearance::Appearance, symbol_diff::DiffViewAction}; #[derive(Default)] pub struct FunctionViewState { @@ -245,17 +231,6 @@ fn ins_context_menu( }); } -fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option { - for (section_idx, section) in obj.sections.iter().enumerate() { - for (symbol_idx, symbol) in section.symbols.iter().enumerate() { - if symbol.name == selected_symbol.symbol_name { - return Some(SymbolRef { section_idx, symbol_idx }); - } - } - } - None -} - #[must_use] #[expect(clippy::too_many_arguments)] fn diff_text_ui( @@ -400,7 +375,7 @@ fn asm_row_ui( } #[must_use] -fn asm_col_ui( +pub(crate) fn asm_col_ui( row: &mut TableRow<'_, '_>, ctx: FunctionDiffContext<'_>, appearance: &Appearance, @@ -433,464 +408,9 @@ fn asm_col_ui( ret } -#[must_use] -#[expect(clippy::too_many_arguments)] -fn asm_table_ui( - ui: &mut egui::Ui, - available_width: f32, - left_ctx: Option>, - right_ctx: Option>, - appearance: &Appearance, - ins_view_state: &FunctionViewState, - symbol_state: &SymbolViewState, - open_sections: (Option, Option), -) -> Option { - let mut ret = None; - let left_len = left_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) - }); - let right_len = right_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) - }); - let instructions_len = match (left_len, right_len) { - (Some(left_len), Some(right_len)) => { - if left_len != right_len { - ui.label("Instruction count mismatch"); - return None; - } - left_len - } - (Some(left_len), None) => left_len, - (None, Some(right_len)) => right_len, - (None, None) => { - ui.label("No symbol selected"); - return None; - } - }; - if left_len.is_some() && right_len.is_some() { - // Joint view - hotkeys::check_scroll_hotkeys(ui, true); - render_table( - ui, - available_width, - 2, - appearance.code_font.size, - instructions_len, - |row, column| { - if column == 0 { - if let Some(ctx) = left_ctx { - if let Some(action) = - asm_col_ui(row, ctx, appearance, ins_view_state, column) - { - ret = Some(action); - } - } - } else if column == 1 { - if let Some(ctx) = right_ctx { - if let Some(action) = - asm_col_ui(row, ctx, appearance, ins_view_state, column) - { - ret = Some(action); - } - } - if row.response().clicked() { - ret = Some(DiffViewAction::ClearDiffHighlight); - } - } - }, - ); - } else { - // Split view, one side is the symbol list - render_strips(ui, available_width, 2, |ui, column| { - if column == 0 { - if let Some(ctx) = left_ctx { - if ctx.has_symbol() { - hotkeys::check_scroll_hotkeys(ui, false); - render_table( - ui, - available_width / 2.0, - 1, - appearance.code_font.size, - instructions_len, - |row, column| { - if let Some(action) = - asm_col_ui(row, ctx, appearance, ins_view_state, column) - { - ret = Some(action); - } - if row.response().clicked() { - ret = Some(DiffViewAction::ClearDiffHighlight); - } - }, - ); - } else if let Some((right_ctx, right_symbol_ref)) = - right_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref))) - { - if let Some(action) = symbol_list_ui( - ui, - SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, - None, - symbol_state, - SymbolFilter::Mapping(right_symbol_ref), - appearance, - column, - open_sections.0, - ) { - match action { - DiffViewAction::Navigate(DiffViewNavigation { - left_symbol: Some(left_symbol_ref), - .. - }) => { - let (right_section, right_symbol) = - right_ctx.obj.section_symbol(right_symbol_ref); - ret = Some(DiffViewAction::SetMapping( - match right_section.map(|s| s.kind) { - Some(ObjSectionKind::Code) => View::FunctionDiff, - _ => View::SymbolDiff, - }, - left_symbol_ref, - SymbolRefByName::new(right_symbol, right_section), - )); - } - _ => { - ret = Some(action); - } - } - } - } - } else { - ui.label("No left object"); - } - } else if column == 1 { - if let Some(ctx) = right_ctx { - if ctx.has_symbol() { - hotkeys::check_scroll_hotkeys(ui, false); - render_table( - ui, - available_width / 2.0, - 1, - appearance.code_font.size, - instructions_len, - |row, column| { - if let Some(action) = - asm_col_ui(row, ctx, appearance, ins_view_state, column) - { - ret = Some(action); - } - if row.response().clicked() { - ret = Some(DiffViewAction::ClearDiffHighlight); - } - }, - ); - } else if let Some((left_ctx, left_symbol_ref)) = - left_ctx.and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| (ctx, symbol_ref))) - { - if let Some(action) = symbol_list_ui( - ui, - SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, - None, - symbol_state, - SymbolFilter::Mapping(left_symbol_ref), - appearance, - column, - open_sections.1, - ) { - match action { - DiffViewAction::Navigate(DiffViewNavigation { - right_symbol: Some(right_symbol_ref), - .. - }) => { - let (left_section, left_symbol) = - left_ctx.obj.section_symbol(left_symbol_ref); - ret = Some(DiffViewAction::SetMapping( - match left_section.map(|s| s.kind) { - Some(ObjSectionKind::Code) => View::FunctionDiff, - _ => View::SymbolDiff, - }, - SymbolRefByName::new(left_symbol, left_section), - right_symbol_ref, - )); - } - _ => { - ret = Some(action); - } - } - } - } - } else { - ui.label("No right object"); - } - } - }); - } - ret -} - #[derive(Clone, Copy)] pub struct FunctionDiffContext<'a> { pub obj: &'a ObjInfo, pub diff: &'a ObjDiff, pub symbol_ref: Option, } - -impl<'a> FunctionDiffContext<'a> { - pub fn new( - obj: Option<&'a (ObjInfo, ObjDiff)>, - selected_symbol: Option<&SymbolRefByName>, - ) -> Option { - obj.map(|(obj, diff)| Self { - obj, - diff, - symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)), - }) - } - - #[inline] - pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() } -} - -#[must_use] -pub fn function_diff_ui( - ui: &mut egui::Ui, - state: &DiffViewState, - appearance: &Appearance, -) -> Option { - let mut ret = None; - let Some(result) = &state.build else { - return ret; - }; - - let mut left_ctx = FunctionDiffContext::new( - result.first_obj.as_ref(), - state.symbol_state.left_symbol.as_ref(), - ); - let mut right_ctx = FunctionDiffContext::new( - result.second_obj.as_ref(), - state.symbol_state.right_symbol.as_ref(), - ); - - // If one side is missing a symbol, but the diff process found a match, use that symbol - let left_diff_symbol = left_ctx.and_then(|ctx| { - ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) - }); - let right_diff_symbol = right_ctx.and_then(|ctx| { - ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) - }); - if left_diff_symbol.is_some() && right_ctx.is_some_and(|ctx| !ctx.has_symbol()) { - let (right_section, right_symbol) = - right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); - let symbol_ref = SymbolRefByName::new(right_symbol, right_section); - right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); - ret = Some(DiffViewAction::Navigate(DiffViewNavigation { - view: Some(View::FunctionDiff), - left_symbol: state.symbol_state.left_symbol.clone(), - right_symbol: Some(symbol_ref), - })); - } else if right_diff_symbol.is_some() && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) { - let (left_section, left_symbol) = - left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); - let symbol_ref = SymbolRefByName::new(left_symbol, left_section); - left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); - ret = Some(DiffViewAction::Navigate(DiffViewNavigation { - view: Some(View::FunctionDiff), - left_symbol: Some(symbol_ref), - right_symbol: state.symbol_state.right_symbol.clone(), - })); - } - - // If both sides are missing a symbol, switch to symbol diff view - if right_ctx.is_some_and(|ctx| !ctx.has_symbol()) - && left_ctx.is_some_and(|ctx| !ctx.has_symbol()) - { - return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - - // Header - let available_width = ui.available_width(); - let mut open_sections = (None, None); - render_header(ui, available_width, 2, |ui, column| { - if column == 0 { - // Left column - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) { - ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); - } - ui.separator(); - if ui - .add_enabled( - !state.scratch_running - && state.scratch_available - && left_ctx.is_some_and(|ctx| ctx.has_symbol()), - egui::Button::new("📲 decomp.me"), - ) - .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") - .on_disabled_hover_text("Scratch configuration missing") - .clicked() - { - if let Some((_section, symbol)) = left_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) - }) { - ret = Some(DiffViewAction::CreateScratch(symbol.name.clone())); - } - } - }); - - if let Some((_section, symbol)) = left_ctx - .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) - { - let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); - ui.label( - RichText::new(name) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - if right_ctx.is_some_and(|m| m.has_symbol()) - && (ui - .button("Change target") - .on_hover_text_at_pointer("Choose a different symbol to use as the target") - .clicked() - || hotkeys::consume_change_target_shortcut(ui.ctx())) - { - if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() { - ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); - } - } - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - - ui.horizontal(|ui| { - ui.label( - RichText::new("Choose target symbol") - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - - ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { - if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { - open_sections.0 = Some(true); - } - if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() - { - open_sections.0 = Some(false); - } - }) - }); - } - } else if column == 1 { - // Right column - ui.horizontal(|ui| { - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - ret = Some(DiffViewAction::Build); - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), - ); - } - }); - ui.separator(); - if ui - .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") - .clicked() - { - ret = Some(DiffViewAction::OpenSourcePath); - } - }); - - if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { - ctx.symbol_ref.map(|symbol_ref| { - (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) - }) - }) { - let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); - ui.label( - RichText::new(name) - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - ui.horizontal(|ui| { - if let Some(match_percent) = symbol_diff.match_percent { - ui.label( - RichText::new(format!("{:.0}%", match_percent.floor())) - .font(appearance.code_font.clone()) - .color(match_color_for_symbol(match_percent, appearance)), - ); - } - if left_ctx.is_some_and(|m| m.has_symbol()) { - ui.separator(); - if ui - .button("Change base") - .on_hover_text_at_pointer( - "Choose a different symbol to use as the base", - ) - .clicked() - || hotkeys::consume_change_base_shortcut(ui.ctx()) - { - if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() { - ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone())); - } - } - } - }); - } else { - ui.label( - RichText::new("Missing") - .font(appearance.code_font.clone()) - .color(appearance.replace_color), - ); - - ui.horizontal(|ui| { - ui.label( - RichText::new("Choose base symbol") - .font(appearance.code_font.clone()) - .color(appearance.highlight_color), - ); - - ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { - if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { - open_sections.1 = Some(true); - } - if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() - { - open_sections.1 = Some(false); - } - }) - }); - } - } - }); - - // Table - let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str())) - .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str())); - if let Some(action) = ui - .push_id(id, |ui| { - asm_table_ui( - ui, - available_width, - left_ctx, - right_ctx, - appearance, - &state.function_state, - &state.symbol_state, - open_sections, - ) - }) - .inner - { - ret = Some(action); - } - ret -} diff --git a/objdiff-gui/src/views/mod.rs b/objdiff-gui/src/views/mod.rs index 16e9380..543fead 100644 --- a/objdiff-gui/src/views/mod.rs +++ b/objdiff-gui/src/views/mod.rs @@ -6,6 +6,7 @@ pub(crate) mod config; pub(crate) mod data_diff; pub(crate) mod debug; pub(crate) mod demangle; +pub(crate) mod diff; pub(crate) mod extab_diff; pub(crate) mod file; pub(crate) mod frame_history; diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index d5a9211..23e95aa 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -1,12 +1,11 @@ use std::{collections::BTreeMap, mem::take, ops::Bound}; use egui::{ - style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, Layout, OpenUrl, - ScrollArea, SelectableLabel, TextEdit, Ui, Widget, + style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, + SelectableLabel, Ui, Widget, }; use objdiff_core::{ arch::ObjArch, - build::BuildStatus, diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff}, jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult}, obj::{ @@ -19,15 +18,10 @@ use crate::{ app::AppStateRef, hotkeys, jobs::{is_create_scratch_available, start_create_scratch}, - views::{ - appearance::Appearance, - column_layout::{render_header, render_strips}, - function_diff::FunctionViewState, - write_text, - }, + views::{appearance::Appearance, function_diff::FunctionViewState, write_text}, }; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct SymbolRefByName { pub symbol_name: String, pub section_name: Option, @@ -79,16 +73,16 @@ pub enum DiffViewAction { SetShowMappedSymbols(bool), } -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone, Default, Eq, PartialEq)] pub struct DiffViewNavigation { - pub view: Option, + pub view: View, pub left_symbol: Option, pub right_symbol: Option, } impl DiffViewNavigation { pub fn symbol_diff() -> Self { - Self { view: Some(View::SymbolDiff), left_symbol: None, right_symbol: None } + Self { view: View::SymbolDiff, left_symbol: None, right_symbol: None } } pub fn with_symbols( @@ -107,8 +101,24 @@ impl DiffViewNavigation { }) }); match column { - 0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 }, - 1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 }, + 0 => Self { view, left_symbol: symbol1, right_symbol: symbol2 }, + 1 => Self { view, left_symbol: symbol2, right_symbol: symbol1 }, + _ => unreachable!("Invalid column index"), + } + } + + pub fn data_diff(section: &ObjSection, column: usize) -> Self { + let symbol = Some(SymbolRefByName { + symbol_name: "".to_string(), + section_name: Some(section.name.clone()), + }); + match column { + 0 => Self { + view: View::DataDiff, + left_symbol: symbol.clone(), + right_symbol: symbol.clone(), + }, + 1 => Self { view: View::DataDiff, left_symbol: symbol.clone(), right_symbol: symbol }, _ => unreachable!("Invalid column index"), } } @@ -151,9 +161,7 @@ impl DiffViewState { // TODO: where should this go? if let Some(result) = self.post_build_nav.take() { - if let Some(view) = result.view { - self.current_view = view; - } + self.current_view = result.view; self.symbol_state.left_symbol = result.left_symbol; self.symbol_state.right_symbol = result.right_symbol; } @@ -219,7 +227,7 @@ impl DiffViewState { }; if (nav.left_symbol.is_some() && nav.right_symbol.is_some()) || (nav.left_symbol.is_none() && nav.right_symbol.is_none()) - || nav.view != Some(View::FunctionDiff) + || nav.view != View::FunctionDiff { // Regular navigation if state.is_selecting_symbol() { @@ -228,9 +236,7 @@ impl DiffViewState { self.post_build_nav = Some(nav); } else { // Navigate immediately - if let Some(view) = nav.view { - self.current_view = view; - } + self.current_view = nav.view; self.symbol_state.left_symbol = nav.left_symbol; self.symbol_state.right_symbol = nav.right_symbol; } @@ -301,7 +307,7 @@ impl DiffViewState { }; state.set_selecting_left(&right_ref.symbol_name); self.post_build_nav = Some(DiffViewNavigation { - view: Some(View::FunctionDiff), + view: View::FunctionDiff, left_symbol: None, right_symbol: Some(right_ref), }); @@ -316,7 +322,7 @@ impl DiffViewState { }; state.set_selecting_right(&left_ref.symbol_name); self.post_build_nav = Some(DiffViewNavigation { - view: Some(View::FunctionDiff), + view: View::FunctionDiff, left_symbol: Some(left_ref), right_symbol: None, }); @@ -337,7 +343,7 @@ impl DiffViewState { self.post_build_nav = Some(DiffViewNavigation::symbol_diff()); } else { self.post_build_nav = Some(DiffViewNavigation { - view: Some(view), + view, left_symbol: Some(left_ref), right_symbol: Some(right_ref), }); @@ -409,13 +415,13 @@ fn symbol_context_menu_ui( let symbol_ref = SymbolRefByName::new(symbol, Some(section)); if column == 0 { ret = Some(DiffViewNavigation { - view: Some(View::FunctionDiff), + view: View::FunctionDiff, left_symbol: Some(symbol_ref), right_symbol: None, }); } else { ret = Some(DiffViewNavigation { - view: Some(View::FunctionDiff), + view: View::FunctionDiff, left_symbol: None, right_symbol: Some(symbol_ref), }); @@ -552,13 +558,8 @@ fn symbol_ui( ))); } ObjSectionKind::Data => { - ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols( - View::DataDiff, - other_ctx, - symbol, - section, - symbol_diff, - column, + ret = Some(DiffViewAction::Navigate(DiffViewNavigation::data_diff( + section, column, ))); } ObjSectionKind::Bss => {} @@ -591,9 +592,15 @@ fn symbol_matches_filter( SymbolFilter::None => true, SymbolFilter::Search(regex) => { regex.is_match(&symbol.name) - || symbol.demangled_name.as_ref().map(|s| regex.is_match(s)).unwrap_or(false) + || symbol.demangled_name.as_deref().is_some_and(|s| regex.is_match(s)) + } + SymbolFilter::Mapping(symbol_ref, regex) => { + diff.target_symbol == Some(symbol_ref) + && regex.is_none_or(|r| { + r.is_match(&symbol.name) + || symbol.demangled_name.as_deref().is_some_and(|s| r.is_match(s)) + }) } - SymbolFilter::Mapping(symbol_ref) => diff.target_symbol == Some(symbol_ref), } } @@ -601,7 +608,7 @@ fn symbol_matches_filter( pub enum SymbolFilter<'a> { None, Search(&'a Regex), - Mapping(SymbolRef), + Mapping(SymbolRef, Option<&'a Regex>), } #[must_use] @@ -619,21 +626,23 @@ pub fn symbol_list_ui( let mut ret = None; ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { let mut mapping = BTreeMap::new(); - if let SymbolFilter::Mapping(target_ref) = filter { + if let SymbolFilter::Mapping(_, _) = filter { let mut show_mapped_symbols = state.show_mapped_symbols; if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() { ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols)); } for mapping_diff in &ctx.diff.mapping_symbols { - if mapping_diff.target_symbol == Some(target_ref) { - if !show_mapped_symbols { - let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref); - if symbol_diff.target_symbol.is_some() { - continue; - } - } - mapping.insert(mapping_diff.symbol_ref, mapping_diff); + let symbol = ctx.obj.section_symbol(mapping_diff.symbol_ref).1; + if !symbol_matches_filter(symbol, mapping_diff, filter) { + continue; } + if !show_mapped_symbols { + let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref); + if symbol_diff.target_symbol.is_some() { + continue; + } + } + mapping.insert(mapping_diff.symbol_ref, mapping_diff); } } else { for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) { @@ -819,206 +828,8 @@ pub fn symbol_list_ui( ret } -fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) { - ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { - ui.horizontal(|ui| { - if !status.cmdline.is_empty() && ui.button("Copy command").clicked() { - ui.output_mut(|output| output.copied_text.clone_from(&status.cmdline)); - } - if ui.button("Copy log").clicked() { - ui.output_mut(|output| { - output.copied_text = format!("{}\n{}", status.stdout, status.stderr) - }); - } - }); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - if !status.cmdline.is_empty() { - ui.label(&status.cmdline); - } - if !status.stdout.is_empty() { - ui.colored_label(appearance.replace_color, &status.stdout); - } - if !status.stderr.is_empty() { - ui.colored_label(appearance.delete_color, &status.stderr); - } - }); - }); -} - -fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) { - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - - ui.colored_label(appearance.replace_color, "No object configured"); - }); -} - #[derive(Copy, Clone)] pub struct SymbolDiffContext<'a> { pub obj: &'a ObjInfo, pub diff: &'a ObjDiff, } - -#[must_use] -pub fn symbol_diff_ui( - ui: &mut Ui, - state: &mut DiffViewState, - appearance: &Appearance, -) -> Option { - let mut ret = None; - let Some(result) = &state.build else { - return ret; - }; - - // Header - let available_width = ui.available_width(); - let mut open_sections = (None, None); - render_header(ui, available_width, 2, |ui, column| { - if column == 0 { - // Left column - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - - ui.label("Target object"); - if result.first_status.success { - if result.first_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.colored_label(appearance.highlight_color, state.object_name.clone()); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); - - ui.horizontal(|ui| { - let mut search = state.search.clone(); - let response = TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui); - if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) { - response.request_focus(); - } - if response.changed() { - ret = Some(DiffViewAction::SetSearch(search)); - } - - ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { - if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { - open_sections.0 = Some(true); - } - if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() { - open_sections.0 = Some(false); - } - }) - }); - } else if column == 1 { - // Right column - ui.horizontal(|ui| { - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.label("Base object"); - }); - ui.separator(); - if ui - .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") - .clicked() - { - ret = Some(DiffViewAction::OpenSourcePath); - } - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if result.second_status.success { - if result.second_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.colored_label(appearance.highlight_color, "OK"); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); - - ui.horizontal(|ui| { - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - ret = Some(DiffViewAction::Build); - } - - ui.with_layout(Layout::right_to_left(egui::Align::TOP), |ui| { - if ui.small_button("⏷").on_hover_text_at_pointer("Expand all").clicked() { - open_sections.1 = Some(true); - } - if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() { - open_sections.1 = Some(false); - } - }) - }); - } - }); - - // Table - let filter = match &state.search_regex { - Some(regex) => SymbolFilter::Search(regex), - _ => SymbolFilter::None, - }; - render_strips(ui, available_width, 2, |ui, column| { - if column == 0 { - // Left column - if result.first_status.success { - if let Some((obj, diff)) = &result.first_obj { - if let Some(result) = symbol_list_ui( - ui, - SymbolDiffContext { obj, diff }, - result - .second_obj - .as_ref() - .map(|(obj, diff)| SymbolDiffContext { obj, diff }), - &state.symbol_state, - filter, - appearance, - column, - open_sections.0, - ) { - ret = Some(result); - } - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.first_status, appearance); - } - } else if column == 1 { - // Right column - if result.second_status.success { - if let Some((obj, diff)) = &result.second_obj { - if let Some(result) = symbol_list_ui( - ui, - SymbolDiffContext { obj, diff }, - result - .first_obj - .as_ref() - .map(|(obj, diff)| SymbolDiffContext { obj, diff }), - &state.symbol_state, - filter, - appearance, - column, - open_sections.1, - ) { - ret = Some(result); - } - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.second_status, appearance); - } - } - }); - ret -}