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 *.frag
*.vert *.vert
*.metal *.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 codegen-units = 1
[workspace.package] [workspace.package]
version = "2.3.4" version = "2.4.0"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
edition = "2021" edition = "2021"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"

View File

@@ -20,6 +20,7 @@ Supports:
- MIPS (N64, PS1, PS2, PSP) - MIPS (N64, PS1, PS2, PSP)
- x86 (COFF only at the moment) - x86 (COFF only at the moment)
- ARM (GBA, DS, 3DS) - ARM (GBA, DS, 3DS)
- ARM64 (Switch, experimental)
See [Usage](#usage) for more information. 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" }, #{ 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 #"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" }, #{ 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 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. # If this is false, then it uses a built-in git library.
@@ -97,7 +98,7 @@ allow = [
"BSL-1.0", "BSL-1.0",
"CC0-1.0", "CC0-1.0",
"MPL-2.0", "MPL-2.0",
"Unicode-DFS-2016", "Unicode-3.0",
"Zlib", "Zlib",
"0BSD", "0BSD",
"OFL-1.1", "OFL-1.1",

View File

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

View File

@@ -844,10 +844,14 @@ impl FunctionDiffUi {
base_color = COLOR_ROTATION[diff.idx % COLOR_ROTATION.len()] 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); let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone(); 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) => { DiffText::Spacing(n) => {
line.spans.push(Span::raw(" ".repeat(n))); line.spans.push(Span::raw(" ".repeat(n)));

View File

@@ -16,7 +16,7 @@ documentation = "https://docs.rs/objdiff-core"
crate-type = ["cdylib", "rlib"] crate-type = ["cdylib", "rlib"]
[features] [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 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"] config = ["dep:bimap", "dep:globset", "dep:semver", "dep:serde_json", "dep:serde_yaml", "dep:serde", "dep:filetime"]
dwarf = ["dep:gimli"] dwarf = ["dep:gimli"]
@@ -24,6 +24,7 @@ mips = ["any-arch", "dep:rabbitizer"]
ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"] ppc = ["any-arch", "dep:cwdemangle", "dep:cwextab", "dep:ppc750cl"]
x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"] x86 = ["any-arch", "dep:cpp_demangle", "dep:iced-x86", "dep:msvc-demangler"]
arm = ["any-arch", "dep:cpp_demangle", "dep:unarm", "dep:arm-attr"] 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"] 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"] 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 } unarm = { version = "1.6", optional = true }
arm-attr = { version = "0.1", 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] [build-dependencies]
prost-build = { version = "0.13", optional = true } prost-build = { version = "0.13", optional = true }
pbjson-build = { version = "0.7", 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). - **`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). - **`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). - **`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. - **`bindings`**: Enables serialization and deserialization of objdiff data structures.

View File

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

View File

@@ -12,6 +12,8 @@ use crate::{
#[cfg(feature = "arm")] #[cfg(feature = "arm")]
mod arm; mod arm;
#[cfg(feature = "arm64")]
mod arm64;
#[cfg(feature = "mips")] #[cfg(feature = "mips")]
pub mod mips; pub mod mips;
#[cfg(feature = "ppc")] #[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)?), Architecture::I386 | Architecture::X86_64 => Box::new(x86::ObjArchX86::new(object)?),
#[cfg(feature = "arm")] #[cfg(feature = "arm")]
Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?), Architecture::Arm => Box::new(arm::ObjArchArm::new(object)?),
#[cfg(feature = "arm64")]
Architecture::Aarch64 => Box::new(arm64::ObjArchArm64::new(object)?),
arch => bail!("Unsupported architecture: {arch:?}"), arch => bail!("Unsupported architecture: {arch:?}"),
}) })
} }

View File

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

View File

@@ -51,7 +51,7 @@ impl ObjArch for ObjArchX86 {
address: 0, address: 0,
size: 0, size: 0,
op: 0, op: 0,
mnemonic: String::new(), mnemonic: Cow::Borrowed("<invalid>"),
args: vec![], args: vec![],
reloc: None, reloc: None,
branch_dest: None, branch_dest: None,
@@ -76,7 +76,7 @@ impl ObjArch for ObjArchX86 {
address, address,
size: instruction.len() as u8, size: instruction.len() as u8,
op, op,
mnemonic: String::new(), mnemonic: Cow::Borrowed("<invalid>"),
args: vec![], args: vec![],
reloc: reloc.cloned(), reloc: reloc.cloned(),
branch_dest: None, branch_dest: None,
@@ -242,7 +242,8 @@ impl FormatterOutput for InstructionFormatterOutput {
fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) { fn write_mnemonic(&mut self, _instruction: &Instruction, text: &str) {
self.formatted.push_str(text); 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( fn write_number(

View File

@@ -1,3 +1,4 @@
#![allow(clippy::needless_lifetimes)] // Generated serde code
use crate::{ use crate::{
diff::{ diff::{
ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo, ObjDataDiff, ObjDataDiffKind, ObjDiff, ObjInsArgDiff, ObjInsBranchFrom, ObjInsBranchTo,
@@ -132,7 +133,7 @@ impl Instruction {
address: instruction.address, address: instruction.address,
size: instruction.size as u32, size: instruction.size as u32,
opcode: instruction.op as u32, opcode: instruction.op as u32,
mnemonic: instruction.mnemonic.clone(), mnemonic: instruction.mnemonic.to_string(),
formatted: instruction.formatted.clone(), formatted: instruction.formatted.clone(),
arguments: instruction.args.iter().map(Argument::new).collect(), arguments: instruction.args.iter().map(Argument::new).collect(),
relocation: instruction.reloc.as_ref().map(|reloc| Relocation::new(object, reloc)), 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 std::ops::AddAssign;
use anyhow::{bail, Result}; use anyhow::{bail, Result};
@@ -173,8 +174,7 @@ impl Report {
continue; continue;
} }
fn is_sub_category(id: &str, parent: &str, sep: char) -> bool { fn is_sub_category(id: &str, parent: &str, sep: char) -> bool {
id.starts_with(parent) id.starts_with(parent) && id.get(parent.len()..).is_some_and(|s| s.starts_with(sep))
&& id.get(parent.len()..).map_or(false, |s| s.starts_with(sep))
} }
let mut sub_categories = self let mut sub_categories = self
.categories .categories

View File

@@ -165,6 +165,8 @@ pub struct ScratchConfig {
pub ctx_path: Option<PathBuf>, pub ctx_path: Option<PathBuf>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub build_ctx: Option<bool>, 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"]; 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()), right_diff.ins.as_ref().and_then(|i| i.reloc.as_ref()),
) )
} }
ObjInsArg::BranchDest(_) => { ObjInsArg::BranchDest(_) => match right {
// Compare dest instruction idx after diffing // Compare dest instruction idx after diffing
left_diff.branch_to.as_ref().map(|b| b.ins_idx) ObjInsArg::BranchDest(_) => {
== right_diff.branch_to.as_ref().map(|b| b.ins_idx) 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> { ) -> Result<InsDiffResult> {
let mut result = InsDiffResult::default(); let mut result = InsDiffResult::default();
if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) { if let (Some(left_ins), Some(right_ins)) = (&left.ins, &right.ins) {
if left_ins.args.len() != right_ins.args.len() // Count only non-PlainText args
|| left_ins.op != right_ins.op let left_args_count = left_ins.iter_args().count();
// Check if any PlainText segments differ (punctuation and spacing) let right_args_count = right_ins.iter_args().count();
// This indicates a more significant difference than a simple arg mismatch if left_args_count != right_args_count || left_ins.op != right_ins.op {
|| !left_ins.args.iter().zip(&right_ins.args).all(|(a, b)| match (a, b) {
(ObjInsArg::PlainText(l), ObjInsArg::PlainText(r)) => l == r,
_ => true,
})
{
// Totally different op // Totally different op
result.kind = ObjInsDiffKind::Replace; result.kind = ObjInsDiffKind::Replace;
state.diff_count += 1; state.diff_count += 1;
@@ -312,7 +313,7 @@ fn compare_ins(
result.kind = ObjInsDiffKind::OpMismatch; result.kind = ObjInsDiffKind::OpMismatch;
state.diff_count += 1; 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) { if arg_eq(config, left_obj, right_obj, a, b, left, right) {
result.left_args_diff.push(None); result.left_args_diff.push(None);
result.right_args_diff.push(None); result.right_args_diff.push(None);
@@ -324,8 +325,11 @@ fn compare_ins(
let a_str = match a { let a_str = match a {
ObjInsArg::PlainText(arg) => arg.to_string(), ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(), ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(), ObjInsArg::Reloc => left_ins
ObjInsArg::BranchDest(arg) => format!("{arg}"), .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) { let a_diff = if let Some(idx) = state.left_args_idx.get(&a_str) {
ObjInsArgDiff { idx: *idx } ObjInsArgDiff { idx: *idx }
@@ -338,8 +342,11 @@ fn compare_ins(
let b_str = match b { let b_str = match b {
ObjInsArg::PlainText(arg) => arg.to_string(), ObjInsArg::PlainText(arg) => arg.to_string(),
ObjInsArg::Arg(arg) => arg.to_string(), ObjInsArg::Arg(arg) => arg.to_string(),
ObjInsArg::Reloc => String::new(), ObjInsArg::Reloc => right_ins
ObjInsArg::BranchDest(arg) => format!("{arg}"), .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) { let b_diff = if let Some(idx) = state.right_args_idx.get(&b_str) {
ObjInsArgDiff { idx: *idx } ObjInsArgDiff { idx: *idx }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,21 @@ pub enum ProjectObjectNode {
Dir(String, Vec<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>( fn find_dir<'a>(
name: &str, name: &str,
nodes: &'a mut Vec<ProjectObjectNode>, nodes: &'a mut Vec<ProjectObjectNode>,
@@ -60,6 +75,14 @@ fn build_nodes(
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::Unit(filename, idx)); 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 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 compiler_flags: String,
pub function_name: String, pub function_name: String,
pub target_obj: PathBuf, pub target_obj: PathBuf,
pub preset_id: Option<u32>,
} }
impl CreateScratchConfig { impl CreateScratchConfig {
@@ -45,6 +46,7 @@ impl CreateScratchConfig {
compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(), compiler_flags: scratch_config.c_flags.clone().unwrap_or_default(),
function_name, function_name,
target_obj: target_path.to_path_buf(), 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 obj_path = project_dir.join(&config.target_obj);
let file = reqwest::blocking::multipart::Part::file(&obj_path) let file = reqwest::blocking::multipart::Part::file(&obj_path)
.with_context(|| format!("Failed to open {}", obj_path.display()))?; .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("compiler", config.compiler.clone())
.text("platform", config.platform.clone()) .text("platform", config.platform.clone())
.text("compiler_flags", config.compiler_flags.clone()) .text("compiler_flags", config.compiler_flags.clone())
.text("diff_label", config.function_name.clone()) .text("diff_label", config.function_name.clone())
.text("diff_flags", diff_flags) .text("diff_flags", diff_flags)
.text("context", context.unwrap_or_default()) .text("context", context.unwrap_or_default())
.text("source_code", "// Move related code from Context tab to here") .text("source_code", "// Move related code from Context tab to here");
.part("target_obj", file); 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 client = reqwest::blocking::Client::new();
let response = client let response = client
.post(formatcp!("{API_HOST}/api/scratch")) .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 = JobContext { status: status.clone(), egui: ctx.clone() };
let context_inner = 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 (tx, rx) = std::sync::mpsc::channel();
let handle = std::thread::spawn(move || { let handle = std::thread::spawn(move || match run(context_inner, rx) {
return match run(context_inner, rx) { Ok(state) => state,
Ok(state) => state, Err(e) => {
Err(e) => { if let Ok(mut w) = status.write() {
if let Ok(mut w) = status.write() { w.error = Some(e);
w.error = Some(e);
}
JobResult::None
} }
}; JobResult::None
}
}); });
let id = JOB_ID.fetch_add(1, Ordering::Relaxed); let id = JOB_ID.fetch_add(1, Ordering::Relaxed);
log::info!("Started job {}", id); log::info!("Started job {}", id);

View File

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

View File

@@ -21,6 +21,7 @@ use strum::{EnumMessage, VariantArray};
use crate::{ use crate::{
app::{AppConfig, AppState, AppStateRef, ObjectConfig}, app::{AppConfig, AppState, AppStateRef, ObjectConfig},
config::ProjectObjectNode, config::ProjectObjectNode,
hotkeys,
jobs::{ jobs::{
check_update::{start_check_update, CheckUpdateResult}, check_update::{start_check_update, CheckUpdateResult},
update::start_update, update::start_update,
@@ -218,7 +219,7 @@ pub fn config_ui(
ui.horizontal(|ui| { ui.horizontal(|ui| {
ui.heading("Project"); ui.heading("Project");
if ui.button(RichText::new("Settings")).clicked() { if ui.button("Settings").clicked() {
*show_config_window = true; *show_config_window = true;
} }
}); });
@@ -254,7 +255,12 @@ pub fn config_ui(
} }
} else { } else {
let had_search = !config_state.object_search.is_empty(); 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 root_open = None;
let mut node_open = NodeOpen::Default; let mut node_open = NodeOpen::Default;

View File

@@ -7,11 +7,14 @@ use objdiff_core::{
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
appearance::Appearance, hotkeys,
column_layout::{render_header, render_table}, views::{
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState}, appearance::Appearance,
write_text, column_layout::{render_header, render_table},
symbol_diff::{DiffViewAction, DiffViewNavigation, DiffViewState},
write_text,
},
}; };
const BYTES_PER_ROW: usize = 16; 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 left_diffs = left_section.map(|(_, section)| split_diffs(&section.data_diff));
let right_diffs = right_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| { render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| {
let i = row.index(); let i = row.index();
let address = i * BYTES_PER_ROW; 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); 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 both sides are missing a symbol, switch to symbol diff view
if !right_ctx.map_or(false, |ctx| ctx.has_section()) if !right_ctx.is_some_and(|ctx| ctx.has_section())
&& !left_ctx.map_or(false, |ctx| ctx.has_section()) && !left_ctx.is_some_and(|ctx| ctx.has_section())
{ {
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
@@ -224,7 +229,7 @@ pub fn data_diff_ui(
render_header(ui, available_width, 2, |ui, column| { render_header(ui, available_width, 2, |ui, column| {
if column == 0 { if column == 0 {
// Left column // Left column
if ui.button("⏴ Back").clicked() { if ui.button("⏴ Back").clicked() || hotkeys::back_pressed(ui.ctx()) {
ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }

View File

@@ -5,13 +5,16 @@ use objdiff_core::{
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
appearance::Appearance, hotkeys,
column_layout::{render_header, render_strips}, views::{
function_diff::FunctionDiffContext, appearance::Appearance,
symbol_diff::{ column_layout::{render_header, render_strips},
match_color_for_symbol, DiffViewAction, DiffViewNavigation, DiffViewState, SymbolRefByName, function_diff::FunctionDiffContext,
View, 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| { let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) 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) = let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section); 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(), left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref), 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) = let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section); 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 both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol()) if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol()) && left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{ {
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
@@ -136,7 +139,7 @@ pub fn extab_diff_ui(
if column == 0 { if column == 0 {
// Left column // Left column
ui.horizontal(|ui| { 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())); ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
ui.separator(); ui.separator();
@@ -144,7 +147,7 @@ pub fn extab_diff_ui(
.add_enabled( .add_enabled(
!state.scratch_running !state.scratch_running
&& state.scratch_available && 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"), egui::Button::new("📲 decomp.me"),
) )
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") .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 // Table
render_strips(ui, available_width, 2, |ui, column| { render_strips(ui, available_width, 2, |ui, column| {
if column == 0 { if column == 0 {

View File

@@ -14,12 +14,15 @@ use objdiff_core::{
}; };
use time::format_description; use time::format_description;
use crate::views::{ use crate::{
appearance::Appearance, hotkeys,
column_layout::{render_header, render_strips, render_table}, views::{
symbol_diff::{ appearance::Appearance,
match_color_for_symbol, symbol_list_ui, DiffViewAction, DiffViewNavigation, DiffViewState, column_layout::{render_header, render_strips, render_table},
SymbolDiffContext, SymbolFilter, SymbolRefByName, SymbolViewState, View, 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()] 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); let name = sym.demangled_name.as_ref().unwrap_or(&sym.name);
label_text = name.clone(); 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) => { DiffText::Spacing(n) => {
ui.add_space(n as f32 * space_width); 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() { if left_len.is_some() && right_len.is_some() {
// Joint view // Joint view
hotkeys::check_scroll_hotkeys(ui, true);
render_table( render_table(
ui, ui,
available_width, available_width,
@@ -463,6 +471,7 @@ fn asm_table_ui(
if column == 0 { if column == 0 {
if let Some(ctx) = left_ctx { if let Some(ctx) = left_ctx {
if ctx.has_symbol() { if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table( render_table(
ui, ui,
available_width / 2.0, available_width / 2.0,
@@ -508,9 +517,6 @@ fn asm_table_ui(
SymbolRefByName::new(right_symbol, right_section), SymbolRefByName::new(right_symbol, right_section),
)); ));
} }
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => { _ => {
ret = Some(action); ret = Some(action);
} }
@@ -523,6 +529,7 @@ fn asm_table_ui(
} else if column == 1 { } else if column == 1 {
if let Some(ctx) = right_ctx { if let Some(ctx) = right_ctx {
if ctx.has_symbol() { if ctx.has_symbol() {
hotkeys::check_scroll_hotkeys(ui, false);
render_table( render_table(
ui, ui,
available_width / 2.0, available_width / 2.0,
@@ -568,9 +575,6 @@ fn asm_table_ui(
right_symbol_ref, right_symbol_ref,
)); ));
} }
DiffViewAction::SetSymbolHighlight(_, _) => {
// Ignore
}
_ => { _ => {
ret = Some(action); ret = Some(action);
} }
@@ -636,7 +640,7 @@ pub fn function_diff_ui(
let right_diff_symbol = right_ctx.and_then(|ctx| { let right_diff_symbol = right_ctx.and_then(|ctx| {
ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).target_symbol) 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) = let (right_section, right_symbol) =
right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(right_symbol, right_section); 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(), left_symbol: state.symbol_state.left_symbol.clone(),
right_symbol: Some(symbol_ref), 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) = let (left_section, left_symbol) =
left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap());
let symbol_ref = SymbolRefByName::new(left_symbol, left_section); 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 both sides are missing a symbol, switch to symbol diff view
if right_ctx.map_or(false, |ctx| !ctx.has_symbol()) if right_ctx.is_some_and(|ctx| !ctx.has_symbol())
&& left_ctx.map_or(false, |ctx| !ctx.has_symbol()) && left_ctx.is_some_and(|ctx| !ctx.has_symbol())
{ {
return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff())); return Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
@@ -671,7 +675,7 @@ pub fn function_diff_ui(
if column == 0 { if column == 0 {
// Left column // Left column
ui.horizontal(|ui| { 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())); ret = Some(DiffViewAction::Navigate(DiffViewNavigation::symbol_diff()));
} }
ui.separator(); ui.separator();
@@ -679,7 +683,7 @@ pub fn function_diff_ui(
.add_enabled( .add_enabled(
!state.scratch_running !state.scratch_running
&& state.scratch_available && 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"), egui::Button::new("📲 decomp.me"),
) )
.on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") .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()) .font(appearance.code_font.clone())
.color(appearance.highlight_color), .color(appearance.highlight_color),
); );
if right_ctx.map_or(false, |m| m.has_symbol()) if right_ctx.is_some_and(|m| m.has_symbol())
&& ui && (ui
.button("Change target") .button("Change target")
.on_hover_text_at_pointer("Choose a different symbol to use as the target") .on_hover_text_at_pointer("Choose a different symbol to use as the target")
.clicked() .clicked()
|| hotkeys::consume_change_target_shortcut(ui.ctx()))
{ {
if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() { if let Some(symbol_ref) = state.symbol_state.right_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone())); ret = Some(DiffViewAction::SelectingLeft(symbol_ref.clone()));
@@ -773,7 +778,7 @@ pub fn function_diff_ui(
.color(match_color_for_symbol(match_percent, appearance)), .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(); ui.separator();
if ui if ui
.button("Change base") .button("Change base")
@@ -781,6 +786,7 @@ pub fn function_diff_ui(
"Choose a different symbol to use as the base", "Choose a different symbol to use as the base",
) )
.clicked() .clicked()
|| hotkeys::consume_change_base_shortcut(ui.ctx())
{ {
if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() { if let Some(symbol_ref) = state.symbol_state.left_symbol.as_ref() {
ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone())); ret = Some(DiffViewAction::SelectingRight(symbol_ref.clone()));

View File

@@ -3,6 +3,7 @@ use std::cmp::Ordering;
use egui::{ProgressBar, RichText, Widget}; use egui::{ProgressBar, RichText, Widget};
use crate::{ use crate::{
hotkeys,
jobs::{JobQueue, JobStatus}, jobs::{JobQueue, JobStatus},
views::appearance::Appearance, 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 { 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(); let mut statuses = Vec::new();
for job in jobs.iter_mut() { for job in jobs.iter_mut() {
let Ok(status) = job.context.status.read() else { 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 running_jobs = statuses.iter().filter(|s| !s.error).count();
let error_jobs = statuses.iter().filter(|s| s.error).count(); let error_jobs = statuses.iter().filter(|s| s.error).count();
let mut clicked = false;
let spinner = let spinner =
egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color); egui::Spinner::new().size(appearance.ui_font.size * 0.9).color(appearance.text_color);
match running_jobs.cmp(&1) { 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::{ use egui::{
text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea, SelectableLabel, TextEdit, style::ScrollAnimation, text::LayoutJob, CollapsingHeader, Color32, Id, OpenUrl, ScrollArea,
Ui, Widget, SelectableLabel, TextEdit, Ui, Widget,
}; };
use objdiff_core::{ use objdiff_core::{
arch::ObjArch, arch::ObjArch,
@@ -15,6 +15,7 @@ use regex::{Regex, RegexBuilder};
use crate::{ use crate::{
app::AppStateRef, app::AppStateRef,
hotkeys,
jobs::{ jobs::{
create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult}, create_scratch::{start_create_scratch, CreateScratchConfig, CreateScratchResult},
objdiff::{BuildStatus, ObjDiffResult}, objdiff::{BuildStatus, ObjDiffResult},
@@ -56,8 +57,8 @@ pub enum DiffViewAction {
Build, Build,
/// Navigate to a new diff view /// Navigate to a new diff view
Navigate(DiffViewNavigation), Navigate(DiffViewNavigation),
/// Set the highlighted symbols in the symbols view /// Set the highlighted symbols in the symbols view, optionally scrolling them into view.
SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>), SetSymbolHighlight(Option<SymbolRef>, Option<SymbolRef>, bool),
/// Set the symbols view search filter /// Set the symbols view search filter
SetSearch(String), SetSearch(String),
/// Submit the current function to decomp.me /// Submit the current function to decomp.me
@@ -135,6 +136,7 @@ pub struct DiffViewState {
#[derive(Default)] #[derive(Default)]
pub struct SymbolViewState { pub struct SymbolViewState {
pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>), pub highlighted_symbol: (Option<SymbolRef>, Option<SymbolRef>),
pub autoscroll_to_highlighted_symbols: bool,
pub left_symbol: Option<SymbolRefByName>, pub left_symbol: Option<SymbolRefByName>,
pub right_symbol: Option<SymbolRefByName>, pub right_symbol: Option<SymbolRefByName>,
pub reverse_fn_order: bool, 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))); 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 { let Some(action) = action else {
return; return;
}; };
@@ -211,7 +216,6 @@ impl DiffViewState {
// Ignore action if we're already navigating // Ignore action if we're already navigating
return; return;
} }
self.symbol_state.highlighted_symbol = (None, None);
let Ok(mut state) = state.write() else { let Ok(mut state) = state.write() else {
return; return;
}; };
@@ -247,8 +251,9 @@ impl DiffViewState {
self.post_build_nav = Some(nav); 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.highlighted_symbol = (left, right);
self.symbol_state.autoscroll_to_highlighted_symbols = autoscroll;
} }
DiffViewAction::SetSearch(search) => { DiffViewAction::SetSearch(search) => {
self.search_regex = if search.is_empty() { self.search_regex = if search.is_empty() {
@@ -534,7 +539,15 @@ fn symbol_ui(
ret = Some(DiffViewAction::Navigate(result)); 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 { if let Some(section) = section {
match section.kind { match section.kind {
ObjSectionKind::Code => { ObjSectionKind::Code => {
@@ -561,20 +574,18 @@ fn symbol_ui(
} }
} }
} else if response.hovered() { } else if response.hovered() {
ret = Some(if let Some(target_symbol) = symbol_diff.target_symbol { ret = Some(if column == 0 {
if column == 0 { DiffViewAction::SetSymbolHighlight(
DiffViewAction::SetSymbolHighlight( Some(symbol_diff.symbol_ref),
Some(symbol_diff.symbol_ref), symbol_diff.target_symbol,
Some(target_symbol), false,
) )
} else {
DiffViewAction::SetSymbolHighlight(
Some(target_symbol),
Some(symbol_diff.symbol_ref),
)
}
} else { } else {
DiffViewAction::SetSymbolHighlight(None, None) DiffViewAction::SetSymbolHighlight(
symbol_diff.target_symbol,
Some(symbol_diff.symbol_ref),
false,
)
}); });
} }
ret 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.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend);
@@ -838,7 +901,13 @@ pub fn symbol_diff_ui(
}); });
let mut search = state.search.clone(); 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)); ret = Some(DiffViewAction::SetSearch(search));
} }
} else if column == 1 { } else if column == 1 {