🖼 A Rust library for building user interfaces on the web with WebGL.

Related tags

GUI wasm-ui
Overview

wasm-ui

Hey! Welcome to my little experiment. It's a Rust library for building user interfaces on the web. You write the interface in Rust, and wasm-ui compiles it to WebAssembly and renders to a WebGL canvas.

See the roadmap for more info. Today, the library implements a box model, flex layout, and can render rectangles of single colors.

Why am I building this?

  • Building the library is fun. Kinda. It's equal parts pain and enjoyment.
  • I was curious about how Figma and Google Docs use WebGL for their interfaces.
  • WebAssembly can reduce the load time of certain web applications.
  • In theory, it could be ported to native platforms without the overhead of webviews components like Electron or Cordova.
  • There's potential for GPU-accelerating drawing.

Why should you be skeptical?

There are quite a lot of hurdles to overcome. These are the big ones:

  • This is (very) hard to build. I'm effectively rebuilding the layout + render pipeline of a browser.
  • It's harder to use and doesn't interop nicely with existing Javascript libraries.
  • Accessibility must be built from scratch using a parallel DOM structure in order for the browser to generate the accessibility tree.
  • Most websites won't benefit from it.

Again, this is an experiment. Very little works yet, but I still think it's pretty cool. Thanks for checking it out ❤️

Usage

This library isn't distributed yet. If you want to use it, you have to clone it and write code in the core/src directory. I've already written the boilerplate, and the library code lives in the core/crates subdirectory.

The following commands will clone the project, build it, and begin serving the demo application on localhost:8080:

$ git clone https://github.com/harrisonturton/wasm-ui.git
$ cd wasm-ui && ./bin/build
$ ./bin/start

Roadmap

Figuring out what's possible

These are messy one-off experiments to make sure I can actually build everything.

  • Build + deploy Rust on the web
  • Control a WebGL canvas with Rust
  • Render glyphs from a font
  • Implement a basic box layout algorithm (based on Flutter + CSS box model)
  • Pass browser events to the Rust library (cross the JS-WASM boundary)

Actual Roadmap

Now that I've finished with the little experiments, I'm confident that building the UI library is possible. Not easy, but possible. The current challenge is to combine all the individual spike projects, ideally with some half-decent architecture.

  • Build WebGL render driver
  • Build data structure to represent the UI
  • Implement box layout algorithm
  • Implement flex layout
  • Write layout tests 🙈
  • Add box styles

Documentation

Minimal working web example

BrowserDriver { // Forward panic messages to console.error #[cfg(feature = "console_error_panic_hook")] std::panic::set_hook(Box::new(console_error_panic_hook::hook)); let app = App::new(); BrowserDriver::try_new(canvas_id, Box::new(app)).unwrap() } pub struct App {} impl AppDriver for App { fn tick(&mut self, time: f32) -> Box { Box::new(Container { size: (100.0, 100.0).into(), color: Color::blue(), ..Default::default() }) } } ">
use platform::browser::BrowserDriver;
use wasm_bindgen::prelude::wasm_bindgen;

// Use `wee_alloc` as the global allocator, because it is smaller.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

/// This is called from the browser as soon as the WASM package is loaded. It is
/// the main entrypoint to the application.
#[wasm_bindgen]
pub fn start(canvas_id: &str) -> BrowserDriver {
    // Forward panic messages to console.error
    #[cfg(feature = "console_error_panic_hook")]
    std::panic::set_hook(Box::new(console_error_panic_hook::hook));

    let app = App::new();
    BrowserDriver::try_new(canvas_id, Box::new(app)).unwrap()
}

pub struct App {}

impl AppDriver for App {
    fn tick(&mut self, time: f32) -> Box<dyn Layout> {
        Box::new(Container {
            size: (100.0, 100.0).into(),
            color: Color::blue(),
            ..Default::default()
        })
    }
}

App Boilerplate

The library only requires that your application implements the AppDriver trait. This allows your app to be "driven" by a variety of different platforms.

pub trait AppDriver {
    fn tick(&mut self, time: f32) -> Box<dyn Layout>;
}

This is called every frame. The Layout trait is implemented by widgets that can be rendered by the wasm-ui library. For example, a simple app might look like:

pub struct App {
    position: f32,
}

impl AppDriver for App {
    fn tick(&mut self, time: f32) -> Box<dyn Layout> {
        self.position.x += 100.0 * time.sin();
        Box::new(Container {
            size: self.position,
            color: Color::blue(),
            ..Default::default()
        })
    }
}

This will render a blue square that is 100 pixels wide and 100 pixels tall that moves back and forth on the screen.

Note the usage of Default::default(). This allows us to only define the fields we need, rather than being forced to specify every single in a widget. In this case, Container is defined like this:

pub struct Container {
    pub size: Vector2,
    pub color: Color,
    pub child: Option<Box<dyn Layout>>,
}

By using ..Default::default(), it automatically sets Container.child to None. In this example it doesn't help us too much, but it's more useful with widgets that are highly configurable.

Flex Containers

wasm-ui has two main flex containers, Row and Column.

Box::new(Row {
    cross_axis_alignment: CrossAxisAlignment::Center,
    main_axis_alignment: MainAxisAlignment::SpaceEvenly,
    children: vec![
        Flex::Fixed {
            child: Box::new(Container{
                size: (100.0, 200.0).into(),
                color: Color::red(),
                ..Default::default()
            }),
        },
        Flex::Fixed {
            child: Box::new(Container{
                size: (100.0, 100.0).into(),
                color: Color::green(),
                ..Default::default()
            }),
        },
        Flex::Fixed {
            child: Box::new(Container{
                size: (100.0, 100.0).into(),
                color: Color::blue(),
                ..Default::default()
            }),
        },
    ]
})

This will position three rectangles – red, green and blue – horizontally in the center of the screen. The red rectangle will be twice as tall as the green and blue squares.

Screen Shot 2021-11-14 at 2 11 00 pm

If we change the green square to this:

Flex::Flexible {
    flex: 1.0,
    child: Box::new(Container{
        size: (100.0, 100.0).into(),
        color: Color::green(),
        ..Default::default()
    }),
},

Then it will expand to fill the screen in the horizontal direction, pushing the red and blue squares to the edges of the screen.

Screen Shot 2021-11-14 at 2 11 15 pm

The codebase

core/ is the Rust implementation. This gets compiled to WASM and async loaded in the browser.

core/crates/platform implements the platform-specific rendering logic and app drivers. The drivers sit between the application logic and the deploy target. For instance, the browser driver translates browser events into something the application can understand. It also takes the render primitive output of the application and draws it on the WebGL canvas.

Currently I've just implemented this for the browser, but I'm trying to keep it generic. There should be pretty easy to port to any OpenGL target, so I imagine it wouldn't be too much extra work to support native platforms.

core/crates/math a generic linear algebra math library. Implements things like vectors and matrices.

core/src is the demo application to demonstrate how the UI libraries could be used.

bin/ holds some helper scripts to build the project and run the dev server.

web/ contains the scaffolding for the web target. This is a small React project that does nothing except async load the WASM libraries and initialises some event listeners to pass browser events to the browser driver.

Building

./bin/build will build everything and shove it in the /build folder. You can run ./bin/build to only build the core or web parts.

./bin/start will start the dev server on :8080. This will automatically detect changes in the web directory, but you'll have to manually rebuild any Rust changes with ./bin/build core.

Comments
  • Add FlexGroup component

    Add FlexGroup component

    Warning, huge PR. There are 1500+ lines of tests 🤧 I'd split this up if I wasn't working by myself.

    This PR is to implement two new components:

    • FlexGroup
    • Flex

    Which implement a similar layout algorithm as flexbox on the web and flex widgets in Flutter.

    It also makes a series of related changes to other modules, like Vector2, and implements a new test_util workspace crate. I'm hoping the tests set some precedent for how I can begin testing other widgets.

    opened by harrisonturton 3
  • Refactor Container and LayoutBox

    Refactor Container and LayoutBox

    This PR refactors the Container widget adds a new margin property to SizedLayoutBox and LayoutBox.

    Previously, a Container was implementing margin and padding by nesting another widget inside itself and setting its own material to Material::None.

    This is pretty redundant and pollutes the LayoutTree with additional widgets that exist simply to offset their child. This is a symptom of the layout abstraction not being powerful enough; padding and margins are common properties and should be baked into the layout model.

    This is already done in the CSS box model and in Flutter and various other layout libraries.

    The new responsibilities of LayoutBox are:

    • Padding should be implemented by the parent widget passing custom constraints to it's child, and positioning it accordingly
    • Margin should be implemented through the SizedLayoutBox return type

    In the canary_layouts_flex_sidebar canary layout test, it reduces the number of generated LayoutBox items from 9 to 6! That's a ~44% reduction. Good stuff 👌

    opened by harrisonturton 1
  • Add solid borders

    Add solid borders

    This PR refactors Material and the platform/browser module to add support for solid-colored borders around for all LayoutBox elements. Currently it only adds support on Container widgets, but it should be easy to expand this out in the future.

    A border increases the size of the LayoutBox according to the size of the border. This is the same as the CSS box-sizing: content-box property.

    This PR is mixed up with #3 because I was tired and forgot to switch branches before writing new code, and then they got tangled. Since I'm the only one working on this right now, I don't mind 🤷‍♂️ I'll resolve anything when I merge them both.

    opened by harrisonturton 0
  • Refactor flex container

    Refactor flex container

    This PR simplifies the API for the flex container and introduces "canary layout" integration tests. It also adds a bunch of rustdoc comments.

    Simplified flex container API

    Before this PR, flex layout was done inside a FlexGroup which required that each child was wrapped in a Flex::Flexible or Flex::Fixed enum instance:

    let widgets = FlexGroup {
        children: vec![
            Flex::Fixed {
                child: Box::new(/* fixed child */),
            },
            Flex::Flexible {
                flex: 1.0,
                child: Box::new(/* flexible child */),
            },
        ],
        ..Default::default()
    };
    

    This PR removes the Flex num requirement, and renames FlexGroup to Flex. We now only need to wrap flexible children with a Flexible widget:

    let widgets = Flex {
        children: vec![
            Box::new(/* fixed child */),
            Flexible {
                flex: 1.0,
                child: Box::new(/* flexible child */),
            },
        ],
        ..Default::default()
    };
    

    Which is much nicer to use.

    This is achieved by making the Flex struct accept Box<dyn FlexLayout> children instead of Box<dyn Layout>:

    pub trait FlexLayout: Debug {
        fn flex_factor(&self) -> Option<f32>;
        fn flex_layout(&self, tree: &mut LayoutTree, constraints: &BoxConstraints) -> SizedLayoutBox;
    }
    
    pub struct Flex {
        /* snip */
        pub children: Vec<Box<dyn FlexLayout>>,
    }
    

    And implements FlexLayout as a metatrait on all existing Layout types:

    impl<T> FlexLayout for T
    where
        T: Layout,
    {
        fn flex_factor(&self) -> Option<f32> {
            None
        }
    
        fn flex_layout(&self, tree: &mut LayoutTree, constraints: &BoxConstraints) -> SizedLayoutBox {
            self.layout(tree, constraints)
        }
    }
    

    Rust traits to the rescue!

    Canary layout integration tests

    Although the flex module already has an obscene number of tests, they are unit tests that target very specific and very small units of layout logic. They don't how different widgets might interact with each other.

    The integration tests live in the tests/ folder alongside src/ and are for testing the composition of widgets in complex layouts.

    opened by harrisonturton 0
  • This is great!

    This is great!

    Hi @harrisonturton ,

    I've been actively looking for a webGL framework to help me build out a project I'm working on (flowbench.io) and something like this would be super useful. I'm surprised the webGL community hasn't gotten onto something like this sooner.

    I've been considering ensoGL but am finding it a bit intimidating. I love how clean and simple your syntax is.

    Good luck!

    opened by viztastic 0
  • Transparent colors are not rendered with transparency

    Transparent colors are not rendered with transparency

    Currently, transparent colors like this:

    Color::green().alpha(0.5)
    

    Results in a lighter shade of green. When two widgets are drawn on top of each other, the topmost widget just covers the bottom one with its own color, rather than mixing the two.

    The browser rendering engine currently doesn't have a compositing step, it's a very basic immediate-mode renderer that just naively paints the LayoutBox geometry. I think fixing this would require introducing some more logic around layers.

    opened by harrisonturton 1
  • Browser platform module needs tests

    Browser platform module needs tests

    Currently the crates/platform/browser module doesn't have any tests.

    It should have tests.

    Since testing wasm-bindgen directly seems to be a bit tricky, this issue might involve writing a browser test utils library.

    opened by harrisonturton 0
Owner
Harry
CS at ANU | Backend Engineer @Canva
Harry
SixtyFPS is a toolkit to efficiently develop fluid graphical user interfaces for any display: embedded devices and desktop applications. We support multiple programming languages, such as Rust, C++ or JavaScript.

SixtyFPS is a toolkit to efficiently develop fluid graphical user interfaces for any display: embedded devices and desktop applications. We support multiple programming languages, such as Rust, C++ or JavaScript.

SixtyFPS 5.5k Jan 1, 2023
A react-inspired UI library for building multimedia desktop apps with rust and vulkan.

narui A react-inspired UI library for building multimedia desktop apps with rust and vulkan. declarative UI with Ergonomics similar to React with hook

apertus° - open source cinema 42 Jan 1, 2023
Integrate Qml and Rust by building the QMetaObject at compile time.

QMetaObject crate for Rust The qmetaobject crate is a crate which is used to expose rust object to Qt and QML. Objectives Rust procedural macro (custo

Woboq GmbH 495 Jan 3, 2023
Dear ImGui: Bloat-free Graphical User interface for C++ with minimal dependencies

Dear ImGui (This library is available under a free and permissive license, but needs financial support to sustain its continued improvements. In addit

omar 44.4k Jan 5, 2023
A graphical user interface toolkit for audio plugins.

HexoTK - A graphic user interface toolkit for audio plugins State of Development Super early! Building cargo run --example demo TODO / Features Every

Weird Constructor 14 Oct 20, 2022
Sycamore - A reactive library for creating web apps in Rust and WebAssembly

Sycamore What is Sycamore? Sycamore is a modern VDOM-less web library with fine-grained reactivity. Lightning Speed: Sycamore harnesses the full power

Sycamore 1.8k Jan 5, 2023
Build smaller, faster, and more secure desktop applications with a web frontend.

TAURI Tauri Apps footprint: minuscule performance: ludicrous flexibility: gymnastic security: hardened Current Releases Component Descrip

Tauri 56.3k Jan 3, 2023
Native Maps for Web, Mobile and Desktop

mapr Native Maps for Web, Mobile and Linux A map rendering library written in Rust. Example | Book | API | Chat in Matrix Space Project State This pro

MapLibre 942 Jan 2, 2023
An easy-to-use, 2D GUI library written entirely in Rust.

Conrod An easy-to-use, 2D GUI library written entirely in Rust. Guide What is Conrod? A Brief Summary Screenshots and Videos Feature Overview Availabl

PistonDevelopers 3.3k Jan 1, 2023
Rust bindings for the FLTK GUI library.

fltk-rs Rust bindings for the FLTK Graphical User Interface library. The FLTK crate is a crossplatform lightweight gui library which can be statically

Mohammed Alyousef 1.1k Jan 9, 2023
Idiomatic, GTK+-based, GUI library, inspired by Elm, written in Rust

Relm Asynchronous, GTK+-based, GUI library, inspired by Elm, written in Rust. This library is in beta stage: it has not been thoroughly tested and its

null 2.2k Dec 31, 2022
Clear Coat is a Rust wrapper for the IUP GUI library.

Clear Coat Clear Coat is a Rust wrapper for the IUP GUI library. IUP uses native controls and has Windows and GTK backends. A macOS backend has been o

Jordan Miner 18 Feb 13, 2021
A cross-platform GUI library for Rust, inspired by Elm

Iced A cross-platform GUI library for Rust focused on simplicity and type-safety. Inspired by Elm. Features Simple, easy-to-use, batteries-included AP

Héctor Ramón 17.5k Jan 2, 2023
SwiftUI Inspired UI Library written in rust

Mule (Definitely a Work in Progress) The night I started this project I was on the couch drinking a Moscow Mule.

null 40 Jun 22, 2022
Cross-platform native Rust menu library

A cross-platform Rust library for managing the native operating system menus.

Mads Marquart 16 Jan 6, 2023
A cross-platform GUI library for Rust focused on simplicity and type-safety

A cross-platform GUI library for Rust, inspired by Elm

Héctor Ramón 17.5k Jan 8, 2023
Generic tiling window manager library in Rust

Pop Tiler Generic tiling window manager library for Rust, using an architecture based on GhostCell. License Licensed under the GNU Lesser General Publ

Pop!_OS 65 Dec 29, 2022
Tiny library for handling rust strings in windows.

tinywinstr Tiny library for handling rust strings in windows.

Ed Way 1 Oct 25, 2021
A simple, efficient spring animation library for smooth, natural motion in Rust

Natura A simple, efficient spring animation library for smooth, natural motion in Rust Usage Natura is framework-agnostic and works well in 2D and 3D

O(ʒ) 57 Nov 14, 2022