feat(dialogs): reopen file pickers in the last-used directory

Every rfd dialog result funnels through sys::handle_path, which now
records the picked file's folder (persisted to the config dir), and all
pickers are built via sys::file_dialog(), seeded with that folder.
Save-As keeps the current tab's own directory as the first choice and
falls back to the remembered one.

Co-Authored-By: Claude Fable 5 <[email protected]>
This commit is contained in:
Hakan SevenandClaude Fable 5 committed 2026-07-21 09:27:44 +03:00
1 parent e8411c1b6d
commit e78ad7884d
7 files changed
+78 -15

No files matched your search

+1 -1
View File
@@ -153,7 +153,7 @@ pub(super) fn on_ribbon_tool_click(&mut self, tool_id: String, event: ModuleEven
async move {
let exts: Vec<&str> =
extensions.iter().map(|s| s.as_str()).collect();
let path = rfd::AsyncFileDialog::new()
let path = crate::sys::file_dialog()
.set_title(title)
.add_filter(filter_name, &exts)
.add_filter("All Files", &["*"])
+2 -2
View File
@@ -845,7 +845,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
let close = self.close_save_dialog_window();
let pick = Task::perform(
async move {
let mut dlg = rfd::AsyncFileDialog::new()
let mut dlg = crate::sys::file_dialog()
.set_title("Save Drawing As")
.set_file_name(default_name)
.add_filter(filter_label, &[filter_ext]);
@@ -2204,7 +2204,7 @@ pub(super) fn on_open_file(&mut self) -> Task<Message> {
.unwrap_or("export.ctb".into());
Task::perform(
async move {
rfd::AsyncFileDialog::new()
crate::sys::file_dialog()
.set_title("Save Plot Style Table")
.set_file_name(&default_name)
.add_filter("Plot Style Files", &["ctb", "stb", "CTB", "STB"])
+6 -6
View File
@@ -408,7 +408,7 @@ impl OpenCADStudio {
Message::XAttachPick => Task::perform(
async {
let handle = rfd::AsyncFileDialog::new()
let handle = crate::sys::file_dialog()
.set_title("Select External Reference File")
.add_filter("CAD Files", &["dwg", "dxf", "bak", "DWG", "DXF", "BAK"])
.add_filter("DWG Files", &["dwg", "DWG"])
@@ -446,7 +446,7 @@ impl OpenCADStudio {
let name = block_name.clone();
Task::perform(
async move {
let path = rfd::AsyncFileDialog::new()
let path = crate::sys::file_dialog()
.set_title("Save Block As")
.set_file_name("block.dwg")
.add_filter("DWG Files", &["dwg"])
@@ -469,7 +469,7 @@ impl OpenCADStudio {
let csv_clone = csv.clone();
Task::perform(
async move {
let path = rfd::AsyncFileDialog::new()
let path = crate::sys::file_dialog()
.set_title("Save Data Extraction")
.set_file_name("extraction.csv")
.add_filter("CSV", &["csv"])
@@ -512,7 +512,7 @@ impl OpenCADStudio {
}
Task::perform(
async {
rfd::AsyncFileDialog::new()
crate::sys::file_dialog()
.set_title("Export STL")
.set_file_name("export.stl")
.add_filter("STL Files", &["stl"])
@@ -539,7 +539,7 @@ impl OpenCADStudio {
}
Task::perform(
async {
rfd::AsyncFileDialog::new()
crate::sys::file_dialog()
.set_title("Export STEP AP203")
.set_file_name("export.step")
.add_filter("STEP Files", &["step", "stp"])
@@ -559,7 +559,7 @@ impl OpenCADStudio {
// ── OBJ import ────────────────────────────────────────────────
Message::ObjImport => Task::perform(
async {
rfd::AsyncFileDialog::new()
crate::sys::file_dialog()
.set_title("Import OBJ Mesh")
.add_filter("Wavefront OBJ", &["obj", "OBJ"])
.add_filter("All Files", &["*"])
+42
View File
@@ -28,3 +28,45 @@ pub fn config_dir() -> Option<PathBuf> {
p.push("OpenCADStudio");
Some(p)
}
// ── Last file-dialog directory ───────────────────────────────────────────────
use std::path::Path;
use std::sync::{Mutex, OnceLock};
fn last_dir_store() -> &'static Mutex<Option<PathBuf>> {
static STORE: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
STORE.get_or_init(|| {
// Seed from the persisted value; discard it if the folder is gone.
let loaded = config_dir()
.map(|d| d.join("last_dir.txt"))
.and_then(|f| std::fs::read_to_string(f).ok())
.map(|s| PathBuf::from(s.trim()))
.filter(|p| p.is_dir());
Mutex::new(loaded)
})
}
/// The directory the last file dialog picked or saved into, if it still
/// exists — used to seed the next dialog so pickers reopen where the user
/// left off. Persisted across runs.
pub fn last_dialog_dir() -> Option<PathBuf> {
last_dir_store().lock().ok()?.clone().filter(|p| p.is_dir())
}
/// Record the directory of a path a file dialog just returned.
pub fn remember_dialog_dir(file_path: &Path) {
let Some(dir) = file_path.parent().filter(|d| d.is_dir()) else {
return;
};
if let Ok(mut store) = last_dir_store().lock() {
if store.as_deref() == Some(dir) {
return; // unchanged — skip the disk write
}
*store = Some(dir.to_path_buf());
}
if let Some(cfg) = config_dir() {
let _ = std::fs::create_dir_all(&cfg);
let _ = std::fs::write(cfg.join("last_dir.txt"), dir.display().to_string());
}
}
+4 -4
View File
@@ -40,7 +40,7 @@ const PHASE_FINALIZING: u8 = 3;
/// the parser thread starts.
#[cfg(not(target_arch = "wasm32"))]
pub async fn pick_open_path() -> Option<(PathBuf, u64)> {
let handle = rfd::AsyncFileDialog::new()
let handle = crate::sys::file_dialog()
.set_title("Open CAD file")
.add_filter("CAD Files", &["dwg", "dxf", "bak", "sv$", "DWG", "DXF", "BAK"])
.add_filter("DWG Files", &["dwg", "DWG"])
@@ -103,7 +103,7 @@ pub async fn open_path_with_phase(
#[cfg(target_arch = "wasm32")]
pub async fn pick_and_load_web(
) -> Result<(String, PathBuf, CadDocument, DerivedCaches), String> {
let handle = rfd::AsyncFileDialog::new()
let handle = crate::sys::file_dialog()
.set_title("Open CAD file")
.add_filter("CAD Files", &["dwg", "dxf", "DWG", "DXF"])
.add_filter("All Files", &["*"])
@@ -364,7 +364,7 @@ pub fn write_backup(path: &std::path::Path) {
/// Show a file-open dialog and load the selected CTB or STB file.
pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
let handle = rfd::AsyncFileDialog::new()
let handle = crate::sys::file_dialog()
.set_title("Load Plot Style Table")
.add_filter("Plot Style Tables", &["ctb", "stb", "CTB", "STB"])
.add_filter("CTB Files", &["ctb", "CTB"])
@@ -380,7 +380,7 @@ pub async fn pick_plot_style() -> Option<plot_style::PlotStyleTable> {
/// Show a file-open dialog for raster images and decode the selected file.
/// Returns `(path, pixel_width, pixel_height)` or an error string.
pub async fn pick_image_file() -> Result<(PathBuf, u32, u32), String> {
let handle = rfd::AsyncFileDialog::new()
let handle = crate::sys::file_dialog()
.set_title("Select Image File")
.add_filter("Images", &["png", "jpg", "jpeg", "bmp", "tiff", "tif"])
.add_filter("PNG", &["png"])
+1 -1
View File
@@ -100,7 +100,7 @@ pub fn export_pdf(
/// Show a PDF save-file dialog and return the chosen path (or None if cancelled).
#[cfg(not(target_arch = "wasm32"))]
pub async fn pick_pdf_path_owned(stem: String) -> Option<std::path::PathBuf> {
rfd::AsyncFileDialog::new()
crate::sys::file_dialog()
.set_title("Export as PDF")
.set_file_name(&format!("{stem}.pdf"))
.add_filter("PDF Files", &["pdf"])
+22 -1
View File
@@ -39,7 +39,11 @@ pub async fn read_clipboard_text() -> Option<String> {
/// (a follow-up).
#[cfg(not(target_arch = "wasm32"))]
pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf {
h.path().to_path_buf()
let p = h.path().to_path_buf();
// Every dialog result funnels through here — remember its folder so the
// next dialog opens where the user left off.
crate::config::remember_dialog_dir(&p);
p
}
#[cfg(target_arch = "wasm32")]
@@ -47,6 +51,23 @@ pub fn handle_path(h: &rfd::FileHandle) -> std::path::PathBuf {
std::path::PathBuf::from(h.file_name())
}
/// New async file dialog seeded with the last directory a dialog was used in.
/// All pickers should start from this instead of `rfd::AsyncFileDialog::new()`.
#[cfg(not(target_arch = "wasm32"))]
pub fn file_dialog() -> rfd::AsyncFileDialog {
let dlg = rfd::AsyncFileDialog::new();
match crate::config::last_dialog_dir() {
Some(dir) => dlg.set_directory(dir),
None => dlg,
}
}
/// Web: no filesystem paths, nothing to remember.
#[cfg(target_arch = "wasm32")]
pub fn file_dialog() -> rfd::AsyncFileDialog {
rfd::AsyncFileDialog::new()
}
/// Trigger a browser download of `bytes` as `name`. Builds a Blob, points a
/// hidden `<a download>` at it and clicks it programmatically — because this
/// runs inside the Save button's click (a user gesture), the file downloads