Initial commit.

This commit is contained in:
Jonathan Johnson 2023-06-27 09:21:38 -07:00
commit 3f099070c2
No known key found for this signature in database
GPG key ID: A66D6A34D6620579
7 changed files with 1480 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
/target
/Cargo.lock

9
Cargo.toml Normal file
View file

@ -0,0 +1,9 @@
[package]
name = "appit"
version = "0.1.0"
edition = "2021"
[dependencies]
winit = "0.28.6"
raw-window-handle = "0.5.1"

40
README.md Normal file
View file

@ -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.

1
src/event_loop.rs Normal file
View file

@ -0,0 +1 @@
pub struct App {}

220
src/lib.rs Normal file
View file

@ -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<AppMessage>,
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<AppMessage>,
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<WindowMessage>,
) -> Result<Option<Arc<winit::window::Window>>, OsError> {
self.running
.windows
.open(&self.event_loop, window, sender)
.map(Some)
}
}
#[derive(Default, Clone)]
struct Windows {
data: Arc<Mutex<HashMap<WindowId, OpenWindow>>>,
}
impl Windows {
fn get(&self, id: WindowId) -> Option<Arc<winit::window::Window>> {
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<AppMessage>,
attrs: WindowAttributes,
sender: mpsc::SyncSender<WindowMessage>,
) -> Result<Arc<winit::window::Window>, 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<winit::window::Window>,
sender: mpsc::SyncSender<WindowMessage>,
}

335
src/private.rs Normal file
View file

@ -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<WindowMessage>,
) -> Result<Option<Arc<winit::window::Window>>, OsError>;
}
pub enum AppMessage {
OpenWindow {
attrs: WindowAttributes,
sender: mpsc::SyncSender<WindowMessage>,
open_sender: mpsc::SyncSender<Result<Arc<winit::window::Window>, 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<u32>),
/// The position of the window has changed. Contains the window's new position.
///
/// ## Platform-specific
///
/// - **iOS / Android / Web / Wayland:** Unsupported.
Moved(PhysicalPosition<i32>),
/// 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<f64>,
},
/// 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<u32>,
},
/// 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<winit::event::WindowEvent<'a>> 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,
},
}
}
}

873
src/window.rs Normal file
View file

@ -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<Arc<winit::window::Window>> {
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<Size>,
pub min_inner_size: Option<Size>,
pub max_inner_size: Option<Size>,
pub position: Option<Position>,
pub resizable: bool,
pub enabled_buttons: WindowButtons,
pub title: String,
pub fullscreen: Option<Fullscreen>,
pub maximized: bool,
pub visible: bool,
pub transparent: bool,
pub decorations: bool,
pub window_icon: Option<Icon>,
pub preferred_theme: Option<Theme>,
pub resize_increments: Option<Size>,
pub content_protected: bool,
pub window_level: WindowLevel,
pub parent_window: Option<Window>,
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<Option<Window>, 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::<Behavior>(self.context));
Ok(Some(window))
}
}
/// A window that is running in its own thread.
pub struct RunningWindow {
window: Arc<winit::window::Window>,
next_redraw_target: Option<RedrawTarget>,
messages: mpsc::Receiver<WindowMessage>,
app: App,
inner_size: PhysicalSize<u32>,
location: PhysicalPosition<i32>,
cursor_location: Option<PhysicalPosition<f64>>,
mouse_buttons: HashSet<MouseButton>,
keys: HashSet<VirtualKeyCode>,
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<RedrawTarget> {
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<u32> {
self.inner_size
}
/// Returns the current location of the window, in pixels.
#[must_use]
pub const fn location(&self) -> PhysicalPosition<i32> {
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<PhysicalPosition<f64>> {
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<Behavior>(&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<Behavior>(&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<Behavior>(&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<Item = VirtualKeyCode> + '_ {
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<Item = MouseButton> + '_ {
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<WindowMessage>,
) -> Result<Option<Arc<winit::window::Window>>, 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<Option<RedrawTarget>> for TimeUntilRedraw {
fn from(value: Option<RedrawTarget>) -> 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: &App) -> WindowBuilder<'_, Self, App>
where
App: Application,
Self::Context: Default,
{
Self::build_with(app, <Self::Context as Default>::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: &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: &App) -> Result<Option<Window>, 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: &App, context: Self::Context) -> Result<Option<Window>, 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<f64>,
) {
}
/// 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,
) {
}
}