use alloc::collections::BTreeMap;
use azul_core::{
dom::{DomId, NodeId},
dom::ScrollbarOrientation,
geom::{LogicalPosition, LogicalRect, LogicalSize},
gpu::{GpuEventChanges, GpuTransformKeyEvent, GpuValueCache},
resources::TransformKey,
task::{Duration, Instant, SystemTimeDiff},
transform::ComputedTransform3D,
};
use crate::{
managers::scroll_state::ScrollManager,
solver3::{
fc::DEFAULT_SCROLLBAR_WIDTH_PX,
layout_tree::LayoutTree,
scrollbar::compute_scrollbar_geometry_with_button_size,
},
};
pub const DEFAULT_FADE_DELAY_MS: u64 = 500;
pub const DEFAULT_FADE_DURATION_MS: u64 = 200;
#[derive(Debug, Clone)]
pub struct GpuStateManager {
pub caches: BTreeMap<DomId, GpuValueCache>,
pub fade_delay: Duration,
pub fade_duration: Duration,
pub fade_states: BTreeMap<(DomId, NodeId), ScrollbarFadeState>,
pub scrollbar_fade_active: bool,
pub pending_changes: GpuEventChanges,
}
impl Default for GpuStateManager {
fn default() -> Self {
Self::new(
Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DELAY_MS)),
Duration::System(SystemTimeDiff::from_millis(DEFAULT_FADE_DURATION_MS)),
)
}
}
#[derive(Debug, Clone)]
pub struct ScrollbarFadeState {
pub last_activity_time: Option<Instant>,
pub needs_vertical: bool,
pub needs_horizontal: bool,
}
#[derive(Debug, Default)]
#[must_use]
pub struct GpuTickResult {
pub needs_repaint: bool,
pub changes: GpuEventChanges,
}
impl GpuStateManager {
pub fn new(fade_delay: Duration, fade_duration: Duration) -> Self {
Self {
caches: BTreeMap::new(),
fade_delay,
fade_duration,
fade_states: BTreeMap::new(),
scrollbar_fade_active: false,
pending_changes: GpuEventChanges::empty(),
}
}
pub fn take_pending_changes(&mut self) -> GpuEventChanges {
core::mem::take(&mut self.pending_changes)
}
pub fn tick(&mut self, now: Instant) -> GpuTickResult {
let mut needs_repaint = false;
let fade_delay = self.fade_delay;
let fade_duration = self.fade_duration;
for (&(dom_id, node_id), fade_state) in &self.fade_states {
let cache = match self.caches.get_mut(&dom_id) {
Some(c) => c,
None => continue,
};
let opacity = Self::calculate_fade_opacity(
fade_state.last_activity_time.as_ref(),
&now,
fade_delay,
fade_duration,
);
if fade_state.needs_vertical {
let key = (dom_id, node_id);
if let Some(old_val) = cache.scrollbar_v_opacity_values.get(&key) {
if (old_val - opacity).abs() > 0.001 {
cache.scrollbar_v_opacity_values.insert(key, opacity);
needs_repaint = true;
}
}
}
if fade_state.needs_horizontal {
let key = (dom_id, node_id);
if let Some(old_val) = cache.scrollbar_h_opacity_values.get(&key) {
if (old_val - opacity).abs() > 0.001 {
cache.scrollbar_h_opacity_values.insert(key, opacity);
needs_repaint = true;
}
}
}
}
GpuTickResult {
needs_repaint,
changes: GpuEventChanges::empty(),
}
}
fn calculate_fade_opacity(
last_activity: Option<&Instant>,
now: &Instant,
fade_delay: Duration,
fade_duration: Duration,
) -> f32 {
let Some(last_activity) = last_activity else {
return 0.0;
};
let time_since_activity = now.duration_since(last_activity);
if time_since_activity.div(&fade_delay) < 1.0 {
return 1.0;
}
let fade_progress = (time_since_activity.div(&fade_duration) - fade_delay.div(&fade_duration)).min(1.0);
(1.0 - fade_progress).max(0.0)
}
pub fn record_scroll_activity(
&mut self,
dom_id: DomId,
node_id: NodeId,
now: Instant,
needs_vertical: bool,
needs_horizontal: bool,
) {
let state = self.fade_states
.entry((dom_id, node_id))
.or_insert(ScrollbarFadeState {
last_activity_time: None,
needs_vertical: false,
needs_horizontal: false,
});
state.last_activity_time = Some(now);
state.needs_vertical = needs_vertical;
state.needs_horizontal = needs_horizontal;
}
pub fn get_cache(&self, dom_id: DomId) -> Option<&GpuValueCache> {
self.caches.get(&dom_id)
}
pub fn get_or_create_cache(&mut self, dom_id: DomId) -> &mut GpuValueCache {
self.caches.entry(dom_id).or_default()
}
pub fn update_scrollbar_transforms(
&mut self,
dom_id: DomId,
scroll_manager: &ScrollManager,
layout_tree: &LayoutTree,
) -> GpuEventChanges {
let mut changes = GpuEventChanges::empty();
let gpu_cache = self.get_or_create_cache(dom_id);
for (node_idx, node) in layout_tree.nodes.iter().enumerate() {
let warm = layout_tree.warm(node_idx);
let Some(scrollbar_info) = warm.and_then(|w| w.scrollbar_info.as_ref()) else {
continue;
};
let Some(node_id) = node.dom_node_id else {
continue;
};
let scroll_offset = scroll_manager
.get_current_offset(dom_id, node_id)
.unwrap_or_default();
let border_box_size = node.used_size.unwrap_or_default();
let nbp = node.box_props.unpack();
let border = &nbp.border;
let inner_size = LogicalSize {
width: (border_box_size.width - border.left - border.right).max(0.0),
height: (border_box_size.height - border.top - border.bottom).max(0.0),
};
let inner_rect = LogicalRect {
origin: LogicalPosition::new(0.0, 0.0),
size: inner_size,
};
let content_size = layout_tree.get_content_size(node_idx);
if scrollbar_info.needs_vertical {
let is_overlay = scrollbar_info.scrollbar_height == 0.0;
let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
scrollbar_info.visual_width_px
} else if !is_overlay {
scrollbar_info.scrollbar_height
} else {
DEFAULT_SCROLLBAR_WIDTH_PX
};
let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
let v_geom = compute_scrollbar_geometry_with_button_size(
ScrollbarOrientation::Vertical,
inner_rect,
content_size,
scroll_offset.y,
scrollbar_width_px,
scrollbar_info.needs_horizontal,
button_size,
);
let transform =
ComputedTransform3D::new_translation(0.0, v_geom.thumb_offset, 0.0);
update_transform_key(gpu_cache, &mut changes, dom_id, node_id, transform);
}
if scrollbar_info.needs_horizontal {
let is_overlay = scrollbar_info.scrollbar_width == 0.0;
let scrollbar_width_px = if scrollbar_info.visual_width_px > 0.0 {
scrollbar_info.visual_width_px
} else if !is_overlay {
scrollbar_info.scrollbar_width
} else {
DEFAULT_SCROLLBAR_WIDTH_PX
};
let button_size = if is_overlay { 0.0 } else { scrollbar_width_px };
let h_geom = compute_scrollbar_geometry_with_button_size(
ScrollbarOrientation::Horizontal,
inner_rect,
content_size,
scroll_offset.x,
scrollbar_width_px,
scrollbar_info.needs_vertical,
button_size,
);
let transform =
ComputedTransform3D::new_translation(h_geom.thumb_offset, 0.0, 0.0);
update_h_transform_key(gpu_cache, &mut changes, dom_id, node_id, transform);
}
}
changes
}
pub fn get_gpu_value_cache(&self) -> BTreeMap<DomId, GpuValueCache> {
self.caches.clone()
}
}
fn update_transform_key(
gpu_cache: &mut GpuValueCache,
changes: &mut GpuEventChanges,
dom_id: DomId,
node_id: NodeId,
transform: ComputedTransform3D,
) {
if let Some(existing_transform) = gpu_cache.current_transform_values.get(&node_id) {
if *existing_transform != transform {
let transform_key = gpu_cache.transform_keys[&node_id];
changes
.transform_key_changes
.push(GpuTransformKeyEvent::Changed(
node_id,
transform_key,
*existing_transform,
transform,
));
gpu_cache
.current_transform_values
.insert(node_id, transform);
}
} else {
let transform_key = TransformKey::unique();
gpu_cache.transform_keys.insert(node_id, transform_key);
gpu_cache
.current_transform_values
.insert(node_id, transform);
changes
.transform_key_changes
.push(GpuTransformKeyEvent::Added(
node_id,
transform_key,
transform,
));
}
}
fn update_h_transform_key(
gpu_cache: &mut GpuValueCache,
changes: &mut GpuEventChanges,
dom_id: DomId,
node_id: NodeId,
transform: ComputedTransform3D,
) {
if let Some(existing_transform) = gpu_cache.h_current_transform_values.get(&node_id) {
if *existing_transform != transform {
let transform_key = gpu_cache.h_transform_keys[&node_id];
changes
.transform_key_changes
.push(GpuTransformKeyEvent::Changed(
node_id,
transform_key,
*existing_transform,
transform,
));
gpu_cache
.h_current_transform_values
.insert(node_id, transform);
}
} else {
let transform_key = TransformKey::unique();
gpu_cache.h_transform_keys.insert(node_id, transform_key);
gpu_cache
.h_current_transform_values
.insert(node_id, transform);
changes
.transform_key_changes
.push(GpuTransformKeyEvent::Added(
node_id,
transform_key,
transform,
));
}
}