Compare commits

..

No commits in common. "5bfa47fce9ab4d783951fde4aa57231a28f1d6c4" and "bf3ba485393f2169a55c0fa12124759aba0b264b" have entirely different histories.

9 changed files with 167 additions and 273 deletions

14
Cargo.lock generated
View File

@ -840,9 +840,9 @@ dependencies = [
[[package]] [[package]]
name = "cwdemangle" name = "cwdemangle"
version = "0.1.6" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c251bc5553377b3dc85c7b9b3955cfc2eb5a7b5544cf65adc2d53c2a4c2f4162" checksum = "b58d34a3a03cfe0a4ebfd03aeda6ee8a0f2e99bd3308476a8a89815add3ec373"
dependencies = [ dependencies = [
"argh", "argh",
] ]
@ -2457,7 +2457,7 @@ dependencies = [
[[package]] [[package]]
name = "objdiff" name = "objdiff"
version = "0.4.1" version = "0.4.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"byteorder", "byteorder",
@ -3044,9 +3044,9 @@ dependencies = [
[[package]] [[package]]
name = "rustls-webpki" name = "rustls-webpki"
version = "0.101.4" version = "0.101.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d93931baf2d282fff8d3a532bbfd7653f734643161b87e3e01e59a04439bf0d" checksum = "15f36a6828982f422756984e47912a7a51dcbc2a197aa791158f8ca61cd8204e"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted",
@ -4051,9 +4051,9 @@ dependencies = [
[[package]] [[package]]
name = "webpki" name = "webpki"
version = "0.22.1" version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e" checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd"
dependencies = [ dependencies = [
"ring", "ring",
"untrusted", "untrusted",

View File

@ -1,6 +1,6 @@
[package] [package]
name = "objdiff" name = "objdiff"
version = "0.4.1" version = "0.4.0"
edition = "2021" edition = "2021"
rust-version = "1.65" rust-version = "1.65"
authors = ["Luke Street <luke@street.dev>"] authors = ["Luke Street <luke@street.dev>"]
@ -27,7 +27,7 @@ byteorder = "1.4.3"
bytes = "1.4.0" bytes = "1.4.0"
cfg-if = "1.0.0" cfg-if = "1.0.0"
const_format = "0.2.31" const_format = "0.2.31"
cwdemangle = "0.1.6" cwdemangle = "0.1.5"
dirs = "5.0.1" dirs = "5.0.1"
eframe = { version = "0.22.0", features = ["persistence"] } eframe = { version = "0.22.0", features = ["persistence"] }
egui = "0.22.0" egui = "0.22.0"

View File

@ -50,11 +50,8 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
// objdiff.json // objdiff.json
{ {
"custom_make": "ninja", "custom_make": "ninja",
// Only required if objects use "path" instead of "target_path" and "base_path".
"target_dir": "build/asm", "target_dir": "build/asm",
"base_dir": "build/src", "base_dir": "build/src",
"build_target": true, "build_target": true,
"watch_patterns": [ "watch_patterns": [
"*.c", "*.c",
@ -66,15 +63,8 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
], ],
"objects": [ "objects": [
{ {
"name": "main/MetroTRK/mslsupp",
// Option 1: Relative to target_dir and base_dir
"path": "MetroTRK/mslsupp.o", "path": "MetroTRK/mslsupp.o",
// Option 2: Explicit paths from project root "name": "MetroTRK/mslsupp",
// Useful for more complex directory layouts
"target_path": "build/asm/MetroTRK/mslsupp.o",
"base_path": "build/src/MetroTRK/mslsupp.o",
"reverse_fn_order": false "reverse_fn_order": false
}, },
// ... // ...
@ -82,43 +72,25 @@ file as well. You can then add `objdiff.json` to your `.gitignore` to prevent it
} }
``` ```
`custom_make` _(optional)_: By default, objdiff will use `make` to build the project. - `custom_make` _(optional)_: By default, objdiff will use `make` to build the project.
If the project uses a different build system (e.g. `ninja`), specify it here. If the project uses a different build system (e.g. `ninja`), specify it here.
- `target_dir`: Relative from the root of the project, this where the "target" or "expected" objects are located.
`target_dir` _(optional)_: Relative from the root of the project, this where the "target" or "expected" objects are located. These are the **intended result** of the match.
These are the **intended result** of the match. - `base_dir`: Relative from the root of the project, this is where the "base" or "actual" objects are located.
These are objects built from the **current source code**.
`base_dir` _(optional)_: Relative from the root of the project, this is where the "base" or "actual" objects are located. - `build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
These are objects built from the **current source code**.
`build_target`: If true, objdiff will tell the build system to build the target objects before diffing (e.g.
`make path/to/target.o`). `make path/to/target.o`).
This is useful if the target objects are not built by default or can change based on project configuration or edits This is useful if the target objects are not built by default or can change based on project configuration or edits
to assembly files. to assembly files.
Requires the build system to be configured properly. Requires the build system to be configured properly.
- `watch_patterns` _(optional)_: A list of glob patterns to watch for changes.
`watch_patterns` _(optional)_: A list of glob patterns to watch for changes. ([Supported syntax](https://docs.rs/globset/latest/globset/#syntax))
([Supported syntax](https://docs.rs/globset/latest/globset/#syntax)) If any of these files change, objdiff will automatically rebuild the objects and re-compare them.
If any of these files change, objdiff will automatically rebuild the objects and re-compare them. - `objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation.
If not specified, objdiff will use the default patterns listed above. - `path`: Relative path to the object from the `target_dir` and `base_dir`.
- `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
`objects` _(optional)_: If specified, objdiff will display a list of objects in the sidebar for easy navigation. - `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
> `name` _(optional)_: The name of the object in the UI. If not specified, the object's `path` will be used.
>
> `path`: Relative path to the object from the `target_dir` and `base_dir`.
> Requires `target_dir` and `base_dir` to be specified.
>
> `target_path`: Path to the target object from the project root.
> Required if `path` is not specified.
>
> `base_path`: Path to the base object from the project root.
> Required if `path` is not specified.
>
> `reverse_fn_order` _(optional)_: Displays function symbols in reversed order.
Used to support MWCC's `-inline deferred` option, which reverses the order of functions in the object file.
## License ## License

View File

@ -40,18 +40,6 @@ pub struct ViewState {
pub show_project_config: bool, pub show_project_config: bool,
} }
/// The configuration for a single object file.
#[derive(Clone, Eq, PartialEq, serde::Deserialize, serde::Serialize)]
pub struct ObjectConfig {
pub name: String,
pub target_path: PathBuf,
pub base_path: PathBuf,
pub reverse_fn_order: Option<bool>,
}
#[inline]
fn bool_true() -> bool { true }
#[derive(Default, Clone, serde::Deserialize, serde::Serialize)] #[derive(Default, Clone, serde::Deserialize, serde::Serialize)]
pub struct AppConfig { pub struct AppConfig {
pub custom_make: Option<String>, pub custom_make: Option<String>,
@ -59,10 +47,9 @@ pub struct AppConfig {
pub project_dir: Option<PathBuf>, pub project_dir: Option<PathBuf>,
pub target_obj_dir: Option<PathBuf>, pub target_obj_dir: Option<PathBuf>,
pub base_obj_dir: Option<PathBuf>, pub base_obj_dir: Option<PathBuf>,
pub selected_obj: Option<ObjectConfig>, pub obj_path: Option<String>,
pub build_target: bool, pub build_target: bool,
#[serde(default = "bool_true")] pub watcher_enabled: bool,
pub rebuild_on_changes: bool,
pub auto_update_check: bool, pub auto_update_check: bool,
pub watch_patterns: Vec<Glob>, pub watch_patterns: Vec<Glob>,
@ -78,8 +65,6 @@ pub struct AppConfig {
pub obj_change: bool, pub obj_change: bool,
#[serde(skip)] #[serde(skip)]
pub queue_build: bool, pub queue_build: bool,
#[serde(skip)]
pub project_config_loaded: bool,
} }
impl AppConfig { impl AppConfig {
@ -87,7 +72,7 @@ impl AppConfig {
self.project_dir = Some(path); self.project_dir = Some(path);
self.target_obj_dir = None; self.target_obj_dir = None;
self.base_obj_dir = None; self.base_obj_dir = None;
self.selected_obj = None; self.obj_path = None;
self.build_target = false; self.build_target = false;
self.objects.clear(); self.objects.clear();
self.object_nodes.clear(); self.object_nodes.clear();
@ -95,25 +80,24 @@ impl AppConfig {
self.config_change = true; self.config_change = true;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
self.project_config_loaded = false;
} }
pub fn set_target_obj_dir(&mut self, path: PathBuf) { pub fn set_target_obj_dir(&mut self, path: PathBuf) {
self.target_obj_dir = Some(path); self.target_obj_dir = Some(path);
self.selected_obj = None; self.obj_path = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
pub fn set_base_obj_dir(&mut self, path: PathBuf) { pub fn set_base_obj_dir(&mut self, path: PathBuf) {
self.base_obj_dir = Some(path); self.base_obj_dir = Some(path);
self.selected_obj = None; self.obj_path = None;
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
pub fn set_selected_obj(&mut self, object: ObjectConfig) { pub fn set_obj_path(&mut self, path: String) {
self.selected_obj = Some(object); self.obj_path = Some(path);
self.obj_change = true; self.obj_change = true;
self.queue_build = false; self.queue_build = false;
} }
@ -274,19 +258,19 @@ impl App {
if config.obj_change { if config.obj_change {
*diff_state = Default::default(); *diff_state = Default::default();
if config.selected_obj.is_some() { if config.obj_path.is_some() {
config.queue_build = true; config.queue_build = true;
} }
config.obj_change = false; config.obj_change = false;
} }
if self.modified.swap(false, Ordering::Relaxed) && config.rebuild_on_changes { if self.modified.swap(false, Ordering::Relaxed) {
config.queue_build = true; config.queue_build = true;
} }
// Don't clear `queue_build` if a build is running. A file may have been modified during // Don't clear `queue_build` if a build is running. A file may have been modified during
// the build, so we'll start another build after the current one finishes. // the build, so we'll start another build after the current one finishes.
if config.queue_build && config.selected_obj.is_some() && !jobs.is_running(Job::ObjDiff) { if config.queue_build && config.obj_path.is_some() && !jobs.is_running(Job::ObjDiff) {
jobs.push(start_build(self.config.clone())); jobs.push(start_build(self.config.clone()));
config.queue_build = false; config.queue_build = false;
} }

View File

@ -6,7 +6,7 @@ use std::{
use anyhow::{Context, Result}; use anyhow::{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;
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
#[serde(default)] #[serde(default)]
@ -15,7 +15,7 @@ pub struct ProjectConfig {
pub target_dir: Option<PathBuf>, pub target_dir: Option<PathBuf>,
pub base_dir: Option<PathBuf>, pub base_dir: Option<PathBuf>,
pub build_target: bool, pub build_target: bool,
pub watch_patterns: Option<Vec<Glob>>, pub watch_patterns: Vec<Glob>,
#[serde(alias = "units")] #[serde(alias = "units")]
pub objects: Vec<ProjectObject>, pub objects: Vec<ProjectObject>,
} }
@ -23,24 +23,10 @@ pub struct ProjectConfig {
#[derive(Default, Clone, serde::Deserialize)] #[derive(Default, Clone, serde::Deserialize)]
pub struct ProjectObject { pub struct ProjectObject {
pub name: Option<String>, pub name: Option<String>,
pub path: Option<PathBuf>, pub path: PathBuf,
pub target_path: Option<PathBuf>,
pub base_path: Option<PathBuf>,
pub reverse_fn_order: Option<bool>, pub reverse_fn_order: Option<bool>,
} }
impl ProjectObject {
pub fn name(&self) -> &str {
if let Some(name) = &self.name {
name
} else if let Some(path) = &self.path {
path.to_str().unwrap_or("[invalid path]")
} else {
"[unknown]"
}
}
}
#[derive(Clone)] #[derive(Clone)]
pub enum ProjectObjectNode { pub enum ProjectObjectNode {
File(String, ProjectObject), File(String, ProjectObject),
@ -67,22 +53,11 @@ fn find_dir<'a>(
unreachable!(); unreachable!();
} }
fn build_nodes( fn build_nodes(objects: &[ProjectObject]) -> Vec<ProjectObjectNode> {
objects: &[ProjectObject],
project_dir: &Path,
target_obj_dir: &Option<PathBuf>,
base_obj_dir: &Option<PathBuf>,
) -> Vec<ProjectObjectNode> {
let mut nodes = vec![]; let mut nodes = vec![];
for object in objects { for object in objects {
let mut out_nodes = &mut nodes; let mut out_nodes = &mut nodes;
let path = if let Some(name) = &object.name { let path = object.name.as_ref().map(Path::new).unwrap_or(&object.path);
Path::new(name)
} else if let Some(path) = &object.path {
path
} else {
continue;
};
if let Some(parent) = path.parent() { if let Some(parent) = path.parent() {
for component in parent.components() { for component in parent.components() {
if let Component::Normal(name) = component { if let Component::Normal(name) = component {
@ -91,23 +66,8 @@ fn build_nodes(
} }
} }
} }
let mut object = object.clone();
if let (Some(target_obj_dir), Some(path), None) =
(target_obj_dir, &object.path, &object.target_path)
{
object.target_path = Some(target_obj_dir.join(path));
} else if let Some(path) = &object.target_path {
object.target_path = Some(project_dir.join(path));
}
if let (Some(base_obj_dir), Some(path), None) =
(base_obj_dir, &object.path, &object.base_path)
{
object.base_path = Some(base_obj_dir.join(path));
} else if let Some(path) = &object.base_path {
object.base_path = Some(project_dir.join(path));
}
let filename = path.file_name().unwrap().to_str().unwrap().to_string(); let filename = path.file_name().unwrap().to_str().unwrap().to_string();
out_nodes.push(ProjectObjectNode::File(filename, object)); out_nodes.push(ProjectObjectNode::File(filename, object.clone()));
} }
nodes nodes
} }
@ -124,14 +84,10 @@ pub fn load_project_config(config: &mut AppConfig) -> Result<()> {
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));
config.build_target = project_config.build_target; config.build_target = project_config.build_target;
config.watch_patterns = project_config.watch_patterns.unwrap_or_else(|| { config.watch_patterns = project_config.watch_patterns;
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect()
});
config.watcher_change = true; config.watcher_change = true;
config.objects = project_config.objects; config.objects = project_config.objects;
config.object_nodes = config.object_nodes = build_nodes(&config.objects);
build_nodes(&config.objects, project_dir, &config.target_obj_dir, &config.base_obj_dir);
config.project_config_loaded = true;
} }
Ok(()) Ok(())
} }

View File

@ -1,6 +1,6 @@
use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver}; use std::{path::Path, process::Command, str::from_utf8, sync::mpsc::Receiver};
use anyhow::{anyhow, Context, Error, Result}; use anyhow::{Context, Error, Result};
use time::OffsetDateTime; use time::OffsetDateTime;
use crate::{ use crate::{
@ -76,51 +76,47 @@ fn run_build(
config: AppConfigRef, config: AppConfigRef,
) -> Result<Box<ObjDiffResult>> { ) -> Result<Box<ObjDiffResult>> {
let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone(); let config = config.read().map_err(|_| Error::msg("Failed to lock app config"))?.clone();
let obj_config = config.selected_obj.as_ref().ok_or_else(|| Error::msg("Missing obj path"))?; let obj_path = config.obj_path.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 mut target_path = config
anyhow!( .target_obj_dir
"Target path '{}' doesn't begin with '{}'", .as_ref()
obj_config.target_path.display(), .ok_or_else(|| Error::msg("Missing target obj dir"))?
project_dir.display() .to_owned();
) target_path.push(obj_path);
})?; let mut base_path =
let base_path_rel = obj_config.base_path.strip_prefix(project_dir).map_err(|_| { config.base_obj_dir.as_ref().ok_or_else(|| Error::msg("Missing base obj dir"))?.to_owned();
anyhow!( base_path.push(obj_path);
"Base path '{}' doesn't begin with '{}'", let target_path_rel = target_path
obj_config.base_path.display(), .strip_prefix(project_dir)
project_dir.display() .context("Failed to create relative target obj path")?;
) let base_path_rel =
})?; base_path.strip_prefix(project_dir).context("Failed to create relative base obj path")?;
let total = if config.build_target { 5 } else { 4 }; let total = if config.build_target { 5 } else { 4 };
let first_status = if config.build_target { let first_status = if config.build_target {
update_status(status, format!("Building target {}", target_path_rel.display()), 0, total, &cancel)?; update_status(status, format!("Building target {obj_path}"), 0, total, &cancel)?;
run_make(project_dir, target_path_rel, &config) run_make(project_dir, target_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)?; update_status(status, format!("Building base {obj_path}"), 1, total, &cancel)?;
let second_status = run_make(project_dir, base_path_rel, &config); 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 = if first_status.success {
update_status(status, format!("Loading target {}", target_path_rel.display()), 2, total, &cancel)?; update_status(status, format!("Loading target {obj_path}"), 2, total, &cancel)?;
Some(elf::read(&obj_config.target_path).with_context(|| { Some(elf::read(&target_path)?)
format!("Failed to read object '{}'", obj_config.target_path.display())
})?)
} else { } else {
None None
}; };
let mut second_obj = if second_status.success { let mut second_obj = if second_status.success {
update_status(status, format!("Loading base {}", base_path_rel.display()), 3, total, &cancel)?; update_status(status, format!("Loading base {obj_path}"), 3, total, &cancel)?;
Some(elf::read(&obj_config.base_path).with_context(|| { Some(elf::read(&base_path)?)
format!("Failed to read object '{}'", obj_config.base_path.display())
})?)
} else { } else {
None None
}; };

View File

@ -17,7 +17,7 @@ use globset::Glob;
use self_update::cargo_crate_version; use self_update::cargo_crate_version;
use crate::{ use crate::{
app::{AppConfig, AppConfigRef, ObjectConfig}, app::{AppConfig, AppConfigRef},
config::{ProjectObject, ProjectObjectNode}, config::{ProjectObject, ProjectObjectNode},
jobs::{ jobs::{
check_update::{start_check_update, CheckUpdateResult}, check_update::{start_check_update, CheckUpdateResult},
@ -79,7 +79,7 @@ impl ConfigViewState {
} }
} }
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[ const DEFAULT_WATCH_PATTERNS: &[&str] = &[
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm", "*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
"*.inc", "*.py", "*.yml", "*.txt", "*.json", "*.inc", "*.py", "*.yml", "*.txt", "*.json",
]; ];
@ -129,7 +129,7 @@ pub fn config_ui(
selected_wsl_distro, selected_wsl_distro,
target_obj_dir, target_obj_dir,
base_obj_dir, base_obj_dir,
selected_obj, obj_path,
auto_update_check, auto_update_check,
objects, objects,
object_nodes, object_nodes,
@ -205,9 +205,9 @@ pub fn config_ui(
} }
}); });
let mut new_selected_obj = selected_obj.clone(); if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
if objects.is_empty() { let mut new_build_obj = obj_path.clone();
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { if objects.is_empty() {
if ui.button("Select object").clicked() { if ui.button("Select object").clicked() {
if let Some(path) = rfd::FileDialog::new() if let Some(path) = rfd::FileDialog::new()
.set_directory(&target_dir) .set_directory(&target_dir)
@ -215,99 +215,88 @@ pub fn config_ui(
.pick_file() .pick_file()
{ {
if let Ok(obj_path) = path.strip_prefix(&base_dir) { if let Ok(obj_path) = path.strip_prefix(&base_dir) {
let target_path = target_dir.join(obj_path); new_build_obj = Some(obj_path.display().to_string());
new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(),
target_path,
base_path: path,
reverse_fn_order: 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); new_build_obj = Some(obj_path.display().to_string());
new_selected_obj = Some(ObjectConfig {
name: obj_path.display().to_string(),
target_path: path,
base_path,
reverse_fn_order: None,
});
} }
} }
} }
if let Some(obj) = selected_obj { if let Some(obj) = obj_path {
ui.label( ui.label(
RichText::new(&obj.name) RichText::new(&*obj)
.color(appearance.replace_color) .color(appearance.replace_color)
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); );
} }
} else { } else {
ui.colored_label(appearance.delete_color, "Missing project settings"); let had_search = !state.object_search.is_empty();
} egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
} else {
let had_search = !state.object_search.is_empty();
egui::TextEdit::singleline(&mut state.object_search).hint_text("Filter").ui(ui);
let mut root_open = None; let mut root_open = None;
let mut node_open = NodeOpen::Default; let mut node_open = NodeOpen::Default;
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() { if ui.small_button("").on_hover_text_at_pointer("Collapse all").clicked() {
root_open = Some(false); root_open = Some(false);
node_open = NodeOpen::Close; node_open = NodeOpen::Close;
} }
if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() { if ui.small_button("").on_hover_text_at_pointer("Expand all").clicked() {
root_open = Some(true);
node_open = NodeOpen::Open;
}
if ui
.add_enabled(obj_path.is_some(), egui::Button::new("").small())
.on_hover_text_at_pointer("Current object")
.clicked()
{
root_open = Some(true);
node_open = NodeOpen::Object;
}
});
if state.object_search.is_empty() {
if had_search {
root_open = Some(true);
node_open = NodeOpen::Object;
}
} else if !had_search {
root_open = Some(true); root_open = Some(true);
node_open = NodeOpen::Open; node_open = NodeOpen::Open;
} }
if ui
.add_enabled(selected_obj.is_some(), egui::Button::new("").small()) CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId {
.on_hover_text_at_pointer("Current object") size: appearance.ui_font.size,
.clicked() family: appearance.code_font.family.clone(),
{ }))
root_open = Some(true); .open(root_open)
node_open = NodeOpen::Object; .default_open(true)
} .show(ui, |ui| {
}); let mut nodes = Cow::Borrowed(object_nodes);
if state.object_search.is_empty() { if !state.object_search.is_empty() {
if had_search { let search = state.object_search.to_ascii_lowercase();
root_open = Some(true); nodes = Cow::Owned(
node_open = NodeOpen::Object; object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(),
} );
} else if !had_search { }
root_open = Some(true);
node_open = NodeOpen::Open; ui.style_mut().wrap = Some(false);
for node in nodes.iter() {
display_node(ui, &mut new_build_obj, node, appearance, node_open);
}
});
} }
CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId { if new_build_obj != *obj_path {
size: appearance.ui_font.size, if let Some(obj) = new_build_obj {
family: appearance.code_font.family.clone(), // Will set obj_changed, which will trigger a rebuild
})) config_guard.set_obj_path(obj);
.open(root_open)
.default_open(true)
.show(ui, |ui| {
let mut nodes = Cow::Borrowed(object_nodes);
if !state.object_search.is_empty() {
let search = state.object_search.to_ascii_lowercase();
nodes = Cow::Owned(
object_nodes.iter().filter_map(|node| filter_node(node, &search)).collect(),
);
} }
ui.style_mut().wrap = Some(false);
for node in nodes.iter() {
display_node(ui, &mut new_selected_obj, node, appearance, node_open);
}
});
}
if new_selected_obj != *selected_obj {
if let Some(obj) = new_selected_obj {
// Will set obj_changed, which will trigger a rebuild
config_guard.set_selected_obj(obj);
} }
} if config_guard.obj_path.is_some()
if config_guard.selected_obj.is_some() && ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked() {
{ state.queue_build = true;
state.queue_build = true; }
} else {
ui.colored_label(appearance.delete_color, "Missing project settings");
} }
ui.separator(); ui.separator();
@ -315,13 +304,13 @@ pub fn config_ui(
fn display_object( fn display_object(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, obj_path: &mut Option<String>,
name: &str, name: &str,
object: &ProjectObject, object: &ProjectObject,
appearance: &Appearance, appearance: &Appearance,
) { ) {
let object_name = object.name(); let path_string = object.path.to_string_lossy().to_string();
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name); let selected = matches!(obj_path, Some(path) if path == &path_string);
let color = if selected { appearance.emphasized_text_color } else { appearance.text_color }; let color = if selected { appearance.emphasized_text_color } else { appearance.text_color };
if SelectableLabel::new( if SelectableLabel::new(
selected, selected,
@ -335,12 +324,7 @@ fn display_object(
.ui(ui) .ui(ui)
.clicked() .clicked()
{ {
*selected_obj = Some(ObjectConfig { *obj_path = Some(path_string);
name: object_name.to_string(),
target_path: object.target_path.clone().unwrap_or_default(),
base_path: object.base_path.clone().unwrap_or_default(),
reverse_fn_order: object.reverse_fn_order,
});
} }
} }
@ -355,17 +339,17 @@ enum NodeOpen {
fn display_node( fn display_node(
ui: &mut egui::Ui, ui: &mut egui::Ui,
selected_obj: &mut Option<ObjectConfig>, obj_path: &mut Option<String>,
node: &ProjectObjectNode, node: &ProjectObjectNode,
appearance: &Appearance, appearance: &Appearance,
node_open: NodeOpen, node_open: NodeOpen,
) { ) {
match node { match node {
ProjectObjectNode::File(name, object) => { ProjectObjectNode::File(name, object) => {
display_object(ui, selected_obj, name, object, appearance); display_object(ui, obj_path, name, object, appearance);
} }
ProjectObjectNode::Dir(name, children) => { ProjectObjectNode::Dir(name, children) => {
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path)); let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path));
let open = match node_open { let open = match node_open {
NodeOpen::Default => None, NodeOpen::Default => None,
NodeOpen::Open => Some(true), NodeOpen::Open => Some(true),
@ -388,18 +372,21 @@ fn display_node(
.open(open) .open(open)
.show(ui, |ui| { .show(ui, |ui| {
for node in children { for node in children {
display_node(ui, selected_obj, node, appearance, node_open); display_node(ui, obj_path, node, appearance, node_open);
} }
}); });
} }
} }
} }
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool { fn contains_node(node: &ProjectObjectNode, path: &str) -> bool {
match node { match node {
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name, ProjectObjectNode::File(_, object) => {
let path_string = object.path.to_string_lossy().to_string();
path == path_string
}
ProjectObjectNode::Dir(_, children) => { ProjectObjectNode::Dir(_, children) => {
children.iter().any(|node| contains_node(node, selected_obj)) children.iter().any(|node| contains_node(node, path))
} }
} }
} }
@ -457,12 +444,11 @@ fn pick_folder_ui(
label: &str, label: &str,
tooltip: impl FnOnce(&mut egui::Ui), tooltip: impl FnOnce(&mut egui::Ui),
appearance: &Appearance, appearance: &Appearance,
enabled: bool,
) -> egui::Response { ) -> egui::Response {
let response = ui.horizontal(|ui| { let response = ui.horizontal(|ui| {
subheading(ui, label, appearance); subheading(ui, label, appearance);
ui.link(HELP_ICON).on_hover_ui(tooltip); ui.link(HELP_ICON).on_hover_ui(tooltip);
ui.add_enabled(enabled, egui::Button::new("Select")) ui.button("Select")
}); });
ui.label(format_path(dir, appearance)); ui.label(format_path(dir, appearance));
response.inner response.inner
@ -520,7 +506,6 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
true,
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().pick_folder() { if let Some(path) = rfd::FileDialog::new().pick_folder() {
@ -581,7 +566,6 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
!config.project_config_loaded,
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
@ -631,7 +615,6 @@ fn split_obj_config_ui(
ui.label(job); ui.label(job);
}, },
appearance, appearance,
!config.project_config_loaded,
); );
if response.clicked() { if response.clicked() {
if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() {
@ -643,7 +626,7 @@ fn split_obj_config_ui(
subheading(ui, "Watch settings", appearance); subheading(ui, "Watch settings", appearance);
let response = let response =
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| { ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| {
let mut job = LayoutJob::default(); let mut job = LayoutJob::default();
job.append( job.append(
"Automatically re-run the build & diff when files change.", "Automatically re-run the build & diff when files change.",

View File

@ -1,4 +1,4 @@
use egui::{ProgressBar, RichText, Widget}; use egui::{ProgressBar, Widget};
use crate::{jobs::JobQueue, views::appearance::Appearance}; use crate::{jobs::JobQueue, views::appearance::Appearance};
@ -31,7 +31,7 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
bar.ui(ui); bar.ui(ui);
const STATUS_LENGTH: usize = 80; const STATUS_LENGTH: usize = 80;
if let Some(err) = &status.error { if let Some(err) = &status.error {
let err_string = format!("{:#}", err); let err_string = err.to_string();
ui.colored_label( ui.colored_label(
appearance.delete_color, appearance.delete_color,
if err_string.len() > STATUS_LENGTH - 10 { if err_string.len() > STATUS_LENGTH - 10 {
@ -39,15 +39,13 @@ pub fn jobs_ui(ui: &mut egui::Ui, jobs: &mut JobQueue, appearance: &Appearance)
} else { } else {
format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7) format!("Error: {:width$}", err_string, width = STATUS_LENGTH - 7)
}, },
) );
.on_hover_text_at_pointer(RichText::new(err_string).color(appearance.delete_color));
} else { } else {
ui.label(if status.status.len() > STATUS_LENGTH - 3 { ui.label(if status.status.len() > STATUS_LENGTH - 3 {
format!("{}", &status.status[0..STATUS_LENGTH - 3]) format!("{}", &status.status[0..STATUS_LENGTH - 3])
} else { } else {
format!("{:width$}", &status.status, width = STATUS_LENGTH) format!("{:width$}", &status.status, width = STATUS_LENGTH)
}) });
.on_hover_text_at_pointer(&status.status);
} }
}); });
} }

View File

@ -62,10 +62,15 @@ impl DiffViewState {
self.symbol_state.disable_reverse_fn_order = false; self.symbol_state.disable_reverse_fn_order = false;
if let Ok(config) = config.read() { if let Ok(config) = config.read() {
if let Some(obj_config) = &config.selected_obj { if let Some(obj_path) = &config.obj_path {
if let Some(value) = obj_config.reverse_fn_order { if let Some(object) = config.objects.iter().find(|object| {
self.symbol_state.reverse_fn_order = value; let path_string = object.path.to_string_lossy().to_string();
self.symbol_state.disable_reverse_fn_order = true; &path_string == obj_path
}) {
if let Some(value) = object.reverse_fn_order {
self.symbol_state.reverse_fn_order = value;
self.symbol_state.disable_reverse_fn_order = true;
}
} }
} }
} }