ferroui_inspect/
target.rs1use crate::{display_value, Highlight, Inspector, NodeId, TreeKind};
8use ferroui_base::{FerroObject, FerroPropertyRegistry, Rect, Ref, StyledElement, Visual, WeakRef};
9use ferroui_controls::TopLevel;
10use std::cell::RefCell;
11use std::collections::HashSet;
12use std::rc::Rc;
13
14pub type InspectResult<T> = Result<T, String>;
15
16#[derive(Clone, Debug)]
17pub struct Node {
18 pub id: NodeId,
19 pub parent: Option<NodeId>,
20 pub name: String,
21 pub attributes: Vec<String>,
24 pub children: Vec<NodeId>,
25}
26
27#[derive(Clone, Debug)]
28pub struct Property {
29 pub name: String,
30 pub value: String,
31 pub priority: String,
32 pub local: bool,
34 pub read_only: bool,
35}
36
37#[derive(Clone, Debug)]
38pub struct MatchedStyle {
39 pub selector: String,
40 pub properties: Vec<(String, String)>,
41}
42
43pub use crate::BoxModel;
44
45pub struct Target {
46 inspector: Rc<RefCell<Inspector>>,
47 root: WeakRef<TopLevel>,
48 pub tree_kind: TreeKind,
50 members: HashSet<NodeId>,
51 highlight: Option<Highlight>,
52}
53
54impl Target {
55 pub fn new(root: &Ref<TopLevel>) -> Self {
57 Self::with_inspector(Inspector::shared(), root)
58 }
59
60 pub fn with_inspector(inspector: Rc<RefCell<Inspector>>, root: &Ref<TopLevel>) -> Self {
61 Self {
62 inspector,
63 root: root.downgrade(),
64 tree_kind: TreeKind::Visual,
65 members: HashSet::new(),
66 highlight: None,
67 }
68 }
69
70 pub fn root(&self) -> InspectResult<Ref<TopLevel>> {
71 self.root
72 .upgrade()
73 .filter(|root| root.platform_impl().is_some())
74 .ok_or_else(|| "Target has closed".into())
75 }
76
77 pub fn id_for(&mut self, element: &Ref<StyledElement>) -> NodeId {
78 self.inspector.borrow_mut().node_id(element)
79 }
80
81 pub fn snapshot(&mut self) -> InspectResult<Vec<Node>> {
82 let root = self.root()?.upcast::<FerroObject>();
83 let tree = if self.tree_kind == TreeKind::Logical {
84 TreeKind::Logical
85 } else {
86 TreeKind::Visual
87 };
88 let mut inspector = self.inspector.borrow_mut();
89 let mut result = Vec::new();
90 let mut pending = vec![(root, None)];
91 let mut seen = HashSet::new();
92 while let Some((object, parent)) = pending.pop() {
93 let Some(element) = object.cast::<StyledElement>() else {
94 continue;
95 };
96 let id = inspector.node_id(&object);
97 if !seen.insert(id) {
98 continue;
99 }
100 let children: Vec<_> = inspector
101 .children(&object, tree)
102 .into_iter()
103 .filter(|c| c.cast::<StyledElement>().is_some())
104 .collect();
105 let child_ids = children.iter().map(|c| inspector.node_id(c)).collect();
106 let mut attributes = vec!["class".into(), element.classes().snapshot().join(" ")];
107 if let Some(name) = element.name() {
108 attributes.extend(["id".into(), name.clone(), "Name".into(), name]);
109 }
110 for name in ["Text", "IsEnabled", "IsVisible"] {
111 if let Some(p) =
112 FerroPropertyRegistry::instance().find_registered(element.get_type(), name)
113 {
114 attributes.extend([
115 name.into(),
116 display_value(Some(&element.get_value_untyped(p))),
117 ]);
118 }
119 }
120 result.push(Node {
121 id,
122 parent,
123 name: element.get_type().name().into(),
124 attributes,
125 children: child_ids,
126 });
127 pending.extend(children.into_iter().rev().map(|c| (c, Some(id))));
128 }
129 self.members = seen;
130 Ok(result)
131 }
132
133 pub fn element(&mut self, id: NodeId) -> InspectResult<Ref<StyledElement>> {
135 self.snapshot()?;
136 if !self.members.contains(&id) {
137 return Err(format!("Unknown or detached node {id}"));
138 }
139 self.inspector
140 .borrow()
141 .resolve(id)?
142 .cast::<StyledElement>()
143 .ok_or_else(|| format!("Node {id} is not an element"))
144 }
145
146 pub fn query(&mut self, scope: Option<NodeId>, query: &str) -> InspectResult<Vec<NodeId>> {
149 let nodes = self.snapshot()?;
150 if let Some(scope) = scope {
151 self.element(scope)?;
152 }
153 let inspector = self.inspector.borrow();
154 let selector = inspector.parse_selector(query)?;
155 let mut descendants = HashSet::new();
156 let mut found = Vec::new();
157 for node in nodes {
158 let in_scope = scope.is_none()
159 || node.parent == scope
160 || node.parent.is_some_and(|p| descendants.contains(&p));
161 if in_scope {
162 descendants.insert(node.id);
163 }
164 if in_scope && Some(node.id) != scope {
165 let Some(element) = inspector
166 .resolve(node.id)
167 .ok()
168 .and_then(|o| o.cast::<StyledElement>())
169 else {
170 continue;
171 };
172 if selector
173 .as_ref()
174 .is_none_or(|s| s.match_(&element, None, false).is_match())
175 {
176 found.push(node.id);
177 }
178 }
179 }
180 Ok(found)
181 }
182
183 pub fn properties(&mut self, id: NodeId) -> InspectResult<Vec<Property>> {
184 let element = self.element(id)?;
185 Ok(Inspector::properties(&element)
186 .into_iter()
187 .map(|p| Property {
188 value: display_value(Some(&p.value)),
189 priority: p.priority_name(),
190 local: p.is_local(),
191 read_only: p.read_only,
192 name: p.name,
193 })
194 .collect())
195 }
196
197 pub fn set_attribute(&mut self, id: NodeId, name: &str, value: &str) -> InspectResult<()> {
200 let element = self.element(id)?;
201 match name {
202 "id" | "Name" => Err("Name is immutable after styling".into()),
203 "class" => {
204 let classes = element.classes();
205 if value.split_whitespace().any(|c| c.starts_with(':')) {
207 return Err("Pseudo-classes are read-only".into());
208 }
209 for class in classes.snapshot().iter().filter(|c| !c.starts_with(':')) {
210 classes.remove(class);
211 }
212 classes.add_range(value.split_whitespace());
213 Ok(())
214 }
215 _ => {
216 Inspector::set_property(&element, name, Some(Rc::new(value.to_owned()))).map(|_| ())
217 }
218 }
219 }
220
221 pub fn matched_styles(&mut self, id: NodeId) -> InspectResult<Vec<MatchedStyle>> {
223 let element = self.element(id)?;
224 Ok(Inspector::styles(&element)
225 .into_iter()
226 .filter(|frame| frame.active)
227 .map(|frame| MatchedStyle {
228 selector: frame.selector.unwrap_or(frame.kind),
229 properties: frame
230 .setters
231 .into_iter()
232 .map(|(name, value)| (name, display_value(value.as_ref())))
233 .collect(),
234 })
235 .collect())
236 }
237
238 pub fn box_model(&mut self, id: NodeId) -> InspectResult<BoxModel> {
239 let visual = self
240 .element(id)?
241 .cast::<Visual>()
242 .ok_or("Node is not a visual")?;
243 Inspector::box_model(&visual, &self.root()?.upcast())
244 }
245
246 pub fn bounds(&self) -> InspectResult<Rect> {
248 Ok(Rect::from_size(self.root()?.bounds().size()))
249 }
250
251 pub fn screenshot(&self) -> InspectResult<Vec<u8>> {
253 Ok(Inspector::screenshot(&self.root()?.upcast(), 1.0)?.data)
254 }
255
256 pub fn hide_highlight(&mut self) {
257 self.highlight = None;
258 }
259
260 pub fn highlight(&mut self, id: NodeId) -> InspectResult<()> {
261 self.hide_highlight();
262 let visual = self
263 .element(id)?
264 .cast::<Visual>()
265 .ok_or("Node is not a visual")?;
266 self.highlight = Some(self.inspector.borrow_mut().highlight(&visual)?);
267 Ok(())
268 }
269}