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
10#[cfg(feature = "std")]
11use std::collections::HashMap;
12#[cfg(not(feature = "std"))]
13use alloc::collections::BTreeMap as HashMap;
14
15use azul_core::{
16    dom::{DomId, NodeId},
17    dom::ScrollbarOrientation,
18    geom::{LogicalPosition, LogicalRect, LogicalSize},
19    gpu::{GpuEventChanges, GpuTransformKeyEvent, GpuValueCache},
20    resources::TransformKey,
21    task::{Duration, SystemTimeDiff},
22    transform::ComputedTransform3D,
23};
24
25use crate::{
26    managers::scroll_state::ScrollManager,
27    solver3::{
28        fc::DEFAULT_SCROLLBAR_WIDTH_PX,
29        layout_tree::LayoutTree,
30        scrollbar::compute_scrollbar_geometry_with_button_size,
31    },
32};
33
34/// Default delay before scrollbars start fading out (500ms)
35pub const DEFAULT_FADE_DELAY_MS: u64 = 500;
36/// Default duration of scrollbar fade-out animation (200ms)
37pub const DEFAULT_FADE_DURATION_MS: u64 = 200;
38
39/// Manages GPU-accelerated properties across all DOMs.
40///
41/// The `GpuStateManager` maintains caches for transform and opacity keys
42/// that are used by the GPU renderer. It handles:
43///
44/// - Scrollbar thumb position transforms (updated on scroll)
45/// - Opacity fading for scrollbars (fade in on activity, fade out after delay)
46/// - Per-DOM GPU value caches for efficient rendering
47#[derive(Debug, Clone)]
48pub struct GpuStateManager {
49    /// GPU value caches indexed by DOM ID
50    pub caches: BTreeMap<DomId, GpuValueCache>,
51    /// Delay before scrollbars start fading out after last activity
52    pub fade_delay: Duration,
53    /// Duration of the fade-out animation
54    pub fade_duration: Duration,
55    /// Whether any scrollbar has non-zero opacity and needs continued frame
56    /// generation. Set during both the `fade_delay` period (opacity == 1.0)
57    /// and the active fade-out phase (0 < opacity < 1).
58    /// Set by `LayoutWindow::synchronize_scrollbar_opacity`, read by the platform render loop.
59    pub scrollbar_fade_active: bool,
60    /// GPU events produced during layout (CSS transform / opacity synchronization,
61    /// scrollbar transform / opacity updates) that have not yet been pushed to
62    /// the renderer. Drained by the platform render path when a transaction is
63    /// built.
64    pub pending_changes: GpuEventChanges,
65}
66
67impl Default for GpuStateManager {
68    fn default() -> Self {
69        Self::new(
70            Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DELAY_MS)),
71            Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DURATION_MS)),
72        )
73    }
74}
75
76impl GpuStateManager {
77    /// Creates a new GPU state manager with specified fade timing.
78    #[must_use] pub fn new(fade_delay: Duration, fade_duration: Duration) -> Self {
79        Self {
80            caches: BTreeMap::new(),
81            fade_delay,
82            fade_duration,
83            scrollbar_fade_active: false,
84            pending_changes: GpuEventChanges::empty(),
85        }
86    }
87
88    /// Take any queued transform / opacity events that have been accumulated
89    /// during layout. Clears the internal buffer.
90    pub fn take_pending_changes(&mut self) -> GpuEventChanges {
91        core::mem::take(&mut self.pending_changes)
92    }
93
94    // NOTE: the per-frame scrollbar-fade interpolation lives in
95    // `LayoutWindow::synchronize_scrollbar_opacity` (layout/src/window.rs),
96    // which reads the last-activity time straight from the `ScrollManager` and
97    // is the single source of truth for fade opacity. An earlier duplicate
98    // tick-based subsystem here (`tick` / `record_scroll_activity` /
99    // `calculate_fade_opacity` + a `fade_states` map) was never wired into any
100    // render loop and has been removed.
101
102    /// Gets or creates the GPU cache for a specific DOM.
103    #[must_use] pub fn get_cache(&self, dom_id: DomId) -> Option<&GpuValueCache> {
104        self.caches.get(&dom_id)
105    }
106
107    pub fn get_or_create_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
108        self.caches.entry(dom_id).or_default()
109    }
110
111    /// Updates scrollbar thumb transforms based on current scroll positions.
112    ///
113    /// Calculates the transform needed to position scrollbar thumbs correctly
114    /// based on the scroll offset and content/container sizes. Returns the
115    /// GPU event changes that need to be applied by the renderer.
116    pub fn update_scrollbar_transforms(
117        &mut self,
118        dom_id: DomId,
119        scroll_manager: &ScrollManager,
120        layout_tree: &LayoutTree,
121    ) -> GpuEventChanges {
122        let mut changes = GpuEventChanges::empty();
123        let gpu_cache = self.get_or_create_cache(dom_id);
124
125        for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
126            let warm = layout_tree.warm(node_idx);
127            let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
128                continue;
129            };
130            let Some(node_id) = node.dom_node_id else {
131                continue;
132            };
133
134            let scroll_offset = scroll_manager
135                .get_current_offset(dom_id, node_id)
136                .unwrap_or_default();
137
138            // Compute inner_rect (padding-box) by subtracting borders from used_size
139            let border_box_size = node.used_size.unwrap_or_default();
140            let nbp = node.box_props.unpack();
141            let border = &nbp.border;
142            let inner_size = LogicalSize {
143                width: (border_box_size.width - border.left - border.right).max(0.0),
144                height: (border_box_size.height - border.top - border.bottom).max(0.0),
145            };
146            // Use zero origin since we only need the geometry ratios, not absolute position
147            let inner_rect = LogicalRect {
148                origin: LogicalPosition::new(0.0, 0.0),
149                size: inner_size,
150            };
151
152            // Use get_content_size() as the single source of truth for content dimensions
153            let content_size = layout_tree.get_content_size(node_idx);
154
155            if scrollbar_info.needs_vertical {
156                // Use the visual width from the scrollbar style — same value used
157                // by display_list.rs to paint the scrollbar. For overlay scrollbars,
158                // visual_width_px is non-zero (e.g. 8.0) even though the layout-
159                // reserved width (scrollbar_height) is 0.0.
160                let is_overlay = scrollbar_info.scrollbar_height == 0.0;
161                let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
162                    scrollbar_info.visual_width_px
163                } else if !is_overlay {
164                    scrollbar_info.scrollbar_height
165                } else {
166                    DEFAULT_SCROLLBAR_WIDTH_PX
167                };
168                // Overlay scrollbars (macOS-style) have no arrow buttons
169                let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
170
171                let v_geom = compute_scrollbar_geometry_with_button_size(
172                    ScrollbarOrientation::Vertical,
173                    inner_rect,
174                    content_size,
175                    scroll_offset.y,
176                    scrollbar_width_px,
177                    scrollbar_info.needs_horizontal,
178                    button_size,
179                );
180
181                let transform =
182                    ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
183                update_scrollbar_transform_key(gpu_cache, &mut changes, node_id, transform, ScrollbarOrientation::Vertical);
184            }
185
186            if scrollbar_info.needs_horizontal {
187                let is_overlay = scrollbar_info.scrollbar_width == 0.0;
188                let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
189                    scrollbar_info.visual_width_px
190                } else if !is_overlay {
191                    scrollbar_info.scrollbar_width
192                } else {
193                    DEFAULT_SCROLLBAR_WIDTH_PX
194                };
195                let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
196
197                let h_geom = compute_scrollbar_geometry_with_button_size(
198                    ScrollbarOrientation::Horizontal,
199                    inner_rect,
200                    content_size,
201                    scroll_offset.x,
202                    scrollbar_width_px,
203                    scrollbar_info.needs_vertical,
204                    button_size,
205                );
206
207                let transform =
208                    ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
209                update_scrollbar_transform_key(gpu_cache, &mut changes, node_id, transform, ScrollbarOrientation::Horizontal);
210            }
211        }
212
213        changes
214    }
215}
216
217/// Updates or creates a scrollbar transform key in the GPU cache for the given orientation.
218fn update_scrollbar_transform_key(
219    gpu_cache: &mut GpuValueCache,
220    changes: &mut GpuEventChanges,
221    node_id: NodeId,
222    transform: ComputedTransform3D,
223    orientation: ScrollbarOrientation,
224) {
225    let (keys, values) = match orientation {
226        ScrollbarOrientation::Vertical => (
227            &mut gpu_cache.transform_keys,
228            &mut gpu_cache.current_transform_values,
229        ),
230        ScrollbarOrientation::Horizontal => (
231            &mut gpu_cache.h_transform_keys,
232            &mut gpu_cache.h_current_transform_values,
233        ),
234    };
235
236    if let Some(existing_transform) = values.get(&node_id) {
237        if *existing_transform != transform {
238            let Some(&transform_key) = keys.get(&node_id) else {
239                return;
240            };
241            changes
242                .transform_key_changes
243                .push(GpuTransformKeyEvent::Changed(
244                    node_id,
245                    transform_key,
246                    *existing_transform,
247                    transform,
248                ));
249            values.insert(node_id, transform);
250        }
251    } else {
252        let transform_key = TransformKey::unique();
253        keys.insert(node_id, transform_key);
254        values.insert(node_id, transform);
255        changes
256            .transform_key_changes
257            .push(GpuTransformKeyEvent::Added(
258                node_id,
259                transform_key,
260                transform,
261            ));
262    }
263}
264
265impl crate::managers::NodeIdRemap for GpuStateManager {
266    /// Remap the per-node GPU caches (transform / opacity keys and values).
267    ///
268    /// Without this, a rebuild that shifts `NodeIds` left the scrollbar/CSS
269    /// transform + opacity keys attached to the wrong node — the visible symptom
270    /// being a scrollbar thumb (or an animated element) that keeps painting at a
271    /// stale offset, plus `scrollbar_fade_active` never settling because the
272    /// platform loop keeps generating frames for a node that is not the one
273    /// actually scrolling.
274    fn remap_node_ids(&mut self, dom: DomId, map: &crate::managers::NodeIdMap) {
275        let Some(cache) = self.caches.get_mut(&dom) else {
276            return;
277        };
278
279        remap_hashmap(&mut cache.transform_keys, map);
280        remap_hashmap(&mut cache.current_transform_values, map);
281        remap_hashmap(&mut cache.h_transform_keys, map);
282        remap_hashmap(&mut cache.h_current_transform_values, map);
283        remap_hashmap(&mut cache.css_transform_keys, map);
284        remap_hashmap(&mut cache.css_current_transform_values, map);
285        remap_hashmap(&mut cache.opacity_keys, map);
286        remap_hashmap(&mut cache.current_opacity_values, map);
287        remap_dom_hashmap(&mut cache.scrollbar_v_opacity_keys, dom, map);
288        remap_dom_hashmap(&mut cache.scrollbar_h_opacity_keys, dom, map);
289        remap_dom_hashmap(&mut cache.scrollbar_v_opacity_values, dom, map);
290        remap_dom_hashmap(&mut cache.scrollbar_h_opacity_values, dom, map);
291    }
292}
293
294/// Rewrite `NodeId` keys, dropping entries for unmounted nodes.
295fn remap_hashmap<V>(map: &mut HashMap<NodeId, V>, node_map: &crate::managers::NodeIdMap) {
296    let old = core::mem::take(map);
297    for (old_id, v) in old {
298        if let Some(new_id) = node_map.resolve(old_id) {
299            map.insert(new_id, v);
300        }
301    }
302}
303
304/// Rewrite `(DomId, NodeId)` keys for `dom` only, dropping unmounted nodes.
305fn remap_dom_hashmap<V>(
306    map: &mut HashMap<(DomId, NodeId), V>,
307    dom: DomId,
308    node_map: &crate::managers::NodeIdMap,
309) {
310    let old = core::mem::take(map);
311    for ((d, old_id), v) in old {
312        if d != dom {
313            map.insert((d, old_id), v);
314        } else if let Some(new_id) = node_map.resolve(old_id) {
315            map.insert((d, new_id), v);
316        }
317    }
318}