| 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 | |
| 6 | use i_slint_compiler::expression_tree::Expression; |
| 7 | use i_slint_compiler::langtype::{Function, Type}; |
| 8 | use i_slint_compiler::object_tree::PropertyVisibility; |
| 9 | use i_slint_compiler::typeloader::TypeLoader; |
| 10 | use i_slint_compiler::typeregister::TypeRegister; |
| 11 | use smol_str::{SmolStr, ToSmolStr}; |
| 12 | use std::collections::BTreeMap; |
| 13 | use std::collections::HashSet; |
| 14 | use std::fmt::Display; |
| 15 | use std::rc::Rc; |
| 16 | |
| 17 | #[derive (PartialEq, Debug)] |
| 18 | struct PropertyInfo { |
| 19 | ty: Type, |
| 20 | vis: PropertyVisibility, |
| 21 | pure: bool, |
| 22 | } |
| 23 | |
| 24 | impl 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)] |
| 37 | struct Component { |
| 38 | properties: BTreeMap<String, PropertyInfo>, |
| 39 | accessible_role: Option<String>, |
| 40 | } |
| 41 | |
| 42 | #[derive (Default)] |
| 43 | struct Style { |
| 44 | components: BTreeMap<SmolStr, Component>, |
| 45 | structs: BTreeMap<SmolStr, Type>, |
| 46 | } |
| 47 | |
| 48 | fn 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 | |
| 140 | fn 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 | |
| 187 | fn 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 ] |
| 242 | fn 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 | |