1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! Test that all styles have the same API.
5
6use i_slint_compiler::expression_tree::Expression;
7use i_slint_compiler::langtype::{Function, Type};
8use i_slint_compiler::object_tree::PropertyVisibility;
9use i_slint_compiler::typeloader::TypeLoader;
10use i_slint_compiler::typeregister::TypeRegister;
11use smol_str::{SmolStr, ToSmolStr};
12use std::collections::BTreeMap;
13use std::collections::HashSet;
14use std::fmt::Display;
15use std::rc::Rc;
16
17#[derive(PartialEq, Debug)]
18struct PropertyInfo {
19 ty: Type,
20 vis: PropertyVisibility,
21 pure: bool,
22}
23
24impl Display for PropertyInfo {
25 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26 write!(f, "{}/{}{:?}", self.ty, if self.pure { "pure-" } else { "" }, self.vis)?;
27 if let Type::Callback(cb: &Rc) = &self.ty {
28 if !cb.arg_names.is_empty() {
29 write!(f, "{:?}", cb.arg_names)?
30 }
31 }
32 Ok(())
33 }
34}
35
36#[derive(Default)]
37struct Component {
38 properties: BTreeMap<String, PropertyInfo>,
39 accessible_role: Option<String>,
40}
41
42#[derive(Default)]
43struct Style {
44 components: BTreeMap<SmolStr, Component>,
45 structs: BTreeMap<SmolStr, Type>,
46}
47
48fn load_component(component: &Rc<i_slint_compiler::object_tree::Component>) -> Component {
49 let mut result = Component::default();
50 let mut elem = component.root_element.clone();
51 loop {
52 result.properties.extend(
53 elem.borrow()
54 .property_declarations
55 .iter()
56 .filter(|(_, v)| v.visibility != PropertyVisibility::Private)
57 .map(|(k, v)| {
58 (
59 k.to_string(),
60 PropertyInfo {
61 ty: v.property_type.clone(),
62 vis: v.visibility,
63 pure: v.pure.unwrap_or(false),
64 },
65 )
66 }),
67 );
68
69 if result.accessible_role.is_none() {
70 if let Some(role) = elem.borrow().bindings.get("accessible-role") {
71 match &role.borrow().expression {
72 Expression::Invalid => (),
73 Expression::EnumerationValue(e) => {
74 result.accessible_role = Some(e.enumeration.values[e.value].to_string())
75 }
76 e => panic!(
77 "accessible-role not an EnumerationValue : {e:?} (for {:?})",
78 role.borrow().span
79 ),
80 };
81 }
82 }
83
84 let e = match &elem.borrow().base_type {
85 i_slint_compiler::langtype::ElementType::Component(r) => r.root_element.clone(),
86 i_slint_compiler::langtype::ElementType::Builtin(b) => {
87 let builtins = i_slint_compiler::typeregister::reserved_properties()
88 .map(|x| x.0.to_smolstr())
89 .collect::<HashSet<_>>();
90 result.properties.extend(
91 b.properties.iter().filter(|(k, _)| !builtins.contains(*k)).map(|(k, v)| {
92 (
93 k.to_string(),
94 PropertyInfo {
95 ty: v.ty.clone(),
96 vis: v.property_visibility,
97 pure: false,
98 },
99 )
100 }),
101 );
102 // Synthesize focus() and `clear-focus()` as styles written in .slint will have it but the qt style exposes NativeXX directly.
103 if b.accepts_focus {
104 result.properties.insert(
105 "focus".into(),
106 PropertyInfo {
107 ty: Type::Function(Rc::new(Function {
108 return_type: Type::Void,
109 args: vec![],
110 arg_names: vec![],
111 })),
112 vis: PropertyVisibility::Public,
113 pure: false,
114 },
115 );
116 result.properties.insert(
117 "clear-focus".into(),
118 PropertyInfo {
119 ty: Type::Function(Rc::new(Function {
120 return_type: Type::Void,
121 args: vec![],
122 arg_names: vec![],
123 })),
124 vis: PropertyVisibility::Public,
125 pure: false,
126 },
127 );
128 }
129 break;
130 }
131 i_slint_compiler::langtype::ElementType::Native(_) => unreachable!(),
132 i_slint_compiler::langtype::ElementType::Error => unreachable!(),
133 i_slint_compiler::langtype::ElementType::Global => break,
134 };
135 elem = e;
136 }
137 result
138}
139
140fn load_style(style_name: String) -> Style {
141 let mut config = i_slint_compiler::CompilerConfiguration::new(
142 i_slint_compiler::generator::OutputFormat::Llr,
143 );
144 config.style = Some(style_name);
145 let mut diag = i_slint_compiler::diagnostics::BuildDiagnostics::default();
146 let mut loader = TypeLoader::new(TypeRegister::builtin(), config, &mut diag);
147 // ensure that the style is loaded
148 spin_on::spin_on(loader.import_component("std-widgets.slint", "Button", &mut diag));
149
150 if diag.has_errors() {
151 #[cfg(feature = "display-diagnostics")]
152 diag.print();
153 panic!("error parsing style {}", loader.compiler_config.style.as_ref().unwrap());
154 }
155
156 let doc = loader
157 .get_document(&loader.resolve_import_path(None, "std-widgets.slint").unwrap().0)
158 .unwrap();
159
160 let mut style = Style::default();
161
162 for (name, what) in doc.exports.iter() {
163 let name = &**name;
164 match what {
165 itertools::Either::Left(component) => {
166 let component = load_component(component);
167 let old = style.components.insert(name.clone(), component);
168 assert!(
169 old.is_none(),
170 "Duplicated component '{name}' in style {}",
171 loader.compiler_config.style.as_ref().unwrap()
172 );
173 }
174 itertools::Either::Right(ty) => {
175 let old = style.structs.insert(name.clone(), ty.clone());
176 assert!(
177 old.is_none(),
178 "Duplicated struct '{name}' in style {}",
179 loader.compiler_config.style.as_ref().unwrap()
180 );
181 }
182 }
183 }
184 style
185}
186
187fn compare_styles(base: &Style, mut other: Style, style_name: &str) -> bool {
188 let mut ok = true;
189 for (compo_name, c1) in base.components.iter() {
190 // These more or less internals component can have different properties
191 let ignore_extra =
192 matches!(compo_name.as_str(), "TabImpl" | "TabWidgetImpl" | "StyleMetrics");
193 if let Some(mut c2) = other.components.remove(compo_name) {
194 if c1.accessible_role != c2.accessible_role {
195 eprintln!(
196 "Mismatch accessible-role for {compo_name} in {style_name} : {:?} != {:?}",
197 c2.accessible_role, c1.accessible_role
198 );
199 ok = false;
200 }
201
202 for (prop_name, p1) in c1.properties.iter() {
203 if let Some(p2) = c2.properties.remove(prop_name) {
204 if p1 != &p2 {
205 eprintln!("Mismatch property info '{compo_name}::{prop_name}' in {style_name} : {p1} != {p2}",);
206 ok = false;
207 }
208 } else if !ignore_extra {
209 eprintln!("Property '{compo_name}::{prop_name}' not found in {style_name}");
210 ok = false;
211 }
212 }
213 // Extra property on StyleMetrics are allowed
214 if !c2.properties.is_empty() && !ignore_extra {
215 for prop_name in c2.properties.keys() {
216 eprintln!("Extra property '{compo_name}::{prop_name}' found in {style_name}");
217 }
218 ok = false;
219 }
220 } else {
221 eprintln!("Component '{compo_name}' not found in {style_name}");
222 ok = false;
223 }
224 }
225 if !other.components.is_empty() {
226 for compo_name in other.components.keys() {
227 eprintln!("Extra component '{compo_name}' found in {style_name}");
228 }
229 ok = false;
230 }
231 if base.structs != other.structs {
232 eprintln!(
233 "Mismatch struct export in '{style_name}': {:?} != {:?}",
234 base.structs, other.structs
235 );
236 ok = false;
237 }
238 ok
239}
240
241#[test]
242fn check_styles() {
243 let base = load_style("fluent".into());
244
245 let mut ok = true;
246 for s in i_slint_compiler::fileaccess::styles() {
247 let other = load_style(s.into());
248 ok &= compare_styles(&base, other, s);
249 }
250
251 assert!(ok);
252}
253