1 | // Copyright © SixtyFPS GmbH <info@slint.dev> |
2 | // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial |
3 | |
4 | // cSpell: ignore fillcolor fontcolor graphviz tpdf |
5 | |
6 | use std::{ |
7 | collections::HashMap, |
8 | rc::{Rc, Weak}, |
9 | }; |
10 | |
11 | use i_slint_compiler::object_tree::{Component, Element, ElementRc}; |
12 | |
13 | #[derive (Clone)] |
14 | struct ElementInfo { |
15 | id: usize, |
16 | component_id: String, |
17 | node_definition: String, |
18 | } |
19 | |
20 | impl ElementInfo { |
21 | fn node_id(&self) -> String { |
22 | format!("n {}" , self.id) |
23 | } |
24 | } |
25 | |
26 | pub struct ElementMap(HashMap<*mut Element, String>); |
27 | |
28 | impl ElementMap { |
29 | #[allow (unused)] |
30 | pub fn node_name(&self, element: &ElementRc) -> Option<&String> { |
31 | self.0.get(&element.as_ptr()) |
32 | } |
33 | } |
34 | |
35 | #[derive (Default)] |
36 | struct State { |
37 | components: HashMap<*const Component, String>, |
38 | elements: HashMap<*mut Element, ElementInfo>, |
39 | current_node_number: usize, |
40 | mark_up: Option<ElementRc>, |
41 | } |
42 | |
43 | impl State { |
44 | fn generate_subgraph(&self, name: &str, elements: &[ElementInfo]) -> Vec<String> { |
45 | let mut result = Vec::new(); |
46 | |
47 | let spaces = if name == "root" { |
48 | " " |
49 | } else { |
50 | result.push(format!(" subgraph cluster_ {name} {{" )); |
51 | result.push(format!(" label = \"{name}\"" )); |
52 | result.push(" color = black" .to_string()); |
53 | " " |
54 | }; |
55 | |
56 | for ei in elements { |
57 | if ei.component_id == name { |
58 | result.push(format!(" {spaces}{}" , ei.node_definition)); |
59 | } |
60 | } |
61 | |
62 | if name != "root" { |
63 | result.push(" }" .to_string()); |
64 | } |
65 | |
66 | result |
67 | } |
68 | |
69 | fn generate(&self, connections: Vec<String>) -> String { |
70 | let mut result = vec!["digraph ElementTree {" .to_string()]; |
71 | |
72 | let mut values: Vec<_> = self.components.values().cloned().collect(); |
73 | values.sort_unstable(); |
74 | let mut elements: Vec<_> = self.elements.values().cloned().collect(); |
75 | elements.sort_unstable_by(|left, right| left.id.partial_cmp(&right.id).unwrap()); |
76 | |
77 | for cn in &values { |
78 | result.extend_from_slice(&self.generate_subgraph(cn, &elements)); |
79 | } |
80 | |
81 | result.extend_from_slice(&connections); |
82 | result.push("}" .to_string()); |
83 | |
84 | result.join(" \n" ) |
85 | } |
86 | |
87 | fn get_node_number(&mut self) -> usize { |
88 | let result = self.current_node_number; |
89 | self.current_node_number += 1; |
90 | result |
91 | } |
92 | |
93 | fn element_info(&mut self, element: &ElementRc) -> Option<ElementInfo> { |
94 | self.elements.get(&element.as_ptr()).cloned() |
95 | } |
96 | |
97 | fn register_component(&mut self, comp: &Rc<Component>, name: String) { |
98 | self.components.entry(Rc::as_ptr(comp)).or_insert(name); |
99 | } |
100 | } |
101 | |
102 | /// This function prints a tree of `Element`s starting below `element` in |
103 | /// graphviz format. You may optionally pass one element to highlight in the |
104 | /// output. |
105 | /// |
106 | /// Arrows moving from one component to the next are printed in blue. Elements |
107 | /// lowered from a Layout are printed as boxes, all other elements are round. |
108 | /// |
109 | /// The function returns a map from `ElementRc` to a String containing the |
110 | /// node name of that element in the dot file. This is nice as it allows to |
111 | /// follow some process as it traverses the tree without having to regenerate |
112 | /// the output all the time. |
113 | /// |
114 | /// You can cut and paste the output into |
115 | /// <https://dreampuf.github.io/GraphvizOnline/> |
116 | /// or into some text file and then run `dot -Tpdf /path/to/file > graph.pdf` |
117 | /// to generate a PDF file out of it. |
118 | #[allow (unused)] |
119 | pub fn as_dot(element: &ElementRc, mark_up: Option<ElementRc>) -> ElementMap { |
120 | let mut state: State = State::default(); |
121 | state.mark_up = mark_up; |
122 | state.register_component( |
123 | &Weak::upgrade(&element.borrow().enclosing_component).unwrap(), |
124 | name:"root" .to_string(), |
125 | ); |
126 | |
127 | let (_, connections: Vec) = recurse_into_element(&mut state, element); |
128 | |
129 | i_slint_core::debug_log!( |
130 | "Statistics: {} elements in {} components" , |
131 | state.elements.len(), |
132 | state.components.len() |
133 | ); |
134 | i_slint_core::debug_log!(" {}" , state.generate(connections)); |
135 | |
136 | let map: HashMap<_, _> = state.elements.iter().map(|(k: &*mut Element, v: &ElementInfo)| (*k, v.node_id())).collect(); |
137 | ElementMap(map) |
138 | } |
139 | |
140 | fn recurse_into_element(state: &mut State, element: &ElementRc) -> (usize, Vec<String>) { |
141 | if let Some(existing) = state.element_info(element) { |
142 | return (existing.id, Vec::new()); |
143 | } |
144 | |
145 | let node_number = state.get_node_number(); |
146 | add_element_node(state, element, node_number); |
147 | |
148 | let e = element.borrow(); |
149 | |
150 | let mut lines = Vec::new(); |
151 | |
152 | if let i_slint_compiler::langtype::ElementType::Component(comp) = &e.base_type { |
153 | state.register_component(comp, format!("c {}" , state.components.len())); |
154 | let component_root = comp.root_element.clone(); |
155 | let (root_id, component_lines) = recurse_into_element(state, &component_root); |
156 | lines.extend_from_slice(&component_lines); |
157 | lines.push(format!(" n {node_number} -> n {root_id} [color = blue]" )); |
158 | } |
159 | |
160 | for c in &e.children { |
161 | let (child_node_number, child_result) = recurse_into_element(state, c); |
162 | lines.extend_from_slice(&child_result); |
163 | lines.push(format!(" n {} -> n {}" , node_number, child_node_number)); |
164 | } |
165 | |
166 | (node_number, lines) |
167 | } |
168 | |
169 | fn add_element_node(state: &mut State, element: &ElementRc, node_number: usize) { |
170 | let e = element.borrow(); |
171 | let layout = if e.debug.iter().any(|d| d.1.is_some()) { ",shape = box" } else { "" }; |
172 | let repeated = if e.repeated.is_some() { ",color = blue" } else { "" }; |
173 | let component = if matches!(e.base_type, i_slint_compiler::langtype::ElementType::Component(_)) |
174 | { |
175 | ",fontcolor = blue" |
176 | } else { |
177 | "" |
178 | }; |
179 | let mark_up = if let Some(mu) = &state.mark_up { |
180 | if Rc::ptr_eq(element, mu) { |
181 | ",style=filled,fillcolor=lightgrey" |
182 | } else { |
183 | "" |
184 | } |
185 | } else { |
186 | "" |
187 | }; |
188 | |
189 | let component_id = state.components.get(&e.enclosing_component.as_ptr()).unwrap().clone(); |
190 | |
191 | state.elements.insert( |
192 | element.as_ptr(), |
193 | ElementInfo { |
194 | id: node_number, |
195 | component_id, |
196 | node_definition: format!( |
197 | " \"n {node_number}\" [label= \"n {node_number}: {}: {}\"{}{}{}{}]" , |
198 | e.id, e.base_type, repeated, layout, component, mark_up |
199 | ), |
200 | }, |
201 | ); |
202 | } |
203 | |