Skip to main content

ferroui_inspect/
target.rs

1//! One top level (a window or a popup) as the target of the Chrome DevTools Protocol endpoint
2//! (`ferroui-devtools`). A target is a view of the shared [`Inspector`]: the node IDs, selectors,
3//! properties, styles, layout boxes, screenshots and highlights are the inspector's; the target
4//! keeps its root, the tree it shows (visual or logical), which nodes are in that tree, and its
5//! highlight. Every operation checks that the node is in the current tree, so a control that has
6//! been detached cannot be driven through its ID until it is attached again. UI thread only.
7use 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    /// Name and value pairs: `class`, `id` and `Name` (the name of the element), `Text`,
22    /// `IsEnabled` and `IsVisible`.
23    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    /// Whether the value is a local value (CSS's inline style).
33    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    /// The tree of the snapshots: visual or logical.
49    pub tree_kind: TreeKind,
50    members: HashSet<NodeId>,
51    highlight: Option<Highlight>,
52}
53
54impl Target {
55    /// A target on the shared inspector of this thread.
56    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    /// The element of a node of the current tree.
134    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    /// The nodes below `scope` (the whole tree without one) that match the selector, in document
147    /// order. Uses the framework's selector syntax and matching rules (including logical ancestry).
148    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    /// `class` replaces the classes (not the pseudo-classes); any other name sets the local value
198    /// of a property from text. The name of an element cannot change after it has been styled.
199    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                // Pseudo-classes are framework state, not editable CSS classes.
206                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    /// The active frames that apply to the node, with their setters.
222    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    /// The bounds of the top level, from its origin.
247    pub fn bounds(&self) -> InspectResult<Rect> {
248        Ok(Rect::from_size(self.root()?.bounds().size()))
249    }
250
251    /// A PNG of the whole top level.
252    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}