slint/api/node/rust/interpreter/value.rs
Milian Wolff 69c68b22b2 Also wrap langtype::Type::Struct in an Rc
This makes copying such types much cheaper and will allow us to
intern common struct types in the future too. This further
drops the sample cost for langtype.rs from ~6.6% down to 4.0%.

We are now also able to share/intern common struct types.

Before:
```
  Time (mean ± σ):      1.073 s ±  0.021 s    [User: 0.759 s, System: 0.215 s]
  Range (min … max):    1.034 s …  1.105 s    10 runs

        allocations:            3074261
```

After:
```
  Time (mean ± σ):      1.034 s ±  0.026 s    [User: 0.733 s, System: 0.201 s]
  Range (min … max):    1.000 s …  1.078 s    10 runs

        allocations:            2917476
```
2024-10-28 09:39:54 +01:00

317 lines
13 KiB
Rust

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
use crate::{
js_into_rust_model, rust_into_js_model, ReadOnlyRustModel, RgbaColor, SlintBrush,
SlintImageData,
};
use i_slint_compiler::langtype::Type;
use i_slint_core::graphics::{Image, Rgba8Pixel, SharedPixelBuffer};
use i_slint_core::model::{ModelRc, SharedVectorModel};
use i_slint_core::{Brush, Color, SharedVector};
use napi::bindgen_prelude::*;
use napi::{Env, JsBoolean, JsNumber, JsObject, JsString, JsUnknown, Result};
use napi_derive::napi;
use slint_interpreter::Value;
use smol_str::SmolStr;
#[napi(js_name = "ValueType")]
pub enum JsValueType {
Void,
Number,
String,
Bool,
Model,
Struct,
Brush,
Image,
}
impl From<slint_interpreter::ValueType> for JsValueType {
fn from(value_type: slint_interpreter::ValueType) -> Self {
match value_type {
slint_interpreter::ValueType::Number => JsValueType::Number,
slint_interpreter::ValueType::String => JsValueType::String,
slint_interpreter::ValueType::Bool => JsValueType::Bool,
slint_interpreter::ValueType::Model => JsValueType::Model,
slint_interpreter::ValueType::Struct => JsValueType::Struct,
slint_interpreter::ValueType::Brush => JsValueType::Brush,
slint_interpreter::ValueType::Image => JsValueType::Image,
_ => JsValueType::Void,
}
}
}
#[napi(js_name = "Property")]
pub struct JsProperty {
pub name: String,
pub value_type: JsValueType,
}
pub fn to_js_unknown(env: &Env, value: &Value) -> Result<JsUnknown> {
match value {
Value::Void => env.get_null().map(|v| v.into_unknown()),
Value::Number(number) => env.create_double(*number).map(|v| v.into_unknown()),
Value::String(string) => env.create_string(string).map(|v| v.into_unknown()),
Value::Bool(value) => env.get_boolean(*value).map(|v| v.into_unknown()),
Value::Image(image) => Ok(SlintImageData::from(image.clone())
.into_instance(*env)?
.as_object(*env)
.into_unknown()),
Value::Struct(struct_value) => {
let mut o = env.create_object()?;
for (field_name, field_value) in struct_value.iter() {
o.set_property(
env.create_string(&field_name.replace('-', "_"))?,
to_js_unknown(env, field_value)?,
)?;
}
Ok(o.into_unknown())
}
Value::Brush(brush) => {
Ok(SlintBrush::from(brush.clone()).into_instance(*env)?.as_object(*env).into_unknown())
}
Value::Model(model) => {
if let Some(maybe_js_model) = rust_into_js_model(model) {
maybe_js_model
} else {
let model_wrapper: ReadOnlyRustModel = model.clone().into();
model_wrapper.into_js(env)
}
}
Value::EnumerationValue(_, value) => env.create_string(value).map(|v| v.into_unknown()),
_ => env.get_undefined().map(|v| v.into_unknown()),
}
}
pub fn to_value(env: &Env, unknown: JsUnknown, typ: &Type) -> Result<Value> {
match typ {
Type::Float32
| Type::Int32
| Type::Duration
| Type::Angle
| Type::PhysicalLength
| Type::LogicalLength
| Type::Rem
| Type::Percent
| Type::UnitProduct(_) => {
let js_number: Result<JsNumber> = unknown.try_into();
Ok(Value::Number(js_number?.get_double()?))
}
Type::String => {
let js_string: JsString = unknown.try_into()?;
Ok(Value::String(js_string.into_utf8()?.as_str()?.into()))
}
Type::Bool => {
let js_bool: JsBoolean = unknown.try_into()?;
Ok(Value::Bool(js_bool.get_value()?))
}
Type::Color => {
match unknown.get_type() {
Ok(ValueType::String) => {
return Ok(unknown.coerce_to_string().and_then(|str| string_to_brush(str))?);
}
Ok(ValueType::Object) => {
if let Ok(rgb_color) = unknown.coerce_to_object() {
return brush_from_color(rgb_color);
}
}
_ => {}
}
Err(napi::Error::from_reason(
"Cannot convert object to brush, because the given object is neither a brush, color, nor a string".to_string()
))
}
Type::Brush => {
match unknown.get_type() {
Ok(ValueType::String) => {
return Ok(unknown.coerce_to_string().and_then(|str| string_to_brush(str))?);
}
Ok(ValueType::Object) => {
if let Ok(obj) = unknown.coerce_to_object() {
// this is used to make the color property of the `Brush` interface optional.
let properties = obj.get_property_names()?;
if properties.get_array_length()? == 0 {
return Ok(Value::Brush(Brush::default()));
}
if let Some(color) = obj.get::<&str, RgbaColor>("color").ok().flatten() {
if color.red() < 0.
|| color.green() < 0.
|| color.blue() < 0.
|| color.alpha() < 0.
{
return Err(Error::from_reason(
"A channel of Color cannot be negative",
));
}
return Ok(Value::Brush(Brush::SolidColor(Color::from_argb_u8(
color.alpha() as u8,
color.red() as u8,
color.green() as u8,
color.blue() as u8,
))));
} else {
return brush_from_color(obj);
}
}
}
_ => {}
}
Err(napi::Error::from_reason(
"Cannot convert object to brush, because the given object is neither a brush, color, nor a string".to_string()
))
}
Type::Image => {
let object = unknown.coerce_to_object()?;
if let Some(direct_image) = object.get("image").ok().flatten() {
Ok(Value::Image(env.get_value_external::<Image>(&direct_image)?.clone()))
} else {
let get_size_prop = |name| {
object
.get::<_, JsUnknown>(name)
.ok()
.flatten()
.and_then(|prop| prop.coerce_to_number().ok())
.and_then(|number| number.get_int64().ok())
.and_then(|i64_num| i64_num.try_into().ok())
.ok_or_else(
|| napi::Error::from_reason(
format!("Cannot convert object to image, because the provided object does not have an u32 `{name}` property")
))
};
fn try_convert_image<BufferType: AsRef<[u8]> + FromNapiValue>(
object: &JsObject,
width: u32,
height: u32,
) -> Result<SharedPixelBuffer<Rgba8Pixel>> {
let buffer =
object.get::<_, BufferType>("data").ok().flatten().ok_or_else(|| {
napi::Error::from_reason(
"data property does not have suitable array buffer type"
.to_string(),
)
})?;
const BPP: usize = core::mem::size_of::<Rgba8Pixel>();
let actual_size = buffer.as_ref().len();
let expected_size: usize = (width as usize) * (height as usize) * BPP;
if actual_size != expected_size {
return Err(napi::Error::from_reason(format!(
"data property does not have the correct size; expected {} (width) * {} (height) * {} = {}; got {}",
width, height, BPP, actual_size, expected_size
)));
}
Ok(SharedPixelBuffer::clone_from_slice(buffer.as_ref(), width, height))
}
let width: u32 = get_size_prop("width")?;
let height: u32 = get_size_prop("height")?;
let pixel_buffer =
try_convert_image::<Uint8ClampedArray>(&object, width, height)
.or_else(|_| try_convert_image::<Buffer>(&object, width, height))?;
Ok(Value::Image(Image::from_rgba8(pixel_buffer)))
}
}
Type::Struct(s) => {
let js_object = unknown.coerce_to_object()?;
Ok(Value::Struct(
s.fields
.iter()
.map(|(pro_name, pro_ty)| {
let prop: JsUnknown = js_object
.get_property(env.create_string(&pro_name.replace('-', "_"))?)?;
let prop_value = if prop.get_type()? == napi::ValueType::Undefined {
slint_interpreter::default_value_for_type(pro_ty)
} else {
to_value(env, prop, pro_ty)?
};
Ok((pro_name.to_string(), prop_value))
})
.collect::<Result<_, _>>()?,
))
}
Type::Array(a) => {
if unknown.is_array()? {
let array = Array::from_unknown(unknown)?;
let mut vec = vec![];
for i in 0..array.len() {
vec.push(to_value(
env,
array.get(i)?.ok_or(napi::Error::from_reason(format!(
"Cannot access array element at index {}",
i
)))?,
a,
)?);
}
Ok(Value::Model(ModelRc::new(SharedVectorModel::from(SharedVector::from_slice(
&vec,
)))))
} else {
let rust_model =
unknown.coerce_to_object().and_then(|obj| js_into_rust_model(env, &obj, &a))?;
Ok(Value::Model(rust_model))
}
}
Type::Enumeration(e) => {
let js_string: JsString = unknown.try_into()?;
let value: SmolStr = js_string.into_utf8()?.as_str()?.into();
if !e.values.contains(&value) {
return Err(napi::Error::from_reason(format!(
"{value} is not a value of enum {}",
e.name
)));
}
Ok(Value::EnumerationValue(e.name.to_string(), value.to_string()))
}
Type::Invalid
| Type::Model
| Type::Void
| Type::InferredProperty
| Type::InferredCallback
| Type::Function { .. }
| Type::Callback { .. }
| Type::ComponentFactory { .. }
| Type::Easing
| Type::PathData
| Type::LayoutCache
| Type::ElementReference => Err(napi::Error::from_reason("reason")),
}
}
fn string_to_brush(js_string: JsString) -> Result<Value> {
let string = js_string.into_utf8()?.as_str()?.to_string();
let c = string
.parse::<css_color_parser2::Color>()
.map_err(|_| napi::Error::from_reason(format!("Could not convert {string} to Brush.")))?;
Ok(Value::Brush(Brush::from(Color::from_argb_u8((c.a * 255.) as u8, c.r, c.g, c.b)).into()))
}
fn brush_from_color(rgb_color: Object) -> Result<Value> {
let red: f64 = rgb_color.get("red")?.ok_or(Error::from_reason("Property red is missing"))?;
let green: f64 =
rgb_color.get("green")?.ok_or(Error::from_reason("Property green is missing"))?;
let blue: f64 = rgb_color.get("blue")?.ok_or(Error::from_reason("Property blue is missing"))?;
let alpha: f64 = rgb_color.get("alpha")?.unwrap_or(255.);
if red < 0. || green < 0. || blue < 0. || alpha < 0. {
return Err(Error::from_reason("A channel of Color cannot be negative"));
}
return Ok(Value::Brush(Brush::SolidColor(Color::from_argb_u8(
alpha as u8,
red as u8,
green as u8,
blue as u8,
))));
}