wgui: Smooth scrolling, TransformStack: fix scrollable area boundaries (mouse wheel), separate into raw_dim and visual_dim, refactoring

This commit is contained in:
Aleksander
2025-10-04 18:40:44 +02:00
parent 231850cf73
commit 40cc27f7b0
7 changed files with 196 additions and 87 deletions

View File

@@ -6,10 +6,11 @@ use taffy::TraversePartialTree;
use crate::{
drawing,
event::EventAlterables,
layout::Widget,
renderer_vk::text::{TextShadow, custom_glyph::CustomGlyph},
stack::{self, ScissorBoundary, ScissorStack, TransformStack},
widget::{self},
widget::{self, ScrollbarInfo, WidgetState},
};
use super::{layout::Layout, widget::DrawState};
@@ -34,8 +35,8 @@ impl Boundary {
let transform = transform_stack.get();
Self {
pos: Vec2::new(transform.abs_pos.x, transform.abs_pos.y),
size: Vec2::new(transform.dim.x, transform.dim.y),
pos: transform.abs_pos,
size: transform.raw_dim,
}
}
@@ -45,7 +46,7 @@ impl Boundary {
Self {
pos: Vec2::ZERO,
size: Vec2::new(transform.dim.x, transform.dim.y),
size: transform.raw_dim,
}
}
@@ -152,8 +153,9 @@ pub enum RenderPrimitive {
}
pub struct DrawParams<'a> {
pub layout: &'a Layout,
pub layout: &'a mut Layout,
pub debug_draw: bool,
pub alpha: f32, // timestep alpha, 0.0 - 1.0, used for motion interpolation if rendering above tick rate: smoother animations or scrolling
}
pub fn has_overflow_clip(style: &taffy::Style) -> bool {
@@ -175,6 +177,45 @@ fn primitive_debug_rect(boundary: &Boundary, transform: &Mat4, color: drawing::C
)
}
pub fn push_transform_stack(
transform_stack: &mut TransformStack,
l: &taffy::Layout,
scroll_shift: Vec2,
widget_state: &WidgetState,
) {
let raw_dim = Vec2::new(l.size.width, l.size.height);
let visual_dim = raw_dim + scroll_shift;
transform_stack.push(stack::Transform {
rel_pos: Vec2::new(l.location.x, l.location.y) - scroll_shift,
transform: widget_state.data.transform,
raw_dim,
visual_dim,
abs_pos: Default::default(),
transform_rel: Default::default(),
});
}
/// returns true if scissor has been pushed
pub fn push_scissor_stack(
transform_stack: &mut TransformStack,
scissor_stack: &mut ScissorStack,
scroll_shift: Vec2,
info: &Option<ScrollbarInfo>,
style: &taffy::Style,
) -> bool {
let scissor_pushed = info.is_some() && has_overflow_clip(style);
if !scissor_pushed {
return false;
}
let mut boundary_absolute = drawing::Boundary::construct_absolute(transform_stack);
boundary_absolute.pos += scroll_shift;
scissor_stack.push(ScissorBoundary(boundary_absolute));
true
}
fn draw_widget(
params: &DrawParams,
state: &mut DrawState,
@@ -189,17 +230,16 @@ fn draw_widget(
let mut widget_state = widget.state();
let (scroll_shift, info) = match widget::get_scrollbar_info(l) {
Some(info) => (widget_state.get_scroll_shift(&info, l), Some(info)),
None => (Vec2::default(), None),
let (scroll_shift, wants_redraw, info) = match widget::get_scrollbar_info(l) {
Some(info) => {
let (scrolling, wants_redraw) = widget_state.get_scroll_shift_smooth(&info, l, params.alpha);
(scrolling, wants_redraw, Some(info))
}
None => (Vec2::default(), false, None),
};
state.transform_stack.push(stack::Transform {
rel_pos: Vec2::new(l.location.x, l.location.y) - scroll_shift,
transform: widget_state.data.transform,
dim: Vec2::new(l.size.width, l.size.height),
..Default::default()
});
// see layout.rs push_event_widget too
push_transform_stack(state.transform_stack, l, scroll_shift, &widget_state);
if params.debug_draw {
let boundary = drawing::Boundary::construct_relative(state.transform_stack);
@@ -210,13 +250,9 @@ fn draw_widget(
));
}
let scissor_pushed = info.is_some() && has_overflow_clip(style);
let scissor_pushed = push_scissor_stack(state.transform_stack, state.scissor_stack, scroll_shift, &info, style);
if scissor_pushed {
let mut boundary_absolute = drawing::Boundary::construct_absolute(state.transform_stack);
boundary_absolute.pos += scroll_shift;
state.scissor_stack.push(ScissorBoundary(boundary_absolute));
if params.debug_draw {
let mut boundary_relative = drawing::Boundary::construct_relative(state.transform_stack);
boundary_relative.pos += scroll_shift;
@@ -254,6 +290,10 @@ fn draw_widget(
if let Some(info) = &info {
widget_state.draw_scrollbars(state, &draw_params, info);
}
if wants_redraw {
state.alterables.mark_redraw();
}
}
fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taffy::NodeId) {
@@ -279,7 +319,7 @@ fn draw_children(params: &DrawParams, state: &mut DrawState, parent_node_id: taf
}
}
pub fn draw(params: &DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
pub fn draw(params: &mut DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
let mut primitives = Vec::<RenderPrimitive>::new();
let mut transform_stack = TransformStack::new();
let mut scissor_stack = ScissorStack::new();
@@ -292,14 +332,18 @@ pub fn draw(params: &DrawParams) -> anyhow::Result<Vec<RenderPrimitive>> {
panic!();
};
let mut alterables = EventAlterables::default();
let mut state = DrawState {
primitives: &mut primitives,
transform_stack: &mut transform_stack,
scissor_stack: &mut scissor_stack,
layout: params.layout,
alterables: &mut alterables,
};
draw_widget(params, &mut state, params.layout.root_node, style, root_widget);
params.layout.process_alterables(alterables)?;
Ok(primitives)
}

View File

@@ -1,5 +1,6 @@
use std::{
cell::{RefCell, RefMut},
collections::HashSet,
rc::Rc,
};
@@ -75,9 +76,9 @@ pub enum Event {
impl Event {
fn test_transform_pos(transform: &Transform, pos: Vec2) -> bool {
pos.x >= transform.abs_pos.x
&& pos.x < transform.abs_pos.x + transform.dim.x
&& pos.x < transform.abs_pos.x + transform.visual_dim.x
&& pos.y >= transform.abs_pos.y
&& pos.y < transform.abs_pos.y + transform.dim.y
&& pos.y < transform.abs_pos.y + transform.visual_dim.y
}
pub fn test_mouse_within_transform(&self, transform: &Transform) -> bool {
@@ -96,6 +97,7 @@ pub struct EventAlterables {
pub dirty_nodes: Vec<taffy::NodeId>,
pub style_set_requests: Vec<(taffy::NodeId, taffy::Style)>,
pub animations: Vec<animation::Animation>,
pub widgets_to_tick: HashSet<WidgetID>, // widgets which needs to be ticked in the next `Layout::update()` fn
pub transform_stack: TransformStack,
pub scissor_stack: ScissorStack,
pub needs_redraw: bool,
@@ -115,6 +117,10 @@ impl EventAlterables {
self.dirty_nodes.push(node_id);
}
pub fn mark_tick(&mut self, widget_id: WidgetID) {
self.widgets_to_tick.insert(widget_id);
}
pub const fn trigger_haptics(&mut self) {
self.trigger_haptics = true;
}

View File

@@ -7,15 +7,15 @@ use std::{
use crate::{
animation::Animations,
components::{Component, InitData},
drawing::{self, has_overflow_clip, Boundary},
drawing::{self, Boundary, has_overflow_clip, push_scissor_stack, push_transform_stack},
event::{self, CallbackDataCommon, EventAlterables, EventListenerCollection},
globals::WguiGlobals,
stack::{self, ScissorBoundary},
widget::{self, div::WidgetDiv, EventParams, WidgetObj, WidgetState},
widget::{self, EventParams, WidgetObj, WidgetState, div::WidgetDiv},
};
use glam::{vec2, Vec2};
use slotmap::{new_key_type, HopSlotMap, SecondaryMap};
use glam::{Vec2, vec2};
use slotmap::{HopSlotMap, SecondaryMap, new_key_type};
use taffy::{NodeId, TaffyTree, TraversePartialTree};
new_key_type! {
@@ -112,7 +112,8 @@ pub struct LayoutState {
pub struct Layout {
pub state: LayoutState,
pub components_to_init: VecDeque<Component>,
pub components_to_init: Vec<Component>,
pub widgets_to_tick: Vec<WidgetID>,
pub root_widget: WidgetID,
pub root_node: taffy::NodeId,
@@ -218,25 +219,32 @@ impl Layout {
self.needs_redraw = true;
}
fn process_pending_components(&mut self) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default();
while let Some(c) = self.components_to_init.pop_front() {
fn process_pending_components(&mut self, alterables: &mut EventAlterables) -> anyhow::Result<()> {
for comp in &self.components_to_init {
let mut common = CallbackDataCommon {
state: &self.state,
alterables: &mut alterables,
alterables,
};
c.0.init(&mut InitData { common: &mut common });
comp.0.init(&mut InitData { common: &mut common });
}
self.process_alterables(alterables)?;
self.components_to_init.clear();
Ok(())
}
fn process_pending_widget_ticks(&mut self, alterables: &mut EventAlterables) {
for widget_id in &self.widgets_to_tick {
let Some(widget) = self.state.widgets.get(*widget_id) else {
continue;
};
widget.state().tick(*widget_id, alterables);
}
self.widgets_to_tick.clear();
}
pub fn defer_component_init(&mut self, component: Component) {
self.components_to_init.push_back(component);
self.components_to_init.push(component);
}
fn push_event_children<U1, U2>(
@@ -276,24 +284,20 @@ impl Layout {
let mut widget = widget.0.borrow_mut();
let (scroll_shift, info) = match widget::get_scrollbar_info(l) {
Some(info) => (widget.get_scroll_shift(&info, l), Some(info)),
Some(info) => (widget.get_scroll_shift_raw(&info, l), Some(info)),
None => (Vec2::default(), None),
};
alterables.transform_stack.push(stack::Transform {
rel_pos: Vec2::new(l.location.x, l.location.y) - scroll_shift,
transform: widget.data.transform,
dim: Vec2::new(l.size.width, l.size.height),
..Default::default()
});
// see drawing.rs draw_widget too
push_transform_stack(&mut alterables.transform_stack, l, scroll_shift, &widget);
// see drawing.rs too
let scissor_pushed = info.is_some() && has_overflow_clip(style);
if scissor_pushed {
let mut boundary_absolute = drawing::Boundary::construct_absolute(&alterables.transform_stack);
boundary_absolute.pos += scroll_shift;
alterables.scissor_stack.push(ScissorBoundary(boundary_absolute));
}
let scissor_pushed = push_scissor_stack(
&mut alterables.transform_stack,
&mut alterables.scissor_stack,
scroll_shift,
&info,
style,
);
let mut iter_children = true;
@@ -399,7 +403,8 @@ impl Layout {
needs_redraw: true,
haptics_triggered: false,
animations: Animations::default(),
components_to_init: VecDeque::new(),
components_to_init: Vec::new(),
widgets_to_tick: Vec::new(),
})
}
@@ -467,9 +472,9 @@ impl Layout {
pub fn tick(&mut self) -> anyhow::Result<()> {
let mut alterables = EventAlterables::default();
self.animations.tick(&self.state, &mut alterables);
self.process_pending_components()?;
self.process_pending_components(&mut alterables)?;
self.process_pending_widget_ticks(&mut alterables);
self.process_alterables(alterables)?;
Ok(())
}
@@ -493,6 +498,12 @@ impl Layout {
}
}
if !alterables.widgets_to_tick.is_empty() {
for widget_id in &alterables.widgets_to_tick {
self.widgets_to_tick.push(*widget_id);
}
}
for request in alterables.style_set_requests {
if let Err(e) = self.state.tree.set_style(request.0, request.1) {
log::error!("failed to set style for taffy widget ID {:?}: {:?}", request.0, e);

View File

@@ -53,8 +53,9 @@ impl<T: StackItem<T>, const STACK_MAX: usize> Default for GenericStack<T, STACK_
#[derive(Copy, Clone)]
pub struct Transform {
pub rel_pos: Vec2,
pub dim: Vec2, // for convenience
pub abs_pos: Vec2, // for convenience, will be set after pushing
pub visual_dim: Vec2, // for convenience
pub raw_dim: Vec2, // for convenience
pub abs_pos: Vec2, // for convenience, will be set after pushing
pub transform: glam::Mat4,
pub transform_rel: glam::Mat4,
}
@@ -64,7 +65,8 @@ impl Default for Transform {
Self {
abs_pos: Default::default(),
rel_pos: Default::default(),
dim: Default::default(),
visual_dim: Default::default(),
raw_dim: Default::default(),
transform: Mat4::IDENTITY,
transform_rel: Default::default(),
}

View File

@@ -22,7 +22,9 @@ pub mod util;
pub struct WidgetData {
hovered: usize,
pressed: usize,
pub scrolling: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll
pub scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll
pub scrolling_cur: Vec2, // normalized, used for smooth scrolling animation
pub scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks
pub transform: glam::Mat4,
}
@@ -87,7 +89,9 @@ impl WidgetState {
data: WidgetData {
hovered: 0,
pressed: 0,
scrolling: Vec2::default(),
scrolling_target: Vec2::default(),
scrolling_cur: Vec2::default(),
scrolling_cur_prev: Vec2::default(),
transform: glam::Mat4::IDENTITY,
},
obj,
@@ -101,6 +105,7 @@ pub struct DrawState<'a> {
pub primitives: &'a mut Vec<RenderPrimitive>,
pub transform_stack: &'a mut TransformStack,
pub scissor_stack: &'a mut ScissorStack,
pub alterables: &'a mut EventAlterables,
}
// per-widget draw params
@@ -116,6 +121,7 @@ pub trait WidgetObj: AnyTrait {
fn set_id(&mut self, id: WidgetID); // always set at insertion
fn draw(&mut self, state: &mut DrawState, params: &DrawParams);
fn measure(
&mut self,
_known_dimensions: taffy::Size<Option<f32>>,
@@ -133,12 +139,6 @@ pub struct EventParams<'a> {
pub layout: &'a taffy::Layout,
}
impl EventParams<'_> {
pub const fn mark_redraw(&mut self) {
self.alterables.needs_redraw = true;
}
}
pub enum EventResult {
Pass, // widget acknowledged it and allows the event to pass to the children
Consumed, // widget triggered an action, do not pass to children
@@ -212,10 +212,27 @@ macro_rules! call_event {
}
impl WidgetState {
pub fn get_scroll_shift(&self, info: &ScrollbarInfo, l: &taffy::Layout) -> Vec2 {
pub fn get_scroll_shift_smooth(&self, info: &ScrollbarInfo, l: &taffy::Layout, timestep_alpha: f32) -> (Vec2, bool) {
let currently_animating = self.data.scrolling_cur != self.data.scrolling_cur_prev;
let scrolling = self
.data
.scrolling_cur_prev
.lerp(self.data.scrolling_cur, timestep_alpha);
(
Vec2::new(
(info.content_size.x - l.content_box_width()) * scrolling.x,
(info.content_size.y - l.content_box_height()) * scrolling.y,
),
currently_animating,
)
}
pub fn get_scroll_shift_raw(&self, info: &ScrollbarInfo, l: &taffy::Layout) -> Vec2 {
Vec2::new(
(info.content_size.x - l.content_box_width()) * self.data.scrolling.x,
(info.content_size.y - l.content_box_height()) * self.data.scrolling.y,
(info.content_size.x - l.content_box_width()) * self.data.scrolling_target.x,
(info.content_size.y - l.content_box_height()) * self.data.scrolling_target.y,
)
}
@@ -223,6 +240,31 @@ impl WidgetState {
self.obj.draw(state, params);
}
pub fn tick(&mut self, this_widget_id: WidgetID, alterables: &mut EventAlterables) {
let scrolling_cur = &mut self.data.scrolling_cur;
let scrolling_cur_prev = &mut self.data.scrolling_cur_prev;
let scrolling_target = &mut self.data.scrolling_target;
*scrolling_cur_prev = *scrolling_cur;
if scrolling_cur != scrolling_target {
// the magic part
*scrolling_cur = scrolling_cur.lerp(*scrolling_target, 0.2);
// trigger tick request again
alterables.mark_tick(this_widget_id);
alterables.mark_redraw();
let epsilon = 0.00001;
if (scrolling_cur.x - scrolling_target.x).abs() < epsilon
&& (scrolling_cur.y - scrolling_target.y).abs() < epsilon
{
log::info!("stopped animating");
*scrolling_cur = *scrolling_target;
}
}
}
pub fn draw_scrollbars(&mut self, state: &mut DrawState, params: &DrawParams, info: &ScrollbarInfo) {
let (enabled_horiz, enabled_vert) = get_scroll_enabled(params.style);
if !enabled_horiz && !enabled_vert {
@@ -248,10 +290,10 @@ impl WidgetState {
PrimitiveExtent {
boundary: drawing::Boundary::from_pos_size(
Vec2::new(
transform.abs_pos.x + transform.dim.x * (1.0 - info.handle_size.x) * self.data.scrolling.x,
transform.abs_pos.y + transform.dim.y - thickness - margin,
transform.abs_pos.x + transform.raw_dim.x * (1.0 - info.handle_size.x) * self.data.scrolling_cur.x,
transform.abs_pos.y + transform.raw_dim.y - thickness - margin,
),
Vec2::new(transform.dim.x * info.handle_size.x, thickness),
Vec2::new(transform.raw_dim.x * info.handle_size.x, thickness),
),
transform: transform.transform,
},
@@ -265,10 +307,10 @@ impl WidgetState {
PrimitiveExtent {
boundary: drawing::Boundary::from_pos_size(
Vec2::new(
transform.abs_pos.x + transform.dim.x - thickness - margin,
transform.abs_pos.y + transform.dim.y * (1.0 - info.handle_size.y) * self.data.scrolling.y,
transform.abs_pos.x + transform.raw_dim.x - thickness - margin,
transform.abs_pos.y + transform.raw_dim.y * (1.0 - info.handle_size.y) * self.data.scrolling_cur.y,
),
Vec2::new(thickness, transform.dim.y * info.handle_size.y),
Vec2::new(thickness, transform.raw_dim.y * info.handle_size.y),
),
transform: transform.transform,
},
@@ -293,25 +335,25 @@ impl WidgetState {
return false;
};
let step_pixels = 32.0;
let step_pixels = 64.0;
if info.handle_size.x < 1.0 && wheel.pos.x != 0.0 {
// Horizontal scrolling
let mult = (1.0 / (l.content_box_width() - info.content_size.x)) * step_pixels;
let new_scroll = (self.data.scrolling.x + wheel.shift.x * mult).clamp(0.0, 1.0);
if self.data.scrolling.x != new_scroll {
self.data.scrolling.x = new_scroll;
params.mark_redraw();
let new_scroll = (self.data.scrolling_target.x + wheel.shift.x * mult).clamp(0.0, 1.0);
if self.data.scrolling_target.x != new_scroll {
self.data.scrolling_target.x = new_scroll;
params.alterables.mark_tick(self.obj.get_id());
}
}
if info.handle_size.y < 1.0 && wheel.pos.y != 0.0 {
// Vertical scrolling
let mult = (1.0 / (l.content_box_height() - info.content_size.y)) * step_pixels;
let new_scroll = (self.data.scrolling.y + wheel.shift.y * mult).clamp(0.0, 1.0);
if self.data.scrolling.y != new_scroll {
self.data.scrolling.y = new_scroll;
params.mark_redraw();
let new_scroll = (self.data.scrolling_target.y + wheel.shift.y * mult).clamp(0.0, 1.0);
if self.data.scrolling_target.y != new_scroll {
self.data.scrolling_target.y = new_scroll;
params.alterables.mark_tick(self.obj.get_id());
}
}