diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index d8530f7..dbf2038 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -303,7 +303,7 @@ fn find_function(obj: &ObjInfo, name: &str) -> Option { None } -#[allow(dead_code)] +#[expect(dead_code)] struct FunctionDiffUi { relax_reloc_diffs: bool, left_highlight: HighlightKind, @@ -758,7 +758,7 @@ impl FunctionDiffUi { self.scroll_y += self.per_page / if half { 2 } else { 1 }; } - #[allow(clippy::too_many_arguments)] + #[expect(clippy::too_many_arguments)] fn print_sym( &self, out: &mut Text<'static>, diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index c8826d4..6b5ff92 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -112,12 +112,12 @@ impl ProjectObject { } pub fn complete(&self) -> Option { - #[allow(deprecated)] + #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.complete).or(self.complete) } pub fn reverse_fn_order(&self) -> Option { - #[allow(deprecated)] + #[expect(deprecated)] self.metadata.as_ref().and_then(|m| m.reverse_fn_order).or(self.reverse_fn_order) } diff --git a/objdiff-core/src/diff/code.rs b/objdiff-core/src/diff/code.rs index 8e99bd5..21bd0f8 100644 --- a/objdiff-core/src/diff/code.rs +++ b/objdiff-core/src/diff/code.rs @@ -211,7 +211,7 @@ fn arg_eq( left_diff: &ObjInsDiff, right_diff: &ObjInsDiff, ) -> bool { - return match left { + match left { ObjInsArg::PlainText(l) => match right { ObjInsArg::PlainText(r) => l == r, _ => false, @@ -236,7 +236,7 @@ fn arg_eq( left_diff.branch_to.as_ref().map(|b| b.ins_idx) == right_diff.branch_to.as_ref().map(|b| b.ins_idx) } - }; + } } #[derive(Default)] diff --git a/objdiff-core/src/diff/mod.rs b/objdiff-core/src/diff/mod.rs index caa35b5..4d253b8 100644 --- a/objdiff-core/src/diff/mod.rs +++ b/objdiff-core/src/diff/mod.rs @@ -161,6 +161,7 @@ pub struct DiffObjConfig { #[serde(default = "default_true")] pub space_between_args: bool, pub combine_data_sections: bool, + pub symbol_overrides: SymbolOverrides, // x86 pub x86_formatter: X86Formatter, // MIPS @@ -182,6 +183,7 @@ impl Default for DiffObjConfig { relax_reloc_diffs: false, space_between_args: true, combine_data_sections: false, + symbol_overrides: Default::default(), x86_formatter: Default::default(), mips_abi: Default::default(), mips_instr_category: Default::default(), @@ -378,7 +380,7 @@ pub fn diff_objs( right: Option<&ObjInfo>, prev: Option<&ObjInfo>, ) -> Result { - let symbol_matches = matching_symbols(left, right, prev)?; + let symbol_matches = matching_symbols(left, right, prev, &config.symbol_overrides)?; let section_matches = matching_sections(left, right)?; let mut left = left.map(|p| (p, ObjDiff::new_from_obj(p))); let mut right = right.map(|p| (p, ObjDiff::new_from_obj(p))); @@ -551,11 +553,61 @@ struct SectionMatch { section_kind: ObjSectionKind, } +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)] +pub struct SymbolOverride { + pub left: Option, + pub right: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Default, serde::Deserialize, serde::Serialize)] +pub struct SymbolOverrides(pub Vec); + +impl SymbolOverrides { + pub fn get(&self, name: &str) -> Option<&SymbolOverride> { + self.0.iter().find(|o| { + o.left.as_ref().is_some_and(|l| l == name) + || o.right.as_ref().is_some_and(|r| r == name) + }) + } + + pub fn remove_left(&mut self, left: &str, right: &str) { + self.0.retain(|o| { + o.left.as_ref().map_or(true, |l| l != left) + && o.right.as_ref().map_or(true, |r| r != right) + }); + self.0.push(SymbolOverride { left: None, right: Some(right.to_string()) }); + // println!("{:?}", self.0); + } + + pub fn remove_right(&mut self, left: &str, right: &str) { + self.0.retain(|o| { + o.left.as_ref().map_or(true, |l| l != left) + && o.right.as_ref().map_or(true, |r| r != right) + }); + self.0.push(SymbolOverride { left: Some(left.to_string()), right: None }); + // println!("{:?}", self.0); + } + + pub fn set(&mut self, left: String, right: String) { + self.0.retain(|o| { + o.left.as_ref().map_or(true, |l| l != &left && l != &right) + && o.right.as_ref().map_or(true, |r| r != &left && r != &right) + }); + if left != right { + self.0.push(SymbolOverride { left: Some(left), right: Some(right) }); + } + // println!("{:?}", self.0); + } + + pub fn clear(&mut self) { self.0.clear(); } +} + /// Find matching symbols between each object. fn matching_symbols( left: Option<&ObjInfo>, right: Option<&ObjInfo>, prev: Option<&ObjInfo>, + overrides: &SymbolOverrides, ) -> Result> { let mut matches = Vec::new(); let mut right_used = HashSet::new(); @@ -564,8 +616,8 @@ fn matching_symbols( for (symbol_idx, symbol) in section.symbols.iter().enumerate() { let symbol_match = SymbolMatch { left: Some(SymbolRef { section_idx, symbol_idx }), - right: find_symbol(right, symbol, section, Some(&right_used)), - prev: find_symbol(prev, symbol, section, None), + right: find_symbol(right, symbol, section, Some(&right_used), overrides), + prev: find_symbol(prev, symbol, section, None, &Default::default()), section_kind: section.kind, }; matches.push(symbol_match); @@ -597,7 +649,7 @@ fn matching_symbols( matches.push(SymbolMatch { left: None, right: Some(symbol_ref), - prev: find_symbol(prev, symbol, section, None), + prev: find_symbol(prev, symbol, section, None, &Default::default()), section_kind: section.kind, }); } @@ -637,8 +689,23 @@ fn find_symbol( in_symbol: &ObjSymbol, in_section: &ObjSection, used: Option<&HashSet>, + overrides: &SymbolOverrides, ) -> Option { let obj = obj?; + // Check for a symbol override + if let Some(symbol_override) = overrides.get(&in_symbol.name) { + symbol_override.left.as_ref()?; + let right_name = symbol_override.right.as_ref()?; + if let Some((section_idx, section)) = + obj.sections.iter().enumerate().find(|(_, s)| s.name == in_section.name) + { + if let Some((symbol_idx, _)) = unmatched_symbols(section, section_idx, used) + .find(|(_, symbol)| &symbol.name == right_name) + { + return Some(SymbolRef { section_idx, symbol_idx }); + } + } + } // Try to find an exact name match for (section_idx, section) in obj.sections.iter().enumerate() { if section.kind != in_section.kind { diff --git a/objdiff-core/src/obj/mod.rs b/objdiff-core/src/obj/mod.rs index 6e96d85..68af116 100644 --- a/objdiff-core/src/obj/mod.rs +++ b/objdiff-core/src/obj/mod.rs @@ -112,6 +112,15 @@ pub struct ObjIns { pub orig: Option, } +#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Default)] +pub enum ObjSymbolKind { + #[default] + Unknown, + Function, + Object, + Section, +} + #[derive(Debug, Clone)] pub struct ObjSymbol { pub name: String, @@ -120,6 +129,7 @@ pub struct ObjSymbol { pub section_address: u64, pub size: u64, pub size_known: bool, + pub kind: ObjSymbolKind, pub flags: ObjSymbolFlagSet, pub addend: i64, /// Original virtual address (from .note.split section) diff --git a/objdiff-core/src/obj/read.rs b/objdiff-core/src/obj/read.rs index c7d4265..6154bf5 100644 --- a/objdiff-core/src/obj/read.rs +++ b/objdiff-core/src/obj/read.rs @@ -23,6 +23,7 @@ use crate::{ obj::{ split_meta::{SplitMeta, SPLITMETA_SECTION}, ObjInfo, ObjReloc, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlagSet, ObjSymbolFlags, + ObjSymbolKind, }, util::{read_u16, read_u32}, }; @@ -94,6 +95,13 @@ fn to_obj_symbol( }) .unwrap_or(&[]); + let kind = match symbol.kind() { + SymbolKind::Text => ObjSymbolKind::Function, + SymbolKind::Data => ObjSymbolKind::Object, + SymbolKind::Section => ObjSymbolKind::Section, + _ => ObjSymbolKind::Unknown, + }; + Ok(ObjSymbol { name: name.to_string(), demangled_name, @@ -101,6 +109,7 @@ fn to_obj_symbol( section_address, size: symbol.size(), size_known: symbol.size() != 0, + kind, flags, addend, virtual_address, @@ -173,12 +182,19 @@ fn symbols_by_section( result.sort_by(|a, b| a.address.cmp(&b.address).then(a.size.cmp(&b.size))); let mut iter = result.iter_mut().peekable(); while let Some(symbol) = iter.next() { - if symbol.size == 0 { + if symbol.kind == ObjSymbolKind::Unknown && symbol.size == 0 { if let Some(next_symbol) = iter.peek() { symbol.size = next_symbol.address - symbol.address; } else { symbol.size = (section.address + section.size) - symbol.address; } + // Set symbol kind if we ended up with a non-zero size + if symbol.size > 0 { + symbol.kind = match section.kind { + ObjSectionKind::Code => ObjSymbolKind::Function, + ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object, + }; + } } } if result.is_empty() { @@ -196,6 +212,10 @@ fn symbols_by_section( section_address: 0, size: section.size, size_known: true, + kind: match section.kind { + ObjSectionKind::Code => ObjSymbolKind::Function, + ObjSectionKind::Data | ObjSectionKind::Bss => ObjSymbolKind::Object, + }, flags: Default::default(), addend: 0, virtual_address: None, @@ -281,6 +301,7 @@ fn find_section_symbol( section_address: 0, size: 0, size_known: false, + kind: ObjSymbolKind::Section, flags: Default::default(), addend: address as i64 - section.address() as i64, virtual_address: None, @@ -568,6 +589,7 @@ fn update_combined_symbol(symbol: ObjSymbol, address_change: i64) -> Result bool { self.jobs.iter().any(|job| { if let Some(handle) = &job.handle { diff --git a/objdiff-gui/src/views/column_layout.rs b/objdiff-gui/src/views/column_layout.rs new file mode 100644 index 0000000..9148311 --- /dev/null +++ b/objdiff-gui/src/views/column_layout.rs @@ -0,0 +1,82 @@ +use egui::{Align, Layout, Sense, Vec2}; +use egui_extras::{Column, Size, StripBuilder, TableBuilder, TableRow}; + +pub fn render_header( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + mut add_contents: impl FnMut(&mut egui::Ui, usize), +) { + let column_width = available_width / num_columns as f32; + ui.allocate_ui_with_layout( + Vec2 { x: available_width, y: 100.0 }, + Layout::left_to_right(Align::Min), + |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); + for i in 0..num_columns { + ui.allocate_ui_with_layout( + Vec2 { x: column_width, y: 100.0 }, + Layout::top_down(Align::Min), + |ui| { + ui.set_width(column_width); + add_contents(ui, i); + }, + ); + } + }, + ); + ui.separator(); +} + +pub fn render_table( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + row_height: f32, + total_rows: usize, + mut add_contents: impl FnMut(&mut TableRow, usize), +) { + ui.style_mut().interaction.selectable_labels = false; + let column_width = available_width / num_columns as f32; + let available_height = ui.available_height(); + let table = TableBuilder::new(ui) + .striped(false) + .cell_layout(Layout::left_to_right(Align::Min)) + .columns(Column::exact(column_width).clip(true), num_columns) + .resizable(false) + .auto_shrink([false, false]) + .min_scrolled_height(available_height) + .sense(Sense::click()); + table.body(|body| { + body.rows(row_height, total_rows, |mut row| { + row.set_hovered(false); // Disable hover effect + for i in 0..num_columns { + add_contents(&mut row, i); + } + }); + }); +} + +pub fn render_strips( + ui: &mut egui::Ui, + available_width: f32, + num_columns: usize, + mut add_contents: impl FnMut(&mut egui::Ui, usize), +) { + let column_width = available_width / num_columns as f32; + StripBuilder::new(ui).size(Size::remainder()).clip(true).vertical(|mut strip| { + strip.strip(|builder| { + builder.sizes(Size::exact(column_width), num_columns).clip(true).horizontal( + |mut strip| { + for i in 0..num_columns { + strip.cell(|ui| { + ui.push_id(i, |ui| { + add_contents(ui, i); + }); + }); + } + }, + ); + }); + }); +} diff --git a/objdiff-gui/src/views/data_diff.rs b/objdiff-gui/src/views/data_diff.rs index 1798596..121ec37 100644 --- a/objdiff-gui/src/views/data_diff.rs +++ b/objdiff-gui/src/views/data_diff.rs @@ -1,7 +1,6 @@ use std::{cmp::min, default::Default, mem::take}; -use egui::{text::LayoutJob, Align, Label, Layout, Sense, Vec2, Widget}; -use egui_extras::{Column, TableBuilder}; +use egui::{text::LayoutJob, Id, Label, RichText, Sense, Widget}; use objdiff_core::{ diff::{ObjDataDiff, ObjDataDiffKind, ObjDiff}, obj::ObjInfo, @@ -10,6 +9,7 @@ use time::format_description; use crate::views::{ appearance::Appearance, + column_layout::{render_header, render_table}, symbol_diff::{DiffViewState, SymbolRefByName, View}, write_text, }; @@ -17,7 +17,8 @@ use crate::views::{ const BYTES_PER_ROW: usize = 16; fn find_section(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option { - obj.sections.iter().position(|section| section.name == selected_symbol.section_name) + let section_name = selected_symbol.section_name.as_ref()?; + obj.sections.iter().position(|section| §ion.name == section_name) } fn data_row_ui(ui: &mut egui::Ui, address: usize, diffs: &[ObjDataDiff], appearance: &Appearance) { @@ -131,20 +132,41 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec> { split_diffs } +#[derive(Clone, Copy)] +struct SectionDiffContext<'a> { + obj: &'a ObjInfo, + diff: &'a ObjDiff, + section_index: Option, +} + +impl<'a> SectionDiffContext<'a> { + pub fn new( + obj: Option<&'a (ObjInfo, ObjDiff)>, + selected_symbol: Option<&SymbolRefByName>, + ) -> Option { + obj.map(|(obj, diff)| Self { + obj, + diff, + section_index: selected_symbol + .and_then(|selected_symbol| find_section(obj, selected_symbol)), + }) + } + + #[inline] + pub fn has_section(&self) -> bool { self.section_index.is_some() } +} + fn data_table_ui( - table: TableBuilder<'_>, - left_obj: Option<&(ObjInfo, ObjDiff)>, - right_obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + available_width: f32, + left_ctx: Option>, + right_ctx: Option>, config: &Appearance, ) -> Option<()> { - let left_section = left_obj.and_then(|(obj, diff)| { - find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) - }); - let right_section = right_obj.and_then(|(obj, diff)| { - find_section(obj, selected_symbol).map(|i| (&obj.sections[i], &diff.sections[i])) - }); - + let left_section = left_ctx + .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); + let right_section = right_ctx + .and_then(|ctx| ctx.section_index.map(|i| (&ctx.obj.sections[i], &ctx.diff.sections[i]))); let total_bytes = left_section .or(right_section)? .1 @@ -159,118 +181,113 @@ fn data_table_ui( let left_diffs = left_section.map(|(_, section)| split_diffs(§ion.data_diff)); let right_diffs = right_section.map(|(_, section)| split_diffs(§ion.data_diff)); - table.body(|body| { - body.rows(config.code_font.size, total_rows, |mut row| { - let row_index = row.index(); - let address = row_index * BYTES_PER_ROW; - row.col(|ui| { + render_table(ui, available_width, 2, config.code_font.size, total_rows, |row, column| { + let i = row.index(); + let address = i * BYTES_PER_ROW; + row.col(|ui| { + if column == 0 { if let Some(left_diffs) = &left_diffs { - data_row_ui(ui, address, &left_diffs[row_index], config); + data_row_ui(ui, address, &left_diffs[i], config); } - }); - row.col(|ui| { + } else if column == 1 { if let Some(right_diffs) = &right_diffs { - data_row_ui(ui, address, &right_diffs[row_index], config); + data_row_ui(ui, address, &right_diffs[i], config); } - }); + } }); }); Some(()) } pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { + let Some(result) = &state.build else { return; }; + let left_ctx = + SectionDiffContext::new(result.first_obj.as_ref(), state.symbol_state.left_symbol.as_ref()); + let right_ctx = SectionDiffContext::new( + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // 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()) + { + state.current_view = View::SymbolDiff; + state.symbol_state.left_symbol = None; + state.symbol_state.right_symbol = None; + return; + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, &selected_symbol.symbol_name); - ui.label("Diff target:"); - }); - }, - ); + if ui.button("⏴ Back").clicked() { + state.current_view = View::SymbolDiff; + } + if let Some(section) = + left_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) + { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); - }); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + state.queue_build = true; + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } + }); + }); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.label(""); - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); + if let Some(section) = + right_ctx.and_then(|ctx| ctx.section_index.map(|i| &ctx.obj.sections[i])) + { + ui.label( + RichText::new(section.name.clone()) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } + }); // Table - ui.style_mut().interaction.selectable_labels = false; - let available_height = ui.available_height(); - let table = TableBuilder::new(ui) - .striped(false) - .cell_layout(Layout::left_to_right(Align::Min)) - .columns(Column::exact(column_width).clip(true), 2) - .resizable(false) - .auto_shrink([false, false]) - .min_scrolled_height(available_height); - data_table_ui( - table, - result.first_obj.as_ref(), - result.second_obj.as_ref(), - selected_symbol, - appearance, - ); + let id = + Id::new(state.symbol_state.left_symbol.as_ref().and_then(|s| s.section_name.as_deref())) + .with(state.symbol_state.right_symbol.as_ref().and_then(|s| s.section_name.as_deref())); + ui.push_id(id, |ui| { + data_table_ui(ui, available_width, left_ctx, right_ctx, appearance); + }); } diff --git a/objdiff-gui/src/views/extab_diff.rs b/objdiff-gui/src/views/extab_diff.rs index 8372b3e..9a3a083 100644 --- a/objdiff-gui/src/views/extab_diff.rs +++ b/objdiff-gui/src/views/extab_diff.rs @@ -1,14 +1,14 @@ -use egui::{Align, Layout, ScrollArea, Ui, Vec2}; -use egui_extras::{Size, StripBuilder}; +use egui::{RichText, ScrollArea}; use objdiff_core::{ arch::ppc::ExceptionInfo, - diff::ObjDiff, obj::{ObjInfo, ObjSymbol, SymbolRef}, }; use time::format_description; use crate::views::{ appearance::Appearance, + column_layout::{render_header, render_strips}, + function_diff::FunctionDiffContext, symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, }; @@ -48,14 +48,12 @@ fn find_extab_entry<'a>(obj: &'a ObjInfo, symbol: &ObjSymbol) -> Option<&'a Exce } fn extab_text_ui( - ui: &mut Ui, - obj: &(ObjInfo, ObjDiff), - symbol_ref: SymbolRef, + ui: &mut egui::Ui, + ctx: FunctionDiffContext<'_>, + symbol: &ObjSymbol, appearance: &Appearance, ) -> Option<()> { - let (_section, symbol) = obj.0.section_symbol(symbol_ref); - - if let Some(extab_entry) = find_extab_entry(&obj.0, symbol) { + if let Some(extab_entry) = find_extab_entry(&ctx.obj, symbol) { let text = decode_extab(extab_entry); ui.colored_label(appearance.replace_color, &text); return Some(()); @@ -65,137 +63,178 @@ fn extab_text_ui( } fn extab_ui( - ui: &mut Ui, - obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + ctx: FunctionDiffContext<'_>, appearance: &Appearance, - _left: bool, + _column: usize, ) { ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - let symbol = obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - - if let (Some(object), Some(symbol_ref)) = (obj, symbol) { - extab_text_ui(ui, object, symbol_ref, appearance); + if let Some((_section, symbol)) = + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + { + extab_text_ui(ui, ctx, symbol, appearance); } }); }); } pub fn extab_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { + let Some(result) = &state.build else { return; }; + let mut left_ctx = FunctionDiffContext::new( + result.first_obj.as_ref(), + state.symbol_state.left_symbol.as_ref(), + ); + let mut right_ctx = FunctionDiffContext::new( + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // If one side is missing a symbol, but the diff process found a match, use that symbol + let left_diff_symbol = left_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).diff_symbol) + }); + let right_diff_symbol = right_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).diff_symbol) + }); + if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (right_section, right_symbol) = + right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(right_symbol, right_section); + right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); + state.symbol_state.right_symbol = Some(symbol_ref); + } else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (left_section, left_symbol) = + left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(left_symbol, left_section); + left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); + state.symbol_state.left_symbol = Some(symbol_ref); + } + + // 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()) + { + state.current_view = View::SymbolDiff; + state.symbol_state.left_symbol = None; + state.symbol_state.right_symbol = None; + return; + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - }); - - let name = selected_symbol - .demangled_symbol_name - .as_deref() - .unwrap_or(&selected_symbol.symbol_name); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, name); - ui.label("Diff target:"); - }); - }, - ); + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() { + state.current_view = View::SymbolDiff; + } + ui.separator(); + if ui + .add_enabled( + !state.scratch_running + && state.scratch_available + && left_ctx.map_or(false, |ctx| ctx.has_symbol()), + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + state.queue_scratch = true; + } + }); + if let Some((_section, symbol)) = left_ctx + .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) + { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if let Some(match_percent) = result - .second_obj - .as_ref() - .and_then(|(obj, diff)| { - find_symbol(obj, selected_symbol).map(|sref| { - &diff.sections[sref.section_idx].symbols[sref.symbol_idx] - }) - }) - .and_then(|symbol| symbol.match_percent) - { - ui.colored_label( - match_color_for_symbol(match_percent, appearance), - format!("{:.0}%", match_percent.floor()), - ); - } else { - ui.colored_label(appearance.replace_color, "Missing"); - } - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); - - // Table - StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { - strip.strip(|builder| { - builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { - strip.cell(|ui| { - extab_ui(ui, result.first_obj.as_ref(), selected_symbol, appearance, true); - }); - strip.cell(|ui| { - extab_ui(ui, result.second_obj.as_ref(), selected_symbol, appearance, false); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + state.queue_build = true; + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + state.queue_open_source_path = true; + } }); - }); + + if let Some(((_section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| { + (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) + }) + }) { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + if let Some(match_percent) = symbol_diff.match_percent { + ui.label( + RichText::new(format!("{:.0}%", match_percent.floor())) + .font(appearance.code_font.clone()) + .color(match_color_for_symbol(match_percent, appearance)), + ); + } + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + } + } + }); + + // Table + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + extab_ui(ui, ctx, appearance, column); + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + extab_ui(ui, ctx, appearance, column); + } + } }); } diff --git a/objdiff-gui/src/views/function_diff.rs b/objdiff-gui/src/views/function_diff.rs index 65cfd2c..c1517da 100644 --- a/objdiff-gui/src/views/function_diff.rs +++ b/objdiff-gui/src/views/function_diff.rs @@ -1,28 +1,28 @@ use std::default::Default; -use egui::{text::LayoutJob, Align, Label, Layout, Response, Sense, Vec2, Widget}; -use egui_extras::{Column, TableBuilder, TableRow}; +use egui::{text::LayoutJob, Id, Label, Response, RichText, Sense, Widget}; +use egui_extras::{ TableRow}; use objdiff_core::{ arch::ObjArch, diff::{ display::{display_diff, DiffText, HighlightKind}, ObjDiff, ObjInsDiff, ObjInsDiffKind, }, - obj::{ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, SymbolRef}, + obj::{ + ObjInfo, ObjIns, ObjInsArg, ObjInsArgValue, ObjSection, ObjSymbol, ObjSymbolKind, SymbolRef, + }, }; use time::format_description; use crate::views::{ appearance::Appearance, - symbol_diff::{match_color_for_symbol, DiffViewState, SymbolRefByName, View}, + column_layout::{render_header, render_strips, render_table}, + symbol_diff::{ + match_color_for_symbol, symbol_list_ui, DiffViewState, SymbolDiffContext, SymbolFilter, + SymbolOverrideAction, SymbolRefByName, SymbolUiResult, SymbolViewState, View, + }, }; -#[derive(Copy, Clone, Eq, PartialEq)] -enum ColumnId { - Left, - Right, -} - #[derive(Default)] pub struct FunctionViewState { left_highlight: HighlightKind, @@ -30,16 +30,17 @@ pub struct FunctionViewState { } impl FunctionViewState { - fn highlight(&self, column: ColumnId) -> &HighlightKind { + fn highlight(&self, column: usize) -> &HighlightKind { match column { - ColumnId::Left => &self.left_highlight, - ColumnId::Right => &self.right_highlight, + 0 => &self.left_highlight, + 1 => &self.right_highlight, + _ => &HighlightKind::None, } } - fn set_highlight(&mut self, column: ColumnId, highlight: HighlightKind) { + fn set_highlight(&mut self, column: usize, highlight: HighlightKind) { match column { - ColumnId::Left => { + 0 => { if highlight == self.left_highlight { if highlight == self.right_highlight { self.left_highlight = HighlightKind::None; @@ -51,7 +52,7 @@ impl FunctionViewState { self.left_highlight = highlight; } } - ColumnId::Right => { + 1 => { if highlight == self.right_highlight { if highlight == self.left_highlight { self.left_highlight = HighlightKind::None; @@ -63,6 +64,7 @@ impl FunctionViewState { self.right_highlight = highlight; } } + _ => {} } } @@ -223,14 +225,14 @@ fn find_symbol(obj: &ObjInfo, selected_symbol: &SymbolRefByName) -> Option, ins_diff: &ObjInsDiff, appearance: &Appearance, ins_view_state: &mut FunctionViewState, - column: ColumnId, + column: usize, space_width: f32, response_cb: impl Fn(Response) -> Response, ) { @@ -317,7 +319,7 @@ fn asm_row_ui( symbol: &ObjSymbol, appearance: &Appearance, ins_view_state: &mut FunctionViewState, - column: ColumnId, + column: usize, response_cb: impl Fn(Response) -> Response, ) { ui.spacing_mut().item_spacing.x = 0.0; @@ -344,20 +346,22 @@ fn asm_row_ui( fn asm_col_ui( row: &mut TableRow<'_, '_>, - obj: &(ObjInfo, ObjDiff), - symbol_ref: SymbolRef, + ctx: FunctionDiffContext<'_>, appearance: &Appearance, ins_view_state: &mut FunctionViewState, - column: ColumnId, + column: usize, ) { - let (section, symbol) = obj.0.section_symbol(symbol_ref); + let Some(symbol_ref) = ctx.symbol_ref else { + return; + }; + let (section, symbol) = ctx.obj.section_symbol(symbol_ref); let section = section.unwrap(); - let ins_diff = &obj.1.symbol_diff(symbol_ref).instructions[row.index()]; + let ins_diff = &ctx.diff.symbol_diff(symbol_ref).instructions[row.index()]; let response_cb = |response: Response| { if let Some(ins) = &ins_diff.ins { response.context_menu(|ui| ins_context_menu(ui, section, ins, symbol)); response.on_hover_ui_at_pointer(|ui| { - ins_hover_ui(ui, obj.0.arch.as_ref(), section, ins, symbol, appearance) + ins_hover_ui(ui, ctx.obj.arch.as_ref(), section, ins, symbol, appearance) }) } else { response @@ -369,213 +373,391 @@ fn asm_col_ui( response_cb(response); } -fn empty_col_ui(row: &mut TableRow<'_, '_>) { - row.col(|ui| { - ui.label(""); - }); -} - fn asm_table_ui( - table: TableBuilder<'_>, - left_obj: Option<&(ObjInfo, ObjDiff)>, - right_obj: Option<&(ObjInfo, ObjDiff)>, - selected_symbol: &SymbolRefByName, + ui: &mut egui::Ui, + available_width: f32, + left_ctx: Option>, + right_ctx: Option>, appearance: &Appearance, ins_view_state: &mut FunctionViewState, -) -> Option<()> { - let left_symbol = left_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - let right_symbol = right_obj.and_then(|(obj, _)| find_symbol(obj, selected_symbol)); - let instructions_len = match (left_symbol, right_symbol) { - (Some(left_symbol_ref), Some(right_symbol_ref)) => { - let left_len = left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len(); - let right_len = right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len(); - debug_assert_eq!(left_len, right_len); + symbol_state: &mut SymbolViewState, +) -> Option { + let left_len = left_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) + }); + let right_len = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).instructions.len()) + }); + let instructions_len = match (left_len, right_len) { + (Some(left_len), Some(right_len)) => { + if left_len != right_len { + ui.label("Instruction count mismatch"); + return None; + } left_len } - (Some(left_symbol_ref), None) => { - left_obj.unwrap().1.symbol_diff(left_symbol_ref).instructions.len() - } - (None, Some(right_symbol_ref)) => { - right_obj.unwrap().1.symbol_diff(right_symbol_ref).instructions.len() + (Some(left_len), None) => left_len, + (None, Some(right_len)) => right_len, + (None, None) => { + ui.label("No symbol selected"); + return None; } - (None, None) => return None, }; - table.body(|body| { - body.rows(appearance.code_font.size, instructions_len, |mut row| { - row.set_hovered(false); // Disable row hover effect - if let (Some(left_obj), Some(left_symbol_ref)) = (left_obj, left_symbol) { - asm_col_ui( - &mut row, - left_obj, - left_symbol_ref, - appearance, - ins_view_state, - ColumnId::Left, - ); - } else { - empty_col_ui(&mut row); - } - if let (Some(right_obj), Some(right_symbol_ref)) = (right_obj, right_symbol) { - asm_col_ui( - &mut row, - right_obj, - right_symbol_ref, - appearance, - ins_view_state, - ColumnId::Right, - ); - } else { - empty_col_ui(&mut row); - } - if row.response().clicked() { - ins_view_state.clear_highlight(); + let mut ret = None; + if left_len.is_some() && right_len.is_some() { + // Joint view + render_table( + ui, + available_width, + 2, + appearance.code_font.size, + instructions_len, + |row, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + asm_col_ui(row, ctx, appearance, ins_view_state, column); + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + asm_col_ui(row, ctx, appearance, ins_view_state, column); + } + if row.response().clicked() { + ins_view_state.clear_highlight(); + } + } + }, + ); + } else { + // Split view, one side is the symbol list + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + if let Some(ctx) = left_ctx { + if ctx.has_symbol() { + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + instructions_len, + |row, column| { + asm_col_ui(row, ctx, appearance, ins_view_state, column); + if row.response().clicked() { + ins_view_state.clear_highlight(); + } + }, + ); + } else if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, + None, + symbol_state, + SymbolFilter::Kind(ObjSymbolKind::Function), + appearance, + column, + ) { + let right_symbol = right_ctx + .and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + }) + .map(|(section, symbol)| SymbolRefByName::new(symbol, section)); + if let (Some(left_symbol), Some(right_symbol)) = + (result.left_symbol, right_symbol) + { + ret = Some(SymbolOverrideAction::Set(left_symbol, right_symbol)); + } + } + } else { + ui.label("No left object"); + } + } else if column == 1 { + if let Some(ctx) = right_ctx { + if ctx.has_symbol() { + render_table( + ui, + available_width / 2.0, + 1, + appearance.code_font.size, + instructions_len, + |row, column| { + asm_col_ui(row, ctx, appearance, ins_view_state, column); + if row.response().clicked() { + ins_view_state.clear_highlight(); + } + }, + ); + } else if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj: ctx.obj, diff: ctx.diff }, + None, + symbol_state, + SymbolFilter::Kind(ObjSymbolKind::Function), + appearance, + column, + ) { + let left_symbol = left_ctx + .and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref)) + }) + .map(|(section, symbol)| SymbolRefByName::new(symbol, section)); + if let (Some(left_symbol), Some(right_symbol)) = + (left_symbol, result.right_symbol) + { + ret = Some(SymbolOverrideAction::Set(left_symbol, right_symbol)); + } + } + } else { + ui.label("No right object"); + } } }); - }); - Some(()) + } + ret +} + +#[derive(Clone, Copy)] +pub struct FunctionDiffContext<'a> { + pub obj: &'a ObjInfo, + pub diff: &'a ObjDiff, + pub symbol_ref: Option, +} + +impl<'a> FunctionDiffContext<'a> { + pub fn new( + obj: Option<&'a (ObjInfo, ObjDiff)>, + selected_symbol: Option<&SymbolRefByName>, + ) -> Option { + obj.map(|(obj, diff)| Self { + obj, + diff, + symbol_ref: selected_symbol.and_then(|s| find_symbol(obj, s)), + }) + } + + #[inline] + pub fn has_symbol(&self) -> bool { self.symbol_ref.is_some() } } pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &Appearance) { - let (Some(result), Some(selected_symbol)) = (&state.build, &state.symbol_state.selected_symbol) - else { + let Some(result) = &state.build else { return; }; + let mut left_ctx = FunctionDiffContext::new( + result.first_obj.as_ref(), + state.symbol_state.left_symbol.as_ref(), + ); + let mut right_ctx = FunctionDiffContext::new( + result.second_obj.as_ref(), + state.symbol_state.right_symbol.as_ref(), + ); + + // If one side is missing a symbol, but the diff process found a match, use that symbol + let left_diff_symbol = left_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).diff_symbol) + }); + let right_diff_symbol = right_ctx.and_then(|ctx| { + ctx.symbol_ref.and_then(|symbol_ref| ctx.diff.symbol_diff(symbol_ref).diff_symbol) + }); + if left_diff_symbol.is_some() && right_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (right_section, right_symbol) = + right_ctx.unwrap().obj.section_symbol(left_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(right_symbol, right_section); + right_ctx = FunctionDiffContext::new(result.second_obj.as_ref(), Some(&symbol_ref)); + state.symbol_state.right_symbol = Some(symbol_ref); + } else if right_diff_symbol.is_some() && left_ctx.map_or(false, |ctx| !ctx.has_symbol()) { + let (left_section, left_symbol) = + left_ctx.unwrap().obj.section_symbol(right_diff_symbol.unwrap()); + let symbol_ref = SymbolRefByName::new(left_symbol, left_section); + left_ctx = FunctionDiffContext::new(result.first_obj.as_ref(), Some(&symbol_ref)); + state.symbol_state.left_symbol = Some(symbol_ref); + } + + // 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()) + { + state.current_view = View::SymbolDiff; + state.symbol_state.left_symbol = None; + state.symbol_state.right_symbol = None; + return; + } + // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui.button("⏴ Back").clicked() { - state.current_view = View::SymbolDiff; - } - ui.separator(); - if ui - .add_enabled( - !state.scratch_running && state.scratch_available, - egui::Button::new("📲 decomp.me"), - ) - .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") - .on_disabled_hover_text("Scratch configuration missing") - .clicked() - { - state.queue_scratch = true; - } - }); - - let name = selected_symbol - .demangled_symbol_name - .as_deref() - .unwrap_or(&selected_symbol.symbol_name); - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.colored_label(appearance.highlight_color, name); - ui.label("Diff target:"); + ui.horizontal(|ui| { + if ui.button("⏴ Back").clicked() { + state.current_view = View::SymbolDiff; + } + ui.separator(); + if ui + .add_enabled( + !state.scratch_running + && state.scratch_available + && left_ctx.map_or(false, |ctx| ctx.has_symbol()), + egui::Button::new("📲 decomp.me"), + ) + .on_hover_text_at_pointer("Create a new scratch on decomp.me (beta)") + .on_disabled_hover_text("Scratch configuration missing") + .clicked() + { + state.queue_scratch = true; + } + }); + + if let Some((section, symbol)) = left_ctx + .and_then(|ctx| ctx.symbol_ref.map(|symbol_ref| ctx.obj.section_symbol(symbol_ref))) + { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + if right_ctx.map_or(false, |m| m.has_symbol()) + && ui + .button("Change target") + .on_hover_text_at_pointer("Choose a different symbol to use as the target") + .clicked() + { + state.match_action = Some(SymbolOverrideAction::ClearLeft( + SymbolRefByName::new(symbol, section), + state.symbol_state.right_symbol.clone().unwrap(), + )); + state.post_build_nav = Some(SymbolUiResult { + view: Some(View::FunctionDiff), + left_symbol: None, + right_symbol: state.symbol_state.right_symbol.clone(), }); - }, - ); - + state.queue_build = true; + } + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + ui.label( + RichText::new("Choose target symbol") + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - if ui - .add_enabled(!state.build_running, egui::Button::new("Build")) - .clicked() - { - state.queue_build = true; - } - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if state.build_running { - ui.colored_label(appearance.replace_color, "Building…"); - } else { - ui.label("Last built:"); - let format = - format_description::parse("[hour]:[minute]:[second]").unwrap(); - ui.label( - result - .time - .to_offset(appearance.utc_offset) - .format(&format) - .unwrap(), - ); - } - }); + ui.horizontal(|ui| { + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + state.queue_build = true; + } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if state.build_running { + ui.colored_label(appearance.replace_color, "Building…"); + } else { + ui.label("Last built:"); + let format = format_description::parse("[hour]:[minute]:[second]").unwrap(); + ui.label( + result.time.to_offset(appearance.utc_offset).format(&format).unwrap(), + ); + } + }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + state.queue_open_source_path = true; + } + }); + + if let Some(((section, symbol), symbol_diff)) = right_ctx.and_then(|ctx| { + ctx.symbol_ref.map(|symbol_ref| { + (ctx.obj.section_symbol(symbol_ref), ctx.diff.symbol_diff(symbol_ref)) + }) + }) { + let name = symbol.demangled_name.as_deref().unwrap_or(&symbol.name); + ui.label( + RichText::new(name) + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + ui.horizontal(|ui| { + if let Some(match_percent) = symbol_diff.match_percent { + ui.label( + RichText::new(format!("{:.0}%", match_percent.floor())) + .font(appearance.code_font.clone()) + .color(match_color_for_symbol(match_percent, appearance)), + ); + } + if left_ctx.map_or(false, |m| m.has_symbol()) { ui.separator(); if ui - .add_enabled( - state.source_path_available, - egui::Button::new("🖹 Source file"), + .button("Change base") + .on_hover_text_at_pointer( + "Choose a different symbol to use as the base", ) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") .clicked() { - state.queue_open_source_path = true; - } - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if let Some(match_percent) = result - .second_obj - .as_ref() - .and_then(|(obj, diff)| { - find_symbol(obj, selected_symbol).map(|sref| { - &diff.sections[sref.section_idx].symbols[sref.symbol_idx] - }) - }) - .and_then(|symbol| symbol.match_percent) - { - ui.colored_label( - match_color_for_symbol(match_percent, appearance), - format!("{:.0}%", match_percent.floor()), - ); - } else { - ui.colored_label(appearance.replace_color, "Missing"); + state.match_action = Some(SymbolOverrideAction::ClearRight( + state.symbol_state.left_symbol.clone().unwrap(), + SymbolRefByName::new(symbol, section), + )); + state.post_build_nav = Some(SymbolUiResult { + view: Some(View::FunctionDiff), + left_symbol: state.symbol_state.left_symbol.clone(), + right_symbol: None, + }); + state.queue_build = true; } - ui.label("Diff base:"); - }); - }, - ); - }, - ); - ui.separator(); + } + }); + } else { + ui.label( + RichText::new("Missing") + .font(appearance.code_font.clone()) + .color(appearance.replace_color), + ); + ui.label( + RichText::new("Choose base symbol") + .font(appearance.code_font.clone()) + .color(appearance.highlight_color), + ); + } + } + }); // Table - ui.style_mut().interaction.selectable_labels = false; - let available_height = ui.available_height(); - let table = TableBuilder::new(ui) - .striped(false) - .cell_layout(Layout::left_to_right(Align::Min)) - .columns(Column::exact(column_width).clip(true), 2) - .resizable(false) - .auto_shrink([false, false]) - .min_scrolled_height(available_height) - .sense(Sense::click()); - asm_table_ui( - table, - result.first_obj.as_ref(), - result.second_obj.as_ref(), - selected_symbol, - appearance, - &mut state.function_state, - ); + let id = Id::new(state.symbol_state.left_symbol.as_ref().map(|s| s.symbol_name.as_str())) + .with(state.symbol_state.right_symbol.as_ref().map(|s| s.symbol_name.as_str())); + if let Some(result) = ui + .push_id(id, |ui| { + asm_table_ui( + ui, + available_width, + left_ctx, + right_ctx, + appearance, + &mut state.function_state, + &mut state.symbol_state, + ) + }) + .inner + { + match result { + SymbolOverrideAction::Set(left, right) => { + state.match_action = Some(SymbolOverrideAction::Set(left.clone(), right.clone())); + state.post_build_nav = Some(SymbolUiResult { + view: Some(View::FunctionDiff), + left_symbol: Some(left), + right_symbol: Some(right), + }); + } + _ => todo!(), + } + state.queue_build = true; + } } diff --git a/objdiff-gui/src/views/mod.rs b/objdiff-gui/src/views/mod.rs index 7b31c54..16e9380 100644 --- a/objdiff-gui/src/views/mod.rs +++ b/objdiff-gui/src/views/mod.rs @@ -1,6 +1,7 @@ use egui::{text::LayoutJob, Color32, FontId, TextFormat}; pub(crate) mod appearance; +pub(crate) mod column_layout; pub(crate) mod config; pub(crate) mod data_diff; pub(crate) mod debug; diff --git a/objdiff-gui/src/views/symbol_diff.rs b/objdiff-gui/src/views/symbol_diff.rs index 8d9618c..874ad6f 100644 --- a/objdiff-gui/src/views/symbol_diff.rs +++ b/objdiff-gui/src/views/symbol_diff.rs @@ -8,7 +8,9 @@ use egui_extras::{Size, StripBuilder}; use objdiff_core::{ arch::ObjArch, diff::{ObjDiff, ObjSymbolDiff}, - obj::{ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, SymbolRef}, + obj::{ + ObjInfo, ObjSection, ObjSectionKind, ObjSymbol, ObjSymbolFlags, ObjSymbolKind, SymbolRef, + }, }; use regex::{Regex, RegexBuilder}; @@ -19,17 +21,33 @@ use crate::{ objdiff::{BuildStatus, ObjDiffResult}, Job, JobQueue, JobResult, }, - views::{appearance::Appearance, function_diff::FunctionViewState, write_text}, + views::{ + appearance::Appearance, + column_layout::{render_header, render_strips}, + function_diff::FunctionViewState, + write_text, + }, }; +#[derive(Debug, Clone)] pub struct SymbolRefByName { pub symbol_name: String, pub demangled_symbol_name: Option, - pub section_name: String, + pub section_name: Option, } -#[allow(clippy::enum_variant_names)] -#[derive(Default, Eq, PartialEq, Copy, Clone)] +impl SymbolRefByName { + pub fn new(symbol: &ObjSymbol, section: Option<&ObjSection>) -> Self { + Self { + symbol_name: symbol.name.clone(), + demangled_symbol_name: symbol.demangled_name.clone(), + section_name: section.map(|s| s.name.clone()), + } + } +} + +#[expect(clippy::enum_variant_names)] +#[derive(Debug, Default, Eq, PartialEq, Copy, Clone, Hash)] pub enum View { #[default] SymbolDiff, @@ -38,6 +56,57 @@ pub enum View { ExtabDiff, } +#[derive(Debug, Clone, Default)] +pub struct SymbolUiResult { + pub view: Option, + pub left_symbol: Option, + pub right_symbol: Option, +} + +impl SymbolUiResult { + pub fn function_diff( + view: View, + other_ctx: Option>, + symbol: &ObjSymbol, + section: &ObjSection, + symbol_diff: &ObjSymbolDiff, + column: usize, + ) -> Self { + let symbol1 = Some(SymbolRefByName::new(symbol, Some(section))); + let symbol2 = symbol_diff.diff_symbol.and_then(|symbol_ref| { + other_ctx.map(|ctx| { + let (section, symbol) = ctx.obj.section_symbol(symbol_ref); + SymbolRefByName::new(symbol, section) + }) + }); + match column { + 0 => Self { view: Some(view), left_symbol: symbol1, right_symbol: symbol2 }, + 1 => Self { view: Some(view), left_symbol: symbol2, right_symbol: symbol1 }, + _ => unreachable!("Invalid column index"), + } + } + + pub fn data_diff(section: &ObjSection) -> Self { + let symbol = SymbolRefByName { + symbol_name: section.name.clone(), + demangled_symbol_name: None, + section_name: Some(section.name.clone()), + }; + Self { + view: Some(View::DataDiff), + left_symbol: Some(symbol.clone()), + right_symbol: Some(symbol), + } + } +} + +#[derive(Debug, Clone)] +pub enum SymbolOverrideAction { + ClearLeft(SymbolRefByName, SymbolRefByName), + ClearRight(SymbolRefByName, SymbolRefByName), + Set(SymbolRefByName, SymbolRefByName), +} + #[derive(Default)] pub struct DiffViewState { pub build: Option>, @@ -54,12 +123,16 @@ pub struct DiffViewState { pub scratch_running: bool, pub source_path_available: bool, pub queue_open_source_path: bool, + pub match_action: Option, + pub post_build_nav: Option, + pub object_name: String, } #[derive(Default)] pub struct SymbolViewState { pub highlighted_symbol: (Option, Option), - pub selected_symbol: Option, + pub left_symbol: Option, + pub right_symbol: Option, pub reverse_fn_order: bool, pub disable_reverse_fn_order: bool, pub show_hidden_symbols: bool, @@ -70,6 +143,13 @@ impl DiffViewState { jobs.results.retain_mut(|result| match result { JobResult::ObjDiff(result) => { self.build = take(result); + if let Some(result) = self.post_build_nav.take() { + if let Some(view) = result.view { + self.current_view = view; + } + self.symbol_state.left_symbol = result.left_symbol; + self.symbol_state.right_symbol = result.right_symbol; + } false } JobResult::CreateScratch(result) => { @@ -93,6 +173,8 @@ impl DiffViewState { self.source_path_available = false; } self.scratch_available = CreateScratchConfig::is_available(&state.config); + self.object_name = + state.config.selected_obj.as_ref().map(|o| o.name.clone()).unwrap_or_default(); } } @@ -101,9 +183,25 @@ impl DiffViewState { ctx.output_mut(|o| o.open_url = Some(OpenUrl::new_tab(result.scratch_url))); } - if self.queue_build { + if self.queue_build && !jobs.is_running(Job::ObjDiff) { self.queue_build = false; if let Ok(mut state) = state.write() { + match self.match_action.take() { + Some(SymbolOverrideAction::ClearLeft(left_ref, right_ref)) => { + let symbol_overrides = &mut state.config.diff_obj_config.symbol_overrides; + symbol_overrides.remove_left(&left_ref.symbol_name, &right_ref.symbol_name); + } + Some(SymbolOverrideAction::ClearRight(left_ref, right_ref)) => { + let symbol_overrides = &mut state.config.diff_obj_config.symbol_overrides; + symbol_overrides + .remove_right(&left_ref.symbol_name, &right_ref.symbol_name); + } + Some(SymbolOverrideAction::Set(left_ref, right_ref)) => { + let symbol_overrides = &mut state.config.diff_obj_config.symbol_overrides; + symbol_overrides.set(left_ref.symbol_name, right_ref.symbol_name); + } + None => {} + } state.queue_build = true; } } @@ -111,7 +209,7 @@ impl DiffViewState { if self.queue_scratch { self.queue_scratch = false; if let Some(function_name) = - self.symbol_state.selected_symbol.as_ref().map(|sym| sym.symbol_name.clone()) + self.symbol_state.left_symbol.as_ref().map(|sym| sym.symbol_name.clone()) { if let Ok(state) = state.read() { match CreateScratchConfig::from_config(&state.config, function_name) { @@ -158,11 +256,13 @@ pub fn match_color_for_symbol(match_percent: f32, appearance: &Appearance) -> Co fn symbol_context_menu_ui( ui: &mut Ui, - state: &mut SymbolViewState, - arch: &dyn ObjArch, + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, symbol: &ObjSymbol, + symbol_diff: &ObjSymbolDiff, section: Option<&ObjSection>, -) -> Option { + column: usize, +) -> Option { let mut ret = None; ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); @@ -185,14 +285,17 @@ fn symbol_context_menu_ui( } } if let Some(section) = section { - let has_extab = arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some(); + let has_extab = + ctx.obj.arch.ppc().and_then(|ppc| ppc.extab_for_symbol(symbol)).is_some(); if has_extab && ui.button("Decode exception table").clicked() { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: symbol.name.clone(), - demangled_symbol_name: symbol.demangled_name.clone(), - section_name: section.name.clone(), - }); - ret = Some(View::ExtabDiff); + ret = Some(SymbolUiResult::function_diff( + View::ExtabDiff, + other_ctx, + symbol, + section, + symbol_diff, + column, + )); ui.close_menu(); } } @@ -232,17 +335,18 @@ fn symbol_hover_ui(ui: &mut Ui, arch: &dyn ObjArch, symbol: &ObjSymbol, appearan } #[must_use] -#[allow(clippy::too_many_arguments)] +#[expect(clippy::too_many_arguments)] fn symbol_ui( ui: &mut Ui, - arch: &dyn ObjArch, + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, symbol: &ObjSymbol, symbol_diff: &ObjSymbolDiff, section: Option<&ObjSection>, state: &mut SymbolViewState, appearance: &Appearance, - left: bool, -) -> Option { + column: usize, +) -> Option { if symbol.flags.0.contains(ObjSymbolFlags::Hidden) && !state.show_hidden_symbols { return None; } @@ -252,7 +356,7 @@ fn symbol_ui( if let Some(demangled) = &symbol.demangled_name { demangled } else { &symbol.name }; let mut selected = false; if let Some(sym_ref) = - if left { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 } + if column == 0 { state.highlighted_symbol.0 } else { state.highlighted_symbol.1 } { selected = symbol_diff.symbol_ref == sym_ref; } @@ -292,33 +396,38 @@ fn symbol_ui( write_text(") ", appearance.text_color, &mut job, appearance.code_font.clone()); } write_text(name, appearance.highlight_color, &mut job, appearance.code_font.clone()); - let response = SelectableLabel::new(selected, job) - .ui(ui) - .on_hover_ui_at_pointer(|ui| symbol_hover_ui(ui, arch, symbol, appearance)); + let response = SelectableLabel::new(selected, job).ui(ui).on_hover_ui_at_pointer(|ui| { + symbol_hover_ui(ui, ctx.obj.arch.as_ref(), symbol, appearance) + }); response.context_menu(|ui| { - ret = ret.or(symbol_context_menu_ui(ui, state, arch, symbol, section)); + if let Some(result) = + symbol_context_menu_ui(ui, ctx, other_ctx, symbol, symbol_diff, section, column) + { + ret = Some(result); + } }); if response.clicked() { if let Some(section) = section { - if section.kind == ObjSectionKind::Code { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: symbol.name.clone(), - demangled_symbol_name: symbol.demangled_name.clone(), - section_name: section.name.clone(), - }); - ret = Some(View::FunctionDiff); - } else if section.kind == ObjSectionKind::Data { - state.selected_symbol = Some(SymbolRefByName { - symbol_name: section.name.clone(), - demangled_symbol_name: None, - section_name: section.name.clone(), - }); - ret = Some(View::DataDiff); + match section.kind { + ObjSectionKind::Code => { + ret = Some(SymbolUiResult::function_diff( + View::FunctionDiff, + other_ctx, + symbol, + section, + symbol_diff, + column, + )); + } + ObjSectionKind::Data => { + ret = Some(SymbolUiResult::data_diff(section)); + } + ObjSectionKind::Bss => {} } } } else if response.hovered() { state.highlighted_symbol = if let Some(diff_symbol) = symbol_diff.diff_symbol { - if left { + if column == 0 { (Some(symbol_diff.symbol_ref), Some(diff_symbol)) } else { (Some(diff_symbol), Some(symbol_diff.symbol_ref)) @@ -330,52 +439,83 @@ fn symbol_ui( ret } -fn symbol_matches_search(symbol: &ObjSymbol, search_regex: Option<&Regex>) -> bool { - if let Some(search_regex) = search_regex { - search_regex.is_match(&symbol.name) - || symbol.demangled_name.as_ref().map(|s| search_regex.is_match(s)).unwrap_or(false) - } else { - true +fn symbol_matches_filter(symbol: &ObjSymbol, filter: SymbolFilter<'_>) -> bool { + match filter { + SymbolFilter::None => true, + SymbolFilter::Search(regex) => { + regex.is_match(&symbol.name) + || symbol.demangled_name.as_ref().map(|s| regex.is_match(s)).unwrap_or(false) + } + SymbolFilter::Kind(kind) => symbol.kind == kind, } } +#[derive(Copy, Clone)] +pub enum SymbolFilter<'a> { + None, + Search(&'a Regex), + Kind(ObjSymbolKind), +} + #[must_use] -fn symbol_list_ui( +pub fn symbol_list_ui( ui: &mut Ui, - obj: &(ObjInfo, ObjDiff), + ctx: SymbolDiffContext<'_>, + other_ctx: Option>, state: &mut SymbolViewState, - search_regex: Option<&Regex>, + filter: SymbolFilter<'_>, appearance: &Appearance, - left: bool, -) -> Option { + column: usize, +) -> Option { let mut ret = None; - let arch = obj.0.arch.as_ref(); ScrollArea::both().auto_shrink([false, false]).show(ui, |ui| { ui.scope(|ui| { ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - if !obj.0.common.is_empty() { + // Skip sections with all symbols filtered out + if !ctx.obj.common.is_empty() + && (matches!(filter, SymbolFilter::None) + || ctx + .obj + .common + .iter() + .zip(&ctx.diff.common) + .any(|(symbol, symbol_diff)| symbol_matches_filter(symbol, filter))) + { CollapsingHeader::new(".comm").default_open(true).show(ui, |ui| { - for (symbol, symbol_diff) in obj.0.common.iter().zip(&obj.1.common) { - if !symbol_matches_search(symbol, search_regex) { + for (symbol, symbol_diff) in ctx.obj.common.iter().zip(&ctx.diff.common) { + if !symbol_matches_filter(symbol, filter) { continue; } - ret = ret.or(symbol_ui( + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, None, state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } }); } - for (section, section_diff) in obj.0.sections.iter().zip(&obj.1.sections) { + for (section, section_diff) in ctx.obj.sections.iter().zip(&ctx.diff.sections) { + // Skip sections with all symbols filtered out + if !matches!(filter, SymbolFilter::None) + && !section + .symbols + .iter() + .zip(§ion_diff.symbols) + .any(|(symbol, symbol_diff)| symbol_matches_filter(symbol, filter)) + { + continue; + } let mut header = LayoutJob::simple_singleline( format!("{} ({:x})", section.name, section.size), appearance.code_font.clone(), @@ -409,37 +549,43 @@ fn symbol_list_ui( for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols).rev() { - if !symbol_matches_search(symbol, search_regex) { + if !symbol_matches_filter(symbol, filter) { continue; } - ret = ret.or(symbol_ui( + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, Some(section), state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } } else { for (symbol, symbol_diff) in section.symbols.iter().zip(§ion_diff.symbols) { - if !symbol_matches_search(symbol, search_regex) { + if !symbol_matches_filter(symbol, filter) { continue; } - ret = ret.or(symbol_ui( + if let Some(result) = symbol_ui( ui, - arch, + ctx, + other_ctx, symbol, symbol_diff, Some(section), state, appearance, - left, - )); + column, + ) { + ret = Some(result); + } } } }); @@ -487,6 +633,12 @@ fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) { }); } +#[derive(Copy, Clone)] +pub struct SymbolDiffContext<'a> { + pub obj: &'a ObjInfo, + pub diff: &'a ObjDiff, +} + pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { let DiffViewState { build, current_view, symbol_state, search, search_regex, .. } = state; let Some(result) = build else { @@ -495,147 +647,132 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea // Header let available_width = ui.available_width(); - let column_width = available_width / 2.0; - ui.allocate_ui_with_layout( - Vec2 { x: available_width, y: 100.0 }, - Layout::left_to_right(Align::Min), - |ui| { - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate); - + render_header(ui, available_width, 2, |ui, column| { + if column == 0 { // Left column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - - ui.label("Build target:"); - if result.first_status.success { - if result.first_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.label("OK"); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); - - if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() { - if search.is_empty() { - *search_regex = None; - } else if let Ok(regex) = - RegexBuilder::new(search).case_insensitive(true).build() - { - *search_regex = Some(regex); - } else { - *search_regex = None; - } + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + + ui.label("Target object"); + if result.first_status.success { + if result.first_obj.is_none() { + ui.colored_label(appearance.replace_color, "Missing"); + } else { + ui.colored_label(appearance.highlight_color, state.object_name.clone()); } - }, - ); + } else { + ui.colored_label(appearance.delete_color, "Fail"); + } + }); + if TextEdit::singleline(search).hint_text("Filter symbols").ui(ui).changed() { + if search.is_empty() { + *search_regex = None; + } else if let Ok(regex) = RegexBuilder::new(search).case_insensitive(true).build() { + *search_regex = Some(regex); + } else { + *search_regex = None; + } + } + } else if column == 1 { // Right column - ui.allocate_ui_with_layout( - Vec2 { x: column_width, y: 100.0 }, - Layout::top_down(Align::Min), - |ui| { - ui.set_width(column_width); - - ui.horizontal(|ui| { - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - ui.label("Build base:"); - }); - ui.separator(); - if ui - .add_enabled( - state.source_path_available, - egui::Button::new("🖹 Source file"), - ) - .on_hover_text_at_pointer("Open the source file in the default editor") - .on_disabled_hover_text("Source file metadata missing") - .clicked() - { - state.queue_open_source_path = true; - } - }); - - ui.scope(|ui| { - ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); - if result.second_status.success { - if result.second_obj.is_none() { - ui.colored_label(appearance.replace_color, "Missing"); - } else { - ui.label("OK"); - } - } else { - ui.colored_label(appearance.delete_color, "Fail"); - } - }); + ui.horizontal(|ui| { + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + ui.label("Base object"); + }); + ui.separator(); + if ui + .add_enabled(state.source_path_available, egui::Button::new("🖹 Source file")) + .on_hover_text_at_pointer("Open the source file in the default editor") + .on_disabled_hover_text("Source file metadata missing") + .clicked() + { + state.queue_open_source_path = true; + } + }); - if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { - state.queue_build = true; + ui.scope(|ui| { + ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace); + if result.second_status.success { + if result.second_obj.is_none() { + ui.colored_label(appearance.replace_color, "Missing"); + } else { + ui.colored_label(appearance.highlight_color, "OK"); } - }, - ); - }, - ); - ui.separator(); + } else { + ui.colored_label(appearance.delete_color, "Fail"); + } + }); + + if ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() { + state.queue_build = true; + } + } + }); // Table + let filter = match search_regex { + Some(regex) => SymbolFilter::Search(regex), + _ => SymbolFilter::None, + }; let mut ret = None; - StripBuilder::new(ui).size(Size::remainder()).vertical(|mut strip| { - strip.strip(|builder| { - builder.sizes(Size::remainder(), 2).horizontal(|mut strip| { - strip.cell(|ui| { - ui.push_id("left", |ui| { - if result.first_status.success { - if let Some(obj) = &result.first_obj { - ret = ret.or(symbol_list_ui( - ui, - obj, - symbol_state, - search_regex.as_ref(), - appearance, - true, - )); - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.first_status, appearance); - } - }); - }); - strip.cell(|ui| { - ui.push_id("right", |ui| { - if result.second_status.success { - if let Some(obj) = &result.second_obj { - ret = ret.or(symbol_list_ui( - ui, - obj, - symbol_state, - search_regex.as_ref(), - appearance, - false, - )); - } else { - missing_obj_ui(ui, appearance); - } - } else { - build_log_ui(ui, &result.second_status, appearance); - } - }); - }); - }); - }); + render_strips(ui, available_width, 2, |ui, column| { + if column == 0 { + // Left column + if result.first_status.success { + if let Some((obj, diff)) = &result.first_obj { + if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + result + .second_obj + .as_ref() + .map(|(obj, diff)| SymbolDiffContext { obj, diff }), + symbol_state, + filter, + appearance, + column, + ) { + ret = Some(result); + } + } else { + missing_obj_ui(ui, appearance); + } + } else { + build_log_ui(ui, &result.first_status, appearance); + } + } else if column == 1 { + // Right column + if result.second_status.success { + if let Some((obj, diff)) = &result.second_obj { + if let Some(result) = symbol_list_ui( + ui, + SymbolDiffContext { obj, diff }, + result + .first_obj + .as_ref() + .map(|(obj, diff)| SymbolDiffContext { obj, diff }), + symbol_state, + filter, + appearance, + column, + ) { + ret = Some(result); + } + } else { + missing_obj_ui(ui, appearance); + } + } else { + build_log_ui(ui, &result.second_status, appearance); + } + } }); - - if let Some(view) = ret { - *current_view = view; + if let Some(result) = ret { + if let Some(view) = result.view { + *current_view = view; + } + symbol_state.left_symbol = result.left_symbol; + symbol_state.right_symbol = result.right_symbol; } }