Compare commits

...

3 Commits

Author SHA1 Message Date
Luke Street 711f40b591 I forgot to bump the Cargo.toml version, oops 2023-09-10 00:24:53 -04:00
Luke Street 26932b2e44 Support `min_version` field in `objdiff.json` 2023-09-09 23:54:25 -04:00
Luke Street 192a06bc0b Project configuration improvements
- Support `completed` field for objects in project config. In object tree, displays red for incomplete, green for complete.
- Add support for one-sided diffs. A project can include objects without an associated source file for viewing.
- Add versioning to AppConfig, supporting upgrades without losing user configuration.
2023-09-09 23:43:12 -04:00
12 changed files with 445 additions and 163 deletions

4
Cargo.lock generated
View File

@ -2457,7 +2457,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff" name = "objdiff"
version = "0.4.1" version = "0.4.3"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
@ -2483,7 +2483,9 @@ dependencies = [
"rabbitizer", "rabbitizer",
"reqwest", "reqwest",
"rfd", "rfd",
"ron",
"self_update", "self_update",
"semver",
"serde", "serde",
"serde_json", "serde_json",
"serde_yaml", "serde_yaml",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff" name = "objdiff"
version = "0.4.1" version = "0.4.3"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.65"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -42,6 +42,8 @@ png = "0.17.9"
ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" } ppc750cl = { git = "https://github.com/terorie/ppc750cl", rev = "9ae36eef34aa6d74e00972c7671f547a2acfd0aa" }
rabbitizer = "1.7.4" rabbitizer = "1.7.4"
rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal'] rfd = { version = "0.11.4" } #, default-features = false, features = ['xdg-portal']
ron = "0.8.0"
semver = "1.0.17"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
serde_json = "1.0.104" serde_json = "1.0.104"
serde_yaml = "0.9.25" serde_yaml = "0.9.25"

View File

@ -14,13 +14,14 @@ use notify::{RecursiveMode, Watcher};
use time::UtcOffset; use time::UtcOffset;
use crate::{ use crate::{
app_config::{deserialize_config, AppConfigVersion},
config::{ config::{
build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES, build_globset, load_project_config, ProjectObject, ProjectObjectNode, CONFIG_FILENAMES,
}, },
jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus}, jobs::{objdiff::start_build, Job, JobQueue, JobResult, JobStatus},
views::{ views::{
appearance::{appearance_window, Appearance}, appearance::{appearance_window, Appearance},
config::{config_ui, project_window, ConfigViewState}, config::{config_ui, project_window, ConfigViewState, DEFAULT_WATCH_PATTERNS},
data_diff::data_diff_ui, data_diff::data_diff_ui,
demangle::{demangle_window, DemangleViewState}, demangle::{demangle_window, DemangleViewState},
function_diff::function_diff_ui, function_diff::function_diff_ui,
@ -44,16 +45,21 @@ pub struct ViewState {
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)] #[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig { pub struct ObjectConfig {
pub name: String, pub name: String,
pub target_path: PathBuf, pub target_path: Option<PathBuf>,
pub base_path: PathBuf, pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
} }
#[inline] #[inline]
fn bool_true() -> bool { true } fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)] #[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig { pub struct AppConfig {
// TODO: https://github.com/ron-rs/ron/pull/455
// #[serde(flatten)]
// pub version: AppConfigVersion,
pub version: u32,
pub custom_make: Option<String>, pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>, pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>, pub project_dir: Option<PathBuf>,
@ -82,6 +88,31 @@ pub struct AppConfig {
pub project_config_loaded: bool, pub project_config_loaded: bool,
} }
impl Default for AppConfig {
fn default() -> Self {
Self {
version: AppConfigVersion::default().version,
custom_make: None,
selected_wsl_distro: None,
project_dir: None,
target_obj_dir: None,
base_obj_dir: None,
selected_obj: None,
build_target: false,
rebuild_on_changes: true,
auto_update_check: true,
watch_patterns: DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(),
objects: vec![],
object_nodes: vec![],
watcher_change: false,
config_change: false,
obj_change: false,
queue_build: false,
project_config_loaded: false,
}
}
}
impl AppConfig { impl AppConfig {
pub fn set_project_dir(&mut self, path: PathBuf) { pub fn set_project_dir(&mut self, path: PathBuf) {
self.project_dir = Some(path); self.project_dir = Some(path);
@ -133,8 +164,8 @@ pub struct App {
should_relaunch: bool, should_relaunch: bool,
} }
const APPEARANCE_KEY: &str = "appearance"; pub const APPEARANCE_KEY: &str = "appearance";
const CONFIG_KEY: &str = "app_config"; pub const CONFIG_KEY: &str = "app_config";
impl App { impl App {
/// Called once before the first frame. /// Called once before the first frame.
@ -153,7 +184,7 @@ impl App {
if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) { if let Some(appearance) = eframe::get_value::<Appearance>(storage, APPEARANCE_KEY) {
app.appearance = appearance; app.appearance = appearance;
} }
if let Some(mut config) = eframe::get_value::<AppConfig>(storage, CONFIG_KEY) { if let Some(mut config) = deserialize_config(storage) {
if config.project_dir.is_some() { if config.project_dir.is_some() {
config.config_change = true; config.config_change = true;
config.watcher_change = true; config.watcher_change = true;

96
src/app_config.rs Normal file
View File

@ -0,0 +1,96 @@
use std::path::PathBuf;
use eframe::Storage;
use globset::Glob;
use crate::app::{AppConfig, ObjectConfig, CONFIG_KEY};
#[derive(Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfigVersion {
pub version: u32,
}
impl Default for AppConfigVersion {
fn default() -> Self { Self { version: 1 } }
}
/// Deserialize the AppConfig from storage, handling upgrades from older versions.
pub fn deserialize_config(storage: &dyn Storage) -> Option<AppConfig> {
let str = storage.get_string(CONFIG_KEY)?;
match ron::from_str::<AppConfigVersion>(&str) {
Ok(version) => match version.version {
1 => from_str::<AppConfig>(&str),
_ => {
log::warn!("Unknown config version: {}", version.version);
None
}
},
Err(e) => {
log::warn!("Failed to decode config version: {e}");
// Try to decode as v0
from_str::<AppConfigV0>(&str).map(|c| c.into_config())
}
}
}
fn from_str<T>(str: &str) -> Option<T>
where T: serde::de::DeserializeOwned {
match ron::from_str(str) {
Ok(config) => Some(config),
Err(err) => {
log::warn!("Failed to decode config: {err}");
None
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct ObjectConfigV0 {
pub name: String,
pub target_path: PathBuf,
pub base_path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
impl ObjectConfigV0 {
fn into_config(self) -> ObjectConfig {
ObjectConfig {
name: self.name,
target_path: Some(self.target_path),
base_path: Some(self.base_path),
reverse_fn_order: self.reverse_fn_order,
complete: None,
}
}
}
#[derive(serde::Deserialize, serde::Serialize)]
pub struct AppConfigV0 {
pub custom_make: Option<String>,
pub selected_wsl_distro: Option<String>,
pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfigV0>,
pub build_target: bool,
pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>,
}
impl AppConfigV0 {
fn into_config(self) -> AppConfig {
log::info!("Upgrading configuration from v0");
AppConfig {
custom_make: self.custom_make,
selected_wsl_distro: self.selected_wsl_distro,
project_dir: self.project_dir,
target_obj_dir: self.target_obj_dir,
base_obj_dir: self.base_obj_dir,
selected_obj: self.selected_obj.map(|obj| obj.into_config()),
build_target: self.build_target,
auto_update_check: self.auto_update_check,
watch_patterns: self.watch_patterns,
..Default::default()
}
}
}

View File

@ -3,7 +3,7 @@ use std::{
path::{Component, Path, PathBuf}, path::{Component, Path, PathBuf},
}; };
use anyhow::{Context, Result}; use anyhow::{bail, Context, Result};
use globset::{Glob, GlobSet, GlobSetBuilder}; use globset::{Glob, GlobSet, GlobSetBuilder};
use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS}; use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS};
@ -11,6 +11,7 @@ use crate::{app::AppConfig, views::config::DEFAULT_WATCH_PATTERNS};
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
#[serde(default)] #[serde(default)]
pub struct ProjectConfig { pub struct ProjectConfig {
pub min_version: Option<String>,
pub custom_make: Option<String>, pub custom_make: Option<String>,
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
@ -27,6 +28,7 @@ pub struct ProjectObject {
pub target_path: Option<PathBuf>, pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>, pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
pub complete: Option<bool>,
} }
impl ProjectObject { impl ProjectObject {
@ -120,6 +122,14 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
}; };
if let Some(result) = try_project_config(project_dir) { if let Some(result) = try_project_config(project_dir) {
let project_config = result?; let project_config = result?;
if let Some(min_version) = &project_config.min_version {
let version_str = env!("CARGO_PKG_VERSION");
let version = semver::Version::parse(version_str).unwrap();
let version_req = semver::VersionReq::parse(&format!(">={min_version}"))?;
if !version_req.matches(&version) {
bail!("Project requires objdiff version {} or higher", min_version);
}
}
config.custom_make = project_config.custom_make; config.custom_make = project_config.custom_make;
config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p)); config.target_obj_dir = project_config.target_dir.map(|p| project_dir.join(p));
config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p)); config.base_obj_dir = project_config.base_dir.map(|p| project_dir.join(p));

View File

@ -372,66 +372,80 @@ fn find_section_and_symbol(obj: &ObjInfo, name: &str) -> Option<(usize, usize)>
None None
} }
pub fn diff_objs(left: &mut ObjInfo, right: &mut ObjInfo) -> Result<()> { pub fn diff_objs(mut left: Option<&mut ObjInfo>, mut right: Option<&mut ObjInfo>) -> Result<()> {
for left_section in &mut left.sections { if let Some(left) = left.as_mut() {
if left_section.kind == ObjSectionKind::Code { for left_section in &mut left.sections {
for left_symbol in &mut left_section.symbols { if left_section.kind == ObjSectionKind::Code {
if let Some((right_section_idx, right_symbol_idx)) = for left_symbol in &mut left_section.symbols {
find_section_and_symbol(right, &left_symbol.name) if let Some((right, (right_section_idx, right_symbol_idx))) =
{ right.as_mut().and_then(|obj| {
let right_section = &mut right.sections[right_section_idx]; find_section_and_symbol(obj, &left_symbol.name).map(|s| (obj, s))
let right_symbol = &mut right_section.symbols[right_symbol_idx]; })
left_symbol.diff_symbol = Some(right_symbol.name.clone()); {
right_symbol.diff_symbol = Some(left_symbol.name.clone()); let right_section = &mut right.sections[right_section_idx];
diff_code( let right_symbol = &mut right_section.symbols[right_symbol_idx];
left.architecture, left_symbol.diff_symbol = Some(right_symbol.name.clone());
&left_section.data, right_symbol.diff_symbol = Some(left_symbol.name.clone());
&right_section.data, diff_code(
left_symbol, left.architecture,
right_symbol, &left_section.data,
&left_section.relocations, &right_section.data,
&right_section.relocations, left_symbol,
&left.line_info, right_symbol,
&right.line_info, &left_section.relocations,
)?; &right_section.relocations,
} else { &left.line_info,
no_diff_code( &right.line_info,
left.architecture, )?;
&left_section.data, } else {
left_symbol, no_diff_code(
&left_section.relocations, left.architecture,
&left.line_info, &left_section.data,
)?; left_symbol,
&left_section.relocations,
&left.line_info,
)?;
}
} }
} } else if let Some(right_section) = right
} else { .as_mut()
let Some(right_section) = .and_then(|obj| obj.sections.iter_mut().find(|s| s.name == left_section.name))
right.sections.iter_mut().find(|s| s.name == left_section.name) {
else { if left_section.kind == ObjSectionKind::Data {
continue; diff_data(left_section, right_section);
}; // diff_data_symbols(left_section, right_section)?;
if left_section.kind == ObjSectionKind::Data { } else if left_section.kind == ObjSectionKind::Bss {
diff_data(left_section, right_section); diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?;
// diff_data_symbols(left_section, right_section)?; }
} else if left_section.kind == ObjSectionKind::Bss { } else if left_section.kind == ObjSectionKind::Data {
diff_bss_symbols(&mut left_section.symbols, &mut right_section.symbols)?; no_diff_data(left_section);
} }
} }
} }
for right_section in right.sections.iter_mut().filter(|s| s.kind == ObjSectionKind::Code) { if let Some(right) = right.as_mut() {
for right_symbol in &mut right_section.symbols { for right_section in right.sections.iter_mut() {
if right_symbol.instructions.is_empty() { if right_section.kind == ObjSectionKind::Code {
no_diff_code( for right_symbol in &mut right_section.symbols {
right.architecture, if right_symbol.instructions.is_empty() {
&right_section.data, no_diff_code(
right_symbol, right.architecture,
&right_section.relocations, &right_section.data,
&right.line_info, right_symbol,
)?; &right_section.relocations,
&right.line_info,
)?;
}
}
} else if right_section.kind == ObjSectionKind::Data
&& right_section.data_diff.is_empty()
{
no_diff_data(right_section);
} }
} }
} }
diff_bss_symbols(&mut left.common, &mut right.common)?; if let (Some(left), Some(right)) = (left, right) {
diff_bss_symbols(&mut left.common, &mut right.common)?;
}
Ok(()) Ok(())
} }
@ -710,3 +724,12 @@ fn diff_data(left: &mut ObjSection, right: &mut ObjSection) {
left.data_diff = left_diff; left.data_diff = left_diff;
right.data_diff = right_diff; right.data_diff = right_diff;
} }
fn no_diff_data(section: &mut ObjSection) {
section.data_diff = vec![ObjDataDiff {
data: section.data.clone(),
kind: ObjDataDiffKind::None,
len: section.data.len(),
symbol: String::new(),
}];
}

View File

@ -79,56 +79,101 @@ fn run_build(
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?;
let project_dir = let project_dir =
config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?; config.project_dir.as_ref().ok_or_else(|| Error::msg("Missing project dir"))?;
let target_path_rel = obj_config.target_path.strip_prefix(project_dir).map_err(|_| { let target_path_rel = if let Some(target_path) = &obj_config.target_path {
anyhow!( Some(target_path.strip_prefix(project_dir).map_err(|_| {
"Target path '{}' doesn't begin with '{}'", anyhow!(
obj_config.target_path.display(), "Target path '{}' doesn't begin with '{}'",
project_dir.display() target_path.display(),
) project_dir.display()
})?; )
let base_path_rel = obj_config.base_path.strip_prefix(project_dir).map_err(|_| { })?)
anyhow!( } else {
"Base path '{}' doesn't begin with '{}'", None
obj_config.base_path.display(), };
project_dir.display() let base_path_rel = if let Some(base_path) = &obj_config.base_path {
) Some(base_path.strip_prefix(project_dir).map_err(|_| {
})?; anyhow!(
"Base path '{}' doesn't begin with '{}'",
base_path.display(),
project_dir.display()
)
})?)
} else {
None
};
let total = if config.build_target { 5 } else { 4 }; let mut total = 3;
let first_status = if config.build_target { if config.build_target && target_path_rel.is_some() {
update_status(status, format!("Building target {}", target_path_rel.display()), 0, total, &cancel)?; total += 1;
run_make(project_dir, target_path_rel, &config) }
if base_path_rel.is_some() {
total += 1;
}
let first_status = match target_path_rel {
Some(target_path_rel) if config.build_target => {
update_status(
status,
format!("Building target {}", target_path_rel.display()),
0,
total,
&cancel,
)?;
run_make(project_dir, target_path_rel, &config)
}
_ => BuildStatus { success: true, log: String::new() },
};
let second_status = if let Some(base_path_rel) = base_path_rel {
update_status(
status,
format!("Building base {}", base_path_rel.display()),
1,
total,
&cancel,
)?;
run_make(project_dir, base_path_rel, &config)
} else { } else {
BuildStatus { success: true, log: String::new() } BuildStatus { success: true, log: String::new() }
}; };
update_status(status, format!("Building base {}", base_path_rel.display()), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config);
let time = OffsetDateTime::now_utc(); let time = OffsetDateTime::now_utc();
let mut first_obj = if first_status.success { let mut first_obj =
update_status(status, format!("Loading target {}", target_path_rel.display()), 2, total, &cancel)?; match &obj_config.target_path {
Some(elf::read(&obj_config.target_path).with_context(|| { Some(target_path) if first_status.success => {
format!("Failed to read object '{}'", obj_config.target_path.display()) update_status(
})?) status,
} else { format!("Loading target {}", target_path_rel.unwrap().display()),
None 2,
total,
&cancel,
)?;
Some(elf::read(target_path).with_context(|| {
format!("Failed to read object '{}'", target_path.display())
})?)
}
_ => None,
};
let mut second_obj = match &obj_config.base_path {
Some(base_path) if second_status.success => {
update_status(
status,
format!("Loading base {}", base_path_rel.unwrap().display()),
3,
total,
&cancel,
)?;
Some(
elf::read(base_path)
.with_context(|| format!("Failed to read object '{}'", base_path.display()))?,
)
}
_ => None,
}; };
let mut second_obj = if second_status.success { update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
update_status(status, format!("Loading base {}", base_path_rel.display()), 3, total, &cancel)?; diff_objs(first_obj.as_mut(), second_obj.as_mut())?;
Some(elf::read(&obj_config.base_path).with_context(|| {
format!("Failed to read object '{}'", obj_config.base_path.display())
})?)
} else {
None
};
if let (Some(first_obj), Some(second_obj)) = (&mut first_obj, &mut second_obj) {
update_status(status, "Performing diff".to_string(), 4, total, &cancel)?;
diff_objs(first_obj, second_obj)?;
}
update_status(status, "Complete".to_string(), total, total, &cancel)?; update_status(status, "Complete".to_string(), total, total, &cancel)?;
Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time })) Ok(Box::new(ObjDiffResult { first_status, second_status, first_obj, second_obj, time }))

View File

@ -3,6 +3,7 @@
pub use app::App; pub use app::App;
mod app; mod app;
mod app_config;
mod config; mod config;
mod diff; mod diff;
mod editops; mod editops;

View File

@ -40,6 +40,7 @@ pub struct ConfigViewState {
pub watch_pattern_text: String, pub watch_pattern_text: String,
pub load_error: Option<String>, pub load_error: Option<String>,
pub object_search: String, pub object_search: String,
pub filter_diffable: bool,
#[cfg(windows)] #[cfg(windows)]
pub available_wsl_distros: Option<Vec<String>>, pub available_wsl_distros: Option<Vec<String>>,
} }
@ -218,17 +219,19 @@ pub fn config_ui(
let target_path = target_dir.join(obj_path); let target_path = target_dir.join(obj_path);
new_selected_obj = Some(ObjectConfig { new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path, target_path: Some(target_path),
base_path: path, base_path: Some(path),
reverse_fn_order: None, reverse_fn_order: None,
complete: None,
}); });
} else if let Ok(obj_path) = path.strip_prefix(&target_dir) { } else if let Ok(obj_path) = path.strip_prefix(&target_dir) {
let base_path = base_dir.join(obj_path); let base_path = base_dir.join(obj_path);
new_selected_obj = Some(ObjectConfig { new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(), name: obj_path.display().to_string(),
target_path: path, target_path: Some(path),
base_path, base_path: Some(base_path),
reverse_fn_order: None, reverse_fn_order: None,
complete: None,
}); });
} }
} }
@ -266,6 +269,13 @@ pub fn config_ui(
root_open = Some(true); root_open = Some(true);
node_open = NodeOpen::Object; node_open = NodeOpen::Object;
} }
if ui
.selectable_label(state.filter_diffable, "Diffable")
.on_hover_text_at_pointer("Only show objects with a source file")
.clicked()
{
state.filter_diffable = !state.filter_diffable;
}
}); });
if state.object_search.is_empty() { if state.object_search.is_empty() {
if had_search { if had_search {
@ -285,10 +295,13 @@ pub fn config_ui(
.default_open(true) .default_open(true)
.show(ui, |ui| { .show(ui, |ui| {
let mut nodes = Cow::Borrowed(object_nodes); let mut nodes = Cow::Borrowed(object_nodes);
if !state.object_search.is_empty() { if !state.object_search.is_empty() || state.filter_diffable {
let search = state.object_search.to_ascii_lowercase(); let search = state.object_search.to_ascii_lowercase();
nodes = Cow::Owned( nodes = Cow::Owned(
object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(), object_nodes
.iter()
.filter_map(|node| filter_node(node, &search, state.filter_diffable))
.collect(),
); );
} }
@ -322,8 +335,18 @@ fn display_object(
) { ) {
let object_name = object.name(); let object_name = object.name();
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
let color = if selected { appearance.emphasized_text_color } else { appearance.text_color }; let color = if selected {
if SelectableLabel::new( appearance.emphasized_text_color
} else if let Some(complete) = object.complete {
if complete {
appearance.insert_color
} else {
appearance.delete_color
}
} else {
appearance.text_color
};
let clicked = SelectableLabel::new(
selected, selected,
RichText::new(name) RichText::new(name)
.font(FontId { .font(FontId {
@ -333,13 +356,16 @@ fn display_object(
.color(color), .color(color),
) )
.ui(ui) .ui(ui)
.clicked() .clicked();
{ // Always recreate ObjectConfig if selected, in case the project config changed.
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
if selected || clicked {
*selected_obj = Some(ObjectConfig { *selected_obj = Some(ObjectConfig {
name: object_name.to_string(), name: object_name.to_string(),
target_path: object.target_path.clone().unwrap_or_default(), target_path: object.target_path.clone(),
base_path: object.base_path.clone().unwrap_or_default(), base_path: object.base_path.clone(),
reverse_fn_order: object.reverse_fn_order, reverse_fn_order: object.reverse_fn_order,
complete: object.complete,
}); });
} }
} }
@ -404,21 +430,30 @@ fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool
} }
} }
fn filter_node(node: &ProjectObjectNode, search: &str) -> Option<ProjectObjectNode> { fn filter_node(
node: &ProjectObjectNode,
search: &str,
filter_diffable: bool,
) -> Option<ProjectObjectNode> {
match node { match node {
ProjectObjectNode::File(name, _) => { ProjectObjectNode::File(name, object) => {
if name.to_ascii_lowercase().contains(search) { if (search.is_empty() || name.to_ascii_lowercase().contains(search))
&& (!filter_diffable || object.base_path.is_some())
{
Some(node.clone()) Some(node.clone())
} else { } else {
None None
} }
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
if name.to_ascii_lowercase().contains(search) { if (search.is_empty() || name.to_ascii_lowercase().contains(search)) && !filter_diffable
{
return Some(node.clone()); return Some(node.clone());
} }
let new_children = let new_children = children
children.iter().filter_map(|child| filter_node(child, search)).collect::<Vec<_>>(); .iter()
.filter_map(|child| filter_node(child, search, filter_diffable))
.collect::<Vec<_>>();
if !new_children.is_empty() { if !new_children.is_empty() {
Some(ProjectObjectNode::Dir(name.clone(), new_children)) Some(ProjectObjectNode::Dir(name.clone(), new_children))
} else { } else {

View File

@ -132,31 +132,39 @@ fn split_diffs(diffs: &[ObjDataDiff]) -> Vec<Vec<ObjDataDiff>> {
fn data_table_ui( fn data_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference, selected_symbol: &SymbolReference,
config: &Appearance, config: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_section = find_section(left_obj, selected_symbol)?; let left_section = left_obj.and_then(|obj| find_section(obj, selected_symbol));
let right_section = find_section(right_obj, selected_symbol)?; let right_section = right_obj.and_then(|obj| find_section(obj, selected_symbol));
let total_bytes = left_section.data_diff.iter().fold(0usize, |accum, item| accum + item.len); let total_bytes = left_section
.or(right_section)?
.data_diff
.iter()
.fold(0usize, |accum, item| accum + item.len);
if total_bytes == 0 { if total_bytes == 0 {
return None; return None;
} }
let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1; let total_rows = (total_bytes - 1) / BYTES_PER_ROW + 1;
let left_diffs = split_diffs(&left_section.data_diff); let left_diffs = left_section.map(|section| split_diffs(&section.data_diff));
let right_diffs = split_diffs(&right_section.data_diff); let right_diffs = right_section.map(|section| split_diffs(&section.data_diff));
table.body(|body| { table.body(|body| {
body.rows(config.code_font.size, total_rows, |row_index, mut row| { body.rows(config.code_font.size, total_rows, |row_index, mut row| {
let address = row_index * BYTES_PER_ROW; let address = row_index * BYTES_PER_ROW;
row.col(|ui| { row.col(|ui| {
data_row_ui(ui, address, &left_diffs[row_index], config); if let Some(left_diffs) = &left_diffs {
data_row_ui(ui, address, &left_diffs[row_index], config);
}
}); });
row.col(|ui| { row.col(|ui| {
data_row_ui(ui, address, &right_diffs[row_index], config); if let Some(right_diffs) = &right_diffs {
data_row_ui(ui, address, &right_diffs[row_index], config);
}
}); });
}); });
}); });
@ -243,15 +251,19 @@ pub fn data_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance: &A
ui.separator(); ui.separator();
// Table // Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) { let available_height = ui.available_height();
let available_height = ui.available_height(); let table = TableBuilder::new(ui)
let table = TableBuilder::new(ui) .striped(false)
.striped(false) .cell_layout(Layout::left_to_right(Align::Min))
.cell_layout(Layout::left_to_right(Align::Min)) .columns(Column::exact(column_width).clip(true), 2)
.columns(Column::exact(column_width).clip(true), 2) .resizable(false)
.resizable(false) .auto_shrink([false, false])
.auto_shrink([false, false]) .min_scrolled_height(available_height);
.min_scrolled_height(available_height); data_table_ui(
data_table_ui(table, left_obj, right_obj, selected_symbol, appearance); table,
} result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@ -377,13 +377,13 @@ fn asm_row_ui(
fn asm_table_ui( fn asm_table_ui(
table: TableBuilder<'_>, table: TableBuilder<'_>,
left_obj: &ObjInfo, left_obj: Option<&ObjInfo>,
right_obj: &ObjInfo, right_obj: Option<&ObjInfo>,
selected_symbol: &SymbolReference, selected_symbol: &SymbolReference,
appearance: &Appearance, appearance: &Appearance,
) -> Option<()> { ) -> Option<()> {
let left_symbol = find_symbol(left_obj, selected_symbol); let left_symbol = left_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let right_symbol = find_symbol(right_obj, selected_symbol); let right_symbol = right_obj.and_then(|obj| find_symbol(obj, selected_symbol));
let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?; let instructions_len = left_symbol.or(right_symbol).map(|s| s.instructions.len())?;
table.body(|body| { table.body(|body| {
body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| { body.rows(appearance.code_font.size, instructions_len, |row_index, mut row| {
@ -492,7 +492,7 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
&format!("{match_percent:.0}%"), &format!("{match_percent:.0}%"),
); );
} else { } else {
ui.label(""); ui.colored_label(appearance.replace_color, "Missing");
} }
ui.label("Diff base:"); ui.label("Diff base:");
}); });
@ -503,15 +503,19 @@ pub fn function_diff_ui(ui: &mut egui::Ui, state: &mut DiffViewState, appearance
ui.separator(); ui.separator();
// Table // Table
if let (Some(left_obj), Some(right_obj)) = (&result.first_obj, &result.second_obj) { let available_height = ui.available_height();
let available_height = ui.available_height(); let table = TableBuilder::new(ui)
let table = TableBuilder::new(ui) .striped(false)
.striped(false) .cell_layout(Layout::left_to_right(Align::Min))
.cell_layout(Layout::left_to_right(Align::Min)) .columns(Column::exact(column_width).clip(true), 2)
.columns(Column::exact(column_width).clip(true), 2) .resizable(false)
.resizable(false) .auto_shrink([false, false])
.auto_shrink([false, false]) .min_scrolled_height(available_height);
.min_scrolled_height(available_height); asm_table_ui(
asm_table_ui(table, left_obj, right_obj, selected_symbol, appearance); table,
} result.first_obj.as_ref(),
result.second_obj.as_ref(),
selected_symbol,
appearance,
);
} }

View File

@ -1,7 +1,7 @@
use std::mem::take; use std::mem::take;
use egui::{ use egui::{
text::LayoutJob, Align, CollapsingHeader, Color32, Layout, Rgba, ScrollArea, SelectableLabel, text::LayoutJob, Align, CollapsingHeader, Color32, Layout, ScrollArea, SelectableLabel,
TextEdit, Ui, Vec2, Widget, TextEdit, Ui, Vec2, Widget,
}; };
use egui_extras::{Size, StripBuilder}; use egui_extras::{Size, StripBuilder};
@ -263,6 +263,15 @@ fn build_log_ui(ui: &mut Ui, status: &BuildStatus, appearance: &Appearance) {
}); });
} }
fn missing_obj_ui(ui: &mut Ui, appearance: &Appearance) {
ui.scope(|ui| {
ui.style_mut().override_text_style = Some(egui::TextStyle::Monospace);
ui.style_mut().wrap = Some(false);
ui.colored_label(appearance.replace_color, "No object configured");
});
}
pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) { pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appearance) {
let DiffViewState { build, current_view, symbol_state, search, .. } = state; let DiffViewState { build, current_view, symbol_state, search, .. } = state;
let Some(result) = build else { let Some(result) = build else {
@ -289,9 +298,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui.label("Build target:"); ui.label("Build target:");
if result.first_status.success { if result.first_status.success {
ui.label("OK"); if result.first_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else { } else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); ui.colored_label(appearance.delete_color, "Fail");
} }
}); });
@ -312,9 +325,13 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
ui.label("Build base:"); ui.label("Build base:");
if result.second_status.success { if result.second_status.success {
ui.label("OK"); if result.second_obj.is_none() {
ui.colored_label(appearance.replace_color, "Missing");
} else {
ui.label("OK");
}
} else { } else {
ui.colored_label(Rgba::from_rgb(1.0, 0.0, 0.0), "Fail"); ui.colored_label(appearance.delete_color, "Fail");
} }
}); });
@ -348,6 +365,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
&lower_search, &lower_search,
appearance, appearance,
)); ));
} else {
missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.first_status, appearance); build_log_ui(ui, &result.first_status, appearance);
@ -365,6 +384,8 @@ pub fn symbol_diff_ui(ui: &mut Ui, state: &mut DiffViewState, appearance: &Appea
&lower_search, &lower_search,
appearance, appearance,
)); ));
} else {
missing_obj_ui(ui, appearance);
} }
} else { } else {
build_log_ui(ui, &result.second_status, appearance); build_log_ui(ui, &result.second_status, appearance);