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.

2 commits

133 changed files with 250 additions and 8881 deletions

10
404.html Normal file
View file

@ -0,0 +1,10 @@
<!doctype html>
<html>
<head>
<title>File Not Found: 404.</title>
</head>
<body>
<h1>Oops!</h1>
<h2>File Not Found: 404.</h2>
</body>
</html>

1
CNAME Normal file
View file

@ -0,0 +1 @@
alchemy.rs

View file

@ -1,12 +0,0 @@
[workspace]
members = [
"styles",
"macros",
"lifecycle",
"alchemy",
"examples/layout"
]
[profile.release]
lto = true
panic = "abort"

146
README.md
View file

@ -1,140 +1,24 @@
<a href="https://alchemy.rs/" title="Alchemy - A Rust GUI Framework"><img src="https://github.com/ryanmcgrath/alchemy/blob/trunk/assets/alchemy_logo_250x.png?raw=true" alt="Potion" /></a> # Alchemy Website
The source for `alchemy.rs`, which is hosted on GitHub Pages.
Alchemy - A Rust GUI Framework ## To Edit...
========================================================== This website is a static site, backed by [Zola](https://www.getzola.org/documentation/getting-started/installation/). You'll need to install it to develop it locally. Otherwise... have at it.
[![Crates.io](https://img.shields.io/crates/v/alchemy.svg)](https://crates.io/crates/alchemy) If you make substantial changes, make sure the build command outputs to the proper directory:
[Homepage](https://alchemy.rs) • [API Documentation](https://docs.rs/alchemy/) To build:
Alchemy is an _experimental_ Rust GUI Framework, backed by native widgets on each platform it supports, with an API that's a blend of those found in AppKit, UIKit, and React Native. It aims to provide an API that feels at home in Rust, while striving to provide a visual appearance that's easy to scan and parse. It does not, and will never, require nightly. It's still early stages, but feedback and contributions are welcome. ```
cd src
## Supported Platforms ./build
Alchemy will, ideally, support the platforms listed below. At the moment, the `Cocoa` backend is the most complete, as I develop on a Mac and know the framework more than I'd care to admit. This list will be updated as more frameworks are added.
- `cocoa`, which provides backing widgets, windows and assorted frameworks for `macOS`.
- `cocoa-touch`, which provides backing widgets, windows and assorted frameworks for `iOS`.
- `gtk`, which affords a `GTK` layer. This is mostly intended for GNOME users; if you'd like to run it elsewhere, you're on your own.
- `uwp`, which affords a `"UWP"` layer for Microsoft platforms that support it. This will be a bit of a hack, provided by linking into the [microsoft/WinObjC](https://github.com/Microsoft/WinObjC/) framework, originally intended for porting `iOS` applications to `UWP`. Down the road, if or when a proper `UWP` library for Rust surfaces, I'd be happy to look at replacing this.
Support for more platforms is desired - for example, I think an [`OrbTk`](https://gitlab.redox-os.org/redox-os/orbtk) or [`Piston`](https://www.piston.rs) backend could be cool to see. A `web` backend would be awesome to support. A [`winapi-rs`](https://github.com/retep998/winapi-rs) backend could be cool, too!
## What Currently Works...?
At the moment, the following is implemented:
- A basic `cocoa` API, which implements the `Application` and `Window` lifecycles. `<View />`, `<Text />`, and `<Fragment />` are supported as well.
- A basic `reconciliation` module, which handles computing changes to the widget tree and applying them as necessary. It currently follows a design similar to React pre-16; I'm open to changing this if someone wants to collaborate.
- A CSS parser, based on the work done over in [servo/servo](https://github.com/servo/servo). It doesn't support cascading, and follows an API closer to that of React Native's. This is intentional.
- An RSX system, based on work done in [bodil/typed-html](https://github.com/bodil/typed-html) by Bodil Stokke. This was actually the project that made me circle back to the entire thing, too.
- Macros for easy UI construction - `rsx! {}`, which transforms JSX-ish syntax into element trees for the reconciler to work with, and `styles! {}`, which pre-process CSS into their styles.
- A CSS layout system, based off the work done over in [vislyhq/stretch](https://github.com/vislyhq/stretch). At the moment, this project includes a fork with a newer underlying API by [msiglreith](https://github.com/msiglreith/stretch/tree/index). Once the API is merged upstream, it's likely the dependency would change to `stretch` proper.
You can clone this repo and `cargo run` from the root to see the example app.
## What's it look like?
``` rust
use alchemy::{AppDelegate, Error, RSX, rsx, styles, View, Window, WindowDelegate};
struct AppState {
window: Window
}
impl AppDelegate for AppState {
fn did_finish_launching(&mut self) {
self.window.set_title("Test");
self.window.set_dimensions(10., 10., 600., 600.);
self.window.show();
}
}
struct WindowState;
impl WindowDelegate for WindowState {
fn render(&self) -> Result<RSX, Error> {
Ok(rsx! {
<View styles=["box"]>
<View styles=["innerbox"] />
</View>
})
}
}
fn main() {
let app = alchemy::shared_app();
app.register_styles("default", styles! {
box {
background-color: #307ace;
width: 300;
height: 300;
margin-top: 10;
padding-top: 10;
}
innerbox {
background-color: #003366;
width: 200;
height: 200;
}
});
app.run(AppState {
window: Window::new(WindowState {
})
});
}
``` ```
## Does it support custom Components? To run:
Yes. Alchemy implements the React component lifecycle - although it does not (currently) implement Hooks, and may or may not implement them in the future. The class-based lifecycle maps fairly well to Rust idioms already, as you really never wanted to subclass in React anyway.
A custom component would look like the following: ```
cd src
``` rust zola serve
use alchemy::{Component, ComponentKey, Error, Props, rsx, RSX};
#[derive(Default)]
pub struct MySpecialWidgetProps;
#[derive(Props)]
pub struct MySpecialWidget {
props: MySpecialWidgetProps
}
impl Component for MySpecialWidget {
fn new(key: ComponentKey) -> MySpecialWidget {
MySpecialWidget {}
}
fn component_did_mount(&mut self) {
// Do whatever you want. Fire a network request or something, I dunno.
}
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> {
Ok(RSX::None)
}
}
``` ```
Rust allows the lifecycle to have a few cool guarantees that you can't really get in JavaScript - for instance, props don't actually belong to you... but it was a weird aspect of class-based components in JavaScript where you'd be able to arbitrarily call `this.props.whatever`. Function based components actually communicated it better, in that they were passed in - with Rust, it's very clear that you just get a reference. ## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).
Alchemy follows [this diagram of React's lifecycle methods](https://twitter.com/dan_abramov/status/981712092611989509) to a T for the most part. What's cool is that methods that shouldn't have side effects, we can call as straight up borrows... and the ones that are allowed to have mutable side effects, we can call them as `&mut self`. You can, of course, still incur side effects by doing something else, but being able to imply the intention directly in the API is kind of cool.
## License
I'm dual licensing this, due to the licenses that some of the projects it depends on being that. If there's some other (more appropriate) way to do this, please feel free to open an issue.
* Mozilla Public License, Version 2.0, ([LICENSE-MPL](LICENSE-MPL.md) or https://www.mozilla.org/en-US/MPL/)
* MIT License ([LICENSE-MIT](LICENSE-MIT.md) or https://opensource.org/licenses/MIT)
### Contributing
Before contributing, please read the [contributors guide](https://github.com/ryanmcgrath/alchemy/blob/trunk/CONTRIBUTING.md)
for useful information about setting up Alchemy locally, coding style and common abbreviations.
Unless you explicitly state otherwise, any contribution you intentionally submit
for inclusion in the work, should be dual-licensed as above, without any additional terms or conditions.
## Notes
- Major thanks to [David McNeil](https://github.com/davidMcneil) for graciously allowing me to take the `alchemy` name on crates.io. Hot take, if we had user namespacing, this wouldn't be an issue!
- Cheers to [diesel-rs/diesel](https://github.com/diesel-rs/diesel), who have a very well laid out repository that a bunch of this structure was cribbed from.
- Questions or comments that you don't think warrant an issue? Feel free to [poke me over on Twitter](https://twitter.com/ryanmcgrath/) or email me ([ryan@rymc.io](mailto:ryan@rymc.io)).

View file

@ -1,39 +0,0 @@
[package]
name = "alchemy"
description = "A cross-platform GUI framework written in Rust. Adapts to native view-layers on each platform. UIKit/React inspired."
version = "0.2.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", "css", "styles", "layout", "react"]
[badges]
maintenance = { status = "actively-developed" }
[features]
cocoa = ["alchemy-cocoa", "alchemy-lifecycle/cocoa"]
[dependencies]
alchemy-cocoa = { version = "0.1", path = "../cocoa", 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"] }
mime = "0.3.13"
htmlescape = "0.3.1"
language-tags = "0.2.2"
lazy_static = "1.3"
matches = "0.1"
phf = "0.7"
proc-macro-hack = "0.5.4"
proc-macro-nested = "0.1.3"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
strum = "0.15.0"
strum_macros = "0.15.0"
toml = "0.5"
[package.metadata.docs.rs]
features = ["cocoa"]
default-target = "x86_64-apple-darwin"

View file

@ -1,16 +0,0 @@
# Alchemy Core
This crate implements the core Alchemy application, which is what users ultimately import. Applications are a singleton; some might not like this, but it enables a design pattern that meshes a bit better with existing GUI framework systems and patterns.
The general pattern for developing with Alchemy is as follows:
``` bash
[Alchemy API] -> [Inner Mutability] -> [Platform Bridge (implemented in other crates)]
|
|
|- [Delegate]
```
The delegate pattern is cribbed from AppKit/UIKit, where it tends to work quite nicely as a way to respond to system level events.
## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).

View file

@ -1,146 +0,0 @@
//! 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.
//!
//! This ensures that you can respond to application lifecycles, and so
//! routing things around works correctly.
use std::sync::{Arc, Mutex};
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 {}
/// 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.
pub struct App {
pub(crate) bridge: Mutex<Option<PlatformAppBridge>>,
pub delegate: Mutex<Box<AppDelegate>>,
pub windows: WindowManager
}
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<App> {
let app = Arc::new(App {
bridge: Mutex::new(None),
delegate: Mutex::new(Box::new(DefaultAppDelegate {})),
windows: WindowManager::new()
});
let app_ptr: *const App = &*app;
app.configure_bridge(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<S: 'static + AppDelegate>(&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();
}
}
}
/// Implementing `AppDelegate` for `App` serves two purposes - for one, we're able to
/// separate the inner implementaton from the abstract one by referring to a trait type, avoiding
/// a cyclical dependency... and two, it allows us to react to these events on the App layer for
/// our own purposes, while still forwarding them on to the delegate.
impl AppDelegate for App {
/// Called when the application will finish launching.
fn will_finish_launching(&mut self) {
let mut delegate = self.delegate.lock().unwrap();
delegate.will_finish_launching();
}
/// Called when the application did finish launching.
fn did_finish_launching(&mut self) {
let mut delegate = self.delegate.lock().unwrap();
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();
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();
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();
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();
delegate.did_resign_active();
}
/// Called when the application should terminate - we can use it
/// to avoid termination if Alchemy needs more time for something,
/// for whatever reason.
fn should_terminate(&self) -> bool {
let delegate = self.delegate.lock().unwrap();
delegate.should_terminate()
}
/// Called when the application is about to terminate.
fn will_terminate(&mut self) {
let mut delegate = self.delegate.lock().unwrap();
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

@ -1,39 +0,0 @@
//! A Fragment is for components that want to return or hoist multiple inner
//! child nodes. `impl IntoIterator` can't be used in trait returns right now,
//! and this API more or less matches what React presents, so I'm fine with it...
//! but as the language stabilizes even further I'd love to get rid of this and
//! just allow returning arbitrary iterators.
use alchemy_lifecycle::ComponentKey;
use alchemy_lifecycle::traits::{Component, Props};
pub struct FragmentProps;
/// Fragments are special - you can do something like the following in cases where you
/// want to render some views without requiring an intermediate view.
///
/// ```
/// <Fragment>
/// <View />
/// <View />
/// <View />
/// </Fragment>
/// ```
#[derive(Default, Debug)]
pub struct Fragment;
impl Fragment {
pub fn default_props() -> FragmentProps {
FragmentProps {}
}
}
impl Props for Fragment {
fn set_props(&mut self, _: &mut std::any::Any) {}
}
impl Component for Fragment {
fn new(_: ComponentKey) -> Fragment {
Fragment {}
}
}

View file

@ -1,12 +0,0 @@
//! This module implements basic core Components (View, Label, etc).
//! End-users are of course free to implement their own; the core
//! Components in this module should just be enough to build a
//! functioning app.
pub mod fragment;
pub mod view;
pub mod text;
pub use fragment::Fragment;
pub use view::View;
pub use text::Text;

View file

@ -1,84 +0,0 @@
//! Handles hoisting per-platform specific Text components.
//! Each platform needs the freedom to do some specific things,
//! hence why they're all (somewhat annoyingly, but lovingly) re-implemented
//! as bridges.
use std::sync::{Mutex};
use alchemy_styles::styles::{Appearance, Layout};
use alchemy_lifecycle::ComponentKey;
use alchemy_lifecycle::error::Error;
use alchemy_lifecycle::rsx::RSX;
use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType};
#[cfg(feature = "cocoa")]
use alchemy_cocoa::text::{Text as PlatformTextBridge};
pub struct TextProps;
/// Text rendering is a complicated mess, and being able to defer to the
/// backing platform for this is amazing. This is a very common Component.
///
/// Views accept styles and event callbacks as props. For example:
///
/// ```
/// <Text styles=["styleKey1", "styleKey2"] />
/// ```
pub struct Text(Mutex<PlatformTextBridge>);
impl Text {
pub fn default_props() -> TextProps { TextProps {} }
// This is very naive for now, but it's fine - we probably
// want to do some fun stuff here later with stylized text
// rendering anyway.
//fn compare_and_update_text(&mut self, props: &Props) {
/*let text = props.*/
//}
}
impl Props for Text {
fn set_props(&mut self, _: &mut std::any::Any) {}
}
impl Component for Text {
fn new(_: ComponentKey) -> Text {
Text(Mutex::new(PlatformTextBridge::new()))
}
fn has_native_backing_node(&self) -> bool { true }
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> {
let bridge = self.0.lock().unwrap();
Some(bridge.borrow_native_backing_node())
}
// Shouldn't be allowed to have child <Text> elements... or, should it?
// Panic might not be right here, but eh, should probably do something.
//fn append_child_component(&self, _component: &Component) {}
fn apply_styles(&self, appearance: &Appearance, layout: &Layout) {
let mut bridge = self.0.lock().unwrap();
bridge.apply_styles(appearance, layout);
}
fn component_did_mount(&mut self) {
let mut bridge = self.0.lock().unwrap();
bridge.render();
}
// This one is a bit tricky, due to the way we have to do props + children in Rust.
// Here, we set it as the new text on render(), and then ensure it gets rendered on
// `component_did_update()` and `component_did_mount()`.
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> {
let text = children.iter().map(|child| match child {
RSX::VirtualText(s) => s.0.to_owned(),
_ => String::new()
}).collect::<String>();
let mut bridge = self.0.lock().unwrap();
bridge.set_text(text);
Ok(RSX::None)
}
}

View file

@ -1,79 +0,0 @@
//! Handles hoisting per-platform specific View components.
//! Each platform needs the freedom to do some specific things,
//! hence why they're all (somewhat annoyingly, but lovingly) re-implemented
//! as bridges.
use std::sync::Mutex;
use alchemy_styles::{Appearance, Layout};
use alchemy_lifecycle::ComponentKey;
use alchemy_lifecycle::error::Error;
use alchemy_lifecycle::rsx::RSX;
use alchemy_lifecycle::traits::{Component, Props, PlatformSpecificNodeType};
use crate::components::Fragment;
#[cfg(feature = "cocoa")]
use alchemy_cocoa::view::{View as PlatformViewBridge};
pub struct ViewProps;
/// Views are the most basic piece of the API. If you want to display something, you'll
/// probably be reaching for a View first and foremost.
///
/// Views accept styles and event callbacks as props. For example:
///
/// ```
/// <View styles=["styleKey1", "styleKey2"] />
/// ```
pub struct View {
bridge: Mutex<PlatformViewBridge>
}
impl Default for View {
fn default() -> View {
View {
bridge: Mutex::new(PlatformViewBridge::new())
}
}
}
impl View {
pub fn default_props() -> ViewProps {
ViewProps {}
}
}
impl Props for View {
fn set_props(&mut self, _: &mut std::any::Any) {}
}
impl Component for View {
fn new(_: ComponentKey) -> View {
View::default()
}
fn has_native_backing_node(&self) -> bool { true }
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> {
let bridge = self.bridge.lock().unwrap();
Some(bridge.borrow_native_backing_node())
}
fn append_child_node(&self, node: PlatformSpecificNodeType) {
let mut bridge = self.bridge.lock().unwrap();
bridge.append_child(node);
}
fn apply_styles(&self, appearance: &Appearance, layout: &Layout) {
let mut bridge = self.bridge.lock().unwrap();
bridge.apply_styles(appearance, layout);
}
fn render(&self, children: Vec<RSX>) -> Result<RSX, Error> {
Ok(RSX::node("Fragment", "".into(), |key| {
Box::new(<Fragment as Component>::new(key))
}, Box::new(ViewProps {}), children))
}
}

View file

@ -1,49 +0,0 @@
//! Alchemy is a Rust GUI framework that implements the React Component lifecycle on top of a
//! delegate system inspired by those found in AppKit/UIKit. It's backed by native widgets
//! per-platform, but doesn't bind you to any one design style or visual appearance.
//!
//! 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;
pub use alchemy_lifecycle::{ComponentKey, text};
pub use alchemy_lifecycle::traits::{
AppDelegate, Component, Props as ComponentProps, WindowDelegate
};
pub use alchemy_lifecycle::error::Error;
pub use alchemy_lifecycle::rsx::{
RSX, VirtualNode, VirtualText
};
#[proc_macro_hack(support_nested)]
pub use alchemy_macros::rsx;
#[proc_macro_hack]
pub use alchemy_macros::styles;
pub use alchemy_macros::Props;
pub use alchemy_styles::{Color, styles as style_attributes, SpacedSet, StyleSheet, StylesList};
mod app;
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> = 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,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

@ -1,170 +0,0 @@
//! Implements the Window API. It attempts to provide a nice, common interface across
//! per-platform Window APIs.
use std::sync::{Arc, Mutex};
use alchemy_lifecycle::{ComponentKey, RENDER_ENGINE};
use alchemy_lifecycle::rsx::RSX;
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.
pub struct AppWindow {
pub id: usize,
pub style_keys: StylesList,
pub title: String,
pub dimensions: (f64, f64, f64, f64),
pub bridge: PlatformWindowBridge,
pub delegate: Box<WindowDelegate>,
pub render_key: ComponentKey
}
impl AppWindow {
/// Re-renders a window. This method calls `render()` on the `WindowDelegate`, patches it into
/// the root tree node, and then diffs the old (current) tree against a new tree by walking it
/// and determining what needs to be changed. This also calculates and applies layout and
/// styling.
///
/// This method is called on the `show` event, and in rare cases can be useful to call
/// directly.
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);
self.bridge.apply_styles(&appearance);
let children = match self.delegate.render() {
Ok(opt) => opt,
Err(e) => {
eprintln!("Error rendering window! {}", e);
RSX::None
}
};
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); }
}
}
pub fn set_title(&mut self, title: &str) {
self.title = title.into();
self.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);
}
/// Renders and calls through to the native platform window show method.
pub fn show(&mut self) {
self.render();
self.bridge.show();
}
/// Calls through to the native platform window close method.
pub fn close(&mut self) {
self.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<Mutex<AppWindow>>);
impl Window {
/// Creates a new window.
pub fn new<S: 'static + WindowDelegate>(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(),
title: "".into(),
dimensions: (0., 0., 0., 0.),
bridge: bridge,
delegate: Box::new(delegate),
render_key: key
})))
}
/// 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();
window.render();
}
pub fn set_title(&self, title: &str) {
let mut window = self.0.lock().unwrap();
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();
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();
window.show();
}
/// Hides a window. On some platforms, this is minimizing... on others, like macOS, it's
/// actually hiding. On mobile, this shouldn't do anything.
pub fn hide(&self) {
}
/// 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();
window.close();
}
}
impl Clone for Window {
/// Clones a `Window` by cloning the inner `AppWindow`.
fn clone(&self) -> Window {
Window(self.0.clone())
}
}
impl Drop for Window {
/// When a `Window` is dropped, we want to ensure that it's closed, so we'll silently call
/// `.close()` to be safe.
fn drop(&mut self) {
self.close();
}
}

View file

@ -1,8 +0,0 @@
# Alchemy Assets
This folder contains assets used for Alchemy (graphics, etc).
## The Logo
[The potion bottle graphic is a vector graphic from Vecteezy](https://www.vecteezy.com/vector-art/124561-free-magic-item-vector). Interested in contributing logo work? Please get in touch!
## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

View file

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Before After
Before After

View file

@ -1,6 +0,0 @@
cyclomatic-complexity-threshold = 30
doc-valid-idents = [
"MiB", "GiB", "TiB", "PiB", "EiB",
"DirectX", "OpenGL", "TrueType",
"GitHub"
]

View file

@ -1,27 +0,0 @@
[package]
name = "alchemy-cocoa"
description = "Cocoa 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", "cocoa", "macos", "appkit", "react"]
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
alchemy-lifecycle = { version = "0.1", path = "../lifecycle", features = ["cocoa"] }
alchemy-styles = { version = "0.1", path = "../styles" }
objc = "0.2.6"
objc_id = "0.1.1"
dispatch = "0.1.4"
cocoa = "0.18.4"
core-foundation = "0.6"
core-graphics = "0.17.1"
[package.metadata.docs.rs]
features = ["cocoa"]
default-target = "x86_64-apple-darwin"

View file

@ -1,5 +0,0 @@
# Alchemy-Cocoa
This crate implements a backend for Cocoa-based widgets, such as `NSView`, `NSTextField`, and so on. Note that while it's under development currently, the fate of AppKit is still kind of a gray area. If Apple ends up pushing Marzipan as "the" solution, it's possible this might become obsolete, or would run in tandem with the iOS crate for iOS/Marzipan.
## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).

View file

@ -1,153 +0,0 @@
//! A wrapper for `NSApplication` on macOS. If you opt in to the `cocoa` feature on
//! Alchemy, this will loop system-level application events back to your `AppDelegate`.
use std::sync::{Once, ONCE_INIT};
use cocoa::base::{id, nil};
use cocoa::appkit::{NSApplication, NSRunningApplication};
use objc_id::Id;
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel};
use objc::{msg_send, class, sel, sel_impl};
use alchemy_lifecycle::traits::AppDelegate;
static ALCHEMY_APP_PTR: &str = "alchemyParentAppPtr";
/// A wrapper for `NSApplication`. It holds (retains) pointers for the Objective-C runtime,
/// which is where our application instance lives. It also injects an `NSObject` subclass,
/// which acts as the Delegate, looping back into our Alchemy shared application.
pub struct App {
pub inner: Id<Object>,
pub delegate: Id<Object>
}
impl App {
/// Creates an NSAutoReleasePool, configures various NSApplication properties (e.g, activation
/// policies), injects an `NSObject` delegate wrapper, and retains everything on the
/// Objective-C side of things.
pub fn new<T: AppDelegate>(parent_app_ptr: *const T) -> Self {
let inner = unsafe {
let _pool = cocoa::foundation::NSAutoreleasePool::new(nil);
let app = cocoa::appkit::NSApp();
app.setActivationPolicy_(cocoa::appkit::NSApplicationActivationPolicyRegular);
Id::from_ptr(app)
};
let delegate = unsafe {
let delegate_class = register_app_delegate_class::<T>();
let delegate: id = msg_send![delegate_class, new];
(&mut *delegate).set_ivar(ALCHEMY_APP_PTR, parent_app_ptr as usize);
msg_send![&*inner, setDelegate:delegate];
Id::from_ptr(delegate)
};
App {
delegate: delegate,
inner: inner
}
}
/// Kicks off the NSRunLoop for the NSApplication instance. This blocks when called.
pub fn run(&self) {
unsafe {
let current_app = cocoa::appkit::NSRunningApplication::currentApplication(nil);
current_app.activateWithOptions_(cocoa::appkit::NSApplicationActivateIgnoringOtherApps);
let shared_app: id = msg_send![class!(NSApplication), sharedApplication];
msg_send![shared_app, run];
}
}
}
/// Fires when the Application Delegate receives a `applicationWillFinishLaunching` notification.
extern fn will_finish_launching<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).will_finish_launching();
};
}
/// Fires when the Application Delegate receives a `applicationDidFinishLaunching` notification.
extern fn did_finish_launching<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).did_finish_launching();
};
}
/// Fires when the Application Delegate receives a `applicationWillBecomeActive` notification.
extern fn will_become_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).will_become_active();
};
}
/// Fires when the Application Delegate receives a `applicationDidBecomeActive` notification.
extern fn did_become_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).did_become_active();
};
}
/// Fires when the Application Delegate receives a `applicationWillResignActive` notification.
extern fn will_resign_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).will_resign_active();
};
}
/// Fires when the Application Delegate receives a `applicationDidResignActive` notification.
extern fn did_resign_active<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).did_resign_active();
};
}
/// Fires when the Application Delegate receives a `applicationWillTerminate` notification.
extern fn will_terminate<T: AppDelegate>(this: &Object, _: Sel, _: id) {
unsafe {
let app_ptr: usize = *this.get_ivar(ALCHEMY_APP_PTR);
let app = app_ptr as *mut T;
(*app).will_terminate();
};
}
/// Registers an `NSObject` application delegate, and configures it for the various callbacks and
/// pointers we need to have.
fn register_app_delegate_class<T: AppDelegate>() -> *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("AlchemyAppDelegate", superclass).unwrap();
decl.add_ivar::<usize>(ALCHEMY_APP_PTR);
// Add callback methods
decl.add_method(sel!(applicationWillFinishLaunching:), will_finish_launching::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationDidFinishLaunching:), did_finish_launching::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationWillBecomeActive:), will_become_active::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationDidBecomeActive:), did_become_active::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationWillResignActive:), will_resign_active::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationDidResignActive:), did_resign_active::<T> as extern fn(&Object, _, _));
decl.add_method(sel!(applicationWillTerminate:), will_terminate::<T> as extern fn(&Object, _, _));
DELEGATE_CLASS = decl.register();
});
unsafe {
DELEGATE_CLASS
}
}

View file

@ -1,29 +0,0 @@
//! Implements a conversion method for taking an `alchemy::Color` and turning it into
//! an `NSColor`.
use objc_id::Id;
use objc::runtime::Object;
use objc::{class, msg_send, sel, sel_impl};
use core_graphics::base::CGFloat;
use alchemy_styles::color::Color;
pub trait IntoNSColor {
fn into_nscolor(&self) -> Id<Object>;
}
impl IntoNSColor for Color {
/// This creates an NSColor, retains it, and returns it. Dropping this value will
/// call `release` on the Objective-C side.
fn into_nscolor(&self) -> Id<Object> {
let red = self.red as CGFloat / 255.0;
let green = self.green as CGFloat / 255.0;
let blue = self.blue as CGFloat / 255.0;
let alpha = self.alpha as CGFloat / 255.0;
unsafe {
Id::from_ptr(msg_send![class!(NSColor), colorWithRed:red green:green blue:blue alpha:alpha])
}
}
}

View file

@ -1,24 +0,0 @@
//! This crate provides a Cocoa backend for Alchemy, the Rust GUI framework.
//! This means that, on macOS, you'll be using native `NSView`, `NSTextField`,
//! and other assorted controls. Where possible, it attempts to opt into
//! smoother rendering paths (e.g, layer-backed views, drawing subview layers
//! together where appropriate).
//!
//! # License
//!
//! Copyright 2018 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 color;
pub mod app;
pub mod text;
pub mod view;
pub mod window;

View file

@ -1,140 +0,0 @@
//! This wraps NTextField on macOS, and configures it to act like a label
//! with standard behavior that most users would expect.
use std::sync::{Once, ONCE_INIT};
use objc_id::{Id, ShareId};
use objc::{msg_send, sel, sel_impl};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use cocoa::base::{id, nil, YES, NO};
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString};
use crate::color::IntoNSColor;
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 {
text: String,
inner_mut: Id<Object>,
inner_share: ShareId<Object>,
background_color: Id<Object>,
text_color: Id<Object>
}
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 {
let (inner_mut, inner_share) = unsafe {
let initial_string = NSString::alloc(nil).init_str("");
let view: id = msg_send![register_class(), labelWithString:initial_string];
msg_send![view, setSelectable:YES];
msg_send![view, setDrawsBackground:YES];
msg_send![view, setBezeled:NO];
msg_send![view, setEditable:NO];
msg_send![view, setWantsLayer:YES];
msg_send![view, setLayerContentsRedrawPolicy:1];
let x = view.clone();
(Id::from_ptr(view), ShareId::from_ptr(x))
};
Text {
text: "".into(),
inner_mut: inner_mut,
inner_share: inner_share,
background_color: Color::transparent().into_nscolor(),
text_color: Color::transparent().into_nscolor()
}
}
/// 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 {
self.inner_share.clone()
}
/// Appends a child NSText (or subclassed type) to this view.
pub fn append_child(&mut self, child: PlatformSpecificNodeType) {
unsafe {
msg_send![&*self.inner_mut, addSubview:child];
}
}
/// 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) {
unsafe {
let rect = NSRect::new(
NSPoint::new(layout.location.x.into(), layout.location.y.into()),
NSSize::new(layout.size.width.into(), layout.size.height.into())
);
self.background_color = appearance.background_color.into_nscolor();
self.text_color = appearance.text_color.into_nscolor();
msg_send![&*self.inner_mut, setFrame:rect];
msg_send![&*self.inner_mut, setBackgroundColor:&*self.background_color];
msg_send![&*self.inner_mut, setTextColor:&*self.text_color];
}
}
pub fn set_text(&mut self, text: String) {
self.text = text;
}
pub fn render(&mut self) {
unsafe {
let string_value = NSString::alloc(nil).init_str(&self.text);
msg_send![&*self.inner_mut, setStringValue:string_value];
}
}
}
/// This is used for some specific calls, where macOS NSText needs to be
/// forcefully dragged into the modern age (e.g, position coordinates from top left...).
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/// Registers an `NSText` subclass, and configures it to hold some ivars for various things we need
/// to store.
fn register_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = ONCE_INIT;
INIT.call_once(|| unsafe {
let superclass = Class::get("NSTextField").unwrap();
let mut decl = ClassDecl::new("AlchemyTextField", superclass).unwrap();
// Force NSText to render from the top-left, not bottom-left
//decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Request optimized backing layers
//decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
//decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Ensure mouse events and so on work
//decl.add_method(sel!(acceptsFirstResponder), update_layer as extern fn(&Object, _));
// A pointer back to our Text, for forwarding mouse + etc events.
// Note that NSText's don't really have a "delegate", I'm just using it here
// for common terminology sake.
decl.add_ivar::<usize>(ALCHEMY_DELEGATE);
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -1,139 +0,0 @@
//! Implements a View Component struct. The most common
//! basic building block of any app. Wraps NSView on macOS.
use std::sync::{Once, ONCE_INIT};
use objc_id::{Id, ShareId};
use objc::{msg_send, sel, sel_impl};
use objc::declare::ClassDecl;
use objc::runtime::{Class, Object, Sel, BOOL};
use cocoa::base::{id, nil, YES};
use cocoa::foundation::{NSRect, NSPoint, NSSize};
use crate::color::IntoNSColor;
use alchemy_styles::{Appearance, Color, Layout};
use alchemy_lifecycle::traits::PlatformSpecificNodeType;
static ALCHEMY_DELEGATE: &str = "alchemyDelegate";
static BACKGROUND_COLOR: &str = "alchemyBackgroundColor";
/// 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 {
inner_mut: Id<Object>,
inner_share: ShareId<Object>,
background_color: Id<Object>
}
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 {
let (inner_mut, inner_share) = unsafe {
let rect_zero = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
let alloc: id = msg_send![register_class(), alloc];
let view: id = msg_send![alloc, initWithFrame:rect_zero];
msg_send![view, setWantsLayer:YES];
msg_send![view, setLayerContentsRedrawPolicy:1];
let x = view.clone();
(Id::from_ptr(view), ShareId::from_ptr(x))
};
View {
inner_mut: inner_mut,
inner_share: inner_share,
background_color: Color::transparent().into_nscolor()
}
}
/// 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 {
self.inner_share.clone()
}
/// Appends a child NSView (or subclassed type) to this view.
pub fn append_child(&mut self, child: PlatformSpecificNodeType) {
unsafe {
msg_send![&*self.inner_mut, addSubview:child];
}
}
/// 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) {
unsafe {
let rect = NSRect::new(
NSPoint::new(layout.location.x.into(), layout.location.y.into()),
NSSize::new(layout.size.width.into(), layout.size.height.into())
);
self.background_color = appearance.background_color.into_nscolor();
self.inner_mut.set_ivar(BACKGROUND_COLOR, &*self.background_color);
msg_send![&*self.inner_mut, setFrame:rect];
msg_send![&*self.inner_mut, setNeedsDisplay:YES];
}
}
}
/// This is used for some specific calls, where macOS NSView needs to be
/// forcefully dragged into the modern age (e.g, position coordinates from top left...).
extern fn enforce_normalcy(_: &Object, _: Sel) -> BOOL {
return YES;
}
/// When an `NSView` has `updateLayer` called, it will get passed through here, at which point we
/// instruct the layer how it should render (e.g, background color).
extern fn update_layer(this: &Object, _: Sel) {
unsafe {
let background_color: id = *this.get_ivar(BACKGROUND_COLOR);
if background_color != nil {
let layer: id = msg_send![this, layer];
let cg: id = msg_send![background_color, CGColor];
msg_send![layer, setBackgroundColor:cg];
}
}
}
/// Registers an `NSView` subclass, and configures it to hold some ivars for various things we need
/// to store.
fn register_class() -> *const Class {
static mut VIEW_CLASS: *const Class = 0 as *const Class;
static INIT: Once = ONCE_INIT;
INIT.call_once(|| unsafe {
let superclass = Class::get("NSView").unwrap();
let mut decl = ClassDecl::new("AlchemyView", superclass).unwrap();
// Force NSView to render from the top-left, not bottom-left
decl.add_method(sel!(isFlipped), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Opt-in to AutoLayout
//decl.add_method(sel!(requiresConstraintBasedLayout), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Request optimized backing layers
decl.add_method(sel!(updateLayer), update_layer as extern fn(&Object, _));
decl.add_method(sel!(wantsUpdateLayer), enforce_normalcy as extern fn(&Object, _) -> BOOL);
// Ensure mouse events and so on work
//decl.add_method(sel!(acceptsFirstResponder), update_layer as extern fn(&Object, _));
// A pointer back to our View, for forwarding mouse + etc events.
// Note that NSView's don't really have a "delegate", I'm just using it here
// for common terminology sake.
decl.add_ivar::<usize>(ALCHEMY_DELEGATE);
decl.add_ivar::<id>(BACKGROUND_COLOR);
VIEW_CLASS = decl.register();
});
unsafe { VIEW_CLASS }
}

View file

@ -1,169 +0,0 @@
//! Implements an `NSWindow` wrapper for MacOS, backed by
//! Cocoa and associated widgets. This also handles looping back
//! lifecycle events, such as window resizing or close events.
use std::sync::{Once, ONCE_INIT};
use cocoa::base::{id, nil, YES, NO};
use cocoa::appkit::{NSWindow, NSWindowStyleMask, NSBackingStoreType};
use cocoa::foundation::{NSRect, NSPoint, NSSize, NSString, NSAutoreleasePool};
use objc_id::ShareId;
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_styles::Appearance;
static APP_PTR: &str = "alchemyAppPtr";
static WINDOW_MANAGER_ID: &str = "alchemyWindowManagerID";
/// A wrapper for `NSWindow`. Holds (retains) pointers for the Objective-C runtime
/// where our `NSWindow` and associated delegate live.
pub struct Window {
pub inner: ShareId<Object>,
pub delegate: ShareId<Object>
}
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: AppDelegate>(window_id: usize, content_view: ShareId<Object>, app_ptr: *const T) -> Window {
let dimensions = NSRect::new(NSPoint::new(0., 0.), NSSize::new(0., 0.));
let style = NSWindowStyleMask::NSResizableWindowMask |
NSWindowStyleMask::NSUnifiedTitleAndToolbarWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask |
NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSFullSizeContentViewWindowMask;
let inner = unsafe {
let window = NSWindow::alloc(nil).initWithContentRect_styleMask_backing_defer_(
dimensions,
style,
NSBackingStoreType::NSBackingStoreBuffered,
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];
//}
ShareId::from_ptr(window)
};
let delegate = unsafe {
let delegate_class = register_window_class::<T>();
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);
msg_send![inner, setDelegate:delegate];
ShareId::from_ptr(delegate)
};
Window {
inner: inner,
delegate: delegate
}
}
pub fn set_title(&mut self, title: &str) {
unsafe {
let title = NSString::alloc(nil).init_str(title);
msg_send![&*self.inner, setTitle:title];
}
}
pub fn set_dimensions(&mut self, x: f64, y: f64, width: f64, height: f64) {
unsafe {
let dimensions = NSRect::new(
NSPoint::new(x.into(), y.into()),
NSSize::new(width.into(), height.into())
);
msg_send![&*self.inner, setFrame:dimensions display:YES];
}
}
/// 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) {
unsafe {
msg_send![&*self.inner, makeKeyAndOrderFront:nil];
}
}
/// 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) {
unsafe {
msg_send![&*self.inner, close];
}
}
}
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) {
// This bridging link needs to be broken on Drop.
unsafe {
msg_send![&*self.inner, setDelegate:nil];
}
}
}
/// 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<T: AppDelegate>(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);
};
}
/// Injects an `NSObject` delegate subclass, with some callback and pointer ivars for what we
/// need to do.
fn register_window_class<T: AppDelegate>() -> *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();
decl.add_ivar::<usize>(APP_PTR);
decl.add_ivar::<usize>(WINDOW_MANAGER_ID);
decl.add_method(sel!(windowWillClose:), will_close::<T> as extern fn(&Object, _, _));
DELEGATE_CLASS = decl.register();
});
unsafe {
DELEGATE_CLASS
}
}

View file

@ -1,8 +0,0 @@
[package]
name = "layout"
version = "0.1.0"
authors = ["Ryan McGrath <ryan@rymc.io>"]
edition = "2018"
[dependencies]
alchemy = { path = "../../alchemy", version = "0.2.0", features = ["cocoa"] }

View file

@ -1,124 +0,0 @@
#![recursion_limit="256"]
/// demo/main.rs
///
/// Used to sketch out application structure/feel/etc.
///
/// @author Ryan McGrath <ryan@rymc.io>
/// @created March 26th, 2019
use alchemy::{
AppDelegate, Component, ComponentKey, Fragment, Error, Props, rsx, RSX, styles, text,
Text, View, Window, WindowDelegate
};
pub struct AppState {
window: Window
}
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<RSX>) -> Result<RSX, Error> {
Ok(rsx! {
<Fragment>
<View styles=["wut1"]></View>
{children}
</Fragment>
})
}
}
pub struct WindowState;
impl WindowDelegate for WindowState {
fn will_close(&mut self) {
println!("Closing!?");
}
fn render(&self) -> Result<RSX, Error> {
let messages = vec!["LOL"]; //, "wut", "BERT"];
Ok(rsx! {
<View styles={&messages}>
<Text styles=["message"]>"Hello there, my name is Bert"</Text>
<View styles=["boxxx"] />
{messages.iter().map(|message| rsx! {
<Text styles=["text"]>{text!("{}", message)}</Text>
})}
<View styles=["box1"]>
<Banner>
<View styles=["innermostBox"] />
</Banner>
</View>
</View>
})
}
}
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 {
})
});
}

View file

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.1 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.7 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 629 B

After

Width:  |  Height:  |  Size: 629 B

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 17 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 193 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Before After
Before After

View file

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Before After
Before After

169
index.html Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,19 +0,0 @@
[package]
name = "alchemy-lifecycle"
description = "A crate containing traits used in Alchemy, the Rust cross-platform GUI framework."
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", "css", "styles", "layout", "ui"]
[features]
cocoa = ["objc", "objc_id"]
[dependencies]
alchemy-styles = { version = "0.1", path = "../styles" }
objc = { version = "0.2.6", optional = true }
objc_id = { version = "0.1.1", optional = true }
serde_json = "1"

View file

@ -1,5 +0,0 @@
# Alchemy-Lifecycle
This crate holds traits used in Alchemy, such as `AppDelegate`, `WindowDelegate`, and `Component`. It also holds the RSX node/tag system, since it's standalone and `Component` ends up requiring it anyway.
## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).

View file

@ -1,7 +0,0 @@
//! Implements an Error type. Currently we just alias this to
//! Box<Error>, because I'm not sure how this should really look. Consider
//! it an implementation detail hook that could change down the road.
/// A generic Error type that we use. It currently just aliases to `Box<std::error::Error>`,
/// but could change in the future.
pub type Error = Box<std::error::Error>;

View file

@ -1,38 +0,0 @@
//! Lifecycle aspects for Alchemy.
//!
//! What's a lifecycle? Well, it includes things like delegates (App+Window),
//! where they act as hooks for the system to inform you of events. It includes
//! things like `Component`s, which instruct your views how to exist.
//!
//! It also includes the `RSX` enum, which is what `render()` methods generally
//! return. It's common enough to multiple crates, and is intricately linked to the
//! `Component` lifecycle, so it'll live here.
//!
//! This crate also includes the diffing and patching system for the widget tree -
//! it needs to live with the `Component` lifecycle to enable state updating.
pub use std::sync::Arc;
use alchemy_styles::lazy_static;
pub mod error;
pub mod rsx;
pub mod traits;
mod reconciler;
use reconciler::RenderEngine;
pub use reconciler::key::ComponentKey;
lazy_static! {
pub static ref RENDER_ENGINE: RenderEngine = RenderEngine::new();
}
#[macro_export]
macro_rules! text {
($t:expr) => {
alchemy::RSX::text($t)
};
($format:tt, $($tail:expr),*) => {
alchemy::RSX::text(format!($format, $($tail),*))
};
}

View file

@ -1,32 +0,0 @@
//! Implements a set of Error types that could happen during a diff/patch/reflow
//! run. These are mostly internal to the rendering engine itself, but could potentially
//! show up elsewhere.
use crate::reconciler::key::ComponentKey;
#[derive(Debug)]
pub enum RenderEngineError {
InvalidKey,
InvalidRootComponent,
InvalidComponentKey(ComponentKey)
}
impl std::fmt::Display for RenderEngineError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match *self {
RenderEngineError::InvalidComponentKey(ref node) => write!(f, "Invalid component key {:?}", node),
RenderEngineError::InvalidRootComponent => write!(f, "Invalid component type! Root nodes must be a natively backed node."),
RenderEngineError::InvalidKey => write!(f, "An invalid key was passed to the render engine.")
}
}
}
impl std::error::Error for RenderEngineError {
fn description(&self) -> &str {
match *self {
RenderEngineError::InvalidComponentKey(_) => "The key is not part of the component storage instance",
RenderEngineError::InvalidRootComponent => "The root component must be a natively backed Component instance.",
RenderEngineError::InvalidKey => "An invalid key was passed to the render engine."
}
}
}

View file

@ -1,31 +0,0 @@
//! This is a generic view used for avoiding a circular dependency issue.
//! You should never need to touch this.
use std::any::Any;
use crate::ComponentKey;
use crate::traits::{Component, Props};
#[derive(Default)]
pub struct GenericRootViewProps;
/// This is never actually created, and is here primarily to avoid a circular
/// depedency issue (we can't import the View from alchemy's core crate, since the core crate
/// depends on this crate).
pub struct GenericRootView;
impl GenericRootView {
fn get_default_props() -> GenericRootViewProps {
GenericRootViewProps {}
}
}
impl Props for GenericRootView {
fn set_props(&mut self, _: &mut Any) {}
}
impl Component for GenericRootView {
fn new(_: ComponentKey) -> GenericRootView {
GenericRootView {}
}
}

View file

@ -1,15 +0,0 @@
//! Internal struct used for tracking component instances and their
//! associated metadata (layout, appearance, etc).
use alchemy_styles::{Appearance, StylesList};
use alchemy_styles::stretch::node::{Node as LayoutNode};
use crate::traits::Component;
pub(crate) struct Instance {
pub(crate) tag: &'static str,
pub(crate) style_keys: StylesList,
pub(crate) component: Box<Component + 'static>,
pub(crate) appearance: Appearance,
pub(crate) layout: Option<LayoutNode>
}

View file

@ -1,51 +0,0 @@
//! Implements an auto-incrementing ID for Component instances.
use std::sync::Mutex;
use alchemy_styles::lazy_static;
lazy_static! {
/// Global stretch instance id allocator.
pub(crate) static ref INSTANCE_ALLOCATOR: Mutex<Allocator> = Mutex::new(Allocator::new());
}
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub(crate) struct Id {
id: u32
}
pub(crate) struct Allocator {
new_id: u32
}
impl Allocator {
pub fn new() -> Self {
Allocator { new_id: 1 }
}
pub fn allocate(&mut self) -> Id {
let id = self.new_id;
self.new_id += 1;
Id { id: id }
}
}
/// Used as a key for Component storage. Component instances receive these
/// in their constructor methods, and should retain them as a tool to update their
/// state.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub struct ComponentKey {
pub(crate) instance: Id,
pub(crate) local: Id,
}
impl ComponentKey {
/// A placeholder value, used purely for ensuring the diffing algorithm remains
/// readable by reducing some unwrapping hell.
pub fn placeholder() -> ComponentKey {
ComponentKey {
instance: Id { id: 0 },
local: Id { id: 0 }
}
}
}

View file

@ -1,382 +0,0 @@
//! Implements tree diffing, updating, and so on. Unlike a lot of the VDom implementations
//! you find littered around the web, this is a bit more ECS-ish, and expects Components to retain
//! their `ComponentKey` passed in their constructor if they want to update. Doing this
//! enables us to avoid re-scanning or diffing an entire tree.
use std::sync::Mutex;
use std::error::Error;
use alchemy_styles::THEME_ENGINE;
use alchemy_styles::styles::{Appearance, Dimension, Number, Size, Style};
use alchemy_styles::stretch::node::{Node as LayoutNode, Stretch as LayoutStore};
use crate::rsx::{RSX, VirtualNode};
use crate::traits::Component;
pub mod key;
use key::ComponentKey;
pub mod storage;
use storage::ComponentStore;
pub mod error;
use error::RenderEngineError;
mod instance;
use instance::Instance;
mod generic_root_view_stub;
use generic_root_view_stub::{GenericRootView, GenericRootViewProps};
struct GenericRootProps;
pub struct RenderEngine {
queued_state_updates: Mutex<Vec<i32>>,
components: Mutex<ComponentStore>,
layouts: Mutex<LayoutStore>
}
impl RenderEngine {
pub(crate) fn new() -> RenderEngine {
RenderEngine {
queued_state_updates: Mutex::new(vec![]),
components: Mutex::new(ComponentStore::new()),
layouts: Mutex::new(LayoutStore::new())
}
}
// pub fn queue_update_for(&self, component_ptr: usize, updater: Box<Fn() -> Component + Send + Sync + 'static>) {
// }
/// `Window`'s (or anything "root" in nature) need to register with the
/// reconciler for things like setState to work properly. When they do so,
/// they get a key back. When they want to instruct the global `RenderEngine`
/// to re-render or update their tree, they pass that key and whatever the new tree
/// should be.
pub fn register_root_component<C: Component + 'static>(&self, component: C) -> Result<ComponentKey, Box<Error>> {
// Conceivably, this doesn't NEED to be a thing... but for now it is. If you've stumbled
// upon here, wayward traveler, in need of a non-native-root-component, please open an
// issue to discuss. :)
if !component.has_native_backing_node() {
return Err(Box::new(RenderEngineError::InvalidRootComponent {}));
}
let mut component_store = self.components.lock().unwrap();
let mut layouts_store = self.layouts.lock().unwrap();
let component_key = component_store.new_key();
component_store.insert(component_key, Instance {
tag: "root",
style_keys: "root".into(),
component: Box::new(component),
appearance: Appearance::default(),
layout: Some(layouts_store.new_node(Style::default(), vec![])?)
})?;
Ok(component_key)
}
/// Rendering the root node is a bit different than rendering or updating other nodes, as we
/// never want to unmount it, and the results come from a non-`Component` entity (e.g, a
/// `Window`). Thus, for this one, we do some manual mucking with what we know is the
/// root view (a `Window` or such root component would call this with it's registered
/// `ComponentKey`), and then recurse based on the children.
pub fn diff_and_render_root(
&self,
key: ComponentKey,
dimensions: (f64, f64),
child: RSX
) -> Result<(), Box<Error>> {
let mut component_store = self.components.lock().unwrap();
let mut layout_store = self.layouts.lock().unwrap();
let new_root_node = RSX::node("root", "root".into(), |_| {
Box::new(GenericRootView {})
}, Box::new(GenericRootViewProps {}), match child {
RSX::VirtualNode(node) => {
if node.tag == "Fragment" {
node.children
} else {
vec![RSX::VirtualNode(node)]
}
},
_ => vec![]
});
recursively_diff_tree(key, new_root_node, &mut component_store, &mut layout_store)?;
let layout_node = {
let mut root_instance = component_store.get_mut(key)?;
let layout = root_instance.layout.unwrap();
let mut style = Style::default();
THEME_ENGINE.configure_styles_for_keys(&root_instance.style_keys, &mut style, &mut root_instance.appearance);
style.size = Size {
width: Dimension::Points(dimensions.0 as f32),
height: Dimension::Points(dimensions.1 as f32)
};
layout_store.set_style(layout, style);
layout
};
layout_store.compute_layout(layout_node, Size {
width: Number::Defined(dimensions.0 as f32),
height: Number::Defined(dimensions.1 as f32)
})?;
walk_and_apply_styles(key, &mut component_store, &mut layout_store)?;
Ok(())
}
}
/// Given two trees, will diff them to see if we need to replace or update. Depending on the
/// result, we'll either recurse down a level, or tear down and build up a new tree. The final
/// parameter on this method, `is_root_entity_view`, should only be passed for `Window` or other
/// such instances, as it instructs us to skip the first level since these ones act different.
fn recursively_diff_tree(
key: ComponentKey,
new_tree: RSX,
component_store: &mut ComponentStore,
layout_store: &mut LayoutStore
) -> Result<(), Box<Error>> {
// First we need to determine if this node is being replaced or updated. A replace happens if
// two nodes are different types - in this case, we check their tag values. This is also a case
// where, for instance, if the RSX tag is `::None` or `::VirtualText`, we'll treat it as
// replacing with nothing.
let is_replace = match &new_tree {
RSX::VirtualNode(new_tree) => {
let old_tree = component_store.get(key)?;
old_tree.tag != new_tree.tag
},
// The algorithm will know below not to recurse if we're trying to diff text or empty
// values. We return false here to avoid entering the `is_replace` phase; `Component`
// instances (like <Text />) handle taking the child VirtualText instances and working with
// them to pass to a native widget.
_ => false
};
if is_replace {
unmount_component_tree(key, component_store, layout_store)?;
//mount_component_tree(
return Ok(());
}
// At this point, we know it's an update pass. Now we need to do a few things:
//
// - Diff our `props` and figure out what actions we can take or shortcut.
// - Let the `Component` instance determine what it should render.
// - Recurse into the child trees if necessary.
let mut old_children = component_store.children(key)?;
old_children.reverse();
if let RSX::VirtualNode(mut child) = new_tree {
for new_child_tree in child.children {
match old_children.pop() {
// If there's a key in the old children for this position, it's
// something we need to update, so let's recurse right back into it.
Some(old_child_key) => {
recursively_diff_tree(
old_child_key,
new_child_tree,
component_store,
layout_store
)?;
},
// If there's no matching old key in this position, then we've got a
// new component instance to mount. This part now diverts into the Mount
// phase.
None => {
if let RSX::VirtualNode(tr33amimustfeelohlol) = new_child_tree {
let new_child_key = mount_component_tree(
tr33amimustfeelohlol,
component_store,
layout_store
)?;
component_store.add_child(key, new_child_key)?;
link_layout_nodess(key, new_child_key, component_store, layout_store)?;
}
}
}
}
}
// Trim the fat. If we still have child nodes after diffing in the new child trees,
// then they're ones that simply need to be unmounted and dropped.
if old_children.len() > 0 {
for child in old_children {
unmount_component_tree(child, component_store, layout_store)?;
}
}
Ok(())
}
/// Given a new `RSX` tree, a `ComponentStore`, and a `LayoutStore`, will recursively construct the
/// tree, emitting required lifecycle events and persisting values. This happens in an inward-out
/// fashion, which helps avoid unnecessary reflow in environments where it can get tricky.
///
/// This method returns a Result, the `Ok` variant containing a tuple of Vecs. These are the child
/// Component instances and Layout instances that need to be set in the stores.
fn mount_component_tree(
tree: VirtualNode,
component_store: &mut ComponentStore,
layout_store: &mut LayoutStore
) -> Result<ComponentKey, Box<Error>> {
let key = component_store.new_key();
let component = (tree.create_component_fn)(key);
let is_native_backed = component.has_native_backing_node();
// let state = get_derived_state_from_props()
let mut instance = Instance {
tag: tree.tag,
style_keys: tree.styles,
component: component,
appearance: Appearance::default(),
layout: None
};
if is_native_backed {
let mut style = Style::default();
THEME_ENGINE.configure_styles_for_keys(&instance.style_keys, &mut style, &mut instance.appearance);
instance.layout = Some(layout_store.new_node(style, vec![])?);
}
let rendered = instance.component.render(tree.children);
// instance.get_snapshot_before_update()
component_store.insert(key, instance)?;
match rendered {
Ok(child) => if let RSX::VirtualNode(child) = child {
// We want to support Components being able to return arbitrary iteratable
// elements, but... well, it's not quite that simple. Thus we'll offer a <Fragment>
// tag similar to what React does, which just hoists the children out of it and
// discards the rest.
if child.tag == "Fragment" {
for child_tree in child.children {
if let RSX::VirtualNode(child_tree) = child_tree {
let child_key = mount_component_tree(child_tree, component_store, layout_store)?;
component_store.add_child(key, child_key)?;
if is_native_backed {
link_layout_nodess(key, child_key, component_store, layout_store)?;
}
}
}
} else {
let child_key = mount_component_tree(child, component_store, layout_store)?;
component_store.add_child(key, child_key)?;
if is_native_backed {
link_layout_nodess(key, child_key, component_store, layout_store)?;
}
}
},
Err(e) => {
// return an RSX::VirtualNode(ErrorComponentView) or something?
/* instance.get_derived_state_from_error(e) */
// render error state or something I guess?
/* instance.component_did_catch(e, info) */
eprintln!("Error rendering: {}", e);
}
}
let instance_lol = component_store.get_mut(key)?;
instance_lol.component.component_did_mount();
Ok(key)
}
/// Given a `ComponentKey`, a `ComponentStore`, and a `LayoutStore`, will recursively walk the tree found at
/// said key, emitting required lifecycle events and dropping values. This happens in an inward-out
/// fashion, so deepest nodes/components get destroyed first to ensure that the backing widget tree
/// doesn't get some weird dangling issue.
fn unmount_component_tree(
key: ComponentKey,
component_store: &mut ComponentStore,
layout_store: &mut LayoutStore
) -> Result<Vec<LayoutNode>, Box<Error>> {
let mut instance = component_store.remove(key)?;
instance.component.component_will_unmount();
let mut layout_nodes = vec![];
let children = component_store.children(key)?;
for child in children {
match unmount_component_tree(child, component_store, layout_store) {
Ok(mut child_layout_nodes) => {
if let Some(parent_layout_node) = instance.layout {
for node in child_layout_nodes {
layout_store.remove_child(parent_layout_node, node)?;
}
} else {
layout_nodes.append(&mut child_layout_nodes);
}
},
Err(e) => { eprintln!("Error unmounting a component tree: {}", e); }
}
}
// remove node from backing tree
Ok(layout_nodes)
}
/// Given a tree, will walk the branches until it finds the next root nodes to connect.
/// While this sounds slow, in practice it rarely has to go far in any direction. This could
/// potentially be done away with some hoisting magic in the `mount()` recursion, but I couldn't
/// find a pattern that didn't feel like some utter magic in Rust.
///
/// It might be because I'm writing this at 3AM. Feel free to improve it.
fn link_layout_nodess(
parent: ComponentKey,
child: ComponentKey,
components: &mut ComponentStore,
layouts: &mut LayoutStore
) -> Result<(), Box<Error>> {
if let (Ok(parent_instance), Ok(child_instance)) = (components.get(parent), components.get(child)) {
if let (Some(parent_layout), Some(child_layout)) = (parent_instance.layout, child_instance.layout) {
layouts.add_child(parent_layout, child_layout)?;
if let Some(platform_node) = child_instance.component.borrow_native_backing_node() {
parent_instance.component.append_child_node(platform_node);
}
return Ok(());
}
}
let children = components.children(child)?;
for child_key in children {
link_layout_nodess(parent, child_key, components, layouts)?;
}
Ok(())
}
/// Walks the tree and passes necessary Layout and Appearance-based styles to Components so they can
/// update their backing widgets accordingly. This happens after a layout computation, typically.
fn walk_and_apply_styles(
key: ComponentKey,
components: &mut ComponentStore,
layouts: &mut LayoutStore
) -> Result<(), Box<Error>> {
let instance = components.get_mut(key)?;
if let Some(layout_key) = instance.layout {
instance.component.apply_styles(
&instance.appearance,
layouts.layout(layout_key)?
);
}
for child in components.children(key)? {
walk_and_apply_styles(child, components, layouts)?;
}
Ok(())
}

View file

@ -1,166 +0,0 @@
//! Implements storage for Component instances, in a way that allows us to
//! short-circuit the rendering process so we don't have to re-scan entire
//! tree structures when updating state.
use std::collections::HashMap;
pub use alchemy_styles::Appearance;
use crate::reconciler::error::{RenderEngineError as Error};
use crate::reconciler::instance::Instance;
use crate::reconciler::key::{Allocator, Id, INSTANCE_ALLOCATOR, ComponentKey};
/// This is a clone of a structure you'll also find over in stretch. We do this separately
/// here for two reasons.
///
/// - First, a Component may have children that don't require styles or layout passes. These nodes
/// should not have `Style` or `Appearance` nodes created, but we do need the correct parent/child
/// relationships in place.
/// - The `Storage` pieces of stretch are realistically an implementation detail that we shouldn't
/// rely on.
struct Storage<T>(HashMap<ComponentKey, T>);
impl<T> Storage<T> {
pub fn new() -> Self {
Storage(HashMap::new())
}
pub fn get(&self, key: ComponentKey) -> Result<&T, Error> {
match self.0.get(&key) {
Some(v) => Ok(v),
None => Err(Error::InvalidComponentKey(key)),
}
}
pub fn get_mut(&mut self, key: ComponentKey) -> Result<&mut T, Error> {
match self.0.get_mut(&key) {
Some(v) => Ok(v),
None => Err(Error::InvalidComponentKey(key)),
}
}
pub fn remove(&mut self, key: ComponentKey) -> Result<T, Error> {
match self.0.remove(&key) {
Some(v) => Ok(v),
None => Err(Error::InvalidComponentKey(key))
}
}
pub fn insert(&mut self, key: ComponentKey, value: T) -> Option<T> {
self.0.insert(key, value)
}
}
impl<T> std::ops::Index<&ComponentKey> for Storage<T> {
type Output = T;
fn index(&self, idx: &ComponentKey) -> &T {
&(self.0)[idx]
}
}
pub(crate) struct ComponentStore {
id: Id,
nodes: Allocator,
components: Storage<Instance>,
parents: Storage<Vec<ComponentKey>>,
children: Storage<Vec<ComponentKey>>
}
impl ComponentStore {
pub fn new() -> Self {
ComponentStore {
id: INSTANCE_ALLOCATOR.lock().unwrap().allocate(),
nodes: Allocator::new(),
components: Storage::new(),
parents: Storage::new(),
children: Storage::new()
}
}
pub fn new_key(&mut self) -> ComponentKey {
let local = self.nodes.allocate();
ComponentKey { instance: self.id, local }
}
pub fn insert(
&mut self,
key: ComponentKey,
instance: Instance
) -> Result<(), Error> {
/*for child in &children {
self.parents.get_mut(*child)?.push(key);
}*/
self.components.insert(key, instance);
self.parents.insert(key, Vec::with_capacity(1));
self.children.insert(key, vec![]); //children);
Ok(())
}
pub fn remove(&mut self, key: ComponentKey) -> Result<Instance, Error> {
self.parents.remove(key)?;
self.children.remove(key)?;
self.components.remove(key)
}
pub fn add_child(&mut self, key: ComponentKey, child: ComponentKey) -> Result<(), Error> {
self.parents.get_mut(child)?.push(key);
self.children.get_mut(key)?.push(child);
Ok(())
}
pub fn set_children(&mut self, key: ComponentKey, children: Vec<ComponentKey>) -> Result<(), Error> {
// Remove node as parent from all its current children.
for child in self.children.get(key)? {
self.parents.get_mut(*child)?.retain(|p| *p != key);
}
*self.children.get_mut(key)? = Vec::with_capacity(children.len());
// Build up relation node <-> child
for child in children {
self.parents.get_mut(child)?.push(key);
self.children.get_mut(key)?.push(child);
}
Ok(())
}
pub fn remove_child(&mut self, key: ComponentKey, child: ComponentKey) -> Result<ComponentKey, Error> {
match self.children(key)?.iter().position(|n| *n == child) {
Some(index) => self.remove_child_at_index(key, index),
None => Err(Error::InvalidComponentKey(child)),
}
}
pub fn remove_child_at_index(&mut self, key: ComponentKey, index: usize) -> Result<ComponentKey, Error> {
let child = self.children.get_mut(key)?.remove(index);
self.parents.get_mut(child)?.retain(|p| *p != key);
Ok(child)
}
pub fn replace_child_at_index(&mut self, key: ComponentKey, index: usize, child: ComponentKey) -> Result<ComponentKey, Error> {
self.parents.get_mut(child)?.push(key);
let old_child = std::mem::replace(&mut self.children.get_mut(key)?[index], child);
self.parents.get_mut(old_child)?.retain(|p| *p != key);
Ok(old_child)
}
pub fn children(&self, key: ComponentKey) -> Result<Vec<ComponentKey>, Error> {
self.children.get(key).map(Clone::clone)
}
pub fn child_count(&self, key: ComponentKey) -> Result<usize, Error> {
self.children.get(key).map(Vec::len)
}
pub fn get(&self, key: ComponentKey) -> Result<&Instance, Error> {
self.components.get(key)
}
pub fn get_mut(&mut self, key: ComponentKey) -> Result<&mut Instance, Error> {
self.components.get_mut(key)
}
}

View file

@ -1,84 +0,0 @@
//! This module holds pieces pertaining to `RSX` element(s), which are lightweight
//! structs that represent how something should be flushed to the screen. Alchemy
//! uses these to build and alter UI; they're typically returned from `render()`
//! methods.
use std::any::Any;
use std::fmt::{Debug, Display};
use alchemy_styles::StylesList;
mod virtual_node;
pub use virtual_node::VirtualNode;
mod virtual_text;
pub use virtual_text::VirtualText;
use crate::reconciler::key::ComponentKey;
use crate::traits::Component;
/// An enum representing the types of nodes that the
/// system can work with. `None`, `VirtualText`, or `VirtualNode`.
pub enum RSX {
None,
VirtualText(VirtualText),
VirtualNode(VirtualNode)
}
impl RSX {
/// Shorthand method for creating a new `RSX::VirtualNode` instance. Rarely should you call
/// this yourself; the `rsx! {}` macro handles this for you.
pub fn node<P: Any + 'static>(
tag: &'static str,
styles: StylesList,
create_fn: fn(key: ComponentKey) -> Box<Component>,
props: P,
children: Vec<RSX>
) -> RSX {
RSX::VirtualNode(VirtualNode {
tag: tag,
create_component_fn: create_fn,
styles: styles,
props: Box::new(props),
children: children
})
}
/// Shorthand method for creating a new `RSX::VirtualText` instance. Rarely should you call
/// this yourself; the `rsx! {}` and `text!()` macros handle this for you.
pub fn text(s: String) -> RSX {
RSX::VirtualText(VirtualText(s))
}
}
impl IntoIterator for RSX {
type Item = RSX;
type IntoIter = std::vec::IntoIter<RSX>;
/// Turn a single `RSX` node into an iterable instance.
fn into_iter(self) -> Self::IntoIter {
vec![self].into_iter()
}
}
impl Display for RSX {
/// Specialized rendering for displaying RSX nodes.
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
match self {
RSX::VirtualNode(node) => { std::fmt::Display::fmt(&node, f) },
RSX::VirtualText(text) => { std::fmt::Display::fmt(&text, f) }
RSX::None => { Ok(()) }
}
}
}
impl Debug for RSX {
/// Specialized rendering for debugging RSX nodes.
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
RSX::VirtualNode(node) => { std::fmt::Debug::fmt(&node, f) },
RSX::VirtualText(text) => { std::fmt::Debug::fmt(&text, f) }
RSX::None => { Ok(()) }
}
}
}

View file

@ -1,79 +0,0 @@
//! Implements a Props struct that mostly acts as expected. For arbitrary primitive values,
//! it shadows a `serde_json::Value`.
use serde_json::Value;
use std::collections::HashMap;
use alchemy_styles::StylesList;
use crate::rsx::RSX;
/// A value stored inside the `attributes` field on a `Props` instance.
/// It shadows `serde_json::Value`, but also allows for some other value
/// types common to Alchemy.
#[derive(Clone, Debug)]
pub enum AttributeType {
Value(Value),
//RSX(RSX)
//EventHandler(Box<ComponentEventHandler>)
}
impl<'a> From<&'a str> for AttributeType {
/// Converts a &str to a storable AttributeType.
fn from(f: &str) -> Self {
AttributeType::Value(Value::String(f.to_string()))
}
}
/// Emulates props from React, in a sense. Common keys such as `children`, `key` and `styles`
/// are extracted out for fast access, and everything else found gets put into the `attributes`
/// HashMap.
#[derive(Clone, Debug, Default)]
pub struct Props {
pub attributes: HashMap<&'static str, AttributeType>,
//pub children: Vec<RSX>,
pub key: String,
pub styles: StylesList
}
impl Props {
/// A helper method for constructing Properties.
pub fn new(
key: String,
styles: StylesList,
attributes: HashMap<&'static str, AttributeType>,
//children: Vec<RSX>
) -> Props {
Props {
attributes: attributes,
//children: children,
key: key,
styles: styles
}
}
/*/// A helper method used for constructing root-level Properties.
pub(crate) fn root(children: Vec<RSX>) -> Props {
Props {
attributes: HashMap::new(),
children: children,
key: "".into(),
styles: "root".into()
}
}
/// Returns a Vec of RSX nodes, which are really just cloned pointers for the most part.
pub fn children(&self) -> Vec<RSX> {
self.children.clone()
}*/
/// Returns a Option<&AttributeType> from the `attributes` inner HashMap.
pub fn get(&self, key: &str) -> Option<&AttributeType> {
match key {
"children" => { None },
"key" => { None },
"styles" => { None },
_ => { None } //self.attributes.get(key) }
}
}
}

View file

@ -1,54 +0,0 @@
//! Implements the `RSX::VirtualNode` struct, which is a bit of a recursive
//! structure.
use std::any::Any;
use std::fmt::{Display, Debug};
use alchemy_styles::StylesList;
use crate::reconciler::key::ComponentKey;
use crate::rsx::RSX;
use crate::traits::Component;
/// A VirtualNode is akin to an `Element` in React terms. Here, we provide a way
/// for lazy `Component` instantiation, properties, children and so on.
pub struct VirtualNode {
/// Used in debugging/printing/etc.
pub tag: &'static str,
/// Used for determining which CSS styles should be applied to this node.
/// This property is accessed often enough that it's separated out here.
pub styles: StylesList,
/// `Component` instances are created on-demand, if the reconciler deems it be so. This
/// is a closure that should return an instance of the correct type.
pub create_component_fn: fn(key: ComponentKey) -> Box<Component>,
/// When some RSX is returned, we scoop up the props inside a special block, and then shove
/// them in here as an `Any` object. When you `derive(Props)` on a `Component` struct, it
/// creates a setter that specifically handles downcasting and persisting props for you.
pub props: Box<Any>,
/// Child components for this node.
pub children: Vec<RSX>
}
impl Display for VirtualNode {
/// Special formatting for displaying nodes.
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "<{}>", self.tag)?;
for child in &self.children {
write!(f, "{:?}", child)?;
}
write!(f, "</{}>", self.tag)
}
}
impl Debug for VirtualNode {
/// Special formatting for debugging nodes.
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "VirtualNode({})", self.tag)
}
}

View file

@ -1,29 +0,0 @@
//! Implements `RSX::VirtualText`, which holds data pertaining to <Text>, primarily.
use std::fmt::{Display, Debug};
/// Currently a wrapper for `String`, but could be something else down the road. Frees
/// us from needing to change the public API later.
#[derive(Clone)]
pub struct VirtualText(pub String);
impl VirtualText {
/// Given a `String`, returns a `VirtualText` node.
pub fn new(s: String) -> VirtualText {
VirtualText(s)
}
}
impl Display for VirtualText {
/// Formatting for `VirtualText` display.
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
write!(f, "{}", self.0)
}
}
impl Debug for VirtualText {
/// Formatting for `VirtualText` debugging.
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "VirtualText({})", self.0)
}
}

View file

@ -1,193 +0,0 @@
//! Traits that are used in Alchemy. Alchemy implements a React-based Component
//! lifecycle, coupled with a delegate pattern inspired by those found in AppKit/UIKit.
use std::any::Any;
use alchemy_styles::styles::{Appearance, Layout};
//use crate::RENDER_ENGINE;
use crate::error::Error;
use crate::reconciler::key::ComponentKey;
use crate::rsx::RSX;
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
#[cfg(feature = "cocoa")]
pub type PlatformSpecificNodeType = objc_id::ShareId<objc::runtime::Object>;
/// A per-platform wrapped Pointer type, used for attaching views/widgets.
#[cfg(not(feature = "cocoa"))]
pub type PlatformSpecificNodeType = ();
/*fn update<C: Component, F: Fn() -> Box<C> + Send + Sync + 'static>(component: &Component, updater: F) {
let component_ptr = component as *const C as usize;
RENDER_ENGINE.queue_update_for(component_ptr, Box::new(updater));
}*/
/// 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 {
/// Fired when an Application is about to finish launching.
fn will_finish_launching(&mut self) {}
/// Fired when an Application has finished launching - this is a good place to, say, show your
/// window.
fn did_finish_launching(&mut self) {}
/// Fired when an Application will become active.
fn will_become_active(&mut self) {}
/// Fired when an Application became active.
fn did_become_active(&mut self) {}
/// Fired when an Application will resign active. You can use this to, say, persist resources
/// or state.
fn will_resign_active(&mut self) {}
/// Fired when an Application has resigned active.
fn did_resign_active(&mut self) {}
/// Fired when an Application is going to terminate. You can use this to, say, instruct the
/// system to "wait a minute, lemme finish".
fn should_terminate(&self) -> bool { true }
/// Fired when the Application has determined "no, you're done, stop the world".
fn will_terminate(&mut self) {}
/// A private trait method that you shouldn't call. This may change or disappear in later
/// releases. Do not rely on this.
fn _window_will_close(&self, _window_id: usize) {}
}
/// 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 {
/// 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) { }
/// Called as the first step in the `render` tree. Every Window contains its own content view
/// that is special, called the root. Widget trees are added to it as necessary, bootstrapped
/// from here.
fn render(&self) -> Result<RSX, Error> { Ok(RSX::None) }
}
pub trait Props {
fn set_props(&mut self, new_props: &mut Any);
}
/// The `Component` lifecycle, mostly inspired from React, with a few extra methods for views that
/// need to have a backing native layer. A good breakdown of the React Component lifecycle can be
/// found [in this tweet](https://twitter.com/dan_abramov/status/981712092611989509?lang=en).
///
/// Alchemy does not currently implement Hooks, and at the moment has no plans to do so (the API
/// doesn't feel comfortable in Rust, in any way I tried). If you think you have an interesting
/// proposal for this, feel free to open an issue!
pub trait Component: Props + Send + Sync {
fn new(key: ComponentKey) -> Self where Self: Sized;
/// Indicates whether a Component instance carries a native backing node. If you return `true`
/// from this, the reconciler will opt-in to the native backing layer. Returns `false` by
/// default.
fn has_native_backing_node(&self) -> bool { false }
/// Returns a wrapped-per-platform pointer type that the backing framework tree can use.
fn borrow_native_backing_node(&self) -> Option<PlatformSpecificNodeType> { None }
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `node`, you need to instruct the system how to append it to the tree at your point.
fn append_child_node(&self, _component: PlatformSpecificNodeType) {}
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `node`, you need to instruct the system how to replace it in the tree at your point.
fn replace_child_node(&self, _component: PlatformSpecificNodeType) {}
/// If you implement a Native-backed component, you'll need to implement this. Given a
/// `node`, you need to instruct the system how to remove it from the tree at your point.
fn remove_child_node(&self, _component: PlatformSpecificNodeType) {}
/// Given a configured 'appearance' and computed `layout`, this method should transform them
/// into appropriate calls to the backing native node.
fn apply_styles(&self, _appearance: &Appearance, _layout: &Layout) {}
/// Invoked right before calling the render method, both on the initial mount and on subsequent updates.
/// It should return an object to update the state, or null to update nothing.
/// This method exists for rare use cases where the state depends on changes in props over time.
fn get_derived_state_from_props(&self) {}
/// Invoked right before the most recently rendered output is committed to the backing layer tree.
/// It enables your component to capture some information from the tree (e.g. scroll position) before it's
/// potentially changed. Any value returned by this lifecycle will be passed as a parameter
/// to component_did_update().
///
/// This use case is not common, but it may occur in UIs like a chat thread that need to handle scroll
/// position in a special way. A snapshot value (or None) should be returned.
fn get_snapshot_before_update(&self) {}
/// Invoked immediately after a component is mounted (inserted into the tree).
/// If you need to load data from a remote endpoint, this is a good place to instantiate the network request.
/// This method is also a good place to set up any subscriptions. If you do that, dont forget to unsubscribe
/// in component_will_unmount().
fn component_did_mount(&mut self) {}
/// Invoked immediately after updating occurs. This method is not called for the initial render.
/// This is also a good place to do network requests as long as you compare the current props to previous props
/// (e.g. a network request may not be necessary if the props have not changed).
fn component_did_update(&mut self) {}
/// Invoked immediately before a component is unmounted and destroyed. Perform any necessary cleanup in this
/// method, such as invalidating timers, canceling network requests, or cleaning up any subscriptions that
/// were created in component_did_mount().
///
/// You should not call set state in this method because the component will never be re-rendered. Once a
/// component instance is unmounted, it will never be mounted again.
fn component_will_unmount(&mut self) {}
/// Invoked after an error has been thrown by a descendant component. Called during the "commit" phase,
/// so side-effects are permitted. It should be used for things like logging errors (e.g,
/// Sentry).
fn component_did_catch(&mut self /* error: */) {}
/// Use this to let Alchemy know if a components output is not affected by the current change in state
/// or props. The default behavior is to re-render on every state change, and in the vast majority of
/// cases you should rely on the default behavior.
///
/// This is invoked before rendering when new props or state are being received. Defaults to true. This
/// method is not called for the initial render or when force_update() is used. This method only exists
/// as a performance optimization. Do not rely on it to “prevent” a rendering, as this can lead to bugs.
fn should_component_update(&self) -> bool { true }
/// The only required method for a `Component`. Should return a Result of RSX nodes, or an
/// Error (in very rare cases, such as trying to get a key from a strange HashMap or
/// something).
///
/// The render() function should be pure, meaning that it does not modify component state, it
/// returns the same result each time its invoked, and it does not directly interact with the
/// backing rendering framework.
///
/// If you need to interact with the native layer, perform your work in component_did_mount() or the other
/// 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<RSX>) -> Result<RSX, Error> { 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.
///
/// This is called during the "render" phase, so side-effects are not permitted.
/// For those use cases, use component_did_catch() instead.
fn get_derived_state_from_error(&self, _error: ()) {}
/// By default, when your components state or props change, your component will re-render.
/// If your `render()` method depends on some other data, you can tell Alchemy that the component
/// needs re-rendering by calling `force_update()`.
///
/// Calling `force_update()` will cause `render()` to be called on the component, skipping
/// `should_component_update()`. This will trigger the normal lifecycle methods for child components,
/// including the `should_component_update()` method of each child. Alchemy will still only update the
/// backing widget tree if the markup changes.
///
/// Normally, you should try to avoid all uses of `force_update()` and only read from `this.props`
/// and `this.state` in `render()`.
fn force_update(&self) {}
}

View file

@ -1,30 +0,0 @@
[package]
name = "alchemy-macros"
description = "A crate containing macros used in Alchemy, the Rust cross-platform GUI framework."
version = "0.1.0"
edition = "2018"
authors = ["Ryan McGrath <ryan@rymc.io>"]
build = "src/build.rs"
license = "MPL-2.0+"
repository = "https://github.com/ryanmcgrath/alchemy"
categories = ["gui", "rendering::engine", "multimedia"]
keywords = ["gui", "css", "styles", "layout", "ui"]
[lib]
proc-macro = true
[badges]
maintenance = { status = "actively-developed" }
[dependencies]
ansi_term = "0.11.0"
lalrpop-util = "0.16.1"
proc-macro2 = { version = "0.4.24", features = ["nightly"] }
proc-macro-hack = "0.5.2"
quote = "0.6.10"
alchemy-styles = { version = "0.1", path = "../styles", features = ["parser", "tokenize"] }
syn = "0.15"
[build-dependencies]
lalrpop = "0.16.1"
version_check = "0.1.5"

View file

@ -1,8 +0,0 @@
# Alchemy-Macros
This crate holds macros for two things, primarily:
- `rsx! {}`, which transforms `<View></<View>` tags into their proper `RSX` calls. Much of this is forked from the awesome work done by [Bodil Stokke in typed-html](https://github.com/bodil/typed-html).
- `styles! {}`, which transforms CSS style nodes into `Vec<Styles>`, which the rendering engine uses to theme and style nodes. This relies on the [CSS Parser from Servo](https://github.com/servo/rust-cssparser). Styles do not support cascading; this is a design decision, as inheritance is already a bit of a taboo in Rust, so to do it in styling code feels really odd and involves a mental shift the deeper you go. Opt to apply successive style keys, conditionally if need be, to achieve the same thing with a compositional approach.
## Questions, Comments?
Open an issue, or hit me up on [Twitter](https://twitter.com/ryanmcgrath/).

View file

@ -1,11 +0,0 @@
extern crate lalrpop;
extern crate version_check;
fn main() {
lalrpop::process_root().unwrap();
if version_check::is_nightly().unwrap_or(false) {
println!("cargo:rustc-cfg=can_join_spans");
println!("cargo:rustc-cfg=can_show_location_of_runtime_parse_error");
}
}

View file

@ -1,116 +0,0 @@
use ansi_term::Style;
use lalrpop_util::ParseError::*;
use crate::lexer::Token;
use proc_macro2::{Ident, TokenStream};
use quote::{quote, quote_spanned};
pub type ParseError = lalrpop_util::ParseError<usize, Token, HtmlParseError>;
#[derive(Debug)]
pub enum HtmlParseError {
TagMismatch { open: Ident, close: Ident },
}
fn pprint_token(token: &str) -> &str {
match token {
"BraceGroupToken" => "code block",
"LiteralToken" => "literal",
"IdentToken" => "identifier",
a => a,
}
}
fn pprint_tokens(tokens: &[String]) -> String {
let tokens: Vec<&str> = tokens.iter().map(|s| pprint_token(&s)).collect();
if tokens.len() > 1 {
let start = tokens[..tokens.len() - 1].join(", ");
let end = &tokens[tokens.len() - 1];
format!("{} or {}", start, end)
} else {
tokens[0].to_string()
}
}
fn is_in_node_position(tokens: &[String]) -> bool {
use std::collections::HashSet;
let input: HashSet<&str> = tokens.iter().map(String::as_str).collect();
let output: HashSet<&str> = ["\"<\"", "BraceGroupToken", "LiteralToken"]
.iter()
.cloned()
.collect();
input == output
}
pub fn parse_error(input: &[Token], error: &ParseError) -> TokenStream {
match error {
InvalidToken { location } => {
let span = input[*location].span();
quote_spanned! {span=>
compile_error! { "invalid token" }
}
}
UnrecognizedToken {
token: None,
expected,
} => {
let msg = format!(
"unexpected end of macro; missing {}",
pprint_tokens(&expected)
);
quote! {
compile_error! { #msg }
}
}
UnrecognizedToken {
token: Some((_, token, _)),
expected,
} => {
let span = token.span();
let error_msg = format!("expected {}", pprint_tokens(&expected));
let error = quote_spanned! {span=>
compile_error! { #error_msg }
};
let help = if is_in_node_position(expected) && token.is_ident() {
// special case: you probably meant to quote that text
let help_msg = format!(
"text nodes need to be quoted, eg. {}",
Style::new().bold().paint("<p>\"Hello Joe!\"</p>")
);
Some(quote_spanned! {span=>
compile_error! { #help_msg }
})
} else {
None
};
quote! {{
#error
#help
}}
}
ExtraToken {
token: (_, token, _),
} => {
let span = token.span();
quote_spanned! {span=>
compile_error! { "superfluous token" }
}
}
User {
error: HtmlParseError::TagMismatch { open, close },
} => {
let close_span = close.span();
let close_msg = format!("expected closing tag '</{}>', found '</{}>'", open, close);
let close_error = quote_spanned! {close_span=>
compile_error! { #close_msg }
};
let open_span = open.span();
let open_error = quote_spanned! {open_span=>
compile_error! { "unclosed tag" }
};
quote! {{
#close_error
#open_error
}}
}
}
}

View file

@ -1,303 +0,0 @@
use crate::lexer::{self, Token, to_stream};
use crate::error::HtmlParseError;
use crate::rsx::{Node, Element};
//use crate::declare::Declare;
use crate::map::StringyMap;
use proc_macro2::{Delimiter, Ident, Literal, Group, TokenTree};
use lalrpop_util::ParseError;
use crate::span;
grammar;
/// Match a B separated list of zero or more A, return a list of A.
Separated<A, B>: Vec<A> = {
<v:(<A> B)*> <e:A?> => match e {
None => v,
Some(e) => {
let mut v = v;
v.push(e);
v
}
}
}
/// Match a B separated list of one or more A, return a list of tokens, including the Bs.
/// Both A and B must resolve to a Token.
SeparatedInc<A, B>: Vec<Token> = {
<v:(A B)*> <e:A> => {
let mut out = Vec::new();
for (a, b) in v {
out.push(a);
out.push(b);
}
out.push(e);
out
}
}
Ident: Ident = IdentToken => {
match <> {
Token::Ident(ident) => ident,
_ => unreachable!()
}
};
Literal: Literal = LiteralToken => {
match <> {
Token::Literal(literal) => literal,
_ => unreachable!()
}
};
GroupToken = {
BraceGroupToken,
BracketGroupToken,
ParenGroupToken,
};
/// A kebab case HTML ident, converted to a snake case ident.
HtmlIdent: Ident = {
<init:(<Ident> "-")*> <last:Ident> => {
let mut init = init;
init.push(last);
let (span, name) = init.into_iter().fold((None, String::new()), |(span, name), token| {
(
match span {
None => Some(token.span().unstable()),
Some(span) => {
#[cfg(can_join_spans)]
{
span.join(token.span().unstable())
}
#[cfg(not(can_join_spans))]
{
Some(span)
}
}
},
if name.is_empty() {
name + &token.to_string()
} else {
name + "_" + &token.to_string()
}
)
});
Ident::new(&name, span::from_unstable(span.unwrap()))
}
};
// The HTML macro
/// An approximation of a Rust expression.
BareExpression: Token = "&"? (IdentToken ":" ":")* SeparatedInc<IdentToken, "."> ParenGroupToken? => {
let (reference, left, right, args) = (<>);
let mut out = Vec::new();
if let Some(reference) = reference {
out.push(reference);
}
for (ident, c1, c2) in left {
out.push(ident);
out.push(c1);
out.push(c2);
}
out.extend(right);
if let Some(args) = args {
out.push(args);
}
Group::new(Delimiter::Brace, to_stream(out)).into()
};
AttrValue: Token = {
LiteralToken,
GroupToken,
BareExpression,
};
Attr: (Ident, Token) = <name:HtmlIdent> "=" <value:AttrValue> => (name, value);
Attrs: StringyMap<Ident, TokenTree> = Attr* => <>.into();
OpeningTag: (Ident, StringyMap<Ident, TokenTree>) = "<" <HtmlIdent> <Attrs> ">";
ClosingTag: Ident = "<" "/" <HtmlIdent> ">";
SingleTag: Element = "<" <name:HtmlIdent> <attributes:Attrs> "/" ">" => {
Element {
name,
attributes,
children: Vec::new(),
}
};
ParentTag: Element = <opening:OpeningTag> <children:Node*> <closing:ClosingTag> =>? {
let (name, attributes) = opening;
let closing_name = closing.to_string();
if closing_name == name.to_string() {
Ok(Element {
name,
attributes,
children,
})
} else {
Err(ParseError::User { error: HtmlParseError::TagMismatch {
open: name.into(),
close: closing.into(),
}})
}
};
Element = {
SingleTag,
ParentTag,
};
TextNode = Literal;
CodeBlock: Group = BraceGroupToken => match <> {
Token::Group(_, group) => group,
_ => unreachable!()
};
Node: Node = {
Element => Node::Element(<>),
TextNode => Node::Text(<>),
CodeBlock => Node::Block(<>),
};
pub NodeWithType: (Node, Option<Vec<Token>>) = {
Node => (<>, None),
<Node> ":" <TypeSpec> => {
let (node, spec) = (<>);
(node, Some(spec))
},
};
// The declare macro
TypePath: Vec<Token> = {
IdentToken => vec![<>],
TypePath ":" ":" IdentToken => {
let (mut path, c1, c2, last) = (<>);
path.push(c1);
path.push(c2);
path.push(last);
path
}
};
Reference: Vec<Token> = "&" ("'" IdentToken)? => {
let (amp, lifetime) = (<>);
let mut out = vec![amp];
if let Some((tick, ident)) = lifetime {
out.push(tick);
out.push(ident);
}
out
};
TypeArgs: Vec<Token> = {
TypeSpec,
TypeArgs "," TypeSpec => {
let (mut args, comma, last) = (<>);
args.push(comma);
args.extend(last);
args
}
};
TypeArgList: Vec<Token> = "<" TypeArgs ">" => {
let (left, mut args, right) = (<>);
args.insert(0, left);
args.push(right);
args
};
FnReturnType: Vec<Token> = "-" ">" TypeSpec => {
let (dash, right, spec) = (<>);
let mut out = vec![dash, right];
out.extend(spec);
out
};
FnArgList: Vec<Token> = ParenGroupToken FnReturnType? => {
let (args, rt) = (<>);
let mut out = vec![args];
if let Some(rt) = rt {
out.extend(rt);
}
out
};
TypeArgSpec = {
TypeArgList,
FnArgList,
};
TypeSpec: Vec<Token> = Reference? TypePath TypeArgSpec? => {
let (reference, path, args) = (<>);
let mut out = Vec::new();
if let Some(reference) = reference {
out.extend(reference);
}
out.extend(path);
if let Some(args) = args {
out.extend(args);
}
out
};
TypeDecl: (Ident, Vec<Token>) = <HtmlIdent> ":" <TypeSpec>;
TypeDecls: Vec<(Ident, Vec<Token>)> = {
TypeDecl => vec![<>],
<decls:TypeDecls> "," <decl:TypeDecl> => {
let mut decls = decls;
decls.push(decl);
decls
},
};
Attributes = "{" <TypeDecls> ","? "}";
TypePathList = "[" <Separated<TypePath, ",">> "]";
IdentList = "[" <Separated<Ident, ",">> "]";
Groups = "in" <TypePathList>;
Children: (Option<Vec<Token>>) = "with" <opt:TypePath?> => {
opt
};
extern {
type Location = usize;
type Error = HtmlParseError;
enum lexer::Token {
"<" => Token::Punct('<', _),
">" => Token::Punct('>', _),
"/" => Token::Punct('/', _),
"=" => Token::Punct('=', _),
"-" => Token::Punct('-', _),
":" => Token::Punct(':', _),
"." => Token::Punct('.', _),
"," => Token::Punct(',', _),
"&" => Token::Punct('&', _),
"'" => Token::Punct('\'', _),
";" => Token::Punct(';', _),
"{" => Token::GroupOpen(Delimiter::Brace, _),
"}" => Token::GroupClose(Delimiter::Brace, _),
"[" => Token::GroupOpen(Delimiter::Bracket, _),
"]" => Token::GroupClose(Delimiter::Bracket, _),
"in" => Token::Keyword(lexer::Keyword::In, _),
"with" => Token::Keyword(lexer::Keyword::With, _),
IdentToken => Token::Ident(_),
LiteralToken => Token::Literal(_),
ParenGroupToken => Token::Group(Delimiter::Parenthesis, _),
BraceGroupToken => Token::Group(Delimiter::Brace, _),
BracketGroupToken => Token::Group(Delimiter::Bracket, _),
}
}

View file

@ -1,20 +0,0 @@
//! Utility functions, originally written by Bodil Stokke
//! over in [typed-html](https://github.com/bodil/typed-html).
use proc_macro2::{Ident, Span, TokenStream, TokenTree};
use std::str::FromStr;
pub fn new_raw(string: &str, span: Span) -> Ident {
// Validate that it is an ident.
let _ = Ident::new(string, span);
let s = format!("r#{}", string);
let tts = TokenStream::from_str(&s).unwrap();
let mut ident = match tts.into_iter().next().unwrap() {
TokenTree::Ident(ident) => ident,
_ => unreachable!(),
};
ident.set_span(span);
ident
}

View file

@ -1,142 +0,0 @@
//! Implements the Lexer used for parsing RSX, originally
//! written by Bodil Stokke over in
//! [typed-html](https://github.com/bodil/typed-html).
use crate::error::HtmlParseError;
use proc_macro2::{Delimiter, Group, Ident, Literal, Punct, Span, TokenStream, TokenTree};
use std::iter::FromIterator;
pub type Spanned<Tok, Loc, Error> = Result<(Loc, Tok, Loc), Error>;
#[derive(Clone, Debug)]
pub enum Token {
Ident(Ident),
Literal(Literal),
Punct(char, Punct),
Group(Delimiter, Group),
GroupOpen(Delimiter, Span),
GroupClose(Delimiter, Span),
Keyword(Keyword, Ident),
}
impl Token {
pub fn span(&self) -> Span {
match self {
Token::Ident(ident) => ident.span(),
Token::Literal(literal) => literal.span(),
Token::Punct(_, punct) => punct.span(),
Token::Group(_, group) => group.span(),
Token::GroupOpen(_, span) => *span,
Token::GroupClose(_, span) => *span,
Token::Keyword(_, ident) => ident.span(),
}
}
pub fn is_ident(&self) -> bool {
match self {
Token::Ident(_) => true,
_ => false,
}
}
}
impl From<Token> for TokenTree {
fn from(token: Token) -> Self {
match token {
Token::Ident(ident) => TokenTree::Ident(ident),
Token::Literal(literal) => TokenTree::Literal(literal),
Token::Punct(_, punct) => TokenTree::Punct(punct),
Token::Group(_, group) => TokenTree::Group(group),
Token::GroupOpen(_, _) => panic!("Can't convert a GroupOpen token to a TokenTree"),
Token::GroupClose(_, _) => panic!("Can't convert a GroupClose token to a TokenTree"),
Token::Keyword(_, ident) => TokenTree::Ident(ident),
}
}
}
impl From<Token> for TokenStream {
fn from(token: Token) -> Self {
TokenStream::from_iter(vec![TokenTree::from(token)])
}
}
impl From<Ident> for Token {
fn from(ident: Ident) -> Self {
Token::Ident(ident)
}
}
impl From<Literal> for Token {
fn from(literal: Literal) -> Self {
Token::Literal(literal)
}
}
impl From<Punct> for Token {
fn from(punct: Punct) -> Self {
Token::Punct(punct.as_char(), punct)
}
}
impl From<Group> for Token {
fn from(group: Group) -> Self {
Token::Group(group.delimiter(), group)
}
}
#[derive(Debug, Clone)]
pub enum Keyword {
In,
With,
}
pub fn to_stream<I: IntoIterator<Item = Token>>(tokens: I) -> TokenStream {
let mut stream = TokenStream::new();
stream.extend(tokens.into_iter().map(TokenTree::from));
stream
}
pub fn unroll_stream(stream: TokenStream, deep: bool) -> Vec<Token> {
let mut vec = Vec::new();
for tt in stream {
match tt {
TokenTree::Ident(ident) => vec.push(ident.into()),
TokenTree::Literal(literal) => vec.push(literal.into()),
TokenTree::Punct(punct) => vec.push(punct.into()),
TokenTree::Group(ref group) if deep && group.delimiter() != Delimiter::Parenthesis => {
vec.push(Token::GroupOpen(group.delimiter(), group.span()));
let sub = unroll_stream(group.stream(), deep);
vec.extend(sub);
vec.push(Token::GroupClose(group.delimiter(), group.span()));
}
TokenTree::Group(group) => vec.push(group.into()),
}
}
vec
}
pub struct Lexer<'a> {
stream: &'a [Token],
pos: usize,
}
impl<'a> Lexer<'a> {
pub fn new(stream: &'a [Token]) -> Self {
Lexer { stream, pos: 0 }
}
}
impl<'a> Iterator for Lexer<'a> {
type Item = Spanned<Token, usize, HtmlParseError>;
fn next(&mut self) -> Option<Self::Item> {
match self.stream.get(self.pos) {
None => None,
Some(token) => {
self.pos += 1;
Some(Ok((self.pos - 1, token.clone(), self.pos)))
}
}
}
}

View file

@ -1,107 +0,0 @@
#![recursion_limit = "128"]
#![cfg_attr(can_show_location_of_runtime_parse_error, feature(proc_macro_span))]
//! Implements macros used in Alchemy.
//!
//! - `rsx! {}`, which turns RSX tags into `RSX` node trees.
//! - `styles! {}`, which turns CSS stylesheet strings into `Vec<Styles>`.
//!
//! In general, you should prefer using these to constructing the above values manually.
//!
//! Much of the `rsx! {}` support is achieved by forking code riginally written by Bodil Stokke
//! over in [typed-html](https://github.com/bodil/typed-html).
extern crate proc_macro;
mod error;
mod rsx;
mod ident;
mod lexer;
mod map;
mod parser;
mod span;
use proc_macro::TokenStream;
use proc_macro2::{Ident, TokenStream as TokenStream2, Literal, Span};
use proc_macro_hack::proc_macro_hack;
use quote::quote;
use syn::{DeriveInput, parse_macro_input};
use alchemy_styles::cssparser::{Parser, ParserInput, RuleListParser};
use alchemy_styles::styles_parser::{Rule, RuleParser};
/// Implements the `rsx! {}` macro, which turns RSX tags into `RSX` node trees.
#[proc_macro_hack]
pub fn rsx(input: TokenStream) -> TokenStream {
let stream = lexer::unroll_stream(input.into(), false);
let result = rsx::expand_rsx(&stream);
TokenStream::from(match result {
Err(err) => error::parse_error(&stream, &err),
Ok((node, ty)) => match node.into_token_stream(&ty) {
Err(err) => err,
Ok(success) => success,
},
})
}
/// Implements the `styles! {}` macro, which turns CSS stylesheet strings into `Vec<Styles>`.
#[proc_macro_hack]
pub fn styles(input: TokenStream) -> TokenStream {
let s = input.to_string().replace(" ", "");
let mut input = ParserInput::new(&s);
let mut parser = Parser::new(&mut input);
let parsed: Vec<Rule> = RuleListParser::new_for_stylesheet(&mut parser, RuleParser {})
.collect::<Vec<_>>()
.into_iter()
.filter_map(|rule| {
rule.ok()
})
.collect();
let mut body = TokenStream2::new();
for rule in parsed {
let mut stream = TokenStream2::new();
for style in rule.styles {
stream.extend(quote!(#style,));
}
let key = Literal::string(&rule.key);
body.extend(quote!(styles.insert(#key, vec![#stream]);))
}
quote!(alchemy::StyleSheet::new({
use alchemy::style_attributes::*;
use alchemy::Color;
let mut styles = std::collections::HashMap::new();
#body
styles
})).into()
}
/// Implements a derive macro for automating props setting and conversion.
#[proc_macro_derive(Props)]
pub fn writable_props_derive(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let name = &input.ident;
let name_props = Ident::new(&format!("{}Props", name), Span::call_site());
let generics = input.generics;
let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
TokenStream::from(quote! {
impl #impl_generics #name #ty_generics #where_clause {
pub fn default_props() -> #name_props {
#name_props::default()
}
}
impl #impl_generics alchemy::ComponentProps for #name #ty_generics #where_clause {
fn set_props(&mut self, new_props: &mut std::any::Any) {
match new_props.downcast_ref::<#name_props>() {
Some(props) => { },
None => { panic!("Woah there, somehow the wrong props were being passed!"); }
}
}
}
})
}

View file

@ -1,54 +0,0 @@
//! Implements StringyMap, originally written by Bodil Stokke
//! over in [typed-html](https://github.com/bodil/typed-html).
use std::collections::BTreeMap;
#[derive(Clone)]
pub struct StringyMap<K, V>(BTreeMap<String, (K, V)>);
impl<K, V> StringyMap<K, V>
where
K: ToString,
{
pub fn new() -> Self {
StringyMap(BTreeMap::new())
}
pub fn insert(&mut self, k: K, v: V) -> Option<V> {
let s = k.to_string();
self.0.insert(s, (k, v)).map(|(_, v)| v)
}
pub fn remove(&mut self, k: &K) -> Option<V> {
let s = k.to_string();
self.0.remove(&s).map(|(_, v)| v)
}
pub fn iter(&self) -> impl Iterator<Item = &(K, V)> {
self.0.values()
}
pub fn keys(&self) -> impl Iterator<Item = &K> {
self.0.values().map(|(k, _)| k)
}
#[allow(dead_code)]
pub fn len(&self) -> usize {
self.0.len()
}
}
impl<K, V, OK, OV> From<Vec<(OK, OV)>> for StringyMap<K, V>
where
OK: Into<K>,
OV: Into<V>,
K: ToString,
{
fn from(vec: Vec<(OK, OV)>) -> Self {
let mut out = Self::new();
for (key, value) in vec {
out.insert(key.into(), value.into());
}
out
}
}

View file

@ -1,6 +0,0 @@
//! Implements parsing, originally written by Bodil Stokke
//! over in [typed-html](https://github.com/bodil/typed-html).
use lalrpop_util::lalrpop_mod;
lalrpop_mod!(pub grammar);

View file

@ -1,240 +0,0 @@
use proc_macro2::{Delimiter, Group, Ident, Literal, Span, TokenStream, TokenTree};
use quote::{quote, quote_spanned};
use crate::error::ParseError;
use crate::ident;
use crate::lexer::{/*to_stream, */Lexer, Token};
use crate::map::StringyMap;
use crate::parser::grammar;
use std::iter::FromIterator;
#[derive(Clone)]
pub enum Node {
Element(Element),
Text(Literal),
Block(Group),
}
impl Node {
pub fn into_token_stream(self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
match self {
Node::Element(el) => el.into_token_stream(ty),
Node::Text(text) => {
let text = TokenTree::Literal(text);
Ok(quote!(alchemy::RSX::text(#text.to_string())))
}
Node::Block(group) => {
let span = group.span();
let error =
"you cannot use a block as a top level element or a required child element";
Err(quote_spanned! { span=>
compile_error! { #error }
})
}
}
}
fn into_child_stream(self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
match self {
Node::Element(el) => {
let el = el.into_token_stream(ty)?;
Ok(quote!(
/*element.*/children.push(#el);
))
}
tx @ Node::Text(_) => {
let tx = tx.into_token_stream(ty)?;
Ok(quote!(
/*element.*/children.push(#tx);
))
}
Node::Block(group) => {
let group: TokenTree = group.into();
Ok(quote!(
for child in #group.into_iter() {
/*element.*/children.push(child);
}
))
}
}
}
}
#[derive(Clone)]
pub struct Element {
pub name: Ident,
pub attributes: StringyMap<Ident, TokenTree>,
pub children: Vec<Node>,
}
fn extract_event_handlers(
attrs: &mut StringyMap<Ident, TokenTree>,
) -> StringyMap<Ident, TokenTree> {
let mut events = StringyMap::new();
let keys: Vec<Ident> = attrs.keys().cloned().collect();
for key in keys {
let key_name = key.to_string();
let prefix = "on";
if key_name.starts_with(prefix) {
let event_name = &key_name[prefix.len()..];
let value = attrs.remove(&key).unwrap();
events.insert(ident::new_raw(event_name, key.span()), value);
}
}
events
}
fn process_value(value: &TokenTree) -> TokenStream {
match value {
TokenTree::Group(g) if g.delimiter() == Delimiter::Bracket => {
let content = g.stream();
quote!( [ #content ] )
}
TokenTree::Group(g) if g.delimiter() == Delimiter::Parenthesis => {
let content = g.stream();
quote!( ( #content ) )
}
v => TokenStream::from_iter(vec![v.clone()]),
}
}
fn is_string_literal(literal: &Literal) -> bool {
// This is the worst API
literal.to_string().starts_with('"')
}
#[allow(dead_code)]
fn stringify_ident(ident: &Ident) -> String {
let s = ident.to_string();
if s.starts_with("r#") {
s[2..].to_string()
} else {
s
}
}
impl Element {
fn into_token_stream(mut self, ty: &Option<Vec<Token>>) -> Result<TokenStream, TokenStream> {
let name = self.name;
let name_str = name.to_string();
let typename: TokenTree = Ident::new(&name_str, name.span()).into();
let events = extract_event_handlers(&mut self.attributes);
let attrs = self.attributes.iter().map(|(key, value)| {
let name = key.to_string();
let token = TokenTree::Ident(ident::new_raw(&name, key.span()));
(name, token, value)
});
let mut attributes = TokenStream::new();
let mut styles = TokenStream::new();
styles.extend(quote!(alchemy::SpacedSet::new()));
for (attr_str, key, value) in attrs {
match value {
TokenTree::Literal(lit) if is_string_literal(lit) => {
let mut eprintln_msg = "ERROR: ".to_owned();
#[cfg(can_show_location_of_runtime_parse_error)]
{
let span = lit.span();
eprintln_msg += &format!(
"{}:{}:{}: ",
span.unstable()
.source_file()
.path()
.to_str()
.unwrap_or("unknown"),
span.unstable().start().line,
span.unstable().start().column
);
}
eprintln_msg += &format!(
"<{} {}={}> failed to parse attribute value: {{}}",
name_str, attr_str, lit,
);
#[cfg(not(can_show_location_of_runtime_parse_error))]
{
eprintln_msg += "\nERROR: rebuild with nightly to print source location";
}
attributes.extend(quote!(
props.#key = #lit.parse().unwrap_or_else(|err| {
eprintln!(#eprintln_msg, err);
panic!("Failed to parse string literal");
});
));
},
value => {
let prop = key.to_string();
let value = process_value(value);
if prop == "r#styles" {
styles = quote!(std::convert::Into::into(#value));
continue;
}
if prop == "r#key" {
continue;
}
attributes.extend(quote!(
props.#key = std::convert::Into::into(#value);
));
}
}
}
for (key, _value) in events.iter() {
if ty.is_none() {
let mut err = quote_spanned! { key.span() =>
compile_error! { "when using event handlers, you must declare the output type inside the rsx! macro" }
};
let hint = quote_spanned! { Span::call_site() =>
compile_error! { "for example: change rsx!(<div>...</div>) to rsx!(<div>...</div> : String)" }
};
err.extend(hint);
return Err(err);
}
//let key = TokenTree::Ident(key.clone());
//let value = process_value(value);
/*body.extend(quote!(
element.events.#key = Some(alchemy::dom::events::IntoEventHandler::into_event_handler(#value));
));*/
}
/*let mut args = TokenStream::new();
let mut type_annotation = TokenStream::new();
if let Some(ty) = ty {
let type_var = to_stream(ty.clone());
type_annotation.extend(quote!(: #typename<#type_var>));
}*/
let mut children = TokenStream::new();
children.extend(self.children.into_iter().map(|node| {
node.into_child_stream(ty)
}).collect::<Result<Vec<TokenStream>, TokenStream>>()?);
let component_name = Literal::string(&typename.to_string());
Ok(quote! {
alchemy::RSX::node(#component_name, #styles, |key| {
Box::new(<#typename as alchemy::Component>::new(key))
}, {
let props = #typename::default_props();
#attributes
Box::new(props)
}, {
let mut children = vec![];
#children
children
})
})
}
}
// FIXME report a decent error when the macro contains multiple top level elements
pub fn expand_rsx(input: &[Token]) -> Result<(Node, Option<Vec<Token>>), ParseError> {
grammar::NodeWithTypeParser::new().parse(Lexer::new(input))
}

View file

@ -1,13 +0,0 @@
//! Utility functions, originally written by Bodil Stokke
//! over in [typed-html](https://github.com/bodil/typed-html).
use proc_macro;
use proc_macro2;
pub fn from_unstable(span: proc_macro::Span) -> proc_macro2::Span {
let ident = proc_macro::Ident::new("_", span);
let tt = proc_macro::TokenTree::Ident(ident);
let tts = proc_macro::TokenStream::from(tt);
let tts2 = proc_macro2::TokenStream::from(tts);
tts2.into_iter().next().unwrap().span()
}

2
robots.txt Normal file
View file

@ -0,0 +1,2 @@
User-agent: *
Sitemap: https://alchemy.rs/sitemap.xml

View file

@ -1 +0,0 @@
1.35.0

8
sitemap.xml Normal file
View file

@ -0,0 +1,8 @@
<urlset xmlns="https://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
<url>
<loc>https://alchemy.rs/</loc>
</url>
</urlset>

8
src/build Executable file
View file

@ -0,0 +1,8 @@
#!/bin/bash
# Feel free to change this via pull request if you want a different shell. ;P
rm -rf ../css ../images ../favicons
zola build
mv public/* ../
rm -rf public

View file

@ -10,7 +10,7 @@ highlight_code = true
highlight_theme = "dracula" highlight_theme = "dracula"
# Whether to build a search index to be used later on by a JavaScript library # Whether to build a search index to be used later on by a JavaScript library
build_search_index = true build_search_index = false
[extra] [extra]
# Put all your custom variables here # Put all your custom variables here

BIN
src/static/banner.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 149 KiB

20
src/static/css/layout.css Normal file
View file

@ -0,0 +1,20 @@
* { box-sizing: border-box; }
html, body { padding 0; margin: 0; background: #131414; color: #e1edf1; font-family: Helvetica Neue,Helvetica,sans-serif; }
body { max-width: 900px; margin: 0 auto; }
a, a:visited { color: #ca1134; }
a:hover { color: #d90b31; background: #f4f4f4; }
pre { font-size: 1rem; line-height: 1.3rem; border-radius: 4px; overflow: auto; padding: 1.3rem; margin-bottom: 1.5rem; font-family: "Anonymous Pro", Consolas, Menlo, Monaco, Lucida Console, Liberation Mono, DejaVu Sans Mono, Bitstream Vera Sans Mono, Courier New, monospace, serif; }
pre span { font-style: normal !important; text-decoration: none !important; }
#banner { width: 100%; }
h1 { color: #f3f3f3; font-size: 2.1rem; line-height: 2.4rem; font-family: aleo; margin: .5rem 0 .6rem; }
h2 { font-size: 1.3rem; line-height: 1.8rem; font-family: aleo, georgia, serif; margin: 0; padding: 2rem 1.3rem .45rem 0; color: #959e9d; }
p { font-size: 1rem; line-height: 1.4rem; }
#tempGetStarted { text-align: center; }
#tempGetStarted h2 { padding-right: 0; }
.getStartedBtn { display: inline-block; font: normal 1.4rem/2rem aleo, georgia, serif; padding: 1.4rem 4rem; background: #307ace; text-decoration: none; color: #e1edf1; border-radius: 8px; }
.gh { margin-right: 1.4rem; }
footer { text-align: center; margin-top: 4rem; margin-bottom: 2rem; font-size: .8rem; line-height: 1rem; color: #555; }

12
src/static/css/reset.css Normal file

File diff suppressed because one or more lines are too long

BIN
src/static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

Some files were not shown because too many files have changed in this diff Show more