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 Button widget, containing a Text widget which displays the text "Increase".
  • A Button widget, containing a Text widget which displays the text "Decrease".
  • A Text widget 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.