Skip to main content

azul_layout/
timer.rs

1//! Timer callback information and utilities for azul-layout
2//!
3//! This module provides Timer, TimerCallbackInfo and related types for
4//! managing timers that run on the main UI thread.
5
6use core::ffi::c_void;
7
8use azul_core::{
9    callbacks::{TimerCallbackReturn, Update},
10    dom::{DomId, OptionDomNodeId},
11    geom::{LogicalPosition, LogicalSize, OptionLogicalPosition},
12    id::NodeId,
13    menu::Menu,
14    refany::{OptionRefAny, RefAny},
15    resources::ImageRef,
16    task::{
17        Duration, GetSystemTimeCallback, Instant, OptionDuration, OptionInstant, TerminateTimer,
18        ThreadId, TimerId,
19    },
20    window::{KeyboardState, MouseState, WindowFlags},
21};
22
23use azul_css::AzString;
24
25use crate::{
26    callbacks::CallbackInfo,
27    thread::Thread,
28    window_state::{FullWindowState, WindowCreateOptions},
29};
30
31/// Default timer tick interval in milliseconds when no interval is configured.
32const DEFAULT_TIMER_TICK_MS: u64 = 10;
33
34/// Callback type for timers
35pub type TimerCallbackType = extern "C" fn(
36    /* timer internal refany */ RefAny,
37    TimerCallbackInfo,
38) -> TimerCallbackReturn;
39
40/// Callback that runs on every frame on the main thread
41#[repr(C)]
42pub struct TimerCallback {
43    pub cb: TimerCallbackType,
44    /// For FFI: stores the foreign callable (e.g., `PyFunction`)
45    /// Native Rust code sets this to None
46    pub ctx: OptionRefAny,
47}
48
49impl TimerCallback {
50    pub fn create(cb: TimerCallbackType) -> Self {
51        Self {
52            cb,
53            ctx: OptionRefAny::None,
54        }
55    }
56}
57
58impl core::fmt::Debug for TimerCallback {
59    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
60        write!(f, "TimerCallback {{ cb: {:p} }}", self.cb as *const ())
61    }
62}
63
64impl Clone for TimerCallback {
65    fn clone(&self) -> Self {
66        Self {
67            cb: self.cb,
68            ctx: self.ctx.clone(),
69        }
70    }
71}
72
73impl From<TimerCallbackType> for TimerCallback {
74    fn from(cb: TimerCallbackType) -> Self {
75        Self {
76            cb,
77            ctx: OptionRefAny::None,
78        }
79    }
80}
81
82impl PartialEq for TimerCallback {
83    fn eq(&self, other: &Self) -> bool {
84        std::ptr::eq(self.cb as *const (), other.cb as *const ())
85    }
86}
87
88impl Eq for TimerCallback {}
89
90impl PartialOrd for TimerCallback {
91    fn partial_cmp(&self, other: &Self) -> Option<core::cmp::Ordering> {
92        (self.cb as *const () as usize).partial_cmp(&(other.cb as *const () as usize))
93    }
94}
95
96impl Ord for TimerCallback {
97    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
98        (self.cb as *const () as usize).cmp(&(other.cb as *const () as usize))
99    }
100}
101
102impl core::hash::Hash for TimerCallback {
103    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
104        (self.cb as *const () as usize).hash(state);
105    }
106}
107
108/// A `Timer` is a function that runs on every frame or at intervals.
109#[derive(Debug, Clone, PartialEq, Eq, Hash)]
110#[repr(C)]
111pub struct Timer {
112    pub refany: RefAny,
113    pub node_id: OptionDomNodeId,
114    pub created: Instant,
115    pub last_run: OptionInstant,
116    pub run_count: usize,
117    pub delay: OptionDuration,
118    pub interval: OptionDuration,
119    pub timeout: OptionDuration,
120    pub callback: TimerCallback,
121}
122
123impl Timer {
124    pub fn create<C: Into<TimerCallback>>(
125        refany: RefAny,
126        callback: C,
127        get_system_time_fn: GetSystemTimeCallback,
128    ) -> Self {
129        Self {
130            refany,
131            node_id: None.into(),
132            created: (get_system_time_fn.cb)(),
133            run_count: 0,
134            last_run: OptionInstant::None,
135            delay: OptionDuration::None,
136            interval: OptionDuration::None,
137            timeout: OptionDuration::None,
138            callback: callback.into(),
139        }
140    }
141
142    #[must_use] pub const fn tick_millis(&self) -> u64 {
143        match self.interval.as_ref() {
144            Some(Duration::System(s)) => s.millis(),
145            Some(Duration::Tick(s)) => s.tick_diff,
146            None => DEFAULT_TIMER_TICK_MS,
147        }
148    }
149
150    #[must_use] pub fn is_about_to_finish(&self, instant_now: &Instant) -> bool {
151        match self.timeout {
152            OptionDuration::Some(timeout) => {
153                instant_now.duration_since(&self.created).greater_than(&timeout)
154            }
155            OptionDuration::None => false,
156        }
157    }
158
159    #[must_use] pub fn instant_of_next_run(&self) -> Instant {
160        let last_run = self.last_run.as_ref().map_or(&self.created, |s| s);
161
162        last_run
163            .clone()
164            .add_optional_duration(self.delay.as_ref())
165            .add_optional_duration(self.interval.as_ref())
166    }
167
168    #[inline]
169    #[must_use] pub const fn with_delay(mut self, delay: Duration) -> Self {
170        self.delay = OptionDuration::Some(delay);
171        self
172    }
173
174    #[inline]
175    #[must_use] pub const fn with_interval(mut self, interval: Duration) -> Self {
176        self.interval = OptionDuration::Some(interval);
177        self
178    }
179
180    #[inline]
181    #[must_use] pub const fn with_timeout(mut self, timeout: Duration) -> Self {
182        self.timeout = OptionDuration::Some(timeout);
183        self
184    }
185
186    /// Invoke the timer callback and update internal state.
187    ///
188    /// Returns a `TimerCallbackReturn` with `DoNothing` + `Continue` if the timer
189    /// is not ready to run yet (delay not elapsed for first run, or interval not
190    /// elapsed for subsequent runs). Forces `Terminate` when the timeout expires.
191    pub fn invoke(
192        &mut self,
193        callback_info: &CallbackInfo,
194        get_system_time_fn: &GetSystemTimeCallback,
195    ) -> TimerCallbackReturn {
196        let now = (get_system_time_fn.cb)();
197
198        // Check if timer should run based on last_run, delay, and interval
199        match self.last_run.as_ref() {
200            Some(last_run) => {
201                // Timer has run before - check interval
202                if let OptionDuration::Some(interval) = self.interval {
203                    if now.duration_since(last_run).smaller_than(&interval) {
204                        return TimerCallbackReturn {
205                            should_update: Update::DoNothing,
206                            should_terminate: TerminateTimer::Continue,
207                        };
208                    }
209                }
210            }
211            None => {
212                // Timer has never run - check delay (first run)
213                if let OptionDuration::Some(delay) = self.delay {
214                    if now.duration_since(&self.created).smaller_than(&delay) {
215                        return TimerCallbackReturn {
216                            should_update: Update::DoNothing,
217                            should_terminate: TerminateTimer::Continue,
218                        };
219                    }
220                }
221            }
222        }
223
224        let is_about_to_finish = self.is_about_to_finish(&now);
225
226        // Create a new TimerCallbackInfo wrapping the callback_info
227        // CallbackInfo is Copy, so we can just copy it directly
228        let mut timer_callback_info = TimerCallbackInfo {
229            callback_info: *callback_info,
230            node_id: self.node_id,
231            frame_start: now.clone(),
232            call_count: self.run_count,
233            is_about_to_finish,
234            _abi_ref: core::ptr::null(),
235            _abi_mut: core::ptr::null_mut(),
236        };
237
238        let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
239
240        if is_about_to_finish {
241            result.should_terminate = TerminateTimer::Terminate;
242        }
243
244        self.run_count += 1;
245        self.last_run = OptionInstant::Some(now);
246
247        result
248    }
249}
250
251impl Default for Timer {
252    fn default() -> Self {
253        extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
254            TimerCallbackReturn::terminate_unchanged()
255        }
256
257        const extern "C" fn default_time() -> Instant {
258            Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
259        }
260
261        let cb: TimerCallbackType = default_callback;
262        Self::create(
263            RefAny::new(()),
264            cb,
265            GetSystemTimeCallback { cb: default_time },
266        )
267    }
268}
269
270/// Information passed to timer callbacks.
271///
272/// This wraps `CallbackInfo` and adds timer-specific fields like `call_count` and `frame_start`.
273/// `CallbackInfo` methods are available via explicit delegation methods below.
274#[derive(Debug, Clone)]
275#[repr(C)]
276#[allow(clippy::pub_underscore_fields)] // _abi_ref/_abi_mut: intentional FFI/api.json ABI-stability placeholder fields
277pub struct TimerCallbackInfo {
278    pub callback_info: CallbackInfo,
279    pub node_id: OptionDomNodeId,
280    pub frame_start: Instant,
281    pub call_count: usize,
282    pub is_about_to_finish: bool,
283    pub _abi_ref: *const c_void,
284    pub _abi_mut: *mut c_void,
285}
286
287impl TimerCallbackInfo {
288    #[must_use] pub const fn create(
289        callback_info: CallbackInfo,
290        node_id: OptionDomNodeId,
291        frame_start: Instant,
292        call_count: usize,
293        is_about_to_finish: bool,
294    ) -> Self {
295        Self {
296            callback_info,
297            node_id,
298            frame_start,
299            call_count,
300            is_about_to_finish,
301            _abi_ref: core::ptr::null(),
302            _abi_mut: core::ptr::null_mut(),
303        }
304    }
305
306    #[must_use] pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
307        let node_id = self.node_id.into_option()?;
308        self.callback_info.get_node_size(node_id)
309    }
310
311    #[must_use] pub fn get_attached_node_position(&self) -> Option<LogicalPosition> {
312        let node_id = self.node_id.into_option()?;
313        self.callback_info.get_node_position(node_id)
314    }
315
316    #[must_use] pub const fn get_callback_info(&self) -> &CallbackInfo {
317        &self.callback_info
318    }
319
320    pub const fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
321        &mut self.callback_info
322    }
323
324    // ==================== Delegated CallbackInfo methods ====================
325    // These methods delegate to the inner callback_info to provide the same API
326    // as CallbackInfo without using Deref (which causes issues with FFI codegen)
327
328    /// Get the callable for FFI language bindings (Python, etc.)
329    #[must_use] pub fn get_ctx(&self) -> OptionRefAny {
330        self.callback_info.get_ctx()
331    }
332
333    /// Add a timer to this window (applied after callback returns)
334    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
335        self.callback_info.add_timer(timer_id, timer);
336    }
337
338    /// Remove a timer from this window (applied after callback returns)
339    pub fn remove_timer(&mut self, timer_id: TimerId) {
340        self.callback_info.remove_timer(timer_id);
341    }
342
343    /// Add a thread to this window (applied after callback returns)
344    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
345        self.callback_info.add_thread(thread_id, thread);
346    }
347
348    /// Remove a thread from this window (applied after callback returns)
349    pub fn remove_thread(&mut self, thread_id: ThreadId) {
350        self.callback_info.remove_thread(thread_id);
351    }
352
353    /// Stop event propagation (applied after callback returns)
354    pub fn stop_propagation(&mut self) {
355        self.callback_info.stop_propagation();
356    }
357
358    /// Create a new window (applied after callback returns)
359    pub fn create_window(&mut self, options: WindowCreateOptions) {
360        self.callback_info.create_window(options);
361    }
362
363    /// Close the current window (applied after callback returns)
364    pub fn close_window(&mut self) {
365        self.callback_info.close_window();
366    }
367
368    /// Modify the window state (applied after callback returns)
369    pub fn modify_window_state(&mut self, state: FullWindowState) {
370        self.callback_info.modify_window_state(state);
371    }
372
373    /// Add an image to the image cache (applied after callback returns)
374    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
375        self.callback_info.add_image_to_cache(id, image);
376    }
377
378    /// Remove an image from the image cache (applied after callback returns)
379    pub fn remove_image_from_cache(&mut self, id: AzString) {
380        self.callback_info.remove_image_from_cache(id);
381    }
382
383    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
384    ///
385    /// This is the most efficient way to update animated GL textures from a timer.
386    /// Triggers only texture re-rendering - no DOM rebuild or display list resubmission.
387    pub fn update_all_image_callbacks(&mut self) {
388        self.callback_info.update_all_image_callbacks();
389    }
390
391    /// Trigger re-rendering of a `VirtualView` (applied after callback returns)
392    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
393        self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
394    }
395
396    /// Reload system fonts (applied after callback returns)
397    pub fn reload_system_fonts(&mut self) {
398        self.callback_info.reload_system_fonts();
399    }
400
401    /// Prevent the default action
402    pub fn prevent_default(&mut self) {
403        self.callback_info.prevent_default();
404    }
405
406    /// Open a menu
407    pub fn open_menu(&mut self, menu: Menu) {
408        self.callback_info.open_menu(menu);
409    }
410
411    /// Open a menu at a specific position
412    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
413        self.callback_info.open_menu_at(menu, position);
414    }
415
416    /// Show a tooltip at the current cursor position
417    pub fn show_tooltip(&mut self, text: AzString) {
418        self.callback_info.show_tooltip(text);
419    }
420
421    /// Show a tooltip at a specific position
422    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
423        self.callback_info.show_tooltip_at(text, position);
424    }
425
426    /// Hide the currently displayed tooltip
427    pub fn hide_tooltip(&mut self) {
428        self.callback_info.hide_tooltip();
429    }
430
431    /// Open a menu positioned relative to the currently hit node
432    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
433        self.callback_info.open_menu_for_hit_node(menu)
434    }
435
436    /// Get current window flags
437    #[must_use] pub const fn get_current_window_flags(&self) -> WindowFlags {
438        self.callback_info.get_current_window_flags()
439    }
440
441    /// Get current keyboard state
442    #[must_use] pub fn get_current_keyboard_state(&self) -> KeyboardState {
443        self.callback_info.get_current_keyboard_state()
444    }
445
446    /// Get current mouse state
447    #[must_use] pub const fn get_current_mouse_state(&self) -> MouseState {
448        self.callback_info.get_current_mouse_state()
449    }
450
451    /// Get the cursor position relative to the hit node
452    #[must_use] pub const fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
453        self.callback_info.get_cursor_relative_to_node()
454    }
455
456    /// Get the cursor position relative to the viewport
457    #[must_use] pub const fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
458        self.callback_info.get_cursor_relative_to_viewport()
459    }
460
461    /// Get the current cursor position
462    #[must_use] pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
463        self.callback_info.get_cursor_position()
464    }
465
466    /// Get the current time (when the timer callback started)
467    #[must_use] pub fn get_current_time(&self) -> Instant {
468        self.frame_start.clone()
469    }
470
471    /// Check if any node in a specific DOM is focused
472    #[must_use] pub fn is_dom_focused(&self, dom_id: DomId) -> bool {
473        self.callback_info.is_dom_focused(dom_id)
474    }
475
476    /// Check if pen is in contact
477    #[must_use] pub fn is_pen_in_contact(&self) -> bool {
478        self.callback_info.is_pen_in_contact()
479    }
480
481    /// Check if pen eraser is active
482    #[must_use] pub fn is_pen_eraser(&self) -> bool {
483        self.callback_info.is_pen_eraser()
484    }
485
486    /// Check if pen barrel button is pressed
487    #[must_use] pub fn is_pen_barrel_button_pressed(&self) -> bool {
488        self.callback_info.is_pen_barrel_button_pressed()
489    }
490
491    /// Check if dragging is active
492    #[must_use] pub const fn is_dragging(&self) -> bool {
493        self.callback_info.get_current_mouse_state().left_down
494    }
495
496    /// Check if drag is active
497    #[must_use] pub const fn is_drag_active(&self) -> bool {
498        self.callback_info.get_current_mouse_state().left_down
499    }
500
501    /// Check if node drag is active
502    #[must_use] pub const fn is_node_drag_active(&self) -> bool {
503        self.callback_info.get_current_mouse_state().left_down
504    }
505
506    /// Check if file drag is active
507    #[must_use] pub fn is_file_drag_active(&self) -> bool {
508        self.callback_info.is_file_drag_active()
509    }
510
511    /// Check if there's sufficient history for gestures
512    #[must_use] pub fn has_sufficient_history_for_gestures(&self) -> bool {
513        self.callback_info.has_sufficient_history_for_gestures()
514    }
515
516    // ==================== Scroll Management (timer architecture) ====================
517
518    /// Get a read-only snapshot of a scroll node's bounds and position.
519    ///
520    /// Timer callbacks use this to read current scroll state for physics calculation.
521    #[must_use] pub fn get_scroll_node_info(
522        &self,
523        dom_id: DomId,
524        node_id: NodeId,
525    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
526        self.callback_info.get_scroll_node_info(dom_id, node_id)
527    }
528
529    /// Find the closest scrollable ancestor of a node.
530    ///
531    /// Used by auto-scroll timer to find which container to scroll when
532    /// the user drags beyond the container edge.
533    #[must_use] pub fn find_scroll_parent(
534        &self,
535        dom_id: DomId,
536        node_id: NodeId,
537    ) -> Option<NodeId> {
538        self.callback_info.find_scroll_parent(dom_id, node_id)
539    }
540
541    /// Get the scroll input queue for consuming pending scroll inputs.
542    ///
543    /// The physics timer calls `take_all()` each tick to drain inputs
544    /// recorded by platform event handlers.
545    #[cfg(feature = "std")]
546    #[must_use] pub fn get_scroll_input_queue(
547        &self,
548    ) -> crate::managers::scroll_state::ScrollInputQueue {
549        self.callback_info.get_scroll_input_queue()
550    }
551
552    /// Scroll a node to a specific position (via transactional `CallbackChange`).
553    ///
554    /// This is the primary way for timer callbacks to update scroll positions.
555    /// The change is applied after the callback returns.
556    pub fn scroll_to(
557        &mut self,
558        dom_id: DomId,
559        node_id: azul_core::styled_dom::NodeHierarchyItemId,
560        position: LogicalPosition,
561    ) {
562        self.callback_info.scroll_to(dom_id, node_id, position);
563    }
564
565    /// Scroll to position without clamping (for rubber-banding/overscroll).
566    pub fn scroll_to_unclamped(
567        &mut self,
568        dom_id: DomId,
569        node_id: azul_core::styled_dom::NodeHierarchyItemId,
570        position: LogicalPosition,
571    ) {
572        self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
573    }
574
575    // Cursor blink timer methods
576    
577    /// Set cursor visibility state (for cursor blink timer)
578    pub fn set_cursor_visibility(&mut self, visible: bool) {
579        self.callback_info.set_cursor_visibility(visible);
580    }
581    
582    /// Toggle cursor visibility (for cursor blink timer).
583    pub fn set_cursor_visibility_toggle(&mut self) {
584        use crate::callbacks::CallbackChange;
585        self.callback_info.push_change(CallbackChange::ToggleCursorVisibility);
586    }
587    
588    /// Reset cursor blink state on user input
589    pub fn reset_cursor_blink(&mut self) {
590        self.callback_info.reset_cursor_blink();
591    }
592}
593
594/// Optional Timer type for API compatibility
595#[derive(Debug, Clone)]
596#[repr(C, u8)]
597// FFI Option enum; boxing the Some variant would break the #[repr(C, u8)] C ABI / api.json.
598#[allow(variant_size_differences)] // repr(C,u8) FFI enum: boxing the large variant would change the C ABI (api.json bindings); size disparity accepted
599#[allow(clippy::large_enum_variant)]
600pub enum OptionTimer {
601    None,
602    Some(Timer),
603}
604
605impl From<Option<Timer>> for OptionTimer {
606    fn from(o: Option<Timer>) -> Self {
607        o.map_or_else(|| Self::None, Self::Some)
608    }
609}
610
611impl OptionTimer {
612    #[must_use] pub fn into_option(self) -> Option<Timer> {
613        match self {
614            Self::None => None,
615            Self::Some(t) => Some(t),
616        }
617    }
618}