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