use super::*;
use std::rc::Rc;
use waterui_core::animation::Animation;
use waterui_core::layout::Point;
use waterui_layout::collection_transition::CollectionTransition;
#[derive(Clone, Copy)]
pub(super) enum EntryPhase {
Stable,
Entering(Instant),
Exiting(Instant),
}
impl EntryPhase {
fn factor(self, now: Instant, animation: &Animation) -> f32 {
match self {
Self::Stable => 1.0,
Self::Entering(start) => animation.progress(now.saturating_duration_since(start)),
Self::Exiting(start) => 1.0 - animation.progress(now.saturating_duration_since(start)),
}
}
fn is_transitioning(self, now: Instant, animation: &Animation) -> bool {
match self {
Self::Stable => false,
Self::Entering(start) | Self::Exiting(start) => {
!animation.is_complete(now.saturating_duration_since(start))
}
}
}
fn is_finished_exit(self, now: Instant, animation: &Animation) -> bool {
matches!(self, Self::Exiting(start) if animation.is_complete(now.saturating_duration_since(start)))
}
const fn suppresses_accessibility(self) -> bool {
matches!(self, Self::Exiting(_))
}
}
pub(super) struct CollectionTransitionRuntime {
animation: Animation,
axis: Option<TransitionAxis>,
}
#[derive(Clone, Copy)]
struct TransitionAxis {
vertical: bool,
spacing: f64,
}
pub(super) struct CollectionEntry {
pub(super) id: CollectionItemId,
pub(super) node: RenderNode,
phase: EntryPhase,
factor: f32,
}
impl CollectionEntry {
pub(super) fn stable(id: CollectionItemId, node: RenderNode) -> Self {
Self {
id,
node,
phase: EntryPhase::Stable,
factor: 1.0,
}
}
}
pub(super) fn collection_transition_runtime(
env: &Environment,
layout: &dyn Layout,
) -> Option<CollectionTransitionRuntime> {
let transition = env.get::<CollectionTransition>()?;
let axis = lazy_stack_axis_config(
layout,
nami::Computed::constant(waterui_core::layout::LayoutDirection::default()),
)
.map(|config| match config {
LazyStackAxisConfig::Vertical { spacing, .. } => TransitionAxis {
vertical: true,
spacing: f64::from(spacing.get()),
},
LazyStackAxisConfig::Horizontal { spacing, .. } => TransitionAxis {
vertical: false,
spacing: f64::from(spacing.get()),
},
});
Some(CollectionTransitionRuntime {
animation: transition.animation.clone(),
axis,
})
}
fn next_live_phase(
transition: Option<&CollectionTransitionRuntime>,
previous: EntryPhase,
now: Instant,
) -> EntryPhase {
match (transition.is_some(), previous) {
(false, _) => EntryPhase::Stable,
(true, EntryPhase::Exiting(_)) => EntryPhase::Entering(now),
(true, phase) => phase,
}
}
pub(crate) struct CollectionNode {
pub(super) layout: Box<dyn Layout>,
pub(super) views: AnyViews<AnyView>,
pub(super) env: Environment,
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
#[cfg(feature = "accessibility")]
pub(super) accessibility_container_env: Option<Environment>,
pub(super) entries: Vec<CollectionEntry>,
pub(super) placed: Vec<Rect>,
pub(super) transition: Option<CollectionTransitionRuntime>,
pub(super) dirty: Rc<Cell<bool>>,
pub(super) _dirty_key: Rc<()>,
pub(super) _guard: BoxWatcherGuard,
}
pub(crate) struct LazyStackNode {
pub(super) axis: LazyStackAxisConfig,
pub(super) views: AnyViews<AnyView>,
pub(super) env: Environment,
#[cfg(feature = "accessibility")]
pub(super) accessibility_identity: Rc<()>,
#[cfg(feature = "accessibility")]
pub(super) accessibility_container_env: Option<Environment>,
pub(super) extent_index: RefCell<VirtualExtentIndex>,
pub(super) item_cache: RefCell<VisibleSubviewCache<CollectionItemId>>,
pub(super) visible_range: RefCell<Range<usize>>,
pub(super) estimate: Cell<f64>,
pub(super) estimate_sample: Cell<Option<(Option<f32>, Size)>>,
pub(super) dirty: Rc<Cell<bool>>,
pub(super) _dirty_key: Rc<()>,
pub(super) _guard: BoxWatcherGuard,
pub(super) _direction_guard: BoxWatcherGuard,
}
impl CollectionNode {
fn has_active_transition(&self) -> bool {
self.entries
.iter()
.any(|entry| !matches!(entry.phase, EntryPhase::Stable))
}
pub(super) fn measure(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
proposal: ProposalSize,
) -> ViewDimensions {
if self.has_active_transition()
&& let Some(runtime) = &self.transition
&& let Some(axis) = runtime.axis
{
return ViewDimensions::new(
self.measure_transitioning_stack(state, theme, proposal, axis),
);
}
let cell = RefCell::new(state);
let subs: Vec<NodeSubView> = self
.entries
.iter()
.map(|entry| NodeSubView::new(&entry.node, &cell, &self.env, theme))
.collect();
let refs: Vec<&dyn SubView> = subs.iter().map(|sub| sub as &dyn SubView).collect();
ViewDimensions::new(self.layout.size_that_fits(proposal, &refs))
}
fn measure_transitioning_stack(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
proposal: ProposalSize,
axis: TransitionAxis,
) -> LayoutSize {
let cell = RefCell::new(state);
let item_proposal = if axis.vertical {
ProposalSize::new(proposal.width, None)
} else {
ProposalSize::new(None, proposal.height)
};
let mut main = 0.0_f64;
let mut cross = 0.0_f64;
let mut first_visible = true;
for entry in &self.entries {
let factor = f64::from(entry.factor);
if factor <= f64::EPSILON {
continue;
}
let sub = NodeSubView::new(&entry.node, &cell, &self.env, theme);
let size = sub.measure(item_proposal).size;
let (item_main, item_cross) = if axis.vertical {
(f64::from(size.height), f64::from(size.width))
} else {
(f64::from(size.width), f64::from(size.height))
};
if !first_visible {
main += axis.spacing * factor;
}
first_visible = false;
main += item_main * factor;
cross = cross.max(item_cross);
}
#[allow(clippy::cast_possible_truncation)]
if axis.vertical {
LayoutSize::new(cross as f32, main as f32)
} else {
LayoutSize::new(main as f32, cross as f32)
}
}
pub(super) fn layout(
&mut self,
renderer: &mut HydrolysisRenderer,
proposal: ProposalSize,
size: Size,
) {
let env = self.env.clone();
let theme = renderer.theme();
let mut placements = {
let cell = RefCell::new(&mut renderer.state);
let subs: Vec<NodeSubView> = self
.entries
.iter()
.map(|entry| NodeSubView::new(&entry.node, &cell, &env, &theme))
.collect();
let refs: Vec<&dyn SubView> = subs.iter().map(|sub| sub as &dyn SubView).collect();
self.layout.place(Rect::from_size(size), proposal, &refs)
};
if self.has_active_transition()
&& let Some(runtime) = &self.transition
&& let Some(axis) = runtime.axis
{
let mut cursor = 0.0_f32;
let mut first_visible = true;
for (entry, placement) in self.entries.iter().zip(&mut placements) {
let rect = &mut placement.frame;
let factor = entry.factor;
if factor <= f32::EPSILON {
*rect = place_on_axis(*rect, axis, cursor);
continue;
}
#[allow(clippy::cast_possible_truncation)]
if !first_visible {
cursor += axis.spacing as f32 * factor;
}
first_visible = false;
*rect = place_on_axis(*rect, axis, cursor);
let extent = if axis.vertical {
rect.height()
} else {
rect.width()
};
cursor += extent * factor;
}
}
for (entry, placement) in self.entries.iter_mut().zip(&placements) {
entry
.node
.layout(renderer, &env, placement.proposal, *placement.frame.size());
}
self.placed = placements
.into_iter()
.map(|placement| placement.frame)
.collect();
}
pub(super) fn flush(&self, renderer: &mut HydrolysisRenderer, ctx: RenderContext) {
#[cfg(feature = "accessibility")]
let container_scope = self.accessibility_container_env.as_ref().map(|env| {
renderer.push_accessibility_owner(&self.accessibility_identity);
let scope = renderer.begin_accessibility_container(
transformed_rect(ctx.hit_transform, ctx.bounds),
env,
);
renderer.pop_accessibility_owner();
scope
});
let axis = self.transition.as_ref().and_then(|runtime| runtime.axis);
for (entry, rect) in self.entries.iter().zip(self.placed.iter()) {
let factor = entry.factor;
if factor <= f32::EPSILON {
continue;
}
let child_ctx = ctx.child(
vello::kurbo::Affine::translate((f64::from(rect.x()), f64::from(rect.y()))),
vello::kurbo::Rect::new(
0.0,
0.0,
f64::from(rect.width()),
f64::from(rect.height()),
),
);
if entry.phase.suppresses_accessibility() {
renderer.with_suppressed_accessibility(|renderer| {
Self::flush_entry(renderer, entry, child_ctx, &self.env, factor, axis);
});
} else {
Self::flush_entry(renderer, entry, child_ctx, &self.env, factor, axis);
}
}
#[cfg(feature = "accessibility")]
if let Some(container_scope) = container_scope {
renderer.push_accessibility_owner(&self.accessibility_identity);
renderer.end_accessibility_container(container_scope);
renderer.pop_accessibility_owner();
}
}
#[cfg(feature = "accessibility")]
pub(super) fn emit_accessibility(&self, renderer: &mut SemanticCore) {
let container_scope = self.accessibility_container_env.as_ref().map(|env| {
renderer.push_accessibility_owner(&self.accessibility_identity);
let scope = renderer.begin_accessibility_container_semantic(env);
renderer.pop_accessibility_owner();
scope
});
for entry in &self.entries {
if entry.factor <= f32::EPSILON {
continue;
}
if entry.phase.suppresses_accessibility() {
renderer.with_suppressed_accessibility(|renderer| {
entry.node.emit_accessibility(renderer, &self.env);
});
} else {
entry.node.emit_accessibility(renderer, &self.env);
}
}
if let Some(container_scope) = container_scope {
renderer.push_accessibility_owner(&self.accessibility_identity);
renderer.end_accessibility_container(container_scope);
renderer.pop_accessibility_owner();
}
}
fn flush_entry(
renderer: &mut HydrolysisRenderer,
entry: &CollectionEntry,
child_ctx: RenderContext,
env: &Environment,
factor: f32,
axis: Option<TransitionAxis>,
) {
if factor >= 1.0 {
entry.node.flush(renderer, child_ctx, env);
return;
}
let bounds = child_ctx.bounds;
let clip = match axis {
Some(TransitionAxis { vertical: true, .. }) => vello::kurbo::Rect::new(
0.0,
0.0,
bounds.width(),
bounds.height() * f64::from(factor),
),
Some(TransitionAxis {
vertical: false, ..
}) => vello::kurbo::Rect::new(
0.0,
0.0,
bounds.width() * f64::from(factor),
bounds.height(),
),
None => bounds,
};
renderer.push_layer_rect(factor, child_ctx.transform, clip);
let previous_opacity = renderer.hit_test.hit_test_opacity;
renderer.hit_test.hit_test_opacity = previous_opacity * factor;
entry.node.flush(renderer, child_ctx, env);
renderer.hit_test.hit_test_opacity = previous_opacity;
renderer.pop_layer();
}
pub(super) fn reconcile(&mut self, renderer: &mut SemanticCore) {
let env = self.env.clone();
let len = self.views.len().get();
let now = renderer.frame_instant;
let animated = self.transition.is_some();
let ids: Vec<CollectionItemId> = (0..len)
.map(|index| {
self.views
.get_id(index)
.unwrap_or_else(|| panic!("hydrolysis collection: item {index} has no id"))
})
.collect();
let live: std::collections::BTreeSet<CollectionItemId> = ids.iter().copied().collect();
let mut reuse_by_id: std::collections::BTreeMap<CollectionItemId, CollectionEntry> =
std::collections::BTreeMap::new();
let mut head_dead: Vec<CollectionEntry> = Vec::new();
let mut dead_after: std::collections::BTreeMap<CollectionItemId, Vec<CollectionEntry>> =
std::collections::BTreeMap::new();
{
let mut last_live: Option<CollectionItemId> = None;
for entry in self.entries.drain(..) {
if live.contains(&entry.id) {
last_live = Some(entry.id);
reuse_by_id.insert(entry.id, entry);
} else if animated {
match last_live {
Some(anchor) => dead_after.entry(anchor).or_default().push(entry),
None => head_dead.push(entry),
}
}
}
}
let begin_exit = |mut entry: CollectionEntry| -> CollectionEntry {
if !matches!(entry.phase, EntryPhase::Exiting(_)) {
entry.phase = EntryPhase::Exiting(now);
}
entry
};
let mut next = Vec::with_capacity(len + head_dead.len());
next.extend(head_dead.into_iter().map(begin_exit));
for (index, id) in ids.into_iter().enumerate() {
let entry = match reuse_by_id.remove(&id) {
Some(mut previous) => {
previous.phase = next_live_phase(self.transition.as_ref(), previous.phase, now);
previous
}
None => {
let view = self
.views
.get_view(index)
.unwrap_or_else(|| panic!("hydrolysis collection: item {index} missing"));
CollectionEntry {
id,
node: RenderNode::build(normalize_layout_view(view, &env), &env, renderer),
phase: if animated {
EntryPhase::Entering(now)
} else {
EntryPhase::Stable
},
factor: if animated { 0.0 } else { 1.0 },
}
}
};
next.push(entry);
if let Some(dead) = dead_after.remove(&id) {
next.extend(dead.into_iter().map(begin_exit));
}
}
next.extend(dead_after.into_values().flatten().map(begin_exit));
self.entries = next;
}
pub(super) fn advance_transitions(&mut self, renderer: &mut SemanticCore) -> bool {
let Some(runtime) = &self.transition else {
return false;
};
let animation = runtime.animation.clone();
let now = renderer.frame_instant;
let before = self.entries.len();
self.entries
.retain(|entry| !entry.phase.is_finished_exit(now, &animation));
let dropped = self.entries.len() != before;
let mut transitioning = false;
for entry in &mut self.entries {
if let EntryPhase::Entering(start) = entry.phase
&& animation.is_complete(now.saturating_duration_since(start))
{
entry.phase = EntryPhase::Stable;
}
entry.factor = entry.phase.factor(now, &animation).clamp(0.0, 1.0);
transitioning |= entry.phase.is_transitioning(now, &animation);
}
if transitioning {
renderer.signals.request_refresh();
}
dropped || transitioning
}
}
fn place_on_axis(rect: Rect, axis: TransitionAxis, main_position: f32) -> Rect {
let origin = if axis.vertical {
Point::new(rect.x(), main_position)
} else {
Point::new(main_position, rect.y())
};
Rect::new(origin, *rect.size())
}
impl LazyStackNode {
pub(super) fn patch_visible(&self, renderer: &mut SemanticCore) -> bool {
let changed = self.item_cache.borrow_mut().patch_for_parent(renderer);
if changed {
self.estimate_sample.set(None);
}
changed
}
fn spacing(&self) -> f64 {
match &self.axis {
LazyStackAxisConfig::Vertical { spacing, .. }
| LazyStackAxisConfig::Horizontal { spacing, .. } => f64::from(spacing.get()),
}
}
fn item_proposal(&self, cross: Option<f32>) -> ProposalSize {
match &self.axis {
LazyStackAxisConfig::Vertical { .. } => ProposalSize::new(cross, None),
LazyStackAxisConfig::Horizontal { .. } => ProposalSize::new(None, cross),
}
}
fn measure_item(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
index: usize,
cross: Option<f32>,
) -> (Size, StretchAxis) {
let id = self
.views
.get_id(index)
.unwrap_or_else(|| panic!("hydrolysis LazyStack item {index} has no id"));
let proposal = self.item_proposal(cross);
if let Some(item) = self.item_cache.borrow().get(&id) {
return (
item.measure_built_with_proposal(state, &self.env, theme, proposal),
item.stretch_axis(),
);
}
let view = self
.views
.get_view(index)
.unwrap_or_else(|| panic!("hydrolysis LazyStack failed to materialize item {index}"));
let view = normalize_layout_view(view, &self.env);
state.measurement.begin_transient_measurement();
let result = {
let bound = RefCell::new(&mut *state);
let subview = HydroSubview::from_view(&view, &bound, &self.env, theme);
(subview.measure(proposal).size, subview.stretch_axis())
};
state.measurement.end_transient_measurement();
result
}
fn main_extent(&self, size: Size) -> f64 {
match &self.axis {
LazyStackAxisConfig::Vertical { .. } => f64::from(size.height),
LazyStackAxisConfig::Horizontal { .. } => f64::from(size.width),
}
}
fn ensure_estimate(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
cross: Option<f32>,
) -> Size {
if let Some((previous, size)) = self.estimate_sample.get()
&& previous == cross
&& !self.dirty.get()
{
return size;
}
let (size, _) = self.measure_item(state, theme, 0, cross);
let extent = self.main_extent(size);
self.estimate.set(extent.max(1.0));
self.estimate_sample.set(Some((cross, size)));
self.dirty.set(true);
self.prepare_extent_index(self.views.len().get());
self.extent_index.borrow_mut().set_measured(0, extent);
size
}
fn prepare_extent_index(&self, count: usize) {
let estimate = self.estimate.get();
if count == 0 || estimate <= 0.0 {
return;
}
let spacing = self.spacing();
let dirty = self.dirty.replace(false);
if dirty || !self.extent_index.borrow().matches(count, estimate, spacing) {
self.extent_index
.borrow_mut()
.reset(count, estimate, spacing);
}
}
fn refresh_visible_extents(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
count: usize,
cross: Option<f32>,
) {
let visible = self.visible_range.borrow().clone();
let cache = self.item_cache.borrow();
for index in visible.start.min(count)..visible.end.min(count) {
let id = self
.views
.get_id(index)
.unwrap_or_else(|| panic!("hydrolysis LazyStack item {index} has no id"));
let Some(item) = cache.get(&id) else {
continue;
};
let proposal = self.item_proposal(cross);
let size = item.measure_built_with_proposal(state, &self.env, theme, proposal);
let extent = self.main_extent(size);
self.extent_index.borrow_mut().set_measured(index, extent);
}
}
#[allow(clippy::cast_possible_truncation)]
pub(super) fn measure(
&self,
state: &mut HydroState,
theme: &Rc<dyn crate::engine::WidgetTheme>,
proposal: ProposalSize,
) -> ViewDimensions {
let count = self.views.len().get();
if count == 0 {
return ViewDimensions::new(Size::zero());
}
let cross = match &self.axis {
LazyStackAxisConfig::Vertical { .. } => proposal.width,
LazyStackAxisConfig::Horizontal { .. } => proposal.height,
};
let sample = self.ensure_estimate(state, theme, cross);
self.prepare_extent_index(count);
self.refresh_visible_extents(state, theme, count, cross);
let main = self.extent_index.borrow().total_extent() as f32;
let size = match &self.axis {
LazyStackAxisConfig::Vertical { .. } => Size::new(sample.width, main),
LazyStackAxisConfig::Horizontal { .. } => Size::new(main, sample.height),
};
ViewDimensions::new(size)
}
pub(super) fn flush(
&self,
renderer: &mut HydrolysisRenderer,
ctx: RenderContext,
_env: &Environment,
) {
let count = self.views.len().get();
if count == 0 {
return;
}
#[cfg(feature = "accessibility")]
let container_scope = self.accessibility_container_env.as_ref().map(|env| {
renderer.push_accessibility_owner(&self.accessibility_identity);
let scope = renderer.begin_accessibility_container(
transformed_rect(ctx.hit_transform, ctx.bounds),
env,
);
renderer.pop_accessibility_owner();
scope
});
let cross = match &self.axis {
LazyStackAxisConfig::Vertical { .. } => Some(ctx.bounds.width() as f32),
LazyStackAxisConfig::Horizontal { .. } => Some(ctx.bounds.height() as f32),
};
let theme = renderer.theme();
self.ensure_estimate(&mut renderer.state, &theme, cross);
self.prepare_extent_index(count);
let total_extent_before = self.extent_index.borrow().total_extent();
self.item_cache.borrow_mut().begin_frame();
let visible = renderer
.lazy
.lazy_viewport_stack
.last()
.map(|viewport| {
(ctx.transform.inverse() * viewport.transform).transform_rect_bbox(viewport.bounds)
})
.unwrap_or(ctx.bounds);
let (visible_start, visible_end) = match &self.axis {
LazyStackAxisConfig::Vertical { .. } => {
(visible.y0 - ctx.bounds.y0, visible.y1 - ctx.bounds.y0)
}
LazyStackAxisConfig::Horizontal { .. }
if self.axis.direction().get().is_right_to_left() =>
{
(
ctx.bounds.x0 + ctx.bounds.x1 - visible.x1,
ctx.bounds.x0 + ctx.bounds.x1 - visible.x0,
)
}
LazyStackAxisConfig::Horizontal { .. } => {
(visible.x0 - ctx.bounds.x0, visible.x1 - ctx.bounds.x0)
}
};
let spacing = self.spacing();
let window = self
.extent_index
.borrow()
.visible_window(visible_start, visible_end);
*self.visible_range.borrow_mut() = window.start..window.end;
let mut cursor = window.leading_offset;
for index in window.start..window.end {
let id = self
.views
.get_id(index)
.unwrap_or_else(|| panic!("hydrolysis LazyStack item {index} has no id"));
let proposal = self.item_proposal(cross);
let (size, stretch) = {
let env = &self.env;
let views = &self.views;
let mut cache = self.item_cache.borrow_mut();
cache
.entry(id, || {
let view = views.get_view(index).unwrap_or_else(|| {
panic!("hydrolysis LazyStack failed to materialize item {index}")
});
normalize_layout_view(view, env)
})
.patch_and_measure(renderer, env, proposal)
};
let child_rect = place_lazy_stack_item(&self.axis, stretch, size, ctx.bounds, cursor);
let extent = match &self.axis {
LazyStackAxisConfig::Vertical { .. } => child_rect.height(),
LazyStackAxisConfig::Horizontal { .. } => child_rect.width(),
};
self.extent_index.borrow_mut().set_measured(index, extent);
{
let env = &self.env;
let views = &self.views;
let mut cache = self.item_cache.borrow_mut();
let subview = cache.entry(id, || {
let view = views.get_view(index).unwrap_or_else(|| {
panic!("hydrolysis LazyStack failed to materialize item {index}")
});
normalize_layout_view(view, env)
});
subview.flush_in_rect(renderer, ctx, env, proposal, child_rect);
}
cursor += extent;
if index + 1 < count {
cursor += spacing;
}
}
self.item_cache.borrow_mut().end_frame();
#[cfg(feature = "accessibility")]
if let Some(container_scope) = container_scope {
renderer.push_accessibility_owner(&self.accessibility_identity);
renderer.end_accessibility_container(container_scope);
renderer.pop_accessibility_owner();
}
let total_extent_after = self.extent_index.borrow().total_extent();
if (total_extent_after - total_extent_before).abs() > 0.5 {
renderer.request_refresh();
}
}
#[cfg(feature = "accessibility")]
pub(super) fn emit_accessibility(&self, renderer: &mut SemanticCore) {
let count = self.views.len().get();
if count == 0 {
return;
}
let container_scope = self.accessibility_container_env.as_ref().map(|env| {
renderer.push_accessibility_owner(&self.accessibility_identity);
let scope = renderer.begin_accessibility_container_semantic(env);
renderer.pop_accessibility_owner();
scope
});
self.item_cache.borrow_mut().begin_frame();
for index in 0..count {
let id = self
.views
.get_id(index)
.unwrap_or_else(|| panic!("hydrolysis LazyStack item {index} has no id"));
let env = &self.env;
let views = &self.views;
let mut cache = self.item_cache.borrow_mut();
let subview = cache.entry(id, || {
let view = views.get_view(index).unwrap_or_else(|| {
panic!("hydrolysis LazyStack failed to materialize item {index}")
});
normalize_layout_view(view, env)
});
subview.emit_accessibility(renderer, env);
}
self.item_cache.borrow_mut().end_frame();
if let Some(container_scope) = container_scope {
renderer.push_accessibility_owner(&self.accessibility_identity);
renderer.end_accessibility_container(container_scope);
renderer.pop_accessibility_owner();
}
}
}