diff --git a/Cargo.lock b/Cargo.lock index 0c0eb678..e990c565 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -192,6 +192,7 @@ dependencies = [ "clipboard 0.5.0 (registry+https://github.com/rust-lang/crates.io-index)", "emigui 0.1.0", "glium 0.24.0 (registry+https://github.com/rust-lang/crates.io-index)", + "serde_json 1.0.51 (registry+https://github.com/rust-lang/crates.io-index)", "webbrowser 0.5.2 (registry+https://github.com/rust-lang/crates.io-index)", ] diff --git a/README.md b/README.md index 23dabb03..fddcd4fb 100644 --- a/README.md +++ b/README.md @@ -36,11 +36,15 @@ Mostly a tech demo at this point. I hope to find time to work more on this in th Features: -* Text +* Labels * Buttons, checkboxes, radio buttons and sliders * Horizontal or vertical layout * Column layout * Collapsible headers (sections) +* Windows +* Resizable regions +* Vertical scolling +* Simple text input * Anti-aliased rendering of circles, rounded rectangles and lines. diff --git a/emigui/README.md b/emigui/README.md index 0a9fe6fb..effb9611 100644 --- a/emigui/README.md +++ b/emigui/README.md @@ -41,6 +41,7 @@ This is the core library crate Emigui. It is fully platform independent without ### Web version: * [x] Scroll input * [x] Change to resize cursor on hover +* [ ] Make it a JS library for easily creating your own stuff ### Animations Add extremely quick animations for some things, maybe 2-3 frames. For instance: @@ -63,10 +64,15 @@ Add extremely quick animations for some things, maybe 2-3 frames. For instance: ### Input * [ ] Distinguish between clicks and drags * [ ] Double-click -* [ ] Text +* [x] Text + +### Debugability / Inspection +* [x] Widget debug rectangles +* [ ] Easily debug why something keeps expanding + ### Other -* [ ] Persist UI state in external storage +* [x] Persist UI state in external storage * [ ] Pixel-perfect rendering (round positions to nearest pixel). * [ ] Build in a profiler which tracks which region in which window takes up CPU. * [ ] Draw as flame graph diff --git a/emigui/src/containers/collapsing_header.rs b/emigui/src/containers/collapsing_header.rs index 97c9b4be..2f399605 100644 --- a/emigui/src/containers/collapsing_header.rs +++ b/emigui/src/containers/collapsing_header.rs @@ -1,16 +1,18 @@ use crate::{layout::Direction, *}; -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +#[serde(default)] pub(crate) struct State { - pub open: bool, - pub toggle_time: f64, + open: bool, + #[serde(skip)] // Times are relative, and we don't want to continue animations anyway + toggle_time: f64, } impl Default for State { fn default() -> Self { Self { open: false, - toggle_time: -std::f64::INFINITY, + toggle_time: -f64::INFINITY, } } } @@ -59,7 +61,7 @@ impl CollapsingHeader { ); let state = { - let mut memory = region.ctx.memory.lock(); + let mut memory = region.ctx.memory(); let mut state = memory.collapsing_headers.entry(id).or_insert(State { open: default_open, ..Default::default() diff --git a/emigui/src/containers/floating.rs b/emigui/src/containers/floating.rs index d3ccadb9..028756f9 100644 --- a/emigui/src/containers/floating.rs +++ b/emigui/src/containers/floating.rs @@ -7,8 +7,8 @@ use std::{fmt::Debug, hash::Hash, sync::Arc}; use crate::*; -#[derive(Clone, Copy, Debug)] -pub struct State { +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub(crate) struct State { /// Last known pos pub pos: Pos2, @@ -50,7 +50,7 @@ impl Floating { let id = ctx.register_unique_id(self.id, "Floating", default_pos); let layer = Layer::Window(id); - let (mut state, _is_new) = match ctx.memory.lock().get_floating(id) { + let (mut state, _is_new) = match ctx.memory().get_floating(id) { Some(state) => (state, false), None => { let state = State { @@ -90,15 +90,15 @@ impl Floating { state.pos = state.pos.round(); if move_interact.active || mouse_pressed_on_floating(ctx, id) { - ctx.memory.lock().move_floating_to_top(id); + ctx.memory().move_floating_to_top(id); } - ctx.memory.lock().set_floating_state(id, state); + ctx.memory().set_floating_state(id, state); } } fn mouse_pressed_on_floating(ctx: &Context, id: Id) -> bool { if let Some(mouse_pos) = ctx.input.mouse_pos { - ctx.input.mouse_pressed && ctx.memory.lock().layer_at(mouse_pos) == Layer::Window(id) + ctx.input.mouse_pressed && ctx.memory().layer_at(mouse_pos) == Layer::Window(id) } else { false } diff --git a/emigui/src/containers/resize.rs b/emigui/src/containers/resize.rs index 5c83f91e..dad304a3 100644 --- a/emigui/src/containers/resize.rs +++ b/emigui/src/containers/resize.rs @@ -1,9 +1,9 @@ #![allow(unused_variables)] // TODO use crate::*; -#[derive(Clone, Copy, Debug)] -pub struct State { - pub size: Vec2, +#[derive(Clone, Copy, Debug, Deserialize, Serialize)] +pub(crate) struct State { + size: Vec2, } // TODO: auto-shink/grow should be part of another container! @@ -229,7 +229,7 @@ impl Resize { paint_resize_corner(region, &corner_rect, &corner_interact); if corner_interact.hovered || corner_interact.active { - region.ctx().output.lock().cursor_icon = CursorIcon::ResizeNwSe; + region.ctx().output().cursor_icon = CursorIcon::ResizeNwSe; } region.memory().resize.insert(id, state); diff --git a/emigui/src/containers/scroll_area.rs b/emigui/src/containers/scroll_area.rs index ae6edfb8..3e1560d9 100644 --- a/emigui/src/containers/scroll_area.rs +++ b/emigui/src/containers/scroll_area.rs @@ -1,11 +1,12 @@ use crate::*; -#[derive(Clone, Copy, Debug, Default)] -pub struct State { +#[derive(Clone, Copy, Debug, Default, Deserialize, Serialize)] +#[serde(default)] +pub(crate) struct State { /// Positive offset means scrolling down/right - pub offset: Vec2, + offset: Vec2, - pub show_scroll: bool, // TODO: default value? + show_scroll: bool, // TODO: default value? } // TODO: rename VScroll @@ -49,8 +50,7 @@ impl ScrollArea { let scroll_area_id = outer_region.id.with("scroll_area"); let mut state = ctx - .memory - .lock() + .memory() .scroll_areas .get(&scroll_area_id) .cloned() @@ -105,7 +105,7 @@ impl ScrollArea { } // TODO: check that nothing else is being inteacted with - if outer_region.contains_mouse(&outer_rect) && ctx.memory.lock().active_id.is_none() { + if outer_region.contains_mouse(&outer_rect) && ctx.memory().active_id.is_none() { state.offset.y -= ctx.input.scroll_delta.y; } @@ -194,9 +194,7 @@ impl ScrollArea { state.show_scroll = show_scroll_this_frame; outer_region - .ctx() - .memory - .lock() + .memory() .scroll_areas .insert(scroll_area_id, state); } diff --git a/emigui/src/context.rs b/emigui/src/context.rs index 0bf42f64..44825368 100644 --- a/emigui/src/context.rs +++ b/emigui/src/context.rs @@ -12,10 +12,10 @@ pub struct Context { /// Raw input from last frame. Use `input()` instead. pub(crate) last_raw_input: RawInput, pub(crate) input: GuiInput, - pub(crate) memory: Mutex, + memory: Mutex, pub(crate) graphics: Mutex, - pub output: Mutex, + output: Mutex, /// Used to debug name clashes of e.g. windows used_ids: Mutex>, @@ -51,6 +51,31 @@ impl Context { } } + pub fn memory(&self) -> parking_lot::MutexGuard { + self.memory.lock() + } + + pub fn output(&self) -> parking_lot::MutexGuard { + self.output.lock() + } + + pub fn input(&self) -> &GuiInput { + &self.input + } + + /// Raw input from last frame. Use `input()` instead. + pub fn last_raw_input(&self) -> &RawInput { + &self.last_raw_input + } + + pub fn style(&self) -> Style { + *self.style.lock() + } + + pub fn set_style(&self, style: Style) { + *self.style.lock() = style; + } + /// Useful for pixel-perfect rendering pub fn round_to_pixel(&self, point: f32) -> f32 { (point * self.input.pixels_per_point).round() / self.input.pixels_per_point @@ -64,23 +89,6 @@ impl Context { vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y)) } - /// Raw input from last frame. Use `input()` instead. - pub fn last_raw_input(&self) -> &RawInput { - &self.last_raw_input - } - - pub fn input(&self) -> &GuiInput { - &self.input - } - - pub fn style(&self) -> Style { - *self.style.lock() - } - - pub fn set_style(&self, style: Style) { - *self.style.lock() = style; - } - // TODO: move pub fn begin_frame(&mut self, gui_input: GuiInput) { self.used_ids.lock().clear(); diff --git a/emigui/src/emigui.rs b/emigui/src/emigui.rs index 2e4ae935..b3e1f6ff 100644 --- a/emigui/src/emigui.rs +++ b/emigui/src/emigui.rs @@ -12,8 +12,8 @@ struct Stats { /// Encapsulates input, layout and painting for ease of use. /// TODO: merge into Context pub struct Emigui { - pub last_input: RawInput, - pub ctx: Arc, + last_input: RawInput, + ctx: Arc, stats: Stats, mesher_options: MesherOptions, } @@ -28,13 +28,17 @@ impl Emigui { } } + pub fn ctx(&self) -> &Arc { + &self.ctx + } + pub fn texture(&self) -> &Texture { self.ctx.fonts.texture() } pub fn begin_frame(&mut self, new_input: RawInput) { if !self.last_input.mouse_down || self.last_input.mouse_pos.is_none() { - self.ctx.memory.lock().active_id = None; + self.ctx.memory().active_id = None; } let gui_input = GuiInput::from_last_and_new(&self.last_input, &new_input); diff --git a/emigui/src/id.rs b/emigui/src/id.rs index 4783e519..aa6fc603 100644 --- a/emigui/src/id.rs +++ b/emigui/src/id.rs @@ -31,7 +31,7 @@ use std::{collections::hash_map::DefaultHasher, hash::Hash}; use crate::math::Pos2; -#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq)] +#[derive(Clone, Copy, Debug, Hash, Eq, PartialEq, Deserialize, Serialize)] pub struct Id(u64); impl Id { diff --git a/emigui/src/memory.rs b/emigui/src/memory.rs index 4304302f..8debf58d 100644 --- a/emigui/src/memory.rs +++ b/emigui/src/memory.rs @@ -5,12 +5,15 @@ use crate::{ Id, Layer, Pos2, Rect, }; -#[derive(Clone, Debug, Default)] +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(default)] pub struct Memory { /// The widget being interacted with (e.g. dragged, in case of a slider). + #[serde(skip)] pub(crate) active_id: Option, /// The widget with keyboard focus (i.e. a text input field). + #[serde(skip)] pub(crate) kb_focus_id: Option, // states of various types of widgets @@ -24,11 +27,11 @@ pub struct Memory { } impl Memory { - pub fn get_floating(&mut self, id: Id) -> Option { + pub(crate) fn get_floating(&mut self, id: Id) -> Option { self.floating.get(&id).cloned() } - pub fn set_floating_state(&mut self, id: Id, state: floating::State) { + pub(crate) fn set_floating_state(&mut self, id: Id, state: floating::State) { let did_insert = self.floating.insert(id, state).is_none(); if did_insert { self.floating_order.push(id); diff --git a/emigui/src/region.rs b/emigui/src/region.rs index f2aacbab..51bc6454 100644 --- a/emigui/src/region.rs +++ b/emigui/src/region.rs @@ -119,11 +119,11 @@ impl Region { } pub fn memory(&self) -> parking_lot::MutexGuard { - self.ctx.memory.lock() + self.ctx.memory() } pub fn output(&self) -> parking_lot::MutexGuard { - self.ctx.output.lock() + self.ctx.output() } pub fn fonts(&self) -> &Fonts { diff --git a/emigui/src/widgets.rs b/emigui/src/widgets.rs index 0196c3d6..db103d50 100644 --- a/emigui/src/widgets.rs +++ b/emigui/src/widgets.rs @@ -101,10 +101,10 @@ impl Widget for Hyperlink { let (text, text_size) = font.layout_multiline(&self.text, region.available_width()); let interact = region.reserve_space(text_size, Some(id)); if interact.hovered { - region.ctx().output.lock().cursor_icon = CursorIcon::PointingHand; + region.ctx().output().cursor_icon = CursorIcon::PointingHand; } if interact.clicked { - region.ctx().output.lock().open_url = Some(self.url); + region.ctx().output().open_url = Some(self.url); } if interact.hovered { diff --git a/emigui/src/widgets/text_edit.rs b/emigui/src/widgets/text_edit.rs index 1d549e51..6d3604d4 100644 --- a/emigui/src/widgets/text_edit.rs +++ b/emigui/src/widgets/text_edit.rs @@ -57,7 +57,7 @@ impl<'t> Widget for TextEdit<'t> { match event { Event::Copy | Event::Cut => { // TODO: cut - region.ctx().output.lock().copied_text = self.text.clone(); + region.ctx().output().copied_text = self.text.clone(); } Event::Text(text) => { if text == "\u{7f}" { diff --git a/emigui_glium/Cargo.toml b/emigui_glium/Cargo.toml index 68481094..05f08c06 100644 --- a/emigui_glium/Cargo.toml +++ b/emigui_glium/Cargo.toml @@ -9,4 +9,5 @@ emigui = { path = "../emigui" } clipboard = "0.5" glium = "0.24" +serde_json = "1" webbrowser = "0.5" diff --git a/emigui_glium/src/lib.rs b/emigui_glium/src/lib.rs index 6028ee49..e825f131 100644 --- a/emigui_glium/src/lib.rs +++ b/emigui_glium/src/lib.rs @@ -165,3 +165,32 @@ pub fn handle_output( .gl_window() .set_cursor(translate_cursor(output.cursor_icon)); } + +// ---------------------------------------------------------------------------- + +pub fn read_memory(ctx: &Context, memory_json_path: impl AsRef) { + match std::fs::File::open(memory_json_path) { + Ok(file) => { + let reader = std::io::BufReader::new(file); + match serde_json::from_reader(reader) { + Ok(memory) => { + *ctx.memory() = memory; + } + Err(err) => { + eprintln!("ERROR: Failed to parse memory json: {}", err); + } + } + } + Err(err) => { + eprintln!("ERROR: Failed to read memory file: {}", err); + } + } +} + +pub fn write_memory( + ctx: &Context, + memory_json_path: impl AsRef, +) -> Result<(), Box> { + serde_json::to_writer(std::fs::File::create(memory_json_path)?, &*ctx.memory())?; + Ok(()) +} diff --git a/emigui_wasm/src/lib.rs b/emigui_wasm/src/lib.rs index cf97555f..ee0dac84 100644 --- a/emigui_wasm/src/lib.rs +++ b/emigui_wasm/src/lib.rs @@ -37,3 +37,30 @@ pub fn local_storage_set(key: &str, value: &str) { pub fn local_storage_remove(key: &str) { local_storage().map(|storage| storage.remove_item(key)); } + +pub fn load_memory(ctx: &emigui::Context) { + if let Some(memory_string) = local_storage_get("emigui_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: &emigui::Context) { + match serde_json::to_string(&*ctx.memory()) { + Ok(json) => { + local_storage_set("emigui_memory_json", &json); + } + Err(err) => { + console_log(format!( + "ERROR: Failed to seriealize memory as json: {}", + err + )); + } + } +} diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index bc2493f8..9b59d03d 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -46,6 +46,9 @@ fn main() { let mut example_app = ExampleWindow::default(); let mut clipboard = emigui_glium::init_clipboard(); + let memory_path = "emigui.json"; + emigui_glium::read_memory(&emigui.ctx(), memory_path); + while running { { // Keep smooth frame rate. TODO: proper vsync @@ -113,6 +116,10 @@ fn main() { painter.paint_batches(&display, paint_batches, emigui.texture()); emigui_glium::handle_output(output, &display, clipboard.as_mut()); } + + if let Err(err) = emigui_glium::write_memory(&emigui.ctx(), memory_path) { + eprintln!("ERROR: Failed to save emigui state: {}", err); + } } pub fn mean_frame_time(frame_times: &VecDeque) -> f64 { diff --git a/example_wasm/src/lib.rs b/example_wasm/src/lib.rs index 744062a7..31f96023 100644 --- a/example_wasm/src/lib.rs +++ b/example_wasm/src/lib.rs @@ -31,9 +31,11 @@ pub struct State { impl State { fn new(canvas_id: &str, pixels_per_point: f32) -> Result { + let emigui = Emigui::new(pixels_per_point); + emigui_wasm::load_memory(emigui.ctx()); Ok(State { example_app: Default::default(), - emigui: Emigui::new(pixels_per_point), + emigui, webgl_painter: emigui_wasm::webgl::Painter::new(canvas_id)?, frame_times: Default::default(), }) @@ -111,6 +113,8 @@ impl State { pixels_per_point, )?; + emigui_wasm::save_memory(self.emigui.ctx()); // TODO: don't save every frame + Ok(output) } }