mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-30 13:56:46 -06:00
* egui037: migrate CADCraft UI and preserve font notices * M0: clarify screenshot cancellation after issue * Document migration UI contracts and regression assertions
135 lines
5.1 KiB
Rust
135 lines
5.1 KiB
Rust
//! Browser uploads: File > Open… on the web, through web-sys only. There is no file system to pick
|
|
//! a path from, so the picker is a hidden `<input type="file">`; the chosen file (or one dropped on
|
|
//! the page) is read in the background and handed to the app on its next frame, which opens it
|
|
//! with OPEN `{data, name}` ([`CadApp::open_bytes`]).
|
|
|
|
use std::cell::Cell;
|
|
use std::rc::Rc;
|
|
|
|
use cadcraft_ui_egui::CadApp;
|
|
use wasm_bindgen::JsCast as _;
|
|
use web_sys::js_sys;
|
|
|
|
/// The hidden file input (one at a time; a cancelled pick leaves it until the next one).
|
|
const INPUT_ID: &str = "cadcraft_open_input";
|
|
/// What the picker offers: the formats OPEN reads.
|
|
const ACCEPT: &str = ".dxf,.dwg";
|
|
|
|
/// A file read in the browser (name, contents), or why it couldn't be read.
|
|
type Upload = Result<(String, Vec<u8>), String>;
|
|
|
|
/// Files read in the background, waiting for the app's next frame.
|
|
#[derive(Clone)]
|
|
pub struct Inbox {
|
|
files: Rc<Cell<Vec<Upload>>>,
|
|
ctx: egui::Context,
|
|
}
|
|
|
|
impl Inbox {
|
|
pub fn new(ctx: &egui::Context) -> Self {
|
|
Inbox { files: Rc::default(), ctx: ctx.clone() }
|
|
}
|
|
|
|
/// Show the browser's file picker and return at once. A chosen file arrives through
|
|
/// [`Self::drain`]; cancelling the picker does nothing.
|
|
pub fn pick(&self) {
|
|
if let Err(e) = self.show_picker() {
|
|
self.push(Err(format!("Open: the browser's file picker is not available ({e})")));
|
|
}
|
|
}
|
|
|
|
fn show_picker(&self) -> Result<(), String> {
|
|
let document = web_sys::window().and_then(|w| w.document()).ok_or("no browser document")?;
|
|
let body = document.body().ok_or("the page has no body")?;
|
|
if let Some(old) = document.get_element_by_id(INPUT_ID) {
|
|
old.remove();
|
|
}
|
|
let input = document
|
|
.create_element("input")
|
|
.map_err(js_error)?
|
|
.dyn_into::<web_sys::HtmlInputElement>()
|
|
.map_err(|_| "could not create a file input".to_string())?;
|
|
input.set_type("file");
|
|
input.set_accept(ACCEPT);
|
|
input.set_id(INPUT_ID);
|
|
input.set_attribute("style", "display:none").map_err(js_error)?;
|
|
let (inbox, chosen) = (self.clone(), input.clone());
|
|
// Called once, when a file was chosen (browsers don't fire `change` on cancel).
|
|
let on_change = wasm_bindgen::closure::Closure::once_into_js(move || {
|
|
if let Some(file) = chosen.files().and_then(|f| f.get(0)) {
|
|
inbox.read(file);
|
|
}
|
|
chosen.remove();
|
|
});
|
|
input.set_onchange(Some(on_change.unchecked_ref()));
|
|
body.append_child(&input).map_err(js_error)?;
|
|
input.click();
|
|
Ok(())
|
|
}
|
|
|
|
/// Read the files dropped on the page this frame (eframe hands them over unread on the web).
|
|
/// The list is taken, not cloned: a later pass or hidden tick must not read the same drop.
|
|
pub fn take_dropped(&self, ctx: &egui::Context) {
|
|
for f in ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files)) {
|
|
if let Some(file) = f.web_file() {
|
|
self.read(file.clone());
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Read `file` in the background; it arrives through [`Self::drain`]. A file too large to
|
|
/// open is refused before it is read.
|
|
fn read(&self, file: web_sys::File) {
|
|
let name = file.name();
|
|
// `File.size` is a whole number of bytes; anything else counts as too large.
|
|
let size = file.size();
|
|
let len = if size.is_finite() && size >= 0.0 { size as u64 } else { u64::MAX };
|
|
if let Some(e) = CadApp::open_size_error(&name, len) {
|
|
return self.push(Err(e));
|
|
}
|
|
let inbox = self.clone();
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
let r = match wasm_bindgen_futures::JsFuture::from(file.array_buffer()).await {
|
|
Ok(buf) => Ok((name, js_sys::Uint8Array::new(&buf).to_vec())),
|
|
Err(e) => Err(format!("Open: could not read {name}: {}", js_error(e))),
|
|
};
|
|
inbox.push(r);
|
|
});
|
|
}
|
|
|
|
/// A file read elsewhere (sent by the page embedding CADCraft): opened on the next frame like
|
|
/// a picked file, or refused when too large to open.
|
|
pub fn receive(&self, name: String, bytes: Vec<u8>) {
|
|
match CadApp::open_size_error(&name, bytes.len() as u64) {
|
|
Some(e) => self.push(Err(e)),
|
|
None => self.push(Ok((name, bytes))),
|
|
}
|
|
}
|
|
|
|
/// A file from elsewhere refused before it was read (`error` says why).
|
|
pub fn refuse(&self, error: String) {
|
|
self.push(Err(error));
|
|
}
|
|
|
|
fn push(&self, upload: Upload) {
|
|
let mut files = self.files.take();
|
|
files.push(upload);
|
|
self.files.set(files);
|
|
self.ctx.request_repaint();
|
|
}
|
|
|
|
/// Open what arrived since the last frame (or report why it couldn't be read).
|
|
pub fn drain(&self, app: &mut CadApp) {
|
|
for upload in self.files.take() {
|
|
match upload {
|
|
Ok((name, bytes)) => app.open_bytes(&name, &bytes),
|
|
Err(e) => app.open_failed(e),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn js_error(e: wasm_bindgen::JsValue) -> String {
|
|
e.as_string().unwrap_or_else(|| format!("{e:?}"))
|
|
}
|