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

            
6
use core::ffi::c_void;
7

            
8
use 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

            
23
use azul_css::AzString;
24

            
25
use 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.
32
const DEFAULT_TIMER_TICK_MS: u64 = 10;
33

            
34
/// Callback type for timers
35
pub 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)]
42
pub 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

            
49
impl TimerCallback {
50
    pub fn create(cb: TimerCallbackType) -> Self {
51
        Self {
52
            cb,
53
            ctx: OptionRefAny::None,
54
        }
55
    }
56
}
57

            
58
impl 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

            
64
impl Clone for TimerCallback {
65
    fn clone(&self) -> Self {
66
        Self {
67
            cb: self.cb,
68
            ctx: self.ctx.clone(),
69
        }
70
    }
71
}
72

            
73
impl From<TimerCallbackType> for TimerCallback {
74
5
    fn from(cb: TimerCallbackType) -> Self {
75
5
        Self {
76
5
            cb,
77
5
            ctx: OptionRefAny::None,
78
5
        }
79
5
    }
80
}
81

            
82
impl 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

            
88
impl Eq for TimerCallback {}
89

            
90
impl 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

            
96
impl 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

            
102
impl 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)]
111
pub 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

            
123
impl Timer {
124
5
    pub fn create<C: Into<TimerCallback>>(
125
5
        refany: RefAny,
126
5
        callback: C,
127
5
        get_system_time_fn: GetSystemTimeCallback,
128
5
    ) -> Self {
129
5
        Timer {
130
5
            refany,
131
5
            node_id: None.into(),
132
5
            created: (get_system_time_fn.cb)(),
133
5
            run_count: 0,
134
5
            last_run: OptionInstant::None,
135
5
            delay: OptionDuration::None,
136
5
            interval: OptionDuration::None,
137
5
            timeout: OptionDuration::None,
138
5
            callback: callback.into(),
139
5
        }
140
5
    }
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

            
254
impl Default for Timer {
255
5
    fn default() -> Self {
256
        extern "C" fn default_callback(_: RefAny, _: TimerCallbackInfo) -> TimerCallbackReturn {
257
            TimerCallbackReturn::terminate_unchanged()
258
        }
259

            
260
5
        extern "C" fn default_time() -> Instant {
261
5
            Instant::Tick(azul_core::task::SystemTick { tick_counter: 0 })
262
5
        }
263

            
264
5
        Timer::create(
265
5
            RefAny::new(()),
266
5
            default_callback as TimerCallbackType,
267
5
            GetSystemTimeCallback { cb: default_time },
268
        )
269
5
    }
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)]
278
pub 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

            
288
impl 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)]
604
pub enum OptionTimer {
605
    None,
606
    Some(Timer),
607
}
608

            
609
impl 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

            
618
impl 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
}