From 3f099070c22ec9f837136420ba56966fd1b1f135 Mon Sep 17 00:00:00 2001 From: Jonathan Johnson Date: Tue, 27 Jun 2023 09:21:38 -0700 Subject: [PATCH] Initial commit. --- .gitignore | 2 + Cargo.toml | 9 + README.md | 40 +++ src/event_loop.rs | 1 + src/lib.rs | 220 ++++++++++++ src/private.rs | 335 ++++++++++++++++++ src/window.rs | 873 ++++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 1480 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 src/event_loop.rs create mode 100644 src/lib.rs create mode 100644 src/private.rs create mode 100644 src/window.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4fffb2f --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +/Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..026fa8a --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "appit" +version = "0.1.0" +edition = "2021" + + +[dependencies] +winit = "0.28.6" +raw-window-handle = "0.5.1" diff --git a/README.md b/README.md new file mode 100644 index 0000000..8f29be2 --- /dev/null +++ b/README.md @@ -0,0 +1,40 @@ +# appit + +An opinionated wrapper for `winit` that provides a trait-based approach to +implementing multi-window applications. + +This crate's main type is `WindowBehavior`, a trait that provides functions for +nearly every `winit::event::WindowEvent`. This allows you to implement exactly +which events you wish to respond to, and ignore the rest without a large match +statement. + +This crate also keeps track of the redraw state of the window, and allows +scheduling redraws in the future. + +```rust,no_run +use appit::WindowBehavior; + +struct MyWindow; + +impl WindowBehavior for MyWindow { + type Context = (); + + fn initialize(_window: &mut appit::RunningWindow, _context: Self::Context) -> Self { + Self + } + + fn redraw(&mut self, window: &mut appit::RunningWindow) { + println!("Should redraw"); + } +} + +fn main() { + MyWindow::run() +} +``` + +## Why not use this crate? + +- Very new, largely untested. +- Not all platforms support threads, and a single-window, single-thread code + path is not supported yet. diff --git a/src/event_loop.rs b/src/event_loop.rs new file mode 100644 index 0000000..73f756a --- /dev/null +++ b/src/event_loop.rs @@ -0,0 +1 @@ +pub struct App {} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f67dee5 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,220 @@ +#![doc = include_str!("../README.md")] +#![warn(missing_docs, clippy::pedantic)] +#![deny(unsafe_code)] +#![allow(clippy::module_name_repetitions)] + +mod private; +mod window; + +use raw_window_handle::HasRawWindowHandle; +pub use window::{RunningWindow, Window, WindowBehavior, WindowBuilder}; + +use winit::error::OsError; +use winit::window::WindowId; + +use std::collections::HashMap; +use std::sync::{mpsc, Arc, Mutex, PoisonError}; +use winit::event_loop::{ControlFlow, EventLoopBuilder, EventLoopProxy, EventLoopWindowTarget}; +use winit::{event::Event, event_loop::EventLoop}; + +use crate::private::{AppMessage, WindowEvent, WindowMessage}; +use crate::window::WindowAttributes; + +/// An application that is not yet running. +pub struct PendingApp { + event_loop: EventLoop, + running: App, +} + +impl Default for PendingApp { + fn default() -> Self { + Self::new() + } +} + +impl PendingApp { + /// Returns a new app with no windows. If no windows are opened before the + /// app is run, the app will immediately close. + #[must_use] + pub fn new() -> Self { + let event_loop = EventLoopBuilder::with_user_event().build(); + let proxy = event_loop.create_proxy(); + Self { + event_loop, + running: App { + proxy, + windows: Windows::default(), + }, + } + } + + /// Begins running the application. This function will never return. + /// + /// Internally this runs the [`EventLoop`]. + pub fn run(self) -> ! { + self.event_loop.run(move |event, target, control_flow| { + *control_flow = ControlFlow::Wait; + match event { + Event::WindowEvent { window_id, event } => { + let event = WindowEvent::from(event); + self.running + .windows + .send(window_id, WindowMessage::Event(event)); + } + Event::RedrawRequested(window_id) => { + self.running.windows.send(window_id, WindowMessage::Redraw); + } + Event::UserEvent(message) => match message { + AppMessage::CloseWindow(window_id) => { + if self.running.windows.close(window_id) { + *control_flow = ControlFlow::ExitWithCode(0); + } + } + AppMessage::OpenWindow { + attrs, + sender, + open_sender, + } => { + let result = self.running.windows.open(target, attrs, sender); + let _result = open_sender.send(result); + } + }, + Event::NewEvents(_) + | Event::DeviceEvent { .. } + | Event::Suspended + | Event::Resumed + | Event::MainEventsCleared + | Event::RedrawEventsCleared + | Event::LoopDestroyed => {} + } + }); + } +} + +/// A reference to a multi-window application. +#[derive(Clone)] +pub struct App { + proxy: EventLoopProxy, + windows: Windows, +} + +/// A type that has a handle to the application thread. +pub trait Application: private::ApplicationSealed {} + +impl Application for PendingApp {} + +impl private::ApplicationSealed for PendingApp { + fn app(&self) -> App { + self.running.clone() + } + + fn open( + &self, + window: WindowAttributes, + sender: mpsc::SyncSender, + ) -> Result>, OsError> { + self.running + .windows + .open(&self.event_loop, window, sender) + .map(Some) + } +} + +#[derive(Default, Clone)] +struct Windows { + data: Arc>>, +} + +impl Windows { + fn get(&self, id: WindowId) -> Option> { + let windows = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + windows.get(&id).map(|w| w.winit.clone()) + } + + #[allow(unsafe_code)] + fn open( + &self, + target: &EventLoopWindowTarget, + attrs: WindowAttributes, + sender: mpsc::SyncSender, + ) -> Result, OsError> { + let mut builder = winit::window::WindowBuilder::new() + .with_active(attrs.active) + .with_resizable(attrs.resizable) + .with_enabled_buttons(attrs.enabled_buttons) + .with_title(attrs.title) + .with_maximized(attrs.maximized) + .with_visible(attrs.visible) + .with_transparent(attrs.transparent) + .with_decorations(attrs.decorations) + .with_window_level(attrs.window_level) + .with_content_protected(attrs.content_protected) + .with_fullscreen(attrs.fullscreen) + .with_window_icon(attrs.window_icon) + .with_theme(attrs.preferred_theme); + + if let Some(inner_size) = attrs.inner_size { + builder = builder.with_inner_size(inner_size); + } + if let Some(min_inner_size) = attrs.min_inner_size { + builder = builder.with_min_inner_size(min_inner_size); + } + if let Some(max_inner_size) = attrs.max_inner_size { + builder = builder.with_max_inner_size(max_inner_size); + } + if let Some(position) = attrs.position { + builder = builder.with_position(position); + } + if let Some(resize_increments) = attrs.resize_increments { + builder = builder.with_resize_increments(resize_increments); + } + let mut windows = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + if let Some(parent_window) = attrs.parent_window { + let parent_window = windows + .get(&parent_window.id()) + .expect("invalid parent window"); + // SAFETY: The only way for us to resolve to a winit Window is for + // the window to still be in our list of open windows. This + // guarantees that the window handle is still valid. + unsafe { + builder = builder.with_parent_window(Some(parent_window.winit.raw_window_handle())); + } + } + let winit = Arc::new(builder.build(target)?); + windows.insert( + winit.id(), + OpenWindow { + winit: winit.clone(), + sender, + }, + ); + Ok(winit) + } + + pub fn send(&self, window: WindowId, message: WindowMessage) { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + if let Some(open_window) = data.get(&window) { + match open_window.sender.try_send(message) { + Ok(()) => {} + Err(mpsc::TrySendError::Full(_)) => { + eprintln!("Dropping event for {window:?}."); + } + Err(mpsc::TrySendError::Disconnected(_)) => { + // Window no longer active, remove it. + data.remove(&window); + } + } + } + } + + pub fn close(&self, window: WindowId) -> bool { + let mut data = self.data.lock().map_or_else(PoisonError::into_inner, |g| g); + data.remove(&window); + data.is_empty() + } +} + +struct OpenWindow { + winit: Arc, + sender: mpsc::SyncSender, +} diff --git a/src/private.rs b/src/private.rs new file mode 100644 index 0000000..96f0e8b --- /dev/null +++ b/src/private.rs @@ -0,0 +1,335 @@ +use std::path::PathBuf; +use std::sync::{mpsc, Arc}; + +use winit::dpi::{PhysicalPosition, PhysicalSize}; +use winit::error::OsError; +use winit::event::{ + AxisId, DeviceId, ElementState, Ime, KeyboardInput, ModifiersState, MouseButton, + MouseScrollDelta, Touch, TouchPhase, +}; +use winit::window::{Theme, WindowId}; + +use crate::window::WindowAttributes; +use crate::App; + +pub trait ApplicationSealed { + fn app(&self) -> App; + fn open( + &self, + window: WindowAttributes, + sender: mpsc::SyncSender, + ) -> Result>, OsError>; +} + +pub enum AppMessage { + OpenWindow { + attrs: WindowAttributes, + sender: mpsc::SyncSender, + open_sender: mpsc::SyncSender, OsError>>, + }, + CloseWindow(WindowId), +} + +#[derive(Debug)] +pub enum WindowMessage { + Redraw, + Event(WindowEvent), +} + +#[derive(Debug)] +pub enum WindowEvent { + /// The size of the window has changed. Contains the client area's new dimensions. + Resized(PhysicalSize), + + /// The position of the window has changed. Contains the window's new position. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web / Wayland:** Unsupported. + Moved(PhysicalPosition), + + /// The window has been requested to close. + CloseRequested, + + /// The window has been destroyed. + Destroyed, + + /// A file has been dropped into the window. + /// + /// When the user drops multiple files at once, this event will be emitted for each file + /// separately. + DroppedFile(PathBuf), + + /// A file is being hovered over the window. + /// + /// When the user hovers multiple files at once, this event will be emitted for each file + /// separately. + HoveredFile(PathBuf), + + /// A file was hovered, but has exited the window. + /// + /// There will be a single `HoveredFileCancelled` event triggered even if multiple files were + /// hovered. + HoveredFileCancelled, + + /// The window received a unicode character. + /// + /// See also the [`Ime`](Self::Ime) event for more complex character sequences. + ReceivedCharacter(char), + + /// The window gained or lost focus. + /// + /// The parameter is true if the window has gained focus, and false if it has lost focus. + Focused(bool), + + /// An event from the keyboard has been received. + KeyboardInput { + device_id: DeviceId, + input: KeyboardInput, + /// If `true`, the event was generated synthetically by winit + /// in one of the following circumstances: + /// + /// * Synthetic key press events are generated for all keys pressed + /// when a window gains focus. Likewise, synthetic key release events + /// are generated for all keys pressed when a window goes out of focus. + /// ***Currently, this is only functional on X11 and Windows*** + /// + /// Otherwise, this value is always `false`. + is_synthetic: bool, + }, + + /// The keyboard modifiers have changed. + /// + /// ## Platform-specific + /// + /// - **Web:** This API is currently unimplemented on the web. This isn't by design - it's an + /// issue, and it should get fixed - but it's the current state of the API. + ModifiersChanged(ModifiersState), + + /// An event from an input method. + /// + /// **Note:** You have to explicitly enable this event using [`Window::set_ime_allowed`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Web:** Unsupported. + Ime(Ime), + + /// The cursor has moved on the window. + CursorMoved { + device_id: DeviceId, + + /// (x,y) coords in pixels relative to the top-left corner of the window. Because the range of this data is + /// limited by the display area and it may have been transformed by the OS to implement effects such as cursor + /// acceleration, it should not be used to implement non-cursor-like interactions such as 3D camera control. + position: PhysicalPosition, + }, + + /// The cursor has entered the window. + CursorEntered { + device_id: DeviceId, + }, + + /// The cursor has left the window. + CursorLeft { + device_id: DeviceId, + }, + + /// A mouse wheel movement or touchpad scroll occurred. + MouseWheel { + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + }, + + /// An mouse button press has been received. + MouseInput { + device_id: DeviceId, + state: ElementState, + button: MouseButton, + }, + + /// Touchpad pressure event. + /// + /// At the moment, only supported on Apple forcetouch-capable macbooks. + /// The parameters are: pressure level (value between 0 and 1 representing how hard the touchpad + /// is being pressed) and stage (integer representing the click level). + TouchpadPressure { + device_id: DeviceId, + pressure: f32, + stage: i64, + }, + + /// Motion on some analog axis. May report data redundant to other, more specific events. + AxisMotion { + device_id: DeviceId, + axis: AxisId, + value: f64, + }, + + /// Touch event has been received + Touch(Touch), + + /// The window's scale factor has changed. + /// + /// The following user actions can cause DPI changes: + /// + /// * Changing the display's resolution. + /// * Changing the display's scale factor (e.g. in Control Panel on Windows). + /// * Moving the window to a display with a different scale factor. + /// + /// After this event callback has been processed, the window will be resized to whatever value + /// is pointed to by the `new_inner_size` reference. By default, this will contain the size suggested + /// by the OS, but it can be changed to any value. + /// + /// For more information about DPI in general, see the [`dpi`](crate::dpi) module. + ScaleFactorChanged { + scale_factor: f64, + new_inner_size: PhysicalSize, + }, + + /// The system window theme has changed. + /// + /// Applications might wish to react to this to change the theme of the content of the window + /// when the system changes the window theme. + /// + /// ## Platform-specific + /// + /// At the moment this is only supported on Windows. + ThemeChanged(Theme), + + /// The window has been occluded (completely hidden from view). + /// + /// This is different to window visibility as it depends on whether the window is closed, + /// minimised, set invisible, or fully occluded by another window. + /// + /// Platform-specific behavior: + /// - **iOS / Android / Web / Wayland / Windows:** Unsupported. + Occluded(bool), + + TouchpadMagnify { + device_id: DeviceId, + delta: f64, + phase: TouchPhase, + }, + SmartMagnify { + device_id: DeviceId, + }, + TouchpadRotate { + device_id: DeviceId, + delta: f32, + phase: TouchPhase, + }, +} + +impl<'a> From> for WindowEvent { + #[allow(clippy::too_many_lines)] // it's a match statement + fn from(event: winit::event::WindowEvent<'a>) -> Self { + match event { + winit::event::WindowEvent::Resized(size) => Self::Resized(size), + winit::event::WindowEvent::Moved(pos) => Self::Moved(pos), + winit::event::WindowEvent::CloseRequested => Self::CloseRequested, + winit::event::WindowEvent::Destroyed => Self::Destroyed, + winit::event::WindowEvent::DroppedFile(path) => Self::DroppedFile(path), + winit::event::WindowEvent::HoveredFile(path) => Self::HoveredFile(path), + winit::event::WindowEvent::HoveredFileCancelled => Self::HoveredFileCancelled, + winit::event::WindowEvent::ReceivedCharacter(ch) => Self::ReceivedCharacter(ch), + winit::event::WindowEvent::Focused(focused) => Self::Focused(focused), + winit::event::WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + } => Self::KeyboardInput { + device_id, + input, + is_synthetic, + }, + + winit::event::WindowEvent::ModifiersChanged(modifiers) => { + Self::ModifiersChanged(modifiers) + } + winit::event::WindowEvent::Ime(ime) => Self::Ime(ime), + winit::event::WindowEvent::CursorMoved { + device_id, + position, + .. + } => Self::CursorMoved { + device_id, + position, + }, + winit::event::WindowEvent::CursorEntered { device_id } => { + Self::CursorEntered { device_id } + } + winit::event::WindowEvent::CursorLeft { device_id } => Self::CursorLeft { device_id }, + winit::event::WindowEvent::MouseWheel { + device_id, + delta, + phase, + .. + } => Self::MouseWheel { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::MouseInput { + device_id, + state, + button, + .. + } => Self::MouseInput { + device_id, + state, + button, + }, + winit::event::WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + } => Self::TouchpadPressure { + device_id, + pressure, + stage, + }, + winit::event::WindowEvent::AxisMotion { + device_id, + axis, + value, + } => Self::AxisMotion { + device_id, + axis, + value, + }, + winit::event::WindowEvent::Touch(touch) => Self::Touch(touch), + winit::event::WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => Self::ScaleFactorChanged { + scale_factor, + new_inner_size: *new_inner_size, + }, + winit::event::WindowEvent::ThemeChanged(theme) => Self::ThemeChanged(theme), + winit::event::WindowEvent::Occluded(occluded) => Self::Occluded(occluded), + winit::event::WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + } => Self::TouchpadMagnify { + device_id, + delta, + phase, + }, + winit::event::WindowEvent::SmartMagnify { device_id } => { + Self::SmartMagnify { device_id } + } + winit::event::WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + } => Self::TouchpadRotate { + device_id, + delta, + phase, + }, + } + } +} diff --git a/src/window.rs b/src/window.rs new file mode 100644 index 0000000..e3f9313 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,873 @@ +use std::collections::HashSet; +use std::ops::{Deref, DerefMut}; +use std::path::PathBuf; +use std::sync::{mpsc, Arc}; +use std::thread; +use std::time::{Duration, Instant}; + +use winit::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; +use winit::error::OsError; +use winit::event::{ + AxisId, DeviceId, ElementState, Ime, KeyboardInput, ModifiersState, MouseButton, + MouseScrollDelta, Touch, TouchPhase, VirtualKeyCode, +}; +use winit::window::{Fullscreen, Icon, Theme, WindowButtons, WindowId, WindowLevel}; + +use crate::private::{self, WindowEvent}; +use crate::{App, AppMessage, Application, PendingApp, WindowMessage}; + +/// A weak reference to a running window. +#[derive(Clone)] +pub struct Window { + app: App, + id: WindowId, +} + +impl Window { + /// Returns the winit id of the window. + #[must_use] + pub const fn id(&self) -> WindowId { + self.id + } + + /// Returns a clone of the winit [`Window`](winit::window::Window) if the + /// window is still open. + #[must_use] + pub fn winit(&self) -> Option> { + self.app.windows.get(self.id) + } +} + +/// A builder for a window. +/// +/// This type is similar to winit's +/// [`WindowBuilder`](winit::window::WindowBuilder), except that it only +/// supports the cross-platform interface. Support for additional +/// platform-specific settings may be possible as long as all types introduced +/// are `Send`. +pub struct WindowBuilder<'a, Behavior, Application> +where + Behavior: self::WindowBehavior, +{ + owner: &'a Application, + context: Behavior::Context, + attributes: WindowAttributes, +} +impl<'a, Behavior, Application> Deref for WindowBuilder<'a, Behavior, Application> +where + Behavior: self::WindowBehavior, +{ + type Target = WindowAttributes; + + fn deref(&self) -> &Self::Target { + &self.attributes + } +} + +impl<'a, Behavior, Application> DerefMut for WindowBuilder<'a, Behavior, Application> +where + Behavior: self::WindowBehavior, +{ + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.attributes + } +} + +#[allow(clippy::struct_excessive_bools)] +pub struct WindowAttributes { + pub inner_size: Option, + pub min_inner_size: Option, + pub max_inner_size: Option, + pub position: Option, + pub resizable: bool, + pub enabled_buttons: WindowButtons, + pub title: String, + pub fullscreen: Option, + pub maximized: bool, + pub visible: bool, + pub transparent: bool, + pub decorations: bool, + pub window_icon: Option, + pub preferred_theme: Option, + pub resize_increments: Option, + pub content_protected: bool, + pub window_level: WindowLevel, + pub parent_window: Option, + pub active: bool, +} + +impl Default for WindowAttributes { + fn default() -> Self { + let defaults = winit::window::WindowAttributes::default(); + Self { + inner_size: defaults.inner_size, + min_inner_size: defaults.min_inner_size, + max_inner_size: defaults.max_inner_size, + position: defaults.position, + resizable: defaults.resizable, + enabled_buttons: defaults.enabled_buttons, + title: defaults.title, + fullscreen: defaults.fullscreen, + maximized: defaults.maximized, + visible: defaults.visible, + transparent: defaults.transparent, + decorations: defaults.decorations, + window_icon: defaults.window_icon, + preferred_theme: defaults.preferred_theme, + resize_increments: defaults.resize_increments, + content_protected: defaults.content_protected, + window_level: defaults.window_level, + active: defaults.active, + parent_window: None, + } + } +} + +impl<'a, Behavior, Application> WindowBuilder<'a, Behavior, Application> +where + Behavior: self::WindowBehavior, + Application: crate::Application, +{ + pub(crate) fn new(owner: &'a Application, context: Behavior::Context) -> Self { + Self { + owner, + context, + attributes: WindowAttributes::default(), + } + } + + /// Opens the window, if the application is still running or has not started + /// running. The events of the window will be processed in a thread spawned + /// by this function. + /// + /// If the application has shut down, this function returns None. + /// + /// # Errors + /// + /// The only errors this funciton can return arise from + /// [`winit::window::WindowBuilder::build`]. + pub fn open(self) -> Result, winit::error::OsError> { + // The window's thread shouldn't ever block for long periods of time. To + // avoid a "frozen" window causing massive memory allocations, we'll use + // a fixed-size channel and be cautious to not block the main event loop + // by always using try_send. + let (sender, receiver) = mpsc::sync_channel(128); + let Some(winit) = self.owner.open(self.attributes, sender)? else { + return Ok(None) + }; + let window = Window { + id: winit.id(), + app: self.owner.app(), + }; + let mut running_window = RunningWindow { + messages: receiver, + app: self.owner.app(), + occluded: winit.is_visible().unwrap_or(false), + focused: winit.has_focus(), + inner_size: winit.inner_size(), + location: winit.inner_position().unwrap_or_default(), + scale: winit.scale_factor(), + theme: winit.theme().unwrap_or(Theme::Dark), + window: winit, + next_redraw_target: None, + close: false, + modifiers: ModifiersState::default(), + cursor_location: None, + mouse_buttons: HashSet::default(), + keys: HashSet::default(), + }; + + thread::spawn(move || running_window.run_with::(self.context)); + + Ok(Some(window)) + } +} + +/// A window that is running in its own thread. +pub struct RunningWindow { + window: Arc, + next_redraw_target: Option, + messages: mpsc::Receiver, + app: App, + inner_size: PhysicalSize, + location: PhysicalPosition, + cursor_location: Option>, + mouse_buttons: HashSet, + keys: HashSet, + scale: f64, + close: bool, + occluded: bool, + focused: bool, + theme: Theme, + modifiers: ModifiersState, +} + +impl RunningWindow { + /// Returns a reference to the underlying window. + #[must_use] + pub fn winit(&self) -> &winit::window::Window { + &self.window + } + + /// Returns the target for when the window will be redrawn. + #[must_use] + pub const fn next_redraw_target(&self) -> Option { + self.next_redraw_target + } + + /// Sets the window to redraw as soon as it can. + pub fn set_needs_redraw(&mut self) { + self.next_redraw_target = Some(RedrawTarget::Immediate); + } + + /// Sets the window to redraw at the provided time. + /// + /// If the window is already set to redraw sooner, this function does + /// nothing. + pub fn redraw_at(&mut self, instant: Instant) { + // Make sure this new scheduled time isn't further out than our current target. + match self.next_redraw_target { + Some(RedrawTarget::Immediate) => return, + Some(RedrawTarget::Scheduled(at)) => { + if at < instant { + return; + } + } + None => {} + } + + self.next_redraw_target = Some(RedrawTarget::Scheduled(instant)); + } + + /// Sets the window to redraw after a `duration`. + /// + /// If the window is already set to redraw sooner, this function does + /// nothing. + pub fn redraw_in(&mut self, duration: Duration) { + self.redraw_at(Instant::now() + duration); + } + + /// Returns the current size of the interior of the window, in pixels. + #[must_use] + pub const fn inner_size(&self) -> PhysicalSize { + self.inner_size + } + + /// Returns the current location of the window, in pixels. + #[must_use] + pub const fn location(&self) -> PhysicalPosition { + self.location + } + + /// Returns the location of the cursor relative to the window's upper-left + /// corner, in pixels. + #[must_use] + pub const fn cursor_location(&self) -> Option> { + self.cursor_location + } + + /// Returns the current scale factor for the window. + #[must_use] + pub const fn scale(&self) -> f64 { + self.scale + } + + /// Returns true if the window is currently invisible, hidden behind other + /// windows, minimized, or otherwise hidden from the user's view. + #[must_use] + pub const fn occluded(&self) -> bool { + self.occluded + } + + /// Returns true if the window is currently focused for keyboard input. + #[must_use] + pub const fn focused(&self) -> bool { + self.focused + } + + /// Returns the current theme of the window. + #[must_use] + pub const fn theme(&self) -> Theme { + self.theme + } + + /// Returns the current state of the keyboard modifier keys. + #[must_use] + pub const fn modifiers(&self) -> ModifiersState { + self.modifiers + } + + fn run_with(&mut self, context: Behavior::Context) + where + Behavior: self::WindowBehavior, + { + let mut behavior = Behavior::initialize(self, context); + while !self.close && self.process_messages_until_redraw(&mut behavior) { + self.next_redraw_target = None; + behavior.redraw(self); + } + drop(behavior); + + // Do not notify the main thread to close the window until after the + // behavior is dropped. This upholds the requirement for RawWindowHandle + // by making sure that any resources required by the behavior have had a + // chance to be freed. + let _result = self + .app + .proxy + .send_event(AppMessage::CloseWindow(self.window.id())); + } + + fn process_messages_until_redraw(&mut self, behavior: &mut Behavior) -> bool + where + Behavior: self::WindowBehavior, + { + loop { + let message = match TimeUntilRedraw::from(self.next_redraw_target) { + // The scheduled redraw time has already elapsed, or we need to + // redraw. Process messages that are already enqueued, but don't + // block. + TimeUntilRedraw::None => match self.messages.try_recv() { + Ok(message) => message, + Err(mpsc::TryRecvError::Disconnected) => return false, + Err(mpsc::TryRecvError::Empty) => return true, + }, + // We have a scheduled time for the next frame, and it hasn't + // elapsed yet. + TimeUntilRedraw::Some(duration_remaining) => { + match self.messages.recv_timeout(duration_remaining) { + Ok(message) => message, + Err(mpsc::RecvTimeoutError::Timeout) => return true, + Err(mpsc::RecvTimeoutError::Disconnected) => return false, + } + } + // No scheduled redraw time, sleep until the next message. + TimeUntilRedraw::Indefinite => match self.messages.recv() { + Ok(message) => message, + Err(_) => return false, + }, + }; + + if !self.handle_message(message, behavior) { + break false; + } + } + } + + #[allow(clippy::too_many_lines)] // can't avoid the match + fn handle_message(&mut self, message: WindowMessage, behavior: &mut Behavior) -> bool + where + Behavior: self::WindowBehavior, + { + match message { + WindowMessage::Redraw => { + self.set_needs_redraw(); + } + WindowMessage::Event(evt) => match evt { + WindowEvent::CloseRequested => { + if behavior.close_requested(self) { + self.close(); + } + } + WindowEvent::Focused(focused) => { + self.focused = focused; + behavior.focus_changed(self); + } + WindowEvent::Occluded(occluded) => { + self.occluded = occluded; + behavior.occlusion_changed(self); + } + WindowEvent::ScaleFactorChanged { + scale_factor, + new_inner_size, + } => { + // Ensure both values are updated before any behavior + // callbacks are invoked. + self.scale = scale_factor; + let inner_size_changed = self.inner_size != new_inner_size; + self.inner_size = new_inner_size; + behavior.scale_factor_changed(self); + if inner_size_changed { + behavior.resized(self); + } + } + WindowEvent::Resized(new_inner_size) => { + if self.inner_size != new_inner_size { + self.inner_size = new_inner_size; + behavior.resized(self); + } + } + WindowEvent::Moved(location) => { + self.location = location; + } + WindowEvent::Destroyed => { + return false; + } + WindowEvent::ThemeChanged(theme) => { + self.theme = theme; + behavior.theme_changed(self); + } + WindowEvent::DroppedFile(path) => { + behavior.dropped_file(self, path); + } + WindowEvent::HoveredFile(path) => { + behavior.hovered_file(self, path); + } + WindowEvent::HoveredFileCancelled => { + behavior.hovered_file_cancelled(self); + } + WindowEvent::ReceivedCharacter(char) => { + behavior.received_character(self, char); + } + WindowEvent::KeyboardInput { + device_id, + input, + is_synthetic, + } => { + if let Some(keycode) = input.virtual_keycode { + match input.state { + ElementState::Pressed => { + self.keys.insert(keycode); + } + ElementState::Released => { + self.keys.remove(&keycode); + } + } + } + behavior.keyboard_input(self, device_id, input, is_synthetic); + } + WindowEvent::ModifiersChanged(modifiers) => { + self.modifiers = modifiers; + behavior.modifiers_changed(self); + } + WindowEvent::Ime(ime) => { + behavior.ime(self, ime); + } + WindowEvent::CursorMoved { + device_id, + position, + } => { + self.cursor_location = Some(position); + behavior.cursor_moved(self, device_id, position); + } + WindowEvent::CursorEntered { device_id } => { + behavior.cursor_entered(self, device_id); + } + WindowEvent::CursorLeft { device_id } => { + self.cursor_location = None; + behavior.cursor_left(self, device_id); + } + WindowEvent::MouseWheel { + device_id, + delta, + phase, + } => { + behavior.mouse_wheel(self, device_id, delta, phase); + } + WindowEvent::MouseInput { + device_id, + state, + button, + } => { + match state { + ElementState::Pressed => { + self.mouse_buttons.insert(button); + } + ElementState::Released => { + self.mouse_buttons.remove(&button); + } + } + behavior.mouse_input(self, device_id, state, button); + } + WindowEvent::TouchpadPressure { + device_id, + pressure, + stage, + } => { + behavior.touchpad_pressure(self, device_id, pressure, stage); + } + WindowEvent::AxisMotion { + device_id, + axis, + value, + } => { + behavior.axis_motion(self, device_id, axis, value); + } + WindowEvent::Touch(touch) => { + behavior.touch(self, touch); + } + WindowEvent::TouchpadMagnify { + device_id, + delta, + phase, + } => { + behavior.touchpad_magnify(self, device_id, delta, phase); + } + WindowEvent::SmartMagnify { device_id } => { + behavior.smart_magnify(self, device_id); + } + WindowEvent::TouchpadRotate { + device_id, + delta, + phase, + } => { + behavior.touchpad_rotate(self, device_id, delta, phase); + } + }, + } + + true + } + + /// Sets this window to close as soon as possible. + pub fn close(&mut self) { + self.close = true; + self.set_needs_redraw(); + } + + /// Returns an iterator of the currently pressed keys. + /// + /// This iterator does not guarantee any specific order. + pub fn pressed_keys(&self) -> impl Iterator + '_ { + self.keys.iter().copied() + } + + /// Returns true if the given key code is currently pressed. + #[must_use] + pub fn key_pressed(&self, keycode: &VirtualKeyCode) -> bool { + self.keys.contains(keycode) + } + + /// Returns an iterator of the currently pressed mouse buttons. + /// + /// This iterator does not guarantee any specific order. + pub fn pressed_mouse_buttons(&self) -> impl Iterator + '_ { + self.mouse_buttons.iter().copied() + } + + /// Returns true if the button is currently pressed. + #[must_use] + pub fn mouse_button_pressed(&self, button: &MouseButton) -> bool { + self.mouse_buttons.contains(button) + } +} + +impl Application for RunningWindow {} + +impl private::ApplicationSealed for RunningWindow { + fn app(&self) -> App { + self.app.clone() + } + + fn open( + &self, + attrs: WindowAttributes, + sender: mpsc::SyncSender, + ) -> Result>, OsError> { + let (open_sender, open_receiver) = mpsc::sync_channel(1); + if self + .app + .proxy + .send_event(AppMessage::OpenWindow { + attrs, + sender, + open_sender, + }) + .is_ok() + { + if let Ok(window) = open_receiver.recv() { + return window.map(Some); + } + } + + Ok(None) + } +} + +#[derive(Clone, Copy, Eq, PartialEq, Debug)] +pub enum RedrawTarget { + Immediate, + Scheduled(Instant), +} + +impl From> for TimeUntilRedraw { + fn from(value: Option) -> Self { + match value { + Some(RedrawTarget::Immediate) => TimeUntilRedraw::None, + Some(RedrawTarget::Scheduled(at)) => match at.checked_duration_since(Instant::now()) { + Some(remaining) if !remaining.is_zero() => TimeUntilRedraw::Some(remaining), + _ => TimeUntilRedraw::None, + }, + None => Self::Indefinite, + } + } +} + +#[derive(Debug)] +enum TimeUntilRedraw { + None, + Some(Duration), + Indefinite, +} + +/// The behavior that drives the contents of a window. +/// +/// With winit and appit, the act of populating the window is up to the +/// consumers of the libraries. This trait provides functions for each of the +/// events a window may receive, enabling the type to react and update its +/// state. +pub trait WindowBehavior: Sized + 'static { + /// A type that is passed to [`initialize()`](Self::initialize). + /// + /// This allows providing data to the window from the thread that is opening + /// the window without requiring that `WindowBehavior` also be `Send`. + type Context: Send; + + /// Returns a new window builder for this behavior. When the window is + /// initialized, a default [`Context`](Self::Context) will be passed. + fn build(app: &App) -> WindowBuilder<'_, Self, App> + where + App: Application, + Self::Context: Default, + { + Self::build_with(app, ::default()) + } + + /// Returns a new window builder for this behavior. When the window is + /// initialized, the provided context will be passed. + fn build_with(app: &App, context: Self::Context) -> WindowBuilder<'_, Self, App> + where + App: Application, + { + WindowBuilder::new(app, context) + } + + /// Opens a new window with a default instance of this behavior's + /// [`Context`](Self::Context). The events of the window will be processed + /// in a thread spawned by this function. + /// + /// If the application has shut down, this function returns None. + /// + /// # Errors + /// + /// The only errors this funciton can return arise from + /// [`winit::window::WindowBuilder::build`]. + fn open(app: &App) -> Result, OsError> + where + App: Application, + Self::Context: Default, + { + Self::build(app).open() + } + + /// Opens a new window with the provided [`Context`](Self::Context). The + /// events of the window will be processed in a thread spawned by this + /// function. + /// + /// If the application has shut down, this function returns None. + /// + /// # Errors + /// + /// The only errors this funciton can return arise from + /// [`winit::window::WindowBuilder::build`]. + fn open_with(app: &App, context: Self::Context) -> Result, OsError> + where + App: Application, + { + Self::build_with(app, context).open() + } + + /// Runs a window with a default instance of this behavior's + /// [`Context`](Self::Context). + /// + /// This function is shorthand for creating a [`PendingApp`], opening this + /// window inside of it, and running the pending app. + fn run() -> ! + where + Self::Context: Default, + { + let app = PendingApp::new(); + Self::open(&app).expect("error opening initial window"); + app.run() + } + + /// Runs a window with the provided [`Context`](Self::Context). + /// + /// This function is shorthand for creating a [`PendingApp`], opening this + /// window inside of it, and running the pending app. + fn run_with(context: Self::Context) -> ! { + let app = PendingApp::new(); + Self::open_with(&app, context).expect("error opening initial window"); + app.run() + } + + /// Returns a new instance of this behavior after initializing itself with + /// the window and context. + fn initialize(window: &mut RunningWindow, context: Self::Context) -> Self; + + /// Displays the contents of the window. + fn redraw(&mut self, window: &mut RunningWindow); + + /// The window has been requested to be closed. This can happen as a result + /// of the user clicking the close button. + /// + /// If the window should be closed, return true. To prevent closing the + /// window, return false. + #[allow(unused_variables)] + fn close_requested(&mut self, window: &mut RunningWindow) -> bool { + true + } + + /// The window has gained or lost keyboard focus. + /// [`RunningWindow::focused()`] returns the current state. + #[allow(unused_variables)] + fn focus_changed(&mut self, window: &mut RunningWindow) {} + + /// The window has been occluded or revealed. [`RunningWindow::occluded()`] + /// returns the current state. + #[allow(unused_variables)] + fn occlusion_changed(&mut self, window: &mut RunningWindow) {} + + /// The window's scale factor has changed. [`RunningWindow::scale()`] + /// returns the current scale. + #[allow(unused_variables)] + fn scale_factor_changed(&mut self, window: &mut RunningWindow) {} + + /// The window has been resized. [`RunningWindow::inner_size()`] + /// returns the current size. + #[allow(unused_variables)] + fn resized(&mut self, window: &mut RunningWindow) {} + + /// The window's theme has been updated. [`RunningWindow::theme()`] + /// returns the current theme. + #[allow(unused_variables)] + fn theme_changed(&mut self, window: &mut RunningWindow) {} + + /// A file has been dropped on the window. + #[allow(unused_variables)] + fn dropped_file(&mut self, window: &mut RunningWindow, path: PathBuf) {} + + /// A file is hovering over the window. + #[allow(unused_variables)] + fn hovered_file(&mut self, window: &mut RunningWindow, path: PathBuf) {} + + /// A file being overed has been cancelled. + #[allow(unused_variables)] + fn hovered_file_cancelled(&mut self, window: &mut RunningWindow) {} + + /// An input event has generated a character. + #[allow(unused_variables)] + fn received_character(&mut self, window: &mut RunningWindow, char: char) {} + + /// A keyboard event occurred while the window was focused. + #[allow(unused_variables)] + fn keyboard_input( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + input: KeyboardInput, + is_synthetic: bool, + ) { + } + + /// The keyboard modifier keys have changed. [`RunningWindow::modifiers()`] + /// returns the current modifier keys state. + #[allow(unused_variables)] + fn modifiers_changed(&mut self, window: &mut RunningWindow) {} + + /// An international input even thas occurred for the window. + #[allow(unused_variables)] + fn ime(&mut self, window: &mut RunningWindow, ime: Ime) {} + + /// A cursor has moved over the window. + #[allow(unused_variables)] + fn cursor_moved( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + position: PhysicalPosition, + ) { + } + + /// A cursor has hovered over the window. + #[allow(unused_variables)] + fn cursor_entered(&mut self, window: &mut RunningWindow, device_id: DeviceId) {} + + /// A cursor is no longer hovering over the window. + #[allow(unused_variables)] + fn cursor_left(&mut self, window: &mut RunningWindow, device_id: DeviceId) {} + + /// An event from a mouse wheel. + #[allow(unused_variables)] + fn mouse_wheel( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + delta: MouseScrollDelta, + phase: TouchPhase, + ) { + } + + /// A mouse button was pressed or released. + #[allow(unused_variables)] + fn mouse_input( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + state: ElementState, + button: MouseButton, + ) { + } + + /// A pressure-sensitive touchpad was touched. + #[allow(unused_variables)] + fn touchpad_pressure( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + pressure: f32, + stage: i64, + ) { + } + + /// A multi-axis input device has registered motion. + #[allow(unused_variables)] + fn axis_motion( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + axis: AxisId, + value: f64, + ) { + } + + /// A touch event. + #[allow(unused_variables)] + fn touch(&mut self, window: &mut RunningWindow, touch: Touch) {} + + /// A touchpad-originated magnification gesture. + #[allow(unused_variables)] + fn touchpad_magnify( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + delta: f64, + phase: TouchPhase, + ) { + } + + /// A request to smart-magnify the window. + #[allow(unused_variables)] + fn smart_magnify(&mut self, window: &mut RunningWindow, device_id: DeviceId) {} + + /// A touchpad-originated rotation gesture. + #[allow(unused_variables)] + fn touchpad_rotate( + &mut self, + window: &mut RunningWindow, + device_id: DeviceId, + delta: f32, + phase: TouchPhase, + ) { + } +}