From 4658e654a2e444ce0cb84fe5667ea9919932affe Mon Sep 17 00:00:00 2001 From: Jared Moulton Date: Tue, 12 Nov 2024 22:22:18 -0700 Subject: [PATCH] fix drag jitter (#674) --- README.md | 1 - src/app_state.rs | 2 ++ src/context.rs | 66 +++++++++++++++++++++++++++----------------- src/easing.rs | 4 +-- src/window_handle.rs | 7 ++++- 5 files changed, 51 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 3e31857e..ce75bbc6 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ Inspired by [Xilem](https://github.com/linebender/xilem), [Leptos](https://githu - **Cross-platform**: Floem supports Windows, macOS and Linux with rendering using [wgpu](https://github.com/gfx-rs/wgpu). In case a GPU is unavailable, a CPU renderer powered by [tiny-skia](https://github.com/RazrFalcon/tiny-skia) will be used. - **Fine-grained reactivity**: The entire library is built around reactive primitives inspired by [leptos_reactive](https://crates.io/crates/leptos_reactive). The reactive "signals" allow you to keep your UI up-to-date with minimal effort, all while maintaining very high performance. - **Performance**: The view tree is constructed only once, safeguarding you from accidentally creating a bottleneck in a view generation function that slows down your entire application. Floem also provides tools to help you write efficient UI code, such as a [virtual list](https://github.com/lapce/floem/tree/main/examples/virtual_list). -- **Ergonomic API**: Floem aspires to have a highly ergonmic API that is a joy to use. - **Flexbox layout**: Using [Taffy](https://crates.io/crates/taffy), the library provides the Flexbox and Grid layout systems, which can be applied to any View node. - **Customizable widgets**: Widgets are highly customizable. You can customize both the appearance and behavior of widgets using the styling API, which supports theming with classes. You can also install third-party themes. - **Transitions and Animations**: Floem supports both transitions and animations. Transitions, like css transitions, can animate any property that can be interpolated and can be applied alongside other styles, including in classes. diff --git a/src/app_state.rs b/src/app_state.rs index ca9e885a..82c69605 100644 --- a/src/app_state.rs +++ b/src/app_state.rs @@ -44,6 +44,7 @@ pub struct AppState { /// regardless of the status of the animation pub(crate) cursor: Option, pub(crate) last_cursor: CursorIcon, + pub(crate) last_cursor_location: Point, pub(crate) keyboard_navigation: bool, pub(crate) window_menu: HashMap>, pub(crate) context_menu: HashMap>, @@ -76,6 +77,7 @@ impl AppState { hovered: HashSet::new(), cursor: None, last_cursor: CursorIcon::Default, + last_cursor_location: Default::default(), keyboard_navigation: false, grid_bps: GridBreakpoints::default(), window_menu: HashMap::new(), diff --git a/src/context.rs b/src/context.rs index 6180c60a..a26e26b7 100644 --- a/src/context.rs +++ b/src/context.rs @@ -16,6 +16,7 @@ use web_time::{Duration, Instant}; use taffy::prelude::NodeId; use crate::animate::{AnimStateKind, RepeatMode}; +use crate::easing::{Easing, Linear}; use crate::renderer::Renderer; use crate::style::DisplayProp; use crate::view_state::IsHiddenState; @@ -50,6 +51,7 @@ pub struct DragState { pub(crate) id: ViewId, pub(crate) offset: Vec2, pub(crate) released_at: Option, + pub(crate) release_location: Option, } pub(crate) enum FrameUpdate { @@ -253,26 +255,26 @@ impl<'a> EventCx<'a> { .as_ref() .filter(|(drag_id, _)| drag_id == &view_id) { - let vec2 = pointer_event.pos - *drag_start; - + let offset = pointer_event.pos - *drag_start; if let Some(dragging) = self .app_state .dragging .as_mut() .filter(|d| d.id == view_id && d.released_at.is_none()) { - // update the dragging offset if the view is dragging and not released - dragging.offset = vec2; + // update the mouse position if the view is dragging and not released + dragging.offset = drag_start.to_vec2(); self.app_state.request_paint(view_id); - } else if vec2.x.abs() + vec2.y.abs() > 1.0 { + } else if offset.x.abs() + offset.y.abs() > 1.0 { // start dragging when moved 1 px self.app_state.active = None; - self.update_active(view_id); self.app_state.dragging = Some(DragState { id: view_id, - offset: vec2, + offset: drag_start.to_vec2(), released_at: None, + release_location: None, }); + self.update_active(view_id); self.app_state.request_paint(view_id); view_id.apply_event(&EventListener::DragStart, &event); } @@ -312,6 +314,7 @@ impl<'a> EventCx<'a> { { let dragging_id = dragging.id; dragging.released_at = Some(Instant::now()); + dragging.release_location = Some(pointer_event.pos); self.app_state.request_paint(view_id); dragging_id.apply_event(&EventListener::DragEnd, &event); } @@ -1018,41 +1021,50 @@ impl<'a> PaintCx<'a> { paint_border(self, &layout_props, &view_style_props, size); paint_outline(self, &view_style_props, size) } - let mut drag_set_to_none = false; + if let Some(dragging) = self.app_state.dragging.as_ref() { if dragging.id == id { - let dragging_offset = dragging.offset; - let mut offset_scale = None; - if let Some(released_at) = dragging.released_at { - const LIMIT: f64 = 300.0; + let transform = if let Some((released_at, release_location)) = + dragging.released_at.zip(dragging.release_location) + { + let easing = Linear; + const ANIMATION_DURATION_MS: f64 = 300.0; let elapsed = released_at.elapsed().as_millis() as f64; - if elapsed < LIMIT { - offset_scale = Some(1.0 - elapsed / LIMIT); + let progress = elapsed / ANIMATION_DURATION_MS; + + if !(easing.finished(progress)) { + let offset_scale = 1.0 - easing.eval(progress); + let release_offset = release_location.to_vec2() - dragging.offset; + + // Schedule next animation frame exec_after(Duration::from_millis(8), move |_| { id.request_paint(); }); + + Some(self.transform * Affine::translate(release_offset * offset_scale)) } else { drag_set_to_none = true; + None } } else { - offset_scale = Some(1.0); - } + // Handle active dragging + let translation = + self.app_state.last_cursor_location.to_vec2() - dragging.offset; + Some(self.transform.with_translation(translation)) + }; - if let Some(offset_scale) = offset_scale { - let offset = dragging_offset * offset_scale; + if let Some(transform) = transform { self.save(); - - let mut new = self.transform.as_coeffs(); - new[4] += offset.x; - new[5] += offset.y; - self.transform = Affine::new(new); + self.transform = transform; self.paint_state.renderer_mut().transform(self.transform); self.set_z_index(1000); self.clear_clip(); + // Apply styles let style = view_state.borrow().combined_style.clone(); let mut view_style_props = view_state.borrow().view_style_props.clone(); + if let Some(dragging_style) = view_state.borrow().dragging_style.clone() { let style = style.apply(dragging_style); let mut _new_frame = false; @@ -1063,27 +1075,31 @@ impl<'a> PaintCx<'a> { &mut _new_frame, ); } + + // Paint with drag styling let layout_props = view_state.borrow().layout_props.clone(); // Important: If any method early exit points are added in this // code block, they MUST call CURRENT_DRAG_PAINTING_ID.take() before // returning. + CURRENT_DRAG_PAINTING_ID.set(Some(id)); - paint_bg(self, &view_style_props, size); + paint_bg(self, &view_style_props, size); view.borrow_mut().paint(self); paint_border(self, &layout_props, &view_style_props, size); paint_outline(self, &view_style_props, size); self.restore(); + CURRENT_DRAG_PAINTING_ID.take(); } } } + if drag_set_to_none { self.app_state.dragging = None; } - self.restore(); } diff --git a/src/easing.rs b/src/easing.rs index acc4643d..1e4a2d50 100644 --- a/src/easing.rs +++ b/src/easing.rs @@ -9,7 +9,7 @@ pub trait Easing: std::fmt::Debug { None } fn finished(&self, time: f64) -> bool { - time >= 1. || time <= 0. + !(0. ..1.).contains(&time) } } @@ -213,7 +213,7 @@ impl Spring { } } - const THRESHOLD: f64 = 0.005; + pub const THRESHOLD: f64 = 0.005; pub fn finished(&self, time: f64) -> bool { let position = self.eval(time); let velocity = self.velocity(time); diff --git a/src/window_handle.rs b/src/window_handle.rs index 8a6292ac..3bf3d428 100644 --- a/src/window_handle.rs +++ b/src/window_handle.rs @@ -207,7 +207,12 @@ impl WindowHandle { app_state: &mut self.app_state, }; - let is_pointer_move = matches!(&event, Event::PointerMove(_)); + let is_pointer_move = if let Event::PointerMove(pme) = &event { + cx.app_state.last_cursor_location = pme.pos; + true + } else { + false + }; let (was_hovered, was_dragging_over) = if is_pointer_move { cx.app_state.cursor = None; let was_hovered = std::mem::take(&mut cx.app_state.hovered);