From c8d5f6d81dd0e896159585e33c0224c3cbb868f9 Mon Sep 17 00:00:00 2001 From: Moritz Mechelk Date: Sat, 7 Dec 2024 20:39:36 +0100 Subject: [PATCH] feat: universal animation frames for pens (#1309) --- crates/rnote-engine/src/engine/mod.rs | 51 +++++++++++++++++++- crates/rnote-engine/src/pens/penbehaviour.rs | 5 ++ crates/rnote-engine/src/pens/penholder.rs | 12 ++++- crates/rnote-ui/src/canvas/mod.rs | 25 ++++++++++ 4 files changed, 91 insertions(+), 2 deletions(-) diff --git a/crates/rnote-engine/src/engine/mod.rs b/crates/rnote-engine/src/engine/mod.rs index 7a8d6e6a92..cbcd141ea1 100644 --- a/crates/rnote-engine/src/engine/mod.rs +++ b/crates/rnote-engine/src/engine/mod.rs @@ -34,7 +34,7 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use std::sync::Arc; use std::time::Instant; -use tracing::error; +use tracing::{debug, error}; /// An immutable view into the engine, excluding the penholder. #[derive(Debug)] @@ -45,6 +45,7 @@ pub struct EngineView<'a> { pub store: &'a StrokeStore, pub camera: &'a Camera, pub audioplayer: &'a Option, + pub animation: &'a Animation, } /// Constructs an `EngineView` from an identifier containing an `Engine` instance. @@ -58,6 +59,7 @@ macro_rules! engine_view { store: &$engine.store, camera: &$engine.camera, audioplayer: &$engine.audioplayer, + animation: &$engine.animation, } }; } @@ -71,6 +73,7 @@ pub struct EngineViewMut<'a> { pub store: &'a mut StrokeStore, pub camera: &'a mut Camera, pub audioplayer: &'a mut Option, + pub animation: &'a mut Animation, } /// Constructs an `EngineViewMut` from an identifier containing an `Engine` instance. @@ -84,6 +87,7 @@ macro_rules! engine_view_mut { store: &mut $engine.store, camera: &mut $engine.camera, audioplayer: &mut $engine.audioplayer, + animation: &mut $engine.animation, } }; } @@ -98,6 +102,7 @@ impl EngineViewMut<'_> { store: self.store, camera: self.camera, audioplayer: self.audioplayer, + animation: self.animation, } } } @@ -179,6 +184,39 @@ impl EngineTaskReceiver { } } +#[derive(Debug, Clone, Default)] +pub struct Animation { + frame_in_flight: bool, +} + +impl Animation { + /// Claim an animation frame. + /// + /// Returns whether an animation frame was already claimed. + pub fn claim_frame(&mut self) -> bool { + if self.frame_in_flight { + debug!("Animation frame already in flight, skipping"); + true + } else { + self.frame_in_flight = true; + false + } + } + + pub fn frame_in_flight(&self) -> bool { + self.frame_in_flight + } + + pub fn process_frame(&mut self) -> bool { + if self.frame_in_flight { + self.frame_in_flight = false; + true + } else { + false + } + } +} + /// The engine. #[derive(Debug, Serialize, Deserialize)] #[serde(default, rename = "engine")] @@ -205,6 +243,8 @@ pub struct Engine { #[serde(skip)] audioplayer: Option, #[serde(skip)] + pub animation: Animation, + #[serde(skip)] visual_debug: bool, // the task sender. Must not be modified, only cloned. #[serde(skip)] @@ -241,6 +281,7 @@ impl Default for Engine { optimize_epd: false, audioplayer: None, + animation: Animation::default(), visual_debug: false, tasks_tx: EngineTaskSender(tasks_tx), tasks_rx: Some(EngineTaskReceiver(tasks_rx)), @@ -885,4 +926,12 @@ impl Engine { } widget_flags } + + /// Handle a requested animation frame. + /// + /// Can request another frame using `EngineViewMut#animation.claim_frame()`. + pub fn handle_animation_frame(&mut self, optimize_epd: bool) { + self.penholder + .handle_animation_frame(&mut engine_view_mut!(self), optimize_epd); + } } diff --git a/crates/rnote-engine/src/pens/penbehaviour.rs b/crates/rnote-engine/src/pens/penbehaviour.rs index b76a954e0a..6568c6fb1f 100644 --- a/crates/rnote-engine/src/pens/penbehaviour.rs +++ b/crates/rnote-engine/src/pens/penbehaviour.rs @@ -32,6 +32,11 @@ pub trait PenBehaviour: DrawableOnDoc { engine_view: &mut EngineViewMut, ) -> (EventResult, WidgetFlags); + /// Handle a requested animation frame. + /// + /// Can request another frame using `EngineViewMut#animation.claim_frame()`. + fn handle_animation_frame(&mut self, _engine_view: &mut EngineViewMut, _optimize_epd: bool) {} + /// Fetch clipboard content from the pen. /// /// The fetched content can be available in multiple formats, diff --git a/crates/rnote-engine/src/pens/penholder.rs b/crates/rnote-engine/src/pens/penholder.rs index 66952fbe25..3a2f9d4523 100644 --- a/crates/rnote-engine/src/pens/penholder.rs +++ b/crates/rnote-engine/src/pens/penholder.rs @@ -253,12 +253,22 @@ impl PenHolder { widget_flags |= wf; } - // Always redraw after handling a pen event + // Always redraw after handling a pen event. + // + // This is also needed because pens might have claimed/requested an animation frame. widget_flags.redraw = true; (event_result.propagate, widget_flags) } + /// Handle a requested animation frame. + /// + /// Can request another frame using `EngineViewMut#animation.claim_frame()`. + pub fn handle_animation_frame(&mut self, engine_view: &mut EngineViewMut, optimize_epd: bool) { + self.current_pen + .handle_animation_frame(engine_view, optimize_epd); + } + /// Handle a pressed shortcut key. pub fn handle_pressed_shortcut_key( &mut self, diff --git a/crates/rnote-ui/src/canvas/mod.rs b/crates/rnote-ui/src/canvas/mod.rs index 11bee39b2b..ca064fa3be 100644 --- a/crates/rnote-ui/src/canvas/mod.rs +++ b/crates/rnote-ui/src/canvas/mod.rs @@ -75,6 +75,7 @@ mod imp { pub(crate) engine: RefCell, pub(crate) engine_task_handler_handle: RefCell>>, + pub(crate) animation_callback_id: RefCell>, pub(crate) output_file: RefCell>, pub(crate) output_file_watcher_task: RefCell>>, @@ -168,6 +169,7 @@ mod imp { engine: RefCell::new(engine), engine_task_handler_handle: RefCell::new(None), + animation_callback_id: RefCell::new(None), output_file: RefCell::new(None), output_file_watcher_task: RefCell::new(None), @@ -245,6 +247,29 @@ mod imp { *self.engine_task_handler_handle.borrow_mut() = Some(engine_task_handler_handle); + let animation_callback_id = obj.add_tick_callback(clone!( + #[weak(rename_to=canvas)] + obj, + #[upgrade_or] + glib::ControlFlow::Break, + move |_widget, _frame_clock| { + if canvas.engine_mut().animation.process_frame() { + let optimize_epd = canvas.engine_ref().optimize_epd(); + canvas.engine_mut().handle_animation_frame(optimize_epd); + + // if optimize_epd is enabled, we only redraw the canvas + // when no follow-up frame has been requested (i.e. the animation is done) + if !optimize_epd || !canvas.engine_ref().animation.frame_in_flight() { + canvas.queue_draw(); + } + } + + glib::ControlFlow::Continue + } + )); + + *self.animation_callback_id.borrow_mut() = Some(animation_callback_id); + self.setup_input(); }