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 descr rfind unindented
5
6use crate::common::{ComponentInformation, Position, PropertyChange};
7use crate::language::DocumentCache;
8
9use i_slint_compiler::langtype::{DefaultSizeBinding, ElementType};
10use lsp_types::Url;
11
12use std::{path::Path, rc::Rc};
13
14#[cfg(target_arch = "wasm32")]
15use crate::wasm_prelude::UrlWasm;
16
17fn builtin_component_info(name: &str, fills_parent: bool) -> ComponentInformation {
18 let (category, is_layout) = match name {
19 "GridLayout" | "HorizontalLayout" | "VerticalLayout" => ("Layout", true),
20 "Dialog" | "Window" | "PopupWindow" => ("Window Management", false),
21 "FocusScope" | "TouchArea" => ("Event Handling", false),
22 "Text" => ("Text Handling", false),
23 _ => ("Primitives", false),
24 };
25
26 let default_properties = match name {
27 "Text" | "TextInput" => vec![PropertyChange::new("text", format!("\"{name}\""))],
28 "Image" => vec![PropertyChange::new("source", "@image-url(\"EDIT_ME.png\")".to_string())],
29 _ => vec![],
30 };
31
32 ComponentInformation {
33 name: name.to_string(),
34 category: category.to_string(),
35 is_global: false,
36 is_builtin: true,
37 is_std_widget: false,
38 is_layout,
39 fills_parent: is_layout || fills_parent,
40 is_exported: true,
41 defined_at: None,
42 default_properties,
43 }
44}
45
46fn std_widgets_info(name: &str, is_global: bool) -> ComponentInformation {
47 let (category, is_layout) = match name {
48 "GridBox" | "HorizontalBox" | "VerticalBox" => ("Layout", true),
49 "LineEdit" | "TextEdit" => ("Text Handling", false),
50 "Button" | "CheckBox" | "ComboBox" | "Slider" | "SpinBox" | "Switch" => ("Input", false),
51 "ProgressIndicator" | "Spinner" => ("Status", false),
52 "ListView" | "StandardListView" | "StandardTableView" => ("Views", false),
53 _ => ("Widgets", false),
54 };
55
56 let default_properties = match name {
57 "Button" | "CheckBox" | "LineEdit" | "Switch" | "TextEdit" => {
58 vec![PropertyChange::new("text", format!("\"{name}\""))]
59 }
60 "ComboBox" => {
61 vec![PropertyChange::new("model", "[\"first\", \"second\", \"third\"]".to_string())]
62 }
63 "Slider" | "SpinBox" => vec![
64 PropertyChange::new("minimum", "0".to_string()),
65 PropertyChange::new("value", "42".to_string()),
66 PropertyChange::new("maximum", "100".to_string()),
67 ],
68 "StandardButton" => vec![PropertyChange::new("kind", "ok".to_string())],
69 _ => vec![],
70 };
71
72 ComponentInformation {
73 name: name.to_string(),
74 category: category.to_string(),
75 is_global,
76 is_builtin: false,
77 is_std_widget: true,
78 is_layout,
79 fills_parent: is_layout,
80 is_exported: true,
81 defined_at: None,
82 default_properties,
83 }
84}
85
86fn exported_project_component_info(
87 name: &str,
88 is_global: bool,
89 position: Position,
90) -> ComponentInformation {
91 ComponentInformation {
92 name: name.to_string(),
93 category: "User Defined".to_string(),
94 is_global,
95 is_builtin: false,
96 is_std_widget: false,
97 is_layout: false,
98 fills_parent: false,
99 is_exported: true,
100 defined_at: Some(position),
101 default_properties: vec![],
102 }
103}
104
105fn file_local_component_info(name: &str, position: Position) -> ComponentInformation {
106 ComponentInformation {
107 name: name.to_string(),
108 category: "User Defined".to_string(),
109 is_global: false,
110 is_builtin: false,
111 is_std_widget: false,
112 is_layout: false,
113 fills_parent: false,
114 is_exported: false,
115 defined_at: Some(position),
116 default_properties: vec![],
117 }
118}
119
120pub fn builtin_components(document_cache: &DocumentCache, result: &mut Vec<ComponentInformation>) {
121 let registry: Ref<'_, TypeRegister> = document_cache.documents.global_type_registry.borrow();
122 result.extend(iter:registry.all_elements().iter().filter_map(|(name: &String, ty: &ElementType)| match ty {
123 ElementType::Builtin(b: &Rc) if !b.is_internal => {
124 let fills_parent: bool =
125 matches!(b.default_size_binding, DefaultSizeBinding::ExpandsToParentGeometry);
126 Some(builtin_component_info(name, fills_parent))
127 }
128 _ => None,
129 }));
130}
131
132pub fn all_exported_components(
133 document_cache: &DocumentCache,
134 filter: &mut dyn FnMut(&ComponentInformation) -> bool,
135 result: &mut Vec<ComponentInformation>,
136) {
137 for file in document_cache.documents.all_files() {
138 let Some(doc) = document_cache.documents.get_document(file) else { continue };
139 let is_builtin = file.starts_with("builtin:/");
140 let is_std_widget = is_builtin
141 && file.file_name().map(|f| f.to_str() == Some("std-widgets.slint")).unwrap_or(false);
142
143 for (exported_name, ty) in &*doc.exports {
144 let Some(c) = ty.as_ref().left() else {
145 continue;
146 };
147
148 let to_push = if is_std_widget && !exported_name.as_str().ends_with("Impl") {
149 Some(std_widgets_info(exported_name.as_str(), c.is_global()))
150 } else if !is_builtin {
151 let Ok(url) = Url::from_file_path(file) else {
152 continue;
153 };
154 let offset =
155 c.node.as_ref().map(|n| n.text_range().start().into()).unwrap_or_default();
156 Some(exported_project_component_info(
157 exported_name.as_str(),
158 c.is_global(),
159 Position { url, offset },
160 ))
161 } else {
162 continue;
163 };
164
165 let Some(to_push) = to_push else {
166 continue;
167 };
168
169 if filter(&to_push) {
170 continue;
171 }
172
173 result.push(to_push);
174 }
175 }
176}
177
178pub fn file_local_components(
179 document_cache: &DocumentCache,
180 file: &Path,
181 result: &mut Vec<ComponentInformation>,
182) {
183 let Ok(url: Url) = Url::from_file_path(file) else {
184 return;
185 };
186
187 let Some(doc: &Document) = document_cache.documents.get_document(path:file) else { return };
188 let exported_components =
189 doc.exports.iter().filter_map(|(_, e)| e.as_ref().left()).cloned().collect::<Vec<_>>();
190 for component: &Rc in &*doc.inner_components {
191 // component.exported_global_names is always empty since the pass populating it has not
192 // run.
193 if !exported_components.iter().any(|rc: &Rc| Rc::ptr_eq(this:rc, other:component)) {
194 let offset: u32 =
195 component.node.as_ref().map(|n: &SyntaxNode| n.text_range().start().into()).unwrap_or_default();
196 result.push(file_local_component_info(
197 &component.id,
198 Position { url: url.clone(), offset },
199 ));
200 }
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn builtin_component_catalog() {
210 let (dc, _, _) = crate::language::test::loaded_document_cache(r#""#.to_string());
211
212 let mut result = Default::default();
213 builtin_components(&dc, &mut result);
214
215 assert!(result.iter().all(|ci| !ci.is_std_widget));
216 assert!(result.iter().all(|ci| ci.is_exported));
217 assert!(result.iter().all(|ci| ci.is_builtin));
218 assert!(result.iter().all(|ci| !ci.is_global));
219 assert!(result.iter().any(|ci| &ci.name == "TouchArea"));
220 assert!(!result.iter().any(|ci| &ci.name == "AboutSlint"));
221 assert!(!result.iter().any(|ci| &ci.name == "ProgressIndicator"));
222 }
223
224 #[test]
225 fn exported_component_catalog_std_widgets_only() {
226 let (dc, _, _) = crate::language::test::loaded_document_cache(r#""#.to_string());
227
228 let mut result = Default::default();
229 all_exported_components(&dc, &mut |_| false, &mut result);
230
231 assert!(result.iter().all(|ci| ci.is_std_widget));
232 assert!(result.iter().all(|ci| ci.is_exported));
233 assert!(result.iter().all(|ci| !ci.is_builtin));
234 // assert!(result.iter().all(|ci| ci.is_global)); // mixed!
235 assert!(!result.iter().any(|ci| &ci.name == "TouchArea"));
236 assert!(result.iter().any(|ci| &ci.name == "AboutSlint"));
237 assert!(result.iter().any(|ci| &ci.name == "ProgressIndicator"));
238 }
239
240 #[test]
241 fn exported_component_catalog_filtered() {
242 let (dc, _, _) = crate::language::test::loaded_document_cache(r#""#.to_string());
243
244 let mut result = Default::default();
245 all_exported_components(&dc, &mut |_| true, &mut result);
246
247 assert!(result.is_empty());
248 }
249
250 #[test]
251 fn exported_component_catalog_exported_component() {
252 let baseline = {
253 let (dc, _, _) = crate::language::test::loaded_document_cache(r#""#.to_string());
254
255 let mut result = Default::default();
256 all_exported_components(&dc, &mut |_| false, &mut result);
257 result.len()
258 };
259
260 let (dc, _, _) = crate::language::test::loaded_document_cache(
261 r#"export component Test1 {}"#.to_string(),
262 );
263
264 let mut result = Default::default();
265 all_exported_components(&dc, &mut |_| false, &mut result);
266
267 assert!(result.iter().any(|ci| &ci.name == "Test1"));
268 assert!(!result.iter().any(|ci| &ci.name == "TouchArea"));
269 assert!(result.iter().any(|ci| &ci.name == "AboutSlint"));
270 assert!(result.iter().any(|ci| &ci.name == "ProgressIndicator"));
271 assert_eq!(result.len(), baseline + 1);
272 }
273
274 #[test]
275 fn local_component_catalog_one_unexported_component() {
276 let (dc, url, _) =
277 crate::language::test::loaded_document_cache(r#"component Test1 {}"#.to_string());
278
279 let mut result = Default::default();
280 file_local_components(&dc, &url.to_file_path().unwrap(), &mut result);
281 assert!(result.is_empty()); // Test1 is implicitly exported!
282 }
283
284 #[test]
285 fn local_component_catalog_two_unexported_components_without_export() {
286 let (dc, url, _) = crate::language::test::loaded_document_cache(
287 r#"
288 component Test1 {}
289 component Test2 {}"#
290 .to_string(),
291 );
292
293 let mut result = Default::default();
294 file_local_components(&dc, &url.to_file_path().unwrap(), &mut result);
295 assert_eq!(result.len(), 1);
296
297 let test1 = result.iter().find(|ci| &ci.name == "Test1").unwrap();
298 assert!(!test1.is_std_widget);
299 assert!(!test1.is_builtin);
300 assert!(!test1.is_exported);
301 assert!(!test1.is_global);
302 assert!(!result.iter().any(|ci| &ci.name == "Test2")); // Test2 is implicitly exported
303 }
304 #[test]
305 fn local_component_catalog_two_unexported_components_with_export() {
306 let (dc, url, _) = crate::language::test::loaded_document_cache(
307 r#"
308 component Test1 {}
309 export component Export1 {}
310 component Test2 {}"#
311 .to_string(),
312 );
313
314 let mut result = Default::default();
315 file_local_components(&dc, &url.to_file_path().unwrap(), &mut result);
316 assert_eq!(result.len(), 2);
317
318 let test1 = result.iter().find(|ci| &ci.name == "Test1").unwrap();
319 assert!(!test1.is_std_widget);
320 assert!(!test1.is_builtin);
321 assert!(!test1.is_exported);
322 assert!(!test1.is_global);
323 let test2 = result.iter().find(|ci| &ci.name == "Test2").unwrap();
324 assert!(!test2.is_std_widget);
325 assert!(!test2.is_builtin);
326 assert!(!test2.is_exported);
327 assert!(!test2.is_global);
328 assert!(!result.iter().any(|ci| &ci.name == "Export1"));
329 }
330}
331