aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authors-ol <s+removethis@s-ol.nu>2026-01-23 14:24:55 +0000
committers-ol <s+removethis@s-ol.nu>2026-01-23 15:59:20 +0000
commit532be0e90aa151f2b087a08329bbae344e63dd10 (patch)
treecc1bb0f2e9959abfd193956c922a3070d3d0266e
parentbetter node picker UX (diff)
downloadnodetoy-532be0e90aa151f2b087a08329bbae344e63dd10.tar.gz
nodetoy-532be0e90aa151f2b087a08329bbae344e63dd10.zip
generic save / save as
-rw-r--r--src/main.rs100
-rw-r--r--src/wasm.rs62
2 files changed, 133 insertions, 29 deletions
diff --git a/src/main.rs b/src/main.rs
index 0e95c19..ba1c356 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,7 +7,7 @@ use egui_snarl::{
};
use lazy_static::lazy_static;
use log::*;
-use std::collections::HashMap;
+use std::{collections::HashMap, path::PathBuf};
mod library;
mod node;
@@ -43,6 +43,8 @@ impl Into<PinInfo> for Type {
pub struct Viewer {
dirty: bool,
+ filename: Option<PathBuf>,
+ save_as_dialog: Option<String>,
}
impl SnarlViewer<AnyNode> for Viewer {
@@ -220,7 +222,11 @@ impl App {
App {
snarl,
- viewer: Viewer { dirty: true },
+ viewer: Viewer {
+ dirty: true,
+ filename: None,
+ save_as_dialog: None,
+ },
preview: preview::Preview::new(cx).expect("Failed to init preview"),
}
}
@@ -232,6 +238,8 @@ impl eframe::App for App {
egui::MenuBar::new().ui(ui, |ui| {
egui::widgets::global_theme_preference_switch(ui);
+ let mut do_save = false;
+
ui.menu_button("File", |ui| {
if ui.button("Open").clicked() {
#[cfg(not(target_arch = "wasm32"))]
@@ -266,27 +274,40 @@ impl eframe::App for App {
ui.separator();
- #[cfg(not(target_arch = "wasm32"))]
- if ui.button("Save").clicked() {
- if let Some(path) = rfd::FileDialog::new()
- .add_filter("Nodetoy shader", &["nt.json"])
- .save_file()
+ if ui
+ .add_enabled(self.viewer.filename.is_some(), egui::Button::new("Save"))
+ .clicked()
+ {
+ do_save = true;
+ }
+
+ if ui.button("Save as").clicked() {
+ #[cfg(not(target_arch = "wasm32"))]
{
- let file = std::fs::File::create(path).unwrap();
- serde_json::to_writer(file, &self.snarl).unwrap();
+ if let Some(path) = rfd::FileDialog::new()
+ .add_filter("Nodetoy shader", &["nt.json"])
+ .save_file()
+ {
+ self.viewer.filename = Some(path);
+ do_save = true;
+ }
}
- }
- #[cfg(target_arch = "wasm32")]
- if ui.button("Download").clicked() {
- wasm::download_file(&self.snarl, "shader.nt.json")
- .expect("successful download");
+ #[cfg(target_arch = "wasm32")]
+ {
+ self.viewer.save_as_dialog = self
+ .viewer
+ .filename
+ .as_ref()
+ .map(|p| p.as_os_str().to_string_lossy().to_string())
+ .or(Some("Untitled".to_string()));
+ }
}
-
ui.separator();
if ui.button("Clear All").clicked() {
self.snarl = Snarl::default();
+ self.viewer.filename = None;
}
#[cfg(not(target_arch = "wasm32"))]
@@ -294,6 +315,55 @@ impl eframe::App for App {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
});
+
+ #[cfg(target_arch = "wasm32")]
+ if let Some(buffer) = &mut self.viewer.save_as_dialog {
+ let modal = egui::Modal::new(egui::Id::new("save")).show(ui.ctx(), |ui| {
+ ui.set_width(250.0);
+
+ ui.heading("Save File");
+
+ ui.label("Name:");
+ ui.text_edit_singleline(buffer);
+
+ egui::Sides::new().show(
+ ui,
+ |_ui| {},
+ |ui| {
+ if ui.button("Save").clicked() {
+ if buffer.is_empty() {
+ *buffer = "Unnamed.nt.json".to_string();
+ }
+ if !buffer.ends_with(".json") {
+ *buffer += ".nt.json";
+ }
+
+ self.viewer.filename = Some(PathBuf::from(&buffer));
+ do_save = true;
+ ui.close();
+ }
+
+ if ui.button("Cancel").clicked() {
+ ui.close();
+ }
+ },
+ );
+ });
+
+ if modal.should_close() {
+ self.viewer.save_as_dialog = None;
+ }
+ }
+
+ if do_save {
+ let path = self.viewer.filename.as_ref().unwrap();
+
+ #[cfg(not(target_arch = "wasm32"))]
+ let file = std::fs::File::create(path).unwrap();
+ #[cfg(target_arch = "wasm32")]
+ let file = wasm::DownloadFile::new(path.clone(), "application/json");
+ serde_json::to_writer(file, &self.snarl).unwrap();
+ }
});
});
diff --git a/src/wasm.rs b/src/wasm.rs
index 6ce45e0..05653ff 100644
--- a/src/wasm.rs
+++ b/src/wasm.rs
@@ -1,3 +1,5 @@
+use std::path::PathBuf;
+use std::{io, io::Write};
use web_sys::js_sys::Array;
use web_sys::wasm_bindgen::{JsCast, JsValue};
use web_sys::{Blob, BlobPropertyBag, HtmlAnchorElement, HtmlCanvasElement, Url};
@@ -8,22 +10,54 @@ pub fn get_canvas_element() -> Option<HtmlCanvasElement> {
canvas.dyn_into::<HtmlCanvasElement>().ok()
}
-pub fn download_file<T: serde::Serialize>(value: &T, filename: &str) -> Result<(), JsValue> {
- let json = serde_json::to_string(value).unwrap();
- let parts = Array::of1(&JsValue::from_str(&json));
- let opts = BlobPropertyBag::new();
- opts.set_type("application/json");
+pub struct DownloadFile {
+ data: Vec<u8>,
+ filename: PathBuf,
+ options: BlobPropertyBag,
+}
+
+impl DownloadFile {
+ pub fn new(filename: PathBuf, mime_type: &str) -> Self {
+ let options = BlobPropertyBag::new();
+ options.set_type(mime_type);
+
+ Self {
+ data: Vec::new(),
+ filename,
+ options,
+ }
+ }
+
+ pub fn save(&self) -> Result<(), JsValue> {
+ let string = unsafe { str::from_utf8_unchecked(&self.data) };
+ let parts = Array::of1(&JsValue::from_str(&string));
+ let blob = Blob::new_with_str_sequence_and_options(&parts, &self.options)?;
+ let url = Url::create_object_url_with_blob(&blob)?;
- let blob = Blob::new_with_str_sequence_and_options(&parts, &opts)?;
- let url = Url::create_object_url_with_blob(&blob)?;
+ let document = web_sys::window().unwrap().document().unwrap();
+ let a: HtmlAnchorElement = document.create_element("a")?.dyn_into()?;
- let document = web_sys::window().unwrap().document().unwrap();
- let a: HtmlAnchorElement = document.create_element("a")?.dyn_into()?;
+ a.set_href(&url);
+ a.set_download(&self.filename.as_os_str().to_string_lossy());
+ a.click();
- a.set_href(&url);
- a.set_download(filename);
- a.click();
+ Url::revoke_object_url(&url)?;
+ Ok(())
+ }
+}
+
+impl Write for DownloadFile {
+ fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
+ self.data.write(buf)
+ }
+
+ fn flush(&mut self) -> io::Result<()> {
+ self.data.flush()
+ }
+}
- Url::revoke_object_url(&url)?;
- Ok(())
+impl Drop for DownloadFile {
+ fn drop(&mut self) {
+ self.save().unwrap()
+ }
}