diff --git a/CHANGELOG.md b/CHANGELOG.md index b96a337..bb31d4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Changelog +### Verion 0.20.0 + +### Breaking + +- You **MUST** pick either the `pixels` or `softbuffer` feature now + - Previously this was using pixels only, so set to `pixels` and everything should work the exact same +- Add support for `softbuffer` +- Remove exact dep versions +- Add `set_mouse_cursor()` for TextField +- Add `&Window` as last param on `update()` methods + ### Version 0.19.1 - Draw submenus on the left if there's not enough room on the right diff --git a/Cargo.toml b/Cargo.toml index f59a52c..e531bfc 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "pixels-graphics-lib" -version = "0.19.1" +version = "0.20.0" edition = "2021" authors = ["Emma Britton "] -description = "Simple wrapper library around Pixels/Buffer Graphics" +description = "Simple pixel graphics and GUI library" license-file = "LICENSE" repository = "https://github.com/emmabritton/pixel-graphics-lib" readme = "README.md" @@ -18,27 +18,34 @@ sound = ["simple-game-utils/sound"] file_dialogs = ["directories"] controller_xinput = ["serde", "simple-game-utils/controller_xinput"] images = ["buffer-graphics-lib/image_loading"] -serde = ["dep:serde", "buffer-graphics-lib/serde", "simple-game-utils/serde", "winit/serde"] +serde = ["dep:serde", "buffer-graphics-lib/serde", "simple-game-utils/serde"] mint = ["buffer-graphics-lib/mint"] scenes = ["window_prefs"] embedded = ["buffer-graphics-lib/embedded"] notosans = ["buffer-graphics-lib/notosans"] +pixels = ["dep:pixels", "winit_29", "winit_input_helper"] +softbuffer = ["dep:softbuffer", "winit_30", "window_prefs"] +pixels_serde = ["pixels", "serde", "winit_29/serde"] +softbuffer_serde = ["softbuffer", "serde", "winit_30/serde"] [dependencies] -pixels = "0.13.0" -winit = { version = "0.29.15", features = ["rwh_05"] } -winit_input_helper = { version = "0.16.0" } -thiserror = "1.0.59" -serde = { version = "1.0.202", features = ["derive"], optional = true } -directories = { version = "5.0.1", optional = true } -buffer-graphics-lib = { version = "0.18.1", default-features = false } -rustc-hash = "2.0.0" -simple-game-utils = { version = "0.4.2", default-features = false } -log = "0.4.21" +screen_size = "0.1.0" +pixels = { version = "0.14.0", optional = true } +winit_29 = { package = "winit", version = "0.29", features = ["rwh_05"], optional = true } +winit_30 = { package = "winit", version = "0.30", features = ["rwh_06"], optional = true } +softbuffer = { version = "0.4", optional = true } +winit_input_helper = { version = "0.16", optional = true } +thiserror = "1.0" +serde = { version = "1.0", features = ["derive"], optional = true } +directories = { version = "5.0", optional = true } +buffer-graphics-lib = { version = "0.19.0", default-features = false } +rustc-hash = "2.0" +simple-game-utils = { version = "0.4", default-features = false } +log = "0.4" [dev-dependencies] -fastrand = "2.1.1" -anyhow = "1.0.86" +fastrand = "2.1" +anyhow = "1.0" [[example]] name = "test_dialogs" @@ -54,4 +61,4 @@ required-features = ["images"] [[example]] name = "pre_post_w_controller" -required-features = ["controller"] \ No newline at end of file +required-features = ["controller"] diff --git a/README.md b/README.md index e905289..cbb9d41 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ [![Crates.io](https://img.shields.io/crates/v/pixels-graphics-lib)](https://crates.io/crates/pixels-graphics-lib "Crates.io version") [![Documentation](https://img.shields.io/docsrs/pixels-graphics-lib)](https://docs.rs/pixels-graphics-lib "Documentation") -# Graphics Lib +# Pixels Graphics Lib -This is a simple wrapper around [Pixels](https://github.com/parasyte/pixels), designed to be used -with [Buffer Graphics Lib](https://github.com/emmabritton/buffer-graphics-lib) +Pixel buffer graphics and GUI library. It helps simplify window setup and creation, and event looping. +It uses [buffer graphics lib](https://github.com/emmabritton/buffer-graphics-lib) for drawing to the buffer. ## Usage @@ -13,10 +13,23 @@ with [Buffer Graphics Lib](https://github.com/emmabritton/buffer-graphics-lib) In your `Cargo.toml` file add ```toml -pixels-graphics-lib = "0.19.1" -winit_input_helper = "0.16.0" #only needed if you're not using `run()` +pixels-graphics-lib = { version = "0.20", features = [] } ``` +Inside `features` you **MUST** put one of these: + +| Feature | Renderer | Window creation | +|--------------|------------------------------------------------------------|--------------------------------------------------------| +| `pixels` | [Pixels](https://github.com/parasyte/pixels) | [Winit](https://github.com/rust-windowing/winit) v0.29 | +| `softbuffer` | [Softbuffer](https://github.com/rust-windowing/softbuffer) | [Winit](https://github.com/rust-windowing/winit) v0.30 | + +Both of these use `rwh06` + +This will control how the window is created and managed and how the buffer is rendered to the screen. The main +differences are when the window is scaled to a non integer value (1.2 opposed than 2.0) then pixels will draw your +content in the middle of the window, whereas softbuffer will draw in the top left. Additionally, pixels uses hardware +scaling and softbuffer uses software scaling. + ### Code You can use scenes using `run_scenes` (requires default feature `scenes`): @@ -145,7 +158,32 @@ Built in file selection dialogs, not recommended, use `rfd` ### `mint` -Enables `graphic-shapes/mint` +Enables `buffer-graphics-lib/mint`, +see [Buffer graphics readme](https://github.com/emmabritton/buffer-graphics-lib?tab=readme-ov-file#features) + +### `notosan` + +Enables `buffer-graphics-lib/notosans`, +see [Buffer graphics readme](https://github.com/emmabritton/buffer-graphics-lib?tab=readme-ov-file#features) + +### `embedded` + +Enables `buffer-graphics-lib/embedded`, +see [Buffer graphics readme](https://github.com/emmabritton/buffer-graphics-lib?tab=readme-ov-file#features) + +### `pixels_serde` and `softbuffer_serde` + +Enables `serde` for the `winit` crate being used by `pixels` or `softbuffer` + +## Examples + +Each example must be run with a renderer (pixels or softbuffer), like this: + +`cargo run --example basic --features "pixels"` + +or + +`cargo run --example relative_test --features "softbuffer"` ## Projects @@ -153,13 +191,17 @@ Enables `graphic-shapes/mint` A few retro games +### [Wordle](https://github.com/emmabritton/wordle) + +A wordle clone + ### [ICI Image editor](https://github.com/emmabritton/ici-image-editor) Editor for `IndexedImage`, ICI files ### [USFX Tester](https://github.com/emmabritton/uxfs-test) -Test GUI for [USFX](https://github.com/tversteeg/usfx) +GUI for [USFX](https://github.com/tversteeg/usfx) ### [Fontpad](https://github.com/emmabritton/fontpad) diff --git a/examples/basic.rs b/examples/basic.rs index 2832235..2cbacc3 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -1,6 +1,7 @@ use anyhow::Result; use pixels_graphics_lib::prelude::*; use winit::keyboard::KeyCode; +use winit::window::Window; /// This example shows the minimum code needed to use the library @@ -25,7 +26,7 @@ impl Basic { } impl System for Basic { - fn update(&mut self, _delta: &Timing) { + fn update(&mut self, _delta: &Timing, _: &Window) { if self.greyscale < 255 { self.greyscale += 1; } else { diff --git a/examples/images.rs b/examples/images.rs index b205433..3929423 100644 --- a/examples/images.rs +++ b/examples/images.rs @@ -80,7 +80,7 @@ impl ImageScene { } impl System for ImageScene { - fn update(&mut self, timing: &Timing) { + fn update(&mut self, timing: &Timing, _: &Window) { let sw = self.width; let sh = self.height; diff --git a/examples/layout_test.rs b/examples/layout_test.rs index feb5503..04a55fc 100644 --- a/examples/layout_test.rs +++ b/examples/layout_test.rs @@ -218,6 +218,7 @@ impl Scene for LayoutTest { timing: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.text_field.update(timing); self.spacing.update(timing); diff --git a/examples/menu_bar_test.rs b/examples/menu_bar_test.rs index 2c22bfa..06bb43b 100644 --- a/examples/menu_bar_test.rs +++ b/examples/menu_bar_test.rs @@ -123,6 +123,7 @@ impl Scene for MenuTest { _: &Timing, mouse: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.menubar.on_mouse_move(mouse.xy); Nothing diff --git a/examples/pre_post.rs b/examples/pre_post.rs index 8c02fe3..54b7c87 100644 --- a/examples/pre_post.rs +++ b/examples/pre_post.rs @@ -1,6 +1,8 @@ use pixels_graphics_lib::prelude::*; use pixels_graphics_lib::scenes::SceneUpdateResult::Nothing; use pixels_graphics_lib::ui::prelude::*; +use winit::keyboard::KeyCode; +use winit::window::Window; #[derive(Debug, Clone, PartialEq)] enum SR {} @@ -34,6 +36,7 @@ impl Scene for WhiteTextScene { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } @@ -84,6 +87,7 @@ impl PrePost for ExtrasImpl { _: &MouseData, _: &FxHashSet, _: &mut [Box>], + _: &Window, ) { } @@ -93,6 +97,7 @@ impl PrePost for ExtrasImpl { _: &MouseData, _: &FxHashSet, _: &mut [Box>], + _: &Window, ) { if self.timer.update(timing) { self.pixel.x += 1; diff --git a/examples/pre_post_w_controller.rs b/examples/pre_post_w_controller.rs index 98ea973..25a553f 100644 --- a/examples/pre_post_w_controller.rs +++ b/examples/pre_post_w_controller.rs @@ -41,6 +41,7 @@ impl Scene for WhiteTextScene { _: &MouseData, _: &FxHashSet, _: &GameController, + _: &Window, ) -> SceneUpdateResult { Nothing } @@ -94,6 +95,7 @@ impl PrePost for ExtrasImpl { _: &FxHashSet, _: &mut [Box>], _: &GameController, + _: &Window, ) { } @@ -104,6 +106,7 @@ impl PrePost for ExtrasImpl { _: &FxHashSet, _: &mut [Box>], _: &GameController, + _: &Window, ) { if self.timer.update(timing) { self.pixel.x += 1; @@ -115,7 +118,7 @@ impl PrePost for ExtrasImpl { } fn main() { - let switcher = |style: &UiStyle, scenes: &mut Vec>>, new_scene: SN| {}; + let switcher = |_: &UiStyle, _: &mut Vec>>, _: SN| {}; run_scenes( 100, 100, diff --git a/examples/relative_test.rs b/examples/relative_test.rs index 607ba3d..ddd6e7f 100644 --- a/examples/relative_test.rs +++ b/examples/relative_test.rs @@ -205,6 +205,7 @@ impl Scene for LayoutTest { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } diff --git a/examples/relative_test2.rs b/examples/relative_test2.rs index 3e29a29..8af69c4 100644 --- a/examples/relative_test2.rs +++ b/examples/relative_test2.rs @@ -137,6 +137,7 @@ impl Scene for LayoutTest { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } diff --git a/examples/relative_test3.rs b/examples/relative_test3.rs index 23409ee..6358cb9 100644 --- a/examples/relative_test3.rs +++ b/examples/relative_test3.rs @@ -157,6 +157,7 @@ impl Scene for LayoutTest { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } diff --git a/examples/relative_test4.rs b/examples/relative_test4.rs index df293f1..30c9063 100644 --- a/examples/relative_test4.rs +++ b/examples/relative_test4.rs @@ -118,6 +118,7 @@ impl Scene for LayoutTest { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } diff --git a/examples/relative_test5.rs b/examples/relative_test5.rs index 512c481..2c08c79 100644 --- a/examples/relative_test5.rs +++ b/examples/relative_test5.rs @@ -100,6 +100,7 @@ impl Scene for LayoutTest { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { Nothing } diff --git a/examples/scenes.rs b/examples/scenes.rs index 64c827c..4ec2439 100644 --- a/examples/scenes.rs +++ b/examples/scenes.rs @@ -74,6 +74,7 @@ impl Scene for Scene1 { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.result.clone() } @@ -114,6 +115,7 @@ impl Scene for Scene2 { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.result.clone() } @@ -140,6 +142,7 @@ impl Scene for Scene3 { _: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.result.clone() } diff --git a/examples/ui_test.rs b/examples/ui_test.rs index bccfe64..3f0e320 100644 --- a/examples/ui_test.rs +++ b/examples/ui_test.rs @@ -20,7 +20,7 @@ fn main() -> Result<()> { WIDTH, HEIGHT, "UI Tester", - Some(WindowPreferences::new("app", "emmabritton", "pixels_ui_tester", 1).unwrap()), + Some(WindowPreferences::new("app", "emmabritton", "pixels_ui_tester", 5).unwrap()), switcher, menu, options, @@ -209,6 +209,7 @@ impl Scene for Menu { timing: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.field1.update(timing); self.field2.update(timing); diff --git a/examples/window_pos.rs b/examples/window_pos.rs index 7774669..9db82ac 100644 --- a/examples/window_pos.rs +++ b/examples/window_pos.rs @@ -52,7 +52,7 @@ impl System for WindowPrefsScene { ) } - fn update(&mut self, _delta: &Timing) { + fn update(&mut self, _delta: &Timing, _: &Window) { if self.idx < self.colors.len() - 1 { self.idx += 1; } else { diff --git a/src/dialogs/load_file_dialog.rs b/src/dialogs/load_file_dialog.rs index 942436d..eff7d87 100644 --- a/src/dialogs/load_file_dialog.rs +++ b/src/dialogs/load_file_dialog.rs @@ -7,6 +7,7 @@ use crate::*; use buffer_graphics_lib::prelude::*; use directories::UserDirs; use std::fmt::Debug; +use winit::window::Window; /// You should use something like `rfd` instead of this #[derive(Debug)] @@ -234,6 +235,7 @@ where _: &MouseData, _: &FxHashSet, _: &GameController, + _: &Window, ) -> SceneUpdateResult { self.update(timing) } @@ -244,6 +246,7 @@ where timing: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.update(timing) } diff --git a/src/dialogs/save_file_dialog.rs b/src/dialogs/save_file_dialog.rs index 186ddff..677bcc8 100644 --- a/src/dialogs/save_file_dialog.rs +++ b/src/dialogs/save_file_dialog.rs @@ -6,6 +6,7 @@ use buffer_graphics_lib::prelude::*; use directories::UserDirs; use std::fmt::Debug; use std::path::PathBuf; +use winit::window::Window; /// You should use something like `rfd` instead of this #[derive(Debug)] @@ -261,6 +262,7 @@ where _: &MouseData, _: &FxHashSet, _: &GameController, + _: &Window, ) -> SceneUpdateResult { self.update(timing) } @@ -271,6 +273,7 @@ where timing: &Timing, _: &MouseData, _: &FxHashSet, + _: &Window, ) -> SceneUpdateResult { self.update(timing) } diff --git a/src/integration/mod.rs b/src/integration/mod.rs new file mode 100644 index 0000000..6e1219d --- /dev/null +++ b/src/integration/mod.rs @@ -0,0 +1,6 @@ +#[cfg(feature = "pixels")] +pub mod pixels_winit; +#[cfg(feature = "softbuffer")] +pub mod softbuffer_winit; +#[cfg(feature = "softbuffer")] +mod winit_app; diff --git a/src/integration/pixels_winit.rs b/src/integration/pixels_winit.rs new file mode 100644 index 0000000..338fe07 --- /dev/null +++ b/src/integration/pixels_winit.rs @@ -0,0 +1,261 @@ +use crate::prelude::*; +use crate::GraphicsError::LoadingWindowPref; +use crate::{GraphicsError, MouseData, Options, System}; +use buffer_graphics_lib::Graphics; +use pixels::PixelsBuilder; +use pixels::{Pixels, SurfaceTexture}; +use simple_game_utils::prelude::Timing; +use winit::dpi::LogicalSize; +use winit::dpi::PhysicalSize; +use winit::event::{Event, MouseButton, WindowEvent}; +use winit::event_loop::EventLoop; +use winit::window::CursorGrabMode; +use winit::window::WindowBuilder; +use winit_input_helper::WinitInputHelper; + +fn create_window( + size: (u32, u32), + title: &str, + scale: WindowScaling, + event_loop: &EventLoop<()>, +) -> Result { + 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::Native => window.scale_factor(), + WindowScaling::Double => window.scale_factor() + 2.0, + WindowScaling::Quad => window.scale_factor() + 4.0, + }; + + let px_size: PhysicalSize = LogicalSize::new(size.0, size.1).to_physical(factor); + + window.set_min_inner_size(Some(px_size)); + let _ = window.request_inner_size(px_size); + window.set_visible(true); + + Ok(window) +} + +/// Creates the window and pixels wrapper +/// +/// The inner size mentioned in the arguments refers to the size of the area available to draw in, it doesn't include the window frame, etc +/// +/// This uses logical pixels, where on a low DPI screen each library pixel is one display pixel but on higher DPI screens (and if +/// `scale` != `None`) then a library pixel will be represented by multiple display pixels +/// +/// # Arguments +/// +/// * `canvas_size` - Inner width and height of window in logical pixels +/// * `options` - Scaling, UPS, etc options +/// * `title` - Title for window +/// * `event_loop` - Provided by `EventLoop::new()`, this allows the window to receive events from the OS +/// +/// # Example +/// +/// This creates a 160x160 window: +/// +/// `let (mut window, graphics) = setup(160, 160, "Example", true, &event_loop)?;` +/// +/// # Returns +/// +/// A result with a pair of Window and PixelsWrapper +/// +/// # Errors +/// +/// * `WindowInit` - If the window can not be created +fn setup( + canvas_size: (u32, u32), + 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)) +} + +/// Create and run a loop using Pixels and Winit +/// +/// If you want to use [Scene][scenes::Scene]s consider [run_scenes][scenes::run_scenes] +/// +/// # Arguments +/// * `width` - Width of the whole window canvas in pixels +/// * `height` - Height of the whole window canvas in pixels +/// * `title` - Window title +/// * `system` - Your program +/// * `options` - [Options] controls how fast the program can update, [UiElement] styling, etc +/// +/// # Returns +/// +/// Returns when the program is finished executing either due to it quitting or a fatal error occurring +pub fn run( + width: usize, + height: usize, + title: &str, + mut system: Box, + 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 as u32, height as u32), &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); + //can't return from here so just print out error + 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_u8_rgba(pixels.frame_mut(), width, height) + .expect("Creating graphics wrapper"); + 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, &mut window); + 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(MouseButton::Left) { + mouse.add_down(mouse.xy, MouseButton::Left); + system.on_mouse_down(&mouse, MouseButton::Left); + } + if input.mouse_pressed(MouseButton::Right) { + mouse.add_down(mouse.xy, MouseButton::Right); + system.on_mouse_down(&mouse, MouseButton::Right); + } + if input.mouse_pressed(MouseButton::Middle) { + mouse.add_down(mouse.xy, MouseButton::Middle); + system.on_mouse_down(&mouse, MouseButton::Middle); + } + + if input.mouse_released(MouseButton::Left) { + mouse.add_up(MouseButton::Left); + system.on_mouse_up(&mouse, MouseButton::Left); + } + if input.mouse_released(MouseButton::Right) { + mouse.add_up(MouseButton::Right); + system.on_mouse_up(&mouse, MouseButton::Right); + } + if input.mouse_released(MouseButton::Middle) { + 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(()) +} diff --git a/src/integration/softbuffer_winit.rs b/src/integration/softbuffer_winit.rs new file mode 100644 index 0000000..63b03ad --- /dev/null +++ b/src/integration/softbuffer_winit.rs @@ -0,0 +1,203 @@ +use crate::integration::winit_app::{make_window, run_app, WinitAppBuilder}; +use crate::prelude::*; +use log::error; +use std::num::NonZeroU32; +use std::ops::Deref; +use winit::event::{ElementState, Event, KeyEvent, MouseScrollDelta, TouchPhase, WindowEvent}; +use winit::event_loop::ControlFlow; +use winit::event_loop::EventLoop; +use winit::keyboard::PhysicalKey; + +/// Create and run a loop using Softbuffer and Winit +/// +/// If you want to use [Scene][scenes::Scene]s consider [run_scenes][scenes::run_scenes] +/// +/// # Arguments +/// * `width` - Width of the whole window canvas in pixels +/// * `height` - Height of the whole window canvas in pixels +/// * `title` - Window title +/// * `system` - Your program +/// * `options` - [Options] controls how fast the program can update, [UiElement] styling, etc +/// +/// # Returns +/// +/// Returns when the program is finished executing either due to it quitting or a fatal error occurring +pub fn run( + width: usize, + height: usize, + title: &str, + system: Box, + options: Options, +) -> Result<(), GraphicsError> { + let event_loop = EventLoop::new().unwrap(); + let title = title.to_string(); + let app = WinitAppBuilder::new(system, options, move |elwt, system, options| { + elwt.set_control_flow(options.control_flow); + let (scale, window) = make_window(elwt, system, &options, width, height, title.clone()) + .expect("Window created"); + + let context = softbuffer::Context::new(window.clone()).unwrap(); + let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let size = window.inner_size(); + if let (Some(win_width), Some(win_height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + surface + .resize(win_width, win_height) + .expect("Resized softbuffer"); + } + (scale, window, surface) + }) + .setup(move |state, event, elwt, system, timing, mouse, options| { + let (scale, window, surface) = state; + + timing.update(); + timing.accumulated_time += timing.delta; + while timing.accumulated_time >= timing.fixed_time_step { + system.update(&timing, window.deref()); + timing.accumulated_time -= timing.fixed_time_step; + timing.updates += 1; + } + + if options.control_flow == ControlFlow::Poll { + if event == Event::AboutToWait { + window.request_redraw(); + } + } + + if let Event::WindowEvent { window_id, event } = event { + if window_id == window.id() { + match event { + WindowEvent::Resized(size) => { + if let (Some(win_width), Some(win_height)) = + (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) + { + surface + .resize(win_width, win_height) + .expect("Resized softbuffer"); + + let horz_scale = win_width.get() as usize / width; + let vert_scale = win_height.get() as usize / height; + let new_scale: usize = horz_scale.min(vert_scale); + *scale = new_scale as f64; + } + } + WindowEvent::CloseRequested => { + system.on_window_closed(); + #[cfg(feature = "window_prefs")] + if let Some(mut prefs) = system.window_prefs() { + prefs.store(window.deref()); + //can't return from here so just print out error + let _ = prefs + .save() + .map_err(|err| error!("Unable to save window size/pos: {err:?}")); + } + elwt.exit(); + } + WindowEvent::Occluded(hidden) => system.on_visibility_changed(!hidden), + WindowEvent::Focused(focused) => system.on_focus_changed(focused), + WindowEvent::KeyboardInput { + device_id: _device_id, + event, + is_synthetic: _is_synthetic, + } => match event { + KeyEvent { + physical_key, + state, + repeat, + .. + } => { + if let PhysicalKey::Code(keycode) = physical_key { + match state { + ElementState::Pressed => { + if !repeat { + system.on_key_down(vec![keycode]) + } + } + ElementState::Released => system.on_key_up(vec![keycode]), + } + } + } + }, + WindowEvent::RedrawRequested => { + let mut buffer = surface.buffer_mut().expect("Accessing softbuffer buffer"); + let mut drawing_buffer = Graphics::create_buffer_u32(width, height); + let mut drawing_graphics = + Graphics::new_u32_argb(&mut drawing_buffer, width, height) + .expect("Graphics creation"); + system.render(&mut drawing_graphics); + let mut image = drawing_graphics.copy_to_image(); + if *scale > 1.0 { + let factor = scale.trunc() as usize; + image = image.scale( + Scaling::nearest_neighbour(factor, factor) + .expect("Invalid scaling"), + ); + } + let physical_size = window.inner_size(); + let mut graphics = Graphics::new_u32_argb( + &mut buffer, + physical_size.width as usize, + physical_size.height as usize, + ) + .expect("Graphics creation"); + graphics.draw_image((0, 0), &image); + timing.renders += 1; + buffer.present().expect("Softbuffer presented to screen"); + } + WindowEvent::MouseWheel { + device_id: _device_id, + delta, + phase, + } => { + if phase == TouchPhase::Moved { + match delta { + MouseScrollDelta::LineDelta(_, _) => {} + MouseScrollDelta::PixelDelta(pos) => { + system.on_scroll( + &mouse, + pos.x.round() as isize, + pos.y.round() as isize, + ); + } + } + } + } + WindowEvent::MouseInput { + device_id: _device_id, + state, + button, + } => match state { + ElementState::Pressed => { + mouse.add_down(mouse.xy, button); + system.on_mouse_down(&mouse, button); + } + ElementState::Released => { + mouse.add_up(button); + system.on_mouse_up(&mouse, button); + } + }, + WindowEvent::CursorMoved { + device_id: _device_id, + position, + } => { + mouse.xy = coord!(position.x, position.y) / *scale; + system.on_mouse_move(&mouse); + } + _ => {} + } + } + } + + if system.should_exit() { + elwt.exit(); + } + + timing.update_fps(); + + timing.last = timing.now; + }); + + run_app(event_loop, app).map_err(GraphicsError::WinitInit)?; + Ok(()) +} diff --git a/src/integration/winit_app.rs b/src/integration/winit_app.rs new file mode 100644 index 0000000..5c5bb03 --- /dev/null +++ b/src/integration/winit_app.rs @@ -0,0 +1,214 @@ +use crate::prelude::winit; +use crate::GraphicsError::LoadingWindowPref; +use crate::{GraphicsError, MouseData, Options, System, WindowScaling}; +use log::error; +use simple_game_utils::prelude::Timing; +use std::marker::PhantomData; +use std::rc::Rc; +use winit::application::ApplicationHandler; +use winit::dpi::LogicalSize; +use winit::dpi::PhysicalSize; +use winit::error::EventLoopError; +use winit::event::{Event, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, EventLoop}; +use winit::window::{CursorGrabMode, Window, WindowId}; + +pub(crate) fn make_window( + event_loop: &ActiveEventLoop, + system: &mut Box, + options: &Options, + width: usize, + height: usize, + title: String, +) -> Result<(f64, Rc), GraphicsError> { + let mut attr = Window::default_attributes(); + attr.title = title; + + let mut window: Window = event_loop + .create_window(attr) + .expect("Window created after resuming"); + let mut factor = match options.scaling { + WindowScaling::Native => window.scale_factor(), + WindowScaling::Double => window.scale_factor() + 2.0, + WindowScaling::Quad => window.scale_factor() + 4.0, + }; + let px_size: PhysicalSize = + LogicalSize::new(width as u32, height as u32).to_physical(factor); + + window.set_min_inner_size(Some(px_size)); + let _ = window.request_inner_size(px_size); + window.set_visible(true); + + 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() { + if let Err(e) = prefs.load().map_err(|e| LoadingWindowPref(e.to_string())) { + error!("Unable to restore window size/pos: {e:?}"); + } + prefs.restore(&mut window); + } + if window.inner_size() != px_size { + let horz_scale = window.inner_size().width as usize / width; + let vert_scale = window.inner_size().height as usize / height; + let new_scale: usize = horz_scale.min(vert_scale); + factor = new_scale as f64; + } + Ok((factor, Rc::new(window))) +} + +/// +/// Taken from https://raw.githubusercontent.com/rust-windowing/softbuffer/refs/heads/master/examples/utils/winit_app.rs +/// + +#[allow(unused_mut)] +pub(crate) fn run_app( + event_loop: EventLoop<()>, + mut app: impl ApplicationHandler<()> + 'static, +) -> Result<(), EventLoopError> { + event_loop.run_app(&mut app) +} + +pub(crate) struct WinitApp { + init: Init, + + event: Handler, + + state: Option, + + system: Box, + options: Options, + timing: Timing, + mouse: MouseData, +} + +pub(crate) struct WinitAppBuilder { + init: Init, + _marker: PhantomData>, + system: Box, + options: Options, +} + +impl WinitAppBuilder +where + Init: FnMut(&ActiveEventLoop, &mut Box, &Options) -> T, +{ + pub fn new(system: Box, options: Options, init: Init) -> Self { + Self { + init, + system, + options, + _marker: PhantomData, + } + } + + pub fn setup(self, handler: F) -> WinitApp + where + F: FnMut( + &mut T, + Event<()>, + &ActiveEventLoop, + &mut Box, + &mut Timing, + &mut MouseData, + &Options, + ), + { + WinitApp::new(self.init, handler, self.system, self.options) + } +} + +impl WinitApp +where + Init: FnMut(&ActiveEventLoop, &mut Box, &Options) -> T, + Handler: FnMut( + &mut T, + Event<()>, + &ActiveEventLoop, + &mut Box, + &mut Timing, + &mut MouseData, + &Options, + ), +{ + pub(crate) fn new( + init: Init, + event: Handler, + system: Box, + options: Options, + ) -> Self { + Self { + init, + event, + system, + timing: Timing::new(options.ups), + mouse: MouseData::default(), + options, + state: None, + } + } +} + +impl ApplicationHandler for WinitApp +where + Init: FnMut(&ActiveEventLoop, &mut Box, &Options) -> T, + Handler: FnMut( + &mut T, + Event<()>, + &ActiveEventLoop, + &mut Box, + &mut Timing, + &mut MouseData, + &Options, + ), +{ + fn resumed(&mut self, el: &ActiveEventLoop) { + debug_assert!(self.state.is_none()); + self.state = Some((self.init)(el, &mut self.system, &self.options)); + } + + fn window_event( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + event: WindowEvent, + ) { + let state = self.state.as_mut().unwrap(); + (self.event)( + state, + Event::WindowEvent { window_id, event }, + event_loop, + &mut self.system, + &mut self.timing, + &mut self.mouse, + &self.options, + ); + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + if let Some(state) = self.state.as_mut() { + (self.event)( + state, + Event::AboutToWait, + event_loop, + &mut self.system, + &mut self.timing, + &mut self.mouse, + &self.options, + ); + } + } + + fn suspended(&mut self, _event_loop: &ActiveEventLoop) { + let state = self.state.take(); + debug_assert!(state.is_some()); + drop(state); + } +} diff --git a/src/lib.rs b/src/lib.rs index 8e5af4c..c5f2dee 100755 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,6 @@ //! Rust Graphics Lib //! -//! This is a simple wrapper around [`Pixels`](https://github.com/parasyte/pixels), it provides basic shape, image and text rendering. +//! This is a simple pixel graphics and GUI library, it provides basic shape, image and text rendering. //! //! This boilerplate code is needed to use it: //! @@ -14,7 +14,7 @@ //! } //! //! impl System for Example { -//! fn update(&mut self, delta: &Timing) { +//! fn update(&mut self, timing: &Timing, _: &Window) { //! //! } //! @@ -30,7 +30,11 @@ //! } //!``` +#[cfg(all(not(feature = "pixels"), not(feature = "softbuffer"),))] +compile_error!("You must pick one windowing feature either pixels or softbuffer"); + pub mod dialogs; +mod integration; #[cfg(feature = "scenes")] pub mod scenes; pub mod ui; @@ -38,33 +42,31 @@ pub mod utilities; #[cfg(feature = "window_prefs")] pub mod window_prefs; -use crate::prelude::{coord, Coord, ALL_KEYS}; +use crate::prelude::{winit, 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, MouseButton, WindowEvent}; -use winit::event_loop::EventLoop; +use winit::event::MouseButton; +#[cfg(feature = "softbuffer")] +pub use winit::event_loop::ControlFlow; use winit::keyboard::KeyCode; -use winit::window::{CursorGrabMode, Window, WindowBuilder}; -use winit_input_helper::WinitInputHelper; +use winit::window::Window; pub mod prelude { pub use crate::dialogs::*; - pub use crate::run; + #[cfg(feature = "pixels")] + pub use crate::integration::pixels_winit::run; + #[cfg(feature = "softbuffer")] + pub use crate::integration::softbuffer_winit::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::*; @@ -78,12 +80,18 @@ pub mod prelude { pub use simple_game_utils::prelude::*; pub use winit::event::MouseButton; pub use winit::keyboard::KeyCode; + pub use winit::window::Window; + #[cfg(feature = "pixels")] + pub use winit_29 as winit; + #[cfg(feature = "softbuffer")] + pub use winit_30 as winit; } #[derive(Error, Debug)] pub enum GraphicsError { #[error("Creating a window: {0}")] WindowInit(String), + #[cfg(any(feature = "pixels"))] #[error("Initialising Pixels: {0}")] PixelsInit(#[source] pixels::Error), #[error("Saving window pref: {0}")] @@ -98,103 +106,20 @@ pub enum GraphicsError { #[cfg(feature = "controller")] #[error("Unable to init controller: {0}")] ControllerInit(String), -} - -/// Creates the window and pixels wrapper -/// -/// The inner size mentioned in the arguments refers to the size of the area available to draw in, it doesn't include the window frame, etc -/// -/// This uses logical pixels, where on a low DPI screen each library pixel is one display pixel but on higher DPI screens (and if -/// `scale` != `None`) then a library pixel will be represented by multiple display pixels -/// -/// # Arguments -/// -/// * `canvas_size` - Inner width and height of window in logical pixels -/// * `options` - Scaling, UPS, etc options -/// * `title` - Title for window -/// * `event_loop` - Provided by `EventLoop::new()`, this allows the window to receive events from the OS -/// -/// # Example -/// -/// This creates a 160x160 window: -/// -/// `let (mut window, graphics) = setup(160, 160, "Example", true, &event_loop)?;` -/// -/// # Returns -/// -/// A result with a pair of Window and PixelsWrapper -/// -/// # Errors -/// -/// * `WindowInit` - If the window can not be created -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(any(feature = "pixels", feature = "softbuffer"))] + #[error("Initialing Winit: {0}")] + WinitInit(#[source] winit::error::EventLoopError), } #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)] pub enum WindowScaling { - /// Make the canvas and window be the same of numbers, this ignores DPI - None, - /// Scale the window to account for DPI - Auto, - /// Scale the window by a fixed amount, ignoring DPI - Fixed(usize), - /// Scale the window by a fixed amount and by DPI - /// So, if the monitor DPI is 2x and 2x is passed then the result will be 4x - AutoFixed(usize), -} - -fn create_window( - size: (usize, usize), - title: &str, - scale: WindowScaling, - event_loop: &EventLoop<()>, -) -> Result { - 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) + /// Use system DPI + Native, + /// Use system DPI + 2 + Double, + /// Use system DPI + 4 + Quad, } #[allow(unused_variables)] @@ -207,7 +132,7 @@ pub trait System { fn window_prefs(&mut self) -> Option { None } - fn update(&mut self, timing: &Timing); + fn update(&mut self, timing: &Timing, window: &Window); 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) {} @@ -224,7 +149,8 @@ pub trait System { } /// Options for program windows -#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "pixels_serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "softbuffer_serde", derive(Serialize, Deserialize))] #[derive(Debug, PartialEq)] pub struct Options { /// Target and max number of times [Scene::update] can be called per second @@ -245,6 +171,9 @@ pub struct Options { pub confine_cursor: bool, /// Style data for [UiElement] pub style: UiStyle, + /// Control how the program loops, see [Winit ControlFlow](https://docs.rs/winit/latest/winit/event_loop/enum.ControlFlow.html) + #[cfg(feature = "softbuffer")] + pub control_flow: ControlFlow, } impl Options { @@ -255,6 +184,7 @@ impl Options { hide_cursor: bool, confine_cursor: bool, style: UiStyle, + #[cfg(feature = "softbuffer")] control_flow: ControlFlow, ) -> Self { Self { ups, @@ -263,6 +193,8 @@ impl Options { hide_cursor, confine_cursor, style, + #[cfg(feature = "softbuffer")] + control_flow, } } } @@ -271,190 +203,19 @@ impl Default for Options { fn default() -> Self { Self { ups: 240, - scaling: WindowScaling::Auto, + scaling: WindowScaling::Double, vsync: true, hide_cursor: false, confine_cursor: false, style: UiStyle::default(), + #[cfg(feature = "softbuffer")] + control_flow: ControlFlow::Poll, } } } -/// Helper method that sets up the screen and runs the loop -/// -/// If you want to use [Scene][scenes::Scene]s consider [run_scenes][scenes::run_scenes] -/// -/// # Arguments -/// * `width` - Width of the whole window canvas in pixels -/// * `height` - Height of the whole window canvas in pixels -/// * `title` - Window title -/// * `system` - Your program -/// * `options` - [Options] controls how fast the program can update, [UiElement] styling, etc -pub fn run( - width: usize, - height: usize, - title: &str, - mut system: Box, - 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); - //can't return from here so just print out error - 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(MouseButton::Left) { - mouse.add_down(mouse.xy, MouseButton::Left); - system.on_mouse_down(&mouse, MouseButton::Left); - } - if input.mouse_pressed(MouseButton::Right) { - mouse.add_down(mouse.xy, MouseButton::Right); - system.on_mouse_down(&mouse, MouseButton::Right); - } - if input.mouse_pressed(MouseButton::Middle) { - mouse.add_down(mouse.xy, MouseButton::Middle); - system.on_mouse_down(&mouse, MouseButton::Middle); - } - - if input.mouse_released(MouseButton::Left) { - mouse.add_up(MouseButton::Left); - system.on_mouse_up(&mouse, MouseButton::Left); - } - if input.mouse_released(MouseButton::Right) { - mouse.add_up(MouseButton::Right); - system.on_mouse_up(&mouse, MouseButton::Right); - } - if input.mouse_released(MouseButton::Middle) { - 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))] +#[cfg_attr(feature = "pixels_serde", derive(Serialize, Deserialize))] +#[cfg_attr(feature = "softbuffer_serde", derive(Serialize, Deserialize))] #[derive(Debug, Clone, Eq, PartialEq, Default)] pub struct MouseData { pub xy: Coord, @@ -463,9 +224,7 @@ pub struct MouseData { 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) + !self.buttons.is_empty() } /// Returns the press location if the mouse button is currently held down diff --git a/src/scenes.rs b/src/scenes.rs index 1e7877f..ee0449f 100644 --- a/src/scenes.rs +++ b/src/scenes.rs @@ -1,10 +1,10 @@ use crate::prelude::*; use crate::ui::styles::UiStyle; -use crate::GraphicsError; -use buffer_graphics_lib::prelude::*; use rustc_hash::{FxHashMap, FxHashSet}; use std::fmt::Debug; use winit::event::MouseButton; +use winit::keyboard::KeyCode; +use winit::window::Window; /// Convenience method for programs built using [Scene]s /// @@ -217,6 +217,7 @@ pub trait Scene { mouse: &MouseData, held_keys: &FxHashSet, controller: &GameController, + window: &Window, ) -> SceneUpdateResult; /// During this method the scene should update animations and anything else that relies on time /// or on held keys @@ -238,6 +239,7 @@ pub trait Scene { timing: &Timing, mouse: &MouseData, held_keys: &FxHashSet, + window: &Window, ) -> SceneUpdateResult; /// Called when a child scene is closing /// @@ -294,6 +296,7 @@ pub trait PrePost { mouse: &MouseData, held_keys: &FxHashSet, scenes: &mut [Box>], + window: &Window, ); #[cfg(not(any(feature = "controller", feature = "controller_xinput")))] fn post_update( @@ -302,6 +305,7 @@ pub trait PrePost { mouse: &MouseData, held_keys: &FxHashSet, scenes: &mut [Box>], + window: &Window, ); #[cfg(any(feature = "controller", feature = "controller_xinput"))] fn pre_update( @@ -311,6 +315,7 @@ pub trait PrePost { held_keys: &FxHashSet, scenes: &mut [Box>], controller: &GameController, + window: &Window, ); #[cfg(any(feature = "controller", feature = "controller_xinput"))] fn post_update( @@ -320,6 +325,7 @@ pub trait PrePost { held_keys: &FxHashSet, scenes: &mut [Box>], controller: &GameController, + window: &Window, ); } #[cfg(any(feature = "controller", feature = "controller_xinput"))] @@ -353,6 +359,7 @@ pub fn empty_pre_post() -> Box> { _: &FxHashSet, _: &mut [Box>], _: &GameController, + _: &Window, ) { } @@ -363,6 +370,7 @@ pub fn empty_pre_post() -> Box> { _: &FxHashSet, _: &mut [Box>], _: &GameController, + _: &Window, ) { } } @@ -396,6 +404,7 @@ pub fn empty_pre_post() -> Box> { _: &MouseData, _: &FxHashSet, _: &mut [Box>], + _: &Window, ) { } @@ -405,6 +414,7 @@ pub fn empty_pre_post() -> Box> { _: &MouseData, _: &FxHashSet, _: &mut [Box>], + _: &Window, ) { } } @@ -455,7 +465,7 @@ impl System for Sc self.window_prefs.clone() } - fn update(&mut self, timing: &Timing) { + fn update(&mut self, timing: &Timing, window: &Window) { #[cfg(any(feature = "controller", feature = "controller_xinput"))] self.pre_post.pre_update( timing, @@ -463,6 +473,7 @@ impl System for Sc &self.held_keys, &mut self.scenes, &self.controller, + window, ); #[cfg(not(any(feature = "controller", feature = "controller_xinput")))] self.pre_post.pre_update( @@ -470,14 +481,21 @@ impl System for Sc &MouseData::default(), &self.held_keys, &mut self.scenes, + window, ); #[cfg(any(feature = "controller", feature = "controller_xinput"))] self.controller.update(); if let Some(scene) = self.scenes.last_mut() { #[cfg(any(feature = "controller", feature = "controller_xinput"))] - let result = scene.update(timing, &self.mouse, &self.held_keys, &self.controller); + let result = scene.update( + timing, + &self.mouse, + &self.held_keys, + &self.controller, + window, + ); #[cfg(not(any(feature = "controller", feature = "controller_xinput")))] - let result = scene.update(timing, &self.mouse, &self.held_keys); + let result = scene.update(timing, &self.mouse, &self.held_keys, window); match result { SceneUpdateResult::Nothing => {} SceneUpdateResult::Push(pop_current, name) => { @@ -501,6 +519,7 @@ impl System for Sc &self.held_keys, &mut self.scenes, &self.controller, + window, ); #[cfg(not(any(feature = "controller", feature = "controller_xinput")))] self.pre_post.post_update( @@ -508,6 +527,7 @@ impl System for Sc &MouseData::default(), &self.held_keys, &mut self.scenes, + window, ); if self.scenes.is_empty() { self.should_exit = true; diff --git a/src/ui/layout/mod.rs b/src/ui/layout/mod.rs index ebd4a2f..b229db0 100644 --- a/src/ui/layout/mod.rs +++ b/src/ui/layout/mod.rs @@ -1,3 +1,72 @@ +//! UI Layout +//! +//! There's three main approaches for positioning UI +//! 1) Absolute: +//! Each view is positioned manually at specific coords +//! 2) Assisted +//! Using the [RowLayout] and [ColumnLayout] macros views are positioned in rows and columns (with spacing and padding) +//! 3) Relative +//! Using [LayoutContext] and [layout!](crate::ui::layout::relative::layout) to position and size views relative to the context and each other +//! +//! # Examples +//! +//! ### 1 Absolute +//! ```rust +//!# use pixels_graphics_lib::prelude::*; +//!# use pixels_graphics_lib::ui::prelude::*; +//!# let style = UiStyle::default(); +//! const PADDING: usize = 4; +//! const BUTTON_HEIGHT: usize = 30; +//! const BUTTON_WIDTH: usize = 70; +//! const HEIGHT: usize = 100; +//! const WIDTH: usize = 100; +//! let background = Rect::new((0,0), (WIDTH, HEIGHT)); +//! let label = Text::new("Are you sure?", TextPos::px((100, 50)), (WHITE, PixelFont::Standard6x7, Positioning::Center)); +//! let positive = Button::new((PADDING, HEIGHT - PADDING - BUTTON_HEIGHT), "Yes", Some(BUTTON_WIDTH), &style.button); +//! let negative = Button::new((WIDTH - PADDING - BUTTON_WIDTH, HEIGHT - PADDING - BUTTON_HEIGHT), "No", Some(BUTTON_WIDTH), &style.button); +//! ``` +//! +//! ### 2 Assisted +//! +//!```rust +//!# use buffer_graphics_lib::text::PixelFont::Standard6x7; +//!# use pixels_graphics_lib::column_layout; +//!# use pixels_graphics_lib::ui::prelude::*; +//!# use pixels_graphics_lib::prelude::*; +//!# let style = UiStyle::default(); +//! let mut lbl_name = Label::new(Text::new("Name", TextPos::px(Coord::default()), (WHITE, Standard6x7))); +//! let mut txt_name = TextField::new(Coord::default(), 30, Standard6x7, (None, None), "", &[TextFilter::Letters], &style.text_field); +//! let mut cta = Button::new(Coord::default(), "Submit", None, &style.button); +//! column_layout!(Rect::new((0,0),(200,200)), ColumnGravity::Left, padding: 4, views: lbl_name, txt_name, cta); +//! ``` +//! +//! ### 3 Relative +//! ```rust +//! use pixels_graphics_lib::layout; +//!# use pixels_graphics_lib::prelude::*; +//!# use pixels_graphics_lib::ui::layout::relative::LayoutContext; +//!# use pixels_graphics_lib::ui::prelude::*; +//!# let style = UiStyle::default(); +//! const BUTTON_WIDTH: usize = 70; +//! const PADDING: usize = 6; +//! const HEIGHT: usize = 100; +//! const WIDTH: usize = 100; +//! let background = Rect::new((0,0), (WIDTH, HEIGHT)); +//! let context = LayoutContext::new_with_padding(background.clone(), PADDING); +//! let mut label = Label::new(Text::new("Are you sure?", TextPos::px((100, 50)), (WHITE, PixelFont::Standard6x7, Positioning::Center))); +//! let mut positive = Button::new((0,0), "Yes", Some(BUTTON_WIDTH), &style.button); +//! let mut negative = Button::new((0,0), "No", Some(BUTTON_WIDTH), &style.button); +//! +//! layout!(context, label, align_centerh); +//! layout!(context, label, align_top); +//! +//! layout!(context, positive, align_left); +//! layout!(context, positive, align_bottom); +//! +//! layout!(context, negative, align_left); +//! layout!(context, negative, align_bottom); +//! ``` + use crate::prelude::Rect; use crate::ui::PixelView; use std::fmt::Debug; @@ -54,7 +123,7 @@ macro_rules! bounds { /// let button1 = Button::new("Button", ...); /// let button2 = Button::new("Button", ...); /// let button3 = Button::new("Button", ...); -/// column_layout!(Rect::new(16,16,16,16), ColumnGravity::Left, padding: 2, spacing: 8, views: button3, button1, button2); +/// column_layout!(Rect::new((16,16),(16,16)), ColumnGravity::Left, padding: 2, spacing: 8, views: button3, button1, button2); /// // button3 top left will be (18, 18) /// // button1 top left will be (18, 18 + 8 + button3.height) /// // button2 top left will be (18, 18 + 8 + button3.height + 8 + button2.height) @@ -62,7 +131,7 @@ macro_rules! bounds { #[macro_export] macro_rules! column_layout { ($bounds:expr, $gravity:expr, $(padding: $padding:expr,)? $(spacing: $margin:expr,)? views: $($views:expr),+ $(,)?) => {{ - $crate::ui::layout::column::ColumnLayout::new(or_else!($($padding)?, 0), or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*]) + $crate::ui::layout::column::ColumnLayout::new($crate::or_else!($($padding)?, 0),$crate::or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*]) }}; } @@ -81,7 +150,7 @@ macro_rules! column_layout { /// let button1 = Button::new("Button", ...); /// let button2 = Button::new("Button", ...); /// let button3 = Button::new("Button", ...); -/// row_layout!(Rect::new(16,16,16,16), RowGravity::Top, spacing: 8, views: button3, button1, button2); +/// row_layout!(Rect::new((16,16),(16,16)), RowGravity::Top, spacing: 8, views: button3, button1, button2); /// // button3 top left will be (16,16) /// // button1 top left will be (16 + 8 + button3.width, 16) /// // button2 top left will be (16 + 8 + 8 + button3.width + button1.width, 16) @@ -89,7 +158,7 @@ macro_rules! column_layout { #[macro_export] macro_rules! row_layout { ($bounds:expr, $gravity:expr, $(padding: $padding:expr,)? $(spacing: $margin:expr,)? views: $($views:expr),+ $(,)?) => {{ - $crate::ui::layout::row::RowLayout::new(or_else!($($padding)?, 0), or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*]) + $crate::ui::layout::row::RowLayout::new($crate::or_else!($($padding)?, 0), $crate::or_else!($($margin)?, 0), $bounds, $gravity).layout(&mut [$(&mut $views,)*]) }}; } diff --git a/src/ui/layout/relative.rs b/src/ui/layout/relative.rs index 1f4cde0..95a14c3 100644 --- a/src/ui/layout/relative.rs +++ b/src/ui/layout/relative.rs @@ -412,7 +412,7 @@ pub fn grow_by_parent( /// /// Views must impl [PixelView] and to use `grow` they must also impl [LayoutView] /// -/// `offset` replaces the default offset from context +/// `offset` replaces the default offset from context (if it was set) /// /// # Usage /// diff --git a/src/ui/styles/defaults.rs b/src/ui/styles/defaults.rs index c532099..3952f67 100644 --- a/src/ui/styles/defaults.rs +++ b/src/ui/styles/defaults.rs @@ -221,9 +221,9 @@ impl Default for DropdownItemStyle { impl DropdownItemStyle { fn dropdown_arrow_for_font(font: PixelFont, colors: FocusColorSet) -> IconSet { - let mut buffer = Graphics::create_buffer(font.size().0, font.size().1); - let mut graphics = - Graphics::new(&mut buffer, font.size().0, font.size().1).unwrap_or_else(|err| { + let mut buffer = Graphics::create_buffer_u8(font.size().0, font.size().1); + let mut graphics = Graphics::new_u8_rgba(&mut buffer, font.size().0, font.size().1) + .unwrap_or_else(|err| { panic!( "Unable to create graphics using {font:?} when generating menu arrow: {err:?}" ) @@ -290,10 +290,10 @@ impl Default for CheckboxStyle { impl CheckboxStyle { fn icons() -> [IndexedImage; 2] { let size = 9; - let mut box_buffer = Graphics::create_buffer(size, size); - let mut check_buffer = Graphics::create_buffer(size, size); - let mut box_graphics = Graphics::new(&mut box_buffer, size, size).unwrap(); - let mut check_graphics = Graphics::new(&mut check_buffer, size, size).unwrap(); + let mut box_buffer = Graphics::create_buffer_u8(size, size); + let mut check_buffer = Graphics::create_buffer_u8(size, size); + let mut box_graphics = Graphics::new_u8_rgba(&mut box_buffer, size, size).unwrap(); + let mut check_graphics = Graphics::new_u8_rgba(&mut check_buffer, size, size).unwrap(); box_graphics.draw_rect(Rect::new((0, 0), (size - 1, size - 1)), stroke(WHITE)); check_graphics.draw_line((1, 4), (3, 5), WHITE); check_graphics.draw_line((1, 4), (3, 6), WHITE); diff --git a/src/ui/text_field.rs b/src/ui/text_field.rs index 0521cc5..0e78873 100644 --- a/src/ui/text_field.rs +++ b/src/ui/text_field.rs @@ -1,3 +1,4 @@ +use crate::prelude::winit; use crate::prelude::*; use crate::ui::prelude::*; use crate::ui::styles::TextFieldStyle; @@ -7,9 +8,30 @@ use buffer_graphics_lib::prelude::Positioning::LeftCenter; use buffer_graphics_lib::prelude::WrappingStrategy::Cutoff; use buffer_graphics_lib::prelude::*; use std::ops::RangeInclusive; +use winit::keyboard::KeyCode; +#[cfg(feature = "softbuffer")] +use winit::window::Cursor; +use winit::window::{CursorIcon, Window}; const CURSOR_BLINK_RATE: f64 = 0.5; +/// Set focus on the first view passed, clear focus on all others +/// +/// # Usage +/// ```rust +///# use buffer_graphics_lib::prelude::PixelFont::Standard6x7; +///# use pixels_graphics_lib::prelude::*; +///# use pixels_graphics_lib::swap_focus; +///# use pixels_graphics_lib::ui::prelude::*; +///# let style= UiStyle::default(); +/// let mut field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let mut field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let mut field3 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// +/// swap_focus!(field1, field2, field3); +/// +/// assert!(field1.is_focused()); +/// ``` #[macro_export] macro_rules! swap_focus { ($focus:expr, $( $unfocus:expr ),* $(,)? ) => {{ @@ -18,11 +40,120 @@ macro_rules! swap_focus { }}; } +/// Clear focus on all views +/// +/// # Usage +/// ```rust +///# use buffer_graphics_lib::prelude::PixelFont::Standard6x7; +///# use pixels_graphics_lib::prelude::*; +///# use pixels_graphics_lib::unfocus; +///# use pixels_graphics_lib::ui::prelude::*; +///# let style= UiStyle::default(); +/// let mut field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let mut field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let mut field3 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// +/// field1.focus(); +/// +/// unfocus!(field1, field2, field3); +/// +/// assert!(!field1.is_focused()); +/// ``` #[macro_export] macro_rules! unfocus { ( $( $unfocus:expr ),* $(,)? ) => {$($unfocus.unfocus();)*}; } +/// Set the mouse cursor to an I if it's over a [TextField] +/// +/// # Params +/// * `window` - A [Window] +/// * `mouse_coord` - [Coord] from [MouseData] or equivalent +/// * `view` - vararg [TextField]s +/// * `custom_hover_cursor` - Defaults to CursorIcon::Text +/// * `custom_default_cursor` - Defaults to CursorIcon::Default +/// +/// # Usage +/// +/// ```rust +///# use buffer_graphics_lib::prelude::*; +///# use buffer_graphics_lib::text::PixelFont::Standard6x7; +///# use winit::window::Window; +///# use pixels_graphics_lib::prelude::*; +///# use pixels_graphics_lib::ui::prelude::{set_mouse_cursor, TextField, UiStyle}; +///# fn method(window: &Window) { +///# let style = UiStyle::default(); +/// let field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// +/// let mouse_coord = Coord::new(10,10); +/// +/// set_mouse_cursor(window, mouse_coord, None, None, &[&field1, &field2]); +///# } +/// ``` +#[cfg(feature = "pixels")] +pub fn set_mouse_cursor>( + window: &Window, + mouse_coord: C, + custom_hover_cursor: Option, + custom_default_cursor: Option, + views: &[&TextField], +) { + let coord = mouse_coord.into(); + for view in views { + if view.bounds.contains(coord) { + window.set_cursor_icon(custom_hover_cursor.unwrap_or(CursorIcon::Text)); + return; + } + } + window.set_cursor_icon(custom_default_cursor.unwrap_or(CursorIcon::Default)); +} + +/// Set the mouse cursor to an I if it's over a [TextField] +/// +/// # Params +/// * `window` - A [Window] +/// * `mouse_coord` - [Coord] from [MouseData] or equivalent +/// * `view` - vararg [TextField]s +/// * `custom_hover_cursor` - Defaults to CursorIcon::Text +/// * `custom_default_cursor` - Defaults to CursorIcon::Default +/// +/// # Usage +/// +/// ```rust +///# use buffer_graphics_lib::prelude::*; +///# use buffer_graphics_lib::text::PixelFont::Standard6x7; +///# use winit::window::Window; +///# use pixels_graphics_lib::prelude::*; +///# use pixels_graphics_lib::ui::prelude::{set_mouse_cursor, TextField, UiStyle}; +///# fn method(window: &Window) { +///# let style = UiStyle::default(); +/// let field1 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// let field2 = TextField::new(Coord::default(), 10, Standard6x7, (None, None), "", &[], &style.text_field); +/// +/// let mouse_coord = Coord::new(10,10); +/// +/// set_mouse_cursor(window, mouse_coord, None, None, &[&field1, &field2]); +///# } +/// ``` +#[cfg(feature = "softbuffer")] +pub fn set_mouse_cursor>( + window: &Window, + mouse_coord: C, + custom_hover_cursor: Option, + custom_default_cursor: Option, + views: &[&TextField], +) { + let coord = mouse_coord.into(); + for view in views { + if view.bounds.contains(coord) { + window.set_cursor(custom_hover_cursor.unwrap_or(Cursor::Icon(CursorIcon::Text))); + return; + } + } + window.set_cursor(custom_default_cursor.unwrap_or(Cursor::Icon(CursorIcon::Default))); +} + #[derive(Debug, Eq, PartialEq, Clone)] pub enum TextFilter { /// a-z diff --git a/src/utilities.rs b/src/utilities.rs index 008da3b..69c158a 100644 --- a/src/utilities.rs +++ b/src/utilities.rs @@ -1,7 +1,8 @@ +use crate::prelude::winit; use winit::keyboard::KeyCode; pub mod virtual_key_codes { - use winit::keyboard::KeyCode; + use super::*; pub const ALL_KEYS: [KeyCode; 96] = [ KeyCode::KeyA, diff --git a/src/window_prefs.rs b/src/window_prefs.rs index 3047867..7811289 100644 --- a/src/window_prefs.rs +++ b/src/window_prefs.rs @@ -1,3 +1,4 @@ +use crate::prelude::*; use serde::{Deserialize, Serialize}; use simple_game_utils::prelude::*; use winit::dpi::{PhysicalPosition, PhysicalSize};