diff --git a/docs/TODO_ux.md b/docs/TODO_ux.md index 3c4588eb41..bff2e96ab6 100644 --- a/docs/TODO_ux.md +++ b/docs/TODO_ux.md @@ -67,3 +67,5 @@ - switch ezgui (could make it generic and have piston or glium support, but maybe not worth it) - render text +- change ezgui API to allow uploading geometry once +- undo the y inversion hacks at last! diff --git a/ezgui/Cargo.toml b/ezgui/Cargo.toml index c1fa7c8bd1..4f22c41e97 100644 --- a/ezgui/Cargo.toml +++ b/ezgui/Cargo.toml @@ -8,6 +8,8 @@ edition = "2018" abstutil = { path = "../abstutil" } dimensioned = { git = "https://github.com/paholg/dimensioned", rev = "0e1076ebfa5128d1ee544bdc9754c948987b6fe3", features = ["serde"] } geom = { path = "../geom" } +glium = "0.23.0" +glutin = "0.19.0" log = "0.4.5" ordered-float = "1.0.1" palette = "0.4" diff --git a/ezgui/src/camera.rs b/ezgui/src/camera.rs new file mode 100644 index 0000000000..06000f405a --- /dev/null +++ b/ezgui/src/camera.rs @@ -0,0 +1,171 @@ +use glutin; +use std::f32; + +pub struct CameraState { + aspect_ratio: f32, + position: (f32, f32, f32), + direction: (f32, f32, f32), + + moving_up: bool, + moving_left: bool, + moving_down: bool, + moving_right: bool, + moving_forward: bool, + moving_backward: bool, +} + +impl CameraState { + pub fn new() -> CameraState { + CameraState { + aspect_ratio: 1024.0 / 768.0, + position: (0.1, 0.1, 1.0), + direction: (0.0, 0.0, -1.0), + moving_up: false, + moving_left: false, + moving_down: false, + moving_right: false, + moving_forward: false, + moving_backward: false, + } + } + + pub fn get_perspective(&self) -> [[f32; 4]; 4] { + let fov = f32::consts::PI / 2.0; + let zfar = 1024.0; + let znear = 0.1; + + let f = 1.0 / (fov / 2.0).tan(); + + // note: remember that this is column-major, so the lines of code are actually columns + [ + [f / self.aspect_ratio, 0.0, 0.0, 0.0], + [0.0, f, 0.0, 0.0], + [0.0, 0.0, (zfar + znear) / (zfar - znear), 1.0], + [0.0, 0.0, -(2.0 * zfar * znear) / (zfar - znear), 0.0], + ] + } + + pub fn get_view(&self) -> [[f32; 4]; 4] { + let f = { + let f = self.direction; + let len = f.0 * f.0 + f.1 * f.1 + f.2 * f.2; + let len = len.sqrt(); + (f.0 / len, f.1 / len, f.2 / len) + }; + + let up = (0.0, 1.0, 0.0); + + let s = ( + f.1 * up.2 - f.2 * up.1, + f.2 * up.0 - f.0 * up.2, + f.0 * up.1 - f.1 * up.0, + ); + + let s_norm = { + let len = s.0 * s.0 + s.1 * s.1 + s.2 * s.2; + let len = len.sqrt(); + (s.0 / len, s.1 / len, s.2 / len) + }; + + let u = ( + s_norm.1 * f.2 - s_norm.2 * f.1, + s_norm.2 * f.0 - s_norm.0 * f.2, + s_norm.0 * f.1 - s_norm.1 * f.0, + ); + + let p = ( + -self.position.0 * s.0 - self.position.1 * s.1 - self.position.2 * s.2, + -self.position.0 * u.0 - self.position.1 * u.1 - self.position.2 * u.2, + -self.position.0 * f.0 - self.position.1 * f.1 - self.position.2 * f.2, + ); + + // note: remember that this is column-major, so the lines of code are actually columns + [ + [s_norm.0, u.0, f.0, 0.0], + [s_norm.1, u.1, f.1, 0.0], + [s_norm.2, u.2, f.2, 0.0], + [p.0, p.1, p.2, 1.0], + ] + } + + fn update(&mut self) { + let f = { + let f = self.direction; + let len = f.0 * f.0 + f.1 * f.1 + f.2 * f.2; + let len = len.sqrt(); + (f.0 / len, f.1 / len, f.2 / len) + }; + + let up = (0.0, 1.0, 0.0); + + let s = ( + f.1 * up.2 - f.2 * up.1, + f.2 * up.0 - f.0 * up.2, + f.0 * up.1 - f.1 * up.0, + ); + + let s = { + let len = s.0 * s.0 + s.1 * s.1 + s.2 * s.2; + let len = len.sqrt(); + (s.0 / len, s.1 / len, s.2 / len) + }; + + let u = ( + s.1 * f.2 - s.2 * f.1, + s.2 * f.0 - s.0 * f.2, + s.0 * f.1 - s.1 * f.0, + ); + + let speed = 0.1; + + if self.moving_up { + self.position.0 += u.0 * speed; + self.position.1 += u.1 * speed; + self.position.2 += u.2 * speed; + } + + if self.moving_left { + self.position.0 -= s.0 * speed; + self.position.1 -= s.1 * speed; + self.position.2 -= s.2 * speed; + } + + if self.moving_down { + self.position.0 -= u.0 * speed; + self.position.1 -= u.1 * speed; + self.position.2 -= u.2 * speed; + } + + if self.moving_right { + self.position.0 += s.0 * speed; + self.position.1 += s.1 * speed; + self.position.2 += s.2 * speed; + } + + if self.moving_forward { + self.position.0 += f.0 * speed; + self.position.1 += f.1 * speed; + self.position.2 += f.2 * speed; + } + + if self.moving_backward { + self.position.0 -= f.0 * speed; + self.position.1 -= f.1 * speed; + self.position.2 -= f.2 * speed; + } + } + + pub fn process_input(&mut self, input: glutin::KeyboardInput) { + let pressed = input.state == glutin::ElementState::Pressed; + match input.virtual_keycode { + Some(glutin::VirtualKeyCode::Up) => self.moving_up = pressed, + Some(glutin::VirtualKeyCode::Down) => self.moving_down = pressed, + Some(glutin::VirtualKeyCode::Left) => self.moving_left = pressed, + Some(glutin::VirtualKeyCode::Right) => self.moving_right = pressed, + Some(glutin::VirtualKeyCode::Q) => self.moving_forward = pressed, + Some(glutin::VirtualKeyCode::A) => self.moving_backward = pressed, + _ => {} + }; + self.update(); + } +} diff --git a/ezgui/src/event.rs b/ezgui/src/event.rs index 63ea8162b2..59dedc0ca7 100644 --- a/ezgui/src/event.rs +++ b/ezgui/src/event.rs @@ -1,4 +1,5 @@ use crate::ScreenPt; +use glium::glutin; use piston::input as pi; #[derive(Clone, Copy, PartialEq)] @@ -24,6 +25,50 @@ pub enum Event { } impl Event { + pub fn from_glutin_event(ev: glutin::WindowEvent) -> Option { + match ev { + glutin::WindowEvent::MouseInput { state, button, .. } => match (button, state) { + (glutin::MouseButton::Left, glutin::ElementState::Pressed) => { + Some(Event::LeftMouseButtonDown) + } + (glutin::MouseButton::Left, glutin::ElementState::Released) => { + Some(Event::LeftMouseButtonUp) + } + (glutin::MouseButton::Right, glutin::ElementState::Pressed) => { + Some(Event::RightMouseButtonDown) + } + (glutin::MouseButton::Right, glutin::ElementState::Released) => { + Some(Event::RightMouseButtonUp) + } + _ => None, + }, + glutin::WindowEvent::KeyboardInput { input, .. } => { + if let Some(key) = Key::from_glutin_key(input) { + if input.state == glutin::ElementState::Pressed { + Some(Event::KeyPress(key)) + } else { + Some(Event::KeyRelease(key)) + } + } else { + None + } + } + glutin::WindowEvent::CursorMoved { position, .. } => { + Some(Event::MouseMovedTo(ScreenPt::new(position.x, position.y))) + } + //glutin::WindowEvent::MouseWheel { delta, .. } => Event::MouseWheelScroll(), + glutin::WindowEvent::Resized(size) => { + Some(Event::WindowResized(size.width, size.height)) + } + glutin::WindowEvent::Focused(gained) => Some(if gained { + Event::WindowGainedCursor + } else { + Event::WindowLostCursor + }), + _ => None, + } + } + pub fn from_piston_event(ev: pi::Event) -> Event { use piston::input::{ ButtonEvent, CursorEvent, MouseCursorEvent, MouseScrollEvent, PressEvent, ReleaseEvent, @@ -348,4 +393,79 @@ impl Key { } }) } + + fn from_glutin_key(input: glutin::KeyboardInput) -> Option { + let key = input.virtual_keycode?; + Some(match key { + glutin::VirtualKeyCode::A => Key::A, + glutin::VirtualKeyCode::B => Key::B, + glutin::VirtualKeyCode::C => Key::C, + glutin::VirtualKeyCode::D => Key::D, + glutin::VirtualKeyCode::E => Key::E, + glutin::VirtualKeyCode::F => Key::F, + glutin::VirtualKeyCode::G => Key::G, + glutin::VirtualKeyCode::H => Key::H, + glutin::VirtualKeyCode::I => Key::I, + glutin::VirtualKeyCode::J => Key::J, + glutin::VirtualKeyCode::K => Key::K, + glutin::VirtualKeyCode::L => Key::L, + glutin::VirtualKeyCode::M => Key::M, + glutin::VirtualKeyCode::N => Key::N, + glutin::VirtualKeyCode::O => Key::O, + glutin::VirtualKeyCode::P => Key::P, + glutin::VirtualKeyCode::Q => Key::Q, + glutin::VirtualKeyCode::R => Key::R, + glutin::VirtualKeyCode::S => Key::S, + glutin::VirtualKeyCode::T => Key::T, + glutin::VirtualKeyCode::U => Key::U, + glutin::VirtualKeyCode::V => Key::V, + glutin::VirtualKeyCode::W => Key::W, + glutin::VirtualKeyCode::X => Key::X, + glutin::VirtualKeyCode::Y => Key::Y, + glutin::VirtualKeyCode::Z => Key::Z, + glutin::VirtualKeyCode::Key1 => Key::Num1, + glutin::VirtualKeyCode::Key2 => Key::Num2, + glutin::VirtualKeyCode::Key3 => Key::Num3, + glutin::VirtualKeyCode::Key4 => Key::Num4, + glutin::VirtualKeyCode::Key5 => Key::Num5, + glutin::VirtualKeyCode::Key6 => Key::Num6, + glutin::VirtualKeyCode::Key7 => Key::Num7, + glutin::VirtualKeyCode::Key8 => Key::Num8, + glutin::VirtualKeyCode::Key9 => Key::Num9, + glutin::VirtualKeyCode::Key0 => Key::Num0, + glutin::VirtualKeyCode::LBracket => Key::LeftBracket, + glutin::VirtualKeyCode::RBracket => Key::RightBracket, + glutin::VirtualKeyCode::Space => Key::Space, + glutin::VirtualKeyCode::Slash => Key::Slash, + glutin::VirtualKeyCode::Period => Key::Dot, + glutin::VirtualKeyCode::Comma => Key::Comma, + glutin::VirtualKeyCode::Escape => Key::Escape, + glutin::VirtualKeyCode::Return => Key::Enter, + glutin::VirtualKeyCode::Tab => Key::Tab, + glutin::VirtualKeyCode::Back => Key::Backspace, + glutin::VirtualKeyCode::LShift => Key::LeftShift, + glutin::VirtualKeyCode::LControl => Key::LeftControl, + glutin::VirtualKeyCode::LAlt => Key::LeftAlt, + glutin::VirtualKeyCode::Left => Key::LeftArrow, + glutin::VirtualKeyCode::Right => Key::RightArrow, + glutin::VirtualKeyCode::Up => Key::UpArrow, + glutin::VirtualKeyCode::Down => Key::DownArrow, + glutin::VirtualKeyCode::F1 => Key::F1, + glutin::VirtualKeyCode::F2 => Key::F2, + glutin::VirtualKeyCode::F3 => Key::F3, + glutin::VirtualKeyCode::F4 => Key::F4, + glutin::VirtualKeyCode::F5 => Key::F5, + glutin::VirtualKeyCode::F6 => Key::F6, + glutin::VirtualKeyCode::F7 => Key::F7, + glutin::VirtualKeyCode::F8 => Key::F8, + glutin::VirtualKeyCode::F9 => Key::F9, + glutin::VirtualKeyCode::F10 => Key::F10, + glutin::VirtualKeyCode::F11 => Key::F11, + glutin::VirtualKeyCode::F12 => Key::F12, + _ => { + println!("Unknown glutin key {:?}", key); + return None; + } + }) + } } diff --git a/ezgui/src/fragment.glsl b/ezgui/src/fragment.glsl new file mode 100644 index 0000000000..a9d9f42bb3 --- /dev/null +++ b/ezgui/src/fragment.glsl @@ -0,0 +1,8 @@ +#version 140 + +in vec4 pass_color; +out vec4 f_color; + +void main() { + f_color = pass_color; +} diff --git a/ezgui/src/lib.rs b/ezgui/src/lib.rs index 1512332623..2f3b7b5d1d 100644 --- a/ezgui/src/lib.rs +++ b/ezgui/src/lib.rs @@ -1,3 +1,4 @@ +mod camera; mod canvas; mod color; mod event; @@ -12,6 +13,7 @@ mod text_box; mod top_menu; mod wizard; +use crate::camera::CameraState; pub use crate::canvas::{Canvas, HorizontalAlignment, VerticalAlignment, BOTTOM_LEFT, CENTERED}; pub use crate::color::Color; pub use crate::event::{Event, Key}; @@ -25,6 +27,7 @@ pub use crate::text_box::TextBox; pub use crate::top_menu::{Folder, TopMenu}; pub use crate::wizard::{Wizard, WrappedWizard}; use geom::Pt2D; +use glium::{implement_vertex, uniform, Surface}; use graphics::Transformed; use opengl_graphics::GlGraphics; use std::mem; @@ -212,3 +215,189 @@ pub enum InputResult { StillActive, Done(String, T), } + +#[derive(Copy, Clone)] +struct Vertex { + position: [f32; 2], + // TODO Maybe pass color as a uniform instead + color: [f32; 4], +} + +implement_vertex!(Vertex, position, color); + +type Uniforms<'a> = glium::uniforms::UniformsStorage< + 'a, + [[f32; 4]; 4], + glium::uniforms::UniformsStorage<'a, [[f32; 4]; 4], glium::uniforms::EmptyUniforms>, +>; + +pub struct NewGfxCtx<'a> { + display: &'a glium::Display, + target: &'a mut glium::Frame, + program: &'a glium::Program, + uniforms: Uniforms<'a>, + params: glium::DrawParameters<'a>, +} + +impl<'a> NewGfxCtx<'a> { + pub fn new( + display: &'a glium::Display, + target: &'a mut glium::Frame, + program: &'a glium::Program, + ) -> NewGfxCtx<'a> { + let params = glium::DrawParameters { + depth: glium::Depth { + test: glium::DepthTest::IfLess, + write: true, + ..Default::default() + }, + ..Default::default() + }; + + let mut camera = CameraState::new(); + // TODO setup camera based on canvas + let uniforms = uniform! { + persp_matrix: camera.get_perspective(), + view_matrix: camera.get_view(), + }; + + NewGfxCtx { + display, + target, + program, + uniforms, + params, + } + } + + // Up to the caller to call unfork()! + // TODO Canvas doesn't understand this change, so things like text drawing that use + // map_to_screen will just be confusing. + pub fn fork(&mut self, top_left: Pt2D, zoom: f64) -> Uniforms { + let mut camera = CameraState::new(); + // TODO setup camera based on values above + let mut uniforms = uniform! { + persp_matrix: camera.get_perspective(), + view_matrix: camera.get_view(), + }; + + mem::swap(&mut self.uniforms, &mut uniforms); + uniforms + } + + pub fn fork_screenspace(&mut self) -> Uniforms { + self.fork(Pt2D::new(0.0, 0.0), 1.0) + } + + pub fn unfork(&mut self, old_uniforms: Uniforms<'a>) { + // TODO What do we need to do to re-upload? + self.uniforms = old_uniforms; + } + + pub fn clear(&mut self, color: Color) { + self.target + .clear_color_and_depth((color.0[0], color.0[1], color.0[2], color.0[3]), 1.0); + } + + // Use graphics::Line internally for now, but make it easy to switch to something else by + // picking this API now. + pub fn draw_line(&mut self, color: Color, thickness: f64, line: &geom::Line) { + self.draw_polygon(color, &line.to_polyline().make_polygons(thickness)); + } + + pub fn draw_rounded_line(&mut self, color: Color, thickness: f64, line: &geom::Line) { + self.draw_line(color, thickness, line); + self.draw_circle(color, &geom::Circle::new(line.pt1(), thickness / 2.0)); + self.draw_circle(color, &geom::Circle::new(line.pt2(), thickness / 2.0)); + } + + pub fn draw_arrow(&mut self, color: Color, thickness: f64, line: &geom::Line) { + // TODO Raw method doesn't work yet in all cases... + /*graphics::Line::new_round(color.0, thickness).draw_arrow( + [ + line.pt1().x(), + line.pt1().y(), + line.pt2().x(), + line.pt2().y(), + ], + 2.0 * thickness, + &self.ctx.draw_state, + self.ctx.transform, + self.gfx, + );*/ + + /*use dimensioned::si; + let head_size = 2.0 * thickness; + let angle = line.angle(); + let triangle_height = (head_size / 2.0).sqrt() * si::M; + self.draw_polygon( + color, + &geom::Polygon::new(&vec![ + //line.pt2(), + //line.pt2().project_away(head_size, angle.rotate_degs(-135.0)), + line.reverse() + .dist_along(triangle_height) + .project_away(thickness / 2.0, angle.rotate_degs(90.0)), + line.pt1() + .project_away(thickness / 2.0, angle.rotate_degs(90.0)), + line.pt1() + .project_away(thickness / 2.0, angle.rotate_degs(-90.0)), + line.reverse() + .dist_along(triangle_height) + .project_away(thickness / 2.0, angle.rotate_degs(-90.0)), + //line.pt2().project_away(head_size, angle.rotate_degs(135.0)), + ]), + ); + self.draw_polygon( + color, + &geom::Polygon::new(&vec![ + line.pt2(), + line.pt2() + .project_away(head_size, angle.rotate_degs(-135.0)), + line.pt2().project_away(head_size, angle.rotate_degs(135.0)), + ]), + );*/ + } + + pub fn draw_polygon(&mut self, color: Color, poly: &geom::Polygon) { + for tri in &poly.triangles { + let vb = glium::VertexBuffer::new( + self.display, + &[ + Vertex { + position: [tri.pt1.x() as f32, tri.pt1.y() as f32], + color: color.0, + }, + Vertex { + position: [tri.pt2.x() as f32, tri.pt2.y() as f32], + color: color.0, + }, + Vertex { + position: [tri.pt3.x() as f32, tri.pt3.y() as f32], + color: color.0, + }, + ], + ) + .unwrap(); + let indices = glium::index::NoIndices(glium::index::PrimitiveType::TrianglesList); + + self.target + .draw(&vb, &indices, &self.program, &self.uniforms, &self.params) + .unwrap(); + } + } + + pub fn draw_circle(&mut self, color: Color, circle: &geom::Circle) { + /*graphics::Ellipse::new(color.0).draw( + [ + circle.center.x() - circle.radius, + circle.center.y() - circle.radius, + 2.0 * circle.radius, + 2.0 * circle.radius, + ], + &self.ctx.draw_state, + self.ctx.transform, + self.gfx, + );*/ + } +} diff --git a/ezgui/src/runner.rs b/ezgui/src/runner.rs index c2d5153c5c..b1f2c15e82 100644 --- a/ezgui/src/runner.rs +++ b/ezgui/src/runner.rs @@ -1,12 +1,14 @@ use crate::input::{ContextMenu, ModalMenuState}; -use crate::{Canvas, Event, GfxCtx, ModalMenu, TopMenu, UserInput}; +use crate::{Canvas, Event, GfxCtx, ModalMenu, NewGfxCtx, TopMenu, UserInput}; use abstutil::Timer; +use glium::glutin; use glutin_window::GlutinWindow; use opengl_graphics::{GlGraphics, OpenGL}; use piston::event_loop::{EventLoop, EventSettings, Events}; use piston::window::WindowSettings; use std::io::Write; -use std::{env, fs, panic, process}; +use std::time::{Duration, Instant}; +use std::{env, fs, panic, process, thread}; pub trait GUI { // Called once @@ -172,6 +174,13 @@ impl> State { self } + fn new_draw(&mut self, display: &glium::Display, program: &glium::Program) { + let mut target = display.draw(); + // TODO call draw + NewGfxCtx::new(&display, &mut target, program); + target.finish().unwrap(); + } + fn draw(&mut self, g: &mut GfxCtx) { // If the very first event is render, then just wait. if let Some(ref data) = self.last_data { @@ -309,3 +318,67 @@ impl ScreenCaptureState { writeln!(file, "rm -f combine.sh").unwrap(); } } + +pub fn new_run>(mut gui: G, window_title: &str) { + // DPI is broken on my system; force the old behavior. + env::set_var("WINIT_HIDPI_FACTOR", "1.0"); + + let mut events_loop = glutin::EventsLoop::new(); + let window = glutin::WindowBuilder::new() + .with_title(window_title) + .with_dimensions(glutin::dpi::LogicalSize::new( + gui.get_mut_canvas().window_width, + gui.get_mut_canvas().window_height, + )); + let context = glutin::ContextBuilder::new().with_depth_buffer(24); + let display = glium::Display::new(window, context, &events_loop).unwrap(); + let program = glium::Program::from_source( + &display, + include_str!("vertex.glsl"), + include_str!("fragment.glsl"), + None, + ) + .unwrap(); + + let mut state = State { + last_event_mode: EventLoopMode::InputOnly, + context_menu: ContextMenu::Inactive, + top_menu: gui.top_menu(), + modal_state: ModalMenuState::new(G::modal_menus()), + last_data: None, + screen_cap: None, + gui, + }; + + let mut accumulator = Duration::new(0, 0); + let mut previous_clock = Instant::now(); + loop { + state.new_draw(&display, &program); + state.after_render(); + + events_loop.poll_events(|event| { + if let glutin::Event::WindowEvent { event, .. } = event { + if event == glutin::WindowEvent::CloseRequested { + state.gui.before_quit(); + process::exit(0); + } + if state.screen_cap.is_none() { + // TODO manage laziness differently + //state = state.event(event, &mut events); + } + } + }); + + let now = Instant::now(); + accumulator += now - previous_clock; + previous_clock = now; + + let fixed_time_stamp = Duration::new(0, 16_666_667); + while accumulator >= fixed_time_stamp { + accumulator -= fixed_time_stamp; + // TODO send off an update event + } + + thread::sleep(fixed_time_stamp - accumulator); + } +} diff --git a/ezgui/src/vertex.glsl b/ezgui/src/vertex.glsl new file mode 100644 index 0000000000..d875663658 --- /dev/null +++ b/ezgui/src/vertex.glsl @@ -0,0 +1,15 @@ +#version 140 + +uniform mat4 persp_matrix; +uniform mat4 view_matrix; + +in vec2 position; +in vec4 color; +out vec4 pass_color; + +void main() { + pass_color = color; + + gl_Position = vec4(position, 0.0, 1.0); + gl_Position = persp_matrix * view_matrix * vec4(position, 0.0, 1.0); +}