1
//! Centralized GPU state management.
2
//!
3
//! This module provides management of GPU property keys
4
//! (opacity, transforms, etc.), fade-in/fade-out animations
5
//! for scrollbar opacity - as a single source of truth for
6
//! the GPU cache.
7

            
8
use alloc::collections::BTreeMap;
9

            
10
use azul_core::{
11
    dom::{DomId, NodeId},
12
    dom::ScrollbarOrientation,
13
    geom::{LogicalPosition, LogicalRect, LogicalSize},
14
    gpu::{GpuEventChanges, GpuTransformKeyEvent, GpuValueCache},
15
    resources::TransformKey,
16
    task::{Duration, Instant, SystemTimeDiff},
17
    transform::ComputedTransform3D,
18
};
19

            
20
use crate::{
21
    managers::scroll_state::ScrollManager,
22
    solver3::{
23
        fc::DEFAULT_SCROLLBAR_WIDTH_PX,
24
        layout_tree::LayoutTree,
25
        scrollbar::compute_scrollbar_geometry_with_button_size,
26
    },
27
};
28

            
29
/// Default delay before scrollbars start fading out (500ms)
30
pub const DEFAULT_FADE_DELAY_MS: u64 = 500;
31
/// Default duration of scrollbar fade-out animation (200ms)
32
pub const DEFAULT_FADE_DURATION_MS: u64 = 200;
33

            
34
/// Manages GPU-accelerated properties across all DOMs.
35
///
36
/// The `GpuStateManager` maintains caches for transform and opacity keys
37
/// that are used by the GPU renderer. It handles:
38
///
39
/// - Scrollbar thumb position transforms (updated on scroll)
40
/// - Opacity fading for scrollbars (fade in on activity, fade out after delay)
41
/// - Per-DOM GPU value caches for efficient rendering
42
#[derive(Debug, Clone)]
43
pub struct GpuStateManager {
44
    /// GPU value caches indexed by DOM ID
45
    pub caches: BTreeMap<DomId, GpuValueCache>,
46
    /// Delay before scrollbars start fading out after last activity
47
    pub fade_delay: Duration,
48
    /// Duration of the fade-out animation
49
    pub fade_duration: Duration,
50
    /// Per-scrollbar fade state: (DomId, NodeId) → last activity time
51
    pub fade_states: BTreeMap<(DomId, NodeId), ScrollbarFadeState>,
52
    /// Whether any scrollbar has non-zero opacity and needs continued frame
53
    /// generation. Set during both the fade_delay period (opacity == 1.0)
54
    /// and the active fade-out phase (0 < opacity < 1).
55
    /// Set by `LayoutWindow::synchronize_scrollbar_opacity`, read by the platform render loop.
56
    pub scrollbar_fade_active: bool,
57
    /// GPU events produced during layout (CSS transform / opacity synchronization,
58
    /// scrollbar transform / opacity updates) that have not yet been pushed to
59
    /// the renderer. Drained by the platform render path when a transaction is
60
    /// built.
61
    pub pending_changes: GpuEventChanges,
62
}
63

            
64
impl Default for GpuStateManager {
65
    fn default() -> Self {
66
        Self::new(
67
            Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DELAY_MS)),
68
            Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DURATION_MS)),
69
        )
70
    }
71
}
72

            
73
/// Internal state for tracking per-scrollbar fade activity.
74
///
75
/// Stores the last scroll activity time so that `tick()` can
76
/// independently recalculate opacity values each frame without
77
/// needing access to the `ScrollManager`.
78
#[derive(Debug, Clone)]
79
pub struct ScrollbarFadeState {
80
    /// Timestamp of last scroll activity for this scrollbar
81
    pub last_activity_time: Option<Instant>,
82
    /// Whether this scrollbar needs vertical fading
83
    pub needs_vertical: bool,
84
    /// Whether this scrollbar needs horizontal fading
85
    pub needs_horizontal: bool,
86
}
87

            
88
/// Result of a GPU state tick operation.
89
///
90
/// Contains information about whether the GPU state changed and
91
/// what specific changes occurred for the renderer to process.
92
#[derive(Debug, Default)]
93
#[must_use]
94
pub struct GpuTickResult {
95
    /// Whether any GPU state changed requiring a repaint
96
    pub needs_repaint: bool,
97
    /// Detailed changes to transform and opacity keys
98
    pub changes: GpuEventChanges,
99
}
100

            
101
impl GpuStateManager {
102
    /// Creates a new GPU state manager with specified fade timing.
103
2321
    pub fn new(fade_delay: Duration, fade_duration: Duration) -> Self {
104
2321
        Self {
105
2321
            caches: BTreeMap::new(),
106
2321
            fade_delay,
107
2321
            fade_duration,
108
2321
            fade_states: BTreeMap::new(),
109
2321
            scrollbar_fade_active: false,
110
2321
            pending_changes: GpuEventChanges::empty(),
111
2321
        }
112
2321
    }
113

            
114
    /// Take any queued transform / opacity events that have been accumulated
115
    /// during layout. Clears the internal buffer.
116
3
    pub fn take_pending_changes(&mut self) -> GpuEventChanges {
117
3
        core::mem::take(&mut self.pending_changes)
118
3
    }
119

            
120
    /// Advances GPU state by one tick, interpolating animated opacity values.
121
    ///
122
    /// This should be called each frame to update opacity transitions
123
    /// for smooth scrollbar fading. Returns whether a repaint is needed
124
    /// (i.e., any opacity value changed).
125
    pub fn tick(&mut self, now: Instant) -> GpuTickResult {
126
        let mut needs_repaint = false;
127
        let fade_delay = self.fade_delay;
128
        let fade_duration = self.fade_duration;
129

            
130
        // Iterate over all tracked fade states and recalculate opacity
131
        for (&(dom_id, node_id), fade_state) in &self.fade_states {
132
            let cache = match self.caches.get_mut(&dom_id) {
133
                Some(c) => c,
134
                None => continue,
135
            };
136

            
137
            let opacity = Self::calculate_fade_opacity(
138
                fade_state.last_activity_time.as_ref(),
139
                &now,
140
                fade_delay,
141
                fade_duration,
142
            );
143

            
144
            // Update vertical opacity
145
            if fade_state.needs_vertical {
146
                let key = (dom_id, node_id);
147
                if let Some(old_val) = cache.scrollbar_v_opacity_values.get(&key) {
148
                    if (old_val - opacity).abs() > 0.001 {
149
                        cache.scrollbar_v_opacity_values.insert(key, opacity);
150
                        needs_repaint = true;
151
                    }
152
                }
153
            }
154

            
155
            // Update horizontal opacity
156
            if fade_state.needs_horizontal {
157
                let key = (dom_id, node_id);
158
                if let Some(old_val) = cache.scrollbar_h_opacity_values.get(&key) {
159
                    if (old_val - opacity).abs() > 0.001 {
160
                        cache.scrollbar_h_opacity_values.insert(key, opacity);
161
                        needs_repaint = true;
162
                    }
163
                }
164
            }
165
        }
166

            
167
        GpuTickResult {
168
            needs_repaint,
169
            changes: GpuEventChanges::empty(),
170
        }
171
    }
172

            
173
    /// Calculate scrollbar opacity based on elapsed time since last activity.
174
    ///
175
    /// Three-phase model:
176
    /// 1. During `fade_delay`: fully visible (1.0)
177
    /// 2. During `fade_duration` after delay: linear fade from 1.0 to 0.0
178
    /// 3. After delay + duration: fully hidden (0.0)
179
    fn calculate_fade_opacity(
180
        last_activity: Option<&Instant>,
181
        now: &Instant,
182
        fade_delay: Duration,
183
        fade_duration: Duration,
184
    ) -> f32 {
185
        let Some(last_activity) = last_activity else {
186
            return 0.0;
187
        };
188

            
189
        let time_since_activity = now.duration_since(last_activity);
190

            
191
        // Phase 1: Scrollbar stays fully visible during fade_delay
192
        if time_since_activity.div(&fade_delay) < 1.0 {
193
            return 1.0;
194
        }
195

            
196
        // Phase 2: Fade out over fade_duration
197
        // Compute (time_since_activity - fade_delay) / fade_duration
198
        let fade_progress = (time_since_activity.div(&fade_duration) - fade_delay.div(&fade_duration)).min(1.0);
199

            
200
        // Phase 3: Fully faded
201
        (1.0 - fade_progress).max(0.0)
202
    }
203

            
204
    /// Record scroll activity for a scrollbar node, resetting the fade timer.
205
    ///
206
    /// This should be called whenever scroll activity occurs to keep the
207
    /// scrollbar visible and reset the fade-out timer.
208
    pub fn record_scroll_activity(
209
        &mut self,
210
        dom_id: DomId,
211
        node_id: NodeId,
212
        now: Instant,
213
        needs_vertical: bool,
214
        needs_horizontal: bool,
215
    ) {
216
        let state = self.fade_states
217
            .entry((dom_id, node_id))
218
            .or_insert(ScrollbarFadeState {
219
                last_activity_time: None,
220
                needs_vertical: false,
221
                needs_horizontal: false,
222
            });
223
        state.last_activity_time = Some(now);
224
        state.needs_vertical = needs_vertical;
225
        state.needs_horizontal = needs_horizontal;
226
    }
227

            
228
    /// Gets or creates the GPU cache for a specific DOM.
229
70
    pub fn get_cache(&self, dom_id: DomId) -> Option<&GpuValueCache> {
230
70
        self.caches.get(&dom_id)
231
70
    }
232

            
233
9559
    pub fn get_or_create_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
234
9559
        self.caches.entry(dom_id).or_default()
235
9559
    }
236

            
237
    /// Updates scrollbar thumb transforms based on current scroll positions.
238
    ///
239
    /// Calculates the transform needed to position scrollbar thumbs correctly
240
    /// based on the scroll offset and content/container sizes. Returns the
241
    /// GPU event changes that need to be applied by the renderer.
242
2275
    pub fn update_scrollbar_transforms(
243
2275
        &mut self,
244
2275
        dom_id: DomId,
245
2275
        scroll_manager: &ScrollManager,
246
2275
        layout_tree: &LayoutTree,
247
2275
    ) -> GpuEventChanges {
248
2275
        let mut changes = GpuEventChanges::empty();
249
2275
        let gpu_cache = self.get_or_create_cache(dom_id);
250

            
251
15295
        for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
252
15295
            let warm = layout_tree.warm(node_idx);
253
15295
            let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
254
5530
                continue;
255
            };
256
9765
            let Some(node_id) = node.dom_node_id else {
257
35
                continue;
258
            };
259

            
260
9730
            let scroll_offset = scroll_manager
261
9730
                .get_current_offset(dom_id, node_id)
262
9730
                .unwrap_or_default();
263

            
264
            // Compute inner_rect (padding-box) by subtracting borders from used_size
265
9730
            let border_box_size = node.used_size.unwrap_or_default();
266
9730
            let nbp = node.box_props.unpack();
267
9730
            let border = &nbp.border;
268
9730
            let inner_size = LogicalSize {
269
9730
                width: (border_box_size.width - border.left - border.right).max(0.0),
270
9730
                height: (border_box_size.height - border.top - border.bottom).max(0.0),
271
9730
            };
272
            // Use zero origin since we only need the geometry ratios, not absolute position
273
9730
            let inner_rect = LogicalRect {
274
9730
                origin: LogicalPosition::new(0.0, 0.0),
275
9730
                size: inner_size,
276
9730
            };
277

            
278
            // Use get_content_size() as the single source of truth for content dimensions
279
9730
            let content_size = layout_tree.get_content_size(node_idx);
280

            
281
9730
            if scrollbar_info.needs_vertical {
282
                // Use the visual width from the scrollbar style — same value used
283
                // by display_list.rs to paint the scrollbar. For overlay scrollbars,
284
                // visual_width_px is non-zero (e.g. 8.0) even though the layout-
285
                // reserved width (scrollbar_height) is 0.0.
286
                let is_overlay = scrollbar_info.scrollbar_height == 0.0;
287
                let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
288
                    scrollbar_info.visual_width_px
289
                } else if !is_overlay {
290
                    scrollbar_info.scrollbar_height
291
                } else {
292
                    DEFAULT_SCROLLBAR_WIDTH_PX
293
                };
294
                // Overlay scrollbars (macOS-style) have no arrow buttons
295
                let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
296

            
297
                let v_geom = compute_scrollbar_geometry_with_button_size(
298
                    ScrollbarOrientation::Vertical,
299
                    inner_rect,
300
                    content_size,
301
                    scroll_offset.y,
302
                    scrollbar_width_px,
303
                    scrollbar_info.needs_horizontal,
304
                    button_size,
305
                );
306

            
307
                let transform =
308
                    ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
309
                update_transform_key(gpu_cache, &mut changes, dom_id, node_id, transform);
310
9730
            }
311

            
312
9730
            if scrollbar_info.needs_horizontal {
313
                let is_overlay = scrollbar_info.scrollbar_width == 0.0;
314
                let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
315
                    scrollbar_info.visual_width_px
316
                } else if !is_overlay {
317
                    scrollbar_info.scrollbar_width
318
                } else {
319
                    DEFAULT_SCROLLBAR_WIDTH_PX
320
                };
321
                let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
322

            
323
                let h_geom = compute_scrollbar_geometry_with_button_size(
324
                    ScrollbarOrientation::Horizontal,
325
                    inner_rect,
326
                    content_size,
327
                    scroll_offset.x,
328
                    scrollbar_width_px,
329
                    scrollbar_info.needs_vertical,
330
                    button_size,
331
                );
332

            
333
                let transform =
334
                    ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
335
                update_h_transform_key(gpu_cache, &mut changes, dom_id, node_id, transform);
336
9730
            }
337
        }
338

            
339
2275
        changes
340
2275
    }
341

            
342
    /// Returns a clone of all GPU value caches.
343
    pub fn get_gpu_value_cache(&self) -> BTreeMap<DomId, GpuValueCache> {
344
        self.caches.clone()
345
    }
346
}
347

            
348
/// Updates or creates a vertical scrollbar transform key in the GPU cache.
349
fn update_transform_key(
350
    gpu_cache: &mut GpuValueCache,
351
    changes: &mut GpuEventChanges,
352
    dom_id: DomId,
353
    node_id: NodeId,
354
    transform: ComputedTransform3D,
355
) {
356
    if let Some(existing_transform) = gpu_cache.current_transform_values.get(&node_id) {
357
        if *existing_transform != transform {
358
            let transform_key = gpu_cache.transform_keys[&node_id];
359
            changes
360
                .transform_key_changes
361
                .push(GpuTransformKeyEvent::Changed(
362
                    node_id,
363
                    transform_key,
364
                    *existing_transform,
365
                    transform,
366
                ));
367
            gpu_cache
368
                .current_transform_values
369
                .insert(node_id, transform);
370
        }
371
    } else {
372
        let transform_key = TransformKey::unique();
373
        gpu_cache.transform_keys.insert(node_id, transform_key);
374
        gpu_cache
375
            .current_transform_values
376
            .insert(node_id, transform);
377
        changes
378
            .transform_key_changes
379
            .push(GpuTransformKeyEvent::Added(
380
                node_id,
381
                transform_key,
382
                transform,
383
            ));
384
    }
385
}
386

            
387
/// Updates or creates a horizontal scrollbar transform key in the GPU cache.
388
fn update_h_transform_key(
389
    gpu_cache: &mut GpuValueCache,
390
    changes: &mut GpuEventChanges,
391
    dom_id: DomId,
392
    node_id: NodeId,
393
    transform: ComputedTransform3D,
394
) {
395
    if let Some(existing_transform) = gpu_cache.h_current_transform_values.get(&node_id) {
396
        if *existing_transform != transform {
397
            let transform_key = gpu_cache.h_transform_keys[&node_id];
398
            changes
399
                .transform_key_changes
400
                .push(GpuTransformKeyEvent::Changed(
401
                    node_id,
402
                    transform_key,
403
                    *existing_transform,
404
                    transform,
405
                ));
406
            gpu_cache
407
                .h_current_transform_values
408
                .insert(node_id, transform);
409
        }
410
    } else {
411
        let transform_key = TransformKey::unique();
412
        gpu_cache.h_transform_keys.insert(node_id, transform_key);
413
        gpu_cache
414
            .h_current_transform_values
415
            .insert(node_id, transform);
416
        changes
417
            .transform_key_changes
418
            .push(GpuTransformKeyEvent::Added(
419
                node_id,
420
                transform_key,
421
                transform,
422
            ));
423
    }
424
}