Skip to content

Commit

Permalink
Make counter component actually reusable
Browse files Browse the repository at this point in the history
  • Loading branch information
geom3trik committed Nov 13, 2023
1 parent b6dcb7f commit cae4b57
Show file tree
Hide file tree
Showing 3 changed files with 170 additions and 18 deletions.
122 changes: 113 additions & 9 deletions src/quickstart/components.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ 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).
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:
Expand All @@ -21,7 +21,7 @@ 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
Then 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.
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.

```rust
impl Counter {
Expand Down Expand Up @@ -67,8 +67,9 @@ impl Counter {
}
}
```
## Step 4: User-configurable binding

The label 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:
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:

```rust
impl Counter {
Expand Down Expand Up @@ -103,7 +104,102 @@ impl Counter {
}
}
```
## Step 4: Using the custom view

## 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:

```rust
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>`:

```rust
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:

```rust
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:

```rust
pub enum CounterEvent {
Decrement,
Increment,
}
```

We can then use this internal event with the buttons:
```rust
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:

```rust
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:

```rust
Expand All @@ -114,7 +210,9 @@ fn main() {

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

Counter::new(cx, AppData::lens);
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))
Expand All @@ -123,7 +221,7 @@ fn main() {

```

We pass it the `AppData::lens`, but the custom view can accept any lens to an `i32` value.
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):

Expand All @@ -136,9 +234,15 @@ fn main() {

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

Counter::new(cx, AppData::count);
Counter::new(cx, AppData::count);
Counter::new(cx, AppData::count);
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))
Expand Down
62 changes: 55 additions & 7 deletions src/quickstart/final_code.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,27 +27,31 @@ impl Model for AppData {
}

// Define a custom view for the counter
pub struct Counter {}

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

impl Counter {
pub fn new<L>(cx: &mut Context, lens: L) -> Handle<Self>
where
L: Lens<Target = i32>
{
Self {}.build(cx, |cx|{
Self {
on_decrement: None,
on_increment: None,
}.build(cx, |cx|{
HStack::new(cx, |cx|{
Button::new(
cx,
|ex| ex.emit(AppEvent::Decrement),
|ex| ex.emit(CounterEvent::Decrement),
|cx| Label::new(cx, Localized::new("dec")),
)
.class("dec");

Button::new(
cx,
|ex| ex.emit(AppEvent::Increment),
|ex| ex.emit(CounterEvent::Increment),
|cx| Label::new(cx, Localized::new("inc")),
)
.class("inc");
Expand All @@ -61,6 +65,48 @@ impl Counter {
}
}

// Internal events
pub enum CounterEvent {
Decrement,
Increment,
}

// Handle internal events
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);
}
}
});
}
}

// Custom modifiers
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;
}

// Implement custom 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)))
}
}

fn main() {
Application::new(|cx|{
// Add CSS stylesheet
Expand All @@ -84,7 +130,9 @@ fn main() {
AppData { count: 0 }.build(cx);

// Add the custom counter view and bind to the model data
Counter::new(cx, AppData::count);
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))
Expand Down
4 changes: 2 additions & 2 deletions src/quickstart/localization.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,13 +62,13 @@ To localize the text in our application we use the `Localized` type within the l
```rust
Button::new(
cx,
|ex| ex.emit(AppEvent::Decrement),
|ex| ex.emit(CounterEvent::Decrement),
|cx| Label::new(cx, Localized::new("dec")),
);

Button::new(
cx,
|ex| ex.emit(AppEvent::Increment),
|ex| ex.emit(CounterEvent::Increment),
|cx| Label::new(cx, Localized::new("inc")),
);
```
Expand Down

0 comments on commit cae4b57

Please sign in to comment.