Foreword

Welcome to the Vizia book!

Vizia is a crate for building GUI applications. It presently supports Windows, Linux, and MacOS desktop targets, as well as audio plugins and Web.

The Vizia project's home is https://github.com/vizia and the main Vizia repository is https://github.com/vizia/vizia.

This book aims to act as a guide and reference for Vizia, and hopes to teach you how to use Vizia, whether you've done any GUI programming in the past or not.

Getting Started

Installing Rust

The Vizia framework is built using the Rust programming language. Currently, to use Vizia, you will need to install the Rust compiler by following the instructions over at https://www.rust-lang.org/learn/get-started.

Running the Examples

The Vizia repository on github contains a number of example applications. To run these examples, first clone the repository to a local directory, then with your terminal of choice, navigate to this directory and run the following command:

cargo run --example name_of_example

Where name_of_example should be replaced with the example name.

Join the Community

For help with vizia, or to get involved with contributing to the project, come join us on our Discord server.

Overview

In this quick start guide we'll build a very simple counter application consisting of two buttons, one for incrementing the counter and one for decrementing, and a label showing the counter value.

This guide will introduce the reader to the basics of vizia, including setting up an application, composing and modifying views, layout, styling, reactivity, localization and accessibility. The final application will look like the following:

Image showing a finished counter vizia application.

Reactive UI

Vizia is a reactive UI framework. This means that visual elements which represent some state of the application will update when this state changes. Interacting with these visual elements causes the application state to change.

A reactive UI then is a feedback loop of application state change and visual element updates.

In Vizia, this pattern can be broken down into four concepts:

  1. Models - Data representing the state of an application.
  2. Views - The visual elements which present the application state as a graphical user interface.
  3. Binding - The link between model data and views which causes them to update when the data changes.
  4. Events - Messages which views send to models to trigger changes to the data.

Setting Up

Creating a new project

First, let's create a new Rust project using the following command:

cargo new --bin hello_vizia

This will generate a hello_vizia directory with the following contents:

.
├── Cargo.toml
├── .git
├── .gitignore
└── src
    └── main.rs

Adding Vizia as a dependency

Open the Cargo.toml file and add the following to the dependencies:

[package]
name = "hello_vizia"
version = "0.1.0"
edition = "2021"

[dependencies]
vizia = {git = "https://github.com/vizia/vizia"}

Creating an Application

The first step to building a GUI with vizia is to create an application. Creating a new application creates a root window and a context. Views declared within the closure passed to Application::new() are added to the context and rendered into the root window.

Add the following code to the main.rs file, replacing the hello world code that was generated for us:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{
        // Content goes here
    })
    .run();    
}

The run() method on the Application causes the program to enter the event loop and for the main window to display.

We can run our application with cargo run in the terminal, which should result in the following:

An empty vizia application window

Modifying the Window

When creating an Application the properties of the window can be changed using window modifiers. These modifiers are methods called on the application prior to calling run().

For example, the title() and inner_size() window modifiers can be used to set the title and size of the window respectively.

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

An empty vizia application window with a custom title and inner size

Adding Views

Views are the building bocks of a vizia GUI and are used to visually present model data and to act as controls which, when interacted with, send events to mutate model data.

We'll learn more about models and events in the following sections.

Adding a label

We can declare a Label view with the following code:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{
        Label::new(cx, "Hello Vizia");
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

The first argument to the new() method of the label is a mutable reference to Context, shortened to cx. This allows the view to build itself into the application. For the second argument we pass it a string to display.

A vizia application window with a label view reading 'Hello Vizia'

Modifying Views

Modifiers are used to customize the appearance and behavior of views in a declarative way. Many of the built-in modifiers in Vizia can be applied to any View, which includes built-in views as well as user-defined views.

Customizing the label with a modifier

Applying modifiers to a view changes the properties of a view without rebuilding it. For example, we can use the background_color() modifier to set the background color of the label view:

Label::new(cx, "Hello Vizia")
    .background_color(Color::rgb(200, 200, 200));

Note how this overrides the default background color of the label, which is provided by a CSS stylesheet.

Multiple modifiers can be chained together to achieve more complex view configuration. Because modifiers are just changing the properties of a view, the order of modifiers is not important.

Label::new(cx, "Hello Vizia")
    .width(Pixels(200.0))
    .border_width(Pixels(1.0))
    .border_color(Color::black())
    .background_color(Color::rgb(200, 200, 200));

View specific modifiers

Some views have modifiers which are specific to that view type. For example, the Slider view has a modifier for setting the slider range:

Slider::new(cx, AppData::value)
    .range(0.0..100.0);

View specific modifiers can still be combined with regular modifiers, and the order doesn't matter. Both of these produce the same result:

Slider::new(cx, AppData::value)
    .range(0.0..100.0)
    .width(Pixels(200.0));
Slider::new(cx, AppData::value)
    .width(Pixels(200.0))
    .range(0.0..100.0);

Composing Views

Composition of views is achieved through container views. These views take a closure which allows us to build child views within them. Some container views may also arrange their content in a particular way.

For example, the HStack container view will arrange its contents into a horizontal row. Let's use this to declare the rest of the views for our counter application:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"));
            Button::new(cx, |cx| Label::new(cx, "Increment"));
            Label::new(cx, "0");
        });
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

For now we have hard-coded the label to display the number 0, but later we will hook this up to some model data so that it updates when the data changes. We've also removed the modifiers from the label, as we'll be replacing these with CSS styling later on.

Note that the Button view is also a container, and is designed to allow things like a button with both text and a leading or trailing icon.

A vizia app showing two buttons and a label

Composing views together forms a tree, where each view has a single parent and zero or more children. For example, for the code above the view tree can be depicted with the following diagram:

Diagram of the vizia application view tree

Customizing the Layout

So far we have a horizontal row of buttons and a label, but they're positioned in the top left corner. Let's use layout modifiers to position the views in the center of the window with some space between them.

Centering the views

By default the HStack view will stretch to fill its parent, in this case the window. We can center the contents of the HStack by applying stretch space around all of the children, which we can do using the child_space() modifier and setting it to Stretch(1.0). Then we can add horizontal space between the children using the col_between() modifier:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"));
            Button::new(cx, |cx| Label::new(cx, "Increment"));
            Label::new(cx, "0");
        })
        .child_space(Stretch(1.0))
        .col_between(Pixels(20.0));
    })
    .inner_size((400, 100))
    .run();
}
A vizia app showing two buttons and a label

Understanding the layout system

The layout system used by vizia is called morphorm and can achieve results similar to flexbox on the web but with fewer concepts to learn. With morphorm there is only space and size. Vizia determines the position and size of views based on a number of layout properties which can be configured. A detailed guide of the layout system can be found here.

Styling the Application

Previously we saw how modifiers can be used to style views inline. However, vizia also allows views to be styled with Cascading Style Sheets (CSS) so that style rules can be shared by multiple views. Additionally, stylesheets can be reloaded at runtime by pressing the F5 key.

Adding class names to the views

First we'll add some class names to our views, using the class style modifier, so we can target them with a CSS stylesheet:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"))
                .class("dec");
            Button::new(cx, |cx| Label::new(cx, "Increment"))
                .class("inc");
            Label::new(cx, "0")
                .class("count");
        })
        .class("row");
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

Creating a stylesheet

Next, we'll create a style.css file in the src directory with the following CSS code:

.row {
    child-space: 1s;
    col-between: 20px;
}

button {
    border-width: 0px;
}

button.dec {
    background-color: rgb(170, 50, 50);
}

button.inc {
    background-color: rgb(50, 170, 50);
}

label.count {
    child-space: 1s;
    border-width: 1px;
    border-color: #808080;
    border-radius: 4px;
    width: 50px;
    height: 32px;
}

Adding the stylesheet to the app

Finally, we'll add the CSS file to the vizia application using the .add_stylesheet() function on the context. Here we're using the include_style!() macro, which will dynamically load the stylesheet at runtime in debug mode, but include the stylesheet into the binary in release mode. This should be done just after creating the application:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

        // Add the stylesheet to the app
        cx.add_stylesheet(include_style!("src/style.css"))
            .expect("Failed to load stylesheet");

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"))
                .class("dec");
            Button::new(cx, |cx| Label::new(cx, "Increment"))
                .class("inc");
            Label::new(cx, "0")
                .class("count");
        })
        .class("row");
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

If we run the app now with cargo run we get the following:

A vizia app showing two buttons and a label

Animating Styles with Transitions

Many of the style and layout properties in vizia can be animated. The simplest way to animate style properties is through CSS transitions.

Transitions are animations for style rule properties which apply when a view matches that rule. Transitions are specified with the transition CSS property, and you must specify the property to animate and the duration of the animation. Optionally you can also specify any delay on the animation, as well as the timing function used.

The default styling for some of the built-in views already has some of these transition. For example, if you hover a button you'll see its background color animate to a slightly lighter color.

Declaring a transition

For example, we can create a transition for the background color of a view when hovered:

use vizia::prelude::*;

const STYLE: &str = r#"
    .my_view {
        background-color: red;
    }

    .my_view:hover {
        background-color: blue;
        transition: background-color 100ms;
    }
"#;

fn main () {
    Application::new(|cx|{

        cx.add_stylesheet(STYLE);

        Element::new(cx)
            .class("my_view")
            .size(Pixels(200.0));
    })
    .run();
}

Note here that we have not used the include_style!() macro within the call to cx.add_stylesheet as the stylesheet is defined as a constant within the Rust code.

// GIF here

Note that the transition only occurs when the cursor hovers the element and not when the cursor leaves the element (unless the transition did not complete when the cursor left). This is because the transition has been specified on the :hover state of the element, and so the background color will transition when going to this state.

To transition back again, we need to specify a transition on the non-hover state as well:

.my_view {
    background-color: red;
    transition: background-color 100ms;
}

.my_view:hover {
    background-color: blue;
    transition: background-color 100ms;
}

// GIF here

Model Data

So far we've created the views for our counter application but we haven't declared the application data with the count value we want to display and modify.

Application data in Vizia is stored in models. Views can then bind to the data in these models in order to react to changes in the data.

Declaring a model

A model definition can be any type, typically a struct, which implements the Model trait:

pub struct AppData {
    pub count: i32,
}

impl Model for AppData {}

Building the model into the tree

To use a model, an instance of the data must be built into the application with the build() method:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{
        AppData { count: 0 }.build(cx); // Build the data into the app

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"))
                .class("dec");
            Button::new(cx, |cx| Label::new(cx, "Increment"))
                .class("inc");
            Label::new(cx, "0")
                .class("count");
        })
        .class("row");

    })
    .title("Counter")
    .inner_size((400, 200))
    .run();
}

This builds the model data into the tree, in this case at the root Window. Internally, models and views are stored separately, however, for processes like event propagation, models can be thought of as existing within the tree, with an associated view.

Therefore, the model-view tree for the above code can be depicted with the following diagram:

Diagram of a basic model-view tree depicting a Window view, with an associated AppData model, as well as a child HStack view with two child Button views and a Label view

If the AppData had been built within the contents of the HStack, then the model would be associated with the HStack rather than the Window.

Lenses and Data Binding

Now that we have some model data we can bind the count to the Label view.

Data binding is the concept of linking model data to views, so that when the model data is changed, the views observing this data update in response.

In Vizia, data binding is achieved through the use of lenses. A lens is an object which allows you to select some part of a model and inspect its value. These lens objects are then used to form a binding between views and these parts of the model, updating when only these specific parts have changed.

Generating lenses

The Lens derive macro can be used to generate a lens for each field of a struct. These lenses can then be used to transform a reference to the struct into a reference to each of its fields. The generated lenses are given the same name as the field and placed in a module with the same name as the struct. For example, deriving Lens on the model we defined before:

#[derive(Lens)]
pub struct AppData {
    count: i32,
}

impl Model for AppData {}

A lens to the count field of the AppData struct is generated as AppData::count.

Binding the label

With the generated AppData::count lens, we can bind the count data to the Label by passing the lens in place of the string:

use vizia::prelude::*;

fn main() {
    Application::new(|cx|{

        AppData { count: 0 }.build(cx);

        HStack::new(cx, |cx|{
            Button::new(cx, |cx| Label::new(cx, "Decrement"))
                .class("dec");
            Button::new(cx, |cx| Label::new(cx, "Increment"))
                .class("inc");
            Label::new(cx, AppData::count) // Bind the label to the count data
                .class("count");
        })
        .class("row");
    })
    .title("Counter")
    .inner_size((400, 100))
    .run();
}

This sets up a binding which updates the value of the label whenever the count changes. We can depict this with the following diagram, where the green arrow shows the direct link between the data and the label:

Diagram of model-view-tree with an arrow from 'AppData' to 'Label' representing data binding

Modifier bindings

Many modifiers also accept a lens as well as a value. When a lens is supplied to a modifier, a binding is set up which will update the modified property when the bound to model data changes. For example:


#[derive(Lens)]
pub struct AppData {
    color: Color,
}

...

Label::new(cx, "Hello World")
    .background_color(AppData::color);

Mutating State with Events

The label is now bound to the data so that it updates when the count changes, so now we need to hook up the buttons to change the count.

Vizia uses events to communicate actions to update model or view data. These events propagate through the tree, typically from the view which emits the event, up through the ancestors of the view, to the main window and through any models on the way.

Declaring events

An event contains a message which can be any type, but is typically an enum. We'll declare an event enum with two variants, one for incrementing the count and one for decrementing:

pub enum AppEvent {
    Increment,
    Decrement,
}

Emitting events

Events are usually emitted in response to some action on a view. For this we can the on_press callback provided by the Button. When the button is pressed this callback is called. We can use the provided EventContext to emit our events up the tree:

Button::new(cx, |cx| Label::new(cx, "Decrement"))
    .on_press(|ex| ex.emit(AppEvent::Decrement))
    .class("dec");
Button::new(cx, |cx| Label::new(cx, "Increment"))
    .on_press(|ex| ex.emit(AppEvent::Increment))
    .class("inc");

The flow of events from the buttons, up through the visual tree, to AppData model can be described with the following diagram, where the red arrows indicate the direction of event propagation:

Diagram of event propagation

Handling events

Events are handled by views and models with the event() method of the View or Model traits. Let's fill in the Model implementation by implementing the event method:

impl Model for AppData {
      fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|app_event, meta| match app_event {
            AppEvent::Decrement => self.count -= 1,
            AppEvent::Increment => self.count += 1,
        });
    }
}

Calling map() on an event attempts to cast the event message to the specified type and calls the provided closure if it succeeds.

The closure provides the message type and an EventMeta, which can be used to query the origin and target views of the event, or to consume the event to prevent it propagating further.

If we run the application now we can see that the buttons cause the state to mutate, which then causes the label to update.

Making the Counter Reusable

In this section we're going to turn our counter into a component by declaring a custom view. This will make our counter reusable so we can easily create multiple instances or export the counter as a component in a library.

Step 1: Creating a custom view struct

First we declare a struct which will contain any view-specific state:

pub struct Counter {}

Although we could store the count value within the view, we've chosen instead to make this view 'stateless', and instead we'll provide it with a lens to bind to some external state (typically from a model), and some callbacks for emitting events when the buttons are pressed.

Step 2: Implementing the view trait

Next, we'll implement the View trait for the custom counter view:

impl View for Counter {}

The View trait has methods for responding to events and for custom drawing, but for now we'll leave this implementation empty.

Step 3: Building the sub-components of the view

Next we'll implement a constructor for the counter view. To use our view in a vizia application, the constructor must build the view into the context, which returns a Handle we can use to apply modifiers on our view.

impl Counter {
    pub fn new(cx: &mut Context) -> Handle<Self> {
        Self {

        }.build(cx, |cx|{

        })
    }
}

The build() function, provided by the View trait, takes a closure which we can use to construct the content of the custom view. Here we move the code which makes up the counter:

impl Counter {
    pub fn new(cx: &mut Context) -> Handle<Self> {
        Self {

        }.build(cx, |cx|{
            HStack::new(cx, |cx|{
                Button::new(cx, |cx| Label::new(cx, "Decrement"))
                    .on_press(|ex| ex.emit(AppEvent::Decrement))
                    .class("dec");

                Button::new(cx, |cx| Label::new(cx, "Increment"))
                    .on_press(|ex| ex.emit(AppEvent::Increment))
                    .class("inc");
                
                Label::new(cx, AppData::count)
                    .class("count");
            })
            .child_space(Stretch(1.0))
            .col_between(Pixels(20.0));
        })
    }
}

Step 4: User-configurable binding

The label within the counter is currently using the AppData::count lens, however, this will only work if that specific lens is in scope. To make this component truly reusable we need to pass a lens in via the constructor. To do this we use a generic and constrain the type to implement the Lens trait. This trait has a Target associated type which we can use to specify that the binding is for an i32 value. Then we simply pass the lens directly to the constructor of the label:

impl Counter {
    pub fn new<L>(cx: &mut Context, lens: L) -> Handle<Self> 
    where
        L: Lens<Target = i32>,
    {
        Self {

        }.build(cx, |cx|{
            HStack::new(cx, |cx|{
                Button::new(cx, |cx| Label::new(cx, "Decrement"))
                    .on_press(|ex| ex.emit(AppEvent::Decrement))
                    .class("dec");

                Button::new(cx, |cx| Label::new(cx, "Increment"))
                    .on_press(|ex| ex.emit(AppEvent::Increment))
                    .class("inc");
                
                Label::new(cx, lens)
                    .class("count");
            })
            .child_space(Stretch(1.0))
            .col_between(Pixels(20.0));
        })
    }
}

Step 5 - User-configurable events

The last part required to make the counter truly reusable is to remove the dependency on AppEvent. To do this we'll add a couple of callbacks to the counter to allow the user to emit their own events when the buttons are presses.

Adding callbacks to the view

First, change the Counter struct to look like this:

pub struct Counter {
    on_increment: Option<Box<dyn Fn(&mut EventContext)>>,
    on_decrement: Option<Box<dyn Fn(&mut EventContext)>>,
}

These boxed function pointers provide the callbacks that will be called when the increment and decrement buttons are pressed.

Custom modifiers

Next we'll need to add some custom modifiers so the user can configure these callbacks. To do this we can define a trait and implement it on Handle<'_, Counter>:

pub trait CounterModifiers {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
}

We can use the modify() method on Handle to directly set the callbacks when implementing the modifiers:

impl<'a> CounterModifiers for Handle<'a, Counter> {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_increment = Some(Box::new(callback)))
    }

    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_decrement = Some(Box::new(callback)))
    }
}

Internal event handling

Unfortunately we can't just call these callbacks from the action callback of the buttons. Instead we'll need to emit some internal events which the counter can receive, and then the counter can call the callbacks. Define an internal event enum for the counter like so:

pub enum CounterEvent {
    Decrement,
    Increment,
}

We can then use this internal event with the buttons:

Button::new(
    cx, 
    |ex| ex.emit(CounterEvent::Decrement), 
    |cx| Label::new(cx, "Decrement")
)
.class("dec");

Button::new(
    cx, 
    |ex| ex.emit(CounterEvent::Increment), 
    |cx| Label::new(cx, "Increment")
)
.class("inc");

Finally, we respond to these events in the event() method of the View trait for the Counter, calling the appropriate callback:

impl View for Counter {
    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|counter_event, meta| match counter_event{
            CounterEvent::Increment => {
                if let Some(callback) = &self.on_increment {
                    (callback)(cx);
                }
            }

            CounterEvent::Decrement => {
                if let Some(callback) = &self.on_decrement {
                    (callback)(cx);
                }
            }
        });
    }
}

To recap, now when the user presses on one of the buttons, the button will emit an internal CounterEvent, which is then handled by the Counter view to call the appropriate callback, which the user can set using the custom modifiers we added using the CounterModifiers trait.

Step 6: Using the custom view

Finally, we can use our custom view in the application:

fn main() {
    Application::new(|cx|{

        cx.add_stylesheet(include_style!("src/style.css"))
            .expect("Failed to load stylesheet");

        AppData { count: 0 }.build(cx);

        Counter::new(cx, AppData::lens)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

We pass it the AppData::lens, but the custom view can accept any lens to an i32 value. We also provide it with callbacks that should trigger when the increment and decrement buttons are pressed. In this case the callbacks will emit AppEvent events to mutate the model data.

When we run our app now it will seem like nothing has changed. However, now that our counter is a component, we could easily add multiple counters all bound to the same data (or different data):

fn main() {
    Application::new(|cx|{

        cx.add_stylesheet(include_style!("src/style.css"))
            .expect("Failed to load stylesheet");

        AppData { count: 0 }.build(cx);

        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

Vizia app with three counter components

Localizing the Application

An important part of building a GUI is making sure the application is usable for different regions around the world. Vizia uses fluent to provide translatable text for an application.

Creating fluent files

Fluent files provide a key-value store for translated text strings which vizia uses to localize text in an application.

Let's add two fluent (.ftl) files to our application. We'll call them the same name, counter.ftl, but place them within separate directories, en-Us and es, within a resources directory.

Your project folder structure should now look like this:

.
├── Cargo.toml
├── .git
├── .gitignore
└── src
    ├── resources
    │   ├── en-US
    │   │   └── counter.ftl
    │   └── es
    │       └── counter.ftl
    ├── main.rs
    └── style.css

resources/en-US/counter.ftl

inc = Increment
dec = Decrement

resources/es/counter.ftl

inc = Incrementar
dec = Decrementar

Adding translations to the application

cx.add_translation(
    langid!("en-US"),
    include_str!("resources/en-US/counter.ftl").to_owned(),
);

cx.add_translation(
    langid!("es"),
    include_str!("resources/es/counter.ftl").to_owned(),
);

Localizing text

To localize the text in our application we use the Localized type within the labels of the buttons, passing the translation keys to the constructor:

Button::new(cx, |cx| Label::new(cx, Localized::new("dec")));

Button::new(cx, |cx| Label::new(cx, Localized::new("inc")));

When the application is run these Localized objects are replaced with the translated strings from the fluent files based on the system locale.

Testing localization

The locale used for selecting translations is stored in a model called the Environment. By default the locale used for translations is set to the system locale, however, we can use an EnvironmentEvent to set the locale to a user-specified value. This is useful for testing the localization of an application.

cx.emit(EnvironmentEvent::SetLocale(langid!("es")));

If we run our app now we'll see that the text has been translated into Spanish. Because the buttons are set up to hug their content, the widths of the buttons have automatically updated to accommodate the slightly longer text strings.

A counter application translated into spanish

Note that if you're following this tutorial on a machine where the system locale is already set to Spanish then you'll see the Spanish translations without needing to emit the SetLocale event. To see the English versions of the text replace the "es" with "en-US" when emitting the event.

Making the Application Accessible

Making the application accessibility is about making it so that assistive technologies, such as a screen reader, can navigate and query the application.

Our application so far is actually already mostly accessible as the built-in views, such as the buttons, are already set up to be accessible. However, even though the built-in views are accessible, this does not mean the app is automatically accessible.

For the case of our counter, when the increment or decrement buttons are pressed, causing the count to change, a screen reader does not know to speak the current count to inform the user of the change. To account for this we can use something called a 'live region'.

A live region is a view which has changing content but is not itself interactive. In the counter application a label shows the current counter value. This label is not itself interactive but has content which changes, and so should be marked as a live region. This will cause, for example, a screen reader to announce the value when the count changes.

A view can be marked as a live region with the live() modifier:

Label::new(cx, AppData::count)
    .class("count")
    .live(Live::Assertive);

If we were to use our counter application with a screen reader enabled now, the count value would be spoken when either of the buttons are pressed.

The Final Code

Rust

use vizia::prelude::*;

// Define the application data
#[derive(Lens)]
pub struct AppData {
    count: i32,
}

// Define events for mutating the application data
pub enum AppEvent {
    Increment,
    Decrement,
}

// Mutate application data in response to events
impl Model for AppData {
    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|app_event, meta| match app_event {
            AppEvent::Decrement => self.count -= 1,
            AppEvent::Increment => self.count += 1,
        });
    }
}

// Define a custom view for the counter
pub struct Counter {
    on_increment: Option<Box<dyn Fn(&mut EventContext)>>,
    on_decrement: Option<Box<dyn Fn(&mut EventContext)>>,
}

impl Counter {
    pub fn new<L>(cx: &mut Context, lens: L) -> Handle<Self>
    where
        L: Lens<Target = i32>
    {
        Self {
            on_decrement: None,
            on_increment: None,
        }.build(cx, |cx|{
            HStack::new(cx, |cx|{
                Button::new(cx, |cx| Label::new(cx, Localized::new("dec")))
                    .on_press(|ex| ex.emit(CounterEvent::Decrement))
                    .class("dec");

                Button::new(cx, |cx| Label::new(cx, Localized::new("inc")))
                    .on_press(|ex| ex.emit(CounterEvent::Increment))
                    .class("inc");
                
                Label::new(cx, AppData::count)
                    .class("count")
                    .live(Live::Assertive);
            })
            .class("row");
        })
    }
}

// Internal events
pub enum CounterEvent {
    Decrement,
    Increment,
}

// Handle internal events
impl View for Counter {
    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|counter_event, meta| match counter_event{
            CounterEvent::Increment => {
                if let Some(callback) = &self.on_increment {
                    (callback)(cx);
                }
            }

            CounterEvent::Decrement => {
                if let Some(callback) = &self.on_decrement {
                    (callback)(cx);
                }
            }
        });
    }
}

// Custom modifiers
pub trait CounterModifiers {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
}

// Implement custom modifiers
impl<'a> CounterModifiers for Handle<'a, Counter> {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_increment = Some(Box::new(callback)))
    }

    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_decrement = Some(Box::new(callback)))
    }
}

fn main() {
    Application::new(|cx|{

        cx.add_stylesheet(include_style!("src/style.css")).expect("Failed to load stylesheet");

        cx.add_translation(
            langid!("en-US"),
            include_str!("resources/en-US/counter.ftl").to_owned(),
        );

        cx.add_translation(
            langid!("es"),
            include_str!("resources/es/counter.ftl").to_owned(),
        );

        // Uncomment to test with spanish locale.
        // If system locale is already Spanish, replace "es" with "en-US".
        // cx.emit(EnvironmentEvent::SetLocale(langid!("es")));

        // Build model data into the application
        AppData { count: 0 }.build(cx);

        // Add the custom counter view and bind to the model data
        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

CSS

.row {
    child-space: 1s;
    col-between: 20px;
}

button {
    border-width: 0px;
}

button.dec {
    background-color: rgb(170, 50, 50);
}

button.inc {
    background-color: rgb(50, 170, 50);
}

label.count {
    child-space: 1s;
    border-width: 1px;
    border-color: #808080;
    border-radius: 4px;
    width: 50px;
    height: 32px;
}

Fluent

resources/en-US/counter.ftl

inc = Increment
dec = Decrement

resources/es/counter.ftl

inc = Incrementar
dec = Decrementar

Data Binding

Data binding is the concept of linking model data to views, so that when the model data is changed, the views observing this data update automatically in response. Therefore, it is data binding which provides the mechanism for reactivity in Vizia.

In Vizia, data binding is achieved through the use of lenses. A lens is an object which allows you to select some part of a model and inspect its value. These lens objects are then used to form a binding between views and these parts of the model, updating when only these specific parts have changed.

Lenses

The Lens derive macro can be used to generate a lens for each field of a struct. These lenses can then be used to transform a reference to the struct into a reference to each of its fields. The generated lenses are given the same name as the field and placed in a module with the same name as the struct. For example, given the following definition of some model data:

#[derive(Lens)]
pub struct AppData {
    color: Color,
}

impl Model for AppData {}

A lens to the color field of the AppData struct is generated as AppData::color.

Property Binding

We can then use this lens with the background_color modifier of a view to set up a binding, so that when the data changes the background color is updated. Passing lenses to modifiers is known as property binding.

#[derive(Lens)]
pub struct AppData {
    color: Color,
}

impl Model for AppData {}

fn main() {
    Application::new(|cx|{
        Label::new(cx, "Hello Vizia").background_color(AppData::color);
    }).run();
}

View Binding

Some views accept a lens as an input. When provided a lens, the view sets up a binding to the data. For example, the Label view accepts a lens to any type which implements ToString:

#[derive(Lens)]
pub struct Person {
    pub name: String,
}

impl Model for Person {}

Application::new(|cx|{
    Label::new(cx, Person::name);
})
.run()

When the name field changes, the text of the label updates to show the new value.

The Data Trait

When views bind to model data the binding system must determine whether the data has changed. To do this it stores a copy of the data for each binding. To be able to compare the previous data to the new version, the data type must implement the Data trait.

Note that this is only required for the data types which are bound to. The model itself does not need to implement Data unless a view is to bind to the entire model and not just a field within.

Most simple types already implement the Data trait, but for custom types there is a derive macro that can be used as long as the field types also implement the Data trait:

#[derive(Clone, Data)]
pub struct CustomData {
    text: String,
} 

The Data trait also requires that the type implements Clone. This is because when the binding system has determined the data has changed, it must replace its stored copy of the data with a new one so it can recompare on the next update cycle.

Lens Map

The map() method on a lens can be used to derive data from the target of the lens. This is useful for when the lens target is not the right type for the binding, but a value of the correct type can be derived from it.

For example, let's say we have some string data representing a name in our model, but we only want to display the first letter within a label:

use vizia::prelude::*;

#[derive(Lens)]
pub struct AppData {
    pub name: String,
}

impl Model for AppData {}

fn main() {
    Application::new(|cx|{

        AppData {
            name: String::from("John Doe"),
        }.build(cx);

        Label::new(cx, AppData::name.map(|name| name.chars().nth(0).unwrap()));
    })
    .inner_size((400, 100))
    .run();
}

Note that in this example we're assuming that the string is not empty.

Now when the name field of the model changes the label will update to display the new first letter.

Conditional Views

The Binding view provides a way to explicitely control which parts of the view tree get rebuilt when some state changes, specifically anything within the content closure of a Binding view.

Because of this, a regular if statement can be used to conditionally rebuild views. In the following example, a label view is built into the tree when a boolean state is true, else the view is removed from the tree.

use vizia::prelude::*;

#[derive(Lens)]
struct AppData {
    show_view: bool,
}

enum AppEvent {
    ToggleShowView,
}

impl Model for AppData {
    fn event(&mut self, cx: &mut Context, event: &mut Event) {
        event.map(|app_event, _| match app_event {
            AppEvent::ToggleShowView => self.show_view ^= true,
        });
    }
}

fn main() {
    Application::new(|cx|{

        AppData {
            show_view: false,
        }.build(cx);

        Label::new(cx, "Show View")
            .on_press(|cx| cx.emit(AppEvent::ToggleShowView));
        
        Binding::new(cx, AppData::show_view, |cx, show|{
            if show.get(cx) {
                Label::new(cx, "Surprise!");
            }
        });
    })
    .inner_size((400, 100))
    .run();
}

Binding Nested Data

Let's say we have the following application data structure:

#[derive(Lens)]
pub struct AppData {
    nested: NestedData,
}

#[derive(Lens)]
pub struct NestedData {
    name: String,
}

Provided that both the parent and nested structures derive the Lens trait, we can use the then() lens modifier to produce a lens which targets the nested data:

fn main() {
    Application::new(|cx|{

        AppData {
            nested: NestedData {
                name: String::from("John Doe"),
            },
        }.build(cx);

        Label::new(cx, AppData::nested.then(NestedData::name));
    })
    .inner_size((400, 100))
    .run();
}

If the nested data structure does not dderive Lens, then the map_ref() modifier can be used:

fn main() {
    Application::new(|cx|{

        AppData {
            nested: NestedData {
                name: String::from("John Doe"),
            },
        }.build(cx);

        Label::new(cx, AppData::nested.map_ref(|nested| &nested.name));
    })
    .inner_size((400, 100))
    .run();
}