Introduce a rem unit in the type system

This allows specifying font sizes relative to the Window's
default-font-size, similar to CSS rem.
This commit is contained in:
Simon Hausmann 2022-11-03 20:00:55 +01:00 committed by Simon Hausmann
parent 75ccd897ed
commit d8a1f2cf01
22 changed files with 232 additions and 58 deletions

View file

@ -238,6 +238,7 @@ fn to_debug_string(
Type::Duration
| Type::PhysicalLength
| Type::LogicalLength
| Type::Rem
| Type::Angle
| Type::Percent
| Type::UnitProduct(_) => Expression::BinaryExpression {

View file

@ -19,6 +19,7 @@ pub use crate::passes::resolving;
/// A function built into the run-time
pub enum BuiltinFunction {
GetWindowScaleFactor,
GetWindowDefaultFontSize,
AnimationTick,
Debug,
Mod,
@ -81,6 +82,9 @@ impl BuiltinFunction {
return_type: Box::new(Type::UnitProduct(vec![(Unit::Phx, 1), (Unit::Px, -1)])),
args: vec![],
},
BuiltinFunction::GetWindowDefaultFontSize => {
Type::Function { return_type: Box::new(Type::LogicalLength), args: vec![] }
}
BuiltinFunction::AnimationTick => {
Type::Function { return_type: Type::Duration.into(), args: vec![] }
}
@ -171,6 +175,7 @@ impl BuiltinFunction {
fn is_pure(&self) -> bool {
match self {
BuiltinFunction::GetWindowScaleFactor => false,
BuiltinFunction::GetWindowDefaultFontSize => false,
BuiltinFunction::AnimationTick => false,
BuiltinFunction::DarkColorScheme => false,
// Even if it is not pure, we optimize it away anyway
@ -291,6 +296,8 @@ declare_units! {
In = "in" -> LogicalLength * 96,
/// Points
Pt = "pt" -> LogicalLength * 96./72.,
/// Logical pixels multiplied with the window's default-font-size
Rem = "rem" -> Rem,
// durations
@ -977,25 +984,47 @@ impl Expression {
}
(left, right) => match (left.as_unit_product(), right.as_unit_product()) {
(Some(left), Some(right)) => {
if let Some(power) =
if let Some(conversion_powers) =
crate::langtype::unit_product_length_conversion(&left, &right)
{
let op = if power < 0 { '*' } else { '/' };
let mut result = self;
for _ in 0..power.abs() {
result = Expression::BinaryExpression {
lhs: Box::new(result),
rhs: Box::new(Expression::FunctionCall {
function: Box::new(Expression::BuiltinFunctionReference(
BuiltinFunction::GetWindowScaleFactor,
Some(node.to_source_location()),
)),
arguments: vec![],
source_location: Some(node.to_source_location()),
}),
op,
let apply_power = |mut result, power: i8, builtin_fn| {
let op = if power < 0 { '*' } else { '/' };
for _ in 0..power.abs() {
result = Expression::BinaryExpression {
lhs: Box::new(result),
rhs: Box::new(Expression::FunctionCall {
function: Box::new(
Expression::BuiltinFunctionReference(
builtin_fn,
Some(node.to_source_location()),
),
),
arguments: vec![],
source_location: Some(node.to_source_location()),
}),
op,
}
}
result
};
let mut result = self;
if conversion_powers.rem_to_px_power != 0 {
result = apply_power(
result,
conversion_powers.rem_to_px_power,
BuiltinFunction::GetWindowDefaultFontSize,
)
}
if conversion_powers.px_to_phx_power != 0 {
result = apply_power(
result,
conversion_powers.px_to_phx_power,
BuiltinFunction::GetWindowScaleFactor,
)
}
result
} else {
self
@ -1087,6 +1116,7 @@ impl Expression {
Type::Angle => Expression::NumberLiteral(0., Unit::Deg),
Type::PhysicalLength => Expression::NumberLiteral(0., Unit::Phx),
Type::LogicalLength => Expression::NumberLiteral(0., Unit::Px),
Type::Rem => Expression::NumberLiteral(0., Unit::Rem),
Type::Percent => Expression::NumberLiteral(100., Unit::Percent),
Type::Image => Expression::ImageReference {
resource_ref: ImageReference::None,

View file

@ -314,6 +314,7 @@ impl CppType for Type {
Type::Angle => Some("float".to_owned()),
Type::PhysicalLength => Some("float".to_owned()),
Type::LogicalLength => Some("float".to_owned()),
Type::Rem => Some("float".to_owned()),
Type::Percent => Some("float".to_owned()),
Type::Bool => Some("bool".to_owned()),
Type::Struct { name: Some(name), node: Some(_), .. } => Some(ident(name)),
@ -2395,6 +2396,10 @@ fn compile_builtin_function_call(
let window = access_window_field(ctx);
format!("{}.scale_factor()", window)
}
BuiltinFunction::GetWindowDefaultFontSize => {
let window_item_name = ident(&ctx.public_component.item_tree.root.items[0].name);
format!("{}->{}.default_font_size.get()", ctx.generator_state, window_item_name)
}
BuiltinFunction::AnimationTick => "slint::cbindgen_private::slint_animation_tick()".into(),
BuiltinFunction::Debug => {
format!("std::cout << {} << std::endl;", a.join("<<"))

View file

@ -76,6 +76,7 @@ fn rust_primitive_type(ty: &Type) -> Option<proc_macro2::TokenStream> {
Type::Angle => Some(quote!(f32)),
Type::PhysicalLength => Some(quote!(slint::private_unstable_api::re_exports::Coord)),
Type::LogicalLength => Some(quote!(slint::private_unstable_api::re_exports::Coord)),
Type::Rem => Some(quote!(f32)),
Type::Percent => Some(quote!(f32)),
Type::Bool => Some(quote!(bool)),
Type::Image => Some(quote!(slint::private_unstable_api::re_exports::Image)),
@ -1863,6 +1864,7 @@ fn compile_expression(expr: &Expression, ctx: &EvaluationContext) -> TokenStream
| Type::LogicalLength
| Type::Angle
| Type::Percent
| Type::Rem
) =>
{
(Some(quote!(as f64)), Some(quote!(as f64)))
@ -2152,6 +2154,13 @@ fn compile_builtin_function_call(
let window_adapter_tokens = access_window_adapter_field(ctx);
quote!(slint::private_unstable_api::re_exports::WindowInner::from_pub(#window_adapter_tokens.window()).scale_factor())
}
BuiltinFunction::GetWindowDefaultFontSize => {
let window_item_name = ident(&ctx.public_component.item_tree.root.items[0].name);
let root_access = &ctx.generator_state;
let root_component_id = inner_component_id(&ctx.public_component.item_tree.root);
let item_field = access_component_field_offset(&root_component_id, &window_item_name);
quote!((#item_field + slint::private_unstable_api::re_exports::WindowItem::FIELD_OFFSETS.default_font_size).apply_pin(#root_access.as_pin_ref()).get().get())
}
BuiltinFunction::AnimationTick => {
quote!(slint::private_unstable_api::re_exports::animation_tick())
}

View file

@ -41,6 +41,7 @@ pub enum Type {
Duration,
PhysicalLength,
LogicalLength,
Rem,
Angle,
Percent,
Image,
@ -93,6 +94,7 @@ impl core::cmp::PartialEq for Type {
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),
@ -153,6 +155,7 @@ impl Display for Type {
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"),
@ -207,6 +210,7 @@ impl Type {
| Self::Angle
| Self::PhysicalLength
| Self::LogicalLength
| Self::Rem
| Self::Percent
| Self::Image
| Self::Bool
@ -266,6 +270,10 @@ impl Type {
| (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,
@ -291,6 +299,7 @@ impl Type {
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),
@ -764,62 +773,86 @@ impl EnumerationValue {
}
}
#[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<i8> {
let mut it1 = a.iter();
let mut it2 = b.iter();
let (mut v1, mut v2) = (it1.next(), it2.next());
let mut ppx = 0;
let mut lpx = 0;
loop {
match (v1, v2) {
(None, None) => return (ppx == -lpx && ppx != 0).then(|| ppx),
(Some(a), Some(b)) if a == b => (),
(Some((Unit::Phx, a)), Some((Unit::Phx, b))) => ppx += a - b,
(Some((Unit::Px, a)), Some((Unit::Px, b))) => lpx += a - b,
(Some((Unit::Phx, a)), _) => {
ppx += *a;
v1 = it1.next();
continue;
}
(_, Some((Unit::Phx, b))) => {
ppx += -b;
v2 = it2.next();
continue;
}
(Some((Unit::Px, a)), _) => {
lpx += *a;
v1 = it1.next();
continue;
}
(_, Some((Unit::Px, b))) => {
lpx += -b;
v2 = it2.next();
continue;
}
_ => return None,
};
v1 = it1.next();
v2 = it2.next();
pub fn unit_product_length_conversion(
a: &[(Unit, i8)],
b: &[(Unit, i8)],
) -> Option<LengthConversionPowers> {
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(|| result)
}
#[test]
fn unit_product_length_conversion_test() {
use Option::None;
use Unit::*;
assert_eq!(unit_product_length_conversion(&[(Px, 1)], &[(Phx, 1)]), Some(-1));
assert_eq!(unit_product_length_conversion(&[(Phx, -2)], &[(Px, -2)]), Some(-2));
assert_eq!(unit_product_length_conversion(&[(Px, 1), (Phx, -2)], &[(Phx, -1)]), Some(-1));
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(-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 })
);
}

View file

@ -195,6 +195,7 @@ impl Expression {
| Type::Angle
| Type::PhysicalLength
| Type::LogicalLength
| Type::Rem
| Type::UnitProduct(_) => Expression::NumberLiteral(0.),
Type::Percent => Expression::NumberLiteral(1.),
Type::String => Expression::StringLiteral(String::new()),

View file

@ -68,6 +68,7 @@ fn callback_cost(_callback: &crate::llr::PropertyReference, _ctx: &EvaluationCon
fn builtin_function_cost(function: BuiltinFunction) -> isize {
match function {
BuiltinFunction::GetWindowScaleFactor => PROPERTY_ACCESS_COST,
BuiltinFunction::GetWindowDefaultFontSize => PROPERTY_ACCESS_COST,
BuiltinFunction::AnimationTick => PROPERTY_ACCESS_COST,
BuiltinFunction::Debug => isize::MAX,
BuiltinFunction::Mod => 10,

View file

@ -30,6 +30,11 @@ fn check_expression(component: &Rc<Component>, e: &Expression, diag: &mut BuildD
diag.push_error("Cannot convert between logical and physical length in a global component, because the scale factor is not known".into(), loc);
}
}
Expression::BuiltinFunctionReference(BuiltinFunction::GetWindowDefaultFontSize, loc) => {
if component.is_global() {
diag.push_error("Cannot convert between rem and logical length in a global component, because the default font size is not known".into(), loc);
}
}
_ => e.visit(|e| check_expression(component, e, diag)),
}
}

View file

@ -8,8 +8,11 @@ global Glob := {
// ^error{Cannot convert between logical and physical length in a global component, because the scale factor is not known}
property <float> ratio: allowed / 1phx;
// ^error{Cannot convert between logical and physical length in a global component, because the scale factor is not known}
property <float> should_work: 45px / 8px;
property <length> converted_rem: 2rem;
// ^error{Cannot convert between rem and logical length in a global component, because the default font size is not known}
property <float> should_work: 45px / 8px + (4rem / 2rem);
property <length> allowed: 45px * 5;
property <relative-font-size> rem_allowed: 42rem;
}
X := Rectangle {

View file

@ -227,6 +227,7 @@ impl TypeRegister {
register.insert_type(Type::Easing);
register.insert_type(Type::Angle);
register.insert_type(Type::Brush);
register.insert_type(Type::Rem);
BUILTIN_ENUMS.with(|e| e.fill_register(&mut register));