/rsx-embedding

Several macros and helpers for embedding RSX Renderers on various platforms

Primary LanguageRust

Under heavy research and development, please don't use this yet!

rsx-embedding

License: MPL 2.0 Build Status

Several macros and helpers for embedding RSX Renderers on various platforms, rendering display lists generated by the RSX Primitives library. Handles JSON and WebRender display items, targetting web platforms (WebGL, Canvas, DOM), iOS using UIView and native controls, VR using WebVR, and native using WebRender, powered by glutin and gleam.

Purpose

This crate allows Rust-based frontends built using RSX to be embedded across a multitude of platforms.

How to use

For quick and easy example demos, simply check out here.

To build your own project from scratch, follow the steps outlined below.

Writing RSX code

To get access to the rsx!, css! and link! macros, as well as the renderers, add this to your Cargo.toml file:

[dependencies]
rsx = { git = "https://github.com/victorporof/rsx.git" }
rsx-embedding = { git = "https://github.com/victorporof/rsx-embedding.git" }

...and clone the rsx-renderers crate as a submodule:

git submodule add https://github.com/victorporof/rsx-renderers.git

Why a submodule and not a dependency? The rsx-renderers contains helpers for rendering in external environments, is used this way to allow linking to the pre-built renderers as .js and .swift files for web and iOS targets respectively, without having to rely on build steps. It can be equally straightforward to depend on them normally, as a dependency listed on crates.io or a link to a github repository, but that could involve several build steps using webpack, gulp, CocoaPods etc.

Then, simply import the library into your code and use the rsx!, css! macros to parse RSX and CSS into rsx_dom::DOMNode, or rsx_dom::Stylesheet data structures respectively. Use the link! macro, combined with feature gates to specify and link to the target platform. Enable proc_macro and link_args if the current version of the Rust nighly compiler still has them turned off by default.

main.rs

#![feature(box_syntax)]
#![feature(proc_macro)]
#![feature(link_args)]

extern crate rsx;
#[macro_use]
extern crate rsx_embedding;

use rsx::{css, load_font, load_image, rsx};
use rsx_embedding::rsx_dom::types::*;
use rsx_embedding::rsx_stylesheet::types::*;
use rsx_embedding::rsx_primitives::prelude::{DOMNode, FileCache, FontCache, ImageCache};
use rsx_embedding::rsx_resources::fonts::types::{EncodedFont, FontId};
use rsx_embedding::rsx_resources::images::types::{EncodedImage, ImageEncodingFormat, ImageId};

fn setup(_: &mut FileCache, images: &mut ImageCache, fonts: &mut FontCache) {
    let logo = load_image!("fixtures/images/Quantum.png");
    images.add_image(ImageId::new("logo"), &logo).ok();

    let font = load_font!("fixtures/fonts/FreeSans.ttf");
    fonts.add_font(FontId::new("font"), &font, 0).ok();
}

fn render() -> DOMNode {
    let mut stylesheet = css!("src/example.css");

    rsx! {
        <view style={stylesheet.take(".center")}>
            <view style={stylesheet.take(".root")}>
                <image style={stylesheet.take(".image")} src="logo" />
                <text style={stylesheet.take(".text")}>
                    { "Hello World!" }
                </text>
            </view>
        </view>
    }
}

link!(setup, render);

The link! macro's behavior depends on the rsx-renderers/*-embedding feature gate and will target the appropriate code for externing and turning on the link flags necessary to have Rust code available to external consumers, such as asmjs-unknown-emscripten, wasm32-unknown-emscripten, x86_64-apple-darwin etc.

Writing code for native targets with WebRender

For native targets, similarly to any other targets, implementation code remains the same but different features must be specified in the crate.

Enable compiler plugin macros and a native WebRender-based renderer using WebRender display lists like this:

Cargo.toml

[features]
default = ["target-native"]

target-native = [
  "rsx-renderers/webrender-display-list",
  "rsx-renderers/native-embedding"
]

Then simply:

cargo build

Writing code for web targets

For web targets, similarly to any other targets, implementation code remains the same but different features must be specified in the crate. Targetting the web involves enabling some custom features in your Cargo.toml file, as well as installing emscripten and embedding into a regular web project.

Enable compiler plugin macros and web-based renderers using JSON-serialized display lists like this:

Cargo.toml

[features]
default = ["target-web"]

target-web = [
  "rsx-renderers/json-display-list",
  "rsx-renderers/web-embedding"
]

To build, you need to install Emscripten, and a couple of Rust targets.

Installing Rust targets

rustup target add asmjs-unknown-emscripten
rustup target add wasm32-unknown-emscripten

Installing Emscripten

Follow the steps outlined in the official docs, or build from source from the Github repo. If you're using fish as your shell, the emsdk_env.sh script won't add the necessary entries to your $PATH, so either run it under sh or add the paths yourself.

./emsdk update
./emsdk install latest
./emsdk activate latest
source ./emsdk_env.sh

Building

cargo build --target=asmjs-unknown-emscripten

This will generate the apropriate asm.js code inside the cargo targets directory, which can then be used in content code. embedding this into a document is the client's responsibility, and fairly straightforward (see below).

Embedding into web targets

The dist/target-web directory contains the webpacked web renderers readily available for embedding. These are RSXCanvasRenderer (for 2D and WebGL contexts) and RSXDOMRenderer (for creating a web-based DOM tree from a display list) respectively. The latter retains a11y features.

<style>
  body {
    margin: 0;
  }
</style>

<body>
  <script type="text/javascript" src="rsx-renderers/dist/target-web/renderer.min.js" charset="utf-8"></script>
  <script type="text/javascript" src="target/asmjs-unknown-emscripten/../example.js" charset="utf-8"></script>
  <script>
    const getDisplayList = cwrap('__get_display_list', 'string', ['number', 'number']);

    const width = window.innerWidth;
    const height = window.innerHeight;
    const displayList = JSON.parse(getDisplayList(width, height));

    const canvas = new RSXCanvasRenderer(width, height);
    canvas.mount(document.body);
    canvas.draw(displayList);
  </script>
</body>

Writing code for iOS targets

For iOS targets, implementation code remains the same but different features must be specified for the crate, building and linking jemalloc, as well as configuring a new Swift-powered XCode project being necessary.

For example, enable compiler plugin macros and ios-based renderers using JSON-serialized display lists like this:

Cargo.toml

[lib]
name = "demo"
path = "src/main.rs"
crate-type = ["staticlib", "cdylib"]

[features]
default = ["target-ios"]

target-ios = [
  "rsx-renderers/json-display-list",
  "rsx-renderers/ios-embedding"
]

To build the above example for iOS, install the appropriate architectures, as well as cargo-lipo:

rustup target add aarch64-apple-ios armv7-apple-ios armv7s-apple-ios x86_64-apple-ios i386-apple-ios
cargo install cargo-lipo

Then simply:

cargo lipo --release

Embedding into iOS targets

To embed into an iOS project, a few steps are necessary: building and linking jemalloc (required for enabling several underlying Servo components to work), configuring a new Swift-powered XCode project and importing the necessary renderers.

Building jemalloc

To build a universal jemalloc iOS library, one can either get really angry (or sad) trying for a while, or use this script instead. It will download, configure, build and install the library into a "jemalloc" subdirectory.

./build-jemalloc.sh
Creating the XCode project

Setting up XCode is fairly easy and follows the steps described here, with the additional linking of jemalloc and importing the necessary renderers into the project.

Header files

Link the following files to a new Swift-powered XCode project.

externals.h
#include <stdint.h>

const char* __get_display_list(float, float);
void __free_display_list(char *);
bridging-header.h
#ifndef bridging_header_h
#define bridging_header_h

#include "externals.h"

#endif

Don't forget to specify the bridging header file under the Project Settings. See this tutorial for how to do that.

Linking libraries

Link the libresolv.tbd library, as well as the jemalloc and library created with cargo lipo --release in the steps above (located under cargo/target/universal/release/).

Don't forget to specify the library search paths under the Project Settings. See this tutorial for how to do that.

Use CocoaPods to handle JSON (Optional)

If you'd like to deal with JSON in a more easier way, installing CocoaPods and depending on SwiftyJSON is a good idea.

sudo gem install cocoapods
pod init

Add this to your podfile:

target 'Example' do
  use_frameworks!
  pod 'SwiftyJSON'
end

Then install with:

pod install
Enable cross-origin http loads (Optional)

To display images loaded from remote URLs in your project, add this to your Info.plist:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
Integrating renderers

The src/target-ios directory contains the iOS renderers readily available for embedding. This only includes UIViewRenderer for the time being, which uses iOS UIView and subclasses to render the display list.

Add UIViewRenderer.swift to your XCode project, then:

ViewController.swift
import Foundation
import SwiftyJSON

class Bridge {
    func getDisplayList(width: Float, height: Float) -> JSON {
        let result = __get_display_list(width, height)
        let swift_result = String(cString: result!)
        let json = JSON(data: swift_result.data(using: .utf8)!)
        __free_display_list(UnsafeMutablePointer(mutating: result))
        return json
    }
}

...

let sizeRect = self.view.frame.size
let width = Float(sizeRect.width)
let height = Float(sizeRect.height)

let bridge = Bridge()
let displayList = bridge.getDisplayList(width: width, height: height)

let renderer = UIViewRenderer(deviceWidth: width, deviceHeight: height)
renderer.mount(parentView: self.view)
renderer.draw(displayList: displayList)

Reexports

Note: rsx-renderers also re-exports the rsx-primitives crate, the rsx-dom crate, the rsx-stylesheet crate and the rsx-layout crate.