the memes will be perfect
This commit is contained in:
commit
9323a47044
3
.gitignore
vendored
Executable file
3
.gitignore
vendored
Executable file
|
@ -0,0 +1,3 @@
|
||||||
|
/target
|
||||||
|
Cargo.lock
|
||||||
|
.idea
|
20
Cargo.toml
Executable file
20
Cargo.toml
Executable file
|
@ -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",
|
||||||
|
]
|
13
abletk-common/Cargo.toml
Executable file
13
abletk-common/Cargo.toml
Executable file
|
@ -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"] }
|
30
abletk-common/src/color.rs
Executable file
30
abletk-common/src/color.rs
Executable file
|
@ -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<Color> 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
abletk-common/src/lib.rs
Executable file
54
abletk-common/src/lib.rs
Executable file
|
@ -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<S: AsRef<str>>(&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()
|
||||||
|
}
|
||||||
|
}
|
50
abletk-common/src/rect.rs
Executable file
50
abletk-common/src/rect.rs
Executable file
|
@ -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<Rect> 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<Rect> 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<Rect> for D2D_RECT_F {
|
||||||
|
fn from(rect: Rect) -> Self {
|
||||||
|
Self {
|
||||||
|
left: rect.left,
|
||||||
|
right: rect.right,
|
||||||
|
top: rect.top,
|
||||||
|
bottom: rect.bottom,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
26
abletk-direct2d/Cargo.toml
Executable file
26
abletk-direct2d/Cargo.toml
Executable file
|
@ -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",
|
||||||
|
]
|
218
abletk-direct2d/src/lib.rs
Executable file
218
abletk-direct2d/src/lib.rs
Executable file
|
@ -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<ID2D1HwndRenderTarget>,
|
||||||
|
brush: Option<ID2D1SolidColorBrush>,
|
||||||
|
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<C: Into<D2D1_COLOR_F>>(&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::<Vec<_>>(),
|
||||||
|
&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<R: Into<D2D_RECT_F>>(&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()
|
||||||
|
}
|
||||||
|
}
|
13
abletk-macros/Cargo.toml
Executable file
13
abletk-macros/Cargo.toml
Executable file
|
@ -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"] }
|
73
abletk-macros/src/lib.rs
Executable file
73
abletk-macros/src/lib.rs
Executable file
|
@ -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()
|
||||||
|
}
|
128
src/application.rs
Executable file
128
src/application.rs
Executable file
|
@ -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<WindowId, Window>,
|
||||||
|
events: HashMap<ApplicationEvent, Vec<Box<dyn FnMut(&mut Context)>>>,
|
||||||
|
ctx: Rc<RefCell<Context>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Application {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Default::default()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn apply_plugin<P>(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<const N: usize>(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())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
56
src/context.rs
Executable file
56
src/context.rs
Executable file
|
@ -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<WindowBuilder>,
|
||||||
|
rt: Option<Runtime>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<const N: usize>(&mut self, builders: [WindowBuilder; N]) {
|
||||||
|
self.window_queue.extend(builders);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Block on a future using the internal tokio runtime instance.
|
||||||
|
pub fn block_on<F: Future>(&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<WindowBuilder> {
|
||||||
|
self.window_queue.pop_back()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn set_rt(&mut self, rt: Runtime) {
|
||||||
|
self.rt = Some(rt)
|
||||||
|
}
|
||||||
|
}
|
5
src/event/application.rs
Executable file
5
src/event/application.rs
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub enum Event {
|
||||||
|
AllWindowsClosed,
|
||||||
|
}
|
2
src/event/mod.rs
Executable file
2
src/event/mod.rs
Executable file
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod application;
|
||||||
|
pub mod window;
|
5
src/event/window.rs
Executable file
5
src/event/window.rs
Executable file
|
@ -0,0 +1,5 @@
|
||||||
|
#[non_exhaustive]
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
|
pub enum Event {
|
||||||
|
Closed,
|
||||||
|
}
|
19
src/lib.rs
Executable file
19
src/lib.rs
Executable file
|
@ -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;
|
||||||
|
}
|
9
src/main.rs
Executable file
9
src/main.rs
Executable file
|
@ -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!")))
|
||||||
|
}
|
30
src/platform.rs
Executable file
30
src/platform.rs
Executable file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
26
src/plugin.rs
Executable file
26
src/plugin.rs
Executable file
|
@ -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()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
20
src/widget/label.rs
Executable file
20
src/widget/label.rs
Executable file
|
@ -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<S: Into<String>>(text: S) -> Self {
|
||||||
|
Self {
|
||||||
|
text: text.into()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
8
src/widget/mod.rs
Executable file
8
src/widget/mod.rs
Executable file
|
@ -0,0 +1,8 @@
|
||||||
|
mod label;
|
||||||
|
|
||||||
|
use abletk_common::Renderer;
|
||||||
|
pub use label::*;
|
||||||
|
|
||||||
|
pub trait Widget {
|
||||||
|
fn draw(&self, renderer: &Renderer);
|
||||||
|
}
|
210
src/window.rs
Executable file
210
src/window.rs
Executable file
|
@ -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<Event, Vec<fn(&mut Context, &mut Window)>>,
|
||||||
|
root: Box<dyn Widget>,
|
||||||
|
ctx: Rc<RefCell<Context>>,
|
||||||
|
renderer: Renderer,
|
||||||
|
background: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Window {
|
||||||
|
pub fn new<S: Into<String>>(
|
||||||
|
ctx: Rc<RefCell<Context>>,
|
||||||
|
root: Box<dyn Widget>,
|
||||||
|
event_loop: &EventLoopWindowTarget<()>,
|
||||||
|
events: HashMap<Event, Vec<fn(&mut Context, &mut Window)>>,
|
||||||
|
always_on_top: bool,
|
||||||
|
background: Color,
|
||||||
|
decorations: bool,
|
||||||
|
maximized: bool,
|
||||||
|
resizable: bool,
|
||||||
|
title: S,
|
||||||
|
transparent: bool,
|
||||||
|
visible: bool,
|
||||||
|
) -> Result<Self, OsError> {
|
||||||
|
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<W: Widget + 'static>(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<S: AsRef<str> + Into<String>>(&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<u32>) {
|
||||||
|
self.renderer.resized(size.width, size.height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct WindowBuilder {
|
||||||
|
events: HashMap<Event, Vec<fn(&mut Context, &mut Window)>>,
|
||||||
|
always_on_top: bool,
|
||||||
|
background: Option<Color>,
|
||||||
|
decorations: bool,
|
||||||
|
maximized: bool,
|
||||||
|
resizable: bool,
|
||||||
|
title: Option<String>,
|
||||||
|
transparent: bool,
|
||||||
|
visible: bool,
|
||||||
|
root: Box<dyn Widget>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl WindowBuilder {
|
||||||
|
pub fn new<W: Widget + 'static>(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<S: Into<String>>(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<RefCell<Context>>,
|
||||||
|
event_loop: &EventLoopWindowTarget<()>
|
||||||
|
) -> Result<Window, OsError> {
|
||||||
|
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,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue