We can use the same asset based approach to build out images for our menu system. UI images can have slightly different requirements than game images though, such as needing to fit arbitrary text inside of them for buttons or other UI elements.
In our case, we’ll use a technique called “nine-patch” or “nine-slice” to cut up a square image into 9 boxes: a 3x3 grid.
The four boxes that capture the corners of our image will not scale at all to accommodate content inside the image. This keeps the corners from stretching or skewing in odd ways.
The top and bottom boxes will scale horizontally, the left and right boxes will scale vertically, and the center box that’s left over will scale in any direction.
For our menu, we’ll use this image of a green square with rounded corners to contain the buttons and replace our white background.
In the GameMenu
widget, add the following code. It is responsible for retrieving a Handle
to the green square we just showed from the ImageAssets
resource and giving it to Kayak to manage.
let green_panel = context
.query_world::<Res<ImageAssets>, _, _>(|assets| {
assets.green_panel.clone()
});
let container = context
.get_global_mut::<World>()
.map(|mut world| {
world
.get_resource_mut::<ImageManager>()
.unwrap()
.get(&green_panel)
})
.unwrap();
query_world
accepts three type arguments and a closure, as you can see in the kayak_ui source code here if you wish. We’re using the same turbofish syntax we use when we call .collect::<Vec<String>>()
on an iterator.
The first type argument is the type we’re looking for. In this example we’re looking for the ImageAssets
resource in the same way we would if we were making a bevy Query
.
The second and third type arguments Rust can infer for us here, so we use _
to let that happen. Kayak imposes some requirements on the second type argument to make sure it implements the FnMut
trait, so we can be sure that our closure will implement the required behaviors when the inference happens.
The second type argument is the type of the closure we’re passing in as an argument and the third type argument is the return type of the closure.
In this case the closure we pass in receives the Res<ImageAssets>
as an argument, and returns a handle to the green panel we set up in our asset loading.
After we get the handle, we have to give it to Kayak which will make sure we have a strong handle to our UI, meaning the image is definitely loaded.
We can do this by getting an exclusive reference to the ImageManager
and using get
with our image handle which will perform a get_or_insert type behavior into the ImageManager
's internal hash map.
The ImageManager
can be accessed via an exclusive reference to the World
which context
can get us.
.get_global_mut
lets us pass in the World
as a type argument and returns an Result<ResMut<World>, CantGetResource>
.
If we’re successful in getting an exclusive reference to the world, we can use .map
to work with the world in a limited scope. We could easily store the world
in a variable instead of doing this map, but we also want to make sure we can drop the exclusive references when we’re done, which will happen at the end of the expression.
All of this gets us back a u16
that we can pass to our NinePatch
widget.
We can then use the handle we got from the image manager and specify a border
for the image. This border defines the corner squares of our nine-patch image. The green square is a 100px image and the corner art take up about 10px of that, so we’ll make the corners 10x10 squares and let the rest of the boxes fill in the gaps.
<NinePatch
styles={Some(container_styles)}
border={Edge::all(10.0)}
handle={container}
>
That gives us a green panel behind the buttons that will grow to accommodate any size content while also keeps the correct aspect ratio for the corners.