2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
use std::string::FromUtf16Error;
|
2023-08-07 17:11:56 -07:00
|
|
|
|
use std::{
|
2023-08-10 22:36:22 -07:00
|
|
|
|
borrow::Cow,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
mem::take,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
path::{PathBuf, MAIN_SEPARATOR},
|
2023-08-07 17:11:56 -07:00
|
|
|
|
};
|
2022-09-08 14:19:20 -07:00
|
|
|
|
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
use anyhow::{Context, Result};
|
2022-12-06 14:53:32 -08:00
|
|
|
|
use const_format::formatcp;
|
2023-08-07 17:11:56 -07:00
|
|
|
|
use egui::{
|
|
|
|
|
output::OpenUrl, text::LayoutJob, CollapsingHeader, FontFamily, FontId, RichText,
|
2023-11-21 08:48:18 -08:00
|
|
|
|
SelectableLabel, TextFormat, Widget, WidgetText,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
};
|
|
|
|
|
use globset::Glob;
|
2022-12-06 14:53:32 -08:00
|
|
|
|
use self_update::cargo_crate_version;
|
2022-09-11 17:31:58 -07:00
|
|
|
|
|
2022-09-08 14:19:20 -07:00
|
|
|
|
use crate::{
|
2023-09-03 06:28:22 -07:00
|
|
|
|
app::{AppConfig, AppConfigRef, ObjectConfig},
|
2023-08-12 11:18:09 -07:00
|
|
|
|
config::{ProjectObject, ProjectObjectNode},
|
2023-11-21 08:48:18 -08:00
|
|
|
|
diff::DiffAlg,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
jobs::{
|
|
|
|
|
check_update::{start_check_update, CheckUpdateResult},
|
|
|
|
|
update::start_update,
|
|
|
|
|
Job, JobQueue, JobResult,
|
|
|
|
|
},
|
2022-12-06 14:53:32 -08:00
|
|
|
|
update::RELEASE_URL,
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
views::{
|
|
|
|
|
appearance::Appearance,
|
|
|
|
|
file::{FileDialogResult, FileDialogState},
|
|
|
|
|
},
|
2022-09-08 14:19:20 -07:00
|
|
|
|
};
|
|
|
|
|
|
2023-08-09 18:53:04 -07:00
|
|
|
|
#[derive(Default)]
|
|
|
|
|
pub struct ConfigViewState {
|
|
|
|
|
pub check_update: Option<Box<CheckUpdateResult>>,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
pub check_update_running: bool,
|
|
|
|
|
pub queue_check_update: bool,
|
|
|
|
|
pub update_running: bool,
|
|
|
|
|
pub queue_update: bool,
|
|
|
|
|
pub build_running: bool,
|
|
|
|
|
pub queue_build: bool,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
pub watch_pattern_text: String,
|
|
|
|
|
pub load_error: Option<String>,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
pub object_search: String,
|
2023-09-09 20:43:12 -07:00
|
|
|
|
pub filter_diffable: bool,
|
2023-11-21 08:50:11 -08:00
|
|
|
|
pub filter_incomplete: bool,
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2023-08-09 18:53:04 -07:00
|
|
|
|
pub available_wsl_distros: Option<Vec<String>>,
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
pub file_dialog_state: FileDialogState,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-12 11:18:09 -07:00
|
|
|
|
impl ConfigViewState {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
pub fn pre_update(&mut self, jobs: &mut JobQueue, config: &AppConfigRef) {
|
2023-08-12 11:18:09 -07:00
|
|
|
|
jobs.results.retain_mut(|result| {
|
|
|
|
|
if let JobResult::CheckUpdate(result) = result {
|
|
|
|
|
self.check_update = take(result);
|
|
|
|
|
false
|
|
|
|
|
} else {
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
self.build_running = jobs.is_running(Job::ObjDiff);
|
|
|
|
|
self.check_update_running = jobs.is_running(Job::CheckUpdate);
|
|
|
|
|
self.update_running = jobs.is_running(Job::Update);
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
|
|
|
|
|
// Check async file dialog results
|
|
|
|
|
match self.file_dialog_state.poll() {
|
|
|
|
|
FileDialogResult::None => {}
|
|
|
|
|
FileDialogResult::ProjectDir(path) => {
|
|
|
|
|
let mut guard = config.write().unwrap();
|
|
|
|
|
guard.set_project_dir(path.to_path_buf());
|
|
|
|
|
}
|
|
|
|
|
FileDialogResult::TargetDir(path) => {
|
|
|
|
|
let mut guard = config.write().unwrap();
|
|
|
|
|
guard.set_target_obj_dir(path.to_path_buf());
|
|
|
|
|
}
|
|
|
|
|
FileDialogResult::BaseDir(path) => {
|
|
|
|
|
let mut guard = config.write().unwrap();
|
|
|
|
|
guard.set_base_obj_dir(path.to_path_buf());
|
|
|
|
|
}
|
|
|
|
|
FileDialogResult::Object(path) => {
|
|
|
|
|
let mut guard = config.write().unwrap();
|
|
|
|
|
if let (Some(base_dir), Some(target_dir)) =
|
|
|
|
|
(&guard.base_obj_dir, &guard.target_obj_dir)
|
|
|
|
|
{
|
|
|
|
|
if let Ok(obj_path) = path.strip_prefix(base_dir) {
|
|
|
|
|
let target_path = target_dir.join(obj_path);
|
|
|
|
|
guard.set_selected_obj(ObjectConfig {
|
|
|
|
|
name: obj_path.display().to_string(),
|
|
|
|
|
target_path: Some(target_path),
|
|
|
|
|
base_path: Some(path),
|
|
|
|
|
reverse_fn_order: None,
|
|
|
|
|
complete: None,
|
|
|
|
|
});
|
|
|
|
|
} else if let Ok(obj_path) = path.strip_prefix(target_dir) {
|
|
|
|
|
let base_path = base_dir.join(obj_path);
|
|
|
|
|
guard.set_selected_obj(ObjectConfig {
|
|
|
|
|
name: obj_path.display().to_string(),
|
|
|
|
|
target_path: Some(path),
|
|
|
|
|
base_path: Some(base_path),
|
|
|
|
|
reverse_fn_order: None,
|
|
|
|
|
complete: None,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-12 11:18:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
pub fn post_update(&mut self, ctx: &egui::Context, jobs: &mut JobQueue, config: &AppConfigRef) {
|
2023-08-12 11:18:09 -07:00
|
|
|
|
if self.queue_build {
|
|
|
|
|
self.queue_build = false;
|
|
|
|
|
if let Ok(mut config) = config.write() {
|
|
|
|
|
config.queue_build = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.queue_check_update {
|
|
|
|
|
self.queue_check_update = false;
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
jobs.push_once(Job::CheckUpdate, || start_check_update(ctx));
|
2023-08-12 11:18:09 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if self.queue_update {
|
|
|
|
|
self.queue_update = false;
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
jobs.push_once(Job::Update, || start_update(ctx));
|
2023-08-12 11:18:09 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-03 06:28:22 -07:00
|
|
|
|
pub const DEFAULT_WATCH_PATTERNS: &[&str] = &[
|
2023-08-07 17:11:56 -07:00
|
|
|
|
"*.c", "*.cp", "*.cpp", "*.cxx", "*.h", "*.hp", "*.hpp", "*.hxx", "*.s", "*.S", "*.asm",
|
|
|
|
|
"*.inc", "*.py", "*.yml", "*.txt", "*.json",
|
|
|
|
|
];
|
|
|
|
|
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
fn process_utf16(bytes: &[u8]) -> Result<String, FromUtf16Error> {
|
|
|
|
|
let u16_bytes: Vec<u16> = bytes
|
|
|
|
|
.chunks_exact(2)
|
|
|
|
|
.filter_map(|c| Some(u16::from_ne_bytes(c.try_into().ok()?)))
|
|
|
|
|
.collect();
|
|
|
|
|
String::from_utf16(&u16_bytes)
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
fn wsl_cmd(args: &[&str]) -> Result<String> {
|
2022-09-11 17:45:27 -07:00
|
|
|
|
use std::{os::windows::process::CommandExt, process::Command};
|
2022-09-11 17:31:58 -07:00
|
|
|
|
let output = Command::new("wsl")
|
|
|
|
|
.args(args)
|
|
|
|
|
.creation_flags(winapi::um::winbase::CREATE_NO_WINDOW)
|
|
|
|
|
.output()
|
|
|
|
|
.context("Failed to execute wsl")?;
|
|
|
|
|
process_utf16(&output.stdout).context("Failed to process stdout")
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
fn fetch_wsl2_distros() -> Vec<String> {
|
|
|
|
|
wsl_cmd(&["-l", "-q"])
|
|
|
|
|
.map(|stdout| {
|
|
|
|
|
stdout
|
|
|
|
|
.split('\n')
|
|
|
|
|
.filter(|s| !s.trim().is_empty())
|
|
|
|
|
.map(|s| s.trim().to_string())
|
|
|
|
|
.collect()
|
|
|
|
|
})
|
|
|
|
|
.unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 18:53:04 -07:00
|
|
|
|
pub fn config_ui(
|
|
|
|
|
ui: &mut egui::Ui,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
config: &AppConfigRef,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
show_config_window: &mut bool,
|
|
|
|
|
state: &mut ConfigViewState,
|
|
|
|
|
appearance: &Appearance,
|
|
|
|
|
) {
|
2022-09-08 14:19:20 -07:00
|
|
|
|
let mut config_guard = config.write().unwrap();
|
2022-09-11 10:52:55 -07:00
|
|
|
|
let AppConfig {
|
2022-09-11 17:31:58 -07:00
|
|
|
|
selected_wsl_distro,
|
2022-09-13 16:52:25 -07:00
|
|
|
|
target_obj_dir,
|
|
|
|
|
base_obj_dir,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
selected_obj,
|
2022-12-06 14:53:32 -08:00
|
|
|
|
auto_update_check,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
objects,
|
|
|
|
|
object_nodes,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
..
|
2022-09-11 10:52:55 -07:00
|
|
|
|
} = &mut *config_guard;
|
2022-09-08 14:19:20 -07:00
|
|
|
|
|
2022-12-06 14:53:32 -08:00
|
|
|
|
ui.heading("Updates");
|
|
|
|
|
ui.checkbox(auto_update_check, "Check for updates on startup");
|
2023-08-12 11:18:09 -07:00
|
|
|
|
if ui.add_enabled(!state.check_update_running, egui::Button::new("Check now")).clicked() {
|
|
|
|
|
state.queue_check_update = true;
|
2022-12-06 14:53:32 -08:00
|
|
|
|
}
|
|
|
|
|
ui.label(format!("Current version: {}", cargo_crate_version!())).on_hover_ui_at_pointer(|ui| {
|
|
|
|
|
ui.label(formatcp!("Git branch: {}", env!("VERGEN_GIT_BRANCH")));
|
|
|
|
|
ui.label(formatcp!("Git commit: {}", env!("VERGEN_GIT_SHA")));
|
|
|
|
|
ui.label(formatcp!("Build target: {}", env!("VERGEN_CARGO_TARGET_TRIPLE")));
|
2023-05-10 23:47:57 -07:00
|
|
|
|
ui.label(formatcp!("Debug: {}", env!("VERGEN_CARGO_DEBUG")));
|
2022-12-06 14:53:32 -08:00
|
|
|
|
});
|
2023-08-12 11:18:09 -07:00
|
|
|
|
if let Some(result) = &state.check_update {
|
|
|
|
|
ui.label(format!("Latest version: {}", result.latest_release.version));
|
|
|
|
|
if result.update_available {
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.colored_label(appearance.insert_color, "Update available");
|
2022-12-06 14:53:32 -08:00
|
|
|
|
ui.horizontal(|ui| {
|
2023-08-12 11:18:09 -07:00
|
|
|
|
if result.found_binary
|
2022-12-07 22:49:21 -08:00
|
|
|
|
&& ui
|
2023-08-12 11:18:09 -07:00
|
|
|
|
.add_enabled(!state.update_running, egui::Button::new("Automatic"))
|
2022-12-06 14:53:32 -08:00
|
|
|
|
.on_hover_text_at_pointer(
|
|
|
|
|
"Automatically download and replace the current build",
|
|
|
|
|
)
|
2022-12-07 22:49:21 -08:00
|
|
|
|
.clicked()
|
|
|
|
|
{
|
2023-08-12 11:18:09 -07:00
|
|
|
|
state.queue_update = true;
|
2022-12-06 14:53:32 -08:00
|
|
|
|
}
|
|
|
|
|
if ui
|
|
|
|
|
.button("Manual")
|
|
|
|
|
.on_hover_text_at_pointer("Open a link to the latest release on GitHub")
|
|
|
|
|
.clicked()
|
|
|
|
|
{
|
2023-05-10 23:47:57 -07:00
|
|
|
|
ui.output_mut(|output| {
|
|
|
|
|
output.open_url =
|
|
|
|
|
Some(OpenUrl { url: RELEASE_URL.to_string(), new_tab: true })
|
|
|
|
|
});
|
2022-12-06 14:53:32 -08:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ui.separator();
|
|
|
|
|
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(feature = "wsl")]
|
2022-09-11 17:31:58 -07:00
|
|
|
|
{
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui.heading("Build");
|
2023-08-09 18:53:04 -07:00
|
|
|
|
if state.available_wsl_distros.is_none() {
|
|
|
|
|
state.available_wsl_distros = Some(fetch_wsl2_distros());
|
2022-09-11 17:31:58 -07:00
|
|
|
|
}
|
|
|
|
|
egui::ComboBox::from_label("Run in WSL2")
|
2023-08-10 22:36:22 -07:00
|
|
|
|
.selected_text(selected_wsl_distro.as_ref().unwrap_or(&"Disabled".to_string()))
|
2022-09-11 17:31:58 -07:00
|
|
|
|
.show_ui(ui, |ui| {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
ui.selectable_value(selected_wsl_distro, None, "Disabled");
|
2023-08-09 18:53:04 -07:00
|
|
|
|
for distro in state.available_wsl_distros.as_ref().unwrap() {
|
2022-09-11 17:31:58 -07:00
|
|
|
|
ui.selectable_value(selected_wsl_distro, Some(distro.clone()), distro);
|
|
|
|
|
}
|
|
|
|
|
});
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui.separator();
|
2022-09-11 17:31:58 -07:00
|
|
|
|
}
|
2023-11-21 09:15:41 -08:00
|
|
|
|
#[cfg(not(feature = "wsl"))]
|
2022-09-11 17:45:27 -07:00
|
|
|
|
{
|
|
|
|
|
let _ = selected_wsl_distro;
|
|
|
|
|
}
|
2022-09-11 17:31:58 -07:00
|
|
|
|
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.heading("Project");
|
|
|
|
|
if ui.button(RichText::new("Settings")).clicked() {
|
2023-08-09 18:53:04 -07:00
|
|
|
|
*show_config_window = true;
|
2022-12-06 14:53:32 -08:00
|
|
|
|
}
|
2023-08-07 17:11:56 -07:00
|
|
|
|
});
|
2022-09-11 17:31:58 -07:00
|
|
|
|
|
2023-09-03 06:28:22 -07:00
|
|
|
|
let mut new_selected_obj = selected_obj.clone();
|
|
|
|
|
if objects.is_empty() {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
if let (Some(_base_dir), Some(target_dir)) = (base_obj_dir, target_obj_dir) {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if ui.button("Select object").clicked() {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
state.file_dialog_state.queue(
|
|
|
|
|
|| {
|
|
|
|
|
Box::pin(
|
|
|
|
|
rfd::AsyncFileDialog::new()
|
|
|
|
|
.set_directory(&target_dir)
|
|
|
|
|
.add_filter("Object file", &["o", "elf"])
|
|
|
|
|
.pick_file(),
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
FileDialogResult::Object,
|
|
|
|
|
);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
2023-09-03 06:28:22 -07:00
|
|
|
|
if let Some(obj) = selected_obj {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
ui.label(
|
2023-09-03 06:28:22 -07:00
|
|
|
|
RichText::new(&obj.name)
|
2023-08-10 22:36:22 -07:00
|
|
|
|
.color(appearance.replace_color)
|
|
|
|
|
.family(FontFamily::Monospace),
|
|
|
|
|
);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
2023-08-09 18:53:04 -07:00
|
|
|
|
} else {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
ui.colored_label(appearance.delete_color, "Missing project settings");
|
|
|
|
|
}
|
|
|
|
|
} 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 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() {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
root_open = Some(true);
|
|
|
|
|
node_open = NodeOpen::Open;
|
|
|
|
|
}
|
2023-09-03 06:28:22 -07:00
|
|
|
|
if ui
|
|
|
|
|
.add_enabled(selected_obj.is_some(), egui::Button::new("⌖").small())
|
|
|
|
|
.on_hover_text_at_pointer("Current object")
|
|
|
|
|
.clicked()
|
|
|
|
|
{
|
|
|
|
|
root_open = Some(true);
|
|
|
|
|
node_open = NodeOpen::Object;
|
|
|
|
|
}
|
2023-09-09 20:43:12 -07:00
|
|
|
|
if ui
|
|
|
|
|
.selectable_label(state.filter_diffable, "Diffable")
|
|
|
|
|
.on_hover_text_at_pointer("Only show objects with a source file")
|
|
|
|
|
.clicked()
|
|
|
|
|
{
|
|
|
|
|
state.filter_diffable = !state.filter_diffable;
|
|
|
|
|
}
|
2023-11-21 08:50:11 -08:00
|
|
|
|
if ui
|
|
|
|
|
.selectable_label(state.filter_incomplete, "Incomplete")
|
|
|
|
|
.on_hover_text_at_pointer("Only show objects not marked complete")
|
|
|
|
|
.clicked()
|
|
|
|
|
{
|
|
|
|
|
state.filter_incomplete = !state.filter_incomplete;
|
|
|
|
|
}
|
2023-09-03 06:28:22 -07:00
|
|
|
|
});
|
|
|
|
|
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);
|
|
|
|
|
node_open = NodeOpen::Open;
|
2022-09-11 10:52:55 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-09-03 06:28:22 -07:00
|
|
|
|
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| {
|
|
|
|
|
let mut nodes = Cow::Borrowed(object_nodes);
|
2023-11-21 08:50:11 -08:00
|
|
|
|
if !state.object_search.is_empty() || state.filter_diffable || state.filter_incomplete {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
let search = state.object_search.to_ascii_lowercase();
|
|
|
|
|
nodes = Cow::Owned(
|
2023-09-09 20:43:12 -07:00
|
|
|
|
object_nodes
|
|
|
|
|
.iter()
|
2023-11-21 08:50:11 -08:00
|
|
|
|
.filter_map(|node| {
|
|
|
|
|
filter_node(
|
|
|
|
|
node,
|
|
|
|
|
&search,
|
|
|
|
|
state.filter_diffable,
|
|
|
|
|
state.filter_incomplete,
|
|
|
|
|
)
|
|
|
|
|
})
|
2023-09-09 20:43:12 -07:00
|
|
|
|
.collect(),
|
2023-09-03 06:28:22 -07:00
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
}
|
2023-09-03 06:28:22 -07:00
|
|
|
|
|
|
|
|
|
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);
|
2022-09-11 10:52:55 -07:00
|
|
|
|
}
|
2023-09-03 06:28:22 -07:00
|
|
|
|
}
|
|
|
|
|
if config_guard.selected_obj.is_some()
|
|
|
|
|
&& ui.add_enabled(!state.build_running, egui::Button::new("Build")).clicked()
|
|
|
|
|
{
|
|
|
|
|
state.queue_build = true;
|
2022-09-08 14:19:20 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ui.separator();
|
|
|
|
|
}
|
2023-08-07 17:11:56 -07:00
|
|
|
|
|
2023-08-12 11:18:09 -07:00
|
|
|
|
fn display_object(
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui: &mut egui::Ui,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
selected_obj: &mut Option<ObjectConfig>,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
name: &str,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
object: &ProjectObject,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
appearance: &Appearance,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
) {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
let object_name = object.name();
|
|
|
|
|
let selected = matches!(selected_obj, Some(obj) if obj.name == object_name);
|
2023-09-09 20:43:12 -07:00
|
|
|
|
let color = if selected {
|
|
|
|
|
appearance.emphasized_text_color
|
|
|
|
|
} else if let Some(complete) = object.complete {
|
|
|
|
|
if complete {
|
|
|
|
|
appearance.insert_color
|
|
|
|
|
} else {
|
|
|
|
|
appearance.delete_color
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
appearance.text_color
|
|
|
|
|
};
|
|
|
|
|
let clicked = SelectableLabel::new(
|
2023-08-07 17:11:56 -07:00
|
|
|
|
selected,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
RichText::new(name)
|
|
|
|
|
.font(FontId {
|
|
|
|
|
size: appearance.ui_font.size,
|
|
|
|
|
family: appearance.code_font.family.clone(),
|
|
|
|
|
})
|
2023-08-12 11:18:09 -07:00
|
|
|
|
.color(color),
|
2023-08-07 17:11:56 -07:00
|
|
|
|
)
|
|
|
|
|
.ui(ui)
|
2023-09-09 20:43:12 -07:00
|
|
|
|
.clicked();
|
|
|
|
|
// Always recreate ObjectConfig if selected, in case the project config changed.
|
|
|
|
|
// ObjectConfig is compared using equality, so this won't unnecessarily trigger a rebuild.
|
|
|
|
|
if selected || clicked {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
*selected_obj = Some(ObjectConfig {
|
|
|
|
|
name: object_name.to_string(),
|
2023-09-09 20:43:12 -07:00
|
|
|
|
target_path: object.target_path.clone(),
|
|
|
|
|
base_path: object.base_path.clone(),
|
2023-09-03 06:28:22 -07:00
|
|
|
|
reverse_fn_order: object.reverse_fn_order,
|
2023-09-09 20:43:12 -07:00
|
|
|
|
complete: object.complete,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
});
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-10 22:36:22 -07:00
|
|
|
|
#[derive(Default, Copy, Clone, PartialEq, Eq, Debug)]
|
|
|
|
|
enum NodeOpen {
|
|
|
|
|
#[default]
|
|
|
|
|
Default,
|
|
|
|
|
Open,
|
|
|
|
|
Close,
|
|
|
|
|
Object,
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 17:11:56 -07:00
|
|
|
|
fn display_node(
|
|
|
|
|
ui: &mut egui::Ui,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
selected_obj: &mut Option<ObjectConfig>,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
node: &ProjectObjectNode,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
appearance: &Appearance,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
node_open: NodeOpen,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
) {
|
|
|
|
|
match node {
|
2023-08-12 11:18:09 -07:00
|
|
|
|
ProjectObjectNode::File(name, object) => {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
display_object(ui, selected_obj, name, object, appearance);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
2023-08-12 11:18:09 -07:00
|
|
|
|
ProjectObjectNode::Dir(name, children) => {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
let contains_obj = selected_obj.as_ref().map(|path| contains_node(node, path));
|
2023-08-10 22:36:22 -07:00
|
|
|
|
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)
|
2023-08-07 17:11:56 -07:00
|
|
|
|
.show(ui, |ui| {
|
|
|
|
|
for node in children {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
display_node(ui, selected_obj, node, appearance, node_open);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-03 06:28:22 -07:00
|
|
|
|
fn contains_node(node: &ProjectObjectNode, selected_obj: &ObjectConfig) -> bool {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
match node {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
ProjectObjectNode::File(_, object) => object.name() == selected_obj.name,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
ProjectObjectNode::Dir(_, children) => {
|
2023-09-03 06:28:22 -07:00
|
|
|
|
children.iter().any(|node| contains_node(node, selected_obj))
|
2023-08-12 11:18:09 -07:00
|
|
|
|
}
|
2023-08-10 22:36:22 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-09-09 20:43:12 -07:00
|
|
|
|
fn filter_node(
|
|
|
|
|
node: &ProjectObjectNode,
|
|
|
|
|
search: &str,
|
|
|
|
|
filter_diffable: bool,
|
2023-11-21 08:50:11 -08:00
|
|
|
|
filter_incomplete: bool,
|
2023-09-09 20:43:12 -07:00
|
|
|
|
) -> Option<ProjectObjectNode> {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
match node {
|
2023-09-09 20:43:12 -07:00
|
|
|
|
ProjectObjectNode::File(name, object) => {
|
|
|
|
|
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
2023-10-09 09:47:22 -07:00
|
|
|
|
&& (!filter_diffable
|
|
|
|
|
|| (object.base_path.is_some() && object.target_path.is_some()))
|
2023-11-21 08:50:11 -08:00
|
|
|
|
&& (!filter_incomplete || matches!(object.complete, None | Some(false)))
|
2023-09-09 20:43:12 -07:00
|
|
|
|
{
|
2023-08-10 22:36:22 -07:00
|
|
|
|
Some(node.clone())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-08-12 11:18:09 -07:00
|
|
|
|
ProjectObjectNode::Dir(name, children) => {
|
2023-11-21 08:50:11 -08:00
|
|
|
|
if (search.is_empty() || name.to_ascii_lowercase().contains(search))
|
|
|
|
|
&& !filter_diffable
|
|
|
|
|
&& !filter_incomplete
|
2023-09-09 20:43:12 -07:00
|
|
|
|
{
|
2023-08-10 22:36:22 -07:00
|
|
|
|
return Some(node.clone());
|
|
|
|
|
}
|
2023-09-09 20:43:12 -07:00
|
|
|
|
let new_children = children
|
|
|
|
|
.iter()
|
2023-11-21 08:50:11 -08:00
|
|
|
|
.filter_map(|child| filter_node(child, search, filter_diffable, filter_incomplete))
|
2023-09-09 20:43:12 -07:00
|
|
|
|
.collect::<Vec<_>>();
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if !new_children.is_empty() {
|
2023-08-12 11:18:09 -07:00
|
|
|
|
Some(ProjectObjectNode::Dir(name.clone(), new_children))
|
2023-08-10 22:36:22 -07:00
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 17:11:56 -07:00
|
|
|
|
const HELP_ICON: &str = "ℹ";
|
|
|
|
|
|
2023-08-09 18:53:04 -07:00
|
|
|
|
fn subheading(ui: &mut egui::Ui, text: &str, appearance: &Appearance) {
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui.label(
|
2023-08-09 18:53:04 -07:00
|
|
|
|
RichText::new(text).size(appearance.ui_font.size).color(appearance.emphasized_text_color),
|
2023-08-07 17:11:56 -07:00
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2023-08-09 18:53:04 -07:00
|
|
|
|
fn format_path(path: &Option<PathBuf>, appearance: &Appearance) -> RichText {
|
|
|
|
|
let mut color = appearance.replace_color;
|
|
|
|
|
let text = if let Some(dir) = path {
|
|
|
|
|
if let Some(rel) = dirs::home_dir().and_then(|home| dir.strip_prefix(&home).ok()) {
|
|
|
|
|
format!("~{}{}", MAIN_SEPARATOR, rel.display())
|
|
|
|
|
} else {
|
|
|
|
|
format!("{}", dir.display())
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
color = appearance.delete_color;
|
|
|
|
|
"[none]".to_string()
|
|
|
|
|
};
|
|
|
|
|
RichText::new(text).color(color).family(FontFamily::Monospace)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn pick_folder_ui(
|
|
|
|
|
ui: &mut egui::Ui,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
dir: &Option<PathBuf>,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
label: &str,
|
|
|
|
|
tooltip: impl FnOnce(&mut egui::Ui),
|
|
|
|
|
appearance: &Appearance,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
enabled: bool,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
) -> egui::Response {
|
|
|
|
|
let response = ui.horizontal(|ui| {
|
2023-08-09 18:53:04 -07:00
|
|
|
|
subheading(ui, label, appearance);
|
|
|
|
|
ui.link(HELP_ICON).on_hover_ui(tooltip);
|
2023-09-03 06:28:22 -07:00
|
|
|
|
ui.add_enabled(enabled, egui::Button::new("Select"))
|
2023-08-09 18:53:04 -07:00
|
|
|
|
});
|
|
|
|
|
ui.label(format_path(dir, appearance));
|
2023-08-10 22:36:22 -07:00
|
|
|
|
response.inner
|
2023-08-09 18:53:04 -07:00
|
|
|
|
}
|
|
|
|
|
|
2023-08-07 17:11:56 -07:00
|
|
|
|
pub fn project_window(
|
|
|
|
|
ctx: &egui::Context,
|
2023-08-12 11:18:09 -07:00
|
|
|
|
config: &AppConfigRef,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
show: &mut bool,
|
|
|
|
|
state: &mut ConfigViewState,
|
|
|
|
|
appearance: &Appearance,
|
2023-08-07 17:11:56 -07:00
|
|
|
|
) {
|
|
|
|
|
let mut config_guard = config.write().unwrap();
|
2023-08-09 18:53:04 -07:00
|
|
|
|
|
|
|
|
|
egui::Window::new("Project").open(show).show(ctx, |ui| {
|
|
|
|
|
split_obj_config_ui(ui, &mut config_guard, state, appearance);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if let Some(error) = &state.load_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.load_error = None;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn split_obj_config_ui(
|
|
|
|
|
ui: &mut egui::Ui,
|
|
|
|
|
config: &mut AppConfig,
|
|
|
|
|
state: &mut ConfigViewState,
|
|
|
|
|
appearance: &Appearance,
|
|
|
|
|
) {
|
|
|
|
|
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,
|
|
|
|
|
);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
|
2023-08-10 22:36:22 -07:00
|
|
|
|
let response = pick_folder_ui(
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
&config.project_dir,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
"Project directory",
|
|
|
|
|
|ui| {
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append("The root project directory.\n\n", 0.0, text_format.clone());
|
|
|
|
|
job.append(
|
|
|
|
|
"If a configuration file exists, it will be loaded automatically.",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
ui.label(job);
|
|
|
|
|
},
|
|
|
|
|
appearance,
|
2023-09-03 06:28:22 -07:00
|
|
|
|
true,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if response.clicked() {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
state.file_dialog_state.queue(
|
|
|
|
|
|| Box::pin(rfd::AsyncFileDialog::new().pick_folder()),
|
|
|
|
|
FileDialogResult::ProjectDir,
|
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
}
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.separator();
|
2023-08-07 17:11:56 -07:00
|
|
|
|
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
subheading(ui, "Custom make program", appearance);
|
|
|
|
|
ui.link(HELP_ICON).on_hover_ui(|ui| {
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append("By default, objdiff will build with ", 0.0, text_format.clone());
|
|
|
|
|
job.append("make", 0.0, code_format.clone());
|
|
|
|
|
job.append(
|
|
|
|
|
".\nIf the project uses a different build system (e.g. ",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
2023-08-07 17:11:56 -07:00
|
|
|
|
);
|
2023-08-09 18:53:04 -07:00
|
|
|
|
job.append("ninja", 0.0, code_format.clone());
|
|
|
|
|
job.append(
|
|
|
|
|
"), specify it here.\nThe program must be in your ",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append("PATH", 0.0, code_format.clone());
|
|
|
|
|
job.append(".", 0.0, text_format.clone());
|
|
|
|
|
ui.label(job);
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-08-10 22:36:22 -07:00
|
|
|
|
let mut custom_make_str = config.custom_make.clone().unwrap_or_default();
|
2023-08-09 18:53:04 -07:00
|
|
|
|
if ui.text_edit_singleline(&mut custom_make_str).changed() {
|
|
|
|
|
if custom_make_str.is_empty() {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.custom_make = None;
|
2023-08-09 18:53:04 -07:00
|
|
|
|
} else {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.custom_make = Some(custom_make_str);
|
2023-08-09 18:53:04 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
ui.separator();
|
2023-08-07 17:11:56 -07:00
|
|
|
|
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if let Some(project_dir) = config.project_dir.clone() {
|
|
|
|
|
let response = pick_folder_ui(
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
&config.target_obj_dir,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
"Target build directory",
|
|
|
|
|
|ui| {
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"This contains the \"target\" or \"expected\" objects, which are the intended result of the match.\n\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
2023-08-07 17:11:56 -07:00
|
|
|
|
);
|
2023-08-09 18:53:04 -07:00
|
|
|
|
job.append(
|
|
|
|
|
"These are usually created by the project's build system or assembled.",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
2023-08-07 17:11:56 -07:00
|
|
|
|
);
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.label(job);
|
|
|
|
|
},
|
|
|
|
|
appearance,
|
2023-10-07 11:48:34 -07:00
|
|
|
|
config.project_config_info.is_none(),
|
2023-08-09 18:53:04 -07:00
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if response.clicked() {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
state.file_dialog_state.queue(
|
|
|
|
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|
|
|
|
FileDialogResult::TargetDir,
|
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
}
|
|
|
|
|
ui.checkbox(&mut config.build_target, "Build target objects").on_hover_ui(|ui| {
|
2023-08-09 18:53:04 -07:00
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"Tells the build system to produce the target object.\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append("For example, this would call ", 0.0, text_format.clone());
|
|
|
|
|
job.append("make path/to/target.o", 0.0, code_format.clone());
|
|
|
|
|
job.append(".\n\n", 0.0, text_format.clone());
|
|
|
|
|
job.append(
|
|
|
|
|
"This is useful if the target objects are not already built\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append(
|
|
|
|
|
"or if they can change based on project configuration,\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append(
|
|
|
|
|
"but requires that the build system is configured correctly.",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
ui.label(job);
|
|
|
|
|
});
|
|
|
|
|
ui.separator();
|
2023-08-07 17:11:56 -07:00
|
|
|
|
|
2023-08-10 22:36:22 -07:00
|
|
|
|
let response = pick_folder_ui(
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui,
|
2023-08-10 22:36:22 -07:00
|
|
|
|
&config.base_obj_dir,
|
2023-08-09 18:53:04 -07:00
|
|
|
|
"Base build directory",
|
|
|
|
|
|ui| {
|
2023-08-07 17:11:56 -07:00
|
|
|
|
let mut job = LayoutJob::default();
|
2023-08-09 18:53:04 -07:00
|
|
|
|
job.append(
|
|
|
|
|
"This contains the objects built from your decompiled code.",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
ui.label(job);
|
2023-08-09 18:53:04 -07:00
|
|
|
|
},
|
|
|
|
|
appearance,
|
2023-10-07 11:48:34 -07:00
|
|
|
|
config.project_config_info.is_none(),
|
2023-08-09 18:53:04 -07:00
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
if response.clicked() {
|
Repaint rework: more responsive, less energy
Previously, we repainted every frame on Windows at full refresh rate.
This is an enormous waste, as the UI will be static most of the time.
This was to work around a bug with `rfd` + `eframe`.
On other platforms, we only repainted every frame when a job was running,
which was better, but still not ideal. We also had a 100ms deadline, so
we'd repaint at ~10fps minimum to catch new events (file watcher, jobs).
This removes all repaint logic from the main loop and moves it into the
individual places where we change state from another thread.
For example, the file watcher thread will now immediately notify egui
to repaint, rather than relying on the 100ms deadline we had previously.
Jobs, when updating their status, also notify egui to repaint.
For `rfd` file dialogs, this migrates to using the async API built on top of
a polling thread + `pollster`. This interacts better with `eframe` on Windows.
Overall, this should reduce repaints and improve responsiveness to
file changes and background tasks.
2023-11-21 11:34:26 -08:00
|
|
|
|
state.file_dialog_state.queue(
|
|
|
|
|
|| Box::pin(rfd::AsyncFileDialog::new().set_directory(&project_dir).pick_folder()),
|
|
|
|
|
FileDialogResult::BaseDir,
|
|
|
|
|
);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
}
|
2023-10-07 11:48:34 -07:00
|
|
|
|
ui.checkbox(&mut config.build_base, "Build base objects").on_hover_ui(|ui| {
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"Tells the build system to produce the base object.\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append("For example, this would call ", 0.0, text_format.clone());
|
|
|
|
|
job.append("make path/to/base.o", 0.0, code_format.clone());
|
|
|
|
|
job.append(".\n\n", 0.0, text_format.clone());
|
|
|
|
|
job.append(
|
|
|
|
|
"This can be disabled if you're running the build system\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append(
|
|
|
|
|
"externally, and just want objdiff to reload the files\n",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
job.append("when they change.", 0.0, text_format.clone());
|
|
|
|
|
ui.label(job);
|
|
|
|
|
});
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.separator();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
subheading(ui, "Watch settings", appearance);
|
2023-08-10 22:36:22 -07:00
|
|
|
|
let response =
|
2023-09-03 06:28:22 -07:00
|
|
|
|
ui.checkbox(&mut config.rebuild_on_changes, "Rebuild on changes").on_hover_ui(|ui| {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"Automatically re-run the build & diff when files change.",
|
|
|
|
|
0.0,
|
|
|
|
|
text_format.clone(),
|
|
|
|
|
);
|
|
|
|
|
ui.label(job);
|
|
|
|
|
});
|
2023-08-09 18:53:04 -07:00
|
|
|
|
if response.changed() {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.watcher_change = true;
|
2023-08-09 18:53:04 -07:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ui.horizontal(|ui| {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
ui.label(RichText::new("File patterns").color(appearance.text_color));
|
2023-08-09 18:53:04 -07:00
|
|
|
|
if ui.button("Reset").clicked() {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.watch_patterns =
|
2023-08-09 18:53:04 -07:00
|
|
|
|
DEFAULT_WATCH_PATTERNS.iter().map(|s| Glob::new(s).unwrap()).collect();
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.watcher_change = true;
|
2023-08-09 18:53:04 -07:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
let mut remove_at: Option<usize> = None;
|
2023-08-10 22:36:22 -07:00
|
|
|
|
for (idx, glob) in config.watch_patterns.iter().enumerate() {
|
2023-08-09 18:53:04 -07:00
|
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
ui.label(
|
|
|
|
|
RichText::new(format!("{}", glob))
|
|
|
|
|
.color(appearance.text_color)
|
|
|
|
|
.family(FontFamily::Monospace),
|
|
|
|
|
);
|
|
|
|
|
if ui.small_button("-").clicked() {
|
|
|
|
|
remove_at = Some(idx);
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
2023-08-09 18:53:04 -07:00
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
if let Some(idx) = remove_at {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.watch_patterns.remove(idx);
|
|
|
|
|
config.watcher_change = true;
|
2023-08-09 18:53:04 -07:00
|
|
|
|
}
|
|
|
|
|
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) {
|
2023-08-10 22:36:22 -07:00
|
|
|
|
config.watch_patterns.push(glob);
|
|
|
|
|
config.watcher_change = true;
|
2023-08-09 18:53:04 -07:00
|
|
|
|
state.watch_pattern_text.clear();
|
2023-08-07 17:11:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
2023-11-21 08:48:18 -08:00
|
|
|
|
|
|
|
|
|
pub fn diff_options_window(
|
|
|
|
|
ctx: &egui::Context,
|
|
|
|
|
config: &AppConfigRef,
|
|
|
|
|
show: &mut bool,
|
|
|
|
|
appearance: &Appearance,
|
|
|
|
|
) {
|
|
|
|
|
let mut config_guard = config.write().unwrap();
|
|
|
|
|
egui::Window::new("Diff Options").open(show).show(ctx, |ui| {
|
|
|
|
|
diff_options_ui(ui, &mut config_guard, appearance);
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn diff_options_ui(ui: &mut egui::Ui, config: &mut AppConfig, appearance: &Appearance) {
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"Current default: ",
|
|
|
|
|
0.0,
|
|
|
|
|
TextFormat::simple(appearance.ui_font.clone(), appearance.text_color),
|
|
|
|
|
);
|
|
|
|
|
job.append(
|
|
|
|
|
diff_alg_to_string(DiffAlg::default()),
|
|
|
|
|
0.0,
|
|
|
|
|
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
|
|
|
|
);
|
|
|
|
|
ui.label(job);
|
|
|
|
|
let mut job = LayoutJob::default();
|
|
|
|
|
job.append(
|
|
|
|
|
"Previous default: ",
|
|
|
|
|
0.0,
|
|
|
|
|
TextFormat::simple(appearance.ui_font.clone(), appearance.text_color),
|
|
|
|
|
);
|
|
|
|
|
job.append(
|
|
|
|
|
"Levenshtein",
|
|
|
|
|
0.0,
|
|
|
|
|
TextFormat::simple(appearance.ui_font.clone(), appearance.emphasized_text_color),
|
|
|
|
|
);
|
|
|
|
|
ui.label(job);
|
|
|
|
|
ui.label("Please provide feedback!");
|
|
|
|
|
if diff_alg_ui(ui, "Code diff algorithm", &mut config.code_alg) {
|
|
|
|
|
config.queue_reload = true;
|
|
|
|
|
}
|
|
|
|
|
if diff_alg_ui(ui, "Data diff algorithm", &mut config.data_alg) {
|
|
|
|
|
config.queue_reload = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn diff_alg_ui(ui: &mut egui::Ui, label: impl Into<WidgetText>, alg: &mut DiffAlg) -> bool {
|
|
|
|
|
let response = egui::ComboBox::from_label(label)
|
|
|
|
|
.selected_text(diff_alg_to_string(*alg))
|
|
|
|
|
.show_ui(ui, |ui| {
|
|
|
|
|
ui.selectable_value(alg, DiffAlg::Patience, "Patience").changed()
|
|
|
|
|
| ui.selectable_value(alg, DiffAlg::Levenshtein, "Levenshtein").changed()
|
|
|
|
|
| ui.selectable_value(alg, DiffAlg::Myers, "Myers").changed()
|
|
|
|
|
| ui.selectable_value(alg, DiffAlg::Lcs, "LCS").changed()
|
|
|
|
|
});
|
|
|
|
|
response.inner.unwrap_or(false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fn diff_alg_to_string(alg: DiffAlg) -> &'static str {
|
|
|
|
|
match alg {
|
|
|
|
|
DiffAlg::Patience => "Patience",
|
|
|
|
|
DiffAlg::Levenshtein => "Levenshtein",
|
|
|
|
|
DiffAlg::Lcs => "LCS",
|
|
|
|
|
DiffAlg::Myers => "Myers",
|
|
|
|
|
}
|
|
|
|
|
}
|