Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

at main 8.1 kB View raw
1use core::fmt; 2 3use accesskit::{Node, TreeUpdate}; 4use bone_app::WindowPoint; 5use thiserror::Error; 6 7use crate::scenario::{NodeRef, PointerTarget}; 8 9#[derive(Clone, Debug, PartialEq, Eq)] 10pub struct Candidate { 11 pub node: NodeRef, 12 pub label: String, 13} 14 15impl fmt::Display for Candidate { 16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 17 write!(f, "{} {:?}", self.node, self.label) 18 } 19} 20 21fn list(candidates: &[Candidate]) -> String { 22 if candidates.is_empty() { 23 return "none".to_owned(); 24 } 25 candidates 26 .iter() 27 .map(ToString::to_string) 28 .collect::<Vec<_>>() 29 .join(", ") 30} 31 32#[derive(Clone, Debug, PartialEq, Eq, Error)] 33pub enum ResolveError { 34 #[error("node {node} not in tree; labeled nodes: {}", list(.candidates))] 35 NodeNotFound { 36 node: NodeRef, 37 candidates: Vec<Candidate>, 38 }, 39 #[error("node {node} has no bounds")] 40 NodeWithoutBounds { node: NodeRef }, 41 #[error("no node label contains {label:?}; labeled nodes: {}", list(.candidates))] 42 LabelNotFound { 43 label: String, 44 candidates: Vec<Candidate>, 45 }, 46 #[error("label {label:?} is ambiguous; matches: {}", list(.matches))] 47 LabelAmbiguous { 48 label: String, 49 matches: Vec<Candidate>, 50 }, 51} 52 53pub fn resolve_target( 54 target: &PointerTarget, 55 tree: &TreeUpdate, 56) -> Result<WindowPoint, ResolveError> { 57 match target { 58 PointerTarget::At { x, y } => Ok(WindowPoint::new(*x, *y)), 59 PointerTarget::Node(node) => resolve_node(*node, tree), 60 PointerTarget::Label(label) => resolve_label(label, tree), 61 } 62} 63 64fn resolve_node(node: NodeRef, tree: &TreeUpdate) -> Result<WindowPoint, ResolveError> { 65 tree.nodes 66 .iter() 67 .find(|(id, _)| *id == node.node_id()) 68 .ok_or_else(|| ResolveError::NodeNotFound { 69 node, 70 candidates: labeled(tree), 71 }) 72 .and_then(|(_, n)| center(node, n)) 73} 74 75fn resolve_label(needle: &str, tree: &TreeUpdate) -> Result<WindowPoint, ResolveError> { 76 let needle_fold = needle.to_lowercase(); 77 let matches: Vec<(NodeRef, &Node, String)> = tree 78 .nodes 79 .iter() 80 .filter_map(|(id, n)| { 81 NodeRef::from_node_id(*id) 82 .zip(n.label()) 83 .filter(|(_, label)| label.to_lowercase().contains(&needle_fold)) 84 .map(|(node, label)| (node, n, label.to_owned())) 85 }) 86 .collect(); 87 let exact: Vec<&(NodeRef, &Node, String)> = matches 88 .iter() 89 .filter(|(_, _, label)| label.to_lowercase() == needle_fold) 90 .collect(); 91 match (matches.as_slice(), exact.as_slice()) { 92 ([], _) => Err(ResolveError::LabelNotFound { 93 label: needle.to_owned(), 94 candidates: labeled(tree), 95 }), 96 ([(node, n, _)], _) | (_, [(node, n, _)]) => center(*node, n), 97 _ => Err(ResolveError::LabelAmbiguous { 98 label: needle.to_owned(), 99 matches: matches 100 .iter() 101 .map(|(node, _, label)| Candidate { 102 node: *node, 103 label: label.clone(), 104 }) 105 .collect(), 106 }), 107 } 108} 109 110fn center(node: NodeRef, n: &Node) -> Result<WindowPoint, ResolveError> { 111 n.bounds() 112 .map(|r| WindowPoint::new((r.x0 + r.x1) * 0.5, (r.y0 + r.y1) * 0.5)) 113 .ok_or(ResolveError::NodeWithoutBounds { node }) 114} 115 116fn labeled(tree: &TreeUpdate) -> Vec<Candidate> { 117 tree.nodes 118 .iter() 119 .filter_map(|(id, n)| { 120 NodeRef::from_node_id(*id) 121 .zip(n.label()) 122 .map(|(node, label)| Candidate { 123 node, 124 label: label.to_owned(), 125 }) 126 }) 127 .collect() 128} 129 130#[cfg(test)] 131mod tests { 132 use accesskit::{NodeId, Rect, Role, Tree, TreeId}; 133 134 use super::*; 135 136 fn button(label: &str, bounds: Option<Rect>) -> Node { 137 let mut node = Node::new(Role::Button); 138 node.set_label(label); 139 if let Some(rect) = bounds { 140 node.set_bounds(rect); 141 } 142 node 143 } 144 145 fn rect(x0: f64, y0: f64, x1: f64, y1: f64) -> Rect { 146 Rect { x0, y0, x1, y1 } 147 } 148 149 fn tree(nodes: Vec<(NodeId, Node)>) -> TreeUpdate { 150 TreeUpdate { 151 nodes, 152 tree: Some(Tree::new(NodeId(1))), 153 tree_id: TreeId::ROOT, 154 focus: NodeId(1), 155 } 156 } 157 158 fn node_ref(raw: u64) -> NodeRef { 159 match NodeRef::from_node_id(NodeId(raw)) { 160 Some(node) => node, 161 None => panic!("raw must be non-zero"), 162 } 163 } 164 165 fn shell() -> TreeUpdate { 166 tree(vec![ 167 (NodeId(1), Node::new(Role::Window)), 168 ( 169 NodeId(2), 170 button("Extrude", Some(rect(10.0, 20.0, 30.0, 40.0))), 171 ), 172 ( 173 NodeId(3), 174 button("Extrude Cut", Some(rect(40.0, 20.0, 60.0, 40.0))), 175 ), 176 ( 177 NodeId(4), 178 button("Fillet", Some(rect(70.0, 20.0, 90.0, 40.0))), 179 ), 180 ]) 181 } 182 183 fn resolved(target: &PointerTarget, tree: &TreeUpdate) -> WindowPoint { 184 match resolve_target(target, tree) { 185 Ok(point) => point, 186 Err(e) => panic!("resolve failed: {e}"), 187 } 188 } 189 190 #[test] 191 fn coordinates_pass_through() { 192 let point = resolved(&PointerTarget::At { x: 12.5, y: 7.0 }, &shell()); 193 assert_eq!(point, WindowPoint::new(12.5, 7.0)); 194 } 195 196 #[test] 197 fn unique_label_substring_hits_bounds_center() { 198 let point = resolved(&PointerTarget::Label("fill".to_owned()), &shell()); 199 assert_eq!(point, WindowPoint::new(80.0, 30.0)); 200 } 201 202 #[test] 203 fn exact_label_wins_over_substring_ambiguity() { 204 let point = resolved(&PointerTarget::Label("Extrude".to_owned()), &shell()); 205 assert_eq!(point, WindowPoint::new(20.0, 30.0)); 206 } 207 208 #[test] 209 fn ambiguous_label_names_the_matches() { 210 let needle = PointerTarget::Label("trude".to_owned()); 211 match resolve_target(&needle, &shell()) { 212 Err(ResolveError::LabelAmbiguous { matches, .. }) => { 213 let labels: Vec<&str> = matches.iter().map(|c| c.label.as_str()).collect(); 214 assert_eq!(labels, vec!["Extrude", "Extrude Cut"]); 215 } 216 other => panic!("expected ambiguity, got {other:?}"), 217 } 218 } 219 220 #[test] 221 fn missing_label_names_the_candidates() { 222 let needle = PointerTarget::Label("Revolve".to_owned()); 223 match resolve_target(&needle, &shell()) { 224 Err(ref err @ ResolveError::LabelNotFound { ref candidates, .. }) => { 225 assert_eq!(candidates.len(), 3); 226 let message = err.to_string(); 227 assert!(message.contains("Extrude Cut"), "message was: {message}"); 228 assert!(message.contains("Fillet"), "message was: {message}"); 229 } 230 other => panic!("expected not-found, got {other:?}"), 231 } 232 } 233 234 #[test] 235 fn node_ref_resolves_to_bounds_center() { 236 let point = resolved(&PointerTarget::Node(node_ref(3)), &shell()); 237 assert_eq!(point, WindowPoint::new(50.0, 30.0)); 238 } 239 240 #[test] 241 fn missing_node_ref_names_the_candidates() { 242 match resolve_target(&PointerTarget::Node(node_ref(99)), &shell()) { 243 Err(ResolveError::NodeNotFound { candidates, .. }) => { 244 assert_eq!(candidates.len(), 3); 245 } 246 other => panic!("expected not-found, got {other:?}"), 247 } 248 } 249 250 #[test] 251 fn node_without_bounds_is_an_error() { 252 let update = tree(vec![ 253 (NodeId(1), Node::new(Role::Window)), 254 (NodeId(2), button("Ghost", None)), 255 ]); 256 match resolve_target(&PointerTarget::Label("Ghost".to_owned()), &update) { 257 Err(ResolveError::NodeWithoutBounds { node }) => assert_eq!(node, node_ref(2)), 258 other => panic!("expected missing bounds, got {other:?}"), 259 } 260 } 261}