use serde::{Deserialize, Serialize};
use crate::{
BoundsSet, ElementId, ErrorCode, FileMakerError, LayoutTrace, Provenance, Rect,
ResolvedElement, ResolvedPage, ResolvedScene, ResourceLimits, Result, Unit,
};
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct ElementInspection {
pub page: usize,
pub id: ElementId,
pub bounds: BoundsSet,
pub collidable: bool,
pub layer: String,
pub z_index: i32,
pub provenance: Provenance,
pub layout_trace: LayoutTrace,
pub table_fragment: Option<usize>,
pub table_rows: Option<usize>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct PageInspection {
pub page: usize,
pub role: crate::PageRole,
pub exclusions: Vec<String>,
pub regions: Vec<String>,
pub safe: Option<Rect>,
pub elements: usize,
pub occupied: Option<Rect>,
pub overflow: Vec<ElementId>,
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct LayoutExplanation {
pub id: ElementId,
pub page: usize,
pub bounds: BoundsSet,
pub measured: bool,
pub patch_count: usize,
pub components: Vec<String>,
pub source: String,
pub decisions: Vec<String>,
pub trace: LayoutTrace,
}
pub struct SceneInspector<'a> {
scene: &'a ResolvedScene,
}
impl<'a> SceneInspector<'a> {
#[must_use]
pub const fn new(scene: &'a ResolvedScene) -> Self {
Self { scene }
}
pub fn inspect_element(&self, id: &ElementId) -> Result<ElementInspection> {
let (page, element) = self.find(id)?;
Ok(ElementInspection {
page,
id: element.id.clone(),
bounds: element.bounds,
collidable: element.collidable,
layer: element.layer.clone(),
z_index: element.z_index,
provenance: element.provenance.clone(),
layout_trace: element.layout_trace.clone(),
table_fragment: element.table.as_ref().map(|table| table.index),
table_rows: element.table.as_ref().map(|table| table.rows.len()),
})
}
pub fn inspect_page(&self, page: usize) -> Result<PageInspection> {
let page_ref = self.page(page)?;
let page_bounds = Rect::new(
Unit::ZERO,
Unit::ZERO,
page_ref.size.width,
page_ref.size.height,
)?;
let mut occupied: Option<Rect> = None;
let mut overflow = Vec::new();
for element in &page_ref.elements {
occupied = Some(match occupied {
Some(bounds) => bounds.union(element.bounds.layout)?,
None => element.bounds.layout,
});
if !contains_rect(page_bounds, element.bounds.visual)? {
overflow.push(element.id.clone());
}
}
Ok(PageInspection {
page,
role: page_ref.role,
exclusions: page_ref
.exclusions
.iter()
.map(|exclusion| exclusion.name.clone())
.collect(),
regions: page_ref
.regions
.iter()
.map(|region| region.name.clone())
.collect(),
safe: page_ref
.page_template
.as_ref()
.map(crate::PageTemplate::safe_bounds)
.transpose()?,
elements: page_ref.elements.len(),
occupied,
overflow,
})
}
pub fn explain_layout(&self, id: &ElementId) -> Result<LayoutExplanation> {
let (page, element) = self.find(id)?;
let mut decisions = vec![format!(
"layout=({}, {}, {}, {})",
element.bounds.layout.origin.x.raw(),
element.bounds.layout.origin.y.raw(),
element.bounds.layout.size.width.raw(),
element.bounds.layout.size.height.raw()
)];
if element.bounds.intrinsic != element.bounds.layout {
decisions.push(format!(
"measurement intrinsic=({}, {}, {}, {}) constrained by layout",
element.bounds.intrinsic.origin.x.raw(),
element.bounds.intrinsic.origin.y.raw(),
element.bounds.intrinsic.size.width.raw(),
element.bounds.intrinsic.size.height.raw()
));
} else {
decisions.push("measurement matched proposed layout".to_owned());
}
decisions.push(format!(
"source x={:?} y={:?} width={:?} height={:?} region={:?}",
element.layout_trace.geometry.x,
element.layout_trace.geometry.y,
element.layout_trace.geometry.width,
element.layout_trace.geometry.height,
element.layout_trace.geometry.region
));
if !element.layout_trace.geometry.anchors.is_empty() {
decisions.push(format!(
"anchors={:?}",
element.layout_trace.geometry.anchors
));
}
decisions.push(format!(
"collision enabled={} bounds={:?} policy={:?} reflowed={}",
element.layout_trace.collision_policy.enabled,
element.layout_trace.collision_policy.bounds,
element.layout_trace.collision_policy.resolution,
element.layout_trace.reflowed
));
if element.layout_trace.initial_page != page {
decisions.push(format!(
"page/reflow moved placement from page {} to page {}",
element.layout_trace.initial_page + 1,
page + 1
));
} else {
decisions.push(format!("page/reflow retained page {}", page + 1));
}
if let Some(table) = &element.table {
decisions.push(format!(
"table fragment {} contains {} rows",
table.index,
table.rows.len()
));
}
Ok(LayoutExplanation {
id: element.id.clone(),
page,
bounds: element.bounds,
measured: element.bounds.intrinsic != element.bounds.layout,
patch_count: element.provenance.patches.len(),
components: element.provenance.components.clone(),
source: element.provenance.source.clone(),
decisions,
trace: element.layout_trace.clone(),
})
}
pub fn query_free_regions(&self, page: usize, minimum: crate::Size) -> Result<Vec<Rect>> {
self.query_free_regions_bounded(page, minimum, &ResourceLimits::default())
}
pub fn query_free_regions_bounded(
&self,
page: usize,
minimum: crate::Size,
limits: &ResourceLimits,
) -> Result<Vec<Rect>> {
crate::resolved::validate_scene_contract(self.scene, limits)?;
let page_ref = self.page(page)?;
let mut budget = crate::diagnostic_budget::DiagnosticBudget::new(limits)?;
let mut free = vec![Rect::new(
Unit::ZERO,
Unit::ZERO,
page_ref.size.width,
page_ref.size.height,
)?];
for element in page_ref
.elements
.iter()
.filter(|element| element.collidable)
{
let mut next = Vec::new();
for region in free {
budget.operation()?;
let pieces = subtract(region, element.bounds.collision)?;
budget.retained(next.len().saturating_add(pieces.len()))?;
next.extend(pieces);
}
free = next;
}
for exclusion in &page_ref.exclusions {
let mut next = Vec::new();
for region in free {
budget.operation()?;
let pieces = subtract(region, exclusion.bounds)?;
budget.retained(next.len().saturating_add(pieces.len()))?;
next.extend(pieces);
}
free = next;
}
free.retain(|region| {
region.size.width >= minimum.width && region.size.height >= minimum.height
});
free.sort_by_key(|region| {
(
region.origin.y,
region.origin.x,
region.size.height,
region.size.width,
)
});
Ok(free)
}
fn find(&self, id: &ElementId) -> Result<(usize, &ResolvedElement)> {
self.scene
.pages
.iter()
.find_map(|page| {
page.elements
.iter()
.find(|element| &element.id == id)
.map(|element| (page.index, element))
})
.ok_or_else(|| inspect_error(format!("element `{}` was not found", id.as_str())))
}
fn page(&self, index: usize) -> Result<&ResolvedPage> {
self.scene
.pages
.get(index)
.ok_or_else(|| inspect_error(format!("page {index} was not found")))
}
}
pub(crate) fn subtract(region: Rect, occupied: Rect) -> Result<Vec<Rect>> {
let Some(overlap) = region.intersection(occupied)? else {
return Ok(vec![region]);
};
let mut result = Vec::with_capacity(4);
if overlap.origin.y > region.origin.y {
result.push(Rect::new(
region.origin.x,
region.origin.y,
region.size.width,
overlap.origin.y.checked_sub(region.origin.y)?,
)?);
}
if overlap.bottom()? < region.bottom()? {
result.push(Rect::new(
region.origin.x,
overlap.bottom()?,
region.size.width,
region.bottom()?.checked_sub(overlap.bottom()?)?,
)?);
}
if overlap.origin.x > region.origin.x {
result.push(Rect::new(
region.origin.x,
overlap.origin.y,
overlap.origin.x.checked_sub(region.origin.x)?,
overlap.size.height,
)?);
}
if overlap.right()? < region.right()? {
result.push(Rect::new(
overlap.right()?,
overlap.origin.y,
region.right()?.checked_sub(overlap.right()?)?,
overlap.size.height,
)?);
}
Ok(result)
}
fn contains_rect(outer: Rect, inner: Rect) -> Result<bool> {
Ok(inner.origin.x >= outer.origin.x
&& inner.origin.y >= outer.origin.y
&& inner.right()? <= outer.right()?
&& inner.bottom()? <= outer.bottom()?)
}
fn inspect_error(message: impl Into<String>) -> FileMakerError {
FileMakerError::new(ErrorCode::LayoutInvalid, message)
}