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

/// The application structure.
struct MyApp;

impl Application for MyApp {
    /// The theme to use.
    type Theme = CelesteTheme;
    /// The global state of the application.
    type State = ();

    fn build(context: AppContext, _: Self::State) -> 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(
                        EvalSignal::new(move || {
                            counter.set(*counter.get() + 1);

                            Update::DRAW
                        })
                        .hook(&context)
                        .maybe(),
                    ),
                )
            },
            {
                let counter = counter.clone();

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

                            Update::DRAW
                        })
                        .hook(&context)
                        .maybe(),
                    ),
                )
            },
            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 of the application.
    fn config(&self) -> MayConfig<Self::Theme> {
        MayConfig::default()
    }
}

fn main() {
    /// Run the application without a real global state.
    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:

// 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:

Text::new("Hello, World!".to_string())

See the hello world example for full usage.

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:

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.

Container

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

See the counter example for basic usage.

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:

Checkbox::new(MaybeSignal::signal(some_signal))

See the checkbox example for full usage.

Image

Image example

The Image widget can be used to display... well... images.

You will most probably need the image crate to convert an image to Rgba8.

Image::new(
    ImageData::new(
        Blob::from(
            image::load_from_memory(IMAGE_DATA)
                .unwrap()
                .into_rgba8()
                .to_vec(),
        ),
        ImageFormat::Rgba8,
        427,
        640,
    )
)

See the image example for full usage.

Slider

Slider example

The Slider widget allows the user to select from a range of values (from 0.0 to 1.0) using a sliding mechanic.

let value = context.use_signal(StateSignal::new(0.0f32));

Slider::new(value.maybe())

For a more complex example, see the slider example.

Icon

Icon example

The Icon widget is used to display SVG icons and works similar to the image widget.

You can use the svg_icon! macro to load an SVG icon from a file.

let icon: SvgIcon = svg_icon!("./assets/logo.svg");

Icon::new(icon)

For a full example, see the icon example.

Widget Fetcher

The WidgetFetcher widget can be used to asynchronously run data in the background and build a widget based on the result. This allows you to build widgets based on the state of the data (loaded or not).

WidgetFetcher::new(some_data(), Update::DRAW, |data: Option<MyData>| {
    Text::new(if let Some(data) = data {
        Text::new(data.to_string())
    } else {
        Text::new("Loading...".to_string())
    })
})

See the fetcher example for full usage.

Gesture Detector

The GestureDetector widget allows you to capture widget interactions with its child widget. It can be used to easily create custom widgets or react to user interaction.

GestureDetector::new(Text::new("Gesture Detector".to_string()))
    .with_on_hover(
        EvalSignal::new(move || {
            println!("Hovered");
            Update::DRAW
        })
        .hook(&context)
        .maybe(),
    )
    .with_on_release(
        EvalSignal::new(move || {
            println!("Release");
            Update::DRAW
        })
        .hook(&context)
        .maybe(),
    )
    .with_on_press(
        EvalSignal::new(move || {
            println!("Press");
            Update::DRAW
        })
        .hook(&context)
        .maybe(),
    )

For a full example, see the gesture detector example.

Canvas

Canvas example

The Canvas widget allows you to draw custom shapes on the screen without creating your own widget.

Enable the canvas feature to use it.

Canvas::new(|scene, _| {
    scene.stroke(
        &Stroke::new(10.0),
        Affine::default(),
        &Brush::Solid(palette::css::GREEN),
        None,
        &Circle::new(Point::new(100.0, 100.0), 50.0),
    );
})

For a full example, see the canvas example.

Animator

The Animator widget allows you to animate the child widget over time.

It requires you to provide the duration, the child widget and a callback that will be called every frame.

This code snippet would animate the font size of a text widget from 0.0 to 30.0 over 2.0 seconds:

let font_size = context.use_state(0.0);;

Animator::new(
    Duration::from_millis(2000),
    Text::new("Hello World!".to_string()).with_font_size(font_size.maybe()),
    move |_, f| {
        // `f` is a value between `0.0` and `1.0` based on the time passed.
        font_size.set(f * 30.0);

        Update::DRAW
    },
)

For a full example, see the animation example.

Advanced Maycoon

There is more to Maycoon than widgets and signals. You can explore the entire framework by reading through this guide.

Components

Components can be used to easily compose widgets out of already existing ones without having to write your own Widget implementation.

They look similar to Applications but can be used as seperate widgets.

See the following code snippet for a counter component:

pub struct Counter {
    counter: ArcSignal<i32>,
    layout: MaybeSignal<LayoutStyle>,
}

impl Counter {
    pub fn new(counter: ArcSignal<i32>) -> Composed<Self> {
        Counter {
            counter,
            layout: LayoutStyle::default().into(),
        }
        .compose()
    }
}

impl Component for Counter {
    fn build(&self, context: AppContext) -> impl Widget + 'static {
        let counter = self.counter.clone();

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

                Box::new(
                    Button::new(Text::new("Increase".to_string())).with_on_pressed(
                        EvalSignal::new(move || {
                            counter.set(*counter.get() + 1);
                            Update::DRAW
                        })
                        .hook(&context)
                        .maybe(),
                    ),
                )
            },
            {
                let counter = counter.clone();

                Box::new(
                    Button::new(Text::new("Decrease".to_string())).with_on_pressed(
                        EvalSignal::new(move || {
                            counter.set(*counter.get() - 1);
                            Update::DRAW
                        })
                        .hook(&context)
                        .maybe(),
                    ),
                )
            },
            Box::new(Text::new(
                MaybeSignal::signal(counter).map(|i| Ref::Owned(i.to_string())),
            )),
        ])
        .with_layout_style(self.layout.get().clone())
    }

    fn widget_id(&self) -> WidgetId {
        WidgetId::new("my-example", "Counter")
    }
}

impl WidgetLayoutExt for Counter {
    fn set_layout_style(&mut self, layout_style: impl Into<MaybeSignal<LayoutStyle>>) {
        self.layout = layout_style.into();
    }
}

Notice that the Counter::new method returns a Composed type which is a wrapper to turn the component into an actual widget. It's recommended to always return Composed<...> for cleaner code usage.

Composed also allows you to call methods of the inner component using deref coercion which is why Counter::new(...).with_layout_style(...) is valid code, even though it doesn't return a Counter instance.

See the component example for full usage.

Plugins

Maycoon supports plugins to extend its capabilities. They must be specified at the Application level using the plugins method:

struct MyApp;

impl Application for MyApp {
    type Theme = CelesteTheme;
    type State = ();

    fn build(_: AppContext, _: Self::State) -> impl Widget {
        todo!("Your code")
    }

    fn config(&self) -> MayConfig<Self::Theme> {
        todo!("Your code")
    }

    fn plugins(&self) -> PluginManager<Self::Theme> {
        let mut plugins = PluginManager::new();

        plugins.register(MyPlugin);

        plugins
    }
}

Plugins are not only used to extend the framework, but also directly manipulate the internal state of the application.

A simple plugin may look like this:

pub struct MyPlugin;

impl<T: Theme> Plugin<T> for MyPlugin {
    fn name(&self) -> &'static str {
        "my_plugin"
    }

    fn on_register(&mut self, _manager: &mut PluginManager<T>) {
        println!("Hello World!");
    }

    fn on_unregister(&mut self, _manager: &mut PluginManager<T>) {
        println!("Bye World!");
    }
}

See the plugin example for full usage.