From 9323a47044a57d43928320fcc5f40a41535160ce Mon Sep 17 00:00:00 2001 From: TheOddGarlic Date: Sat, 16 Apr 2022 20:18:23 +0300 Subject: [PATCH] the memes will be perfect --- .gitignore | 3 + Cargo.toml | 20 ++++ abletk-common/Cargo.toml | 13 +++ abletk-common/src/color.rs | 30 +++++ abletk-common/src/lib.rs | 54 +++++++++ abletk-common/src/rect.rs | 50 +++++++++ abletk-direct2d/Cargo.toml | 26 +++++ abletk-direct2d/src/lib.rs | 218 +++++++++++++++++++++++++++++++++++++ abletk-macros/Cargo.toml | 13 +++ abletk-macros/src/lib.rs | 73 +++++++++++++ src/application.rs | 128 ++++++++++++++++++++++ src/context.rs | 56 ++++++++++ src/event/application.rs | 5 + src/event/mod.rs | 2 + src/event/window.rs | 5 + src/lib.rs | 19 ++++ src/main.rs | 9 ++ src/platform.rs | 30 +++++ src/plugin.rs | 26 +++++ src/widget/label.rs | 20 ++++ src/widget/mod.rs | 8 ++ src/window.rs | 210 +++++++++++++++++++++++++++++++++++ 22 files changed, 1018 insertions(+) create mode 100755 .gitignore create mode 100755 Cargo.toml create mode 100755 abletk-common/Cargo.toml create mode 100755 abletk-common/src/color.rs create mode 100755 abletk-common/src/lib.rs create mode 100755 abletk-common/src/rect.rs create mode 100755 abletk-direct2d/Cargo.toml create mode 100755 abletk-direct2d/src/lib.rs create mode 100755 abletk-macros/Cargo.toml create mode 100755 abletk-macros/src/lib.rs create mode 100755 src/application.rs create mode 100755 src/context.rs create mode 100755 src/event/application.rs create mode 100755 src/event/mod.rs create mode 100755 src/event/window.rs create mode 100755 src/lib.rs create mode 100755 src/main.rs create mode 100755 src/platform.rs create mode 100755 src/plugin.rs create mode 100755 src/widget/label.rs create mode 100755 src/widget/mod.rs create mode 100755 src/window.rs diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..b471067 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +/target +Cargo.lock +.idea diff --git a/Cargo.toml b/Cargo.toml new file mode 100755 index 0000000..b8c26fa --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "abletk" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +abletk-macros = { path = "./abletk-macros" } +abletk-common = { path = "./abletk-common" } +tokio = { version = "1", features = ["macros", "rt", "rt-multi-thread", "sync", "time"] } +winit = "0.26.1" +raw-window-handle = "0.4.3" + +[workspace] +members = [ + "abletk-macros", + "abletk-common", + "abletk-direct2d", +] diff --git a/abletk-common/Cargo.toml b/abletk-common/Cargo.toml new file mode 100755 index 0000000..f292f86 --- /dev/null +++ b/abletk-common/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "abletk-common" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +raw-window-handle = "0.4.3" + +[target.'cfg(windows)'.dependencies] +abletk-direct2d = { path = "../abletk-direct2d" } +windows = { version = "0.35.0", features = ["Win32_Graphics_Direct2D_Common"] } diff --git a/abletk-common/src/color.rs b/abletk-common/src/color.rs new file mode 100755 index 0000000..703d0f7 --- /dev/null +++ b/abletk-common/src/color.rs @@ -0,0 +1,30 @@ +#[cfg(windows)] +use windows::Win32::Graphics::Direct2D::Common::D2D1_COLOR_F; + +#[derive(Copy, Clone, Debug)] +pub enum Color { + RGBA(f32, f32, f32, f32), + RGB(f32, f32, f32), +} + +impl Color { + pub fn to_rgba(&self) -> (f32, f32, f32, f32) { + match *self { + Self::RGBA(r, g, b, a) => (r, g, b, a), + Self::RGB(r, g, b) => (r, g, b, 1.0), + } + } +} + +#[cfg(windows)] +impl From for D2D1_COLOR_F { + fn from(color: Color) -> Self { + let rgba = color.to_rgba(); + Self { + r: rgba.0, + g: rgba.1, + b: rgba.2, + a: rgba.3, + } + } +} diff --git a/abletk-common/src/lib.rs b/abletk-common/src/lib.rs new file mode 100755 index 0000000..96939e5 --- /dev/null +++ b/abletk-common/src/lib.rs @@ -0,0 +1,54 @@ +pub mod color; +pub mod rect; + +use raw_window_handle::RawWindowHandle; +use color::Color; +use rect::Rect; + +#[cfg(windows)] +use abletk_direct2d as backend; + +pub struct Renderer { + renderer: backend::Renderer, +} + +impl Renderer { + pub fn new(handle: RawWindowHandle) -> Self { + let handle = match handle { + RawWindowHandle::Win32(handle) => handle, + _ => todo!() + }; + + Self { + renderer: backend::Renderer::new(handle) + } + } + + pub fn begin_draw(&mut self) { + self.renderer.begin_draw() + } + + pub fn clear(&self, color: Color) { + self.renderer.clear(color) + } + + pub fn draw_text>(&self, text: S) { + self.renderer.draw_text(text.as_ref()) + } + + pub fn end_draw(&self) { + self.renderer.end_draw() + } + + pub fn fill_rect(&self, rect: Rect) { + self.renderer.fill_rect(rect) + } + + pub fn resized(&mut self, width: u32, height: u32) { + self.renderer.resized(width, height) + } + + pub fn size(&self) -> (f32, f32) { + self.renderer.size() + } +} diff --git a/abletk-common/src/rect.rs b/abletk-common/src/rect.rs new file mode 100755 index 0000000..09a3e4f --- /dev/null +++ b/abletk-common/src/rect.rs @@ -0,0 +1,50 @@ +#[cfg(windows)] +use windows::Win32::{ + Foundation::RECT, + Graphics::Direct2D::Common::{D2D_RECT_F, D2D_RECT_U}, +}; + +// fixme: consider splitting this into two types (f32 and u32) or make this generic +#[derive(Copy, Clone, Debug)] +pub struct Rect { + pub left: f32, + pub right: f32, + pub top: f32, + pub bottom: f32, +} + +#[cfg(windows)] +impl From for RECT { + fn from(rect: Rect) -> Self { + Self { + left: rect.left as i32, + right: rect.right as i32, + top: rect.top as i32, + bottom: rect.bottom as i32, + } + } +} + +#[cfg(windows)] +impl From for D2D_RECT_U { + fn from(rect: Rect) -> Self { + Self { + left: rect.left as u32, + right: rect.right as u32, + top: rect.top as u32, + bottom: rect.bottom as u32, + } + } +} + +#[cfg(windows)] +impl From for D2D_RECT_F { + fn from(rect: Rect) -> Self { + Self { + left: rect.left, + right: rect.right, + top: rect.top, + bottom: rect.bottom, + } + } +} diff --git a/abletk-direct2d/Cargo.toml b/abletk-direct2d/Cargo.toml new file mode 100755 index 0000000..bed950b --- /dev/null +++ b/abletk-direct2d/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "abletk-direct2d" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +raw-window-handle = "0.4.3" + +[dependencies.windows] +version = "0.35.0" +features = [ + "Foundation_Numerics", + "Win32_System_Com", + "Win32_Foundation", + "Win32_Graphics_Direct2D_Common", + "Win32_Graphics_Direct2D", + "Win32_Graphics_DirectWrite", + "Win32_Graphics_Dxgi_Common", + "Win32_Graphics_Gdi", + "Win32_System_LibraryLoader", + "Win32_System_SystemInformation", + "Win32_UI_WindowsAndMessaging", + "alloc", +] diff --git a/abletk-direct2d/src/lib.rs b/abletk-direct2d/src/lib.rs new file mode 100755 index 0000000..9312c9b --- /dev/null +++ b/abletk-direct2d/src/lib.rs @@ -0,0 +1,218 @@ +use std::mem; +use raw_window_handle::Win32Handle; +use windows::{ + core::Interface, Foundation::Numerics::*, Win32::Foundation::*, + Win32::Graphics::Direct2D::Common::*, Win32::Graphics::Direct2D::*, + Win32::Graphics::DirectWrite::*, Win32::UI::WindowsAndMessaging::*, +}; + +/// The default brush color is black. +const DEFAULT_BRUSH_COLOR: D2D1_COLOR_F = D2D1_COLOR_F { + r: 0.0, + g: 0.0, + b: 0.0, + a: 1.0, +}; + +const DEFAULT_BRUSH_PROPERTIES: D2D1_BRUSH_PROPERTIES = D2D1_BRUSH_PROPERTIES { + opacity: 1.0, + // Matrix3x2::identity() is not a const fn but it could be + // + // I (TheOddGarlic) sent windows-rs a PR and it got merged but now we wait + // for it to be included in the next release + transform: Matrix3x2 { + M11: 1.0, M12: 0.0, M21: 0.0, M22: 1.0, M31: 0.0, M32: 0.0 + } +}; + +pub struct Renderer { + handle: Win32Handle, + factory: ID2D1Factory1, + dw_factory: IDWriteFactory, + target: Option, + brush: Option, + stroke_style: ID2D1StrokeStyle, + system_fonts: IDWriteFontCollection, + text_format: IDWriteTextFormat +} + +impl Renderer { + pub fn new(handle: Win32Handle) -> Self { + let factory = create_factory(); + let dw_factory = create_dw_factory(); + let system_fonts = create_system_font_collection(&dw_factory); + let text_format = create_text_format(&dw_factory, &system_fonts); + let stroke_style = unsafe { + factory.CreateStrokeStyle(&Default::default(), &[]) + .unwrap() + }; + + Self { + handle, + factory, + dw_factory, + target: None, + brush: None, + stroke_style, + system_fonts, + text_format, + } + } + + pub fn begin_draw(&mut self) { + self.setup_target(); + + unsafe { self.target.as_ref().unwrap().BeginDraw() } + } + + pub fn clear>(&self, color: C) { + unsafe { self.target.as_ref().unwrap().Clear(&color.into()) } + } + + pub fn draw_text(&self, text: &str) { + unsafe { + self.target.as_ref().unwrap().DrawText( + // fixme: make this not hacky + &text.as_bytes().iter().map(|b| *b as u16).collect::>(), + &self.text_format, + // fixme: unhardcode this (needs layout stuff probably) + &D2D_RECT_F { + left: 0.0, + top: 0.0, + right: 100.0, + bottom: 100.0, + }, + self.brush.as_ref().unwrap(), + D2D1_DRAW_TEXT_OPTIONS_NONE, + DWRITE_MEASURING_MODE_NATURAL, + ); + } + } + + pub fn end_draw(&self) { + unsafe { + self.target.as_ref().unwrap() + .EndDraw(std::ptr::null_mut(), std::ptr::null_mut()).unwrap() + } + } + + pub fn fill_rect>(&self, rect: R) { + unsafe { + self.target.as_ref().unwrap() + .FillRectangle( + &rect.into(), + self.brush.as_ref().unwrap()) + } + } + + pub fn resized(&mut self, width: u32, height: u32) { + unsafe { + if let Some(target) = self.target.as_ref() { + target.Resize(&D2D_SIZE_U { + width, + height + }).unwrap() + } + } + } + + pub fn size(&self) -> (f32, f32) { + let size = unsafe { + self.target.as_ref().unwrap().GetSize() + }; + (size.width, size.height) + } + + fn setup_target(&mut self) { + if self.target.is_none() { + let mut rect = RECT::default(); + unsafe { + GetClientRect( + mem::transmute::<_, HWND>(self.handle.hwnd), + &mut rect + ); + } + + let d2d_rect = D2D_SIZE_U { + width: (rect.right - rect.left) as u32, + height: (rect.bottom - rect.top) as u32, + }; + + let render_properties = D2D1_RENDER_TARGET_PROPERTIES::default(); + let hwnd_render_properties = D2D1_HWND_RENDER_TARGET_PROPERTIES { + hwnd: unsafe { mem::transmute(self.handle.hwnd) }, + pixelSize: d2d_rect, + presentOptions: D2D1_PRESENT_OPTIONS_NONE, + }; + + let target = unsafe { + self.factory + .CreateHwndRenderTarget( + &render_properties, + &hwnd_render_properties) + .unwrap() + }; + + let brush = unsafe { + target.CreateSolidColorBrush(&DEFAULT_BRUSH_COLOR, &DEFAULT_BRUSH_PROPERTIES) + .unwrap() + }; + + self.target = Some(target); + self.brush = Some(brush); + } + } +} + +fn create_factory() -> ID2D1Factory1 { + let mut options = D2D1_FACTORY_OPTIONS::default(); + + if cfg!(debug_assertions) { + options.debugLevel = D2D1_DEBUG_LEVEL_INFORMATION; + } + + let mut result = None; + + unsafe { + D2D1CreateFactory( + D2D1_FACTORY_TYPE_SINGLE_THREADED, + &ID2D1Factory1::IID, + &options, + mem::transmute(&mut result) + ).map(|()| result.unwrap()) + }.unwrap() +} + +fn create_dw_factory() -> IDWriteFactory { + unsafe { + mem::transmute(DWriteCreateFactory( + DWRITE_FACTORY_TYPE_SHARED, + &IDWriteFactory::IID + ).unwrap()) + } +} + +fn create_system_font_collection(factory: &IDWriteFactory) -> IDWriteFontCollection { + unsafe { + let mut result = None; + factory.GetSystemFontCollection(&mut result, false) + .map(|_| result.unwrap()) + .unwrap() + } +} + +fn create_text_format(factory: &IDWriteFactory, fonts: &IDWriteFontCollection) + -> IDWriteTextFormat +{ + unsafe { + factory.CreateTextFormat( + "Arial", + fonts, + DWRITE_FONT_WEIGHT_NORMAL, + DWRITE_FONT_STYLE_NORMAL, + DWRITE_FONT_STRETCH_NORMAL, + 10.0 * 96.0 / 72.0, + "en-US", + ).unwrap() + } +} diff --git a/abletk-macros/Cargo.toml b/abletk-macros/Cargo.toml new file mode 100755 index 0000000..b18b1a2 --- /dev/null +++ b/abletk-macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "abletk-macros" +version = "0.1.0" +edition = "2021" + +[lib] +proc-macro = true + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +quote = "1.0.18" +syn = { version = "1.0.91", features = ["full"] } diff --git a/abletk-macros/src/lib.rs b/abletk-macros/src/lib.rs new file mode 100755 index 0000000..e427b60 --- /dev/null +++ b/abletk-macros/src/lib.rs @@ -0,0 +1,73 @@ +//! # abletk-macros +//! +//! This crate implements procedural macros to be used with AbleTK. You +//! shouldn't depend on this directly. + +#[macro_use] extern crate quote; + +use proc_macro::TokenStream; +use syn::{ItemFn, ReturnType, Type}; +use syn::spanned::Spanned; + +#[proc_macro_attribute] +pub fn launch(_attr: TokenStream, item: TokenStream) -> TokenStream { + let mut func: ItemFn = syn::parse(item).expect("parsing failed"); + assert_ne!(func.sig.ident, "main", "Function cannot be named main"); + assert_eq!(func.sig.inputs.len(), 0, "Function cannot take any arguments"); + + let asyncness = func.sig.asyncness.is_some(); + let ident = func.sig.ident.clone(); + // could be simplified if we could deref inside nested patterns + match &mut func.sig.output { + ReturnType::Type(_, ty) => { + if let Type::Infer(_) = **ty { + let span = ty.span(); + *ty = syn::parse2( + quote_spanned!(span=> ::abletk::application::Application) + ).unwrap(); + } + } + ReturnType::Default => { + let span = func.sig.span(); + let new = if asyncness { + quote_spanned! { span => + async fn #ident() -> ::abletk::application::Application + } + } else { + quote_spanned! { span => + fn #ident() -> ::abletk::application::Application + } + }; + func.sig = syn::parse2(new).unwrap(); + } + } + + let out = if asyncness { + quote! { + fn main() { + let rt = ::tokio::runtime::Builder::new_multi_thread() + // todo: make this the application name + .thread_name("abletk-tokio-runtime-worker") + .enable_time() + .build().unwrap(); + + rt.block_on(#ident()).launch(rt); + } + #func + } + } else { + quote! { + fn main() { + let rt = ::tokio::runtime::Builder::new_multi_thread() + // todo: make this the application name + .thread_name("abletk-tokio-runtime-worker") + .enable_time() + .build().unwrap(); + + #ident().launch(rt); + } + #func + } + }; + out.into() +} diff --git a/src/application.rs b/src/application.rs new file mode 100755 index 0000000..4139648 --- /dev/null +++ b/src/application.rs @@ -0,0 +1,128 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use winit::event::{Event, WindowEvent as WinitWindowEvent}; +use winit::event_loop::EventLoop; +use winit::window::WindowId; +use crate::context::Context; +use crate::event::{ + application::Event as ApplicationEvent, + window::Event as WindowEvent, +}; +use crate::plugin::Plugin; +use crate::window::{Window, WindowBuilder}; + +macro_rules! emit_app_event { + ($app:expr, $event:expr) => { + if let Some(handlers) = $app.events.get_mut(&$event) { + handlers.iter_mut().for_each(|handler| handler(&mut $app.ctx.borrow_mut())); + } + }; +} + +pub struct Application { + winit_event_loop: EventLoop<()>, + windows: HashMap, + events: HashMap>>, + ctx: Rc>, +} + +impl Application { + pub fn new() -> Self { + Default::default() + } + + pub fn apply_plugin

(self, plugin: P) -> Self + where + P: Plugin + { + plugin.apply(self) + } + + pub fn add_window(mut self, builder: WindowBuilder) -> Self { + let window = builder + .build(self.ctx.clone(), &self.winit_event_loop) + .unwrap(); + self.windows.insert(window.id(), window); + self + } + + pub fn add_windows(mut self, + builders: [WindowBuilder; N] + ) -> Self { + self.windows.extend(builders.map(|builder| { + let window = builder + .build(self.ctx.clone(), &self.winit_event_loop) + .unwrap(); + (window.id(), window) + })); + self + } + + pub fn on_event(mut self, + event: ApplicationEvent, + handler: fn(&mut Context), + ) -> Self { + if let Some(handlers) = self.events.get_mut(&event) { + handlers.push(Box::new(handler)); + } else { + self.events.insert(event, vec![Box::new(handler)]); + } + + self + } + + /// This method enters the event loop. You probably don't want to call this + /// directly, the `launch` macro will call this in the generated main + /// function. + pub fn launch(mut self, rt: tokio::runtime::Runtime) { + self.ctx.borrow_mut().set_rt(rt); + + self.winit_event_loop.run(move |event, target, control_flow| { + *control_flow = self.ctx.borrow().control_flow(); + + while let Some(builder) = self.ctx.borrow_mut().pop_window_builder() { + let window = builder.build(self.ctx.clone(), target).unwrap(); + self.windows.insert(window.id(), window); + } + + match event { + Event::WindowEvent { + window_id, + event: WinitWindowEvent::CloseRequested, + } => { + self.windows.get_mut(&window_id).unwrap() + .emit_event(WindowEvent::Closed); + self.windows.remove(&window_id); + + if self.windows.is_empty() { + emit_app_event!(self, ApplicationEvent::AllWindowsClosed); + } + } + Event::WindowEvent { + window_id, + event: WinitWindowEvent::Resized(size) + } => self.windows.get_mut(&window_id).unwrap().resized(size), + Event::MainEventsCleared => { + // determine if state changed and request redraw if needed + // rinse and repeat for every window + } + Event::RedrawRequested(window_id) => + self.windows.get_mut(&window_id).unwrap().render(), + + _ => () + } + }) + } +} + +impl Default for Application { + fn default() -> Self { + Self { + winit_event_loop: EventLoop::new(), + windows: Default::default(), + events: Default::default(), + ctx: Rc::new(RefCell::new(Context::new())), + } + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100755 index 0000000..4ab756c --- /dev/null +++ b/src/context.rs @@ -0,0 +1,56 @@ +use std::collections::vec_deque::VecDeque; +use std::future::Future; +use tokio::runtime::Runtime; +use winit::event_loop::ControlFlow; +use crate::window::WindowBuilder; + +/// Contains functionality that is shared across all of the application. +pub struct Context { + control_flow: ControlFlow, + window_queue: VecDeque, + rt: Option, +} + +impl Context { + /// Schedules the creation of a new window for the next iteration of the + /// event loop. + pub fn add_window(&mut self, builder: WindowBuilder) { + self.window_queue.push_back(builder); + } + + /// Schedules the creation of new windows for the next iteration of the + /// event loop. + pub fn add_windows(&mut self, builders: [WindowBuilder; N]) { + self.window_queue.extend(builders); + } + + /// Block on a future using the internal tokio runtime instance. + pub fn block_on(&self, future: F) -> F::Output { + self.rt.as_ref().unwrap().block_on(future) + } + + /// Schedules shutdown for the next iteration of the event loop. + pub fn quit(&mut self) { + self.control_flow = ControlFlow::Exit; + } + + pub(crate) fn new() -> Self { + Self { + control_flow: ControlFlow::Wait, + window_queue: VecDeque::new(), + rt: None, + } + } + + pub(crate) fn control_flow(&self) -> ControlFlow { + self.control_flow + } + + pub(crate) fn pop_window_builder(&mut self) -> Option { + self.window_queue.pop_back() + } + + pub(crate) fn set_rt(&mut self, rt: Runtime) { + self.rt = Some(rt) + } +} diff --git a/src/event/application.rs b/src/event/application.rs new file mode 100755 index 0000000..0609525 --- /dev/null +++ b/src/event/application.rs @@ -0,0 +1,5 @@ +#[non_exhaustive] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Event { + AllWindowsClosed, +} diff --git a/src/event/mod.rs b/src/event/mod.rs new file mode 100755 index 0000000..08ab1d0 --- /dev/null +++ b/src/event/mod.rs @@ -0,0 +1,2 @@ +pub mod application; +pub mod window; diff --git a/src/event/window.rs b/src/event/window.rs new file mode 100755 index 0000000..67561d3 --- /dev/null +++ b/src/event/window.rs @@ -0,0 +1,5 @@ +#[non_exhaustive] +#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] +pub enum Event { + Closed, +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100755 index 0000000..6e8c7c7 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,19 @@ +//! Able GUI Toolkit (AbleTK) +#[doc(inline)] pub use abletk_macros::*; +pub mod application; +pub mod context; +pub mod event; +pub mod platform; +pub mod plugin; +pub mod widget; +pub mod window; +pub mod prelude { + pub use crate::application::*; + pub use crate::event::application::Event as ApplicationEvent; + pub use crate::event::window::Event as WindowEvent; + pub use crate::platform::Platform; + pub use crate::widget::Label; + pub use crate::widget::Widget; + pub use crate::window::*; + pub use crate::launch; +} diff --git a/src/main.rs b/src/main.rs new file mode 100755 index 0000000..35788d8 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,9 @@ +use abletk::plugin::QuitPlugin; +use abletk::prelude::*; + +#[launch] +fn launch() -> _ { + Application::new() + .apply_plugin(QuitPlugin) + .add_window(Window::builder(Label::new("Hello, AbleTK!"))) +} diff --git a/src/platform.rs b/src/platform.rs new file mode 100755 index 0000000..1a11d9e --- /dev/null +++ b/src/platform.rs @@ -0,0 +1,30 @@ +#[cfg(target_os = "windows")] const TARGET_PLATFORM: Platform = Platform::Windows; +#[cfg(target_os = "linux")] const TARGET_PLATFORM: Platform = Platform::Linux; +#[cfg(target_os = "macos")] const TARGET_PLATFORM: Platform = Platform::MacOS; +#[cfg(not(any( + target_os = "windows", + target_os = "linux", + target_os = "macos", +)))] +const TARGET_PLATFORM: Platform = Platform::Unknown; + +#[non_exhaustive] +#[derive(Debug, Copy, Clone, Eq, PartialEq)] +/// An enum of operating systems that are supported by AbleTK. This does not +/// correspond to backends used by AbleTK, and is to be used by applications +/// that have platform-dependent behaviour. +// Contributors: name these according to their marketing names, including +// capitalisation, only changing the first letter to an upper +// case letter. E.g. Windows not Win32, MacOS not MacOs or macOS. +pub enum Platform { + Windows, + Linux, + MacOS, + Unknown +} + +impl Platform { + pub const fn target() -> Self { + TARGET_PLATFORM + } +} diff --git a/src/plugin.rs b/src/plugin.rs new file mode 100755 index 0000000..8876b05 --- /dev/null +++ b/src/plugin.rs @@ -0,0 +1,26 @@ +use crate::application::Application; +use crate::event::application::Event; +use crate::prelude::Platform; + +/// Plugins are modular configurators for AbleTK applications. +pub trait Plugin { + fn apply(&self, app: Application) -> Application; +} + +/// A plugin that automatically quits the app if all windows are destroyed, on +/// non-macOS platforms. +/// +/// ## Platform-specific +/// - **macOS:** It is common for macOS apps to not quit if all windows are +/// destroyed, so this plugin does exactly that. +pub struct QuitPlugin; + +impl Plugin for QuitPlugin { + fn apply(&self, app: Application) -> Application { + app.on_event(Event::AllWindowsClosed, |ctx| { + if Platform::target() != Platform::MacOS { + ctx.quit() + } + }) + } +} diff --git a/src/widget/label.rs b/src/widget/label.rs new file mode 100755 index 0000000..756d7b6 --- /dev/null +++ b/src/widget/label.rs @@ -0,0 +1,20 @@ +use abletk_common::Renderer; +use crate::widget::Widget; + +pub struct Label { + text: String +} + +impl Widget for Label { + fn draw(&self, renderer: &Renderer) { + renderer.draw_text(&self.text); + } +} + +impl Label { + pub fn new>(text: S) -> Self { + Self { + text: text.into() + } + } +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs new file mode 100755 index 0000000..e78aa15 --- /dev/null +++ b/src/widget/mod.rs @@ -0,0 +1,8 @@ +mod label; + +use abletk_common::Renderer; +pub use label::*; + +pub trait Widget { + fn draw(&self, renderer: &Renderer); +} diff --git a/src/window.rs b/src/window.rs new file mode 100755 index 0000000..7050343 --- /dev/null +++ b/src/window.rs @@ -0,0 +1,210 @@ +use std::cell::RefCell; +use std::collections::HashMap; +use std::rc::Rc; +use raw_window_handle::HasRawWindowHandle; +use winit::{ + event_loop::EventLoopWindowTarget, + window::{Window as WinitWindow, WindowBuilder as WinitWindowBuilder}, +}; +use winit::dpi::PhysicalSize; +use winit::error::OsError; +use winit::window::WindowId; +use abletk_common::color::Color; +use abletk_common::Renderer; +use crate::context::Context; +use crate::event::window::Event; +use crate::widget::Widget; + +pub struct Window { + window: WinitWindow, + events: HashMap>, + root: Box, + ctx: Rc>, + renderer: Renderer, + background: Color, +} + +impl Window { + pub fn new>( + ctx: Rc>, + root: Box, + event_loop: &EventLoopWindowTarget<()>, + events: HashMap>, + always_on_top: bool, + background: Color, + decorations: bool, + maximized: bool, + resizable: bool, + title: S, + transparent: bool, + visible: bool, + ) -> Result { + let window = WinitWindowBuilder::new() + .with_always_on_top(always_on_top) + .with_decorations(decorations) + .with_maximized(maximized) + .with_resizable(resizable) + .with_title(title) + .with_transparent(transparent) + .with_visible(visible) + .build(event_loop)?; + + Ok(Self { + renderer: Renderer::new(window.raw_window_handle()), + window, + background, + events, + root, + ctx, + }) + } + + pub fn builder(root: W) -> WindowBuilder { + WindowBuilder::new(root) + } + + pub fn render(&mut self) { + eprintln!("Rendering window with id {:?}", self.id()); + + self.renderer.begin_draw(); + self.renderer.clear(self.background); + self.root.draw(&self.renderer); + self.renderer.end_draw() + } + + pub fn set_always_on_top(&self, value: bool) { + self.window.set_always_on_top(value) + } + + pub fn focus(&self) { + self.window.focus_window() + } + + pub fn redraw(&self) { + self.window.request_redraw() + } + + pub fn set_title + Into>(&mut self, title: S) { + self.window.set_title(title.as_ref()) + } + + pub(crate) fn emit_event(&mut self, event: Event) { + if let Some(handlers) = self.events.get(&event) { + let ctx = self.ctx.clone(); + let ctx = &mut ctx.borrow_mut(); + handlers.clone().iter().for_each(|handler| handler(ctx, self)) + } + } + + pub(crate) fn id(&self) -> WindowId { + self.window.id() + } + + pub(crate) fn resized(&mut self, size: PhysicalSize) { + self.renderer.resized(size.width, size.height) + } +} + +pub struct WindowBuilder { + events: HashMap>, + always_on_top: bool, + background: Option, + decorations: bool, + maximized: bool, + resizable: bool, + title: Option, + transparent: bool, + visible: bool, + root: Box, +} + +impl WindowBuilder { + pub fn new(root: W) -> Self { + Self { + events: Default::default(), + always_on_top: false, + background: None, + decorations: true, + maximized: false, + resizable: true, + title: None, + transparent: false, + visible: true, + root: Box::new(root), + } + } + + pub fn on_event(mut self, + event: Event, + handler: fn(&mut Context, &mut Window), + ) -> Self { + if let Some(handlers) = self.events.get_mut(&event) { + handlers.push(handler); + } else { + self.events.insert(event, vec![handler]); + } + + self + } + + pub fn always_on_top(mut self, value: bool) -> Self { + self.always_on_top = value; + self + } + + pub fn background(mut self, value: Color) -> Self { + self.background = Some(value); + self + } + + pub fn decorations(mut self, value: bool) -> Self { + self.decorations = value; + self + } + + pub fn maximized(mut self, value: bool) -> Self { + self.maximized = value; + self + } + + pub fn resizable(mut self, value: bool) -> Self { + self.resizable = value; + self + } + + pub fn title>(mut self, value: S) -> Self { + self.title = Some(value.into()); + self + } + + pub fn transparent(mut self, value: bool) -> Self { + self.transparent = value; + self + } + + pub fn visible(mut self, value: bool) -> Self { + self.visible = value; + self + } + + pub fn build(self, + ctx: Rc>, + event_loop: &EventLoopWindowTarget<()> + ) -> Result { + Window::new( + ctx, + self.root, + event_loop, + self.events, + // todo: make this the application name + self.always_on_top, + self.background.unwrap_or(Color::RGB(255.0, 255.0, 255.0)), + self.decorations, + self.maximized, + self.resizable, + self.title.unwrap_or("AbleTK Window".into()), + self.transparent, + self.visible, + ) + } +}