Making the Counter Reusable

In this section we're going to turn our counter into a component by declaring a custom view. This will make our counter reusable so we can easily create multiple instances or export the counter as a component in a library.

Step 1: Creating a custom view struct

First we declare a struct which will contain any view-specific state:

pub struct Counter {}

Although we could store the count value within the view, we've chosen instead to make this view 'stateless', and instead we'll provide it with a lens to bind to some external state (typically from a model), and some callbacks for emitting events when the buttons are pressed.

Step 2: Implementing the view trait

Next, we'll implement the View trait for the custom counter view:

impl View for Counter {}

The View trait has methods for responding to events and for custom drawing, but for now we'll leave this implementation empty.

Step 3: Building the sub-components of the view

Next we'll implement a constructor for the counter view. To use our view in a vizia application, the constructor must build the view into the context, which returns a Handle we can use to apply modifiers on our view.

impl Counter {
    pub fn new(cx: &mut Context) -> Handle<Self> {
        Self {

        }.build(cx, |cx|{

        })
    }
}

The build() function, provided by the View trait, takes a closure which we can use to construct the content of the custom view. Here we move the code which makes up the counter:

impl Counter {
    pub fn new(cx: &mut Context) -> Handle<Self> {
        Self {

        }.build(cx, |cx|{
            HStack::new(cx, |cx|{
                Button::new(cx, |cx| Label::new(cx, "Decrement"))
                    .on_press(|ex| ex.emit(AppEvent::Decrement))
                    .class("dec");

                Button::new(cx, |cx| Label::new(cx, "Increment"))
                    .on_press(|ex| ex.emit(AppEvent::Increment))
                    .class("inc");
                
                Label::new(cx, AppData::count)
                    .class("count");
            })
            .child_space(Stretch(1.0))
            .col_between(Pixels(20.0));
        })
    }
}

Step 4: User-configurable binding

The label within the counter is currently using the AppData::count lens, however, this will only work if that specific lens is in scope. To make this component truly reusable we need to pass a lens in via the constructor. To do this we use a generic and constrain the type to implement the Lens trait. This trait has a Target associated type which we can use to specify that the binding is for an i32 value. Then we simply pass the lens directly to the constructor of the label:

impl Counter {
    pub fn new<L>(cx: &mut Context, lens: L) -> Handle<Self> 
    where
        L: Lens<Target = i32>,
    {
        Self {

        }.build(cx, |cx|{
            HStack::new(cx, |cx|{
                Button::new(cx, |cx| Label::new(cx, "Decrement"))
                    .on_press(|ex| ex.emit(AppEvent::Decrement))
                    .class("dec");

                Button::new(cx, |cx| Label::new(cx, "Increment"))
                    .on_press(|ex| ex.emit(AppEvent::Increment))
                    .class("inc");
                
                Label::new(cx, lens)
                    .class("count");
            })
            .child_space(Stretch(1.0))
            .col_between(Pixels(20.0));
        })
    }
}

Step 5 - User-configurable events

The last part required to make the counter truly reusable is to remove the dependency on AppEvent. To do this we'll add a couple of callbacks to the counter to allow the user to emit their own events when the buttons are presses.

Adding callbacks to the view

First, change the Counter struct to look like this:

pub struct Counter {
    on_increment: Option<Box<dyn Fn(&mut EventContext)>>,
    on_decrement: Option<Box<dyn Fn(&mut EventContext)>>,
}

These boxed function pointers provide the callbacks that will be called when the increment and decrement buttons are pressed.

Custom modifiers

Next we'll need to add some custom modifiers so the user can configure these callbacks. To do this we can define a trait and implement it on Handle<'_, Counter>:

pub trait CounterModifiers {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self;
}

We can use the modify() method on Handle to directly set the callbacks when implementing the modifiers:

impl<'a> CounterModifiers for Handle<'a, Counter> {
    fn on_increment<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_increment = Some(Box::new(callback)))
    }

    fn on_decrement<F: Fn(&mut EventContext) + 'static>(self, callback: F) -> Self {
        self.modify(|counter| counter.on_decrement = Some(Box::new(callback)))
    }
}

Internal event handling

Unfortunately we can't just call these callbacks from the action callback of the buttons. Instead we'll need to emit some internal events which the counter can receive, and then the counter can call the callbacks. Define an internal event enum for the counter like so:

pub enum CounterEvent {
    Decrement,
    Increment,
}

We can then use this internal event with the buttons:

Button::new(
    cx, 
    |ex| ex.emit(CounterEvent::Decrement), 
    |cx| Label::new(cx, "Decrement")
)
.class("dec");

Button::new(
    cx, 
    |ex| ex.emit(CounterEvent::Increment), 
    |cx| Label::new(cx, "Increment")
)
.class("inc");

Finally, we respond to these events in the event() method of the View trait for the Counter, calling the appropriate callback:

impl View for Counter {
    fn event(&mut self, cx: &mut EventContext, event: &mut Event) {
        event.map(|counter_event, meta| match counter_event{
            CounterEvent::Increment => {
                if let Some(callback) = &self.on_increment {
                    (callback)(cx);
                }
            }

            CounterEvent::Decrement => {
                if let Some(callback) = &self.on_decrement {
                    (callback)(cx);
                }
            }
        });
    }
}

To recap, now when the user presses on one of the buttons, the button will emit an internal CounterEvent, which is then handled by the Counter view to call the appropriate callback, which the user can set using the custom modifiers we added using the CounterModifiers trait.

Step 6: Using the custom view

Finally, we can use our custom view in the application:

fn main() {
    Application::new(|cx|{

        cx.add_stylesheet(include_style!("src/style.css"))
            .expect("Failed to load stylesheet");

        AppData { count: 0 }.build(cx);

        Counter::new(cx, AppData::lens)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

We pass it the AppData::lens, but the custom view can accept any lens to an i32 value. We also provide it with callbacks that should trigger when the increment and decrement buttons are pressed. In this case the callbacks will emit AppEvent events to mutate the model data.

When we run our app now it will seem like nothing has changed. However, now that our counter is a component, we could easily add multiple counters all bound to the same data (or different data):

fn main() {
    Application::new(|cx|{

        cx.add_stylesheet(include_style!("src/style.css"))
            .expect("Failed to load stylesheet");

        AppData { count: 0 }.build(cx);

        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
        Counter::new(cx, AppData::count)
            .on_increment(|cx| cx.emit(AppEvent::Increment))
            .on_decrement(|cx| cx.emit(AppEvent::Decrement));
    })
    .title("Counter")
    .inner_size((400, 150))
    .run();
}

Vizia app with three counter components