Skip to main content

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}