new ui, fork and strip down ggez

This commit is contained in:
Alula 2020-08-19 21:11:32 +02:00
parent 09edf27e91
commit 1993720f34
No known key found for this signature in database
GPG Key ID: 3E00485503A1D8BA
51 changed files with 9789 additions and 49 deletions

View File

@ -9,9 +9,17 @@ lto = true
panic = 'abort'
[dependencies]
approx = "0.3"
bitflags = "1"
byteorder = "1.3"
directories = "2"
gfx = "0.18"
gfx_core = "0.9"
gfx_device_gl = "0.16"
ggez = { git = "https://github.com/ggez/ggez", rev = "4f4bdebff463881c36325c7e10520c9a4fd8f75c" }
gfx_window_glutin = "0.30"
gilrs = "0.7"
glyph_brush = "0.5"
glutin = "0.20"
imgui = "0.4.0"
imgui-ext = "0.3.0"
imgui-gfx-renderer = "0.4.0"
@ -20,12 +28,18 @@ image = {version = "0.22", default-features = false, features = ["png_codec", "p
itertools = "0.9.0"
lazy_static = "1.4.0"
log = "0.4"
lyon = "0.13"
maplit = "1.0.2"
mint = "0.5"
nalgebra = {version = "0.18", features = ["mint"] }
num-traits = "0.2.12"
paste = "1.0.0"
pretty_env_logger = "0.4.0"
rodio = { version = "0.11", default-features = false, features = ["flac", "vorbis", "wav"] }
serde = "1"
serde_derive = "1"
smart-default = "0.5"
strum = "0.18.0"
strum_macros = "0.18.0"
toml = "0.5"
owning_ref = "0.4.1"
winit = { version = "0.19.3" }

View File

@ -54,7 +54,7 @@ impl<S: Num + Copy> Rect<S> {
}
}
pub fn from(rect: ggez::graphics::Rect) -> Rect<f32> {
pub fn from(rect: crate::ggez::graphics::Rect) -> Rect<f32> {
Rect {
left: rect.x,
top: rect.y,

View File

@ -1,4 +1,4 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
use crate::frame::Frame;
use crate::SharedGameState;

7
src/ggez/LICENSE Normal file
View File

@ -0,0 +1,7 @@
Copyright 2020 Alula
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

450
src/ggez/conf.rs Normal file
View File

@ -0,0 +1,450 @@
//! The `conf` module contains functions for loading and saving game
//! configurations.
//!
//! A [`Conf`](struct.Conf.html) struct is used to create a config file
//! which specifies hardware setup stuff, mostly video display settings.
//!
//! By default a ggez game will search its resource paths for a `/conf.toml`
//! file and load values from it when the [`Context`](../struct.Context.html) is created. This file
//! must be complete (ie you cannot just fill in some fields and have the
//! rest be default) and provides a nice way to specify settings that
//! can be tweaked such as window resolution, multisampling options, etc.
//! If no file is found, it will create a `Conf` object from the settings
//! passed to the [`ContextBuilder`](../struct.ContextBuilder.html).
use std::io;
use toml;
use crate::ggez::error::GameResult;
/// Possible fullscreen modes.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum FullscreenType {
/// Windowed mode.
Windowed,
/// True fullscreen, which used to be preferred 'cause it can have
/// small performance benefits over windowed fullscreen.
True,
/// Windowed fullscreen, generally preferred over real fullscreen
/// these days 'cause it plays nicer with multiple monitors.
Desktop,
}
/// A builder structure containing window settings
/// that can be set at runtime and changed with [`graphics::set_mode()`](../graphics/fn.set_mode.html).
///
/// Defaults:
///
/// ```rust
/// # use ggez::conf::*;
/// # fn main() { assert_eq!(
/// WindowMode {
/// width: 800.0,
/// height: 600.0,
/// maximized: false,
/// fullscreen_type: FullscreenType::Windowed,
/// borderless: false,
/// min_width: 0.0,
/// max_width: 0.0,
/// min_height: 0.0,
/// max_height: 0.0,
/// resizable: false,
/// }
/// # , WindowMode::default());}
/// ```
#[derive(Debug, Copy, Clone, SmartDefault, Serialize, Deserialize, PartialEq)]
pub struct WindowMode {
/// Window width
#[default = 800.0]
pub width: f32,
/// Window height
#[default = 600.0]
pub height: f32,
/// Whether or not to maximize the window
#[default = false]
pub maximized: bool,
/// Fullscreen type
#[default(FullscreenType::Windowed)]
pub fullscreen_type: FullscreenType,
/// Whether or not to show window decorations
#[default = false]
pub borderless: bool,
/// Minimum width for resizable windows; 0 means no limit
#[default = 0.0]
pub min_width: f32,
/// Minimum height for resizable windows; 0 means no limit
#[default = 0.0]
pub min_height: f32,
/// Maximum width for resizable windows; 0 means no limit
#[default = 0.0]
pub max_width: f32,
/// Maximum height for resizable windows; 0 means no limit
#[default = 0.0]
pub max_height: f32,
/// Whether or not the window is resizable
#[default = false]
pub resizable: bool,
}
impl WindowMode {
/// Set default window size, or screen resolution in true fullscreen mode.
pub fn dimensions(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self
}
/// Set whether the window should be maximized.
pub fn maximized(mut self, maximized: bool) -> Self {
self.maximized = maximized;
self
}
/// Set the fullscreen type.
pub fn fullscreen_type(mut self, fullscreen_type: FullscreenType) -> Self {
self.fullscreen_type = fullscreen_type;
self
}
/// Set whether a window should be borderless in windowed mode.
pub fn borderless(mut self, borderless: bool) -> Self {
self.borderless = borderless;
self
}
/// Set minimum window dimensions for windowed mode.
pub fn min_dimensions(mut self, width: f32, height: f32) -> Self {
self.min_width = width;
self.min_height = height;
self
}
/// Set maximum window dimensions for windowed mode.
pub fn max_dimensions(mut self, width: f32, height: f32) -> Self {
self.max_width = width;
self.max_height = height;
self
}
/// Set resizable.
pub fn resizable(mut self, resizable: bool) -> Self {
self.resizable = resizable;
self
}
}
/// A builder structure containing window settings
/// that must be set at init time and cannot be changed afterwards.
///
/// Defaults:
///
/// ```rust
/// # use ggez::conf::*;
/// # fn main() { assert_eq!(
/// WindowSetup {
/// title: "An easy, good game".to_owned(),
/// samples: NumSamples::Zero,
/// vsync: true,
/// icon: "".to_owned(),
/// srgb: true,
/// }
/// # , WindowSetup::default()); }
/// ```
#[derive(Debug, Clone, SmartDefault, Serialize, Deserialize, PartialEq)]
pub struct WindowSetup {
/// The window title.
#[default(String::from("An easy, good game"))]
pub title: String,
/// Number of samples to use for multisample anti-aliasing.
#[default(NumSamples::Zero)]
pub samples: NumSamples,
/// Whether or not to enable vsync.
#[default = true]
pub vsync: bool,
/// A file path to the window's icon.
/// It takes a path rooted in the `resources` directory (see the [`filesystem`](../filesystem/index.html)
/// module for details), and an empty string results in a blank/default icon.
#[default(String::new())]
pub icon: String,
/// Whether or not to enable sRGB (gamma corrected color)
/// handling on the display.
#[default = true]
pub srgb: bool,
}
impl WindowSetup {
/// Set window title.
pub fn title(mut self, title: &str) -> Self {
self.title = title.to_owned();
self
}
/// Set number of samples to use for multisample anti-aliasing.
pub fn samples(mut self, samples: NumSamples) -> Self {
self.samples = samples;
self
}
/// Set whether vsync is enabled.
pub fn vsync(mut self, vsync: bool) -> Self {
self.vsync = vsync;
self
}
/// Set the window's icon.
pub fn icon(mut self, icon: &str) -> Self {
self.icon = icon.to_owned();
self
}
/// Set sRGB color mode.
pub fn srgb(mut self, active: bool) -> Self {
self.srgb = active;
self
}
}
/// Possible backends.
/// Currently, only OpenGL and OpenGL ES Core specs are supported,
/// but this lets you specify which to use as well as the version numbers.
///
/// Defaults:
///
/// ```rust
/// # use ggez::conf::*;
/// # fn main() { assert_eq!(
/// Backend::OpenGL {
/// major: 3,
/// minor: 2,
/// }
/// # , Backend::default()); }
/// ```
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, SmartDefault)]
#[serde(tag = "type")]
pub enum Backend {
/// Defaults to OpenGL 3.2, which is supported by basically
/// every machine since 2009 or so (apart from the ones that don't).
#[default]
OpenGL {
/// OpenGL major version
#[default = 3]
major: u8,
/// OpenGL minor version
#[default = 2]
minor: u8,
},
/// OpenGL ES, defaults to 3.0. Used for phones and other mobile
/// devices. Using something older
/// than 3.0 starts to running into sticky limitations, particularly
/// with instanced drawing (used for `SpriteBatch`), but might be
/// possible.
OpenGLES {
/// OpenGL ES major version
#[default = 3]
major: u8,
/// OpenGL ES minor version
#[default = 0]
minor: u8,
},
}
impl Backend {
/// Set requested OpenGL/OpenGL ES version.
pub fn version(self, new_major: u8, new_minor: u8) -> Self {
match self {
Backend::OpenGL { .. } => Backend::OpenGL {
major: new_major,
minor: new_minor,
},
Backend::OpenGLES { .. } => Backend::OpenGLES {
major: new_major,
minor: new_minor,
},
}
}
/// Use OpenGL
pub fn gl(self) -> Self {
match self {
Backend::OpenGLES { major, minor } => Backend::OpenGL { major, minor },
gl => gl,
}
}
/// Use OpenGL ES
pub fn gles(self) -> Self {
match self {
Backend::OpenGL { major, minor } => Backend::OpenGLES { major, minor },
es => es,
}
}
}
/// The possible number of samples for multisample anti-aliasing.
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub enum NumSamples {
/// Multisampling disabled.
Zero = 0,
/// One sample
One = 1,
/// Two samples
Two = 2,
/// Four samples
Four = 4,
/// Eight samples
Eight = 8,
/// Sixteen samples
Sixteen = 16,
}
impl NumSamples {
/// Create a `NumSamples` from a number.
/// Returns `None` if `i` is invalid.
pub fn from_u32(i: u32) -> Option<NumSamples> {
match i {
0 => Some(NumSamples::Zero),
1 => Some(NumSamples::One),
2 => Some(NumSamples::Two),
4 => Some(NumSamples::Four),
8 => Some(NumSamples::Eight),
16 => Some(NumSamples::Sixteen),
_ => None,
}
}
}
/// Defines which submodules to enable in ggez.
/// If one tries to use a submodule that is not enabled,
/// it will panic. Currently, not all subsystems can be
/// disabled.
///
/// Defaults:
///
/// ```rust
/// # use ggez::conf::*;
/// # fn main() { assert_eq!(
/// ModuleConf {
/// gamepad: true,
/// audio: true,
/// }
/// # , ModuleConf::default()); }
/// ```
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, SmartDefault)]
pub struct ModuleConf {
// Gamepad is disabled by default on OSX
// See issue #588 in general, #577 for specifics.
/// The gamepad input module.
#[cfg(target_os = "macos")]
#[default = false]
pub gamepad: bool,
/// The gamepad input module.
#[cfg(not(target_os = "macos"))]
#[default = true]
pub gamepad: bool,
/// The audio module.
#[default = true]
pub audio: bool,
}
impl ModuleConf {
/// Sets whether or not to enable the gamepad input module.
pub fn gamepad(mut self, gamepad: bool) -> Self {
self.gamepad = gamepad;
self
}
/// Sets whether or not to enable the audio module.
pub fn audio(mut self, audio: bool) -> Self {
self.audio = audio;
self
}
}
/// A structure containing configuration data
/// for the game engine.
///
/// Defaults:
///
/// ```rust
/// # use ggez::conf::*;
/// # fn main() { assert_eq!(
/// Conf {
/// window_mode: WindowMode::default(),
/// window_setup: WindowSetup::default(),
/// backend: Backend::default(),
/// modules: ModuleConf::default(),
/// }
/// # , Conf::default()); }
/// ```
#[derive(Serialize, Deserialize, Debug, PartialEq, SmartDefault, Clone)]
pub struct Conf {
/// Window setting information that can be set at runtime
pub window_mode: WindowMode,
/// Window setting information that must be set at init-time
pub window_setup: WindowSetup,
/// Graphics backend configuration
pub backend: Backend,
/// Which modules to enable.
pub modules: ModuleConf,
}
impl Conf {
/// Same as `Conf::default()`
pub fn new() -> Self {
Self::default()
}
/// Load a TOML file from the given `Read` and attempts to parse
/// a `Conf` from it.
pub fn from_toml_file<R: io::Read>(file: &mut R) -> GameResult<Conf> {
let mut s = String::new();
let _ = file.read_to_string(&mut s)?;
let decoded = toml::from_str(&s)?;
Ok(decoded)
}
/// Saves the `Conf` to the given `Write` object,
/// formatted as TOML.
pub fn to_toml_file<W: io::Write>(&self, file: &mut W) -> GameResult {
let s = toml::to_vec(self)?;
file.write_all(&s)?;
Ok(())
}
/// Sets the window mode
pub fn window_mode(mut self, window_mode: WindowMode) -> Self {
self.window_mode = window_mode;
self
}
/// Sets the backend
pub fn backend(mut self, backend: Backend) -> Self {
self.backend = backend;
self
}
/// Sets the backend
pub fn modules(mut self, modules: ModuleConf) -> Self {
self.modules = modules;
self
}
}
#[cfg(test)]
mod tests {
use crate::conf;
/// Tries to encode and decode a `Conf` object
/// and makes sure it gets the same result it had.
#[test]
fn headless_encode_round_trip() {
let c1 = conf::Conf::new();
let mut writer = Vec::new();
let _c = c1.to_toml_file(&mut writer).unwrap();
let mut reader = writer.as_slice();
let c2 = conf::Conf::from_toml_file(&mut reader).unwrap();
assert_eq!(c1, c2);
}
}

327
src/ggez/context.rs Normal file
View File

@ -0,0 +1,327 @@
use std::fmt;
/// We re-export winit so it's easy for people to use the same version as we are
/// without having to mess around figuring it out.
pub use winit;
use crate::ggez::conf;
use crate::ggez::error::GameResult;
use crate::ggez::event::winit_event;
use crate::ggez::filesystem::Filesystem;
use crate::ggez::graphics::{self, Point2};
use crate::ggez::input::{gamepad, keyboard, mouse};
use crate::ggez::timer;
/// A `Context` is an object that holds on to global resources.
/// It basically tracks hardware state such as the screen, audio
/// system, timers, and so on. Generally this type can **not**
/// be shared/sent between threads and only one `Context` can exist at a time. Trying
/// to create a second one will fail. It is fine to drop a `Context`
/// and create a new one, but this will also close and re-open your
/// game's window.
///
/// Most functions that interact with the hardware, for instance
/// drawing things, playing sounds, or loading resources (which then
/// need to be transformed into a format the hardware likes) will need
/// to access the `Context`. It is an error to create some type that
/// relies upon a `Context`, such as `Image`, and then drop the `Context`
/// and try to draw the old `Image` with the new `Context`. Most types
/// include checks to make this panic in debug mode, but it's not perfect.
///
/// All fields in this struct are basically undocumented features,
/// only here to make it easier to debug, or to let advanced users
/// hook into the guts of ggez and make it do things it normally
/// can't. Most users shouldn't need to touch these things directly,
/// since implementation details may change without warning. The
/// public and stable API is `ggez`'s module-level functions and
/// types.
pub struct Context {
/// Filesystem state
pub filesystem: Filesystem,
/// Graphics state
pub(crate) gfx_context: crate::graphics::context::GraphicsContext,
/// Timer state
pub timer_context: timer::TimeContext,
/// Keyboard context
pub keyboard_context: keyboard::KeyboardContext,
/// Mouse context
pub mouse_context: mouse::MouseContext,
/// Gamepad context
pub gamepad_context: Box<dyn gamepad::GamepadContext>,
/// The Conf object the Context was created with.
/// It's here just so that we can see the original settings,
/// updating it will have no effect.
pub(crate) conf: conf::Conf,
/// Controls whether or not the event loop should be running.
/// Set this with `ggez::event::quit()`.
pub continuing: bool,
/// Context-specific unique ID.
/// Compiles to nothing in release mode, and so
/// vanishes; meanwhile we get dead-code warnings.
#[allow(dead_code)]
debug_id: DebugId,
}
impl fmt::Debug for Context {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<Context: {:p}>", self)
}
}
impl Context {
/// Tries to create a new Context using settings from the given [`Conf`](../conf/struct.Conf.html) object.
/// Usually called by [`ContextBuilder::build()`](struct.ContextBuilder.html#method.build).
fn from_conf(conf: conf::Conf, mut fs: Filesystem) -> GameResult<(Context, winit::EventsLoop)> {
let debug_id = DebugId::new();
let events_loop = winit::EventsLoop::new();
let timer_context = timer::TimeContext::new();
let backend_spec = graphics::GlBackendSpec::from(conf.backend);
let graphics_context = graphics::context::GraphicsContext::new(
&mut fs,
&events_loop,
&conf.window_setup,
conf.window_mode,
backend_spec,
debug_id,
)?;
let mouse_context = mouse::MouseContext::new();
let keyboard_context = keyboard::KeyboardContext::new();
let gamepad_context: Box<dyn gamepad::GamepadContext> = if conf.modules.gamepad {
Box::new(gamepad::GilrsGamepadContext::new()?)
} else {
Box::new(gamepad::NullGamepadContext::default())
};
let ctx = Context {
conf,
filesystem: fs,
gfx_context: graphics_context,
continuing: true,
timer_context,
keyboard_context,
gamepad_context,
mouse_context,
debug_id,
};
Ok((ctx, events_loop))
}
// TODO LATER: This should be a function in `ggez::event`, per the
// "functions are stable, methods and fields are unstable" promise
// given above.
/// Feeds an `Event` into the `Context` so it can update any internal
/// state it needs to, such as detecting window resizes. If you are
/// rolling your own event loop, you should call this on the events
/// you receive before processing them yourself.
pub fn process_event(&mut self, event: &winit::Event) {
match event.clone() {
winit_event::Event::WindowEvent { event, .. } => match event {
winit_event::WindowEvent::Resized(logical_size) => {
let hidpi_factor = self.gfx_context.window.get_hidpi_factor();
let physical_size = logical_size.to_physical(hidpi_factor as f64);
self.gfx_context.window.resize(physical_size);
self.gfx_context.resize_viewport();
}
winit_event::WindowEvent::CursorMoved {
position: logical_position,
..
} => {
self.mouse_context.set_last_position(Point2::new(
logical_position.x as f32,
logical_position.y as f32,
));
}
winit_event::WindowEvent::MouseInput { button, state, .. } => {
let pressed = match state {
winit_event::ElementState::Pressed => true,
winit_event::ElementState::Released => false,
};
self.mouse_context.set_button(button, pressed);
}
winit_event::WindowEvent::KeyboardInput {
input:
winit::KeyboardInput {
state,
virtual_keycode: Some(keycode),
modifiers,
..
},
..
} => {
let pressed = match state {
winit_event::ElementState::Pressed => true,
winit_event::ElementState::Released => false,
};
self.keyboard_context
.set_modifiers(keyboard::KeyMods::from(modifiers));
self.keyboard_context.set_key(keycode, pressed);
}
winit_event::WindowEvent::HiDpiFactorChanged(_) => {
// Nope.
}
_ => (),
},
winit_event::Event::DeviceEvent { event, .. } => {
if let winit_event::DeviceEvent::MouseMotion { delta: (x, y) } = event {
self.mouse_context
.set_last_delta(Point2::new(x as f32, y as f32));
}
}
_ => (),
};
}
}
use std::borrow::Cow;
use std::path;
/// A builder object for creating a [`Context`](struct.Context.html).
#[derive(Debug, Clone, PartialEq)]
pub struct ContextBuilder {
pub(crate) game_id: String,
pub(crate) conf: conf::Conf,
pub(crate) paths: Vec<path::PathBuf>,
pub(crate) memory_zip_files: Vec<Cow<'static, [u8]>>,
pub(crate) load_conf_file: bool,
}
impl ContextBuilder {
/// Create a new `ContextBuilder` with default settings.
pub fn new(game_id: &str) -> Self {
Self {
game_id: game_id.to_string(),
conf: conf::Conf::default(),
paths: vec![],
memory_zip_files: vec![],
load_conf_file: true,
}
}
/// Sets the window setup settings.
pub fn window_setup(mut self, setup: conf::WindowSetup) -> Self {
self.conf.window_setup = setup;
self
}
/// Sets the window mode settings.
pub fn window_mode(mut self, mode: conf::WindowMode) -> Self {
self.conf.window_mode = mode;
self
}
/// Sets the graphics backend.
pub fn backend(mut self, backend: conf::Backend) -> Self {
self.conf.backend = backend;
self
}
/// Sets the modules configuration.
pub fn modules(mut self, modules: conf::ModuleConf) -> Self {
self.conf.modules = modules;
self
}
/// Sets all the config options, overriding any previous
/// ones from [`window_setup()`](#method.window_setup),
/// [`window_mode()`](#method.window_mode), and
/// [`backend()`](#method.backend).
pub fn conf(mut self, conf: conf::Conf) -> Self {
self.conf = conf;
self
}
/// Add a new read-only filesystem path to the places to search
/// for resources.
pub fn add_resource_path<T>(mut self, path: T) -> Self
where
T: Into<path::PathBuf>,
{
self.paths.push(path.into());
self
}
/// Specifies whether or not to load the `conf.toml` file if it
/// exists and use its settings to override the provided values.
/// Defaults to `true` which is usually what you want, but being
/// able to fiddle with it is sometimes useful for debugging.
pub fn with_conf_file(mut self, load_conf_file: bool) -> Self {
self.load_conf_file = load_conf_file;
self
}
/// Build the `Context`.
pub fn build(self) -> GameResult<(Context, winit::EventsLoop)> {
let mut fs = Filesystem::new(self.game_id.as_ref())?;
for path in &self.paths {
fs.mount(path, true);
}
let config = if self.load_conf_file {
fs.read_config().unwrap_or(self.conf)
} else {
self.conf
};
Context::from_conf(config, fs)
}
}
#[cfg(debug_assertions)]
use std::sync::atomic::{AtomicUsize, Ordering};
#[cfg(debug_assertions)]
static DEBUG_ID_COUNTER: AtomicUsize = AtomicUsize::new(0);
/// This is a type that contains a unique ID for each `Context` and
/// is contained in each thing created from the `Context` which
/// becomes invalid when the `Context` goes away (for example, `Image` because
/// it contains texture handles). When compiling without assertions
/// (in release mode) it is replaced with a zero-size type, compiles
/// down to nothing, disappears entirely with a puff of optimization logic.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg(debug_assertions)]
pub(crate) struct DebugId(u32);
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
#[cfg(not(debug_assertions))]
pub(crate) struct DebugId;
#[cfg(debug_assertions)]
impl DebugId {
pub fn new() -> Self {
let id = DEBUG_ID_COUNTER.fetch_add(1, Ordering::SeqCst) as u32;
// fetch_add() wraps on overflow so we check for overflow explicitly.
// JUST IN CASE YOU TRY TO CREATE 2^32 CONTEXTS IN ONE PROGRAM! muahahahahaaa
assert!(DEBUG_ID_COUNTER.load(Ordering::SeqCst) as u32 > id);
DebugId(id)
}
pub fn get(ctx: &Context) -> Self {
DebugId(ctx.debug_id.0)
}
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn assert(&self, ctx: &Context) {
if *self != ctx.debug_id {
panic!("Tried to use a resource with a Context that did not create it; this should never happen!");
}
}
}
#[cfg(not(debug_assertions))]
impl DebugId {
pub fn new() -> Self {
DebugId
}
pub fn get(_ctx: &Context) -> Self {
DebugId
}
pub fn assert(&self, _ctx: &Context) {
// Do nothing.
}
}

227
src/ggez/error.rs Normal file
View File

@ -0,0 +1,227 @@
//! Error types and conversion functions.
use std;
use std::error::Error;
use std::fmt;
use std::sync::Arc;
use gfx;
use glutin;
use winit;
use gilrs;
use image;
use lyon;
use toml;
/// An enum containing all kinds of game framework errors.
#[derive(Debug, Clone)]
pub enum GameError {
/// An error in the filesystem layout
FilesystemError(String),
/// An error in the config file
ConfigError(String),
/// Happens when an `winit::EventsLoopProxy` attempts to
/// wake up an `winit::EventsLoop` that no longer exists.
EventLoopError(String),
/// An error trying to load a resource, such as getting an invalid image file.
ResourceLoadError(String),
/// Unable to find a resource; the `Vec` is the paths it searched for and associated errors
ResourceNotFound(String, Vec<(std::path::PathBuf, GameError)>),
/// Something went wrong in the renderer
RenderError(String),
/// Something went wrong in the audio playback
AudioError(String),
/// Something went wrong trying to set or get window properties.
WindowError(String),
/// Something went wrong trying to create a window
WindowCreationError(Arc<glutin::CreationError>),
/// Something went wrong trying to read from a file
IOError(Arc<std::io::Error>),
/// Something went wrong trying to load/render a font
FontError(String),
/// Something went wrong applying video settings.
VideoError(String),
/// Something went wrong compiling shaders
ShaderProgramError(gfx::shade::ProgramError),
/// Something went wrong with the `gilrs` gamepad-input library.
GamepadError(String),
/// Something went wrong with the `lyon` shape-tesselation library.
LyonError(String),
}
impl fmt::Display for GameError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
GameError::ConfigError(ref s) => write!(f, "Config error: {}", s),
GameError::ResourceLoadError(ref s) => write!(f, "Error loading resource: {}", s),
GameError::ResourceNotFound(ref s, ref paths) => write!(
f,
"Resource not found: {}, searched in paths {:?}",
s, paths
),
GameError::WindowError(ref e) => write!(f, "Window creation error: {}", e),
_ => write!(f, "GameError {:?}", self),
}
}
}
impl Error for GameError {
fn cause(&self) -> Option<&dyn Error> {
match *self {
GameError::WindowCreationError(ref e) => Some(&**e),
GameError::IOError(ref e) => Some(&**e),
GameError::ShaderProgramError(ref e) => Some(e),
_ => None,
}
}
}
/// A convenient result type consisting of a return type and a `GameError`
pub type GameResult<T = ()> = Result<T, GameError>;
impl From<std::io::Error> for GameError {
fn from(e: std::io::Error) -> GameError {
GameError::IOError(Arc::new(e))
}
}
impl From<toml::de::Error> for GameError {
fn from(e: toml::de::Error) -> GameError {
let errstr = format!("TOML decode error: {}", e);
GameError::ConfigError(errstr)
}
}
impl From<toml::ser::Error> for GameError {
fn from(e: toml::ser::Error) -> GameError {
let errstr = format!("TOML error (possibly encoding?): {}", e);
GameError::ConfigError(errstr)
}
}
impl From<image::ImageError> for GameError {
fn from(e: image::ImageError) -> GameError {
let errstr = format!("Image load error: {}", e);
GameError::ResourceLoadError(errstr)
}
}
impl From<gfx::PipelineStateError<std::string::String>> for GameError {
fn from(e: gfx::PipelineStateError<std::string::String>) -> GameError {
let errstr = format!(
"Error constructing pipeline!\nThis should probably not be \
happening; it probably means an error in a shader or \
something.\nError was: {:?}",
e
);
GameError::VideoError(errstr)
}
}
impl From<gfx::mapping::Error> for GameError {
fn from(e: gfx::mapping::Error) -> GameError {
let errstr = format!("Buffer mapping error: {:?}", e);
GameError::VideoError(errstr)
}
}
impl<S, D> From<gfx::CopyError<S, D>> for GameError
where
S: fmt::Debug,
D: fmt::Debug,
{
fn from(e: gfx::CopyError<S, D>) -> GameError {
let errstr = format!("Memory copy error: {:?}", e);
GameError::VideoError(errstr)
}
}
impl From<gfx::CombinedError> for GameError {
fn from(e: gfx::CombinedError) -> GameError {
let errstr = format!("Texture+view load error: {}", e);
GameError::VideoError(errstr)
}
}
impl From<gfx::texture::CreationError> for GameError {
fn from(e: gfx::texture::CreationError) -> GameError {
gfx::CombinedError::from(e).into()
}
}
impl From<gfx::ResourceViewError> for GameError {
fn from(e: gfx::ResourceViewError) -> GameError {
gfx::CombinedError::from(e).into()
}
}
impl From<gfx::TargetViewError> for GameError {
fn from(e: gfx::TargetViewError) -> GameError {
gfx::CombinedError::from(e).into()
}
}
impl<T> From<gfx::UpdateError<T>> for GameError
where
T: fmt::Debug + fmt::Display + 'static,
{
fn from(e: gfx::UpdateError<T>) -> GameError {
let errstr = format!("Buffer update error: {}", e);
GameError::VideoError(errstr)
}
}
impl From<gfx::shade::ProgramError> for GameError {
fn from(e: gfx::shade::ProgramError) -> GameError {
GameError::ShaderProgramError(e)
}
}
impl From<winit::EventsLoopClosed> for GameError {
fn from(_: glutin::EventsLoopClosed) -> GameError {
let e = "An event loop proxy attempted to wake up an event loop that no longer exists."
.to_owned();
GameError::EventLoopError(e)
}
}
impl From<glutin::CreationError> for GameError {
fn from(s: glutin::CreationError) -> GameError {
GameError::WindowCreationError(Arc::new(s))
}
}
impl From<glutin::ContextError> for GameError {
fn from(s: glutin::ContextError) -> GameError {
GameError::RenderError(format!("OpenGL context error: {}", s))
}
}
impl From<gilrs::Error> for GameError {
fn from(s: gilrs::Error) -> GameError {
let errstr = format!("Gamepad error: {}", s);
GameError::GamepadError(errstr)
}
}
impl From<lyon::lyon_tessellation::TessellationError> for GameError {
fn from(s: lyon::lyon_tessellation::TessellationError) -> GameError {
let errstr = format!(
"Error while tesselating shape (did you give it an infinity or NaN?): {:?}",
s
);
GameError::LyonError(errstr)
}
}
impl From<lyon::lyon_tessellation::geometry_builder::GeometryBuilderError> for GameError {
fn from(s: lyon::lyon_tessellation::geometry_builder::GeometryBuilderError) -> GameError {
let errstr = format!(
"Error while building geometry (did you give it too many vertices?): {:?}",
s
);
GameError::LyonError(errstr)
}
}

285
src/ggez/event.rs Normal file
View File

@ -0,0 +1,285 @@
//! The `event` module contains traits and structs to actually run your game mainloop
//! and handle top-level state, as well as handle input events such as keyboard
//! and mouse.
//!
//! If you don't want to use `ggez`'s built in event loop, you can
//! write your own mainloop and check for events on your own. This is
//! not particularly hard, there's nothing special about the
//! `EventHandler` trait. It just tries to simplify the process a
//! little. For examples of how to write your own main loop, see the
//! source code for this module, or the [`eventloop`
//! example](https://github.com/ggez/ggez/blob/master/examples/eventloop.rs).
use gilrs;
use winit::{self, dpi};
// TODO LATER: I kinda hate all these re-exports. I kinda hate
// a lot of the details of the `EventHandler` and input now though,
// and look forward to ripping it all out and replacing it with newer winit.
/// A mouse button.
pub use winit::MouseButton;
/// An analog axis of some device (gamepad thumbstick, joystick...).
pub use gilrs::Axis;
/// A button of some device (gamepad, joystick...).
pub use gilrs::Button;
/// `winit` events; nested in a module for re-export neatness.
pub mod winit_event {
pub use super::winit::{
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseScrollDelta,
TouchPhase, WindowEvent,
};
}
pub use crate::ggez::input::gamepad::GamepadId;
pub use crate::ggez::input::keyboard::{KeyCode, KeyMods};
use self::winit_event::*;
/// `winit` event loop.
pub use winit::EventsLoop;
use crate::ggez::context::Context;
use crate::ggez::error::GameResult;
/// A trait defining event callbacks. This is your primary interface with
/// `ggez`'s event loop. Implement this trait for a type and
/// override at least the [`update()`](#tymethod.update) and
/// [`draw()`](#tymethod.draw) methods, then pass it to
/// [`event::run()`](fn.run.html) to run the game's mainloop.
///
/// The default event handlers do nothing, apart from
/// [`key_down_event()`](#tymethod.key_down_event), which will by
/// default exit the game if the escape key is pressed. Just
/// override the methods you want to use.
pub trait EventHandler {
/// Called upon each logic update to the game.
/// This should be where the game's logic takes place.
fn update(&mut self, _ctx: &mut Context) -> GameResult;
/// Called to do the drawing of your game.
/// You probably want to start this with
/// [`graphics::clear()`](../graphics/fn.clear.html) and end it
/// with [`graphics::present()`](../graphics/fn.present.html) and
/// maybe [`timer::yield_now()`](../timer/fn.yield_now.html).
fn draw(&mut self, _ctx: &mut Context) -> GameResult;
/// A mouse button was pressed
fn mouse_button_down_event(
&mut self,
_ctx: &mut Context,
_button: MouseButton,
_x: f32,
_y: f32,
) {
}
/// A mouse button was released
fn mouse_button_up_event(
&mut self,
_ctx: &mut Context,
_button: MouseButton,
_x: f32,
_y: f32,
) {
}
/// The mouse was moved; it provides both absolute x and y coordinates in the window,
/// and relative x and y coordinates compared to its last position.
fn mouse_motion_event(&mut self, _ctx: &mut Context, _x: f32, _y: f32, _dx: f32, _dy: f32) {}
/// The mousewheel was scrolled, vertically (y, positive away from and negative toward the user)
/// or horizontally (x, positive to the right and negative to the left).
fn mouse_wheel_event(&mut self, _ctx: &mut Context, _x: f32, _y: f32) {}
/// A keyboard button was pressed.
///
/// The default implementation of this will call `ggez::event::quit()`
/// when the escape key is pressed. If you override this with
/// your own event handler you have to re-implment that
/// functionality yourself.
fn key_down_event(
&mut self,
ctx: &mut Context,
keycode: KeyCode,
_keymods: KeyMods,
_repeat: bool,
) {
if keycode == KeyCode::Escape {
quit(ctx);
}
}
/// A keyboard button was released.
fn key_up_event(&mut self, _ctx: &mut Context, _keycode: KeyCode, _keymods: KeyMods) {}
/// A unicode character was received, usually from keyboard input.
/// This is the intended way of facilitating text input.
fn text_input_event(&mut self, _ctx: &mut Context, _character: char) {}
/// A gamepad button was pressed; `id` identifies which gamepad.
/// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about
/// the gamepad.
fn gamepad_button_down_event(&mut self, _ctx: &mut Context, _btn: Button, _id: GamepadId) {}
/// A gamepad button was released; `id` identifies which gamepad.
/// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about
/// the gamepad.
fn gamepad_button_up_event(&mut self, _ctx: &mut Context, _btn: Button, _id: GamepadId) {}
/// A gamepad axis moved; `id` identifies which gamepad.
/// Use [`input::gamepad()`](../input/fn.gamepad.html) to get more info about
/// the gamepad.
fn gamepad_axis_event(&mut self, _ctx: &mut Context, _axis: Axis, _value: f32, _id: GamepadId) {
}
/// Called when the window is shown or hidden.
fn focus_event(&mut self, _ctx: &mut Context, _gained: bool) {}
/// Called upon a quit event. If it returns true,
/// the game does not exit (the quit event is cancelled).
fn quit_event(&mut self, _ctx: &mut Context) -> bool {
debug!("quit_event() callback called, quitting...");
false
}
/// Called when the user resizes the window, or when it is resized
/// via [`graphics::set_mode()`](../graphics/fn.set_mode.html).
fn resize_event(&mut self, _ctx: &mut Context, _width: f32, _height: f32) {}
}
/// Terminates the [`ggez::event::run()`](fn.run.html) loop by setting
/// [`Context.continuing`](struct.Context.html#structfield.continuing)
/// to `false`.
pub fn quit(ctx: &mut Context) {
ctx.continuing = false;
}
/// Runs the game's main loop, calling event callbacks on the given state
/// object as events occur.
///
/// It does not try to do any type of framerate limiting. See the
/// documentation for the [`timer`](../timer/index.html) module for more info.
pub fn run<S>(ctx: &mut Context, events_loop: &mut EventsLoop, state: &mut S) -> GameResult
where
S: EventHandler,
{
use crate::ggez::input::{keyboard, mouse};
while ctx.continuing {
// If you are writing your own event loop, make sure
// you include `timer_context.tick()` and
// `ctx.process_event()` calls. These update ggez's
// internal state however necessary.
ctx.timer_context.tick();
events_loop.poll_events(|event| {
ctx.process_event(&event);
match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::Resized(logical_size) => {
// let actual_size = logical_size;
state.resize_event(
ctx,
logical_size.width as f32,
logical_size.height as f32,
);
}
WindowEvent::CloseRequested => {
if !state.quit_event(ctx) {
quit(ctx);
}
}
WindowEvent::Focused(gained) => {
state.focus_event(ctx, gained);
}
WindowEvent::ReceivedCharacter(ch) => {
state.text_input_event(ctx, ch);
}
WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Pressed,
virtual_keycode: Some(keycode),
modifiers,
..
},
..
} => {
let repeat = keyboard::is_key_repeated(ctx);
state.key_down_event(ctx, keycode, modifiers.into(), repeat);
}
WindowEvent::KeyboardInput {
input:
KeyboardInput {
state: ElementState::Released,
virtual_keycode: Some(keycode),
modifiers,
..
},
..
} => {
state.key_up_event(ctx, keycode, modifiers.into());
}
WindowEvent::MouseWheel { delta, .. } => {
let (x, y) = match delta {
MouseScrollDelta::LineDelta(x, y) => (x, y),
MouseScrollDelta::PixelDelta(dpi::LogicalPosition { x, y }) => {
(x as f32, y as f32)
}
};
state.mouse_wheel_event(ctx, x, y);
}
WindowEvent::MouseInput {
state: element_state,
button,
..
} => {
let position = mouse::position(ctx);
match element_state {
ElementState::Pressed => {
state.mouse_button_down_event(ctx, button, position.x, position.y)
}
ElementState::Released => {
state.mouse_button_up_event(ctx, button, position.x, position.y)
}
}
}
WindowEvent::CursorMoved { .. } => {
let position = mouse::position(ctx);
let delta = mouse::delta(ctx);
state.mouse_motion_event(ctx, position.x, position.y, delta.x, delta.y);
}
_x => {
// trace!("ignoring window event {:?}", x);
}
},
Event::DeviceEvent { event, .. } => match event {
_ => (),
},
Event::Awakened => (),
Event::Suspended(_) => (),
}
});
// Handle gamepad events if necessary.
if ctx.conf.modules.gamepad {
while let Some(gilrs::Event { id, event, .. }) = ctx.gamepad_context.next_event() {
match event {
gilrs::EventType::ButtonPressed(button, _) => {
state.gamepad_button_down_event(ctx, button, GamepadId(id));
}
gilrs::EventType::ButtonReleased(button, _) => {
state.gamepad_button_up_event(ctx, button, GamepadId(id));
}
gilrs::EventType::AxisChanged(axis, value, _) => {
state.gamepad_axis_event(ctx, axis, value, GamepadId(id));
}
_ => {}
}
}
}
state.update(ctx)?;
state.draw(ctx)?;
}
Ok(())
}

540
src/ggez/filesystem.rs Normal file
View File

@ -0,0 +1,540 @@
//! A cross-platform interface to the filesystem.
//!
//! This module provides access to files in specific places:
//!
//! * The `resources/` subdirectory in the same directory as the
//! program executable, if any,
//! * The `resources.zip` file in the same
//! directory as the program executable, if any,
//! * The root folder of the game's "save" directory which is in a
//! platform-dependent location,
//! such as `~/.local/share/<gameid>/` on Linux. The `gameid`
//! is the the string passed to
//! [`ContextBuilder::new()`](../struct.ContextBuilder.html#method.new).
//! Some platforms such as Windows also incorporate the `author` string into
//! the path.
//!
//! These locations will be searched for files in the order listed, and the first file
//! found used. That allows game assets to be easily distributed as an archive
//! file, but locally overridden for testing or modding simply by putting
//! altered copies of them in the game's `resources/` directory. It
//! is loosely based off of the `PhysicsFS` library.
//!
//! See the source of the [`files` example](https://github.com/ggez/ggez/blob/master/examples/files.rs) for more details.
//!
//! Note that the file lookups WILL follow symlinks! This module's
//! directory isolation is intended for convenience, not security, so
//! don't assume it will be secure.
use std::env;
use std::fmt;
use std::io;
use std::path;
use crate::ggez::{Context, GameError, GameResult};
use crate::ggez::conf;
use crate::ggez::vfs::{self, VFS};
pub use crate::ggez::vfs::OpenOptions;
const CONFIG_NAME: &str = "/conf.toml";
/// A structure that contains the filesystem state and cache.
#[derive(Debug)]
pub struct Filesystem {
vfs: vfs::OverlayFS,
/*resources_path: path::PathBuf,
user_config_path: path::PathBuf,
user_data_path: path::PathBuf,*/
}
/// Represents a file, either in the filesystem, or in the resources zip file,
/// or whatever.
pub enum File {
/// A wrapper for a VFile trait object.
VfsFile(Box<dyn vfs::VFile>),
}
impl fmt::Debug for File {
// Make this more useful?
// But we can't seem to get a filename out of a file,
// soooooo.
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
File::VfsFile(ref _file) => write!(f, "VfsFile"),
}
}
}
impl io::Read for File {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
File::VfsFile(ref mut f) => f.read(buf),
}
}
}
impl io::Write for File {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
match *self {
File::VfsFile(ref mut f) => f.write(buf),
}
}
fn flush(&mut self) -> io::Result<()> {
match *self {
File::VfsFile(ref mut f) => f.flush(),
}
}
}
impl Filesystem {
/// Create a new `Filesystem` instance, using the given `id` and (on
/// some platforms) the `author` as a portion of the user
/// directory path. This function is called automatically by
/// ggez, the end user should never need to call it.
pub fn new(id: &str) -> GameResult<Filesystem> {
let mut root_path = env::current_exe()?;
// Ditch the filename (if any)
if root_path.file_name().is_some() {
let _ = root_path.pop();
}
// Set up VFS to merge resource path, root path, and zip path.
let mut overlay = vfs::OverlayFS::new();
/*let mut resources_path;
let mut resources_zip_path;
let user_data_path;
let user_config_path;
let project_dirs = match ProjectDirs::from("", author, id) {
Some(dirs) => dirs,
None => {
return Err(GameError::FilesystemError(String::from(
"No valid home directory path could be retrieved.",
)));
}
};
// <game exe root>/resources/
{
resources_path = root_path.clone();
resources_path.push("resources");
trace!("Resources path: {:?}", resources_path);
let physfs = vfs::PhysicalFS::new(&resources_path, true);
overlay.push_back(Box::new(physfs));
}
// <root>/resources.zip
{
resources_zip_path = root_path.clone();
resources_zip_path.push("resources.zip");
if resources_zip_path.exists() {
trace!("Resources zip file: {:?}", resources_zip_path);
let zipfs = vfs::ZipFS::new(&resources_zip_path)?;
overlay.push_back(Box::new(zipfs));
} else {
trace!("No resources zip file found");
}
}
// Per-user data dir,
// ~/.local/share/whatever/
{
user_data_path = project_dirs.data_local_dir();
trace!("User-local data path: {:?}", user_data_path);
let physfs = vfs::PhysicalFS::new(&user_data_path, true);
overlay.push_back(Box::new(physfs));
}
// Writeable local dir, ~/.config/whatever/
// Save game dir is read-write
{
user_config_path = project_dirs.config_dir();
trace!("User-local configuration path: {:?}", user_config_path);
let physfs = vfs::PhysicalFS::new(&user_config_path, false);
overlay.push_back(Box::new(physfs));
}*/
let fs = Filesystem {
vfs: overlay,
//user_config_path: user_config_path.to_path_buf(),
//user_data_path: user_data_path.to_path_buf(),
};
Ok(fs)
}
/// Opens the given `path` and returns the resulting `File`
/// in read-only mode.
pub(crate) fn open<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.vfs.open(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Opens a file in the user directory with the given
/// [`filesystem::OpenOptions`](struct.OpenOptions.html).
/// Note that even if you open a file read-write, it can only
/// write to files in the "user" directory.
pub(crate) fn open_options<P: AsRef<path::Path>>(
&mut self,
path: P,
options: OpenOptions,
) -> GameResult<File> {
self.vfs
.open_options(path.as_ref(), options)
.map(|f| File::VfsFile(f))
.map_err(|e| {
GameError::ResourceLoadError(format!(
"Tried to open {:?} but got error: {:?}",
path.as_ref(),
e
))
})
}
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub(crate) fn create<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<File> {
self.vfs.create(path.as_ref()).map(|f| File::VfsFile(f))
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub(crate) fn create_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.mkdir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub(crate) fn delete<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.rm(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub(crate) fn delete_dir<P: AsRef<path::Path>>(&mut self, path: P) -> GameResult<()> {
self.vfs.rmrf(path.as_ref())
}
/// Check whether a file or directory exists.
pub(crate) fn exists<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs.exists(path.as_ref())
}
/// Check whether a path points at a file.
pub(crate) fn is_file<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
.metadata(path.as_ref())
.map(|m| m.is_file())
.unwrap_or(false)
}
/// Check whether a path points at a directory.
pub(crate) fn is_dir<P: AsRef<path::Path>>(&self, path: P) -> bool {
self.vfs
.metadata(path.as_ref())
.map(|m| m.is_dir())
.unwrap_or(false)
}
/// Returns a list of all files and directories in the resource directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub(crate) fn read_dir<P: AsRef<path::Path>>(
&mut self,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
let itr = self.vfs.read_dir(path.as_ref())?.map(|fname| {
fname.expect("Could not read file in read_dir()? Should never happen, I hope!")
});
Ok(Box::new(itr))
}
fn write_to_string(&mut self) -> String {
use std::fmt::Write;
let mut s = String::new();
for vfs in self.vfs.roots() {
write!(s, "Source {:?}", vfs).expect("Could not write to string; should never happen?");
match vfs.read_dir(path::Path::new("/")) {
Ok(files) => {
for itm in files {
write!(s, " {:?}", itm)
.expect("Could not write to string; should never happen?");
}
}
Err(e) => write!(s, " Could not read source: {:?}", e)
.expect("Could not write to string; should never happen?"),
}
}
s
}
/// Prints the contents of all data directories
/// to standard output. Useful for debugging.
pub(crate) fn print_all(&mut self) {
println!("{}", self.write_to_string());
}
/// Outputs the contents of all data directories,
/// using the "info" log level of the [`log`](https://docs.rs/log/) crate.
/// Useful for debugging.
pub(crate) fn log_all(&mut self) {
info!("{}", self.write_to_string());
}
/// Adds the given (absolute) path to the list of directories
/// it will search to look for resources.
///
/// You probably shouldn't use this in the general case, since it is
/// harder than it looks to make it bulletproof across platforms.
/// But it can be very nice for debugging and dev purposes, such as
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
pub(crate) fn mount(&mut self, path: &path::Path, readonly: bool) {
let physfs = vfs::PhysicalFS::new(path, readonly);
trace!("Mounting new path: {:?}", physfs);
self.vfs.push_back(Box::new(physfs));
}
/// Looks for a file named `/conf.toml` in any resource directory and
/// loads it if it finds it.
/// If it can't read it for some reason, returns an error.
pub(crate) fn read_config(&mut self) -> GameResult<conf::Conf> {
let conf_path = path::Path::new(CONFIG_NAME);
if self.is_file(conf_path) {
let mut file = self.open(conf_path)?;
let c = conf::Conf::from_toml_file(&mut file)?;
Ok(c)
} else {
Err(GameError::ConfigError(String::from(
"Config file not found",
)))
}
}
/// Takes a `Conf` object and saves it to the user directory,
/// overwriting any file already there.
pub(crate) fn write_config(&mut self, conf: &conf::Conf) -> GameResult<()> {
let conf_path = path::Path::new(CONFIG_NAME);
let mut file = self.create(conf_path)?;
conf.to_toml_file(&mut file)?;
if self.is_file(conf_path) {
Ok(())
} else {
Err(GameError::ConfigError(format!(
"Failed to write config file at {}",
conf_path.to_string_lossy()
)))
}
}
}
/// Opens the given path and returns the resulting `File`
/// in read-only mode.
pub fn open<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.open(path)
}
/// Opens a file in the user directory with the given `filesystem::OpenOptions`.
/// Note that even if you open a file read-only, it can only access
/// files in the user directory.
pub fn open_options<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
options: OpenOptions,
) -> GameResult<File> {
ctx.filesystem.open_options(path, options)
}
/// Creates a new file in the user directory and opens it
/// to be written to, truncating it if it already exists.
pub fn create<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult<File> {
ctx.filesystem.create(path)
}
/// Create an empty directory in the user dir
/// with the given name. Any parents to that directory
/// that do not exist will be created.
pub fn create_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.create_dir(path.as_ref())
}
/// Deletes the specified file in the user dir.
pub fn delete<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.delete(path.as_ref())
}
/// Deletes the specified directory in the user dir,
/// and all its contents!
pub fn delete_dir<P: AsRef<path::Path>>(ctx: &mut Context, path: P) -> GameResult {
ctx.filesystem.delete_dir(path.as_ref())
}
/// Check whether a file or directory exists.
pub fn exists<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.exists(path.as_ref())
}
/// Check whether a path points at a file.
pub fn is_file<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.is_file(path)
}
/// Check whether a path points at a directory.
pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
ctx.filesystem.is_dir(path)
}
/// Returns a list of all files and directories in the resource directory,
/// in no particular order.
///
/// Lists the base directory if an empty path is given.
pub fn read_dir<P: AsRef<path::Path>>(
ctx: &mut Context,
path: P,
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
ctx.filesystem.read_dir(path)
}
/// Prints the contents of all data directories.
/// Useful for debugging.
pub fn print_all(ctx: &mut Context) {
ctx.filesystem.print_all()
}
/// Outputs the contents of all data directories,
/// using the "info" log level of the `log` crate.
/// Useful for debugging.
///
/// See the [`logging` example](https://github.com/ggez/ggez/blob/master/examples/eventloop.rs)
/// for how to collect log information.
pub fn log_all(ctx: &mut Context) {
ctx.filesystem.log_all()
}
/// Adds the given (absolute) path to the list of directories
/// it will search to look for resources.
///
/// You probably shouldn't use this in the general case, since it is
/// harder than it looks to make it bulletproof across platforms.
/// But it can be very nice for debugging and dev purposes, such as
/// by pushing `$CARGO_MANIFEST_DIR/resources` to it
pub fn mount(ctx: &mut Context, path: &path::Path, readonly: bool) {
ctx.filesystem.mount(path, readonly)
}
/// Looks for a file named `/conf.toml` in any resource directory and
/// loads it if it finds it.
/// If it can't read it for some reason, returns an error.
pub fn read_config(ctx: &mut Context) -> GameResult<conf::Conf> {
ctx.filesystem.read_config()
}
/// Takes a `Conf` object and saves it to the user directory,
/// overwriting any file already there.
pub fn write_config(ctx: &mut Context, conf: &conf::Conf) -> GameResult {
ctx.filesystem.write_config(conf)
}
#[cfg(test)]
mod tests {
use std::io::{Read, Write};
use std::path;
use crate::ggez::conf;
use crate::ggez::error::*;
use crate::ggez::filesystem::*;
use crate::ggez::vfs;
fn dummy_fs_for_tests() -> Filesystem {
let mut path = path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
path.push("resources");
let physfs = vfs::PhysicalFS::new(&path, false);
let mut ofs = vfs::OverlayFS::new();
ofs.push_front(Box::new(physfs));
Filesystem {
vfs: ofs,
}
}
#[test]
fn headless_test_file_exists() {
let f = dummy_fs_for_tests();
let tile_file = path::Path::new("/tile.png");
assert!(f.exists(tile_file));
assert!(f.is_file(tile_file));
let tile_file = path::Path::new("/oglebog.png");
assert!(!f.exists(tile_file));
assert!(!f.is_file(tile_file));
assert!(!f.is_dir(tile_file));
}
#[test]
fn headless_test_read_dir() {
let mut f = dummy_fs_for_tests();
let dir_contents_size = f.read_dir("/").unwrap().count();
assert!(dir_contents_size > 0);
}
#[test]
fn headless_test_create_delete_file() {
let mut fs = dummy_fs_for_tests();
let test_file = path::Path::new("/testfile.txt");
let bytes = "test".as_bytes();
{
let mut file = fs.create(test_file).unwrap();
let _ = file.write(bytes).unwrap();
}
{
let mut buffer = Vec::new();
let mut file = fs.open(test_file).unwrap();
let _ = file.read_to_end(&mut buffer).unwrap();
assert_eq!(bytes, buffer.as_slice());
}
fs.delete(test_file).unwrap();
}
#[test]
fn headless_test_file_not_found() {
let mut fs = dummy_fs_for_tests();
{
let rel_file = "testfile.txt";
match fs.open(rel_file) {
Err(GameError::ResourceNotFound(_, _)) => (),
Err(e) => panic!("Invalid error for opening file with relative path: {:?}", e),
Ok(f) => panic!("Should have gotten an error but instead got {:?}!", f),
}
}
{
// This absolute path should work on Windows too since we
// completely remove filesystem roots.
match fs.open("/ooglebooglebarg.txt") {
Err(GameError::ResourceNotFound(_, _)) => (),
Err(e) => panic!("Invalid error for opening nonexistent file: {}", e),
Ok(f) => panic!("Should have gotten an error but instead got {:?}", f),
}
}
}
#[test]
fn headless_test_write_config() {
let mut f = dummy_fs_for_tests();
let conf = conf::Conf::new();
// The config file should end up in
// the resources directory with this
match f.write_config(&conf) {
Ok(_) => (),
Err(e) => panic!("{:?}", e),
}
// Remove the config file!
f.delete(CONFIG_NAME).unwrap();
}
}

Binary file not shown.

164
src/ggez/graphics/canvas.rs Normal file
View File

@ -0,0 +1,164 @@
//! I guess these docs will never appear since we re-export the canvas
//! module from graphics...
use gfx::format::Swizzle;
use gfx::handle::RawRenderTargetView;
use gfx::memory::{Bind, Usage};
use gfx::texture::{AaMode, Kind};
use gfx::Factory;
use crate::ggez::conf;
use crate::ggez::context::DebugId;
use crate::ggez::error::*;
use crate::ggez::graphics::*;
use crate::ggez::Context;
/// A generic canvas independent of graphics backend. This type should
/// never need to be used directly; use [`graphics::Canvas`](type.Canvas.html)
/// instead.
#[derive(Debug)]
pub struct CanvasGeneric<Spec>
where
Spec: BackendSpec,
{
target: RawRenderTargetView<Spec::Resources>,
image: Image,
debug_id: DebugId,
}
/// A canvas that can be rendered to instead of the screen (sometimes referred
/// to as "render target" or "render to texture"). Set the canvas with the
/// [`graphics::set_canvas()`](fn.set_canvas.html) function, and then anything you
/// draw will be drawn to the canvas instead of the screen.
///
/// Resume drawing to the screen by calling `graphics::set_canvas(None)`.
///
/// A `Canvas` allows graphics to be rendered to images off-screen
/// in order to do things like saving to an image file or creating cool effects
/// by using shaders that render to an image.
/// If you just want to draw multiple things efficiently, look at
/// [`SpriteBatch`](spritebatch/struct.Spritebatch.html).
pub type Canvas = CanvasGeneric<GlBackendSpec>;
impl Canvas {
/// Create a new `Canvas` with the given size and number of samples.
pub fn new(
ctx: &mut Context,
width: u16,
height: u16,
samples: conf::NumSamples,
) -> GameResult<Canvas> {
let debug_id = DebugId::get(ctx);
let aa = match samples {
conf::NumSamples::One => AaMode::Single,
s => AaMode::Multi(s as u8),
};
let kind = Kind::D2(width, height, aa);
let levels = 1;
let color_format = ctx.gfx_context.color_format();
let factory = &mut ctx.gfx_context.factory;
let texture_create_info = gfx::texture::Info {
kind,
levels,
format: color_format.0,
bind: Bind::SHADER_RESOURCE | Bind::RENDER_TARGET | Bind::TRANSFER_SRC,
usage: Usage::Data,
};
let tex = factory.create_texture_raw(texture_create_info, Some(color_format.1), None)?;
let resource_desc = gfx::texture::ResourceDesc {
channel: color_format.1,
layer: None,
min: 0,
max: levels - 1,
swizzle: Swizzle::new(),
};
let resource = factory.view_texture_as_shader_resource_raw(&tex, resource_desc)?;
let render_desc = gfx::texture::RenderDesc {
channel: color_format.1,
level: 0,
layer: None,
};
let target = factory.view_texture_as_render_target_raw(&tex, render_desc)?;
Ok(Canvas {
target,
image: Image {
texture: resource,
texture_handle: tex,
sampler_info: ctx.gfx_context.default_sampler_info,
blend_mode: None,
width,
height,
debug_id,
},
debug_id,
})
}
/// Create a new `Canvas` with the current window dimensions.
pub fn with_window_size(ctx: &mut Context) -> GameResult<Canvas> {
use crate::graphics;
let (w, h) = graphics::drawable_size(ctx);
// Default to no multisampling
Canvas::new(ctx, w as u16, h as u16, conf::NumSamples::One)
}
/// Gets the backend `Image` that is being rendered to.
pub fn image(&self) -> &Image {
&self.image
}
/// Get the filter mode for the image.
pub fn filter(&self) -> FilterMode {
self.image.filter()
}
/// Set the filter mode for the canvas.
pub fn set_filter(&mut self, mode: FilterMode) {
self.image.set_filter(mode)
}
/// Destroys the `Canvas` and returns the `Image` it contains.
pub fn into_inner(self) -> Image {
// TODO: This texture is created with different settings
// than the default; does that matter?
// Test; we really just need to add Bind::TRANSFER_SRC
// and change the Usage's to match to make them identical.
// Ask termhn maybe?
self.image
}
}
impl Drawable for Canvas {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
self.debug_id.assert(ctx);
// Gotta flip the image on the Y axis here
// to account for OpenGL's origin being at the bottom-left.
let mut flipped_param = param;
flipped_param.scale.y *= -1.0;
flipped_param.dest.y += f32::from(self.image.height()) * param.scale.y;
self.image.draw(ctx, flipped_param)?;
Ok(())
}
fn dimensions(&self, _: &mut Context) -> Option<Rect> {
Some(self.image.dimensions())
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.image.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.image.blend_mode
}
}
/// Set the `Canvas` to render to. Specifying `Option::None` will cause all
/// rendering to be done directly to the screen.
pub fn set_canvas(ctx: &mut Context, target: Option<&Canvas>) {
match target {
Some(surface) => {
surface.debug_id.assert(ctx);
ctx.gfx_context.data.out = surface.target.clone();
}
None => {
ctx.gfx_context.data.out = ctx.gfx_context.screen_render_target.clone();
}
};
}

View File

@ -0,0 +1,606 @@
use std::cell::RefCell;
use std::rc::Rc;
use gfx::traits::FactoryExt;
use gfx::Factory;
use glutin;
use glyph_brush::{GlyphBrush, GlyphBrushBuilder};
use winit::{self, dpi};
use crate::ggez::conf::{FullscreenType, WindowMode, WindowSetup};
use crate::ggez::context::DebugId;
use crate::ggez::filesystem::Filesystem;
use crate::ggez::graphics::*;
use crate::ggez::error::GameResult;
/// A structure that contains graphics state.
/// For instance,
/// window info, DPI, rendering pipeline state, etc.
///
/// As an end-user you shouldn't ever have to touch this.
pub(crate) struct GraphicsContextGeneric<B>
where
B: BackendSpec,
{
shader_globals: Globals,
pub(crate) projection: Matrix4,
pub(crate) modelview_stack: Vec<Matrix4>,
pub(crate) white_image: ImageGeneric<B>,
pub(crate) screen_rect: Rect,
color_format: gfx::format::Format,
depth_format: gfx::format::Format,
srgb: bool,
pub(crate) backend_spec: B,
pub(crate) window: glutin::WindowedContext,
pub(crate) multisample_samples: u8,
pub(crate) device: Box<B::Device>,
pub(crate) factory: Box<B::Factory>,
pub(crate) encoder: gfx::Encoder<B::Resources, B::CommandBuffer>,
pub(crate) screen_render_target: gfx::handle::RawRenderTargetView<B::Resources>,
#[allow(dead_code)]
pub(crate) depth_view: gfx::handle::RawDepthStencilView<B::Resources>,
pub(crate) data: pipe::Data<B::Resources>,
pub(crate) quad_slice: gfx::Slice<B::Resources>,
pub(crate) quad_vertex_buffer: gfx::handle::Buffer<B::Resources, Vertex>,
pub(crate) default_sampler_info: texture::SamplerInfo,
pub(crate) samplers: SamplerCache<B>,
default_shader: ShaderId,
pub(crate) current_shader: Rc<RefCell<Option<ShaderId>>>,
pub(crate) shaders: Vec<Box<dyn ShaderHandle<B>>>,
pub(crate) glyph_brush: GlyphBrush<'static, DrawParam>,
pub(crate) glyph_cache: ImageGeneric<B>,
pub(crate) glyph_state: Rc<RefCell<spritebatch::SpriteBatch>>,
}
impl<B> fmt::Debug for GraphicsContextGeneric<B>
where
B: BackendSpec,
{
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "<GraphicsContext: {:p}>", self)
}
}
/// A concrete graphics context for GL rendering.
pub(crate) type GraphicsContext = GraphicsContextGeneric<GlBackendSpec>;
impl GraphicsContextGeneric<GlBackendSpec> {
/// Create a new GraphicsContext
pub(crate) fn new(
filesystem: &mut Filesystem,
events_loop: &winit::EventsLoop,
window_setup: &WindowSetup,
window_mode: WindowMode,
backend: GlBackendSpec,
debug_id: DebugId,
) -> GameResult<Self> {
let srgb = window_setup.srgb;
let color_format = if srgb {
gfx::format::Format(
gfx::format::SurfaceType::R8_G8_B8_A8,
gfx::format::ChannelType::Srgb,
)
} else {
gfx::format::Format(
gfx::format::SurfaceType::R8_G8_B8_A8,
gfx::format::ChannelType::Unorm,
)
};
let depth_format = gfx::format::Format(
gfx::format::SurfaceType::D24_S8,
gfx::format::ChannelType::Unorm,
);
// WINDOW SETUP
let gl_builder = glutin::ContextBuilder::new()
.with_gl(glutin::GlRequest::Specific(
backend.api(),
backend.version_tuple(),
))
.with_gl_profile(glutin::GlProfile::Core)
.with_multisampling(window_setup.samples as u16)
// 24 color bits, 8 alpha bits
.with_pixel_format(24, 8)
.with_vsync(window_setup.vsync);
let window_size =
dpi::LogicalSize::from((f64::from(window_mode.width), f64::from(window_mode.height)));
let mut window_builder = winit::WindowBuilder::new()
.with_title(window_setup.title.clone())
.with_dimensions(window_size)
.with_resizable(window_mode.resizable);
window_builder = if !window_setup.icon.is_empty() {
let icon = load_icon(window_setup.icon.as_ref(), filesystem)?;
window_builder.with_window_icon(Some(icon))
} else {
window_builder
};
let (window, device, mut factory, screen_render_target, depth_view) = backend.init(
window_builder,
gl_builder,
events_loop,
color_format,
depth_format,
)?;
// see winit #548 about DPI.
// We basically ignore it and if it's wrong, that's a winit bug
// since we have no good control over it.
{
// Log a bunch of OpenGL state info pulled out of winit and gfx
let dpi::LogicalSize {
width: w,
height: h,
} = window
.get_outer_size()
.ok_or_else(|| GameError::VideoError("Window doesn't exist!".to_owned()))?;
let dpi::LogicalSize {
width: dw,
height: dh,
} = window
.get_inner_size()
.ok_or_else(|| GameError::VideoError("Window doesn't exist!".to_owned()))?;
let hidpi_factor = window.get_hidpi_factor();
debug!(
"Window created, desired size {}x{}, hidpi factor {}.",
window_mode.width, window_mode.height, hidpi_factor
);
let (major, minor) = backend.version_tuple();
debug!(
" Window logical outer size: {}x{}, logical drawable size: {}x{}",
w, h, dw, dh
);
let device_info = backend.info(&device);
debug!(
" Asked for : {:?} {}.{} Core, vsync: {}",
backend.api(),
major,
minor,
window_setup.vsync
);
debug!(" Actually got: {}", device_info);
}
// GFX SETUP
let mut encoder = GlBackendSpec::encoder(&mut factory);
let blend_modes = [
BlendMode::Alpha,
BlendMode::Add,
BlendMode::Subtract,
BlendMode::Invert,
BlendMode::Multiply,
BlendMode::Replace,
BlendMode::Lighten,
BlendMode::Darken,
];
let multisample_samples = window_setup.samples as u8;
let (vs_text, fs_text) = backend.shaders();
let (shader, draw) = create_shader(
vs_text,
fs_text,
EmptyConst,
"Empty",
&mut encoder,
&mut factory,
multisample_samples,
Some(&blend_modes[..]),
color_format,
debug_id,
)?;
let rect_inst_props = factory.create_buffer(
1,
gfx::buffer::Role::Vertex,
gfx::memory::Usage::Dynamic,
gfx::memory::Bind::SHADER_RESOURCE,
)?;
let (quad_vertex_buffer, mut quad_slice) =
factory.create_vertex_buffer_with_slice(&QUAD_VERTS, &QUAD_INDICES[..]);
quad_slice.instances = Some((1, 0));
let globals_buffer = factory.create_constant_buffer(1);
let mut samplers: SamplerCache<GlBackendSpec> = SamplerCache::new();
let sampler_info =
texture::SamplerInfo::new(texture::FilterMethod::Bilinear, texture::WrapMode::Clamp);
let sampler = samplers.get_or_insert(sampler_info, &mut factory);
let white_image = ImageGeneric::make_raw(
&mut factory,
&sampler_info,
1,
1,
&[255, 255, 255, 255],
color_format,
debug_id,
)?;
let texture = white_image.texture.clone();
let typed_thingy = backend.raw_to_typed_shader_resource(texture);
let data = pipe::Data {
vbuf: quad_vertex_buffer.clone(),
tex: (typed_thingy, sampler),
rect_instance_properties: rect_inst_props,
globals: globals_buffer,
out: screen_render_target.clone(),
};
// Glyph cache stuff.
let glyph_brush =
GlyphBrushBuilder::using_font_bytes(Font::default_font_bytes().to_vec()).build();
let (glyph_cache_width, glyph_cache_height) = glyph_brush.texture_dimensions();
let initial_contents =
vec![255; 4 * glyph_cache_width as usize * glyph_cache_height as usize];
let glyph_cache = ImageGeneric::make_raw(
&mut factory,
&sampler_info,
glyph_cache_width as u16,
glyph_cache_height as u16,
&initial_contents,
color_format,
debug_id,
)?;
let glyph_state = Rc::new(RefCell::new(spritebatch::SpriteBatch::new(
glyph_cache.clone(),
)));
// Set initial uniform values
let left = 0.0;
let right = window_mode.width;
let top = 0.0;
let bottom = window_mode.height;
let initial_projection = Matrix4::identity(); // not the actual initial projection matrix, just placeholder
let initial_transform = Matrix4::identity();
let globals = Globals {
mvp_matrix: initial_projection.into(),
};
let mut gfx = Self {
shader_globals: globals,
projection: initial_projection,
modelview_stack: vec![initial_transform],
white_image,
screen_rect: Rect::new(left, top, right - left, bottom - top),
color_format,
depth_format,
srgb,
backend_spec: backend,
window,
multisample_samples,
device: Box::new(device as <GlBackendSpec as BackendSpec>::Device),
factory: Box::new(factory as <GlBackendSpec as BackendSpec>::Factory),
encoder,
screen_render_target,
depth_view,
data,
quad_slice,
quad_vertex_buffer,
default_sampler_info: sampler_info,
samplers,
default_shader: shader.shader_id(),
current_shader: Rc::new(RefCell::new(None)),
shaders: vec![draw],
glyph_brush,
glyph_cache,
glyph_state,
};
gfx.set_window_mode(window_mode)?;
// Calculate and apply the actual initial projection matrix
let w = window_mode.width;
let h = window_mode.height;
let rect = Rect {
x: 0.0,
y: 0.0,
w,
h,
};
gfx.set_projection_rect(rect);
gfx.calculate_transform_matrix();
gfx.update_globals()?;
Ok(gfx)
}
}
// This is kinda awful 'cause it copies a couple times,
// but still better than
// having `winit` try to do the image loading for us.
// see https://github.com/tomaka/winit/issues/661
pub(crate) fn load_icon(icon_file: &Path, filesystem: &mut Filesystem) -> GameResult<winit::Icon> {
use ::image;
use ::image::GenericImageView;
use std::io::Read;
use winit::Icon;
let mut buf = Vec::new();
let mut reader = filesystem.open(icon_file)?;
let _ = reader.read_to_end(&mut buf)?;
let i = image::load_from_memory(&buf)?;
let image_data = i.to_rgba();
Icon::from_rgba(image_data.to_vec(), i.width(), i.height()).map_err(|e| {
let msg = format!("Could not load icon: {:?}", e);
GameError::ResourceLoadError(msg)
})
}
impl<B> GraphicsContextGeneric<B>
where
B: BackendSpec + 'static,
{
/// Sends the current value of the graphics context's shader globals
/// to the graphics card.
pub(crate) fn update_globals(&mut self) -> GameResult {
self.encoder
.update_buffer(&self.data.globals, &[self.shader_globals], 0)?;
Ok(())
}
/// Recalculates the context's Model-View-Projection matrix based on
/// the matrices on the top of the respective stacks and the projection
/// matrix.
pub(crate) fn calculate_transform_matrix(&mut self) {
let modelview = self
.modelview_stack
.last()
.expect("Transform stack empty; should never happen");
let mvp = self.projection * modelview;
self.shader_globals.mvp_matrix = mvp.into();
}
/// Pushes a homogeneous transform matrix to the top of the transform
/// (model) matrix stack.
pub(crate) fn push_transform(&mut self, t: Matrix4) {
self.modelview_stack.push(t);
}
/// Pops the current transform matrix off the top of the transform
/// (model) matrix stack. Will never pop the last transform.
pub(crate) fn pop_transform(&mut self) {
if self.modelview_stack.len() > 1 {
let _ = self.modelview_stack.pop();
}
}
/// Sets the current model-view transform matrix.
pub(crate) fn set_transform(&mut self, t: Matrix4) {
assert!(
!self.modelview_stack.is_empty(),
"Tried to set a transform on an empty transform stack!"
);
let last = self
.modelview_stack
.last_mut()
.expect("Transform stack empty; should never happen!");
*last = t;
}
/// Gets a copy of the current transform matrix.
pub(crate) fn transform(&self) -> Matrix4 {
assert!(
!self.modelview_stack.is_empty(),
"Tried to get a transform on an empty transform stack!"
);
let last = self
.modelview_stack
.last()
.expect("Transform stack empty; should never happen!");
*last
}
/// Converts the given `DrawParam` into an `InstanceProperties` object and
/// sends it to the graphics card at the front of the instance buffer.
pub(crate) fn update_instance_properties(&mut self, draw_params: DrawTransform) -> GameResult {
let mut new_draw_params = draw_params;
new_draw_params.color = draw_params.color;
let properties = new_draw_params.to_instance_properties(self.srgb);
self.encoder
.update_buffer(&self.data.rect_instance_properties, &[properties], 0)?;
Ok(())
}
/// Draws with the current encoder, slice, and pixel shader. Prefer calling
/// this method from `Drawables` so that the pixel shader gets used
pub(crate) fn draw(&mut self, slice: Option<&gfx::Slice<B::Resources>>) -> GameResult {
let slice = slice.unwrap_or(&self.quad_slice);
let id = (*self.current_shader.borrow()).unwrap_or(self.default_shader);
let shader_handle = &self.shaders[id];
shader_handle.draw(&mut self.encoder, slice, &self.data)?;
Ok(())
}
/// Sets the blend mode of the active shader
pub(crate) fn set_blend_mode(&mut self, mode: BlendMode) -> GameResult {
let id = (*self.current_shader.borrow()).unwrap_or(self.default_shader);
let shader_handle = &mut self.shaders[id];
shader_handle.set_blend_mode(mode)
}
/// Gets the current blend mode of the active shader
pub(crate) fn blend_mode(&self) -> BlendMode {
let id = (*self.current_shader.borrow()).unwrap_or(self.default_shader);
let shader_handle = &self.shaders[id];
shader_handle.blend_mode()
}
/// Shortcut function to set the projection matrix to an
/// orthographic projection based on the given `Rect`.
///
/// Call `update_globals()` to apply it after calling this.
pub(crate) fn set_projection_rect(&mut self, rect: Rect) {
/// Creates an orthographic projection matrix.
/// Because nalgebra gets frumple when you try to make
/// one that is upside-down.
/// This is fixed now (issue here: https://github.com/rustsim/nalgebra/issues/365)
/// but removing this kinda isn't worth it.
fn ortho(
left: f32,
right: f32,
top: f32,
bottom: f32,
far: f32,
near: f32,
) -> [[f32; 4]; 4] {
let c0r0 = 2.0 / (right - left);
let c0r1 = 0.0;
let c0r2 = 0.0;
let c0r3 = 0.0;
let c1r0 = 0.0;
let c1r1 = 2.0 / (top - bottom);
let c1r2 = 0.0;
let c1r3 = 0.0;
let c2r0 = 0.0;
let c2r1 = 0.0;
let c2r2 = -2.0 / (far - near);
let c2r3 = 0.0;
let c3r0 = -(right + left) / (right - left);
let c3r1 = -(top + bottom) / (top - bottom);
let c3r2 = -(far + near) / (far - near);
let c3r3 = 1.0;
// our matrices are column-major, so here we are.
[
[c0r0, c0r1, c0r2, c0r3],
[c1r0, c1r1, c1r2, c1r3],
[c2r0, c2r1, c2r2, c2r3],
[c3r0, c3r1, c3r2, c3r3],
]
}
self.screen_rect = rect;
self.projection = Matrix4::from(ortho(
rect.x,
rect.x + rect.w,
rect.y,
rect.y + rect.h,
-1.0,
1.0,
));
}
/// Sets the raw projection matrix to the given Matrix.
///
/// Call `update_globals()` to apply after calling this.
pub(crate) fn set_projection(&mut self, mat: Matrix4) {
self.projection = mat;
}
/// Gets a copy of the raw projection matrix.
pub(crate) fn projection(&self) -> Matrix4 {
self.projection
}
/// Sets window mode from a WindowMode object.
pub(crate) fn set_window_mode(&mut self, mode: WindowMode) -> GameResult {
let window = &self.window;
window.set_maximized(mode.maximized);
// TODO LATER: find out if single-dimension constraints are possible?
let min_dimensions = if mode.min_width > 0.0 && mode.min_height > 0.0 {
Some(dpi::LogicalSize {
width: f64::from(mode.min_width),
height: f64::from(mode.min_height),
})
} else {
None
};
window.set_min_dimensions(min_dimensions);
let max_dimensions = if mode.max_width > 0.0 && mode.max_height > 0.0 {
Some(dpi::LogicalSize {
width: f64::from(mode.max_width),
height: f64::from(mode.max_height),
})
} else {
None
};
window.set_max_dimensions(max_dimensions);
let monitor = window.get_current_monitor();
match mode.fullscreen_type {
FullscreenType::Windowed => {
window.set_fullscreen(None);
window.set_decorations(!mode.borderless);
window.set_inner_size(dpi::LogicalSize {
width: f64::from(mode.width),
height: f64::from(mode.height),
});
window.set_resizable(mode.resizable);
}
FullscreenType::True => {
window.set_fullscreen(Some(monitor));
window.set_inner_size(dpi::LogicalSize {
width: f64::from(mode.width),
height: f64::from(mode.height),
});
}
FullscreenType::Desktop => {
let position = monitor.get_position();
let dimensions = monitor.get_dimensions();
let hidpi_factor = window.get_hidpi_factor();
window.set_fullscreen(None);
window.set_decorations(false);
window.set_inner_size(dimensions.to_logical(hidpi_factor));
window.set_position(position.to_logical(hidpi_factor));
}
}
Ok(())
}
/// Communicates changes in the viewport size between glutin and gfx.
///
/// Also replaces gfx.screen_render_target and gfx.depth_view,
/// so it may cause squirrelliness to
/// happen with canvases or other things that touch it.
pub(crate) fn resize_viewport(&mut self) {
if let Some((cv, dv)) = self.backend_spec.resize_viewport(
&self.screen_render_target,
&self.depth_view,
self.color_format(),
self.depth_format(),
&self.window,
) {
self.screen_render_target = cv;
self.depth_view = dv;
}
}
/// Returns the screen color format used by the context.
pub(crate) fn color_format(&self) -> gfx::format::Format {
self.color_format
}
/// Returns the screen depth format used by the context.
///
pub(crate) fn depth_format(&self) -> gfx::format::Format {
self.depth_format
}
/// Simple shortcut to check whether the context's color
/// format is SRGB or not.
pub(crate) fn is_srgb(&self) -> bool {
if let gfx::format::Format(_, gfx::format::ChannelType::Srgb) = self.color_format() {
true
} else {
false
}
}
}

View File

@ -0,0 +1,264 @@
use crate::graphics::*;
use mint;
/// A struct containing all the necessary info for drawing a [`Drawable`](trait.Drawable.html).
///
/// This struct implements the `Default` trait, so to set only some parameter
/// you can just do:
///
/// ```rust
/// # use ggez::*;
/// # use ggez::graphics::*;
/// # fn t<P>(ctx: &mut Context, drawable: &P) where P: Drawable {
/// let my_dest = nalgebra::Point2::new(13.0, 37.0);
/// graphics::draw(ctx, drawable, DrawParam::default().dest(my_dest) );
/// # }
/// ```
///
/// As a shortcut, it also implements `From` for a variety of tuple types.
#[derive(Debug, Copy, Clone, PartialEq)]
pub struct DrawParam {
/// A portion of the drawable to clip, as a fraction of the whole image.
/// Defaults to the whole image `(0,0 to 1,1)` if omitted.
pub src: Rect,
/// The position to draw the graphic expressed as a `Point2`.
pub dest: mint::Point2<f32>,
/// The orientation of the graphic in radians.
pub rotation: f32,
/// The x/y scale factors expressed as a `Vector2`.
pub scale: mint::Vector2<f32>,
/// An offset from the center for transform operations like scale/rotation,
/// with `0,0` meaning the origin and `1,1` meaning the opposite corner from the origin.
/// By default these operations are done from the top-left corner, so to rotate something
/// from the center specify `Point2::new(0.5, 0.5)` here.
pub offset: mint::Point2<f32>,
/// A color to draw the target with.
/// Default: white.
pub color: Color,
}
impl Default for DrawParam {
fn default() -> Self {
DrawParam {
src: Rect::one(),
dest: mint::Point2 { x: 0.0, y: 0.0 },
rotation: 0.0,
scale: mint::Vector2 { x: 1.0, y: 1.0 },
offset: mint::Point2 { x: 0.0, y: 0.0 },
color: WHITE,
}
}
}
impl DrawParam {
/// Create a new DrawParam with default values.
pub fn new() -> Self {
Self::default()
}
/// Set the source rect
pub fn src(mut self, src: Rect) -> Self {
self.src = src;
self
}
/// Set the dest point
pub fn dest<P>(mut self, dest: P) -> Self
where
P: Into<mint::Point2<f32>>,
{
let p: mint::Point2<f32> = dest.into();
self.dest = p;
self
}
/// Set the drawable color. This will be blended with whatever
/// color the drawn object already is.
pub fn color(mut self, color: Color) -> Self {
self.color = color;
self
}
/// Set the rotation of the drawable.
pub fn rotation(mut self, rotation: f32) -> Self {
self.rotation = rotation;
self
}
/// Set the scaling factors of the drawable.
pub fn scale<V>(mut self, scale: V) -> Self
where
V: Into<mint::Vector2<f32>>,
{
let p: mint::Vector2<f32> = scale.into();
self.scale = p;
self
}
/// Set the transformation offset of the drawable.
pub fn offset<P>(mut self, offset: P) -> Self
where
P: Into<mint::Point2<f32>>,
{
let p: mint::Point2<f32> = offset.into();
self.offset = p;
self
}
/// A [`DrawParam`](struct.DrawParam.html) that has been crunched down to a single matrix.
fn to_na_matrix(&self) -> Matrix4 {
// Calculate a matrix equivalent to doing this:
// type Vec3 = na::Vector3<f32>;
// let translate = Matrix4::new_translation(&Vec3::new(self.dest.x, self.dest.y, 0.0));
// let offset = Matrix4::new_translation(&Vec3::new(self.offset.x, self.offset.y, 0.0));
// let offset_inverse =
// Matrix4::new_translation(&Vec3::new(-self.offset.x, -self.offset.y, 0.0));
// let axis_angle = Vec3::z() * self.rotation;
// let rotation = Matrix4::new_rotation(axis_angle);
// let scale = Matrix4::new_nonuniform_scaling(&Vec3::new(self.scale.x, self.scale.y, 1.0));
// translate * offset * rotation * scale * offset_inverse
let cosr = self.rotation.cos();
let sinr = self.rotation.sin();
let m00 = cosr * self.scale.x;
let m01 = -sinr * self.scale.y;
let m10 = sinr * self.scale.x;
let m11 = cosr * self.scale.y;
let m03 = self.offset.x * (1.0 - m00) - self.offset.y * m01 + self.dest.x;
let m13 = self.offset.y * (1.0 - m11) - self.offset.x * m10 + self.dest.y;
Matrix4::new(m00, m01, 0.0, m03, m10, m11, 0.0, m13, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0)
}
/// A [`DrawParam`](struct.DrawParam.html) that has been crunched down to a single
///matrix. Because of this it only contains the transform part (rotation/scale/etc),
/// with no src/dest/color info.
pub fn to_matrix(&self) -> mint::ColumnMatrix4<f32> {
self.to_na_matrix().into()
}
}
/// Create a `DrawParam` from a location.
/// Note that this takes a single-element tuple.
/// It's a little weird but keeps the trait implementations
/// from clashing.
impl<P> From<(P,)> for DrawParam
where
P: Into<mint::Point2<f32>>,
{
fn from(location: (P,)) -> Self {
DrawParam::new().dest(location.0)
}
}
/// Create a `DrawParam` from a location and color
impl<P> From<(P, Color)> for DrawParam
where
P: Into<mint::Point2<f32>>,
{
fn from((location, color): (P, Color)) -> Self {
DrawParam::new().dest(location).color(color)
}
}
/// Create a `DrawParam` from a location, rotation and color
impl<P> From<(P, f32, Color)> for DrawParam
where
P: Into<mint::Point2<f32>>,
{
fn from((location, rotation, color): (P, f32, Color)) -> Self {
DrawParam::new()
.dest(location)
.rotation(rotation)
.color(color)
}
}
/// Create a `DrawParam` from a location, rotation, offset and color
impl<P> From<(P, f32, P, Color)> for DrawParam
where
P: Into<mint::Point2<f32>>,
{
fn from((location, rotation, offset, color): (P, f32, P, Color)) -> Self {
DrawParam::new()
.dest(location)
.rotation(rotation)
.offset(offset)
.color(color)
}
}
/// Create a `DrawParam` from a location, rotation, offset, scale and color
impl<P, V> From<(P, f32, P, V, Color)> for DrawParam
where
P: Into<mint::Point2<f32>>,
V: Into<mint::Vector2<f32>>,
{
fn from((location, rotation, offset, scale, color): (P, f32, P, V, Color)) -> Self {
DrawParam::new()
.dest(location)
.rotation(rotation)
.offset(offset)
.scale(scale)
.color(color)
}
}
/// A [`DrawParam`](struct.DrawParam.html) that has been crunched down to a single matrix.
/// This is a lot less useful for doing transformations than I'd hoped; basically, we sometimes
/// have to modify parameters of a `DrawParam` based *on* the parameters of a `DrawParam`, for
/// instance when scaling images so that they are in units of pixels. This makes it really
/// hard to extract scale and rotation and such, so meh.
///
/// It's still useful for a couple internal things though, so it's kept around.
#[derive(Debug, Copy, Clone, PartialEq)]
pub(crate) struct DrawTransform {
/// The transform matrix for the DrawParams
pub matrix: Matrix4,
/// A portion of the drawable to clip, as a fraction of the whole image.
/// Defaults to the whole image (1.0) if omitted.
pub src: Rect,
/// A color to draw the target with.
/// Default: white.
pub color: Color,
}
impl Default for DrawTransform {
fn default() -> Self {
DrawTransform {
matrix: na::one(),
src: Rect::one(),
color: WHITE,
}
}
}
impl From<DrawParam> for DrawTransform {
fn from(param: DrawParam) -> Self {
let transform = param.to_matrix();
DrawTransform {
src: param.src,
color: param.color,
matrix: transform.into(),
}
}
}
impl DrawTransform {
pub(crate) fn to_instance_properties(&self, srgb: bool) -> InstanceProperties {
let mat: [[f32; 4]; 4] = self.matrix.into();
let color: [f32; 4] = if srgb {
let linear_color: types::LinearColor = self.color.into();
linear_color.into()
} else {
self.color.into()
};
InstanceProperties {
src: self.src.into(),
col1: mat[0],
col2: mat[1],
col3: mat[2],
col4: mat[3],
color,
}
}
}

393
src/ggez/graphics/image.rs Normal file
View File

@ -0,0 +1,393 @@
use std::io::Read;
use std::path;
use ::image;
use gfx;
use crate::ggez::context::{Context, DebugId};
use crate::ggez::error::GameError;
use crate::ggez::error::GameResult;
use crate::ggez::filesystem;
use crate::ggez::graphics;
use crate::ggez::graphics::shader::*;
use crate::ggez::graphics::*;
/// Generic in-GPU-memory image data available to be drawn on the screen.
/// You probably just want to look at the `Image` type.
#[derive(Clone, PartialEq)]
pub struct ImageGeneric<B>
where
B: BackendSpec,
{
pub(crate) texture: gfx::handle::RawShaderResourceView<B::Resources>,
pub(crate) texture_handle: gfx::handle::RawTexture<B::Resources>,
pub(crate) sampler_info: gfx::texture::SamplerInfo,
pub(crate) blend_mode: Option<BlendMode>,
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) debug_id: DebugId,
}
impl<B> ImageGeneric<B>
where
B: BackendSpec,
{
/// A helper function that just takes a factory directly so we can make an image
/// without needing the full context object, so we can create an Image while still
/// creating the GraphicsContext.
pub(crate) fn make_raw(
factory: &mut <B as BackendSpec>::Factory,
sampler_info: &texture::SamplerInfo,
width: u16,
height: u16,
rgba: &[u8],
color_format: gfx::format::Format,
debug_id: DebugId,
) -> GameResult<Self> {
if width == 0 || height == 0 {
let msg = format!(
"Tried to create a texture of size {}x{}, each dimension must
be >0",
width, height
);
return Err(GameError::ResourceLoadError(msg));
}
// Check for overflow, which might happen on 32-bit systems.
// Textures can be max u16*u16, pixels, but then have 4 bytes per pixel.
let uwidth = width as usize;
let uheight = height as usize;
let expected_bytes = uwidth
.checked_mul(uheight)
.and_then(|size| size.checked_mul(4))
.ok_or_else(|| {
let msg = format!(
"Integer overflow in Image::make_raw, image size: {} {}",
uwidth, uheight
);
GameError::ResourceLoadError(msg)
})?;
if expected_bytes != rgba.len() {
let msg = format!(
"Tried to create a texture of size {}x{}, but gave {} bytes of data (expected {})",
width,
height,
rgba.len(),
expected_bytes
);
return Err(GameError::ResourceLoadError(msg));
}
let kind = gfx::texture::Kind::D2(width, height, gfx::texture::AaMode::Single);
use gfx::memory::Bind;
let gfx::format::Format(surface_format, channel_type) = color_format;
let texinfo = gfx::texture::Info {
kind,
levels: 1,
format: surface_format,
bind: Bind::SHADER_RESOURCE
| Bind::RENDER_TARGET
| Bind::TRANSFER_SRC
| Bind::TRANSFER_DST,
usage: gfx::memory::Usage::Dynamic,
};
let raw_tex = factory.create_texture_raw(
texinfo,
Some(channel_type),
Some((&[rgba], gfx::texture::Mipmap::Provided)),
)?;
let resource_desc = gfx::texture::ResourceDesc {
channel: channel_type,
layer: None,
min: 0,
max: raw_tex.get_info().levels - 1,
swizzle: gfx::format::Swizzle::new(),
};
let raw_view = factory.view_texture_as_shader_resource_raw(&raw_tex, resource_desc)?;
Ok(Self {
texture: raw_view,
texture_handle: raw_tex,
sampler_info: *sampler_info,
blend_mode: None,
width,
height,
debug_id,
})
}
}
/// In-GPU-memory image data available to be drawn on the screen,
/// using the OpenGL backend.
///
/// Under the hood this is just an `Arc`'ed texture handle and
/// some metadata, so cloning it is fairly cheap; it doesn't
/// make another copy of the underlying image data.
pub type Image = ImageGeneric<GlBackendSpec>;
/// The supported formats for saving an image.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum ImageFormat {
/// .png image format (defaults to RGBA with 8-bit channels.)
Png,
}
impl Image {
/// Load a new image from the file at the given path. The documentation for the
/// [`filesystem`](../filesystem/index.html) module explains how the path must be specified.
pub fn new<P: AsRef<path::Path>>(context: &mut Context, path: P) -> GameResult<Self> {
let img = {
let mut buf = Vec::new();
let mut reader = context.filesystem.open(path)?;
let _ = reader.read_to_end(&mut buf)?;
image::load_from_memory(&buf)?.to_rgba()
};
let (width, height) = img.dimensions();
Self::from_rgba8(context, width as u16, height as u16, &img)
}
/// Creates a new `Image` from the given buffer of `u8` RGBA values.
///
/// The pixel layout is row-major. That is,
/// the first 4 `u8` values make the top-left pixel in the `Image`, the
/// next 4 make the next pixel in the same row, and so on to the end of
/// the row. The next `width * 4` values make up the second row, and so
/// on.
pub fn from_rgba8(
context: &mut Context,
width: u16,
height: u16,
rgba: &[u8],
) -> GameResult<Self> {
let debug_id = DebugId::get(context);
let color_format = context.gfx_context.color_format();
Self::make_raw(
&mut *context.gfx_context.factory,
&context.gfx_context.default_sampler_info,
width,
height,
rgba,
color_format,
debug_id,
)
}
/// Dumps the `Image`'s data to a `Vec` of `u8` RGBA values.
pub fn to_rgba8(&self, ctx: &mut Context) -> GameResult<Vec<u8>> {
use gfx::memory::Typed;
use gfx::traits::FactoryExt;
let gfx = &mut ctx.gfx_context;
let w = self.width;
let h = self.height;
// Note: In the GFX example, the download buffer is created ahead of time
// and updated on screen resize events. This may be preferable, but then
// the buffer also needs to be updated when we switch to/from a canvas.
// Unsure of the performance impact of creating this as it is needed.
// Probably okay for now though, since this probably won't be a super
// common operation.
let dl_buffer = gfx
.factory
.create_download_buffer::<[u8; 4]>(w as usize * h as usize)?;
let factory = &mut *gfx.factory;
let mut local_encoder = GlBackendSpec::encoder(factory);
local_encoder.copy_texture_to_buffer_raw(
&self.texture_handle,
None,
gfx::texture::RawImageInfo {
xoffset: 0,
yoffset: 0,
zoffset: 0,
width: w as u16,
height: h as u16,
depth: 0,
format: gfx.color_format(),
mipmap: 0,
},
dl_buffer.raw(),
0,
)?;
local_encoder.flush(&mut *gfx.device);
let reader = gfx.factory.read_mapping(&dl_buffer)?;
// intermediary buffer to avoid casting.
let mut data = Vec::with_capacity(self.width as usize * self.height as usize * 4);
// Assuming OpenGL backend whose typical readback option (glReadPixels) has origin at bottom left.
// Image formats on the other hand usually deal with top right.
for y in (0..self.height as usize).rev() {
data.extend(
reader
.iter()
.skip(y * self.width as usize)
.take(self.width as usize)
.flatten(),
);
}
Ok(data)
}
/// Encode the `Image` to the given file format and
/// write it out to the given path.
///
/// See the [`filesystem`](../filesystem/index.html) module docs for where exactly
/// the file will end up.
pub fn encode<P: AsRef<path::Path>>(
&self,
ctx: &mut Context,
format: ImageFormat,
path: P,
) -> GameResult {
use std::io;
let data = self.to_rgba8(ctx)?;
let f = filesystem::create(ctx, path)?;
let writer = &mut io::BufWriter::new(f);
let color_format = image::ColorType::RGBA(8);
match format {
ImageFormat::Png => image::png::PNGEncoder::new(writer)
.encode(
&data,
u32::from(self.width),
u32::from(self.height),
color_format,
)
.map_err(Into::into),
}
}
/// A little helper function that creates a new `Image` that is just
/// a solid square of the given size and color. Mainly useful for
/// debugging.
pub fn solid(context: &mut Context, size: u16, color: Color) -> GameResult<Self> {
let (r, g, b, a) = color.into();
let pixel_array: [u8; 4] = [r, g, b, a];
let size_squared = size as usize * size as usize;
let mut buffer = Vec::with_capacity(size_squared);
for _i in 0..size_squared {
buffer.extend(&pixel_array[..]);
}
Image::from_rgba8(context, size, size, &buffer)
}
/// Return the width of the image.
pub fn width(&self) -> u16 {
self.width
}
/// Return the height of the image.
pub fn height(&self) -> u16 {
self.height
}
/// Get the filter mode for the image.
pub fn filter(&self) -> FilterMode {
self.sampler_info.filter.into()
}
/// Set the filter mode for the image.
pub fn set_filter(&mut self, mode: FilterMode) {
self.sampler_info.filter = mode.into();
}
/// Returns the dimensions of the image.
pub fn dimensions(&self) -> Rect {
Rect::new(0.0, 0.0, f32::from(self.width()), f32::from(self.height()))
}
/// Gets the `Image`'s `WrapMode` along the X and Y axes.
pub fn wrap(&self) -> (WrapMode, WrapMode) {
(self.sampler_info.wrap_mode.0, self.sampler_info.wrap_mode.1)
}
/// Sets the `Image`'s `WrapMode` along the X and Y axes.
pub fn set_wrap(&mut self, wrap_x: WrapMode, wrap_y: WrapMode) {
self.sampler_info.wrap_mode.0 = wrap_x;
self.sampler_info.wrap_mode.1 = wrap_y;
}
}
impl fmt::Debug for Image {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"<Image: {}x{}, {:p}, texture address {:p}, sampler: {:?}>",
self.width(),
self.height(),
self,
&self.texture,
&self.sampler_info
)
}
}
impl Drawable for Image {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
self.debug_id.assert(ctx);
let gfx = &mut ctx.gfx_context;
let src_width = param.src.w;
let src_height = param.src.h;
// We have to mess with the scale to make everything
// be its-unit-size-in-pixels.
let real_scale = nalgebra::Vector2::new(
param.scale.x * src_width * f32::from(self.width),
param.scale.y * src_height * f32::from(self.height),
);
let mut new_param = param;
new_param.scale = real_scale.into();
gfx.update_instance_properties(new_param.into())?;
let sampler = gfx
.samplers
.get_or_insert(self.sampler_info, gfx.factory.as_mut());
gfx.data.vbuf = gfx.quad_vertex_buffer.clone();
let typed_thingy = gfx
.backend_spec
.raw_to_typed_shader_resource(self.texture.clone());
gfx.data.tex = (typed_thingy, sampler);
let previous_mode: Option<BlendMode> = if let Some(mode) = self.blend_mode {
let current_mode = gfx.blend_mode();
if current_mode != mode {
gfx.set_blend_mode(mode)?;
Some(current_mode)
} else {
None
}
} else {
None
};
gfx.draw(None)?;
if let Some(mode) = previous_mode {
gfx.set_blend_mode(mode)?;
}
Ok(())
}
fn dimensions(&self, _: &mut Context) -> Option<graphics::Rect> {
Some(self.dimensions())
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.blend_mode
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::ContextBuilder;
#[test]
fn test_invalid_image_size() {
let (ctx, _) = &mut ContextBuilder::new("unittest", "unittest").build().unwrap();
let _i = assert!(Image::from_rgba8(ctx, 0, 0, &vec![]).is_err());
let _i = assert!(Image::from_rgba8(ctx, 3432, 432, &vec![]).is_err());
let _i = Image::from_rgba8(ctx, 2, 2, &vec![99; 16]).unwrap();
}
}

682
src/ggez/graphics/mesh.rs Normal file
View File

@ -0,0 +1,682 @@
use crate::ggez::context::DebugId;
use crate::ggez::error::GameError;
use crate::ggez::graphics::*;
use gfx::traits::FactoryExt;
use lyon;
use lyon::tessellation as t;
pub use self::t::{FillOptions, FillRule, LineCap, LineJoin, StrokeOptions};
/// A builder for creating [`Mesh`](struct.Mesh.html)es.
///
/// This allows you to easily make one `Mesh` containing
/// many different complex pieces of geometry. They don't
/// have to be connected to each other, and will all be
/// drawn at once.
///
/// The following example shows how to build a mesh containing a line and a circle:
///
/// ```rust,no_run
/// # use ggez::*;
/// # use ggez::graphics::*;
/// # use ggez::nalgebra::Point2;
/// # fn main() -> GameResult {
/// # let ctx = &mut ContextBuilder::new("foo", "bar").build().unwrap().0;
/// let mesh: Mesh = MeshBuilder::new()
/// .line(&[Point2::new(20.0, 20.0), Point2::new(40.0, 20.0)], 4.0, (255, 0, 0).into())?
/// .circle(DrawMode::fill(), Point2::new(60.0, 38.0), 40.0, 1.0, (0, 255, 0).into())
/// .build(ctx)?;
/// # Ok(()) }
/// ```
/// A more sophisticated example:
///
/// ```rust,no_run
/// use ggez::{Context, GameResult, nalgebra as na};
/// use ggez::graphics::{self, DrawMode, MeshBuilder};
///
/// fn draw_danger_signs(ctx: &mut Context) -> GameResult {
/// // Initialize a builder instance.
/// let mesh = MeshBuilder::new()
/// // Add vertices for 3 lines (in an approximate equilateral triangle).
/// .line(
/// &[
/// na::Point2::new(0.0, 0.0),
/// na::Point2::new(-30.0, 52.0),
/// na::Point2::new(30.0, 52.0),
/// na::Point2::new(0.0, 0.0),
/// ],
/// 1.0,
/// graphics::WHITE,
/// )?
/// // Add vertices for an exclamation mark!
/// .ellipse(DrawMode::fill(), na::Point2::new(0.0, 25.0), 2.0, 15.0, 2.0, graphics::WHITE,)
/// .circle(DrawMode::fill(), na::Point2::new(0.0, 45.0), 2.0, 2.0, graphics::WHITE,)
/// // Finalize then unwrap. Unwrapping via `?` operator either yields the final `Mesh`,
/// // or propagates the error (note return type).
/// .build(ctx)?;
/// // Draw 3 meshes in a line, 1st and 3rd tilted by 1 radian.
/// graphics::draw(ctx, &mesh, (na::Point2::new(50.0, 50.0), -1.0, graphics::WHITE))?;
/// graphics::draw(ctx, &mesh, (na::Point2::new(150.0, 50.0), 0.0, graphics::WHITE))?;
/// graphics::draw(ctx, &mesh, (na::Point2::new(250.0, 50.0), 1.0, graphics::WHITE))?;
/// Ok(())
/// }
/// ```
#[derive(Debug, Clone)]
pub struct MeshBuilder {
buffer: t::geometry_builder::VertexBuffers<Vertex, u32>,
image: Option<Image>,
}
impl Default for MeshBuilder {
fn default() -> Self {
Self {
buffer: t::VertexBuffers::new(),
image: None,
}
}
}
impl MeshBuilder {
/// Create a new `MeshBuilder`.
pub fn new() -> Self {
Self::default()
}
/// Create a new mesh for a line of one or more connected segments.
pub fn line<P>(&mut self, points: &[P], width: f32, color: Color) -> GameResult<&mut Self>
where
P: Into<mint::Point2<f32>> + Clone,
{
self.polyline(DrawMode::stroke(width), points, color)
}
/// Create a new mesh for a circle.
///
/// For the meaning of the `tolerance` parameter, [see here](https://docs.rs/lyon_geom/0.11.0/lyon_geom/#flattening).
pub fn circle<P>(
&mut self,
mode: DrawMode,
point: P,
radius: f32,
tolerance: f32,
color: Color,
) -> &mut Self
where
P: Into<mint::Point2<f32>>,
{
{
let point = point.into();
let buffers = &mut self.buffer;
let vb = VertexBuilder {
color: LinearColor::from(color),
};
match mode {
DrawMode::Fill(fill_options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::fill_circle(
t::math::point(point.x, point.y),
radius,
&fill_options.with_tolerance(tolerance),
builder,
);
}
DrawMode::Stroke(options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::stroke_circle(
t::math::point(point.x, point.y),
radius,
&options.with_tolerance(tolerance),
builder,
);
}
};
}
self
}
/// Create a new mesh for an ellipse.
///
/// For the meaning of the `tolerance` parameter, [see here](https://docs.rs/lyon_geom/0.11.0/lyon_geom/#flattening).
pub fn ellipse<P>(
&mut self,
mode: DrawMode,
point: P,
radius1: f32,
radius2: f32,
tolerance: f32,
color: Color,
) -> &mut Self
where
P: Into<mint::Point2<f32>>,
{
{
let buffers = &mut self.buffer;
let point = point.into();
let vb = VertexBuilder {
color: LinearColor::from(color),
};
match mode {
DrawMode::Fill(fill_options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::fill_ellipse(
t::math::point(point.x, point.y),
t::math::vector(radius1, radius2),
t::math::Angle { radians: 0.0 },
&fill_options.with_tolerance(tolerance),
builder,
);
}
DrawMode::Stroke(options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::stroke_ellipse(
t::math::point(point.x, point.y),
t::math::vector(radius1, radius2),
t::math::Angle { radians: 0.0 },
&options.with_tolerance(tolerance),
builder,
);
}
};
}
self
}
/// Create a new mesh for a series of connected lines.
pub fn polyline<P>(
&mut self,
mode: DrawMode,
points: &[P],
color: Color,
) -> GameResult<&mut Self>
where
P: Into<mint::Point2<f32>> + Clone,
{
if points.len() < 2 {
return Err(GameError::LyonError(
"MeshBuilder::polyline() got a list of < 2 points".to_string(),
));
}
self.polyline_inner(mode, points, false, color)
}
/// Create a new mesh for a closed polygon.
/// The points given must be in clockwise order,
/// otherwise at best the polygon will not draw.
pub fn polygon<P>(
&mut self,
mode: DrawMode,
points: &[P],
color: Color,
) -> GameResult<&mut Self>
where
P: Into<mint::Point2<f32>> + Clone,
{
if points.len() < 3 {
return Err(GameError::LyonError(
"MeshBuilder::polygon() got a list of < 3 points".to_string(),
));
}
self.polyline_inner(mode, points, true, color)
}
fn polyline_inner<P>(
&mut self,
mode: DrawMode,
points: &[P],
is_closed: bool,
color: Color,
) -> GameResult<&mut Self>
where
P: Into<mint::Point2<f32>> + Clone,
{
{
assert!(points.len() > 1);
let buffers = &mut self.buffer;
let points = points.iter().cloned().map(|p| {
let mint_point: mint::Point2<f32> = p.into();
t::math::point(mint_point.x, mint_point.y)
});
let vb = VertexBuilder {
color: LinearColor::from(color),
};
match mode {
DrawMode::Fill(options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let tessellator = &mut t::FillTessellator::new();
let _ = t::basic_shapes::fill_polyline(points, tessellator, &options, builder)?;
}
DrawMode::Stroke(options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::stroke_polyline(points, is_closed, &options, builder);
}
};
}
Ok(self)
}
/// Create a new mesh for a rectangle.
pub fn rectangle(&mut self, mode: DrawMode, bounds: Rect, color: Color) -> &mut Self {
{
let buffers = &mut self.buffer;
let rect = t::math::rect(bounds.x, bounds.y, bounds.w, bounds.h);
let vb = VertexBuilder {
color: LinearColor::from(color),
};
match mode {
DrawMode::Fill(fill_options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::fill_rectangle(&rect, &fill_options, builder);
}
DrawMode::Stroke(options) => {
let builder = &mut t::BuffersBuilder::new(buffers, vb);
let _ = t::basic_shapes::stroke_rectangle(&rect, &options, builder);
}
};
}
self
}
/// Create a new [`Mesh`](struct.Mesh.html) from a raw list of triangles.
/// The length of the list must be a multiple of 3.
///
/// Currently does not support UV's or indices.
pub fn triangles<P>(&mut self, triangles: &[P], color: Color) -> GameResult<&mut Self>
where
P: Into<mint::Point2<f32>> + Clone,
{
{
if (triangles.len() % 3) != 0 {
let msg = format!(
"Called Mesh::triangles() with points that have a length not a multiple of 3."
);
return Err(GameError::LyonError(msg));
}
let tris = triangles
.iter()
.cloned()
.map(|p| {
// Gotta turn ggez Point2's into lyon FillVertex's
let mint_point = p.into();
let np = lyon::math::point(mint_point.x, mint_point.y);
let nv = lyon::math::vector(mint_point.x, mint_point.y);
t::FillVertex {
position: np,
normal: nv,
}
})
// Removing this collect might be nice, but is not easy.
// We can chunk a slice, but can't chunk an arbitrary
// iterator.
// Using the itertools crate doesn't really make anything
// nicer, so we'll just live with it.
.collect::<Vec<_>>();
let tris = tris.chunks(3);
let vb = VertexBuilder {
color: LinearColor::from(color),
};
let builder: &mut t::BuffersBuilder<_, _, _, _> =
&mut t::BuffersBuilder::new(&mut self.buffer, vb);
use lyon::tessellation::GeometryBuilder;
builder.begin_geometry();
for tri in tris {
// Ideally this assert makes bounds-checks only happen once.
assert!(tri.len() == 3);
let fst = tri[0];
let snd = tri[1];
let thd = tri[2];
let i1 = builder.add_vertex(fst)?;
let i2 = builder.add_vertex(snd)?;
let i3 = builder.add_vertex(thd)?;
builder.add_triangle(i1, i2, i3);
}
let _ = builder.end_geometry();
}
Ok(self)
}
/// Takes an `Image` to apply to the mesh.
pub fn texture(&mut self, texture: Image) -> &mut Self {
self.image = Some(texture);
self
}
/// Creates a `Mesh` from a raw list of triangles defined from vertices
/// and indices. You may also
/// supply an `Image` to use as a texture, if you pass `None`, it will
/// just use a pure white texture.
///
/// This is the most primitive mesh-creation method, but allows you full
/// control over the tesselation and texturing. It has the same constraints
/// as `Mesh::from_raw()`.
pub fn raw<V>(&mut self, verts: &[V], indices: &[u32], texture: Option<Image>) -> &mut Self
where
V: Into<Vertex> + Clone,
{
assert!(self.buffer.vertices.len() + verts.len() < (std::u32::MAX as usize));
assert!(self.buffer.indices.len() + indices.len() < (std::u32::MAX as usize));
let next_idx = self.buffer.vertices.len() as u32;
// Can we remove the clone here?
// I can't find a way to, because `into()` consumes its source and
// `Borrow` or `AsRef` aren't really right.
let vertices = verts.iter().cloned().map(|v: V| -> Vertex { v.into() });
let indices = indices.iter().map(|i| (*i) + next_idx);
self.buffer.vertices.extend(vertices);
self.buffer.indices.extend(indices);
self.image = texture;
self
}
/// Takes the accumulated geometry and load it into GPU memory,
/// creating a single `Mesh`.
pub fn build(&self, ctx: &mut Context) -> GameResult<Mesh> {
Mesh::from_raw(
ctx,
&self.buffer.vertices,
&self.buffer.indices,
self.image.clone(),
)
}
}
#[derive(Copy, Clone, PartialEq, Debug)]
struct VertexBuilder {
color: LinearColor,
}
impl t::VertexConstructor<t::FillVertex, Vertex> for VertexBuilder {
fn new_vertex(&mut self, vertex: t::FillVertex) -> Vertex {
Vertex {
pos: [vertex.position.x, vertex.position.y],
uv: [vertex.position.x, vertex.position.y],
color: self.color.into(),
}
}
}
impl t::VertexConstructor<t::StrokeVertex, Vertex> for VertexBuilder {
fn new_vertex(&mut self, vertex: t::StrokeVertex) -> Vertex {
Vertex {
pos: [vertex.position.x, vertex.position.y],
uv: [0.0, 0.0],
color: self.color.into(),
}
}
}
/// 2D polygon mesh.
///
/// All of its creation methods are just shortcuts for doing the same operation
/// via a [`MeshBuilder`](struct.MeshBuilder.html).
#[derive(Debug, Clone, PartialEq)]
pub struct Mesh {
buffer: gfx::handle::Buffer<gfx_device_gl::Resources, Vertex>,
slice: gfx::Slice<gfx_device_gl::Resources>,
blend_mode: Option<BlendMode>,
image: Image,
debug_id: DebugId,
rect: Rect,
}
impl Mesh {
/// Create a new mesh for a line of one or more connected segments.
pub fn new_line<P>(
ctx: &mut Context,
points: &[P],
width: f32,
color: Color,
) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>> + Clone,
{
let mut mb = MeshBuilder::new();
let _ = mb.polyline(DrawMode::stroke(width), points, color);
mb.build(ctx)
}
/// Create a new mesh for a circle.
pub fn new_circle<P>(
ctx: &mut Context,
mode: DrawMode,
point: P,
radius: f32,
tolerance: f32,
color: Color,
) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>>,
{
let mut mb = MeshBuilder::new();
let _ = mb.circle(mode, point, radius, tolerance, color);
mb.build(ctx)
}
/// Create a new mesh for an ellipse.
pub fn new_ellipse<P>(
ctx: &mut Context,
mode: DrawMode,
point: P,
radius1: f32,
radius2: f32,
tolerance: f32,
color: Color,
) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>>,
{
let mut mb = MeshBuilder::new();
let _ = mb.ellipse(mode, point, radius1, radius2, tolerance, color);
mb.build(ctx)
}
/// Create a new mesh for series of connected lines.
pub fn new_polyline<P>(
ctx: &mut Context,
mode: DrawMode,
points: &[P],
color: Color,
) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>> + Clone,
{
let mut mb = MeshBuilder::new();
let _ = mb.polyline(mode, points, color);
mb.build(ctx)
}
/// Create a new mesh for closed polygon.
/// The points given must be in clockwise order,
/// otherwise at best the polygon will not draw.
pub fn new_polygon<P>(
ctx: &mut Context,
mode: DrawMode,
points: &[P],
color: Color,
) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>> + Clone,
{
if points.len() < 3 {
return Err(GameError::LyonError(
"Mesh::new_polygon() got a list of < 3 points".to_string(),
));
}
let mut mb = MeshBuilder::new();
let _ = mb.polygon(mode, points, color);
mb.build(ctx)
}
/// Create a new mesh for a rectangle
pub fn new_rectangle(
ctx: &mut Context,
mode: DrawMode,
bounds: Rect,
color: Color,
) -> GameResult<Mesh> {
let mut mb = MeshBuilder::new();
let _ = mb.rectangle(mode, bounds, color);
mb.build(ctx)
}
/// Create a new `Mesh` from a raw list of triangle points.
pub fn from_triangles<P>(ctx: &mut Context, triangles: &[P], color: Color) -> GameResult<Mesh>
where
P: Into<mint::Point2<f32>> + Clone,
{
let mut mb = MeshBuilder::new();
let _ = mb.triangles(triangles, color);
mb.build(ctx)
}
/// Creates a `Mesh` from a raw list of triangles defined from points
/// and indices, with the given UV texture coordinates. You may also
/// supply an `Image` to use as a texture, if you pass `None`, it will
/// just use a pure white texture. The indices should draw the points in
/// clockwise order, otherwise at best the mesh will not draw.
///
/// This is the most primitive mesh-creation method, but allows
/// you full control over the tesselation and texturing. As such
/// it will return an error, panic, or produce incorrect/invalid
/// output (that may later cause drawing to panic), if:
///
/// * `indices` contains a value out of bounds of `verts`
/// * `verts` is longer than `u32::MAX` elements.
/// * `indices` do not specify triangles in clockwise order.
pub fn from_raw<V>(
ctx: &mut Context,
verts: &[V],
indices: &[u32],
texture: Option<Image>,
) -> GameResult<Mesh>
where
V: Into<Vertex> + Clone,
{
// Sanity checks to return early with helpful error messages.
if verts.len() > (std::u32::MAX as usize) {
let msg = format!(
"Tried to build a mesh with {} vertices, max is u32::MAX",
verts.len()
);
return Err(GameError::LyonError(msg));
}
if indices.len() > (std::u32::MAX as usize) {
let msg = format!(
"Tried to build a mesh with {} indices, max is u32::MAX",
indices.len()
);
return Err(GameError::LyonError(msg));
}
if verts.len() < 3 {
let msg = format!("Trying to build mesh with < 3 vertices, this is usually due to invalid input to a `Mesh` or MeshBuilder`.");
return Err(GameError::LyonError(msg));
}
if indices.len() < 3 {
let msg = format!("Trying to build mesh with < 3 indices, this is usually due to invalid input to a `Mesh` or MeshBuilder`. Indices:\n {:#?}", indices);
return Err(GameError::LyonError(msg));
}
if indices.len() % 3 != 0 {
let msg = format!("Trying to build mesh with an array of indices that is not a multiple of 3, this is usually due to invalid input to a `Mesh` or MeshBuilder`.");
return Err(GameError::LyonError(msg));
}
let verts: Vec<Vertex> = verts.iter().cloned().map(Into::into).collect();
let rect = bbox_for_vertices(&verts).expect("No vertices in MeshBuilder");
let (vbuf, slice) = ctx
.gfx_context
.factory
.create_vertex_buffer_with_slice(&verts[..], indices);
Ok(Mesh {
buffer: vbuf,
slice,
blend_mode: None,
image: texture.unwrap_or_else(|| ctx.gfx_context.white_image.clone()),
debug_id: DebugId::get(ctx),
rect,
})
}
/// Replaces the vertices in the `Mesh` with the given ones. This MAY be faster
/// than re-creating a `Mesh` with [`Mesh::from_raw()`](#method.from_raw) due to
/// reusing memory instead of allocating and deallocating it, both on the CPU and
/// GPU side. There's too much variation in implementations and drivers to promise
/// it will actually be faster though. At worst, it will be the same speed.
pub fn set_vertices(&mut self, ctx: &mut Context, verts: &[Vertex], indices: &[u32]) {
// This is in principle faster than throwing away an existing mesh and
// creating a new one with `Mesh::from_raw()`, but really only because it
// doesn't take `Into<Vertex>` and so doesn't need to create an intermediate
// `Vec`. It still creates a new GPU buffer and replaces the old one instead
// of just copying into the old one.
//
// By default we create `Mesh` with a read-only GPU buffer, which I am
// a little hesitant to change... partially because doing that with
// `Image` has caused some subtle edge case bugs.
//
// It's not terribly hard to do in principle though, just tedious;
// start at `Factory::create_vertex_buffer_with_slice()`, drill down to
// <https://docs.rs/gfx/0.17.1/gfx/traits/trait.Factory.html#tymethod.create_buffer_raw>,
// and fill in the bits between with the appropriate values.
let (vbuf, slice) = ctx
.gfx_context
.factory
.create_vertex_buffer_with_slice(verts, indices);
self.buffer = vbuf;
self.slice = slice;
}
}
impl Drawable for Mesh {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
self.debug_id.assert(ctx);
let gfx = &mut ctx.gfx_context;
gfx.update_instance_properties(param.into())?;
gfx.data.vbuf = self.buffer.clone();
let texture = self.image.texture.clone();
let sampler = gfx
.samplers
.get_or_insert(self.image.sampler_info, gfx.factory.as_mut());
let typed_thingy = gfx.backend_spec.raw_to_typed_shader_resource(texture);
gfx.data.tex = (typed_thingy, sampler);
gfx.draw(Some(&self.slice))?;
Ok(())
}
fn dimensions(&self, _ctx: &mut Context) -> Option<Rect> {
Some(self.rect)
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.blend_mode
}
}
fn bbox_for_vertices(verts: &[Vertex]) -> Option<Rect> {
if verts.is_empty() {
return None;
}
let [x0, y0] = verts[0].pos;
let mut x_max = x0;
let mut x_min = x0;
let mut y_max = y0;
let mut y_min = y0;
for v in verts {
let x = v.pos[0];
let y = v.pos[1];
x_max = f32::max(x_max, x);
x_min = f32::min(x_min, x);
y_max = f32::max(y_max, y);
y_min = f32::min(y_min, y);
}
Some(Rect {
w: x_max - x_min,
h: y_max - y_min,
x: x_min,
y: y_min,
})
}

1064
src/ggez/graphics/mod.rs Normal file

File diff suppressed because it is too large Load Diff

581
src/ggez/graphics/shader.rs Normal file
View File

@ -0,0 +1,581 @@
//! The `shader` module allows user-defined shaders to be used
//! with ggez for cool and spooky effects. See the
//! [`shader`](https://github.com/ggez/ggez/blob/devel/examples/shader.rs)
//! and [`shadows`](https://github.com/ggez/ggez/blob/devel/examples/shadows.rs)
//! examples for a taste.
#![allow(unsafe_code)]
use gfx::format;
use gfx::handle::*;
use gfx::preset::blend;
use gfx::pso::buffer::*;
use gfx::pso::*;
use gfx::shade::*;
use gfx::state::*;
use gfx::traits::{FactoryExt, Pod};
use gfx::*;
use std::cell::RefCell;
use std::collections::HashMap;
use std::fmt;
use std::io::prelude::*;
use std::marker::PhantomData;
use std::path::Path;
use std::rc::Rc;
use crate::ggez::context::DebugId;
use crate::ggez::error::*;
use crate::ggez::graphics;
use crate::ggez::Context;
/// A type for empty shader data for shaders that do not require any additional
/// data to be sent to the GPU
#[derive(Clone, Copy, Debug)]
pub struct EmptyConst;
impl<F> Structure<F> for EmptyConst {
fn query(_name: &str) -> Option<Element<F>> {
None
}
}
unsafe impl Pod for EmptyConst {}
/// An enum for specifying default and custom blend modes
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum BlendMode {
/// When combining two fragments, add their values together, saturating
/// at 1.0
Add,
/// When combining two fragments, subtract the source value from the
/// destination value
Subtract,
/// When combining two fragments, add the value of the source times its
/// alpha channel with the value of the destination multiplied by the inverse
/// of the source alpha channel. Has the usual transparency effect: mixes the
/// two colors using a fraction of each one specified by the alpha of the source.
Alpha,
/// When combining two fragments, subtract the destination color from a constant
/// color using the source color as weight. Has an invert effect with the constant
/// color as base and source color controlling displacement from the base color.
/// A white source color and a white value results in plain invert. The output
/// alpha is same as destination alpha.
Invert,
/// When combining two fragments, multiply their values together.
Multiply,
/// When combining two fragments, choose the source value
Replace,
/// When combining two fragments, choose the lighter value
Lighten,
/// When combining two fragments, choose the darker value
Darken,
}
impl From<BlendMode> for Blend {
fn from(bm: BlendMode) -> Self {
match bm {
BlendMode::Add => blend::ADD,
BlendMode::Subtract => Blend {
color: BlendChannel {
equation: Equation::Sub,
source: Factor::One,
destination: Factor::One,
},
alpha: BlendChannel {
equation: Equation::Sub,
source: Factor::One,
destination: Factor::One,
},
},
BlendMode::Alpha => blend::ALPHA,
BlendMode::Invert => blend::INVERT,
BlendMode::Multiply => blend::MULTIPLY,
BlendMode::Replace => blend::REPLACE,
BlendMode::Lighten => Blend {
color: BlendChannel {
equation: Equation::Max,
source: Factor::One,
destination: Factor::One,
},
alpha: BlendChannel {
equation: Equation::Add,
source: Factor::ZeroPlus(BlendValue::SourceAlpha),
destination: Factor::OneMinus(BlendValue::SourceAlpha),
},
},
BlendMode::Darken => Blend {
color: BlendChannel {
equation: Equation::Min,
source: Factor::One,
destination: Factor::One,
},
alpha: BlendChannel {
equation: Equation::Add,
source: Factor::ZeroPlus(BlendValue::SourceAlpha),
destination: Factor::OneMinus(BlendValue::SourceAlpha),
},
},
}
}
}
/// A struct to easily store a set of pipeline state objects that are
/// associated with a specific shader program.
///
/// In gfx, because Vulkan and DX are more strict
/// about how blend modes work than GL is, blend modes are
/// baked in as a piece of state for a PSO and you can't change it
/// dynamically. After chatting with @kvark on IRC and looking
/// how he does it in three-rs, the best way to change blend
/// modes is to just make multiple PSOs with respective blend modes baked in.
/// The `PsoSet` struct is basically just a hash map for easily
/// storing each shader set's PSOs and then retrieving them based
/// on a [`BlendMode`](enum.BlendMode.html).
struct PsoSet<Spec, C>
where
Spec: graphics::BackendSpec,
C: Structure<ConstFormat>,
{
psos: HashMap<BlendMode, PipelineState<Spec::Resources, ConstMeta<C>>>,
}
impl<Spec, C> PsoSet<Spec, C>
where
Spec: graphics::BackendSpec,
C: Structure<ConstFormat>,
{
pub fn new(cap: usize) -> Self {
Self {
psos: HashMap::with_capacity(cap),
}
}
pub fn insert_mode(
&mut self,
mode: BlendMode,
pso: PipelineState<Spec::Resources, ConstMeta<C>>,
) {
let _ = self.psos.insert(mode, pso);
}
pub fn mode(
&self,
mode: BlendMode,
) -> GameResult<&PipelineState<Spec::Resources, ConstMeta<C>>> {
match self.psos.get(&mode) {
Some(pso) => Ok(pso),
None => Err(GameError::RenderError(
"Could not find a pipeline for the specified shader and BlendMode".into(),
)),
}
}
}
/// An ID used by the ggez graphics context to uniquely identify a shader
pub type ShaderId = usize;
/// A `ShaderGeneric` reprensents a handle user-defined shader that can be used
/// with a ggez graphics context that is generic over `gfx::Resources`
///
/// As an end-user you shouldn't ever have to touch this and should use
/// [`Shader`](type.Shader.html) instead.
#[derive(Clone)]
pub struct ShaderGeneric<Spec: graphics::BackendSpec, C: Structure<ConstFormat>> {
id: ShaderId,
buffer: Buffer<Spec::Resources, C>,
debug_id: DebugId,
}
/// A `Shader` represents a handle to a user-defined shader that can be used
/// with a ggez graphics context
pub type Shader<C> = ShaderGeneric<graphics::GlBackendSpec, C>;
pub(crate) fn create_shader<C, S, Spec>(
vertex_source: &[u8],
pixel_source: &[u8],
consts: C,
name: S,
encoder: &mut Encoder<Spec::Resources, Spec::CommandBuffer>,
factory: &mut Spec::Factory,
multisample_samples: u8,
blend_modes: Option<&[BlendMode]>,
color_format: format::Format,
debug_id: DebugId,
) -> GameResult<(ShaderGeneric<Spec, C>, Box<dyn ShaderHandle<Spec>>)>
where
C: 'static + Pod + Structure<ConstFormat> + Clone + Copy,
S: Into<String>,
Spec: graphics::BackendSpec + 'static,
{
let buffer = factory.create_constant_buffer(1);
encoder.update_buffer(&buffer, &[consts], 0)?;
let default_mode = vec![BlendMode::Alpha];
let blend_modes = blend_modes.unwrap_or(&default_mode[..]);
let mut psos = PsoSet::new(blend_modes.len());
let name: String = name.into();
for mode in blend_modes {
let init = ConstInit::<C>(
graphics::pipe::Init {
out: (
"Target0",
color_format,
ColorMask::all(),
Some((*mode).into()),
),
..graphics::pipe::new()
},
name.clone(),
PhantomData,
);
let set = factory.create_shader_set(vertex_source, pixel_source)?;
let sample = if multisample_samples > 1 {
Some(MultiSample)
} else {
None
};
let rasterizer = Rasterizer {
front_face: FrontFace::CounterClockwise,
cull_face: CullFace::Nothing,
method: RasterMethod::Fill,
offset: None,
samples: sample,
};
let pso = factory.create_pipeline_state(&set, Primitive::TriangleList, rasterizer, init)?;
psos.insert_mode(*mode, pso);
}
let program = ShaderProgram {
buffer: buffer.clone(),
psos,
active_blend_mode: blend_modes[0],
};
let draw: Box<dyn ShaderHandle<Spec>> = Box::new(program);
let id = 0;
let shader = ShaderGeneric {
id,
buffer,
debug_id,
};
Ok((shader, draw))
}
impl<Spec, C> ShaderGeneric<Spec, C>
where
Spec: graphics::BackendSpec,
C: 'static + Pod + Structure<ConstFormat> + Clone + Copy,
{
/// Create a new `Shader` given source files, constants and a name.
///
/// In order to use a specific blend mode when this shader is being
/// used, you must include that blend mode as part of the
/// `blend_modes` parameter at creation. If `None` is given, only the
/// default [`Alpha`](enum.BlendMode.html#variant.Alpha) blend mode is used.
pub fn new<P: AsRef<Path>, S: Into<String>>(
ctx: &mut Context,
vertex_path: P,
pixel_path: P,
consts: C,
name: S,
blend_modes: Option<&[BlendMode]>,
) -> GameResult<Shader<C>> {
let vertex_source = {
let mut buf = Vec::new();
let mut reader = ctx.filesystem.open(vertex_path)?;
let _ = reader.read_to_end(&mut buf)?;
buf
};
let pixel_source = {
let mut buf = Vec::new();
let mut reader = ctx.filesystem.open(pixel_path)?;
let _ = reader.read_to_end(&mut buf)?;
buf
};
Shader::from_u8(
ctx,
&vertex_source,
&pixel_source,
consts,
name,
blend_modes,
)
}
/// Create a new `Shader` directly from GLSL source code.
///
/// In order to use a specific blend mode when this shader is being
/// used, you must include that blend mode as part of the
/// `blend_modes` parameter at creation. If `None` is given, only the
/// default [`Alpha`](enum.BlendMode.html#variant.Alpha) blend mode is used.
pub fn from_u8<S: Into<String>>(
ctx: &mut Context,
vertex_source: &[u8],
pixel_source: &[u8],
consts: C,
name: S,
blend_modes: Option<&[BlendMode]>,
) -> GameResult<Shader<C>> {
let debug_id = DebugId::get(ctx);
let color_format = ctx.gfx_context.color_format();
let (mut shader, draw) = create_shader(
vertex_source,
pixel_source,
consts,
name,
&mut ctx.gfx_context.encoder,
&mut *ctx.gfx_context.factory,
ctx.gfx_context.multisample_samples,
blend_modes,
color_format,
debug_id,
)?;
shader.id = ctx.gfx_context.shaders.len();
ctx.gfx_context.shaders.push(draw);
Ok(shader)
}
}
impl<C> Shader<C>
where
C: 'static + Pod + Structure<ConstFormat> + Clone + Copy,
{
/// Send data to the GPU for use with the `Shader`
pub fn send(&self, ctx: &mut Context, consts: C) -> GameResult {
ctx.gfx_context
.encoder
.update_buffer(&self.buffer, &[consts], 0)?;
Ok(())
}
/// Gets the shader ID for the `Shader` which is used by the
/// graphics context for identifying shaders in its cache
pub fn shader_id(&self) -> ShaderId {
self.id
}
}
impl<Spec, C> fmt::Debug for ShaderGeneric<Spec, C>
where
Spec: graphics::BackendSpec,
C: Structure<ConstFormat>,
{
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "<Shader[{}]: {:p}>", self.id, self)
}
}
struct ShaderProgram<Spec: graphics::BackendSpec, C: Structure<ConstFormat>> {
buffer: Buffer<Spec::Resources, C>,
psos: PsoSet<Spec, C>,
active_blend_mode: BlendMode,
}
impl<Spec, C> fmt::Debug for ShaderProgram<Spec, C>
where
Spec: graphics::BackendSpec,
C: Structure<ConstFormat>,
{
fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "<ShaderProgram: {:p}>", self)
}
}
/// A trait that is used to create trait objects to abstract away the
/// `gfx::Structure<ConstFormat>` type of the constant data for drawing
pub trait ShaderHandle<Spec: graphics::BackendSpec>: fmt::Debug {
/// Draw with the current Shader
fn draw(
&self,
encoder: &mut Encoder<Spec::Resources, Spec::CommandBuffer>,
slice: &Slice<Spec::Resources>,
data: &graphics::pipe::Data<Spec::Resources>,
) -> GameResult;
/// Sets the shader program's blend mode
fn set_blend_mode(&mut self, mode: BlendMode) -> GameResult;
/// Gets the shader program's current blend mode
fn blend_mode(&self) -> BlendMode;
}
impl<Spec, C> ShaderHandle<Spec> for ShaderProgram<Spec, C>
where
Spec: graphics::BackendSpec,
C: Structure<ConstFormat>,
{
fn draw(
&self,
encoder: &mut Encoder<Spec::Resources, Spec::CommandBuffer>,
slice: &Slice<Spec::Resources>,
data: &graphics::pipe::Data<Spec::Resources>,
) -> GameResult {
let pso = self.psos.mode(self.active_blend_mode)?;
encoder.draw(slice, pso, &ConstData(data, &self.buffer));
Ok(())
}
fn set_blend_mode(&mut self, mode: BlendMode) -> GameResult {
let _ = self.psos.mode(mode)?;
self.active_blend_mode = mode;
Ok(())
}
fn blend_mode(&self) -> BlendMode {
self.active_blend_mode
}
}
/// A lock for RAII shader regions. The shader automatically gets cleared once
/// the lock goes out of scope, restoring the previous shader (if any).
///
/// Essentially, binding a [`Shader`](type.Shader.html) will return one of these,
/// and the shader will remain active as long as this object exists. When this is
/// dropped, the previous shader is restored.
#[derive(Debug, Clone)]
pub struct ShaderLock {
cell: Rc<RefCell<Option<ShaderId>>>,
previous_shader: Option<ShaderId>,
}
impl Drop for ShaderLock {
fn drop(&mut self) {
*self.cell.borrow_mut() = self.previous_shader;
}
}
/// Use a shader until the returned lock goes out of scope
pub fn use_shader<C>(ctx: &mut Context, ps: &Shader<C>) -> ShaderLock
where
C: Structure<ConstFormat>,
{
ps.debug_id.assert(ctx);
let cell = Rc::clone(&ctx.gfx_context.current_shader);
let previous_shader = *cell.borrow();
set_shader(ctx, ps);
ShaderLock {
cell,
previous_shader,
}
}
/// Set the current shader for the `Context` to render with
pub fn set_shader<C>(ctx: &mut Context, ps: &Shader<C>)
where
C: Structure<ConstFormat>,
{
ps.debug_id.assert(ctx);
*ctx.gfx_context.current_shader.borrow_mut() = Some(ps.id);
}
/// Clears the the current shader for the `Context`, restoring the default shader.
///
/// However, calling this and then dropping a [`ShaderLock`](struct.ShaderLock.html)
/// will still set the shader to whatever was set when the `ShaderLock` was created.
pub fn clear_shader(ctx: &mut Context) {
*ctx.gfx_context.current_shader.borrow_mut() = None;
}
#[derive(Debug)]
struct ConstMeta<C: Structure<ConstFormat>>(graphics::pipe::Meta, ConstantBuffer<C>);
#[derive(Debug)]
struct ConstData<'a, R: Resources, C: 'a>(&'a graphics::pipe::Data<R>, &'a Buffer<R, C>);
impl<'a, R, C> PipelineData<R> for ConstData<'a, R, C>
where
R: Resources,
C: Structure<ConstFormat>,
{
type Meta = ConstMeta<C>;
fn bake_to(
&self,
out: &mut RawDataSet<R>,
meta: &Self::Meta,
man: &mut Manager<R>,
access: &mut AccessInfo<R>,
) {
self.0.bake_to(out, &meta.0, man, access);
meta.1.bind_to(out, self.1, man, access);
}
}
#[derive(Debug)]
struct ConstInit<'a, C>(graphics::pipe::Init<'a>, String, PhantomData<C>);
impl<'a, C> PipelineInit for ConstInit<'a, C>
where
C: Structure<ConstFormat>,
{
type Meta = ConstMeta<C>;
fn link_to<'s>(
&self,
desc: &mut Descriptor,
info: &'s ProgramInfo,
) -> Result<Self::Meta, InitError<&'s str>> {
let mut meta1 = ConstantBuffer::<C>::new();
let mut index = None;
for (i, cb) in info.constant_buffers.iter().enumerate() {
match meta1.link_constant_buffer(cb, &self.1.as_str()) {
Some(Ok(d)) => {
assert!(meta1.is_active());
desc.constant_buffers[cb.slot as usize] = Some(d);
index = Some(i);
break;
}
Some(Err(e)) => return Err(InitError::ConstantBuffer(&cb.name, Some(e))),
None => (),
}
}
if let Some(index) = index {
// create a local clone of the program info so that we can remove
// the var we found from the `constant_buffer`
let mut program_info = info.clone();
let _ = program_info.constant_buffers.remove(index);
let meta0 = match self.0.link_to(desc, &program_info) {
Ok(m) => m,
Err(e) => {
// unfortunately... the error lifetime is bound to the
// lifetime of our cloned program info which is bad since it
// will go out of scope at the end of the function, so lets
// convert the error to one that is bound to the lifetime of
// the program info that was passed in!
macro_rules! fixlifetimes {
($e:ident {
$( $ty:path => $a:ident, )*
}) => {{
match $e {
$( $ty(name, _) => {
let var = info.$a.iter().find(|v| v.name == name).unwrap();
// We can do better with the error data...
return Err($ty(&var.name, None));
} )*
}
}}
}
fixlifetimes!(e {
InitError::VertexImport => vertex_attributes,
InitError::ConstantBuffer => constant_buffers,
InitError::GlobalConstant => globals,
InitError::ResourceView => textures,
InitError::UnorderedView => unordereds,
InitError::Sampler => samplers,
InitError::PixelExport => outputs,
})
}
};
Ok(ConstMeta(meta0, meta1))
} else {
Ok(ConstMeta(self.0.link_to(desc, info)?, meta1))
}
}
}

View File

@ -0,0 +1,14 @@
#version 150 core
uniform sampler2D t_Texture;
in vec2 v_Uv;
in vec4 v_Color;
out vec4 Target0;
layout (std140) uniform Globals {
mat4 u_MVP;
};
void main() {
Target0 = texture(t_Texture, v_Uv) * v_Color;
}

View File

@ -0,0 +1,28 @@
#version 150 core
in vec2 a_Pos;
in vec2 a_Uv;
in vec4 a_VertColor;
in vec4 a_Src;
in vec4 a_TCol1;
in vec4 a_TCol2;
in vec4 a_TCol3;
in vec4 a_TCol4;
in vec4 a_Color;
layout (std140) uniform Globals {
mat4 u_MVP;
};
out vec2 v_Uv;
out vec4 v_Color;
void main() {
v_Uv = a_Uv * a_Src.zw + a_Src.xy;
v_Color = a_Color * a_VertColor;
mat4 instance_transform = mat4(a_TCol1, a_TCol2, a_TCol3, a_TCol4);
vec4 position = instance_transform * vec4(a_Pos, 0.0, 1.0);
gl_Position = u_MVP * position;
}

View File

@ -0,0 +1,14 @@
#version 300 es
uniform mediump sampler2D t_Texture;
in mediump vec2 v_Uv;
in mediump vec4 v_Color;
out mediump vec4 Target0;
layout (std140) uniform Globals {
mediump mat4 u_MVP;
};
void main() {
Target0 = texture(t_Texture, v_Uv) * v_Color;
}

View File

@ -0,0 +1,28 @@
#version 300 es
in mediump vec2 a_Pos;
in mediump vec2 a_Uv;
in mediump vec4 a_VertColor;
in mediump vec4 a_Src;
in mediump vec4 a_TCol1;
in mediump vec4 a_TCol2;
in mediump vec4 a_TCol3;
in mediump vec4 a_TCol4;
in mediump vec4 a_Color;
layout (std140) uniform Globals {
mediump mat4 u_MVP;
};
out mediump vec2 v_Uv;
out mediump vec4 v_Color;
void main() {
v_Uv = a_Uv * a_Src.zw + a_Src.xy;
v_Color = a_Color * a_VertColor;
mat4 instance_transform = mat4(a_TCol1, a_TCol2, a_TCol3, a_TCol4);
vec4 position = instance_transform * vec4(a_Pos, 0.0, 1.0);
gl_Position = u_MVP * position;
}

View File

@ -0,0 +1,16 @@
#version 150
uniform sampler2D font_tex;
in vec2 f_tex_pos;
in vec4 f_color;
out vec4 Target0;
void main() {
float alpha = texture(font_tex, f_tex_pos).r;
if (alpha <= 0.0) {
discard;
}
Target0 = f_color * vec4(1.0, 1.0, 1.0, alpha);
}

View File

@ -0,0 +1,43 @@
#version 150
uniform mat4 transform;
in vec3 left_top;
in vec2 right_bottom;
in vec2 tex_left_top;
in vec2 tex_right_bottom;
in vec4 color;
out vec2 f_tex_pos;
out vec4 f_color;
// generate positional data based on vertex ID
void main() {
vec2 pos = vec2(0.0);
float left = left_top.x;
float right = right_bottom.x;
float top = left_top.y;
float bottom = right_bottom.y;
switch (gl_VertexID) {
case 0:
pos = vec2(left, top);
f_tex_pos = tex_left_top;
break;
case 1:
pos = vec2(right, top);
f_tex_pos = vec2(tex_right_bottom.x, tex_left_top.y);
break;
case 2:
pos = vec2(left, bottom);
f_tex_pos = vec2(tex_left_top.x, tex_right_bottom.y);
break;
case 3:
pos = vec2(right, bottom);
f_tex_pos = tex_right_bottom;
break;
}
f_color = color;
gl_Position = transform * vec4(pos, left_top.z, 1.0);
}

View File

@ -0,0 +1,220 @@
//! A [`SpriteBatch`](struct.SpriteBatch.html) is a way to
//! efficiently draw a large number of copies of the same image, or part
//! of the same image. It's useful for implementing tiled maps,
//! spritesheets, particles, and other such things.
//!
//! Essentially this uses a technique called "instancing" to queue up
//! a large amount of location/position data in a buffer, then feed it
//! to the graphics card all in one go.
//!
//! Also it's super slow in `rustc`'s default debug mode, because
//! `rustc` adds a lot of checking to the vector accesses and math.
//! If you use it, it's recommended to crank up the `opt-level` for
//! debug mode in your game's `Cargo.toml`.
use crate::ggez::context::Context;
use crate::ggez::error;
use crate::ggez::error::GameResult;
use crate::ggez::graphics::shader::BlendMode;
use crate::ggez::graphics::types::FilterMode;
use crate::ggez::graphics::{self, transform_rect, BackendSpec, DrawParam, DrawTransform, Rect};
use gfx;
use gfx::Factory;
/// A `SpriteBatch` draws a number of copies of the same image, using a single draw call.
///
/// This is generally faster than drawing the same sprite with many
/// invocations of [`draw()`](../fn.draw.html), though it has a bit of
/// overhead to set up the batch. This overhead makes it run very
/// slowly in `debug` mode because it spends a lot of time on array
/// bounds checking and un-optimized math; you need to build with
/// optimizations enabled to really get the speed boost.
#[derive(Debug, Clone, PartialEq)]
pub struct SpriteBatch {
image: graphics::Image,
sprites: Vec<graphics::DrawParam>,
blend_mode: Option<BlendMode>,
}
/// An index of a particular sprite in a `SpriteBatch`.
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SpriteIdx(usize);
impl SpriteBatch {
/// Creates a new `SpriteBatch`, drawing with the given image.
///
/// Takes ownership of the `Image`, but cloning an `Image` is
/// cheap since they have an internal `Arc` containing the actual
/// image data.
pub fn new(image: graphics::Image) -> Self {
Self {
image,
sprites: vec![],
blend_mode: None,
}
}
/// Adds a new sprite to the sprite batch.
///
/// Returns a handle with which to modify the sprite using
/// [`set()`](#method.set)
pub fn add<P>(&mut self, param: P) -> SpriteIdx
where
P: Into<graphics::DrawParam>,
{
self.sprites.push(param.into());
SpriteIdx(self.sprites.len() - 1)
}
/// Alters a sprite in the batch to use the given draw params
pub fn set<P>(&mut self, handle: SpriteIdx, param: P) -> GameResult
where
P: Into<graphics::DrawParam>,
{
if handle.0 < self.sprites.len() {
self.sprites[handle.0] = param.into();
Ok(())
} else {
Err(error::GameError::RenderError(String::from(
"Provided index is out of bounds.",
)))
}
}
/// Immediately sends all data in the batch to the graphics card.
///
/// Generally just calling [`graphics::draw()`](../fn.draw.html) on the `SpriteBatch`
/// will do this automatically.
fn flush(&self, ctx: &mut Context, image: &graphics::Image) -> GameResult {
// This is a little awkward but this is the right place
// to do whatever transformations need to happen to DrawParam's.
// We have a Context, and *everything* must pass through this
// function to be drawn, so.
// Though we do awkwardly have to allocate a new vector.
// ...though upon benchmarking, the actual allocation is basically nothing,
// the cost in debug mode is alllll math.
let new_sprites = self
.sprites
.iter()
.map(|param| {
// Copy old params
let mut new_param = *param;
let src_width = param.src.w;
let src_height = param.src.h;
let real_scale = graphics::Vector2::new(
src_width * param.scale.x * f32::from(image.width),
src_height * param.scale.y * f32::from(image.height),
);
new_param.scale = real_scale.into();
new_param.color = new_param.color;
let primitive_param = graphics::DrawTransform::from(new_param);
primitive_param.to_instance_properties(ctx.gfx_context.is_srgb())
})
.collect::<Vec<_>>();
let gfx = &mut ctx.gfx_context;
if gfx.data.rect_instance_properties.len() < self.sprites.len() {
gfx.data.rect_instance_properties = gfx.factory.create_buffer(
self.sprites.len(),
gfx::buffer::Role::Vertex,
gfx::memory::Usage::Dynamic,
gfx::memory::Bind::TRANSFER_DST,
)?;
}
gfx.encoder
.update_buffer(&gfx.data.rect_instance_properties, &new_sprites[..], 0)?;
Ok(())
}
/// Removes all data from the sprite batch.
pub fn clear(&mut self) {
self.sprites.clear();
}
/// Unwraps and returns the contained `Image`
pub fn into_inner(self) -> graphics::Image {
self.image
}
/// Replaces the contained `Image`, returning the old one.
pub fn set_image(&mut self, image: graphics::Image) -> graphics::Image {
use std::mem;
mem::replace(&mut self.image, image)
}
/// Get the filter mode for the SpriteBatch.
pub fn filter(&self) -> FilterMode {
self.image.filter()
}
/// Set the filter mode for the SpriteBatch.
pub fn set_filter(&mut self, mode: FilterMode) {
self.image.set_filter(mode);
}
}
impl graphics::Drawable for SpriteBatch {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
// Awkwardly we must update values on all sprites and such.
// Also awkwardly we have this chain of colors with differing priorities.
self.flush(ctx, &self.image)?;
let gfx = &mut ctx.gfx_context;
let sampler = gfx
.samplers
.get_or_insert(self.image.sampler_info, gfx.factory.as_mut());
gfx.data.vbuf = gfx.quad_vertex_buffer.clone();
let typed_thingy = gfx
.backend_spec
.raw_to_typed_shader_resource(self.image.texture.clone());
gfx.data.tex = (typed_thingy, sampler);
let mut slice = gfx.quad_slice.clone();
slice.instances = Some((self.sprites.len() as u32, 0));
let curr_transform = gfx.transform();
let m: DrawTransform = param.into();
gfx.push_transform(m.matrix * curr_transform);
gfx.calculate_transform_matrix();
gfx.update_globals()?;
let previous_mode: Option<BlendMode> = if let Some(mode) = self.blend_mode {
let current_mode = gfx.blend_mode();
if current_mode != mode {
gfx.set_blend_mode(mode)?;
Some(current_mode)
} else {
None
}
} else {
None
};
gfx.draw(Some(&slice))?;
if let Some(mode) = previous_mode {
gfx.set_blend_mode(mode)?;
}
gfx.pop_transform();
gfx.calculate_transform_matrix();
gfx.update_globals()?;
Ok(())
}
fn dimensions(&self, _ctx: &mut Context) -> Option<Rect> {
if self.sprites.is_empty() {
return None;
}
let dimensions = self.image.dimensions();
self.sprites
.iter()
.map(|&param| transform_rect(dimensions, param))
.fold(None, |acc: Option<Rect>, rect| {
Some(if let Some(acc) = acc {
acc.combine_with(rect)
} else {
rect
})
})
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.blend_mode
}
}

717
src/ggez/graphics/text.rs Normal file
View File

@ -0,0 +1,717 @@
use std::borrow::Cow;
use std::cell::RefCell;
use std::f32;
use std::fmt;
use std::io::Read;
use std::path;
use glyph_brush::{self, FontId, Layout, SectionText, VariedSection};
pub use glyph_brush::{HorizontalAlign as Align, rusttype::Scale};
use glyph_brush::GlyphPositioner;
use mint;
use crate::ggez::graphics::{BlendMode, Color, Drawable, DrawParam, FilterMode, Rect, WHITE, draw, Image, BackendSpec, GlBackendSpec, Point2};
use crate::ggez::{Context, GameResult};
use gfx::texture::ImageInfoCommon;
/// Default size for fonts.
pub const DEFAULT_FONT_SCALE: f32 = 16.0;
/// A handle referring to a loaded Truetype font.
///
/// This is just an integer referring to a loaded font stored in the
/// `Context`, so is cheap to copy. Note that fonts are cached and
/// currently never *removed* from the cache, since that would
/// invalidate the whole cache and require re-loading all the other
/// fonts. So, you do not want to load a font more than once.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct Font {
font_id: FontId,
// Add DebugId? It makes Font::default() less convenient.
}
/// A piece of text with optional color, font and font scale information.
/// Drawing text generally involves one or more of these.
/// These options take precedence over any similar field/argument.
/// Implements `From` for `char`, `&str`, `String` and
/// `(String, Font, Scale)`.
#[derive(Clone, Debug)]
pub struct TextFragment {
/// Text string itself.
pub text: String,
/// Fragment's color, defaults to text's color.
pub color: Option<Color>,
/// Fragment's font, defaults to text's font.
pub font: Option<Font>,
/// Fragment's scale, defaults to text's scale.
pub scale: Option<Scale>,
}
impl Default for TextFragment {
fn default() -> Self {
TextFragment {
text: "".into(),
color: None,
font: None,
scale: None,
}
}
}
impl TextFragment {
/// Creates a new fragment from `String` or `&str`.
pub fn new<T: Into<Self>>(text: T) -> Self {
text.into()
}
/// Set fragment's color, overrides text's color.
pub fn color(mut self, color: Color) -> TextFragment {
self.color = Some(color);
self
}
/// Set fragment's font, overrides text's font.
pub fn font(mut self, font: Font) -> TextFragment {
self.font = Some(font);
self
}
/// Set fragment's scale, overrides text's scale.
pub fn scale(mut self, scale: Scale) -> TextFragment {
self.scale = Some(scale);
self
}
}
impl<'a> From<&'a str> for TextFragment {
fn from(text: &'a str) -> TextFragment {
TextFragment {
text: text.to_owned(),
..Default::default()
}
}
}
impl From<char> for TextFragment {
fn from(ch: char) -> TextFragment {
TextFragment {
text: ch.to_string(),
..Default::default()
}
}
}
impl From<String> for TextFragment {
fn from(text: String) -> TextFragment {
TextFragment {
text,
..Default::default()
}
}
}
impl<T> From<(T, Font, f32)> for TextFragment
where
T: Into<TextFragment>,
{
fn from((text, font, scale): (T, Font, f32)) -> TextFragment {
text.into().font(font).scale(Scale::uniform(scale))
}
}
/// Cached font metrics that we can keep attached to a `Text`
/// so we don't have to keep recalculating them.
#[derive(Clone, Debug)]
struct CachedMetrics {
string: Option<String>,
width: Option<u32>,
height: Option<u32>,
}
impl Default for CachedMetrics {
fn default() -> CachedMetrics {
CachedMetrics {
string: None,
width: None,
height: None,
}
}
}
/// Drawable text object. Essentially a list of [`TextFragment`](struct.TextFragment.html)'s
/// and some cached size information.
///
/// It implements [`Drawable`](trait.Drawable.html) so it can be drawn immediately with
/// [`graphics::draw()`](fn.draw.html), or many of them can be queued with [`graphics::queue_text()`](fn.queue_text.html)
/// and then all drawn at once with [`graphics::draw_queued_text()`](fn.draw_queued_text.html).
#[derive(Debug, Clone)]
pub struct Text {
fragments: Vec<TextFragment>,
blend_mode: Option<BlendMode>,
filter_mode: FilterMode,
bounds: Point2,
layout: Layout<glyph_brush::BuiltInLineBreaker>,
font_id: FontId,
font_scale: Scale,
cached_metrics: RefCell<CachedMetrics>,
}
impl Default for Text {
fn default() -> Self {
Text {
fragments: Vec::new(),
blend_mode: None,
filter_mode: FilterMode::Linear,
bounds: Point2::new(f32::INFINITY, f32::INFINITY),
layout: Layout::default(),
font_id: FontId::default(),
font_scale: Scale::uniform(DEFAULT_FONT_SCALE),
cached_metrics: RefCell::new(CachedMetrics::default()),
}
}
}
impl Text {
/// Creates a `Text` from a `TextFragment`.
///
/// ```rust
/// # use ggez::graphics::Text;
/// # fn main() {
/// let text = Text::new("foo");
/// # }
/// ```
pub fn new<F>(fragment: F) -> Text
where
F: Into<TextFragment>,
{
let mut text = Text::default();
let _ = text.add(fragment);
text
}
/// Appends a `TextFragment` to the `Text`.
pub fn add<F>(&mut self, fragment: F) -> &mut Text
where
F: Into<TextFragment>,
{
self.fragments.push(fragment.into());
self.invalidate_cached_metrics();
self
}
/// Returns a read-only slice of all `TextFragment`'s.
pub fn fragments(&self) -> &[TextFragment] {
&self.fragments
}
/// Returns a mutable slice with all fragments.
pub fn fragments_mut(&mut self) -> &mut [TextFragment] {
&mut self.fragments
}
/// Specifies rectangular dimensions to try and fit contents inside of,
/// by wrapping, and alignment within the bounds. To disable wrapping,
/// give it a layout with `f32::INF` for the x value.
pub fn set_bounds<P>(&mut self, bounds: P, alignment: Align) -> &mut Text
where
P: Into<mint::Point2<f32>>,
{
self.bounds = Point2::from(bounds.into());
if self.bounds.x == f32::INFINITY {
// Layouts don't make any sense if we don't wrap text at all.
self.layout = Layout::default();
} else {
self.layout = self.layout.h_align(alignment);
}
self.invalidate_cached_metrics();
self
}
/// Specifies text's font and font scale; used for fragments that don't have their own.
pub fn set_font(&mut self, font: Font, font_scale: Scale) -> &mut Text {
self.font_id = font.font_id;
self.font_scale = font_scale;
self.invalidate_cached_metrics();
self
}
/// Converts `Text` to a type `glyph_brush` can understand and queue.
fn generate_varied_section(
&self,
relative_dest: Point2,
color: Option<Color>,
) -> VariedSection {
let sections: Vec<SectionText> = self
.fragments
.iter()
.map(|fragment| {
let color = fragment.color.or(color).unwrap_or(WHITE);
let font_id = fragment
.font
.map(|font| font.font_id)
.unwrap_or(self.font_id);
let scale = fragment.scale.unwrap_or(self.font_scale);
SectionText {
text: &fragment.text,
color: <[f32; 4]>::from(color),
font_id,
scale,
}
})
.collect();
let relative_dest_x = {
// This positions text within bounds with relative_dest being to the left, always.
let mut dest_x = relative_dest.x;
if self.bounds.x != f32::INFINITY {
use glyph_brush::Layout::Wrap;
match self.layout {
Wrap {
h_align: Align::Center,
..
} => dest_x += self.bounds.x * 0.5,
Wrap {
h_align: Align::Right,
..
} => dest_x += self.bounds.x,
_ => (),
}
}
dest_x
};
let relative_dest = (relative_dest_x, relative_dest.y);
VariedSection {
screen_position: relative_dest,
bounds: (self.bounds.x, self.bounds.y),
layout: self.layout,
text: sections,
..Default::default()
}
}
fn invalidate_cached_metrics(&mut self) {
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
*metrics = CachedMetrics::default();
// Returning early avoids a double-borrow in the "else"
// part.
return;
}
warn!("Cached metrics RefCell has been poisoned.");
self.cached_metrics = RefCell::new(CachedMetrics::default());
}
/// Returns the string that the text represents.
pub fn contents(&self) -> String {
if let Ok(metrics) = self.cached_metrics.try_borrow() {
if let Some(ref string) = metrics.string {
return string.clone();
}
}
let string_accm: String = self
.fragments
.iter()
.map(|frag| frag.text.as_str())
.collect();
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
metrics.string = Some(string_accm.clone());
}
string_accm
}
/// Calculates, caches, and returns width and height of formatted and wrapped text.
fn calculate_dimensions(&self, context: &mut Context) -> (u32, u32) {
let mut max_width = 0;
let mut max_height = 0;
{
let varied_section = self.generate_varied_section(Point2::new(0.0, 0.0), None);
use glyph_brush::GlyphCruncher;
let glyphs = context.gfx_context.glyph_brush.glyphs(varied_section);
for positioned_glyph in glyphs {
if let Some(rect) = positioned_glyph.pixel_bounding_box() {
let font = positioned_glyph.font().expect("Glyph doesn't have a font");
let v_metrics = font.v_metrics(positioned_glyph.scale());
let max_y = positioned_glyph.position().y + positioned_glyph.scale().y
- v_metrics.ascent;
let max_y = max_y.ceil() as u32;
max_width = std::cmp::max(max_width, rect.max.x as u32);
max_height = std::cmp::max(max_height, max_y);
}
}
}
let (width, height) = (max_width, max_height);
if let Ok(mut metrics) = self.cached_metrics.try_borrow_mut() {
metrics.width = Some(width);
metrics.height = Some(height);
}
(width, height)
}
/// Returns the width and height of the formatted and wrapped text.
pub fn dimensions(&self, context: &mut Context) -> (u32, u32) {
if let Ok(metrics) = self.cached_metrics.try_borrow() {
if let (Some(width), Some(height)) = (metrics.width, metrics.height) {
return (width, height);
}
}
self.calculate_dimensions(context)
}
/// Returns the width of formatted and wrapped text, in screen coordinates.
pub fn width(&self, context: &mut Context) -> u32 {
self.dimensions(context).0
}
/// Returns the height of formatted and wrapped text, in screen coordinates.
pub fn height(&self, context: &mut Context) -> u32 {
self.dimensions(context).1
}
}
impl Drawable for Text {
fn draw(&self, ctx: &mut Context, param: DrawParam) -> GameResult {
// Converts fraction-of-bounding-box to screen coordinates, as required by `draw_queued()`.
queue_text(ctx, self, Point2::new(0.0, 0.0), Some(param.color));
draw_queued_text(ctx, param, self.blend_mode, self.filter_mode)
}
fn dimensions(&self, ctx: &mut Context) -> Option<Rect> {
let (w, h) = self.dimensions(ctx);
Some(Rect {
w: w as _,
h: h as _,
x: 0.0,
y: 0.0,
})
}
fn set_blend_mode(&mut self, mode: Option<BlendMode>) {
self.blend_mode = mode;
}
fn blend_mode(&self) -> Option<BlendMode> {
self.blend_mode
}
}
impl Font {
/// Load a new TTF font from the given file.
pub fn new<P>(context: &mut Context, path: P) -> GameResult<Font>
where
P: AsRef<path::Path> + fmt::Debug,
{
use crate::filesystem;
let mut stream = filesystem::open(context, path.as_ref())?;
let mut buf = Vec::new();
let _ = stream.read_to_end(&mut buf)?;
Font::new_glyph_font_bytes(context, &buf)
}
/// Loads a new TrueType font from given bytes and into a `gfx::GlyphBrush` owned
/// by the `Context`.
pub fn new_glyph_font_bytes(context: &mut Context, bytes: &[u8]) -> GameResult<Self> {
// Take a Cow here to avoid this clone where unnecessary?
// Nah, let's not complicate things more than necessary.
let v = bytes.to_vec();
let font_id = context.gfx_context.glyph_brush.add_font_bytes(v);
Ok(Font { font_id })
}
/// Returns the baked-in bytes of default font (currently `DejaVuSerif.ttf`).
pub(crate) fn default_font_bytes() -> &'static [u8] {
include_bytes!("DejaVuSansMono.ttf")
}
}
impl Default for Font {
fn default() -> Self {
Font { font_id: FontId(0) }
}
}
/// Queues the `Text` to be drawn by [`draw_queued_text()`](fn.draw_queued_text.html).
/// `relative_dest` is relative to the [`DrawParam::dest`](struct.DrawParam.html#structfield.dest)
/// passed to `draw_queued()`. Note, any `Text` drawn via [`graphics::draw()`](fn.draw.html)
/// will also draw everything already the queue.
pub fn queue_text<P>(context: &mut Context, batch: &Text, relative_dest: P, color: Option<Color>)
where
P: Into<mint::Point2<f32>>,
{
let p = Point2::from(relative_dest.into());
let varied_section = batch.generate_varied_section(p, color);
context.gfx_context.glyph_brush.queue(varied_section);
}
/// Exposes `glyph_brush`'s drawing API in case `ggez`'s text drawing is insufficient.
/// It takes `glyph_brush`'s `VariedSection` and `GlyphPositioner`, which give you lower-
/// level control over how text is drawn.
pub fn queue_text_raw<'a, S, G>(context: &mut Context, section: S, custom_layout: Option<&G>)
where
S: Into<Cow<'a, VariedSection<'a>>>,
G: GlyphPositioner,
{
let brush = &mut context.gfx_context.glyph_brush;
match custom_layout {
Some(layout) => brush.queue_custom_layout(section, layout),
None => brush.queue(section),
}
}
/// Draws all of the [`Text`](struct.Text.html)s added via [`queue_text()`](fn.queue_text.html).
///
/// the `DrawParam` applies to everything in the queue; offset is in
/// screen coordinates; color is ignored - specify it when using
/// `queue_text()` instead.
///
/// Note that all text will, and in fact must, be drawn with the same
/// `BlendMode` and `FilterMode`. This is unfortunate but currently
/// unavoidable, see [this issue](https://github.com/ggez/ggez/issues/561)
/// for more info.
pub fn draw_queued_text<D>(
ctx: &mut Context,
param: D,
blend: Option<BlendMode>,
filter: FilterMode,
) -> GameResult
where
D: Into<DrawParam>,
{
let param: DrawParam = param.into();
let gb = &mut ctx.gfx_context.glyph_brush;
let encoder = &mut ctx.gfx_context.encoder;
let gc = &ctx.gfx_context.glyph_cache.texture_handle;
let backend = &ctx.gfx_context.backend_spec;
let action = gb.process_queued(
|rect, tex_data| update_texture::<GlBackendSpec>(backend, encoder, gc, rect, tex_data),
to_vertex,
);
match action {
Ok(glyph_brush::BrushAction::ReDraw) => {
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
spritebatch.set_blend_mode(blend);
spritebatch.set_filter(filter);
draw(ctx, &*spritebatch, param)?;
}
Ok(glyph_brush::BrushAction::Draw(drawparams)) => {
// Gotta clone the image to avoid double-borrow's.
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
spritebatch.clear();
spritebatch.set_blend_mode(blend);
spritebatch.set_filter(filter);
for p in &drawparams {
// Ignore returned sprite index.
let _ = spritebatch.add(*p);
}
draw(ctx, &*spritebatch, param)?;
}
Err(glyph_brush::BrushError::TextureTooSmall { suggested }) => {
let (new_width, new_height) = suggested;
let data = vec![255; 4 * new_width as usize * new_height as usize];
let new_glyph_cache =
Image::from_rgba8(ctx, new_width as u16, new_height as u16, &data)?;
ctx.gfx_context.glyph_cache = new_glyph_cache.clone();
let spritebatch = ctx.gfx_context.glyph_state.clone();
let spritebatch = &mut *spritebatch.borrow_mut();
let _ = spritebatch.set_image(new_glyph_cache);
ctx.gfx_context
.glyph_brush
.resize_texture(new_width, new_height);
}
}
Ok(())
}
fn update_texture<B>(
backend: &B,
encoder: &mut gfx::Encoder<B::Resources, B::CommandBuffer>,
texture: &gfx::handle::RawTexture<B::Resources>,
rect: glyph_brush::rusttype::Rect<u32>,
tex_data: &[u8],
) where
B: BackendSpec,
{
let offset = [rect.min.x as u16, rect.min.y as u16];
let size = [rect.width() as u16, rect.height() as u16];
let info = ImageInfoCommon {
xoffset: offset[0],
yoffset: offset[1],
zoffset: 0,
width: size[0],
height: size[1],
depth: 0,
format: (),
mipmap: 0,
};
let tex_data_chunks: Vec<[u8; 4]> = tex_data.iter().map(|c| [255, 255, 255, *c]).collect();
let typed_tex = backend.raw_to_typed_texture(texture.clone());
encoder
.update_texture::<<super::BuggoSurfaceFormat as gfx::format::Formatted>::Surface, super::BuggoSurfaceFormat>(
&typed_tex, None, info, &tex_data_chunks,
)
.unwrap();
}
/// I THINK what we're going to need to do is have a
/// `SpriteBatch` that actually does the stuff and stores the
/// UV's and verts and such, while
///
/// Basically, `glyph_brush`'s "`to_vertex`" callback is really
/// `to_quad`; in the default code it
fn to_vertex(v: glyph_brush::GlyphVertex) -> DrawParam {
let src_rect = Rect {
x: v.tex_coords.min.x,
y: v.tex_coords.min.y,
w: v.tex_coords.max.x - v.tex_coords.min.x,
h: v.tex_coords.max.y - v.tex_coords.min.y,
};
// it LOOKS like pixel_coords are the output coordinates?
// I'm not sure though...
let dest_pt = Point2::new(v.pixel_coords.min.x as f32, v.pixel_coords.min.y as f32);
DrawParam::default()
.src(src_rect)
.dest(dest_pt)
.color(v.color.into())
}
#[cfg(test)]
mod tests {
/*
use super::*;
#[test]
fn test_metrics() {
let f = Font::default_font().expect("Could not get default font");
assert_eq!(f.height(), 17);
assert_eq!(f.width("Foo!"), 33);
// http://www.catipsum.com/index.php
let text_to_wrap = "Walk on car leaving trail of paw prints on hood and windshield sniff \
other cat's butt and hang jaw half open thereafter for give attitude. \
Annoy kitten\nbrother with poking. Mrow toy mouse squeak roll over. \
Human give me attention meow.";
let (len, v) = f.wrap(text_to_wrap, 250);
println!("{} {:?}", len, v);
assert_eq!(len, 249);
/*
let wrapped_text = vec![
"Walk on car leaving trail of paw prints",
"on hood and windshield sniff other",
"cat\'s butt and hang jaw half open",
"thereafter for give attitude. Annoy",
"kitten",
"brother with poking. Mrow toy",
"mouse squeak roll over. Human give",
"me attention meow."
];
*/
let wrapped_text = vec![
"Walk on car leaving trail of paw",
"prints on hood and windshield",
"sniff other cat\'s butt and hang jaw",
"half open thereafter for give",
"attitude. Annoy kitten",
"brother with poking. Mrow toy",
"mouse squeak roll over. Human",
"give me attention meow.",
];
assert_eq!(&v, &wrapped_text);
}
// We sadly can't have this test in the general case because it needs to create a Context,
// which creates a window, which fails on a headless server like our CI systems. :/
//#[test]
#[allow(dead_code)]
fn test_wrapping() {
use conf;
let c = conf::Conf::new();
let (ctx, _) = &mut Context::load_from_conf("test_wrapping", "ggez", c)
.expect("Could not create context?");
let font = Font::default_font().expect("Could not get default font");
let text_to_wrap = "Walk on car leaving trail of paw prints on hood and windshield sniff \
other cat's butt and hang jaw half open thereafter for give attitude. \
Annoy kitten\nbrother with poking. Mrow toy mouse squeak roll over. \
Human give me attention meow.";
let wrap_length = 250;
let (len, v) = font.wrap(text_to_wrap, wrap_length);
assert!(len < wrap_length);
for line in &v {
let t = Text::new(ctx, line, &font).unwrap();
println!(
"Width is claimed to be <= {}, should be <= {}, is {}",
len,
wrap_length,
t.width()
);
// Why does this not match? x_X
//assert!(t.width() as usize <= len);
assert!(t.width() as usize <= wrap_length);
}
}
*/
}
/*
// Creates a gfx texture with the given data
fn create_texture<F, R>(
factory: &mut F,
width: u32,
height: u32,
) -> Result<(TexSurfaceHandle<R>, TexShaderView<R>), Box<dyn Error>>
where
R: gfx::Resources,
F: gfx::Factory<R>,
{
let kind = texture::Kind::D2(
width as texture::Size,
height as texture::Size,
texture::AaMode::Single,
);
let tex = factory.create_texture(
kind,
1 as texture::Level,
gfx::memory::Bind::SHADER_RESOURCE,
gfx::memory::Usage::Dynamic,
Some(<TexChannel as format::ChannelTyped>::get_channel_type()),
)?;
let view =
factory.view_texture_as_shader_resource::<TexForm>(&tex, (0, 0), format::Swizzle::new())?;
Ok((tex, view))
}
// Updates a texture with the given data (used for updating the GlyphCache texture)
#[inline]
fn update_texture<R, C>(
encoder: &mut gfx::Encoder<R, C>,
texture: &handle::Texture<R, TexSurface>,
offset: [u16; 2],
size: [u16; 2],
data: &[u8],
) where
R: gfx::Resources,
C: gfx::CommandBuffer<R>,
{
let info = texture::ImageInfoCommon {
xoffset: offset[0],
yoffset: offset[1],
zoffset: 0,
width: size[0],
height: size[1],
depth: 0,
format: (),
mipmap: 0,
};
encoder
.update_texture::<TexSurface, TexForm>(texture, None, info, data)
.unwrap();
}
*/

738
src/ggez/graphics/types.rs Normal file
View File

@ -0,0 +1,738 @@
pub(crate) use nalgebra as na;
use std::f32;
use std::u32;
use crate::graphics::{FillOptions, StrokeOptions};
/// A 2 dimensional point representing a location
pub(crate) type Point2 = na::Point2<f32>;
/// A 2 dimensional vector representing an offset of a location
pub(crate) type Vector2 = na::Vector2<f32>;
/// A 4 dimensional matrix representing an arbitrary 3d transformation
pub(crate) type Matrix4 = na::Matrix4<f32>;
/// A simple 2D rectangle.
///
/// The origin of the rectangle is at the top-left,
/// with x increasing to the right and y increasing down.
#[derive(Copy, Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
pub struct Rect {
/// X coordinate of the left edge of the rect.
pub x: f32,
/// Y coordinate of the top edge of the rect.
pub y: f32,
/// Total width of the rect
pub w: f32,
/// Total height of the rect.
pub h: f32,
}
impl Rect {
/// Create a new `Rect`.
pub const fn new(x: f32, y: f32, w: f32, h: f32) -> Self {
Rect { x, y, w, h }
}
/// Creates a new `Rect` a la Love2D's `love.graphics.newQuad`,
/// as a fraction of the reference rect's size.
pub fn fraction(x: f32, y: f32, w: f32, h: f32, reference: &Rect) -> Rect {
Rect {
x: x / reference.w,
y: y / reference.h,
w: w / reference.w,
h: h / reference.h,
}
}
/// Create a new rect from `i32` coordinates.
pub const fn new_i32(x: i32, y: i32, w: i32, h: i32) -> Self {
Rect {
x: x as f32,
y: y as f32,
w: w as f32,
h: h as f32,
}
}
/// Create a new `Rect` with all values zero.
pub const fn zero() -> Self {
Self::new(0.0, 0.0, 0.0, 0.0)
}
/// Creates a new `Rect` at `0,0` with width and height 1.
pub const fn one() -> Self {
Self::new(0.0, 0.0, 1.0, 1.0)
}
/// Gets the `Rect`'s x and y coordinates as a `Point2`.
pub const fn point(&self) -> mint::Point2<f32> {
mint::Point2 {
x: self.x,
y: self.y,
}
}
/// Returns the left edge of the `Rect`
pub const fn left(&self) -> f32 {
self.x
}
/// Returns the right edge of the `Rect`
pub fn right(&self) -> f32 {
self.x + self.w
}
/// Returns the top edge of the `Rect`
pub const fn top(&self) -> f32 {
self.y
}
/// Returns the bottom edge of the `Rect`
pub fn bottom(&self) -> f32 {
self.y + self.h
}
/// Checks whether the `Rect` contains a `Point`
pub fn contains<P>(&self, point: P) -> bool
where
P: Into<mint::Point2<f32>>,
{
let point = point.into();
point.x >= self.left()
&& point.x <= self.right()
&& point.y <= self.bottom()
&& point.y >= self.top()
}
/// Checks whether the `Rect` overlaps another `Rect`
pub fn overlaps(&self, other: &Rect) -> bool {
self.left() <= other.right()
&& self.right() >= other.left()
&& self.top() <= other.bottom()
&& self.bottom() >= other.top()
}
/// Translates the `Rect` by an offset of (x, y)
pub fn translate<V>(&mut self, offset: V)
where
V: Into<mint::Vector2<f32>>,
{
let offset = offset.into();
self.x += offset.x;
self.y += offset.y;
}
/// Moves the `Rect`'s origin to (x, y)
pub fn move_to<P>(&mut self, destination: P)
where
P: Into<mint::Point2<f32>>,
{
let destination = destination.into();
self.x = destination.x;
self.y = destination.y;
}
/// Scales the `Rect` by a factor of (sx, sy),
/// growing towards the bottom-left
pub fn scale(&mut self, sx: f32, sy: f32) {
self.w *= sx;
self.h *= sy;
}
/// Calculated the new Rect around the rotated one.
pub fn rotate(&mut self, rotation: f32) {
let rotation = na::Rotation2::new(rotation);
let x0 = self.x;
let y0 = self.y;
let x1 = self.right();
let y1 = self.bottom();
let points = [
rotation * na::Point2::new(x0, y0),
rotation * na::Point2::new(x0, y1),
rotation * na::Point2::new(x1, y0),
rotation * na::Point2::new(x1, y1),
];
let p0 = points[0];
let mut x_max = p0.x;
let mut x_min = p0.x;
let mut y_max = p0.y;
let mut y_min = p0.y;
for p in &points {
x_max = f32::max(x_max, p.x);
x_min = f32::min(x_min, p.x);
y_max = f32::max(y_max, p.y);
y_min = f32::min(y_min, p.y);
}
*self = Rect {
w: x_max - x_min,
h: y_max - y_min,
x: x_min,
y: y_min,
}
}
/// Returns a new `Rect` that includes all points of these two `Rect`s.
pub fn combine_with(self, other: Rect) -> Rect {
let x = f32::min(self.x, other.x);
let y = f32::min(self.y, other.y);
let w = f32::max(self.right(), other.right()) - x;
let h = f32::max(self.bottom(), other.bottom()) - y;
Rect { x, y, w, h }
}
}
impl approx::AbsDiffEq for Rect {
type Epsilon = f32;
fn default_epsilon() -> Self::Epsilon {
f32::default_epsilon()
}
fn abs_diff_eq(&self, other: &Self, epsilon: Self::Epsilon) -> bool {
f32::abs_diff_eq(&self.x, &other.x, epsilon)
&& f32::abs_diff_eq(&self.y, &other.y, epsilon)
&& f32::abs_diff_eq(&self.w, &other.w, epsilon)
&& f32::abs_diff_eq(&self.h, &other.h, epsilon)
}
}
impl approx::RelativeEq for Rect {
fn default_max_relative() -> Self::Epsilon {
f32::default_max_relative()
}
fn relative_eq(
&self,
other: &Self,
epsilon: Self::Epsilon,
max_relative: Self::Epsilon,
) -> bool {
f32::relative_eq(&self.x, &other.x, epsilon, max_relative)
&& f32::relative_eq(&self.y, &other.y, epsilon, max_relative)
&& f32::relative_eq(&self.w, &other.w, epsilon, max_relative)
&& f32::relative_eq(&self.h, &other.h, epsilon, max_relative)
}
}
impl From<[f32; 4]> for Rect {
fn from(val: [f32; 4]) -> Self {
Rect::new(val[0], val[1], val[2], val[3])
}
}
impl From<Rect> for [f32; 4] {
fn from(val: Rect) -> Self {
[val.x, val.y, val.w, val.h]
}
}
/// A RGBA color in the `sRGB` color space represented as `f32`'s in the range `[0.0-1.0]`
///
/// For convenience, [`WHITE`](constant.WHITE.html) and [`BLACK`](constant.BLACK.html) are provided.
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
pub struct Color {
/// Red component
pub r: f32,
/// Green component
pub g: f32,
/// Blue component
pub b: f32,
/// Alpha component
pub a: f32,
}
/// White
pub const WHITE: Color = Color {
r: 1.0,
g: 1.0,
b: 1.0,
a: 1.0,
};
/// Black
pub const BLACK: Color = Color {
r: 0.0,
g: 0.0,
b: 0.0,
a: 1.0,
};
impl Color {
/// Create a new `Color` from four `f32`'s in the range `[0.0-1.0]`
pub const fn new(r: f32, g: f32, b: f32, a: f32) -> Self {
Color { r, g, b, a }
}
/// Create a new `Color` from four `u8`'s in the range `[0-255]`
pub fn from_rgba(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::from((r, g, b, a))
}
/// Create a new `Color` from three u8's in the range `[0-255]`,
/// with the alpha component fixed to 255 (opaque)
pub fn from_rgb(r: u8, g: u8, b: u8) -> Color {
Color::from((r, g, b))
}
/// Return a tuple of four `u8`'s in the range `[0-255]` with the `Color`'s
/// components.
pub fn to_rgba(self) -> (u8, u8, u8, u8) {
self.into()
}
/// Return a tuple of three `u8`'s in the range `[0-255]` with the `Color`'s
/// components.
pub fn to_rgb(self) -> (u8, u8, u8) {
self.into()
}
/// Convert a packed `u32` containing `0xRRGGBBAA` into a `Color`
pub fn from_rgba_u32(c: u32) -> Color {
let c = c.to_be_bytes();
Color::from((c[0], c[1], c[2], c[3]))
}
/// Convert a packed `u32` containing `0x00RRGGBB` into a `Color`.
/// This lets you do things like `Color::from_rgb_u32(0xCD09AA)` easily if you want.
pub fn from_rgb_u32(c: u32) -> Color {
let c = c.to_be_bytes();
Color::from((c[1], c[2], c[3]))
}
/// Convert a `Color` into a packed `u32`, containing `0xRRGGBBAA` as bytes.
pub fn to_rgba_u32(self) -> u32 {
let (r, g, b, a): (u8, u8, u8, u8) = self.into();
u32::from_be_bytes([r, g, b, a])
}
/// Convert a `Color` into a packed `u32`, containing `0x00RRGGBB` as bytes.
pub fn to_rgb_u32(self) -> u32 {
let (r, g, b, _a): (u8, u8, u8, u8) = self.into();
u32::from_be_bytes([0, r, g, b])
}
}
impl From<(u8, u8, u8, u8)> for Color {
/// Convert a `(R, G, B, A)` tuple of `u8`'s in the range `[0-255]` into a `Color`
fn from(val: (u8, u8, u8, u8)) -> Self {
let (r, g, b, a) = val;
let rf = (f32::from(r)) / 255.0;
let gf = (f32::from(g)) / 255.0;
let bf = (f32::from(b)) / 255.0;
let af = (f32::from(a)) / 255.0;
Color::new(rf, gf, bf, af)
}
}
impl From<(u8, u8, u8)> for Color {
/// Convert a `(R, G, B)` tuple of `u8`'s in the range `[0-255]` into a `Color`,
/// with a value of 255 for the alpha element (i.e., no transparency.)
fn from(val: (u8, u8, u8)) -> Self {
let (r, g, b) = val;
Color::from((r, g, b, 255))
}
}
impl From<[f32; 4]> for Color {
/// Turns an `[R, G, B, A] array of `f32`'s into a `Color` with no format changes.
/// All inputs should be in the range `[0.0-1.0]`.
fn from(val: [f32; 4]) -> Self {
Color::new(val[0], val[1], val[2], val[3])
}
}
impl From<(f32, f32, f32)> for Color {
/// Convert a `(R, G, B)` tuple of `f32`'s in the range `[0.0-1.0]` into a `Color`,
/// with a value of 1.0 to for the alpha element (ie, no transparency.)
fn from(val: (f32, f32, f32)) -> Self {
let (r, g, b) = val;
Color::new(r, g, b, 1.0)
}
}
impl From<(f32, f32, f32, f32)> for Color {
/// Convert a `(R, G, B, A)` tuple of `f32`'s in the range `[0.0-1.0]` into a `Color`
fn from(val: (f32, f32, f32, f32)) -> Self {
let (r, g, b, a) = val;
Color::new(r, g, b, a)
}
}
impl From<Color> for (u8, u8, u8, u8) {
/// Convert a `Color` into a `(R, G, B, A)` tuple of `u8`'s in the range of `[0-255]`.
fn from(color: Color) -> Self {
let r = (color.r * 255.0) as u8;
let g = (color.g * 255.0) as u8;
let b = (color.b * 255.0) as u8;
let a = (color.a * 255.0) as u8;
(r, g, b, a)
}
}
impl From<Color> for (u8, u8, u8) {
/// Convert a `Color` into a `(R, G, B)` tuple of `u8`'s in the range of `[0-255]`,
/// ignoring the alpha term.
fn from(color: Color) -> Self {
let (r, g, b, _) = color.into();
(r, g, b)
}
}
impl From<Color> for [f32; 4] {
/// Convert a `Color` into an `[R, G, B, A]` array of `f32`'s in the range of `[0.0-1.0]`.
fn from(color: Color) -> Self {
[color.r, color.g, color.b, color.a]
}
}
/// A RGBA color in the *linear* color space,
/// suitable for shoving into a shader.
#[derive(Copy, Clone, PartialEq, Debug, Serialize, Deserialize)]
pub(crate) struct LinearColor {
/// Red component
pub r: f32,
/// Green component
pub g: f32,
/// Blue component
pub b: f32,
/// Alpha component
pub a: f32,
}
impl From<Color> for LinearColor {
/// Convert an (sRGB) Color into a linear color,
/// per https://en.wikipedia.org/wiki/Srgb#The_reverse_transformation
fn from(c: Color) -> Self {
fn f(component: f32) -> f32 {
let a = 0.055;
if component <= 0.04045 {
component / 12.92
} else {
((component + a) / (1.0 + a)).powf(2.4)
}
}
LinearColor {
r: f(c.r),
g: f(c.g),
b: f(c.b),
a: c.a,
}
}
}
impl From<LinearColor> for Color {
fn from(c: LinearColor) -> Self {
fn f(component: f32) -> f32 {
let a = 0.055;
if component <= 0.003_130_8 {
component * 12.92
} else {
(1.0 + a) * component.powf(1.0 / 2.4)
}
}
Color {
r: f(c.r),
g: f(c.g),
b: f(c.b),
a: c.a,
}
}
}
impl From<LinearColor> for [f32; 4] {
fn from(color: LinearColor) -> Self {
[color.r, color.g, color.b, color.a]
}
}
/// Specifies whether a mesh should be drawn
/// filled or as an outline.
#[derive(Debug, Copy, Clone)]
pub enum DrawMode {
/// A stroked line with given parameters, see `StrokeOptions` documentation.
Stroke(StrokeOptions),
/// A filled shape with given parameters, see `FillOptions` documentation.
Fill(FillOptions),
}
impl DrawMode {
/// Constructs a DrawMode that draws a stroke with the given width
pub fn stroke(width: f32) -> DrawMode {
DrawMode::Stroke(StrokeOptions::default().with_line_width(width))
}
/// Constructs a DrawMode that fills shapes with default fill options.
pub fn fill() -> DrawMode {
DrawMode::Fill(FillOptions::default())
}
}
/// Specifies what blending method to use when scaling up/down images.
#[derive(Debug, Copy, Clone)]
pub enum FilterMode {
/// Use linear interpolation (ie, smooth)
Linear,
/// Use nearest-neighbor interpolation (ie, pixelated)
Nearest,
}
use gfx::texture::FilterMethod;
impl From<FilterMethod> for FilterMode {
fn from(f: FilterMethod) -> Self {
match f {
FilterMethod::Scale => FilterMode::Nearest,
_other => FilterMode::Linear,
}
}
}
impl From<FilterMode> for FilterMethod {
fn from(f: FilterMode) -> Self {
match f {
FilterMode::Nearest => FilterMethod::Scale,
FilterMode::Linear => FilterMethod::Bilinear,
}
}
}
/// Specifies how to wrap textures.
pub use gfx::texture::WrapMode;
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
use std::f32::consts::PI;
#[test]
fn headless_test_color_conversions() {
let white = Color::new(1.0, 1.0, 1.0, 1.0);
let w1 = Color::from((255, 255, 255, 255));
assert_eq!(white, w1);
let w2: u32 = white.to_rgba_u32();
assert_eq!(w2, 0xFFFF_FFFFu32);
let grey = Color::new(0.5019608, 0.5019608, 0.5019608, 1.0);
let g1 = Color::from((128, 128, 128, 255));
assert_eq!(grey, g1);
let g2: u32 = grey.to_rgba_u32();
assert_eq!(g2, 0x8080_80FFu32);
let black = Color::new(0.0, 0.0, 0.0, 1.0);
let b1 = Color::from((0, 0, 0, 255));
assert_eq!(black, b1);
let b2: u32 = black.to_rgba_u32();
assert_eq!(b2, 0x0000_00FFu32);
assert_eq!(black, Color::from_rgb_u32(0x00_0000u32));
assert_eq!(black, Color::from_rgba_u32(0x00_0000FFu32));
let puce1 = Color::from_rgb_u32(0xCC_8899u32);
let puce2 = Color::from_rgba_u32(0xCC88_99FFu32);
let puce3 = Color::from((0xCC, 0x88, 0x99, 255));
let puce4 = Color::new(0.80, 0.53333336, 0.60, 1.0);
assert_eq!(puce1, puce2);
assert_eq!(puce1, puce3);
assert_eq!(puce1, puce4);
}
#[test]
fn headless_test_rect_scaling() {
let r1 = Rect::new(0.0, 0.0, 128.0, 128.0);
let r2 = Rect::fraction(0.0, 0.0, 32.0, 32.0, &r1);
assert_eq!(r2, Rect::new(0.0, 0.0, 0.25, 0.25));
let r2 = Rect::fraction(32.0, 32.0, 32.0, 32.0, &r1);
assert_eq!(r2, Rect::new(0.25, 0.25, 0.25, 0.25));
}
#[test]
fn headless_test_rect_contains() {
let r = Rect::new(0.0, 0.0, 128.0, 128.0);
println!("{} {} {} {}", r.top(), r.bottom(), r.left(), r.right());
let p = Point2::new(1.0, 1.0);
assert!(r.contains(p));
let p = Point2::new(500.0, 0.0);
assert!(!r.contains(p));
}
#[test]
fn headless_test_rect_overlaps() {
let r1 = Rect::new(0.0, 0.0, 128.0, 128.0);
let r2 = Rect::new(0.0, 0.0, 64.0, 64.0);
assert!(r1.overlaps(&r2));
let r2 = Rect::new(100.0, 0.0, 128.0, 128.0);
assert!(r1.overlaps(&r2));
let r2 = Rect::new(500.0, 0.0, 64.0, 64.0);
assert!(!r1.overlaps(&r2));
}
#[test]
fn headless_test_rect_transform() {
let mut r1 = Rect::new(0.0, 0.0, 64.0, 64.0);
let r2 = Rect::new(64.0, 64.0, 64.0, 64.0);
r1.translate(Vector2::new(64.0, 64.0));
assert!(r1 == r2);
let mut r1 = Rect::new(0.0, 0.0, 64.0, 64.0);
let r2 = Rect::new(0.0, 0.0, 128.0, 128.0);
r1.scale(2.0, 2.0);
assert!(r1 == r2);
let mut r1 = Rect::new(32.0, 32.0, 64.0, 64.0);
let r2 = Rect::new(64.0, 64.0, 64.0, 64.0);
r1.move_to(Point2::new(64.0, 64.0));
assert!(r1 == r2);
}
#[test]
fn headless_test_rect_combine_with() {
{
let a = Rect {
x: 0.0,
y: 0.0,
w: 1.0,
h: 1.0,
};
let b = Rect {
x: 0.0,
y: 0.0,
w: 1.0,
h: 1.0,
};
let c = a.combine_with(b);
assert_relative_eq!(a, b);
assert_relative_eq!(a, c);
}
{
let a = Rect {
x: 0.0,
y: 0.0,
w: 1.0,
h: 2.0,
};
let b = Rect {
x: 0.0,
y: 0.0,
w: 2.0,
h: 1.0,
};
let real = a.combine_with(b);
let expected = Rect {
x: 0.0,
y: 0.0,
w: 2.0,
h: 2.0,
};
assert_relative_eq!(real, expected);
}
{
let a = Rect {
x: -1.0,
y: 0.0,
w: 2.0,
h: 2.0,
};
let b = Rect {
x: 0.0,
y: -1.0,
w: 1.0,
h: 1.0,
};
let real = a.combine_with(b);
let expected = Rect {
x: -1.0,
y: -1.0,
w: 2.0,
h: 3.0,
};
assert_relative_eq!(real, expected);
}
}
#[test]
fn headless_test_rect_rotate() {
{
let mut r = Rect {
x: -0.5,
y: -0.5,
w: 1.0,
h: 1.0,
};
let expected = r;
r.rotate(PI * 2.0);
assert_relative_eq!(r, expected);
}
{
let mut r = Rect {
x: 0.0,
y: 0.0,
w: 1.0,
h: 2.0,
};
r.rotate(PI * 0.5);
let expected = Rect {
x: -2.0,
y: 0.0,
w: 2.0,
h: 1.0,
};
assert_relative_eq!(r, expected);
}
{
let mut r = Rect {
x: 0.0,
y: 0.0,
w: 1.0,
h: 2.0,
};
r.rotate(PI);
let expected = Rect {
x: -1.0,
y: -2.0,
w: 1.0,
h: 2.0,
};
assert_relative_eq!(r, expected);
}
{
let mut r = Rect {
x: -0.5,
y: -0.5,
w: 1.0,
h: 1.0,
};
r.rotate(PI * 0.5);
let expected = Rect {
x: -0.5,
y: -0.5,
w: 1.0,
h: 1.0,
};
assert_relative_eq!(r, expected);
}
{
let mut r = Rect {
x: 1.0,
y: 1.0,
w: 0.5,
h: 2.0,
};
r.rotate(PI * 0.5);
let expected = Rect {
x: -3.0,
y: 1.0,
w: 2.0,
h: 0.5,
};
assert_relative_eq!(r, expected);
}
}
}

107
src/ggez/input/gamepad.rs Normal file
View File

@ -0,0 +1,107 @@
//! Gamepad utility functions.
//!
//! This is going to be a bit of a work-in-progress as gamepad input
//! gets fleshed out. The `gilrs` crate needs help to add better
//! cross-platform support. Why not give it a hand?
use std::fmt;
pub use gilrs::{self, Event, Gamepad, Gilrs};
/// A unique identifier for a particular GamePad
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct GamepadId(pub(crate) gilrs::GamepadId);
use crate::ggez::context::Context;
use crate::ggez::error::GameResult;
/// Trait object defining a gamepad/joystick context.
pub trait GamepadContext {
/// Returns a gamepad event.
fn next_event(&mut self) -> Option<Event>;
/// returns the `Gamepad` associated with an id.
fn gamepad(&self, id: GamepadId) -> Gamepad;
}
/// A structure that contains gamepad state using `gilrs`.
pub struct GilrsGamepadContext {
pub(crate) gilrs: Gilrs,
}
impl fmt::Debug for GilrsGamepadContext {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "<GilrsGamepadContext: {:p}>", self)
}
}
impl GilrsGamepadContext {
pub(crate) fn new() -> GameResult<Self> {
let gilrs = Gilrs::new()?;
Ok(GilrsGamepadContext { gilrs })
}
}
impl GamepadContext for GilrsGamepadContext {
fn next_event(&mut self) -> Option<Event> {
self.gilrs.next_event()
}
fn gamepad(&self, id: GamepadId) -> Gamepad {
self.gilrs.gamepad(id.0)
}
}
/// A structure that implements [`GamepadContext`](trait.GamepadContext.html)
/// but does nothing; a stub for when you don't need it or are
/// on a platform that `gilrs` doesn't support.
#[derive(Debug, Clone, Copy, Default)]
pub(crate) struct NullGamepadContext {}
impl GamepadContext for NullGamepadContext {
fn next_event(&mut self) -> Option<Event> {
panic!("Gamepad module disabled")
}
fn gamepad(&self, _id: GamepadId) -> Gamepad {
panic!("Gamepad module disabled")
}
}
/// Returns the `Gamepad` associated with an `id`.
pub fn gamepad(ctx: &Context, id: GamepadId) -> Gamepad {
ctx.gamepad_context.gamepad(id)
}
// Properties gamepads might want:
// Number of buttons
// Number of axes
// Name/ID
// Is it connected? (For consoles?)
// Whether or not they support vibration
/*
/// Lists all gamepads. With metainfo, maybe?
pub fn list_gamepads() {
unimplemented!()
}
/// Returns the state of the given axis on a gamepad.
pub fn axis() {
unimplemented!()
}
/// Returns the state of the given button on a gamepad.
pub fn button_pressed() {
unimplemented!()
}
*/
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn gilrs_init() {
assert!(GilrsGamepadContext::new().is_ok());
}
}

413
src/ggez/input/keyboard.rs Normal file
View File

@ -0,0 +1,413 @@
//! Keyboard utility functions; allow querying state of keyboard keys and modifiers.
//!
//! Example:
//!
//! ```rust, compile
//! use ggez::event::{self, EventHandler, KeyCode, KeyMods};
//! use ggez::{graphics, nalgebra as na, timer};
//! use ggez::input::keyboard;
//! use ggez::{Context, GameResult};
//!
//! struct MainState {
//! position_x: f32,
//! }
//!
//! impl EventHandler for MainState {
//! fn update(&mut self, ctx: &mut Context) -> GameResult {
//! // Increase or decrease `position_x` by 0.5, or by 5.0 if Shift is held.
//! if keyboard::is_key_pressed(ctx, KeyCode::Right) {
//! if keyboard::is_mod_active(ctx, KeyMods::SHIFT) {
//! self.position_x += 4.5;
//! }
//! self.position_x += 0.5;
//! } else if keyboard::is_key_pressed(ctx, KeyCode::Left) {
//! if keyboard::is_mod_active(ctx, KeyMods::SHIFT) {
//! self.position_x -= 4.5;
//! }
//! self.position_x -= 0.5;
//! }
//! Ok(())
//! }
//!
//! fn draw(&mut self, ctx: &mut Context) -> GameResult {
//! graphics::clear(ctx, [0.1, 0.2, 0.3, 1.0].into());
//! // Create a circle at `position_x` and draw
//! let circle = graphics::Mesh::new_circle(
//! ctx,
//! graphics::DrawMode::fill(),
//! na::Point2::new(self.position_x, 380.0),
//! 100.0,
//! 2.0,
//! graphics::WHITE,
//! )?;
//! graphics::draw(ctx, &circle, graphics::DrawParam::default())?;
//! graphics::present(ctx)?;
//! timer::yield_now();
//! Ok(())
//! }
//!
//! fn key_down_event(&mut self, ctx: &mut Context, key: KeyCode, mods: KeyMods, _: bool) {
//! match key {
//! // Quit if Shift+Ctrl+Q is pressed.
//! KeyCode::Q => {
//! if mods.contains(KeyMods::SHIFT & KeyMods::CTRL) {
//! println!("Terminating!");
//! event::quit(ctx);
//! } else if mods.contains(KeyMods::SHIFT) || mods.contains(KeyMods::CTRL) {
//! println!("You need to hold both Shift and Control to quit.");
//! } else {
//! println!("Now you're not even trying!");
//! }
//! }
//! _ => (),
//! }
//! }
//! }
//! ```
use crate::ggez::context::Context;
use std::collections::HashSet;
use winit::ModifiersState;
/// A key code.
pub use winit::VirtualKeyCode as KeyCode;
bitflags! {
/// Bitflags describing the state of keyboard modifiers, such as `Control` or `Shift`.
#[derive(Default)]
pub struct KeyMods: u8 {
/// No modifiers; equivalent to `KeyMods::default()` and
/// [`KeyMods::empty()`](struct.KeyMods.html#method.empty).
const NONE = 0b0000_0000;
/// Left or right Shift key.
const SHIFT = 0b0000_0001;
/// Left or right Control key.
const CTRL = 0b0000_0010;
/// Left or right Alt key.
const ALT = 0b0000_0100;
/// Left or right Win/Cmd/equivalent key.
const LOGO = 0b0000_1000;
}
}
impl From<ModifiersState> for KeyMods {
fn from(state: ModifiersState) -> Self {
let mut keymod = KeyMods::empty();
if state.shift {
keymod |= Self::SHIFT;
}
if state.ctrl {
keymod |= Self::CTRL;
}
if state.alt {
keymod |= Self::ALT;
}
if state.logo {
keymod |= Self::LOGO;
}
keymod
}
}
/// Tracks held down keyboard keys, active keyboard modifiers,
/// and figures out if the system is sending repeat keystrokes.
#[derive(Clone, Debug)]
pub struct KeyboardContext {
active_modifiers: KeyMods,
/// A simple mapping of which key code has been pressed.
/// We COULD use a `Vec<bool>` but turning Rust enums to and from
/// integers is unsafe and a set really is what we want anyway.
pressed_keys_set: HashSet<KeyCode>,
// These two are necessary for tracking key-repeat.
last_pressed: Option<KeyCode>,
current_pressed: Option<KeyCode>,
}
impl KeyboardContext {
pub(crate) fn new() -> Self {
Self {
active_modifiers: KeyMods::empty(),
// We just use 256 as a number Big Enough For Keyboard Keys to try to avoid resizing.
pressed_keys_set: HashSet::with_capacity(256),
last_pressed: None,
current_pressed: None,
}
}
pub(crate) fn set_key(&mut self, key: KeyCode, pressed: bool) {
if pressed {
let _ = self.pressed_keys_set.insert(key);
self.last_pressed = self.current_pressed;
self.current_pressed = Some(key);
} else {
let _ = self.pressed_keys_set.remove(&key);
self.current_pressed = None;
}
self.set_key_modifier(key, pressed);
}
/// Take a modifier key code and alter our state.
///
/// Double check that this edge handling is necessary;
/// winit sounds like it should do this for us,
/// see https://docs.rs/winit/0.18.0/winit/struct.KeyboardInput.html#structfield.modifiers
///
/// ...more specifically, we should refactor all this to consistant-ify events a bit and
/// make winit do more of the work.
/// But to quote Scott Pilgrim, "This is... this is... Booooooring."
fn set_key_modifier(&mut self, key: KeyCode, pressed: bool) {
if pressed {
match key {
KeyCode::LShift | KeyCode::RShift => self.active_modifiers |= KeyMods::SHIFT,
KeyCode::LControl | KeyCode::RControl => self.active_modifiers |= KeyMods::CTRL,
KeyCode::LAlt | KeyCode::RAlt => self.active_modifiers |= KeyMods::ALT,
KeyCode::LWin | KeyCode::RWin => self.active_modifiers |= KeyMods::LOGO,
_ => (),
}
} else {
match key {
KeyCode::LShift | KeyCode::RShift => self.active_modifiers -= KeyMods::SHIFT,
KeyCode::LControl | KeyCode::RControl => self.active_modifiers -= KeyMods::CTRL,
KeyCode::LAlt | KeyCode::RAlt => self.active_modifiers -= KeyMods::ALT,
KeyCode::LWin | KeyCode::RWin => self.active_modifiers -= KeyMods::LOGO,
_ => (),
}
}
}
pub(crate) fn set_modifiers(&mut self, keymods: KeyMods) {
self.active_modifiers = keymods;
}
pub(crate) fn is_key_pressed(&self, key: KeyCode) -> bool {
self.pressed_keys_set.contains(&key)
}
pub(crate) fn is_key_repeated(&self) -> bool {
if self.last_pressed.is_some() {
self.last_pressed == self.current_pressed
} else {
false
}
}
pub(crate) fn pressed_keys(&self) -> &HashSet<KeyCode> {
&self.pressed_keys_set
}
pub(crate) fn active_mods(&self) -> KeyMods {
self.active_modifiers
}
}
impl Default for KeyboardContext {
fn default() -> Self {
Self::new()
}
}
/// Checks if a key is currently pressed down.
pub fn is_key_pressed(ctx: &Context, key: KeyCode) -> bool {
ctx.keyboard_context.is_key_pressed(key)
}
/// Checks if the last keystroke sent by the system is repeated,
/// like when a key is held down for a period of time.
pub fn is_key_repeated(ctx: &Context) -> bool {
ctx.keyboard_context.is_key_repeated()
}
/// Returns a reference to the set of currently pressed keys.
pub fn pressed_keys(ctx: &Context) -> &HashSet<KeyCode> {
ctx.keyboard_context.pressed_keys()
}
/// Checks if keyboard modifier (or several) is active.
pub fn is_mod_active(ctx: &Context, keymods: KeyMods) -> bool {
ctx.keyboard_context.active_mods().contains(keymods)
}
/// Returns currently active keyboard modifiers.
pub fn active_mods(ctx: &Context) -> KeyMods {
ctx.keyboard_context.active_mods()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn key_mod_conversions() {
assert_eq!(
KeyMods::empty(),
KeyMods::from(ModifiersState {
shift: false,
ctrl: false,
alt: false,
logo: false,
})
);
assert_eq!(
KeyMods::SHIFT,
KeyMods::from(ModifiersState {
shift: true,
ctrl: false,
alt: false,
logo: false,
})
);
assert_eq!(
KeyMods::SHIFT | KeyMods::ALT,
KeyMods::from(ModifiersState {
shift: true,
ctrl: false,
alt: true,
logo: false,
})
);
assert_eq!(
KeyMods::SHIFT | KeyMods::ALT | KeyMods::CTRL,
KeyMods::from(ModifiersState {
shift: true,
ctrl: true,
alt: true,
logo: false,
})
);
assert_eq!(
KeyMods::SHIFT - KeyMods::ALT,
KeyMods::from(ModifiersState {
shift: true,
ctrl: false,
alt: false,
logo: false,
})
);
assert_eq!(
(KeyMods::SHIFT | KeyMods::ALT) - KeyMods::ALT,
KeyMods::from(ModifiersState {
shift: true,
ctrl: false,
alt: false,
logo: false,
})
);
assert_eq!(
KeyMods::SHIFT - (KeyMods::ALT | KeyMods::SHIFT),
KeyMods::from(ModifiersState {
shift: false,
ctrl: false,
alt: false,
logo: false,
})
);
}
#[test]
fn pressed_keys_tracking() {
let mut keyboard = KeyboardContext::new();
assert_eq!(keyboard.pressed_keys(), &[].iter().cloned().collect());
assert!(!keyboard.is_key_pressed(KeyCode::A));
keyboard.set_key(KeyCode::A, true);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::A].iter().cloned().collect()
);
assert!(keyboard.is_key_pressed(KeyCode::A));
keyboard.set_key(KeyCode::A, false);
assert_eq!(keyboard.pressed_keys(), &[].iter().cloned().collect());
assert!(!keyboard.is_key_pressed(KeyCode::A));
keyboard.set_key(KeyCode::A, true);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::A].iter().cloned().collect()
);
assert!(keyboard.is_key_pressed(KeyCode::A));
keyboard.set_key(KeyCode::A, true);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::A].iter().cloned().collect()
);
keyboard.set_key(KeyCode::B, true);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::A, KeyCode::B].iter().cloned().collect()
);
keyboard.set_key(KeyCode::B, true);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::A, KeyCode::B].iter().cloned().collect()
);
keyboard.set_key(KeyCode::A, false);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::B].iter().cloned().collect()
);
keyboard.set_key(KeyCode::A, false);
assert_eq!(
keyboard.pressed_keys(),
&[KeyCode::B].iter().cloned().collect()
);
keyboard.set_key(KeyCode::B, false);
assert_eq!(keyboard.pressed_keys(), &[].iter().cloned().collect());
}
#[test]
fn keyboard_modifiers() {
let mut keyboard = KeyboardContext::new();
// this test is mostly useless and is primarily for code coverage
assert_eq!(keyboard.active_mods(), KeyMods::default());
keyboard.set_modifiers(KeyMods::from(ModifiersState {
shift: true,
ctrl: true,
alt: true,
logo: true,
}));
// these test the workaround for https://github.com/tomaka/winit/issues/600
assert_eq!(
keyboard.active_mods(),
KeyMods::SHIFT | KeyMods::CTRL | KeyMods::ALT | KeyMods::LOGO
);
keyboard.set_key(KeyCode::LControl, false);
assert_eq!(
keyboard.active_mods(),
KeyMods::SHIFT | KeyMods::ALT | KeyMods::LOGO
);
keyboard.set_key(KeyCode::RAlt, false);
assert_eq!(keyboard.active_mods(), KeyMods::SHIFT | KeyMods::LOGO);
keyboard.set_key(KeyCode::LWin, false);
assert_eq!(keyboard.active_mods(), KeyMods::SHIFT);
}
#[test]
fn repeated_keys_tracking() {
let mut keyboard = KeyboardContext::new();
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, false);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), true);
keyboard.set_key(KeyCode::A, false);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::B, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::A, true);
assert_eq!(keyboard.is_key_repeated(), true);
keyboard.set_key(KeyCode::B, true);
assert_eq!(keyboard.is_key_repeated(), false);
keyboard.set_key(KeyCode::B, true);
assert_eq!(keyboard.is_key_repeated(), true);
}
}

4
src/ggez/input/mod.rs Normal file
View File

@ -0,0 +1,4 @@
//! Input handling modules for keyboard, mouse and gamepad.
pub mod gamepad;
pub mod keyboard;
pub mod mouse;

124
src/ggez/input/mouse.rs Normal file
View File

@ -0,0 +1,124 @@
//! Mouse utility functions.
use crate::ggez::context::Context;
use crate::ggez::error::GameError;
use crate::ggez::error::GameResult;
use crate::ggez::graphics;
use crate::ggez::graphics::Point2;
use std::collections::HashMap;
use winit::dpi;
pub use winit::{MouseButton, MouseCursor};
/// Stores state information for the mouse.
#[derive(Clone, Debug)]
pub struct MouseContext {
last_position: Point2,
last_delta: Point2,
buttons_pressed: HashMap<MouseButton, bool>,
cursor_type: MouseCursor,
cursor_grabbed: bool,
cursor_hidden: bool,
}
impl MouseContext {
pub(crate) fn new() -> Self {
Self {
last_position: Point2::origin(),
last_delta: Point2::origin(),
cursor_type: MouseCursor::Default,
buttons_pressed: HashMap::new(),
cursor_grabbed: false,
cursor_hidden: false,
}
}
pub(crate) fn set_last_position(&mut self, p: Point2) {
self.last_position = p;
}
pub(crate) fn set_last_delta(&mut self, p: Point2) {
self.last_delta = p;
}
pub(crate) fn set_button(&mut self, button: MouseButton, pressed: bool) {
let _ = self.buttons_pressed.insert(button, pressed);
}
fn button_pressed(&self, button: MouseButton) -> bool {
*(self.buttons_pressed.get(&button).unwrap_or(&false))
}
}
impl Default for MouseContext {
fn default() -> Self {
Self::new()
}
}
/// Returns the current mouse cursor type of the window.
pub fn cursor_type(ctx: &Context) -> MouseCursor {
ctx.mouse_context.cursor_type
}
/// Modifies the mouse cursor type of the window.
pub fn set_cursor_type(ctx: &mut Context, cursor_type: MouseCursor) {
ctx.mouse_context.cursor_type = cursor_type;
graphics::window(ctx).set_cursor(cursor_type);
}
/// Get whether or not the mouse is grabbed (confined to the window)
pub fn cursor_grabbed(ctx: &Context) -> bool {
ctx.mouse_context.cursor_grabbed
}
/// Set whether or not the mouse is grabbed (confined to the window)
pub fn set_cursor_grabbed(ctx: &mut Context, grabbed: bool) -> GameResult<()> {
ctx.mouse_context.cursor_grabbed = grabbed;
graphics::window(ctx)
.grab_cursor(grabbed)
.map_err(|e| GameError::WindowError(e.to_string()))
}
/// Set whether or not the mouse is hidden (invisible)
pub fn cursor_hidden(ctx: &Context) -> bool {
ctx.mouse_context.cursor_hidden
}
/// Set whether or not the mouse is hidden (invisible).
pub fn set_cursor_hidden(ctx: &mut Context, hidden: bool) {
ctx.mouse_context.cursor_hidden = hidden;
graphics::window(ctx).hide_cursor(hidden)
}
/// Get the current position of the mouse cursor, in pixels.
/// Complement to [`set_position()`](fn.set_position.html).
/// Uses strictly window-only coordinates.
pub fn position(ctx: &Context) -> mint::Point2<f32> {
ctx.mouse_context.last_position.into()
}
/// Set the current position of the mouse cursor, in pixels.
/// Uses strictly window-only coordinates.
pub fn set_position<P>(ctx: &mut Context, point: P) -> GameResult<()>
where
P: Into<mint::Point2<f32>>,
{
let mintpoint = point.into();
ctx.mouse_context.last_position = Point2::from(mintpoint);
graphics::window(ctx)
.set_cursor_position(dpi::LogicalPosition {
x: f64::from(mintpoint.x),
y: f64::from(mintpoint.y),
})
.map_err(|_| GameError::WindowError("Couldn't set mouse cursor position!".to_owned()))
}
/// Get the distance the cursor was moved during last frame, in pixels.
pub fn delta(ctx: &Context) -> mint::Point2<f32> {
ctx.mouse_context.last_delta.into()
}
/// Returns whether or not the given mouse button is pressed.
pub fn button_pressed(ctx: &Context, button: MouseButton) -> bool {
ctx.mouse_context.button_pressed(button)
}

108
src/ggez/mod.rs Normal file
View File

@ -0,0 +1,108 @@
//! # What is this?
//!
//! ggez is a Rust library to create a Good Game Easily.
//!
//! More specifically, ggez is a lightweight game framework for making
//! 2D games with minimum friction. It aims to implement an API based
//! on (a Rustified version of) the [LÖVE](https://love2d.org/) game
//! framework. This means it contains basic and portable 2D
//! drawing, sound, resource loading and event handling.
//!
//! For a fuller outline, see the [README.md](https://github.com/ggez/ggez/)
//!
//! ## Usage
//!
//! ggez consists of three main parts: A [`Context`](struct.Context.html) object
//! which contains all the state required to interface with the computer's
//! hardware, an [`EventHandler`](event/trait.EventHandler.html) trait that the
//! user implements to register callbacks for events, and various sub-modules such as
//! [`graphics`](graphics/index.html) and [`audio`](audio/index.html) that provide
//! the functionality to actually get stuff done.
//!
//! The general pattern is to create a struct holding your game's data which implements
//! the `EventHandler` trait. Create a [`ContextBuilder`](struct.ContextBuilder.html)
//! object with configuration settings, use it to create a new `Context` object,
//! and then call [`event::run()`](event/fn.run.html) with the `Context` and an instance of
//! your `EventHandler` to run your game's main loop.
//!
//! ## Basic Project Template
//!
//! ```rust,no_run
//! use ggez::{Context, ContextBuilder, GameResult};
//! use ggez::event::{self, EventHandler};
//! use ggez::graphics;
//!
//! fn main() {
//! // Make a Context and an EventLoop.
//! let (mut ctx, mut event_loop) =
//! ContextBuilder::new("game_name", "author_name")
//! .build()
//! .unwrap();
//!
//! // Create an instance of your event handler.
//! // Usually, you should provide it with the Context object
//! // so it can load resources like images during setup.
//! let mut my_game = MyGame::new(&mut ctx);
//!
//! // Run!
//! match event::run(&mut ctx, &mut event_loop, &mut my_game) {
//! Ok(_) => println!("Exited cleanly."),
//! Err(e) => println!("Error occured: {}", e)
//! }
//! }
//!
//! struct MyGame {
//! // Your state here...
//! }
//!
//! impl MyGame {
//! pub fn new(_ctx: &mut Context) -> MyGame {
//! // Load/create resources here: images, fonts, sounds, etc.
//! MyGame { }
//! }
//! }
//!
//! impl EventHandler for MyGame {
//! fn update(&mut self, _ctx: &mut Context) -> GameResult<()> {
//! // Update code here...
//! # Ok(())
//! }
//!
//! fn draw(&mut self, ctx: &mut Context) -> GameResult<()> {
//! graphics::clear(ctx, graphics::WHITE);
//!
//! // Draw code here...
//!
//! graphics::present(ctx)
//! }
//! }
//!
//! ```
#![deny(missing_docs)]
#![deny(missing_debug_implementations)]
#![deny(unused_results)]
// This is not as strong a constraint as `#![forbid(unsafe_code)]` but is good enough.
// It means the only place we use unsafe is then in the modules noted as allowing it.
#![deny(unsafe_code)]
#![warn(bare_trait_objects)]
#![warn(missing_copy_implementations)]
pub extern crate mint;
pub extern crate nalgebra;
pub mod conf;
mod context;
pub mod error;
pub mod event;
pub mod filesystem;
pub mod graphics;
pub mod input;
pub mod timer;
mod vfs;
#[cfg(test)]
pub mod tests;
pub use crate::ggez::context::{Context, ContextBuilder};
pub use crate::ggez::error::*;

128
src/ggez/tests/audio.rs Normal file
View File

@ -0,0 +1,128 @@
use crate::audio::SoundSource;
use crate::tests;
use crate::*;
#[test]
fn audio_load_ogg() {
let (c, _e) = &mut tests::make_context();
// OGG format
let filename = "/pew.ogg";
let _sound = audio::Source::new(c, filename).unwrap();
let _sound = audio::SpatialSource::new(c, filename).unwrap();
}
#[test]
fn audio_load_wav() {
let (c, _e) = &mut tests::make_context();
// WAV format
let filename = "/pew.wav";
let _sound = audio::Source::new(c, filename).unwrap();
let _sound = audio::SpatialSource::new(c, filename).unwrap();
}
#[test]
fn audio_load_flac() {
let (c, _e) = &mut tests::make_context();
// FLAC format
let filename = "/pew.flac";
let _sound = audio::Source::new(c, filename).unwrap();
let _sound = audio::SpatialSource::new(c, filename).unwrap();
}
#[test]
fn fail_when_loading_nonexistent_file() {
let (c, _e) = &mut tests::make_context();
// File does not exist
let filename = "/does-not-exist.ogg";
assert!(audio::Source::new(c, filename).is_err());
assert!(audio::SpatialSource::new(c, filename).is_err());
}
#[test]
fn fail_when_loading_non_audio_file() {
let (c, _e) = &mut tests::make_context();
let filename = "/player.png";
assert!(audio::Source::new(c, filename).is_err());
assert!(audio::SpatialSource::new(c, filename).is_err());
}
#[test]
fn playing_returns_correct_value_based_on_state() {
let (c, _e) = &mut tests::make_context();
let mut sound = audio::Source::new(c, "/pew.ogg").unwrap();
assert!(!sound.playing());
sound.play().unwrap();
assert!(sound.playing());
sound.pause();
assert!(!sound.playing());
sound.resume();
assert!(sound.playing());
sound.stop();
assert!(!sound.playing());
}
#[test]
fn paused_returns_correct_value_based_on_state() {
let (c, _e) = &mut tests::make_context();
let mut sound = audio::Source::new(c, "/pew.ogg").unwrap();
assert!(!sound.paused());
sound.play().unwrap();
assert!(!sound.paused());
sound.pause();
assert!(sound.paused());
sound.resume();
assert!(!sound.paused());
sound.pause();
assert!(sound.paused());
sound.stop();
assert!(!sound.paused());
}
#[test]
fn volume_persists_after_stop() {
let (c, _e) = &mut tests::make_context();
let filename = "/pew.ogg";
test_volume_after_stop(audio::Source::new(c, filename).unwrap());
test_volume_after_stop(audio::SpatialSource::new(c, filename).unwrap());
fn test_volume_after_stop(mut sound: impl SoundSource) {
let volume = 0.8;
sound.set_volume(volume);
assert_eq!(sound.volume(), volume);
sound.stop();
assert_eq!(sound.volume(), volume);
}
}
#[test]
fn volume_persists_after_play() {
let (c, _e) = &mut tests::make_context();
let filename = "/pew.ogg";
test_volume(audio::Source::new(c, filename).unwrap());
test_volume(audio::SpatialSource::new(c, filename).unwrap());
fn test_volume(mut sound: impl SoundSource) {
let volume = 0.8;
assert_eq!(sound.volume(), 1.0);
sound.set_volume(volume);
assert_eq!(sound.volume(), volume);
sound.play().unwrap();
assert_eq!(sound.volume(), volume);
}
}

32
src/ggez/tests/conf.rs Normal file
View File

@ -0,0 +1,32 @@
use crate::*;
use std::env;
use std::path;
#[test]
#[ignore]
pub fn context_build_tests() {
let confs = vec![
conf::Conf::default().window_mode(conf::WindowMode::default().dimensions(800.0, 600.0)),
conf::Conf::default().window_mode(conf::WindowMode::default().dimensions(400.0, 400.0)),
conf::Conf::default().window_mode(conf::WindowMode::default().resizable(false)),
conf::Conf::default().window_mode(
conf::WindowMode::default().fullscreen_type(conf::FullscreenType::Windowed),
),
conf::Conf::default()
.window_mode(conf::WindowMode::default().fullscreen_type(conf::FullscreenType::True)),
conf::Conf::default().modules(conf::ModuleConf::default().audio(false)),
];
for conf in confs.into_iter() {
let mut cb = ContextBuilder::new("ggez_unit_tests", "ggez").conf(conf);
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let mut path = path::PathBuf::from(manifest_dir);
path.push("resources");
cb = cb.add_resource_path(path);
}
let (c, _e) = cb.clone().build().unwrap();
let (w, h) = graphics::drawable_size(&c);
assert_eq!(w, cb.conf.window_mode.width.into());
assert_eq!(h, cb.conf.window_mode.height.into());
// Can't really test whether or not the window is resizable?
}
}

View File

@ -0,0 +1,19 @@
use crate::tests;
use crate::*;
use std::io::Write;
#[test]
fn filesystem_create_correct_paths() {
let (c, _e) = &mut tests::make_context();
{
let mut f = filesystem::create(c, "/filesystem_create_path").unwrap();
let _ = f.write(b"foo").unwrap();
}
let userdata_path = filesystem::user_config_dir(c);
let userdata_path = &mut userdata_path.to_owned();
userdata_path.push("filesystem_create_path");
println!("Userdata path: {:?}", userdata_path);
assert!(userdata_path.is_file());
}

175
src/ggez/tests/graphics.rs Normal file
View File

@ -0,0 +1,175 @@
use crate::graphics::Color;
use crate::tests;
use crate::*;
use cgmath::Point2;
// use std::path;
#[test]
fn image_encode() {
let (c, _e) = &mut tests::make_context();
let image = graphics::Image::new(c, "/player.png").unwrap();
image
.encode(c, graphics::ImageFormat::Png, "/encode_test.png")
.unwrap();
}
fn get_rgba_sample(rgba_buf: &[u8], width: usize, sample_pos: Point2<f32>) -> (u8, u8, u8, u8) {
(
rgba_buf[(width * sample_pos.y as usize + sample_pos.x as usize) * 4 + 0],
rgba_buf[(width * sample_pos.y as usize + sample_pos.x as usize) * 4 + 1],
rgba_buf[(width * sample_pos.y as usize + sample_pos.x as usize) * 4 + 2],
rgba_buf[(width * sample_pos.y as usize + sample_pos.x as usize) * 4 + 3],
)
}
fn save_screenshot_test(c: &mut Context) {
graphics::clear(c, Color::new(0.1, 0.2, 0.3, 1.0));
let width = graphics::drawable_size(c).0;
let height = graphics::drawable_size(c).1;
let topleft = graphics::DrawParam::new()
.color(graphics::WHITE)
.dest(Point2::new(0.0, 0.0));
let topright = graphics::DrawParam::new()
.color(Color::new(1.0, 0.0, 0.0, 1.0))
.dest(Point2::new(width / 2.0, 0.0));
let bottomleft = graphics::DrawParam::new()
.color(Color::new(0.0, 1.0, 0.0, 1.0))
.dest(Point2::new(0.0, height / 2.0));
let bottomright = graphics::DrawParam::new()
.color(Color::new(0.0, 0.0, 1.0, 1.0))
.dest(Point2::new(width / 2.0, height / 2.0));
let rect = graphics::Mesh::new_rectangle(
c,
graphics::DrawMode::fill(),
graphics::types::Rect {
x: 0.0,
y: 0.0,
w: width / 2.0,
h: height / 2.0,
},
graphics::WHITE,
)
.unwrap();
graphics::draw(c, &rect, topleft).unwrap();
graphics::draw(c, &rect, topright).unwrap();
graphics::draw(c, &rect, bottomleft).unwrap();
graphics::draw(c, &rect, bottomright).unwrap();
// Don't do graphics::present(c) since calling it once (!) would mean that the result of our draw operation
// went to the front buffer and the active screen texture is actually empty.
c.gfx_context.encoder.flush(&mut *c.gfx_context.device);
let screenshot = graphics::screenshot(c).unwrap();
// Check if screenshot has right general properties
assert_eq!(width as u16, screenshot.width);
assert_eq!(height as u16, screenshot.height);
assert_eq!(None, screenshot.blend_mode);
// Image comparision or rendered output is hard, but we *know* that top left should be white.
// So take a samples in the middle of each rectangle we drew and compare.
// Note that we only use fully saturated colors to avoid any issues with color spaces.
let rgba_buf = screenshot.to_rgba8(c).unwrap();
let half_rect = cgmath::Vector2::new(width / 4.0, height / 4.0);
let width = width as usize;
assert_eq!(
topleft.color.to_rgba(),
get_rgba_sample(&rgba_buf, width, Point2::from(topleft.dest) + half_rect)
);
assert_eq!(
topright.color.to_rgba(),
get_rgba_sample(&rgba_buf, width, Point2::from(topright.dest) + half_rect)
);
assert_eq!(
bottomleft.color.to_rgba(),
get_rgba_sample(&rgba_buf, width, Point2::from(bottomleft.dest) + half_rect)
);
assert_eq!(
bottomright.color.to_rgba(),
get_rgba_sample(&rgba_buf, width, Point2::from(bottomright.dest) + half_rect)
);
// save screenshot (no check, just to see if it doesn't crash)
screenshot
.encode(c, graphics::ImageFormat::Png, "/screenshot_test.png")
.unwrap();
}
#[test]
fn save_screenshot() {
let (c, _e) = &mut tests::make_context();
save_screenshot_test(c);
}
// Not supported, see https://github.com/ggez/ggez/issues/751
// #[test]
// fn save_screenshot_with_antialiasing() {
// let cb = ContextBuilder::new("ggez_unit_tests", "ggez")
// .window_setup(conf::WindowSetup::default().samples(conf::NumSamples::Eight));
// let (c, _e) = &mut tests::make_context_from_contextbuilder(cb);
// save_screenshot_test(c);
// }
#[test]
fn load_images() {
let (c, _e) = &mut tests::make_context();
let image = graphics::Image::new(c, "/player.png").unwrap();
image
.encode(c, graphics::ImageFormat::Png, "/player_save_test.png")
.unwrap();
let _i2 = graphics::Image::new(c, "/player_save_test.png").unwrap();
}
#[test]
fn sanity_check_window_sizes() {
let (c, e) = &mut tests::make_context();
// Make sure that window sizes are what we ask for, and not what hidpi gives us.
let w = c.conf.window_mode.width;
let h = c.conf.window_mode.height;
let size = graphics::drawable_size(c);
assert_eq!(w, size.0);
assert_eq!(h, size.1);
let outer_size = graphics::size(c);
assert!(size.0 <= outer_size.0);
assert!(size.1 <= outer_size.1);
// Make sure resizing the window works.
let w = 100.0;
let h = 200.0;
graphics::set_drawable_size(c, w, h).unwrap();
// ahahaha this apparently REQUIRES a delay between setting
// the size and it actually altering, at least on Linux X11
std::thread::sleep(std::time::Duration::from_millis(100));
// Maybe we need to run the event pump too? It seems VERY flaky.
// Sometimes you need one, sometimes you need both...
e.poll_events(|event| {
c.process_event(&event);
});
let size = graphics::drawable_size(c);
assert_eq!(w, size.0);
assert_eq!(h, size.1);
}
/// Ensure that the transform stack applies operations in the correct order.
#[test]
fn test_transform_stack_order() {
let (ctx, _e) = &mut tests::make_context();
let p1 = graphics::DrawParam::default();
let p2 = graphics::DrawParam::default();
let t1 = p1.to_matrix();
let t2 = p2.to_matrix();
graphics::push_transform(ctx, Some(t1));
graphics::mul_transform(ctx, t2);
let res = crate::nalgebra::Matrix4::<f32>::from(graphics::transform(ctx));
let m1: crate::nalgebra::Matrix4<f32> = t1.into();
let m2: crate::nalgebra::Matrix4<f32> = t2.into();
assert_eq!(res, m2 * m1);
}

78
src/ggez/tests/mesh.rs Normal file
View File

@ -0,0 +1,78 @@
use crate::tests;
use crate::*;
const TRIANGLE_VERTS: &[graphics::Vertex] = &[
graphics::Vertex {
pos: [0.0, 0.0],
uv: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
},
graphics::Vertex {
pos: [0.0, 0.0],
uv: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
},
graphics::Vertex {
pos: [0.0, 0.0],
uv: [0.0, 0.0],
color: [1.0, 1.0, 1.0, 1.0],
},
];
/// Mesh creation fails if verts or indices are empty.
#[test]
fn test_mesh_verts_empty() {
let (mut ctx, _ev) = tests::make_context();
let bad_verts: Vec<graphics::Vertex> = vec![];
let bad_indices = vec![];
let good_indices = vec![0, 1, 2];
let m = graphics::Mesh::from_raw(&mut ctx, &bad_verts, &bad_indices, None);
assert!(m.is_err());
let m = graphics::Mesh::from_raw(&mut ctx, &bad_verts, &good_indices, None);
assert!(m.is_err());
let m = graphics::Mesh::from_raw(&mut ctx, TRIANGLE_VERTS, &bad_indices, None);
assert!(m.is_err());
let m = graphics::Mesh::from_raw(&mut ctx, TRIANGLE_VERTS, &good_indices, None);
assert!(m.is_ok());
}
/// Mesh creation fails if not enough indices to make a triangle.
#[test]
fn test_mesh_verts_invalid_count() {
let (mut ctx, _ev) = tests::make_context();
let indices: Vec<u32> = vec![0, 1];
let m = graphics::Mesh::from_raw(&mut ctx, TRIANGLE_VERTS, &indices, None);
assert!(m.is_err());
let indices: Vec<u32> = vec![0, 1, 2, 0];
let m = graphics::Mesh::from_raw(&mut ctx, TRIANGLE_VERTS, &indices, None);
assert!(m.is_err());
}
#[test]
fn test_mesh_points_clockwise() {
let (mut ctx, _ev) = tests::make_context();
// Points in CCW order
let points: Vec<graphics::Point2> = vec![
graphics::Point2::new(0.0, 0.0),
graphics::Point2::new(0.0, 1.0),
graphics::Point2::new(1.0, 1.0),
];
let _trapezoid_mesh = graphics::Mesh::new_polygon(
&mut ctx,
graphics::DrawMode::fill(),
&points,
[0.0, 0.0, 1.0, 1.0].into(),
)
.unwrap();
// TODO LATER: This is actually tricky to test for well...
// We don't actually check for CCW points in
// the `Mesh` building functions yet, so this will never fail.
//assert!(trapezoid_mesh.is_err());
}

27
src/ggez/tests/mod.rs Normal file
View File

@ -0,0 +1,27 @@
//! Utility functions shared among various unit tests.
use crate::*;
use std::env;
use std::path;
mod audio;
mod conf;
mod filesystem;
mod graphics;
mod mesh;
mod text;
pub fn make_context_from_contextbuilder(mut cb: ContextBuilder) -> (Context, event::EventsLoop) {
if let Ok(manifest_dir) = env::var("CARGO_MANIFEST_DIR") {
let mut path = path::PathBuf::from(manifest_dir);
path.push("resources");
cb = cb.add_resource_path(path);
}
cb.build().unwrap()
}
/// Make a basic `Context` with sane defaults.
pub fn make_context() -> (Context, event::EventsLoop) {
let cb = ContextBuilder::new("ggez_unit_tests", "ggez");
make_context_from_contextbuilder(cb)
}

56
src/ggez/tests/text.rs Normal file
View File

@ -0,0 +1,56 @@
// #[cfg(all(test, has_display))]
use crate::tests;
use crate::*;
#[test]
fn test_calculated_text_width() {
let (ctx, _ev) = &mut tests::make_context();
let font = graphics::Font::default();
let text = graphics::Text::new(("Hello There", font, 24.0));
let expected_width = text.width(ctx);
// For now we just test against a known value, since rendering it
// is odd.
assert_eq!(expected_width, 123);
// let rendered_width = graphics::Text::new((text, font, 24)).unwrap().width();
// println!("Text: {:?}, expected: {}, rendered: {}", text, expected_width, rendered_width);
// assert_eq!(expected_width as usize, rendered_width as usize);
}
/// Make sure that the "height" of text with ascenders/descenders
/// is the same as text without
#[test]
fn test_calculated_text_height() {
let (ctx, _ev) = &mut tests::make_context();
let font = graphics::Font::default();
let text1 = graphics::Text::new(("strength", font, 24.0));
let text2 = graphics::Text::new(("moves", font, 24.0));
let h1 = text1.height(ctx);
let h2 = text2.height(ctx);
assert_eq!(h1, h2);
}
#[test]
fn test_monospace_text_is_actually_monospace() {
let (ctx, _ev) = &mut tests::make_context();
let font = graphics::Font::new(ctx, "/DejaVuSansMono.ttf").unwrap();
let text1 = graphics::Text::new(("Hello 1", font, 24.0));
let text2 = graphics::Text::new(("Hello 2", font, 24.0));
let text3 = graphics::Text::new(("Hello 3", font, 24.0));
let text4 = graphics::Text::new(("Hello 4", font, 24.0));
let width1 = text1.width(ctx);
let width2 = text3.width(ctx);
let width3 = text2.width(ctx);
let width4 = text4.width(ctx);
assert_eq!(width1, width2);
assert_eq!(width2, width3);
assert_eq!(width3, width4);
}

286
src/ggez/timer.rs Normal file
View File

@ -0,0 +1,286 @@
//! Timing and measurement functions.
//!
//! ggez does not try to do any framerate limitation by default. If
//! you want to run at anything other than full-bore max speed all the
//! time, call [`thread::yield_now()`](https://doc.rust-lang.org/std/thread/fn.yield_now.html)
//! (or [`timer::yield_now()`](fn.yield_now.html) which does the same
//! thing) to yield to the OS so it has a chance to breathe before continuing
//! with your game. This should prevent it from using 100% CPU as much unless it
//! really needs to. Enabling vsync by setting
//! [`conf.window_setup.vsync`](../conf/struct.WindowSetup.html#structfield.vsync)
//! in your [`Conf`](../conf/struct.Conf.html) object is generally the best
//! way to cap your displayed framerate.
//!
//! For a more detailed tutorial in how to handle frame timings in games,
//! see <http://gafferongames.com/game-physics/fix-your-timestep/>
use crate::ggez::context::Context;
use std::cmp;
use std::f64;
use std::thread;
use std::time;
/// A simple buffer that fills
/// up to a limit and then holds the last
/// N items that have been inserted into it,
/// overwriting old ones in a round-robin fashion.
///
/// It's not quite a ring buffer 'cause you can't
/// remove items from it, it just holds the last N
/// things.
#[derive(Debug, Clone)]
struct LogBuffer<T>
where
T: Clone,
{
head: usize,
size: usize,
/// The number of actual samples inserted, used for
/// smarter averaging.
samples: usize,
contents: Vec<T>,
}
impl<T> LogBuffer<T>
where
T: Clone + Copy,
{
fn new(size: usize, init_val: T) -> LogBuffer<T> {
LogBuffer {
head: 0,
size,
contents: vec![init_val; size],
// Never divide by 0
samples: 1,
}
}
/// Pushes a new item into the `LogBuffer`, overwriting
/// the oldest item in it.
fn push(&mut self, item: T) {
self.head = (self.head + 1) % self.contents.len();
self.contents[self.head] = item;
self.size = cmp::min(self.size + 1, self.contents.len());
self.samples += 1;
}
/// Returns a slice pointing at the contents of the buffer.
/// They are in *no particular order*, and if not all the
/// slots are filled, the empty slots will be present but
/// contain the initial value given to [`new()`](#method.new).
///
/// We're only using this to log FPS for a short time,
/// so we don't care for the second or so when it's inaccurate.
fn contents(&self) -> &[T] {
if self.samples > self.size {
&self.contents
} else {
&self.contents[..self.samples]
}
}
/// Returns the most recent value in the buffer.
fn latest(&self) -> T {
self.contents[self.head]
}
}
/// A structure that contains our time-tracking state.
#[derive(Debug)]
pub struct TimeContext {
init_instant: time::Instant,
last_instant: time::Instant,
frame_durations: LogBuffer<time::Duration>,
residual_update_dt: time::Duration,
frame_count: usize,
}
// How many frames we log update times for.
const TIME_LOG_FRAMES: usize = 200;
impl TimeContext {
/// Creates a new `TimeContext` and initializes the start to this instant.
pub fn new() -> TimeContext {
let initial_dt = time::Duration::from_millis(16);
TimeContext {
init_instant: time::Instant::now(),
last_instant: time::Instant::now(),
frame_durations: LogBuffer::new(TIME_LOG_FRAMES, initial_dt),
residual_update_dt: time::Duration::from_secs(0),
frame_count: 0,
}
}
/// Update the state of the `TimeContext` to record that
/// another frame has taken place. Necessary for the FPS
/// tracking and [`check_update_time()`](fn.check_update_time.html)
/// functions to work.
///
/// It's usually not necessary to call this function yourself,
/// [`event::run()`](../event/fn.run.html) will do it for you.
pub fn tick(&mut self) {
let now = time::Instant::now();
let time_since_last = now - self.last_instant;
self.frame_durations.push(time_since_last);
self.last_instant = now;
self.frame_count += 1;
self.residual_update_dt += time_since_last;
}
}
impl Default for TimeContext {
fn default() -> Self {
Self::new()
}
}
/// Get the time between the start of the last frame and the current one;
/// in other words, the length of the last frame.
pub fn delta(ctx: &Context) -> time::Duration {
let tc = &ctx.timer_context;
tc.frame_durations.latest()
}
/// Gets the average time of a frame, averaged
/// over the last 200 frames.
pub fn average_delta(ctx: &Context) -> time::Duration {
let tc = &ctx.timer_context;
let sum: time::Duration = tc.frame_durations.contents().iter().sum();
// If our buffer is actually full, divide by its size.
// Otherwise divide by the number of samples we've added
if tc.frame_durations.samples > tc.frame_durations.size {
sum / (tc.frame_durations.size as u32)
} else {
sum / (tc.frame_durations.samples as u32)
}
}
/// A convenience function to convert a Rust `Duration` type
/// to a (less precise but more useful) `f64`.
///
/// Does not make sure that the `Duration` is within the bounds
/// of the `f64`.
pub fn duration_to_f64(d: time::Duration) -> f64 {
let seconds = d.as_secs() as f64;
let nanos = f64::from(d.subsec_nanos());
seconds + (nanos * 1e-9)
}
/// A convenience function to create a Rust `Duration` type
/// from a (less precise but more useful) `f64`.
///
/// Only handles positive numbers correctly.
pub fn f64_to_duration(t: f64) -> time::Duration {
debug_assert!(t > 0.0, "f64_to_duration passed a negative number!");
let seconds = t.trunc();
let nanos = t.fract() * 1e9;
time::Duration::new(seconds as u64, nanos as u32)
}
/// Returns a `Duration` representing how long each
/// frame should be to match the given fps.
///
/// Approximately.
fn fps_as_duration(fps: u32) -> time::Duration {
let target_dt_seconds = 1.0 / f64::from(fps);
f64_to_duration(target_dt_seconds)
}
/// Gets the FPS of the game, averaged over the last
/// 200 frames.
pub fn fps(ctx: &Context) -> f64 {
let duration_per_frame = average_delta(ctx);
let seconds_per_frame = duration_to_f64(duration_per_frame);
1.0 / seconds_per_frame
}
/// Returns the time since the game was initialized,
/// as reported by the system clock.
pub fn time_since_start(ctx: &Context) -> time::Duration {
let tc = &ctx.timer_context;
time::Instant::now() - tc.init_instant
}
/// Check whether or not the desired amount of time has elapsed
/// since the last frame.
///
/// This function will return true if the time since the last
/// [`update()`](../event/trait.EventHandler.html#tymethod.update)
/// call has been equal to or greater to the update FPS indicated by
/// the `target_fps`. It keeps track of fractional frames, so if you
/// want 60 fps (16.67 ms/frame) and the game stutters so that there
/// is 40 ms between `update()` calls, this will return `true` twice
/// in a row even in the same frame, then taking into account the
/// residual 6.67 ms to catch up to the next frame before returning
/// `true` again.
///
/// The intention is to for it to be called in a while loop
/// in your `update()` callback:
///
/// ```rust
/// # use ggez::*;
/// # fn update_game_physics() -> GameResult { Ok(()) }
/// # struct State;
/// # impl ggez::event::EventHandler for State {
/// fn update(&mut self, ctx: &mut Context) -> GameResult {
/// while(timer::check_update_time(ctx, 60)) {
/// update_game_physics()?;
/// }
/// Ok(())
/// }
/// # fn draw(&mut self, _ctx: &mut Context) -> GameResult { Ok(()) }
/// # }
/// ```
pub fn check_update_time(ctx: &mut Context, target_fps: u32) -> bool {
let timedata = &mut ctx.timer_context;
let target_dt = fps_as_duration(target_fps);
if timedata.residual_update_dt > target_dt {
timedata.residual_update_dt -= target_dt;
true
} else {
false
}
}
/// Returns the fractional amount of a frame not consumed
/// by [`check_update_time()`](fn.check_update_time.html).
/// For example, if the desired
/// update frame time is 40 ms (25 fps), and 45 ms have
/// passed since the last frame, [`check_update_time()`](fn.check_update_time.html)
/// will return `true` and `remaining_update_time()` will
/// return 5 ms -- the amount of time "overflowing" from one
/// frame to the next.
///
/// The intention is for it to be called in your
/// [`draw()`](../event/trait.EventHandler.html#tymethod.draw) callback
/// to interpolate physics states for smooth rendering.
/// (see <http://gafferongames.com/game-physics/fix-your-timestep/>)
pub fn remaining_update_time(ctx: &mut Context) -> time::Duration {
ctx.timer_context.residual_update_dt
}
/// Pauses the current thread for the target duration.
/// Just calls [`std::thread::sleep()`](https://doc.rust-lang.org/std/thread/fn.sleep.html)
/// so it's as accurate as that is (which is usually not very).
pub fn sleep(duration: time::Duration) {
thread::sleep(duration);
}
/// Yields the current timeslice to the OS.
///
/// This just calls [`std::thread::yield_now()`](https://doc.rust-lang.org/std/thread/fn.yield_now.html)
/// but it's handy to have here.
pub fn yield_now() {
thread::yield_now();
}
/// Gets the number of times the game has gone through its event loop.
///
/// Specifically, the number of times that [`TimeContext::tick()`](struct.TimeContext.html#method.tick)
/// has been called by it.
pub fn ticks(ctx: &Context) -> usize {
ctx.timer_context.frame_count
}

671
src/ggez/vfs.rs Normal file
View File

@ -0,0 +1,671 @@
//! A virtual file system layer that lets us define multiple
//! "file systems" with various backing stores, then merge them
//! together.
//!
//! Basically a re-implementation of the C library `PhysFS`. The
//! `vfs` crate does something similar but has a couple design
//! decisions that make it kind of incompatible with this use case:
//! the relevant trait for it has generic methods so we can't use it
//! as a trait object, and its path abstraction is not the most
//! convenient.
use std::collections::VecDeque;
use std::fmt::{self, Debug};
use std::fs;
use std::io::{Read, Seek, Write};
use std::path::{self, Path, PathBuf};
use crate::ggez::error::{GameError, GameResult};
fn convenient_path_to_str(path: &path::Path) -> GameResult<&str> {
path.to_str().ok_or_else(|| {
let errmessage = format!("Invalid path format for resource: {:?}", path);
GameError::FilesystemError(errmessage)
})
}
pub trait VFile: Read + Write + Seek + Debug {}
impl<T> VFile for T where T: Read + Write + Seek + Debug {}
/// Options for opening files
///
/// We need our own version of this structure because the one in
/// `std` annoyingly doesn't let you read the read/write/create/etc
/// state out of it.
#[must_use]
#[derive(Debug, Default, Copy, Clone, PartialEq)]
pub struct OpenOptions {
read: bool,
write: bool,
create: bool,
append: bool,
truncate: bool,
}
impl OpenOptions {
/// Create a new instance
pub fn new() -> OpenOptions {
Default::default()
}
/// Open for reading
pub fn read(mut self, read: bool) -> OpenOptions {
self.read = read;
self
}
/// Open for writing
pub fn write(mut self, write: bool) -> OpenOptions {
self.write = write;
self
}
/// Create the file if it does not exist yet
pub fn create(mut self, create: bool) -> OpenOptions {
self.create = create;
self
}
/// Append at the end of the file
pub fn append(mut self, append: bool) -> OpenOptions {
self.append = append;
self
}
/// Truncate the file to 0 bytes after opening
pub fn truncate(mut self, truncate: bool) -> OpenOptions {
self.truncate = truncate;
self
}
fn to_fs_openoptions(self) -> fs::OpenOptions {
let mut opt = fs::OpenOptions::new();
let _ = opt
.read(self.read)
.write(self.write)
.create(self.create)
.append(self.append)
.truncate(self.truncate)
.create(self.create);
opt
}
}
pub trait VFS: Debug {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>>;
/// Open the file at this path for reading
fn open(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(path, OpenOptions::new().read(true))
}
/// Open the file at this path for writing, truncating it if it exists already
fn create(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(
path,
OpenOptions::new().write(true).create(true).truncate(true),
)
}
/// Open the file at this path for appending, creating it if necessary
fn append(&self, path: &Path) -> GameResult<Box<dyn VFile>> {
self.open_options(
path,
OpenOptions::new().write(true).create(true).append(true),
)
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult;
/// Remove a file or an empty directory.
fn rm(&self, path: &Path) -> GameResult;
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult;
/// Check if the file exists
fn exists(&self, path: &Path) -> bool;
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>>;
/// Retrieve all file and directory entries in the given directory.
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>>;
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf>;
}
pub trait VMetadata {
/// Returns whether or not it is a directory.
/// Note that zip files don't actually have directories, awkwardly,
/// just files with very long names.
fn is_dir(&self) -> bool;
/// Returns whether or not it is a file.
fn is_file(&self) -> bool;
/// Returns the length of the thing. If it is a directory,
/// the result of this is undefined/platform dependent.
fn len(&self) -> u64;
}
/// A VFS that points to a directory and uses it as the root of its
/// file hierarchy.
///
/// It IS allowed to have symlinks in it! They're surprisingly
/// difficult to get rid of.
#[derive(Clone)]
pub struct PhysicalFS {
root: PathBuf,
readonly: bool,
}
#[derive(Debug, Clone)]
pub struct PhysicalMetadata(fs::Metadata);
impl VMetadata for PhysicalMetadata {
fn is_dir(&self) -> bool {
self.0.is_dir()
}
fn is_file(&self) -> bool {
self.0.is_file()
}
fn len(&self) -> u64 {
self.0.len()
}
}
/// This takes an absolute path and returns either a sanitized relative
/// version of it, or None if there's something bad in it.
///
/// What we want is an absolute path with no `..`'s in it, so, something
/// like "/foo" or "/foo/bar.txt". This means a path with components
/// starting with a `RootDir`, and zero or more `Normal` components.
///
/// We gotta return a new path because there's apparently no real good way
/// to turn an absolute path into a relative path with the same
/// components (other than the first), and pushing an absolute `Path`
/// onto a `PathBuf` just completely nukes its existing contents.
fn sanitize_path(path: &path::Path) -> Option<PathBuf> {
let mut c = path.components();
match c.next() {
Some(path::Component::RootDir) => (),
_ => return None,
}
fn is_normal_component(comp: path::Component) -> Option<&str> {
match comp {
path::Component::Normal(s) => s.to_str(),
_ => None,
}
}
// This could be done more cleverly but meh
let mut accm = PathBuf::new();
for component in c {
if let Some(s) = is_normal_component(component) {
accm.push(s)
} else {
return None;
}
}
Some(accm)
}
impl PhysicalFS {
pub fn new(root: &Path, readonly: bool) -> Self {
PhysicalFS {
root: root.into(),
readonly,
}
}
/// Takes a given path (&str) and returns
/// a new PathBuf containing the canonical
/// absolute path you get when appending it
/// to this filesystem's root.
fn to_absolute(&self, p: &Path) -> GameResult<PathBuf> {
if let Some(safe_path) = sanitize_path(p) {
let mut root_path = self.root.clone();
root_path.push(safe_path);
Ok(root_path)
} else {
let msg = format!(
"Path {:?} is not valid: must be an absolute path with no \
references to parent directories",
p
);
Err(GameError::FilesystemError(msg))
}
}
/// Creates the PhysicalFS's root directory if necessary.
/// Idempotent.
/// This way we can not create the directory until it's
/// actually used, though it IS a tiny bit of a performance
/// malus.
fn create_root(&self) -> GameResult {
if !self.root.exists() {
fs::create_dir_all(&self.root).map_err(GameError::from)
} else {
Ok(())
}
}
}
impl Debug for PhysicalFS {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<PhysicalFS root: {}>", self.root.display())
}
}
impl VFS for PhysicalFS {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
if self.readonly
&& (open_options.write
|| open_options.create
|| open_options.append
|| open_options.truncate)
{
let msg = format!(
"Cannot alter file {:?} in root {:?}, filesystem read-only",
path, self
);
return Err(GameError::FilesystemError(msg));
}
self.create_root()?;
let p = self.to_absolute(path)?;
open_options
.to_fs_openoptions()
.open(p)
.map(|x| Box::new(x) as Box<dyn VFile>)
.map_err(GameError::from)
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to make directory {} but FS is \
read-only"
.to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
//println!("Creating {:?}", p);
fs::DirBuilder::new()
.recursive(true)
.create(p)
.map_err(GameError::from)
}
/// Remove a file
fn rm(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to remove file {} but FS is read-only".to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
if p.is_dir() {
fs::remove_dir(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult {
if self.readonly {
return Err(GameError::FilesystemError(
"Tried to remove file/dir {} but FS is \
read-only"
.to_string(),
));
}
self.create_root()?;
let p = self.to_absolute(path)?;
if p.is_dir() {
fs::remove_dir_all(p).map_err(GameError::from)
} else {
fs::remove_file(p).map_err(GameError::from)
}
}
/// Check if the file exists
fn exists(&self, path: &Path) -> bool {
match self.to_absolute(path) {
Ok(p) => p.exists(),
_ => false,
}
}
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>> {
self.create_root()?;
let p = self.to_absolute(path)?;
p.metadata()
.map(|m| Box::new(PhysicalMetadata(m)) as Box<dyn VMetadata>)
.map_err(GameError::from)
}
/// Retrieve the path entries in this path
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
self.create_root()?;
let p = self.to_absolute(path)?;
// This is inconvenient because path() returns the full absolute
// path of the bloody file, which is NOT what we want!
// But if we use file_name() to just get the name then it is ALSO not what we want!
// what we WANT is the full absolute file path, *relative to the resources dir*.
// So that we can do read_dir("/foobar/"), and for each file, open it and query
// it and such by name.
// So we build the paths ourself.
let direntry_to_path = |entry: &fs::DirEntry| -> GameResult<PathBuf> {
let fname = entry
.file_name()
.into_string()
.expect("Non-unicode char in file path? Should never happen, I hope!");
let mut pathbuf = PathBuf::from(path);
pathbuf.push(fname);
Ok(pathbuf)
};
let itr = fs::read_dir(p)?
.map(|entry| direntry_to_path(&entry?))
.collect::<Vec<_>>()
.into_iter();
Ok(Box::new(itr))
}
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf> {
Some(self.root.clone())
}
}
/// A structure that joins several VFS's together in order.
#[derive(Debug)]
pub struct OverlayFS {
roots: VecDeque<Box<dyn VFS>>,
}
impl OverlayFS {
pub fn new() -> Self {
Self {
roots: VecDeque::new(),
}
}
/// Adds a new VFS to the front of the list.
/// Currently unused, I suppose, but good to
/// have at least for tests.
#[allow(dead_code)]
pub fn push_front(&mut self, fs: Box<dyn VFS>) {
self.roots.push_front(fs);
}
/// Adds a new VFS to the end of the list.
pub fn push_back(&mut self, fs: Box<dyn VFS>) {
self.roots.push_back(fs);
}
pub fn roots(&self) -> &VecDeque<Box<dyn VFS>> {
&self.roots
}
}
impl VFS for OverlayFS {
/// Open the file at this path with the given options
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
let mut tried: Vec<(PathBuf, GameError)> = vec![];
for vfs in &self.roots {
match vfs.open_options(path, open_options) {
Err(e) => {
if let Some(vfs_path) = vfs.to_path_buf() {
tried.push((vfs_path, e));
} else {
tried.push((PathBuf::from("<invalid path>"), e));
}
}
f => return f,
}
}
let errmessage = String::from(convenient_path_to_str(path)?);
Err(GameError::ResourceNotFound(errmessage, tried))
}
/// Create a directory at the location by this path
fn mkdir(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.mkdir(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not find anywhere writeable to make dir {:?}",
path
)))
}
/// Remove a file
fn rm(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.rm(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not remove file {:?}",
path
)))
}
/// Remove a file or directory and all its contents
fn rmrf(&self, path: &Path) -> GameResult {
for vfs in &self.roots {
match vfs.rmrf(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not remove file/dir {:?}",
path
)))
}
/// Check if the file exists
fn exists(&self, path: &Path) -> bool {
for vfs in &self.roots {
if vfs.exists(path) {
return true;
}
}
false
}
/// Get the file's metadata
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>> {
for vfs in &self.roots {
match vfs.metadata(path) {
Err(_) => (),
f => return f,
}
}
Err(GameError::FilesystemError(format!(
"Could not get metadata for file/dir {:?}",
path
)))
}
/// Retrieve the path entries in this path
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
// This is tricky 'cause we have to actually merge iterators together...
// Doing it the simple and stupid way works though.
let mut v = Vec::new();
for fs in &self.roots {
if let Ok(rddir) = fs.read_dir(path) {
v.extend(rddir)
}
}
Ok(Box::new(v.into_iter()))
}
/// Retrieve the actual location of the VFS root, if available.
fn to_path_buf(&self) -> Option<PathBuf> {
None
}
}
#[cfg(test)]
mod tests {
use std::io::{self, BufRead};
use super::*;
#[test]
fn headless_test_path_filtering() {
// Valid pahts
let p = path::Path::new("/foo");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/foo/");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/foo/bar.txt");
assert!(sanitize_path(p).is_some());
let p = path::Path::new("/");
assert!(sanitize_path(p).is_some());
// Invalid paths
let p = path::Path::new("../foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("foo");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../../");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/foo/../bop");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("/../bar");
assert!(sanitize_path(p).is_none());
let p = path::Path::new("");
assert!(sanitize_path(p).is_none());
}
#[test]
fn headless_test_read() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, true);
let f = fs.open(Path::new("/Cargo.toml")).unwrap();
let mut bf = io::BufReader::new(f);
let mut s = String::new();
let _ = bf.read_line(&mut s).unwrap();
// Trim whitespace from string 'cause it will
// potentially be different on Windows and Unix.
let trimmed_string = s.trim();
assert_eq!(trimmed_string, "[package]");
}
#[test]
fn headless_test_read_overlay() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs1 = PhysicalFS::new(cargo_path, true);
let mut f2path = PathBuf::from(cargo_path);
f2path.push("src");
let fs2 = PhysicalFS::new(&f2path, true);
let mut ofs = OverlayFS::new();
ofs.push_back(Box::new(fs1));
ofs.push_back(Box::new(fs2));
assert!(ofs.exists(Path::new("/Cargo.toml")));
assert!(ofs.exists(Path::new("/lib.rs")));
assert!(!ofs.exists(Path::new("/foobaz.rs")));
}
#[test]
fn headless_test_physical_all() {
let cargo_path = Path::new(env!("CARGO_MANIFEST_DIR"));
let fs = PhysicalFS::new(cargo_path, false);
let testdir = Path::new("/testdir");
let f1 = Path::new("/testdir/file1.txt");
// Delete testdir if it is still lying around
if fs.exists(testdir) {
fs.rmrf(testdir).unwrap();
}
assert!(!fs.exists(testdir));
// Create and delete test dir
fs.mkdir(testdir).unwrap();
assert!(fs.exists(testdir));
fs.rm(testdir).unwrap();
assert!(!fs.exists(testdir));
let test_string = "Foo!";
fs.mkdir(testdir).unwrap();
{
let mut f = fs.append(f1).unwrap();
let _ = f.write(test_string.as_bytes()).unwrap();
}
{
let mut buf = Vec::new();
let mut f = fs.open(f1).unwrap();
let _ = f.read_to_end(&mut buf).unwrap();
assert_eq!(&buf[..], test_string.as_bytes());
}
{
// Test metadata()
let m = fs.metadata(f1).unwrap();
assert!(m.is_file());
assert!(!m.is_dir());
assert_eq!(m.len(), 4);
let m = fs.metadata(testdir).unwrap();
assert!(!m.is_file());
assert!(m.is_dir());
// Not exactly sure what the "length" of a directory is, buuuuuut...
// It appears to vary based on the platform in fact.
// On my desktop, it's 18.
// On Travis's VM, it's 4096.
// On Appveyor's VM, it's 0.
// So, it's meaningless.
//assert_eq!(m.len(), 18);
}
{
// Test read_dir()
let r = fs.read_dir(testdir).unwrap();
assert_eq!(r.count(), 1);
let r = fs.read_dir(testdir).unwrap();
for f in r {
let fname = f.unwrap();
assert!(fs.exists(&fname));
}
}
{
assert!(fs.exists(f1));
fs.rm(f1).unwrap();
assert!(!fs.exists(f1));
}
fs.rmrf(testdir).unwrap();
assert!(!fs.exists(testdir));
}
// BUGGO: TODO: Make sure all functions are tested for OverlayFS and ZipFS!!
}

View File

@ -1,4 +1,4 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
use imgui::{Condition, im_str, ImStr, ImString, Window};
use itertools::Itertools;
@ -35,7 +35,8 @@ impl LiveDebugger {
Window::new(im_str!("Error!"))
.resizable(false)
.collapsible(false)
.size([300.0, 100.0], Condition::Always)
.position([((state.screen_size.0 - 300.0) / 2.0).floor(), ((state.screen_size.1 - 100.0) / 2.0).floor()], Condition::Appearing)
.size([300.0, 100.0], Condition::Appearing)
.build(ui, || {
ui.push_item_width(-1.0);
ui.text(self.error.as_ref().unwrap());
@ -47,8 +48,9 @@ impl LiveDebugger {
}
Window::new(im_str!("Map selector"))
.resizable(false)
.collapsed(true, Condition::FirstUseEver)
.size([240.0, 270.0], Condition::FirstUseEver)
.size([240.0, 280.0], Condition::FirstUseEver)
.build(ui, || {
if self.stages.is_empty() {
for s in state.stages.iter() {

View File

@ -1,23 +1,33 @@
extern crate strum;
#[macro_use]
extern crate strum_macros;
#[macro_use]
extern crate bitflags;
#[macro_use]
extern crate gfx;
#[macro_use]
extern crate log;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate smart_default;
use std::{env, mem};
use std::path;
use ggez::{Context, ContextBuilder, event, filesystem, GameResult};
use ggez::conf::{WindowMode, WindowSetup};
use ggez::event::{KeyCode, KeyMods};
use ggez::graphics;
use ggez::graphics::DrawParam;
use ggez::input::keyboard;
use ggez::mint::ColumnMatrix4;
use ggez::nalgebra::Vector2;
use log::*;
use pretty_env_logger::env_logger::Env;
use winit::{ElementState, Event, KeyboardInput, WindowEvent};
use crate::engine_constants::EngineConstants;
use crate::ggez::{Context, ContextBuilder, event, filesystem, GameResult};
use crate::ggez::conf::{WindowMode, WindowSetup};
use crate::ggez::event::{KeyCode, KeyMods};
use crate::ggez::graphics;
use crate::ggez::graphics::DrawParam;
use crate::ggez::input::keyboard;
use crate::ggez::mint::ColumnMatrix4;
use crate::ggez::nalgebra::Vector2;
use crate::live_debugger::LiveDebugger;
use crate::scene::loading_scene::LoadingScene;
use crate::scene::Scene;
@ -31,6 +41,7 @@ mod engine_constants;
mod entity;
mod enemy;
mod frame;
mod ggez;
mod live_debugger;
mod map;
mod player;
@ -131,7 +142,7 @@ impl Game {
texture_set: TextureSet::new(base_path),
base_path: str!(base_path),
stages: Vec::new(),
sound_manager: SoundManager::new(),
sound_manager: SoundManager::new(ctx),
constants,
scale,
screen_size,
@ -217,7 +228,7 @@ pub fn main() -> GameResult {
info!("Resource directory: {:?}", resource_dir);
info!("Initializing engine...");
let cb = ContextBuilder::new("doukutsu-rs", "Alula")
let cb = ContextBuilder::new("doukutsu-rs")
.window_setup(WindowSetup::default().title("Cave Story (doukutsu-rs)"))
.window_mode(WindowMode::default().dimensions(854.0, 480.0))
.add_resource_path(resource_dir);

View File

@ -1,4 +1,4 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
use num_traits::clamp;
use crate::bitfield;

View File

@ -1,11 +1,10 @@
use ggez::{Context, GameResult};
use ggez::nalgebra::clamp;
use log::info;
use num_traits::abs;
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::ggez::{Context, GameResult, timer};
use crate::ggez::nalgebra::clamp;
use crate::live_debugger::LiveDebugger;
use crate::player::Player;
use crate::scene::Scene;
@ -272,8 +271,7 @@ impl Scene for GameScene {
self.draw_tiles(state, ctx, TileLayer::Foreground)?;
self.draw_hud(state, ctx)?;
self.draw_number(16.0, 50.0, abs(self.player.x) as usize, Alignment::Left, state, ctx)?;
self.draw_number(16.0, 58.0, abs(self.player.y) as usize, Alignment::Left, state, ctx)?;
self.draw_number(state.canvas_size.0 - 8.0, 8.0, timer::fps(ctx) as usize, Alignment::Right, state, ctx)?;
Ok(())
}

View File

@ -1,4 +1,4 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
use crate::scene::game_scene::GameScene;
use crate::scene::Scene;
@ -23,7 +23,7 @@ impl Scene for LoadingScene {
if self.tick == 1 {
let stages = StageData::load_stage_table(ctx, &state.base_path)?;
state.stages = stages;
state.next_scene = Some(Box::new(GameScene::new(state, ctx, 53)?));
state.next_scene = Some(Box::new(GameScene::new(state, ctx, 0)?));
}
self.tick += 1;

View File

@ -1,4 +1,4 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
use crate::live_debugger::LiveDebugger;
use crate::SharedGameState;

View File

@ -1,27 +1,31 @@
use std::io::{Cursor, Read};
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
pub mod pixtone;
pub struct SoundManager {
intro: Cursor<Vec<u8>>,
sloop: Cursor<Vec<u8>>,
intro: Vec<u8>,
sloop: Vec<u8>,
}
//unsafe impl Send for SoundManager {}
impl SoundManager {
pub fn new() -> SoundManager {
pub fn new(ctx: &mut Context) -> SoundManager {
SoundManager {
intro: Cursor::new(Vec::new()),
sloop: Cursor::new(Vec::new()),
intro: Vec::new(),
sloop: Vec::new(),
}
}
pub fn play_song(&mut self, ctx: &mut Context) -> GameResult {
/*self.intro.get_mut().clear();
ggez::filesystem::open(ctx, "/Soundtracks/Arranged/oside_intro.ogg")?.read_to_end(self.intro.get_mut())?;
/*self.intro.clear();
self.sloop.clear();
ggez::filesystem::open(ctx, "/base/Ogg11/curly_intro.ogg")?.read_to_end(&mut self.intro)?;
ggez::filesystem::open(ctx, "/base/Ogg11/curly_loop.ogg")?.read_to_end(&mut self.sloop)?;
let sink = rodio::play_once(ctx.audio_context.device(), self.intro.clone())?;
let sink = Sink::new(ctx.audio_context.device());
sink.append(rodio::Decoder::new(Cursor::new(self.intro.clone()))?);
sink.append(rodio::Decoder::new(Cursor::new(self.sloop.clone()))?);
sink.detach();*/
Ok(())

View File

@ -3,10 +3,10 @@ use std::str::from_utf8;
use byteorder::LE;
use byteorder::ReadBytesExt;
use ggez::{Context, filesystem, GameResult};
use ggez::GameError::ResourceLoadError;
use log::info;
use crate::ggez::{Context, filesystem, GameResult};
use crate::ggez::GameError::ResourceLoadError;
use crate::map::Map;
#[derive(Debug, PartialEq, Eq, Hash)]

View File

@ -1,11 +1,11 @@
use ggez::{Context, GameResult};
use crate::ggez::{Context, GameResult};
struct TextScript {
}
impl TextScript {
pub fn load(ctx: &mut Context, filename: &str) -> GameResult<TextScript> {
pub fn load(filename: &str, ctx: &mut Context) -> GameResult<TextScript> {
let tsc = TextScript {};
Ok(tsc)
}

View File

@ -1,11 +1,11 @@
use std::collections::HashMap;
use std::io::Read;
use ggez::{Context, GameError, GameResult};
use ggez::filesystem;
use ggez::graphics::{Drawable, DrawParam, FilterMode, Image, Rect};
use ggez::graphics::spritebatch::SpriteBatch;
use ggez::nalgebra::{Point2, Vector2};
use crate::ggez::{Context, GameError, GameResult};
use crate::ggez::filesystem;
use crate::ggez::graphics::{Drawable, DrawParam, FilterMode, Image, Rect};
use crate::ggez::graphics::spritebatch::SpriteBatch;
use crate::ggez::nalgebra::{Point2, Vector2};
use itertools::Itertools;
use log::info;

View File

@ -1,14 +1,15 @@
use std::time::Instant;
use ggez::{Context, GameResult, graphics};
use ggez::GameError::RenderError;
use imgui::{FontConfig, FontSource};
use imgui::sys::*;
use imgui_gfx_renderer::{Renderer, Shaders};
use imgui_gfx_renderer::gfx::format::Rgba8;
use imgui_gfx_renderer::gfx::handle::RenderTargetView;
use imgui_gfx_renderer::gfx::memory::Typed;
use imgui_winit_support::{HiDpiMode, WinitPlatform};
use crate::ggez::{Context, GameResult, graphics};
use crate::ggez::GameError::RenderError;
use crate::live_debugger::LiveDebugger;
use crate::scene::Scene;
use crate::SharedGameState;
@ -36,7 +37,81 @@ impl UI {
config: Some(FontConfig::default()),
},
]);
imgui.style_mut().use_classic_colors();
imgui.style_mut().window_padding = [4.0, 6.0];
imgui.style_mut().frame_padding = [8.0, 6.0];
imgui.style_mut().item_spacing = [8.0, 6.0];
imgui.style_mut().item_inner_spacing = [8.0, 6.0];
imgui.style_mut().indent_spacing = 20.0;
imgui.style_mut().scrollbar_size = 20.0;
imgui.style_mut().grab_min_size = 5.0;
imgui.style_mut().window_border_size = 0.0;
imgui.style_mut().child_border_size = 1.0;
imgui.style_mut().popup_border_size = 1.0;
imgui.style_mut().frame_border_size = 1.0;
imgui.style_mut().tab_border_size = 0.0;
imgui.style_mut().window_rounding = 0.0;
imgui.style_mut().child_rounding = 0.0;
imgui.style_mut().frame_rounding = 0.0;
imgui.style_mut().popup_rounding = 0.0;
imgui.style_mut().scrollbar_rounding = 0.0;
imgui.style_mut().grab_rounding = 0.0;
imgui.style_mut().tab_rounding = 0.0;
imgui.style_mut().window_title_align = [0.50, 0.50];
imgui.style_mut().window_rounding = 0.0;
let colors = &mut imgui.style_mut().colors;
colors[ImGuiCol_Text as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_TextDisabled as usize] = [0.50, 0.50, 0.50, 1.00];
colors[ImGuiCol_WindowBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_ChildBg as usize] = [0.05, 0.05, 0.05, 0.60];
colors[ImGuiCol_PopupBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_Border as usize] = [0.40, 0.40, 0.40, 1.00];
colors[ImGuiCol_BorderShadow as usize] = [1.00, 1.00, 1.00, 0.00];
colors[ImGuiCol_FrameBg as usize] = [0.00, 0.00, 0.00, 0.60];
colors[ImGuiCol_FrameBgHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_FrameBgActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_TitleBg as usize] = [0.06, 0.06, 0.06, 1.00];
colors[ImGuiCol_TitleBgActive as usize] = [0.00, 0.00, 0.00, 1.00];
colors[ImGuiCol_TitleBgCollapsed as usize] = [0.06, 0.06, 0.06, 0.40];
colors[ImGuiCol_MenuBarBg as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ScrollbarBg as usize] = [0.14, 0.14, 0.14, 0.40];
colors[ImGuiCol_ScrollbarGrab as usize] = [0.31, 0.31, 0.31, 0.30];
colors[ImGuiCol_ScrollbarGrabHovered as usize] = [1.00, 1.00, 1.00, 0.30];
colors[ImGuiCol_ScrollbarGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_CheckMark as usize] = [0.90, 0.90, 0.90, 1.00];
colors[ImGuiCol_SliderGrab as usize] = [0.31, 0.31, 0.31, 1.00];
colors[ImGuiCol_SliderGrabActive as usize] = [1.00, 1.00, 1.00, 0.50];
colors[ImGuiCol_Button as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_ButtonHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_ButtonActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Header as usize] = [0.14, 0.14, 0.14, 1.00];
colors[ImGuiCol_HeaderHovered as usize] = [0.84, 0.37, 0.00, 0.20];
colors[ImGuiCol_HeaderActive as usize] = [0.84, 0.37, 0.00, 1.00];
colors[ImGuiCol_Separator as usize] = [0.50, 0.50, 0.43, 0.50];
colors[ImGuiCol_SeparatorHovered as usize] = [0.75, 0.45, 0.10, 0.78];
colors[ImGuiCol_SeparatorActive as usize] = [0.75, 0.45, 0.10, 1.00];
colors[ImGuiCol_ResizeGrip as usize] = [0.98, 0.65, 0.26, 0.25];
colors[ImGuiCol_ResizeGripHovered as usize] = [0.98, 0.65, 0.26, 0.67];
colors[ImGuiCol_ResizeGripActive as usize] = [0.98, 0.65, 0.26, 0.95];
colors[ImGuiCol_Tab as usize] = [0.17, 0.10, 0.04, 0.94];
colors[ImGuiCol_TabHovered as usize] = [0.84, 0.37, 0.00, 0.60];
colors[ImGuiCol_TabActive as usize] = [0.67, 0.30, 0.00, 0.68];
colors[ImGuiCol_TabUnfocused as usize] = [0.06, 0.05, 0.05, 0.69];
colors[ImGuiCol_TabUnfocusedActive as usize] = [0.36, 0.17, 0.03, 0.64];
colors[ImGuiCol_PlotLines as usize] = [0.39, 0.39, 0.39, 1.00];
colors[ImGuiCol_PlotLinesHovered as usize] = [0.35, 0.92, 1.00, 1.00];
colors[ImGuiCol_PlotHistogram as usize] = [0.00, 0.20, 0.90, 1.00];
colors[ImGuiCol_PlotHistogramHovered as usize] = [0.00, 0.40, 1.00, 1.00];
colors[ImGuiCol_TextSelectedBg as usize] = [0.98, 0.65, 0.26, 0.35];
colors[ImGuiCol_DragDropTarget as usize] = [0.00, 0.00, 1.00, 0.90];
colors[ImGuiCol_NavHighlight as usize] = [0.98, 0.65, 0.26, 1.00];
colors[ImGuiCol_NavWindowingHighlight as usize] = [0.00, 0.00, 0.00, 0.70];
colors[ImGuiCol_NavWindowingDimBg as usize] = [0.20, 0.20, 0.20, 0.20];
colors[ImGuiCol_ModalWindowDimBg as usize] = [0.20, 0.20, 0.20, 0.35];
let mut platform = WinitPlatform::init(&mut imgui);
platform.attach_window(imgui.io_mut(), graphics::window(ctx), HiDpiMode::Rounded);