fission_test/linter.rs
1use fission_ir::{CoreIR, LayoutOp, Op, WidgetId};
2use fission_layout::{LayoutRect, LayoutSnapshot};
3
4#[derive(Debug)]
5pub enum LayoutViolation {
6 Overflow {
7 parent: WidgetId,
8 child: WidgetId,
9 parent_rect: LayoutRect,
10 child_rect: LayoutRect,
11 },
12 ZeroSizeInteractive {
13 node: WidgetId,
14 rect: LayoutRect,
15 role: String,
16 },
17 // Add more violations as needed
18}
19
20pub struct LayoutLinter<'a> {
21 ir: &'a CoreIR,
22 snapshot: &'a LayoutSnapshot,
23}
24
25impl<'a> LayoutLinter<'a> {
26 pub fn new(ir: &'a CoreIR, snapshot: &'a LayoutSnapshot) -> Self {
27 Self { ir, snapshot }
28 }
29
30 pub fn check(&self) -> Vec<LayoutViolation> {
31 let mut violations = Vec::new();
32 if let Some(root) = self.ir.root {
33 self.check_recursive(root, &mut violations);
34 }
35 violations
36 }
37
38 fn check_recursive(&self, node_id: WidgetId, violations: &mut Vec<LayoutViolation>) {
39 let node = self.ir.nodes.get(&node_id).expect("Node missing in IR");
40 let geom = self.snapshot.get_node_geometry(node_id);
41
42 if let Some(geom) = geom {
43 // Check Interactive Visibility
44 if let Op::Semantics(s) = &node.op {
45 if s.focusable || !s.actions.entries.is_empty() {
46 if geom.rect.width() < 1.0 || geom.rect.height() < 1.0 {
47 violations.push(LayoutViolation::ZeroSizeInteractive {
48 node: node_id,
49 rect: geom.rect,
50 role: format!("{:?}", s.role),
51 });
52 }
53 }
54 }
55
56 // Check Containment (if not Scroll or Overflow allowed)
57 let allows_overflow = matches!(node.op, Op::Layout(LayoutOp::Scroll { .. }));
58
59 // Note: Absolute children (Positioned, AbsoluteFill) are relative to nearest positioned ancestor,
60 // not necessarily direct parent. For simple checking, we might skip them or check against the appropriate ancestor.
61 // For now, let's check direct flow children.
62
63 for child_id in &node.children {
64 if let Some(_child_geom) = self.snapshot.get_node_geometry(*child_id) {
65 if let Some(child_node) = self.ir.nodes.get(child_id) {
66 let is_absolute = matches!(
67 child_node.op,
68 Op::Layout(LayoutOp::Positioned { .. })
69 | Op::Layout(LayoutOp::AbsoluteFill)
70 );
71
72 if !allows_overflow && !is_absolute {
73 // Check if child is roughly inside parent (allow small float error)
74 // A child can be smaller than parent, but shouldn't exceed bounds unless allowed.
75 // Overflow is visible by default in our layout model.
76 // But "Toast border smaller than content" is an overflow issue we want to catch.
77 // Let's flag if child is strictly larger than parent in dimensions?
78 // Or if child rect is not contained in parent rect?
79 // Many UIs intentionally overflow (shadows, badges).
80 // Let's focus on "Content rect > Parent Border Rect" for Containers.
81 }
82 }
83 self.check_recursive(*child_id, violations);
84 }
85 }
86 }
87 }
88}