1use crate::input::Input;
2use base64::Engine;
3use ferroui_base::input::InputElement;
4use ferroui_base::interactivity::RoutingStrategies;
5use ferroui_base::reactive::{Disposable, IDisposable};
6use ferroui_base::{Point, Rect, Ref, StyledElement};
7use ferroui_controls::automation::peers::{AutomationPeer, ControlAutomationPeer};
8use ferroui_controls::{Control, TopLevel};
9use ferroui_inspect::target::{Node, Target};
10use ferroui_inspect::{NodeId, TreeKind};
11use serde_json::{json, Value};
12use std::cell::RefCell;
13use std::collections::{HashMap, HashSet};
14use std::rc::Rc;
15
16pub(crate) type Result<T> = std::result::Result<T, Error>;
17#[derive(Debug)]
18pub(crate) struct Error {
19 pub code: i32,
20 pub message: String,
21}
22impl Error {
23 pub fn params(message: impl Into<String>) -> Self {
24 Self {
25 code: -32602,
26 message: message.into(),
27 }
28 }
29 pub fn server(message: impl Into<String>) -> Self {
30 Self {
31 code: -32000,
32 message: message.into(),
33 }
34 }
35 pub fn method(method: &str) -> Self {
36 Self {
37 code: -32601,
38 message: format!("Unsupported method: {method}"),
39 }
40 }
41}
42impl From<String> for Error {
43 fn from(value: String) -> Self {
44 Self::server(value)
45 }
46}
47pub(crate) fn string<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
48 p[key]
49 .as_str()
50 .ok_or_else(|| Error::params(format!("{key} must be a string")))
51}
52pub(crate) fn number(p: &Value, key: &str) -> Result<f64> {
53 p[key]
54 .as_f64()
55 .filter(|n| n.is_finite())
56 .ok_or_else(|| Error::params(format!("{key} must be a finite number")))
57}
58fn node_id(p: &Value) -> Result<NodeId> {
59 p["nodeId"]
60 .as_u64()
61 .or_else(|| p["backendNodeId"].as_u64())
62 .filter(|n| *n > 0)
63 .ok_or_else(|| Error::params("nodeId is required"))
64}
65fn depth(p: &Value, default: i64) -> Result<i64> {
66 let d = p
67 .get("depth")
68 .map(|d| {
69 d.as_i64()
70 .ok_or_else(|| Error::params("depth must be an integer"))
71 })
72 .transpose()?
73 .unwrap_or(default);
74 if d < -1 || d == 0 || d > 256 {
75 Err(Error::params("depth must be -1 or 1..256"))
76 } else {
77 Ok(if d == -1 { 256 } else { d })
78 }
79}
80fn quad(r: Rect) -> Value {
81 json!([
82 r.x,
83 r.y,
84 r.x + r.width,
85 r.y,
86 r.x + r.width,
87 r.y + r.height,
88 r.x,
89 r.y + r.height
90 ])
91}
92fn rect(r: Rect) -> Value {
93 json!({"x":r.x,"y":r.y,"width":r.width,"height":r.height})
94}
95fn style(properties: Vec<Value>) -> Value {
96 json!({"cssProperties":properties,"shorthandEntries":[]})
97}
98fn dom_node(id: NodeId, nodes: &HashMap<NodeId, &Node>, depth: i64) -> Value {
99 let n = nodes[&id];
100 let mut value = json!({"nodeId":id,"backendNodeId":id,"nodeType":1,"nodeName":n.name,"localName":n.name,"nodeValue":"","childNodeCount":n.children.len(),"attributes":n.attributes});
101 if let Some(parent) = n.parent {
102 value["parentId"] = json!(parent);
103 }
104 if depth > 0 {
105 value["children"] = n
106 .children
107 .iter()
108 .filter(|id| nodes.contains_key(id))
109 .map(|id| dom_node(*id, nodes, depth - 1))
110 .collect();
111 }
112 value
113}
114
115pub struct CdpSession {
118 pub(crate) inspector: Target,
119 input: Input,
120 pub(crate) enabled: HashSet<String>,
121 target_id: String,
122 inspect_subscriptions: Vec<Rc<dyn IDisposable>>,
123 picked: Rc<RefCell<Vec<(Point, bool)>>>,
124 events: Vec<Value>,
125 last_tree: Option<Vec<Node>>,
126 pub(crate) log_cursor: u64,
127}
128impl CdpSession {
129 pub fn new(root: &Ref<TopLevel>, target_id: impl Into<String>) -> Self {
130 Self {
131 inspector: Target::new(root),
132 input: Input::default(),
133 enabled: HashSet::new(),
134 target_id: target_id.into(),
135 inspect_subscriptions: Vec::new(),
136 picked: Rc::new(RefCell::new(Vec::new())),
137 events: Vec::new(),
138 last_tree: None,
139 log_cursor: 0,
140 }
141 }
142 pub fn handle(&mut self, message: &str) -> Vec<Value> {
143 let request: Value = match serde_json::from_str(message) {
144 Ok(v) => v,
145 Err(e) => {
146 return vec![json!({"id":null,"error":{"code":-32700,"message":e.to_string()}})]
147 }
148 };
149 let id = request["id"].clone();
150 let result = if !request["id"].is_u64() {
151 Err(Error {
152 code: -32600,
153 message: "Request requires a nonnegative integer id".into(),
154 })
155 } else {
156 string(&request, "method").and_then(|method| {
157 self.command(method, request.get("params").unwrap_or(&json!({})))
158 })
159 };
160 let response = response(id, result);
161 if request["method"] == "DOM.requestChildNodes"
162 || request["method"] == "DOM.pushNodesByBackendIdsToFrontend"
163 || request["method"] == "DOM.pushNodeByPathToFrontend"
164 {
165 let mut messages = std::mem::take(&mut self.events);
166 messages.push(response);
167 messages
168 } else {
169 let mut messages = vec![response];
170 messages.append(&mut self.events);
171 messages
172 }
173 }
174 pub(crate) fn command(&mut self, method: &str, p: &Value) -> Result<Value> {
175 if !p.is_object() {
176 return Err(Error::params("params must be an object"));
177 }
178 self.inspector.root()?;
179 if let Some(domain) = method.strip_suffix(".enable").filter(|d| {
180 matches!(
181 *d,
182 "DOM" | "CSS" | "Page" | "Runtime" | "Log" | "Accessibility" | "Overlay"
183 )
184 }) {
185 let newly_enabled = self.enabled.insert(domain.into());
186 if domain == "Runtime" && newly_enabled {
187 self.events.push(json!({"method":"Runtime.executionContextCreated","params":{"context":{"id":1,"origin":"ferroui://","name":"FerroUI (no JavaScript engine)","uniqueId":format!("ferroui-{}",self.target_id),"auxData":{"isDefault":true,"frameId":self.target_id}}}}));
188 }
189 return Ok(json!({}));
190 }
191 if let Some(domain) = method.strip_suffix(".disable").filter(|d| {
192 matches!(
193 *d,
194 "DOM" | "CSS" | "Page" | "Runtime" | "Log" | "Accessibility" | "Overlay"
195 )
196 }) {
197 self.enabled.remove(domain);
198 if domain == "Overlay" {
199 self.stop_inspecting();
200 self.inspector.hide_highlight();
201 }
202 return Ok(json!({}));
203 }
204 match method {
205 "DOM.getDocument" => {
206 self.enabled.insert("DOM".into());
207 let depth = depth(p,1)?;
208 let tree = match p["tree"].as_str() { None => self.inspector.tree_kind, Some("visual") => TreeKind::Visual, Some("logical") => TreeKind::Logical, _ => return Err(Error::params("tree must be visual or logical")) };
209 self.inspector.tree_kind = tree;
210 let nodes = self.inspector.snapshot()?;
211 self.last_tree = Some(nodes.clone());
212 let map = nodes.iter().map(|n| (n.id,n)).collect();
213 let children: Vec<_> = nodes.first().map(|n| dom_node(n.id,&map,depth-1)).into_iter().collect();
214 Ok(json!({"root":{"nodeId":1,"backendNodeId":1,"nodeType":9,"nodeName":"#document","localName":"","nodeValue":"","documentURL":self.url(),"baseURL":self.url(),"xmlVersion":"1.0","childNodeCount":1,"children":children}}))
215 }
216 "DOM.describeNode" | "DOM.requestChildNodes" => {
217 let id = node_id(p)?; let depth = depth(p,1)?;
218 let nodes = self.inspector.snapshot()?;
219 let map: HashMap<_,_> = nodes.iter().map(|n| (n.id,n)).collect();
220 if id == 1 {
221 if method == "DOM.describeNode" { return self.command("DOM.getDocument",p).map(|v| json!({"node":v["root"]})); }
222 let children: Vec<_> = nodes.first().map(|n| dom_node(n.id,&map,depth-1)).into_iter().collect();
223 self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":1,"nodes":children}}));
224 } else {
225 self.inspector.element(id)?;
226 if method == "DOM.describeNode" { return Ok(json!({"node":dom_node(id,&map,depth)})); }
227 let children: Vec<_> = map[&id].children.iter().map(|id| dom_node(*id,&map,depth-1)).collect();
228 self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":id,"nodes":children}}));
229 }
230 Ok(json!({}))
231 }
232 "DOM.pushNodeByPathToFrontend" => {
233 let path = string(p, "path")?;
234 let parts: Vec<_> = path.split(',').collect();
235 if parts.is_empty() || parts.len() % 2 != 0 || parts.len() > 512 { return Err(Error::params("Invalid node path")); }
236 let nodes = self.inspector.snapshot()?;
237 let map: HashMap<_,_> = nodes.iter().map(|n|(n.id,n)).collect();
238 let mut children: Vec<_> = nodes.first().map(|n|n.id).into_iter().collect();
239 let mut id = 0;
240 for pair in parts.as_chunks::<2>().0.iter() {
241 let index: usize = pair[0].parse().map_err(|_|Error::params("Invalid node path index"))?;
242 let Some(node) = children.get(index).and_then(|id|map.get(id)) else { return Ok(json!({"nodeId":0})); };
243 if node.name != pair[1] { return Ok(json!({"nodeId":0})); }
244 id = node.id; children = node.children.clone();
245 }
246 self.command("DOM.pushNodesByBackendIdsToFrontend", &json!({"backendNodeIds":[id]}))?;
247 Ok(json!({"nodeId":id}))
248 }
249 "DOM.pushNodesByBackendIdsToFrontend" => {
250 let requested = p["backendNodeIds"].as_array().ok_or_else(|| Error::params("backendNodeIds must be an array"))?;
251 let ids: Vec<u64> = requested.iter().map(|id| id.as_u64().ok_or_else(||Error::params("Invalid backendNodeId"))).collect::<Result<_>>()?;
252 let nodes = self.inspector.snapshot()?;
253 let map: HashMap<_,_> = nodes.iter().map(|n|(n.id,n)).collect();
254 let result: Vec<_> = ids.iter().map(|id| if *id == 1 || map.contains_key(id) { *id } else { 0 }).collect();
255 let mut parents = HashSet::new();
256 for id in &result {
257 let mut current = map.get(id).and_then(|n|n.parent);
258 while let Some(parent) = current { parents.insert(parent); current = map.get(&parent).and_then(|n|n.parent); }
259 }
260 if result.iter().any(|id|*id>1) {
261 let roots: Vec<_> = nodes.first().map(|n|dom_node(n.id,&map,0)).into_iter().collect();
262 self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":1,"nodes":roots}}));
263 for n in nodes.iter().filter(|n|parents.contains(&n.id)) {
264 let children: Vec<_> = n.children.iter().map(|id|dom_node(*id,&map,0)).collect();
265 self.events.push(json!({"method":"DOM.setChildNodes","params":{"parentId":n.id,"nodes":children}}));
266 }
267 }
268 Ok(json!({"nodeIds":result}))
269 }
270 "DOM.getAttributes" => {
271 let id = node_id(p)?;
272 let nodes = self.inspector.snapshot()?;
273 Ok(json!({"attributes":nodes.iter().find(|n|n.id == id).ok_or_else(||Error::server("Unknown node"))?.attributes}))
274 }
275 "DOM.querySelector" | "DOM.querySelectorAll" => {
276 let scope = node_id(p)?; let selector = string(p,"selector")?;
277 let ids = self.inspector.query(if scope==1 {None} else {Some(scope)},selector)?;
278 Ok(if method.ends_with("All") { json!({"nodeIds":ids}) } else { json!({"nodeId":ids.first().copied().unwrap_or(0)}) })
279 }
280 "DOM.setAttributeValue" => { self.inspector.set_attribute(node_id(p)?,string(p,"name")?,string(p,"value")?)?; Ok(json!({})) }
281 "DOM.getBoxModel" => {
282 let b = self.inspector.box_model(node_id(p)?)?;
283 Ok(json!({"model":{"content":quad(b.content),"padding":quad(b.padding),"border":quad(b.border),"margin":quad(b.margin),"width":b.border.width.round() as i64,"height":b.border.height.round() as i64}}))
284 }
285 "DOM.getContentQuads" => Ok(json!({"quads":[quad(self.inspector.box_model(node_id(p)?)?.content)]})),
286 "DOM.getNodeForLocation" => {
287 let root = self.inspector.root()?;
288 let visual = root.get_visual_at(Point::new(number(p,"x")?,number(p,"y")?)).ok_or_else(||Error::server("No node at point"))?;
289 let id = self.inspector.id_for(&visual.upcast::<StyledElement>());
290 Ok(json!({"nodeId":id,"backendNodeId":id,"frameId":self.target_id}))
291 }
292 "DOM.focus" => { self.inspector.element(node_id(p)?)?.cast::<InputElement>().ok_or_else(||Error::server("Node cannot receive input"))?.focus(); Ok(json!({})) }
293 "CSS.getComputedStyleForNode" => Ok(json!({"computedStyle":self.inspector.properties(node_id(p)?)?.iter().map(|p|json!({"name":p.name,"value":p.value})).collect::<Vec<_>>()})),
294 "CSS.getInlineStylesForNode" => {
295 let id = node_id(p)?;
296 Ok(json!({"inlineStyle":style(self.inspector.properties(id)?.into_iter().filter(|p|p.local).map(|p|json!({"name":p.name,"value":p.value})).collect())}))
297 }
298 "CSS.getMatchedStylesForNode" => {
299 let id = node_id(p)?;
300 let rules: Vec<_> = self.inspector.matched_styles(id)?.into_iter().map(|s| json!({"rule":{"selectorList":{"selectors":[{"text":s.selector}],"text":s.selector},"origin":"regular","style":style(s.properties.into_iter().map(|(name,value)|json!({"name":name,"value":value})).collect())},"matchingSelectors":[0]})).collect();
301 Ok(json!({"matchedCSSRules":rules,"inherited":[],"pseudoElements":[],"inlineStyle":style(self.inspector.properties(id)?.into_iter().filter(|p|p.local).map(|p|json!({"name":p.name,"value":p.value})).collect())}))
302 }
303 "Overlay.highlightNode" => { self.inspector.highlight(node_id(p)?)?; Ok(json!({})) }
304 "Overlay.hideHighlight" => { self.inspector.hide_highlight(); Ok(json!({})) }
305 "Overlay.setInspectMode" => { self.inspect_mode(string(p,"mode")?)?; Ok(json!({})) }
306 "Page.captureScreenshot" => {
307 if p["format"].as_str().is_some_and(|f|f!="png") || p.get("clip").is_some() { return Err(Error::params("Only full-window PNG screenshots are supported")); }
308 Ok(json!({"data":base64::engine::general_purpose::STANDARD.encode(self.inspector.screenshot()?)}))
309 }
310 "Page.getLayoutMetrics" => {
311 let bounds = self.inspector.bounds()?;
312 let layout = json!({"pageX":0,"pageY":0,"clientWidth":bounds.width,"clientHeight":bounds.height});
313 let visual = json!({"offsetX":0,"offsetY":0,"pageX":0,"pageY":0,"clientWidth":bounds.width,"clientHeight":bounds.height,"scale":1,"zoom":1});
314 Ok(json!({"layoutViewport":layout,"visualViewport":visual,"contentSize":rect(bounds),"cssLayoutViewport":layout,"cssVisualViewport":visual,"cssContentSize":rect(bounds)}))
315 }
316 "Page.getFrameTree" | "Page.getResourceTree" => Ok(json!({"frameTree":{"resources":[],"frame":{"id":self.target_id,"loaderId":self.target_id,"url":self.url(),"domainAndRegistry":"","securityOrigin":"ferroui://","mimeType":"application/xml","secureContextType":"Secure","crossOriginIsolatedContextType":"NotIsolated","gatedAPIFeatures":[]}}})),
317 "Accessibility.getFullAXTree" | "Accessibility.queryAXTree" => self.accessibility(p,method.ends_with("queryAXTree")),
318 "Runtime.evaluate" | "Runtime.callFunctionOn" => Err(Error::server("FerroUI has no JavaScript runtime. Use DOM, CSS and Input commands through a CDP session.")),
319 "Schema.getDomains" => {
320 let schema: Value = serde_json::from_str(include_str!("protocol.json")).expect("embedded protocol");
321 Ok(json!({"domains":schema["domains"].as_array().unwrap().iter().map(|d|json!({"name":d["domain"],"version":"1.3"})).collect::<Vec<_>>()}))
322 }
323 "Runtime.runIfWaitingForDebugger" => Ok(json!({})),
324 "Page.setLifecycleEventsEnabled" | "Page.setInterceptFileChooserDialog" | "Emulation.setFocusEmulationEnabled" | "Emulation.setEmulatedMedia" | "Network.enable" | "Network.disable" | "Target.setAutoAttach" => Ok(json!({})),
326 "Page.addScriptToEvaluateOnNewDocument" if p["source"].as_str() == Some("") => Ok(json!({"identifier":"empty-script"})),
327 "Page.addScriptToEvaluateOnNewDocument" => Err(Error::server("FerroUI has no document scripts")),
328 method if method.starts_with("Input.") => self.input.dispatch(&self.inspector.root()?,method,p),
329 _ => Err(Error::method(method)),
330 }
331 }
332 fn url(&self) -> String {
333 format!("ferroui://{}", self.target_id)
334 }
335 fn stop_inspecting(&mut self) {
336 for s in self.inspect_subscriptions.drain(..) {
337 s.dispose();
338 }
339 self.picked.borrow_mut().clear();
340 }
341 fn inspect_mode(&mut self, mode: &str) -> Result<()> {
342 if !matches!(mode, "none" | "searchForNode") {
343 return Err(Error::params("mode must be none or searchForNode"));
344 }
345 self.stop_inspecting();
346 self.inspector.hide_highlight();
347 if mode == "none" {
348 return Ok(());
349 }
350 let root = self.inspector.root()?;
351 let queue = self.picked.clone();
352 let weak = root.downgrade();
353 let token = root.add_handler_with(
354 InputElement::pointer_moved_event(),
355 move |_, e| {
356 if let Some(root) = weak.upgrade() {
357 let mut q = queue.borrow_mut();
358 if q.len() < 64 {
359 q.push((e.get_position(Some(&root)), false));
360 }
361 }
362 },
363 RoutingStrategies::TUNNEL,
364 true,
365 );
366 let weak = root.downgrade();
367 self.inspect_subscriptions.push(Disposable::create(move || {
368 if let Some(root) = weak.upgrade() {
369 root.remove_handler(InputElement::pointer_moved_event(), token);
370 }
371 }));
372 let queue = self.picked.clone();
373 let weak = root.downgrade();
374 let token = root.add_handler_with(
375 InputElement::pointer_pressed_event(),
376 move |_, e| {
377 if let Some(root) = weak.upgrade() {
378 let mut q = queue.borrow_mut();
379 if q.len() < 64 {
380 q.push((e.get_position(Some(&root)), true));
381 }
382 e.set_handled(true);
383 }
384 },
385 RoutingStrategies::TUNNEL,
386 true,
387 );
388 let weak = root.downgrade();
389 self.inspect_subscriptions.push(Disposable::create(move || {
390 if let Some(root) = weak.upgrade() {
391 root.remove_handler(InputElement::pointer_pressed_event(), token);
392 }
393 }));
394 Ok(())
395 }
396 pub fn poll(&mut self) -> Vec<Value> {
398 let points = std::mem::take(&mut *self.picked.borrow_mut());
399 for (point, pressed) in points {
400 if let Ok(root) = self.inspector.root() {
401 if let Some(visual) = root.get_visual_at(point) {
402 let id = self.inspector.id_for(&visual.upcast::<StyledElement>());
403 let _ = self.inspector.highlight(id);
404 self.events.push(if pressed { json!({"method":"Overlay.inspectNodeRequested","params":{"backendNodeId":id}}) } else { json!({"method":"Overlay.nodeHighlightRequested","params":{"nodeId":id}}) });
405 }
406 }
407 }
408 if self.enabled.contains("DOM") {
409 if let Ok(nodes) = self.inspector.snapshot() {
410 if let Some(previous) = &self.last_tree {
411 let topology_changed = previous.len() != nodes.len()
412 || previous.iter().zip(&nodes).any(|(a, b)| {
413 a.id != b.id
414 || a.parent != b.parent
415 || a.name != b.name
416 || a.children != b.children
417 });
418 if topology_changed {
419 self.events
420 .push(json!({"method":"DOM.documentUpdated","params":{}}));
421 } else {
422 for (before, after) in previous.iter().zip(&nodes) {
423 let old: HashMap<_, _> = before
424 .attributes
425 .as_chunks::<2>()
426 .0
427 .iter()
428 .map(|p| (&p[0], &p[1]))
429 .collect();
430 let new: HashMap<_, _> = after
431 .attributes
432 .as_chunks::<2>()
433 .0
434 .iter()
435 .map(|p| (&p[0], &p[1]))
436 .collect();
437 for (name, value) in &new {
438 if old.get(name) != Some(value) {
439 self.events.push(json!({"method":"DOM.attributeModified","params":{"nodeId":after.id,"name":name,"value":value}}));
440 }
441 }
442 for name in old.keys().filter(|name| !new.contains_key(*name)) {
443 self.events.push(json!({"method":"DOM.attributeRemoved","params":{"nodeId":after.id,"name":name}}));
444 }
445 }
446 }
447 }
448 self.last_tree = Some(nodes);
449 }
450 }
451 std::mem::take(&mut self.events)
452 }
453 fn accessibility(&mut self, p: &Value, query: bool) -> Result<Value> {
454 let root: Ref<Control> =
455 if query && (p.get("nodeId").is_some() || p.get("backendNodeId").is_some()) {
456 self.inspector
457 .element(node_id(p)?)?
458 .cast::<Control>()
459 .ok_or_else(|| Error::server("Node has no automation peer"))?
460 } else {
461 self.inspector.root()?.upcast()
462 };
463 let peer = ControlAutomationPeer::create_peer_for_element(&root);
464 let max_depth = if p.get("depth").is_some() {
465 depth(p, 256)?
466 } else {
467 256
468 };
469 let mut pending: Vec<(Ref<AutomationPeer>, Option<String>, i64)> = vec![(peer, None, 0)];
470 let mut nodes = Vec::new();
471 let mut seen = HashSet::new();
472 while let Some((peer, parent, d)) = pending.pop() {
473 let Some(owner) = peer
474 .cast::<ControlAutomationPeer>()
475 .and_then(|p| p.try_owner())
476 else {
477 continue;
478 };
479 let id = self.inspector.id_for(&owner.upcast::<StyledElement>());
480 if !seen.insert(id) {
481 continue;
482 }
483 let children = if d < max_depth {
484 peer.get_children().iter().cloned().collect::<Vec<_>>()
485 } else {
486 vec![]
487 };
488 let child_ids: Vec<_> = children
489 .iter()
490 .filter_map(|c| {
491 c.cast::<ControlAutomationPeer>()
492 .and_then(|p| p.try_owner())
493 })
494 .map(|c| {
495 self.inspector
496 .id_for(&c.upcast::<StyledElement>())
497 .to_string()
498 })
499 .collect();
500 let role = format!("{:?}", peer.get_automation_control_type());
501 let name = peer.get_name();
502 if !query
503 || (p["accessibleName"].as_str().is_none_or(|n| n == name)
504 && p["role"]
505 .as_str()
506 .is_none_or(|r| r.eq_ignore_ascii_case(&role)))
507 {
508 let mut node = json!({"nodeId":id.to_string(),"backendDOMNodeId":id,"ignored":false,"role":{"type":"role","value":role},"name":{"type":"computedString","value":name},"description":{"type":"computedString","value":peer.get_help_text()},"childIds":child_ids,"properties":[{"name":"disabled","value":{"type":"boolean","value":!peer.is_enabled()}},{"name":"focusable","value":{"type":"boolean","value":peer.is_keyboard_focusable()}},{"name":"focused","value":{"type":"boolean","value":peer.has_keyboard_focus()}}]});
509 if let Some(parent) = parent {
510 node["parentId"] = json!(parent);
511 }
512 nodes.push(node);
513 }
514 pending.extend(
515 children
516 .into_iter()
517 .rev()
518 .map(|c| (c, Some(id.to_string()), d + 1)),
519 );
520 }
521 Ok(json!({"nodes":nodes}))
522 }
523}
524impl Drop for CdpSession {
525 fn drop(&mut self) {
526 self.stop_inspecting();
527 }
528}
529pub(crate) fn response(id: Value, result: Result<Value>) -> Value {
530 match result {
531 Ok(result) => json!({"id":id,"result":result}),
532 Err(e) => json!({"id":id,"error":{"code":e.code,"message":e.message}}),
533 }
534}