Diff view refactor

This commit is contained in:
Luke Street 2025-02-02 17:32:08 -07:00
parent 3e6efb7736
commit d938988d43
8 changed files with 814 additions and 1118 deletions

View File

@ -18,4 +18,4 @@ authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
repository = "https://github.com/encounter/objdiff" repository = "https://github.com/encounter/objdiff"
rust-version = "1.81" rust-version = "1.82"

View File

@ -33,16 +33,14 @@ use crate::{
arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState, arch_config_window, config_ui, general_config_ui, project_window, ConfigViewState,
CONFIG_DISABLED_TEXT, CONFIG_DISABLED_TEXT,
}, },
data_diff::data_diff_ui,
debug::debug_window, debug::debug_window,
demangle::{demangle_window, DemangleViewState}, demangle::{demangle_window, DemangleViewState},
extab_diff::extab_diff_ui, diff::diff_view_ui,
frame_history::FrameHistory, frame_history::FrameHistory,
function_diff::function_diff_ui,
graphics::{graphics_window, GraphicsConfig, GraphicsViewState}, graphics::{graphics_window, GraphicsConfig, GraphicsViewState},
jobs::{jobs_menu_ui, jobs_window}, jobs::{jobs_menu_ui, jobs_window},
rlwinm::{rlwinm_decode_window, RlwinmDecodeViewState}, 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; let mut action = None;
egui::CentralPanel::default().show(ctx, |ui| { 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 = diff_view_ui(ui, diff_state, appearance);
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)
};
}); });
project_window(ctx, state, show_project_config, config_state, appearance); project_window(ctx, state, show_project_config, config_state, appearance);

View File

@ -4,28 +4,15 @@ use std::{
mem::take, mem::take,
}; };
use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget}; use egui::{text::LayoutJob, Label, Sense, Widget};
use objdiff_core::{ use objdiff_core::{
diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff, ObjDiff}, diff::{ObjDataDiff, ObjDataDiffKind, ObjDataRelocDiff},
obj::ObjInfo, obj::ObjInfo,
}; };
use time::format_description;
use crate::{ use crate::views::{appearance::Appearance, write_text};
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
},
};
const BYTES_PER_ROW: usize = 16; pub(crate) const BYTES_PER_ROW: usize = 16;
fn find_section(obj: &ObjInfo, section_name: &str) -> Option<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}
fn data_row_hover_ui( fn data_row_hover_ui(
ui: &mut egui::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, ui: &mut egui::Ui,
obj: Option<&ObjInfo>, obj: Option<&ObjInfo>,
address: usize, address: usize,
@ -212,7 +199,7 @@ fn data_row_ui(
} }
} }
fn split_diffs( pub(crate) fn split_diffs(
diffs: &[ObjDataDiff], diffs: &[ObjDataDiff],
reloc_diffs: &[ObjDataRelocDiff], reloc_diffs: &[ObjDataRelocDiff],
) -> Vec<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>> { ) -> Vec<Vec<(ObjDataDiff, Vec<ObjDataRelocDiff>)>> {
@ -273,169 +260,3 @@ fn split_diffs(
} }
split_diffs split_diffs
} }
#[derive(Clone, Copy)]
struct SectionDiffContext<'a> {
obj: &'a ObjInfo,
diff: &'a ObjDiff,
section_index: Option<usize>,
}
impl<'a> SectionDiffContext<'a> {
pub fn new(obj: Option<&'a (ObjInfo, ObjDiff)>, section_name: Option<&str>) -> Option<Self> {
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<SectionDiffContext<'_>>,
right_ctx: Option<SectionDiffContext<'_>>,
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(&section.data_diff, &section.reloc_diff));
let right_diffs =
right_section.map(|(_, section)| split_diffs(&section.data_diff, &section.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<DiffViewAction> {
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
}

View File

@ -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<DiffViewAction> {
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<bool>,
) -> Option<DiffViewAction> {
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(&section_diff.data_diff, &section_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<SymbolRef> {
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<usize> {
obj.sections.iter().position(|section| section.name == section_name)
}

View File

@ -1,22 +1,10 @@
use egui::{RichText, ScrollArea}; use egui::ScrollArea;
use objdiff_core::{ use objdiff_core::{
arch::ppc::ExceptionInfo, arch::ppc::ExceptionInfo,
obj::{ObjInfo, ObjSymbol}, obj::{ObjInfo, ObjSymbol},
}; };
use time::format_description;
use crate::{ use crate::views::{appearance::Appearance, function_diff::FunctionDiffContext};
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolRefByName, View,
},
},
};
fn decode_extab(extab: &ExceptionInfo) -> String { fn decode_extab(extab: &ExceptionInfo) -> String {
let mut text = String::from(""); let mut text = String::from("");
@ -57,7 +45,7 @@ fn extab_text_ui(
None None
} }
fn extab_ui( pub(crate) fn extab_ui(
ui: &mut egui::Ui, ui: &mut egui::Ui,
ctx: FunctionDiffContext<'_>, ctx: FunctionDiffContext<'_>,
appearance: &Appearance, 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<DiffViewAction> {
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
}

View File

@ -1,30 +1,16 @@
use std::{cmp::Ordering, default::Default}; 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 egui_extras::TableRow;
use objdiff_core::{ use objdiff_core::{
diff::{ diff::{
display::{display_diff, DiffText, HighlightKind}, display::{display_diff, DiffText, HighlightKind},
ObjDiff, ObjInsDiff, ObjInsDiffKind, ObjDiff, ObjInsDiff, ObjInsDiffKind,
}, },
obj::{ obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef},
ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSectionKind, ObjSymbol,
SymbolRef,
},
}; };
use time::format_description;
use crate::{ use crate::views::{appearance::Appearance, symbol_diff::DiffViewAction};
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,
},
},
};
#[derive(Default)] #[derive(Default)]
pub struct FunctionViewState { pub struct FunctionViewState {
@ -245,17 +231,6 @@ fn ins_context_menu(
}); });
} }
fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option<SymbolRef> {
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] #[must_use]
#[expect(clippy::too_many_arguments)] #[expect(clippy::too_many_arguments)]
fn diff_text_ui( fn diff_text_ui(
@ -400,7 +375,7 @@ fn asm_row_ui(
} }
#[must_use] #[must_use]
fn asm_col_ui( pub(crate) fn asm_col_ui(
row: &mut TableRow<'_, '_>, row: &mut TableRow<'_, '_>,
ctx: FunctionDiffContext<'_>, ctx: FunctionDiffContext<'_>,
appearance: &Appearance, appearance: &Appearance,
@ -433,464 +408,9 @@ fn asm_col_ui(
ret ret
} }
#[must_use]
#[expect(clippy::too_many_arguments)]
fn asm_table_ui(
ui: &mut egui::Ui,
available_width: f32,
left_ctx: Option<FunctionDiffContext<'_>>,
right_ctx: Option<FunctionDiffContext<'_>>,
appearance: &Appearance,
ins_view_state: &FunctionViewState,
symbol_state: &SymbolViewState,
open_sections: (Option<bool>, Option<bool>),
) -> Option<DiffViewAction> {
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)] #[derive(Clone, Copy)]
pub struct FunctionDiffContext<'a> { pub struct FunctionDiffContext<'a> {
pub obj: &'a ObjInfo, pub obj: &'a ObjInfo,
pub diff: &'a ObjDiff, pub diff: &'a ObjDiff,
pub symbol_ref: Option<SymbolRef>, pub symbol_ref: Option<SymbolRef>,
} }
impl<'a> FunctionDiffContext<'a> {
pub fn new(
obj: Option<&'a (ObjInfo, ObjDiff)>,
selected_symbol: Option<&SymbolRefByName>,
) -> Option<Self> {
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<DiffViewAction> {
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
}

View File

@ -6,6 +6,7 @@ pub(crate) mod config;
pub(crate) mod data_diff; pub(crate) mod data_diff;
pub(crate) mod debug; pub(crate) mod debug;
pub(crate) mod demangle; pub(crate) mod demangle;
pub(crate) mod diff;
pub(crate) mod extab_diff; pub(crate) mod extab_diff;
pub(crate) mod file; pub(crate) mod file;
pub(crate) mod frame_history; pub(crate) mod frame_history;

View File

@ -1,12 +1,11 @@
use std::{collections::BTreeMap, mem::take, ops::Bound}; use std::{collections::BTreeMap, mem::take, ops::Bound};
use egui::{ use egui::{
style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, Layout, OpenUrl, style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea,
ScrollArea, SelectableLabel, TextEdit, Ui, Widget, SelectableLabel, Ui, Widget,
}; };
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
build::BuildStatus,
diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff}, diff::{display::HighlightKind, ObjDiff, ObjSymbolDiff},
jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult}, jobs::{create_scratch::CreateScratchResult, objdiff::ObjDiffResult, Job, JobQueue, JobResult},
obj::{ obj::{
@ -19,15 +18,10 @@ use crate::{
app::AppStateRef, app::AppStateRef,
hotkeys, hotkeys,
jobs::{is_create_scratch_available, start_create_scratch}, jobs::{is_create_scratch_available, start_create_scratch},
views::{ views::{appearance::Appearance, function_diff::FunctionViewState, write_text},
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionViewState,
write_text,
},
}; };
#[derive(Debug, Clone)] #[derive(Debug, Clone, Eq, PartialEq)]
pub struct SymbolRefByName { pub struct SymbolRefByName {
pub symbol_name: String, pub symbol_name: String,
pub section_name: Option<String>, pub section_name: Option<String>,
@ -79,16 +73,16 @@ pub enum DiffViewAction {
SetShowMappedSymbols(bool), SetShowMappedSymbols(bool),
} }
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default, Eq, PartialEq)]
pub struct DiffViewNavigation { pub struct DiffViewNavigation {
pub view: Option<View>, pub view: View,
pub left_symbol: Option<SymbolRefByName>, pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>, pub right_symbol: Option<SymbolRefByName>,
} }
impl DiffViewNavigation { impl DiffViewNavigation {
pub fn symbol_diff() -> Self { 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( pub fn with_symbols(
@ -107,8 +101,24 @@ impl DiffViewNavigation {
}) })
}); });
match column { match column {
0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 }, 0 => Self { view, left_symbol: symbol1, right_symbol: symbol2 },
1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 }, 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"), _ => unreachable!("Invalid column index"),
} }
} }
@ -151,9 +161,7 @@ impl DiffViewState {
// TODO: where should this go? // TODO: where should this go?
if let Some(result) = self.post_build_nav.take() { if let Some(result) = self.post_build_nav.take() {
if let Some(view) = result.view { self.current_view = result.view;
self.current_view = view;
}
self.symbol_state.left_symbol = result.left_symbol; self.symbol_state.left_symbol = result.left_symbol;
self.symbol_state.right_symbol = result.right_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()) if (nav.left_symbol.is_some() && nav.right_symbol.is_some())
|| (nav.left_symbol.is_none() && nav.right_symbol.is_none()) || (nav.left_symbol.is_none() && nav.right_symbol.is_none())
|| nav.view != Some(View::FunctionDiff) || nav.view != View::FunctionDiff
{ {
// Regular navigation // Regular navigation
if state.is_selecting_symbol() { if state.is_selecting_symbol() {
@ -228,9 +236,7 @@ impl DiffViewState {
self.post_build_nav = Some(nav); self.post_build_nav = Some(nav);
} else { } else {
// Navigate immediately // Navigate immediately
if let Some(view) = nav.view { self.current_view = nav.view;
self.current_view = view;
}
self.symbol_state.left_symbol = nav.left_symbol; self.symbol_state.left_symbol = nav.left_symbol;
self.symbol_state.right_symbol = nav.right_symbol; self.symbol_state.right_symbol = nav.right_symbol;
} }
@ -301,7 +307,7 @@ impl DiffViewState {
}; };
state.set_selecting_left(&right_ref.symbol_name); state.set_selecting_left(&right_ref.symbol_name);
self.post_build_nav = Some(DiffViewNavigation { self.post_build_nav = Some(DiffViewNavigation {
view: Some(View::FunctionDiff), view: View::FunctionDiff,
left_symbol: None, left_symbol: None,
right_symbol: Some(right_ref), right_symbol: Some(right_ref),
}); });
@ -316,7 +322,7 @@ impl DiffViewState {
}; };
state.set_selecting_right(&left_ref.symbol_name); state.set_selecting_right(&left_ref.symbol_name);
self.post_build_nav = Some(DiffViewNavigation { self.post_build_nav = Some(DiffViewNavigation {
view: Some(View::FunctionDiff), view: View::FunctionDiff,
left_symbol: Some(left_ref), left_symbol: Some(left_ref),
right_symbol: None, right_symbol: None,
}); });
@ -337,7 +343,7 @@ impl DiffViewState {
self.post_build_nav = Some(DiffViewNavigation::symbol_diff()); self.post_build_nav = Some(DiffViewNavigation::symbol_diff());
} else { } else {
self.post_build_nav = Some(DiffViewNavigation { self.post_build_nav = Some(DiffViewNavigation {
view: Some(view), view,
left_symbol: Some(left_ref), left_symbol: Some(left_ref),
right_symbol: Some(right_ref), right_symbol: Some(right_ref),
}); });
@ -409,13 +415,13 @@ fn symbol_context_menu_ui(
let symbol_ref = SymbolRefByName::new(symbol, Some(section)); let symbol_ref = SymbolRefByName::new(symbol, Some(section));
if column == 0 { if column == 0 {
ret = Some(DiffViewNavigation { ret = Some(DiffViewNavigation {
view: Some(View::FunctionDiff), view: View::FunctionDiff,
left_symbol: Some(symbol_ref), left_symbol: Some(symbol_ref),
right_symbol: None, right_symbol: None,
}); });
} else { } else {
ret = Some(DiffViewNavigation { ret = Some(DiffViewNavigation {
view: Some(View::FunctionDiff), view: View::FunctionDiff,
left_symbol: None, left_symbol: None,
right_symbol: Some(symbol_ref), right_symbol: Some(symbol_ref),
}); });
@ -552,13 +558,8 @@ fn symbol_ui(
))); )));
} }
ObjSectionKind::Data => { ObjSectionKind::Data => {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::with_symbols( ret = Some(DiffViewAction::Navigate(DiffViewNavigation::data_diff(
View::DataDiff, section, column,
other_ctx,
symbol,
section,
symbol_diff,
column,
))); )));
} }
ObjSectionKind::Bss => {} ObjSectionKind::Bss => {}
@ -591,9 +592,15 @@ fn symbol_matches_filter(
SymbolFilter::None => true, SymbolFilter::None => true,
SymbolFilter::Search(regex) => { SymbolFilter::Search(regex) => {
regex.is_match(&symbol.name) 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> { pub enum SymbolFilter<'a> {
None, None,
Search(&'a Regex), Search(&'a Regex),
Mapping(SymbolRef), Mapping(SymbolRef, Option<&'a Regex>),
} }
#[must_use] #[must_use]
@ -619,21 +626,23 @@ pub fn symbol_list_ui(
let mut ret = None; let mut ret = None;
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
let mut mapping = BTreeMap::new(); 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; let mut show_mapped_symbols = state.show_mapped_symbols;
if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() { if ui.checkbox(&mut show_mapped_symbols, "Show mapped symbols").changed() {
ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols)); ret = Some(DiffViewAction::SetShowMappedSymbols(show_mapped_symbols));
} }
for mapping_diff in &ctx.diff.mapping_symbols { for mapping_diff in &ctx.diff.mapping_symbols {
if mapping_diff.target_symbol == Some(target_ref) { let symbol = ctx.obj.section_symbol(mapping_diff.symbol_ref).1;
if !show_mapped_symbols { if !symbol_matches_filter(symbol, mapping_diff, filter) {
let symbol_diff = ctx.diff.symbol_diff(mapping_diff.symbol_ref); continue;
if symbol_diff.target_symbol.is_some() {
continue;
}
}
mapping.insert(mapping_diff.symbol_ref, mapping_diff);
} }
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 { } else {
for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) { for (symbol, diff) in ctx.obj.common.iter().zip(&ctx.diff.common) {
@ -819,206 +828,8 @@ pub fn symbol_list_ui(
ret 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)] #[derive(Copy, Clone)]
pub struct SymbolDiffContext<'a> { pub struct SymbolDiffContext<'a> {
pub obj: &'a ObjInfo, pub obj: &'a ObjInfo,
pub diff: &'a ObjDiff, pub diff: &'a ObjDiff,
} }
#[must_use]
pub fn symbol_diff_ui(
ui: &mut Ui,
state: &mut DiffViewState,
appearance: &Appearance,
) -> Option<DiffViewAction> {
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
}