Skip to main content

azul_layout/managers/
gpu_state.rs

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
8use alloc::collections::BTreeMap;
9
10use 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
20use 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)
30pub const DEFAULT_FADE_DELAY_MS: u64 = 500;
31/// Default duration of scrollbar fade-out animation (200ms)
32pub 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)]
43pub 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
64impl 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)]
79pub 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]
94pub 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
101impl GpuStateManager {
102    /// Creates a new GPU state manager with specified fade timing.
103    pub fn new(fade_delay: Duration, fade_duration: Duration) -> Self {
104        Self {
105            caches: BTreeMap::new(),
106            fade_delay,
107            fade_duration,
108            fade_states: BTreeMap::new(),
109            scrollbar_fade_active: false,
110            pending_changes: GpuEventChanges::empty(),
111        }
112    }
113
114    /// Take any queued transform / opacity events that have been accumulated
115    /// during layout. Clears the internal buffer.
116    pub fn take_pending_changes(&mut self) -> GpuEventChanges {
117        core::mem::take(&mut self.pending_changes)
118    }
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    pub fn get_cache(&self, dom_id: DomId) -> Option<&GpuValueCache> {
230        self.caches.get(&dom_id)
231    }
232
233    pub fn get_or_create_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
234        self.caches.entry(dom_id).or_default()
235    }
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    pub fn update_scrollbar_transforms(
243        &mut self,
244        dom_id: DomId,
245        scroll_manager: &ScrollManager,
246        layout_tree: &LayoutTree,
247    ) -> GpuEventChanges {
248        let mut changes = GpuEventChanges::empty();
249        let gpu_cache = self.get_or_create_cache(dom_id);
250
251        for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
252            let warm = layout_tree.warm(node_idx);
253            let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
254                continue;
255            };
256            let Some(node_id) = node.dom_node_id else {
257                continue;
258            };
259
260            let scroll_offset = scroll_manager
261                .get_current_offset(dom_id, node_id)
262                .unwrap_or_default();
263
264            // Compute inner_rect (padding-box) by subtracting borders from used_size
265            let border_box_size = node.used_size.unwrap_or_default();
266            let nbp = node.box_props.unpack();
267            let border = &nbp.border;
268            let inner_size = LogicalSize {
269                width: (border_box_size.width - border.left - border.right).max(0.0),
270                height: (border_box_size.height - border.top - border.bottom).max(0.0),
271            };
272            // Use zero origin since we only need the geometry ratios, not absolute position
273            let inner_rect = LogicalRect {
274                origin: LogicalPosition::new(0.0, 0.0),
275                size: inner_size,
276            };
277
278            // Use get_content_size() as the single source of truth for content dimensions
279            let content_size = layout_tree.get_content_size(node_idx);
280
281            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            }
311
312            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            }
337        }
338
339        changes
340    }
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.
349fn 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.
388fn 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}