diff --git a/alchemy/Cargo.toml b/alchemy/Cargo.toml index a376ab6..45a006c 100644 --- a/alchemy/Cargo.toml +++ b/alchemy/Cargo.toml @@ -13,10 +13,13 @@ keywords = ["gui", "css", "styles", "layout", "react"] maintenance = { status = "actively-developed" } [features] +default = [] cocoa = ["alchemy-cocoa", "alchemy-lifecycle/cocoa"] +gtkrs = ["alchemy-gtkrs", "alchemy-lifecycle/gtkrs"] [dependencies] alchemy-cocoa = { version = "0.1", path = "../cocoa", optional = true } +alchemy-gtkrs = { version = "0.1", path = "../gtk", optional = true } alchemy-lifecycle = { version = "0.1", path = "../lifecycle" } alchemy-macros = { version = "0.1", path = "../macros" } alchemy-styles = { version = "0.1", path = "../styles", features = ["parser"] } diff --git a/alchemy/src/app.rs b/alchemy/src/app.rs index f1230d2..be47cf7 100644 --- a/alchemy/src/app.rs +++ b/alchemy/src/app.rs @@ -1,79 +1,49 @@ //! This module implements the Application structure and associated -//! lifecycle methods. You typically never create this struct yourself; -//! in Alchemy, there's a global `shared_app` that you should use to work -//! with the `App` struct. +//! lifecycle methods. //! //! This ensures that you can respond to application lifecycles, and so //! routing things around works correctly. -use std::sync::{Arc, Mutex}; +use std::cell::RefCell; -use alchemy_styles::{StyleSheet, THEME_ENGINE}; use alchemy_lifecycle::traits::AppDelegate; -use crate::window::WindowManager; - #[cfg(feature = "cocoa")] pub use alchemy_cocoa::app::{App as PlatformAppBridge}; -/// A default delegate that is mostly used for creating the initial struct, -/// without requiring the actual `AppDelegate` from the user. Will ideally -/// never see the light of day. -struct DefaultAppDelegate; -impl AppDelegate for DefaultAppDelegate {} +#[cfg(feature = "gtkrs")] +pub use alchemy_gtkrs::app::{App as PlatformAppBridge}; /// The Application structure itself. It holds a Mutex'd platform bridge, to /// handle communicating with the platform-specific app instance, along with a -/// delegate to forward events to. The `ThemeEngine` and `WindowManager` are -/// also stored here for easy access. +/// delegate to forward events to. pub struct App { - pub(crate) bridge: Mutex>, - pub delegate: Mutex>, - pub windows: WindowManager + pub bridge: Option>, + pub delegate: RefCell> } impl App { /// Creates a new app, allocated on the heap. Provides a pointer to /// said allocated instance so that the platform-specific app instances /// can loop events back around. - pub(crate) fn new() -> Arc { - let app = Arc::new(App { - bridge: Mutex::new(None), - delegate: Mutex::new(Box::new(DefaultAppDelegate {})), - windows: WindowManager::new() + pub fn new(state: S) -> Box { + let mut app = Box::new(App { + bridge: None, + delegate: RefCell::new(Box::new(state)) }); let app_ptr: *const App = &*app; - app.configure_bridge(app_ptr); + app.bridge = Some(RefCell::new(PlatformAppBridge::new(app_ptr))); + app } - /// Handles providing the app pointer to the inner bridge. - pub(crate) fn configure_bridge(&self, ptr: *const App) { - let mut bridge = self.bridge.lock().unwrap(); - *bridge = Some(PlatformAppBridge::new(ptr)); - } - - /// Convenience method for registering one-off styles. Typically, you would want - /// to store your stylesheets as separate files, to enable hot-reloading - but it's - /// conceivable that you might want to just have them in your app, too, and this enables - /// that use case. - pub fn register_styles(&self, theme_key: &str, stylesheet: StyleSheet) { - THEME_ENGINE.register_styles(theme_key, stylesheet); - } - /// Runs the app instance, by setting the necessary delegate and forwarding the run call /// to the inner backing application. This is a blocking operation; if you run this, you /// will want to begin your app (for real) in `AppDelegate::did_finish_launching()`. - pub fn run(&self, state: S) { - { - let mut delegate = self.delegate.lock().unwrap(); - *delegate = Box::new(state); - } - - let lock = self.bridge.lock().unwrap(); - if let Some(bridge) = &*lock { - bridge.run(); + pub fn run(&self) { + if let Some(bridge) = &self.bridge { + bridge.borrow_mut().run(); } } } @@ -85,41 +55,41 @@ impl App { impl AppDelegate for App { /// Called when the application will finish launching. fn will_finish_launching(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.will_finish_launching(); } /// Called when the application did finish launching. fn did_finish_launching(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.did_finish_launching(); } /// Called when the application will become active. We can use this, for instance, /// to resume rendering cycles and so on. fn will_become_active(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.will_become_active(); } /// Called when the application did become active. We can use this, for instance, /// to resume rendering cycles and so on. fn did_become_active(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.did_become_active(); } /// Called when the application will resigned active. We can use this, for instance, /// to pause rendering cycles and so on. fn will_resign_active(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.will_resign_active(); } /// Called when the application has resigned active. We can use this, for instance, /// to pause rendering cycles and so on. fn did_resign_active(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.did_resign_active(); } @@ -127,20 +97,13 @@ impl AppDelegate for App { /// to avoid termination if Alchemy needs more time for something, /// for whatever reason. fn should_terminate(&self) -> bool { - let delegate = self.delegate.lock().unwrap(); + let delegate = self.delegate.borrow_mut(); delegate.should_terminate() } /// Called when the application is about to terminate. fn will_terminate(&mut self) { - let mut delegate = self.delegate.lock().unwrap(); + let mut delegate = self.delegate.borrow_mut(); delegate.will_terminate(); } - - /// This is a private method, and you should not attempt to call it or - /// rely on it. It exists to enable easy loopback of Window-level events - /// on some platforms. - fn _window_will_close(&self, window_id: usize) { - self.windows.will_close(window_id); - } } diff --git a/alchemy/src/components/text.rs b/alchemy/src/components/text.rs index 105a537..cba0d8c 100644 --- a/alchemy/src/components/text.rs +++ b/alchemy/src/components/text.rs @@ -15,6 +15,9 @@ use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType}; #[cfg(feature = "cocoa")] use alchemy_cocoa::text::{Text as PlatformTextBridge}; +#[cfg(feature = "gtkrs")] +use alchemy_gtkrs::text::{Text as PlatformTextBridge}; + pub struct TextProps; /// Text rendering is a complicated mess, and being able to defer to the diff --git a/alchemy/src/components/view.rs b/alchemy/src/components/view.rs index a8ccdaf..6f0bb9c 100644 --- a/alchemy/src/components/view.rs +++ b/alchemy/src/components/view.rs @@ -17,6 +17,9 @@ use crate::components::Fragment; #[cfg(feature = "cocoa")] use alchemy_cocoa::view::{View as PlatformViewBridge}; +#[cfg(feature = "gtkrs")] +use alchemy_gtkrs::view::{View as PlatformViewBridge}; + pub struct ViewProps; /// Views are the most basic piece of the API. If you want to display something, you'll diff --git a/alchemy/src/lib.rs b/alchemy/src/lib.rs index 3bf0bfd..efcbcef 100644 --- a/alchemy/src/lib.rs +++ b/alchemy/src/lib.rs @@ -5,7 +5,6 @@ //! CSS support (no cascading) provides a familiar syntax for developers who tend to work on //! UI/UX projects, and the Component lifecycle is familiar enough to anyone who's touched React. -use std::sync::Arc; pub use lazy_static::lazy_static; use proc_macro_hack::proc_macro_hack; @@ -29,21 +28,10 @@ pub use alchemy_macros::Props; pub use alchemy_styles::{Color, styles as style_attributes, SpacedSet, StyleSheet, StylesList}; mod app; -use app::App; +pub use app::App; pub mod components; pub use components::{Fragment, Text, View}; pub mod window; pub use window::Window; - -lazy_static! { - pub(crate) static ref SHARED_APP: Arc = App::new(); -} - -/// This function supports calling the shared global application instance from anywhere in your -/// code. It's useful in cases where you need to have an escape hatch, but if you're using it as -/// such, you may want to double check your Application design to make sure you need it. -pub fn shared_app() -> Arc { - SHARED_APP.clone() -} diff --git a/alchemy/src/window/window.rs b/alchemy/src/window.rs similarity index 59% rename from alchemy/src/window/window.rs rename to alchemy/src/window.rs index 9c4a84b..014d8c9 100644 --- a/alchemy/src/window/window.rs +++ b/alchemy/src/window.rs @@ -1,7 +1,8 @@ //! Implements the Window API. It attempts to provide a nice, common interface across //! per-platform Window APIs. -use std::sync::{Arc, Mutex}; +use std::rc::Rc; +use std::cell::RefCell; use alchemy_lifecycle::{ComponentKey, RENDER_ENGINE}; use alchemy_lifecycle::rsx::RSX; @@ -9,23 +10,21 @@ use alchemy_lifecycle::traits::{Component, WindowDelegate}; use alchemy_styles::{Appearance, Style, StylesList, THEME_ENGINE}; -use crate::{App, SHARED_APP}; use crate::components::View; #[cfg(feature = "cocoa")] use alchemy_cocoa::window::{Window as PlatformWindowBridge}; -/// AppWindow contains the inner details of a Window. It's guarded by a Mutex on `Window`, -/// and you shouldn't create this yourself, but it's documented here so you can understand what -/// it holds. +#[cfg(feature = "gtkrs")] +use alchemy_gtkrs::window::{Window as PlatformWindowBridge}; + pub struct AppWindow { - pub id: usize, - pub style_keys: StylesList, + pub styles: StylesList, pub title: String, pub dimensions: (f64, f64, f64, f64), - pub bridge: PlatformWindowBridge, + pub bridge: Option, pub delegate: Box, - pub render_key: ComponentKey + render_key: Option } impl AppWindow { @@ -39,9 +38,11 @@ impl AppWindow { pub fn render(&mut self) { let mut style = Style::default(); let mut appearance = Appearance::default(); - THEME_ENGINE.configure_styles_for_keys(&self.style_keys, &mut style, &mut appearance); + THEME_ENGINE.configure_styles_for_keys(&self.styles, &mut style, &mut appearance); - self.bridge.apply_styles(&appearance); + if let Some(bridge) = &mut self.bridge { + bridge.apply_styles(&appearance); + } let children = match self.delegate.render() { Ok(opt) => opt, @@ -51,90 +52,102 @@ impl AppWindow { } }; - match RENDER_ENGINE.diff_and_render_root(self.render_key, ( - self.dimensions.2, - self.dimensions.3 - ), children) { - Ok(_) => { } - Err(e) => { eprintln!("Error rendering window! {}", e); } + if let Some(render_key) = self.render_key { + match RENDER_ENGINE.diff_and_render_root(render_key, ( + self.dimensions.2, + self.dimensions.3 + ), children) { + Ok(_) => { } + Err(e) => { eprintln!("Error rendering window! {}", e); } + } } } pub fn set_title(&mut self, title: &str) { self.title = title.into(); - self.bridge.set_title(title); + if let Some(bridge) = &mut self.bridge { + bridge.set_title(title); + } } pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) { self.dimensions = (x, y, width, height); - self.bridge.set_dimensions(x, y, width, height); + if let Some(bridge) = &mut self.bridge { + bridge.set_dimensions(x, y, width, height); + } } /// Renders and calls through to the native platform window show method. pub fn show(&mut self) { self.render(); - self.bridge.show(); + if let Some(bridge) = &mut self.bridge { + bridge.show(); + } } /// Calls through to the native platform window close method. pub fn close(&mut self) { - self.bridge.close(); + if let Some(bridge) = &mut self.bridge { + bridge.close(); + } } } -/// Windows represented... well, a Window. When you create one, you get the Window back. When you -/// show one, a clone of the pointer is added to the window manager, and removed on close. -pub struct Window(pub(crate) Arc>); +impl WindowDelegate for AppWindow {} + +/// Window represents... well, a Window. When you create one, you get the Window back. +pub struct Window(pub Rc>); impl Window { /// Creates a new window. - pub fn new(delegate: S) -> Window { - let window_id = SHARED_APP.windows.allocate_new_window_id(); - let view = View::default(); - let shared_app_ptr: *const App = &**SHARED_APP; - - // This unwrap() is fine, since we implement View ourselves in Alchemy - let backing_node = view.borrow_native_backing_node().unwrap(); - let bridge = PlatformWindowBridge::new(window_id, backing_node, shared_app_ptr); - - let key = match RENDER_ENGINE.register_root_component(view) { - Ok(key) => key, - Err(_e) => { panic!("Uhhhh this really messed up"); } - }; - - Window(Arc::new(Mutex::new(AppWindow { - id: window_id, - style_keys: "".into(), + pub fn new(delegate: S) -> Window { + let app_window = Rc::new(RefCell::new(AppWindow { + styles: "".into(), title: "".into(), dimensions: (0., 0., 0., 0.), - bridge: bridge, + bridge: None, delegate: Box::new(delegate), - render_key: key - }))) + render_key: None + })); + + let app_ptr: *const RefCell = &*app_window; + { + // This unwrap() is fine, since we implement View ourselves in Alchemy + let view = View::default(); + let backing_node = view.borrow_native_backing_node().unwrap(); + + let mut window = app_window.borrow_mut(); + window.bridge = Some(PlatformWindowBridge::new(backing_node, app_ptr)); + window.render_key = match RENDER_ENGINE.register_root_component(view) { + Ok(key) => Some(key), + Err(_e) => { panic!("Uhhhh this really messed up"); } + }; + } + + Window(app_window) } /// Renders a window. By default, a window renders nothing - make sure you implement `render()` /// on your `WindowDelegate`. Note that calling `.show()` implicitly calls this for you, so you /// rarely need to call this yourself. pub fn render(&self) { - let mut window = self.0.lock().unwrap(); + let window = self.0.borrow_mut(); window.render(); } pub fn set_title(&self, title: &str) { - let mut window = self.0.lock().unwrap(); + let mut window = self.0.borrow_mut(); window.set_title(title); } pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) { - let mut window = self.0.lock().unwrap(); + let mut window = self.0.borrow_mut(); window.set_dimensions(x, y, width, height); } /// Registers this window with the window manager, renders it, and shows it. pub fn show(&self) { - SHARED_APP.windows.add(self.0.clone()); - let mut window = self.0.lock().unwrap(); + let mut window = self.0.borrow_mut(); window.show(); } @@ -147,9 +160,7 @@ impl Window { /// Closes the window, unregistering it from the window manager in the process and ensuring the /// necessary delegate method(s) are fired. pub fn close(&self) { - let window_id = self.0.lock().unwrap().id; - SHARED_APP.windows.will_close(window_id); - let mut window = self.0.lock().unwrap(); + let mut window = self.0.borrow_mut(); window.close(); } } diff --git a/alchemy/src/window/manager.rs b/alchemy/src/window/manager.rs deleted file mode 100644 index 630c6e0..0000000 --- a/alchemy/src/window/manager.rs +++ /dev/null @@ -1,60 +0,0 @@ -//! Per-platform windows have their own nuances, and typically, their own windowservers. -//! We don't want to take away from that, but we do want to avoid scenarios where things get -//! a bit weird. -//! -//! Consider the following: let's say we have a `Window` instantiated in Rust, and we call -//! `.show()` on it. Then the window drops, on the Rust side. We should probably clean up our side, -//! right? -//! -//! There's also the fact that a user could opt to close a window. If that happens, we want to be -//! able to remove it from our structure... hence this manager that acts as a lightweight interface -//! for managing per-platform Window instances. - -use std::sync::{Arc, Mutex}; -use crate::window::AppWindow; - -/// A struct that provides a Window Manager, via some interior mutability magic. -pub struct WindowManager(Mutex>>>); - -impl WindowManager { - /// Creates a new WindowManager instance. - pub(crate) fn new() -> WindowManager { - WindowManager(Mutex::new(Vec::with_capacity(1))) - } - - /// Locks and acquires a new window ID, which our Windows use to loop back for - /// events and callbacks. - pub(crate) fn allocate_new_window_id(&self) -> usize { - let windows = self.0.lock().unwrap(); - windows.len() + 1 - } - - /// Adds an `AppWindow` to this instance. - pub(crate) fn add(&self, window: Arc>) { - let mut windows = self.0.lock().unwrap(); - if let None = windows.iter().position(|w| Arc::ptr_eq(&w, &window)) { - windows.push(window); - } - } - - /// On a `will_close` event, our delegates will loop back here and notify that a window - /// with x id is closing, and should be removed. The `WindowDelegate` `will_close()` event - /// is fired here. - /// - /// At the end of this, the window drops. - pub(crate) fn will_close(&self, window_id: usize) { - let mut windows = self.0.lock().unwrap(); - if let Some(index) = windows.iter().position(|window| { - let mut w = window.lock().unwrap(); - - if w.id == window_id { - w.delegate.will_close(); - return true; - } - - false - }) { - windows.remove(index); - } - } -} diff --git a/alchemy/src/window/mod.rs b/alchemy/src/window/mod.rs deleted file mode 100644 index 5cc5848..0000000 --- a/alchemy/src/window/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -//! This module implements Windows and their associated lifecycles. - -mod manager; -pub(crate) use manager::WindowManager; - -pub mod window; -pub use window::{AppWindow, Window}; diff --git a/cocoa/src/window.rs b/cocoa/src/window.rs index 64b4dd9..798b2a6 100644 --- a/cocoa/src/window.rs +++ b/cocoa/src/window.rs @@ -2,6 +2,8 @@ //! Cocoa and associated widgets. This also handles looping back //! lifecycle events, such as window resizing or close events. +use std::cell::RefCell; + use std::sync::{Once, ONCE_INIT}; use cocoa::base::{id, nil, YES, NO}; @@ -13,11 +15,10 @@ use objc::declare::ClassDecl; use objc::runtime::{Class, Object, Sel}; use objc::{msg_send, sel, sel_impl}; -use alchemy_lifecycle::traits::{AppDelegate, Component}; +use alchemy_lifecycle::traits::WindowDelegate; use alchemy_styles::Appearance; -static APP_PTR: &str = "alchemyAppPtr"; -static WINDOW_MANAGER_ID: &str = "alchemyWindowManagerID"; +static WINDOW_PTR: &str = "alchemyWindowPtr"; /// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime /// where our `NSWindow` and associated delegate live. @@ -30,7 +31,7 @@ impl Window { /// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance), /// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime /// pointers. - pub fn new(window_id: usize, content_view: ShareId, app_ptr: *const T) -> Window { + pub fn new(content_view: ShareId, window_ptr: *const RefCell) -> Window { let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)); let style = NSWindowStyleMask::NSResizableWindowMask | @@ -45,17 +46,13 @@ impl Window { NO ).autorelease(); - msg_send![window, setTitlebarAppearsTransparent:YES]; - //msg_send![window, setTitleVisibility:1]; - // This is very important! NSWindow is an old class and has some behavior that we need // to disable, like... this. If we don't set this, we'll segfault entirely because the // Objective-C runtime gets out of sync. msg_send![window, setReleasedWhenClosed:NO]; - //if let Some(view_ptr) = content_view.borrow_native_backing_node() { - msg_send![window, setContentView:content_view]; - //} + msg_send![window, setTitlebarAppearsTransparent:YES]; + msg_send![window, setContentView:content_view]; ShareId::from_ptr(window) }; @@ -63,8 +60,7 @@ impl Window { let delegate = unsafe { let delegate_class = register_window_class::(); let delegate: id = msg_send![delegate_class, new]; - (&mut *delegate).set_ivar(APP_PTR, app_ptr as usize); - (&mut *delegate).set_ivar(WINDOW_MANAGER_ID, window_id); + (&mut *delegate).set_ivar(WINDOW_PTR, window_ptr as usize); msg_send![inner, setDelegate:delegate]; ShareId::from_ptr(delegate) }; @@ -136,27 +132,25 @@ impl Drop for Window { /// Called when a Window receives a `windowWillClose:` event. Loops back to the shared /// Alchemy app instance, so that our window manager can act appropriately. -extern fn will_close(this: &Object, _: Sel, _: id) { +extern fn will_close(this: &Object, _: Sel, _: id) { unsafe { - let app_ptr: usize = *this.get_ivar(APP_PTR); - let window_id: usize = *this.get_ivar(WINDOW_MANAGER_ID); - let app = app_ptr as *mut T; - (*app)._window_will_close(window_id); + let window_ptr: usize = *this.get_ivar(WINDOW_PTR); + let window = window_ptr as *mut T; + (*window).will_close(); }; } /// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we /// need to do. -fn register_window_class() -> *const Class { +fn register_window_class() -> *const Class { static mut DELEGATE_CLASS: *const Class = 0 as *const Class; static INIT: Once = ONCE_INIT; INIT.call_once(|| unsafe { let superclass = Class::get("NSObject").unwrap(); - let mut decl = ClassDecl::new("alchemyWindowDelegateShim", superclass).unwrap(); + let mut decl = ClassDecl::new("AlchemyWindowDelegateShim", superclass).unwrap(); - decl.add_ivar::(APP_PTR); - decl.add_ivar::(WINDOW_MANAGER_ID); + decl.add_ivar::(WINDOW_PTR); decl.add_method(sel!(windowWillClose:), will_close:: as extern fn(&Object, _, _)); diff --git a/examples/layout/Cargo.toml b/examples/layout/Cargo.toml index c3cc60e..419c159 100644 --- a/examples/layout/Cargo.toml +++ b/examples/layout/Cargo.toml @@ -5,4 +5,4 @@ authors = ["Ryan McGrath "] edition = "2018" [dependencies] -alchemy = { path = "../../alchemy", version = "0.2.0", features = ["cocoa"] } +alchemy = { path = "../../alchemy", version = "0.2.0", features = ["gtkrs"] } diff --git a/examples/layout/src/main.rs b/examples/layout/src/main.rs index 2ea858f..0861b5c 100644 --- a/examples/layout/src/main.rs +++ b/examples/layout/src/main.rs @@ -8,40 +8,22 @@ /// @created March 26th, 2019 use alchemy::{ - AppDelegate, Component, ComponentKey, Fragment, Error, Props, rsx, RSX, styles, text, - Text, View, Window, WindowDelegate + App, AppDelegate, Error, rsx, + RSX, text, Text, View, Window, WindowDelegate }; pub struct AppState { - window: Window + window: Option } impl AppDelegate for AppState { fn did_finish_launching(&mut self) { - self.window.set_title("Layout Test"); - self.window.set_dimensions(100., 100., 600., 600.); - self.window.show(); - } -} - -#[derive(Default)] -struct BannerProps {} - -#[derive(Props)] -struct Banner; - -impl Component for Banner { - fn new(_key: ComponentKey) -> Banner { - Banner {} - } - - fn render(&self, children: Vec) -> Result { - Ok(rsx! { - - - {children} - - }) + let mut window = Window::new(WindowState {}); + window.set_title("Layout Test"); + window.set_dimensions(100., 100., 600., 600.); + window.show(); + self.window = Some(window); + println!("Should be showing"); } } @@ -54,6 +36,7 @@ impl WindowDelegate for WindowState { fn render(&self) -> Result { let messages = vec!["LOL"]; //, "wut", "BERT"]; + Ok(rsx! { "Hello there, my name is Bert" @@ -61,64 +44,13 @@ impl WindowDelegate for WindowState { {messages.iter().map(|message| rsx! { {text!("{}", message)} })} - - - - - }) } } fn main() { - let app = alchemy::shared_app(); - - app.register_styles("default", styles! { - root { background-color: #000; } - - LOL { - background-color: #307ace; - width: 500; - height: 230; - padding-top: 20; - padding-left: 20; - padding-right: 40; - } - - message { width: 500; height: 100; background-color: yellow; color: black; } - text { width: 500; height: 100; background-color: blue; color: white; } - - boxxx { - background-color: rgba(245, 217, 28, .8); - width: 100; - height: 100; - margin-top: 40; - margin-right: 20; - } - - box1 { - background-color: #f51c69; - width: 250; - height: 100; - } - - wut1 { - background-color: black; - width: 50; - height: 230; - } - - innermostBox { - background-color: green; - width: 20; - height: 20; - } - }); - - app.run(AppState { - window: Window::new(WindowState { - - }) - }); + App::new(AppState { + window: None + }).run() } diff --git a/gtk/Cargo.toml b/gtk/Cargo.toml new file mode 100644 index 0000000..cff6464 --- /dev/null +++ b/gtk/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "alchemy-gtkrs" +description = "GTK bindings for Alchemy, a cross-platform GUI framework written in Rust." +version = "0.1.0" +edition = "2018" +authors = ["Ryan McGrath "] +license = "MPL-2.0+" +repository = "https://github.com/ryanmcgrath/alchemy" +categories = ["gui", "rendering::engine", "multimedia"] +keywords = ["gui", "gtk", "react"] + +[badges] +maintenance = { status = "actively-developed" } + +[dependencies] +alchemy-lifecycle = { version = "0.1", path = "../lifecycle", features = ["gtkrs"] } +alchemy-styles = { version = "0.1", path = "../styles" } +gtk = { version = "0.6.0", features = ["v3_16"] } +gio = "0.6.0" diff --git a/gtk/src/app.rs b/gtk/src/app.rs new file mode 100644 index 0000000..8f09e1f --- /dev/null +++ b/gtk/src/app.rs @@ -0,0 +1,44 @@ +//! A wrapper for `Application` on GTK-based systems. If you opt in to the `gtkrs` feature on +//! Alchemy, this will loop system-level application events back to your `AppDelegate`. + +use std::env; + +use gio::{ApplicationFlags}; +use gtk::{Application}; +use gio::prelude::{ApplicationExt, ApplicationExtManual}; + +use alchemy_lifecycle::traits::AppDelegate; + +/// A wrapper for `gtk::Application`. +pub struct App { + pub inner: Application +} + +impl App { + /// Creates a `gtk::Application` instance, and wires up appropriate lifecycle handlers to loop + /// back to the `AppDelegate`. + pub fn new(parent_app_ptr: *const T) -> Self { + let inner = Application::new("lol.my.app", ApplicationFlags::FLAGS_NONE) + .expect("Could not create GTK Application instance!"); + + inner.connect_activate(move |app| { + println!("ACTIVATED"); + let app = parent_app_ptr as *mut T; + unsafe { + (*app).did_finish_launching(); + } + println!("HELLO"); + }); + + println!("MADE"); + App { + inner: inner + } + } + + /// Kicks off the Run Loop for the Application instance. This blocks when called. + pub fn run(&self) { + println!("RUNNING"); + self.inner.run(&env::args().collect::>()); + } +} diff --git a/gtk/src/lib.rs b/gtk/src/lib.rs new file mode 100644 index 0000000..645916b --- /dev/null +++ b/gtk/src/lib.rs @@ -0,0 +1,22 @@ +//! This crate provides a GTK backend for Alchemy, the Rust GUI framework. +//! This means that, on GTK-based systems, you'll be using native views +//! and other assorted controls. Where possible, it attempts to opt into +//! smoother rendering paths. +//! +//! # License +//! +//! Copyright 2019 Ryan McGrath. See the license files included in the root repository +//! for more information, along with credit to applicable parties for who this project +//! would not have happened. +//! +//! # Code of Conduct +//! +//! Please note that this project is released with a [Contributor Code of +//! Conduct][coc]. By participating in this project you agree to abide by its terms. +//! +//! [coc]: https://www.contributor-covenant.org/version/1/4/code-of-conduct + +pub mod app; +pub mod text; +pub mod view; +pub mod window; diff --git a/gtk/src/text.rs b/gtk/src/text.rs new file mode 100644 index 0000000..c6e11b0 --- /dev/null +++ b/gtk/src/text.rs @@ -0,0 +1,47 @@ +//! This wraps NTextField on macOS, and configures it to act like a label +//! with standard behavior that most users would expect. + +use alchemy_styles::{Color, Layout, Appearance}; + +use alchemy_lifecycle::traits::PlatformSpecificNodeType; + +static ALCHEMY_DELEGATE: &str = "alchemyDelegate"; + +/// A wrapper for `NSText`. This holds retained pointers for the Objective-C +/// runtime - namely, the view itself, and associated things such as background +/// colors and so forth. +#[derive(Debug)] +pub struct Text { +} + +impl Text { + /// Allocates a new `NSTextField` on the Objective-C side, ensuring that things like coordinate + /// flipping occur (macOS still uses (0,0) as lower-left by default), and opting in to layer + /// backed views for smoother scrolling. + pub fn new() -> Text { + Text { + } + } + + /// Returns a pointer to the underlying Objective-C view. The pointer is not mutable; however, + /// you can send messages to it (unsafely). + pub fn borrow_native_backing_node(&self) -> PlatformSpecificNodeType { + () + } + + /// Appends a child NSText (or subclassed type) to this view. + pub fn append_child(&mut self, child: PlatformSpecificNodeType) { + } + + /// Given a `&Style`, will set the frame, background color, borders and so forth. It then + /// calls `setNeedsDisplay:YES` on the Objective-C side, so that Cocoa will re-render this + /// view. + pub fn apply_styles(&mut self, appearance: &Appearance, layout: &Layout) { + } + + pub fn set_text(&mut self, text: String) { + } + + pub fn render(&mut self) { + } +} diff --git a/gtk/src/view.rs b/gtk/src/view.rs new file mode 100644 index 0000000..915b054 --- /dev/null +++ b/gtk/src/view.rs @@ -0,0 +1,38 @@ +//! Implements a View Component struct. The most common +//! basic building block of any app. Wraps NSView on macOS. + +use alchemy_styles::{Appearance, Color, Layout}; + +use alchemy_lifecycle::traits::PlatformSpecificNodeType; + +/// A wrapper for `NSView`. This holds retained pointers for the Objective-C +/// runtime - namely, the view itself, and associated things such as background +/// colors and so forth. +#[derive(Debug)] +pub struct View {} + +impl View { + /// Allocates a new `NSView` on the Objective-C side, ensuring that things like coordinate + /// flipping occur (macOS still uses (0,0) as lower-left by default), and opting in to layer + /// backed views for smoother scrolling. + pub fn new() -> View { + View { + } + } + + /// Returns a pointer to the underlying Objective-C view. The pointer is not mutable; however, + /// you can send messages to it (unsafely). + pub fn borrow_native_backing_node(&self) -> PlatformSpecificNodeType { + () + } + + /// Appends a child NSView (or subclassed type) to this view. + pub fn append_child(&mut self, child: PlatformSpecificNodeType) { + } + + /// Given a `&Style`, will set the frame, background color, borders and so forth. It then + /// calls `setNeedsDisplay:YES` on the Objective-C side, so that Cocoa will re-render this + /// view. + pub fn apply_styles(&mut self, appearance: &Appearance, layout: &Layout) { + } +} diff --git a/gtk/src/window.rs b/gtk/src/window.rs new file mode 100644 index 0000000..f04b174 --- /dev/null +++ b/gtk/src/window.rs @@ -0,0 +1,73 @@ +//! Implements a `gtk::ApplicationWindow` wrapper for GTK-based systems. +//! This also handles looping back lifecycle events, such as window +//! resizing or close events. + +use std::cell::RefCell; + +use gtk::{ + ContainerExt, + GtkWindowExt, WidgetExt, + Window as GtkWindow, WindowType +}; + +use alchemy_lifecycle::traits::WindowDelegate; +use alchemy_styles::Appearance; + +/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime +/// where our `NSWindow` and associated delegate live. +pub struct Window { + pub inner: GtkWindow +} + +impl Window { + /// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance), + /// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime + /// pointers. + pub fn new(content_view: (), app_ptr: *const RefCell) -> Window { + Window { + inner: GtkWindow::new(WindowType::Toplevel) + } + } + + pub fn set_title(&mut self, title: &str) { + self.inner.set_title(title); + } + + pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) { + self.inner.set_position(gtk::WindowPosition::Center); + self.inner.set_default_size(width as i32, height as i32); + } + + /// Normally used for setting platform-specific styles; on macOS we choose not to do this and + /// just have the content view handle the background color, as calling window + /// setBackgroundColor causes some notable lag on resizing. + pub fn apply_styles(&mut self, _appearance: &Appearance) { } + + /// On macOS, calling `show()` is equivalent to calling `makeKeyAndOrderFront`. This is the + /// most common use case, hence why this method was chosen - if you want or need something + /// else, feel free to open an issue to discuss. + /// + /// You should never be calling this yourself, mind you - Alchemy core handles this for you. + pub fn show(&self) { + let button = gtk::Button::new_with_label("Click me!"); + self.inner.add(&button); + self.inner.show_all(); + } + + /// On macOS, calling `close()` is equivalent to calling... well, `close`. It closes the + /// window. + /// + /// I dunno what else to say here, lol. + /// + /// You should never be calling this yourself, mind you - Alchemy core handles this for you. + pub fn close(&self) { + } +} + +impl Drop for Window { + /// When a Window is dropped on the Rust side, we want to ensure that we break the delegate + /// link on the Objective-C side. While this shouldn't actually be an issue, I'd rather be + /// safer than sorry. + fn drop(&mut self) { + } +} diff --git a/lifecycle/Cargo.toml b/lifecycle/Cargo.toml index a4694db..05c29a2 100644 --- a/lifecycle/Cargo.toml +++ b/lifecycle/Cargo.toml @@ -10,10 +10,13 @@ categories = ["gui", "rendering::engine", "multimedia"] keywords = ["gui", "css", "styles", "layout", "ui"] [features] +default = [] cocoa = ["objc", "objc_id"] +gtkrs = ["gtk"] [dependencies] alchemy-styles = { version = "0.1", path = "../styles" } +gtk = { version = "0.6.0", features = ["v3_16"], optional = true } objc = { version = "0.2.6", optional = true } objc_id = { version = "0.1.1", optional = true } serde_json = "1" diff --git a/lifecycle/src/reconciler/mod.rs b/lifecycle/src/reconciler/mod.rs index 093b9dc..d1390d6 100644 --- a/lifecycle/src/reconciler/mod.rs +++ b/lifecycle/src/reconciler/mod.rs @@ -114,7 +114,7 @@ impl RenderEngine { width: Dimension::Points(dimensions.0 as f32), height: Dimension::Points(dimensions.1 as f32) }; - layout_store.set_style(layout, style); + layout_store.set_style(layout, style)?; layout }; diff --git a/lifecycle/src/traits.rs b/lifecycle/src/traits.rs index 1a1d6d6..3d7d135 100644 --- a/lifecycle/src/traits.rs +++ b/lifecycle/src/traits.rs @@ -15,8 +15,9 @@ use crate::rsx::RSX; pub type PlatformSpecificNodeType = objc_id::ShareId; /// A per-platform wrapped Pointer type, used for attaching views/widgets. -#[cfg(not(feature = "cocoa"))] +#[cfg(feature = "gtkrs")] pub type PlatformSpecificNodeType = (); +//pub type PlatformSpecificNodeType = gtk::WidgetExt; /*fn update Box + Send + Sync + 'static>(component: &Component, updater: F) { let component_ptr = component as *const C as usize; @@ -26,7 +27,7 @@ pub type PlatformSpecificNodeType = (); /// Each platform tends to have their own startup routine, their own runloop, and so on. /// Alchemy recognizes this and provides an `AppDelegate` that receives events at a system /// level and allows the user to operate within the established framework per-system. -pub trait AppDelegate: Send + Sync { +pub trait AppDelegate { /// Fired when an Application is about to finish launching. fn will_finish_launching(&mut self) {} @@ -61,7 +62,7 @@ pub trait AppDelegate: Send + Sync { /// Each platform has their own `Window` API, which Alchemy attempts to pair down to one consistent /// API. This also acts as the bootstrapping point for a `render` tree. -pub trait WindowDelegate: Send + Sync { +pub trait WindowDelegate { /// Fired when this Window will close. You can use this to clean up or destroy resources, /// timers, and other things. fn will_close(&mut self) { } @@ -169,7 +170,7 @@ pub trait Component: Props + Send + Sync { /// lifecycle methods instead. Keeping `render()` pure makes components easier to think about. /// /// This method is not called if should_component_update() returns `false`. - fn render(&self, children: Vec) -> Result { Ok(RSX::None) } + fn render(&self, _children: Vec) -> Result { Ok(RSX::None) } /// This lifecycle is invoked after an error has been thrown by a descendant component. It receives /// the error that was thrown as a parameter and should return a value to update state.