Compare commits

...
This repository has been archived on 2026-03-31. You can view files and clone it, but you cannot make any changes to it's state, such as pushing and creating new issues, pull requests or comments.

1 commit
trunk ... gtk

20 changed files with 378 additions and 301 deletions

View file

@ -13,10 +13,13 @@ keywords = ["gui", "css", "styles", "layout", "react"]
maintenance = { status = "actively-developed" } maintenance = { status = "actively-developed" }
[features] [features]
default = []
cocoa = ["alchemy-cocoa", "alchemy-lifecycle/cocoa"] cocoa = ["alchemy-cocoa", "alchemy-lifecycle/cocoa"]
gtkrs = ["alchemy-gtkrs", "alchemy-lifecycle/gtkrs"]
[dependencies] [dependencies]
alchemy-cocoa = { version = "0.1", path = "../cocoa", optional = true } 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-lifecycle = { version = "0.1", path = "../lifecycle" }
alchemy-macros = { version = "0.1", path = "../macros" } alchemy-macros = { version = "0.1", path = "../macros" }
alchemy-styles = { version = "0.1", path = "../styles", features = ["parser"] } alchemy-styles = { version = "0.1", path = "../styles", features = ["parser"] }

View file

@ -1,79 +1,49 @@
//! This module implements the Application structure and associated //! This module implements the Application structure and associated
//! lifecycle methods. You typically never create this struct yourself; //! lifecycle methods.
//! in Alchemy, there's a global `shared_app` that you should use to work
//! with the `App` struct.
//! //!
//! This ensures that you can respond to application lifecycles, and so //! This ensures that you can respond to application lifecycles, and so
//! routing things around works correctly. //! 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 alchemy_lifecycle::traits::AppDelegate;
use crate::window::WindowManager;
#[cfg(feature = "cocoa")] #[cfg(feature = "cocoa")]
pub use alchemy_cocoa::app::{App as PlatformAppBridge}; pub use alchemy_cocoa::app::{App as PlatformAppBridge};
/// A default delegate that is mostly used for creating the initial struct, #[cfg(feature = "gtkrs")]
/// without requiring the actual `AppDelegate` from the user. Will ideally pub use alchemy_gtkrs::app::{App as PlatformAppBridge};
/// never see the light of day.
struct DefaultAppDelegate;
impl AppDelegate for DefaultAppDelegate {}
/// The Application structure itself. It holds a Mutex'd platform bridge, to /// The Application structure itself. It holds a Mutex'd platform bridge, to
/// handle communicating with the platform-specific app instance, along with a /// handle communicating with the platform-specific app instance, along with a
/// delegate to forward events to. The `ThemeEngine` and `WindowManager` are /// delegate to forward events to.
/// also stored here for easy access.
pub struct App { pub struct App {
pub(crate) bridge: Mutex<Option<PlatformAppBridge>>, pub bridge: Option<RefCell<PlatformAppBridge>>,
pub delegate: Mutex<Box<AppDelegate>>, pub delegate: RefCell<Box<AppDelegate>>
pub windows: WindowManager
} }
impl App { impl App {
/// Creates a new app, allocated on the heap. Provides a pointer to /// Creates a new app, allocated on the heap. Provides a pointer to
/// said allocated instance so that the platform-specific app instances /// said allocated instance so that the platform-specific app instances
/// can loop events back around. /// can loop events back around.
pub(crate) fn new() -> Arc<App> { pub fn new<S: AppDelegate + 'static>(state: S) -> Box<App> {
let app = Arc::new(App { let mut app = Box::new(App {
bridge: Mutex::new(None), bridge: None,
delegate: Mutex::new(Box::new(DefaultAppDelegate {})), delegate: RefCell::new(Box::new(state))
windows: WindowManager::new()
}); });
let app_ptr: *const App = &*app; let app_ptr: *const App = &*app;
app.configure_bridge(app_ptr); app.bridge = Some(RefCell::new(PlatformAppBridge::new(app_ptr)));
app 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 /// 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 /// 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()`. /// will want to begin your app (for real) in `AppDelegate::did_finish_launching()`.
pub fn run<S: 'static + AppDelegate>(&self, state: S) { pub fn run(&self) {
{ if let Some(bridge) = &self.bridge {
let mut delegate = self.delegate.lock().unwrap(); bridge.borrow_mut().run();
*delegate = Box::new(state);
}
let lock = self.bridge.lock().unwrap();
if let Some(bridge) = &*lock {
bridge.run();
} }
} }
} }
@ -85,41 +55,41 @@ impl App {
impl AppDelegate for App { impl AppDelegate for App {
/// Called when the application will finish launching. /// Called when the application will finish launching.
fn will_finish_launching(&mut self) { fn will_finish_launching(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.will_finish_launching(); delegate.will_finish_launching();
} }
/// Called when the application did finish launching. /// Called when the application did finish launching.
fn did_finish_launching(&mut self) { fn did_finish_launching(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.did_finish_launching(); delegate.did_finish_launching();
} }
/// Called when the application will become active. We can use this, for instance, /// Called when the application will become active. We can use this, for instance,
/// to resume rendering cycles and so on. /// to resume rendering cycles and so on.
fn will_become_active(&mut self) { fn will_become_active(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.will_become_active(); delegate.will_become_active();
} }
/// Called when the application did become active. We can use this, for instance, /// Called when the application did become active. We can use this, for instance,
/// to resume rendering cycles and so on. /// to resume rendering cycles and so on.
fn did_become_active(&mut self) { fn did_become_active(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.did_become_active(); delegate.did_become_active();
} }
/// Called when the application will resigned active. We can use this, for instance, /// Called when the application will resigned active. We can use this, for instance,
/// to pause rendering cycles and so on. /// to pause rendering cycles and so on.
fn will_resign_active(&mut self) { fn will_resign_active(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.will_resign_active(); delegate.will_resign_active();
} }
/// Called when the application has resigned active. We can use this, for instance, /// Called when the application has resigned active. We can use this, for instance,
/// to pause rendering cycles and so on. /// to pause rendering cycles and so on.
fn did_resign_active(&mut self) { fn did_resign_active(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.did_resign_active(); delegate.did_resign_active();
} }
@ -127,20 +97,13 @@ impl AppDelegate for App {
/// to avoid termination if Alchemy needs more time for something, /// to avoid termination if Alchemy needs more time for something,
/// for whatever reason. /// for whatever reason.
fn should_terminate(&self) -> bool { fn should_terminate(&self) -> bool {
let delegate = self.delegate.lock().unwrap(); let delegate = self.delegate.borrow_mut();
delegate.should_terminate() delegate.should_terminate()
} }
/// Called when the application is about to terminate. /// Called when the application is about to terminate.
fn will_terminate(&mut self) { fn will_terminate(&mut self) {
let mut delegate = self.delegate.lock().unwrap(); let mut delegate = self.delegate.borrow_mut();
delegate.will_terminate(); 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);
}
} }

View file

@ -15,6 +15,9 @@ use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType};
#[cfg(feature = "cocoa")] #[cfg(feature = "cocoa")]
use alchemy_cocoa::text::{Text as PlatformTextBridge}; use alchemy_cocoa::text::{Text as PlatformTextBridge};
#[cfg(feature = "gtkrs")]
use alchemy_gtkrs::text::{Text as PlatformTextBridge};
pub struct TextProps; pub struct TextProps;
/// Text rendering is a complicated mess, and being able to defer to the /// Text rendering is a complicated mess, and being able to defer to the

View file

@ -17,6 +17,9 @@ use crate::components::Fragment;
#[cfg(feature = "cocoa")] #[cfg(feature = "cocoa")]
use alchemy_cocoa::view::{View as PlatformViewBridge}; use alchemy_cocoa::view::{View as PlatformViewBridge};
#[cfg(feature = "gtkrs")]
use alchemy_gtkrs::view::{View as PlatformViewBridge};
pub struct ViewProps; pub struct ViewProps;
/// Views are the most basic piece of the API. If you want to display something, you'll /// Views are the most basic piece of the API. If you want to display something, you'll

View file

@ -5,7 +5,6 @@
//! CSS support (no cascading) provides a familiar syntax for developers who tend to work on //! 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. //! 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; pub use lazy_static::lazy_static;
use proc_macro_hack::proc_macro_hack; 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}; pub use alchemy_styles::{Color, styles as style_attributes, SpacedSet, StyleSheet, StylesList};
mod app; mod app;
use app::App; pub use app::App;
pub mod components; pub mod components;
pub use components::{Fragment, Text, View}; pub use components::{Fragment, Text, View};
pub mod window; pub mod window;
pub use window::Window; pub use window::Window;
lazy_static! {
pub(crate) static ref SHARED_APP: Arc<App> = 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<App> {
SHARED_APP.clone()
}

View file

@ -1,7 +1,8 @@
//! Implements the Window API. It attempts to provide a nice, common interface across //! Implements the Window API. It attempts to provide a nice, common interface across
//! per-platform Window APIs. //! 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::{ComponentKey, RENDER_ENGINE};
use alchemy_lifecycle::rsx::RSX; use alchemy_lifecycle::rsx::RSX;
@ -9,23 +10,21 @@ use alchemy_lifecycle::traits::{Component, WindowDelegate};
use alchemy_styles::{Appearance, Style, StylesList, THEME_ENGINE}; use alchemy_styles::{Appearance, Style, StylesList, THEME_ENGINE};
use crate::{App, SHARED_APP};
use crate::components::View; use crate::components::View;
#[cfg(feature = "cocoa")] #[cfg(feature = "cocoa")]
use alchemy_cocoa::window::{Window as PlatformWindowBridge}; use alchemy_cocoa::window::{Window as PlatformWindowBridge};
/// AppWindow contains the inner details of a Window. It's guarded by a Mutex on `Window`, #[cfg(feature = "gtkrs")]
/// and you shouldn't create this yourself, but it's documented here so you can understand what use alchemy_gtkrs::window::{Window as PlatformWindowBridge};
/// it holds.
pub struct AppWindow { pub struct AppWindow {
pub id: usize, pub styles: StylesList,
pub style_keys: StylesList,
pub title: String, pub title: String,
pub dimensions: (f64, f64, f64, f64), pub dimensions: (f64, f64, f64, f64),
pub bridge: PlatformWindowBridge, pub bridge: Option<PlatformWindowBridge>,
pub delegate: Box<WindowDelegate>, pub delegate: Box<WindowDelegate>,
pub render_key: ComponentKey render_key: Option<ComponentKey>
} }
impl AppWindow { impl AppWindow {
@ -39,9 +38,11 @@ impl AppWindow {
pub fn render(&mut self) { pub fn render(&mut self) {
let mut style = Style::default(); let mut style = Style::default();
let mut appearance = Appearance::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() { let children = match self.delegate.render() {
Ok(opt) => opt, Ok(opt) => opt,
@ -51,90 +52,102 @@ impl AppWindow {
} }
}; };
match RENDER_ENGINE.diff_and_render_root(self.render_key, ( if let Some(render_key) = self.render_key {
self.dimensions.2, match RENDER_ENGINE.diff_and_render_root(render_key, (
self.dimensions.3 self.dimensions.2,
), children) { self.dimensions.3
Ok(_) => { } ), children) {
Err(e) => { eprintln!("Error rendering window! {}", e); } Ok(_) => { }
Err(e) => { eprintln!("Error rendering window! {}", e); }
}
} }
} }
pub fn set_title(&mut self, title: &str) { pub fn set_title(&mut self, title: &str) {
self.title = title.into(); 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) { pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) {
self.dimensions = (x, y, width, height); 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. /// Renders and calls through to the native platform window show method.
pub fn show(&mut self) { pub fn show(&mut self) {
self.render(); self.render();
self.bridge.show(); if let Some(bridge) = &mut self.bridge {
bridge.show();
}
} }
/// Calls through to the native platform window close method. /// Calls through to the native platform window close method.
pub fn close(&mut self) { 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 impl WindowDelegate for AppWindow {}
/// show one, a clone of the pointer is added to the window manager, and removed on close.
pub struct Window(pub(crate) Arc<Mutex<AppWindow>>); /// Window represents... well, a Window. When you create one, you get the Window back.
pub struct Window(pub Rc<RefCell<AppWindow>>);
impl Window { impl Window {
/// Creates a new window. /// Creates a new window.
pub fn new<S: 'static + WindowDelegate>(delegate: S) -> Window { pub fn new<S: 'static + WindowDelegate>(delegate: S) -> Window {
let window_id = SHARED_APP.windows.allocate_new_window_id(); let app_window = Rc::new(RefCell::new(AppWindow {
let view = View::default(); styles: "".into(),
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(),
title: "".into(), title: "".into(),
dimensions: (0., 0., 0., 0.), dimensions: (0., 0., 0., 0.),
bridge: bridge, bridge: None,
delegate: Box::new(delegate), delegate: Box::new(delegate),
render_key: key render_key: None
}))) }));
let app_ptr: *const RefCell<AppWindow> = &*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()` /// 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 /// on your `WindowDelegate`. Note that calling `.show()` implicitly calls this for you, so you
/// rarely need to call this yourself. /// rarely need to call this yourself.
pub fn render(&self) { pub fn render(&self) {
let mut window = self.0.lock().unwrap(); let window = self.0.borrow_mut();
window.render(); window.render();
} }
pub fn set_title(&self, title: &str) { 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); window.set_title(title);
} }
pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) { 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); window.set_dimensions(x, y, width, height);
} }
/// Registers this window with the window manager, renders it, and shows it. /// Registers this window with the window manager, renders it, and shows it.
pub fn show(&self) { pub fn show(&self) {
SHARED_APP.windows.add(self.0.clone()); let mut window = self.0.borrow_mut();
let mut window = self.0.lock().unwrap();
window.show(); window.show();
} }
@ -147,9 +160,7 @@ impl Window {
/// Closes the window, unregistering it from the window manager in the process and ensuring the /// Closes the window, unregistering it from the window manager in the process and ensuring the
/// necessary delegate method(s) are fired. /// necessary delegate method(s) are fired.
pub fn close(&self) { pub fn close(&self) {
let window_id = self.0.lock().unwrap().id; let mut window = self.0.borrow_mut();
SHARED_APP.windows.will_close(window_id);
let mut window = self.0.lock().unwrap();
window.close(); window.close();
} }
} }

View file

@ -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<Vec<Arc<Mutex<AppWindow>>>>);
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<Mutex<AppWindow>>) {
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);
}
}
}

View file

@ -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};

View file

@ -2,6 +2,8 @@
//! Cocoa and associated widgets. This also handles looping back //! Cocoa and associated widgets. This also handles looping back
//! lifecycle events, such as window resizing or close events. //! lifecycle events, such as window resizing or close events.
use std::cell::RefCell;
use std::sync::{Once, ONCE_INIT}; use std::sync::{Once, ONCE_INIT};
use cocoa::base::{id, nil, YES, NO}; use cocoa::base::{id, nil, YES, NO};
@ -13,11 +15,10 @@ use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel}; use objc::runtime::{Class, Object, Sel};
use objc::{msg_send, sel, sel_impl}; use objc::{msg_send, sel, sel_impl};
use alchemy_lifecycle::traits::{AppDelegate, Component}; use alchemy_lifecycle::traits::WindowDelegate;
use alchemy_styles::Appearance; use alchemy_styles::Appearance;
static APP_PTR: &str = "alchemyAppPtr"; static WINDOW_PTR: &str = "alchemyWindowPtr";
static WINDOW_MANAGER_ID: &str = "alchemyWindowManagerID";
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime /// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
/// where our `NSWindow` and associated delegate live. /// 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), /// Creates a new `NSWindow` instance, configures it appropriately (e.g, titlebar appearance),
/// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime /// injects an `NSObject` delegate wrapper, and retains the necessary Objective-C runtime
/// pointers. /// pointers.
pub fn new<T: AppDelegate>(window_id: usize, content_view: ShareId<Object>, app_ptr: *const T) -> Window { pub fn new<T: WindowDelegate>(content_view: ShareId<Object>, window_ptr: *const RefCell<T>) -> Window {
let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.)); let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
let style = NSWindowStyleMask::NSResizableWindowMask | let style = NSWindowStyleMask::NSResizableWindowMask |
@ -45,17 +46,13 @@ impl Window {
NO NO
).autorelease(); ).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 // 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 // to disable, like... this. If we don't set this, we'll segfault entirely because the
// Objective-C runtime gets out of sync. // Objective-C runtime gets out of sync.
msg_send![window, setReleasedWhenClosed:NO]; msg_send![window, setReleasedWhenClosed:NO];
//if let Some(view_ptr) = content_view.borrow_native_backing_node() { msg_send![window, setTitlebarAppearsTransparent:YES];
msg_send![window, setContentView:content_view]; msg_send![window, setContentView:content_view];
//}
ShareId::from_ptr(window) ShareId::from_ptr(window)
}; };
@ -63,8 +60,7 @@ impl Window {
let delegate = unsafe { let delegate = unsafe {
let delegate_class = register_window_class::<T>(); let delegate_class = register_window_class::<T>();
let delegate: id = msg_send![delegate_class, new]; let delegate: id = msg_send![delegate_class, new];
(&mut *delegate).set_ivar(APP_PTR, app_ptr as usize); (&mut *delegate).set_ivar(WINDOW_PTR, window_ptr as usize);
(&mut *delegate).set_ivar(WINDOW_MANAGER_ID, window_id);
msg_send![inner, setDelegate:delegate]; msg_send![inner, setDelegate:delegate];
ShareId::from_ptr(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 /// Called when a Window receives a `windowWillClose:` event. Loops back to the shared
/// Alchemy app instance, so that our window manager can act appropriately. /// Alchemy app instance, so that our window manager can act appropriately.
extern fn will_close<T: AppDelegate>(this: &Object, _: Sel, _: id) { extern fn will_close<T: WindowDelegate>(this: &Object, _: Sel, _: id) {
unsafe { unsafe {
let app_ptr: usize = *this.get_ivar(APP_PTR); let window_ptr: usize = *this.get_ivar(WINDOW_PTR);
let window_id: usize = *this.get_ivar(WINDOW_MANAGER_ID); let window = window_ptr as *mut T;
let app = app_ptr as *mut T; (*window).will_close();
(*app)._window_will_close(window_id);
}; };
} }
/// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we /// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we
/// need to do. /// need to do.
fn register_window_class<T: AppDelegate>() -> *const Class { fn register_window_class<T: WindowDelegate>() -> *const Class {
static mut DELEGATE_CLASS: *const Class = 0 as *const Class; static mut DELEGATE_CLASS: *const Class = 0 as *const Class;
static INIT: Once = ONCE_INIT; static INIT: Once = ONCE_INIT;
INIT.call_once(|| unsafe { INIT.call_once(|| unsafe {
let superclass = Class::get("NSObject").unwrap(); 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::<usize>(APP_PTR); decl.add_ivar::<usize>(WINDOW_PTR);
decl.add_ivar::<usize>(WINDOW_MANAGER_ID);
decl.add_method(sel!(windowWillClose:), will_close::<T> as extern fn(&Object, _, _)); decl.add_method(sel!(windowWillClose:), will_close::<T> as extern fn(&Object, _, _));

View file

@ -5,4 +5,4 @@ authors = ["Ryan McGrath <ryan@rymc.io>"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
alchemy = { path = "../../alchemy", version = "0.2.0", features = ["cocoa"] } alchemy = { path = "../../alchemy", version = "0.2.0", features = ["gtkrs"] }

View file

@ -8,40 +8,22 @@
/// @created March 26th, 2019 /// @created March 26th, 2019
use alchemy::{ use alchemy::{
AppDelegate, Component, ComponentKey, Fragment, Error, Props, rsx, RSX, styles, text, App, AppDelegate, Error, rsx,
Text, View, Window, WindowDelegate RSX, text, Text, View, Window, WindowDelegate
}; };
pub struct AppState { pub struct AppState {
window: Window window: Option<Window>
} }
impl AppDelegate for AppState { impl AppDelegate for AppState {
fn did_finish_launching(&mut self) { fn did_finish_launching(&mut self) {
self.window.set_title("Layout Test"); let mut window = Window::new(WindowState {});
self.window.set_dimensions(100., 100., 600., 600.); window.set_title("Layout Test");
self.window.show(); window.set_dimensions(100., 100., 600., 600.);
} window.show();
} self.window = Some(window);
println!("Should be showing");
#[derive(Default)]
struct BannerProps {}
#[derive(Props)]
struct Banner;
impl Component for Banner {
fn new(_key: ComponentKey) -> Banner {
Banner {}
}
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> {
Ok(rsx! {
<Fragment>
<View styles=["wut1"]></View>
{children}
</Fragment>
})
} }
} }
@ -54,6 +36,7 @@ impl WindowDelegate for WindowState {
fn render(&self) -> Result<RSX, Error> { fn render(&self) -> Result<RSX, Error> {
let messages = vec!["LOL"]; //, "wut", "BERT"]; let messages = vec!["LOL"]; //, "wut", "BERT"];
Ok(rsx! { Ok(rsx! {
<View styles={&messages}> <View styles={&messages}>
<Text styles=["message"]>"Hello there, my name is Bert"</Text> <Text styles=["message"]>"Hello there, my name is Bert"</Text>
@ -61,64 +44,13 @@ impl WindowDelegate for WindowState {
{messages.iter().map(|message| rsx! { {messages.iter().map(|message| rsx! {
<Text styles=["text"]>{text!("{}", message)}</Text> <Text styles=["text"]>{text!("{}", message)}</Text>
})} })}
<View styles=["box1"]>
<Banner>
<View styles=["innermostBox"] />
</Banner>
</View>
</View> </View>
}) })
} }
} }
fn main() { fn main() {
let app = alchemy::shared_app(); App::new(AppState {
window: None
app.register_styles("default", styles! { }).run()
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 {
})
});
} }

19
gtk/Cargo.toml Normal file
View file

@ -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 <ryan@rymc.io>"]
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"

44
gtk/src/app.rs Normal file
View file

@ -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<T: AppDelegate + 'static>(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::<Vec<_>>());
}
}

22
gtk/src/lib.rs Normal file
View file

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

47
gtk/src/text.rs Normal file
View file

@ -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) {
}
}

38
gtk/src/view.rs Normal file
View file

@ -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) {
}
}

73
gtk/src/window.rs Normal file
View file

@ -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<T: WindowDelegate>(content_view: (), app_ptr: *const RefCell<T>) -> 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) {
}
}

View file

@ -10,10 +10,13 @@ categories = ["gui", "rendering::engine", "multimedia"]
keywords = ["gui", "css", "styles", "layout", "ui"] keywords = ["gui", "css", "styles", "layout", "ui"]
[features] [features]
default = []
cocoa = ["objc", "objc_id"] cocoa = ["objc", "objc_id"]
gtkrs = ["gtk"]
[dependencies] [dependencies]
alchemy-styles = { version = "0.1", path = "../styles" } 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 = { version = "0.2.6", optional = true }
objc_id = { version = "0.1.1", optional = true } objc_id = { version = "0.1.1", optional = true }
serde_json = "1" serde_json = "1"

View file

@ -114,7 +114,7 @@ impl RenderEngine {
width: Dimension::Points(dimensions.0 as f32), width: Dimension::Points(dimensions.0 as f32),
height: Dimension::Points(dimensions.1 as f32) height: Dimension::Points(dimensions.1 as f32)
}; };
layout_store.set_style(layout, style); layout_store.set_style(layout, style)?;
layout layout
}; };

View file

@ -15,8 +15,9 @@ use crate::rsx::RSX;
pub type PlatformSpecificNodeType = objc_id::ShareId<objc::runtime::Object>; pub type PlatformSpecificNodeType = objc_id::ShareId<objc::runtime::Object>;
/// A per-platform wrapped Pointer type, used for attaching views/widgets. /// A per-platform wrapped Pointer type, used for attaching views/widgets.
#[cfg(not(feature = "cocoa"))] #[cfg(feature = "gtkrs")]
pub type PlatformSpecificNodeType = (); pub type PlatformSpecificNodeType = ();
//pub type PlatformSpecificNodeType = gtk::WidgetExt;
/*fn update<C: Component, F: Fn() -> Box<C> + Send + Sync + 'static>(component: &Component, updater: F) { /*fn update<C: Component, F: Fn() -> Box<C> + Send + Sync + 'static>(component: &Component, updater: F) {
let component_ptr = component as *const C as usize; 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. /// 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 /// 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. /// 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. /// Fired when an Application is about to finish launching.
fn will_finish_launching(&mut self) {} 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 /// 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. /// 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, /// Fired when this Window will close. You can use this to clean up or destroy resources,
/// timers, and other things. /// timers, and other things.
fn will_close(&mut self) { } 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. /// lifecycle methods instead. Keeping `render()` pure makes components easier to think about.
/// ///
/// This method is not called if should_component_update() returns `false`. /// This method is not called if should_component_update() returns `false`.
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> { Ok(RSX::None) } fn render(&self, _children: Vec<RSX>) -> Result<RSX, Error> { Ok(RSX::None) }
/// This lifecycle is invoked after an error has been thrown by a descendant component. It receives /// 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. /// the error that was thrown as a parameter and should return a value to update state.