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        self.cb as *const () as usize == other.cb as *const () as usize
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        Timer {
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    pub 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    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    pub fn instant_of_next_run(&self) -> Instant {
160        let last_run = match self.last_run.as_ref() {
161            Some(s) => s,
162            None => &self.created,
163        };
164
165        last_run
166            .clone()
167            .add_optional_duration(self.delay.as_ref())
168            .add_optional_duration(self.interval.as_ref())
169    }
170
171    #[inline]
172    pub fn with_delay(mut self, delay: Duration) -> Self {
173        self.delay = OptionDuration::Some(delay);
174        self
175    }
176
177    #[inline]
178    pub fn with_interval(mut self, interval: Duration) -> Self {
179        self.interval = OptionDuration::Some(interval);
180        self
181    }
182
183    #[inline]
184    pub fn with_timeout(mut self, timeout: Duration) -> Self {
185        self.timeout = OptionDuration::Some(timeout);
186        self
187    }
188
189    /// Invoke the timer callback and update internal state.
190    ///
191    /// Returns a `TimerCallbackReturn` with `DoNothing` + `Continue` if the timer
192    /// is not ready to run yet (delay not elapsed for first run, or interval not
193    /// elapsed for subsequent runs). Forces `Terminate` when the timeout expires.
194    pub fn invoke(
195        &mut self,
196        callback_info: &CallbackInfo,
197        get_system_time_fn: &GetSystemTimeCallback,
198    ) -> TimerCallbackReturn {
199        let now = (get_system_time_fn.cb)();
200
201        // Check if timer should run based on last_run, delay, and interval
202        match self.last_run.as_ref() {
203            Some(last_run) => {
204                // Timer has run before - check interval
205                if let OptionDuration::Some(interval) = self.interval {
206                    if now.duration_since(last_run).smaller_than(&interval) {
207                        return TimerCallbackReturn {
208                            should_update: Update::DoNothing,
209                            should_terminate: TerminateTimer::Continue,
210                        };
211                    }
212                }
213            }
214            None => {
215                // Timer has never run - check delay (first run)
216                if let OptionDuration::Some(delay) = self.delay {
217                    if now.duration_since(&self.created).smaller_than(&delay) {
218                        return TimerCallbackReturn {
219                            should_update: Update::DoNothing,
220                            should_terminate: TerminateTimer::Continue,
221                        };
222                    }
223                }
224            }
225        }
226
227        let is_about_to_finish = self.is_about_to_finish(&now);
228
229        // Create a new TimerCallbackInfo wrapping the callback_info
230        // CallbackInfo is Copy, so we can just copy it directly
231        let mut timer_callback_info = TimerCallbackInfo {
232            callback_info: *callback_info,
233            node_id: self.node_id,
234            frame_start: now.clone(),
235            call_count: self.run_count,
236            is_about_to_finish,
237            _abi_ref: core::ptr::null(),
238            _abi_mut: core::ptr::null_mut(),
239        };
240
241        let mut result = (self.callback.cb)(self.refany.clone(), timer_callback_info);
242
243        if is_about_to_finish {
244            result.should_terminate = TerminateTimer::Terminate;
245        }
246
247        self.run_count += 1;
248        self.last_run = OptionInstant::Some(now);
249
250        result
251    }
252}
253
254impl Default for Timer {
255    fn default() -> Self {
256        extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
257            TimerCallbackReturn::terminate_unchanged()
258        }
259
260        extern "C" fn default_time() -> Instant {
261            Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
262        }
263
264        Timer::create(
265            RefAny::new(()),
266            default_callback as TimerCallbackType,
267            GetSystemTimeCallback { cb: default_time },
268        )
269    }
270}
271
272/// Information passed to timer callbacks.
273///
274/// This wraps `CallbackInfo` and adds timer-specific fields like `call_count` and `frame_start`.
275/// `CallbackInfo` methods are available via explicit delegation methods below.
276#[derive(Clone)]
277#[repr(C)]
278pub struct TimerCallbackInfo {
279    pub callback_info: CallbackInfo,
280    pub node_id: OptionDomNodeId,
281    pub frame_start: Instant,
282    pub call_count: usize,
283    pub is_about_to_finish: bool,
284    pub _abi_ref: *const c_void,
285    pub _abi_mut: *mut c_void,
286}
287
288impl TimerCallbackInfo {
289    pub fn create(
290        callback_info: CallbackInfo,
291        node_id: OptionDomNodeId,
292        frame_start: Instant,
293        call_count: usize,
294        is_about_to_finish: bool,
295    ) -> Self {
296        Self {
297            callback_info,
298            node_id,
299            frame_start,
300            call_count,
301            is_about_to_finish,
302            _abi_ref: core::ptr::null(),
303            _abi_mut: core::ptr::null_mut(),
304        }
305    }
306
307    pub fn get_attached_node_size(&self) -> Option<LogicalSize> {
308        let node_id = self.node_id.into_option()?;
309        self.callback_info.get_node_size(node_id)
310    }
311
312    pub fn get_attached_node_position(&self) -> Option<azul_core::geom::LogicalPosition> {
313        let node_id = self.node_id.into_option()?;
314        self.callback_info.get_node_position(node_id)
315    }
316
317    pub fn get_callback_info(&self) -> &CallbackInfo {
318        &self.callback_info
319    }
320
321    pub fn get_callback_info_mut(&mut self) -> &mut CallbackInfo {
322        &mut self.callback_info
323    }
324
325    // ==================== Delegated CallbackInfo methods ====================
326    // These methods delegate to the inner callback_info to provide the same API
327    // as CallbackInfo without using Deref (which causes issues with FFI codegen)
328
329    /// Get the callable for FFI language bindings (Python, etc.)
330    pub fn get_ctx(&self) -> OptionRefAny {
331        self.callback_info.get_ctx()
332    }
333
334    /// Add a timer to this window (applied after callback returns)
335    pub fn add_timer(&mut self, timer_id: TimerId, timer: Timer) {
336        self.callback_info.add_timer(timer_id, timer);
337    }
338
339    /// Remove a timer from this window (applied after callback returns)
340    pub fn remove_timer(&mut self, timer_id: TimerId) {
341        self.callback_info.remove_timer(timer_id);
342    }
343
344    /// Add a thread to this window (applied after callback returns)
345    pub fn add_thread(&mut self, thread_id: ThreadId, thread: Thread) {
346        self.callback_info.add_thread(thread_id, thread);
347    }
348
349    /// Remove a thread from this window (applied after callback returns)
350    pub fn remove_thread(&mut self, thread_id: ThreadId) {
351        self.callback_info.remove_thread(thread_id);
352    }
353
354    /// Stop event propagation (applied after callback returns)
355    pub fn stop_propagation(&mut self) {
356        self.callback_info.stop_propagation();
357    }
358
359    /// Create a new window (applied after callback returns)
360    pub fn create_window(&mut self, options: WindowCreateOptions) {
361        self.callback_info.create_window(options);
362    }
363
364    /// Close the current window (applied after callback returns)
365    pub fn close_window(&mut self) {
366        self.callback_info.close_window();
367    }
368
369    /// Modify the window state (applied after callback returns)
370    pub fn modify_window_state(&mut self, state: FullWindowState) {
371        self.callback_info.modify_window_state(state);
372    }
373
374    /// Add an image to the image cache (applied after callback returns)
375    pub fn add_image_to_cache(&mut self, id: AzString, image: ImageRef) {
376        self.callback_info.add_image_to_cache(id, image);
377    }
378
379    /// Remove an image from the image cache (applied after callback returns)
380    pub fn remove_image_from_cache(&mut self, id: AzString) {
381        self.callback_info.remove_image_from_cache(id);
382    }
383
384    /// Re-render ALL image callbacks across all DOMs (applied after callback returns)
385    ///
386    /// This is the most efficient way to update animated GL textures from a timer.
387    /// Triggers only texture re-rendering - no DOM rebuild or display list resubmission.
388    pub fn update_all_image_callbacks(&mut self) {
389        self.callback_info.update_all_image_callbacks();
390    }
391
392    /// Trigger re-rendering of a VirtualView (applied after callback returns)
393    pub fn trigger_virtual_view_rerender(&mut self, dom_id: DomId, node_id: NodeId) {
394        self.callback_info.trigger_virtual_view_rerender(dom_id, node_id);
395    }
396
397    /// Reload system fonts (applied after callback returns)
398    pub fn reload_system_fonts(&mut self) {
399        self.callback_info.reload_system_fonts();
400    }
401
402    /// Prevent the default action
403    pub fn prevent_default(&mut self) {
404        self.callback_info.prevent_default();
405    }
406
407    /// Open a menu
408    pub fn open_menu(&mut self, menu: Menu) {
409        self.callback_info.open_menu(menu);
410    }
411
412    /// Open a menu at a specific position
413    pub fn open_menu_at(&mut self, menu: Menu, position: LogicalPosition) {
414        self.callback_info.open_menu_at(menu, position);
415    }
416
417    /// Show a tooltip at the current cursor position
418    pub fn show_tooltip(&mut self, text: AzString) {
419        self.callback_info.show_tooltip(text);
420    }
421
422    /// Show a tooltip at a specific position
423    pub fn show_tooltip_at(&mut self, text: AzString, position: LogicalPosition) {
424        self.callback_info.show_tooltip_at(text, position);
425    }
426
427    /// Hide the currently displayed tooltip
428    pub fn hide_tooltip(&mut self) {
429        self.callback_info.hide_tooltip();
430    }
431
432    /// Open a menu positioned relative to the currently hit node
433    pub fn open_menu_for_hit_node(&mut self, menu: Menu) -> bool {
434        self.callback_info.open_menu_for_hit_node(menu)
435    }
436
437    /// Get current window flags
438    pub fn get_current_window_flags(&self) -> WindowFlags {
439        self.callback_info.get_current_window_flags()
440    }
441
442    /// Get current keyboard state
443    pub fn get_current_keyboard_state(&self) -> KeyboardState {
444        self.callback_info.get_current_keyboard_state()
445    }
446
447    /// Get current mouse state
448    pub fn get_current_mouse_state(&self) -> MouseState {
449        self.callback_info.get_current_mouse_state()
450    }
451
452    /// Get the cursor position relative to the hit node
453    pub fn get_cursor_relative_to_node(&self) -> azul_core::geom::OptionCursorNodePosition {
454        self.callback_info.get_cursor_relative_to_node()
455    }
456
457    /// Get the cursor position relative to the viewport
458    pub fn get_cursor_relative_to_viewport(&self) -> OptionLogicalPosition {
459        self.callback_info.get_cursor_relative_to_viewport()
460    }
461
462    /// Get the current cursor position
463    pub fn get_cursor_position(&self) -> Option<LogicalPosition> {
464        self.callback_info.get_cursor_position()
465    }
466
467    /// Get the current time (when the timer callback started)
468    pub fn get_current_time(&self) -> Instant {
469        self.frame_start.clone()
470    }
471
472    /// Check if the DOM is focused
473    pub fn is_dom_focused(&self) -> bool {
474        // TimerCallbackInfo doesn't have direct focus info
475        true // Timers run regardless of focus
476    }
477
478    /// Check if pen is in contact
479    pub fn is_pen_in_contact(&self) -> bool {
480        false // Not available in timer context
481    }
482
483    /// Check if pen eraser is active
484    pub fn is_pen_eraser(&self) -> bool {
485        false // Not available in timer context
486    }
487
488    /// Check if pen barrel button is pressed
489    pub fn is_pen_barrel_button_pressed(&self) -> bool {
490        false // Not available in timer context
491    }
492
493    /// Check if dragging is active
494    pub fn is_dragging(&self) -> bool {
495        self.callback_info.get_current_mouse_state().left_down
496    }
497
498    /// Check if drag is active
499    pub fn is_drag_active(&self) -> bool {
500        self.callback_info.get_current_mouse_state().left_down
501    }
502
503    /// Check if node drag is active
504    pub fn is_node_drag_active(&self) -> bool {
505        self.callback_info.get_current_mouse_state().left_down
506    }
507
508    /// Check if file drag is active
509    pub fn is_file_drag_active(&self) -> bool {
510        false // Timers don't track file drags
511    }
512
513    /// Check if there's sufficient history for gestures
514    pub fn has_sufficient_history_for_gestures(&self) -> bool {
515        false // Timers don't track gesture history
516    }
517
518    // ==================== Scroll Management (timer architecture) ====================
519
520    /// Get a read-only snapshot of a scroll node's bounds and position.
521    ///
522    /// Timer callbacks use this to read current scroll state for physics calculation.
523    pub fn get_scroll_node_info(
524        &self,
525        dom_id: azul_core::dom::DomId,
526        node_id: azul_core::id::NodeId,
527    ) -> Option<crate::managers::scroll_state::ScrollNodeInfo> {
528        self.callback_info.get_scroll_node_info(dom_id, node_id)
529    }
530
531    /// Find the closest scrollable ancestor of a node.
532    ///
533    /// Used by auto-scroll timer to find which container to scroll when
534    /// the user drags beyond the container edge.
535    pub fn find_scroll_parent(
536        &self,
537        dom_id: azul_core::dom::DomId,
538        node_id: azul_core::id::NodeId,
539    ) -> Option<azul_core::id::NodeId> {
540        self.callback_info.find_scroll_parent(dom_id, node_id)
541    }
542
543    /// Get the scroll input queue for consuming pending scroll inputs.
544    ///
545    /// The physics timer calls `take_all()` each tick to drain inputs
546    /// recorded by platform event handlers.
547    #[cfg(feature = "std")]
548    pub fn get_scroll_input_queue(
549        &self,
550    ) -> crate::managers::scroll_state::ScrollInputQueue {
551        self.callback_info.get_scroll_input_queue()
552    }
553
554    /// Scroll a node to a specific position (via transactional CallbackChange).
555    ///
556    /// This is the primary way for timer callbacks to update scroll positions.
557    /// The change is applied after the callback returns.
558    pub fn scroll_to(
559        &mut self,
560        dom_id: azul_core::dom::DomId,
561        node_id: azul_core::styled_dom::NodeHierarchyItemId,
562        position: azul_core::geom::LogicalPosition,
563    ) {
564        self.callback_info.scroll_to(dom_id, node_id, position);
565    }
566
567    /// Scroll to position without clamping (for rubber-banding/overscroll).
568    pub fn scroll_to_unclamped(
569        &mut self,
570        dom_id: azul_core::dom::DomId,
571        node_id: azul_core::styled_dom::NodeHierarchyItemId,
572        position: azul_core::geom::LogicalPosition,
573    ) {
574        self.callback_info.scroll_to_unclamped(dom_id, node_id, position);
575    }
576
577    // Cursor blink timer methods
578    
579    /// Set cursor visibility state (for cursor blink timer)
580    pub fn set_cursor_visibility(&mut self, visible: bool) {
581        self.callback_info.set_cursor_visibility(visible);
582    }
583    
584    /// Toggle cursor visibility (for cursor blink timer).
585    ///
586    /// NOTE: Currently always sets visibility to `true` — proper toggle logic
587    /// requires a `CallbackChange::ToggleCursorVisibility` variant or reading
588    /// current state, which is not yet implemented.
589    pub fn set_cursor_visibility_toggle(&mut self) {
590        use crate::callbacks::CallbackChange;
591        // TODO: implement actual toggle — needs CallbackChange::ToggleCursorVisibility
592        self.callback_info.push_change(CallbackChange::SetCursorVisibility { visible: true });
593    }
594    
595    /// Reset cursor blink state on user input
596    pub fn reset_cursor_blink(&mut self) {
597        self.callback_info.reset_cursor_blink();
598    }
599}
600
601/// Optional Timer type for API compatibility
602#[derive(Debug, Clone)]
603#[repr(C, u8)]
604pub enum OptionTimer {
605    None,
606    Some(Timer),
607}
608
609impl From<Option<Timer>> for OptionTimer {
610    fn from(o: Option<Timer>) -> Self {
611        match o {
612            None => OptionTimer::None,
613            Some(t) => OptionTimer::Some(t),
614        }
615    }
616}
617
618impl OptionTimer {
619    pub fn into_option(self) -> Option<Timer> {
620        match self {
621            OptionTimer::None => None,
622            OptionTimer::Some(t) => Some(t),
623        }
624    }
625}