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/ 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/ 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/!

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/ we'll declare and export the resources module.

mod resources;

pub use self::{
    // ...

Storing Application Resources

Create the file phantom_app/src/ 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) {

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/

use crate::Resources;
use phantom_dependencies::{
    image::{self, io::Reader},
        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<()> {
    log::info!("Phantom app started");

    let event_loop = EventLoop::new();
    let mut window_builder = WindowBuilder::new()
        .with_inner_size(PhysicalSize::new(config.width, config.height));

    // TODO: Load the window icon

    let mut window = window_builder
        .map_err(ApplicationError::CreateWindow)?; |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,
        } if *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;

            _ => {}
        _ => {}

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/

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()))?
        .map_err(|error| ApplicationError::DecodeIconFile(error, icon_path.to_string()))?
    let (width, height) = image.dimensions();
    let icon = Icon::from_rgba(image.into_raw(), width, height)
    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/ with the following code.

use phantom::{
    app::{run, AppConfig},

pub struct Editor;

fn main() -> Result<()> {
    Ok(run(AppConfig {
        icon: Some("assets/icon/phantom.png".to_string()),

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.



Windows (powershell):