From 6b81f07cc805c818791682f725f9f32f7bab7794 Mon Sep 17 00:00:00 2001 From: oo8dev Date: Sat, 2 May 2026 19:21:30 +0200 Subject: [PATCH] wgui: Swipe kinetic scrolling support (#504) * wgui: use DeviceBitmask instead of usize * wgui: Swipe logic works (wip) * wgui: Kinetic scrolling, rubberband effect --- .../src/util/networking/skymap_catalog.rs | 11 +- dash-frontend/src/util/wgui_simple.rs | 1 + uidev/src/main.rs | 10 +- wayvr/src/gui/panel/mod.rs | 18 +- wayvr/src/ipc/event_queue.rs | 2 - wayvr/src/overlays/dashboard.rs | 20 +- wgui/src/components/button.rs | 13 + wgui/src/components/checkbox.rs | 12 + wgui/src/components/slider.rs | 6 +- wgui/src/components/tabs.rs | 2 +- wgui/src/event.rs | 32 ++- wgui/src/layout.rs | 71 ++++-- wgui/src/widget/mod.rs | 230 ++++++++++++++---- 13 files changed, 326 insertions(+), 102 deletions(-) diff --git a/dash-frontend/src/util/networking/skymap_catalog.rs b/dash-frontend/src/util/networking/skymap_catalog.rs index db1feee6..d7d21ca2 100644 --- a/dash-frontend/src/util/networking/skymap_catalog.rs +++ b/dash-frontend/src/util/networking/skymap_catalog.rs @@ -1,12 +1,7 @@ -#![allow(dead_code)] -use std::path::PathBuf; - -// TODO: Remove later -use serde::{Deserialize, Serialize}; -use wlx_common::{async_executor::AsyncExecutor, config_io}; - use crate::util::networking::{self, WAYVR_SKYMAPS_ROOT, http_client}; - +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use wlx_common::{async_executor::AsyncExecutor, config_io}; pub type SkymapUuid = uuid::Uuid; #[derive(Copy, Clone, Serialize, Deserialize, Debug)] diff --git a/dash-frontend/src/util/wgui_simple.rs b/dash-frontend/src/util/wgui_simple.rs index f09b69ab..af4d393c 100644 --- a/dash-frontend/src/util/wgui_simple.rs +++ b/dash-frontend/src/util/wgui_simple.rs @@ -45,6 +45,7 @@ pub fn create_button(par: CreateButtonParams) -> anyhow::Result<()> { Ok(()) } +#[allow(dead_code)] pub fn create_label(layout: &mut Layout, id_parent: WidgetID, content: Translation) -> anyhow::Result<()> { let label = WidgetLabel::create( &mut layout.state, diff --git a/uidev/src/main.rs b/uidev/src/main.rs index a812d1ce..69e95fa2 100644 --- a/uidev/src/main.rs +++ b/uidev/src/main.rs @@ -143,7 +143,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(x, y), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -157,7 +157,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseWheel(MouseWheelEvent { delta: Vec2::new(pos.x as f32 / 5.0, pos.y as f32 / 5.0), pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -177,7 +177,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseDown(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -190,7 +190,7 @@ fn main() -> Result<(), Box> { &wgui::event::Event::MouseUp(MouseButtonEvent { pos: mouse / scale, index: MouseButtonIndex::Left, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), @@ -209,7 +209,7 @@ fn main() -> Result<(), Box> { .push_event( &wgui::event::Event::MouseMotion(MouseMotionEvent { pos: mouse / scale, - device: 0, + device: wgui::event::DeviceBitmask(0), }), &mut (), &mut (), diff --git a/wayvr/src/gui/panel/mod.rs b/wayvr/src/gui/panel/mod.rs index c0f6d8ac..04d067a1 100644 --- a/wayvr/src/gui/panel/mod.rs +++ b/wayvr/src/gui/panel/mod.rs @@ -22,7 +22,7 @@ use wgui::{ slider::ComponentSlider, }, event::{ - Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, + DeviceBitmask, Event as WguiEvent, EventCallback, EventListenerID, EventListenerKind, InternalStateChangeEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent, MouseWheelEvent, }, @@ -368,7 +368,7 @@ impl OverlayBackend for GuiPanel { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } @@ -376,7 +376,7 @@ impl OverlayBackend for GuiPanel { fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -397,7 +397,9 @@ impl OverlayBackend for GuiPanel { } fn on_left(&mut self, app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(app, &e); } @@ -414,13 +416,13 @@ impl OverlayBackend for GuiPanel { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(app, &e); @@ -429,11 +431,11 @@ impl OverlayBackend for GuiPanel { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(app, &e); } diff --git a/wayvr/src/ipc/event_queue.rs b/wayvr/src/ipc/event_queue.rs index c150c4c4..13c9adb4 100644 --- a/wayvr/src/ipc/event_queue.rs +++ b/wayvr/src/ipc/event_queue.rs @@ -1,5 +1,3 @@ -#![allow(dead_code)] - use std::{cell::RefCell, collections::VecDeque, rc::Rc}; #[derive(Debug)] diff --git a/wayvr/src/overlays/dashboard.rs b/wayvr/src/overlays/dashboard.rs index a0b531d7..316f66ff 100644 --- a/wayvr/src/overlays/dashboard.rs +++ b/wayvr/src/overlays/dashboard.rs @@ -8,8 +8,8 @@ use wayvr_ipc::{ }; use wgui::{ event::{ - Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, MouseMotionEvent, - MouseWheelEvent, + DeviceBitmask, Event as WguiEvent, MouseButtonEvent, MouseButtonIndex, MouseLeaveEvent, + MouseMotionEvent, MouseWheelEvent, }, gfx::cmd::WGfxClearMode, renderer_vk::context::Context as WguiContext, @@ -205,7 +205,7 @@ impl OverlayBackend for DashFrontend { let e = WguiEvent::MouseWheel(MouseWheelEvent { delta: vec2(delta.x, delta.y) / 8.0, pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } @@ -213,7 +213,7 @@ impl OverlayBackend for DashFrontend { fn on_hover(&mut self, _app: &mut AppState, hit: &PointerHit) -> HoverResult { let e = &WguiEvent::MouseMotion(MouseMotionEvent { pos: hit.uv * self.inner.layout.content_size, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.has_focus[hit.pointer] = true; @@ -235,7 +235,9 @@ impl OverlayBackend for DashFrontend { } fn on_left(&mut self, _app: &mut AppState, pointer: usize) { - let e = WguiEvent::MouseLeave(MouseLeaveEvent { device: pointer }); + let e = WguiEvent::MouseLeave(MouseLeaveEvent { + device: DeviceBitmask::from_usize(pointer), + }); self.has_focus[pointer] = false; self.push_event(&e); } @@ -252,13 +254,13 @@ impl OverlayBackend for DashFrontend { WguiEvent::MouseDown(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) } else { WguiEvent::MouseUp(MouseButtonEvent { pos: hit.uv * self.inner.layout.content_size, index, - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }) }; self.push_event(&e); @@ -267,11 +269,11 @@ impl OverlayBackend for DashFrontend { if !pressed && !self.has_focus[hit.pointer] { let e = WguiEvent::MouseMotion(MouseMotionEvent { pos: vec2(-1., -1.), - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); let e = WguiEvent::MouseLeave(MouseLeaveEvent { - device: hit.pointer, + device: DeviceBitmask::from_usize(hit.pointer), }); self.push_event(&e); } diff --git a/wgui/src/components/button.rs b/wgui/src/components/button.rs index ab2bfda8..e543f059 100644 --- a/wgui/src/components/button.rs +++ b/wgui/src/components/button.rs @@ -334,6 +334,18 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + state.hovered = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -552,6 +564,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul let listeners = &mut root.widget.state().event_listeners; let anim_mult = ess.layout.state.theme.animation_mult; vec![ + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_enter(data.clone(), state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), register_event_mouse_press(state.clone(), listeners), diff --git a/wgui/src/components/checkbox.rs b/wgui/src/components/checkbox.rs index 552e7ed6..4a49e775 100644 --- a/wgui/src/components/checkbox.rs +++ b/wgui/src/components/checkbox.rs @@ -240,6 +240,17 @@ fn register_event_mouse_leave( ) } +fn register_event_mouse_cancel(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { + listeners.register( + EventListenerKind::MouseCancel, + Box::new(move |_common, _event_data, (), ()| { + let mut state = state.borrow_mut(); + state.down = false; + Ok(EventResult::Pass) + }), + ) +} + fn register_event_mouse_press(state: Rc>, listeners: &mut EventListenerCollection) -> EventListenerID { listeners.register( EventListenerKind::MousePress, @@ -439,6 +450,7 @@ pub fn construct(ess: &mut ConstructEssentials, params: Params) -> anyhow::Resul vec![ register_event_mouse_enter(state.clone(), listeners, params.tooltip, anim_mult), register_event_mouse_leave(state.clone(), listeners, anim_mult), + register_event_mouse_cancel(state.clone(), listeners), register_event_mouse_press(state.clone(), listeners), register_event_mouse_release(data.clone(), state.clone(), listeners), ] diff --git a/wgui/src/components/slider.rs b/wgui/src/components/slider.rs index 0814f329..004d4b22 100644 --- a/wgui/src/components/slider.rs +++ b/wgui/src/components/slider.rs @@ -11,8 +11,8 @@ use crate::{ }, drawing::{self}, event::{ - self, CallbackDataCommon, CallbackMetadata, EventAlterables, EventListenerCollection, EventListenerKind, - StyleSetRequest, + self, CallbackDataCommon, CallbackMetadata, DeviceBitmask, EventAlterables, EventListenerCollection, + EventListenerKind, StyleSetRequest, }, i18n::Translation, layout::{WidgetID, WidgetPair}, @@ -77,7 +77,7 @@ pub struct Params { } struct State { - dragged_by: Option, + dragged_by: Option, hovered: bool, values: ValuesMinMax, on_value_changed: Option, diff --git a/wgui/src/components/tabs.rs b/wgui/src/components/tabs.rs index 293cc00a..de6acb66 100644 --- a/wgui/src/components/tabs.rs +++ b/wgui/src/components/tabs.rs @@ -9,7 +9,7 @@ use crate::{ layout::WidgetPair, widget::{ConstructEssentials, div::WidgetDiv}, }; -use std::{cell::RefCell, fmt::Pointer, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use taffy::{ AlignItems, prelude::{length, percent}, diff --git a/wgui/src/event.rs b/wgui/src/event.rs index cba47205..5e09650e 100644 --- a/wgui/src/event.rs +++ b/wgui/src/event.rs @@ -19,6 +19,17 @@ use crate::{ widget::{EventResult, WidgetData, WidgetObj}, }; +#[derive(Debug, Eq, PartialEq, Clone, Copy)] +pub struct DeviceBitmask(pub u8); + +impl DeviceBitmask { + pub fn from_usize(mask: usize) -> Self { + // more than 8 input devices? + debug_assert!(mask & !0xff == 0); + Self(mask as u8) + } +} + #[derive(Debug, Clone, Copy)] pub enum MouseButtonIndex { Left, @@ -30,28 +41,28 @@ pub enum MouseButtonIndex { pub struct MouseButtonEvent { pub index: MouseButtonIndex, pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } #[derive(Debug, Clone, Copy)] pub struct MousePosition { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseLeaveEvent { - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseMotionEvent { pub pos: Vec2, - pub device: usize, + pub device: DeviceBitmask, } pub struct MouseWheelEvent { pub pos: Vec2, /* mouse position */ pub delta: Vec2, /* wheel delta */ - pub device: usize, + pub device: DeviceBitmask, } #[derive(Clone)] @@ -70,6 +81,7 @@ pub enum Event { MouseMotion(MouseMotionEvent), MouseUp(MouseButtonEvent), MouseWheel(MouseWheelEvent), + MouseCancel, // Called if the user started scrolling by swiping above the button, to cancel all currently pressed buttons (prevent clicks) TextInput(TextInputEvent), } @@ -106,6 +118,7 @@ pub struct EventAlterables { pub dirty_widgets: Vec, pub components_to_refresh_once: Vec, pub style_set_requests: Vec<(WidgetID, StyleSetRequest)>, + pub global_events_to_emit: Vec, pub animations: Vec, pub widgets_to_tick: HashSet, // widgets which needs to be ticked in the next `Layout::update()` fn pub transform_stack: TransformStack, @@ -134,6 +147,10 @@ impl EventAlterables { self.mark_redraw(); } + pub fn emit_global_event(&mut self, event: Event) { + self.global_events_to_emit.push(event); + } + pub fn mark_tick(&mut self, widget_id: WidgetID) { self.widgets_to_tick.insert(widget_id); } @@ -224,9 +241,10 @@ impl CallbackMetadata { #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum EventListenerKind { MousePress, - MouseRelease, - MouseEnter, MouseMotion, + MouseRelease, + MouseCancel, + MouseEnter, MouseLeave, TextInput, InternalStateChange, diff --git a/wgui/src/layout.rs b/wgui/src/layout.rs index 0d195e32..6bcda51f 100644 --- a/wgui/src/layout.rs +++ b/wgui/src/layout.rs @@ -11,7 +11,7 @@ use crate::{ drawing::{ self, ANSI_BOLD_CODE, ANSI_RESET_CODE, Boundary, PushScissorStackResult, push_scissor_stack, push_transform_stack, }, - event::{self, CallbackDataCommon, EventAlterables}, + event::{self, CallbackDataCommon, Event, EventAlterables}, globals::WguiGlobals, sound::WguiSoundType, task::Tasks, @@ -173,6 +173,8 @@ pub struct Layout { pub widgets_to_tick: Vec, + global_events_to_emit: Vec, + // *Main root* // contains content_root_widget and topmost widgets pub tree_root_widget: WidgetID, @@ -450,19 +452,25 @@ impl Layout { .as_ref() .is_none_or(PushScissorStackResult::should_display) { - // check children first - self.push_event_children(node_id, event, event_result, alterables, user_data)?; + let res_priority = widget.process_event_priority( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + )?; - if event_result.can_propagate() { - let mut params = EventParams { - state: &self.state, - layout: l, - alterables, - node_id, - style, - }; + if res_priority.can_propagate() { + // check children first + self.push_event_children(node_id, event, event_result, alterables, user_data)?; - widget.process_event(widget_id, node_id, event, event_result, user_data, &mut params)?; + if event_result.can_propagate() { + widget.process_event( + &mut self.get_event_params(l, node_id, style, alterables), + widget_id, + event, + event_result, + user_data, + )?; + } } } @@ -474,6 +482,22 @@ impl Layout { Ok(()) } + fn get_event_params<'a>( + &'a self, + l: &'a taffy::Layout, + node_id: taffy::NodeId, + style: &'a taffy::Style, + alterables: &'a mut EventAlterables, + ) -> EventParams<'a> { + EventParams { + node_id, + style, + state: &self.state, + alterables, + taffy_layout: l, + } + } + pub const fn check_toggle_needs_redraw(&mut self) -> bool { if self.needs_redraw { self.needs_redraw = false; @@ -508,6 +532,18 @@ impl Layout { &mut (user1, user2), )?; self.process_alterables(alterables)?; + + let mut alterables = EventAlterables::default(); + for event in std::mem::take(&mut self.global_events_to_emit) { + self.push_event_widget( + self.tree_root_node, + &event, + &mut event_result, + &mut alterables, + &mut (user1, user2), + )?; + } + Ok(event_result) } @@ -580,6 +616,7 @@ impl Layout { sounds_to_play_once: Vec::new(), focused_component: None, alterables: Default::default(), + global_events_to_emit: Vec::new(), }) } @@ -815,10 +852,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 widget_id in alterables.widgets_to_tick { + self.widgets_to_tick.push(widget_id); + } + + for event in alterables.global_events_to_emit { + self.global_events_to_emit.push(event); } for c in alterables.components_to_refresh_once { diff --git a/wgui/src/widget/mod.rs b/wgui/src/widget/mod.rs index 08f7aeda..961d916d 100644 --- a/wgui/src/widget/mod.rs +++ b/wgui/src/widget/mod.rs @@ -1,5 +1,5 @@ use anyhow::Context; -use glam::Vec2; +use glam::{FloatExt, Vec2}; use taffy::{NodeId, TaffyTree}; use super::drawing::RenderPrimitive; @@ -8,7 +8,8 @@ use crate::{ any::AnyTrait, drawing::{self, PrimitiveExtent}, event::{ - self, CallbackData, CallbackDataCommon, CallbackMetadata, Event, EventAlterables, EventListenerCollection, + self, CallbackData, CallbackDataCommon, CallbackMetadata, DeviceBitmask, Event, EventAlterables, + EventListenerCollection, EventListenerKind::{self, InternalStateChange, MouseLeave}, MouseWheelEvent, }, @@ -25,57 +26,66 @@ pub mod rectangle; pub mod sprite; pub mod util; +const SWIPE_START_THRESHOLD_UNITS: f32 = 16.0; +const KINETIC_VELOCITY_DAMPING: f32 = 0.92; +const KINETIC_VELOCITY_BRAKE: f32 = 0.85; +const RUBBERBANDING_DAMP: f32 = 0.8; + pub struct WidgetData { - hovered: usize, - pressed: usize, - 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 + hovered: DeviceBitmask, + pressed: DeviceBitmask, + scrolling_target: Vec2, // normalized, 0.0-1.0. Not used in case if overflow != scroll + scrolling_cur: Vec2, // normalized, used for smooth scrolling animation + scrolling_cur_prev: Vec2, // for motion interpolation while rendering between ticks + scrolling_velocity: Vec2, + press_down_start_mouse_pos: Option, + swipe_running: bool, + swipe_scroll_start: Vec2, // normalized, 0.0-1.0 pub transform: glam::Mat4, pub cached_absolute_boundary: drawing::Boundary, // updated in Layout::push_event_widget } impl WidgetData { - pub const fn set_device_pressed(&mut self, device: usize, pressed: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_pressed(&mut self, device: DeviceBitmask, pressed: bool) -> bool { + let bit = 1 << device.0; let state_changed; if pressed { - state_changed = self.pressed == 0; - self.pressed |= bit; + state_changed = self.pressed.0 == 0; + self.pressed.0 |= bit; } else { - state_changed = self.pressed == bit; - self.pressed &= !bit; + state_changed = self.pressed.0 == bit; + self.pressed.0 &= !bit; } state_changed } - pub const fn set_device_hovered(&mut self, device: usize, hovered: bool) -> bool { - let bit = 1 << device; + pub const fn set_device_hovered(&mut self, device: DeviceBitmask, hovered: bool) -> bool { + let bit = 1 << device.0; let state_changed; if hovered { - state_changed = self.hovered == 0; - self.hovered |= bit; + state_changed = self.hovered.0 == 0; + self.hovered.0 |= bit; } else { - state_changed = self.hovered == bit; - self.hovered &= !bit; + state_changed = self.hovered.0 == bit; + self.hovered.0 &= !bit; } state_changed } - pub const fn get_pressed(&self, device: usize) -> bool { - self.pressed & (1 << device) != 0 + pub const fn get_pressed(&self, device: DeviceBitmask) -> bool { + self.pressed.0 & (1 << device.0) != 0 } - pub const fn get_hovered(&self, device: usize) -> bool { - self.hovered & (1 << device) != 0 + pub const fn get_hovered(&self, device: DeviceBitmask) -> bool { + self.hovered.0 & (1 << device.0) != 0 } pub const fn is_pressed(&self) -> bool { - self.pressed != 0 + self.pressed.0 != 0 } pub const fn is_hovered(&self) -> bool { - self.hovered != 0 + self.hovered.0 != 0 } } @@ -116,13 +126,17 @@ impl WidgetState { fn new(flags: WidgetStateFlags, obj: Box) -> Self { Self { data: WidgetData { - hovered: 0, - pressed: 0, + hovered: DeviceBitmask(0), + pressed: DeviceBitmask(0), scrolling_target: Vec2::default(), scrolling_cur: Vec2::default(), scrolling_cur_prev: Vec2::default(), transform: glam::Mat4::IDENTITY, cached_absolute_boundary: drawing::Boundary::default(), + press_down_start_mouse_pos: None, + swipe_running: false, + swipe_scroll_start: Vec2::default(), + scrolling_velocity: Vec2::default(), }, obj, event_listeners: EventListenerCollection::default(), @@ -192,7 +206,7 @@ pub struct EventParams<'a> { pub style: &'a taffy::Style, pub state: &'a LayoutState, pub alterables: &'a mut EventAlterables, - pub layout: &'a taffy::Layout, + pub taffy_layout: &'a taffy::Layout, } #[derive(Clone, Copy, Eq, PartialEq, PartialOrd)] @@ -221,6 +235,22 @@ fn get_scroll_enabled(style: &taffy::Style) -> (bool, bool) { ) } +const OVERFLOW_START_THRESHOLD_UNITS: f32 = 3.0; // don't show scrollbars for nearly non-scrollable lists + +fn get_scroll_active_axis(style: &taffy::Style, taffy_layout: &taffy::Layout) -> (bool, bool) { + let (enabled_horiz, enabled_vert) = get_scroll_enabled(style); + if !enabled_horiz && !enabled_vert { + return (false, false); + } + + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + + ( + overflow.x >= OVERFLOW_START_THRESHOLD_UNITS, + overflow.y >= OVERFLOW_START_THRESHOLD_UNITS, + ) +} + pub struct ScrollbarInfo { // total contents size of the currently scrolling widget content_size: Vec2, @@ -229,15 +259,13 @@ pub struct ScrollbarInfo { handle_size: Vec2, } -pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { - let overflow_start_threshold_units = 3.0; // don't show scrollbars for nearly non-scrollable lists - - let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); - if overflow.x < overflow_start_threshold_units && overflow.y < overflow_start_threshold_units { +pub fn get_scrollbar_info(taffy_layout: &taffy::Layout) -> Option { + let overflow = Vec2::new(taffy_layout.scroll_width(), taffy_layout.scroll_height()); + if overflow.x < OVERFLOW_START_THRESHOLD_UNITS && overflow.y < OVERFLOW_START_THRESHOLD_UNITS { return None; // not overflowing } - let content_size = Vec2::new(l.content_size.width, l.content_size.height); + let content_size = Vec2::new(taffy_layout.content_size.width, taffy_layout.content_size.height); let handle_size = 1.0 - (overflow / content_size); Some(ScrollbarInfo { @@ -246,6 +274,15 @@ pub fn get_scrollbar_info(l: &taffy::Layout) -> Option { }) } +impl ScrollbarInfo { + fn get_potential_scroll_axis_multiplier(&self, taffy_layout: &taffy::Layout) -> Vec2 { + Vec2::new( + self.content_size.x - taffy_layout.content_box_width(), + self.content_size.y - taffy_layout.content_box_height(), + ) + } +} + impl dyn WidgetObj { pub fn get_as(&self) -> Option<&T> { let any = self.as_any(); @@ -348,6 +385,46 @@ impl WidgetState { let scrolling_cur_prev = &mut self.data.scrolling_cur_prev; let scrolling_target = &mut self.data.scrolling_target; + let mut perform_next_tick = false; + + // check for boundaries + if !self.data.swipe_running { + // top bound + if scrolling_target.y < 0.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // left bound + if scrolling_target.x < 0.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x *= RUBBERBANDING_DAMP; + perform_next_tick = true; + } + + // right bound + if scrolling_target.x > 1.0 { + self.data.scrolling_velocity.x *= KINETIC_VELOCITY_BRAKE; + scrolling_target.x = 1.0.lerp(scrolling_target.x, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + + // bottom bound + if scrolling_target.y > 1.0 { + self.data.scrolling_velocity.y *= KINETIC_VELOCITY_BRAKE; + scrolling_target.y = 1.0.lerp(scrolling_target.y, RUBBERBANDING_DAMP); + perform_next_tick = true; + } + } + + // Perform kinetic velocity animation + if self.data.scrolling_velocity.length_squared() > 1e-9 { + *scrolling_target += self.data.scrolling_velocity; + self.data.scrolling_velocity *= KINETIC_VELOCITY_DAMPING; + perform_next_tick = true; + } + *scrolling_cur_prev = *scrolling_cur; if scrolling_cur != scrolling_target { @@ -355,8 +432,7 @@ impl WidgetState { *scrolling_cur = scrolling_cur.lerp(*scrolling_target, 0.2); // trigger tick request again - alterables.mark_tick(this_widget_id); - alterables.mark_redraw(); + perform_next_tick = true; let epsilon = 0.00001; if (scrolling_cur.x - scrolling_target.x).abs() < epsilon @@ -365,6 +441,11 @@ impl WidgetState { *scrolling_cur = *scrolling_target; } } + + if perform_next_tick { + alterables.mark_tick(this_widget_id); + alterables.mark_redraw(); + } } pub fn draw_scrollbars(&mut self, state: &mut DrawState, params: &DrawParams, info: &ScrollbarInfo) { @@ -432,13 +513,13 @@ impl WidgetState { return false; } - let l = params.layout; + let l = params.taffy_layout; let overflow = Vec2::new(l.scroll_width(), l.scroll_height()); if overflow.x == 0.0 && overflow.y == 0.0 { return false; // not overflowing } - let Some(info) = get_scrollbar_info(params.layout) else { + let Some(info) = get_scrollbar_info(l) else { return false; }; @@ -451,7 +532,7 @@ impl WidgetState { } let mult = (1.0 / (content_box_length - content_length)) * step_pixels; - let new_scroll = (*scrolling_target + wheel_delta * mult).clamp(0.0, 1.0); + let new_scroll = *scrolling_target + wheel_delta * mult; if *scrolling_target == new_scroll { return; } @@ -479,20 +560,81 @@ impl WidgetState { true } + // this is called before calling children of this widget + pub fn process_event_priority( + &mut self, + params: &mut EventParams, + widget_id: WidgetID, + event: &Event, + ) -> anyhow::Result { + match &event { + Event::MouseDown(e) => { + // firstly, check if this widget is scrollable at all + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + if active_x || active_y { + self.data.press_down_start_mouse_pos = Some(e.pos); + self.data.swipe_scroll_start = self.data.scrolling_target; + } + } + Event::MouseUp(_e) => { + if self.data.swipe_running { + self.data.swipe_running = false; + let kinetic_force = (self.data.scrolling_cur - self.data.scrolling_cur_prev) * 20.0; + self.data.scrolling_velocity += kinetic_force; + params.alterables.mark_tick(widget_id); + params.alterables.mark_redraw(); + } + self.data.press_down_start_mouse_pos = None; + } + Event::MouseMotion(e) => { + if let Some(start_mouse_pos) = &self.data.press_down_start_mouse_pos { + let (active_x, active_y) = get_scroll_active_axis(¶ms.style, ¶ms.taffy_layout); + + if !self.data.swipe_running { + if (active_x && (e.pos.x - start_mouse_pos.x).abs() >= SWIPE_START_THRESHOLD_UNITS) + || (active_y && (e.pos.y - start_mouse_pos.y).abs() >= SWIPE_START_THRESHOLD_UNITS) + { + self.data.swipe_running = true; + params.alterables.emit_global_event(Event::MouseCancel); + } + } + + if self.data.swipe_running + && let Some(scrollbar_info) = get_scrollbar_info(params.taffy_layout) + { + let mouse_diff = e.pos - start_mouse_pos; + + let mult = scrollbar_info.get_potential_scroll_axis_multiplier(params.taffy_layout); + + let scroll_diff_x = if mult.x != 0.0 { -mouse_diff.x / mult.x } else { 0.0 }; + let scroll_diff_y = if mult.y != 0.0 { -mouse_diff.y / mult.y } else { 0.0 }; + + self.data.scrolling_target = self.data.swipe_scroll_start + Vec2::new(scroll_diff_x, scroll_diff_y); + params.alterables.mark_tick(self.obj.get_id()); + + return Ok(EventResult::Consumed); + } + } + } + _ => {} + } + + Ok(EventResult::Pass) + } + pub fn process_event<'a, 'b, U1: 'static, U2: 'static>( &mut self, + params: &'a mut EventParams<'a>, widget_id: WidgetID, - node_id: taffy::NodeId, event: &Event, event_result: &'a mut EventResult, user_data: &'a mut (&'b mut U1, &'b mut U2), - params: &'a mut EventParams<'a>, ) -> anyhow::Result<()> { let hovered = event.test_mouse_within_transform(params.alterables.transform_stack.get()); let mut invoke_data = InvokeData { widget_id, - node_id, + node_id: params.node_id, event_result, user_data, params, @@ -508,6 +650,9 @@ impl WidgetState { CallbackMetadata::TextInput(e.clone()), )?); } + Event::MouseCancel => { + res = Some(self.invoke_listeners(&mut invoke_data, EventListenerKind::MouseCancel, CallbackMetadata::None)?); + } Event::MouseDown(e) => { if hovered && self.data.set_device_pressed(e.device, true) { res = Some(self.invoke_listeners( @@ -528,7 +673,6 @@ impl WidgetState { } Event::MouseMotion(e) => { let hover_state_changed = self.data.set_device_hovered(e.device, hovered); - if hover_state_changed { if self.data.is_hovered() { res =