Skip to content

Commit

Permalink
Merge branch 'master' of github.com:slint-ui/slint
Browse files Browse the repository at this point in the history
  • Loading branch information
FloVanGH committed Jul 12, 2024
2 parents ca5ac29 + e3aab79 commit 2b129b8
Show file tree
Hide file tree
Showing 9 changed files with 118 additions and 30 deletions.
2 changes: 1 addition & 1 deletion api/python/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ The types used for properties in the Slint Language each translate to specific t
| `physical_length` | `float` | |
| `duration` | `float` | The number of milliseconds |
| `angle` | `float` | The angle in degrees |
| structure | `dict` | Structures are mapped to Python dictionaries where each structure field is an item. |
| structure | `dict`/`Struct` | When reading, structures are mapped to data classes, when writing dicts are also accepted. |
| array | `slint.Model` | |

### Arrays and Models
Expand Down
1 change: 1 addition & 0 deletions api/python/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ fn slint(_py: Python<'_>, m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<brush::PyColor>()?;
m.add_class::<brush::PyBrush>()?;
m.add_class::<models::PyModelBase>()?;
m.add_class::<value::PyStruct>()?;
m.add_function(wrap_pyfunction!(run_event_loop, m)?)?;
m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?;

Expand Down
15 changes: 10 additions & 5 deletions api/python/slint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,15 @@ def call(*args):
for global_name in compdef.globals:
global_class = _build_global_class(compdef, global_name)

def global_getter(self):
wrapper = global_class()
setattr(wrapper, "__instance__", self.__instance__)
return wrapper
properties_and_callbacks[global_name] = property(global_getter)
def mk_global(global_class):
def global_getter(self):
wrapper = global_class()
setattr(wrapper, "__instance__", self.__instance__)
return wrapper

return property(global_getter)

properties_and_callbacks[global_name] = mk_global(global_class)

return type("SlintClassWrapper", (Component,), properties_and_callbacks)

Expand Down Expand Up @@ -277,3 +281,4 @@ def callback(global_name=None, name=None):
Model = models.Model
Timer = native.Timer
TimerMode = native.TimerMode
Struct = native.PyStruct
17 changes: 12 additions & 5 deletions api/python/tests/test_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ def test_property_access():
export struct MyStruct {
title: string,
finished: bool,
dash-prop: bool,
}
export component Test {
Expand All @@ -36,6 +37,7 @@ def test_property_access():
in property <MyStruct> structprop: {
title: "builtin",
finished: true,
dash-prop: true,
};
in property <image> imageprop: @image-url("../../../examples/printerdemo/ui/images/cat.jpg");
Expand Down Expand Up @@ -75,11 +77,16 @@ def test_property_access():
instance.set_property("boolprop", 0)

structval = instance.get_property("structprop")
assert isinstance(structval, dict)
assert structval == {'title': 'builtin', 'finished': True}
instance.set_property("structprop", {'title': 'new', 'finished': False})
assert instance.get_property("structprop") == {
'title': 'new', 'finished': False}
assert isinstance(structval, native.PyStruct)
assert structval.title == "builtin"
assert structval.finished == True
assert structval.dash_prop == True
instance.set_property(
"structprop", {'title': 'new', 'finished': False, 'dash_prop': False})
structval = instance.get_property("structprop")
assert structval.title == "new"
assert structval.finished == False
assert structval.dash_prop == False

imageval = instance.get_property("imageprop")
assert imageval.width == 320
Expand Down
2 changes: 2 additions & 0 deletions api/python/tests/test_load_file.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def test_load_file_wrapper():

assert instance.MyGlobal.minus_one(100) == 99

assert instance.SecondGlobal.second == "second"

del instance


Expand Down
4 changes: 4 additions & 0 deletions api/python/tests/test_load_file.slint
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ export global MyGlobal {
}
}

export global SecondGlobal {
out property <string> second: "second";
}

export component App inherits Window {
in-out property <string> hello: "World";
callback say-hello(string) -> string;
Expand Down
80 changes: 73 additions & 7 deletions api/python/value.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

use pyo3::prelude::*;
use pyo3::types::{IntoPyDict, PyDict};
use pyo3::types::PyDict;

use std::collections::HashMap;

pub struct PyValue(pub slint_interpreter::Value);
struct PyValueRef<'a>(&'a slint_interpreter::Value);
Expand Down Expand Up @@ -41,11 +43,9 @@ impl<'a> ToPyObject for PyValueRef<'a> {
crate::models::PyModelShared::rust_into_js_model(model)
.unwrap_or_else(|| crate::models::ReadOnlyRustModel::from(model).into_py(py))
}
slint_interpreter::Value::Struct(structval) => structval
.iter()
.map(|(name, val)| (name.to_string().into_py(py), PyValueRef(val).into_py(py)))
.into_py_dict_bound(py)
.into_py(py),
slint_interpreter::Value::Struct(structval) => {
PyStruct { data: structval.clone() }.into_py(py)
}
slint_interpreter::Value::Brush(brush) => {
crate::brush::PyBrush::from(brush.clone()).into_py(py)
}
Expand Down Expand Up @@ -90,13 +90,18 @@ impl FromPyObject<'_> for PyValue {
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>()
.map(|rustmodel| slint_interpreter::Value::Model(rustmodel.0.clone()))
})
.or_else(|_| {
ob.extract::<PyRef<'_, PyStruct>>().and_then(|pystruct| {
Ok(slint_interpreter::Value::Struct(pystruct.data.clone()))
})
})
.or_else(|_| {
ob.extract::<&PyDict>().and_then(|dict| {
let dict_items: Result<Vec<(String, slint_interpreter::Value)>, PyErr> = dict
.iter()
.map(|(name, pyval)| {
let name = name.extract::<&str>()?.to_string();
let slintval: PyValue = pyval.extract()?;
let slintval = PyValue::extract(pyval)?;
Ok((name, slintval.0))
})
.collect::<Result<Vec<(_, _)>, PyErr>>();
Expand All @@ -114,3 +119,64 @@ impl From<slint_interpreter::Value> for PyValue {
Self(value)
}
}

#[pyclass(subclass, unsendable)]
#[derive(Clone, Default)]
pub struct PyStruct {
data: slint_interpreter::Struct,
}

#[pymethods]
impl PyStruct {
#[new]
fn new() -> Self {
Default::default()
}

fn __getattr__(&self, key: &str) -> PyResult<PyValue> {
self.data.get_field(key).map_or_else(
|| {
Err(pyo3::exceptions::PyAttributeError::new_err(format!(
"Python: No such field {key} on PyStruct"
)))
},
|value| Ok(value.clone().into()),
)
}
fn __setattr__(&mut self, py: Python<'_>, key: String, value: PyObject) -> PyResult<()> {
let pv: PyValue = value.extract(py)?;
self.data.set_field(key, pv.0);
Ok(())
}

fn __iter__(slf: PyRef<'_, Self>) -> PyStructFieldIterator {
PyStructFieldIterator {
inner: slf
.data
.iter()
.map(|(name, val)| (name.to_string(), val.clone()))
.collect::<HashMap<_, _>>()
.into_iter(),
}
}

fn __copy__(&self) -> Self {
self.clone()
}
}

#[pyclass(unsendable)]
struct PyStructFieldIterator {
inner: std::collections::hash_map::IntoIter<String, slint_interpreter::Value>,
}

#[pymethods]
impl PyStructFieldIterator {
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
slf
}

fn __next__(mut slf: PyRefMut<'_, Self>) -> Option<(String, PyValue)> {
slf.inner.next().map(|(name, val)| (name, PyValue(val)))
}
}
18 changes: 10 additions & 8 deletions examples/memory/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import os
import random
import itertools
import copy
import slint
from slint import Color, ListModel, Timer, TimerMode

Expand All @@ -14,31 +15,32 @@ class MainWindow(slint.loader.memory.MainWindow):
def __init__(self):
super().__init__()
initial_tiles = self.memory_tiles
tiles = ListModel(itertools.chain(initial_tiles, initial_tiles))
tiles = ListModel(itertools.chain(
map(copy.copy, initial_tiles), map(copy.copy, initial_tiles)))
random.shuffle(tiles)
self.memory_tiles = tiles

@slint.callback
def check_if_pair_solved(self):
flipped_tiles = [(index, tile) for index, tile in enumerate(
self.memory_tiles) if tile["image-visible"] and not tile["solved"]]
flipped_tiles = [(index, copy.copy(tile)) for index, tile in enumerate(
self.memory_tiles) if tile.image_visible and not tile.solved]
if len(flipped_tiles) == 2:
tile1_index, tile1 = flipped_tiles[0]
tile2_index, tile2 = flipped_tiles[1]
is_pair_solved = tile1["image"].path == tile2["image"].path
is_pair_solved = tile1.image.path == tile2.image.path
if is_pair_solved:
tile1["solved"] = True
tile1.solved = True
self.memory_tiles[tile1_index] = tile1
tile2["solved"] = True
tile2.solved = True
self.memory_tiles[tile2_index] = tile2
else:
self.disable_tiles = True

def reenable_tiles():
self.disable_tiles = False
tile1["image-visible"] = False
tile1.image_visible = False
self.memory_tiles[tile1_index] = tile1
tile2["image-visible"] = False
tile2.image_visible = False
self.memory_tiles[tile2_index] = tile2

Timer.single_shot(timedelta(seconds=1), reenable_tiles)
Expand Down
9 changes: 5 additions & 4 deletions examples/printerdemo/python/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import slint
from datetime import timedelta, datetime
import os
import copy
import sys
sys.path.append(os.path.join(os.path.dirname(__file__), ".."))

Expand Down Expand Up @@ -48,13 +49,13 @@ def cancel_job(self, index):
def update_jobs(self):
if len(self.printer_queue) <= 0:
return
top_item = self.printer_queue[0]
top_item["progress"] += 1
if top_item["progress"] >= 100:
top_item = copy.copy(self.printer_queue[0])
top_item.progress += 1
if top_item.progress >= 100:
del self.printer_queue[0]
if len(self.printer_queue) == 0:
return
top_item = self.printer_queue[0]
top_item = copy.copy(self.printer_queue[0])
self.printer_queue[0] = top_item


Expand Down

0 comments on commit 2b129b8

Please sign in to comment.