1mod frames;
9mod input;
10mod logs;
11mod schema;
12pub mod target;
13use base64::Engine;
14use ferroui_base::controls::{IResourceHost, ResourceKey};
15use ferroui_base::data::core::{ValueType, ValueTypes};
16use ferroui_base::data::{BindingOperations, BindingPriority};
17use ferroui_base::diagnostics::FerroObjectDiagnosticExtensions;
18use ferroui_base::layout::Layoutable;
19use ferroui_base::media::imaging::{PngBitmapEncoderOptions, RenderTargetBitmap};
20use ferroui_base::media::{Colors, SolidColorBrush};
21use ferroui_base::reactive::{Disposable, IDisposable};
22use ferroui_base::styling::{ControlTheme, Selector, Style};
23use ferroui_base::threading::Dispatcher;
24use ferroui_base::{
25 BoxedValue, FerroObject, FerroProperty, FerroPropertyRegistry, PixelSize, Rect, Ref,
26 StyledElement, Thickness, TypeInfo, Vector, Visual, WeakRef,
27};
28use ferroui_controls::automation::peers::{AutomationPeer, ControlAutomationPeer};
29use ferroui_controls::primitives::{AdornerLayer, TemplatedControl};
30use ferroui_controls::{Application, Border, Control, TopLevel};
31use ferroui_markup::markup::parsers::{SelectorGrammar, SelectorParser, SelectorSyntax};
32pub use logs::{LogCapture, LogEntry, LogGuard};
33pub use schema::{tools, validate};
34use serde_json::{json, Value};
35use std::cell::RefCell;
36use std::collections::{HashMap, HashSet};
37use std::rc::Rc;
38
39#[derive(Clone, Copy, Debug, Default)]
41pub struct Permissions {
42 pub allow_property_writes: bool,
43 pub allow_input: bool,
44}
45
46pub type InspectResult = Result<Value, String>;
47pub type NodeId = u64;
48
49#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
51pub enum TreeKind {
52 #[default]
53 Visual,
54 Logical,
55 Automation,
56}
57impl TreeKind {
58 fn parse(name: &str) -> Self {
59 match name {
60 "logical" => Self::Logical,
61 "automation" => Self::Automation,
62 _ => Self::Visual,
63 }
64 }
65}
66
67#[derive(Clone)]
69pub struct PropertyInfo {
70 pub property: &'static FerroProperty,
71 pub name: String,
73 pub value: BoxedValue,
74 pub priority: Option<BindingPriority>,
76 pub read_only: bool,
77 pub has_binding: bool,
78 pub overridden_current_value: bool,
79}
80impl PropertyInfo {
81 pub fn priority_name(&self) -> String {
82 self.priority
83 .map_or_else(|| "Direct".into(), |priority| format!("{priority:?}"))
84 }
85 pub fn is_local(&self) -> bool {
86 self.priority == Some(BindingPriority::LocalValue)
87 }
88}
89
90#[derive(Clone)]
93pub struct StyleFrame {
94 pub kind: String,
95 pub selector: Option<String>,
97 pub active: bool,
98 pub priority: String,
99 pub setters: Vec<(String, Option<BoxedValue>)>,
100}
101
102#[derive(Clone, Debug)]
104pub struct BoxModel {
105 pub content: Rect,
106 pub padding: Rect,
107 pub border: Rect,
108 pub margin: Rect,
109}
110
111pub struct Png {
112 pub data: Vec<u8>,
113 pub width: f64,
114 pub height: f64,
115}
116
117pub struct Highlight(Rc<dyn IDisposable>);
119impl Drop for Highlight {
120 fn drop(&mut self) {
121 self.0.dispose();
122 }
123}
124
125ferroui_base::ferro_thread_local! {
126 static SHARED: RefCell<Option<Rc<RefCell<Inspector>>>> = const { RefCell::new(None) };
127}
128
129pub struct Inspector {
130 next_id: NodeId,
131 nodes: HashMap<NodeId, WeakRef<FerroObject>>,
132 addresses: HashMap<usize, NodeId>,
133 roots: Vec<WeakRef<TopLevel>>,
134 frames: HashMap<NodeId, frames::FrameStats>,
135 overlays: Vec<WeakRef<Visual>>,
136}
137
138impl Default for Inspector {
139 fn default() -> Self {
140 Self::new()
141 }
142}
143
144impl Inspector {
145 pub fn new() -> Self {
146 Dispatcher::ui_thread().verify_access();
147 Self {
148 next_id: 2,
149 nodes: HashMap::new(),
150 addresses: HashMap::new(),
151 roots: Vec::new(),
152 frames: HashMap::new(),
153 overlays: Vec::new(),
154 }
155 }
156
157 pub fn shared() -> Rc<RefCell<Inspector>> {
159 SHARED.with(|shared| {
160 shared
161 .borrow_mut()
162 .get_or_insert_with(|| Rc::new(RefCell::new(Inspector::new())))
163 .clone()
164 })
165 }
166
167 pub fn register_root(&mut self, root: &TopLevel) {
169 if !self.roots.iter().any(|weak| weak.points_to(&root.to_ref())) {
170 self.roots.push(root.to_ref().downgrade());
171 }
172 }
173
174 pub fn node_id(&mut self, object: &FerroObject) -> NodeId {
175 let address = object as *const FerroObject as usize;
176 if let Some(id) = self.addresses.get(&address) {
177 if self
178 .nodes
179 .get(id)
180 .and_then(WeakRef::upgrade)
181 .is_some_and(|old| old == object.to_ref())
182 {
183 return *id;
184 }
185 }
186 let id = self.next_id;
187 self.next_id += 1;
188 self.nodes.insert(id, object.to_ref().downgrade());
189 self.addresses.insert(address, id);
190 id
191 }
192
193 pub fn resolve(&self, id: NodeId) -> Result<Ref<FerroObject>, String> {
194 self.nodes
195 .get(&id)
196 .and_then(WeakRef::upgrade)
197 .ok_or_else(|| format!("Unknown or expired node_id: {id}"))
198 }
199
200 fn prune(&mut self) {
201 self.nodes.retain(|_, weak| weak.upgrade().is_some());
202 self.addresses.retain(|_, id| self.nodes.contains_key(id));
203 self.overlays.retain(|weak| weak.upgrade().is_some());
204 }
205
206 pub fn windows(&mut self) -> Vec<Ref<TopLevel>> {
208 self.prune();
209 self.roots.retain(|weak| weak.upgrade().is_some());
210 let mut roots: Vec<_> = self
211 .roots
212 .iter()
213 .filter_map(WeakRef::upgrade)
214 .filter(|r| r.platform_impl().is_some())
215 .collect();
216 if let Some(lifetime) = Application::current().and_then(|a| a.application_lifetime()) {
217 if let Some(desktop) = lifetime.as_classic_desktop_style_application_lifetime() {
218 for window in desktop.windows() {
219 let root = window.upcast::<TopLevel>();
220 if !roots.contains(&root) {
221 roots.push(root);
222 }
223 }
224 }
225 }
226 for root in &roots {
227 let id = self.node_id(root);
228 self.frames
229 .entry(id)
230 .or_insert_with(|| frames::FrameStats::new(root));
231 }
232 let live: HashSet<_> = roots.iter().map(|r| self.node_id(r)).collect();
233 self.frames.retain(|id, _| live.contains(id));
234 roots
235 }
236
237 fn roots(&mut self, args: &Value) -> Result<Vec<Ref<FerroObject>>, String> {
238 if let Some(id) = args["node_id"].as_u64() {
239 Ok(vec![self.resolve(id)?])
240 } else {
241 Ok(self.windows().into_iter().map(Ref::upcast).collect())
242 }
243 }
244
245 fn object(&self, args: &Value) -> Result<Ref<FerroObject>, String> {
246 self.resolve(args["node_id"].as_u64().ok_or("node_id required")?)
247 }
248
249 pub fn visual_object(object: &Ref<FerroObject>) -> Ref<FerroObject> {
251 object
252 .cast::<ControlAutomationPeer>()
253 .and_then(|p| p.try_owner())
254 .map(Ref::upcast)
255 .unwrap_or_else(|| object.clone())
256 }
257
258 pub fn is_overlay(&self, object: &FerroObject) -> bool {
260 self.overlays
261 .iter()
262 .filter_map(WeakRef::upgrade)
263 .any(|overlay| overlay.upcast::<FerroObject>() == object.to_ref())
264 }
265
266 pub fn children(&self, object: &Ref<FerroObject>, tree: TreeKind) -> Vec<Ref<FerroObject>> {
268 let children: Vec<Ref<FerroObject>> = match tree {
269 TreeKind::Automation => Self::peer(object)
270 .map(|p| p.get_children().iter().cloned().map(Ref::upcast).collect())
271 .unwrap_or_default(),
272 TreeKind::Logical => object
273 .cast::<StyledElement>()
274 .map(|o| {
275 o.logical_children()
276 .to_vec()
277 .into_iter()
278 .map(Ref::upcast)
279 .collect()
280 })
281 .unwrap_or_default(),
282 TreeKind::Visual => object
283 .cast::<Visual>()
284 .map(|o| {
285 o.visual_children()
286 .to_vec()
287 .into_iter()
288 .map(Ref::upcast)
289 .collect()
290 })
291 .unwrap_or_default(),
292 };
293 if self.overlays.is_empty() {
294 return children;
295 }
296 children
297 .into_iter()
298 .filter(|child| !self.is_overlay(child))
299 .collect()
300 }
301
302 pub fn peer(object: &Ref<FerroObject>) -> Result<Ref<AutomationPeer>, String> {
303 object
304 .cast::<AutomationPeer>()
305 .or_else(|| {
306 object
307 .cast::<Control>()
308 .map(|c| ControlAutomationPeer::create_peer_for_element(&c))
309 })
310 .ok_or("Node has no automation peer".into())
311 }
312
313 fn registered(object: &FerroObject) -> Vec<&'static FerroProperty> {
314 let registry = FerroPropertyRegistry::instance();
315 let mut result = registry.get_registered_for(object).to_vec();
316 result.extend(
317 registry
318 .get_registered_attached(object.get_type())
319 .iter()
320 .copied(),
321 );
322 result.sort_by_key(|p| property_name(p));
323 result.dedup_by_key(|p| *p as *const FerroProperty);
324 result
325 }
326
327 pub fn property(object: &FerroObject, name: &str) -> Result<&'static FerroProperty, String> {
329 Self::registered(object)
330 .into_iter()
331 .find(|p| property_name(p) == name)
332 .ok_or_else(|| format!("Unknown property: {name}"))
333 }
334
335 pub fn properties(object: &FerroObject) -> Vec<PropertyInfo> {
337 Self::registered(object)
338 .into_iter()
339 .map(|p| {
340 let diagnostic = object.get_diagnostic(p);
341 PropertyInfo {
342 property: p,
343 name: property_name(p),
344 value: diagnostic.value().clone(),
345 priority: (!p.is_direct()).then(|| diagnostic.priority()),
346 read_only: p.is_read_only(),
347 has_binding: BindingOperations::get_binding_expression_base(object, p)
348 .is_some(),
349 overridden_current_value: diagnostic.is_overridden_current_value(),
350 }
351 })
352 .collect()
353 }
354
355 pub fn set_property(
359 object: &FerroObject,
360 name: &str,
361 value: Option<BoxedValue>,
362 ) -> Result<&'static FerroProperty, String> {
363 let property = Self::property(object, name)?;
364 if property.is_read_only() {
365 return Err(format!("{name} is read-only"));
366 }
367 let converted = ValueTypes::try_convert(
368 value.as_ref(),
369 ValueType::new(property.property_type(), property.property_type_name()),
370 )
371 .flatten()
372 .ok_or_else(|| format!("Cannot convert the value to the type of {name}"))?;
373 if !property.is_valid_value(converted.as_any()) {
374 return Err(format!("Validation of {name} rejected the value"));
375 }
376 object.set_value_untyped(property, converted.as_any(), BindingPriority::LocalValue);
377 Ok(property)
378 }
379
380 pub fn clear_property(
381 object: &FerroObject,
382 name: &str,
383 ) -> Result<&'static FerroProperty, String> {
384 let property = Self::property(object, name)?;
385 if property.is_read_only() {
386 return Err(format!("{name} is read-only"));
387 }
388 object.clear_value_untyped(property);
389 Ok(property)
390 }
391
392 pub fn styles(object: &FerroObject) -> Vec<StyleFrame> {
394 object
395 .get_value_store_diagnostic()
396 .applied_frames()
397 .iter()
398 .map(|frame| {
399 let source = frame.source().and_then(|s| ValueTypes::as_object(&*s));
400 let selector = source
401 .as_ref()
402 .and_then(|s| s.cast::<Style>())
403 .and_then(|s| s.selector())
404 .map(|s| s.to_string_with_owner(None))
405 .or_else(|| {
406 source.and_then(|s| s.cast::<ControlTheme>()).map(|t| {
407 format!(
408 "Theme {}",
409 t.target_type().map(TypeInfo::name).unwrap_or_default()
410 )
411 })
412 });
413 StyleFrame {
414 kind: format!("{:?}", frame.type_()),
415 selector,
416 active: frame.is_active(),
417 priority: format!("{:?}", frame.priority()),
418 setters: frame
419 .values()
420 .into_iter()
421 .map(|v| (property_name(v.property), v.value))
422 .collect(),
423 }
424 })
425 .collect()
426 }
427
428 pub fn parse_selector(&self, text: &str) -> Result<Option<Selector>, String> {
432 if text.trim().is_empty() || text.len() > 4096 {
433 return Err("Selector must contain 1..4096 bytes".into());
434 }
435 let mut nesting = 0usize;
436 for character in text.chars() {
437 if character == '(' {
438 nesting += 1;
439 }
440 if nesting > 32 {
441 return Err("Selector nesting is too deep".into());
442 }
443 if character == ')' {
444 nesting = nesting.saturating_sub(1);
445 }
446 }
447 validate_selector(&SelectorGrammar::parse(text).map_err(|e| e.to_string())?)?;
448 let mut live: HashMap<String, &'static TypeInfo> = HashMap::new();
449 for object in self.nodes.values().filter_map(WeakRef::upgrade) {
450 let mut type_ = Some(object.get_type());
451 while let Some(t) = type_ {
452 live.insert(t.name().into(), t);
453 type_ = t.base_type();
454 }
455 }
456 SelectorParser::new(move |namespace, name| {
457 if namespace.is_empty() {
458 TypeInfo::find_by_name(name).or_else(|| live.get(name).copied())
459 } else {
460 TypeInfo::find(namespace, name)
461 }
462 })
463 .parse(text)
464 .map_err(|e| e.to_string())
465 }
466
467 pub fn box_model(visual: &Ref<Visual>, relative_to: &Ref<Visual>) -> Result<BoxModel, String> {
470 let transform = visual
471 .transform_to_visual(relative_to)
472 .ok_or("Node has no transform to the root")?;
473 let border = Rect::from_size(visual.bounds().size());
474 let margin = visual.get_value(Layoutable::margin_property());
475 let padding = visual.get_value(TemplatedControl::padding_property());
476 let border_thickness = visual
477 .cast::<Border>()
478 .map(|b| b.border_thickness())
479 .or_else(|| {
480 visual
481 .cast::<TemplatedControl>()
482 .map(|c| c.border_thickness())
483 })
484 .unwrap_or_default();
485 let padding_rect = border.deflate_thickness(border_thickness);
486 Ok(BoxModel {
487 content: padding_rect
488 .deflate_thickness(padding)
489 .transform_to_aabb(transform),
490 padding: padding_rect.transform_to_aabb(transform),
491 border: border.transform_to_aabb(transform),
492 margin: border
493 .inflate_thickness(margin)
494 .transform_to_aabb(transform),
495 })
496 }
497
498 pub fn screenshot(visual: &Ref<Visual>, scale: f64) -> Result<Png, String> {
500 let size = visual.bounds().size();
501 let width = (size.width * scale).ceil();
502 let height = (size.height * scale).ceil();
503 if !width.is_finite()
504 || !height.is_finite()
505 || width < 1.0
506 || height < 1.0
507 || width * height > 16_000_000.0
508 {
509 return Err("Screenshot size is empty or exceeds 16 megapixels".into());
510 }
511 let bitmap = RenderTargetBitmap::with_dpi(
512 PixelSize::new(width as i32, height as i32),
513 Vector::new(96.0 * scale, 96.0 * scale),
514 );
515 bitmap.render(visual);
516 let mut data = Vec::new();
517 let result = bitmap.save(&mut data, &PngBitmapEncoderOptions::DEFAULT.into());
518 bitmap.dispose();
519 result.map_err(|e| e.to_string())?;
520 Ok(Png {
521 data,
522 width,
523 height,
524 })
525 }
526
527 pub fn highlight(&mut self, visual: &Ref<Visual>) -> Result<Highlight, String> {
530 let layer = AdornerLayer::get_adorner_layer(visual).ok_or("Node has no adorner layer")?;
531 let border = Border::new();
532 border.set_is_hit_test_visible(false);
533 border.set_border_thickness(Thickness::uniform(2.0));
534 border.set_border_brush(Some(
535 SolidColorBrush::with_color(Colors::DODGER_BLUE).into(),
536 ));
537 AdornerLayer::set_adorned_element(&border, visual.clone());
538 self.overlays
539 .push(border.clone().upcast::<Visual>().downgrade());
540 layer.children().add(border.clone());
541 Ok(Highlight(Disposable::create(move || {
542 layer.children().remove(&border);
543 })))
544 }
545
546 fn describe(&mut self, object: &Ref<FerroObject>, properties: Option<&Value>) -> Value {
547 let id = self.node_id(object);
548 let visual_object = Self::visual_object(object);
549 let mut result = json!({"node_id":id,"type":visual_object.get_type().name()});
550 if let Some(styled) = visual_object.cast::<StyledElement>() {
551 result["name"] = json!(styled.name());
552 }
553 if let Some(visual) = visual_object.cast::<Visual>() {
554 result["bounds"] = rect(visual.bounds());
555 result["visible"] = json!(visual.is_visible());
556 if let Some(root) = visual.visual_root() {
557 if let Some(transform) = visual.transform_to_visual(&root) {
558 result["bounds_in_root"] =
559 rect(Rect::from_size(visual.bounds().size()).transform_to_aabb(transform));
560 result["transform_to_root"] = json!(format!("{transform:?}"));
561 }
562 }
563 }
564 if let Some(layout) = visual_object.cast::<Layoutable>() {
565 result["desired_size"] =
566 json!({"width":layout.desired_size().width,"height":layout.desired_size().height});
567 }
568 if let Ok(peer) = Self::peer(object) {
569 result["automation_id"] = json!(peer.get_automation_id());
570 result["text"] = json!(peer.get_name());
571 result["role"] = json!(format!("{:?}", peer.get_automation_control_type()));
572 result["enabled"] = json!(peer.is_enabled());
573 }
574 if let Some(properties) = properties {
575 result["properties"] = properties_json(&visual_object, Some(properties));
576 }
577 result
578 }
579
580 fn tree(
581 &mut self,
582 object: Ref<FerroObject>,
583 tree: TreeKind,
584 depth: u64,
585 properties: Option<&Value>,
586 visited: &mut HashSet<NodeId>,
587 ) -> Value {
588 let object = if tree == TreeKind::Automation {
589 Self::peer(&object).map(Ref::upcast).unwrap_or(object)
590 } else {
591 object
592 };
593 let id = self.node_id(&object);
594 if visited.len() >= 10000 || !visited.insert(id) {
595 return json!({"node_id":id,"truncated":true});
596 }
597 let mut result = self.describe(&object, properties);
598 let children = self.children(&object, tree);
599 result["child_count"] = json!(children.len());
600 if depth > 0 {
601 let mut snapshots = Vec::new();
602 for child in children {
603 if visited.len() >= 10000 {
604 result["truncated"] = json!(true);
605 break;
606 }
607 snapshots.push(self.tree(child, tree, depth - 1, properties, visited));
608 }
609 result["children"] = Value::Array(snapshots);
610 } else if !children.is_empty() {
611 result["truncated"] = json!(true);
612 }
613 result
614 }
615
616 fn find(&mut self, args: &Value) -> Result<(Vec<Ref<FerroObject>>, bool), String> {
619 let selector = args["selector"]
620 .as_str()
621 .map(|s| self.parse_selector(s))
622 .transpose()?
623 .flatten();
624 let mut stack = self.roots(args)?;
625 stack.reverse();
626 let mut visited = HashSet::new();
627 let mut found = Vec::new();
628 let limit = args["limit"].as_u64().unwrap_or(1000) as usize;
629 while let Some(object) = stack.pop() {
630 if !visited.insert(self.node_id(&object)) {
631 continue;
632 }
633 if visited.len() > 10000 {
634 return Ok((found, true));
635 }
636 let mut children = self.children(&object, TreeKind::Visual);
637 children.extend(self.children(&object, TreeKind::Logical));
638 stack.extend(children.into_iter().rev());
639 let target = Self::visual_object(&object);
640 if args["type"]
641 .as_str()
642 .is_some_and(|s| target.get_type().name() != s)
643 {
644 continue;
645 }
646 if args["name"].as_str().is_some_and(|s| {
647 target
648 .cast::<StyledElement>()
649 .and_then(|o| o.name())
650 .as_deref()
651 != Some(s)
652 }) {
653 continue;
654 }
655 if let Some(selector) = &selector {
656 if !target
657 .cast::<StyledElement>()
658 .is_some_and(|o| selector.match_(&o, None, false).is_match())
659 {
660 continue;
661 }
662 }
663 if let Some(s) = args["automation_id"].as_str() {
664 if Self::peer(&target)
665 .ok()
666 .and_then(|p| p.get_automation_id())
667 .as_deref()
668 != Some(s)
669 {
670 continue;
671 }
672 }
673 if let Some(s) = args["text"].as_str() {
674 if !Self::peer(&target).is_ok_and(|p| p.get_name().contains(s)) {
675 continue;
676 }
677 }
678 found.push(target);
679 if found.len() >= limit {
680 break;
681 }
682 }
683 Ok((found, false))
684 }
685
686 pub fn execute(&mut self, name: &str, args: &Value, permissions: Permissions) -> InspectResult {
691 Dispatcher::ui_thread().verify_access();
692 validate(name, args)?;
693 catch(|| {
694 if is_input(name) {
695 let object = self.prepare_input(args, permissions)?;
696 input::execute(name, &object, args, permissions)?;
697 return Ok(json!({"ok":true}));
698 }
699 self.execute_inner(name, args, permissions)
700 })
701 }
702
703 fn prepare_input(
704 &mut self,
705 args: &Value,
706 permissions: Permissions,
707 ) -> Result<Ref<FerroObject>, String> {
708 if !permissions.allow_input {
709 return Err("Input is disabled".into());
710 }
711 self.prune();
712 self.object(args)
713 }
714
715 fn execute_inner(
716 &mut self,
717 name: &str,
718 args: &Value,
719 permissions: Permissions,
720 ) -> InspectResult {
721 self.prune();
722 match name {
723 "list_windows" => {
724 let roots = self.windows();
725 Ok(Value::Array(
726 roots
727 .iter()
728 .map(|r| self.describe(&r.clone().upcast(), None))
729 .collect(),
730 ))
731 }
732 "get_tree" => {
733 let roots = self.roots(args)?;
734 let mut visited = HashSet::new();
735 let tree = TreeKind::parse(args["tree"].as_str().unwrap_or("visual"));
736 Ok(Value::Array(
737 roots
738 .into_iter()
739 .map(|root| {
740 self.tree(
741 root,
742 tree,
743 args["depth"].as_u64().unwrap_or(8),
744 args.get("properties"),
745 &mut visited,
746 )
747 })
748 .collect(),
749 ))
750 }
751 "find_elements" => {
752 let (found, truncated) = self.find(args)?;
754 if truncated && found.is_empty() {
755 return Err("Search exceeded 10000 nodes; scope it with node_id".into());
756 }
757 Ok(Value::Array(
758 found.iter().map(|o| self.describe(o, None)).collect(),
759 ))
760 }
761 "get_properties" => Ok(properties_json(
762 &Self::visual_object(&self.object(args)?),
763 args.get("properties"),
764 )),
765 "set_property" => {
766 if !permissions.allow_property_writes {
767 return Err("Property writes are disabled".into());
768 }
769 let object = Self::visual_object(&self.object(args)?);
770 let name = args["property"].as_str().unwrap();
771 let property = if args["clear"].as_bool().unwrap_or(false) {
772 Self::clear_property(&object, name)?
773 } else {
774 let value = args
775 .get("value")
776 .ok_or("value required unless clear is true")?;
777 let source: Option<BoxedValue> = match value {
778 Value::Null => None,
779 Value::Bool(v) => Some(Rc::new(*v)),
780 Value::Number(v) => Some(Rc::new(v.as_f64().ok_or("Invalid number")?)),
781 Value::String(v) => Some(Rc::new(v.clone())),
782 _ => {
783 return Err(
784 "Use a boolean, number, string (framework syntax), or null".into()
785 )
786 }
787 };
788 Self::set_property(&object, name, source)?
789 };
790 Ok(properties_json(
791 &object,
792 Some(&json!([property_name(property)])),
793 ))
794 }
795 "get_styles" => {
796 let object = Self::visual_object(&self.object(args)?);
797 Ok(Value::Array(
798 Self::styles(&object)
799 .into_iter()
800 .map(|frame| {
801 json!({"kind":frame.kind,"selector":frame.selector,"active":frame.active,"priority":frame.priority,"setters":frame.setters.into_iter().map(|(property,value)|json!({"property":property,"value":boxed(value)})).collect::<Vec<_>>()})
802 })
803 .collect(),
804 ))
805 }
806 "get_resources" => {
807 let object = Self::visual_object(&self.object(args)?)
808 .cast::<StyledElement>()
809 .ok_or("Node is not a styled element")?;
810 let host: &dyn IResourceHost = &*object;
811 let keys: Vec<ResourceKey> = if let Some(keys) = args["keys"].as_array() {
812 keys.iter()
813 .map(|k| ResourceKey::from(k.as_str().unwrap()))
814 .collect()
815 } else {
816 object
818 .resources_if_created()
819 .map(|resources| resources.keys())
820 .unwrap_or_default()
821 };
822 Ok(Value::Array(keys.into_iter().map(|key| { let value = host.try_find_resource(&key,object.actual_theme_variant().as_ref()); json!({"key":key.as_str().map(str::to_owned).unwrap_or_else(||format!("{key:?}")),"found":value.is_some(),"value":boxed(value.flatten())}) }).collect()))
823 }
824 "screenshot" => {
825 let object = self
826 .roots(args)?
827 .into_iter()
828 .next()
829 .ok_or("No window available")?;
830 let visual = Self::visual_object(&object)
831 .cast::<Visual>()
832 .ok_or("Node is not visual")?;
833 let png = Self::screenshot(&visual, args["scale"].as_f64().unwrap_or(1.0))?;
834 Ok(
835 json!({"mimeType":"image/png","data":base64::engine::general_purpose::STANDARD.encode(png.data),"width":png.width,"height":png.height}),
836 )
837 }
838 "wait_for" => {
839 let (found, truncated) = self.find(args)?;
840 if truncated && found.is_empty() {
841 return Err("Search exceeded 10000 nodes; scope it with node_id".into());
842 }
843 let condition = args["condition"].as_str().unwrap_or("exists");
844 if condition == "property_equals"
845 && (args["property"].as_str().is_none() || args.get("value").is_none())
846 {
847 return Err("property and value required for property_equals".into());
848 }
849 let matched = match condition {
850 "absent" => found.is_empty(),
851 "visible" => found.iter().any(|o| {
852 o.cast::<Visual>().is_some_and(|v| v.is_visible())
853 && Self::peer(o).is_ok_and(|p| !p.is_offscreen())
854 }),
855 "enabled" => found
856 .iter()
857 .any(|o| Self::peer(o).is_ok_and(|p| p.is_enabled())),
858 "property_equals" => found.iter().any(|o| {
859 Self::property(o, args["property"].as_str().unwrap())
860 .is_ok_and(|p| boxed(Some(o.get_value_untyped(p))) == args["value"])
861 }),
862 _ => !found.is_empty(),
863 };
864 Ok(
865 json!({"matched":matched,"node_ids":found.iter().map(|o|self.node_id(o)).collect::<Vec<_>>()}),
866 )
867 }
868 "get_logs" => Ok(LogCapture::snapshot(args["after"].as_u64().unwrap_or(0))),
869 "get_frame_stats" => {
870 let windows = self.windows();
871 Ok(Value::Array(
872 windows
873 .iter()
874 .map(|w| {
875 let t = w.renderer_diagnostics().last_layout_pass_timing();
876 let id = self.node_id(w);
877 let mut stats = self.frames[&id].snapshot();
878 stats["node_id"] = json!(id);
879 stats["layout_pass_count"] = json!(t.pass_counter);
880 stats["layout_ms"] = json!(t.elapsed.as_secs_f64() * 1000.0);
881 stats
882 })
883 .collect(),
884 ))
885 }
886 _ => Err(format!("Unknown tool: {name}")),
887 }
888 }
889}
890
891pub fn execute(name: &str, args: &Value, permissions: Permissions) -> InspectResult {
897 Dispatcher::ui_thread().verify_access();
898 validate(name, args)?;
899 let shared = Inspector::shared();
900 if is_input(name) {
901 let object = {
902 let mut inspector = shared
903 .try_borrow_mut()
904 .map_err(|_| "The inspector is busy".to_owned())?;
905 inspector.prepare_input(args, permissions)?
906 };
907 return catch(|| {
908 input::execute(name, &object, args, permissions)?;
909 Ok(json!({"ok":true}))
910 });
911 }
912 let mut inspector = shared
913 .try_borrow_mut()
914 .map_err(|_| "The inspector is busy".to_owned())?;
915 catch(|| inspector.execute_inner(name, args, permissions))
916}
917
918fn is_input(name: &str) -> bool {
919 matches!(
920 name,
921 "click" | "type_text" | "press_key" | "scroll" | "invoke_pattern"
922 )
923}
924
925fn catch(f: impl FnOnce() -> InspectResult) -> InspectResult {
926 std::panic::catch_unwind(std::panic::AssertUnwindSafe(f)).unwrap_or_else(|error| {
927 let message = error
928 .downcast_ref::<String>()
929 .map(String::as_str)
930 .or_else(|| error.downcast_ref::<&str>().copied())
931 .unwrap_or("UI operation panicked");
932 Err(message.to_owned())
933 })
934}
935
936fn properties_json(object: &FerroObject, filter: Option<&Value>) -> Value {
937 let names = filter.and_then(Value::as_array);
938 Value::Array(
939 Inspector::properties(object)
940 .into_iter()
941 .filter(|p| names.is_none_or(|names| names.iter().any(|n| n == &p.name)))
942 .map(|p| {
943 json!({"name":p.name,"type":p.property.property_type_name(),"value":boxed(Some(p.value.clone())),"read_only":p.read_only,"priority":p.priority_name(),"binding":p.has_binding,"overridden_current_value":p.overridden_current_value})
944 })
945 .collect(),
946 )
947}
948
949pub fn property_name(p: &FerroProperty) -> String {
951 if p.is_attached() {
952 format!("{}.{}", p.owner_type().name(), p.name())
953 } else {
954 p.name().to_owned()
955 }
956}
957
958pub fn display_value(value: Option<&BoxedValue>) -> String {
960 value
961 .cloned()
962 .and_then(ValueTypes::normalize)
963 .as_ref()
964 .map(|v| ValueTypes::to_display_string(Some(v)))
965 .unwrap_or_default()
966}
967
968fn rect(r: Rect) -> Value {
969 json!({"x":r.x,"y":r.y,"width":r.width,"height":r.height})
970}
971fn boxed(value: Option<BoxedValue>) -> Value {
972 let Some(value) = value.and_then(ValueTypes::normalize) else {
973 return Value::Null;
974 };
975 macro_rules! scalar {($($t:ty),*) => {$(if let Some(v) = value.downcast_ref::<$t>() {return json!(v);})*};}
976 scalar!(bool, String, i32, i64, u32, u64, usize);
977 if let Some(v) = value.downcast_ref::<f64>() {
978 return if v.is_finite() {
979 json!(v)
980 } else {
981 json!(v.to_string())
982 };
983 }
984 if let Some(v) = value.downcast_ref::<f32>() {
985 return if v.is_finite() {
986 json!(v)
987 } else {
988 json!(v.to_string())
989 };
990 }
991 ValueTypes::try_to_string(&*value)
992 .map(Value::String)
993 .unwrap_or_else(|| json!({"opaque":true}))
994}
995
996fn validate_selector(syntax: &[SelectorSyntax]) -> Result<(), String> {
999 let mut term = false;
1000 for item in syntax {
1001 match item {
1002 SelectorSyntax::Nesting => {
1003 return Err("Style nesting selectors have no parent style in a query".into())
1004 }
1005 SelectorSyntax::Child
1006 | SelectorSyntax::Descendant
1007 | SelectorSyntax::Template
1008 | SelectorSyntax::Comma => {
1009 if !term {
1010 return Err("Selector combinator requires a preceding selector".into());
1011 }
1012 term = false;
1013 }
1014 SelectorSyntax::Not { argument } => {
1015 validate_selector(argument)?;
1016 term = true;
1017 }
1018 _ => term = true,
1019 }
1020 }
1021 if !term {
1022 return Err("Selector requires a final selector term".into());
1023 }
1024 Ok(())
1025}
1026
1027#[cfg(test)]
1028mod tests;