Merge branch 'ui' into 'master'

Ui

See merge request veloren/fresh!14

Former-commit-id: 0222d01456af717d4956c7aff373fcefe1527042
This commit is contained in:
Joshua Barretto 2019-04-01 11:40:43 +00:00
commit b0f483184c
67 changed files with 4715 additions and 590 deletions

1
.gitignore vendored
View File

@ -19,3 +19,4 @@
# Veloren
**/server_conf.toml
**/keybinds.toml
assets/voxygen

View File

@ -1,13 +1,3 @@
# Fresh
An experiment
## Compile
```
git clone https://gitlab.com/veloren/fresh.git
cd fresh
git submodule update --init --recursive
rustup default nightly
cargo build
```
An experiment

1
assets/voxygen Submodule

@ -0,0 +1 @@
Subproject commit e25c963fe1726e7bdfd81633ed9b7004bc36dcf8

View File

@ -1 +0,0 @@
tmp/

3
chat-cli/.gitignore vendored
View File

@ -1,3 +1,4 @@
/target
**/*.rs.bk
/assets
**/*.rs.bk.rar
Cargo.lock

View File

@ -138,11 +138,10 @@ impl Client {
// Handle new messages from the server
frontend_events.append(&mut self.handle_new_messages()?);
// Step 3
// Step 1
if let Some(ecs_entity) = self.player {
// TODO: remove this
const PLAYER_VELOCITY: f32 = 100.0;
// TODO: Set acceleration instead
self.state.write_component(ecs_entity, comp::phys::Vel(Vec3::from(input.move_dir * PLAYER_VELOCITY)));
}

View File

@ -18,6 +18,8 @@ gfx = "0.17"
gfx_device_gl = { version = "0.15", optional = true }
gfx_window_glutin = "0.28"
glutin = "0.19"
conrod_core = "0.63"
conrod_winit = "0.63"
# ECS
specs = "0.14"

View File

@ -1,17 +1,22 @@
#version 330 core
in vec3 f_pos;
in vec2 f_uv;
layout (std140)
uniform u_locals {
vec4 bounds;
};
in vec4 f_color;
flat in uint f_mode;
uniform sampler2D u_tex;
out vec4 tgt_color;
void main() {
tgt_color = texture(u_tex, f_uv);
// Text
if (f_mode == uint(0)) {
tgt_color = f_color * vec4(1.0, 1.0, 1.0, texture(u_tex, f_uv).a);
// Image
} else if (f_mode == uint(1)) {
tgt_color = texture(u_tex, f_uv);
// 2D Geometry
} else if (f_mode == uint(2)) {
tgt_color = f_color;
}
}

View File

@ -1,22 +1,19 @@
#version 330 core
in vec3 v_pos;
in vec2 v_pos;
in vec2 v_uv;
layout (std140)
uniform u_locals {
vec4 bounds;
};
in vec4 v_color;
in uint v_mode;
uniform sampler2D u_tex;
out vec3 f_pos;
out vec2 f_uv;
flat out uint f_mode;
out vec4 f_color;
void main() {
f_uv = v_uv;
f_pos = vec3(vec2(bounds.x, bounds.y) + v_pos.xy * vec2(bounds.z, bounds.w), 0);
f_pos.xy = vec2(f_pos.x * 2.0 - 1.0, f_pos.y * -2.0 + 1.0);
gl_Position = vec4(f_pos, 1);
f_color = v_color;
gl_Position = vec4(v_pos, 0.0, 1.0);
f_mode = v_mode;
}

165
voxygen/src/hud/chat.rs Normal file
View File

@ -0,0 +1,165 @@
use crate::ui::Ui;
use conrod_core::{
input::Key,
position::Dimension,
text::font::Id as FontId,
widget::{Id, Button, List, Rectangle, Text, TextEdit},
widget_ids, Color, Colorable, Positionable, Sizeable, UiCell, Widget,
};
use std::collections::VecDeque;
widget_ids! {
struct Ids {
message_box,
message_box_bg,
input,
input_bg,
chat_arrow,
}
}
// Chat Behaviour:
// Input Window is only shown when the player presses Enter (graphical overlay to make it look better?)
// Instead of a Scrollbar it could have 3 Arrows at it's left side
// First two: Scroll the chat up and down
// Last one: Gets back to the bottom of the chat
// Consider making this a custom Widget
pub struct Chat {
ids: Ids,
messages: VecDeque<String>,
input: String,
new_messages: bool,
}
impl Chat {
pub fn new(ui: &mut Ui) -> Self {
Chat {
ids: Ids::new(ui.id_generator()),
messages: VecDeque::new(),
input: String::new(),
new_messages: false,
}
}
pub fn input_box_id(&self) -> Id {
self.ids.input
}
pub fn new_message(&mut self, msg: String) {
self.messages.push_back(msg);
self.new_messages = true;
}
// Determine if the message box is scrolled to the bottom
// (i.e. the player is viewing new messages)
// If so scroll down when new messages are added
fn scroll_new_messages(&self, ui_widgets: &mut UiCell) {
if let Some(scroll) = ui_widgets
.widget_graph()
.widget(self.ids.message_box)
.and_then(|widget| widget.maybe_y_scroll_state)
{
// If previously scrolled to the bottom stay there
if self.scrolled_to_bottom(ui_widgets) {
self.scroll_to_bottom(ui_widgets);
}
}
}
fn scrolled_to_bottom(&self, ui_widgets: &UiCell) -> bool {
// could be more efficient to cache result and update it when a scroll event has occurred instead of every frame
if let Some(scroll) = ui_widgets
.widget_graph()
.widget(self.ids.message_box)
.and_then(|widget| widget.maybe_y_scroll_state)
{
scroll.offset >= scroll.offset_bounds.start
} else {
false
}
}
fn scroll_to_bottom(&self, ui_widgets: &mut UiCell) {
ui_widgets.scroll_widget(self.ids.message_box, [0.0, std::f64::MAX]);
}
pub fn update_layout(&mut self, ui_widgets: &mut UiCell, font: FontId, imgs: &super::Imgs) -> Option<String> {
// Maintain scrolling
if self.new_messages {
self.scroll_new_messages(ui_widgets);
self.new_messages = false;
}
// Chat input with rectangle as background
let text_edit = TextEdit::new(&self.input)
.w(470.0)
.restrict_to_height(false)
.font_size(14)
.font_id(font)
.bottom_left_with_margins_on(ui_widgets.window, 10.0, 10.0);
let dims = match (
text_edit.get_x_dimension(ui_widgets),
text_edit.get_y_dimension(ui_widgets),
) {
(Dimension::Absolute(x), Dimension::Absolute(y)) => [x, y],
_ => [0.0, 0.0],
};
Rectangle::fill(dims)
.rgba(0.0, 0.0, 0.0, 0.8)
.x_position(text_edit.get_x_position(ui_widgets))
.y_position(text_edit.get_y_position(ui_widgets))
.set(self.ids.input_bg, ui_widgets);
if let Some(str) = text_edit.set(self.ids.input, ui_widgets) {
self.input = str.to_string();
self.input.retain(|c| c != '\n');
}
// Message box
Rectangle::fill([470.0, 180.0])
.rgba(0.0, 0.0, 0.0, 0.4)
.up_from(self.ids.input, 0.0)
.set(self.ids.message_box_bg, ui_widgets);
let (mut items, scrollbar) = List::flow_down(self.messages.len())
.middle_of(self.ids.message_box_bg)
.scrollbar_next_to()
.scrollbar_thickness(18.0)
.scrollbar_color(Color::Rgba(0.0, 0.0, 0.0, 1.0))
.set(self.ids.message_box, ui_widgets);
while let Some(item) = items.next(ui_widgets) {
item.set(
Text::new(&self.messages[item.i])
.font_size(14)
.font_id(font)
.rgba(220.0, 220.0, 220.0, 1.0),
ui_widgets,
)
}
//if let Some(s) = scrollbar {
// s.set(ui_widgets)
//}
// Chat Arrow
if !self.scrolled_to_bottom(ui_widgets) {
if Button::image(imgs.chat_arrow)
.w_h(22.0, 22.0)
.hover_image(imgs.chat_arrow_mo)
.press_image(imgs.chat_arrow_press)
.bottom_right_with_margins_on(self.ids.message_box_bg, 2.0, 2.0)
.set(self.ids.chat_arrow, ui_widgets)
.was_clicked()
{
self.scroll_to_bottom(ui_widgets);
}
}
// If enter is pressed send the current message
if ui_widgets
.widget_input(self.ids.input)
.presses()
.key()
.any(|key_press| match key_press.key {
Key::Return => true,
_ => false,
})
{
let new_message = self.input.clone();
self.input.clear();
Some(new_message)
} else {
None
}
}
}

1225
voxygen/src/hud/mod.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,9 @@
#![feature(drain_filter)]
#![recursion_limit="2048"]
pub mod anim;
pub mod error;
pub mod hud;
pub mod key_state;
pub mod menu;
pub mod mesh;
@ -23,7 +25,7 @@ use pretty_env_logger;
// Crate
use crate::{
menu::title::TitleState,
menu::main::MainMenuState,
window::Window,
};
@ -37,6 +39,7 @@ impl GlobalState {
/// effects a state may have made).
pub fn on_play_state_changed(&mut self) {
self.window.grab_cursor(false);
self.window.needs_refresh_resize();
}
}
@ -75,8 +78,8 @@ fn main() {
};
// Set up the initial play state
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(TitleState::new(
&mut global_state.window.renderer_mut(),
let mut states: Vec<Box<dyn PlayState>> = vec![Box::new(MainMenuState::new(
&mut global_state.window,
))];
states.last().map(|current_state| {
log::info!("Started game with state '{}'", current_state.name())

View File

@ -0,0 +1,85 @@
mod ui;
use crate::{
window::{Event, Window},
session::SessionState,
GlobalState, PlayState, PlayStateResult,
};
use common::clock::Clock;
use std::time::Duration;
use ui::CharSelectionUi;
use vek::*;
const FPS: u64 = 60;
pub struct CharSelectionState {
char_selection_ui: CharSelectionUi,
}
impl CharSelectionState {
/// Create a new `CharSelectionState`
pub fn new(window: &mut Window) -> Self {
Self {
char_selection_ui: CharSelectionUi::new(window),
}
}
}
// The background colour
const BG_COLOR: Rgba<f32> = Rgba {
r: 0.0,
g: 0.3,
b: 1.0,
a: 1.0,
};
impl PlayState for CharSelectionState {
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
// Set up an fps clock
let mut clock = Clock::new();
loop {
// Handle window events
for event in global_state.window.fetch_events() {
match event {
Event::Close => return PlayStateResult::Shutdown,
// Pass events to ui
Event::Ui(event) => {
self.char_selection_ui.handle_event(event);
}
// Ignore all other events
_ => {}
}
}
global_state.window.renderer_mut().clear(BG_COLOR);
// Maintain the UI
for event in self.char_selection_ui.maintain(global_state.window.renderer_mut()) {
match event {
ui::Event::Logout => return PlayStateResult::Pop,
ui::Event::Play => return PlayStateResult::Push(
Box::new(SessionState::new(&mut global_state.window).unwrap()) // TODO: Handle this error
),
}
}
// Draw the UI to the screen
self.char_selection_ui.render(global_state.window.renderer_mut());
// Finish the frame
global_state.window.renderer_mut().flush();
global_state
.window
.swap_buffers()
.expect("Failed to swap window buffers");
// Wait for the next tick
clock.tick(Duration::from_millis(1000 / FPS));
}
}
fn name(&self) -> &'static str {
"Title"
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,87 @@
mod ui;
use super::char_selection::CharSelectionState;
use crate::{
window::{Event, Window},
GlobalState, PlayState, PlayStateResult,
};
use common::clock::Clock;
use std::time::Duration;
use ui::{Event as MainMenuEvent, MainMenuUi};
use vek::*;
const FPS: u64 = 60;
pub struct MainMenuState {
main_menu_ui: MainMenuUi,
}
impl MainMenuState {
/// Create a new `MainMenuState`
pub fn new(window: &mut Window) -> Self {
Self {
main_menu_ui: MainMenuUi::new(window),
}
}
}
// Background colour
const BG_COLOR: Rgba<f32> = Rgba {
r: 0.0,
g: 0.3,
b: 1.0,
a: 1.0,
};
impl PlayState for MainMenuState {
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
// Set up an fps clock
let mut clock = Clock::new();
loop {
// Handle window events
for event in global_state.window.fetch_events() {
match event {
Event::Close => return PlayStateResult::Shutdown,
// Pass events to ui
Event::Ui(event) => {
self.main_menu_ui.handle_event(event);
}
// Ignore all other events
_ => {}
}
}
global_state.window.renderer_mut().clear(BG_COLOR);
// Maintain the UI
for event in self.main_menu_ui.maintain(global_state.window.renderer_mut()) {
match event {
MainMenuEvent::LoginAttempt{ username, server_address } =>
// For now just start a new session
return PlayStateResult::Push(
Box::new(CharSelectionState::new(&mut global_state.window))
),
MainMenuEvent::Quit => return PlayStateResult::Shutdown,
}
}
// Draw the UI to the screen
self.main_menu_ui.render(global_state.window.renderer_mut());
// Finish the frame
global_state.window.renderer_mut().flush();
global_state
.window
.swap_buffers()
.expect("Failed to swap window buffers");
// Wait for the next tick
clock.tick(Duration::from_millis(1000 / FPS));
}
}
fn name(&self) -> &'static str {
"Title"
}
}

307
voxygen/src/menu/main/ui.rs Normal file
View File

@ -0,0 +1,307 @@
use crate::{
render::Renderer,
ui::{self, ScaleMode, Ui},
window::Window,
};
use conrod_core::{
color::TRANSPARENT,
image::Id as ImgId,
text::font::Id as FontId,
widget::{text_box::Event as TextBoxEvent, Button, Image, TextBox},
widget_ids, Borderable, Color,
Colorable, Labelable, Positionable, Sizeable, Widget,
};
widget_ids! {
struct Ids {
// Background and logo
bg,
v_logo,
alpha_version,
// Login, Singleplayer
login_button,
login_text,
address_text,
address_bg,
address_field,
username_text,
username_bg,
username_field,
singleplayer_button,
singleplayer_text,
// Buttons
servers_button,
settings_button,
quit_button,
}
}
struct Imgs {
bg: ImgId,
v_logo: ImgId,
input_bg: ImgId,
login_button: ImgId,
login_button_hover: ImgId,
login_button_press: ImgId,
button: ImgId,
button_hover: ImgId,
button_press: ImgId,
}
impl Imgs {
fn new(ui: &mut Ui, renderer: &mut Renderer) -> Imgs {
// TODO: update paths
let mut load = |filename| {
let image = image::open(
&[
env!("CARGO_MANIFEST_DIR"),
"/../assets/voxygen/",
filename,
]
.concat(),
)
.unwrap();
ui.new_image(renderer, &image).unwrap()
};
Imgs {
bg: load("background/bg_main.png"),
v_logo: load("element/v_logo.png"),
// Input fields
input_bg: load("element/misc_backgrounds/textbox.png"),
// Login button
login_button: load("element/buttons/button_login.png"),
login_button_hover: load("element/buttons/button_login_hover.png"),
login_button_press: load("element/buttons/button_login_press.png"),
// Servers, settings, and quit buttons
button: load("element/buttons/button.png"),
button_hover: load("element/buttons/button_hover.png"),
button_press: load("element/buttons/button_press.png"),
}
}
}
pub enum Event {
LoginAttempt {
username: String,
server_address: String,
},
Quit,
}
pub struct MainMenuUi {
ui: Ui,
ids: Ids,
imgs: Imgs,
font_metamorph: FontId,
font_opensans: FontId,
username: String,
server_address: String,
}
impl MainMenuUi {
pub fn new(window: &mut Window) -> Self {
let mut ui = Ui::new(window).unwrap();
// TODO: adjust/remove this, right now it is used to demonstrate window scaling functionality
ui.scaling_mode(ScaleMode::RelativeToWindow([1920.0, 1080.0].into()));
// Generate ids
let ids = Ids::new(ui.id_generator());
// Load images
let imgs = Imgs::new(&mut ui, window.renderer_mut());
// Load fonts
let font_opensans = ui.new_font(
conrod_core::text::font::from_file(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../assets/voxygen/font/OpenSans-Regular.ttf"
))
.unwrap(),
);
let font_metamorph = ui.new_font(
conrod_core::text::font::from_file(concat!(
env!("CARGO_MANIFEST_DIR"),
"/../assets/voxygen/font/Metamorphous-Regular.ttf"
))
.unwrap(),
);
Self {
ui,
imgs,
ids,
font_metamorph,
font_opensans,
username: "Username".to_string(),
server_address: "Server Address".to_string(),
}
}
fn update_layout(&mut self) -> Vec<Event> {
let mut events = Vec::new();
let ref mut ui_widgets = self.ui.set_widgets();
// Background image, Veloren logo, Alpha-Version Label
Image::new(self.imgs.bg)
.middle_of(ui_widgets.window)
.set(self.ids.bg, ui_widgets);
Button::image(self.imgs.v_logo)
.w_h(346.0, 111.0)
.top_left_with_margins(30.0, 40.0)
.label("Alpha 0.1")
.label_rgba(255.0, 255.0, 255.0, 1.0)
.label_font_size(10)
.label_y(conrod_core::position::Relative::Scalar(-40.0))
.label_x(conrod_core::position::Relative::Scalar(-100.0))
.set(self.ids.v_logo, ui_widgets);
// Input fields
// Used when the login button is pressed, or enter is pressed within input field
macro_rules! login {
() => {
events.push(Event::LoginAttempt {
username: self.username.clone(),
server_address: self.server_address.clone(),
});
};
}
// Username
// TODO: get a lower resolution and cleaner input_bg.png
Image::new(self.imgs.input_bg)
.w_h(337.0, 67.0)
.middle_of(ui_widgets.window)
.set(self.ids.username_bg, ui_widgets);
for event in TextBox::new(&self.username)
.w_h(580.0 / 2.0, 60.0 / 2.0)
.mid_bottom_with_margin_on(self.ids.username_bg, 44.0 / 2.0)
.font_size(20)
.font_id(self.font_opensans)
.text_color(Color::Rgba(220.0, 220.0, 220.0, 0.8))
// transparent background
.color(TRANSPARENT)
.border_color(TRANSPARENT)
.set(self.ids.username_field, ui_widgets)
{
match event {
TextBoxEvent::Update(username) => {
// Note: TextBox limits the input string length to what fits in it
self.username = username.to_string();
}
TextBoxEvent::Enter => login!(),
}
}
// Server address
Image::new(self.imgs.input_bg)
.w_h(337.0, 67.0)
.down_from(self.ids.username_bg, 10.0)
.set(self.ids.address_bg, ui_widgets);
for event in TextBox::new(&self.server_address)
.w_h(580.0 / 2.0, 60.0 / 2.0)
.mid_bottom_with_margin_on(self.ids.address_bg, 44.0 / 2.0)
.font_size(20)
.font_id(self.font_opensans)
.text_color(Color::Rgba(220.0, 220.0, 220.0, 0.8))
// transparent background
.color(TRANSPARENT)
.border_color(TRANSPARENT)
.set(self.ids.address_field, ui_widgets)
{
match event {
TextBoxEvent::Update(server_address) => {
self.server_address = server_address.to_string();
}
TextBoxEvent::Enter => login!(),
}
}
// Login button
if Button::image(self.imgs.login_button)
.hover_image(self.imgs.login_button_hover)
.press_image(self.imgs.login_button_press)
.w_h(258.0, 68.0)
.down_from(self.ids.address_bg, 20.0)
.align_middle_x_of(self.ids.address_bg)
.label("Login")
.label_rgba(220.0, 220.0, 220.0, 0.8)
.label_font_size(28)
.label_y(conrod_core::position::Relative::Scalar(5.0))
.set(self.ids.login_button, ui_widgets)
.was_clicked()
{
login!();
}
//Singleplayer button
if Button::image(self.imgs.login_button)
.hover_image(self.imgs.login_button_hover)
.press_image(self.imgs.login_button_press)
.w_h(258.0, 68.0)
.down_from(self.ids.login_button, 20.0)
.align_middle_x_of(self.ids.address_bg)
.label("Singleplayer")
.label_rgba(220.0, 220.0, 220.0, 0.8)
.label_font_size(26)
.label_y(conrod_core::position::Relative::Scalar(5.0))
.label_x(conrod_core::position::Relative::Scalar(2.0))
.set(self.ids.singleplayer_button, ui_widgets)
.was_clicked()
{
login!();
}
// Quit
if Button::image(self.imgs.button)
.w_h(203.0, 53.0)
.bottom_left_with_margins_on(ui_widgets.window, 60.0, 30.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label("Quit")
.label_rgba(220.0, 220.0, 220.0, 0.8)
.label_font_size(20)
.label_y(conrod_core::position::Relative::Scalar(3.0))
.set(self.ids.quit_button, ui_widgets)
.was_clicked()
{
events.push(Event::Quit);
};
// Settings
if Button::image(self.imgs.button)
.w_h(203.0, 53.0)
.up_from(self.ids.quit_button, 8.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label("Settings")
.label_rgba(220.0, 220.0, 220.0, 0.8)
.label_font_size(20)
.label_y(conrod_core::position::Relative::Scalar(3.0))
.set(self.ids.settings_button, ui_widgets)
.was_clicked()
{};
// Servers
if Button::image(self.imgs.button)
.w_h(203.0, 53.0)
.up_from(self.ids.settings_button, 8.0)
.hover_image(self.imgs.button_hover)
.press_image(self.imgs.button_press)
.label("Servers")
.label_rgba(220.0, 220.0, 220.0, 0.8)
.label_font_size(20)
.label_y(conrod_core::position::Relative::Scalar(3.0))
.set(self.ids.servers_button, ui_widgets)
.was_clicked()
{};
events
}
pub fn handle_event(&mut self, event: ui::Event) {
self.ui.handle_event(event);
}
pub fn maintain(&mut self, renderer: &mut Renderer) -> Vec<Event> {
let events = self.update_layout();
self.ui.maintain(renderer);
events
}
pub fn render(&self, renderer: &mut Renderer) {
self.ui.render(renderer);
}
}

View File

@ -1 +1,2 @@
pub mod title;
pub mod char_selection;
pub mod main;

View File

@ -1,74 +0,0 @@
// Library
use vek::*;
use image;
// Crate
use crate::{
PlayState,
PlayStateResult,
GlobalState,
window::Event,
session::SessionState,
render::Renderer,
ui::{
Ui,
element::{
Widget,
image::Image,
},
},
};
pub struct TitleState {
ui: Ui,
}
impl TitleState {
/// Create a new `TitleState`
pub fn new(renderer: &mut Renderer) -> Self {
let img = Image::new(renderer, &image::open(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/test.png")).unwrap()).unwrap();
let widget = Widget::new(renderer, img).unwrap();
Self {
ui: Ui::new(renderer, widget).unwrap(),
}
}
}
// The background colour
const BG_COLOR: Rgba<f32> = Rgba { r: 0.0, g: 0.3, b: 1.0, a: 1.0 };
impl PlayState for TitleState {
fn play(&mut self, global_state: &mut GlobalState) -> PlayStateResult {
loop {
// Handle window events
for event in global_state.window.fetch_events() {
match event {
Event::Close => return PlayStateResult::Shutdown,
// When space is pressed, start a session
Event::Char(' ') => return PlayStateResult::Push(
Box::new(SessionState::new(global_state.window.renderer_mut()).unwrap()), // TODO: Handle this error
),
// Ignore all other events
_ => {},
}
}
// Clear the screen
global_state.window.renderer_mut().clear(BG_COLOR);
// Maintain the UI
self.ui.maintain(global_state.window.renderer_mut());
// Draw the UI to the screen
self.ui.render(global_state.window.renderer_mut());
// Finish the frame
global_state.window.renderer_mut().flush();
global_state.window
.swap_buffers()
.expect("Failed to swap window buffers");
}
}
fn name(&self) -> &'static str { "Title" }
}

View File

@ -13,6 +13,11 @@ impl<P: Pipeline> Mesh<P> {
Self { verts: vec![] }
}
/// Clear vertices, allows reusing allocated memory of the underlying Vec
pub fn clear(&mut self) {
self.verts.clear();
}
/// Get a slice referencing the vertices of this mesh.
pub fn vertices(&self) -> &[P::Vertex] {
&self.verts

View File

@ -30,9 +30,10 @@ pub use self::{
Locals as TerrainLocals,
},
ui::{
create_quad_mesh as create_ui_quad_mesh,
create_quad as create_ui_quad,
create_tri as create_ui_tri,
Mode as UiMode,
UiPipeline,
Locals as UiLocals,
},
},
};
@ -48,6 +49,7 @@ use gfx;
pub enum RenderError {
PipelineError(gfx::PipelineStateError<String>),
UpdateError(gfx::UpdateError<usize>),
TexUpdateError(gfx::UpdateError<[u16; 3]>),
CombinedError(gfx::CombinedError),
}

View File

@ -1,73 +1,109 @@
// Library
use gfx::{
self,
// Macros
gfx_defines,
gfx_vertex_struct_meta,
gfx_constant_struct_meta,
gfx_impl_struct_meta,
gfx_pipeline,
gfx_pipeline_inner,
};
// Local
use vek::*;
use super::super::{
Pipeline,
TgtColorFmt,
TgtDepthFmt,
Mesh,
Quad,
Pipeline,
TgtColorFmt,
TgtDepthFmt,
Mesh,
Quad,
Tri,
};
gfx_defines! {
vertex Vertex {
pos: [f32; 3] = "v_pos",
pos: [f32; 2] = "v_pos",
uv: [f32; 2] = "v_uv",
}
constant Locals {
bounds: [f32; 4] = "bounds",
color: [f32; 4] = "v_color",
mode: u32 = "v_mode",
}
pipeline pipe {
vbuf: gfx::VertexBuffer<Vertex> = (),
locals: gfx::ConstantBuffer<Locals> = "u_locals",
tex: gfx::TextureSampler<[f32; 4]> = "u_tex",
tgt_color: gfx::RenderTarget<TgtColorFmt> = "tgt_color",
scissor: gfx::Scissor = (),
tgt_color: gfx::BlendTarget<TgtColorFmt> = ("tgt_color", gfx::state::ColorMask::all(), gfx::preset::blend::ALPHA),
tgt_depth: gfx::DepthTarget<TgtDepthFmt> = gfx::preset::depth::PASS_TEST,
}
}
impl Locals {
pub fn default() -> Self {
Self { bounds: [0.0, 0.0, 1.0, 1.0] }
}
pub fn new(bounds: [f32; 4]) -> Self {
Self {
bounds,
}
}
}
pub struct UiPipeline;
impl Pipeline for UiPipeline {
type Vertex = Vertex;
}
pub fn create_quad_mesh() -> Mesh<UiPipeline> {
let mut mesh = Mesh::new();
/// Draw text from the text cache texture `tex` in the fragment shader.
pub const MODE_TEXT: u32 = 0;
/// Draw an image from the texture at `tex` in the fragment shader.
pub const MODE_IMAGE: u32 = 1;
/// Ignore `tex` and draw simple, colored 2D geometry.
pub const MODE_GEOMETRY: u32 = 2;
#[rustfmt::skip]
mesh.push_quad(Quad::new(
Vertex { pos: [0.0, 0.0, 0.0], uv: [0.0, 0.0] },
Vertex { pos: [0.0, 1.0, 0.0], uv: [0.0, 1.0] },
Vertex { pos: [1.0, 1.0, 0.0], uv: [1.0, 1.0] },
Vertex { pos: [1.0, 0.0, 0.0], uv: [1.0, 0.0] },
));
mesh
pub enum Mode {
Text,
Image,
Geometry,
}
impl Mode {
fn value(self) -> u32 {
match self {
Mode::Text => MODE_TEXT,
Mode::Image => MODE_IMAGE,
Mode::Geometry => MODE_GEOMETRY,
}
}
}
pub fn create_quad(rect: Aabr<f32>, uv_rect: Aabr<f32>, color: [f32; 4], mode: Mode) -> Quad<UiPipeline> {
let mode_val = mode.value();
let v = |pos, uv| {
Vertex {
pos,
uv,
color,
mode: mode_val,
}
};
let aabr_to_lbrt = |aabr: Aabr<f32>| (
aabr.min.x, aabr.min.y,
aabr.max.x, aabr.max.y,
);
let (l, b, r, t) = aabr_to_lbrt(rect);
let (uv_l, uv_b, uv_r, uv_t) = aabr_to_lbrt(uv_rect);
Quad::new(
v([r, t], [uv_r, uv_t]),
v([l, t], [uv_l, uv_t]),
v([l, b], [uv_l, uv_b]),
v([r, b], [uv_r, uv_b]),
)
}
pub fn create_tri(tri: [[f32; 2]; 3], uv_tri: [[f32; 2]; 3], color: [f32; 4], mode: Mode) -> Tri<UiPipeline> {
let mode_val = mode.value();
let v = |pos, uv| {
Vertex {
pos,
uv,
color,
mode: mode_val,
}
};
Tri::new(
v([tri[0][0], tri[0][1]], [uv_tri[0][0], uv_tri[0][1]]),
v([tri[1][0], tri[1][1]], [uv_tri[1][0], uv_tri[1][1]]),
v([tri[2][0], tri[2][1]], [uv_tri[2][0], uv_tri[2][1]]),
)
}

View File

@ -1,12 +1,9 @@
// Library
use vek::*;
use gfx::{
self,
traits::{Device, FactoryExt},
};
use image;
// Local
use super::{
consts::Consts,
mesh::Mesh,
@ -173,6 +170,34 @@ impl Renderer {
)
}
/// Create a new dynamic texture (gfx::memory::Usage::Dynamic) with the specified dimensions
pub fn create_dynamic_texture<P: Pipeline>(
&mut self,
dims: Vec2<u16>
) -> Result<Texture<P>, RenderError> {
Texture::new_dynamic(
&mut self.factory,
dims.x,
dims.y,
)
}
/// Update a texture with the provided offset, size, and data
pub fn update_texture<P: Pipeline>(
&mut self,
texture: &Texture<P>,
offset: [u16; 2],
size: [u16; 2],
data: &[[u8; 4]]
) -> Result<(), RenderError> {
texture.update(
&mut self.encoder,
offset,
size,
data,
)
}
/// Queue the rendering of the provided skybox model in the upcoming frame.
pub fn render_skybox(
&mut self,
@ -239,15 +264,16 @@ impl Renderer {
pub fn render_ui_element(
&mut self,
model: &Model<ui::UiPipeline>,
locals: &Consts<ui::Locals>,
tex: &Texture<ui::UiPipeline>,
scissor: Aabr<u16>,
) {
let Aabr { min, max } = scissor;
self.encoder.draw(
&model.slice,
&self.ui_pipeline.pso,
&ui::pipe::Data {
vbuf: model.vbuf.clone(),
locals: locals.buf.clone(),
scissor: gfx::Rect { x: min.x, y: min.y, w: max.x - min.x, h: max.y - min.y },
tex: (tex.srv.clone(), tex.sampler.clone()),
tgt_color: self.tgt_color_view.clone(),
tgt_depth: self.tgt_depth_view.clone(),

View File

@ -10,6 +10,7 @@ use image::{
DynamicImage,
GenericImageView,
};
use vek::Vec2;
// Local
use super::{
@ -54,4 +55,64 @@ impl<P: Pipeline> Texture<P> {
_phantom: PhantomData,
})
}
pub fn new_dynamic(
factory: &mut gfx_backend::Factory,
width: u16,
height: u16,
) -> Result<Self, RenderError> {
let tex = factory.create_texture(
gfx::texture::Kind::D2(
width,
height,
gfx::texture::AaMode::Single,
),
1 as gfx::texture::Level,
gfx::memory::Bind::SHADER_RESOURCE,
gfx::memory::Usage::Dynamic,
Some(<<ShaderFormat as gfx::format::Formatted>::Channel as gfx::format::ChannelTyped>::get_channel_type()),
)
.map_err(|err| RenderError::CombinedError(gfx::CombinedError::Texture(err)))?;
let srv =
factory.view_texture_as_shader_resource::<ShaderFormat>(&tex, (0, 0), gfx::format::Swizzle::new())
.map_err(|err| RenderError::CombinedError(gfx::CombinedError::Resource(err)))?;
Ok(Self {
tex,
srv,
sampler: factory.create_sampler(gfx::texture::SamplerInfo::new(
gfx::texture::FilterMethod::Bilinear,
gfx::texture::WrapMode::Clamp,
)),
_phantom: PhantomData,
})
}
// Updates a texture with the given data (used for updating the glyph cache texture)
pub fn update(
&self,
encoder: &mut gfx::Encoder<gfx_backend::Resources, gfx_backend::CommandBuffer>,
offset: [u16; 2],
size: [u16; 2],
data: &[[u8; 4]],
) -> Result<(), RenderError> {
let info = gfx::texture::ImageInfoCommon {
xoffset: offset[0],
yoffset: offset[1],
zoffset: 0,
width: size[0],
height: size[1],
depth: 0,
format: (),
mipmap: 0,
};
encoder
.update_texture::<<ShaderFormat as gfx::format::Formatted>::Surface, ShaderFormat>(&self.tex, None, info, data)
.map_err(|err| RenderError::TexUpdateError(err))
}
/// Get dimensions of the represented image
pub fn get_dimensions(&self) -> Vec2<u16> {
let (w, h, ..) = self.tex.get_info().kind.get_dimensions();
Vec2::new(w, h)
}
}

View File

@ -53,7 +53,7 @@ pub struct Scene {
// TODO: Make a proper asset loading system
fn load_segment(filename: &'static str) -> Segment {
Segment::from(dot_vox::load(&(concat!(env!("CARGO_MANIFEST_DIR"), "/test_assets/").to_string() + filename)).unwrap())
Segment::from(dot_vox::load(&(concat!(env!("CARGO_MANIFEST_DIR"), "/../assets/voxygen/voxel/").to_string() + filename)).unwrap())
}
impl Scene {

View File

@ -18,9 +18,10 @@ use crate::{
PlayStateResult,
GlobalState,
key_state::KeyState,
window::{Event, Key},
window::{Event, Key, Window},
render::Renderer,
scene::Scene,
hud::{Hud, Event as HudEvent},
};
const FPS: u64 = 60;
@ -29,18 +30,20 @@ pub struct SessionState {
scene: Scene,
client: Client,
key_state: KeyState,
hud: Hud,
}
/// Represents an active game session (i.e: one that is being played)
impl SessionState {
/// Create a new `SessionState`
pub fn new(renderer: &mut Renderer) -> Result<Self, Error> {
pub fn new(window: &mut Window) -> Result<Self, Error> {
let client = Client::new(([127, 0, 0, 1], 59003))?.with_test_state(); // <--- TODO: Remove this
Ok(Self {
// Create a scene for this session. The scene handles visible elements of the game world
scene: Scene::new(renderer, &client),
scene: Scene::new(window.renderer_mut(), &client),
client,
key_state: KeyState::new(),
hud: Hud::new(window),
})
}
}
@ -60,7 +63,14 @@ impl SessionState {
let dir_vec = self.key_state.dir_vec();
let move_dir = unit_vecs.0 * dir_vec[0] + unit_vecs.1 * dir_vec[1];
self.client.tick(client::Input { move_dir }, dt)?;
for event in self.client.tick(client::Input { move_dir }, dt)? {
match event {
client::Event::Chat(msg) => {
self.hud.new_message(msg);
}
}
}
Ok(())
}
@ -78,6 +88,8 @@ impl SessionState {
// Render the screen using the global renderer
self.scene.render_to(renderer);
// Draw the UI to the screen
self.hud.render(renderer);
// Finish the frame
renderer.flush();
@ -105,13 +117,23 @@ impl PlayState for SessionState {
loop {
// Handle window events
for event in global_state.window.fetch_events() {
// Pass all events to the ui first
if self.hud.handle_event(event.clone()) {
continue;
}
let _handled = match event {
Event::Close => return PlayStateResult::Shutdown,
// When 'q' is pressed, exit the session
Event::Char('q') => return PlayStateResult::Pop,
// When 'm' is pressed, open/close the in-game test menu
Event::Char('m') => self.hud.toggle_menu(),
// Close windows on esc
Event::KeyDown(Key::Escape) => self.hud.toggle_windows(),
// Toggle cursor grabbing
Event::KeyDown(Key::ToggleCursor) => {
global_state.window.grab_cursor(!global_state.window.is_cursor_grabbed());
self.hud.update_grab(global_state.window.is_cursor_grabbed());
},
// Movement Key Pressed
Event::KeyDown(Key::MoveForward) => self.key_state.up = true,
@ -135,6 +157,17 @@ impl PlayState for SessionState {
// Maintain the scene
self.scene.maintain(global_state.window.renderer_mut(), &self.client);
// Maintain the UI
for event in self.hud.maintain(global_state.window.renderer_mut()) {
match event {
HudEvent::SendMessage(msg) => {
// TODO: Handle result
self.client.send_chat(msg);
},
HudEvent::Logout => return PlayStateResult::Pop,
HudEvent::Quit => return PlayStateResult::Shutdown,
}
}
// Render the session
self.render(global_state.window.renderer_mut());

View File

@ -1,77 +0,0 @@
// Standard
use std::rc::Rc;
// Library
use image::DynamicImage;
use vek::*;
// Crate
use crate::render::{
Consts,
UiLocals,
Renderer,
Texture,
UiPipeline,
};
// Local
use super::{
super::{
UiError,
Cache,
},
Element,
Bounds,
SizeRequest,
};
#[derive(Clone)]
pub struct Image {
texture: Rc<Texture<UiPipeline>>,
locals: Consts<UiLocals>,
}
impl Image {
pub fn new(renderer: &mut Renderer, image: &DynamicImage) -> Result<Self, UiError> {
Ok(Self {
texture: Rc::new(
renderer.create_texture(image)
.map_err(|err| UiError::RenderError(err))?
),
locals: renderer.create_consts(&[UiLocals::default()])
.map_err(|err| UiError::RenderError(err))?,
})
}
}
impl Element for Image {
fn get_hsize_request(&self) -> SizeRequest { SizeRequest::indifferent() }
fn get_vsize_request(&self) -> SizeRequest { SizeRequest::indifferent() }
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.update_consts(&mut self.locals, &[UiLocals::new(
[bounds.x, bounds.y, bounds.w, bounds.h],
)])
.expect("Could not update UI image consts");
}
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.render_ui_element(
cache.model(),
&self.locals,
&self.texture,
);
}
}

View File

@ -1,181 +0,0 @@
pub mod image;
// Standard
use std::rc::Rc;
// Library
use vek::*;
// Crate
use crate::render::{
Renderer,
Texture,
Consts,
UiLocals,
UiPipeline,
};
// Local
use super::{
UiError,
Cache,
Span,
SizeRequest,
};
// Bounds
pub type Bounds<T> = Rect<T, T>;
pub trait BoundsExt {
fn relative_to(self, other: Self) -> Self;
}
impl BoundsExt for Bounds<f32> {
fn relative_to(self, other: Self) -> Self {
Self::new(
other.x + self.x * other.w,
other.y + self.y * other.h,
self.w * other.w,
self.h * other.h,
)
}
}
pub trait BoundsSpan {
fn in_resolution(self, resolution: Vec2<f32>) -> Bounds<f32>;
}
impl BoundsSpan for Bounds<Span> {
fn in_resolution(self, resolution: Vec2<f32>) -> Bounds<f32> {
Bounds::new(
self.x.to_rel(resolution.x).rel,
self.y.to_rel(resolution.y).rel,
self.w.to_rel(resolution.x).rel,
self.h.to_rel(resolution.y).rel,
)
}
}
// Element
pub trait Element: 'static {
//fn deep_clone(&self) -> Rc<dyn Element>;
fn get_hsize_request(&self) -> SizeRequest;
fn get_vsize_request(&self) -> SizeRequest;
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
);
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
);
}
// Surface
#[derive(Clone)]
pub enum Surface {
Transparent,
Color(Rgba<f32>),
Texture(Rc<Texture<UiPipeline>>),
Bevel,
}
// Widget
#[derive(Clone)]
pub struct Widget<E: Element> {
inner: Box<E>,
background: Surface,
margin_top: Span,
margin_bottom: Span,
margin_left: Span,
margin_right: Span,
locals: Consts<UiLocals>,
}
impl<E: Element> Widget<E> {
pub fn new(renderer: &mut Renderer, inner: E) -> Result<Box<Self>, UiError> {
Ok(Box::new(Self {
inner: Box::new(inner),
background: Surface::Transparent,
margin_top: Span::rel(0.2),
margin_bottom: Span::rel(0.2),
margin_left: Span::rel(0.2),
margin_right: Span::rel(0.2),
locals: renderer.create_consts(&[UiLocals::default()])
.map_err(|err| UiError::RenderError(err))?,
}))
}
fn get_inner_bounds(&self) -> Bounds<Span> {
Bounds::new(
self.margin_left,
self.margin_top,
Span::full() - self.margin_left - self.margin_right,
Span::full() - self.margin_top - self.margin_bottom,
)
}
}
impl<E: Element> Element for Widget<E> {
fn get_hsize_request(&self) -> SizeRequest {
self.inner.get_hsize_request() + self.margin_left + self.margin_right
}
fn get_vsize_request(&self) -> SizeRequest {
self.inner.get_vsize_request() + self.margin_top + self.margin_bottom
}
fn maintain(
&mut self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.update_consts(&mut self.locals, &[UiLocals::new(
[bounds.x, bounds.y, bounds.w, bounds.h],
)])
.expect("Could not update UI image consts");
let inner_bounds = self
.get_inner_bounds()
.in_resolution(resolution)
.relative_to(bounds);
self.inner.maintain(renderer, cache, inner_bounds, resolution);
}
fn render(
&self,
renderer: &mut Renderer,
cache: &Cache,
bounds: Bounds<f32>,
resolution: Vec2<f32>,
) {
renderer.render_ui_element(
cache.model(),
&self.locals,
&cache.blank_texture(),
);
let inner_bounds = self
.get_inner_bounds()
.in_resolution(resolution)
.relative_to(bounds);
self.inner.render(renderer, cache, inner_bounds, resolution);
}
}

View File

@ -1,85 +1,579 @@
pub mod element;
pub mod size_request;
pub mod span;
mod widgets;
// Reexports
pub use self::{
span::Span,
size_request::SizeRequest,
};
pub use widgets::toggle_button::ToggleButton;
// Library
use image::DynamicImage;
// Crate
use conrod_core::{
Ui as CrUi,
UiBuilder,
UiCell,
text::{
Font,
GlyphCache,
font::Id as FontId,
},
image::{Map, Id as ImgId},
widget::{Id as WidgId, id::Generator},
render::Primitive,
event::Input,
input::{touch::Touch, Widget, Motion, Button, MouseButton},
};
use vek::*;
use crate::{
Error,
render::{
RenderError,
Renderer,
Model,
Mesh,
Texture,
UiPipeline,
create_ui_quad_mesh,
UiMode,
create_ui_quad,
create_ui_tri,
},
};
// Local
use self::element::{
Element,
Bounds,
window::Window,
};
#[derive(Debug)]
pub enum UiError {
RenderError(RenderError),
}
pub struct Cache {
model: Model<UiPipeline>,
blank_texture: Texture<UiPipeline>,
}
impl Cache {
pub fn new(renderer: &mut Renderer) -> Result<Self, Error> {
Ok(Self {
model: renderer.create_model(&create_ui_quad_mesh())?,
blank_texture: renderer.create_texture(&DynamicImage::new_rgba8(1, 1))?,
#[derive(Clone)]
pub struct Event(Input);
impl Event {
pub fn try_from(event: glutin::Event, window: &glutin::GlWindow) -> Option<Self> {
conrod_winit::convert_event(event, window.window()).map(|input| {
Self(input)
})
}
pub fn is_keyboard_or_mouse(&self) -> bool {
match self.0 {
Input::Press(_) | Input::Release(_) | Input::Motion(_) | Input::Touch(_) | Input::Text(_) => true,
_ => false,
}
}
pub fn is_keyboard(&self) -> bool {
match self.0 {
Input::Press(Button::Keyboard(_)) | Input::Release(Button::Keyboard(_)) | Input::Text(_) => true,
_ => false,
}
}
pub fn new_resize(dims: Vec2<f64>) -> Self {
Self(Input::Resize(dims.x, dims.y))
}
}
pub fn model(&self) -> &Model<UiPipeline> { &self.model }
pub struct Cache {
blank_texture: Texture<UiPipeline>,
glyph_cache: GlyphCache<'static>,
glyph_cache_tex: Texture<UiPipeline>,
}
// TODO: Should functions be returning UiError instead of Error?
impl Cache {
pub fn new(renderer: &mut Renderer) -> Result<Self, Error> {
let (w, h) = renderer.get_resolution().into_tuple();
const SCALE_TOLERANCE: f32 = 0.1;
const POSITION_TOLERANCE: f32 = 0.1;
Ok(Self {
blank_texture: renderer.create_texture(&DynamicImage::new_rgba8(1, 1))?,
glyph_cache: GlyphCache::builder()
.dimensions(w as u32, h as u32)
.scale_tolerance(SCALE_TOLERANCE)
.position_tolerance(POSITION_TOLERANCE)
.build(),
glyph_cache_tex: renderer.create_dynamic_texture((w, h).into())?,
})
}
pub fn blank_texture(&self) -> &Texture<UiPipeline> { &self.blank_texture }
pub fn glyph_cache_tex(&self) -> &Texture<UiPipeline> { &self.glyph_cache_tex }
pub fn glyph_cache_mut_and_tex(&mut self) -> (&mut GlyphCache<'static>, &Texture<UiPipeline>) { (&mut self.glyph_cache, &self.glyph_cache_tex) }
}
enum DrawKind {
Image(ImgId),
// Text and non-textured geometry
Plain,
}
enum DrawCommand {
Draw {
kind: DrawKind,
model: Model<UiPipeline>,
},
Scissor(Aabr<u16>),
}
impl DrawCommand {
fn image(model: Model<UiPipeline>, img_id: ImgId) -> DrawCommand {
DrawCommand::Draw {
kind: DrawKind::Image(img_id),
model,
}
}
fn plain(model: Model<UiPipeline>) -> DrawCommand {
DrawCommand::Draw {
kind: DrawKind::Plain,
model,
}
}
}
// How to scale the ui
pub enum ScaleMode {
// Scale against physical size
Absolute(f64),
// Use the dpi factor provided by the windowing system (i.e. use logical size)
DpiFactor,
// Scale based on the window's physical size, but maintain aspect ratio of widgets
// Contains width and height of the "default" window size (ie where there should be no scaling)
RelativeToWindow(Vec2<f64>),
}
struct Scale {
// Type of scaling to use
mode: ScaleMode,
// Current dpi factor
dpi_factor: f64,
// Current logical window size
window_dims: Vec2<f64>,
}
impl Scale {
fn new(window: &Window, mode: ScaleMode) -> Self {
let window_dims = window.logical_size();
let dpi_factor = window.renderer().get_resolution().x as f64 / window_dims.x;
Scale {
mode,
dpi_factor,
window_dims,
}
}
// Change the scaling mode
pub fn scaling_mode(&mut self, mode: ScaleMode) {
self.mode = mode;
}
// Calculate factor to transform between logical coordinates and our scaled coordinates
fn scale_factor_logical(&self) -> f64 {
match self.mode {
ScaleMode::Absolute(scale) => scale / self.dpi_factor,
ScaleMode::DpiFactor => 1.0,
ScaleMode::RelativeToWindow(dims) => (self.window_dims.x / dims.x).min(self.window_dims.y / dims.y),
}
}
// Calculate factor to transform between physical coordinates and our scaled coordinates
fn scale_factor_physical(&self) -> f64 {
self.scale_factor_logical() * self.dpi_factor
}
// Updates internal window size (and/or dpi_factor)
fn window_resized(&mut self, new_dims: Vec2<f64>, renderer: &Renderer) {
self.dpi_factor = renderer.get_resolution().x as f64 / new_dims.x;
self.window_dims = new_dims;
}
// Get scaled window size
fn scaled_window_size(&self) -> Vec2<f64> {
self.window_dims / self.scale_factor_logical()
}
// Transform point from logical to scaled coordinates
fn scale_point(&self, point: Vec2<f64>) -> Vec2<f64> {
point / self.scale_factor_logical()
}
}
pub struct Ui {
base: Box<dyn Element>,
ui: CrUi,
image_map: Map<Texture<UiPipeline>>,
cache: Cache,
// Draw commands for the next render
draw_commands: Vec<DrawCommand>,
// Stores new window size for updating scaling
window_resized: Option<Vec2<f64>>,
// Scaling of the ui
scale: Scale,
}
impl Ui {
pub fn new<E: Element>(renderer: &mut Renderer, base: Box<E>) -> Result<Self, Error> {
pub fn new(window: &mut Window) -> Result<Self, Error> {
let scale = Scale::new(window, ScaleMode::Absolute(1.0));
let win_dims = scale.scaled_window_size().into_array();
Ok(Self {
base,
cache: Cache::new(renderer)?,
ui: UiBuilder::new(win_dims).build(),
image_map: Map::new(),
cache: Cache::new(window.renderer_mut())?,
window_resized: None,
draw_commands: vec![],
scale,
})
}
// Set the scaling mode of the ui
pub fn scaling_mode(&mut self, mode: ScaleMode) {
self.scale.scaling_mode(mode);
// Give conrod the new size
let (w, h) = self.scale.scaled_window_size().into_tuple();
self.ui.handle_event(Input::Resize(w, h));
}
pub fn new_image(&mut self, renderer: &mut Renderer, image: &DynamicImage) -> Result<ImgId, Error> {
Ok(self.image_map.insert(renderer.create_texture(image)?))
}
pub fn new_font(&mut self, font: Font) -> FontId {
self.ui.fonts.insert(font)
}
pub fn id_generator(&mut self) -> Generator {
self.ui.widget_id_generator()
}
pub fn set_widgets(&mut self) -> UiCell {
self.ui.set_widgets()
}
// Workaround because conrod currently gives us no way to programmatically set which widget is capturing key input
// Note the widget must be visible and not covered by other widgets at its center for this to work
// Accepts option so widget can be "unfocused"
pub fn focus_widget(&mut self, id: Option<WidgId>) {
let (x, y) = match id {
// get position of widget
Some(id) => if let Some([x, y]) = self.ui.xy_of(id) {
(x, y)
} else {
return;
},
// outside window (origin is center)
None => (self.ui.win_h, self.ui.win_w)
};
// things to consider: does cursor need to be moved back?, should we check if the mouse if already pressed and then trigger a release?
self.ui.handle_event(
Input::Motion(Motion::MouseCursor { x, y })
);
self.ui.handle_event(
Input::Press(Button::Mouse(MouseButton::Left))
);
self.ui.handle_event(
Input::Release(Button::Mouse(MouseButton::Left))
);
}
pub fn handle_event(&mut self, event: Event) {
match event.0 {
Input::Resize(w, h) => self.window_resized = Some(Vec2::new(w, h)),
Input::Touch(touch) => self.ui.handle_event(
Input::Touch(Touch {
xy: self.scale.scale_point(touch.xy.into()).into_array(),
..touch
})
),
Input::Motion(motion) => self.ui.handle_event(
Input::Motion( match motion {
Motion::MouseCursor { x, y } => {
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
Motion::MouseCursor { x, y }
}
Motion::MouseRelative { x, y } => {
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
Motion::MouseRelative { x, y }
}
Motion::Scroll { x, y } => {
let (x, y) = self.scale.scale_point(Vec2::new(x, y)).into_tuple();
Motion::Scroll { x, y }
}
_ => motion,
})
),
_ => self.ui.handle_event(event.0),
}
}
pub fn widget_input(&self, id: WidgId) -> Widget {
self.ui.widget_input(id)
}
pub fn maintain(&mut self, renderer: &mut Renderer) {
self.base.maintain(
renderer,
&self.cache,
Bounds::new(0.0, 0.0, 1.0, 1.0),
renderer.get_resolution().map(|e| e as f32),
)
let ref mut ui = self.ui;
// Regenerate draw commands and associated models only if the ui changed
if let Some(mut primitives) = ui.draw_if_changed() {
self.draw_commands.clear();
let mut mesh = Mesh::new();
let mut current_img = None;
let window_scizzor = default_scissor(renderer);
let mut current_scizzor = window_scizzor;
// Switches to the `Plain` state and completes the previous `Command` if not already in the
// `Plain` state.
macro_rules! switch_to_plain_state {
() => {
if let Some(image_id) = current_img.take() {
self.draw_commands.push(DrawCommand::image(renderer.create_model(&mesh).unwrap(), image_id));
mesh.clear();
}
};
}
let p_scale_factor = self.scale.scale_factor_physical();
while let Some(prim) = primitives.next() {
let Primitive {kind, scizzor, id, rect} = prim;
// Check for a change in the scizzor
let new_scizzor = {
let (l, b, w, h) = scizzor.l_b_w_h();
// Calculate minimum x and y coordinates while
// - flipping y axis (from +up to +down)
// - moving origin to top-left corner (from middle)
let min_x = ui.win_w / 2.0 + l;
let min_y = ui.win_h / 2.0 - b - h;
Aabr {
min: Vec2 {
x: (min_x * p_scale_factor) as u16,
y: (min_y * p_scale_factor) as u16,
},
max: Vec2 {
x: ((min_x + w) * p_scale_factor) as u16,
y: ((min_y + h) * p_scale_factor) as u16,
}
}
.intersection(window_scizzor)
};
if new_scizzor != current_scizzor {
// Finish the current command
match current_img.take() {
None =>
self.draw_commands.push(DrawCommand::plain(renderer.create_model(&mesh).unwrap())),
Some(image_id) =>
self.draw_commands.push(DrawCommand::image(renderer.create_model(&mesh).unwrap(), image_id)),
}
mesh.clear();
// Update the scizzor and produce a command.
current_scizzor = new_scizzor;
self.draw_commands.push(DrawCommand::Scissor(new_scizzor));
}
// Functions for converting for conrod scalar coords to GL vertex coords (-1.0 to 1.0)
let vx = |x: f64| (x / ui.win_w * 2.0) as f32;
let vy = |y: f64| (y / ui.win_h * 2.0) as f32;
let gl_aabr = |rect: conrod_core::Rect| {
let (l, r, b, t) = rect.l_r_b_t();
Aabr {
min: Vec2::new(vx(l), vy(b)),
max: Vec2::new(vx(r), vy(t)),
}
};
use conrod_core::render::PrimitiveKind;
match kind {
// TODO: use source_rect
PrimitiveKind::Image { image_id, color, source_rect } => {
// Switch to the `Image` state for this image if we're not in it already.
let new_image_id = image_id;
match current_img {
// If we're already in the drawing mode for this image, we're done.
Some(image_id) if image_id == new_image_id => (),
// If we were in the `Plain` drawing state, switch to Image drawing state.
None => {
self.draw_commands.push(DrawCommand::plain(renderer.create_model(&mesh).unwrap()));
mesh.clear();
current_img = Some(new_image_id);
}
// If we were drawing a different image, switch state to draw *this* image.
Some(image_id) => {
self.draw_commands.push(DrawCommand::image(renderer.create_model(&mesh).unwrap(), image_id));
mesh.clear();
current_img = Some(new_image_id);
}
}
let color = color.unwrap_or(conrod_core::color::WHITE).to_fsa();
// Transform the source rectangle into uv coordinates
let (image_w, image_h) = self.image_map
.get(&image_id)
.expect("Image does not exist in image map")
.get_dimensions()
.map(|e| e as f64)
.into_tuple();
let (uv_l, uv_r, uv_t, uv_b) = match source_rect {
Some(src_rect) => {
let (l, r, b, t) = src_rect.l_r_b_t();
((l / image_w) as f32,
(r / image_w) as f32,
(b / image_h) as f32,
(t / image_h) as f32)
}
None => (0.0, 1.0, 0.0, 1.0),
};
let uv = Aabr {
min: Vec2::new(uv_l, uv_b),
max: Vec2::new(uv_r, uv_t),
};
mesh.push_quad(create_ui_quad(
gl_aabr(rect),
uv,
color,
UiMode::Image,
));
}
PrimitiveKind::Text { color, text, font_id } => {
switch_to_plain_state!();
// Get screen width and height
let (screen_w, screen_h) = renderer.get_resolution().map(|e| e as f32).into_tuple();
// Calculate dpi factor
let dpi_factor = screen_w / ui.win_w as f32;
let positioned_glyphs = text.positioned_glyphs(dpi_factor);
let (glyph_cache, cache_tex) = self.cache.glyph_cache_mut_and_tex();
// Queue the glyphs to be cached
for glyph in positioned_glyphs {
glyph_cache.queue_glyph(font_id.index(), glyph.clone());
}
glyph_cache.cache_queued(|rect, data| {
let offset = [rect.min.x as u16, rect.min.y as u16];
let size = [rect.width() as u16, rect.height() as u16];
let new_data = data.iter().map(|x| [255, 255, 255, *x]).collect::<Vec<[u8; 4]>>();
renderer.update_texture(cache_tex, offset, size, &new_data);
}).unwrap();
// TODO: consider gamma....
let color = color.to_fsa();
for g in positioned_glyphs {
if let Ok(Some((uv_rect, screen_rect))) = glyph_cache.rect_for(font_id.index(), g) {
let uv = Aabr {
min: Vec2::new(uv_rect.min.x, uv_rect.max.y),
max: Vec2::new(uv_rect.max.x, uv_rect.min.y),
};
let rect = Aabr {
min: Vec2::new(
(screen_rect.min.x as f32 / screen_w - 0.5) * 2.0,
(screen_rect.max.y as f32 / screen_h - 0.5) * -2.0,
),
max: Vec2::new(
(screen_rect.max.x as f32 / screen_w - 0.5) * 2.0,
(screen_rect.min.y as f32 / screen_h - 0.5) * -2.0,
),
};
mesh.push_quad(create_ui_quad(
rect,
uv,
color,
UiMode::Text,
));
}
}
}
PrimitiveKind::Rectangle { color } => {
// TODO: consider gamma/linear conversion....
let color = color.to_fsa();
// Don't draw a transparent rectangle
if color[3] == 0.0 {
continue;
}
switch_to_plain_state!();
mesh.push_quad(create_ui_quad(
gl_aabr(rect),
Aabr {
min: Vec2::new(0.0, 0.0),
max: Vec2::new(0.0, 0.0),
},
color,
UiMode::Geometry,
));
}
PrimitiveKind::TrianglesSingleColor { color, triangles } => {
// Don't draw transparent triangle or switch state if there are actually no triangles
let color: [f32; 4] = color.into();
if triangles.is_empty() || color[3] == 0.0 {
continue;
}
switch_to_plain_state!();
for tri in triangles {
let p1 = Vec2::new(vx(tri[0][0]), vy(tri[0][1]));
let p2 = Vec2::new(vx(tri[1][0]), vy(tri[1][1]));
let p3 = Vec2::new(vx(tri[2][0]), vy(tri[2][1]));
// If triangle is clockwise reverse it
let (v1, v2): (Vec3<f32>, Vec3<f32>) = ((p2 - p1).into(), (p3 - p1).into());
let triangle = if v1.cross(v2).z > 0.0 {[
p1.into_array(),
p2.into_array(),
p3.into_array(),
]} else {[
p2.into_array(),
p1.into_array(),
p3.into_array(),
]};
mesh.push_tri(create_ui_tri(
triangle,
[[0.0; 2]; 3],
color,
UiMode::Geometry,
));
}
}
_ => {}
// TODO: Add these
//PrimitiveKind::Other {..} => {println!("primitive kind other with id {:?}", id);}
//PrimitiveKind::TrianglesMultiColor {..} => {println!("primitive kind multicolor with id {:?}", id);}
}
}
// Enter the final command
match current_img {
None =>
self.draw_commands.push(DrawCommand::plain(renderer.create_model(&mesh).unwrap())),
Some(image_id) =>
self.draw_commands.push(DrawCommand::image(renderer.create_model(&mesh).unwrap(), image_id)),
}
// Handle window resizing
if let Some(new_dims) = self.window_resized.take() {
self.scale.window_resized(new_dims, renderer);
let (w, h) = self.scale.scaled_window_size().into_tuple();
self.ui.handle_event(Input::Resize(w, h));
}
}
}
pub fn render(&self, renderer: &mut Renderer) {
self.base.render(
renderer,
&self.cache,
Bounds::new(0.0, 0.0, 1.0, 1.0),
renderer.get_resolution().map(|e| e as f32),
);
let mut scissor = default_scissor(renderer);
for draw_command in self.draw_commands.iter() {
match draw_command {
DrawCommand::Scissor(scizzor) => {
scissor = *scizzor;
}
DrawCommand::Draw { kind, model } => {
let tex = match kind {
DrawKind::Image(image_id) => {
self.image_map.get(&image_id).expect("Image does not exist in image map")
}
DrawKind::Plain => {
self.cache.glyph_cache_tex()
}
};
renderer.render_ui_element(&model, &tex, scissor);
}
}
}
}
}
fn default_scissor(renderer: &mut Renderer) -> Aabr<u16> {
let (screen_w, screen_h) = renderer.get_resolution().map(|e| e as u16).into_tuple();
Aabr {
min: Vec2 { x: 0, y: 0 },
max: Vec2 { x: screen_w, y: screen_h }
}
}

View File

@ -1,30 +0,0 @@
// Standard
use std::ops::Add;
// Local
use super::Span;
pub struct SizeRequest {
min: Span,
max: Span,
}
impl SizeRequest {
pub fn indifferent() -> Self {
Self {
min: Span::rel(0.0),
max: Span::rel(std::f32::INFINITY),
}
}
}
impl Add<Span> for SizeRequest {
type Output = Self;
fn add(self, span: Span) -> Self {
Self {
min: self.min + span,
max: self.max + span,
}
}
}

View File

@ -1,47 +0,0 @@
// Standard
use std::ops::{Add, Sub};
#[derive(Copy, Clone)]
pub struct Span {
pub rel: f32,
pub abs: f32,
}
impl Span {
pub fn rel(rel: f32) -> Self { Self { rel, abs: 0.0 } }
pub fn abs(abs: f32) -> Self { Self { rel: 0.0, abs } }
pub fn full() -> Self { Self { rel: 1.0, abs: 0.0 } }
pub fn half() -> Self { Self { rel: 0.5, abs: 0.0 } }
pub fn none() -> Self { Self { rel: 0.0, abs: 0.0 } }
pub fn to_abs(self, res: f32) -> Self {
Self { rel: 0.0, abs: self.rel * res + self.abs }
}
pub fn to_rel(self, res: f32) -> Self {
Self { rel: self.rel + self.abs / res, abs: 0.0 }
}
}
impl Add for Span {
type Output = Self;
fn add(self, other: Self) -> Self {
Self {
rel: self.rel + other.rel,
abs: self.abs + other.abs,
}
}
}
impl Sub for Span {
type Output = Self;
fn sub(self, other: Self) -> Self {
Self {
rel: self.rel - other.rel,
abs: self.abs - other.abs,
}
}
}

View File

@ -0,0 +1 @@
pub mod toggle_button;

View File

@ -0,0 +1,118 @@
use conrod_core::{
widget::{self, button},
image,
WidgetCommon,
Widget,
Sizeable,
Color,
Rect,
Positionable,
widget_ids,
};
#[derive(Clone, WidgetCommon)]
pub struct ToggleButton {
#[conrod(common_builder)]
common: widget::CommonBuilder,
value: bool,
f_image: button::Image,
t_image: button::Image,
}
widget_ids! {
struct Ids {
button,
}
}
pub struct State {
ids: Ids,
}
impl ToggleButton {
pub fn new(value: bool, f_image_id: image::Id, t_image_id: image::Id) -> Self {
ToggleButton {
common: widget::CommonBuilder::default(),
value,
f_image: button::Image {
image_id: f_image_id,
hover_image_id: None,
press_image_id: None,
src_rect: None,
color: button::ImageColor::None,
},
t_image: button::Image {
image_id: t_image_id,
hover_image_id: None,
press_image_id: None,
src_rect: None,
color: button::ImageColor::None,
},
}
}
pub fn source_rectangle(mut self, rect: Rect) -> Self {
self.f_image.src_rect = Some(rect);
self.t_image.src_rect = Some(rect);
self
}
pub fn image_colors(mut self, f_color: Color, t_color: Color) -> Self {
self.f_image.color = button::ImageColor::Normal(f_color);
self.t_image.color = button::ImageColor::Normal(t_color);
self
}
pub fn image_color_with_feedback(mut self, f_color: Color, t_color: Color) -> Self {
self.f_image.color = button::ImageColor::WithFeedback(f_color);
self.t_image.color = button::ImageColor::WithFeedback(t_color);
self
}
pub fn hover_images(mut self, f_id: image::Id, t_id: image::Id) -> Self {
self.f_image.hover_image_id = Some(f_id);
self.t_image.hover_image_id = Some(t_id);
self
}
pub fn press_images(mut self, f_id: image::Id, t_id: image::Id) -> Self {
self.f_image.press_image_id = Some(f_id);
self.t_image.press_image_id = Some(t_id);
self
}
}
impl Widget for ToggleButton {
type State = State;
type Style = ();
type Event = bool;
fn init_state(&self, id_gen: widget::id::Generator) -> Self::State {
State { ids: Ids::new(id_gen) }
}
fn style(&self) -> Self::Style {
()
}
fn update(self, args: widget::UpdateArgs<Self>) -> Self::Event {
let widget::UpdateArgs{ id, state, ui, rect, .. } = args;
let ToggleButton { mut value, f_image, t_image, .. } = self;
// Check if button was clicked
// (can't use .set().was_clicked() because we are changing the image and this is after setting the widget which causes flickering as it takes a frame to change after the mouse button is lifted)
if ui.widget_input(state.ids.button).clicks().left().count() % 2 == 1 {
value = !value;
}
let image = if value { t_image } else { f_image };
let (x, y, w, h) = rect.x_y_w_h();
// Button
let mut button = button::Button::image(image.image_id)
.x_y(x, y)
.w_h(w, h)
.parent(id);
button.show = image;
button.set(state.ids.button, ui);
value
}
}

View File

@ -1,10 +1,5 @@
// Library
use glutin;
use gfx_window_glutin;
use vek::*;
use std::collections::HashMap;
// Crate
use crate::{
Error,
render::{
@ -12,6 +7,7 @@ use crate::{
TgtColorFmt,
TgtDepthFmt,
},
ui,
};
pub struct Window {
@ -19,6 +15,7 @@ pub struct Window {
renderer: Renderer,
window: glutin::GlWindow,
cursor_grabbed: bool,
needs_refresh_resize: bool,
key_map: HashMap<glutin::VirtualKeyCode, Key>,
}
@ -28,13 +25,13 @@ impl Window {
let events_loop = glutin::EventsLoop::new();
let win_builder = glutin::WindowBuilder::new()
.with_title("Veloren (Voxygen)")
.with_dimensions(glutin::dpi::LogicalSize::new(800.0, 500.0))
.with_maximized(false);
.with_title("Veloren")
.with_dimensions(glutin::dpi::LogicalSize::new(1366.0, 768.0))
.with_maximized(true);
let ctx_builder = glutin::ContextBuilder::new()
.with_gl(glutin::GlRequest::Specific(glutin::Api::OpenGl, (3, 2)))
.with_vsync(true);
.with_vsync(false);
let (
window,
@ -49,7 +46,9 @@ impl Window {
).map_err(|err| Error::BackendError(Box::new(err)))?;
let mut key_map = HashMap::new();
key_map.insert(glutin::VirtualKeyCode::Escape, Key::ToggleCursor);
key_map.insert(glutin::VirtualKeyCode::Tab, Key::ToggleCursor);
key_map.insert(glutin::VirtualKeyCode::Escape, Key::Escape);
key_map.insert(glutin::VirtualKeyCode::Return, Key::Enter);
key_map.insert(glutin::VirtualKeyCode::W, Key::MoveForward);
key_map.insert(glutin::VirtualKeyCode::A, Key::MoveLeft);
key_map.insert(glutin::VirtualKeyCode::S, Key::MoveBack);
@ -65,6 +64,7 @@ impl Window {
)?,
window,
cursor_grabbed: false,
needs_refresh_resize: false,
key_map,
});
tmp
@ -74,6 +74,13 @@ impl Window {
pub fn renderer_mut(&mut self) -> &mut Renderer { &mut self.renderer }
pub fn fetch_events(&mut self) -> Vec<Event> {
let mut events = vec![];
// Refresh ui size (used when changing playstates)
if self.needs_refresh_resize {
events.push(Event::Ui(ui::Event::new_resize(self.logical_size())));
self.needs_refresh_resize = false;
}
// Copy data that is needed by the events closure to avoid lifetime errors
// TODO: Remove this if/when the compiler permits it
let cursor_grabbed = self.cursor_grabbed;
@ -81,42 +88,49 @@ impl Window {
let window = &mut self.window;
let key_map = &self.key_map;
let mut events = vec![];
self.events_loop.poll_events(|event| match event {
glutin::Event::WindowEvent { event, .. } => match event {
glutin::WindowEvent::CloseRequested => events.push(Event::Close),
glutin::WindowEvent::Resized(glutin::dpi::LogicalSize { width, height }) => {
let (mut color_view, mut depth_view) = renderer.target_views_mut();
gfx_window_glutin::update_views(
&window,
&mut color_view,
&mut depth_view,
);
events.push(Event::Resize(Vec2::new(width as u32, height as u32)));
},
glutin::WindowEvent::ReceivedCharacter(c) => events.push(Event::Char(c)),
glutin::WindowEvent::KeyboardInput { input, .. } => match input.virtual_keycode {
Some(keycode) => match key_map.get(&keycode) {
Some(&key) => events.push(match input.state {
glutin::ElementState::Pressed => Event::KeyDown(key),
_ => Event::KeyUp(key),
}),
self.events_loop.poll_events(|event| {
// Get events for ui
if let Some(event) = ui::Event::try_from(event.clone(), &window) {
events.push(Event::Ui(event));
}
match event {
glutin::Event::WindowEvent { event, .. } => match event {
glutin::WindowEvent::CloseRequested => events.push(Event::Close),
glutin::WindowEvent::Resized(glutin::dpi::LogicalSize { width, height }) => {
let (mut color_view, mut depth_view) = renderer.target_views_mut();
gfx_window_glutin::update_views(
&window,
&mut color_view,
&mut depth_view,
);
events.push(Event::Resize(Vec2::new(width as u32, height as u32)));
},
glutin::WindowEvent::ReceivedCharacter(c) => events.push(Event::Char(c)),
glutin::WindowEvent::KeyboardInput { input, .. } => match input.virtual_keycode {
Some(keycode) => match key_map.get(&keycode) {
Some(&key) => events.push(match input.state {
glutin::ElementState::Pressed => Event::KeyDown(key),
_ => Event::KeyUp(key),
}),
_ => {},
},
_ => {},
},
_ => {},
},
_ => {},
},
glutin::Event::DeviceEvent { event, .. } => match event {
glutin::DeviceEvent::MouseMotion { delta: (dx, dy), .. } if cursor_grabbed =>
glutin::Event::DeviceEvent { event, .. } => match event {
glutin::DeviceEvent::MouseMotion { delta: (dx, dy), .. } if cursor_grabbed =>
events.push(Event::CursorPan(Vec2::new(dx as f32, dy as f32))),
glutin::DeviceEvent::MouseWheel {
delta: glutin::MouseScrollDelta::LineDelta(_x, y),
..
} if cursor_grabbed => events.push(Event::Zoom(y as f32)),
glutin::DeviceEvent::MouseWheel {
delta: glutin::MouseScrollDelta::LineDelta(_x, y),
..
} if cursor_grabbed => events.push(Event::Zoom(y as f32)),
_ => {},
},
_ => {},
},
_ => {},
}
});
events
}
@ -136,6 +150,15 @@ impl Window {
self.window.grab_cursor(grab)
.expect("Failed to grab/ungrab cursor");
}
pub fn needs_refresh_resize(&mut self) {
self.needs_refresh_resize = true;
}
pub fn logical_size(&self) -> Vec2<f64> {
let (w, h) = self.window.get_inner_size().unwrap_or(glutin::dpi::LogicalSize::new(0.0, 0.0)).into();
Vec2::new(w, h)
}
}
/// Represents a key that the game recognises after keyboard mapping
@ -146,9 +169,12 @@ pub enum Key {
MoveBack,
MoveLeft,
MoveRight,
Enter,
Escape,
}
/// Represents an incoming event from the window
#[derive(Clone)]
pub enum Event {
/// The window has been requested to close.
Close,
@ -164,4 +190,6 @@ pub enum Event {
KeyDown(Key),
/// A key that the game recognises has been released down
KeyUp(Key),
/// Event that the ui uses
Ui(ui::Event),
}

View File

@ -1 +0,0 @@
*.png filter=lfs diff=lfs merge=lfs -text

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
voxygen/test_assets/test.png (Stored with Git LFS)

Binary file not shown.

BIN
voxygen/test_assets/wall.png (Stored with Git LFS)

Binary file not shown.