From 56dac46280f4a82d6d5a1f0e051c3cfcb84e263b Mon Sep 17 00:00:00 2001 From: Luke Street Date: Tue, 23 Sep 2025 12:11:40 -0600 Subject: [PATCH] Support overriding diff options in project config (& for individual units) (#263) * Support loading diff options from project config * Support per-unit option overrides --- Cargo.lock | 10 ++ config.schema.json | 33 ++++++ objdiff-cli/src/cmd/diff.rs | 60 +++++++---- objdiff-cli/src/cmd/report.rs | 70 ++++++++++--- objdiff-core/src/config/mod.rs | 60 +++++++++++ objdiff-gui/Cargo.toml | 1 + objdiff-gui/src/app.rs | 76 +++++++++++--- objdiff-gui/src/config.rs | 39 ++++++- objdiff-gui/src/jobs.rs | 2 +- objdiff-gui/src/views/config.rs | 162 +++++++++++++++++++++--------- objdiff-gui/src/views/demangle.rs | 6 +- 11 files changed, 414 insertions(+), 105 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a91c47f..9225573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1255,6 +1255,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "egui-notify" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cd148c4c3fe05be0d9facf90add19a1531c1d7bfb9c7e4dbc179cfb31844d49" +dependencies = [ + "egui", +] + [[package]] name = "egui-wgpu" version = "0.32.3" @@ -3564,6 +3573,7 @@ dependencies = [ "dirs", "eframe", "egui", + "egui-notify", "egui_extras", "exec", "filetime", diff --git a/config.schema.json b/config.schema.json index 21e3416..3bd59c5 100644 --- a/config.schema.json +++ b/config.schema.json @@ -111,6 +111,25 @@ "items": { "$ref": "#/$defs/progress_category" } + }, + "options": { + "type": "object", + "description": "Diff configuration options that should be applied automatically when the project is loaded.", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + }, + "examples": [ + { + "demangler": "gnu_legacy" + } + ] } }, "$defs": { @@ -156,6 +175,20 @@ "additionalProperties": { "type": "string" } + }, + "options": { + "type": "object", + "description": "Diff configuration options that should be applied when this unit is active.", + "additionalProperties": { + "oneOf": [ + { + "type": "boolean" + }, + { + "type": "string" + } + ] + } } } }, diff --git a/objdiff-cli/src/cmd/diff.rs b/objdiff-cli/src/cmd/diff.rs index f70509e..177fd40 100644 --- a/objdiff-cli/src/cmd/diff.rs +++ b/objdiff-cli/src/cmd/diff.rs @@ -24,7 +24,8 @@ use objdiff_core::{ watcher::{Watcher, create_watcher}, }, config::{ - ProjectConfig, ProjectObject, ProjectObjectMetadata, build_globset, + ProjectConfig, ProjectObject, ProjectObjectMetadata, ProjectOptions, apply_project_options, + build_globset, path::{check_path_buf, platform_path, platform_path_serde_option}, }, diff::{DiffObjConfig, MappingConfig, ObjectDiff}, @@ -77,11 +78,11 @@ pub struct Args { } pub fn run(args: Args) -> Result<()> { - let (target_path, base_path, project_config) = + let (target_path, base_path, project_config, unit_options) = match (&args.target, &args.base, &args.project, &args.unit) { (Some(_), Some(_), None, None) | (Some(_), None, None, None) - | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None), + | (None, Some(_), None, None) => (args.target.clone(), args.base.clone(), None, None), (None, None, p, u) => { let project = match p { Some(project) => project.clone(), @@ -106,28 +107,32 @@ pub fn run(args: Args) -> Result<()> { .base_dir .as_ref() .map(|p| project.join(p.with_platform_encoding())); - let objects = project_config - .units + let units = project_config.units.as_deref().unwrap_or_default(); + let objects = units .iter() - .flatten() - .map(|o| { - ObjectConfig::new( - o, - &project, - target_obj_dir.as_deref(), - base_obj_dir.as_deref(), + .enumerate() + .map(|(idx, o)| { + ( + ObjectConfig::new( + o, + &project, + target_obj_dir.as_deref(), + base_obj_dir.as_deref(), + ), + idx, ) }) .collect::>(); - let object = if let Some(u) = u { + let (object, unit_idx) = if let Some(u) = u { objects .iter() - .find(|obj| obj.name == *u) + .find(|(obj, _)| obj.name == *u) + .map(|(obj, idx)| (obj, *idx)) .ok_or_else(|| anyhow!("Unit not found: {}", u))? } else if let Some(symbol_name) = &args.symbol { let mut idx = None; let mut count = 0usize; - for (i, obj) in objects.iter().enumerate() { + for (i, (obj, unit_idx)) in objects.iter().enumerate() { if obj .target_path .as_deref() @@ -135,7 +140,7 @@ pub fn run(args: Args) -> Result<()> { .transpose()? .unwrap_or(false) { - idx = Some(i); + idx = Some((i, *unit_idx)); count += 1; if count > 1 { break; @@ -144,7 +149,7 @@ pub fn run(args: Args) -> Result<()> { } match (count, idx) { (0, None) => bail!("Symbol not found: {}", symbol_name), - (1, Some(i)) => &objects[i], + (1, Some((i, unit_idx))) => (&objects[i].0, unit_idx), (2.., Some(_)) => bail!( "Multiple instances of {} were found, try specifying a unit", symbol_name @@ -154,18 +159,29 @@ pub fn run(args: Args) -> Result<()> { } else { bail!("Must specify one of: symbol, project and unit, target and base objects") }; + let unit_options = units.get(unit_idx).and_then(|u| u.options().cloned()); let target_path = object.target_path.clone(); let base_path = object.base_path.clone(); - (target_path, base_path, Some(project_config)) + (target_path, base_path, Some(project_config), unit_options) } _ => bail!("Either target and base or project and unit must be specified"), }; - run_interactive(args, target_path, base_path, project_config) + run_interactive(args, target_path, base_path, project_config, unit_options) } -fn build_config_from_args(args: &Args) -> Result<(DiffObjConfig, MappingConfig)> { +fn build_config_from_args( + args: &Args, + project_config: Option<&ProjectConfig>, + unit_options: Option<&ProjectOptions>, +) -> Result<(DiffObjConfig, MappingConfig)> { let mut diff_config = DiffObjConfig::default(); + if let Some(options) = project_config.and_then(|config| config.options.as_ref()) { + apply_project_options(&mut diff_config, options)?; + } + if let Some(options) = unit_options { + apply_project_options(&mut diff_config, options)?; + } apply_config_args(&mut diff_config, &args.config)?; let mut mapping_config = MappingConfig { mappings: Default::default(), @@ -316,11 +332,13 @@ fn run_interactive( target_path: Option, base_path: Option, project_config: Option, + unit_options: Option, ) -> Result<()> { let Some(symbol_name) = &args.symbol else { bail!("Interactive mode requires a symbol name") }; let time_format = time::format_description::parse_borrowed::<2>("[hour]:[minute]:[second]") .context("Failed to parse time format")?; - let (diff_obj_config, mapping_config) = build_config_from_args(&args)?; + let (diff_obj_config, mapping_config) = + build_config_from_args(&args, project_config.as_ref(), unit_options.as_ref())?; let mut state = AppState { jobs: Default::default(), waker: Default::default(), diff --git a/objdiff-cli/src/cmd/report.rs b/objdiff-cli/src/cmd/report.rs index 4eee29b..afc1554 100644 --- a/objdiff-cli/src/cmd/report.rs +++ b/objdiff-cli/src/cmd/report.rs @@ -7,7 +7,7 @@ use objdiff_core::{ ChangeItem, ChangeItemInfo, ChangeUnit, Changes, ChangesInput, Measures, REPORT_VERSION, Report, ReportCategory, ReportItem, ReportItemMetadata, ReportUnit, ReportUnitMetadata, }, - config::path::platform_path, + config::{ProjectObject, ProjectOptions, apply_project_options, path::platform_path}, diff, obj::{self, SectionKind, SymbolFlag, SymbolKind}, }; @@ -83,14 +83,13 @@ pub fn run(args: Args) -> Result<()> { } fn generate(args: GenerateArgs) -> Result<()> { - let mut diff_config = diff::DiffObjConfig { + let base_diff_config = diff::DiffObjConfig { function_reloc_diffs: diff::FunctionRelocDiffs::None, combine_data_sections: true, combine_text_sections: true, ppc_calculate_pool_relocations: false, ..Default::default() }; - apply_config_args(&mut diff_config, &args.config)?; let output_format = OutputFormat::from_option(args.format.as_deref())?; let project_dir = args.project.as_deref().unwrap_or_else(|| Utf8PlatformPath::new(".")); @@ -101,31 +100,44 @@ fn generate(args: GenerateArgs) -> Result<()> { Some((Err(err), _)) => bail!("Failed to load project configuration: {}", err), None => bail!("No project configuration found"), }; - info!( - "Generating report for {} units (using {} threads)", - project.units().len(), - if args.deduplicate { 1 } else { rayon::current_num_threads() } - ); - let target_obj_dir = project.target_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); let base_obj_dir = project.base_dir.as_ref().map(|p| project_dir.join(p.with_platform_encoding())); - let objects = project - .units + let project_units = project.units.as_deref().unwrap_or_default(); + let objects = project_units .iter() - .flatten() - .map(|o| { - ObjectConfig::new(o, project_dir, target_obj_dir.as_deref(), base_obj_dir.as_deref()) + .enumerate() + .map(|(idx, o)| { + ( + ObjectConfig::new( + o, + project_dir, + target_obj_dir.as_deref(), + base_obj_dir.as_deref(), + ), + idx, + ) }) .collect::>(); + info!( + "Generating report for {} units (using {} threads)", + objects.len(), + if args.deduplicate { 1 } else { rayon::current_num_threads() } + ); let start = Instant::now(); let mut units = vec![]; let mut existing_functions: HashSet = HashSet::new(); if args.deduplicate { // If deduplicating, we need to run single-threaded - for object in &objects { + for (object, unit_idx) in &objects { + let diff_config = build_unit_diff_config( + &base_diff_config, + project.options.as_ref(), + project_units.get(*unit_idx).and_then(ProjectObject::options), + &args.config, + )?; if let Some(unit) = report_object(object, &diff_config, Some(&mut existing_functions))? { units.push(unit); @@ -134,7 +146,15 @@ fn generate(args: GenerateArgs) -> Result<()> { } else { let vec = objects .par_iter() - .map(|object| report_object(object, &diff_config, None)) + .map(|(object, unit_idx)| { + let diff_config = build_unit_diff_config( + &base_diff_config, + project.options.as_ref(), + project_units.get(*unit_idx).and_then(ProjectObject::options), + &args.config, + )?; + report_object(object, &diff_config, None) + }) .collect::>>>()?; units = vec.into_iter().flatten().collect(); } @@ -156,6 +176,24 @@ fn generate(args: GenerateArgs) -> Result<()> { Ok(()) } +fn build_unit_diff_config( + base: &diff::DiffObjConfig, + project_options: Option<&ProjectOptions>, + unit_options: Option<&ProjectOptions>, + cli_args: &[String], +) -> Result { + let mut diff_config = base.clone(); + if let Some(options) = project_options { + apply_project_options(&mut diff_config, options)?; + } + if let Some(options) = unit_options { + apply_project_options(&mut diff_config, options)?; + } + // CLI args override project and unit options + apply_config_args(&mut diff_config, cli_args)?; + Ok(diff_config) +} + fn report_object( object: &ObjectConfig, diff_config: &diff::DiffObjConfig, diff --git a/objdiff-core/src/config/mod.rs b/objdiff-core/src/config/mod.rs index 909365d..5a279e7 100644 --- a/objdiff-core/src/config/mod.rs +++ b/objdiff-core/src/config/mod.rs @@ -1,6 +1,7 @@ pub mod path; use alloc::{ + borrow::Cow, collections::BTreeMap, string::{String, ToString}, vec::Vec, @@ -45,6 +46,8 @@ pub struct ProjectConfig { pub units: Option>, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub progress_categories: Option>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub options: Option, } impl ProjectConfig { @@ -116,6 +119,8 @@ pub struct ProjectObject { pub metadata: Option, #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] pub symbol_mappings: Option>, + #[cfg_attr(feature = "serde", serde(skip_serializing_if = "Option::is_none"))] + pub options: Option, } #[derive(Default, Clone)] @@ -143,6 +148,15 @@ pub struct ProjectProgressCategory { pub name: String, } +pub type ProjectOptions = BTreeMap; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize), serde(untagged))] +pub enum ProjectOptionValue { + Bool(bool), + String(String), +} + impl ProjectObject { pub fn name(&self) -> &str { if let Some(name) = &self.name { @@ -179,6 +193,8 @@ impl ProjectObject { pub fn auto_generated(&self) -> Option { self.metadata.as_ref().and_then(|m| m.auto_generated) } + + pub fn options(&self) -> Option<&ProjectOptions> { self.options.as_ref() } } #[derive(Default, Clone, Eq, PartialEq)] @@ -310,3 +326,47 @@ pub fn build_globset(vec: &[Glob]) -> Result { } builder.build() } + +#[cfg(feature = "any-arch")] +pub fn apply_project_options( + diff_config: &mut crate::diff::DiffObjConfig, + options: &ProjectOptions, +) -> Result<()> { + use core::str::FromStr; + + use crate::diff::{ConfigEnum, ConfigPropertyId, ConfigPropertyKind}; + + let mut result = Ok(()); + for (key, value) in options.iter() { + let property_id = ConfigPropertyId::from_str(key) + .map_err(|()| anyhow!("Invalid configuration property: {key}"))?; + let value = match value { + ProjectOptionValue::Bool(value) => Cow::Borrowed(if *value { "true" } else { "false" }), + ProjectOptionValue::String(value) => Cow::Borrowed(value.as_str()), + }; + if diff_config.set_property_value_str(property_id, &value).is_err() { + if result.is_err() { + // Already returning an error, skip further errors + continue; + } + let mut expected = String::new(); + match property_id.kind() { + ConfigPropertyKind::Boolean => expected.push_str("true, false"), + ConfigPropertyKind::Choice(variants) => { + for (idx, variant) in variants.iter().enumerate() { + if idx > 0 { + expected.push_str(", "); + } + expected.push_str(variant.value); + } + } + } + result = Err(anyhow!( + "Invalid value for {}. Expected one of: {}", + property_id.name(), + expected + )); + } + } + result +} diff --git a/objdiff-gui/Cargo.toml b/objdiff-gui/Cargo.toml index b487983..1885177 100644 --- a/objdiff-gui/Cargo.toml +++ b/objdiff-gui/Cargo.toml @@ -32,6 +32,7 @@ cwdemangle = "1.0" dirs = "6.0" egui = "0.32" egui_extras = "0.32" +egui-notify = "0.20" filetime = "0.2" float-ord = "0.3" font-kit = "0.14" diff --git a/objdiff-gui/src/app.rs b/objdiff-gui/src/app.rs index b0552a5..71bdaa2 100644 --- a/objdiff-gui/src/app.rs +++ b/objdiff-gui/src/app.rs @@ -11,14 +11,15 @@ use std::{ time::Instant, }; +use egui::text::LayoutJob; use filetime::FileTime; use globset::Glob; use objdiff_core::{ build::watcher::{Watcher, create_watcher}, config::{ - ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, build_globset, - default_ignore_patterns, default_watch_patterns, path::platform_path_serde_option, - save_project_config, + ProjectConfig, ProjectConfigInfo, ProjectObject, ScratchConfig, apply_project_options, + build_globset, default_ignore_patterns, default_watch_patterns, + path::platform_path_serde_option, save_project_config, }, diff::DiffObjConfig, jobs::{Job, JobQueue, JobResult}, @@ -164,7 +165,7 @@ pub struct AppState { pub selecting_left: Option, /// The left object symbol name that we're selecting a right symbol for pub selecting_right: Option, - pub config_error: Option, + pub top_left_toasts: egui_notify::Toasts, } impl Default for AppState { @@ -183,11 +184,23 @@ impl Default for AppState { last_mod_check: Instant::now(), selecting_left: None, selecting_right: None, - config_error: None, + top_left_toasts: create_toasts(egui_notify::Anchor::TopLeft), } } } +pub fn create_toasts(anchor: egui_notify::Anchor) -> egui_notify::Toasts { + egui_notify::Toasts::default() + .with_anchor(anchor) + .with_margin(egui::vec2(10.0, 32.0)) + .with_shadow(egui::Shadow { + offset: [0, 0], + blur: 0, + spread: 1, + color: egui::Color32::GRAY, + }) +} + #[derive(Clone, serde::Deserialize, serde::Serialize)] pub struct AppConfig { // TODO: https://github.com/ron-rs/ron/pull/455 @@ -321,6 +334,24 @@ impl AppState { self.selecting_right = None; } + pub fn effective_diff_config(&self) -> DiffObjConfig { + let mut config = self.config.diff_obj_config.clone(); + if let Some(project) = self.current_project_config.as_ref() { + if let Some(options) = project.options.as_ref() { + // Ignore errors here, we display them when loading the project config + let _ = apply_project_options(&mut config, options); + } + if let Some(selected) = self.config.selected_obj.as_ref() + && let Some(units) = project.units.as_deref() + && let Some(unit) = units.iter().find(|unit| unit.name() == selected.name) + && let Some(options) = unit.options() + { + let _ = apply_project_options(&mut config, options); + } + } + config + } + pub fn set_selecting_left(&mut self, right: &str) { let Some(object) = self.config.selected_obj.as_mut() else { return; @@ -401,11 +432,22 @@ impl AppState { match save_project_config(config, info) { Ok(new_info) => *info = new_info, Err(e) => { - log::error!("Failed to save project config: {e}"); - self.config_error = Some(format!("Failed to save project config: {e}")); + log::error!("Failed to save project config: {e:#}"); + self.show_error_toast("Failed to save project config", &e); } } } + + pub fn show_error_toast(&mut self, context: &str, e: &anyhow::Error) { + let mut job = LayoutJob::default(); + job.append(context, 0.0, Default::default()); + job.append("\n", 0.0, Default::default()); + job.append(&format!("{e:#}"), 0.0, egui::TextFormat { + color: egui::Color32::LIGHT_RED, + ..Default::default() + }); + self.top_left_toasts.error(job).closable(true).duration(None); + } } pub type AppStateRef = Arc>; @@ -548,12 +590,9 @@ impl App { if state.config_change { state.config_change = false; - match load_project_config(state) { - Ok(()) => state.config_error = None, - Err(e) => { - log::error!("Failed to load project config: {e}"); - state.config_error = Some(e.to_string()); - } + if let Err(e) = load_project_config(state) { + log::error!("Failed to load project config: {e:#}"); + state.show_error_toast("Failed to load project config", &e); } } @@ -579,7 +618,10 @@ impl App { .map_err(anyhow::Error::new) }) { Ok(watcher) => self.watcher = Some(watcher), - Err(e) => log::error!("Failed to create watcher: {e}"), + Err(e) => { + log::error!("Failed to create watcher: {e:#}"); + state.show_error_toast("Failed to create file watcher", &e); + } } state.watcher_change = false; } @@ -806,7 +848,7 @@ impl eframe::App for App { let mut action = None; egui::CentralPanel::default().show(ctx, |ui| { let state = state.read().unwrap(); - action = diff_view_ui(ui, diff_state, appearance, &state.config.diff_obj_config); + action = diff_view_ui(ui, diff_state, appearance, &state.effective_diff_config()); }); project_window(ctx, state, show_project_config, config_state, appearance); @@ -818,6 +860,10 @@ impl eframe::App for App { graphics_window(ctx, show_graphics, frame_history, graphics_state, appearance); jobs_window(ctx, show_jobs, jobs, appearance); + if let Ok(mut state) = self.state.write() { + state.top_left_toasts.show(ctx); + } + self.post_update(ctx, action); } diff --git a/objdiff-gui/src/config.rs b/objdiff-gui/src/config.rs index ad25bc9..f054a5e 100644 --- a/objdiff-gui/src/config.rs +++ b/objdiff-gui/src/config.rs @@ -1,6 +1,11 @@ use anyhow::Result; use globset::Glob; -use objdiff_core::config::{default_ignore_patterns, default_watch_patterns, try_project_config}; +use objdiff_core::{ + config::{ + apply_project_options, default_ignore_patterns, default_watch_patterns, try_project_config, + }, + diff::DiffObjConfig, +}; use typed_path::{Utf8UnixComponent, Utf8UnixPath}; use crate::app::{AppState, ObjectConfig}; @@ -124,6 +129,38 @@ pub fn load_project_config(state: &mut AppState) -> Result<()> { state.object_nodes = build_nodes(&mut state.objects); state.current_project_config = Some(project_config); state.project_config_info = Some(info); + if let Some(options) = + state.current_project_config.as_ref().and_then(|project| project.options.as_ref()) + { + let mut diff_config = DiffObjConfig::default(); + if let Err(e) = apply_project_options(&mut diff_config, options) { + log::error!("Failed to apply project config options: {e:#}"); + state.show_error_toast("Failed to apply project config options", &e); + } + } + if let Some(project) = state.current_project_config.as_ref() + && let Some(units) = project.units.as_deref() + { + let mut unit_option_errors = Vec::new(); + for unit in units { + if let Some(options) = unit.options() { + let mut diff_config = DiffObjConfig::default(); + if let Err(e) = apply_project_options(&mut diff_config, options) { + unit_option_errors.push((unit.name().to_string(), e)); + } + } + } + for (unit_name, error) in unit_option_errors { + log::error!( + "Failed to apply project config options for unit {}: {error:#}", + unit_name + ); + state.show_error_toast( + &format!("Failed to apply project config options for unit {unit_name}"), + &error, + ); + } + } // Reload selected object if let Some(selected_obj) = &state.config.selected_obj { diff --git a/objdiff-gui/src/jobs.rs b/objdiff-gui/src/jobs.rs index 543191f..acce176 100644 --- a/objdiff-gui/src/jobs.rs +++ b/objdiff-gui/src/jobs.rs @@ -106,7 +106,7 @@ pub fn create_objdiff_config(state: &AppState) -> objdiff::ObjDiffConfig { .as_ref() .and_then(|obj| obj.base_path.as_ref()) .cloned(), - diff_obj_config: state.config.diff_obj_config.clone(), + diff_obj_config: state.effective_diff_config(), mapping_config: MappingConfig { mappings: state .config diff --git a/objdiff-gui/src/views/config.rs b/objdiff-gui/src/views/config.rs index 32956d1..6f76feb 100644 --- a/objdiff-gui/src/views/config.rs +++ b/objdiff-gui/src/views/config.rs @@ -519,8 +519,7 @@ fn format_path(path: &Option, appearance: &Appearance) -> R RichText::new(text).color(color).family(FontFamily::Monospace) } -pub const CONFIG_DISABLED_TEXT: &str = - "Option disabled because it's set by the project configuration file."; +pub const CONFIG_DISABLED_TEXT: &str = "Value is overridden by the project configuration."; fn pick_folder_ui( ui: &mut egui::Ui, @@ -533,8 +532,13 @@ fn pick_folder_ui( let response = ui.horizontal(|ui| { subheading(ui, label, appearance); ui.link(HELP_ICON).on_hover_ui(tooltip); - ui.add_enabled(enabled, egui::Button::new("Select")) - .on_disabled_hover_text(CONFIG_DISABLED_TEXT) + let button = ui + .add_enabled(enabled, egui::Button::new("Select")) + .on_disabled_hover_text(CONFIG_DISABLED_TEXT); + if !enabled { + project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); + } + button }); ui.label(format_path(dir, appearance)); response.inner @@ -552,17 +556,6 @@ pub fn project_window( egui::Window::new("Project").open(show).show(ctx, |ui| { split_obj_config_ui(ui, &mut state_guard, config_state, appearance); }); - - if let Some(error) = &state_guard.config_error { - let mut open = true; - egui::Window::new("Error").open(&mut open).show(ctx, |ui| { - ui.label("Failed to load project config:"); - ui.colored_label(appearance.delete_color, error); - }); - if !open { - state_guard.config_error = None; - } - } } fn split_obj_config_ui( @@ -623,6 +616,9 @@ fn split_obj_config_ui( job.append(".", 0.0, text_format.clone()); ui.label(job); }); + if state.project_config_info.is_some() { + project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); + } }); let mut custom_make_str = state.config.custom_make.clone().unwrap_or_default(); if ui @@ -831,6 +827,9 @@ fn patterns_ui( *patterns = on_reset(); change = true; } + if has_project_config { + project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); + } }); let mut remove_at: Option = None; for (idx, glob) in patterns.iter().enumerate() { @@ -885,20 +884,64 @@ pub fn arch_config_window( }); } +fn project_override_badge(ui: &mut egui::Ui) -> egui::Response { + ui.add(egui::Label::new(RichText::new("⛭").color(ui.visuals().warn_fg_color)).selectable(false)) +} + fn config_property_ui( ui: &mut egui::Ui, state: &mut AppState, property_id: ConfigPropertyId, ) -> bool { let mut changed = false; - let current_value = state.config.diff_obj_config.get_property_value(property_id); - match (property_id.kind(), current_value) { - (ConfigPropertyKind::Boolean, ConfigPropertyValue::Boolean(mut checked)) => { - let mut response = ui.checkbox(&mut checked, property_id.name()); - if let Some(description) = property_id.description() { - response = response.on_hover_text(description); - } - if response.changed() { + let is_overridden = state.current_project_config.as_ref().is_some_and(|config| { + let key = property_id.name(); + if let Some(selected) = state.config.selected_obj.as_ref() + && let Some(units) = config.units.as_deref() + && let Some(unit) = units.iter().find(|unit| unit.name() == selected.name) + && let Some(options) = unit.options() + && options.contains_key(key) + { + return true; + } + if let Some(options) = config.options.as_ref() + && options.contains_key(key) + { + return true; + } + false + }); + let override_value = + is_overridden.then(|| state.effective_diff_config().get_property_value(property_id)); + let base_value = state.config.diff_obj_config.get_property_value(property_id); + match (property_id.kind(), base_value, override_value) { + ( + ConfigPropertyKind::Boolean, + ConfigPropertyValue::Boolean(base_checked), + override_value, + ) => { + let mut checked = match override_value { + Some(ConfigPropertyValue::Boolean(value)) => value, + _ => base_checked, + }; + let response = ui + .horizontal(|ui| { + let mut response = ui + .add_enabled( + !is_overridden, + egui::widgets::Checkbox::new(&mut checked, property_id.name()), + ) + .on_disabled_hover_text(CONFIG_DISABLED_TEXT); + if let Some(description) = property_id.description() { + response = response.on_hover_text(description); + } + if is_overridden { + project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); + } + response + }) + .inner; + if !is_overridden && response.changed() { state .config .diff_obj_config @@ -907,7 +950,11 @@ fn config_property_ui( changed = true; } } - (ConfigPropertyKind::Choice(variants), ConfigPropertyValue::Choice(selected)) => { + ( + ConfigPropertyKind::Choice(variants), + ConfigPropertyValue::Choice(base_selected), + override_value, + ) => { fn variant_name(variant: &ConfigEnumVariantInfo) -> String { if variant.is_default { format!("{} (default)", variant.name) @@ -915,36 +962,51 @@ fn config_property_ui( variant.name.to_string() } } + let display_selected = match override_value { + Some(ConfigPropertyValue::Choice(value)) => value, + _ => base_selected, + }; let selected_variant = variants .iter() - .find(|v| v.value == selected) + .find(|v| v.value == display_selected) .or_else(|| variants.iter().find(|v| v.is_default)) .expect("Invalid choice variant"); - let response = egui::ComboBox::new(property_id.name(), property_id.name()) - .selected_text(variant_name(selected_variant)) - .show_ui(ui, |ui| { - for variant in variants { - let mut response = - ui.selectable_label(selected == variant.value, variant_name(variant)); - if let Some(description) = variant.description { - response = response.on_hover_text(description); - } - if response.clicked() { - state - .config - .diff_obj_config - .set_property_value( - property_id, - ConfigPropertyValue::Choice(variant.value), - ) - .expect("Failed to set property value"); - changed = true; - } - } - }) - .response; - if let Some(description) = property_id.description() { - response.on_hover_text(description); + let mut new_value: Option<&'static str> = None; + ui.horizontal(|ui| { + let inner = ui.add_enabled_ui(!is_overridden, |ui| { + egui::ComboBox::new(property_id.name(), property_id.name()) + .selected_text(variant_name(selected_variant)) + .show_ui(ui, |ui| { + for variant in variants { + let mut response = ui.selectable_label( + display_selected == variant.value, + variant_name(variant), + ); + if let Some(description) = variant.description { + response = response.on_hover_text(description); + } + if response.clicked() { + new_value = Some(variant.value); + } + } + }); + }); + let mut response = inner.response.on_disabled_hover_text(CONFIG_DISABLED_TEXT); + if let Some(description) = property_id.description() { + response = response.on_hover_text(description); + } + if is_overridden { + project_override_badge(ui).on_hover_text(CONFIG_DISABLED_TEXT); + } + response + }); + if !is_overridden && let Some(value) = new_value { + state + .config + .diff_obj_config + .set_property_value(property_id, ConfigPropertyValue::Choice(value)) + .expect("Failed to set property value"); + changed = true; } } _ => panic!("Incompatible property kind and value"), diff --git a/objdiff-gui/src/views/demangle.rs b/objdiff-gui/src/views/demangle.rs index 33097c9..55e469f 100644 --- a/objdiff-gui/src/views/demangle.rs +++ b/objdiff-gui/src/views/demangle.rs @@ -21,7 +21,11 @@ pub fn demangle_window( .show_ui(ui, |ui| { for demangler in Demangler::variants() { if *demangler != Demangler::None { - ui.selectable_value(&mut state.demangler, *demangler, demangler.name()); + let response = + ui.selectable_value(&mut state.demangler, *demangler, demangler.name()); + if let Some(description) = demangler.description() { + response.on_hover_text(description); + } } } });