Skip to content

Commit

Permalink
Python: Initial support for implementing models in Python
Browse files Browse the repository at this point in the history
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
  • Loading branch information
tronical committed Mar 5, 2024
1 parent 82d784a commit 2f313f8
Show file tree
Hide file tree
Showing 6 changed files with 393 additions and 1 deletion.
2 changes: 2 additions & 0 deletions api/python/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod interpreter;
use interpreter::{ComponentCompiler, PyDiagnostic, PyDiagnosticLevel, PyValueType};
mod brush;
mod errors;
mod models;
mod timer;
mod value;

Expand Down Expand Up @@ -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)?)?;

Expand Down
204 changes: 204 additions & 0 deletions api/python/models.rs
Original file line number Diff line number Diff line change
@@ -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())
}
}
1 change: 1 addition & 0 deletions api/python/slint/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ def load_file(path):
Image = native.PyImage
Color = native.PyColor
Brush = native.PyBrush
Model = native.PyModelBase
71 changes: 71 additions & 0 deletions api/python/slint/models.py
Original file line number Diff line number Diff line change
@@ -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)
Loading

0 comments on commit 2f313f8

Please sign in to comment.