Browse Source

Store/restore emigui memory state (window positions, sizes etc)

readable-ids
Emil Ernerfeldt 5 years ago
parent
commit
bfbb669d02
  1. 1
      Cargo.lock
  2. 6
      README.md
  3. 10
      emigui/README.md
  4. 12
      emigui/src/containers/collapsing_header.rs
  5. 12
      emigui/src/containers/floating.rs
  6. 8
      emigui/src/containers/resize.rs
  7. 18
      emigui/src/containers/scroll_area.rs
  8. 34
      emigui/src/context.rs
  9. 10
      emigui/src/emigui.rs
  10. 2
      emigui/src/id.rs
  11. 9
      emigui/src/memory.rs
  12. 4
      emigui/src/region.rs
  13. 4
      emigui/src/widgets.rs
  14. 2
      emigui/src/widgets/text_edit.rs
  15. 1
      emigui_glium/Cargo.toml
  16. 29
      emigui_glium/src/lib.rs
  17. 27
      emigui_wasm/src/lib.rs
  18. 7
      example_glium/src/main.rs
  19. 6
      example_wasm/src/lib.rs

1
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)",
]

6
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.

10
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

12
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()

12
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
}

8
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);

18
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);
}

34
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>,
memory: Mutex<Memory>,
pub(crate) graphics: Mutex<GraphicLayers>,
pub output: Mutex<Output>,
output: Mutex<Output>,
/// Used to debug name clashes of e.g. windows
used_ids: Mutex<HashMap<Id, Pos2>>,
@ -51,17 +51,16 @@ impl Context {
}
}
/// 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
pub fn memory(&self) -> parking_lot::MutexGuard<Memory> {
self.memory.lock()
}
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y))
pub fn output(&self) -> parking_lot::MutexGuard<Output> {
self.output.lock()
}
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y))
pub fn input(&self) -> &GuiInput {
&self.input
}
/// Raw input from last frame. Use `input()` instead.
@ -69,10 +68,6 @@ impl Context {
&self.last_raw_input
}
pub fn input(&self) -> &GuiInput {
&self.input
}
pub fn style(&self) -> Style {
*self.style.lock()
}
@ -81,6 +76,19 @@ impl Context {
*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
}
pub fn round_pos_to_pixels(&self, pos: Pos2) -> Pos2 {
pos2(self.round_to_pixel(pos.x), self.round_to_pixel(pos.y))
}
pub fn round_vec_to_pixels(&self, vec: Vec2) -> Vec2 {
vec2(self.round_to_pixel(vec.x), self.round_to_pixel(vec.y))
}
// TODO: move
pub fn begin_frame(&mut self, gui_input: GuiInput) {
self.used_ids.lock().clear();

10
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<Context>,
last_input: RawInput,
ctx: Arc<Context>,
stats: Stats,
mesher_options: MesherOptions,
}
@ -28,13 +28,17 @@ impl Emigui {
}
}
pub fn ctx(&self) -> &Arc<Context> {
&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);

2
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 {

9
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<Id>,
/// The widget with keyboard focus (i.e. a text input field).
#[serde(skip)]
pub(crate) kb_focus_id: Option<Id>,
// states of various types of widgets
@ -24,11 +27,11 @@ pub struct Memory {
}
impl Memory {
pub fn get_floating(&mut self, id: Id) -> Option<floating::State> {
pub(crate) fn get_floating(&mut self, id: Id) -> Option<floating::State> {
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);

4
emigui/src/region.rs

@ -119,11 +119,11 @@ impl Region {
}
pub fn memory(&self) -> parking_lot::MutexGuard<Memory> {
self.ctx.memory.lock()
self.ctx.memory()
}
pub fn output(&self) -> parking_lot::MutexGuard<Output> {
self.ctx.output.lock()
self.ctx.output()
}
pub fn fonts(&self) -> &Fonts {

4
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 {

2
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}" {

1
emigui_glium/Cargo.toml

@ -9,4 +9,5 @@ emigui = { path = "../emigui" }
clipboard = "0.5"
glium = "0.24"
serde_json = "1"
webbrowser = "0.5"

29
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<std::path::Path>) {
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<std::path::Path>,
) -> Result<(), Box<dyn std::error::Error>> {
serde_json::to_writer(std::fs::File::create(memory_json_path)?, &*ctx.memory())?;
Ok(())
}

27
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
));
}
}
}

7
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>) -> f64 {

6
example_wasm/src/lib.rs

@ -31,9 +31,11 @@ pub struct State {
impl State {
fn new(canvas_id: &str, pixels_per_point: f32) -> Result<State, JsValue> {
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)
}
}

Loading…
Cancel
Save