Skip to content

Commit

Permalink
Add FileSelect Widget (#12)
Browse files Browse the repository at this point in the history
Implements the FileSelection Widget as a button in the settings Gui that opens a file dialog when pressed. The user can select a file, and the WASI-compatible path to that file gets stored in the settings map.

Co-authored-by: Christopher Serr <christopher.serr@gmail.com>
AlexKnauth and CryZe authored Dec 21, 2023
1 parent 958e95c commit bb6b0e3
Showing 4 changed files with 342 additions and 129 deletions.
281 changes: 163 additions & 118 deletions Cargo.lock

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -6,9 +6,10 @@ edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
atomic = "0.6.0"
anyhow = "1.0.75"
arc-swap = "1.6.0"
atomic = "0.6.0"
bstr = "1.8.0"
byte-unit = "5.0.3"
clap = { version = "4.4.6", default-features = false, features = ["derive", "std"] }
eframe = "0.24.1"
@@ -18,6 +19,7 @@ egui_plot = "0.24.1"
hdrhistogram = { version = "7.5.2", default-features = false }
indexmap = "2.0.0"
livesplit-auto-splitting = { git = "https://github.com/LiveSplit/livesplit-core" }
mime_guess = "2.0.4"

[profile.max-opt]
inherits = "release"
115 changes: 115 additions & 0 deletions src/file_filter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
use std::{
path::{Path, PathBuf},
sync::Arc,
};

use bstr::ByteSlice;
use livesplit_auto_splitting::settings::FileFilter;

pub fn build(filters: Arc<Vec<FileFilter>>) -> egui_file::Filter<PathBuf> {
Box::new(move |p: &Path| {
let name = p.file_name().unwrap_or_default().as_encoded_bytes();
filters.iter().any(|filter| matches_filter(name, filter))
})
}

fn matches_filter(file_name: &[u8], filter: &FileFilter) -> bool {
match filter {
FileFilter::Name {
description: _,
pattern,
} => pattern
.split(' ')
.any(|pattern| matches_single_pattern(file_name, pattern.as_bytes())),
FileFilter::MimeType(mime_type) => matches_mime_type(file_name, mime_type),
}
}

fn matches_single_pattern(mut file_name: &[u8], mut pattern: &[u8]) -> bool {
let mut strip_any = false;
while !pattern.is_empty() {
strip_any = if let [b'*', rem @ ..] = pattern {
pattern = rem;
true
} else {
let (fixed, rem) = pattern.split_at(
pattern
.iter()
.position(|&b| b == b'*')
.unwrap_or(pattern.len()),
);
pattern = rem;
file_name = if strip_any {
let Some((_, rem)) = file_name.split_once_str(fixed) else {
return false;
};
rem
} else {
let Some(rem) = file_name.strip_prefix(fixed.as_bytes()) else {
return false;
};
rem
};
false
};
}
strip_any || file_name.is_empty()
}

fn matches_mime_type(file_name: &[u8], mime_type: &str) -> bool {
let Some((top, sub)) = mime_type.split_once('/') else {
return false;
};
let Some(extensions) = mime_guess::get_extensions(top, sub) else {
return false;
};
let Some((_, extension)) = file_name.rsplit_once_str(&[b'.']) else {
return false;
};
extensions.iter().any(|ext| extension == ext.as_bytes())
}

#[cfg(test)]
mod test {
use super::*;

#[test]
fn test_matches_single_pattern() {
assert!(matches_single_pattern(b"bar.exe", b"*.exe"));
assert!(!matches_single_pattern(b"bar.exeafter", b"*.exe"));
assert!(!matches_single_pattern(b"beforebar.exe", b"bar*"));
assert!(matches_single_pattern(b"beforebarafter", b"*bar*"));
assert!(matches_single_pattern(b"bar.txt", b"*.txt"));
assert!(matches_single_pattern(b"quick brown fox", b"*ick*row*ox"));
assert!(matches_single_pattern(b"quick brown fox", b"q*ick*row*ox"));
assert!(!matches_single_pattern(b"quick brown fox", b"*row*ox*ick*"));
}

#[test]
fn test_matches_mime_type() {
assert!(matches_mime_type(b"foo.txt", "text/plain"));
assert!(matches_mime_type(b"foo.jpg", "image/jpeg"));
assert!(matches_mime_type(b"foo.jpeg", "image/jpeg"));
assert!(matches_mime_type(b"foo.png", "image/png"));

assert!(!matches_mime_type(b"foo.txt", "image/*"));
assert!(matches_mime_type(b"foo.jpg", "image/*"));
assert!(matches_mime_type(b"foo.jpeg", "image/*"));
assert!(matches_mime_type(b"foo.png", "image/*"));

assert!(!matches_mime_type(b"txt", "text/plain"));
assert!(!matches_mime_type(b"jpg", "image/jpeg"));
assert!(!matches_mime_type(b"jpeg", "image/jpeg"));
assert!(!matches_mime_type(b"png", "image/png"));

assert!(!matches_mime_type(b"footxt", "text/plain"));
assert!(!matches_mime_type(b"foojpg", "image/jpeg"));
assert!(!matches_mime_type(b"foojpeg", "image/jpeg"));
assert!(!matches_mime_type(b"foopng", "image/png"));

assert!(!matches_mime_type(b"foo.txt", "image/jpeg"));
assert!(!matches_mime_type(b"foo.jpg", "image/png"));
assert!(!matches_mime_type(b"foo.jpeg", "image/png"));
assert!(!matches_mime_type(b"foo.png", "text/plain"));
}
}
71 changes: 61 additions & 10 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -31,11 +31,12 @@ use egui_plot::{Bar, BarChart, Legend, Plot, VLine};
use hdrhistogram::Histogram;
use indexmap::IndexMap;
use livesplit_auto_splitting::{
settings, time, AutoSplitter, CompiledAutoSplitter, Config, ExecutionGuard, Runtime, Timer,
TimerState,
settings, time, wasi_path, AutoSplitter, CompiledAutoSplitter, Config, ExecutionGuard, Runtime,
Timer, TimerState,
};

mod clear_vec;
mod file_filter;

enum Tab {
Main,
@@ -278,13 +279,19 @@ struct AppState {
module_modified_time: Option<SystemTime>,
script_modified_time: Option<SystemTime>,
optimize: bool,
open_file_dialog: Option<(FileDialog, bool)>,
open_file_dialog: Option<(FileDialog, FileDialogInfo)>,
module: Option<CompiledAutoSplitter>,
shared_state: Arc<SharedState>,
timer: DebuggerTimer,
runtime: livesplit_auto_splitting::Runtime,
}

enum FileDialogInfo {
Wasm,
Script,
SettingsWidget(Arc<str>),
}

struct TabViewer<'a> {
state: &'a mut AppState,
}
@@ -309,7 +316,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
if ui.button("Open").clicked() {
let mut dialog = FileDialog::open_file(self.state.path.clone());
dialog.open();
self.state.open_file_dialog = Some((dialog, true));
self.state.open_file_dialog = Some((dialog, FileDialogInfo::Wasm));
}
if let Some(auto_splitter) = &*self.state.shared_state.auto_splitter.load() {
if ui.button("Restart").clicked() {
@@ -330,7 +337,7 @@ impl egui_dock::TabViewer for TabViewer<'_> {
let mut dialog =
FileDialog::open_file(self.state.script_path.clone());
dialog.open();
self.state.open_file_dialog = Some((dialog, false));
self.state.open_file_dialog = Some((dialog, FileDialogInfo::Script));
}
if self.state.shared_state.auto_splitter.load().is_some() {
if let Some(script_path) = &self.state.script_path {
@@ -607,6 +614,32 @@ impl egui_dock::TabViewer for TabViewer<'_> {
}
}
}
settings::WidgetKind::FileSelect { ref filters } => {
ui.add_space(spacing);
let settings_map = runtime.settings_map();
let current_path: Option<PathBuf> =
match settings_map.get(&setting.key) {
Some(settings::Value::String(path)) => {
wasi_path::to_native(path)
}
_ => None,
};

let mut button = ui.button(&*setting.description);
if let Some(tooltip) = &setting.tooltip {
button = button.on_hover_text(&**tooltip);
}

if button.clicked() {
let mut dialog = FileDialog::open_file(current_path)
.show_files_filter(file_filter::build(filters.clone()));
dialog.open();
self.state.open_file_dialog = Some((
dialog,
FileDialogInfo::SettingsWidget(setting.key.clone()),
));
}
}
});
ui.end_row();
}
@@ -800,13 +833,31 @@ impl App for Debugger {
}
}

if let Some((dialog, is_wasm)) = &mut self.state.open_file_dialog {
if let Some((dialog, info)) = &mut self.state.open_file_dialog {
if dialog.show(ctx).selected() {
if let Some(file) = dialog.path().map(ToOwned::to_owned) {
if *is_wasm {
self.state.load(Load::File(file));
} else {
self.state.set_script_path(file);
match info {
FileDialogInfo::Wasm => self.state.load(Load::File(file)),
FileDialogInfo::Script => self.state.set_script_path(file),
FileDialogInfo::SettingsWidget(key) => {
if let Some(s) = wasi_path::from_native(&file) {
if let Some(runtime) =
&*self.state.shared_state.auto_splitter.load()
{
loop {
let old = runtime.settings_map();
let mut new = old.clone();
new.insert(
key.clone(),
settings::Value::String(s.as_ref().into()),
);
if runtime.set_settings_map_if_unchanged(&old, new) {
break;
}
}
}
}
}
}
}
}

0 comments on commit bb6b0e3

Please sign in to comment.