diff --git a/CHANGES.md b/CHANGES.md index a35311df..3f674778 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ## WIP - Fix compile issues with font-related features +- [BREAKING] Replace 'lifecycle' module with 'input' module: + - [BREAKING] Rename `EventStream` to `Input` + - Integrate the input state cache directly into `Input` + - [BREAKING] The `blinds::Window` struct and the `Event` enums are now wrapped with methods that use `quicksilver::geom::Vector` instead of `mint::Vector2` ## v0.4.0-alpha0.3 - Update `golem` to `v0.1.1` to fix non-power-of-2 textures diff --git a/Cargo.toml b/Cargo.toml index 1c234e8c..cfd9dd15 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -30,7 +30,7 @@ ttf = ["font", "elefont/rusttype", "rusttype"] maintenance = { status = "actively-developed" } [dependencies] -blinds = { version = "0.1.0", default-features = false, features = ["gl"] } +blinds = { version = "0.1.4", default-features = false, features = ["gl", "image"] } bytemuck = "1.0" elefont = { version = "0.1.3", features = ["rusttype", "unicode-normalization"], optional = true } gestalt = { version = "0.1", optional = true } diff --git a/README.md b/README.md index 7747607e..06cc68e4 100644 --- a/README.md +++ b/README.md @@ -29,14 +29,13 @@ Then replace `src/main.rs` with the following (the contents of quicksilver's use quicksilver::{ geom::{Rectangle, Vector}, graphics::{Color, Graphics}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + input::{Input, Window}, + Result, Settings, run, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Square Example", ..Settings::default() }, @@ -44,7 +43,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Clear the screen to a blank, white color gfx.clear(Color::WHITE); // Paint a blue square with a red outline in the center of our screen @@ -55,7 +54,7 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu // Send the data to be drawn gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } ``` diff --git a/examples/00_window.rs b/examples/00_window.rs index 8a2f6841..c61267c4 100644 --- a/examples/00_window.rs +++ b/examples/00_window.rs @@ -1,12 +1,7 @@ // Example 0: The Window // The simplest example: Do absolutely nothing other than just opening a window -use mint::Vector2; -use quicksilver::{ - graphics::Graphics, - lifecycle::{run, EventStream, Settings, Window}, - Result, -}; +use quicksilver::{run, Graphics, Input, Result, Settings, Window}; // main() serves as our kicking-off point, but it doesn't have our application logic // Actual logic goes in our app function, which is async @@ -14,7 +9,6 @@ use quicksilver::{ fn main() { run( Settings { - size: Vector2 { x: 800.0, y: 600.0 }, title: "Window Example", ..Settings::default() }, @@ -23,9 +17,9 @@ fn main() { } // Our actual logic! Not much to see for this example -async fn app(_window: Window, _gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(_window: Window, _gfx: Graphics, mut input: Input) -> Result<()> { loop { - while let Some(_) = events.next_event().await { + while let Some(_) = input.next_event().await { // Normally we'd do some processing here } // And then we'd do updates and drawing here diff --git a/examples/01_square.rs b/examples/01_square.rs index 741c94f1..a9f3ac4c 100644 --- a/examples/01_square.rs +++ b/examples/01_square.rs @@ -2,15 +2,13 @@ // Open a window, and draw a colored square in it use quicksilver::{ geom::{Rectangle, Vector}, - graphics::{Color, Graphics}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::Color, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Square Example", ..Settings::default() }, @@ -18,7 +16,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Clear the screen to a blank, white color gfx.clear(Color::WHITE); // Paint a blue square with a red outline in the center of our screen @@ -29,6 +27,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu // Send the data to be drawn gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/02_image.rs b/examples/02_image.rs index 8ead523c..61f86389 100644 --- a/examples/02_image.rs +++ b/examples/02_image.rs @@ -2,15 +2,13 @@ // Draw an image to the screen use quicksilver::{ geom::{Rectangle, Vector}, - graphics::{Color, Graphics, Image}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::{Color, Image}, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Image Example", ..Settings::default() }, @@ -19,7 +17,7 @@ fn main() { } // This time we might return an error, so we use a Result -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Load the image and wait for it to finish // We also use '?' to handle errors like file-not-found let image = Image::load(&gfx, "image.png").await?; @@ -30,6 +28,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/03_rgb_triangle.rs b/examples/03_rgb_triangle.rs index b745b3de..0a6d5770 100644 --- a/examples/03_rgb_triangle.rs +++ b/examples/03_rgb_triangle.rs @@ -2,15 +2,13 @@ // Open a window, and draw the standard GPU triangle use quicksilver::{ geom::Vector, - graphics::{Color, Element, Graphics, Mesh, Vertex}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::{Color, Element, Mesh, Vertex}, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "RGB Triangle Example", ..Settings::default() }, @@ -18,7 +16,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Clear the screen to a blank, black color gfx.clear(Color::BLACK); // Paint a triangle with red, green, and blue vertices, blending the colors for the pixels in-between @@ -54,6 +52,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu // Send the data to be drawn gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/04_render_to_texture.rs b/examples/04_render_to_texture.rs index f1fcd0af..ecaefd5b 100644 --- a/examples/04_render_to_texture.rs +++ b/examples/04_render_to_texture.rs @@ -2,15 +2,13 @@ // Render some data to an image, and draw that image to the screen use quicksilver::{ geom::{Circle, Rectangle, Vector}, - graphics::{Color, Graphics, Image, PixelFormat, Surface}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::{Color, Image, PixelFormat, Surface}, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Square Example", ..Settings::default() }, @@ -18,7 +16,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { gfx.clear(Color::WHITE); // Create a surface, which allows rendering to an image let mut surface = Surface::new( @@ -50,6 +48,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/05_blending.rs b/examples/05_blending.rs index e5a5a241..63f22e81 100644 --- a/examples/05_blending.rs +++ b/examples/05_blending.rs @@ -3,15 +3,13 @@ use quicksilver::{ geom::{Rectangle, Vector}, graphics::blend::{BlendChannel, BlendFactor, BlendFunction, BlendInput, BlendMode}, - graphics::{Color, Graphics, Image}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::{Color, Image}, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Blend Example", ..Settings::default() }, @@ -20,7 +18,7 @@ fn main() { } // This time we might return an error, so we use a Result -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { let image = Image::load(&gfx, "image.png").await?; gfx.clear(Color::WHITE); // Set the blend pipeline @@ -63,6 +61,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/06_timers.rs b/examples/06_timers.rs index 7d132629..c15dfbe9 100644 --- a/examples/06_timers.rs +++ b/examples/06_timers.rs @@ -2,15 +2,13 @@ // Use timers to know when to draw and to have a consistent update cycle. use quicksilver::{ geom::{Rectangle, Vector}, - graphics::{Color, Graphics}, - lifecycle::{run, EventStream, Settings, Window}, - Result, Timer, + graphics::Color, + run, Graphics, Input, Result, Settings, Timer, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Square Example", ..Settings::default() }, @@ -18,7 +16,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Clear the screen to a blank, white color gfx.clear(Color::WHITE); @@ -31,7 +29,7 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu let mut rect = Rectangle::new(Vector::new(0.0, 100.0), Vector::new(100.0, 100.0)); loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} // We use a while loop rather than an if so that we can try to catch up in the event of having a slow down. while update_timer.tick() { diff --git a/examples/07_text.rs b/examples/07_text.rs index d4cff8f7..76f889f1 100644 --- a/examples/07_text.rs +++ b/examples/07_text.rs @@ -2,15 +2,13 @@ // Write some text on the screen use quicksilver::{ geom::Vector, - graphics::{Color, Graphics, VectorFont}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + graphics::{Color, VectorFont}, + run, Graphics, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Font Example", ..Settings::default() }, @@ -18,7 +16,7 @@ fn main() { ); } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { // Load the Font, just like loading any other asset let ttf = VectorFont::load("font.ttf").await?; let mut font = ttf.to_renderer(&gfx, 72.0)?; @@ -33,6 +31,6 @@ async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Resu gfx.present(&window)?; loop { - while let Some(_) = events.next_event().await {} + while let Some(_) = input.next_event().await {} } } diff --git a/examples/08_input.rs b/examples/08_input.rs new file mode 100644 index 00000000..2fcd1fc7 --- /dev/null +++ b/examples/08_input.rs @@ -0,0 +1,50 @@ +// Example 8: Input +// Respond to user keyboard and mouse input onscreen +use quicksilver::{ + geom::{Circle, Rectangle, Vector}, + graphics::Color, + input::Key, + run, Graphics, Input, Result, Settings, Window, +}; + +fn main() { + run( + Settings { + title: "Input Example", + ..Settings::default() + }, + app, + ); +} + +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { + // Keep track of the position of the square + let mut square_position = Vector::new(300, 300); + loop { + while let Some(_) = input.next_event().await {} + // Check the state of the keys, and move the square accordingly + const SPEED: f32 = 2.0; + if input.key_down(Key::A) { + square_position.x -= SPEED; + } + if input.key_down(Key::D) { + square_position.x += SPEED; + } + if input.key_down(Key::W) { + square_position.y -= SPEED; + } + if input.key_down(Key::S) { + square_position.y += SPEED; + } + + gfx.clear(Color::WHITE); + // Paint a blue square at the given position + gfx.fill_rect( + &Rectangle::new(square_position, Vector::new(64.0, 64.0)), + Color::BLUE, + ); + // Paint a red square at the mouse position + gfx.fill_circle(&Circle::new(input.mouse().location(), 32.0), Color::RED); + gfx.present(&window)?; + } +} diff --git a/examples/09_events.rs b/examples/09_events.rs new file mode 100644 index 00000000..2d3066e5 --- /dev/null +++ b/examples/09_events.rs @@ -0,0 +1,65 @@ +// Example 9: Events +// Draw user-typed text to the screen via events +use quicksilver::{ + geom::Vector, + graphics::{Color, VectorFont}, + input::{Event, Key}, + run, Graphics, Input, Result, Settings, Window, +}; + +fn main() { + run( + Settings { + title: "Event Example", + ..Settings::default() + }, + app, + ); +} + +async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { + // We'll need a font to render text and a string to store it + let ttf = VectorFont::load("font.ttf").await?; + let mut font = ttf.to_renderer(&gfx, 36.0)?; + let mut string = String::new(); + // Instead of looping forever, terminate on a given input + let mut running = true; + while running { + while let Some(event) = input.next_event().await { + match event { + Event::KeyboardInput(key) if key.is_down() => { + if key.key() == Key::Escape { + // If the user strikes escape, end the program + running = false; + } else if key.key() == Key::Back { + // If the user strikes Backspace, remove a character from our string + string.pop(); + } + } + Event::ReceivedCharacter(c) => { + // If the user types a printable character, put it into the string + let chr = c.character(); + if !chr.is_control() { + string.push(chr); + } + } + _ => (), + } + } + + // Draw our string to the screen, wrapping at word boundaries + gfx.clear(Color::WHITE); + font.draw_wrapping( + &mut gfx, + &string, + Some(500.0), + Color::BLACK, + Vector::new(100.0, 100.0), + )?; + gfx.present(&window)?; + } + + // Unlike all our earlier examples, our game loop might end early (e.g. before the user closes + // the window.) We have to return Ok(()) because of this + Ok(()) +} diff --git a/examples/loading_screen.rs b/examples/loading_screen.rs index b20e91f4..bc783852 100644 --- a/examples/loading_screen.rs +++ b/examples/loading_screen.rs @@ -11,14 +11,12 @@ use std::time::Duration; use quicksilver::{ geom::{Rectangle, Vector}, graphics::{Color, Graphics}, - lifecycle::{run, EventStream, Settings, Window}, - Result, + run, Input, Result, Settings, Window, }; fn main() { run( Settings { - size: Vector::new(800.0, 600.0).into(), title: "Square Example", ..Settings::default() }, @@ -63,7 +61,7 @@ fn draw_loader(window: &Window, gfx: &mut Graphics, progress: usize, total: usiz Ok(()) } -async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +async fn app(window: Window, mut gfx: Graphics, mut events: Input) -> Result<()> { for i in 0..STEPS { draw_loader(&window, &mut gfx, i, STEPS)?; load_something(); diff --git a/src/graphics.rs b/src/graphics.rs index c5e6453f..cd37695e 100644 --- a/src/graphics.rs +++ b/src/graphics.rs @@ -7,7 +7,7 @@ //! //! For loading and drawing images, to the screen, use [`Image`]. //! -//! [`run`]: crate::lifecycle::run +//! [`run`]: crate::run::run use crate::QuicksilverError; @@ -31,6 +31,7 @@ pub use self::surface::Surface; pub use self::vertex::{Element, Vertex}; use crate::geom::*; +use crate::Window; use golem::*; use std::iter; use std::mem::size_of; @@ -527,7 +528,7 @@ impl Graphics { } /// Send the draw data to the GPU and paint it to the Window - pub fn present(&mut self, win: &blinds::Window) -> Result<(), QuicksilverError> { + pub fn present(&mut self, win: &Window) -> Result<(), QuicksilverError> { self.flush(None)?; win.present(); @@ -545,8 +546,8 @@ impl Graphics { /// The units given are physical units, not logical units. As such when using [`Window::size`], /// be sure to multiply by [`Window::scale_factor`]. /// - /// [`Window::size`]: crate::lifecycle::Window::size - /// [`Window::scale_factor`]: crate::lifecycle::Window::scale_factor + /// [`Window::size`]: crate::Window::size + /// [`Window::scale_factor`]: crate::Window::scale_factor pub fn set_viewport(&self, x: u32, y: u32, width: u32, height: u32) { self.ctx.set_viewport(x, y, width, height); } @@ -565,7 +566,7 @@ impl Graphics { } /// Set the viewport to cover the window, taking into account DPI - pub fn fit_to_window(&self, window: &blinds::Window) { + pub fn fit_to_window(&self, window: &Window) { let size = window.size(); let scale = window.scale_factor(); let width = size.x * scale; diff --git a/src/input.rs b/src/input.rs new file mode 100644 index 00000000..284e47d3 --- /dev/null +++ b/src/input.rs @@ -0,0 +1,250 @@ +//! Read events / input state +//! +//! The main struct for this module is [`Input`], which is provided by the [`run`] method to your +//! app. [`Input`] allows you to read events from the user, which can range from [`window resizes`] +//! to [`key presses`]. +//! +//! There is also an optional feature (enabled by default) to cache input for user convenience. +//! This allows you to write quick expressions like `if input.key_down(Key::W)` rather than having +//! to write code to handle the event when it comes in. +//! +//! [`run`]: crate::run::run +//! [`window resizes`]: ResizedEvent +//! [`key presses`]: KeyboardEvent + +use crate::geom::Vector; + +pub use blinds::event::{ + FocusChangedEvent, GamepadAxisEvent, GamepadButtonEvent, GamepadConnectedEvent, + GamepadDisconnectedEvent, KeyboardEvent, ModifiersChangedEvent, PointerEnteredEvent, + PointerInputEvent, PointerLeftEvent, ReceivedCharacterEvent, ScaleFactorChangedEvent, + ScrollDelta, +}; +#[cfg(feature = "event-cache")] +use blinds::event_cache::EventCache; +/// The button and axis values of a gamepad +#[cfg(feature = "event-cache")] +pub use blinds::event_cache::GamepadState; +pub use blinds::{GamepadAxis, GamepadButton, GamepadId, Key, MouseButton, PointerId}; + +/// The source of events and input device state +pub struct Input { + source: blinds::EventStream, + #[cfg(feature = "event-cache")] + cache: EventCache, +} + +impl Input { + pub(crate) fn new(source: blinds::EventStream) -> Input { + Input { + source, + #[cfg(feature = "event-cache")] + cache: EventCache::new(), + } + } + + /// Retrieve the next event from the environment, or wait until there is one + /// + /// If an event has occured since this method was last called, it will be return as + /// `Some(event)`. Once all events have been handled, `None` will be returned. At this point you + /// should run any update or drawing logic in your app. When this method is called after it + /// returns `None`, it will yield control back to the environment until your app should run + /// again. + pub async fn next_event(&mut self) -> Option { + while let Some(ev) = self.source.next_event().await { + #[cfg(feature = "event-cache")] + self.cache.process_event(&ev); + // If there is an event, it might not be something Quicksilver can process. + // If it's not, skip this and get the next event + if let Some(ev) = conv(ev) { + return Some(ev); + } + } + + // We didn't have any Some events before we hit a None + None + } +} + +#[cfg(feature = "event-cache")] +impl Input { + /// Check if a given key is down + pub fn key_down(&self, key: Key) -> bool { + self.cache.key(key) + } + + /// The state of the global mouse + /// + /// Under a system with touch input or with multiple cursors, this may report erratic results. + /// The state here is tracked for every pointer event, regardless of pointer ID. + pub fn mouse(&self) -> PointerState { + self.cache.mouse().into() + } + + /// The state of the given pointer + #[allow(clippy::trivially_copy_pass_by_ref)] + pub fn pointer(&self, id: &PointerId) -> Option { + self.cache.pointer(id).map(|p| p.into()) + } + + /// The pointer ID and values that have been tracked + pub fn pointers(&self) -> impl Iterator { + self.cache.pointers().map(|(id, p)| (id, p.into())) + } + + /// The state of the given gamepad + pub fn gamepad(&self, id: &GamepadId) -> Option<&GamepadState> { + self.cache.gamepad(id) + } + + /// The gamepad ID and values that have been tracked + pub fn gamepads(&self) -> impl Iterator { + self.cache.gamepads() + } +} + +/// The buttons and location of a given pointer +#[cfg(feature = "event-cache")] +pub struct PointerState { + left: bool, + right: bool, + middle: bool, + location: Vector, +} + +#[cfg(feature = "event-cache")] +impl PointerState { + pub fn left(&self) -> bool { + self.left + } + + pub fn right(&self) -> bool { + self.right + } + + pub fn middle(&self) -> bool { + self.middle + } + + pub fn location(&self) -> Vector { + self.location + } +} + +#[cfg(feature = "event-cache")] +impl From<&blinds::event_cache::PointerState> for PointerState { + fn from(ps: &blinds::event_cache::PointerState) -> PointerState { + PointerState { + left: ps.left(), + right: ps.right(), + middle: ps.middle(), + location: ps.location().into(), + } + } +} + +#[derive(Clone, Debug)] +#[non_exhaustive] +/// An indicator something has changed or input has been dispatched +pub enum Event { + /// The size of the window has changed, see [`Window::size`] + /// + /// [`Window::size`]: crate::Window::size + Resized(ResizedEvent), + /// The scale factor of the window has changed, see [`Window::scale_factor`] + /// + /// [`Window::scale_factor`]: crate::Window::scale_factor + ScaleFactorChanged(ScaleFactorChangedEvent), + /// The window has gained operating system focus (true), or lost it (false) + FocusChanged(FocusChangedEvent), + /// The user typed a character, used for text input + /// + /// Don't use keyboard events for text! Depending on how the user's operating system and + /// keyboard layout are configured, different keys may produce different Unicode characters. + ReceivedCharacter(ReceivedCharacterEvent), + /// A key has been pressed, released, or held down + /// + /// Operating systems often have key repeat settings that cause duplicate events to be + /// generated for a single press. + KeyboardInput(KeyboardEvent), + /// A pointer entered the window + PointerEntered(PointerEnteredEvent), + /// A pointer has exited the window + PointerLeft(PointerLeftEvent), + /// A pointer has a new position, relative to the window's top-left + PointerMoved(PointerMovedEvent), + /// A button on a pointer, likely a mouse, has produced an input + PointerInput(PointerInputEvent), + /// The mousewheel has scrolled, either in lines or pixels (depending on the input method) + ScrollInput(ScrollDelta), + /// The keyboard modifiers (e.g. shift, alt, ctrl) have changed + ModifiersChanged(ModifiersChangedEvent), + /// A gamepad has been connected + GamepadConnected(GamepadConnectedEvent), + /// A gamepad has been disconnected + GamepadDisconnected(GamepadDisconnectedEvent), + /// A gamepad button has been pressed or released + GamepadButton(GamepadButtonEvent), + /// A gamepad axis has changed its value + GamepadAxis(GamepadAxisEvent), +} + +#[derive(Clone, Debug)] +/// See [`Event::Resized`] +pub struct ResizedEvent { + size: Vector, +} + +impl ResizedEvent { + /// The new size of the window + pub fn size(&self) -> Vector { + self.size + } +} + +#[derive(Clone, Debug)] +/// See [`Event::PointerMoved`] +/// +/// [`Event::PointerMoved`]: crate::input::Event::PointerMoved +pub struct PointerMovedEvent { + id: PointerId, + location: Vector, +} + +impl PointerMovedEvent { + pub fn pointer(&self) -> &PointerId { + &self.id + } + + /// The logical location of the pointer, relative to the top-left of the window + pub fn location(&self) -> Vector { + self.location + } +} + +fn conv(ev: blinds::Event) -> Option { + use Event::*; + Some(match ev { + blinds::Event::Resized(x) => Resized(ResizedEvent { + size: x.logical_size().into(), + }), + blinds::Event::ScaleFactorChanged(x) => ScaleFactorChanged(x), + blinds::Event::FocusChanged(x) => FocusChanged(x), + blinds::Event::ReceivedCharacter(x) => ReceivedCharacter(x), + blinds::Event::KeyboardInput(x) => KeyboardInput(x), + blinds::Event::PointerEntered(x) => PointerEntered(x), + blinds::Event::PointerLeft(x) => PointerLeft(x), + blinds::Event::PointerMoved(x) => PointerMoved(PointerMovedEvent { + id: *x.pointer(), + location: x.location().into(), + }), + blinds::Event::PointerInput(x) => PointerInput(x), + blinds::Event::ScrollInput(x) => ScrollInput(x), + blinds::Event::ModifiersChanged(x) => ModifiersChanged(x), + blinds::Event::GamepadConnected(x) => GamepadConnected(x), + blinds::Event::GamepadDisconnected(x) => GamepadDisconnected(x), + blinds::Event::GamepadButton(x) => GamepadButton(x), + blinds::Event::GamepadAxis(x) => GamepadAxis(x), + _ => return None, + }) +} diff --git a/src/lib.rs b/src/lib.rs index f8eefc3c..611aecd5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,18 +26,16 @@ //! ```no_run //! // Example 1: The Square //! // Open a window, and draw a colored square in it -//! use mint::Vector2; //! use quicksilver::{ //! geom::{Rectangle, Vector}, //! graphics::{Color, Graphics}, -//! lifecycle::{run, EventStream, Settings, Window}, -//! Result, +//! input::{Input, Window}, +//! Result, Settings, run, //! }; //! //! fn main() { //! run( //! Settings { -//! size: Vector2 { x: 800.0, y: 600.0 }, //! title: "Square Example", //! ..Settings::default() //! }, @@ -45,7 +43,7 @@ //! ); //! } //! -//! async fn app(window: Window, mut gfx: Graphics, mut events: EventStream) -> Result<()> { +//! async fn app(window: Window, mut gfx: Graphics, mut input: Input) -> Result<()> { //! // Clear the screen to a blank, white color //! gfx.clear(Color::WHITE); //! // Paint a blue square with a red outline in the center of our screen @@ -56,7 +54,7 @@ //! // Send the data to be drawn //! gfx.present(&window)?; //! loop { -//! while let Some(_) = events.next_event().await {} +//! while let Some(_) = input.next_event().await {} //! } //! } //! ``` @@ -154,7 +152,7 @@ mod error; pub mod geom; pub mod graphics; -pub mod lifecycle; +pub mod input; #[cfg(feature = "saving")] pub mod saving { //! A module to manage cross-platform save data via the [`gestalt`] library @@ -162,8 +160,16 @@ pub mod saving { } pub use crate::error::QuicksilverError; +mod run; mod timer; +mod window; +pub use blinds::CursorIcon; +pub use run::{run, Settings}; pub use timer::Timer; +pub use window::Window; + +pub use graphics::Graphics; +pub use input::Input; /// Load a file as a [`Future`] /// diff --git a/src/lifecycle.rs b/src/lifecycle.rs deleted file mode 100644 index bb7f5743..00000000 --- a/src/lifecycle.rs +++ /dev/null @@ -1,67 +0,0 @@ -//! Manage events, input, and the window via the [`blinds`] library -//! -//! The [`run`] function is the entry point for all applications -use crate::geom::{Rectangle, Transform}; -use crate::graphics::Graphics; -use std::error::Error; -use std::future::Future; - -pub use blinds::event; -#[cfg(feature = "event-cache")] -pub use blinds::{CachedEventStream, EventCache}; -pub use blinds::{ - CursorIcon, Event, EventStream, GamepadAxis, GamepadButton, GamepadId, Key, MouseButton, - PointerId, Settings, Window, -}; - -/// The entry point of a Quicksilver application -/// -/// It provides your application (represented by an async closure or function) with a [`Window`], -/// [`Graphics`] context, and [`EventStream`]. -pub fn run(settings: Settings, app: F) -> ! -where - E: Into>, - T: 'static + Future>, - F: 'static + FnOnce(Window, Graphics, EventStream) -> T, -{ - #[cfg(feature = "easy-log")] - set_logger(); - - let size = settings.size; - let screen_region = Rectangle::new_sized(size); - - blinds::run_gl(settings, move |window, ctx, events| { - #[cfg(not(target_arch = "wasm32"))] - { - if std::env::set_current_dir("static").is_err() { - log::warn!("Warning: no asset directory found. Please place all your assets inside a directory called 'static' so they can be loaded"); - log::warn!("Execution continuing, but any asset-not-found errors are likely due to the lack of a 'static' directory.") - } - } - - let ctx = golem::Context::from_glow(ctx).unwrap(); - let mut graphics = Graphics::new(ctx).unwrap(); - graphics.set_projection(Transform::orthographic(screen_region)); - - async { - match app(window, graphics, events).await { - Ok(()) => log::info!("Exited successfully"), - Err(err) => { - let err = err.into(); - log::error!("Error: {:?}", err); - panic!("{:?}", err); - } - } - } - }); -} - -#[cfg(feature = "easy-log")] -fn set_logger() { - #[cfg(target_arch = "wasm32")] - web_logger::custom_init(web_logger::Config { - level: log::Level::Debug, - }); - #[cfg(not(target_arch = "wasm32"))] - simple_logger::init_with_level(log::Level::Debug).expect("A logger was already initialized"); -} diff --git a/src/run.rs b/src/run.rs new file mode 100644 index 00000000..330d9c59 --- /dev/null +++ b/src/run.rs @@ -0,0 +1,144 @@ +use crate::geom::{Rectangle, Transform, Vector}; +use crate::graphics::Graphics; +use crate::input::Input; +use std::error::Error; +use std::future::Future; + +/// Initial window and behavior options +pub struct Settings { + /// The size of the window + pub size: Vector, + /// If the cursor should be visible over the application, or if the cursor should be hidden + pub cursor_icon: Option, + /// If the application should be fullscreen + pub fullscreen: bool, + /// The icon on the window or the favicon on the tab + pub icon_path: Option<&'static str>, + /// How many samples to do for MSAA + /// + /// By default it is None; if it is Some, it should be a non-zero power of two + /// + /// Does nothing on web currently + pub multisampling: Option, + /// Enable or disable vertical sync + /// + /// Does nothing on web + pub vsync: bool, + /// If the window can be resized by the user + /// + /// Does nothing on web + pub resizable: bool, + /// The title of your application + pub title: &'static str, + /// The severity level of logs to show + /// + /// By default, it is set to Warn + pub log_level: log::Level, + /// On desktop, whether to assume the assets are in the 'static/' directory + /// + /// By default, this is on for comfortable parity between stdweb and desktop. If you know you + /// don't need that, feel free to toggle this off + pub use_static_dir: bool, +} + +impl Default for Settings { + fn default() -> Settings { + Settings { + size: Vector::new(1024.0, 768.0), + cursor_icon: Some(blinds::CursorIcon::Default), + fullscreen: false, + icon_path: None, + multisampling: None, + vsync: true, + resizable: false, + title: "", + log_level: log::Level::Warn, + use_static_dir: true, + } + } +} + +/// The entry point of a Quicksilver application +/// +/// It provides your application (represented by an async closure or function) with a [`Window`], +/// [`Graphics`] context, and [`Input`]. +/// +/// [`Graphics`]: crate::Graphics +/// [`Window`]: crate::Window +/// [`Input`]: crate::Input +pub fn run(settings: Settings, app: F) -> ! +where + E: Into>, + T: 'static + Future>, + F: 'static + FnOnce(crate::Window, Graphics, Input) -> T, +{ + #[cfg(feature = "easy-log")] + set_logger(settings.log_level); + + let size = settings.size; + let screen_region = Rectangle::new_sized(size); + + blinds::run_gl((&settings).into(), move |window, ctx, events| { + #[cfg(not(target_arch = "wasm32"))] + { + if settings.use_static_dir && std::env::set_current_dir("static").is_err() { + log::warn!("Warning: no asset directory found. Please place all your assets inside a directory called 'static' so they can be loaded"); + log::warn!("Execution continuing, but any asset-not-found errors are likely due to the lack of a 'static' directory.") + } + } + + let ctx = golem::Context::from_glow(ctx).unwrap(); + let mut graphics = Graphics::new(ctx).unwrap(); + graphics.set_projection(Transform::orthographic(screen_region)); + + async { + match app(crate::Window(window), graphics, Input::new(events)).await { + Ok(()) => log::info!("Exited successfully"), + Err(err) => { + let err = err.into(); + log::error!("Error: {:?}", err); + panic!("{:?}", err); + } + } + } + }); +} + +#[cfg(feature = "easy-log")] +fn set_logger(level: log::Level) { + #[cfg(target_arch = "wasm32")] + web_logger::custom_init(web_logger::Config { level }); + #[cfg(not(target_arch = "wasm32"))] + simple_logger::init_with_level(level).expect("A logger was already initialized"); +} + +impl From<&Settings> for blinds::Settings { + fn from(settings: &Settings) -> blinds::Settings { + blinds::Settings { + size: settings.size.into(), + cursor_icon: settings.cursor_icon, + fullscreen: settings.fullscreen, + icon_path: settings.icon_path, + multisampling: settings.multisampling, + vsync: settings.vsync, + resizable: settings.resizable, + title: settings.title, + } + } +} + +impl From for Settings { + fn from(settings: blinds::Settings) -> Settings { + Settings { + size: settings.size.into(), + cursor_icon: settings.cursor_icon, + fullscreen: settings.fullscreen, + icon_path: settings.icon_path, + multisampling: settings.multisampling, + vsync: settings.vsync, + resizable: settings.resizable, + title: settings.title, + ..Settings::default() + } + } +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 00000000..1090052f --- /dev/null +++ b/src/window.rs @@ -0,0 +1,63 @@ +use crate::geom::Vector; +use blinds::CursorIcon; + +/// The window on the user's desktop or in the browser tab +pub struct Window(pub(crate) blinds::Window); + +impl Window { + /// Set the cursor icon to some value, or set it to invisible (None) + pub fn set_cursor_icon(&self, icon: Option) { + self.0.set_cursor_icon(icon); + } + + /// Get the size of the window in logical units + /// + /// On a high-dpi display, this doesn't correspond to physical pixels and must be multiplied by + /// [`scale`] when passing sizes to functions like `glViewport`. + /// + /// [`scale`]: Window::scale_factor + pub fn size(&self) -> Vector { + self.0.size().into() + } + + /// Set the size of the inside of the window in logical units + pub fn set_size(&self, size: Vector) { + self.0.set_size(size.into()); + } + + /// Set the title of the window or browser tab + pub fn set_title(&self, title: &str) { + self.0.set_title(title); + } + + /// Set if the window should be fullscreen or not + /// + /// On desktop, it will instantly become fullscreen (borderless windowed on Windows and Linux, + /// and fullscreen on macOS). On web, it will become fullscreen after the next user + /// interaction, due to browser API restrictions. + pub fn set_fullscreen(&self, fullscreen: bool) { + self.0.set_fullscreen(fullscreen); + } + + /// The DPI scale factor of the window + /// + /// Mostly, this isn't important to you. Some computer screens have more "physical" pixels than + /// "logical" pixels, allowing them to draw sharper-looking images. Quicksilver abstracts this + /// away. However, if you are manually [`setting the viewport`], you need to take this into + /// account. + /// + /// + /// [`setting the viewport`]: crate::Graphics::set_viewport + pub fn scale_factor(&self) -> f32 { + self.0.scale_factor() + } + + /// Draw the current frame to the screen + /// + /// If vsync is enabled, this will block until the frame is completed on desktop. On web, there + /// is no way to control vsync, or to manually control presentation, so this function is a + /// no-op. + pub fn present(&self) { + self.0.present(); + } +}