mirror of
https://github.com/encounter/objdiff.git
synced 2025-12-16 08:27:04 +00:00
* Fix missing dependency feature for objdiff-gui * Update .gitignore * Add enter and back hotkeys * Add scroll hotkeys * Add hotkeys to select the next symbol above/below the current one in the listing * Do not clear highlighted symbol when backing out of diff view * Do not clear highlighted symbol when hovering mouse over an unpaired symbol * Auto-scroll the keyboard-selected symbols into view if offscreen * Fix some hotkeys stealing input from focused widgets e.g. The symbol list was stealing the W/S key presses when typing into the symbol filter text edit. If the user actually wants to use these shortcuts while a widget is focused, they can simply press the escape key to unfocus all widgets and then press the shortcut. * Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits * Add space as alternative to enter hotkey This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget. * Add hotkeys to change target and base functions * Split function diff view: Enable PageUp/PageDown/Home/End for scrolling * Add escape as an alternative to back hotkey * Fix auto-scrolling to highlighted symbol only working for the left side The flag is cleared after one scroll to avoid doing it continuously, but this breaks when we need to scroll to both the left and the right symbol at the same time. So now each side has its own flag to keep track of this state independently. * Simplify clearing of the autoscroll flag, remove &mut State * Found a better place to clear the autoscroll flag DiffViewState::post_update is where the flag gets set, so clearing it right before that at the start of the function seems to make the most sense, instead of doing it in App::update.
254 lines
9.4 KiB
Rust
254 lines
9.4 KiB
Rust
use egui::{RichText, 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,
|
|
},
|
|
},
|
|
};
|
|
|
|
fn decode_extab(extab: &ExceptionInfo) -> String {
|
|
let mut text = String::from("");
|
|
|
|
let mut dtor_names: Vec<String> = vec![];
|
|
for dtor in &extab.dtors {
|
|
//For each function name, use the demangled name by default,
|
|
//and if not available fallback to the original name
|
|
let name: String = match &dtor.demangled_name {
|
|
Some(demangled_name) => demangled_name.to_string(),
|
|
None => dtor.name.clone(),
|
|
};
|
|
dtor_names.push(name);
|
|
}
|
|
if let Some(decoded) = extab.data.to_string(dtor_names) {
|
|
text += decoded.as_str();
|
|
}
|
|
|
|
text
|
|
}
|
|
|
|
fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a ExceptionInfo> {
|
|
obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol))
|
|
}
|
|
|
|
fn extab_text_ui(
|
|
ui: &mut egui::Ui,
|
|
ctx: FunctionDiffContext<'_>,
|
|
symbol: &ObjSymbol,
|
|
appearance: &Appearance,
|
|
) -> Option<()> {
|
|
if let Some(extab_entry) = find_extab_entry(ctx.obj, symbol) {
|
|
let text = decode_extab(extab_entry);
|
|
ui.colored_label(appearance.replace_color, &text);
|
|
return Some(());
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
fn extab_ui(
|
|
ui: &mut egui::Ui,
|
|
ctx: FunctionDiffContext<'_>,
|
|
appearance: &Appearance,
|
|
_column: usize,
|
|
) {
|
|
ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| {
|
|
ui.scope(|ui| {
|
|
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
|
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
|
|
|
|
if let Some((_section, symbol)) =
|
|
ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))
|
|
{
|
|
extab_text_ui(ui, ctx, symbol, appearance);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
|
|
#[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
|
|
}
|