1use cvkg_core::layout::Rect;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
5pub enum LayoutModality {
6 #[default]
8 Pointer,
9 Touch,
11 AccessibilityZoom,
13}
14
15impl LayoutModality {
16 pub fn min_tap_target(self) -> f32 {
18 match self {
19 LayoutModality::Pointer => 0.0,
20 LayoutModality::Touch => 44.0,
21 LayoutModality::AccessibilityZoom => 44.0,
22 }
23 }
24
25 pub fn spacing_multiplier(self) -> f32 {
27 match self {
28 LayoutModality::Pointer => 1.0,
29 LayoutModality::Touch => 1.25,
30 LayoutModality::AccessibilityZoom => 2.0,
31 }
32 }
33
34 pub fn adapt_size(self, size: cvkg_core::Size) -> cvkg_core::Size {
36 let min = self.min_tap_target();
37 cvkg_core::Size {
38 width: size.width.max(min),
39 height: size.height.max(min),
40 }
41 }
42}
43
44#[derive(Debug, Clone, PartialEq)]
46pub struct FocusCandidate {
47 pub hash: u64,
49 pub rect: Rect,
51 pub tab_index: Option<i32>,
53}
54
55pub fn compute_focus_order(mut candidates: Vec<FocusCandidate>) -> Vec<u64> {
57 let mut explicit: Vec<FocusCandidate> = candidates
58 .iter()
59 .filter(|c| c.tab_index.map_or(false, |t| t > 0))
60 .cloned()
61 .collect();
62 candidates.retain(|c| !c.tab_index.map_or(false, |t| t > 0));
63
64 explicit.sort_by(|a, b| {
65 let ta = a.tab_index.unwrap_or(i32::MAX);
66 let tb = b.tab_index.unwrap_or(i32::MAX);
67 ta.cmp(&tb)
68 .then_with(|| a.rect.y.total_cmp(&b.rect.y))
69 .then_with(|| a.rect.x.total_cmp(&b.rect.x))
70 });
71
72 let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
73 candidates.sort_by(|a, b| {
74 row_bucket(&a.rect)
75 .cmp(&row_bucket(&b.rect))
76 .then_with(|| a.rect.x.total_cmp(&b.rect.x))
77 });
78
79 explicit
80 .into_iter()
81 .chain(candidates)
82 .map(|c| c.hash)
83 .collect()
84}
85
86pub fn validate_reading_order(order: &[FocusCandidate]) -> Result<(), String> {
88 let natural: Vec<&FocusCandidate> = order
89 .iter()
90 .filter(|c| !c.tab_index.map_or(false, |t| t > 0))
91 .collect();
92
93 let row_bucket = |r: &Rect| (r.y / 8.0).floor() as i32;
94 for window in natural.windows(2) {
95 let a = window[0];
96 let b = window[1];
97 if row_bucket(&b.rect) < row_bucket(&a.rect) {
98 return Err(format!(
99 "reading order violation: view 0x{:X} (y≈{:.1}) precedes view 0x{:X} (y≈{:.1}) visually",
100 b.hash, b.rect.y, a.hash, a.rect.y
101 ));
102 }
103 if row_bucket(&a.rect) == row_bucket(&b.rect) && b.rect.x < a.rect.x - 1.0 {
104 return Err(format!(
105 "reading order violation: view 0x{:X} (x≈{:.1}) precedes view 0x{:X} (x≈{:.1}) on same row",
106 b.hash, b.rect.x, a.hash, a.rect.x
107 ));
108 }
109 }
110 Ok(())
111}