diff --git a/internal/backends/qt/qt_window.rs b/internal/backends/qt/qt_window.rs index 65170f844bb..5cf1a267a7d 100644 --- a/internal/backends/qt/qt_window.rs +++ b/internal/backends/qt/qt_window.rs @@ -230,7 +230,9 @@ cpp! {{ rust!(Slint_mouseWheelEvent [rust_window: &QtWindow as "void*", pos: qttypes::QPointF as "QPointF", delta: qttypes::QPoint as "QPoint", phase: usize as "int"] { let position = LogicalPoint::new(pos.x as _, pos.y as _); let phase = match phase as _ { - key_generated::Qt_ScrollPhase_NoScrollPhase => TouchPhase::Cancelled, + // If we don't know the scroll phase, this is likely a mouse wheel scroll, which + // should be mapped to TouchPhase::Moved to align with the winit backend. + key_generated::Qt_ScrollPhase_NoScrollPhase => TouchPhase::Moved, key_generated::Qt_ScrollPhase_ScrollBegin => TouchPhase::Started, key_generated::Qt_ScrollPhase_ScrollUpdate => TouchPhase::Moved, key_generated::Qt_ScrollPhase_ScrollEnd => TouchPhase::Ended, diff --git a/internal/core/animations/physics_simulation.rs b/internal/core/animations/physics_simulation.rs index 03615361247..ede8d6bbd40 100644 --- a/internal/core/animations/physics_simulation.rs +++ b/internal/core/animations/physics_simulation.rs @@ -24,15 +24,18 @@ enum Direction { /// Common simulation trait /// All simulations must implement this trait pub trait Simulation { - fn step(&mut self, new_tick: Instant) -> (f32, bool); - fn curr_value(&self) -> f32; + fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool; } /// Trait to convert parameter objects into a simulation /// All parameter objects must implement this trait! pub trait Parameter { type Output; - fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output; + fn simulation( + self, + start_value: f32, + limit_value: core::pin::Pin>>, + ) -> Self::Output; } /// Input parameters for the `ConstantDeceleration` simulation @@ -46,11 +49,80 @@ impl ConstantDecelerationParameters { pub fn new(initial_velocity: f32, deceleration: f32) -> Self { Self { initial_velocity, deceleration } } + + /// Creates a new `ConstantDecelerationParameters` parameter object based on the distance + /// to travel and duration of the animation. + /// The deceleration is chosen such that the animation covers the given distance at the end of + /// the animation and the velocity becomes zero at the same time (after duration_secs). + /// + // * `distance` - the distance to cover with this animation + // * `duration_secs` - the duration of the animation in seconds + pub fn new_with_distance(distance: f32, duration_secs: f32) -> Self { + debug_assert!(duration_secs > 0., "Duration must be greater than zero"); + + // The initial velocity and deceleration are calculated based on the distance and duration to cover the given distance in the given time. + // + // The calculation is based on the equations of motion for constant acceleration: + // => v0 * t + 0.5 * a * t^2 = d + // + // + // Where t = duration_secs, d = distance, v0 = initial_velocity and a = -deceleration + // Warning! a is acceleration, not deceleration, so we need to flip the sign at the end + // + // We want to reach the limit value at the end of the animation, and the velocity should become zero at the same time, so we can determine `a` based on: + // v0 + a * t = 0 + // + // => a = -v0 / t + // + // Then we can solve for `v0` and `a`: + // + // v0 * t + 0.5 * -(v0 / t) * t^2 = d + // v0 * t + 0.5 * -v0 * t = d + // v0 * (t + -0.5 * t) = d + // v0 * (0.5 * t) = d + // => v0 = d / (0.5 * t) + // + let d = distance; + let t = duration_secs; + let v0 = d / (0.5 * t); + let a = -(v0 / t); + // deceleration: therefore -a + Self::new(v0, -a) + } + + /// Calculates the remaining distance to the limit value at a given time based on the initial velocity and deceleration. + pub fn remaining_distance(&self, time_elapsed: core::time::Duration) -> f32 { + debug_assert!(self.deceleration != 0., "deceleration must not be zero"); + debug_assert!( + self.deceleration.signum() == self.initial_velocity.signum(), + "deceleration must actually decelerate the velocity" + ); + + // The animation stops if the velocity becomes zero. + // Therefore we can calculate the animation duration based on the initial velocity and deceleration: + // v0 + a * t = 0 + // => t = -v0 / a + // Note: our deceleration is `-a` negated + let total_duration = self.initial_velocity / self.deceleration; + + if time_elapsed.as_secs_f32() < total_duration { + // Based on the equations of motion for constant acceleration we can calculate the remaining distance at a given time: + 0.5 * (-self.deceleration) + * (total_duration.powi(2) - time_elapsed.as_secs_f32().powi(2)) + + self.initial_velocity * (total_duration - time_elapsed.as_secs_f32()) + } else { + 0. + } + } } impl Parameter for ConstantDecelerationParameters { type Output = ConstantDeceleration; - fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output { + fn simulation( + self, + start_value: f32, + limit_value: core::pin::Pin>>, + ) -> Self::Output { ConstantDeceleration::new(start_value, limit_value, self) } } @@ -61,8 +133,7 @@ impl Parameter for ConstantDecelerationParameters { pub struct ConstantDeceleration { /// If the limit is not reached, it is also fine. Also exceeding the limit can be ok, /// but at the end of the animation the limit shall not be exceeded - limit_value: f32, - curr_val: f32, + limit_value: core::pin::Pin>>, velocity: f32, data: ConstantDecelerationParameters, direction: Direction, @@ -76,18 +147,22 @@ impl ConstantDeceleration { /// * `limit_value` - value at which the simulation ends if the velocity did not get zero before /// * `initial_velocity` - the initial velocity of the point /// * `data` - the properties of this simulation - pub fn new(start_value: f32, limit_value: f32, data: ConstantDecelerationParameters) -> Self { + pub fn new( + start_value: f32, + limit_value: core::pin::Pin>>, + data: ConstantDecelerationParameters, + ) -> Self { Self::new_internal(start_value, limit_value, data, crate::animations::current_tick()) } fn new_internal( start_value: f32, - limit_value: f32, + limit_value: core::pin::Pin>>, mut data: ConstantDecelerationParameters, start_time: Instant, ) -> Self { let mut initial_velocity = data.initial_velocity; - let direction = if start_value == limit_value { + let direction = if start_value == limit_value.as_ref().get() { if initial_velocity >= 0. { data.deceleration = f32::abs(data.deceleration); Direction::Increasing @@ -95,7 +170,7 @@ impl ConstantDeceleration { data.deceleration = -f32::abs(data.deceleration); Direction::Decreasing } - } else if start_value < limit_value { + } else if start_value < limit_value.as_ref().get() { data.deceleration = f32::abs(data.deceleration); assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction initial_velocity = f32::abs(initial_velocity); @@ -107,17 +182,12 @@ impl ConstantDeceleration { Direction::Decreasing }; - Self { - limit_value, - curr_val: start_value, - velocity: initial_velocity, - data, - direction, - start_time, - } + Self { limit_value, velocity: initial_velocity, data, direction, start_time } } - fn step_internal(&mut self, new_tick: Instant) -> (f32, bool) { + fn step_internal(&mut self, current: &mut f32, new_tick: Instant) -> bool { + let limit_value = self.limit_value.as_ref().get(); + // We have to prevent go go beyond the limit where velocity gets zero let duration = f32::min( new_tick.duration_since(self.start_time).as_secs_f32(), @@ -128,40 +198,36 @@ impl ConstantDeceleration { let new_velocity = self.velocity - duration * self.data.deceleration; - self.curr_val += duration * (self.velocity + new_velocity) / 2.; // Trapezoidal integration + *current += duration * (self.velocity + new_velocity) / 2.; // Trapezoidal integration self.velocity = new_velocity; match self.direction { Direction::Increasing => { - if self.curr_val >= self.limit_value { - self.curr_val = self.limit_value; + if *current >= limit_value { + *current = limit_value; self.velocity = 0.; - return (self.curr_val, true); + return true; } else if self.velocity <= 0. { - return (self.curr_val, true); + return true; } } Direction::Decreasing => { - if self.curr_val <= self.limit_value { - self.curr_val = self.limit_value; + if *current <= limit_value { + *current = limit_value; self.velocity = 0.; - return (self.curr_val, true); + return true; } else if self.velocity >= 0. { - return (self.curr_val, true); + return true; } } } - (self.curr_val, false) + false } } impl Simulation for ConstantDeceleration { - fn curr_value(&self) -> f32 { - self.curr_val - } - - fn step(&mut self, new_tick: Instant) -> (f32, bool) { - self.step_internal(new_tick) + fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool { + self.step_internal(current, new_tick) } } @@ -170,6 +236,10 @@ mod tests { use super::*; use core::time::Duration; + fn test_limit_property(value: f32) -> core::pin::Pin>> { + alloc::boxed::Box::pin(crate::Property::new(value)) + } + #[test] fn constant_deceleration_start_eq_limit() { const START_VALUE: f32 = 10.; @@ -179,12 +249,17 @@ mod tests { let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION); let time = Instant::now(); - let mut simulation = - ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone()); + let mut simulation = ConstantDeceleration::new_internal( + START_VALUE, + test_limit_property(LIMIT_VALUE), + parameters, + time, + ); - let res = simulation.step(time + Duration::from_hours(10)); - assert_eq!(res.1, true); - assert_eq!(res.0, START_VALUE); + let mut current = START_VALUE; + let finished = simulation.step(&mut current, time + Duration::from_hours(10)); + assert_eq!(finished, true); + assert_eq!(current, START_VALUE); } /// The velocity becomes zero before we are reaching the limit @@ -198,17 +273,22 @@ mod tests { let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION); let mut time = Instant::now(); - let mut simulation = - ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone()); + let mut simulation = ConstantDeceleration::new_internal( + START_VALUE, + test_limit_property(LIMIT_VALUE), + parameters, + time, + ); + let mut current = START_VALUE; // Velocity does not become zero let mut duration = Duration::from_secs(1); assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32() - 0.5 * DECELERATION * duration.as_secs_f32().powi(2) ); @@ -217,15 +297,15 @@ mod tests { duration = Duration::from_hours(10); assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * INITIAL_VELOCITY / DECELERATION - 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2) ); - assert!(res < LIMIT_VALUE); // We reached velocity zero before we reached the position limit + assert!(current < LIMIT_VALUE); // We reached velocity zero before we reached the position limit } /// We reach the position limit before the velocity got zero @@ -238,15 +318,20 @@ mod tests { let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION); let mut time = Instant::now(); - let mut simulation = - ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone()); + let mut simulation = ConstantDeceleration::new_internal( + START_VALUE, + test_limit_property(LIMIT_VALUE), + parameters, + time, + ); + let mut current = START_VALUE; let duration = Duration::from_secs(1); assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); - assert_eq!(res, LIMIT_VALUE); // Limit reached + assert_eq!(current, LIMIT_VALUE); // Limit reached } /// We don't reach the position limit. Before the velocity gets zero @@ -261,16 +346,21 @@ mod tests { let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION); let mut time = Instant::now(); - let mut simulation = - ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone()); + let mut simulation = ConstantDeceleration::new_internal( + START_VALUE, + test_limit_property(LIMIT_VALUE), + parameters, + time, + ); + let mut current = START_VALUE; let mut duration = Duration::from_secs(1); assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32() - INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2) ); @@ -278,10 +368,10 @@ mod tests { duration = Duration::from_hours(10); assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION) - 0.5 * INITIAL_VELOCITY.signum() @@ -289,7 +379,7 @@ mod tests { * (INITIAL_VELOCITY / DECELERATION).powi(2) ); - assert!(res > LIMIT_VALUE); // We reached velocity zero before we reached the position limit + assert!(current > LIMIT_VALUE); // We reached velocity zero before we reached the position limit } /// We reach the position limit before the velocity got zero @@ -303,15 +393,20 @@ mod tests { let parameters = ConstantDecelerationParameters::new(INITIAL_VELOCITY, DECELERATION); let mut time = Instant::now(); - let mut simulation = - ConstantDeceleration::new_internal(START_VALUE, LIMIT_VALUE, parameters, time.clone()); + let mut simulation = ConstantDeceleration::new_internal( + START_VALUE, + test_limit_property(LIMIT_VALUE), + parameters, + time, + ); + let mut current = START_VALUE; let duration = Duration::from_secs(3); assert!(f32::abs(DECELERATION * duration.as_secs_f32()) > f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); - assert_eq!(res, LIMIT_VALUE); // Limit reached + assert_eq!(current, LIMIT_VALUE); // Limit reached } } @@ -357,7 +452,11 @@ impl ConstantDecelerationSpringDamperParameters { #[cfg(test)] impl Parameter for ConstantDecelerationSpringDamperParameters { type Output = ConstantDecelerationSpringDamper; - fn simulation(self, start_value: f32, limit_value: f32) -> Self::Output { + fn simulation( + self, + start_value: f32, + limit_value: core::pin::Pin>>, + ) -> Self::Output { ConstantDecelerationSpringDamper::new(start_value, limit_value, self) } } @@ -379,9 +478,8 @@ enum State { pub struct ConstantDecelerationSpringDamper { /// If the limit is not reached, it is also fine. Also exceeding the limit can be ok, /// but at the end of the animation the limit shall not be exceeded - limit_value: f32, + limit_value: core::pin::Pin>>, curr_val_zeroed: f32, - curr_val: f32, velocity: f32, data: ConstantDecelerationSpringDamperParameters, direction: Direction, @@ -400,7 +498,7 @@ pub struct ConstantDecelerationSpringDamper { impl ConstantDecelerationSpringDamper { pub fn new( start_value: f32, - limit_value: f32, + limit_value: core::pin::Pin>>, data: ConstantDecelerationSpringDamperParameters, ) -> Self { Self::new_internal(start_value, limit_value, data, crate::animations::current_tick()) @@ -408,13 +506,13 @@ impl ConstantDecelerationSpringDamper { fn new_internal( start_value: f32, - limit_value: f32, + limit_value: core::pin::Pin>>, mut data: ConstantDecelerationSpringDamperParameters, start_time: Instant, ) -> Self { let mut initial_velocity = data.initial_velocity; let mut state = State::Deceleration; - let direction = if start_value == limit_value { + let direction = if start_value == limit_value.as_ref().get() { state = State::Done; if initial_velocity >= 0. { data.deceleration = f32::abs(data.deceleration); @@ -423,7 +521,7 @@ impl ConstantDecelerationSpringDamper { data.deceleration = -f32::abs(data.deceleration); Direction::Decreasing } - } else if start_value < limit_value { + } else if start_value < limit_value.as_ref().get() { data.deceleration = f32::abs(data.deceleration); assert!(initial_velocity >= 0.); // Makes no sense yet that the velocity goes into the other direction initial_velocity = f32::abs(initial_velocity); @@ -448,7 +546,6 @@ impl ConstantDecelerationSpringDamper { Self { limit_value, - curr_val: start_value, curr_val_zeroed: 0., velocity: initial_velocity, data, @@ -463,15 +560,23 @@ impl ConstantDecelerationSpringDamper { } } - fn step_internal(&mut self, new_tick: Instant) -> (f32, bool) { + fn new_value(&self) -> f32 { + self.limit_value.as_ref().get() + self.curr_val_zeroed + } + + fn step_internal(&mut self, current: &mut f32, new_tick: Instant) -> bool { match self.state { - State::Deceleration => self.state_deceleration(new_tick), - State::SpringDamper => self.state_spring_damper(new_tick), - State::Done => (self.curr_value(), true), + State::Deceleration => self.state_deceleration(current, new_tick), + State::SpringDamper => self.state_spring_damper(current, new_tick), + State::Done => { + *current = self.new_value(); + true + } } } - fn state_deceleration(&mut self, new_tick: Instant) -> (f32, bool) { + fn state_deceleration(&mut self, current: &mut f32, new_tick: Instant) -> bool { + let limit_value = self.limit_value.as_ref().get(); let duration_unlimited = new_tick.duration_since(self.start_time); // We have to prevent go go beyond the limit where velocity gets zero let duration = f32::min( @@ -482,7 +587,7 @@ impl ConstantDecelerationSpringDamper { self.start_time = new_tick; let new_velocity = self.velocity - (duration * self.data.deceleration); - let new_val = self.curr_val + (duration * (self.velocity + new_velocity) / 2.); // Trapezoidal integration + let new_val = *current + (duration * (self.velocity + new_velocity) / 2.); // Trapezoidal integration enum S { LimitReached, @@ -491,9 +596,9 @@ impl ConstantDecelerationSpringDamper { } let s = match self.direction { - Direction::Increasing if new_val > self.limit_value => S::LimitReached, + Direction::Increasing if new_val > limit_value => S::LimitReached, Direction::Increasing if new_velocity <= 0. => S::VelocityZero, - Direction::Decreasing if new_val < self.limit_value => S::LimitReached, + Direction::Decreasing if new_val < limit_value => S::LimitReached, Direction::Decreasing if new_velocity >= 0. => S::VelocityZero, _ => S::None, }; @@ -504,8 +609,7 @@ impl ConstantDecelerationSpringDamper { // time when reaching the limit // solving p_limit = p_old + v_old * dt - 0.5 * a * dt^2 let root = f32::sqrt( - self.velocity.powi(2) - - self.data.deceleration * (self.limit_value - self.curr_val) as f32, + self.velocity.powi(2) - self.data.deceleration * (limit_value - *current), ); // The smaller is the relevant. The larger is when the initial velocity got zero and due to the constant acceleration we turn let dt = f32::min( @@ -515,7 +619,7 @@ impl ConstantDecelerationSpringDamper { self.velocity -= dt * self.data.deceleration; // Velocity at limit value point. Solved `new_val` equation for new_velocity self.curr_val_zeroed = 0.; - self.curr_val = self.limit_value; + *current = limit_value; const X0: f32 = 0.; // Relative point self.constant_a = self.velocity.signum() @@ -527,6 +631,7 @@ impl ConstantDecelerationSpringDamper { self.constant_phi = f32::atan(self.w_d * X0 / (self.velocity + self.damping_ratio * self.w_n * X0)); self.state_spring_damper( + current, new_tick + (duration_unlimited - core::time::Duration::from_millis((dt * 1000.) as u64)), @@ -534,18 +639,18 @@ impl ConstantDecelerationSpringDamper { } S::VelocityZero => { self.velocity = 0.; - self.curr_val = new_val; - (self.curr_val, true) + *current = new_val; + true } S::None => { self.velocity = new_velocity; - self.curr_val = new_val; - (self.curr_val, false) + *current = new_val; + false } } } - fn state_spring_damper(&mut self, new_tick: Instant) -> (f32, bool) { + fn state_spring_damper(&mut self, current: &mut f32, new_tick: Instant) -> bool { // Here we use absolute time because it simplifies the equation let t = (new_tick - self.start_time).as_secs_f32(); // Underdamped spring damper equation @@ -555,36 +660,34 @@ impl ConstantDecelerationSpringDamper { * f32::sin(self.w_d * t + self.constant_phi); self.curr_val_zeroed = new_val; // relative value + let limit_value = self.limit_value.as_ref().get(); let max_time = 2. * core::f32::consts::PI / self.w_d; - let current_val = self.curr_value(); + let current_val = self.new_value(); + *current = current_val; let finished = match self.direction { Direction::Increasing => { // We are comming back from a value higher than the limit - current_val < self.limit_value || t > max_time + current_val < limit_value || t > max_time } Direction::Decreasing => { // We are comming back from a value lower than the limit - current_val > self.limit_value || t > max_time + current_val > limit_value || t > max_time } }; if finished { self.velocity = 0.; - self.curr_val = self.limit_value; + *current = limit_value; self.curr_val_zeroed = 0.; self.state = State::Done; } - (current_val, finished) + finished } } #[cfg(test)] impl Simulation for ConstantDecelerationSpringDamper { - fn curr_value(&self) -> f32 { - self.curr_val + self.curr_val_zeroed - } - - fn step(&mut self, new_tick: Instant) -> (f32, bool) { - self.step_internal(new_tick) + fn step(&mut self, current: &mut f32, new_tick: Instant) -> bool { + self.step_internal(current, new_tick) } } @@ -593,6 +696,10 @@ mod tests_spring_damper { use super::*; use core::{f32::consts::PI, time::Duration}; + fn test_limit_property(value: f32) -> core::pin::Pin>> { + alloc::boxed::Box::pin(crate::Property::new(value)) + } + #[test] fn calculate_parameters() { const INITIAL_VELOCITY: f32 = 50.; @@ -627,13 +734,14 @@ mod tests_spring_damper { let time = Instant::now(); let mut simulation = ConstantDecelerationSpringDamper::new_internal( START_VALUE, - LIMIT_VALUE, + test_limit_property(LIMIT_VALUE), parameters, - time.clone(), + time, ); - let res = simulation.step(time); - assert_eq!(res.0, START_VALUE); - assert_eq!(res.1, true); + let mut current = START_VALUE; + let finished = simulation.step(&mut current, time); + assert_eq!(current, START_VALUE); + assert_eq!(finished, true); assert_eq!(simulation.state, State::Done); } @@ -651,17 +759,22 @@ mod tests_spring_damper { ); let mut time = Instant::now(); - let mut simulation = - ConstantDecelerationSpringDamper::new_internal(10., 2000., parameters, time.clone()); + let mut simulation = ConstantDecelerationSpringDamper::new_internal( + 10., + test_limit_property(2000.), + parameters, + time, + ); + let mut current = 10.; // Velocity does not become zero let mut duration = Duration::from_secs(1); assert!(DECELERATION * duration.as_secs_f32() < INITIAL_VELOCITY); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!( - res, + current, 10. + 50. * duration.as_secs_f32() - 0.5 * DECELERATION * duration.as_secs_f32().powi(2) ); @@ -670,15 +783,15 @@ mod tests_spring_damper { duration = Duration::from_hours(10); assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!( - res, + current, 10. + 50. * INITIAL_VELOCITY / DECELERATION - 0.5 * DECELERATION * (INITIAL_VELOCITY / DECELERATION).powi(2) ); - assert!(res < 2000.); // We reached velocity zero before we reached the position limit + assert!(current < 2000.); // We reached velocity zero before we reached the position limit } /// We don't reach the position limit. Before the velocity gets zero @@ -700,18 +813,19 @@ mod tests_spring_damper { let mut time = Instant::now(); let mut simulation = ConstantDecelerationSpringDamper::new_internal( START_VALUE, - LIMIT_VALUE, + test_limit_property(LIMIT_VALUE), parameters, - time.clone(), + time, ); + let mut current = START_VALUE; let mut duration = Duration::from_secs(1); assert!(f32::abs(DECELERATION * duration.as_secs_f32()) < f32::abs(INITIAL_VELOCITY)); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * duration.as_secs_f32() - INITIAL_VELOCITY.signum() * 0.5 * DECELERATION * duration.as_secs_f32().powi(2) ); @@ -719,10 +833,10 @@ mod tests_spring_damper { duration = Duration::from_hours(10); assert!(Duration::from_secs((INITIAL_VELOCITY / DECELERATION) as u64) < duration); time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!( - res, + current, START_VALUE + INITIAL_VELOCITY * f32::abs(INITIAL_VELOCITY / DECELERATION) - 0.5 * INITIAL_VELOCITY.signum() @@ -730,7 +844,7 @@ mod tests_spring_damper { * (INITIAL_VELOCITY / DECELERATION).powi(2) ); - assert!(res > LIMIT_VALUE); // We reached velocity zero before we reached the position limit + assert!(current > LIMIT_VALUE); // We reached velocity zero before we reached the position limit } /// We reach the position limit before the velocity got zero and so we run into the spring damper system @@ -751,30 +865,31 @@ mod tests_spring_damper { let mut time = Instant::now(); let mut simulation = ConstantDecelerationSpringDamper::new_internal( START_VALUE, - LIMIT_VALUE, + test_limit_property(LIMIT_VALUE), parameters, - time.clone(), + time, ); + let mut current = START_VALUE; let duration = Duration::from_secs(1); assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!(simulation.state, State::Deceleration); - assert!(res < LIMIT_VALUE); // We are still in the constant deceleration state + assert!(current < LIMIT_VALUE); // We are still in the constant deceleration state time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64); - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!(simulation.state, State::SpringDamper); - assert!(res > LIMIT_VALUE); + assert!(current > LIMIT_VALUE); time += Duration::from_hours(10); - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!(simulation.state, State::Done); - assert_eq!(res, LIMIT_VALUE); + assert_eq!(current, LIMIT_VALUE); } /// We reach the position limit before the velocity got zero and so we run into the spring damper system @@ -795,29 +910,30 @@ mod tests_spring_damper { let mut time = Instant::now(); let mut simulation = ConstantDecelerationSpringDamper::new_internal( START_VALUE, - LIMIT_VALUE, + test_limit_property(LIMIT_VALUE), parameters, - time.clone(), + time, ); + let mut current = START_VALUE; let duration = Duration::from_secs(1); assert!(f32::abs(DECELERATION) * duration.as_secs_f32() < f32::abs(INITIAL_VELOCITY)); // We don't reach the limit where the velocity gets zero time += duration; - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!(simulation.state, State::Deceleration); - assert!(res > LIMIT_VALUE); // We are still in the constant deceleration state + assert!(current > LIMIT_VALUE); // We are still in the constant deceleration state time += Duration::from_secs((HALF_PERIOD_TIME / 2.) as u64); - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, false); assert_eq!(simulation.state, State::SpringDamper); - assert!(res < LIMIT_VALUE); + assert!(current < LIMIT_VALUE); time += Duration::from_hours(10); - let (res, finished) = simulation.step(time); + let finished = simulation.step(&mut current, time); assert_eq!(finished, true); assert_eq!(simulation.state, State::Done); - assert_eq!(res, LIMIT_VALUE); + assert_eq!(current, LIMIT_VALUE); } } diff --git a/internal/core/items/flickable.rs b/internal/core/items/flickable.rs index 3f9f9a78b4d..c7074d76fc3 100644 --- a/internal/core/items/flickable.rs +++ b/internal/core/items/flickable.rs @@ -6,11 +6,11 @@ //! The `Flickable` item use super::{ - Item, ItemConsts, ItemRc, ItemRendererRef, KeyEventResult, PointerEventButton, - PropertyAnimation, RenderingResult, VoidArg, + Item, ItemConsts, ItemRc, ItemRendererRef, KeyEventResult, PointerEventButton, RenderingResult, + VoidArg, }; -use crate::animations::physics_simulation; -use crate::animations::{EasingCurve, Instant}; +use crate::animations::Instant; +use crate::animations::physics_simulation::ConstantDecelerationParameters; use crate::input::InternalKeyEvent; use crate::input::{ FocusEvent, FocusEventResult, InputEventFilterResult, InputEventResult, MouseEvent, TouchPhase, @@ -38,7 +38,7 @@ use i_slint_core_macros::*; #[allow(unused)] use num_traits::Float; mod data_ringbuffer; -use data_ringbuffer::PositionTimeRingBuffer; +use data_ringbuffer::VelocityRingBuffer; /// Deceleration during the animation. It slows down the initial velocity of the simulation /// so that the simulation stops at some point if it didn't reach the limit @@ -47,8 +47,7 @@ const DECELERATION: f32 = 2000.; /// Fixed-duration animation used for wheel scrolling, where we don't have enough phase /// information to derive a fling velocity. /// The unit is: millisecond -const WHEEL_SCROLL_DURATION: i32 = 180; -const WHEEL_SCROLL_EASING: EasingCurve = EasingCurve::CubicBezier([0.0, 0.0, 0.58, 1.0]); +const WHEEL_SCROLL_DURATION: Duration = Duration::from_millis(180); /// The maximum duration between a move and a release event to start an animation /// If the duration is larger than this value, no animation will be executed because /// it is not desired @@ -147,7 +146,7 @@ impl Item for Flickable { || pos.y < 0 as _ || pos.x_length() > geometry.width_length() || pos.y_length() > geometry.height_length()) - && self.data.inner.borrow().pressed_time.is_none() + && self.data.inner.borrow().pressed_mouse_state.is_none() { return InputEventFilterResult::Intercept; } @@ -385,11 +384,14 @@ enum CaptureEvents { #[derive(Default)] struct FlickableDataInner { - /// The position in which the press was made - pressed_pos: LogicalPoint, - pressed_time: Option, - pressed_viewport_pos: LogicalPoint, - pressed_viewport_size: LogicalSize, + /// The time and position in which the press was made + /// + /// The position is in the coordinate system of the flickable, not of the viewport. + pressed_mouse_state: Option<(Instant, LogicalPoint)>, + /// The last mouse position received, used to calculate the delta when flicking with the mouse. + /// + /// This position is in the coordinate system of the flickable, not of the viewport. + last_mouse_position: LogicalPoint, /// Set to true if the flickable is flicking and capturing all mouse event, not forwarding back to the children capture_events: Option, /// Heuristics for filtering scroll events from children after we have scrolled ourselves. @@ -399,22 +401,17 @@ struct FlickableDataInner { /// stop filtering scroll event until the next scroll event. last_scroll_event: Option<(Instant, LogicalPoint)>, - /// Ringbuffer to store the last move events. From those data the velocity can be + /// Ringbuffer to store the last move deltas. From those data the velocity can be /// calculated required for the animation after the release event - position_time_rb: PositionTimeRingBuffer<5>, + velocity_rb: VelocityRingBuffer<5>, - final_pos: Option, + /// The animation details of the currently running animation for smooth mouse wheel scrolling. + /// This allows us to add the missing delta of the animation to the next scroll event if the user scrolls again + /// before the animation is finished. + running_animation: Option<(Instant, [Option; 2])>, } impl FlickableDataInner { - fn wheel_scroll_animation() -> PropertyAnimation { - PropertyAnimation { - duration: WHEEL_SCROLL_DURATION, - easing: WHEEL_SCROLL_EASING, - ..Default::default() - } - } - fn should_capture_scroll(&self, timeout: Duration, position: LogicalPoint) -> bool { self.last_scroll_event.is_some_and(|(last_time, last_position)| { // Note: Squared length for MCU support, which use i32 coords. @@ -439,7 +436,7 @@ impl FlickableDataInner { fn process_wheel_event( &mut self, flick: Pin<&Flickable>, - delta: LogicalVector, + mut delta: LogicalVector, position: LogicalPoint, phase: TouchPhase, flick_rc: &ItemRc, @@ -451,48 +448,91 @@ impl FlickableDataInner { // Release the capture immediately, this event is not meant for this Flickable. self.capture_events = None; self.last_scroll_event = None; + self.running_animation = None; + self.velocity_rb = VelocityRingBuffer::default(); return InputEventResult::EventIgnored; } let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); - let mut old_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); + let current_pos = LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); if self.capture_events.is_none() && matches!(phase, TouchPhase::Moved) - && let Some(pos) = self.final_pos + && let Some((start_time, [x_simulation, y_simulation])) = &self.running_animation { - // If the animation is not finished, we use final value of the animation, otherwise we slow the scrolling down - old_pos = pos; + // If the animation is not finished, we add the remaining animations delta. + let animation_duration = crate::animations::current_tick().duration_since(*start_time); + + if let Some(x_simulation) = x_simulation { + delta.x += x_simulation.remaining_distance(animation_duration); + } + if let Some(y_simulation) = y_simulation { + delta.y += y_simulation.remaining_distance(animation_duration); + } } - let new_pos = ensure_in_bound(flick, old_pos + delta, flick_rc); + let new_pos = ensure_in_bound(flick, current_pos + delta, flick_rc); + delta = new_pos - current_pos; + + if phase != TouchPhase::Ended { + viewport_x.remove_binding(); + viewport_y.remove_binding(); + self.running_animation = None; + } match phase { TouchPhase::Cancelled => { viewport_x.set(new_pos.x_length()); viewport_y.set(new_pos.y_length()); self.last_scroll_event = Some((crate::animations::current_tick(), position)); - self.final_pos = None; } TouchPhase::Started => { - self.position_time_rb = PositionTimeRingBuffer::default(); + self.velocity_rb = VelocityRingBuffer::default(); self.capture_events = Some(CaptureEvents::MouseWheel); self.last_scroll_event = Some((crate::animations::current_tick(), position)); - self.final_pos = None; } TouchPhase::Moved => { if self.capture_events.is_some_and(|capture| capture == CaptureEvents::MouseWheel) { // Touchpad case with different phases - self.position_time_rb.push(crate::animations::current_tick(), new_pos); + self.velocity_rb.push(crate::animations::current_tick(), new_pos - current_pos); viewport_x.set(new_pos.x_length()); viewport_y.set(new_pos.y_length()); } else { // Mousewheel case with no phase - let animation = Self::wheel_scroll_animation(); - viewport_x.set_animated_value(new_pos.x_length(), animation.clone()); - viewport_y.set_animated_value(new_pos.y_length(), animation); - self.final_pos = Some(new_pos); + // Add a short animation that covers the delta for smooth scrolling + // + // Note that this animation must support the viewport_x/_y and width/height + // changing, as e.g. the ListView might resize the viewport if it gets a new size + // estimate. + // + // At the time of writing, in practice this means we must use a physics animation. + let [limit_x, limit_y] = Self::flick_limits(flick_rc, delta); + + let x_simulation = (delta.x != 0.).then(|| { + let simulation = ConstantDecelerationParameters::new_with_distance( + delta.x, + WHEEL_SCROLL_DURATION.as_secs_f32(), + ); + viewport_x.set_physic_animation_value(limit_x, simulation.clone()); + simulation + }); + + let y_simulation = (delta.y != 0.).then(|| { + let simulation = ConstantDecelerationParameters::new_with_distance( + delta.y, + WHEEL_SCROLL_DURATION.as_secs_f32(), + ); + viewport_y.set_physic_animation_value(limit_y, simulation.clone()); + simulation + }); + + if delta.x != 0 as Coord || delta.y != 0 as Coord { + (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&()); + } + + self.running_animation = + Some((crate::animations::current_tick(), [x_simulation, y_simulation])); } self.last_scroll_event = Some((crate::animations::current_tick(), position)); } @@ -509,8 +549,8 @@ impl FlickableDataInner { } } - let flicked = - old_pos.x_length() != new_pos.x_length() || old_pos.y_length() != new_pos.y_length(); + let flicked = current_pos.x_length() != new_pos.x_length() + || current_pos.y_length() != new_pos.y_length(); if flicked { (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&()); InputEventResult::EventAccepted @@ -524,44 +564,79 @@ impl FlickableDataInner { } } - fn animate(&self, flick: Pin<&Flickable>, flick_rc: &ItemRc) { - if let Some(last_time) = self.position_time_rb.last_time() { - let (time, dist) = self.position_time_rb.diff(); - let millis = time.as_millis(); + fn flick_limits( + flick_rc: &ItemRc, + flick_velocity: LogicalVector, + ) -> [Pin>>; 2] { + let flick_weak = flick_rc.downgrade(); + let calculate_limits = move || { + flick_weak + .upgrade() + .and_then(|flick_rc| { + flick_rc.downcast::().map(move |flick| (flick_rc, flick)) + }) + .map(|(flick_rc, flick)| { + let flick = flick.as_pin_ref(); + ensure_in_bound( + flick, + LogicalPoint::from_lengths( + -flick.viewport_width(), + -flick.viewport_height(), + ), + &flick_rc, + ) + }) + }; + + let limit_x = if flick_velocity.x < 0 as Coord { + let property = Box::pin(Property::new(0.0)); + property.set_binding({ + let calculate_limits = calculate_limits.clone(); + move || calculate_limits().map(|limit| limit.x_length().get()).unwrap_or(0.0) + }); + property + } else { + Box::pin(Property::new(0.0)) + }; + + let limit_y = if flick_velocity.y < 0 as Coord { + let property = Box::pin(Property::new(0.0)); + property.set_binding(move || { + calculate_limits().map(|limit| limit.y_length().get()).unwrap_or(0.0) + }); + property + } else { + Box::pin(Property::new(0.0)) + }; + + [limit_x, limit_y] + } + fn animate(&self, flick: Pin<&Flickable>, flick_rc: &ItemRc) { + if let Some(last_time) = self.velocity_rb.last_time() { + let mean_velocity = self.velocity_rb.mean_velocity(); if self.capture_events.is_some() - && dist.square_length() > (DISTANCE_THRESHOLD.get() * DISTANCE_THRESHOLD.get()) as _ - && millis > 0 + && mean_velocity.square_length() > 0 as Coord && crate::animations::current_tick().duration_since(last_time) < MAX_DURATION { let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); - let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(); - let vh = (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(); - let limit_x = - if dist.x < 0 as Coord { -vw } else { euclid::Length::new(Coord::default()) }; - let limit_y = - if dist.y < 0 as Coord { -vh } else { euclid::Length::new(Coord::default()) }; - - let limit = - ensure_in_bound(flick, LogicalPoint::from_lengths(limit_x, limit_y), flick_rc); + + let [limit_x, limit_y] = Self::flick_limits(flick_rc, mean_velocity); + { - let simulation = physics_simulation::ConstantDecelerationParameters::new( - dist.x as f32 / (millis as f32 / 1000.), - DECELERATION, - ); - viewport_x.set_physic_animation_value(limit.x_length(), simulation); + let simulation = + ConstantDecelerationParameters::new(mean_velocity.x, DECELERATION); + viewport_x.set_physic_animation_value(limit_x, simulation); } { - let animation_y = physics_simulation::ConstantDecelerationParameters::new( - dist.y as f32 / (millis as f32 / 1000.), - DECELERATION, - ); - viewport_y.set_physic_animation_value(limit.y_length(), animation_y); + let animation_y = + ConstantDecelerationParameters::new(mean_velocity.y, DECELERATION); + viewport_y.set_physic_animation_value(limit_y, animation_y); } - if dist.x != 0 as Coord || dist.y != 0 as Coord { + if mean_velocity.x != 0 as Coord || mean_velocity.y != 0 as Coord { (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&()); } } @@ -603,21 +678,13 @@ impl FlickableData { let mut inner = self.inner.borrow_mut(); match event { MouseEvent::Pressed { position, button: PointerEventButton::Left, .. } => { - inner.position_time_rb = PositionTimeRingBuffer::default(); - inner.pressed_pos = *position; - inner.pressed_time = Some(crate::animations::current_tick()); - inner.pressed_viewport_pos = LogicalPoint::from_lengths( - (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(), - (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(), - ); - inner.pressed_viewport_size = LogicalSize::from_lengths( - (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(), - (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(), - ); - let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); - x.set(x.get()); // Stop animation by removing the binding - let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); - y.set(y.get()); // Stop animation by removing the binding + inner.velocity_rb = VelocityRingBuffer::default(); + inner.pressed_mouse_state = Some((crate::animations::current_tick(), *position)); + inner.last_mouse_position = *position; + let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); + viewport_x.remove_binding(); // Stop animation by removing the binding + let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); + viewport_y.remove_binding(); // Stop animation by removing the binding if inner.capture_events.is_some() { InputEventFilterResult::Intercept @@ -626,7 +693,7 @@ impl FlickableData { } } MouseEvent::Exit | MouseEvent::Released { button: PointerEventButton::Left, .. } => { - inner.pressed_time = None; + inner.pressed_mouse_state = None; if inner.capture_events.is_some() { InputEventFilterResult::Intercept } else { @@ -635,28 +702,17 @@ impl FlickableData { } MouseEvent::Moved { position, .. } => { let do_intercept = inner.capture_events.is_some() - || inner.pressed_time.is_some_and(|pressed_time| { - if crate::animations::current_tick() - pressed_time > DURATION_THRESHOLD { - return false; - } - // Check if the mouse was moved more than the DISTANCE_THRESHOLD in a - // direction in which the flickable can flick - let diff = *position - inner.pressed_pos; - let geo = Flickable::geometry_without_virtual_keyboard(flick_rc); - let w = geo.width_length(); - let h = geo.height_length(); - let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(); - let vh = - (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(); - let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(); - let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(); - let zero = LogicalLength::zero(); - ((vw > w || x != zero) && abs(diff.x_length()) > DISTANCE_THRESHOLD) - || ((vh > h || y != zero) && abs(diff.y_length()) > DISTANCE_THRESHOLD) - }); + || inner.pressed_mouse_state.is_some_and( + |(pressed_time, pressed_mouse_position)| { + let mouse_delta = *position - pressed_mouse_position; + + crate::animations::current_tick() - pressed_time <= DURATION_THRESHOLD + && self.should_capture_mouse_direction(mouse_delta, flick, flick_rc) + }, + ); if do_intercept { InputEventFilterResult::Intercept - } else if inner.pressed_time.is_some() { + } else if inner.pressed_mouse_state.is_some() { InputEventFilterResult::ForwardAndInterceptGrab } else { InputEventFilterResult::ForwardEvent @@ -719,6 +775,27 @@ impl FlickableData { } } + fn should_capture_mouse_direction( + &self, + mouse_delta: LogicalVector, + flick: Pin<&Flickable>, + flick_rc: &ItemRc, + ) -> bool { + let flickable_geometry = Flickable::geometry_without_virtual_keyboard(flick_rc); + let flickable_width = flickable_geometry.width_length(); + let flickable_height = flickable_geometry.height_length(); + let viewport_width = flick.viewport_width(); + let viewport_height = flick.viewport_height(); + let zero = LogicalLength::zero(); + + // We should capture the mouse movement, if the flickable can move in this + // axis, and the mouse has moved more than the threshold in this axis. + ((viewport_width > flickable_width || flick.viewport_x() != zero) + && abs(mouse_delta.x_length()) > DISTANCE_THRESHOLD) + || ((viewport_height > flickable_height || flick.viewport_y() != zero) + && abs(mouse_delta.y_length()) > DISTANCE_THRESHOLD) + } + fn handle_mouse( &self, flick: Pin<&Flickable>, @@ -736,83 +813,91 @@ impl FlickableData { if inner.capture_events.is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen) { let was_capturing = true; inner.animate(flick, flick_rc); - inner.final_pos = None; inner.capture_events = None; - inner.pressed_time = None; + inner.pressed_mouse_state = None; if was_capturing { InputEventResult::EventAccepted } else { InputEventResult::EventIgnored } } else if inner.capture_events.is_none() { - inner.pressed_time = None; + inner.pressed_mouse_state = None; InputEventResult::EventIgnored } else { InputEventResult::EventIgnored } } MouseEvent::Moved { position, .. } => { - if inner.pressed_time.is_some() { - inner.final_pos = None; - inner.position_time_rb.push(crate::animations::current_tick(), *position); - let current_viewport_size = LogicalSize::from_lengths( - (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(), - (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(), - ); - - // Update reference points when the size of the viewport changes to - // avoid 'jumping' during scrolling. - // This happens when the height estimate of a ListView changes after - // new items are loaded. - if current_viewport_size != inner.pressed_viewport_size { - inner.pressed_viewport_size = current_viewport_size; - - inner.pressed_viewport_pos = LogicalPoint::from_lengths( - (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick).get(), - (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick).get(), - ); - - inner.pressed_pos = *position; - }; - - let new_pos = inner.pressed_viewport_pos + (*position - inner.pressed_pos); - - let x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); - let y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); - let should_capture = || { - let geo = Flickable::geometry_without_virtual_keyboard(flick_rc); - let w = geo.width_length(); - let h = geo.height_length(); - let vw = (Flickable::FIELD_OFFSETS.viewport_width()).apply_pin(flick).get(); - let vh = - (Flickable::FIELD_OFFSETS.viewport_height()).apply_pin(flick).get(); - let zero = LogicalLength::zero(); - ((vw > w || x.get() != zero) - && abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD) - || ((vh > h || y.get() != zero) - && abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD) - }; - - if inner.capture_events.is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen) - || should_capture() + // Important constraint: The viewport_y might not be stable, and might jump around + // wildly! + // This is especially the case if a ListView is involved, which will continuously + // update its own viewport_y to keep the current item visible, which can cause the + // viewport_y to jump. + // + // So to correctly calculate the mouse delta, we need to use the position of + // the mouse in the flickables coordinate system and never the viewport coordinate + // system. + if let Some((_pressed_time, _pressed_mouse_position)) = inner.pressed_mouse_state { + let mouse_delta = *position - inner.last_mouse_position; + inner.velocity_rb.push(crate::animations::current_tick(), mouse_delta); + + let is_capturing = inner + .capture_events + .is_some_and(|f| f == CaptureEvents::MouseOrTouchScreen); + if is_capturing + || self.should_capture_mouse_direction(mouse_delta, flick, flick_rc) { - let new_pos = ensure_in_bound(flick, new_pos, flick_rc); - - let old_pos = (x.get(), y.get()); - x.set(new_pos.x_length()); - y.set(new_pos.y_length()); - if old_pos.0 != new_pos.x_length() || old_pos.1 != new_pos.y_length() { + // The drag event is meant to move the viewport, set it to the new position + // and start capturing mouse events. + let viewport_x = (Flickable::FIELD_OFFSETS.viewport_x()).apply_pin(flick); + let viewport_y = (Flickable::FIELD_OFFSETS.viewport_y()).apply_pin(flick); + let current_viewport_position = + LogicalPoint::from_lengths(viewport_x.get(), viewport_y.get()); + + // We calculate the new viewport position by adding the mouse delta in the flickable + // coordinate system to the current viewport position. + // Do not rely on the existing viewport position to be stable, as e.g. the + // ListView will continuously update it. + // So we cannot calculate the delta in viewport coordinates. + let new_viewport_position = current_viewport_position + mouse_delta; + let new_viewport_position = + ensure_in_bound(flick, new_viewport_position, flick_rc); + + viewport_x.set(new_viewport_position.x_length()); + viewport_y.set(new_viewport_position.y_length()); + if current_viewport_position != new_viewport_position { (Flickable::FIELD_OFFSETS.flicked()).apply_pin(flick).call(&()); } + // Only update the mouse position if we are actually applying the delta. + // When the drag starts, there is a short dead zone that is determined by the + // DISTANCE_THRESHOLD. We want to apply that threshold to the + // delta once we've overcome it, so we need to update the position that we + // calculate the delta from only after we've cleared the deadzone and are + // actually moving. + // + // Note: As an alternative to updating the last_mouse_position to the new mouse position, + // we could also update it by the amount that the viewport actually moved. + // This would cause the mouse to stick to a given position in the viewport + // instead of starting to drift if the drag goes into the viewport limits. + // Then this code would need to be: + // + // inner.last_mouse_position += new_viewport_position - current_viewport_position; + // + // But at least for a touchscreen, the current behavior is more intuitive. + inner.last_mouse_position = *position; + inner.capture_events = Some(CaptureEvents::MouseOrTouchScreen); + InputEventResult::GrabMouse - } else if abs(x.get() - new_pos.x_length()) > DISTANCE_THRESHOLD - || abs(y.get() - new_pos.y_length()) > DISTANCE_THRESHOLD + } else if abs(mouse_delta.x_length()) > DISTANCE_THRESHOLD + || abs(mouse_delta.y_length()) > DISTANCE_THRESHOLD { // drag in a unsupported direction gives up the grab InputEventResult::EventIgnored } else { + // the mouse was moved, but not enough to start the drag, we still want to accept further events + // so that we may pass the threshold at some point InputEventResult::EventAccepted } } else { diff --git a/internal/core/items/flickable/data_ringbuffer.rs b/internal/core/items/flickable/data_ringbuffer.rs index c5ea7ba0db9..610a982580d 100644 --- a/internal/core/items/flickable/data_ringbuffer.rs +++ b/internal/core/items/flickable/data_ringbuffer.rs @@ -1,40 +1,39 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -//! This module contains a simple ringbuffer to store time and location tuples. It is used in the flickable to -//! determine the initial velocity of the animation +//! This module contains a simple ringbuffer to store time and delta tuples. +//! It is used in the flickable to determine the initial velocity of the animation. use crate::Coord; use crate::animations::Instant; -use crate::lengths::LogicalPoint; -use crate::lengths::LogicalPx; +use crate::lengths::{LogicalPx, LogicalVector}; use core::time::Duration; use euclid::Vector2D; -/// Simple ringbuffer storing time and position tuples +/// Simple ringbuffer storing time and delta tuples #[derive(Debug)] -pub(crate) struct PositionTimeRingBuffer { +pub(crate) struct VelocityRingBuffer { /// Pointing to the next free element curr_index: usize, /// Indicates if the buffer is full full: bool, - values: [(Instant, LogicalPoint); N], + values: [(Instant, Vector2D); N], } -impl Default for PositionTimeRingBuffer { +impl Default for VelocityRingBuffer { fn default() -> Self { - Self { curr_index: 0, full: false, values: [(Instant::now(), LogicalPoint::default()); N] } + Self { curr_index: 0, full: false, values: [(Instant::now(), Vector2D::default()); N] } } } -impl PositionTimeRingBuffer { +impl VelocityRingBuffer { /// Indicates if the buffer is empty pub fn empty(&self) -> bool { !(self.full || self.curr_index > 0) } /// Add a new element to the ringbuffer - pub fn push(&mut self, time: Instant, value: LogicalPoint) { + pub fn push(&mut self, time: Instant, value: LogicalVector) { if self.curr_index < self.values.len() { self.values[self.curr_index] = (time, value); } @@ -50,26 +49,38 @@ impl PositionTimeRingBuffer { if self.curr_index > 0 { self.curr_index - 1 } else { N - 1 } } + fn len(&self) -> usize { + if self.full { N } else { self.curr_index } + } + /// Returns the last time value added to the buffer if not empty otherwise None pub fn last_time(&self) -> Option { if !self.empty() { Some(self.values[self.latest_index()].0) } else { None } } - /// Returns the difference between the oldest and the newest point - pub fn diff(&self) -> (Duration, Vector2D) { - if self.full { - let oldest = self.values[self.curr_index]; - let newest = if self.curr_index > 0 { - self.values[self.curr_index - 1] - } else { - self.values[self.values.len() - 1] - }; - (newest.0.duration_since(oldest.0), newest.1 - oldest.1) - } else { - let oldest = self.values[0]; - let newest = self.values[usize::max(0, self.curr_index - 1)]; - (newest.0.duration_since(oldest.0), newest.1 - oldest.1) + pub fn mean_velocity(&self) -> LogicalVector { + let len = self.len(); + if len < 2 { + return Default::default(); + } + + let oldest_index = if self.full { self.curr_index } else { 0 }; + let newest_index = self.latest_index(); + let duration = self.values[newest_index].0.duration_since(self.values[oldest_index].0); + if duration == Duration::ZERO { + return Default::default(); } + + // The oldest recorded delta happened before the oldest timestamp in the covered time span, + // so it does not belong to the average velocity between oldest and newest. + let mut total_delta = LogicalVector::default(); + let mut index = (oldest_index + 1) % N; + for _ in 1..len { + total_delta += self.values[index].1; + index = (index + 1) % N; + } + + total_delta / duration.as_secs_f32() } } @@ -77,116 +88,111 @@ impl PositionTimeRingBuffer { mod tests { use super::*; use crate::animations::Instant; - use crate::lengths::LogicalPoint; use core::time::Duration; #[test] fn test_empty_buffer() { - let buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default(); + let buffer: VelocityRingBuffer<5> = VelocityRingBuffer::default(); assert!(buffer.empty()); assert_eq!(buffer.curr_index, 0); assert!(!buffer.full); assert_eq!(buffer.last_time(), None); + assert_eq!(buffer.mean_velocity(), Vector2D::default()); } #[test] fn test_push_single_element() { - let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default(); + let mut buffer: VelocityRingBuffer<5> = VelocityRingBuffer::default(); let time = Instant::now(); - let point = LogicalPoint::new(10.0, 20.0); + let delta = Vector2D::new(10.0, 20.0); - buffer.push(time, point); + buffer.push(time, delta); assert!(!buffer.empty()); assert_eq!(buffer.curr_index, 1); assert!(!buffer.full); assert_eq!(buffer.latest_index(), 0); assert_eq!(buffer.last_time(), Some(time)); - - assert_eq!(buffer.diff(), (Duration::from_millis(0), Vector2D::new(0., 0.))); + assert_eq!(buffer.mean_velocity(), Vector2D::default()); } /// Buffer not complete full #[test] fn test_push_two_elements() { - let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default(); + let mut buffer: VelocityRingBuffer<5> = VelocityRingBuffer::default(); let time = Instant::now(); - buffer.push(time, LogicalPoint::new(10.0, 20.0)); - buffer.push(time + Duration::from_millis(13), LogicalPoint::new(13.0, -5.0)); + buffer.push(time, Vector2D::new(10.0, 20.0)); + buffer.push(time + Duration::from_millis(100), Vector2D::new(13.0, -5.0)); assert!(!buffer.empty()); assert_eq!(buffer.curr_index, 2); assert!(!buffer.full); assert_eq!(buffer.latest_index(), 1); - assert_eq!(buffer.last_time(), Some(time + Duration::from_millis(13))); + assert_eq!(buffer.last_time(), Some(time + Duration::from_millis(100))); - assert_eq!(buffer.diff(), (Duration::from_millis(13), Vector2D::new(3., -25.))); + assert_eq!(buffer.mean_velocity(), Vector2D::new(130.0, -50.0)); } #[test] fn test_push_until_full() { - let mut buffer: PositionTimeRingBuffer<5> = PositionTimeRingBuffer::default(); + let mut buffer: VelocityRingBuffer<5> = VelocityRingBuffer::default(); let base_time = Instant::now(); // Push elements to fill the buffer for i in 0..5 { - let time = base_time + Duration::from_millis(i * 3 as u64); - let point = LogicalPoint::new(i as f32, -2. * i as f32); - buffer.push(time, point); + let time = base_time + Duration::from_millis(i * 100); + buffer.push(time, Vector2D::new(1.0, -2.0)); } assert!(!buffer.empty()); assert_eq!(buffer.curr_index, 0); assert!(buffer.full); - assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(4 * 3))); + assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(400))); assert_eq!(buffer.latest_index(), 4); - assert_eq!(buffer.diff(), (Duration::from_millis(12), Vector2D::new(4., -8.))); + assert_eq!(buffer.mean_velocity(), Vector2D::new(10.0, -20.0)); } #[test] fn test_push_beyond_capacity() { const CAP: usize = 5; - let mut buffer: PositionTimeRingBuffer = PositionTimeRingBuffer::default(); + let mut buffer: VelocityRingBuffer = VelocityRingBuffer::default(); let base_time = Instant::now(); // Push more than capacity for i in 0..(CAP + 2) { - let time = base_time + Duration::from_millis(i as u64); - let point = LogicalPoint::new(i as f32, i as f32 * 2. + 100.); - buffer.push(time, point); + let time = base_time + Duration::from_millis(i as u64 * 100); + buffer.push(time, Vector2D::new(1.0, 2.0)); } assert!(!buffer.empty()); assert!(buffer.full); assert_eq!(buffer.curr_index, 2); assert_eq!(buffer.latest_index(), 1); - assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(6))); + assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(600))); - assert_eq!(buffer.diff(), (Duration::from_millis(4), Vector2D::new(4., 4. * 2.))); + assert_eq!(buffer.mean_velocity(), Vector2D::new(10.0, 20.0)); } #[test] fn test_push_beyond_capacity_wrap_back() { const CAP: usize = 5; - let mut buffer: PositionTimeRingBuffer = PositionTimeRingBuffer::default(); + let mut buffer: VelocityRingBuffer = VelocityRingBuffer::default(); let base_time = Instant::now(); // Push more than capacity for i in 0..CAP { - let time = base_time + Duration::from_millis(i as u64); - let point = LogicalPoint::new(i as f32 * 3., i as f32 * -2. + 100.); - buffer.push(time, point); + let time = base_time + Duration::from_millis(i as u64 * 100); + buffer.push(time, Vector2D::new(3.0, -2.0)); } assert!(!buffer.empty()); assert!(buffer.full); assert_eq!(buffer.curr_index, 0); assert_eq!(buffer.latest_index(), CAP - 1); - assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(4))); + assert_eq!(buffer.last_time(), Some(base_time + Duration::from_millis(400))); - // Wrapping back must be done - assert_eq!(buffer.diff(), (Duration::from_millis(4), Vector2D::new(4. * 3., 4. * -2.))); + assert_eq!(buffer.mean_velocity(), Vector2D::new(30.0, -20.0)); } } diff --git a/internal/core/model/repeater.rs b/internal/core/model/repeater.rs index 9d051949257..55fd46bd644 100644 --- a/internal/core/model/repeater.rs +++ b/internal/core/model/repeater.rs @@ -349,15 +349,17 @@ fn update_visible_instances( viewport_height.set(LogicalLength::new(state.cached_item_height * row_count as Coord)); viewport_width.set(LogicalLength::new(vp_width)); // If an animation is ongoing we should not interrupt it - if !viewport_y.has_binding() { - let new_viewport_y = -state.anchor_y + new_offset_y; - if new_viewport_y != viewport_y.get().get() { - viewport_y.set(LogicalLength::new(new_viewport_y)); - } - state.previous_viewport_y = new_viewport_y; - } else { - state.previous_viewport_y = viewport_y.get().0; + let new_viewport_y = -state.anchor_y + new_offset_y; + // Important: Use get_internal here, the viewport_y may have a binding on it (especially + // a physical animation). + // We must not yet trigger a re-evaluation of that binding, as we have already updated the + // viewport_width and viewport_height, but the viewport_y is not yet consistent. + // So the physics animations limit value may be inconsistent. + if new_viewport_y != viewport_y.get_internal().get() { + viewport_y.set(LogicalLength::new(new_viewport_y)); } + state.previous_viewport_y = new_viewport_y; + break; } diff --git a/internal/core/properties/properties_animations.rs b/internal/core/properties/properties_animations.rs index 5ceb885057f..c42643e8b82 100644 --- a/internal/core/properties/properties_animations.rs +++ b/internal/core/properties/properties_animations.rs @@ -3,7 +3,7 @@ use super::*; use crate::{ - animations::physics_simulation, + animations::physics_simulation::{self, Simulation}, items::{AnimationDirection, PropertyAnimation}, lengths::LogicalLength, }; @@ -37,25 +37,26 @@ where } /// Single iteration of the animation - pub fn compute_interpolated_value(&mut self) -> (crate::Coord, bool) { + pub fn update_value(&mut self, target: &mut crate::Coord) -> bool { match self.state { AnimationState::Delaying => { // Decide on next state: self.state = AnimationState::Animating { current_iteration: 0 }; - self.compute_interpolated_value() + self.update_value(target) } AnimationState::Animating { current_iteration: _ } => { - let (val, finished) = self.simulation.step(crate::animations::current_tick()); + // TODO: Pass in Coord directly? + let mut value: f32 = *target as f32; + let finished = self.simulation.step(&mut value, crate::animations::current_tick()); + *target = value as crate::Coord; if finished { self.state = AnimationState::Done { iteration_count: 0 }; - self.compute_interpolated_value() + true } else { - (val as crate::Coord, false) + false } } - AnimationState::Done { iteration_count: _ } => { - (self.simulation.curr_value() as crate::Coord, true) - } + AnimationState::Done { iteration_count: _ } => true, } } } @@ -286,6 +287,18 @@ impl InterpolatedPropertyValue for LogicalLength { } impl Property { + /// Evaluate the property and remove the (animation) binding of this property. + /// + /// Note that a binding can intercept this via intercept_set_binding and still remain on the property. + /// (e.g. two-way-bindings will not be removed with this call!) + pub fn remove_binding(self: Pin<&Self>) { + // FIXME: This is a bit of a hack, set_animated_value will call set_binding on the internal handle, + // which will call intercept_set_binding, which will check if the binding should be removed or not. + // In the case of two-way bindings, we want to keep the binding, but reset the value to the current one, + // so that any animation binding is removed, but the two-way-binding is kept. + self.set_animated_value(self.get(), PropertyAnimation::default()); + } + /// Change the value of this property, by animating (interpolating) from the current property's value /// to the specified parameter value. The animation is done according to the parameters described by /// the PropertyAnimation object. @@ -365,36 +378,41 @@ impl Property { } } -impl Property> { +unsafe impl BindingCallable> + for RefCell> +{ + fn evaluate(self: Pin<&Self>, value: &mut Length) -> BindingResult { + let finished = self.borrow_mut().update_value(&mut value.0); + if finished { + BindingResult::RemoveBinding + } else { + crate::animations::CURRENT_ANIMATION_DRIVER + .with(|driver| driver.set_has_active_animations()); + BindingResult::KeepBinding + } + } + + // This binding should not be removed if the value is updated externally. + fn intercept_set(self: Pin<&Self>, _value: &Length) -> bool { + true + } +} + +impl Property> { /// Change the value by using a physics animation pub fn set_physic_animation_value< S: physics_simulation::Simulation + 'static, AD: physics_simulation::Parameter, >( &self, - value: Length, + limit_value: Pin>>, simulation_data: AD, ) { - let d = RefCell::new(PropertyPhysicsAnimationData::new( - simulation_data.simulation(self.get_internal().0 as f32, value.0 as f32), - )); // Safety: the BindingCallable will cast its argument to T unsafe { - self.handle.set_binding( - move |val: &mut Length| { - let (value, finished) = d.borrow_mut().compute_interpolated_value(); - *val = Length::new(value); - if finished { - BindingResult::RemoveBinding - } else { - crate::animations::CURRENT_ANIMATION_DRIVER - .with(|driver| driver.set_has_active_animations()); - BindingResult::KeepBinding - } - }, - #[cfg(slint_debug_property)] - self.debug_name.borrow().as_str(), - ); + self.handle.set_binding::, core::cell::RefCell>>(RefCell::new(PropertyPhysicsAnimationData::new( + simulation_data.simulation(self.get_internal().0 as f32, limit_value), + ))); } self.handle.mark_dirty( #[cfg(slint_debug_property)] diff --git a/tests/cases/elements/flickable_in_flickable.slint b/tests/cases/elements/flickable_in_flickable.slint index eef5767c8d6..80647e77b42 100644 --- a/tests/cases/elements/flickable_in_flickable.slint +++ b/tests/cases/elements/flickable_in_flickable.slint @@ -1,7 +1,7 @@ // Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 -TestCase := Window { +export component TestCase inherits Window { width: 500px; height: 500px; no-frame: false; @@ -11,7 +11,7 @@ TestCase := Window { y: 10px; width: parent.width - 20px; height: parent.height - 20px; - viewport_width: width; + viewport_width: self.width; viewport_height: 980px; inner := Flickable { @@ -22,10 +22,10 @@ TestCase := Window { } } - property outer_y: - outer.viewport-y; - property inner_x: - inner.viewport-x; + out property outer_y: - outer.viewport-y; + out property inner_x: - inner.viewport-x; - property test: outer.viewport-x == 0 && inner.viewport-y == 0; + out property test: outer.viewport-x == 0 && inner.viewport-y == 0; } @@ -49,22 +49,14 @@ assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 0.); // now, scroll down -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 202.0) }); -assert_eq!(instance.get_inner_x(), 0.); -assert_eq!(instance.get_outer_y(), 0.); -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 200.0) }); -assert_eq!(instance.get_inner_x(), 0.); -assert_eq!(instance.get_outer_y(), 0.); -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 198.0) }); -assert_eq!(instance.get_inner_x(), 0.); -assert_eq!(instance.get_outer_y(), 2.); -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 170.0) }); +instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 270.0) }); +// should move the outer one now, it has passed the threshold when moving up already assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 30.); -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 150.0) }); +instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(150.0, 250.0) }); assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 50.); -instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(100.0, 150.0) }); +instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(100.0, 250.0) }); assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 50.); slint_testing::mock_elapsed_time(100); @@ -83,6 +75,9 @@ instance.window().dispatch_event(WindowEvent::PointerPressed { position: Logical slint_testing::mock_elapsed_time(16); assert_eq!(instance.get_inner_x(), 0.); assert_eq!(instance.get_outer_y(), 500.); +// TODO: A small movement will currently break this the delayed pressed event, as the outer flickable +// is then accepting the event, which cancels the delayed pressed event! +// instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(95.0, 100.0) }); instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(90.0, 100.0) }); slint_testing::mock_elapsed_time(120); // we need to wait enough because of the delay in the outer one instance.window().dispatch_event(WindowEvent::PointerMoved { position: LogicalPosition::new(90.0, 100.0) }); diff --git a/tests/cases/elements/listview_differing_heights.slint b/tests/cases/elements/listview_differing_heights.slint new file mode 100644 index 00000000000..5e6e6599817 --- /dev/null +++ b/tests/cases/elements/listview_differing_heights.slint @@ -0,0 +1,249 @@ +// Copyright © SixtyFPS GmbH +// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 + +// Tests that ListView can handle items with different heights using all three scrolling methods: +// 1. mouse/touch dragging +// 2. scroll wheel +// 3. touchpad two-finger scrolling (scroll wheel with a touchphase) + +import { ListView, ScrollView } from "std-widgets.slint"; +export component Test inherits Window { + width: 600px; + height: 600px; + + listview := ListView { + mouse-drag-pan-enabled: true; + + for i in 100: Rectangle { + height: (i.mod(10) * 50 + 10) * 1px; + background: i.mod(2) == 0 ? @linear-gradient(0deg, blue, darkblue) : @linear-gradient(0deg, red, darkred); + Text { + text: i; + font-size: 24px; + } + TouchArea { + // used to detect which element is currently in the middle of the viewport + clicked => { + root.clicked-index = i; + } + } + } + } + + out property height_ <=> self.height; + out property viewport-y <=> listview.viewport-y; + out property viewport-height <=> listview.viewport-height; + + out property clicked-index: 0; +} + + +/* + +```rust +use slint::{platform::PointerEventButton, platform::WindowEvent, LogicalPosition}; +use slint_testing::{LogicalPoint, MouseEvent, TouchPhase}; + +#[derive(Clone, Copy, PartialEq, Eq)] +enum ScrollDirection { + Down, + Up, +} + +impl ScrollDirection { + fn name(self) -> &'static str { + match self { + Self::Down => "down", + Self::Up => "up", + } + } + + fn delta_y(self, delta: f32) -> f32 { + match self { + Self::Down => -delta, + Self::Up => delta, + } + } +} + +let instance = Test::new().unwrap(); +let window = instance.window(); +let inner = slint_testing::WindowInner::from_pub(&window); +let scroll_position = LogicalPoint::new(300.0, 300.0); + +let send_event = |event| { + window.dispatch_event(event); +}; + +let send_pressed_event = |x, y| { + send_event(WindowEvent::PointerPressed { + position: LogicalPosition::new(x, y), + button: PointerEventButton::Left, + }); +}; + +let send_released_event = |x, y| { + send_event(WindowEvent::PointerReleased { + position: LogicalPosition::new(x, y), + button: PointerEventButton::Left, + }); +}; + +let send_moved_event = |x, y| { + send_event(WindowEvent::PointerMoved { position: LogicalPosition::new(x, y) }); +}; + +let send_wheel_event = |delta_y, phase| { + inner.process_mouse_input(MouseEvent::Wheel { + position: scroll_position, + delta_x: 0.0, + delta_y, + phase, + }); +}; + +let probe_index = |direction| { + let probe_y = match direction { + ScrollDirection::Down => instance.get_height_() - 1.0, + ScrollDirection::Up => 1.0, + }; + send_pressed_event(300.0, probe_y); + send_released_event(300.0, probe_y); + instance.get_clicked_index() +}; + +let wait_for_scroll_animation = || { + let mut current_viewport_y = instance.get_viewport_y(); + loop { + slint_testing::mock_elapsed_time(16); + let new_viewport_y = instance.get_viewport_y(); + if current_viewport_y == new_viewport_y { + break; + } + current_viewport_y = new_viewport_y; + } +}; + +let assert_scroll_progress = |current_index: &mut i32, direction: ScrollDirection, method_name: &str| { + let new_index = probe_index(direction); + let progressed = match direction { + ScrollDirection::Down => new_index >= *current_index, + ScrollDirection::Up => new_index <= *current_index, + }; + assert!( + progressed, + "Expected to scroll {} with {}, but the index went from {} to {}", + direction.name(), + method_name, + *current_index, + new_index + ); + *current_index = new_index; +}; + +let run_bidirectional_scroll_test = |method_name: &str, scroll_step: &mut dyn FnMut(ScrollDirection)| { + let target_viewport_y = |direction| match direction { + ScrollDirection::Down => -instance.get_viewport_height() + instance.get_height_(), + ScrollDirection::Up => 0.0, + }; + + for direction in [ScrollDirection::Down, ScrollDirection::Up] { + let mut current_index = probe_index(direction); + let target_index = match direction { + ScrollDirection::Down => 99, + ScrollDirection::Up => 0, + }; + + while match direction { + ScrollDirection::Down => instance.get_viewport_y() > target_viewport_y(direction), + ScrollDirection::Up => instance.get_viewport_y() < target_viewport_y(direction), + } { + // Note: probing the index sends click events, which would disturb the + // active scroll gesture or its velocity calculation. Only probe before + // the gesture starts and after the animation settled. + scroll_step(direction); + wait_for_scroll_animation(); + + assert_scroll_progress(&mut current_index, direction, method_name); + } + + let viewport_y = instance.get_viewport_y(); + let expected_viewport_y = target_viewport_y(direction); + assert_eq!( + viewport_y, + expected_viewport_y, + "Expected {} scrolling with {} to reach {}, but got {}", + direction.name(), + method_name, + expected_viewport_y, + viewport_y + ); + assert_eq!( + instance.get_clicked_index(), + target_index, + "Expected {} scrolling with {} to reach element {}, but got {}", + direction.name(), + method_name, + target_index, + instance.get_clicked_index() + ); + } +}; + +// Test scrolling with mouse dragging. +let mut mouse_drag_step = |direction| { + match direction { + ScrollDirection::Down => { + send_pressed_event(300.0, 300.0); + slint_testing::mock_elapsed_time(16); + + send_moved_event(300.0, 200.0); + slint_testing::mock_elapsed_time(16); + + send_moved_event(300.0, 150.0); + slint_testing::mock_elapsed_time(16); + + send_released_event(300.0, 100.0); + slint_testing::mock_elapsed_time(16); + } + ScrollDirection::Up => { + send_pressed_event(300.0, 50.0); + slint_testing::mock_elapsed_time(16); + + send_moved_event(300.0, 100.0); + slint_testing::mock_elapsed_time(16); + + send_moved_event(300.0, 150.0); + slint_testing::mock_elapsed_time(16); + + send_released_event(300.0, 200.0); + slint_testing::mock_elapsed_time(16); + } + } +}; +run_bidirectional_scroll_test("mouse dragging", &mut mouse_drag_step); + +// Test scrolling with mouse wheel events. Regular wheel events only use TouchPhase::Moved. +let mut mouse_wheel_step = |direction: ScrollDirection| { + send_wheel_event(direction.delta_y(400.0), TouchPhase::Moved); +}; +run_bidirectional_scroll_test("mouse wheel", &mut mouse_wheel_step); + +// Test scrolling with touchpad events. Touchpad scrolling uses Started/Moved/Ended. +let mut touchpad_step = |direction: ScrollDirection| { + send_wheel_event(0.0, TouchPhase::Started); + slint_testing::mock_elapsed_time(16); + + send_wheel_event(direction.delta_y(250.0), TouchPhase::Moved); + slint_testing::mock_elapsed_time(16); + + send_wheel_event(direction.delta_y(150.0), TouchPhase::Moved); + slint_testing::mock_elapsed_time(16); + + send_wheel_event(0.0, TouchPhase::Ended); + slint_testing::mock_elapsed_time(16); +}; +run_bidirectional_scroll_test("touchpad scrolling", &mut touchpad_step); +``` + +*/