diff --git a/src/app.rs b/src/app.rs index 0b0d599..47ca86f 100644 --- a/src/app.rs +++ b/src/app.rs @@ -61,6 +61,40 @@ pub struct AppConfig { pub watcher_change: bool, #[serde(skip)] pub config_change: bool, + #[serde(skip)] + pub obj_change: bool, +} + +impl AppConfig { + pub fn set_project_dir(&mut self, path: PathBuf) { + self.project_dir = Some(path); + self.target_obj_dir = None; + self.base_obj_dir = None; + self.obj_path = None; + self.build_target = false; + self.units.clear(); + self.unit_nodes.clear(); + self.watcher_change = true; + self.config_change = true; + self.obj_change = true; + } + + pub fn set_target_obj_dir(&mut self, path: PathBuf) { + self.target_obj_dir = Some(path); + self.obj_path = None; + self.obj_change = true; + } + + pub fn set_base_obj_dir(&mut self, path: PathBuf) { + self.base_obj_dir = Some(path); + self.obj_path = None; + self.obj_change = true; + } + + pub fn set_obj_path(&mut self, path: String) { + self.obj_path = Some(path); + self.obj_change = true; + } } #[derive(Default)] @@ -109,97 +143,8 @@ impl App { app.relaunch_path = relaunch_path; app } -} -impl eframe::App for App { - /// Called each time the UI needs repainting, which may be many times per second. - /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. - fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { - if self.should_relaunch { - frame.close(); - return; - } - - let Self { config, appearance, view_state, .. } = self; - ctx.set_style(appearance.apply(ctx.style().as_ref())); - - let ViewState { - jobs, - show_appearance_config, - demangle_state, - show_demangle, - diff_state, - config_state, - show_project_config, - } = view_state; - - egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { - egui::menu::bar(ui, |ui| { - ui.menu_button("File", |ui| { - if ui.button("Appearance…").clicked() { - *show_appearance_config = !*show_appearance_config; - } - if ui.button("Quit").clicked() { - frame.close(); - } - }); - ui.menu_button("Tools", |ui| { - if ui.button("Demangle…").clicked() { - *show_demangle = !*show_demangle; - } - }); - }); - }); - - if diff_state.current_view == View::FunctionDiff - && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) - { - egui::CentralPanel::default().show(ctx, |ui| { - if function_diff_ui(ui, jobs, diff_state, appearance) { - jobs.push(start_build(config.clone())); - } - }); - } else if diff_state.current_view == View::DataDiff - && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) - { - egui::CentralPanel::default().show(ctx, |ui| { - if data_diff_ui(ui, jobs, diff_state, appearance) { - jobs.push(start_build(config.clone())); - } - }); - } else { - egui::SidePanel::left("side_panel").show(ctx, |ui| { - config_ui(ui, config, jobs, show_project_config, config_state, appearance); - jobs_ui(ui, jobs, appearance); - }); - - egui::CentralPanel::default().show(ctx, |ui| { - symbol_diff_ui(ui, diff_state, appearance); - }); - } - - project_window(ctx, config, show_project_config, config_state, appearance); - appearance_window(ctx, show_appearance_config, appearance); - demangle_window(ctx, show_demangle, demangle_state, appearance); - - // Windows + request_repaint_after breaks dialogs: - // https://github.com/emilk/egui/issues/2003 - if cfg!(windows) || jobs.any_running() { - ctx.request_repaint(); - } else { - ctx.request_repaint_after(Duration::from_millis(100)); - } - } - - /// Called by the frame work to save state before shutdown. - fn save(&mut self, storage: &mut dyn eframe::Storage) { - if let Ok(config) = self.config.read() { - eframe::set_value(storage, CONFIG_KEY, &*config); - } - eframe::set_value(storage, APPEARANCE_KEY, &self.appearance); - } - - fn post_rendering(&mut self, _window_size_px: [u32; 2], _frame: &eframe::Frame) { + fn pre_update(&mut self) { let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; for (job, result) in jobs.iter_finished() { @@ -251,61 +196,168 @@ impl eframe::App for App { } } jobs.clear_finished(); + } - if let Ok(mut config) = self.config.write() { - let config = &mut *config; + fn post_update(&mut self) { + let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state; + let Ok(mut config) = self.config.write() else { + return; + }; + let config = &mut *config; - if self.config_modified.swap(false, Ordering::Relaxed) { - config.config_change = true; - } + if self.config_modified.swap(false, Ordering::Relaxed) { + config.config_change = true; + } - if config.config_change { - config.config_change = false; - match load_project_config(config) { - Ok(()) => config_state.load_error = None, - Err(e) => { - log::error!("Failed to load project config: {e}"); - config_state.load_error = Some(format!("{e}")); - } + if config.config_change { + config.config_change = false; + match load_project_config(config) { + Ok(()) => config_state.load_error = None, + Err(e) => { + log::error!("Failed to load project config: {e}"); + config_state.load_error = Some(format!("{e}")); } } - - if config.watcher_change { - drop(self.watcher.take()); - - if let Some(project_dir) = &config.project_dir { - if !config.watch_patterns.is_empty() { - match build_globset(&config.watch_patterns) - .map_err(anyhow::Error::new) - .and_then(|globset| { - create_watcher( - self.modified.clone(), - self.config_modified.clone(), - project_dir, - globset, - ) - .map_err(anyhow::Error::new) - }) { - Ok(watcher) => self.watcher = Some(watcher), - Err(e) => log::error!("Failed to create watcher: {e}"), - } - } - config.watcher_change = false; - } - } - - if config.obj_path.is_some() - && self.modified.swap(false, Ordering::Relaxed) - && !jobs.is_running(Job::ObjDiff) - { - jobs.push(start_build(self.config.clone())); - } - - if config_state.queue_update_check { - jobs.push(start_check_update()); - config_state.queue_update_check = false; - } } + + if config.watcher_change { + drop(self.watcher.take()); + + if let Some(project_dir) = &config.project_dir { + if !config.watch_patterns.is_empty() { + match build_globset(&config.watch_patterns) + .map_err(anyhow::Error::new) + .and_then(|globset| { + create_watcher( + self.modified.clone(), + self.config_modified.clone(), + project_dir, + globset, + ) + .map_err(anyhow::Error::new) + }) { + Ok(watcher) => self.watcher = Some(watcher), + Err(e) => log::error!("Failed to create watcher: {e}"), + } + } + config.watcher_change = false; + } + } + + if config.obj_path.is_some() + && self.modified.swap(false, Ordering::Relaxed) + && !jobs.is_running(Job::ObjDiff) + { + jobs.push(start_build(self.config.clone())); + } + + if config.obj_change { + *diff_state = Default::default(); + jobs.push(start_build(self.config.clone())); + config.obj_change = false; + } + + if config_state.queue_update_check { + jobs.push(start_check_update()); + config_state.queue_update_check = false; + } + } +} + +impl eframe::App for App { + /// Called each time the UI needs repainting, which may be many times per second. + /// Put your widgets into a `SidePanel`, `TopPanel`, `CentralPanel`, `Window` or `Area`. + fn update(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) { + if self.should_relaunch { + frame.close(); + return; + } + + self.pre_update(); + + let Self { config, appearance, view_state, .. } = self; + ctx.set_style(appearance.apply(ctx.style().as_ref())); + + let ViewState { + jobs, + show_appearance_config, + demangle_state, + show_demangle, + diff_state, + config_state, + show_project_config, + } = view_state; + + egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { + egui::menu::bar(ui, |ui| { + ui.menu_button("File", |ui| { + if ui.button("Appearance…").clicked() { + *show_appearance_config = !*show_appearance_config; + ui.close_menu(); + } + if ui.button("Quit").clicked() { + frame.close(); + } + }); + ui.menu_button("Tools", |ui| { + if ui.button("Demangle…").clicked() { + *show_demangle = !*show_demangle; + ui.close_menu(); + } + }); + }); + }); + + if diff_state.current_view == View::FunctionDiff + && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) + { + egui::CentralPanel::default().show(ctx, |ui| { + if function_diff_ui(ui, jobs, diff_state, appearance) { + jobs.push(start_build(config.clone())); + } + }); + } else if diff_state.current_view == View::DataDiff + && matches!(&diff_state.build, Some(b) if b.first_status.success && b.second_status.success) + { + egui::CentralPanel::default().show(ctx, |ui| { + if data_diff_ui(ui, jobs, diff_state, appearance) { + jobs.push(start_build(config.clone())); + } + }); + } else { + egui::SidePanel::left("side_panel").show(ctx, |ui| { + egui::ScrollArea::both().show(ui, |ui| { + config_ui(ui, config, jobs, show_project_config, config_state, appearance); + jobs_ui(ui, jobs, appearance); + }); + }); + + egui::CentralPanel::default().show(ctx, |ui| { + symbol_diff_ui(ui, diff_state, appearance); + }); + } + + project_window(ctx, config, show_project_config, config_state, appearance); + appearance_window(ctx, show_appearance_config, appearance); + demangle_window(ctx, show_demangle, demangle_state, appearance); + + self.post_update(); + + // Windows + request_repaint_after breaks dialogs: + // https://github.com/emilk/egui/issues/2003 + if cfg!(windows) || self.view_state.jobs.any_running() { + ctx.request_repaint(); + } else { + ctx.request_repaint_after(Duration::from_millis(100)); + } + } + + /// Called by the frame work to save state before shutdown. + fn save(&mut self, storage: &mut dyn eframe::Storage) { + if let Ok(config) = self.config.read() { + eframe::set_value(storage, CONFIG_KEY, &*config); + } + eframe::set_value(storage, APPEARANCE_KEY, &self.appearance); } } diff --git a/src/views/config.rs b/src/views/config.rs index 007ea05..c5e9c8b 100644 --- a/src/views/config.rs +++ b/src/views/config.rs @@ -1,6 +1,7 @@ #[cfg(windows)] use std::string::FromUtf16Error; use std::{ + borrow::Cow, path::{PathBuf, MAIN_SEPARATOR}, sync::{Arc, RwLock}, }; @@ -29,6 +30,7 @@ pub struct ConfigViewState { pub watch_pattern_text: String, pub queue_update_check: bool, pub load_error: Option, + pub unit_search: String, #[cfg(windows)] pub available_wsl_distros: Option>, } @@ -139,9 +141,9 @@ pub fn config_ui( state.available_wsl_distros = Some(fetch_wsl2_distros()); } egui::ComboBox::from_label("Run in WSL2") - .selected_text(selected_wsl_distro.as_ref().unwrap_or(&"None".to_string())) + .selected_text(selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string())) .show_ui(ui, |ui| { - ui.selectable_value(selected_wsl_distro, None, "None"); + ui.selectable_value(selected_wsl_distro, None, "Disabled"); for distro in state.available_wsl_distros.as_ref().unwrap() { ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro); } @@ -163,7 +165,7 @@ pub fn config_ui( if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) { let mut new_build_obj = obj_path.clone(); if units.is_empty() { - if ui.button("Select obj").clicked() { + if ui.button("Select object").clicked() { if let Some(path) = rfd::FileDialog::new() .set_directory(&target_dir) .add_filter("Object file", &["o", "elf"]) @@ -177,33 +179,81 @@ pub fn config_ui( } } if let Some(obj) = obj_path { - ui.label(&*obj); + ui.label( + RichText::new(&*obj) + .color(appearance.replace_color) + .family(FontFamily::Monospace), + ); } } else { - CollapsingHeader::new(RichText::new("Objects").font(FontId { + let had_search = !state.unit_search.is_empty(); + egui::TextEdit::singleline(&mut state.unit_search).hint_text("Filter").ui(ui); + + let mut root_open = None; + let mut node_open = NodeOpen::Default; + ui.horizontal(|ui| { + if ui.small_button("⏶").on_hover_text_at_pointer("Collapse all").clicked() { + root_open = Some(false); + node_open = NodeOpen::Close; + } + 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.unit_search.is_empty() { + if had_search { + root_open = Some(true); + node_open = NodeOpen::Object; + } + } else if !had_search { + root_open = Some(true); + node_open = NodeOpen::Open; + } + + CollapsingHeader::new(RichText::new("🗀 Objects").font(FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone(), })) + .open(root_open) .default_open(true) .show(ui, |ui| { - for node in unit_nodes { - display_node(ui, &mut new_build_obj, node, appearance); + let mut nodes = Cow::Borrowed(unit_nodes); + if !state.unit_search.is_empty() { + let search = state.unit_search.to_ascii_lowercase(); + nodes = Cow::Owned( + unit_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_build_obj, node, appearance, node_open); } }); } - let mut build = false; if new_build_obj != *obj_path { - *obj_path = new_build_obj; - // TODO apply reverse_fn_order - build = true; + if let Some(obj) = new_build_obj { + // Will set obj_changed, which will trigger a rebuild + config_guard.set_obj_path(obj); + // TODO apply reverse_fn_order + } } - if obj_path.is_some() && ui.button("Build").clicked() { - build = true; - } - if build { + if config_guard.obj_path.is_some() && ui.button("Build").clicked() { + // Rebuild immediately jobs.push(start_build(config.clone())); } + } else { + ui.colored_label(appearance.delete_color, "Missing project settings"); } // ui.checkbox(&mut view_config.reverse_fn_order, "Reverse function order (deferred)"); @@ -221,10 +271,12 @@ fn display_unit( let selected = matches!(obj_path, Some(path) if path == &path_string); if SelectableLabel::new( selected, - RichText::new(name).font(FontId { - size: appearance.ui_font.size, - family: appearance.code_font.family.clone(), - }), + RichText::new(name) + .font(FontId { + size: appearance.ui_font.size, + family: appearance.code_font.family.clone(), + }) + .color(appearance.text_color), ) .ui(ui) .clicked() @@ -233,31 +285,91 @@ fn display_unit( } } +#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)] +enum NodeOpen { + #[default] + Default, + Open, + Close, + Object, +} + fn display_node( ui: &mut egui::Ui, obj_path: &mut Option, node: &ProjectUnitNode, appearance: &Appearance, + node_open: NodeOpen, ) { match node { ProjectUnitNode::File(name, unit) => { display_unit(ui, obj_path, name, unit, appearance); } ProjectUnitNode::Dir(name, children) => { - CollapsingHeader::new(RichText::new(name).font(FontId { - size: appearance.ui_font.size, - family: appearance.code_font.family.clone(), - })) - .default_open(false) + let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path)); + let open = match node_open { + NodeOpen::Default => None, + NodeOpen::Open => Some(true), + NodeOpen::Close => Some(false), + NodeOpen::Object => contains_obj, + }; + let color = if contains_obj == Some(true) { + appearance.replace_color + } else { + appearance.text_color + }; + CollapsingHeader::new( + RichText::new(name) + .font(FontId { + size: appearance.ui_font.size, + family: appearance.code_font.family.clone(), + }) + .color(color), + ) + .open(open) .show(ui, |ui| { for node in children { - display_node(ui, obj_path, node, appearance); + display_node(ui, obj_path, node, appearance, node_open); } }); } } } +fn contains_node(node: &ProjectUnitNode, path: &str) -> bool { + match node { + ProjectUnitNode::File(_, unit) => { + let path_string = unit.path.to_string_lossy().to_string(); + path == path_string + } + ProjectUnitNode::Dir(_, children) => children.iter().any(|node| contains_node(node, path)), + } +} + +fn filter_node(node: &ProjectUnitNode, search: &str) -> Option { + match node { + ProjectUnitNode::File(name, _) => { + if name.to_ascii_lowercase().contains(search) { + Some(node.clone()) + } else { + None + } + } + ProjectUnitNode::Dir(name, children) => { + if name.to_ascii_lowercase().contains(search) { + return Some(node.clone()); + } + let new_children = + children.iter().filter_map(|child| filter_node(child, search)).collect::>(); + if !new_children.is_empty() { + Some(ProjectUnitNode::Dir(name.clone(), new_children)) + } else { + None + } + } + } +} + const HELP_ICON: &str = "ℹ"; fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) { @@ -283,20 +395,18 @@ fn format_path(path: &Option, appearance: &Appearance) -> RichText { fn pick_folder_ui( ui: &mut egui::Ui, - dir: &mut Option, + dir: &Option, label: &str, tooltip: impl FnOnce(&mut egui::Ui), - clicked: impl FnOnce(&mut Option), appearance: &Appearance, -) { - ui.horizontal(|ui| { +) -> egui::Response { + let response = ui.horizontal(|ui| { subheading(ui, label, appearance); ui.link(HELP_ICON).on_hover_ui(tooltip); - if ui.button("Select").clicked() { - clicked(dir); - } + ui.button("Select") }); ui.label(format_path(dir, appearance)); + response.inner } pub fn project_window( @@ -330,29 +440,15 @@ fn split_obj_config_ui( state: &mut ConfigViewState, appearance: &Appearance, ) { - let AppConfig { - custom_make, - project_dir, - target_obj_dir, - base_obj_dir, - obj_path, - build_target, - config_change, - watcher_change, - watcher_enabled, - watch_patterns, - .. - } = config; - let text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color); let code_format = TextFormat::simple( FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() }, appearance.emphasized_text_color, ); - pick_folder_ui( + let response = pick_folder_ui( ui, - project_dir, + &config.project_dir, "Project directory", |ui| { let mut job = LayoutJob::default(); @@ -364,18 +460,13 @@ fn split_obj_config_ui( ); ui.label(job); }, - |project_dir| { - if let Some(path) = rfd::FileDialog::new().pick_folder() { - *project_dir = Some(path); - *config_change = true; - *watcher_change = true; - *target_obj_dir = None; - *base_obj_dir = None; - *obj_path = None; - } - }, appearance, ); + if response.clicked() { + if let Some(path) = rfd::FileDialog::new().pick_folder() { + config.set_project_dir(path); + } + } ui.separator(); ui.horizontal(|ui| { @@ -400,20 +491,20 @@ fn split_obj_config_ui( ui.label(job); }); }); - let mut custom_make_str = custom_make.clone().unwrap_or_default(); + let mut custom_make_str = config.custom_make.clone().unwrap_or_default(); if ui.text_edit_singleline(&mut custom_make_str).changed() { if custom_make_str.is_empty() { - *custom_make = None; + config.custom_make = None; } else { - *custom_make = Some(custom_make_str); + config.custom_make = Some(custom_make_str); } } ui.separator(); - if let Some(project_dir) = project_dir { - pick_folder_ui( + if let Some(project_dir) = config.project_dir.clone() { + let response = pick_folder_ui( ui, - target_obj_dir, + &config.target_obj_dir, "Target build directory", |ui| { let mut job = LayoutJob::default(); @@ -429,16 +520,14 @@ fn split_obj_config_ui( ); ui.label(job); }, - |target_obj_dir| { - if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() - { - *target_obj_dir = Some(path); - *obj_path = None; - } - }, appearance, ); - ui.checkbox(build_target, "Build target objects").on_hover_ui(|ui| { + if response.clicked() { + if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { + config.set_target_obj_dir(path); + } + } + ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| { let mut job = LayoutJob::default(); job.append( "Tells the build system to produce the target object.\n", @@ -467,9 +556,9 @@ fn split_obj_config_ui( }); ui.separator(); - pick_folder_ui( + let response = pick_folder_ui( ui, - base_obj_dir, + &config.base_obj_dir, "Base build directory", |ui| { let mut job = LayoutJob::default(); @@ -480,42 +569,41 @@ fn split_obj_config_ui( ); ui.label(job); }, - |base_obj_dir| { - if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() - { - *base_obj_dir = Some(path); - *obj_path = None; - } - }, appearance, ); + if response.clicked() { + if let Some(path) = rfd::FileDialog::new().set_directory(&project_dir).pick_folder() { + config.set_base_obj_dir(path); + } + } ui.separator(); } subheading(ui, "Watch settings", appearance); - let response = ui.checkbox(watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| { - let mut job = LayoutJob::default(); - job.append( - "Automatically re-run the build & diff when files change.", - 0.0, - text_format.clone(), - ); - ui.label(job); - }); + let response = + ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| { + let mut job = LayoutJob::default(); + job.append( + "Automatically re-run the build & diff when files change.", + 0.0, + text_format.clone(), + ); + ui.label(job); + }); if response.changed() { - *watcher_change = true; + config.watcher_change = true; }; ui.horizontal(|ui| { - ui.label(RichText::new("File Patterns").color(appearance.text_color)); + ui.label(RichText::new("File patterns").color(appearance.text_color)); if ui.button("Reset").clicked() { - *watch_patterns = + config.watch_patterns = DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect(); - *watcher_change = true; + config.watcher_change = true; } }); let mut remove_at: Option = None; - for (idx, glob) in watch_patterns.iter().enumerate() { + for (idx, glob) in config.watch_patterns.iter().enumerate() { ui.horizontal(|ui| { ui.label( RichText::new(format!("{}", glob)) @@ -528,15 +616,15 @@ fn split_obj_config_ui( }); } if let Some(idx) = remove_at { - watch_patterns.remove(idx); - *watcher_change = true; + config.watch_patterns.remove(idx); + config.watcher_change = true; } ui.horizontal(|ui| { egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui); if ui.small_button("+").clicked() { if let Ok(glob) = Glob::new(&state.watch_pattern_text) { - watch_patterns.push(glob); - *watcher_change = true; + config.watch_patterns.push(glob); + config.watcher_change = true; state.watch_pattern_text.clear(); } }