mirror of https://github.com/encounter/objdiff.git
Updates to Objects pane & config improvements
This commit is contained in:
parent
91d11c83d6
commit
eaf0fabc2d
330
src/app.rs
330
src/app.rs
|
@ -61,6 +61,40 @@ pub struct AppConfig {
|
||||||
pub watcher_change: bool,
|
pub watcher_change: bool,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub config_change: bool,
|
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)]
|
#[derive(Default)]
|
||||||
|
@ -109,97 +143,8 @@ impl App {
|
||||||
app.relaunch_path = relaunch_path;
|
app.relaunch_path = relaunch_path;
|
||||||
app
|
app
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
impl eframe::App for App {
|
fn pre_update(&mut self) {
|
||||||
/// 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) {
|
|
||||||
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
let ViewState { jobs, diff_state, config_state, .. } = &mut self.view_state;
|
||||||
|
|
||||||
for (job, result) in jobs.iter_finished() {
|
for (job, result) in jobs.iter_finished() {
|
||||||
|
@ -251,61 +196,168 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
jobs.clear_finished();
|
jobs.clear_finished();
|
||||||
|
}
|
||||||
|
|
||||||
if let Ok(mut config) = self.config.write() {
|
fn post_update(&mut self) {
|
||||||
let config = &mut *config;
|
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) {
|
if self.config_modified.swap(false, Ordering::Relaxed) {
|
||||||
config.config_change = true;
|
config.config_change = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if config.config_change {
|
if config.config_change {
|
||||||
config.config_change = false;
|
config.config_change = false;
|
||||||
match load_project_config(config) {
|
match load_project_config(config) {
|
||||||
Ok(()) => config_state.load_error = None,
|
Ok(()) => config_state.load_error = None,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Failed to load project config: {e}");
|
log::error!("Failed to load project config: {e}");
|
||||||
config_state.load_error = Some(format!("{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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
use std::string::FromUtf16Error;
|
use std::string::FromUtf16Error;
|
||||||
use std::{
|
use std::{
|
||||||
|
borrow::Cow,
|
||||||
path::{PathBuf, MAIN_SEPARATOR},
|
path::{PathBuf, MAIN_SEPARATOR},
|
||||||
sync::{Arc, RwLock},
|
sync::{Arc, RwLock},
|
||||||
};
|
};
|
||||||
|
@ -29,6 +30,7 @@ pub struct ConfigViewState {
|
||||||
pub watch_pattern_text: String,
|
pub watch_pattern_text: String,
|
||||||
pub queue_update_check: bool,
|
pub queue_update_check: bool,
|
||||||
pub load_error: Option<String>,
|
pub load_error: Option<String>,
|
||||||
|
pub unit_search: String,
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub available_wsl_distros: Option<Vec<String>>,
|
pub available_wsl_distros: Option<Vec<String>>,
|
||||||
}
|
}
|
||||||
|
@ -139,9 +141,9 @@ pub fn config_ui(
|
||||||
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
||||||
}
|
}
|
||||||
egui::ComboBox::from_label("Run in WSL2")
|
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| {
|
.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() {
|
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
||||||
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
|
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) {
|
if let (Some(base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
||||||
let mut new_build_obj = obj_path.clone();
|
let mut new_build_obj = obj_path.clone();
|
||||||
if units.is_empty() {
|
if units.is_empty() {
|
||||||
if ui.button("Select obj").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)
|
||||||
.add_filter("Object file", &["o", "elf"])
|
.add_filter("Object file", &["o", "elf"])
|
||||||
|
@ -177,33 +179,81 @@ pub fn config_ui(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let Some(obj) = obj_path {
|
if let Some(obj) = obj_path {
|
||||||
ui.label(&*obj);
|
ui.label(
|
||||||
|
RichText::new(&*obj)
|
||||||
|
.color(appearance.replace_color)
|
||||||
|
.family(FontFamily::Monospace),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} 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,
|
size: appearance.ui_font.size,
|
||||||
family: appearance.code_font.family.clone(),
|
family: appearance.code_font.family.clone(),
|
||||||
}))
|
}))
|
||||||
|
.open(root_open)
|
||||||
.default_open(true)
|
.default_open(true)
|
||||||
.show(ui, |ui| {
|
.show(ui, |ui| {
|
||||||
for node in unit_nodes {
|
let mut nodes = Cow::Borrowed(unit_nodes);
|
||||||
display_node(ui, &mut new_build_obj, node, appearance);
|
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 {
|
if new_build_obj != *obj_path {
|
||||||
*obj_path = new_build_obj;
|
if let Some(obj) = new_build_obj {
|
||||||
// TODO apply reverse_fn_order
|
// Will set obj_changed, which will trigger a rebuild
|
||||||
build = true;
|
config_guard.set_obj_path(obj);
|
||||||
|
// TODO apply reverse_fn_order
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if obj_path.is_some() && ui.button("Build").clicked() {
|
if config_guard.obj_path.is_some() && ui.button("Build").clicked() {
|
||||||
build = true;
|
// Rebuild immediately
|
||||||
}
|
|
||||||
if build {
|
|
||||||
jobs.push(start_build(config.clone()));
|
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)");
|
// 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);
|
let selected = matches!(obj_path, Some(path) if path == &path_string);
|
||||||
if SelectableLabel::new(
|
if SelectableLabel::new(
|
||||||
selected,
|
selected,
|
||||||
RichText::new(name).font(FontId {
|
RichText::new(name)
|
||||||
size: appearance.ui_font.size,
|
.font(FontId {
|
||||||
family: appearance.code_font.family.clone(),
|
size: appearance.ui_font.size,
|
||||||
}),
|
family: appearance.code_font.family.clone(),
|
||||||
|
})
|
||||||
|
.color(appearance.text_color),
|
||||||
)
|
)
|
||||||
.ui(ui)
|
.ui(ui)
|
||||||
.clicked()
|
.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(
|
fn display_node(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
obj_path: &mut Option<String>,
|
obj_path: &mut Option<String>,
|
||||||
node: &ProjectUnitNode,
|
node: &ProjectUnitNode,
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
|
node_open: NodeOpen,
|
||||||
) {
|
) {
|
||||||
match node {
|
match node {
|
||||||
ProjectUnitNode::File(name, unit) => {
|
ProjectUnitNode::File(name, unit) => {
|
||||||
display_unit(ui, obj_path, name, unit, appearance);
|
display_unit(ui, obj_path, name, unit, appearance);
|
||||||
}
|
}
|
||||||
ProjectUnitNode::Dir(name, children) => {
|
ProjectUnitNode::Dir(name, children) => {
|
||||||
CollapsingHeader::new(RichText::new(name).font(FontId {
|
let contains_obj = obj_path.as_ref().map(|path| contains_node(node, path));
|
||||||
size: appearance.ui_font.size,
|
let open = match node_open {
|
||||||
family: appearance.code_font.family.clone(),
|
NodeOpen::Default => None,
|
||||||
}))
|
NodeOpen::Open => Some(true),
|
||||||
.default_open(false)
|
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| {
|
.show(ui, |ui| {
|
||||||
for node in children {
|
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<ProjectUnitNode> {
|
||||||
|
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::<Vec<_>>();
|
||||||
|
if !new_children.is_empty() {
|
||||||
|
Some(ProjectUnitNode::Dir(name.clone(), new_children))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const HELP_ICON: &str = "ℹ";
|
const HELP_ICON: &str = "ℹ";
|
||||||
|
|
||||||
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
|
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
|
||||||
|
@ -283,20 +395,18 @@ fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
|
||||||
|
|
||||||
fn pick_folder_ui(
|
fn pick_folder_ui(
|
||||||
ui: &mut egui::Ui,
|
ui: &mut egui::Ui,
|
||||||
dir: &mut Option<PathBuf>,
|
dir: &Option<PathBuf>,
|
||||||
label: &str,
|
label: &str,
|
||||||
tooltip: impl FnOnce(&mut egui::Ui),
|
tooltip: impl FnOnce(&mut egui::Ui),
|
||||||
clicked: impl FnOnce(&mut Option<PathBuf>),
|
|
||||||
appearance: &Appearance,
|
appearance: &Appearance,
|
||||||
) {
|
) -> egui::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);
|
||||||
if ui.button("Select").clicked() {
|
ui.button("Select")
|
||||||
clicked(dir);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
ui.label(format_path(dir, appearance));
|
ui.label(format_path(dir, appearance));
|
||||||
|
response.inner
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn project_window(
|
pub fn project_window(
|
||||||
|
@ -330,29 +440,15 @@ fn split_obj_config_ui(
|
||||||
state: &mut ConfigViewState,
|
state: &mut ConfigViewState,
|
||||||
appearance: &Appearance,
|
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 text_format = TextFormat::simple(appearance.ui_font.clone(), appearance.text_color);
|
||||||
let code_format = TextFormat::simple(
|
let code_format = TextFormat::simple(
|
||||||
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
|
FontId { size: appearance.ui_font.size, family: appearance.code_font.family.clone() },
|
||||||
appearance.emphasized_text_color,
|
appearance.emphasized_text_color,
|
||||||
);
|
);
|
||||||
|
|
||||||
pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
project_dir,
|
&config.project_dir,
|
||||||
"Project directory",
|
"Project directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
@ -364,18 +460,13 @@ fn split_obj_config_ui(
|
||||||
);
|
);
|
||||||
ui.label(job);
|
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,
|
appearance,
|
||||||
);
|
);
|
||||||
|
if response.clicked() {
|
||||||
|
if let Some(path) = rfd::FileDialog::new().pick_folder() {
|
||||||
|
config.set_project_dir(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
@ -400,20 +491,20 @@ fn split_obj_config_ui(
|
||||||
ui.label(job);
|
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 ui.text_edit_singleline(&mut custom_make_str).changed() {
|
||||||
if custom_make_str.is_empty() {
|
if custom_make_str.is_empty() {
|
||||||
*custom_make = None;
|
config.custom_make = None;
|
||||||
} else {
|
} else {
|
||||||
*custom_make = Some(custom_make_str);
|
config.custom_make = Some(custom_make_str);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
if let Some(project_dir) = project_dir {
|
if let Some(project_dir) = config.project_dir.clone() {
|
||||||
pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
target_obj_dir,
|
&config.target_obj_dir,
|
||||||
"Target build directory",
|
"Target build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
@ -429,16 +520,14 @@ fn split_obj_config_ui(
|
||||||
);
|
);
|
||||||
ui.label(job);
|
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,
|
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();
|
let mut job = LayoutJob::default();
|
||||||
job.append(
|
job.append(
|
||||||
"Tells the build system to produce the target object.\n",
|
"Tells the build system to produce the target object.\n",
|
||||||
|
@ -467,9 +556,9 @@ fn split_obj_config_ui(
|
||||||
});
|
});
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
pick_folder_ui(
|
let response = pick_folder_ui(
|
||||||
ui,
|
ui,
|
||||||
base_obj_dir,
|
&config.base_obj_dir,
|
||||||
"Base build directory",
|
"Base build directory",
|
||||||
|ui| {
|
|ui| {
|
||||||
let mut job = LayoutJob::default();
|
let mut job = LayoutJob::default();
|
||||||
|
@ -480,42 +569,41 @@ fn split_obj_config_ui(
|
||||||
);
|
);
|
||||||
ui.label(job);
|
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,
|
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();
|
ui.separator();
|
||||||
}
|
}
|
||||||
|
|
||||||
subheading(ui, "Watch settings", appearance);
|
subheading(ui, "Watch settings", appearance);
|
||||||
let response = ui.checkbox(watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| {
|
let response =
|
||||||
let mut job = LayoutJob::default();
|
ui.checkbox(&mut config.watcher_enabled, "Rebuild on changes").on_hover_ui(|ui| {
|
||||||
job.append(
|
let mut job = LayoutJob::default();
|
||||||
"Automatically re-run the build & diff when files change.",
|
job.append(
|
||||||
0.0,
|
"Automatically re-run the build & diff when files change.",
|
||||||
text_format.clone(),
|
0.0,
|
||||||
);
|
text_format.clone(),
|
||||||
ui.label(job);
|
);
|
||||||
});
|
ui.label(job);
|
||||||
|
});
|
||||||
if response.changed() {
|
if response.changed() {
|
||||||
*watcher_change = true;
|
config.watcher_change = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
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() {
|
if ui.button("Reset").clicked() {
|
||||||
*watch_patterns =
|
config.watch_patterns =
|
||||||
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
||||||
*watcher_change = true;
|
config.watcher_change = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let mut remove_at: Option<usize> = None;
|
let mut remove_at: Option<usize> = None;
|
||||||
for (idx, glob) in watch_patterns.iter().enumerate() {
|
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
ui.label(
|
ui.label(
|
||||||
RichText::new(format!("{}", glob))
|
RichText::new(format!("{}", glob))
|
||||||
|
@ -528,15 +616,15 @@ fn split_obj_config_ui(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if let Some(idx) = remove_at {
|
if let Some(idx) = remove_at {
|
||||||
watch_patterns.remove(idx);
|
config.watch_patterns.remove(idx);
|
||||||
*watcher_change = true;
|
config.watcher_change = true;
|
||||||
}
|
}
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui);
|
egui::TextEdit::singleline(&mut state.watch_pattern_text).desired_width(100.0).show(ui);
|
||||||
if ui.small_button("+").clicked() {
|
if ui.small_button("+").clicked() {
|
||||||
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
if let Ok(glob) = Glob::new(&state.watch_pattern_text) {
|
||||||
watch_patterns.push(glob);
|
config.watch_patterns.push(glob);
|
||||||
*watcher_change = true;
|
config.watcher_change = true;
|
||||||
state.watch_pattern_text.clear();
|
state.watch_pattern_text.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue