From a2a7c71c22794e0c1e7785ff26323e66253f0a85 Mon Sep 17 00:00:00 2001 From: Aleksander Date: Fri, 20 Jun 2025 13:06:04 +0200 Subject: [PATCH] Animated transforms on hover --- wgui/src/animation.rs | 63 ++++++++++++------- wgui/src/components/button.rs | 2 +- wgui/src/drawing.rs | 4 +- wgui/src/layout.rs | 59 +++++++++-------- wgui/src/renderer_vk/util.rs | 12 +++- wgui/src/widget/mod.rs | 6 ++ .../src/overlays/keyboard/builder.rs | 45 +++++++++---- 7 files changed, 125 insertions(+), 66 deletions(-) diff --git a/wgui/src/animation.rs b/wgui/src/animation.rs index 7681ed9..5bbdc3d 100644 --- a/wgui/src/animation.rs +++ b/wgui/src/animation.rs @@ -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, @@ -110,29 +112,40 @@ impl Animation { fn call( &self, - widgets: &WidgetMap, + widget_map: &WidgetMap, + widget_node_map: &WidgetNodeMap, + tree: &taffy::tree::TaffyTree, dirty_nodes: &mut Vec, 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, dirty_nodes: &mut Vec, 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, dirty_nodes: &mut Vec, 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 { } }); } -} \ No newline at end of file +} diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index 81acac8..fce6731 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -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, diff --git a/wgui/src/drawing.rs b/wgui/src/drawing.rs index 2d8ced3..9f4d1cb 100644 --- a/wgui/src/drawing.rs +++ b/wgui/src/drawing.rs @@ -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> { 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!(); }; diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index 8ab664c..daa27f8 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -18,6 +18,18 @@ use taffy::{TaffyTree, TraversePartialTree}; pub type WidgetID = slotmap::DefaultKey; pub type BoxWidget = Arc>; pub type WidgetMap = HopSlotMap; +#[derive(Default)] +pub struct WidgetNodeMap(pub HashMap); + +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, @@ -31,8 +43,8 @@ pub struct Layout { pub assets: Box, - pub widget_states: WidgetMap, - pub widget_node_map: HashMap, + 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, - widget_node_map: &mut HashMap, - vec: &mut WidgetMap, + widget_map: &mut WidgetMap, + widget_node_map: &mut WidgetNodeMap, parent_node: Option, 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 { - 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) -> anyhow::Result { 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; }; diff --git a/wgui/src/renderer_vk/util.rs b/wgui/src/renderer_vk/util.rs index 771dd00..98b2cb3 100644 --- a/wgui/src/renderer_vk/util.rs +++ b/wgui/src/renderer_vk/util.rs @@ -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)) +} diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 2fcdf07..d77cc54 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -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) -> anyhow::Result { Ok(Self { data: WidgetData { diff --git a/wlx-overlay-s/src/overlays/keyboard/builder.rs b/wlx-overlay-s/src/overlays/keyboard/builder.rs index 49520bf..c367d8d 100644 --- a/wlx-overlay-s/src/overlays/keyboard/builder.rs +++ b/wlx-overlay-s/src/overlays/keyboard/builder.rs @@ -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, _keyboard_state: Rc>, @@ -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::(); - 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::(); - 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; }), ));