From 1d3836ba80e86904c6e50a02f4c2a846821aeef5 Mon Sep 17 00:00:00 2001 From: Emil Ernerfeldt Date: Fri, 17 Apr 2020 14:26:36 +0200 Subject: [PATCH] Add rudimentary support for gui windows that you can move around --- emigui/src/emigui.rs | 10 +- emigui/src/layers.rs | 43 +++++++++ emigui/src/layout.rs | 194 ++++++++++++++++++++++++-------------- emigui/src/lib.rs | 6 +- emigui/src/math.rs | 3 + emigui/src/mesher.rs | 4 +- emigui/src/style.rs | 2 +- emigui/src/types.rs | 8 ++ emigui/src/window.rs | 98 +++++++++++++++++++ example_glium/src/main.rs | 8 +- 10 files changed, 297 insertions(+), 79 deletions(-) create mode 100644 emigui/src/layers.rs create mode 100644 emigui/src/window.rs diff --git a/emigui/src/emigui.rs b/emigui/src/emigui.rs index f7a2cc1d..928be4b8 100644 --- a/emigui/src/emigui.rs +++ b/emigui/src/emigui.rs @@ -6,7 +6,7 @@ use crate::{ mesher::Mesher, types::{GuiInput, PaintCmd}, widgets::*, - FontDefinitions, Fonts, Mesh, RawInput, Texture, + FontDefinitions, Fonts, Layer, Mesh, RawInput, Texture, }; #[derive(Clone, Copy, Default)] @@ -47,17 +47,17 @@ impl Emigui { self.data = Arc::new(new_data); } - pub fn whole_screen_region(&mut self) -> layout::Region { - let size = self.data.input.screen_size; - layout::Region { + pub fn whole_screen_region(&mut self) -> Region { + Region { data: self.data.clone(), + layer: Layer::Background, style: self.data.style(), id: Default::default(), dir: layout::Direction::Vertical, align: layout::Align::Center, cursor: Default::default(), bounding_size: Default::default(), - available_space: size, + available_space: self.data.input.screen_size, } } diff --git a/emigui/src/layers.rs b/emigui/src/layers.rs new file mode 100644 index 00000000..b84c871e --- /dev/null +++ b/emigui/src/layers.rs @@ -0,0 +1,43 @@ +use std::collections::HashMap; + +use crate::{Id, PaintCmd}; + +// TODO: support multiple windows +#[derive(Clone, Copy, Eq, PartialEq, Hash)] +pub enum Layer { + Background, + Window(Id), + Popup, +} + +type PaintList = Vec; + +/// TODO: improve this +#[derive(Clone, Default)] +pub struct GraphicLayers { + bg: PaintList, + windows: HashMap, + popup: PaintList, +} + +impl GraphicLayers { + pub fn layer(&mut self, layer: Layer) -> &mut PaintList { + match layer { + Layer::Background => &mut self.bg, + Layer::Window(id) => self.windows.entry(id).or_default(), + Layer::Popup => &mut self.popup, + } + } + + pub fn drain(&mut self) -> impl ExactSizeIterator { + let mut all_commands: Vec<_> = self.bg.drain(..).collect(); + + // TODO: order + for window in self.windows.values_mut() { + all_commands.extend(window.drain(..)); + } + + all_commands.extend(self.popup.drain(..)); + all_commands.into_iter() + } +} diff --git a/emigui/src/layout.rs b/emigui/src/layout.rs index 3bef440d..5016ec4b 100644 --- a/emigui/src/layout.rs +++ b/emigui/src/layout.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashSet, + collections::{HashMap, HashSet}, hash::Hash, sync::{Arc, Mutex}, }; @@ -12,6 +12,8 @@ use crate::{ style::Style, types::*, widgets::{Label, Widget}, + window::WindowState, + GraphicLayers, Layer, }; // ---------------------------------------------------------------------------- @@ -66,6 +68,44 @@ pub struct Memory { /// Which foldable regions are open. open_foldables: HashSet, + + windows: HashMap, + + /// Top is last + window_order: Vec, +} + +impl Memory { + /// default_rect: where to put it if it does NOT exist + pub fn get_or_create_window(&mut self, id: Id, default_rect: Rect) -> WindowState { + if let Some(state) = self.windows.get(&id) { + *state + } else { + let state = WindowState { rect: default_rect }; + self.windows.insert(id, state); + self.window_order.push(id); + state + } + } + + pub fn set_window_state(&mut self, id: Id, state: WindowState) { + self.windows.insert(id, state); + } + + pub fn layer_at(&self, pos: Vec2) -> Layer { + for window_id in self.window_order.iter().rev() { + if let Some(state) = self.windows.get(window_id) { + if state.rect.contains(pos) { + return Layer::Window(*window_id); + } + } + } + Layer::Background + } + + pub fn move_window_to_top(&mut self, _id: Id) { + // TODO + } } // ---------------------------------------------------------------------------- @@ -103,6 +143,7 @@ impl Default for Align { // ---------------------------------------------------------------------------- +// TODO: newtype pub type Id = u64; pub fn make_id(source: &H) -> Id { @@ -114,25 +155,7 @@ pub fn make_id(source: &H) -> Id { // ---------------------------------------------------------------------------- -/// TODO: improve this -#[derive(Clone, Default)] -pub struct GraphicLayers { - pub(crate) graphics: Vec, - pub(crate) hovering_graphics: Vec, -} - -impl GraphicLayers { - pub fn drain(&mut self) -> impl ExactSizeIterator { - // TODO: there must be a nicer way to do this? - let mut all_commands: Vec<_> = self.graphics.drain(..).collect(); - all_commands.extend(self.hovering_graphics.drain(..)); - all_commands.into_iter() - } -} - -// ---------------------------------------------------------------------------- - -// TODO: give a better name. +// TODO: give a better name. Context? /// Contains the input, style and output of all GUI commands. pub struct Data { /// The default style for new regions @@ -190,6 +213,42 @@ impl Data { pub fn any_active(&self) -> bool { self.memory.lock().unwrap().active_id.is_some() } + + pub fn interact(&self, layer: Layer, rect: Rect, interaction_id: Option) -> InteractInfo { + let mut memory = self.memory.lock().unwrap(); + + // TODO: check for windows on top of the current rect! + + let hovered = if let Some(mouse_pos) = self.input.mouse_pos { + if rect.contains(mouse_pos) { + let is_something_else_active = + memory.active_id.is_some() && memory.active_id != interaction_id; + + !is_something_else_active && layer == memory.layer_at(mouse_pos) + } else { + false + } + } else { + false + }; + let active = if interaction_id.is_some() { + if hovered && self.input.mouse_clicked { + memory.active_id = interaction_id; + } + memory.active_id == interaction_id + } else { + false + }; + + let clicked = hovered && self.input.mouse_released; + + InteractInfo { + rect, + hovered, + clicked, + active, + } + } } impl Data { @@ -205,14 +264,15 @@ pub fn show_popup(data: &Arc, window_pos: Vec2, add_contents: F) where F: FnOnce(&mut Region), { - // TODO: nicer way to do layering! - let num_graphics_before = data.graphics.lock().unwrap().graphics.len(); + let layer = Layer::Popup; + let where_to_put_background = data.graphics.lock().unwrap().layer(layer).len(); let style = data.style(); let window_padding = style.window_padding; - let mut popup_region = Region { + let mut contents_region = Region { data: data.clone(), + layer: Layer::Popup, style, id: Default::default(), dir: Direction::Vertical, @@ -222,26 +282,30 @@ where available_space: vec2(data.input.screen_size.x.min(350.0), std::f32::INFINITY), // TODO: popup/tooltip width }; - add_contents(&mut popup_region); + add_contents(&mut contents_region); + + // Now insert popup background: // TODO: handle the last item_spacing in a nicer way - let inner_size = popup_region.bounding_size - style.item_spacing; + let inner_size = contents_region.bounding_size - style.item_spacing; let outer_size = inner_size + 2.0 * window_padding; let rect = Rect::from_min_size(window_pos, outer_size); let mut graphics = data.graphics.lock().unwrap(); - let popup_graphics = graphics.graphics.split_off(num_graphics_before); - graphics.hovering_graphics.push(PaintCmd::Rect { - corner_radius: 5.0, - fill_color: Some(style.background_fill_color()), - outline: Some(Outline { - color: color::gray(255, 255), // TODO - width: 1.0, - }), - rect, - }); - graphics.hovering_graphics.extend(popup_graphics); + let graphics = graphics.layer(layer); + graphics.insert( + where_to_put_background, + PaintCmd::Rect { + corner_radius: 5.0, + fill_color: Some(style.background_fill_color()), + outline: Some(Outline { + color: color::WHITE, + width: 1.0, + }), + rect, + }, + ); } // ---------------------------------------------------------------------------- @@ -252,6 +316,9 @@ where pub struct Region { pub(crate) data: Arc, + /// Where to put the graphics output of this Region + pub(crate) layer: Layer, + pub(crate) style: Style, /// Unique ID of this region. @@ -262,14 +329,15 @@ pub struct Region { pub(crate) align: Align, - /// Changes only along self.dir + /// Where the next widget will be put. + /// Progresses along self.dir pub(crate) cursor: Vec2, - /// Bounding box children. + /// Bounding box of children. /// We keep track of our max-size along the orthogonal to self.dir pub(crate) bounding_size: Vec2, - /// This how much space we can take up without overflowing our parent. + /// This how much more space we can take up without overflowing our parent. /// Shrinks as cursor increments. pub(crate) available_space: Vec2, } @@ -279,7 +347,12 @@ impl Region { /// Can be used for free painting. /// NOTE: all coordinates are screen coordinates! pub fn add_paint_cmd(&mut self, paint_cmd: PaintCmd) { - self.data.graphics.lock().unwrap().graphics.push(paint_cmd) + self.data + .graphics + .lock() + .unwrap() + .layer(self.layer) + .push(paint_cmd) } pub fn add_paint_cmds(&mut self, mut cmds: Vec) { @@ -287,7 +360,7 @@ impl Region { .graphics .lock() .unwrap() - .graphics + .layer(self.layer) .append(&mut cmds) } @@ -374,9 +447,12 @@ impl Region { let stroke_color = self.style.interact_stroke_color(&interact); self.add_paint_cmd(PaintCmd::Rect { - corner_radius: 3.0, + corner_radius: 5.0, fill_color: Some(fill_color), - outline: None, + outline: Some(Outline { + width: 1.0, + color: color::WHITE, + }), rect: interact.rect, }); @@ -427,6 +503,7 @@ impl Region { let indent = vec2(self.style.indent, 0.0); let mut child_region = Region { data: self.data.clone(), + layer: self.layer, style: self.style, id: self.id, dir: self.dir, @@ -444,6 +521,7 @@ impl Region { pub fn relative_region(&mut self, rect: Rect) -> Region { Region { data: self.data.clone(), + layer: self.layer, style: self.style, id: self.id, dir: self.dir, @@ -485,6 +563,7 @@ impl Region { { let mut child_region = Region { data: self.data.clone(), + layer: self.layer, style: self.style, id: self.id, dir, @@ -532,6 +611,7 @@ impl Region { let mut columns: Vec = (0..num_columns) .map(|col_idx| Region { data: self.data.clone(), + layer: self.layer, style: self.style, id: self.make_child_id(&("column", col_idx)), dir: Direction::Vertical, @@ -565,35 +645,11 @@ impl Region { pub fn reserve_space(&mut self, size: Vec2, interaction_id: Option) -> InteractInfo { let pos = self.reserve_space_without_padding(size + self.style.item_spacing); let rect = Rect::from_min_size(pos, size); - let mut memory = self.data.memory.lock().unwrap(); - - let is_something_else_active = - memory.active_id.is_some() && memory.active_id != interaction_id; - - let hovered = if let Some(mouse_pos) = self.input().mouse_pos { - !is_something_else_active && rect.contains(mouse_pos) - } else { - false - }; - let active = if interaction_id.is_some() { - if hovered && self.input().mouse_clicked { - memory.active_id = interaction_id; - } - memory.active_id == interaction_id - } else { - false - }; - - let clicked = hovered && self.input().mouse_released; - InteractInfo { - rect, - hovered, - clicked, - active, - } + self.data.interact(self.layer, rect, interaction_id) } /// Reserve this much space and move the cursor. + /// Returns where to put the widget. pub fn reserve_space_without_padding(&mut self, size: Vec2) -> Vec2 { let mut pos = self.cursor; if self.dir == Direction::Horizontal { diff --git a/emigui/src/lib.rs b/emigui/src/lib.rs index 7e8ab469..5ffca308 100644 --- a/emigui/src/lib.rs +++ b/emigui/src/lib.rs @@ -11,6 +11,7 @@ mod emigui; pub mod example_app; mod font; mod fonts; +mod layers; mod layout; pub mod math; pub mod mesher; @@ -18,15 +19,18 @@ mod style; mod texture_atlas; mod types; pub mod widgets; +mod window; pub use { crate::emigui::Emigui, color::Color, fonts::{FontDefinitions, Fonts, TextStyle}, - layout::{Align, Region}, + layers::*, + layout::{Align, Id, Region}, math::*, mesher::{Mesh, Vertex}, style::Style, texture_atlas::Texture, types::*, + window::Window, }; diff --git a/emigui/src/math.rs b/emigui/src/math.rs index 08660925..29704441 100644 --- a/emigui/src/math.rs +++ b/emigui/src/math.rs @@ -149,6 +149,9 @@ impl Rect { pub fn expand(self, amnt: f32) -> Self { Rect::from_center_size(self.center(), self.size() + 2.0 * vec2(amnt, amnt)) } + pub fn translate(self, amnt: Vec2) -> Self { + Rect::from_min_size(self.min() + amnt, self.size()) + } pub fn contains(&self, p: Vec2) -> bool { self.min.x <= p.x diff --git a/emigui/src/mesher.rs b/emigui/src/mesher.rs index a5660880..f4d1b4c2 100644 --- a/emigui/src/mesher.rs +++ b/emigui/src/mesher.rs @@ -389,8 +389,8 @@ impl Mesher { add_arc(vec2(max.x - cr, min.y + cr), 3); } - if let Some(color) = fill_color { - self.fill_closed_path(&path_points, &path_normals, *color); + if let Some(fill_color) = fill_color { + self.fill_closed_path(&path_points, &path_normals, *fill_color); } if let Some(outline) = outline { self.paint_path( diff --git a/emigui/src/style.rs b/emigui/src/style.rs index 1f6e5837..db8d94a9 100644 --- a/emigui/src/style.rs +++ b/emigui/src/style.rs @@ -44,7 +44,7 @@ impl Default for Style { impl Style { /// e.g. the background of the slider pub fn background_fill_color(&self) -> Color { - gray(34, 200) + gray(34, 230) } pub fn text_color(&self) -> Color { diff --git a/emigui/src/types.rs b/emigui/src/types.rs index 09142f2d..1b524f36 100644 --- a/emigui/src/types.rs +++ b/emigui/src/types.rs @@ -38,6 +38,9 @@ pub struct GuiInput { /// Current position of the mouse in points. pub mouse_pos: Option, + /// How much the mouse moved compared to last frame, in points. + pub mouse_move: Vec2, + /// Size of the screen in points. pub screen_size: Vec2, @@ -47,11 +50,16 @@ pub struct GuiInput { impl GuiInput { pub fn from_last_and_new(last: &RawInput, new: &RawInput) -> GuiInput { + let mouse_move = new + .mouse_pos + .and_then(|new| last.mouse_pos.map(|last| new - last)) + .unwrap_or_default(); GuiInput { mouse_down: new.mouse_down, mouse_clicked: !last.mouse_down && new.mouse_down, mouse_released: last.mouse_down && !new.mouse_down, mouse_pos: new.mouse_pos, + mouse_move, screen_size: new.screen_size, pixels_per_point: new.pixels_per_point, } diff --git a/emigui/src/window.rs b/emigui/src/window.rs new file mode 100644 index 00000000..2d7620a9 --- /dev/null +++ b/emigui/src/window.rs @@ -0,0 +1,98 @@ +use std::sync::Arc; + +use crate::{ + layout::{make_id, Data, Direction}, + widgets::Label, + *, +}; + +#[derive(Clone, Copy, Debug)] +pub struct WindowState { + /// Last known pos/size + pub rect: Rect, +} + +pub struct Window { + /// The title of the window and by default the source of its identity. + title: String, +} + +impl Window { + pub fn new>(title: S) -> Self { + Self { + title: title.into(), + } + } + + pub fn show(self, data: Arc, add_contents: F) + where + F: FnOnce(&mut Region), + { + let id = make_id(&self.title); + + let mut state = data.memory.lock().unwrap().get_or_create_window( + id, + Rect::from_min_size( + vec2(400.0, 200.0), // TODO + vec2(200.0, 200.0), // TODO + ), + ); + + let layer = Layer::Window(id); + let where_to_put_background = data.graphics.lock().unwrap().layer(layer).len(); + + let style = data.style(); + let window_padding = style.window_padding; + + let mut contents_region = Region { + data: data.clone(), + layer: Layer::Popup, + style, + id: Default::default(), + dir: Direction::Vertical, + align: Align::Min, + cursor: state.rect.min() + window_padding, + bounding_size: vec2(0.0, 0.0), + available_space: vec2(data.input.screen_size.x.min(350.0), std::f32::INFINITY), // TODO: window.width + }; + + // Show top bar: + contents_region.add(Label::new(self.title).text_style(TextStyle::Heading)); + + add_contents(&mut contents_region); + + // Now insert window background: + + // TODO: handle the last item_spacing in a nicer way + let inner_size = contents_region.bounding_size - style.item_spacing; + let outer_size = inner_size + 2.0 * window_padding; + + state.rect = Rect::from_min_size(state.rect.min(), outer_size); + + let mut graphics = data.graphics.lock().unwrap(); + let graphics = graphics.layer(layer); + graphics.insert( + where_to_put_background, + PaintCmd::Rect { + corner_radius: 5.0, + fill_color: Some(style.background_fill_color()), + outline: Some(Outline { + color: color::WHITE, + width: 1.0, + }), + rect: state.rect, + }, + ); + + let interact = data.interact(layer, state.rect, Some(id)); + if interact.active { + state.rect = state.rect.translate(data.input().mouse_move); + } + + let mut memory = data.memory.lock().unwrap(); + if interact.active || interact.clicked { + memory.move_window_to_top(id); + } + memory.set_window_state(id, state); + } +} diff --git a/example_glium/src/main.rs b/example_glium/src/main.rs index eab80155..e18de2e4 100644 --- a/example_glium/src/main.rs +++ b/example_glium/src/main.rs @@ -8,7 +8,7 @@ use { label, math::vec2, widgets::{Button, Label}, - Align, Emigui, + Align, Emigui, Window, }, emigui_glium::Painter, glium::glutin, @@ -90,6 +90,12 @@ fn main() { } example_app.ui(&mut region); emigui.ui(&mut region); + + // TODO: Make it simpler to show a window + Window::new("Test window").show(region.data().clone(), |region| { + region.add(label!("Grab the window and move it around!")); + }); + let mesh = emigui.paint(); painter.paint(&display, mesh, emigui.texture()); }