Skip to main content

blockworx/
import.rs

1//! The file dialogs behind the "Import…" command and behind an artwork pick:
2//! the toolkit half of both paths, reached only through
3//! [`Dialogs`](crate::dialogs::Dialogs), so a test answers a picker instead of
4//! opening one. What an imported file *means* is
5//! [`blockworx_editor::import`],
6//! and where a picked image goes is the session's; this is only how the bytes
7//! are picked.
8//!
9//! Each dialog runs off the UI thread (a background thread natively, an async
10//! task on the web) so the canvas keeps repainting while it is up; what it
11//! picked arrives through a channel and is dispatched on the UI thread.
12
13use std::sync::mpsc::Receiver;
14
15use blockworx_doc::block_model::Asset;
16
17// Only the native build opens a file by name: the web one has no container
18// path and no Open dialog to remember.
19#[cfg(not(target_arch = "wasm32"))]
20pub use blockworx_editor::import::file_stem;
21
22/// Open the import file dialog off the UI thread (so the canvas keeps
23/// repainting) and return the channel its result arrives on: the picked file's
24/// name and bytes, or `None` if cancelled or unreadable.
25#[cfg(not(target_arch = "wasm32"))]
26pub fn spawn_import_dialog(ctx: &egui::Context) -> Receiver<Option<(String, Vec<u8>)>> {
27    let (tx, rx) = std::sync::mpsc::channel();
28    let thread_ctx = ctx.clone();
29    std::thread::spawn(move || {
30        let picked = rfd::FileDialog::new()
31            .add_filter("Image", &["png", "svg"])
32            .pick_file()
33            .and_then(|path| {
34                let name = path.file_name()?.to_string_lossy().into_owned();
35                Some((name, std::fs::read(&path).ok()?))
36            });
37        let _ = tx.send(picked);
38        thread_ctx.request_repaint();
39    });
40    ctx.request_repaint();
41    rx
42}
43/// Web variant: no filesystem or thread, so the async `rfd` dialog runs as a
44/// `spawn_local` task and reads the picked file's bytes from its handle.
45#[cfg(target_arch = "wasm32")]
46pub fn spawn_import_dialog(ctx: &egui::Context) -> Receiver<Option<(String, Vec<u8>)>> {
47    let (tx, rx) = std::sync::mpsc::channel();
48    let task_ctx = ctx.clone();
49    wasm_bindgen_futures::spawn_local(async move {
50        let picked = if let Some(file) = rfd::AsyncFileDialog::new()
51            .add_filter("Image", &["png", "svg"])
52            .pick_file()
53            .await
54        {
55            Some((file.file_name(), file.read().await))
56        } else {
57            None
58        };
59        let _ = tx.send(picked);
60        task_ctx.request_repaint();
61    });
62    ctx.request_repaint();
63    rx
64}
65
66/// Open the image file dialog off the UI thread and return the channel its
67/// result arrives on: the chosen file read into an [`Asset`], or `None` if
68/// cancelled or unreadable. Shared by the two commands that want artwork — an
69/// image of its own, and a block's icon.
70#[cfg(not(target_arch = "wasm32"))]
71pub fn spawn_image_dialog(ctx: &egui::Context) -> Receiver<Option<Asset>> {
72    let (tx, rx) = std::sync::mpsc::channel();
73    let thread_ctx = ctx.clone();
74    std::thread::spawn(move || {
75        let image = rfd::FileDialog::new()
76            .add_filter("Image", &["svg", "png"])
77            .pick_file()
78            .and_then(|path| read_image(&path));
79        let _ = tx.send(image);
80        thread_ctx.request_repaint();
81    });
82    ctx.request_repaint();
83    rx
84}
85
86/// Web variant of [`spawn_image_dialog`]: there is no filesystem or thread, so
87/// the async `rfd` dialog runs as a `spawn_local` task and reads the picked
88/// file's bytes directly from its handle (branching SVG/PNG on the file name).
89#[cfg(target_arch = "wasm32")]
90pub fn spawn_image_dialog(ctx: &egui::Context) -> Receiver<Option<Asset>> {
91    let (tx, rx) = std::sync::mpsc::channel();
92    let task_ctx = ctx.clone();
93    wasm_bindgen_futures::spawn_local(async move {
94        let image = if let Some(file) = rfd::AsyncFileDialog::new()
95            .add_filter("Image", &["svg", "png"])
96            .pick_file()
97            .await
98        {
99            let is_png = file.file_name().to_ascii_lowercase().ends_with(".png");
100            let bytes = file.read().await;
101            if is_png {
102                Some(Asset::Png(bytes.into()))
103            } else {
104                String::from_utf8(bytes)
105                    .ok()
106                    .map(|text| Asset::Svg(text.into_bytes().into()))
107            }
108        } else {
109            None
110        };
111        let _ = tx.send(image);
112        task_ctx.request_repaint();
113    });
114    ctx.request_repaint();
115    rx
116}
117
118/// Read `path` into an [`Asset`], branching on its extension: `.png` loads as
119/// raw bytes, anything else is read as SVG text. `None` if the file can't be
120/// read.
121#[cfg(not(target_arch = "wasm32"))]
122fn read_image(path: &std::path::Path) -> Option<Asset> {
123    let is_png = path
124        .extension()
125        .is_some_and(|ext| ext.eq_ignore_ascii_case("png"));
126    if is_png {
127        std::fs::read(path)
128            .ok()
129            .map(|bytes| Asset::Png(bytes.into()))
130    } else {
131        std::fs::read_to_string(path)
132            .ok()
133            .map(|text| Asset::Svg(text.into_bytes().into()))
134    }
135}