use alloc::vec::Vec;
#[cfg(feature = "std")]
use std::sync::atomic::{AtomicU64, Ordering};
use azul_core::{
dom::{DomId, NodeId},
drag::{ActiveDragType, AutoScrollDirection, DragContext, DragData},
geom::{LogicalPosition, PhysicalPositionI32},
hit_test::HitTest,
task::{Duration as CoreDuration, Instant as CoreInstant},
window::WindowPosition,
};
use azul_css::{impl_option, impl_option_inner};
#[cfg(feature = "std")]
static NEXT_EVENT_ID: AtomicU64 = AtomicU64::new(1);
#[cfg(feature = "std")]
pub fn allocate_event_id() -> u64 {
NEXT_EVENT_ID.fetch_add(1, Ordering::Relaxed)
}
#[cfg(not(feature = "std"))]
pub fn allocate_event_id() -> u64 {
0
}
#[allow(clippy::cast_possible_truncation)] fn duration_to_millis(duration: CoreDuration) -> u64 {
match duration {
#[cfg(feature = "std")]
CoreDuration::System(system_diff) => {
let std_duration: std::time::Duration = system_diff.into();
std_duration.as_millis() as u64
}
#[cfg(not(feature = "std"))]
CoreDuration::System(system_diff) => {
system_diff.secs * 1000 + (system_diff.nanos / 1_000_000) as u64
}
CoreDuration::Tick(tick_diff) => {
tick_diff.tick_diff
}
}
}
pub const MAX_SAMPLES_PER_SESSION: usize = 1000;
pub const DEFAULT_SAMPLE_TIMEOUT_MS: u64 = 2000;
const DRAIN_BATCH_SIZE: usize = 100;
pub const TOUCH_CONTACT_BUTTON_STATE: u8 = 0x01;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct GestureDetectionConfig {
pub drag_distance_threshold: f32,
pub double_click_time_threshold_ms: u64,
pub double_click_distance_threshold: f32,
pub long_press_time_threshold_ms: u64,
pub long_press_distance_threshold: f32,
pub min_samples_for_gesture: usize,
pub swipe_velocity_threshold: f32,
pub pinch_scale_threshold: f32,
pub rotation_angle_threshold: f32,
pub sample_cleanup_interval_ms: u64,
}
impl Default for GestureDetectionConfig {
fn default() -> Self {
Self {
drag_distance_threshold: 5.0,
double_click_time_threshold_ms: 500,
double_click_distance_threshold: 5.0,
long_press_time_threshold_ms: 500,
long_press_distance_threshold: 10.0,
min_samples_for_gesture: 2,
swipe_velocity_threshold: 500.0, pinch_scale_threshold: 0.1, rotation_angle_threshold: 0.1, sample_cleanup_interval_ms: DEFAULT_SAMPLE_TIMEOUT_MS,
}
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct InputSample {
pub position: LogicalPosition,
pub screen_position: LogicalPosition,
pub timestamp: CoreInstant,
pub button_state: u8,
pub event_id: u64,
pub pressure: f32,
pub tilt: (f32, f32),
pub touch_radius: (f32, f32),
}
impl_option!(
InputSample,
OptionInputSample,
copy = false,
[Debug, Clone, PartialEq]
);
#[derive(Debug, Clone, PartialEq)]
pub struct InputSession {
pub samples: Vec<InputSample>,
pub ended: bool,
pub session_id: u64,
pub window_position_at_start: WindowPosition,
}
impl InputSession {
fn new(session_id: u64, first_sample: InputSample, window_position: WindowPosition) -> Self {
Self {
samples: vec![first_sample],
ended: false,
session_id,
window_position_at_start: window_position,
}
}
#[must_use] pub fn first_sample(&self) -> Option<&InputSample> {
self.samples.first()
}
#[must_use] pub fn last_sample(&self) -> Option<&InputSample> {
self.samples.last()
}
#[must_use] pub fn duration_ms(&self) -> Option<u64> {
let first = self.first_sample()?;
let last = self.last_sample()?;
let duration = last.timestamp.duration_since(&first.timestamp);
Some(duration_to_millis(duration))
}
#[must_use] pub fn total_distance(&self) -> f32 {
if self.samples.len() < 2 {
return 0.0;
}
let mut total = 0.0;
for i in 1..self.samples.len() {
let prev = &self.samples[i - 1];
let curr = &self.samples[i];
let dx = curr.position.x - prev.position.x;
let dy = curr.position.y - prev.position.y;
total += dx.hypot(dy);
}
total
}
#[must_use] pub fn direct_distance(&self) -> Option<f32> {
let first = self.first_sample()?;
let last = self.last_sample()?;
let dx = last.position.x - first.position.x;
let dy = last.position.y - first.position.y;
Some(dx.hypot(dy))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct DetectedDrag {
pub start_position: LogicalPosition,
pub current_position: LogicalPosition,
pub direct_distance: f32,
pub total_distance: f32,
pub duration_ms: u64,
pub sample_count: usize,
pub session_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub struct DetectedLongPress {
pub position: LogicalPosition,
pub duration_ms: u64,
pub callback_invoked: bool,
pub session_id: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(C)]
pub enum GestureDirection {
Up,
Down,
Left,
Right,
}
impl_option!(
GestureDirection,
OptionGestureDirection,
[Debug, Clone, Copy, PartialEq, Eq]
);
impl_option!(
DetectedPinch,
OptionDetectedPinch,
[Debug, Clone, Copy, PartialEq]
);
impl_option!(
DetectedRotation,
OptionDetectedRotation,
[Debug, Clone, Copy, PartialEq]
);
impl_option!(
DetectedLongPress,
OptionDetectedLongPress,
[Debug, Clone, Copy, PartialEq, Eq]
);
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct DetectedPinch {
pub scale: f32,
pub center: LogicalPosition,
pub initial_distance: f32,
pub current_distance: f32,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct DetectedRotation {
pub angle_radians: f32,
pub center: LogicalPosition,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct PenState {
pub position: LogicalPosition,
pub pressure: f32,
pub tilt: crate::callbacks::PenTilt,
pub in_contact: bool,
pub is_eraser: bool,
pub barrel_button_pressed: bool,
pub device_id: u64,
pub tangential_pressure: f32,
pub barrel_roll_rad: f32,
pub tool_id: u32,
}
impl_option!(PenState, OptionPenState, [Debug, Clone, Copy, PartialEq]);
impl Default for PenState {
fn default() -> Self {
Self {
position: LogicalPosition::zero(),
pressure: 0.0,
tilt: crate::callbacks::PenTilt {
x_tilt: 0.0,
y_tilt: 0.0,
},
in_contact: false,
is_eraser: false,
barrel_button_pressed: false,
device_id: 0,
tangential_pressure: 0.0,
barrel_roll_rad: 0.0,
tool_id: 0,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C)]
pub struct WacomPadState {
pub express_keys: u32,
pub touch_ring: f32,
pub touch_ring_active: bool,
pub device_id: u64,
}
impl_option!(
WacomPadState,
OptionWacomPadState,
[Debug, Clone, Copy, PartialEq]
);
impl Default for WacomPadState {
fn default() -> Self {
Self {
express_keys: 0,
touch_ring: 0.0,
touch_ring_active: false,
device_id: 0,
}
}
}
impl WacomPadState {
#[must_use] pub const fn express_key(&self, index: u32) -> bool {
index < 32 && (self.express_keys & (1u32 << index)) != 0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct GestureAndDragManager {
pub config: GestureDetectionConfig,
pub input_sessions: Vec<InputSession>,
pub active_drag: Option<DragContext>,
pub pen_state: Option<PenState>,
pub previous_pen_state: Option<PenState>,
pub pen_event_pending: bool,
pub pad_state: Option<WacomPadState>,
long_press_callbacks_invoked: Vec<u64>,
next_session_id: u64,
pub native_gesture: Option<NativeGestureEvent>,
touch_sessions: alloc::collections::btree_map::BTreeMap<u64, u64>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[repr(C, u8)]
pub enum NativeGestureEvent {
DoubleClick,
LongPress(DetectedLongPress),
Swipe(GestureDirection),
Pinch(DetectedPinch),
Rotation(DetectedRotation),
}
impl Default for GestureAndDragManager {
fn default() -> Self {
Self::new()
}
}
impl GestureAndDragManager {
#[must_use] pub const fn debug_counts(&self) -> (usize, usize) {
(self.input_sessions.len(), self.long_press_callbacks_invoked.len())
}
#[must_use] pub fn new() -> Self {
Self {
config: GestureDetectionConfig::default(),
input_sessions: Vec::new(),
next_session_id: 1,
active_drag: None,
pen_state: None,
previous_pen_state: None,
pen_event_pending: false,
pad_state: None,
long_press_callbacks_invoked: Vec::new(),
native_gesture: None,
touch_sessions: alloc::collections::btree_map::BTreeMap::new(),
}
}
pub const fn inject_native_gesture(&mut self, gesture: NativeGestureEvent) {
self.native_gesture = Some(gesture);
}
pub const fn clear_native_gesture(&mut self) {
self.native_gesture = None;
}
#[must_use] pub fn with_config(config: GestureDetectionConfig) -> Self {
Self {
config,
..Self::new()
}
}
pub fn start_input_session(
&mut self,
position: LogicalPosition,
timestamp: CoreInstant,
button_state: u8,
window_position: WindowPosition,
screen_position: LogicalPosition,
) -> u64 {
self.start_input_session_with_pen(
position,
timestamp,
button_state,
allocate_event_id(),
0.5, (0.0, 0.0), (0.0, 0.0), window_position,
screen_position,
)
}
pub fn start_input_session_with_pen(
&mut self,
position: LogicalPosition,
timestamp: CoreInstant,
button_state: u8,
event_id: u64,
pressure: f32,
tilt: (f32, f32),
touch_radius: (f32, f32),
window_position: WindowPosition,
screen_position: LogicalPosition,
) -> u64 {
let last_ended_idx = self.input_sessions.iter().rposition(|s| s.ended);
let mut idx = 0usize;
self.input_sessions.retain(|session| {
let keep = !session.ended || Some(idx) == last_ended_idx;
idx += 1;
keep
});
let session_id = self.next_session_id;
self.next_session_id += 1;
let sample = InputSample {
position,
screen_position,
timestamp,
button_state,
event_id,
pressure,
tilt,
touch_radius,
};
let session = InputSession::new(session_id, sample, window_position);
self.input_sessions.push(session);
session_id
}
pub fn record_input_sample(
&mut self,
position: LogicalPosition,
timestamp: CoreInstant,
button_state: u8,
screen_position: LogicalPosition,
) -> bool {
self.record_input_sample_with_pen(
position,
timestamp,
button_state,
allocate_event_id(),
0.5, (0.0, 0.0), (0.0, 0.0), screen_position,
)
}
pub fn record_input_sample_with_pen(
&mut self,
position: LogicalPosition,
timestamp: CoreInstant,
button_state: u8,
event_id: u64,
pressure: f32,
tilt: (f32, f32),
touch_radius: (f32, f32),
screen_position: LogicalPosition,
) -> bool {
let Some(session) = self.input_sessions.last_mut() else {
return false;
};
if session.ended {
return false;
}
if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
let remove_count = session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
session.samples.drain(0..remove_count);
}
session.samples.push(InputSample {
position,
screen_position,
timestamp,
button_state,
event_id,
pressure,
tilt,
touch_radius,
});
true
}
pub fn end_current_session(&mut self) {
if let Some(session) = self.input_sessions.last_mut() {
session.ended = true;
}
}
pub fn touch_down(
&mut self,
touch_id: u64,
position: LogicalPosition,
timestamp: CoreInstant,
window_position: WindowPosition,
screen_position: LogicalPosition,
) {
let session_id = self.start_input_session(
position,
timestamp,
TOUCH_CONTACT_BUTTON_STATE,
window_position,
screen_position,
);
self.touch_sessions.insert(touch_id, session_id);
}
pub fn touch_move(
&mut self,
touch_id: u64,
position: LogicalPosition,
timestamp: CoreInstant,
screen_position: LogicalPosition,
) -> bool {
let Some(session_id) = self.touch_sessions.get(&touch_id).copied() else {
return false;
};
self.record_sample_for_session(session_id, position, timestamp, screen_position)
}
pub fn touch_up(
&mut self,
touch_id: u64,
position: LogicalPosition,
timestamp: CoreInstant,
screen_position: LogicalPosition,
) {
let Some(session_id) = self.touch_sessions.remove(&touch_id) else {
return;
};
let _ = self.record_sample_for_session(session_id, position, timestamp, screen_position);
if let Some(session) = self
.input_sessions
.iter_mut()
.find(|s| s.session_id == session_id)
{
session.ended = true;
}
}
pub fn touch_cancel_all(&mut self) {
let ids: Vec<u64> = self.touch_sessions.values().copied().collect();
self.touch_sessions.clear();
for session_id in ids {
if let Some(session) = self
.input_sessions
.iter_mut()
.find(|s| s.session_id == session_id)
{
session.ended = true;
}
}
}
fn record_sample_for_session(
&mut self,
session_id: u64,
position: LogicalPosition,
timestamp: CoreInstant,
screen_position: LogicalPosition,
) -> bool {
let Some(session) = self
.input_sessions
.iter_mut()
.find(|s| s.session_id == session_id)
else {
return false;
};
if session.ended {
return false;
}
if session.samples.len() >= MAX_SAMPLES_PER_SESSION {
let remove_count =
session.samples.len() - MAX_SAMPLES_PER_SESSION + DRAIN_BATCH_SIZE;
session.samples.drain(0..remove_count);
}
session.samples.push(InputSample {
position,
screen_position,
timestamp,
button_state: TOUCH_CONTACT_BUTTON_STATE,
event_id: allocate_event_id(),
pressure: 0.5,
tilt: (0.0, 0.0),
touch_radius: (0.0, 0.0),
});
true
}
#[allow(clippy::needless_pass_by_value)]
pub fn clear_old_sessions(&mut self, current_time: CoreInstant) {
self.input_sessions.retain(|session| {
if let Some(last_sample) = session.last_sample() {
let duration = current_time.duration_since(&last_sample.timestamp);
let age_ms = duration_to_millis(duration);
age_ms < self.config.sample_cleanup_interval_ms
} else {
false
}
});
let valid_session_ids: Vec<u64> =
self.input_sessions.iter().map(|s| s.session_id).collect();
self.long_press_callbacks_invoked
.retain(|id| valid_session_ids.contains(id));
}
pub fn clear_all_sessions(&mut self) {
self.input_sessions.clear();
self.long_press_callbacks_invoked.clear();
}
pub const fn update_pen_state(
&mut self,
position: LogicalPosition,
pressure: f32,
tilt: (f32, f32),
in_contact: bool,
is_eraser: bool,
barrel_button_pressed: bool,
device_id: u64,
) {
self.update_pen_state_full(
position,
pressure,
tilt,
in_contact,
is_eraser,
barrel_button_pressed,
device_id,
0.0,
0.0,
0,
);
}
pub const fn update_pen_state_full(
&mut self,
position: LogicalPosition,
pressure: f32,
tilt: (f32, f32),
in_contact: bool,
is_eraser: bool,
barrel_button_pressed: bool,
device_id: u64,
tangential_pressure: f32,
barrel_roll_rad: f32,
tool_id: u32,
) {
self.previous_pen_state = self.pen_state;
self.pen_state = Some(PenState {
position,
pressure,
tilt: crate::callbacks::PenTilt {
x_tilt: tilt.0,
y_tilt: tilt.1,
},
in_contact,
is_eraser,
barrel_button_pressed,
device_id,
tangential_pressure,
barrel_roll_rad,
tool_id,
});
self.pen_event_pending = true;
}
pub const fn clear_pen_state(&mut self) {
self.previous_pen_state = self.pen_state;
self.pen_state = None;
self.pen_event_pending = true;
}
#[must_use] pub const fn get_pen_state(&self) -> Option<&PenState> {
self.pen_state.as_ref()
}
#[must_use] pub const fn get_previous_pen_state(&self) -> Option<&PenState> {
self.previous_pen_state.as_ref()
}
pub const fn clear_pen_event_pending(&mut self) {
self.pen_event_pending = false;
}
pub const fn update_pad_state(&mut self, pad: WacomPadState) {
self.pad_state = Some(pad);
}
#[must_use] pub const fn get_pad_state(&self) -> Option<&WacomPadState> {
self.pad_state.as_ref()
}
pub const fn clear_pad_state(&mut self) {
self.pad_state = None;
}
#[must_use] pub fn detect_drag(&self) -> Option<DetectedDrag> {
let session = self.get_current_session()?;
if session.samples.len() < self.config.min_samples_for_gesture {
return None;
}
let direct_distance = session.direct_distance()?;
if direct_distance >= self.config.drag_distance_threshold {
let first = session.first_sample()?;
let last = session.last_sample()?;
Some(DetectedDrag {
start_position: first.position,
current_position: last.position,
direct_distance,
total_distance: session.total_distance(),
duration_ms: session.duration_ms()?,
sample_count: session.samples.len(),
session_id: session.session_id,
})
} else {
None
}
}
#[must_use] pub fn detect_long_press(&self) -> Option<DetectedLongPress> {
if let Some(NativeGestureEvent::LongPress(lp)) = self.native_gesture {
return Some(lp);
}
let session = self.get_current_session()?;
if session.ended {
return None; }
let duration_ms = session.duration_ms()?;
if duration_ms < self.config.long_press_time_threshold_ms {
return None;
}
let distance = session.direct_distance()?;
if distance <= self.config.long_press_distance_threshold {
let first = session.first_sample()?;
let callback_invoked = self
.long_press_callbacks_invoked
.contains(&session.session_id);
Some(DetectedLongPress {
position: first.position,
duration_ms,
callback_invoked,
session_id: session.session_id,
})
} else {
None
}
}
pub fn mark_current_long_press_invoked(&mut self) {
if let Some(id) = self.get_current_session().map(|s| s.session_id) {
self.mark_long_press_callback_invoked(id);
}
}
pub fn mark_long_press_callback_invoked(&mut self, session_id: u64) {
if !self.long_press_callbacks_invoked.contains(&session_id) {
self.long_press_callbacks_invoked.push(session_id);
}
}
#[must_use] pub fn detect_double_click(&self) -> bool {
if matches!(self.native_gesture, Some(NativeGestureEvent::DoubleClick)) {
return true;
}
let sessions = &self.input_sessions;
if sessions.len() < 2 {
return false;
}
let prev_session = &sessions[sessions.len() - 2];
let last_session = &sessions[sessions.len() - 1];
if !prev_session.ended || !last_session.ended {
return false;
}
let prev_first = prev_session.first_sample();
let last_first = last_session.first_sample();
let (Some(prev_first), Some(last_first)) = (prev_first, last_first) else {
return false;
};
let duration = last_first.timestamp.duration_since(&prev_first.timestamp);
let time_delta_ms = duration_to_millis(duration);
if time_delta_ms > self.config.double_click_time_threshold_ms {
return false;
}
let dx = last_first.position.x - prev_first.position.x;
let dy = last_first.position.y - prev_first.position.y;
let distance = dx.hypot(dy);
distance < self.config.double_click_distance_threshold
}
#[must_use] pub fn detect_click_count(&self) -> u32 {
let sessions = &self.input_sessions;
let n = sessions.len();
if n == 0 {
return 1;
}
let mut recent: Vec<&InputSession> = Vec::new();
for s in sessions.iter().rev() {
if !s.ended {
continue;
}
recent.push(s);
if recent.len() >= 3 {
break;
}
}
if recent.is_empty() {
return 1;
}
let mut count = 1u32;
for i in 0..recent.len() - 1 {
let later = recent[i];
let earlier = recent[i + 1];
let Some(later_start) = later.first_sample() else {
break;
};
let Some(earlier_start) = earlier.first_sample() else {
break;
};
let duration = later_start.timestamp.duration_since(&earlier_start.timestamp);
let time_delta_ms = duration_to_millis(duration);
if time_delta_ms > self.config.double_click_time_threshold_ms {
break;
}
let dx = later_start.position.x - earlier_start.position.x;
let dy = later_start.position.y - earlier_start.position.y;
let distance = dx.hypot(dy);
if distance >= self.config.double_click_distance_threshold {
break;
}
count += 1;
}
if count > 3 { 1 } else { count }
}
#[must_use] pub fn get_drag_direction(&self) -> Option<GestureDirection> {
let session = self.get_current_session()?;
let first = session.first_sample()?;
let last = session.last_sample()?;
let dx = last.position.x - first.position.x;
let dy = last.position.y - first.position.y;
let direction = match (dx.abs() > dy.abs(), dx > 0.0, dy > 0.0) {
(true, true, _) => GestureDirection::Right,
(true, false, _) => GestureDirection::Left,
(false, _, true) => GestureDirection::Down,
(false, _, false) => GestureDirection::Up,
};
Some(direction)
}
#[allow(clippy::cast_precision_loss)] #[must_use] pub fn get_gesture_velocity(&self) -> Option<f32> {
let session = self.get_current_session()?;
if session.samples.len() < 2 {
return None;
}
let total_distance = session.total_distance();
let duration_ms = session.duration_ms()?;
if duration_ms == 0 {
return None;
}
let duration_secs = duration_ms as f32 / 1000.0;
Some(total_distance / duration_secs)
}
#[must_use] pub fn is_swipe(&self) -> bool {
self.get_gesture_velocity()
.is_some_and(|v| v >= self.config.swipe_velocity_threshold)
}
#[must_use] pub fn detect_swipe_direction(&self) -> Option<GestureDirection> {
if let Some(NativeGestureEvent::Swipe(d)) = self.native_gesture {
return Some(d);
}
if !self.is_swipe() {
return None;
}
self.get_drag_direction()
}
#[allow(clippy::similar_names)] #[must_use] pub fn detect_pinch(&self) -> Option<DetectedPinch> {
if let Some(NativeGestureEvent::Pinch(p)) = self.native_gesture {
return Some(p);
}
if self.input_sessions.len() < 2 {
return None;
}
let session1 = &self.input_sessions[self.input_sessions.len() - 2];
let session2 = &self.input_sessions[self.input_sessions.len() - 1];
if session1.ended || session2.ended {
return None;
}
let first1 = session1.first_sample()?;
let first2 = session2.first_sample()?;
let last1 = session1.last_sample()?;
let last2 = session2.last_sample()?;
let dx_initial = first2.position.x - first1.position.x;
let dy_initial = first2.position.y - first1.position.y;
let initial_distance = dx_initial.hypot(dy_initial);
let dx_current = last2.position.x - last1.position.x;
let dy_current = last2.position.y - last1.position.y;
let current_distance = dx_current.hypot(dy_current);
if initial_distance < 1.0 {
return None;
}
let scale = current_distance / initial_distance;
let scale_threshold = 1.0 + self.config.pinch_scale_threshold;
if scale > 1.0 / scale_threshold && scale < scale_threshold {
return None; }
let center = LogicalPosition {
x: f32::midpoint(last1.position.x, last2.position.x),
y: f32::midpoint(last1.position.y, last2.position.y),
};
let duration = last1.timestamp.duration_since(&first1.timestamp);
let duration_ms = duration_to_millis(duration);
Some(DetectedPinch {
scale,
center,
initial_distance,
current_distance,
duration_ms,
})
}
#[allow(clippy::similar_names)] #[must_use] pub fn detect_rotation(&self) -> Option<DetectedRotation> {
const PI: f32 = core::f32::consts::PI;
if let Some(NativeGestureEvent::Rotation(r)) = self.native_gesture {
return Some(r);
}
if self.input_sessions.len() < 2 {
return None;
}
let session1 = &self.input_sessions[self.input_sessions.len() - 2];
let session2 = &self.input_sessions[self.input_sessions.len() - 1];
if session1.ended || session2.ended {
return None;
}
let first1 = session1.first_sample()?;
let first2 = session2.first_sample()?;
let last1 = session1.last_sample()?;
let last2 = session2.last_sample()?;
let center = LogicalPosition {
x: f32::midpoint(last1.position.x, last2.position.x),
y: f32::midpoint(last1.position.y, last2.position.y),
};
let dx_initial = first2.position.x - first1.position.x;
let dy_initial = first2.position.y - first1.position.y;
let initial_angle = dy_initial.atan2(dx_initial);
let dx_current = last2.position.x - last1.position.x;
let dy_current = last2.position.y - last1.position.y;
let current_angle = dy_current.atan2(dx_current);
let mut angle_diff = current_angle - initial_angle;
#[allow(clippy::while_float)] while angle_diff > PI {
angle_diff -= 2.0 * PI;
}
#[allow(clippy::while_float)] while angle_diff < -PI {
angle_diff += 2.0 * PI;
}
if angle_diff.abs() < self.config.rotation_angle_threshold {
return None;
}
let duration = last1.timestamp.duration_since(&first1.timestamp);
let duration_ms = duration_to_millis(duration);
Some(DetectedRotation {
angle_radians: angle_diff,
center,
duration_ms,
})
}
#[must_use] pub fn get_current_session(&self) -> Option<&InputSession> {
self.input_sessions.last()
}
#[must_use] pub fn get_current_mouse_position(&self) -> Option<LogicalPosition> {
self.get_current_session()
.and_then(|s| s.last_sample())
.map(|sample| sample.position)
}
#[must_use] pub fn get_drag_delta(&self) -> Option<(f32, f32)> {
let session = self.get_current_session()?;
let first = session.first_sample()?;
let last = session.last_sample()?;
Some((
last.position.x - first.position.x,
last.position.y - first.position.y,
))
}
#[must_use] pub fn get_drag_delta_screen(&self) -> Option<(f32, f32)> {
let session = self.get_current_session()?;
let first = session.first_sample()?;
let last = session.last_sample()?;
Some((
last.screen_position.x - first.screen_position.x,
last.screen_position.y - first.screen_position.y,
))
}
#[must_use] pub fn get_drag_delta_screen_incremental(&self) -> Option<(f32, f32)> {
let session = self.get_current_session()?;
let len = session.samples.len();
if len < 2 {
return None;
}
let prev = &session.samples[len - 2];
let last = &session.samples[len - 1];
Some((
last.screen_position.x - prev.screen_position.x,
last.screen_position.y - prev.screen_position.y,
))
}
#[must_use] pub fn get_window_position_at_session_start(&self) -> Option<WindowPosition> {
let session = self.get_current_session()?;
Some(session.window_position_at_start)
}
#[must_use] pub const fn get_drag_context(&self) -> Option<&DragContext> {
self.active_drag.as_ref()
}
pub const fn get_drag_context_mut(&mut self) -> Option<&mut DragContext> {
self.active_drag.as_mut()
}
pub fn activate_node_drag(
&mut self,
dom_id: DomId,
node_id: NodeId,
drag_data: DragData,
_start_hit_test: Option<HitTest>,
) {
if let Some(detected) = self.detect_drag() {
self.active_drag = Some(DragContext::node_drag(
dom_id,
node_id,
detected.start_position,
drag_data,
detected.session_id,
));
}
}
pub fn activate_window_drag(
&mut self,
initial_window_position: WindowPosition,
_start_hit_test: Option<HitTest>,
) {
if let Some(detected) = self.detect_drag() {
self.active_drag = Some(DragContext::window_move(
detected.start_position,
initial_window_position,
detected.session_id,
));
}
}
pub const fn update_active_drag_positions(&mut self, position: LogicalPosition) {
if let Some(ref mut drag) = self.active_drag {
drag.update_position(position);
}
}
pub fn update_drop_target(&mut self, target: Option<azul_core::dom::DomNodeId>) {
if let Some(ref mut drag) = self.active_drag {
match &mut drag.drag_type {
ActiveDragType::Node(ref mut node_drag) => {
node_drag.current_drop_target = target.into();
}
ActiveDragType::FileDrop(ref mut file_drop) => {
file_drop.drop_target = target.into();
}
_ => {}
}
}
}
pub const fn update_auto_scroll_direction(&mut self, direction: AutoScrollDirection) {
if let Some(ref mut drag) = self.active_drag {
if let Some(text_drag) = drag.as_text_selection_mut() {
text_drag.auto_scroll_direction = direction;
}
}
}
pub const fn end_drag(&mut self) -> Option<DragContext> {
self.active_drag.take()
}
pub fn cancel_drag(&mut self) {
if let Some(ref mut drag) = self.active_drag {
drag.cancelled = true;
}
self.active_drag = None;
}
#[must_use] pub const fn is_dragging(&self) -> bool {
self.active_drag.is_some()
}
#[must_use] pub fn is_text_selection_dragging(&self) -> bool {
self.active_drag.as_ref().is_some_and(DragContext::is_text_selection)
}
#[must_use] pub fn is_scrollbar_dragging(&self) -> bool {
self.active_drag.as_ref().is_some_and(DragContext::is_scrollbar_thumb)
}
#[must_use] pub fn is_node_drag_active(&self) -> bool {
self.active_drag.as_ref().is_some_and(DragContext::is_node_drag)
}
#[must_use] pub fn is_node_dragging(&self, dom_id: DomId, node_id: NodeId) -> bool {
self.active_drag.as_ref().is_some_and(|d| {
d.as_node_drag().is_some_and(|node_drag| node_drag.dom_id == dom_id && node_drag.node_id == node_id)
})
}
#[must_use] pub fn is_window_dragging(&self) -> bool {
self.active_drag.as_ref().is_some_and(DragContext::is_window_move)
}
#[must_use] pub fn is_file_dropping(&self) -> bool {
self.active_drag.as_ref().is_some_and(DragContext::is_file_drop)
}
#[must_use] pub const fn session_count(&self) -> usize {
self.input_sessions.len()
}
#[must_use] pub fn current_session_id(&self) -> Option<u64> {
self.get_current_session().map(|s| s.session_id)
}
#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_drag_delta(&self) -> Option<(i32, i32)> {
let drag = self.active_drag.as_ref()?.as_window_move()?;
let delta_x = drag.current_position.x - drag.start_position.x;
let delta_y = drag.current_position.y - drag.start_position.y;
match drag.initial_window_position {
WindowPosition::Initialized(_initial_pos) => Some((delta_x as i32, delta_y as i32)),
_ => None,
}
}
#[allow(clippy::cast_possible_truncation)] #[must_use] pub fn get_window_position_from_drag(&self) -> Option<WindowPosition> {
let drag = self.active_drag.as_ref()?.as_window_move()?;
let delta_x = drag.current_position.x - drag.start_position.x;
let delta_y = drag.current_position.y - drag.start_position.y;
match drag.initial_window_position {
WindowPosition::Initialized(initial_pos) => {
Some(WindowPosition::Initialized(PhysicalPositionI32::new(
initial_pos.x + delta_x as i32,
initial_pos.y + delta_y as i32,
)))
}
_ => None,
}
}
#[must_use] pub fn get_scrollbar_scroll_offset(&self) -> Option<f32> {
self.active_drag.as_ref()?.calculate_scrollbar_scroll_offset()
}
}
impl crate::managers::NodeIdRemap for GestureAndDragManager {
fn remap_node_ids(&mut self, dom_id: DomId, map: &crate::managers::NodeIdMap) {
if let Some(ref mut drag) = self.active_drag {
if !drag.remap_node_ids(dom_id, map.as_btree_map()) {
drag.cancelled = true;
self.active_drag = None;
}
}
}
}
#[cfg(test)]
mod touch_session_tests {
use super::*;
use azul_core::task::{Instant as TestInstant, SystemTick};
fn ts(n: u64) -> CoreInstant {
TestInstant::Tick(SystemTick::new(n))
}
fn pos(x: f32, y: f32) -> LogicalPosition {
LogicalPosition { x, y }
}
#[test]
fn two_fingers_open_two_concurrent_sessions() {
let mut m = GestureAndDragManager::new();
m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
assert_eq!(m.input_sessions.len(), 2);
assert!(!m.input_sessions[0].ended);
assert!(!m.input_sessions[1].ended);
}
#[test]
fn moves_land_in_the_correct_session_not_the_last_one() {
let mut m = GestureAndDragManager::new();
m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
assert!(m.touch_move(1, pos(90.0, 100.0), ts(2), pos(90.0, 100.0)));
assert_eq!(m.input_sessions[0].samples.len(), 2, "finger 1 session grew");
assert_eq!(m.input_sessions[1].samples.len(), 1, "finger 2 session untouched");
}
#[test]
fn spread_gesture_is_detected_as_pinch_out() {
let mut m = GestureAndDragManager::new();
m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
m.touch_move(1, pos(50.0, 100.0), ts(2), pos(50.0, 100.0));
m.touch_move(2, pos(250.0, 100.0), ts(3), pos(250.0, 100.0));
let pinch = m.detect_pinch().expect("two concurrent touch sessions must yield a pinch");
assert!(
pinch.scale > 1.5,
"spread must read as pinch-out (scale {}), initial {} current {}",
pinch.scale,
pinch.initial_distance,
pinch.current_distance
);
}
#[test]
fn touch_up_ends_only_its_own_session() {
let mut m = GestureAndDragManager::new();
m.touch_down(1, pos(100.0, 100.0), ts(0), WindowPosition::Uninitialized, pos(100.0, 100.0));
m.touch_down(2, pos(200.0, 100.0), ts(1), WindowPosition::Uninitialized, pos(200.0, 100.0));
m.touch_up(1, pos(100.0, 100.0), ts(2), pos(100.0, 100.0));
assert!(m.input_sessions[0].ended);
assert!(!m.input_sessions[1].ended);
assert!(!m.touch_move(1, pos(0.0, 0.0), ts(3), pos(0.0, 0.0)));
}
}