From bb327771019b57f9d73936ba307eee1c76c79919 Mon Sep 17 00:00:00 2001 From: Patrick Walton Date: Wed, 13 Mar 2019 16:32:39 -0700 Subject: [PATCH] Initial work toward VR support --- Cargo.lock | 13 +- .../pathfinderdemo/PathfinderActivity.java | 28 +- .../PathfinderDemoRenderer.java | 3 + .../PathfinderDemoSurfaceView.java | 2 + demo/android/rust/Cargo.toml | 1 + demo/android/rust/src/lib.rs | 37 ++- demo/common/src/lib.rs | 256 ++++++++++++------ demo/common/src/ui.rs | 52 ++-- demo/common/src/window.rs | 4 +- demo/native/src/main.rs | 7 +- gl/src/lib.rs | 8 +- gpu/src/lib.rs | 3 +- renderer/src/gpu/renderer.rs | 33 ++- renderer/src/gpu_data.rs | 13 + ui/src/lib.rs | 161 +++++++---- 15 files changed, 432 insertions(+), 189 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 62ba61c8..d71e7e72 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -186,7 +186,7 @@ dependencies = [ "arrayvec 0.4.10 (registry+https://github.com/rust-lang/crates.io-index)", "cfg-if 0.1.6 (registry+https://github.com/rust-lang/crates.io-index)", "crossbeam-utils 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "memoffset 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)", "nodrop 0.1.13 (registry+https://github.com/rust-lang/crates.io-index)", "scopeguard 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -473,7 +473,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] name = "lazy_static" -version = "1.2.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" [[package]] @@ -633,6 +633,7 @@ dependencies = [ "egl 0.2.7 (registry+https://github.com/rust-lang/crates.io-index)", "gl 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", "jni 0.11.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "pathfinder_demo 0.1.0", "pathfinder_geometry 0.3.0", "pathfinder_gl 0.1.0", @@ -939,7 +940,7 @@ version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "crossbeam-deque 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", "num_cpus 1.10.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -1073,7 +1074,7 @@ version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "bitflags 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", "libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)", "num 0.1.42 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)", @@ -1224,7 +1225,7 @@ name = "thread_local" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ - "lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)", + "lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] @@ -1412,7 +1413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" "checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" "checksum khronos 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c0711aaa80e6ba6eb1fa8978f1f46bfcb38ceb2f3f33f3736efbff39dac89f50" "checksum khronos_api 2.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "037ab472c33f67b5fbd3e9163a2645319e5356fcd355efa6d4eb7fff4bbcb554" -"checksum lazy_static 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "a374c89b9db55895453a74c1e38861d9deec0b01b405a82516e9d5de4820dea1" +"checksum lazy_static 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)" = "bc5729f27f159ddd61f4df6228e827e86643d4d3e7c32183cb30a1c08f604a14" "checksum libc 0.1.12 (registry+https://github.com/rust-lang/crates.io-index)" = "e32a70cf75e5846d53a673923498228bbec6a8624708a9ea5645f075d6276122" "checksum libc 0.2.48 (registry+https://github.com/rust-lang/crates.io-index)" = "e962c7641008ac010fa60a7dfdc1712449f29c44ef2d4702394aea943ee75047" "checksum libflate 0.1.20 (registry+https://github.com/rust-lang/crates.io-index)" = "54d1ddf9c52870243c5689d7638d888331c1116aa5b398f3ba1acfa7d8758ca1" diff --git a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderActivity.java b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderActivity.java index e07a3575..69450ce4 100644 --- a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderActivity.java +++ b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderActivity.java @@ -10,6 +10,7 @@ import android.support.v7.app.ActionBar; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.view.MotionEvent; import android.view.View; @@ -18,6 +19,8 @@ import android.view.View; * status bar and navigation/system bar) with user interaction. */ public class PathfinderActivity extends AppCompatActivity { + private PathfinderDemoRenderer mRenderer; + /** * Whether or not the system UI should be auto-hidden after * {@link #AUTO_HIDE_DELAY_MILLIS} milliseconds. @@ -111,6 +114,7 @@ public class PathfinderActivity extends AppCompatActivity { init(); } + @SuppressLint("ClickableViewAccessibility") private void init() { setContentView(R.layout.activity_pathfinder); @@ -118,6 +122,7 @@ public class PathfinderActivity extends AppCompatActivity { mControlsView = findViewById(R.id.fullscreen_content_controls); mContentView = findViewById(R.id.fullscreen_content); + /* // Set up the user interaction to manually show or hide the system UI. mContentView.setOnClickListener(new View.OnClickListener() { @Override @@ -125,9 +130,30 @@ public class PathfinderActivity extends AppCompatActivity { toggle(); } }); + */ mContentView.setEGLContextClientVersion(3); - mContentView.setRenderer(new PathfinderDemoRenderer(getAssets())); + mRenderer = new PathfinderDemoRenderer(getAssets()); + mContentView.setRenderer(mRenderer); + + mContentView.setOnTouchListener(new View.OnTouchListener() { + @Override + public boolean onTouch(View view, MotionEvent event) { + int x = Math.round(event.getX()); + int y = Math.round(event.getY()); + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + Log.i("Pathfinder", "DOWN " + x + " " + y); + PathfinderDemoRenderer.pushMouseDownEvent(x, y); + break; + case MotionEvent.ACTION_MOVE: + Log.i("Pathfinder", "MOVE " + x + " " + y); + PathfinderDemoRenderer.pushMouseDraggedEvent(x, y); + break; + } + return true; + } + }); } @Override diff --git a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoRenderer.java b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoRenderer.java index ed553700..681de831 100644 --- a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoRenderer.java +++ b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoRenderer.java @@ -12,6 +12,9 @@ public class PathfinderDemoRenderer extends Object implements GLSurfaceView.Rend private static native void init(PathfinderDemoResourceLoader resourceLoader); private static native void runOnce(); + public static native void pushMouseDownEvent(int x, int y); + public static native void pushMouseDraggedEvent(int x, int y); + static { System.loadLibrary("pathfinder_android_demo"); } diff --git a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoSurfaceView.java b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoSurfaceView.java index d89e71ef..6c019e05 100644 --- a/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoSurfaceView.java +++ b/demo/android/app/src/main/java/graphics/pathfinder/pathfinderdemo/PathfinderDemoSurfaceView.java @@ -3,6 +3,8 @@ package graphics.pathfinder.pathfinderdemo; import android.content.Context; import android.opengl.GLSurfaceView; import android.util.AttributeSet; +import android.view.MotionEvent; +import android.view.View; public class PathfinderDemoSurfaceView extends GLSurfaceView { public PathfinderDemoSurfaceView(Context context) { diff --git a/demo/android/rust/Cargo.toml b/demo/android/rust/Cargo.toml index d2313bd6..b9d3be63 100644 --- a/demo/android/rust/Cargo.toml +++ b/demo/android/rust/Cargo.toml @@ -11,6 +11,7 @@ crate_type = ["cdylib"] egl = "0.2" gl = "0.6" jni = "0.11" +lazy_static = "1.3" [dependencies.pathfinder_demo] path = "../../common" diff --git a/demo/android/rust/src/lib.rs b/demo/android/rust/src/lib.rs index 5ce0fca0..266e87a7 100644 --- a/demo/android/rust/src/lib.rs +++ b/demo/android/rust/src/lib.rs @@ -8,6 +8,9 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. +#[macro_use] +extern crate lazy_static; + use jni::{JNIEnv, JavaVM}; use jni::objects::{GlobalRef, JByteBuffer, JClass, JObject, JValue}; use pathfinder_demo::DemoApp; @@ -16,9 +19,16 @@ use pathfinder_geometry::basic::point::Point2DI32; use pathfinder_gl::GLVersion; use pathfinder_gpu::resources::ResourceLoader; use std::cell::RefCell; +use std::ffi::CString; use std::io::Error as IOError; +use std::mem; use std::os::raw::c_void; use std::path::PathBuf; +use std::sync::Mutex; + +lazy_static! { + static ref EVENT_QUEUE: Mutex> = Mutex::new(vec![]); +} thread_local! { static DEMO_APP: RefCell>> = RefCell::new(None); @@ -43,18 +53,31 @@ pub unsafe extern "system" fn Java_graphics_pathfinder_pathfinderdemo_PathfinderDemoRenderer_runOnce(env: JNIEnv, class: JClass) { DEMO_APP.with(|demo_app| { + let mut event_queue = EVENT_QUEUE.lock().unwrap(); if let Some(ref mut demo_app) = *demo_app.borrow_mut() { - demo_app.run_once(vec![]); + demo_app.run_once(mem::replace(&mut *event_queue, vec![])); } }); } #[no_mangle] pub unsafe extern "system" fn - Java_graphics_pathfinder_pathfinderdemo_PathfinderDemoRenderer_pushMouseDown(env: JNIEnv, - class: JClass, - x: i32, - y: i32) { + Java_graphics_pathfinder_pathfinderdemo_PathfinderDemoRenderer_pushMouseDownEvent( + env: JNIEnv, + class: JClass, + x: i32, + y: i32) { + EVENT_QUEUE.lock().unwrap().push(Event::MouseDown(Point2DI32::new(x, y))) +} + +#[no_mangle] +pub unsafe extern "system" fn + Java_graphics_pathfinder_pathfinderdemo_PathfinderDemoRenderer_pushMouseDraggedEvent( + env: JNIEnv, + class: JClass, + x: i32, + y: i32) { + EVENT_QUEUE.lock().unwrap().push(Event::MouseDragged(Point2DI32::new(x, y))) } struct WindowImpl; @@ -70,11 +93,11 @@ impl Window for WindowImpl { } fn size(&self) -> Point2DI32 { - Point2DI32::new(1080, 1920) + Point2DI32::new(1920, 1080) } fn drawable_size(&self) -> Point2DI32 { - Point2DI32::new(1080, 1920) + Point2DI32::new(1920, 1080) } fn mouse_position(&self) -> Point2DI32 { diff --git a/demo/common/src/lib.rs b/demo/common/src/lib.rs index 6f121bb9..b14b9d92 100644 --- a/demo/common/src/lib.rs +++ b/demo/common/src/lib.rs @@ -17,7 +17,7 @@ use clap::{App, Arg}; use image::ColorType; use jemallocator; use pathfinder_geometry::basic::point::{Point2DF32, Point2DI32, Point3DF32}; -use pathfinder_geometry::basic::rect::RectF32; +use pathfinder_geometry::basic::rect::{RectF32, RectI32}; use pathfinder_geometry::basic::transform2d::Transform2DF32; use pathfinder_geometry::basic::transform3d::{Perspective, Transform3DF32}; use pathfinder_geometry::color::ColorU; @@ -27,17 +27,18 @@ use pathfinder_gpu::{DepthFunc, DepthState, Device, Primitive, RenderState, Sten use pathfinder_gpu::{StencilState, UniformData}; use pathfinder_renderer::builder::{RenderOptions, RenderTransform, SceneBuilder}; use pathfinder_renderer::gpu::renderer::Renderer; -use pathfinder_renderer::gpu_data::BuiltScene; +use pathfinder_renderer::gpu_data::{BuiltScene, Stats}; use pathfinder_renderer::post::{DEFRINGING_KERNEL_CORE_GRAPHICS, STEM_DARKENING_FACTORS}; use pathfinder_renderer::scene::Scene; use pathfinder_renderer::z_buffer::ZBuffer; use pathfinder_svg::BuiltSVG; -use pathfinder_ui::UIEvent; +use pathfinder_ui::{MousePosition, UIEvent}; use rayon::ThreadPoolBuilder; use std::f32::consts::FRAC_PI_4; use std::ffi::CString; use std::fs::File; use std::io::Read; +use std::iter; use std::mem; use std::panic; use std::path::{Path, PathBuf}; @@ -98,6 +99,7 @@ pub struct DemoApp where W: Window { dirty: bool, expire_message_event_id: u32, message_epoch: u32, + last_mouse_position: Point2DI32, ui: DemoUI, scene_thread_proxy: SceneThreadProxy, @@ -120,20 +122,24 @@ impl DemoApp where W: Window { let options = Options::get(resources); let (window_size, drawable_size) = (window.size(), window.drawable_size()); + let view_box_size = view_box_size(options.mode, &window); let built_svg = load_scene(resources, &options.input_path); let message = get_svg_building_message(&built_svg); let scene_view_box = built_svg.scene.view_box; let scene_is_monochrome = built_svg.scene.is_monochrome(); - let renderer = Renderer::new(device, resources, drawable_size); + let renderer = Renderer::new(device, + resources, + RectI32::new(Point2DI32::default(), view_box_size), + drawable_size); let scene_thread_proxy = SceneThreadProxy::new(built_svg.scene, options.clone()); - scene_thread_proxy.set_drawable_size(window.drawable_size()); + scene_thread_proxy.set_drawable_size(view_box_size); - let camera = if options.three_d { - Camera::new_3d(scene_view_box) + let camera = if options.mode == Mode::TwoD { + Camera::new_2d(scene_view_box, view_box_size) } else { - Camera::new_2d(scene_view_box, drawable_size) + Camera::new_3d(scene_view_box) }; let ground_program = GroundProgram::new(&renderer.device, resources); @@ -164,6 +170,7 @@ impl DemoApp where W: Window { dirty: true, expire_message_event_id, message_epoch, + last_mouse_position: Point2DI32::default(), ui, scene_thread_proxy, @@ -180,31 +187,35 @@ impl DemoApp where W: Window { self.build_scene(); // Handle events. - let ui_event = self.handle_events(events); + let ui_events = self.handle_events(events); // Draw the scene. let render_msg = self.scene_thread_proxy.receiver.recv().unwrap(); - self.draw_scene(render_msg, ui_event); + self.draw_scene(render_msg, ui_events); } fn build_scene(&mut self) { - let drawable_size = self.window.drawable_size(); + let view_box_size = view_box_size(self.ui.mode, &self.window); let render_transform = match self.camera { Camera::ThreeD { ref mut transform, ref mut velocity } => { if transform.offset(*velocity) { self.dirty = true; } - let perspective = transform.to_perspective(drawable_size); + let perspective = transform.to_perspective(view_box_size); RenderTransform::Perspective(perspective) } Camera::TwoD(transform) => RenderTransform::Transform2D(transform), }; - let count = if self.frame_counter == 0 { 2 } else { 1 }; - for _ in 0..count { + let is_first_frame = self.frame_counter == 0; + let frame_count = if is_first_frame { 2 } else { 1 }; + for _ in 0..frame_count { + let viewport_count = self.ui.mode.viewport_count(); + let render_transforms = iter::repeat(render_transform.clone()).take(viewport_count) + .collect(); self.scene_thread_proxy.sender.send(MainToSceneMsg::Build(BuildOptions { - render_transform: render_transform.clone(), + render_transforms, stem_darkening_font_size: if self.ui.stem_darkening_effect_enabled { Some(APPROX_FONT_SIZE * self.scale_factor) } else { @@ -213,13 +224,13 @@ impl DemoApp where W: Window { })).unwrap(); } - if count == 2 { + if is_first_frame { self.dirty = true; } } - fn handle_events(&mut self, events: Vec) -> UIEvent { - let mut ui_event = UIEvent::None; + fn handle_events(&mut self, events: Vec) -> Vec { + let mut ui_events = vec![]; self.dirty = false; for event in events { @@ -230,25 +241,29 @@ impl DemoApp where W: Window { self.dirty = true; } Event::WindowResized => { - self.scene_thread_proxy.set_drawable_size(self.window.drawable_size()); + let view_box_size = view_box_size(self.ui.mode, &self.window); + self.scene_thread_proxy.set_drawable_size(view_box_size); self.renderer.set_main_framebuffer_size(self.window.drawable_size()); self.dirty = true; } - Event::MouseDown(position) => { - ui_event = UIEvent::MouseDown(position.scale(self.scale_factor as i32)); + Event::MouseDown(new_position) => { + let mouse_position = self.process_mouse_position(new_position); + ui_events.push(UIEvent::MouseDown(mouse_position)); } - Event::MouseMoved { relative_position, .. } if self.mouselook_enabled => { + Event::MouseMoved(new_position) if self.mouselook_enabled => { + let mouse_position = self.process_mouse_position(new_position); if let Camera::ThreeD { ref mut transform, .. } = self.camera { - let rotation = relative_position.to_f32().scale(MOUSELOOK_ROTATION_SPEED); + let rotation = mouse_position.relative + .to_f32() + .scale(MOUSELOOK_ROTATION_SPEED); transform.yaw += rotation.x(); transform.pitch += rotation.y(); self.dirty = true; } } - Event::MouseDragged { position, relative_position } => { - let absolute_position = position.scale(self.scale_factor as i32); - let relative_position = relative_position.scale(self.scale_factor as i32); - ui_event = UIEvent::MouseDragged { absolute_position, relative_position }; + Event::MouseDragged(new_position) => { + let mouse_position = self.process_mouse_position(new_position); + ui_events.push(UIEvent::MouseDragged(mouse_position)); self.dirty = true; } Event::Zoom(d_dist) => { @@ -312,34 +327,58 @@ impl DemoApp where W: Window { } } - ui_event + ui_events } - fn draw_scene(&mut self, render_msg: SceneToMainMsg, mut ui_event: UIEvent) { - let SceneToMainMsg::Render { - built_scene, - transform: render_transform, - tile_time, - } = render_msg; + fn process_mouse_position(&mut self, new_position: Point2DI32) -> MousePosition { + let absolute = new_position.scale(self.scale_factor as i32); + let relative = absolute - self.last_mouse_position; + self.last_mouse_position = absolute; + MousePosition { absolute, relative } + } + fn draw_scene(&mut self, render_msg: SceneToMainMsg, mut ui_events: Vec) { self.renderer.device.clear(Some(self.background_color().to_f32().0), Some(1.0), Some(0)); - self.draw_environment(&render_transform); - self.render_vector_scene(&built_scene); + + let SceneToMainMsg::Render { render_scenes, tile_time } = render_msg; + let mut render_stats = None; + + for (viewport_index, render_scene) in render_scenes.iter().enumerate() { + self.draw_environment(viewport_index, &render_scene.transform); + self.render_vector_scene(viewport_index, &render_scene.built_scene); + + match render_stats { + None => { + render_stats = Some(RenderStats { + rendering_time: self.renderer.shift_timer_query(), + stats: render_scene.built_scene.stats(), + }) + } + Some(ref mut render_stats) => { + render_stats.stats = render_stats.stats + render_scene.built_scene.stats() + } + } + } + + let drawable_size = self.window.drawable_size(); + self.renderer.set_viewport(RectI32::new(Point2DI32::default(), drawable_size)); if self.pending_screenshot_path.is_some() { self.take_screenshot(); } - let rendering_time = self.renderer.shift_timer_query(); - let stats = built_scene.stats(); - self.renderer.debug_ui.add_sample(stats, tile_time, rendering_time); - self.renderer.debug_ui.draw(&self.renderer.device); - - if !ui_event.is_none() { - self.dirty = true; + if let Some(render_stats) = render_stats { + self.renderer.debug_ui.add_sample(render_stats.stats, + tile_time, + render_stats.rendering_time); + self.renderer.draw_debug_ui(); + } + + for ui_event in &ui_events { + self.dirty = true; + self.renderer.debug_ui.ui.event_queue.push(*ui_event); } - self.renderer.debug_ui.ui.event = ui_event; self.renderer.debug_ui.ui.mouse_position = get_mouse_position(&self.window, self.scale_factor); self.ui.show_text_effects = self.scene_is_monochrome; @@ -350,39 +389,43 @@ impl DemoApp where W: Window { &mut self.renderer.debug_ui, &mut ui_action); - ui_event = mem::replace(&mut self.renderer.debug_ui.ui.event, UIEvent::None); + ui_events = self.renderer.debug_ui.ui.event_queue.drain(); self.handle_ui_action(&mut ui_action); // Switch camera mode (2D/3D) if requested. // // FIXME(pcwalton): This mess should really be an MVC setup. - match (&self.camera, self.ui.three_d_enabled) { - (&Camera::TwoD { .. }, true) => self.camera = Camera::new_3d(self.scene_view_box), - (&Camera::ThreeD { .. }, false) => { + match (&self.camera, self.ui.mode) { + (&Camera::TwoD { .. }, Mode::ThreeD) | (&Camera::TwoD { .. }, Mode::VR) => { + self.camera = Camera::new_3d(self.scene_view_box); + } + (&Camera::ThreeD { .. }, Mode::TwoD) => { let drawable_size = self.window.drawable_size(); self.camera = Camera::new_2d(self.scene_view_box, drawable_size); } _ => {} } - match ui_event { - UIEvent::MouseDown(_) if self.camera.is_3d() => { - // If nothing handled the mouse-down event, toggle mouselook. - self.mouselook_enabled = !self.mouselook_enabled; - } - UIEvent::MouseDragged { relative_position, .. } => { - if let Camera::TwoD(ref mut transform) = self.camera { - *transform = transform.post_translate(relative_position.to_f32()); + for ui_event in ui_events { + match ui_event { + UIEvent::MouseDown(_) if self.camera.is_3d() => { + // If nothing handled the mouse-down event, toggle mouselook. + self.mouselook_enabled = !self.mouselook_enabled; } + UIEvent::MouseDragged(position) => { + if let Camera::TwoD(ref mut transform) = self.camera { + *transform = transform.post_translate(position.relative.to_f32()); + } + } + _ => {} } - _ => {} } self.window.present(); self.frame_counter += 1; } - fn draw_environment(&self, render_transform: &RenderTransform) { + fn draw_environment(&self, viewport_index: usize, render_transform: &RenderTransform) { let perspective = match *render_transform { RenderTransform::Transform2D(..) => return, RenderTransform::Perspective(perspective) => perspective, @@ -449,7 +492,12 @@ impl DemoApp where W: Window { }); } - fn render_vector_scene(&mut self, built_scene: &BuiltScene) { + fn render_vector_scene(&mut self, viewport_index: usize, built_scene: &BuiltScene) { + let view_box_size = view_box_size(self.ui.mode, &self.window); + let viewport_origin_x = viewport_index as i32 * view_box_size.x(); + let viewport = RectI32::new(Point2DI32::new(viewport_origin_x, 0), view_box_size); + self.renderer.set_viewport(viewport); + if self.ui.gamma_correction_effect_enabled { self.renderer.enable_gamma_correction(self.background_color()); } else { @@ -462,10 +510,10 @@ impl DemoApp where W: Window { self.renderer.disable_subpixel_aa(); } - if self.ui.three_d_enabled { - self.renderer.enable_depth(); - } else { + if self.ui.mode == Mode::TwoD { self.renderer.disable_depth(); + } else { + self.renderer.enable_depth(); } self.renderer.render_scene(&built_scene); @@ -485,10 +533,10 @@ impl DemoApp where W: Window { self.scene_thread_proxy.set_drawable_size(self.window.drawable_size()); let drawable_size = self.window.drawable_size(); - self.camera = if self.ui.three_d_enabled { - Camera::new_3d(built_svg.scene.view_box) - } else { + self.camera = if self.ui.mode == Mode::TwoD { Camera::new_2d(built_svg.scene.view_box, drawable_size) + } else { + Camera::new_3d(built_svg.scene.view_box) }; self.scene_thread_proxy.load_scene(built_svg.scene); @@ -546,6 +594,7 @@ impl DemoApp where W: Window { fn background_color(&self) -> ColorU { if self.ui.dark_background_enabled { DARK_BG_COLOR } else { LIGHT_BG_COLOR } } + } struct SceneThreadProxy { @@ -593,15 +642,18 @@ impl SceneThread { self.scene.view_box = RectF32::new(Point2DF32::default(), size.to_f32()); } MainToSceneMsg::Build(build_options) => { - let render_transform = build_options.render_transform.clone(); let start_time = Instant::now(); - let built_scene = build_scene(&self.scene, build_options, self.options.jobs); + let render_scenes = build_options.render_transforms + .iter() + .map(|render_transform| { + let built_scene = build_scene(&self.scene, + &build_options, + (*render_transform).clone(), + self.options.jobs); + RenderScene { built_scene, transform: (*render_transform).clone() } + }).collect(); let tile_time = Instant::now() - start_time; - self.sender.send(SceneToMainMsg::Render { - built_scene, - transform: render_transform, - tile_time, - }).unwrap(); + self.sender.send(SceneToMainMsg::Render { render_scenes, tile_time }).unwrap(); } } } @@ -615,18 +667,23 @@ enum MainToSceneMsg { } struct BuildOptions { - render_transform: RenderTransform, + render_transforms: Vec, stem_darkening_font_size: Option, } enum SceneToMainMsg { - Render { built_scene: BuiltScene, transform: RenderTransform, tile_time: Duration } + Render { render_scenes: Vec, tile_time: Duration } +} + +pub struct RenderScene { + built_scene: BuiltScene, + transform: RenderTransform, } #[derive(Clone)] pub struct Options { jobs: Option, - three_d: bool, + mode: Mode, input_path: Option, } @@ -641,14 +698,22 @@ impl Options { .takes_value(true) .help("Number of threads to use"), ) - .arg(Arg::with_name("3d").short("3").long("3d").help("Run in 3D")) + .arg(Arg::with_name("3d").short("3").long("3d").help("Run in 3D").conflicts_with("vr")) + .arg(Arg::with_name("vr").short("V").long("vr").help("Run in VR").conflicts_with("3d")) .arg(Arg::with_name("INPUT").help("Path to the SVG file to render").index(1)) .get_matches(); let jobs: Option = matches .value_of("jobs") .map(|string| string.parse().unwrap()); - let three_d = matches.is_present("3d"); + + let mode = if matches.is_present("3d") { + Mode::ThreeD + } else if matches.is_present("vr") { + Mode::VR + } else { + Mode::TwoD + }; let input_path = matches.value_of("INPUT").map(PathBuf::from); @@ -659,10 +724,29 @@ impl Options { } thread_pool_builder.build_global().unwrap(); - Options { jobs, three_d, input_path } + Options { jobs, mode, input_path } } } +#[derive(Clone, Copy, PartialEq)] +pub enum Mode { + TwoD = 0, + ThreeD = 1, + VR = 2, +} + +impl Mode { + fn viewport_count(self) -> usize { + match self { Mode::TwoD | Mode::ThreeD => 1, Mode::VR => 2 } + } +} + +#[derive(Clone, Copy)] +struct RenderStats { + rendering_time: Option, + stats: Stats, +} + fn load_scene(resource_loader: &dyn ResourceLoader, input_path: &Option) -> BuiltSVG { let mut data; match *input_path { @@ -680,11 +764,15 @@ fn load_scene(resource_loader: &dyn ResourceLoader, input_path: &Option BuiltSVG::from_tree(Tree::from_data(&data, &UsvgOptions::default()).unwrap()) } -fn build_scene(scene: &Scene, build_options: BuildOptions, jobs: Option) -> BuiltScene { +fn build_scene(scene: &Scene, + build_options: &BuildOptions, + render_transform: RenderTransform, + jobs: Option) + -> BuiltScene { let z_buffer = ZBuffer::new(scene.view_box); let render_options = RenderOptions { - transform: build_options.render_transform, + transform: render_transform, dilation: match build_options.stem_darkening_font_size { None => Point2DF32::default(), Some(font_size) => { @@ -835,3 +923,11 @@ fn emit_message(ui: &mut DemoUI, W::push_user_event(expire_message_event_id, expected_epoch); }); } + +fn view_box_size(mode: Mode, window: &W) -> Point2DI32 where W: Window { + let window_drawable_size = window.drawable_size(); + match mode { + Mode::TwoD | Mode::ThreeD => window_drawable_size, + Mode::VR => Point2DI32::new(window_drawable_size.x() / 2, window_drawable_size.y()), + } +} diff --git a/demo/common/src/ui.rs b/demo/common/src/ui.rs index 7ef46d87..b9ce123c 100644 --- a/demo/common/src/ui.rs +++ b/demo/common/src/ui.rs @@ -8,7 +8,7 @@ // option. This file may not be copied, modified, or distributed // except according to those terms. -use crate::Options; +use crate::{Mode, Options}; use crate::window::Window; use pathfinder_geometry::basic::point::Point2DI32; use pathfinder_geometry::basic::rect::RectI32; @@ -16,7 +16,7 @@ use pathfinder_gpu::Device; use pathfinder_gpu::resources::ResourceLoader; use pathfinder_renderer::gpu::debug::DebugUI; use pathfinder_ui::{BUTTON_HEIGHT, BUTTON_TEXT_OFFSET, BUTTON_WIDTH, FONT_ASCENT, PADDING}; -use pathfinder_ui::{SWITCH_SIZE, TEXT_COLOR, TOOLTIP_HEIGHT, WINDOW_COLOR}; +use pathfinder_ui::{TEXT_COLOR, TOOLTIP_HEIGHT, WINDOW_COLOR}; use std::f32::consts::PI; use std::path::PathBuf; @@ -56,7 +56,7 @@ pub struct DemoUI where D: Device { // FIXME(pcwalton): Factor the below out into a model class. - pub three_d_enabled: bool, + pub mode: Mode, pub dark_background_enabled: bool, pub gamma_correction_effect_enabled: bool, pub stem_darkening_effect_enabled: bool, @@ -90,7 +90,7 @@ impl DemoUI where D: Device { effects_panel_visible: false, rotate_panel_visible: false, - three_d_enabled: options.three_d, + mode: options.mode, dark_background_enabled: false, gamma_correction_effect_enabled: false, stem_darkening_effect_enabled: false, @@ -121,7 +121,6 @@ impl DemoUI where D: Device { let mut position = Point2DI32::new(PADDING, bottom - BUTTON_HEIGHT); let button_size = Point2DI32::new(BUTTON_WIDTH, BUTTON_HEIGHT); - let switch_size = Point2DI32::new(SWITCH_SIZE, BUTTON_HEIGHT); // Draw text effects button. if self.show_text_effects { @@ -158,14 +157,24 @@ impl DemoUI where D: Device { debug_ui.ui.draw_tooltip(device, "Take Screenshot", RectI32::new(position, button_size)); position += Point2DI32::new(BUTTON_WIDTH + PADDING, 0); - // Draw 3D switch. - self.three_d_enabled = debug_ui.ui.draw_text_switch(device, - position, - "2D", - "3D", - self.three_d_enabled); - debug_ui.ui.draw_tooltip(device, "2D/3D Mode", RectI32::new(position, switch_size)); - position += Point2DI32::new(SWITCH_SIZE + PADDING, 0); + // Draw mode switch. + let new_mode = debug_ui.ui.draw_text_switch(device, + position, + &["2D", "3D", "VR"], + self.mode as u8); + if new_mode != self.mode as u8 { + self.mode = match new_mode { + 0 => Mode::TwoD, + 1 => Mode::ThreeD, + _ => Mode::VR, + }; + } + let mode_switch_width = debug_ui.ui.measure_switch(3); + let mode_switch_size = Point2DI32::new(mode_switch_width, BUTTON_HEIGHT); + debug_ui.ui.draw_tooltip(device, + "2D/3D/VR Mode", + RectI32::new(position, mode_switch_size)); + position += Point2DI32::new(mode_switch_width + PADDING, 0); // Draw background switch. self.dark_background_enabled = debug_ui.ui.draw_image_switch(device, @@ -173,14 +182,18 @@ impl DemoUI where D: Device { &self.bg_light_texture, &self.bg_dark_texture, self.dark_background_enabled); - debug_ui.ui.draw_tooltip(device, "Background Color", RectI32::new(position, switch_size)); - position += Point2DI32::new(SWITCH_SIZE + PADDING, 0); + let background_color_switch_width = debug_ui.ui.measure_switch(2); + let background_color_switch_size = Point2DI32::new(background_color_switch_width, + BUTTON_HEIGHT); + let background_color_switch_rect = RectI32::new(position, background_color_switch_size); + debug_ui.ui.draw_tooltip(device, "Background Color", background_color_switch_rect); + position += Point2DI32::new(background_color_switch_width + PADDING, 0); // Draw effects panel, if necessary. self.draw_effects_panel(device, debug_ui); // Draw rotate and zoom buttons, if applicable. - if self.three_d_enabled { + if self.mode != Mode::TwoD { return; } @@ -281,7 +294,7 @@ impl DemoUI where D: Device { let widget_rect = RectI32::new(Point2DI32::new(widget_x, widget_y), Point2DI32::new(SLIDER_WIDTH, SLIDER_KNOB_HEIGHT)); if let Some(position) = debug_ui.ui - .event + .event_queue .handle_mouse_down_or_dragged_in_rect(widget_rect) { self.rotation = position.x(); *action = UIAction::Rotate(self.rotation()); @@ -313,10 +326,11 @@ impl DemoUI where D: Device { let text_y = window_y + PADDING + BUTTON_TEXT_OFFSET + (BUTTON_HEIGHT + PADDING) * index; debug_ui.ui.draw_text(device, text, Point2DI32::new(text_x, text_y), false); - let switch_x = PADDING + EFFECTS_PANEL_WIDTH - (SWITCH_SIZE + PADDING); + let switch_width = debug_ui.ui.measure_switch(2); + let switch_x = PADDING + EFFECTS_PANEL_WIDTH - (switch_width + PADDING); let switch_y = window_y + PADDING + (BUTTON_HEIGHT + PADDING) * index; let switch_position = Point2DI32::new(switch_x, switch_y); - debug_ui.ui.draw_text_switch(device, switch_position, "Off", "On", value) + debug_ui.ui.draw_text_switch(device, switch_position, &["Off", "On"], value as u8) != 0 } } diff --git a/demo/common/src/window.rs b/demo/common/src/window.rs index c07725ef..1c98d5ea 100644 --- a/demo/common/src/window.rs +++ b/demo/common/src/window.rs @@ -36,8 +36,8 @@ pub enum Event { KeyDown(Keycode), KeyUp(Keycode), MouseDown(Point2DI32), - MouseMoved { position: Point2DI32, relative_position: Point2DI32 }, - MouseDragged { position: Point2DI32, relative_position: Point2DI32 }, + MouseMoved(Point2DI32), + MouseDragged(Point2DI32), Zoom(f32), User { message_type: u32, message_data: u32 }, } diff --git a/demo/native/src/main.rs b/demo/native/src/main.rs index fc3dea01..c35a29c1 100644 --- a/demo/native/src/main.rs +++ b/demo/native/src/main.rs @@ -167,13 +167,12 @@ impl WindowImpl { SDLEvent::MouseButtonDown { x, y, .. } => { Some(Event::MouseDown(Point2DI32::new(x, y))) } - SDLEvent::MouseMotion { x, y, xrel, yrel, mousestate, .. } => { + SDLEvent::MouseMotion { x, y, mousestate, .. } => { let position = Point2DI32::new(x, y); - let relative_position = Point2DI32::new(xrel, yrel); if mousestate.left() { - Some(Event::MouseDragged { position, relative_position }) + Some(Event::MouseDragged(position)) } else { - Some(Event::MouseMoved { position, relative_position }) + Some(Event::MouseMoved(position)) } } SDLEvent::Quit { .. } => Some(Event::Quit), diff --git a/gl/src/lib.rs b/gl/src/lib.rs index 10e44d19..cf6e1620 100644 --- a/gl/src/lib.rs +++ b/gl/src/lib.rs @@ -12,6 +12,7 @@ use gl::types::{GLboolean, GLchar, GLenum, GLfloat, GLint, GLsizei, GLsizeiptr, GLuint, GLvoid}; use pathfinder_geometry::basic::point::Point2DI32; +use pathfinder_geometry::basic::rect::RectI32; use pathfinder_gpu::{BlendState, BufferTarget, BufferUploadMode, DepthFunc, Device, Primitive}; use pathfinder_gpu::{RenderState, ShaderKind, StencilFunc, TextureFormat}; use pathfinder_gpu::{UniformData, VertexAttrType}; @@ -584,10 +585,13 @@ impl Device for GLDevice { } #[inline] - fn bind_default_framebuffer(&self, size: Point2DI32) { + fn bind_default_framebuffer(&self, viewport: RectI32) { unsafe { gl::BindFramebuffer(gl::FRAMEBUFFER, 0); ck(); - gl::Viewport(0, 0, size.x(), size.y()); ck(); + gl::Viewport(viewport.origin().x(), + viewport.origin().y(), + viewport.size().x(), + viewport.size().y()); ck(); } } diff --git a/gpu/src/lib.rs b/gpu/src/lib.rs index f5112d06..e0f7793e 100644 --- a/gpu/src/lib.rs +++ b/gpu/src/lib.rs @@ -13,6 +13,7 @@ use crate::resources::ResourceLoader; use image::ImageFormat; use pathfinder_geometry::basic::point::Point2DI32; +use pathfinder_geometry::basic::rect::RectI32; use pathfinder_simd::default::F32x4; use std::env; use std::fs::File; @@ -91,7 +92,7 @@ pub trait Device { // TODO(pcwalton): Go bindless... fn bind_vertex_array(&self, vertex_array: &Self::VertexArray); fn bind_buffer(&self, buffer: &Self::Buffer, target: BufferTarget); - fn bind_default_framebuffer(&self, size: Point2DI32); + fn bind_default_framebuffer(&self, viewport: RectI32); fn bind_framebuffer(&self, framebuffer: &Self::Framebuffer); fn bind_texture(&self, texture: &Self::Texture, unit: u32); diff --git a/renderer/src/gpu/renderer.rs b/renderer/src/gpu/renderer.rs index c4548d75..bb4a22be 100644 --- a/renderer/src/gpu/renderer.rs +++ b/renderer/src/gpu/renderer.rs @@ -14,6 +14,7 @@ use crate::post::DefringingKernel; use crate::scene::ObjectShader; use crate::tiles::{TILE_HEIGHT, TILE_WIDTH}; use pathfinder_geometry::basic::point::{Point2DI32, Point3DF32}; +use pathfinder_geometry::basic::rect::RectI32; use pathfinder_geometry::color::ColorU; use pathfinder_gpu::resources::ResourceLoader; use pathfinder_gpu::{BlendState, BufferTarget, BufferUploadMode, DepthFunc, DepthState, Device}; @@ -68,13 +69,17 @@ pub struct Renderer where D: Device { pub debug_ui: DebugUI, // Extra info + viewport: RectI32, main_framebuffer_size: Point2DI32, postprocess_options: PostprocessOptions, use_depth: bool, } impl Renderer where D: Device { - pub fn new(device: D, resources: &dyn ResourceLoader, main_framebuffer_size: Point2DI32) + pub fn new(device: D, + resources: &dyn ResourceLoader, + viewport: RectI32, + main_framebuffer_size: Point2DI32) -> Renderer { let fill_program = FillProgram::new(&device, resources); let solid_tile_program = SolidTileProgram::new(&device, resources); @@ -144,6 +149,7 @@ impl Renderer where D: Device { debug_ui, + viewport, main_framebuffer_size, postprocess_options: PostprocessOptions::default(), use_depth: false, @@ -181,6 +187,11 @@ impl Renderer where D: Device { self.pending_timer_queries.push_back(timer_query); } + pub fn draw_debug_ui(&self) { + self.device.bind_default_framebuffer(self.viewport); + self.debug_ui.draw(&self.device); + } + pub fn shift_timer_query(&mut self) -> Option { let query = self.pending_timer_queries.front()?; if !self.device.timer_query_is_available(&query) { @@ -192,9 +203,13 @@ impl Renderer where D: Device { Some(result) } + #[inline] + pub fn set_viewport(&mut self, new_viewport: RectI32) { + self.viewport = new_viewport; + } + #[inline] pub fn set_main_framebuffer_size(&mut self, new_framebuffer_size: Point2DI32) { - self.main_framebuffer_size = new_framebuffer_size; self.debug_ui.ui.set_framebuffer_size(new_framebuffer_size); } @@ -299,7 +314,7 @@ impl Renderer where D: Device { self.device.bind_vertex_array(&self.mask_tile_vertex_array.vertex_array); self.device.use_program(&self.mask_tile_program.program); self.device.set_uniform(&self.mask_tile_program.framebuffer_size_uniform, - UniformData::Vec2(self.main_framebuffer_size.0.to_f32x4())); + UniformData::Vec2(self.viewport.size().to_f32().0)); self.device.set_uniform(&self.mask_tile_program.tile_size_uniform, UniformData::Vec2(I32x4::new(TILE_WIDTH as i32, TILE_HEIGHT as i32, @@ -339,7 +354,7 @@ impl Renderer where D: Device { self.device.bind_vertex_array(&self.solid_tile_vertex_array.vertex_array); self.device.use_program(&self.solid_tile_program.program); self.device.set_uniform(&self.solid_tile_program.framebuffer_size_uniform, - UniformData::Vec2(self.main_framebuffer_size.0.to_f32x4())); + UniformData::Vec2(self.viewport.size().0.to_f32x4())); self.device.set_uniform(&self.solid_tile_program.tile_size_uniform, UniformData::Vec2(I32x4::new(TILE_WIDTH as i32, TILE_HEIGHT as i32, @@ -365,12 +380,12 @@ impl Renderer where D: Device { } fn postprocess(&mut self) { - self.device.bind_default_framebuffer(self.main_framebuffer_size); + self.device.bind_default_framebuffer(self.viewport); self.device.bind_vertex_array(&self.postprocess_vertex_array.vertex_array); self.device.use_program(&self.postprocess_program.program); self.device.set_uniform(&self.postprocess_program.framebuffer_size_uniform, - UniformData::Vec2(self.main_framebuffer_size.to_f32().0)); + UniformData::Vec2(self.viewport.size().to_f32().0)); match self.postprocess_options.defringing_kernel { Some(ref kernel) => { self.device.set_uniform(&self.postprocess_program.kernel_uniform, @@ -434,7 +449,7 @@ impl Renderer where D: Device { if self.postprocessing_needed() { self.device.bind_framebuffer(self.postprocess_source_framebuffer.as_ref().unwrap()); } else { - self.device.bind_default_framebuffer(self.main_framebuffer_size); + self.device.bind_default_framebuffer(self.viewport); } } @@ -447,10 +462,10 @@ impl Renderer where D: Device { match self.postprocess_source_framebuffer { Some(ref framebuffer) if self.device.texture_size(self.device.framebuffer_texture(framebuffer)) == - self.main_framebuffer_size => {} + self.viewport.size() => {} _ => { let texture = self.device.create_texture(TextureFormat::RGBA8, - self.main_framebuffer_size); + self.viewport.size()); self.postprocess_source_framebuffer = Some(self.device.create_framebuffer(texture)) } }; diff --git a/renderer/src/gpu_data.rs b/renderer/src/gpu_data.rs index 340565d5..430ac670 100644 --- a/renderer/src/gpu_data.rs +++ b/renderer/src/gpu_data.rs @@ -18,6 +18,7 @@ use pathfinder_geometry::basic::point::{Point2DF32, Point3DF32}; use pathfinder_geometry::basic::rect::{RectF32, RectI32}; use pathfinder_geometry::util; use pathfinder_simd::default::{F32x4, I32x4}; +use std::ops::Add; #[derive(Debug)] pub struct BuiltObject { @@ -306,3 +307,15 @@ impl TileObjectPrimitive { } } } + +impl Add for Stats { + type Output = Stats; + fn add(self, other: Stats) -> Stats { + Stats { + object_count: other.object_count, + solid_tile_count: other.solid_tile_count, + mask_tile_count: other.mask_tile_count, + fill_count: other.fill_count, + } + } +} diff --git a/ui/src/lib.rs b/ui/src/lib.rs index 694e354c..04332920 100644 --- a/ui/src/lib.rs +++ b/ui/src/lib.rs @@ -25,8 +25,7 @@ use pathfinder_gpu::{BlendState, BufferTarget, BufferUploadMode, Device, Primiti use pathfinder_gpu::{UniformData, VertexAttrType}; use pathfinder_simd::default::F32x4; use serde_json; -use std::fs::File; -use std::io::BufReader; +use std::mem; pub const PADDING: i32 = 12; @@ -37,8 +36,6 @@ pub const BUTTON_WIDTH: i32 = PADDING * 2 + ICON_SIZE; pub const BUTTON_HEIGHT: i32 = PADDING * 2 + ICON_SIZE; pub const BUTTON_TEXT_OFFSET: i32 = PADDING + 36; -pub const SWITCH_SIZE: i32 = SWITCH_HALF_SIZE * 2 + 1; - pub const TOOLTIP_HEIGHT: i32 = FONT_ASCENT + PADDING * 2; const DEBUG_TEXTURE_VERTEX_SIZE: usize = 8; @@ -46,7 +43,7 @@ const DEBUG_SOLID_VERTEX_SIZE: usize = 4; const ICON_SIZE: i32 = 48; -const SWITCH_HALF_SIZE: i32 = 96; +const SWITCH_SEGMENT_SIZE: i32 = 96; pub static TEXT_COLOR: ColorU = ColorU { r: 255, g: 255, b: 255, a: 255 }; pub static WINDOW_COLOR: ColorU = ColorU { r: 0, g: 0, b: 0, a: 255 - 90 }; @@ -67,7 +64,7 @@ static RECT_LINE_INDICES: [u32; 8] = [0, 1, 1, 2, 2, 3, 3, 0]; static OUTLINE_RECT_LINE_INDICES: [u32; 8] = [0, 1, 2, 3, 4, 5, 6, 7]; pub struct UI where D: Device { - pub event: UIEvent, + pub event_queue: UIEventQueue, pub mouse_position: Point2DF32, framebuffer_size: Point2DI32, @@ -98,7 +95,7 @@ impl UI where D: Device { CORNER_OUTLINE_PNG_NAME); UI { - event: UIEvent::None, + event_queue: UIEventQueue::new(), mouse_position: Point2DF32::default(), framebuffer_size, @@ -251,6 +248,11 @@ impl UI where D: Device { next } + #[inline] + pub fn measure_switch(&self, segment_count: u8) -> i32 { + SWITCH_SEGMENT_SIZE * segment_count as i32 + (segment_count - 1) as i32 + } + pub fn draw_solid_rounded_rect(&self, device: &D, rect: RectI32, color: ColorU) { let corner_texture = self.corner_texture(true); let corner_rects = CornerRects::new(device, rect, corner_texture); @@ -311,6 +313,13 @@ impl UI where D: Device { self.draw_solid_rects_with_vertex_data(device, &vertex_data, index_data, color, false); } + // TODO(pcwalton): `LineSegmentI32`. + fn draw_line(&self, device: &D, from: Point2DI32, to: Point2DI32, color: ColorU) { + let vertex_data = vec![DebugSolidVertex::new(from), DebugSolidVertex::new(to)]; + self.draw_solid_rects_with_vertex_data(device, &vertex_data, &[0, 1], color, false); + + } + fn draw_rounded_rect_corners(&self, device: &D, color: ColorU, @@ -409,30 +418,32 @@ impl UI where D: Device { origin + Point2DI32::new(PADDING, PADDING), texture, BUTTON_ICON_COLOR); - self.event.handle_mouse_down_in_rect(button_rect).is_some() + self.event_queue.handle_mouse_down_in_rect(button_rect).is_some() } pub fn draw_text_switch(&mut self, device: &D, - origin: Point2DI32, - off_text: &str, - on_text: &str, - mut value: bool) - -> bool { - value = self.draw_switch(device, origin, value); + mut origin: Point2DI32, + segment_labels: &[&str], + mut value: u8) + -> u8 { + value = self.draw_switch(device, origin, value, segment_labels.len() as u8); - let off_size = self.measure_text(off_text); - let on_size = self.measure_text(on_text); - let off_offset = SWITCH_HALF_SIZE / 2 - off_size / 2; - let on_offset = SWITCH_HALF_SIZE + SWITCH_HALF_SIZE / 2 - on_size / 2; - let text_top = BUTTON_TEXT_OFFSET; - - self.draw_text(device, off_text, origin + Point2DI32::new(off_offset, text_top), !value); - self.draw_text(device, on_text, origin + Point2DI32::new(on_offset, text_top), value); + origin = origin + Point2DI32::new(0, BUTTON_TEXT_OFFSET); + for (segment_index, segment_label) in segment_labels.iter().enumerate() { + let label_width = self.measure_text(segment_label); + let offset = SWITCH_SEGMENT_SIZE / 2 - label_width / 2; + self.draw_text(device, + segment_label, + origin + Point2DI32::new(offset, 0), + segment_index as u8 == value); + origin += Point2DI32::new(SWITCH_SEGMENT_SIZE + 1, 0); + } value } + /// TODO(pcwalton): Support switches with more than two segments. pub fn draw_image_switch(&mut self, device: &D, origin: Point2DI32, @@ -440,10 +451,10 @@ impl UI where D: Device { on_texture: &D::Texture, mut value: bool) -> bool { - value = self.draw_switch(device, origin, value); + value = self.draw_switch(device, origin, value as u8, 2) != 0; - let off_offset = SWITCH_HALF_SIZE / 2 - device.texture_size(off_texture).x() / 2; - let on_offset = SWITCH_HALF_SIZE + SWITCH_HALF_SIZE / 2 - + let off_offset = SWITCH_SEGMENT_SIZE / 2 - device.texture_size(off_texture).x() / 2; + let on_offset = SWITCH_SEGMENT_SIZE + SWITCH_SEGMENT_SIZE / 2 - device.texture_size(on_texture).x() / 2; let off_color = if !value { WINDOW_COLOR } else { TEXT_COLOR }; @@ -461,24 +472,35 @@ impl UI where D: Device { value } - fn draw_switch(&mut self, device: &D, origin: Point2DI32, mut value: bool) -> bool { - let widget_rect = RectI32::new(origin, Point2DI32::new(SWITCH_SIZE, BUTTON_HEIGHT)); - if self.event.handle_mouse_down_in_rect(widget_rect).is_some() { - value = !value; + fn draw_switch(&mut self, device: &D, origin: Point2DI32, mut value: u8, segment_count: u8) + -> u8 { + let widget_width = self.measure_switch(segment_count); + let widget_rect = RectI32::new(origin, Point2DI32::new(widget_width, BUTTON_HEIGHT)); + if let Some(position) = self.event_queue.handle_mouse_down_in_rect(widget_rect) { + let segment = (position.x() / (SWITCH_SEGMENT_SIZE + 1)) as u8; + value = segment.min(segment_count - 1); } self.draw_solid_rounded_rect(device, widget_rect, WINDOW_COLOR); self.draw_rounded_rect_outline(device, widget_rect, OUTLINE_COLOR); - let highlight_size = Point2DI32::new(SWITCH_HALF_SIZE, BUTTON_HEIGHT); - if !value { - self.draw_solid_rounded_rect(device, RectI32::new(origin, highlight_size), TEXT_COLOR); - } else { - let x_offset = SWITCH_HALF_SIZE + 1; - self.draw_solid_rounded_rect(device, - RectI32::new(origin + Point2DI32::new(x_offset, 0), - highlight_size), - TEXT_COLOR); + let highlight_size = Point2DI32::new(SWITCH_SEGMENT_SIZE, BUTTON_HEIGHT); + let x_offset = value as i32 * SWITCH_SEGMENT_SIZE + (value as i32 - 1); + self.draw_solid_rounded_rect(device, + RectI32::new(origin + Point2DI32::new(x_offset, 0), + highlight_size), + TEXT_COLOR); + + let mut segment_origin = origin + Point2DI32::new(SWITCH_SEGMENT_SIZE + 1, 0); + for next_segment_index in 1..segment_count { + let prev_segment_index = next_segment_index - 1; + if value != prev_segment_index && value != next_segment_index { + self.draw_line(device, + segment_origin, + segment_origin + Point2DI32::new(0, BUTTON_HEIGHT), + TEXT_COLOR); + } + segment_origin = segment_origin + Point2DI32::new(SWITCH_SEGMENT_SIZE + 1, 0); } value @@ -666,42 +688,65 @@ fn set_color_uniform(device: &D, uniform: &D::Uniform, color: ColorU) where D device.set_uniform(uniform, UniformData::Vec4(color * F32x4::splat(1.0 / 255.0))); } +#[derive(Clone, Copy)] pub enum UIEvent { - None, - MouseDown(Point2DI32), - MouseDragged { - absolute_position: Point2DI32, - relative_position: Point2DI32, - } + MouseDown(MousePosition), + MouseDragged(MousePosition), } -impl UIEvent { - pub fn is_none(&self) -> bool { - match *self { UIEvent::None => true, _ => false } +pub struct UIEventQueue { + events: Vec, +} + +impl UIEventQueue { + fn new() -> UIEventQueue { + UIEventQueue { events: vec![] } + } + + pub fn push(&mut self, event: UIEvent) { + self.events.push(event); + } + + pub fn drain(&mut self) -> Vec { + mem::replace(&mut self.events, vec![]) } fn handle_mouse_down_in_rect(&mut self, rect: RectI32) -> Option { - if let UIEvent::MouseDown(point) = *self { - if rect.contains_point(point) { - *self = UIEvent::None; - return Some(point - rect.origin()); + let (mut remaining_events, mut result) = (vec![], None); + for event in self.events.drain(..) { + match event { + UIEvent::MouseDown(position) if rect.contains_point(position.absolute) => { + result = Some(position.absolute - rect.origin()); + } + event => remaining_events.push(event), } } - None + self.events = remaining_events; + result } pub fn handle_mouse_down_or_dragged_in_rect(&mut self, rect: RectI32) -> Option { - match *self { - UIEvent::MouseDown(point) | UIEvent::MouseDragged { absolute_position: point, .. } - if rect.contains_point(point) => { - *self = UIEvent::None; - Some(point - rect.origin()) + let (mut remaining_events, mut result) = (vec![], None); + for event in self.events.drain(..) { + match event { + UIEvent::MouseDown(position) | UIEvent::MouseDragged(position) if + rect.contains_point(position.absolute) => { + result = Some(position.absolute - rect.origin()); + } + event => remaining_events.push(event), } - _ => None, } + self.events = remaining_events; + result } } +#[derive(Clone, Copy)] +pub struct MousePosition { + pub absolute: Point2DI32, + pub relative: Point2DI32, +} + #[derive(Deserialize)] #[allow(dead_code)] pub struct DebugFont {