use std::any::Any;
use std::collections::HashMap;
use kurbo::{Point, Size};
use super::RenderRoot;
use crate::layout::BoxConstraints;
use crate::view::{View, WidgetId};
use crate::widget::LayoutCtx;
#[derive(Default)]
pub(super) struct LayoutAudit {
pub(super) enabled: bool,
pub(super) mismatches: u64,
frame: u64,
last_logged: HashMap<(&'static str, Option<String>), u64>,
}
struct AuditGeom {
id: WidgetId,
type_name: &'static str,
label: Option<String>,
origin: Point,
size: Size,
}
impl<State: 'static, V: View<State>> RenderRoot<State, V> {
pub fn layout(&mut self, window_size: Size) -> Size {
self.layout_inner(window_size, None)
}
pub fn layout_with_text(&mut self, window_size: Size, text_ctx: &mut dyn Any) -> Size {
self.layout_inner(window_size, Some(text_ctx))
}
fn layout_inner(&mut self, window_size: Size, text_ctx: Option<&mut dyn Any>) -> Size {
if self.window_size != window_size {
self.bump_layout_epoch();
}
self.window_size = window_size;
if self.overlay_pod_dirty() {
self.bump_layout_epoch();
}
if self.root_id.is_none() {
return Size::ZERO;
}
if !(self.layout_audit.enabled && self.layout_skip) {
return self.layout_pass(window_size, text_ctx, !self.layout_skip);
}
let mut text_ctx = text_ctx;
self.layout_pass(window_size, text_ctx.as_deref_mut(), false);
let before = self.audit_snapshot();
let size = self.layout_pass(window_size, text_ctx, true);
let after = self.audit_snapshot();
self.audit_compare(&before, &after);
size
}
fn audit_snapshot(&self) -> Vec<AuditGeom> {
let nodes = self.tree.inspect();
let origins: HashMap<WidgetId, Point> =
nodes.iter().map(|n| (n.id, n.bounds.origin())).collect();
nodes
.into_iter()
.map(|n| {
let parent = n
.parent
.and_then(|p| origins.get(&p).copied())
.unwrap_or(Point::ZERO);
AuditGeom {
id: n.id,
type_name: n.type_name,
label: n.debug_label.map(|l| l.into_owned()),
origin: Point::new(n.bounds.x0 - parent.x, n.bounds.y0 - parent.y),
size: n.bounds.size(),
}
})
.collect()
}
fn audit_compare(&mut self, before: &[AuditGeom], after: &[AuditGeom]) {
const TOLERANCE: f64 = 1e-3;
let frame = self.layout_audit.frame;
self.layout_audit.frame += 1;
let by_id: HashMap<WidgetId, &AuditGeom> = before.iter().map(|g| (g.id, g)).collect();
for now in after {
let Some(was) = by_id.get(&now.id) else {
continue;
};
let differs = (was.origin.x - now.origin.x).abs() > TOLERANCE
|| (was.origin.y - now.origin.y).abs() > TOLERANCE
|| (was.size.width - now.size.width).abs() > TOLERANCE
|| (was.size.height - now.size.height).abs() > TOLERANCE;
if !differs {
continue;
}
self.layout_audit.mismatches += 1;
let key = (now.type_name, now.label.clone());
let due = self
.layout_audit
.last_logged
.get(&key)
.is_none_or(|last| frame.saturating_sub(*last) >= 60);
if due {
self.layout_audit.last_logged.insert(key, frame);
log::warn!(
"layout-audit: {} (label {:?}) changed geometry without signalling layout: skipped pass origin {:?} size {:?}, forced pass origin {:?} size {:?}",
now.type_name,
now.label,
was.origin,
was.size,
now.origin,
now.size,
);
}
}
}
fn layout_pass(
&mut self,
window_size: Size,
text_ctx: Option<&mut dyn Any>,
force: bool,
) -> Size {
let Some(root_id) = self.root_id else {
return Size::ZERO;
};
let bc = BoxConstraints::loose(window_size);
let theme = self.theme.as_deref();
let insets = self.insets;
let layout_epoch = self.layout_epoch;
let Some(pod) = self.tree.pod_mut(root_id) else {
return Size::ZERO;
};
let mut ctx = LayoutCtx::with_resources(text_ctx, theme);
ctx.set_window_insets(insets);
ctx.set_window_size(window_size);
ctx.set_layout_epoch(layout_epoch);
ctx.set_layout_force(force);
let size = pod.widget_mut().layout(&mut ctx, &bc);
pod.set_layout(Point::ZERO, size);
size
}
}