use crate::node::{Node, NodeData};
use crate::tree::NodeTree;
use blitz_traits::node_id::NodeId;
use kurbo::Rect;
use rustc_hash::{FxHashMap, FxHashSet};
const MAX_REGIONS: usize = 32;
#[derive(Debug, Clone, Default)]
pub struct PaintDamage {
pub generation: u64,
regions: Vec<Rect>,
}
impl PaintDamage {
pub fn is_empty(&self) -> bool {
self.regions.is_empty()
}
pub fn regions(&self) -> &[Rect] {
&self.regions
}
pub fn intersects(&self, region: Rect) -> bool {
self.regions.iter().any(|changed| {
changed.x0 < region.x1
&& region.x0 < changed.x1
&& changed.y0 < region.y1
&& region.y0 < changed.y1
})
}
fn add(&mut self, rect: Rect) {
if rect.width() <= 0.0 || rect.height() <= 0.0 {
return;
}
if self.regions.len() < MAX_REGIONS {
self.regions.push(rect);
return;
}
let collapsed = self
.regions
.iter()
.copied()
.fold(rect, |acc, existing| acc.union(existing));
self.regions.clear();
self.regions.push(collapsed);
}
}
#[derive(Debug, Default)]
pub(crate) struct PaintDamageTracker {
enabled: bool,
previous: FxHashMap<NodeId, Rect>,
repainted: FxHashSet<NodeId>,
recording: bool,
damage: PaintDamage,
}
impl PaintDamageTracker {
pub(crate) fn set_enabled(&mut self, enabled: bool) {
if self.enabled == enabled {
return;
}
self.enabled = enabled;
self.previous.clear();
self.repainted.clear();
self.damage = PaintDamage::default();
}
pub(crate) fn is_enabled(&self) -> bool {
self.enabled
}
pub(crate) fn damage(&self) -> &PaintDamage {
&self.damage
}
pub(crate) fn begin_resolve(&mut self) {
if !self.enabled {
return;
}
self.repainted.clear();
self.recording = true;
self.damage.regions.clear();
}
pub(crate) fn end_propagation(&mut self) {
self.recording = false;
}
pub(crate) fn note_own_damage(&mut self, node_id: NodeId) {
if self.enabled && self.recording {
self.repainted.insert(node_id);
}
}
pub(crate) fn capture(&mut self, nodes: &NodeTree) {
if !self.enabled {
return;
}
let mut origins = FxHashMap::with_capacity_and_hasher(nodes.len(), Default::default());
let mut current = FxHashMap::with_capacity_and_hasher(nodes.len(), Default::default());
for (node_id, node) in nodes.iter() {
let Some(rect) = painted_box(nodes, node_id, node, &mut origins) else {
continue;
};
match self.previous.remove(&node_id) {
Some(old) if old == rect => {}
Some(old) => {
self.damage.add(old);
self.damage.add(rect);
}
None => self.damage.add(rect),
}
current.insert(node_id, rect);
}
for (_, old) in self.previous.drain() {
self.damage.add(old);
}
self.previous = current;
let mut repainted = std::mem::take(&mut self.repainted);
for node_id in repainted.iter() {
if let Some(rect) = painting_ancestor(nodes, *node_id, &self.previous) {
self.damage.add(rect);
}
}
repainted.clear();
self.repainted = repainted;
if !self.damage.is_empty() {
self.damage.generation = self.damage.generation.wrapping_add(1);
}
}
}
fn painted_box(
nodes: &NodeTree,
node_id: NodeId,
node: &Node,
origins: &mut FxHashMap<NodeId, (f64, f64)>,
) -> Option<Rect> {
if !has_layout(node) {
return None;
}
let (x, y) = absolute_origin(nodes, node_id, origins);
let size = node.final_layout().size;
Some(Rect::new(
x,
y,
x + f64::from(size.width),
y + f64::from(size.height),
))
}
fn has_layout(node: &Node) -> bool {
matches!(
node.data,
NodeData::Element(_) | NodeData::AnonymousBlock(_) | NodeData::Document(_)
)
}
fn painting_ancestor(
nodes: &NodeTree,
node_id: NodeId,
boxes: &FxHashMap<NodeId, Rect>,
) -> Option<Rect> {
let mut current = node_id;
loop {
let node = nodes.get(current)?;
if let Some(rect) = boxes.get(¤t) {
if rect.width() > 0.0 && rect.height() > 0.0 {
return Some(*rect);
}
}
current = node.layout_parent.get().or(node.parent)?;
}
}
fn absolute_origin(
nodes: &NodeTree,
node_id: NodeId,
origins: &mut FxHashMap<NodeId, (f64, f64)>,
) -> (f64, f64) {
if let Some(cached) = origins.get(&node_id) {
return *cached;
}
let Some(node) = nodes.get(node_id) else {
return (0.0, 0.0);
};
let (mut x, mut y) = if has_layout(node) {
let layout = node.final_layout();
(f64::from(layout.location.x), f64::from(layout.location.y))
} else {
(0.0, 0.0)
};
if let Some(parent_id) = node.layout_parent.get() {
let (parent_x, parent_y) = absolute_origin(nodes, parent_id, origins);
if let Some(parent) = nodes.get(parent_id) {
let scroll = parent.scroll_offset();
x += parent_x - scroll.x;
y += parent_y - scroll.y;
} else {
x += parent_x;
y += parent_y;
}
}
origins.insert(node_id, (x, y));
(x, y)
}