Foreword

Welcome to the Vizia book!

Vizia is a crate for building desktop GUI applications. It presently supports Windows, Linux, and MacOS desktop targets.

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() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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.

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() -> Result<(), ApplicationError> {
    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 using the alignment() modifier and setting it to Alignment::Center. Then we can add horizontal space between the children using the gap() modifier:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    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");
        })
        .alignment(Alignment::Center)
        .gap(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. 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() -> Result<(), ApplicationError> {
    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 {
    alignment: center;
    gap: 20px;
}

button {
    border-width: 0px;
}

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

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

label.count {
    alignment: center;
    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() -> Result<(), ApplicationError> {
    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 () -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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");
            })
            .class("row");
        })
    }
}

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");
            })
            .class("row");
        })
    }
}

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() -> Result<(), ApplicationError> {
    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, lens)
                    .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() -> Result<(), ApplicationError> {
    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

Creating an Application

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

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx|{
        // Content goes here
    })
    .run()   
}

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

An empty vizia application main window

Multiple Windows

While an application provides a default main window, additional windows can be created with the Window view:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx|{
        // Main window content

        Window::new(cx, |cx| {
            // Secondary window content
        });
    })
    .run()
}

Windows, like other views, are built into the view tree. Therefore, they can access data in models further up the tree from them, and if the containing view is destroyed the window is closed.

A binding view can be used to a conditionally create windows:

Binding::new(cx, AppData::show_window, |cx, show_subwindow| {
    if show_subwindow.get(cx) {
        Window::new(cx, |cx| {
            
        })
        .on_close(|cx| {
            cx.emit(AppEvent::WindowClosed);
        });
    }
});

Here we've used the on_close window modifier to reset the app state when the window is closed by the user.

Modifying Window Properties

The properties of a window can be changed using window modifiers. For the main window, these modifiers are 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() -> Result<(), ApplicationError> {
    Application::new(|cx|{

    })
    .title("My Awesome Application")
    .inner_size((400, 200))
    .run()
}

A window with the title 'My Awesome Application'

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.

Declaring views

For example, we can declare a Label view to display a text string:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx|{
        Label::new(cx, "Hello World");
    })
    .inner_size((400, 200))
    .run()
}

A label view

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 and is passed from view to view.

Composing views

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

For example, the VStack container view will arrange its contents into a vertical column:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx|{
        
        VStack::new(cx, |cx|{    
            Label::new(cx, "Hello");
            Label::new(cx, "World");
        });
    })
    .inner_size((400, 100))
    .run()
}

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 a basic view tree depicting a Window view with a child HStack view with two child Label views.

The Window is the parent of the VStack, while the VStack is the parent of both the Labels. Therefore, the Window is an ancestor of the Labels and the Labels are descendants of the window. This terminology is relevant when writing CSS style rules, which we'll cover later in this guide.

Mofiying Views

Modifiers are used to customize the appearance and behaviour 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.

Customize a view with a modifier

Modifiers are functions which are called on a Handle, which is returned by the constructor of all views. 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 a label view:

Label::new(cx, "Hello World")
    .background_color(Color::rgb(255, 0, 0));

A label with a background color modifier

Multiple modifiers can be chained together to acheieve more complex view configuration.

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

A label with a multiple modifiers applied

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);

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);

Custom View Modifiers

To create a set of custom view modifiers, first declare a trait with the desired modifier functions. The modifier functions must take self by value and return Self.

pub trait CustomModifiers:  {
    fn title(self) -> Self;
}

Next, we can implement the custom modifiers for all views like so:

impl<'a, V: View> CustomModifiers for Handle<'a, V> {
    fn title(self) -> Self {
        self.font_size(24.0).font_weight(FontWeightKeyword::Bold)
    }
}

Sometimes it may be more appropriate to implement the custom modifiers for specific views. For example, we can implement the custom modifiers just the Label view like so:

impl<'a> CustomModifiers for Handle<'a, Label> {
    fn title(self) -> Self {
        self.font_size(24.0).font_weight(FontWeightKeyword::Bold)
    }
}

As long as CustomModifiers is imported we can then use the custom title() modifier like any other modifier on a label:

Label::new("Some Kind of Title").title();

A label with a custom modifier

Models

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 Models

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

pub struct Person {
    pub name: String,
    pub email: String,
}

impl Model for Person {}

Building Models

A model definition can be built into the view tree with the build() method:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx|{
        Person {
            name: String::from("John Doe"),
            email: String::from("john.doe@company.com"),
        }.build(cx);

        HStack::new(cx, |cx|{
            Label::new(cx, "Hello");
            Label::new(cx, "World");
        });
    })
    .run();
}

This builds the model data into the tree, in this case at the root Window.

Internally, Vizia enforces a separation between views and models by storing them separately. However, for processes like event propagation, models can be thought of as existing within the tree, with an associated parent view.

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, and with a child HStack view with two child Label views.

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.

Environment

The Environment is a built-in model used to specify system specific application data, such as the current system locale and current system theme preference, which can then be used by any view in the application.

For example, we can bind to the locale and conditionally change the properties of a view depending on the selected language:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx| {
        Binding::new(cx, Environment::locale, |cx, locale| {
            match locale.get(cx).to_string().as_ref() {
                "en-US" => {
                    Element::new(cx).background_color(Color::from("#006847"));
                }

                "fr" => {
                    Element::new(cx).background_color(Color::from("#004768"));
                }

                _ => {}
            }
        });
    })
    .run()
}

// Image here

The above example has an Element which will change color depending on the locale between en-US (US English) and fr (french).

By default the environment will use values specified by the system, such as the system specified language, but we can override these values with an environment event.

For example, we can toggle between two locales with a pair of checkboxes:

use vizia::prelude::*;

fn main() -> Result<(), ApplicationError> {
    Application::new(|cx| {
        cx.emit(EnvironmentEvent::SetLocale("en-US".parse().unwrap()));

        HStack::new(cx, |cx| {
            Checkbox::new(cx, Environment::locale.map(|locale| {
                    locale.to_string() == "en-US"
                }))
                .on_toggle(|cx| {
                    cx.emit(EnvironmentEvent::SetLocale("en-US".parse().unwrap()))
                });
            Label::new(cx, "English");

            Checkbox::new(cx, Environment::locale.map(|locale| {
                    locale.to_string() == "fr"
                }))
                .on_toggle(|cx| {
                    cx.emit(EnvironmentEvent::SetLocale("fr".parse().unwrap()))
                });
                .left(Pixels(10.0));
            Label::new(cx, "French");
        })
        .space(Pixels(10.0))
        .child_top(Stretch(1.0))
        .child_bottom(Stretch(1.0))
        .col_between(Pixels(5.0))
        .height(Auto);

        Binding::new(cx, Environment::locale, |cx, locale| {
            match locale.get(cx).to_string().as_ref() {
                "en-US" => {
                    Element::new(cx).background_color(Color::from("#006847"));
                }

                "fr" => {
                    Element::new(cx).background_color(Color::from("#004768"));
                }

                _ => {}
            }
        });
    })
    .run()
}

// Image here

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() -> Result<(), ApplicationError> {
    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 {}

fn main() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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() -> Result<(), ApplicationError> {
    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 derive Lens, then the map_ref() modifier can be used:

fn main() -> Result<(), ApplicationError> {
    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()
}

Events

Events are used to communicate actions to update model or view data.

Events propagate through the tree from origin to target, 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:

use vizia::prelude::*;

pub struct AppData {
    name: String,
}

pub enum PersonEvent {
    UpdateName(String),
}

Emitting Events

Events are usually emitted in response to some action on a view. For example, a button takes an action and a view to display. When the button is pressed the action is triggered, emitting an event up the tree.

use vizia::prelude::*;

pub enum AppEvent {
    UpdateName(String),
}

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

impl Model for Person {}

fn main() {
    Application::new(|cx| {
        AppData { 
            name: String::from("John Doe"),
        }.build(cx);

        Label::new(cx, Person::name);
        
        Button::new(
            cx,
            |cx| cx.emit(AppEvent::UpdateName(String::from("Rob Doe"))),
            |cx| Label::new(cx, "Update"),
        );
    })
    .inner_size((400, 100))
    .run();
}

Handling Events

Events are handled by views and models with the event() method of the View or Model traits.

use vizia::prelude::*;

pub enum AppEvent {
    UpdateName(String),
}

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

impl Model for Person {
    fn event(cx: &mut Context, event: &mut Event) {
        event.map(|app_event, meta| match app_event {
            AppEvent::UpdateName(new_name) => {
                self.name = new_name.clone();
            }
        });
    }
}

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

Note that in the above example the rust compiler is able to infer the message type from the match statement.

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, for example:

event.map(|person_event, meta| match person_event {
    AppEvent::UpdateName(new_name) => {
        self.name = new_name.clone();

        // Consume the event to stop it propagating
        meta.consume();
    }
});

Event Propagation

Events propagate through the view tree according to their specified Propagation. Using cx.emit() will produce an event which propagates up the tree from ancestor to ancestor.

To send an event directly to another view, there is cx.emit_to(message, target), which takes a message as well as a target id.

Stylesheets

Styling refers to modifying the visual properties of a view, such as its background, border, font, etc.

Previously it was shown how modifiers can be used to style views inline. However, it is also possible for multiple views to share the same styling through the use of Cascading Style Sheets (CSS).

Vizia can use CSS to apply style rules to multiple views simultaneously. A CSS string can be defined within a rust file as a constant, or within an external CSS file.

Adding a constant stylesheet string

To add a stylesheet which is already a string in rust code, use the add_stylesheet() method on Context. For example:

use vizia::prelude::*;

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

fn main() {
    Application::new(|cx|{
        
        cx.add_stylesheet(STYLE);
        
        Element::new(cx)
            .size(Pixels(100.0));
    })
}

Adding an external CSS file

To add a stylesheet which is defined in a separate .css file, use add_stylesheet() method with the include_style!() macro. For example:

/* style.css */
element {
    background-color: blue;
}
/* main.rs */
use vizia::prelude::*;

fn main() {
    Application::new(|cx|{
        
        cx.add_stylesheet(include_style!("style.css"));
        
        Element::new(cx)
            .size(Pixels(100.0));
    })
}

External stylesheets can be hot-reloaded using the F5 key while the application is running in debug mode.

Rules & Selectors

Vizia currently supports a custom subset of the CSS standard. This section provides an overview and reference of the supported CSS selectors, selector combinators and pseudo-classes available in vizia.

Style Rules

A typical style rule might look something like this:

hstack.one label {
    background-color: red;
    width: 30px;
}

The first part before the opening brace is called a selector, which determines which views the rule applies to, and the part inside the brackets are a list of properties and values to apply to the styling of matching views.

Selectors

Basic Selectors

SelectorDescription
typeSelects all views that have the given element name, e.g. button will select all Button views.
classSselects all views that have the given class name prefixed with a period, e.g. .class-name will match any view that has class("class-name"). A class name can be added to a view with the class style modifier. The toggle_class modifier can be used to conditionally add/remove a class from a view, typically with the use of a lens to a boolean.
IDSelects views with the specified ID name, prefixed with a hash, e.g. #id-name will match the view that has id("id-name"). An ID name can be added to a view with the id style modifier and must be a unique name.
universalThe universal selector, denoted with an asterisk, selects all views.

Combinators

Using combinators, we can combine selectors to select views based on their relationship to other views within the tree.

CombinatorDescription
descendantSelects views which match the selector after the space if they are descended from an view which matches the selector before the space. For example, hstack label will match any Label which has an HStack as an ancestor further up the tree.
childSelects views which match the selector after the greater than character (>) if they are the child of a view which matches the selector before the greater than character. For example, hstack > label will match any Label which has an HStack as a parent.
subsquent-siblingThe subsequent-sibling combinator, denoted with a tilde (~), selects siblings. Given A ~ B, all views matching B will be selected if they are preceded by A, provided both A and B share the same parent.
next-siblingThe next-sibling combinator, denoted by the plus symbol (+), is similar to the subsequent-sibling. However, given A + B, it only matches B if B is immediately preceded by A, with both sharing the same parent.

Pseudo-classes

Pseudo-classDescription
:rootSelects the root window.
:hoverSelects the currently hovered view.
:checkedSelects any view which has been marked as checked. A view can be marked as checked with the checked style modifier.
:disabledSelects any view which has been marked as disabled. A view can be marked as disabled with the disabled style modifier.
:activeSelects any view which has been marked as active.
:focusSelects the currently focused view.
:focus-visibleSelects the currently focused view if the view was focused via keyboard navigation.

List of supported style properties

This section provides a list of the currently supported style properties in vizia. This excludes layout properties which are detailed in the layout section of this guide.

For the corresponding modifier name, replace any hyphens with underscores. For example, background-color in CSS becomes the background_color() modifier in Rust.

PropertyTypeInitial ValueInheritedAnimatable
backdrop-filterFilternoneNoYes
background-colorColortransparentNoYes
background-imageBackgroundImagenoneNoNo
background-sizeBackgroundSizeauto autoNoYes
blend-modeBlendModenormalNoNo
bordershorthand
border-colorColortransparentNoYes
border-styleBorderStylesolidNoNo
border-widthLengthOrPercentage0NoYes
caret-colorColortransparentYesYes
clip-pathClipPathnoneNoYes
colorColortransparentYesYes
corner-radiusCornerRadius0NoYes
corner-top-left-radiusLengthOrPercentage0NoYes
corner-top-right-radiusLengthOrPercentage0NoYes
corner-bottom-left-radiusLengthOrPercentage0NoYes
corner-bottom-right-radiusLengthOrPercentage0NoYes
corner-shapeRect<CornerShape>roundNoNo
corner-top-left-shapeCornerShaperoundNoNo
corner-top-right-shapeCornerShaperoundNoNo
corner-bottom-left-shapeCornerShaperoundNoNo
corner-bottom-right-shapeCornerShaperoundNoNo
cursorCursorIcondefaultNoNo
displayDisplayflexNoYes
font-familyFontFamilyYesNo
font-sizeFontSize16YesYes
font-slantFontSlantnormalYesNo
font-weightFontWidthregularYesYes
font-widthFontWeightnormalYesYes
font-variation-settingsFontVariationnoneYes
line-clampu321NoNo
opacityf321.0NoYes
outlineshorthandNo
outline-colorColortransparentNoYes
outline-offsetLengthOrPercentage0NoYes
outline-widthLengthOrPercentage0NoYes
overflowOverflowvisibleNoNo
overflow-xOverflowvisibleNoNo
overflow-yOverflowvisibleNoNo
pointer-eventsPointerEventsautoNoNo
rotateAngle0NoYes
scaleScale1NoYes
selection-colorColortransparentYesYes
shadowShadowNoYes
text-alignTextAlignleftNoNo
text-decorationshorthand
text-decoration-lineYesNo
text-overflowclipNoNo
text-wrapbooltrueNoNo
transformTransformNoYes
transform-originPositionNoYes
transition
translateTranslateNoYes
visibilityVisibilityNoNo
z-indexi320NoNo

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, and optionally any delay on the animation and a timing function.

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|{
        Element::new(cx)
            .class("my_view")
            .size(Pixels(200.0));
    })
    .run();
}

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;
}

Layer Properties

Visibility

The visibility property shows or hides a view without affecting its layout.

/* Keyword values */
visibility: visible;
visibility: hidden;

Opacity

The opacity property determines how much of the content behind a view can be seen.

opacity: 0.9;

opacity: 90%;

Blend mode

The blend-mode property determines how a view's content should blend with the content of the view's behind it.

/* Keyword values */
blend-mode: normal;
blend-mode: multiply;
blend-mode: screen;
blend-mode: overlay;
blend-mode: darken;
blend-mode: lighten;
blend-mode: color-dodge;
blend-mode: color-burn;
blend-mode: hard-light;
blend-mode: soft-light;
blend-mode: difference;
blend-mode: exclusion;
blend-mode: hue;
blend-mode: saturation;
blend-mode: color;
blend-mode: luminosity;
blend-mode: plus;

Z-Index

The z-index property determines the z-order of a view and its descendants. Overlapping elements with a larger z-index cover those with a smaller one.

Overflow

The overflow shorthand property sets the desired behavior when content does not fit in the view's bounds in the horizontal and/or vertical direction.

/* Keyword values */
overflow: visible;
overflow: hidden;
overflow: hidden visible;

The overflow can be set for each axis using the overflow-x and overflow-y properties.

Clip Path

The clip-path property creates a clipping region that sets what part of an element should be shown. Parts that are inside the region are shown, while those outside are hidden.

/* Keyword values */
clip-path: auto;

/* <basic-shape> values */
clip-path: inset(100px 50px);
clip-path: rect(5px 5px 160px 145px);

Background Properties

Background Color

Sets the background-color of a view.

/* Keyword values */
background-color: red;
background-color: indigo;

/* Hexadecimal value */
background-color: #bbff00; /* Fully opaque */
background-color: #bf0; /* Fully opaque shorthand */
background-color: #11ffee00; /* Fully transparent */
background-color: #1fe0; /* Fully transparent shorthand */
background-color: #11ffeeff; /* Fully opaque */
background-color: #1fef; /* Fully opaque shorthand */

/* RGB value */
background-color: rgb(255 255 128); /* Fully opaque */
background-color: rgb(117 190 218 / 50%); /* 50% transparent */

/* HSL value */
background-color: hsl(50 33% 25%); /* Fully opaque */
background-color: hsl(50 33% 25% / 75%); /* 75% opaque, i.e. 25% transparent */

The background color is rendered behind any background-image but will show through any transparency in the image.

Background Image

The background-image property sets one or more background images on a view.

/* single image */
background-image: linear-gradient(black, white);
background-image: url("cat-front.png");

/* multiple images */
background-image: radial-gradient(circle, #0000 45%, #000f 48%),
  radial-gradient(ellipse farthest-corner, #fc1c14 20%, #cf15cf 80%);

Background Size

The background-size property sets the size of the view's background image. The image can be left to its natural size, stretched, or constrained to fit the available space.

/* Keyword values */
background-size: cover;
background-size: contain;

/* One-value syntax */
/* the width of the image (height becomes 'auto') */
background-size: 50%;
background-size: 3.2em;
background-size: 12px;
background-size: auto;

/* Two-value syntax */
/* first value: width of the image, second value: height */
background-size: 50% auto;
background-size: 3em 25%;
background-size: auto 6px;
background-size: auto auto;

/* Multiple backgrounds */
background-size: auto, auto; /* Not to be confused with `auto auto` */
background-size: 50%, 25%, 25%;
background-size: 6px, auto, contain;

Background Position

The background-position CSS property sets the initial position for each background image.

/* Keyword values */
background-position: top;
background-position: bottom;
background-position: left;
background-position: right;
background-position: center;

/* <percentage> values */
background-position: 25% 75%;

/* <length> values */
background-position: 0 0;
background-position: 1cm 2cm;
background-position: 10ch 8em;

/* Multiple images */
background-position: 0 0, center;

Border Properties

Border (shorthand)

The border property is shorthand for border-width, border-style, and border-color, and sets the border of a view.

/* style */
border: solid;

/* width | style */
border: 2px dashed;

/* style | color */
border: solid red;

/* width | style | color */
border: 5px dashed green;

Border Width

The border-width property sets the width of a view's border.

border-width: 5px;
border-width: 20%;

Border Style

The border-style property sets the style of a view's border.

border-style: none;
border-style: solid;
border-style: dashed;
border-style: dotted;

Border Color

The border-color property sets the color of a view's border.

border-color: red;
border-color: #566;

Corner Properties

Corner Radius

The corner-radius property rounds the corners of a view's outer border edge. You can set a single radius to make circular corners.

/* Radius is set for all 4 sides */
corner-radius: 10px;

/* top-left-and-bottom-right | top-right-and-bottom-left */
corner-radius: 10px 5%;

/* top-left | top-right-and-bottom-left | bottom-right */
corner-radius: 2px 4px 2px;

/* top-left | top-right | bottom-right | bottom-left */
corner-radius: 1px 0 3px 4px;

Individual corner radii can be set using the corner-top-left-radius, corner-top-right-radius, corner-bottom-left-radius, and corner-bottom-right-radius properties.

A circular shape can be achieved by setting the corner radius to 50%:

corner-radius: 50%;

Corner Shape

Effects

Shadows

The shadow property adds shadow effects around a view's frame. You can set multiple shadows separated by commas. A shadow is described by X and Y offsets relative to the element, blur and spread radius, and color.

/* A color and two length values */
/* <color> | <length> | <length> */
shadow: red 60px -16px;

/* Three length values and a color */
/* <length> | <length> | <length> | <color> */
shadow: 10px 5px 5px black;

/* Four length values and a color */
/* <length> | <length> | <length> | <length> | <color> */
shadow: 2px 2px 2px 1px rgb(0 0 0 / 20%);

/* inset, length values, and a color */
/* <inset> | <length> | <length> | <color> */
shadow: inset 5em 1em gold;

/* Any number of shadows, separated by commas */
shadow:
  3px 3px red inset,
  -1em 0 0.4em olive;

Backdrop Filter

The backdrop-filter property allows you apply a blur effect to the area behind a view. Because it applies to everything behind the view, to see the effect the view or its background needs to be transparent or partially transparent.

backdrop-filter: blur(2px);

Outline Properties

Outline (shorthand)

The outline property is shorthand for outline-width and outline-color.

/* width | color */
outline: 2px red;

Outline Width

The outline-width property sets the width of a view's outline.

outline-width: 5px;
outline-width: 20%;

Outline Offset

The outline-offset property sets the amount of space between an outline and the edge or border of a view.

/* <length> values */
outline-offset: 3px;

Outline Color

The outline-color property sets the color of a view's outline.

outline-color: red;
outline-color: #566;

Text Properties

Font Family

The font-family property specifies a prioritized list of one or more font family names and/or generic family names for the selected view.

/* A font family name and a generic family name */
font-family: "Gill Sans Extrabold", sans-serif;
font-family: "Goudy Bookletter 1911", sans-serif;

/* A generic family name only */
font-family: serif;
font-family: sans-serif;
font-family: monospace;
font-family: cursive;
font-family: fantasy;

Font Size

The font-size property sets the size of the font.

/* <absolute-size> values */
font-size: xx-small;
font-size: x-small;
font-size: small;
font-size: medium;
font-size: large;
font-size: x-large;
font-size: xx-large;
font-size: xxx-large;

/* <relative-size> values */
font-size: smaller;
font-size: larger;

/* <length> values */
font-size: 12px;

Font Weight

The font-weight property determines the weight (or boldness) of the font. The weights available depend on the font-family that is currently set.

/* <font-weight-absolute> keyword values */
font-weight: normal;
font-weight: bold;

/* <font-weight-absolute> numeric values [1,1000] */
font-weight: 100;
font-weight: 200;
font-weight: 300;
font-weight: 400; /* normal */
font-weight: 500;
font-weight: 600;
font-weight: 700; /* bold */
font-weight: 800;
font-weight: 900;

Font Slant

The font-slant property determines whether a font should be styled with a normal, italic, or oblique face from its font-family.

font-slant: normal;
font-slant: italic;
font-slant: oblique;
font-slant: oblique 10deg;

Font Width

The font-width property selects a normal, condensed, or expanded face from a font.

/* keyword values */
font-width: normal;
font-width: ultra-condensed;
font-width: extra-condensed;
font-width: condensed;
font-width: semi-condensed;
font-width: semi-expanded;
font-width: expanded;
font-width: extra-expanded;
font-width: ultra-expanded;

Font Variation Settings

The font-variation-settings property provides low-level control over variable font characteristics by letting you specify the four letter axis names of the characteristics you want to vary along with their values.

/* Set values for variable font axis names */
font-variation-settings: "xhgt" 0.7;

Transform Properties

Transforms are a post-layout effect and therefore affect the rendering of a view but not its layout.

Transform

The transform property lets you rotate, scale, skew, or translate a view.

/* Function values */
transform: matrix(1, 2, 3, 4, 5, 6);
transform: rotate(0.5turn);
transform: rotateX(10deg);
transform: rotateY(10deg);
transform: translate(12px, 50%);
transform: translateX(2em);
transform: translateY(3in);
transform: scale(2, 0.5);
transform: scaleX(2);
transform: scaleY(0.5);
transform: skew(30deg, 20deg);
transform: skewX(30deg);
transform: skewY(1.07rad);

Transform Origin

The transform-origin property determines the origin for a view's transformations.

/* One-value syntax */
transform-origin: 2px;
transform-origin: bottom;

/* x-offset | y-offset */
transform-origin: 3cm 2px;

/* x-offset-keyword | y-offset */
transform-origin: left 2px;

/* x-offset-keyword | y-offset-keyword */
transform-origin: right top;

/* y-offset-keyword | x-offset-keyword */
transform-origin: top right;

Translate

The translate property allows you to specify translation transforms individually and independently of the transform property.

/* Single values */
translate: 100px;
translate: 50%;

/* Two values */
translate: 100px 200px;
translate: 50% 105px;

Rotate

The rotate property allows you to specify rotation transforms individually and independently of the transform property.

/* Angle value */
rotate: 90deg;
rotate: 0.25turn;
rotate: 1.57rad;

Scale

The scale property allows you to specify scale transforms individually and independently of the transform property.

/* Single values */
/* values of more than 1 or 100% make the element grow */
scale: 2;
/* values of less than 1 or 100% make the element shrink */
scale: 50%;

/* Two values */
scale: 2 0.5;

Other Properties

Cursor

The cursor property sets the mouse cursor, if any, to show when the mouse pointer is over a view.

/* Keyword value */
cursor: auto;
cursor: pointer;
/* … */
cursor: zoom-out;

Pointer Events

The pointer-events property sets under what circumstances (if any) a view can become the target of pointer events.

/* Keyword values */
pointer-events: auto;
pointer-events: none;

Layout

The position and size of a view is determined by its layout properties. Vizia uses a custom layout system called morphorm which can achieve similar results to flexbox.

The following sections details the functioning of the layout system:

Units

Many of the layout properties used in vizia use the Units type to specify their value. The Units type has four variants:

  • Pixels
  • Percentage
  • Stretch
  • Auto

Not all variants may have an effect on a particular property. For example, the padding properties do not use the stretch or auto variants.

Pixels

The pixels variant allows space and size to be specified with a fixed number of logical pixels. The physical space or size is determined by the window scale factor:

physical_pixels = logical_pixels * scale_factor

Percentage

The percentage variant allows space and size to be specified as a fraction of the parent size:

computed_value = percentage_value * parent_size / 100.0

The dimension is consistent, so specifying the left space as a percentage will use the parent width to calculate the desired space.

Stretch

The stretch variant allows space and size within a stack to be specified as a ratio of the remaining free space of the parent after subtracting any fixed-size space and size.

This is best understood with an example. For two views in a horizontal stack, the first with a width of stretch factor 1.0 and the second with a width of stretch factor 2.0, the first will occupy 1/3 of the horizontal free space and the second will occupy 2/3 of the horizontal free space.

Auto

The auto variant is typically the default value for a layout property and has no effect. The exception to this is with the size and size constraint properties, where an auto value represents the total size of the children of a view. So for example, setting the width to auto will cause the view to 'hug' its children in the horizontal direction.

Layout Properties

This section provides a list of the currently supported style properties in vizia.

PropertyTypeInitial ValueInheritedAnimatable
layout-typeLayoutTypecolumnNoNo
position-typePositionTyperelativeNoNo
gapshorthand
horizontal-gapUnitsautoNoYes
vertical-gapUnitsautoNoYes
min-gapshorthand
min-horizontal-gapUnitsautoNoYes
min-vertical-gapUnitsautoNoYes
max-gapshorthand
max-horizontal-gapUnits0pxNoYes
max-vertical-gapUnitssolidNoYes
paddingshorthand
padding-leftUnitsautoNoYes
padding-rightUnitsautoNoYes
padding-topUnitsautoNoYes
padding-bottomUnitsautoNoYes
sizeshorthand
widthUnits1sNoYes
heightUnits1sNoYes
min-sizeshorthand
min-widthUnitsautoNoYes
min-heightUnitsautoNoYes
max-sizeshorthand
max-widthUnitsautoNoYes
max-heightUnitsautoNoYes
spaceshorthand
leftUnitsautoNoYes
rightUnitsautoNoYes
topUnitsautoNoYes
bottomUnitsautoNoYes

Size

The size of a view is controlled by the width and height properties, and can be specified using pixels, a percentage, a stretch factor, or auto.

Fixed Size (Pixels)

The width and height of a view can be specified with a number of logical pixels. In this case changes to the size of the parent view, or to the children of the view, will have no effect on its size.

fixed_width

Percentage Size

The width and height of a view can also be specified as a percentage of the parent view size.

percentage_width

Stretch Size

The width and height of a view can also be specified with a stretch factor, which will cause the view to fill a proportion of the free space.

stretch_width

For example, given the following code:

HStack::new(cx, |cx|{
    Label::new(cx, "Hello")
        .background_color(Color::gray())
        .width(Stretch(1.0));

    Label::new(cx, "World")
        .width(Stretch(2.0));
});

The first label occupies 1/3 of the horizontal space and the second occupies 2/3 of the free space.

The free space is the size of the parent in the main axis (width for row, height for column) minus any fixed space/size.

Auto Size (Hug)

The width and height of a view can be specified as auto, which results in the view 'hugging' its children in the specified axis.

auto_width

For example, given the following code:

HStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.height(Auto);

The height of the HStack is specified as Auto, which causes the computed height to become the maximum of its child heights.

If we had specified the hstack width to be Auto, then the computed width would be the sum of the widths of its children.

The width and height of a view can be specified using the respective layout modifiers which use the Units type:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .width(Pixels(200.0))
    .height(Pixels(30.0));

The width and height can also be set simultaneously with the size layout modifier:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .size(Pixels(50.0));

Or in CSS:

.hello_label {
    width: 20px;
    height: 1s;
}

Layout Type (Direction)

The layout-type determines the direction which a parent will stack its children. A parent element can arrange its children into a vertical stack (layout-type: column) or a horizontal stack (layout-type: row).

layout_type

The layout-type of a view can be specified using the respective layout modifier:

VStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.layout_type(LayoutType::Row);

Or in CSS:

.container {
    layout-type: row;
}

Alignment

The alignment property determines how the children will be aligned within a view. There are 9 options:

  • top-left
  • top-center
  • top-right
  • left
  • center
  • right
  • bottom-left
  • bottom-center
  • bottom-right

Alignment also applies to text within a view, unless overridden by the text-align property.

alignment

The alignment of a view can be specified using the respective layout modifier:

VStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.alignment(Alignment::Center);

Or in CSS:

.container {
    alignment: center;
}

Padding

The padding property, Shorthand for padding-left, padding-top, padding-right, and padding-bottom, determines the spacing between the parent bounds and the children of a view. It can be specified as pixels or a percentage.

padding

The padding-left, padding-top, padding-right, and padding-bottom of a view can be specified using the respective layout modifiers:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .padding_left(Pixels(5.0))
    .padding_top(Pixels(10.0))
    .padding_right(Pixels(15.0))
    .padding_bottom(Pixels(20.0));

The padding modifier can also be used to set all four sides simultaneously:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .padding(Pixels(20.0));

Or in CSS:

.hello_label {
    padding_left: 5px;
    padding_top: 10px;
    padding_right: 15px;
    padding_bottom: 20px;
}

Gap

The gap property, shorthand for horizontal-gap and vertical-gap, determines the spacing between the children of a view. It can be specified as pixels, a percentage, or a stretch factor.

gap

Stretch Gap

Setting the gap to a stretch factor will result in evenly distributed space between children.

stretch_gap

Negative Gap

A negative pixels value for gap can be used and results in the children of the view overlapping.

negative_gap

The gap of a view can be specified using the respective layout modifier:

VStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.gap(Pixels(20.0));

Or in CSS:

.container {
    gap: 20px;
}

The horizontal-gap and vertical-gap can also be set independently:

VStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.horizontal_gap(Pixels(20.0))
.vertical_gap(Pixels(10.0));

Or in CSS:

.container {
    horizontal-gap: 20px;
    vertical-gap: 10px;
}

Position Type

The position type property determines whether a view should be positioned in-line with its siblings in a stack (position-type: relative), which is the default, or out-of-line and independently of its siblings (position-type: absolute).

position-type

The position-type of a view can be specified using the respective layout modifier:

VStack::new(cx, |cx|{
    Label::new(cx, "Hello");
    Label::new(cx, "World");
})
.position_type(PositionType::Absolute);

Or in CSS:

.container {
    position-type: absolute;
}

Spacing

Spacing applies only to children with a position type of absolute, and is specified using the left, right, top, and bottom properties, or the space property as a shorthand. Each of these properties can have a value in pixels, a percentage, or a stretch factor.

A combination of pixels and stretch spacing can be used to align a view within its parent. For example, stretch factors can be used to center a view by applying equal stretch factors to all spacing properties.

spacing

The left, top, right, and bottom of a view can be specified using the respective layout modifiers:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .position_type(PositionType::Absolute)
    .left(Pixels(5.0))
    .top(Pixels(10.0))
    .right(Pixels(15.0))
    .bottom(Pixels(20.0));

The space modifier can also be used to set all four sides simultaneously:

Label::new(cx, "Hello World")
    .background_color(Color::gray())
    .position_type(PositionType::Absolute)
    .space(Pixels(20.0));

Or in CSS:

.hello_label {
    left: 5px;
    top: 10px;
    right: 15px;
    bottom: 20px;
}

Constraints

Constraint properties can be used to specify a minimum or maximum value for size or gap.

Size Constraints

Size constraints can have a value in pixels, a percentage, or auto.

The min-size property, which is shorthand for min_width and min_height, can be used to set a minimum constraint for the size of a view.

min_width

The max-size property, which is shorthand for max_width and max_height, can be used to set a maximum constraint for the size of a view.

max_width

Auto min-width/height

An auto min-width or min-height can be used to specify that a view should not be any smaller than its contents, i.e. the sum or max of its children depending on layout type, in the horizontal or vertical directions respectively.

This is useful in combination with a stretch size, so the view can contract with its parent container but still maintain a minimum size of its content, for example the text of a view.

auto_min_width

Auto max-width/height

An auto max-width or max-height can be used to specify that a view should not be any larger than its contents, i.e. the sum or max of its children depending on layout type, in the horizontal or vertical directions respectively.

This is useful in combination with a stretch size, so the view can grow with its parent container but no larger than the size of its content.

auto_max_width

Gap Constraints

Gap constraints can have a value in pixels or a percentage.

The min-gap property, which is shorthand for min_horizontal_gap and min_vertical_gap, can be used to set a minimum constraint for the gap between the children of a view. This is particularly useful in combination with a stretch gap, so that the children are evenly distributed but cannot be closer than the minimum gap When the parent container shrinks.

min_gap

Similarly, the max-gap property, which is shorthand for max_horizontal_gap and max_vertical_gap, can be used to set a maximum constraint for the gap between the children of a view.

max_gap