Creating an Application
To create various applications and games with our engine, we will need to handle tasks such as creating a window, tracking input from various sources (keyboard, controllers, etc), and keeping track of our application state. These tasks and more will all be encapsulated within our phantom_app
crate!
Application Configuration
We'll start off by creating a data structure representing our application's initial configuration. This will allow consumers of the crate to customize features such as window size, the title, the icon, etc.
In our crates/phantom_app/lib.rs
we'll replace the contents of the file with the following:
mod app;
pub use self::app::*;
Now we will create a file named crates/phantom_app/app.rs
and begin building our application config.
pub struct AppConfig {
pub width: u32,
pub height: u32,
pub is_fullscreen: bool,
pub title: String,
pub icon: Option<String>,
}
impl Default for AppConfig {
fn default() -> Self {
Self {
width: 1024,
height: 768,
is_fullscreen: false,
title: "Phantom Application".to_string(),
icon: None,
}
}
}
Application Dependencies
Creating a window requires different boilerplate on each platform. Thankfully an excellent open source library for handling window creation named winit exists.
Let's add that to our phantom_dependencies
project:
cargo add winit -p phantom_dependencies
And let's not forget to re-export it in crates/phantom_dependencies/lib.rs
!
pub use winit;
Application Resources
Let's create another module to store resources necessary for the application to run.
Declaring the Resources Module
In our crates/phantom_app/lib.rs
we'll declare and export the resources
module.
...
mod resources;
pub use self::{
// ...
resources::*,
};
Storing Application Resources
Create the file phantom_app/src/resources.rs
with the following contents:
use phantom_dependencies::winit::window::Window;
pub struct Resources<'a> {
pub window: &'a mut Window,
}
impl<'a> Resources<'a> {
pub fn set_cursor_visible(&mut self, visible: bool) {
self.window.set_cursor_visible(visible)
}
}
Application Creation
Now we can do something exciting and get a window visible on screen!
First, run the following command to allow macros from thiserror
to work for phantom_app
:
cargo add thiserror -p phantom_app
Now, let's add the following code to phantom_app/src/app.rs
.
use crate::Resources;
use phantom_dependencies::{
env_logger,
image::{self, io::Reader},
log,
thiserror::Error,
winit::{
self,
dpi::PhysicalSize,
event::{ElementState, Event, VirtualKeyCode, WindowEvent},
event_loop::{ControlFlow, EventLoop},
window::{Icon, WindowBuilder},
},
};
#[derive(Error, Debug)]
pub enum ApplicationError {
#[error("Failed to create a window!")]
CreateWindow(#[source] winit::error::OsError),
}
type Result<T, E = ApplicationError> = std::result::Result<T, E>;
// ...
pub fn run(config: AppConfig) -> Result<()> {
env_logger::init();
log::info!("Phantom app started");
let event_loop = EventLoop::new();
let mut window_builder = WindowBuilder::new()
.with_title(config.title.to_string())
.with_inner_size(PhysicalSize::new(config.width, config.height));
// TODO: Load the window icon
let mut window = window_builder
.build(&event_loop)
.map_err(ApplicationError::CreateWindow)?;
event_loop.run(move |event, _, control_flow| {
let resources = Resources {
window: &mut window,
};
if let Err(error) = run_loop(&event, control_flow, resources) {
log::error!("Application error: {}", error);
}
});
}
fn run_loop(event: &Event<()>, control_flow: &mut ControlFlow, resources: Resources) -> Result<()> {
match event {
Event::WindowEvent {
ref event,
window_id,
} if *window_id == resources.window.id() => match event {
WindowEvent::CloseRequested => *control_flow = ControlFlow::Exit,
WindowEvent::KeyboardInput { input, .. } => {
if let (Some(VirtualKeyCode::Escape), ElementState::Pressed) =
(input.virtual_keycode, input.state)
{
*control_flow = ControlFlow::Exit;
}
}
_ => {}
},
_ => {}
}
Ok(())
}
Loading the Window Icon
To load the window icon, we'll need a library for loading images. We'll use the image crate.
cargo add image -p phantom_dependencies
Then re-export it in phantom_dependencies/lib.rs
.
pub use image;
We can now add errors for our icon loading code:
#[error("Failed to create icon file!")]
CreateIcon(#[source] winit::window::BadIcon),
...
#[error("Failed to decode icon file at path: {1}")]
DecodeIconFile(#[source] image::ImageError, String),
#[error("Failed to open icon file at path: {1}")]
OpenIconFile(#[source] io::Error, String),
Now we can replace our TODO
with the following code!
if let Some(icon_path) = config.icon.as_ref() {
let image = Reader::open(icon_path)
.map_err(|error| ApplicationError::OpenIconFile(error, icon_path.to_string()))?
.decode()
.map_err(|error| ApplicationError::DecodeIconFile(error, icon_path.to_string()))?
.into_rgba8();
let (width, height) = image.dimensions();
let icon = Icon::from_rgba(image.into_raw(), width, height)
.map_err(ApplicationError::CreateIcon)?;
window_builder = window_builder.with_window_icon(Some(icon));
}
Instantiating the Editor
Now that we've written the necessary window creation code in our library, we can setup our editor
application.
Replace the contents of apps/editor/src/main.rs
with the following code.
use phantom::{
app::{run, AppConfig},
dependencies::anyhow::Result,
};
#[derive(Default)]
pub struct Editor;
fn main() -> Result<()> {
Ok(run(AppConfig {
icon: Some("assets/icon/phantom.png".to_string()),
..Default::default()
})?)
}
Run the application again with cargo run -r --bin editor
and you should see a blank window with our phantom logo!
Viewing the console logs
To view the console logs, set the RUST_LOG
environment variable to debug
.
Mac/Linux:
RUST_LOG="debug"
Windows (powershell):
$env:RUST_LOG="debug"