#![deny(clippy::all)]
#![allow(clippy::ptr_arg)]
pub mod dialogs;
#[cfg(feature = "scenes")]
pub mod scenes;
pub mod ui;
pub mod utilities;
#[cfg(feature = "window_prefs")]
pub mod window_prefs;
use crate::prelude::{coord, Coord, ALL_KEYS};
use crate::ui::styles::UiStyle;
#[cfg(feature = "window_prefs")]
use crate::window_prefs::WindowPreferences;
#[cfg(feature = "window_prefs")]
use crate::GraphicsError::LoadingWindowPref;
pub use buffer_graphics_lib;
use buffer_graphics_lib::Graphics;
use pixels::{Pixels, PixelsBuilder, SurfaceTexture};
use rustc_hash::FxHashMap;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use simple_game_utils::prelude::*;
use thiserror::Error;
use winit::dpi::LogicalSize;
use winit::event::{Event, WindowEvent};
use winit::event_loop::EventLoop;
use winit::keyboard::KeyCode;
use winit::window::{CursorGrabMode, Window, WindowBuilder};
use winit_input_helper::WinitInputHelper;
pub mod prelude {
pub use crate::dialogs::*;
pub use crate::run;
#[cfg(feature = "scenes")]
pub use crate::scenes::*;
pub use crate::setup;
pub use crate::utilities::virtual_key_codes::*;
#[cfg(feature = "window_prefs")]
pub use crate::window_prefs::*;
pub use crate::GraphicsError;
pub use crate::MouseButton;
pub use crate::MouseData;
pub use crate::Options;
pub use crate::System;
pub use crate::WindowScaling;
pub use buffer_graphics_lib::prelude::*;
pub use simple_game_utils::prelude::*;
pub use winit::keyboard::KeyCode;
}
#[derive(Error, Debug)]
pub enum GraphicsError {
#[error("Creating a window: {0}")]
WindowInit(String),
#[error("Initialising Pixels: {0}")]
PixelsInit(#[source] pixels::Error),
#[error("Saving window pref: {0}")]
SavingWindowPref(String),
#[cfg(feature = "window_prefs")]
#[error("Loading window pref: {0}")]
LoadingWindowPref(String),
#[error("Invalid pixel array length, expected: {0}, found: {1}")]
ImageInitSize(usize, usize),
#[error("Both images must be the same size, expected: {0}x{1}, found: {2}x{3}")]
ImageBlendSize(usize, usize, usize, usize),
#[cfg(feature = "controller")]
#[error("Unable to init controller: {0}")]
ControllerInit(String),
}
pub fn setup(
canvas_size: (usize, usize),
options: &Options,
title: &str,
event_loop: &EventLoop<()>,
) -> Result<(Window, Pixels), GraphicsError> {
let win = create_window(canvas_size, title, options.scaling, event_loop)?;
let surface = SurfaceTexture::new(win.inner_size().width, win.inner_size().height, &win);
let pixels = PixelsBuilder::new(canvas_size.0 as u32, canvas_size.1 as u32, surface)
.enable_vsync(options.vsync)
.build()
.map_err(GraphicsError::PixelsInit)?;
Ok((win, pixels))
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum WindowScaling {
None,
Auto,
Fixed(usize),
AutoFixed(usize),
}
fn create_window(
size: (usize, usize),
title: &str,
scale: WindowScaling,
event_loop: &EventLoop<()>,
) -> Result<Window, GraphicsError> {
let window = WindowBuilder::new()
.with_visible(false)
.with_title(title)
.build(event_loop)
.map_err(|err| GraphicsError::WindowInit(format!("{err:?}")))?;
let factor = match scale {
WindowScaling::None => 1.,
WindowScaling::Auto => window.scale_factor().ceil(),
WindowScaling::Fixed(amount) => {
if amount == 0 {
return Err(GraphicsError::WindowInit(String::from(
"Fixed window scaling must be at least 1",
)));
}
amount as f64
}
WindowScaling::AutoFixed(amount) => {
if amount == 0 {
return Err(GraphicsError::WindowInit(String::from(
"AutoFixed window scaling must be at least 1",
)));
}
amount as f64 + window.scale_factor().ceil()
}
};
let px_size = LogicalSize::new(size.0 as f64 * factor, size.1 as f64 * factor);
window.set_min_inner_size(Some(px_size));
let _ = window.request_inner_size(px_size);
window.set_visible(true);
Ok(window)
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
pub enum MouseButton {
Left,
Right,
Middle,
}
#[allow(unused_variables)]
pub trait System {
fn keys_used(&self) -> &[KeyCode] {
&ALL_KEYS
}
#[cfg(feature = "window_prefs")]
fn window_prefs(&mut self) -> Option<WindowPreferences> {
None
}
fn update(&mut self, timing: &Timing);
fn render(&mut self, graphics: &mut Graphics);
fn on_mouse_move(&mut self, mouse: &MouseData) {}
fn on_mouse_down(&mut self, mouse: &MouseData, button: MouseButton) {}
fn on_mouse_up(&mut self, mouse: &MouseData, button: MouseButton) {}
fn on_scroll(&mut self, mouse: &MouseData, x_diff: isize, y_diff: isize) {}
fn on_key_down(&mut self, keys: Vec<KeyCode>) {}
fn on_key_up(&mut self, keys: Vec<KeyCode>) {}
fn on_window_closed(&mut self) {}
fn on_visibility_changed(&mut self, visible: bool) {}
fn on_focus_changed(&mut self, focused: bool) {}
fn should_exit(&mut self) -> bool {
false
}
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Eq, PartialEq)]
pub struct Options {
pub ups: usize,
pub scaling: WindowScaling,
pub vsync: bool,
pub hide_cursor: bool,
pub confine_cursor: bool,
pub style: UiStyle,
}
impl Options {
pub fn new(
ups: usize,
scaling: WindowScaling,
vsync: bool,
hide_cursor: bool,
confine_cursor: bool,
style: UiStyle,
) -> Self {
Self {
ups,
scaling,
vsync,
hide_cursor,
confine_cursor,
style,
}
}
}
impl Default for Options {
fn default() -> Self {
Self {
ups: 240,
scaling: WindowScaling::Auto,
vsync: true,
hide_cursor: false,
confine_cursor: false,
style: UiStyle::default(),
}
}
}
pub fn run(
width: usize,
height: usize,
title: &str,
mut system: Box<dyn System>,
options: Options,
) -> Result<(), GraphicsError> {
let event_loop = EventLoop::new().expect("Failed to setup event loop");
let mut input = WinitInputHelper::new();
let (mut window, mut pixels) = setup((width, height), &options, title, &event_loop)?;
if options.confine_cursor {
#[cfg(target_os = "macos")]
let _ = window.set_cursor_grab(CursorGrabMode::Locked);
#[cfg(not(target_os = "macos"))]
let _ = window.set_cursor_grab(CursorGrabMode::Confined);
}
if options.hide_cursor {
window.set_cursor_visible(false);
}
#[cfg(feature = "window_prefs")]
if let Some(mut prefs) = system.window_prefs() {
prefs.load().map_err(|e| LoadingWindowPref(e.to_string()))?;
prefs.restore(&mut window);
}
let mut timing = Timing::new(options.ups);
let mut mouse = MouseData::default();
event_loop
.run(move |event, target| {
timing.update();
match &event {
Event::LoopExiting => {
system.on_window_closed();
#[cfg(feature = "window_prefs")]
if let Some(mut prefs) = system.window_prefs() {
prefs.store(&window);
let _ = prefs
.save()
.map_err(|err| eprintln!("Unable to save prefs: {err:?}"));
}
}
Event::WindowEvent { event, .. } => match event {
WindowEvent::Occluded(hidden) => system.on_visibility_changed(!hidden),
WindowEvent::Focused(focused) => system.on_focus_changed(*focused),
WindowEvent::RedrawRequested => {
let mut graphics =
Graphics::new(pixels.frame_mut(), width, height).unwrap();
system.render(&mut graphics);
timing.renders += 1;
if pixels
.render()
.map_err(|e| eprintln!("pixels.render() failed: {e:?}"))
.is_err()
{
system.on_window_closed();
target.exit();
return;
}
}
_ => {}
},
_ => {}
}
timing.accumulated_time += timing.delta;
while timing.accumulated_time >= timing.fixed_time_step {
system.update(&timing);
timing.accumulated_time -= timing.fixed_time_step;
timing.updates += 1;
}
if input.update(&event) {
if input.close_requested() || input.destroyed() {
system.on_window_closed();
target.exit();
return;
}
if let Some(size) = input.window_resized() {
pixels
.resize_surface(size.width, size.height)
.expect("Unable to resize buffer");
}
if let Some(mc) = input.cursor() {
let (x, y) = pixels
.window_pos_to_pixel(mc)
.unwrap_or_else(|pos| pixels.clamp_pixel_pos(pos));
mouse.xy = coord!(x, y);
system.on_mouse_move(&mouse);
}
let mut held_buttons = vec![];
for button in system.keys_used() {
if input.key_held(*button) {
held_buttons.push(*button);
}
}
if !held_buttons.is_empty() {
system.on_key_down(held_buttons);
}
let mut released_buttons = vec![];
for button in system.keys_used() {
if input.key_released(*button) {
released_buttons.push(*button);
}
}
if !released_buttons.is_empty() {
system.on_key_up(released_buttons);
}
if input.mouse_pressed(0) {
mouse.add_down(mouse.xy, MouseButton::Left);
system.on_mouse_down(&mouse, MouseButton::Left);
}
if input.mouse_pressed(1) {
mouse.add_down(mouse.xy, MouseButton::Right);
system.on_mouse_down(&mouse, MouseButton::Right);
}
if input.mouse_pressed(2) {
mouse.add_down(mouse.xy, MouseButton::Middle);
system.on_mouse_down(&mouse, MouseButton::Middle);
}
if input.mouse_released(0) {
mouse.add_up(MouseButton::Left);
system.on_mouse_up(&mouse, MouseButton::Left);
}
if input.mouse_released(1) {
mouse.add_up(MouseButton::Right);
system.on_mouse_up(&mouse, MouseButton::Right);
}
if input.mouse_released(2) {
mouse.add_up(MouseButton::Middle);
system.on_mouse_up(&mouse, MouseButton::Middle);
}
let scroll = input.scroll_diff();
if scroll.0 != 0.0 || scroll.1 != 0.0 {
system.on_scroll(&mouse, scroll.0.trunc() as isize, scroll.1.trunc() as isize);
}
window.request_redraw();
}
if system.should_exit() {
target.exit();
}
timing.update_fps();
timing.last = timing.now;
})
.expect("Error when executing event loop");
Ok(())
}
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Eq, PartialEq, Default)]
pub struct MouseData {
pub xy: Coord,
buttons: FxHashMap<MouseButton, Coord>,
}
impl MouseData {
pub fn any_held(&self) -> bool {
self.buttons.contains_key(&MouseButton::Left)
|| self.buttons.contains_key(&MouseButton::Right)
|| self.buttons.contains_key(&MouseButton::Middle)
}
pub fn is_down(&self, button: MouseButton) -> Option<Coord> {
self.buttons.get(&button).cloned()
}
pub(crate) fn add_up(&mut self, button: MouseButton) {
self.buttons.remove(&button);
}
pub(crate) fn add_down(&mut self, xy: Coord, button: MouseButton) {
self.buttons.insert(button, xy);
}
}