diff options
-rw-r--r-- | Cargo.toml | 9 | ||||
-rw-r--r-- | src/events.rs | 115 | ||||
-rw-r--r-- | src/lib.rs | 63 | ||||
-rw-r--r-- | src/phosphor.rs | 199 | ||||
-rw-r--r-- | src/render.rs | 67 | ||||
-rw-r--r-- | src/window.rs | 397 | ||||
-rw-r--r-- | src/window_builder.rs | 30 | ||||
-rw-r--r-- | src/window_controller.rs | 49 | ||||
-rw-r--r-- | src/window_manager.rs | 139 | ||||
-rw-r--r-- | src/window_program.rs | 13 |
10 files changed, 573 insertions, 508 deletions
@@ -1,6 +1,6 @@ [package] name = "phosphor" -version = "2.0.0" +version = "3.0.0-rc1" authors = ["Ben Bridle"] edition = "2021" description = "Window management library" @@ -8,11 +8,12 @@ description = "Window management library" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -buffer = { git = "git://benbridle.com/buffer", "tag" = "v1.0.0" } +buffer = { git = "git://benbridle.com/buffer", "tag" = "v1.1.0" } +event_queue = { git = "git://benbridle.com/event_queue", "tag" = "v1.1.0" } geometry = { git = "git://benbridle.com/geometry", "tag" = "v1.0.0" } -softbuffer = "0.3.1" -winit = { version = "0.28.1", default-features=false } +softbuffer = "0.4.6" +winit = { version = "0.30.5", default-features=false, features=["rwh_06"] } [features] default = ["x11", "wayland"] diff --git a/src/events.rs b/src/events.rs new file mode 100644 index 0000000..b738e5b --- /dev/null +++ b/src/events.rs @@ -0,0 +1,115 @@ +use crate::*; + +use winit::dpi::PhysicalSize; +use winit::event::ElementState; +use winit::keyboard::KeyCode; +use winit::window::CursorIcon; + +use std::path::PathBuf; + + +pub enum Request { + SetTitle(String), + SetSize(Dimensions), + SetSizeBounds(SizeBounds), + SetResizable(bool), + SetFullscreen(bool), + SetVisible(bool), + SetPixelScale(u32), + SetCursor(Option<CursorIcon>), + Redraw, + CreateWindow(WindowBuilder), + CloseWindow, +} + + +#[derive(Debug)] +pub enum Event { + Initialise, + CloseRequest, + Close, + Resize(Dimensions), + FocusChange(bool), + CursorEnter, + CursorExit, + CursorMove(Position), + ScrollLines { axis: Axis, distance: f32 }, + ScrollPixels { axis: Axis, distance: f32 }, + MouseButton { button: MouseButton, action: Action }, + KeyboardInput { key: KeyCode, action: Action }, + CharacterInput(char), + ModifierChange(ModifiersState), + FileDrop(PathBuf), +} + + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Action { + Pressed, + Released +} + +impl Action { + pub fn is_pressed(self) -> bool { + self == Action::Pressed + } +} + +impl From<ElementState> for Action { + fn from(value: ElementState) -> Self { + match value { + ElementState::Pressed => Action::Pressed, + ElementState::Released => Action::Released, + } + } +} + + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Axis { + Horizontal, + Vertical, +} + + +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum MouseButton { + Left, + Middle, + Right, +} + + +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct SizeBounds { + pub min_width: Option<u32>, + pub max_width: Option<u32>, + pub min_height: Option<u32>, + pub max_height: Option<u32>, +} + +impl SizeBounds { + pub fn as_min_max_size(&self, scale: u32) -> (PhysicalSize<u32>, PhysicalSize<u32>) { + ( + PhysicalSize { + width: self.min_width.unwrap_or(0).saturating_mul(scale), + height: self.min_height.unwrap_or(0).saturating_mul(scale), + }, + PhysicalSize { + width: self.max_width.unwrap_or(u32::MAX).saturating_mul(scale), + height: self.max_height.unwrap_or(u32::MAX).saturating_mul(scale), + }, + ) + } +} + +impl Default for SizeBounds { + fn default() -> Self { + Self { + min_width: None, + max_width: None, + min_height: None, + max_height: None, + } + } +} @@ -1,56 +1,19 @@ -mod render; +mod events; mod window; -mod window_controller; -mod window_manager; +mod window_builder; +mod window_program; +mod phosphor; -pub use render::*; -pub use window::*; -pub use window_controller::*; -pub use window_manager::*; +pub use events::{Request, Event, Action, Axis, MouseButton, SizeBounds}; +pub(crate) use window::PhosphorWindow; +pub use window_builder::WindowBuilder; +pub use window_program::WindowProgram; pub use buffer::*; -pub use winit::{ - event::{ModifiersState, ElementState}, - event::VirtualKeyCode as KeyCode, - window::CursorIcon, -}; -pub use std::num::NonZeroU32; +pub use phosphor::Phosphor; +pub use event_queue::EventWriter; -// ----------------------------------------------------------------------------- +pub use winit::keyboard::{KeyCode, ModifiersState}; -#[derive(Copy, Clone)] -pub struct KeyboardInput { - pub action: Action, - pub key: KeyCode, -} - -impl TryFrom<winit::event::KeyboardInput> for KeyboardInput { - type Error = (); - - fn try_from(input: winit::event::KeyboardInput) -> Result<Self, ()> { - if let Some(key) = input.virtual_keycode { - Ok( Self { action: input.state.into(), key } ) - } else { - Err(()) - } - } -} - -// ----------------------------------------------------------------------------- - -#[derive(Clone, Copy, PartialEq, Debug)] -pub enum Action { Pressed, Released } - -impl Action { - pub fn is_pressed(&self) -> bool { *self == Self::Pressed } - pub fn is_released(&self) -> bool { *self == Self::Released } -} - -impl From<ElementState> for Action { - fn from(value: ElementState) -> Self { - match value { - ElementState::Pressed => Action::Pressed, - ElementState::Released => Action::Released, - } - } -} +pub type Position = geometry::Point<i32>; +pub type Dimensions = geometry::Dimensions<u32>; diff --git a/src/phosphor.rs b/src/phosphor.rs new file mode 100644 index 0000000..1e50735 --- /dev/null +++ b/src/phosphor.rs @@ -0,0 +1,199 @@ +use crate::*; + +use winit::application::ApplicationHandler; +use winit::dpi::{PhysicalPosition, PhysicalSize}; +use winit::event::{MouseButton as WinitMouseButton, MouseScrollDelta, StartCause, WindowEvent}; +use winit::event_loop::{ActiveEventLoop, ControlFlow, EventLoop}; +use winit::keyboard::PhysicalKey; +use winit::window::WindowId; +use winit::error::EventLoopError; + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + + +pub struct Phosphor { + event_loop: EventLoop<()>, + builders: Vec<WindowBuilder>, +} + +impl Phosphor { + pub fn new() -> Result<Self, ()> { + if let Ok(event_loop) = EventLoop::new() { + Ok( Self { + event_loop, + builders: Vec::new(), + } ) + } else { + Err(()) + } + } + + pub fn create_window(&mut self, window: WindowBuilder) { + self.builders.push(window); + } + + pub fn run(self) -> Result<(), EventLoopError> { + let mut application = PhosphorApplication { + builders: self.builders, + windows: HashMap::new(), + frame_start: Instant::now(), + }; + self.event_loop.run_app(&mut application) + } +} + + +struct PhosphorApplication { + builders: Vec<WindowBuilder>, + windows: HashMap<WindowId, PhosphorWindow>, + frame_start: Instant, +} + +impl PhosphorApplication { + pub fn handle_builders_and_destructors(&mut self, event_loop: &ActiveEventLoop) { + // Find marked windows, handle final requests and destroy. + let mut marked_ids = Vec::new(); + for (id, window) in &mut self.windows { + if window.marked_for_destruction { + window.handle_requests(&mut self.builders); + marked_ids.push(id.clone()); + } + } + for id in marked_ids { + self.windows.remove(&id); + } + + // Build any newly-requested windows. + while !self.builders.is_empty() { + for builder in std::mem::take(&mut self.builders) { + let mut window = PhosphorWindow::from_builder(builder, event_loop); + window.program.handle_event(Event::Initialise, &mut window.requests.as_writer()); + window.handle_requests(&mut self.builders); + self.windows.insert(window.winit.id(), window); + } + } + + // If all windows have been destroyed, close the event loop. + if self.windows.is_empty() { + event_loop.exit(); + } + } + + pub fn handle_window_event(&mut self, event: WindowEvent, id: WindowId) { + let window = match self.windows.get_mut(&id) { + Some(w) => w, + None => return, + }; + macro_rules! handle { ($event:expr) => { + window.program.handle_event($event, &mut window.requests.as_writer()) + }; } + + match event { + WindowEvent::Resized( PhysicalSize { width, height } ) => { + window.update_buffer_size(Dimensions::new(width, height)); + } + WindowEvent::CloseRequested => { + handle!(Event::CloseRequest); + } + WindowEvent::Destroyed => { + handle!(Event::Close); + } + WindowEvent::DroppedFile(path) => { + handle!(Event::FileDrop(path)); + } + WindowEvent::Focused(focused) => { + handle!(Event::FocusChange(focused)); + } + WindowEvent::KeyboardInput { event, .. } => { + if let Some(smol_str) = event.text { + if event.state.is_pressed() { + for c in smol_str.chars() { + handle!(Event::CharacterInput(c)); + } + } + } + if let PhysicalKey::Code(code) = event.physical_key { + handle!(Event::KeyboardInput { + key: code, + action: event.state.into(), + }); + } + } + WindowEvent::ModifiersChanged(modifiers) => { + handle!(Event::ModifierChange(modifiers.state())); + } + WindowEvent::CursorMoved { position, .. } => { + let pointer = Position::new( + position.x as i32 / window.scale() as i32, + position.y as i32 / window.scale() as i32, + ); + if window.pointer != Some(pointer) { + window.pointer = Some(pointer); + handle!(Event::CursorMove(pointer)); + } + } + WindowEvent::CursorEntered { .. } => { + handle!(Event::CursorEnter); + } + WindowEvent::CursorLeft { .. } => { + handle!(Event::CursorExit); + } + WindowEvent::MouseWheel { delta, .. } => match delta { + MouseScrollDelta::LineDelta(x, y) => { + if x != 0.0 { handle!(Event::ScrollLines { axis: Axis::Horizontal, distance: -x }); } + if y != 0.0 { handle!(Event::ScrollLines { axis: Axis::Vertical, distance: -y }); } + } + MouseScrollDelta::PixelDelta(PhysicalPosition {x, y}) => { + if x != 0.0 { handle!(Event::ScrollPixels { axis: Axis::Horizontal, distance: -x as f32 }); } + if y != 0.0 { handle!(Event::ScrollPixels { axis: Axis::Vertical, distance: -y as f32 }); } + } + } + WindowEvent::MouseInput { state, button, .. } => { + let action = state.into(); + match button { + WinitMouseButton::Left => handle!(Event::MouseButton { button: MouseButton::Left, action }), + WinitMouseButton::Middle => handle!(Event::MouseButton { button: MouseButton::Middle, action }), + WinitMouseButton::Right => handle!(Event::MouseButton { button: MouseButton::Right, action }), + _ => (), + } + } + WindowEvent::RedrawRequested => { + window.redraw(); + }, + _ => (), + } + } +} + +impl ApplicationHandler for PhosphorApplication { + fn resumed(&mut self, _event_loop: &ActiveEventLoop) {} + + fn new_events(&mut self, event_loop: &ActiveEventLoop, cause: StartCause) { + if let StartCause::Init = cause { + // Ensure a minimum duration between frames. + const MINIMUM_WAIT: Duration = Duration::from_millis(1); + std::thread::sleep(MINIMUM_WAIT.saturating_sub(self.frame_start.elapsed())); + self.frame_start = Instant::now(); + + event_loop.set_control_flow(ControlFlow::Poll); + self.handle_builders_and_destructors(event_loop); + } + } + + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + for (_, window) in &mut self.windows { + window.program.process(&mut window.requests.as_writer()); + window.handle_requests(&mut self.builders); + } + self.handle_builders_and_destructors(event_loop); + } + + fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) { + self.handle_window_event(event, id); + if let Some(window) = self.windows.get_mut(&id) { + window.handle_requests(&mut self.builders); + } + self.handle_builders_and_destructors(event_loop); + } +} diff --git a/src/render.rs b/src/render.rs deleted file mode 100644 index e7ec02d..0000000 --- a/src/render.rs +++ /dev/null @@ -1,67 +0,0 @@ -/// Tells a window controller whether the previous render state is intact and -/// can be updated, or was destroyed and needs to be fully redrawn. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RenderHint { - /// The buffer content is unchanged, only updates are required. - Update, - /// The buffer content has been destroyed, a full redraw is required. - Redraw, -} - -impl RenderHint { - pub fn is_update(&self) -> bool { *self == RenderHint::Update } - pub fn is_redraw(&self) -> bool { *self == RenderHint::Redraw } -} - -impl std::ops::BitAnd for RenderHint { - type Output = RenderHint; - fn bitand(self, other: RenderHint) -> Self::Output { - if self == RenderHint::Redraw || other == RenderHint::Redraw { - RenderHint::Redraw - } else { - RenderHint::Update - } - } -} - -impl std::ops::BitAndAssign for RenderHint { - fn bitand_assign(&mut self, other: RenderHint) { - *self = *self & other; - } -} - -/// A request to the window manager for a render pass to be run. -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum RenderRequest { - None, - Render(RenderHint), -} - -impl RenderRequest { - pub const NONE: RenderRequest = RenderRequest::None; - pub const UPDATE: RenderRequest = RenderRequest::Render(RenderHint::Update); - pub const REDRAW: RenderRequest = RenderRequest::Render(RenderHint::Redraw); - - pub fn is_none(&self) -> bool { *self == RenderRequest::None } - pub fn is_some(&self) -> bool { *self != RenderRequest::None } - pub fn is_update(&self) -> bool { *self == RenderRequest::UPDATE } - pub fn is_redraw(&self) -> bool { *self == RenderRequest::REDRAW } -} - -impl std::ops::BitAnd for RenderRequest { - type Output = RenderRequest; - fn bitand(self, other: RenderRequest) -> Self::Output { - use RenderRequest::*; - match (self, other) { - (None, req) => req, - (req, None) => req, - (Render(a), Render(b)) => Render(a & b), - } - } -} - -impl std::ops::BitAndAssign for RenderRequest { - fn bitand_assign(&mut self, other: RenderRequest) { - *self = *self & other; - } -} diff --git a/src/window.rs b/src/window.rs index 261af36..377d460 100644 --- a/src/window.rs +++ b/src/window.rs @@ -1,251 +1,250 @@ use crate::*; -use buffer::HasDimensions; -use winit::dpi::{Size, PhysicalSize}; -use winit::event_loop::EventLoopWindowTarget; -use winit::window::{Fullscreen, WindowId}; - -pub struct Window { - pub controller: Box<dyn WindowController>, - window: winit::window::Window, - pixel_scale: u32, - buffer: Buffer, - surface: softbuffer::Surface, +use buffer::{Dimensions, HasDimensions}; +use event_queue::EventQueue; + +use winit::dpi::PhysicalSize; +use winit::event_loop::ActiveEventLoop; +use winit::window::{CursorIcon, Fullscreen, Window}; + +use std::rc::Rc; +use std::cmp::{min, max}; + +fn to_physical_size(dimensions: Dimensions, scale: u32) -> PhysicalSize<u32> { + PhysicalSize { + width: max(1, dimensions.width * scale), + height: max(1, dimensions.height * scale), + } +} + + +pub(crate) struct PhosphorWindow { + pub winit: Rc<Window>, + pub program: Box<dyn WindowProgram>, + pub requests: EventQueue<Request>, + pub pointer: Option<Position>, + pub marked_for_destruction: bool, + pub size_bounds: SizeBounds, // logical dimensions + + scale: u32, + buffer: buffer::Buffer, + surface: softbuffer::Surface<Rc<Window>, Rc<Window>>, surface_dimensions: Dimensions, - #[allow(dead_code)] context: softbuffer::Context, - render_request: RenderRequest, - previous_cursor_position: Option<Point>, - previous_cursor: Option<CursorIcon>, - previous_title: String, + redraw_full: bool, } -impl Window { - pub fn new(event_loop: &EventLoopWindowTarget<()>, controller: Box<dyn WindowController>) -> Self { - let pixel_scale = controller.pixel_scale().get(); - let increment = PhysicalSize { width: pixel_scale, height: pixel_scale }; - let mut builder = winit::window::WindowBuilder::new() - .with_title(controller.title()) - .with_resize_increments(increment); - if let Some(exact_dimensions) = controller.exact_size() { - let exact_size = dim_to_size(exact_dimensions * pixel_scale); - builder = builder - .with_resizable(false) - .with_inner_size(exact_size) - .with_min_inner_size(exact_size) - .with_max_inner_size(exact_size); - } else if let Some(initial_dimensions) = controller.initial_size() { - builder = builder.with_inner_size(dim_to_size(initial_dimensions * pixel_scale)); - if let Some(min_dimensions) = controller.minimum_size() { - builder = builder.with_min_inner_size(dim_to_size(min_dimensions * pixel_scale)); - } - if let Some(max_dimensions) = controller.maximum_size() { - builder = builder.with_max_inner_size(dim_to_size(max_dimensions * pixel_scale)); - } + +impl PhosphorWindow { + pub fn from_builder(builder: WindowBuilder, event_loop: &ActiveEventLoop) -> Self { + let mut attr = Window::default_attributes(); + let scale = builder.scale; + let size_bounds = builder.size_bounds.unwrap_or(SizeBounds::default()); + + if let Some(size_bounds) = builder.size_bounds { + let (min_size, max_size) = size_bounds.as_min_max_size(scale); + attr = attr.with_min_inner_size(min_size); + attr = attr.with_max_inner_size(max_size); + } + if let Some(dimensions) = builder.dimensions { + attr = attr.with_inner_size(to_physical_size(dimensions, scale)); + } + match builder.fullscreen { + true => attr = attr.with_fullscreen(Some(Fullscreen::Borderless(None))), + false => attr = attr.with_fullscreen(None), }; - let window = builder.build(event_loop).unwrap(); - let context = unsafe { softbuffer::Context::new(&window) }.unwrap(); - let surface = unsafe { softbuffer::Surface::new(&context, &window) }.unwrap(); + let increment = PhysicalSize { width: scale, height: scale }; + attr = attr.with_resize_increments(increment); + + attr = attr.with_title(match builder.title { + Some(title) => title, + None => String::from("Untitled"), + }); + + let window = Rc::new(event_loop.create_window(attr).unwrap()); + let context = softbuffer::Context::new(window.clone()).unwrap(); + let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + + if let Some(cursor) = builder.cursor { + window.set_cursor(cursor); + window.set_cursor_visible(true); + } else { + window.set_cursor_visible(false); + } Self { - controller, - window, - pixel_scale, - buffer: Buffer::new(Dimensions::ZERO), + winit: window, + program: builder.program, + requests: EventQueue::new(), + pointer: None, + marked_for_destruction: false, + size_bounds, + + scale, + buffer: buffer::Buffer::new(Dimensions::ZERO), surface, surface_dimensions: Dimensions::ZERO, - context, - render_request: RenderRequest::REDRAW, - previous_cursor_position: None, - previous_cursor: Some(CursorIcon::Default), - previous_title: String::new(), + redraw_full: true, } } - pub fn id(&self) -> WindowId { - self.window.id() + pub fn handle_requests(&mut self, builders: &mut Vec<WindowBuilder>) { + for request in self.requests.drain() { + match request { + Request::CreateWindow(builder) => builders.push(builder), + Request::SetTitle(title) => self.winit.set_title(&title), + Request::SetSize(dimensions) => self.set_size(dimensions), + Request::SetSizeBounds(bounds) => self.set_size_bounds(bounds), + Request::SetResizable(resizable) => self.winit.set_resizable(resizable), + Request::SetFullscreen(fullscreen) => self.set_fullscreen(fullscreen), + Request::SetVisible(visible) => self.winit.set_visible(visible), + Request::SetPixelScale(scale) => self.set_scale(scale), + Request::SetCursor(cursor) => self.set_cursor(cursor), + Request::Redraw => self.winit.request_redraw(), + Request::CloseWindow => { + self.marked_for_destruction = true; + self.program.handle_event(Event::Close, &mut self.requests.as_writer()); + }, + } + } } - pub fn update_title(&mut self) { - let title = self.controller.title(); - if title != self.previous_title { - self.window.set_title(&title); - self.previous_title = title; - } + pub fn set_size(&mut self, dimensions: Dimensions) { + let size = to_physical_size(dimensions, self.scale); + let _ = self.winit.request_inner_size(size); } - pub fn update_cursor(&mut self) { - let cursor = self.controller.cursor(); - if cursor != self.previous_cursor { - self.window.set_cursor_visible(cursor.is_some()); - if let Some(icon) = cursor { - self.window.set_cursor_icon(icon); - } - self.previous_cursor = cursor; + pub fn set_size_bounds(&mut self, bounds: SizeBounds) { + if self.size_bounds != bounds { + self.size_bounds = bounds; + self.update_size_bounds(); } } - /// This method only requests that the window size be changed; it does not - /// alter any buffer sizes or struct fields. When the window size eventually - /// changes, the state updates are handled by `resize_buffer_and_surface`. - pub fn update_window_size(&mut self) { - let fullscreen = self.controller.fullscreen(); - if fullscreen != self.window.fullscreen().is_some() { - if fullscreen { - if let Some(monitor) = self.window.current_monitor() { - if let Some(mode) = monitor.video_modes().nth(0) { - self.window.set_fullscreen(Some(Fullscreen::Exclusive(mode))); - } - }; - } else { - self.window.set_fullscreen(None); - } - } + pub fn update_size_bounds(&mut self) { + // Set the minimum and maximum dimensions of the window. + let (min_size, max_size) = self.size_bounds.as_min_max_size(self.scale); + self.winit.set_min_inner_size(Some(min_size)); + self.winit.set_max_inner_size(Some(max_size)); + + // Resize the window to fit the new size bounds. + let current_size = self.winit.inner_size(); + let width = min(max(current_size.width, min_size.width), max_size.width); + let height = min(max(current_size.height, min_size.height), max_size.height); + let _ = self.winit.request_inner_size(PhysicalSize { width, height }); + self.update_buffer_size(Dimensions { width, height }); + } - let pixel_scale = self.controller.pixel_scale().get(); - if let Some(dimensions) = self.controller.exact_size() { - // Prevent the window properties from being modified every frame - // and interrupting the actions of the window manager. - if pixel_scale == self.pixel_scale - && dimensions * pixel_scale == self.surface_dimensions { - return; - } - self.window.set_resizable(false); - let size = dim_to_size(dimensions * pixel_scale); - self.window.set_min_inner_size(Some(size)); - self.window.set_max_inner_size(Some(size)); - self.window.set_inner_size(size); - self.render_request = RenderRequest::REDRAW; - self.resize_buffer_and_surface(dimensions * pixel_scale); - } else { - // Prevent the window properties from being modified every frame - // and interrupting the actions of the window manager. - if pixel_scale == self.pixel_scale { - return; - } - self.window.set_resizable(true); - let size = self.controller.minimum_size().map(|d| dim_to_size(d * pixel_scale)); - self.window.set_min_inner_size(size); - let size = self.controller.maximum_size().map(|d| dim_to_size(d * pixel_scale)); - self.window.set_max_inner_size(size); - let increment = PhysicalSize { width: pixel_scale, height: pixel_scale }; - self.window.set_resize_increments(Some(increment)); - self.render_request = RenderRequest::REDRAW; - self.resize_buffer_and_surface(self.surface_dimensions); - } + pub fn scale(&self) -> u32 { + self.scale } - /// Resize the frame buffer and screen surface to the new size of the window. - pub fn resize_buffer_and_surface(&mut self, physical_dimensions: Dimensions) { - if let Some((width, height)) = dim_to_nonzero_size(physical_dimensions) { - self.surface.resize(width, height).unwrap(); - self.surface_dimensions = physical_dimensions; - }; - self.pixel_scale = self.controller.pixel_scale().get(); - let logical_dimensions = physical_dimensions / self.pixel_scale; - if logical_dimensions != self.buffer.dimensions() { - self.buffer.resize(logical_dimensions); - self.controller.on_resize(logical_dimensions); - self.render_request = RenderRequest::REDRAW; + pub fn set_scale(&mut self, scale: u32) { + if self.scale != scale { + self.scale = max(1, scale); + let increment = PhysicalSize { width: scale, height: scale }; + self.winit.set_resize_increments(Some(increment)); + self.update_size_bounds(); + self.requests.write(Request::Redraw); } } - pub fn move_cursor(&mut self, position: Point) { - // Convert cursor position from physical position to logical position. - let pixel_scale = self.controller.pixel_scale().get() as i32; - let logical_position = Point::new(position.x / pixel_scale, position.y / pixel_scale); - // The cursor position is rounded to i32 from f64, so we need to ignore - // duplicate consecutive cursor positions. - if self.previous_cursor_position != Some(logical_position) { - self.previous_cursor_position = Some(logical_position); - self.controller.on_cursor_move(logical_position); + pub fn set_fullscreen(&mut self, fullscreen: bool) { + self.winit.set_fullscreen( + match fullscreen { + true => Some(Fullscreen::Borderless(None)), + false => None, + } + ); + } + + pub fn set_cursor(&mut self, cursor: Option<CursorIcon>) { + if let Some(cursor) = cursor { + self.winit.set_cursor(cursor); + self.winit.set_cursor_visible(true); + } else { + self.winit.set_cursor_visible(false); } } - pub fn handle_render_request(&mut self) { - if let RenderRequest::Render(hint) = self.controller.render_request() { - self.render_request &= RenderRequest::Render(hint); - self.window.request_redraw(); - } else if let RenderRequest::Render(_) = self.render_request { - self.window.request_redraw(); + /// Called with new physical dimensions when window is resized. + pub fn update_buffer_size(&mut self, dimensions: Dimensions) { + if dimensions.is_zero() { + return; + } + // Set surface to physical dimensions. + unsafe { + self.surface.resize( + std::num::NonZeroU32::new_unchecked(dimensions.width), + std::num::NonZeroU32::new_unchecked(dimensions.height), + ).unwrap(); + } + self.surface_dimensions = dimensions; + + // Set buffer to logical dimensions. + let logical = dimensions / self.scale; + if logical != self.buffer.dimensions() { + self.buffer.resize(logical); + self.program.handle_event(Event::Resize(logical), &mut self.requests.as_writer()); + self.redraw_full = true; } } - pub fn render(&mut self) { - // If the surface dimensions are zero, the first resize event hasn't yet - // come through, and the Surface object will panic when `.present()` is - // called on it later in this method. We will instead skip rendering for - // this frame, knowing that the pending resize will trigger a re-render. + pub fn redraw(&mut self) { + // The surface dimensions will be zero until the first resize event + // arrives, which would cause a panic when calling `surface.present()`. if self.surface_dimensions.is_zero() { return; } - let scale = self.pixel_scale as usize; - let physical_dim = self.surface_dimensions; - let logical_dim = self.buffer.dimensions(); - if let RenderRequest::Render(hint) = self.render_request { - self.controller.on_render(&mut self.buffer, hint); - } else { - // TODO: Look into the cause of reduntant unrequested render - // requests under Windows. - self.controller.on_render(&mut self.buffer, RenderHint::Redraw); - } + // Render the program to the buffer. + self.program.render(&mut self.buffer, self.redraw_full); + self.redraw_full = false; + // Render the scaled buffer to the surface. let buffer = self.buffer.as_u32_slice(); let mut surface = self.surface.buffer_mut().unwrap(); - if scale == 1 { - let overlap = std::cmp::min(buffer.len(), surface.len()); - surface[..overlap].copy_from_slice(&buffer[..overlap]); + if self.scale == 1 && self.buffer.dimensions() == self.surface_dimensions { + let length = min(buffer.len(), surface.len()); + surface[..length].copy_from_slice(&buffer[..length]); } else { - let logical_width = logical_dim.width as usize; - let logical_height = logical_dim.height as usize; - let physical_width = physical_dim.width as usize; - let physical_content_width = logical_dim.width as usize * scale; - let row_excess = physical_width.saturating_sub(physical_content_width); + let scale = self.scale as usize; + let surface_width = self.surface_dimensions.width as usize; + let surface_height = self.surface_dimensions.height as usize; + let buffer_width = self.buffer.dimensions().width as usize; + let buffer_height = self.buffer.dimensions().height as usize; + let blank_width = surface_width.saturating_sub(buffer_width * scale); + let min_width = min(buffer_width, surface_width / scale); + let min_height = min(buffer_height, surface_height / scale); + let mut bi: usize = 0; let mut si: usize = 0; - let mut row_start: usize = 0; - let mut row_end: usize = 0; - for _y in 0..logical_height { - for py in 0..scale { - if py == 0 { - row_start = si; - for _x in 0..logical_width { - surface[si..si+scale].fill(buffer[bi]); - si += scale; - bi += 1; - } - // Fill the excess space on the right edge with black. - surface[si..si+row_excess].fill(0); - si += row_excess; - row_end = si; - } else { - surface.copy_within(row_start..row_end, si); - si += physical_width; - } + for _ in 0..min_height { + // Horizontally scale a buffer row. + let row_start = si; + for _ in 0..min_width { + surface[si..si+scale].fill(buffer[bi]); + si += scale; + bi += 1; + } + // Fill fractional right edge with black. + surface[si..si+blank_width].fill(0); + si += blank_width; + let row_end = si; + // Vertically scale the buffer row. + for _ in 0..scale.saturating_sub(1) { + surface.copy_within(row_start..row_end, si); + si += surface_width; } } - // Fill the excess space on the bottom edge with black. - let excess = surface.len().saturating_sub(si); - surface[si..si+excess].fill(0); + // Fill fractional bottom edge with black. + surface[si..].fill(0); } - surface.present().unwrap(); - // Reset render_request back to the lowest variant for the next frame. - self.render_request = RenderRequest::None; + surface.present().unwrap(); } } -fn dim_to_size(dimensions: Dimensions) -> Size { - Size::Physical( PhysicalSize { - width: std::cmp::max(1, dimensions.width), - height: std::cmp::max(1, dimensions.height), - }) -} -fn dim_to_nonzero_size(dimensions: Dimensions) -> Option<(NonZeroU32, NonZeroU32)> { - let width = NonZeroU32::new(dimensions.width)?; - let height = NonZeroU32::new(dimensions.height)?; - return Some((width, height)); -} + diff --git a/src/window_builder.rs b/src/window_builder.rs new file mode 100644 index 0000000..55c99f2 --- /dev/null +++ b/src/window_builder.rs @@ -0,0 +1,30 @@ +use crate::*; + +use winit::window::{Cursor, Icon}; + + +pub struct WindowBuilder { + pub program: Box<dyn WindowProgram>, + pub size_bounds: Option<SizeBounds>, + pub dimensions: Option<Dimensions>, + pub scale: u32, + pub title: Option<String>, + pub icon: Option<Icon>, + pub cursor: Option<Cursor>, + pub fullscreen: bool, +} + +impl WindowBuilder { + pub fn new(program: Box<dyn WindowProgram>) -> Self { + Self { + program, + size_bounds: None, + dimensions: None, + scale: 1, + title: None, + icon: None, + cursor: None, + fullscreen: false, + } + } +} diff --git a/src/window_controller.rs b/src/window_controller.rs deleted file mode 100644 index 848730d..0000000 --- a/src/window_controller.rs +++ /dev/null @@ -1,49 +0,0 @@ -use crate::*; -use std::num::NonZeroU32; -use std::path::Path; - -const NON_ZERO_ONE: NonZeroU32 = unsafe { NonZeroU32::new_unchecked(1) }; - -pub trait WindowController { - fn title(&self) -> String { String::from("Phosphor") } - fn initial_size(&self) -> Option<Dimensions> { None } - fn minimum_size(&self) -> Option<Dimensions> { None } - fn maximum_size(&self) -> Option<Dimensions> { None } - fn exact_size(&self) -> Option<Dimensions> { None } - fn fullscreen(&self) -> bool { false } - fn pixel_scale(&self) -> NonZeroU32 { NON_ZERO_ONE } - - fn cursor(&mut self) -> Option<CursorIcon> { Some(CursorIcon::Default) } - fn render_request(&mut self) -> RenderRequest { RenderRequest::None } - - fn is_visible(&self) -> bool { true } - - fn on_init(&mut self) {} - fn on_resize(&mut self, _size: Dimensions) {} - fn on_move(&mut self, _position: Point) {} - fn on_focus_change(&mut self, _is_focused: bool) {} - fn on_process(&mut self) {} - fn on_render(&mut self, _buffer: &mut buffer::Buffer, _hint: RenderHint) {} - fn on_close_request(&mut self) {} - fn on_close(&mut self) {} - - fn on_cursor_enter(&mut self) {} - fn on_cursor_exit(&mut self) {} - fn on_cursor_move(&mut self, _position: Point) {} - - fn on_left_mouse_button(&mut self, _action: Action) {} - fn on_middle_mouse_button(&mut self, _action: Action) {} - fn on_right_mouse_button(&mut self, _action: Action) {} - - fn on_line_scroll_horizontal(&mut self, _delta: f64) {} - fn on_line_scroll_vertical(&mut self, _delta: f64) {} - fn on_pixel_scroll_horizontal(&mut self, _delta: f64) {} - fn on_pixel_scroll_vertical(&mut self, _delta: f64) {} - - fn on_keyboard_input(&mut self, _input: KeyboardInput) {} - fn on_keyboard_modifier_change(&mut self, _modifiers: ModifiersState) {} - fn on_character_input(&mut self, _input: char) {} - fn on_file_hover(&mut self, _path: &Path) {} - fn on_file_hover_cancel(&mut self) {} - fn on_file_drop(&mut self, _path: &Path) {} -} diff --git a/src/window_manager.rs b/src/window_manager.rs deleted file mode 100644 index 8999bcf..0000000 --- a/src/window_manager.rs +++ /dev/null @@ -1,139 +0,0 @@ -use crate::*; -use std::collections::HashMap; -use std::time::Duration; -use winit::{event::*, event_loop::{EventLoop, ControlFlow}, window::WindowId}; - -pub struct WindowManager { - event_loop: EventLoop<()>, - windows: HashMap<WindowId, Window>, - min_frame_duration: Duration, -} - -impl WindowManager { - pub fn new() -> Self { - Self { - event_loop: EventLoop::new(), - windows: HashMap::new(), - min_frame_duration: Duration::ZERO, - } - } - - pub fn with_frame_limit(mut self, limit: u64) -> Self { - self.min_frame_duration = match limit { - 0 => Duration::ZERO, - _ => Duration::from_nanos(1_000_000_000 / limit), - }; - return self; - } - - /// Add a window to the window manager before the event loop begins. - pub fn add_window(&mut self, controller: Box<dyn WindowController>) { - let window = Window::new(&self.event_loop, controller); - self.windows.insert(window.id(), window); - } - - /// Start the event loop, passing program control to the window manager. - pub fn run(mut self) -> ! { - self.event_loop.run(move |event, _window_target, control_flow| { - match event { - // Called when the event loop is first initialized. - Event::NewEvents(StartCause::Init) => (), - - Event::NewEvents(..) => { - control_flow.set_wait_timeout(self.min_frame_duration); - } - - // Called when an application suspends on a mobile platform. - Event::Suspended => (), - - // Called when an application resumes, or after initialization on non-mobile platforms. - Event::Resumed => (), - - Event::WindowEvent { window_id, event } => { - if let Some(window) = self.windows.get_mut(&window_id) { - use WindowEvent::*; - - match event { - Resized(dim) => window.resize_buffer_and_surface(Dimensions::new(dim.width, dim.height)), - Moved(position) => window.controller.on_move(Point::new(position.x, position.y)), - Focused(is_focused) => window.controller.on_focus_change(is_focused), - - CursorEntered { .. } => window.controller.on_cursor_enter(), - CursorLeft { .. } => window.controller.on_cursor_exit(), - CursorMoved { position: p, .. } => { window.move_cursor(Point::new(p.x as i32, p.y as i32)) } - - MouseWheel {delta, ..} => match delta { - MouseScrollDelta::LineDelta(x, y) => { - let (x, y) = (-x as f64, -y as f64); - if x != 0.0 {window.controller.on_line_scroll_horizontal(x)} - if y != 0.0 {window.controller.on_line_scroll_vertical(y)} - } - MouseScrollDelta::PixelDelta(point) => { - let (x, y) = (-point.x, -point.y); - if x != 0.0 {window.controller.on_pixel_scroll_horizontal(x)} - if y != 0.0 {window.controller.on_pixel_scroll_vertical(y)} - } - } - MouseInput { state, button, .. } => match button { - MouseButton::Left => window.controller.on_left_mouse_button(state.into()), - MouseButton::Middle => window.controller.on_middle_mouse_button(state.into()), - MouseButton::Right => window.controller.on_right_mouse_button(state.into()), - MouseButton::Other(_) => (), - } - - KeyboardInput { input, .. } => if let Ok(input) = input.try_into() { window.controller.on_keyboard_input(input)}, - ModifiersChanged(state) => window.controller.on_keyboard_modifier_change(state), - ReceivedCharacter(character) => window.controller.on_character_input(character), - HoveredFile(path) => window.controller.on_file_hover(&path), - HoveredFileCancelled => window.controller.on_file_hover_cancel(), - DroppedFile(path) => window.controller.on_file_drop(&path), - - CloseRequested => { - window.controller.on_close_request(); - *control_flow = ControlFlow::Exit; - }, - Destroyed => window.controller.on_close(), - - Ime(_) => (), - AxisMotion { .. } => (), - Touch(_) => (), - TouchpadRotate { .. } => (), - TouchpadPressure { .. } => (), - TouchpadMagnify { .. } => (), - SmartMagnify { .. } => (), - ScaleFactorChanged { .. } => (), - ThemeChanged(_) => (), - Occluded(_) => (), - } - } - } - - // Called before any render events are called. - Event::MainEventsCleared => { - for window in self.windows.values_mut() { - window.controller.on_process(); - window.update_title(); - window.update_window_size(); - window.update_cursor(); - window.handle_render_request(); - } - } - - // Called if a window has requested to be rendered. - Event::RedrawRequested(window_id) => { - if let Some(window) = self.windows.get_mut(&window_id) { - window.render(); - } - } - - // Called after all rendering has completed, or if there were no render requests. - Event::RedrawEventsCleared => (), - - // Called before the program closes. - Event::LoopDestroyed => (), - - _ => (), - } - }) - } -} diff --git a/src/window_program.rs b/src/window_program.rs new file mode 100644 index 0000000..9a5df2d --- /dev/null +++ b/src/window_program.rs @@ -0,0 +1,13 @@ +use crate::*; + +use buffer::Buffer; +use event_queue::*; + + +pub trait WindowProgram { + fn handle_event(&mut self, event: Event, requests: &mut EventWriter<Request>); + + fn process(&mut self, requests: &mut EventWriter<Request>); + + fn render(&mut self, buffer: &mut Buffer, full: bool); +} |