Azul - Desktop GUI framework

WARNING: The features advertised in this README may not work yet.

Build Status Linux / macOSBuild status WindowsCoverage StatusLICENSERust Compiler Version

Azul is a free, functional, immediate mode GUI framework that is built on the Mozilla WebRender rendering engine for rapid development of desktop applications that are written in Rust and use a CSS / DOM model for layout and styling.

Website | Tutorial / user guide | Video demo | Discord Chat

About

Azul is a library for creating graphical user interfaces or GUIs in Rust. It mixesparadigms from functional, immediate mode GUI programming commonly found in gamesand game engines with an API suitable for developing desktop applications.Instead of focusing on an object-oriented approach to GUI programming ("a buttonis an object"), it focuses on combining objects by composition ("a button is a function")and achieves complex layouts by composing widgets into a larger DOM tree.

Azul separates the concerns of business logic / callbacks, data model and UIrendering / styling by not letting the UI / rendering logic have mutable accessto the application data. Widgets of your user interface are seen as a "view" intoyour applications data, they are not "objects that manage their own state", likein so many other toolkits. Widgets are simply functions that render a certain state,more complex widgets combine buttons by calling a function multiple times.

The generated DOM itself is immutable and gets re-generated every frame. This makes testingand debugging very easy, since the UI is a pure function, mapping from a specific applicationstate into a visual interface. For layouting, Azul features a custom CSS-like layout engine,which closely follows the CSS flexbox model.

Hello World

Here is what a Hello World application in Azul looks like:

Hello World Application

This application is created by the following code:

  1. extern crate azul;
  2.  
  3. use azul::{
  4. prelude::*,
  5. widgets::{button::Button, label::Label},
  6. };
  7.  
  8. struct DataModel {
  9. counter: usize,
  10. }
  11.  
  12. impl Layout for DataModel {
  13. // Model renders View
  14. fn layout(&self, _: LayoutInfo<Self>) -> Dom<Self> {
  15. let label = Label::new(format!("{}", self.counter)).dom();
  16. let button = Button::with_label("Update counter")
  17. .dom()
  18. .with_callback(On::MouseUp, Callback(update_counter));
  19.  
  20. Dom::new(NodeType::Div).with_child(label).with_child(button)
  21. }
  22. }
  23.  
  24. // View updates Model
  25. fn update_counter(
  26. app_state: &mut AppState<DataModel>,
  27. _event: &mut CallbackInfo<DataModel>,
  28. ) -> UpdateScreen {
  29. app_state.data.modify(|state| state.counter += 1);
  30. Redraw
  31. }
  32.  
  33. fn main() {
  34. let mut app = App::new(DataModel { counter: 0 }, AppConfig::default()).unwrap();
  35. let window = app
  36. .create_window(WindowCreateOptions::default(), css::native())
  37. .unwrap();
  38. app.run(window).unwrap();
  39. }

Read more about the Hello-World application …

Programming model

In order to comply with Rust's mutability rules, the application lifecycle in Azulconsists of three states that are called over and over again. The framework determinesexactly when a repaint is necessary, you don't need to worry about manually repaintingyour UI:

Azul callback model

Azul works through composition instead of inheritance - widgets are composed of otherwidgets, instead of inheriting from them (since Rust does not support inheritance).The main layout() function of a production-ready application could look somethinglike this:

  1. impl Layout for DataModel {
  2. fn layout(&self, _info: LayoutInfo<Self>) -> Dom<DataModel> {
  3. match self.state {
  4. LoginScreen => {
  5. Dom::new(NodeType::Div).with_id("login_screen")
  6. .with_child(render_hello_mgs())
  7. .with_child(render_login_with_button())
  8. .with_child(render_password())
  9. .with_child(render_username_field())
  10. },
  11. EmailList(emails) => {
  12. Dom::new(NodeType::Div).with_id("email_list_container")
  13. .with_child(render_task_bar())
  14. .with_child(emails.iter().map(render_email).collect())
  15. .with_child(render_status_bar())
  16. }
  17. }
  18. }
  19. }

One defining feature is that Azul automatically determines when a UI repaint isnecessary and therefore you don't need to worry about manually redrawing your UI.

Read more about the programming model …

Features

Easy two-way data binding

When programming reusable and common UI elements, such as lists, tables or slidersyou don't want the user having to write code to update the UI state of these widgets.Previously, this could only be solved by inheritance, but due to Azul's uniquearchitecture, it is possible to create widgets that update themselves purely bycomposition, for example:

  1. struct DataModel {
  2. text_input: TextInputState,
  3. }
  4.  
  5. impl Layout for DataModel {
  6. fn layout(&self, info: LayoutInfo<Self>) -> Dom<Self> {
  7. // Create a new text input field
  8. TextInput::new()
  9. // ... bind it to self.text_input - will automatically update
  10. .bind(info.window, &self.text_input, &self)
  11. // ... and render it in the UI
  12. .dom(&self.text_input)
  13. .with_callback(On::KeyUp, Callback(print_text_field))
  14. }
  15. }
  16.  
  17. fn print_text_field(app_state: &mut AppState<DataModel>, _event: &mut CallbackInfo<DataModel>) -> UpdateScreen {
  18. println!("You've typed: {}", app_state.data.lock().unwrap().text_input.text);
  19. DontRedraw
  20. }

Read more about two-way data binding …

CSS styling & layout engine

Azul features a CSS-like layout and styling engine that is modeled after theflexbox model - i.e. by default, every element will try to stretch to the dimensionsof its parent. The layout itself is handled by a simple and fast flexbox layout solver.

Read more about CSS styling …

Asynchronous UI programming

Azul features multiple ways of preventing your UI from being blocked, such as"Tasks" (threads that are managed by the Azul runtime) and "Daemons"(callback functions that can be optionally used as timers or timeouts).

Read more about async IO …

SVG / GPU-accelerated 2D Vector drawing

For drawing non-rectangular shapes, such as triangles, circles, polygons or SVG files,Azul provides a GPU-accelerated 2D renderer, featuring lines drawing (incl. bezier curves),rects, circles, arbitrary polygons, text (incl. translation / rotation and text-on-curvepositioning), hit-testing texts, caching and an (optional) SVG parsing module.

Azul SVG Tiger drawing

Read more about SVG drawing …

OpenGL API

While Azul can't help you (yet) with 3D content, it does provide easy ways to hookinto the OpenGL context of the running application - you can draw everything youwant to an OpenGL texture, which will then be composited into the frame usingWebRender.

Read more about OpenGL drawing …

UI Testing

Due to the separation of the UI, the data model and the callbacks, Azul applicationsare very easy to test:

  1. #[test]
  2. fn test_it_should_increase_the_counter() {
  3. let mut initial_state = AppState::new(DataModel { counter: 0 });
  4. let expected_state = AppState::new(DataModel { counter: 1 });
  5. update_counter(&mut initial_state, &mut CallbackInfo::mock());
  6. assert_eq!(initial_state, expected_state);
  7. }

Read more about testing …

Performance

A default window, with no fonts or images added takes up roughly 23MB of RAM and5MB in binary size. This usage can go up once you load more images and fonts, sinceAzul has to load and keep the images in RAM.

The frame time (i.e. the time necessary to draw a single frame, including layout)lies between 2 - 5 milliseconds, which equals roughly 200 - 500 frames per second.However, Azul limits this frame time and only redraws the window when absolutelynecessary, in order to not waste the users battery life.

The startup time depends on how many fonts / images you add on startup, thedefault time is between 100 and 200 ms for an app with no images and a single font.

While Azul can run in software rendering mode (automatically switching to thebuilt-in OSMesa), it isn't intended to run on microcontrollers or devices withextremely low memory requirements.

Thanks

Several projects have helped severely during the development and should be credited:

  • Chris Tollidays limn framework has helpeda lot with discovering undocumented parts of WebRender.
  • Nicolas Silva for his work on lyon - without this,the SVG renderer wouldn't have been possible

License

This library is MIT-licensed. It was developed by Maps4Print,for quickly prototyping and producing desktop GUI cross-platform applications,such as vector or photo editors.

For licensing questions, please contact opensource@maps4print.com