Compare commits

..

27 Commits

Author SHA1 Message Date
0899e6779c Add alt shortcut support to many elements 2024-12-01 22:36:15 -07:00
LagoLunatic
95528fa8d2 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.
2024-12-01 22:36:15 -07:00
LagoLunatic
517b84e766 Simplify clearing of the autoscroll flag, remove &mut State 2024-12-01 22:36:15 -07:00
LagoLunatic
45dd73f5a9 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.
2024-12-01 22:36:15 -07:00
LagoLunatic
d7d7a7f14a Add escape as an alternative to back hotkey 2024-12-01 22:36:15 -07:00
LagoLunatic
441b30070e Split function diff view: Enable PageUp/PageDown/Home/End for scrolling 2024-12-01 22:36:15 -07:00
LagoLunatic
28bd7182d1 Add hotkeys to change target and base functions 2024-12-01 22:36:13 -07:00
LagoLunatic
3f03a75825 Add space as alternative to enter hotkey
This is for consistency with egui's builtint enter/space hotkey for interacting with the focused widget.
2024-12-01 22:36:05 -07:00
LagoLunatic
4fb64a3ad4 Add Ctrl+F/S shortcuts for focusing the object and symbol filter text edits 2024-12-01 22:36:05 -07:00
LagoLunatic
77c104c67b 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.
2024-12-01 22:36:05 -07:00
LagoLunatic
046f3d6999 Auto-scroll the keyboard-selected symbols into view if offscreen 2024-12-01 22:36:05 -07:00
LagoLunatic
2427b06584 Do not clear highlighted symbol when hovering mouse over an unpaired symbol 2024-12-01 22:36:05 -07:00
LagoLunatic
157e99de6f Do not clear highlighted symbol when backing out of diff view 2024-12-01 22:36:05 -07:00
LagoLunatic
b571787732 Add hotkeys to select the next symbol above/below the current one in the listing 2024-12-01 22:36:05 -07:00
LagoLunatic
dbf86ec3cf Add scroll hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic
acc1189150 Add enter and back hotkeys 2024-12-01 22:36:05 -07:00
LagoLunatic
123253c322 Update .gitignore 2024-12-01 22:36:05 -07:00
LagoLunatic
ef680a5e7d Fix missing dependency feature for objdiff-gui 2024-12-01 22:36:05 -07:00
7aa878b48e Update all dependencies & clippy fixes 2024-12-01 22:22:35 -07:00
a119d9a6dd Add scratch preset_id field for decomp.me
Resolves #133
2024-11-07 09:27:13 -07:00
robojumper
ebf653816a Combine nested otherwise empty directories in objects view (#137) 2024-11-07 08:21:39 -07:00
424434edd6 Experimental ARM64 support
Based on yaxpeax-arm, but with a heavy dose of
custom code to work around its limitations.

Please report any issues or unhandled relocations.
2024-10-31 17:39:12 -06:00
7f14b684bf Ignore PlainText segments when diffing 2024-10-31 17:27:27 -06:00
c5da7f7dd5 Show diff color when symbols differ 2024-10-31 17:26:59 -06:00
2fd655850a Ignore Absolute relocations and log warning 2024-10-31 17:24:49 -06:00
79bd7317c1 Match BranchDest->Reloc with relaxed relocation diffs 2024-10-31 17:24:33 -06:00
21f8f2407c Relax symbol comparison logic
The Ghidra delinker plugin emits functions with type STT_OBJECT,
rather than STT_FUNC. The current logic was preventing these from
being compared based on their symbol type. Relax this condition
for now.
2024-10-29 22:46:02 -06:00
37 changed files with 4172 additions and 570 deletions

2
.gitignore vendored
View File

@@ -18,4 +18,4 @@ android.keystore
*.frag
*.vert
*.metal
.vscode/launch.json
.vscode/

1167
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,7 @@ strip = "debuginfo"
codegen-units = 1
[workspace.package]
version = "2.3.4"
version = "2.4.0"
authors = ["Luke Street <luke@street.dev>"]
edition = "2021"
license = "MIT OR Apache-2.0"

View File

@@ -20,6 +20,7 @@ Supports:
- MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment)
- ARM (GBA, DS, 3DS)
- ARM64 (Switch, experimental)
See [Usage](#usage) for more information.

View File

@@ -73,6 +73,7 @@ ignore = [
#{ id = "RUSTSEC-0000-0000", reason = "you can specify a reason the advisory is ignored" },
#"a-crate-that-is-yanked@0.1.1", # you can also ignore yanked crate versions if you wish
#{ crate = "a-crate-that-is-yanked@0.1.1", reason = "you can specify why you are ignoring the yanked crate" },
{ id = "RUSTSEC-2024-0384", reason = "Unmaintained indirect dependency" },
]
# If this is true, then cargo deny will use the git executable to fetch advisory database.
# If this is false, then it uses a built-in git library.
@@ -97,7 +98,7 @@ allow = [
"BSL-1.0",
"CC0-1.0",
"MPL-2.0",
"Unicode-DFS-2016",
"Unicode-3.0",
"Zlib",
"0BSD",
"OFL-1.1",

View File

@@ -20,7 +20,7 @@ enable-ansi-support = "0.2"
memmap2 = "0.9"
objdiff-core = { path = "../objdiff-core", features = ["all"] }
prost = "0.13"
ratatui = "0.28"
ratatui = "0.29"
rayon = "1.10"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

View File

@@ -844,10 +844,14 @@ impl FunctionDiffUi {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
}
}
DiffText::Symbol(sym) => {
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
base_color = Color::White;
if let Some(diff) = diff {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()]
} else {
base_color = Color::White;
}
}
DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n)));

View File

@@ -16,7 +16,7 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"]
[features]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "bindings"]
all = ["config", "dwarf", "mips", "ppc", "x86", "arm", "arm64", "bindings"]
any-arch = ["config", "dep:bimap", "dep:strum", "dep:similar", "dep:flagset", "dep:log", "dep:memmap2", "dep:byteorder", "dep:num-traits"] # Implicit, used to check if any arch is enabled
config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"]
@@ -24,6 +24,7 @@ mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"]
arm64 = ["any-arch", "dep:cpp_demangle", "dep:yaxpeax-arch", "dep:yaxpeax-arm"]
bindings = ["dep:serde_json", "dep:prost", "dep:pbjson", "dep:serde", "dep:prost-build", "dep:pbjson-build"]
wasm = ["bindings", "any-arch", "dep:console_error_panic_hook", "dep:console_log", "dep:wasm-bindgen", "dep:tsify-next", "dep:log"]
@@ -76,6 +77,10 @@ msvc-demangler = { version = "0.10", optional = true }
unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", optional = true }
# arm64
yaxpeax-arch = { version = "0.3", default-features = false, features = ["std"], optional = true }
yaxpeax-arm = { version = "0.3", default-features = false, features = ["std"], optional = true }
[build-dependencies]
prost-build = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", optional = true }

View File

@@ -11,4 +11,5 @@ objdiff-core contains the core functionality of [objdiff](https://github.com/enc
- **`ppc`**: Enables the PowerPC backend powered by [ppc750cl](https://github.com/encounter/ppc750cl).
- **`x86`**: Enables the x86 backend powered by [iced-x86](https://crates.io/crates/iced-x86).
- **`arm`**: Enables the ARM backend powered by [unarm](https://github.com/AetiasHax/unarm).
- **`arm64`**: Enables the ARM64 backend powered by [yaxpeax-arm](https://github.com/iximeow/yaxpeax-arm).
- **`bindings`**: Enables serialization and deserialization of objdiff data structures.

View File

@@ -124,11 +124,9 @@ impl ObjArch for ObjArchArm {
.get(&SectionIndex(section_index))
.map(|x| x.as_slice())
.unwrap_or(&fallback_mappings);
let first_mapping_idx =
match mapping_symbols.binary_search_by_key(&start_addr, |x| x.address) {
Ok(idx) => idx,
Err(idx) => idx - 1,
};
let first_mapping_idx = mapping_symbols
.binary_search_by_key(&start_addr, |x| x.address)
.unwrap_or_else(|idx| idx - 1);
let first_mapping = mapping_symbols[first_mapping_idx].mapping;
let mut mappings_iter =
@@ -215,7 +213,7 @@ impl ObjArch for ObjArchArm {
address: address as u64,
size: (parser.address - address) as u8,
op: ins.opcode_id(),
mnemonic: parsed_ins.mnemonic.to_string(),
mnemonic: Cow::Borrowed(parsed_ins.mnemonic),
args,
reloc,
branch_dest,
@@ -234,7 +232,7 @@ impl ObjArch for ObjArchArm {
section: &ObjSection,
address: u64,
reloc: &Relocation,
) -> anyhow::Result<i64> {
) -> Result<i64> {
let address = address as usize;
Ok(match reloc.flags() {
// ARM calls

File diff suppressed because it is too large Load Diff

View File

@@ -119,7 +119,7 @@ impl ObjArch for ObjArchMips {
let op = instruction.unique_id as u16;
ops.push(op);
let mnemonic = instruction.opcode_name().to_string();
let mnemonic = instruction.opcode_name();
let is_branch = instruction.is_branch();
let branch_offset = instruction.branch_offset();
let mut branch_dest = if is_branch {
@@ -202,7 +202,7 @@ impl ObjArch for ObjArchMips {
address: cur_addr as u64,
size: 4,
op,
mnemonic,
mnemonic: Cow::Borrowed(mnemonic),
args,
reloc: reloc.cloned(),
branch_dest,

View File

@@ -12,6 +12,8 @@ use crate::{
#[cfg(feature = "arm")]
mod arm;
#[cfg(feature = "arm64")]
mod arm64;
#[cfg(feature = "mips")]
pub mod mips;
#[cfg(feature = "ppc")]
@@ -165,6 +167,8 @@ pub fn new_arch(object: &File) -> Result<Box<dyn ObjArch>> {
Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
#[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"),
})
}

View File

@@ -143,7 +143,7 @@ impl ObjArch for ObjArchPpc {
insts.push(ObjIns {
address: cur_addr as u64,
size: 4,
mnemonic: simplified.mnemonic.to_string(),
mnemonic: Cow::Borrowed(simplified.mnemonic),
args,
reloc: reloc.cloned(),
op: ins.op as u16,

View File

@@ -51,7 +51,7 @@ impl ObjArch for ObjArchX86 {
address: 0,
size: 0,
op: 0,
mnemonic: String::new(),
mnemonic: Cow::Borrowed("<invalid>"),
args: vec![],
reloc: None,
branch_dest: None,
@@ -76,7 +76,7 @@ impl ObjArch for ObjArchX86 {
address,
size: instruction.len() as u8,
op,
mnemonic: String::new(),
mnemonic: Cow::Borrowed("<invalid>"),
args: vec![],
reloc: reloc.cloned(),
branch_dest: None,
@@ -242,7 +242,8 @@ impl FormatterOutput for InstructionFormatterOutput {
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
self.formatted.push_str(text);
self.ins.mnemonic = text.to_string();
// TODO: can iced-x86 guarantee 'static here?
self.ins.mnemonic = Cow::Owned(text.to_string());
}
fn write_number(

View File

@@ -1,3 +1,4 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use crate::{
diff::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
@@ -132,7 +133,7 @@ impl Instruction {
address: instruction.address,
size: instruction.size as u32,
opcode: instruction.op as u32,
mnemonic: instruction.mnemonic.clone(),
mnemonic: instruction.mnemonic.to_string(),
formatted: instruction.formatted.clone(),
arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)),

View File

@@ -1,3 +1,4 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use std::ops::AddAssign;
use anyhow::{bail, Result};
@@ -173,8 +174,7 @@ impl Report {
continue;
}
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent)
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
}
let mut sub_categories = self
.categories

View File

@@ -165,6 +165,8 @@ pub struct ScratchConfig {
pub ctx_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub preset_id: Option<u32>,
}
pub const CONFIG_FILENAMES: [&str; 3] = ["objdiff.json", "objdiff.yml", "objdiff.yaml"];

View File

@@ -259,11 +259,17 @@ fn arg_eq(
right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
)
}
ObjInsArg::BranchDest(_) => {
ObjInsArg::BranchDest(_) => match right {
// Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
ObjInsArg::BranchDest(_) => {
left_diff.branch_to.as_ref().map(|b| b.ins_idx)
== right_diff.branch_to.as_ref().map(|b| b.ins_idx)
}
// If relocations are relaxed, match if left is a constant and right is a reloc
// Useful for instances where the target object is created without relocations
ObjInsArg::Reloc => config.relax_reloc_diffs,
_ => false,
},
}
}
@@ -293,15 +299,10 @@ fn compare_ins(
) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len()
|| left_ins.op != right_ins.op
// Check if any PlainText segments differ (punctuation and spacing)
// This indicates a more significant difference than a simple arg mismatch
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
_ => true,
})
{
// Count only non-PlainText args
let left_args_count = left_ins.iter_args().count();
let right_args_count = right_ins.iter_args().count();
if left_args_count != right_args_count || left_ins.op != right_ins.op {
// Totally different op
result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1;
@@ -312,7 +313,7 @@ fn compare_ins(
result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1;
}
for (a, b) in left_ins.args.iter().zip(&right_ins.args) {
for (a, b) in left_ins.iter_args().zip(right_ins.iter_args()) {
if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None);
result.right_args_diff.push(None);
@@ -324,8 +325,11 @@ fn compare_ins(
let a_str = match a {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(),
ObjInsArg::BranchDest(arg) => format!("{arg}"),
ObjInsArg::Reloc => left_ins
.reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
};
let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx }
@@ -338,8 +342,11 @@ fn compare_ins(
let b_str = match b {
ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(),
ObjInsArg::BranchDest(arg) => format!("{arg}"),
ObjInsArg::Reloc => right_ins
.reloc
.as_ref()
.map_or_else(|| "<unknown>".to_string(), |r| r.target.name.clone()),
ObjInsArg::BranchDest(arg) => arg.to_string(),
};
let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx }

View File

@@ -22,7 +22,7 @@ pub enum DiffText<'a> {
/// Branch destination
BranchDest(u64, Option<&'a ObjInsArgDiff>),
/// Symbol name
Symbol(&'a ObjSymbol),
Symbol(&'a ObjSymbol, Option<&'a ObjInsArgDiff>),
/// Number of spaces
Spacing(usize),
/// End of line
@@ -58,20 +58,23 @@ pub fn display_diff<E>(
cb(DiffText::Spacing(4))?;
}
cb(DiffText::Opcode(&ins.mnemonic, ins.op))?;
let mut arg_diff_idx = 0; // non-PlainText index
for (i, arg) in ins.args.iter().enumerate() {
if i == 0 {
cb(DiffText::Spacing(1))?;
}
let diff = ins_diff.arg_diff.get(i).and_then(|o| o.as_ref());
let diff = ins_diff.arg_diff.get(arg_diff_idx).and_then(|o| o.as_ref());
match arg {
ObjInsArg::PlainText(s) => {
cb(DiffText::Basic(s))?;
}
ObjInsArg::Arg(v) => {
cb(DiffText::Argument(v, diff))?;
arg_diff_idx += 1;
}
ObjInsArg::Reloc => {
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb)?;
display_reloc_name(ins.reloc.as_ref().unwrap(), &mut cb, diff)?;
arg_diff_idx += 1;
}
ObjInsArg::BranchDest(dest) => {
if let Some(dest) = dest.checked_sub(base_addr) {
@@ -79,6 +82,7 @@ pub fn display_diff<E>(
} else {
cb(DiffText::Basic("<unknown>"))?;
}
arg_diff_idx += 1;
}
}
}
@@ -92,8 +96,9 @@ pub fn display_diff<E>(
fn display_reloc_name<E>(
reloc: &ObjReloc,
mut cb: impl FnMut(DiffText) -> Result<(), E>,
diff: Option<&ObjInsArgDiff>,
) -> Result<(), E> {
cb(DiffText::Symbol(&reloc.target))?;
cb(DiffText::Symbol(&reloc.target, diff))?;
match reloc.addend.cmp(&0i64) {
Ordering::Greater => cb(DiffText::Basic(&format!("+{:#x}", reloc.addend))),
Ordering::Less => cb(DiffText::Basic(&format!("-{:#x}", -reloc.addend))),
@@ -106,7 +111,7 @@ impl PartialEq<DiffText<'_>> for HighlightKind {
match (self, other) {
(HighlightKind::Opcode(a), DiffText::Opcode(_, b)) => a == b,
(HighlightKind::Arg(a), DiffText::Argument(b, _)) => a.loose_eq(b),
(HighlightKind::Symbol(a), DiffText::Symbol(b)) => a == &b.name,
(HighlightKind::Symbol(a), DiffText::Symbol(b, _)) => a == &b.name,
(HighlightKind::Address(a), DiffText::Address(b) | DiffText::BranchDest(b, _)) => {
a == b
}
@@ -124,7 +129,7 @@ impl From<DiffText<'_>> for HighlightKind {
match value {
DiffText::Opcode(_, op) => HighlightKind::Opcode(op),
DiffText::Argument(arg, _) => HighlightKind::Arg(arg.clone()),
DiffText::Symbol(sym) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Symbol(sym, _) => HighlightKind::Symbol(sym.name.to_string()),
DiffText::Address(addr) | DiffText::BranchDest(addr, _) => HighlightKind::Address(addr),
_ => HighlightKind::None,
}

View File

@@ -245,7 +245,7 @@ pub struct ObjInsDiff {
pub branch_from: Option<ObjInsBranchFrom>,
/// Branches to instruction
pub branch_to: Option<ObjInsBranchTo>,
/// Arg diffs
/// Arg diffs (only contains non-PlainText args)
pub arg_diff: Vec<Option<ObjInsArgDiff>>,
}
@@ -577,7 +577,7 @@ fn generate_mapping_symbols(
let Some(base_symbol_ref) = symbol_ref_by_name(base_obj, base_name) else {
return Ok(());
};
let (base_section, base_symbol) = base_obj.section_symbol(base_symbol_ref);
let (base_section, _base_symbol) = base_obj.section_symbol(base_symbol_ref);
let Some(base_section) = base_section else {
return Ok(());
};
@@ -588,9 +588,7 @@ fn generate_mapping_symbols(
for (target_section_index, target_section) in
target_obj.sections.iter().enumerate().filter(|(_, s)| s.kind == base_section.kind)
{
for (target_symbol_index, _target_symbol) in
target_section.symbols.iter().enumerate().filter(|(_, s)| s.kind == base_symbol.kind)
{
for (target_symbol_index, _target_symbol) in target_section.symbols.iter().enumerate() {
let target_symbol_ref =
SymbolRef { section_idx: target_section_index, symbol_idx: target_symbol_index };
match base_section.kind {

View File

@@ -85,6 +85,9 @@ pub enum ObjInsArg {
}
impl ObjInsArg {
#[inline]
pub fn is_plain_text(&self) -> bool { matches!(self, ObjInsArg::PlainText(_)) }
pub fn loose_eq(&self, other: &ObjInsArg) -> bool {
match (self, other) {
(ObjInsArg::Arg(a), ObjInsArg::Arg(b)) => a.loose_eq(b),
@@ -100,7 +103,7 @@ pub struct ObjIns {
pub address: u64,
pub size: u8,
pub op: u16,
pub mnemonic: String,
pub mnemonic: Cow<'static, str>,
pub args: Vec<ObjInsArg>,
pub reloc: Option<ObjReloc>,
pub branch_dest: Option<u64>,
@@ -112,6 +115,14 @@ pub struct ObjIns {
pub orig: Option<String>,
}
impl ObjIns {
/// Iterate over non-PlainText arguments.
#[inline]
pub fn iter_args(&self) -> impl DoubleEndedIterator<Item = &ObjInsArg> {
self.args.iter().filter(|a| !a.is_plain_text())
}
}
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)]
pub enum ObjSymbolKind {
#[default]

View File

@@ -65,10 +65,7 @@ fn to_obj_symbol(
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::Hidden);
}
#[cfg(feature = "ppc")]
if arch
.ppc()
.and_then(|a| a.extab.as_ref())
.map_or(false, |e| e.contains_key(&symbol.index().0))
if arch.ppc().and_then(|a| a.extab.as_ref()).is_some_and(|e| e.contains_key(&symbol.index().0))
{
flags = ObjSymbolFlagSet(flags.0 | ObjSymbolFlags::HasExtra);
}
@@ -335,6 +332,10 @@ fn relocations_by_section(
};
symbol
}
RelocationTarget::Absolute => {
log::warn!("Ignoring absolute relocation @ {}:{:#x}", section.name, address);
continue;
}
_ => bail!("Unhandled relocation target: {:?}", reloc.target()),
};
let flags = reloc.flags(); // TODO validate reloc here?

View File

@@ -25,7 +25,7 @@ wsl = []
[dependencies]
anyhow = "1.0"
bytes = "1.7"
bytes = "1.9"
cfg-if = "1.0"
const_format = "0.2"
cwdemangle = "1.0"
@@ -42,7 +42,7 @@ notify = { git = "https://github.com/notify-rs/notify", rev = "128bf6230c03d39db
objdiff-core = { path = "../objdiff-core", features = ["all"] }
open = "5.3"
png = "0.17"
pollster = "0.3"
pollster = "0.4"
regex = "1.11"
rfd = { version = "0.15" } #, default-features = false, features = ['xdg-portal']
rlwinmdec = "1.0"
@@ -51,7 +51,7 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
shell-escape = "0.1"
strum = { version = "0.26", features = ["derive"] }
tempfile = "3.13"
tempfile = "3.14"
time = { version = "0.3", features = ["formatting", "local-offset"] }
# Keep version in sync with egui
@@ -95,7 +95,7 @@ exec = "0.3"
# native:
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
tracing-subscriber = "0.3"
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# web:
[target.'cfg(target_arch = "wasm32")'.dependencies]

View File

@@ -25,6 +25,7 @@ use time::UtcOffset;
use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{load_project_config, ProjectObjectNode},
hotkeys,
jobs::{
objdiff::{start_build, ObjDiffConfig},
Job, JobQueue, JobResult, JobStatus,
@@ -527,6 +528,10 @@ impl App {
}
fn post_update(&mut self, ctx: &egui::Context, action: Option<DiffViewAction>) {
if action.is_some() {
ctx.request_repaint();
}
self.appearance.post_update(ctx);
let ViewState { jobs, diff_state, config_state, graphics_state, .. } = &mut self.view_state;
@@ -690,13 +695,13 @@ impl eframe::App for App {
*show_side_panel = !*show_side_panel;
}
ui.separator();
ui.menu_button("File", |ui| {
ui.menu_button(hotkeys::button_alt_text(ui, "_File"), |ui| {
#[cfg(debug_assertions)]
if ui.button("Debug…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Debug…")).clicked() {
*show_debug = !*show_debug;
ui.close_menu();
}
if ui.button("Project…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Project…")).clicked() {
*show_project_config = !*show_project_config;
ui.close_menu();
}
@@ -705,10 +710,11 @@ impl eframe::App for App {
} else {
vec![]
};
let recent_projects_text = hotkeys::button_alt_text(ui, "_Recent Projects…");
if recent_projects.is_empty() {
ui.add_enabled(false, egui::Button::new("Recent projects"));
ui.add_enabled(false, egui::Button::new(recent_projects_text));
} else {
ui.menu_button("Recent Projects", |ui| {
ui.menu_button(recent_projects_text, |ui| {
if ui.button("Clear").clicked() {
state.write().unwrap().config.recent_projects.clear();
};
@@ -721,36 +727,39 @@ impl eframe::App for App {
}
});
}
if ui.button("Appearance…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Appearance…")).clicked() {
*show_appearance_config = !*show_appearance_config;
ui.close_menu();
}
if ui.button("Graphics…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Graphics…")).clicked() {
*show_graphics = !*show_graphics;
ui.close_menu();
}
if ui.button("Quit").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Quit")).clicked() {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
ui.menu_button("Tools", |ui| {
if ui.button("Demangle…").clicked() {
ui.menu_button(hotkeys::button_alt_text(ui, "_Tools"), |ui| {
if ui.button(hotkeys::button_alt_text(ui, "_Demangle…")).clicked() {
*show_demangle = !*show_demangle;
ui.close_menu();
}
if ui.button("Rlwinm Decoder…").clicked() {
if ui.button(hotkeys::button_alt_text(ui, "_Rlwinm Decoder…")).clicked() {
*show_rlwinm_decode = !*show_rlwinm_decode;
ui.close_menu();
}
});
ui.menu_button("Diff Options", |ui| {
if ui.button("Arch Settings…").clicked() {
ui.menu_button(hotkeys::button_alt_text(ui, "_Diff Options"), |ui| {
if ui.button(hotkeys::button_alt_text(ui, "_Arch Settings…")).clicked() {
*show_arch_config = !*show_arch_config;
ui.close_menu();
}
let mut state = state.write().unwrap();
let response = ui
.checkbox(&mut state.config.rebuild_on_changes, "Rebuild on changes")
.checkbox(
&mut state.config.rebuild_on_changes,
hotkeys::button_alt_text(ui, "_Rebuild on changes"),
)
.on_hover_text("Automatically re-run the build & diff when files change.");
if response.changed() {
state.watcher_change = true;
@@ -759,18 +768,21 @@ impl eframe::App for App {
!diff_state.symbol_state.disable_reverse_fn_order,
egui::Checkbox::new(
&mut diff_state.symbol_state.reverse_fn_order,
"Reverse function order (-inline deferred)",
hotkeys::button_alt_text(
ui,
"Reverse function order (-inline _deferred)",
),
),
)
.on_disabled_hover_text(CONFIG_DISABLED_TEXT);
ui.checkbox(
&mut diff_state.symbol_state.show_hidden_symbols,
"Show hidden symbols",
hotkeys::button_alt_text(ui, "Show _hidden symbols"),
);
if ui
.checkbox(
&mut state.config.diff_obj_config.relax_reloc_diffs,
"Relax relocation diffs",
hotkeys::button_alt_text(ui, "Rela_x relocation diffs"),
)
.on_hover_text(
"Ignores differences in relocation targets. (Address, name, etc)",
@@ -782,7 +794,7 @@ impl eframe::App for App {
if ui
.checkbox(
&mut state.config.diff_obj_config.space_between_args,
"Space between args",
hotkeys::button_alt_text(ui, "_Space between args"),
)
.changed()
{
@@ -791,14 +803,17 @@ impl eframe::App for App {
if ui
.checkbox(
&mut state.config.diff_obj_config.combine_data_sections,
"Combine data sections",
hotkeys::button_alt_text(ui, "Combine _data sections"),
)
.on_hover_text("Combines data sections with equal names.")
.changed()
{
state.queue_reload = true;
}
if ui.button("Clear custom symbol mappings").clicked() {
if ui
.button(hotkeys::button_alt_text(ui, "_Clear custom symbol mappings"))
.clicked()
{
state.clear_mappings();
diff_state.post_build_nav = Some(DiffViewNavigation::symbol_diff());
state.queue_reload = true;

View File

@@ -71,6 +71,7 @@ impl ScratchConfigV1 {
c_flags: self.c_flags,
ctx_path: self.ctx_path,
build_ctx: self.build_ctx.then_some(true),
preset_id: None,
}
}
}

View File

@@ -12,6 +12,21 @@ pub enum ProjectObjectNode {
Dir(String, Vec<ProjectObjectNode>),
}
fn join_single_dir_entries(nodes: &mut Vec<ProjectObjectNode>) {
for node in nodes {
if let ProjectObjectNode::Dir(my_name, my_nodes) = node {
join_single_dir_entries(my_nodes);
// If this directory consists of a single sub-directory...
if let [ProjectObjectNode::Dir(sub_name, sub_nodes)] = &mut my_nodes[..] {
// ... join the two names with a path separator and eliminate the layer
*my_name += "/";
*my_name += sub_name;
*my_nodes = std::mem::take(sub_nodes);
}
}
}
}
fn find_dir<'a>(
name: &str,
nodes: &'a mut Vec<ProjectObjectNode>,
@@ -60,6 +75,14 @@ fn build_nodes(
let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::Unit(filename, idx));
}
// Within the top-level module directories, join paths. Leave the
// top-level name intact though since it's the module name.
for node in &mut nodes {
if let ProjectObjectNode::Dir(_, sub_nodes) = node {
join_single_dir_entries(sub_nodes);
}
}
nodes
}

228
objdiff-gui/src/hotkeys.rs Normal file
View File

@@ -0,0 +1,228 @@
use egui::{
style::ScrollAnimation, text::LayoutJob, vec2, Align, Context, FontSelection, Key,
KeyboardShortcut, Modifiers, PointerButton, RichText, Ui, WidgetText,
};
fn any_widget_focused(ctx: &Context) -> bool { ctx.memory(|mem| mem.focused().is_some()) }
pub fn enter_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Enter)
|| i.key_pressed(Key::Space)
|| i.pointer.button_pressed(PointerButton::Extra2)
})
}
pub fn back_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.key_pressed(Key::Backspace)
|| i.key_pressed(Key::Escape)
|| i.pointer.button_pressed(PointerButton::Extra1)
})
}
pub fn up_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowUp) || i.key_pressed(Key::W))
}
pub fn down_pressed(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| i.key_pressed(Key::ArrowDown) || i.key_pressed(Key::S))
}
pub fn page_up_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageUp)) }
pub fn page_down_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::PageDown)) }
pub fn home_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::Home)) }
pub fn end_pressed(ctx: &Context) -> bool { ctx.input_mut(|i| i.key_pressed(Key::End)) }
pub fn check_scroll_hotkeys(ui: &mut egui::Ui, include_small_increments: bool) {
let ui_height = ui.available_rect_before_wrap().height();
if up_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, ui_height / 10.0), ScrollAnimation::none());
} else if down_pressed(ui.ctx()) && include_small_increments {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height / 10.0), ScrollAnimation::none());
} else if page_up_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, ui_height), ScrollAnimation::none());
} else if page_down_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -ui_height), ScrollAnimation::none());
} else if home_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, f32::INFINITY), ScrollAnimation::none());
} else if end_pressed(ui.ctx()) {
ui.scroll_with_delta_animation(vec2(0.0, -f32::INFINITY), ScrollAnimation::none());
}
}
pub fn consume_up_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowUp) || i.consume_key(Modifiers::NONE, Key::W)
})
}
pub fn consume_down_key(ctx: &Context) -> bool {
if any_widget_focused(ctx) {
return false;
}
ctx.input_mut(|i| {
i.consume_key(Modifiers::NONE, Key::ArrowDown) || i.consume_key(Modifiers::NONE, Key::S)
})
}
const OBJECT_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::F);
pub fn consume_object_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&OBJECT_FILTER_SHORTCUT))
}
const SYMBOL_FILTER_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::S);
pub fn consume_symbol_filter_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&SYMBOL_FILTER_SHORTCUT))
}
const CHANGE_TARGET_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::T);
pub fn consume_change_target_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_TARGET_SHORTCUT))
}
const CHANGE_BASE_SHORTCUT: KeyboardShortcut = KeyboardShortcut::new(Modifiers::CTRL, Key::B);
pub fn consume_change_base_shortcut(ctx: &Context) -> bool {
ctx.input_mut(|i| i.consume_shortcut(&CHANGE_BASE_SHORTCUT))
}
fn shortcut_key(text: &str) -> (usize, char, Key) {
let i = text.chars().position(|c| c == '_').expect("No underscore in text");
let key = text.chars().nth(i + 1).expect("No character after underscore");
let shortcut_key = match key {
'a' | 'A' => Key::A,
'b' | 'B' => Key::B,
'c' | 'C' => Key::C,
'd' | 'D' => Key::D,
'e' | 'E' => Key::E,
'f' | 'F' => Key::F,
'g' | 'G' => Key::G,
'h' | 'H' => Key::H,
'i' | 'I' => Key::I,
'j' | 'J' => Key::J,
'k' | 'K' => Key::K,
'l' | 'L' => Key::L,
'm' | 'M' => Key::M,
'n' | 'N' => Key::N,
'o' | 'O' => Key::O,
'p' | 'P' => Key::P,
'q' | 'Q' => Key::Q,
'r' | 'R' => Key::R,
's' | 'S' => Key::S,
't' | 'T' => Key::T,
'u' | 'U' => Key::U,
'v' | 'V' => Key::V,
'w' | 'W' => Key::W,
'x' | 'X' => Key::X,
'y' | 'Y' => Key::Y,
'z' | 'Z' => Key::Z,
_ => panic!("Invalid key {}", key),
};
(i, key, shortcut_key)
}
fn alt_text_ui(ui: &Ui, text: &str, i: usize, key: char, interactive: bool) -> WidgetText {
let color = if interactive {
ui.visuals().widgets.inactive.text_color()
} else {
ui.visuals().widgets.noninteractive.text_color()
};
let mut job = LayoutJob::default();
if i > 0 {
let text = &text[..i];
RichText::new(text).color(color).append_to(
&mut job,
ui.style(),
FontSelection::Default,
Align::Center,
);
}
let mut rt = RichText::new(key).color(color);
if ui.input(|i| i.modifiers.alt) {
rt = rt.underline();
}
rt.append_to(&mut job, ui.style(), FontSelection::Default, Align::Center);
if i < text.len() - 1 {
let text = &text[i + 2..];
RichText::new(text).color(color).append_to(
&mut job,
ui.style(),
FontSelection::Default,
Align::Center,
);
}
job.into()
}
pub fn button_alt_text(ui: &Ui, text: &str) -> WidgetText {
let (n, c, key) = shortcut_key(text);
let result = alt_text_ui(ui, text, n, c, true);
if ui.input_mut(|i| check_alt_key(i, c, key)) {
let btn_id = ui.next_auto_id();
ui.memory_mut(|m| m.request_focus(btn_id));
ui.input_mut(|i| {
i.events.push(egui::Event::Key {
key: Key::Enter,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Default::default(),
})
});
}
result
}
pub fn alt_text(ui: &Ui, text: &str, enter: bool) -> WidgetText {
let (n, c, key) = shortcut_key(text);
let result = alt_text_ui(ui, text, n, c, false);
if ui.input_mut(|i| check_alt_key(i, c, key)) {
let btn_id = ui.next_auto_id();
ui.memory_mut(|m| m.request_focus(btn_id));
if enter {
ui.input_mut(|i| {
i.events.push(egui::Event::Key {
key: Key::Enter,
physical_key: None,
pressed: true,
repeat: false,
modifiers: Default::default(),
})
});
}
}
result
}
fn check_alt_key(i: &mut egui::InputState, c: char, key: Key) -> bool {
if i.consume_key(Modifiers::ALT, key) {
// Remove any text input events that match the key
let cs = c.to_string();
i.events.retain(|e| !matches!(e, egui::Event::Text(t) if *t == cs));
true
} else {
false
}
}

View File

@@ -23,6 +23,7 @@ pub struct CreateScratchConfig {
pub compiler_flags: String,
pub function_name: String,
pub target_obj: PathBuf,
pub preset_id: Option<u32>,
}
impl CreateScratchConfig {
@@ -45,6 +46,7 @@ impl CreateScratchConfig {
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name,
target_obj: target_path.to_path_buf(),
preset_id: scratch_config.preset_id,
})
}
@@ -101,15 +103,18 @@ fn run_create_scratch(
let obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?;
let form = reqwest::blocking::multipart::Form::new()
let mut form = reqwest::blocking::multipart::Form::new()
.text("compiler", config.compiler.clone())
.text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here")
.part("target_obj", file);
.text("source_code", "// Move related code from Context tab to here");
if let Some(preset) = config.preset_id {
form = form.text("preset", preset.to_string());
}
form = form.part("target_obj", file);
let client = reqwest::blocking::Client::new();
let response = client
.post(formatcp!("{API_HOST}/api/scratch"))

View File

@@ -152,16 +152,14 @@ fn start_job(
let context = JobContext { status: status.clone(), egui: ctx.clone() };
let context_inner = JobContext { status: status.clone(), egui: ctx.clone() };
let (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || {
return match run(context_inner, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
w.error = Some(e);
}
JobResult::None
let handle = std::thread::spawn(move || match run(context_inner, rx) {
Ok(state) => state,
Err(e) => {
if let Ok(mut w) = status.write() {
w.error = Some(e);
}
};
JobResult::None
}
});
let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id);

View File

@@ -4,6 +4,7 @@ mod app;
mod app_config;
mod config;
mod fonts;
mod hotkeys;
mod jobs;
mod update;
mod views;

View File

@@ -21,6 +21,7 @@ use strum::{EnumMessage, VariantArray};
use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode,
hotkeys,
jobs::{
check_update::{start_check_update, CheckUpdateResult},
update::start_update,
@@ -218,7 +219,7 @@ pub fn config_ui(
ui.horizontal(|ui| {
ui.heading("Project");
if ui.button(RichText::new("Settings")).clicked() {
if ui.button("Settings").clicked() {
*show_config_window = true;
}
});
@@ -254,7 +255,12 @@ pub fn config_ui(
}
} else {
let had_search = !config_state.object_search.is_empty();
egui::TextEdit::singleline(&mut config_state.object_search).hint_text("Filter").ui(ui);
let response = egui::TextEdit::singleline(&mut config_state.object_search)
.hint_text(hotkeys::alt_text(ui, "Filter _objects", false))
.ui(ui);
if hotkeys::consume_object_filter_shortcut(ui.ctx()) {
response.request_focus();
}
let mut root_open = None;
let mut node_open = NodeOpen::Default;

View File

@@ -7,11 +7,14 @@ use objdiff_core::{
};
use time::format_description;
use crate::views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
use crate::{
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
},
};
const BYTES_PER_ROW: usize = 16;
@@ -176,6 +179,8 @@ fn data_table_ui(
let left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_section.map(|(_, section)| split_diffs(&section.data_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;
@@ -213,8 +218,8 @@ pub fn data_diff_ui(
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.map_or(false, |ctx| ctx.has_section())
&& !left_ctx.map_or(false, |ctx| ctx.has_section())
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()));
}
@@ -224,7 +229,7 @@ pub fn data_diff_ui(
render_header(ui, available_width, 2, |ui, column| {
if column == 0 {
// Left column
if ui.button("⏴ Back").clicked() {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}

View File

@@ -5,13 +5,16 @@ use objdiff_core::{
};
use time::format_description;
use crate::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::{
hotkeys,
views::{
appearance::Appearance,
column_layout::{render_header, render_strips},
function_diff::FunctionDiffContext,
symbol_diff::{
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState,
SymbolRefByName, View,
},
},
};
@@ -101,7 +104,7 @@ pub fn extab_diff_ui(
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.map_or(false, |ctx| !ctx.has_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);
@@ -111,7 +114,7 @@ pub fn extab_diff_ui(
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
} 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);
@@ -124,8 +127,8 @@ pub fn extab_diff_ui(
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
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()));
}
@@ -136,7 +139,7 @@ pub fn extab_diff_ui(
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
@@ -144,7 +147,7 @@ pub fn extab_diff_ui(
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
&& 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)")
@@ -232,6 +235,8 @@ pub fn extab_diff_ui(
}
});
hotkeys::check_scroll_hotkeys(ui, true);
// Table
render_strips(ui, available_width, 2, |ui, column| {
if column == 0 {

View File

@@ -14,12 +14,15 @@ use objdiff_core::{
};
use time::format_description;
use crate::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::{
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,
},
},
};
@@ -292,10 +295,14 @@ fn diff_text_ui(
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
}
}
DiffText::Symbol(sym) => {
DiffText::Symbol(sym, diff) => {
let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone();
base_color = appearance.emphasized_text_color;
if let Some(diff) = diff {
base_color = appearance.diff_colors[diff.idx % appearance.diff_colors.len()]
} else {
base_color = appearance.emphasized_text_color;
}
}
DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width);
@@ -428,6 +435,7 @@ fn asm_table_ui(
};
if left_len.is_some() && right_len.is_some() {
// Joint view
hotkeys::check_scroll_hotkeys(ui, true);
render_table(
ui,
available_width,
@@ -463,6 +471,7 @@ fn asm_table_ui(
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,
@@ -508,9 +517,6 @@ fn asm_table_ui(
SymbolRefByName::new(right_symbol, right_section),
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
@@ -523,6 +529,7 @@ fn asm_table_ui(
} 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,
@@ -568,9 +575,6 @@ fn asm_table_ui(
right_symbol_ref,
));
}
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => {
ret = Some(action);
}
@@ -636,7 +640,7 @@ pub fn function_diff_ui(
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.map_or(false, |ctx| !ctx.has_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);
@@ -646,7 +650,7 @@ pub fn function_diff_ui(
left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref),
}));
} else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) {
} 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);
@@ -659,8 +663,8 @@ pub fn function_diff_ui(
}
// If both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol())
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()));
}
@@ -671,7 +675,7 @@ pub fn function_diff_ui(
if column == 0 {
// Left column
ui.horizontal(|ui| {
if ui.button("⏴ Back").clicked() {
if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
}
ui.separator();
@@ -679,7 +683,7 @@ pub fn function_diff_ui(
.add_enabled(
!state.scratch_running
&& state.scratch_available
&& left_ctx.map_or(false, |ctx| ctx.has_symbol()),
&& 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)")
@@ -703,11 +707,12 @@ pub fn function_diff_ui(
.font(appearance.code_font.clone())
.color(appearance.highlight_color),
);
if right_ctx.map_or(false, |m| m.has_symbol())
&& ui
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()));
@@ -773,7 +778,7 @@ pub fn function_diff_ui(
.color(match_color_for_symbol(match_percent, appearance)),
);
}
if left_ctx.map_or(false, |m| m.has_symbol()) {
if left_ctx.is_some_and(|m| m.has_symbol()) {
ui.separator();
if ui
.button("Change base")
@@ -781,6 +786,7 @@ pub fn function_diff_ui(
"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()));

View File

@@ -3,6 +3,7 @@ use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget};
use crate::{
hotkeys,
jobs::{JobQueue, JobStatus},
views::appearance::Appearance,
};
@@ -94,7 +95,14 @@ impl From<&JobStatus> for JobStatusDisplay {
}
pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance) -> bool {
ui.label("Jobs:");
let mut clicked = false;
if egui::Label::new(hotkeys::alt_text(ui, "_Jobs:", true))
.sense(egui::Sense::click())
.ui(ui)
.clicked()
{
clicked = true;
}
let mut statuses = Vec::new();
for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else {
@@ -105,7 +113,6 @@ pub fn jobs_menu_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appeara
let running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) {

View File

@@ -1,8 +1,8 @@
use std::{collections::BTreeMap, mem::take};
use std::{collections::BTreeMap, mem::take, ops::Bound};
use egui::{
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit,
Ui, Widget,
style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea,
SelectableLabel, TextEdit, Ui, Widget,
};
use objdiff_core::{
arch::ObjArch,
@@ -15,6 +15,7 @@ use regex::{Regex, RegexBuilder};
use crate::{
app::AppStateRef,
hotkeys,
jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult},
@@ -56,8 +57,8 @@ pub enum DiffViewAction {
Build,
/// Navigate to a new diff view
Navigate(DiffViewNavigation),
/// Set the highlighted symbols in the symbols view
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>),
/// Set the highlighted symbols in the symbols view, optionally scrolling them into view.
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>, bool),
/// Set the symbols view search filter
SetSearch(String),
/// Submit the current function to decomp.me
@@ -135,6 +136,7 @@ pub struct DiffViewState {
#[derive(Default)]
pub struct SymbolViewState {
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub autoscroll_to_highlighted_symbols: bool,
pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool,
@@ -197,6 +199,9 @@ impl DiffViewState {
ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url)));
}
// Clear the autoscroll flag so that it doesn't scroll continuously.
self.symbol_state.autoscroll_to_highlighted_symbols = false;
let Some(action) = action else {
return;
};
@@ -211,7 +216,6 @@ impl DiffViewState {
// Ignore action if we're already navigating
return;
}
self.symbol_state.highlighted_symbol = (None, None);
let Ok(mut state) = state.write() else {
return;
};
@@ -247,8 +251,9 @@ impl DiffViewState {
self.post_build_nav = Some(nav);
}
}
DiffViewAction::SetSymbolHighlight(left, right) => {
DiffViewAction::SetSymbolHighlight(left, right, autoscroll) => {
self.symbol_state.highlighted_symbol = (left, right);
self.symbol_state.autoscroll_to_highlighted_symbols = autoscroll;
}
DiffViewAction::SetSearch(search) => {
self.search_regex = if search.is_empty() {
@@ -534,7 +539,15 @@ fn symbol_ui(
ret = Some(DiffViewAction::Navigate(result));
}
});
if response.clicked() {
if selected && state.autoscroll_to_highlighted_symbols {
// Automatically scroll the view to encompass the selected symbol in case the user selected
// an offscreen symbol by using a keyboard shortcut.
ui.scroll_to_rect_animation(response.rect, None, ScrollAnimation::none());
// This autoscroll state flag will be reset in DiffViewState::post_update at the end of
// every frame so that we don't continuously scroll the view back when the user is trying to
// manually scroll away.
}
if response.clicked() || (selected && hotkeys::enter_pressed(ui.ctx())) {
if let Some(section) = section {
match section.kind {
ObjSectionKind::Code => {
@@ -561,20 +574,18 @@ fn symbol_ui(
}
}
} else if response.hovered() {
ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol {
if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
Some(target_symbol),
)
} else {
DiffViewAction::SetSymbolHighlight(
Some(target_symbol),
Some(symbol_diff.symbol_ref),
)
}
ret = Some(if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(symbol_diff.symbol_ref),
symbol_diff.target_symbol,
false,
)
} else {
DiffViewAction::SetSymbolHighlight(None, None)
DiffViewAction::SetSymbolHighlight(
symbol_diff.target_symbol,
Some(symbol_diff.symbol_ref),
false,
)
});
}
ret
@@ -648,6 +659,58 @@ pub fn symbol_list_ui(
}
}
hotkeys::check_scroll_hotkeys(ui, false);
let mut new_key_value_to_highlight = None;
if let Some(sym_ref) =
if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 }
{
let up = if hotkeys::consume_up_key(ui.ctx()) {
Some(true)
} else if hotkeys::consume_down_key(ui.ctx()) {
Some(false)
} else {
None
};
if let Some(mut up) = up {
if state.reverse_fn_order {
up = !up;
}
new_key_value_to_highlight = if up {
mapping.range(..sym_ref).next_back()
} else {
mapping.range((Bound::Excluded(sym_ref), Bound::Unbounded)).next()
};
};
} else {
// No symbol is highlighted in this column. Select the topmost symbol instead.
// Note that we intentionally do not consume the up/down key presses in this case, but
// we do when a symbol is highlighted. This is so that if only one column has a symbol
// highlighted, that one takes precedence over the one with nothing highlighted.
if hotkeys::up_pressed(ui.ctx()) || hotkeys::down_pressed(ui.ctx()) {
new_key_value_to_highlight = if state.reverse_fn_order {
mapping.last_key_value()
} else {
mapping.first_key_value()
};
}
}
if let Some((new_sym_ref, new_symbol_diff)) = new_key_value_to_highlight {
ret = Some(if column == 0 {
DiffViewAction::SetSymbolHighlight(
Some(*new_sym_ref),
new_symbol_diff.target_symbol,
true,
)
} else {
DiffViewAction::SetSymbolHighlight(
new_symbol_diff.target_symbol,
Some(*new_sym_ref),
true,
)
});
}
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
@@ -838,7 +901,13 @@ pub fn symbol_diff_ui(
});
let mut search = state.search.clone();
if TextEdit::singleline(&mut search).hint_text("Filter symbols").ui(ui).changed() {
let response = TextEdit::singleline(&mut search)
.hint_text(hotkeys::alt_text(ui, "Filter _symbols", false))
.ui(ui);
if hotkeys::consume_symbol_filter_shortcut(ui.ctx()) {
response.request_focus();
}
if response.changed() {
ret = Some(DiffViewAction::SetSearch(search));
}
} else if column == 1 {