use crate::layout::Axis;
use crate::renderer::PaintContext;
use crate::transform::Transform;
use crate::tree::WidgetId;
use crate::widgets::Rect;
pub struct ChildPaintOptions {
pub scroll_offset: (f32, f32),
pub cull_rect: Option<Rect>,
pub cache_requires_full_visibility: bool,
pub children_sorted_along: Option<Axis>,
pub children_reach: f32,
}
impl Default for ChildPaintOptions {
fn default() -> Self {
Self {
scroll_offset: (0.0, 0.0),
cull_rect: None,
cache_requires_full_visibility: false,
children_sorted_along: None,
children_reach: 0.0,
}
}
}
fn visible_window<'a>(
children: &'a [WidgetId],
ctx: &mut PaintContext,
opts: &ChildPaintOptions,
) -> &'a [WidgetId] {
let tree = ctx.tree();
if children.len() < 2 {
return children;
}
let (Some(cull), Some(axis)) = (opts.cull_rect, opts.children_sorted_along) else {
if opts.cull_rect.is_some() {
crate::render_stats::record_paint_window_declined(children.len() as u64);
}
return children;
};
let window = &children[tree.window_of(children, axis, cull.span(axis), opts.children_reach)];
crate::render_stats::record_paint_window(children.len() as u64, window.len() as u64);
if window.len() < children.len() {
ctx.mark_partial();
}
window
}
pub fn paint_children(ctx: &mut PaintContext, children: &[WidgetId], opts: &ChildPaintOptions) {
for &child_id in visible_window(children, ctx, opts) {
ctx.paint_child(child_id, opts);
}
}
impl PaintContext<'_> {
pub fn paint_child(&mut self, child_id: WidgetId, opts: &ChildPaintOptions) {
let (offset_x, offset_y) = opts.scroll_offset;
let child_bounds = self.tree().get_bounds(child_id).unwrap_or_default();
let child_local = Rect::new(0.0, 0.0, child_bounds.width, child_bounds.height);
let (child_x, child_y) = (child_bounds.x, child_bounds.y);
let child_position = Transform::translate(child_x - offset_x, child_y - offset_y);
if let Some(ref cull) = opts.cull_rect {
let drawn = child_bounds.outset(self.tree().paint_overflow(child_id));
if !cull.intersects(&drawn) {
crate::render_stats::record_paint_child_culled();
self.mark_partial();
return;
}
}
if self.reuse_cached(
child_id,
(child_x, child_y),
child_local,
child_position,
opts,
) {
return;
}
let cull_in_child_space = opts.cull_rect.map(|cull| {
Rect::new(cull.x - child_x, cull.y - child_y, cull.width, cull.height)
});
self.paint_child_placed(child_id, child_local, child_position, cull_in_child_space);
}
pub(crate) fn paint_child_placed(
&mut self,
child_id: WidgetId,
child_local: Rect,
placement: Transform,
cull_rect: Option<Rect>,
) {
let mut child_ctx = self.add_child(child_id, child_local);
child_ctx.set_transform(placement);
if let Some(cull) = cull_rect {
child_ctx.set_cull_rect(cull);
}
child_ctx.tree().with_widget(child_id, |child| {
crate::reactive::with_signal_tracking(child_id, crate::jobs::JobType::Paint, || {
child.paint(&mut child_ctx)
})
});
crate::render_stats::record_paint_child_painted();
}
fn reuse_cached(
&mut self,
child_id: WidgetId,
(child_x, child_y): (f32, f32),
child_local: Rect,
child_position: Transform,
opts: &ChildPaintOptions,
) -> bool {
if opts.cache_requires_full_visibility {
let Some(ref cull) = opts.cull_rect else {
return false;
};
let fully_visible = child_x >= cull.x
&& child_x + child_local.width <= cull.x + cull.width
&& child_y >= cull.y
&& child_y + child_local.height <= cull.y + cull.height;
if !fully_visible {
return false;
}
}
if self.tree().needs_paint(child_id) {
return false;
}
let Some(cached) = self.tree().cached_paint(child_id) else {
return false;
};
if cached.bounds.width != child_local.width || cached.bounds.height != child_local.height {
return false;
}
debug_assert!(
cached.parent_position.is_pure_translation(),
"a cached child was placed with something other than a translation"
);
if cached.parent_position == child_position {
self.add_child_rc(std::rc::Rc::clone(cached));
} else {
let mut reused = (**cached).clone();
let pos = &cached.parent_position;
let user_part =
Transform::translate(-pos.tx(), -pos.ty()).then(&cached.local_transform);
reused.local_transform = child_position.then(&user_part);
reused.parent_position = child_position;
reused.bounds = child_local;
reused.repainted.set(false);
self.add_child_rc(std::rc::Rc::new(reused));
}
crate::render_stats::record_paint_child_cached();
true
}
}
#[cfg(test)]
mod culling_a_dirty_child {
use crate::layout::{Constraints, Flex};
use crate::renderer::RenderNode;
use crate::tree::{Tree, WidgetId};
use crate::widgets::{Color, Scroll, Widget, container};
const MARKED: f32 = 60.0;
fn drew_the_marked_row(node: &RenderNode) -> bool {
(node.children.is_empty() && (node.bounds.width - MARKED).abs() < 0.01)
|| node.children.iter().any(|c| drew_the_marked_row(c))
}
fn settle(tree: &mut Tree, id: WidgetId) {
tree.clear_needs_paint(id);
for child in tree.get_children(id).to_vec() {
settle(tree, child);
}
}
#[test]
fn a_write_does_not_paint_a_row_nobody_can_see() {
crate::reactive::owner::create_root_owner();
let mut rows: Vec<_> = (0..20)
.map(|_| {
container()
.width(120.0)
.height(24.0)
.background(Color::BLUE)
})
.collect();
rows[4] = container()
.width(MARKED)
.height(24.0)
.background(Color::rgb(1.0, 0.0, 0.0));
let mut tree = Tree::new();
let root = tree.register(Box::new(
container()
.width(200.0)
.height(120.0)
.scroll(Scroll::vertical())
.layout(Flex::column().spacing(8.0))
.children(rows),
) as Box<dyn Widget>);
tree.with_widget_mut(root, |w, id, t| w.register_children(t, id));
tree.layout_widget(root, Constraints::new(0.0, 0.0, 400.0, 120.0));
let mut first = RenderNode::new(root.as_u64());
tree.paint_widget(root, &mut first, 1.0);
assert!(
!drew_the_marked_row(&first),
"row four starts just past the fold, which is what makes this the case"
);
settle(&mut tree, root);
let row = tree.get_children(root)[4];
tree.mark_needs_paint(row);
let mut second = RenderNode::new(root.as_u64());
tree.paint_widget(root, &mut second, 1.0);
assert!(
!drew_the_marked_row(&second),
"a write to a row outside the viewport painted it anyway, into a \
region the scroller clips away"
);
}
}