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 Box
ed 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 Update
s 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 aRef
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
orAppContext::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
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
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
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() }