Animated transforms on hover

This commit is contained in:
Aleksander
2025-06-20 13:06:04 +02:00
parent df320a5c7b
commit a2a7c71c22
7 changed files with 125 additions and 66 deletions

View File

@@ -1,9 +1,9 @@
use glam::FloatExt;
use glam::{FloatExt, Vec2};
use crate::{
event::WidgetCallback,
layout::{WidgetID, WidgetMap},
widget::WidgetObj,
layout::{WidgetID, WidgetMap, WidgetNodeMap},
widget::{WidgetData, WidgetObj},
};
pub enum AnimationEasing {
@@ -36,8 +36,10 @@ impl AnimationEasing {
pub struct CallbackData<'a> {
pub obj: &'a mut dyn WidgetObj,
pub data: &'a mut WidgetData,
pub widgets: &'a WidgetMap,
pub widget_id: WidgetID,
pub widget_size: Vec2,
pub pos: f32, // 0.0 (start of animation) - 1.0 (end of animation)
pub needs_redraw: bool,
pub dirty_nodes: &'a mut Vec<taffy::NodeId>,
@@ -110,29 +112,40 @@ impl Animation {
fn call(
&self,
widgets: &WidgetMap,
widget_map: &WidgetMap,
widget_node_map: &WidgetNodeMap,
tree: &taffy::tree::TaffyTree<WidgetID>,
dirty_nodes: &mut Vec<taffy::NodeId>,
pos: f32,
) -> CallResult {
let mut res = CallResult::default();
if let Some(widget) = widgets.get(self.target_widget).cloned() {
let mut widget = widget.lock().unwrap();
let Some(widget) = widget_map.get(self.target_widget).cloned() else {
return res; // failed
};
let data = &mut CallbackData {
widget_id: self.target_widget,
dirty_nodes,
widgets,
obj: widget.obj.as_mut(),
pos,
needs_redraw: false,
};
let widget_node = widget_node_map.get(self.target_widget);
let layout = tree.layout(widget_node).unwrap(); // should always succeed
(self.callback)(data);
let mut widget = widget.lock().unwrap();
if data.needs_redraw {
res.needs_redraw = true;
}
let (data, obj) = widget.get_data_obj_mut();
let data = &mut CallbackData {
widget_id: self.target_widget,
dirty_nodes,
widgets: widget_map,
widget_size: Vec2::new(layout.size.width, layout.size.height),
obj,
data,
pos,
needs_redraw: false,
};
(self.callback)(data);
if data.needs_redraw {
res.needs_redraw = true;
}
res
@@ -147,7 +160,9 @@ pub struct Animations {
impl Animations {
pub fn tick(
&mut self,
widgets: &WidgetMap,
widget_map: &WidgetMap,
widget_node_map: &WidgetNodeMap,
tree: &taffy::tree::TaffyTree<WidgetID>,
dirty_nodes: &mut Vec<taffy::NodeId>,
needs_redraw: &mut bool,
) {
@@ -163,7 +178,7 @@ impl Animations {
anim.pos_prev = anim.pos;
anim.pos = pos;
let res = anim.call(widgets, dirty_nodes, 1.0);
let res = anim.call(widget_map, widget_node_map, tree, dirty_nodes, 1.0);
if anim.last_tick || res.needs_redraw {
*needs_redraw = true;
@@ -179,14 +194,16 @@ impl Animations {
pub fn process(
&mut self,
widgets: &WidgetMap,
widget_map: &WidgetMap,
widget_node_map: &WidgetNodeMap,
tree: &taffy::tree::TaffyTree<WidgetID>,
dirty_nodes: &mut Vec<taffy::NodeId>,
alpha: f32,
needs_redraw: &mut bool,
) {
for anim in &mut self.running_animations {
let pos = anim.pos_prev.lerp(anim.pos, alpha);
let res = anim.call(widgets, dirty_nodes, pos);
let res = anim.call(widget_map, widget_node_map, tree, dirty_nodes, pos);
if res.needs_redraw {
*needs_redraw = true;
@@ -213,4 +230,4 @@ impl Animations {
}
});
}
}
}

View File

@@ -135,7 +135,7 @@ pub fn construct(
},
)?;
let mut widget = layout.widget_states.get(rect_id).unwrap().lock().unwrap();
let mut widget = layout.widget_map.get(rect_id).unwrap().lock().unwrap();
let button = Arc::new(Button {
body: rect_id,

View File

@@ -156,7 +156,7 @@ fn draw_children(
continue;
};
let Some(widget) = layout.widget_states.get(widget_id) else {
let Some(widget) = layout.widget_map.get(widget_id) else {
debug_assert!(false);
continue;
};
@@ -172,7 +172,7 @@ pub fn draw(layout: &Layout) -> anyhow::Result<Vec<RenderPrimitive>> {
let mut transform_stack = TransformStack::new();
let model = glam::Mat4::IDENTITY;
let Some(root_widget) = layout.widget_states.get(layout.root_widget) else {
let Some(root_widget) = layout.widget_map.get(layout.root_widget) else {
panic!();
};

View File

@@ -18,6 +18,18 @@ use taffy::{TaffyTree, TraversePartialTree};
pub type WidgetID = slotmap::DefaultKey;
pub type BoxWidget = Arc<Mutex<WidgetState>>;
pub type WidgetMap = HopSlotMap<slotmap::DefaultKey, BoxWidget>;
#[derive(Default)]
pub struct WidgetNodeMap(pub HashMap<WidgetID, taffy::NodeId>);
impl WidgetNodeMap {
pub fn get(&self, widget_id: WidgetID) -> taffy::NodeId {
let Some(node) = self.0.get(&widget_id).cloned() else {
// this shouldn't happen!
panic!("node_map is corrupted");
};
node
}
}
struct PushEventState<'a> {
pub animations: &'a mut Vec<animation::Animation>,
@@ -31,8 +43,8 @@ pub struct Layout {
pub assets: Box<dyn AssetProvider>,
pub widget_states: WidgetMap,
pub widget_node_map: HashMap<WidgetID, taffy::NodeId>,
pub widget_map: WidgetMap,
pub widget_node_map: WidgetNodeMap,
pub root_widget: WidgetID,
pub root_node: taffy::NodeId,
@@ -48,47 +60,40 @@ pub struct Layout {
fn add_child_internal(
tree: &mut taffy::TaffyTree<WidgetID>,
widget_node_map: &mut HashMap<WidgetID, taffy::NodeId>,
vec: &mut WidgetMap,
widget_map: &mut WidgetMap,
widget_node_map: &mut WidgetNodeMap,
parent_node: Option<taffy::NodeId>,
widget: WidgetState,
style: taffy::Style,
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
#[allow(clippy::arc_with_non_send_sync)]
let child_id = vec.insert(Arc::new(Mutex::new(widget)));
let child_id = widget_map.insert(Arc::new(Mutex::new(widget)));
let child_node = tree.new_leaf_with_context(style, child_id)?;
if let Some(parent_node) = parent_node {
tree.add_child(parent_node, child_node)?;
}
widget_node_map.insert(child_id, child_node);
widget_node_map.0.insert(child_id, child_node);
Ok((child_id, child_node))
}
impl Layout {
pub fn get_node(&self, widget_id: WidgetID) -> anyhow::Result<taffy::NodeId> {
let Some(node) = self.widget_node_map.get(&widget_id).cloned() else {
anyhow::bail!("invalid parent widget");
};
Ok(node)
}
pub fn add_child(
&mut self,
parent_widget_id: WidgetID,
widget: WidgetState,
style: taffy::Style,
) -> anyhow::Result<(WidgetID, taffy::NodeId)> {
let parent_node = self.get_node(parent_widget_id)?;
let parent_node = self.widget_node_map.get(parent_widget_id);
self.needs_redraw = true;
add_child_internal(
&mut self.tree,
&mut self.widget_map,
&mut self.widget_node_map,
&mut self.widget_states,
Some(parent_node),
widget,
style,
@@ -123,7 +128,7 @@ impl Layout {
let style = self.tree.style(node_id)?;
let Some(widget) = self.widget_states.get(widget_id) else {
let Some(widget) = self.widget_map.get(widget_id) else {
debug_assert!(false);
anyhow::bail!("invalid widget");
};
@@ -146,7 +151,7 @@ impl Layout {
event,
&mut EventParams {
transform_stack: state.transform_stack,
widgets: &self.widget_states,
widgets: &self.widget_map,
tree: &self.tree,
animations: state.animations,
needs_redraw: &mut state.needs_redraw,
@@ -235,13 +240,13 @@ impl Layout {
pub fn new(assets: Box<dyn AssetProvider>) -> anyhow::Result<Self> {
let mut tree = TaffyTree::new();
let mut widget_node_map = HashMap::new();
let mut widget_states = HopSlotMap::new();
let mut widget_node_map = WidgetNodeMap::default();
let mut widget_map = HopSlotMap::new();
let (root_widget, root_node) = add_child_internal(
&mut tree,
&mut widget_map,
&mut widget_node_map,
&mut widget_states,
None, // no parent
Div::create()?,
taffy::Style {
@@ -257,7 +262,7 @@ impl Layout {
root_node,
root_widget,
widget_node_map,
widget_states,
widget_map,
needs_redraw: true,
haptics_triggered: false,
animations: Animations::default(),
@@ -269,7 +274,9 @@ impl Layout {
let mut dirty_nodes = Vec::new();
self.animations.process(
&self.widget_states,
&self.widget_map,
&self.widget_node_map,
&self.tree,
&mut dirty_nodes,
timestep_alpha,
&mut self.needs_redraw,
@@ -301,7 +308,7 @@ impl Layout {
match node_context {
None => taffy::Size::ZERO,
Some(h) => {
if let Some(w) = self.widget_states.get(*h) {
if let Some(w) = self.widget_map.get(*h) {
w.lock()
.unwrap()
.obj
@@ -330,7 +337,9 @@ impl Layout {
let mut dirty_nodes = Vec::new();
self.animations.tick(
&self.widget_states,
&self.widget_map,
&self.widget_node_map,
&self.tree,
&mut dirty_nodes,
&mut self.needs_redraw,
);
@@ -344,7 +353,7 @@ impl Layout {
// helper function
pub fn add_event_listener(&self, widget_id: WidgetID, listener: EventListener) {
let Some(widget) = self.widget_states.get(widget_id) else {
let Some(widget) = self.widget_map.get(widget_id) else {
debug_assert!(false);
return;
};

View File

@@ -1,3 +1,4 @@
use glam::{Mat4, Vec2, Vec3};
use vulkano::buffer::BufferContents;
// binary compatible mat4 which could be transparently used by vulkano BufferContents
@@ -6,13 +7,20 @@ use vulkano::buffer::BufferContents;
pub struct WMat4(pub [f32; 16]);
impl WMat4 {
pub fn from_glam(mat: &glam::Mat4) -> WMat4 {
pub fn from_glam(mat: &Mat4) -> WMat4 {
WMat4(*mat.as_ref())
}
}
impl Default for WMat4 {
fn default() -> Self {
Self(*glam::Mat4::IDENTITY.as_ref())
Self(*Mat4::IDENTITY.as_ref())
}
}
// works just like CSS transform-origin 50% 50%
pub fn centered_matrix(box_size: Vec2, input: &Mat4) -> Mat4 {
Mat4::from_translation(Vec3::new(box_size.x / 2.0, box_size.y / 2.0, 0.0))
* *input
* Mat4::from_translation(Vec3::new(-box_size.x / 2.0, -box_size.y / 2.0, 0.0))
}

View File

@@ -74,6 +74,12 @@ pub struct WidgetState {
}
impl WidgetState {
pub fn get_data_obj_mut(&mut self) -> (&mut WidgetData, &mut dyn WidgetObj) {
let data = &mut self.data;
let obj = self.obj.as_mut();
(data, obj)
}
fn new(obj: Box<dyn WidgetObj>) -> anyhow::Result<WidgetState> {
Ok(Self {
data: WidgetData {

View File

@@ -1,10 +1,11 @@
use std::{cell::RefCell, collections::HashMap, rc::Rc};
use glam::{Affine2, vec2, vec3a};
use glam::{Affine2, Mat4, Vec2, Vec3, vec2, vec3a};
use wgui::{
animation::{Animation, AnimationEasing},
drawing::Color,
event::{self, EventListener},
renderer_vk::util,
taffy::{self, prelude::length},
widget::{
div::Div,
@@ -161,7 +162,7 @@ where
let key_state = {
let widget = panel
.layout
.widget_states
.widget_map
.get(*widget_id)
.unwrap() // want panic
.lock()
@@ -277,6 +278,28 @@ where
})
}
const BUTTON_HOVER_SCALE: f32 = 0.1;
fn get_anim_transform(pos: f32, widget_size: Vec2) -> Mat4 {
util::centered_matrix(
widget_size,
&Mat4::from_scale(Vec3::splat(BUTTON_HOVER_SCALE.mul_add(pos, 1.0))),
)
}
fn set_anim_color(key_state: &KeyState, rect: &mut Rectangle, pos: f32) {
let br1 = pos * 0.25;
let br2 = pos * 0.15;
rect.params.color.r = key_state.color.r + br1;
rect.params.color.g = key_state.color.g + br1;
rect.params.color.b = key_state.color.b + br1;
rect.params.color2.r = key_state.color2.r + br2;
rect.params.color2.g = key_state.color2.g + br2;
rect.params.color2.b = key_state.color2.b + br2;
}
fn on_enter_anim(
key_state: Rc<KeyState>,
_keyboard_state: Rc<RefCell<KeyboardState>>,
@@ -284,14 +307,12 @@ fn on_enter_anim(
) {
data.animations.push(Animation::new(
data.widget_id,
5,
AnimationEasing::OutQuad,
10,
AnimationEasing::OutBack,
Box::new(move |data| {
let rect = data.obj.get_as_mut::<Rectangle>();
let brightness = data.pos * 0.5;
rect.params.color.r = key_state.color.r + brightness;
rect.params.color.g = key_state.color.g + brightness;
rect.params.color.b = key_state.color.b + brightness;
set_anim_color(&key_state, rect, data.pos);
data.data.transform = get_anim_transform(data.pos, data.widget_size);
data.needs_redraw = true;
}),
));
@@ -304,14 +325,12 @@ fn on_leave_anim(
) {
data.animations.push(Animation::new(
data.widget_id,
5,
15,
AnimationEasing::OutQuad,
Box::new(move |data| {
let rect = data.obj.get_as_mut::<Rectangle>();
let brightness = (1.0 - data.pos) * 0.5;
rect.params.color.r = key_state.color.r + brightness;
rect.params.color.g = key_state.color.g + brightness;
rect.params.color.b = key_state.color.b + brightness;
set_anim_color(&key_state, rect, 1.0 - data.pos);
data.data.transform = get_anim_transform(1.0 - data.pos, data.widget_size);
data.needs_redraw = true;
}),
));