mirror of
https://github.com/slint-ui/slint.git
synced 2025-08-04 02:39:28 +00:00
Python: Initial support for implementing models in Python
This provides a Model base class in Python and sub-classes of that can be set as data models in slint. The ListModel is provided as basic sub-class operating on a list() and allowing mutation and notifying the view on the Slint side. Similarly, an array declared in Slint is exposed as an object to Python that looks like a Model. Both support the sequence protocol. Fixes #4135
This commit is contained in:
parent
82d784a4d4
commit
2f313f84ec
6 changed files with 393 additions and 1 deletions
|
@ -6,6 +6,7 @@ mod interpreter;
|
|||
use interpreter::{ComponentCompiler, PyDiagnostic, PyDiagnosticLevel, PyValueType};
|
||||
mod brush;
|
||||
mod errors;
|
||||
mod models;
|
||||
mod timer;
|
||||
mod value;
|
||||
|
||||
|
@ -38,6 +39,7 @@ fn slint(_py: Python<'_>, m: &PyModule) -> PyResult<()> {
|
|||
m.add_class::<timer::PyTimer>()?;
|
||||
m.add_class::<brush::PyColor>()?;
|
||||
m.add_class::<brush::PyBrush>()?;
|
||||
m.add_class::<models::PyModelBase>()?;
|
||||
m.add_function(wrap_pyfunction!(run_event_loop, m)?)?;
|
||||
m.add_function(wrap_pyfunction!(quit_event_loop, m)?)?;
|
||||
|
||||
|
|
204
api/python/models.rs
Normal file
204
api/python/models.rs
Normal file
|
@ -0,0 +1,204 @@
|
|||
// Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||
|
||||
use std::cell::RefCell;
|
||||
use std::rc::Rc;
|
||||
|
||||
use i_slint_core::model::{Model, ModelNotify, ModelRc};
|
||||
|
||||
use pyo3::exceptions::PyIndexError;
|
||||
use pyo3::gc::PyVisit;
|
||||
use pyo3::prelude::*;
|
||||
use pyo3::PyTraverseError;
|
||||
|
||||
use crate::value::PyValue;
|
||||
|
||||
pub struct PyModelShared {
|
||||
notify: ModelNotify,
|
||||
self_ref: RefCell<Option<PyObject>>,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[pyclass(unsendable, weakref, subclass)]
|
||||
pub struct PyModelBase {
|
||||
inner: Rc<PyModelShared>,
|
||||
}
|
||||
|
||||
impl PyModelBase {
|
||||
pub fn as_model(&self) -> ModelRc<slint_interpreter::Value> {
|
||||
self.inner.clone().into()
|
||||
}
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl PyModelBase {
|
||||
#[new]
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
inner: Rc::new(PyModelShared {
|
||||
notify: Default::default(),
|
||||
self_ref: RefCell::new(None),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn init_self(&self, self_ref: PyObject) {
|
||||
*self.inner.self_ref.borrow_mut() = Some(self_ref);
|
||||
}
|
||||
|
||||
fn notify_row_added(&self, index: usize, count: usize) {
|
||||
self.inner.notify.row_added(index, count)
|
||||
}
|
||||
|
||||
fn notify_row_changed(&self, index: usize) {
|
||||
self.inner.notify.row_changed(index)
|
||||
}
|
||||
|
||||
fn notify_row_removed(&self, index: usize, count: usize) {
|
||||
self.inner.notify.row_removed(index, count)
|
||||
}
|
||||
|
||||
fn __traverse__(&self, visit: PyVisit<'_>) -> Result<(), PyTraverseError> {
|
||||
if let Some(this) = self.inner.self_ref.borrow().as_ref() {
|
||||
visit.call(this)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn __clear__(&mut self) {
|
||||
*self.inner.self_ref.borrow_mut() = None;
|
||||
}
|
||||
}
|
||||
|
||||
impl i_slint_core::model::Model for PyModelShared {
|
||||
type Data = slint_interpreter::Value;
|
||||
|
||||
fn row_count(&self) -> usize {
|
||||
Python::with_gil(|py| {
|
||||
let obj = self.self_ref.borrow();
|
||||
let Some(obj) = obj.as_ref() else {
|
||||
eprintln!("Python: Model implementation is lacking self object (in row_count)");
|
||||
return 0;
|
||||
};
|
||||
let result = match obj.call_method0(py, "row_count") {
|
||||
Ok(result) => result,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Python: Model implementation of row_count() threw an exception: {}",
|
||||
err
|
||||
);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
match result.extract::<usize>(py) {
|
||||
Ok(count) => count,
|
||||
Err(err) => {
|
||||
eprintln!("Python: Model implementation of row_count() returned value that cannot be cast to usize: {}", err);
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn row_data(&self, row: usize) -> Option<Self::Data> {
|
||||
Python::with_gil(|py| {
|
||||
let obj = self.self_ref.borrow();
|
||||
let Some(obj) = obj.as_ref() else {
|
||||
eprintln!("Python: Model implementation is lacking self object (in row_data)");
|
||||
return None;
|
||||
};
|
||||
|
||||
let result = match obj.call_method1(py, "row_data", (row,)) {
|
||||
Ok(result) => result,
|
||||
Err(err) if err.is_instance_of::<PyIndexError>(py) => return None,
|
||||
Err(err) => {
|
||||
eprintln!(
|
||||
"Python: Model implementation of row_data() threw an exception: {}",
|
||||
err
|
||||
);
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
match result.extract::<PyValue>(py) {
|
||||
Ok(pv) => Some(pv.0),
|
||||
Err(err) => {
|
||||
eprintln!("Python: Model implementation of row_data() returned value that cannot be converted to Rust: {}", err);
|
||||
None
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn model_tracker(&self) -> &dyn i_slint_core::model::ModelTracker {
|
||||
&self.notify
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn core::any::Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl PyModelShared {
|
||||
pub fn rust_into_js_model(model: &ModelRc<slint_interpreter::Value>) -> Option<PyObject> {
|
||||
model
|
||||
.as_any()
|
||||
.downcast_ref::<PyModelShared>()
|
||||
.and_then(|rust_model| rust_model.self_ref.borrow().clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
pub struct ReadOnlyRustModel(pub ModelRc<slint_interpreter::Value>);
|
||||
|
||||
#[pymethods]
|
||||
impl ReadOnlyRustModel {
|
||||
fn row_count(&self) -> usize {
|
||||
self.0.row_count()
|
||||
}
|
||||
|
||||
fn row_data(&self, row: usize) -> Option<PyValue> {
|
||||
self.0.row_data(row).map(|value| value.into())
|
||||
}
|
||||
|
||||
fn __len__(&self) -> usize {
|
||||
self.row_count()
|
||||
}
|
||||
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> ReadOnlyRustModelIterator {
|
||||
ReadOnlyRustModelIterator { model: slf.0.clone(), row: 0 }
|
||||
}
|
||||
|
||||
fn __getitem__(&self, index: usize) -> Option<PyValue> {
|
||||
self.row_data(index)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&ModelRc<slint_interpreter::Value>> for ReadOnlyRustModel {
|
||||
fn from(model: &ModelRc<slint_interpreter::Value>) -> Self {
|
||||
Self(model.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[pyclass(unsendable)]
|
||||
struct ReadOnlyRustModelIterator {
|
||||
model: ModelRc<slint_interpreter::Value>,
|
||||
row: usize,
|
||||
}
|
||||
|
||||
#[pymethods]
|
||||
impl ReadOnlyRustModelIterator {
|
||||
fn __iter__(slf: PyRef<'_, Self>) -> PyRef<'_, Self> {
|
||||
slf
|
||||
}
|
||||
|
||||
fn __next__(&mut self) -> Option<PyValue> {
|
||||
if self.row >= self.model.row_count() {
|
||||
return None;
|
||||
}
|
||||
let row = self.row;
|
||||
self.row += 1;
|
||||
self.model.row_data(row).map(|value| value.into())
|
||||
}
|
||||
}
|
|
@ -14,3 +14,4 @@ def load_file(path):
|
|||
Image = native.PyImage
|
||||
Color = native.PyColor
|
||||
Brush = native.PyBrush
|
||||
Model = native.PyModelBase
|
||||
|
|
71
api/python/slint/models.py
Normal file
71
api/python/slint/models.py
Normal file
|
@ -0,0 +1,71 @@
|
|||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||
|
||||
from . import slint as native
|
||||
|
||||
|
||||
class Model(native.PyModelBase):
|
||||
def __new__(cls, *args):
|
||||
return super().__new__(cls)
|
||||
|
||||
def __init__(self, lst=None):
|
||||
self.init_self(self)
|
||||
|
||||
def __len__(self):
|
||||
return self.row_count()
|
||||
|
||||
def __getitem__(self, index):
|
||||
return self.row_data(index)
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
self.set_row_data(index, value)
|
||||
|
||||
def __iter__(self):
|
||||
return ModelIterator(self)
|
||||
|
||||
|
||||
class ListModel(Model):
|
||||
def __init__(self, lst=None):
|
||||
super().__init__()
|
||||
self.list = lst or []
|
||||
|
||||
def row_count(self):
|
||||
return len(self.list)
|
||||
|
||||
def row_data(self, row):
|
||||
return self.list[row]
|
||||
|
||||
def set_row_data(self, row, data):
|
||||
self.list[row] = data
|
||||
super().notify_row_changed(row)
|
||||
|
||||
def __delitem__(self, key):
|
||||
if isinstance(key, slice):
|
||||
start, stop, step = key.indices(len(self.list))
|
||||
del self.list[key]
|
||||
count = len(range(start, stop, step))
|
||||
super().notify_row_removed(start, count)
|
||||
else:
|
||||
del self.list[key]
|
||||
super().notify_row_removed(key, 1)
|
||||
|
||||
def append(self, value):
|
||||
index = len(self.list)
|
||||
self.list.append(value)
|
||||
super().notify_row_added(index, 1)
|
||||
|
||||
|
||||
class ModelIterator:
|
||||
def __init__(self, model):
|
||||
self.model = model
|
||||
self.index = 0
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if self.index >= self.model.row_count():
|
||||
raise StopIteration()
|
||||
index = self.index
|
||||
self.index += 1
|
||||
return self.model.row_data(index)
|
103
api/python/tests/test_models.py
Normal file
103
api/python/tests/test_models.py
Normal file
|
@ -0,0 +1,103 @@
|
|||
# Copyright © SixtyFPS GmbH <info@slint.dev>
|
||||
# SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-1.1 OR LicenseRef-Slint-commercial
|
||||
|
||||
from slint import slint as native
|
||||
from slint import models as models
|
||||
|
||||
|
||||
def test_model_notify():
|
||||
compiler = native.ComponentCompiler()
|
||||
|
||||
compdef = compiler.build_from_source("""
|
||||
export component App {
|
||||
width: 300px;
|
||||
height: 300px;
|
||||
|
||||
out property<length> layout-height: layout.height;
|
||||
in-out property<[length]> fixed-height-model;
|
||||
|
||||
VerticalLayout {
|
||||
alignment: start;
|
||||
|
||||
layout := VerticalLayout {
|
||||
for fixed-height in fixed-height-model: Rectangle {
|
||||
background: blue;
|
||||
height: fixed-height;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
""", "")
|
||||
assert compdef != None
|
||||
|
||||
instance = compdef.create()
|
||||
assert instance != None
|
||||
|
||||
model = models.ListModel([100, 0])
|
||||
|
||||
instance.set_property(
|
||||
"fixed-height-model", model)
|
||||
|
||||
assert instance.get_property("layout-height") == 100
|
||||
model.set_row_data(1, 50)
|
||||
assert instance.get_property("layout-height") == 150
|
||||
model.append(75)
|
||||
assert instance.get_property("layout-height") == 225
|
||||
del model[1:]
|
||||
assert instance.get_property("layout-height") == 100
|
||||
|
||||
assert isinstance(instance.get_property(
|
||||
"fixed-height-model"), models.ListModel)
|
||||
|
||||
|
||||
def test_model_from_list():
|
||||
compiler = native.ComponentCompiler()
|
||||
|
||||
compdef = compiler.build_from_source("""
|
||||
export component App {
|
||||
in-out property<[int]> data: [1, 2, 3, 4];
|
||||
}
|
||||
""", "")
|
||||
assert compdef != None
|
||||
|
||||
instance = compdef.create()
|
||||
assert instance != None
|
||||
|
||||
model = instance.get_property("data")
|
||||
assert model.row_count() == 4
|
||||
assert model.row_data(2) == 3
|
||||
|
||||
instance.set_property("data", models.ListModel([0]))
|
||||
instance.set_property("data", model)
|
||||
assert list(instance.get_property("data")) == [1, 2, 3, 4]
|
||||
|
||||
|
||||
def test_python_model_sequence():
|
||||
model = models.ListModel([1, 2, 3, 4, 5])
|
||||
|
||||
assert len(model) == 5
|
||||
assert list(model) == [1, 2, 3, 4, 5]
|
||||
model[0] = 100
|
||||
assert list(model) == [100, 2, 3, 4, 5]
|
||||
assert model[2] == 3
|
||||
|
||||
|
||||
def test_rust_model_sequence():
|
||||
compiler = native.ComponentCompiler()
|
||||
|
||||
compdef = compiler.build_from_source("""
|
||||
export component App {
|
||||
in-out property<[int]> data: [1, 2, 3, 4, 5];
|
||||
}
|
||||
""", "")
|
||||
assert compdef != None
|
||||
|
||||
instance = compdef.create()
|
||||
assert instance != None
|
||||
|
||||
model = instance.get_property("data")
|
||||
|
||||
assert len(model) == 5
|
||||
assert list(model) == [1, 2, 3, 4, 5]
|
||||
assert model[2] == 3
|
|
@ -37,7 +37,10 @@ impl<'a> ToPyObject for PyValueRef<'a> {
|
|||
slint_interpreter::Value::Image(image) => {
|
||||
crate::image::PyImage::from(image).into_py(py)
|
||||
}
|
||||
slint_interpreter::Value::Model(_) => todo!(),
|
||||
slint_interpreter::Value::Model(model) => {
|
||||
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)))
|
||||
|
@ -72,6 +75,14 @@ impl FromPyObject<'_> for PyValue {
|
|||
ob.extract::<PyRef<'_, crate::brush::PyBrush>>()
|
||||
.map(|pybrush| slint_interpreter::Value::Brush(pybrush.brush.clone()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<PyRef<'_, crate::models::PyModelBase>>()
|
||||
.map(|pymodel| slint_interpreter::Value::Model(pymodel.as_model()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<PyRef<'_, crate::models::ReadOnlyRustModel>>()
|
||||
.map(|rustmodel| slint_interpreter::Value::Model(rustmodel.0.clone()))
|
||||
})
|
||||
.or_else(|_| {
|
||||
ob.extract::<&PyDict>().and_then(|dict| {
|
||||
let dict_items: Result<Vec<(String, slint_interpreter::Value)>, PyErr> = dict
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue