In my opinion, Bevy’s UI solution has long been a point of struggle. UI definitions are written in compiled Rust, which means you have to go through a compile cycle each time you intend to test a UI change. This is unconventional for UI solutions, which typically allow you to hot-reload (JS/TS) or at least avoid compilation cycles to test changes, only requiring reloads (Unity as I remember it)
However, bevy_cobweb_ui is a crate that allows you to specify UI in a format called .cob which turns into standard Bevy UI under the hood at runtime, and is even hot reloadable. It also inherits an animation (aka easing) system from an older Bevy UI solution called sickle_ui. Being able to specify these animations in .cob is a step even above standard Unity.
In my Bevy game Mercator I make heavy use of this system. You can download the demo right now if you wish to see the UI in action. The .cob files involved in the UI are also readily available from the assets folder attached to the zip file.
Below I’ve described a few UI elements and how I made them:
Screen Recording 2025-03-15 at 2.40.16 PM.mov
I was trying to go for an effect that makes it easier to identify which “item box” you are pointing your cursor at. Here I make use of two tricks: pseudo-state switching and transform rotation.
From assets/item_box.cob:
Multi<Animated<Height>>[
{
state:[Disabled]
idle:$idle_size
hover:$idle_size
hover_with:{ duration:$duration ease:OutCubic delay:$delay}
unhover_with:{ duration:$duration ease:OutCubic delay:$delay}
},
{
state:[Checked]
idle:$idle_size
hover:$hover_size
hover_with:{ duration:$duration ease:OutCubic delay:$delay}
unhover_with:{ duration:$duration ease:OutCubic delay:$delay}
}
]
Step by step on what this code represents:
Height represents the y-axis size of the node. It can be specified in Cobweb UI both as part of FlexNode/AbsoluteNode or as a standalone attribute as done here.Animated represents the ability to specify different values for different “real states” the node finds itself in, such as idle or hover. When it transitions from one of these “real states” to the other, it does so using hover_with or unhover_with. You can specify how long the animation should take, a delay before it begins, and the easing type (here OutCubic causes the majority of the height increase to happen nearly instantly before a taper-off)Multi represents the ability to specify different animations for different pseudo-states. These are not manipulated directly by actions on screen (such as hovering or clicking) but can instead be specified in code. Using PseudoStateParam and Commands you can set or unset pseudo-states of a given entity at any point in time. Here the box is generally in the “checked” pseudo-state, but sometimes I switch the boxes to the “disabled” pseudo-state when the animation isn’t desired.b. Transform rotation
Rotation (to my knowledge) can’t be easily manipulated this way, instead you’ll need to use the fact that Cobweb UI is just Bevy UI under the hood:
pub fn item_box_hover_cosmetic<'a>(l: &mut LoadedScene<'a, UiBuilder<'a, Entity>>) {
let e_id = l.id();
l.on_pointer_enter(
move |mut c: Commands, mut t_q: Query<&mut Transform, With<ItemBox>>| {
let Ok(mut t) = t_q.get_mut(e_id) else {
return;
};
bounce_box(&mut t);
c.trigger(SpawnSound::new("music/hover.ogg"));
},
);
}
pub fn bounce_box_factor(t: &mut Transform, factor: f32) {
t.rotation =
Quat::from_rotation_z(factor * (0.4 * if fastrand::bool() { -1. } else { 1. }) * PI / 2.);
}