Introduction

Maycoon is a framework for creating interactive user interfaces using Rust.

It is built on top of vello and winit for rendering and window creation while using wgpu as the graphics backend.

This makes it compatible with many platforms and easy to use with different graphics backends.

The framework also includes a reactive signal system for managing internal application state, as well as a parallel task runner using a the futures crate for asynchronous programming.

Quick Start

Installation

If you haven't already, you can install the Rust Programming Language using rustup.

To create a new cargo project, run cargo new --bin <project-name>.

After creating the project, you can cd into the folder and then run cargo add maycoon to add maycoon as a dependency.

Feature Gates

Most cargo dependencies have feature gates that toggle special features. You can configure them via the features array inside dependencies like this:

[package]
name = "my_project"
description = "My Awesome App"
# other stuff...

[dependencies]
maycoon = { version = "*", features = ["vg"] }

Following maycoon features are available at the time of writing:

  • vg - Enables structures and functions for drawing vector graphics using vello.

  • macros - Enables useful macros for easier development.

A Basic Counter App

The easiest way to learn about Maycoon is to start studying some examples. In this Tutorial we will find out how to create a basic counter App with increase and decrease buttons, as well as a text widget to display the count.

Setup

For creating a new project, see the Installation Guide.

The App

First we need to import the necessary items from the maycoon crate:

use maycoon::core::app::context::AppContext;
use maycoon::core::app::update::Update;
use maycoon::core::app::Application;
use maycoon::core::config::MayConfig;
use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle};
use maycoon::core::reference::Ref;
use maycoon::core::signal::eval::EvalSignal;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::signal::{MaybeSignal, Signal};
use maycoon::core::widget::{Widget, WidgetLayoutExt};
use maycoon::math::Vector2;
use maycoon::theme::theme::celeste::CelesteTheme;
use maycoon::widgets::button::Button;
use maycoon::widgets::container::Container;
use maycoon::widgets::text::Text;

// Our application structure
struct MyApp;

impl Application for MyApp {
    // The theme we want to use
    type Theme = CelesteTheme;

    // The root widget
    fn build(context: AppContext) -> impl Widget {
        let counter = context.use_signal(StateSignal::new(0));

        Container::new(vec![
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Increase".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() + 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Decrease".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() - 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();
                Box::new(Text::new(counter.map(|i| Ref::Owned(i.to_string()))))
            },
        ])
        .with_layout_style(LayoutStyle {
            size: Vector2::<Dimension>::new(Dimension::percent(1.0), Dimension::percent(1.0)),
            flex_direction: FlexDirection::Column,
            align_items: Some(AlignItems::Center),
            ..Default::default()
        })
    }

    // The configuration
    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    MyApp.run()
}

A Container widget draws and handles a collection of widgets specified as a Vector of Boxed Widgets. In our case, we need two buttons: Increase and decrease to manipulate our counter value, as well as a text widget for displaying the counter value.

We use the MaybeSignal::signal function to pass the signals to the buttons and pass fixed strings to the "Increase" and "Decrease" buttons.

For the two buttons, we need to define Updates to apply updates to the App.

The Update::DRAW constant tells the App to only re-draw and not re-layout the application when pressing the buttons.

The with_layout_style function applies a custom layout which centers the widgets in our case.

NOTE: You need to clone signals before using them inside move closures to use them inside multiple closures.

Running the App

To launch the App, you can run cargo run.

You should see a window with "Increase" and "Decrease" buttons along with a text.

Configuration

You can configure your app using the MayConfig struct.

It has many different fields to customize your application to your liking:

window: WindowConfig

Configures the application window.

title: String

The title of the window in desktop environments.

size: Vector2<f64>

The initial size of the window in desktop environments.

min_size: Vector2<f64>

The minimum size the window can have in desktop environments.

max_size: Vector2<f64>

The maximum size the window can have in desktop environments.

resizable: bool

Whether the window can be resized in desktop environments.

maximized: bool

Whether the window is maximized in desktop environments.

mode: WindowMode

The mode of the window in desktop environments. Can be WindowMode::Windowed, WindowMode::Borderless or WindowMode::Fullscreen.

level: WindowLevel

The level of the window in desktop environments. Can be WindowLevel::Normal, WindowLevel::AlwaysOnTop or WindowLevel::AlwaysOnBottom.

visible: bool

If the window should be visible on startup in desktop environments.

blur: bool

If the window background should be blurred in desktop environments.

transparent: bool

If the window background should be transparent in desktop environments. May not be compatible with all desktop environments.

position: Option<Point2<f64>>

The initial position of the window in desktop environments. Uses the default positioning if None.

active: bool

If the window should be active/focused on startup in desktop environments.

buttons: WindowButtons

The window buttons to enable in desktop environments.

decorations: bool

If the window should have decorations (borders) in desktop environments.

resize_increments: Option<Vector2<f64>>

The resize increments of the window in desktop environments. May not be compatible with all desktop environments.

content_protected: bool

Prevents window capturing by some apps (not all though).

icon: Option<WindowIcon>

The window icon in desktop environments.

cursor: Cursor

The window cursor in desktop environments.

close_on_request: bool

If the window should exit on close request (pressing the close window button) in desktop environments.

render: RenderConfig

Configures the application renderer.

antialiasing: AaConfig

The antialiasing config. Can affect rendering performance. Can be AaConfig::Area, AaConfig::Msaa8 or AaConfig::Msaa16.

cpu: bool

If the backend should use the CPU for most drawing operations. The GPU is still used during rasterization.

present_mode: PresentMode

The presentation mode of the window/surface. Can be PresentMode::AutoVsync, PresentMode::AutoNoVsync, PresentMode::Fifo, PresentMode::FifoRelaxed, PresentMode::Immediate or PresentMode::Mailbox.

init_threads: Option<NonZeroUsize>

The number of threads the renderer uses for initialization. When None, the number of logical cores is used.

device_selector: fn(&Vec<DeviceHandle>) -> &DeviceHandle

A selector function to determine which device to use for rendering.

tasks: Option<TasksConfig>

Task Runner Configuration. If None, the task runner won't be enabled.

stack_size: usize

The stack size of each thread of the task runner thread pool. Defaults to 1 MB.

workers: NonZeroUsize

The amount of worker threads of the task runner thread pool. Defaults to half of the available parallelism.

theme: Theme

The application theme.

State Management

What is state management?

State management is the process of keeping track of the state of your application and providing a way to modify it.

Applications are usually just a giant event loop with logic. This means that you can't just change the state randomly and expect sane results. That's why there is state management.

A good state management system should be: reactive, fast and memoy efficient.

The Signal System

Maycoon uses signals to manage state using listeners and thread-safe mechanics.

A signal is composed out of following components:

  • a value, often but not always mutable.
  • a list of listeners to notify when the value changes.
  • a thread-safe way to modify the value.

Beware that not all signal types are mutable and some are merely side-effects.

Using Signal

The Signal trait has many methods, but the most important ones are:

  • get: Returns a reference to the inner value as a Ref enum.
  • set: Sets the inner value and notifies all listeners.
  • notify: Notifies all listeners attached to the signal.
  • listen: Attaches a listener to the signal.

In order to use signals, you must first "hook" them into the application lifecycle using Signal::hook or AppContext::use_signal like this:

#![allow(unused)]
fn main() {
// hook the signal into the application
let value = context.use_signal(StateSignal::new("Hello World".to_string()));

// use the signal inside a widget
Text::new(MaybeSignal::signal(value))
}

MaybeSignal is merely an enum which can be either a signal or a fixed value.

Passing signals to widgets using MaybeSignal::signal can make these signals mutate over the lifetime of the application, depending on which widget you pass it to.

Do's and Dont's

Do...

  • ...use signals sparingly and prefer MaybeSignal::value for fixed values.
  • ...use Signal::hook or AppContext::use_signal to hook signals into the application.
  • ...use MaybeSignal::signal to pass signals to widgets.
  • ...use specific signals for specific needs.
  • ...use signals for local state management.

Don't...

  • ...use Signal::set_value directly to mutate values.
  • ...create signals without hooking them into the application.
  • ...pass signals as mutable references.
  • ...pass signals to widgets without using MaybeSignal::signal.
  • ...use signals for global state management.

Built-in Themes

WIP

Widgets

Maycoon has a collection of built-in widgets with basic and advanced logic.

Every widget has precise documentation, an example and fully featured theming properties with documentation.

Text

Hello World example

A generic widget that displays a text...

What else is there to say?

See for yourself:

#![allow(unused)]
fn main() {
Text::new("Hello, World!".to_string())
}

or see the "hello-world" example:

use maycoon::core::app::context::AppContext;
use maycoon::core::app::Application;
use maycoon::core::config::MayConfig;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::signal::MaybeSignal;
use maycoon::core::widget::Widget;
use maycoon::theme::theme::celeste::CelesteTheme;
use maycoon::widgets::text::Text;

struct MyApp;

impl Application for MyApp {
    type Theme = CelesteTheme;

    fn build(context: AppContext) -> impl Widget {
        let value = context.use_signal(StateSignal::new("Hello World ".to_string()));

        Text::new(MaybeSignal::signal(value))
    }

    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    MyApp.run()
}

Button

Counter example

The button widget is probably one of the most essential things in all of the UI making world.

They have an on_pressed callback that is called when the button is pressed.

Buttons can be used like this:

#![allow(unused)]
fn main() {
Button::new(
    Text::new("Increase".to_string())
).with_on_pressed(
    MaybeSignal::signal(
        context.use_signal(
            EvalSignal::new(move || {
                println!("Pressed");
                Update::DRAW
            })
        )
    ),
)
}

For a more complex example, see the counter example:

use maycoon::core::app::context::AppContext;
use maycoon::core::app::update::Update;
use maycoon::core::app::Application;
use maycoon::core::config::MayConfig;
use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle};
use maycoon::core::reference::Ref;
use maycoon::core::signal::eval::EvalSignal;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::signal::{MaybeSignal, Signal};
use maycoon::core::widget::{Widget, WidgetLayoutExt};
use maycoon::math::Vector2;
use maycoon::theme::theme::celeste::CelesteTheme;
use maycoon::widgets::button::Button;
use maycoon::widgets::container::Container;
use maycoon::widgets::text::Text;

struct MyApp;

impl Application for MyApp {
    type Theme = CelesteTheme;

    fn build(context: AppContext) -> impl Widget {
        let counter = context.use_signal(StateSignal::new(0));

        Container::new(vec![
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Increase".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() + 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Decrease".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() - 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();
                Box::new(Text::new(counter.map(|i| Ref::Owned(i.to_string()))))
            },
        ])
        .with_layout_style(LayoutStyle {
            size: Vector2::<Dimension>::new(Dimension::percent(1.0), Dimension::percent(1.0)),
            flex_direction: FlexDirection::Column,
            align_items: Some(AlignItems::Center),
            ..Default::default()
        })
    }

    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    MyApp.run()
}

Container

A list or container of widgets. Similar to div-Elements in HTML.

See the counter example for basic usage:

use maycoon::core::app::context::AppContext;
use maycoon::core::app::update::Update;
use maycoon::core::app::Application;
use maycoon::core::config::MayConfig;
use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle};
use maycoon::core::reference::Ref;
use maycoon::core::signal::eval::EvalSignal;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::signal::{MaybeSignal, Signal};
use maycoon::core::widget::{Widget, WidgetLayoutExt};
use maycoon::math::Vector2;
use maycoon::theme::theme::celeste::CelesteTheme;
use maycoon::widgets::button::Button;
use maycoon::widgets::container::Container;
use maycoon::widgets::text::Text;

struct MyApp;

impl Application for MyApp {
    type Theme = CelesteTheme;

    fn build(context: AppContext) -> impl Widget {
        let counter = context.use_signal(StateSignal::new(0));

        Container::new(vec![
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Increase".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() + 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Decrease".to_string())).with_on_pressed(
                        MaybeSignal::signal(context.use_signal(EvalSignal::new(move || {
                            counter.set(*counter.get() - 1);

                            Update::DRAW
                        }))),
                    ),
                )
            },
            {
                let counter = counter.clone();
                Box::new(Text::new(counter.map(|i| Ref::Owned(i.to_string()))))
            },
        ])
        .with_layout_style(LayoutStyle {
            size: Vector2::<Dimension>::new(Dimension::percent(1.0), Dimension::percent(1.0)),
            flex_direction: FlexDirection::Column,
            align_items: Some(AlignItems::Center),
            ..Default::default()
        })
    }

    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    MyApp.run()
}

Checkbox

Checkbox example

The checkbox widget is used to toggle a value, which should be bound to a signal.

It can be used like this:

#![allow(unused)]
fn main() {
Checkbox::new(MaybeSignal::signal(some_signal))
}

or the full example:

use maycoon::core::app::context::AppContext;
use maycoon::core::app::Application;
use maycoon::core::config::MayConfig;
use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle};
use maycoon::core::reference::Ref;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::signal::{MaybeSignal, Signal};
use maycoon::core::widget::{Widget, WidgetLayoutExt};
use maycoon::math::Vector2;
use maycoon::theme::theme::celeste::CelesteTheme;
use maycoon::widgets::checkbox::Checkbox;
use maycoon::widgets::container::Container;
use maycoon::widgets::text::Text;

struct MyApp;

impl Application for MyApp {
    type Theme = CelesteTheme;

    fn build(context: AppContext) -> impl Widget {
        let checked = context.use_signal(StateSignal::new(false));

        Container::new(vec![
            {
                let checked = checked.clone();

                Box::new(Checkbox::new(MaybeSignal::signal(checked)))
            },
            {
                let checked = checked.clone();

                Box::new(Text::new(checked.map(|val| Ref::Owned(val.to_string()))))
            },
        ])
        .with_layout_style(LayoutStyle {
            size: Vector2::<Dimension>::new(Dimension::percent(1.0), Dimension::percent(1.0)),
            flex_direction: FlexDirection::Column,
            align_items: Some(AlignItems::Center),
            ..Default::default()
        })
    }

    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    MyApp.run()
}

Advanced Maycoon