mirror of
https://github.com/slint-ui/slint.git
synced 2025-12-23 09:19:32 +00:00
No point in stuffing u16 into f32 just to reuse an existing type. OTOH ArrayOfU16 can be reused for other things than grid layout organized data, so give it a more generic name than GridLayoutOrganizedData (we should do the same for LayoutCache, at least if we ever reuse the type for an additional purpose)
328 lines
13 KiB
Rust
328 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::{
|
|
ReadOnlyRustModel, RgbaColor, SlintBrush, SlintImageData, js_into_rust_model,
|
|
rust_into_js_model,
|
|
};
|
|
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,
|
|
StyledText,
|
|
}
|
|
|
|
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,
|
|
slint_interpreter::ValueType::StyledText => JsValueType::StyledText,
|
|
_ => 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 unknown.coerce_to_string().and_then(string_to_brush);
|
|
}
|
|
Ok(ValueType::Object) => {
|
|
if let Ok(rgb_color_or_brush) = unknown.coerce_to_object() {
|
|
if let Some(direct_brush) =
|
|
rgb_color_or_brush.get("brush").ok().flatten().and_then(
|
|
|maybe_slintbrush| {
|
|
env.get_value_external::<Brush>(&maybe_slintbrush).ok()
|
|
},
|
|
)
|
|
{
|
|
return Ok(Value::Brush(direct_brush.color().into()));
|
|
}
|
|
return brush_from_color(rgb_color_or_brush);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
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 unknown.coerce_to_string().and_then(string_to_brush);
|
|
}
|
|
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} (width) * {height} (height) * {BPP} = {actual_size}; got {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::new();
|
|
|
|
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::ArrayOfU16
|
|
| Type::ElementReference
|
|
| Type::StyledText => 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))))
|
|
}
|
|
|
|
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"));
|
|
}
|
|
|
|
Ok(Value::Brush(Brush::SolidColor(Color::from_argb_u8(
|
|
alpha as u8,
|
|
red as u8,
|
|
green as u8,
|
|
blue as u8,
|
|
))))
|
|
}
|