Use async file picker API

This commit is contained in:
Simon Gellis 2026-08-11 22:53:31 -04:00
parent ff8353cacc
commit 8ed1019b44
No known key found for this signature in database
GPG Key ID: DA576912FED9577B
4 changed files with 80 additions and 22 deletions

54
src/filepicker.rs Normal file
View File

@ -0,0 +1,54 @@
use std::{
path::PathBuf,
sync::{Arc, Weak},
};
use winit::window::Window;
pub struct FilePicker {
window: Option<Weak<Window>>,
}
impl FilePicker {
pub fn new() -> Self {
Self { window: None }
}
pub fn set_window(&mut self, window: &Arc<Window>) {
self.window = Some(Arc::downgrade(window));
}
pub fn new_dialog(&self) -> FileDialogBuilder {
let mut dialog = rfd::AsyncFileDialog::new();
if let Some(window) = self.window.as_ref().and_then(|w| w.upgrade()) {
dialog = dialog.set_parent(&window);
}
FileDialogBuilder { dialog }
}
}
pub struct FileDialogBuilder {
dialog: rfd::AsyncFileDialog,
}
impl FileDialogBuilder {
pub fn add_filter(self, name: impl Into<String>, extensions: &[impl ToString]) -> Self {
Self {
dialog: self.dialog.add_filter(name, extensions),
}
}
pub fn set_file_name(self, file_name: impl Into<String>) -> Self {
Self {
dialog: self.dialog.set_file_name(file_name),
}
}
pub fn pick_file(self) -> Option<PathBuf> {
pollster::block_on(self.dialog.pick_file()).map(Into::into)
}
pub fn save_file(self) -> Option<PathBuf> {
pollster::block_on(self.dialog.save_file()).map(Into::into)
}
}

View File

@ -23,6 +23,7 @@ mod audio;
mod config; mod config;
mod controller; mod controller;
mod emulator; mod emulator;
mod filepicker;
mod gdbserver; mod gdbserver;
mod graphics; mod graphics;
mod images; mod images;

View File

@ -8,6 +8,7 @@ use crate::{
app::UserEvent, app::UserEvent,
config::{COLOR_PRESETS, SimConfig}, config::{COLOR_PRESETS, SimConfig},
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId, SimState},
filepicker::FilePicker,
images::ImageTextureLoader, images::ImageTextureLoader,
input::{Command, MappingProvider, ShortcutProvider}, input::{Command, MappingProvider, ShortcutProvider},
memory::MemoryClient, memory::MemoryClient,
@ -46,7 +47,7 @@ pub struct GameWindow {
messages: mpsc::Receiver<Toast>, messages: mpsc::Receiver<Toast>,
message_sink: mpsc::Sender<Toast>, message_sink: mpsc::Sender<Toast>,
color_picker: Option<ColorPickerState>, color_picker: Option<ColorPickerState>,
window: Option<Arc<winit::window::Window>>, file_picker: FilePicker,
memory: Arc<MemoryClient>, memory: Arc<MemoryClient>,
images: Arc<ImageTextureLoader>, images: Arc<ImageTextureLoader>,
mappings: MappingProvider, mappings: MappingProvider,
@ -88,7 +89,7 @@ impl GameWindow {
messages, messages,
message_sink, message_sink,
color_picker: None, color_picker: None,
window: None, file_picker: FilePicker::new(),
memory: memory.clone(), memory: memory.clone(),
images: images.clone(), images: images.clone(),
mappings: mappings.clone(), mappings: mappings.clone(),
@ -249,7 +250,9 @@ impl GameWindow {
for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) { for command in ui.input_mut(|input| self.shortcuts.consume_all(input)) {
match command { match command {
Command::OpenRom => { Command::OpenRom => {
let rom = rfd::FileDialog::new() let rom = self
.file_picker
.new_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
@ -305,7 +308,9 @@ impl GameWindow {
.add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom)) .add(self.button_for(ui.ctx(), "Open ROM", Command::OpenRom))
.clicked() .clicked()
{ {
let rom = rfd::FileDialog::new() let rom = self
.file_picker
.new_dialog()
.add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"]) .add_filter("Virtual Boy ROMs", &["vb", "vbrom", "elf", "isx"])
.pick_file(); .pick_file();
if let Some(path) = rom { if let Some(path) = rom {
@ -459,13 +464,12 @@ impl GameWindow {
self.client self.client
.send_command(EmulatorCommand::Screenshot(self.sim_id, tx)); .send_command(EmulatorCommand::Screenshot(self.sim_id, tx));
let bytes = rx.await.context("Could not take screenshot")?; let bytes = rx.await.context("Could not take screenshot")?;
let mut file_dialog = rfd::FileDialog::new() let file = self
.file_picker
.new_dialog()
.add_filter("PNG images", &["png"]) .add_filter("PNG images", &["png"])
.set_file_name("screenshot.png"); .set_file_name("screenshot.png")
if let Some(window) = self.window.as_ref() { .save_file();
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
let Some(path) = file else { let Some(path) = file else {
return Ok(None); return Ok(None);
}; };
@ -759,7 +763,7 @@ impl AppWindow for GameWindow {
)); ));
self.screen = Some(screen); self.screen = Some(screen);
} }
self.window = Some(args.window.clone()); self.file_picker.set_window(args.window);
} }
} }

View File

@ -1,12 +1,12 @@
use std::{fs, sync::Arc, time::Duration}; use std::{fs, time::Duration};
use anyhow::Result; use anyhow::Result;
use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder}; use egui::{Button, CentralPanel, Checkbox, Label, ViewportBuilder};
use egui_notify::{Anchor, Toast, Toasts}; use egui_notify::{Anchor, Toast, Toasts};
use winit::window::Window;
use crate::{ use crate::{
emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId}, emulator::{EmulatorClient, EmulatorCommand, EmulatorState, SimId},
filepicker::FilePicker,
profiler::{Profiler, ProfilerStatus}, profiler::{Profiler, ProfilerStatus},
window::{AppWindow, InitArgs}, window::{AppWindow, InitArgs},
}; };
@ -16,7 +16,7 @@ pub struct ProfileWindow {
client: EmulatorClient, client: EmulatorClient,
profiler: Profiler, profiler: Profiler,
toasts: Toasts, toasts: Toasts,
window: Option<Arc<Window>>, file_picker: FilePicker,
} }
impl ProfileWindow { impl ProfileWindow {
@ -29,7 +29,7 @@ impl ProfileWindow {
.with_anchor(Anchor::BottomLeft) .with_anchor(Anchor::BottomLeft)
.with_margin((10.0, 10.0).into()) .with_margin((10.0, 10.0).into())
.reverse(true), .reverse(true),
window: None, file_picker: FilePicker::new(),
} }
} }
@ -62,13 +62,12 @@ impl ProfileWindow {
fn try_finish_recording(&mut self) -> Result<Option<String>> { fn try_finish_recording(&mut self) -> Result<Option<String>> {
let bytes_receiver = self.profiler.finish_recording(); let bytes_receiver = self.profiler.finish_recording();
let mut file_dialog = rfd::FileDialog::new() let file = self
.file_picker
.new_dialog()
.add_filter("Profiler files", &["json"]) .add_filter("Profiler files", &["json"])
.set_file_name("profile.json"); .set_file_name("profile.json")
if let Some(window) = self.window.as_ref() { .save_file();
file_dialog = file_dialog.set_parent(window);
}
let file = file_dialog.save_file();
if let Some(path) = file { if let Some(path) = file {
let bytes = pollster::block_on(bytes_receiver)?; let bytes = pollster::block_on(bytes_receiver)?;
let _ = fs::remove_file(&path); let _ = fs::remove_file(&path);
@ -164,6 +163,6 @@ impl AppWindow for ProfileWindow {
} }
fn on_init(&mut self, args: InitArgs) { fn on_init(&mut self, args: InitArgs) {
self.window = Some(args.window.clone()); self.file_picker.set_window(args.window);
} }
} }