Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions api/node/__test__/js_value_conversion.spec.mts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ import {
const filename = fileURLToPath(import.meta.url).replace("build", "__test__");
const dirname = path.dirname(filename);

function ctrlPlusShortcutText() {
return process.platform === "darwin" ? "⌘+" : "Ctrl++";
}

function createNonNullInstance(definition: {
App?: { create(): private_api.ComponentInstance | null };
}): private_api.ComponentInstance {
Expand Down Expand Up @@ -147,6 +151,39 @@ test("get/set bool properties", () => {
}
});

test("get/set keys properties", () => {
const compiler = new private_api.ComponentCompiler();
const definition = compiler.buildFromSource(
`export component App {
in-out property <keys> shortcut: @keys(Control + Plus);
out property <string> shortcut-text: shortcut.to-string();
}`,
"",
);
const instance = createNonNullInstance(definition);

expect(instance.getProperty("shortcut-text")).toBe(ctrlPlusShortcutText());

instance.setProperty("shortcut", "Escape");
expect(instance.getProperty("shortcut-text")).toBe("Escape");

instance.setProperty("shortcut", {
key: "Plus",
control: true,
ignoreShift: true,
});
expect(instance.getProperty("shortcut-text")).toBe(ctrlPlusShortcutText());

let thrownError: any;
try {
instance.setProperty("shortcut", "");
} catch (error) {
thrownError = error;
}
expect(thrownError).toBeDefined();
expect(thrownError.message).toContain("must not be empty");
});

test("set struct properties", () => {
const compiler = new private_api.ComponentCompiler();
const definition = compiler.buildFromSource(
Expand Down
46 changes: 40 additions & 6 deletions api/node/rust/interpreter/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ use crate::{
};
use i_slint_compiler::langtype::Type;
use i_slint_core::graphics::{Image, Rgba8Pixel, SharedPixelBuffer};
use i_slint_core::input::{KeyboardModifiers, Keys};
use i_slint_core::model::{ModelRc, SharedVectorModel};
use i_slint_core::{Brush, Color, SharedVector};
use napi::bindgen_prelude::*;
Expand Down Expand Up @@ -314,12 +315,7 @@ pub fn to_value(env: &Env, unknown: Unknown<'_>, typ: &Type) -> Result<Value> {

Ok(Value::EnumerationValue(e.name.to_string(), value.to_string()))
}
Type::Keys => {
let obj = unknown.coerce_to_object()?;
let keys_instance: ClassInstance<SlintKeys> =
ClassInstance::from_unknown(obj.into_unknown(env)?)?;
Ok(Value::Keys(keys_instance.inner.clone()))
}
Type::Keys => Ok(Value::Keys(js_into_keys(unknown)?)),
Type::Invalid
| Type::Model
| Type::Void
Expand All @@ -337,6 +333,44 @@ pub fn to_value(env: &Env, unknown: Unknown<'_>, typ: &Type) -> Result<Value> {
}
}

fn js_into_keys(unknown: Unknown<'_>) -> Result<Keys> {
match unknown.get_type()? {
ValueType::String => Keys::from_key_name_or_string(&expect_string(unknown)?)
.map_err(|err| napi::Error::from_reason(err.to_string())),
ValueType::Object => {
if let Ok(keys_instance) = ClassInstance::<SlintKeys>::from_unknown(unknown) {
return Ok(keys_instance.inner.clone());
}

let obj = unknown.coerce_to_object()?;
let key = obj
.get::<String>("key")?
.ok_or_else(|| napi::Error::from_reason("Property key is missing"))?;
let mut keys = Keys::from_key_name_or_string(&key)
.map_err(|err| napi::Error::from_reason(err.to_string()))?;
let mut modifiers = KeyboardModifiers::default();
modifiers.alt = obj.get::<bool>("alt")?.unwrap_or(false);
modifiers.control = obj.get::<bool>("control")?.unwrap_or(false);
modifiers.shift = obj.get::<bool>("shift")?.unwrap_or(false);
modifiers.meta = obj.get::<bool>("meta")?.unwrap_or(false);
keys = keys.with_modifiers(modifiers);

if obj.get::<bool>("ignoreShift")?.unwrap_or(false) {
keys = keys.ignoring_shift();
}
if obj.get::<bool>("ignoreAlt")?.unwrap_or(false) {
keys = keys.ignoring_alt();
}

Ok(keys)
}
vt => Err(napi::Error::new(
napi::Status::InvalidArg,
format!("expect String or Object, got: {vt:?}"),
)),
}
}

fn string_to_brush(js_string: napi::JsString<'_>) -> Result<Value> {
let string = js_string.into_utf8()?.as_str()?.to_string();

Expand Down
34 changes: 30 additions & 4 deletions api/python/slint/interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -412,8 +412,20 @@ impl ComponentInstance {
}

fn set_property(&self, name: &str, value: Bound<'_, PyAny>) -> PyResult<()> {
let pv =
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
let normalized_name = normalize_identifier(name);
let ty = self
.instance
.definition()
.properties_and_callbacks()
.find_map(|(prop_name, (ty, _))| {
(normalize_identifier(&prop_name) == normalized_name).then_some(ty)
})
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("no such property"))?;
let pv = TypeCollection::slint_value_from_py_value_bound_for_type(
&value,
&ty,
Some(&self.type_collection),
)?;
Ok(self.instance.set_property(name, pv).map_err(|e| PySetPropertyError(e))?)
}

Expand All @@ -433,8 +445,22 @@ impl ComponentInstance {
prop_name: &str,
value: Bound<'_, PyAny>,
) -> PyResult<()> {
let pv =
TypeCollection::slint_value_from_py_value_bound(&value, Some(&self.type_collection))?;
let normalized_prop_name = normalize_identifier(prop_name);
let ty = self
.instance
.definition()
.global_properties_and_callbacks(global_name)
.and_then(|mut props| {
props.find_map(|(name, (ty, _))| {
(normalize_identifier(&name) == normalized_prop_name).then_some(ty)
})
})
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("no such property"))?;
let pv = TypeCollection::slint_value_from_py_value_bound_for_type(
&value,
&ty,
Some(&self.type_collection),
)?;
Ok(self
.instance
.set_global_property(global_name, prop_name, pv)
Expand Down
4 changes: 2 additions & 2 deletions api/python/slint/slint/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from collections.abc import Iterable
from abc import abstractmethod
import typing
from typing import Any, cast, Iterator
from typing import Any, Self, cast, Iterator


class Model[T](native.PyModelBase, Iterable[T]):
Expand All @@ -15,7 +15,7 @@ class Model[T](native.PyModelBase, Iterable[T]):

Models are iterable and can be used in for loops."""

def __new__(cls, *args: Any) -> "Model[T]":
def __new__(cls, *args: Any) -> Self:
return super().__new__(cls)

def __init__(self) -> None:
Expand Down
15 changes: 15 additions & 0 deletions api/python/slint/tests/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,14 @@
from slint import slint as native
from slint.slint import Image, Color, Brush
import os
import sys
from pathlib import Path


def ctrl_plus_shortcut_text() -> str:
return "⌘+" if sys.platform == "darwin" else "Ctrl++"


def test_property_access() -> None:
compiler = native.Compiler()

Expand All @@ -29,6 +34,7 @@ def test_property_access() -> None:
in property <int> intprop: 42;
in property <float> floatprop: 100;
in property <bool> boolprop: true;
in property <keys> keysprop: @keys(Control + Plus);
in property <image> imgprop;
in property <brush> brushprop: Colors.rgb(255, 0, 255);
in property <color> colprop: Colors.rgb(0, 255, 0);
Expand All @@ -38,6 +44,7 @@ def test_property_access() -> None:
finished: true,
dash-prop: true,
};
out property <string> keysproptext: keysprop.to-string();
in property <image> imageprop: @image-url("../../../../demos/printerdemo/ui/images/cat.jpg");

callback test-callback();
Expand Down Expand Up @@ -77,6 +84,14 @@ def test_property_access() -> None:
with pytest.raises(ValueError, match="wrong type"):
instance.set_property("boolprop", 0)

assert instance.get_property("keysproptext") == ctrl_plus_shortcut_text()
instance.set_property("keysprop", "Escape")
assert instance.get_property("keysproptext") == "Escape"
instance.set_property(
"keysprop", {"key": "Plus", "control": True, "ignore_shift": True}
)
assert instance.get_property("keysproptext") == ctrl_plus_shortcut_text()

structval = instance.get_property("structprop")
assert isinstance(structval, native.PyStruct)
assert structval.title == "builtin"
Expand Down
64 changes: 64 additions & 0 deletions api/python/slint/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::rc::Rc;

use i_slint_compiler::langtype::Type;

use i_slint_core::input::{KeyboardModifiers, Keys};
use i_slint_core::model::{Model, ModelRc};

use crate::keys::PyKeys;
Expand Down Expand Up @@ -392,4 +393,67 @@ impl TypeCollection {
}
model
}

pub fn slint_value_from_py_value_bound_for_type(
ob: &Bound<'_, PyAny>,
expected_type: &Type,
type_collection: Option<&Self>,
) -> PyResult<slint_interpreter::Value> {
if let Type::Keys = expected_type {
return py_value_to_keys(ob).map(slint_interpreter::Value::Keys);
}

Self::slint_value_from_py_value_bound(ob, type_collection)
}
}

fn py_value_to_keys(ob: &Bound<'_, PyAny>) -> PyResult<Keys> {
if let Ok(keys) = ob.extract::<PyRef<'_, PyKeys>>() {
return Ok(keys.keys.clone());
}

if let Ok(key) = ob.extract::<&str>() {
return Keys::from_key_name_or_string(key)
.map_err(|err| pyo3::exceptions::PyValueError::new_err(err.to_string()));
}

let dict = ob.cast::<PyDict>().map_err(|_| {
pyo3::exceptions::PyTypeError::new_err("Keys values must be strings or dicts")
})?;

let key = dict
.get_item("key")?
.ok_or_else(|| pyo3::exceptions::PyValueError::new_err("Keys dict requires a key field"))?
.extract::<String>()?;
let mut keys = Keys::from_key_name_or_string(&key)
.map_err(|err| pyo3::exceptions::PyValueError::new_err(err.to_string()))?;

let mut modifiers = KeyboardModifiers::default();
if let Some(value) = dict.get_item("alt")? {
modifiers.alt = value.extract::<bool>()?;
}
if let Some(value) = dict.get_item("control")? {
modifiers.control = value.extract::<bool>()?;
}
if let Some(value) = dict.get_item("shift")? {
modifiers.shift = value.extract::<bool>()?;
}
if let Some(value) = dict.get_item("meta")? {
modifiers.meta = value.extract::<bool>()?;
}

keys = keys.with_modifiers(modifiers);

if let Some(value) = dict.get_item("ignore_shift")?
&& value.extract::<bool>()?
{
keys = keys.ignoring_shift();
}
if let Some(value) = dict.get_item("ignore_alt")?
&& value.extract::<bool>()?
{
keys = keys.ignoring_alt();
}

Ok(keys)
}
1 change: 1 addition & 0 deletions api/rs/slint/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ pub use i_slint_core::api::*;
pub use i_slint_core::component_factory::ComponentFactory;
#[cfg(not(target_arch = "wasm32"))]
pub use i_slint_core::graphics::{BorrowedOpenGLTextureBuilder, BorrowedOpenGLTextureOrigin};
pub use i_slint_core::input::{KeyboardModifiers, Keys, KeysError};
pub use i_slint_core::items::{StandardListViewItem, TableColumn};
pub use i_slint_core::model::{
FilterModel, MapModel, Model, ModelExt, ModelNotify, ModelPeer, ModelRc, ModelTracker,
Expand Down
Loading
Loading