use super::super::ElementHostWidget;
use crate::declarative::layout_helpers::clamp_to_constraints;
use crate::declarative::prelude::*;
use crate::cache_key::CacheKeyBuilder;
use crate::layout_constraints::{AvailableSpace, LayoutConstraints, LayoutSize};
use crate::tree::{
UiDebugInvalidationDetail, UiDebugInvalidationSource, UiDebugScrollAxis,
UiDebugScrollNodeTelemetry, UiDebugScrollOverflowObservationTelemetry,
};
use fret_core::FrameId;
use fret_core::time::{Duration, Instant};
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
struct ScrollLayoutProfileConfig {
min_elapsed: Duration,
min_self_measure: Duration,
}
impl ScrollLayoutProfileConfig {
fn from_env() -> Option<Self> {
let cfg = crate::runtime_config::ui_runtime_config().scroll_layout_profile?;
Some(Self {
min_elapsed: cfg.min_elapsed,
min_self_measure: cfg.min_self_measure,
})
}
}
fn scroll_layout_profile_config() -> Option<&'static ScrollLayoutProfileConfig> {
static CONFIG: OnceLock<Option<ScrollLayoutProfileConfig>> = OnceLock::new();
CONFIG
.get_or_init(ScrollLayoutProfileConfig::from_env)
.as_ref()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct ScrollLayoutProbeKey {
avail_w: u64,
avail_h: u64,
}
#[derive(Debug, Default, Clone)]
struct ScrollLayoutProbeCacheState {
frame_id: FrameId,
entries: Vec<(ScrollLayoutProbeKey, Size)>,
last_max_child: Size,
}
fn available_space_cache_key(space: AvailableSpace) -> u64 {
match space {
AvailableSpace::Definite(px) => px.0.to_bits() as u64,
AvailableSpace::MinContent => 1 << 62,
AvailableSpace::MaxContent => 2 << 62,
}
}
fn scroll_defer_unbounded_probe_on_resize_enabled() -> bool {
crate::runtime_config::ui_runtime_config().scroll_defer_unbounded_probe_on_resize
}
fn scroll_defer_unbounded_probe_on_invalidation_enabled() -> bool {
crate::runtime_config::ui_runtime_config().scroll_defer_unbounded_probe_on_invalidation
}
fn scroll_defer_unbounded_probe_stable_frames() -> u8 {
crate::runtime_config::ui_runtime_config().scroll_defer_unbounded_probe_stable_frames
}
fn scroll_overflow_observation_is_authoritative(
observation: UiDebugScrollOverflowObservationTelemetry,
) -> bool {
!observation.deep_scan_budget_hit
&& (!observation.wrapper_peel_budget_hit || observation.deep_scan_enabled)
}
fn scroll_overflow_observation_needs_follow_up_probe(
observation: UiDebugScrollOverflowObservationTelemetry,
) -> bool {
(observation.wrapper_peel_budget_hit || observation.deep_scan_budget_hit)
&& !scroll_overflow_observation_is_authoritative(observation)
}
fn maybe_schedule_extent_probe_after_observation_budget_hit<H: UiHost>(
app: &mut H,
tree: &mut UiTree<H>,
window: AppWindowId,
node: NodeId,
element: GlobalElementId,
observation: UiDebugScrollOverflowObservationTelemetry,
) -> bool {
if !scroll_overflow_observation_needs_follow_up_probe(observation) {
return false;
}
let first_set = crate::elements::with_element_state(
app,
window,
element,
crate::element::ScrollState::default,
|state| {
let prev = state.pending_extent_probe;
state.pending_extent_probe = true;
!prev
},
);
if !first_set {
return false;
}
tree.schedule_barrier_relayout_with_source_and_detail(
node,
UiDebugInvalidationSource::Other,
UiDebugInvalidationDetail::ScrollExtentsObservationBudgetHit,
);
true
}
trait ScrollOverflowTree {
fn children_ref(&self, node: NodeId) -> &[NodeId];
fn node_bounds(&self, node: NodeId) -> Option<Rect>;
fn node_is_absolute(&mut self, node: NodeId) -> bool;
fn node_clips_descendant_scroll_overflow(&mut self, node: NodeId) -> bool;
}
struct UiTreeScrollOverflowTree<'a, 'b, H: UiHost> {
tree: &'a crate::tree::UiTree<H>,
app: &'b mut H,
window: AppWindowId,
}
impl<H: UiHost> ScrollOverflowTree for UiTreeScrollOverflowTree<'_, '_, H> {
fn children_ref(&self, node: NodeId) -> &[NodeId] {
self.tree.children_ref(node)
}
fn node_bounds(&self, node: NodeId) -> Option<Rect> {
self.tree.node_bounds(node)
}
fn node_is_absolute(&mut self, node: NodeId) -> bool {
crate::declarative::frame::layout_style_for_node(self.app, self.window, node).position
== crate::element::PositionStyle::Absolute
}
fn node_clips_descendant_scroll_overflow(&mut self, node: NodeId) -> bool {
let layout_clips = matches!(
crate::declarative::frame::layout_style_for_node(self.app, self.window, node).overflow,
crate::element::Overflow::Clip
);
layout_clips
|| crate::declarative::frame::element_record_for_node(self.app, self.window, node)
.is_some_and(|record| {
matches!(
record.instance,
crate::declarative::frame::ElementInstance::Scroll(_)
| crate::declarative::frame::ElementInstance::ViewportSurface(_)
| crate::declarative::frame::ElementInstance::VirtualList(_)
)
})
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
struct ScrollOverflowObservedExtents {
loose: Size,
trusted: Size,
}
#[derive(Debug, Clone)]
struct ScrollOverflowObservedNode {
extent: Size,
children: Vec<NodeId>,
pinned_to_content_extent_x: bool,
pinned_to_content_extent_y: bool,
}
const SCROLL_OVERFLOW_NONLEAF_OVERSHOOT_TOLERANCE: f32 = 32.0;
fn record_scroll_overflow_candidate<T: ScrollOverflowTree>(
tree: &T,
axis: crate::element::ScrollAxis,
extent_may_be_stale: bool,
content_size: Size,
node: NodeId,
right: f32,
bottom: f32,
root_loose: &mut Size,
root_trusted: &mut Size,
root_suspicious_nonleaf_x: &mut bool,
root_suspicious_nonleaf_y: &mut bool,
) {
root_loose.width = Px(root_loose.width.0.max(right));
root_loose.height = Px(root_loose.height.0.max(bottom));
let node_has_children = !tree.children_ref(node).is_empty();
let suspicious_x = axis.scroll_x()
&& extent_may_be_stale
&& node_has_children
&& right + 0.5 >= content_size.width.0;
let suspicious_y = axis.scroll_y()
&& extent_may_be_stale
&& node_has_children
&& bottom + 0.5 >= content_size.height.0;
if suspicious_x {
*root_suspicious_nonleaf_x = true;
} else {
root_trusted.width = Px(root_trusted.width.0.max(right));
}
if suspicious_y {
*root_suspicious_nonleaf_y = true;
} else {
root_trusted.height = Px(root_trusted.height.0.max(bottom));
}
}
fn trust_scroll_overflow_nonleaf_axis(
own: f32,
child_frontier: f32,
child_count: usize,
pinned_to_content_extent: bool,
) -> Px {
let own = own.max(0.0);
let child_frontier = child_frontier.max(0.0);
if child_frontier <= 0.0 {
return Px(own);
}
if own + 0.5 < child_frontier {
return Px(child_frontier);
}
if child_count != 1 {
return Px(own.max(child_frontier));
}
if pinned_to_content_extent && child_frontier + 0.5 < own {
return Px(child_frontier);
}
if own <= child_frontier + SCROLL_OVERFLOW_NONLEAF_OVERSHOOT_TOLERANCE {
return Px(own.max(child_frontier));
}
Px(child_frontier)
}
fn validate_scroll_overflow_observed_subtree(
nodes: &HashMap<NodeId, ScrollOverflowObservedNode>,
node: NodeId,
memo: &mut HashMap<NodeId, Size>,
) -> Size {
if let Some(size) = memo.get(&node).copied() {
return size;
}
let Some(info) = nodes.get(&node) else {
return Size::new(Px(0.0), Px(0.0));
};
let mut child_frontier = Size::new(Px(0.0), Px(0.0));
let mut has_child = false;
for &child in &info.children {
let Some(_) = nodes.get(&child) else {
continue;
};
has_child = true;
let validated = validate_scroll_overflow_observed_subtree(nodes, child, memo);
child_frontier.width = Px(child_frontier.width.0.max(validated.width.0));
child_frontier.height = Px(child_frontier.height.0.max(validated.height.0));
}
let trusted = if has_child {
Size::new(
trust_scroll_overflow_nonleaf_axis(
info.extent.width.0,
child_frontier.width.0,
info.children.len(),
info.pinned_to_content_extent_x,
),
trust_scroll_overflow_nonleaf_axis(
info.extent.height.0,
child_frontier.height.0,
info.children.len(),
info.pinned_to_content_extent_y,
),
)
} else {
info.extent
};
memo.insert(node, trusted);
trusted
}
fn observe_scroll_overflow_extents<T: ScrollOverflowTree>(
tree: &mut T,
barrier_roots: &[NodeId],
content_bounds: Rect,
axis: crate::element::ScrollAxis,
content_size: Size,
extent_may_be_stale: bool,
deep_scan_allowed: bool,
) -> (
ScrollOverflowObservedExtents,
UiDebugScrollOverflowObservationTelemetry,
) {
let mut observed_loose = Size::new(Px(0.0), Px(0.0));
let mut observed_trusted = Size::new(Px(0.0), Px(0.0));
const MAX_BARRIER_WRAPPER_CHAIN: usize = 8;
const OVERFLOW_SCAN_BUDGET_NODES: usize = 4096;
let mut wrapper_peeled_max: u8 = 0;
let mut wrapper_peel_budget_hit: bool = false;
let mut immediate_children_visited: u16 = 0;
let mut immediate_children_skipped_absolute: u16 = 0;
let mut deep_scan_enabled: bool = false;
let mut deep_scan_visited: u16 = 0;
let mut deep_scan_budget_hit: bool = false;
let mut deep_scan_skipped_absolute: u16 = 0;
for &barrier_root in barrier_roots {
let mut root_loose = Size::new(Px(0.0), Px(0.0));
let mut root_trusted = Size::new(Px(0.0), Px(0.0));
let mut root_suspicious_nonleaf_x = false;
let mut root_suspicious_nonleaf_y = false;
let mut observe_root = barrier_root;
let mut peeled: u8 = 0;
for _ in 0..MAX_BARRIER_WRAPPER_CHAIN {
let children = tree.children_ref(observe_root);
if children.len() != 1 {
break;
}
let child = children[0];
if tree.node_is_absolute(child) || tree.node_clips_descendant_scroll_overflow(child) {
break;
}
let Some(parent_bounds) = tree.node_bounds(observe_root) else {
break;
};
let Some(child_bounds) = tree.node_bounds(child) else {
break;
};
let same_origin = (parent_bounds.origin.x.0 - child_bounds.origin.x.0).abs() <= 0.5
&& (parent_bounds.origin.y.0 - child_bounds.origin.y.0).abs() <= 0.5;
let same_size = (parent_bounds.size.width.0 - child_bounds.size.width.0).abs() <= 0.5
&& (parent_bounds.size.height.0 - child_bounds.size.height.0).abs() <= 0.5;
if same_origin && same_size {
observe_root = child;
peeled = peeled.saturating_add(1);
continue;
}
break;
}
wrapper_peeled_max = wrapper_peeled_max.max(peeled);
if (peeled as usize) >= MAX_BARRIER_WRAPPER_CHAIN {
wrapper_peel_budget_hit = true;
}
let mut any = false;
if let Some(bounds) = tree.node_bounds(observe_root) {
let same_origin = (bounds.origin.x.0 - content_bounds.origin.x.0).abs() <= 0.5
&& (bounds.origin.y.0 - content_bounds.origin.y.0).abs() <= 0.5;
let same_size = (bounds.size.width.0 - content_bounds.size.width.0).abs() <= 0.5
&& (bounds.size.height.0 - content_bounds.size.height.0).abs() <= 0.5;
if !(same_origin && same_size) {
let right =
(bounds.origin.x.0 + bounds.size.width.0 - content_bounds.origin.x.0).max(0.0);
let bottom =
(bounds.origin.y.0 + bounds.size.height.0 - content_bounds.origin.y.0).max(0.0);
root_loose.width = Px(root_loose.width.0.max(right));
root_loose.height = Px(root_loose.height.0.max(bottom));
root_trusted.width = Px(root_trusted.width.0.max(right));
root_trusted.height = Px(root_trusted.height.0.max(bottom));
}
}
let observe_children: Vec<NodeId> = tree.children_ref(observe_root).to_vec();
for child in observe_children {
immediate_children_visited = immediate_children_visited.saturating_add(1);
if tree.node_is_absolute(child) {
immediate_children_skipped_absolute =
immediate_children_skipped_absolute.saturating_add(1);
continue;
}
let Some(bounds) = tree.node_bounds(child) else {
continue;
};
any = true;
let right =
(bounds.origin.x.0 + bounds.size.width.0 - content_bounds.origin.x.0).max(0.0);
let bottom =
(bounds.origin.y.0 + bounds.size.height.0 - content_bounds.origin.y.0).max(0.0);
record_scroll_overflow_candidate(
&*tree,
axis,
extent_may_be_stale,
content_size,
child,
right,
bottom,
&mut root_loose,
&mut root_trusted,
&mut root_suspicious_nonleaf_x,
&mut root_suspicious_nonleaf_y,
);
}
if !any {
let Some(bounds) = tree.node_bounds(observe_root) else {
continue;
};
let right =
(bounds.origin.x.0 + bounds.size.width.0 - content_bounds.origin.x.0).max(0.0);
let bottom =
(bounds.origin.y.0 + bounds.size.height.0 - content_bounds.origin.y.0).max(0.0);
record_scroll_overflow_candidate(
&*tree,
axis,
extent_may_be_stale,
content_size,
observe_root,
right,
bottom,
&mut root_loose,
&mut root_trusted,
&mut root_suspicious_nonleaf_x,
&mut root_suspicious_nonleaf_y,
);
}
let deep_scan_needed = deep_scan_allowed
&& extent_may_be_stale
&& ((axis.scroll_x()
&& (root_trusted.width.0 <= content_size.width.0 + 0.5
|| root_suspicious_nonleaf_x))
|| (axis.scroll_y()
&& (root_trusted.height.0 <= content_size.height.0 + 0.5
|| root_suspicious_nonleaf_y)));
if deep_scan_needed {
deep_scan_enabled = true;
let mut visited: usize = 0;
let mut stack: Vec<NodeId> = vec![observe_root];
let mut observed_nodes: HashMap<NodeId, ScrollOverflowObservedNode> = HashMap::new();
while let Some(id) = stack.pop() {
visited = visited.saturating_add(1);
if visited > OVERFLOW_SCAN_BUDGET_NODES {
deep_scan_budget_hit = true;
break;
}
deep_scan_visited = deep_scan_visited.max(visited as u16);
if id != observe_root && tree.node_is_absolute(id) {
deep_scan_skipped_absolute = deep_scan_skipped_absolute.saturating_add(1);
continue;
}
let Some(bounds) = tree.node_bounds(id) else {
continue;
};
let right =
(bounds.origin.x.0 + bounds.size.width.0 - content_bounds.origin.x.0).max(0.0);
let bottom =
(bounds.origin.y.0 + bounds.size.height.0 - content_bounds.origin.y.0).max(0.0);
root_loose.width = Px(root_loose.width.0.max(right));
root_loose.height = Px(root_loose.height.0.max(bottom));
let mut children: Vec<NodeId> = Vec::new();
let stop_at_nested_scroll_boundary =
id != observe_root && tree.node_clips_descendant_scroll_overflow(id);
if !stop_at_nested_scroll_boundary {
let child_ids: Vec<NodeId> = tree.children_ref(id).to_vec();
for child in child_ids {
if tree.node_is_absolute(child) {
deep_scan_skipped_absolute =
deep_scan_skipped_absolute.saturating_add(1);
continue;
}
children.push(child);
}
}
for &child in children.iter().rev() {
stack.push(child);
}
observed_nodes.insert(
id,
ScrollOverflowObservedNode {
extent: Size::new(Px(right), Px(bottom)),
children,
pinned_to_content_extent_x: axis.scroll_x()
&& extent_may_be_stale
&& right + 0.5 >= content_size.width.0,
pinned_to_content_extent_y: axis.scroll_y()
&& extent_may_be_stale
&& bottom + 0.5 >= content_size.height.0,
},
);
}
if observed_nodes.get(&observe_root).is_some() {
let mut memo: HashMap<NodeId, Size> = HashMap::new();
root_trusted = validate_scroll_overflow_observed_subtree(
&observed_nodes,
observe_root,
&mut memo,
);
}
}
observed_loose.width = Px(observed_loose.width.0.max(root_loose.width.0));
observed_loose.height = Px(observed_loose.height.0.max(root_loose.height.0));
observed_trusted.width = Px(observed_trusted.width.0.max(root_trusted.width.0));
observed_trusted.height = Px(observed_trusted.height.0.max(root_trusted.height.0));
}
(
ScrollOverflowObservedExtents {
loose: observed_loose,
trusted: observed_trusted,
},
UiDebugScrollOverflowObservationTelemetry {
extent_may_be_stale,
barrier_roots: barrier_roots.len().min(u8::MAX as usize) as u8,
wrapper_peel_budget: MAX_BARRIER_WRAPPER_CHAIN.min(u8::MAX as usize) as u8,
wrapper_peeled_max,
wrapper_peel_budget_hit,
immediate_children_visited,
immediate_children_skipped_absolute,
deep_scan_enabled,
deep_scan_budget_nodes: OVERFLOW_SCAN_BUDGET_NODES.min(u16::MAX as usize) as u16,
deep_scan_visited,
deep_scan_budget_hit,
deep_scan_skipped_absolute,
},
)
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
enum ScrollDeferredUnboundedProbeKind {
#[default]
None,
Invalidation,
Resize,
}
#[derive(Debug, Default, Clone, Copy)]
struct ScrollDeferredUnboundedProbeState {
kind: ScrollDeferredUnboundedProbeKind,
stable_frames: u8,
pending_invalidation_probe: bool,
}
fn non_default_size(size: Size) -> Option<Size> {
(size != Size::default()).then_some(size)
}
fn scroll_measure_child_constraints(
axis: crate::element::ScrollAxis,
probe_unbounded_for_measure: bool,
available: Size,
) -> LayoutConstraints {
LayoutConstraints::new(
LayoutSize::new(None, None),
LayoutSize::new(
if axis.scroll_x() && probe_unbounded_for_measure {
AvailableSpace::MaxContent
} else if probe_unbounded_for_measure && available.width.0 <= 0.0 {
AvailableSpace::MaxContent
} else {
AvailableSpace::Definite(available.width)
},
if axis.scroll_y() && probe_unbounded_for_measure {
AvailableSpace::MaxContent
} else if probe_unbounded_for_measure && available.height.0 <= 0.0 {
AvailableSpace::MaxContent
} else {
AvailableSpace::Definite(available.height)
},
),
)
}
fn scroll_intrinsic_measure_cache_key(
axis: crate::element::ScrollAxis,
child_constraints: LayoutConstraints,
scale_factor: f32,
) -> crate::element::ScrollIntrinsicMeasureCacheKey {
crate::element::ScrollIntrinsicMeasureCacheKey {
avail_w: available_space_cache_key(child_constraints.available.width),
avail_h: available_space_cache_key(child_constraints.available.height),
axis: match axis {
crate::element::ScrollAxis::X => 0,
crate::element::ScrollAxis::Y => 1,
crate::element::ScrollAxis::Both => 2,
},
probe_unbounded: matches!(
child_constraints.available.width,
AvailableSpace::MaxContent
) || matches!(
child_constraints.available.height,
AvailableSpace::MaxContent
),
scale_bits: scale_factor.to_bits(),
}
}
#[derive(Debug, Default, Clone, Copy)]
struct ScrollProbeSeed {
intrinsic_cached_max_child: Option<Size>,
cached_max_child: Option<Size>,
retained_measured_max_child: Option<Size>,
last_max_child: Option<Size>,
}
impl ScrollProbeSeed {
fn can_defer_with_seed(self) -> bool {
self.deferred_max_child().is_some()
}
fn deferred_max_child(self) -> Option<Size> {
self.cached_max_child
.or(self.intrinsic_cached_max_child)
.or(self.last_max_child)
.or(self.retained_measured_max_child)
}
}
fn resolve_retained_measured_max_child<H: UiHost>(cx: &LayoutCx<'_, H>) -> Option<Size> {
let mut max_child = Size::new(Px(0.0), Px(0.0));
let mut found = false;
for &child in cx.children {
if let Some(size) = cx.tree.node_measured_size(child).and_then(non_default_size) {
found = true;
max_child.width = Px(max_child.width.0.max(size.width.0));
max_child.height = Px(max_child.height.0.max(size.height.0));
}
}
found.then_some(max_child)
}
fn resolve_scroll_probe_seed<H: UiHost>(
cx: &LayoutCx<'_, H>,
intrinsic_measure_cache: Option<crate::element::ScrollIntrinsicMeasureCache>,
intrinsic_cache_key: Option<crate::element::ScrollIntrinsicMeasureCacheKey>,
retained_measured_max_child: Option<Size>,
last_max_child: Option<Size>,
at_end_with_invalidated_child: bool,
) -> ScrollProbeSeed {
let mut seed = ScrollProbeSeed {
retained_measured_max_child,
last_max_child,
..Default::default()
};
if let Some(cache_key) = intrinsic_cache_key
&& cx.children.len() == 1
{
let child = cx.children[0];
seed.intrinsic_cached_max_child = intrinsic_measure_cache
.and_then(|cache| (cache.key == cache_key).then_some(cache.max_child))
.and_then(non_default_size);
if !at_end_with_invalidated_child && !cx.tree.node_needs_layout(child) {
seed.cached_max_child = seed.intrinsic_cached_max_child;
}
}
seed
}
#[derive(Debug, Clone, Copy)]
struct ScrollDeferredProbeInputs {
force_probe_now: bool,
wants_unbounded_probe: bool,
can_defer_with_seed: bool,
defer_probe_on_invalidation: bool,
should_defer_on_resize: bool,
should_defer_on_invalidation: bool,
children_layout_invalidated: bool,
at_scroll_extent_edge: bool,
stable_frames_required: u8,
}
fn clear_scroll_deferred_probe_state(state: &mut ScrollDeferredUnboundedProbeState) {
state.kind = ScrollDeferredUnboundedProbeKind::None;
state.stable_frames = 0;
state.pending_invalidation_probe = false;
}
fn update_scroll_deferred_probe_state(
state: &mut ScrollDeferredUnboundedProbeState,
inputs: ScrollDeferredProbeInputs,
) -> bool {
let mut defer_this_frame = false;
if inputs.force_probe_now || !inputs.wants_unbounded_probe || !inputs.can_defer_with_seed {
clear_scroll_deferred_probe_state(state);
return false;
}
if inputs.should_defer_on_resize {
defer_this_frame = true;
state.kind = ScrollDeferredUnboundedProbeKind::Resize;
state.stable_frames = 0;
return defer_this_frame;
}
match state.kind {
ScrollDeferredUnboundedProbeKind::Resize => {
if inputs.stable_frames_required == 0 {
clear_scroll_deferred_probe_state(state);
} else {
state.stable_frames = state.stable_frames.saturating_add(1);
if state.stable_frames < inputs.stable_frames_required {
defer_this_frame = true;
} else {
clear_scroll_deferred_probe_state(state);
}
}
}
ScrollDeferredUnboundedProbeKind::Invalidation => {
if inputs.at_scroll_extent_edge || !inputs.defer_probe_on_invalidation {
clear_scroll_deferred_probe_state(state);
} else if inputs.children_layout_invalidated {
defer_this_frame = true;
state.pending_invalidation_probe = true;
state.stable_frames = 0;
} else if inputs.stable_frames_required == 0 {
clear_scroll_deferred_probe_state(state);
} else {
state.stable_frames = state.stable_frames.saturating_add(1);
if state.stable_frames < inputs.stable_frames_required {
defer_this_frame = true;
} else {
clear_scroll_deferred_probe_state(state);
}
}
}
ScrollDeferredUnboundedProbeKind::None => {
if inputs.should_defer_on_invalidation {
defer_this_frame = true;
state.kind = ScrollDeferredUnboundedProbeKind::Invalidation;
state.pending_invalidation_probe = true;
state.stable_frames = 0;
}
}
}
defer_this_frame
}
#[derive(Debug, Clone, Copy)]
struct ScrollAuthoritativeExtentCommit {
max_child: Size,
intrinsic_cache_key: Option<crate::element::ScrollIntrinsicMeasureCacheKey>,
probe_cache_key: Option<(FrameId, ScrollLayoutProbeKey)>,
clear_pending_invalidation_probe: bool,
clear_pending_extent_probe: bool,
}
fn commit_scroll_authoritative_extent<H: UiHost>(
app: &mut H,
window: AppWindowId,
element: GlobalElementId,
commit: ScrollAuthoritativeExtentCommit,
) {
crate::elements::with_element_state(
app,
window,
element,
ScrollLayoutProbeCacheState::default,
|state| {
if let Some((frame_id, key)) = commit.probe_cache_key {
if state.frame_id != frame_id {
state.frame_id = frame_id;
state.entries.clear();
}
state.entries.push((key, commit.max_child));
}
state.last_max_child = commit.max_child;
},
);
if commit.clear_pending_invalidation_probe {
crate::elements::with_element_state(
app,
window,
element,
ScrollDeferredUnboundedProbeState::default,
clear_scroll_deferred_probe_state,
);
}
crate::elements::with_element_state(
app,
window,
element,
crate::element::ScrollState::default,
|state| {
if commit.clear_pending_extent_probe {
state.pending_extent_probe = false;
}
state.intrinsic_measure_cache =
commit
.intrinsic_cache_key
.map(|key| crate::element::ScrollIntrinsicMeasureCache {
key,
max_child: commit.max_child,
});
},
);
}
impl ElementHostWidget {
pub(super) fn layout_virtual_list_impl<H: UiHost>(
&mut self,
cx: &mut LayoutCx<'_, H>,
window: AppWindowId,
props: crate::element::VirtualListProps,
) -> Size {
let axis = props.axis;
let (
mut metrics,
prev_items_revision,
render_window_range,
prev_window_range,
prev_offset_x,
prev_offset_y,
prev_viewport_w,
prev_viewport_h,
mut layout_scratch,
) = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::VirtualListState::default,
|state| {
state.metrics.ensure_with_mode(
props.measure_mode,
props.len,
props.estimate_row_height,
props.gap,
props.scroll_margin,
);
(
std::mem::take(&mut state.metrics),
state.items_revision,
state.render_window_range,
state.window_range,
state.offset_x,
state.offset_y,
state.viewport_w,
state.viewport_h,
std::mem::take(&mut state.layout_scratch),
)
},
);
let content_extent = metrics.total_height();
let should_remeasure_visible_items = props.items_revision != prev_items_revision;
let desired_w = match props.layout.size.width {
Length::Px(px) => Px(px.0.max(0.0)),
Length::Fill => cx.available.width,
Length::Fraction(f) => {
let f = if f.is_finite() { f.max(0.0) } else { 0.0 };
Px((cx.available.width.0 * f).max(0.0))
}
Length::Auto => match axis {
fret_core::Axis::Vertical => cx.available.width,
fret_core::Axis::Horizontal => {
Px(content_extent.0.min(cx.available.width.0.max(0.0)))
}
},
};
let desired_h = match props.layout.size.height {
Length::Px(px) => Px(px.0.max(0.0)),
Length::Fill => cx.available.height,
Length::Fraction(f) => {
let f = if f.is_finite() { f.max(0.0) } else { 0.0 };
Px((cx.available.height.0 * f).max(0.0))
}
Length::Auto => match axis {
fret_core::Axis::Vertical => {
Px(content_extent.0.min(cx.available.height.0.max(0.0)))
}
fret_core::Axis::Horizontal => cx.available.height,
},
};
let size =
clamp_to_constraints(Size::new(desired_w, desired_h), props.layout, cx.available);
let viewport = match axis {
fret_core::Axis::Vertical => Px(size.height.0.max(0.0)),
fret_core::Axis::Horizontal => Px(size.width.0.max(0.0)),
};
let mut needs_redraw = false;
let cross_extent = match axis {
fret_core::Axis::Vertical => size.width,
fret_core::Axis::Horizontal => size.height,
};
if metrics.reset_measured_cache_if_cross_extent_changed(cross_extent) {
needs_redraw = true;
}
props.scroll_handle.set_items_count(props.len);
self.scroll_child_transform = Some(super::super::ScrollChildTransform {
handle: props.scroll_handle.base_handle().clone(),
axis: match axis {
fret_core::Axis::Vertical => crate::element::ScrollAxis::Y,
fret_core::Axis::Horizontal => crate::element::ScrollAxis::X,
},
});
let handle_offset = props.scroll_handle.offset();
let handle_offset_axis = match axis {
fret_core::Axis::Vertical => handle_offset.y,
fret_core::Axis::Horizontal => handle_offset.x,
};
let mut offset = metrics.clamp_offset(handle_offset_axis, viewport);
let deferred_scroll_to_item = props.scroll_handle.deferred_scroll_to_item().is_some();
let mut deferred_scroll_consumed = false;
let is_probe_layout = cx.pass_kind == crate::layout_pass::LayoutPassKind::Probe;
if !is_probe_layout
&& viewport.0 > 0.0
&& props.len > 0
&& let Some((index, strategy)) = props.scroll_handle.deferred_scroll_to_item()
{
deferred_scroll_consumed = true;
offset = metrics.scroll_offset_for_item(index, viewport, offset, strategy);
props
.scroll_handle
.clear_deferred_scroll_to_item(cx.app.frame_id());
}
offset = metrics.clamp_offset(offset, viewport);
if (handle_offset_axis.0 - offset.0).abs() > 0.01 {
needs_redraw = true;
}
let visible_range = metrics.visible_range(offset, viewport, 0);
let anchor = visible_range.map(|r| r.start_index);
let anchor_offset_in_viewport = anchor.map(|anchor| {
let start = metrics.offset_for_index(anchor);
Px((offset.0 - start.0).max(0.0))
});
layout_scratch.measured_updates.clear();
layout_scratch.measured_updates.reserve(cx.children.len());
match props.measure_mode {
crate::element::VirtualListMeasureMode::Measured => {
let item_constraints = LayoutConstraints::new(
LayoutSize::new(
match axis {
fret_core::Axis::Vertical => Some(size.width),
fret_core::Axis::Horizontal => None,
},
match axis {
fret_core::Axis::Vertical => None,
fret_core::Axis::Horizontal => Some(size.height),
},
),
LayoutSize::new(
match axis {
fret_core::Axis::Vertical => AvailableSpace::Definite(size.width),
fret_core::Axis::Horizontal => AvailableSpace::MaxContent,
},
match axis {
fret_core::Axis::Vertical => AvailableSpace::MaxContent,
fret_core::Axis::Horizontal => AvailableSpace::Definite(size.height),
},
),
);
for (&child, item) in cx.children.iter().zip(props.visible_items.iter()) {
let idx = item.index;
let should_measure =
should_remeasure_visible_items || !metrics.is_measured(idx);
let measured_extent = if should_measure {
#[cfg(test)]
crate::virtual_list::debug_record_virtual_list_item_measure();
let measured = cx.measure_in(child, item_constraints);
match axis {
fret_core::Axis::Vertical => Px(measured.height.0.max(0.0)),
fret_core::Axis::Horizontal => Px(measured.width.0.max(0.0)),
}
} else {
metrics.height_at(idx)
};
layout_scratch
.measured_updates
.push((child, idx, measured_extent));
}
let mut any_measured_change = false;
for (_, idx, measured_extent) in &layout_scratch.measured_updates {
if metrics.set_measured_height(*idx, *measured_extent) {
any_measured_change = true;
}
}
if any_measured_change || should_remeasure_visible_items {
needs_redraw = true;
if !is_probe_layout
&& let (Some(anchor), Some(anchor_offset_in_viewport)) =
(anchor, anchor_offset_in_viewport)
{
let prev_offset = offset;
let desired =
Px(metrics.offset_for_index(anchor).0 + anchor_offset_in_viewport.0);
offset = metrics.clamp_offset(desired, viewport);
if (prev_offset.0 - offset.0).abs() > 0.01 {
needs_redraw = true;
}
}
}
}
crate::element::VirtualListMeasureMode::Fixed => {
for (&child, item) in cx.children.iter().zip(props.visible_items.iter()) {
let idx = item.index;
let estimated_extent = metrics.height_at(idx);
layout_scratch
.measured_updates
.push((child, idx, estimated_extent));
}
}
crate::element::VirtualListMeasureMode::Known => {
for (&child, item) in cx.children.iter().zip(props.visible_items.iter()) {
let idx = item.index;
let known_extent = metrics.height_at(idx);
layout_scratch
.measured_updates
.push((child, idx, known_extent));
}
}
}
let content_extent = metrics.total_height();
props
.scroll_handle
.set_viewport_size_internal(Size::new(size.width, size.height));
let content_size = match axis {
fret_core::Axis::Vertical => Size::new(size.width, content_extent),
fret_core::Axis::Horizontal => Size::new(content_extent, size.height),
};
props.scroll_handle.set_content_size_internal(content_size);
let prev_offset = props.scroll_handle.offset();
let clamped = metrics.clamp_offset(offset, viewport);
if (clamped.0 - offset.0).abs() > 0.01 {
needs_redraw = true;
}
match axis {
fret_core::Axis::Vertical => {
props
.scroll_handle
.set_offset_internal(fret_core::Point::new(prev_offset.x, clamped));
}
fret_core::Axis::Horizontal => {
props
.scroll_handle
.set_offset_internal(fret_core::Point::new(clamped, prev_offset.y));
}
}
offset = clamped;
self.scroll_child_transform = Some(super::super::ScrollChildTransform {
handle: props.scroll_handle.base_handle().clone(),
axis: match axis {
fret_core::Axis::Vertical => crate::element::ScrollAxis::Y,
fret_core::Axis::Horizontal => crate::element::ScrollAxis::X,
},
});
layout_scratch.barrier_roots.clear();
layout_scratch
.barrier_roots
.reserve(layout_scratch.measured_updates.len());
let mut should_defer_overscan_layout = false;
if !is_probe_layout && props.overscan > 0 && viewport.0 > 0.0 {
if deferred_scroll_consumed {
should_defer_overscan_layout = true;
} else {
let prev_state_viewport_axis = match axis {
fret_core::Axis::Vertical => prev_viewport_h,
fret_core::Axis::Horizontal => prev_viewport_w,
};
let prev_state_offset_axis = match axis {
fret_core::Axis::Vertical => prev_offset_y,
fret_core::Axis::Horizontal => prev_offset_x,
};
let viewport_unchanged = (prev_state_viewport_axis.0 - viewport.0).abs() <= 0.01
&& prev_state_viewport_axis.0 > 0.0;
if viewport_unchanged {
let prev_clamped = metrics.clamp_offset(prev_state_offset_axis, viewport);
let prev_visible = metrics.visible_range(prev_clamped, viewport, 0);
let large_index_jump = match (prev_visible, visible_range) {
(Some(prev), Some(now)) => {
let prev_len = prev
.end_index
.saturating_sub(prev.start_index)
.saturating_add(1);
let threshold = prev_len
.saturating_mul(4)
.max(props.overscan.saturating_mul(8));
now.start_index.abs_diff(prev.start_index) > threshold
}
_ => {
let delta_px = (offset.0 - prev_clamped.0).abs();
delta_px > (viewport.0 * 3.0)
}
};
if large_index_jump {
should_defer_overscan_layout = true;
}
}
}
}
let bounds_for_start_and_extent = |start: Px, extent: Px| -> Rect {
let origin = match axis {
fret_core::Axis::Vertical => {
let y = cx.bounds.origin.y.0 + start.0;
fret_core::Point::new(cx.bounds.origin.x, Px(y))
}
fret_core::Axis::Horizontal => {
let x = cx.bounds.origin.x.0 + start.0;
fret_core::Point::new(Px(x), cx.bounds.origin.y)
}
};
match axis {
fret_core::Axis::Vertical => Rect::new(origin, Size::new(size.width, extent)),
fret_core::Axis::Horizontal => Rect::new(origin, Size::new(extent, size.height)),
}
};
let use_visible_item_starts = props.measure_mode
!= crate::element::VirtualListMeasureMode::Measured
&& layout_scratch.measured_updates.len() == props.visible_items.len();
let mut prev_idx: Option<usize> = None;
let mut prev_start: Px = Px(0.0);
let mut prev_extent: Px = Px(0.0);
let gap = metrics.gap();
for (pos, (child, idx, measured_extent)) in
layout_scratch.measured_updates.iter().enumerate()
{
let start = if use_visible_item_starts {
props
.visible_items
.get(pos)
.map(|item| item.start)
.unwrap_or_else(|| metrics.offset_for_index(*idx))
} else {
let start = if let Some(prev) = prev_idx
&& *idx == prev.saturating_add(1)
{
Px(prev_start.0 + prev_extent.0 + gap.0)
} else {
metrics.offset_for_index(*idx)
};
prev_idx = Some(*idx);
prev_start = start;
prev_extent = *measured_extent;
start
};
if should_defer_overscan_layout {
let Some(visible) = visible_range else {
continue;
};
if *idx < visible.start_index || *idx > visible.end_index {
continue;
}
}
let child_bounds = bounds_for_start_and_extent(start, *measured_extent);
layout_scratch.barrier_roots.push((*child, child_bounds));
}
if !is_probe_layout {
cx.solve_barrier_child_roots_if_needed(&layout_scratch.barrier_roots);
}
for (child, child_bounds) in &layout_scratch.barrier_roots {
let _ = cx.layout_in(*child, *child_bounds);
}
let window_range = if !is_probe_layout {
metrics.visible_range(offset, viewport, props.overscan)
} else {
None
};
crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::VirtualListState::default,
|state| {
if !is_probe_layout {
match axis {
fret_core::Axis::Vertical => {
state.offset_y = offset;
if state.viewport_h != viewport {
state.viewport_h = viewport;
needs_redraw = true;
}
}
fret_core::Axis::Horizontal => {
state.offset_x = offset;
if state.viewport_w != viewport {
state.viewport_w = viewport;
needs_redraw = true;
}
}
}
if viewport.0 > 0.0 {
state.has_final_viewport = true;
}
state.window_range = window_range;
state.deferred_scroll_offset_hint = None;
}
state.items_revision = props.items_revision;
state.metrics = metrics;
state.layout_scratch = layout_scratch;
},
);
let window_mismatch = {
if is_probe_layout || viewport.0 <= 0.0 {
false
} else if let Some(visible) = visible_range {
match render_window_range {
None => visible.count > 0,
Some(rendered) => {
if rendered.count == 0 {
visible.count > 0
} else {
let rendered_start =
rendered.start_index.saturating_sub(rendered.overscan);
let rendered_end = (rendered.end_index + rendered.overscan)
.min(rendered.count.saturating_sub(1));
visible.start_index < rendered_start || visible.end_index > rendered_end
}
}
}
} else {
false
}
};
if cx.tree.debug_enabled() {
let scroll_to_item_consumed_in_frame = props
.scroll_handle
.scroll_to_item_consumed_in_frame(cx.app.frame_id());
let scroll_to_item_in_frame =
deferred_scroll_to_item || scroll_to_item_consumed_in_frame;
let policy_key = {
let mut b = CacheKeyBuilder::new();
b.write_u32(axis as u32);
b.write_u32(props.measure_mode as u32);
b.write_u64(props.overscan as u64);
b.write_px(props.estimate_row_height);
b.write_px(props.gap);
b.write_px(props.scroll_margin);
b.finish()
};
let inputs_key = {
let mut b = CacheKeyBuilder::new();
b.write_u64(policy_key);
b.write_u64(props.len as u64);
b.write_u64(props.items_revision);
b.write_px(viewport);
b.write_px(offset);
b.write_px(content_extent);
b.finish()
};
let prev_offset_state = match axis {
fret_core::Axis::Vertical => prev_offset_y,
fret_core::Axis::Horizontal => prev_offset_x,
};
let prev_viewport_state = match axis {
fret_core::Axis::Vertical => prev_viewport_h,
fret_core::Axis::Horizontal => prev_viewport_w,
};
let (window_shift_reason, window_shift_apply_mode, window_shift_invalidation_detail) =
if window_mismatch {
let reason = if scroll_to_item_in_frame {
crate::tree::UiDebugVirtualListWindowShiftReason::ScrollToItem
} else if props.items_revision != prev_items_revision {
crate::tree::UiDebugVirtualListWindowShiftReason::ItemsRevision
} else if (viewport.0 - prev_viewport_state.0).abs() > 0.01 {
crate::tree::UiDebugVirtualListWindowShiftReason::ViewportResize
} else if (offset.0 - prev_offset_state.0).abs() > 0.01 {
crate::tree::UiDebugVirtualListWindowShiftReason::ScrollOffset
} else if prev_window_range.map(|r| (r.count, r.overscan))
!= window_range.map(|r| (r.count, r.overscan))
{
crate::tree::UiDebugVirtualListWindowShiftReason::InputsChange
} else {
crate::tree::UiDebugVirtualListWindowShiftReason::Unknown
};
let retained_host = crate::elements::with_window_state(
&mut *cx.app,
window,
|window_state| {
window_state.has_state::<crate::windowed_surface_host::RetainedVirtualListHostMarker>(self.element)
},
);
let mode = if retained_host {
crate::tree::UiDebugVirtualListWindowShiftApplyMode::RetainedReconcile
} else {
crate::tree::UiDebugVirtualListWindowShiftApplyMode::NonRetainedRerender
};
let invalidation_detail = if cx.tree.view_cache_enabled() && !retained_host {
Some(match reason {
crate::tree::UiDebugVirtualListWindowShiftReason::ScrollToItem => {
crate::tree::UiDebugInvalidationDetail::ScrollHandleScrollToItemWindowUpdate
}
crate::tree::UiDebugVirtualListWindowShiftReason::ViewportResize => {
crate::tree::UiDebugInvalidationDetail::ScrollHandleViewportResizeWindowUpdate
}
crate::tree::UiDebugVirtualListWindowShiftReason::ItemsRevision => {
crate::tree::UiDebugInvalidationDetail::ScrollHandleItemsRevisionWindowUpdate
}
_ => crate::tree::UiDebugInvalidationDetail::ScrollHandleWindowUpdate,
})
} else {
None
};
(Some(reason), Some(mode), invalidation_detail)
} else {
(None, None, None)
};
cx.tree
.debug_record_virtual_list_window(crate::tree::UiDebugVirtualListWindow {
source: crate::tree::UiDebugVirtualListWindowSource::Layout,
node: cx.node,
element: self.element,
axis,
is_probe_layout,
items_len: props.len,
items_revision: props.items_revision,
prev_items_revision,
measure_mode: props.measure_mode,
overscan: props.overscan,
estimate_row_height: props.estimate_row_height,
gap: props.gap,
scroll_margin: props.scroll_margin,
viewport,
prev_viewport: prev_viewport_state,
offset,
prev_offset: prev_offset_state,
content_extent,
policy_key,
inputs_key,
window_range,
prev_window_range,
render_window_range,
deferred_scroll_to_item: scroll_to_item_in_frame,
deferred_scroll_consumed: deferred_scroll_consumed
|| scroll_to_item_consumed_in_frame,
window_mismatch,
window_shift_kind: if window_mismatch {
crate::tree::UiDebugVirtualListWindowShiftKind::Escape
} else {
crate::tree::UiDebugVirtualListWindowShiftKind::None
},
window_shift_reason,
window_shift_apply_mode,
window_shift_invalidation_detail,
});
}
if !is_probe_layout && cx.tree.view_cache_enabled() && window_mismatch {
needs_redraw = true;
}
if needs_redraw && let Some(window) = cx.window {
cx.app.request_redraw(window);
}
size
}
pub(super) fn layout_scroll_impl<H: UiHost>(
&mut self,
cx: &mut LayoutCx<'_, H>,
window: AppWindowId,
props: crate::element::ScrollProps,
) -> Size {
let profile_cfg = scroll_layout_profile_config();
let profile_started = profile_cfg.is_some().then(Instant::now);
let mut t_measure_children: Duration = Duration::default();
let mut t_solve_barrier: Duration = Duration::default();
let mut t_layout_children: Duration = Duration::default();
let is_probe_layout = cx.pass_kind == crate::layout_pass::LayoutPassKind::Probe;
let post_layout_extents_mode = !is_probe_layout
&& props.probe_unbounded
&& matches!(props.axis, crate::element::ScrollAxis::Y)
&& cx.available.width.0 > 0.0
&& cx.available.height.0 > 0.0
&& !matches!(props.layout.size.height, Length::Auto);
let external_handle = props.scroll_handle.clone();
let (handle, intrinsic_measure_cache, pending_extent_probe) =
crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
crate::element::ScrollState::default,
|state| {
(
external_handle
.as_ref()
.unwrap_or(&state.scroll_handle)
.clone(),
state.intrinsic_measure_cache,
state.pending_extent_probe,
)
},
);
let available = if is_probe_layout {
let last = handle.viewport_size();
if last.width.0 > 0.0 && last.height.0 > 0.0 {
last
} else {
cx.available
}
} else {
cx.available
};
let prev_offset = handle.offset();
let prev_max_offset = handle.max_offset();
let at_scroll_extent_edge = match props.axis {
crate::element::ScrollAxis::X => prev_offset.x.0 + 0.5 >= prev_max_offset.x.0,
crate::element::ScrollAxis::Y => prev_offset.y.0 + 0.5 >= prev_max_offset.y.0,
crate::element::ScrollAxis::Both => {
prev_offset.x.0 + 0.5 >= prev_max_offset.x.0
|| prev_offset.y.0 + 0.5 >= prev_max_offset.y.0
}
};
let direct_children_layout_invalidated = cx
.children
.iter()
.copied()
.any(|child| cx.tree.node_layout_invalidated(child));
let descendant_subtree_layout_dirty = (at_scroll_extent_edge || post_layout_extents_mode)
&& cx
.children
.iter()
.copied()
.any(|child| cx.tree.node_subtree_layout_dirty(child));
let children_layout_invalidated =
direct_children_layout_invalidated || descendant_subtree_layout_dirty;
let forced_barrier_child_roots: Vec<NodeId> = if post_layout_extents_mode {
cx.children
.iter()
.copied()
.filter(|&child| {
cx.tree.node_subtree_layout_dirty(child)
&& !cx.tree.node_layout_invalidated(child)
})
.collect()
} else {
Vec::new()
};
let force_barrier_child_root_relayout = !forced_barrier_child_roots.is_empty();
let at_end_with_invalidated_child = at_scroll_extent_edge && children_layout_invalidated;
let frame_id = cx.app.frame_id();
let retained_measured_max_child = resolve_retained_measured_max_child(cx);
let last_max_child = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
ScrollLayoutProbeCacheState::default,
|state| {
if state.frame_id != frame_id {
state.frame_id = frame_id;
state.entries.clear();
}
non_default_size(state.last_max_child)
},
);
let wants_unbounded_probe = props.probe_unbounded
&& (props.axis.scroll_x() || props.axis.scroll_y())
&& !post_layout_extents_mode
&& !is_probe_layout;
let defer_probe_on_resize = scroll_defer_unbounded_probe_on_resize_enabled();
let defer_probe_on_invalidation = scroll_defer_unbounded_probe_on_invalidation_enabled();
let prev_viewport = handle.viewport_size();
let viewport_known = prev_viewport.width.0 > 0.0 && prev_viewport.height.0 > 0.0;
let viewport_changed = viewport_known
&& (prev_viewport.width.0.to_bits() != available.width.0.to_bits()
|| prev_viewport.height.0.to_bits() != available.height.0.to_bits());
let deferred_child_constraints =
scroll_measure_child_constraints(props.axis, true, available);
let deferred_seed_cache_key =
(!is_probe_layout && cx.children.len() == 1 && wants_unbounded_probe).then(|| {
scroll_intrinsic_measure_cache_key(
props.axis,
deferred_child_constraints,
cx.scale_factor,
)
});
let deferred_probe_seed = resolve_scroll_probe_seed(
cx,
intrinsic_measure_cache,
deferred_seed_cache_key,
retained_measured_max_child,
last_max_child,
at_end_with_invalidated_child,
);
let can_defer_probe_with_cached_children = deferred_probe_seed.can_defer_with_seed();
let viewport_became_known_during_resize = !viewport_known
&& cx.tree.interactive_resize_active()
&& available.width.0 > 0.0
&& available.height.0 > 0.0
&& can_defer_probe_with_cached_children;
let should_defer_unbounded_probe_on_resize = wants_unbounded_probe
&& defer_probe_on_resize
&& can_defer_probe_with_cached_children
&& !pending_extent_probe
&& (viewport_changed || viewport_became_known_during_resize);
let should_defer_unbounded_probe_on_invalidation = wants_unbounded_probe
&& defer_probe_on_invalidation
&& can_defer_probe_with_cached_children
&& !pending_extent_probe
&& children_layout_invalidated
&& !at_scroll_extent_edge;
let stable_frames_required = scroll_defer_unbounded_probe_stable_frames();
let (defer_state, defer_this_frame) = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
ScrollDeferredUnboundedProbeState::default,
|state| {
let defer_this_frame = update_scroll_deferred_probe_state(
state,
ScrollDeferredProbeInputs {
force_probe_now: pending_extent_probe,
wants_unbounded_probe,
can_defer_with_seed: can_defer_probe_with_cached_children,
defer_probe_on_invalidation,
should_defer_on_resize: should_defer_unbounded_probe_on_resize,
should_defer_on_invalidation: should_defer_unbounded_probe_on_invalidation,
children_layout_invalidated,
at_scroll_extent_edge,
stable_frames_required,
},
);
(*state, defer_this_frame)
},
);
if defer_this_frame {
let schedule_follow_up = match defer_state.kind {
ScrollDeferredUnboundedProbeKind::Invalidation => true,
ScrollDeferredUnboundedProbeKind::Resize => !viewport_changed,
ScrollDeferredUnboundedProbeKind::None => false,
};
if schedule_follow_up {
cx.tree.schedule_barrier_relayout_with_source_and_detail(
cx.node,
UiDebugInvalidationSource::Other,
UiDebugInvalidationDetail::ScrollDeferredProbe,
);
cx.request_redraw();
}
}
let must_probe_for_growing_extent = pending_extent_probe
|| (at_scroll_extent_edge
&& (children_layout_invalidated || defer_state.pending_invalidation_probe));
let probe_unbounded_for_measure =
props.probe_unbounded && (!post_layout_extents_mode || must_probe_for_growing_extent);
let child_constraints =
scroll_measure_child_constraints(props.axis, probe_unbounded_for_measure, available);
let intrinsic_cache_key = (!is_probe_layout && cx.children.len() == 1).then(|| {
scroll_intrinsic_measure_cache_key(props.axis, child_constraints, cx.scale_factor)
});
let probe_seed = resolve_scroll_probe_seed(
cx,
intrinsic_measure_cache,
intrinsic_cache_key,
retained_measured_max_child,
last_max_child,
at_end_with_invalidated_child,
);
let intrinsic_cached_max_child = probe_seed.intrinsic_cached_max_child;
let mut cached_max_child = probe_seed.cached_max_child;
if must_probe_for_growing_extent {
cached_max_child = None;
}
let key = ScrollLayoutProbeKey {
avail_w: available_space_cache_key(child_constraints.available.width),
avail_h: available_space_cache_key(child_constraints.available.height),
};
let cached = crate::elements::with_element_state(
&mut *cx.app,
window,
self.element,
ScrollLayoutProbeCacheState::default,
|state| {
if state.frame_id != frame_id {
state.frame_id = frame_id;
state.entries.clear();
}
let cached = state
.entries
.iter()
.find_map(|(k, v)| (*k == key).then_some(*v));
if let Some(cached) = cached {
state.last_max_child = cached;
}
cached
},
);
let mut needs_authoritative_cache_commit_from_same_frame_probe = false;
let max_child = if must_probe_for_growing_extent {
let measure_started = profile_cfg.is_some().then(Instant::now);
let mut max_child = Size::new(Px(0.0), Px(0.0));
for &child in cx.children {
let child_size = cx.measure_in(child, child_constraints);
max_child.width = Px(max_child.width.0.max(child_size.width.0));
max_child.height = Px(max_child.height.0.max(child_size.height.0));
}
if let Some(started) = measure_started {
t_measure_children = started.elapsed();
}
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child,
intrinsic_cache_key,
probe_cache_key: Some((frame_id, key)),
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: true,
},
);
max_child
} else if let Some(cached) = cached_max_child {
cached
} else if let Some(cached) = cached {
needs_authoritative_cache_commit_from_same_frame_probe = true;
cached
} else if defer_this_frame {
if let Some(max_child) = deferred_probe_seed.deferred_max_child() {
max_child
} else {
debug_assert!(
false,
"deferred scroll probe frame must have a retained seed before skipping measure"
);
let measure_started = profile_cfg.is_some().then(Instant::now);
let mut max_child = Size::new(Px(0.0), Px(0.0));
for &child in cx.children {
let child_size = cx.measure_in(child, child_constraints);
max_child.width = Px(max_child.width.0.max(child_size.width.0));
max_child.height = Px(max_child.height.0.max(child_size.height.0));
}
if let Some(started) = measure_started {
t_measure_children = started.elapsed();
}
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child,
intrinsic_cache_key,
probe_cache_key: Some((frame_id, key)),
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: false,
},
);
max_child
}
} else {
let measure_started = profile_cfg.is_some().then(Instant::now);
let mut max_child = Size::new(Px(0.0), Px(0.0));
for &child in cx.children {
let child_size = cx.measure_in(child, child_constraints);
max_child.width = Px(max_child.width.0.max(child_size.width.0));
max_child.height = Px(max_child.height.0.max(child_size.height.0));
}
if let Some(started) = measure_started {
t_measure_children = started.elapsed();
}
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child,
intrinsic_cache_key,
probe_cache_key: Some((frame_id, key)),
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: false,
},
);
max_child
};
let mut clamp_available = available;
if probe_unbounded_for_measure {
if clamp_available.width.0 <= 0.0 {
clamp_available.width = Px(max_child.width.0.max(0.0));
}
if clamp_available.height.0 <= 0.0 {
clamp_available.height = Px(max_child.height.0.max(0.0));
}
}
let desired = clamp_to_constraints(max_child, props.layout, clamp_available);
const ROUND_EPSILON: f32 = 0.001;
let previous_content = handle.content_size();
let trust_live_edge_probe_shrink =
pending_extent_probe || (cx.children.len() == 1 && direct_children_layout_invalidated);
let content_w = if props.axis.scroll_x() && probe_unbounded_for_measure {
let measured = Px((max_child.width.0.max(0.0) - ROUND_EPSILON).ceil().max(0.0));
if post_layout_extents_mode {
if trust_live_edge_probe_shrink {
Px(measured.0.max(desired.width.0.max(0.0)))
} else {
Px(previous_content
.width
.0
.max(measured.0.max(desired.width.0.max(0.0))))
}
} else {
measured
}
} else if post_layout_extents_mode && props.axis.scroll_x() {
Px(previous_content.width.0.max(desired.width.0.max(0.0)))
} else {
desired.width
};
let content_h = if props.axis.scroll_y() && probe_unbounded_for_measure {
let measured = Px((max_child.height.0.max(0.0) - ROUND_EPSILON)
.ceil()
.max(0.0));
if post_layout_extents_mode {
if trust_live_edge_probe_shrink {
Px(measured.0.max(desired.height.0.max(0.0)))
} else {
Px(previous_content
.height
.0
.max(measured.0.max(desired.height.0.max(0.0))))
}
} else {
measured
}
} else if post_layout_extents_mode && props.axis.scroll_y() {
Px(previous_content.height.0.max(desired.height.0.max(0.0)))
} else {
desired.height
};
let mut content_w = Px(content_w.0.max(desired.width.0.max(0.0)));
let mut content_h = Px(content_h.0.max(desired.height.0.max(0.0)));
if crate::runtime_config::ui_runtime_config().debug_scroll_extent_probe
&& !is_probe_layout
&& probe_unbounded_for_measure
&& ((props.axis.scroll_x() && content_w.0 + 0.5 < previous_content.width.0)
|| (props.axis.scroll_y() && content_h.0 + 0.5 < previous_content.height.0))
{
eprintln!(
"scroll extent measured smaller element={:?} node={:?} axis={:?} previous=({:.1},{:.1}) measured=({:.1},{:.1}) max_child=({:.1},{:.1}) desired=({:.1},{:.1}) cached_probe={} cached_intrinsic={} cached_max_child={} direct_invalidated={} descendant_dirty={} at_edge={} defer_this_frame={} pending_probe={}",
self.element,
cx.node,
props.axis,
previous_content.width.0,
previous_content.height.0,
content_w.0,
content_h.0,
max_child.width.0,
max_child.height.0,
desired.width.0,
desired.height.0,
cached.is_some(),
intrinsic_cached_max_child.is_some(),
cached_max_child.is_some(),
direct_children_layout_invalidated,
descendant_subtree_layout_dirty,
at_scroll_extent_edge,
defer_this_frame,
pending_extent_probe,
);
}
let debug_test_id: Option<Arc<str>> = if cx.tree.debug_enabled() {
let mut current = Some(cx.node);
let mut steps: u8 = 0;
let mut found: Option<Arc<str>> = None;
while let Some(node) = current {
if let Some(record) =
crate::declarative::element_record_for_node(cx.app, window, node)
&& let Some(decoration) = record.semantics_decoration.as_ref()
&& let Some(test_id) = decoration.test_id.as_ref()
{
found = Some(test_id.clone());
break;
}
current = cx.tree.node_parent(node);
steps = steps.saturating_add(1);
if steps >= 48 {
break;
}
}
found
} else {
None
};
if !is_probe_layout {
handle.set_viewport_size_internal(desired);
handle.set_content_size_internal(Size::new(content_w, content_h));
let prev = handle.offset();
handle.set_offset_internal(prev);
cx.tree
.debug_record_scroll_node_telemetry(UiDebugScrollNodeTelemetry {
node: cx.node,
element: Some(self.element),
test_id: debug_test_id.clone(),
axis: match props.axis {
crate::element::ScrollAxis::X => UiDebugScrollAxis::X,
crate::element::ScrollAxis::Y => UiDebugScrollAxis::Y,
crate::element::ScrollAxis::Both => UiDebugScrollAxis::Both,
},
offset: handle.offset(),
viewport: handle.viewport_size(),
content: handle.content_size(),
observed_extent: None,
overflow_observation: None,
});
if needs_authoritative_cache_commit_from_same_frame_probe {
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child,
intrinsic_cache_key,
probe_cache_key: None,
clear_pending_invalidation_probe: false,
clear_pending_extent_probe: false,
},
);
}
}
self.scroll_child_transform = Some(super::super::ScrollChildTransform {
handle: handle.clone(),
axis: props.axis,
});
let mut content_bounds = Rect::new(cx.bounds.origin, Size::new(content_w, content_h));
let overflow_ctx = if post_layout_extents_mode {
let mut ctx = cx.overflow_ctx;
if props.axis.scroll_x() {
ctx.probe_available_override.width = Some(AvailableSpace::MaxContent);
ctx.allow_overflow_on_auto.width = true;
}
if props.axis.scroll_y() {
ctx.probe_available_override.height = Some(AvailableSpace::MaxContent);
ctx.allow_overflow_on_auto.height = true;
}
ctx
} else {
cx.overflow_ctx
};
cx.with_overflow_context(overflow_ctx, |cx| {
if !is_probe_layout {
if force_barrier_child_root_relayout {
for &child in &forced_barrier_child_roots {
cx.tree.invalidate(child, Invalidation::Layout);
}
}
let solve_started = profile_cfg.is_some().then(Instant::now);
match cx.children {
[child] => {
cx.solve_barrier_child_root_if_needed(*child, content_bounds);
}
children => {
let roots: Vec<(NodeId, Rect)> =
children.iter().map(|&c| (c, content_bounds)).collect();
cx.solve_barrier_child_roots_if_needed(&roots);
}
}
if let Some(started) = solve_started {
t_solve_barrier = started.elapsed();
}
}
let layout_started = profile_cfg.is_some().then(Instant::now);
for &child in cx.children {
let _ = cx.layout_in(child, content_bounds);
}
if let Some(started) = layout_started {
t_layout_children = started.elapsed();
}
});
if !is_probe_layout {
let mut relayout_with_updated_content_bounds = false;
let mut authoritative_observation_cleared_pending = false;
let mut authoritative_observation_committed_this_pass = false;
let extent_may_be_stale = post_layout_extents_mode
|| defer_this_frame
|| cached_max_child.is_some()
|| cached.is_some();
let mut tree = UiTreeScrollOverflowTree {
tree: cx.tree,
app: cx.app,
window,
};
let post_layout_shrink_revalidation = post_layout_extents_mode
&& ((props.axis.scroll_x() && content_w.0 > desired.width.0 + 0.5)
|| (props.axis.scroll_y() && content_h.0 > desired.height.0 + 0.5))
&& children_layout_invalidated;
let shrink_validation_enabled = defer_this_frame
|| post_layout_shrink_revalidation
|| (cx.children.len() == 1
&& probe_unbounded_for_measure
&& (direct_children_layout_invalidated || cached.is_some())
&& ((props.axis.scroll_x() && content_w.0 > desired.width.0 + 0.5)
|| (props.axis.scroll_y() && content_h.0 > desired.height.0 + 0.5)));
let post_layout_authoritative_scan = post_layout_extents_mode
&& children_layout_invalidated
&& !must_probe_for_growing_extent;
let deep_scan_allowed =
(at_scroll_extent_edge && extent_may_be_stale && !must_probe_for_growing_extent)
|| shrink_validation_enabled
|| post_layout_authoritative_scan;
let (observed, observation) = observe_scroll_overflow_extents(
&mut tree,
cx.children,
content_bounds,
props.axis,
Size::new(content_w, content_h),
extent_may_be_stale,
deep_scan_allowed,
);
if crate::runtime_config::ui_runtime_config().debug_scroll_extent_probe
&& scroll_overflow_observation_needs_follow_up_probe(observation)
{
eprintln!(
"scroll extent observation budget hit element={:?} node={:?} axis={:?} peel={}/{} deep_scan={} visited={}/{} stale_hint={}",
self.element,
cx.node,
props.axis,
observation.wrapper_peeled_max,
observation.wrapper_peel_budget,
observation.deep_scan_enabled,
observation.deep_scan_visited,
observation.deep_scan_budget_nodes,
observation.extent_may_be_stale,
);
}
let budget_hit_needs_follow_up_probe = probe_unbounded_for_measure
&& !pending_extent_probe
&& post_layout_extents_mode
&& ((props.axis.scroll_x() && max_child.width.0 + 0.5 < previous_content.width.0)
|| (props.axis.scroll_y()
&& max_child.height.0 + 0.5 < previous_content.height.0));
if (!probe_unbounded_for_measure || budget_hit_needs_follow_up_probe)
&& maybe_schedule_extent_probe_after_observation_budget_hit(
&mut *cx.app,
cx.tree,
window,
cx.node,
self.element,
observation,
)
{
cx.request_redraw();
}
let authoritative_post_layout_observation = post_layout_authoritative_scan
&& scroll_overflow_observation_is_authoritative(observation);
if authoritative_post_layout_observation {
let next_content_w = if props.axis.scroll_x() && observed.trusted.width.0 > 0.0 {
Px(observed.trusted.width.0.max(desired.width.0.max(0.0)))
} else {
content_w
};
let next_content_h = if props.axis.scroll_y() && observed.trusted.height.0 > 0.0 {
Px(observed.trusted.height.0.max(desired.height.0.max(0.0)))
} else {
content_h
};
let changed = (next_content_w.0 - content_w.0).abs() > 0.5
|| (next_content_h.0 - content_h.0).abs() > 0.5;
if changed {
if crate::runtime_config::ui_runtime_config().debug_scroll_extent_probe {
eprintln!(
"scroll extent authoritative sync element={:?} node={:?} axis={:?} previous=({:.1},{:.1}) observed_trusted=({:.1},{:.1}) desired=({:.1},{:.1}) stale_hint={} deep_scan={} invalidated={}",
self.element,
cx.node,
props.axis,
content_w.0,
content_h.0,
observed.trusted.width.0,
observed.trusted.height.0,
desired.width.0,
desired.height.0,
extent_may_be_stale,
observation.deep_scan_enabled,
children_layout_invalidated,
);
}
content_w = next_content_w;
content_h = next_content_h;
relayout_with_updated_content_bounds = true;
handle.set_content_size_internal(Size::new(content_w, content_h));
let prev = handle.offset();
handle.set_offset_internal(prev);
cx.tree
.debug_record_scroll_node_telemetry(UiDebugScrollNodeTelemetry {
node: cx.node,
element: Some(self.element),
test_id: debug_test_id.clone(),
axis: match props.axis {
crate::element::ScrollAxis::X => UiDebugScrollAxis::X,
crate::element::ScrollAxis::Y => UiDebugScrollAxis::Y,
crate::element::ScrollAxis::Both => UiDebugScrollAxis::Both,
},
offset: handle.offset(),
viewport: handle.viewport_size(),
content: handle.content_size(),
observed_extent: None,
overflow_observation: None,
});
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child: Size::new(content_w, content_h),
intrinsic_cache_key,
probe_cache_key: None,
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: pending_extent_probe,
},
);
authoritative_observation_cleared_pending = true;
}
} else {
let mut changed_grow = false;
if props.axis.scroll_x()
&& observed.trusted.width.0 > 0.0
&& observed.trusted.width.0 > content_w.0 + 0.5
{
content_w = Px(observed.trusted.width.0.max(desired.width.0.max(0.0)));
changed_grow = true;
}
if props.axis.scroll_y()
&& observed.trusted.height.0 > 0.0
&& observed.trusted.height.0 > content_h.0 + 0.5
{
content_h = Px(observed.trusted.height.0.max(desired.height.0.max(0.0)));
changed_grow = true;
}
if changed_grow {
relayout_with_updated_content_bounds = true;
if crate::runtime_config::ui_runtime_config().debug_scroll_extent_probe {
eprintln!(
"scroll extent grew element={:?} node={:?} axis={:?} content=({:.1},{:.1}) observed=({:.1},{:.1}) viewport=({:.1},{:.1}) pending_probe={} must_probe={}",
self.element,
cx.node,
props.axis,
handle.content_size().width.0,
handle.content_size().height.0,
observed.loose.width.0,
observed.loose.height.0,
handle.viewport_size().width.0,
handle.viewport_size().height.0,
pending_extent_probe,
must_probe_for_growing_extent,
);
}
handle.set_content_size_internal(Size::new(content_w, content_h));
let prev = handle.offset();
handle.set_offset_internal(prev);
cx.tree
.debug_record_scroll_node_telemetry(UiDebugScrollNodeTelemetry {
node: cx.node,
element: Some(self.element),
test_id: debug_test_id.clone(),
axis: match props.axis {
crate::element::ScrollAxis::X => UiDebugScrollAxis::X,
crate::element::ScrollAxis::Y => UiDebugScrollAxis::Y,
crate::element::ScrollAxis::Both => UiDebugScrollAxis::Both,
},
offset: handle.offset(),
viewport: handle.viewport_size(),
content: handle.content_size(),
observed_extent: None,
overflow_observation: None,
});
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child: Size::new(content_w, content_h),
intrinsic_cache_key,
probe_cache_key: None,
clear_pending_invalidation_probe:
scroll_overflow_observation_is_authoritative(observation),
clear_pending_extent_probe: scroll_overflow_observation_is_authoritative(
observation,
)
&& pending_extent_probe,
},
);
authoritative_observation_committed_this_pass =
scroll_overflow_observation_is_authoritative(observation);
authoritative_observation_cleared_pending =
scroll_overflow_observation_is_authoritative(observation);
}
if shrink_validation_enabled
&& !authoritative_observation_committed_this_pass
&& scroll_overflow_observation_is_authoritative(observation)
{
let post_layout_shrink_has_layout_evidence_x = post_layout_shrink_revalidation
&& observed.loose.width.0 > 0.0
&& observed.loose.width.0 + 0.5 < content_w.0;
let post_layout_shrink_has_layout_evidence_y = post_layout_shrink_revalidation
&& observed.loose.height.0 > 0.0
&& observed.loose.height.0 + 0.5 < content_h.0;
let multi_child_post_layout_shrink_revalidation =
post_layout_shrink_revalidation && cx.children.len() > 1;
let can_shrink_x = defer_this_frame
|| post_layout_shrink_has_layout_evidence_x
|| multi_child_post_layout_shrink_revalidation
|| observed.trusted.width.0 > desired.width.0 + 0.5;
let can_shrink_y = defer_this_frame
|| post_layout_shrink_has_layout_evidence_y
|| multi_child_post_layout_shrink_revalidation
|| observed.trusted.height.0 > desired.height.0 + 0.5;
let mut changed = false;
if props.axis.scroll_x()
&& can_shrink_x
&& observed.trusted.width.0 > 0.0
&& observed.trusted.width.0 + 0.5 < content_w.0
{
content_w = Px(observed.trusted.width.0.max(desired.width.0.max(0.0)));
changed = true;
}
if props.axis.scroll_y()
&& can_shrink_y
&& observed.trusted.height.0 > 0.0
&& observed.trusted.height.0 + 0.5 < content_h.0
{
content_h = Px(observed.trusted.height.0.max(desired.height.0.max(0.0)));
changed = true;
}
if changed {
if crate::runtime_config::ui_runtime_config().debug_scroll_extent_probe {
eprintln!(
"scroll extent shrank element={:?} node={:?} axis={:?} content=({:.1},{:.1}) observed_trusted=({:.1},{:.1}) observed_loose=({:.1},{:.1}) desired=({:.1},{:.1}) post_layout_shrink={} defer_this_frame={}",
self.element,
cx.node,
props.axis,
handle.content_size().width.0,
handle.content_size().height.0,
observed.trusted.width.0,
observed.trusted.height.0,
observed.loose.width.0,
observed.loose.height.0,
desired.width.0,
desired.height.0,
post_layout_shrink_revalidation,
defer_this_frame,
);
}
relayout_with_updated_content_bounds = true;
handle.set_content_size_internal(Size::new(content_w, content_h));
let prev = handle.offset();
handle.set_offset_internal(prev);
cx.tree
.debug_record_scroll_node_telemetry(UiDebugScrollNodeTelemetry {
node: cx.node,
element: Some(self.element),
test_id: debug_test_id.clone(),
axis: match props.axis {
crate::element::ScrollAxis::X => UiDebugScrollAxis::X,
crate::element::ScrollAxis::Y => UiDebugScrollAxis::Y,
crate::element::ScrollAxis::Both => UiDebugScrollAxis::Both,
},
offset: handle.offset(),
viewport: handle.viewport_size(),
content: handle.content_size(),
observed_extent: None,
overflow_observation: None,
});
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child: Size::new(content_w, content_h),
intrinsic_cache_key,
probe_cache_key: None,
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: pending_extent_probe,
},
);
authoritative_observation_cleared_pending = true;
}
}
}
let deferred_probe_state_still_armed =
defer_state.kind != ScrollDeferredUnboundedProbeKind::None || pending_extent_probe;
let authoritative_observation_completed_without_extent_change =
!authoritative_observation_cleared_pending
&& scroll_overflow_observation_is_authoritative(observation)
&& deferred_probe_state_still_armed;
if authoritative_observation_completed_without_extent_change {
commit_scroll_authoritative_extent(
&mut *cx.app,
window,
self.element,
ScrollAuthoritativeExtentCommit {
max_child: Size::new(content_w, content_h),
intrinsic_cache_key,
probe_cache_key: None,
clear_pending_invalidation_probe: defer_state.kind
!= ScrollDeferredUnboundedProbeKind::None,
clear_pending_extent_probe: pending_extent_probe,
},
);
}
if relayout_with_updated_content_bounds {
content_bounds = Rect::new(cx.bounds.origin, Size::new(content_w, content_h));
cx.with_overflow_context(overflow_ctx, |cx| {
if !is_probe_layout {
if force_barrier_child_root_relayout {
for &child in &forced_barrier_child_roots {
cx.tree.invalidate(child, Invalidation::Layout);
}
}
let solve_started = profile_cfg.is_some().then(Instant::now);
match cx.children {
[child] => {
cx.solve_barrier_child_root_if_needed(*child, content_bounds);
}
children => {
let roots: Vec<(NodeId, Rect)> =
children.iter().map(|&c| (c, content_bounds)).collect();
cx.solve_barrier_child_roots_if_needed(&roots);
}
}
if let Some(started) = solve_started {
t_solve_barrier += started.elapsed();
}
}
let layout_started = profile_cfg.is_some().then(Instant::now);
for &child in cx.children {
let _ = cx.layout_in(child, content_bounds);
}
if let Some(started) = layout_started {
t_layout_children += started.elapsed();
}
});
}
if observation.wrapper_peel_budget_hit || observation.deep_scan_budget_hit {
cx.tree
.debug_record_scroll_node_telemetry(UiDebugScrollNodeTelemetry {
node: cx.node,
element: Some(self.element),
test_id: debug_test_id.clone(),
axis: match props.axis {
crate::element::ScrollAxis::X => UiDebugScrollAxis::X,
crate::element::ScrollAxis::Y => UiDebugScrollAxis::Y,
crate::element::ScrollAxis::Both => UiDebugScrollAxis::Both,
},
offset: handle.offset(),
viewport: handle.viewport_size(),
content: handle.content_size(),
observed_extent: Some(observed.trusted),
overflow_observation: Some(observation),
});
}
}
if let Some(cfg) = profile_cfg
&& let Some(started) = profile_started
{
let total = started.elapsed();
if total >= cfg.min_elapsed && t_measure_children >= cfg.min_self_measure {
let element_path: Option<String> = {
#[cfg(feature = "diagnostics")]
{
Some(crate::elements::with_window_state(
&mut *cx.app,
window,
|st| {
st.debug_path_for_element(self.element)
.unwrap_or_else(|| "<unknown>".to_string())
},
))
}
#[cfg(not(feature = "diagnostics"))]
{
None
}
};
tracing::info!(
window = ?cx.window,
node = ?cx.node,
element = self.element.0,
pass = ?cx.pass_kind,
axis = ?props.axis,
probe_unbounded = props.probe_unbounded,
children = cx.children.len(),
available_w = cx.available.width.0,
available_h = cx.available.height.0,
desired_w = desired.width.0,
desired_h = desired.height.0,
content_w = content_w.0,
content_h = content_h.0,
measure_children_us = t_measure_children.as_micros() as u64,
solve_barrier_us = t_solve_barrier.as_micros() as u64,
layout_children_us = t_layout_children.as_micros() as u64,
total_us = total.as_micros() as u64,
element_path = element_path.as_deref().unwrap_or("<unknown>"),
"scroll layout profile"
);
}
}
desired
}
pub(super) fn layout_scrollbar_impl<H: UiHost>(
&mut self,
cx: &mut LayoutCx<'_, H>,
props: crate::element::ScrollbarProps,
) -> Size {
clamp_to_constraints(cx.available, props.layout, cx.available)
}
}
#[cfg(test)]
mod budget_probe_tests {
use super::*;
#[derive(Default)]
struct TestWidget;
impl<H: UiHost> Widget<H> for TestWidget {
fn layout(&mut self, _cx: &mut LayoutCx<'_, H>) -> Size {
Size::new(Px(0.0), Px(0.0))
}
fn paint(&mut self, _cx: &mut PaintCx<'_, H>) {}
}
fn make_observation(
wrapper_budget_hit: bool,
deep_scan_enabled: bool,
deep_scan_budget_hit: bool,
) -> UiDebugScrollOverflowObservationTelemetry {
UiDebugScrollOverflowObservationTelemetry {
extent_may_be_stale: true,
barrier_roots: 1,
wrapper_peel_budget: 8,
wrapper_peeled_max: if wrapper_budget_hit { 8 } else { 0 },
wrapper_peel_budget_hit: wrapper_budget_hit,
immediate_children_visited: 0,
immediate_children_skipped_absolute: 0,
deep_scan_enabled,
deep_scan_budget_nodes: 4096,
deep_scan_visited: if deep_scan_enabled {
if deep_scan_budget_hit { 4096 } else { 128 }
} else {
0
},
deep_scan_budget_hit,
deep_scan_skipped_absolute: 0,
}
}
#[test]
fn scroll_post_layout_observation_budget_hit_schedules_probe_next_frame() {
let mut app = crate::test_host::TestHost::new();
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
let window = AppWindowId::default();
ui.set_window(window);
ui.set_debug_enabled(true);
let root = ui.create_node(TestWidget);
ui.set_root(root);
let element_peel = GlobalElementId(101);
let element_deep = GlobalElementId(202);
let node_peel = ui.create_node_for_element(element_peel, TestWidget);
let node_deep = ui.create_node_for_element(element_deep, TestWidget);
ui.set_children(root, vec![node_peel, node_deep]);
assert!(
maybe_schedule_extent_probe_after_observation_budget_hit(
&mut app,
&mut ui,
window,
node_peel,
element_peel,
make_observation(true, false, false),
),
"expected wrapper-peel budget hit to schedule a probe"
);
assert!(
maybe_schedule_extent_probe_after_observation_budget_hit(
&mut app,
&mut ui,
window,
node_deep,
element_deep,
make_observation(false, true, true),
),
"expected deep-scan budget hit to schedule a probe"
);
let pending = ui.take_pending_barrier_relayouts();
assert!(
pending.contains(&node_peel) && pending.contains(&node_deep),
"expected both scroll nodes to be scheduled for barrier relayout, got={pending:?}"
);
let pending_probe_peel = crate::elements::with_element_state(
&mut app,
window,
element_peel,
crate::element::ScrollState::default,
|state| state.pending_extent_probe,
);
let pending_probe_deep = crate::elements::with_element_state(
&mut app,
window,
element_deep,
crate::element::ScrollState::default,
|state| state.pending_extent_probe,
);
assert!(
pending_probe_peel && pending_probe_deep,
"expected budget-hit scheduling to set pending_extent_probe"
);
assert!(
!maybe_schedule_extent_probe_after_observation_budget_hit(
&mut app,
&mut ui,
window,
node_peel,
element_peel,
make_observation(true, false, false),
),
"expected repeated scheduling to be a no-op once pending"
);
assert!(
ui.take_pending_barrier_relayouts().is_empty(),
"expected no additional pending barrier relayouts after a no-op schedule"
);
}
#[test]
fn scroll_post_layout_observation_deep_scan_can_resolve_wrapper_budget_hit() {
let mut app = crate::test_host::TestHost::new();
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
let window = AppWindowId::default();
ui.set_window(window);
let root = ui.create_node(TestWidget);
ui.set_root(root);
let element = GlobalElementId(404);
let node = ui.create_node_for_element(element, TestWidget);
ui.set_children(root, vec![node]);
assert!(
!maybe_schedule_extent_probe_after_observation_budget_hit(
&mut app,
&mut ui,
window,
node,
element,
make_observation(true, true, false),
),
"expected completed deep scan to resolve wrapper-peel budget hit without scheduling another probe"
);
assert!(
ui.take_pending_barrier_relayouts().is_empty(),
"expected resolved wrapper budget hit to avoid scheduling a barrier relayout"
);
let pending_probe = crate::elements::with_element_state(
&mut app,
window,
element,
crate::element::ScrollState::default,
|state| state.pending_extent_probe,
);
assert!(
!pending_probe,
"expected resolved wrapper budget hit to leave pending_extent_probe cleared"
);
}
#[test]
fn scroll_post_layout_observation_budget_hit_schedules_probe_without_edge_requirement() {
let mut app = crate::test_host::TestHost::new();
let mut ui: UiTree<crate::test_host::TestHost> = UiTree::new();
let window = AppWindowId::default();
ui.set_window(window);
let root = ui.create_node(TestWidget);
ui.set_root(root);
let element = GlobalElementId(303);
let node = ui.create_node_for_element(element, TestWidget);
ui.set_children(root, vec![node]);
assert!(maybe_schedule_extent_probe_after_observation_budget_hit(
&mut app,
&mut ui,
window,
node,
element,
make_observation(false, true, true),
));
let pending = ui.take_pending_barrier_relayouts();
assert!(
pending.contains(&node),
"expected non-edge budget hit to still schedule a barrier relayout, got={pending:?}"
);
let pending_probe = crate::elements::with_element_state(
&mut app,
window,
element,
crate::element::ScrollState::default,
|state| state.pending_extent_probe,
);
assert!(
pending_probe,
"expected budget hit to set pending_extent_probe before edge"
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use fret_core::{Point, Rect};
use slotmap::SlotMap;
use std::collections::{HashMap, HashSet};
#[derive(Default)]
struct TestOverflowTree {
children: HashMap<NodeId, Vec<NodeId>>,
bounds: HashMap<NodeId, Rect>,
absolute: HashSet<NodeId>,
clip_overflow: HashSet<NodeId>,
}
impl ScrollOverflowTree for TestOverflowTree {
fn children_ref(&self, node: NodeId) -> &[NodeId] {
self.children.get(&node).map(Vec::as_slice).unwrap_or(&[])
}
fn node_bounds(&self, node: NodeId) -> Option<Rect> {
self.bounds.get(&node).copied()
}
fn node_is_absolute(&mut self, node: NodeId) -> bool {
self.absolute.contains(&node)
}
fn node_clips_descendant_scroll_overflow(&mut self, node: NodeId) -> bool {
self.clip_overflow.contains(&node)
}
}
fn rect_xywh(x: f32, y: f32, w: f32, h: f32) -> Rect {
Rect::new(
Point::new(Px(x), Px(y)),
Size::new(Px(w.max(0.0)), Px(h.max(0.0))),
)
}
#[test]
fn scroll_observed_overflow_peels_same_bounds_wrapper_chain() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let wrapper = ids.insert(());
let content_root = ids.insert(());
let leaf_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![wrapper]);
tree.children.insert(wrapper, vec![content_root]);
tree.children.insert(content_root, vec![leaf_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(content_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(leaf_overflow, rect_xywh(0.0, 0.0, 100.0, 300.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, _telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
false,
false,
);
assert_eq!(observed.trusted.height, Px(300.0));
}
#[test]
fn scroll_observed_overflow_bounded_scan_discovers_deeper_overflow() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let observe_root = ids.insert(());
let wrapper_a = ids.insert(());
let wrapper_b = ids.insert(());
let deep_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![observe_root]);
tree.children
.insert(observe_root, vec![wrapper_a, wrapper_b]);
tree.children.insert(wrapper_a, vec![deep_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(observe_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper_a, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper_b, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(deep_overflow, rect_xywh(0.0, 0.0, 100.0, 300.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, _telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert_eq!(observed.trusted.height, Px(300.0));
}
#[test]
fn scroll_observed_overflow_ignores_stale_nonleaf_extent_until_descendants_confirm_it() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let stale_wrapper = ids.insert(());
let leaf = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![stale_wrapper]);
tree.children.insert(stale_wrapper, vec![leaf]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(stale_wrapper, rect_xywh(0.0, 0.0, 100.0, 300.0));
tree.bounds.insert(leaf, rect_xywh(0.0, 0.0, 100.0, 200.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(300.0)),
true,
true,
);
assert_eq!(observed.loose.height, Px(300.0));
assert_eq!(observed.trusted.height, Px(200.0));
assert!(telemetry.deep_scan_enabled);
}
#[test]
fn scroll_observed_overflow_preserves_multi_child_container_padding() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let container = ids.insert(());
let child_a = ids.insert(());
let child_b = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![container]);
tree.children.insert(container, vec![child_a, child_b]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(container, rect_xywh(0.0, 0.0, 100.0, 300.0));
tree.bounds
.insert(child_a, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(child_b, rect_xywh(0.0, 150.0, 100.0, 80.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(350.0)),
true,
true,
);
assert_eq!(observed.loose.height, Px(300.0));
assert_eq!(observed.trusted.height, Px(300.0));
assert!(telemetry.deep_scan_enabled);
}
#[test]
fn scroll_observed_overflow_prefers_descendant_frontier_over_shorter_outer_shell() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let outer_shell = ids.insert(());
let inner_shell = ids.insert(());
let leaf = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![outer_shell]);
tree.children.insert(outer_shell, vec![inner_shell]);
tree.children.insert(inner_shell, vec![leaf]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(outer_shell, rect_xywh(0.0, 0.0, 100.0, 280.0));
tree.bounds
.insert(inner_shell, rect_xywh(0.0, 0.0, 100.0, 416.0));
tree.bounds.insert(leaf, rect_xywh(0.0, 0.0, 100.0, 400.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(500.0)),
true,
true,
);
assert_eq!(observed.loose.height, Px(416.0));
assert_eq!(observed.trusted.height, Px(416.0));
assert!(telemetry.deep_scan_enabled);
}
#[test]
fn scroll_observed_overflow_does_not_deep_scan_when_not_stale() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let observe_root = ids.insert(());
let wrapper = ids.insert(());
let sibling = ids.insert(());
let deep_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![observe_root]);
tree.children.insert(observe_root, vec![wrapper, sibling]);
tree.children.insert(wrapper, vec![deep_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(observe_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(sibling, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(deep_overflow, rect_xywh(0.0, 0.0, 100.0, 300.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, _telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
false,
true,
);
assert_eq!(observed.trusted.height, Px(100.0));
}
#[test]
fn scroll_observed_overflow_wrapper_peel_stops_before_nested_viewport_boundary() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let nested_viewport = ids.insert(());
let deep_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![nested_viewport]);
tree.children.insert(nested_viewport, vec![deep_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(nested_viewport, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(deep_overflow, rect_xywh(0.0, 0.0, 100.0, 320.0));
tree.clip_overflow.insert(nested_viewport);
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert!(telemetry.deep_scan_enabled);
assert_eq!(observed.trusted.height, Px(100.0));
}
#[test]
fn scroll_observed_overflow_wrapper_peel_stops_before_absolute_child() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let absolute_child = ids.insert(());
let deep_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![absolute_child]);
tree.children.insert(absolute_child, vec![deep_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(absolute_child, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(deep_overflow, rect_xywh(0.0, 0.0, 100.0, 320.0));
tree.absolute.insert(absolute_child);
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, _telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert_eq!(observed.trusted.height, Px(100.0));
}
#[test]
fn scroll_observed_overflow_excludes_absolute_nodes() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let abs_child = ids.insert(());
let normal_child = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children
.insert(barrier_root, vec![abs_child, normal_child]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(abs_child, rect_xywh(0.0, 0.0, 100.0, 800.0));
tree.bounds
.insert(normal_child, rect_xywh(0.0, 0.0, 100.0, 300.0));
tree.absolute.insert(abs_child);
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, _telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert_eq!(observed.trusted.height, Px(300.0));
}
#[test]
fn scroll_observed_overflow_telemetry_reports_budget_hits() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let mut chain: Vec<NodeId> = Vec::new();
for _ in 0..(8 + 4096 + 16) {
chain.push(ids.insert(()));
}
let mut tree = TestOverflowTree::default();
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.children.insert(barrier_root, vec![chain[0]]);
for (ix, &id) in chain.iter().enumerate() {
tree.bounds.insert(id, rect_xywh(0.0, 0.0, 100.0, 100.0));
if let Some(&next) = chain.get(ix + 1) {
tree.children.insert(id, vec![next]);
}
}
let last = *chain.last().expect("non-empty chain");
tree.bounds.insert(last, rect_xywh(0.0, 0.0, 100.0, 1000.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (_observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert!(telemetry.wrapper_peel_budget_hit);
assert!(telemetry.deep_scan_enabled);
assert!(telemetry.deep_scan_budget_hit);
assert_eq!(telemetry.deep_scan_budget_nodes, 4096);
assert_eq!(telemetry.deep_scan_visited, 4096);
}
#[test]
fn scroll_observed_overflow_deep_scan_resolves_wrapper_chain_beyond_peel_budget() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let mut chain: Vec<NodeId> = (0..10).map(|_| ids.insert(())).collect();
let leaf = ids.insert(());
chain.push(leaf);
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![chain[0]]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
for window in chain.windows(2) {
let parent = window[0];
let child = window[1];
tree.children.insert(parent, vec![child]);
tree.bounds
.insert(parent, rect_xywh(0.0, 0.0, 100.0, 100.0));
}
tree.bounds.insert(leaf, rect_xywh(0.0, 0.0, 100.0, 320.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
true,
);
assert!(telemetry.wrapper_peel_budget_hit);
assert!(telemetry.deep_scan_enabled);
assert!(!telemetry.deep_scan_budget_hit);
assert_eq!(observed.trusted.height, Px(320.0));
}
#[test]
fn scroll_observed_overflow_respects_deep_scan_allowed_flag() {
let mut ids: SlotMap<NodeId, ()> = SlotMap::with_key();
let barrier_root = ids.insert(());
let observe_root = ids.insert(());
let wrapper_a = ids.insert(());
let wrapper_b = ids.insert(());
let deep_overflow = ids.insert(());
let mut tree = TestOverflowTree::default();
tree.children.insert(barrier_root, vec![observe_root]);
tree.children
.insert(observe_root, vec![wrapper_a, wrapper_b]);
tree.children.insert(wrapper_a, vec![deep_overflow]);
tree.bounds
.insert(barrier_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(observe_root, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper_a, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(wrapper_b, rect_xywh(0.0, 0.0, 100.0, 100.0));
tree.bounds
.insert(deep_overflow, rect_xywh(0.0, 0.0, 100.0, 300.0));
let content_bounds = rect_xywh(0.0, 0.0, 100.0, 100.0);
let (observed, telemetry) = observe_scroll_overflow_extents(
&mut tree,
&[barrier_root],
content_bounds,
crate::element::ScrollAxis::Y,
Size::new(Px(100.0), Px(100.0)),
true,
false,
);
assert_eq!(observed.trusted.height, Px(100.0));
assert!(!telemetry.deep_scan_enabled);
assert_eq!(telemetry.deep_scan_visited, 0);
assert!(!telemetry.deep_scan_budget_hit);
}
#[test]
fn authoritative_extent_commit_clears_deferred_probe_state() {
let mut app = crate::test_host::TestHost::new();
let window = AppWindowId::default();
let element = GlobalElementId(9001);
crate::elements::with_element_state(
&mut app,
window,
element,
ScrollDeferredUnboundedProbeState::default,
|state| {
state.kind = ScrollDeferredUnboundedProbeKind::Invalidation;
state.stable_frames = 2;
state.pending_invalidation_probe = true;
},
);
commit_scroll_authoritative_extent(
&mut app,
window,
element,
ScrollAuthoritativeExtentCommit {
max_child: Size::new(Px(120.0), Px(160.0)),
intrinsic_cache_key: None,
probe_cache_key: None,
clear_pending_invalidation_probe: true,
clear_pending_extent_probe: false,
},
);
let state = crate::elements::with_element_state(
&mut app,
window,
element,
ScrollDeferredUnboundedProbeState::default,
|state| *state,
);
assert_eq!(
state.kind,
ScrollDeferredUnboundedProbeKind::None,
"authoritative extent commit should end deferred probe mode instead of leaving the state machine armed"
);
assert_eq!(
state.stable_frames, 0,
"authoritative extent commit should clear deferred probe stability bookkeeping"
);
assert!(
!state.pending_invalidation_probe,
"authoritative extent commit should clear the pending invalidation probe flag"
);
}
}