diff options
| author | s-ol <s+removethis@s-ol.nu> | 2026-01-23 14:24:55 +0000 |
|---|---|---|
| committer | s-ol <s+removethis@s-ol.nu> | 2026-01-23 15:59:20 +0000 |
| commit | 532be0e90aa151f2b087a08329bbae344e63dd10 (patch) | |
| tree | cc1bb0f2e9959abfd193956c922a3070d3d0266e | |
| parent | better node picker UX (diff) | |
| download | nodetoy-532be0e90aa151f2b087a08329bbae344e63dd10.tar.gz nodetoy-532be0e90aa151f2b087a08329bbae344e63dd10.zip | |
generic save / save as
| -rw-r--r-- | src/main.rs | 100 | ||||
| -rw-r--r-- | src/wasm.rs | 62 |
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() + } } |
