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

View File

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

View File

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

View File

@@ -1,3 +1,4 @@
use glam::{Mat4, Vec2, Vec3};
use vulkano::buffer::BufferContents; use vulkano::buffer::BufferContents;
// binary compatible mat4 which could be transparently used by vulkano 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]); pub struct WMat4(pub [f32; 16]);
impl WMat4 { impl WMat4 {
pub fn from_glam(mat: &glam::Mat4) -> WMat4 { pub fn from_glam(mat: &Mat4) -> WMat4 {
WMat4(*mat.as_ref()) WMat4(*mat.as_ref())
} }
} }
impl Default for WMat4 { impl Default for WMat4 {
fn default() -> Self { 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 { 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> { fn new(obj: Box<dyn WidgetObj>) -> anyhow::Result<WidgetState> {
Ok(Self { Ok(Self {
data: WidgetData { data: WidgetData {

View File

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