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 counter.
Setup
For creating a new project, see the Installation Guide.
The Struct
Every Maycoon App starts with an implementation of Application.
We will create an empty struct to represent our app.
struct MyApp;
impl Application for MyApp {
// implementation goes here...
}
The Types
Your application requires some fundamental types to be defined.
// ...
impl Application for MyApp {
type Theme = CelesteTheme;
type Graphics = DefaultGraphics;
type State = ();
}
The Theme type specifies the Theme we want to use. A theme object is responsible for styling the whole UI and must be
specified at compile time.
The Graphics type specifies which graphics backend to use. We'll stick with the default one for now.
The State type allows us to specify a global application state. You could use an integer as state to represent the
counter, but we will handle the counter function later in another way, so we'll just use an empty tuple.
The UI
Since our application is supposed to do something, we need to add some UI logic.
impl Application for MyApp {
// ...
fn build(context: AppContext, _: Self::State) -> impl Widget {
Container::new(vec![
Box::new(Button::new(Text::new("Increase".to_string()))),
Box::new(Button::new(Text::new("Decrease".to_string()))),
Box::new(Text::new(/* counter? */)),
]).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()
})
}
}
You can see, that we implemented a method called build that returns a Widget.
This is our entry point to the actual application lifecycle. We want three widgets:
- An increase button
- A decrease button
- A text widget to display the counter
We use a Container to contain all of these widgets. You can think of this Container as a <div> in HTML.
Inside the Container, we define a vector of boxed widgets:
- A
Buttonwidget, containing aTextwidget which displays the text "Increase". - A
Buttonwidget, containing aTextwidget which displays the text "Decrease". - A
Textwidget to display the counter.
We also define a LayoutStyle for the Container to make it fill the whole screen and center its children.
The logic
So far, our UI doesn't contain any actual logic.
To fix this, we add a signal to our build method:
// ...
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.mutate(|c| *c += 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.mutate(|c| *c -= 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()
})
}
// ...
Wow. A lot to unpack here.
Let's start with the counter variable. It creates a state signal (simplest form of a signal) with an initial value of
0.
We could just create one with StateSignal::new(...), but we would need to hook it into the application lifecycle which
context.use_state(...) already does.
Then we create an EvalSignal which is just a fancy closure that gets called and returns data every time it's pressed.
We mutate the counter widget and return Update::DRAW to trigger a redraw (since we just need to redraw the UI and not
re-layout it).
We use the counter signal and map it to a string (in order to use it in our Text).
Note that we clone the counter multiple times. This is required, as of right now, to move ownership of the signal into the closures.
The Config
After the UI logic, we need to define a config for our application.
// ...
impl Application for MyApp {
// ...
fn config(&self) -> MayConfig<Self::Theme, Self::Graphics> {
MayConfig::default()
}
// ...
}
We use the default config for now. See the Configuration section for more information.
The final touch
We can now run the application in our main function.
fn main() {
MyApp.run(());
}
The ´run´ method requires a state of type Self::State as an argument.
We use an empty tuple, as said before.
The Result
Congratulations! This is your first application.
The final code should look like this:
// Of course, we need to import all of the required items
use maycoon::core::app::Application;
use maycoon::core::app::context::AppContext;
use maycoon::core::app::update::Update;
use maycoon::core::config::MayConfig;
use maycoon::core::layout::{AlignItems, Dimension, FlexDirection, LayoutStyle};
use maycoon::core::reference::Ref;
use maycoon::core::signal::Signal;
use maycoon::core::signal::eval::EvalSignal;
use maycoon::core::signal::state::StateSignal;
use maycoon::core::vgi::DefaultGraphics;
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;
type Graphics = DefaultGraphics;
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.mutate(|c| *c += 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.mutate(|c| *c -= 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()
})
}
fn config(&self) -> MayConfig<Self::Theme, Self::Graphics> {
MayConfig::default()
}
}
fn main() {
MyApp.run(())
}
You can now proceed to the State Management section.