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 modules. You can add features by editing
the Cargo.toml
like this:
[package]
name = "my_project"
description = "My Awesome App"
# other stuff...
[dependencies]
maycoon = { version = "*", features = ["vg"] }
Following 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.
Setup
For creating a new project, see the Installation Guide.
We need to enable the macros
feature, which is enabled by default, so you shouldn't worry about it.
The App
First we need to import the necessary items from the maycoon
crate:
use maycoon::core::app::update::Update; use maycoon::core::app::MayApp; use maycoon::core::config::MayConfig; use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; use maycoon::macros::{val, State}; use maycoon::math::Vector2; use maycoon::widgets::button::Button; use maycoon::widgets::container::Container; use maycoon::widgets::text::Text; #[derive(State)] struct MyState { count: i32, } fn main() { MayApp::new(MayConfig::default()).run( MyState { count: 0 }, Container::new(vec![ Box::new( Button::new(Text::new("Increase".to_string())).with_on_pressed( |state: &mut MyState| { state.count += 1; Update::DRAW }, ), ), Box::new( Button::new(Text::new("Decrease".to_string())).with_on_pressed( |state: &mut MyState| { state.count -= 1; Update::DRAW }, ), ), Box::new(Text::new(val!(|state: &MyState| state.count.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() }), ); }
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.
We use the val!()
macro to automatically create a Val
from a closure.
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 App surface.
The with_layout_style
function applies a custom layout which centers the widgets in our case.
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 pop up.
Configuration
You can configure your app using the MayConfig
struct just like this:
fn main() { let config = MayConfig { window: WindowConfig { title: "New App".to_string(), // Window Title size: Vector2::new(800.0, 600.0), // Window Size min_size: None, // Min Window Size max_size: None, // Max Window Size resizable: true, // If the window is resizeable maximized: false, // If the window is maximized on startup mode: WindowMode::default(), // The window mode (windowed, fullscreen or exclusive) level: Default::default(), // The window level (always top, bottom, normal) visible: true, // If the window is visible on startup blur: false, // If the background of the window should be blurred transparent: false, // If the window background should be transparent position: None, // The position (or None if you don't care) active: true, // If the window should be active (in focus) on startup buttons: WindowButtons::all(), // The enabled window buttons decorations: true, // If the window should have decorations/borders corners: Default::default(), // The corner configuration resize_increments: None, // Optional resize increments content_protected: false, // If the content should be protected icon: None, // Optional window icon cursor: Cursor::default(), // The cursor close_on_request: true, // If the window should close when requested via window button }, renderer: RenderConfig { antialiasing: AaConfig::Area, // The Anti-Aliasing method cpu: false, // If the App should partially use the CPU (still requires a valid GPU) to render present_mode: PresentMode::AutoNoVsync, // The buffer presentation mode }, theme: MyAwesomeTheme, // The App Theme }; // your code... }
State Management
What is state management?
When executing an application, we can't store and retrieve values from frame to frame.
To actually store and access data between frames, we need a global application state structure.
In Maycoon, we can use #[derive(State)]
to automatically implement the State
trait for a struct.
#![allow(unused)] fn main() { #[derive(State)] struct MyState { my_data: MyDataType, } }
Managing a state.
There are multiple implementation for handling application states among different frameworks, but Maycoon uses its own kind of state management system.
Every value inside a widget (such as a text) should be wrapped inside a Val<T>
where T is the data type.
The Val<T>
itself is an enum and has two variants:
Val::State { factory, .. }
for state dependent values.Val::Val(T)
for state independent values.
State dependent values
State dependent means, you need access to the global application State
to produce a value.
This might be a simple field (state.value
) or a more complex operation (e.g. retrieving something from a database).
To wrap a state dependent value inside a State
you can use the Val::new_state()
function, but we recommend using the val!()
macro to create a state value like this:
#![allow(unused)] fn main() { // simple val!(|state: MyState| state.counter); // more complex val!(|state: MyState| { let value = state.input; let life = meaning_of_life(value); return if life.is_some() { "life has meaning".to_string() } else { "life has no meaning".to_string() } }); // or just using the `new_state` constructor Val::new_state(|state: MyState| state.counter + 1); }
State independent values
State independent means, you do not need to access the global application State
to produce a value.
This means that you can use literals or other "constants" or simply state independent values.
You can create these values using the Val::new_val
, the val!()
macro or simply cast a value into a Val
like this:
#![allow(unused)] fn main() { // constructor Val::new_val(3); // `val!()` macro val!("Hello World".to_string()); // cast `into()` the `Val` 103.into(); }
State Mutability
When using Val
to access data, a State
is always borrowed as immutable variable. This means you only have read-access to the inner value.
If you want to make a widget with mutable state (like an on_click
handler), you can use a custom Fn(&mut State)
and call it in your update
function, when creating a widget.
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::MayApp; use maycoon::core::config::MayConfig; use maycoon::macros::{val, State}; use maycoon::widgets::text::Text; #[derive(State)] struct MyState; fn main() { MayApp::new(MayConfig::default()) .run(MyState, Text::new("Hello, World!".to_string())); }
Button
The button widget is probably one of the most essential things in all of the UI making world.
You can create buttons using
#![allow(unused)] fn main() { Button::new( Text::new("Hello".to_string()) ).with_on_pressed(|state: &mut MyState| { println!("Hello World"); Update::DRAW }) }
For a more complex example, see the counter example:
use maycoon::core::app::update::Update; use maycoon::core::app::MayApp; use maycoon::core::config::MayConfig; use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; use maycoon::core::state::Val; use maycoon::macros::{val, State}; use maycoon::math::Vector2; use maycoon::widgets::button::Button; use maycoon::widgets::container::Container; use maycoon::widgets::text::Text; #[derive(State)] struct MyState { count: i32, } fn main() { MayApp::new(MayConfig::default()).run( MyState { count: 0 }, Container::new(vec![ Val::new_val(Box::new( Button::new(Text::new("Increase".to_string())).with_on_pressed( |state: &mut MyState| { state.count += 1; Update::DRAW }, ), )), Val::new_val(Box::new( Button::new(Text::new("Decrease".to_string())).with_on_pressed( |state: &mut MyState| { state.count -= 1; Update::DRAW }, ), )), Val::new_val(Box::new(Text::new(val!(|state: &MyState| state .count .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() }), ); }
Container
A container is not a real UI widget that displays something, but a list of widget aligned in a certain way.
See the counter example for basic usage:
use maycoon::core::app::update::Update; use maycoon::core::app::MayApp; use maycoon::core::config::MayConfig; use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; use maycoon::core::state::Val; use maycoon::macros::{val, State}; use maycoon::math::Vector2; use maycoon::widgets::button::Button; use maycoon::widgets::container::Container; use maycoon::widgets::text::Text; #[derive(State)] struct MyState { count: i32, } fn main() { MayApp::new(MayConfig::default()).run( MyState { count: 0 }, Container::new(vec![ Val::new_val(Box::new( Button::new(Text::new("Increase".to_string())).with_on_pressed( |state: &mut MyState| { state.count += 1; Update::DRAW }, ), )), Val::new_val(Box::new( Button::new(Text::new("Decrease".to_string())).with_on_pressed( |state: &mut MyState| { state.count -= 1; Update::DRAW }, ), )), Val::new_val(Box::new(Text::new(val!(|state: &MyState| state .count .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() }), ); }
Checkbox
The checkbox widget is used to toggle a value, which should be bound to your state.
#![allow(unused)] fn main() { Checkbox::new( val!(|state: &MyState| state.checked) ).with_on_change(|state| { state.checked = !state.checked; Update::DRAW }) }
or the full example:
use maycoon::core::app::update::Update; use maycoon::core::app::MayApp; use maycoon::core::config::MayConfig; use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle}; use maycoon::core::state::Val; use maycoon::macros::{val, State}; use maycoon::math::Vector2; use maycoon::widgets::checkbox::Checkbox; use maycoon::widgets::container::Container; use maycoon::widgets::text::Text; #[derive(State)] struct MyState { checked: bool, } fn main() { MayApp::new(MayConfig::default()).run( MyState { checked: false }, Container::new(vec![ Val::new_val(Box::new( Checkbox::new(val!(|state: &MyState| state.checked)).with_on_change(|state| { state.checked = !state.checked; Update::DRAW }), )), Val::new_val(Box::new(Text::new(val!( |state: &MyState| if state.checked { "Checked".to_string() } else { "Unchecked".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() }), ); }