// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 // cSpell: ignore fillcolor fontcolor graphviz tpdf use std::{ collections::HashMap, rc::{Rc, Weak}, }; use i_slint_compiler::object_tree::{Component, Element, ElementRc}; #[derive(Clone)] struct ElementInfo { id: usize, component_id: String, node_definition: String, } impl ElementInfo { fn node_id(&self) -> String { format!("n{}", self.id) } } pub struct ElementMap(HashMap<*mut Element, String>); impl ElementMap { #[allow(unused)] pub fn node_name(&self, element: &ElementRc) -> Option<&String> { self.0.get(&element.as_ptr()) } } #[derive(Default)] struct State { components: HashMap<*const Component, String>, elements: HashMap<*mut Element, ElementInfo>, current_node_number: usize, mark_up: Option, } impl State { fn generate_subgraph(&self, name: &str, elements: &[ElementInfo]) -> Vec { let mut result = Vec::new(); let spaces = if name == "root" { " " } else { result.push(format!(" subgraph cluster_{name} {{")); result.push(format!(" label = \"{name}\"")); result.push(" color = black".to_string()); " " }; for ei in elements { if ei.component_id == name { result.push(format!("{spaces}{}", ei.node_definition)); } } if name != "root" { result.push(" }".to_string()); } result } fn generate(&self, connections: Vec) -> String { let mut result = vec!["digraph ElementTree {".to_string()]; let mut values: Vec<_> = self.components.values().cloned().collect(); values.sort_unstable(); let mut elements: Vec<_> = self.elements.values().cloned().collect(); elements.sort_unstable_by(|left, right| left.id.partial_cmp(&right.id).unwrap()); for cn in &values { result.extend_from_slice(&self.generate_subgraph(cn, &elements)); } result.extend_from_slice(&connections); result.push("}".to_string()); result.join("\n") } fn get_node_number(&mut self) -> usize { let result = self.current_node_number; self.current_node_number += 1; result } fn element_info(&mut self, element: &ElementRc) -> Option { self.elements.get(&element.as_ptr()).cloned() } fn register_component(&mut self, comp: &Rc, name: String) { self.components.entry(Rc::as_ptr(comp)).or_insert(name); } } /// This function prints a tree of `Element`s starting below `element` in /// graphviz format. You may optionally pass one element to highlight in the /// output. /// /// Arrows moving from one component to the next are printed in blue. Elements /// lowered from a Layout are printed as boxes, all other elements are round. /// /// The function returns a map from `ElementRc` to a String containing the /// node name of that element in the dot file. This is nice as it allows to /// follow some process as it traverses the tree without having to regenerate /// the output all the time. /// /// You can cut and paste the output into /// /// or into some text file and then run `dot -Tpdf /path/to/file > graph.pdf` /// to generate a PDF file out of it. #[allow(unused)] pub fn as_dot(element: &ElementRc, mark_up: Option) -> ElementMap { let mut state = State { mark_up, ..State::default() }; state.register_component( &Weak::upgrade(&element.borrow().enclosing_component).unwrap(), "root".to_string(), ); let (_, connections) = recurse_into_element(&mut state, element); i_slint_core::debug_log!( "Statistics: {} elements in {} components", state.elements.len(), state.components.len() ); i_slint_core::debug_log!("{}", state.generate(connections)); let map: HashMap<_, _> = state.elements.iter().map(|(k, v)| (*k, v.node_id())).collect(); ElementMap(map) } fn recurse_into_element(state: &mut State, element: &ElementRc) -> (usize, Vec) { if let Some(existing) = state.element_info(element) { return (existing.id, Vec::new()); } let node_number = state.get_node_number(); add_element_node(state, element, node_number); let e = element.borrow(); let mut lines = Vec::new(); if let i_slint_compiler::langtype::ElementType::Component(comp) = &e.base_type { state.register_component(comp, format!("c{}", state.components.len())); let component_root = comp.root_element.clone(); let (root_id, component_lines) = recurse_into_element(state, &component_root); lines.extend_from_slice(&component_lines); lines.push(format!(" n{node_number} -> n{root_id} [color = blue]")); } for c in &e.children { let (child_node_number, child_result) = recurse_into_element(state, c); lines.extend_from_slice(&child_result); lines.push(format!(" n{node_number} -> n{child_node_number}")); } (node_number, lines) } fn add_element_node(state: &mut State, element: &ElementRc, node_number: usize) { let e = element.borrow(); let layout = if e.debug.iter().any(|d| d.layout.is_some()) { ",shape = box" } else { "" }; let repeated = if e.repeated.is_some() { ",color = blue" } else { "" }; let component = if matches!(e.base_type, i_slint_compiler::langtype::ElementType::Component(_)) { ",fontcolor = blue" } else { "" }; let mark_up = if let Some(mu) = &state.mark_up { if Rc::ptr_eq(element, mu) { ",style=filled,fillcolor=lightgrey" } else { "" } } else { "" }; let component_id = state.components.get(&e.enclosing_component.as_ptr()).unwrap().clone(); state.elements.insert( element.as_ptr(), ElementInfo { id: node_number, component_id, node_definition: format!( " \"n{node_number}\" [label=\"n{node_number}:{}:{}\"{}{}{}{}]", e.id, e.base_type, repeated, layout, component, mark_up ), }, ); }