1use crate::{Control, Kind, NodeId, Ui};
18
19#[derive(Debug, Clone, PartialEq, Eq)]
21pub enum Step {
22 Name(String),
24 Text(String),
26 Kind(String),
28 Reference(String),
30 Any,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
36pub struct Query(pub Vec<Step>);
37
38impl Query {
39 pub fn parse(text: &str) -> Result<Query, String> {
41 let mut steps = Vec::new();
42 let mut chars = text.trim().chars().peekable();
43 while chars.peek().is_some() {
44 let mut word = String::new();
45 let mut quoted = false;
46 while let Some(&c) = chars.peek() {
47 chars.next();
48 if c == '"' {
49 quoted = !quoted;
50 } else if c.is_whitespace() && !quoted {
51 break;
52 } else {
53 word.push(c);
54 }
55 }
56 if quoted {
57 return Err(format!("unclosed quote in {text:?}"));
58 }
59 while chars.peek().is_some_and(|c| c.is_whitespace()) {
60 chars.next();
61 }
62 if word.is_empty() {
63 continue;
64 }
65 steps.push(match word.split_once(':') {
66 Some(("text", t)) => Step::Text(t.to_string()),
67 Some(("kind", k)) => Step::Kind(k.to_ascii_lowercase()),
68 Some(("ref", r)) => Step::Reference(r.to_string()),
69 Some((other, _)) if !other.is_empty() && !other.contains('/') => {
70 return Err(format!("unknown step {word:?} (use text:, kind: or ref:)"));
71 }
72 _ if word == "*" => Step::Any,
73 _ => Step::Name(word),
74 });
75 }
76 if steps.is_empty() {
77 return Err("empty query".into());
78 }
79 Ok(Query(steps))
80 }
81}
82
83#[derive(Debug, Clone, PartialEq)]
85pub struct ElementInfo {
86 pub id: NodeId,
88 pub name: String,
90 pub reference: String,
92 pub kind: &'static str,
94 pub text: String,
96 pub rect: [i32; 4],
98 pub enabled: bool,
100 pub checked: Option<bool>,
102 pub value: Option<f32>,
104}
105
106impl Ui {
107 pub fn query(&self, roots: &[NodeId], query: &Query) -> Vec<NodeId> {
110 let mut found: Vec<NodeId> = roots
111 .iter()
112 .copied()
113 .filter(|&r| self.nodes[r].visible())
114 .collect();
115 for (i, step) in query.0.iter().enumerate() {
116 let mut next = Vec::new();
117 for &start in &found {
118 self.collect(start, step, i == 0, &mut next);
120 }
121 let mut seen = std::collections::BTreeSet::new();
122 next.retain(|id| seen.insert(*id));
123 found = next;
124 }
125 found
126 }
127
128 fn collect(&self, id: NodeId, step: &Step, include_self: bool, out: &mut Vec<NodeId>) {
129 if !self.nodes[id].visible() {
130 return;
131 }
132 if include_self && self.matches(id, step) {
133 out.push(id);
134 }
135 for c in self.child_ids(id) {
136 self.collect(c, step, true, out);
137 }
138 }
139
140 fn matches(&self, id: NodeId, step: &Step) -> bool {
141 let n = &self.nodes[id];
142 match step {
143 Step::Name(name) => &n.name == name,
144 Step::Text(text) => !text.is_empty() && self.text_of(id).eq_ignore_ascii_case(text),
145 Step::Kind(kind) => self.kind_name(id) == kind,
146 Step::Reference(r) => n
147 .reference
148 .to_ascii_lowercase()
149 .contains(&r.to_ascii_lowercase()),
150 Step::Any => true,
151 }
152 }
153
154 pub fn kind_name(&self, id: NodeId) -> &'static str {
158 match &self.nodes[id].kind {
159 Kind::Group => "group",
160 Kind::Ribbon { .. } => "ribbon",
161 Kind::Border { .. } => "border",
162 Kind::Image { .. } => "image",
163 Kind::AnimatedImage { .. } => "animation",
164 Kind::Button { .. } => "button",
165 Kind::Text(_) => "text",
166 Kind::Label { .. } => "label",
167 Kind::Viewport => "viewport",
168 Kind::List(_) => "list",
169 Kind::Lines { .. } => "lines",
170 Kind::Picture { .. } => "picture",
171 Kind::Unsupported(_) => "unsupported",
172 Kind::Control(c) => control_name(c),
173 }
174 }
175
176 pub fn text_of(&self, id: NodeId) -> String {
178 match &self.nodes[id].kind {
179 Kind::Text(t) => t.text.clone(),
180 Kind::Label { text, .. } => text.text.clone(),
181 Kind::Button { content, .. } => content.map(|c| self.text_of(c)).unwrap_or_default(),
182 Kind::Control(Control::Edit(e)) => e.text.text.clone(),
183 _ => String::new(),
184 }
185 }
186
187 pub fn info(&self, id: NodeId) -> ElementInfo {
189 let n = &self.nodes[id];
190 let control = matches!(n.kind, Kind::Control(_));
191 ElementInfo {
192 id,
193 name: n.name.clone(),
194 reference: n.reference.clone(),
195 kind: self.kind_name(id),
196 text: self.text_of(id),
197 rect: n.rect,
198 enabled: n.usable(),
199 checked: matches!(n.kind, Kind::Control(Control::Check { .. }))
200 .then(|| self.checked(id)),
201 value: if control { self.value(id) } else { None },
202 }
203 }
204}
205
206fn control_name(c: &Control) -> &'static str {
208 match c {
209 Control::Slider(_) => "slider",
210 Control::Scroll { .. } => "scroll",
211 Control::Progress { .. } => "progress",
212 Control::Edit(_) => "edit",
213 Control::Graph { .. } => "graph",
214 Control::Table { .. } => "table",
215 Control::Layout(_) => "layout",
216 Control::Tabs { .. } => "tabs",
217 Control::Clip { .. } => "clip",
218 Control::Container { .. } => "container",
219 Control::Check { .. } => "check",
220 Control::Combo { .. } => "combo",
221 Control::Spinner { .. } => "spinner",
222 Control::Page { .. } => "page",
223 Control::Radio { .. } => "radio",
224 Control::Document { .. } => "document",
225 }
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231
232 #[test]
233 fn parses_names_kinds_text_and_references() {
234 let q =
235 Query::parse(r#"ref:Dialog/Options kind:button text:"New Game" buttonOk *"#).unwrap();
236 assert_eq!(
237 q.0,
238 vec![
239 Step::Reference("Dialog/Options".into()),
240 Step::Kind("button".into()),
241 Step::Text("New Game".into()),
242 Step::Name("buttonOk".into()),
243 Step::Any,
244 ]
245 );
246 }
247
248 #[test]
249 fn rejects_bad_queries() {
250 assert!(Query::parse("").is_err());
251 assert!(Query::parse("text:\"open").is_err());
252 assert!(Query::parse("colour:red").is_err());
253 }
254}