Skip to content

Commit

Permalink
node: better ergonomics for structs and enums (#6500)
Browse files Browse the repository at this point in the history
  • Loading branch information
FloVanGH authored Oct 10, 2024
1 parent 9138105 commit 499a522
Show file tree
Hide file tree
Showing 6 changed files with 469 additions and 220 deletions.
69 changes: 69 additions & 0 deletions api/node/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
```
64 changes: 64 additions & 0 deletions api/node/__test__/api.spec.mts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
12 changes: 12 additions & 0 deletions api/node/__test__/resources/test-enum.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// 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;
}
12 changes: 12 additions & 0 deletions api/node/__test__/resources/test-struct.slint
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// 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;
}
71 changes: 70 additions & 1 deletion api/node/rust/interpreter/component_compiler.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// Copyright © SixtyFPS GmbH <[email protected]>
// 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;

Expand All @@ -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>,
}

Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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()
}
Expand All @@ -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()
}
}
Loading

0 comments on commit 499a522

Please sign in to comment.