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 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:
// 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:
Text::new("Hello, World!".to_string())
See the hello world example for full usage.
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:
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
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
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
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
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
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 Application
s 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.