Skip to content

Commit

Permalink
Creating A Button widget
Browse files Browse the repository at this point in the history
We’re going to take the same approach that we took to wrapping our menu with the nine-patch green square and apply it to our buttons.

We’re also going to also build up some extra logic for swapping images when the button is clicked.

To do this we’ll create our own button widget. The button widget will turn our `GameMenu` usage into this. Each `Button` from before becomes a `SnakeButton` from the `button` submodule. All of the styles are in the `SnakeButton` widget, and the buttons can still accept event handlers.

```rust
rsx! {
    <If condition={show_menus}>
         <NinePatch
             styles={Some(container_styles)}
             border={Edge::all(10.0)}
             handle={container}
         >
             <button::SnakeButton
                 on_event={Some(on_click_new_game)}
                 >
                 <Text
                     size={20.0}
                     content={"New Game".to_string()}
                 />
             </button::SnakeButton>
             <button::SnakeButton
                 on_event={Some(on_click_settings)}
                 >
                 <Text
                     size={20.0}
                     content={"Settings".to_string()}
                 />
             </button::SnakeButton>
             <button::SnakeButton
                 on_event={Some(on_click_exit)}
                 >
                 <Text
                     size={20.0}
                     content={"Exit".to_string()}
                 />
             </button::SnakeButton>
         </NinePatch>
     </If>
 }
```

In `ui.rs` create a new button submodule. This does not need to be public as we won’t be sharing it outside of the ui module.

```rust
mod button;
```

The file we create for our button submodule will be in `src/ui/button.rs`.

Our button widget will be called `SnakeButton` and it’s declared in a similar way to the `GameMenu` widget. The biggest difference is that our `SnakeButton` widget needs to accept props, so we create an additional struct to define what props it takes.

We need to derive `WidgetProps` as well as a few other more familiar traits, and use `prop_field` on the props we’re defining. An `Option` means that we don’t always have to supply this prop. Only a [specific subset](https://github.com/StarArawn/kayak_ui/blob/7649c636c432f20a3888491f9e8e2cc4a23d6e3d/kayak_render_macros/src/widget_props.rs#L13-L17) of the props need this `prop_field` macro: the ones that Kayak defines for us. If we had a `String` prop it wouldn’t need the macro.

Any prop we’re going to use when we use the `SnakeButton` needs to be `pub`.

The widget’s return value is going to be a `NinePatch` instead of a button. This will let us use a nine-patch image as the background of our button. This image handle will also get swapped out when we `MouseDown` on the button.

We could put the `Text` component in our button as well, instead of accepting arbitrary children, but I’ve learned in my web-based work that defining just the container and allowing consumers to put whatever they want in that container is generally the better move for a re-usable component.

```rust
pub struct SnakeButtonProps {
    #[prop_field(Styles)]
    pub styles: Option<Style>,
    #[prop_field(OnEvent)]
    pub on_event: Option<OnEvent>,
    #[prop_field(Children)]
    pub children: Option<kayak_ui::core::Children>,
}

pub fn SnakeButton(props: SnakeButtonProps) {
    ...
    rsx! {
        <NinePatch
            border={Edge::all(24.0)}
            handle={current_button_handle.get()}
            styles={Some(button_styles)}
            on_event={Some(on_event)}
        >
            {children}
        </NinePatch>
    }
}
```

Now that the button is a NinePatch, we also need to grab some of the default Button styles from the Button widget we were using. We’ll also copy in the styles we originally defined for our buttons.

When we `cargo run` later, you’ll notice that the `background_color` is no longer used, even though we set it.

We also take this opportunity to accept any styles the user of the widget might have set (or use the defaults) by using struct update syntax (`..`). This is a lot like a spreading an object in JavaScript, although not exactly the same.

```rust
pub fn SnakeButton(props: SnakeButtonProps) {
    let button_styles = Style {
        background_color: StyleProp::Value(Color::BLACK),
        height: StyleProp::Value(Units::Pixels(50.0)),
        width: StyleProp::Value(Units::Pixels(200.0)),
        padding_top: StyleProp::Value(Units::Stretch(1.0)),
        padding_bottom: StyleProp::Value(Units::Stretch(
            1.0,
        )),
        padding_left: StyleProp::Value(Units::Stretch(1.0)),
        padding_right: StyleProp::Value(Units::Stretch(
            1.0,
        )),
        cursor: CursorIcon::Hand.into(),
        ..props.styles.clone().unwrap_or_default()
    };
...
}
```

We’re going to use the exact same method of getting the image assets that we used for the last NinePatch. The biggest difference here is that we’re using tuples to return multiple handles and that we’ve given `image_manager` it’s own variable so we can call `.get` twice instead of once.

These will give us the images we use for the regular button state as well as the mouse-down state.

```rust
let (blue_button09, blue_button10) = context
    .query_world::<Res<ImageAssets>, _, _>(|assets| {
        (
            assets.blue_button09.clone(),
            assets.blue_button10.clone(),
        )
    });

let (blue_button_handle, blue_button_hover_handle) =
    context
        .get_global_mut::<World>()
        .map(|mut world| {
            let mut image_manager = world
                .get_resource_mut::<ImageManager>()
                .unwrap();
            (
                image_manager.get(&blue_button09),
                image_manager.get(&blue_button10),
            )
        })
        .unwrap();
```

If you’ve ever worked with state in a modern web framework, you’ll recognize this as something similar to `use_state` in React or similar concepts in other frameworks.

We create some state that Kayak will handle binding to for us. The state type is a `u16` and the original value we set is the `blue_button_handle`.

We can change this value now and the component will react to the changed state.

```rust
let current_button_handle = context
    .create_state::<u16>(blue_button_handle)
    .unwrap();
```

Next we have our event handlers.

The event handler closure we construct will outlive the function we’re defining it in because we give it to the `NinePatch` widget which will call it... sometime, whenever the user clicks the button.

This is a problem because the variables we’re using like `cloned_current_button_handle` and `parent_on_event` will only live until the end of this function and the closure, which lives longer, is still trying to reference them.

We can use the `move` keyword to *force* the closure to take ownership of the variables it’s using which solves this problem. This is why we’ve cloned these values as well, because we’ll be sending the clones off to live with the closure forever.

If we didn’t clone them then, in the case of `current_button_handle`, we wouldn’t be able to use it after we moved it. In the case of `on_event`, it’s a partial move because it lives inside of the props struct, which means that we wouldn’t be able to use the `props` struct later.

If we weren’t using these values later in our `rsx` macro, then we could freely move them without cloning.

```rust
let cloned_current_button_handle =
    current_button_handle.clone();
let parent_on_event = props.on_event.clone();
let on_event = OnEvent::new(move |ctx, event| {
    match event.event_type {
        EventType::MouseDown(..) => {
            cloned_current_button_handle.set(blue_button_hover_handle);
        }
        EventType::MouseUp(..) => {
            cloned_current_button_handle.set(blue_button_handle);
        }
        EventType::Click(..) => {
            match &parent_on_event {
                Some(v) => v.try_call(ctx, event),
                None => todo!(),
            };
        }
        _ => (),
    }
});
```

Finally, we need to apply the user’s desired children to the button. We have to use `props.get_children()` outside of the `rsx` macro because of the way the macro works.

```rust
let children = props.get_children();
```

If we `cargo run` now, we see nice chonky clickable buttons in our menu!
  • Loading branch information
ChristopherBiscardi committed Apr 24, 2022
1 parent 5263bdf commit 4f694e4
Show file tree
Hide file tree
Showing 2 changed files with 111 additions and 21 deletions.
29 changes: 8 additions & 21 deletions src/ui.rs
Expand Up @@ -20,13 +20,14 @@ use kayak_ui::{
widget, Binding, Bound, Color, EventType, Index,
MutableBound, OnEvent,
},
widgets::{App, Button, If, NinePatch, Text},
widgets::{App, If, NinePatch, Text},
};

use crate::{
assets::ImageAssets, GameState, STARTING_GAME_STATE,
};

mod button;
pub struct UiPlugin;

impl Plugin for UiPlugin {
Expand Down Expand Up @@ -88,17 +89,6 @@ fn GameMenu() {
..Default::default()
};

let button_styles = Style {
background_color: StyleProp::Value(Color::BLACK),
height: StyleProp::Value(Units::Pixels(50.0)),
width: StyleProp::Value(Units::Pixels(200.0)),
padding_top: StyleProp::Value(Units::Stretch(1.0)),
padding_bottom: StyleProp::Value(Units::Stretch(
1.0,
)),
..Default::default()
};

let show_menus = {
let gamestate = context
.query_world::<Res<Binding<GameState>>, _, _>(
Expand Down Expand Up @@ -164,33 +154,30 @@ fn GameMenu() {
border={Edge::all(10.0)}
handle={container}
>
<Button
<button::SnakeButton
on_event={Some(on_click_new_game)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"New Game".to_string()}
/>
</Button>
<Button
</button::SnakeButton>
<button::SnakeButton
on_event={Some(on_click_settings)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"Settings".to_string()}
/>
</Button>
<Button
</button::SnakeButton>
<button::SnakeButton
on_event={Some(on_click_exit)}
styles={Some(button_styles)}
>
<Text
size={20.0}
content={"Exit".to_string()}
/>
</Button>
</button::SnakeButton>
</NinePatch>
</If>
}
Expand Down
103 changes: 103 additions & 0 deletions src/ui/button.rs
@@ -0,0 +1,103 @@
use bevy::prelude::{Res, World};
use kayak_ui::{
bevy::ImageManager,
core::{
rsx,
styles::{Edge, Style, StyleProp, Units},
widget, Bound, Color, CursorIcon, EventType,
MutableBound, OnEvent, WidgetProps,
},
widgets::NinePatch,
};

use crate::assets::ImageAssets;

#[derive(WidgetProps, Clone, Debug, Default, PartialEq)]
pub struct SnakeButtonProps {
#[prop_field(Styles)]
pub styles: Option<Style>,
#[prop_field(OnEvent)]
pub on_event: Option<OnEvent>,
#[prop_field(Children)]
pub children: Option<kayak_ui::core::Children>,
}

#[widget]
pub fn SnakeButton(props: SnakeButtonProps) {
let button_styles = Style {
background_color: StyleProp::Value(Color::BLACK),
height: StyleProp::Value(Units::Pixels(50.0)),
width: StyleProp::Value(Units::Pixels(200.0)),
padding_top: StyleProp::Value(Units::Stretch(1.0)),
padding_bottom: StyleProp::Value(Units::Stretch(
1.0,
)),
padding_left: StyleProp::Value(Units::Stretch(1.0)),
padding_right: StyleProp::Value(Units::Stretch(
1.0,
)),
cursor: CursorIcon::Hand.into(),
..props.styles.clone().unwrap_or_default()
};

let (blue_button09, blue_button10) = context
.query_world::<Res<ImageAssets>, _, _>(|assets| {
(
assets.blue_button09.clone(),
assets.blue_button10.clone(),
)
});

let (blue_button_handle, blue_button_hover_handle) =
context
.get_global_mut::<World>()
.map(|mut world| {
let mut image_manager = world
.get_resource_mut::<ImageManager>()
.unwrap();
(
image_manager.get(&blue_button09),
image_manager.get(&blue_button10),
)
})
.unwrap();

let current_button_handle = context
.create_state::<u16>(blue_button_handle)
.unwrap();

let cloned_current_button_handle =
current_button_handle.clone();
let parent_on_event = props.on_event.clone();
let on_event = OnEvent::new(move |ctx, event| {
match event.event_type {
EventType::MouseDown(..) => {
cloned_current_button_handle
.set(blue_button_hover_handle);
}
EventType::MouseUp(..) => {
cloned_current_button_handle
.set(blue_button_handle);
}
EventType::Click(..) => {
match &parent_on_event {
Some(v) => v.try_call(ctx, event),
None => todo!(),
};
}
_ => (),
}
});

let children = props.get_children();
rsx! {
<NinePatch
border={Edge::all(24.0)}
handle={current_button_handle.get()}
styles={Some(button_styles)}
on_event={Some(on_event)}
>
{children}
</NinePatch>
}
}

0 comments on commit 4f694e4

Please sign in to comment.