// Copyright © SixtyFPS GmbH // SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0 use std::borrow::Cow; use std::collections::{BTreeMap, HashMap}; use std::fmt::Display; use std::rc::Rc; use itertools::Itertools; use smol_str::SmolStr; use crate::expression_tree::{BuiltinFunction, Expression, Unit}; use crate::object_tree::{Component, PropertyVisibility}; use crate::parser::syntax_nodes; use crate::typeregister::TypeRegister; #[derive(Debug, Clone, Default)] pub enum Type { /// Correspond to an uninitialized type, or an error #[default] Invalid, /// The type of an expression that return nothing Void, /// The type of a property two way binding whose type was not yet inferred InferredProperty, /// The type of a callback alias whose type was not yet inferred InferredCallback, Callback(Rc), Function(Rc), ComponentFactory, // Other property types: Float32, Int32, String, Color, Duration, PhysicalLength, LogicalLength, Rem, Angle, Percent, Image, Bool, /// Fake type that can represent anything that can be converted into a model. Model, PathData, // Either a vector of path elements or a two vectors of events and coordinates Easing, Brush, /// This is usually a model Array(Rc), Struct(Rc), Enumeration(Rc), /// A type made up of the product of several "unit" types. /// The first parameter is the unit, and the second parameter is the power. /// The vector should be sorted by 1) the power, 2) the unit. UnitProduct(Vec<(Unit, i8)>), ElementReference, /// This is a `SharedArray` LayoutCache, } impl core::cmp::PartialEq for Type { fn eq(&self, other: &Self) -> bool { match self { Type::Invalid => matches!(other, Type::Invalid), Type::Void => matches!(other, Type::Void), Type::InferredProperty => matches!(other, Type::InferredProperty), Type::InferredCallback => matches!(other, Type::InferredCallback), Type::Callback(lhs) => { matches!(other, Type::Callback(rhs) if lhs == rhs) } Type::Function(lhs) => { matches!(other, Type::Function(rhs) if lhs == rhs) } Type::ComponentFactory => matches!(other, Type::ComponentFactory), Type::Float32 => matches!(other, Type::Float32), Type::Int32 => matches!(other, Type::Int32), Type::String => matches!(other, Type::String), Type::Color => matches!(other, Type::Color), Type::Duration => matches!(other, Type::Duration), Type::Angle => matches!(other, Type::Angle), Type::PhysicalLength => matches!(other, Type::PhysicalLength), Type::LogicalLength => matches!(other, Type::LogicalLength), Type::Rem => matches!(other, Type::Rem), Type::Percent => matches!(other, Type::Percent), Type::Image => matches!(other, Type::Image), Type::Bool => matches!(other, Type::Bool), Type::Model => matches!(other, Type::Model), Type::PathData => matches!(other, Type::PathData), Type::Easing => matches!(other, Type::Easing), Type::Brush => matches!(other, Type::Brush), Type::Array(a) => matches!(other, Type::Array(b) if a == b), Type::Struct(lhs) => { matches!(other, Type::Struct(rhs) if lhs.fields == rhs.fields && lhs.name == rhs.name) } Type::Enumeration(lhs) => matches!(other, Type::Enumeration(rhs) if lhs == rhs), Type::UnitProduct(a) => matches!(other, Type::UnitProduct(b) if a == b), Type::ElementReference => matches!(other, Type::ElementReference), Type::LayoutCache => matches!(other, Type::LayoutCache), } } } impl Display for Type { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Type::Invalid => write!(f, ""), Type::Void => write!(f, "void"), Type::InferredProperty => write!(f, "?"), Type::InferredCallback => write!(f, "callback"), Type::Callback(callback) => { write!(f, "callback")?; if !callback.args.is_empty() { write!(f, "(")?; for (i, arg) in callback.args.iter().enumerate() { if i > 0 { write!(f, ",")?; } write!(f, "{arg}")?; } write!(f, ")")? } write!(f, "-> {}", callback.return_type)?; Ok(()) } Type::ComponentFactory => write!(f, "component-factory"), Type::Function(function) => { write!(f, "function(")?; for (i, arg) in function.args.iter().enumerate() { if i > 0 { write!(f, ",")?; } write!(f, "{arg}")?; } write!(f, ") -> {}", function.return_type) } Type::Float32 => write!(f, "float"), Type::Int32 => write!(f, "int"), Type::String => write!(f, "string"), Type::Duration => write!(f, "duration"), Type::Angle => write!(f, "angle"), Type::PhysicalLength => write!(f, "physical-length"), Type::LogicalLength => write!(f, "length"), Type::Rem => write!(f, "relative-font-size"), Type::Percent => write!(f, "percent"), Type::Color => write!(f, "color"), Type::Image => write!(f, "image"), Type::Bool => write!(f, "bool"), Type::Model => write!(f, "model"), Type::Array(t) => write!(f, "[{t}]"), Type::Struct(t) => write!(f, "{t}"), Type::PathData => write!(f, "pathdata"), Type::Easing => write!(f, "easing"), Type::Brush => write!(f, "brush"), Type::Enumeration(enumeration) => write!(f, "enum {}", enumeration.name), Type::UnitProduct(vec) => { const POWERS: &[char] = &['⁰', '¹', '²', '³', '⁴', '⁵', '⁶', '⁷', '⁸', '⁹']; let mut x = vec.iter().map(|(unit, power)| { if *power == 1 { return unit.to_string(); } let mut res = format!("{}{}", unit, if *power < 0 { "⁻" } else { "" }); let value = power.abs().to_string(); for x in value.as_bytes() { res.push(POWERS[(x - b'0') as usize]); } res }); write!(f, "({})", x.join("×")) } Type::ElementReference => write!(f, "element ref"), Type::LayoutCache => write!(f, "layout cache"), } } } impl From> for Type { fn from(value: Rc) -> Self { Self::Struct(value) } } impl Type { /// valid type for properties pub fn is_property_type(&self) -> bool { matches!( self, Self::Float32 | Self::Int32 | Self::String | Self::Color | Self::ComponentFactory | Self::Duration | Self::Angle | Self::PhysicalLength | Self::LogicalLength | Self::Rem | Self::Percent | Self::Image | Self::Bool | Self::Easing | Self::Enumeration(_) | Self::ElementReference | Self::Struct { .. } | Self::Array(_) | Self::Brush | Self::InferredProperty ) } pub fn ok_for_public_api(&self) -> bool { !matches!(self, Self::Easing) } /// Assume it is an enumeration, panic if it isn't pub fn as_enum(&self) -> &Rc { match self { Type::Enumeration(e) => e, _ => panic!("should be an enumeration, bug in compiler pass"), } } /// Return true if the type can be converted to the other type pub fn can_convert(&self, other: &Self) -> bool { let can_convert_struct = |a: &BTreeMap, b: &BTreeMap| { // the struct `b` has property that the struct `a` doesn't let mut has_more_property = false; for (k, v) in b { match a.get(k) { Some(t) if !t.can_convert(v) => return false, None => has_more_property = true, _ => (), } } if has_more_property { // we should reject the conversion if `a` has property that `b` doesn't have if a.keys().any(|k| !b.contains_key(k)) { return false; } } true }; match (self, other) { (a, b) if a == b => true, (_, Type::Invalid) | (_, Type::Void) | (Type::Float32, Type::Int32) | (Type::Float32, Type::String) | (Type::Int32, Type::Float32) | (Type::Int32, Type::String) | (Type::Float32, Type::Model) | (Type::Int32, Type::Model) | (Type::PhysicalLength, Type::LogicalLength) | (Type::LogicalLength, Type::PhysicalLength) | (Type::Rem, Type::LogicalLength) | (Type::Rem, Type::PhysicalLength) | (Type::LogicalLength, Type::Rem) | (Type::PhysicalLength, Type::Rem) | (Type::Percent, Type::Float32) | (Type::Brush, Type::Color) | (Type::Color, Type::Brush) => true, (Type::Array(a), Type::Model) if a.is_property_type() => true, (Type::Struct(a), Type::Struct(b)) => can_convert_struct(&a.fields, &b.fields), (Type::UnitProduct(u), o) => match o.as_unit_product() { Some(o) => unit_product_length_conversion(u.as_slice(), o.as_slice()).is_some(), None => false, }, (o, Type::UnitProduct(u)) => match o.as_unit_product() { Some(o) => unit_product_length_conversion(u.as_slice(), o.as_slice()).is_some(), None => false, }, _ => false, } } /// If this is a number type which should be used with an unit, this returns the default unit /// otherwise, returns None pub fn default_unit(&self) -> Option { match self { Type::Duration => Some(Unit::Ms), Type::PhysicalLength => Some(Unit::Phx), Type::LogicalLength => Some(Unit::Px), Type::Rem => Some(Unit::Rem), // Unit::Percent is special that it does not combine with other units like Type::Percent => None, Type::Angle => Some(Unit::Deg), Type::Invalid => None, Type::Void => None, Type::InferredProperty | Type::InferredCallback => None, Type::Callback { .. } => None, Type::ComponentFactory => None, Type::Function { .. } => None, Type::Float32 => None, Type::Int32 => None, Type::String => None, Type::Color => None, Type::Image => None, Type::Bool => None, Type::Model => None, Type::PathData => None, Type::Easing => None, Type::Brush => None, Type::Array(_) => None, Type::Struct { .. } => None, Type::Enumeration(_) => None, Type::UnitProduct(_) => None, Type::ElementReference => None, Type::LayoutCache => None, } } /// Return a unit product vector even for single scalar pub fn as_unit_product(&self) -> Option> { match self { Type::UnitProduct(u) => Some(u.clone()), Type::Float32 | Type::Int32 => Some(Vec::new()), Type::Percent => Some(Vec::new()), _ => self.default_unit().map(|u| vec![(u, 1)]), } } } #[derive(Debug, Clone)] pub enum BuiltinPropertyDefault { None, Expr(Expression), /// When materializing a property of this type, it will be initialized with an Expression that depends on the ElementRc WithElement(fn(&crate::object_tree::ElementRc) -> Expression), /// The property is actually not a property but a builtin function BuiltinFunction(BuiltinFunction), } impl BuiltinPropertyDefault { pub fn expr(&self, elem: &crate::object_tree::ElementRc) -> Option { match self { BuiltinPropertyDefault::None => None, BuiltinPropertyDefault::Expr(expression) => Some(expression.clone()), BuiltinPropertyDefault::WithElement(init_expr) => Some(init_expr(elem)), BuiltinPropertyDefault::BuiltinFunction(..) => { unreachable!("can't get an expression for functions") } } } } /// Information about properties in NativeClass #[derive(Debug, Clone)] pub struct BuiltinPropertyInfo { /// The property type pub ty: Type, /// When != None, this is the initial value that we will have to set if no other binding were specified pub default_value: BuiltinPropertyDefault, pub property_visibility: PropertyVisibility, } impl BuiltinPropertyInfo { pub fn new(ty: Type) -> Self { Self { ty, default_value: BuiltinPropertyDefault::None, property_visibility: PropertyVisibility::InOut, } } pub fn is_native_output(&self) -> bool { matches!(self.property_visibility, PropertyVisibility::InOut | PropertyVisibility::Output) } } impl From for BuiltinPropertyInfo { fn from(function: BuiltinFunction) -> Self { Self { ty: Type::Function(function.ty()), default_value: BuiltinPropertyDefault::BuiltinFunction(function), property_visibility: PropertyVisibility::Public, } } } /// The base of an element #[derive(Clone, Debug, derive_more::From)] pub enum ElementType { /// The element is based of a component Component(Rc), /// The element is a builtin element Builtin(Rc), /// The native type was resolved by the resolve_native_class pass. Native(Rc), /// The base element couldn't be looked up Error, /// This should be the base type of the root element of a global component Global, } impl PartialEq for ElementType { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Component(a), Self::Component(b)) => Rc::ptr_eq(a, b), (Self::Builtin(a), Self::Builtin(b)) => Rc::ptr_eq(a, b), (Self::Native(a), Self::Native(b)) => Rc::ptr_eq(a, b), (Self::Error, Self::Error) | (Self::Global, Self::Global) => true, _ => false, } } } impl ElementType { pub fn lookup_property<'a>(&self, name: &'a str) -> PropertyLookupResult<'a> { match self { Self::Component(c) => c.root_element.borrow().lookup_property(name), Self::Builtin(b) => { let resolved_name = if let Some(alias_name) = b.native_class.lookup_alias(name.as_ref()) { Cow::Owned(alias_name.to_string()) } else { Cow::Borrowed(name) }; match b.properties.get(resolved_name.as_ref()) { None => { if b.is_non_item_type { PropertyLookupResult::invalid(resolved_name) } else { crate::typeregister::reserved_property(name) } } Some(p) => PropertyLookupResult { resolved_name, property_type: p.ty.clone(), property_visibility: p.property_visibility, declared_pure: None, is_local_to_component: false, is_in_direct_base: false, builtin_function: match &p.default_value { BuiltinPropertyDefault::BuiltinFunction(f) => Some(f.clone()), _ => None, }, }, } } Self::Native(n) => { let resolved_name = if let Some(alias_name) = n.lookup_alias(name.as_ref()) { Cow::Owned(alias_name.to_string()) } else { Cow::Borrowed(name) }; let property_type = n.lookup_property(resolved_name.as_ref()).cloned().unwrap_or_default(); PropertyLookupResult { resolved_name, property_type, property_visibility: PropertyVisibility::InOut, declared_pure: None, is_local_to_component: false, is_in_direct_base: false, builtin_function: None, } } _ => PropertyLookupResult::invalid(Cow::Borrowed(name)), } } /// List of sub properties valid for the auto completion pub fn property_list(&self) -> Vec<(SmolStr, Type)> { match self { Self::Component(c) => { let mut r = c.root_element.borrow().base_type.property_list(); r.extend( c.root_element .borrow() .property_declarations .iter() .filter(|(_, d)| d.visibility != PropertyVisibility::Private) .map(|(k, d)| (k.clone(), d.property_type.clone())), ); r } Self::Builtin(b) => { b.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect() } Self::Native(n) => { n.properties.iter().map(|(k, t)| (k.clone(), t.ty.clone())).collect() } _ => Vec::new(), } } /// This function looks at the element and checks whether it can have Elements of type `name` as children. /// In addition to what `accepts_child_element` does, this method also probes the type of `name`. /// It returns an Error if that is not possible or an `ElementType` if it is. pub fn lookup_type_for_child_element( &self, name: &str, tr: &TypeRegister, ) -> Result { match self { Self::Component(component) => { let base_type = match &*component.child_insertion_point.borrow() { Some(insert_in) => insert_in.0.borrow().base_type.clone(), None => { let base_type = component.root_element.borrow().base_type.clone(); if base_type == tr.empty_type() { return Err(format!("'{}' cannot have children. Only components with @children can have children", component.id)); } base_type } }; base_type.lookup_type_for_child_element(name, tr) } Self::Builtin(builtin) => { if builtin.disallow_global_types_as_child_elements { if let Some(child_type) = builtin.additional_accepted_child_types.get(name) { return Ok(child_type.clone().into()); } else if builtin.additional_accept_self && name == builtin.native_class.class_name { return Ok(builtin.clone().into()); } let mut valid_children: Vec<_> = builtin.additional_accepted_child_types.keys().cloned().collect(); if builtin.additional_accept_self { valid_children.push(builtin.native_class.class_name.clone()); } valid_children.sort(); let err = if valid_children.is_empty() { format!("{} cannot have children elements", builtin.native_class.class_name,) } else { format!( "{} is not allowed within {}. Only {} are valid children", name, builtin.native_class.class_name, valid_children.join(" ") ) }; return Err(err); } let err = match tr.lookup_element(name) { Err(e) => e, Ok(t) => { if !tr.expose_internal_types && matches!(&t, Self::Builtin(e) if e.is_internal) { format!("Unknown element '{name}'. (The type exist as an internal type, but cannot be accessed in this scope)") } else { return Ok(t); } } }; if let Some(child_type) = builtin.additional_accepted_child_types.get(name) { return Ok(child_type.clone().into()); } else if builtin.additional_accept_self && name == builtin.native_class.class_name { return Ok(builtin.clone().into()); } match tr.lookup(name) { Type::Invalid => Err(err), ty => Err(format!("'{ty}' cannot be used as an element")), } } _ => tr.lookup_element(name).and_then(|t| { if !tr.expose_internal_types && matches!(&t, Self::Builtin(e) if e.is_internal) { Err(format!("Unknown element '{name}'. (The type exist as an internal type, but cannot be accessed in this scope)")) } else { Ok(t) } }) } } /// Assume this is a builtin type, panic if it isn't pub fn as_builtin(&self) -> &BuiltinElement { match self { Self::Builtin(b) => b, Self::Component(_) => panic!("This should not happen because of inlining"), _ => panic!("invalid type"), } } /// Assume this is a builtin type, panic if it isn't pub fn as_native(&self) -> &NativeClass { match self { Self::Native(b) => b, Self::Component(_) => { panic!("This should not happen because of native class resolution") } _ => panic!("invalid type"), } } /// Assume it is a Component, panic if it isn't pub fn as_component(&self) -> &Rc { match self { Self::Component(c) => c, _ => panic!("should be a component because of the repeater_component pass"), } } /// Returns the Slint type name if applicable (for example `Rectangle` or `MyButton` when `component MyButton {}` is used as `MyButton` element) pub fn type_name(&self) -> Option<&str> { match self { ElementType::Component(component) => Some(&component.id), ElementType::Builtin(b) => Some(&b.name), ElementType::Native(_) => None, // Too late, caller should call this function before the native class lowering ElementType::Error => None, ElementType::Global => None, } } } impl Display for ElementType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Component(c) => c.id.fmt(f), Self::Builtin(b) => b.name.fmt(f), Self::Native(b) => b.class_name.fmt(f), Self::Error => write!(f, ""), Self::Global => Ok(()), } } } impl Default for ElementType { fn default() -> Self { Self::Error } } #[derive(Debug, Clone, Default)] pub struct NativeClass { pub parent: Option>, pub class_name: SmolStr, pub cpp_vtable_getter: String, pub properties: HashMap, pub deprecated_aliases: HashMap, pub cpp_type: Option, pub rust_type_constructor: Option, } impl NativeClass { pub fn new(class_name: &str) -> Self { let cpp_vtable_getter = format!("SLINT_GET_ITEM_VTABLE({class_name}VTable)"); Self { class_name: class_name.into(), cpp_vtable_getter, properties: Default::default(), ..Default::default() } } pub fn new_with_properties( class_name: &str, properties: impl IntoIterator, ) -> Self { let mut class = Self::new(class_name); class.properties = properties.into_iter().collect(); class } pub fn property_count(&self) -> usize { self.properties.len() + self.parent.clone().map(|p| p.property_count()).unwrap_or_default() } pub fn lookup_property(&self, name: &str) -> Option<&Type> { if let Some(bty) = self.properties.get(name) { Some(&bty.ty) } else if let Some(parent_class) = &self.parent { parent_class.lookup_property(name) } else { None } } pub fn lookup_alias(&self, name: &str) -> Option<&str> { if let Some(alias_target) = self.deprecated_aliases.get(name) { Some(alias_target) } else if self.properties.contains_key(name) { None } else if let Some(parent_class) = &self.parent { parent_class.lookup_alias(name) } else { None } } } #[derive(Debug, Clone, Copy, PartialEq, Default)] pub enum DefaultSizeBinding { /// There should not be a default binding for the size #[default] None, /// The size should default to `width:100%; height:100%` ExpandsToParentGeometry, /// The size should default to the item's implicit size ImplicitSize, } #[derive(Debug, Clone, Default)] pub struct BuiltinElement { pub name: SmolStr, pub native_class: Rc, pub properties: BTreeMap, /// Additional builtin element that can be accpeted as child of this element /// (example `Tab` in `TabWidget`, `Row` in `GridLayout` and the path elements in `Path`) pub additional_accepted_child_types: HashMap>, /// `Self` is conceptually in `additional_accepted_child_types` (which it can't otherwise that'd make a Rc loop) pub additional_accept_self: bool, pub disallow_global_types_as_child_elements: bool, /// Non-item type do not have reserved properties (x/width/rowspan/...) added to them (eg: PropertyAnimation) pub is_non_item_type: bool, pub accepts_focus: bool, pub is_global: bool, pub default_size_binding: DefaultSizeBinding, /// When true this is an internal type not shown in the auto-completion pub is_internal: bool, } impl BuiltinElement { pub fn new(native_class: Rc) -> Self { Self { name: native_class.class_name.clone(), native_class, ..Default::default() } } } #[derive(PartialEq, Debug)] pub struct PropertyLookupResult<'a> { pub resolved_name: std::borrow::Cow<'a, str>, pub property_type: Type, pub property_visibility: PropertyVisibility, pub declared_pure: Option, /// True if the property is part of the the current component (for visibility purposes) pub is_local_to_component: bool, /// True if the property in the direct base of the component (for visibility purposes) pub is_in_direct_base: bool, /// If the property is a builtin function pub builtin_function: Option, } impl<'a> PropertyLookupResult<'a> { pub fn is_valid(&self) -> bool { self.property_type != Type::Invalid } /// Can this property be used in an assignment pub fn is_valid_for_assignment(&self) -> bool { !matches!( (self.property_visibility, self.is_local_to_component), (PropertyVisibility::Private, false) | (PropertyVisibility::Input, true) | (PropertyVisibility::Output, false) ) } pub fn invalid(resolved_name: Cow<'a, str>) -> Self { Self { resolved_name, property_type: Type::Invalid, property_visibility: PropertyVisibility::Private, declared_pure: None, is_local_to_component: false, is_in_direct_base: false, builtin_function: None, } } } #[derive(Debug, Clone, PartialEq)] pub struct Function { pub return_type: Type, pub args: Vec, /// The optional names of the arguments (empty string means not set). /// The names are not technically part of the type, but it is good to have them available for auto-completion pub arg_names: Vec, } #[derive(Debug, Clone)] pub struct Struct { pub fields: BTreeMap, /// When declared in .slint as `struct Foo := { }`, then the name is "Foo" /// When there is no node, but there is a name, then it is a builtin type pub name: Option, /// When declared in .slint, this is the node of the declaration. pub node: Option, /// derived pub rust_attributes: Option>, } impl Display for Struct { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(name) = &self.name { if let Some(separator_pos) = name.rfind("::") { // write the slint type and not the native type write!(f, "{}", &name[separator_pos + 2..]) } else { write!(f, "{name}") } } else { write!(f, "{{ ")?; for (k, v) in &self.fields { write!(f, "{k}: {v},")?; } write!(f, "}}") } } } #[derive(Debug, Clone)] pub struct Enumeration { pub name: SmolStr, pub values: Vec, pub default_value: usize, // index in values // For non-builtins enums, this is the declaration node pub node: Option, } impl PartialEq for Enumeration { fn eq(&self, other: &Self) -> bool { self.name.eq(&other.name) } } impl Enumeration { pub fn default_value(self: Rc) -> EnumerationValue { EnumerationValue { value: self.default_value, enumeration: self.clone() } } pub fn try_value_from_string(self: Rc, value: &str) -> Option { self.values.iter().enumerate().find_map(|(idx, name)| { if name == value { Some(EnumerationValue { value: idx, enumeration: self.clone() }) } else { None } }) } } #[derive(Clone, Debug)] pub struct EnumerationValue { pub value: usize, // index in enumeration.values pub enumeration: Rc, } impl PartialEq for EnumerationValue { fn eq(&self, other: &Self) -> bool { Rc::ptr_eq(&self.enumeration, &other.enumeration) && self.value == other.value } } impl std::fmt::Display for EnumerationValue { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.enumeration.values[self.value].fmt(f) } } impl EnumerationValue { pub fn to_pascal_case(&self) -> String { crate::generator::to_pascal_case(&self.enumeration.values[self.value]) } } #[derive(Debug, PartialEq)] pub struct LengthConversionPowers { pub rem_to_px_power: i8, pub px_to_phx_power: i8, } /// If the `Type::UnitProduct(a)` can be converted to `Type::UnitProduct(a)` by multiplying /// by the scale factor, return that scale factor, otherwise, return None pub fn unit_product_length_conversion( a: &[(Unit, i8)], b: &[(Unit, i8)], ) -> Option { let mut units = [0i8; 16]; for (u, count) in a { units[*u as usize] += count; } for (u, count) in b { units[*u as usize] -= count; } if units[Unit::Px as usize] + units[Unit::Phx as usize] + units[Unit::Rem as usize] != 0 { return None; } if units[Unit::Rem as usize] != 0 && units[Unit::Phx as usize] == -units[Unit::Rem as usize] && units[Unit::Px as usize] == 0 { units[Unit::Px as usize] = -units[Unit::Rem as usize]; units[Unit::Phx as usize] = -units[Unit::Rem as usize]; } let result = LengthConversionPowers { rem_to_px_power: if units[Unit::Rem as usize] != 0 { units[Unit::Px as usize] } else { 0 }, px_to_phx_power: if units[Unit::Px as usize] != 0 { units[Unit::Phx as usize] } else { 0 }, }; units[Unit::Px as usize] = 0; units[Unit::Phx as usize] = 0; units[Unit::Rem as usize] = 0; units.into_iter().all(|x| x == 0).then_some(result) } #[test] fn unit_product_length_conversion_test() { use Option::None; use Unit::*; assert_eq!( unit_product_length_conversion(&[(Px, 1)], &[(Phx, 1)]), Some(LengthConversionPowers { rem_to_px_power: 0, px_to_phx_power: -1 }) ); assert_eq!( unit_product_length_conversion(&[(Phx, -2)], &[(Px, -2)]), Some(LengthConversionPowers { rem_to_px_power: 0, px_to_phx_power: -2 }) ); assert_eq!( unit_product_length_conversion(&[(Px, 1), (Phx, -2)], &[(Phx, -1)]), Some(LengthConversionPowers { rem_to_px_power: 0, px_to_phx_power: -1 }) ); assert_eq!( unit_product_length_conversion( &[(Deg, 3), (Phx, 2), (Ms, -1)], &[(Phx, 4), (Deg, 3), (Ms, -1), (Px, -2)] ), Some(LengthConversionPowers { rem_to_px_power: 0, px_to_phx_power: -2 }) ); assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, -1)]), None); assert_eq!(unit_product_length_conversion(&[(Deg, 1), (Phx, -2)], &[(Px, -2)]), None); assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, -1)]), None); assert_eq!( unit_product_length_conversion(&[(Rem, 1)], &[(Px, 1)]), Some(LengthConversionPowers { rem_to_px_power: -1, px_to_phx_power: 0 }) ); assert_eq!( unit_product_length_conversion(&[(Rem, 1)], &[(Phx, 1)]), Some(LengthConversionPowers { rem_to_px_power: -1, px_to_phx_power: -1 }) ); assert_eq!( unit_product_length_conversion(&[(Rem, 2)], &[(Phx, 2)]), Some(LengthConversionPowers { rem_to_px_power: -2, px_to_phx_power: -2 }) ); }