#![deny(warnings)] #![warn(clippy::all)] pub mod webgl; // ---------------------------------------------------------------------------- use std::sync::Arc; use wasm_bindgen::JsValue; pub struct State { ctx: Arc, webgl_painter: webgl::Painter, frame_times: egui::MovementTracker, frame_start: Option, } impl State { pub fn new(canvas_id: &str) -> Result { let ctx = egui::Context::new(); load_memory(&ctx); Ok(State { ctx, webgl_painter: webgl::Painter::new(canvas_id)?, frame_times: egui::MovementTracker::new(1000, 1.0), frame_start: None, }) } pub fn begin_frame(&mut self, raw_input: egui::RawInput) -> egui::Ui { self.frame_start = Some(now_sec()); self.ctx.begin_frame(raw_input) } pub fn end_frame(&mut self) -> Result { let frame_start = self .frame_start .take() .expect("unmatched calls to begin_frame/end_frame"); let bg_color = egui::color::srgba(0, 0, 0, 0); // Use background css color. let (output, batches) = self.ctx.end_frame(); let now = now_sec(); self.frame_times.add(now, (now - frame_start) as f32); self.webgl_painter.paint_batches( bg_color, batches, self.ctx.texture(), self.ctx.pixels_per_point(), )?; save_memory(&self.ctx); // TODO: don't save every frame Ok(output) } pub fn painter_debug_info(&self) -> String { self.webgl_painter.debug_info() } /// excludes painting pub fn cpu_usage(&self) -> f32 { self.frame_times.average().unwrap_or_default() } pub fn fps(&self) -> f32 { 1.0 / self.frame_times.mean_time_interval().unwrap_or_default() } } // ---------------------------------------------------------------------------- // Helpers to hide some of the verbosity of web_sys pub fn console_log(s: String) { web_sys::console::log_1(&s.into()); } pub fn screen_size() -> Option { let window = web_sys::window()?; Some(egui::Vec2::new( window.inner_width().ok()?.as_f64()? as f32, window.inner_height().ok()?.as_f64()? as f32, )) } pub fn now_sec() -> f64 { web_sys::window() .expect("should have a Window") .performance() .expect("should have a Performance") .now() / 1000.0 } pub fn seconds_since_midnight() -> f64 { let d = js_sys::Date::new_0(); let seconds = (d.get_hours() * 60 + d.get_minutes()) * 60 + d.get_seconds(); return seconds as f64 + 1e-3 * (d.get_milliseconds() as f64); } pub fn pixels_per_point() -> f32 { let pixels_per_point = web_sys::window().unwrap().device_pixel_ratio() as f32; if pixels_per_point > 0.0 && pixels_per_point.is_finite() { pixels_per_point } else { 1.0 } } pub fn resize_to_screen_size(canvas_id: &str) -> Option<()> { use wasm_bindgen::JsCast; let document = web_sys::window()?.document()?; let canvas = document.get_element_by_id(canvas_id)?; let canvas: web_sys::HtmlCanvasElement = canvas.dyn_into::().ok()?; let screen_size = screen_size()?; let pixels_per_point = pixels_per_point(); canvas .style() .set_property("width", &format!("{}px", screen_size.x)) .ok()?; canvas .style() .set_property("height", &format!("{}px", screen_size.y)) .ok()?; canvas.set_width((screen_size.x * pixels_per_point).round() as u32); canvas.set_height((screen_size.y * pixels_per_point).round() as u32); Some(()) } pub fn local_storage() -> Option { web_sys::window()?.local_storage().ok()? } pub fn local_storage_get(key: &str) -> Option { local_storage().map(|storage| storage.get_item(key).ok())?? } pub fn local_storage_set(key: &str, value: &str) { local_storage().map(|storage| storage.set_item(key, value)); } pub fn local_storage_remove(key: &str) { local_storage().map(|storage| storage.remove_item(key)); } pub fn load_memory(ctx: &egui::Context) { if let Some(memory_string) = local_storage_get("egui_memory_json") { match serde_json::from_str(&memory_string) { Ok(memory) => { *ctx.memory() = memory; } Err(err) => { console_log(format!("ERROR: Failed to parse memory json: {}", err)); } } } } pub fn save_memory(ctx: &egui::Context) { match serde_json::to_string(&*ctx.memory()) { Ok(json) => { local_storage_set("egui_memory_json", &json); } Err(err) => { console_log(format!( "ERROR: Failed to seriealize memory as json: {}", err )); } } } pub fn handle_output(output: &egui::Output) { set_cursor_icon(output.cursor_icon); if let Some(url) = &output.open_url { open_url(url); } } pub fn set_cursor_icon(cursor: egui::CursorIcon) -> Option<()> { let document = web_sys::window()?.document()?; document .body()? .style() .set_property("cursor", cursor_web_name(cursor)) .ok() } fn cursor_web_name(cursor: egui::CursorIcon) -> &'static str { use egui::CursorIcon::*; match cursor { Default => "default", PointingHand => "pointer", ResizeHorizontal => "ew-resize", ResizeNeSw => "nesw-resize", ResizeNwSe => "nwse-resize", ResizeVertical => "ns-resize", Text => "text", // "no-drop" // "not-allowed" // default, help, pointer, progress, wait, cell, crosshair, text, alias, copy, move, grab, grabbing, } } pub fn open_url(url: &str) -> Option<()> { web_sys::window()? .open_with_url_and_target(url, "_self") .ok()?; Some(()) } /// e.g. "#fragment" part of "www.example.com/index.html#fragment" pub fn location_hash() -> Option { web_sys::window()?.location().hash().ok() }