node: better ergonomics for structs and enums (#6500)

This commit is contained in:
FloVanGH 2024-10-10 04:12:32 +00:00 committed by GitHub
parent 9138105d7f
commit 499a522f99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 470 additions and 221 deletions

View file

@ -273,3 +273,72 @@ model.push(4); // this works
// does NOT work, getting the model does not return the right object
// component.model.push(5);
```
### structs
An exported struct can be created either by defing of an object literal or by using the new keyword.
**`my-component.slint`**
```slint
export struct Person {
name: string,
age: int
}
export component MyComponent inherits Window {
in-out property <Person> person;
}
```
**`main.js`**
```js
import * as slint from "slint-ui";
let ui = slint.loadFile("my-component.slint");
let component = new ui.MyComponent();
// object literal
component.person = { name: "Peter", age: 22 };
// new keyword (sets property values to default e.g. '' for string)
component.person = new ui.Person();
// new keyword with parameters
component.person = new ui.Person({ name: "Tim", age: 30 });
```
### enums
A value of an exported enum can be set as string or by usign the value from the exported enum.
**`my-component.slint`**
```slint
export enum Position {
top,
bottom
}
export component MyComponent inherits Window {
in-out property <Position> position;
}
```
**`main.js`**
```js
import * as slint from "slint-ui";
let ui = slint.loadFile("my-component.slint");
let component = new ui.MyComponent();
// set enum value as string
component.position = "top";
// use the value of the enum
component.position = ui.Position.bottom;
```

View file

@ -204,3 +204,67 @@ test("loadSource component instances and modules are sealed", (t) => {
{ instanceOf: TypeError },
);
});
test("loadFile struct", (t) => {
const demo = loadFile(
path.join(dirname, "resources/test-struct.slint"),
) as any;
const test = new demo.Test({
check: new demo.TestStruct(),
});
t.deepEqual(test.check, { text: "", flag: false, value: 0 });
});
test("loadFile struct constructor parameters", (t) => {
const demo = loadFile(
path.join(dirname, "resources/test-struct.slint"),
) as any;
const test = new demo.Test({
check: new demo.TestStruct({ text: "text", flag: true, value: 12 }),
});
t.deepEqual(test.check, { text: "text", flag: true, value: 12 });
test.check = new demo.TestStruct({
text: "hello world",
flag: false,
value: 8,
});
t.deepEqual(test.check, { text: "hello world", flag: false, value: 8 });
});
test("loadFile struct constructor more parameters", (t) => {
const demo = loadFile(
path.join(dirname, "resources/test-struct.slint"),
) as any;
const test = new demo.Test({
check: new demo.TestStruct({
text: "text",
flag: true,
value: 12,
noProp: "hello",
}),
});
t.deepEqual(test.check, { text: "text", flag: true, value: 12 });
});
test("loadFile enum", (t) => {
const demo = loadFile(
path.join(dirname, "resources/test-enum.slint"),
) as any;
const test = new demo.Test({
check: demo.TestEnum.b,
});
t.deepEqual(test.check, "b");
test.check = demo.TestEnum.c;
t.deepEqual(test.check, "c");
});

View file

@ -0,0 +1,12 @@
// 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
export enum TestEnum {
a,
b,
c
}
export component Test {
in-out property <TestEnum> check;
}

View file

@ -0,0 +1,12 @@
// 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
export struct TestStruct {
text: string,
flag: bool,
value: float
}
export component Test {
in-out property <TestStruct> check;
}

View file

@ -1,10 +1,16 @@
// 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::to_js_unknown;
use super::JsComponentDefinition;
use super::JsDiagnostic;
use i_slint_compiler::langtype::Type;
use itertools::Itertools;
use napi::Env;
use napi::JsUnknown;
use slint_interpreter::Compiler;
use slint_interpreter::Value;
use std::collections::HashMap;
use std::path::PathBuf;
@ -13,6 +19,7 @@ use std::path::PathBuf;
#[napi(js_name = "ComponentCompiler")]
pub struct JsComponentCompiler {
internal: Compiler,
structs_and_enums: Vec<Type>,
diagnostics: Vec<slint_interpreter::Diagnostic>,
}
@ -44,7 +51,7 @@ impl JsComponentCompiler {
compiler.set_include_paths(include_paths);
compiler.set_library_paths(library_paths);
Self { internal: compiler, diagnostics: vec![] }
Self { internal: compiler, diagnostics: vec![], structs_and_enums: vec![] }
}
#[napi(setter)]
@ -99,12 +106,72 @@ impl JsComponentCompiler {
self.diagnostics.iter().map(|d| JsDiagnostic::from(d.clone())).collect()
}
#[napi(getter)]
pub fn structs(&self, env: Env) -> HashMap<String, JsUnknown> {
fn convert_type(env: &Env, ty: &Type) -> Option<(String, JsUnknown)> {
match ty {
Type::Struct { fields, name: Some(name), node: Some(_), .. } => {
let struct_instance = to_js_unknown(
env,
&Value::Struct(slint_interpreter::Struct::from_iter(fields.iter().map(
|(name, field_type)| {
(
name.to_string(),
slint_interpreter::default_value_for_type(field_type),
)
},
))),
);
return Some((name.to_string(), struct_instance.ok()?));
}
_ => return None,
}
}
self.structs_and_enums
.iter()
.filter_map(|ty| convert_type(&env, ty))
.into_iter()
.collect::<HashMap<String, JsUnknown>>()
}
#[napi(getter)]
pub fn enums(&self, env: Env) -> HashMap<String, JsUnknown> {
fn convert_type(env: &Env, ty: &Type) -> Option<(String, JsUnknown)> {
match ty {
Type::Enumeration(en) => {
let mut o = env.create_object().ok()?;
for value in en.values.iter() {
let value = value.replace('-', "_");
o.set_property(
env.create_string(&value).ok()?,
env.create_string(&value).ok()?.into_unknown(),
)
.ok()?;
}
return Some((en.name.clone(), o.into_unknown()));
}
_ => return None,
}
}
self.structs_and_enums
.iter()
.filter_map(|ty| convert_type(&env, ty))
.into_iter()
.collect::<HashMap<String, JsUnknown>>()
}
/// Compile a .slint file into a ComponentDefinition
///
/// Returns the compiled `ComponentDefinition` if there were no errors.
#[napi]
pub fn build_from_path(&mut self, path: String) -> HashMap<String, JsComponentDefinition> {
let r = spin_on::spin_on(self.internal.build_from_path(PathBuf::from(path)));
self.structs_and_enums =
r.structs_and_enums(i_slint_core::InternalToken {}).cloned().collect::<Vec<_>>();
self.diagnostics = r.diagnostics().collect();
r.components().map(|c| (c.name().to_owned(), c.into())).collect()
}
@ -118,6 +185,8 @@ impl JsComponentCompiler {
) -> HashMap<String, JsComponentDefinition> {
let r = spin_on::spin_on(self.internal.build_from_source(source_code, PathBuf::from(path)));
self.diagnostics = r.diagnostics().collect();
self.structs_and_enums =
r.structs_and_enums(i_slint_core::InternalToken {}).cloned().collect::<Vec<_>>();
r.components().map(|c| (c.name().to_owned(), c.into())).collect()
}
}

View file

@ -266,6 +266,10 @@ type LoadData =
from: "source";
};
function translateName(key: string): string {
return key.replace(/-/g, "_");
}
function loadSlint(loadData: LoadData): Object {
const { filePath, options } = loadData.fileData;
@ -309,20 +313,55 @@ function loadSlint(loadData: LoadData): Object {
const slint_module = Object.create({});
// generate structs
const structs = compiler.structs;
for (const key in compiler.structs) {
Object.defineProperty(slint_module, translateName(key), {
value: function (properties: any) {
const defaultObject = structs[key];
const newObject = Object.create({});
for (const propertyKey in defaultObject) {
const propertyName = translateName(propertyKey);
const propertyValue =
properties !== undefined &&
Object.hasOwn(properties, propertyName)
? properties[propertyName]
: defaultObject[propertyKey];
Object.defineProperty(newObject, propertyName, {
value: propertyValue,
writable: true,
enumerable: true,
});
}
return Object.seal(newObject);
},
});
}
// generate enums
const enums = compiler.enums;
for (const key in enums) {
Object.defineProperty(slint_module, translateName(key), {
value: Object.seal(enums[key]),
enumerable: true,
});
}
Object.keys(definitions).forEach((key) => {
const definition = definitions[key];
Object.defineProperty(
slint_module,
definition.name.replace(/-/g, "_"),
{
Object.defineProperty(slint_module, translateName(definition.name), {
value: function (properties: any) {
const instance = definition.create();
if (instance == null) {
throw Error(
"Could not create a component handle for" +
filePath,
"Could not create a component handle for" + filePath,
);
}
@ -338,12 +377,10 @@ function loadSlint(loadData: LoadData): Object {
const componentHandle = new Component(instance!);
instance!.definition().properties.forEach((prop) => {
const propName = prop.name.replace(/-/g, "_");
const propName = translateName(prop.name);
if (componentHandle[propName] !== undefined) {
console.warn(
"Duplicated property name " + propName,
);
console.warn("Duplicated property name " + propName);
} else {
Object.defineProperty(componentHandle, propName, {
get() {
@ -358,7 +395,7 @@ function loadSlint(loadData: LoadData): Object {
});
instance!.definition().callbacks.forEach((cb) => {
const callbackName = cb.replace(/-/g, "_");
const callbackName = translateName(cb);
if (componentHandle[callbackName] !== undefined) {
console.warn(
@ -367,7 +404,7 @@ function loadSlint(loadData: LoadData): Object {
} else {
Object.defineProperty(
componentHandle,
cb.replace(/-/g, "_"),
translateName(cb),
{
get() {
return function () {
@ -387,7 +424,7 @@ function loadSlint(loadData: LoadData): Object {
});
instance!.definition().functions.forEach((cb) => {
const functionName = cb.replace(/-/g, "_");
const functionName = translateName(cb);
if (componentHandle[functionName] !== undefined) {
console.warn(
@ -396,7 +433,7 @@ function loadSlint(loadData: LoadData): Object {
} else {
Object.defineProperty(
componentHandle,
cb.replace(/-/g, "_"),
translateName(cb),
{
get() {
return function () {
@ -415,9 +452,7 @@ function loadSlint(loadData: LoadData): Object {
// globals
instance!.definition().globals.forEach((globalName) => {
if (componentHandle[globalName] !== undefined) {
console.warn(
"Duplicated property name " + globalName,
);
console.warn("Duplicated property name " + globalName);
} else {
const globalObject = Object.create({});
@ -425,10 +460,7 @@ function loadSlint(loadData: LoadData): Object {
.definition()
.globalProperties(globalName)
.forEach((prop) => {
const propName = prop.name.replace(
/-/g,
"_",
);
const propName = translateName(prop.name);
if (globalObject[propName] !== undefined) {
console.warn(
@ -465,11 +497,9 @@ function loadSlint(loadData: LoadData): Object {
.definition()
.globalCallbacks(globalName)
.forEach((cb) => {
const callbackName = cb.replace(/-/g, "_");
const callbackName = translateName(cb);
if (
globalObject[callbackName] !== undefined
) {
if (globalObject[callbackName] !== undefined) {
console.warn(
"Duplicated property name " +
cb +
@ -479,16 +509,14 @@ function loadSlint(loadData: LoadData): Object {
} else {
Object.defineProperty(
globalObject,
cb.replace(/-/g, "_"),
translateName(cb),
{
get() {
return function () {
return instance!.invokeGlobal(
globalName,
cb,
Array.from(
arguments,
),
Array.from(arguments),
);
};
},
@ -509,11 +537,9 @@ function loadSlint(loadData: LoadData): Object {
.definition()
.globalFunctions(globalName)
.forEach((cb) => {
const functionName = cb.replace(/-/g, "_");
const functionName = translateName(cb);
if (
globalObject[functionName] !== undefined
) {
if (globalObject[functionName] !== undefined) {
console.warn(
"Duplicated function name " +
cb +
@ -523,16 +549,14 @@ function loadSlint(loadData: LoadData): Object {
} else {
Object.defineProperty(
globalObject,
cb.replace(/-/g, "_"),
translateName(cb),
{
get() {
return function () {
return instance!.invokeGlobal(
globalName,
cb,
Array.from(
arguments,
),
Array.from(arguments),
);
};
},
@ -553,8 +577,7 @@ function loadSlint(loadData: LoadData): Object {
return Object.seal(componentHandle);
},
},
);
});
});
return Object.seal(slint_module);
}